@typed/virtual-modules 1.0.0-beta.1
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/README.md +135 -0
- package/dist/CompilerHostAdapter.d.ts +3 -0
- package/dist/CompilerHostAdapter.d.ts.map +1 -0
- package/dist/CompilerHostAdapter.js +160 -0
- package/dist/LanguageServiceAdapter.d.ts +3 -0
- package/dist/LanguageServiceAdapter.d.ts.map +1 -0
- package/dist/LanguageServiceAdapter.js +488 -0
- package/dist/LanguageServiceSession.d.ts +16 -0
- package/dist/LanguageServiceSession.d.ts.map +1 -0
- package/dist/LanguageServiceSession.js +122 -0
- package/dist/NodeModulePluginLoader.d.ts +8 -0
- package/dist/NodeModulePluginLoader.d.ts.map +1 -0
- package/dist/NodeModulePluginLoader.js +175 -0
- package/dist/PluginManager.d.ts +10 -0
- package/dist/PluginManager.d.ts.map +1 -0
- package/dist/PluginManager.js +151 -0
- package/dist/TypeInfoApi.d.ts +71 -0
- package/dist/TypeInfoApi.d.ts.map +1 -0
- package/dist/TypeInfoApi.js +855 -0
- package/dist/VmcConfigLoader.d.ts +31 -0
- package/dist/VmcConfigLoader.d.ts.map +1 -0
- package/dist/VmcConfigLoader.js +231 -0
- package/dist/VmcResolverLoader.d.ts +30 -0
- package/dist/VmcResolverLoader.d.ts.map +1 -0
- package/dist/VmcResolverLoader.js +71 -0
- package/dist/collectTypeTargetSpecs.d.ts +6 -0
- package/dist/collectTypeTargetSpecs.d.ts.map +1 -0
- package/dist/collectTypeTargetSpecs.js +19 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/internal/VirtualRecordStore.d.ts +60 -0
- package/dist/internal/VirtualRecordStore.d.ts.map +1 -0
- package/dist/internal/VirtualRecordStore.js +199 -0
- package/dist/internal/materializeVirtualFile.d.ts +12 -0
- package/dist/internal/materializeVirtualFile.d.ts.map +1 -0
- package/dist/internal/materializeVirtualFile.js +28 -0
- package/dist/internal/path.d.ts +40 -0
- package/dist/internal/path.d.ts.map +1 -0
- package/dist/internal/path.js +71 -0
- package/dist/internal/sanitize.d.ts +6 -0
- package/dist/internal/sanitize.d.ts.map +1 -0
- package/dist/internal/sanitize.js +15 -0
- package/dist/internal/tsInternal.d.ts +65 -0
- package/dist/internal/tsInternal.d.ts.map +1 -0
- package/dist/internal/tsInternal.js +99 -0
- package/dist/internal/validation.d.ts +28 -0
- package/dist/internal/validation.d.ts.map +1 -0
- package/dist/internal/validation.js +66 -0
- package/dist/typeTargetBootstrap.d.ts +33 -0
- package/dist/typeTargetBootstrap.d.ts.map +1 -0
- package/dist/typeTargetBootstrap.js +57 -0
- package/dist/types.d.ts +405 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/package.json +38 -0
- package/src/CompilerHostAdapter.test.ts +180 -0
- package/src/CompilerHostAdapter.ts +316 -0
- package/src/LanguageServiceAdapter.test.ts +521 -0
- package/src/LanguageServiceAdapter.ts +631 -0
- package/src/LanguageServiceSession.ts +160 -0
- package/src/LanguageServiceTester.integration.test.ts +268 -0
- package/src/NodeModulePluginLoader.test.ts +170 -0
- package/src/NodeModulePluginLoader.ts +268 -0
- package/src/PluginManager.test.ts +178 -0
- package/src/PluginManager.ts +218 -0
- package/src/TypeInfoApi.test.ts +1211 -0
- package/src/TypeInfoApi.ts +1228 -0
- package/src/VmcConfigLoader.test.ts +108 -0
- package/src/VmcConfigLoader.ts +297 -0
- package/src/VmcResolverLoader.test.ts +181 -0
- package/src/VmcResolverLoader.ts +116 -0
- package/src/collectTypeTargetSpecs.ts +22 -0
- package/src/index.ts +35 -0
- package/src/internal/VirtualRecordStore.ts +304 -0
- package/src/internal/materializeVirtualFile.ts +38 -0
- package/src/internal/path.ts +106 -0
- package/src/internal/sanitize.ts +16 -0
- package/src/internal/tsInternal.ts +127 -0
- package/src/internal/validation.ts +85 -0
- package/src/typeTargetBootstrap.ts +75 -0
- package/src/types.ts +535 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import ts from "typescript";
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { attachLanguageServiceAdapter } from "./LanguageServiceAdapter.js";
|
|
8
|
+
import { PluginManager } from "./PluginManager.js";
|
|
9
|
+
import { createTypeInfoApiSession } from "./TypeInfoApi.js";
|
|
10
|
+
import type { LanguageServiceWatchHost } from "./types.js";
|
|
11
|
+
|
|
12
|
+
const tempDirs: string[] = [];
|
|
13
|
+
|
|
14
|
+
const getDiagnosticMessage = (d: ts.Diagnostic): string =>
|
|
15
|
+
typeof d.messageText === "string"
|
|
16
|
+
? d.messageText
|
|
17
|
+
: ts.flattenDiagnosticMessageText(d.messageText, "\n");
|
|
18
|
+
|
|
19
|
+
const createTempDir = (): string => {
|
|
20
|
+
const dir = mkdtempSync(join(tmpdir(), "typed-vm-ls-"));
|
|
21
|
+
tempDirs.push(dir);
|
|
22
|
+
return dir;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
while (tempDirs.length > 0) {
|
|
27
|
+
const dir = tempDirs.pop();
|
|
28
|
+
if (dir) {
|
|
29
|
+
rmSync(dir, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("attachLanguageServiceAdapter", () => {
|
|
35
|
+
it("resolves virtual module imports through host patching", () => {
|
|
36
|
+
const dir = createTempDir();
|
|
37
|
+
const entryFile = join(dir, "entry.ts");
|
|
38
|
+
writeFileSync(
|
|
39
|
+
entryFile,
|
|
40
|
+
`
|
|
41
|
+
import type { Foo } from "virtual:foo";
|
|
42
|
+
export const value: Foo = { n: 1 };
|
|
43
|
+
`,
|
|
44
|
+
"utf8",
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const files = new Map<string, { version: number; content: string }>([
|
|
48
|
+
[entryFile, { version: 1, content: ts.sys.readFile(entryFile) ?? "" }],
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const host: ts.LanguageServiceHost = {
|
|
52
|
+
getCompilationSettings: () => ({
|
|
53
|
+
strict: true,
|
|
54
|
+
noEmit: true,
|
|
55
|
+
target: ts.ScriptTarget.ESNext,
|
|
56
|
+
module: ts.ModuleKind.ESNext,
|
|
57
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
58
|
+
skipLibCheck: true,
|
|
59
|
+
}),
|
|
60
|
+
getScriptFileNames: () => [...files.keys()],
|
|
61
|
+
getScriptVersion: (fileName) => String(files.get(fileName)?.version ?? 0),
|
|
62
|
+
getScriptSnapshot: (fileName) => {
|
|
63
|
+
const content = files.get(fileName)?.content ?? ts.sys.readFile(fileName);
|
|
64
|
+
if (!content) return undefined;
|
|
65
|
+
return ts.ScriptSnapshot.fromString(content);
|
|
66
|
+
},
|
|
67
|
+
getCurrentDirectory: () => dir,
|
|
68
|
+
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
|
|
69
|
+
fileExists: (fileName) => files.has(fileName) || ts.sys.fileExists(fileName),
|
|
70
|
+
readFile: (fileName) => files.get(fileName)?.content ?? ts.sys.readFile(fileName),
|
|
71
|
+
readDirectory: (...args: Parameters<typeof ts.sys.readDirectory>) =>
|
|
72
|
+
ts.sys.readDirectory(...args),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const languageService = ts.createLanguageService(host);
|
|
76
|
+
const manager = new PluginManager([
|
|
77
|
+
{
|
|
78
|
+
name: "virtual",
|
|
79
|
+
shouldResolve: (id) => id === "virtual:foo",
|
|
80
|
+
build: () => `export interface Foo { n: number }`,
|
|
81
|
+
},
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const adapter = attachLanguageServiceAdapter({
|
|
85
|
+
ts,
|
|
86
|
+
languageService,
|
|
87
|
+
languageServiceHost: host,
|
|
88
|
+
resolver: manager,
|
|
89
|
+
projectRoot: dir,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const diagnostics = languageService.getSemanticDiagnostics(entryFile);
|
|
93
|
+
expect(diagnostics).toHaveLength(0);
|
|
94
|
+
expect(
|
|
95
|
+
languageService
|
|
96
|
+
.getProgram()
|
|
97
|
+
?.getSourceFiles()
|
|
98
|
+
.some((sourceFile) => sourceFile.fileName.includes("__virtual_")),
|
|
99
|
+
).toBe(true);
|
|
100
|
+
|
|
101
|
+
adapter.dispose();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("keeps record stale and adds diagnostic when rebuild fails, then clears on success", () => {
|
|
105
|
+
const dir = createTempDir();
|
|
106
|
+
const entryFile = join(dir, "entry.ts");
|
|
107
|
+
const depFile = join(dir, "dep.ts");
|
|
108
|
+
writeFileSync(
|
|
109
|
+
entryFile,
|
|
110
|
+
`import type { Foo } from "virtual:foo"; export const x: Foo = { n: 1 };`,
|
|
111
|
+
"utf8",
|
|
112
|
+
);
|
|
113
|
+
writeFileSync(depFile, "export const d = 1;", "utf8");
|
|
114
|
+
|
|
115
|
+
let buildCount = 0;
|
|
116
|
+
let watchCallback: (() => void) | undefined;
|
|
117
|
+
const files = new Map<string, { version: number; content: string }>([
|
|
118
|
+
[entryFile, { version: 1, content: ts.sys.readFile(entryFile) ?? "" }],
|
|
119
|
+
[depFile, { version: 1, content: ts.sys.readFile(depFile) ?? "" }],
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
const host: ts.LanguageServiceHost & {
|
|
123
|
+
resolveModuleNames?: (...args: unknown[]) => (ts.ResolvedModule | undefined)[];
|
|
124
|
+
} = {
|
|
125
|
+
getCompilationSettings: () => ({
|
|
126
|
+
strict: true,
|
|
127
|
+
noEmit: true,
|
|
128
|
+
target: ts.ScriptTarget.ESNext,
|
|
129
|
+
module: ts.ModuleKind.ESNext,
|
|
130
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
131
|
+
skipLibCheck: true,
|
|
132
|
+
}),
|
|
133
|
+
getScriptFileNames: () => [...files.keys()],
|
|
134
|
+
getScriptVersion: (fileName) => String(files.get(fileName)?.version ?? 0),
|
|
135
|
+
getScriptSnapshot: (fileName) => {
|
|
136
|
+
const content = files.get(fileName)?.content ?? ts.sys.readFile(fileName);
|
|
137
|
+
if (!content) return undefined;
|
|
138
|
+
return ts.ScriptSnapshot.fromString(content);
|
|
139
|
+
},
|
|
140
|
+
getCurrentDirectory: () => dir,
|
|
141
|
+
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
|
|
142
|
+
fileExists: (fileName) => files.has(fileName) || ts.sys.fileExists(fileName),
|
|
143
|
+
readFile: (fileName) => files.get(fileName)?.content ?? ts.sys.readFile(fileName),
|
|
144
|
+
readDirectory: (...args: Parameters<typeof ts.sys.readDirectory>) =>
|
|
145
|
+
ts.sys.readDirectory(...args),
|
|
146
|
+
resolveModuleNames: (..._args: unknown[]): (ts.ResolvedModule | undefined)[] => [],
|
|
147
|
+
resolveModuleNameLiterals: (literals: readonly { readonly text: string }[]) =>
|
|
148
|
+
literals.map(() => ({ resolvedModule: undefined as ts.ResolvedModuleFull | undefined })),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const watchHost: LanguageServiceWatchHost = {
|
|
152
|
+
watchFile: (path: string, callback: ts.FileWatcherCallback) => {
|
|
153
|
+
watchCallback = () => callback(path, ts.FileWatcherEventKind.Changed);
|
|
154
|
+
return { close: () => {} };
|
|
155
|
+
},
|
|
156
|
+
watchDirectory: () => ({ close: () => {} }),
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const manager = new PluginManager([
|
|
160
|
+
{
|
|
161
|
+
name: "virtual",
|
|
162
|
+
shouldResolve: (id) => id === "virtual:foo",
|
|
163
|
+
build: (_id, _importer, api) => {
|
|
164
|
+
buildCount++;
|
|
165
|
+
if (buildCount === 2) {
|
|
166
|
+
throw new Error("second build failed");
|
|
167
|
+
}
|
|
168
|
+
api.file("./dep.ts", { baseDir: dir, watch: true });
|
|
169
|
+
return "export interface Foo { n: number }";
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
const languageService = ts.createLanguageService(host);
|
|
175
|
+
attachLanguageServiceAdapter({
|
|
176
|
+
ts,
|
|
177
|
+
languageService,
|
|
178
|
+
languageServiceHost: host,
|
|
179
|
+
resolver: manager,
|
|
180
|
+
projectRoot: dir,
|
|
181
|
+
watchHost,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
languageService.getSemanticDiagnostics(entryFile);
|
|
185
|
+
expect(buildCount).toBeGreaterThanOrEqual(1);
|
|
186
|
+
|
|
187
|
+
watchCallback?.();
|
|
188
|
+
const diag1 = languageService.getSemanticDiagnostics(entryFile);
|
|
189
|
+
const rebuildFailedDiag = diag1.filter((d) =>
|
|
190
|
+
getDiagnosticMessage(d).includes("rebuild failed"),
|
|
191
|
+
);
|
|
192
|
+
if (rebuildFailedDiag.length > 0) {
|
|
193
|
+
watchCallback?.();
|
|
194
|
+
const diag2 = languageService.getSemanticDiagnostics(entryFile);
|
|
195
|
+
const stillFailed = diag2.filter((d) => getDiagnosticMessage(d).includes("rebuild failed"));
|
|
196
|
+
expect(stillFailed.length).toBe(0);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("detects re-entrant resolution and returns diagnostic", () => {
|
|
201
|
+
const dir = createTempDir();
|
|
202
|
+
const entryFile = join(dir, "entry.ts");
|
|
203
|
+
writeFileSync(entryFile, `import "virtual:foo"; import "virtual:bar";`, "utf8");
|
|
204
|
+
const files = new Map<string, { version: number; content: string }>([
|
|
205
|
+
[entryFile, { version: 1, content: ts.sys.readFile(entryFile) ?? "" }],
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
const host: ts.LanguageServiceHost & { _triggerResolve?: () => void } = {
|
|
209
|
+
getCompilationSettings: () => ({
|
|
210
|
+
strict: true,
|
|
211
|
+
noEmit: true,
|
|
212
|
+
target: ts.ScriptTarget.ESNext,
|
|
213
|
+
module: ts.ModuleKind.ESNext,
|
|
214
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
215
|
+
skipLibCheck: true,
|
|
216
|
+
}),
|
|
217
|
+
getScriptFileNames: () => [...files.keys()],
|
|
218
|
+
getScriptVersion: (fileName) => String(files.get(fileName)?.version ?? 0),
|
|
219
|
+
getScriptSnapshot: (fileName) => {
|
|
220
|
+
const content = files.get(fileName)?.content ?? ts.sys.readFile(fileName);
|
|
221
|
+
if (!content) return undefined;
|
|
222
|
+
return ts.ScriptSnapshot.fromString(content);
|
|
223
|
+
},
|
|
224
|
+
getCurrentDirectory: () => dir,
|
|
225
|
+
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
|
|
226
|
+
fileExists: (fileName) => files.has(fileName) || ts.sys.fileExists(fileName),
|
|
227
|
+
readFile: (fileName) => files.get(fileName)?.content ?? ts.sys.readFile(fileName),
|
|
228
|
+
readDirectory: (...args: Parameters<typeof ts.sys.readDirectory>) =>
|
|
229
|
+
ts.sys.readDirectory(...args),
|
|
230
|
+
resolveModuleNames: (
|
|
231
|
+
_moduleNames: string[],
|
|
232
|
+
_containingFile: string,
|
|
233
|
+
_reusedNames: string[] | undefined,
|
|
234
|
+
_redirectedReference: ts.ResolvedProjectReference | undefined,
|
|
235
|
+
_compilerOptions: ts.CompilerOptions,
|
|
236
|
+
_containingSourceFile?: ts.SourceFile,
|
|
237
|
+
): (ts.ResolvedModule | undefined)[] => [],
|
|
238
|
+
resolveModuleNameLiterals: (literals: readonly { readonly text: string }[]) =>
|
|
239
|
+
literals.map(() => ({ resolvedModule: undefined as ts.ResolvedModuleFull | undefined })),
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const manager = new PluginManager([
|
|
243
|
+
{
|
|
244
|
+
name: "virtual-foo",
|
|
245
|
+
shouldResolve: (id) => id === "virtual:foo",
|
|
246
|
+
build: (_id, _importer, api) => {
|
|
247
|
+
const h = (api as { _host?: typeof host })._host;
|
|
248
|
+
if (h?.getScriptFileNames) {
|
|
249
|
+
h._triggerResolve?.();
|
|
250
|
+
}
|
|
251
|
+
return "export const foo = 1;";
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: "virtual-bar",
|
|
256
|
+
shouldResolve: (id) => id === "virtual:bar",
|
|
257
|
+
build: () => "export const bar = 2;",
|
|
258
|
+
},
|
|
259
|
+
]);
|
|
260
|
+
|
|
261
|
+
const languageService = ts.createLanguageService(host);
|
|
262
|
+
attachLanguageServiceAdapter({
|
|
263
|
+
ts,
|
|
264
|
+
languageService,
|
|
265
|
+
languageServiceHost: host,
|
|
266
|
+
resolver: manager,
|
|
267
|
+
projectRoot: dir,
|
|
268
|
+
createTypeInfoApiSession: () => ({
|
|
269
|
+
api: Object.assign(
|
|
270
|
+
{
|
|
271
|
+
file: () => ({ ok: false as const, error: "file-not-in-program" as const }),
|
|
272
|
+
directory: () => [],
|
|
273
|
+
},
|
|
274
|
+
{ _host: host },
|
|
275
|
+
),
|
|
276
|
+
consumeDependencies: () => [],
|
|
277
|
+
}),
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const patchedResolve = (
|
|
281
|
+
host as ts.LanguageServiceHost & { resolveModuleNames?: (...args: unknown[]) => unknown[] }
|
|
282
|
+
).resolveModuleNames;
|
|
283
|
+
host._triggerResolve = () => {
|
|
284
|
+
patchedResolve?.(["virtual:bar"], entryFile, undefined, undefined, {}, undefined);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
expect(() => languageService.getSemanticDiagnostics(entryFile)).not.toThrow();
|
|
288
|
+
const diagnostics = languageService.getSemanticDiagnostics(entryFile);
|
|
289
|
+
const reentrantDiag = diagnostics.filter((d) => getDiagnosticMessage(d).includes("Re-entrant"));
|
|
290
|
+
expect(reentrantDiag.length).toBeGreaterThanOrEqual(0);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("debounces watch callbacks when debounceMs is set", () => {
|
|
294
|
+
vi.useFakeTimers();
|
|
295
|
+
const dir = createTempDir();
|
|
296
|
+
const entryFile = join(dir, "entry.ts");
|
|
297
|
+
const depFile = join(dir, "dep.ts");
|
|
298
|
+
writeFileSync(
|
|
299
|
+
entryFile,
|
|
300
|
+
`import type { Foo } from "virtual:foo"; export const x: Foo = { n: 1 };`,
|
|
301
|
+
"utf8",
|
|
302
|
+
);
|
|
303
|
+
writeFileSync(depFile, "export const d = 1;", "utf8");
|
|
304
|
+
let buildCount = 0;
|
|
305
|
+
let watchCallback: (() => void) | undefined;
|
|
306
|
+
const files = new Map<string, { version: number; content: string }>([
|
|
307
|
+
[entryFile, { version: 1, content: ts.sys.readFile(entryFile) ?? "" }],
|
|
308
|
+
[depFile, { version: 1, content: ts.sys.readFile(depFile) ?? "" }],
|
|
309
|
+
]);
|
|
310
|
+
const host: ts.LanguageServiceHost = {
|
|
311
|
+
getCompilationSettings: () => ({
|
|
312
|
+
strict: true,
|
|
313
|
+
noEmit: true,
|
|
314
|
+
target: ts.ScriptTarget.ESNext,
|
|
315
|
+
module: ts.ModuleKind.ESNext,
|
|
316
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
317
|
+
skipLibCheck: true,
|
|
318
|
+
}),
|
|
319
|
+
getScriptFileNames: () => [...files.keys()],
|
|
320
|
+
getScriptVersion: (fileName) => String(files.get(fileName)?.version ?? 0),
|
|
321
|
+
getScriptSnapshot: (fileName) => {
|
|
322
|
+
const content = files.get(fileName)?.content ?? ts.sys.readFile(fileName);
|
|
323
|
+
if (!content) return undefined;
|
|
324
|
+
return ts.ScriptSnapshot.fromString(content);
|
|
325
|
+
},
|
|
326
|
+
getCurrentDirectory: () => dir,
|
|
327
|
+
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
|
|
328
|
+
fileExists: (fileName) => files.has(fileName) || ts.sys.fileExists(fileName),
|
|
329
|
+
readFile: (fileName) => files.get(fileName)?.content ?? ts.sys.readFile(fileName),
|
|
330
|
+
readDirectory: (...args: Parameters<typeof ts.sys.readDirectory>) =>
|
|
331
|
+
ts.sys.readDirectory(...args),
|
|
332
|
+
};
|
|
333
|
+
const watchHost: LanguageServiceWatchHost = {
|
|
334
|
+
watchFile: (path: string, callback: ts.FileWatcherCallback) => {
|
|
335
|
+
watchCallback = () => callback(path, ts.FileWatcherEventKind.Changed);
|
|
336
|
+
return { close: () => {} };
|
|
337
|
+
},
|
|
338
|
+
watchDirectory: () => ({ close: () => {} }),
|
|
339
|
+
};
|
|
340
|
+
const manager = new PluginManager([
|
|
341
|
+
{
|
|
342
|
+
name: "virtual",
|
|
343
|
+
shouldResolve: (id) => id === "virtual:foo",
|
|
344
|
+
build: (_id, _importer, api) => {
|
|
345
|
+
buildCount++;
|
|
346
|
+
api.file("./dep.ts", { baseDir: dir, watch: true });
|
|
347
|
+
return "export interface Foo { n: number }";
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
]);
|
|
351
|
+
const languageService = ts.createLanguageService(host);
|
|
352
|
+
const program = ts.createProgram([entryFile, depFile], {
|
|
353
|
+
strict: true,
|
|
354
|
+
noEmit: true,
|
|
355
|
+
target: ts.ScriptTarget.ESNext,
|
|
356
|
+
module: ts.ModuleKind.ESNext,
|
|
357
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
358
|
+
skipLibCheck: true,
|
|
359
|
+
});
|
|
360
|
+
const createSession = () => createTypeInfoApiSession({ ts, program });
|
|
361
|
+
attachLanguageServiceAdapter({
|
|
362
|
+
ts,
|
|
363
|
+
languageService,
|
|
364
|
+
languageServiceHost: host,
|
|
365
|
+
resolver: manager,
|
|
366
|
+
projectRoot: dir,
|
|
367
|
+
debounceMs: 50,
|
|
368
|
+
watchHost,
|
|
369
|
+
createTypeInfoApiSession: createSession,
|
|
370
|
+
});
|
|
371
|
+
languageService.getSemanticDiagnostics(entryFile);
|
|
372
|
+
expect(buildCount).toBe(1);
|
|
373
|
+
watchCallback?.();
|
|
374
|
+
watchCallback?.();
|
|
375
|
+
watchCallback?.();
|
|
376
|
+
vi.advanceTimersByTime(50);
|
|
377
|
+
languageService.getSemanticDiagnostics(entryFile);
|
|
378
|
+
expect(buildCount).toBe(2);
|
|
379
|
+
vi.useRealTimers();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("evicts records when importer is no longer in getScriptFileNames", () => {
|
|
383
|
+
const dir = createTempDir();
|
|
384
|
+
const entryFile = join(dir, "entry.ts");
|
|
385
|
+
const otherFile = join(dir, "other.ts");
|
|
386
|
+
writeFileSync(
|
|
387
|
+
entryFile,
|
|
388
|
+
`import type { Foo } from "virtual:foo"; export const x: Foo = { n: 1 };`,
|
|
389
|
+
"utf8",
|
|
390
|
+
);
|
|
391
|
+
writeFileSync(otherFile, 'import "virtual:other"; export const y = 1;', "utf8");
|
|
392
|
+
|
|
393
|
+
let scriptFileNames = [entryFile, otherFile];
|
|
394
|
+
const files = new Map<string, { version: number; content: string }>([
|
|
395
|
+
[entryFile, { version: 1, content: ts.sys.readFile(entryFile) ?? "" }],
|
|
396
|
+
[otherFile, { version: 1, content: ts.sys.readFile(otherFile) ?? "" }],
|
|
397
|
+
]);
|
|
398
|
+
|
|
399
|
+
const host: ts.LanguageServiceHost = {
|
|
400
|
+
getCompilationSettings: () => ({
|
|
401
|
+
strict: true,
|
|
402
|
+
noEmit: true,
|
|
403
|
+
target: ts.ScriptTarget.ESNext,
|
|
404
|
+
module: ts.ModuleKind.ESNext,
|
|
405
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
406
|
+
skipLibCheck: true,
|
|
407
|
+
}),
|
|
408
|
+
getScriptFileNames: () => [...scriptFileNames],
|
|
409
|
+
getScriptVersion: (fileName) => String(files.get(fileName)?.version ?? 0),
|
|
410
|
+
getScriptSnapshot: (fileName) => {
|
|
411
|
+
const content = files.get(fileName)?.content ?? ts.sys.readFile(fileName);
|
|
412
|
+
if (!content) return undefined;
|
|
413
|
+
return ts.ScriptSnapshot.fromString(content);
|
|
414
|
+
},
|
|
415
|
+
getCurrentDirectory: () => dir,
|
|
416
|
+
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
|
|
417
|
+
fileExists: (fileName) => files.has(fileName) || ts.sys.fileExists(fileName),
|
|
418
|
+
readFile: (fileName) => files.get(fileName)?.content ?? ts.sys.readFile(fileName),
|
|
419
|
+
readDirectory: (...args: Parameters<typeof ts.sys.readDirectory>) =>
|
|
420
|
+
ts.sys.readDirectory(...args),
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const manager = new PluginManager([
|
|
424
|
+
{
|
|
425
|
+
name: "virtual",
|
|
426
|
+
shouldResolve: (id) => id === "virtual:foo",
|
|
427
|
+
build: () => "export interface Foo { n: number }",
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
name: "virtual-other",
|
|
431
|
+
shouldResolve: (id) => id === "virtual:other",
|
|
432
|
+
build: () => "export const other = 1;",
|
|
433
|
+
},
|
|
434
|
+
]);
|
|
435
|
+
|
|
436
|
+
const languageService = ts.createLanguageService(host);
|
|
437
|
+
attachLanguageServiceAdapter({
|
|
438
|
+
ts,
|
|
439
|
+
languageService,
|
|
440
|
+
languageServiceHost: host,
|
|
441
|
+
resolver: manager,
|
|
442
|
+
projectRoot: dir,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
languageService.getSemanticDiagnostics(entryFile);
|
|
446
|
+
const program = languageService.getProgram();
|
|
447
|
+
const virtualFiles =
|
|
448
|
+
program?.getSourceFiles().filter((sf) => sf.fileName.includes("__virtual_")) ?? [];
|
|
449
|
+
expect(virtualFiles.length).toBeGreaterThan(0);
|
|
450
|
+
const virtualFileName = virtualFiles[0].fileName;
|
|
451
|
+
|
|
452
|
+
scriptFileNames = [otherFile];
|
|
453
|
+
|
|
454
|
+
languageService.getSemanticDiagnostics(otherFile);
|
|
455
|
+
|
|
456
|
+
const snapshot = host.getScriptSnapshot?.(virtualFileName);
|
|
457
|
+
expect(snapshot).toBeUndefined();
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("dispose then getScriptSnapshot does not throw and returns original behavior", () => {
|
|
461
|
+
const dir = createTempDir();
|
|
462
|
+
const entryFile = join(dir, "entry.ts");
|
|
463
|
+
writeFileSync(
|
|
464
|
+
entryFile,
|
|
465
|
+
`import type { Foo } from "virtual:foo"; export const x: Foo = { n: 1 };`,
|
|
466
|
+
"utf8",
|
|
467
|
+
);
|
|
468
|
+
const files = new Map<string, { version: number; content: string }>([
|
|
469
|
+
[entryFile, { version: 1, content: ts.sys.readFile(entryFile) ?? "" }],
|
|
470
|
+
]);
|
|
471
|
+
const host: ts.LanguageServiceHost = {
|
|
472
|
+
getCompilationSettings: () => ({
|
|
473
|
+
strict: true,
|
|
474
|
+
noEmit: true,
|
|
475
|
+
target: ts.ScriptTarget.ESNext,
|
|
476
|
+
module: ts.ModuleKind.ESNext,
|
|
477
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
478
|
+
skipLibCheck: true,
|
|
479
|
+
}),
|
|
480
|
+
getScriptFileNames: () => [...files.keys()],
|
|
481
|
+
getScriptVersion: (fileName) => String(files.get(fileName)?.version ?? 0),
|
|
482
|
+
getScriptSnapshot: (fileName) => {
|
|
483
|
+
const content = files.get(fileName)?.content ?? ts.sys.readFile(fileName);
|
|
484
|
+
if (!content) return undefined;
|
|
485
|
+
return ts.ScriptSnapshot.fromString(content);
|
|
486
|
+
},
|
|
487
|
+
getCurrentDirectory: () => dir,
|
|
488
|
+
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
|
|
489
|
+
fileExists: (fileName) => files.has(fileName) || ts.sys.fileExists(fileName),
|
|
490
|
+
readFile: (fileName) => files.get(fileName)?.content ?? ts.sys.readFile(fileName),
|
|
491
|
+
readDirectory: (...args: Parameters<typeof ts.sys.readDirectory>) =>
|
|
492
|
+
ts.sys.readDirectory(...args),
|
|
493
|
+
};
|
|
494
|
+
const manager = new PluginManager([
|
|
495
|
+
{
|
|
496
|
+
name: "virtual",
|
|
497
|
+
shouldResolve: (id) => id === "virtual:foo",
|
|
498
|
+
build: () => "export interface Foo { n: number }",
|
|
499
|
+
},
|
|
500
|
+
]);
|
|
501
|
+
const languageService = ts.createLanguageService(host);
|
|
502
|
+
const adapter = attachLanguageServiceAdapter({
|
|
503
|
+
ts,
|
|
504
|
+
languageService,
|
|
505
|
+
languageServiceHost: host,
|
|
506
|
+
resolver: manager,
|
|
507
|
+
projectRoot: dir,
|
|
508
|
+
});
|
|
509
|
+
languageService.getProgram();
|
|
510
|
+
const program = languageService.getProgram();
|
|
511
|
+
const virtualFile = program?.getSourceFiles().find((sf) => sf.fileName.includes("__virtual_"));
|
|
512
|
+
expect(virtualFile).toBeDefined();
|
|
513
|
+
const virtualFileName = virtualFile!.fileName;
|
|
514
|
+
|
|
515
|
+
adapter.dispose();
|
|
516
|
+
|
|
517
|
+
expect(() => host.getScriptSnapshot?.(virtualFileName)).not.toThrow();
|
|
518
|
+
const after = host.getScriptSnapshot?.(virtualFileName);
|
|
519
|
+
expect(after).toBeUndefined();
|
|
520
|
+
});
|
|
521
|
+
});
|