@vmz/vmz 0.0.4 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-assemble.d.ts +52 -0
- package/dist/build-assemble.js +191 -0
- package/dist/cdn-policy.d.ts +22 -4
- package/dist/cdn-policy.js +109 -10
- package/dist/cli.js +106 -24
- package/dist/delivery-profile.d.ts +74 -0
- package/dist/delivery-profile.js +279 -0
- package/dist/dev-session.js +2 -1
- package/dist/document-build.js +9 -4
- package/dist/document-enrich.js +8 -0
- package/dist/embedded-packaging.d.ts +22 -0
- package/dist/embedded-packaging.js +113 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +8 -1
- package/dist/locale-check.js +11 -8
- package/dist/locale-cmd.js +2 -2
- package/dist/locale-route-emit.d.ts +34 -0
- package/dist/locale-route-emit.js +134 -0
- package/dist/locale-router.d.ts +20 -0
- package/dist/locale-router.js +68 -0
- package/dist/log.d.ts +2 -2
- package/dist/log.js +11 -3
- package/dist/pack.d.ts +40 -0
- package/dist/pack.js +108 -0
- package/dist/plugin-host.d.ts +1 -1
- package/dist/plugin-host.js +2 -2
- package/dist/production-test-pack.d.ts +0 -14
- package/dist/production-test-pack.js +23 -28
- package/dist/release-pack.js +12 -3
- package/dist/server-artifact.d.ts +140 -0
- package/dist/server-artifact.js +205 -0
- package/dist/server-language-backend.d.ts +89 -0
- package/dist/server-language-backend.js +121 -0
- package/dist/static-emit.d.ts +10 -1
- package/dist/static-emit.js +140 -26
- package/package.json +12 -12
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B5 Assemble dispatch + B6 build-proof (per-build semantic id slots).
|
|
3
|
+
*/
|
|
4
|
+
export declare const BUILD_PROOF_SCHEMA = "vmz.build.proof.v0";
|
|
5
|
+
export declare const ASSEMBLE_MANIFEST_SCHEMA = "vmz.assemble.manifest.v0";
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} outDir
|
|
8
|
+
* @param {any} ctx
|
|
9
|
+
*/
|
|
10
|
+
export declare function assembleDelivery(outDir: any, ctx: any): Promise<{
|
|
11
|
+
manifest: {
|
|
12
|
+
schema: string;
|
|
13
|
+
profileId: any;
|
|
14
|
+
assembly: any;
|
|
15
|
+
serverRuntime: any;
|
|
16
|
+
steps: any[];
|
|
17
|
+
};
|
|
18
|
+
path: string;
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} outDir
|
|
22
|
+
* @param {any} ctx
|
|
23
|
+
*/
|
|
24
|
+
export declare function emitBuildProof(outDir: any, ctx: any): {
|
|
25
|
+
proof: {
|
|
26
|
+
schema: string;
|
|
27
|
+
profileId: any;
|
|
28
|
+
assembly: any;
|
|
29
|
+
selectionDigest: any;
|
|
30
|
+
packDigest: any;
|
|
31
|
+
assembleDigest: any;
|
|
32
|
+
release: boolean;
|
|
33
|
+
semanticIds: string[];
|
|
34
|
+
slots: {
|
|
35
|
+
'server-host': {
|
|
36
|
+
status: string;
|
|
37
|
+
};
|
|
38
|
+
'static-delivery': {
|
|
39
|
+
status: string;
|
|
40
|
+
};
|
|
41
|
+
'site-fallback': {
|
|
42
|
+
status: string;
|
|
43
|
+
};
|
|
44
|
+
'asset-graph': {
|
|
45
|
+
status: string;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
productionReadyClaim: boolean;
|
|
49
|
+
note: string;
|
|
50
|
+
};
|
|
51
|
+
path: string;
|
|
52
|
+
};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B5 Assemble dispatch + B6 build-proof (per-build semantic id slots).
|
|
3
|
+
*/
|
|
4
|
+
// @ts-nocheck
|
|
5
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { semanticIdsForAssembly, sha256Hex, canonicalJson } from './delivery-profile.js';
|
|
8
|
+
import { emitServerArtifact } from './server-artifact.js';
|
|
9
|
+
import { emitEmbeddedPackaging } from './embedded-packaging.js';
|
|
10
|
+
import { emitSiteDelivery } from './site-delivery.js';
|
|
11
|
+
import { emitWebStatic } from './static-emit.js';
|
|
12
|
+
export const BUILD_PROOF_SCHEMA = 'vmz.build.proof.v0';
|
|
13
|
+
export const ASSEMBLE_MANIFEST_SCHEMA = 'vmz.assemble.manifest.v0';
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} outDir
|
|
16
|
+
* @param {any} ctx
|
|
17
|
+
*/
|
|
18
|
+
export async function assembleDelivery(outDir, ctx) {
|
|
19
|
+
const { selection, profile } = ctx;
|
|
20
|
+
const assembly = selection.assembly;
|
|
21
|
+
const result = {
|
|
22
|
+
schema: ASSEMBLE_MANIFEST_SCHEMA,
|
|
23
|
+
profileId: selection.profileId,
|
|
24
|
+
assembly,
|
|
25
|
+
serverRuntime: selection.serverRuntime || null,
|
|
26
|
+
steps: [],
|
|
27
|
+
};
|
|
28
|
+
if (assembly === 'static-cdn' || assembly === 'cdn+server') {
|
|
29
|
+
const staticResult = await emitWebStatic(outDir, { origin: ctx.origin });
|
|
30
|
+
result.steps.push({
|
|
31
|
+
kind: 'static-cdn',
|
|
32
|
+
digest: staticResult.digest,
|
|
33
|
+
htmlFiles: staticResult.htmlFiles?.length ?? 0,
|
|
34
|
+
skipped: staticResult.skipped?.length ?? 0,
|
|
35
|
+
});
|
|
36
|
+
result.staticDelivery = {
|
|
37
|
+
digest: staticResult.digest,
|
|
38
|
+
htmlFiles: staticResult.htmlFiles,
|
|
39
|
+
skipped: staticResult.skipped,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (assembly === 'local-static') {
|
|
43
|
+
result.steps.push({
|
|
44
|
+
kind: 'local-static',
|
|
45
|
+
status: 'modules-ready',
|
|
46
|
+
note: 'client modules packed; no ServerArtifact',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (assembly === 'server-host' || assembly === 'cdn+server') {
|
|
50
|
+
const server = emitServerArtifact(outDir, {
|
|
51
|
+
profileId: selection.profileId,
|
|
52
|
+
assembly,
|
|
53
|
+
serverRuntime: selection.serverRuntime || 'node',
|
|
54
|
+
packDigest: ctx.pack?.packDigest || null,
|
|
55
|
+
});
|
|
56
|
+
result.steps.push({
|
|
57
|
+
kind: 'server-host',
|
|
58
|
+
status: 'emitted',
|
|
59
|
+
digest: server.artifact.artifactDigest,
|
|
60
|
+
publicRoutes: server.artifact.publicRoutes?.length ?? 0,
|
|
61
|
+
internalCapabilities: server.artifact.internalCapabilities?.length ?? 0,
|
|
62
|
+
httpContractDigest: server.httpContractDigest,
|
|
63
|
+
});
|
|
64
|
+
result.serverArtifact = {
|
|
65
|
+
digest: server.artifact.artifactDigest,
|
|
66
|
+
httpContractDigest: server.httpContractDigest,
|
|
67
|
+
schema: server.artifact.schema,
|
|
68
|
+
selectedRuntime: server.artifact.selectedRuntime,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const siteAuthoring = profile.sources || null;
|
|
72
|
+
if (siteAuthoring || assembly === 'rust-embedded') {
|
|
73
|
+
if (!siteAuthoring) {
|
|
74
|
+
throw new Error('rust-embedded requires delivery sources (SiteDeliveryContract); cannot assemble without embedded|filesystem|remote baselines');
|
|
75
|
+
}
|
|
76
|
+
const site = emitSiteDelivery(outDir, siteAuthoring, {
|
|
77
|
+
siteId: ctx.siteId,
|
|
78
|
+
});
|
|
79
|
+
result.steps.push({
|
|
80
|
+
kind: 'site-delivery',
|
|
81
|
+
digest: site.contract.contractDigest,
|
|
82
|
+
});
|
|
83
|
+
result.siteDelivery = {
|
|
84
|
+
digest: site.contract.contractDigest,
|
|
85
|
+
schema: site.contract.schema,
|
|
86
|
+
};
|
|
87
|
+
if (assembly === 'rust-embedded') {
|
|
88
|
+
const pack = emitEmbeddedPackaging(outDir, {
|
|
89
|
+
siteId: ctx.siteId,
|
|
90
|
+
contractDigest: site.contract.contractDigest,
|
|
91
|
+
});
|
|
92
|
+
result.steps.push({
|
|
93
|
+
kind: 'embedded-packaging',
|
|
94
|
+
digest: pack.index.indexDigest,
|
|
95
|
+
objectCount: pack.index.objectCount,
|
|
96
|
+
});
|
|
97
|
+
result.embeddedPackaging = {
|
|
98
|
+
digest: pack.index.indexDigest,
|
|
99
|
+
objectCount: pack.index.objectCount,
|
|
100
|
+
schema: pack.index.schema,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
result.packDigest = ctx.pack?.packDigest || null;
|
|
105
|
+
result.assembleDigest = sha256Hex(canonicalJson({ ...result, assembleDigest: undefined }));
|
|
106
|
+
const vmzDir = path.join(outDir, '_vmz');
|
|
107
|
+
mkdirSync(vmzDir, { recursive: true });
|
|
108
|
+
const file = path.join(vmzDir, 'assemble-manifest.json');
|
|
109
|
+
writeFileSync(file, `${JSON.stringify(result, null, 2)}\n`, 'utf8');
|
|
110
|
+
return { manifest: result, path: file };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* @param {string} outDir
|
|
114
|
+
* @param {any} ctx
|
|
115
|
+
*/
|
|
116
|
+
export function emitBuildProof(outDir, ctx) {
|
|
117
|
+
const semanticIds = semanticIdsForAssembly(ctx.selection.assembly);
|
|
118
|
+
const slots = {
|
|
119
|
+
'server-host': { status: 'not-applicable' },
|
|
120
|
+
'static-delivery': { status: 'not-applicable' },
|
|
121
|
+
'site-fallback': { status: 'not-applicable' },
|
|
122
|
+
'asset-graph': { status: 'not-applicable' },
|
|
123
|
+
};
|
|
124
|
+
for (const id of semanticIds) {
|
|
125
|
+
if (id === 'static-delivery') {
|
|
126
|
+
const step = (ctx.assemble?.steps || []).find((s) => s.kind === 'static-cdn');
|
|
127
|
+
slots[id] = step
|
|
128
|
+
? { status: 'emitted', detail: `digest=${String(step.digest).slice(0, 12)}` }
|
|
129
|
+
: { status: 'pending', detail: 'assembly requires static emit' };
|
|
130
|
+
}
|
|
131
|
+
else if (id === 'site-fallback') {
|
|
132
|
+
const siteStep = (ctx.assemble?.steps || []).find((s) => s.kind === 'site-delivery');
|
|
133
|
+
const packStep = (ctx.assemble?.steps || []).find((s) => s.kind === 'embedded-packaging');
|
|
134
|
+
if (siteStep && packStep) {
|
|
135
|
+
slots[id] = {
|
|
136
|
+
status: 'emitted',
|
|
137
|
+
detail: `contract=${String(siteStep.digest).slice(0, 12)}; pack=${String(packStep.digest).slice(0, 12)}; objects=${packStep.objectCount ?? '?'}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
else if (siteStep && siteStep.status !== 'skipped') {
|
|
141
|
+
slots[id] = {
|
|
142
|
+
status: 'pending',
|
|
143
|
+
detail: 'site contract emitted; embedded packaging missing',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
slots[id] = { status: 'pending', detail: siteStep?.reason || 'no site sources' };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else if (id === 'server-host') {
|
|
151
|
+
const step = (ctx.assemble?.steps || []).find((s) => s.kind === 'server-host');
|
|
152
|
+
slots[id] =
|
|
153
|
+
step && step.status === 'emitted'
|
|
154
|
+
? {
|
|
155
|
+
status: 'emitted',
|
|
156
|
+
detail: `digest=${String(step.digest).slice(0, 12)}; routes=${step.publicRoutes ?? 0}; internal=${step.internalCapabilities ?? 0}`,
|
|
157
|
+
}
|
|
158
|
+
: {
|
|
159
|
+
status: 'pending',
|
|
160
|
+
detail: step?.note || 'ServerArtifact not emitted',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
else if (id === 'asset-graph') {
|
|
164
|
+
slots[id] = {
|
|
165
|
+
status: ctx.pack?.packDigest ? 'pack-digest' : 'pending',
|
|
166
|
+
detail: ctx.pack?.packDigest
|
|
167
|
+
? `pack=${String(ctx.pack.packDigest).slice(0, 12)} units=${ctx.pack.unitCount ?? '?'}`
|
|
168
|
+
: 'pack missing',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const body = {
|
|
173
|
+
schema: BUILD_PROOF_SCHEMA,
|
|
174
|
+
profileId: ctx.selection.profileId,
|
|
175
|
+
assembly: ctx.selection.assembly,
|
|
176
|
+
selectionDigest: ctx.selection.digest || null,
|
|
177
|
+
packDigest: ctx.pack?.packDigest || null,
|
|
178
|
+
assembleDigest: ctx.assemble?.assembleDigest || null,
|
|
179
|
+
release: Boolean(ctx.release),
|
|
180
|
+
semanticIds,
|
|
181
|
+
slots,
|
|
182
|
+
productionReadyClaim: false,
|
|
183
|
+
note: 'Aggregate production-ready requires browser-production + cleared production-proof gaps (08)',
|
|
184
|
+
};
|
|
185
|
+
body.proofDigest = sha256Hex(canonicalJson({ ...body, proofDigest: undefined }));
|
|
186
|
+
const vmzDir = path.join(outDir, '_vmz');
|
|
187
|
+
mkdirSync(vmzDir, { recursive: true });
|
|
188
|
+
const file = path.join(vmzDir, 'build-proof.json');
|
|
189
|
+
writeFileSync(file, `${JSON.stringify(body, null, 2)}\n`, 'utf8');
|
|
190
|
+
return { proof: body, path: file };
|
|
191
|
+
}
|
package/dist/cdn-policy.d.ts
CHANGED
|
@@ -9,9 +9,13 @@ export declare const CACHE_HTML = "public, max-age=0, must-revalidate";
|
|
|
9
9
|
export declare const CACHE_ASSET_IMMUTABLE = "public, max-age=31536000, immutable";
|
|
10
10
|
export declare const CACHE_META = "public, max-age=3600";
|
|
11
11
|
/**
|
|
12
|
-
* Build CDNPolicyManifest from StaticDeliveryManifest (+ optional redirects).
|
|
12
|
+
* Build CDNPolicyManifest from StaticDeliveryManifest (+ optional redirects / locale artifact).
|
|
13
|
+
* Locale-prefixed HTML gets LocaleId-encoded cache keys; Accept-Language must not steal body.
|
|
13
14
|
* @param {Record<string, any>} staticManifest
|
|
14
|
-
* @param {{
|
|
15
|
+
* @param {{
|
|
16
|
+
* redirects?: Array<{ from: string, to: string, status?: number, reason?: string }>,
|
|
17
|
+
* localeArtifact?: Record<string, any> | null,
|
|
18
|
+
* }} [opts]
|
|
15
19
|
*/
|
|
16
20
|
export declare function buildCdnPolicyManifest(staticManifest: any, opts?: {}): {
|
|
17
21
|
schema: string;
|
|
@@ -35,7 +39,14 @@ export declare function buildCdnPolicyManifest(staticManifest: any, opts?: {}):
|
|
|
35
39
|
};
|
|
36
40
|
})[];
|
|
37
41
|
errorDocuments: any;
|
|
38
|
-
routes: any;
|
|
42
|
+
routes: any[];
|
|
43
|
+
localeCache: {
|
|
44
|
+
strategy: string;
|
|
45
|
+
varyAcceptLanguage: boolean;
|
|
46
|
+
defaultLocale: any;
|
|
47
|
+
routeCount: number;
|
|
48
|
+
locales: any[];
|
|
49
|
+
};
|
|
39
50
|
};
|
|
40
51
|
/**
|
|
41
52
|
* Write CDN policy + adapter projections under dist/_vmz.
|
|
@@ -66,7 +77,14 @@ export declare function emitCdnPolicy(distDir: any, staticManifest: any, opts?:
|
|
|
66
77
|
};
|
|
67
78
|
})[];
|
|
68
79
|
errorDocuments: any;
|
|
69
|
-
routes: any;
|
|
80
|
+
routes: any[];
|
|
81
|
+
localeCache: {
|
|
82
|
+
strategy: string;
|
|
83
|
+
varyAcceptLanguage: boolean;
|
|
84
|
+
defaultLocale: any;
|
|
85
|
+
routeCount: number;
|
|
86
|
+
locales: any[];
|
|
87
|
+
};
|
|
70
88
|
};
|
|
71
89
|
adapters: {
|
|
72
90
|
'local-static': {
|
package/dist/cdn-policy.js
CHANGED
|
@@ -7,6 +7,7 @@ import crypto from 'node:crypto';
|
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import http from 'node:http';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
+
import { assertLocaleCacheKey, localeAwareCacheKey } from './locale-router.js';
|
|
10
11
|
export const CDN_POLICY_MANIFEST_SCHEMA = 'vmz.cdn.policy_manifest.v0';
|
|
11
12
|
export const CDN_ADAPTER_PROJECTION_SCHEMA = 'vmz.cdn.adapter_projection.v0';
|
|
12
13
|
/** HTML: revalidate. Hashed/static assets: long immutable. */
|
|
@@ -14,13 +15,23 @@ export const CACHE_HTML = 'public, max-age=0, must-revalidate';
|
|
|
14
15
|
export const CACHE_ASSET_IMMUTABLE = 'public, max-age=31536000, immutable';
|
|
15
16
|
export const CACHE_META = 'public, max-age=3600';
|
|
16
17
|
/**
|
|
17
|
-
* Build CDNPolicyManifest from StaticDeliveryManifest (+ optional redirects).
|
|
18
|
+
* Build CDNPolicyManifest from StaticDeliveryManifest (+ optional redirects / locale artifact).
|
|
19
|
+
* Locale-prefixed HTML gets LocaleId-encoded cache keys; Accept-Language must not steal body.
|
|
18
20
|
* @param {Record<string, any>} staticManifest
|
|
19
|
-
* @param {{
|
|
21
|
+
* @param {{
|
|
22
|
+
* redirects?: Array<{ from: string, to: string, status?: number, reason?: string }>,
|
|
23
|
+
* localeArtifact?: Record<string, any> | null,
|
|
24
|
+
* }} [opts]
|
|
20
25
|
*/
|
|
21
26
|
export function buildCdnPolicyManifest(staticManifest, opts = {}) {
|
|
22
27
|
const origin = String(staticManifest.origin || '');
|
|
23
|
-
const
|
|
28
|
+
const localeArt = opts.localeArtifact || null;
|
|
29
|
+
const localeRedirects = buildOmitPrefixRedirects(staticManifest, localeArt);
|
|
30
|
+
const redirects = [
|
|
31
|
+
{ from: '/home', to: '/', status: 301, reason: 'canonical-alias' },
|
|
32
|
+
...localeRedirects,
|
|
33
|
+
...(opts.redirects || []),
|
|
34
|
+
];
|
|
24
35
|
const headers = [
|
|
25
36
|
{ match: '**/*.html', headers: { 'cache-control': CACHE_HTML } },
|
|
26
37
|
{
|
|
@@ -34,6 +45,47 @@ export function buildCdnPolicyManifest(staticManifest, opts = {}) {
|
|
|
34
45
|
{ match: '**/robots.txt', headers: { 'cache-control': CACHE_META } },
|
|
35
46
|
];
|
|
36
47
|
const errorDocuments = Array.isArray(staticManifest.errorDocuments) ? staticManifest.errorDocuments : [{ status: 404, path: '404.html' }];
|
|
48
|
+
/** @type {any[]} */
|
|
49
|
+
const routes = [];
|
|
50
|
+
/** @type {any[]} */
|
|
51
|
+
const cacheKeyDiagnostics = [];
|
|
52
|
+
for (const r of staticManifest.routes || []) {
|
|
53
|
+
const localeId = r.localeId || null;
|
|
54
|
+
const alternates = Array.isArray(r.seo?.alternates) ? r.seo.alternates : [];
|
|
55
|
+
const cacheKey = localeId
|
|
56
|
+
? localeAwareCacheKey({ routeId: String(r.routeId), localeId: String(localeId), path: String(r.path) })
|
|
57
|
+
: `route=${r.routeId}|path=${r.path}`;
|
|
58
|
+
// When LocaleId is bound, prove the key would still be safe even if Vary: Accept-Language were set.
|
|
59
|
+
if (localeId) {
|
|
60
|
+
const assert = assertLocaleCacheKey({
|
|
61
|
+
cacheKey,
|
|
62
|
+
varyAcceptLanguage: true,
|
|
63
|
+
localeId: String(localeId),
|
|
64
|
+
});
|
|
65
|
+
if (!assert.ok) {
|
|
66
|
+
cacheKeyDiagnostics.push(...(assert.diagnostics || []));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
routes.push({
|
|
70
|
+
routeId: r.routeId,
|
|
71
|
+
path: r.path,
|
|
72
|
+
htmlPath: r.htmlPath,
|
|
73
|
+
canonical: r.seo?.canonical || null,
|
|
74
|
+
localeId,
|
|
75
|
+
cacheKey,
|
|
76
|
+
varyAcceptLanguage: false,
|
|
77
|
+
hreflang: alternates.map((a) => ({
|
|
78
|
+
hreflang: a.hreflang,
|
|
79
|
+
href: a.href,
|
|
80
|
+
localeId: a.localeId || null,
|
|
81
|
+
})),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (cacheKeyDiagnostics.length) {
|
|
85
|
+
const msg = cacheKeyDiagnostics.map((d) => d.message || d.code).join('; ');
|
|
86
|
+
throw new Error(`buildCdnPolicyManifest: locale cache key contract failed: ${msg}`);
|
|
87
|
+
}
|
|
88
|
+
const localeRoutes = routes.filter((r) => r.localeId);
|
|
37
89
|
const body = {
|
|
38
90
|
schema: CDN_POLICY_MANIFEST_SCHEMA,
|
|
39
91
|
applicationId: staticManifest.applicationId || null,
|
|
@@ -44,16 +96,48 @@ export function buildCdnPolicyManifest(staticManifest, opts = {}) {
|
|
|
44
96
|
redirects,
|
|
45
97
|
headers,
|
|
46
98
|
errorDocuments,
|
|
47
|
-
routes
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
99
|
+
routes,
|
|
100
|
+
localeCache: {
|
|
101
|
+
strategy: 'path-locale',
|
|
102
|
+
varyAcceptLanguage: false,
|
|
103
|
+
defaultLocale: localeArt?.defaultLocale || null,
|
|
104
|
+
routeCount: localeRoutes.length,
|
|
105
|
+
locales: [...new Set(localeRoutes.map((r) => r.localeId))],
|
|
106
|
+
},
|
|
53
107
|
};
|
|
54
108
|
body.policyDigest = sha256Hex(canonicalJson(body));
|
|
55
109
|
return body;
|
|
56
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Omit-prefix: /{defaultLocale}/… → canonical unprefixed path (CDN redirect, not Accept-Language).
|
|
113
|
+
* @param {Record<string, any>} staticManifest
|
|
114
|
+
* @param {Record<string, any> | null} localeArt
|
|
115
|
+
*/
|
|
116
|
+
function buildOmitPrefixRedirects(staticManifest, localeArt) {
|
|
117
|
+
const routing = localeArt?.routing || {};
|
|
118
|
+
const defaultLocale = localeArt?.defaultLocale || routing.defaultLocale;
|
|
119
|
+
if (!defaultLocale || routing.defaultPrefix !== 'omit')
|
|
120
|
+
return [];
|
|
121
|
+
/** @type {Array<{ from: string, to: string, status: number, reason: string }>} */
|
|
122
|
+
const out = [];
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
for (const r of staticManifest.routes || []) {
|
|
125
|
+
if (r.localeId !== defaultLocale)
|
|
126
|
+
continue;
|
|
127
|
+
const canonical = String(r.path || '/');
|
|
128
|
+
const from = canonical === '/' ? `/${defaultLocale}` : `/${defaultLocale}${canonical.startsWith('/') ? canonical : `/${canonical}`}`;
|
|
129
|
+
if (seen.has(from))
|
|
130
|
+
continue;
|
|
131
|
+
seen.add(from);
|
|
132
|
+
out.push({
|
|
133
|
+
from,
|
|
134
|
+
to: canonical,
|
|
135
|
+
status: 301,
|
|
136
|
+
reason: 'locale-omit-prefix-default',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
57
141
|
/**
|
|
58
142
|
* Write CDN policy + adapter projections under dist/_vmz.
|
|
59
143
|
* @param {string} distDir
|
|
@@ -61,7 +145,8 @@ export function buildCdnPolicyManifest(staticManifest, opts = {}) {
|
|
|
61
145
|
* @param {{ redirects?: Array<{ from: string, to: string, status?: number, reason?: string }> }} [opts]
|
|
62
146
|
*/
|
|
63
147
|
export function emitCdnPolicy(distDir, staticManifest, opts = {}) {
|
|
64
|
-
const
|
|
148
|
+
const localeArtifact = loadLocaleArtifact(distDir);
|
|
149
|
+
const policy = buildCdnPolicyManifest(staticManifest, { ...opts, localeArtifact });
|
|
65
150
|
const vmzDir = path.join(distDir, '_vmz');
|
|
66
151
|
fs.mkdirSync(vmzDir, { recursive: true });
|
|
67
152
|
fs.writeFileSync(path.join(vmzDir, 'cdn-policy-manifest.json'), `${JSON.stringify(policy, null, 2)}\n`, 'utf8');
|
|
@@ -76,6 +161,20 @@ export function emitCdnPolicy(distDir, staticManifest, opts = {}) {
|
|
|
76
161
|
fs.writeFileSync(path.join(adaptersDir, 'netlify', '_redirects'), String(netlify.files['_redirects'] || ''), 'utf8');
|
|
77
162
|
return { policy, adapters: { 'local-static': local, netlify } };
|
|
78
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* @param {string} distDir
|
|
166
|
+
*/
|
|
167
|
+
function loadLocaleArtifact(distDir) {
|
|
168
|
+
const p = path.join(distDir, '_vmz', 'locale-route-realization.json');
|
|
169
|
+
if (!fs.existsSync(p))
|
|
170
|
+
return null;
|
|
171
|
+
try {
|
|
172
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
79
178
|
/**
|
|
80
179
|
* Project vendor-neutral policy to an adapter. Must preserve redirect targets and forbid SPA fallback.
|
|
81
180
|
* @param {Record<string, any>} policy
|
package/dist/cli.js
CHANGED
|
@@ -16,14 +16,16 @@ import { cmdDocument } from './document-cmd.js';
|
|
|
16
16
|
import { buildIntegratedDocuments, projectHasDocuments } from './document-integrate.js';
|
|
17
17
|
import { cmdLocale } from './locale-cmd.js';
|
|
18
18
|
import { emitLocaleRuntimeModules, localeHasErrors } from './locale-check.js';
|
|
19
|
+
import { emitLocaleRouteRealization } from './locale-route-emit.js';
|
|
19
20
|
import { cmdApplication } from './application-cmd.js';
|
|
20
21
|
import { cmdArtifact } from './release-cmd.js';
|
|
21
22
|
import { cmdRefactor } from './refactor-cmd.js';
|
|
22
23
|
import { cmdExplain } from './explain-cmd.js';
|
|
23
24
|
import { resolveNativeVmzCli } from './resolve-native-cli.js';
|
|
24
|
-
import { emitWebStatic } from './static-emit.js';
|
|
25
|
-
import { emitSiteDelivery } from './site-delivery.js';
|
|
26
25
|
import { loadVmzConfig } from './plugin-host.js';
|
|
26
|
+
import { normalizeDeliveryAuthoring, selectBuildProfile } from './delivery-profile.js';
|
|
27
|
+
import { packFromDeploymentIr } from './pack.js';
|
|
28
|
+
import { assembleDelivery, emitBuildProof } from './build-assemble.js';
|
|
27
29
|
/**
|
|
28
30
|
* @param {string[]} argv
|
|
29
31
|
*/
|
|
@@ -120,9 +122,9 @@ Usage:
|
|
|
120
122
|
|
|
121
123
|
Options:
|
|
122
124
|
--out-dir, -o <dir> Output directory (default: dist)
|
|
123
|
-
--release Release build (
|
|
124
|
-
--profile <name> Delivery profile
|
|
125
|
-
--origin <url> Site origin for
|
|
125
|
+
--release Release build (omit serve-host; pack minify slot; proof)
|
|
126
|
+
--profile <name> Delivery profile (default from config; builtins: web-ssr|web-static|web-client|web-hybrid)
|
|
127
|
+
--origin <url> Site origin for static-cdn canonical/sitemap
|
|
126
128
|
--host <host> Listen host (default: 127.0.0.1)
|
|
127
129
|
--port <port> Listen port (dev: omit = auto from 5173; set = lock)
|
|
128
130
|
--poll-ms <ms> Dev watch poll interval (default: 300)
|
|
@@ -281,7 +283,11 @@ function cmdCheck(args) {
|
|
|
281
283
|
try {
|
|
282
284
|
return runWithPlugins(ws, project, outDir, async () => {
|
|
283
285
|
const report = ws.check();
|
|
284
|
-
const
|
|
286
|
+
const { checkLocales, localeHasErrors } = await import('./locale-check.js');
|
|
287
|
+
const localeReport = checkLocales({ projectRoot: project, checkUnused: false });
|
|
288
|
+
// Locale policy is first-class: missing /locales is warning (not silent), hard errors still fail.
|
|
289
|
+
const errors = log.diagnostics([...(report.diagnostics ?? []), ...(localeReport.diagnostics ?? [])]) ||
|
|
290
|
+
(localeHasErrors(localeReport) ? 1 : 0);
|
|
285
291
|
log.info(`checked ${report.filesChecked} file(s)`);
|
|
286
292
|
return errors ? 1 : 0;
|
|
287
293
|
});
|
|
@@ -290,6 +296,23 @@ function cmdCheck(args) {
|
|
|
290
296
|
ws.dispose();
|
|
291
297
|
}
|
|
292
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Dedupe locale/build diagnostics by code+path+message (runtime + route emit both report missing manifest).
|
|
301
|
+
* @param {Array<{ code?: string, path?: string, message?: string, severity?: string }>} list
|
|
302
|
+
*/
|
|
303
|
+
function dedupeDiagnostics(list) {
|
|
304
|
+
const seen = new Set();
|
|
305
|
+
/** @type {typeof list} */
|
|
306
|
+
const out = [];
|
|
307
|
+
for (const d of list || []) {
|
|
308
|
+
const key = `${d.code || ''}\0${d.path || ''}\0${d.message || ''}\0${d.severity || ''}`;
|
|
309
|
+
if (seen.has(key))
|
|
310
|
+
continue;
|
|
311
|
+
seen.add(key);
|
|
312
|
+
out.push(d);
|
|
313
|
+
}
|
|
314
|
+
return out;
|
|
315
|
+
}
|
|
293
316
|
/**
|
|
294
317
|
* @param {import('./index.js').Workspace} ws
|
|
295
318
|
* @param {string} project
|
|
@@ -316,6 +339,21 @@ async function cmdBuild(args) {
|
|
|
316
339
|
log.info(`build ${project} → ${outDir}`);
|
|
317
340
|
const ws = createWorkspace({ root: project, outDir });
|
|
318
341
|
try {
|
|
342
|
+
const cfg = await loadVmzConfig(project);
|
|
343
|
+
const cliProfile = typeof args.profile === 'string' ? args.profile : '';
|
|
344
|
+
const norm = normalizeDeliveryAuthoring(cfg.delivery ?? null);
|
|
345
|
+
if (!norm.ok) {
|
|
346
|
+
log.diagnostics(norm.diagnostics ?? []);
|
|
347
|
+
log.error('delivery authoring invalid');
|
|
348
|
+
return 1;
|
|
349
|
+
}
|
|
350
|
+
const selected = selectBuildProfile(norm.table, cliProfile);
|
|
351
|
+
if (!selected.ok) {
|
|
352
|
+
log.diagnostics(selected.diagnostics ?? []);
|
|
353
|
+
log.error(`unknown build --profile ${cliProfile || norm.table.default}`);
|
|
354
|
+
return 1;
|
|
355
|
+
}
|
|
356
|
+
log.info(`delivery profile ${selected.selection.profileId} (assembly=${selected.selection.assembly})`);
|
|
319
357
|
const code = await runWithPlugins(ws, project, outDir, () => {
|
|
320
358
|
const report = ws.build(Boolean(args.release));
|
|
321
359
|
const errors = log.diagnostics(report.diagnostics ?? []);
|
|
@@ -330,47 +368,91 @@ async function cmdBuild(args) {
|
|
|
330
368
|
if (code !== 0)
|
|
331
369
|
return code;
|
|
332
370
|
const localeEmit = emitLocaleRuntimeModules(project, outDir);
|
|
371
|
+
const localeRoutes = emitLocaleRouteRealization(project, outDir, {
|
|
372
|
+
origin: typeof args.origin === 'string' ? args.origin : undefined,
|
|
373
|
+
});
|
|
374
|
+
// Always surface locale diagnostics (warnings included) — missing /locales must not be silent.
|
|
375
|
+
// Dedupe: runtime emit + route realization both report the same missing-manifest warning.
|
|
376
|
+
const localeDiags = dedupeDiagnostics([
|
|
377
|
+
...(localeEmit.diagnostics ?? []),
|
|
378
|
+
...(localeRoutes.diagnostics ?? []),
|
|
379
|
+
]);
|
|
380
|
+
log.diagnostics(localeDiags);
|
|
333
381
|
if (!localeEmit.ok || localeHasErrors({ diagnostics: localeEmit.diagnostics })) {
|
|
334
|
-
log.diagnostics(localeEmit.diagnostics ?? []);
|
|
335
382
|
log.error('locale runtime emit failed');
|
|
336
383
|
return 1;
|
|
337
384
|
}
|
|
338
385
|
if (localeEmit.written.length) {
|
|
339
386
|
log.info(`locale runtime emit (${localeEmit.written.length} module(s))`);
|
|
340
387
|
}
|
|
388
|
+
if (!localeRoutes.ok) {
|
|
389
|
+
log.error('locale route realization emit failed');
|
|
390
|
+
return 1;
|
|
391
|
+
}
|
|
392
|
+
if (localeRoutes.written.length) {
|
|
393
|
+
log.info(`locale route realization (${localeRoutes.written.length} artifact(s))`);
|
|
394
|
+
}
|
|
341
395
|
if (projectHasDocuments(project)) {
|
|
342
396
|
const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
|
|
343
397
|
if (!docs.ok)
|
|
344
398
|
return 1;
|
|
345
399
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
400
|
+
let pack = null;
|
|
401
|
+
try {
|
|
402
|
+
pack = packFromDeploymentIr(outDir, {
|
|
403
|
+
release: Boolean(args.release),
|
|
404
|
+
profileId: selected.selection.profileId,
|
|
405
|
+
assembly: selected.selection.assembly,
|
|
406
|
+
coreDist: resolveCoreRuntimeDist(),
|
|
351
407
|
});
|
|
352
|
-
log.info(`
|
|
408
|
+
log.info(`pack ok (units=${pack.manifest.unitCount}, digest=${String(pack.manifest.packDigest).slice(0, 12)}…)`);
|
|
353
409
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
410
|
+
catch (err) {
|
|
411
|
+
log.error(`pack failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
412
|
+
return 1;
|
|
413
|
+
}
|
|
414
|
+
const origin = typeof args.origin === 'string' ? args.origin : undefined;
|
|
415
|
+
let assemble = null;
|
|
416
|
+
try {
|
|
417
|
+
if (selected.selection.assembly === 'static-cdn') {
|
|
418
|
+
log.info(`web-static emit ${outDir}`);
|
|
419
|
+
}
|
|
420
|
+
assemble = await assembleDelivery(outDir, {
|
|
421
|
+
selection: selected.selection,
|
|
422
|
+
profile: {
|
|
423
|
+
...selected.profile,
|
|
424
|
+
sources: selected.profile.sources || (norm.table.sugar ? norm.table.profiles[norm.table.default]?.sources : null),
|
|
425
|
+
},
|
|
426
|
+
siteId: cfg.application?.id || undefined,
|
|
427
|
+
origin,
|
|
428
|
+
pack: pack.manifest,
|
|
429
|
+
});
|
|
430
|
+
for (const step of assemble.manifest.steps || []) {
|
|
431
|
+
if (step.kind === 'static-cdn') {
|
|
432
|
+
log.info(`web-static ok (${step.htmlFiles} html, ${step.skipped} skipped, digest=${String(step.digest).slice(0, 12)}…)`);
|
|
433
|
+
}
|
|
434
|
+
else if (step.kind === 'site-delivery' && step.digest) {
|
|
435
|
+
log.info(`site-delivery ok (digest=${String(step.digest).slice(0, 12)}…)`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
360
438
|
}
|
|
361
|
-
|
|
362
|
-
log.error(`
|
|
439
|
+
catch (err) {
|
|
440
|
+
log.error(`assemble failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
363
441
|
return 1;
|
|
364
442
|
}
|
|
443
|
+
const proof = emitBuildProof(outDir, {
|
|
444
|
+
selection: selected.selection,
|
|
445
|
+
pack: pack.manifest,
|
|
446
|
+
assemble: assemble.manifest,
|
|
447
|
+
release: Boolean(args.release),
|
|
448
|
+
});
|
|
449
|
+
log.info(`build-proof ok (profile=${proof.proof.profileId}, slots=${proof.proof.semanticIds.join(',')})`);
|
|
365
450
|
return 0;
|
|
366
451
|
}
|
|
367
452
|
finally {
|
|
368
453
|
ws.dispose();
|
|
369
454
|
}
|
|
370
455
|
}
|
|
371
|
-
/**
|
|
372
|
-
* @param {Record<string, string | boolean> & { _: string[] }} args
|
|
373
|
-
*/
|
|
374
456
|
async function cmdServe(args) {
|
|
375
457
|
const pathArg = args._[0] ?? '.';
|
|
376
458
|
if (args.build) {
|