@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
@@ -19,8 +19,6 @@ export declare function browserProductionScenarioPack(): {
19
19
  runner: string;
20
20
  required: boolean;
21
21
  detail?: undefined;
22
- quarantine?: undefined;
23
- reason?: undefined;
24
22
  } | {
25
23
  scenarioId: string;
26
24
  category: string;
@@ -29,18 +27,6 @@ export declare function browserProductionScenarioPack(): {
29
27
  runner: string;
30
28
  required: boolean;
31
29
  detail: string;
32
- quarantine?: undefined;
33
- reason?: undefined;
34
- } | {
35
- scenarioId: string;
36
- category: string;
37
- fixture: any;
38
- modes: string[];
39
- runner: string;
40
- required: boolean;
41
- quarantine: boolean;
42
- reason: string;
43
- detail?: undefined;
44
30
  })[];
45
31
  };
46
32
  /** Deterministic CI profile for production-test (no flaky disguise). */
@@ -8,6 +8,7 @@ import crypto from 'node:crypto';
8
8
  import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import { canonicalJson, sha256Hex } from './release-pack.js';
11
+ import { writePrettyJsonFile } from './pretty-json.js';
11
12
  export const PRODUCTION_SCENARIO_PACK_SCHEMA = 'vmz.production.scenario_pack.v0';
12
13
  export const PRODUCTION_CI_PROFILE_SCHEMA = 'vmz.production.ci_profile.v0';
13
14
  export const PRODUCTION_TEST_REPORT_SCHEMA = 'vmz.production.test_report.v0';
@@ -126,56 +127,51 @@ export function browserProductionScenarioPack() {
126
127
  required: true,
127
128
  detail: 'pack digest + CURRENT/PREVIOUS rollback',
128
129
  },
129
- // Explicit quarantinemust never be reported as passed.
130
+ // Previously quarantinednow required with real serve-host/browser runners.
130
131
  {
131
132
  scenarioId: 'production.ui.field.submit',
132
133
  category: 'field',
133
- fixture: null,
134
+ fixture: 'packages/examples/production-inspector',
134
135
  modes: ['browser'],
135
- runner: 'quarantine',
136
- required: false,
137
- quarantine: true,
138
- reason: 'A4: Field input/submit/dup-submit/cancel not assembled (@vmz/ui UI1)',
136
+ runner: 'serve-host-browser',
137
+ required: true,
138
+ detail: 'Field input + validation error on inspector',
139
139
  },
140
140
  {
141
141
  scenarioId: 'production.ui.dialog.focus',
142
142
  category: 'dialog',
143
- fixture: null,
143
+ fixture: 'packages/examples/production-inspector',
144
144
  modes: ['browser'],
145
- runner: 'quarantine',
146
- required: false,
147
- quarantine: true,
148
- reason: 'A4: Dialog focus enter/loop/restore + outside dismiss not assembled',
145
+ runner: 'serve-host-browser',
146
+ required: true,
147
+ detail: 'Dialog open/focus/Escape dismiss on inspector',
149
148
  },
150
149
  {
151
150
  scenarioId: 'production.locale.switch-rtl',
152
151
  category: 'locale',
153
- fixture: null,
152
+ fixture: 'packages/examples/production-router',
154
153
  modes: ['browser'],
155
- runner: 'quarantine',
156
- required: false,
157
- quarantine: true,
158
- reason: 'A4: locale switch / fallback forbidden / RTL not in this pack',
154
+ runner: 'serve-host-browser',
155
+ required: true,
156
+ detail: 'LocaleTransition commit + inspector RTL dir toggle',
159
157
  },
160
158
  {
161
159
  scenarioId: 'production.theme.missing-token',
162
160
  category: 'theme',
163
- fixture: null,
164
- modes: ['browser'],
165
- runner: 'quarantine',
166
- required: false,
167
- quarantine: true,
168
- reason: 'A4: theme switch + missing semantic token not in this pack',
161
+ fixture: 'temp:missing-token',
162
+ modes: ['compile'],
163
+ runner: 'vmz-build',
164
+ required: true,
165
+ detail: 'missing semantic token → build fails unknown_design_token',
169
166
  },
170
167
  {
171
168
  scenarioId: 'production.mount.child-failure',
172
169
  category: 'mount',
173
- fixture: null,
174
- modes: ['browser'],
175
- runner: 'quarantine',
176
- required: false,
177
- quarantine: true,
178
- reason: 'A4: ApplicationMount child failure isolation not in this pack',
170
+ fixture: 'temp:application-isolation',
171
+ modes: ['deployment'],
172
+ runner: 'application-isolation',
173
+ required: true,
174
+ detail: 'ApplicationMount child failure → 503 application_unavailable; siblings survive',
179
175
  },
180
176
  ],
181
177
  };
@@ -436,9 +432,9 @@ export function emitProductionTestArtifacts(root, report, pack, profile) {
436
432
  const reportPath = path.join(dir, 'report.json');
437
433
  const packPath = path.join(dir, 'scenario-pack.json');
438
434
  const profilePath = path.join(dir, 'ci-profile.json');
439
- fs.writeFileSync(reportPath, `${JSON.stringify(stamped, null, 2)}\n`, 'utf8');
440
- fs.writeFileSync(packPath, `${JSON.stringify(pack, null, 2)}\n`, 'utf8');
441
- fs.writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)}\n`, 'utf8');
435
+ writePrettyJsonFile(reportPath, stamped);
436
+ writePrettyJsonFile(packPath, pack);
437
+ writePrettyJsonFile(profilePath, profile);
442
438
  return { reportPath, packPath, profilePath, report: stamped };
443
439
  }
444
440
  /** Assert CI profile forbids JS test-runner disguise. */
@@ -9,6 +9,8 @@
9
9
  import crypto from 'node:crypto';
10
10
  import fs from 'node:fs';
11
11
  import path from 'node:path';
12
+ import { writePrettyJsonFile } from './pretty-json.js';
13
+ import { listPublicPageUnits, unitBrowserPathPattern } from './route-path.js';
12
14
  export const RELEASE_ENVELOPE_SCHEMA = 'vmz.release.envelope.v0';
13
15
  export const APPLICATION_ARTIFACT_SCHEMA = 'vmz.application.artifact.v0';
14
16
  export const DELIVERY_ARTIFACT_MANIFEST_SCHEMA = 'vmz.profile.delivery_artifact_manifest.v0';
@@ -88,21 +90,6 @@ function listContentFiles(distDir) {
88
90
  out.sort();
89
91
  return out;
90
92
  }
91
- /**
92
- * @param {string} chunkId
93
- */
94
- function pathPatternFromChunk(chunkId) {
95
- const rel = chunkId.replace(/^pages\//, '');
96
- const parts = rel.split('/').filter(Boolean);
97
- const segs = [];
98
- for (let i = 0; i < parts.length; i++) {
99
- const p = parts[i];
100
- if (p === 'index' && i === parts.length - 1)
101
- continue;
102
- segs.push(p);
103
- }
104
- return segs.length ? `/${segs.join('/')}` : '/';
105
- }
106
93
  /**
107
94
  * Pack `dist/` into `_vmz` manifests + release envelope (filesystem Delivery Profile).
108
95
  * @param {string} distDir
@@ -124,13 +111,13 @@ export function packRelease(distDir, opts = {}) {
124
111
  for (const rel of files) {
125
112
  fileDigests[rel] = sha256File(path.join(abs, ...rel.split('/')));
126
113
  }
127
- const pages = (deployment.units || []).filter((u) => u.kind === 'page');
114
+ const pages = listPublicPageUnits(deployment);
128
115
  const routeRealization = {
129
116
  schema: ROUTE_REALIZATION_TABLE_SCHEMA,
130
117
  routes: pages.map((u) => ({
131
118
  routeId: String(u.chunkId),
132
119
  chunkId: String(u.chunkId),
133
- pathPattern: pathPatternFromChunk(String(u.chunkId)),
120
+ pathPattern: unitBrowserPathPattern(u),
134
121
  clientEntry: u.clientEntry || null,
135
122
  programIr: u.programIr || null,
136
123
  })),
@@ -200,7 +187,7 @@ export function packRelease(distDir, opts = {}) {
200
187
  * @param {unknown} value
201
188
  */
202
189
  function writeJson(file, value) {
203
- fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
190
+ writePrettyJsonFile(file, value);
204
191
  }
205
192
  /**
206
193
  * @param {string} pointerPath
@@ -241,16 +228,25 @@ export function publishRelease(releasesRoot, distDir, envelope) {
241
228
  if (!digest)
242
229
  throw new Error('publishRelease: envelope missing artifactDigest');
243
230
  const root = path.resolve(releasesRoot);
231
+ const srcDist = path.resolve(distDir);
232
+ // Node fs.cpSync refuses copying a directory into any subdirectory of itself.
233
+ // Releases root must sit beside dist (e.g. .vmz-releases), never under dist/.
244
234
  const dest = path.join(root, digest);
235
+ const destDist = path.join(dest, 'dist');
236
+ if (root === srcDist || root.startsWith(srcDist + path.sep) || destDist.startsWith(srcDist + path.sep)) {
237
+ throw new Error(`publishRelease: releasesRoot must not be under distDir (got releasesRoot=${root}, distDir=${srcDist})`);
238
+ }
245
239
  fs.mkdirSync(dest, { recursive: true });
246
240
  // Immutable snapshot of packed dist (exclude prior releases nesting).
247
- const destDist = path.join(dest, 'dist');
248
241
  fs.rmSync(destDist, { recursive: true, force: true });
249
- fs.cpSync(path.resolve(distDir), destDist, {
242
+ fs.cpSync(srcDist, destDist, {
250
243
  recursive: true,
251
244
  filter: (src) => {
252
245
  const n = src.replace(/\\/g, '/');
253
- return !n.includes('/.vmz-releases/') && !n.includes('/dist/releases');
246
+ return (!n.includes('/.vmz-releases/') &&
247
+ !n.includes('/.vmz-cdn-releases/') &&
248
+ !n.includes('/releases-cdn/') &&
249
+ !/\/dist\/releases(\/|$)/.test(n));
254
250
  },
255
251
  });
256
252
  writeJson(path.join(dest, 'envelope.json'), envelope);
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Browser HTTP path projection from Route Graph / `vmz-deployment.json`.
3
+ * Mini pack ignores this and lowers RouteId → chunk id → page stem.
4
+ */
5
+ export declare function isRouteBoundaryStem(stem: string): boolean;
6
+ export declare function isRouteGroupDir(seg: string): boolean;
7
+ /**
8
+ * File-route fallback (`pages/home` → `/home`, `pages/index` → `/`).
9
+ * Used only when a deployment unit has no `pathPattern`.
10
+ */
11
+ export declare function filePathPatternFromChunk(chunkId: string): string;
12
+ export type DeploymentPageUnit = {
13
+ kind?: string;
14
+ chunkId?: string;
15
+ clientEntry?: string;
16
+ programIr?: string;
17
+ pathPattern?: string;
18
+ routeId?: string;
19
+ };
20
+ /** Canonical Browser HTTP pattern for a page unit. Mini must not read this. */
21
+ export declare function unitBrowserPathPattern(unit: DeploymentPageUnit | null | undefined): string;
22
+ export type PathSeg = {
23
+ kind: 'static';
24
+ value: string;
25
+ } | {
26
+ kind: 'param';
27
+ name: string;
28
+ } | {
29
+ kind: 'catch';
30
+ name: string;
31
+ };
32
+ export declare function parsePathPattern(pattern: string): PathSeg[];
33
+ export declare function listPublicPageUnits(deployment: {
34
+ units?: DeploymentPageUnit[];
35
+ } | null | undefined): DeploymentPageUnit[];
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Browser HTTP path projection from Route Graph / `vmz-deployment.json`.
3
+ * Mini pack ignores this and lowers RouteId → chunk id → page stem.
4
+ */
5
+ export function isRouteBoundaryStem(stem) {
6
+ return stem === 'Layout' || stem === 'Loading' || stem === 'Error' || stem === 'NotFound';
7
+ }
8
+ export function isRouteGroupDir(seg) {
9
+ return typeof seg === 'string' && seg.startsWith('(') && seg.endsWith(')') && seg.length > 2;
10
+ }
11
+ /**
12
+ * File-route fallback (`pages/home` → `/home`, `pages/index` → `/`).
13
+ * Used only when a deployment unit has no `pathPattern`.
14
+ */
15
+ export function filePathPatternFromChunk(chunkId) {
16
+ const rel = String(chunkId || '').replace(/^pages\//, '');
17
+ const parts = rel.split('/').filter(Boolean);
18
+ const segs = [];
19
+ for (let i = 0; i < parts.length; i++) {
20
+ const p = parts[i];
21
+ if (isRouteGroupDir(p))
22
+ continue;
23
+ if (p === 'index' && i === parts.length - 1)
24
+ continue;
25
+ if (isRouteBoundaryStem(p))
26
+ continue;
27
+ segs.push(p);
28
+ }
29
+ return segs.length ? `/${segs.join('/')}` : '/';
30
+ }
31
+ /** Canonical Browser HTTP pattern for a page unit. Mini must not read this. */
32
+ export function unitBrowserPathPattern(unit) {
33
+ const explicit = String(unit?.pathPattern || '').trim();
34
+ if (explicit)
35
+ return explicit.startsWith('/') ? explicit : `/${explicit}`;
36
+ return filePathPatternFromChunk(String(unit?.chunkId || ''));
37
+ }
38
+ export function parsePathPattern(pattern) {
39
+ const raw = String(pattern || '').trim();
40
+ if (!raw || raw === '/')
41
+ return [];
42
+ const parts = raw.replace(/^\/+/, '').replace(/\/+$/, '').split('/').filter(Boolean);
43
+ const segs = [];
44
+ for (const p of parts) {
45
+ if (isRouteGroupDir(p))
46
+ continue;
47
+ segs.push(parsePathSegment(p));
48
+ }
49
+ return segs;
50
+ }
51
+ function parsePathSegment(p) {
52
+ const catchAll = /^\[\.\.\.([^\]]+)\]$/.exec(p);
53
+ const star = /^\*([A-Za-z_][\w]*)$/.exec(p);
54
+ const param = /^\[([^\]]+)\]$/.exec(p);
55
+ const colon = /^:([A-Za-z_][\w]*)$/.exec(p);
56
+ if (catchAll)
57
+ return { kind: 'catch', name: catchAll[1] };
58
+ if (star)
59
+ return { kind: 'catch', name: star[1] };
60
+ if (param)
61
+ return { kind: 'param', name: param[1] };
62
+ if (colon)
63
+ return { kind: 'param', name: colon[1] };
64
+ return { kind: 'static', value: p.toLowerCase() };
65
+ }
66
+ export function listPublicPageUnits(deployment) {
67
+ const units = Array.isArray(deployment?.units) ? deployment.units : [];
68
+ return units.filter((u) => {
69
+ if (u?.kind !== 'page')
70
+ return false;
71
+ const chunkId = String(u.chunkId || '').replace(/\\/g, '/');
72
+ if (!chunkId.startsWith('pages/'))
73
+ return false;
74
+ const stem = chunkId.split('/').pop() || '';
75
+ return !isRouteBoundaryStem(stem);
76
+ });
77
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * P4 ServerArtifact — compiled route decision tree + public ServerRoute contracts
3
+ * + internal capability units + selected runtime adapter. Web Standards Fetch entry.
4
+ */
5
+ export declare const SERVER_ARTIFACT_SCHEMA = "vmz.server.artifact.v0";
6
+ export declare const HTTP_CONTRACT_SCHEMA = "vmz.http.contract.v0";
7
+ export declare const SERVER_RUNTIME_ADAPTER_SCHEMA = "vmz.server.runtime_adapter.v0";
8
+ /**
9
+ * @param {string} outDir
10
+ * @param {{
11
+ * profileId?: string | null,
12
+ * assembly?: string | null,
13
+ * serverRuntime?: string | null,
14
+ * packDigest?: string | null,
15
+ * }} [opts]
16
+ */
17
+ export declare function emitServerArtifact(outDir: any, opts?: {}): {
18
+ artifact: {
19
+ schema: string;
20
+ profileId: any;
21
+ assembly: any;
22
+ selectedRuntime: string;
23
+ entry: {
24
+ kind: string;
25
+ standards: string[];
26
+ rpcPath: string;
27
+ };
28
+ httpContract: {
29
+ schema: string;
30
+ digest: string;
31
+ };
32
+ publicRoutes: {
33
+ verb: string;
34
+ path: string;
35
+ moduleId: string;
36
+ method: string;
37
+ className: string;
38
+ visibility: string;
39
+ kind: string;
40
+ }[];
41
+ internalCapabilities: any[];
42
+ middlewareUnits: any[];
43
+ routeDecisionTree: ({
44
+ id: string;
45
+ match: {
46
+ method: string;
47
+ path: string;
48
+ };
49
+ action: string;
50
+ target: {
51
+ moduleId: string;
52
+ method: string;
53
+ };
54
+ visibility: string;
55
+ } | {
56
+ id: string;
57
+ match: {
58
+ method: string;
59
+ path: string;
60
+ };
61
+ action: string;
62
+ visibility: string;
63
+ })[];
64
+ deploymentSchema: any;
65
+ packDigest: any;
66
+ adapters: {
67
+ node: {
68
+ kind: string;
69
+ status: string;
70
+ entry: string;
71
+ };
72
+ worker: {
73
+ kind: string;
74
+ status: string;
75
+ entry: string;
76
+ };
77
+ deno: {
78
+ kind: string;
79
+ status: string;
80
+ entry: string;
81
+ };
82
+ bun: {
83
+ kind: string;
84
+ status: string;
85
+ entry: string;
86
+ };
87
+ 'rust-host': {
88
+ kind: string;
89
+ status: string;
90
+ entry: string;
91
+ };
92
+ };
93
+ };
94
+ path: string;
95
+ httpContractDigest: string;
96
+ };
97
+ /**
98
+ * @param {Record<string, any>} artifact
99
+ * @param {string} adapterId
100
+ */
101
+ export declare function projectServerRuntimeAdapter(artifact: any, adapterId: any): {
102
+ host: string;
103
+ invoke: string;
104
+ status: string;
105
+ schema: string;
106
+ adapterId: string;
107
+ artifactDigest: any;
108
+ httpContractDigest: any;
109
+ spaFallback: boolean;
110
+ entry: any;
111
+ publicRouteCount: any;
112
+ internalCapabilityCount: any;
113
+ } | {
114
+ host: string;
115
+ invoke: string;
116
+ status: string;
117
+ note: string;
118
+ schema: string;
119
+ adapterId: string;
120
+ artifactDigest: any;
121
+ httpContractDigest: any;
122
+ spaFallback: boolean;
123
+ entry: any;
124
+ publicRouteCount: any;
125
+ internalCapabilityCount: any;
126
+ } | {
127
+ host: string;
128
+ invoke: string;
129
+ status: string;
130
+ note: string;
131
+ consumes: string[];
132
+ schema: string;
133
+ adapterId: string;
134
+ artifactDigest: any;
135
+ httpContractDigest: any;
136
+ spaFallback: boolean;
137
+ entry: any;
138
+ publicRouteCount: any;
139
+ internalCapabilityCount: any;
140
+ };
@@ -0,0 +1,204 @@
1
+ /**
2
+ * P4 ServerArtifact — compiled route decision tree + public ServerRoute contracts
3
+ * + internal capability units + selected runtime adapter. Web Standards Fetch entry.
4
+ */
5
+ // @ts-nocheck
6
+ import crypto from 'node:crypto';
7
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
8
+ import path from 'node:path';
9
+ import { SERVER_RUNTIMES } from './delivery-profile.js';
10
+ import { writePrettyJsonFile } from './pretty-json.js';
11
+ export const SERVER_ARTIFACT_SCHEMA = 'vmz.server.artifact.v0';
12
+ export const HTTP_CONTRACT_SCHEMA = 'vmz.http.contract.v0';
13
+ export const SERVER_RUNTIME_ADAPTER_SCHEMA = 'vmz.server.runtime_adapter.v0';
14
+ const DEFAULT_RPC_PATH = '/__vmz/rpc';
15
+ /**
16
+ * @param {string} outDir
17
+ * @param {{
18
+ * profileId?: string | null,
19
+ * assembly?: string | null,
20
+ * serverRuntime?: string | null,
21
+ * packDigest?: string | null,
22
+ * }} [opts]
23
+ */
24
+ export function emitServerArtifact(outDir, opts = {}) {
25
+ const deployment = readJson(path.join(outDir, 'vmz-deployment.json')) || { schema: null, units: [] };
26
+ const routes = readJson(path.join(outDir, 'vmz-routes.json'));
27
+ const routeRows = Array.isArray(routes) ? routes : [];
28
+ const selectedRuntime = normalizeRuntime(opts.serverRuntime);
29
+ const units = Array.isArray(deployment.units) ? deployment.units : [];
30
+ const publicRoutes = routeRows.map((r) => ({
31
+ verb: String(r.verb || 'GET').toUpperCase(),
32
+ path: String(r.path || ''),
33
+ moduleId: String(r.moduleId || ''),
34
+ method: String(r.method || ''),
35
+ className: r.className != null ? String(r.className) : null,
36
+ visibility: 'public',
37
+ kind: 'server-route',
38
+ }));
39
+ const publicKeys = new Set(publicRoutes.map((r) => `${r.moduleId}::${r.method}`));
40
+ /** @type {Array<Record<string, unknown>>} */
41
+ const internalCapabilities = [];
42
+ for (const u of units) {
43
+ const moduleId = u.serverModuleId != null ? String(u.serverModuleId) : '';
44
+ if (!moduleId)
45
+ continue;
46
+ const caps = Array.isArray(u.capabilities) ? u.capabilities.map(String) : [];
47
+ for (const method of caps) {
48
+ const key = `${moduleId}::${method}`;
49
+ if (publicKeys.has(key))
50
+ continue;
51
+ internalCapabilities.push({
52
+ chunkId: String(u.chunkId || ''),
53
+ moduleId,
54
+ method,
55
+ visibility: 'internal',
56
+ kind: 'capability',
57
+ });
58
+ }
59
+ }
60
+ const routeDecisionTree = [
61
+ {
62
+ id: 'rpc',
63
+ match: { method: 'POST', path: DEFAULT_RPC_PATH },
64
+ action: 'invoke-rpc',
65
+ visibility: 'internal-transport',
66
+ },
67
+ ...publicRoutes.map((r, i) => ({
68
+ id: `public-route-${i}`,
69
+ match: { method: r.verb, path: r.path },
70
+ action: 'invoke-server-route',
71
+ target: { moduleId: r.moduleId, method: r.method },
72
+ visibility: 'public',
73
+ })),
74
+ ];
75
+ const httpContractBody = {
76
+ schema: HTTP_CONTRACT_SCHEMA,
77
+ rpcPath: DEFAULT_RPC_PATH,
78
+ publicRoutes: publicRoutes.map((r) => ({
79
+ verb: r.verb,
80
+ path: r.path,
81
+ moduleId: r.moduleId,
82
+ method: r.method,
83
+ })),
84
+ internalCapabilityCount: internalCapabilities.length,
85
+ entry: 'fetch',
86
+ };
87
+ const httpContractDigest = sha256Hex(canonicalJson(httpContractBody));
88
+ const artifact = {
89
+ schema: SERVER_ARTIFACT_SCHEMA,
90
+ profileId: opts.profileId || null,
91
+ assembly: opts.assembly || null,
92
+ selectedRuntime,
93
+ entry: {
94
+ kind: 'fetch',
95
+ standards: ['Request', 'Response', 'Streams', 'AbortSignal'],
96
+ rpcPath: DEFAULT_RPC_PATH,
97
+ },
98
+ httpContract: {
99
+ schema: HTTP_CONTRACT_SCHEMA,
100
+ digest: httpContractDigest,
101
+ },
102
+ publicRoutes,
103
+ internalCapabilities,
104
+ middlewareUnits: [],
105
+ routeDecisionTree,
106
+ deploymentSchema: deployment.schema || null,
107
+ packDigest: opts.packDigest || null,
108
+ adapters: {
109
+ node: { kind: 'node-http', status: 'runtime', entry: 'handleNodeRequest' },
110
+ worker: { kind: 'fetch', status: 'runtime', entry: 'handleFetchRequest' },
111
+ deno: { kind: 'fetch', status: 'projected', entry: 'handleFetchRequest' },
112
+ bun: { kind: 'fetch', status: 'projected', entry: 'handleFetchRequest' },
113
+ 'rust-host': { kind: 'contract-projection', status: 'projected', entry: 'fetch' },
114
+ },
115
+ };
116
+ artifact.artifactDigest = sha256Hex(canonicalJson({ ...artifact, artifactDigest: undefined }));
117
+ const vmzDir = path.join(outDir, '_vmz');
118
+ mkdirSync(vmzDir, { recursive: true });
119
+ const file = path.join(vmzDir, 'server-artifact.json');
120
+ writePrettyJsonFile(file, artifact);
121
+ const adapterDir = path.join(vmzDir, 'adapters');
122
+ mkdirSync(adapterDir, { recursive: true });
123
+ for (const adapterId of ['worker', 'rust-host']) {
124
+ const projection = projectServerRuntimeAdapter(artifact, adapterId);
125
+ const dir = path.join(adapterDir, adapterId);
126
+ mkdirSync(dir, { recursive: true });
127
+ writePrettyJsonFile(path.join(dir, 'adapter.json'), projection);
128
+ }
129
+ return { artifact, path: file, httpContractDigest };
130
+ }
131
+ /**
132
+ * @param {Record<string, any>} artifact
133
+ * @param {string} adapterId
134
+ */
135
+ export function projectServerRuntimeAdapter(artifact, adapterId) {
136
+ const id = String(adapterId || '').trim();
137
+ if (!SERVER_RUNTIMES.includes(id) && id !== 'worker') {
138
+ throw new Error(`projectServerRuntimeAdapter: unknown adapter ${id}`);
139
+ }
140
+ const base = {
141
+ schema: SERVER_RUNTIME_ADAPTER_SCHEMA,
142
+ adapterId: id,
143
+ artifactDigest: artifact.artifactDigest,
144
+ httpContractDigest: artifact.httpContract?.digest || null,
145
+ spaFallback: false,
146
+ entry: artifact.entry,
147
+ publicRouteCount: Array.isArray(artifact.publicRoutes) ? artifact.publicRoutes.length : 0,
148
+ internalCapabilityCount: Array.isArray(artifact.internalCapabilities) ? artifact.internalCapabilities.length : 0,
149
+ };
150
+ if (id === 'node') {
151
+ return { ...base, host: 'node:http', invoke: 'handleNodeRequest', status: 'runtime' };
152
+ }
153
+ if (id === 'worker' || id === 'deno' || id === 'bun') {
154
+ return {
155
+ ...base,
156
+ host: 'fetch',
157
+ invoke: 'handleFetchRequest',
158
+ status: id === 'worker' ? 'runtime' : 'projected',
159
+ note: id === 'worker'
160
+ ? 'Fetch entry; live thin gated via worker-shaped subprocess host'
161
+ : 'Fetch contract projection; live runtime not gated',
162
+ };
163
+ }
164
+ // rust-host
165
+ return {
166
+ ...base,
167
+ host: 'rust-fetch-consumer',
168
+ invoke: 'fetch',
169
+ status: 'projected',
170
+ note: 'contract projection only — live Rust host binary parity not gated',
171
+ consumes: ['server-artifact.json', 'vmz-routes.json', 'vmz-deployment.json'],
172
+ };
173
+ }
174
+ function normalizeRuntime(raw) {
175
+ const v = String(raw || 'node').trim();
176
+ return SERVER_RUNTIMES.includes(v) ? v : 'node';
177
+ }
178
+ function readJson(file) {
179
+ if (!existsSync(file))
180
+ return null;
181
+ try {
182
+ return JSON.parse(readFileSync(file, 'utf8'));
183
+ }
184
+ catch {
185
+ return null;
186
+ }
187
+ }
188
+ function canonicalJson(value) {
189
+ return JSON.stringify(sortKeys(value));
190
+ }
191
+ function sortKeys(value) {
192
+ if (Array.isArray(value))
193
+ return value.map(sortKeys);
194
+ if (value && typeof value === 'object') {
195
+ const out = {};
196
+ for (const k of Object.keys(value).sort())
197
+ out[k] = sortKeys(value[k]);
198
+ return out;
199
+ }
200
+ return value;
201
+ }
202
+ function sha256Hex(text) {
203
+ return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
204
+ }