@vmz/vmz 0.3.13 → 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 +1 -0
- 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 +352 -60
- package/dist/document/document-base-css.d.ts +1 -1
- package/dist/document/document-base-css.js +1 -1
- package/dist/document/document-designs.js +1 -1
- package/dist/workspace/delivery-runtime-imports.js +6 -0
- package/dist/workspace/pack-native.d.ts +12 -0
- package/dist/workspace/pack-native.js +9 -2
- package/dist/workspace/pack.d.ts +29 -10
- package/dist/workspace/pack.js +136 -25
- package/locales/en-US/diag.json +2 -0
- package/locales/zh-CN/diag.json +2 -0
- package/package.json +20 -20
package/bin/vmz.js
CHANGED
|
@@ -9,4 +9,8 @@ if (major < 24 || (major === 24 && minor < 12)) {
|
|
|
9
9
|
}
|
|
10
10
|
const { runCli } = await import('../dist/cli/cli.js');
|
|
11
11
|
const code = await runCli(process.argv.slice(2));
|
|
12
|
+
// POSIX pipes flush asynchronously, so `process.exit` can drop queued
|
|
13
|
+
// diagnostics before CI captures them. Drain both streams first.
|
|
14
|
+
await new Promise((resolve) => process.stderr.write('', resolve));
|
|
15
|
+
await new Promise((resolve) => process.stdout.write('', resolve));
|
|
12
16
|
process.exit(code ?? 0);
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
{ "src": "browser/direct-host-box.js", "out": "direct-host-box.js" },
|
|
12
12
|
{ "src": "browser/unknown-component.js", "out": "unknown-component.js" },
|
|
13
13
|
{ "src": "shared/dom-attr-normalize.js", "out": "dom-attr-normalize.js" },
|
|
14
|
+
{ "src": "shared/html-digest.js", "out": "html-digest.js" },
|
|
14
15
|
{ "src": "faces/http.js", "out": "vmz-http.js" },
|
|
15
16
|
{ "src": "browser/client-nav.js", "out": "vmz-client-nav.js" }
|
|
16
17
|
],
|
|
@@ -36,14 +36,12 @@ export interface DevReloadPayload {
|
|
|
36
36
|
islandHmr?: boolean;
|
|
37
37
|
rerunLoaders?: string[];
|
|
38
38
|
staticRevision?: string;
|
|
39
|
-
skipEntryRewrite?: boolean;
|
|
40
39
|
outputRevision?: string;
|
|
41
40
|
}
|
|
42
41
|
export declare function parseHmrPlan(raw: string | object | null | undefined): HmrPlanWire | null;
|
|
43
42
|
export declare function hmrRerunLoaders(plan: HmrPlanWire | null | undefined): string[];
|
|
44
43
|
export declare function buildReloadPayload(report: BuildReportWire, hmrPlan: HmrPlanWire | null, opts?: {
|
|
45
44
|
staticRevision?: string;
|
|
46
|
-
skipEntryRewrite?: boolean;
|
|
47
45
|
}): DevReloadPayload;
|
|
48
46
|
export declare function shouldSoftReload(report: BuildReportWire, lastOutputRevision: string | null, opts?: {
|
|
49
47
|
force?: boolean;
|
|
@@ -38,7 +38,6 @@ export function buildReloadPayload(report, hmrPlan, opts = {}) {
|
|
|
38
38
|
islandHmr: islandHmr && !full,
|
|
39
39
|
rerunLoaders,
|
|
40
40
|
staticRevision: opts.staticRevision,
|
|
41
|
-
skipEntryRewrite: opts.skipEntryRewrite,
|
|
42
41
|
outputRevision: report.outputRevision ?? report.output_revision,
|
|
43
42
|
};
|
|
44
43
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { createWorkspace } from '../workspace/public-api.js';
|
|
13
13
|
import { type ConfigCycle } from './config-cycle.js';
|
|
14
|
+
import { packFromDeploymentIr } from '../workspace/pack.js';
|
|
14
15
|
interface DevSessionOptions {
|
|
15
16
|
project: string;
|
|
16
17
|
outDir: string;
|
|
@@ -28,7 +29,9 @@ interface DevSessionOptions {
|
|
|
28
29
|
host: string;
|
|
29
30
|
port: number;
|
|
30
31
|
}) => import('node:child_process').ChildProcess;
|
|
31
|
-
softReloadFn?: (
|
|
32
|
+
softReloadFn?: (body: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
33
|
+
/** Test seam for the browser pack stage (defaults to `packFromDeploymentIr`). */
|
|
34
|
+
packFn?: typeof packFromDeploymentIr;
|
|
32
35
|
/** Override the detached config build process (tests). */
|
|
33
36
|
configCycleFn?: (opts: {
|
|
34
37
|
project: string;
|
|
@@ -38,6 +41,55 @@ interface DevSessionOptions {
|
|
|
38
41
|
/** Built config cycle worker path (defaults to `dist/dev/config-cycle-worker.js`). */
|
|
39
42
|
configCycleWorkerPath?: string;
|
|
40
43
|
}
|
|
44
|
+
export interface PackManifestView {
|
|
45
|
+
chunks?: Array<{
|
|
46
|
+
fileName?: string;
|
|
47
|
+
sourceMap?: string;
|
|
48
|
+
modules?: unknown[];
|
|
49
|
+
}>;
|
|
50
|
+
styles?: Array<{
|
|
51
|
+
fileName?: string;
|
|
52
|
+
sourcePaths?: unknown[];
|
|
53
|
+
}>;
|
|
54
|
+
assets?: Array<{
|
|
55
|
+
fileName?: string;
|
|
56
|
+
}>;
|
|
57
|
+
entryFiles?: Array<{
|
|
58
|
+
file?: string;
|
|
59
|
+
}>;
|
|
60
|
+
dynamicFiles?: Array<{
|
|
61
|
+
from?: string;
|
|
62
|
+
to?: string;
|
|
63
|
+
}>;
|
|
64
|
+
unitChunks?: Array<{
|
|
65
|
+
fileName?: string | null;
|
|
66
|
+
}>;
|
|
67
|
+
}
|
|
68
|
+
export interface PackInputSnapshot {
|
|
69
|
+
files: Array<{
|
|
70
|
+
path: string;
|
|
71
|
+
digest: string;
|
|
72
|
+
}>;
|
|
73
|
+
listings: Array<{
|
|
74
|
+
dir: string;
|
|
75
|
+
names: string[];
|
|
76
|
+
}>;
|
|
77
|
+
deploymentOrigins: string;
|
|
78
|
+
lockfile: {
|
|
79
|
+
path: string;
|
|
80
|
+
digest: string;
|
|
81
|
+
};
|
|
82
|
+
manifest: {
|
|
83
|
+
path: string;
|
|
84
|
+
digest: string;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** 记下决定 pack 产物的全部输入;任何一项取不到就放弃快照(等同于永远重打包)。 */
|
|
88
|
+
export declare function capturePackInputSnapshot(manifest: PackManifestView, outDir: string, project: string): PackInputSnapshot | null;
|
|
89
|
+
/** 返回 null 表示全部输入字节全等;否则返回第一个变化原因。 */
|
|
90
|
+
export declare function packInputsChangedReason(snapshot: PackInputSnapshot, outDir: string): string | null;
|
|
91
|
+
/** 复用上次 manifest 前确认 entry/chunk/island 查表指向的文件仍在盘上;缺任何一个就退回真实 pack。 */
|
|
92
|
+
export declare function missingPackOutput(manifest: PackManifestView, outDir: string): string | null;
|
|
41
93
|
export declare function createDevSession(options: DevSessionOptions): {
|
|
42
94
|
ws: any;
|
|
43
95
|
rebuild: (changes?: Array<{
|
package/dist/dev/dev-session.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* 3. Soft reload failure → **auto-respawn** serve-host (fresh ESM graph), not "keep broken host".
|
|
11
11
|
*/
|
|
12
12
|
import { spawn } from 'node:child_process';
|
|
13
|
-
import {
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { existsSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
14
15
|
import path from 'node:path';
|
|
15
16
|
import { buildIntegratedDocuments, projectHasDocuments } from '../document/document-integrate.js';
|
|
16
17
|
import { createWorkspace, resolveNativePath } from '../workspace/public-api.js';
|
|
@@ -22,6 +23,8 @@ import { coalesceRootBurst, collectDevWatchRoots, classifyWatchRoot, isDependenc
|
|
|
22
23
|
import { createConfigCycle } from './config-cycle.js';
|
|
23
24
|
import { buildReloadPayload, filterGenerationIgnore, parseHmrPlan, registerWrittenOutputsIgnore, shouldSoftReload, staticRevisionFromArtifact, } from './dev-incremental.js';
|
|
24
25
|
import { diffFingerprints, fileFingerprintMap } from '../workspace/watch-diff.js';
|
|
26
|
+
import { packFromDeploymentIr, deploymentUnitOrigins } from '../workspace/pack.js';
|
|
27
|
+
import { loadDeploymentIr } from '../workspace/bundler-adapter.js';
|
|
25
28
|
/** Track serve-host so a failed/killed launcher cannot leave an orphan port. */
|
|
26
29
|
const DEV_HOST_PID = '.vmz-dev-host.pid';
|
|
27
30
|
function cycleFailure(report) {
|
|
@@ -29,6 +32,218 @@ function cycleFailure(report) {
|
|
|
29
32
|
const error = new Error(detail);
|
|
30
33
|
return report.error ? Object.assign(error, { diagnostic: report.error }) : error;
|
|
31
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Pack 输入快照:dev 只在「决定产物字节的全部输入」完全不变时才复用上次产物。
|
|
37
|
+
* 判据是文件内容哈希,不使用 mtime、文件数量或后缀推导;读不到或未知的输入一律按变更处理。
|
|
38
|
+
*/
|
|
39
|
+
const PACK_INPUT_IGNORED_DIRS = new Set(['_vmz', 'node_modules', '.cache', '.git']);
|
|
40
|
+
const PACK_INPUT_IGNORED_NAMES = /^\.vmz-dev-host\.pid$/;
|
|
41
|
+
/** Rust 侧的模块路径带 `//?/`(或 `\\?\`)verbatim 前缀,落盘读取前需要剥离。 */
|
|
42
|
+
function packInputFsPath(filePath) {
|
|
43
|
+
return filePath.replace(/^(?:\\\\\?\\|\/\/\?\/)/, '').replace(/\\/g, '/');
|
|
44
|
+
}
|
|
45
|
+
function readFileDigest(filePath) {
|
|
46
|
+
try {
|
|
47
|
+
return createHash('sha256').update(readFileSync(filePath)).digest('hex');
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function findLockfile(project) {
|
|
54
|
+
let dir = path.resolve(project);
|
|
55
|
+
while (true) {
|
|
56
|
+
const candidate = path.join(dir, 'pnpm-lock.yaml');
|
|
57
|
+
if (existsSync(candidate))
|
|
58
|
+
return candidate;
|
|
59
|
+
const parent = path.dirname(dir);
|
|
60
|
+
if (parent === dir)
|
|
61
|
+
return null;
|
|
62
|
+
dir = parent;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function listPackInputDir(dir) {
|
|
66
|
+
const names = [];
|
|
67
|
+
const walk = (current, prefix) => {
|
|
68
|
+
let entries;
|
|
69
|
+
try {
|
|
70
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
entries.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (PACK_INPUT_IGNORED_DIRS.has(entry.name) || PACK_INPUT_IGNORED_NAMES.test(entry.name))
|
|
78
|
+
continue;
|
|
79
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
80
|
+
if (entry.isDirectory()) {
|
|
81
|
+
if (!walk(path.join(current, entry.name), rel))
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
names.push(rel);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
};
|
|
90
|
+
return walk(dir, '') ? names : null;
|
|
91
|
+
}
|
|
92
|
+
/** manifest 形状不认识时返回 null(而非空集合),避免把未知输入当成「没变化」。 */
|
|
93
|
+
function packInputFiles(manifest) {
|
|
94
|
+
if (!Array.isArray(manifest.chunks) || manifest.chunks.length === 0)
|
|
95
|
+
return null;
|
|
96
|
+
const files = new Set();
|
|
97
|
+
for (const chunk of manifest.chunks) {
|
|
98
|
+
if (!Array.isArray(chunk.modules))
|
|
99
|
+
return null;
|
|
100
|
+
for (const module of chunk.modules) {
|
|
101
|
+
if (typeof module !== 'string' || !module)
|
|
102
|
+
return null;
|
|
103
|
+
files.add(packInputFsPath(module));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (!Array.isArray(manifest.styles))
|
|
107
|
+
return null;
|
|
108
|
+
for (const style of manifest.styles) {
|
|
109
|
+
if (!Array.isArray(style.sourcePaths))
|
|
110
|
+
return null;
|
|
111
|
+
for (const source of style.sourcePaths) {
|
|
112
|
+
if (typeof source !== 'string' || !source)
|
|
113
|
+
return null;
|
|
114
|
+
files.add(packInputFsPath(source));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return [...files].sort();
|
|
118
|
+
}
|
|
119
|
+
/** pack 只经 `deploymentUnitOrigins` 读取 IR;对这份 origins 取规范摘要,避免把构建期 affected/seed 噪声算成输入变更。 */
|
|
120
|
+
function deploymentOriginsDigest(outDir) {
|
|
121
|
+
try {
|
|
122
|
+
const origins = deploymentUnitOrigins(outDir, loadDeploymentIr(outDir));
|
|
123
|
+
const canonical = Object.keys(origins)
|
|
124
|
+
.sort()
|
|
125
|
+
.map((key) => `${key}\u0000${origins[key]}`);
|
|
126
|
+
return createHash('sha256').update(canonical.join('\u0001')).digest('hex');
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** 记下决定 pack 产物的全部输入;任何一项取不到就放弃快照(等同于永远重打包)。 */
|
|
133
|
+
export function capturePackInputSnapshot(manifest, outDir, project) {
|
|
134
|
+
const inputs = packInputFiles(manifest);
|
|
135
|
+
if (!inputs)
|
|
136
|
+
return null;
|
|
137
|
+
const files = [];
|
|
138
|
+
for (const file of inputs) {
|
|
139
|
+
const digest = readFileDigest(file);
|
|
140
|
+
if (!digest)
|
|
141
|
+
return null;
|
|
142
|
+
files.push({ path: file, digest });
|
|
143
|
+
}
|
|
144
|
+
const listings = [];
|
|
145
|
+
for (const dir of [...new Set(files.map((file) => path.dirname(file.path)))].sort()) {
|
|
146
|
+
if (dir.split(/[\\/]/).includes('node_modules'))
|
|
147
|
+
continue;
|
|
148
|
+
const names = listPackInputDir(dir);
|
|
149
|
+
if (!names)
|
|
150
|
+
return null;
|
|
151
|
+
listings.push({ dir, names });
|
|
152
|
+
}
|
|
153
|
+
const deploymentOrigins = deploymentOriginsDigest(outDir);
|
|
154
|
+
if (!deploymentOrigins)
|
|
155
|
+
return null;
|
|
156
|
+
const lockfilePath = findLockfile(project);
|
|
157
|
+
if (!lockfilePath)
|
|
158
|
+
return null;
|
|
159
|
+
const lockfileDigest = readFileDigest(lockfilePath);
|
|
160
|
+
if (!lockfileDigest)
|
|
161
|
+
return null;
|
|
162
|
+
const manifestPath = path.join(outDir, '_vmz', 'pack-manifest.json');
|
|
163
|
+
const manifestDigest = readFileDigest(manifestPath);
|
|
164
|
+
if (!manifestDigest)
|
|
165
|
+
return null;
|
|
166
|
+
return {
|
|
167
|
+
files,
|
|
168
|
+
listings,
|
|
169
|
+
deploymentOrigins,
|
|
170
|
+
lockfile: { path: lockfilePath, digest: lockfileDigest },
|
|
171
|
+
manifest: { path: manifestPath, digest: manifestDigest },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/** 返回 null 表示全部输入字节全等;否则返回第一个变化原因。 */
|
|
175
|
+
export function packInputsChangedReason(snapshot, outDir) {
|
|
176
|
+
const lockfileDigest = readFileDigest(snapshot.lockfile.path);
|
|
177
|
+
if (!lockfileDigest)
|
|
178
|
+
return `lockfile unreadable: ${snapshot.lockfile.path}`;
|
|
179
|
+
if (lockfileDigest !== snapshot.lockfile.digest)
|
|
180
|
+
return 'lockfile changed';
|
|
181
|
+
const manifestDigest = readFileDigest(snapshot.manifest.path);
|
|
182
|
+
if (!manifestDigest)
|
|
183
|
+
return `pack manifest unreadable: ${snapshot.manifest.path}`;
|
|
184
|
+
if (manifestDigest !== snapshot.manifest.digest)
|
|
185
|
+
return 'pack manifest changed';
|
|
186
|
+
const deploymentOrigins = deploymentOriginsDigest(outDir);
|
|
187
|
+
if (!deploymentOrigins)
|
|
188
|
+
return 'deployment IR unreadable';
|
|
189
|
+
if (deploymentOrigins !== snapshot.deploymentOrigins)
|
|
190
|
+
return 'deployment IR changed';
|
|
191
|
+
for (const file of snapshot.files) {
|
|
192
|
+
const digest = readFileDigest(file.path);
|
|
193
|
+
if (!digest)
|
|
194
|
+
return `input unreadable: ${file.path}`;
|
|
195
|
+
if (digest !== file.digest)
|
|
196
|
+
return `input changed: ${file.path}`;
|
|
197
|
+
}
|
|
198
|
+
for (const listing of snapshot.listings) {
|
|
199
|
+
const names = listPackInputDir(listing.dir);
|
|
200
|
+
if (!names)
|
|
201
|
+
return `input directory unreadable: ${listing.dir}`;
|
|
202
|
+
if (names.length !== listing.names.length)
|
|
203
|
+
return `input directory changed: ${listing.dir}`;
|
|
204
|
+
for (let i = 0; i < names.length; i += 1) {
|
|
205
|
+
if (names[i] !== listing.names[i])
|
|
206
|
+
return `input directory changed: ${listing.dir}`;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
/** 复用上次 manifest 前确认 entry/chunk/island 查表指向的文件仍在盘上;缺任何一个就退回真实 pack。 */
|
|
212
|
+
export function missingPackOutput(manifest, outDir) {
|
|
213
|
+
const packDir = path.join(outDir, '_vmz', 'pack');
|
|
214
|
+
if (!existsSync(path.join(outDir, '_vmz', 'pack-manifest.json')))
|
|
215
|
+
return '_vmz/pack-manifest.json';
|
|
216
|
+
const names = new Set();
|
|
217
|
+
for (const entry of manifest.entryFiles ?? [])
|
|
218
|
+
if (typeof entry.file === 'string' && entry.file)
|
|
219
|
+
names.add(entry.file);
|
|
220
|
+
for (const dynamic of manifest.dynamicFiles ?? []) {
|
|
221
|
+
if (typeof dynamic.from === 'string' && dynamic.from)
|
|
222
|
+
names.add(dynamic.from);
|
|
223
|
+
if (typeof dynamic.to === 'string' && dynamic.to)
|
|
224
|
+
names.add(dynamic.to);
|
|
225
|
+
}
|
|
226
|
+
for (const chunk of manifest.chunks ?? []) {
|
|
227
|
+
if (typeof chunk.fileName === 'string' && chunk.fileName)
|
|
228
|
+
names.add(chunk.fileName);
|
|
229
|
+
if (typeof chunk.sourceMap === 'string' && chunk.sourceMap)
|
|
230
|
+
names.add(chunk.sourceMap);
|
|
231
|
+
}
|
|
232
|
+
for (const style of manifest.styles ?? [])
|
|
233
|
+
if (typeof style.fileName === 'string' && style.fileName)
|
|
234
|
+
names.add(style.fileName);
|
|
235
|
+
for (const asset of manifest.assets ?? [])
|
|
236
|
+
if (typeof asset.fileName === 'string' && asset.fileName)
|
|
237
|
+
names.add(asset.fileName);
|
|
238
|
+
for (const unit of manifest.unitChunks ?? [])
|
|
239
|
+
if (typeof unit.fileName === 'string' && unit.fileName)
|
|
240
|
+
names.add(unit.fileName);
|
|
241
|
+
for (const name of names) {
|
|
242
|
+
if (!existsSync(path.join(packDir, ...name.split('/'))))
|
|
243
|
+
return name;
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
32
247
|
export function createDevSession(options) {
|
|
33
248
|
const project = options.project;
|
|
34
249
|
const outDir = options.outDir;
|
|
@@ -39,7 +254,8 @@ export function createDevSession(options) {
|
|
|
39
254
|
let delivery = options.delivery ?? null;
|
|
40
255
|
const createWs = options.createWorkspaceFn ?? createWorkspace;
|
|
41
256
|
const spawnHost = options.spawnHostFn ?? defaultSpawnHost;
|
|
42
|
-
const softReload = options.softReloadFn ??
|
|
257
|
+
const softReload = options.softReloadFn ?? ((body) => defaultReloadRequest(host, port, body));
|
|
258
|
+
const packBrowser = options.packFn ?? packFromDeploymentIr;
|
|
43
259
|
const makeConfigCycle = options.configCycleFn ?? createConfigCycle;
|
|
44
260
|
const configCandidates = configCandidatePaths(project);
|
|
45
261
|
let configCycle = null;
|
|
@@ -48,6 +264,9 @@ export function createDevSession(options) {
|
|
|
48
264
|
let stopped = false;
|
|
49
265
|
let lastOutputRevision = null;
|
|
50
266
|
let lastStaticRevision = null;
|
|
267
|
+
let lastBuildMs = 0;
|
|
268
|
+
let lastPackManifest = null;
|
|
269
|
+
let lastPackInputs = null;
|
|
51
270
|
const generationIgnore = new Set();
|
|
52
271
|
function planAndRebuild(changes) {
|
|
53
272
|
if (changes?.length)
|
|
@@ -61,7 +280,9 @@ export function createDevSession(options) {
|
|
|
61
280
|
catch {
|
|
62
281
|
hmrPlan = null;
|
|
63
282
|
}
|
|
283
|
+
const startedAt = Date.now();
|
|
64
284
|
const report = ws.build();
|
|
285
|
+
lastBuildMs = Date.now() - startedAt;
|
|
65
286
|
return { report, hmrPlan };
|
|
66
287
|
}
|
|
67
288
|
function rebuild(changes) {
|
|
@@ -75,7 +296,6 @@ export function createDevSession(options) {
|
|
|
75
296
|
}
|
|
76
297
|
async function maybeReloadAfterBuild(report, hmrPlan, opts = {}) {
|
|
77
298
|
const rev = report.outputRevision ?? report.output_revision ?? null;
|
|
78
|
-
const skipEntry = Boolean(rev && rev === lastOutputRevision);
|
|
79
299
|
if (!opts.force && !opts.staticOnly && !shouldSoftReload(report, lastOutputRevision)) {
|
|
80
300
|
log.info('rebuild ok — artifact unchanged, skip soft reload');
|
|
81
301
|
if (rev)
|
|
@@ -85,14 +305,12 @@ export function createDevSession(options) {
|
|
|
85
305
|
}
|
|
86
306
|
const payload = buildReloadPayload(report, hmrPlan, {
|
|
87
307
|
staticRevision: lastStaticRevision ?? undefined,
|
|
88
|
-
skipEntryRewrite: skipEntry && !opts.force,
|
|
89
308
|
});
|
|
90
309
|
if (opts.staticOnly) {
|
|
91
310
|
payload.full = false;
|
|
92
311
|
payload.islandHmr = false;
|
|
93
|
-
payload.skipEntryRewrite = true;
|
|
94
312
|
}
|
|
95
|
-
const kind = await reloadAfterBuild(payload);
|
|
313
|
+
const kind = await reloadAfterBuild(payload, { staticOnly: Boolean(opts.staticOnly) });
|
|
96
314
|
if (rev)
|
|
97
315
|
lastOutputRevision = rev;
|
|
98
316
|
registerWrittenOutputsIgnore(report.writtenOutputs ?? report.written_outputs, outDir, project, generationIgnore);
|
|
@@ -117,7 +335,7 @@ export function createDevSession(options) {
|
|
|
117
335
|
return false;
|
|
118
336
|
const mode = report.full ? 'full' : 'affected';
|
|
119
337
|
const chunks = (report.affectedChunks || []).join(', ') || '(none)';
|
|
120
|
-
log.info(`${label} ok (${mode}; chunks=[${chunks}]; ${(report.emitted ?? []).length} emitted)`);
|
|
338
|
+
log.info(`${label} ok (${mode}; chunks=[${chunks}]; ${(report.emitted ?? []).length} emitted; ${lastBuildMs}ms build)`);
|
|
121
339
|
return true;
|
|
122
340
|
}
|
|
123
341
|
/**
|
|
@@ -149,11 +367,11 @@ export function createDevSession(options) {
|
|
|
149
367
|
log.info(`wechat pack ok → ${path.join(project, packRoot)} (WeChat DevTools compiles WXSS here)`);
|
|
150
368
|
return true;
|
|
151
369
|
}
|
|
152
|
-
async function
|
|
370
|
+
async function waitHostUp(timeoutMs = 8000) {
|
|
153
371
|
const start = Date.now();
|
|
154
372
|
while (Date.now() - start < timeoutMs) {
|
|
155
373
|
try {
|
|
156
|
-
const res = await fetch(`http://${host}:${port}/__vmz/
|
|
374
|
+
const res = await fetch(`http://${host}:${port}/__vmz/health`);
|
|
157
375
|
if (res.ok)
|
|
158
376
|
return;
|
|
159
377
|
}
|
|
@@ -162,7 +380,7 @@ export function createDevSession(options) {
|
|
|
162
380
|
}
|
|
163
381
|
await sleep(50);
|
|
164
382
|
}
|
|
165
|
-
throw new Error(`serve-host not
|
|
383
|
+
throw new Error(`serve-host not up on ${host}:${port}`);
|
|
166
384
|
}
|
|
167
385
|
async function waitHostGone(timeoutMs = 3000) {
|
|
168
386
|
const start = Date.now();
|
|
@@ -176,29 +394,93 @@ export function createDevSession(options) {
|
|
|
176
394
|
}
|
|
177
395
|
}
|
|
178
396
|
}
|
|
179
|
-
async function respawnHost(reason) {
|
|
180
|
-
log.warn(`respawning serve-host (${reason})…`);
|
|
181
|
-
killChild(child);
|
|
182
|
-
child = null;
|
|
183
|
-
await waitHostGone();
|
|
184
|
-
child = spawnHost({ project, outDir, host, port });
|
|
185
|
-
await waitHostReady();
|
|
186
|
-
log.info('serve-host respawned — browser SSE will reconnect and reload');
|
|
187
|
-
}
|
|
188
397
|
/**
|
|
189
|
-
*
|
|
190
|
-
*
|
|
398
|
+
* 两阶段重建:host `prepare` 写入口 → CLI 侧 `vmz-pack` → host `commit` 发布。
|
|
399
|
+
* dev 的浏览器交付必须走 pack,否则逐文件模块保留裸包说明符。
|
|
400
|
+
* pack 失败显式 SSE error 且不回退到未打包入口。`staticOnly`(仅 public
|
|
401
|
+
* 资产)不改变客户端图,因此不重打包。
|
|
191
402
|
*/
|
|
192
|
-
async function
|
|
403
|
+
async function reloadWithPack(payload, opts = {}) {
|
|
193
404
|
try {
|
|
194
|
-
await softReload(
|
|
195
|
-
return 'soft';
|
|
405
|
+
await softReload({ phase: 'prepare', payload });
|
|
196
406
|
}
|
|
197
407
|
catch (err) {
|
|
408
|
+
if (opts.initial) {
|
|
409
|
+
log.warn(`initial serve-host load failed (${err}) — host stays up`);
|
|
410
|
+
return 'error';
|
|
411
|
+
}
|
|
198
412
|
log.warn(`soft reload failed (${err}) — auto-respawning serve-host`);
|
|
199
413
|
await respawnHost('soft-reload-failed');
|
|
200
414
|
return 'respawn';
|
|
201
415
|
}
|
|
416
|
+
if (!opts.staticOnly) {
|
|
417
|
+
const startedAt = Date.now();
|
|
418
|
+
const changedReason = lastPackInputs ? packInputsChangedReason(lastPackInputs, outDir) : 'no pack input snapshot';
|
|
419
|
+
const missing = changedReason === null && lastPackManifest ? missingPackOutput(lastPackManifest, outDir) : null;
|
|
420
|
+
if (changedReason === null && !missing && lastPackManifest) {
|
|
421
|
+
log.info(`dev pack skip (inputs unchanged, ${Date.now() - startedAt}ms)`);
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
if (changedReason)
|
|
425
|
+
log.info(`dev pack (inputs changed: ${changedReason})`);
|
|
426
|
+
if (missing)
|
|
427
|
+
log.info(`dev pack (manifest reuse rejected: missing ${missing})`);
|
|
428
|
+
try {
|
|
429
|
+
const { manifest } = packBrowser(outDir, { projectRoot: project });
|
|
430
|
+
lastPackManifest = manifest;
|
|
431
|
+
lastPackInputs = capturePackInputSnapshot(lastPackManifest, outDir, project);
|
|
432
|
+
log.info(`dev pack ok (${manifest.chunkCount} chunks, ${Date.now() - startedAt}ms)`);
|
|
433
|
+
if (!lastPackInputs)
|
|
434
|
+
log.info('dev pack inputs: snapshot unavailable (unknown input) — next rebuild repacks');
|
|
435
|
+
}
|
|
436
|
+
catch (err) {
|
|
437
|
+
lastPackManifest = null;
|
|
438
|
+
lastPackInputs = null;
|
|
439
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
440
|
+
log.error(`dev pack failed: ${message}`);
|
|
441
|
+
try {
|
|
442
|
+
await softReload({
|
|
443
|
+
phase: 'abort',
|
|
444
|
+
error: { message, stack: err instanceof Error ? err.stack : undefined },
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
/* host may be gone; keep the reported pack error */
|
|
449
|
+
}
|
|
450
|
+
return 'error';
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
await softReload({ phase: 'commit' });
|
|
456
|
+
return 'soft';
|
|
457
|
+
}
|
|
458
|
+
catch (err) {
|
|
459
|
+
if (opts.initial) {
|
|
460
|
+
log.warn(`initial serve-host commit failed (${err}) — host stays up`);
|
|
461
|
+
return 'error';
|
|
462
|
+
}
|
|
463
|
+
log.warn(`reload commit failed (${err}) — auto-respawning serve-host`);
|
|
464
|
+
await respawnHost('commit-failed');
|
|
465
|
+
return 'respawn';
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
async function respawnHost(reason) {
|
|
469
|
+
log.warn(`respawning serve-host (${reason})…`);
|
|
470
|
+
killChild(child);
|
|
471
|
+
child = null;
|
|
472
|
+
await waitHostGone();
|
|
473
|
+
child = spawnHost({ project, outDir, host, port });
|
|
474
|
+
await waitHostUp();
|
|
475
|
+
const kind = await reloadWithPack({}, { initial: true });
|
|
476
|
+
if (kind === 'soft')
|
|
477
|
+
log.info('serve-host respawned — browser SSE will reconnect and reload');
|
|
478
|
+
else
|
|
479
|
+
log.warn('serve-host respawned but initial reload failed — fix the error and save');
|
|
480
|
+
}
|
|
481
|
+
/** @param {object} payload */
|
|
482
|
+
async function reloadAfterBuild(payload, opts = {}) {
|
|
483
|
+
return reloadWithPack(payload, opts);
|
|
202
484
|
}
|
|
203
485
|
let docEnginesPromise = null;
|
|
204
486
|
/** `engines.code` / `engines.markdown` for the document layer (loaded once per session). */
|
|
@@ -215,6 +497,33 @@ export function createDevSession(options) {
|
|
|
215
497
|
if (!existsSync(src)) {
|
|
216
498
|
throw new Error(`vmz dev: missing src/ under ${project}`);
|
|
217
499
|
}
|
|
500
|
+
// Snapshot watch roots and their fingerprints **before** the initial build.
|
|
501
|
+
// A save that lands while `vmz dev` boots must still be detected by the first
|
|
502
|
+
// poll. Taking the baseline after the initial reload absorbs that edit into the
|
|
503
|
+
// baseline, so the host serves the pre-edit bytes until the next save.
|
|
504
|
+
const docsRoot = path.join(project, 'documents');
|
|
505
|
+
const localesRoot = path.join(project, 'locales');
|
|
506
|
+
const designsRoot = path.join(project, 'designs');
|
|
507
|
+
const watched = collectDevWatchRoots({ project, outDir });
|
|
508
|
+
const publicRoot = watched.publicRoot;
|
|
509
|
+
/** Candidate config files plus their local reachable deps; refreshed after each cycle. */
|
|
510
|
+
const refreshConfigPaths = () => {
|
|
511
|
+
const deps = configCandidates
|
|
512
|
+
.filter((p) => existsSync(p))
|
|
513
|
+
.flatMap((p) => collectConfigDependencies(p, project));
|
|
514
|
+
return [...new Set([...configCandidates, ...deps])];
|
|
515
|
+
};
|
|
516
|
+
let configPaths = refreshConfigPaths();
|
|
517
|
+
/** @type {string[]} */
|
|
518
|
+
const watchRoots = [...watched.roots];
|
|
519
|
+
/** @type {string[]} */
|
|
520
|
+
let dependencyRoots = [...watched.dependencyRoots];
|
|
521
|
+
/** @type {Map<string, Map<string, string>>} */
|
|
522
|
+
const fingerprints = new Map();
|
|
523
|
+
for (const root of watchRoots) {
|
|
524
|
+
fingerprints.set(root, fileFingerprintMap(root));
|
|
525
|
+
}
|
|
526
|
+
let configFingerprints = configFingerprintMap(configPaths);
|
|
218
527
|
// Config evaluation + plugin contributions run in a detached build process.
|
|
219
528
|
// Functions/plugin objects never cross the boundary; only the data report does.
|
|
220
529
|
if (configCandidates.some((p) => existsSync(p))) {
|
|
@@ -253,27 +562,16 @@ export function createDevSession(options) {
|
|
|
253
562
|
throw new Error(`vmz dev: missing ${hostJs}`);
|
|
254
563
|
}
|
|
255
564
|
child = spawnHost({ project, outDir, host, port });
|
|
256
|
-
await
|
|
565
|
+
await waitHostUp().catch((err) => {
|
|
257
566
|
log.warn(String(err));
|
|
258
567
|
});
|
|
568
|
+
const initialKind = await reloadAfterBuild({}, { staticOnly: false }).catch((err) => {
|
|
569
|
+
log.warn(`initial reload failed: ${err}`);
|
|
570
|
+
return 'error';
|
|
571
|
+
});
|
|
572
|
+
if (initialKind !== 'soft')
|
|
573
|
+
log.warn('initial serve-host reload failed — fix the error and save');
|
|
259
574
|
}
|
|
260
|
-
const docsRoot = path.join(project, 'documents');
|
|
261
|
-
const localesRoot = path.join(project, 'locales');
|
|
262
|
-
const designsRoot = path.join(project, 'designs');
|
|
263
|
-
const watched = collectDevWatchRoots({ project, outDir });
|
|
264
|
-
const publicRoot = watched.publicRoot;
|
|
265
|
-
/** Candidate config files plus their local reachable deps; refreshed after each cycle. */
|
|
266
|
-
const refreshConfigPaths = () => {
|
|
267
|
-
const deps = configCandidates
|
|
268
|
-
.filter((p) => existsSync(p))
|
|
269
|
-
.flatMap((p) => collectConfigDependencies(p, project));
|
|
270
|
-
return [...new Set([...configCandidates, ...deps])];
|
|
271
|
-
};
|
|
272
|
-
let configPaths = refreshConfigPaths();
|
|
273
|
-
/** @type {string[]} */
|
|
274
|
-
const watchRoots = [...watched.roots];
|
|
275
|
-
/** @type {string[]} */
|
|
276
|
-
let dependencyRoots = [...watched.dependencyRoots];
|
|
277
575
|
if (wechatPreview) {
|
|
278
576
|
log.info(`dev → WeChat DevTools (watching ${watchRoots.join(', ')}; keep dist/wechat open)`);
|
|
279
577
|
}
|
|
@@ -283,12 +581,6 @@ export function createDevSession(options) {
|
|
|
283
581
|
if (dependencyRoots.length) {
|
|
284
582
|
log.info(`dev dependency watch roots (${dependencyRoots.length}): ${dependencyRoots.join(', ')}`);
|
|
285
583
|
}
|
|
286
|
-
/** @type {Map<string, Map<string, string>>} */
|
|
287
|
-
const fingerprints = new Map();
|
|
288
|
-
for (const root of watchRoots) {
|
|
289
|
-
fingerprints.set(root, fileFingerprintMap(root));
|
|
290
|
-
}
|
|
291
|
-
let configFingerprints = configFingerprintMap(configPaths);
|
|
292
584
|
const signal = options.signal;
|
|
293
585
|
const onAbort = () => {
|
|
294
586
|
void stop();
|
|
@@ -360,8 +652,7 @@ export function createDevSession(options) {
|
|
|
360
652
|
break;
|
|
361
653
|
if (!wechatPreview && child && child.exitCode != null) {
|
|
362
654
|
log.warn(`serve-host exited (${child.exitCode}) — respawning…`);
|
|
363
|
-
|
|
364
|
-
await waitHostReady().catch(() => { });
|
|
655
|
+
await respawnHost('host-exited');
|
|
365
656
|
continue;
|
|
366
657
|
}
|
|
367
658
|
let batch;
|
|
@@ -459,7 +750,7 @@ export function createDevSession(options) {
|
|
|
459
750
|
commitFingerprints();
|
|
460
751
|
if (!wechatPreview) {
|
|
461
752
|
const kind = await maybeReloadAfterBuild({ reloadRequired: true }, null, { force: true, staticOnly: true });
|
|
462
|
-
if (kind
|
|
753
|
+
if (kind === 'soft' || kind === 'respawn')
|
|
463
754
|
log.info('soft reload ok (public assets)');
|
|
464
755
|
}
|
|
465
756
|
continue;
|
|
@@ -555,7 +846,7 @@ export function createDevSession(options) {
|
|
|
555
846
|
}
|
|
556
847
|
const kind = await maybeReloadAfterBuild(report, hmrPlan, { force: true });
|
|
557
848
|
commitFingerprints();
|
|
558
|
-
if (kind
|
|
849
|
+
if (kind === 'soft' || kind === 'respawn') {
|
|
559
850
|
log.info(kind === 'respawn' ? 'reload ok (respawned; deps)' : 'soft reload ok (full page; deps)');
|
|
560
851
|
}
|
|
561
852
|
continue;
|
|
@@ -592,7 +883,7 @@ export function createDevSession(options) {
|
|
|
592
883
|
force: needFullReload,
|
|
593
884
|
});
|
|
594
885
|
commitFingerprints();
|
|
595
|
-
if (kind
|
|
886
|
+
if (kind === 'soft' || kind === 'respawn') {
|
|
596
887
|
log.info(kind === 'respawn'
|
|
597
888
|
? 'reload ok (respawned serve-host)'
|
|
598
889
|
: needFullReload
|
|
@@ -627,7 +918,7 @@ export function createDevSession(options) {
|
|
|
627
918
|
}
|
|
628
919
|
const kind = await maybeReloadAfterBuild(report, hmrPlan, { force: true });
|
|
629
920
|
commitFingerprints();
|
|
630
|
-
if (kind
|
|
921
|
+
if (kind === 'soft' || kind === 'respawn') {
|
|
631
922
|
log.info(kind === 'respawn' ? 'reload ok (respawned; designs)' : 'soft reload ok (full page; designs)');
|
|
632
923
|
}
|
|
633
924
|
continue;
|
|
@@ -648,7 +939,7 @@ export function createDevSession(options) {
|
|
|
648
939
|
else {
|
|
649
940
|
const kind = await maybeReloadAfterBuild({ reloadRequired: true, full: true }, null, { force: true });
|
|
650
941
|
commitFingerprints();
|
|
651
|
-
if (kind
|
|
942
|
+
if (kind === 'soft' || kind === 'respawn') {
|
|
652
943
|
log.info(kind === 'respawn' ? 'reload ok (respawned; locales)' : 'soft reload ok (full page; locales)');
|
|
653
944
|
}
|
|
654
945
|
if (!batch.docsDirty)
|
|
@@ -670,7 +961,7 @@ export function createDevSession(options) {
|
|
|
670
961
|
else {
|
|
671
962
|
const kind = await maybeReloadAfterBuild({ reloadRequired: true, full: true }, null, { force: true });
|
|
672
963
|
commitFingerprints();
|
|
673
|
-
if (kind
|
|
964
|
+
if (kind === 'soft' || kind === 'respawn') {
|
|
674
965
|
log.info(kind === 'respawn' ? 'reload ok (respawned; docs)' : 'soft reload ok (full page; docs)');
|
|
675
966
|
}
|
|
676
967
|
}
|
|
@@ -740,23 +1031,24 @@ function defaultSpawnHost(opts) {
|
|
|
740
1031
|
return child;
|
|
741
1032
|
}
|
|
742
1033
|
/**
|
|
1034
|
+
* Host reload request(两阶段协议:`prepare` / `commit` / `abort`)。
|
|
743
1035
|
* @param {string} host
|
|
744
1036
|
* @param {number} port
|
|
745
|
-
* @param {
|
|
1037
|
+
* @param {Record<string, unknown>} body
|
|
746
1038
|
*/
|
|
747
|
-
async function
|
|
1039
|
+
async function defaultReloadRequest(host, port, body) {
|
|
748
1040
|
const url = `http://${host}:${port}/__vmz/reload`;
|
|
749
|
-
const body = JSON.stringify(payload);
|
|
750
1041
|
const res = await fetch(url, {
|
|
751
1042
|
method: 'POST',
|
|
752
1043
|
headers: { 'content-type': 'application/json' },
|
|
753
|
-
body,
|
|
1044
|
+
body: JSON.stringify(body),
|
|
754
1045
|
});
|
|
755
1046
|
if (!res.ok)
|
|
756
1047
|
throw new Error(`HTTP ${res.status}`);
|
|
757
1048
|
const json = await res.json().catch(() => ({}));
|
|
758
1049
|
if (!json?.ok)
|
|
759
1050
|
throw new Error(JSON.stringify(json));
|
|
1051
|
+
return json;
|
|
760
1052
|
}
|
|
761
1053
|
function killChild(child) {
|
|
762
1054
|
if (!child || child.killed)
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* colors inside code blocks come from the Shiki theme and stay identical on
|
|
9
9
|
* every site. Sites do not ship document stylesheets.
|
|
10
10
|
*
|
|
11
|
-
* House rules
|
|
11
|
+
* House rules: pure-white base with near-black blocks for
|
|
12
12
|
* rhythm, body text ≥ 16px, small text ≥ 12px at weight ≥ 400, no uppercase
|
|
13
13
|
* letter-spaced micro labels, no 3D / volumetric surfaces.
|
|
14
14
|
*/
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* colors inside code blocks come from the Shiki theme and stay identical on
|
|
9
9
|
* every site. Sites do not ship document stylesheets.
|
|
10
10
|
*
|
|
11
|
-
* House rules
|
|
11
|
+
* House rules: pure-white base with near-black blocks for
|
|
12
12
|
* rhythm, body text ≥ 16px, small text ≥ 12px at weight ≥ 400, no uppercase
|
|
13
13
|
* letter-spaced micro labels, no 3D / volumetric surfaces.
|
|
14
14
|
*/
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Document — wire the compiled app stylesheet and the framework-owned document
|
|
3
3
|
* base layer into static document assets.
|
|
4
4
|
*
|
|
5
|
-
* Layering
|
|
5
|
+
* Layering:
|
|
6
6
|
* - `assets/vmz-designs.css`: the site's compiled design tokens (`vmz-designs.css`
|
|
7
7
|
* / `vmz.css` from the app build, or `designs/styles/index.css` standalone).
|
|
8
8
|
* - `assets/vmz-document.css`: the framework document base (`DOCUMENT_BASE_CSS`),
|
|
@@ -21,6 +21,7 @@ function rewriteFlatDeliveryImports(text) {
|
|
|
21
21
|
['../browser/direct-host-box.js', './direct-host-box.js'],
|
|
22
22
|
['../browser/unknown-component.js', './unknown-component.js'],
|
|
23
23
|
['../shared/dom-attr-normalize.js', './dom-attr-normalize.js'],
|
|
24
|
+
['../shared/html-digest.js', './html-digest.js'],
|
|
24
25
|
['../browser/client-nav.js', './vmz-client-nav.js'],
|
|
25
26
|
['../ssr/dom-ssr.js', './dom-ssr.js'],
|
|
26
27
|
['../faces/server.js', './vmz-runtime.js'],
|
|
@@ -34,6 +35,10 @@ function rewriteFlatDeliveryImports(text) {
|
|
|
34
35
|
}
|
|
35
36
|
function rewriteNestedSsrDelivery(text) {
|
|
36
37
|
let out = text;
|
|
38
|
+
// `shared/` is flattened to the delivery root, so nested SSR files must
|
|
39
|
+
// reach the root copy one level up.
|
|
40
|
+
out = replaceImportPath(out, '../shared/dom-attr-normalize.js', '../dom-attr-normalize.js');
|
|
41
|
+
out = replaceImportPath(out, '../shared/html-digest.js', '../html-digest.js');
|
|
37
42
|
for (const [from, to] of [
|
|
38
43
|
['../browser/dom-core.js', '../dom-core.js'],
|
|
39
44
|
['../browser/direct-host-box.js', '../direct-host-box.js'],
|
|
@@ -45,6 +50,7 @@ function rewriteNestedSsrDelivery(text) {
|
|
|
45
50
|
}
|
|
46
51
|
function rewriteNestedBrowserDelivery(text) {
|
|
47
52
|
let out = replaceImportPath(text, '../../shared/dom-attr-normalize.js', '../../dom-attr-normalize.js');
|
|
53
|
+
out = replaceImportPath(out, '../../shared/html-digest.js', '../../html-digest.js');
|
|
48
54
|
// `dom-ssr.js` 只交付在根(flat barrel),nested `ssr/` 下没有它:
|
|
49
55
|
// island resume 的动态边必须指向根级产物。
|
|
50
56
|
out = replaceImportPath(out, '../../ssr/dom-ssr.js', '../../dom-ssr.js');
|
|
@@ -39,6 +39,12 @@ export interface PackRequest {
|
|
|
39
39
|
entries: PackRequestEntry[];
|
|
40
40
|
target: PackTarget;
|
|
41
41
|
root: string;
|
|
42
|
+
/**
|
|
43
|
+
* Output tree root (absolute). Modules under it are labelled relative to it,
|
|
44
|
+
* so chunk ids stay identical when `--out-dir` moves or the project is built
|
|
45
|
+
* on another machine. Source-tree modules stay relative to `root`.
|
|
46
|
+
*/
|
|
47
|
+
outRoot: string;
|
|
42
48
|
uses: PackRequestUse[];
|
|
43
49
|
publicBase: string;
|
|
44
50
|
/**
|
|
@@ -89,6 +95,10 @@ export interface PackPlan {
|
|
|
89
95
|
entryFiles: PackEntryFile[];
|
|
90
96
|
dynamicFiles: PackDynamicFile[];
|
|
91
97
|
manifestJson: string;
|
|
98
|
+
/** Content digest of this pack input (graph structure + source bytes + emit options). */
|
|
99
|
+
inputDigest: string;
|
|
100
|
+
/** Graph-structure digest; any change invalidates every cached chunk. */
|
|
101
|
+
graphDigest: string;
|
|
92
102
|
}
|
|
93
103
|
export interface PackDiagnostic {
|
|
94
104
|
code: string;
|
|
@@ -126,6 +136,8 @@ export interface BuildPackRequestInput {
|
|
|
126
136
|
entries: Record<string, string>;
|
|
127
137
|
target: PackTarget;
|
|
128
138
|
root: string;
|
|
139
|
+
/** Output tree root: chunk id label base for emitted modules. */
|
|
140
|
+
outRoot: string;
|
|
129
141
|
uses: PackRequestUse[];
|
|
130
142
|
publicBase: string;
|
|
131
143
|
/** Emitted absolute path → source absolute path (projection provenance). */
|
|
@@ -122,7 +122,7 @@ function parseDynamicFile(value, where) {
|
|
|
122
122
|
function parsePackPlan(value) {
|
|
123
123
|
if (!isPlainObject(value))
|
|
124
124
|
throw responseShape('response.plan must be an object');
|
|
125
|
-
rejectUnknownKeys(value, ['artifacts', 'styles', 'assets', 'entryFiles', 'dynamicFiles', 'manifestJson'], 'response.plan');
|
|
125
|
+
rejectUnknownKeys(value, ['artifacts', 'styles', 'assets', 'entryFiles', 'dynamicFiles', 'manifestJson', 'inputDigest', 'graphDigest'], 'response.plan');
|
|
126
126
|
if (!Array.isArray(value.artifacts))
|
|
127
127
|
throw responseShape('response.plan.artifacts must be an array');
|
|
128
128
|
if (!Array.isArray(value.styles))
|
|
@@ -140,6 +140,8 @@ function parsePackPlan(value) {
|
|
|
140
140
|
entryFiles: value.entryFiles.map((e, i) => parseEntryFile(e, `response.plan.entryFiles[${i}]`)),
|
|
141
141
|
dynamicFiles: value.dynamicFiles.map((d, i) => parseDynamicFile(d, `response.plan.dynamicFiles[${i}]`)),
|
|
142
142
|
manifestJson: requireString(value.manifestJson, 'response.plan.manifestJson'),
|
|
143
|
+
inputDigest: requireString(value.inputDigest, 'response.plan.inputDigest'),
|
|
144
|
+
graphDigest: requireString(value.graphDigest, 'response.plan.graphDigest'),
|
|
143
145
|
};
|
|
144
146
|
}
|
|
145
147
|
/**
|
|
@@ -199,6 +201,11 @@ export function buildPackRequest(input) {
|
|
|
199
201
|
throw requestInvalid('root must be a non-empty string');
|
|
200
202
|
if (!path.isAbsolute(input.root))
|
|
201
203
|
throw requestInvalid(`root must be an absolute path: ${input.root}`);
|
|
204
|
+
if (typeof input.outRoot !== 'string' || !input.outRoot) {
|
|
205
|
+
throw requestInvalid('outRoot must be a non-empty string');
|
|
206
|
+
}
|
|
207
|
+
if (!path.isAbsolute(input.outRoot))
|
|
208
|
+
throw requestInvalid(`outRoot must be an absolute path: ${input.outRoot}`);
|
|
202
209
|
if (typeof input.publicBase !== 'string' || !input.publicBase) {
|
|
203
210
|
throw requestInvalid('publicBase must be a non-empty string');
|
|
204
211
|
}
|
|
@@ -249,7 +256,7 @@ export function buildPackRequest(input) {
|
|
|
249
256
|
if (!Object.keys(origins).length)
|
|
250
257
|
origins = undefined;
|
|
251
258
|
}
|
|
252
|
-
const request = { entries, target: input.target, root: input.root, uses, publicBase: input.publicBase, ...(origins ? { origins } : {}) };
|
|
259
|
+
const request = { entries, target: input.target, root: input.root, outRoot: input.outRoot, uses, publicBase: input.publicBase, ...(origins ? { origins } : {}) };
|
|
253
260
|
return JSON.stringify(request);
|
|
254
261
|
}
|
|
255
262
|
function resolveBinding(opts, fn) {
|
package/dist/workspace/pack.d.ts
CHANGED
|
@@ -10,11 +10,14 @@
|
|
|
10
10
|
* `execution: 'full'` + `exports: { kind: 'all' }` (script-tag load,
|
|
11
11
|
* whole-module execution).
|
|
12
12
|
*
|
|
13
|
-
* SSR/Node is a separate graph:
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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`.
|
|
18
21
|
*
|
|
19
22
|
* No vendor materialization, no specifier lowering, and no second hashing
|
|
20
23
|
* pass happen here: `packWrite` emits the final browser JS set under
|
|
@@ -37,6 +40,17 @@ export declare const PACK_PUBLIC_BASE = "/";
|
|
|
37
40
|
* specifiers keep resolving against the emitted tree.
|
|
38
41
|
*/
|
|
39
42
|
export declare function deploymentUnitOrigins(absOut: string, ir: DeploymentIr): Record<string, string>;
|
|
43
|
+
/** 一个 deployment 逻辑 chunkId 在浏览器图里的落点(未进图时字段为 `null`)。 */
|
|
44
|
+
export interface PackUnitChunk {
|
|
45
|
+
/** 运行时逻辑 chunk id(`vmz-deployment.json` 的 `units[].chunkId`)。 */
|
|
46
|
+
logical: string;
|
|
47
|
+
/** 该单元的产物客户端文件(输出树相对路径)。 */
|
|
48
|
+
clientEntry: string;
|
|
49
|
+
/** 承载该文件的 chunk id,未进客户端图时为 `null`。 */
|
|
50
|
+
chunkId: string | null;
|
|
51
|
+
/** 承载该文件的 chunk 文件名,未进客户端图时为 `null`。 */
|
|
52
|
+
fileName: string | null;
|
|
53
|
+
}
|
|
40
54
|
/**
|
|
41
55
|
* Ensure dom split companions sit next to vmz-dom.js (barrel imports ./dom-core.js).
|
|
42
56
|
* Always refresh from `@vmz/core` when present so SSR/runtime fixes are not sticky in outDir.
|
|
@@ -81,12 +95,15 @@ export declare function packFromDeploymentIr(outDir: string, opts?: PackFromDepl
|
|
|
81
95
|
styles: unknown[];
|
|
82
96
|
assets: unknown[];
|
|
83
97
|
chunks: unknown[];
|
|
98
|
+
unitChunks: PackUnitChunk[];
|
|
84
99
|
chunkCount: number;
|
|
85
100
|
styleCount: number;
|
|
86
101
|
entryCount: number;
|
|
87
102
|
minify: string;
|
|
88
103
|
treeShake: string;
|
|
89
104
|
sourceMaps: string;
|
|
105
|
+
packInputDigest: string;
|
|
106
|
+
packGraphDigest: string;
|
|
90
107
|
nativeManifest: unknown;
|
|
91
108
|
packDigest?: string;
|
|
92
109
|
};
|
|
@@ -104,11 +121,11 @@ interface PackSsrFromDeploymentIrOpts {
|
|
|
104
121
|
}
|
|
105
122
|
/**
|
|
106
123
|
* Pack the SSR/Node graph via the Rust `vmz-pack` binding and write
|
|
107
|
-
* `_vmz/pack-node-manifest.json` (v0). Entries are the
|
|
108
|
-
* compiled client modules (`clientEntry`, empty → mirrors
|
|
109
|
-
* `component_entries`: `${chunkId}.client.js`). Node consumers
|
|
110
|
-
* `default`, but the import itself is evaluated, hence
|
|
111
|
-
* named default rather than `all` / `full` / `unconsumed`.
|
|
124
|
+
* `_vmz/pack-node-manifest.json` (v0). Entries are the node-reachable
|
|
125
|
+
* deployment units' compiled client modules (`clientEntry`, empty → mirrors
|
|
126
|
+
* `vmz-artifacts` `component_entries`: `${chunkId}.client.js`). Node consumers
|
|
127
|
+
* only read `default`, but the import itself is evaluated, hence
|
|
128
|
+
* `side-effects` + named default rather than `all` / `full` / `unconsumed`.
|
|
112
129
|
*/
|
|
113
130
|
export declare function packSsrFromDeploymentIr(outDir: string, opts?: PackSsrFromDeploymentIrOpts): {
|
|
114
131
|
manifest: {
|
|
@@ -124,6 +141,8 @@ export declare function packSsrFromDeploymentIr(outDir: string, opts?: PackSsrFr
|
|
|
124
141
|
chunks: unknown[];
|
|
125
142
|
chunkCount: number;
|
|
126
143
|
entryCount: number;
|
|
144
|
+
packInputDigest: string;
|
|
145
|
+
packGraphDigest: string;
|
|
127
146
|
nativeManifest: unknown;
|
|
128
147
|
packDigest?: string;
|
|
129
148
|
};
|
package/dist/workspace/pack.js
CHANGED
|
@@ -10,11 +10,14 @@
|
|
|
10
10
|
* `execution: 'full'` + `exports: { kind: 'all' }` (script-tag load,
|
|
11
11
|
* whole-module execution).
|
|
12
12
|
*
|
|
13
|
-
* SSR/Node is a separate graph:
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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`.
|
|
18
21
|
*
|
|
19
22
|
* No vendor materialization, no specifier lowering, and no second hashing
|
|
20
23
|
* pass happen here: `packWrite` emits the final browser JS set under
|
|
@@ -22,7 +25,7 @@
|
|
|
22
25
|
* entries through `packEntryHref()` against this manifest.
|
|
23
26
|
*/
|
|
24
27
|
import crypto from 'node:crypto';
|
|
25
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync,
|
|
28
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
26
29
|
import path from 'node:path';
|
|
27
30
|
import { loadDeploymentIr } from './bundler-adapter.js';
|
|
28
31
|
import { buildPackRequest, runPackWrite } from './pack-native.js';
|
|
@@ -59,6 +62,43 @@ export function deploymentUnitOrigins(absOut, ir) {
|
|
|
59
62
|
}
|
|
60
63
|
/** Bootstrap modules that are browser pack entries when present in outDir. */
|
|
61
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
|
+
}
|
|
62
102
|
/**
|
|
63
103
|
* Ensure dom split companions sit next to vmz-dom.js (barrel imports ./dom-core.js).
|
|
64
104
|
* Always refresh from `@vmz/core` when present so SSR/runtime fixes are not sticky in outDir.
|
|
@@ -107,11 +147,18 @@ export function packFromDeploymentIr(outDir, opts = {}) {
|
|
|
107
147
|
// Browser graph entries are the bootstrap modules; page/island units join
|
|
108
148
|
// through the bootstrap dynamic-import tables and need projection provenance.
|
|
109
149
|
const origins = deploymentUnitOrigins(absOut, loadDeploymentIr(absOut));
|
|
110
|
-
const requestJson = buildPackRequest({
|
|
150
|
+
const requestJson = buildPackRequest({
|
|
151
|
+
entries,
|
|
152
|
+
target: 'browser',
|
|
153
|
+
root,
|
|
154
|
+
outRoot: absOut,
|
|
155
|
+
uses,
|
|
156
|
+
publicBase: PACK_PUBLIC_BASE,
|
|
157
|
+
origins,
|
|
158
|
+
});
|
|
111
159
|
const packOutDir = path.join(absOut, ...PACK_OUTPUT_DIR_REL.split('/'));
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
rmSync(packOutDir, { recursive: true, force: true });
|
|
160
|
+
// 不再整体清空输出目录:`packWrite` 按内容摘要复用未变 chunk,并在写盘后
|
|
161
|
+
// 逐文件清理不再属于产物闭包的旧 chunk / map / css / assets(不留孤儿)。
|
|
115
162
|
const { plan } = runPackWrite(requestJson, packOutDir, opts.binding ? { binding: opts.binding } : {});
|
|
116
163
|
const ir = loadDeploymentIr(absOut);
|
|
117
164
|
let nativeManifest;
|
|
@@ -151,18 +198,27 @@ export function packFromDeploymentIr(outDir, opts = {}) {
|
|
|
151
198
|
styles: plan.styles,
|
|
152
199
|
assets: plan.assets,
|
|
153
200
|
chunks,
|
|
201
|
+
unitChunks: resolveUnitChunks(absOut, ir.units || [], plan.artifacts),
|
|
154
202
|
chunkCount: chunks.length,
|
|
155
203
|
styleCount: plan.styles.length,
|
|
156
204
|
entryCount: plan.entryFiles.length,
|
|
157
|
-
// `vmz-pack`
|
|
158
|
-
|
|
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',
|
|
159
209
|
// `link` active fixed-point pruning really ran — bootstrap entries
|
|
160
210
|
// declare full execution, internal modules are pruned to reachable exports.
|
|
161
211
|
treeShake: 'vmz-pack-link-pruning',
|
|
162
|
-
// Chunk source maps are
|
|
163
|
-
// to its module line (`<packOut>/<fileName>.map`,
|
|
164
|
-
// on the wire)
|
|
165
|
-
|
|
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,
|
|
166
222
|
nativeManifest,
|
|
167
223
|
};
|
|
168
224
|
body.packDigest = sha256Hex(stableStringify({ ...body }));
|
|
@@ -177,18 +233,61 @@ export const PACK_NODE_MANIFEST_SCHEMA = 'vmz.pack.node-manifest.v0';
|
|
|
177
233
|
export const PACK_NODE_OUTPUT_DIR_REL = '_vmz/pack-node';
|
|
178
234
|
/** Node pack manifest (relative to outDir); `entryFiles` maps logical → chunk file. */
|
|
179
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
|
+
}
|
|
180
275
|
/**
|
|
181
276
|
* Pack the SSR/Node graph via the Rust `vmz-pack` binding and write
|
|
182
|
-
* `_vmz/pack-node-manifest.json` (v0). Entries are the
|
|
183
|
-
* compiled client modules (`clientEntry`, empty → mirrors
|
|
184
|
-
* `component_entries`: `${chunkId}.client.js`). Node consumers
|
|
185
|
-
* `default`, but the import itself is evaluated, hence
|
|
186
|
-
* named default rather than `all` / `full` / `unconsumed`.
|
|
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`.
|
|
187
282
|
*/
|
|
188
283
|
export function packSsrFromDeploymentIr(outDir, opts = {}) {
|
|
189
284
|
const absOut = path.resolve(outDir);
|
|
190
285
|
const root = opts.projectRoot ? path.resolve(opts.projectRoot) : path.dirname(absOut);
|
|
191
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);
|
|
192
291
|
const entries = {};
|
|
193
292
|
const uses = [];
|
|
194
293
|
// Deployment IR may list the same component unit more than once (one entry
|
|
@@ -200,6 +299,8 @@ export function packSsrFromDeploymentIr(outDir, opts = {}) {
|
|
|
200
299
|
const chunkId = String(unit.chunkId || '').replace(/\\/g, '/');
|
|
201
300
|
if (!chunkId)
|
|
202
301
|
throw new Error(`packSsrFromDeploymentIr: deployment unit has an empty chunkId in ${absOut}`);
|
|
302
|
+
if (!nodeReachable.has(chunkId))
|
|
303
|
+
continue;
|
|
203
304
|
// `vmz-artifacts` derives the component entry as `${chunkId}.client.js`
|
|
204
305
|
// when `clientEntry` is empty. Decide it here so the pack request stays
|
|
205
306
|
// explicit, then require the file — a missing entry is never skipped.
|
|
@@ -220,13 +321,20 @@ export function packSsrFromDeploymentIr(outDir, opts = {}) {
|
|
|
220
321
|
uses.push({ logical: chunkId, execution: 'side-effects', exports: { kind: 'named', names: ['default'] } });
|
|
221
322
|
}
|
|
222
323
|
if (!Object.keys(entries).length) {
|
|
223
|
-
throw new Error(`packSsrFromDeploymentIr: deployment IR has no units in ${absOut}`);
|
|
324
|
+
throw new Error(`packSsrFromDeploymentIr: deployment IR has no node-reachable units in ${absOut}`);
|
|
224
325
|
}
|
|
225
326
|
const origins = deploymentUnitOrigins(absOut, ir);
|
|
226
|
-
const requestJson = buildPackRequest({
|
|
327
|
+
const requestJson = buildPackRequest({
|
|
328
|
+
entries,
|
|
329
|
+
target: 'node',
|
|
330
|
+
root,
|
|
331
|
+
outRoot: absOut,
|
|
332
|
+
uses,
|
|
333
|
+
publicBase: PACK_PUBLIC_BASE,
|
|
334
|
+
origins,
|
|
335
|
+
});
|
|
227
336
|
const packOutDir = path.join(absOut, ...PACK_NODE_OUTPUT_DIR_REL.split('/'));
|
|
228
|
-
// 与浏览器 pack
|
|
229
|
-
rmSync(packOutDir, { recursive: true, force: true });
|
|
337
|
+
// 与浏览器 pack 同口径:不清空目录,由 `packWrite` 复用未变 chunk 并清理孤儿。
|
|
230
338
|
const { plan } = runPackWrite(requestJson, packOutDir, opts.binding ? { binding: opts.binding } : {});
|
|
231
339
|
let nativeManifest;
|
|
232
340
|
try {
|
|
@@ -262,6 +370,9 @@ export function packSsrFromDeploymentIr(outDir, opts = {}) {
|
|
|
262
370
|
chunks,
|
|
263
371
|
chunkCount: chunks.length,
|
|
264
372
|
entryCount: plan.entryFiles.length,
|
|
373
|
+
// 同浏览器 pack:输入/图结构摘要来自 Rust 侧内容摘要判定。
|
|
374
|
+
packInputDigest: plan.inputDigest,
|
|
375
|
+
packGraphDigest: plan.graphDigest,
|
|
265
376
|
nativeManifest,
|
|
266
377
|
};
|
|
267
378
|
body.packDigest = sha256Hex(stableStringify({ ...body }));
|
package/locales/en-US/diag.json
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
"vmz::template::illegal_directive": "Illegal or unsupported directive in `<template>`: {detail}",
|
|
11
11
|
"vmz::template::parse_failed": "Failed to parse `<template>`: {detail}",
|
|
12
12
|
"vmz::template::invalid_expr": "Invalid template expression: {detail}",
|
|
13
|
+
"vmz::template::v_model_invalid_target": "`v-model` target must be an assignable identifier or member expression: `{expr}`",
|
|
14
|
+
"vmz::template::v_model_on_file_input": "`v-model` cannot bind an `<input type=\"file\">`",
|
|
13
15
|
"vmz::template::each_missing_key": "`v-for` on `<{tag}>` should declare a `:key`",
|
|
14
16
|
"vmz::template::each_static_key": "`v-for` on `<{tag}>` must not use a static key `{key}`",
|
|
15
17
|
"vmz::template::each_literal_key": "`v-for` on `<{tag}>` must not use a literal key `{key}`",
|
package/locales/zh-CN/diag.json
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
"vmz::template::illegal_directive": "`<template>` 中存在非法或不支持的指令:{detail}",
|
|
7
7
|
"vmz::template::parse_failed": "解析 `<template>` 失败:{detail}",
|
|
8
8
|
"vmz::template::invalid_expr": "模板表达式无效:{detail}",
|
|
9
|
+
"vmz::template::v_model_invalid_target": "`v-model` 的目标必须是可赋值的标识符或成员表达式:`{expr}`",
|
|
10
|
+
"vmz::template::v_model_on_file_input": "`v-model` 不能绑定到 `<input type=\"file\">`",
|
|
9
11
|
"vmz::template::each_missing_key": "`<{tag}>` 上的 `v-for` 应声明 `:key`",
|
|
10
12
|
"vmz::template::each_static_key": "`<{tag}>` 上的 `v-for` 不得使用静态 key `{key}`",
|
|
11
13
|
"vmz::template::each_literal_key": "`<{tag}>` 上的 `v-for` 不得使用字面量 key `{key}`",
|
package/package.json
CHANGED
|
@@ -3,23 +3,23 @@
|
|
|
3
3
|
"vmz": "./bin/vmz.js"
|
|
4
4
|
},
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@vmz/commander": "0.3.
|
|
7
|
-
"@vmz/core": "0.3.
|
|
8
|
-
"@vmz/diagnostic": "0.3.
|
|
9
|
-
"@vmz/plugin": "0.3.
|
|
10
|
-
"@vmz/protocol": "0.3.
|
|
6
|
+
"@vmz/commander": "0.3.14",
|
|
7
|
+
"@vmz/core": "0.3.14",
|
|
8
|
+
"@vmz/diagnostic": "0.3.14",
|
|
9
|
+
"@vmz/plugin": "0.3.14",
|
|
10
|
+
"@vmz/protocol": "0.3.14"
|
|
11
11
|
},
|
|
12
12
|
"description": "VMZ Node toolchain — N-API workspace session + CLI (publish name @vmz/vmz)",
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"@types/node": "^22.15.0",
|
|
15
|
-
"@vmz/plugin": "0.3.
|
|
16
|
-
"@vmz/plugin-katex": "0.3.
|
|
17
|
-
"@vmz/plugin-markdown-it": "0.3.
|
|
18
|
-
"@vmz/plugin-shiki": "0.3.
|
|
19
|
-
"@vmz/test": "0.3.
|
|
15
|
+
"@vmz/plugin": "0.3.14",
|
|
16
|
+
"@vmz/plugin-katex": "0.3.14",
|
|
17
|
+
"@vmz/plugin-markdown-it": "0.3.14",
|
|
18
|
+
"@vmz/plugin-shiki": "0.3.14",
|
|
19
|
+
"@vmz/test": "0.3.14",
|
|
20
20
|
"linkedom": "^0.18.13",
|
|
21
21
|
"typescript": "^5.8.3",
|
|
22
|
-
"vmz-plugin-conformance": "0.3.
|
|
22
|
+
"vmz-plugin-conformance": "0.3.14"
|
|
23
23
|
},
|
|
24
24
|
"engines": {
|
|
25
25
|
"node": ">=24.12.0"
|
|
@@ -80,16 +80,16 @@
|
|
|
80
80
|
"main": "./dist/index.js",
|
|
81
81
|
"name": "@vmz/vmz",
|
|
82
82
|
"optionalDependencies": {
|
|
83
|
-
"@vmz/vmz-darwin-arm64": "0.3.
|
|
84
|
-
"@vmz/vmz-darwin-x64": "0.3.
|
|
85
|
-
"@vmz/vmz-linux-arm64": "0.3.
|
|
86
|
-
"@vmz/vmz-linux-x64": "0.3.
|
|
87
|
-
"@vmz/vmz-win32-arm64": "0.3.
|
|
88
|
-
"@vmz/vmz-win32-x64": "0.3.
|
|
83
|
+
"@vmz/vmz-darwin-arm64": "0.3.14",
|
|
84
|
+
"@vmz/vmz-darwin-x64": "0.3.14",
|
|
85
|
+
"@vmz/vmz-linux-arm64": "0.3.14",
|
|
86
|
+
"@vmz/vmz-linux-x64": "0.3.14",
|
|
87
|
+
"@vmz/vmz-win32-arm64": "0.3.14",
|
|
88
|
+
"@vmz/vmz-win32-x64": "0.3.14"
|
|
89
89
|
},
|
|
90
90
|
"peerDependencies": {
|
|
91
|
-
"@vmz/plugin-markdown-it": "0.3.
|
|
92
|
-
"@vmz/test": "0.3.
|
|
91
|
+
"@vmz/plugin-markdown-it": "0.3.14",
|
|
92
|
+
"@vmz/test": "0.3.14",
|
|
93
93
|
"typescript": "^5.8.3"
|
|
94
94
|
},
|
|
95
95
|
"peerDependenciesMeta": {
|
|
@@ -122,5 +122,5 @@
|
|
|
122
122
|
},
|
|
123
123
|
"type": "module",
|
|
124
124
|
"types": "./dist/index.d.ts",
|
|
125
|
-
"version": "0.3.
|
|
125
|
+
"version": "0.3.14"
|
|
126
126
|
}
|