@vmz/test 0.3.12 → 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.
- package/dist/browser-pack.d.ts +22 -0
- package/dist/browser-pack.js +192 -0
- package/dist/browser.js +31 -5
- package/package.json +3 -3
|
@@ -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 */
|
|
373
|
+
const Comp = (await import(/* @vite-ignore */ cfg.chunkUrl)).default;
|
|
348
374
|
const map = {};
|
|
349
|
-
for (const { name,
|
|
350
|
-
map[name] = (await import(/* @vite-ignore */
|
|
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
|
-
|
|
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.
|
|
4
|
-
"@vmz/protocol": "0.3.
|
|
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.
|
|
54
|
+
"version": "0.3.13"
|
|
55
55
|
}
|