@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.
- package/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +72 -0
- package/README.zh.md +72 -0
- package/bin.js +20 -0
- package/lib/bin.js +76 -0
- package/lib/index.js +2 -0
- package/lib/repository-EG8FQ0lQ.js +759 -0
- package/lib/types/bin.d.ts +3 -0
- package/lib/types/bin.js +77 -0
- package/lib/types/index.d.ts +8 -0
- package/lib/types/pack.d.ts +105 -0
- package/lib/types/repository.d.ts +61 -0
- package/lib/types/rules.d.ts +43 -0
- package/lib/types/transform-image.d.ts +12 -0
- package/package.json +50 -0
|
@@ -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 };
|