@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,631 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import type * as ts from "typescript";
|
|
4
|
+
import {
|
|
5
|
+
type LanguageServiceAdapterOptions,
|
|
6
|
+
type LanguageServiceWatchHost,
|
|
7
|
+
type VirtualModuleAdapterHandle,
|
|
8
|
+
} from "./types.js";
|
|
9
|
+
import { unlinkSync } from "node:fs";
|
|
10
|
+
import {
|
|
11
|
+
materializeVirtualFile,
|
|
12
|
+
rewriteSourceForPreviewLocation,
|
|
13
|
+
} from "./internal/materializeVirtualFile.js";
|
|
14
|
+
import {
|
|
15
|
+
createVirtualRecordStore,
|
|
16
|
+
toResolvedModule,
|
|
17
|
+
type MutableVirtualRecord,
|
|
18
|
+
type ResolveRecordResult,
|
|
19
|
+
} from "./internal/VirtualRecordStore.js";
|
|
20
|
+
import {
|
|
21
|
+
VIRTUAL_MODULE_URI_SCHEME,
|
|
22
|
+
VIRTUAL_NODE_MODULES_RELATIVE,
|
|
23
|
+
} from "./internal/path.js";
|
|
24
|
+
import { Mutable } from "effect/Types";
|
|
25
|
+
|
|
26
|
+
/** Prefix VSCode uses when sending non-file URIs to tsserver (query params are dropped). */
|
|
27
|
+
const IN_MEMORY_RESOURCE_PREFIX = "^";
|
|
28
|
+
|
|
29
|
+
/** Schemes used by VSCode extension for virtual module preview docs. */
|
|
30
|
+
const PREVIEW_SCHEMES = ["virtual-module", VIRTUAL_MODULE_URI_SCHEME] as const;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse a fileName that may be:
|
|
34
|
+
* 1) Full URI: virtual-module:///module.ts?id=x&importer=y (query may be dropped by VSCode)
|
|
35
|
+
* 2) tsserver path: ^/virtual-module/ts-nul-authority/path/to/__virtual_plugin_hash.ts
|
|
36
|
+
*/
|
|
37
|
+
function parsePreviewUri(
|
|
38
|
+
fileName: string,
|
|
39
|
+
): { id: string; importer: string } | { virtualPath: string } | undefined {
|
|
40
|
+
const pathBased = tryParsePathBasedFromTsServer(fileName);
|
|
41
|
+
if (pathBased) return pathBased;
|
|
42
|
+
|
|
43
|
+
if (!fileName.includes("://")) return undefined;
|
|
44
|
+
try {
|
|
45
|
+
const url = new URL(fileName);
|
|
46
|
+
if (
|
|
47
|
+
!PREVIEW_SCHEMES.includes(url.protocol.replace(":", "") as (typeof PREVIEW_SCHEMES)[number])
|
|
48
|
+
)
|
|
49
|
+
return undefined;
|
|
50
|
+
const id = url.searchParams.get("id");
|
|
51
|
+
const importerRaw = url.searchParams.get("importer");
|
|
52
|
+
if (!id || !importerRaw) return undefined;
|
|
53
|
+
const importer = importerRaw.startsWith("file:")
|
|
54
|
+
? (() => {
|
|
55
|
+
try {
|
|
56
|
+
return fileURLToPath(importerRaw);
|
|
57
|
+
} catch {
|
|
58
|
+
return importerRaw;
|
|
59
|
+
}
|
|
60
|
+
})()
|
|
61
|
+
: importerRaw;
|
|
62
|
+
return { id, importer };
|
|
63
|
+
} catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function tryParsePathBasedFromTsServer(fileName: string): { virtualPath: string } | undefined {
|
|
69
|
+
if (!fileName.startsWith(IN_MEMORY_RESOURCE_PREFIX + "/")) return undefined;
|
|
70
|
+
const rest = fileName.slice(IN_MEMORY_RESOURCE_PREFIX.length);
|
|
71
|
+
const parts = rest.split("/");
|
|
72
|
+
if (parts.length < 4) return undefined;
|
|
73
|
+
const [scheme] = [parts[1], parts[2]];
|
|
74
|
+
if (!PREVIEW_SCHEMES.includes(scheme as (typeof PREVIEW_SCHEMES)[number])) return undefined;
|
|
75
|
+
const path = "/" + parts.slice(3).join("/");
|
|
76
|
+
if (!path.includes("__virtual_")) return undefined;
|
|
77
|
+
return { virtualPath: path };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface ProjectServiceLike {
|
|
81
|
+
getOrCreateOpenScriptInfo?(
|
|
82
|
+
fileName: string,
|
|
83
|
+
fileContent: string | undefined,
|
|
84
|
+
scriptKind: ts.ScriptKind,
|
|
85
|
+
hasMixedContent: boolean,
|
|
86
|
+
projectRootPath: string | undefined,
|
|
87
|
+
): unknown;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const ADAPTER_DIAGNOSTIC_CODE = 99001;
|
|
91
|
+
|
|
92
|
+
const toTsDiagnostic = (
|
|
93
|
+
tsMod: typeof import("typescript"),
|
|
94
|
+
message: string,
|
|
95
|
+
file?: ts.SourceFile,
|
|
96
|
+
): ts.Diagnostic => ({
|
|
97
|
+
category: tsMod.DiagnosticCategory.Error,
|
|
98
|
+
code: ADAPTER_DIAGNOSTIC_CODE,
|
|
99
|
+
file,
|
|
100
|
+
start: 0,
|
|
101
|
+
length: 0,
|
|
102
|
+
messageText: message,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export const attachLanguageServiceAdapter = (
|
|
106
|
+
options: LanguageServiceAdapterOptions,
|
|
107
|
+
): VirtualModuleAdapterHandle => {
|
|
108
|
+
if (typeof options.projectRoot !== "string" || options.projectRoot.trim() === "") {
|
|
109
|
+
throw new Error("projectRoot must be a non-empty string");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const host = options.languageServiceHost as ts.LanguageServiceHost & Record<string, unknown>;
|
|
113
|
+
const watchHost = (options.watchHost ??
|
|
114
|
+
(options.languageServiceHost as unknown as LanguageServiceWatchHost)) as
|
|
115
|
+
| LanguageServiceWatchHost
|
|
116
|
+
| undefined;
|
|
117
|
+
|
|
118
|
+
const diagnosticsByFile = new Map<string, ts.Diagnostic[]>();
|
|
119
|
+
let epoch = 0;
|
|
120
|
+
let inResolution = false;
|
|
121
|
+
let inResolveRecord = false;
|
|
122
|
+
let pendingRetry = false;
|
|
123
|
+
|
|
124
|
+
const originalGetScriptFileNames = host.getScriptFileNames?.bind(host);
|
|
125
|
+
const originalResolveModuleNameLiterals = host.resolveModuleNameLiterals?.bind(host);
|
|
126
|
+
const originalResolveModuleNames = host.resolveModuleNames?.bind(host);
|
|
127
|
+
const originalGetScriptSnapshot = host.getScriptSnapshot?.bind(host);
|
|
128
|
+
const originalGetScriptVersion = host.getScriptVersion?.bind(host);
|
|
129
|
+
const originalGetProjectVersion = host.getProjectVersion?.bind(host);
|
|
130
|
+
const originalFileExists = host.fileExists?.bind(host);
|
|
131
|
+
const originalReadFile = host.readFile?.bind(host);
|
|
132
|
+
const originalGetSemanticDiagnostics = options.languageService.getSemanticDiagnostics.bind(
|
|
133
|
+
options.languageService,
|
|
134
|
+
);
|
|
135
|
+
const originalGetSyntacticDiagnostics = options.languageService.getSyntacticDiagnostics.bind(
|
|
136
|
+
options.languageService,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const addDiagnosticForFile = (filePath: string, message: string): void => {
|
|
140
|
+
const diagnostic = toTsDiagnostic(options.ts, message);
|
|
141
|
+
const diagnostics = diagnosticsByFile.get(filePath) ?? [];
|
|
142
|
+
diagnostics.push(diagnostic);
|
|
143
|
+
diagnosticsByFile.set(filePath, diagnostics);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const clearDiagnosticsForFile = (filePath: string): void => {
|
|
147
|
+
diagnosticsByFile.delete(filePath);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const store = createVirtualRecordStore({
|
|
151
|
+
projectRoot: options.projectRoot,
|
|
152
|
+
resolver: options.resolver,
|
|
153
|
+
createTypeInfoApiSession: options.createTypeInfoApiSession,
|
|
154
|
+
debounceMs: options.debounceMs,
|
|
155
|
+
watchHost,
|
|
156
|
+
shouldEvictRecord: (record) => {
|
|
157
|
+
const currentFiles = new Set(originalGetScriptFileNames ? originalGetScriptFileNames() : []);
|
|
158
|
+
return !currentFiles.has(record.importer);
|
|
159
|
+
},
|
|
160
|
+
onFlushStale: () => {
|
|
161
|
+
epoch += 1;
|
|
162
|
+
},
|
|
163
|
+
onBeforeResolve: () => {
|
|
164
|
+
inResolveRecord = true;
|
|
165
|
+
},
|
|
166
|
+
onAfterResolve: () => {
|
|
167
|
+
inResolveRecord = false;
|
|
168
|
+
},
|
|
169
|
+
onRecordResolved: (record) => {
|
|
170
|
+
clearDiagnosticsForFile(record.importer);
|
|
171
|
+
},
|
|
172
|
+
onEvictRecord: (record) => {
|
|
173
|
+
clearDiagnosticsForFile(record.importer);
|
|
174
|
+
if (record.virtualFileName.includes(VIRTUAL_NODE_MODULES_RELATIVE)) {
|
|
175
|
+
try {
|
|
176
|
+
unlinkSync(record.virtualFileName);
|
|
177
|
+
} catch {
|
|
178
|
+
/* ignore if already deleted or missing */
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const { recordsByVirtualFile } = store;
|
|
185
|
+
|
|
186
|
+
const getOrBuildRecord = (id: string, importer: string): ResolveRecordResult => {
|
|
187
|
+
store.evictStaleImporters();
|
|
188
|
+
|
|
189
|
+
if (inResolveRecord) {
|
|
190
|
+
return {
|
|
191
|
+
status: "error",
|
|
192
|
+
diagnostic: {
|
|
193
|
+
code: "re-entrant-resolution",
|
|
194
|
+
pluginName: "",
|
|
195
|
+
message:
|
|
196
|
+
"Re-entrant resolution not allowed; plugins must not trigger module resolution during build()",
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return store.getOrBuildRecord(id, importer);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const rebuildRecordIfNeeded = (record: MutableVirtualRecord): MutableVirtualRecord => {
|
|
205
|
+
if (!record.stale) {
|
|
206
|
+
return record;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const rebuilt = store.resolveRecord(record.id, record.importer, record);
|
|
210
|
+
if (rebuilt.status === "resolved") {
|
|
211
|
+
clearDiagnosticsForFile(record.importer);
|
|
212
|
+
return rebuilt.record;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (rebuilt.status === "error") {
|
|
216
|
+
const diagnostic = toTsDiagnostic(
|
|
217
|
+
options.ts,
|
|
218
|
+
`Virtual module rebuild failed: ${rebuilt.diagnostic.message}`,
|
|
219
|
+
);
|
|
220
|
+
const diagnostics = diagnosticsByFile.get(record.importer) ?? [];
|
|
221
|
+
diagnostics.push(diagnostic);
|
|
222
|
+
diagnosticsByFile.set(record.importer, diagnostics);
|
|
223
|
+
}
|
|
224
|
+
return record;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const fallbackResolveModule = (
|
|
228
|
+
moduleName: string,
|
|
229
|
+
containingFile: string,
|
|
230
|
+
compilerOptions: ts.CompilerOptions | undefined,
|
|
231
|
+
): ts.ResolvedModuleFull | undefined => {
|
|
232
|
+
const result = options.ts.resolveModuleName(moduleName, containingFile, compilerOptions ?? {}, {
|
|
233
|
+
fileExists: (path) => host.fileExists?.(path) ?? false,
|
|
234
|
+
readFile: (path) => host.readFile?.(path),
|
|
235
|
+
directoryExists: (path) =>
|
|
236
|
+
host.directoryExists?.(path) ?? options.ts.sys.directoryExists(path),
|
|
237
|
+
getCurrentDirectory: () =>
|
|
238
|
+
host.getCurrentDirectory?.() ?? options.ts.sys.getCurrentDirectory(),
|
|
239
|
+
getDirectories: (path) => {
|
|
240
|
+
const fromHost = host.getDirectories?.(path);
|
|
241
|
+
if (fromHost !== undefined) return [...fromHost];
|
|
242
|
+
const fromSys = options.ts.sys.getDirectories?.(path);
|
|
243
|
+
return fromSys !== undefined ? [...fromSys] : [];
|
|
244
|
+
},
|
|
245
|
+
realpath: (path) => host.realpath?.(path) ?? options.ts.sys.realpath?.(path) ?? path,
|
|
246
|
+
useCaseSensitiveFileNames:
|
|
247
|
+
host.useCaseSensitiveFileNames?.() ?? options.ts.sys.useCaseSensitiveFileNames,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return result.resolvedModule as ts.ResolvedModuleFull | undefined;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
host.resolveModuleNames = (
|
|
254
|
+
moduleNames: string[],
|
|
255
|
+
containingFile: string,
|
|
256
|
+
reusedNames: string[] | undefined,
|
|
257
|
+
redirectedReference: ts.ResolvedProjectReference | undefined,
|
|
258
|
+
compilerOptions: ts.CompilerOptions,
|
|
259
|
+
containingSourceFile?: ts.SourceFile,
|
|
260
|
+
): (ts.ResolvedModule | undefined)[] => {
|
|
261
|
+
if (inResolution) {
|
|
262
|
+
if (inResolveRecord) {
|
|
263
|
+
const diagnostic = toTsDiagnostic(
|
|
264
|
+
options.ts,
|
|
265
|
+
"Re-entrant resolution not allowed; plugins must not trigger module resolution during build()",
|
|
266
|
+
);
|
|
267
|
+
const diagnostics = diagnosticsByFile.get(containingFile) ?? [];
|
|
268
|
+
diagnostics.push(diagnostic);
|
|
269
|
+
diagnosticsByFile.set(containingFile, diagnostics);
|
|
270
|
+
}
|
|
271
|
+
return moduleNames.map(() => undefined);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
inResolution = true;
|
|
275
|
+
try {
|
|
276
|
+
const parsed = parsePreviewUri(containingFile);
|
|
277
|
+
let effectiveContainingFile = containingFile;
|
|
278
|
+
let importerForVirtual = containingFile;
|
|
279
|
+
if (parsed) {
|
|
280
|
+
if ("virtualPath" in parsed) {
|
|
281
|
+
const r = recordsByVirtualFile.get(parsed.virtualPath);
|
|
282
|
+
if (r) {
|
|
283
|
+
effectiveContainingFile = r.virtualFileName;
|
|
284
|
+
importerForVirtual = r.importer;
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
const r = getOrBuildRecord(parsed.id, parsed.importer);
|
|
288
|
+
if (r.status === "resolved") {
|
|
289
|
+
effectiveContainingFile = r.record.virtualFileName;
|
|
290
|
+
importerForVirtual = r.record.importer;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const fallback = originalResolveModuleNames
|
|
296
|
+
? originalResolveModuleNames(
|
|
297
|
+
moduleNames,
|
|
298
|
+
effectiveContainingFile,
|
|
299
|
+
reusedNames,
|
|
300
|
+
redirectedReference,
|
|
301
|
+
compilerOptions,
|
|
302
|
+
containingSourceFile,
|
|
303
|
+
)
|
|
304
|
+
: moduleNames.map((moduleName) =>
|
|
305
|
+
fallbackResolveModule(moduleName, effectiveContainingFile, compilerOptions),
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
let hadVirtualError = false;
|
|
309
|
+
let hadUnresolvedVirtual = false;
|
|
310
|
+
const results = moduleNames.map((moduleName, index) => {
|
|
311
|
+
const resolved = getOrBuildRecord(moduleName, importerForVirtual);
|
|
312
|
+
if (resolved.status === "resolved") {
|
|
313
|
+
pendingRetry = false;
|
|
314
|
+
return toResolvedModule(options.ts, resolved.record.virtualFileName);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (resolved.status === "error") {
|
|
318
|
+
hadVirtualError = true;
|
|
319
|
+
addDiagnosticForFile(containingFile, resolved.diagnostic.message);
|
|
320
|
+
if (resolved.diagnostic.code === "re-entrant-resolution") {
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (resolved.status === "unresolved" && moduleName.includes(":")) {
|
|
326
|
+
hadUnresolvedVirtual = true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return fallback[index];
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if ((hadVirtualError || hadUnresolvedVirtual) && !pendingRetry) {
|
|
333
|
+
pendingRetry = true;
|
|
334
|
+
epoch += 1;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return results;
|
|
338
|
+
} finally {
|
|
339
|
+
inResolution = false;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const assignResolveModuleNameLiterals = (
|
|
344
|
+
moduleLiterals: readonly { readonly text: string }[],
|
|
345
|
+
containingFile: string,
|
|
346
|
+
redirectedReference: ts.ResolvedProjectReference | undefined,
|
|
347
|
+
compilerOptions: ts.CompilerOptions,
|
|
348
|
+
containingSourceFile: ts.SourceFile | undefined,
|
|
349
|
+
reusedNames?: readonly { readonly text: string }[],
|
|
350
|
+
): readonly ts.ResolvedModuleWithFailedLookupLocations[] => {
|
|
351
|
+
if (inResolution) {
|
|
352
|
+
if (inResolveRecord) {
|
|
353
|
+
const diagnostic = toTsDiagnostic(
|
|
354
|
+
options.ts,
|
|
355
|
+
"Re-entrant resolution not allowed; plugins must not trigger module resolution during build()",
|
|
356
|
+
);
|
|
357
|
+
const diagnostics = diagnosticsByFile.get(containingFile) ?? [];
|
|
358
|
+
diagnostics.push(diagnostic);
|
|
359
|
+
diagnosticsByFile.set(containingFile, diagnostics);
|
|
360
|
+
}
|
|
361
|
+
return moduleLiterals.map(() => ({ resolvedModule: undefined }));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
inResolution = true;
|
|
365
|
+
try {
|
|
366
|
+
const parsed = parsePreviewUri(containingFile);
|
|
367
|
+
let effectiveContainingFile = containingFile;
|
|
368
|
+
let importerForVirtual = containingFile;
|
|
369
|
+
if (parsed) {
|
|
370
|
+
if ("virtualPath" in parsed) {
|
|
371
|
+
const r = recordsByVirtualFile.get(parsed.virtualPath);
|
|
372
|
+
if (r) {
|
|
373
|
+
effectiveContainingFile = r.virtualFileName;
|
|
374
|
+
importerForVirtual = r.importer;
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
const r = getOrBuildRecord(parsed.id, parsed.importer);
|
|
378
|
+
if (r.status === "resolved") {
|
|
379
|
+
effectiveContainingFile = r.record.virtualFileName;
|
|
380
|
+
importerForVirtual = r.record.importer;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const fallback: readonly ts.ResolvedModuleWithFailedLookupLocations[] =
|
|
386
|
+
originalResolveModuleNameLiterals
|
|
387
|
+
? (originalResolveModuleNameLiterals(
|
|
388
|
+
moduleLiterals as unknown as readonly ts.StringLiteralLike[],
|
|
389
|
+
effectiveContainingFile,
|
|
390
|
+
redirectedReference,
|
|
391
|
+
compilerOptions,
|
|
392
|
+
containingSourceFile!,
|
|
393
|
+
reusedNames as readonly ts.StringLiteralLike[] | undefined,
|
|
394
|
+
) as readonly ts.ResolvedModuleWithFailedLookupLocations[])
|
|
395
|
+
: moduleLiterals.map((moduleLiteral) => ({
|
|
396
|
+
resolvedModule: fallbackResolveModule(
|
|
397
|
+
moduleLiteral.text,
|
|
398
|
+
effectiveContainingFile,
|
|
399
|
+
compilerOptions,
|
|
400
|
+
),
|
|
401
|
+
}));
|
|
402
|
+
|
|
403
|
+
let hadVirtualError = false;
|
|
404
|
+
let hadUnresolvedVirtual = false;
|
|
405
|
+
const results = moduleLiterals.map((moduleLiteral, index) => {
|
|
406
|
+
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
|
+
if (resolved.status === "resolved") {
|
|
425
|
+
pendingRetry = false;
|
|
426
|
+
return {
|
|
427
|
+
resolvedModule: toResolvedModule(options.ts, resolved.record.virtualFileName),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (resolved.status === "error") {
|
|
432
|
+
hadVirtualError = true;
|
|
433
|
+
addDiagnosticForFile(containingFile, resolved.diagnostic.message);
|
|
434
|
+
if (resolved.diagnostic.code === "re-entrant-resolution") {
|
|
435
|
+
return fallback[index];
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (resolved.status === "unresolved" && moduleLiteral.text.includes(":")) {
|
|
440
|
+
hadUnresolvedVirtual = true;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return fallback[index];
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
if ((hadVirtualError || hadUnresolvedVirtual) && !pendingRetry) {
|
|
447
|
+
pendingRetry = true;
|
|
448
|
+
epoch += 1;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return results;
|
|
452
|
+
} finally {
|
|
453
|
+
inResolution = false;
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
host.resolveModuleNameLiterals = assignResolveModuleNameLiterals;
|
|
457
|
+
|
|
458
|
+
const projectService = (host as { projectService?: ProjectServiceLike }).projectService;
|
|
459
|
+
|
|
460
|
+
host.getScriptSnapshot = (fileName: string): ts.IScriptSnapshot | undefined => {
|
|
461
|
+
let record = recordsByVirtualFile.get(fileName);
|
|
462
|
+
if (!record) {
|
|
463
|
+
const parsed = parsePreviewUri(fileName);
|
|
464
|
+
if (parsed) {
|
|
465
|
+
if ("virtualPath" in parsed) {
|
|
466
|
+
record = recordsByVirtualFile.get(parsed.virtualPath) ?? undefined;
|
|
467
|
+
} else {
|
|
468
|
+
const resolved = getOrBuildRecord(parsed.id, parsed.importer);
|
|
469
|
+
if (resolved.status === "resolved") record = resolved.record;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (!record) {
|
|
474
|
+
return originalGetScriptSnapshot?.(fileName);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const freshRecord = rebuildRecordIfNeeded(record);
|
|
478
|
+
|
|
479
|
+
let sourceToServe = freshRecord.sourceText;
|
|
480
|
+
const isNodeModulesPath = fileName.includes(VIRTUAL_NODE_MODULES_RELATIVE);
|
|
481
|
+
if (isNodeModulesPath) {
|
|
482
|
+
sourceToServe = rewriteSourceForPreviewLocation(
|
|
483
|
+
freshRecord.sourceText,
|
|
484
|
+
freshRecord.importer,
|
|
485
|
+
fileName,
|
|
486
|
+
);
|
|
487
|
+
materializeVirtualFile(fileName, freshRecord.importer, freshRecord.sourceText);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// In tsserver, setDocument(key, path, sourceFile) requires a ScriptInfo for path (Debug.checkDefined(getScriptInfoForPath(path))).
|
|
491
|
+
// If we never register the virtual file, createProgram → acquireOrUpdateDocument → setDocument throws "Debug Failure".
|
|
492
|
+
// So when projectService exists (tsserver), always register the virtual file so setDocument can find it.
|
|
493
|
+
if (projectService?.getOrCreateOpenScriptInfo) {
|
|
494
|
+
projectService.getOrCreateOpenScriptInfo(
|
|
495
|
+
fileName,
|
|
496
|
+
sourceToServe,
|
|
497
|
+
options.ts.ScriptKind.TS,
|
|
498
|
+
false,
|
|
499
|
+
options.projectRoot,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return options.ts.ScriptSnapshot.fromString(sourceToServe);
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
if (originalGetScriptVersion) {
|
|
507
|
+
host.getScriptVersion = (fileName: string): string => {
|
|
508
|
+
let record = recordsByVirtualFile.get(fileName);
|
|
509
|
+
if (!record) {
|
|
510
|
+
const parsed = parsePreviewUri(fileName);
|
|
511
|
+
if (parsed) {
|
|
512
|
+
if ("virtualPath" in parsed) {
|
|
513
|
+
record = recordsByVirtualFile.get(parsed.virtualPath) ?? undefined;
|
|
514
|
+
} else {
|
|
515
|
+
const resolved = getOrBuildRecord(parsed.id, parsed.importer);
|
|
516
|
+
if (resolved.status === "resolved") record = resolved.record;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (!record) return originalGetScriptVersion(fileName);
|
|
521
|
+
return String(record.version);
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (originalGetProjectVersion) {
|
|
526
|
+
host.getProjectVersion = (): string => `${originalGetProjectVersion()}:vm:${epoch}`;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (originalGetScriptFileNames) {
|
|
530
|
+
host.getScriptFileNames = (): string[] => {
|
|
531
|
+
const files = originalGetScriptFileNames();
|
|
532
|
+
const virtualFiles = [...recordsByVirtualFile.keys()];
|
|
533
|
+
return [...new Set([...files, ...virtualFiles])];
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
host.fileExists = (path: string): boolean => {
|
|
538
|
+
if (recordsByVirtualFile.has(path)) return true;
|
|
539
|
+
const parsed = parsePreviewUri(path);
|
|
540
|
+
if (parsed) {
|
|
541
|
+
if ("virtualPath" in parsed) {
|
|
542
|
+
if (recordsByVirtualFile.has(parsed.virtualPath)) return true;
|
|
543
|
+
} else {
|
|
544
|
+
const resolved = getOrBuildRecord(parsed.id, parsed.importer);
|
|
545
|
+
if (resolved.status === "resolved") return true;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return originalFileExists ? originalFileExists(path) : false;
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
host.readFile = (path: string): string | undefined => {
|
|
552
|
+
let record = recordsByVirtualFile.get(path);
|
|
553
|
+
if (!record) {
|
|
554
|
+
const parsed = parsePreviewUri(path);
|
|
555
|
+
if (parsed) {
|
|
556
|
+
if ("virtualPath" in parsed) {
|
|
557
|
+
record = recordsByVirtualFile.get(parsed.virtualPath) ?? undefined;
|
|
558
|
+
} else {
|
|
559
|
+
const resolved = getOrBuildRecord(parsed.id, parsed.importer);
|
|
560
|
+
if (resolved.status === "resolved") record = resolved.record;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (record) return rebuildRecordIfNeeded(record).sourceText;
|
|
565
|
+
return originalReadFile?.(path);
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
options.languageService.getSemanticDiagnostics = (fileName: string): ts.Diagnostic[] => {
|
|
569
|
+
const diagnostics = originalGetSemanticDiagnostics(fileName);
|
|
570
|
+
const adapterDiagnostics = diagnosticsByFile.get(fileName);
|
|
571
|
+
if (!adapterDiagnostics || adapterDiagnostics.length === 0) {
|
|
572
|
+
return [...diagnostics];
|
|
573
|
+
}
|
|
574
|
+
return [...diagnostics, ...adapterDiagnostics];
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
options.languageService.getSyntacticDiagnostics = (
|
|
578
|
+
fileName: string,
|
|
579
|
+
): ts.DiagnosticWithLocation[] => {
|
|
580
|
+
const diagnostics = originalGetSyntacticDiagnostics(fileName);
|
|
581
|
+
const adapterDiagnostics = diagnosticsByFile.get(fileName);
|
|
582
|
+
if (!adapterDiagnostics || adapterDiagnostics.length === 0) {
|
|
583
|
+
return [...diagnostics];
|
|
584
|
+
}
|
|
585
|
+
const withLocation = adapterDiagnostics.filter(
|
|
586
|
+
(d): d is ts.DiagnosticWithLocation => d.file !== undefined,
|
|
587
|
+
);
|
|
588
|
+
return [...diagnostics, ...withLocation];
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
return {
|
|
592
|
+
dispose(): void {
|
|
593
|
+
for (const [virtualPath] of recordsByVirtualFile) {
|
|
594
|
+
if (virtualPath.includes(VIRTUAL_NODE_MODULES_RELATIVE)) {
|
|
595
|
+
try {
|
|
596
|
+
unlinkSync(virtualPath);
|
|
597
|
+
} catch {
|
|
598
|
+
/* ignore */
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
(host as Mutable<ts.LanguageServiceHost>).resolveModuleNameLiterals =
|
|
603
|
+
originalResolveModuleNameLiterals;
|
|
604
|
+
host.resolveModuleNames = originalResolveModuleNames;
|
|
605
|
+
if (originalGetScriptSnapshot) {
|
|
606
|
+
host.getScriptSnapshot = originalGetScriptSnapshot;
|
|
607
|
+
}
|
|
608
|
+
if (originalGetScriptVersion) {
|
|
609
|
+
host.getScriptVersion = originalGetScriptVersion;
|
|
610
|
+
}
|
|
611
|
+
if (originalGetProjectVersion) {
|
|
612
|
+
host.getProjectVersion = originalGetProjectVersion;
|
|
613
|
+
}
|
|
614
|
+
if (originalGetScriptFileNames) {
|
|
615
|
+
host.getScriptFileNames = originalGetScriptFileNames;
|
|
616
|
+
}
|
|
617
|
+
if (originalFileExists) {
|
|
618
|
+
(host as { fileExists?: (path: string) => boolean }).fileExists = originalFileExists;
|
|
619
|
+
}
|
|
620
|
+
if (originalReadFile) {
|
|
621
|
+
(host as { readFile?: (path: string) => string | undefined }).readFile = originalReadFile;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
options.languageService.getSemanticDiagnostics = originalGetSemanticDiagnostics;
|
|
625
|
+
options.languageService.getSyntacticDiagnostics = originalGetSyntacticDiagnostics;
|
|
626
|
+
|
|
627
|
+
store.dispose();
|
|
628
|
+
diagnosticsByFile.clear();
|
|
629
|
+
},
|
|
630
|
+
};
|
|
631
|
+
};
|