@vmz/vmz 0.0.3 → 0.1.0
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/README.md +6 -4
- package/dist/build-assemble.d.ts +52 -0
- package/dist/build-assemble.js +191 -0
- package/dist/cdn-policy.d.ts +196 -0
- package/dist/cdn-policy.js +443 -0
- package/dist/cli.js +152 -11
- package/dist/content-addressed-assets.d.ts +69 -0
- package/dist/content-addressed-assets.js +206 -0
- package/dist/delivery-profile.d.ts +74 -0
- package/dist/delivery-profile.js +279 -0
- package/dist/dev-session.js +49 -13
- package/dist/document-build.js +9 -4
- package/dist/document-designs.js +30 -2
- package/dist/document-enrich.js +8 -0
- package/dist/embedded-packaging.d.ts +22 -0
- package/dist/embedded-packaging.js +113 -0
- package/dist/index.d.ts +19 -3
- package/dist/index.js +35 -15
- package/dist/invocation.d.ts +8 -31
- package/dist/invocation.js +12 -33
- package/dist/locale-check.d.ts +16 -0
- package/dist/locale-check.js +131 -3
- package/dist/locale-cmd.js +2 -2
- package/dist/locale-route-emit.d.ts +34 -0
- package/dist/locale-route-emit.js +134 -0
- package/dist/locale-router.d.ts +20 -0
- package/dist/locale-router.js +68 -0
- package/dist/log.d.ts +2 -2
- package/dist/log.js +11 -3
- package/dist/pack.d.ts +40 -0
- package/dist/pack.js +108 -0
- package/dist/plugin-host.d.ts +10 -1
- package/dist/plugin-host.js +19 -2
- package/dist/port.d.ts +10 -0
- package/dist/port.js +46 -0
- package/dist/production-observability.d.ts +286 -0
- package/dist/production-observability.js +469 -0
- package/dist/production-test-pack.d.ts +144 -0
- package/dist/production-test-pack.js +447 -0
- package/dist/release-cmd.d.ts +8 -0
- package/dist/release-cmd.js +126 -0
- package/dist/release-pack.d.ts +96 -0
- package/dist/release-pack.js +346 -0
- package/dist/server-artifact.d.ts +140 -0
- package/dist/server-artifact.js +205 -0
- package/dist/server-language-backend.d.ts +89 -0
- package/dist/server-language-backend.js +121 -0
- package/dist/site-delivery.d.ts +134 -0
- package/dist/site-delivery.js +345 -0
- package/dist/static-emit.d.ts +145 -0
- package/dist/static-emit.js +577 -0
- package/package.json +13 -13
package/dist/cli.js
CHANGED
|
@@ -9,15 +9,23 @@ import { HOST_PROTOCOL, createWorkspace, getProtocolVersions, resolveCoreRuntime
|
|
|
9
9
|
import { createDevSession } from './dev-session.js';
|
|
10
10
|
import { gateGlobalProjectCommand, getInvocationContext, isGlobalAllowedCommand } from './invocation.js';
|
|
11
11
|
import { log } from './log.js';
|
|
12
|
+
import { findAvailablePort } from './port.js';
|
|
12
13
|
import { readPackageMeta, resolveWorkspaceDirs } from './resolve.js';
|
|
13
14
|
import { cmdTest } from './test-cmd.js';
|
|
14
15
|
import { cmdDocument } from './document-cmd.js';
|
|
15
16
|
import { buildIntegratedDocuments, projectHasDocuments } from './document-integrate.js';
|
|
16
17
|
import { cmdLocale } from './locale-cmd.js';
|
|
18
|
+
import { emitLocaleRuntimeModules, localeHasErrors } from './locale-check.js';
|
|
19
|
+
import { emitLocaleRouteRealization } from './locale-route-emit.js';
|
|
17
20
|
import { cmdApplication } from './application-cmd.js';
|
|
21
|
+
import { cmdArtifact } from './release-cmd.js';
|
|
18
22
|
import { cmdRefactor } from './refactor-cmd.js';
|
|
19
23
|
import { cmdExplain } from './explain-cmd.js';
|
|
20
24
|
import { resolveNativeVmzCli } from './resolve-native-cli.js';
|
|
25
|
+
import { loadVmzConfig } from './plugin-host.js';
|
|
26
|
+
import { normalizeDeliveryAuthoring, selectBuildProfile } from './delivery-profile.js';
|
|
27
|
+
import { packFromDeploymentIr } from './pack.js';
|
|
28
|
+
import { assembleDelivery, emitBuildProof } from './build-assemble.js';
|
|
21
29
|
/**
|
|
22
30
|
* @param {string[]} argv
|
|
23
31
|
*/
|
|
@@ -66,8 +74,11 @@ export function parseArgs(argv) {
|
|
|
66
74
|
export function printGlobalHelp() {
|
|
67
75
|
console.log(`vmz — global mode (scaffold only)
|
|
68
76
|
|
|
77
|
+
Install faces: @vmz/core (runtime) · @vmz/vmz (this CLI) · optional @vmz/ui / @vmz/plugin-*
|
|
78
|
+
|
|
79
|
+
Three install modes:
|
|
69
80
|
developer monorepo source (packages/runtimes/vmz) — full CLI
|
|
70
|
-
project app node_modules/@vmz/vmz
|
|
81
|
+
project app node_modules/@vmz/vmz — full CLI
|
|
71
82
|
global npm/pnpm -g — only new/init/help/version
|
|
72
83
|
|
|
73
84
|
You are in global mode. Pin \`@vmz/vmz\` in the app so check/build/lsp
|
|
@@ -79,11 +90,11 @@ Usage:
|
|
|
79
90
|
vmz help Show this help
|
|
80
91
|
|
|
81
92
|
Project commands:
|
|
82
|
-
pnpm add -D @vmz/vmz
|
|
93
|
+
pnpm add @vmz/core && pnpm add -D @vmz/vmz
|
|
83
94
|
pnpm exec vmz check
|
|
84
95
|
# or: vmz new my-app && cd my-app && pnpm install
|
|
85
96
|
|
|
86
|
-
If a project \`node_modules/@vmz/vmz\`
|
|
97
|
+
If a project \`node_modules/@vmz/vmz\` exists, a global
|
|
87
98
|
\`vmz <cmd>\` re-execs that bin.
|
|
88
99
|
`);
|
|
89
100
|
}
|
|
@@ -101,6 +112,7 @@ Usage:
|
|
|
101
112
|
vmz test [path] [options] Native test discover / report
|
|
102
113
|
vmz document|docs <cmd> Project /documents domain
|
|
103
114
|
vmz application <cmd> Application Collection / Mount
|
|
115
|
+
vmz artifact <cmd> Release pack / publish / rollback / diff (A3)
|
|
104
116
|
vmz refactor <cmd> DX rename plans / apply
|
|
105
117
|
vmz explain [style] <target> DX causal explain (style Theme chain)
|
|
106
118
|
vmz lsp [root] [--out-dir] Language server (stdio; native CLI)
|
|
@@ -110,9 +122,11 @@ Usage:
|
|
|
110
122
|
|
|
111
123
|
Options:
|
|
112
124
|
--out-dir, -o <dir> Output directory (default: dist)
|
|
113
|
-
--release Release build (
|
|
125
|
+
--release Release build (omit serve-host; pack minify slot; proof)
|
|
126
|
+
--profile <name> Delivery profile (default from config; builtins: web-ssr|web-static|web-client|web-hybrid)
|
|
127
|
+
--origin <url> Site origin for static-cdn canonical/sitemap
|
|
114
128
|
--host <host> Listen host (default: 127.0.0.1)
|
|
115
|
-
--port <port> Listen port (
|
|
129
|
+
--port <port> Listen port (dev: omit = auto from 5173; set = lock)
|
|
116
130
|
--poll-ms <ms> Dev watch poll interval (default: 300)
|
|
117
131
|
--build Build before serve
|
|
118
132
|
--check Format check-only (format)
|
|
@@ -197,6 +211,10 @@ export async function runCli(argv, opts = {}) {
|
|
|
197
211
|
case 'applications':
|
|
198
212
|
case 'app':
|
|
199
213
|
return cmdApplication(rest);
|
|
214
|
+
case 'artifact':
|
|
215
|
+
case 'artifacts':
|
|
216
|
+
case 'release':
|
|
217
|
+
return cmdArtifact(rest);
|
|
200
218
|
case 'refactor':
|
|
201
219
|
return cmdRefactor(rest);
|
|
202
220
|
case 'explain':
|
|
@@ -265,7 +283,11 @@ function cmdCheck(args) {
|
|
|
265
283
|
try {
|
|
266
284
|
return runWithPlugins(ws, project, outDir, async () => {
|
|
267
285
|
const report = ws.check();
|
|
268
|
-
const
|
|
286
|
+
const { checkLocales, localeHasErrors } = await import('./locale-check.js');
|
|
287
|
+
const localeReport = checkLocales({ projectRoot: project, checkUnused: false });
|
|
288
|
+
// Locale policy is first-class: missing /locales is warning (not silent), hard errors still fail.
|
|
289
|
+
const errors = log.diagnostics([...(report.diagnostics ?? []), ...(localeReport.diagnostics ?? [])]) ||
|
|
290
|
+
(localeHasErrors(localeReport) ? 1 : 0);
|
|
269
291
|
log.info(`checked ${report.filesChecked} file(s)`);
|
|
270
292
|
return errors ? 1 : 0;
|
|
271
293
|
});
|
|
@@ -274,11 +296,28 @@ function cmdCheck(args) {
|
|
|
274
296
|
ws.dispose();
|
|
275
297
|
}
|
|
276
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Dedupe locale/build diagnostics by code+path+message (runtime + route emit both report missing manifest).
|
|
301
|
+
* @param {Array<{ code?: string, path?: string, message?: string, severity?: string }>} list
|
|
302
|
+
*/
|
|
303
|
+
function dedupeDiagnostics(list) {
|
|
304
|
+
const seen = new Set();
|
|
305
|
+
/** @type {typeof list} */
|
|
306
|
+
const out = [];
|
|
307
|
+
for (const d of list || []) {
|
|
308
|
+
const key = `${d.code || ''}\0${d.path || ''}\0${d.message || ''}\0${d.severity || ''}`;
|
|
309
|
+
if (seen.has(key))
|
|
310
|
+
continue;
|
|
311
|
+
seen.add(key);
|
|
312
|
+
out.push(d);
|
|
313
|
+
}
|
|
314
|
+
return out;
|
|
315
|
+
}
|
|
277
316
|
/**
|
|
278
317
|
* @param {import('./index.js').Workspace} ws
|
|
279
318
|
* @param {string} project
|
|
280
319
|
* @param {string} outDir
|
|
281
|
-
* @param { => Promise<number> | number} fn
|
|
320
|
+
* @param {() => Promise<number> | number} fn
|
|
282
321
|
*/
|
|
283
322
|
async function runWithPlugins(ws, project, outDir, fn) {
|
|
284
323
|
const { loadVmzConfig, applyPlugins } = await import('./plugin-host.js');
|
|
@@ -300,6 +339,21 @@ async function cmdBuild(args) {
|
|
|
300
339
|
log.info(`build ${project} → ${outDir}`);
|
|
301
340
|
const ws = createWorkspace({ root: project, outDir });
|
|
302
341
|
try {
|
|
342
|
+
const cfg = await loadVmzConfig(project);
|
|
343
|
+
const cliProfile = typeof args.profile === 'string' ? args.profile : '';
|
|
344
|
+
const norm = normalizeDeliveryAuthoring(cfg.delivery ?? null);
|
|
345
|
+
if (!norm.ok) {
|
|
346
|
+
log.diagnostics(norm.diagnostics ?? []);
|
|
347
|
+
log.error('delivery authoring invalid');
|
|
348
|
+
return 1;
|
|
349
|
+
}
|
|
350
|
+
const selected = selectBuildProfile(norm.table, cliProfile);
|
|
351
|
+
if (!selected.ok) {
|
|
352
|
+
log.diagnostics(selected.diagnostics ?? []);
|
|
353
|
+
log.error(`unknown build --profile ${cliProfile || norm.table.default}`);
|
|
354
|
+
return 1;
|
|
355
|
+
}
|
|
356
|
+
log.info(`delivery profile ${selected.selection.profileId} (assembly=${selected.selection.assembly})`);
|
|
303
357
|
const code = await runWithPlugins(ws, project, outDir, () => {
|
|
304
358
|
const report = ws.build(Boolean(args.release));
|
|
305
359
|
const errors = log.diagnostics(report.diagnostics ?? []);
|
|
@@ -313,20 +367,92 @@ async function cmdBuild(args) {
|
|
|
313
367
|
});
|
|
314
368
|
if (code !== 0)
|
|
315
369
|
return code;
|
|
370
|
+
const localeEmit = emitLocaleRuntimeModules(project, outDir);
|
|
371
|
+
const localeRoutes = emitLocaleRouteRealization(project, outDir, {
|
|
372
|
+
origin: typeof args.origin === 'string' ? args.origin : undefined,
|
|
373
|
+
});
|
|
374
|
+
// Always surface locale diagnostics (warnings included) — missing /locales must not be silent.
|
|
375
|
+
// Dedupe: runtime emit + route realization both report the same missing-manifest warning.
|
|
376
|
+
const localeDiags = dedupeDiagnostics([
|
|
377
|
+
...(localeEmit.diagnostics ?? []),
|
|
378
|
+
...(localeRoutes.diagnostics ?? []),
|
|
379
|
+
]);
|
|
380
|
+
log.diagnostics(localeDiags);
|
|
381
|
+
if (!localeEmit.ok || localeHasErrors({ diagnostics: localeEmit.diagnostics })) {
|
|
382
|
+
log.error('locale runtime emit failed');
|
|
383
|
+
return 1;
|
|
384
|
+
}
|
|
385
|
+
if (localeEmit.written.length) {
|
|
386
|
+
log.info(`locale runtime emit (${localeEmit.written.length} module(s))`);
|
|
387
|
+
}
|
|
388
|
+
if (!localeRoutes.ok) {
|
|
389
|
+
log.error('locale route realization emit failed');
|
|
390
|
+
return 1;
|
|
391
|
+
}
|
|
392
|
+
if (localeRoutes.written.length) {
|
|
393
|
+
log.info(`locale route realization (${localeRoutes.written.length} artifact(s))`);
|
|
394
|
+
}
|
|
316
395
|
if (projectHasDocuments(project)) {
|
|
317
396
|
const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
|
|
318
397
|
if (!docs.ok)
|
|
319
398
|
return 1;
|
|
320
399
|
}
|
|
400
|
+
let pack = null;
|
|
401
|
+
try {
|
|
402
|
+
pack = packFromDeploymentIr(outDir, {
|
|
403
|
+
release: Boolean(args.release),
|
|
404
|
+
profileId: selected.selection.profileId,
|
|
405
|
+
assembly: selected.selection.assembly,
|
|
406
|
+
coreDist: resolveCoreRuntimeDist(),
|
|
407
|
+
});
|
|
408
|
+
log.info(`pack ok (units=${pack.manifest.unitCount}, digest=${String(pack.manifest.packDigest).slice(0, 12)}…)`);
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
log.error(`pack failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
412
|
+
return 1;
|
|
413
|
+
}
|
|
414
|
+
const origin = typeof args.origin === 'string' ? args.origin : undefined;
|
|
415
|
+
let assemble = null;
|
|
416
|
+
try {
|
|
417
|
+
if (selected.selection.assembly === 'static-cdn') {
|
|
418
|
+
log.info(`web-static emit ${outDir}`);
|
|
419
|
+
}
|
|
420
|
+
assemble = await assembleDelivery(outDir, {
|
|
421
|
+
selection: selected.selection,
|
|
422
|
+
profile: {
|
|
423
|
+
...selected.profile,
|
|
424
|
+
sources: selected.profile.sources || (norm.table.sugar ? norm.table.profiles[norm.table.default]?.sources : null),
|
|
425
|
+
},
|
|
426
|
+
siteId: cfg.application?.id || undefined,
|
|
427
|
+
origin,
|
|
428
|
+
pack: pack.manifest,
|
|
429
|
+
});
|
|
430
|
+
for (const step of assemble.manifest.steps || []) {
|
|
431
|
+
if (step.kind === 'static-cdn') {
|
|
432
|
+
log.info(`web-static ok (${step.htmlFiles} html, ${step.skipped} skipped, digest=${String(step.digest).slice(0, 12)}…)`);
|
|
433
|
+
}
|
|
434
|
+
else if (step.kind === 'site-delivery' && step.digest) {
|
|
435
|
+
log.info(`site-delivery ok (digest=${String(step.digest).slice(0, 12)}…)`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
log.error(`assemble failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
441
|
+
return 1;
|
|
442
|
+
}
|
|
443
|
+
const proof = emitBuildProof(outDir, {
|
|
444
|
+
selection: selected.selection,
|
|
445
|
+
pack: pack.manifest,
|
|
446
|
+
assemble: assemble.manifest,
|
|
447
|
+
release: Boolean(args.release),
|
|
448
|
+
});
|
|
449
|
+
log.info(`build-proof ok (profile=${proof.proof.profileId}, slots=${proof.proof.semanticIds.join(',')})`);
|
|
321
450
|
return 0;
|
|
322
451
|
}
|
|
323
452
|
finally {
|
|
324
453
|
ws.dispose();
|
|
325
454
|
}
|
|
326
455
|
}
|
|
327
|
-
/**
|
|
328
|
-
* @param {Record<string, string | boolean> & { _: string[] }} args
|
|
329
|
-
*/
|
|
330
456
|
async function cmdServe(args) {
|
|
331
457
|
const pathArg = args._[0] ?? '.';
|
|
332
458
|
if (args.build) {
|
|
@@ -391,7 +517,22 @@ async function cmdDev(args) {
|
|
|
391
517
|
outDir: typeof args['out-dir'] === 'string' ? args['out-dir'] : undefined,
|
|
392
518
|
});
|
|
393
519
|
const host = typeof args.host === 'string' ? args.host : '127.0.0.1';
|
|
394
|
-
const
|
|
520
|
+
const portLocked = Object.prototype.hasOwnProperty.call(args, 'port');
|
|
521
|
+
let port;
|
|
522
|
+
if (portLocked) {
|
|
523
|
+
port = Number(args.port);
|
|
524
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
525
|
+
log.error(`invalid --port ${String(args.port)}`);
|
|
526
|
+
return 1;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
const preferred = 5173;
|
|
531
|
+
port = await findAvailablePort(host, preferred);
|
|
532
|
+
if (port !== preferred) {
|
|
533
|
+
log.info(`port ${preferred} busy → using ${port}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
395
536
|
const pollMs = Number(args['poll-ms'] ?? 300);
|
|
396
537
|
const ac = new AbortController();
|
|
397
538
|
const onSig = () => {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A3: content-addressed assets/<hash> layout for immutable CDN objects.
|
|
3
|
+
* Logical paths stay available for serve/dev; web-static HTML rewrites to hashed URLs.
|
|
4
|
+
* Identical bytes → identical asset path (cross-release / cross-source reuse by digest).
|
|
5
|
+
*/
|
|
6
|
+
export declare const CONTENT_ADDRESSED_ASSETS_SCHEMA = "vmz.content_addressed_assets.v0";
|
|
7
|
+
/**
|
|
8
|
+
* Emit `assets/<sha256>.<ext>` copies and rewrite HTML href/src to hashed URLs.
|
|
9
|
+
* @param {string} distDir
|
|
10
|
+
* @param {{ candidates?: string[], rewriteHtml?: boolean }} [opts]
|
|
11
|
+
*/
|
|
12
|
+
export declare function emitContentAddressedAssets(distDir: any, opts?: {}): {
|
|
13
|
+
manifest: {
|
|
14
|
+
schema: string;
|
|
15
|
+
layout: string;
|
|
16
|
+
immutable: boolean;
|
|
17
|
+
objectCount: number;
|
|
18
|
+
objects: any[];
|
|
19
|
+
rewrittenHtml: number;
|
|
20
|
+
};
|
|
21
|
+
assetsDir: string;
|
|
22
|
+
rewrites: {};
|
|
23
|
+
manifestPath: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Resolve an immutable object by digest under dist/assets (cross-source reuse).
|
|
27
|
+
* @param {string} distDir
|
|
28
|
+
* @param {string} digest
|
|
29
|
+
* @param {string} [ext]
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveAssetByDigest(distDir: any, digest: any, ext?: string): {
|
|
32
|
+
assetPath: string;
|
|
33
|
+
digest: string;
|
|
34
|
+
bytes: number;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Prove two buffers share one asset path (content-address stability).
|
|
38
|
+
* @param {string} distDir
|
|
39
|
+
* @param {Buffer|string} a
|
|
40
|
+
* @param {Buffer|string} b
|
|
41
|
+
* @param {string} [ext]
|
|
42
|
+
*/
|
|
43
|
+
export declare function assertSharedAssetPath(distDir: any, a: any, b: any, ext?: string): {
|
|
44
|
+
ok: boolean;
|
|
45
|
+
reason: string;
|
|
46
|
+
digestA: string;
|
|
47
|
+
digestB: string;
|
|
48
|
+
rel?: undefined;
|
|
49
|
+
assetPath?: undefined;
|
|
50
|
+
digest?: undefined;
|
|
51
|
+
} | {
|
|
52
|
+
ok: boolean;
|
|
53
|
+
reason: string;
|
|
54
|
+
rel: string;
|
|
55
|
+
digestA?: undefined;
|
|
56
|
+
digestB?: undefined;
|
|
57
|
+
assetPath?: undefined;
|
|
58
|
+
digest?: undefined;
|
|
59
|
+
} | {
|
|
60
|
+
ok: boolean;
|
|
61
|
+
assetPath: string;
|
|
62
|
+
digest: string;
|
|
63
|
+
reason?: undefined;
|
|
64
|
+
digestA?: undefined;
|
|
65
|
+
digestB?: undefined;
|
|
66
|
+
rel?: undefined;
|
|
67
|
+
};
|
|
68
|
+
export declare function contentAddressedAssetsDigest(manifest: any): any;
|
|
69
|
+
export declare function sha256Buffer(buf: any): string;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A3: content-addressed assets/<hash> layout for immutable CDN objects.
|
|
3
|
+
* Logical paths stay available for serve/dev; web-static HTML rewrites to hashed URLs.
|
|
4
|
+
* Identical bytes → identical asset path (cross-release / cross-source reuse by digest).
|
|
5
|
+
*/
|
|
6
|
+
// @ts-nocheck
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { canonicalJson, sha256Hex } from './release-pack.js';
|
|
11
|
+
export const CONTENT_ADDRESSED_ASSETS_SCHEMA = 'vmz.content_addressed_assets.v0';
|
|
12
|
+
/** Immutable delivery candidates (client-facing bytes). */
|
|
13
|
+
const DEFAULT_CANDIDATES = [
|
|
14
|
+
'entry-client.js',
|
|
15
|
+
'entry-event.js',
|
|
16
|
+
'vmz.css',
|
|
17
|
+
'vmz-designs.css',
|
|
18
|
+
'vmz-dom.js',
|
|
19
|
+
'vmz-runtime.js',
|
|
20
|
+
'vmz-http.js',
|
|
21
|
+
'vmz-client-nav.js',
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Emit `assets/<sha256>.<ext>` copies and rewrite HTML href/src to hashed URLs.
|
|
25
|
+
* @param {string} distDir
|
|
26
|
+
* @param {{ candidates?: string[], rewriteHtml?: boolean }} [opts]
|
|
27
|
+
*/
|
|
28
|
+
export function emitContentAddressedAssets(distDir, opts = {}) {
|
|
29
|
+
const abs = path.resolve(distDir);
|
|
30
|
+
if (!fs.existsSync(abs)) {
|
|
31
|
+
throw new Error(`emitContentAddressedAssets: missing dist ${abs}`);
|
|
32
|
+
}
|
|
33
|
+
const assetsDir = path.join(abs, 'assets');
|
|
34
|
+
fs.mkdirSync(assetsDir, { recursive: true });
|
|
35
|
+
const candidates = Array.isArray(opts.candidates) ? opts.candidates : collectCandidates(abs);
|
|
36
|
+
/** @type {Array<Record<string, any>>} */
|
|
37
|
+
const objects = [];
|
|
38
|
+
/** @type {Record<string, string>} */
|
|
39
|
+
const rewrites = {};
|
|
40
|
+
for (const rel of candidates) {
|
|
41
|
+
const logical = String(rel).replace(/\\/g, '/').replace(/^\//, '');
|
|
42
|
+
const src = path.join(abs, ...logical.split('/'));
|
|
43
|
+
if (!fs.existsSync(src) || !fs.statSync(src).isFile())
|
|
44
|
+
continue;
|
|
45
|
+
const buf = fs.readFileSync(src);
|
|
46
|
+
const digest = sha256Hex(buf);
|
|
47
|
+
const ext = path.extname(logical) || '';
|
|
48
|
+
const assetRel = `assets/${digest}${ext}`;
|
|
49
|
+
const dest = path.join(abs, ...assetRel.split('/'));
|
|
50
|
+
if (!fs.existsSync(dest)) {
|
|
51
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
52
|
+
fs.writeFileSync(dest, buf);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// Cross-release reuse: identical digest must not be rewritten.
|
|
56
|
+
const existing = sha256Hex(fs.readFileSync(dest));
|
|
57
|
+
if (existing !== digest) {
|
|
58
|
+
throw new Error(`content-address collision at ${assetRel}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
objects.push({
|
|
62
|
+
logicalPath: logical,
|
|
63
|
+
assetPath: assetRel,
|
|
64
|
+
digest,
|
|
65
|
+
bytes: buf.length,
|
|
66
|
+
immutable: true,
|
|
67
|
+
});
|
|
68
|
+
rewrites[`/${logical}`] = `/${assetRel}`;
|
|
69
|
+
rewrites[logical] = assetRel;
|
|
70
|
+
}
|
|
71
|
+
objects.sort((a, b) => (a.logicalPath < b.logicalPath ? -1 : a.logicalPath > b.logicalPath ? 1 : 0));
|
|
72
|
+
let rewrittenHtml = 0;
|
|
73
|
+
if (opts.rewriteHtml !== false) {
|
|
74
|
+
rewrittenHtml = rewriteHtmlReferences(abs, rewrites);
|
|
75
|
+
}
|
|
76
|
+
const manifest = {
|
|
77
|
+
schema: CONTENT_ADDRESSED_ASSETS_SCHEMA,
|
|
78
|
+
layout: 'assets/<sha256>.<ext>',
|
|
79
|
+
immutable: true,
|
|
80
|
+
objectCount: objects.length,
|
|
81
|
+
objects,
|
|
82
|
+
rewrittenHtml,
|
|
83
|
+
};
|
|
84
|
+
manifest.manifestDigest = sha256Hex(canonicalJson({ ...manifest, manifestDigest: undefined }));
|
|
85
|
+
const vmzDir = path.join(abs, '_vmz');
|
|
86
|
+
fs.mkdirSync(vmzDir, { recursive: true });
|
|
87
|
+
const outPath = path.join(vmzDir, 'content-addressed-assets.json');
|
|
88
|
+
fs.writeFileSync(outPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
89
|
+
return { manifest, assetsDir, rewrites, manifestPath: outPath };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Resolve an immutable object by digest under dist/assets (cross-source reuse).
|
|
93
|
+
* @param {string} distDir
|
|
94
|
+
* @param {string} digest
|
|
95
|
+
* @param {string} [ext]
|
|
96
|
+
*/
|
|
97
|
+
export function resolveAssetByDigest(distDir, digest, ext = '') {
|
|
98
|
+
const d = String(digest || '').trim();
|
|
99
|
+
if (!/^[a-f0-9]{64}$/i.test(d))
|
|
100
|
+
return null;
|
|
101
|
+
const suffix = ext && !ext.startsWith('.') ? `.${ext}` : ext;
|
|
102
|
+
const rel = `assets/${d}${suffix}`;
|
|
103
|
+
const abs = path.join(distDir, ...rel.split('/'));
|
|
104
|
+
if (!fs.existsSync(abs))
|
|
105
|
+
return null;
|
|
106
|
+
return { assetPath: rel, digest: d.toLowerCase(), bytes: fs.statSync(abs).size };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Prove two buffers share one asset path (content-address stability).
|
|
110
|
+
* @param {string} distDir
|
|
111
|
+
* @param {Buffer|string} a
|
|
112
|
+
* @param {Buffer|string} b
|
|
113
|
+
* @param {string} [ext]
|
|
114
|
+
*/
|
|
115
|
+
export function assertSharedAssetPath(distDir, a, b, ext = '.js') {
|
|
116
|
+
const da = sha256Hex(a);
|
|
117
|
+
const db = sha256Hex(b);
|
|
118
|
+
if (da !== db) {
|
|
119
|
+
return { ok: false, reason: 'digests differ', digestA: da, digestB: db };
|
|
120
|
+
}
|
|
121
|
+
const assetsDir = path.join(distDir, 'assets');
|
|
122
|
+
fs.mkdirSync(assetsDir, { recursive: true });
|
|
123
|
+
const rel = `assets/${da}${ext}`;
|
|
124
|
+
const dest = path.join(distDir, ...rel.split('/'));
|
|
125
|
+
fs.writeFileSync(dest, typeof a === 'string' ? Buffer.from(a) : a);
|
|
126
|
+
// Second write of identical bytes must be reuse, not fork.
|
|
127
|
+
fs.writeFileSync(dest, typeof b === 'string' ? Buffer.from(b) : b);
|
|
128
|
+
const again = resolveAssetByDigest(distDir, da, ext);
|
|
129
|
+
if (!again || again.assetPath !== rel) {
|
|
130
|
+
return { ok: false, reason: 'resolve missed shared path', rel };
|
|
131
|
+
}
|
|
132
|
+
return { ok: true, assetPath: rel, digest: da };
|
|
133
|
+
}
|
|
134
|
+
function collectCandidates(distDir) {
|
|
135
|
+
/** @type {string[]} */
|
|
136
|
+
const out = [];
|
|
137
|
+
for (const name of DEFAULT_CANDIDATES) {
|
|
138
|
+
if (fs.existsSync(path.join(distDir, name)))
|
|
139
|
+
out.push(name);
|
|
140
|
+
}
|
|
141
|
+
// Include top-level *.client.js and pages/**/*.client.js referenced by resume.
|
|
142
|
+
walk(distDir, distDir, (rel) => {
|
|
143
|
+
if (/\.client\.js$/i.test(rel))
|
|
144
|
+
out.push(rel);
|
|
145
|
+
});
|
|
146
|
+
return [...new Set(out)].sort();
|
|
147
|
+
}
|
|
148
|
+
function walk(root, dir, onFile) {
|
|
149
|
+
for (const name of fs.readdirSync(dir)) {
|
|
150
|
+
if (name === 'assets' || name === '_vmz' || name === 'node_modules')
|
|
151
|
+
continue;
|
|
152
|
+
const full = path.join(dir, name);
|
|
153
|
+
const st = fs.statSync(full);
|
|
154
|
+
if (st.isDirectory())
|
|
155
|
+
walk(root, full, onFile);
|
|
156
|
+
else
|
|
157
|
+
onFile(path.relative(root, full).replace(/\\/g, '/'));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* @param {string} distDir
|
|
162
|
+
* @param {Record<string, string>} rewrites map `/logical` → `/assets/hash.ext`
|
|
163
|
+
*/
|
|
164
|
+
function rewriteHtmlReferences(distDir, rewrites) {
|
|
165
|
+
const pairs = Object.entries(rewrites)
|
|
166
|
+
.filter(([from]) => from.startsWith('/'))
|
|
167
|
+
.sort((a, b) => b[0].length - a[0].length);
|
|
168
|
+
if (!pairs.length)
|
|
169
|
+
return 0;
|
|
170
|
+
let count = 0;
|
|
171
|
+
walkHtml(distDir, (file) => {
|
|
172
|
+
let text = fs.readFileSync(file, 'utf8');
|
|
173
|
+
let next = text;
|
|
174
|
+
for (const [from, to] of pairs) {
|
|
175
|
+
// href="/x" src="/x" and unquoted variants in attributes
|
|
176
|
+
next = next.split(from).join(to);
|
|
177
|
+
}
|
|
178
|
+
if (next !== text) {
|
|
179
|
+
fs.writeFileSync(file, next, 'utf8');
|
|
180
|
+
count += 1;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
return count;
|
|
184
|
+
}
|
|
185
|
+
function walkHtml(dir, onFile) {
|
|
186
|
+
const stack = [dir];
|
|
187
|
+
while (stack.length) {
|
|
188
|
+
const cur = stack.pop();
|
|
189
|
+
for (const name of fs.readdirSync(cur)) {
|
|
190
|
+
if (name === 'assets' || name === '_vmz' || name === 'node_modules')
|
|
191
|
+
continue;
|
|
192
|
+
const full = path.join(cur, name);
|
|
193
|
+
const st = fs.statSync(full);
|
|
194
|
+
if (st.isDirectory())
|
|
195
|
+
stack.push(full);
|
|
196
|
+
else if (name.endsWith('.html'))
|
|
197
|
+
onFile(full);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
export function contentAddressedAssetsDigest(manifest) {
|
|
202
|
+
return manifest.manifestDigest || sha256Hex(canonicalJson({ ...manifest, manifestDigest: undefined }));
|
|
203
|
+
}
|
|
204
|
+
export function sha256Buffer(buf) {
|
|
205
|
+
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
206
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B0 — Delivery profile authoring normalize + CLI --profile resolve.
|
|
3
|
+
* Pure data only; expands legacy site-delivery sugar into profiles[default].
|
|
4
|
+
*/
|
|
5
|
+
export declare const DELIVERY_PROFILE_AUTHORING_SCHEMA = "vmz.delivery.authoring.v0";
|
|
6
|
+
export declare const BUILD_PROFILE_SELECTION_SCHEMA = "vmz.build.profile_selection.v0";
|
|
7
|
+
/** Browser-era assembly kinds (04 B5). */
|
|
8
|
+
export declare const ASSEMBLIES: readonly string[];
|
|
9
|
+
export declare const SERVER_RUNTIMES: readonly string[];
|
|
10
|
+
/** Official built-in aliases when not overridden in config. */
|
|
11
|
+
export declare const BUILTIN_PROFILES: Readonly<{
|
|
12
|
+
'web-client': {
|
|
13
|
+
host: string;
|
|
14
|
+
assembly: string;
|
|
15
|
+
};
|
|
16
|
+
'web-static': {
|
|
17
|
+
host: string;
|
|
18
|
+
assembly: string;
|
|
19
|
+
};
|
|
20
|
+
'web-ssr': {
|
|
21
|
+
host: string;
|
|
22
|
+
assembly: string;
|
|
23
|
+
serverRuntime: string;
|
|
24
|
+
};
|
|
25
|
+
'web-hybrid': {
|
|
26
|
+
host: string;
|
|
27
|
+
assembly: string;
|
|
28
|
+
serverRuntime: string;
|
|
29
|
+
};
|
|
30
|
+
}>;
|
|
31
|
+
export declare function pickSiteAuthoring(raw: any): {
|
|
32
|
+
artifact: string;
|
|
33
|
+
sources: any;
|
|
34
|
+
};
|
|
35
|
+
export declare function normalizeDeliveryAuthoring(raw: any): {
|
|
36
|
+
ok: boolean;
|
|
37
|
+
table: {
|
|
38
|
+
schema: string;
|
|
39
|
+
default: string;
|
|
40
|
+
profiles: {};
|
|
41
|
+
sugar: boolean;
|
|
42
|
+
};
|
|
43
|
+
diagnostics?: undefined;
|
|
44
|
+
} | {
|
|
45
|
+
ok: boolean;
|
|
46
|
+
diagnostics: any[];
|
|
47
|
+
table?: undefined;
|
|
48
|
+
};
|
|
49
|
+
export declare function selectBuildProfile(table: any, cliProfile?: string): {
|
|
50
|
+
ok: boolean;
|
|
51
|
+
diagnostics: {
|
|
52
|
+
code: string;
|
|
53
|
+
message: string;
|
|
54
|
+
}[];
|
|
55
|
+
selection?: undefined;
|
|
56
|
+
profile?: undefined;
|
|
57
|
+
} | {
|
|
58
|
+
ok: boolean;
|
|
59
|
+
selection: {
|
|
60
|
+
schema: string;
|
|
61
|
+
profileId: any;
|
|
62
|
+
host: any;
|
|
63
|
+
assembly: any;
|
|
64
|
+
serverRuntime: any;
|
|
65
|
+
hasSiteSources: boolean;
|
|
66
|
+
authoringDigest: any;
|
|
67
|
+
fromCli: boolean;
|
|
68
|
+
};
|
|
69
|
+
profile: any;
|
|
70
|
+
diagnostics?: undefined;
|
|
71
|
+
};
|
|
72
|
+
export declare function semanticIdsForAssembly(assembly: any): string[];
|
|
73
|
+
export declare function canonicalJson(value: any): string;
|
|
74
|
+
export declare function sha256Hex(text: any): string;
|