@vmz/vmz 0.1.9 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -3,9 +3,9 @@
3
3
  * Node CLI command implementations .
4
4
  */
5
5
  import { spawn } from 'node:child_process';
6
- import { copyFileSync, existsSync } from 'node:fs';
6
+ import { existsSync } from 'node:fs';
7
7
  import path from 'node:path';
8
- import { HOST_PROTOCOL, createWorkspace, getProtocolVersions, resolveCoreRuntimeDist, resolveNativePath } from './index.js';
8
+ import { HOST_PROTOCOL, createWorkspace, getProtocolVersions, materializeServeHostRuntime, resolveCoreRuntimeDist, resolveNativePath, } from './index.js';
9
9
  import { createDevSession } from './dev-session.js';
10
10
  import { gateGlobalProjectCommand, getInvocationContext, isGlobalAllowedCommand } from './invocation.js';
11
11
  import { log } from './log.js';
@@ -501,14 +501,12 @@ async function cmdServe(args) {
501
501
  });
502
502
  const hostJs = path.join(outDir, 'vmz-serve-host.mjs');
503
503
  if (!existsSync(hostJs)) {
504
- const coreDist = resolveCoreRuntimeDist();
505
- const src = coreDist ? path.join(coreDist, 'serve-host.mjs') : null;
506
- if (src && existsSync(src)) {
507
- copyFileSync(src, hostJs);
504
+ try {
505
+ materializeServeHostRuntime(outDir);
508
506
  log.info(`materialized ${hostJs} from @vmz/core (release builds omit it)`);
509
507
  }
510
- else {
511
- log.error(`missing ${hostJs} — run \`vmz build\` (without --release) or ensure @vmz/core is installed`);
508
+ catch (err) {
509
+ log.error(`missing ${hostJs} — run \`vmz build\` (without --release) or ensure @vmz/core is installed (${err instanceof Error ? err.message : err})`);
512
510
  return 1;
513
511
  }
514
512
  }
@@ -1,9 +1,15 @@
1
1
  /**
2
2
  * A3: content-addressed assets/<hash> layout for immutable CDN objects.
3
3
  * Logical paths stay available for serve/dev; static HTML rewrites to hashed URLs.
4
- * Identical bytes identical asset path (cross-release / cross-source reuse by digest).
4
+ * CSS aggregators (vmz.css) rewrite `@import` to hashed sibling paths under assets/.
5
5
  */
6
6
  export declare const CONTENT_ADDRESSED_ASSETS_SCHEMA = "vmz.content_addressed_assets.v0";
7
+ /**
8
+ * Rewrite relative `@import "./foo.css"` to hashed paths under assets/.
9
+ * @param {string} cssText
10
+ * @param {Record<string, string>} rewrites logical (no leading slash) or `/logical` → `assets/hash.ext`
11
+ */
12
+ export declare function rewriteCssImports(cssText: any, rewrites: any): any;
7
13
  /**
8
14
  * Emit `assets/<sha256>.<ext>` copies and rewrite HTML href/src to hashed URLs.
9
15
  * @param {string} distDir
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * A3: content-addressed assets/<hash> layout for immutable CDN objects.
3
3
  * Logical paths stay available for serve/dev; static HTML rewrites to hashed URLs.
4
- * Identical bytes identical asset path (cross-release / cross-source reuse by digest).
4
+ * CSS aggregators (vmz.css) rewrite `@import` to hashed sibling paths under assets/.
5
5
  */
6
6
  // @ts-nocheck
7
7
  import crypto from 'node:crypto';
@@ -16,11 +16,33 @@ const DEFAULT_CANDIDATES = [
16
16
  'entry-event.js',
17
17
  'vmz.css',
18
18
  'vmz-designs.css',
19
+ 'vmz-style.css',
19
20
  'vmz-dom.js',
20
21
  'vmz-runtime.js',
21
22
  'vmz-http.js',
22
23
  'vmz-client-nav.js',
23
24
  ];
25
+ /** CSS files that may @import other logical CSS; processed after leaf CSS is hashed. */
26
+ const CSS_AGGREGATORS = new Set(['vmz.css']);
27
+ const CSS_IMPORT_RE = /@import\s*(?:url\()?['"]?(\.\/)?([^'")\s;]+)['"]?\)?/gi;
28
+ /**
29
+ * Rewrite relative `@import "./foo.css"` to hashed paths under assets/.
30
+ * @param {string} cssText
31
+ * @param {Record<string, string>} rewrites logical (no leading slash) or `/logical` → `assets/hash.ext`
32
+ */
33
+ export function rewriteCssImports(cssText, rewrites) {
34
+ return cssText.replace(CSS_IMPORT_RE, (match, _dot, target) => {
35
+ const logical = String(target || '').replace(/^\.\//, '');
36
+ if (!logical)
37
+ return match;
38
+ const hashed = rewrites[logical] || rewrites[`/${logical}`] || rewrites[`assets/${logical}`];
39
+ if (!hashed)
40
+ return match;
41
+ const rel = hashed.startsWith('/') ? hashed.slice(1) : hashed;
42
+ const sibling = rel.startsWith('assets/') ? `./${path.basename(rel)}` : `./${rel}`;
43
+ return `@import"${sibling}"`;
44
+ });
45
+ }
24
46
  /**
25
47
  * Emit `assets/<sha256>.<ext>` copies and rewrite HTML href/src to hashed URLs.
26
48
  * @param {string} distDir
@@ -38,36 +60,24 @@ export function emitContentAddressedAssets(distDir, opts = {}) {
38
60
  const objects = [];
39
61
  /** @type {Record<string, string>} */
40
62
  const rewrites = {};
41
- for (const rel of candidates) {
42
- const logical = String(rel).replace(/\\/g, '/').replace(/^\//, '');
43
- const src = path.join(abs, ...logical.split('/'));
44
- if (!fs.existsSync(src) || !fs.statSync(src).isFile())
63
+ const ordered = orderCandidates(candidates);
64
+ for (const rel of ordered) {
65
+ ingestCandidate(abs, rel, rewrites, objects, { transform: null });
66
+ }
67
+ // Aggregator CSS (vmz.css) must import hashed leaf files — rewrite then hash.
68
+ for (const rel of ordered) {
69
+ if (!CSS_AGGREGATORS.has(rel))
45
70
  continue;
46
- const buf = fs.readFileSync(src);
47
- const digest = sha256Hex(buf);
48
- const ext = path.extname(logical) || '';
49
- const assetRel = `assets/${digest}${ext}`;
50
- const dest = path.join(abs, ...assetRel.split('/'));
51
- if (!fs.existsSync(dest)) {
52
- fs.mkdirSync(path.dirname(dest), { recursive: true });
53
- fs.writeFileSync(dest, buf);
54
- }
55
- else {
56
- // Cross-release reuse: identical digest must not be rewritten.
57
- const existing = sha256Hex(fs.readFileSync(dest));
58
- if (existing !== digest) {
59
- throw new Error(`content-address collision at ${assetRel}`);
60
- }
61
- }
62
- objects.push({
63
- logicalPath: logical,
64
- assetPath: assetRel,
65
- digest,
66
- bytes: buf.length,
67
- immutable: true,
71
+ const src = path.join(abs, rel);
72
+ if (!fs.existsSync(src))
73
+ continue;
74
+ const rewritten = rewriteCssImports(fs.readFileSync(src, 'utf8'), rewrites);
75
+ removeLogicalObject(objects, rel);
76
+ delete rewrites[`/${rel}`];
77
+ delete rewrites[rel];
78
+ ingestCandidate(abs, rel, rewrites, objects, {
79
+ transform: () => Buffer.from(rewritten, 'utf8'),
68
80
  });
69
- rewrites[`/${logical}`] = `/${assetRel}`;
70
- rewrites[logical] = assetRel;
71
81
  }
72
82
  objects.sort((a, b) => (a.logicalPath < b.logicalPath ? -1 : a.logicalPath > b.logicalPath ? 1 : 0));
73
83
  let rewrittenHtml = 0;
@@ -89,6 +99,74 @@ export function emitContentAddressedAssets(distDir, opts = {}) {
89
99
  writePrettyJsonFile(outPath, manifest);
90
100
  return { manifest, assetsDir, rewrites, manifestPath: outPath };
91
101
  }
102
+ /**
103
+ * @param {string[]} candidates
104
+ */
105
+ function orderCandidates(candidates) {
106
+ const set = new Set(candidates.map((c) => String(c).replace(/\\/g, '/').replace(/^\//, '')));
107
+ /** @type {string[]} */
108
+ const out = [];
109
+ for (const name of DEFAULT_CANDIDATES) {
110
+ if (set.has(name) && !CSS_AGGREGATORS.has(name))
111
+ out.push(name);
112
+ }
113
+ for (const name of [...set].sort()) {
114
+ if (!CSS_AGGREGATORS.has(name) && !out.includes(name))
115
+ out.push(name);
116
+ }
117
+ if (set.has('vmz.css'))
118
+ out.push('vmz.css');
119
+ return out;
120
+ }
121
+ /**
122
+ * @param {Array<Record<string, any>>} objects
123
+ * @param {string} logical
124
+ */
125
+ function removeLogicalObject(objects, logical) {
126
+ const idx = objects.findIndex((o) => o.logicalPath === logical);
127
+ if (idx >= 0)
128
+ objects.splice(idx, 1);
129
+ }
130
+ /**
131
+ * @param {string} absDist
132
+ * @param {string} rel
133
+ * @param {Record<string, string>} rewrites
134
+ * @param {Array<Record<string, any>>} objects
135
+ * @param {{ transform?: ((buf: Buffer) => Buffer) | null }} opts
136
+ */
137
+ function ingestCandidate(absDist, rel, rewrites, objects, opts) {
138
+ const logical = String(rel).replace(/\\/g, '/').replace(/^\//, '');
139
+ const src = path.join(absDist, ...logical.split('/'));
140
+ if (!fs.existsSync(src) || !fs.statSync(src).isFile())
141
+ return;
142
+ let buf = fs.readFileSync(src);
143
+ if (typeof opts.transform === 'function') {
144
+ buf = opts.transform(buf);
145
+ }
146
+ const digest = sha256Hex(buf);
147
+ const ext = path.extname(logical) || '';
148
+ const assetRel = `assets/${digest}${ext}`;
149
+ const dest = path.join(absDist, ...assetRel.split('/'));
150
+ if (!fs.existsSync(dest)) {
151
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
152
+ fs.writeFileSync(dest, buf);
153
+ }
154
+ else {
155
+ const existing = sha256Hex(fs.readFileSync(dest));
156
+ if (existing !== digest) {
157
+ throw new Error(`content-address collision at ${assetRel}`);
158
+ }
159
+ }
160
+ objects.push({
161
+ logicalPath: logical,
162
+ assetPath: assetRel,
163
+ digest,
164
+ bytes: buf.length,
165
+ immutable: true,
166
+ });
167
+ rewrites[`/${logical}`] = `/${assetRel}`;
168
+ rewrites[logical] = assetRel;
169
+ }
92
170
  /**
93
171
  * Resolve an immutable object by digest under dist/assets (cross-source reuse).
94
172
  * @param {string} distDir
@@ -124,7 +202,6 @@ export function assertSharedAssetPath(distDir, a, b, ext = '.js') {
124
202
  const rel = `assets/${da}${ext}`;
125
203
  const dest = path.join(distDir, ...rel.split('/'));
126
204
  fs.writeFileSync(dest, typeof a === 'string' ? Buffer.from(a) : a);
127
- // Second write of identical bytes must be reuse, not fork.
128
205
  fs.writeFileSync(dest, typeof b === 'string' ? Buffer.from(b) : b);
129
206
  const again = resolveAssetByDigest(distDir, da, ext);
130
207
  if (!again || again.assetPath !== rel) {
@@ -139,7 +216,6 @@ function collectCandidates(distDir) {
139
216
  if (fs.existsSync(path.join(distDir, name)))
140
217
  out.push(name);
141
218
  }
142
- // Include top-level *.client.js and pages/**/*.client.js referenced by resume.
143
219
  walk(distDir, distDir, (rel) => {
144
220
  if (/\.client\.js$/i.test(rel))
145
221
  out.push(rel);
@@ -173,7 +249,6 @@ function rewriteHtmlReferences(distDir, rewrites) {
173
249
  let text = fs.readFileSync(file, 'utf8');
174
250
  let next = text;
175
251
  for (const [from, to] of pairs) {
176
- // href="/x" src="/x" and unquoted variants in attributes
177
252
  next = next.split(from).join(to);
178
253
  }
179
254
  if (next !== text) {
package/dist/index.d.ts CHANGED
@@ -53,6 +53,14 @@ export declare function handshake(host?: {
53
53
  * @returns {string | null}
54
54
  */
55
55
  export declare function resolveCoreRuntimeDist(): string;
56
+ /** Runtime companions required by dist/vmz-serve-host.mjs relative imports. */
57
+ export declare const SERVE_HOST_RUNTIME_FILES: string[][];
58
+ /**
59
+ * Copy serve-host + registry bootstrap modules from `@vmz/core` into app outDir.
60
+ * @param {string} outDir
61
+ * @param {string} [coreDist]
62
+ */
63
+ export declare function materializeServeHostRuntime(outDir: any, coreDist?: string): void;
56
64
  /**
57
65
  * @typedef {object} WorkspaceOptions
58
66
  * @property {string} root
@@ -234,6 +242,8 @@ declare const _default: {
234
242
  expectedProtocol: typeof expectedProtocol;
235
243
  resolveNativePath: typeof resolveNativePath;
236
244
  resolveCoreRuntimeDist: typeof resolveCoreRuntimeDist;
245
+ materializeServeHostRuntime: typeof materializeServeHostRuntime;
246
+ SERVE_HOST_RUNTIME_FILES: string[][];
237
247
  loadNative: typeof loadNative;
238
248
  getProtocolVersions: typeof getProtocolVersions;
239
249
  handshake: typeof handshake;
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * Coarse-grained only — no transform hooks / per-AST callbacks.
5
5
  */
6
6
  import { createRequire } from 'node:module';
7
- import { existsSync } from 'node:fs';
7
+ import { copyFileSync, existsSync } from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { fileURLToPath } from 'node:url';
10
10
  import { materializeWechatPackaging } from './wechat-packaging.js';
@@ -144,6 +144,31 @@ export function resolveCoreRuntimeDist() {
144
144
  return nested;
145
145
  return null;
146
146
  }
147
+ /** Runtime companions required by dist/vmz-serve-host.mjs relative imports. */
148
+ export const SERVE_HOST_RUNTIME_FILES = [
149
+ ['serve-host.mjs', 'vmz-serve-host.mjs'],
150
+ ['list-client-components.js', 'list-client-components.js'],
151
+ ['deployment-registry.js', 'deployment-registry.js'],
152
+ ['render-host.js', 'render-host.js'],
153
+ ];
154
+ /**
155
+ * Copy serve-host + registry bootstrap modules from `@vmz/core` into app outDir.
156
+ * @param {string} outDir
157
+ * @param {string} [coreDist]
158
+ */
159
+ export function materializeServeHostRuntime(outDir, coreDist = resolveCoreRuntimeDist()) {
160
+ if (!coreDist) {
161
+ throw new Error('materializeServeHostRuntime: @vmz/core dist not found');
162
+ }
163
+ for (const [srcName, outName] of SERVE_HOST_RUNTIME_FILES) {
164
+ const src = path.join(coreDist, srcName);
165
+ const dst = path.join(outDir, outName);
166
+ if (!existsSync(src)) {
167
+ throw new Error(`materializeServeHostRuntime: missing ${src}`);
168
+ }
169
+ copyFileSync(src, dst);
170
+ }
171
+ }
147
172
  /**
148
173
  * @typedef {object} WorkspaceOptions
149
174
  * @property {string} root
@@ -458,6 +483,8 @@ export default {
458
483
  expectedProtocol,
459
484
  resolveNativePath,
460
485
  resolveCoreRuntimeDist,
486
+ materializeServeHostRuntime,
487
+ SERVE_HOST_RUNTIME_FILES,
461
488
  loadNative,
462
489
  getProtocolVersions,
463
490
  handshake,
@@ -7,7 +7,8 @@ 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 { preloadComponentRegistry } from '@vmz/core/component-registry';
10
+ import { createRenderHost } from '@vmz/core/render-host';
11
+ import { listClientComponentsSync } from '@vmz/core/component-registry';
11
12
  import { emitCdnPolicy } from './cdn-policy.js';
12
13
  import { emitContentAddressedAssets } from './content-addressed-assets.js';
13
14
  import { absoluteUrl, buildLocalePageMeta, localizeBodyLinks } from './locale-router.js';
@@ -30,8 +31,8 @@ export async function emitWebStatic(distDir, opts = {}) {
30
31
  if (!fs.existsSync(domPath)) {
31
32
  throw new Error(`emitWebStatic: missing ${domPath} — run vmz build first`);
32
33
  }
33
- const { renderToString, renderToStream, registerComponents } = await import(pathToFileURL(domPath).href);
34
- await preloadComponentRegistry(distDir, registerComponents);
34
+ const host = await createRenderHost(distDir, { strictDeployment: true, preload: 'none' });
35
+ const { renderToString, renderToStream } = host;
35
36
  const pageCatalog = listPageClientFiles(distDir);
36
37
  /** @type {Array<{
37
38
  * routeId: string,
@@ -100,6 +101,7 @@ export async function emitWebStatic(distDir, opts = {}) {
100
101
  }
101
102
  const meta = await resolvePageMeta(Page, { params, props, pathname: pattern, origin });
102
103
  const layoutChain = resolveLayoutChain(distDir, page.chunkId);
104
+ await host.ensureComponents([page.chunkId, ...layoutChain]);
103
105
  let bodyHtml = '';
104
106
  for await (const chunk of renderToStream(Page, props, {})) {
105
107
  bodyHtml += chunk;
@@ -217,6 +219,7 @@ export async function emitWebStatic(distDir, opts = {}) {
217
219
  const digest = sha256Hex(canonicalJson(manifest));
218
220
  manifest.manifestDigest = digest;
219
221
  writePrettyJsonFile(path.join(vmzDir, 'static-delivery-manifest.json'), manifest);
222
+ emitStaticClientEntries(distDir, pageCatalog);
220
223
  const assets = emitContentAddressedAssets(distDir);
221
224
  manifest.contentAddressedAssets = {
222
225
  schema: assets.manifest.schema,
@@ -558,3 +561,50 @@ function buildSitemap(_origin, generations) {
558
561
  }
559
562
  return native.generateSitemapXml(urls);
560
563
  }
564
+ /**
565
+ * Static CDN must ship entry-client/event like serve-host (content-addressed + HTML rewrite).
566
+ * @param {string} distDir
567
+ * @param {Array<{ chunkId: string }>} pageCatalog
568
+ */
569
+ function emitStaticClientEntries(distDir, pageCatalog) {
570
+ const componentEntries = listClientComponentsSync(distDir, { strict: true });
571
+ const indexChunk = pageCatalog.find((p) => p.chunkId === 'pages/index')?.chunkId || pageCatalog[0]?.chunkId || 'pages/index';
572
+ const resumeEntries = loadPageResumeEntriesSync(distDir, indexChunk);
573
+ const lazySet = new Set(resumeEntries
574
+ .filter((e) => isEventResumeStrategy(e.strategy))
575
+ .map((e) => e.component)
576
+ .filter(Boolean));
577
+ const eager = componentEntries.filter((e) => !lazySet.has(e.name));
578
+ const lazy = componentEntries.filter((e) => lazySet.has(e.name));
579
+ const native = requireNativeAddon();
580
+ if (typeof native.generateServeEntryClient !== 'function') {
581
+ throw new Error('vmz native addon missing generateServeEntryClient — rebuild with `pnpm napi:build`');
582
+ }
583
+ fs.writeFileSync(path.join(distDir, 'entry-client.js'), native.generateServeEntryClient(eager, lazy, ''), 'utf8');
584
+ if (typeof native.generateServeEntryEvent === 'function') {
585
+ fs.writeFileSync(path.join(distDir, 'entry-event.js'), native.generateServeEntryEvent(''), 'utf8');
586
+ }
587
+ }
588
+ /**
589
+ * @param {string} distDir
590
+ * @param {string} chunkId
591
+ */
592
+ function loadPageResumeEntriesSync(distDir, chunkId) {
593
+ try {
594
+ const dep = JSON.parse(fs.readFileSync(path.join(distDir, 'vmz-deployment.json'), 'utf8'));
595
+ const units = Array.isArray(dep.units) ? dep.units : [];
596
+ const page = units.find((u) => u.chunkId === chunkId) || units.find((u) => u.chunkId === 'pages/index') || units.find((u) => u.kind === 'page');
597
+ const entries = Array.isArray(page?.resumeEntries) ? page.resumeEntries : [];
598
+ return entries.map((e) => ({
599
+ component: String(e.component || ''),
600
+ strategy: String(e.strategy || ''),
601
+ }));
602
+ }
603
+ catch {
604
+ return [];
605
+ }
606
+ }
607
+ /** @param {string} strategy */
608
+ function isEventResumeStrategy(strategy) {
609
+ return strategy === 'event' || strategy === 'click' || String(strategy).startsWith('event:');
610
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vmz/vmz",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
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.9",
52
- "@vmz/plugin": "0.1.9",
53
- "@vmz/protocol": "0.1.9",
51
+ "@vmz/core": "0.1.10",
52
+ "@vmz/plugin": "0.1.10",
53
+ "@vmz/protocol": "0.1.10",
54
54
  "jiti": "^2.6.1",
55
55
  "json5": "^2.2.3"
56
56
  },
57
57
  "peerDependencies": {
58
- "@vmz/plugin-markdown-it": "0.1.9",
59
- "@vmz/test": "0.1.9",
58
+ "@vmz/plugin-markdown-it": "0.1.10",
59
+ "@vmz/test": "0.1.10",
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.9",
94
- "@vmz/vmz-win32-arm64": "0.1.9",
95
- "@vmz/vmz-darwin-x64": "0.1.9",
96
- "@vmz/vmz-darwin-arm64": "0.1.9",
97
- "@vmz/vmz-linux-x64": "0.1.9",
98
- "@vmz/vmz-linux-arm64": "0.1.9"
93
+ "@vmz/vmz-win32-x64": "0.1.10",
94
+ "@vmz/vmz-win32-arm64": "0.1.10",
95
+ "@vmz/vmz-darwin-x64": "0.1.10",
96
+ "@vmz/vmz-darwin-arm64": "0.1.10",
97
+ "@vmz/vmz-linux-x64": "0.1.10",
98
+ "@vmz/vmz-linux-arm64": "0.1.10"
99
99
  },
100
100
  "publishConfig": {
101
101
  "access": "public"