@vmz/vmz 0.0.2 → 0.0.4

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 (83) hide show
  1. package/README.md +19 -13
  2. package/dist/application-cmd.d.ts +1 -2
  3. package/dist/application-cmd.js +9 -10
  4. package/dist/bundler-adapter.d.ts +2 -3
  5. package/dist/bundler-adapter.js +2 -3
  6. package/dist/cdn-policy.d.ts +178 -0
  7. package/dist/cdn-policy.js +344 -0
  8. package/dist/cli.d.ts +10 -2
  9. package/dist/cli.js +179 -16
  10. package/dist/content-addressed-assets.d.ts +69 -0
  11. package/dist/content-addressed-assets.js +206 -0
  12. package/dist/dev-session.d.ts +2 -2
  13. package/dist/dev-session.js +51 -16
  14. package/dist/document-build.js +1 -2
  15. package/dist/document-check.d.ts +1 -1
  16. package/dist/document-check.js +4 -4
  17. package/dist/document-cmd.d.ts +1 -2
  18. package/dist/document-cmd.js +2 -3
  19. package/dist/document-designs.js +31 -3
  20. package/dist/document-enrich.js +2 -3
  21. package/dist/document-evidence.d.ts +4 -4
  22. package/dist/document-evidence.js +20 -12
  23. package/dist/document-integrate.d.ts +0 -1
  24. package/dist/document-integrate.js +0 -1
  25. package/dist/document-interactive.d.ts +11 -11
  26. package/dist/document-interactive.js +12 -13
  27. package/dist/document-locale.d.ts +1 -1
  28. package/dist/document-locale.js +1 -1
  29. package/dist/document-markdown.d.ts +1 -2
  30. package/dist/document-markdown.js +13 -7
  31. package/dist/document-scan.d.ts +4 -4
  32. package/dist/document-scan.js +4 -4
  33. package/dist/document-schema.d.ts +28 -29
  34. package/dist/document-schema.js +28 -29
  35. package/dist/explain-cmd.js +3 -3
  36. package/dist/index.d.ts +349 -789
  37. package/dist/index.js +109 -85
  38. package/dist/invocation.d.ts +68 -0
  39. package/dist/invocation.js +169 -0
  40. package/dist/locale-check.d.ts +19 -3
  41. package/dist/locale-check.js +130 -6
  42. package/dist/locale-cmd.js +6 -7
  43. package/dist/locale-delivery.d.ts +38 -38
  44. package/dist/locale-delivery.js +39 -40
  45. package/dist/locale-router.d.ts +39 -40
  46. package/dist/locale-router.js +39 -40
  47. package/dist/locale-runtime.d.ts +39 -39
  48. package/dist/locale-runtime.js +44 -45
  49. package/dist/locale-schema.d.ts +1 -2
  50. package/dist/locale-schema.js +1 -2
  51. package/dist/locale-tooling.d.ts +13 -13
  52. package/dist/locale-tooling.js +14 -15
  53. package/dist/log.d.ts +1 -1
  54. package/dist/log.js +1 -1
  55. package/dist/packages.d.ts +1 -2
  56. package/dist/packages.js +1 -2
  57. package/dist/plugin-host.d.ts +11 -3
  58. package/dist/plugin-host.js +21 -5
  59. package/dist/port.d.ts +10 -0
  60. package/dist/port.js +46 -0
  61. package/dist/production-observability.d.ts +286 -0
  62. package/dist/production-observability.js +469 -0
  63. package/dist/production-test-pack.d.ts +158 -0
  64. package/dist/production-test-pack.js +452 -0
  65. package/dist/refactor-cmd.d.ts +1 -1
  66. package/dist/refactor-cmd.js +6 -6
  67. package/dist/release-cmd.d.ts +8 -0
  68. package/dist/release-cmd.js +126 -0
  69. package/dist/release-pack.d.ts +96 -0
  70. package/dist/release-pack.js +337 -0
  71. package/dist/resolve-native-cli.d.ts +14 -0
  72. package/dist/resolve-native-cli.js +84 -0
  73. package/dist/resolve.d.ts +1 -2
  74. package/dist/resolve.js +1 -2
  75. package/dist/site-delivery.d.ts +134 -0
  76. package/dist/site-delivery.js +345 -0
  77. package/dist/static-emit.d.ts +136 -0
  78. package/dist/static-emit.js +463 -0
  79. package/dist/test-cmd.d.ts +2 -2
  80. package/dist/test-cmd.js +37 -17
  81. package/dist/watch-diff.d.ts +1 -1
  82. package/dist/watch-diff.js +1 -1
  83. package/package.json +32 -17
package/README.md CHANGED
@@ -1,4 +1,7 @@
1
- # VMZ CLI
1
+ # `@vmz/vmz` (bin: `vmz`)
2
+
3
+ CLI / N-API toolchain host. Runtime mental entry is **`@vmz/core`**; optional official UI is **`@vmz/ui`**; plugins are
4
+ **`@vmz/plugin-*`**. Shelved umbrella name: `@vmz/vmz`.
2
5
 
3
6
  When a project reaches the point where it has a browser surface, SSR, server work, documents, tests, and several
4
7
  deployment concerns, the usual workflow becomes a chain of tools that each see a different version of the application. A
@@ -22,27 +25,30 @@ With VMZ, a normal workflow is expected to answer all of these from the same sou
22
25
  - **Test:** what did a user interaction actually cause?
23
26
  - **Explain:** why did the compiler make that decision?
24
27
 
25
- Use it when you are adopting VMZ as the application model. It is intentionally not a compatibility compiler for
26
- arbitrary Vue components, React hooks, or legacy VDOM applications; those ecosystems are better served by their native
27
- toolchains.
28
+ Use it when you are adopting VMZ as the application model. The CLI targets VMZ-native source and semantics; Vue, React,
29
+ and legacy VDOM ecosystems keep their own toolchains.
28
30
 
29
31
  ## One tool, several views of the same program
30
32
 
31
- | Workflow | The question it should answer |
32
- |---|---|
33
- | Development | What changed, and which application regions are affected? |
34
- | Checking | Which state, route, server, or lifetime boundary cannot be proven? |
35
- | Building | What belongs in browser, SSR, resume, and server output? |
36
- | Testing | Did the application behave correctly and avoid unrelated work? |
37
- | Documents | Are project documents connected, localized, and deployable? |
33
+ | Workflow | The question it should answer |
34
+ |-------------|--------------------------------------------------------------------|
35
+ | Development | What changed, and which application regions are affected? |
36
+ | Checking | Which state, route, server, or lifetime boundary cannot be proven? |
37
+ | Building | What belongs in browser, SSR, resume, and server output? |
38
+ | Testing | Did the application behave correctly and avoid unrelated work? |
39
+ | Documents | Are project documents connected, localized, and deployable? |
38
40
 
39
41
  ### Designed for the npm world
40
42
 
41
- VMZ does not ask users to abandon JavaScript packaging. Node remains the npm, plugin, development-server, and orchestration host. A long-lived N-API bridge connects that ecosystem to Rust and oxc without reducing semantic analysis to a sequence of tiny file transforms.
43
+ VMZ does not ask users to abandon JavaScript packaging. Node remains the npm, plugin, development-server, and
44
+ orchestration host. A long-lived N-API bridge connects that ecosystem to Rust and oxc without reducing semantic analysis
45
+ to a sequence of tiny file transforms.
42
46
 
43
47
  ### More than pass or fail
44
48
 
45
- The interesting future of the CLI is explanation. A useful compiler should expose the source span, graph edge, owner, deployment boundary, and fallback reason behind a decision. That is a better developer experience than adding more colored output to an opaque build. 🧭
49
+ The interesting future of the CLI is explanation. A useful compiler should expose the source span, graph edge, owner,
50
+ deployment boundary, and fallback reason behind a decision. That is a better developer experience than adding more
51
+ colored output to an opaque build. 🧭
46
52
 
47
53
  ## License
48
54
 
@@ -1,6 +1,5 @@
1
1
  /**
2
- * `vmz application` — Application Collection / Mount (M0–M5).
3
- * Design: `规划设计/vmz/22`
2
+ * `vmz application` — Application Collection / Mount .
4
3
  */
5
4
  /**
6
5
  * @param {string[]} argv
@@ -1,7 +1,6 @@
1
1
  // @ts-nocheck
2
2
  /**
3
- * `vmz application` — Application Collection / Mount (M0–M5).
4
- * Design: `规划设计/vmz/22`
3
+ * `vmz application` — Application Collection / Mount .
5
4
  */
6
5
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
7
6
  import path from 'node:path';
@@ -42,18 +41,18 @@ export async function cmdApplication(argv) {
42
41
  return 1;
43
42
  }
44
43
  function printHelp() {
45
- console.log(`vmz application — Application Collection / Mount (M0–M5)
44
+ console.log(`vmz application — Application Collection / Mount
46
45
 
47
46
  Usage:
48
47
  vmz application check [host] Validate descriptors + applications.config.json5
49
48
  vmz application list [host] List resolved ApplicationIds / collections / mounts
50
49
  vmz application schemas Print frozen protocol catalog JSON
51
- vmz application relocatable [pkg] M1: ApplicationBase / non_relocatable_url proof
52
- vmz application relocate <manifest.json> M1: apply ApplicationBase to relocation manifest
53
- vmz application artifacts [host] M2: ApplicationArtifact + MountTable boundary
54
- vmz application isolation [host] M3: isolation namespaces + failure containment
55
- vmz application composition [host] M4: catalog consumption + cross-app Link hrefs
56
- vmz application dev [host] M5: sessions / affected / proxy / mounted tests / deploy
50
+ vmz application relocatable [pkg] ApplicationBase / non_relocatable_url proof
51
+ vmz application relocate <manifest.json> apply ApplicationBase to relocation manifest
52
+ vmz application artifacts [host] ApplicationArtifact + MountTable boundary
53
+ vmz application isolation [host] isolation namespaces + failure containment
54
+ vmz application composition [host] catalog consumption + cross-app Link hrefs
55
+ vmz application dev [host] sessions / affected / proxy / mounted tests / deploy
57
56
 
58
57
  Options:
59
58
  --json [file] Emit report JSON to stdout or file
@@ -125,7 +124,7 @@ function emitJson(args, json, printHuman) {
125
124
  }
126
125
  function cmdSchemas() {
127
126
  const native = loadNative();
128
- console.log(native.queryApplicationProtocolCatalog());
127
+ console.log(native.queryApplicationProtocolCatalog);
129
128
  return 0;
130
129
  }
131
130
  /**
@@ -1,7 +1,6 @@
1
1
  /**
2
- * Bundler adapter (N4) — consumes Deployment IR; does not invent VMZ semantics.
2
+ * Bundler adapter (session) — consumes Deployment IR; does not invent VMZ semantics.
3
3
  *
4
- * Design: `规划设计/vmz/14` — VPG/Deployment IR → bundler executes pack/minify/assets.
5
4
  * Vite/Rolldown may call these helpers; they must not reverse the arrow.
6
5
  */
7
6
  /**
@@ -54,7 +53,7 @@ export declare function createVitePluginVmzAdapter(options?: {}): {
54
53
  configResolved(): void;
55
54
  };
56
55
  /**
57
- * Thin Rolldown plugin factory (N4.2) — same contract as Vite adapter: read Deployment IR only.
56
+ * Thin Rolldown plugin factory (deployment) — same contract as Vite adapter: read Deployment IR only.
58
57
  * @param {{ outDir?: string, root?: string }} [options]
59
58
  */
60
59
  export declare function createRolldownPluginVmzAdapter(options?: {}): {
@@ -1,8 +1,7 @@
1
1
  // @ts-nocheck
2
2
  /**
3
- * Bundler adapter (N4) — consumes Deployment IR; does not invent VMZ semantics.
3
+ * Bundler adapter (session) — consumes Deployment IR; does not invent VMZ semantics.
4
4
  *
5
- * Design: `规划设计/vmz/14` — VPG/Deployment IR → bundler executes pack/minify/assets.
6
5
  * Vite/Rolldown may call these helpers; they must not reverse the arrow.
7
6
  */
8
7
  import { existsSync, readFileSync } from 'node:fs';
@@ -90,7 +89,7 @@ export function createVitePluginVmzAdapter(options = {}) {
90
89
  };
91
90
  }
92
91
  /**
93
- * Thin Rolldown plugin factory (N4.2) — same contract as Vite adapter: read Deployment IR only.
92
+ * Thin Rolldown plugin factory (deployment) — same contract as Vite adapter: read Deployment IR only.
94
93
  * @param {{ outDir?: string, root?: string }} [options]
95
94
  */
96
95
  export function createRolldownPluginVmzAdapter(options = {}) {
@@ -0,0 +1,178 @@
1
+ /**
2
+ * A3-cdn: vendor-neutral CDN policy (cache / redirect / error) + local static host.
3
+ * Provider adapters only project the same contract — they must not change RouteId/canonical/CSP.
4
+ */
5
+ export declare const CDN_POLICY_MANIFEST_SCHEMA = "vmz.cdn.policy_manifest.v0";
6
+ export declare const CDN_ADAPTER_PROJECTION_SCHEMA = "vmz.cdn.adapter_projection.v0";
7
+ /** HTML: revalidate. Hashed/static assets: long immutable. */
8
+ export declare const CACHE_HTML = "public, max-age=0, must-revalidate";
9
+ export declare const CACHE_ASSET_IMMUTABLE = "public, max-age=31536000, immutable";
10
+ export declare const CACHE_META = "public, max-age=3600";
11
+ /**
12
+ * Build CDNPolicyManifest from StaticDeliveryManifest (+ optional redirects).
13
+ * @param {Record<string, any>} staticManifest
14
+ * @param {{ redirects?: Array<{ from: string, to: string, status?: number, reason?: string }> }} [opts]
15
+ */
16
+ export declare function buildCdnPolicyManifest(staticManifest: any, opts?: {}): {
17
+ schema: string;
18
+ applicationId: any;
19
+ deliveryProfile: string;
20
+ origin: string;
21
+ spaFallback: boolean;
22
+ staticManifestDigest: any;
23
+ redirects: any[];
24
+ headers: ({
25
+ match: string;
26
+ headers: {
27
+ 'cache-control': string;
28
+ 'x-robots-tag'?: undefined;
29
+ };
30
+ } | {
31
+ match: string;
32
+ headers: {
33
+ 'cache-control': string;
34
+ 'x-robots-tag': string;
35
+ };
36
+ })[];
37
+ errorDocuments: any;
38
+ routes: any;
39
+ };
40
+ /**
41
+ * Write CDN policy + adapter projections under dist/_vmz.
42
+ * @param {string} distDir
43
+ * @param {Record<string, any>} staticManifest
44
+ * @param {{ redirects?: Array<{ from: string, to: string, status?: number, reason?: string }> }} [opts]
45
+ */
46
+ export declare function emitCdnPolicy(distDir: any, staticManifest: any, opts?: {}): {
47
+ policy: {
48
+ schema: string;
49
+ applicationId: any;
50
+ deliveryProfile: string;
51
+ origin: string;
52
+ spaFallback: boolean;
53
+ staticManifestDigest: any;
54
+ redirects: any[];
55
+ headers: ({
56
+ match: string;
57
+ headers: {
58
+ 'cache-control': string;
59
+ 'x-robots-tag'?: undefined;
60
+ };
61
+ } | {
62
+ match: string;
63
+ headers: {
64
+ 'cache-control': string;
65
+ 'x-robots-tag': string;
66
+ };
67
+ })[];
68
+ errorDocuments: any;
69
+ routes: any;
70
+ };
71
+ adapters: {
72
+ 'local-static': {
73
+ schema: string;
74
+ adapterId: string;
75
+ policyDigest: any;
76
+ host: string;
77
+ spaFallback: boolean;
78
+ redirects: any;
79
+ headers: any;
80
+ errorDocuments: any;
81
+ files: {
82
+ _headers?: undefined;
83
+ _redirects?: undefined;
84
+ };
85
+ } | {
86
+ schema: string;
87
+ adapterId: string;
88
+ policyDigest: any;
89
+ host: string;
90
+ spaFallback: boolean;
91
+ redirects: any;
92
+ headers: any;
93
+ errorDocuments: any;
94
+ files: {
95
+ _headers: string;
96
+ _redirects: string;
97
+ };
98
+ };
99
+ netlify: {
100
+ schema: string;
101
+ adapterId: string;
102
+ policyDigest: any;
103
+ host: string;
104
+ spaFallback: boolean;
105
+ redirects: any;
106
+ headers: any;
107
+ errorDocuments: any;
108
+ files: {
109
+ _headers?: undefined;
110
+ _redirects?: undefined;
111
+ };
112
+ } | {
113
+ schema: string;
114
+ adapterId: string;
115
+ policyDigest: any;
116
+ host: string;
117
+ spaFallback: boolean;
118
+ redirects: any;
119
+ headers: any;
120
+ errorDocuments: any;
121
+ files: {
122
+ _headers: string;
123
+ _redirects: string;
124
+ };
125
+ };
126
+ };
127
+ };
128
+ /**
129
+ * Project vendor-neutral policy to an adapter. Must preserve redirect targets and forbid SPA fallback.
130
+ * @param {Record<string, any>} policy
131
+ * @param {'local-static' | 'netlify'} adapterId
132
+ */
133
+ export declare function projectCdnAdapter(policy: any, adapterId: any): {
134
+ schema: string;
135
+ adapterId: string;
136
+ policyDigest: any;
137
+ host: string;
138
+ spaFallback: boolean;
139
+ redirects: any;
140
+ headers: any;
141
+ errorDocuments: any;
142
+ files: {
143
+ _headers?: undefined;
144
+ _redirects?: undefined;
145
+ };
146
+ } | {
147
+ schema: string;
148
+ adapterId: string;
149
+ policyDigest: any;
150
+ host: string;
151
+ spaFallback: boolean;
152
+ redirects: any;
153
+ headers: any;
154
+ errorDocuments: any;
155
+ files: {
156
+ _headers: string;
157
+ _redirects: string;
158
+ };
159
+ };
160
+ /**
161
+ * Local static host that applies CDNPolicyManifest (redirects, cache headers, 404 doc).
162
+ * @param {string} distDir
163
+ * @param {Record<string, any>} policy
164
+ * @param {{ host?: string, port?: number }} [opts]
165
+ * @returns {Promise<{ host: string, port: number, baseUrl: string, close: () => Promise<void> }>}
166
+ */
167
+ export declare function listenLocalStaticHost(distDir: any, policy: any, opts?: {}): Promise<unknown>;
168
+ /**
169
+ * @param {string} distDir
170
+ * @param {Record<string, any>} policy
171
+ */
172
+ export declare function createLocalStaticHandler(distDir: any, policy: any): (req: any, res: any) => void;
173
+ /**
174
+ * Minimal glob matcher for CDN header rules (html / js|css|mjs / exact / prefix).
175
+ * @param {string} pattern
176
+ * @param {string} pathname
177
+ */
178
+ export declare function matchGlob(pattern: any, pathname: any): any;
@@ -0,0 +1,344 @@
1
+ /**
2
+ * A3-cdn: vendor-neutral CDN policy (cache / redirect / error) + local static host.
3
+ * Provider adapters only project the same contract — they must not change RouteId/canonical/CSP.
4
+ */
5
+ // @ts-nocheck
6
+ import crypto from 'node:crypto';
7
+ import fs from 'node:fs';
8
+ import http from 'node:http';
9
+ import path from 'node:path';
10
+ export const CDN_POLICY_MANIFEST_SCHEMA = 'vmz.cdn.policy_manifest.v0';
11
+ export const CDN_ADAPTER_PROJECTION_SCHEMA = 'vmz.cdn.adapter_projection.v0';
12
+ /** HTML: revalidate. Hashed/static assets: long immutable. */
13
+ export const CACHE_HTML = 'public, max-age=0, must-revalidate';
14
+ export const CACHE_ASSET_IMMUTABLE = 'public, max-age=31536000, immutable';
15
+ export const CACHE_META = 'public, max-age=3600';
16
+ /**
17
+ * Build CDNPolicyManifest from StaticDeliveryManifest (+ optional redirects).
18
+ * @param {Record<string, any>} staticManifest
19
+ * @param {{ redirects?: Array<{ from: string, to: string, status?: number, reason?: string }> }} [opts]
20
+ */
21
+ export function buildCdnPolicyManifest(staticManifest, opts = {}) {
22
+ const origin = String(staticManifest.origin || '');
23
+ const redirects = [{ from: '/home', to: '/', status: 301, reason: 'canonical-alias' }, ...(opts.redirects || [])];
24
+ const headers = [
25
+ { match: '**/*.html', headers: { 'cache-control': CACHE_HTML } },
26
+ {
27
+ match: '**/404.html',
28
+ headers: { 'cache-control': CACHE_HTML, 'x-robots-tag': 'noindex, nofollow' },
29
+ },
30
+ // Content-addressed immutable objects (A3 assets/<hash>).
31
+ { match: '**/assets/**', headers: { 'cache-control': CACHE_ASSET_IMMUTABLE } },
32
+ { match: '**/*.{js,css,mjs}', headers: { 'cache-control': CACHE_ASSET_IMMUTABLE } },
33
+ { match: '**/sitemap.xml', headers: { 'cache-control': CACHE_META } },
34
+ { match: '**/robots.txt', headers: { 'cache-control': CACHE_META } },
35
+ ];
36
+ const errorDocuments = Array.isArray(staticManifest.errorDocuments) ? staticManifest.errorDocuments : [{ status: 404, path: '404.html' }];
37
+ const body = {
38
+ schema: CDN_POLICY_MANIFEST_SCHEMA,
39
+ applicationId: staticManifest.applicationId || null,
40
+ deliveryProfile: 'web-static',
41
+ origin,
42
+ spaFallback: false,
43
+ staticManifestDigest: staticManifest.manifestDigest || null,
44
+ redirects,
45
+ headers,
46
+ 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
+ })),
53
+ };
54
+ body.policyDigest = sha256Hex(canonicalJson(body));
55
+ return body;
56
+ }
57
+ /**
58
+ * Write CDN policy + adapter projections under dist/_vmz.
59
+ * @param {string} distDir
60
+ * @param {Record<string, any>} staticManifest
61
+ * @param {{ redirects?: Array<{ from: string, to: string, status?: number, reason?: string }> }} [opts]
62
+ */
63
+ export function emitCdnPolicy(distDir, staticManifest, opts = {}) {
64
+ const policy = buildCdnPolicyManifest(staticManifest, opts);
65
+ const vmzDir = path.join(distDir, '_vmz');
66
+ fs.mkdirSync(vmzDir, { recursive: true });
67
+ fs.writeFileSync(path.join(vmzDir, 'cdn-policy-manifest.json'), `${JSON.stringify(policy, null, 2)}\n`, 'utf8');
68
+ const local = projectCdnAdapter(policy, 'local-static');
69
+ const netlify = projectCdnAdapter(policy, 'netlify');
70
+ const adaptersDir = path.join(vmzDir, 'adapters');
71
+ fs.mkdirSync(path.join(adaptersDir, 'local-static'), { recursive: true });
72
+ 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');
75
+ fs.writeFileSync(path.join(adaptersDir, 'netlify', '_headers'), String(netlify.files['_headers'] || ''), 'utf8');
76
+ fs.writeFileSync(path.join(adaptersDir, 'netlify', '_redirects'), String(netlify.files['_redirects'] || ''), 'utf8');
77
+ return { policy, adapters: { 'local-static': local, netlify } };
78
+ }
79
+ /**
80
+ * Project vendor-neutral policy to an adapter. Must preserve redirect targets and forbid SPA fallback.
81
+ * @param {Record<string, any>} policy
82
+ * @param {'local-static' | 'netlify'} adapterId
83
+ */
84
+ export function projectCdnAdapter(policy, adapterId) {
85
+ if (policy.spaFallback) {
86
+ throw new Error('projectCdnAdapter: spaFallback=true is forbidden');
87
+ }
88
+ if (adapterId === 'local-static') {
89
+ return {
90
+ schema: CDN_ADAPTER_PROJECTION_SCHEMA,
91
+ adapterId: 'local-static',
92
+ policyDigest: policy.policyDigest,
93
+ host: 'vmz-local-static',
94
+ spaFallback: false,
95
+ redirects: policy.redirects,
96
+ headers: policy.headers,
97
+ errorDocuments: policy.errorDocuments,
98
+ files: {},
99
+ };
100
+ }
101
+ if (adapterId === 'netlify') {
102
+ const headerLines = [];
103
+ for (const rule of policy.headers || []) {
104
+ const glob = netlifyGlob(rule.match);
105
+ headerLines.push(glob);
106
+ for (const [k, v] of Object.entries(rule.headers || {})) {
107
+ headerLines.push(` ${headerCase(k)}: ${v}`);
108
+ }
109
+ }
110
+ const redirectLines = [];
111
+ for (const r of policy.redirects || []) {
112
+ redirectLines.push(`${r.from} ${r.to} ${Number(r.status) || 301}`);
113
+ }
114
+ // Explicit only — never `/* /index.html 200`
115
+ const redirectsText = redirectLines.join('\n') + (redirectLines.length ? '\n' : '');
116
+ if (/\*\s+\/index\.html/.test(redirectsText)) {
117
+ throw new Error('netlify adapter refused SPA fallback redirect');
118
+ }
119
+ return {
120
+ schema: CDN_ADAPTER_PROJECTION_SCHEMA,
121
+ adapterId: 'netlify',
122
+ policyDigest: policy.policyDigest,
123
+ host: 'netlify',
124
+ spaFallback: false,
125
+ redirects: policy.redirects,
126
+ headers: policy.headers,
127
+ errorDocuments: policy.errorDocuments,
128
+ files: {
129
+ _headers: headerLines.join('\n') + '\n',
130
+ _redirects: redirectsText,
131
+ },
132
+ };
133
+ }
134
+ throw new Error(`unknown CDN adapter ${adapterId}`);
135
+ }
136
+ /**
137
+ * Local static host that applies CDNPolicyManifest (redirects, cache headers, 404 doc).
138
+ * @param {string} distDir
139
+ * @param {Record<string, any>} policy
140
+ * @param {{ host?: string, port?: number }} [opts]
141
+ * @returns {Promise<{ host: string, port: number, baseUrl: string, close: () => Promise<void> }>}
142
+ */
143
+ export function listenLocalStaticHost(distDir, policy, opts = {}) {
144
+ const host = opts.host || '127.0.0.1';
145
+ const port = Number(opts.port || 0);
146
+ const handler = createLocalStaticHandler(distDir, policy);
147
+ const server = http.createServer(handler);
148
+ return new Promise((resolve, reject) => {
149
+ server.listen(port, host, () => {
150
+ const addr = server.address();
151
+ const actualPort = typeof addr === 'object' && addr ? addr.port : port;
152
+ resolve({
153
+ host,
154
+ port: actualPort,
155
+ baseUrl: `http://${host}:${actualPort}`,
156
+ close: () => new Promise((res, rej) => {
157
+ server.close((err) => (err ? rej(err) : res()));
158
+ }),
159
+ });
160
+ });
161
+ server.on('error', reject);
162
+ });
163
+ }
164
+ /**
165
+ * @param {string} distDir
166
+ * @param {Record<string, any>} policy
167
+ */
168
+ export function createLocalStaticHandler(distDir, policy) {
169
+ const root = path.resolve(distDir);
170
+ return (req, res) => {
171
+ try {
172
+ const url = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
173
+ let pathname = decodeURIComponent(url.pathname || '/');
174
+ const redirect = matchRedirect(policy.redirects || [], pathname);
175
+ if (redirect) {
176
+ const status = Number(redirect.status) || 301;
177
+ const headers = applyHeaderRules(policy.headers || [], pathname, {
178
+ Location: redirect.to,
179
+ });
180
+ res.writeHead(status, headers);
181
+ res.end();
182
+ return;
183
+ }
184
+ let rel = pathname;
185
+ if (rel.endsWith('/'))
186
+ rel += 'index.html';
187
+ if (rel === '/')
188
+ rel = '/index.html';
189
+ const file = safeJoin(root, rel.replace(/^\//, ''));
190
+ if (file && fs.existsSync(file) && fs.statSync(file).isFile()) {
191
+ const body = fs.readFileSync(file);
192
+ const type = contentType(file);
193
+ const headers = applyHeaderRules(policy.headers || [], rel, {
194
+ 'content-type': type,
195
+ });
196
+ res.writeHead(200, headers);
197
+ res.end(body);
198
+ return;
199
+ }
200
+ const errDoc = (policy.errorDocuments || []).find((e) => Number(e.status) === 404);
201
+ const errPath = errDoc?.path || '404.html';
202
+ const abs404 = path.join(root, errPath);
203
+ if (fs.existsSync(abs404)) {
204
+ const body = fs.readFileSync(abs404);
205
+ const headers = applyHeaderRules(policy.headers || [], `/${errPath}`, {
206
+ 'content-type': 'text/html; charset=utf-8',
207
+ });
208
+ res.writeHead(404, headers);
209
+ res.end(body);
210
+ return;
211
+ }
212
+ res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
213
+ res.end('not found');
214
+ }
215
+ catch (err) {
216
+ res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
217
+ res.end(err instanceof Error ? err.message : String(err));
218
+ }
219
+ };
220
+ }
221
+ /**
222
+ * @param {Array<{ from: string, to: string, status?: number }>} redirects
223
+ * @param {string} pathname
224
+ */
225
+ function matchRedirect(redirects, pathname) {
226
+ const p = pathname.replace(/\/+$/, '') || '/';
227
+ for (const r of redirects) {
228
+ const from = String(r.from || '').replace(/\/+$/, '') || '/';
229
+ if (from === p || from === pathname)
230
+ return r;
231
+ }
232
+ return null;
233
+ }
234
+ /**
235
+ * @param {Array<{ match: string, headers: Record<string, string> }>} rules
236
+ * @param {string} pathname
237
+ * @param {Record<string, string>} base
238
+ */
239
+ function applyHeaderRules(rules, pathname, base = {}) {
240
+ /** @type {Record<string, string>} */
241
+ const out = { ...base };
242
+ for (const rule of rules) {
243
+ if (matchGlob(rule.match, pathname)) {
244
+ Object.assign(out, rule.headers || {});
245
+ }
246
+ }
247
+ return out;
248
+ }
249
+ /**
250
+ * Minimal glob matcher for CDN header rules (html / js|css|mjs / exact / prefix).
251
+ * @param {string} pattern
252
+ * @param {string} pathname
253
+ */
254
+ export function matchGlob(pattern, pathname) {
255
+ const p = pathname.startsWith('/') ? pathname : `/${pathname}`;
256
+ const pat = String(pattern || '');
257
+ if (pat === '**/*.html')
258
+ return p.endsWith('.html');
259
+ if (pat === '**/404.html')
260
+ return p === '/404.html' || p.endsWith('/404.html');
261
+ if (pat === '**/assets/**')
262
+ return p === '/assets' || p.startsWith('/assets/');
263
+ if (pat === '**/*.{js,css,mjs}')
264
+ return /\.(js|css|mjs)$/.test(p);
265
+ if (pat === '**/sitemap.xml')
266
+ return p.endsWith('/sitemap.xml') || p === '/sitemap.xml';
267
+ if (pat === '**/robots.txt')
268
+ return p.endsWith('/robots.txt') || p === '/robots.txt';
269
+ if (pat.endsWith('/**')) {
270
+ const prefix = pat.slice(0, -3);
271
+ // Only treat as path prefix when pattern is absolute-ish (starts with / or bare segment).
272
+ if (prefix.startsWith('/')) {
273
+ return p === prefix || p.startsWith(prefix + '/');
274
+ }
275
+ if (!prefix.includes('*')) {
276
+ return p === `/${prefix}` || p.startsWith(`/${prefix}/`);
277
+ }
278
+ }
279
+ return p === pat || p === `/${pat.replace(/^\//, '')}`;
280
+ }
281
+ function netlifyGlob(match) {
282
+ if (match === '**/*.html')
283
+ return '/*.html';
284
+ if (match === '**/404.html')
285
+ return '/404.html';
286
+ if (match === '**/assets/**')
287
+ return '/assets/*';
288
+ if (match === '**/*.{js,css,mjs}')
289
+ return '/*.{js,css,mjs}';
290
+ if (match === '**/sitemap.xml')
291
+ return '/sitemap.xml';
292
+ if (match === '**/robots.txt')
293
+ return '/robots.txt';
294
+ return match.startsWith('/') ? match : `/${match}`;
295
+ }
296
+ function headerCase(name) {
297
+ return String(name)
298
+ .split('-')
299
+ .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
300
+ .join('-');
301
+ }
302
+ function contentType(file) {
303
+ if (file.endsWith('.html'))
304
+ return 'text/html; charset=utf-8';
305
+ if (file.endsWith('.js') || file.endsWith('.mjs'))
306
+ return 'text/javascript; charset=utf-8';
307
+ if (file.endsWith('.css'))
308
+ return 'text/css; charset=utf-8';
309
+ if (file.endsWith('.xml'))
310
+ return 'application/xml';
311
+ if (file.endsWith('.txt'))
312
+ return 'text/plain; charset=utf-8';
313
+ if (file.endsWith('.json'))
314
+ return 'application/json; charset=utf-8';
315
+ return 'application/octet-stream';
316
+ }
317
+ /**
318
+ * @param {string} root
319
+ * @param {string} rel
320
+ */
321
+ function safeJoin(root, rel) {
322
+ const full = path.resolve(root, rel);
323
+ const normRoot = path.resolve(root);
324
+ if (full !== normRoot && !full.startsWith(normRoot + path.sep))
325
+ return null;
326
+ return full;
327
+ }
328
+ function sha256Hex(data) {
329
+ return crypto.createHash('sha256').update(data).digest('hex');
330
+ }
331
+ function canonicalJson(value) {
332
+ return JSON.stringify(sortKeys(value));
333
+ }
334
+ function sortKeys(value) {
335
+ if (Array.isArray(value))
336
+ return value.map(sortKeys);
337
+ if (value && typeof value === 'object') {
338
+ const out = {};
339
+ for (const k of Object.keys(value).sort())
340
+ out[k] = sortKeys(value[k]);
341
+ return out;
342
+ }
343
+ return value;
344
+ }