@vmz/test 0.3.11 → 0.3.13

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.
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Direct browser harness packing for `vmz test --mode browser`.
3
+ *
4
+ * The Direct harness serves raw compiler output (`<chunkId>.client.js`) over a
5
+ * static file server. Those artifacts keep bare package imports (e.g.
6
+ * `@vmz/ui-icons/registry` inside `@vmz/ui` components) that a browser cannot
7
+ * resolve — the `dist/vendor/**` lowering that used to rewrite them is retired.
8
+ * The harness therefore packs the tested component chain through the Rust
9
+ * `vmz-pack` N-API binding (browser target) before mounting, and the page loads
10
+ * the packed entry chunks from `_vmz/pack-test/` instead of the raw artifacts.
11
+ *
12
+ * The wire shape below is the minimal test-host slice of the frozen v0 pack
13
+ * contract (`packWrite(requestJson, outDir)` → response JSON with
14
+ * `plan.entryFiles`); no new contract fields are introduced here.
15
+ */
16
+ /**
17
+ * Pack the direct-harness component set (tested chunk + registry closure) for
18
+ * the browser target into `<outDir>/_vmz/pack-test`. Returns logical chunkId →
19
+ * packed entry file relative to the pack output dir. A missing client entry or
20
+ * a logical absent from the plan is an explicit error — never a silent skip.
21
+ */
22
+ export declare function packHarnessComponents(outDir: string, chunkIds: string[]): Map<string, string>;
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Direct browser harness packing for `vmz test --mode browser`.
3
+ *
4
+ * The Direct harness serves raw compiler output (`<chunkId>.client.js`) over a
5
+ * static file server. Those artifacts keep bare package imports (e.g.
6
+ * `@vmz/ui-icons/registry` inside `@vmz/ui` components) that a browser cannot
7
+ * resolve — the `dist/vendor/**` lowering that used to rewrite them is retired.
8
+ * The harness therefore packs the tested component chain through the Rust
9
+ * `vmz-pack` N-API binding (browser target) before mounting, and the page loads
10
+ * the packed entry chunks from `_vmz/pack-test/` instead of the raw artifacts.
11
+ *
12
+ * The wire shape below is the minimal test-host slice of the frozen v0 pack
13
+ * contract (`packWrite(requestJson, outDir)` → response JSON with
14
+ * `plan.entryFiles`); no new contract fields are introduced here.
15
+ */
16
+ import fs from 'node:fs';
17
+ import { createRequire } from 'node:module';
18
+ import path from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+ const require = createRequire(import.meta.url);
21
+ /** Directory (relative to outDir) where the harness pack is written. */
22
+ const PACK_TEST_OUTPUT_DIR_REL = '_vmz/pack-test';
23
+ let cached;
24
+ /**
25
+ * Same native addon loading as the test host serve path: `VMZ_NATIVE_NODE`
26
+ * override first, then `@vmz/vmz-<short>` package resolution, then the
27
+ * workspace sibling layout. This host must not import the CLI package's
28
+ * internal modules, so the discovery is mirrored here.
29
+ */
30
+ function tryLoadPackBinding() {
31
+ if (cached !== undefined)
32
+ return cached;
33
+ try {
34
+ const envPath = (typeof process.env.VMZ_NATIVE_NODE === 'string' && process.env.VMZ_NATIVE_NODE.trim()) || '';
35
+ if (envPath) {
36
+ cached = require(path.resolve(envPath));
37
+ return cached;
38
+ }
39
+ const { platform, arch } = process;
40
+ let triple = `${platform}-${arch}`;
41
+ if (platform === 'win32' && arch === 'x64')
42
+ triple = 'win32-x64-msvc';
43
+ else if (platform === 'win32' && arch === 'arm64')
44
+ triple = 'win32-arm64-msvc';
45
+ else if (platform === 'darwin' && arch === 'arm64')
46
+ triple = 'darwin-arm64';
47
+ else if (platform === 'darwin' && arch === 'x64')
48
+ triple = 'darwin-x64';
49
+ else if (platform === 'linux' && arch === 'x64')
50
+ triple = 'linux-x64-gnu';
51
+ else if (platform === 'linux' && arch === 'arm64')
52
+ triple = 'linux-arm64-gnu';
53
+ const short = triple === 'win32-x64-msvc'
54
+ ? 'win32-x64'
55
+ : triple === 'win32-arm64-msvc'
56
+ ? 'win32-arm64'
57
+ : triple === 'linux-x64-gnu'
58
+ ? 'linux-x64'
59
+ : triple === 'linux-arm64-gnu'
60
+ ? 'linux-arm64'
61
+ : triple;
62
+ const name = `@vmz/vmz-${short}`;
63
+ const candidates = [];
64
+ try {
65
+ const resolved = require.resolve(`${name}/package.json`);
66
+ const dir = path.dirname(resolved);
67
+ candidates.push(path.join(dir, `vmz.${triple}.node`), path.join(dir, 'vmz.node'));
68
+ }
69
+ catch {
70
+ /* optional dep missing */
71
+ }
72
+ const here = path.dirname(fileURLToPath(import.meta.url));
73
+ candidates.push(path.join(here, '..', '..', '..', 'runtimes', `vmz-${short}`, `vmz.${triple}.node`), path.join(here, '..', '..', '..', 'runtimes', `vmz-${short}`, 'vmz.node'));
74
+ for (const p of candidates) {
75
+ if (fs.existsSync(p)) {
76
+ cached = require(p);
77
+ return cached;
78
+ }
79
+ }
80
+ cached = null;
81
+ }
82
+ catch {
83
+ cached = null;
84
+ }
85
+ return cached;
86
+ }
87
+ function requirePackBinding() {
88
+ const binding = tryLoadPackBinding();
89
+ if (!binding || typeof binding.packWrite !== 'function') {
90
+ throw new Error('browser harness pack: vmz native addon with packWrite missing — run `pnpm napi:build`');
91
+ }
92
+ return binding;
93
+ }
94
+ /** Minimal response read: `ok:false` throws with every diagnostic, `plan.entryFiles` is the only payload consumed. */
95
+ function parsePackEntryFiles(raw) {
96
+ let parsed;
97
+ try {
98
+ parsed = JSON.parse(raw);
99
+ }
100
+ catch (error) {
101
+ throw new Error(`browser harness pack: binding returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
102
+ }
103
+ if (!parsed || typeof parsed !== 'object' || typeof parsed.ok !== 'boolean') {
104
+ throw new Error('browser harness pack: response must be an object with boolean ok');
105
+ }
106
+ const resp = parsed;
107
+ const diagnostics = Array.isArray(resp.diagnostics) ? resp.diagnostics : [];
108
+ if (resp.ok === false) {
109
+ const summary = diagnostics.length
110
+ ? diagnostics.map((d) => `${String(d?.code ?? '')}: ${String(d?.message ?? '')}`).join(' | ')
111
+ : 'binding reported failure without diagnostics';
112
+ throw new Error(`browser harness pack failed: ${summary}`);
113
+ }
114
+ const entryFiles = resp.plan?.entryFiles;
115
+ if (!Array.isArray(entryFiles)) {
116
+ throw new Error('browser harness pack: response plan.entryFiles must be an array');
117
+ }
118
+ return entryFiles.map((e, i) => {
119
+ const item = e;
120
+ if (typeof item?.logical !== 'string' || !item.logical || typeof item?.file !== 'string' || !item.file) {
121
+ throw new Error(`browser harness pack: response plan.entryFiles[${i}] must have string logical and file`);
122
+ }
123
+ return { logical: item.logical, file: item.file };
124
+ });
125
+ }
126
+ /**
127
+ * Pack the direct-harness component set (tested chunk + registry closure) for
128
+ * the browser target into `<outDir>/_vmz/pack-test`. Returns logical chunkId →
129
+ * packed entry file relative to the pack output dir. A missing client entry or
130
+ * a logical absent from the plan is an explicit error — never a silent skip.
131
+ */
132
+ export function packHarnessComponents(outDir, chunkIds) {
133
+ const absOut = path.resolve(outDir);
134
+ const depPath = path.join(absOut, 'vmz-deployment.json');
135
+ if (!fs.existsSync(depPath)) {
136
+ throw new Error(`browser harness pack: missing ${depPath} (run vmz build first)`);
137
+ }
138
+ const dep = JSON.parse(fs.readFileSync(depPath, 'utf8'));
139
+ const units = new Map();
140
+ for (const unit of Array.isArray(dep.units) ? dep.units : []) {
141
+ const chunkId = String(unit?.chunkId || '').replace(/\\/g, '/');
142
+ if (!chunkId || units.has(chunkId))
143
+ continue;
144
+ units.set(chunkId, {
145
+ clientEntry: (unit.clientEntry ? String(unit.clientEntry) : `${chunkId}.client.js`).replace(/\\/g, '/'),
146
+ source: String(unit.source || ''),
147
+ });
148
+ }
149
+ const logicals = [...new Set(chunkIds.map((id) => String(id).replace(/\\/g, '/')).filter((id) => id))];
150
+ const entries = [];
151
+ const uses = [];
152
+ for (const logical of logicals) {
153
+ const clientEntry = units.get(logical)?.clientEntry ?? `${logical}.client.js`;
154
+ const abs = path.join(absOut, ...clientEntry.split('/'));
155
+ if (!fs.existsSync(abs)) {
156
+ throw new Error(`browser harness pack: client entry missing for "${logical}": ${clientEntry} (in ${absOut})`);
157
+ }
158
+ entries.push({ logical, path: abs });
159
+ uses.push({ logical, execution: 'full', exports: { kind: 'all' } });
160
+ }
161
+ // Projection provenance (same semantics as `deploymentUnitOrigins`): emitted
162
+ // realpath → source realpath, only when both sides exist. Bare-specifier
163
+ // package discovery walks up from the source location.
164
+ const origins = {};
165
+ for (const { clientEntry, source } of units.values()) {
166
+ if (!source)
167
+ continue;
168
+ const emitted = path.join(absOut, ...clientEntry.split('/'));
169
+ if (!fs.existsSync(emitted) || !fs.existsSync(source))
170
+ continue;
171
+ origins[fs.realpathSync(emitted)] = fs.realpathSync(source);
172
+ }
173
+ const request = {
174
+ entries,
175
+ target: 'browser',
176
+ root: path.dirname(absOut),
177
+ uses,
178
+ publicBase: '/',
179
+ ...(Object.keys(origins).length ? { origins } : {}),
180
+ };
181
+ const packOutDir = path.join(absOut, ...PACK_TEST_OUTPUT_DIR_REL.split('/'));
182
+ // `packWrite` only adds files — clear the previous harness pack so stale
183
+ // chunks from an older build can never be served.
184
+ fs.rmSync(packOutDir, { recursive: true, force: true });
185
+ const response = requirePackBinding().packWrite(JSON.stringify(request), packOutDir);
186
+ const byLogical = new Map(parsePackEntryFiles(response).map((e) => [e.logical, e.file]));
187
+ const missing = logicals.filter((logical) => !byLogical.has(logical));
188
+ if (missing.length) {
189
+ throw new Error(`browser harness pack: plan has no entry file for: ${missing.join(', ')}`);
190
+ }
191
+ return byLogical;
192
+ }
package/dist/browser.js CHANGED
@@ -18,6 +18,7 @@ import os from 'node:os';
18
18
  import path from 'node:path';
19
19
  import { resolveComponentEntries } from '@vmz/core/component-registry';
20
20
  import { resolveChunkArtifacts } from './compile.js';
21
+ import { packHarnessComponents } from './browser-pack.js';
21
22
  import { createArtifactsDir, writeFailureEvidence, writeTimingOnly } from './browser-evidence.js';
22
23
  import { isServeHostManifest, resolveRoutePath, startServeHost } from './browser-serve.js';
23
24
  import { resolveDeliveryServeRoot } from './delivery-serve-root.js';
@@ -342,12 +343,37 @@ export async function runBrowserManifest(manifest, ctx) {
342
343
  strict: registryStrict,
343
344
  closureRoots: [chunkId.replace(/\\/g, '/')],
344
345
  });
346
+ // Raw compiler artifacts keep bare package imports (e.g. `@vmz/ui-icons/registry`
347
+ // inside `@vmz/ui` components) that the browser cannot resolve — the vendor
348
+ // lowering that used to rewrite them is retired. Pack the harness component
349
+ // chain for the browser target and load the packed entry chunks instead.
350
+ const registryChunkId = (e) => (e.chunkId || `components/${e.name}`).replace(/\\/g, '/');
351
+ const packChunkIds = [...new Set([chunkId.replace(/\\/g, '/'), ...registryEntries.map(registryChunkId)])];
352
+ let packEntryFiles;
353
+ try {
354
+ packEntryFiles = packHarnessComponents(outDir, packChunkIds);
355
+ }
356
+ catch (e) {
357
+ fail(`browser pack: ${e instanceof Error ? e.message : String(e)}`);
358
+ return {
359
+ status: 'error',
360
+ diagnostics,
361
+ planId: null,
362
+ programId,
363
+ };
364
+ }
365
+ const packedHref = (logical) => {
366
+ const file = packEntryFiles.get(logical);
367
+ if (!file)
368
+ throw new Error(`browser pack: no packed entry for "${logical}"`);
369
+ return `${origin}/_vmz/pack-test/${file}`;
370
+ };
345
371
  const boot = await page.evaluate(async (cfg) => {
346
372
  const dom = await import(/* @vite-ignore */ `${cfg.origin}/vmz-dom.js`);
347
- const Comp = (await import(/* @vite-ignore */ `${cfg.origin}/${cfg.chunkPath}.client.js`)).default;
373
+ const Comp = (await import(/* @vite-ignore */ cfg.chunkUrl)).default;
348
374
  const map = {};
349
- for (const { name, entry } of cfg.registry) {
350
- map[name] = (await import(/* @vite-ignore */ `${cfg.origin}/${entry}`)).default;
375
+ for (const { name, url } of cfg.registry) {
376
+ map[name] = (await import(/* @vite-ignore */ url)).default;
351
377
  }
352
378
  if (Object.keys(map).length && typeof dom.registerComponents === 'function') {
353
379
  dom.registerComponents(map);
@@ -370,8 +396,8 @@ export async function runBrowserManifest(manifest, ctx) {
370
396
  return { ok: true };
371
397
  }, {
372
398
  origin,
373
- chunkPath: chunkId.replace(/\\/g, '/'),
374
- registry: registryEntries,
399
+ chunkUrl: packedHref(chunkId.replace(/\\/g, '/')),
400
+ registry: registryEntries.map((e) => ({ name: e.name, url: packedHref(registryChunkId(e)) })),
375
401
  });
376
402
  if (!boot?.ok) {
377
403
  fail(`browser boot: ${boot?.error || 'unknown'}`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "dependencies": {
3
- "@vmz/core": "0.3.11",
4
- "@vmz/protocol": "0.3.11",
3
+ "@vmz/core": "0.3.13",
4
+ "@vmz/protocol": "0.3.13",
5
5
  "linkedom": "^0.18.13",
6
6
  "puppeteer-core": "^24.11.2"
7
7
  },
@@ -51,5 +51,5 @@
51
51
  },
52
52
  "type": "module",
53
53
  "types": "./dist/index.d.ts",
54
- "version": "0.3.11"
54
+ "version": "0.3.13"
55
55
  }