@vmz/vmz 0.1.13 → 0.1.15

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 (56) hide show
  1. package/dist/author-input.d.ts +19 -0
  2. package/dist/author-input.js +52 -0
  3. package/dist/build-assemble.js +18 -7
  4. package/dist/cdn-policy.js +5 -4
  5. package/dist/cli.js +106 -38
  6. package/dist/content-addressed-assets.d.ts +23 -2
  7. package/dist/content-addressed-assets.js +178 -32
  8. package/dist/delivery-profile.d.ts +30 -2
  9. package/dist/delivery-profile.js +139 -5
  10. package/dist/dev-session.d.ts +1 -0
  11. package/dist/dev-session.js +269 -65
  12. package/dist/dev-watch-roots.d.ts +80 -0
  13. package/dist/dev-watch-roots.js +245 -0
  14. package/dist/document-build.d.ts +4 -3
  15. package/dist/document-build.js +104 -82
  16. package/dist/document-check.d.ts +24 -5
  17. package/dist/document-check.js +64 -101
  18. package/dist/document-cmd.js +2 -9
  19. package/dist/document-enrich.d.ts +2 -1
  20. package/dist/document-enrich.js +15 -4
  21. package/dist/document-integrate.js +29 -21
  22. package/dist/document-layout-render.d.ts +20 -0
  23. package/dist/document-layout-render.js +87 -0
  24. package/dist/document-routing-config.d.ts +17 -0
  25. package/dist/document-routing-config.js +31 -0
  26. package/dist/document-schema.js +8 -1
  27. package/dist/embedded-packaging.js +2 -1
  28. package/dist/index.d.ts +18 -2
  29. package/dist/index.js +63 -4
  30. package/dist/locale-check.js +81 -183
  31. package/dist/locale-cmd.js +13 -45
  32. package/dist/locale-route-emit.d.ts +4 -1
  33. package/dist/locale-route-emit.js +7 -32
  34. package/dist/locale-router.js +26 -8
  35. package/dist/native-addon.d.ts +9 -0
  36. package/dist/native-addon.js +84 -0
  37. package/dist/pack.js +3 -2
  38. package/dist/pretty-json.d.ts +19 -0
  39. package/dist/pretty-json.js +43 -0
  40. package/dist/production-observability.js +3 -2
  41. package/dist/production-test-pack.js +4 -3
  42. package/dist/public-static-assets.d.ts +24 -0
  43. package/dist/public-static-assets.js +140 -0
  44. package/dist/release-pack.js +5 -18
  45. package/dist/route-path.d.ts +35 -0
  46. package/dist/route-path.js +77 -0
  47. package/dist/server-artifact.js +4 -3
  48. package/dist/site-delivery.js +4 -3
  49. package/dist/site-favicon.d.ts +31 -0
  50. package/dist/site-favicon.js +140 -0
  51. package/dist/static-emit.d.ts +21 -0
  52. package/dist/static-emit.js +134 -97
  53. package/dist/test-cmd.js +2 -1
  54. package/dist/wechat-packaging.d.ts +22 -0
  55. package/dist/wechat-packaging.js +59 -0
  56. package/package.json +13 -14
@@ -7,9 +7,17 @@ import crypto from 'node:crypto';
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { pathToFileURL } from 'node:url';
10
+ import { createRenderHost } from '@vmz/core/render-host';
11
+ import { resolveRouteLayoutChain } from '@vmz/core/route-layout-chain';
12
+ import { listClientComponentsSync } from '@vmz/core/component-registry';
10
13
  import { emitCdnPolicy } from './cdn-policy.js';
11
14
  import { emitContentAddressedAssets } from './content-addressed-assets.js';
12
15
  import { absoluteUrl, buildLocalePageMeta, localizeBodyLinks } from './locale-router.js';
16
+ import { requireNativeAddon } from './native-addon.js';
17
+ import { writePrettyJsonFile } from './pretty-json.js';
18
+ import { emitPublicStaticAssets } from './public-static-assets.js';
19
+ import { filePathPatternFromChunk, isRouteBoundaryStem, listPublicPageUnits, parsePathPattern, unitBrowserPathPattern } from './route-path.js';
20
+ import { emitSiteFavicon, readSiteFaviconHeadHtml } from './site-favicon.js';
13
21
  export const STATIC_DELIVERY_MANIFEST_SCHEMA = 'vmz.static.delivery_manifest.v0';
14
22
  /**
15
23
  * @param {string} distDir
@@ -26,7 +34,11 @@ export async function emitWebStatic(distDir, opts = {}) {
26
34
  if (!fs.existsSync(domPath)) {
27
35
  throw new Error(`emitWebStatic: missing ${domPath} — run vmz build first`);
28
36
  }
29
- const { renderToString, renderToStream } = await import(pathToFileURL(domPath).href);
37
+ emitSiteFavicon(distDir, { projectRoot: opts.projectRoot });
38
+ const faviconHead = readSiteFaviconHeadHtml(distDir);
39
+ const publicAssets = emitPublicStaticAssets(distDir, { projectRoot: opts.projectRoot });
40
+ const host = await createRenderHost(distDir, { strictDeployment: true, preload: 'none' });
41
+ const { renderToString, renderToStream } = host;
30
42
  const pageCatalog = listPageClientFiles(distDir);
31
43
  /** @type {Array<{
32
44
  * routeId: string,
@@ -43,7 +55,7 @@ export async function emitWebStatic(distDir, opts = {}) {
43
55
  /** @type {Array<{ routeId: string, path: string, chunkId: string, classification: string, reason: string }>} */
44
56
  const skipped = [];
45
57
  for (const page of pageCatalog) {
46
- const pattern = patternFromSegs(page.segs);
58
+ const pattern = page.pathPattern || patternFromSegs(page.segs);
47
59
  const routeId = guessRouteId(distDir, page.chunkId);
48
60
  if (page.segs.some((s) => s.kind === 'param' || s.kind === 'catch')) {
49
61
  skipped.push({
@@ -94,7 +106,8 @@ export async function emitWebStatic(distDir, opts = {}) {
94
106
  }
95
107
  }
96
108
  const meta = await resolvePageMeta(Page, { params, props, pathname: pattern, origin });
97
- const layoutChain = resolveLayoutChain(distDir, page.chunkId);
109
+ const layoutChain = resolveRouteLayoutChain(distDir, page.chunkId);
110
+ await host.ensureComponents([page.chunkId, ...layoutChain]);
98
111
  let bodyHtml = '';
99
112
  for await (const chunk of renderToStream(Page, props, {})) {
100
113
  bodyHtml += chunk;
@@ -127,6 +140,7 @@ export async function emitWebStatic(distDir, opts = {}) {
127
140
  props,
128
141
  meta: gen.meta,
129
142
  cssEntry: readCssEntry(distDir),
143
+ headExtraHtml: faviconHead,
130
144
  });
131
145
  fs.writeFileSync(absHtml, html, 'utf8');
132
146
  generations.push({
@@ -158,11 +172,16 @@ export async function emitWebStatic(distDir, opts = {}) {
158
172
  },
159
173
  cssEntry: readCssEntry(distDir),
160
174
  isErrorDocument: true,
175
+ headExtraHtml: faviconHead,
161
176
  });
162
177
  fs.writeFileSync(path.join(distDir, '404.html'), notFoundHtml, 'utf8');
163
178
  const sitemap = buildSitemap(origin, generations);
164
179
  fs.writeFileSync(path.join(distDir, 'sitemap.xml'), sitemap, 'utf8');
165
- const robots = `User-agent: *\nAllow: /\nDisallow: /404\nSitemap: ${origin}/sitemap.xml\n`;
180
+ const nativeRobots = requireNativeAddon();
181
+ if (typeof nativeRobots.generateRobotsTxt !== 'function') {
182
+ throw new Error('vmz native addon missing generateRobotsTxt — rebuild with `pnpm napi:build`');
183
+ }
184
+ const robots = nativeRobots.generateRobotsTxt(`${origin}/sitemap.xml`);
166
185
  fs.writeFileSync(path.join(distDir, 'robots.txt'), robots, 'utf8');
167
186
  // Hard rule: no SPA fallback shim in artifact.
168
187
  for (const bad of ['_redirects', 'vercel.json', 'netlify.toml']) {
@@ -179,7 +198,7 @@ export async function emitWebStatic(distDir, opts = {}) {
179
198
  const manifest = {
180
199
  schema: STATIC_DELIVERY_MANIFEST_SCHEMA,
181
200
  applicationId,
182
- deliveryProfile: 'web-static',
201
+ deliveryProfile: 'static',
183
202
  origin,
184
203
  generatedAt: new Date().toISOString(),
185
204
  spaFallback: false,
@@ -204,10 +223,17 @@ export async function emitWebStatic(distDir, opts = {}) {
204
223
  sitemap: 'sitemap.xml',
205
224
  robots: 'robots.txt',
206
225
  },
226
+ publicAssets: {
227
+ schema: publicAssets.schema,
228
+ status: publicAssets.status,
229
+ fileCount: publicAssets.fileCount ?? 0,
230
+ source: publicAssets.source || null,
231
+ },
207
232
  };
208
233
  const digest = sha256Hex(canonicalJson(manifest));
209
234
  manifest.manifestDigest = digest;
210
- fs.writeFileSync(path.join(vmzDir, 'static-delivery-manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
235
+ writePrettyJsonFile(path.join(vmzDir, 'static-delivery-manifest.json'), manifest);
236
+ emitStaticClientEntries(distDir, pageCatalog);
211
237
  const assets = emitContentAddressedAssets(distDir);
212
238
  manifest.contentAddressedAssets = {
213
239
  schema: assets.manifest.schema,
@@ -218,7 +244,7 @@ export async function emitWebStatic(distDir, opts = {}) {
218
244
  // Re-stamp static manifest after linking asset digest (HTML already rewritten on disk).
219
245
  delete manifest.manifestDigest;
220
246
  manifest.manifestDigest = sha256Hex(canonicalJson(manifest));
221
- fs.writeFileSync(path.join(vmzDir, 'static-delivery-manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
247
+ writePrettyJsonFile(path.join(vmzDir, 'static-delivery-manifest.json'), manifest);
222
248
  const cdn = emitCdnPolicy(distDir, manifest);
223
249
  return {
224
250
  manifest,
@@ -226,6 +252,7 @@ export async function emitWebStatic(distDir, opts = {}) {
226
252
  skipped,
227
253
  digest: manifest.manifestDigest,
228
254
  assets: assets.manifest,
255
+ publicAssets,
229
256
  cdnPolicy: cdn.policy,
230
257
  cdnAdapters: cdn.adapters,
231
258
  };
@@ -258,8 +285,11 @@ function sortKeys(value) {
258
285
  * @param {string} distDir
259
286
  */
260
287
  function listPageClientFiles(distDir) {
288
+ const fromDep = listPagesFromDeployment(distDir);
289
+ if (fromDep.length)
290
+ return fromDep;
261
291
  const root = path.join(distDir, 'pages');
262
- /** @type {Array<{ chunkId: string, segs: ReturnType<typeof parseChunkSegments> }>} */
292
+ /** @type {Array<{ chunkId: string, segs: ReturnType<typeof parsePathPattern>, pathPattern: string }>} */
263
293
  const out = [];
264
294
  function walk(abs, relParts) {
265
295
  let ents;
@@ -274,10 +304,11 @@ function listPageClientFiles(distDir) {
274
304
  walk(path.join(abs, e.name), [...relParts, e.name]);
275
305
  else if (e.isFile() && e.name.endsWith('.client.js')) {
276
306
  const stem = e.name.replace(/\.client\.js$/, '');
277
- if (stem === 'Layout' || stem === 'Loading' || stem === 'Error' || stem === 'NotFound')
307
+ if (isRouteBoundaryStem(stem))
278
308
  continue;
279
309
  const chunkId = ['pages', ...relParts, stem].join('/');
280
- out.push({ chunkId, segs: parseChunkSegments(chunkId) });
310
+ const pathPattern = filePathPatternFromChunk(chunkId);
311
+ out.push({ chunkId, pathPattern, segs: parsePathPattern(pathPattern) });
281
312
  }
282
313
  }
283
314
  }
@@ -285,32 +316,26 @@ function listPageClientFiles(distDir) {
285
316
  return out;
286
317
  }
287
318
  /**
288
- * @param {string} chunkId
319
+ * @param {string} distDir
289
320
  */
290
- function parseChunkSegments(chunkId) {
291
- const rel = chunkId.replace(/^pages\//, '');
292
- const parts = rel.split('/').filter(Boolean);
293
- /** @type {Array<{ kind: 'static' | 'param' | 'catch', value?: string, name?: string }>} */
294
- const segs = [];
295
- for (let i = 0; i < parts.length; i++) {
296
- const p = parts[i];
297
- if (p.startsWith('(') && p.endsWith(')') && p.length > 2)
298
- continue;
299
- if (p === 'index' && i === parts.length - 1)
300
- continue;
301
- const catchAll = /^\[\.\.\.([^\]]+)\]$/.exec(p);
302
- const param = /^\[([^\]]+)\]$/.exec(p);
303
- if (catchAll)
304
- segs.push({ kind: 'catch', name: catchAll[1] });
305
- else if (param)
306
- segs.push({ kind: 'param', name: param[1] });
307
- else
308
- segs.push({ kind: 'static', value: p.toLowerCase() });
321
+ function listPagesFromDeployment(distDir) {
322
+ const deploymentPath = path.join(distDir, 'vmz-deployment.json');
323
+ if (!fs.existsSync(deploymentPath))
324
+ return [];
325
+ try {
326
+ const deployment = JSON.parse(fs.readFileSync(deploymentPath, 'utf8'));
327
+ return listPublicPageUnits(deployment).map((u) => {
328
+ const chunkId = String(u.chunkId || '').replace(/\\/g, '/');
329
+ const pathPattern = unitBrowserPathPattern(u);
330
+ return { chunkId, pathPattern, segs: parsePathPattern(pathPattern) };
331
+ });
332
+ }
333
+ catch {
334
+ return [];
309
335
  }
310
- return segs;
311
336
  }
312
337
  /**
313
- * @param {ReturnType<typeof parseChunkSegments>} segs
338
+ * @param {ReturnType<typeof parsePathPattern>} segs
314
339
  */
315
340
  function patternFromSegs(segs) {
316
341
  if (!segs.length)
@@ -343,24 +368,6 @@ async function loadCtor(distDir, chunkId) {
343
368
  const mod = await import(`${href}?t=${Date.now()}`);
344
369
  return mod.default;
345
370
  }
346
- /**
347
- * @param {string} distDir
348
- * @param {string} pageChunkId
349
- */
350
- function resolveLayoutChain(distDir, pageChunkId) {
351
- const rel = pageChunkId.replace(/^pages\//, '');
352
- const parts = rel.split('/').filter(Boolean);
353
- parts.pop();
354
- /** @type {string[]} */
355
- const chain = [];
356
- for (let i = parts.length; i >= 0; i--) {
357
- const dirParts = parts.slice(0, i);
358
- const layoutChunk = ['pages', ...dirParts, 'Layout'].join('/');
359
- if (fs.existsSync(path.join(distDir, `${layoutChunk}.client.js`)))
360
- chain.unshift(layoutChunk);
361
- }
362
- return chain;
363
- }
364
371
  /**
365
372
  * @param {string} distDir
366
373
  * @param {string} chunkId
@@ -389,8 +396,8 @@ async function resolvePageMeta(Page, ctx) {
389
396
  else if (Page.meta && typeof Page.meta === 'object') {
390
397
  raw = Page.meta;
391
398
  }
392
- const title = String(raw.title || `${guessTitle(ctx.pathname)} · VMZ`);
393
- const description = String(raw.description || `VMZ page ${ctx.pathname}`);
399
+ const title = String(raw.title || guessTitle(ctx.pathname) || 'App');
400
+ const description = String(raw.description || '');
394
401
  const canonical = String(raw.canonical || `${ctx.origin}${ctx.pathname === '/' ? '/' : ctx.pathname}`);
395
402
  const robots = String(raw.robots || 'index,follow');
396
403
  const lang = String(raw.lang || 'en');
@@ -510,63 +517,93 @@ function readCssEntry(distDir) {
510
517
  * meta: { title: string, description: string, canonical: string, robots: string, lang: string, dir?: string, alternates?: Array<{ hreflang: string, href: string }> },
511
518
  * cssEntry: string | null,
512
519
  * isErrorDocument?: boolean,
520
+ * headExtraHtml?: string,
513
521
  * }} input
514
522
  */
515
523
  function wrapDocument(input) {
516
524
  const propsJson = JSON.stringify(input.props ?? {});
517
- const layoutAttr = input.layoutChain.length ? ` data-vmz-layout="${escapeAttr(input.layoutChain.join(','))}"` : '';
518
- const pageAttr = input.chunkId ? ` data-vmz-page="${escapeAttr(input.chunkId)}"` : '';
519
525
  const localeId = input.meta.lang || 'en';
520
526
  const dir = input.meta.dir || 'ltr';
521
- const localeAttr = ` data-vmz-locale="${escapeAttr(localeId)}" data-vmz-dir="${escapeAttr(dir)}"`;
522
- const cssLink = input.cssEntry ? ` <link rel="stylesheet" href="/${String(input.cssEntry).replace(/^\/+/, '')}" />\n` : '';
523
- const entry = input.isErrorDocument ? '' : ` <script type="module" src="/entry-client.js"></script>\n`;
524
- const hreflang = (input.meta.alternates || [])
525
- .map((a) => ` <link rel="alternate" hreflang="${escapeAttr(a.hreflang)}" href="${escapeAttr(a.href)}" />`)
526
- .join('\n');
527
- const hreflangBlock = hreflang ? `${hreflang}\n` : '';
528
- return `<!DOCTYPE html>
529
- <html lang="${escapeAttr(localeId)}" data-locale="${escapeAttr(localeId)}" dir="${escapeAttr(dir)}">
530
- <head>
531
- <meta charset="utf-8" />
532
- <meta name="viewport" content="width=device-width, initial-scale=1" />
533
- <title>${escapeHtml(input.meta.title)}</title>
534
- <meta name="description" content="${escapeAttr(input.meta.description)}" />
535
- <meta name="robots" content="${escapeAttr(input.meta.robots)}" />
536
- <link rel="canonical" href="${escapeAttr(input.meta.canonical)}" />
537
- ${hreflangBlock} <meta property="og:title" content="${escapeAttr(input.meta.title)}" />
538
- <meta property="og:description" content="${escapeAttr(input.meta.description)}" />
539
- <meta property="og:url" content="${escapeAttr(input.meta.canonical)}" />
540
- ${cssLink}</head>
541
- <body>
542
- <div id="app"${pageAttr}${layoutAttr}${localeAttr} data-vmz-props="${escapeAttr(propsJson)}">${input.bodyHtml}</div>
543
- ${entry}</body>
544
- </html>
545
- `;
527
+ const native = requireNativeAddon();
528
+ if (typeof native.generatePageShell !== 'function') {
529
+ throw new Error('vmz native addon missing generatePageShell — rebuild with `pnpm napi:build`');
530
+ }
531
+ return native.generatePageShell({
532
+ bodyHtml: input.bodyHtml,
533
+ chunkId: input.chunkId || '',
534
+ layoutChain: input.layoutChain || [],
535
+ propsJson,
536
+ meta: {
537
+ title: input.meta.title,
538
+ description: input.meta.description,
539
+ canonical: input.meta.canonical,
540
+ robots: input.meta.robots,
541
+ lang: localeId,
542
+ dir,
543
+ alternates: input.meta.alternates || [],
544
+ },
545
+ // napi Option<String>: omit/undefined = None; null is rejected as String
546
+ ...(input.cssEntry ? { cssEntry: String(input.cssEntry) } : {}),
547
+ isErrorDocument: !!input.isErrorDocument,
548
+ ...(input.headExtraHtml ? { headExtraHtml: String(input.headExtraHtml) } : {}),
549
+ });
546
550
  }
547
551
  /**
548
552
  * @param {string} origin
549
553
  * @param {Array<{ canonical: string, robots: string }>} generations
550
554
  */
551
- function buildSitemap(origin, generations) {
552
- const urls = generations
553
- .filter((g) => !String(g.robots).includes('noindex'))
554
- .map((g) => ` <url>
555
- <loc>${escapeXml(g.canonical)}</loc>
556
- </url>`)
557
- .join('\n');
558
- return `<?xml version="1.0" encoding="UTF-8"?>
559
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
560
- ${urls}
561
- </urlset>
562
- `;
555
+ function buildSitemap(_origin, generations) {
556
+ const urls = generations.filter((g) => !String(g.robots).includes('noindex')).map((g) => ({ loc: g.canonical }));
557
+ const native = requireNativeAddon();
558
+ if (typeof native.generateSitemapXml !== 'function') {
559
+ throw new Error('vmz native addon missing generateSitemapXml — rebuild with `pnpm napi:build`');
560
+ }
561
+ return native.generateSitemapXml(urls);
563
562
  }
564
- function escapeHtml(s) {
565
- return String(s).replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
563
+ /**
564
+ * Static CDN must ship entry-client/event like serve-host (content-addressed + HTML rewrite).
565
+ * @param {string} distDir
566
+ * @param {Array<{ chunkId: string }>} pageCatalog
567
+ */
568
+ function emitStaticClientEntries(distDir, pageCatalog) {
569
+ const componentEntries = listClientComponentsSync(distDir, { strict: true });
570
+ const indexChunk = pageCatalog.find((p) => p.chunkId === 'pages/index')?.chunkId || pageCatalog[0]?.chunkId || 'pages/index';
571
+ const resumeEntries = loadPageResumeEntriesSync(distDir, indexChunk);
572
+ const lazySet = new Set(resumeEntries
573
+ .filter((e) => isEventResumeStrategy(e.strategy))
574
+ .map((e) => e.component)
575
+ .filter(Boolean));
576
+ const eager = componentEntries.filter((e) => !lazySet.has(e.name));
577
+ const lazy = componentEntries.filter((e) => lazySet.has(e.name));
578
+ const native = requireNativeAddon();
579
+ if (typeof native.generateServeEntryClient !== 'function') {
580
+ throw new Error('vmz native addon missing generateServeEntryClient — rebuild with `pnpm napi:build`');
581
+ }
582
+ fs.writeFileSync(path.join(distDir, 'entry-client.js'), native.generateServeEntryClient(eager, lazy, ''), 'utf8');
583
+ if (typeof native.generateServeEntryEvent === 'function') {
584
+ fs.writeFileSync(path.join(distDir, 'entry-event.js'), native.generateServeEntryEvent(''), 'utf8');
585
+ }
566
586
  }
567
- function escapeAttr(s) {
568
- return escapeHtml(s).replaceAll('"', '&quot;');
587
+ /**
588
+ * @param {string} distDir
589
+ * @param {string} chunkId
590
+ */
591
+ function loadPageResumeEntriesSync(distDir, chunkId) {
592
+ try {
593
+ const dep = JSON.parse(fs.readFileSync(path.join(distDir, 'vmz-deployment.json'), 'utf8'));
594
+ const units = Array.isArray(dep.units) ? dep.units : [];
595
+ const page = units.find((u) => u.chunkId === chunkId) || units.find((u) => u.chunkId === 'pages/index') || units.find((u) => u.kind === 'page');
596
+ const entries = Array.isArray(page?.resumeEntries) ? page.resumeEntries : [];
597
+ return entries.map((e) => ({
598
+ component: String(e.component || ''),
599
+ strategy: String(e.strategy || ''),
600
+ }));
601
+ }
602
+ catch {
603
+ return [];
604
+ }
569
605
  }
570
- function escapeXml(s) {
571
- return escapeAttr(s).replaceAll("'", '&apos;');
606
+ /** @param {string} strategy */
607
+ function isEventResumeStrategy(strategy) {
608
+ return strategy === 'event' || strategy === 'click' || String(strategy).startsWith('event:');
572
609
  }
package/dist/test-cmd.js CHANGED
@@ -6,6 +6,7 @@
6
6
  import path from 'node:path';
7
7
  import { createWorkspace } from './index.js';
8
8
  import { log } from './log.js';
9
+ import { generatePrettyJson } from './pretty-json.js';
9
10
  /**
10
11
  * @returns {Promise<typeof import('@vmz/test')>}
11
12
  */
@@ -317,7 +318,7 @@ export async function cmdTest(args) {
317
318
  return errors.length ? 2 : 0;
318
319
  }
319
320
  if (wantJson) {
320
- const text = `${JSON.stringify(report, null, 2)}\n`;
321
+ const text = `${generatePrettyJson(report)}\n`;
321
322
  if (typeof args.json === 'string' && args.json !== 'true') {
322
323
  const { writeFileSync } = await import('node:fs');
323
324
  writeFileSync(path.resolve(String(args.json)), text);
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Materialize `defineConfig({ delivery: { packaging: { wechat } } })` for wechat_pack.
3
+ * Pure data only. Writes `dist/_vmz/wechat-packaging.json` (not a second config entry).
4
+ */
5
+ export declare const WECHAT_PACKAGING_SCHEMA = "vmz.target.wechat_packaging.v0";
6
+ export declare const WECHAT_PACKAGING_REL = "dist/_vmz/wechat-packaging.json";
7
+ /**
8
+ * @param {unknown} delivery
9
+ * @returns {{ schema: string, appId: string, projectName?: string, title?: string }}
10
+ */
11
+ export declare function wechatPackagingFromDelivery(delivery: any): {
12
+ schema: string;
13
+ appId: any;
14
+ };
15
+ /**
16
+ * Load `vmz.config.*` and write the WeChat packaging contract for the Rust packer.
17
+ * @param {string} project
18
+ */
19
+ export declare function materializeWechatPackaging(project: any): {
20
+ schema: string;
21
+ appId: any;
22
+ };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Materialize `defineConfig({ delivery: { packaging: { wechat } } })` for wechat_pack.
3
+ * Pure data only. Writes `dist/_vmz/wechat-packaging.json` (not a second config entry).
4
+ */
5
+ // @ts-nocheck
6
+ import { existsSync, mkdirSync } from 'node:fs';
7
+ import path from 'node:path';
8
+ import { createJiti } from 'jiti';
9
+ import { pickDeliveryPackaging } from './delivery-profile.js';
10
+ import { writePrettyJsonFile } from './pretty-json.js';
11
+ export const WECHAT_PACKAGING_SCHEMA = 'vmz.target.wechat_packaging.v0';
12
+ export const WECHAT_PACKAGING_REL = 'dist/_vmz/wechat-packaging.json';
13
+ const CONFIG_NAMES = ['vmz.config.ts', 'vmz.config.mts', 'vmz.config.mjs', 'vmz.config.js'];
14
+ function loadConfigSync(project) {
15
+ for (const name of CONFIG_NAMES) {
16
+ const full = path.join(project, name);
17
+ if (!existsSync(full))
18
+ continue;
19
+ const jiti = createJiti(import.meta.url, {
20
+ interopDefault: true,
21
+ moduleCache: false,
22
+ });
23
+ return jiti(full);
24
+ }
25
+ return null;
26
+ }
27
+ /**
28
+ * @param {unknown} delivery
29
+ * @returns {{ schema: string, appId: string, projectName?: string, title?: string }}
30
+ */
31
+ export function wechatPackagingFromDelivery(delivery) {
32
+ const diagnostics = [];
33
+ const packaging = pickDeliveryPackaging(delivery && typeof delivery === 'object' ? delivery : {}, diagnostics);
34
+ const wechat = packaging && packaging.wechat ? packaging.wechat : {};
35
+ /** @type {{ schema: string, appId: string, projectName?: string, title?: string }} */
36
+ const out = {
37
+ schema: WECHAT_PACKAGING_SCHEMA,
38
+ appId: typeof wechat.appId === 'string' && wechat.appId.trim() ? wechat.appId.trim() : 'touristappid',
39
+ };
40
+ if (typeof wechat.projectName === 'string' && wechat.projectName.trim()) {
41
+ out.projectName = wechat.projectName.trim();
42
+ }
43
+ if (typeof wechat.title === 'string' && wechat.title.trim()) {
44
+ out.title = wechat.title.trim();
45
+ }
46
+ return out;
47
+ }
48
+ /**
49
+ * Load `vmz.config.*` and write the WeChat packaging contract for the Rust packer.
50
+ * @param {string} project
51
+ */
52
+ export function materializeWechatPackaging(project) {
53
+ const cfg = loadConfigSync(project);
54
+ const spec = wechatPackagingFromDelivery(cfg?.delivery);
55
+ const abs = path.join(project, WECHAT_PACKAGING_REL);
56
+ mkdirSync(path.dirname(abs), { recursive: true });
57
+ writePrettyJsonFile(abs, spec);
58
+ return spec;
59
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vmz/vmz",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "type": "module",
5
5
  "description": "VMZ Node toolchain — N-API workspace session + CLI (publish name @vmz/vmz)",
6
6
  "license": "MIT",
@@ -48,15 +48,14 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "@vmz/core": "0.1.13",
52
- "@vmz/plugin": "0.1.13",
53
- "@vmz/protocol": "0.1.13",
54
- "jiti": "^2.6.1",
55
- "json5": "^2.2.3"
51
+ "@vmz/core": "0.1.15",
52
+ "@vmz/plugin": "0.1.15",
53
+ "@vmz/protocol": "0.1.15",
54
+ "jiti": "^2.6.1"
56
55
  },
57
56
  "peerDependencies": {
58
- "@vmz/plugin-markdown-it": "0.1.13",
59
- "@vmz/test": "0.1.13",
57
+ "@vmz/plugin-markdown-it": "0.1.15",
58
+ "@vmz/test": "0.1.15",
60
59
  "typescript": "^5.8.3"
61
60
  },
62
61
  "peerDependenciesMeta": {
@@ -90,12 +89,12 @@
90
89
  "cli"
91
90
  ],
92
91
  "optionalDependencies": {
93
- "@vmz/vmz-win32-x64": "0.1.13",
94
- "@vmz/vmz-win32-arm64": "0.1.13",
95
- "@vmz/vmz-darwin-x64": "0.1.13",
96
- "@vmz/vmz-darwin-arm64": "0.1.13",
97
- "@vmz/vmz-linux-x64": "0.1.13",
98
- "@vmz/vmz-linux-arm64": "0.1.13"
92
+ "@vmz/vmz-win32-x64": "0.1.15",
93
+ "@vmz/vmz-win32-arm64": "0.1.15",
94
+ "@vmz/vmz-darwin-x64": "0.1.15",
95
+ "@vmz/vmz-darwin-arm64": "0.1.15",
96
+ "@vmz/vmz-linux-x64": "0.1.15",
97
+ "@vmz/vmz-linux-arm64": "0.1.15"
99
98
  },
100
99
  "publishConfig": {
101
100
  "access": "public"