@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
@@ -9,6 +9,8 @@
9
9
  import crypto from 'node:crypto';
10
10
  import fs from 'node:fs';
11
11
  import path from 'node:path';
12
+ import { writePrettyJsonFile } from './pretty-json.js';
13
+ import { listPublicPageUnits, unitBrowserPathPattern } from './route-path.js';
12
14
  export const RELEASE_ENVELOPE_SCHEMA = 'vmz.release.envelope.v0';
13
15
  export const APPLICATION_ARTIFACT_SCHEMA = 'vmz.application.artifact.v0';
14
16
  export const DELIVERY_ARTIFACT_MANIFEST_SCHEMA = 'vmz.profile.delivery_artifact_manifest.v0';
@@ -88,21 +90,6 @@ function listContentFiles(distDir) {
88
90
  out.sort();
89
91
  return out;
90
92
  }
91
- /**
92
- * @param {string} chunkId
93
- */
94
- function pathPatternFromChunk(chunkId) {
95
- const rel = chunkId.replace(/^pages\//, '');
96
- const parts = rel.split('/').filter(Boolean);
97
- const segs = [];
98
- for (let i = 0; i < parts.length; i++) {
99
- const p = parts[i];
100
- if (p === 'index' && i === parts.length - 1)
101
- continue;
102
- segs.push(p);
103
- }
104
- return segs.length ? `/${segs.join('/')}` : '/';
105
- }
106
93
  /**
107
94
  * Pack `dist/` into `_vmz` manifests + release envelope (filesystem Delivery Profile).
108
95
  * @param {string} distDir
@@ -124,13 +111,13 @@ export function packRelease(distDir, opts = {}) {
124
111
  for (const rel of files) {
125
112
  fileDigests[rel] = sha256File(path.join(abs, ...rel.split('/')));
126
113
  }
127
- const pages = (deployment.units || []).filter((u) => u.kind === 'page');
114
+ const pages = listPublicPageUnits(deployment);
128
115
  const routeRealization = {
129
116
  schema: ROUTE_REALIZATION_TABLE_SCHEMA,
130
117
  routes: pages.map((u) => ({
131
118
  routeId: String(u.chunkId),
132
119
  chunkId: String(u.chunkId),
133
- pathPattern: pathPatternFromChunk(String(u.chunkId)),
120
+ pathPattern: unitBrowserPathPattern(u),
134
121
  clientEntry: u.clientEntry || null,
135
122
  programIr: u.programIr || null,
136
123
  })),
@@ -200,7 +187,7 @@ export function packRelease(distDir, opts = {}) {
200
187
  * @param {unknown} value
201
188
  */
202
189
  function writeJson(file, value) {
203
- fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
190
+ writePrettyJsonFile(file, value);
204
191
  }
205
192
  /**
206
193
  * @param {string} pointerPath
@@ -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
  }
@@ -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
  /**
@@ -135,7 +136,7 @@ export function normalizeSiteDelivery(raw, opts = {}) {
135
136
  artifact: String(d.artifact),
136
137
  expectedCompatibility: d.expectedCompatibility || {
137
138
  runtime: 'vmz',
138
- deliveryProfiles: ['web-static', 'filesystem'],
139
+ deliveryProfiles: ['static', 'filesystem'],
139
140
  },
140
141
  sources,
141
142
  resolutionPolicy: {
@@ -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
  }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Site favicon: author SVG → PNG + ICO + `_vmz/site-favicon.json` head links.
3
+ * Convention: `assets/favicon.svg` (or `favicon.svg`) under project / already in dist.
4
+ */
5
+ export declare const SITE_FAVICON_SCHEMA = "vmz.site_favicon.v0";
6
+ /**
7
+ * @param {string} distDir
8
+ * @param {{ projectRoot?: string, skipNative?: boolean }} [opts]
9
+ */
10
+ export declare function emitSiteFavicon(distDir: any, opts?: {}): {
11
+ schema: string;
12
+ status: string;
13
+ source: string;
14
+ svg: string;
15
+ ico: string;
16
+ png: string[];
17
+ headHtml: string;
18
+ } | {
19
+ status: string;
20
+ reason: string;
21
+ };
22
+ /**
23
+ * Read ready head HTML for static/SSR shells (empty when missing).
24
+ * @param {string} distDir
25
+ */
26
+ export declare function readSiteFaviconHeadHtml(distDir: any): any;
27
+ /**
28
+ * PNG-in-ICO (Vista+); widely accepted by modern browsers.
29
+ * @param {Array<{ png: Buffer, size: number }>} images
30
+ */
31
+ export declare function packPngsIntoIco(images: any): Buffer<ArrayBuffer>;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Site favicon: author SVG → PNG + ICO + `_vmz/site-favicon.json` head links.
3
+ * Convention: `assets/favicon.svg` (or `favicon.svg`) under project / already in dist.
4
+ */
5
+ // @ts-nocheck
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { requireNativeAddon } from './native-addon.js';
9
+ import { writePrettyJsonFile } from './pretty-json.js';
10
+ export const SITE_FAVICON_SCHEMA = 'vmz.site_favicon.v0';
11
+ const ICO_SIZES = [16, 32, 48];
12
+ /**
13
+ * @param {string} distDir
14
+ * @param {{ projectRoot?: string, skipNative?: boolean }} [opts]
15
+ */
16
+ export function emitSiteFavicon(distDir, opts = {}) {
17
+ const absDist = path.resolve(distDir);
18
+ const projectRoot = opts.projectRoot ? path.resolve(opts.projectRoot) : null;
19
+ const src = discoverFaviconSvg(absDist, projectRoot);
20
+ if (!src) {
21
+ return { status: 'skipped', reason: 'no assets/favicon.svg' };
22
+ }
23
+ const svgText = fs.readFileSync(src.path, 'utf8');
24
+ const assetsDir = path.join(absDist, 'assets');
25
+ fs.mkdirSync(assetsDir, { recursive: true });
26
+ const svgDest = path.join(assetsDir, 'favicon.svg');
27
+ if (path.resolve(src.path) !== path.resolve(svgDest)) {
28
+ fs.copyFileSync(src.path, svgDest);
29
+ }
30
+ /** @type {Buffer[]} */
31
+ const pngs = [];
32
+ if (!opts.skipNative) {
33
+ const native = requireNativeAddon();
34
+ if (typeof native.rasterizeSvgPng !== 'function') {
35
+ throw new Error('vmz native addon missing rasterizeSvgPng — rebuild with `pnpm napi:build`');
36
+ }
37
+ for (const px of ICO_SIZES) {
38
+ const buf = Buffer.from(native.rasterizeSvgPng(svgText, px));
39
+ pngs.push(buf);
40
+ fs.writeFileSync(path.join(assetsDir, `favicon-${px}.png`), buf);
41
+ }
42
+ }
43
+ let icoPath = null;
44
+ if (pngs.length) {
45
+ const ico = packPngsIntoIco(pngs.map((png, i) => ({ png, size: ICO_SIZES[i] })));
46
+ icoPath = path.join(absDist, 'favicon.ico');
47
+ fs.writeFileSync(icoPath, ico);
48
+ }
49
+ const headParts = [];
50
+ if (icoPath) {
51
+ headParts.push('<link rel="icon" href="/favicon.ico" sizes="any">');
52
+ }
53
+ headParts.push('<link rel="icon" href="/assets/favicon.svg" type="image/svg+xml">');
54
+ for (const px of ICO_SIZES) {
55
+ if (fs.existsSync(path.join(assetsDir, `favicon-${px}.png`))) {
56
+ headParts.push(`<link rel="icon" type="image/png" sizes="${px}x${px}" href="/assets/favicon-${px}.png">`);
57
+ }
58
+ }
59
+ const headHtml = `${headParts.join('\n')}\n`;
60
+ const artifact = {
61
+ schema: SITE_FAVICON_SCHEMA,
62
+ status: 'ready',
63
+ source: src.rel,
64
+ svg: 'assets/favicon.svg',
65
+ ico: icoPath ? 'favicon.ico' : null,
66
+ png: ICO_SIZES.filter((px) => fs.existsSync(path.join(assetsDir, `favicon-${px}.png`))).map((px) => `assets/favicon-${px}.png`),
67
+ headHtml,
68
+ };
69
+ const vmzDir = path.join(absDist, '_vmz');
70
+ fs.mkdirSync(vmzDir, { recursive: true });
71
+ writePrettyJsonFile(path.join(vmzDir, 'site-favicon.json'), artifact);
72
+ return artifact;
73
+ }
74
+ /**
75
+ * Read ready head HTML for static/SSR shells (empty when missing).
76
+ * @param {string} distDir
77
+ */
78
+ export function readSiteFaviconHeadHtml(distDir) {
79
+ try {
80
+ const p = path.join(distDir, '_vmz', 'site-favicon.json');
81
+ if (!fs.existsSync(p))
82
+ return '';
83
+ const j = JSON.parse(fs.readFileSync(p, 'utf8'));
84
+ if (j?.status !== 'ready' || typeof j.headHtml !== 'string')
85
+ return '';
86
+ return j.headHtml;
87
+ }
88
+ catch {
89
+ return '';
90
+ }
91
+ }
92
+ /**
93
+ * @param {string} distDir
94
+ * @param {string|null} projectRoot
95
+ */
96
+ function discoverFaviconSvg(distDir, projectRoot) {
97
+ const candidates = [path.join(distDir, 'assets', 'favicon.svg'), path.join(distDir, 'favicon.svg')];
98
+ if (projectRoot) {
99
+ candidates.push(path.join(projectRoot, 'assets', 'favicon.svg'), path.join(projectRoot, 'favicon.svg'), path.join(projectRoot, 'public', 'assets', 'favicon.svg'), path.join(projectRoot, 'public', 'favicon.svg'));
100
+ }
101
+ for (const abs of candidates) {
102
+ if (fs.existsSync(abs) && fs.statSync(abs).isFile()) {
103
+ return {
104
+ path: abs,
105
+ rel: path.relative(projectRoot || distDir, abs).replace(/\\/g, '/') || 'assets/favicon.svg',
106
+ };
107
+ }
108
+ }
109
+ return null;
110
+ }
111
+ /**
112
+ * PNG-in-ICO (Vista+); widely accepted by modern browsers.
113
+ * @param {Array<{ png: Buffer, size: number }>} images
114
+ */
115
+ export function packPngsIntoIco(images) {
116
+ const count = images.length;
117
+ const header = Buffer.alloc(6);
118
+ header.writeUInt16LE(0, 0);
119
+ header.writeUInt16LE(1, 2);
120
+ header.writeUInt16LE(count, 4);
121
+ const entries = Buffer.alloc(16 * count);
122
+ /** @type {Buffer[]} */
123
+ const blobs = [];
124
+ let offset = 6 + 16 * count;
125
+ for (let i = 0; i < count; i += 1) {
126
+ const { png, size } = images[i];
127
+ const o = i * 16;
128
+ entries.writeUInt8(size >= 256 ? 0 : size, o);
129
+ entries.writeUInt8(size >= 256 ? 0 : size, o + 1);
130
+ entries.writeUInt8(0, o + 2);
131
+ entries.writeUInt8(0, o + 3);
132
+ entries.writeUInt16LE(1, o + 4);
133
+ entries.writeUInt16LE(32, o + 6);
134
+ entries.writeUInt32LE(png.length, o + 8);
135
+ entries.writeUInt32LE(offset, o + 12);
136
+ blobs.push(png);
137
+ offset += png.length;
138
+ }
139
+ return Buffer.concat([header, entries, ...blobs]);
140
+ }
@@ -43,6 +43,12 @@ export declare function emitWebStatic(distDir: any, opts?: {}): Promise<{
43
43
  sitemap: string;
44
44
  robots: string;
45
45
  };
46
+ publicAssets: {
47
+ schema: string;
48
+ status: string;
49
+ fileCount: number;
50
+ source: any;
51
+ };
46
52
  };
47
53
  htmlFiles: any[];
48
54
  skipped: any[];
@@ -55,6 +61,21 @@ export declare function emitWebStatic(distDir: any, opts?: {}): Promise<{
55
61
  objects: any[];
56
62
  rewrittenHtml: number;
57
63
  };
64
+ publicAssets: {
65
+ schema: string;
66
+ status: string;
67
+ reason: string;
68
+ fileCount: number;
69
+ files: any[];
70
+ skippedConflicts: any[];
71
+ } | {
72
+ schema: string;
73
+ status: string;
74
+ source: string;
75
+ fileCount: number;
76
+ files: any[];
77
+ skippedConflicts: any[];
78
+ };
58
79
  cdnPolicy: {
59
80
  schema: string;
60
81
  applicationId: any;