@crazx/dsh-client-modules 0.1.0-rc.7.zw.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +24 -0
- package/README.zh.md +24 -0
- package/lib/client.js +259 -0
- package/lib/index.js +348 -0
- package/lib/invariant.js +34 -0
- package/lib/types/client/index.d.ts +21 -0
- package/lib/types/client/manifest.d.ts +190 -0
- package/lib/types/client/system.d.ts +48 -0
- package/lib/types/index.d.ts +111 -0
- package/lib/types/invariant.d.ts +16 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/client/modules/README.md
|
|
5
|
+
README.md: efaff699839b977cc45f89f3c164402241b90dc2
|
|
6
|
+
README.zh.md: 6ab3c8494d7ea902e8b891008f236aec41301a35
|
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-client-modules
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
Client module system: the browser peer of Node's internal ESM loader, built as a lazy CJS table. The web shell mounts the vendored cordis Loader for entry governance (fiber lifecycle, inject waiting, update/refresh) and injects this package's `ClientModuleLoader` through its `internal` contract — the vendored side's only consumption point is `EntryTree.import`, so replacing `internal` replaces exactly "how plugin code arrives" and nothing else.
|
|
6
|
+
|
|
7
|
+
Lazy CJS model (web2): executing a plugin bundle only REGISTERS its factory (`window.__ModuleLoader__.load({id, factory})`); every module body side effect — CSS injection included — lives in the factory closure and runs at materialization (`factory(require)` → exports, memoized in `loadCache`), not at script execution. A factory that requires another registered-but-unmaterialized module materializes it recursively, so load order needs no external sequencing; require cycles throw (factory-form CJS cannot deliver partial exports). `<id>/client` and the bare id resolve to the same exports (a plugin bundle IS its package's client half).
|
|
8
|
+
|
|
9
|
+
Resolution branch order (`import(specifier)`): platform seed word → shell instance; memoized record → surface; shell-own static registry (`registerStatic`, app-shell) → module; registered factory → materialize; graph row (`window.__DSH_BOOT__`) → load its external classic script + materialize; anything else throws — the runtime mirror of the build-time bundle purity gate. The synchronous `require` handed to factories walks the same order minus the asynchronous load branch and records observed edges into the module record. `prefetch` is the stage-one arrival hook (script load and factory registration only; concurrent calls share one in-flight task); `invalidate` drops the factory and materialized record so the next prefetch/import reloads the script (the HMR hook).
|
|
10
|
+
|
|
11
|
+
The Node half scans enabled Loader entries for web `dsh.client` packages, resolves each `exports["./client"]`, hashes the built bundle into the boot graph, and serves it with its source map under `/plugins`. Source launch maps host imports to TypeScript source but still consumes this built client export; missing files share one build instruction followed by a package/path list, while unrelated filesystem errors remain separate failures.
|
|
12
|
+
|
|
13
|
+
## Model Experience
|
|
14
|
+
|
|
15
|
+
None, as the module loader is browser-side kernel machinery; nothing here reaches a model request.
|
|
16
|
+
|
|
17
|
+
#### KV Cache effect
|
|
18
|
+
|
|
19
|
+
None; this package neither assembles nor sends a provider request.
|
|
20
|
+
|
|
21
|
+
## Known Limitations and Deferred Work
|
|
22
|
+
|
|
23
|
+
- **Flat module graph by design** — every bundle is one module node whose edges point only at table leaves; the interface (`loadCache`/`edges`/`invalidate`) already supports a general module graph, so the externalization granularity can change without an interface change.
|
|
24
|
+
- **No unload bookkeeping of its own** — style removal and fiber teardown ordering live with the HMR driver (`@deepseek-ai/dsh-client-hmr`); the loader only inventories owned style tag ids per record.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-client-modules
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
客户端模块系统:Node 内部 ESM loader 的浏览器端对等实现,以惰性 CJS 表实现。web 外壳挂载 vendored cordis Loader 来治理配置项(fiber 生命周期、inject 等待、update/refresh),并通过其 `internal` 约定注入该包的 `ClientModuleLoader`;vendored 一侧唯一的消费点是 `EntryTree.import`,因此替换 `internal` 恰好只会替换「插件代码如何到达」,不会改变其他内容。
|
|
6
|
+
|
|
7
|
+
惰性 CJS 模型(web2):执行插件 bundle 只会注册其 factory(`window.__ModuleLoader__.load({id, factory})`);每个模块主体的副作用(包括 CSS 注入)都位于 factory 闭包中,在物化时运行(`factory(require)` → 导出表层,并在 `loadCache` 中记忆化),不会在脚本执行时运行。如果 factory 依赖另一个已注册但尚未物化的模块,系统会递归物化它,因此加载顺序无需外部编排;require 循环会抛出异常(factory 形式的 CJS 无法提供部分导出)。`<id>/client` 与裸 id 指向同一表层(一个插件 bundle 就是其包的客户端侧)。
|
|
8
|
+
|
|
9
|
+
解析分支顺序(`import(specifier)`):平台种子词 → 外壳实例;记忆化记录 → 表层;外壳自身的静态注册表(`registerStatic`,app-shell)→ 模块;已注册 factory → 物化;模块图记录(`window.__DSH_BOOT__`)→ 加载外部 classic script + 物化;其他情况一律抛出异常。这是构建时 bundle 纯度门禁的运行时镜像。交给 factory 的同步 `require` 采用相同顺序,但不含异步加载分支,并把观察到的边记录到模块记录中。`prefetch` 是第一阶段到达钩子(只加载脚本并注册 factory;并发调用共享一个进行中的任务);`invalidate` 会丢弃 factory 与物化记录,使下一次 prefetch/import 重新加载脚本;它是 HMR(热模块替换)钩子。
|
|
10
|
+
|
|
11
|
+
Node 侧会扫描已启用的 Loader 配置项以发现 web `dsh.client` 包,解析每个 `exports["./client"]`,把构建后的 bundle 哈希写入启动图,并通过 `/plugins` 提供该文件及其 sourcemap。源码启动会把宿主侧导入映射到 TypeScript 源码,但仍消费这一构建后的客户端导出;缺失文件共享一条构建说明,随后以包/路径列表列出各项,而无关的文件系统错误仍是独立故障。
|
|
12
|
+
|
|
13
|
+
## 模型体验
|
|
14
|
+
|
|
15
|
+
无。模块 loader 属于浏览器侧内核机制;这里没有任何内容进入模型请求。
|
|
16
|
+
|
|
17
|
+
#### KV Cache 影响
|
|
18
|
+
|
|
19
|
+
无;该包既不组装也不发送提供方请求。
|
|
20
|
+
|
|
21
|
+
## 已知限制与暂缓事项
|
|
22
|
+
|
|
23
|
+
- **有意采用扁平模块图**:每个 bundle 是一个模块节点,其边只指向表中的叶节点;接口(`loadCache`/`edges`/`invalidate`)已经支持通用模块图,因此可以改变 externalization 粒度而不更改接口。
|
|
24
|
+
- **自身不维护卸载记录**:样式移除与 fiber 拆卸顺序属于 HMR 驱动器(`@deepseek-ai/dsh-client-hmr`);loader 只在每条记录中登记其拥有的样式标签 id。
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@deepseek-ai/dsh-client-modules",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
//#region lib/types/client/system.js
|
|
8
|
+
/** Default bundle-load hook: same-origin external classic script. */
|
|
9
|
+
const defaultLoadBundle = (url) => new Promise((resolve, reject) => {
|
|
10
|
+
const el = document.createElement("script");
|
|
11
|
+
el.async = true;
|
|
12
|
+
el.src = url;
|
|
13
|
+
el.addEventListener("load", () => {
|
|
14
|
+
el.remove();
|
|
15
|
+
resolve();
|
|
16
|
+
}, { once: true });
|
|
17
|
+
el.addEventListener("error", () => {
|
|
18
|
+
el.remove();
|
|
19
|
+
reject(/* @__PURE__ */ new Error(`client-modules: bundle script ${url} failed to load`));
|
|
20
|
+
}, { once: true });
|
|
21
|
+
document.head.append(el);
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* A plugin bundle IS its package's client half: `<id>/client` (the exports
|
|
25
|
+
* subpath external bundles emit) and the bare graph id name the same
|
|
26
|
+
* exports, so table lookups normalize the suffix away.
|
|
27
|
+
*/
|
|
28
|
+
const stripClientSuffix = (spec) => spec.endsWith("/client") ? spec.slice(0, -7) : spec;
|
|
29
|
+
/**
|
|
30
|
+
* Claim and inventory the <style> tags a factory injected during
|
|
31
|
+
* materialization: preset-emitted tags arrive pre-tagged with data-plugin;
|
|
32
|
+
* any untagged tag is claimed for the materializing plugin (HMR bookkeeping).
|
|
33
|
+
*/
|
|
34
|
+
const claimStyles = (id) => {
|
|
35
|
+
if (typeof document === "undefined") return [];
|
|
36
|
+
for (const el of document.querySelectorAll("style:not([data-plugin])")) el.setAttribute("data-plugin", id);
|
|
37
|
+
const owned = [];
|
|
38
|
+
for (const el of document.querySelectorAll(`style[data-plugin=${JSON.stringify(id)}]`)) owned.push(el.getAttribute("data-plugin-css") ?? id);
|
|
39
|
+
return owned;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* The client module system: state tables plus the arrival/materialization
|
|
43
|
+
* machinery implementing {@link ClientModuleLoader} (whose members carry the
|
|
44
|
+
* contract documentation). Construction indexes the boot rows and installs the
|
|
45
|
+
* `window.__ModuleLoader__` registration sink — once per page.
|
|
46
|
+
*/
|
|
47
|
+
var ClientModuleSystem = class {
|
|
48
|
+
version = "client";
|
|
49
|
+
loadCache = /* @__PURE__ */ new Map();
|
|
50
|
+
seed;
|
|
51
|
+
statics = /* @__PURE__ */ new Map();
|
|
52
|
+
factories = /* @__PURE__ */ new Map();
|
|
53
|
+
/** In-flight prefetch (script load) per id; concurrent callers share it. */
|
|
54
|
+
pendingArrival = /* @__PURE__ */ new Map();
|
|
55
|
+
/** Materialization re-entrancy guard: factory-form CJS cannot deliver partial exports, so a cycle is fatal. */
|
|
56
|
+
materializing = /* @__PURE__ */ new Set();
|
|
57
|
+
graphRows = /* @__PURE__ */ new Map();
|
|
58
|
+
loadBundle;
|
|
59
|
+
/**
|
|
60
|
+
* Build the module system over the parsed boot rows.
|
|
61
|
+
* @param options - Module rows, module-table staticModules, and bundle-load hook.
|
|
62
|
+
*/
|
|
63
|
+
constructor(options) {
|
|
64
|
+
this.seed = new Map(Object.entries(options.staticModules));
|
|
65
|
+
this.loadBundle = options.loadBundle ?? defaultLoadBundle;
|
|
66
|
+
for (const row of options.modules) {
|
|
67
|
+
if (this.graphRows.has(row.id)) throw new Error(`client-modules: duplicate graph entry "${row.id}"`);
|
|
68
|
+
this.graphRows.set(row.id, row);
|
|
69
|
+
}
|
|
70
|
+
const win = globalThis;
|
|
71
|
+
if (win.__ModuleLoader__ !== void 0) throw new Error("client-modules: window.__ModuleLoader__ already installed (double boot?)");
|
|
72
|
+
win.__ModuleLoader__ = { load: (handoff) => {
|
|
73
|
+
if (this.factories.has(handoff.id)) throw new Error(`client-modules: duplicate factory registration for "${handoff.id}" (bundle executed twice without invalidate?)`);
|
|
74
|
+
this.factories.set(handoff.id, handoff.factory);
|
|
75
|
+
} };
|
|
76
|
+
}
|
|
77
|
+
/** Load one graph row so its factory is registered (idempotent per in-flight arrival). */
|
|
78
|
+
arrive(row) {
|
|
79
|
+
const { id, url } = row;
|
|
80
|
+
const pending = this.pendingArrival.get(id);
|
|
81
|
+
if (pending !== void 0) return pending;
|
|
82
|
+
if (this.factories.has(id)) return Promise.resolve();
|
|
83
|
+
const task = this.loadBundle(url).then(() => {
|
|
84
|
+
if (!this.factories.has(id)) throw new Error(`client-modules: bundle ${url} loaded without registering "${id}" via __ModuleLoader__.load`);
|
|
85
|
+
}).finally(() => {
|
|
86
|
+
this.pendingArrival.delete(id);
|
|
87
|
+
});
|
|
88
|
+
this.pendingArrival.set(id, task);
|
|
89
|
+
return task;
|
|
90
|
+
}
|
|
91
|
+
/** Materialize a registered factory (synchronous; memoized in loadCache). */
|
|
92
|
+
materialize(id) {
|
|
93
|
+
const existing = this.loadCache.get(id);
|
|
94
|
+
if (existing !== void 0) return existing;
|
|
95
|
+
const registered = this.factories.get(id);
|
|
96
|
+
/* v8 ignore next -- callers check the factory branch before dispatching here. */
|
|
97
|
+
if (registered === void 0) throw new Error(`client-modules: no registered factory for "${id}"`);
|
|
98
|
+
if (this.materializing.has(id)) throw new Error(`client-modules: require cycle through "${id}" (factory-form CJS cannot deliver partial exports)`);
|
|
99
|
+
this.materializing.add(id);
|
|
100
|
+
try {
|
|
101
|
+
const edges = /* @__PURE__ */ new Set();
|
|
102
|
+
const record = {
|
|
103
|
+
id,
|
|
104
|
+
exports: registered(this.makeRequire(edges)),
|
|
105
|
+
styles: claimStyles(id),
|
|
106
|
+
edges
|
|
107
|
+
};
|
|
108
|
+
this.loadCache.set(id, record);
|
|
109
|
+
return record;
|
|
110
|
+
} finally {
|
|
111
|
+
this.materializing.delete(id);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The synchronous require answered to factories: seed → static → memoized
|
|
116
|
+
* record → registered factory (recursive materialization — this is what
|
|
117
|
+
* makes load order self-resolving). Fetching is async and therefore
|
|
118
|
+
* unreachable from here; an unregistered plugin specifier is loud (and a
|
|
119
|
+
* cross-plugin value import is already a build error upstream).
|
|
120
|
+
*/
|
|
121
|
+
makeRequire(edges) {
|
|
122
|
+
return (spec) => {
|
|
123
|
+
edges.add(spec);
|
|
124
|
+
if (this.seed.has(spec)) return this.seed.get(spec);
|
|
125
|
+
if (this.statics.has(spec)) return this.statics.get(spec);
|
|
126
|
+
const id = stripClientSuffix(spec);
|
|
127
|
+
const record = this.loadCache.get(id);
|
|
128
|
+
if (record !== void 0) return record.exports;
|
|
129
|
+
if (this.factories.has(id)) return this.materialize(id).exports;
|
|
130
|
+
throw new Error(`client-modules: require("${spec}") missed the module table — not a platform seed word, not a shell-own module, and no registered factory (a build-time externals drift, or a forbidden cross-plugin value import)`);
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async import(specifier) {
|
|
134
|
+
if (this.seed.has(specifier)) return this.seed.get(specifier);
|
|
135
|
+
const existing = this.loadCache.get(specifier);
|
|
136
|
+
if (existing !== void 0) return existing.exports;
|
|
137
|
+
if (this.statics.has(specifier)) {
|
|
138
|
+
const exports = this.statics.get(specifier);
|
|
139
|
+
this.loadCache.set(specifier, {
|
|
140
|
+
id: specifier,
|
|
141
|
+
exports,
|
|
142
|
+
styles: [],
|
|
143
|
+
edges: /* @__PURE__ */ new Set()
|
|
144
|
+
});
|
|
145
|
+
return exports;
|
|
146
|
+
}
|
|
147
|
+
if (!this.factories.has(specifier)) {
|
|
148
|
+
const row = this.graphRows.get(specifier);
|
|
149
|
+
if (row === void 0) throw new Error(`client-modules: cannot resolve "${specifier}" — not a seed word, not a shell-own module, and not a row in the boot graph (the runtime mirror of the bundle purity gate)`);
|
|
150
|
+
await this.arrive(row);
|
|
151
|
+
}
|
|
152
|
+
return this.materialize(specifier).exports;
|
|
153
|
+
}
|
|
154
|
+
registerStatic(id, module) {
|
|
155
|
+
if (this.statics.has(id)) throw new Error(`client-modules: shell-own module "${id}" registered twice`);
|
|
156
|
+
this.statics.set(id, module);
|
|
157
|
+
}
|
|
158
|
+
async prefetch(id) {
|
|
159
|
+
if (this.statics.has(id)) return;
|
|
160
|
+
const row = this.graphRows.get(id);
|
|
161
|
+
if (row === void 0) throw new Error(`client-modules: prefetch("${id}") — not a graph entry`);
|
|
162
|
+
await this.arrive(row);
|
|
163
|
+
}
|
|
164
|
+
invalidate(id) {
|
|
165
|
+
this.factories.delete(id);
|
|
166
|
+
this.loadCache.delete(id);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
//#endregion
|
|
170
|
+
//#region lib/types/client/manifest.js
|
|
171
|
+
/**
|
|
172
|
+
* Client module system: the browser peer of Node's internal ESM loader, built
|
|
173
|
+
* as a lazy CJS table. The vendored cordis Loader consumes this object
|
|
174
|
+
* through its `internal` contract (the only call site is `EntryTree.import` →
|
|
175
|
+
* `internal.import`), which keeps entry governance (fiber lifecycle, inject
|
|
176
|
+
* waiting, update/refresh) entirely on the vendored side while this package
|
|
177
|
+
* owns code arrival.
|
|
178
|
+
*
|
|
179
|
+
* Lazy CJS model: executing a plugin bundle only REGISTERS its
|
|
180
|
+
* factory (`window.__ModuleLoader__.load({id, factory})`); every module body
|
|
181
|
+
* side effect — including CSS injection — lives inside the factory closure
|
|
182
|
+
* and runs at materialization, not at script execution. Materialization
|
|
183
|
+
* (factory(require) → exports) happens on first import/require and is
|
|
184
|
+
* memoized in {@link ClientModuleLoader.loadCache}; a factory that requires
|
|
185
|
+
* another registered-but-unmaterialized module materializes it recursively,
|
|
186
|
+
* so load order needs no external sequencing.
|
|
187
|
+
*
|
|
188
|
+
* Resolution branch order (import): seed word → shell instance; memoized
|
|
189
|
+
* record → exports; static registry (shell-own modules, e.g. app-shell) →
|
|
190
|
+
* module; registered factory → materialize; graph row → load + materialize;
|
|
191
|
+
* anything else → throw (loud — the runtime mirror of the
|
|
192
|
+
* build-time bundle purity gate). The synchronous `require` handed to
|
|
193
|
+
* factories walks the same order minus the load branch: loading is async,
|
|
194
|
+
* so only already-registered bundles can be required — and cross-plugin value
|
|
195
|
+
* imports are a build error anyway.
|
|
196
|
+
*
|
|
197
|
+
* This file is the browser-safe contract face (zero node imports): the
|
|
198
|
+
* `__DSH_BOOT__` wire types, the boot-manifest parser, and the boundaries around
|
|
199
|
+
* {@link ClientModuleSystem}. The package root is the host-side service that
|
|
200
|
+
* composes the wire.
|
|
201
|
+
*/
|
|
202
|
+
/**
|
|
203
|
+
* Parse `window.__DSH_BOOT__` into the two consumer views. Wire boundary:
|
|
204
|
+
* a missing or malformed graph throws (the shell shows the loud failure —
|
|
205
|
+
* a page without a valid manifest cannot boot anything).
|
|
206
|
+
* @param wire - the raw `window.__DSH_BOOT__` value.
|
|
207
|
+
* @returns the manifest with optional plugin-view fields normalized.
|
|
208
|
+
*/
|
|
209
|
+
function parseBootManifest(wire) {
|
|
210
|
+
if (typeof wire !== "object" || wire === null) throw new Error("client-modules: window.__DSH_BOOT__ is missing or not an object");
|
|
211
|
+
const graph = wire;
|
|
212
|
+
if (typeof graph.rev !== "string") throw new Error("client-modules: boot manifest rev must be a string");
|
|
213
|
+
if (!Array.isArray(graph.entries)) throw new Error("client-modules: boot manifest entries must be an array");
|
|
214
|
+
const modules = [];
|
|
215
|
+
const plugins = [];
|
|
216
|
+
for (const value of graph.entries) {
|
|
217
|
+
if (typeof value !== "object" || value === null) throw new Error("client-modules: boot manifest entry is not an object");
|
|
218
|
+
const row = value;
|
|
219
|
+
const where = typeof row.id === "string" ? `"${row.id}"` : JSON.stringify(row);
|
|
220
|
+
if (typeof row.id !== "string" || typeof row.url !== "string" || typeof row.rev !== "string") throw new Error(`client-modules: boot manifest entry ${where} must carry string id/url/rev`);
|
|
221
|
+
if (row.inject !== void 0 && (!Array.isArray(row.inject) || row.inject.some((i) => typeof i !== "string"))) throw new Error(`client-modules: boot manifest entry ${where} inject must be a string array`);
|
|
222
|
+
if (row.immediately !== void 0 && typeof row.immediately !== "boolean") throw new Error(`client-modules: boot manifest entry ${where} immediately must be a boolean`);
|
|
223
|
+
modules.push({
|
|
224
|
+
id: row.id,
|
|
225
|
+
url: row.url,
|
|
226
|
+
rev: row.rev
|
|
227
|
+
});
|
|
228
|
+
plugins.push({
|
|
229
|
+
id: row.id,
|
|
230
|
+
inject: row.inject === void 0 ? [] : [...row.inject],
|
|
231
|
+
immediately: row.immediately === true
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
rev: graph.rev,
|
|
236
|
+
modules,
|
|
237
|
+
plugins
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region lib/types/client/index.js
|
|
242
|
+
/**
|
|
243
|
+
* Enroll the kernel-built module system as `ctx.modules`.
|
|
244
|
+
* @param ctx - client root context.
|
|
245
|
+
*/
|
|
246
|
+
function apply(ctx) {
|
|
247
|
+
const modules = globalThis.__DSH_MODULES__;
|
|
248
|
+
if (modules === void 0) throw new Error("client-modules: window.__DSH_MODULES__ missing — the shell kernel must construct the module system before plugin boot");
|
|
249
|
+
ctx.reflect.provide("modules", modules);
|
|
250
|
+
}
|
|
251
|
+
//#endregion
|
|
252
|
+
exports.ClientModuleSystem = ClientModuleSystem;
|
|
253
|
+
exports.apply = apply;
|
|
254
|
+
exports.parseBootManifest = parseBootManifest;
|
|
255
|
+
return module.exports;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
//# sourceMappingURL=client.js.map
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
7
|
+
//#region lib/types/index.js
|
|
8
|
+
/**
|
|
9
|
+
* Node half of the client module system (`dsh.client` dual-face package): scans
|
|
10
|
+
* the host Loader's entries for packages declaring `dsh.client`, composes the
|
|
11
|
+
* `window.__DSH_BOOT__` entry graph (wire single source: {@link WebBootEntry}
|
|
12
|
+
* in `./client/manifest.ts`), serves `/plugins/<id>/client.js` and its source
|
|
13
|
+
* map, taps the index render to inject the boot manifest, and provides the
|
|
14
|
+
* `clientModuleHost` service (the HMR node half's registration/notification
|
|
15
|
+
* face).
|
|
16
|
+
*
|
|
17
|
+
* Scanning is incremental per package — there is no full-rescan code path.
|
|
18
|
+
* Every cordis `internal/plugin` emission (fiber construction/disposal) marks
|
|
19
|
+
* the fiber's entry name dirty; a microtask flush reconciles each dirty name
|
|
20
|
+
* against the live loader entries. The activation pass seeds the same dirty
|
|
21
|
+
* set with all current entries and flushes synchronously, so first scan and
|
|
22
|
+
* steady state share one implementation. Package metadata (including the
|
|
23
|
+
* negative "not a client package" verdict) is cached per name and never
|
|
24
|
+
* expires — plugin-set changes take effect on restart; bundle content
|
|
25
|
+
* changes reach the graph only through
|
|
26
|
+
* {@link ClientModuleRegistry.rebuilt}.
|
|
27
|
+
* @module @deepseek-ai/dsh-client-modules
|
|
28
|
+
*/
|
|
29
|
+
/** Recovery instruction shared by grouped startup and steady-state bundle diagnostics. */
|
|
30
|
+
const CLIENT_BUNDLE_BUILD_INSTRUCTION = "run `pnpm run build` before launch";
|
|
31
|
+
/** Missing built client export, retained as structured data for activation-error grouping. */
|
|
32
|
+
var MissingClientBundleError = class extends Error {
|
|
33
|
+
packageName;
|
|
34
|
+
clientPath;
|
|
35
|
+
constructor(packageName, clientPath, cause) {
|
|
36
|
+
super([
|
|
37
|
+
`client-modules: client bundle not found; ${CLIENT_BUNDLE_BUILD_INSTRUCTION}:`,
|
|
38
|
+
` package: ${packageName}`,
|
|
39
|
+
` path: ${clientPath}`
|
|
40
|
+
].join("\n"), { cause });
|
|
41
|
+
this.packageName = packageName;
|
|
42
|
+
this.clientPath = clientPath;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
/** Activation failures grouped by actionable package-build errors and unrelated failures. */
|
|
46
|
+
var ClientPackageCompositionError = class extends AggregateError {
|
|
47
|
+
constructor(failures) {
|
|
48
|
+
const missingBundles = failures.filter((error) => error instanceof MissingClientBundleError);
|
|
49
|
+
const otherFailures = failures.filter((error) => !(error instanceof MissingClientBundleError));
|
|
50
|
+
const packageNoun = failures.length === 1 ? "package" : "packages";
|
|
51
|
+
const lines = [`client-modules: ${String(failures.length)} client ${packageNoun} failed to compose:`];
|
|
52
|
+
if (missingBundles.length > 0) {
|
|
53
|
+
lines.push(` client bundles not found; ${CLIENT_BUNDLE_BUILD_INSTRUCTION}:`);
|
|
54
|
+
for (const error of missingBundles) lines.push(` - package: ${error.packageName}`, ` path: ${error.clientPath}`);
|
|
55
|
+
}
|
|
56
|
+
if (otherFailures.length > 0) lines.push(" other failures:", ...otherFailures.map((error) => ` - ${error.message}`));
|
|
57
|
+
super(failures, lines.join("\n"));
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
/** Narrow an unknown parsed JSON value to the `dsh.client` declaration, throwing on malformed fields. */
|
|
61
|
+
function parseDshClient(pkgName, value) {
|
|
62
|
+
if (value === void 0) return void 0;
|
|
63
|
+
if (typeof value !== "object" || value === null) throw new Error(`client-modules: ${pkgName} has a non-object dsh.client declaration`);
|
|
64
|
+
const decl = value;
|
|
65
|
+
if (typeof decl.platform !== "string") throw new Error(`client-modules: ${pkgName} dsh.client.platform must be a string`);
|
|
66
|
+
if (decl.inject !== void 0 && (!Array.isArray(decl.inject) || decl.inject.some((i) => typeof i !== "string"))) throw new Error(`client-modules: ${pkgName} dsh.client.inject must be a string array`);
|
|
67
|
+
if (decl.immediately !== void 0 && typeof decl.immediately !== "boolean") throw new Error(`client-modules: ${pkgName} dsh.client.immediately must be a boolean`);
|
|
68
|
+
return {
|
|
69
|
+
platform: decl.platform,
|
|
70
|
+
...decl.inject !== void 0 ? { inject: decl.inject } : {},
|
|
71
|
+
...decl.immediately !== void 0 ? { immediately: decl.immediately } : {}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Resolve `exports["./client"]` to a relative path, accepting the string and one-level conditional forms. */
|
|
75
|
+
function clientExportOf(pkgName, exportsField) {
|
|
76
|
+
if (typeof exportsField !== "object" || exportsField === null) return void 0;
|
|
77
|
+
const client = exportsField["./client"];
|
|
78
|
+
if (client === void 0) return void 0;
|
|
79
|
+
if (typeof client === "string") return client;
|
|
80
|
+
if (typeof client === "object" && client !== null) {
|
|
81
|
+
const fallback = client.default;
|
|
82
|
+
if (typeof fallback === "string") return fallback;
|
|
83
|
+
}
|
|
84
|
+
throw new Error(`client-modules: ${pkgName} exports["./client"] must be a string or an object with a string default`);
|
|
85
|
+
}
|
|
86
|
+
/** sha1 content hash shortened to 12 hex chars (bundle rev / graph rev). */
|
|
87
|
+
function shortHash(input) {
|
|
88
|
+
return createHash("sha1").update(input).digest("hex").slice(0, 12);
|
|
89
|
+
}
|
|
90
|
+
/** Graph row for one bundle rev (url carries the rev as its cache-busting query). */
|
|
91
|
+
function graphRow(id, rev, injectEdges, immediately) {
|
|
92
|
+
return {
|
|
93
|
+
id,
|
|
94
|
+
url: `/plugins/${id}/client.js?rev=${rev}`,
|
|
95
|
+
rev,
|
|
96
|
+
...injectEdges !== void 0 ? { inject: injectEdges } : {},
|
|
97
|
+
...immediately ? { immediately: true } : {}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Inject the boot entry graph into index.html: `window.__DSH_BOOT__` as the
|
|
102
|
+
* first script in <head> (before the shell bundle reads it). `<` is escaped in
|
|
103
|
+
* the JSON so plugin-controlled strings cannot break out of the script element.
|
|
104
|
+
* @param html - the index.html source.
|
|
105
|
+
* @param graph - the composed entry graph.
|
|
106
|
+
* @returns the html with the graph script injected.
|
|
107
|
+
*/
|
|
108
|
+
function injectBootManifest(html, graph) {
|
|
109
|
+
const script = `<script>window.__DSH_BOOT__ = ${JSON.stringify(graph).replaceAll("<", "\\u003c")}<\/script>`;
|
|
110
|
+
const head = html.indexOf("<head>");
|
|
111
|
+
if (head !== -1) return `${html.slice(0, head + 6)}${script}${html.slice(head + 6)}`;
|
|
112
|
+
return `${script}${html}`;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The web plugin table service: incremental `dsh.client` scan + wire composition
|
|
116
|
+
* + bundle route + index tap. Construction runs the activation scan
|
|
117
|
+
* synchronously — a malformed declaration or missing bundle among the
|
|
118
|
+
* already-loaded entries aggregates into one loud throw (FAILED fiber; the
|
|
119
|
+
* boot activation audit reports it).
|
|
120
|
+
*/
|
|
121
|
+
var ClientModuleRegistry = class extends Service {
|
|
122
|
+
static inject = ["webServer", "loader"];
|
|
123
|
+
table = /* @__PURE__ */ new Map();
|
|
124
|
+
pkgMeta = /* @__PURE__ */ new Map();
|
|
125
|
+
rebuildListeners = /* @__PURE__ */ new Set();
|
|
126
|
+
graphListeners = /* @__PURE__ */ new Set();
|
|
127
|
+
dirty = /* @__PURE__ */ new Set();
|
|
128
|
+
resolvePkgJson;
|
|
129
|
+
flushQueued = false;
|
|
130
|
+
composed;
|
|
131
|
+
/**
|
|
132
|
+
* Build the service: subscribe, seed, and run the activation flush.
|
|
133
|
+
* @param ctx - plugin context carrying webServer and loader.
|
|
134
|
+
*/
|
|
135
|
+
constructor(ctx) {
|
|
136
|
+
super(ctx, "clientModules");
|
|
137
|
+
if (ctx.baseUrl === void 0) throw new Error("client-modules: ctx.baseUrl is unset — the node half needs the config-tree anchor to resolve plugin packages");
|
|
138
|
+
const require = createRequire(ctx.baseUrl);
|
|
139
|
+
this.resolvePkgJson = (spec) => require.resolve(`${spec}/package.json`);
|
|
140
|
+
ctx.on("internal/plugin", (fiber) => {
|
|
141
|
+
const entryName = fiber.entry?.options.name;
|
|
142
|
+
if (entryName === void 0) return;
|
|
143
|
+
this.dirty.add(entryName);
|
|
144
|
+
if (this.flushQueued) return;
|
|
145
|
+
this.flushQueued = true;
|
|
146
|
+
queueMicrotask(() => {
|
|
147
|
+
this.flushQueued = false;
|
|
148
|
+
this.flush((err) => {
|
|
149
|
+
ctx.logger.warn(err);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
for (const entry of ctx.loader.entries()) this.dirty.add(entry.options.name);
|
|
154
|
+
this.composed = this.compose();
|
|
155
|
+
const failures = [];
|
|
156
|
+
this.flush((err) => failures.push(err));
|
|
157
|
+
if (failures.length > 0) throw new ClientPackageCompositionError(failures);
|
|
158
|
+
ctx.effect(() => ctx.webServer.register({
|
|
159
|
+
kind: "prefix",
|
|
160
|
+
path: "/plugins",
|
|
161
|
+
handler: this.serveBundle
|
|
162
|
+
}), "client-modules: bundle route");
|
|
163
|
+
ctx.effect(() => ctx.webServer.tapIndex((html) => injectBootManifest(html, this.composed)), "client-modules: boot manifest injection");
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Current composed entry graph (stable object between changes).
|
|
167
|
+
* @returns the graph served as `window.__DSH_BOOT__`.
|
|
168
|
+
*/
|
|
169
|
+
graph() {
|
|
170
|
+
return this.composed;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Absolute path of an entry's client bundle.
|
|
174
|
+
* @param id - entry id (package name).
|
|
175
|
+
* @returns the path, or undefined for an unknown id.
|
|
176
|
+
*/
|
|
177
|
+
clientPath(id) {
|
|
178
|
+
return this.table.get(id)?.clientPath;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Re-hash one bundle (the HMR watch's registration hook — the only entry
|
|
182
|
+
* point through which bundle content changes reach the graph).
|
|
183
|
+
* @param id - entry id (package name).
|
|
184
|
+
* @returns the new rev, or undefined for an unknown id.
|
|
185
|
+
*/
|
|
186
|
+
rebuilt(id) {
|
|
187
|
+
const record = this.table.get(id);
|
|
188
|
+
if (record === void 0) return void 0;
|
|
189
|
+
const rev = shortHash(readFileSync(record.clientPath));
|
|
190
|
+
if (rev === record.entry.rev) return rev;
|
|
191
|
+
record.entry = graphRow(id, rev, record.entry.inject, record.entry.immediately === true);
|
|
192
|
+
this.composed = this.compose();
|
|
193
|
+
for (const notify of this.rebuildListeners) try {
|
|
194
|
+
notify(id, rev);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
this.ctx.logger.error(error);
|
|
197
|
+
}
|
|
198
|
+
this.notifyGraphChanged();
|
|
199
|
+
return rev;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Subscribe to bundle rebuilds; fires only when the re-hash changed the rev.
|
|
203
|
+
* @param listener - receives the entry id and its new bundle rev.
|
|
204
|
+
* @returns the unsubscriber.
|
|
205
|
+
*/
|
|
206
|
+
onRebuilt(listener) {
|
|
207
|
+
this.rebuildListeners.add(listener);
|
|
208
|
+
return () => {
|
|
209
|
+
this.rebuildListeners.delete(listener);
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Fires after any flush that recomposed the graph (row added/removed, or a
|
|
214
|
+
* rebuilt rev change). Pull model: listeners re-read {@link graph}.
|
|
215
|
+
* @param listener - notified with no payload.
|
|
216
|
+
* @returns the unsubscriber.
|
|
217
|
+
*/
|
|
218
|
+
onGraphChanged(listener) {
|
|
219
|
+
this.graphListeners.add(listener);
|
|
220
|
+
return () => {
|
|
221
|
+
this.graphListeners.delete(listener);
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
compose() {
|
|
225
|
+
const entries = [...this.table.values()].map((record) => record.entry);
|
|
226
|
+
return {
|
|
227
|
+
rev: shortHash(JSON.stringify(entries)),
|
|
228
|
+
entries
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
notifyGraphChanged() {
|
|
232
|
+
for (const listener of this.graphListeners) try {
|
|
233
|
+
listener();
|
|
234
|
+
} catch (error) {
|
|
235
|
+
this.ctx.logger.error(error);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
resolveMeta(pkgName) {
|
|
239
|
+
const cached = this.pkgMeta.get(pkgName);
|
|
240
|
+
if (cached !== void 0) return cached;
|
|
241
|
+
let pkgPath;
|
|
242
|
+
try {
|
|
243
|
+
pkgPath = this.resolvePkgJson(pkgName);
|
|
244
|
+
} catch {
|
|
245
|
+
this.pkgMeta.set(pkgName, null);
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
249
|
+
const dsh = pkg.dsh;
|
|
250
|
+
const decl = parseDshClient(pkgName, dsh !== null && typeof dsh === "object" ? dsh.client : void 0);
|
|
251
|
+
if (decl === void 0 || decl.platform !== "web") {
|
|
252
|
+
this.pkgMeta.set(pkgName, null);
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const clientRel = clientExportOf(pkgName, pkg.exports);
|
|
256
|
+
if (clientRel === void 0) throw new Error(`client-modules: ${pkgName} declares dsh.client but exports no "./client" bundle`);
|
|
257
|
+
const meta = {
|
|
258
|
+
clientPath: join(dirname(pkgPath), clientRel),
|
|
259
|
+
...decl.inject !== void 0 ? { inject: decl.inject } : {},
|
|
260
|
+
immediately: decl.immediately === true
|
|
261
|
+
};
|
|
262
|
+
this.pkgMeta.set(pkgName, meta);
|
|
263
|
+
return meta;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Read the activation-time bundle revision.
|
|
267
|
+
* @param pkgName - package that declares the client bundle.
|
|
268
|
+
* @param clientPath - absolute path of the built client artifact.
|
|
269
|
+
* @returns the bundle content's short hash for use as its revision.
|
|
270
|
+
* @throws {MissingClientBundleError} when the read fails with `ENOENT`; other filesystem errors are rethrown unchanged.
|
|
271
|
+
*/
|
|
272
|
+
initialBundleRevision(pkgName, clientPath) {
|
|
273
|
+
try {
|
|
274
|
+
return shortHash(readFileSync(clientPath));
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if (error.code !== "ENOENT") throw error;
|
|
277
|
+
throw new MissingClientBundleError(pkgName, clientPath, error);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/** Reconcile one entry name against the live loader entries. @returns whether the table changed. */
|
|
281
|
+
processOne(entryName) {
|
|
282
|
+
let qualifies = false;
|
|
283
|
+
for (const entry of this.ctx.loader.entries()) if (entry.options.name === entryName && entry.fiber !== void 0 && !entry.disabled) {
|
|
284
|
+
qualifies = true;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
if (!qualifies) return this.table.delete(entryName);
|
|
288
|
+
if (this.table.has(entryName)) return false;
|
|
289
|
+
const meta = this.resolveMeta(entryName);
|
|
290
|
+
if (meta === null) return false;
|
|
291
|
+
const rev = this.initialBundleRevision(entryName, meta.clientPath);
|
|
292
|
+
this.table.set(entryName, {
|
|
293
|
+
entry: graphRow(entryName, rev, meta.inject, meta.immediately),
|
|
294
|
+
clientPath: meta.clientPath
|
|
295
|
+
});
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
flush(onError) {
|
|
299
|
+
let changed = false;
|
|
300
|
+
for (const entryName of [...this.dirty]) {
|
|
301
|
+
this.dirty.delete(entryName);
|
|
302
|
+
try {
|
|
303
|
+
if (this.processOne(entryName)) changed = true;
|
|
304
|
+
} catch (error) {
|
|
305
|
+
onError(error instanceof Error ? error : new Error(String(error)));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (changed) {
|
|
309
|
+
this.composed = this.compose();
|
|
310
|
+
this.notifyGraphChanged();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
serveBundle = async (req, res) => {
|
|
314
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
315
|
+
res.writeHead(405);
|
|
316
|
+
res.end();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
/* v8 ignore next -- `?? '/'` arm: node:http always sets url on server requests. */
|
|
320
|
+
const pathname = decodeURIComponent(new URL(req.url ?? "/", "http://x").pathname);
|
|
321
|
+
const prefix = "/plugins/";
|
|
322
|
+
const mapSuffix = "/client.js.map";
|
|
323
|
+
const bundleSuffix = "/client.js";
|
|
324
|
+
const isSourceMap = pathname.startsWith(prefix) && pathname.endsWith(mapSuffix);
|
|
325
|
+
const suffix = isSourceMap ? mapSuffix : bundleSuffix;
|
|
326
|
+
const clientPath = pathname.startsWith(prefix) && pathname.endsWith(suffix) ? this.clientPath(pathname.slice(9, -suffix.length)) : void 0;
|
|
327
|
+
const path = clientPath === void 0 ? void 0 : `${clientPath}${isSourceMap ? ".map" : ""}`;
|
|
328
|
+
if (path === void 0) {
|
|
329
|
+
res.writeHead(404);
|
|
330
|
+
res.end();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
const body = await readFile(path);
|
|
335
|
+
res.writeHead(200, {
|
|
336
|
+
"content-type": isSourceMap ? "application/json; charset=utf-8" : "text/javascript; charset=utf-8",
|
|
337
|
+
"cache-control": "no-cache",
|
|
338
|
+
"content-length": body.length
|
|
339
|
+
});
|
|
340
|
+
res.end(body);
|
|
341
|
+
} catch {
|
|
342
|
+
res.writeHead(404);
|
|
343
|
+
res.end();
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
};
|
|
347
|
+
//#endregion
|
|
348
|
+
export { ClientModuleRegistry, ClientModuleRegistry as default, injectBootManifest };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-client-modules`.
|
|
4
|
+
* @module @deepseek-ai/dsh-client-modules/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@deepseek-ai/dsh-client-modules";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "client-modules-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* Owned relation: the node half's boot entry graph must stay self-consistent
|
|
13
|
+
* — every row must resolve a clientPath under the same id (the
|
|
14
|
+
* /plugins/<id>/client.js URL it advertises would otherwise 404 on a browser
|
|
15
|
+
* that just received the graph). Checked on every scan trigger (cordis
|
|
16
|
+
* 'internal/plugin'): graph() and clientPath() read the same table object,
|
|
17
|
+
* so the relation holds at any instant — no need to wait out the node half's
|
|
18
|
+
* own microtask-debounced flush.
|
|
19
|
+
*/
|
|
20
|
+
const install = (ctx, fail) => {
|
|
21
|
+
ctx.on("internal/plugin", () => {
|
|
22
|
+
const host = ctx.get("clientModules");
|
|
23
|
+
if (host === void 0) return;
|
|
24
|
+
for (const row of host.graph().entries) if (host.clientPath(row.id) === void 0) fail(`web plugin graph row "${row.id}" advertises ${row.url} but resolves no client bundle path — the served __DSH_BOOT__ would 404 on fetch`);
|
|
25
|
+
}, { global: true });
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Register this package's invariant companion.
|
|
29
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
30
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
31
|
+
*/
|
|
32
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
33
|
+
//#endregion
|
|
34
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half (the standard `./client` export): the module-system class and
|
|
3
|
+
* wire contract, plus the enrollment plugin face. The module system itself is
|
|
4
|
+
* built by the shell kernel BEFORE cordis exists (the bootstrap exception —
|
|
5
|
+
* the mechanism that loads plugins cannot arrive through
|
|
6
|
+
* itself); the plugin face only enrolls that pre-existing instance by
|
|
7
|
+
* providing it as `ctx.modules`. The kernel statically registers this module,
|
|
8
|
+
* so the graph row for this package never triggers a real fetch — arrival is
|
|
9
|
+
* a no-op against the already-registered entry.
|
|
10
|
+
* @module @deepseek-ai/dsh-client-modules/client
|
|
11
|
+
*/
|
|
12
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
13
|
+
export { ClientModuleSystem } from './system.ts';
|
|
14
|
+
export { parseBootManifest } from './manifest.ts';
|
|
15
|
+
export type { BootManifest, BootModuleRow, BootPluginRow, ClientModuleLoader, ClientModuleRecord, ClientModuleSystemOptions, ClientPluginHandoff, DshWindow, WebBootEntry, WebBootGraph, } from './manifest.ts';
|
|
16
|
+
/**
|
|
17
|
+
* Enroll the kernel-built module system as `ctx.modules`.
|
|
18
|
+
* @param ctx - client root context.
|
|
19
|
+
*/
|
|
20
|
+
export declare function apply(ctx: Context): void;
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client module system: the browser peer of Node's internal ESM loader, built
|
|
3
|
+
* as a lazy CJS table. The vendored cordis Loader consumes this object
|
|
4
|
+
* through its `internal` contract (the only call site is `EntryTree.import` →
|
|
5
|
+
* `internal.import`), which keeps entry governance (fiber lifecycle, inject
|
|
6
|
+
* waiting, update/refresh) entirely on the vendored side while this package
|
|
7
|
+
* owns code arrival.
|
|
8
|
+
*
|
|
9
|
+
* Lazy CJS model: executing a plugin bundle only REGISTERS its
|
|
10
|
+
* factory (`window.__ModuleLoader__.load({id, factory})`); every module body
|
|
11
|
+
* side effect — including CSS injection — lives inside the factory closure
|
|
12
|
+
* and runs at materialization, not at script execution. Materialization
|
|
13
|
+
* (factory(require) → exports) happens on first import/require and is
|
|
14
|
+
* memoized in {@link ClientModuleLoader.loadCache}; a factory that requires
|
|
15
|
+
* another registered-but-unmaterialized module materializes it recursively,
|
|
16
|
+
* so load order needs no external sequencing.
|
|
17
|
+
*
|
|
18
|
+
* Resolution branch order (import): seed word → shell instance; memoized
|
|
19
|
+
* record → exports; static registry (shell-own modules, e.g. app-shell) →
|
|
20
|
+
* module; registered factory → materialize; graph row → load + materialize;
|
|
21
|
+
* anything else → throw (loud — the runtime mirror of the
|
|
22
|
+
* build-time bundle purity gate). The synchronous `require` handed to
|
|
23
|
+
* factories walks the same order minus the load branch: loading is async,
|
|
24
|
+
* so only already-registered bundles can be required — and cross-plugin value
|
|
25
|
+
* imports are a build error anyway.
|
|
26
|
+
*
|
|
27
|
+
* This file is the browser-safe contract face (zero node imports): the
|
|
28
|
+
* `__DSH_BOOT__` wire types, the boot-manifest parser, and the boundaries around
|
|
29
|
+
* {@link ClientModuleSystem}. The package root is the host-side service that
|
|
30
|
+
* composes the wire.
|
|
31
|
+
*/
|
|
32
|
+
import type { ClientModuleSystem } from './system.ts';
|
|
33
|
+
declare module '@deepseek-ai/cordis' {
|
|
34
|
+
interface Context {
|
|
35
|
+
/** The client module system the web shell builds at boot (provided by the `./client` wrapper plugin). */
|
|
36
|
+
modules: ClientModuleLoader;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* One composed client entry pushed by the host (a graph row). Wire
|
|
41
|
+
* single source: the host node half (package root) produces this same shape.
|
|
42
|
+
* `immediately` marks stage-one prefetch; `inject` is informational graph
|
|
43
|
+
* metadata (the authoritative edges live in each package's `dsh.client`
|
|
44
|
+
* declaration and reach fibers through entry creation).
|
|
45
|
+
*/
|
|
46
|
+
export interface WebBootEntry {
|
|
47
|
+
/** Entry name == package name. */
|
|
48
|
+
id: string;
|
|
49
|
+
/** Bundle endpoint, '/plugins/<id>/client.js?rev=<rev>'. */
|
|
50
|
+
url: string;
|
|
51
|
+
/** Bundle content hash (cache-busting consistency anchor). */
|
|
52
|
+
rev: string;
|
|
53
|
+
/** Package-name dependency edges, informational (preflight display / HMR diffing). */
|
|
54
|
+
inject?: string[];
|
|
55
|
+
/** Stage-one prefetch mark: load the script for factory registration during module-face boot. */
|
|
56
|
+
immediately?: boolean;
|
|
57
|
+
}
|
|
58
|
+
/** The composed client entry graph the host injects as `window.__DSH_BOOT__`. */
|
|
59
|
+
export interface WebBootGraph {
|
|
60
|
+
/** Consistency anchor over the whole graph (content + bundle hashes). */
|
|
61
|
+
rev: string;
|
|
62
|
+
/** Composed entries; order carries no semantics (activation order is fiber inject waiting). */
|
|
63
|
+
entries: WebBootEntry[];
|
|
64
|
+
}
|
|
65
|
+
/** The npm-package view of one boot row: what the module table needs to fetch the bundle. */
|
|
66
|
+
export interface BootModuleRow {
|
|
67
|
+
/** Entry name == package name (module-table key). */
|
|
68
|
+
id: string;
|
|
69
|
+
/** Bundle endpoint, '/plugins/<id>/client.js?rev=<rev>'. */
|
|
70
|
+
url: string;
|
|
71
|
+
/** Bundle content hash. */
|
|
72
|
+
rev: string;
|
|
73
|
+
}
|
|
74
|
+
/** The cordis-plugin view of one boot row: what entry composition needs (optional wire fields normalized). */
|
|
75
|
+
export interface BootPluginRow {
|
|
76
|
+
/** Entry name == package name. */
|
|
77
|
+
id: string;
|
|
78
|
+
/** Package-name dependency edges ([] when the wire omits them). */
|
|
79
|
+
inject: string[];
|
|
80
|
+
/** Stage-one prefetch tier (false when the wire omits it). */
|
|
81
|
+
immediately: boolean;
|
|
82
|
+
}
|
|
83
|
+
/** The parsed boot manifest: one wire, two consumer views. */
|
|
84
|
+
export interface BootManifest {
|
|
85
|
+
/** Consistency anchor over the whole graph. */
|
|
86
|
+
rev: string;
|
|
87
|
+
/** Rows as the module table consumes them. */
|
|
88
|
+
modules: BootModuleRow[];
|
|
89
|
+
/** Rows as entry composition consumes them. */
|
|
90
|
+
plugins: BootPluginRow[];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Parse `window.__DSH_BOOT__` into the two consumer views. Wire boundary:
|
|
94
|
+
* a missing or malformed graph throws (the shell shows the loud failure —
|
|
95
|
+
* a page without a valid manifest cannot boot anything).
|
|
96
|
+
* @param wire - the raw `window.__DSH_BOOT__` value.
|
|
97
|
+
* @returns the manifest with optional plugin-view fields normalized.
|
|
98
|
+
*/
|
|
99
|
+
export declare function parseBootManifest(wire: unknown): BootManifest;
|
|
100
|
+
/** The shape a client bundle hands to `window.__ModuleLoader__.load` (registration handoff). */
|
|
101
|
+
export interface ClientPluginHandoff {
|
|
102
|
+
/** Plugin id (package name) — the registration key; must match the graph row being executed. */
|
|
103
|
+
id: string;
|
|
104
|
+
/**
|
|
105
|
+
* Closure factory holding the whole bundle body: receives the synchronous
|
|
106
|
+
* require bound to the module table and returns the bundle's exports. Runs
|
|
107
|
+
* once, at materialization.
|
|
108
|
+
*/
|
|
109
|
+
factory: (require: (spec: string) => unknown) => Record<string, unknown>;
|
|
110
|
+
}
|
|
111
|
+
/** Window API of the web boot protocol: the host-injected graph, registration sink, and kernel handoff slot. */
|
|
112
|
+
export interface DshWindow {
|
|
113
|
+
/** Host-composed entry graph, injected before the shell bundle runs; wire-boundary raw until {@link parseBootManifest}. */
|
|
114
|
+
__DSH_BOOT__?: unknown;
|
|
115
|
+
/** Bundle registration sink; installed once per page by the {@link ClientModuleSystem} constructor. */
|
|
116
|
+
__ModuleLoader__?: {
|
|
117
|
+
load(handoff: ClientPluginHandoff): void;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Kernel handoff slot: the shell kernel stores the instance here right
|
|
121
|
+
* after construction (before cordis exists) so the `./client` wrapper
|
|
122
|
+
* plugin can provide it as `ctx.modules`. Missing slot at wrapper apply
|
|
123
|
+
* time = kernel sequencing bug, thrown loud.
|
|
124
|
+
*/
|
|
125
|
+
__DSH_MODULES__?: ClientModuleSystem;
|
|
126
|
+
}
|
|
127
|
+
/** Per-module bookkeeping in {@link ClientModuleLoader.loadCache} (module-graph boundary, flat today). */
|
|
128
|
+
export interface ClientModuleRecord {
|
|
129
|
+
/** Module id (entry name / package name). */
|
|
130
|
+
id: string;
|
|
131
|
+
/** Materialized exports (`module.exports` from a factory, or a statically registered shell module). */
|
|
132
|
+
exports: unknown;
|
|
133
|
+
/** Owned `<style data-plugin>` tag ids (`data-plugin-css` values) injected during materialization. */
|
|
134
|
+
styles: string[];
|
|
135
|
+
/** Observed `require()` edges (module-graph boundary; only table words can appear today). */
|
|
136
|
+
edges: Set<string>;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The internal-contract subset the vendored Loader and the client HMR plugin
|
|
140
|
+
* consume. Mounted on `ctx.loader.internal` by the shell boot and provided
|
|
141
|
+
* as `ctx.modules`.
|
|
142
|
+
*/
|
|
143
|
+
export interface ClientModuleLoader {
|
|
144
|
+
/** Discriminant against Node's internal loader shapes ('v1'/'v2'). */
|
|
145
|
+
version: 'client';
|
|
146
|
+
/** Materialized-module registry: id → record. The governance-side read API for entry exports. */
|
|
147
|
+
loadCache: Map<string, ClientModuleRecord>;
|
|
148
|
+
/**
|
|
149
|
+
* Internal contract consumed by the vendored Loader's `tree.import`. Resolves
|
|
150
|
+
* `specifier` through the branch order documented on the module, fetching
|
|
151
|
+
* and executing a bundle when needed.
|
|
152
|
+
* @param specifier - module specifier (entry name or table word).
|
|
153
|
+
* @param parentURL - importer URL (unused — the client module graph is flat).
|
|
154
|
+
* @param attrs - Import attributes (unused; interface parity with Node's loader contract).
|
|
155
|
+
* @returns the module's exports.
|
|
156
|
+
*/
|
|
157
|
+
import(specifier: string, parentURL: string, attrs: Record<string, unknown>): Promise<unknown>;
|
|
158
|
+
/**
|
|
159
|
+
* Register a shell-own module (app-shell — code that ships inside the shell
|
|
160
|
+
* bundle and never arrives as a plugin bundle).
|
|
161
|
+
* @param id - entry name (shell-owned pseudo id).
|
|
162
|
+
* @param module - the statically imported module namespace.
|
|
163
|
+
*/
|
|
164
|
+
registerStatic(id: string, module: unknown): void;
|
|
165
|
+
/**
|
|
166
|
+
* Stage-one arrival: load the entry's script to register its factory (no
|
|
167
|
+
* materialization — module side effects wait for import).
|
|
168
|
+
* No-op for static-registered ids and ids whose factory is already
|
|
169
|
+
* registered; concurrent calls share one in-flight task. To force a fresh
|
|
170
|
+
* load (HMR), {@link invalidate} first.
|
|
171
|
+
* @param id - graph entry name.
|
|
172
|
+
*/
|
|
173
|
+
prefetch(id: string): Promise<void>;
|
|
174
|
+
/**
|
|
175
|
+
* Full reset of one module: drop its registered factory and materialized
|
|
176
|
+
* record so the next prefetch/import reloads it (the HMR invalidation hook).
|
|
177
|
+
* @param id - entry name to invalidate.
|
|
178
|
+
*/
|
|
179
|
+
invalidate(id: string): void;
|
|
180
|
+
}
|
|
181
|
+
/** Options for {@link ClientModuleSystem} (assembled by the web shell kernel at boot). */
|
|
182
|
+
export interface ClientModuleSystemOptions {
|
|
183
|
+
/** Boot rows in the module-table view (from {@link parseBootManifest}). */
|
|
184
|
+
modules: BootModuleRow[];
|
|
185
|
+
/** Module-table seed: platform-singleton specifier → shell instance. */
|
|
186
|
+
staticModules: Record<string, unknown>;
|
|
187
|
+
/** Bundle-load hook. Defaults to a same-origin classic `<script src>` element. */
|
|
188
|
+
loadBundle?: (url: string) => Promise<void>;
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClientModuleSystem — the implementation behind the {@link ClientModuleLoader}
|
|
3
|
+
* contract. The conceptual contract (lazy CJS model, resolution branch order) is
|
|
4
|
+
* documented on the public interfaces in `./manifest.ts`; this file owns the
|
|
5
|
+
* state tables and the load/materialize machinery.
|
|
6
|
+
*/
|
|
7
|
+
import type { ClientModuleLoader, ClientModuleRecord, ClientModuleSystemOptions } from './manifest.ts';
|
|
8
|
+
/**
|
|
9
|
+
* The client module system: state tables plus the arrival/materialization
|
|
10
|
+
* machinery implementing {@link ClientModuleLoader} (whose members carry the
|
|
11
|
+
* contract documentation). Construction indexes the boot rows and installs the
|
|
12
|
+
* `window.__ModuleLoader__` registration sink — once per page.
|
|
13
|
+
*/
|
|
14
|
+
export declare class ClientModuleSystem implements ClientModuleLoader {
|
|
15
|
+
readonly version = "client";
|
|
16
|
+
readonly loadCache: Map<string, ClientModuleRecord>;
|
|
17
|
+
private readonly seed;
|
|
18
|
+
private readonly statics;
|
|
19
|
+
private readonly factories;
|
|
20
|
+
/** In-flight prefetch (script load) per id; concurrent callers share it. */
|
|
21
|
+
private readonly pendingArrival;
|
|
22
|
+
/** Materialization re-entrancy guard: factory-form CJS cannot deliver partial exports, so a cycle is fatal. */
|
|
23
|
+
private readonly materializing;
|
|
24
|
+
private readonly graphRows;
|
|
25
|
+
private readonly loadBundle;
|
|
26
|
+
/**
|
|
27
|
+
* Build the module system over the parsed boot rows.
|
|
28
|
+
* @param options - Module rows, module-table staticModules, and bundle-load hook.
|
|
29
|
+
*/
|
|
30
|
+
constructor(options: ClientModuleSystemOptions);
|
|
31
|
+
/** Load one graph row so its factory is registered (idempotent per in-flight arrival). */
|
|
32
|
+
private arrive;
|
|
33
|
+
/** Materialize a registered factory (synchronous; memoized in loadCache). */
|
|
34
|
+
private materialize;
|
|
35
|
+
/**
|
|
36
|
+
* The synchronous require answered to factories: seed → static → memoized
|
|
37
|
+
* record → registered factory (recursive materialization — this is what
|
|
38
|
+
* makes load order self-resolving). Fetching is async and therefore
|
|
39
|
+
* unreachable from here; an unregistered plugin specifier is loud (and a
|
|
40
|
+
* cross-plugin value import is already a build error upstream).
|
|
41
|
+
*/
|
|
42
|
+
private makeRequire;
|
|
43
|
+
import(specifier: string): Promise<unknown>;
|
|
44
|
+
registerStatic(id: string, module: unknown): void;
|
|
45
|
+
prefetch(id: string): Promise<void>;
|
|
46
|
+
invalidate(id: string): void;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=system.d.ts.map
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node half of the client module system (`dsh.client` dual-face package): scans
|
|
3
|
+
* the host Loader's entries for packages declaring `dsh.client`, composes the
|
|
4
|
+
* `window.__DSH_BOOT__` entry graph (wire single source: {@link WebBootEntry}
|
|
5
|
+
* in `./client/manifest.ts`), serves `/plugins/<id>/client.js` and its source
|
|
6
|
+
* map, taps the index render to inject the boot manifest, and provides the
|
|
7
|
+
* `clientModuleHost` service (the HMR node half's registration/notification
|
|
8
|
+
* face).
|
|
9
|
+
*
|
|
10
|
+
* Scanning is incremental per package — there is no full-rescan code path.
|
|
11
|
+
* Every cordis `internal/plugin` emission (fiber construction/disposal) marks
|
|
12
|
+
* the fiber's entry name dirty; a microtask flush reconciles each dirty name
|
|
13
|
+
* against the live loader entries. The activation pass seeds the same dirty
|
|
14
|
+
* set with all current entries and flushes synchronously, so first scan and
|
|
15
|
+
* steady state share one implementation. Package metadata (including the
|
|
16
|
+
* negative "not a client package" verdict) is cached per name and never
|
|
17
|
+
* expires — plugin-set changes take effect on restart; bundle content
|
|
18
|
+
* changes reach the graph only through
|
|
19
|
+
* {@link ClientModuleRegistry.rebuilt}.
|
|
20
|
+
* @module @deepseek-ai/dsh-client-modules
|
|
21
|
+
*/
|
|
22
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
23
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
24
|
+
import type { WebBootGraph } from './client/manifest.ts';
|
|
25
|
+
export type { BootManifest, BootModuleRow, BootPluginRow, WebBootEntry, WebBootGraph, } from './client/manifest.ts';
|
|
26
|
+
declare module '@deepseek-ai/cordis' {
|
|
27
|
+
interface Context {
|
|
28
|
+
/** The web plugin table (provided by the client-modules node half). */
|
|
29
|
+
clientModules: ClientModuleRegistry;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Inject the boot entry graph into index.html: `window.__DSH_BOOT__` as the
|
|
34
|
+
* first script in <head> (before the shell bundle reads it). `<` is escaped in
|
|
35
|
+
* the JSON so plugin-controlled strings cannot break out of the script element.
|
|
36
|
+
* @param html - the index.html source.
|
|
37
|
+
* @param graph - the composed entry graph.
|
|
38
|
+
* @returns the html with the graph script injected.
|
|
39
|
+
*/
|
|
40
|
+
export declare function injectBootManifest(html: string, graph: WebBootGraph): string;
|
|
41
|
+
/**
|
|
42
|
+
* The web plugin table service: incremental `dsh.client` scan + wire composition
|
|
43
|
+
* + bundle route + index tap. Construction runs the activation scan
|
|
44
|
+
* synchronously — a malformed declaration or missing bundle among the
|
|
45
|
+
* already-loaded entries aggregates into one loud throw (FAILED fiber; the
|
|
46
|
+
* boot activation audit reports it).
|
|
47
|
+
*/
|
|
48
|
+
export declare class ClientModuleRegistry extends Service {
|
|
49
|
+
static inject: string[];
|
|
50
|
+
private readonly table;
|
|
51
|
+
private readonly pkgMeta;
|
|
52
|
+
private readonly rebuildListeners;
|
|
53
|
+
private readonly graphListeners;
|
|
54
|
+
private readonly dirty;
|
|
55
|
+
private readonly resolvePkgJson;
|
|
56
|
+
private flushQueued;
|
|
57
|
+
private composed;
|
|
58
|
+
/**
|
|
59
|
+
* Build the service: subscribe, seed, and run the activation flush.
|
|
60
|
+
* @param ctx - plugin context carrying webServer and loader.
|
|
61
|
+
*/
|
|
62
|
+
constructor(ctx: Context);
|
|
63
|
+
/**
|
|
64
|
+
* Current composed entry graph (stable object between changes).
|
|
65
|
+
* @returns the graph served as `window.__DSH_BOOT__`.
|
|
66
|
+
*/
|
|
67
|
+
graph(): WebBootGraph;
|
|
68
|
+
/**
|
|
69
|
+
* Absolute path of an entry's client bundle.
|
|
70
|
+
* @param id - entry id (package name).
|
|
71
|
+
* @returns the path, or undefined for an unknown id.
|
|
72
|
+
*/
|
|
73
|
+
clientPath(id: string): string | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* Re-hash one bundle (the HMR watch's registration hook — the only entry
|
|
76
|
+
* point through which bundle content changes reach the graph).
|
|
77
|
+
* @param id - entry id (package name).
|
|
78
|
+
* @returns the new rev, or undefined for an unknown id.
|
|
79
|
+
*/
|
|
80
|
+
rebuilt(id: string): string | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* Subscribe to bundle rebuilds; fires only when the re-hash changed the rev.
|
|
83
|
+
* @param listener - receives the entry id and its new bundle rev.
|
|
84
|
+
* @returns the unsubscriber.
|
|
85
|
+
*/
|
|
86
|
+
onRebuilt(listener: (id: string, rev: string) => void): () => void;
|
|
87
|
+
/**
|
|
88
|
+
* Fires after any flush that recomposed the graph (row added/removed, or a
|
|
89
|
+
* rebuilt rev change). Pull model: listeners re-read {@link graph}.
|
|
90
|
+
* @param listener - notified with no payload.
|
|
91
|
+
* @returns the unsubscriber.
|
|
92
|
+
*/
|
|
93
|
+
onGraphChanged(listener: () => void): () => void;
|
|
94
|
+
private compose;
|
|
95
|
+
private notifyGraphChanged;
|
|
96
|
+
private resolveMeta;
|
|
97
|
+
/**
|
|
98
|
+
* Read the activation-time bundle revision.
|
|
99
|
+
* @param pkgName - package that declares the client bundle.
|
|
100
|
+
* @param clientPath - absolute path of the built client artifact.
|
|
101
|
+
* @returns the bundle content's short hash for use as its revision.
|
|
102
|
+
* @throws {MissingClientBundleError} when the read fails with `ENOENT`; other filesystem errors are rethrown unchanged.
|
|
103
|
+
*/
|
|
104
|
+
private initialBundleRevision;
|
|
105
|
+
/** Reconcile one entry name against the live loader entries. @returns whether the table changed. */
|
|
106
|
+
private processOne;
|
|
107
|
+
private flush;
|
|
108
|
+
private readonly serveBundle;
|
|
109
|
+
}
|
|
110
|
+
export default ClientModuleRegistry;
|
|
111
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-client-modules`.
|
|
3
|
+
* @module @deepseek-ai/dsh-client-modules/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "client-modules-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crazx/dsh-client-modules",
|
|
3
|
+
"description": "Client module system, dual-face: node half composes the __DSH_BOOT__ entry graph (incremental dsh.client scan, bundle route, index tap, webPlugins service); browser half is the lazy-CJS module table the vendored cordis Loader consumes as its internal seam",
|
|
4
|
+
"version": "0.1.0-rc.7.zw.2",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/aka-danielZhang/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/client/modules"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./client": {
|
|
22
|
+
"types": "./lib/types/client/index.d.ts",
|
|
23
|
+
"default": "./lib/client.js"
|
|
24
|
+
},
|
|
25
|
+
"./invariant": {
|
|
26
|
+
"types": "./lib/types/invariant.d.ts",
|
|
27
|
+
"default": "./lib/invariant.js"
|
|
28
|
+
},
|
|
29
|
+
"./src/*": "./src/*",
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"dsh": {
|
|
33
|
+
"client": {
|
|
34
|
+
"platform": "web",
|
|
35
|
+
"inject": [],
|
|
36
|
+
"immediately": true
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@deepseek-ai/cordis-plugin-loader": "^0.1.0-rc.7",
|
|
42
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.7",
|
|
43
|
+
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.7",
|
|
44
|
+
"@deepseek-ai/cordis": "^0.1.0-rc.7"
|
|
45
|
+
},
|
|
46
|
+
"files": [
|
|
47
|
+
"lib/index.js",
|
|
48
|
+
"lib/invariant.js",
|
|
49
|
+
"lib/client.js",
|
|
50
|
+
"lib/types/**/*.d.ts"
|
|
51
|
+
],
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.7",
|
|
54
|
+
"@deepseek-ai/cordis": "^0.1.0-rc.7"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"bundle": "tsdown",
|
|
58
|
+
"watch": "tsdown --watch"
|
|
59
|
+
}
|
|
60
|
+
}
|