@vmz/vmz 0.1.0 → 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 (40) hide show
  1. package/dist/build-assemble.js +4 -3
  2. package/dist/cdn-policy.js +5 -8
  3. package/dist/cli.js +20 -7
  4. package/dist/content-addressed-assets.js +2 -1
  5. package/dist/delivery-profile.d.ts +10 -0
  6. package/dist/delivery-profile.js +69 -0
  7. package/dist/dev-session.d.ts +6 -0
  8. package/dist/dev-session.js +166 -40
  9. package/dist/document-build.js +24 -28
  10. package/dist/document-cmd.js +2 -9
  11. package/dist/document-integrate.js +10 -17
  12. package/dist/embedded-packaging.js +3 -7
  13. package/dist/index.d.ts +19 -1
  14. package/dist/index.js +62 -2
  15. package/dist/locale-check.js +28 -58
  16. package/dist/locale-cmd.js +10 -42
  17. package/dist/locale-route-emit.d.ts +4 -1
  18. package/dist/locale-route-emit.js +7 -32
  19. package/dist/mini-host.d.ts +47 -0
  20. package/dist/mini-host.js +202 -0
  21. package/dist/native-addon.d.ts +9 -0
  22. package/dist/native-addon.js +84 -0
  23. package/dist/pack-client-packages.d.ts +25 -0
  24. package/dist/pack-client-packages.js +399 -0
  25. package/dist/pack.d.ts +21 -3
  26. package/dist/pack.js +21 -6
  27. package/dist/pretty-json.d.ts +19 -0
  28. package/dist/pretty-json.js +43 -0
  29. package/dist/production-observability.js +3 -2
  30. package/dist/production-test-pack.js +4 -3
  31. package/dist/release-pack.js +5 -18
  32. package/dist/route-path.d.ts +35 -0
  33. package/dist/route-path.js +77 -0
  34. package/dist/server-artifact.js +5 -6
  35. package/dist/site-delivery.js +3 -2
  36. package/dist/static-emit.js +67 -86
  37. package/dist/test-cmd.js +2 -1
  38. package/dist/wechat-packaging.d.ts +22 -0
  39. package/dist/wechat-packaging.js +59 -0
  40. package/package.json +12 -12
@@ -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
+ }
@@ -4,9 +4,10 @@
4
4
  */
5
5
  // @ts-nocheck
6
6
  import crypto from 'node:crypto';
7
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
7
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { SERVER_RUNTIMES } from './delivery-profile.js';
10
+ import { writePrettyJsonFile } from './pretty-json.js';
10
11
  export const SERVER_ARTIFACT_SCHEMA = 'vmz.server.artifact.v0';
11
12
  export const HTTP_CONTRACT_SCHEMA = 'vmz.http.contract.v0';
12
13
  export const SERVER_RUNTIME_ADAPTER_SCHEMA = 'vmz.server.runtime_adapter.v0';
@@ -116,14 +117,14 @@ export function emitServerArtifact(outDir, opts = {}) {
116
117
  const vmzDir = path.join(outDir, '_vmz');
117
118
  mkdirSync(vmzDir, { recursive: true });
118
119
  const file = path.join(vmzDir, 'server-artifact.json');
119
- writeFileSync(file, `${JSON.stringify(artifact, null, 2)}\n`, 'utf8');
120
+ writePrettyJsonFile(file, artifact);
120
121
  const adapterDir = path.join(vmzDir, 'adapters');
121
122
  mkdirSync(adapterDir, { recursive: true });
122
123
  for (const adapterId of ['worker', 'rust-host']) {
123
124
  const projection = projectServerRuntimeAdapter(artifact, adapterId);
124
125
  const dir = path.join(adapterDir, adapterId);
125
126
  mkdirSync(dir, { recursive: true });
126
- writeFileSync(path.join(dir, 'adapter.json'), `${JSON.stringify(projection, null, 2)}\n`, 'utf8');
127
+ writePrettyJsonFile(path.join(dir, 'adapter.json'), projection);
127
128
  }
128
129
  return { artifact, path: file, httpContractDigest };
129
130
  }
@@ -144,9 +145,7 @@ export function projectServerRuntimeAdapter(artifact, adapterId) {
144
145
  spaFallback: false,
145
146
  entry: artifact.entry,
146
147
  publicRouteCount: Array.isArray(artifact.publicRoutes) ? artifact.publicRoutes.length : 0,
147
- internalCapabilityCount: Array.isArray(artifact.internalCapabilities)
148
- ? artifact.internalCapabilities.length
149
- : 0,
148
+ internalCapabilityCount: Array.isArray(artifact.internalCapabilities) ? artifact.internalCapabilities.length : 0,
150
149
  };
151
150
  if (id === 'node') {
152
151
  return { ...base, host: 'node:http', invoke: 'handleNodeRequest', status: 'runtime' };
@@ -6,6 +6,7 @@
6
6
  import crypto from 'node:crypto';
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
+ import { writePrettyJsonFile } from './pretty-json.js';
9
10
  export const SITE_DELIVERY_CONTRACT_SCHEMA = 'vmz.site.delivery_contract.v0';
10
11
  export const SITE_DELIVERY_RESOLUTION_SCHEMA = 'vmz.site.delivery_resolution.v0';
11
12
  /**
@@ -318,11 +319,11 @@ export function emitSiteDelivery(outDir, deliveryRaw, opts = {}) {
318
319
  }
319
320
  const vmzDir = path.join(outDir, '_vmz');
320
321
  fs.mkdirSync(vmzDir, { recursive: true });
321
- fs.writeFileSync(path.join(vmzDir, 'site-delivery-contract.json'), `${JSON.stringify(norm.contract, null, 2)}\n`, 'utf8');
322
+ writePrettyJsonFile(path.join(vmzDir, 'site-delivery-contract.json'), norm.contract);
322
323
  let resolution = null;
323
324
  if (opts.probes) {
324
325
  resolution = resolveSiteRelease(norm.contract, opts.probes);
325
- fs.writeFileSync(path.join(vmzDir, 'site-delivery-resolution.json'), `${JSON.stringify(resolution, null, 2)}\n`, 'utf8');
326
+ writePrettyJsonFile(path.join(vmzDir, 'site-delivery-resolution.json'), resolution);
326
327
  }
327
328
  return { contract: norm.contract, resolution };
328
329
  }
@@ -10,6 +10,9 @@ import { pathToFileURL } from 'node:url';
10
10
  import { emitCdnPolicy } from './cdn-policy.js';
11
11
  import { emitContentAddressedAssets } from './content-addressed-assets.js';
12
12
  import { absoluteUrl, buildLocalePageMeta, localizeBodyLinks } from './locale-router.js';
13
+ import { requireNativeAddon } from './native-addon.js';
14
+ import { writePrettyJsonFile } from './pretty-json.js';
15
+ import { filePathPatternFromChunk, isRouteBoundaryStem, listPublicPageUnits, parsePathPattern, unitBrowserPathPattern, } from './route-path.js';
13
16
  export const STATIC_DELIVERY_MANIFEST_SCHEMA = 'vmz.static.delivery_manifest.v0';
14
17
  /**
15
18
  * @param {string} distDir
@@ -43,7 +46,7 @@ export async function emitWebStatic(distDir, opts = {}) {
43
46
  /** @type {Array<{ routeId: string, path: string, chunkId: string, classification: string, reason: string }>} */
44
47
  const skipped = [];
45
48
  for (const page of pageCatalog) {
46
- const pattern = patternFromSegs(page.segs);
49
+ const pattern = page.pathPattern || patternFromSegs(page.segs);
47
50
  const routeId = guessRouteId(distDir, page.chunkId);
48
51
  if (page.segs.some((s) => s.kind === 'param' || s.kind === 'catch')) {
49
52
  skipped.push({
@@ -119,9 +122,7 @@ export async function emitWebStatic(distDir, opts = {}) {
119
122
  const absHtml = path.join(distDir, gen.htmlPath);
120
123
  fs.mkdirSync(path.dirname(absHtml), { recursive: true });
121
124
  // Each LocaleId HTML must retain locale on same-app Links (realization authority).
122
- const localizedBody = gen.localeId && localeArt
123
- ? localizeBodyLinks(bodyHtml, gen.localeId, localeArt)
124
- : bodyHtml;
125
+ const localizedBody = gen.localeId && localeArt ? localizeBodyLinks(bodyHtml, gen.localeId, localeArt) : bodyHtml;
125
126
  const html = wrapDocument({
126
127
  bodyHtml: localizedBody,
127
128
  chunkId: page.chunkId,
@@ -164,7 +165,11 @@ export async function emitWebStatic(distDir, opts = {}) {
164
165
  fs.writeFileSync(path.join(distDir, '404.html'), notFoundHtml, 'utf8');
165
166
  const sitemap = buildSitemap(origin, generations);
166
167
  fs.writeFileSync(path.join(distDir, 'sitemap.xml'), sitemap, 'utf8');
167
- const robots = `User-agent: *\nAllow: /\nDisallow: /404\nSitemap: ${origin}/sitemap.xml\n`;
168
+ const nativeRobots = requireNativeAddon();
169
+ if (typeof nativeRobots.generateRobotsTxt !== 'function') {
170
+ throw new Error('vmz native addon missing generateRobotsTxt — rebuild with `pnpm napi:build`');
171
+ }
172
+ const robots = nativeRobots.generateRobotsTxt(`${origin}/sitemap.xml`);
168
173
  fs.writeFileSync(path.join(distDir, 'robots.txt'), robots, 'utf8');
169
174
  // Hard rule: no SPA fallback shim in artifact.
170
175
  for (const bad of ['_redirects', 'vercel.json', 'netlify.toml']) {
@@ -209,7 +214,7 @@ export async function emitWebStatic(distDir, opts = {}) {
209
214
  };
210
215
  const digest = sha256Hex(canonicalJson(manifest));
211
216
  manifest.manifestDigest = digest;
212
- fs.writeFileSync(path.join(vmzDir, 'static-delivery-manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
217
+ writePrettyJsonFile(path.join(vmzDir, 'static-delivery-manifest.json'), manifest);
213
218
  const assets = emitContentAddressedAssets(distDir);
214
219
  manifest.contentAddressedAssets = {
215
220
  schema: assets.manifest.schema,
@@ -220,7 +225,7 @@ export async function emitWebStatic(distDir, opts = {}) {
220
225
  // Re-stamp static manifest after linking asset digest (HTML already rewritten on disk).
221
226
  delete manifest.manifestDigest;
222
227
  manifest.manifestDigest = sha256Hex(canonicalJson(manifest));
223
- fs.writeFileSync(path.join(vmzDir, 'static-delivery-manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
228
+ writePrettyJsonFile(path.join(vmzDir, 'static-delivery-manifest.json'), manifest);
224
229
  const cdn = emitCdnPolicy(distDir, manifest);
225
230
  return {
226
231
  manifest,
@@ -260,8 +265,11 @@ function sortKeys(value) {
260
265
  * @param {string} distDir
261
266
  */
262
267
  function listPageClientFiles(distDir) {
268
+ const fromDep = listPagesFromDeployment(distDir);
269
+ if (fromDep.length)
270
+ return fromDep;
263
271
  const root = path.join(distDir, 'pages');
264
- /** @type {Array<{ chunkId: string, segs: ReturnType<typeof parseChunkSegments> }>} */
272
+ /** @type {Array<{ chunkId: string, segs: ReturnType<typeof parsePathPattern>, pathPattern: string }>} */
265
273
  const out = [];
266
274
  function walk(abs, relParts) {
267
275
  let ents;
@@ -276,10 +284,11 @@ function listPageClientFiles(distDir) {
276
284
  walk(path.join(abs, e.name), [...relParts, e.name]);
277
285
  else if (e.isFile() && e.name.endsWith('.client.js')) {
278
286
  const stem = e.name.replace(/\.client\.js$/, '');
279
- if (stem === 'Layout' || stem === 'Loading' || stem === 'Error' || stem === 'NotFound')
287
+ if (isRouteBoundaryStem(stem))
280
288
  continue;
281
289
  const chunkId = ['pages', ...relParts, stem].join('/');
282
- out.push({ chunkId, segs: parseChunkSegments(chunkId) });
290
+ const pathPattern = filePathPatternFromChunk(chunkId);
291
+ out.push({ chunkId, pathPattern, segs: parsePathPattern(pathPattern) });
283
292
  }
284
293
  }
285
294
  }
@@ -287,32 +296,26 @@ function listPageClientFiles(distDir) {
287
296
  return out;
288
297
  }
289
298
  /**
290
- * @param {string} chunkId
299
+ * @param {string} distDir
291
300
  */
292
- function parseChunkSegments(chunkId) {
293
- const rel = chunkId.replace(/^pages\//, '');
294
- const parts = rel.split('/').filter(Boolean);
295
- /** @type {Array<{ kind: 'static' | 'param' | 'catch', value?: string, name?: string }>} */
296
- const segs = [];
297
- for (let i = 0; i < parts.length; i++) {
298
- const p = parts[i];
299
- if (p.startsWith('(') && p.endsWith(')') && p.length > 2)
300
- continue;
301
- if (p === 'index' && i === parts.length - 1)
302
- continue;
303
- const catchAll = /^\[\.\.\.([^\]]+)\]$/.exec(p);
304
- const param = /^\[([^\]]+)\]$/.exec(p);
305
- if (catchAll)
306
- segs.push({ kind: 'catch', name: catchAll[1] });
307
- else if (param)
308
- segs.push({ kind: 'param', name: param[1] });
309
- else
310
- segs.push({ kind: 'static', value: p.toLowerCase() });
301
+ function listPagesFromDeployment(distDir) {
302
+ const deploymentPath = path.join(distDir, 'vmz-deployment.json');
303
+ if (!fs.existsSync(deploymentPath))
304
+ return [];
305
+ try {
306
+ const deployment = JSON.parse(fs.readFileSync(deploymentPath, 'utf8'));
307
+ return listPublicPageUnits(deployment).map((u) => {
308
+ const chunkId = String(u.chunkId || '').replace(/\\/g, '/');
309
+ const pathPattern = unitBrowserPathPattern(u);
310
+ return { chunkId, pathPattern, segs: parsePathPattern(pathPattern) };
311
+ });
312
+ }
313
+ catch {
314
+ return [];
311
315
  }
312
- return segs;
313
316
  }
314
317
  /**
315
- * @param {ReturnType<typeof parseChunkSegments>} segs
318
+ * @param {ReturnType<typeof parsePathPattern>} segs
316
319
  */
317
320
  function patternFromSegs(segs) {
318
321
  if (!segs.length)
@@ -391,8 +394,8 @@ async function resolvePageMeta(Page, ctx) {
391
394
  else if (Page.meta && typeof Page.meta === 'object') {
392
395
  raw = Page.meta;
393
396
  }
394
- const title = String(raw.title || `${guessTitle(ctx.pathname)} · VMZ`);
395
- const description = String(raw.description || `VMZ page ${ctx.pathname}`);
397
+ const title = String(raw.title || guessTitle(ctx.pathname) || 'App');
398
+ const description = String(raw.description || '');
396
399
  const canonical = String(raw.canonical || `${ctx.origin}${ctx.pathname === '/' ? '/' : ctx.pathname}`);
397
400
  const robots = String(raw.robots || 'index,follow');
398
401
  const lang = String(raw.lang || 'en');
@@ -438,10 +441,7 @@ function expandLocaleStaticGenerations(input) {
438
441
  const locales = (localeArt.locales || []).map((l) => l.id);
439
442
  const directions = Object.fromEntries((localeArt.locales || []).map((l) => [l.id, l.direction || 'ltr']));
440
443
  const defaultLocale = localeArt.defaultLocale || locales[0];
441
- const forRoute = (localeArt.realizations || []).filter((r) => r.routeId === routeId ||
442
- r.routeId === input.chunkId ||
443
- r.pathPattern === pattern ||
444
- (r.path === pattern && !r.prefixed));
444
+ const forRoute = (localeArt.realizations || []).filter((r) => r.routeId === routeId || r.routeId === input.chunkId || r.pathPattern === pattern || (r.path === pattern && !r.prefixed));
445
445
  /** @type {any[]} */
446
446
  const out = [];
447
447
  for (const loc of locales) {
@@ -519,59 +519,40 @@ function readCssEntry(distDir) {
519
519
  */
520
520
  function wrapDocument(input) {
521
521
  const propsJson = JSON.stringify(input.props ?? {});
522
- const layoutAttr = input.layoutChain.length ? ` data-vmz-layout="${escapeAttr(input.layoutChain.join(','))}"` : '';
523
- const pageAttr = input.chunkId ? ` data-vmz-page="${escapeAttr(input.chunkId)}"` : '';
524
522
  const localeId = input.meta.lang || 'en';
525
523
  const dir = input.meta.dir || 'ltr';
526
- const localeAttr = ` data-vmz-locale="${escapeAttr(localeId)}" data-vmz-dir="${escapeAttr(dir)}"`;
527
- const cssLink = input.cssEntry ? ` <link rel="stylesheet" href="/${String(input.cssEntry).replace(/^\/+/, '')}" />\n` : '';
528
- const entry = input.isErrorDocument ? '' : ` <script type="module" src="/entry-client.js"></script>\n`;
529
- const hreflang = (input.meta.alternates || [])
530
- .map((a) => ` <link rel="alternate" hreflang="${escapeAttr(a.hreflang)}" href="${escapeAttr(a.href)}" />`)
531
- .join('\n');
532
- const hreflangBlock = hreflang ? `${hreflang}\n` : '';
533
- return `<!DOCTYPE html>
534
- <html lang="${escapeAttr(localeId)}" data-locale="${escapeAttr(localeId)}" dir="${escapeAttr(dir)}">
535
- <head>
536
- <meta charset="utf-8" />
537
- <meta name="viewport" content="width=device-width, initial-scale=1" />
538
- <title>${escapeHtml(input.meta.title)}</title>
539
- <meta name="description" content="${escapeAttr(input.meta.description)}" />
540
- <meta name="robots" content="${escapeAttr(input.meta.robots)}" />
541
- <link rel="canonical" href="${escapeAttr(input.meta.canonical)}" />
542
- ${hreflangBlock} <meta property="og:title" content="${escapeAttr(input.meta.title)}" />
543
- <meta property="og:description" content="${escapeAttr(input.meta.description)}" />
544
- <meta property="og:url" content="${escapeAttr(input.meta.canonical)}" />
545
- ${cssLink}</head>
546
- <body>
547
- <div id="app"${pageAttr}${layoutAttr}${localeAttr} data-vmz-props="${escapeAttr(propsJson)}">${input.bodyHtml}</div>
548
- ${entry}</body>
549
- </html>
550
- `;
524
+ const native = requireNativeAddon();
525
+ if (typeof native.generatePageShell !== 'function') {
526
+ throw new Error('vmz native addon missing generatePageShell — rebuild with `pnpm napi:build`');
527
+ }
528
+ return native.generatePageShell({
529
+ bodyHtml: input.bodyHtml,
530
+ chunkId: input.chunkId || '',
531
+ layoutChain: input.layoutChain || [],
532
+ propsJson,
533
+ meta: {
534
+ title: input.meta.title,
535
+ description: input.meta.description,
536
+ canonical: input.meta.canonical,
537
+ robots: input.meta.robots,
538
+ lang: localeId,
539
+ dir,
540
+ alternates: input.meta.alternates || [],
541
+ },
542
+ // napi Option<String>: omit/undefined = None; null is rejected as String
543
+ ...(input.cssEntry ? { cssEntry: String(input.cssEntry) } : {}),
544
+ isErrorDocument: !!input.isErrorDocument,
545
+ });
551
546
  }
552
547
  /**
553
548
  * @param {string} origin
554
549
  * @param {Array<{ canonical: string, robots: string }>} generations
555
550
  */
556
- function buildSitemap(origin, generations) {
557
- const urls = generations
558
- .filter((g) => !String(g.robots).includes('noindex'))
559
- .map((g) => ` <url>
560
- <loc>${escapeXml(g.canonical)}</loc>
561
- </url>`)
562
- .join('\n');
563
- return `<?xml version="1.0" encoding="UTF-8"?>
564
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
565
- ${urls}
566
- </urlset>
567
- `;
568
- }
569
- function escapeHtml(s) {
570
- return String(s).replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
571
- }
572
- function escapeAttr(s) {
573
- return escapeHtml(s).replaceAll('"', '&quot;');
574
- }
575
- function escapeXml(s) {
576
- return escapeAttr(s).replaceAll("'", '&apos;');
551
+ function buildSitemap(_origin, generations) {
552
+ const urls = generations.filter((g) => !String(g.robots).includes('noindex')).map((g) => ({ loc: g.canonical }));
553
+ const native = requireNativeAddon();
554
+ if (typeof native.generateSitemapXml !== 'function') {
555
+ throw new Error('vmz native addon missing generateSitemapXml — rebuild with `pnpm napi:build`');
556
+ }
557
+ return native.generateSitemapXml(urls);
577
558
  }
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.0",
3
+ "version": "0.1.1",
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,15 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "@vmz/core": "0.1.0",
52
- "@vmz/plugin": "0.1.0",
53
- "@vmz/protocol": "0.1.0",
51
+ "@vmz/core": "0.1.1",
52
+ "@vmz/plugin": "0.1.1",
53
+ "@vmz/protocol": "0.1.1",
54
54
  "jiti": "^2.6.1",
55
55
  "json5": "^2.2.3"
56
56
  },
57
57
  "peerDependencies": {
58
- "@vmz/plugin-markdown-it": "0.1.0",
59
- "@vmz/test": "0.1.0",
58
+ "@vmz/plugin-markdown-it": "0.1.1",
59
+ "@vmz/test": "0.1.1",
60
60
  "typescript": "^5.8.3"
61
61
  },
62
62
  "peerDependenciesMeta": {
@@ -90,12 +90,12 @@
90
90
  "cli"
91
91
  ],
92
92
  "optionalDependencies": {
93
- "@vmz/vmz-win32-x64": "0.1.0",
94
- "@vmz/vmz-win32-arm64": "0.1.0",
95
- "@vmz/vmz-darwin-x64": "0.1.0",
96
- "@vmz/vmz-darwin-arm64": "0.1.0",
97
- "@vmz/vmz-linux-x64": "0.1.0",
98
- "@vmz/vmz-linux-arm64": "0.1.0"
93
+ "@vmz/vmz-win32-x64": "0.1.1",
94
+ "@vmz/vmz-win32-arm64": "0.1.1",
95
+ "@vmz/vmz-darwin-x64": "0.1.1",
96
+ "@vmz/vmz-darwin-arm64": "0.1.1",
97
+ "@vmz/vmz-linux-x64": "0.1.1",
98
+ "@vmz/vmz-linux-arm64": "0.1.1"
99
99
  },
100
100
  "publishConfig": {
101
101
  "access": "public"