@vmz/vmz 0.3.12 → 0.3.14
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/bin/vmz.js +4 -0
- package/delivery-runtime-files.json +2 -0
- package/dist/cli/cli.js +46 -23
- package/dist/cli/format-cmd.d.ts +9 -17
- package/dist/cli/format-cmd.js +37 -49
- package/dist/cli/format-config.d.ts +6 -9
- package/dist/cli/format-config.js +5 -29
- package/dist/delivery/delivery-profile.js +1 -1
- package/dist/dev/dev-incremental.d.ts +0 -2
- package/dist/dev/dev-incremental.js +0 -1
- package/dist/dev/dev-session.d.ts +53 -1
- package/dist/dev/dev-session.js +368 -65
- package/dist/document/document-base-css.d.ts +15 -0
- package/dist/document/document-base-css.js +578 -0
- package/dist/document/document-build.d.ts +1 -0
- package/dist/document/document-build.js +94 -78
- package/dist/document/document-client.d.ts +13 -0
- package/dist/document/document-client.js +217 -0
- package/dist/document/document-cmd.js +3 -1
- package/dist/document/document-designs.d.ts +9 -3
- package/dist/document/document-designs.js +20 -34
- package/dist/document/document-enrich.d.ts +9 -0
- package/dist/document/document-enrich.js +55 -0
- package/dist/document/document-highlight.d.ts +26 -0
- package/dist/document/document-highlight.js +54 -0
- package/dist/document/document-integrate.d.ts +5 -0
- package/dist/document/document-integrate.js +1 -0
- package/dist/document/document-interactive.js +10 -2
- package/dist/document/document-layout-render.js +6 -2
- package/dist/document/document-schema.d.ts +5 -0
- package/dist/workspace/build-assemble.js +18 -6
- package/dist/workspace/content-addressed-assets.d.ts +6 -17
- package/dist/workspace/content-addressed-assets.js +10 -95
- package/dist/workspace/delivery-runtime-imports.js +9 -0
- package/dist/workspace/native-addon.d.ts +4 -0
- package/dist/workspace/pack-native.d.ts +151 -0
- package/dist/workspace/pack-native.js +280 -0
- package/dist/workspace/pack.d.ts +131 -32
- package/dist/workspace/pack.js +381 -65
- package/dist/workspace/plugin-host.d.ts +4 -4
- package/dist/workspace/plugin-host.js +6 -6
- package/dist/workspace/production-observability.d.ts +2 -0
- package/dist/workspace/production-observability.js +6 -2
- package/dist/workspace/project-build.js +26 -4
- package/dist/workspace/public-api.d.ts +7 -3
- package/dist/workspace/public-api.js +5 -3
- package/dist/workspace/static-emit.d.ts +30 -0
- package/dist/workspace/static-emit.js +211 -18
- package/dist/workspace/user-config.d.ts +112 -0
- package/dist/workspace/user-config.js +12 -0
- package/host-runtime-files.json +1 -0
- package/locales/en-US/cli.json +5 -4
- package/locales/en-US/diag.json +2 -0
- package/locales/zh-CN/diag.json +2 -0
- package/package.json +20 -20
- package/dist/workspace/pack-client-packages.d.ts +0 -34
- package/dist/workspace/pack-client-packages.js +0 -550
package/dist/workspace/pack.js
CHANGED
|
@@ -1,26 +1,109 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pack stage:
|
|
3
|
-
*
|
|
2
|
+
* Pack stage: run the browser delivery graph through the Rust `vmz-pack`
|
|
3
|
+
* binding (N-API `packPlan`/`packWrite`, frozen v0 contract) and record the
|
|
4
|
+
* resulting chunk/asset mapping in `_vmz/pack-manifest.json`.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Browser graph roots are the bootstrap modules (`entry-client.js` /
|
|
7
|
+
* `entry-event.js`) emitted by the compiler / static delivery path; page and
|
|
8
|
+
* island modules enter the graph through the bootstrap's literal dynamic
|
|
9
|
+
* import tables, never as pack entries. Bootstrap entries declare
|
|
10
|
+
* `execution: 'full'` + `exports: { kind: 'all' }` (script-tag load,
|
|
11
|
+
* whole-module execution).
|
|
12
|
+
*
|
|
13
|
+
* SSR/Node is a separate graph: Node entries are the deployment units
|
|
14
|
+
* reachable from the page / application roots through `dependsOn` (logical =
|
|
15
|
+
* `chunkId`), packed under `_vmz/pack-node/` and indexed by
|
|
16
|
+
* `_vmz/pack-node-manifest.json`. Components have no Node consumer unless a
|
|
17
|
+
* page or layout reaches them, so orphan units are not packed. Node consumers
|
|
18
|
+
* never fall back to the unpacked root `${chunkId}.client.js`; browser
|
|
19
|
+
* bootstrap modules are not Node entries because they bind `dom.browser` /
|
|
20
|
+
* `document`.
|
|
21
|
+
*
|
|
22
|
+
* No vendor materialization, no specifier lowering, and no second hashing
|
|
23
|
+
* pass happen here: `packWrite` emits the final browser JS set under
|
|
24
|
+
* `_vmz/pack/` with content-hashed names, and consumers resolve logical
|
|
25
|
+
* entries through `packEntryHref()` against this manifest.
|
|
11
26
|
*/
|
|
12
27
|
import crypto from 'node:crypto';
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
28
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
14
29
|
import path from 'node:path';
|
|
15
|
-
import { loadDeploymentIr
|
|
16
|
-
import {
|
|
30
|
+
import { loadDeploymentIr } from './bundler-adapter.js';
|
|
31
|
+
import { buildPackRequest, runPackWrite } from './pack-native.js';
|
|
17
32
|
import { deliveryRuntimeFilePairs } from './delivery-runtime-files.js';
|
|
18
33
|
import { rewriteDeliveryImports } from './delivery-runtime-imports.js';
|
|
19
34
|
import { writePrettyJsonFile } from './pretty-json.js';
|
|
20
35
|
export const PACK_MANIFEST_SCHEMA = 'vmz.pack.manifest.v0';
|
|
36
|
+
/** Directory (relative to outDir) where `packWrite` emits the final browser JS set. */
|
|
37
|
+
export const PACK_OUTPUT_DIR_REL = '_vmz/pack';
|
|
38
|
+
/** Site root base — VMZ static delivery has no configurable base (see delivery-profile). */
|
|
39
|
+
export const PACK_PUBLIC_BASE = '/';
|
|
40
|
+
/**
|
|
41
|
+
* Projection provenance for the pack graph: each deployment unit's emitted
|
|
42
|
+
* client entry maps back to its source `.vmz`. Bare-specifier package discovery
|
|
43
|
+
* then walks up from the source location — dependency components (e.g. a
|
|
44
|
+
* `@vmz/ui` component importing its own peer) keep their source package's
|
|
45
|
+
* node_modules visibility, matching Node realpath semantics. Relative
|
|
46
|
+
* specifiers keep resolving against the emitted tree.
|
|
47
|
+
*/
|
|
48
|
+
export function deploymentUnitOrigins(absOut, ir) {
|
|
49
|
+
const origins = {};
|
|
50
|
+
for (const unit of ir.units || []) {
|
|
51
|
+
const source = String(unit.source || '');
|
|
52
|
+
const chunkId = String(unit.chunkId || '').replace(/\\/g, '/');
|
|
53
|
+
if (!source || !chunkId)
|
|
54
|
+
continue;
|
|
55
|
+
const rel = (unit.clientEntry ? String(unit.clientEntry) : `${chunkId}.client.js`).replace(/\\/g, '/');
|
|
56
|
+
const emitted = path.join(absOut, ...rel.split('/'));
|
|
57
|
+
if (!existsSync(emitted) || !existsSync(source))
|
|
58
|
+
continue;
|
|
59
|
+
origins[realpathSync(emitted)] = realpathSync(source);
|
|
60
|
+
}
|
|
61
|
+
return origins;
|
|
62
|
+
}
|
|
63
|
+
/** Bootstrap modules that are browser pack entries when present in outDir. */
|
|
64
|
+
const BOOTSTRAP_ENTRIES = ['entry-client.js', 'entry-event.js'];
|
|
65
|
+
/**
|
|
66
|
+
* 模块路径比较键:去掉 Windows verbatim 前缀(Rust `canonicalize` 给 `//?/E:/…`,
|
|
67
|
+
* Node `realpathSync` 给 `E:/…`)并统一分隔符,避免同一文件因前缀差异查不到。
|
|
68
|
+
*/
|
|
69
|
+
function moduleKey(filePath) {
|
|
70
|
+
return filePath.replace(/^(?:\\\\\?\\|\/\/\?\/)/, '').replace(/\\/g, '/');
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 逻辑 chunkId → 浏览器 chunk 的精确查表:按部署单元的产物客户端文件与
|
|
74
|
+
* chunk `modules` 的同路径匹配,不做后缀匹配、不猜命名规则。单元产物未进
|
|
75
|
+
* 客户端图(无消费者)时显式记 `null`,消费方可据此报错而不是静默取错 chunk。
|
|
76
|
+
*/
|
|
77
|
+
function resolveUnitChunks(absOut, deploymentUnits, artifacts) {
|
|
78
|
+
const chunkOfModule = new Map();
|
|
79
|
+
for (const artifact of artifacts) {
|
|
80
|
+
for (const module of artifact.modules) {
|
|
81
|
+
chunkOfModule.set(moduleKey(module), { chunkId: artifact.chunkId, fileName: artifact.fileName });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const out = [];
|
|
85
|
+
for (const unit of deploymentUnits) {
|
|
86
|
+
const logical = String(unit.chunkId || '').replace(/\\/g, '/');
|
|
87
|
+
if (!logical)
|
|
88
|
+
continue;
|
|
89
|
+
const clientEntry = (unit.clientEntry ? String(unit.clientEntry) : `${logical}.client.js`).replace(/\\/g, '/');
|
|
90
|
+
const abs = path.join(absOut, ...clientEntry.split('/'));
|
|
91
|
+
const hit = chunkOfModule.get(moduleKey(existsSync(abs) ? realpathSync(abs) : abs)) ?? null;
|
|
92
|
+
out.push({
|
|
93
|
+
logical,
|
|
94
|
+
clientEntry,
|
|
95
|
+
chunkId: hit ? hit.chunkId : null,
|
|
96
|
+
fileName: hit ? hit.fileName : null,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
out.sort((a, b) => (a.logical < b.logical ? -1 : a.logical > b.logical ? 1 : 0));
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
21
102
|
/**
|
|
22
103
|
* Ensure dom split companions sit next to vmz-dom.js (barrel imports ./dom-core.js).
|
|
23
104
|
* Always refresh from `@vmz/core` when present so SSR/runtime fixes are not sticky in outDir.
|
|
105
|
+
* Document surfaces (Node-side document layout mount) consume this; browser delivery
|
|
106
|
+
* runtime files reach the pack graph through `@vmz/core` bare imports in the Rust graph.
|
|
24
107
|
* @param {string} outDir
|
|
25
108
|
* @param {string | null | undefined} coreDist `@vmz/core` dist root
|
|
26
109
|
* @returns {string[]} copied relative names
|
|
@@ -41,37 +124,61 @@ export function ensureRuntimeCompanions(outDir, coreDist) {
|
|
|
41
124
|
}
|
|
42
125
|
return copied;
|
|
43
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Pack the browser delivery graph via the Rust `vmz-pack` binding and write
|
|
129
|
+
* `_vmz/pack-manifest.json` (v0). Entries are the bootstrap modules that
|
|
130
|
+
* already exist in `outDir` — an empty bootstrap set is an explicit request
|
|
131
|
+
* error, never a silently skipped stage.
|
|
132
|
+
*/
|
|
44
133
|
export function packFromDeploymentIr(outDir, opts = {}) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
134
|
+
const absOut = path.resolve(outDir);
|
|
135
|
+
const root = opts.projectRoot ? path.resolve(opts.projectRoot) : path.dirname(absOut);
|
|
136
|
+
const entries = {};
|
|
137
|
+
for (const logical of BOOTSTRAP_ENTRIES) {
|
|
138
|
+
const abs = path.join(absOut, logical);
|
|
139
|
+
if (existsSync(abs))
|
|
140
|
+
entries[logical] = abs;
|
|
141
|
+
}
|
|
142
|
+
const uses = Object.keys(entries).map((logical) => ({
|
|
143
|
+
logical,
|
|
144
|
+
execution: 'full',
|
|
145
|
+
exports: { kind: 'all' },
|
|
146
|
+
}));
|
|
147
|
+
// Browser graph entries are the bootstrap modules; page/island units join
|
|
148
|
+
// through the bootstrap dynamic-import tables and need projection provenance.
|
|
149
|
+
const origins = deploymentUnitOrigins(absOut, loadDeploymentIr(absOut));
|
|
150
|
+
const requestJson = buildPackRequest({
|
|
151
|
+
entries,
|
|
152
|
+
target: 'browser',
|
|
153
|
+
root,
|
|
154
|
+
outRoot: absOut,
|
|
155
|
+
uses,
|
|
156
|
+
publicBase: PACK_PUBLIC_BASE,
|
|
157
|
+
origins,
|
|
158
|
+
});
|
|
159
|
+
const packOutDir = path.join(absOut, ...PACK_OUTPUT_DIR_REL.split('/'));
|
|
160
|
+
// 不再整体清空输出目录:`packWrite` 按内容摘要复用未变 chunk,并在写盘后
|
|
161
|
+
// 逐文件清理不再属于产物闭包的旧 chunk / map / css / assets(不留孤儿)。
|
|
162
|
+
const { plan } = runPackWrite(requestJson, packOutDir, opts.binding ? { binding: opts.binding } : {});
|
|
163
|
+
const ir = loadDeploymentIr(absOut);
|
|
164
|
+
let nativeManifest;
|
|
165
|
+
try {
|
|
166
|
+
nativeManifest = JSON.parse(plan.manifestJson);
|
|
74
167
|
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
throw new Error(`pack manifestJson from binding is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
170
|
+
}
|
|
171
|
+
const chunks = plan.artifacts.map((a) => {
|
|
172
|
+
const buf = Buffer.from(a.code, 'utf8');
|
|
173
|
+
return {
|
|
174
|
+
chunkId: a.chunkId,
|
|
175
|
+
fileName: a.fileName,
|
|
176
|
+
modules: a.modules,
|
|
177
|
+
bytes: buf.length,
|
|
178
|
+
digest: sha256Hex(buf),
|
|
179
|
+
sourceMap: a.sourceMapFileName,
|
|
180
|
+
};
|
|
181
|
+
});
|
|
75
182
|
const body = {
|
|
76
183
|
schema: PACK_MANIFEST_SCHEMA,
|
|
77
184
|
profileId: opts.profileId || null,
|
|
@@ -79,33 +186,241 @@ export function packFromDeploymentIr(outDir, opts = {}) {
|
|
|
79
186
|
release: Boolean(opts.release),
|
|
80
187
|
preferredClientFace: opts.preferredClientFace || '@vmz/core/dom/client',
|
|
81
188
|
deploymentSchema: ir.schema,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
189
|
+
target: 'browser',
|
|
190
|
+
publicBase: PACK_PUBLIC_BASE,
|
|
191
|
+
entries: Object.keys(entries)
|
|
192
|
+
.sort()
|
|
193
|
+
.map((logical) => ({ logical, file: path.relative(absOut, entries[logical]).replace(/\\/g, '/') })),
|
|
194
|
+
entryFiles: plan.entryFiles,
|
|
195
|
+
dynamicFiles: plan.dynamicFiles,
|
|
196
|
+
// CSS artifacts are final bytes under `_vmz/pack/` (`packWrite` wrote
|
|
197
|
+
// them); the wire carries identity/provenance only, never content.
|
|
198
|
+
styles: plan.styles,
|
|
199
|
+
assets: plan.assets,
|
|
200
|
+
chunks,
|
|
201
|
+
unitChunks: resolveUnitChunks(absOut, ir.units || [], plan.artifacts),
|
|
202
|
+
chunkCount: chunks.length,
|
|
203
|
+
styleCount: plan.styles.length,
|
|
204
|
+
entryCount: plan.entryFiles.length,
|
|
205
|
+
// Chunk text is token-minified inside `vmz-pack` while preserving string,
|
|
206
|
+
// template and regex bodies plus ASI-significant line breaks. Identifiers
|
|
207
|
+
// are not renamed and dead code is not eliminated.
|
|
208
|
+
minify: 'tokens',
|
|
209
|
+
// `link` active fixed-point pruning really ran — bootstrap entries
|
|
210
|
+
// declare full execution, internal modules are pruned to reachable exports.
|
|
211
|
+
treeShake: 'vmz-pack-link-pruning',
|
|
212
|
+
// Chunk source maps are v3 token-level: every minified token maps back
|
|
213
|
+
// through the assembled line to its module line (`<packOut>/<fileName>.map`,
|
|
214
|
+
// `sourceMapFileName` on the wire). Generated columns are token columns,
|
|
215
|
+
// source columns stay at the assembled slice start; `names` is unused.
|
|
216
|
+
sourceMaps: 'chunk-v3-token-mappings',
|
|
217
|
+
// `packInputDigest` 是本次 pack 输入(图结构 + 各模块源字节 + emit 选项)
|
|
218
|
+
// 的内容摘要;`packGraphDigest` 只看图结构,任一边界/模块/边变化即整体
|
|
219
|
+
// 失效,不允许部分 chunk 复用。两者都来自 Rust 侧,不使用 mtime/时间戳。
|
|
220
|
+
packInputDigest: plan.inputDigest,
|
|
221
|
+
packGraphDigest: plan.graphDigest,
|
|
222
|
+
nativeManifest,
|
|
102
223
|
};
|
|
103
224
|
body.packDigest = sha256Hex(stableStringify({ ...body }));
|
|
104
|
-
const vmzDir = path.join(
|
|
225
|
+
const vmzDir = path.join(absOut, '_vmz');
|
|
105
226
|
mkdirSync(vmzDir, { recursive: true });
|
|
106
227
|
const file = path.join(vmzDir, 'pack-manifest.json');
|
|
107
228
|
writePrettyJsonFile(file, body);
|
|
108
|
-
return { manifest: body, path: file
|
|
229
|
+
return { manifest: body, path: file };
|
|
230
|
+
}
|
|
231
|
+
export const PACK_NODE_MANIFEST_SCHEMA = 'vmz.pack.node-manifest.v0';
|
|
232
|
+
/** Directory (relative to outDir) where `packWrite` emits the Node JS set. */
|
|
233
|
+
export const PACK_NODE_OUTPUT_DIR_REL = '_vmz/pack-node';
|
|
234
|
+
/** Node pack manifest (relative to outDir); `entryFiles` maps logical → chunk file. */
|
|
235
|
+
export const PACK_NODE_MANIFEST_REL = '_vmz/pack-node-manifest.json';
|
|
236
|
+
/**
|
|
237
|
+
* Node graph roots and their `dependsOn` closure — the only chunk ids a Node
|
|
238
|
+
* consumer can request. Pages and the application shell are roots; components
|
|
239
|
+
* enter through `dependsOn` (pages merge their `layoutChain` into it). Standalone
|
|
240
|
+
* layout units (`layouts/*`, emitted with kind `other`) are roots of their own
|
|
241
|
+
* because `loadCtor` asks for page / layout chunk ids directly and no page lists
|
|
242
|
+
* them in `dependsOn`. Units outside the closure have no Node consumer: the SSR
|
|
243
|
+
* registry imports a component entry only when some root reaches it.
|
|
244
|
+
*/
|
|
245
|
+
function nodeReachableChunkIds(ir) {
|
|
246
|
+
const dependsOn = new Map();
|
|
247
|
+
const roots = [];
|
|
248
|
+
for (const unit of ir.units || []) {
|
|
249
|
+
const chunkId = String(unit.chunkId || '').replace(/\\/g, '/');
|
|
250
|
+
if (!chunkId)
|
|
251
|
+
continue;
|
|
252
|
+
const deps = dependsOn.get(chunkId) ?? [];
|
|
253
|
+
for (const dep of unit.dependsOn || []) {
|
|
254
|
+
const id = String(dep).replace(/\\/g, '/');
|
|
255
|
+
if (id)
|
|
256
|
+
deps.push(id);
|
|
257
|
+
}
|
|
258
|
+
dependsOn.set(chunkId, deps);
|
|
259
|
+
if (unit.kind === 'page' || unit.kind === 'application' || unit.kind === 'other')
|
|
260
|
+
roots.push(chunkId);
|
|
261
|
+
}
|
|
262
|
+
const reachable = new Set();
|
|
263
|
+
const queue = roots.slice();
|
|
264
|
+
while (queue.length) {
|
|
265
|
+
const id = queue.pop();
|
|
266
|
+
if (reachable.has(id))
|
|
267
|
+
continue;
|
|
268
|
+
reachable.add(id);
|
|
269
|
+
for (const dep of dependsOn.get(id) ?? [])
|
|
270
|
+
if (!reachable.has(dep))
|
|
271
|
+
queue.push(dep);
|
|
272
|
+
}
|
|
273
|
+
return reachable;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Pack the SSR/Node graph via the Rust `vmz-pack` binding and write
|
|
277
|
+
* `_vmz/pack-node-manifest.json` (v0). Entries are the node-reachable
|
|
278
|
+
* deployment units' compiled client modules (`clientEntry`, empty → mirrors
|
|
279
|
+
* `vmz-artifacts` `component_entries`: `${chunkId}.client.js`). Node consumers
|
|
280
|
+
* only read `default`, but the import itself is evaluated, hence
|
|
281
|
+
* `side-effects` + named default rather than `all` / `full` / `unconsumed`.
|
|
282
|
+
*/
|
|
283
|
+
export function packSsrFromDeploymentIr(outDir, opts = {}) {
|
|
284
|
+
const absOut = path.resolve(outDir);
|
|
285
|
+
const root = opts.projectRoot ? path.resolve(opts.projectRoot) : path.dirname(absOut);
|
|
286
|
+
const ir = loadDeploymentIr(absOut);
|
|
287
|
+
if (!(ir.units || []).length) {
|
|
288
|
+
throw new Error(`packSsrFromDeploymentIr: deployment IR has no units in ${absOut}`);
|
|
289
|
+
}
|
|
290
|
+
const nodeReachable = nodeReachableChunkIds(ir);
|
|
291
|
+
const entries = {};
|
|
292
|
+
const uses = [];
|
|
293
|
+
// Deployment IR may list the same component unit more than once (one entry
|
|
294
|
+
// per consuming page). The node graph identity is `chunkId`, so duplicates
|
|
295
|
+
// collapse into one entry; a same-identity unit pointing at a different
|
|
296
|
+
// client file is a contract violation, not something to silently pick.
|
|
297
|
+
const seen = new Map();
|
|
298
|
+
for (const unit of ir.units || []) {
|
|
299
|
+
const chunkId = String(unit.chunkId || '').replace(/\\/g, '/');
|
|
300
|
+
if (!chunkId)
|
|
301
|
+
throw new Error(`packSsrFromDeploymentIr: deployment unit has an empty chunkId in ${absOut}`);
|
|
302
|
+
if (!nodeReachable.has(chunkId))
|
|
303
|
+
continue;
|
|
304
|
+
// `vmz-artifacts` derives the component entry as `${chunkId}.client.js`
|
|
305
|
+
// when `clientEntry` is empty. Decide it here so the pack request stays
|
|
306
|
+
// explicit, then require the file — a missing entry is never skipped.
|
|
307
|
+
const rel = (unit.clientEntry ? String(unit.clientEntry) : `${chunkId}.client.js`).replace(/\\/g, '/');
|
|
308
|
+
if (seen.has(chunkId)) {
|
|
309
|
+
if (seen.get(chunkId) !== rel) {
|
|
310
|
+
throw new Error(`packSsrFromDeploymentIr: deployment unit "${chunkId}" maps to conflicting client entries: ` +
|
|
311
|
+
`${seen.get(chunkId)} and ${rel} (in ${absOut})`);
|
|
312
|
+
}
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const abs = path.join(absOut, ...rel.split('/'));
|
|
316
|
+
if (!existsSync(abs)) {
|
|
317
|
+
throw new Error(`packSsrFromDeploymentIr: unit "${chunkId}" client entry missing: ${rel} (in ${absOut})`);
|
|
318
|
+
}
|
|
319
|
+
seen.set(chunkId, rel);
|
|
320
|
+
entries[chunkId] = abs;
|
|
321
|
+
uses.push({ logical: chunkId, execution: 'side-effects', exports: { kind: 'named', names: ['default'] } });
|
|
322
|
+
}
|
|
323
|
+
if (!Object.keys(entries).length) {
|
|
324
|
+
throw new Error(`packSsrFromDeploymentIr: deployment IR has no node-reachable units in ${absOut}`);
|
|
325
|
+
}
|
|
326
|
+
const origins = deploymentUnitOrigins(absOut, ir);
|
|
327
|
+
const requestJson = buildPackRequest({
|
|
328
|
+
entries,
|
|
329
|
+
target: 'node',
|
|
330
|
+
root,
|
|
331
|
+
outRoot: absOut,
|
|
332
|
+
uses,
|
|
333
|
+
publicBase: PACK_PUBLIC_BASE,
|
|
334
|
+
origins,
|
|
335
|
+
});
|
|
336
|
+
const packOutDir = path.join(absOut, ...PACK_NODE_OUTPUT_DIR_REL.split('/'));
|
|
337
|
+
// 与浏览器 pack 同口径:不清空目录,由 `packWrite` 复用未变 chunk 并清理孤儿。
|
|
338
|
+
const { plan } = runPackWrite(requestJson, packOutDir, opts.binding ? { binding: opts.binding } : {});
|
|
339
|
+
let nativeManifest;
|
|
340
|
+
try {
|
|
341
|
+
nativeManifest = JSON.parse(plan.manifestJson);
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
throw new Error(`node pack manifestJson from binding is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
345
|
+
}
|
|
346
|
+
const chunks = plan.artifacts.map((a) => {
|
|
347
|
+
const buf = Buffer.from(a.code, 'utf8');
|
|
348
|
+
return {
|
|
349
|
+
chunkId: a.chunkId,
|
|
350
|
+
fileName: a.fileName,
|
|
351
|
+
modules: a.modules,
|
|
352
|
+
bytes: buf.length,
|
|
353
|
+
digest: sha256Hex(buf),
|
|
354
|
+
sourceMap: a.sourceMapFileName,
|
|
355
|
+
};
|
|
356
|
+
});
|
|
357
|
+
// dist roots carry no package.json; Node needs one beside the chunk set to
|
|
358
|
+
// treat content-hashed `.js` files as ESM.
|
|
359
|
+
mkdirSync(packOutDir, { recursive: true });
|
|
360
|
+
writeFileSync(path.join(packOutDir, 'package.json'), `${JSON.stringify({ type: 'module' })}\n`, 'utf8');
|
|
361
|
+
const body = {
|
|
362
|
+
schema: PACK_NODE_MANIFEST_SCHEMA,
|
|
363
|
+
target: 'node',
|
|
364
|
+
deploymentSchema: ir.schema,
|
|
365
|
+
entries: Object.keys(entries)
|
|
366
|
+
.sort()
|
|
367
|
+
.map((logical) => ({ logical, file: path.relative(absOut, entries[logical]).replace(/\\/g, '/') })),
|
|
368
|
+
entryFiles: plan.entryFiles,
|
|
369
|
+
dynamicFiles: plan.dynamicFiles,
|
|
370
|
+
chunks,
|
|
371
|
+
chunkCount: chunks.length,
|
|
372
|
+
entryCount: plan.entryFiles.length,
|
|
373
|
+
// 同浏览器 pack:输入/图结构摘要来自 Rust 侧内容摘要判定。
|
|
374
|
+
packInputDigest: plan.inputDigest,
|
|
375
|
+
packGraphDigest: plan.graphDigest,
|
|
376
|
+
nativeManifest,
|
|
377
|
+
};
|
|
378
|
+
body.packDigest = sha256Hex(stableStringify({ ...body }));
|
|
379
|
+
const file = path.join(absOut, ...PACK_NODE_MANIFEST_REL.split('/'));
|
|
380
|
+
writePrettyJsonFile(file, body);
|
|
381
|
+
return { manifest: body, path: file };
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Resolve a Node logical entry (`chunkId`) to its packed absolute file from
|
|
385
|
+
* `_vmz/pack-node-manifest.json`. Missing manifest, missing logical, or a
|
|
386
|
+
* missing chunk file is an explicit error — never a fallback to the unpacked
|
|
387
|
+
* root `${chunkId}.client.js`.
|
|
388
|
+
*/
|
|
389
|
+
export function packNodeEntryFile(outDir, chunkId) {
|
|
390
|
+
const absOut = path.resolve(outDir);
|
|
391
|
+
const manifestPath = path.join(absOut, ...PACK_NODE_MANIFEST_REL.split('/'));
|
|
392
|
+
if (!existsSync(manifestPath)) {
|
|
393
|
+
throw new Error(`packNodeEntryFile: missing ${manifestPath} — run vmz build (node pack) first`);
|
|
394
|
+
}
|
|
395
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
396
|
+
const hit = (manifest.entryFiles || []).find((e) => e && e.logical === chunkId);
|
|
397
|
+
if (!hit || typeof hit.file !== 'string' || !hit.file) {
|
|
398
|
+
throw new Error(`packNodeEntryFile: node pack manifest has no entry file for "${chunkId}"`);
|
|
399
|
+
}
|
|
400
|
+
const abs = path.join(absOut, ...PACK_NODE_OUTPUT_DIR_REL.split('/'), ...hit.file.split('/'));
|
|
401
|
+
if (!existsSync(abs)) {
|
|
402
|
+
throw new Error(`packNodeEntryFile: node pack chunk missing on disk: ${path.relative(absOut, abs)}`);
|
|
403
|
+
}
|
|
404
|
+
return abs;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Resolve the final browser URL of a packed logical entry from
|
|
408
|
+
* `_vmz/pack-manifest.json` (`entryFiles` is the only logical→physical map).
|
|
409
|
+
* Missing manifest or missing logical entry is an explicit error — no
|
|
410
|
+
* fallback to unpacked root bytes.
|
|
411
|
+
*/
|
|
412
|
+
export function packEntryHref(outDir, logical) {
|
|
413
|
+
const manifestPath = path.join(path.resolve(outDir), '_vmz', 'pack-manifest.json');
|
|
414
|
+
if (!existsSync(manifestPath)) {
|
|
415
|
+
throw new Error(`packEntryHref: missing ${manifestPath} — run vmz build (pack) first`);
|
|
416
|
+
}
|
|
417
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
418
|
+
const hit = (manifest.entryFiles || []).find((e) => e && e.logical === logical);
|
|
419
|
+
if (!hit || typeof hit.file !== 'string' || !hit.file) {
|
|
420
|
+
throw new Error(`packEntryHref: pack manifest has no entry file for "${logical}"`);
|
|
421
|
+
}
|
|
422
|
+
const base = String(manifest.publicBase || PACK_PUBLIC_BASE).replace(/\/$/, '');
|
|
423
|
+
return `${base}/${PACK_OUTPUT_DIR_REL}/${hit.file}`;
|
|
109
424
|
}
|
|
110
425
|
function stableStringify(value) {
|
|
111
426
|
return JSON.stringify(sortKeys(value));
|
|
@@ -115,12 +430,13 @@ function sortKeys(value) {
|
|
|
115
430
|
return value.map(sortKeys);
|
|
116
431
|
if (value && typeof value === 'object') {
|
|
117
432
|
const out = {};
|
|
118
|
-
for (const k of Object.keys(value).sort())
|
|
433
|
+
for (const k of Object.keys(value).sort()) {
|
|
119
434
|
out[k] = sortKeys(value[k]);
|
|
435
|
+
}
|
|
120
436
|
return out;
|
|
121
437
|
}
|
|
122
438
|
return value;
|
|
123
439
|
}
|
|
124
|
-
function sha256Hex(
|
|
125
|
-
return crypto.createHash('sha256').update(
|
|
440
|
+
function sha256Hex(data) {
|
|
441
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
126
442
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Plugin protocol v1 helpers + typed config loading.
|
|
3
3
|
*/
|
|
4
|
-
import { contentHash,
|
|
5
|
-
export { contentHash,
|
|
4
|
+
import { contentHash, definePlugin } from '@vmz/plugin';
|
|
5
|
+
export { contentHash, definePlugin };
|
|
6
6
|
export declare const PLUGIN_PROTOCOL = "0.1.0";
|
|
7
7
|
export declare const CONFIG_NAMES: string[];
|
|
8
8
|
export declare const CONFIG_LOAD_FAILED = "vmz::config::load_failed";
|
|
@@ -13,9 +13,9 @@ export declare const PLUGIN_ENTRY_INVALID = "vmz::plugin::invalid_entry";
|
|
|
13
13
|
* @returns {Promise<{
|
|
14
14
|
* plugins: import('@vmz/plugin').VmzPlugin[],
|
|
15
15
|
* engines: import('@vmz/plugin').VmzEngines,
|
|
16
|
-
* delivery: import('
|
|
16
|
+
* delivery: import('./user-config.js').DeliveryAuthoring | null,
|
|
17
17
|
* application: { id?: string } | null,
|
|
18
|
-
* format: import('
|
|
18
|
+
* format: import('./user-config.js').VmzFormatConfig | null,
|
|
19
19
|
* path: string | null,
|
|
20
20
|
* pluginPath: string | null,
|
|
21
21
|
* }>}
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { pathToFileURL } from 'node:url';
|
|
7
|
-
import { PLUGIN_PROTOCOL as PLUGIN_PROTOCOL_PKG, contentHash,
|
|
7
|
+
import { PLUGIN_PROTOCOL as PLUGIN_PROTOCOL_PKG, contentHash, definePlugin } from '@vmz/plugin';
|
|
8
8
|
import { log } from './log.js';
|
|
9
9
|
import { resolveWorkspacePackages } from './packages.js';
|
|
10
|
-
export { contentHash,
|
|
10
|
+
export { contentHash, definePlugin };
|
|
11
11
|
export const PLUGIN_PROTOCOL = PLUGIN_PROTOCOL_PKG;
|
|
12
12
|
export const CONFIG_NAMES = ['vmz.config.ts', 'vmz.config.mts', 'vmz.config.mjs', 'vmz.config.js'];
|
|
13
13
|
const ROOT_PLUGIN_NAMES = ['vmz.plugin.ts', 'vmz.plugin.mts', 'vmz.plugin.mjs', 'vmz.plugin.js'];
|
|
@@ -61,9 +61,9 @@ function configDiagnosticError(code, sourcePath, err) {
|
|
|
61
61
|
* @returns {Promise<{
|
|
62
62
|
* plugins: import('@vmz/plugin').VmzPlugin[],
|
|
63
63
|
* engines: import('@vmz/plugin').VmzEngines,
|
|
64
|
-
* delivery: import('
|
|
64
|
+
* delivery: import('./user-config.js').DeliveryAuthoring | null,
|
|
65
65
|
* application: { id?: string } | null,
|
|
66
|
-
* format: import('
|
|
66
|
+
* format: import('./user-config.js').VmzFormatConfig | null,
|
|
67
67
|
* path: string | null,
|
|
68
68
|
* pluginPath: string | null,
|
|
69
69
|
* }>}
|
|
@@ -73,11 +73,11 @@ export async function loadVmzConfig(project) {
|
|
|
73
73
|
const plugins = [];
|
|
74
74
|
/** @type {import('@vmz/plugin').VmzEngines} */
|
|
75
75
|
let engines = {};
|
|
76
|
-
/** @type {import('
|
|
76
|
+
/** @type {import('./user-config.js').DeliveryAuthoring | null} */
|
|
77
77
|
let delivery = null;
|
|
78
78
|
/** @type {{ id?: string } | null} */
|
|
79
79
|
let application = null;
|
|
80
|
-
/** @type {import('
|
|
80
|
+
/** @type {import('./user-config.js').VmzFormatConfig | null} */
|
|
81
81
|
let format = null;
|
|
82
82
|
/** @type {string | null} */
|
|
83
83
|
let configPath = null;
|
|
@@ -95,6 +95,8 @@ export declare function checkCapabilityClosure(cap: any, policy: any): {
|
|
|
95
95
|
export declare function applySecurityHeadersToCdnPolicy(cdnPolicy: any, security: any): any;
|
|
96
96
|
/**
|
|
97
97
|
* Measure dist sizes for budget gates.
|
|
98
|
+
* Browser delivery JS final set = packed chunks under `_vmz/pack/`
|
|
99
|
+
* (root `*.client.js` / bootstrap files are Node SSR / pack inputs, not delivery bytes).
|
|
98
100
|
* @param {string} distDir
|
|
99
101
|
*/
|
|
100
102
|
export declare function measureDistBudgets(distDir: any): {
|
|
@@ -398,13 +398,16 @@ export function applySecurityHeadersToCdnPolicy(cdnPolicy, security) {
|
|
|
398
398
|
}
|
|
399
399
|
/**
|
|
400
400
|
* Measure dist sizes for budget gates.
|
|
401
|
+
* Browser delivery JS final set = packed chunks under `_vmz/pack/`
|
|
402
|
+
* (root `*.client.js` / bootstrap files are Node SSR / pack inputs, not delivery bytes).
|
|
401
403
|
* @param {string} distDir
|
|
402
404
|
*/
|
|
403
405
|
export function measureDistBudgets(distDir) {
|
|
404
406
|
let htmlBytes = 0;
|
|
405
407
|
let clientJsBytes = 0;
|
|
406
408
|
let artifactBytes = 0;
|
|
407
|
-
const
|
|
409
|
+
const abs = path.resolve(distDir);
|
|
410
|
+
const stack = [abs];
|
|
408
411
|
while (stack.length) {
|
|
409
412
|
const dir = stack.pop();
|
|
410
413
|
if (!fs.existsSync(dir))
|
|
@@ -419,7 +422,8 @@ export function measureDistBudgets(distDir) {
|
|
|
419
422
|
artifactBytes += st.size;
|
|
420
423
|
if (name.endsWith('.html'))
|
|
421
424
|
htmlBytes += st.size;
|
|
422
|
-
|
|
425
|
+
const rel = path.relative(abs, full).replace(/\\/g, '/');
|
|
426
|
+
if (rel.startsWith('_vmz/pack/') && name.endsWith('.js')) {
|
|
423
427
|
clientJsBytes += st.size;
|
|
424
428
|
}
|
|
425
429
|
}
|