@vmz/vmz 0.0.4 → 0.1.1
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/build-assemble.d.ts +52 -0
- package/dist/build-assemble.js +192 -0
- package/dist/cdn-policy.d.ts +22 -4
- package/dist/cdn-policy.js +109 -13
- package/dist/cli.js +122 -27
- package/dist/content-addressed-assets.js +2 -1
- package/dist/delivery-profile.d.ts +84 -0
- package/dist/delivery-profile.js +348 -0
- package/dist/dev-session.d.ts +6 -0
- package/dist/dev-session.js +167 -40
- package/dist/document-build.js +33 -32
- package/dist/document-cmd.js +2 -9
- package/dist/document-enrich.js +8 -0
- package/dist/document-integrate.js +10 -17
- package/dist/embedded-packaging.d.ts +22 -0
- package/dist/embedded-packaging.js +109 -0
- package/dist/index.d.ts +26 -1
- package/dist/index.js +69 -2
- package/dist/locale-check.js +39 -66
- package/dist/locale-cmd.js +12 -44
- package/dist/locale-route-emit.d.ts +37 -0
- package/dist/locale-route-emit.js +109 -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/mini-host.d.ts +47 -0
- package/dist/mini-host.js +202 -0
- package/dist/native-addon.d.ts +9 -0
- package/dist/native-addon.js +84 -0
- package/dist/pack-client-packages.d.ts +25 -0
- package/dist/pack-client-packages.js +399 -0
- package/dist/pack.d.ts +58 -0
- package/dist/pack.js +123 -0
- package/dist/plugin-host.d.ts +1 -1
- package/dist/plugin-host.js +2 -2
- package/dist/pretty-json.d.ts +19 -0
- package/dist/pretty-json.js +43 -0
- package/dist/production-observability.js +3 -2
- package/dist/production-test-pack.d.ts +0 -14
- package/dist/production-test-pack.js +27 -31
- package/dist/release-pack.js +17 -21
- package/dist/route-path.d.ts +35 -0
- package/dist/route-path.js +77 -0
- package/dist/server-artifact.d.ts +140 -0
- package/dist/server-artifact.js +204 -0
- package/dist/server-language-backend.d.ts +89 -0
- package/dist/server-language-backend.js +121 -0
- package/dist/site-delivery.js +3 -2
- package/dist/static-emit.d.ts +10 -1
- package/dist/static-emit.js +192 -97
- package/dist/test-cmd.js +2 -1
- package/dist/wechat-packaging.d.ts +22 -0
- package/dist/wechat-packaging.js +59 -0
- package/package.json +12 -12
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Language DSL backends (lang on `<script server>`).
|
|
3
|
+
* Author surface is a VMZ DSL flavor — not full target-language source.
|
|
4
|
+
*/
|
|
5
|
+
/** @typedef {'ts' | 'rust' | 'python' | 'java'} ServerLangId */
|
|
6
|
+
export declare const SERVER_LANG_IDS: readonly string[];
|
|
7
|
+
/** @type {Record<string, ServerLangId>} */
|
|
8
|
+
export declare const SERVER_LANG_ALIASES: Readonly<{
|
|
9
|
+
ts: "ts";
|
|
10
|
+
typescript: "ts";
|
|
11
|
+
rust: "rust";
|
|
12
|
+
python: "python";
|
|
13
|
+
java: "java";
|
|
14
|
+
}>;
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {{
|
|
17
|
+
* langId: ServerLangId,
|
|
18
|
+
* aliases: string[],
|
|
19
|
+
* compatibleRuntimes: string[],
|
|
20
|
+
* implemented: boolean,
|
|
21
|
+
* artifactRoot: string,
|
|
22
|
+
* }} ServerLanguageBackendMeta
|
|
23
|
+
*/
|
|
24
|
+
/** @type {Record<ServerLangId, ServerLanguageBackendMeta>} */
|
|
25
|
+
export declare const SERVER_LANGUAGE_BACKENDS: Readonly<{
|
|
26
|
+
ts: {
|
|
27
|
+
langId: string;
|
|
28
|
+
aliases: string[];
|
|
29
|
+
compatibleRuntimes: string[];
|
|
30
|
+
implemented: boolean;
|
|
31
|
+
artifactRoot: string;
|
|
32
|
+
};
|
|
33
|
+
rust: {
|
|
34
|
+
langId: string;
|
|
35
|
+
aliases: string[];
|
|
36
|
+
compatibleRuntimes: string[];
|
|
37
|
+
implemented: boolean;
|
|
38
|
+
artifactRoot: string;
|
|
39
|
+
};
|
|
40
|
+
python: {
|
|
41
|
+
langId: string;
|
|
42
|
+
aliases: string[];
|
|
43
|
+
compatibleRuntimes: string[];
|
|
44
|
+
implemented: boolean;
|
|
45
|
+
artifactRoot: string;
|
|
46
|
+
};
|
|
47
|
+
java: {
|
|
48
|
+
langId: string;
|
|
49
|
+
aliases: string[];
|
|
50
|
+
compatibleRuntimes: string[];
|
|
51
|
+
implemented: boolean;
|
|
52
|
+
artifactRoot: string;
|
|
53
|
+
};
|
|
54
|
+
}>;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve author `lang` attr (or null/undefined for default TS).
|
|
57
|
+
* @param {string | null | undefined} raw
|
|
58
|
+
* @returns {{
|
|
59
|
+
* ok: true, langId: ServerLangId, backend: ServerLanguageBackendMeta
|
|
60
|
+
* } | {
|
|
61
|
+
* ok: false, code: string, message: string
|
|
62
|
+
* }}
|
|
63
|
+
*/
|
|
64
|
+
export declare function resolveServerLanguage(raw: any): {
|
|
65
|
+
ok: boolean;
|
|
66
|
+
code: string;
|
|
67
|
+
message: string;
|
|
68
|
+
langId?: undefined;
|
|
69
|
+
backend?: undefined;
|
|
70
|
+
} | {
|
|
71
|
+
ok: boolean;
|
|
72
|
+
langId: any;
|
|
73
|
+
backend: any;
|
|
74
|
+
code?: undefined;
|
|
75
|
+
message?: undefined;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* @param {ServerLangId} langId
|
|
79
|
+
* @param {string | null | undefined} serverRuntime
|
|
80
|
+
*/
|
|
81
|
+
export declare function assertLangRuntimePair(langId: any, serverRuntime: any): {
|
|
82
|
+
ok: boolean;
|
|
83
|
+
code: string;
|
|
84
|
+
message: string;
|
|
85
|
+
} | {
|
|
86
|
+
ok: boolean;
|
|
87
|
+
code?: undefined;
|
|
88
|
+
message?: undefined;
|
|
89
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Language DSL backends (lang on `<script server>`).
|
|
3
|
+
* Author surface is a VMZ DSL flavor — not full target-language source.
|
|
4
|
+
*/
|
|
5
|
+
// @ts-nocheck
|
|
6
|
+
/** @typedef {'ts' | 'rust' | 'python' | 'java'} ServerLangId */
|
|
7
|
+
export const SERVER_LANG_IDS = Object.freeze(['ts', 'rust', 'python', 'java']);
|
|
8
|
+
/** @type {Record<string, ServerLangId>} */
|
|
9
|
+
export const SERVER_LANG_ALIASES = Object.freeze({
|
|
10
|
+
ts: 'ts',
|
|
11
|
+
typescript: 'ts',
|
|
12
|
+
rust: 'rust',
|
|
13
|
+
python: 'python',
|
|
14
|
+
java: 'java',
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {{
|
|
18
|
+
* langId: ServerLangId,
|
|
19
|
+
* aliases: string[],
|
|
20
|
+
* compatibleRuntimes: string[],
|
|
21
|
+
* implemented: boolean,
|
|
22
|
+
* artifactRoot: string,
|
|
23
|
+
* }} ServerLanguageBackendMeta
|
|
24
|
+
*/
|
|
25
|
+
/** @type {Record<ServerLangId, ServerLanguageBackendMeta>} */
|
|
26
|
+
export const SERVER_LANGUAGE_BACKENDS = Object.freeze({
|
|
27
|
+
ts: {
|
|
28
|
+
langId: 'ts',
|
|
29
|
+
aliases: ['ts', 'typescript'],
|
|
30
|
+
compatibleRuntimes: ['node', 'worker', 'deno', 'bun'],
|
|
31
|
+
implemented: true,
|
|
32
|
+
artifactRoot: 'dist/#server',
|
|
33
|
+
},
|
|
34
|
+
rust: {
|
|
35
|
+
langId: 'rust',
|
|
36
|
+
aliases: ['rust'],
|
|
37
|
+
compatibleRuntimes: ['rust-host'],
|
|
38
|
+
implemented: true,
|
|
39
|
+
artifactRoot: 'target/vmz/server-rust',
|
|
40
|
+
},
|
|
41
|
+
python: {
|
|
42
|
+
langId: 'python',
|
|
43
|
+
aliases: ['python'],
|
|
44
|
+
compatibleRuntimes: ['python-host'],
|
|
45
|
+
implemented: false,
|
|
46
|
+
artifactRoot: 'target/vmz/server-python',
|
|
47
|
+
},
|
|
48
|
+
java: {
|
|
49
|
+
langId: 'java',
|
|
50
|
+
aliases: ['java'],
|
|
51
|
+
compatibleRuntimes: ['jvm-host'],
|
|
52
|
+
implemented: false,
|
|
53
|
+
artifactRoot: 'target/vmz/server-java',
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
/**
|
|
57
|
+
* Resolve author `lang` attr (or null/undefined for default TS).
|
|
58
|
+
* @param {string | null | undefined} raw
|
|
59
|
+
* @returns {{
|
|
60
|
+
* ok: true, langId: ServerLangId, backend: ServerLanguageBackendMeta
|
|
61
|
+
* } | {
|
|
62
|
+
* ok: false, code: string, message: string
|
|
63
|
+
* }}
|
|
64
|
+
*/
|
|
65
|
+
export function resolveServerLanguage(raw) {
|
|
66
|
+
const trimmed = raw == null ? '' : String(raw).trim();
|
|
67
|
+
if (!trimmed) {
|
|
68
|
+
return { ok: true, langId: 'ts', backend: SERVER_LANGUAGE_BACKENDS.ts };
|
|
69
|
+
}
|
|
70
|
+
const langId = SERVER_LANG_ALIASES[trimmed];
|
|
71
|
+
if (!langId) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
code: 'vmz::server::unknown_language',
|
|
75
|
+
message: `unknown script language \`${trimmed}\`; use ts|typescript|rust|python|java`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const backend = SERVER_LANGUAGE_BACKENDS[langId];
|
|
79
|
+
if (!backend.implemented) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
code: 'vmz::server::language_backend_unimplemented',
|
|
83
|
+
message: `\`<script server lang="${langId}">\` is registered but not implemented yet`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return { ok: true, langId, backend };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* @param {ServerLangId} langId
|
|
90
|
+
* @param {string | null | undefined} serverRuntime
|
|
91
|
+
*/
|
|
92
|
+
export function assertLangRuntimePair(langId, serverRuntime) {
|
|
93
|
+
const backend = SERVER_LANGUAGE_BACKENDS[langId];
|
|
94
|
+
if (!backend) {
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
code: 'vmz::server::unknown_language',
|
|
98
|
+
message: `unknown langId ${langId}`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (langId === 'ts') {
|
|
102
|
+
// TS stays compatible with existing node/worker defaults; rust-host rejects ts.
|
|
103
|
+
if (serverRuntime === 'rust-host' || serverRuntime === 'python-host' || serverRuntime === 'jvm-host') {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
code: 'vmz::server::lang_runtime_mismatch',
|
|
107
|
+
message: `lang=ts is incompatible with serverRuntime=${serverRuntime}`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return { ok: true };
|
|
111
|
+
}
|
|
112
|
+
const rt = String(serverRuntime || '').trim();
|
|
113
|
+
if (!backend.compatibleRuntimes.includes(rt)) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
code: 'vmz::server::lang_runtime_mismatch',
|
|
117
|
+
message: `lang=${langId} requires serverRuntime in [${backend.compatibleRuntimes.join('|')}] (got ${rt || '(none)'})`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return { ok: true };
|
|
121
|
+
}
|
package/dist/site-delivery.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import crypto from 'node:crypto';
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
|
+
import { writePrettyJsonFile } from './pretty-json.js';
|
|
9
10
|
export const SITE_DELIVERY_CONTRACT_SCHEMA = 'vmz.site.delivery_contract.v0';
|
|
10
11
|
export const SITE_DELIVERY_RESOLUTION_SCHEMA = 'vmz.site.delivery_resolution.v0';
|
|
11
12
|
/**
|
|
@@ -318,11 +319,11 @@ export function emitSiteDelivery(outDir, deliveryRaw, opts = {}) {
|
|
|
318
319
|
}
|
|
319
320
|
const vmzDir = path.join(outDir, '_vmz');
|
|
320
321
|
fs.mkdirSync(vmzDir, { recursive: true });
|
|
321
|
-
|
|
322
|
+
writePrettyJsonFile(path.join(vmzDir, 'site-delivery-contract.json'), norm.contract);
|
|
322
323
|
let resolution = null;
|
|
323
324
|
if (opts.probes) {
|
|
324
325
|
resolution = resolveSiteRelease(norm.contract, opts.probes);
|
|
325
|
-
|
|
326
|
+
writePrettyJsonFile(path.join(vmzDir, 'site-delivery-resolution.json'), resolution);
|
|
326
327
|
}
|
|
327
328
|
return { contract: norm.contract, resolution };
|
|
328
329
|
}
|
package/dist/static-emit.d.ts
CHANGED
|
@@ -29,11 +29,13 @@ export declare function emitWebStatic(distDir: any, opts?: {}): Promise<{
|
|
|
29
29
|
chunkId: any;
|
|
30
30
|
htmlPath: any;
|
|
31
31
|
classification: any;
|
|
32
|
+
localeId: any;
|
|
32
33
|
seo: {
|
|
33
34
|
title: any;
|
|
34
35
|
description: any;
|
|
35
36
|
canonical: any;
|
|
36
37
|
robots: any;
|
|
38
|
+
alternates: any;
|
|
37
39
|
};
|
|
38
40
|
}[];
|
|
39
41
|
skipped: any[];
|
|
@@ -75,7 +77,14 @@ export declare function emitWebStatic(distDir: any, opts?: {}): Promise<{
|
|
|
75
77
|
};
|
|
76
78
|
})[];
|
|
77
79
|
errorDocuments: any;
|
|
78
|
-
routes: any;
|
|
80
|
+
routes: any[];
|
|
81
|
+
localeCache: {
|
|
82
|
+
strategy: string;
|
|
83
|
+
varyAcceptLanguage: boolean;
|
|
84
|
+
defaultLocale: any;
|
|
85
|
+
routeCount: number;
|
|
86
|
+
locales: any[];
|
|
87
|
+
};
|
|
79
88
|
};
|
|
80
89
|
cdnAdapters: {
|
|
81
90
|
'local-static': {
|
package/dist/static-emit.js
CHANGED
|
@@ -9,6 +9,10 @@ import path from 'node:path';
|
|
|
9
9
|
import { pathToFileURL } from 'node:url';
|
|
10
10
|
import { emitCdnPolicy } from './cdn-policy.js';
|
|
11
11
|
import { emitContentAddressedAssets } from './content-addressed-assets.js';
|
|
12
|
+
import { absoluteUrl, buildLocalePageMeta, localizeBodyLinks } from './locale-router.js';
|
|
13
|
+
import { requireNativeAddon } from './native-addon.js';
|
|
14
|
+
import { writePrettyJsonFile } from './pretty-json.js';
|
|
15
|
+
import { filePathPatternFromChunk, isRouteBoundaryStem, listPublicPageUnits, parsePathPattern, unitBrowserPathPattern, } from './route-path.js';
|
|
12
16
|
export const STATIC_DELIVERY_MANIFEST_SCHEMA = 'vmz.static.delivery_manifest.v0';
|
|
13
17
|
/**
|
|
14
18
|
* @param {string} distDir
|
|
@@ -42,7 +46,7 @@ export async function emitWebStatic(distDir, opts = {}) {
|
|
|
42
46
|
/** @type {Array<{ routeId: string, path: string, chunkId: string, classification: string, reason: string }>} */
|
|
43
47
|
const skipped = [];
|
|
44
48
|
for (const page of pageCatalog) {
|
|
45
|
-
const pattern = patternFromSegs(page.segs);
|
|
49
|
+
const pattern = page.pathPattern || patternFromSegs(page.segs);
|
|
46
50
|
const routeId = guessRouteId(distDir, page.chunkId);
|
|
47
51
|
if (page.segs.some((s) => s.kind === 'param' || s.kind === 'catch')) {
|
|
48
52
|
skipped.push({
|
|
@@ -104,29 +108,44 @@ export async function emitWebStatic(distDir, opts = {}) {
|
|
|
104
108
|
continue;
|
|
105
109
|
bodyHtml = await renderToString(Layout, {}, { slotHtml: bodyHtml });
|
|
106
110
|
}
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
const localeArt = loadLocaleArtifact(distDir);
|
|
112
|
+
// Locale artifact routeId is chunkId (`pages/about`), not the page class name.
|
|
113
|
+
const localeWrites = expandLocaleStaticGenerations({
|
|
114
|
+
localeArt,
|
|
115
|
+
routeId: page.chunkId,
|
|
112
116
|
chunkId: page.chunkId,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
meta,
|
|
116
|
-
cssEntry: readCssEntry(distDir),
|
|
117
|
-
});
|
|
118
|
-
fs.writeFileSync(absHtml, html, 'utf8');
|
|
119
|
-
generations.push({
|
|
120
|
-
routeId,
|
|
121
|
-
path: pattern,
|
|
122
|
-
chunkId: page.chunkId,
|
|
123
|
-
htmlPath: htmlPath.replaceAll('\\', '/'),
|
|
124
|
-
classification: 'Static',
|
|
125
|
-
title: meta.title,
|
|
126
|
-
description: meta.description,
|
|
127
|
-
canonical: meta.canonical,
|
|
128
|
-
robots: meta.robots,
|
|
117
|
+
pattern,
|
|
118
|
+
origin,
|
|
119
|
+
baseMeta: meta,
|
|
129
120
|
});
|
|
121
|
+
for (const gen of localeWrites) {
|
|
122
|
+
const absHtml = path.join(distDir, gen.htmlPath);
|
|
123
|
+
fs.mkdirSync(path.dirname(absHtml), { recursive: true });
|
|
124
|
+
// Each LocaleId HTML must retain locale on same-app Links (realization authority).
|
|
125
|
+
const localizedBody = gen.localeId && localeArt ? localizeBodyLinks(bodyHtml, gen.localeId, localeArt) : bodyHtml;
|
|
126
|
+
const html = wrapDocument({
|
|
127
|
+
bodyHtml: localizedBody,
|
|
128
|
+
chunkId: page.chunkId,
|
|
129
|
+
layoutChain,
|
|
130
|
+
props,
|
|
131
|
+
meta: gen.meta,
|
|
132
|
+
cssEntry: readCssEntry(distDir),
|
|
133
|
+
});
|
|
134
|
+
fs.writeFileSync(absHtml, html, 'utf8');
|
|
135
|
+
generations.push({
|
|
136
|
+
routeId,
|
|
137
|
+
path: gen.path,
|
|
138
|
+
chunkId: page.chunkId,
|
|
139
|
+
htmlPath: gen.htmlPath.replaceAll('\\', '/'),
|
|
140
|
+
classification: 'Static',
|
|
141
|
+
title: gen.meta.title,
|
|
142
|
+
description: meta.description,
|
|
143
|
+
canonical: gen.meta.canonical,
|
|
144
|
+
robots: gen.meta.robots,
|
|
145
|
+
localeId: gen.localeId || null,
|
|
146
|
+
alternates: Array.isArray(gen.meta.alternates) ? gen.meta.alternates : [],
|
|
147
|
+
});
|
|
148
|
+
}
|
|
130
149
|
}
|
|
131
150
|
const notFoundHtml = wrapDocument({
|
|
132
151
|
bodyHtml: '<main><h1>Not Found</h1><p>route-static-404</p></main>',
|
|
@@ -146,7 +165,11 @@ export async function emitWebStatic(distDir, opts = {}) {
|
|
|
146
165
|
fs.writeFileSync(path.join(distDir, '404.html'), notFoundHtml, 'utf8');
|
|
147
166
|
const sitemap = buildSitemap(origin, generations);
|
|
148
167
|
fs.writeFileSync(path.join(distDir, 'sitemap.xml'), sitemap, 'utf8');
|
|
149
|
-
const
|
|
168
|
+
const nativeRobots = requireNativeAddon();
|
|
169
|
+
if (typeof nativeRobots.generateRobotsTxt !== 'function') {
|
|
170
|
+
throw new Error('vmz native addon missing generateRobotsTxt — rebuild with `pnpm napi:build`');
|
|
171
|
+
}
|
|
172
|
+
const robots = nativeRobots.generateRobotsTxt(`${origin}/sitemap.xml`);
|
|
150
173
|
fs.writeFileSync(path.join(distDir, 'robots.txt'), robots, 'utf8');
|
|
151
174
|
// Hard rule: no SPA fallback shim in artifact.
|
|
152
175
|
for (const bad of ['_redirects', 'vercel.json', 'netlify.toml']) {
|
|
@@ -174,11 +197,13 @@ export async function emitWebStatic(distDir, opts = {}) {
|
|
|
174
197
|
chunkId: g.chunkId,
|
|
175
198
|
htmlPath: g.htmlPath,
|
|
176
199
|
classification: g.classification,
|
|
200
|
+
localeId: g.localeId || null,
|
|
177
201
|
seo: {
|
|
178
202
|
title: g.title,
|
|
179
203
|
description: g.description,
|
|
180
204
|
canonical: g.canonical,
|
|
181
205
|
robots: g.robots,
|
|
206
|
+
alternates: g.alternates || [],
|
|
182
207
|
},
|
|
183
208
|
})),
|
|
184
209
|
skipped,
|
|
@@ -189,7 +214,7 @@ export async function emitWebStatic(distDir, opts = {}) {
|
|
|
189
214
|
};
|
|
190
215
|
const digest = sha256Hex(canonicalJson(manifest));
|
|
191
216
|
manifest.manifestDigest = digest;
|
|
192
|
-
|
|
217
|
+
writePrettyJsonFile(path.join(vmzDir, 'static-delivery-manifest.json'), manifest);
|
|
193
218
|
const assets = emitContentAddressedAssets(distDir);
|
|
194
219
|
manifest.contentAddressedAssets = {
|
|
195
220
|
schema: assets.manifest.schema,
|
|
@@ -200,7 +225,7 @@ export async function emitWebStatic(distDir, opts = {}) {
|
|
|
200
225
|
// Re-stamp static manifest after linking asset digest (HTML already rewritten on disk).
|
|
201
226
|
delete manifest.manifestDigest;
|
|
202
227
|
manifest.manifestDigest = sha256Hex(canonicalJson(manifest));
|
|
203
|
-
|
|
228
|
+
writePrettyJsonFile(path.join(vmzDir, 'static-delivery-manifest.json'), manifest);
|
|
204
229
|
const cdn = emitCdnPolicy(distDir, manifest);
|
|
205
230
|
return {
|
|
206
231
|
manifest,
|
|
@@ -240,8 +265,11 @@ function sortKeys(value) {
|
|
|
240
265
|
* @param {string} distDir
|
|
241
266
|
*/
|
|
242
267
|
function listPageClientFiles(distDir) {
|
|
268
|
+
const fromDep = listPagesFromDeployment(distDir);
|
|
269
|
+
if (fromDep.length)
|
|
270
|
+
return fromDep;
|
|
243
271
|
const root = path.join(distDir, 'pages');
|
|
244
|
-
/** @type {Array<{ chunkId: string, segs: ReturnType<typeof
|
|
272
|
+
/** @type {Array<{ chunkId: string, segs: ReturnType<typeof parsePathPattern>, pathPattern: string }>} */
|
|
245
273
|
const out = [];
|
|
246
274
|
function walk(abs, relParts) {
|
|
247
275
|
let ents;
|
|
@@ -256,10 +284,11 @@ function listPageClientFiles(distDir) {
|
|
|
256
284
|
walk(path.join(abs, e.name), [...relParts, e.name]);
|
|
257
285
|
else if (e.isFile() && e.name.endsWith('.client.js')) {
|
|
258
286
|
const stem = e.name.replace(/\.client\.js$/, '');
|
|
259
|
-
if (stem
|
|
287
|
+
if (isRouteBoundaryStem(stem))
|
|
260
288
|
continue;
|
|
261
289
|
const chunkId = ['pages', ...relParts, stem].join('/');
|
|
262
|
-
|
|
290
|
+
const pathPattern = filePathPatternFromChunk(chunkId);
|
|
291
|
+
out.push({ chunkId, pathPattern, segs: parsePathPattern(pathPattern) });
|
|
263
292
|
}
|
|
264
293
|
}
|
|
265
294
|
}
|
|
@@ -267,32 +296,26 @@ function listPageClientFiles(distDir) {
|
|
|
267
296
|
return out;
|
|
268
297
|
}
|
|
269
298
|
/**
|
|
270
|
-
* @param {string}
|
|
299
|
+
* @param {string} distDir
|
|
271
300
|
*/
|
|
272
|
-
function
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
segs.push({ kind: 'catch', name: catchAll[1] });
|
|
287
|
-
else if (param)
|
|
288
|
-
segs.push({ kind: 'param', name: param[1] });
|
|
289
|
-
else
|
|
290
|
-
segs.push({ kind: 'static', value: p.toLowerCase() });
|
|
301
|
+
function listPagesFromDeployment(distDir) {
|
|
302
|
+
const deploymentPath = path.join(distDir, 'vmz-deployment.json');
|
|
303
|
+
if (!fs.existsSync(deploymentPath))
|
|
304
|
+
return [];
|
|
305
|
+
try {
|
|
306
|
+
const deployment = JSON.parse(fs.readFileSync(deploymentPath, 'utf8'));
|
|
307
|
+
return listPublicPageUnits(deployment).map((u) => {
|
|
308
|
+
const chunkId = String(u.chunkId || '').replace(/\\/g, '/');
|
|
309
|
+
const pathPattern = unitBrowserPathPattern(u);
|
|
310
|
+
return { chunkId, pathPattern, segs: parsePathPattern(pathPattern) };
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
return [];
|
|
291
315
|
}
|
|
292
|
-
return segs;
|
|
293
316
|
}
|
|
294
317
|
/**
|
|
295
|
-
* @param {ReturnType<typeof
|
|
318
|
+
* @param {ReturnType<typeof parsePathPattern>} segs
|
|
296
319
|
*/
|
|
297
320
|
function patternFromSegs(segs) {
|
|
298
321
|
if (!segs.length)
|
|
@@ -371,12 +394,96 @@ async function resolvePageMeta(Page, ctx) {
|
|
|
371
394
|
else if (Page.meta && typeof Page.meta === 'object') {
|
|
372
395
|
raw = Page.meta;
|
|
373
396
|
}
|
|
374
|
-
const title = String(raw.title ||
|
|
375
|
-
const description = String(raw.description ||
|
|
397
|
+
const title = String(raw.title || guessTitle(ctx.pathname) || 'App');
|
|
398
|
+
const description = String(raw.description || '');
|
|
376
399
|
const canonical = String(raw.canonical || `${ctx.origin}${ctx.pathname === '/' ? '/' : ctx.pathname}`);
|
|
377
400
|
const robots = String(raw.robots || 'index,follow');
|
|
378
401
|
const lang = String(raw.lang || 'en');
|
|
379
|
-
return { title, description, canonical, robots, lang };
|
|
402
|
+
return { title, description, canonical, robots, lang, alternates: [] };
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* @param {string} distDir
|
|
406
|
+
*/
|
|
407
|
+
function loadLocaleArtifact(distDir) {
|
|
408
|
+
const p = path.join(distDir, '_vmz', 'locale-route-realization.json');
|
|
409
|
+
if (!fs.existsSync(p))
|
|
410
|
+
return null;
|
|
411
|
+
try {
|
|
412
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Expand one Static route across LocaleId realizations (hreflang seed + prefixed HTML).
|
|
420
|
+
* @param {{
|
|
421
|
+
* localeArt: any,
|
|
422
|
+
* routeId: string,
|
|
423
|
+
* chunkId: string,
|
|
424
|
+
* pattern: string,
|
|
425
|
+
* origin: string,
|
|
426
|
+
* baseMeta: { title: string, description: string, canonical: string, robots: string, lang: string, alternates?: any[] },
|
|
427
|
+
* }} input
|
|
428
|
+
*/
|
|
429
|
+
function expandLocaleStaticGenerations(input) {
|
|
430
|
+
const { localeArt, routeId, pattern, origin, baseMeta } = input;
|
|
431
|
+
if (!localeArt?.realizations?.length) {
|
|
432
|
+
return [
|
|
433
|
+
{
|
|
434
|
+
path: pattern,
|
|
435
|
+
htmlPath: htmlPathForRoute(pattern),
|
|
436
|
+
localeId: null,
|
|
437
|
+
meta: baseMeta,
|
|
438
|
+
},
|
|
439
|
+
];
|
|
440
|
+
}
|
|
441
|
+
const locales = (localeArt.locales || []).map((l) => l.id);
|
|
442
|
+
const directions = Object.fromEntries((localeArt.locales || []).map((l) => [l.id, l.direction || 'ltr']));
|
|
443
|
+
const defaultLocale = localeArt.defaultLocale || locales[0];
|
|
444
|
+
const forRoute = (localeArt.realizations || []).filter((r) => r.routeId === routeId || r.routeId === input.chunkId || r.pathPattern === pattern || (r.path === pattern && !r.prefixed));
|
|
445
|
+
/** @type {any[]} */
|
|
446
|
+
const out = [];
|
|
447
|
+
for (const loc of locales) {
|
|
448
|
+
const hit = forRoute.find((r) => r.localeId === loc);
|
|
449
|
+
if (!hit)
|
|
450
|
+
continue;
|
|
451
|
+
const built = buildLocalePageMeta({
|
|
452
|
+
routeId,
|
|
453
|
+
localeId: loc,
|
|
454
|
+
direction: directions[loc],
|
|
455
|
+
title: baseMeta.title,
|
|
456
|
+
description: baseMeta.description,
|
|
457
|
+
origin,
|
|
458
|
+
realizations: localeArt.realizations,
|
|
459
|
+
locales,
|
|
460
|
+
defaultLocale,
|
|
461
|
+
});
|
|
462
|
+
out.push({
|
|
463
|
+
path: hit.path,
|
|
464
|
+
htmlPath: htmlPathForRoute(hit.path),
|
|
465
|
+
localeId: loc,
|
|
466
|
+
meta: {
|
|
467
|
+
title: baseMeta.title,
|
|
468
|
+
description: baseMeta.description,
|
|
469
|
+
canonical: built.canonical || absoluteUrl(origin, hit.path),
|
|
470
|
+
robots: baseMeta.robots,
|
|
471
|
+
lang: loc,
|
|
472
|
+
dir: directions[loc] || 'ltr',
|
|
473
|
+
alternates: built.alternates || [],
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return out.length
|
|
478
|
+
? out
|
|
479
|
+
: [
|
|
480
|
+
{
|
|
481
|
+
path: pattern,
|
|
482
|
+
htmlPath: htmlPathForRoute(pattern),
|
|
483
|
+
localeId: null,
|
|
484
|
+
meta: baseMeta,
|
|
485
|
+
},
|
|
486
|
+
];
|
|
380
487
|
}
|
|
381
488
|
function guessTitle(pathname) {
|
|
382
489
|
if (pathname === '/')
|
|
@@ -405,59 +512,47 @@ function readCssEntry(distDir) {
|
|
|
405
512
|
* chunkId: string,
|
|
406
513
|
* layoutChain: string[],
|
|
407
514
|
* props: Record<string, unknown>,
|
|
408
|
-
* meta: { title: string, description: string, canonical: string, robots: string, lang: string },
|
|
515
|
+
* meta: { title: string, description: string, canonical: string, robots: string, lang: string, dir?: string, alternates?: Array<{ hreflang: string, href: string }> },
|
|
409
516
|
* cssEntry: string | null,
|
|
410
517
|
* isErrorDocument?: boolean,
|
|
411
518
|
* }} input
|
|
412
519
|
*/
|
|
413
520
|
function wrapDocument(input) {
|
|
414
521
|
const propsJson = JSON.stringify(input.props ?? {});
|
|
415
|
-
const
|
|
416
|
-
const
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
522
|
+
const localeId = input.meta.lang || 'en';
|
|
523
|
+
const dir = input.meta.dir || 'ltr';
|
|
524
|
+
const native = requireNativeAddon();
|
|
525
|
+
if (typeof native.generatePageShell !== 'function') {
|
|
526
|
+
throw new Error('vmz native addon missing generatePageShell — rebuild with `pnpm napi:build`');
|
|
527
|
+
}
|
|
528
|
+
return native.generatePageShell({
|
|
529
|
+
bodyHtml: input.bodyHtml,
|
|
530
|
+
chunkId: input.chunkId || '',
|
|
531
|
+
layoutChain: input.layoutChain || [],
|
|
532
|
+
propsJson,
|
|
533
|
+
meta: {
|
|
534
|
+
title: input.meta.title,
|
|
535
|
+
description: input.meta.description,
|
|
536
|
+
canonical: input.meta.canonical,
|
|
537
|
+
robots: input.meta.robots,
|
|
538
|
+
lang: localeId,
|
|
539
|
+
dir,
|
|
540
|
+
alternates: input.meta.alternates || [],
|
|
541
|
+
},
|
|
542
|
+
// napi Option<String>: omit/undefined = None; null is rejected as String
|
|
543
|
+
...(input.cssEntry ? { cssEntry: String(input.cssEntry) } : {}),
|
|
544
|
+
isErrorDocument: !!input.isErrorDocument,
|
|
545
|
+
});
|
|
437
546
|
}
|
|
438
547
|
/**
|
|
439
548
|
* @param {string} origin
|
|
440
549
|
* @param {Array<{ canonical: string, robots: string }>} generations
|
|
441
550
|
*/
|
|
442
|
-
function buildSitemap(
|
|
443
|
-
const urls = generations
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
450
|
-
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
451
|
-
${urls}
|
|
452
|
-
</urlset>
|
|
453
|
-
`;
|
|
454
|
-
}
|
|
455
|
-
function escapeHtml(s) {
|
|
456
|
-
return String(s).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
457
|
-
}
|
|
458
|
-
function escapeAttr(s) {
|
|
459
|
-
return escapeHtml(s).replaceAll('"', '"');
|
|
460
|
-
}
|
|
461
|
-
function escapeXml(s) {
|
|
462
|
-
return escapeAttr(s).replaceAll("'", ''');
|
|
551
|
+
function buildSitemap(_origin, generations) {
|
|
552
|
+
const urls = generations.filter((g) => !String(g.robots).includes('noindex')).map((g) => ({ loc: g.canonical }));
|
|
553
|
+
const native = requireNativeAddon();
|
|
554
|
+
if (typeof native.generateSitemapXml !== 'function') {
|
|
555
|
+
throw new Error('vmz native addon missing generateSitemapXml — rebuild with `pnpm napi:build`');
|
|
556
|
+
}
|
|
557
|
+
return native.generateSitemapXml(urls);
|
|
463
558
|
}
|
package/dist/test-cmd.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { createWorkspace } from './index.js';
|
|
8
8
|
import { log } from './log.js';
|
|
9
|
+
import { generatePrettyJson } from './pretty-json.js';
|
|
9
10
|
/**
|
|
10
11
|
* @returns {Promise<typeof import('@vmz/test')>}
|
|
11
12
|
*/
|
|
@@ -317,7 +318,7 @@ export async function cmdTest(args) {
|
|
|
317
318
|
return errors.length ? 2 : 0;
|
|
318
319
|
}
|
|
319
320
|
if (wantJson) {
|
|
320
|
-
const text = `${
|
|
321
|
+
const text = `${generatePrettyJson(report)}\n`;
|
|
321
322
|
if (typeof args.json === 'string' && args.json !== 'true') {
|
|
322
323
|
const { writeFileSync } = await import('node:fs');
|
|
323
324
|
writeFileSync(path.resolve(String(args.json)), text);
|