@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.
Files changed (55) hide show
  1. package/dist/build-assemble.d.ts +52 -0
  2. package/dist/build-assemble.js +192 -0
  3. package/dist/cdn-policy.d.ts +22 -4
  4. package/dist/cdn-policy.js +109 -13
  5. package/dist/cli.js +122 -27
  6. package/dist/content-addressed-assets.js +2 -1
  7. package/dist/delivery-profile.d.ts +84 -0
  8. package/dist/delivery-profile.js +348 -0
  9. package/dist/dev-session.d.ts +6 -0
  10. package/dist/dev-session.js +167 -40
  11. package/dist/document-build.js +33 -32
  12. package/dist/document-cmd.js +2 -9
  13. package/dist/document-enrich.js +8 -0
  14. package/dist/document-integrate.js +10 -17
  15. package/dist/embedded-packaging.d.ts +22 -0
  16. package/dist/embedded-packaging.js +109 -0
  17. package/dist/index.d.ts +26 -1
  18. package/dist/index.js +69 -2
  19. package/dist/locale-check.js +39 -66
  20. package/dist/locale-cmd.js +12 -44
  21. package/dist/locale-route-emit.d.ts +37 -0
  22. package/dist/locale-route-emit.js +109 -0
  23. package/dist/locale-router.d.ts +20 -0
  24. package/dist/locale-router.js +68 -0
  25. package/dist/log.d.ts +2 -2
  26. package/dist/log.js +11 -3
  27. package/dist/mini-host.d.ts +47 -0
  28. package/dist/mini-host.js +202 -0
  29. package/dist/native-addon.d.ts +9 -0
  30. package/dist/native-addon.js +84 -0
  31. package/dist/pack-client-packages.d.ts +25 -0
  32. package/dist/pack-client-packages.js +399 -0
  33. package/dist/pack.d.ts +58 -0
  34. package/dist/pack.js +123 -0
  35. package/dist/plugin-host.d.ts +1 -1
  36. package/dist/plugin-host.js +2 -2
  37. package/dist/pretty-json.d.ts +19 -0
  38. package/dist/pretty-json.js +43 -0
  39. package/dist/production-observability.js +3 -2
  40. package/dist/production-test-pack.d.ts +0 -14
  41. package/dist/production-test-pack.js +27 -31
  42. package/dist/release-pack.js +17 -21
  43. package/dist/route-path.d.ts +35 -0
  44. package/dist/route-path.js +77 -0
  45. package/dist/server-artifact.d.ts +140 -0
  46. package/dist/server-artifact.js +204 -0
  47. package/dist/server-language-backend.d.ts +89 -0
  48. package/dist/server-language-backend.js +121 -0
  49. package/dist/site-delivery.js +3 -2
  50. package/dist/static-emit.d.ts +10 -1
  51. package/dist/static-emit.js +192 -97
  52. package/dist/test-cmd.js +2 -1
  53. package/dist/wechat-packaging.d.ts +22 -0
  54. package/dist/wechat-packaging.js +59 -0
  55. 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
+ }
@@ -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 {{ redirects?: Array<{ from: string, to: string, status?: number, reason?: string }> }} [opts]
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': {
@@ -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 {{ redirects?: Array<{ from: string, to: string, status?: number, reason?: string }> }} [opts]
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 redirects = [{ from: '/home', to: '/', status: 301, reason: 'canonical-alias' }, ...(opts.redirects || [])];
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: (staticManifest.routes || []).map((r) => ({
48
- routeId: r.routeId,
49
- path: r.path,
50
- htmlPath: r.htmlPath,
51
- canonical: r.seo?.canonical || null,
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 policy = buildCdnPolicyManifest(staticManifest, opts);
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
- fs.writeFileSync(path.join(vmzDir, 'cdn-policy-manifest.json'), `${JSON.stringify(policy, null, 2)}\n`, 'utf8');
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
- fs.writeFileSync(path.join(adaptersDir, 'local-static', 'projection.json'), `${JSON.stringify(local, null, 2)}\n`, 'utf8');
74
- fs.writeFileSync(path.join(adaptersDir, 'netlify', 'projection.json'), `${JSON.stringify(netlify, null, 2)}\n`, 'utf8');
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