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