@deepseek-ai/dsh-experimental-webworker-packer 0.1.6-alpha.1

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.
@@ -0,0 +1,759 @@
1
+ import { DEFAULT_ROOT, DEFAULT_ROOT as DEFAULT_ROOT$1, IMAGE_CONFIG_PATH, IMAGE_EMPTY_DIRECTORIES, IMAGE_MANIFEST_PATH, IMAGE_OVERLAY_DIRECTORIES, LOWERING_VERSION, MODULE_PROXIES, MODULE_PROXY_PREFIXES, MemoryVfs, REPLACED_EXTERNAL_PACKAGES, WorkerModuleLoader, lowerModuleSource, packTar } from "@deepseek-ai/dsh-experimental-webworker-runtime";
2
+ import { existsSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync } from "node:fs";
3
+ import { dirname, join, relative } from "node:path";
4
+ import { gzipSync } from "node:zlib";
5
+ import picomatch from "picomatch";
6
+ import yaml from "js-yaml";
7
+ import { entryListSchema } from "@deepseek-ai/cordis-plugin-include";
8
+ import { execFileSync } from "node:child_process";
9
+ import { tmpdir } from "node:os";
10
+ import { DSH_HOME_ENV } from "@deepseek-ai/dsh-home-paths";
11
+ //#region lib/types/transform-image.js
12
+ /**
13
+ * The wrapper contract packed bodies are emitted against, and the image-entry
14
+ * types the pack pass consumes.
15
+ *
16
+ * One transform serves both sides — the pack pass lowers with the runtime's
17
+ * own `lowerModuleSource`, never a reimplementation — and the image records
18
+ * the contract version it was lowered against. Bodies emitted against a
19
+ * different wrapper contract are refused at mount time rather than
20
+ * half-working at run time.
21
+ * @module @deepseek-ai/dsh-experimental-webworker-packer/src/transform-image
22
+ */
23
+ /** Wrapper contract the packed bodies are emitted against. */
24
+ const WRAPPER_CONTRACT = LOWERING_VERSION;
25
+ //#endregion
26
+ //#region lib/types/rules.js
27
+ /**
28
+ * Pack rule tables: the one place the image's include/exclude decisions live.
29
+ * Patterns are picomatch globs. Exclude patterns match tree-root-relative
30
+ * paths (so `src/**` drops only a root-level source tree), page-asset
31
+ * patterns match image paths. Traversal mechanics — nested `node_modules`
32
+ * flattening and dot-directory pruning — stay in the collector; these tables
33
+ * hold the judgement calls.
34
+ */
35
+ /**
36
+ * Paths dropped from every collected tree. Test trees, sourcemaps,
37
+ * declarations, and archives never resolve at runtime while dominating the
38
+ * byte count. Third-party `src/` directories remain eligible because package
39
+ * entrypoints may resolve to JavaScript there.
40
+ */
41
+ const EXCLUDE = [
42
+ "tests/**",
43
+ "test/**",
44
+ "__tests__/**",
45
+ "coverage/**",
46
+ "**/*.map",
47
+ "**/*.tsbuildinfo",
48
+ "**/*.tgz",
49
+ "**/*.tar",
50
+ "**/*.tar.gz",
51
+ "**/*.d.ts",
52
+ "**/*.d.mts",
53
+ "**/*.d.cts"
54
+ ];
55
+ /**
56
+ * Additional paths dropped from workspace and vendored packages only. Their
57
+ * runtime plane is built `lib/`; a workspace `dist/` is a page-asset tree the
58
+ * static deployment serves itself. External packages may place runtime code
59
+ * under either directory.
60
+ */
61
+ const EXCLUDE_WORKSPACE = ["src/**", "dist/**"];
62
+ /**
63
+ * Image paths that belong to the PAGE, not to the worker's loader.
64
+ *
65
+ * A package's `lib/client.js` is its browser bundle behind the `./client`
66
+ * export: the page's own module system evaluates it with its own wrapper,
67
+ * which has no ambient-store parameter. Transforming those bodies would
68
+ * inject calls the page cannot resolve, so they ship untransformed — their
69
+ * only change is the trailing debugger-name line every JavaScript entry
70
+ * gains — and the manifest's all-or-nothing claim stays true, because the
71
+ * worker loader never evaluates them (the tunnel serves them as bytes).
72
+ */
73
+ const PAGE_ASSETS = ["node_modules/*/lib/client.js", "node_modules/@*/*/lib/client.js"];
74
+ /**
75
+ * Image specifiers the worker assembly requires directly, beyond the composed
76
+ * roster: they are requested by worker-bundle code, so no image file
77
+ * references them and the reachability sweep must seed them as roots. Keep in
78
+ * step with the literal `require`/`resolve` calls in the runtime's
79
+ * `worker-host.ts`.
80
+ */
81
+ const IMAGE_ENTRY_SEEDS = [
82
+ "@deepseek-ai/dsh-app-boot",
83
+ "@deepseek-ai/dsh-cmdline",
84
+ "@deepseek-ai/cordis",
85
+ "@deepseek-ai/cordis-plugin-include",
86
+ "js-yaml"
87
+ ];
88
+ //#endregion
89
+ //#region lib/types/pack.js
90
+ /**
91
+ * VFS image packer: turns one composed profile plus a package index into the single
92
+ * gzip-compressed tar the browser runtime inflates and mounts as its filesystem.
93
+ *
94
+ * Nothing is compiled here. The image carries the repository's real build products,
95
+ * so a preview deployment debugs exactly what the served deployment ships. What the
96
+ * pass does add is the pack-time module transform and the manifest that records the
97
+ * wrapper contract it was transformed against.
98
+ *
99
+ * This module holds no repository knowledge: paths, globs, and the composition come
100
+ * in as parameters, so the same library packs a different tree by being called
101
+ * differently. Locating those inputs is the CLI's job.
102
+ * @module @deepseek-ai/dsh-experimental-webworker-packer/src/pack
103
+ */
104
+ /** Image path of the manifest; the layout contract's name, re-exported for callers. */
105
+ const MANIFEST_PATH = IMAGE_MANIFEST_PATH;
106
+ /** Image path of the composed profile; the layout contract's name, re-exported for callers. */
107
+ const CONFIG_PATH = IMAGE_CONFIG_PATH;
108
+ /**
109
+ * Manifest field the runtime judges the image by: the wrapper contract every packed
110
+ * body was emitted against. The runtime refuses an image whose value is not its own
111
+ * contract, because those bodies assume different wrapper semantics.
112
+ */
113
+ const CONTRACT_FIELD = "lowered";
114
+ /** Exclude matcher over tree-root-relative paths ({@link EXCLUDE}). */
115
+ const excluded = picomatch([...EXCLUDE], { dot: true });
116
+ /** Workspace exclude matcher: {@link EXCLUDE} plus {@link EXCLUDE_WORKSPACE}. */
117
+ const workspaceExcluded = picomatch([...EXCLUDE, ...EXCLUDE_WORKSPACE], { dot: true });
118
+ /** Page-asset matcher over image paths ({@link PAGE_ASSETS}). */
119
+ const pageAsset = picomatch([...PAGE_ASSETS], { dot: true });
120
+ const readJson = (file) => JSON.parse(readFileSync(file, "utf8"));
121
+ /**
122
+ * Package name of a module specifier.
123
+ * @param specifier - Module specifier, possibly with a subpath.
124
+ * @returns The package name (`@scope/pkg/sub` → `@scope/pkg`).
125
+ */
126
+ function packageNameOf(specifier) {
127
+ const [first = specifier, second = ""] = specifier.split("/");
128
+ return first.startsWith("@") ? `${first}/${second}` : first;
129
+ }
130
+ /**
131
+ * Collect module-specifier `name` fields from parsed entry rows, recursively
132
+ * through nested `config` row lists (groups). Builtin rows (`cordis:group`)
133
+ * and preset metadata documents carry names that are not module specifiers;
134
+ * only names with a scope or a path separator count.
135
+ * @param rows - Parsed YAML value; anything but an entry array is ignored.
136
+ * @param names - Package names collected so far.
137
+ */
138
+ function moduleNamesOf(rows, names) {
139
+ if (!Array.isArray(rows)) return;
140
+ for (const row of rows) {
141
+ if (typeof row !== "object" || row === null) continue;
142
+ const { name, config } = row;
143
+ if (typeof name === "string" && (name.startsWith("@") || name.includes("/"))) names.add(packageNameOf(name));
144
+ moduleNamesOf(config, names);
145
+ }
146
+ }
147
+ /**
148
+ * Package names the composition names.
149
+ * @param config - Composed profile; `!!js` scalars parse under Include's dialect.
150
+ * @returns Package names, deduplicated.
151
+ */
152
+ function rosterOf(config) {
153
+ const names = /* @__PURE__ */ new Set();
154
+ moduleNamesOf(yaml.load(config, { schema: entryListSchema }), names);
155
+ return [...names];
156
+ }
157
+ /**
158
+ * Package names the compositions under one config tree name.
159
+ * @param root - Directory to walk.
160
+ * @returns Package names, deduplicated.
161
+ */
162
+ function treeRosterOf(root) {
163
+ const names = /* @__PURE__ */ new Set();
164
+ const walk = (directory) => {
165
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
166
+ const absolute = join(directory, entry.name);
167
+ if (entry.isDirectory()) {
168
+ walk(absolute);
169
+ continue;
170
+ }
171
+ if (!entry.name.endsWith(".yml") && !entry.name.endsWith(".yaml")) continue;
172
+ moduleNamesOf(yaml.load(readFileSync(absolute, "utf8"), { schema: entryListSchema }), names);
173
+ }
174
+ };
175
+ walk(root);
176
+ return [...names];
177
+ }
178
+ /**
179
+ * Resolve one dependency the way Node does: walk up from the importer.
180
+ * @param fromDirectory - Directory to start at.
181
+ * @param name - Package name.
182
+ * @returns The real path of the package directory, or undefined.
183
+ */
184
+ function resolveDependency(fromDirectory, name) {
185
+ let directory = fromDirectory;
186
+ for (;;) {
187
+ const candidate = join(directory, "node_modules", name);
188
+ if (existsSync(join(candidate, "package.json"))) return realpathSync(candidate);
189
+ const parent = dirname(directory);
190
+ if (parent === directory) return void 0;
191
+ directory = parent;
192
+ }
193
+ }
194
+ /**
195
+ * Collect files under one directory. Traversal mechanics live here — nested
196
+ * package/config collection flattens nested `node_modules` and prunes dot
197
+ * directories, while seed collection preserves every directory. Every file
198
+ * judgement comes in through `keep` (the {@link EXCLUDE} tables and the npm
199
+ * publish view, or an unconditional seed predicate).
200
+ * @param root - Source directory.
201
+ * @param into - Image entries to add to.
202
+ * @param prefix - Image path prefix.
203
+ * @param keep - Filter over root-relative paths.
204
+ * @param preserveDirectories - Whether dot directories and nested `node_modules`
205
+ * are ordinary fixture content rather than package-manager residue.
206
+ */
207
+ function collectTree(root, into, prefix, keep, preserveDirectories = false) {
208
+ const walk = (directory) => {
209
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
210
+ if (entry.isDirectory()) {
211
+ if (!preserveDirectories && (entry.name === "node_modules" || entry.name.startsWith("."))) continue;
212
+ walk(join(directory, entry.name));
213
+ continue;
214
+ }
215
+ if (!entry.isFile()) continue;
216
+ const absolute = join(directory, entry.name);
217
+ const relativePath = relative(root, absolute).replaceAll("\\", "/");
218
+ if (!keep(relativePath)) continue;
219
+ into[`${prefix}/${relativePath}`] = readFileSync(absolute);
220
+ }
221
+ };
222
+ walk(root);
223
+ }
224
+ /**
225
+ * Predicate for npm's `files` allowlist, with standard glob semantics
226
+ * (picomatch). A pattern admits the path itself and everything under it, so a
227
+ * bare directory name publishes its whole tree; `!` patterns subtract from the
228
+ * admitted set; package.json is always published.
229
+ * @param patterns - The package.json `files` array.
230
+ * @returns Predicate over package-root-relative paths.
231
+ */
232
+ function publishedFilter(patterns) {
233
+ const strings = patterns.filter((pattern) => typeof pattern === "string");
234
+ const normalize = (pattern) => pattern.replace(/^\.\//, "").replace(/\/+$/, "");
235
+ const widen = (pattern) => [pattern, `${pattern}/**`];
236
+ const positive = strings.filter((pattern) => !pattern.startsWith("!")).map(normalize).flatMap(widen);
237
+ const negative = strings.filter((pattern) => pattern.startsWith("!")).map((pattern) => normalize(pattern.slice(1))).flatMap(widen);
238
+ const admits = picomatch(positive, { dot: true });
239
+ const denies = negative.length > 0 ? picomatch(negative, { dot: true }) : () => false;
240
+ return (path) => path === "package.json" || admits(path) && !denies(path);
241
+ }
242
+ /**
243
+ * Keep only the JavaScript the worker can reach, transforming it on the way.
244
+ *
245
+ * Roots are the export faces of every materialized workspace and vendored
246
+ * package — the harness addresses them by constructed name at runtime (Loader
247
+ * rows, typert faces, delegating providers such as `-auto` pickers), so the
248
+ * sweep prunes files only inside third-party packages — plus the worker
249
+ * assembly's own image entries. Resolution runs the runtime loader's own
250
+ * algorithm over the candidate set, so pack-time reachability and boot-time
251
+ * resolution cannot drift, and a request that resolves nowhere — an undeclared
252
+ * or missing dependency — fails the pack rather than the boot.
253
+ *
254
+ * Two entry classes stay out of the walk by rule: page assets
255
+ * ({@link PAGE_ASSETS}) are evaluated by the page's module system, and
256
+ * non-JavaScript entries always stay because data reads go through fs paths
257
+ * this pass cannot see.
258
+ * @param files - Candidate entries after the publish-view filter.
259
+ * @param options - Pack options carrying the sweep roots.
260
+ * @param rootPackages - Roster package names from the workspace.
261
+ * @param root - Virtual root the candidates mount under.
262
+ * @returns The final entries plus the sweep's counts.
263
+ */
264
+ /** Trailing `sourceMappingURL` comment; the image carries no `.map` files. */
265
+ const DANGLING_SOURCE_MAP = /\n\/\/# sourceMappingURL=\S+\s*$/;
266
+ /**
267
+ * Name one JavaScript entry for the debugger: append the `sourceURL` magic
268
+ * comment V8 stacks and DevTools read, so the entry shows under its
269
+ * repository path instead of as an anonymous VM script (worker `new Function`
270
+ * bodies) or blob entry (page bundles). A trailing `sourceMappingURL` comment
271
+ * is stripped first — its `.map` never ships, and once the script has a name
272
+ * the debugger would resolve the reference against it and report a load
273
+ * failure per script. Only the final line is touched, so every other line
274
+ * keeps its number; evaluation cost stays at pack time, where the names are
275
+ * already deterministic.
276
+ * @param bytes - Entry body as the image would otherwise hold it.
277
+ * @param name - Debugger name for the entry.
278
+ * @param decoder - Shared UTF-8 decoder.
279
+ * @param encoder - Shared UTF-8 encoder.
280
+ * @returns The named body.
281
+ */
282
+ function nameForDebugger(bytes, name, decoder, encoder) {
283
+ const source = decoder.decode(bytes).replace(DANGLING_SOURCE_MAP, "\n");
284
+ return encoder.encode(`${source}\n//# sourceURL=${name}`);
285
+ }
286
+ /**
287
+ * Debugger names for image entries: a workspace or vendored package file is
288
+ * named by its repository path (`packages/<group>/<pkg>/lib/index.js`), the
289
+ * shape a reader navigates; an external package file keeps its image key —
290
+ * it has no repository path, and its pnpm store path would name a hash.
291
+ * @param workspaces - Package name → absolute repository directory.
292
+ * @param resolveFrom - Repository root the names are relative to.
293
+ * @returns Mapper from an image key to the entry's debugger name.
294
+ */
295
+ function debuggerNamer(workspaces, resolveFrom) {
296
+ const repoDirs = new Map([...workspaces].map(([name, directory]) => [name, relative(resolveFrom, directory).replaceAll("\\", "/")]));
297
+ return (key) => {
298
+ if (!key.startsWith("node_modules/")) return key;
299
+ const rest = key.slice(13);
300
+ const segments = rest.split("/");
301
+ const packageName = segments[0]?.startsWith("@") === true ? segments.slice(0, 2).join("/") : segments[0] ?? "";
302
+ const directory = repoDirs.get(packageName);
303
+ return directory === void 0 ? key : `${directory}${rest.slice(packageName.length)}`;
304
+ };
305
+ }
306
+ function sweepImage(files, options, rootPackages, root) {
307
+ const decoder = new TextDecoder();
308
+ const encoder = new TextEncoder();
309
+ const vfs = new MemoryVfs();
310
+ for (const [name, bytes] of Object.entries(files)) if (name.endsWith("/")) vfs.seedDirectory(`${root}/${name}`);
311
+ else vfs.seed(`${root}/${name}`, bytes);
312
+ const stub = () => ({});
313
+ const loader = new WorkerModuleLoader({
314
+ vfs,
315
+ root,
316
+ staticModules: Object.fromEntries(Object.keys(MODULE_PROXIES).map((name) => [name, stub])),
317
+ staticModulePrefixes: Object.fromEntries(Object.keys(MODULE_PROXY_PREFIXES).map((name) => [name, stub]))
318
+ });
319
+ const queue = (options.entries ?? IMAGE_ENTRY_SEEDS).map((specifier) => ({
320
+ specifier,
321
+ from: root,
322
+ importer: "worker assembly entry"
323
+ }));
324
+ for (const name of rootPackages) {
325
+ const manifestBytes = files[`node_modules/${name}/package.json`];
326
+ if (manifestBytes === void 0) continue;
327
+ let manifest;
328
+ try {
329
+ manifest = JSON.parse(decoder.decode(manifestBytes));
330
+ } catch {
331
+ continue;
332
+ }
333
+ const subpaths = manifest.exports === void 0 ? ["."] : Object.keys(manifest.exports).filter((key) => key.startsWith(".") && !key.includes("*"));
334
+ for (const subpath of subpaths) queue.push({
335
+ specifier: subpath === "." ? name : `${name}/${subpath.slice(2)}`,
336
+ from: root,
337
+ importer: `workspace face ${name}`
338
+ });
339
+ }
340
+ const reached = /* @__PURE__ */ new Map();
341
+ const seen = /* @__PURE__ */ new Set();
342
+ const failures = [];
343
+ const tolerated = /* @__PURE__ */ new Set();
344
+ let visited = 0;
345
+ let rewritten = 0;
346
+ for (let entry = queue.shift(); entry !== void 0; entry = queue.shift()) {
347
+ const { specifier, from, importer } = entry;
348
+ let resolution;
349
+ try {
350
+ resolution = loader.resolve(specifier, from);
351
+ } catch (reason) {
352
+ if (importer.startsWith("node_modules/") && !importer.startsWith("node_modules/@deepseek-ai/") || entry.meta === true) tolerated.add(`${importer}: "${specifier}"`);
353
+ else failures.push(`${importer}: "${specifier}" — ${reason.message}`);
354
+ continue;
355
+ }
356
+ if (resolution.kind === "static") continue;
357
+ const path = resolution.path;
358
+ if (seen.has(path)) continue;
359
+ seen.add(path);
360
+ const key = path.slice(root.length + 1);
361
+ const bytes = files[key];
362
+ if (bytes === void 0) continue;
363
+ if (!/\.[cm]?js$/.test(key) || pageAsset(key)) {
364
+ reached.set(key, bytes);
365
+ continue;
366
+ }
367
+ visited += 1;
368
+ const { code, lowered, moduleRequests, metaResolveRequests } = lowerModuleSource({
369
+ filename: `/${key}`,
370
+ source: decoder.decode(bytes)
371
+ });
372
+ if (lowered) rewritten += 1;
373
+ reached.set(key, lowered ? encoder.encode(code) : bytes);
374
+ const directory = path.slice(0, path.lastIndexOf("/"));
375
+ for (const request of moduleRequests) queue.push({
376
+ specifier: request,
377
+ from: directory,
378
+ importer: key
379
+ });
380
+ for (const request of metaResolveRequests) queue.push({
381
+ specifier: request,
382
+ from: directory,
383
+ importer: key,
384
+ meta: true
385
+ });
386
+ }
387
+ if (failures.length > 0) throw new Error(`vfs image: ${String(failures.length)} unresolvable module request(s); an undeclared or missing dependency fails the pack rather than the boot:
388
+ ` + failures.join("\n "));
389
+ const swept = {};
390
+ const debuggerName = debuggerNamer(options.workspaces, options.resolveFrom);
391
+ let javascriptEntries = 0;
392
+ let dropped = 0;
393
+ for (const [name, bytes] of Object.entries(files)) {
394
+ const isJs = /\.[cm]?js$/.test(name);
395
+ if (!isJs || pageAsset(name)) {
396
+ swept[name] = isJs ? nameForDebugger(bytes, debuggerName(name), decoder, encoder) : bytes;
397
+ if (isJs) javascriptEntries += 1;
398
+ continue;
399
+ }
400
+ const kept = reached.get(name);
401
+ if (kept === void 0) {
402
+ dropped += 1;
403
+ continue;
404
+ }
405
+ swept[name] = nameForDebugger(kept, debuggerName(name), decoder, encoder);
406
+ javascriptEntries += 1;
407
+ }
408
+ return {
409
+ swept,
410
+ transform: {
411
+ visited,
412
+ rewritten
413
+ },
414
+ javascriptEntries,
415
+ droppedJavascriptEntries: dropped,
416
+ unresolvedExternalRequests: [...tolerated]
417
+ };
418
+ }
419
+ /**
420
+ * Drop executable scripts from the image.
421
+ *
422
+ * A shebang says "program", not "module": nothing in a browser can spawn one and no
423
+ * consumer reads their bytes (the packages that expose a launcher path are replaced
424
+ * by stubs that answer with a string). They are also the one place top-level `await`
425
+ * appears in the closure, which a CommonJS body cannot express.
426
+ * @param files - Image entries, mutated.
427
+ * @returns The dropped entry names.
428
+ */
429
+ function dropExecutables(files) {
430
+ const decoder = new TextDecoder();
431
+ const dropped = [];
432
+ for (const [name, bytes] of Object.entries(files)) {
433
+ if (!/\.[cm]?js$/.test(name)) continue;
434
+ if (decoder.decode(bytes.subarray(0, 2)) !== "#!") continue;
435
+ dropped.push(name);
436
+ delete files[name];
437
+ }
438
+ return dropped;
439
+ }
440
+ /**
441
+ * Materialize the dependency closure of every roster package into the image.
442
+ * @param roster - Package names to start from.
443
+ * @param options - Pack options carrying the workspace index and resolution root.
444
+ * @returns Image entries, per-package file counts, and unresolved dependencies.
445
+ */
446
+ function materialize(roster, options) {
447
+ const files = {};
448
+ const packages = /* @__PURE__ */ new Map();
449
+ const missing = [];
450
+ const replaced = new Set(REPLACED_EXTERNAL_PACKAGES);
451
+ const queue = roster.map((name) => ({
452
+ name,
453
+ from: options.resolveFrom
454
+ }));
455
+ for (let entry = queue.shift(); entry !== void 0; entry = queue.shift()) {
456
+ const { name, from } = entry;
457
+ if (packages.has(name) || replaced.has(name)) continue;
458
+ const directory = options.workspaces.get(name) ?? resolveDependency(from, name);
459
+ if (directory === void 0) {
460
+ missing.push(`${name} (from ${relative(options.resolveFrom, from) || "."})`);
461
+ continue;
462
+ }
463
+ const manifest = readJson(join(directory, "package.json"));
464
+ const prefix = `node_modules/${name}`;
465
+ const before = Object.keys(files).length;
466
+ if (options.workspaces.has(name)) {
467
+ const published = Array.isArray(manifest.files) ? publishedFilter(manifest.files) : void 0;
468
+ collectTree(directory, files, prefix, (relativePath) => !workspaceExcluded(relativePath) && (published === void 0 || published(relativePath)));
469
+ } else collectTree(directory, files, prefix, (relativePath) => !excluded(relativePath));
470
+ packages.set(name, Object.keys(files).length - before);
471
+ for (const field of ["dependencies", "peerDependencies"]) {
472
+ if (field === "peerDependencies" && !options.workspaces.has(name)) continue;
473
+ const dependencies = manifest[field];
474
+ if (typeof dependencies !== "object" || dependencies === null) continue;
475
+ for (const dependency of Object.keys(dependencies)) queue.push({
476
+ name: dependency,
477
+ from: directory
478
+ });
479
+ }
480
+ }
481
+ return {
482
+ files,
483
+ packages,
484
+ missing
485
+ };
486
+ }
487
+ /** Gzip header byte that records the packing platform; RFC 1952 §2.3.1 spells 255 "unknown". */
488
+ const GZIP_OS_UNKNOWN = 255;
489
+ /** Offset of that byte in the gzip member header. */
490
+ const GZIP_OS_OFFSET = 9;
491
+ /**
492
+ * Compress the archive into one gzip member the same tree always produces
493
+ * byte for byte.
494
+ *
495
+ * Two header fields would otherwise carry build facts: zlib writes no
496
+ * modification time and no original file name for a buffer (`gzipSync` is handed
497
+ * neither), and it fills the operating-system byte from the platform it was built
498
+ * for, which would make the same tree pack differently on Linux and macOS. That
499
+ * byte is overwritten with "unknown" — every gzip reader ignores it, and the
500
+ * artifact stops depending on where it was packed.
501
+ * @param archive - the ustar archive.
502
+ * @returns the compressed image bytes.
503
+ */
504
+ function compressImage(archive) {
505
+ const compressed = gzipSync(archive, { level: 9 });
506
+ compressed[GZIP_OS_OFFSET] = GZIP_OS_UNKNOWN;
507
+ return compressed;
508
+ }
509
+ /**
510
+ * Pack one VFS image.
511
+ *
512
+ * The manifest's claim is all-or-nothing: it names the one contract every packed body
513
+ * was emitted against. A module the transform cannot express therefore fails the pack
514
+ * rather than downgrading the image, because a mostly-transformed image boots into
515
+ * errors far from their cause.
516
+ * @param options - Composition, package index, and paths.
517
+ * @returns The compressed image plus what went into it.
518
+ * @throws When a config tree or workspace directory named in the options is missing,
519
+ * because a silently thinner image fails much later and much less clearly.
520
+ */
521
+ function packVfsImage(options) {
522
+ const root = options.root ?? DEFAULT_ROOT;
523
+ const encoder = new TextEncoder();
524
+ const configTrees = options.configTrees ?? [];
525
+ for (const tree of configTrees) if (!existsSync(tree.directory)) throw new Error(`vfs image: config tree ${tree.mount} is missing at ${tree.directory}`);
526
+ const roster = [...new Set([...rosterOf(options.config), ...configTrees.filter((tree) => tree.scanRoster === true).flatMap((tree) => treeRosterOf(tree.directory))])];
527
+ const { files, packages, missing } = materialize(roster, options);
528
+ files[CONFIG_PATH] = encoder.encode(options.config);
529
+ for (const tree of configTrees) collectTree(tree.directory, files, tree.mount, (relativePath) => !excluded(relativePath));
530
+ const executables = dropExecutables(files);
531
+ const { swept, transform, javascriptEntries, droppedJavascriptEntries, unresolvedExternalRequests } = sweepImage(files, options, [...packages.keys()].filter((name) => options.workspaces.has(name)), root);
532
+ swept[MANIFEST_PATH] = encoder.encode(`${JSON.stringify({
533
+ root,
534
+ profile: options.profile,
535
+ [CONTRACT_FIELD]: WRAPPER_CONTRACT,
536
+ javascriptEntries,
537
+ visitedEntries: transform.visited,
538
+ rewrittenEntries: transform.rewritten
539
+ }, null, 2)}\n`);
540
+ for (const directory of options.emptyDirectories ?? IMAGE_EMPTY_DIRECTORIES) swept[directory] = new Uint8Array(0);
541
+ return {
542
+ image: compressImage(packTar(swept)),
543
+ files: swept,
544
+ packages,
545
+ workspacePackages: [...packages.keys()].filter((name) => options.workspaces.has(name)).length,
546
+ roster,
547
+ missing,
548
+ executables,
549
+ pageBundles: Object.keys(swept).filter((name) => pageAsset(name)),
550
+ javascriptEntries,
551
+ droppedJavascriptEntries,
552
+ unresolvedExternalRequests,
553
+ transform,
554
+ contract: WRAPPER_CONTRACT
555
+ };
556
+ }
557
+ /**
558
+ * Pack opaque data trees into one ordered VFS overlay.
559
+ *
560
+ * Overlay mounts are restricted to the runtime-owned data directories, so an
561
+ * overlay cannot replace configuration, the lowering manifest, or modules.
562
+ * Files bypass package excludes and module reachability processing; later
563
+ * trees replace earlier files at the same path.
564
+ * @param trees - Absolute source directories and their data-directory mounts.
565
+ * @returns Deterministic compressed archive plus its uncompressed entries.
566
+ */
567
+ function packVfsOverlay(trees) {
568
+ const files = {};
569
+ for (const tree of trees) {
570
+ if (!existsSync(tree.directory)) throw new Error(`vfs overlay: tree ${tree.mount} is missing at ${tree.directory}`);
571
+ const mount = tree.mount.replace(/^\.\//, "").replace(/\/$/, "");
572
+ const first = mount.split("/")[0];
573
+ if (mount === "" || first === void 0 || !IMAGE_OVERLAY_DIRECTORIES.includes(first) || mount.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) throw new Error(`vfs overlay: mount ${JSON.stringify(tree.mount)} must stay under ${IMAGE_OVERLAY_DIRECTORIES.join(" or ")}`);
574
+ collectTree(tree.directory, files, mount, () => true, true);
575
+ }
576
+ return {
577
+ image: compressImage(packTar(files)),
578
+ files
579
+ };
580
+ }
581
+ //#endregion
582
+ //#region lib/types/repository.js
583
+ /**
584
+ * Repository knowledge for the packer: where this tree's workspaces, profile
585
+ * composition, and config trees are, and how to report a pack.
586
+ *
587
+ * The library half takes all of this as parameters. Keeping the lookup here is what
588
+ * lets the same library pack a different tree, and what keeps `pack.ts` free of
589
+ * assumptions about pnpm workspaces or the `dsh` CLI.
590
+ * @module @deepseek-ai/dsh-experimental-webworker-packer/src/repository
591
+ */
592
+ /**
593
+ * Repository directories scanned for workspace and vendored packages. The
594
+ * image only ever materializes runtime packages, which live here. The Landlock
595
+ * package family contributes its unchanged JavaScript entry from `native/`;
596
+ * examples and python never occur on a roster's dependency chain.
597
+ */
598
+ const WORKSPACE_SCAN_ROOTS = [
599
+ "vendor",
600
+ "packages",
601
+ "native/system/packages",
602
+ "apps"
603
+ ];
604
+ /** Composition entry point package: the `dsh` CLI, run from source. */
605
+ const CLI_PACKAGE = "apps/cli";
606
+ /** Composition entry point: the `dsh` CLI, run from source. */
607
+ const CLI_ENTRY = `${CLI_PACKAGE}/src/bin.ts`;
608
+ /** Repository-owned deterministic filesystem content offered by the preview. */
609
+ const PREVIEW_EXAMPLE_ROOT = "packages/experimental/webworker-runtime/tests/fixtures/vfs-example";
610
+ /**
611
+ * Index every workspace and vendored package by name.
612
+ * @param repoRoot - Absolute repository root.
613
+ * @returns Package name to absolute directory.
614
+ */
615
+ function indexWorkspacePackages(repoRoot) {
616
+ const index = /* @__PURE__ */ new Map();
617
+ const visit = (directory) => {
618
+ const manifest = join(directory, "package.json");
619
+ if (existsSync(manifest)) {
620
+ const name = JSON.parse(readFileSync(manifest, "utf8")).name;
621
+ if (typeof name === "string") index.set(name, directory);
622
+ return;
623
+ }
624
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
625
+ if (!entry.isDirectory()) continue;
626
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
627
+ visit(join(directory, entry.name));
628
+ }
629
+ };
630
+ for (const scanRoot of WORKSPACE_SCAN_ROOTS) {
631
+ const absolute = join(repoRoot, scanRoot);
632
+ if (existsSync(absolute)) visit(absolute);
633
+ }
634
+ return index;
635
+ }
636
+ /**
637
+ * Compose one profile through the real CLI dump path, leaving `!!js`
638
+ * unevaluated. The dump runs against a throwaway Harness home and default
639
+ * layers only, so the image is the shipped profile: the machine's `$DSH_HOME`
640
+ * — its profile manifest with locally installed bundles, and its patch files —
641
+ * would otherwise leak this machine's plugins into the image and break the
642
+ * same-tree-same-bytes guarantee.
643
+ * @param repoRoot - Absolute repository root.
644
+ * @param profile - Profile name to compose.
645
+ * @returns The composed YAML.
646
+ */
647
+ function composeProfile(repoRoot, profile) {
648
+ const home = mkdtempSync(join(tmpdir(), "dsh-pack-home-"));
649
+ try {
650
+ return execFileSync(process.execPath, [
651
+ "--import",
652
+ "tsx/esm",
653
+ join(repoRoot, CLI_ENTRY),
654
+ "--profile",
655
+ profile,
656
+ "--dump-default-config"
657
+ ], {
658
+ cwd: repoRoot,
659
+ encoding: "utf8",
660
+ maxBuffer: 64 * 1024 * 1024,
661
+ env: {
662
+ ...process.env,
663
+ [DSH_HOME_ENV]: home
664
+ }
665
+ });
666
+ } finally {
667
+ rmSync(home, {
668
+ recursive: true,
669
+ force: true
670
+ });
671
+ }
672
+ }
673
+ /**
674
+ * Config trees the CLI package declares for deployment images
675
+ * (`dsh.configTrees` in its package.json): `path` is relative to the CLI
676
+ * package root, `mount` is the image path, `scanRoster` feeds the tree's yml
677
+ * plugin rows into the pack roster. The CLI owns its config layout; this
678
+ * reader follows the declaration instead of naming directories. A malformed
679
+ * declaration refuses the pack.
680
+ * @param repoRoot - Absolute repository root.
681
+ * @returns Trees with absolute source directories.
682
+ */
683
+ function configTrees(repoRoot) {
684
+ const packageDir = join(repoRoot, CLI_PACKAGE);
685
+ const declared = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8")).dsh?.configTrees;
686
+ if (declared === void 0) return [];
687
+ if (!Array.isArray(declared)) throw new Error(`vfs image: ${CLI_PACKAGE} dsh.configTrees must be an array`);
688
+ const mounts = /* @__PURE__ */ new Set();
689
+ return declared.map((entry, index) => {
690
+ const tree = entry;
691
+ const at = `${CLI_PACKAGE} dsh.configTrees[${String(index)}]`;
692
+ if (tree === null || typeof tree !== "object" || typeof tree.mount !== "string" || tree.mount === "" || typeof tree.path !== "string" || tree.path === "" || tree.scanRoster !== void 0 && typeof tree.scanRoster !== "boolean") throw new Error(`vfs image: ${at} must declare a string mount, a string path, and an optional boolean scanRoster`);
693
+ if (mounts.has(tree.mount)) throw new Error(`vfs image: ${at} repeats mount ${JSON.stringify(tree.mount)}`);
694
+ mounts.add(tree.mount);
695
+ return {
696
+ mount: tree.mount,
697
+ directory: join(packageDir, tree.path),
698
+ ...tree.scanRoster === void 0 ? {} : { scanRoster: tree.scanRoster }
699
+ };
700
+ });
701
+ }
702
+ /**
703
+ * Built-in filesystem fixtures offered by the repository preview.
704
+ * Session and Workspace semantics remain opaque here; the owning runtime tests
705
+ * validate those files through their production readers.
706
+ * @param repoRoot - Absolute repository root.
707
+ * @returns Named chooser entries and their overlay trees.
708
+ */
709
+ function previewFixtures(repoRoot) {
710
+ const root = join(repoRoot, PREVIEW_EXAMPLE_ROOT);
711
+ return [{
712
+ id: "vfs-example",
713
+ label: "Built-in showcase",
714
+ description: "Sample workspace, tool cards, subagents, and paged history.",
715
+ trees: ["home", "workspace"].map((mount) => ({
716
+ mount,
717
+ directory: join(root, mount)
718
+ }))
719
+ }];
720
+ }
721
+ /**
722
+ * Render one pack as the lines a build log should carry.
723
+ *
724
+ * Refusals and unresolved dependencies are the two states a reader must not miss, so
725
+ * they are spelled out rather than counted.
726
+ * @param result - What the pack produced.
727
+ * @param repoRoot - Absolute repository root, for relative paths.
728
+ * @param outputFile - Where the image was written.
729
+ * @returns Lines to print.
730
+ */
731
+ function describePack(result, repoRoot, outputFile) {
732
+ const sizeOf = (prefix) => Object.entries(result.files).filter(([name]) => name.startsWith(prefix)).reduce((sum, [, bytes]) => sum + bytes.byteLength, 0);
733
+ const megabytes = (bytes) => `${(bytes / 1024 / 1024).toFixed(2)} MB`;
734
+ const workspaceCount = result.workspacePackages;
735
+ const heaviest = [...result.packages.entries()].map(([name, count]) => ({
736
+ name,
737
+ count,
738
+ bytes: sizeOf(`node_modules/${name}/`)
739
+ })).sort((left, right) => right.bytes - left.bytes).slice(0, 12);
740
+ return [
741
+ `vfs image: ${relative(repoRoot, outputFile)}`,
742
+ ` roster entries ${String(result.roster.length)}`,
743
+ ` packages ${String(result.packages.size)} (${String(workspaceCount)} workspace)`,
744
+ ` files ${String(Object.keys(result.files).length)}`,
745
+ ` raw ${megabytes(Object.values(result.files).reduce((sum, bytes) => sum + bytes.byteLength, 0))}`,
746
+ ` compressed ${megabytes(result.image.byteLength)}`,
747
+ ` config + presets ${megabytes(sizeOf("config/"))}`,
748
+ ` javascript entries ${String(result.javascriptEntries)} (dropped ${String(result.executables.length)} executable scripts, ${String(result.pageBundles.length)} page bundles verbatim)`,
749
+ ` wrapper contract ${result.contract}`,
750
+ ` transform ${String(result.transform.rewritten)} of ${String(result.transform.visited)} reached entries rewritten, ${String(result.droppedJavascriptEntries)} unreachable dropped`,
751
+ ` unresolved ${String(result.unresolvedExternalRequests.length)} third-party request(s) left to fail loud at require time`,
752
+ " heaviest packages:",
753
+ ...heaviest.map((entry) => ` ${entry.bytes.toString().padStart(9)} B ${entry.name} (${String(entry.count)} files)`),
754
+ ...result.missing.length === 0 ? [] : [" unresolved dependencies:", ...result.missing.map((entry) => ` ${entry}`)],
755
+ ""
756
+ ];
757
+ }
758
+ //#endregion
759
+ export { previewFixtures as a, MANIFEST_PATH as c, WRAPPER_CONTRACT as d, indexWorkspacePackages as i, packVfsImage as l, configTrees as n, CONFIG_PATH as o, describePack as r, DEFAULT_ROOT$1 as s, composeProfile as t, packVfsOverlay as u };