dsh-toolfold 0.1.8 → 0.1.10

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/lib/index.js CHANGED
@@ -1,137 +1,395 @@
1
+ import { createRequire } from "node:module";
2
+ import z from "schemastery";
3
+ import { readFileSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+ //#region src/host/version.js
1
6
  /**
2
- * dsh-toolfold — HOST half.
3
- *
4
- * Registers the `toolfold` settings namespace in the DSH settings service
5
- * (persisted in `~/.dsh/settings.yaml`, the same document every product and
6
- * family plugin uses) and serves the browser half through one same-origin
7
- * JSON route:
8
- *
9
- * GET /api/dsh-toolfold/settings → { ok, value: { value, revision, writable } }
10
- * POST /api/dsh-toolfold/settings → body { op: 'set'|'unset', field, value?,
11
- * expectedRevision? }
12
- * → { ok, value: { value, revision, writable } }
13
- *
14
- * The browser half prefers the official `settingsScope` transport when the
15
- * deployment exposes this namespace, then this route, then browser
16
- * localStorage as a degraded fallback. `value` is always the fully resolved
17
- * section (schema defaults + composition base + the user's settings.yaml
18
- * overrides), so the client never re-implements the resolution.
19
- *
20
- * The same package also declares `dsh.bundle.patch` (see cordis.patch.yml),
21
- * which makes `dsh plugin --profile <name> add <this package>` install AND
22
- * mount both halves in one command.
23
- */
24
- import { settingsNamespace } from '@deepseek-ai/dsh-settings'
25
- import z from 'schemastery'
26
-
27
- export const name = 'toolfold'
28
- export const inject = ['settings', 'webServer']
29
-
30
- const NS = settingsNamespace('toolfold')
31
- const API_PATH = '/api/dsh-toolfold/settings'
32
- const FIELDS = ['durMs', 'keepThink', 'splitThink', 'stats']
33
-
7
+ * dsh-toolfold — version-range helpers (host side, zero dependencies).
8
+ *
9
+ * The single source of truth for the supported DSH product range is this
10
+ * package's own `engines.dsh` field (an npm-style range such as
11
+ * ">=0.1.2-rc.1 <0.1.3 || >=0.1.5-rc.1 <0.1.6"). The build stamps that
12
+ * field into the host artifact via tsdown `define` (__DSH_ENGINES__, see
13
+ * tsdown.config.mjs) and the host half judges the running DSH with the
14
+ * matcher below — no range is hardcoded anywhere, so widening support is
15
+ * a one-line package.json edit + rebuild.
16
+ *
17
+ * Supported range grammar (the subset npm engines ranges actually use):
18
+ * range := branch ("||" branch)*
19
+ * branch := comparator ((" " | ",") comparator)*
20
+ * comparator := (">=" | "<=" | ">" | "<" | "=" | "==")? version
21
+ * A bare version means "=". Anything else (caret/tilde/x-ranges, hyphen
22
+ * ranges, junk) invalidates its branch; a range with no valid branch
23
+ * matches nothing and the caller reports 'unknown' instead of crying wolf.
24
+ *
25
+ * Prerelease gating follows npm semver: a prerelease running version only
26
+ * satisfies a branch that names a prerelease on the same
27
+ * [major, minor, patch] tuple (e.g. 0.1.5-rc.1 satisfies
28
+ * ">=0.1.5-rc.1 <0.1.6", but 0.1.3-rc.1 does NOT satisfy
29
+ * ">=0.1.2-rc.1 <0.1.3").
30
+ */
31
+ /** Parse "v1.2.3-rc.4+build" into comparable parts; null on junk. */
32
+ function parseVersion(value) {
33
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(String(value));
34
+ if (!match) return null;
35
+ return {
36
+ core: [
37
+ Number(match[1]),
38
+ Number(match[2]),
39
+ Number(match[3])
40
+ ],
41
+ pre: match[4] === void 0 ? null : match[4].split(".")
42
+ };
43
+ }
44
+ /** Compare two parsed versions (semver precedence, prerelease-aware): -1/0/1. */
45
+ function compareParsed(a, b) {
46
+ for (let i = 0; i < 3; i++) if (a.core[i] !== b.core[i]) return a.core[i] < b.core[i] ? -1 : 1;
47
+ if (a.pre === null && b.pre === null) return 0;
48
+ if (a.pre === null) return 1;
49
+ if (b.pre === null) return -1;
50
+ const len = Math.max(a.pre.length, b.pre.length);
51
+ for (let i = 0; i < len; i++) {
52
+ const x = a.pre[i];
53
+ const y = b.pre[i];
54
+ if (x === void 0) return -1;
55
+ if (y === void 0) return 1;
56
+ const xn = /^\d+$/.test(x);
57
+ const yn = /^\d+$/.test(y);
58
+ if (xn && yn) {
59
+ const dx = Number(x);
60
+ const dy = Number(y);
61
+ if (dx !== dy) return dx < dy ? -1 : 1;
62
+ } else if (xn !== yn) return xn ? -1 : 1;
63
+ else if (x !== y) return x < y ? -1 : 1;
64
+ }
65
+ return 0;
66
+ }
67
+ /**
68
+ * Parse one "||" branch into comparators; null when the branch is unusable.
69
+ * The operator may be separated from its version by whitespace (npm
70
+ * semver tolerates ">= 1.2.3"), so comparators are matched globally and
71
+ * the gaps between matches must contain only whitespace/commas — anything
72
+ * else invalidates the branch.
73
+ */
74
+ function parseBranch(text) {
75
+ const src = String(text).trim();
76
+ if (src === "") return null;
77
+ const re = /(>=|<=|>|<|==|=)?\s*(v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)/g;
78
+ const comps = [];
79
+ let pos = 0;
80
+ let m;
81
+ while ((m = re.exec(src)) !== null) {
82
+ if (m[0] === "") {
83
+ re.lastIndex++;
84
+ continue;
85
+ }
86
+ if (!/^[\s,]*$/.test(src.slice(pos, m.index))) return null;
87
+ const v = parseVersion(m[2]);
88
+ if (v === null) return null;
89
+ comps.push({
90
+ op: m[1] === void 0 || m[1] === "==" ? "=" : m[1],
91
+ v
92
+ });
93
+ pos = m.index + m[0].length;
94
+ }
95
+ if (!/^[\s,]*$/.test(src.slice(pos))) return null;
96
+ return comps.length === 0 ? null : comps;
97
+ }
98
+ /** Parse a full range string into its valid branches; [] when unusable. */
99
+ function parseEnginesRange(rangeStr) {
100
+ if (typeof rangeStr !== "string") return [];
101
+ const branches = [];
102
+ for (const part of rangeStr.split("||")) {
103
+ const comps = parseBranch(part.trim());
104
+ if (comps !== null) branches.push(comps);
105
+ }
106
+ return branches;
107
+ }
108
+ function testComp(op, cmp) {
109
+ switch (op) {
110
+ case ">=": return cmp >= 0;
111
+ case "<=": return cmp <= 0;
112
+ case ">": return cmp > 0;
113
+ case "<": return cmp < 0;
114
+ default: return cmp === 0;
115
+ }
116
+ }
117
+ function sameCore(a, b) {
118
+ return a.core[0] === b.core[0] && a.core[1] === b.core[1] && a.core[2] === b.core[2];
119
+ }
120
+ /** npm semver prerelease gate for one branch. */
121
+ function gateOk(comps, ver) {
122
+ if (ver.pre === null) return true;
123
+ return comps.some((c) => c.v.pre !== null && sameCore(c.v, ver));
124
+ }
125
+ /** True when the parsed version satisfies at least one branch. */
126
+ function satisfiesEngines(parsed, branches) {
127
+ for (const comps of branches) {
128
+ let ok = true;
129
+ for (const c of comps) if (!testComp(c.op, compareParsed(parsed, c.v))) {
130
+ ok = false;
131
+ break;
132
+ }
133
+ if (ok && gateOk(comps, parsed)) return true;
134
+ }
135
+ return false;
136
+ }
137
+ /** Lowest lower bound across all branches (>=, >, =); null when unbounded. */
138
+ function lowestFloor(branches) {
139
+ let floor = null;
140
+ for (const comps of branches) for (const c of comps) if (c.op === ">=" || c.op === ">" || c.op === "=") {
141
+ if (floor === null || compareParsed(c.v, floor) < 0) floor = c.v;
142
+ }
143
+ return floor;
144
+ }
145
+ /**
146
+ * Judge a running version against a raw engines range string:
147
+ * 'ok' | 'old' | 'new' | 'unknown'. Never throws. Below every supported
148
+ * floor → 'old'; outside anywhere else (including between "||" gaps) →
149
+ * 'new'; unusable input on either side → 'unknown' (fail silent, never
150
+ * false-alarm).
151
+ */
152
+ function compatFor(version, rangeStr) {
153
+ const parsed = parseVersion(version);
154
+ if (parsed === null) return "unknown";
155
+ const branches = parseEnginesRange(rangeStr);
156
+ if (branches.length === 0) return "unknown";
157
+ if (satisfiesEngines(parsed, branches)) return "ok";
158
+ const floor = lowestFloor(branches);
159
+ if (floor !== null && compareParsed(parsed, floor) < 0) return "old";
160
+ return "new";
161
+ }
162
+ //#endregion
163
+ //#region src/host/index.js
164
+ /**
165
+ * dsh-toolfold — HOST half.
166
+ *
167
+ * Registers the `toolfold` settings namespace in the DSH settings service
168
+ * (persisted in `~/.dsh/settings.yaml`, the same document every product and
169
+ * family plugin uses) and serves the browser half through one same-origin
170
+ * JSON route:
171
+ *
172
+ * GET /api/dsh-toolfold/settings → { ok, value: { value, revision, writable } }
173
+ * POST /api/dsh-toolfold/settings → body { op: 'set'|'unset', field, value?,
174
+ * expectedRevision? }
175
+ * → { ok, value: { value, revision, writable } }
176
+ *
177
+ * Every success response additionally carries `dsh: { version, state, range }` —
178
+ * the running DSH product version, its compatibility with the supported
179
+ * range ('ok' | 'old' | 'new' | 'unknown'), and the raw `engines.dsh`
180
+ * requirement the verdict was judged against. DSH does not enforce
181
+ * `engines.dsh` anywhere, so the host half reports the mismatch itself
182
+ * and the settings card warns the user (once-only console warning +
183
+ * hover-tip icon quoting the live range).
184
+ *
185
+ * The browser half prefers the official `settingsScope` transport when the
186
+ * deployment exposes this namespace, then this route, then browser
187
+ * localStorage as a degraded fallback. `value` is always the fully resolved
188
+ * section (schema defaults + composition base + the user's settings.yaml
189
+ * overrides), so the client never re-implements the resolution.
190
+ *
191
+ * The same package also declares `dsh.bundle.patch` (see cordis.patch.yml),
192
+ * which makes `dsh plugin --profile <name> add <this package>` install AND
193
+ * mount both halves in one command.
194
+ */
195
+ const name = "toolfold";
196
+ const inject = ["settings", "webServer"];
197
+ const NS = "toolfold";
198
+ const API_PATH = "/api/dsh-toolfold/settings";
199
+ const FIELDS = [
200
+ "enabled",
201
+ "durMs",
202
+ "thinkMode",
203
+ "keepThink",
204
+ "thinkAuto",
205
+ "splitThink",
206
+ "stats"
207
+ ];
34
208
  const SCHEMA = z.object({
35
- durMs: z.number().step(10).min(0).max(2000).default(240),
36
- keepThink: z.boolean().default(false),
37
- splitThink: z.boolean().default(true),
38
- stats: z.boolean().default(false),
39
- })
40
-
209
+ enabled: z.boolean().default(true),
210
+ durMs: z.number().step(10).min(0).max(2e3).default(240),
211
+ thinkMode: z.union([
212
+ z.const("auto"),
213
+ z.const("keep"),
214
+ z.const("hide")
215
+ ]),
216
+ keepThink: z.boolean().default(false),
217
+ thinkAuto: z.boolean().default(true),
218
+ splitThink: z.boolean().default(true),
219
+ stats: z.boolean().default(false)
220
+ });
221
+ /**
222
+ * Resolve the canonical think mode for one section. An explicitly set,
223
+ * valid thinkMode wins; otherwise derive it from the deprecated keys so
224
+ * pre-merge preferences survive the upgrade; default 'auto'. Total —
225
+ * never throws. (Client twin: resolveThinkMode in src/client/settings.js.)
226
+ */
227
+ function resolveThinkMode(section) {
228
+ var mode = section !== void 0 && section !== null ? section.thinkMode : void 0;
229
+ if (mode === "keep" || mode === "hide" || mode === "auto") return mode;
230
+ if (section !== void 0 && section !== null && section.thinkAuto === false) return section.keepThink === true ? "keep" : "hide";
231
+ return "auto";
232
+ }
233
+ const ENGINES_RANGE = ">=0.1.2-rc.1 <0.1.3 || >=0.1.5-rc.1 <0.1.6";
234
+ /**
235
+ * The @deepseek-ai/dsh version this process was launched from: process.argv[1]
236
+ * is the CLI's entry, and module resolution from that file reaches the
237
+ * @deepseek-ai/dsh package.json it belongs to. Null when undetectable
238
+ * (workers, dev launchers without a resolvable package).
239
+ */
240
+ let cachedDshVersion;
241
+ function dshVersion() {
242
+ if (cachedDshVersion !== void 0) return cachedDshVersion;
243
+ cachedDshVersion = null;
244
+ try {
245
+ const entry = process.argv && process.argv[1];
246
+ if (!entry) return cachedDshVersion;
247
+ const pkgPath = createRequire(resolve(entry)).resolve("@deepseek-ai/dsh/package.json");
248
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
249
+ if (pkg && pkg.name === "@deepseek-ai/dsh" && typeof pkg.version === "string" && pkg.version !== "") cachedDshVersion = pkg.version;
250
+ } catch (error) {}
251
+ return cachedDshVersion;
252
+ }
253
+ /**
254
+ * Running DSH vs the supported range (see ./version.js): the report the
255
+ * settings card renders its warning icon from. `range` is the raw
256
+ * `engines.dsh` string so the card can quote the live requirement instead
257
+ * of hardcoding it: { version, state: 'ok'|'old'|'new'|'unknown', range }.
258
+ */
259
+ function dshCompat() {
260
+ const version = dshVersion();
261
+ const range = ENGINES_RANGE;
262
+ if (version === null || false) return {
263
+ version,
264
+ state: "unknown",
265
+ range
266
+ };
267
+ return {
268
+ version,
269
+ state: compatFor(version, range),
270
+ range
271
+ };
272
+ }
41
273
  /** Write one JSON response. */
42
274
  function json(res, status, body) {
43
- res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
44
- res.end(JSON.stringify(body))
275
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
276
+ res.end(JSON.stringify(body));
45
277
  }
46
-
47
278
  /** Read a JSON request body (bounded). */
48
279
  function readJsonBody(req) {
49
- return new Promise((resolve, reject) => {
50
- let size = 0
51
- const chunks = []
52
- req.on('data', (chunk) => {
53
- size += chunk.length
54
- if (size > 64 * 1024) {
55
- reject(new Error('body-too-large'))
56
- req.destroy()
57
- return
58
- }
59
- chunks.push(chunk)
60
- })
61
- req.on('end', () => {
62
- if (chunks.length === 0) {
63
- resolve({})
64
- return
65
- }
66
- try {
67
- resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')))
68
- } catch {
69
- reject(new Error('invalid-json'))
70
- }
71
- })
72
- req.on('error', reject)
73
- })
280
+ return new Promise((resolve, reject) => {
281
+ let size = 0;
282
+ const chunks = [];
283
+ req.on("data", (chunk) => {
284
+ size += chunk.length;
285
+ if (size > 65536) {
286
+ reject(/* @__PURE__ */ new Error("body-too-large"));
287
+ req.destroy();
288
+ return;
289
+ }
290
+ chunks.push(chunk);
291
+ });
292
+ req.on("end", () => {
293
+ if (chunks.length === 0) {
294
+ resolve({});
295
+ return;
296
+ }
297
+ try {
298
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
299
+ } catch {
300
+ reject(/* @__PURE__ */ new Error("invalid-json"));
301
+ }
302
+ });
303
+ req.on("error", reject);
304
+ });
74
305
  }
75
-
76
- export function apply(ctx) {
77
- // Fiber-scoped registration: removed when this plugin is stopped/removed.
78
- ctx.settings.register(NS, SCHEMA, { base: {} })
79
-
80
- /** Current resolved section + revision + writability, as one JSON view. */
81
- const snapshot = () => {
82
- let value = ctx.settings.get(NS)
83
- let revision
84
- for (const descriptor of ctx.settings.describe()) {
85
- if (descriptor.ns === NS) {
86
- value = descriptor.value
87
- revision = descriptor.revision
88
- break
89
- }
90
- }
91
- return {
92
- ok: true,
93
- value: {
94
- value,
95
- ...(revision === undefined ? {} : { revision }),
96
- writable: ctx.settings.writable,
97
- },
98
- }
99
- }
100
-
101
- const handler = (req, res) => {
102
- if (req.method === 'GET') {
103
- try {
104
- json(res, 200, snapshot())
105
- } catch (error) {
106
- json(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) })
107
- }
108
- return
109
- }
110
- if (req.method === 'POST') {
111
- readJsonBody(req).then((body) => {
112
- const field = String(body?.field ?? '')
113
- if (!FIELDS.includes(field)) {
114
- json(res, 400, { ok: false, error: 'invalid-field' })
115
- return
116
- }
117
- const op = body?.op === 'unset' ? 'unset' : 'set'
118
- const ops = op === 'unset'
119
- ? [{ op: 'unset', path: [field] }]
120
- : [{ op: 'set', path: [field], value: body.value }]
121
- const expectedRevision = typeof body?.expectedRevision === 'number' ? body.expectedRevision : undefined
122
- return ctx.settings.mutate(NS, ops, expectedRevision)
123
- .then(() => json(res, 200, snapshot()))
124
- .catch((error) => json(res, 400, { ok: false, error: error instanceof Error ? error.message : String(error) }))
125
- }, (error) => {
126
- json(res, 400, { ok: false, error: error instanceof Error ? error.message : String(error) })
127
- })
128
- return
129
- }
130
- json(res, 405, { ok: false, error: 'method-not-allowed' })
131
- }
132
-
133
- ctx.effect(() => {
134
- const dispose = ctx.webServer.register({ kind: 'exact', path: API_PATH, handler })
135
- return () => dispose()
136
- })
306
+ function apply(ctx) {
307
+ ctx.settings.register(NS, SCHEMA, { base: {} });
308
+ /** Current resolved section + revision + writability, as one JSON view. */
309
+ const snapshot = () => {
310
+ let value = ctx.settings.get(NS);
311
+ let revision;
312
+ for (const descriptor of ctx.settings.describe()) if (descriptor.ns === NS) {
313
+ value = descriptor.value;
314
+ revision = descriptor.revision;
315
+ break;
316
+ }
317
+ return {
318
+ ok: true,
319
+ value: {
320
+ value: {
321
+ ...value,
322
+ thinkMode: resolveThinkMode(value)
323
+ },
324
+ ...revision === void 0 ? {} : { revision },
325
+ writable: ctx.settings.writable
326
+ }
327
+ };
328
+ };
329
+ const handler = (req, res) => {
330
+ if (req.method === "GET") {
331
+ try {
332
+ json(res, 200, {
333
+ ...snapshot(),
334
+ dsh: dshCompat()
335
+ });
336
+ } catch (error) {
337
+ json(res, 500, {
338
+ ok: false,
339
+ error: error instanceof Error ? error.message : String(error)
340
+ });
341
+ }
342
+ return;
343
+ }
344
+ if (req.method === "POST") {
345
+ readJsonBody(req).then((body) => {
346
+ const field = String(body?.field ?? "");
347
+ if (!FIELDS.includes(field)) {
348
+ json(res, 400, {
349
+ ok: false,
350
+ error: "invalid-field"
351
+ });
352
+ return;
353
+ }
354
+ const ops = (body?.op === "unset" ? "unset" : "set") === "unset" ? [{
355
+ op: "unset",
356
+ path: [field]
357
+ }] : [{
358
+ op: "set",
359
+ path: [field],
360
+ value: body.value
361
+ }];
362
+ const expectedRevision = typeof body?.expectedRevision === "number" ? body.expectedRevision : void 0;
363
+ return ctx.settings.mutate(NS, ops, expectedRevision).then(() => json(res, 200, {
364
+ ...snapshot(),
365
+ dsh: dshCompat()
366
+ })).catch((error) => json(res, 400, {
367
+ ok: false,
368
+ error: error instanceof Error ? error.message : String(error)
369
+ }));
370
+ }, (error) => {
371
+ json(res, 400, {
372
+ ok: false,
373
+ error: error instanceof Error ? error.message : String(error)
374
+ });
375
+ });
376
+ return;
377
+ }
378
+ json(res, 405, {
379
+ ok: false,
380
+ error: "method-not-allowed"
381
+ });
382
+ };
383
+ ctx.effect(() => {
384
+ const dispose = ctx.webServer.register({
385
+ kind: "exact",
386
+ path: API_PATH,
387
+ handler
388
+ });
389
+ return () => dispose();
390
+ });
137
391
  }
392
+ //#endregion
393
+ export { apply, inject, name };
394
+
395
+ //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-toolfold",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "工具调用与思考的折叠显示:连续工具调用折叠为最后一个调用(可展开),已完成的思考随组折叠(可保留、展开时按原顺序显示),进行中的思考保持独立显示。",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,20 +27,31 @@
27
27
  }
28
28
  },
29
29
  "files": [
30
- "lib",
30
+ "lib/index.js",
31
+ "lib/client.js",
31
32
  "cordis.patch.yml",
32
33
  "scripts",
33
34
  "README.md",
34
35
  "README.en.md",
35
36
  "CHANGELOG.md"
36
37
  ],
38
+ "engines": {
39
+ "dsh": ">=0.1.2-rc.1 <0.1.3 || >=0.1.5-rc.1 <0.1.6"
40
+ },
37
41
  "peerDependencies": {
38
- "@deepseek-ai/dsh-settings": ">=0.1.0-rc.7 <0.1.1 || >=0.1.1-rc.0 <0.1.2",
39
42
  "schemastery": "^3.18.0"
40
43
  },
44
+ "devDependencies": {
45
+ "jsdom": "29.1.1",
46
+ "playwright": "1.61.1",
47
+ "tsdown": "^0.22.2"
48
+ },
41
49
  "license": "MIT",
42
50
  "scripts": {
51
+ "build": "tsdown",
52
+ "build:watch": "tsdown --watch",
43
53
  "test:engine": "node tools/engine-smoke.mjs",
54
+ "test:version": "node tools/version-smoke.mjs",
44
55
  "probe:live": "node tools/live-probe.mjs",
45
56
  "install:dsh": "node scripts/install-dsh.cjs",
46
57
  "uninstall:dsh": "node scripts/install-dsh.cjs uninstall"
@@ -1,69 +0,0 @@
1
- /**
2
- * build-dynamic.js — generate lib/dynamic-body.js (the `code.client` body for
3
- * cordis_define) from lib/client.js.
4
- *
5
- * The dynamic runner wraps the body in `async () => { ... }` and evaluates it
6
- * in a scope where `React` is a closure parameter, so the body must:
7
- * - NOT redeclare `var React` (var hoisting would shadow the closure
8
- * parameter and the settings card would silently never render);
9
- * - end with `return plugin;` (a bare function body has no exports).
10
- *
11
- * Usage: node lib/build-dynamic.js
12
- */
13
- 'use strict';
14
-
15
- const fs = require('fs');
16
- const path = require('path');
17
-
18
- const srcPath = path.join(__dirname, 'client.js');
19
- const outPath = path.join(__dirname, 'dynamic-body.js');
20
-
21
- let src = fs.readFileSync(srcPath, 'utf8');
22
-
23
- // 1. Extract the factory function body: everything between
24
- // "factory: function (require) {" and the final "\n});".
25
- const factoryMarker = 'factory: function (require) {';
26
- const start = src.indexOf(factoryMarker);
27
- if (start === -1) throw new Error('factory marker not found in client.js');
28
- const bodyStart = start + factoryMarker.length;
29
-
30
- const endMarker = '\n});';
31
- const end = src.lastIndexOf(endMarker);
32
- if (end === -1) throw new Error('module end marker not found in client.js');
33
-
34
- let body = src.slice(bodyStart, end);
35
- // Drop the factory's own closing brace (the trailing " }").
36
- body = body.replace(/\n[ \t]*\}\s*$/, '');
37
-
38
- // 2. Remove the CommonJS export boilerplate.
39
- const boilerplate =
40
- ' var module = { exports: {} };\n' +
41
- ' var exports = module.exports;\n' +
42
- ' Object.defineProperty(exports, Symbol.toStringTag, { value: \'Module\' });\n';
43
- if (body.indexOf(boilerplate) === -1) throw new Error('module boilerplate not found');
44
- body = body.replace(boilerplate, '');
45
-
46
- // 3. Replace the React require block with an explanatory comment (see header).
47
- const reactBlock =
48
- ' // React is a platform seed word in the web shell; the settings card\n' +
49
- ' // needs it, the folding engine does not.\n' +
50
- ' var React = null;\n' +
51
- ' try {\n' +
52
- ' React = require(\'react\');\n' +
53
- ' } catch (err) {\n' +
54
- ' React = null;\n' +
55
- ' }\n';
56
- if (body.indexOf(reactBlock) === -1) throw new Error('React block not found');
57
- body = body.replace(reactBlock,
58
- ' // React is the runner\'s closure parameter; the folding engine does not\n' +
59
- ' // need it directly, and redeclaring it here would shadow the parameter.\n');
60
-
61
- // 4. Replace the module.exports tail with a bare `return plugin;`.
62
- const tail =
63
- ' module.exports = plugin;\n' +
64
- ' return module.exports;';
65
- if (body.indexOf(tail) === -1) throw new Error('export tail not found');
66
- body = body.replace(tail, 'return plugin;\n');
67
-
68
- fs.writeFileSync(outPath, body, 'utf8');
69
- console.log('wrote', outPath, body.length, 'bytes');