@vmz/vmz 0.1.13 → 0.1.15

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 (56) hide show
  1. package/dist/author-input.d.ts +19 -0
  2. package/dist/author-input.js +52 -0
  3. package/dist/build-assemble.js +18 -7
  4. package/dist/cdn-policy.js +5 -4
  5. package/dist/cli.js +106 -38
  6. package/dist/content-addressed-assets.d.ts +23 -2
  7. package/dist/content-addressed-assets.js +178 -32
  8. package/dist/delivery-profile.d.ts +30 -2
  9. package/dist/delivery-profile.js +139 -5
  10. package/dist/dev-session.d.ts +1 -0
  11. package/dist/dev-session.js +269 -65
  12. package/dist/dev-watch-roots.d.ts +80 -0
  13. package/dist/dev-watch-roots.js +245 -0
  14. package/dist/document-build.d.ts +4 -3
  15. package/dist/document-build.js +104 -82
  16. package/dist/document-check.d.ts +24 -5
  17. package/dist/document-check.js +64 -101
  18. package/dist/document-cmd.js +2 -9
  19. package/dist/document-enrich.d.ts +2 -1
  20. package/dist/document-enrich.js +15 -4
  21. package/dist/document-integrate.js +29 -21
  22. package/dist/document-layout-render.d.ts +20 -0
  23. package/dist/document-layout-render.js +87 -0
  24. package/dist/document-routing-config.d.ts +17 -0
  25. package/dist/document-routing-config.js +31 -0
  26. package/dist/document-schema.js +8 -1
  27. package/dist/embedded-packaging.js +2 -1
  28. package/dist/index.d.ts +18 -2
  29. package/dist/index.js +63 -4
  30. package/dist/locale-check.js +81 -183
  31. package/dist/locale-cmd.js +13 -45
  32. package/dist/locale-route-emit.d.ts +4 -1
  33. package/dist/locale-route-emit.js +7 -32
  34. package/dist/locale-router.js +26 -8
  35. package/dist/native-addon.d.ts +9 -0
  36. package/dist/native-addon.js +84 -0
  37. package/dist/pack.js +3 -2
  38. package/dist/pretty-json.d.ts +19 -0
  39. package/dist/pretty-json.js +43 -0
  40. package/dist/production-observability.js +3 -2
  41. package/dist/production-test-pack.js +4 -3
  42. package/dist/public-static-assets.d.ts +24 -0
  43. package/dist/public-static-assets.js +140 -0
  44. package/dist/release-pack.js +5 -18
  45. package/dist/route-path.d.ts +35 -0
  46. package/dist/route-path.js +77 -0
  47. package/dist/server-artifact.js +4 -3
  48. package/dist/site-delivery.js +4 -3
  49. package/dist/site-favicon.d.ts +31 -0
  50. package/dist/site-favicon.js +140 -0
  51. package/dist/static-emit.d.ts +21 -0
  52. package/dist/static-emit.js +134 -97
  53. package/dist/test-cmd.js +2 -1
  54. package/dist/wechat-packaging.d.ts +22 -0
  55. package/dist/wechat-packaging.js +59 -0
  56. package/package.json +13 -14
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
- import JSON5 from 'json5';
7
+ import { parseAuthorInput } from './author-input.js';
8
8
  import { parseArgs } from './cli.js';
9
9
  import { checkLocales, emitLocaleTypedModules, localeHasErrors, planLocaleRename } from './locale-check.js';
10
10
  import { checkLocaleDelivery } from './locale-delivery.js';
@@ -12,6 +12,7 @@ import { checkLocaleRouter } from './locale-router.js';
12
12
  import { checkLocaleRuntime } from './locale-runtime.js';
13
13
  import { checkLocaleConformance, diffLocaleCatalogs, explainLocaleMessage, extractHardcodedText, pseudoLocalizeCatalog, } from './locale-tooling.js';
14
14
  import { log } from './log.js';
15
+ import { emitPrettyJson, generatePrettyJson } from './pretty-json.js';
15
16
  function printLocaleHelp() {
16
17
  console.log(`vmz locale — /locales application i18n (–)
17
18
 
@@ -91,14 +92,7 @@ function cmdLocaleCheck(args) {
91
92
  const report = checkLocales({ projectRoot, strict: Boolean(args.strict) });
92
93
  const jsonOut = args.json;
93
94
  if (jsonOut) {
94
- const text = JSON.stringify(report, null, 2);
95
- if (typeof jsonOut === 'string') {
96
- fs.writeFileSync(jsonOut, `${text}\n`, 'utf8');
97
- log.info(`wrote ${jsonOut}`);
98
- }
99
- else {
100
- console.log(text);
101
- }
95
+ emitPrettyJson(jsonOut, report, { logWrote: (p) => log.info(`wrote ${p}`) });
102
96
  }
103
97
  else {
104
98
  for (const d of report.diagnostics) {
@@ -151,7 +145,7 @@ function cmdLocaleRename(args) {
151
145
  const report = checkLocales({ projectRoot, checkUnused: false });
152
146
  const plan = planLocaleRename(report, fromId, toId);
153
147
  if (args.json) {
154
- console.log(JSON.stringify(plan, null, 2));
148
+ emitPrettyJson(args.json, plan);
155
149
  }
156
150
  else if (plan.status !== 'ready') {
157
151
  log.error(plan.error || 'rename failed');
@@ -181,14 +175,7 @@ function cmdLocaleRuntimeCheck(args) {
181
175
  });
182
176
  const { session: _session, ...jsonReport } = report;
183
177
  if (args.json) {
184
- const text = JSON.stringify(jsonReport, null, 2);
185
- if (typeof args.json === 'string') {
186
- fs.writeFileSync(args.json, `${text}\n`, 'utf8');
187
- log.info(`wrote ${args.json}`);
188
- }
189
- else {
190
- console.log(text);
191
- }
178
+ emitJson(args, jsonReport);
192
179
  }
193
180
  else {
194
181
  for (const d of report.diagnostics) {
@@ -205,7 +192,7 @@ function loadRoutesFixture(projectRoot) {
205
192
  const candidates = [path.join(projectRoot, 'locale-routes.json5'), path.join(projectRoot, 'locale-routes.json')];
206
193
  for (const p of candidates) {
207
194
  if (fs.existsSync(p)) {
208
- return JSON5.parse(fs.readFileSync(p, 'utf8'));
195
+ return parseAuthorInput(fs.readFileSync(p, 'utf8'));
209
196
  }
210
197
  }
211
198
  return {
@@ -231,14 +218,7 @@ function cmdLocaleRouterCheck(args) {
231
218
  origin: routesFile.origin || 'https://example.test',
232
219
  });
233
220
  if (args.json) {
234
- const text = JSON.stringify(report, null, 2);
235
- if (typeof args.json === 'string') {
236
- fs.writeFileSync(args.json, `${text}\n`, 'utf8');
237
- log.info(`wrote ${args.json}`);
238
- }
239
- else {
240
- console.log(text);
241
- }
221
+ emitJson(args, report);
242
222
  }
243
223
  else {
244
224
  for (const d of report.diagnostics) {
@@ -268,14 +248,7 @@ function cmdLocaleDeliveryCheck(args) {
268
248
  planVersion: 'plan.v0',
269
249
  });
270
250
  if (args.json) {
271
- const text = JSON.stringify(report, null, 2);
272
- if (typeof args.json === 'string') {
273
- fs.writeFileSync(args.json, `${text}\n`, 'utf8');
274
- log.info(`wrote ${args.json}`);
275
- }
276
- else {
277
- console.log(text);
278
- }
251
+ emitJson(args, report);
279
252
  }
280
253
  else {
281
254
  for (const d of report.diagnostics) {
@@ -290,14 +263,9 @@ function cmdLocaleDeliveryCheck(args) {
290
263
  return report.status === 'ready' ? 0 : 1;
291
264
  }
292
265
  function emitJson(args, report) {
293
- const text = JSON.stringify(report, null, 2);
294
- if (typeof args.json === 'string') {
295
- fs.writeFileSync(args.json, `${text}\n`, 'utf8');
296
- log.info(`wrote ${args.json}`);
297
- }
298
- else {
299
- console.log(text);
300
- }
266
+ emitPrettyJson(args.json, report, {
267
+ logWrote: (p) => log.info(`wrote ${p}`),
268
+ });
301
269
  }
302
270
  function cmdLocaleExplain(args) {
303
271
  const messageId = typeof args._[0] === 'string' ? args._[0] : '';
@@ -401,7 +369,7 @@ function cmdLocalePseudo(args) {
401
369
  const outDir = (typeof args.out === 'string' && args.out) || path.join(projectRoot, 'dist', 'locales-pseudo');
402
370
  fs.mkdirSync(outDir, { recursive: true });
403
371
  const outFile = path.join(outDir, `${report.pseudoLocale}.json`);
404
- fs.writeFileSync(outFile, `${JSON.stringify(report.catalog, null, 2)}\n`, 'utf8');
372
+ fs.writeFileSync(outFile, `${generatePrettyJson(report.catalog)}\n`, 'utf8');
405
373
  log.info(`locale pseudo: source=${sourceLocale} keys=${Object.keys(report.catalog).length} out=${outFile}`);
406
374
  }
407
375
  return report.status === 'ready' ? 0 : 1;
@@ -415,7 +383,7 @@ function cmdLocaleConformance(args) {
415
383
  const routesPath = path.join(projectRoot, 'locale-routes.json5');
416
384
  if (fs.existsSync(routesPath)) {
417
385
  try {
418
- const routesFile = JSON5.parse(fs.readFileSync(routesPath, 'utf8'));
386
+ const routesFile = parseAuthorInput(fs.readFileSync(routesPath, 'utf8'));
419
387
  routeIds = (routesFile.routes || []).map((r) => r.routeId);
420
388
  }
421
389
  catch {
@@ -26,7 +26,10 @@ export declare function emitLocaleRouteRealization(projectRoot: any, distDir: an
26
26
  defaultLocale: any;
27
27
  };
28
28
  origin: string;
29
- routes: any;
29
+ routes: {
30
+ routeId: string;
31
+ path: string;
32
+ }[];
30
33
  realizations: any[];
31
34
  pageMetas: any[];
32
35
  };
@@ -7,34 +7,9 @@ import fs from 'node:fs';
7
7
  import path from 'node:path';
8
8
  import { checkLocales, localeHasErrors } from './locale-check.js';
9
9
  import { buildLocalePageMeta, buildLocaleRouteRealizationTable } from './locale-router.js';
10
+ import { writePrettyJsonFile } from './pretty-json.js';
11
+ import { listPublicPageUnits, unitBrowserPathPattern } from './route-path.js';
10
12
  export const LOCALE_ROUTE_REALIZATION_ARTIFACT_SCHEMA = 'vmz.locale.route_realization.v0';
11
- /**
12
- * @param {string} chunkId
13
- */
14
- function pathPatternFromChunk(chunkId) {
15
- const rel = String(chunkId || '').replace(/^pages\//, '');
16
- const parts = rel.split('/').filter(Boolean);
17
- const segs = [];
18
- for (let i = 0; i < parts.length; i++) {
19
- const p = parts[i];
20
- if (p === 'index' && i === parts.length - 1)
21
- continue;
22
- segs.push(p);
23
- }
24
- return segs.length ? `/${segs.join('/')}` : '/';
25
- }
26
- /**
27
- * Layouts are not public RouteNodes for locale realization / SEO.
28
- * @param {string} chunkId
29
- */
30
- function isPublicPageChunk(chunkId) {
31
- const id = String(chunkId || '');
32
- if (!id.startsWith('pages/'))
33
- return false;
34
- if (/(^|\/)Layout$/.test(id))
35
- return false;
36
- return true;
37
- }
38
13
  /**
39
14
  * @param {string} projectRoot
40
15
  * @param {string} distDir
@@ -59,10 +34,10 @@ export function emitLocaleRouteRealization(projectRoot, distDir, opts = {}) {
59
34
  };
60
35
  }
61
36
  const deployment = JSON.parse(fs.readFileSync(deploymentPath, 'utf8'));
62
- const pages = (deployment.units || []).filter((u) => u.kind === 'page' && isPublicPageChunk(String(u.chunkId)));
37
+ const pages = listPublicPageUnits(deployment);
63
38
  const routes = pages.map((u) => ({
64
39
  routeId: String(u.chunkId),
65
- path: pathPatternFromChunk(String(u.chunkId)),
40
+ path: unitBrowserPathPattern(u),
66
41
  }));
67
42
  const localeEntries = report.manifest?.locales || [];
68
43
  const locales = localeEntries.map((l) => l.id);
@@ -117,14 +92,14 @@ export function emitLocaleRouteRealization(projectRoot, distDir, opts = {}) {
117
92
  const vmzDir = path.join(distDir, '_vmz');
118
93
  fs.mkdirSync(vmzDir, { recursive: true });
119
94
  const outPath = path.join(vmzDir, 'locale-route-realization.json');
120
- fs.writeFileSync(outPath, `${JSON.stringify(artifact, null, 2)}\n`, 'utf8');
95
+ writePrettyJsonFile(outPath, artifact);
121
96
  const manifestOut = path.join(vmzDir, 'locale-manifest.json');
122
- fs.writeFileSync(manifestOut, `${JSON.stringify({
97
+ writePrettyJsonFile(manifestOut, {
123
98
  schema: 'vmz.locale.manifest.v0',
124
99
  defaultLocale,
125
100
  locales: artifact.locales,
126
101
  routing: artifact.routing,
127
- }, null, 2)}\n`, 'utf8');
102
+ });
128
103
  return {
129
104
  ok: true,
130
105
  written: ['_vmz/locale-route-realization.json', '_vmz/locale-manifest.json'],
@@ -90,16 +90,34 @@ export function buildLocaleRouteRealizationTable(input) {
90
90
  pathPattern: normalizePath(route.path),
91
91
  };
92
92
  realizations.push(entry);
93
- const prev = pathOwners.get(entry.path);
94
- if (prev && prev !== `${route.routeId}@${localeId}`) {
95
- diagnostics.push({
96
- code: DIAG_LOCALE_ROUTE_COLLISION,
97
- severity: 'error',
98
- message: `path ${entry.path} claimed by ${prev} and ${route.routeId}@${localeId}`,
99
- });
93
+ // `none` / `domain`: LocaleId is Host preference — many locales share one URL path.
94
+ // Collision only when *different RouteIds* claim the same path.
95
+ if (routing.strategy === 'none' || routing.strategy === 'domain') {
96
+ const prevRoute = pathOwners.get(entry.path);
97
+ if (prevRoute && prevRoute !== route.routeId) {
98
+ diagnostics.push({
99
+ code: DIAG_LOCALE_ROUTE_COLLISION,
100
+ severity: 'error',
101
+ message: `path ${entry.path} claimed by ${prevRoute} and ${route.routeId}`,
102
+ });
103
+ }
104
+ else {
105
+ pathOwners.set(entry.path, route.routeId);
106
+ }
100
107
  }
101
108
  else {
102
- pathOwners.set(entry.path, `${route.routeId}@${localeId}`);
109
+ const owner = `${route.routeId}@${localeId}`;
110
+ const prev = pathOwners.get(entry.path);
111
+ if (prev && prev !== owner) {
112
+ diagnostics.push({
113
+ code: DIAG_LOCALE_ROUTE_COLLISION,
114
+ severity: 'error',
115
+ message: `path ${entry.path} claimed by ${prev} and ${owner}`,
116
+ });
117
+ }
118
+ else {
119
+ pathOwners.set(entry.path, owner);
120
+ }
103
121
  }
104
122
  }
105
123
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Load the N-API addon or throw (production printers must not fall back to TS).
3
+ * @returns {any}
4
+ */
5
+ export declare function requireNativeAddon(): any;
6
+ /**
7
+ * @returns {any | null}
8
+ */
9
+ export declare function tryLoadNativeAddon(): any;
@@ -0,0 +1,84 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Load the N-API `.node` addon without importing `index.js`
4
+ * (avoids cycles with modules re-exported from the package entry).
5
+ */
6
+ import fs from 'node:fs';
7
+ import { createRequire } from 'node:module';
8
+ import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ const require = createRequire(import.meta.url);
11
+ /** @type {any} */
12
+ let cached;
13
+ /**
14
+ * Load the N-API addon or throw (production printers must not fall back to TS).
15
+ * @returns {any}
16
+ */
17
+ export function requireNativeAddon() {
18
+ const native = tryLoadNativeAddon();
19
+ if (!native) {
20
+ throw new Error('vmz native addon missing — run `pnpm napi:build` (CodeGenerators live in vmz-generator via N-API)');
21
+ }
22
+ return native;
23
+ }
24
+ /**
25
+ * @returns {any | null}
26
+ */
27
+ export function tryLoadNativeAddon() {
28
+ if (cached !== undefined)
29
+ return cached;
30
+ try {
31
+ const envPath = (typeof process.env.VMZ_NATIVE_NODE === 'string' && process.env.VMZ_NATIVE_NODE.trim()) || '';
32
+ if (envPath) {
33
+ cached = require(path.resolve(envPath));
34
+ return cached;
35
+ }
36
+ const { platform, arch } = process;
37
+ let triple = `${platform}-${arch}`;
38
+ if (platform === 'win32' && arch === 'x64')
39
+ triple = 'win32-x64-msvc';
40
+ else if (platform === 'win32' && arch === 'arm64')
41
+ triple = 'win32-arm64-msvc';
42
+ else if (platform === 'darwin' && arch === 'arm64')
43
+ triple = 'darwin-arm64';
44
+ else if (platform === 'darwin' && arch === 'x64')
45
+ triple = 'darwin-x64';
46
+ else if (platform === 'linux' && arch === 'x64')
47
+ triple = 'linux-x64-gnu';
48
+ else if (platform === 'linux' && arch === 'arm64')
49
+ triple = 'linux-arm64-gnu';
50
+ const short = triple === 'win32-x64-msvc'
51
+ ? 'win32-x64'
52
+ : triple === 'win32-arm64-msvc'
53
+ ? 'win32-arm64'
54
+ : triple === 'linux-x64-gnu'
55
+ ? 'linux-x64'
56
+ : triple === 'linux-arm64-gnu'
57
+ ? 'linux-arm64'
58
+ : triple;
59
+ const pkgRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
60
+ const name = `@vmz/vmz-${short}`;
61
+ /** @type {string[]} */
62
+ const candidates = [];
63
+ try {
64
+ const resolved = require.resolve(`${name}/package.json`);
65
+ const dir = path.dirname(resolved);
66
+ candidates.push(path.join(dir, `vmz.${triple}.node`), path.join(dir, 'vmz.node'));
67
+ }
68
+ catch {
69
+ /* optional */
70
+ }
71
+ candidates.push(path.join(pkgRoot, 'node_modules', name, `vmz.${triple}.node`), path.join(pkgRoot, 'node_modules', name, 'vmz.node'));
72
+ for (const p of candidates) {
73
+ if (fs.existsSync(p)) {
74
+ cached = require(p);
75
+ return cached;
76
+ }
77
+ }
78
+ cached = null;
79
+ }
80
+ catch {
81
+ cached = null;
82
+ }
83
+ return cached;
84
+ }
package/dist/pack.js CHANGED
@@ -5,10 +5,11 @@
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
11
  import { packClientBareImports } from './pack-client-packages.js';
12
+ import { writePrettyJsonFile } from './pretty-json.js';
12
13
  export const PACK_MANIFEST_SCHEMA = 'vmz.pack.manifest.v0';
13
14
  /**
14
15
  * Ensure dom split companions sit next to vmz-dom.js (barrel imports ./dom-core.js).
@@ -100,7 +101,7 @@ export function packFromDeploymentIr(outDir, opts = {}) {
100
101
  const vmzDir = path.join(outDir, '_vmz');
101
102
  mkdirSync(vmzDir, { recursive: true });
102
103
  const file = path.join(vmzDir, 'pack-manifest.json');
103
- writeFileSync(file, `${JSON.stringify(body, null, 2)}\n`, 'utf8');
104
+ writePrettyJsonFile(file, body);
104
105
  return { manifest: body, path: file, clientPackages };
105
106
  }
106
107
  function stableStringify(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. */
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Opaque site-root static files: project `public/**` → deploy root (static / web-static).
3
+ * Not dependency packaging (no node_modules / @dxo/* scanning).
4
+ */
5
+ export declare const PUBLIC_STATIC_ASSETS_SCHEMA = "vmz.public_static_assets.v0";
6
+ /**
7
+ * @param {string} distDir
8
+ * @param {{ projectRoot?: string, publicDir?: string }} [opts]
9
+ */
10
+ export declare function emitPublicStaticAssets(distDir: any, opts?: {}): {
11
+ schema: string;
12
+ status: string;
13
+ reason: string;
14
+ fileCount: number;
15
+ files: any[];
16
+ skippedConflicts: any[];
17
+ } | {
18
+ schema: string;
19
+ status: string;
20
+ source: string;
21
+ fileCount: number;
22
+ files: any[];
23
+ skippedConflicts: any[];
24
+ };
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Opaque site-root static files: project `public/**` → deploy root (static / web-static).
3
+ * Not dependency packaging (no node_modules / @dxo/* scanning).
4
+ */
5
+ // @ts-nocheck
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { writePrettyJsonFile } from './pretty-json.js';
9
+ export const PUBLIC_STATIC_ASSETS_SCHEMA = 'vmz.public_static_assets.v0';
10
+ /** Relative paths under dist that VMZ owns; public/ must not clobber. */
11
+ const RESERVED_EXACT = new Set([
12
+ 'entry-client.js',
13
+ 'entry-event.js',
14
+ 'vmz-dom.js',
15
+ 'vmz-runtime.js',
16
+ 'vmz-http.js',
17
+ 'vmz-client-nav.js',
18
+ 'vmz.css',
19
+ 'vmz-designs.css',
20
+ 'vmz-style.css',
21
+ 'vmz-deployment.json',
22
+ 'vmz-routes.json',
23
+ 'vmz-plugin-targets.json',
24
+ 'sitemap.xml',
25
+ 'robots.txt',
26
+ '404.html',
27
+ ]);
28
+ /**
29
+ * @param {string} distDir
30
+ * @param {{ projectRoot?: string, publicDir?: string }} [opts]
31
+ */
32
+ export function emitPublicStaticAssets(distDir, opts = {}) {
33
+ const absDist = path.resolve(distDir);
34
+ const projectRoot = opts.projectRoot ? path.resolve(opts.projectRoot) : null;
35
+ const publicDir = opts.publicDir ? path.resolve(opts.publicDir) : projectRoot ? path.join(projectRoot, 'public') : null;
36
+ if (!publicDir || !fs.existsSync(publicDir) || !fs.statSync(publicDir).isDirectory()) {
37
+ const skipped = {
38
+ schema: PUBLIC_STATIC_ASSETS_SCHEMA,
39
+ status: 'skipped',
40
+ reason: publicDir ? 'public/ missing' : 'no projectRoot',
41
+ fileCount: 0,
42
+ files: [],
43
+ skippedConflicts: [],
44
+ };
45
+ writeArtifact(absDist, skipped);
46
+ return skipped;
47
+ }
48
+ /** @type {Array<{ path: string, bytes: number }>} */
49
+ const files = [];
50
+ /** @type {Array<{ path: string, reason: string }>} */
51
+ const skippedConflicts = [];
52
+ walkFiles(publicDir, (absFile) => {
53
+ const rel = path.relative(publicDir, absFile).replace(/\\/g, '/');
54
+ if (!rel || rel.startsWith('..'))
55
+ return;
56
+ if (shouldSkipReserved(rel)) {
57
+ skippedConflicts.push({ path: rel, reason: 'reserved-vmz-path' });
58
+ return;
59
+ }
60
+ const dest = path.join(absDist, ...rel.split('/'));
61
+ if (fs.existsSync(dest) && isReservedExisting(rel, dest, absDist)) {
62
+ skippedConflicts.push({ path: rel, reason: 'would-clobber-generated' });
63
+ return;
64
+ }
65
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
66
+ fs.copyFileSync(absFile, dest);
67
+ files.push({ path: rel, bytes: fs.statSync(dest).size });
68
+ });
69
+ files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
70
+ const artifact = {
71
+ schema: PUBLIC_STATIC_ASSETS_SCHEMA,
72
+ status: 'ready',
73
+ source: 'public/',
74
+ fileCount: files.length,
75
+ files,
76
+ skippedConflicts,
77
+ };
78
+ writeArtifact(absDist, artifact);
79
+ return artifact;
80
+ }
81
+ /**
82
+ * @param {string} rel
83
+ */
84
+ function shouldSkipReserved(rel) {
85
+ if (rel === '_vmz' || rel.startsWith('_vmz/'))
86
+ return true;
87
+ if (RESERVED_EXACT.has(rel))
88
+ return true;
89
+ if (rel.startsWith('pages/') || rel.startsWith('components/'))
90
+ return true;
91
+ if (/\.client\.js$/i.test(rel))
92
+ return true;
93
+ if (rel.startsWith('assets/') && /^assets\/[a-f0-9]{64}\./i.test(rel))
94
+ return true;
95
+ return false;
96
+ }
97
+ /**
98
+ * After compile, many generated files already exist; do not let public overwrite them.
99
+ * Allow overwrite of non-reserved paths (e.g. re-copy same wasm).
100
+ * @param {string} rel
101
+ * @param {string} dest
102
+ * @param {string} absDist
103
+ */
104
+ function isReservedExisting(rel, dest, absDist) {
105
+ if (shouldSkipReserved(rel))
106
+ return true;
107
+ // Never overwrite generated HTML shells that static-emit will (or did) write.
108
+ if (/\.html$/i.test(rel) && fs.existsSync(dest)) {
109
+ const text = fs.readFileSync(dest, 'utf8');
110
+ if (text.includes('data-vmz-page') || text.includes('entry-client'))
111
+ return true;
112
+ }
113
+ void absDist;
114
+ return false;
115
+ }
116
+ /**
117
+ * @param {string} dir
118
+ * @param {(absFile: string) => void} onFile
119
+ */
120
+ function walkFiles(dir, onFile) {
121
+ for (const name of fs.readdirSync(dir)) {
122
+ if (name === '.' || name === '..')
123
+ continue;
124
+ const full = path.join(dir, name);
125
+ const st = fs.statSync(full);
126
+ if (st.isDirectory())
127
+ walkFiles(full, onFile);
128
+ else if (st.isFile())
129
+ onFile(full);
130
+ }
131
+ }
132
+ /**
133
+ * @param {string} absDist
134
+ * @param {Record<string, unknown>} artifact
135
+ */
136
+ function writeArtifact(absDist, artifact) {
137
+ const vmzDir = path.join(absDist, '_vmz');
138
+ fs.mkdirSync(vmzDir, { recursive: true });
139
+ writePrettyJsonFile(path.join(vmzDir, 'public-static-assets.json'), artifact);
140
+ }