@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
@@ -0,0 +1,463 @@
1
+ /**
2
+ * A3-static: emit per-route HTML + 404 + SEO head + StaticDeliveryManifest.
3
+ * Reuses the same Direct SSR path as serve-host (no second SSG runtime).
4
+ */
5
+ // @ts-nocheck
6
+ import crypto from 'node:crypto';
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { pathToFileURL } from 'node:url';
10
+ import { emitCdnPolicy } from './cdn-policy.js';
11
+ import { emitContentAddressedAssets } from './content-addressed-assets.js';
12
+ export const STATIC_DELIVERY_MANIFEST_SCHEMA = 'vmz.static.delivery_manifest.v0';
13
+ /**
14
+ * @param {string} distDir
15
+ * @param {{
16
+ * origin?: string,
17
+ * applicationId?: string,
18
+ * staticParams?: Record<string, Array<Record<string, string>>>,
19
+ * }} [opts]
20
+ */
21
+ export async function emitWebStatic(distDir, opts = {}) {
22
+ const origin = String(opts.origin || process.env.VMZ_SITE_ORIGIN || 'https://example.test').replace(/\/$/, '');
23
+ const applicationId = opts.applicationId || path.basename(path.dirname(distDir));
24
+ const domPath = path.join(distDir, 'vmz-dom.js');
25
+ if (!fs.existsSync(domPath)) {
26
+ throw new Error(`emitWebStatic: missing ${domPath} — run vmz build first`);
27
+ }
28
+ const { renderToString, renderToStream } = await import(pathToFileURL(domPath).href);
29
+ const pageCatalog = listPageClientFiles(distDir);
30
+ /** @type {Array<{
31
+ * routeId: string,
32
+ * path: string,
33
+ * chunkId: string,
34
+ * htmlPath: string,
35
+ * classification: string,
36
+ * title: string,
37
+ * description: string,
38
+ * canonical: string,
39
+ * robots: string,
40
+ * }>} */
41
+ const generations = [];
42
+ /** @type {Array<{ routeId: string, path: string, chunkId: string, classification: string, reason: string }>} */
43
+ const skipped = [];
44
+ for (const page of pageCatalog) {
45
+ const pattern = patternFromSegs(page.segs);
46
+ const routeId = guessRouteId(distDir, page.chunkId);
47
+ if (page.segs.some((s) => s.kind === 'param' || s.kind === 'catch')) {
48
+ skipped.push({
49
+ routeId,
50
+ path: pattern,
51
+ chunkId: page.chunkId,
52
+ classification: 'ServerRequired',
53
+ reason: 'dynamic params require explicit StaticRouteSource (not in this thin slice)',
54
+ });
55
+ continue;
56
+ }
57
+ const Page = await loadCtor(distDir, page.chunkId);
58
+ if (!Page) {
59
+ skipped.push({
60
+ routeId,
61
+ path: pattern,
62
+ chunkId: page.chunkId,
63
+ classification: 'UnsupportedForStatic',
64
+ reason: 'missing page ctor',
65
+ });
66
+ continue;
67
+ }
68
+ const params = {};
69
+ if (typeof Page.access === 'function') {
70
+ const access = await Page.access({ params, pathname: pattern, chunkId: page.chunkId, method: 'GET' });
71
+ const kind = access && typeof access === 'object' ? String(access.kind || 'allow') : 'allow';
72
+ if (kind !== 'allow') {
73
+ skipped.push({
74
+ routeId,
75
+ path: pattern,
76
+ chunkId: page.chunkId,
77
+ classification: 'ServerRequired',
78
+ reason: `access result ${kind} is request-bound`,
79
+ });
80
+ continue;
81
+ }
82
+ }
83
+ let props = { ...params };
84
+ if (typeof Page.load === 'function') {
85
+ const loaded = await Page.load({
86
+ params,
87
+ pathname: pattern,
88
+ chunkId: page.chunkId,
89
+ searchParams: new URLSearchParams(),
90
+ });
91
+ if (loaded && typeof loaded === 'object' && !Array.isArray(loaded)) {
92
+ props = { ...props, ...loaded };
93
+ }
94
+ }
95
+ const meta = await resolvePageMeta(Page, { params, props, pathname: pattern, origin });
96
+ const layoutChain = resolveLayoutChain(distDir, page.chunkId);
97
+ let bodyHtml = '';
98
+ for await (const chunk of renderToStream(Page, props, {})) {
99
+ bodyHtml += chunk;
100
+ }
101
+ for (let i = layoutChain.length - 1; i >= 0; i--) {
102
+ const Layout = await loadCtor(distDir, layoutChain[i]);
103
+ if (!Layout)
104
+ continue;
105
+ bodyHtml = await renderToString(Layout, {}, { slotHtml: bodyHtml });
106
+ }
107
+ const htmlPath = htmlPathForRoute(pattern);
108
+ const absHtml = path.join(distDir, htmlPath);
109
+ fs.mkdirSync(path.dirname(absHtml), { recursive: true });
110
+ const html = wrapDocument({
111
+ bodyHtml,
112
+ chunkId: page.chunkId,
113
+ layoutChain,
114
+ props,
115
+ meta,
116
+ cssEntry: readCssEntry(distDir),
117
+ });
118
+ fs.writeFileSync(absHtml, html, 'utf8');
119
+ generations.push({
120
+ routeId,
121
+ path: pattern,
122
+ chunkId: page.chunkId,
123
+ htmlPath: htmlPath.replaceAll('\\', '/'),
124
+ classification: 'Static',
125
+ title: meta.title,
126
+ description: meta.description,
127
+ canonical: meta.canonical,
128
+ robots: meta.robots,
129
+ });
130
+ }
131
+ const notFoundHtml = wrapDocument({
132
+ bodyHtml: '<main><h1>Not Found</h1><p>route-static-404</p></main>',
133
+ chunkId: '',
134
+ layoutChain: [],
135
+ props: {},
136
+ meta: {
137
+ title: 'Not Found',
138
+ description: 'Page not found',
139
+ canonical: `${origin}/404`,
140
+ robots: 'noindex,nofollow',
141
+ lang: 'en',
142
+ },
143
+ cssEntry: readCssEntry(distDir),
144
+ isErrorDocument: true,
145
+ });
146
+ fs.writeFileSync(path.join(distDir, '404.html'), notFoundHtml, 'utf8');
147
+ const sitemap = buildSitemap(origin, generations);
148
+ fs.writeFileSync(path.join(distDir, 'sitemap.xml'), sitemap, 'utf8');
149
+ const robots = `User-agent: *\nAllow: /\nDisallow: /404\nSitemap: ${origin}/sitemap.xml\n`;
150
+ fs.writeFileSync(path.join(distDir, 'robots.txt'), robots, 'utf8');
151
+ // Hard rule: no SPA fallback shim in artifact.
152
+ for (const bad of ['_redirects', 'vercel.json', 'netlify.toml']) {
153
+ const p = path.join(distDir, bad);
154
+ if (fs.existsSync(p)) {
155
+ const text = fs.readFileSync(p, 'utf8');
156
+ if (/\/\*|\bspa\b|index\.html/i.test(text) && /fallback|rewrite|redirects/i.test(text)) {
157
+ throw new Error(`emitWebStatic: forbidden SPA fallback config present: ${bad}`);
158
+ }
159
+ }
160
+ }
161
+ const vmzDir = path.join(distDir, '_vmz');
162
+ fs.mkdirSync(vmzDir, { recursive: true });
163
+ const manifest = {
164
+ schema: STATIC_DELIVERY_MANIFEST_SCHEMA,
165
+ applicationId,
166
+ deliveryProfile: 'web-static',
167
+ origin,
168
+ generatedAt: new Date().toISOString(),
169
+ spaFallback: false,
170
+ errorDocuments: [{ status: 404, path: '404.html' }],
171
+ routes: generations.map((g) => ({
172
+ routeId: g.routeId,
173
+ path: g.path,
174
+ chunkId: g.chunkId,
175
+ htmlPath: g.htmlPath,
176
+ classification: g.classification,
177
+ seo: {
178
+ title: g.title,
179
+ description: g.description,
180
+ canonical: g.canonical,
181
+ robots: g.robots,
182
+ },
183
+ })),
184
+ skipped,
185
+ seoArtifacts: {
186
+ sitemap: 'sitemap.xml',
187
+ robots: 'robots.txt',
188
+ },
189
+ };
190
+ const digest = sha256Hex(canonicalJson(manifest));
191
+ manifest.manifestDigest = digest;
192
+ fs.writeFileSync(path.join(vmzDir, 'static-delivery-manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
193
+ const assets = emitContentAddressedAssets(distDir);
194
+ manifest.contentAddressedAssets = {
195
+ schema: assets.manifest.schema,
196
+ manifestDigest: assets.manifest.manifestDigest,
197
+ objectCount: assets.manifest.objectCount,
198
+ layout: assets.manifest.layout,
199
+ };
200
+ // Re-stamp static manifest after linking asset digest (HTML already rewritten on disk).
201
+ delete manifest.manifestDigest;
202
+ manifest.manifestDigest = sha256Hex(canonicalJson(manifest));
203
+ fs.writeFileSync(path.join(vmzDir, 'static-delivery-manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
204
+ const cdn = emitCdnPolicy(distDir, manifest);
205
+ return {
206
+ manifest,
207
+ htmlFiles: generations.map((g) => g.htmlPath),
208
+ skipped,
209
+ digest: manifest.manifestDigest,
210
+ assets: assets.manifest,
211
+ cdnPolicy: cdn.policy,
212
+ cdnAdapters: cdn.adapters,
213
+ };
214
+ }
215
+ /**
216
+ * @param {string} data
217
+ */
218
+ function sha256Hex(data) {
219
+ return crypto.createHash('sha256').update(data).digest('hex');
220
+ }
221
+ /**
222
+ * @param {unknown} value
223
+ */
224
+ function canonicalJson(value) {
225
+ return JSON.stringify(sortKeys(value));
226
+ }
227
+ function sortKeys(value) {
228
+ if (Array.isArray(value))
229
+ return value.map(sortKeys);
230
+ if (value && typeof value === 'object') {
231
+ /** @type {Record<string, unknown>} */
232
+ const out = {};
233
+ for (const k of Object.keys(value).sort())
234
+ out[k] = sortKeys(value[k]);
235
+ return out;
236
+ }
237
+ return value;
238
+ }
239
+ /**
240
+ * @param {string} distDir
241
+ */
242
+ function listPageClientFiles(distDir) {
243
+ const root = path.join(distDir, 'pages');
244
+ /** @type {Array<{ chunkId: string, segs: ReturnType<typeof parseChunkSegments> }>} */
245
+ const out = [];
246
+ function walk(abs, relParts) {
247
+ let ents;
248
+ try {
249
+ ents = fs.readdirSync(abs, { withFileTypes: true });
250
+ }
251
+ catch {
252
+ return;
253
+ }
254
+ for (const e of ents) {
255
+ if (e.isDirectory())
256
+ walk(path.join(abs, e.name), [...relParts, e.name]);
257
+ else if (e.isFile() && e.name.endsWith('.client.js')) {
258
+ const stem = e.name.replace(/\.client\.js$/, '');
259
+ if (stem === 'Layout' || stem === 'Loading' || stem === 'Error' || stem === 'NotFound')
260
+ continue;
261
+ const chunkId = ['pages', ...relParts, stem].join('/');
262
+ out.push({ chunkId, segs: parseChunkSegments(chunkId) });
263
+ }
264
+ }
265
+ }
266
+ walk(root, []);
267
+ return out;
268
+ }
269
+ /**
270
+ * @param {string} chunkId
271
+ */
272
+ function parseChunkSegments(chunkId) {
273
+ const rel = chunkId.replace(/^pages\//, '');
274
+ const parts = rel.split('/').filter(Boolean);
275
+ /** @type {Array<{ kind: 'static' | 'param' | 'catch', value?: string, name?: string }>} */
276
+ const segs = [];
277
+ for (let i = 0; i < parts.length; i++) {
278
+ const p = parts[i];
279
+ if (p.startsWith('(') && p.endsWith(')') && p.length > 2)
280
+ continue;
281
+ if (p === 'index' && i === parts.length - 1)
282
+ continue;
283
+ const catchAll = /^\[\.\.\.([^\]]+)\]$/.exec(p);
284
+ const param = /^\[([^\]]+)\]$/.exec(p);
285
+ if (catchAll)
286
+ segs.push({ kind: 'catch', name: catchAll[1] });
287
+ else if (param)
288
+ segs.push({ kind: 'param', name: param[1] });
289
+ else
290
+ segs.push({ kind: 'static', value: p.toLowerCase() });
291
+ }
292
+ return segs;
293
+ }
294
+ /**
295
+ * @param {ReturnType<typeof parseChunkSegments>} segs
296
+ */
297
+ function patternFromSegs(segs) {
298
+ if (!segs.length)
299
+ return '/';
300
+ return `/${segs
301
+ .map((s) => {
302
+ if (s.kind === 'static')
303
+ return s.value;
304
+ if (s.kind === 'param')
305
+ return `[${s.name}]`;
306
+ return `[...${s.name}]`;
307
+ })
308
+ .join('/')}`;
309
+ }
310
+ /**
311
+ * @param {string} pathname
312
+ */
313
+ function htmlPathForRoute(pathname) {
314
+ const p = pathname === '/' ? '' : pathname.replace(/^\//, '').replace(/\/+$/, '');
315
+ if (!p)
316
+ return 'index.html';
317
+ return path.join(...p.split('/'), 'index.html');
318
+ }
319
+ /**
320
+ * @param {string} distDir
321
+ * @param {string} chunkId
322
+ */
323
+ async function loadCtor(distDir, chunkId) {
324
+ const href = pathToFileURL(path.join(distDir, `${chunkId}.client.js`)).href;
325
+ const mod = await import(`${href}?t=${Date.now()}`);
326
+ return mod.default;
327
+ }
328
+ /**
329
+ * @param {string} distDir
330
+ * @param {string} pageChunkId
331
+ */
332
+ function resolveLayoutChain(distDir, pageChunkId) {
333
+ const rel = pageChunkId.replace(/^pages\//, '');
334
+ const parts = rel.split('/').filter(Boolean);
335
+ parts.pop();
336
+ /** @type {string[]} */
337
+ const chain = [];
338
+ for (let i = parts.length; i >= 0; i--) {
339
+ const dirParts = parts.slice(0, i);
340
+ const layoutChunk = ['pages', ...dirParts, 'Layout'].join('/');
341
+ if (fs.existsSync(path.join(distDir, `${layoutChunk}.client.js`)))
342
+ chain.unshift(layoutChunk);
343
+ }
344
+ return chain;
345
+ }
346
+ /**
347
+ * @param {string} distDir
348
+ * @param {string} chunkId
349
+ */
350
+ function guessRouteId(distDir, chunkId) {
351
+ try {
352
+ const js = fs.readFileSync(path.join(distDir, `${chunkId}.client.js`), 'utf8');
353
+ const m = /export default class (\w+)/.exec(js);
354
+ if (m)
355
+ return m[1];
356
+ }
357
+ catch {
358
+ /* ignore */
359
+ }
360
+ return chunkId.split('/').pop() || chunkId;
361
+ }
362
+ /**
363
+ * @param {any} Page
364
+ * @param {{ params: Record<string, string>, props: Record<string, unknown>, pathname: string, origin: string }} ctx
365
+ */
366
+ async function resolvePageMeta(Page, ctx) {
367
+ let raw = {};
368
+ if (typeof Page.meta === 'function') {
369
+ raw = (await Page.meta(ctx)) || {};
370
+ }
371
+ else if (Page.meta && typeof Page.meta === 'object') {
372
+ raw = Page.meta;
373
+ }
374
+ const title = String(raw.title || `${guessTitle(ctx.pathname)} · VMZ`);
375
+ const description = String(raw.description || `VMZ page ${ctx.pathname}`);
376
+ const canonical = String(raw.canonical || `${ctx.origin}${ctx.pathname === '/' ? '/' : ctx.pathname}`);
377
+ const robots = String(raw.robots || 'index,follow');
378
+ const lang = String(raw.lang || 'en');
379
+ return { title, description, canonical, robots, lang };
380
+ }
381
+ function guessTitle(pathname) {
382
+ if (pathname === '/')
383
+ return 'Home';
384
+ return pathname
385
+ .split('/')
386
+ .filter(Boolean)
387
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
388
+ .join(' / ');
389
+ }
390
+ /**
391
+ * @param {string} distDir
392
+ */
393
+ function readCssEntry(distDir) {
394
+ try {
395
+ const dep = JSON.parse(fs.readFileSync(path.join(distDir, 'vmz-deployment.json'), 'utf8'));
396
+ return dep.cssEntry || null;
397
+ }
398
+ catch {
399
+ return null;
400
+ }
401
+ }
402
+ /**
403
+ * @param {{
404
+ * bodyHtml: string,
405
+ * chunkId: string,
406
+ * layoutChain: string[],
407
+ * props: Record<string, unknown>,
408
+ * meta: { title: string, description: string, canonical: string, robots: string, lang: string },
409
+ * cssEntry: string | null,
410
+ * isErrorDocument?: boolean,
411
+ * }} input
412
+ */
413
+ function wrapDocument(input) {
414
+ const propsJson = JSON.stringify(input.props ?? {});
415
+ const layoutAttr = input.layoutChain.length ? ` data-vmz-layout="${escapeAttr(input.layoutChain.join(','))}"` : '';
416
+ const pageAttr = input.chunkId ? ` data-vmz-page="${escapeAttr(input.chunkId)}"` : '';
417
+ const cssLink = input.cssEntry ? ` <link rel="stylesheet" href="/${String(input.cssEntry).replace(/^\/+/, '')}" />\n` : '';
418
+ const entry = input.isErrorDocument ? '' : ` <script type="module" src="/entry-client.js"></script>\n`;
419
+ return `<!DOCTYPE html>
420
+ <html lang="${escapeAttr(input.meta.lang)}">
421
+ <head>
422
+ <meta charset="utf-8" />
423
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
424
+ <title>${escapeHtml(input.meta.title)}</title>
425
+ <meta name="description" content="${escapeAttr(input.meta.description)}" />
426
+ <meta name="robots" content="${escapeAttr(input.meta.robots)}" />
427
+ <link rel="canonical" href="${escapeAttr(input.meta.canonical)}" />
428
+ <meta property="og:title" content="${escapeAttr(input.meta.title)}" />
429
+ <meta property="og:description" content="${escapeAttr(input.meta.description)}" />
430
+ <meta property="og:url" content="${escapeAttr(input.meta.canonical)}" />
431
+ ${cssLink}</head>
432
+ <body>
433
+ <div id="app"${pageAttr}${layoutAttr} data-vmz-props="${escapeAttr(propsJson)}">${input.bodyHtml}</div>
434
+ ${entry}</body>
435
+ </html>
436
+ `;
437
+ }
438
+ /**
439
+ * @param {string} origin
440
+ * @param {Array<{ canonical: string, robots: string }>} generations
441
+ */
442
+ function buildSitemap(origin, generations) {
443
+ const urls = generations
444
+ .filter((g) => !String(g.robots).includes('noindex'))
445
+ .map((g) => ` <url>
446
+ <loc>${escapeXml(g.canonical)}</loc>
447
+ </url>`)
448
+ .join('\n');
449
+ return `<?xml version="1.0" encoding="UTF-8"?>
450
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
451
+ ${urls}
452
+ </urlset>
453
+ `;
454
+ }
455
+ function escapeHtml(s) {
456
+ return String(s).replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
457
+ }
458
+ function escapeAttr(s) {
459
+ return escapeHtml(s).replaceAll('"', '&quot;');
460
+ }
461
+ function escapeXml(s) {
462
+ return escapeAttr(s).replaceAll("'", '&apos;');
463
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * `vmz test` command 鈥?discovery / build / filter / TestReport orchestration.
3
- * Semantics live in `@vmz/test`.
2
+ * `vmz test` command discovery / build / filter / TestReport orchestration.
3
+ * Semantics live in `@vmz/test` (optional peer — not installed with bare `@vmz/vmz`).
4
4
  */
5
5
  /**
6
6
  * @param {Record<string, string | boolean> & { _: string[] }} args
package/dist/test-cmd.js CHANGED
@@ -1,17 +1,37 @@
1
1
  // @ts-nocheck
2
2
  /**
3
- * `vmz test` command 鈥?discovery / build / filter / TestReport orchestration.
4
- * Semantics live in `@vmz/test`.
3
+ * `vmz test` command discovery / build / filter / TestReport orchestration.
4
+ * Semantics live in `@vmz/test` (optional peer — not installed with bare `@vmz/vmz`).
5
5
  */
6
6
  import path from 'node:path';
7
- import { discoverTestManifests, buildTestReport, parseModes, buildForCompile, runCompileManifest, runLogicManifest, runSsrManifest, runResumeManifest, runBrowserManifest, runDeploymentManifest, } from '@vmz/test';
8
7
  import { createWorkspace } from './index.js';
9
8
  import { log } from './log.js';
9
+ /**
10
+ * @returns {Promise<typeof import('@vmz/test')>}
11
+ */
12
+ async function loadTestPackage() {
13
+ try {
14
+ return await import('@vmz/test');
15
+ }
16
+ catch (e) {
17
+ const detail = e instanceof Error ? e.message : String(e);
18
+ throw new Error('`vmz test` needs `@vmz/test` (optional peer of `@vmz/vmz`).\n' + ' Install: pnpm add -D @vmz/test\n' + ` Detail: ${detail}`);
19
+ }
20
+ }
10
21
  /**
11
22
  * @param {Record<string, string | boolean> & { _: string[] }} args
12
23
  * @returns {Promise<number>}
13
24
  */
14
25
  export async function cmdTest(args) {
26
+ let test;
27
+ try {
28
+ test = await loadTestPackage();
29
+ }
30
+ catch (e) {
31
+ log.error(e instanceof Error ? e.message : String(e));
32
+ return 1;
33
+ }
34
+ const { discoverTestManifests, buildTestReport, parseModes, buildForCompile, runCompileManifest, runLogicManifest, runSsrManifest, runResumeManifest, runBrowserManifest, runDeploymentManifest, } = test;
15
35
  const project = path.resolve(String(args._[0] || '.'));
16
36
  let modes;
17
37
  try {
@@ -74,7 +94,7 @@ export async function cmdTest(args) {
74
94
  const ws = createWorkspace({ root: project, outDir });
75
95
  try {
76
96
  if (typeof ws.selectTestsAffected !== 'function') {
77
- log.error('selectTestsAffected missing on Workspace 鈥?rebuild native (`pnpm napi:build`)');
97
+ log.error('selectTestsAffected missing on Workspace rebuild native (`pnpm napi:build`)');
78
98
  return 1;
79
99
  }
80
100
  const raw = ws.selectTestsAffected();
@@ -85,14 +105,14 @@ export async function cmdTest(args) {
85
105
  log.error(`test selection not JSON: ${e}`);
86
106
  return 1;
87
107
  }
88
- log.info(`affected selection: ${testSelection.status} 鈥?${testSelection.reason} (chunks=${(testSelection.affectedChunkIds || []).length})`);
108
+ log.info(`affected selection: ${testSelection.status} — ${testSelection.reason} (chunks=${(testSelection.affectedChunkIds || []).length})`);
89
109
  const ids = new Set((testSelection.testIds || []).map(String));
90
110
  const chunks = new Set((testSelection.affectedChunkIds || []).map(String));
91
111
  if (ids.size > 0) {
92
112
  selected = selected.filter((m) => ids.has(String(m.id || '')));
93
113
  }
94
114
  else if (chunks.size > 0) {
95
- // Scaffold fallback: match manifest program.chunkId until graph鈫抰est edges exist.
115
+ // Scaffold fallback: match manifest program.chunkId until graph->test edges exist.
96
116
  selected = selected.filter((m) => {
97
117
  const program = m.program && typeof m.program === 'object' ? m.program : {};
98
118
  const chunk = program.chunkId ? String(program.chunkId) : '';
@@ -115,13 +135,13 @@ export async function cmdTest(args) {
115
135
  selected = selected.filter((m) => Array.isArray(m.modes) && m.modes.some((x) => modes.includes(x)));
116
136
  }
117
137
  /** @type {Array<{
118
- * testId: string,
119
- * file: string,
120
- * modes: string[],
121
- * programId: string|null,
122
- * planId: string|null,
123
- * status: string,
124
- * diagnostics: unknown[],
138
+ * testId: string,
139
+ * file: string,
140
+ * modes: string[],
141
+ * programId: string|null,
142
+ * planId: string|null,
143
+ * status: string,
144
+ * diagnostics: unknown[],
125
145
  * }>} */
126
146
  let tests;
127
147
  if (wantList) {
@@ -288,7 +308,7 @@ export async function cmdTest(args) {
288
308
  else {
289
309
  console.log(`vmz test: ${tests.length} test(s) under ${project}`);
290
310
  for (const t of tests) {
291
- console.log(` ${t.testId}\t${t.file}\t[${t.modes.join(',')}]`);
311
+ console.log(` ${t.testId}\t${t.file}\t[${t.modes.join(',')}]`);
292
312
  }
293
313
  }
294
314
  if (errors.length) {
@@ -316,10 +336,10 @@ export async function cmdTest(args) {
316
336
  }
317
337
  console.log(`vmz test: ${tests.length} test(s) — ${reportStatus}`);
318
338
  for (const t of tests) {
319
- console.log(` ${t.status}\t${t.testId}\t${t.file}`);
339
+ console.log(` ${t.status}\t${t.testId}\t${t.file}`);
320
340
  for (const d of t.diagnostics || []) {
321
341
  if (d && typeof d === 'object' && d.severity === 'error') {
322
- console.log(` ! ${d.message}`);
342
+ console.log(` ! ${d.message}`);
323
343
  if (process.env.GITHUB_ACTIONS === 'true') {
324
344
  const msg = String(d.message || 'error').replace(/[\r\n]+/g, ' ');
325
345
  console.log(`::error title=vmz test ${t.testId}::${msg}`);
@@ -333,7 +353,7 @@ export async function cmdTest(args) {
333
353
  }
334
354
  }
335
355
  if (tests.length === 0) {
336
- console.log(' (add *.vmz.test.json manifests)');
356
+ console.log(' (add *.vmz.test.json manifests)');
337
357
  }
338
358
  if (errors.length)
339
359
  return 2;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Track per-file fingerprints so dev rebuilds only dirty leaves (N4).
2
+ * Track per-file fingerprints so dev rebuilds only dirty leaves (session).
3
3
  */
4
4
  /**
5
5
  * @param {string} srcDir
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  /**
3
- * Track per-file fingerprints so dev rebuilds only dirty leaves (N4).
3
+ * Track per-file fingerprints so dev rebuilds only dirty leaves (session).
4
4
  */
5
5
  import { existsSync, readdirSync, statSync } from 'node:fs';
6
6
  import path from 'node:path';
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@vmz/vmz",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "type": "module",
5
- "description": "VMZ Node toolchain — N-API workspace session + CLI",
5
+ "description": "VMZ Node toolchain — N-API workspace session + CLI (publish name @vmz/vmz)",
6
6
  "license": "MIT",
7
7
  "bin": {
8
8
  "vmz": "./bin/vmz.js"
@@ -48,25 +48,40 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "@vmz/plugin": "0.0.2",
52
- "@vmz/plugin-markdown-it": "0.0.2",
53
- "@vmz/protocol": "0.0.2",
54
- "@vmz/test": "0.0.2",
51
+ "@vmz/core": "0.0.4",
52
+ "@vmz/plugin": "0.0.4",
53
+ "@vmz/protocol": "0.0.4",
55
54
  "jiti": "^2.6.1",
56
- "json5": "^2.2.3",
55
+ "json5": "^2.2.3"
56
+ },
57
+ "peerDependencies": {
58
+ "@vmz/plugin-markdown-it": "0.0.4",
59
+ "@vmz/test": "0.0.4",
57
60
  "typescript": "^5.8.3"
58
61
  },
62
+ "peerDependenciesMeta": {
63
+ "@vmz/plugin-markdown-it": {
64
+ "optional": true
65
+ },
66
+ "@vmz/test": {
67
+ "optional": true
68
+ },
69
+ "typescript": {
70
+ "optional": true
71
+ }
72
+ },
59
73
  "files": [
60
74
  "bin",
61
75
  "dist",
62
- "*.node",
63
76
  "README.md"
64
77
  ],
65
78
  "scripts": {
66
- "build:native": "node ../../../scripts/build-napi.mjs",
79
+ "build:native": "node ../../../scripts/build/napi.mjs",
67
80
  "build": "pnpm run build:native && pnpm run build:js",
68
- "test": "node ./tests/run-node-tests.mjs",
69
- "build:js": "tsc -p tsconfig.json && node ../../../scripts/copy-vmz-public-api.mjs"
81
+ "test": "node --experimental-strip-types ./tests/run-node-tests.ts",
82
+ "verify": "node --import ../../../scripts/test/resolve-ts-from-js.mjs --experimental-strip-types ./tests/conformance/run.ts",
83
+ "verify:list": "node --import ../../../scripts/test/resolve-ts-from-js.mjs --experimental-strip-types ./tests/conformance/run.ts --list",
84
+ "build:js": "tsc -p tsconfig.json && node ../../../scripts/build/copy-vmz-public-api.mjs"
70
85
  },
71
86
  "keywords": [
72
87
  "vmz",
@@ -75,12 +90,12 @@
75
90
  "cli"
76
91
  ],
77
92
  "optionalDependencies": {
78
- "@vmz/vmz-win32-x64": "0.0.2",
79
- "@vmz/vmz-win32-arm64": "0.0.2",
80
- "@vmz/vmz-darwin-x64": "0.0.2",
81
- "@vmz/vmz-darwin-arm64": "0.0.2",
82
- "@vmz/vmz-linux-x64": "0.0.2",
83
- "@vmz/vmz-linux-arm64": "0.0.2"
93
+ "@vmz/vmz-win32-x64": "0.0.4",
94
+ "@vmz/vmz-win32-arm64": "0.0.4",
95
+ "@vmz/vmz-darwin-x64": "0.0.4",
96
+ "@vmz/vmz-darwin-arm64": "0.0.4",
97
+ "@vmz/vmz-linux-x64": "0.0.4",
98
+ "@vmz/vmz-linux-arm64": "0.0.4"
84
99
  },
85
100
  "publishConfig": {
86
101
  "access": "public"