@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,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,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B5 Assemble dispatch + B6 build-proof (per-build semantic id slots).
|
|
3
|
+
*/
|
|
4
|
+
// @ts-nocheck
|
|
5
|
+
import { mkdirSync } 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
|
+
import { writePrettyJsonFile } from './pretty-json.js';
|
|
13
|
+
export const BUILD_PROOF_SCHEMA = 'vmz.build.proof.v0';
|
|
14
|
+
export const ASSEMBLE_MANIFEST_SCHEMA = 'vmz.assemble.manifest.v0';
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} outDir
|
|
17
|
+
* @param {any} ctx
|
|
18
|
+
*/
|
|
19
|
+
export async function assembleDelivery(outDir, ctx) {
|
|
20
|
+
const { selection, profile } = ctx;
|
|
21
|
+
const assembly = selection.assembly;
|
|
22
|
+
const result = {
|
|
23
|
+
schema: ASSEMBLE_MANIFEST_SCHEMA,
|
|
24
|
+
profileId: selection.profileId,
|
|
25
|
+
assembly,
|
|
26
|
+
serverRuntime: selection.serverRuntime || null,
|
|
27
|
+
steps: [],
|
|
28
|
+
};
|
|
29
|
+
if (assembly === 'static-cdn' || assembly === 'cdn+server') {
|
|
30
|
+
const staticResult = await emitWebStatic(outDir, { origin: ctx.origin });
|
|
31
|
+
result.steps.push({
|
|
32
|
+
kind: 'static-cdn',
|
|
33
|
+
digest: staticResult.digest,
|
|
34
|
+
htmlFiles: staticResult.htmlFiles?.length ?? 0,
|
|
35
|
+
skipped: staticResult.skipped?.length ?? 0,
|
|
36
|
+
});
|
|
37
|
+
result.staticDelivery = {
|
|
38
|
+
digest: staticResult.digest,
|
|
39
|
+
htmlFiles: staticResult.htmlFiles,
|
|
40
|
+
skipped: staticResult.skipped,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (assembly === 'local-static') {
|
|
44
|
+
result.steps.push({
|
|
45
|
+
kind: 'local-static',
|
|
46
|
+
status: 'modules-ready',
|
|
47
|
+
note: 'client modules packed; no ServerArtifact',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (assembly === 'server-host' || assembly === 'cdn+server') {
|
|
51
|
+
const server = emitServerArtifact(outDir, {
|
|
52
|
+
profileId: selection.profileId,
|
|
53
|
+
assembly,
|
|
54
|
+
serverRuntime: selection.serverRuntime || 'node',
|
|
55
|
+
packDigest: ctx.pack?.packDigest || null,
|
|
56
|
+
});
|
|
57
|
+
result.steps.push({
|
|
58
|
+
kind: 'server-host',
|
|
59
|
+
status: 'emitted',
|
|
60
|
+
digest: server.artifact.artifactDigest,
|
|
61
|
+
publicRoutes: server.artifact.publicRoutes?.length ?? 0,
|
|
62
|
+
internalCapabilities: server.artifact.internalCapabilities?.length ?? 0,
|
|
63
|
+
httpContractDigest: server.httpContractDigest,
|
|
64
|
+
});
|
|
65
|
+
result.serverArtifact = {
|
|
66
|
+
digest: server.artifact.artifactDigest,
|
|
67
|
+
httpContractDigest: server.httpContractDigest,
|
|
68
|
+
schema: server.artifact.schema,
|
|
69
|
+
selectedRuntime: server.artifact.selectedRuntime,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const siteAuthoring = profile.sources || null;
|
|
73
|
+
if (siteAuthoring || assembly === 'rust-embedded') {
|
|
74
|
+
if (!siteAuthoring) {
|
|
75
|
+
throw new Error('rust-embedded requires delivery sources (SiteDeliveryContract); cannot assemble without embedded|filesystem|remote baselines');
|
|
76
|
+
}
|
|
77
|
+
const site = emitSiteDelivery(outDir, siteAuthoring, {
|
|
78
|
+
siteId: ctx.siteId,
|
|
79
|
+
});
|
|
80
|
+
result.steps.push({
|
|
81
|
+
kind: 'site-delivery',
|
|
82
|
+
digest: site.contract.contractDigest,
|
|
83
|
+
});
|
|
84
|
+
result.siteDelivery = {
|
|
85
|
+
digest: site.contract.contractDigest,
|
|
86
|
+
schema: site.contract.schema,
|
|
87
|
+
};
|
|
88
|
+
if (assembly === 'rust-embedded') {
|
|
89
|
+
const pack = emitEmbeddedPackaging(outDir, {
|
|
90
|
+
siteId: ctx.siteId,
|
|
91
|
+
contractDigest: site.contract.contractDigest,
|
|
92
|
+
});
|
|
93
|
+
result.steps.push({
|
|
94
|
+
kind: 'embedded-packaging',
|
|
95
|
+
digest: pack.index.indexDigest,
|
|
96
|
+
objectCount: pack.index.objectCount,
|
|
97
|
+
});
|
|
98
|
+
result.embeddedPackaging = {
|
|
99
|
+
digest: pack.index.indexDigest,
|
|
100
|
+
objectCount: pack.index.objectCount,
|
|
101
|
+
schema: pack.index.schema,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
result.packDigest = ctx.pack?.packDigest || null;
|
|
106
|
+
result.assembleDigest = sha256Hex(canonicalJson({ ...result, assembleDigest: undefined }));
|
|
107
|
+
const vmzDir = path.join(outDir, '_vmz');
|
|
108
|
+
mkdirSync(vmzDir, { recursive: true });
|
|
109
|
+
const file = path.join(vmzDir, 'assemble-manifest.json');
|
|
110
|
+
writePrettyJsonFile(file, result);
|
|
111
|
+
return { manifest: result, path: file };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* @param {string} outDir
|
|
115
|
+
* @param {any} ctx
|
|
116
|
+
*/
|
|
117
|
+
export function emitBuildProof(outDir, ctx) {
|
|
118
|
+
const semanticIds = semanticIdsForAssembly(ctx.selection.assembly);
|
|
119
|
+
const slots = {
|
|
120
|
+
'server-host': { status: 'not-applicable' },
|
|
121
|
+
'static-delivery': { status: 'not-applicable' },
|
|
122
|
+
'site-fallback': { status: 'not-applicable' },
|
|
123
|
+
'asset-graph': { status: 'not-applicable' },
|
|
124
|
+
};
|
|
125
|
+
for (const id of semanticIds) {
|
|
126
|
+
if (id === 'static-delivery') {
|
|
127
|
+
const step = (ctx.assemble?.steps || []).find((s) => s.kind === 'static-cdn');
|
|
128
|
+
slots[id] = step
|
|
129
|
+
? { status: 'emitted', detail: `digest=${String(step.digest).slice(0, 12)}` }
|
|
130
|
+
: { status: 'pending', detail: 'assembly requires static emit' };
|
|
131
|
+
}
|
|
132
|
+
else if (id === 'site-fallback') {
|
|
133
|
+
const siteStep = (ctx.assemble?.steps || []).find((s) => s.kind === 'site-delivery');
|
|
134
|
+
const packStep = (ctx.assemble?.steps || []).find((s) => s.kind === 'embedded-packaging');
|
|
135
|
+
if (siteStep && packStep) {
|
|
136
|
+
slots[id] = {
|
|
137
|
+
status: 'emitted',
|
|
138
|
+
detail: `contract=${String(siteStep.digest).slice(0, 12)}; pack=${String(packStep.digest).slice(0, 12)}; objects=${packStep.objectCount ?? '?'}`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
else if (siteStep && siteStep.status !== 'skipped') {
|
|
142
|
+
slots[id] = {
|
|
143
|
+
status: 'pending',
|
|
144
|
+
detail: 'site contract emitted; embedded packaging missing',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
slots[id] = { status: 'pending', detail: siteStep?.reason || 'no site sources' };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else if (id === 'server-host') {
|
|
152
|
+
const step = (ctx.assemble?.steps || []).find((s) => s.kind === 'server-host');
|
|
153
|
+
slots[id] =
|
|
154
|
+
step && step.status === 'emitted'
|
|
155
|
+
? {
|
|
156
|
+
status: 'emitted',
|
|
157
|
+
detail: `digest=${String(step.digest).slice(0, 12)}; routes=${step.publicRoutes ?? 0}; internal=${step.internalCapabilities ?? 0}`,
|
|
158
|
+
}
|
|
159
|
+
: {
|
|
160
|
+
status: 'pending',
|
|
161
|
+
detail: step?.note || 'ServerArtifact not emitted',
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
else if (id === 'asset-graph') {
|
|
165
|
+
slots[id] = {
|
|
166
|
+
status: ctx.pack?.packDigest ? 'pack-digest' : 'pending',
|
|
167
|
+
detail: ctx.pack?.packDigest
|
|
168
|
+
? `pack=${String(ctx.pack.packDigest).slice(0, 12)} units=${ctx.pack.unitCount ?? '?'}`
|
|
169
|
+
: 'pack missing',
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const body = {
|
|
174
|
+
schema: BUILD_PROOF_SCHEMA,
|
|
175
|
+
profileId: ctx.selection.profileId,
|
|
176
|
+
assembly: ctx.selection.assembly,
|
|
177
|
+
selectionDigest: ctx.selection.digest || null,
|
|
178
|
+
packDigest: ctx.pack?.packDigest || null,
|
|
179
|
+
assembleDigest: ctx.assemble?.assembleDigest || null,
|
|
180
|
+
release: Boolean(ctx.release),
|
|
181
|
+
semanticIds,
|
|
182
|
+
slots,
|
|
183
|
+
productionReadyClaim: false,
|
|
184
|
+
note: 'Aggregate production-ready requires browser-production + cleared production-proof gaps (08)',
|
|
185
|
+
};
|
|
186
|
+
body.proofDigest = sha256Hex(canonicalJson({ ...body, proofDigest: undefined }));
|
|
187
|
+
const vmzDir = path.join(outDir, '_vmz');
|
|
188
|
+
mkdirSync(vmzDir, { recursive: true });
|
|
189
|
+
const file = path.join(vmzDir, 'build-proof.json');
|
|
190
|
+
writePrettyJsonFile(file, body);
|
|
191
|
+
return { proof: body, path: file };
|
|
192
|
+
}
|
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,8 @@ 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';
|
|
11
|
+
import { writePrettyJsonFile } from './pretty-json.js';
|
|
10
12
|
export const CDN_POLICY_MANIFEST_SCHEMA = 'vmz.cdn.policy_manifest.v0';
|
|
11
13
|
export const CDN_ADAPTER_PROJECTION_SCHEMA = 'vmz.cdn.adapter_projection.v0';
|
|
12
14
|
/** HTML: revalidate. Hashed/static assets: long immutable. */
|
|
@@ -14,13 +16,19 @@ export const CACHE_HTML = 'public, max-age=0, must-revalidate';
|
|
|
14
16
|
export const CACHE_ASSET_IMMUTABLE = 'public, max-age=31536000, immutable';
|
|
15
17
|
export const CACHE_META = 'public, max-age=3600';
|
|
16
18
|
/**
|
|
17
|
-
* Build CDNPolicyManifest from StaticDeliveryManifest (+ optional redirects).
|
|
19
|
+
* Build CDNPolicyManifest from StaticDeliveryManifest (+ optional redirects / locale artifact).
|
|
20
|
+
* Locale-prefixed HTML gets LocaleId-encoded cache keys; Accept-Language must not steal body.
|
|
18
21
|
* @param {Record<string, any>} staticManifest
|
|
19
|
-
* @param {{
|
|
22
|
+
* @param {{
|
|
23
|
+
* redirects?: Array<{ from: string, to: string, status?: number, reason?: string }>,
|
|
24
|
+
* localeArtifact?: Record<string, any> | null,
|
|
25
|
+
* }} [opts]
|
|
20
26
|
*/
|
|
21
27
|
export function buildCdnPolicyManifest(staticManifest, opts = {}) {
|
|
22
28
|
const origin = String(staticManifest.origin || '');
|
|
23
|
-
const
|
|
29
|
+
const localeArt = opts.localeArtifact || null;
|
|
30
|
+
const localeRedirects = buildOmitPrefixRedirects(staticManifest, localeArt);
|
|
31
|
+
const redirects = [{ from: '/home', to: '/', status: 301, reason: 'canonical-alias' }, ...localeRedirects, ...(opts.redirects || [])];
|
|
24
32
|
const headers = [
|
|
25
33
|
{ match: '**/*.html', headers: { 'cache-control': CACHE_HTML } },
|
|
26
34
|
{
|
|
@@ -34,6 +42,47 @@ export function buildCdnPolicyManifest(staticManifest, opts = {}) {
|
|
|
34
42
|
{ match: '**/robots.txt', headers: { 'cache-control': CACHE_META } },
|
|
35
43
|
];
|
|
36
44
|
const errorDocuments = Array.isArray(staticManifest.errorDocuments) ? staticManifest.errorDocuments : [{ status: 404, path: '404.html' }];
|
|
45
|
+
/** @type {any[]} */
|
|
46
|
+
const routes = [];
|
|
47
|
+
/** @type {any[]} */
|
|
48
|
+
const cacheKeyDiagnostics = [];
|
|
49
|
+
for (const r of staticManifest.routes || []) {
|
|
50
|
+
const localeId = r.localeId || null;
|
|
51
|
+
const alternates = Array.isArray(r.seo?.alternates) ? r.seo.alternates : [];
|
|
52
|
+
const cacheKey = localeId
|
|
53
|
+
? localeAwareCacheKey({ routeId: String(r.routeId), localeId: String(localeId), path: String(r.path) })
|
|
54
|
+
: `route=${r.routeId}|path=${r.path}`;
|
|
55
|
+
// When LocaleId is bound, prove the key would still be safe even if Vary: Accept-Language were set.
|
|
56
|
+
if (localeId) {
|
|
57
|
+
const assert = assertLocaleCacheKey({
|
|
58
|
+
cacheKey,
|
|
59
|
+
varyAcceptLanguage: true,
|
|
60
|
+
localeId: String(localeId),
|
|
61
|
+
});
|
|
62
|
+
if (!assert.ok) {
|
|
63
|
+
cacheKeyDiagnostics.push(...(assert.diagnostics || []));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
routes.push({
|
|
67
|
+
routeId: r.routeId,
|
|
68
|
+
path: r.path,
|
|
69
|
+
htmlPath: r.htmlPath,
|
|
70
|
+
canonical: r.seo?.canonical || null,
|
|
71
|
+
localeId,
|
|
72
|
+
cacheKey,
|
|
73
|
+
varyAcceptLanguage: false,
|
|
74
|
+
hreflang: alternates.map((a) => ({
|
|
75
|
+
hreflang: a.hreflang,
|
|
76
|
+
href: a.href,
|
|
77
|
+
localeId: a.localeId || null,
|
|
78
|
+
})),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (cacheKeyDiagnostics.length) {
|
|
82
|
+
const msg = cacheKeyDiagnostics.map((d) => d.message || d.code).join('; ');
|
|
83
|
+
throw new Error(`buildCdnPolicyManifest: locale cache key contract failed: ${msg}`);
|
|
84
|
+
}
|
|
85
|
+
const localeRoutes = routes.filter((r) => r.localeId);
|
|
37
86
|
const body = {
|
|
38
87
|
schema: CDN_POLICY_MANIFEST_SCHEMA,
|
|
39
88
|
applicationId: staticManifest.applicationId || null,
|
|
@@ -44,16 +93,48 @@ export function buildCdnPolicyManifest(staticManifest, opts = {}) {
|
|
|
44
93
|
redirects,
|
|
45
94
|
headers,
|
|
46
95
|
errorDocuments,
|
|
47
|
-
routes
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
96
|
+
routes,
|
|
97
|
+
localeCache: {
|
|
98
|
+
strategy: 'path-locale',
|
|
99
|
+
varyAcceptLanguage: false,
|
|
100
|
+
defaultLocale: localeArt?.defaultLocale || null,
|
|
101
|
+
routeCount: localeRoutes.length,
|
|
102
|
+
locales: [...new Set(localeRoutes.map((r) => r.localeId))],
|
|
103
|
+
},
|
|
53
104
|
};
|
|
54
105
|
body.policyDigest = sha256Hex(canonicalJson(body));
|
|
55
106
|
return body;
|
|
56
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Omit-prefix: /{defaultLocale}/… → canonical unprefixed path (CDN redirect, not Accept-Language).
|
|
110
|
+
* @param {Record<string, any>} staticManifest
|
|
111
|
+
* @param {Record<string, any> | null} localeArt
|
|
112
|
+
*/
|
|
113
|
+
function buildOmitPrefixRedirects(staticManifest, localeArt) {
|
|
114
|
+
const routing = localeArt?.routing || {};
|
|
115
|
+
const defaultLocale = localeArt?.defaultLocale || routing.defaultLocale;
|
|
116
|
+
if (!defaultLocale || routing.defaultPrefix !== 'omit')
|
|
117
|
+
return [];
|
|
118
|
+
/** @type {Array<{ from: string, to: string, status: number, reason: string }>} */
|
|
119
|
+
const out = [];
|
|
120
|
+
const seen = new Set();
|
|
121
|
+
for (const r of staticManifest.routes || []) {
|
|
122
|
+
if (r.localeId !== defaultLocale)
|
|
123
|
+
continue;
|
|
124
|
+
const canonical = String(r.path || '/');
|
|
125
|
+
const from = canonical === '/' ? `/${defaultLocale}` : `/${defaultLocale}${canonical.startsWith('/') ? canonical : `/${canonical}`}`;
|
|
126
|
+
if (seen.has(from))
|
|
127
|
+
continue;
|
|
128
|
+
seen.add(from);
|
|
129
|
+
out.push({
|
|
130
|
+
from,
|
|
131
|
+
to: canonical,
|
|
132
|
+
status: 301,
|
|
133
|
+
reason: 'locale-omit-prefix-default',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
57
138
|
/**
|
|
58
139
|
* Write CDN policy + adapter projections under dist/_vmz.
|
|
59
140
|
* @param {string} distDir
|
|
@@ -61,21 +142,36 @@ export function buildCdnPolicyManifest(staticManifest, opts = {}) {
|
|
|
61
142
|
* @param {{ redirects?: Array<{ from: string, to: string, status?: number, reason?: string }> }} [opts]
|
|
62
143
|
*/
|
|
63
144
|
export function emitCdnPolicy(distDir, staticManifest, opts = {}) {
|
|
64
|
-
const
|
|
145
|
+
const localeArtifact = loadLocaleArtifact(distDir);
|
|
146
|
+
const policy = buildCdnPolicyManifest(staticManifest, { ...opts, localeArtifact });
|
|
65
147
|
const vmzDir = path.join(distDir, '_vmz');
|
|
66
148
|
fs.mkdirSync(vmzDir, { recursive: true });
|
|
67
|
-
|
|
149
|
+
writePrettyJsonFile(path.join(vmzDir, 'cdn-policy-manifest.json'), policy);
|
|
68
150
|
const local = projectCdnAdapter(policy, 'local-static');
|
|
69
151
|
const netlify = projectCdnAdapter(policy, 'netlify');
|
|
70
152
|
const adaptersDir = path.join(vmzDir, 'adapters');
|
|
71
153
|
fs.mkdirSync(path.join(adaptersDir, 'local-static'), { recursive: true });
|
|
72
154
|
fs.mkdirSync(path.join(adaptersDir, 'netlify'), { recursive: true });
|
|
73
|
-
|
|
74
|
-
|
|
155
|
+
writePrettyJsonFile(path.join(adaptersDir, 'local-static', 'projection.json'), local);
|
|
156
|
+
writePrettyJsonFile(path.join(adaptersDir, 'netlify', 'projection.json'), netlify);
|
|
75
157
|
fs.writeFileSync(path.join(adaptersDir, 'netlify', '_headers'), String(netlify.files['_headers'] || ''), 'utf8');
|
|
76
158
|
fs.writeFileSync(path.join(adaptersDir, 'netlify', '_redirects'), String(netlify.files['_redirects'] || ''), 'utf8');
|
|
77
159
|
return { policy, adapters: { 'local-static': local, netlify } };
|
|
78
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* @param {string} distDir
|
|
163
|
+
*/
|
|
164
|
+
function loadLocaleArtifact(distDir) {
|
|
165
|
+
const p = path.join(distDir, '_vmz', 'locale-route-realization.json');
|
|
166
|
+
if (!fs.existsSync(p))
|
|
167
|
+
return null;
|
|
168
|
+
try {
|
|
169
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
79
175
|
/**
|
|
80
176
|
* Project vendor-neutral policy to an adapter. Must preserve redirect targets and forbid SPA fallback.
|
|
81
177
|
* @param {Record<string, any>} policy
|