@vmz/vmz 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/build-assemble.d.ts +52 -0
- package/dist/build-assemble.js +191 -0
- package/dist/cdn-policy.d.ts +196 -0
- package/dist/cdn-policy.js +443 -0
- package/dist/cli.js +152 -11
- package/dist/content-addressed-assets.d.ts +69 -0
- package/dist/content-addressed-assets.js +206 -0
- package/dist/delivery-profile.d.ts +74 -0
- package/dist/delivery-profile.js +279 -0
- package/dist/dev-session.js +49 -13
- package/dist/document-build.js +9 -4
- package/dist/document-designs.js +30 -2
- package/dist/document-enrich.js +8 -0
- package/dist/embedded-packaging.d.ts +22 -0
- package/dist/embedded-packaging.js +113 -0
- package/dist/index.d.ts +19 -3
- package/dist/index.js +35 -15
- package/dist/invocation.d.ts +8 -31
- package/dist/invocation.js +12 -33
- package/dist/locale-check.d.ts +16 -0
- package/dist/locale-check.js +131 -3
- package/dist/locale-cmd.js +2 -2
- package/dist/locale-route-emit.d.ts +34 -0
- package/dist/locale-route-emit.js +134 -0
- package/dist/locale-router.d.ts +20 -0
- package/dist/locale-router.js +68 -0
- package/dist/log.d.ts +2 -2
- package/dist/log.js +11 -3
- package/dist/pack.d.ts +40 -0
- package/dist/pack.js +108 -0
- package/dist/plugin-host.d.ts +10 -1
- package/dist/plugin-host.js +19 -2
- package/dist/port.d.ts +10 -0
- package/dist/port.js +46 -0
- package/dist/production-observability.d.ts +286 -0
- package/dist/production-observability.js +469 -0
- package/dist/production-test-pack.d.ts +144 -0
- package/dist/production-test-pack.js +447 -0
- package/dist/release-cmd.d.ts +8 -0
- package/dist/release-cmd.js +126 -0
- package/dist/release-pack.d.ts +96 -0
- package/dist/release-pack.js +346 -0
- package/dist/server-artifact.d.ts +140 -0
- package/dist/server-artifact.js +205 -0
- package/dist/server-language-backend.d.ts +89 -0
- package/dist/server-language-backend.js +121 -0
- package/dist/site-delivery.d.ts +134 -0
- package/dist/site-delivery.js +345 -0
- package/dist/static-emit.d.ts +145 -0
- package/dist/static-emit.js +577 -0
- package/package.json +13 -13
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A3 filesystem release packaging — digests, atomic pointer, rollback, artifact diff.
|
|
3
|
+
*
|
|
4
|
+
* Not the full CDN/StaticDelivery matrix. Same VPG build (`dist/`) is packed into
|
|
5
|
+
* `_vmz/*` manifests + content digests; publish retains previous release for rollback
|
|
6
|
+
* without rebuild.
|
|
7
|
+
*/
|
|
8
|
+
// @ts-nocheck
|
|
9
|
+
import crypto from 'node:crypto';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
export const RELEASE_ENVELOPE_SCHEMA = 'vmz.release.envelope.v0';
|
|
13
|
+
export const APPLICATION_ARTIFACT_SCHEMA = 'vmz.application.artifact.v0';
|
|
14
|
+
export const DELIVERY_ARTIFACT_MANIFEST_SCHEMA = 'vmz.profile.delivery_artifact_manifest.v0';
|
|
15
|
+
export const ROUTE_REALIZATION_TABLE_SCHEMA = 'vmz.profile.route_realization_table.v0';
|
|
16
|
+
export const ARTIFACT_DIFF_SCHEMA = 'vmz.artifact.diff.v0';
|
|
17
|
+
/**
|
|
18
|
+
* @param {Buffer | string} data
|
|
19
|
+
*/
|
|
20
|
+
export function sha256Hex(data) {
|
|
21
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} filePath
|
|
25
|
+
*/
|
|
26
|
+
export function sha256File(filePath) {
|
|
27
|
+
return sha256Hex(fs.readFileSync(filePath));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* @param {unknown} value
|
|
31
|
+
*/
|
|
32
|
+
export function canonicalJson(value) {
|
|
33
|
+
return JSON.stringify(sortKeys(value));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* @param {unknown} value
|
|
37
|
+
* @returns {unknown}
|
|
38
|
+
*/
|
|
39
|
+
function sortKeys(value) {
|
|
40
|
+
if (Array.isArray(value))
|
|
41
|
+
return value.map(sortKeys);
|
|
42
|
+
if (value && typeof value === 'object') {
|
|
43
|
+
/** @type {Record<string, unknown>} */
|
|
44
|
+
const out = {};
|
|
45
|
+
for (const k of Object.keys(value).sort()) {
|
|
46
|
+
out[k] = sortKeys(/** @type {Record<string, unknown>} */ (value)[k]);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* @param {string} distDir
|
|
54
|
+
* @returns {string[]}
|
|
55
|
+
*/
|
|
56
|
+
function listContentFiles(distDir) {
|
|
57
|
+
/** @type {string[]} */
|
|
58
|
+
const out = [];
|
|
59
|
+
const skipDir = new Set(['_vmz', 'node_modules']);
|
|
60
|
+
const skipName = new Set(['vmz-serve-host.mjs', 'vmz-serve-host.js']);
|
|
61
|
+
/** @param {string} abs */
|
|
62
|
+
/** @param {string} rel */
|
|
63
|
+
function walk(abs, rel) {
|
|
64
|
+
let ents;
|
|
65
|
+
try {
|
|
66
|
+
ents = fs.readdirSync(abs, { withFileTypes: true });
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
for (const e of ents) {
|
|
72
|
+
if (e.name.startsWith('.'))
|
|
73
|
+
continue;
|
|
74
|
+
const nextAbs = path.join(abs, e.name);
|
|
75
|
+
const nextRel = rel ? `${rel}/${e.name}` : e.name;
|
|
76
|
+
if (e.isDirectory()) {
|
|
77
|
+
if (skipDir.has(e.name))
|
|
78
|
+
continue;
|
|
79
|
+
walk(nextAbs, nextRel);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (skipName.has(e.name))
|
|
83
|
+
continue;
|
|
84
|
+
out.push(nextRel.replace(/\\/g, '/'));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
walk(distDir, '');
|
|
88
|
+
out.sort();
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
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
|
+
/**
|
|
107
|
+
* Pack `dist/` into `_vmz` manifests + release envelope (filesystem Delivery Profile).
|
|
108
|
+
* @param {string} distDir
|
|
109
|
+
* @param {{ applicationId?: string }} [opts]
|
|
110
|
+
*/
|
|
111
|
+
export function packRelease(distDir, opts = {}) {
|
|
112
|
+
const abs = path.resolve(distDir);
|
|
113
|
+
if (!fs.existsSync(abs)) {
|
|
114
|
+
throw new Error(`packRelease: missing dist ${abs}`);
|
|
115
|
+
}
|
|
116
|
+
const deploymentPath = path.join(abs, 'vmz-deployment.json');
|
|
117
|
+
if (!fs.existsSync(deploymentPath)) {
|
|
118
|
+
throw new Error(`packRelease: missing ${deploymentPath}`);
|
|
119
|
+
}
|
|
120
|
+
const deployment = JSON.parse(fs.readFileSync(deploymentPath, 'utf8'));
|
|
121
|
+
const files = listContentFiles(abs);
|
|
122
|
+
/** @type {Record<string, string>} */
|
|
123
|
+
const fileDigests = {};
|
|
124
|
+
for (const rel of files) {
|
|
125
|
+
fileDigests[rel] = sha256File(path.join(abs, ...rel.split('/')));
|
|
126
|
+
}
|
|
127
|
+
const pages = (deployment.units || []).filter((u) => u.kind === 'page');
|
|
128
|
+
const routeRealization = {
|
|
129
|
+
schema: ROUTE_REALIZATION_TABLE_SCHEMA,
|
|
130
|
+
routes: pages.map((u) => ({
|
|
131
|
+
routeId: String(u.chunkId),
|
|
132
|
+
chunkId: String(u.chunkId),
|
|
133
|
+
pathPattern: pathPatternFromChunk(String(u.chunkId)),
|
|
134
|
+
clientEntry: u.clientEntry || null,
|
|
135
|
+
programIr: u.programIr || null,
|
|
136
|
+
})),
|
|
137
|
+
};
|
|
138
|
+
const programParts = pages
|
|
139
|
+
.map((u) => u.programIr)
|
|
140
|
+
.filter(Boolean)
|
|
141
|
+
.map((rel) => fileDigests[String(rel).replace(/\\/g, '/')] || '')
|
|
142
|
+
.filter(Boolean)
|
|
143
|
+
.sort();
|
|
144
|
+
const programDigest = sha256Hex(programParts.join('|'));
|
|
145
|
+
const styleDigest = typeof deployment.styleBundleHash === 'string' && deployment.styleBundleHash ? deployment.styleBundleHash : null;
|
|
146
|
+
const deploymentDigest = fileDigests['vmz-deployment.json'] || sha256File(deploymentPath);
|
|
147
|
+
const applicationId = opts.applicationId || 'production-router';
|
|
148
|
+
const applicationArtifact = {
|
|
149
|
+
schema: APPLICATION_ARTIFACT_SCHEMA,
|
|
150
|
+
applicationId,
|
|
151
|
+
deliveryProfile: 'filesystem',
|
|
152
|
+
programDigest,
|
|
153
|
+
planDigest: programDigest,
|
|
154
|
+
deploymentDigest,
|
|
155
|
+
styleDigest,
|
|
156
|
+
routeDigest: sha256Hex(canonicalJson(routeRealization)),
|
|
157
|
+
fileDigests,
|
|
158
|
+
publicRouteContracts: routeRealization.routes.map((r) => r.routeId),
|
|
159
|
+
};
|
|
160
|
+
applicationArtifact.integrity = sha256Hex(canonicalJson({ ...applicationArtifact, integrity: undefined }));
|
|
161
|
+
const deliveryManifest = {
|
|
162
|
+
schema: DELIVERY_ARTIFACT_MANIFEST_SCHEMA,
|
|
163
|
+
deliveryProfile: 'filesystem',
|
|
164
|
+
copiesSemanticIr: false,
|
|
165
|
+
applicationId,
|
|
166
|
+
applicationIntegrity: applicationArtifact.integrity,
|
|
167
|
+
routeCount: routeRealization.routes.length,
|
|
168
|
+
assetCount: Object.keys(fileDigests).length,
|
|
169
|
+
styleDigest,
|
|
170
|
+
deploymentDigest,
|
|
171
|
+
};
|
|
172
|
+
const envelopeBody = {
|
|
173
|
+
schema: RELEASE_ENVELOPE_SCHEMA,
|
|
174
|
+
applicationId,
|
|
175
|
+
deliveryProfile: 'filesystem',
|
|
176
|
+
applicationIntegrity: applicationArtifact.integrity,
|
|
177
|
+
deploymentDigest,
|
|
178
|
+
programDigest,
|
|
179
|
+
styleDigest,
|
|
180
|
+
routeDigest: applicationArtifact.routeDigest,
|
|
181
|
+
fileDigests,
|
|
182
|
+
manifests: {
|
|
183
|
+
applicationArtifact: '_vmz/application-artifact.json',
|
|
184
|
+
deliveryArtifactManifest: '_vmz/delivery-artifact-manifest.json',
|
|
185
|
+
routeRealization: '_vmz/route-realization.json',
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
const artifactDigest = sha256Hex(canonicalJson(envelopeBody));
|
|
189
|
+
const envelope = { ...envelopeBody, artifactDigest };
|
|
190
|
+
const vmzDir = path.join(abs, '_vmz');
|
|
191
|
+
fs.mkdirSync(vmzDir, { recursive: true });
|
|
192
|
+
writeJson(path.join(vmzDir, 'application-artifact.json'), applicationArtifact);
|
|
193
|
+
writeJson(path.join(vmzDir, 'delivery-artifact-manifest.json'), deliveryManifest);
|
|
194
|
+
writeJson(path.join(vmzDir, 'route-realization.json'), routeRealization);
|
|
195
|
+
writeJson(path.join(vmzDir, 'release-envelope.json'), envelope);
|
|
196
|
+
return envelope;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* @param {string} file
|
|
200
|
+
* @param {unknown} value
|
|
201
|
+
*/
|
|
202
|
+
function writeJson(file, value) {
|
|
203
|
+
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* @param {string} pointerPath
|
|
207
|
+
* @param {string} digest
|
|
208
|
+
*/
|
|
209
|
+
export function atomicWritePointer(pointerPath, digest) {
|
|
210
|
+
const dir = path.dirname(pointerPath);
|
|
211
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
212
|
+
const tmp = `${pointerPath}.${process.pid}.${Date.now()}.tmp`;
|
|
213
|
+
fs.writeFileSync(tmp, `${digest.trim()}\n`, 'utf8');
|
|
214
|
+
try {
|
|
215
|
+
fs.renameSync(tmp, pointerPath);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
if (fs.existsSync(pointerPath))
|
|
219
|
+
fs.unlinkSync(pointerPath);
|
|
220
|
+
fs.renameSync(tmp, pointerPath);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* @param {string} pointerPath
|
|
225
|
+
* @returns {string | null}
|
|
226
|
+
*/
|
|
227
|
+
export function readPointer(pointerPath) {
|
|
228
|
+
if (!fs.existsSync(pointerPath))
|
|
229
|
+
return null;
|
|
230
|
+
const t = fs.readFileSync(pointerPath, 'utf8').trim();
|
|
231
|
+
return t || null;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Publish packed dist into releases root; retain previous pointer for rollback.
|
|
235
|
+
* @param {string} releasesRoot
|
|
236
|
+
* @param {string} distDir
|
|
237
|
+
* @param {ReturnType<typeof packRelease>} envelope
|
|
238
|
+
*/
|
|
239
|
+
export function publishRelease(releasesRoot, distDir, envelope) {
|
|
240
|
+
const digest = envelope.artifactDigest;
|
|
241
|
+
if (!digest)
|
|
242
|
+
throw new Error('publishRelease: envelope missing artifactDigest');
|
|
243
|
+
const root = path.resolve(releasesRoot);
|
|
244
|
+
const srcDist = path.resolve(distDir);
|
|
245
|
+
// Node fs.cpSync refuses copying a directory into any subdirectory of itself.
|
|
246
|
+
// Releases root must sit beside dist (e.g. .vmz-releases), never under dist/.
|
|
247
|
+
const dest = path.join(root, digest);
|
|
248
|
+
const destDist = path.join(dest, 'dist');
|
|
249
|
+
if (root === srcDist || root.startsWith(srcDist + path.sep) || destDist.startsWith(srcDist + path.sep)) {
|
|
250
|
+
throw new Error(`publishRelease: releasesRoot must not be under distDir (got releasesRoot=${root}, distDir=${srcDist})`);
|
|
251
|
+
}
|
|
252
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
253
|
+
// Immutable snapshot of packed dist (exclude prior releases nesting).
|
|
254
|
+
fs.rmSync(destDist, { recursive: true, force: true });
|
|
255
|
+
fs.cpSync(srcDist, destDist, {
|
|
256
|
+
recursive: true,
|
|
257
|
+
filter: (src) => {
|
|
258
|
+
const n = src.replace(/\\/g, '/');
|
|
259
|
+
return (!n.includes('/.vmz-releases/') &&
|
|
260
|
+
!n.includes('/.vmz-cdn-releases/') &&
|
|
261
|
+
!n.includes('/releases-cdn/') &&
|
|
262
|
+
!/\/dist\/releases(\/|$)/.test(n));
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
writeJson(path.join(dest, 'envelope.json'), envelope);
|
|
266
|
+
const currentPath = path.join(root, 'CURRENT');
|
|
267
|
+
const previousPath = path.join(root, 'PREVIOUS');
|
|
268
|
+
const prev = readPointer(currentPath);
|
|
269
|
+
if (prev && prev !== digest) {
|
|
270
|
+
atomicWritePointer(previousPath, prev);
|
|
271
|
+
}
|
|
272
|
+
atomicWritePointer(currentPath, digest);
|
|
273
|
+
return {
|
|
274
|
+
digest,
|
|
275
|
+
previous: prev && prev !== digest ? prev : readPointer(previousPath),
|
|
276
|
+
currentPath,
|
|
277
|
+
releaseDir: dest,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Rollback CURRENT → PREVIOUS without rebuild.
|
|
282
|
+
* @param {string} releasesRoot
|
|
283
|
+
*/
|
|
284
|
+
export function rollbackRelease(releasesRoot) {
|
|
285
|
+
const root = path.resolve(releasesRoot);
|
|
286
|
+
const currentPath = path.join(root, 'CURRENT');
|
|
287
|
+
const previousPath = path.join(root, 'PREVIOUS');
|
|
288
|
+
const current = readPointer(currentPath);
|
|
289
|
+
const previous = readPointer(previousPath);
|
|
290
|
+
if (!previous) {
|
|
291
|
+
throw new Error('rollbackRelease: no PREVIOUS pointer');
|
|
292
|
+
}
|
|
293
|
+
if (!fs.existsSync(path.join(root, previous, 'envelope.json'))) {
|
|
294
|
+
throw new Error(`rollbackRelease: missing retained release ${previous}`);
|
|
295
|
+
}
|
|
296
|
+
if (current) {
|
|
297
|
+
atomicWritePointer(previousPath, current);
|
|
298
|
+
}
|
|
299
|
+
atomicWritePointer(currentPath, previous);
|
|
300
|
+
return {
|
|
301
|
+
restored: previous,
|
|
302
|
+
demoted: current,
|
|
303
|
+
releaseDir: path.join(root, previous),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Structured diff between two release envelopes / digest maps.
|
|
308
|
+
* @param {{ fileDigests?: Record<string, string>, artifactDigest?: string }} a
|
|
309
|
+
* @param {{ fileDigests?: Record<string, string>, artifactDigest?: string }} b
|
|
310
|
+
*/
|
|
311
|
+
export function diffArtifacts(a, b) {
|
|
312
|
+
const da = a.fileDigests || {};
|
|
313
|
+
const db = b.fileDigests || {};
|
|
314
|
+
const keys = new Set([...Object.keys(da), ...Object.keys(db)]);
|
|
315
|
+
/** @type {string[]} */
|
|
316
|
+
const added = [];
|
|
317
|
+
/** @type {string[]} */
|
|
318
|
+
const removed = [];
|
|
319
|
+
/** @type {Array<{ path: string, before: string, after: string }>} */
|
|
320
|
+
const changed = [];
|
|
321
|
+
for (const k of [...keys].sort()) {
|
|
322
|
+
if (!(k in da))
|
|
323
|
+
added.push(k);
|
|
324
|
+
else if (!(k in db))
|
|
325
|
+
removed.push(k);
|
|
326
|
+
else if (da[k] !== db[k])
|
|
327
|
+
changed.push({ path: k, before: da[k], after: db[k] });
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
schema: ARTIFACT_DIFF_SCHEMA,
|
|
331
|
+
beforeDigest: a.artifactDigest || null,
|
|
332
|
+
afterDigest: b.artifactDigest || null,
|
|
333
|
+
added,
|
|
334
|
+
removed,
|
|
335
|
+
changed,
|
|
336
|
+
identical: added.length === 0 && removed.length === 0 && changed.length === 0,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* @param {string} releasesRoot
|
|
341
|
+
* @param {string} digest
|
|
342
|
+
*/
|
|
343
|
+
export function loadReleaseEnvelope(releasesRoot, digest) {
|
|
344
|
+
const p = path.join(path.resolve(releasesRoot), digest, 'envelope.json');
|
|
345
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
346
|
+
}
|
|
@@ -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,205 @@
|
|
|
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, writeFileSync } from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { SERVER_RUNTIMES } from './delivery-profile.js';
|
|
10
|
+
export const SERVER_ARTIFACT_SCHEMA = 'vmz.server.artifact.v0';
|
|
11
|
+
export const HTTP_CONTRACT_SCHEMA = 'vmz.http.contract.v0';
|
|
12
|
+
export const SERVER_RUNTIME_ADAPTER_SCHEMA = 'vmz.server.runtime_adapter.v0';
|
|
13
|
+
const DEFAULT_RPC_PATH = '/__vmz/rpc';
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} outDir
|
|
16
|
+
* @param {{
|
|
17
|
+
* profileId?: string | null,
|
|
18
|
+
* assembly?: string | null,
|
|
19
|
+
* serverRuntime?: string | null,
|
|
20
|
+
* packDigest?: string | null,
|
|
21
|
+
* }} [opts]
|
|
22
|
+
*/
|
|
23
|
+
export function emitServerArtifact(outDir, opts = {}) {
|
|
24
|
+
const deployment = readJson(path.join(outDir, 'vmz-deployment.json')) || { schema: null, units: [] };
|
|
25
|
+
const routes = readJson(path.join(outDir, 'vmz-routes.json'));
|
|
26
|
+
const routeRows = Array.isArray(routes) ? routes : [];
|
|
27
|
+
const selectedRuntime = normalizeRuntime(opts.serverRuntime);
|
|
28
|
+
const units = Array.isArray(deployment.units) ? deployment.units : [];
|
|
29
|
+
const publicRoutes = routeRows.map((r) => ({
|
|
30
|
+
verb: String(r.verb || 'GET').toUpperCase(),
|
|
31
|
+
path: String(r.path || ''),
|
|
32
|
+
moduleId: String(r.moduleId || ''),
|
|
33
|
+
method: String(r.method || ''),
|
|
34
|
+
className: r.className != null ? String(r.className) : null,
|
|
35
|
+
visibility: 'public',
|
|
36
|
+
kind: 'server-route',
|
|
37
|
+
}));
|
|
38
|
+
const publicKeys = new Set(publicRoutes.map((r) => `${r.moduleId}::${r.method}`));
|
|
39
|
+
/** @type {Array<Record<string, unknown>>} */
|
|
40
|
+
const internalCapabilities = [];
|
|
41
|
+
for (const u of units) {
|
|
42
|
+
const moduleId = u.serverModuleId != null ? String(u.serverModuleId) : '';
|
|
43
|
+
if (!moduleId)
|
|
44
|
+
continue;
|
|
45
|
+
const caps = Array.isArray(u.capabilities) ? u.capabilities.map(String) : [];
|
|
46
|
+
for (const method of caps) {
|
|
47
|
+
const key = `${moduleId}::${method}`;
|
|
48
|
+
if (publicKeys.has(key))
|
|
49
|
+
continue;
|
|
50
|
+
internalCapabilities.push({
|
|
51
|
+
chunkId: String(u.chunkId || ''),
|
|
52
|
+
moduleId,
|
|
53
|
+
method,
|
|
54
|
+
visibility: 'internal',
|
|
55
|
+
kind: 'capability',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const routeDecisionTree = [
|
|
60
|
+
{
|
|
61
|
+
id: 'rpc',
|
|
62
|
+
match: { method: 'POST', path: DEFAULT_RPC_PATH },
|
|
63
|
+
action: 'invoke-rpc',
|
|
64
|
+
visibility: 'internal-transport',
|
|
65
|
+
},
|
|
66
|
+
...publicRoutes.map((r, i) => ({
|
|
67
|
+
id: `public-route-${i}`,
|
|
68
|
+
match: { method: r.verb, path: r.path },
|
|
69
|
+
action: 'invoke-server-route',
|
|
70
|
+
target: { moduleId: r.moduleId, method: r.method },
|
|
71
|
+
visibility: 'public',
|
|
72
|
+
})),
|
|
73
|
+
];
|
|
74
|
+
const httpContractBody = {
|
|
75
|
+
schema: HTTP_CONTRACT_SCHEMA,
|
|
76
|
+
rpcPath: DEFAULT_RPC_PATH,
|
|
77
|
+
publicRoutes: publicRoutes.map((r) => ({
|
|
78
|
+
verb: r.verb,
|
|
79
|
+
path: r.path,
|
|
80
|
+
moduleId: r.moduleId,
|
|
81
|
+
method: r.method,
|
|
82
|
+
})),
|
|
83
|
+
internalCapabilityCount: internalCapabilities.length,
|
|
84
|
+
entry: 'fetch',
|
|
85
|
+
};
|
|
86
|
+
const httpContractDigest = sha256Hex(canonicalJson(httpContractBody));
|
|
87
|
+
const artifact = {
|
|
88
|
+
schema: SERVER_ARTIFACT_SCHEMA,
|
|
89
|
+
profileId: opts.profileId || null,
|
|
90
|
+
assembly: opts.assembly || null,
|
|
91
|
+
selectedRuntime,
|
|
92
|
+
entry: {
|
|
93
|
+
kind: 'fetch',
|
|
94
|
+
standards: ['Request', 'Response', 'Streams', 'AbortSignal'],
|
|
95
|
+
rpcPath: DEFAULT_RPC_PATH,
|
|
96
|
+
},
|
|
97
|
+
httpContract: {
|
|
98
|
+
schema: HTTP_CONTRACT_SCHEMA,
|
|
99
|
+
digest: httpContractDigest,
|
|
100
|
+
},
|
|
101
|
+
publicRoutes,
|
|
102
|
+
internalCapabilities,
|
|
103
|
+
middlewareUnits: [],
|
|
104
|
+
routeDecisionTree,
|
|
105
|
+
deploymentSchema: deployment.schema || null,
|
|
106
|
+
packDigest: opts.packDigest || null,
|
|
107
|
+
adapters: {
|
|
108
|
+
node: { kind: 'node-http', status: 'runtime', entry: 'handleNodeRequest' },
|
|
109
|
+
worker: { kind: 'fetch', status: 'runtime', entry: 'handleFetchRequest' },
|
|
110
|
+
deno: { kind: 'fetch', status: 'projected', entry: 'handleFetchRequest' },
|
|
111
|
+
bun: { kind: 'fetch', status: 'projected', entry: 'handleFetchRequest' },
|
|
112
|
+
'rust-host': { kind: 'contract-projection', status: 'projected', entry: 'fetch' },
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
artifact.artifactDigest = sha256Hex(canonicalJson({ ...artifact, artifactDigest: undefined }));
|
|
116
|
+
const vmzDir = path.join(outDir, '_vmz');
|
|
117
|
+
mkdirSync(vmzDir, { recursive: true });
|
|
118
|
+
const file = path.join(vmzDir, 'server-artifact.json');
|
|
119
|
+
writeFileSync(file, `${JSON.stringify(artifact, null, 2)}\n`, 'utf8');
|
|
120
|
+
const adapterDir = path.join(vmzDir, 'adapters');
|
|
121
|
+
mkdirSync(adapterDir, { recursive: true });
|
|
122
|
+
for (const adapterId of ['worker', 'rust-host']) {
|
|
123
|
+
const projection = projectServerRuntimeAdapter(artifact, adapterId);
|
|
124
|
+
const dir = path.join(adapterDir, adapterId);
|
|
125
|
+
mkdirSync(dir, { recursive: true });
|
|
126
|
+
writeFileSync(path.join(dir, 'adapter.json'), `${JSON.stringify(projection, null, 2)}\n`, 'utf8');
|
|
127
|
+
}
|
|
128
|
+
return { artifact, path: file, httpContractDigest };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* @param {Record<string, any>} artifact
|
|
132
|
+
* @param {string} adapterId
|
|
133
|
+
*/
|
|
134
|
+
export function projectServerRuntimeAdapter(artifact, adapterId) {
|
|
135
|
+
const id = String(adapterId || '').trim();
|
|
136
|
+
if (!SERVER_RUNTIMES.includes(id) && id !== 'worker') {
|
|
137
|
+
throw new Error(`projectServerRuntimeAdapter: unknown adapter ${id}`);
|
|
138
|
+
}
|
|
139
|
+
const base = {
|
|
140
|
+
schema: SERVER_RUNTIME_ADAPTER_SCHEMA,
|
|
141
|
+
adapterId: id,
|
|
142
|
+
artifactDigest: artifact.artifactDigest,
|
|
143
|
+
httpContractDigest: artifact.httpContract?.digest || null,
|
|
144
|
+
spaFallback: false,
|
|
145
|
+
entry: artifact.entry,
|
|
146
|
+
publicRouteCount: Array.isArray(artifact.publicRoutes) ? artifact.publicRoutes.length : 0,
|
|
147
|
+
internalCapabilityCount: Array.isArray(artifact.internalCapabilities)
|
|
148
|
+
? artifact.internalCapabilities.length
|
|
149
|
+
: 0,
|
|
150
|
+
};
|
|
151
|
+
if (id === 'node') {
|
|
152
|
+
return { ...base, host: 'node:http', invoke: 'handleNodeRequest', status: 'runtime' };
|
|
153
|
+
}
|
|
154
|
+
if (id === 'worker' || id === 'deno' || id === 'bun') {
|
|
155
|
+
return {
|
|
156
|
+
...base,
|
|
157
|
+
host: 'fetch',
|
|
158
|
+
invoke: 'handleFetchRequest',
|
|
159
|
+
status: id === 'worker' ? 'runtime' : 'projected',
|
|
160
|
+
note: id === 'worker'
|
|
161
|
+
? 'Fetch entry; live thin gated via worker-shaped subprocess host'
|
|
162
|
+
: 'Fetch contract projection; live runtime not gated',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
// rust-host
|
|
166
|
+
return {
|
|
167
|
+
...base,
|
|
168
|
+
host: 'rust-fetch-consumer',
|
|
169
|
+
invoke: 'fetch',
|
|
170
|
+
status: 'projected',
|
|
171
|
+
note: 'contract projection only — live Rust host binary parity not gated',
|
|
172
|
+
consumes: ['server-artifact.json', 'vmz-routes.json', 'vmz-deployment.json'],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function normalizeRuntime(raw) {
|
|
176
|
+
const v = String(raw || 'node').trim();
|
|
177
|
+
return SERVER_RUNTIMES.includes(v) ? v : 'node';
|
|
178
|
+
}
|
|
179
|
+
function readJson(file) {
|
|
180
|
+
if (!existsSync(file))
|
|
181
|
+
return null;
|
|
182
|
+
try {
|
|
183
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function canonicalJson(value) {
|
|
190
|
+
return JSON.stringify(sortKeys(value));
|
|
191
|
+
}
|
|
192
|
+
function sortKeys(value) {
|
|
193
|
+
if (Array.isArray(value))
|
|
194
|
+
return value.map(sortKeys);
|
|
195
|
+
if (value && typeof value === 'object') {
|
|
196
|
+
const out = {};
|
|
197
|
+
for (const k of Object.keys(value).sort())
|
|
198
|
+
out[k] = sortKeys(value[k]);
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
function sha256Hex(text) {
|
|
204
|
+
return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
|
|
205
|
+
}
|