加载与发现
插件加载分三层:
- Loader(
vendor/loader)服务管理 entry 树 + Node 内置 ModuleLoader(--expose-internals)做 ESM/CJS 导入 - Include 插件解析
cordis.patch.yml(含!!js表达式) dsh pluginCLI 把 pnpm 包安装与 bundle layer 对账
Loader 在 internal/plugin/internal/config/internal/update 事件上挂钩,自动处理 fiber.entry 绑定、config 插值、reload 写回。
Loader 服务
vendor/loader/src/index.ts:65-100 的 class Loader extends EntryTree:internal = ModuleLoader.fromInternal()— 拿 Node 内部 ESM loaderbuiltinsdict 给内建 plugin- 构造时注册三个
internal/*钩子:config 插值、update 写回、plugin 生命周期日志
unwrapExports:统一三种 shape
vendor/loader/src/index.ts:192-199 的 unwrapExports(exports) 处理 default/__esModule 双重 interop,让 ESM/CJS/default-export 三种 shape 统一成 plugin 对象。Node 内置 ModuleLoader
dsh 用 --expose-internals 拿到 Node 内部的 ESM loader,这是它能精细控制模块加载(包括 HMR 的缓存清理)的前提。
V1 vs V2
vendor/loader/src/internal.ts:55-102 定义了 ModuleLoaderV1(Node 22/23)与 ModuleLoaderV2(Node 24+)接口,字段差异标注清楚:getModuleJobForImport→getOrCreateModuleJobresolve变 private- 等等
vendor/loader/src/internal.ts:120-132 的 ModuleLoader.fromInternal():Node major ≥24 走 v2,≥22 走 v1,通过 --expose-internals 或 node-addon-require-builtin 拿 internal/modules/esm/loader 的 cascaded loader。dsh plugin CLI:pnpm 对账
是否是 bundle layer
apps/cli/src/plugin.ts:36-45 的 exportsPatch(packageName, profileDir):- 解析 bundle dir
- 读 manifest
- 判定
dsh.bundle.patch是否存在——即"是不是一个 bundle layer"
reconcilePlugins
apps/cli/src/plugin.ts:59-91 的 reconcilePlugins(before, profileDir):pnpm 跑完后对比 before/after 依赖:
- 新依赖且是 bundle → push 进
dsh.profile.bundles - 依赖移除或新版不再声明 bundle → splice 出
路径锚定
apps/cli/src/plugin.ts:104-112 的 anchorPathSpec(argument, cwd):把 ./../file:/link: 相对 spec 重写到调用者 cwd,避免在 profile 目录里自指。patch.yml 的 entry 结构
packages/bundle/base/cordis.patch.yml:15-45 是 base bundle 的 insert 列表,每行:- id: <stable> # 稳定身份
name: '<pkg or path>' # 模块定位
config: ... # 可选
disabled: !!js ... # 可选,唯一被插值的元数据字段关键机制
1. entry id 是稳定身份
Loader entry 的 id 是稳定身份:config 文件改后 loader 按 id diff,只 mount/unmount/reconfigure 变化的行;无 id 的 entry 每次读都生成新 id 被视为"删+加"。
2. disabled: !!js 分流
disabled: !!js <expr> 是唯一被插值的元数据字段,每次 mount 决策时对 loader context 求值——平台/环境差异(如 process.platform === 'win32')在此分流。
3. internal/config 钩子做插值
Loader internal/config 钩子做插值:tree carrier(Group/Include)的 config 保持 literal,叶子 entry 的 config 走 interpolate(ctx, config) 解析 !!js。
4. Loader.Intercept.await
Loader.Intercept.await 让依赖 loader 的插件在 entry tree 还有未完成任务时保持 pending——避免插件在 boot 早期看到不完整的注册表。
5. Profile 是真实 pnpm workspace
dsh plugin add 即 pnpm add,git 依赖的 prepare 脚本被 pnpm ≥10 阻止时 CLI 会提示用户加 allowBuilds。插件管理完全复用 pnpm 的依赖图。