@typed/virtual-modules 1.0.0-beta.1 → 1.0.0-beta.2
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/CompilerHostAdapter.d.ts.map +1 -1
- package/dist/CompilerHostAdapter.js +4 -2
- package/dist/LanguageServiceAdapter.d.ts.map +1 -1
- package/dist/LanguageServiceAdapter.js +16 -16
- package/dist/internal/VirtualRecordStore.d.ts +1 -0
- package/dist/internal/VirtualRecordStore.d.ts.map +1 -1
- package/dist/internal/VirtualRecordStore.js +21 -0
- package/package.json +12 -12
- package/src/CompilerHostAdapter.test.ts +54 -0
- package/src/CompilerHostAdapter.ts +4 -2
- package/src/LanguageServiceAdapter.test.ts +171 -0
- package/src/LanguageServiceAdapter.ts +14 -19
- package/src/internal/VirtualRecordStore.ts +20 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CompilerHostAdapter.d.ts","sourceRoot":"","sources":["../src/CompilerHostAdapter.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,0BAA0B,EAC/B,KAAK,0BAA0B,EAEhC,MAAM,YAAY,CAAC;AASpB,eAAO,MAAM,yBAAyB,GACpC,SAAS,0BAA0B,KAClC,
|
|
1
|
+
{"version":3,"file":"CompilerHostAdapter.d.ts","sourceRoot":"","sources":["../src/CompilerHostAdapter.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,0BAA0B,EAC/B,KAAK,0BAA0B,EAEhC,MAAM,YAAY,CAAC;AASpB,eAAO,MAAM,yBAAyB,GACpC,SAAS,0BAA0B,KAClC,0BA6SF,CAAC"}
|
|
@@ -71,8 +71,9 @@ export const attachCompilerHostAdapter = (options) => {
|
|
|
71
71
|
const fallback = originalResolveModuleNames
|
|
72
72
|
? originalResolveModuleNames(moduleNames, containingFile, reusedNames, redirectedReference, compilerOptions, containingSourceFile)
|
|
73
73
|
: moduleNames.map((moduleName) => fallbackResolveModule(moduleName, containingFile, compilerOptions));
|
|
74
|
+
const effectiveImporter = store.resolveEffectiveImporter(containingFile);
|
|
74
75
|
return moduleNames.map((moduleName, index) => {
|
|
75
|
-
const record = getOrBuildRecord(moduleName,
|
|
76
|
+
const record = getOrBuildRecord(moduleName, effectiveImporter);
|
|
76
77
|
if (!record) {
|
|
77
78
|
return fallback[index];
|
|
78
79
|
}
|
|
@@ -85,8 +86,9 @@ export const attachCompilerHostAdapter = (options) => {
|
|
|
85
86
|
: moduleLiterals.map((moduleLiteral) => ({
|
|
86
87
|
resolvedModule: fallbackResolveModule(moduleLiteral.text, containingFile, compilerOptions),
|
|
87
88
|
}));
|
|
89
|
+
const effectiveImporter = store.resolveEffectiveImporter(containingFile);
|
|
88
90
|
return moduleLiterals.map((moduleLiteral, index) => {
|
|
89
|
-
const record = getOrBuildRecord(moduleLiteral.text,
|
|
91
|
+
const record = getOrBuildRecord(moduleLiteral.text, effectiveImporter);
|
|
90
92
|
if (!record) {
|
|
91
93
|
return fallback[index];
|
|
92
94
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LanguageServiceAdapter.d.ts","sourceRoot":"","sources":["../src/LanguageServiceAdapter.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,6BAA6B,EAElC,KAAK,0BAA0B,EAChC,MAAM,YAAY,CAAC;AAiGpB,eAAO,MAAM,4BAA4B,GACvC,SAAS,6BAA6B,KACrC,
|
|
1
|
+
{"version":3,"file":"LanguageServiceAdapter.d.ts","sourceRoot":"","sources":["../src/LanguageServiceAdapter.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,6BAA6B,EAElC,KAAK,0BAA0B,EAChC,MAAM,YAAY,CAAC;AAiGpB,eAAO,MAAM,4BAA4B,GACvC,SAAS,6BAA6B,KACrC,0BAugBF,CAAC"}
|
|
@@ -201,7 +201,7 @@ export const attachLanguageServiceAdapter = (options) => {
|
|
|
201
201
|
const r = recordsByVirtualFile.get(parsed.virtualPath);
|
|
202
202
|
if (r) {
|
|
203
203
|
effectiveContainingFile = r.virtualFileName;
|
|
204
|
-
importerForVirtual =
|
|
204
|
+
importerForVirtual = store.resolveEffectiveImporter(parsed.virtualPath);
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
else {
|
|
@@ -212,6 +212,13 @@ export const attachLanguageServiceAdapter = (options) => {
|
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
|
+
else {
|
|
216
|
+
const virtualRecord = recordsByVirtualFile.get(containingFile);
|
|
217
|
+
if (virtualRecord) {
|
|
218
|
+
effectiveContainingFile = virtualRecord.virtualFileName;
|
|
219
|
+
importerForVirtual = store.resolveEffectiveImporter(containingFile);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
215
222
|
const fallback = originalResolveModuleNames
|
|
216
223
|
? originalResolveModuleNames(moduleNames, effectiveContainingFile, reusedNames, redirectedReference, compilerOptions, containingSourceFile)
|
|
217
224
|
: moduleNames.map((moduleName) => fallbackResolveModule(moduleName, effectiveContainingFile, compilerOptions));
|
|
@@ -265,7 +272,7 @@ export const attachLanguageServiceAdapter = (options) => {
|
|
|
265
272
|
const r = recordsByVirtualFile.get(parsed.virtualPath);
|
|
266
273
|
if (r) {
|
|
267
274
|
effectiveContainingFile = r.virtualFileName;
|
|
268
|
-
importerForVirtual =
|
|
275
|
+
importerForVirtual = store.resolveEffectiveImporter(parsed.virtualPath);
|
|
269
276
|
}
|
|
270
277
|
}
|
|
271
278
|
else {
|
|
@@ -276,6 +283,13 @@ export const attachLanguageServiceAdapter = (options) => {
|
|
|
276
283
|
}
|
|
277
284
|
}
|
|
278
285
|
}
|
|
286
|
+
else {
|
|
287
|
+
const virtualRecord = recordsByVirtualFile.get(containingFile);
|
|
288
|
+
if (virtualRecord) {
|
|
289
|
+
effectiveContainingFile = virtualRecord.virtualFileName;
|
|
290
|
+
importerForVirtual = store.resolveEffectiveImporter(containingFile);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
279
293
|
const fallback = originalResolveModuleNameLiterals
|
|
280
294
|
? originalResolveModuleNameLiterals(moduleLiterals, effectiveContainingFile, redirectedReference, compilerOptions, containingSourceFile, reusedNames)
|
|
281
295
|
: moduleLiterals.map((moduleLiteral) => ({
|
|
@@ -285,20 +299,6 @@ export const attachLanguageServiceAdapter = (options) => {
|
|
|
285
299
|
let hadUnresolvedVirtual = false;
|
|
286
300
|
const results = moduleLiterals.map((moduleLiteral, index) => {
|
|
287
301
|
const resolved = getOrBuildRecord(moduleLiteral.text, importerForVirtual);
|
|
288
|
-
if (moduleLiteral.text.includes(":")) {
|
|
289
|
-
try {
|
|
290
|
-
require("node:fs").appendFileSync("/tmp/vm-ts-plugin-debug.log", JSON.stringify({
|
|
291
|
-
tag: "LS:resolveLiterals",
|
|
292
|
-
id: moduleLiteral.text,
|
|
293
|
-
status: resolved.status,
|
|
294
|
-
err: resolved.status === "error" ? resolved.diagnostic?.message : undefined,
|
|
295
|
-
t: Date.now(),
|
|
296
|
-
}) + "\n", { flag: "a" });
|
|
297
|
-
}
|
|
298
|
-
catch {
|
|
299
|
-
/* noop */
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
302
|
if (resolved.status === "resolved") {
|
|
303
303
|
pendingRetry = false;
|
|
304
304
|
return {
|
|
@@ -55,6 +55,7 @@ export declare function createVirtualRecordStore(options: VirtualRecordStoreOpti
|
|
|
55
55
|
flushPendingStale: () => void;
|
|
56
56
|
resolveRecord: (id: string, importer: string, previous?: MutableVirtualRecord) => ResolveRecordResult;
|
|
57
57
|
getOrBuildRecord: (id: string, importer: string) => ResolveRecordResult;
|
|
58
|
+
resolveEffectiveImporter: (containingFile: string) => string;
|
|
58
59
|
dispose: () => void;
|
|
59
60
|
};
|
|
60
61
|
//# sourceMappingURL=VirtualRecordStore.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VirtualRecordStore.d.ts","sourceRoot":"","sources":["../../src/internal/VirtualRecordStore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,KAAK,EACV,2BAA2B,EAC3B,uBAAuB,EACvB,mBAAmB,EACnB,uBAAuB,EACxB,MAAM,aAAa,CAAC;AAGrB,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,mBAAmB,EAAE,SAAS,GAAG,OAAO,CAAC,GAAG;IAClF,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;CACvC;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;CAC/B;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,uBAAuB,CAAC;CAC9C;AAED,MAAM,MAAM,mBAAmB,GAC3B,2BAA2B,GAC3B,6BAA6B,GAC7B,wBAAwB,CAAC;AAE7B,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE;QACjB,aAAa,CAAC,OAAO,EAAE,2BAA2B,GAAG,uBAAuB,CAAC;KAC9E,CAAC;IACF,QAAQ,CAAC,wBAAwB,CAAC,EAAE,2BAA2B,CAAC,0BAA0B,CAAC,CAAC;IAC5F,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE;QACnB,SAAS,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,EAAE,CAAC,WAAW,CAAC;QAC/D,cAAc,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,EAAE,SAAS,CAAC,EAAE,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC;KAC1F,CAAC;IACF,oFAAoF;IACpF,QAAQ,CAAC,iBAAiB,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,OAAO,CAAC;IACtE,gFAAgF;IAChF,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IACnC,qGAAqG;IACrG,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAC9D,uEAAuE;IACvE,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;IACtC,8EAA8E;IAC9E,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IACrC,yGAAyG;IACzG,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACnE,iFAAiF;IACjF,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,CAAC;CACjE;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,cAAc,YAAY,CAAC,EAClC,QAAQ,EAAE,MAAM,GACf,EAAE,CAAC,kBAAkB,CAMvB;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB;;;;;0BAoB5C,oBAAoB,KAAG,IAAI;+BAqBxB,IAAI;+BAYF,oBAAoB,KAAG,IAAI;+BAmD3B,MAAM,KAAG,IAAI;6BAtBjB,IAAI;wBAgD5B,MAAM,YACA,MAAM,aACL,oBAAoB,KAC9B,mBAAmB;2BAsDQ,MAAM,YAAY,MAAM,KAAG,mBAAmB;
|
|
1
|
+
{"version":3,"file":"VirtualRecordStore.d.ts","sourceRoot":"","sources":["../../src/internal/VirtualRecordStore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,KAAK,EACV,2BAA2B,EAC3B,uBAAuB,EACvB,mBAAmB,EACnB,uBAAuB,EACxB,MAAM,aAAa,CAAC;AAGrB,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,mBAAmB,EAAE,SAAS,GAAG,OAAO,CAAC,GAAG;IAClF,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;CACvC;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;CAC/B;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,uBAAuB,CAAC;CAC9C;AAED,MAAM,MAAM,mBAAmB,GAC3B,2BAA2B,GAC3B,6BAA6B,GAC7B,wBAAwB,CAAC;AAE7B,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE;QACjB,aAAa,CAAC,OAAO,EAAE,2BAA2B,GAAG,uBAAuB,CAAC;KAC9E,CAAC;IACF,QAAQ,CAAC,wBAAwB,CAAC,EAAE,2BAA2B,CAAC,0BAA0B,CAAC,CAAC;IAC5F,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE;QACnB,SAAS,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,EAAE,CAAC,WAAW,CAAC;QAC/D,cAAc,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,EAAE,SAAS,CAAC,EAAE,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC;KAC1F,CAAC;IACF,oFAAoF;IACpF,QAAQ,CAAC,iBAAiB,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,OAAO,CAAC;IACtE,gFAAgF;IAChF,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IACnC,qGAAqG;IACrG,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAC9D,uEAAuE;IACvE,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;IACtC,8EAA8E;IAC9E,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IACrC,yGAAyG;IACzG,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACnE,iFAAiF;IACjF,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,CAAC;CACjE;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,cAAc,YAAY,CAAC,EAClC,QAAQ,EAAE,MAAM,GACf,EAAE,CAAC,kBAAkB,CAMvB;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB;;;;;0BAoB5C,oBAAoB,KAAG,IAAI;+BAqBxB,IAAI;+BAYF,oBAAoB,KAAG,IAAI;+BAmD3B,MAAM,KAAG,IAAI;6BAtBjB,IAAI;wBAgD5B,MAAM,YACA,MAAM,aACL,oBAAoB,KAC9B,mBAAmB;2BAsDQ,MAAM,YAAY,MAAM,KAAG,mBAAmB;+CAqB1B,MAAM,KAAG,MAAM;mBAa7C,IAAI;EA+BzB"}
|
|
@@ -168,6 +168,26 @@ export function createVirtualRecordStore(options) {
|
|
|
168
168
|
}
|
|
169
169
|
return resolveRecord(id, importer, existing);
|
|
170
170
|
};
|
|
171
|
+
/**
|
|
172
|
+
* Walk the virtual-file chain from containingFile back to the root real-file importer.
|
|
173
|
+
* When a virtual module imports another virtual module, the containing file is a virtual
|
|
174
|
+
* file path; plugins must receive the real file as importer. Returns input unchanged if
|
|
175
|
+
* not a virtual file. Handles cycles by breaking the loop.
|
|
176
|
+
*/
|
|
177
|
+
const resolveEffectiveImporter = (containingFile) => {
|
|
178
|
+
let current = containingFile;
|
|
179
|
+
const visited = new Set();
|
|
180
|
+
while (true) {
|
|
181
|
+
if (visited.has(current))
|
|
182
|
+
break;
|
|
183
|
+
visited.add(current);
|
|
184
|
+
const record = recordsByVirtualFile.get(current);
|
|
185
|
+
if (!record)
|
|
186
|
+
break;
|
|
187
|
+
current = record.importer;
|
|
188
|
+
}
|
|
189
|
+
return current;
|
|
190
|
+
};
|
|
171
191
|
const dispose = () => {
|
|
172
192
|
if (debounceTimer !== undefined) {
|
|
173
193
|
clearTimeout(debounceTimer);
|
|
@@ -194,6 +214,7 @@ export function createVirtualRecordStore(options) {
|
|
|
194
214
|
flushPendingStale,
|
|
195
215
|
resolveRecord,
|
|
196
216
|
getOrBuildRecord,
|
|
217
|
+
resolveEffectiveImporter,
|
|
197
218
|
dispose,
|
|
198
219
|
};
|
|
199
220
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typed/virtual-modules",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -15,24 +15,24 @@
|
|
|
15
15
|
"publishConfig": {
|
|
16
16
|
"access": "public"
|
|
17
17
|
},
|
|
18
|
-
"scripts": {
|
|
19
|
-
"build": "[ -d dist ] || rm -f tsconfig.tsbuildinfo; tsc",
|
|
20
|
-
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
21
|
-
"test": "vitest run --passWithNoTests",
|
|
22
|
-
"test:coverage": "vitest run --coverage"
|
|
23
|
-
},
|
|
24
18
|
"devDependencies": {
|
|
25
19
|
"@manuth/typescript-languageservice-tester": "^5.0.2",
|
|
26
20
|
"@types/node": "^25.3.0",
|
|
27
21
|
"@vitest/coverage-v8": "^4.0.0",
|
|
28
|
-
"typescript": "
|
|
29
|
-
"vitest": "
|
|
22
|
+
"typescript": "5.9.3",
|
|
23
|
+
"vitest": "4.0.18"
|
|
30
24
|
},
|
|
31
25
|
"peerDependencies": {
|
|
32
|
-
"typescript": "
|
|
26
|
+
"typescript": "5.9.3"
|
|
33
27
|
},
|
|
34
28
|
"files": [
|
|
35
29
|
"dist",
|
|
36
30
|
"src"
|
|
37
|
-
]
|
|
38
|
-
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "[ -d dist ] || rm -f tsconfig.tsbuildinfo; tsc",
|
|
34
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
35
|
+
"test": "vitest run --passWithNoTests",
|
|
36
|
+
"test:coverage": "vitest run --coverage"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -72,6 +72,60 @@ export const value: Foo = { n: 1 };
|
|
|
72
72
|
adapter.dispose();
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
it("resolves virtual module that imports another virtual module (virtual-to-virtual)", () => {
|
|
76
|
+
const dir = createTempDir();
|
|
77
|
+
const entry = join(dir, "entry.ts");
|
|
78
|
+
writeFileSync(entry, `import { x } from "virtual:a"; export const out = x;`, "utf8");
|
|
79
|
+
|
|
80
|
+
const receivedImporters: string[] = [];
|
|
81
|
+
const manager = new PluginManager([
|
|
82
|
+
{
|
|
83
|
+
name: "virtual-a",
|
|
84
|
+
shouldResolve: (id) => id === "virtual:a",
|
|
85
|
+
build: (_id, importer) => {
|
|
86
|
+
receivedImporters.push(`a:${importer}`);
|
|
87
|
+
return `import { x } from "virtual:b"; export { x };`;
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "virtual-b",
|
|
92
|
+
shouldResolve: (id) => id === "virtual:b",
|
|
93
|
+
build: (_id, importer) => {
|
|
94
|
+
receivedImporters.push(`b:${importer}`);
|
|
95
|
+
return `export const x = 1;`;
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
const compilerOptions: ts.CompilerOptions = {
|
|
101
|
+
strict: true,
|
|
102
|
+
noEmit: true,
|
|
103
|
+
target: ts.ScriptTarget.ESNext,
|
|
104
|
+
module: ts.ModuleKind.ESNext,
|
|
105
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
106
|
+
skipLibCheck: true,
|
|
107
|
+
};
|
|
108
|
+
const host = ts.createCompilerHost(compilerOptions);
|
|
109
|
+
attachCompilerHostAdapter({
|
|
110
|
+
ts,
|
|
111
|
+
compilerHost: host,
|
|
112
|
+
resolver: manager,
|
|
113
|
+
projectRoot: dir,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const program = ts.createProgram([entry], compilerOptions, host);
|
|
117
|
+
const diagnostics = ts.getPreEmitDiagnostics(program);
|
|
118
|
+
expect(diagnostics).toHaveLength(0);
|
|
119
|
+
expect(receivedImporters).toContain(`a:${entry}`);
|
|
120
|
+
expect(receivedImporters).toContain(`b:${entry}`);
|
|
121
|
+
expect(
|
|
122
|
+
program.getSourceFiles().some((sf) => sf.fileName.includes("__virtual_virtual-a_")),
|
|
123
|
+
).toBe(true);
|
|
124
|
+
expect(
|
|
125
|
+
program.getSourceFiles().some((sf) => sf.fileName.includes("__virtual_virtual-b_")),
|
|
126
|
+
).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
75
129
|
it("evicts virtual record when importer no longer exists (fileExists returns false)", () => {
|
|
76
130
|
const dir = createTempDir();
|
|
77
131
|
const entry1 = join(dir, "entry1.ts");
|
|
@@ -140,8 +140,9 @@ export const attachCompilerHostAdapter = (
|
|
|
140
140
|
fallbackResolveModule(moduleName, containingFile, compilerOptions),
|
|
141
141
|
);
|
|
142
142
|
|
|
143
|
+
const effectiveImporter = store.resolveEffectiveImporter(containingFile);
|
|
143
144
|
return moduleNames.map((moduleName, index) => {
|
|
144
|
-
const record = getOrBuildRecord(moduleName,
|
|
145
|
+
const record = getOrBuildRecord(moduleName, effectiveImporter);
|
|
145
146
|
if (!record) {
|
|
146
147
|
return fallback[index];
|
|
147
148
|
}
|
|
@@ -174,8 +175,9 @@ export const attachCompilerHostAdapter = (
|
|
|
174
175
|
),
|
|
175
176
|
}));
|
|
176
177
|
|
|
178
|
+
const effectiveImporter = store.resolveEffectiveImporter(containingFile);
|
|
177
179
|
return moduleLiterals.map((moduleLiteral, index) => {
|
|
178
|
-
const record = getOrBuildRecord(moduleLiteral.text,
|
|
180
|
+
const record = getOrBuildRecord(moduleLiteral.text, effectiveImporter);
|
|
179
181
|
if (!record) {
|
|
180
182
|
return fallback[index];
|
|
181
183
|
}
|
|
@@ -270,6 +270,12 @@ export const value: Foo = { n: 1 };
|
|
|
270
270
|
{
|
|
271
271
|
file: () => ({ ok: false as const, error: "file-not-in-program" as const }),
|
|
272
272
|
directory: () => [],
|
|
273
|
+
resolveExport: () => {
|
|
274
|
+
throw new Error("not implemented")
|
|
275
|
+
},
|
|
276
|
+
isAssignableTo: () => {
|
|
277
|
+
throw new Error("not implemented")
|
|
278
|
+
}
|
|
273
279
|
},
|
|
274
280
|
{ _host: host },
|
|
275
281
|
),
|
|
@@ -457,6 +463,171 @@ export const value: Foo = { n: 1 };
|
|
|
457
463
|
expect(snapshot).toBeUndefined();
|
|
458
464
|
});
|
|
459
465
|
|
|
466
|
+
it("resolves virtual module that imports another virtual module (virtual-to-virtual)", () => {
|
|
467
|
+
const dir = createTempDir();
|
|
468
|
+
const entryFile = join(dir, "entry.ts");
|
|
469
|
+
writeFileSync(
|
|
470
|
+
entryFile,
|
|
471
|
+
`import { x } from "virtual:a"; export const out = x;`,
|
|
472
|
+
"utf8",
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
const receivedImporters: string[] = [];
|
|
476
|
+
const files = new Map<string, { version: number; content: string }>([
|
|
477
|
+
[entryFile, { version: 1, content: ts.sys.readFile(entryFile) ?? "" }],
|
|
478
|
+
]);
|
|
479
|
+
|
|
480
|
+
const host: ts.LanguageServiceHost = {
|
|
481
|
+
getCompilationSettings: () => ({
|
|
482
|
+
strict: true,
|
|
483
|
+
noEmit: true,
|
|
484
|
+
target: ts.ScriptTarget.ESNext,
|
|
485
|
+
module: ts.ModuleKind.ESNext,
|
|
486
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
487
|
+
skipLibCheck: true,
|
|
488
|
+
}),
|
|
489
|
+
getScriptFileNames: () => [...files.keys()],
|
|
490
|
+
getScriptVersion: (fileName) => String(files.get(fileName)?.version ?? 0),
|
|
491
|
+
getScriptSnapshot: (fileName) => {
|
|
492
|
+
const content = files.get(fileName)?.content ?? ts.sys.readFile(fileName);
|
|
493
|
+
if (!content) return undefined;
|
|
494
|
+
return ts.ScriptSnapshot.fromString(content);
|
|
495
|
+
},
|
|
496
|
+
getCurrentDirectory: () => dir,
|
|
497
|
+
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
|
|
498
|
+
fileExists: (fileName) => files.has(fileName) || ts.sys.fileExists(fileName),
|
|
499
|
+
readFile: (fileName) => files.get(fileName)?.content ?? ts.sys.readFile(fileName),
|
|
500
|
+
readDirectory: (...args: Parameters<typeof ts.sys.readDirectory>) =>
|
|
501
|
+
ts.sys.readDirectory(...args),
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const manager = new PluginManager([
|
|
505
|
+
{
|
|
506
|
+
name: "virtual-a",
|
|
507
|
+
shouldResolve: (id) => id === "virtual:a",
|
|
508
|
+
build: (id, importer) => {
|
|
509
|
+
receivedImporters.push(`a:${importer}`);
|
|
510
|
+
return `import { x } from "virtual:b"; export { x };`;
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
name: "virtual-b",
|
|
515
|
+
shouldResolve: (id) => id === "virtual:b",
|
|
516
|
+
build: (id, importer) => {
|
|
517
|
+
receivedImporters.push(`b:${importer}`);
|
|
518
|
+
return `export const x = 1;`;
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
]);
|
|
522
|
+
|
|
523
|
+
const languageService = ts.createLanguageService(host);
|
|
524
|
+
attachLanguageServiceAdapter({
|
|
525
|
+
ts,
|
|
526
|
+
languageService,
|
|
527
|
+
languageServiceHost: host,
|
|
528
|
+
resolver: manager,
|
|
529
|
+
projectRoot: dir,
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const diagnostics = languageService.getSemanticDiagnostics(entryFile);
|
|
533
|
+
expect(diagnostics).toHaveLength(0);
|
|
534
|
+
expect(receivedImporters).toContain(`a:${entryFile}`);
|
|
535
|
+
expect(receivedImporters).toContain(`b:${entryFile}`);
|
|
536
|
+
expect(
|
|
537
|
+
languageService
|
|
538
|
+
.getProgram()
|
|
539
|
+
?.getSourceFiles()
|
|
540
|
+
.some((sf) => sf.fileName.includes("__virtual_virtual-a_")),
|
|
541
|
+
).toBe(true);
|
|
542
|
+
expect(
|
|
543
|
+
languageService
|
|
544
|
+
.getProgram()
|
|
545
|
+
?.getSourceFiles()
|
|
546
|
+
.some((sf) => sf.fileName.includes("__virtual_virtual-b_")),
|
|
547
|
+
).toBe(true);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it("resolves chain virtual:a -> virtual:b -> virtual:c with root importer", () => {
|
|
551
|
+
const dir = createTempDir();
|
|
552
|
+
const entryFile = join(dir, "entry.ts");
|
|
553
|
+
writeFileSync(
|
|
554
|
+
entryFile,
|
|
555
|
+
`import { z } from "virtual:a"; export const out = z;`,
|
|
556
|
+
"utf8",
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
const receivedImporters: string[] = [];
|
|
560
|
+
const files = new Map<string, { version: number; content: string }>([
|
|
561
|
+
[entryFile, { version: 1, content: ts.sys.readFile(entryFile) ?? "" }],
|
|
562
|
+
]);
|
|
563
|
+
|
|
564
|
+
const host: ts.LanguageServiceHost = {
|
|
565
|
+
getCompilationSettings: () => ({
|
|
566
|
+
strict: true,
|
|
567
|
+
noEmit: true,
|
|
568
|
+
target: ts.ScriptTarget.ESNext,
|
|
569
|
+
module: ts.ModuleKind.ESNext,
|
|
570
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
571
|
+
skipLibCheck: true,
|
|
572
|
+
}),
|
|
573
|
+
getScriptFileNames: () => [...files.keys()],
|
|
574
|
+
getScriptVersion: (fileName) => String(files.get(fileName)?.version ?? 0),
|
|
575
|
+
getScriptSnapshot: (fileName) => {
|
|
576
|
+
const content = files.get(fileName)?.content ?? ts.sys.readFile(fileName);
|
|
577
|
+
if (!content) return undefined;
|
|
578
|
+
return ts.ScriptSnapshot.fromString(content);
|
|
579
|
+
},
|
|
580
|
+
getCurrentDirectory: () => dir,
|
|
581
|
+
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
|
|
582
|
+
fileExists: (fileName) => files.has(fileName) || ts.sys.fileExists(fileName),
|
|
583
|
+
readFile: (fileName) => files.get(fileName)?.content ?? ts.sys.readFile(fileName),
|
|
584
|
+
readDirectory: (...args: Parameters<typeof ts.sys.readDirectory>) =>
|
|
585
|
+
ts.sys.readDirectory(...args),
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const manager = new PluginManager([
|
|
589
|
+
{
|
|
590
|
+
name: "virtual-a",
|
|
591
|
+
shouldResolve: (id) => id === "virtual:a",
|
|
592
|
+
build: (_id, importer) => {
|
|
593
|
+
receivedImporters.push(`a:${importer}`);
|
|
594
|
+
return `import { y } from "virtual:b"; export { y as z };`;
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
name: "virtual-b",
|
|
599
|
+
shouldResolve: (id) => id === "virtual:b",
|
|
600
|
+
build: (_id, importer) => {
|
|
601
|
+
receivedImporters.push(`b:${importer}`);
|
|
602
|
+
return `import { y } from "virtual:c"; export { y };`;
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
name: "virtual-c",
|
|
607
|
+
shouldResolve: (id) => id === "virtual:c",
|
|
608
|
+
build: (_id, importer) => {
|
|
609
|
+
receivedImporters.push(`c:${importer}`);
|
|
610
|
+
return `export const y = 42;`;
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
]);
|
|
614
|
+
|
|
615
|
+
const languageService = ts.createLanguageService(host);
|
|
616
|
+
attachLanguageServiceAdapter({
|
|
617
|
+
ts,
|
|
618
|
+
languageService,
|
|
619
|
+
languageServiceHost: host,
|
|
620
|
+
resolver: manager,
|
|
621
|
+
projectRoot: dir,
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
const diagnostics = languageService.getSemanticDiagnostics(entryFile);
|
|
625
|
+
expect(diagnostics).toHaveLength(0);
|
|
626
|
+
expect(receivedImporters).toContain(`a:${entryFile}`);
|
|
627
|
+
expect(receivedImporters).toContain(`b:${entryFile}`);
|
|
628
|
+
expect(receivedImporters).toContain(`c:${entryFile}`);
|
|
629
|
+
});
|
|
630
|
+
|
|
460
631
|
it("dispose then getScriptSnapshot does not throw and returns original behavior", () => {
|
|
461
632
|
const dir = createTempDir();
|
|
462
633
|
const entryFile = join(dir, "entry.ts");
|
|
@@ -281,7 +281,7 @@ export const attachLanguageServiceAdapter = (
|
|
|
281
281
|
const r = recordsByVirtualFile.get(parsed.virtualPath);
|
|
282
282
|
if (r) {
|
|
283
283
|
effectiveContainingFile = r.virtualFileName;
|
|
284
|
-
importerForVirtual =
|
|
284
|
+
importerForVirtual = store.resolveEffectiveImporter(parsed.virtualPath);
|
|
285
285
|
}
|
|
286
286
|
} else {
|
|
287
287
|
const r = getOrBuildRecord(parsed.id, parsed.importer);
|
|
@@ -290,6 +290,12 @@ export const attachLanguageServiceAdapter = (
|
|
|
290
290
|
importerForVirtual = r.record.importer;
|
|
291
291
|
}
|
|
292
292
|
}
|
|
293
|
+
} else {
|
|
294
|
+
const virtualRecord = recordsByVirtualFile.get(containingFile);
|
|
295
|
+
if (virtualRecord) {
|
|
296
|
+
effectiveContainingFile = virtualRecord.virtualFileName;
|
|
297
|
+
importerForVirtual = store.resolveEffectiveImporter(containingFile);
|
|
298
|
+
}
|
|
293
299
|
}
|
|
294
300
|
|
|
295
301
|
const fallback = originalResolveModuleNames
|
|
@@ -371,7 +377,7 @@ export const attachLanguageServiceAdapter = (
|
|
|
371
377
|
const r = recordsByVirtualFile.get(parsed.virtualPath);
|
|
372
378
|
if (r) {
|
|
373
379
|
effectiveContainingFile = r.virtualFileName;
|
|
374
|
-
importerForVirtual =
|
|
380
|
+
importerForVirtual = store.resolveEffectiveImporter(parsed.virtualPath);
|
|
375
381
|
}
|
|
376
382
|
} else {
|
|
377
383
|
const r = getOrBuildRecord(parsed.id, parsed.importer);
|
|
@@ -380,6 +386,12 @@ export const attachLanguageServiceAdapter = (
|
|
|
380
386
|
importerForVirtual = r.record.importer;
|
|
381
387
|
}
|
|
382
388
|
}
|
|
389
|
+
} else {
|
|
390
|
+
const virtualRecord = recordsByVirtualFile.get(containingFile);
|
|
391
|
+
if (virtualRecord) {
|
|
392
|
+
effectiveContainingFile = virtualRecord.virtualFileName;
|
|
393
|
+
importerForVirtual = store.resolveEffectiveImporter(containingFile);
|
|
394
|
+
}
|
|
383
395
|
}
|
|
384
396
|
|
|
385
397
|
const fallback: readonly ts.ResolvedModuleWithFailedLookupLocations[] =
|
|
@@ -404,23 +416,6 @@ export const attachLanguageServiceAdapter = (
|
|
|
404
416
|
let hadUnresolvedVirtual = false;
|
|
405
417
|
const results = moduleLiterals.map((moduleLiteral, index) => {
|
|
406
418
|
const resolved = getOrBuildRecord(moduleLiteral.text, importerForVirtual);
|
|
407
|
-
if (moduleLiteral.text.includes(":")) {
|
|
408
|
-
try {
|
|
409
|
-
require("node:fs").appendFileSync(
|
|
410
|
-
"/tmp/vm-ts-plugin-debug.log",
|
|
411
|
-
JSON.stringify({
|
|
412
|
-
tag: "LS:resolveLiterals",
|
|
413
|
-
id: moduleLiteral.text,
|
|
414
|
-
status: resolved.status,
|
|
415
|
-
err: resolved.status === "error" ? (resolved as { diagnostic?: { message?: string } }).diagnostic?.message : undefined,
|
|
416
|
-
t: Date.now(),
|
|
417
|
-
}) + "\n",
|
|
418
|
-
{ flag: "a" },
|
|
419
|
-
);
|
|
420
|
-
} catch {
|
|
421
|
-
/* noop */
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
419
|
if (resolved.status === "resolved") {
|
|
425
420
|
pendingRetry = false;
|
|
426
421
|
return {
|
|
@@ -271,6 +271,25 @@ export function createVirtualRecordStore(options: VirtualRecordStoreOptions) {
|
|
|
271
271
|
return resolveRecord(id, importer, existing);
|
|
272
272
|
};
|
|
273
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Walk the virtual-file chain from containingFile back to the root real-file importer.
|
|
276
|
+
* When a virtual module imports another virtual module, the containing file is a virtual
|
|
277
|
+
* file path; plugins must receive the real file as importer. Returns input unchanged if
|
|
278
|
+
* not a virtual file. Handles cycles by breaking the loop.
|
|
279
|
+
*/
|
|
280
|
+
const resolveEffectiveImporter = (containingFile: string): string => {
|
|
281
|
+
let current = containingFile;
|
|
282
|
+
const visited = new Set<string>();
|
|
283
|
+
while (true) {
|
|
284
|
+
if (visited.has(current)) break;
|
|
285
|
+
visited.add(current);
|
|
286
|
+
const record = recordsByVirtualFile.get(current);
|
|
287
|
+
if (!record) break;
|
|
288
|
+
current = record.importer;
|
|
289
|
+
}
|
|
290
|
+
return current;
|
|
291
|
+
};
|
|
292
|
+
|
|
274
293
|
const dispose = (): void => {
|
|
275
294
|
if (debounceTimer !== undefined) {
|
|
276
295
|
clearTimeout(debounceTimer);
|
|
@@ -299,6 +318,7 @@ export function createVirtualRecordStore(options: VirtualRecordStoreOptions) {
|
|
|
299
318
|
flushPendingStale,
|
|
300
319
|
resolveRecord,
|
|
301
320
|
getOrBuildRecord,
|
|
321
|
+
resolveEffectiveImporter,
|
|
302
322
|
dispose,
|
|
303
323
|
};
|
|
304
324
|
}
|