@vmz/vmz 0.0.2 → 0.0.4
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 +19 -13
- package/dist/application-cmd.d.ts +1 -2
- package/dist/application-cmd.js +9 -10
- package/dist/bundler-adapter.d.ts +2 -3
- package/dist/bundler-adapter.js +2 -3
- package/dist/cdn-policy.d.ts +178 -0
- package/dist/cdn-policy.js +344 -0
- package/dist/cli.d.ts +10 -2
- package/dist/cli.js +179 -16
- package/dist/content-addressed-assets.d.ts +69 -0
- package/dist/content-addressed-assets.js +206 -0
- package/dist/dev-session.d.ts +2 -2
- package/dist/dev-session.js +51 -16
- package/dist/document-build.js +1 -2
- package/dist/document-check.d.ts +1 -1
- package/dist/document-check.js +4 -4
- package/dist/document-cmd.d.ts +1 -2
- package/dist/document-cmd.js +2 -3
- package/dist/document-designs.js +31 -3
- package/dist/document-enrich.js +2 -3
- package/dist/document-evidence.d.ts +4 -4
- package/dist/document-evidence.js +20 -12
- package/dist/document-integrate.d.ts +0 -1
- package/dist/document-integrate.js +0 -1
- package/dist/document-interactive.d.ts +11 -11
- package/dist/document-interactive.js +12 -13
- package/dist/document-locale.d.ts +1 -1
- package/dist/document-locale.js +1 -1
- package/dist/document-markdown.d.ts +1 -2
- package/dist/document-markdown.js +13 -7
- package/dist/document-scan.d.ts +4 -4
- package/dist/document-scan.js +4 -4
- package/dist/document-schema.d.ts +28 -29
- package/dist/document-schema.js +28 -29
- package/dist/explain-cmd.js +3 -3
- package/dist/index.d.ts +349 -789
- package/dist/index.js +109 -85
- package/dist/invocation.d.ts +68 -0
- package/dist/invocation.js +169 -0
- package/dist/locale-check.d.ts +19 -3
- package/dist/locale-check.js +130 -6
- package/dist/locale-cmd.js +6 -7
- package/dist/locale-delivery.d.ts +38 -38
- package/dist/locale-delivery.js +39 -40
- package/dist/locale-router.d.ts +39 -40
- package/dist/locale-router.js +39 -40
- package/dist/locale-runtime.d.ts +39 -39
- package/dist/locale-runtime.js +44 -45
- package/dist/locale-schema.d.ts +1 -2
- package/dist/locale-schema.js +1 -2
- package/dist/locale-tooling.d.ts +13 -13
- package/dist/locale-tooling.js +14 -15
- package/dist/log.d.ts +1 -1
- package/dist/log.js +1 -1
- package/dist/packages.d.ts +1 -2
- package/dist/packages.js +1 -2
- package/dist/plugin-host.d.ts +11 -3
- package/dist/plugin-host.js +21 -5
- 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 +158 -0
- package/dist/production-test-pack.js +452 -0
- package/dist/refactor-cmd.d.ts +1 -1
- package/dist/refactor-cmd.js +6 -6
- 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 +337 -0
- package/dist/resolve-native-cli.d.ts +14 -0
- package/dist/resolve-native-cli.js +84 -0
- package/dist/resolve.d.ts +1 -2
- package/dist/resolve.js +1 -2
- package/dist/site-delivery.d.ts +134 -0
- package/dist/site-delivery.js +345 -0
- package/dist/static-emit.d.ts +136 -0
- package/dist/static-emit.js +463 -0
- package/dist/test-cmd.d.ts +2 -2
- package/dist/test-cmd.js +37 -17
- package/dist/watch-diff.d.ts +1 -1
- package/dist/watch-diff.js +1 -1
- package/package.json +32 -17
package/dist/cli.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Node CLI command implementations
|
|
2
|
+
* Node CLI command implementations .
|
|
3
3
|
*/
|
|
4
4
|
/**
|
|
5
5
|
* @param {string[]} argv
|
|
@@ -7,9 +7,17 @@
|
|
|
7
7
|
export declare function parseArgs(argv: any): {
|
|
8
8
|
_: any[];
|
|
9
9
|
};
|
|
10
|
+
export declare function printGlobalHelp(): void;
|
|
11
|
+
export declare function printProjectHelp(): void;
|
|
12
|
+
/** @deprecated use printProjectHelp / printGlobalHelp */
|
|
10
13
|
export declare function printHelp(): void;
|
|
11
14
|
/**
|
|
12
15
|
* @param {string[]} argv
|
|
16
|
+
* @param {{
|
|
17
|
+
* cwd?: string,
|
|
18
|
+
* thisPackageRoot?: string,
|
|
19
|
+
* reexec?: (bin: string, argv: string[]) => Promise<number>,
|
|
20
|
+
* }} [opts]
|
|
13
21
|
* @returns {Promise<number>}
|
|
14
22
|
*/
|
|
15
|
-
export declare function runCli(argv: any): Promise<any>;
|
|
23
|
+
export declare function runCli(argv: any, opts?: {}): Promise<any>;
|
package/dist/cli.js
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
/**
|
|
3
|
-
* Node CLI command implementations
|
|
3
|
+
* Node CLI command implementations .
|
|
4
4
|
*/
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
|
-
import { existsSync } from 'node:fs';
|
|
6
|
+
import { copyFileSync, existsSync } from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
-
import { HOST_PROTOCOL, createWorkspace, getProtocolVersions } from './index.js';
|
|
8
|
+
import { HOST_PROTOCOL, createWorkspace, getProtocolVersions, resolveCoreRuntimeDist } from './index.js';
|
|
9
9
|
import { createDevSession } from './dev-session.js';
|
|
10
|
+
import { gateGlobalProjectCommand, getInvocationContext, isGlobalAllowedCommand } from './invocation.js';
|
|
10
11
|
import { log } from './log.js';
|
|
12
|
+
import { findAvailablePort } from './port.js';
|
|
11
13
|
import { readPackageMeta, resolveWorkspaceDirs } from './resolve.js';
|
|
12
14
|
import { cmdTest } from './test-cmd.js';
|
|
13
15
|
import { cmdDocument } from './document-cmd.js';
|
|
14
16
|
import { buildIntegratedDocuments, projectHasDocuments } from './document-integrate.js';
|
|
15
17
|
import { cmdLocale } from './locale-cmd.js';
|
|
18
|
+
import { emitLocaleRuntimeModules, localeHasErrors } from './locale-check.js';
|
|
16
19
|
import { cmdApplication } from './application-cmd.js';
|
|
20
|
+
import { cmdArtifact } from './release-cmd.js';
|
|
17
21
|
import { cmdRefactor } from './refactor-cmd.js';
|
|
18
22
|
import { cmdExplain } from './explain-cmd.js';
|
|
23
|
+
import { resolveNativeVmzCli } from './resolve-native-cli.js';
|
|
24
|
+
import { emitWebStatic } from './static-emit.js';
|
|
25
|
+
import { emitSiteDelivery } from './site-delivery.js';
|
|
26
|
+
import { loadVmzConfig } from './plugin-host.js';
|
|
19
27
|
/**
|
|
20
28
|
* @param {string[]} argv
|
|
21
29
|
*/
|
|
@@ -61,29 +69,62 @@ export function parseArgs(argv) {
|
|
|
61
69
|
}
|
|
62
70
|
return out;
|
|
63
71
|
}
|
|
64
|
-
export function
|
|
65
|
-
console.log(`vmz —
|
|
72
|
+
export function printGlobalHelp() {
|
|
73
|
+
console.log(`vmz — global mode (scaffold only)
|
|
74
|
+
|
|
75
|
+
Install faces: @vmz/core (runtime) · @vmz/vmz (this CLI) · optional @vmz/ui / @vmz/plugin-*
|
|
76
|
+
|
|
77
|
+
Three install modes:
|
|
78
|
+
developer monorepo source (packages/runtimes/vmz) — full CLI
|
|
79
|
+
project app node_modules/@vmz/vmz — full CLI
|
|
80
|
+
global npm/pnpm -g — only new/init/help/version
|
|
81
|
+
|
|
82
|
+
You are in global mode. Pin \`@vmz/vmz\` in the app so check/build/lsp
|
|
83
|
+
use a traceable project install.
|
|
84
|
+
|
|
85
|
+
Usage:
|
|
86
|
+
vmz new|init <dir> Scaffold (native CLI; Node only gates + forwards)
|
|
87
|
+
vmz version Show host + native protocol versions
|
|
88
|
+
vmz help Show this help
|
|
89
|
+
|
|
90
|
+
Project commands:
|
|
91
|
+
pnpm add @vmz/core && pnpm add -D @vmz/vmz
|
|
92
|
+
pnpm exec vmz check
|
|
93
|
+
# or: vmz new my-app && cd my-app && pnpm install
|
|
94
|
+
|
|
95
|
+
If a project \`node_modules/@vmz/vmz\` exists, a global
|
|
96
|
+
\`vmz <cmd>\` re-execs that bin.
|
|
97
|
+
`);
|
|
98
|
+
}
|
|
99
|
+
export function printProjectHelp() {
|
|
100
|
+
console.log(`vmz — Node toolchain host (project / developer mode)
|
|
66
101
|
|
|
67
102
|
Usage:
|
|
103
|
+
vmz new|init <dir> Scaffold a minimal app (native CLI)
|
|
68
104
|
vmz check [path] Check project via Workspace
|
|
69
105
|
vmz build [path] [options] Build project via Workspace
|
|
70
106
|
vmz serve [path] [options] Serve dist (optional --build)
|
|
71
107
|
vmz dev [path] [options] Long-lived rebuild session (no CLI spawn)
|
|
72
108
|
vmz format [path] [--check] Format .vmz via N-API (oxc codegen)
|
|
73
109
|
vmz lint [path] [--deny-warnings] Lint (= check) via N-API
|
|
74
|
-
vmz test [path] [options] Native test discover / report
|
|
75
|
-
vmz document|docs <cmd> Project /documents domain
|
|
76
|
-
vmz application <cmd> Application Collection / Mount
|
|
77
|
-
vmz
|
|
110
|
+
vmz test [path] [options] Native test discover / report
|
|
111
|
+
vmz document|docs <cmd> Project /documents domain
|
|
112
|
+
vmz application <cmd> Application Collection / Mount
|
|
113
|
+
vmz artifact <cmd> Release pack / publish / rollback / diff (A3)
|
|
114
|
+
vmz refactor <cmd> DX rename plans / apply
|
|
78
115
|
vmz explain [style] <target> DX causal explain (style Theme chain)
|
|
116
|
+
vmz lsp [root] [--out-dir] Language server (stdio; native CLI)
|
|
117
|
+
vmz mcp [root] [--out-dir] MCP server (stdio; native CLI)
|
|
79
118
|
vmz version Show host + native protocol versions
|
|
80
119
|
vmz help Show this help
|
|
81
120
|
|
|
82
121
|
Options:
|
|
83
122
|
--out-dir, -o <dir> Output directory (default: dist)
|
|
84
123
|
--release Release build (build only)
|
|
124
|
+
--profile <name> Delivery profile after build (web-static)
|
|
125
|
+
--origin <url> Site origin for web-static canonical/sitemap
|
|
85
126
|
--host <host> Listen host (default: 127.0.0.1)
|
|
86
|
-
--port <port> Listen port (
|
|
127
|
+
--port <port> Listen port (dev: omit = auto from 5173; set = lock)
|
|
87
128
|
--poll-ms <ms> Dev watch poll interval (default: 300)
|
|
88
129
|
--build Build before serve
|
|
89
130
|
--check Format check-only (format)
|
|
@@ -99,21 +140,51 @@ Options:
|
|
|
99
140
|
--strict Strict document locale/PageKey coverage (document check)
|
|
100
141
|
`);
|
|
101
142
|
}
|
|
143
|
+
/** @deprecated use printProjectHelp / printGlobalHelp */
|
|
144
|
+
export function printHelp() {
|
|
145
|
+
printProjectHelp();
|
|
146
|
+
}
|
|
102
147
|
/**
|
|
103
148
|
* @param {string[]} argv
|
|
149
|
+
* @param {{
|
|
150
|
+
* cwd?: string,
|
|
151
|
+
* thisPackageRoot?: string,
|
|
152
|
+
* reexec?: (bin: string, argv: string[]) => Promise<number>,
|
|
153
|
+
* }} [opts]
|
|
104
154
|
* @returns {Promise<number>}
|
|
105
155
|
*/
|
|
106
|
-
export async function runCli(argv) {
|
|
156
|
+
export async function runCli(argv, opts = {}) {
|
|
107
157
|
const [cmd, ...rest] = argv;
|
|
158
|
+
const inv = getInvocationContext({
|
|
159
|
+
cwd: opts.cwd,
|
|
160
|
+
thisPackageRoot: opts.thisPackageRoot,
|
|
161
|
+
});
|
|
108
162
|
if (!cmd || cmd === 'help' || cmd === '-h' || cmd === '--help') {
|
|
109
|
-
|
|
163
|
+
if (inv.mode === 'global')
|
|
164
|
+
printGlobalHelp();
|
|
165
|
+
else
|
|
166
|
+
printProjectHelp();
|
|
110
167
|
return 0;
|
|
111
168
|
}
|
|
112
169
|
if (cmd === 'version' || cmd === '-V' || cmd === '--version') {
|
|
113
170
|
return cmdVersion();
|
|
114
171
|
}
|
|
172
|
+
if (!isGlobalAllowedCommand(cmd)) {
|
|
173
|
+
const gated = await gateGlobalProjectCommand({
|
|
174
|
+
argv,
|
|
175
|
+
cwd: opts.cwd,
|
|
176
|
+
thisPackageRoot: opts.thisPackageRoot,
|
|
177
|
+
reexec: opts.reexec,
|
|
178
|
+
logError: (msg) => log.error(msg),
|
|
179
|
+
});
|
|
180
|
+
if (gated.action === 'exit')
|
|
181
|
+
return gated.code;
|
|
182
|
+
}
|
|
115
183
|
const args = parseArgs(rest);
|
|
116
184
|
switch (cmd) {
|
|
185
|
+
case 'new':
|
|
186
|
+
case 'init':
|
|
187
|
+
return cmdNativeForward(cmd, rest);
|
|
117
188
|
case 'check':
|
|
118
189
|
return cmdCheck(args);
|
|
119
190
|
case 'build':
|
|
@@ -138,16 +209,57 @@ export async function runCli(argv) {
|
|
|
138
209
|
case 'applications':
|
|
139
210
|
case 'app':
|
|
140
211
|
return cmdApplication(rest);
|
|
212
|
+
case 'artifact':
|
|
213
|
+
case 'artifacts':
|
|
214
|
+
case 'release':
|
|
215
|
+
return cmdArtifact(rest);
|
|
141
216
|
case 'refactor':
|
|
142
217
|
return cmdRefactor(rest);
|
|
143
218
|
case 'explain':
|
|
144
219
|
return cmdExplain(rest);
|
|
220
|
+
case 'lsp':
|
|
221
|
+
return cmdNativeForward('lsp', rest);
|
|
222
|
+
case 'mcp':
|
|
223
|
+
return cmdNativeForward('mcp', rest);
|
|
145
224
|
default:
|
|
146
225
|
log.error(`unknown command \`${cmd}\``);
|
|
147
|
-
|
|
226
|
+
if (inv.mode === 'global')
|
|
227
|
+
printGlobalHelp();
|
|
228
|
+
else
|
|
229
|
+
printProjectHelp();
|
|
148
230
|
return 1;
|
|
149
231
|
}
|
|
150
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Forward to the single native `vmz` binary (vmz-tools).
|
|
235
|
+
* Scaffold / stdio servers live in Rust — Node only gates + re-execs.
|
|
236
|
+
*
|
|
237
|
+
* @param {'new' | 'init' | 'lsp' | 'mcp'} sub
|
|
238
|
+
* @param {string[]} argv
|
|
239
|
+
* @returns {Promise<number>}
|
|
240
|
+
*/
|
|
241
|
+
function cmdNativeForward(sub, argv) {
|
|
242
|
+
const bin = resolveNativeVmzCli();
|
|
243
|
+
if (!bin) {
|
|
244
|
+
log.error('native `vmz` CLI not found (vmz-tools).');
|
|
245
|
+
log.error('Build: cargo build -p vmz-tools');
|
|
246
|
+
log.error('Or set VMZ_NATIVE to the absolute path of that binary.');
|
|
247
|
+
return Promise.resolve(1);
|
|
248
|
+
}
|
|
249
|
+
return new Promise((resolve) => {
|
|
250
|
+
const child = spawn(bin, [sub, ...argv], { stdio: 'inherit' });
|
|
251
|
+
child.on('error', (err) => {
|
|
252
|
+
log.error(`failed to spawn ${bin}: ${err.message}`);
|
|
253
|
+
resolve(1);
|
|
254
|
+
});
|
|
255
|
+
child.on('exit', (code, signal) => {
|
|
256
|
+
if (signal)
|
|
257
|
+
resolve(1);
|
|
258
|
+
else
|
|
259
|
+
resolve(code ?? 1);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
}
|
|
151
263
|
function cmdVersion() {
|
|
152
264
|
const native = getProtocolVersions();
|
|
153
265
|
console.log(`vmz host ${HOST_PROTOCOL}`);
|
|
@@ -217,11 +329,39 @@ async function cmdBuild(args) {
|
|
|
217
329
|
});
|
|
218
330
|
if (code !== 0)
|
|
219
331
|
return code;
|
|
332
|
+
const localeEmit = emitLocaleRuntimeModules(project, outDir);
|
|
333
|
+
if (!localeEmit.ok || localeHasErrors({ diagnostics: localeEmit.diagnostics })) {
|
|
334
|
+
log.diagnostics(localeEmit.diagnostics ?? []);
|
|
335
|
+
log.error('locale runtime emit failed');
|
|
336
|
+
return 1;
|
|
337
|
+
}
|
|
338
|
+
if (localeEmit.written.length) {
|
|
339
|
+
log.info(`locale runtime emit (${localeEmit.written.length} module(s))`);
|
|
340
|
+
}
|
|
220
341
|
if (projectHasDocuments(project)) {
|
|
221
342
|
const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
|
|
222
343
|
if (!docs.ok)
|
|
223
344
|
return 1;
|
|
224
345
|
}
|
|
346
|
+
const cfg = await loadVmzConfig(project);
|
|
347
|
+
if (cfg.delivery) {
|
|
348
|
+
log.info(`site-delivery emit ${outDir}`);
|
|
349
|
+
const site = emitSiteDelivery(outDir, cfg.delivery, {
|
|
350
|
+
siteId: cfg.application?.id || undefined,
|
|
351
|
+
});
|
|
352
|
+
log.info(`site-delivery ok (digest=${String(site.contract.contractDigest).slice(0, 12)}…)`);
|
|
353
|
+
}
|
|
354
|
+
const profile = typeof args.profile === 'string' ? args.profile : '';
|
|
355
|
+
if (profile === 'web-static') {
|
|
356
|
+
const origin = typeof args.origin === 'string' ? args.origin : undefined;
|
|
357
|
+
log.info(`web-static emit ${outDir}`);
|
|
358
|
+
const result = await emitWebStatic(outDir, { origin });
|
|
359
|
+
log.info(`web-static ok (${result.htmlFiles.length} html, ${result.skipped.length} skipped, digest=${result.digest.slice(0, 12)}…)`);
|
|
360
|
+
}
|
|
361
|
+
else if (profile) {
|
|
362
|
+
log.error(`unknown build --profile ${profile} (supported: web-static)`);
|
|
363
|
+
return 1;
|
|
364
|
+
}
|
|
225
365
|
return 0;
|
|
226
366
|
}
|
|
227
367
|
finally {
|
|
@@ -244,8 +384,16 @@ async function cmdServe(args) {
|
|
|
244
384
|
});
|
|
245
385
|
const hostJs = path.join(outDir, 'vmz-serve-host.mjs');
|
|
246
386
|
if (!existsSync(hostJs)) {
|
|
247
|
-
|
|
248
|
-
|
|
387
|
+
const coreDist = resolveCoreRuntimeDist();
|
|
388
|
+
const src = coreDist ? path.join(coreDist, 'serve-host.mjs') : null;
|
|
389
|
+
if (src && existsSync(src)) {
|
|
390
|
+
copyFileSync(src, hostJs);
|
|
391
|
+
log.info(`materialized ${hostJs} from @vmz/core (release builds omit it)`);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
log.error(`missing ${hostJs} — run \`vmz build\` (without --release) or ensure @vmz/core is installed`);
|
|
395
|
+
return 1;
|
|
396
|
+
}
|
|
249
397
|
}
|
|
250
398
|
const host = typeof args.host === 'string' ? args.host : '127.0.0.1';
|
|
251
399
|
const port = Number(args.port ?? 5173);
|
|
@@ -287,7 +435,22 @@ async function cmdDev(args) {
|
|
|
287
435
|
outDir: typeof args['out-dir'] === 'string' ? args['out-dir'] : undefined,
|
|
288
436
|
});
|
|
289
437
|
const host = typeof args.host === 'string' ? args.host : '127.0.0.1';
|
|
290
|
-
const
|
|
438
|
+
const portLocked = Object.prototype.hasOwnProperty.call(args, 'port');
|
|
439
|
+
let port;
|
|
440
|
+
if (portLocked) {
|
|
441
|
+
port = Number(args.port);
|
|
442
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
443
|
+
log.error(`invalid --port ${String(args.port)}`);
|
|
444
|
+
return 1;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
const preferred = 5173;
|
|
449
|
+
port = await findAvailablePort(host, preferred);
|
|
450
|
+
if (port !== preferred) {
|
|
451
|
+
log.info(`port ${preferred} busy → using ${port}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
291
454
|
const pollMs = Number(args['poll-ms'] ?? 300);
|
|
292
455
|
const ac = new AbortController();
|
|
293
456
|
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
|
+
}
|
package/dist/dev-session.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Long-lived Node dev session (
|
|
2
|
+
* Long-lived Node dev session (/session).
|
|
3
3
|
*
|
|
4
4
|
* Rebuilds go through the N-API `Workspace` — never spawn `cargo` / `vmz-tools`.
|
|
5
|
-
*
|
|
5
|
+
* session: only dirty leaves are marked; Workspace emits affected deployment units.
|
|
6
6
|
*/
|
|
7
7
|
/**
|
|
8
8
|
* @typedef {object} DevSessionOptions
|