@vmz/vmz 0.1.21 → 0.1.23
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/dist/cli.js +2 -0
- package/dist/content-addressed-assets.d.ts +10 -7
- package/dist/content-addressed-assets.js +30 -62
- package/dist/github-actions-cmd.d.ts +6 -0
- package/dist/github-actions-cmd.js +120 -0
- package/dist/index.js +1 -0
- package/dist/locale-router.d.ts +3 -11
- package/dist/locale-router.js +2 -52
- package/dist/pack-client-packages.d.ts +4 -2
- package/dist/pack-client-packages.js +23 -33
- package/dist/route-path.d.ts +2 -2
- package/dist/route-path.js +7 -5
- package/dist/server-artifact.d.ts +5 -53
- package/dist/server-artifact.js +28 -167
- package/dist/static-emit.js +32 -55
- package/locales/en-US/cli.json +14 -0
- package/locales/en-US/diag.json +0 -1
- package/locales/zh-CN/diag.json +0 -1
- package/package.json +14 -14
package/dist/cli.js
CHANGED
|
@@ -21,6 +21,7 @@ import { registerArtifactCommands } from './release-cmd.js';
|
|
|
21
21
|
import { registerRefactorCommands } from './refactor-cmd.js';
|
|
22
22
|
import { registerExplainCommand } from './explain-cmd.js';
|
|
23
23
|
import { registerPlanCommands } from './plan-cmd.js';
|
|
24
|
+
import { registerGithubActionsCommand } from './github-actions-cmd.js';
|
|
24
25
|
import { loadVmzConfig } from './plugin-host.js';
|
|
25
26
|
import { normalizeDeliveryAuthoring, resolveProfileArtifactDir, selectBuildProfile } from './delivery-profile.js';
|
|
26
27
|
import { packFromDeploymentIr } from './pack.js';
|
|
@@ -76,6 +77,7 @@ function buildProductCli(opts = {}) {
|
|
|
76
77
|
registerArtifactCommands(cli.command('artifact|artifacts|release', 'cli.cmd.artifact'));
|
|
77
78
|
registerRefactorCommands(cli.command('refactor', 'cli.cmd.refactor'));
|
|
78
79
|
registerExplainCommand(cli);
|
|
80
|
+
registerGithubActionsCommand(cli);
|
|
79
81
|
cli.command('version', 'cli.cmd.version').action(() => cmdVersion());
|
|
80
82
|
return cli;
|
|
81
83
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A3: content-addressed assets/<hash> layout for immutable CDN objects.
|
|
3
|
-
* Logical paths stay available for serve/dev
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* Logical paths stay available for serve/dev. Static HTML must receive hashed
|
|
4
|
+
* `cssEntry` / `moduleScriptSrc` at shell emit time — this module does **not**
|
|
5
|
+
* post-rewrite HTML. CSS aggregators rewrite `@import` before hash. JS under
|
|
6
|
+
* `assets/` rewrites ESM `./` → `../` via oxc (plus dynamic `import("./"+)` form)
|
|
7
|
+
* so barrels resolve at dist root.
|
|
7
8
|
*/
|
|
8
9
|
export declare const CONTENT_ADDRESSED_ASSETS_SCHEMA = "vmz.content_addressed_assets.v0";
|
|
9
10
|
interface ContentAddressedAssetObject {
|
|
@@ -24,7 +25,6 @@ export interface ContentAddressedAssetsManifest {
|
|
|
24
25
|
}
|
|
25
26
|
interface EmitContentAddressedAssetsOpts {
|
|
26
27
|
candidates?: string[];
|
|
27
|
-
rewriteHtml?: boolean;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
30
|
* Rewrite relative `@import "./foo.css"` to hashed paths under assets/.
|
|
@@ -46,9 +46,10 @@ export declare function rewriteCssImports(cssText: any, rewrites: any): any;
|
|
|
46
46
|
*/
|
|
47
47
|
export declare function rewriteJsEntryRelativeImports(jsText: any, _rewrites?: {}): string;
|
|
48
48
|
/**
|
|
49
|
-
* Emit `assets/<sha256>.<ext>` copies and
|
|
49
|
+
* Emit `assets/<sha256>.<ext>` copies and return logical→hashed rewrites.
|
|
50
|
+
* Callers must pass hashed URLs into `generatePageShell` — HTML is never rewritten here.
|
|
50
51
|
* @param {string} distDir
|
|
51
|
-
* @param {{ candidates?: string[]
|
|
52
|
+
* @param {{ candidates?: string[] }} [opts]
|
|
52
53
|
*/
|
|
53
54
|
export declare function emitContentAddressedAssets(distDir: string, opts?: EmitContentAddressedAssetsOpts): {
|
|
54
55
|
manifest: ContentAddressedAssetsManifest;
|
|
@@ -99,6 +100,8 @@ export declare function assertSharedAssetPath(distDir: any, a: any, b: any, ext?
|
|
|
99
100
|
digestB?: undefined;
|
|
100
101
|
rel?: undefined;
|
|
101
102
|
};
|
|
103
|
+
/** Resolve `/logical` → hashed href from emit rewrites (`/assets/<sha>.ext`). */
|
|
104
|
+
export declare function hashedAssetHref(rewrites: Record<string, string>, logical: string | null | undefined): string | null;
|
|
102
105
|
export declare function contentAddressedAssetsDigest(manifest: any): any;
|
|
103
106
|
export declare function sha256Buffer(buf: any): string;
|
|
104
107
|
export {};
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A3: content-addressed assets/<hash> layout for immutable CDN objects.
|
|
3
|
-
* Logical paths stay available for serve/dev
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* Logical paths stay available for serve/dev. Static HTML must receive hashed
|
|
4
|
+
* `cssEntry` / `moduleScriptSrc` at shell emit time — this module does **not**
|
|
5
|
+
* post-rewrite HTML. CSS aggregators rewrite `@import` before hash. JS under
|
|
6
|
+
* `assets/` rewrites ESM `./` → `../` via oxc (plus dynamic `import("./"+)` form)
|
|
7
|
+
* so barrels resolve at dist root.
|
|
7
8
|
*/
|
|
8
9
|
import crypto from 'node:crypto';
|
|
9
10
|
import fs from 'node:fs';
|
|
10
11
|
import path from 'node:path';
|
|
12
|
+
import { requireNativeAddon } from './native-addon.js';
|
|
11
13
|
import { canonicalJson, sha256Hex } from './release-pack.js';
|
|
12
14
|
import { writePrettyJsonFile } from './pretty-json.js';
|
|
13
15
|
export const CONTENT_ADDRESSED_ASSETS_SCHEMA = 'vmz.content_addressed_assets.v0';
|
|
@@ -59,26 +61,25 @@ export function rewriteCssImports(cssText, rewrites) {
|
|
|
59
61
|
* @param {Record<string, string>} [_rewrites]
|
|
60
62
|
*/
|
|
61
63
|
export function rewriteJsEntryRelativeImports(jsText, _rewrites = {}) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
const native = requireNativeAddon();
|
|
65
|
+
if (typeof native.rewriteModuleSpecifiers !== 'function') {
|
|
66
|
+
throw new Error('native missing rewriteModuleSpecifiers — run `pnpm napi:build`');
|
|
67
|
+
}
|
|
68
|
+
// Static import/export specs via oxc AST (`./x` → `../x`). Dynamic
|
|
69
|
+
// `import("./"+id)` is not a literal specifier — rewrite that codegen form only.
|
|
70
|
+
let out = String(native.rewriteModuleSpecifiers(String(jsText ?? ''), JSON.stringify({
|
|
71
|
+
tsExtToJs: false,
|
|
72
|
+
dotSlashToParent: true,
|
|
73
|
+
})));
|
|
64
74
|
out = out.replace(/import\(\s*"\.\/"\s*\+/g, 'import("../"+');
|
|
65
75
|
out = out.replace(/import\(\s*'\.\/'\s*\+/g, "import('../'+");
|
|
66
|
-
const rewriteSpec = (spec) => {
|
|
67
|
-
const logical = String(spec || '').replace(/^\.\//, '');
|
|
68
|
-
if (!logical || logical.startsWith('../') || logical.startsWith('/'))
|
|
69
|
-
return spec;
|
|
70
|
-
return `../${logical}`;
|
|
71
|
-
};
|
|
72
|
-
// import/export … from "./x" | import("./x")
|
|
73
|
-
out = out.replace(/\b((?:import|export)\s+[^'"\n]*?\s+from\s+|import\s*\(\s*)(['"])(\.\/[^'"]+)\2/g, (match, prefix, quote, spec) => `${prefix}${quote}${rewriteSpec(spec)}${quote}`);
|
|
74
|
-
// side-effect: import "./x.js"
|
|
75
|
-
out = out.replace(/(^|[;\s])(import\s*)(['"])(\.\/[^'"]+)\3/gm, (match, lead, kw, quote, spec) => `${lead}${kw}${quote}${rewriteSpec(spec)}${quote}`);
|
|
76
76
|
return out;
|
|
77
77
|
}
|
|
78
78
|
/**
|
|
79
|
-
* Emit `assets/<sha256>.<ext>` copies and
|
|
79
|
+
* Emit `assets/<sha256>.<ext>` copies and return logical→hashed rewrites.
|
|
80
|
+
* Callers must pass hashed URLs into `generatePageShell` — HTML is never rewritten here.
|
|
80
81
|
* @param {string} distDir
|
|
81
|
-
* @param {{ candidates?: string[]
|
|
82
|
+
* @param {{ candidates?: string[] }} [opts]
|
|
82
83
|
*/
|
|
83
84
|
export function emitContentAddressedAssets(distDir, opts = {}) {
|
|
84
85
|
const abs = path.resolve(distDir);
|
|
@@ -141,17 +142,13 @@ export function emitContentAddressedAssets(distDir, opts = {}) {
|
|
|
141
142
|
});
|
|
142
143
|
}
|
|
143
144
|
objects.sort((a, b) => (a.logicalPath < b.logicalPath ? -1 : a.logicalPath > b.logicalPath ? 1 : 0));
|
|
144
|
-
let rewrittenHtml = 0;
|
|
145
|
-
if (opts.rewriteHtml !== false) {
|
|
146
|
-
rewrittenHtml = rewriteHtmlReferences(abs, rewrites);
|
|
147
|
-
}
|
|
148
145
|
const manifest = {
|
|
149
146
|
schema: CONTENT_ADDRESSED_ASSETS_SCHEMA,
|
|
150
147
|
layout: 'assets/<sha256>.<ext>',
|
|
151
148
|
immutable: true,
|
|
152
149
|
objectCount: objects.length,
|
|
153
150
|
objects,
|
|
154
|
-
rewrittenHtml,
|
|
151
|
+
rewrittenHtml: 0,
|
|
155
152
|
};
|
|
156
153
|
manifest.manifestDigest = sha256Hex(canonicalJson({ ...manifest, manifestDigest: undefined }));
|
|
157
154
|
const vmzDir = path.join(abs, '_vmz');
|
|
@@ -299,45 +296,16 @@ function walk(root, dir, onFile) {
|
|
|
299
296
|
onFile(path.relative(root, full).replace(/\\/g, '/'));
|
|
300
297
|
}
|
|
301
298
|
}
|
|
302
|
-
/**
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
let count = 0;
|
|
313
|
-
walkHtml(distDir, (file) => {
|
|
314
|
-
let text = fs.readFileSync(file, 'utf8');
|
|
315
|
-
let next = text;
|
|
316
|
-
for (const [from, to] of pairs) {
|
|
317
|
-
next = next.split(from).join(to);
|
|
318
|
-
}
|
|
319
|
-
if (next !== text) {
|
|
320
|
-
fs.writeFileSync(file, next, 'utf8');
|
|
321
|
-
count += 1;
|
|
322
|
-
}
|
|
323
|
-
});
|
|
324
|
-
return count;
|
|
325
|
-
}
|
|
326
|
-
function walkHtml(dir, onFile) {
|
|
327
|
-
const stack = [dir];
|
|
328
|
-
while (stack.length) {
|
|
329
|
-
const cur = stack.pop();
|
|
330
|
-
for (const name of fs.readdirSync(cur)) {
|
|
331
|
-
if (name === 'assets' || name === '_vmz' || name === 'node_modules')
|
|
332
|
-
continue;
|
|
333
|
-
const full = path.join(cur, name);
|
|
334
|
-
const st = fs.statSync(full);
|
|
335
|
-
if (st.isDirectory())
|
|
336
|
-
stack.push(full);
|
|
337
|
-
else if (name.endsWith('.html'))
|
|
338
|
-
onFile(full);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
299
|
+
/** Resolve `/logical` → hashed href from emit rewrites (`/assets/<sha>.ext`). */
|
|
300
|
+
export function hashedAssetHref(rewrites, logical) {
|
|
301
|
+
if (!logical)
|
|
302
|
+
return null;
|
|
303
|
+
const raw = String(logical).replace(/\\/g, '/');
|
|
304
|
+
const key = raw.startsWith('/') ? raw : `/${raw.replace(/^\.\//, '')}`;
|
|
305
|
+
const hit = rewrites[key] || rewrites[key.slice(1)];
|
|
306
|
+
if (!hit)
|
|
307
|
+
return key.startsWith('/') ? key : `/${key}`;
|
|
308
|
+
return hit.startsWith('/') ? hit : `/${hit}`;
|
|
341
309
|
}
|
|
342
310
|
export function contentAddressedAssetsDigest(manifest) {
|
|
343
311
|
return manifest.manifestDigest || sha256Hex(canonicalJson({ ...manifest, manifestDigest: undefined }));
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vmz github-actions` — monitor workflow runs via N-API → Rust `vmz-github` (octocrab).
|
|
3
|
+
*/
|
|
4
|
+
import type { Cli, ParsedOptions } from '@vmz/commander';
|
|
5
|
+
export declare function registerGithubActionsCommand(cli: Cli): void;
|
|
6
|
+
export declare function cmdGithubActions(args: ParsedOptions): number;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vmz github-actions` — monitor workflow runs via N-API → Rust `vmz-github` (octocrab).
|
|
3
|
+
*/
|
|
4
|
+
import { loadNative } from './index.js';
|
|
5
|
+
import { log } from './log.js';
|
|
6
|
+
import { emitPrettyJson } from './pretty-json.js';
|
|
7
|
+
export function registerGithubActionsCommand(cli) {
|
|
8
|
+
cli.command('github-actions|gha', 'cli.cmd.github-actions')
|
|
9
|
+
.option('--mode <mode>', 'cli.opt.gha.mode')
|
|
10
|
+
.option('--workflow <id>', 'cli.opt.gha.workflow')
|
|
11
|
+
.option('--run-id <id>', 'cli.opt.gha.run-id')
|
|
12
|
+
.option('--ref <sha>', 'cli.opt.gha.ref')
|
|
13
|
+
.option('--branch <name>', 'cli.opt.gha.branch')
|
|
14
|
+
.option('--owner <owner>', 'cli.opt.gha.owner')
|
|
15
|
+
.option('--repo <repo>', 'cli.opt.gha.repo')
|
|
16
|
+
.option('--token <token>', 'cli.opt.gha.token')
|
|
17
|
+
.option('--timeout-minutes <n>', 'cli.opt.gha.timeout-minutes')
|
|
18
|
+
.option('--interval-seconds <n>', 'cli.opt.gha.interval-seconds')
|
|
19
|
+
.option('--required-conclusion <c>', 'cli.opt.gha.required-conclusion')
|
|
20
|
+
.option('--json [file]', 'cli.opt.json')
|
|
21
|
+
.action((options) => cmdGithubActions(options));
|
|
22
|
+
}
|
|
23
|
+
export function cmdGithubActions(args) {
|
|
24
|
+
const native = loadNative();
|
|
25
|
+
if (typeof native.githubActionsMonitorJson !== 'function') {
|
|
26
|
+
log.error('githubActionsMonitorJson missing — rebuild native (`pnpm napi:build`)');
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
const modeRaw = String(args.mode ?? 'status')
|
|
30
|
+
.trim()
|
|
31
|
+
.toLowerCase();
|
|
32
|
+
if (modeRaw !== 'status' && modeRaw !== 'wait') {
|
|
33
|
+
log.errorId('cli.err.gha.mode', { mode: modeRaw });
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
const { owner, repo } = resolveOwnerRepo(args);
|
|
37
|
+
if (!owner || !repo) {
|
|
38
|
+
log.errorId('cli.err.gha.repo');
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
const token = (typeof args.token === 'string' && args.token.trim()) || process.env.GITHUB_TOKEN || process.env.GH_TOKEN || undefined;
|
|
42
|
+
const timeoutMinutes = positiveNumber(args['timeout-minutes'], 45);
|
|
43
|
+
const intervalSeconds = positiveNumber(args['interval-seconds'], 15);
|
|
44
|
+
const request = {
|
|
45
|
+
owner,
|
|
46
|
+
repo,
|
|
47
|
+
token: token || null,
|
|
48
|
+
mode: modeRaw,
|
|
49
|
+
workflow: strOrNull(args.workflow),
|
|
50
|
+
runId: parseRunId(args['run-id']),
|
|
51
|
+
headSha: strOrNull(args.ref),
|
|
52
|
+
branch: strOrNull(args.branch),
|
|
53
|
+
timeoutSecs: Math.round(timeoutMinutes * 60),
|
|
54
|
+
intervalSecs: Math.round(intervalSeconds),
|
|
55
|
+
requiredConclusion: String(args['required-conclusion'] ?? 'success'),
|
|
56
|
+
};
|
|
57
|
+
let resultJson;
|
|
58
|
+
try {
|
|
59
|
+
resultJson = native.githubActionsMonitorJson(JSON.stringify(request));
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
63
|
+
log.error(msg);
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
let result;
|
|
67
|
+
try {
|
|
68
|
+
result = JSON.parse(resultJson);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
log.error('githubActionsMonitorJson returned invalid JSON');
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
log.info(`github-actions: run_id=${result.runId} status=${result.status} conclusion=${result.conclusion ?? 'null'} html_url=${result.htmlUrl}`);
|
|
75
|
+
if (args.json !== undefined && args.json !== false) {
|
|
76
|
+
emitPrettyJson(typeof args.json === 'string' ? args.json : true, result);
|
|
77
|
+
}
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
function resolveOwnerRepo(args) {
|
|
81
|
+
let owner = typeof args.owner === 'string' ? args.owner.trim() : '';
|
|
82
|
+
let repo = typeof args.repo === 'string' ? args.repo.trim() : '';
|
|
83
|
+
if (!owner || !repo) {
|
|
84
|
+
const fromEnv = (process.env.GITHUB_REPOSITORY || '').trim();
|
|
85
|
+
const slash = fromEnv.indexOf('/');
|
|
86
|
+
if (slash > 0) {
|
|
87
|
+
if (!owner)
|
|
88
|
+
owner = fromEnv.slice(0, slash);
|
|
89
|
+
if (!repo)
|
|
90
|
+
repo = fromEnv.slice(slash + 1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!owner || !repo) {
|
|
94
|
+
const fallback = (process.env.VMZ_GITHUB_REPO || 'doki-land/vmz-framework').trim();
|
|
95
|
+
const slash = fallback.indexOf('/');
|
|
96
|
+
if (slash > 0) {
|
|
97
|
+
if (!owner)
|
|
98
|
+
owner = fallback.slice(0, slash);
|
|
99
|
+
if (!repo)
|
|
100
|
+
repo = fallback.slice(slash + 1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { owner, repo };
|
|
104
|
+
}
|
|
105
|
+
function strOrNull(v) {
|
|
106
|
+
if (typeof v !== 'string')
|
|
107
|
+
return null;
|
|
108
|
+
const t = v.trim();
|
|
109
|
+
return t ? t : null;
|
|
110
|
+
}
|
|
111
|
+
function parseRunId(v) {
|
|
112
|
+
if (v === undefined || v === null || v === true || v === false)
|
|
113
|
+
return null;
|
|
114
|
+
const n = Number(v);
|
|
115
|
+
return Number.isFinite(n) && n > 0 ? Math.trunc(n) : null;
|
|
116
|
+
}
|
|
117
|
+
function positiveNumber(v, fallback) {
|
|
118
|
+
const n = Number(v);
|
|
119
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
120
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -127,6 +127,7 @@ export const SERVE_HOST_RUNTIME_FILES = [
|
|
|
127
127
|
['deployment-registry.js', 'deployment-registry.js'],
|
|
128
128
|
['render-host.js', 'render-host.js'],
|
|
129
129
|
['route-layout-chain.js', 'route-layout-chain.js'],
|
|
130
|
+
['localize-body-links.js', 'localize-body-links.js'],
|
|
130
131
|
];
|
|
131
132
|
/** Copy serve-host + registry bootstrap modules from `@vmz/core` into app outDir. */
|
|
132
133
|
export function materializeServeHostRuntime(outDir, coreDist = resolveCoreRuntimeDist()) {
|
package/dist/locale-router.d.ts
CHANGED
|
@@ -130,17 +130,9 @@ export declare function parseLocaleFromPath(pathname: string, supportedLocales:
|
|
|
130
130
|
localeId: string;
|
|
131
131
|
restPath: string;
|
|
132
132
|
};
|
|
133
|
-
export
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
} | string>;
|
|
137
|
-
defaultLocale?: string;
|
|
138
|
-
routing?: LocaleRoutingOpts;
|
|
139
|
-
}
|
|
140
|
-
/** Rewrite a same-app href to the given LocaleId via route realization. */
|
|
141
|
-
export declare function localizeSameAppHref(href: string, localeId: string, artifact: LocaleHrefArtifact): string;
|
|
142
|
-
/** Rewrite `<a data-vmz-route href>` in HTML body to retain `localeId`. */
|
|
143
|
-
export declare function localizeBodyLinks(html: string, localeId: string, artifact: LocaleHrefArtifact, escapeAttr?: (s: string) => string): string;
|
|
133
|
+
export type { LocaleHrefArtifact } from '@vmz/core/localize-body-links';
|
|
134
|
+
/** Rewrite a same-app href — single implementation in `@vmz/core/localize-body-links`. */
|
|
135
|
+
export { localizeBodyLinks, localizeSameAppHref } from '@vmz/core/localize-body-links';
|
|
144
136
|
/** Plan redirect / negotiation for an incoming URL (omit-prefix aware). */
|
|
145
137
|
export declare function planLocalePathNavigation(input: {
|
|
146
138
|
pathname: string;
|
package/dist/locale-router.js
CHANGED
|
@@ -228,58 +228,8 @@ export function parseLocaleFromPath(pathname, supportedLocales) {
|
|
|
228
228
|
}
|
|
229
229
|
return { localeId: null, restPath: normalizePath(pathname) };
|
|
230
230
|
}
|
|
231
|
-
/** Rewrite a same-app href
|
|
232
|
-
export
|
|
233
|
-
if (!href || !localeId || !artifact)
|
|
234
|
-
return href;
|
|
235
|
-
if (href.startsWith('#') || /^(mailto|tel|javascript):/i.test(href))
|
|
236
|
-
return href;
|
|
237
|
-
if (/^[a-z][a-z0-9+.-]*:/i.test(href) && !href.startsWith('/'))
|
|
238
|
-
return href;
|
|
239
|
-
let pathname = String(href);
|
|
240
|
-
let search = '';
|
|
241
|
-
let hash = '';
|
|
242
|
-
const hashIdx = pathname.indexOf('#');
|
|
243
|
-
if (hashIdx >= 0) {
|
|
244
|
-
hash = pathname.slice(hashIdx);
|
|
245
|
-
pathname = pathname.slice(0, hashIdx);
|
|
246
|
-
}
|
|
247
|
-
const qIdx = pathname.indexOf('?');
|
|
248
|
-
if (qIdx >= 0) {
|
|
249
|
-
search = pathname.slice(qIdx);
|
|
250
|
-
pathname = pathname.slice(0, qIdx);
|
|
251
|
-
}
|
|
252
|
-
if (!pathname)
|
|
253
|
-
pathname = '/';
|
|
254
|
-
const supported = (artifact.locales || []).map((l) => (typeof l === 'string' ? l : l.id)).filter(Boolean);
|
|
255
|
-
const defaultLocale = artifact.defaultLocale || artifact.routing?.defaultLocale;
|
|
256
|
-
const routing = {
|
|
257
|
-
strategy: artifact.routing?.strategy || 'prefix',
|
|
258
|
-
defaultPrefix: artifact.routing?.defaultPrefix || 'include',
|
|
259
|
-
defaultLocale,
|
|
260
|
-
};
|
|
261
|
-
const parsed = parseLocaleFromPath(pathname, supported);
|
|
262
|
-
const rest = parsed.restPath || '/';
|
|
263
|
-
const realized = realizeRoutePath(localeId, rest, routing);
|
|
264
|
-
return `${realized.path}${search}${hash}`;
|
|
265
|
-
}
|
|
266
|
-
/** Rewrite `<a data-vmz-route href>` in HTML body to retain `localeId`. */
|
|
267
|
-
export function localizeBodyLinks(html, localeId, artifact, escapeAttr = (s) => String(s).replace(/&/g, '&').replace(/"/g, '"')) {
|
|
268
|
-
if (!html || !localeId || !artifact)
|
|
269
|
-
return html;
|
|
270
|
-
return String(html).replace(/<a\b([^>]*)>/gi, (full, attrs) => {
|
|
271
|
-
if (!/\bdata-vmz-route\s*=/.test(attrs))
|
|
272
|
-
return full;
|
|
273
|
-
const hm = attrs.match(/\bhref\s*=\s*"([^"]*)"/i);
|
|
274
|
-
if (!hm)
|
|
275
|
-
return full;
|
|
276
|
-
const next = localizeSameAppHref(hm[1], localeId, artifact);
|
|
277
|
-
if (next === hm[1])
|
|
278
|
-
return full;
|
|
279
|
-
const newAttrs = attrs.replace(/\bhref\s*=\s*"[^"]*"/i, `href="${escapeAttr(next)}"`);
|
|
280
|
-
return `<a${newAttrs}>`;
|
|
281
|
-
});
|
|
282
|
-
}
|
|
231
|
+
/** Rewrite a same-app href — single implementation in `@vmz/core/localize-body-links`. */
|
|
232
|
+
export { localizeBodyLinks, localizeSameAppHref } from '@vmz/core/localize-body-links';
|
|
283
233
|
/** Plan redirect / negotiation for an incoming URL (omit-prefix aware). */
|
|
284
234
|
export function planLocalePathNavigation(input) {
|
|
285
235
|
const routing = {
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Bare npm/workspace imports are legal on the author surface (01/04).
|
|
5
5
|
* Browser ESM cannot resolve them. Pack materializes reachable package
|
|
6
|
-
* modules under `dist/vendor/<pkg>/…` and rewrites importers to relative paths
|
|
6
|
+
* modules under `dist/vendor/<pkg>/…` and rewrites importers to relative paths
|
|
7
|
+
* via oxc AST (N-API `rewriteModuleSpecifiers`) — no import-text regex.
|
|
7
8
|
*
|
|
8
9
|
* Not full oxc chunk-split/minify (`oxc-pending` remains for release minify).
|
|
9
10
|
*/
|
|
@@ -25,5 +26,6 @@ export declare function packClientBareImports(outDir: string, opts?: PackClientB
|
|
|
25
26
|
};
|
|
26
27
|
/** @param {string} js */
|
|
27
28
|
export declare function collectBareSpecs(js: any): unknown[];
|
|
28
|
-
|
|
29
|
+
/** oxc AST module specifier rewrite (`.ts`→`.js` + optional exact map). */
|
|
30
|
+
export declare function rewriteRelativeTsSpecs(js: any, exactMap?: any): any;
|
|
29
31
|
export {};
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Bare npm/workspace imports are legal on the author surface (01/04).
|
|
5
5
|
* Browser ESM cannot resolve them. Pack materializes reachable package
|
|
6
|
-
* modules under `dist/vendor/<pkg>/…` and rewrites importers to relative paths
|
|
6
|
+
* modules under `dist/vendor/<pkg>/…` and rewrites importers to relative paths
|
|
7
|
+
* via oxc AST (N-API `rewriteModuleSpecifiers`) — no import-text regex.
|
|
7
8
|
*
|
|
8
9
|
* Not full oxc chunk-split/minify (`oxc-pending` remains for release minify).
|
|
9
10
|
*/
|
|
@@ -11,6 +12,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSy
|
|
|
11
12
|
import path from 'node:path';
|
|
12
13
|
import { createRequire } from 'node:module';
|
|
13
14
|
import { resolvePackageRoot } from './packages.js';
|
|
15
|
+
import { requireNativeAddon } from './native-addon.js';
|
|
14
16
|
const VENDOR_DIR = 'vendor';
|
|
15
17
|
const SKIP_PREFIXES = ['node:', 'nodejs:', 'cloudflare:', 'data:', 'http:', 'https:', 'vmz:', '#'];
|
|
16
18
|
export function packClientBareImports(outDir, opts = {}) {
|
|
@@ -327,16 +329,22 @@ function transpileTs(source, filename) {
|
|
|
327
329
|
}
|
|
328
330
|
}
|
|
329
331
|
function rewriteTsExt(p) {
|
|
332
|
+
// Path basename only (output layout) — not ESM source rewrite.
|
|
330
333
|
return String(p)
|
|
331
334
|
.replace(/\.tsx$/i, '.js')
|
|
332
335
|
.replace(/\.ts$/i, '.js');
|
|
333
336
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
337
|
+
/** oxc AST module specifier rewrite (`.ts`→`.js` + optional exact map). */
|
|
338
|
+
export function rewriteRelativeTsSpecs(js, exactMap = null) {
|
|
339
|
+
const native = requireNativeAddon();
|
|
340
|
+
if (typeof native.rewriteModuleSpecifiers !== 'function') {
|
|
341
|
+
throw new Error('native missing rewriteModuleSpecifiers — run `pnpm napi:build`');
|
|
342
|
+
}
|
|
343
|
+
const rules = {
|
|
344
|
+
tsExtToJs: true,
|
|
345
|
+
exact: exactMap && typeof exactMap === 'object' ? exactMap : {},
|
|
346
|
+
};
|
|
347
|
+
return native.rewriteModuleSpecifiers(String(js ?? ''), JSON.stringify(rules));
|
|
340
348
|
}
|
|
341
349
|
function resolveRelativeSource(fromDir, rel) {
|
|
342
350
|
const cleaned = rel.replace(/\?.*$/, '').replace(/#.*$/, '');
|
|
@@ -361,33 +369,15 @@ function resolveRelativeSource(fromDir, rel) {
|
|
|
361
369
|
return null;
|
|
362
370
|
}
|
|
363
371
|
function rewriteBareImports(js, fromFile, bareToVendor) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
372
|
+
/** @type {Record<string, string>} */
|
|
373
|
+
const exact = {};
|
|
374
|
+
for (const [spec, dest] of bareToVendor.entries()) {
|
|
375
|
+
if (isRelativeOrAbsolute(spec) || shouldSkipBare(spec))
|
|
376
|
+
continue;
|
|
368
377
|
let rel = path.relative(path.dirname(fromFile), dest).replace(/\\/g, '/');
|
|
369
378
|
if (!rel.startsWith('.'))
|
|
370
379
|
rel = `./${rel}`;
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
out = out.replace(/\bimport\s*\(\s*(['"])([^'"]+)\1\s*\)/g, (full, quote, spec) => {
|
|
375
|
-
if (isRelativeOrAbsolute(spec) || shouldSkipBare(spec))
|
|
376
|
-
return full;
|
|
377
|
-
const rel = toRel(spec);
|
|
378
|
-
return rel ? `import(${quote}${rel}${quote})` : full;
|
|
379
|
-
});
|
|
380
|
-
out = out.replace(/\bfrom\s+(['"])([^'"]+)\1/g, (full, quote, spec) => {
|
|
381
|
-
if (isRelativeOrAbsolute(spec) || shouldSkipBare(spec))
|
|
382
|
-
return full;
|
|
383
|
-
const rel = toRel(spec);
|
|
384
|
-
return rel ? `from ${quote}${rel}${quote}` : full;
|
|
385
|
-
});
|
|
386
|
-
out = out.replace(/\bimport\s+(['"])([^'"]+)\1/g, (full, quote, spec) => {
|
|
387
|
-
if (isRelativeOrAbsolute(spec) || shouldSkipBare(spec))
|
|
388
|
-
return full;
|
|
389
|
-
const rel = toRel(spec);
|
|
390
|
-
return rel ? `import ${quote}${rel}${quote}` : full;
|
|
391
|
-
});
|
|
392
|
-
return out;
|
|
380
|
+
exact[spec] = rel;
|
|
381
|
+
}
|
|
382
|
+
return rewriteRelativeTsSpecs(js, exact);
|
|
393
383
|
}
|
package/dist/route-path.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export declare function isRouteBoundaryStem(stem: string): boolean;
|
|
|
6
6
|
export declare function isRouteGroupDir(seg: string): boolean;
|
|
7
7
|
/**
|
|
8
8
|
* File-route fallback (`pages/home` → `/home`, `pages/index` → `/`).
|
|
9
|
-
*
|
|
9
|
+
* Kept for tooling diagnostics only — production hosts must use Plan `pathPattern`.
|
|
10
10
|
*/
|
|
11
11
|
export declare function filePathPatternFromChunk(chunkId: string): string;
|
|
12
12
|
export type DeploymentPageUnit = {
|
|
@@ -17,7 +17,7 @@ export type DeploymentPageUnit = {
|
|
|
17
17
|
pathPattern?: string;
|
|
18
18
|
routeId?: string;
|
|
19
19
|
};
|
|
20
|
-
/** Canonical Browser HTTP pattern for a page unit. Mini must not read this. */
|
|
20
|
+
/** Canonical Browser HTTP pattern for a page unit. Mini must not read this. Requires Plan `pathPattern`. */
|
|
21
21
|
export declare function unitBrowserPathPattern(unit: DeploymentPageUnit | null | undefined): string;
|
|
22
22
|
export type PathSeg = {
|
|
23
23
|
kind: 'static';
|
package/dist/route-path.js
CHANGED
|
@@ -10,7 +10,7 @@ export function isRouteGroupDir(seg) {
|
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
12
|
* File-route fallback (`pages/home` → `/home`, `pages/index` → `/`).
|
|
13
|
-
*
|
|
13
|
+
* Kept for tooling diagnostics only — production hosts must use Plan `pathPattern`.
|
|
14
14
|
*/
|
|
15
15
|
export function filePathPatternFromChunk(chunkId) {
|
|
16
16
|
const rel = String(chunkId || '').replace(/^pages\//, '');
|
|
@@ -28,12 +28,14 @@ export function filePathPatternFromChunk(chunkId) {
|
|
|
28
28
|
}
|
|
29
29
|
return segs.length ? `/${segs.join('/')}` : '/';
|
|
30
30
|
}
|
|
31
|
-
/** Canonical Browser HTTP pattern for a page unit. Mini must not read this. */
|
|
31
|
+
/** Canonical Browser HTTP pattern for a page unit. Mini must not read this. Requires Plan `pathPattern`. */
|
|
32
32
|
export function unitBrowserPathPattern(unit) {
|
|
33
33
|
const explicit = String(unit?.pathPattern || '').trim();
|
|
34
|
-
if (explicit)
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
if (!explicit) {
|
|
35
|
+
const chunkId = String(unit?.chunkId || '');
|
|
36
|
+
throw new Error(`unitBrowserPathPattern: page unit ${chunkId || '(unknown)'} missing pathPattern (plan-only host)`);
|
|
37
|
+
}
|
|
38
|
+
return explicit.startsWith('/') ? explicit : `/${explicit}`;
|
|
37
39
|
}
|
|
38
40
|
export function parsePathPattern(pattern) {
|
|
39
41
|
const raw = String(pattern || '').trim();
|
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* P4 ServerArtifact —
|
|
3
|
-
*
|
|
2
|
+
* P4 ServerArtifact — thin host write of Rust/N-API normalized Plan.
|
|
3
|
+
* Assembly / digests / adapters live in vmz-artifacts via N-API.
|
|
4
4
|
*/
|
|
5
5
|
export declare const SERVER_ARTIFACT_SCHEMA = "vmz.server.artifact.v0";
|
|
6
6
|
export declare const HTTP_CONTRACT_SCHEMA = "vmz.http.contract.v0";
|
|
7
7
|
export declare const SERVER_RUNTIME_ADAPTER_SCHEMA = "vmz.server.runtime_adapter.v0";
|
|
8
|
-
/**
|
|
9
|
-
* @param {string} outDir
|
|
10
|
-
* @param {{
|
|
11
|
-
* profileId?: string | null,
|
|
12
|
-
* assembly?: string | null,
|
|
13
|
-
* serverRuntime?: string | null,
|
|
14
|
-
* packDigest?: string | null,
|
|
15
|
-
* }} [opts]
|
|
16
|
-
*/
|
|
17
8
|
interface ServerArtifactOpts {
|
|
18
9
|
profileId?: string | null;
|
|
19
10
|
assembly?: string | null;
|
|
@@ -42,6 +33,7 @@ interface ServerArtifactBody {
|
|
|
42
33
|
packDigest: string | null;
|
|
43
34
|
adapters: Record<string, unknown>;
|
|
44
35
|
artifactDigest?: string;
|
|
36
|
+
[key: string]: unknown;
|
|
45
37
|
}
|
|
46
38
|
export declare function emitServerArtifact(outDir: string, opts?: ServerArtifactOpts): {
|
|
47
39
|
artifact: ServerArtifactBody;
|
|
@@ -49,47 +41,7 @@ export declare function emitServerArtifact(outDir: string, opts?: ServerArtifact
|
|
|
49
41
|
httpContractDigest: string;
|
|
50
42
|
};
|
|
51
43
|
/**
|
|
52
|
-
*
|
|
53
|
-
* @param {string} adapterId
|
|
44
|
+
* Project a runtime adapter document from a normalized artifact (N-API).
|
|
54
45
|
*/
|
|
55
|
-
export declare function projectServerRuntimeAdapter(artifact:
|
|
56
|
-
host: string;
|
|
57
|
-
invoke: string;
|
|
58
|
-
status: string;
|
|
59
|
-
schema: string;
|
|
60
|
-
adapterId: string;
|
|
61
|
-
artifactDigest: any;
|
|
62
|
-
httpContractDigest: any;
|
|
63
|
-
spaFallback: boolean;
|
|
64
|
-
entry: any;
|
|
65
|
-
publicRouteCount: any;
|
|
66
|
-
internalCapabilityCount: any;
|
|
67
|
-
} | {
|
|
68
|
-
host: string;
|
|
69
|
-
invoke: string;
|
|
70
|
-
status: string;
|
|
71
|
-
note: string;
|
|
72
|
-
schema: string;
|
|
73
|
-
adapterId: string;
|
|
74
|
-
artifactDigest: any;
|
|
75
|
-
httpContractDigest: any;
|
|
76
|
-
spaFallback: boolean;
|
|
77
|
-
entry: any;
|
|
78
|
-
publicRouteCount: any;
|
|
79
|
-
internalCapabilityCount: any;
|
|
80
|
-
} | {
|
|
81
|
-
host: string;
|
|
82
|
-
invoke: string;
|
|
83
|
-
status: string;
|
|
84
|
-
note: string;
|
|
85
|
-
consumes: string[];
|
|
86
|
-
schema: string;
|
|
87
|
-
adapterId: string;
|
|
88
|
-
artifactDigest: any;
|
|
89
|
-
httpContractDigest: any;
|
|
90
|
-
spaFallback: boolean;
|
|
91
|
-
entry: any;
|
|
92
|
-
publicRouteCount: any;
|
|
93
|
-
internalCapabilityCount: any;
|
|
94
|
-
};
|
|
46
|
+
export declare function projectServerRuntimeAdapter(artifact: Record<string, unknown>, adapterId: string): any;
|
|
95
47
|
export {};
|
package/dist/server-artifact.js
CHANGED
|
@@ -1,109 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* P4 ServerArtifact —
|
|
3
|
-
*
|
|
2
|
+
* P4 ServerArtifact — thin host write of Rust/N-API normalized Plan.
|
|
3
|
+
* Assembly / digests / adapters live in vmz-artifacts via N-API.
|
|
4
4
|
*/
|
|
5
|
-
import crypto from 'node:crypto';
|
|
6
5
|
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
7
6
|
import path from 'node:path';
|
|
8
|
-
import {
|
|
7
|
+
import { requireNativeAddon } from './native-addon.js';
|
|
9
8
|
import { writePrettyJsonFile } from './pretty-json.js';
|
|
10
9
|
export const SERVER_ARTIFACT_SCHEMA = 'vmz.server.artifact.v0';
|
|
11
10
|
export const HTTP_CONTRACT_SCHEMA = 'vmz.http.contract.v0';
|
|
12
11
|
export const SERVER_RUNTIME_ADAPTER_SCHEMA = 'vmz.server.runtime_adapter.v0';
|
|
13
|
-
const DEFAULT_RPC_PATH = '/__vmz/rpc';
|
|
14
12
|
export function emitServerArtifact(outDir, opts = {}) {
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
})
|
|
29
|
-
const
|
|
30
|
-
/** @type {Array<Record<string, unknown>>} */
|
|
31
|
-
const internalCapabilities = [];
|
|
32
|
-
for (const u of units) {
|
|
33
|
-
const moduleId = u.serverModuleId != null ? String(u.serverModuleId) : '';
|
|
34
|
-
if (!moduleId)
|
|
35
|
-
continue;
|
|
36
|
-
const caps = Array.isArray(u.capabilities) ? u.capabilities.map(String) : [];
|
|
37
|
-
for (const method of caps) {
|
|
38
|
-
const key = `${moduleId}::${method}`;
|
|
39
|
-
if (publicKeys.has(key))
|
|
40
|
-
continue;
|
|
41
|
-
internalCapabilities.push({
|
|
42
|
-
chunkId: String(u.chunkId || ''),
|
|
43
|
-
moduleId,
|
|
44
|
-
method,
|
|
45
|
-
visibility: 'internal',
|
|
46
|
-
kind: 'capability',
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
const routeDecisionTree = [
|
|
51
|
-
{
|
|
52
|
-
id: 'rpc',
|
|
53
|
-
match: { method: 'POST', path: DEFAULT_RPC_PATH },
|
|
54
|
-
action: 'invoke-rpc',
|
|
55
|
-
visibility: 'internal-transport',
|
|
56
|
-
},
|
|
57
|
-
...publicRoutes.map((r, i) => ({
|
|
58
|
-
id: `public-route-${i}`,
|
|
59
|
-
match: { method: r.verb, path: r.path },
|
|
60
|
-
action: 'invoke-server-route',
|
|
61
|
-
target: { moduleId: r.moduleId, method: r.method },
|
|
62
|
-
visibility: 'public',
|
|
63
|
-
})),
|
|
64
|
-
];
|
|
65
|
-
const httpContractBody = {
|
|
66
|
-
schema: HTTP_CONTRACT_SCHEMA,
|
|
67
|
-
rpcPath: DEFAULT_RPC_PATH,
|
|
68
|
-
publicRoutes: publicRoutes.map((r) => ({
|
|
69
|
-
verb: r.verb,
|
|
70
|
-
path: r.path,
|
|
71
|
-
moduleId: r.moduleId,
|
|
72
|
-
method: r.method,
|
|
73
|
-
})),
|
|
74
|
-
internalCapabilityCount: internalCapabilities.length,
|
|
75
|
-
entry: 'fetch',
|
|
76
|
-
};
|
|
77
|
-
const httpContractDigest = sha256Hex(canonicalJson(httpContractBody));
|
|
78
|
-
const artifact = {
|
|
79
|
-
schema: SERVER_ARTIFACT_SCHEMA,
|
|
80
|
-
profileId: opts.profileId || null,
|
|
81
|
-
assembly: opts.assembly || null,
|
|
82
|
-
selectedRuntime,
|
|
83
|
-
entry: {
|
|
84
|
-
kind: 'fetch',
|
|
85
|
-
standards: ['Request', 'Response', 'Streams', 'AbortSignal'],
|
|
86
|
-
rpcPath: DEFAULT_RPC_PATH,
|
|
87
|
-
},
|
|
88
|
-
httpContract: {
|
|
89
|
-
schema: HTTP_CONTRACT_SCHEMA,
|
|
90
|
-
digest: httpContractDigest,
|
|
91
|
-
},
|
|
92
|
-
publicRoutes,
|
|
93
|
-
internalCapabilities,
|
|
94
|
-
middlewareUnits: [],
|
|
95
|
-
routeDecisionTree,
|
|
96
|
-
deploymentSchema: deployment.schema || null,
|
|
97
|
-
packDigest: opts.packDigest || null,
|
|
98
|
-
adapters: {
|
|
99
|
-
node: { kind: 'node-http', status: 'runtime', entry: 'handleNodeRequest' },
|
|
100
|
-
worker: { kind: 'fetch', status: 'runtime', entry: 'handleFetchRequest' },
|
|
101
|
-
deno: { kind: 'fetch', status: 'projected', entry: 'handleFetchRequest' },
|
|
102
|
-
bun: { kind: 'fetch', status: 'projected', entry: 'handleFetchRequest' },
|
|
103
|
-
'rust-host': { kind: 'contract-projection', status: 'projected', entry: 'fetch' },
|
|
104
|
-
},
|
|
105
|
-
};
|
|
106
|
-
artifact.artifactDigest = sha256Hex(canonicalJson({ ...artifact, artifactDigest: undefined }));
|
|
13
|
+
const deploymentPath = path.join(outDir, 'vmz-deployment.json');
|
|
14
|
+
const routesPath = path.join(outDir, 'vmz-routes.json');
|
|
15
|
+
const deploymentJson = existsSync(deploymentPath) ? readFileSync(deploymentPath, 'utf8') : '{}';
|
|
16
|
+
const routesJson = existsSync(routesPath) ? readFileSync(routesPath, 'utf8') : '[]';
|
|
17
|
+
const native = requireNativeAddon();
|
|
18
|
+
if (typeof native.normalizeServerArtifactJson !== 'function') {
|
|
19
|
+
throw new Error('vmz native addon missing normalizeServerArtifactJson — rebuild with `pnpm napi:build`');
|
|
20
|
+
}
|
|
21
|
+
const artifactJson = native.normalizeServerArtifactJson(deploymentJson, routesJson, {
|
|
22
|
+
profileId: opts.profileId ?? null,
|
|
23
|
+
assembly: opts.assembly ?? null,
|
|
24
|
+
serverRuntime: opts.serverRuntime ?? null,
|
|
25
|
+
packDigest: opts.packDigest ?? null,
|
|
26
|
+
});
|
|
27
|
+
const artifact = JSON.parse(artifactJson);
|
|
107
28
|
const vmzDir = path.join(outDir, '_vmz');
|
|
108
29
|
mkdirSync(vmzDir, { recursive: true });
|
|
109
30
|
const file = path.join(vmzDir, 'server-artifact.json');
|
|
@@ -116,79 +37,19 @@ export function emitServerArtifact(outDir, opts = {}) {
|
|
|
116
37
|
mkdirSync(dir, { recursive: true });
|
|
117
38
|
writePrettyJsonFile(path.join(dir, 'adapter.json'), projection);
|
|
118
39
|
}
|
|
119
|
-
return {
|
|
40
|
+
return {
|
|
41
|
+
artifact,
|
|
42
|
+
path: file,
|
|
43
|
+
httpContractDigest: artifact.httpContract?.digest || null,
|
|
44
|
+
};
|
|
120
45
|
}
|
|
121
46
|
/**
|
|
122
|
-
*
|
|
123
|
-
* @param {string} adapterId
|
|
47
|
+
* Project a runtime adapter document from a normalized artifact (N-API).
|
|
124
48
|
*/
|
|
125
49
|
export function projectServerRuntimeAdapter(artifact, adapterId) {
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
128
|
-
throw new Error(
|
|
129
|
-
}
|
|
130
|
-
const base = {
|
|
131
|
-
schema: SERVER_RUNTIME_ADAPTER_SCHEMA,
|
|
132
|
-
adapterId: id,
|
|
133
|
-
artifactDigest: artifact.artifactDigest,
|
|
134
|
-
httpContractDigest: artifact.httpContract?.digest || null,
|
|
135
|
-
spaFallback: false,
|
|
136
|
-
entry: artifact.entry,
|
|
137
|
-
publicRouteCount: Array.isArray(artifact.publicRoutes) ? artifact.publicRoutes.length : 0,
|
|
138
|
-
internalCapabilityCount: Array.isArray(artifact.internalCapabilities) ? artifact.internalCapabilities.length : 0,
|
|
139
|
-
};
|
|
140
|
-
if (id === 'node') {
|
|
141
|
-
return { ...base, host: 'node:http', invoke: 'handleNodeRequest', status: 'runtime' };
|
|
142
|
-
}
|
|
143
|
-
if (id === 'worker' || id === 'deno' || id === 'bun') {
|
|
144
|
-
return {
|
|
145
|
-
...base,
|
|
146
|
-
host: 'fetch',
|
|
147
|
-
invoke: 'handleFetchRequest',
|
|
148
|
-
status: id === 'worker' ? 'runtime' : 'projected',
|
|
149
|
-
note: id === 'worker'
|
|
150
|
-
? 'Fetch entry; live thin gated via worker-shaped subprocess host'
|
|
151
|
-
: 'Fetch contract projection; live runtime not gated',
|
|
152
|
-
};
|
|
50
|
+
const native = requireNativeAddon();
|
|
51
|
+
if (typeof native.projectServerRuntimeAdapterJson !== 'function') {
|
|
52
|
+
throw new Error('vmz native addon missing projectServerRuntimeAdapterJson — rebuild with `pnpm napi:build`');
|
|
153
53
|
}
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
...base,
|
|
157
|
-
host: 'rust-fetch-consumer',
|
|
158
|
-
invoke: 'fetch',
|
|
159
|
-
status: 'projected',
|
|
160
|
-
note: 'contract projection only — live Rust host binary parity not gated',
|
|
161
|
-
consumes: ['server-artifact.json', 'vmz-routes.json', 'vmz-deployment.json'],
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
function normalizeRuntime(raw) {
|
|
165
|
-
const v = String(raw || 'node').trim();
|
|
166
|
-
return SERVER_RUNTIMES.includes(v) ? v : 'node';
|
|
167
|
-
}
|
|
168
|
-
function readJson(file) {
|
|
169
|
-
if (!existsSync(file))
|
|
170
|
-
return null;
|
|
171
|
-
try {
|
|
172
|
-
return JSON.parse(readFileSync(file, 'utf8'));
|
|
173
|
-
}
|
|
174
|
-
catch {
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
function canonicalJson(value) {
|
|
179
|
-
return JSON.stringify(sortKeys(value));
|
|
180
|
-
}
|
|
181
|
-
function sortKeys(value) {
|
|
182
|
-
if (Array.isArray(value))
|
|
183
|
-
return value.map(sortKeys);
|
|
184
|
-
if (value && typeof value === 'object') {
|
|
185
|
-
const out = {};
|
|
186
|
-
for (const k of Object.keys(value).sort())
|
|
187
|
-
out[k] = sortKeys(value[k]);
|
|
188
|
-
return out;
|
|
189
|
-
}
|
|
190
|
-
return value;
|
|
191
|
-
}
|
|
192
|
-
function sha256Hex(text) {
|
|
193
|
-
return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
|
|
54
|
+
return JSON.parse(native.projectServerRuntimeAdapterJson(JSON.stringify(artifact), String(adapterId || '').trim()));
|
|
194
55
|
}
|
package/dist/static-emit.js
CHANGED
|
@@ -10,12 +10,12 @@ import { createRenderHost } from '@vmz/core/render-host';
|
|
|
10
10
|
import { resolveRouteLayoutChain } from '@vmz/core/route-layout-chain';
|
|
11
11
|
import { listClientComponentsSync } from '@vmz/core/component-registry';
|
|
12
12
|
import { emitCdnPolicy } from './cdn-policy.js';
|
|
13
|
-
import { emitContentAddressedAssets } from './content-addressed-assets.js';
|
|
13
|
+
import { emitContentAddressedAssets, hashedAssetHref } from './content-addressed-assets.js';
|
|
14
14
|
import { absoluteUrl, buildLocalePageMeta, localizeBodyLinks } from './locale-router.js';
|
|
15
15
|
import { requireNativeAddon } from './native-addon.js';
|
|
16
16
|
import { writePrettyJsonFile } from './pretty-json.js';
|
|
17
17
|
import { emitPublicStaticAssets } from './public-static-assets.js';
|
|
18
|
-
import {
|
|
18
|
+
import { listPublicPageUnits, parsePathPattern, unitBrowserPathPattern } from './route-path.js';
|
|
19
19
|
import { emitSiteFavicon, readSiteFaviconHeadHtml } from './site-favicon.js';
|
|
20
20
|
export const STATIC_DELIVERY_MANIFEST_SCHEMA = 'vmz.static.delivery_manifest.v0';
|
|
21
21
|
export async function emitWebStatic(distDir, opts = {}) {
|
|
@@ -33,6 +33,13 @@ export async function emitWebStatic(distDir, opts = {}) {
|
|
|
33
33
|
const pageCatalog = listPageClientFiles(distDir);
|
|
34
34
|
const generations = [];
|
|
35
35
|
const skipped = [];
|
|
36
|
+
// Hash immutable assets before any HTML write so shell slots carry final URLs
|
|
37
|
+
// (no post-emit HTML split/join rewrite).
|
|
38
|
+
emitStaticClientEntries(distDir, pageCatalog);
|
|
39
|
+
const assets = emitContentAddressedAssets(distDir);
|
|
40
|
+
const cssLogical = readCssEntry(distDir);
|
|
41
|
+
const cssEntry = hashedAssetHref(assets.rewrites, cssLogical) || hashedAssetHref(assets.rewrites, 'vmz.css');
|
|
42
|
+
const moduleScriptSrc = hashedAssetHref(assets.rewrites, 'entry-client.js') || '/entry-client.js';
|
|
36
43
|
for (const page of pageCatalog) {
|
|
37
44
|
const pattern = page.pathPattern || patternFromSegs(page.segs);
|
|
38
45
|
const routeId = guessRouteId(distDir, page.chunkId);
|
|
@@ -118,7 +125,8 @@ export async function emitWebStatic(distDir, opts = {}) {
|
|
|
118
125
|
layoutChain,
|
|
119
126
|
props,
|
|
120
127
|
meta: gen.meta,
|
|
121
|
-
cssEntry
|
|
128
|
+
cssEntry,
|
|
129
|
+
moduleScriptSrc,
|
|
122
130
|
headExtraHtml: faviconHead,
|
|
123
131
|
});
|
|
124
132
|
fs.writeFileSync(absHtml, html, 'utf8');
|
|
@@ -149,7 +157,8 @@ export async function emitWebStatic(distDir, opts = {}) {
|
|
|
149
157
|
robots: 'noindex,nofollow',
|
|
150
158
|
lang: 'en',
|
|
151
159
|
},
|
|
152
|
-
cssEntry
|
|
160
|
+
cssEntry,
|
|
161
|
+
moduleScriptSrc: '',
|
|
153
162
|
isErrorDocument: true,
|
|
154
163
|
headExtraHtml: faviconHead,
|
|
155
164
|
});
|
|
@@ -208,22 +217,16 @@ export async function emitWebStatic(distDir, opts = {}) {
|
|
|
208
217
|
fileCount: publicAssets.fileCount ?? 0,
|
|
209
218
|
source: 'source' in publicAssets && typeof publicAssets.source === 'string' ? publicAssets.source : null,
|
|
210
219
|
},
|
|
220
|
+
contentAddressedAssets: {
|
|
221
|
+
schema: assets.manifest.schema,
|
|
222
|
+
manifestDigest: assets.manifest.manifestDigest,
|
|
223
|
+
objectCount: assets.manifest.objectCount,
|
|
224
|
+
layout: assets.manifest.layout,
|
|
225
|
+
},
|
|
211
226
|
};
|
|
212
227
|
const digest = sha256Hex(canonicalJson(manifest));
|
|
213
228
|
manifest.manifestDigest = digest;
|
|
214
229
|
writePrettyJsonFile(path.join(vmzDir, 'static-delivery-manifest.json'), manifest);
|
|
215
|
-
emitStaticClientEntries(distDir, pageCatalog);
|
|
216
|
-
const assets = emitContentAddressedAssets(distDir);
|
|
217
|
-
manifest.contentAddressedAssets = {
|
|
218
|
-
schema: assets.manifest.schema,
|
|
219
|
-
manifestDigest: assets.manifest.manifestDigest,
|
|
220
|
-
objectCount: assets.manifest.objectCount,
|
|
221
|
-
layout: assets.manifest.layout,
|
|
222
|
-
};
|
|
223
|
-
// Re-stamp static manifest after linking asset digest (HTML already rewritten on disk).
|
|
224
|
-
delete manifest.manifestDigest;
|
|
225
|
-
manifest.manifestDigest = sha256Hex(canonicalJson(manifest));
|
|
226
|
-
writePrettyJsonFile(path.join(vmzDir, 'static-delivery-manifest.json'), manifest);
|
|
227
230
|
const cdn = emitCdnPolicy(distDir, manifest);
|
|
228
231
|
return {
|
|
229
232
|
manifest,
|
|
@@ -265,53 +268,25 @@ function sortKeys(value) {
|
|
|
265
268
|
*/
|
|
266
269
|
function listPageClientFiles(distDir) {
|
|
267
270
|
const fromDep = listPagesFromDeployment(distDir);
|
|
268
|
-
if (fromDep.length)
|
|
269
|
-
|
|
270
|
-
const root = path.join(distDir, 'pages');
|
|
271
|
-
/** @type {Array<{ chunkId: string, segs: ReturnType<typeof parsePathPattern>, pathPattern: string }>} */
|
|
272
|
-
const out = [];
|
|
273
|
-
function walk(abs, relParts) {
|
|
274
|
-
let ents;
|
|
275
|
-
try {
|
|
276
|
-
ents = fs.readdirSync(abs, { withFileTypes: true });
|
|
277
|
-
}
|
|
278
|
-
catch {
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
for (const e of ents) {
|
|
282
|
-
if (e.isDirectory())
|
|
283
|
-
walk(path.join(abs, e.name), [...relParts, e.name]);
|
|
284
|
-
else if (e.isFile() && e.name.endsWith('.client.js')) {
|
|
285
|
-
const stem = e.name.replace(/\.client\.js$/, '');
|
|
286
|
-
if (isRouteBoundaryStem(stem))
|
|
287
|
-
continue;
|
|
288
|
-
const chunkId = ['pages', ...relParts, stem].join('/');
|
|
289
|
-
const pathPattern = filePathPatternFromChunk(chunkId);
|
|
290
|
-
out.push({ chunkId, pathPattern, segs: parsePathPattern(pathPattern) });
|
|
291
|
-
}
|
|
292
|
-
}
|
|
271
|
+
if (!fromDep.length) {
|
|
272
|
+
throw new Error(`emitWebStatic: no page units with pathPattern in ${path.join(distDir, 'vmz-deployment.json')} (plan-only host)`);
|
|
293
273
|
}
|
|
294
|
-
|
|
295
|
-
return out;
|
|
274
|
+
return fromDep;
|
|
296
275
|
}
|
|
297
276
|
/**
|
|
298
277
|
* @param {string} distDir
|
|
299
278
|
*/
|
|
300
279
|
function listPagesFromDeployment(distDir) {
|
|
301
280
|
const deploymentPath = path.join(distDir, 'vmz-deployment.json');
|
|
302
|
-
if (!fs.existsSync(deploymentPath))
|
|
303
|
-
|
|
304
|
-
try {
|
|
305
|
-
const deployment = JSON.parse(fs.readFileSync(deploymentPath, 'utf8'));
|
|
306
|
-
return listPublicPageUnits(deployment).map((u) => {
|
|
307
|
-
const chunkId = String(u.chunkId || '').replace(/\\/g, '/');
|
|
308
|
-
const pathPattern = unitBrowserPathPattern(u);
|
|
309
|
-
return { chunkId, pathPattern, segs: parsePathPattern(pathPattern) };
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
catch {
|
|
313
|
-
return [];
|
|
281
|
+
if (!fs.existsSync(deploymentPath)) {
|
|
282
|
+
throw new Error(`emitWebStatic: missing ${deploymentPath} (plan-only host)`);
|
|
314
283
|
}
|
|
284
|
+
const deployment = JSON.parse(fs.readFileSync(deploymentPath, 'utf8'));
|
|
285
|
+
return listPublicPageUnits(deployment).map((u) => {
|
|
286
|
+
const chunkId = String(u.chunkId || '').replace(/\\/g, '/');
|
|
287
|
+
const pathPattern = unitBrowserPathPattern(u);
|
|
288
|
+
return { chunkId, pathPattern, segs: parsePathPattern(pathPattern) };
|
|
289
|
+
});
|
|
315
290
|
}
|
|
316
291
|
/**
|
|
317
292
|
* @param {ReturnType<typeof parsePathPattern>} segs
|
|
@@ -491,6 +466,7 @@ function readCssEntry(distDir) {
|
|
|
491
466
|
* props: Record<string, unknown>,
|
|
492
467
|
* meta: { title: string, description: string, canonical: string, robots: string, lang: string, dir?: string, alternates?: Array<{ hreflang: string, href: string }> },
|
|
493
468
|
* cssEntry: string | null,
|
|
469
|
+
* moduleScriptSrc?: string,
|
|
494
470
|
* isErrorDocument?: boolean,
|
|
495
471
|
* headExtraHtml?: string,
|
|
496
472
|
* }} input
|
|
@@ -521,6 +497,7 @@ function wrapDocument(input) {
|
|
|
521
497
|
...(input.cssEntry ? { cssEntry: String(input.cssEntry) } : {}),
|
|
522
498
|
isErrorDocument: !!input.isErrorDocument,
|
|
523
499
|
...(input.headExtraHtml ? { headExtraHtml: String(input.headExtraHtml) } : {}),
|
|
500
|
+
...(input.moduleScriptSrc !== undefined ? { moduleScriptSrc: String(input.moduleScriptSrc) } : {}),
|
|
524
501
|
});
|
|
525
502
|
}
|
|
526
503
|
/**
|
package/locales/en-US/cli.json
CHANGED
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"cli.cmd.refactor": "DX rename plans / apply",
|
|
44
44
|
"cli.cmd.refactor.rename": "Plan or apply a workspace rename",
|
|
45
45
|
"cli.cmd.explain": "DX causal explain",
|
|
46
|
+
"cli.cmd.github-actions": "Monitor GitHub Actions via Rust octocrab (status|wait)",
|
|
46
47
|
"cli.cmd.plan": "Dump frozen Rust plans via N-API",
|
|
47
48
|
"cli.cmd.plan.locale": "LocalePlan from locales/",
|
|
48
49
|
"cli.cmd.plan.document-route": "DocumentRoutePlan from documents/",
|
|
@@ -83,7 +84,20 @@
|
|
|
83
84
|
"cli.opt.apply": "Apply workspace edit plan",
|
|
84
85
|
"cli.opt.explain": "Include rename explain chain",
|
|
85
86
|
"cli.opt.explain.target": "Generic Workspace.explain target",
|
|
87
|
+
"cli.opt.gha.mode": "status (one-shot) or wait (poll until completed)",
|
|
88
|
+
"cli.opt.gha.workflow": "Workflow file name (e.g. ci.yml) or numeric id",
|
|
89
|
+
"cli.opt.gha.run-id": "Watch a specific workflow run id",
|
|
90
|
+
"cli.opt.gha.ref": "Commit SHA filter (head_sha)",
|
|
91
|
+
"cli.opt.gha.branch": "Branch name filter",
|
|
92
|
+
"cli.opt.gha.owner": "Repository owner (default GITHUB_REPOSITORY)",
|
|
93
|
+
"cli.opt.gha.repo": "Repository name (default GITHUB_REPOSITORY)",
|
|
94
|
+
"cli.opt.gha.token": "GitHub token (default GITHUB_TOKEN / GH_TOKEN)",
|
|
95
|
+
"cli.opt.gha.timeout-minutes": "Max wait minutes for mode=wait",
|
|
96
|
+
"cli.opt.gha.interval-seconds": "Poll interval seconds for mode=wait",
|
|
97
|
+
"cli.opt.gha.required-conclusion": "Required conclusion when mode=wait (default success)",
|
|
86
98
|
"cli.err.unknown_plan_kind": "unknown plan kind `{kind}` (locale | document-route)",
|
|
99
|
+
"cli.err.gha.mode": "unknown --mode {mode} (status | wait)",
|
|
100
|
+
"cli.err.gha.repo": "set --owner/--repo or GITHUB_REPOSITORY (owner/repo)",
|
|
87
101
|
"cli.err.unknown_target": "unknown --target {target} (browser | mini-program-wechat)",
|
|
88
102
|
"cli.err.document_build_aborted": "document build aborted due to diagnostics",
|
|
89
103
|
"cli.err.locale_rename_usage": "usage: vmz locale rename <from-message-id> <to-message-id>",
|
package/locales/en-US/diag.json
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
"diag.message": "{message}",
|
|
3
3
|
"diag.detail": "{detail}",
|
|
4
4
|
|
|
5
|
-
"vmz::template::jsx_rejected": "JSX / single-brace expressions are rejected in `<template>`: {detail}",
|
|
6
5
|
"vmz::template::illegal_directive": "Illegal or unsupported directive in `<template>`: {detail}",
|
|
7
6
|
"vmz::template::parse_failed": "Failed to parse `<template>`: {detail}",
|
|
8
7
|
"vmz::template::invalid_expr": "Invalid template expression: {detail}",
|
package/locales/zh-CN/diag.json
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
"diag.message": "{message}",
|
|
3
3
|
"diag.detail": "{detail}",
|
|
4
4
|
|
|
5
|
-
"vmz::template::jsx_rejected": "`<template>` 拒绝 JSX / 单花括号表达式:{detail}",
|
|
6
5
|
"vmz::template::illegal_directive": "`<template>` 中存在非法或不支持的指令:{detail}",
|
|
7
6
|
"vmz::template::parse_failed": "解析 `<template>` 失败:{detail}",
|
|
8
7
|
"vmz::template::invalid_expr": "模板表达式无效:{detail}",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vmz/vmz",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "VMZ Node toolchain — N-API workspace session + CLI (publish name @vmz/vmz)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,16 +48,16 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@vmz/commander": "0.1.
|
|
52
|
-
"@vmz/core": "0.1.
|
|
53
|
-
"@vmz/diagnostic": "0.1.
|
|
54
|
-
"@vmz/plugin": "0.1.
|
|
55
|
-
"@vmz/protocol": "0.1.
|
|
51
|
+
"@vmz/commander": "0.1.23",
|
|
52
|
+
"@vmz/core": "0.1.23",
|
|
53
|
+
"@vmz/diagnostic": "0.1.23",
|
|
54
|
+
"@vmz/plugin": "0.1.23",
|
|
55
|
+
"@vmz/protocol": "0.1.23",
|
|
56
56
|
"jiti": "^2.6.1"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@vmz/plugin-markdown-it": "0.1.
|
|
60
|
-
"@vmz/test": "0.1.
|
|
59
|
+
"@vmz/plugin-markdown-it": "0.1.23",
|
|
60
|
+
"@vmz/test": "0.1.23",
|
|
61
61
|
"typescript": "^5.8.3"
|
|
62
62
|
},
|
|
63
63
|
"peerDependenciesMeta": {
|
|
@@ -92,12 +92,12 @@
|
|
|
92
92
|
"cli"
|
|
93
93
|
],
|
|
94
94
|
"optionalDependencies": {
|
|
95
|
-
"@vmz/vmz-win32-x64": "0.1.
|
|
96
|
-
"@vmz/vmz-win32-arm64": "0.1.
|
|
97
|
-
"@vmz/vmz-darwin-x64": "0.1.
|
|
98
|
-
"@vmz/vmz-darwin-arm64": "0.1.
|
|
99
|
-
"@vmz/vmz-linux-x64": "0.1.
|
|
100
|
-
"@vmz/vmz-linux-arm64": "0.1.
|
|
95
|
+
"@vmz/vmz-win32-x64": "0.1.23",
|
|
96
|
+
"@vmz/vmz-win32-arm64": "0.1.23",
|
|
97
|
+
"@vmz/vmz-darwin-x64": "0.1.23",
|
|
98
|
+
"@vmz/vmz-darwin-arm64": "0.1.23",
|
|
99
|
+
"@vmz/vmz-linux-x64": "0.1.23",
|
|
100
|
+
"@vmz/vmz-linux-arm64": "0.1.23"
|
|
101
101
|
},
|
|
102
102
|
"publishConfig": {
|
|
103
103
|
"access": "public"
|