@vmz/vmz 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/build-assemble.js +4 -3
  2. package/dist/cdn-policy.js +5 -8
  3. package/dist/cli.js +20 -7
  4. package/dist/content-addressed-assets.js +2 -1
  5. package/dist/delivery-profile.d.ts +10 -0
  6. package/dist/delivery-profile.js +69 -0
  7. package/dist/dev-session.d.ts +6 -0
  8. package/dist/dev-session.js +166 -40
  9. package/dist/document-build.js +24 -28
  10. package/dist/document-cmd.js +2 -9
  11. package/dist/document-integrate.js +10 -17
  12. package/dist/embedded-packaging.js +3 -7
  13. package/dist/index.d.ts +19 -1
  14. package/dist/index.js +62 -2
  15. package/dist/locale-check.js +28 -58
  16. package/dist/locale-cmd.js +10 -42
  17. package/dist/locale-route-emit.d.ts +4 -1
  18. package/dist/locale-route-emit.js +7 -32
  19. package/dist/mini-host.d.ts +47 -0
  20. package/dist/mini-host.js +202 -0
  21. package/dist/native-addon.d.ts +9 -0
  22. package/dist/native-addon.js +84 -0
  23. package/dist/pack-client-packages.d.ts +25 -0
  24. package/dist/pack-client-packages.js +399 -0
  25. package/dist/pack.d.ts +21 -3
  26. package/dist/pack.js +21 -6
  27. package/dist/pretty-json.d.ts +19 -0
  28. package/dist/pretty-json.js +43 -0
  29. package/dist/production-observability.js +3 -2
  30. package/dist/production-test-pack.js +4 -3
  31. package/dist/release-pack.js +5 -18
  32. package/dist/route-path.d.ts +35 -0
  33. package/dist/route-path.js +77 -0
  34. package/dist/server-artifact.js +5 -6
  35. package/dist/site-delivery.js +3 -2
  36. package/dist/static-emit.js +67 -86
  37. package/dist/test-cmd.js +2 -1
  38. package/dist/wechat-packaging.d.ts +22 -0
  39. package/dist/wechat-packaging.js +59 -0
  40. package/package.json +12 -12
@@ -0,0 +1,399 @@
1
+ /**
2
+ * Browser-safe client package lowering (Pack stage thin slice).
3
+ *
4
+ * Bare npm/workspace imports are legal on the author surface (01/04).
5
+ * Browser ESM cannot resolve them. Pack materializes reachable package
6
+ * modules under `dist/vendor/<pkg>/…` and rewrites importers to relative paths.
7
+ *
8
+ * Not full oxc chunk-split/minify (`oxc-pending` remains for release minify).
9
+ */
10
+ // @ts-nocheck
11
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
12
+ import path from 'node:path';
13
+ import { createRequire } from 'node:module';
14
+ import { resolvePackageRoot } from './packages.js';
15
+ const VENDOR_DIR = 'vendor';
16
+ const SKIP_PREFIXES = ['node:', 'nodejs:', 'cloudflare:', 'data:', 'http:', 'https:', 'vmz:', '#'];
17
+ /**
18
+ * @param {string} outDir
19
+ * @param {{ projectRoot?: string | null }} [opts]
20
+ * @returns {{ rewrittenFiles: number, vendoredModules: string[], bareSpecs: string[] }}
21
+ */
22
+ export function packClientBareImports(outDir, opts = {}) {
23
+ const projectRoot = opts.projectRoot ? path.resolve(opts.projectRoot) : path.dirname(path.resolve(outDir));
24
+ /** @type {Map<string, string>} bareSpec → absolute vendored .js path */
25
+ const bareToVendor = new Map();
26
+ /** @type {Set<string>} absolute source files already materialized */
27
+ const materializedSources = new Set();
28
+ /** @type {string[]} */
29
+ const bareQueue = [];
30
+ /** @type {string[]} */
31
+ const unresolved = [];
32
+ /** @type {string[]} */
33
+ const skippedVmz = [];
34
+ for (const file of listClientJs(outDir)) {
35
+ for (const spec of collectBareSpecs(readFileSync(file, 'utf8'))) {
36
+ if (!bareQueue.includes(spec))
37
+ bareQueue.push(spec);
38
+ }
39
+ }
40
+ while (bareQueue.length) {
41
+ const spec = bareQueue.shift();
42
+ if (bareToVendor.has(spec))
43
+ continue;
44
+ const resolved = resolveBareToSource(projectRoot, spec);
45
+ if (!resolved) {
46
+ if (!unresolved.includes(spec))
47
+ unresolved.push(spec);
48
+ continue;
49
+ }
50
+ if (/\.vmz$/i.test(resolved.sourceFile)) {
51
+ if (!skippedVmz.includes(spec))
52
+ skippedVmz.push(spec);
53
+ continue;
54
+ }
55
+ const destAbs = materializeSourceTree(outDir, resolved, materializedSources, bareQueue);
56
+ if (destAbs)
57
+ bareToVendor.set(spec, destAbs);
58
+ else if (!unresolved.includes(spec))
59
+ unresolved.push(spec);
60
+ }
61
+ /** @type {string[]} */
62
+ const rewritten = [];
63
+ for (const file of [...listClientJs(outDir), ...listVendorJs(outDir)]) {
64
+ const before = readFileSync(file, 'utf8');
65
+ let after = rewriteBareImports(before, file, bareToVendor);
66
+ after = rewriteRelativeTsSpecs(after);
67
+ if (after !== before) {
68
+ writeFileSync(file, after, 'utf8');
69
+ rewritten.push(path.relative(outDir, file).replace(/\\/g, '/'));
70
+ }
71
+ }
72
+ // Any bare that remains in client (non-vendor) JS after rewrite is still browser-broken.
73
+ /** @type {string[]} */
74
+ const remaining = [];
75
+ for (const file of listClientJs(outDir)) {
76
+ for (const spec of collectBareSpecs(readFileSync(file, 'utf8'))) {
77
+ if (!remaining.includes(spec))
78
+ remaining.push(spec);
79
+ }
80
+ }
81
+ return {
82
+ rewrittenFiles: rewritten.length,
83
+ vendoredModules: [...bareToVendor.values()].map((abs) => path.relative(outDir, abs).replace(/\\/g, '/')),
84
+ bareSpecs: [...bareToVendor.keys()],
85
+ unresolvedBareSpecs: unresolved,
86
+ skippedVmzExports: skippedVmz,
87
+ remainingBareSpecs: remaining,
88
+ };
89
+ }
90
+ /**
91
+ * Materialize `resolved.sourceFile` and its relative import closure under vendor/.
92
+ * @returns {string | null} vendor path for the entry source
93
+ */
94
+ function materializeSourceTree(outDir, resolved, materializedSources, bareQueue) {
95
+ /** @type {string[]} */
96
+ const sourceQueue = [resolved.sourceFile];
97
+ // Subpath bare specs often land on a file already vendored via a relative
98
+ // import from the package root — still map the bare name to that vendor path.
99
+ /** @type {string | null} */
100
+ let entryDest = vendorPathForSource(outDir, resolved.pkgName, resolved.pkgRoot, resolved.sourceFile);
101
+ while (sourceQueue.length) {
102
+ const sourceFile = sourceQueue.shift();
103
+ if (materializedSources.has(sourceFile))
104
+ continue;
105
+ materializedSources.add(sourceFile);
106
+ const destAbs = vendorPathForSource(outDir, resolved.pkgName, resolved.pkgRoot, sourceFile);
107
+ mkdirSync(path.dirname(destAbs), { recursive: true });
108
+ const js = materializeModule(sourceFile);
109
+ writeFileSync(destAbs, js, 'utf8');
110
+ if (sourceFile === resolved.sourceFile)
111
+ entryDest = destAbs;
112
+ for (const bare of collectBareSpecs(js)) {
113
+ if (!bareQueue.includes(bare))
114
+ bareQueue.push(bare);
115
+ }
116
+ for (const rel of collectRelativeSpecs(js)) {
117
+ const target = resolveRelativeSource(path.dirname(sourceFile), rel);
118
+ if (target && !materializedSources.has(target))
119
+ sourceQueue.push(target);
120
+ }
121
+ // Also follow relative imports as written in the *source* (before transpile),
122
+ // so `./catalog.ts` is discovered even if transpile already rewrote to `.js`.
123
+ const raw = readFileSync(sourceFile, 'utf8');
124
+ for (const rel of collectRelativeSpecs(raw)) {
125
+ const target = resolveRelativeSource(path.dirname(sourceFile), rel);
126
+ if (target && !materializedSources.has(target))
127
+ sourceQueue.push(target);
128
+ }
129
+ }
130
+ return entryDest;
131
+ }
132
+ function listClientJs(outDir) {
133
+ /** @type {string[]} */
134
+ const out = [];
135
+ walk(outDir, (file) => {
136
+ const rel = path.relative(outDir, file).replace(/\\/g, '/');
137
+ if (rel.startsWith('_vmz/') || rel.startsWith(`${VENDOR_DIR}/`) || rel.startsWith('#server/') || rel.startsWith('_vmz_server/')) {
138
+ return;
139
+ }
140
+ if (rel.endsWith('.js') || rel.endsWith('.mjs'))
141
+ out.push(file);
142
+ });
143
+ return out;
144
+ }
145
+ function listVendorJs(outDir) {
146
+ const root = path.join(outDir, VENDOR_DIR);
147
+ if (!existsSync(root))
148
+ return [];
149
+ /** @type {string[]} */
150
+ const out = [];
151
+ walk(root, (file) => {
152
+ if (file.endsWith('.js') || file.endsWith('.mjs'))
153
+ out.push(file);
154
+ });
155
+ return out;
156
+ }
157
+ function walk(dir, visit) {
158
+ if (!existsSync(dir))
159
+ return;
160
+ for (const name of readdirSync(dir)) {
161
+ if (name === 'node_modules')
162
+ continue;
163
+ const full = path.join(dir, name);
164
+ let st;
165
+ try {
166
+ st = statSync(full);
167
+ }
168
+ catch {
169
+ continue;
170
+ }
171
+ if (st.isDirectory())
172
+ walk(full, visit);
173
+ else
174
+ visit(full);
175
+ }
176
+ }
177
+ /** @param {string} js */
178
+ export function collectBareSpecs(js) {
179
+ /** @type {Set<string>} */
180
+ const specs = new Set();
181
+ const add = (spec) => {
182
+ if (!spec || isRelativeOrAbsolute(spec) || shouldSkipBare(spec))
183
+ return;
184
+ specs.add(spec);
185
+ };
186
+ // `from 'x'` covers import/export … from
187
+ let m;
188
+ const fromRe = /\bfrom\s+['"]([^'"]+)['"]/g;
189
+ while ((m = fromRe.exec(js)))
190
+ add(m[1]);
191
+ const dynRe = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
192
+ while ((m = dynRe.exec(js)))
193
+ add(m[1]);
194
+ // side-effect: import 'x' (not import( and not import … from)
195
+ const sideRe = /\bimport\s+['"]([^'"]+)['"]/g;
196
+ while ((m = sideRe.exec(js)))
197
+ add(m[1]);
198
+ return [...specs];
199
+ }
200
+ function collectRelativeSpecs(js) {
201
+ /** @type {Set<string>} */
202
+ const specs = new Set();
203
+ const re = /(?:from\s+|import\s*\(\s*)['"](\.[^'"]+)['"]/g;
204
+ let m;
205
+ while ((m = re.exec(js)))
206
+ specs.add(m[1]);
207
+ return [...specs];
208
+ }
209
+ function isRelativeOrAbsolute(spec) {
210
+ return spec.startsWith('.') || spec.startsWith('/') || spec.startsWith('\\');
211
+ }
212
+ function shouldSkipBare(spec) {
213
+ return SKIP_PREFIXES.some((p) => spec.startsWith(p));
214
+ }
215
+ function resolveBareToSource(projectRoot, spec) {
216
+ const { pkgName, subpath } = splitPackageSpec(spec);
217
+ let pkgRoot = resolvePackageRoot(projectRoot, pkgName);
218
+ if (!pkgRoot) {
219
+ let cur = projectRoot;
220
+ for (let i = 0; i < 8; i++) {
221
+ pkgRoot = resolvePackageRoot(cur, pkgName);
222
+ if (pkgRoot)
223
+ break;
224
+ const parent = path.dirname(cur);
225
+ if (parent === cur)
226
+ break;
227
+ cur = parent;
228
+ }
229
+ }
230
+ if (!pkgRoot)
231
+ return null;
232
+ const sourceFile = resolveExportFile(pkgRoot, subpath);
233
+ if (!sourceFile)
234
+ return null;
235
+ return { pkgName, pkgRoot, subpath, sourceFile };
236
+ }
237
+ function splitPackageSpec(spec) {
238
+ if (spec.startsWith('@')) {
239
+ const parts = spec.split('/');
240
+ if (parts.length < 2)
241
+ return { pkgName: spec, subpath: '' };
242
+ return { pkgName: `${parts[0]}/${parts[1]}`, subpath: parts.slice(2).join('/') };
243
+ }
244
+ const i = spec.indexOf('/');
245
+ if (i < 0)
246
+ return { pkgName: spec, subpath: '' };
247
+ return { pkgName: spec.slice(0, i), subpath: spec.slice(i + 1) };
248
+ }
249
+ function resolveExportFile(pkgRoot, subpath) {
250
+ const pkgPath = path.join(pkgRoot, 'package.json');
251
+ if (!existsSync(pkgPath))
252
+ return null;
253
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
254
+ const key = subpath ? `./${subpath}` : '.';
255
+ let target = null;
256
+ if (pkg.exports && typeof pkg.exports === 'object' && !Array.isArray(pkg.exports)) {
257
+ const entry = pkg.exports[key] ?? (!subpath ? pkg.exports['.'] : null);
258
+ target = flattenExportTarget(entry);
259
+ }
260
+ if (!target && !subpath)
261
+ target = pkg.module || pkg.main || './index.js';
262
+ if (!target && subpath) {
263
+ for (const cand of [
264
+ path.join(pkgRoot, 'src', `${subpath}.ts`),
265
+ path.join(pkgRoot, 'src', `${subpath}.js`),
266
+ path.join(pkgRoot, `${subpath}.ts`),
267
+ path.join(pkgRoot, `${subpath}.js`),
268
+ path.join(pkgRoot, 'src', subpath, 'index.ts'),
269
+ path.join(pkgRoot, 'src', subpath, 'index.js'),
270
+ ]) {
271
+ if (existsSync(cand) && statSync(cand).isFile())
272
+ return cand;
273
+ }
274
+ return null;
275
+ }
276
+ if (!target || typeof target !== 'string')
277
+ return null;
278
+ const abs = path.resolve(pkgRoot, target);
279
+ if (existsSync(abs) && statSync(abs).isFile())
280
+ return abs;
281
+ for (const ext of ['.ts', '.tsx', '.js', '.mjs']) {
282
+ const c = abs.endsWith(ext) ? abs : abs + ext;
283
+ if (existsSync(c) && statSync(c).isFile())
284
+ return c;
285
+ }
286
+ return null;
287
+ }
288
+ function flattenExportTarget(entry) {
289
+ if (typeof entry === 'string')
290
+ return entry;
291
+ if (!entry || typeof entry !== 'object')
292
+ return null;
293
+ const v = entry.import || entry.default || entry.require || entry.module || null;
294
+ if (typeof v === 'string')
295
+ return v;
296
+ if (v && typeof v === 'object')
297
+ return flattenExportTarget(v);
298
+ return null;
299
+ }
300
+ function vendorPathForSource(outDir, pkgName, pkgRoot, sourceFile) {
301
+ const rel = path.relative(pkgRoot, sourceFile).replace(/\\/g, '/');
302
+ return path.join(outDir, VENDOR_DIR, packageDirName(pkgName), rewriteTsExt(rel));
303
+ }
304
+ function packageDirName(pkgName) {
305
+ return pkgName.replace(/^@/, '');
306
+ }
307
+ function materializeModule(sourceFile) {
308
+ const ext = path.extname(sourceFile).toLowerCase();
309
+ const raw = readFileSync(sourceFile, 'utf8');
310
+ if (ext === '.ts' || ext === '.tsx')
311
+ return transpileTs(raw, sourceFile);
312
+ return rewriteRelativeTsSpecs(raw);
313
+ }
314
+ function transpileTs(source, filename) {
315
+ try {
316
+ const require = createRequire(import.meta.url);
317
+ const ts = require('typescript');
318
+ const out = ts.transpileModule(source, {
319
+ fileName: filename,
320
+ compilerOptions: {
321
+ module: ts.ModuleKind.ESNext,
322
+ target: ts.ScriptTarget.ES2022,
323
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
324
+ esModuleInterop: true,
325
+ skipLibCheck: true,
326
+ },
327
+ });
328
+ return rewriteRelativeTsSpecs(out.outputText || '');
329
+ }
330
+ catch (err) {
331
+ const msg = err instanceof Error ? err.message : String(err);
332
+ return `/* vmz-pack: typescript transpile failed (${msg}) */\n${rewriteRelativeTsSpecs(source)}`;
333
+ }
334
+ }
335
+ function rewriteTsExt(p) {
336
+ return String(p)
337
+ .replace(/\.tsx$/i, '.js')
338
+ .replace(/\.ts$/i, '.js');
339
+ }
340
+ export function rewriteRelativeTsSpecs(js) {
341
+ return js
342
+ .replace(/(from\s+['"])(\.[^'"]+)\.tsx(['"])/g, '$1$2.js$3')
343
+ .replace(/(from\s+['"])(\.[^'"]+)\.ts(['"])/g, '$1$2.js$3')
344
+ .replace(/(import\s*\(\s*['"])(\.[^'"]+)\.tsx(['"]\s*\))/g, '$1$2.js$3')
345
+ .replace(/(import\s*\(\s*['"])(\.[^'"]+)\.ts(['"]\s*\))/g, '$1$2.js$3');
346
+ }
347
+ function resolveRelativeSource(fromDir, rel) {
348
+ const cleaned = rel.replace(/\?.*$/, '').replace(/#.*$/, '');
349
+ const base = path.resolve(fromDir, cleaned);
350
+ for (const cand of [
351
+ base,
352
+ `${base}.ts`,
353
+ `${base}.tsx`,
354
+ `${base}.js`,
355
+ `${base}.mjs`,
356
+ path.join(base, 'index.ts'),
357
+ path.join(base, 'index.js'),
358
+ ]) {
359
+ try {
360
+ if (existsSync(cand) && statSync(cand).isFile())
361
+ return cand;
362
+ }
363
+ catch {
364
+ /* ignore */
365
+ }
366
+ }
367
+ return null;
368
+ }
369
+ function rewriteBareImports(js, fromFile, bareToVendor) {
370
+ const toRel = (spec) => {
371
+ const dest = bareToVendor.get(spec);
372
+ if (!dest)
373
+ return null;
374
+ let rel = path.relative(path.dirname(fromFile), dest).replace(/\\/g, '/');
375
+ if (!rel.startsWith('.'))
376
+ rel = `./${rel}`;
377
+ return rel;
378
+ };
379
+ let out = js;
380
+ out = out.replace(/\bimport\s*\(\s*(['"])([^'"]+)\1\s*\)/g, (full, quote, spec) => {
381
+ if (isRelativeOrAbsolute(spec) || shouldSkipBare(spec))
382
+ return full;
383
+ const rel = toRel(spec);
384
+ return rel ? `import(${quote}${rel}${quote})` : full;
385
+ });
386
+ out = out.replace(/\bfrom\s+(['"])([^'"]+)\1/g, (full, quote, spec) => {
387
+ if (isRelativeOrAbsolute(spec) || shouldSkipBare(spec))
388
+ return full;
389
+ const rel = toRel(spec);
390
+ return rel ? `from ${quote}${rel}${quote}` : full;
391
+ });
392
+ out = out.replace(/\bimport\s+(['"])([^'"]+)\1/g, (full, quote, spec) => {
393
+ if (isRelativeOrAbsolute(spec) || shouldSkipBare(spec))
394
+ return full;
395
+ const rel = toRel(spec);
396
+ return rel ? `import ${quote}${rel}${quote}` : full;
397
+ });
398
+ return out;
399
+ }
package/dist/pack.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * B4 — Pack stage: consume Deployment IR (VPG-owned units), emit pack manifest.
3
- * Full oxc minify/chunk-split lands progressively; this stage always runs and
4
- * records integrity digests so Assemble/Prove never skip the pack contract.
2
+ * Pack stage: consume Deployment IR (VPG-owned units), emit pack manifest,
3
+ * and lower browser-unreachable bare package imports to `dist/vendor/**`.
4
+ * Full oxc minify/chunk-split lands progressively (`oxc-pending` on release minify).
5
5
  */
6
6
  export declare const PACK_MANIFEST_SCHEMA = "vmz.pack.manifest.v0";
7
7
  /**
@@ -20,6 +20,7 @@ export declare function ensureRuntimeCompanions(outDir: any, coreDist: any): any
20
20
  * assembly?: string,
21
21
  * preferredClientFace?: string,
22
22
  * coreDist?: string | null,
23
+ * projectRoot?: string | null,
23
24
  * }} [opts]
24
25
  */
25
26
  export declare function packFromDeploymentIr(outDir: any, opts?: {}): {
@@ -35,6 +36,23 @@ export declare function packFromDeploymentIr(outDir: any, opts?: {}): {
35
36
  minify: string;
36
37
  treeShakeBasis: string;
37
38
  bundler: string;
39
+ clientPackageLowering: {
40
+ status: string;
41
+ rewrittenFiles: number;
42
+ bareSpecs: any[];
43
+ vendoredModules: string[];
44
+ unresolvedBareSpecs: any[];
45
+ skippedVmzExports: any[];
46
+ remainingBareSpecs: any[];
47
+ };
38
48
  };
39
49
  path: string;
50
+ clientPackages: {
51
+ rewrittenFiles: number;
52
+ vendoredModules: string[];
53
+ bareSpecs: any[];
54
+ unresolvedBareSpecs: any[];
55
+ skippedVmzExports: any[];
56
+ remainingBareSpecs: any[];
57
+ };
40
58
  };
package/dist/pack.js CHANGED
@@ -1,13 +1,15 @@
1
1
  /**
2
- * B4 — Pack stage: consume Deployment IR (VPG-owned units), emit pack manifest.
3
- * Full oxc minify/chunk-split lands progressively; this stage always runs and
4
- * records integrity digests so Assemble/Prove never skip the pack contract.
2
+ * Pack stage: consume Deployment IR (VPG-owned units), emit pack manifest,
3
+ * and lower browser-unreachable bare package imports to `dist/vendor/**`.
4
+ * Full oxc minify/chunk-split lands progressively (`oxc-pending` on release minify).
5
5
  */
6
6
  // @ts-nocheck
7
7
  import crypto from 'node:crypto';
8
- import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import { loadDeploymentIr, planBundleInputs } from './bundler-adapter.js';
11
+ import { packClientBareImports } from './pack-client-packages.js';
12
+ import { writePrettyJsonFile } from './pretty-json.js';
11
13
  export const PACK_MANIFEST_SCHEMA = 'vmz.pack.manifest.v0';
12
14
  /**
13
15
  * Ensure dom split companions sit next to vmz-dom.js (barrel imports ./dom-core.js).
@@ -39,10 +41,14 @@ export function ensureRuntimeCompanions(outDir, coreDist) {
39
41
  * assembly?: string,
40
42
  * preferredClientFace?: string,
41
43
  * coreDist?: string | null,
44
+ * projectRoot?: string | null,
42
45
  * }} [opts]
43
46
  */
44
47
  export function packFromDeploymentIr(outDir, opts = {}) {
45
48
  ensureRuntimeCompanions(outDir, opts.coreDist);
49
+ // Browser ESM cannot resolve bare npm specs — materialize reachable packages first
50
+ // so digests below reflect the rewritten graph.
51
+ const clientPackages = packClientBareImports(outDir, { projectRoot: opts.projectRoot || null });
46
52
  const ir = loadDeploymentIr(outDir);
47
53
  const inputs = planBundleInputs(outDir, ir);
48
54
  const units = [];
@@ -81,13 +87,22 @@ export function packFromDeploymentIr(outDir, opts = {}) {
81
87
  minify: opts.release ? 'oxc-pending' : 'dev-identity',
82
88
  treeShakeBasis: 'vpg-deployment-ir',
83
89
  bundler: 'vmz-pack',
90
+ clientPackageLowering: {
91
+ status: 'thin',
92
+ rewrittenFiles: clientPackages.rewrittenFiles,
93
+ bareSpecs: clientPackages.bareSpecs,
94
+ vendoredModules: clientPackages.vendoredModules,
95
+ unresolvedBareSpecs: clientPackages.unresolvedBareSpecs || [],
96
+ skippedVmzExports: clientPackages.skippedVmzExports || [],
97
+ remainingBareSpecs: clientPackages.remainingBareSpecs || [],
98
+ },
84
99
  };
85
100
  body.packDigest = sha256Hex(stableStringify({ ...body }));
86
101
  const vmzDir = path.join(outDir, '_vmz');
87
102
  mkdirSync(vmzDir, { recursive: true });
88
103
  const file = path.join(vmzDir, 'pack-manifest.json');
89
- writeFileSync(file, `${JSON.stringify(body, null, 2)}\n`, 'utf8');
90
- return { manifest: body, path: file };
104
+ writePrettyJsonFile(file, body);
105
+ return { manifest: body, path: file, clientPackages };
91
106
  }
92
107
  function stableStringify(value) {
93
108
  return JSON.stringify(sortKeys(value));
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Pretty-print a value through `vmz-generator` (N-API).
3
+ * @param {unknown} value
4
+ * @returns {string} Pretty JSON without trailing newline.
5
+ */
6
+ export declare function generatePrettyJson(value: any): any;
7
+ /**
8
+ * Write a pretty JSON artifact with a trailing newline.
9
+ * @param {string} filePath
10
+ * @param {unknown} value
11
+ */
12
+ export declare function writePrettyJsonFile(filePath: any, value: any): void;
13
+ /**
14
+ * CLI `--json` helper: write to a path when `target` is a string, else stdout.
15
+ * @param {string | boolean | undefined} target
16
+ * @param {unknown} value
17
+ * @param {{ logWrote?: (path: string) => void }} [opts]
18
+ */
19
+ export declare function emitPrettyJson(target: any, value: any, opts?: {}): void;
@@ -0,0 +1,43 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Production JSON artifact printer — always via N-API JsonCodeGenerator.
4
+ * Do not use `JSON.stringify(x, null, 2)` for on-disk artifacts.
5
+ */
6
+ import fs from 'node:fs';
7
+ import { requireNativeAddon } from './native-addon.js';
8
+ /**
9
+ * Pretty-print a value through `vmz-generator` (N-API).
10
+ * @param {unknown} value
11
+ * @returns {string} Pretty JSON without trailing newline.
12
+ */
13
+ export function generatePrettyJson(value) {
14
+ const native = requireNativeAddon();
15
+ if (typeof native.generatePrettyJson !== 'function') {
16
+ throw new Error('vmz native addon missing generatePrettyJson — rebuild with `pnpm napi:build`');
17
+ }
18
+ return native.generatePrettyJson(JSON.stringify(value));
19
+ }
20
+ /**
21
+ * Write a pretty JSON artifact with a trailing newline.
22
+ * @param {string} filePath
23
+ * @param {unknown} value
24
+ */
25
+ export function writePrettyJsonFile(filePath, value) {
26
+ fs.writeFileSync(filePath, `${generatePrettyJson(value)}\n`, 'utf8');
27
+ }
28
+ /**
29
+ * CLI `--json` helper: write to a path when `target` is a string, else stdout.
30
+ * @param {string | boolean | undefined} target
31
+ * @param {unknown} value
32
+ * @param {{ logWrote?: (path: string) => void }} [opts]
33
+ */
34
+ export function emitPrettyJson(target, value, opts = {}) {
35
+ const text = generatePrettyJson(value);
36
+ if (typeof target === 'string') {
37
+ fs.writeFileSync(target, `${text}\n`, 'utf8');
38
+ if (typeof opts.logWrote === 'function')
39
+ opts.logWrote(target);
40
+ return;
41
+ }
42
+ console.log(text);
43
+ }
@@ -7,6 +7,7 @@ import crypto from 'node:crypto';
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { canonicalJson, sha256Hex } from './release-pack.js';
10
+ import { writePrettyJsonFile } from './pretty-json.js';
10
11
  export const PRODUCTION_OBSERVABILITY_SCHEMA = 'vmz.production.observability.v0';
11
12
  export const PRODUCTION_TRACE_SCHEMA = 'vmz.production.trace.v0';
12
13
  /** Facets that production traces must be able to carry (08 A5). */
@@ -457,8 +458,8 @@ export function emitProductionObservability(distDir, overrides = {}, meta = {})
457
458
  fs.mkdirSync(vmzDir, { recursive: true });
458
459
  const contractPath = path.join(vmzDir, 'production-observability.json');
459
460
  const tracePath = path.join(vmzDir, 'production-trace.sample.json');
460
- fs.writeFileSync(contractPath, `${JSON.stringify(contract, null, 2)}\n`, 'utf8');
461
- fs.writeFileSync(tracePath, `${JSON.stringify(trace, null, 2)}\n`, 'utf8');
461
+ writePrettyJsonFile(contractPath, contract);
462
+ writePrettyJsonFile(tracePath, trace);
462
463
  return { contract, trace, contractPath, tracePath };
463
464
  }
464
465
  export function observabilityDigest(contract) {
@@ -8,6 +8,7 @@ import crypto from 'node:crypto';
8
8
  import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import { canonicalJson, sha256Hex } from './release-pack.js';
11
+ import { writePrettyJsonFile } from './pretty-json.js';
11
12
  export const PRODUCTION_SCENARIO_PACK_SCHEMA = 'vmz.production.scenario_pack.v0';
12
13
  export const PRODUCTION_CI_PROFILE_SCHEMA = 'vmz.production.ci_profile.v0';
13
14
  export const PRODUCTION_TEST_REPORT_SCHEMA = 'vmz.production.test_report.v0';
@@ -431,9 +432,9 @@ export function emitProductionTestArtifacts(root, report, pack, profile) {
431
432
  const reportPath = path.join(dir, 'report.json');
432
433
  const packPath = path.join(dir, 'scenario-pack.json');
433
434
  const profilePath = path.join(dir, 'ci-profile.json');
434
- fs.writeFileSync(reportPath, `${JSON.stringify(stamped, null, 2)}\n`, 'utf8');
435
- fs.writeFileSync(packPath, `${JSON.stringify(pack, null, 2)}\n`, 'utf8');
436
- fs.writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)}\n`, 'utf8');
435
+ writePrettyJsonFile(reportPath, stamped);
436
+ writePrettyJsonFile(packPath, pack);
437
+ writePrettyJsonFile(profilePath, profile);
437
438
  return { reportPath, packPath, profilePath, report: stamped };
438
439
  }
439
440
  /** Assert CI profile forbids JS test-runner disguise. */
@@ -9,6 +9,8 @@
9
9
  import crypto from 'node:crypto';
10
10
  import fs from 'node:fs';
11
11
  import path from 'node:path';
12
+ import { writePrettyJsonFile } from './pretty-json.js';
13
+ import { listPublicPageUnits, unitBrowserPathPattern } from './route-path.js';
12
14
  export const RELEASE_ENVELOPE_SCHEMA = 'vmz.release.envelope.v0';
13
15
  export const APPLICATION_ARTIFACT_SCHEMA = 'vmz.application.artifact.v0';
14
16
  export const DELIVERY_ARTIFACT_MANIFEST_SCHEMA = 'vmz.profile.delivery_artifact_manifest.v0';
@@ -88,21 +90,6 @@ function listContentFiles(distDir) {
88
90
  out.sort();
89
91
  return out;
90
92
  }
91
- /**
92
- * @param {string} chunkId
93
- */
94
- function pathPatternFromChunk(chunkId) {
95
- const rel = chunkId.replace(/^pages\//, '');
96
- const parts = rel.split('/').filter(Boolean);
97
- const segs = [];
98
- for (let i = 0; i < parts.length; i++) {
99
- const p = parts[i];
100
- if (p === 'index' && i === parts.length - 1)
101
- continue;
102
- segs.push(p);
103
- }
104
- return segs.length ? `/${segs.join('/')}` : '/';
105
- }
106
93
  /**
107
94
  * Pack `dist/` into `_vmz` manifests + release envelope (filesystem Delivery Profile).
108
95
  * @param {string} distDir
@@ -124,13 +111,13 @@ export function packRelease(distDir, opts = {}) {
124
111
  for (const rel of files) {
125
112
  fileDigests[rel] = sha256File(path.join(abs, ...rel.split('/')));
126
113
  }
127
- const pages = (deployment.units || []).filter((u) => u.kind === 'page');
114
+ const pages = listPublicPageUnits(deployment);
128
115
  const routeRealization = {
129
116
  schema: ROUTE_REALIZATION_TABLE_SCHEMA,
130
117
  routes: pages.map((u) => ({
131
118
  routeId: String(u.chunkId),
132
119
  chunkId: String(u.chunkId),
133
- pathPattern: pathPatternFromChunk(String(u.chunkId)),
120
+ pathPattern: unitBrowserPathPattern(u),
134
121
  clientEntry: u.clientEntry || null,
135
122
  programIr: u.programIr || null,
136
123
  })),
@@ -200,7 +187,7 @@ export function packRelease(distDir, opts = {}) {
200
187
  * @param {unknown} value
201
188
  */
202
189
  function writeJson(file, value) {
203
- fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
190
+ writePrettyJsonFile(file, value);
204
191
  }
205
192
  /**
206
193
  * @param {string} pointerPath