@vmz/vmz 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dev-session.js +140 -48
- package/dist/dev-watch-roots.d.ts +67 -0
- package/dist/dev-watch-roots.js +220 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +12 -12
package/dist/dev-session.js
CHANGED
|
@@ -17,6 +17,7 @@ import { buildIntegratedDocuments, projectHasDocuments } from './document-integr
|
|
|
17
17
|
import { createWorkspace, resolveNativePath } from './index.js';
|
|
18
18
|
import { emitLocaleRuntimeModules, localeHasErrors } from './locale-check.js';
|
|
19
19
|
import { log } from './log.js';
|
|
20
|
+
import { coalesceRootBurst, collectDevWatchRoots, isDependencyPath, mergeDirtySets, } from './dev-watch-roots.js';
|
|
20
21
|
import { diffFingerprints, fileFingerprintMap } from './watch-diff.js';
|
|
21
22
|
/**
|
|
22
23
|
* @typedef {object} DevSessionOptions
|
|
@@ -189,13 +190,20 @@ export function createDevSession(options) {
|
|
|
189
190
|
}
|
|
190
191
|
const docsRoot = path.join(project, 'documents');
|
|
191
192
|
const localesRoot = path.join(project, 'locales');
|
|
192
|
-
const
|
|
193
|
+
const watched = collectDevWatchRoots({ project, outDir });
|
|
194
|
+
/** @type {string[]} */
|
|
195
|
+
const watchRoots = [...watched.roots];
|
|
196
|
+
/** @type {string[]} */
|
|
197
|
+
let dependencyRoots = [...watched.dependencyRoots];
|
|
193
198
|
if (wechatPreview) {
|
|
194
199
|
log.info(`dev → WeChat DevTools (watching ${watchRoots.join(', ')}; keep dist/wechat open)`);
|
|
195
200
|
}
|
|
196
201
|
else {
|
|
197
202
|
log.info(`dev → http://${host}:${port} (watching ${watchRoots.join(', ')})`);
|
|
198
203
|
}
|
|
204
|
+
if (dependencyRoots.length) {
|
|
205
|
+
log.info(`dev dependency watch roots (${dependencyRoots.length}): ${dependencyRoots.join(', ')}`);
|
|
206
|
+
}
|
|
199
207
|
/** @type {Map<string, Map<string, string>>} */
|
|
200
208
|
const fingerprints = new Map();
|
|
201
209
|
for (const root of watchRoots) {
|
|
@@ -207,18 +215,48 @@ export function createDevSession(options) {
|
|
|
207
215
|
};
|
|
208
216
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
209
217
|
/**
|
|
210
|
-
*
|
|
218
|
+
* Scan all watch roots into a batch. Does not update fingerprints.
|
|
211
219
|
*/
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
220
|
+
function scanBatch() {
|
|
221
|
+
/** @type {{ srcChanged: string[], srcDeleted: string[], depChanged: string[], depDeleted: string[], docsDirty: boolean, localesDirty: boolean }} */
|
|
222
|
+
const batch = {
|
|
223
|
+
srcChanged: [],
|
|
224
|
+
srcDeleted: [],
|
|
225
|
+
depChanged: [],
|
|
226
|
+
depDeleted: [],
|
|
227
|
+
docsDirty: false,
|
|
228
|
+
localesDirty: false,
|
|
229
|
+
};
|
|
230
|
+
for (const root of watchRoots) {
|
|
231
|
+
const prev = fingerprints.get(root) || new Map();
|
|
232
|
+
const next = fileFingerprintMap(root);
|
|
218
233
|
const diff = diffFingerprints(prev, next);
|
|
219
234
|
if (!diff.changed.length && !diff.deleted.length)
|
|
220
|
-
|
|
221
|
-
|
|
235
|
+
continue;
|
|
236
|
+
if (root === src) {
|
|
237
|
+
batch.srcChanged.push(...diff.changed);
|
|
238
|
+
batch.srcDeleted.push(...diff.deleted);
|
|
239
|
+
}
|
|
240
|
+
else if (root === localesRoot) {
|
|
241
|
+
batch.localesDirty = true;
|
|
242
|
+
}
|
|
243
|
+
else if (root === docsRoot) {
|
|
244
|
+
batch.docsDirty = true;
|
|
245
|
+
}
|
|
246
|
+
else if (dependencyRoots.includes(root)) {
|
|
247
|
+
batch.depChanged.push(...diff.changed);
|
|
248
|
+
batch.depDeleted.push(...diff.deleted);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
// designs or other application roots → treat like docs (full reload)
|
|
252
|
+
batch.docsDirty = true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return batch;
|
|
256
|
+
}
|
|
257
|
+
function commitFingerprints() {
|
|
258
|
+
for (const root of watchRoots) {
|
|
259
|
+
fingerprints.set(root, fileFingerprintMap(root));
|
|
222
260
|
}
|
|
223
261
|
}
|
|
224
262
|
try {
|
|
@@ -232,58 +270,112 @@ export function createDevSession(options) {
|
|
|
232
270
|
await waitHostReady().catch(() => { });
|
|
233
271
|
continue;
|
|
234
272
|
}
|
|
235
|
-
|
|
236
|
-
let batch = { srcChanged: [], srcDeleted: [], docsDirty: false, localesDirty: false };
|
|
273
|
+
let batch;
|
|
237
274
|
try {
|
|
238
|
-
|
|
239
|
-
const prev = fingerprints.get(root) || new Map();
|
|
240
|
-
const next = fileFingerprintMap(root);
|
|
241
|
-
const diff = diffFingerprints(prev, next);
|
|
242
|
-
if (root === src) {
|
|
243
|
-
batch.srcChanged = diff.changed;
|
|
244
|
-
batch.srcDeleted = diff.deleted;
|
|
245
|
-
}
|
|
246
|
-
else if (root === localesRoot) {
|
|
247
|
-
if (diff.changed.length || diff.deleted.length)
|
|
248
|
-
batch.localesDirty = true;
|
|
249
|
-
}
|
|
250
|
-
else if (diff.changed.length || diff.deleted.length) {
|
|
251
|
-
batch.docsDirty = true;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
275
|
+
batch = scanBatch();
|
|
254
276
|
}
|
|
255
277
|
catch (err) {
|
|
256
278
|
log.warn('watch error:', err);
|
|
257
279
|
continue;
|
|
258
280
|
}
|
|
259
|
-
|
|
281
|
+
const srcDirty = batch.srcChanged.length + batch.srcDeleted.length;
|
|
282
|
+
const depDirty = batch.depChanged.length + batch.depDeleted.length;
|
|
283
|
+
if (!srcDirty && !depDirty && !batch.docsDirty && !batch.localesDirty)
|
|
260
284
|
continue;
|
|
261
|
-
|
|
262
|
-
|
|
285
|
+
// Coalesce multi-file bursts without dropping the initial dirty set.
|
|
286
|
+
if (srcDirty > 1) {
|
|
287
|
+
const coalesced = await coalesceRootBurst(src, fingerprints, {
|
|
288
|
+
changed: batch.srcChanged,
|
|
289
|
+
deleted: batch.srcDeleted,
|
|
290
|
+
});
|
|
291
|
+
batch.srcChanged = coalesced.changed;
|
|
292
|
+
batch.srcDeleted = coalesced.deleted;
|
|
263
293
|
}
|
|
264
|
-
|
|
294
|
+
if (depDirty > 1) {
|
|
295
|
+
// Coalesce each dirty dependency root independently, then merge.
|
|
296
|
+
/** @type {{ changed: string[], deleted: string[] }} */
|
|
297
|
+
let acc = { changed: [...batch.depChanged], deleted: [...batch.depDeleted] };
|
|
298
|
+
for (const root of dependencyRoots) {
|
|
299
|
+
const prev = fingerprints.get(root) || new Map();
|
|
300
|
+
const next = fileFingerprintMap(root);
|
|
301
|
+
const peek = diffFingerprints(prev, next);
|
|
302
|
+
if (peek.changed.length + peek.deleted.length <= 1 && !acc.changed.some((f) => isDependencyPath(f, [root]))) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const coalesced = await coalesceRootBurst(root, fingerprints, {
|
|
306
|
+
changed: acc.changed.filter((f) => isDependencyPath(f, [root])),
|
|
307
|
+
deleted: acc.deleted.filter((f) => isDependencyPath(f, [root])),
|
|
308
|
+
});
|
|
309
|
+
const others = {
|
|
310
|
+
changed: acc.changed.filter((f) => !isDependencyPath(f, [root])),
|
|
311
|
+
deleted: acc.deleted.filter((f) => !isDependencyPath(f, [root])),
|
|
312
|
+
};
|
|
313
|
+
acc = mergeDirtySets(others, coalesced);
|
|
314
|
+
}
|
|
315
|
+
batch.depChanged = acc.changed;
|
|
316
|
+
batch.depDeleted = acc.deleted;
|
|
317
|
+
}
|
|
318
|
+
else if (srcDirty === 1 || depDirty === 1) {
|
|
265
319
|
await sleep(200);
|
|
266
320
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
321
|
+
// Refresh residual dirty after settle (without discarding coalesced sets).
|
|
322
|
+
const residual = scanBatch();
|
|
323
|
+
{
|
|
324
|
+
const srcMerged = mergeDirtySets({ changed: batch.srcChanged, deleted: batch.srcDeleted }, { changed: residual.srcChanged, deleted: residual.srcDeleted });
|
|
325
|
+
batch.srcChanged = srcMerged.changed;
|
|
326
|
+
batch.srcDeleted = srcMerged.deleted;
|
|
327
|
+
const depMerged = mergeDirtySets({ changed: batch.depChanged, deleted: batch.depDeleted }, { changed: residual.depChanged, deleted: residual.depDeleted });
|
|
328
|
+
batch.depChanged = depMerged.changed;
|
|
329
|
+
batch.depDeleted = depMerged.deleted;
|
|
330
|
+
batch.docsDirty = batch.docsDirty || residual.docsDirty;
|
|
331
|
+
batch.localesDirty = batch.localesDirty || residual.localesDirty;
|
|
332
|
+
}
|
|
333
|
+
commitFingerprints();
|
|
334
|
+
if (!batch.srcChanged.length &&
|
|
335
|
+
!batch.srcDeleted.length &&
|
|
336
|
+
!batch.depChanged.length &&
|
|
337
|
+
!batch.depDeleted.length &&
|
|
338
|
+
!batch.docsDirty &&
|
|
339
|
+
!batch.localesDirty) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
// Dependency changes: conservative full rebuild + full reload (v0.1.5).
|
|
343
|
+
if (batch.depChanged.length || batch.depDeleted.length) {
|
|
344
|
+
log.info(`dependency change detected (${batch.depChanged.length} update, ${batch.depDeleted.length} delete) — full rebuild…`);
|
|
345
|
+
const report = rebuild();
|
|
346
|
+
if (!printReport(report, 'rebuild')) {
|
|
347
|
+
log.warn('rebuild failed — keeping previous server');
|
|
348
|
+
continue;
|
|
276
349
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
350
|
+
// Refresh watch roots in case the graph gained new packages.
|
|
351
|
+
const refreshed = collectDevWatchRoots({ project, outDir });
|
|
352
|
+
for (const r of refreshed.roots) {
|
|
353
|
+
if (!watchRoots.includes(r)) {
|
|
354
|
+
watchRoots.push(r);
|
|
355
|
+
fingerprints.set(r, fileFingerprintMap(r));
|
|
356
|
+
}
|
|
280
357
|
}
|
|
281
|
-
|
|
282
|
-
|
|
358
|
+
dependencyRoots = [...refreshed.dependencyRoots];
|
|
359
|
+
if (batch.docsDirty || projectHasDocuments(project)) {
|
|
360
|
+
const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
|
|
361
|
+
if (!docs.ok)
|
|
362
|
+
log.warn('document mount rebuild failed — keeping previous docs');
|
|
283
363
|
}
|
|
284
|
-
|
|
285
|
-
|
|
364
|
+
if (wechatPreview) {
|
|
365
|
+
if (!packWechatPreview())
|
|
366
|
+
log.warn('wechat pack failed — keeping previous dist/wechat');
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const kind = await reloadAfterBuild({
|
|
370
|
+
affectedChunks: report.affectedChunks ?? [],
|
|
371
|
+
seedChunks: report.seedChunks ?? [],
|
|
372
|
+
emitted: report.emitted ?? [],
|
|
373
|
+
full: true,
|
|
374
|
+
islandHmr: false,
|
|
375
|
+
});
|
|
376
|
+
log.info(kind === 'respawn' ? 'reload ok (respawned; deps)' : 'soft reload ok (full page; deps)');
|
|
286
377
|
continue;
|
|
378
|
+
}
|
|
287
379
|
let needFullReload = batch.docsDirty;
|
|
288
380
|
if (batch.srcChanged.length || batch.srcDeleted.length) {
|
|
289
381
|
log.info(`change detected (${batch.srcChanged.length} update, ${batch.srcDeleted.length} delete) — affected rebuild…`);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev watch helpers: coalesce multi-file bursts without dropping dirty set,
|
|
3
|
+
* and derive extra watch roots from the compile graph (deployment unit sources).
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {{ changed: string[], deleted: string[] }} DirtySet
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Merge dirty sets: later change cancels delete and vice versa.
|
|
10
|
+
* @param {DirtySet} a
|
|
11
|
+
* @param {DirtySet} b
|
|
12
|
+
* @returns {DirtySet}
|
|
13
|
+
*/
|
|
14
|
+
export declare function mergeDirtySets(a: any, b: any): {
|
|
15
|
+
changed: unknown[];
|
|
16
|
+
deleted: unknown[];
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Wait until `root` stops changing; return the **accumulated** dirty set since `initial`.
|
|
20
|
+
* Updates `fingerprints` for `root` as it polls. Does **not** discard `initial`.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} root
|
|
23
|
+
* @param {Map<string, Map<string, string>>} fingerprints
|
|
24
|
+
* @param {DirtySet} initial
|
|
25
|
+
* @param {{ sleep?: (ms: number) => Promise<void>, maxRounds?: number, settleMs?: number }} [opts]
|
|
26
|
+
* @returns {Promise<DirtySet>}
|
|
27
|
+
*/
|
|
28
|
+
export declare function coalesceRootBurst(root: any, fingerprints: any, initial: any, opts?: {}): Promise<{
|
|
29
|
+
changed: any[];
|
|
30
|
+
deleted: any[];
|
|
31
|
+
}>;
|
|
32
|
+
/**
|
|
33
|
+
* Walk up from a file to find a package.json directory.
|
|
34
|
+
* @param {string} file
|
|
35
|
+
* @returns {string | null}
|
|
36
|
+
*/
|
|
37
|
+
export declare function findPackageRoot(file: any): string;
|
|
38
|
+
/**
|
|
39
|
+
* Prefer package/src when present; otherwise the directory containing the source file.
|
|
40
|
+
* @param {string} sourceFile
|
|
41
|
+
* @returns {string | null}
|
|
42
|
+
*/
|
|
43
|
+
export declare function watchRootForSourceFile(sourceFile: any): string;
|
|
44
|
+
/**
|
|
45
|
+
* Absolute roots for workspace / file / link deps that have a src tree.
|
|
46
|
+
* @param {string} project
|
|
47
|
+
* @returns {string[]}
|
|
48
|
+
*/
|
|
49
|
+
export declare function localLinkDependencyRoots(project: any): any[];
|
|
50
|
+
/**
|
|
51
|
+
* Collect watch roots: project src/locales/documents + compile-graph external sources
|
|
52
|
+
* + local link/workspace package roots. Never adds a bare registry node_modules tree.
|
|
53
|
+
*
|
|
54
|
+
* @param {{ project: string, outDir: string }} opts
|
|
55
|
+
* @returns {{ roots: string[], dependencyRoots: string[], applicationRoots: string[] }}
|
|
56
|
+
*/
|
|
57
|
+
export declare function collectDevWatchRoots(opts: any): {
|
|
58
|
+
roots: any[];
|
|
59
|
+
dependencyRoots: unknown[];
|
|
60
|
+
applicationRoots: any[];
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Classify whether a changed file lives under a dependency watch root (not app src).
|
|
64
|
+
* @param {string} file
|
|
65
|
+
* @param {string[]} dependencyRoots
|
|
66
|
+
*/
|
|
67
|
+
export declare function isDependencyPath(file: any, dependencyRoots: any): any;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Dev watch helpers: coalesce multi-file bursts without dropping dirty set,
|
|
4
|
+
* and derive extra watch roots from the compile graph (deployment unit sources).
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { diffFingerprints, fileFingerprintMap } from './watch-diff.js';
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {{ changed: string[], deleted: string[] }} DirtySet
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Merge dirty sets: later change cancels delete and vice versa.
|
|
14
|
+
* @param {DirtySet} a
|
|
15
|
+
* @param {DirtySet} b
|
|
16
|
+
* @returns {DirtySet}
|
|
17
|
+
*/
|
|
18
|
+
export function mergeDirtySets(a, b) {
|
|
19
|
+
const changed = new Set(a?.changed || []);
|
|
20
|
+
const deleted = new Set(a?.deleted || []);
|
|
21
|
+
for (const f of b?.changed || []) {
|
|
22
|
+
changed.add(f);
|
|
23
|
+
deleted.delete(f);
|
|
24
|
+
}
|
|
25
|
+
for (const f of b?.deleted || []) {
|
|
26
|
+
deleted.add(f);
|
|
27
|
+
changed.delete(f);
|
|
28
|
+
}
|
|
29
|
+
return { changed: [...changed], deleted: [...deleted] };
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Wait until `root` stops changing; return the **accumulated** dirty set since `initial`.
|
|
33
|
+
* Updates `fingerprints` for `root` as it polls. Does **not** discard `initial`.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} root
|
|
36
|
+
* @param {Map<string, Map<string, string>>} fingerprints
|
|
37
|
+
* @param {DirtySet} initial
|
|
38
|
+
* @param {{ sleep?: (ms: number) => Promise<void>, maxRounds?: number, settleMs?: number }} [opts]
|
|
39
|
+
* @returns {Promise<DirtySet>}
|
|
40
|
+
*/
|
|
41
|
+
export async function coalesceRootBurst(root, fingerprints, initial, opts = {}) {
|
|
42
|
+
const sleepFn = opts.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
43
|
+
const maxRounds = opts.maxRounds ?? 20;
|
|
44
|
+
const settleMs = opts.settleMs ?? 220;
|
|
45
|
+
let accumulated = {
|
|
46
|
+
changed: [...(initial?.changed || [])],
|
|
47
|
+
deleted: [...(initial?.deleted || [])],
|
|
48
|
+
};
|
|
49
|
+
let guard = 0;
|
|
50
|
+
while (guard++ < maxRounds) {
|
|
51
|
+
await sleepFn(settleMs);
|
|
52
|
+
const prev = fingerprints.get(root) || new Map();
|
|
53
|
+
const next = fileFingerprintMap(root);
|
|
54
|
+
const diff = diffFingerprints(prev, next);
|
|
55
|
+
fingerprints.set(root, next);
|
|
56
|
+
if (!diff.changed.length && !diff.deleted.length)
|
|
57
|
+
break;
|
|
58
|
+
accumulated = mergeDirtySets(accumulated, diff);
|
|
59
|
+
}
|
|
60
|
+
return accumulated;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Walk up from a file to find a package.json directory.
|
|
64
|
+
* @param {string} file
|
|
65
|
+
* @returns {string | null}
|
|
66
|
+
*/
|
|
67
|
+
export function findPackageRoot(file) {
|
|
68
|
+
let dir = path.dirname(path.resolve(file));
|
|
69
|
+
for (let i = 0; i < 24; i++) {
|
|
70
|
+
if (existsSync(path.join(dir, 'package.json')))
|
|
71
|
+
return dir;
|
|
72
|
+
const parent = path.dirname(dir);
|
|
73
|
+
if (parent === dir)
|
|
74
|
+
break;
|
|
75
|
+
dir = parent;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Prefer package/src when present; otherwise the directory containing the source file.
|
|
81
|
+
* @param {string} sourceFile
|
|
82
|
+
* @returns {string | null}
|
|
83
|
+
*/
|
|
84
|
+
export function watchRootForSourceFile(sourceFile) {
|
|
85
|
+
const abs = path.resolve(sourceFile);
|
|
86
|
+
if (!existsSync(abs))
|
|
87
|
+
return null;
|
|
88
|
+
const pkg = findPackageRoot(abs);
|
|
89
|
+
if (pkg) {
|
|
90
|
+
const src = path.join(pkg, 'src');
|
|
91
|
+
if (existsSync(src))
|
|
92
|
+
return src;
|
|
93
|
+
return pkg;
|
|
94
|
+
}
|
|
95
|
+
return path.dirname(abs);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Absolute roots for workspace / file / link deps that have a src tree.
|
|
99
|
+
* @param {string} project
|
|
100
|
+
* @returns {string[]}
|
|
101
|
+
*/
|
|
102
|
+
export function localLinkDependencyRoots(project) {
|
|
103
|
+
const pkgPath = path.join(path.resolve(project), 'package.json');
|
|
104
|
+
if (!existsSync(pkgPath))
|
|
105
|
+
return [];
|
|
106
|
+
/** @type {string[]} */
|
|
107
|
+
const roots = [];
|
|
108
|
+
try {
|
|
109
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
110
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
111
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
112
|
+
if (typeof spec !== 'string')
|
|
113
|
+
continue;
|
|
114
|
+
let target = null;
|
|
115
|
+
if (spec.startsWith('workspace:')) {
|
|
116
|
+
// Resolve via node_modules (pnpm links workspace packages there).
|
|
117
|
+
const nm = path.join(path.resolve(project), 'node_modules', ...name.split('/'));
|
|
118
|
+
if (existsSync(path.join(nm, 'package.json')))
|
|
119
|
+
target = nm;
|
|
120
|
+
}
|
|
121
|
+
else if (spec.startsWith('file:') || spec.startsWith('link:')) {
|
|
122
|
+
const rel = spec.replace(/^(file|link):/, '');
|
|
123
|
+
target = path.resolve(project, rel);
|
|
124
|
+
}
|
|
125
|
+
if (!target)
|
|
126
|
+
continue;
|
|
127
|
+
try {
|
|
128
|
+
target = realpathSync(target);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* keep as-is */
|
|
132
|
+
}
|
|
133
|
+
const src = path.join(target, 'src');
|
|
134
|
+
if (existsSync(src))
|
|
135
|
+
roots.push(src);
|
|
136
|
+
else if (existsSync(target))
|
|
137
|
+
roots.push(target);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
/* ignore */
|
|
142
|
+
}
|
|
143
|
+
return roots;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Collect watch roots: project src/locales/documents + compile-graph external sources
|
|
147
|
+
* + local link/workspace package roots. Never adds a bare registry node_modules tree.
|
|
148
|
+
*
|
|
149
|
+
* @param {{ project: string, outDir: string }} opts
|
|
150
|
+
* @returns {{ roots: string[], dependencyRoots: string[], applicationRoots: string[] }}
|
|
151
|
+
*/
|
|
152
|
+
export function collectDevWatchRoots(opts) {
|
|
153
|
+
const project = path.resolve(opts.project);
|
|
154
|
+
const outDir = path.resolve(opts.outDir);
|
|
155
|
+
const src = path.join(project, 'src');
|
|
156
|
+
const docsRoot = path.join(project, 'documents');
|
|
157
|
+
const localesRoot = path.join(project, 'locales');
|
|
158
|
+
const designsRoot = path.join(project, 'designs');
|
|
159
|
+
/** @type {string[]} */
|
|
160
|
+
const applicationRoots = [];
|
|
161
|
+
if (existsSync(src))
|
|
162
|
+
applicationRoots.push(src);
|
|
163
|
+
if (existsSync(docsRoot))
|
|
164
|
+
applicationRoots.push(docsRoot);
|
|
165
|
+
if (existsSync(localesRoot))
|
|
166
|
+
applicationRoots.push(localesRoot);
|
|
167
|
+
if (existsSync(designsRoot))
|
|
168
|
+
applicationRoots.push(designsRoot);
|
|
169
|
+
/** @type {Set<string>} */
|
|
170
|
+
const depSet = new Set();
|
|
171
|
+
const depJson = path.join(outDir, 'vmz-deployment.json');
|
|
172
|
+
if (existsSync(depJson)) {
|
|
173
|
+
try {
|
|
174
|
+
const dep = JSON.parse(readFileSync(depJson, 'utf8'));
|
|
175
|
+
for (const unit of dep.units || []) {
|
|
176
|
+
const source = unit?.source;
|
|
177
|
+
if (typeof source !== 'string' || !source)
|
|
178
|
+
continue;
|
|
179
|
+
let abs = path.resolve(source);
|
|
180
|
+
try {
|
|
181
|
+
abs = realpathSync(abs);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
/* keep */
|
|
185
|
+
}
|
|
186
|
+
const underProject = abs === project || abs.startsWith(project + path.sep) || abs.startsWith(project + '/');
|
|
187
|
+
if (underProject)
|
|
188
|
+
continue;
|
|
189
|
+
const root = watchRootForSourceFile(abs);
|
|
190
|
+
if (root)
|
|
191
|
+
depSet.add(path.resolve(root));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
/* ignore corrupt deployment */
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
for (const r of localLinkDependencyRoots(project)) {
|
|
199
|
+
depSet.add(path.resolve(r));
|
|
200
|
+
}
|
|
201
|
+
// Drop dependency roots that are already under an application root.
|
|
202
|
+
const dependencyRoots = [...depSet].filter((r) => {
|
|
203
|
+
return !applicationRoots.some((app) => r === app || r.startsWith(app + path.sep) || r.startsWith(app + '/'));
|
|
204
|
+
});
|
|
205
|
+
const roots = [...applicationRoots];
|
|
206
|
+
for (const r of dependencyRoots) {
|
|
207
|
+
if (!roots.includes(r))
|
|
208
|
+
roots.push(r);
|
|
209
|
+
}
|
|
210
|
+
return { roots, dependencyRoots, applicationRoots };
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Classify whether a changed file lives under a dependency watch root (not app src).
|
|
214
|
+
* @param {string} file
|
|
215
|
+
* @param {string[]} dependencyRoots
|
|
216
|
+
*/
|
|
217
|
+
export function isDependencyPath(file, dependencyRoots) {
|
|
218
|
+
const abs = path.resolve(file);
|
|
219
|
+
return (dependencyRoots || []).some((r) => abs === r || abs.startsWith(r + path.sep) || abs.startsWith(r + '/'));
|
|
220
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -147,6 +147,7 @@ export declare function checkApplicationHostCompositionJson(hostRoot: any, packa
|
|
|
147
147
|
*/
|
|
148
148
|
export declare function checkApplicationDevTestDeployJson(hostRoot: any, packageRoots: any, dirtyPaths?: any[]): any;
|
|
149
149
|
export { createDevSession, listWatchedFiles, srcFingerprint } from './dev-session.js';
|
|
150
|
+
export { coalesceRootBurst, collectDevWatchRoots, mergeDirtySets, localLinkDependencyRoots, watchRootForSourceFile, } from './dev-watch-roots.js';
|
|
150
151
|
export { findAvailablePort } from './port.js';
|
|
151
152
|
export { runCli, parseArgs, printHelp, printGlobalHelp, printProjectHelp } from './cli.js';
|
|
152
153
|
export { findNearestProjectVmz, getInvocationContext, isGlobalAllowedCommand, isUnderNodeModules, resolveThisPackageRoot, resolveVmzBin, gateGlobalProjectCommand, } from './invocation.js';
|
package/dist/index.js
CHANGED
|
@@ -369,6 +369,7 @@ export function checkApplicationDevTestDeployJson(hostRoot, packageRoots, dirtyP
|
|
|
369
369
|
return native.checkApplicationDevTestDeployJson(hostRoot, packageRoots, dirtyPaths);
|
|
370
370
|
}
|
|
371
371
|
export { createDevSession, listWatchedFiles, srcFingerprint } from './dev-session.js';
|
|
372
|
+
export { coalesceRootBurst, collectDevWatchRoots, mergeDirtySets, localLinkDependencyRoots, watchRootForSourceFile, } from './dev-watch-roots.js';
|
|
372
373
|
export { findAvailablePort } from './port.js';
|
|
373
374
|
export { runCli, parseArgs, printHelp, printGlobalHelp, printProjectHelp } from './cli.js';
|
|
374
375
|
export { findNearestProjectVmz, getInvocationContext, isGlobalAllowedCommand, isUnderNodeModules, resolveThisPackageRoot, resolveVmzBin, gateGlobalProjectCommand, } from './invocation.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vmz/vmz",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "VMZ Node toolchain — N-API workspace session + CLI (publish name @vmz/vmz)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,15 +48,15 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@vmz/core": "0.1.
|
|
52
|
-
"@vmz/plugin": "0.1.
|
|
53
|
-
"@vmz/protocol": "0.1.
|
|
51
|
+
"@vmz/core": "0.1.6",
|
|
52
|
+
"@vmz/plugin": "0.1.6",
|
|
53
|
+
"@vmz/protocol": "0.1.6",
|
|
54
54
|
"jiti": "^2.6.1",
|
|
55
55
|
"json5": "^2.2.3"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@vmz/plugin-markdown-it": "0.1.
|
|
59
|
-
"@vmz/test": "0.1.
|
|
58
|
+
"@vmz/plugin-markdown-it": "0.1.6",
|
|
59
|
+
"@vmz/test": "0.1.6",
|
|
60
60
|
"typescript": "^5.8.3"
|
|
61
61
|
},
|
|
62
62
|
"peerDependenciesMeta": {
|
|
@@ -90,12 +90,12 @@
|
|
|
90
90
|
"cli"
|
|
91
91
|
],
|
|
92
92
|
"optionalDependencies": {
|
|
93
|
-
"@vmz/vmz-win32-x64": "0.1.
|
|
94
|
-
"@vmz/vmz-win32-arm64": "0.1.
|
|
95
|
-
"@vmz/vmz-darwin-x64": "0.1.
|
|
96
|
-
"@vmz/vmz-darwin-arm64": "0.1.
|
|
97
|
-
"@vmz/vmz-linux-x64": "0.1.
|
|
98
|
-
"@vmz/vmz-linux-arm64": "0.1.
|
|
93
|
+
"@vmz/vmz-win32-x64": "0.1.6",
|
|
94
|
+
"@vmz/vmz-win32-arm64": "0.1.6",
|
|
95
|
+
"@vmz/vmz-darwin-x64": "0.1.6",
|
|
96
|
+
"@vmz/vmz-darwin-arm64": "0.1.6",
|
|
97
|
+
"@vmz/vmz-linux-x64": "0.1.6",
|
|
98
|
+
"@vmz/vmz-linux-arm64": "0.1.6"
|
|
99
99
|
},
|
|
100
100
|
"publishConfig": {
|
|
101
101
|
"access": "public"
|