@zhin.js/runtime 1.1.2 → 1.1.4
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/lib/config-composer.d.ts +564 -0
- package/lib/config-composer.js +1 -1
- package/lib/hmr-coordinator.js +48 -22
- package/lib/invalidation-planner.js +5 -4
- package/lib/module-dependency-index.d.ts +17 -0
- package/lib/module-dependency-index.js +403 -0
- package/lib/module-runtime.d.ts +6 -0
- package/lib/native-development-runtime.d.ts +6 -3
- package/lib/native-development-runtime.js +69 -18
- package/lib/plugin-scope-assembler.d.ts +4 -1
- package/lib/plugin-scope-assembler.js +12 -0
- package/lib/root-runtime.js +11 -9
- package/lib/source-ownership.d.ts +1 -0
- package/lib/source-ownership.js +9 -2
- package/lib/source-path.d.ts +4 -0
- package/lib/source-path.js +25 -0
- package/lib/subtree-generation-preparer.js +1 -0
- package/lib/topology-generation-preparer.js +1 -0
- package/lib/typescript-specifier-remap.d.ts +7 -9
- package/lib/typescript-specifier-remap.js +28 -3
- package/package.json +15 -13
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { normalizeSourcePath } from './source-path.js';
|
|
2
|
+
import { basename } from 'node:path';
|
|
2
3
|
import { rootPluginId } from '@zhin.js/plugin-runtime';
|
|
3
4
|
const processFiles = new Set([
|
|
4
5
|
'pnpm-lock.yaml',
|
|
@@ -14,7 +15,7 @@ export class InvalidationPlanner {
|
|
|
14
15
|
this.dependencies = dependencies;
|
|
15
16
|
}
|
|
16
17
|
plan(sources) {
|
|
17
|
-
const changed = unique(sources.map((source) =>
|
|
18
|
+
const changed = unique(sources.map((source) => normalizeSourcePath(source)));
|
|
18
19
|
if (changed.some((source) => processFiles.has(basename(source)))) {
|
|
19
20
|
return Object.freeze({
|
|
20
21
|
kind: 'process',
|
|
@@ -32,7 +33,7 @@ export class InvalidationPlanner {
|
|
|
32
33
|
if (basename(source) === 'package.json') {
|
|
33
34
|
addManifestSource(manifestSources, reasons, source);
|
|
34
35
|
}
|
|
35
|
-
const affected = unique([source, ...(this.dependencies?.affectedSources(source) ?? [])].map((item) =>
|
|
36
|
+
const affected = unique([source, ...(this.dependencies?.affectedSources(source) ?? [])].map((item) => normalizeSourcePath(item)));
|
|
36
37
|
let matched = false;
|
|
37
38
|
for (const item of affected) {
|
|
38
39
|
const records = this.ownership.recordsFor(item);
|
|
@@ -95,7 +96,7 @@ export class InvalidationPlanner {
|
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
function addManifestSource(manifestSources, reasons, source) {
|
|
98
|
-
const manifest =
|
|
99
|
+
const manifest = normalizeSourcePath(source);
|
|
99
100
|
manifestSources.add(manifest);
|
|
100
101
|
reasons.add(`Manifest source changed: ${manifest}`);
|
|
101
102
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ModuleDependencyAnalysis {
|
|
2
|
+
readonly dependencies: ReadonlySet<string>;
|
|
3
|
+
readonly commonJsSources: ReadonlySet<string>;
|
|
4
|
+
}
|
|
5
|
+
/** Tracks the local import closure of every module entry loaded by the runtime. */
|
|
6
|
+
export declare class ModuleDependencyIndex {
|
|
7
|
+
#private;
|
|
8
|
+
analyze(entry: string): Promise<ModuleDependencyAnalysis>;
|
|
9
|
+
commit(entry: string, result: ModuleDependencyAnalysis): void;
|
|
10
|
+
/** Marks the changed entry and every entry importing it for lazy re-analysis. */
|
|
11
|
+
invalidate(source: string): void;
|
|
12
|
+
/** Drops entries that no longer belong to the successfully committed generation. */
|
|
13
|
+
retain(entries: readonly string[]): void;
|
|
14
|
+
clear(): void;
|
|
15
|
+
hasCommonJsImpact(source: string): boolean;
|
|
16
|
+
affectedSources(source: string): readonly string[];
|
|
17
|
+
}
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { normalizeSourcePath } from './source-path.js';
|
|
2
|
+
import { access, readFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
import { init, parse } from 'es-module-lexer';
|
|
6
|
+
import { resolve as resolveImport } from 'import-meta-resolve';
|
|
7
|
+
const executableExtensions = Object.freeze([
|
|
8
|
+
'.cjs', '.cts', '.js', '.jsx', '.mjs', '.mts', '.ts', '.tsx',
|
|
9
|
+
]);
|
|
10
|
+
const sourceAlternatives = Object.freeze({
|
|
11
|
+
'.js': Object.freeze(['.ts', '.tsx', '.mts', '.cts', '.js']),
|
|
12
|
+
'.jsx': Object.freeze(['.tsx', '.jsx']),
|
|
13
|
+
'.cjs': Object.freeze(['.cts', '.cjs']),
|
|
14
|
+
'.mjs': Object.freeze(['.mts', '.mjs']),
|
|
15
|
+
});
|
|
16
|
+
/** Tracks the local import closure of every module entry loaded by the runtime. */
|
|
17
|
+
export class ModuleDependencyIndex {
|
|
18
|
+
#dependenciesByEntry = new Map();
|
|
19
|
+
#entriesByDependency = new Map();
|
|
20
|
+
#commonJsSourcesByEntry = new Map();
|
|
21
|
+
#entriesByCommonJsSource = new Map();
|
|
22
|
+
#commonJsByDirectory = new Map();
|
|
23
|
+
#dirtyEntries = new Set();
|
|
24
|
+
async analyze(entry) {
|
|
25
|
+
const normalized = normalizeSourcePath(entry);
|
|
26
|
+
const cached = this.#dependenciesByEntry.get(normalized);
|
|
27
|
+
if (cached && !this.#dirtyEntries.has(normalized)) {
|
|
28
|
+
return analysis(cached, this.#commonJsSourcesByEntry.get(normalized) ?? new Set());
|
|
29
|
+
}
|
|
30
|
+
await init;
|
|
31
|
+
const dependencies = new Set();
|
|
32
|
+
const commonJsSources = new Set();
|
|
33
|
+
const visited = new Set();
|
|
34
|
+
await this.#visit(normalized, normalized, dependencies, commonJsSources, visited);
|
|
35
|
+
return analysis(dependencies, commonJsSources);
|
|
36
|
+
}
|
|
37
|
+
commit(entry, result) {
|
|
38
|
+
const normalized = normalizeSourcePath(entry);
|
|
39
|
+
this.#remove(normalized);
|
|
40
|
+
const dependencies = new Set(result.dependencies);
|
|
41
|
+
const commonJsSources = new Set(result.commonJsSources);
|
|
42
|
+
this.#dependenciesByEntry.set(normalized, dependencies);
|
|
43
|
+
this.#commonJsSourcesByEntry.set(normalized, commonJsSources);
|
|
44
|
+
this.#dirtyEntries.delete(normalized);
|
|
45
|
+
for (const dependency of dependencies) {
|
|
46
|
+
addReverseEntry(this.#entriesByDependency, dependency, normalized);
|
|
47
|
+
}
|
|
48
|
+
for (const source of commonJsSources) {
|
|
49
|
+
addReverseEntry(this.#entriesByCommonJsSource, source, normalized);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Marks the changed entry and every entry importing it for lazy re-analysis. */
|
|
53
|
+
invalidate(source) {
|
|
54
|
+
const normalized = normalizeSourcePath(source);
|
|
55
|
+
if (basename(normalized) === 'package.json') {
|
|
56
|
+
this.#commonJsByDirectory.clear();
|
|
57
|
+
const packageRoot = dirname(normalized);
|
|
58
|
+
for (const [entry, dependencies] of this.#dependenciesByEntry) {
|
|
59
|
+
if (isWithin(packageRoot, entry)
|
|
60
|
+
|| [...dependencies].some((dependency) => isWithin(packageRoot, dependency))) {
|
|
61
|
+
this.#dirtyEntries.add(entry);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (this.#dependenciesByEntry.has(normalized))
|
|
66
|
+
this.#dirtyEntries.add(normalized);
|
|
67
|
+
for (const entry of this.#entriesByDependency.get(normalized) ?? []) {
|
|
68
|
+
this.#dirtyEntries.add(entry);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Drops entries that no longer belong to the successfully committed generation. */
|
|
72
|
+
retain(entries) {
|
|
73
|
+
const active = new Set(entries.map((entry) => normalizeSourcePath(entry)));
|
|
74
|
+
for (const entry of this.#dependenciesByEntry.keys()) {
|
|
75
|
+
if (!active.has(entry))
|
|
76
|
+
this.#remove(entry);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
clear() {
|
|
80
|
+
this.#dependenciesByEntry.clear();
|
|
81
|
+
this.#entriesByDependency.clear();
|
|
82
|
+
this.#commonJsSourcesByEntry.clear();
|
|
83
|
+
this.#entriesByCommonJsSource.clear();
|
|
84
|
+
this.#commonJsByDirectory.clear();
|
|
85
|
+
this.#dirtyEntries.clear();
|
|
86
|
+
}
|
|
87
|
+
hasCommonJsImpact(source) {
|
|
88
|
+
const normalized = normalizeSourcePath(source);
|
|
89
|
+
if (isExplicitCommonJsSource(normalized))
|
|
90
|
+
return true;
|
|
91
|
+
if (this.#entriesByCommonJsSource.has(normalized))
|
|
92
|
+
return true;
|
|
93
|
+
return [...(this.#entriesByDependency.get(normalized) ?? [])].some((entry) => this.#commonJsSourcesByEntry.get(entry)?.has(entry) ?? false);
|
|
94
|
+
}
|
|
95
|
+
affectedSources(source) {
|
|
96
|
+
const normalized = normalizeSourcePath(source);
|
|
97
|
+
const entries = [...(this.#entriesByDependency.get(normalized) ?? [])].sort();
|
|
98
|
+
return Object.freeze([normalized, ...entries.filter((entry) => entry !== normalized)]);
|
|
99
|
+
}
|
|
100
|
+
#remove(entry) {
|
|
101
|
+
for (const dependency of this.#dependenciesByEntry.get(entry) ?? []) {
|
|
102
|
+
removeReverseEntry(this.#entriesByDependency, dependency, entry);
|
|
103
|
+
}
|
|
104
|
+
for (const source of this.#commonJsSourcesByEntry.get(entry) ?? []) {
|
|
105
|
+
removeReverseEntry(this.#entriesByCommonJsSource, source, entry);
|
|
106
|
+
}
|
|
107
|
+
this.#dependenciesByEntry.delete(entry);
|
|
108
|
+
this.#commonJsSourcesByEntry.delete(entry);
|
|
109
|
+
this.#dirtyEntries.delete(entry);
|
|
110
|
+
}
|
|
111
|
+
async #visit(entry, source, dependencies, commonJsSources, visited) {
|
|
112
|
+
if (visited.has(source))
|
|
113
|
+
return;
|
|
114
|
+
visited.add(source);
|
|
115
|
+
if (source !== entry)
|
|
116
|
+
dependencies.add(source);
|
|
117
|
+
if (!executableExtensions.includes(extname(source)))
|
|
118
|
+
return;
|
|
119
|
+
let text;
|
|
120
|
+
try {
|
|
121
|
+
text = await readFile(source, 'utf8');
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const commonJs = await this.#isCommonJsSource(source);
|
|
127
|
+
if (commonJs)
|
|
128
|
+
commonJsSources.add(source);
|
|
129
|
+
const specifiers = commonJs ? commonJsSpecifiers(text) : [];
|
|
130
|
+
try {
|
|
131
|
+
const [imports] = parse(text, source);
|
|
132
|
+
specifiers.push(...imports.flatMap((imported) => imported.n ? [imported.n] : []));
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Import analysis is an HMR optimization. Module loading remains the
|
|
136
|
+
// authority for syntax diagnostics and must not be hidden by the index.
|
|
137
|
+
// CommonJS requires were already collected by the tolerant lexer above.
|
|
138
|
+
if (!commonJs)
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
for (const specifier of specifiers) {
|
|
142
|
+
const dependency = await resolveLocalImport(source, specifier);
|
|
143
|
+
if (dependency) {
|
|
144
|
+
await this.#visit(entry, dependency, dependencies, commonJsSources, visited);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
#isCommonJsSource(source) {
|
|
149
|
+
if (isExplicitCommonJsSource(source))
|
|
150
|
+
return Promise.resolve(true);
|
|
151
|
+
if (['.mjs', '.mts'].includes(extname(source)))
|
|
152
|
+
return Promise.resolve(false);
|
|
153
|
+
const directory = dirname(source);
|
|
154
|
+
const cached = this.#commonJsByDirectory.get(directory);
|
|
155
|
+
if (cached)
|
|
156
|
+
return cached;
|
|
157
|
+
const detected = findCommonJsPackage(directory);
|
|
158
|
+
this.#commonJsByDirectory.set(directory, detected);
|
|
159
|
+
return detected;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function analysis(dependencies, commonJsSources) {
|
|
163
|
+
return Object.freeze({ dependencies, commonJsSources });
|
|
164
|
+
}
|
|
165
|
+
function addReverseEntry(index, source, entry) {
|
|
166
|
+
const entries = index.get(source) ?? new Set();
|
|
167
|
+
entries.add(entry);
|
|
168
|
+
index.set(source, entries);
|
|
169
|
+
}
|
|
170
|
+
function removeReverseEntry(index, source, entry) {
|
|
171
|
+
const entries = index.get(source);
|
|
172
|
+
entries?.delete(entry);
|
|
173
|
+
if (entries?.size === 0)
|
|
174
|
+
index.delete(source);
|
|
175
|
+
}
|
|
176
|
+
/** Collects literal require()/require.resolve() calls outside comments and literal text. */
|
|
177
|
+
function commonJsSpecifiers(source) {
|
|
178
|
+
const specifiers = [];
|
|
179
|
+
let index = 0;
|
|
180
|
+
while (index < source.length) {
|
|
181
|
+
const character = source[index];
|
|
182
|
+
if (character === '"' || character === "'") {
|
|
183
|
+
index = skipQuoted(source, index, character);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (character === '`') {
|
|
187
|
+
index = skipTemplate(source, index);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (character === '/' && source[index + 1] === '/') {
|
|
191
|
+
index = skipLineComment(source, index + 2);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (character === '/' && source[index + 1] === '*') {
|
|
195
|
+
index = skipBlockComment(source, index + 2);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (!isIdentifierStart(character)) {
|
|
199
|
+
index += 1;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const start = index;
|
|
203
|
+
index = skipIdentifier(source, index + 1);
|
|
204
|
+
if (source.slice(start, index) !== 'require' || previousSignificant(source, start) === '.') {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const call = readRequireCall(source, index);
|
|
208
|
+
if (!call)
|
|
209
|
+
continue;
|
|
210
|
+
specifiers.push(call.specifier);
|
|
211
|
+
index = call.end;
|
|
212
|
+
}
|
|
213
|
+
return specifiers;
|
|
214
|
+
}
|
|
215
|
+
function readRequireCall(source, start) {
|
|
216
|
+
let index = skipTrivia(source, start);
|
|
217
|
+
if (source[index] === '.') {
|
|
218
|
+
index = skipTrivia(source, index + 1);
|
|
219
|
+
const identifierStart = index;
|
|
220
|
+
index = skipIdentifier(source, index);
|
|
221
|
+
if (source.slice(identifierStart, index) !== 'resolve')
|
|
222
|
+
return undefined;
|
|
223
|
+
index = skipTrivia(source, index);
|
|
224
|
+
}
|
|
225
|
+
if (source[index] !== '(')
|
|
226
|
+
return undefined;
|
|
227
|
+
index = skipTrivia(source, index + 1);
|
|
228
|
+
const quote = source[index];
|
|
229
|
+
if (quote !== '"' && quote !== "'")
|
|
230
|
+
return undefined;
|
|
231
|
+
const literal = readStringLiteral(source, index, quote);
|
|
232
|
+
if (!literal)
|
|
233
|
+
return undefined;
|
|
234
|
+
index = skipTrivia(source, literal.end);
|
|
235
|
+
if (source[index] !== ')')
|
|
236
|
+
return undefined;
|
|
237
|
+
return { specifier: literal.value, end: index + 1 };
|
|
238
|
+
}
|
|
239
|
+
function readStringLiteral(source, start, quote) {
|
|
240
|
+
let value = '';
|
|
241
|
+
let index = start + 1;
|
|
242
|
+
while (index < source.length) {
|
|
243
|
+
const character = source[index];
|
|
244
|
+
if (character === quote)
|
|
245
|
+
return { value, end: index + 1 };
|
|
246
|
+
if (character === '\\') {
|
|
247
|
+
const escaped = source[index + 1];
|
|
248
|
+
if (!escaped || escaped === '\n' || escaped === '\r')
|
|
249
|
+
return undefined;
|
|
250
|
+
value += escaped;
|
|
251
|
+
index += 2;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (character === '\n' || character === '\r')
|
|
255
|
+
return undefined;
|
|
256
|
+
value += character;
|
|
257
|
+
index += 1;
|
|
258
|
+
}
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
function skipTrivia(source, start) {
|
|
262
|
+
let index = start;
|
|
263
|
+
while (index < source.length) {
|
|
264
|
+
if (/\s/u.test(source[index] ?? '')) {
|
|
265
|
+
index += 1;
|
|
266
|
+
}
|
|
267
|
+
else if (source[index] === '/' && source[index + 1] === '/') {
|
|
268
|
+
index = skipLineComment(source, index + 2);
|
|
269
|
+
}
|
|
270
|
+
else if (source[index] === '/' && source[index + 1] === '*') {
|
|
271
|
+
index = skipBlockComment(source, index + 2);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return index;
|
|
278
|
+
}
|
|
279
|
+
function skipQuoted(source, start, quote) {
|
|
280
|
+
let index = start + 1;
|
|
281
|
+
while (index < source.length) {
|
|
282
|
+
if (source[index] === '\\')
|
|
283
|
+
index += 2;
|
|
284
|
+
else if (source[index] === quote)
|
|
285
|
+
return index + 1;
|
|
286
|
+
else
|
|
287
|
+
index += 1;
|
|
288
|
+
}
|
|
289
|
+
return source.length;
|
|
290
|
+
}
|
|
291
|
+
function skipTemplate(source, start) {
|
|
292
|
+
let index = start + 1;
|
|
293
|
+
while (index < source.length) {
|
|
294
|
+
if (source[index] === '\\')
|
|
295
|
+
index += 2;
|
|
296
|
+
else if (source[index] === '`')
|
|
297
|
+
return index + 1;
|
|
298
|
+
else
|
|
299
|
+
index += 1;
|
|
300
|
+
}
|
|
301
|
+
return source.length;
|
|
302
|
+
}
|
|
303
|
+
function skipLineComment(source, start) {
|
|
304
|
+
const end = source.indexOf('\n', start);
|
|
305
|
+
return end < 0 ? source.length : end + 1;
|
|
306
|
+
}
|
|
307
|
+
function skipBlockComment(source, start) {
|
|
308
|
+
const end = source.indexOf('*/', start);
|
|
309
|
+
return end < 0 ? source.length : end + 2;
|
|
310
|
+
}
|
|
311
|
+
function isIdentifierStart(character) {
|
|
312
|
+
return character !== undefined && /[A-Za-z_$]/u.test(character);
|
|
313
|
+
}
|
|
314
|
+
function skipIdentifier(source, start) {
|
|
315
|
+
let index = start;
|
|
316
|
+
while (/[\w$]/u.test(source[index] ?? ''))
|
|
317
|
+
index += 1;
|
|
318
|
+
return index;
|
|
319
|
+
}
|
|
320
|
+
function previousSignificant(source, start) {
|
|
321
|
+
let index = start - 1;
|
|
322
|
+
while (index >= 0 && /\s/u.test(source[index] ?? ''))
|
|
323
|
+
index -= 1;
|
|
324
|
+
return source[index];
|
|
325
|
+
}
|
|
326
|
+
function isExplicitCommonJsSource(source) {
|
|
327
|
+
return ['.cjs', '.cts'].includes(extname(source));
|
|
328
|
+
}
|
|
329
|
+
async function findCommonJsPackage(start) {
|
|
330
|
+
let directory = start;
|
|
331
|
+
while (true) {
|
|
332
|
+
try {
|
|
333
|
+
const manifest = JSON.parse(await readFile(join(directory, 'package.json'), 'utf8'));
|
|
334
|
+
return manifest.type !== 'module';
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
const parent = dirname(directory);
|
|
338
|
+
if (parent === directory)
|
|
339
|
+
return true;
|
|
340
|
+
directory = parent;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
async function resolveLocalImport(importer, specifier) {
|
|
345
|
+
let requested;
|
|
346
|
+
try {
|
|
347
|
+
if (specifier.startsWith('file:'))
|
|
348
|
+
requested = fileURLToPath(specifier);
|
|
349
|
+
else if (isAbsolute(specifier))
|
|
350
|
+
requested = specifier;
|
|
351
|
+
else if (specifier.startsWith('.'))
|
|
352
|
+
requested = resolve(dirname(importer), specifier);
|
|
353
|
+
else if (specifier.startsWith('#')) {
|
|
354
|
+
const resolvedImport = resolveImport(specifier, pathToFileURL(importer).href);
|
|
355
|
+
if (resolvedImport.startsWith('node:'))
|
|
356
|
+
return undefined;
|
|
357
|
+
requested = fileURLToPath(resolvedImport);
|
|
358
|
+
}
|
|
359
|
+
else
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
for (const candidate of importCandidates(requested)) {
|
|
366
|
+
if (await exists(candidate))
|
|
367
|
+
return normalizeSourcePath(candidate);
|
|
368
|
+
}
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
function importCandidates(requested) {
|
|
372
|
+
const extension = extname(requested);
|
|
373
|
+
if (extension) {
|
|
374
|
+
const alternatives = sourceAlternatives[extension];
|
|
375
|
+
if (!alternatives)
|
|
376
|
+
return [requested];
|
|
377
|
+
const stem = requested.slice(0, -extension.length);
|
|
378
|
+
return [
|
|
379
|
+
requested,
|
|
380
|
+
...alternatives
|
|
381
|
+
.filter((candidate) => candidate !== extension)
|
|
382
|
+
.map((candidate) => `${stem}${candidate}`),
|
|
383
|
+
];
|
|
384
|
+
}
|
|
385
|
+
return [
|
|
386
|
+
requested,
|
|
387
|
+
...executableExtensions.map((candidate) => `${requested}${candidate}`),
|
|
388
|
+
...executableExtensions.map((candidate) => resolve(requested, `index${candidate}`)),
|
|
389
|
+
];
|
|
390
|
+
}
|
|
391
|
+
function isWithin(root, source) {
|
|
392
|
+
const child = relative(root, source);
|
|
393
|
+
return child === '' || (!child.startsWith('..') && !isAbsolute(child));
|
|
394
|
+
}
|
|
395
|
+
async function exists(source) {
|
|
396
|
+
try {
|
|
397
|
+
await access(source);
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
}
|
package/lib/module-runtime.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ export interface ModuleRuntime {
|
|
|
12
12
|
affectedSources?(source: string): readonly string[];
|
|
13
13
|
/** True when this adapter cannot invalidate the complete importer closure safely. */
|
|
14
14
|
requiresProcessRestart?(source: string): boolean;
|
|
15
|
+
/** Starts staging module metadata for a generation transaction. */
|
|
16
|
+
beginGeneration?(): void;
|
|
17
|
+
/** Commits staged metadata and the active module set after the generation commits. */
|
|
18
|
+
commitGeneration?(sources: readonly string[]): void;
|
|
19
|
+
/** Discards metadata staged by a failed generation transaction. */
|
|
20
|
+
rollbackGeneration?(): void;
|
|
15
21
|
/** Replaces the local package roots after a generation commits. */
|
|
16
22
|
updateWatchRoots?(roots: readonly ModuleWatchRoot[]): void;
|
|
17
23
|
watch?(listener: (source: string) => void): Dispose;
|
|
@@ -11,16 +11,19 @@ export interface TsxModuleLoader {
|
|
|
11
11
|
close?(): Promise<void>;
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
* Uses Node's native ESM/TypeScript loader
|
|
15
|
-
*
|
|
16
|
-
* relative import closure cannot be invalidated without a custom loader.
|
|
14
|
+
* Uses Node's native ESM/TypeScript loader with dependency-aware cache busting
|
|
15
|
+
* and portable filesystem watch support.
|
|
17
16
|
*/
|
|
18
17
|
export declare class NativeDevelopmentModuleRuntime implements ModuleRuntime {
|
|
19
18
|
#private;
|
|
20
19
|
constructor(options: NativeDevelopmentModuleRuntimeOptions);
|
|
21
20
|
load<T = unknown>(source: string): Promise<T>;
|
|
22
21
|
invalidate(source: string): void;
|
|
22
|
+
affectedSources(source: string): readonly string[];
|
|
23
23
|
requiresProcessRestart(source: string): boolean;
|
|
24
|
+
beginGeneration(): void;
|
|
25
|
+
commitGeneration(sources: readonly string[]): void;
|
|
26
|
+
rollbackGeneration(): void;
|
|
24
27
|
updateWatchRoots(roots: readonly ModuleWatchRoot[]): void;
|
|
25
28
|
watch(listener: (source: string) => void): Dispose;
|
|
26
29
|
close(): Promise<void>;
|
|
@@ -1,57 +1,74 @@
|
|
|
1
|
+
import { normalizeSourcePath } from './source-path.js';
|
|
1
2
|
import { readdirSync, statSync, watch as watchDirectory, } from 'node:fs';
|
|
2
3
|
import { basename, extname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
4
|
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { ModuleDependencyIndex, } from './module-dependency-index.js';
|
|
6
|
+
import { ensureTypeScriptSpecifierRemap } from './typescript-specifier-remap.js';
|
|
4
7
|
const ignoredDirectories = new Set([
|
|
5
8
|
'.git', '.zhin', 'coverage', 'data', 'dist', 'lib', 'node_modules',
|
|
6
9
|
]);
|
|
7
10
|
const watchedExtensions = new Set([
|
|
8
|
-
'.cjs', '.
|
|
11
|
+
'.cjs', '.cts', '.js', '.json', '.jsx', '.md', '.mjs', '.mts',
|
|
12
|
+
'.ts', '.tsx', '.yaml', '.yml',
|
|
9
13
|
]);
|
|
10
14
|
const capabilityRoots = new Set([
|
|
11
15
|
'adapters', 'agents', 'commands', 'components', 'handlers', 'hooks', 'mcps', 'middlewares', 'pages', 'prompt-sections', 'schedules', 'skills', 'tools',
|
|
12
16
|
]);
|
|
13
17
|
/**
|
|
14
|
-
* Uses Node's native ESM/TypeScript loader
|
|
15
|
-
*
|
|
16
|
-
* relative import closure cannot be invalidated without a custom loader.
|
|
18
|
+
* Uses Node's native ESM/TypeScript loader with dependency-aware cache busting
|
|
19
|
+
* and portable filesystem watch support.
|
|
17
20
|
*/
|
|
18
21
|
export class NativeDevelopmentModuleRuntime {
|
|
19
22
|
#projectRoot;
|
|
20
23
|
#watchEnabled;
|
|
21
24
|
#tsxLoader;
|
|
22
|
-
#
|
|
25
|
+
#dependencies = new ModuleDependencyIndex();
|
|
23
26
|
#watchers = new Set();
|
|
27
|
+
#pendingDependencies;
|
|
24
28
|
#watchRoots;
|
|
29
|
+
#revision = 0;
|
|
25
30
|
#closed = false;
|
|
26
31
|
constructor(options) {
|
|
27
|
-
|
|
32
|
+
ensureTypeScriptSpecifierRemap();
|
|
33
|
+
this.#projectRoot = normalizeSourcePath(options.projectRoot);
|
|
28
34
|
this.#watchEnabled = options.watch ?? true;
|
|
29
35
|
this.#tsxLoader = options.tsxLoader;
|
|
30
36
|
this.#watchRoots = normalizeWatchRoots([this.#projectRoot]);
|
|
31
37
|
}
|
|
32
38
|
async load(source) {
|
|
33
39
|
this.#assertOpen();
|
|
34
|
-
const normalized =
|
|
35
|
-
if (normalized
|
|
40
|
+
const normalized = normalizeSourcePath(source);
|
|
41
|
+
if (isNativeTypeScriptSource(normalized))
|
|
36
42
|
assertNativeTypeScriptSupport();
|
|
43
|
+
const analysis = this.#pendingDependencies?.get(normalized)
|
|
44
|
+
?? await this.#dependencies.analyze(normalized);
|
|
37
45
|
const url = pathToFileURL(normalized);
|
|
38
|
-
url.searchParams.set('zhin-generation', String(this.#
|
|
46
|
+
url.searchParams.set('zhin-generation', String(this.#revision));
|
|
47
|
+
let loaded;
|
|
39
48
|
try {
|
|
40
|
-
|
|
49
|
+
loaded = await import(url.href);
|
|
41
50
|
}
|
|
42
51
|
catch (error) {
|
|
43
52
|
if (!normalized.endsWith('.tsx') || !isUnsupportedTsxError(error) || !this.#tsxLoader) {
|
|
44
53
|
throw error;
|
|
45
54
|
}
|
|
46
|
-
|
|
55
|
+
loaded = await this.#tsxLoader.load(url.href, pathToFileURL(`${this.#projectRoot}${sep}`).href);
|
|
47
56
|
}
|
|
57
|
+
if (this.#pendingDependencies)
|
|
58
|
+
this.#pendingDependencies.set(normalized, analysis);
|
|
59
|
+
else
|
|
60
|
+
this.#dependencies.commit(normalized, analysis);
|
|
61
|
+
return loaded;
|
|
48
62
|
}
|
|
49
63
|
invalidate(source) {
|
|
50
|
-
|
|
51
|
-
this.#
|
|
64
|
+
this.#dependencies.invalidate(source);
|
|
65
|
+
this.#revision += 1;
|
|
66
|
+
}
|
|
67
|
+
affectedSources(source) {
|
|
68
|
+
return this.#dependencies.affectedSources(source);
|
|
52
69
|
}
|
|
53
70
|
requiresProcessRestart(source) {
|
|
54
|
-
const normalized =
|
|
71
|
+
const normalized = normalizeSourcePath(source);
|
|
55
72
|
const packageRoot = nearestWatchRoot(this.#watchRoots, normalized);
|
|
56
73
|
// Installed packages and external paths are intentionally not watched.
|
|
57
74
|
// The HMR coordinator turns this into a visible process restart reason.
|
|
@@ -59,6 +76,16 @@ export class NativeDevelopmentModuleRuntime {
|
|
|
59
76
|
return true;
|
|
60
77
|
if (packageRoot === this.#projectRoot && basename(normalized).startsWith('.env'))
|
|
61
78
|
return true;
|
|
79
|
+
// Native ESM query revisions do not invalidate CommonJS require.cache.
|
|
80
|
+
// Keep .cjs/.cts entries and their statically discovered closure on the
|
|
81
|
+
// process boundary until the module runtime owns a CJS cache transaction.
|
|
82
|
+
if (this.#dependencies.hasCommonJsImpact(normalized))
|
|
83
|
+
return true;
|
|
84
|
+
// A helper with one or more loaded entry importers is safe: the planner
|
|
85
|
+
// invalidates those owned entries and the generation query refreshes their
|
|
86
|
+
// complete project-local import closure.
|
|
87
|
+
if (this.affectedSources(normalized).length > 1)
|
|
88
|
+
return false;
|
|
62
89
|
const parts = relative(packageRoot, normalized).split(sep);
|
|
63
90
|
const capability = parts.findIndex((part) => capabilityRoots.has(part));
|
|
64
91
|
if (capability < 0)
|
|
@@ -90,7 +117,26 @@ export class NativeDevelopmentModuleRuntime {
|
|
|
90
117
|
// Support files inside capability directories (e.g. commands/_utils.ts)
|
|
91
118
|
// are not discovery entries: reloading the entry URL only bumps that
|
|
92
119
|
// entry's zhin-generation, so the importer closure keeps the old code.
|
|
93
|
-
return ['.js', '.json', '.ts', '.tsx']
|
|
120
|
+
return ['.cjs', '.cts', '.js', '.json', '.mjs', '.mts', '.ts', '.tsx']
|
|
121
|
+
.includes(extname(normalized));
|
|
122
|
+
}
|
|
123
|
+
beginGeneration() {
|
|
124
|
+
this.#assertOpen();
|
|
125
|
+
if (this.#pendingDependencies) {
|
|
126
|
+
throw new Error('A module generation transaction is already active');
|
|
127
|
+
}
|
|
128
|
+
this.#pendingDependencies = new Map();
|
|
129
|
+
}
|
|
130
|
+
commitGeneration(sources) {
|
|
131
|
+
this.#assertOpen();
|
|
132
|
+
for (const [entry, analysis] of this.#pendingDependencies ?? []) {
|
|
133
|
+
this.#dependencies.commit(entry, analysis);
|
|
134
|
+
}
|
|
135
|
+
this.#pendingDependencies = undefined;
|
|
136
|
+
this.#dependencies.retain(sources);
|
|
137
|
+
}
|
|
138
|
+
rollbackGeneration() {
|
|
139
|
+
this.#pendingDependencies = undefined;
|
|
94
140
|
}
|
|
95
141
|
updateWatchRoots(roots) {
|
|
96
142
|
this.#assertOpen();
|
|
@@ -122,6 +168,8 @@ export class NativeDevelopmentModuleRuntime {
|
|
|
122
168
|
for (const watcher of this.#watchers)
|
|
123
169
|
watcher.close();
|
|
124
170
|
this.#watchers.clear();
|
|
171
|
+
this.#pendingDependencies = undefined;
|
|
172
|
+
this.#dependencies.clear();
|
|
125
173
|
await this.#tsxLoader?.close?.();
|
|
126
174
|
}
|
|
127
175
|
#assertOpen() {
|
|
@@ -142,7 +190,7 @@ function isNamedCapabilityDirectory(value, allowSnake) {
|
|
|
142
190
|
function isDirectoryCapabilityEntry(parts) {
|
|
143
191
|
return parts.length === 2
|
|
144
192
|
&& parts[1] === `index${extname(parts[1] ?? '')}`
|
|
145
|
-
&& ['.cjs', '.js', '.mjs', '.ts', '.tsx'].includes(extname(parts[1] ?? ''));
|
|
193
|
+
&& ['.cjs', '.cts', '.js', '.mjs', '.mts', '.ts', '.tsx'].includes(extname(parts[1] ?? ''));
|
|
146
194
|
}
|
|
147
195
|
function isNestedDirectoryEntry(parts) {
|
|
148
196
|
const index = parts.indexOf('tools');
|
|
@@ -290,7 +338,10 @@ function isIgnoredSource(root, source) {
|
|
|
290
338
|
return relative(root, source).split(sep).some((segment) => ignoredDirectories.has(segment));
|
|
291
339
|
}
|
|
292
340
|
function isExecutableSource(source) {
|
|
293
|
-
return ['.cjs', '.js', '.mjs', '.ts', '.tsx'].includes(extname(source));
|
|
341
|
+
return ['.cjs', '.cts', '.js', '.mjs', '.mts', '.ts', '.tsx'].includes(extname(source));
|
|
342
|
+
}
|
|
343
|
+
function isNativeTypeScriptSource(source) {
|
|
344
|
+
return ['.cts', '.mts', '.ts'].includes(extname(source));
|
|
294
345
|
}
|
|
295
346
|
function isWithin(root, source) {
|
|
296
347
|
const child = relative(root, source);
|
|
@@ -303,7 +354,7 @@ function isNodeModulesSource(root, source) {
|
|
|
303
354
|
return relative(root, source).split(sep).some((part) => part === 'node_modules');
|
|
304
355
|
}
|
|
305
356
|
function normalizeWatchRoots(roots) {
|
|
306
|
-
const sorted = [...new Set(roots.map((root) =>
|
|
357
|
+
const sorted = [...new Set(roots.map((root) => normalizeSourcePath(root)))].sort((left, right) => left.length - right.length);
|
|
307
358
|
return Object.freeze(sorted.filter((root, index) => !sorted.slice(0, index).some((parent) => isWithin(parent, root))));
|
|
308
359
|
}
|
|
309
360
|
function sameRoots(left, right) {
|