@vmz/vmz 0.1.11 → 0.1.13
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.js +3 -4
- package/dist/cdn-policy.js +4 -5
- package/dist/cli.js +15 -50
- package/dist/content-addressed-assets.d.ts +2 -8
- package/dist/content-addressed-assets.js +34 -110
- package/dist/delivery-profile.d.ts +1 -11
- package/dist/delivery-profile.js +1 -70
- package/dist/dev-session.d.ts +0 -1
- package/dist/dev-session.js +64 -220
- package/dist/document-build.js +34 -61
- package/dist/document-cmd.js +9 -2
- package/dist/document-enrich.d.ts +1 -2
- package/dist/document-enrich.js +4 -15
- package/dist/document-integrate.js +21 -28
- package/dist/embedded-packaging.js +1 -2
- package/dist/index.d.ts +1 -15
- package/dist/index.js +3 -58
- package/dist/locale-check.js +61 -28
- package/dist/locale-cmd.js +42 -10
- package/dist/locale-route-emit.d.ts +1 -4
- package/dist/locale-route-emit.js +32 -7
- package/dist/locale-router.js +8 -26
- package/dist/pack.js +2 -3
- package/dist/production-observability.js +2 -3
- package/dist/production-test-pack.js +3 -4
- package/dist/release-pack.js +18 -5
- package/dist/server-artifact.js +3 -4
- package/dist/site-delivery.js +3 -4
- package/dist/static-emit.js +78 -116
- package/dist/test-cmd.js +1 -2
- package/package.json +12 -12
- package/dist/dev-watch-roots.d.ts +0 -67
- package/dist/dev-watch-roots.js +0 -220
- package/dist/document-host-chrome.d.ts +0 -28
- package/dist/document-host-chrome.js +0 -128
- package/dist/native-addon.d.ts +0 -9
- package/dist/native-addon.js +0 -84
- package/dist/pretty-json.d.ts +0 -19
- package/dist/pretty-json.js +0 -43
- package/dist/route-path.d.ts +0 -35
- package/dist/route-path.js +0 -77
- package/dist/wechat-packaging.d.ts +0 -22
- package/dist/wechat-packaging.js +0 -59
|
@@ -7,9 +7,34 @@ import fs from 'node:fs';
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { checkLocales, localeHasErrors } from './locale-check.js';
|
|
9
9
|
import { buildLocalePageMeta, buildLocaleRouteRealizationTable } from './locale-router.js';
|
|
10
|
-
import { writePrettyJsonFile } from './pretty-json.js';
|
|
11
|
-
import { listPublicPageUnits, unitBrowserPathPattern } from './route-path.js';
|
|
12
10
|
export const LOCALE_ROUTE_REALIZATION_ARTIFACT_SCHEMA = 'vmz.locale.route_realization.v0';
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} chunkId
|
|
13
|
+
*/
|
|
14
|
+
function pathPatternFromChunk(chunkId) {
|
|
15
|
+
const rel = String(chunkId || '').replace(/^pages\//, '');
|
|
16
|
+
const parts = rel.split('/').filter(Boolean);
|
|
17
|
+
const segs = [];
|
|
18
|
+
for (let i = 0; i < parts.length; i++) {
|
|
19
|
+
const p = parts[i];
|
|
20
|
+
if (p === 'index' && i === parts.length - 1)
|
|
21
|
+
continue;
|
|
22
|
+
segs.push(p);
|
|
23
|
+
}
|
|
24
|
+
return segs.length ? `/${segs.join('/')}` : '/';
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Layouts are not public RouteNodes for locale realization / SEO.
|
|
28
|
+
* @param {string} chunkId
|
|
29
|
+
*/
|
|
30
|
+
function isPublicPageChunk(chunkId) {
|
|
31
|
+
const id = String(chunkId || '');
|
|
32
|
+
if (!id.startsWith('pages/'))
|
|
33
|
+
return false;
|
|
34
|
+
if (/(^|\/)Layout$/.test(id))
|
|
35
|
+
return false;
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
13
38
|
/**
|
|
14
39
|
* @param {string} projectRoot
|
|
15
40
|
* @param {string} distDir
|
|
@@ -34,10 +59,10 @@ export function emitLocaleRouteRealization(projectRoot, distDir, opts = {}) {
|
|
|
34
59
|
};
|
|
35
60
|
}
|
|
36
61
|
const deployment = JSON.parse(fs.readFileSync(deploymentPath, 'utf8'));
|
|
37
|
-
const pages =
|
|
62
|
+
const pages = (deployment.units || []).filter((u) => u.kind === 'page' && isPublicPageChunk(String(u.chunkId)));
|
|
38
63
|
const routes = pages.map((u) => ({
|
|
39
64
|
routeId: String(u.chunkId),
|
|
40
|
-
path:
|
|
65
|
+
path: pathPatternFromChunk(String(u.chunkId)),
|
|
41
66
|
}));
|
|
42
67
|
const localeEntries = report.manifest?.locales || [];
|
|
43
68
|
const locales = localeEntries.map((l) => l.id);
|
|
@@ -92,14 +117,14 @@ export function emitLocaleRouteRealization(projectRoot, distDir, opts = {}) {
|
|
|
92
117
|
const vmzDir = path.join(distDir, '_vmz');
|
|
93
118
|
fs.mkdirSync(vmzDir, { recursive: true });
|
|
94
119
|
const outPath = path.join(vmzDir, 'locale-route-realization.json');
|
|
95
|
-
|
|
120
|
+
fs.writeFileSync(outPath, `${JSON.stringify(artifact, null, 2)}\n`, 'utf8');
|
|
96
121
|
const manifestOut = path.join(vmzDir, 'locale-manifest.json');
|
|
97
|
-
|
|
122
|
+
fs.writeFileSync(manifestOut, `${JSON.stringify({
|
|
98
123
|
schema: 'vmz.locale.manifest.v0',
|
|
99
124
|
defaultLocale,
|
|
100
125
|
locales: artifact.locales,
|
|
101
126
|
routing: artifact.routing,
|
|
102
|
-
});
|
|
127
|
+
}, null, 2)}\n`, 'utf8');
|
|
103
128
|
return {
|
|
104
129
|
ok: true,
|
|
105
130
|
written: ['_vmz/locale-route-realization.json', '_vmz/locale-manifest.json'],
|
package/dist/locale-router.js
CHANGED
|
@@ -90,34 +90,16 @@ export function buildLocaleRouteRealizationTable(input) {
|
|
|
90
90
|
pathPattern: normalizePath(route.path),
|
|
91
91
|
};
|
|
92
92
|
realizations.push(entry);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
severity: 'error',
|
|
101
|
-
message: `path ${entry.path} claimed by ${prevRoute} and ${route.routeId}`,
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
pathOwners.set(entry.path, route.routeId);
|
|
106
|
-
}
|
|
93
|
+
const prev = pathOwners.get(entry.path);
|
|
94
|
+
if (prev && prev !== `${route.routeId}@${localeId}`) {
|
|
95
|
+
diagnostics.push({
|
|
96
|
+
code: DIAG_LOCALE_ROUTE_COLLISION,
|
|
97
|
+
severity: 'error',
|
|
98
|
+
message: `path ${entry.path} claimed by ${prev} and ${route.routeId}@${localeId}`,
|
|
99
|
+
});
|
|
107
100
|
}
|
|
108
101
|
else {
|
|
109
|
-
|
|
110
|
-
const prev = pathOwners.get(entry.path);
|
|
111
|
-
if (prev && prev !== owner) {
|
|
112
|
-
diagnostics.push({
|
|
113
|
-
code: DIAG_LOCALE_ROUTE_COLLISION,
|
|
114
|
-
severity: 'error',
|
|
115
|
-
message: `path ${entry.path} claimed by ${prev} and ${owner}`,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
pathOwners.set(entry.path, owner);
|
|
120
|
-
}
|
|
102
|
+
pathOwners.set(entry.path, `${route.routeId}@${localeId}`);
|
|
121
103
|
}
|
|
122
104
|
}
|
|
123
105
|
}
|
package/dist/pack.js
CHANGED
|
@@ -5,11 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
// @ts-nocheck
|
|
7
7
|
import crypto from 'node:crypto';
|
|
8
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { loadDeploymentIr, planBundleInputs } from './bundler-adapter.js';
|
|
11
11
|
import { packClientBareImports } from './pack-client-packages.js';
|
|
12
|
-
import { writePrettyJsonFile } from './pretty-json.js';
|
|
13
12
|
export const PACK_MANIFEST_SCHEMA = 'vmz.pack.manifest.v0';
|
|
14
13
|
/**
|
|
15
14
|
* Ensure dom split companions sit next to vmz-dom.js (barrel imports ./dom-core.js).
|
|
@@ -101,7 +100,7 @@ export function packFromDeploymentIr(outDir, opts = {}) {
|
|
|
101
100
|
const vmzDir = path.join(outDir, '_vmz');
|
|
102
101
|
mkdirSync(vmzDir, { recursive: true });
|
|
103
102
|
const file = path.join(vmzDir, 'pack-manifest.json');
|
|
104
|
-
|
|
103
|
+
writeFileSync(file, `${JSON.stringify(body, null, 2)}\n`, 'utf8');
|
|
105
104
|
return { manifest: body, path: file, clientPackages };
|
|
106
105
|
}
|
|
107
106
|
function stableStringify(value) {
|
|
@@ -7,7 +7,6 @@ import crypto from 'node:crypto';
|
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { canonicalJson, sha256Hex } from './release-pack.js';
|
|
10
|
-
import { writePrettyJsonFile } from './pretty-json.js';
|
|
11
10
|
export const PRODUCTION_OBSERVABILITY_SCHEMA = 'vmz.production.observability.v0';
|
|
12
11
|
export const PRODUCTION_TRACE_SCHEMA = 'vmz.production.trace.v0';
|
|
13
12
|
/** Facets that production traces must be able to carry (08 A5). */
|
|
@@ -458,8 +457,8 @@ export function emitProductionObservability(distDir, overrides = {}, meta = {})
|
|
|
458
457
|
fs.mkdirSync(vmzDir, { recursive: true });
|
|
459
458
|
const contractPath = path.join(vmzDir, 'production-observability.json');
|
|
460
459
|
const tracePath = path.join(vmzDir, 'production-trace.sample.json');
|
|
461
|
-
|
|
462
|
-
|
|
460
|
+
fs.writeFileSync(contractPath, `${JSON.stringify(contract, null, 2)}\n`, 'utf8');
|
|
461
|
+
fs.writeFileSync(tracePath, `${JSON.stringify(trace, null, 2)}\n`, 'utf8');
|
|
463
462
|
return { contract, trace, contractPath, tracePath };
|
|
464
463
|
}
|
|
465
464
|
export function observabilityDigest(contract) {
|
|
@@ -8,7 +8,6 @@ import crypto from 'node:crypto';
|
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { canonicalJson, sha256Hex } from './release-pack.js';
|
|
11
|
-
import { writePrettyJsonFile } from './pretty-json.js';
|
|
12
11
|
export const PRODUCTION_SCENARIO_PACK_SCHEMA = 'vmz.production.scenario_pack.v0';
|
|
13
12
|
export const PRODUCTION_CI_PROFILE_SCHEMA = 'vmz.production.ci_profile.v0';
|
|
14
13
|
export const PRODUCTION_TEST_REPORT_SCHEMA = 'vmz.production.test_report.v0';
|
|
@@ -432,9 +431,9 @@ export function emitProductionTestArtifacts(root, report, pack, profile) {
|
|
|
432
431
|
const reportPath = path.join(dir, 'report.json');
|
|
433
432
|
const packPath = path.join(dir, 'scenario-pack.json');
|
|
434
433
|
const profilePath = path.join(dir, 'ci-profile.json');
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
434
|
+
fs.writeFileSync(reportPath, `${JSON.stringify(stamped, null, 2)}\n`, 'utf8');
|
|
435
|
+
fs.writeFileSync(packPath, `${JSON.stringify(pack, null, 2)}\n`, 'utf8');
|
|
436
|
+
fs.writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)}\n`, 'utf8');
|
|
438
437
|
return { reportPath, packPath, profilePath, report: stamped };
|
|
439
438
|
}
|
|
440
439
|
/** Assert CI profile forbids JS test-runner disguise. */
|
package/dist/release-pack.js
CHANGED
|
@@ -9,8 +9,6 @@
|
|
|
9
9
|
import crypto from 'node:crypto';
|
|
10
10
|
import fs from 'node:fs';
|
|
11
11
|
import path from 'node:path';
|
|
12
|
-
import { writePrettyJsonFile } from './pretty-json.js';
|
|
13
|
-
import { listPublicPageUnits, unitBrowserPathPattern } from './route-path.js';
|
|
14
12
|
export const RELEASE_ENVELOPE_SCHEMA = 'vmz.release.envelope.v0';
|
|
15
13
|
export const APPLICATION_ARTIFACT_SCHEMA = 'vmz.application.artifact.v0';
|
|
16
14
|
export const DELIVERY_ARTIFACT_MANIFEST_SCHEMA = 'vmz.profile.delivery_artifact_manifest.v0';
|
|
@@ -90,6 +88,21 @@ function listContentFiles(distDir) {
|
|
|
90
88
|
out.sort();
|
|
91
89
|
return out;
|
|
92
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* @param {string} chunkId
|
|
93
|
+
*/
|
|
94
|
+
function pathPatternFromChunk(chunkId) {
|
|
95
|
+
const rel = chunkId.replace(/^pages\//, '');
|
|
96
|
+
const parts = rel.split('/').filter(Boolean);
|
|
97
|
+
const segs = [];
|
|
98
|
+
for (let i = 0; i < parts.length; i++) {
|
|
99
|
+
const p = parts[i];
|
|
100
|
+
if (p === 'index' && i === parts.length - 1)
|
|
101
|
+
continue;
|
|
102
|
+
segs.push(p);
|
|
103
|
+
}
|
|
104
|
+
return segs.length ? `/${segs.join('/')}` : '/';
|
|
105
|
+
}
|
|
93
106
|
/**
|
|
94
107
|
* Pack `dist/` into `_vmz` manifests + release envelope (filesystem Delivery Profile).
|
|
95
108
|
* @param {string} distDir
|
|
@@ -111,13 +124,13 @@ export function packRelease(distDir, opts = {}) {
|
|
|
111
124
|
for (const rel of files) {
|
|
112
125
|
fileDigests[rel] = sha256File(path.join(abs, ...rel.split('/')));
|
|
113
126
|
}
|
|
114
|
-
const pages =
|
|
127
|
+
const pages = (deployment.units || []).filter((u) => u.kind === 'page');
|
|
115
128
|
const routeRealization = {
|
|
116
129
|
schema: ROUTE_REALIZATION_TABLE_SCHEMA,
|
|
117
130
|
routes: pages.map((u) => ({
|
|
118
131
|
routeId: String(u.chunkId),
|
|
119
132
|
chunkId: String(u.chunkId),
|
|
120
|
-
pathPattern:
|
|
133
|
+
pathPattern: pathPatternFromChunk(String(u.chunkId)),
|
|
121
134
|
clientEntry: u.clientEntry || null,
|
|
122
135
|
programIr: u.programIr || null,
|
|
123
136
|
})),
|
|
@@ -187,7 +200,7 @@ export function packRelease(distDir, opts = {}) {
|
|
|
187
200
|
* @param {unknown} value
|
|
188
201
|
*/
|
|
189
202
|
function writeJson(file, value) {
|
|
190
|
-
|
|
203
|
+
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
191
204
|
}
|
|
192
205
|
/**
|
|
193
206
|
* @param {string} pointerPath
|
package/dist/server-artifact.js
CHANGED
|
@@ -4,10 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
// @ts-nocheck
|
|
6
6
|
import crypto from 'node:crypto';
|
|
7
|
-
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { SERVER_RUNTIMES } from './delivery-profile.js';
|
|
10
|
-
import { writePrettyJsonFile } from './pretty-json.js';
|
|
11
10
|
export const SERVER_ARTIFACT_SCHEMA = 'vmz.server.artifact.v0';
|
|
12
11
|
export const HTTP_CONTRACT_SCHEMA = 'vmz.http.contract.v0';
|
|
13
12
|
export const SERVER_RUNTIME_ADAPTER_SCHEMA = 'vmz.server.runtime_adapter.v0';
|
|
@@ -117,14 +116,14 @@ export function emitServerArtifact(outDir, opts = {}) {
|
|
|
117
116
|
const vmzDir = path.join(outDir, '_vmz');
|
|
118
117
|
mkdirSync(vmzDir, { recursive: true });
|
|
119
118
|
const file = path.join(vmzDir, 'server-artifact.json');
|
|
120
|
-
|
|
119
|
+
writeFileSync(file, `${JSON.stringify(artifact, null, 2)}\n`, 'utf8');
|
|
121
120
|
const adapterDir = path.join(vmzDir, 'adapters');
|
|
122
121
|
mkdirSync(adapterDir, { recursive: true });
|
|
123
122
|
for (const adapterId of ['worker', 'rust-host']) {
|
|
124
123
|
const projection = projectServerRuntimeAdapter(artifact, adapterId);
|
|
125
124
|
const dir = path.join(adapterDir, adapterId);
|
|
126
125
|
mkdirSync(dir, { recursive: true });
|
|
127
|
-
|
|
126
|
+
writeFileSync(path.join(dir, 'adapter.json'), `${JSON.stringify(projection, null, 2)}\n`, 'utf8');
|
|
128
127
|
}
|
|
129
128
|
return { artifact, path: file, httpContractDigest };
|
|
130
129
|
}
|
package/dist/site-delivery.js
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
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';
|
|
10
9
|
export const SITE_DELIVERY_CONTRACT_SCHEMA = 'vmz.site.delivery_contract.v0';
|
|
11
10
|
export const SITE_DELIVERY_RESOLUTION_SCHEMA = 'vmz.site.delivery_resolution.v0';
|
|
12
11
|
/**
|
|
@@ -136,7 +135,7 @@ export function normalizeSiteDelivery(raw, opts = {}) {
|
|
|
136
135
|
artifact: String(d.artifact),
|
|
137
136
|
expectedCompatibility: d.expectedCompatibility || {
|
|
138
137
|
runtime: 'vmz',
|
|
139
|
-
deliveryProfiles: ['static', 'filesystem'],
|
|
138
|
+
deliveryProfiles: ['web-static', 'filesystem'],
|
|
140
139
|
},
|
|
141
140
|
sources,
|
|
142
141
|
resolutionPolicy: {
|
|
@@ -319,11 +318,11 @@ export function emitSiteDelivery(outDir, deliveryRaw, opts = {}) {
|
|
|
319
318
|
}
|
|
320
319
|
const vmzDir = path.join(outDir, '_vmz');
|
|
321
320
|
fs.mkdirSync(vmzDir, { recursive: true });
|
|
322
|
-
|
|
321
|
+
fs.writeFileSync(path.join(vmzDir, 'site-delivery-contract.json'), `${JSON.stringify(norm.contract, null, 2)}\n`, 'utf8');
|
|
323
322
|
let resolution = null;
|
|
324
323
|
if (opts.probes) {
|
|
325
324
|
resolution = resolveSiteRelease(norm.contract, opts.probes);
|
|
326
|
-
|
|
325
|
+
fs.writeFileSync(path.join(vmzDir, 'site-delivery-resolution.json'), `${JSON.stringify(resolution, null, 2)}\n`, 'utf8');
|
|
327
326
|
}
|
|
328
327
|
return { contract: norm.contract, resolution };
|
|
329
328
|
}
|
package/dist/static-emit.js
CHANGED
|
@@ -7,14 +7,9 @@ import crypto from 'node:crypto';
|
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { pathToFileURL } from 'node:url';
|
|
10
|
-
import { createRenderHost } from '@vmz/core/render-host';
|
|
11
|
-
import { listClientComponentsSync } from '@vmz/core/component-registry';
|
|
12
10
|
import { emitCdnPolicy } from './cdn-policy.js';
|
|
13
11
|
import { emitContentAddressedAssets } from './content-addressed-assets.js';
|
|
14
12
|
import { absoluteUrl, buildLocalePageMeta, localizeBodyLinks } from './locale-router.js';
|
|
15
|
-
import { requireNativeAddon } from './native-addon.js';
|
|
16
|
-
import { writePrettyJsonFile } from './pretty-json.js';
|
|
17
|
-
import { filePathPatternFromChunk, isRouteBoundaryStem, listPublicPageUnits, parsePathPattern, unitBrowserPathPattern } from './route-path.js';
|
|
18
13
|
export const STATIC_DELIVERY_MANIFEST_SCHEMA = 'vmz.static.delivery_manifest.v0';
|
|
19
14
|
/**
|
|
20
15
|
* @param {string} distDir
|
|
@@ -31,8 +26,7 @@ export async function emitWebStatic(distDir, opts = {}) {
|
|
|
31
26
|
if (!fs.existsSync(domPath)) {
|
|
32
27
|
throw new Error(`emitWebStatic: missing ${domPath} — run vmz build first`);
|
|
33
28
|
}
|
|
34
|
-
const
|
|
35
|
-
const { renderToString, renderToStream } = host;
|
|
29
|
+
const { renderToString, renderToStream } = await import(pathToFileURL(domPath).href);
|
|
36
30
|
const pageCatalog = listPageClientFiles(distDir);
|
|
37
31
|
/** @type {Array<{
|
|
38
32
|
* routeId: string,
|
|
@@ -49,7 +43,7 @@ export async function emitWebStatic(distDir, opts = {}) {
|
|
|
49
43
|
/** @type {Array<{ routeId: string, path: string, chunkId: string, classification: string, reason: string }>} */
|
|
50
44
|
const skipped = [];
|
|
51
45
|
for (const page of pageCatalog) {
|
|
52
|
-
const pattern =
|
|
46
|
+
const pattern = patternFromSegs(page.segs);
|
|
53
47
|
const routeId = guessRouteId(distDir, page.chunkId);
|
|
54
48
|
if (page.segs.some((s) => s.kind === 'param' || s.kind === 'catch')) {
|
|
55
49
|
skipped.push({
|
|
@@ -101,7 +95,6 @@ export async function emitWebStatic(distDir, opts = {}) {
|
|
|
101
95
|
}
|
|
102
96
|
const meta = await resolvePageMeta(Page, { params, props, pathname: pattern, origin });
|
|
103
97
|
const layoutChain = resolveLayoutChain(distDir, page.chunkId);
|
|
104
|
-
await host.ensureComponents([page.chunkId, ...layoutChain]);
|
|
105
98
|
let bodyHtml = '';
|
|
106
99
|
for await (const chunk of renderToStream(Page, props, {})) {
|
|
107
100
|
bodyHtml += chunk;
|
|
@@ -169,11 +162,7 @@ export async function emitWebStatic(distDir, opts = {}) {
|
|
|
169
162
|
fs.writeFileSync(path.join(distDir, '404.html'), notFoundHtml, 'utf8');
|
|
170
163
|
const sitemap = buildSitemap(origin, generations);
|
|
171
164
|
fs.writeFileSync(path.join(distDir, 'sitemap.xml'), sitemap, 'utf8');
|
|
172
|
-
const
|
|
173
|
-
if (typeof nativeRobots.generateRobotsTxt !== 'function') {
|
|
174
|
-
throw new Error('vmz native addon missing generateRobotsTxt — rebuild with `pnpm napi:build`');
|
|
175
|
-
}
|
|
176
|
-
const robots = nativeRobots.generateRobotsTxt(`${origin}/sitemap.xml`);
|
|
165
|
+
const robots = `User-agent: *\nAllow: /\nDisallow: /404\nSitemap: ${origin}/sitemap.xml\n`;
|
|
177
166
|
fs.writeFileSync(path.join(distDir, 'robots.txt'), robots, 'utf8');
|
|
178
167
|
// Hard rule: no SPA fallback shim in artifact.
|
|
179
168
|
for (const bad of ['_redirects', 'vercel.json', 'netlify.toml']) {
|
|
@@ -190,7 +179,7 @@ export async function emitWebStatic(distDir, opts = {}) {
|
|
|
190
179
|
const manifest = {
|
|
191
180
|
schema: STATIC_DELIVERY_MANIFEST_SCHEMA,
|
|
192
181
|
applicationId,
|
|
193
|
-
deliveryProfile: 'static',
|
|
182
|
+
deliveryProfile: 'web-static',
|
|
194
183
|
origin,
|
|
195
184
|
generatedAt: new Date().toISOString(),
|
|
196
185
|
spaFallback: false,
|
|
@@ -218,8 +207,7 @@ export async function emitWebStatic(distDir, opts = {}) {
|
|
|
218
207
|
};
|
|
219
208
|
const digest = sha256Hex(canonicalJson(manifest));
|
|
220
209
|
manifest.manifestDigest = digest;
|
|
221
|
-
|
|
222
|
-
emitStaticClientEntries(distDir, pageCatalog);
|
|
210
|
+
fs.writeFileSync(path.join(vmzDir, 'static-delivery-manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
223
211
|
const assets = emitContentAddressedAssets(distDir);
|
|
224
212
|
manifest.contentAddressedAssets = {
|
|
225
213
|
schema: assets.manifest.schema,
|
|
@@ -230,7 +218,7 @@ export async function emitWebStatic(distDir, opts = {}) {
|
|
|
230
218
|
// Re-stamp static manifest after linking asset digest (HTML already rewritten on disk).
|
|
231
219
|
delete manifest.manifestDigest;
|
|
232
220
|
manifest.manifestDigest = sha256Hex(canonicalJson(manifest));
|
|
233
|
-
|
|
221
|
+
fs.writeFileSync(path.join(vmzDir, 'static-delivery-manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
234
222
|
const cdn = emitCdnPolicy(distDir, manifest);
|
|
235
223
|
return {
|
|
236
224
|
manifest,
|
|
@@ -270,11 +258,8 @@ function sortKeys(value) {
|
|
|
270
258
|
* @param {string} distDir
|
|
271
259
|
*/
|
|
272
260
|
function listPageClientFiles(distDir) {
|
|
273
|
-
const fromDep = listPagesFromDeployment(distDir);
|
|
274
|
-
if (fromDep.length)
|
|
275
|
-
return fromDep;
|
|
276
261
|
const root = path.join(distDir, 'pages');
|
|
277
|
-
/** @type {Array<{ chunkId: string, segs: ReturnType<typeof
|
|
262
|
+
/** @type {Array<{ chunkId: string, segs: ReturnType<typeof parseChunkSegments> }>} */
|
|
278
263
|
const out = [];
|
|
279
264
|
function walk(abs, relParts) {
|
|
280
265
|
let ents;
|
|
@@ -289,11 +274,10 @@ function listPageClientFiles(distDir) {
|
|
|
289
274
|
walk(path.join(abs, e.name), [...relParts, e.name]);
|
|
290
275
|
else if (e.isFile() && e.name.endsWith('.client.js')) {
|
|
291
276
|
const stem = e.name.replace(/\.client\.js$/, '');
|
|
292
|
-
if (
|
|
277
|
+
if (stem === 'Layout' || stem === 'Loading' || stem === 'Error' || stem === 'NotFound')
|
|
293
278
|
continue;
|
|
294
279
|
const chunkId = ['pages', ...relParts, stem].join('/');
|
|
295
|
-
|
|
296
|
-
out.push({ chunkId, pathPattern, segs: parsePathPattern(pathPattern) });
|
|
280
|
+
out.push({ chunkId, segs: parseChunkSegments(chunkId) });
|
|
297
281
|
}
|
|
298
282
|
}
|
|
299
283
|
}
|
|
@@ -301,26 +285,32 @@ function listPageClientFiles(distDir) {
|
|
|
301
285
|
return out;
|
|
302
286
|
}
|
|
303
287
|
/**
|
|
304
|
-
* @param {string}
|
|
288
|
+
* @param {string} chunkId
|
|
305
289
|
*/
|
|
306
|
-
function
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
290
|
+
function parseChunkSegments(chunkId) {
|
|
291
|
+
const rel = chunkId.replace(/^pages\//, '');
|
|
292
|
+
const parts = rel.split('/').filter(Boolean);
|
|
293
|
+
/** @type {Array<{ kind: 'static' | 'param' | 'catch', value?: string, name?: string }>} */
|
|
294
|
+
const segs = [];
|
|
295
|
+
for (let i = 0; i < parts.length; i++) {
|
|
296
|
+
const p = parts[i];
|
|
297
|
+
if (p.startsWith('(') && p.endsWith(')') && p.length > 2)
|
|
298
|
+
continue;
|
|
299
|
+
if (p === 'index' && i === parts.length - 1)
|
|
300
|
+
continue;
|
|
301
|
+
const catchAll = /^\[\.\.\.([^\]]+)\]$/.exec(p);
|
|
302
|
+
const param = /^\[([^\]]+)\]$/.exec(p);
|
|
303
|
+
if (catchAll)
|
|
304
|
+
segs.push({ kind: 'catch', name: catchAll[1] });
|
|
305
|
+
else if (param)
|
|
306
|
+
segs.push({ kind: 'param', name: param[1] });
|
|
307
|
+
else
|
|
308
|
+
segs.push({ kind: 'static', value: p.toLowerCase() });
|
|
320
309
|
}
|
|
310
|
+
return segs;
|
|
321
311
|
}
|
|
322
312
|
/**
|
|
323
|
-
* @param {ReturnType<typeof
|
|
313
|
+
* @param {ReturnType<typeof parseChunkSegments>} segs
|
|
324
314
|
*/
|
|
325
315
|
function patternFromSegs(segs) {
|
|
326
316
|
if (!segs.length)
|
|
@@ -399,8 +389,8 @@ async function resolvePageMeta(Page, ctx) {
|
|
|
399
389
|
else if (Page.meta && typeof Page.meta === 'object') {
|
|
400
390
|
raw = Page.meta;
|
|
401
391
|
}
|
|
402
|
-
const title = String(raw.title || guessTitle(ctx.pathname)
|
|
403
|
-
const description = String(raw.description ||
|
|
392
|
+
const title = String(raw.title || `${guessTitle(ctx.pathname)} · VMZ`);
|
|
393
|
+
const description = String(raw.description || `VMZ page ${ctx.pathname}`);
|
|
404
394
|
const canonical = String(raw.canonical || `${ctx.origin}${ctx.pathname === '/' ? '/' : ctx.pathname}`);
|
|
405
395
|
const robots = String(raw.robots || 'index,follow');
|
|
406
396
|
const lang = String(raw.lang || 'en');
|
|
@@ -524,87 +514,59 @@ function readCssEntry(distDir) {
|
|
|
524
514
|
*/
|
|
525
515
|
function wrapDocument(input) {
|
|
526
516
|
const propsJson = JSON.stringify(input.props ?? {});
|
|
517
|
+
const layoutAttr = input.layoutChain.length ? ` data-vmz-layout="${escapeAttr(input.layoutChain.join(','))}"` : '';
|
|
518
|
+
const pageAttr = input.chunkId ? ` data-vmz-page="${escapeAttr(input.chunkId)}"` : '';
|
|
527
519
|
const localeId = input.meta.lang || 'en';
|
|
528
520
|
const dir = input.meta.dir || 'ltr';
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
521
|
+
const localeAttr = ` data-vmz-locale="${escapeAttr(localeId)}" data-vmz-dir="${escapeAttr(dir)}"`;
|
|
522
|
+
const cssLink = input.cssEntry ? ` <link rel="stylesheet" href="/${String(input.cssEntry).replace(/^\/+/, '')}" />\n` : '';
|
|
523
|
+
const entry = input.isErrorDocument ? '' : ` <script type="module" src="/entry-client.js"></script>\n`;
|
|
524
|
+
const hreflang = (input.meta.alternates || [])
|
|
525
|
+
.map((a) => ` <link rel="alternate" hreflang="${escapeAttr(a.hreflang)}" href="${escapeAttr(a.href)}" />`)
|
|
526
|
+
.join('\n');
|
|
527
|
+
const hreflangBlock = hreflang ? `${hreflang}\n` : '';
|
|
528
|
+
return `<!DOCTYPE html>
|
|
529
|
+
<html lang="${escapeAttr(localeId)}" data-locale="${escapeAttr(localeId)}" dir="${escapeAttr(dir)}">
|
|
530
|
+
<head>
|
|
531
|
+
<meta charset="utf-8" />
|
|
532
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
533
|
+
<title>${escapeHtml(input.meta.title)}</title>
|
|
534
|
+
<meta name="description" content="${escapeAttr(input.meta.description)}" />
|
|
535
|
+
<meta name="robots" content="${escapeAttr(input.meta.robots)}" />
|
|
536
|
+
<link rel="canonical" href="${escapeAttr(input.meta.canonical)}" />
|
|
537
|
+
${hreflangBlock} <meta property="og:title" content="${escapeAttr(input.meta.title)}" />
|
|
538
|
+
<meta property="og:description" content="${escapeAttr(input.meta.description)}" />
|
|
539
|
+
<meta property="og:url" content="${escapeAttr(input.meta.canonical)}" />
|
|
540
|
+
${cssLink}</head>
|
|
541
|
+
<body>
|
|
542
|
+
<div id="app"${pageAttr}${layoutAttr}${localeAttr} data-vmz-props="${escapeAttr(propsJson)}">${input.bodyHtml}</div>
|
|
543
|
+
${entry}</body>
|
|
544
|
+
</html>
|
|
545
|
+
`;
|
|
551
546
|
}
|
|
552
547
|
/**
|
|
553
548
|
* @param {string} origin
|
|
554
549
|
* @param {Array<{ canonical: string, robots: string }>} generations
|
|
555
550
|
*/
|
|
556
|
-
function buildSitemap(
|
|
557
|
-
const urls = generations
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
551
|
+
function buildSitemap(origin, generations) {
|
|
552
|
+
const urls = generations
|
|
553
|
+
.filter((g) => !String(g.robots).includes('noindex'))
|
|
554
|
+
.map((g) => ` <url>
|
|
555
|
+
<loc>${escapeXml(g.canonical)}</loc>
|
|
556
|
+
</url>`)
|
|
557
|
+
.join('\n');
|
|
558
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
559
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
560
|
+
${urls}
|
|
561
|
+
</urlset>
|
|
562
|
+
`;
|
|
563
563
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
* @param {string} distDir
|
|
567
|
-
* @param {Array<{ chunkId: string }>} pageCatalog
|
|
568
|
-
*/
|
|
569
|
-
function emitStaticClientEntries(distDir, pageCatalog) {
|
|
570
|
-
const componentEntries = listClientComponentsSync(distDir, { strict: true });
|
|
571
|
-
const indexChunk = pageCatalog.find((p) => p.chunkId === 'pages/index')?.chunkId || pageCatalog[0]?.chunkId || 'pages/index';
|
|
572
|
-
const resumeEntries = loadPageResumeEntriesSync(distDir, indexChunk);
|
|
573
|
-
const lazySet = new Set(resumeEntries
|
|
574
|
-
.filter((e) => isEventResumeStrategy(e.strategy))
|
|
575
|
-
.map((e) => e.component)
|
|
576
|
-
.filter(Boolean));
|
|
577
|
-
const eager = componentEntries.filter((e) => !lazySet.has(e.name));
|
|
578
|
-
const lazy = componentEntries.filter((e) => lazySet.has(e.name));
|
|
579
|
-
const native = requireNativeAddon();
|
|
580
|
-
if (typeof native.generateServeEntryClient !== 'function') {
|
|
581
|
-
throw new Error('vmz native addon missing generateServeEntryClient — rebuild with `pnpm napi:build`');
|
|
582
|
-
}
|
|
583
|
-
fs.writeFileSync(path.join(distDir, 'entry-client.js'), native.generateServeEntryClient(eager, lazy, ''), 'utf8');
|
|
584
|
-
if (typeof native.generateServeEntryEvent === 'function') {
|
|
585
|
-
fs.writeFileSync(path.join(distDir, 'entry-event.js'), native.generateServeEntryEvent(''), 'utf8');
|
|
586
|
-
}
|
|
564
|
+
function escapeHtml(s) {
|
|
565
|
+
return String(s).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
587
566
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
* @param {string} chunkId
|
|
591
|
-
*/
|
|
592
|
-
function loadPageResumeEntriesSync(distDir, chunkId) {
|
|
593
|
-
try {
|
|
594
|
-
const dep = JSON.parse(fs.readFileSync(path.join(distDir, 'vmz-deployment.json'), 'utf8'));
|
|
595
|
-
const units = Array.isArray(dep.units) ? dep.units : [];
|
|
596
|
-
const page = units.find((u) => u.chunkId === chunkId) || units.find((u) => u.chunkId === 'pages/index') || units.find((u) => u.kind === 'page');
|
|
597
|
-
const entries = Array.isArray(page?.resumeEntries) ? page.resumeEntries : [];
|
|
598
|
-
return entries.map((e) => ({
|
|
599
|
-
component: String(e.component || ''),
|
|
600
|
-
strategy: String(e.strategy || ''),
|
|
601
|
-
}));
|
|
602
|
-
}
|
|
603
|
-
catch {
|
|
604
|
-
return [];
|
|
605
|
-
}
|
|
567
|
+
function escapeAttr(s) {
|
|
568
|
+
return escapeHtml(s).replaceAll('"', '"');
|
|
606
569
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
return strategy === 'event' || strategy === 'click' || String(strategy).startsWith('event:');
|
|
570
|
+
function escapeXml(s) {
|
|
571
|
+
return escapeAttr(s).replaceAll("'", ''');
|
|
610
572
|
}
|
package/dist/test-cmd.js
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
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';
|
|
10
9
|
/**
|
|
11
10
|
* @returns {Promise<typeof import('@vmz/test')>}
|
|
12
11
|
*/
|
|
@@ -318,7 +317,7 @@ export async function cmdTest(args) {
|
|
|
318
317
|
return errors.length ? 2 : 0;
|
|
319
318
|
}
|
|
320
319
|
if (wantJson) {
|
|
321
|
-
const text = `${
|
|
320
|
+
const text = `${JSON.stringify(report, null, 2)}\n`;
|
|
322
321
|
if (typeof args.json === 'string' && args.json !== 'true') {
|
|
323
322
|
const { writeFileSync } = await import('node:fs');
|
|
324
323
|
writeFileSync(path.resolve(String(args.json)), text);
|