@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,488 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { unlinkSync } from "node:fs";
|
|
3
|
+
import { materializeVirtualFile, rewriteSourceForPreviewLocation, } from "./internal/materializeVirtualFile.js";
|
|
4
|
+
import { createVirtualRecordStore, toResolvedModule, } from "./internal/VirtualRecordStore.js";
|
|
5
|
+
import { VIRTUAL_MODULE_URI_SCHEME, VIRTUAL_NODE_MODULES_RELATIVE, } from "./internal/path.js";
|
|
6
|
+
/** Prefix VSCode uses when sending non-file URIs to tsserver (query params are dropped). */
|
|
7
|
+
const IN_MEMORY_RESOURCE_PREFIX = "^";
|
|
8
|
+
/** Schemes used by VSCode extension for virtual module preview docs. */
|
|
9
|
+
const PREVIEW_SCHEMES = ["virtual-module", VIRTUAL_MODULE_URI_SCHEME];
|
|
10
|
+
/**
|
|
11
|
+
* Parse a fileName that may be:
|
|
12
|
+
* 1) Full URI: virtual-module:///module.ts?id=x&importer=y (query may be dropped by VSCode)
|
|
13
|
+
* 2) tsserver path: ^/virtual-module/ts-nul-authority/path/to/__virtual_plugin_hash.ts
|
|
14
|
+
*/
|
|
15
|
+
function parsePreviewUri(fileName) {
|
|
16
|
+
const pathBased = tryParsePathBasedFromTsServer(fileName);
|
|
17
|
+
if (pathBased)
|
|
18
|
+
return pathBased;
|
|
19
|
+
if (!fileName.includes("://"))
|
|
20
|
+
return undefined;
|
|
21
|
+
try {
|
|
22
|
+
const url = new URL(fileName);
|
|
23
|
+
if (!PREVIEW_SCHEMES.includes(url.protocol.replace(":", "")))
|
|
24
|
+
return undefined;
|
|
25
|
+
const id = url.searchParams.get("id");
|
|
26
|
+
const importerRaw = url.searchParams.get("importer");
|
|
27
|
+
if (!id || !importerRaw)
|
|
28
|
+
return undefined;
|
|
29
|
+
const importer = importerRaw.startsWith("file:")
|
|
30
|
+
? (() => {
|
|
31
|
+
try {
|
|
32
|
+
return fileURLToPath(importerRaw);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return importerRaw;
|
|
36
|
+
}
|
|
37
|
+
})()
|
|
38
|
+
: importerRaw;
|
|
39
|
+
return { id, importer };
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function tryParsePathBasedFromTsServer(fileName) {
|
|
46
|
+
if (!fileName.startsWith(IN_MEMORY_RESOURCE_PREFIX + "/"))
|
|
47
|
+
return undefined;
|
|
48
|
+
const rest = fileName.slice(IN_MEMORY_RESOURCE_PREFIX.length);
|
|
49
|
+
const parts = rest.split("/");
|
|
50
|
+
if (parts.length < 4)
|
|
51
|
+
return undefined;
|
|
52
|
+
const [scheme] = [parts[1], parts[2]];
|
|
53
|
+
if (!PREVIEW_SCHEMES.includes(scheme))
|
|
54
|
+
return undefined;
|
|
55
|
+
const path = "/" + parts.slice(3).join("/");
|
|
56
|
+
if (!path.includes("__virtual_"))
|
|
57
|
+
return undefined;
|
|
58
|
+
return { virtualPath: path };
|
|
59
|
+
}
|
|
60
|
+
const ADAPTER_DIAGNOSTIC_CODE = 99001;
|
|
61
|
+
const toTsDiagnostic = (tsMod, message, file) => ({
|
|
62
|
+
category: tsMod.DiagnosticCategory.Error,
|
|
63
|
+
code: ADAPTER_DIAGNOSTIC_CODE,
|
|
64
|
+
file,
|
|
65
|
+
start: 0,
|
|
66
|
+
length: 0,
|
|
67
|
+
messageText: message,
|
|
68
|
+
});
|
|
69
|
+
export const attachLanguageServiceAdapter = (options) => {
|
|
70
|
+
if (typeof options.projectRoot !== "string" || options.projectRoot.trim() === "") {
|
|
71
|
+
throw new Error("projectRoot must be a non-empty string");
|
|
72
|
+
}
|
|
73
|
+
const host = options.languageServiceHost;
|
|
74
|
+
const watchHost = (options.watchHost ??
|
|
75
|
+
options.languageServiceHost);
|
|
76
|
+
const diagnosticsByFile = new Map();
|
|
77
|
+
let epoch = 0;
|
|
78
|
+
let inResolution = false;
|
|
79
|
+
let inResolveRecord = false;
|
|
80
|
+
let pendingRetry = false;
|
|
81
|
+
const originalGetScriptFileNames = host.getScriptFileNames?.bind(host);
|
|
82
|
+
const originalResolveModuleNameLiterals = host.resolveModuleNameLiterals?.bind(host);
|
|
83
|
+
const originalResolveModuleNames = host.resolveModuleNames?.bind(host);
|
|
84
|
+
const originalGetScriptSnapshot = host.getScriptSnapshot?.bind(host);
|
|
85
|
+
const originalGetScriptVersion = host.getScriptVersion?.bind(host);
|
|
86
|
+
const originalGetProjectVersion = host.getProjectVersion?.bind(host);
|
|
87
|
+
const originalFileExists = host.fileExists?.bind(host);
|
|
88
|
+
const originalReadFile = host.readFile?.bind(host);
|
|
89
|
+
const originalGetSemanticDiagnostics = options.languageService.getSemanticDiagnostics.bind(options.languageService);
|
|
90
|
+
const originalGetSyntacticDiagnostics = options.languageService.getSyntacticDiagnostics.bind(options.languageService);
|
|
91
|
+
const addDiagnosticForFile = (filePath, message) => {
|
|
92
|
+
const diagnostic = toTsDiagnostic(options.ts, message);
|
|
93
|
+
const diagnostics = diagnosticsByFile.get(filePath) ?? [];
|
|
94
|
+
diagnostics.push(diagnostic);
|
|
95
|
+
diagnosticsByFile.set(filePath, diagnostics);
|
|
96
|
+
};
|
|
97
|
+
const clearDiagnosticsForFile = (filePath) => {
|
|
98
|
+
diagnosticsByFile.delete(filePath);
|
|
99
|
+
};
|
|
100
|
+
const store = createVirtualRecordStore({
|
|
101
|
+
projectRoot: options.projectRoot,
|
|
102
|
+
resolver: options.resolver,
|
|
103
|
+
createTypeInfoApiSession: options.createTypeInfoApiSession,
|
|
104
|
+
debounceMs: options.debounceMs,
|
|
105
|
+
watchHost,
|
|
106
|
+
shouldEvictRecord: (record) => {
|
|
107
|
+
const currentFiles = new Set(originalGetScriptFileNames ? originalGetScriptFileNames() : []);
|
|
108
|
+
return !currentFiles.has(record.importer);
|
|
109
|
+
},
|
|
110
|
+
onFlushStale: () => {
|
|
111
|
+
epoch += 1;
|
|
112
|
+
},
|
|
113
|
+
onBeforeResolve: () => {
|
|
114
|
+
inResolveRecord = true;
|
|
115
|
+
},
|
|
116
|
+
onAfterResolve: () => {
|
|
117
|
+
inResolveRecord = false;
|
|
118
|
+
},
|
|
119
|
+
onRecordResolved: (record) => {
|
|
120
|
+
clearDiagnosticsForFile(record.importer);
|
|
121
|
+
},
|
|
122
|
+
onEvictRecord: (record) => {
|
|
123
|
+
clearDiagnosticsForFile(record.importer);
|
|
124
|
+
if (record.virtualFileName.includes(VIRTUAL_NODE_MODULES_RELATIVE)) {
|
|
125
|
+
try {
|
|
126
|
+
unlinkSync(record.virtualFileName);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
/* ignore if already deleted or missing */
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
const { recordsByVirtualFile } = store;
|
|
135
|
+
const getOrBuildRecord = (id, importer) => {
|
|
136
|
+
store.evictStaleImporters();
|
|
137
|
+
if (inResolveRecord) {
|
|
138
|
+
return {
|
|
139
|
+
status: "error",
|
|
140
|
+
diagnostic: {
|
|
141
|
+
code: "re-entrant-resolution",
|
|
142
|
+
pluginName: "",
|
|
143
|
+
message: "Re-entrant resolution not allowed; plugins must not trigger module resolution during build()",
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return store.getOrBuildRecord(id, importer);
|
|
148
|
+
};
|
|
149
|
+
const rebuildRecordIfNeeded = (record) => {
|
|
150
|
+
if (!record.stale) {
|
|
151
|
+
return record;
|
|
152
|
+
}
|
|
153
|
+
const rebuilt = store.resolveRecord(record.id, record.importer, record);
|
|
154
|
+
if (rebuilt.status === "resolved") {
|
|
155
|
+
clearDiagnosticsForFile(record.importer);
|
|
156
|
+
return rebuilt.record;
|
|
157
|
+
}
|
|
158
|
+
if (rebuilt.status === "error") {
|
|
159
|
+
const diagnostic = toTsDiagnostic(options.ts, `Virtual module rebuild failed: ${rebuilt.diagnostic.message}`);
|
|
160
|
+
const diagnostics = diagnosticsByFile.get(record.importer) ?? [];
|
|
161
|
+
diagnostics.push(diagnostic);
|
|
162
|
+
diagnosticsByFile.set(record.importer, diagnostics);
|
|
163
|
+
}
|
|
164
|
+
return record;
|
|
165
|
+
};
|
|
166
|
+
const fallbackResolveModule = (moduleName, containingFile, compilerOptions) => {
|
|
167
|
+
const result = options.ts.resolveModuleName(moduleName, containingFile, compilerOptions ?? {}, {
|
|
168
|
+
fileExists: (path) => host.fileExists?.(path) ?? false,
|
|
169
|
+
readFile: (path) => host.readFile?.(path),
|
|
170
|
+
directoryExists: (path) => host.directoryExists?.(path) ?? options.ts.sys.directoryExists(path),
|
|
171
|
+
getCurrentDirectory: () => host.getCurrentDirectory?.() ?? options.ts.sys.getCurrentDirectory(),
|
|
172
|
+
getDirectories: (path) => {
|
|
173
|
+
const fromHost = host.getDirectories?.(path);
|
|
174
|
+
if (fromHost !== undefined)
|
|
175
|
+
return [...fromHost];
|
|
176
|
+
const fromSys = options.ts.sys.getDirectories?.(path);
|
|
177
|
+
return fromSys !== undefined ? [...fromSys] : [];
|
|
178
|
+
},
|
|
179
|
+
realpath: (path) => host.realpath?.(path) ?? options.ts.sys.realpath?.(path) ?? path,
|
|
180
|
+
useCaseSensitiveFileNames: host.useCaseSensitiveFileNames?.() ?? options.ts.sys.useCaseSensitiveFileNames,
|
|
181
|
+
});
|
|
182
|
+
return result.resolvedModule;
|
|
183
|
+
};
|
|
184
|
+
host.resolveModuleNames = (moduleNames, containingFile, reusedNames, redirectedReference, compilerOptions, containingSourceFile) => {
|
|
185
|
+
if (inResolution) {
|
|
186
|
+
if (inResolveRecord) {
|
|
187
|
+
const diagnostic = toTsDiagnostic(options.ts, "Re-entrant resolution not allowed; plugins must not trigger module resolution during build()");
|
|
188
|
+
const diagnostics = diagnosticsByFile.get(containingFile) ?? [];
|
|
189
|
+
diagnostics.push(diagnostic);
|
|
190
|
+
diagnosticsByFile.set(containingFile, diagnostics);
|
|
191
|
+
}
|
|
192
|
+
return moduleNames.map(() => undefined);
|
|
193
|
+
}
|
|
194
|
+
inResolution = true;
|
|
195
|
+
try {
|
|
196
|
+
const parsed = parsePreviewUri(containingFile);
|
|
197
|
+
let effectiveContainingFile = containingFile;
|
|
198
|
+
let importerForVirtual = containingFile;
|
|
199
|
+
if (parsed) {
|
|
200
|
+
if ("virtualPath" in parsed) {
|
|
201
|
+
const r = recordsByVirtualFile.get(parsed.virtualPath);
|
|
202
|
+
if (r) {
|
|
203
|
+
effectiveContainingFile = r.virtualFileName;
|
|
204
|
+
importerForVirtual = r.importer;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
const r = getOrBuildRecord(parsed.id, parsed.importer);
|
|
209
|
+
if (r.status === "resolved") {
|
|
210
|
+
effectiveContainingFile = r.record.virtualFileName;
|
|
211
|
+
importerForVirtual = r.record.importer;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const fallback = originalResolveModuleNames
|
|
216
|
+
? originalResolveModuleNames(moduleNames, effectiveContainingFile, reusedNames, redirectedReference, compilerOptions, containingSourceFile)
|
|
217
|
+
: moduleNames.map((moduleName) => fallbackResolveModule(moduleName, effectiveContainingFile, compilerOptions));
|
|
218
|
+
let hadVirtualError = false;
|
|
219
|
+
let hadUnresolvedVirtual = false;
|
|
220
|
+
const results = moduleNames.map((moduleName, index) => {
|
|
221
|
+
const resolved = getOrBuildRecord(moduleName, importerForVirtual);
|
|
222
|
+
if (resolved.status === "resolved") {
|
|
223
|
+
pendingRetry = false;
|
|
224
|
+
return toResolvedModule(options.ts, resolved.record.virtualFileName);
|
|
225
|
+
}
|
|
226
|
+
if (resolved.status === "error") {
|
|
227
|
+
hadVirtualError = true;
|
|
228
|
+
addDiagnosticForFile(containingFile, resolved.diagnostic.message);
|
|
229
|
+
if (resolved.diagnostic.code === "re-entrant-resolution") {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (resolved.status === "unresolved" && moduleName.includes(":")) {
|
|
234
|
+
hadUnresolvedVirtual = true;
|
|
235
|
+
}
|
|
236
|
+
return fallback[index];
|
|
237
|
+
});
|
|
238
|
+
if ((hadVirtualError || hadUnresolvedVirtual) && !pendingRetry) {
|
|
239
|
+
pendingRetry = true;
|
|
240
|
+
epoch += 1;
|
|
241
|
+
}
|
|
242
|
+
return results;
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
inResolution = false;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
const assignResolveModuleNameLiterals = (moduleLiterals, containingFile, redirectedReference, compilerOptions, containingSourceFile, reusedNames) => {
|
|
249
|
+
if (inResolution) {
|
|
250
|
+
if (inResolveRecord) {
|
|
251
|
+
const diagnostic = toTsDiagnostic(options.ts, "Re-entrant resolution not allowed; plugins must not trigger module resolution during build()");
|
|
252
|
+
const diagnostics = diagnosticsByFile.get(containingFile) ?? [];
|
|
253
|
+
diagnostics.push(diagnostic);
|
|
254
|
+
diagnosticsByFile.set(containingFile, diagnostics);
|
|
255
|
+
}
|
|
256
|
+
return moduleLiterals.map(() => ({ resolvedModule: undefined }));
|
|
257
|
+
}
|
|
258
|
+
inResolution = true;
|
|
259
|
+
try {
|
|
260
|
+
const parsed = parsePreviewUri(containingFile);
|
|
261
|
+
let effectiveContainingFile = containingFile;
|
|
262
|
+
let importerForVirtual = containingFile;
|
|
263
|
+
if (parsed) {
|
|
264
|
+
if ("virtualPath" in parsed) {
|
|
265
|
+
const r = recordsByVirtualFile.get(parsed.virtualPath);
|
|
266
|
+
if (r) {
|
|
267
|
+
effectiveContainingFile = r.virtualFileName;
|
|
268
|
+
importerForVirtual = r.importer;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
const r = getOrBuildRecord(parsed.id, parsed.importer);
|
|
273
|
+
if (r.status === "resolved") {
|
|
274
|
+
effectiveContainingFile = r.record.virtualFileName;
|
|
275
|
+
importerForVirtual = r.record.importer;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const fallback = originalResolveModuleNameLiterals
|
|
280
|
+
? originalResolveModuleNameLiterals(moduleLiterals, effectiveContainingFile, redirectedReference, compilerOptions, containingSourceFile, reusedNames)
|
|
281
|
+
: moduleLiterals.map((moduleLiteral) => ({
|
|
282
|
+
resolvedModule: fallbackResolveModule(moduleLiteral.text, effectiveContainingFile, compilerOptions),
|
|
283
|
+
}));
|
|
284
|
+
let hadVirtualError = false;
|
|
285
|
+
let hadUnresolvedVirtual = false;
|
|
286
|
+
const results = moduleLiterals.map((moduleLiteral, index) => {
|
|
287
|
+
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
|
+
if (resolved.status === "resolved") {
|
|
303
|
+
pendingRetry = false;
|
|
304
|
+
return {
|
|
305
|
+
resolvedModule: toResolvedModule(options.ts, resolved.record.virtualFileName),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (resolved.status === "error") {
|
|
309
|
+
hadVirtualError = true;
|
|
310
|
+
addDiagnosticForFile(containingFile, resolved.diagnostic.message);
|
|
311
|
+
if (resolved.diagnostic.code === "re-entrant-resolution") {
|
|
312
|
+
return fallback[index];
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (resolved.status === "unresolved" && moduleLiteral.text.includes(":")) {
|
|
316
|
+
hadUnresolvedVirtual = true;
|
|
317
|
+
}
|
|
318
|
+
return fallback[index];
|
|
319
|
+
});
|
|
320
|
+
if ((hadVirtualError || hadUnresolvedVirtual) && !pendingRetry) {
|
|
321
|
+
pendingRetry = true;
|
|
322
|
+
epoch += 1;
|
|
323
|
+
}
|
|
324
|
+
return results;
|
|
325
|
+
}
|
|
326
|
+
finally {
|
|
327
|
+
inResolution = false;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
host.resolveModuleNameLiterals = assignResolveModuleNameLiterals;
|
|
331
|
+
const projectService = host.projectService;
|
|
332
|
+
host.getScriptSnapshot = (fileName) => {
|
|
333
|
+
let record = recordsByVirtualFile.get(fileName);
|
|
334
|
+
if (!record) {
|
|
335
|
+
const parsed = parsePreviewUri(fileName);
|
|
336
|
+
if (parsed) {
|
|
337
|
+
if ("virtualPath" in parsed) {
|
|
338
|
+
record = recordsByVirtualFile.get(parsed.virtualPath) ?? undefined;
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
const resolved = getOrBuildRecord(parsed.id, parsed.importer);
|
|
342
|
+
if (resolved.status === "resolved")
|
|
343
|
+
record = resolved.record;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (!record) {
|
|
348
|
+
return originalGetScriptSnapshot?.(fileName);
|
|
349
|
+
}
|
|
350
|
+
const freshRecord = rebuildRecordIfNeeded(record);
|
|
351
|
+
let sourceToServe = freshRecord.sourceText;
|
|
352
|
+
const isNodeModulesPath = fileName.includes(VIRTUAL_NODE_MODULES_RELATIVE);
|
|
353
|
+
if (isNodeModulesPath) {
|
|
354
|
+
sourceToServe = rewriteSourceForPreviewLocation(freshRecord.sourceText, freshRecord.importer, fileName);
|
|
355
|
+
materializeVirtualFile(fileName, freshRecord.importer, freshRecord.sourceText);
|
|
356
|
+
}
|
|
357
|
+
// In tsserver, setDocument(key, path, sourceFile) requires a ScriptInfo for path (Debug.checkDefined(getScriptInfoForPath(path))).
|
|
358
|
+
// If we never register the virtual file, createProgram → acquireOrUpdateDocument → setDocument throws "Debug Failure".
|
|
359
|
+
// So when projectService exists (tsserver), always register the virtual file so setDocument can find it.
|
|
360
|
+
if (projectService?.getOrCreateOpenScriptInfo) {
|
|
361
|
+
projectService.getOrCreateOpenScriptInfo(fileName, sourceToServe, options.ts.ScriptKind.TS, false, options.projectRoot);
|
|
362
|
+
}
|
|
363
|
+
return options.ts.ScriptSnapshot.fromString(sourceToServe);
|
|
364
|
+
};
|
|
365
|
+
if (originalGetScriptVersion) {
|
|
366
|
+
host.getScriptVersion = (fileName) => {
|
|
367
|
+
let record = recordsByVirtualFile.get(fileName);
|
|
368
|
+
if (!record) {
|
|
369
|
+
const parsed = parsePreviewUri(fileName);
|
|
370
|
+
if (parsed) {
|
|
371
|
+
if ("virtualPath" in parsed) {
|
|
372
|
+
record = recordsByVirtualFile.get(parsed.virtualPath) ?? undefined;
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
const resolved = getOrBuildRecord(parsed.id, parsed.importer);
|
|
376
|
+
if (resolved.status === "resolved")
|
|
377
|
+
record = resolved.record;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (!record)
|
|
382
|
+
return originalGetScriptVersion(fileName);
|
|
383
|
+
return String(record.version);
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
if (originalGetProjectVersion) {
|
|
387
|
+
host.getProjectVersion = () => `${originalGetProjectVersion()}:vm:${epoch}`;
|
|
388
|
+
}
|
|
389
|
+
if (originalGetScriptFileNames) {
|
|
390
|
+
host.getScriptFileNames = () => {
|
|
391
|
+
const files = originalGetScriptFileNames();
|
|
392
|
+
const virtualFiles = [...recordsByVirtualFile.keys()];
|
|
393
|
+
return [...new Set([...files, ...virtualFiles])];
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
host.fileExists = (path) => {
|
|
397
|
+
if (recordsByVirtualFile.has(path))
|
|
398
|
+
return true;
|
|
399
|
+
const parsed = parsePreviewUri(path);
|
|
400
|
+
if (parsed) {
|
|
401
|
+
if ("virtualPath" in parsed) {
|
|
402
|
+
if (recordsByVirtualFile.has(parsed.virtualPath))
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
const resolved = getOrBuildRecord(parsed.id, parsed.importer);
|
|
407
|
+
if (resolved.status === "resolved")
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return originalFileExists ? originalFileExists(path) : false;
|
|
412
|
+
};
|
|
413
|
+
host.readFile = (path) => {
|
|
414
|
+
let record = recordsByVirtualFile.get(path);
|
|
415
|
+
if (!record) {
|
|
416
|
+
const parsed = parsePreviewUri(path);
|
|
417
|
+
if (parsed) {
|
|
418
|
+
if ("virtualPath" in parsed) {
|
|
419
|
+
record = recordsByVirtualFile.get(parsed.virtualPath) ?? undefined;
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
const resolved = getOrBuildRecord(parsed.id, parsed.importer);
|
|
423
|
+
if (resolved.status === "resolved")
|
|
424
|
+
record = resolved.record;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (record)
|
|
429
|
+
return rebuildRecordIfNeeded(record).sourceText;
|
|
430
|
+
return originalReadFile?.(path);
|
|
431
|
+
};
|
|
432
|
+
options.languageService.getSemanticDiagnostics = (fileName) => {
|
|
433
|
+
const diagnostics = originalGetSemanticDiagnostics(fileName);
|
|
434
|
+
const adapterDiagnostics = diagnosticsByFile.get(fileName);
|
|
435
|
+
if (!adapterDiagnostics || adapterDiagnostics.length === 0) {
|
|
436
|
+
return [...diagnostics];
|
|
437
|
+
}
|
|
438
|
+
return [...diagnostics, ...adapterDiagnostics];
|
|
439
|
+
};
|
|
440
|
+
options.languageService.getSyntacticDiagnostics = (fileName) => {
|
|
441
|
+
const diagnostics = originalGetSyntacticDiagnostics(fileName);
|
|
442
|
+
const adapterDiagnostics = diagnosticsByFile.get(fileName);
|
|
443
|
+
if (!adapterDiagnostics || adapterDiagnostics.length === 0) {
|
|
444
|
+
return [...diagnostics];
|
|
445
|
+
}
|
|
446
|
+
const withLocation = adapterDiagnostics.filter((d) => d.file !== undefined);
|
|
447
|
+
return [...diagnostics, ...withLocation];
|
|
448
|
+
};
|
|
449
|
+
return {
|
|
450
|
+
dispose() {
|
|
451
|
+
for (const [virtualPath] of recordsByVirtualFile) {
|
|
452
|
+
if (virtualPath.includes(VIRTUAL_NODE_MODULES_RELATIVE)) {
|
|
453
|
+
try {
|
|
454
|
+
unlinkSync(virtualPath);
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
/* ignore */
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
host.resolveModuleNameLiterals =
|
|
462
|
+
originalResolveModuleNameLiterals;
|
|
463
|
+
host.resolveModuleNames = originalResolveModuleNames;
|
|
464
|
+
if (originalGetScriptSnapshot) {
|
|
465
|
+
host.getScriptSnapshot = originalGetScriptSnapshot;
|
|
466
|
+
}
|
|
467
|
+
if (originalGetScriptVersion) {
|
|
468
|
+
host.getScriptVersion = originalGetScriptVersion;
|
|
469
|
+
}
|
|
470
|
+
if (originalGetProjectVersion) {
|
|
471
|
+
host.getProjectVersion = originalGetProjectVersion;
|
|
472
|
+
}
|
|
473
|
+
if (originalGetScriptFileNames) {
|
|
474
|
+
host.getScriptFileNames = originalGetScriptFileNames;
|
|
475
|
+
}
|
|
476
|
+
if (originalFileExists) {
|
|
477
|
+
host.fileExists = originalFileExists;
|
|
478
|
+
}
|
|
479
|
+
if (originalReadFile) {
|
|
480
|
+
host.readFile = originalReadFile;
|
|
481
|
+
}
|
|
482
|
+
options.languageService.getSemanticDiagnostics = originalGetSemanticDiagnostics;
|
|
483
|
+
options.languageService.getSyntacticDiagnostics = originalGetSyntacticDiagnostics;
|
|
484
|
+
store.dispose();
|
|
485
|
+
diagnosticsByFile.clear();
|
|
486
|
+
},
|
|
487
|
+
};
|
|
488
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CreateTypeInfoApiSession, TypeTargetSpec } from "./types.js";
|
|
2
|
+
export interface CreateLanguageServiceSessionFactoryOptions {
|
|
3
|
+
readonly ts: typeof import("typescript");
|
|
4
|
+
readonly projectRoot: string;
|
|
5
|
+
readonly typeTargetSpecs?: readonly TypeTargetSpec[];
|
|
6
|
+
readonly tsconfigPath?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Creates a LanguageService-backed CreateTypeInfoApiSession factory.
|
|
10
|
+
* Use when you need type-aware virtual module resolution with a program that
|
|
11
|
+
* evolves as files change (e.g. Vite dev server, standalone tooling).
|
|
12
|
+
*
|
|
13
|
+
* Returns undefined if tsconfig cannot be found or parsed.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createLanguageServiceSessionFactory(options: CreateLanguageServiceSessionFactoryOptions): CreateTypeInfoApiSession | undefined;
|
|
16
|
+
//# sourceMappingURL=LanguageServiceSession.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LanguageServiceSession.d.ts","sourceRoot":"","sources":["../src/LanguageServiceSession.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,wBAAwB,EAAsB,cAAc,EAAE,MAAM,YAAY,CAAC;AAqB/F,MAAM,WAAW,0CAA0C;IACzD,QAAQ,CAAC,EAAE,EAAE,cAAc,YAAY,CAAC,CAAC;IACzC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IACrD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;;;;GAMG;AACH,wBAAgB,mCAAmC,CACjD,OAAO,EAAE,0CAA0C,GAClD,wBAAwB,GAAG,SAAS,CAkHtC"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a CreateTypeInfoApiSession factory backed by a TypeScript Language Service.
|
|
3
|
+
* The program evolves over time as files change (via getModifiedTime versioning),
|
|
4
|
+
* so type-aware virtual module builds stay current during development.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import { createTypeInfoApiSession } from "./TypeInfoApi.js";
|
|
9
|
+
import { ensureTypeTargetBootstrapFile, getProgramWithTypeTargetBootstrap, getTypeTargetBootstrapPath, } from "./typeTargetBootstrap.js";
|
|
10
|
+
function findTsconfig(fromDir) {
|
|
11
|
+
let dir = resolve(fromDir);
|
|
12
|
+
const root = resolve(dir, "/");
|
|
13
|
+
while (dir !== root) {
|
|
14
|
+
const candidate = join(dir, "tsconfig.json");
|
|
15
|
+
if (existsSync(candidate))
|
|
16
|
+
return candidate;
|
|
17
|
+
const parent = dirname(dir);
|
|
18
|
+
if (parent === dir)
|
|
19
|
+
break;
|
|
20
|
+
dir = parent;
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Creates a LanguageService-backed CreateTypeInfoApiSession factory.
|
|
26
|
+
* Use when you need type-aware virtual module resolution with a program that
|
|
27
|
+
* evolves as files change (e.g. Vite dev server, standalone tooling).
|
|
28
|
+
*
|
|
29
|
+
* Returns undefined if tsconfig cannot be found or parsed.
|
|
30
|
+
*/
|
|
31
|
+
export function createLanguageServiceSessionFactory(options) {
|
|
32
|
+
const { ts, projectRoot, typeTargetSpecs, tsconfigPath: explicitTsconfigPath } = options;
|
|
33
|
+
const tsconfigPath = explicitTsconfigPath ?? findTsconfig(projectRoot);
|
|
34
|
+
if (!tsconfigPath)
|
|
35
|
+
return undefined;
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
// oxlint-disable-next-line typescript/unbound-method
|
|
39
|
+
const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
|
|
40
|
+
if (configFile.error)
|
|
41
|
+
return undefined;
|
|
42
|
+
const configDir = dirname(tsconfigPath);
|
|
43
|
+
parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, configDir, undefined, tsconfigPath);
|
|
44
|
+
if (parsed.errors.length > 0)
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
let rootNames = parsed.fileNames;
|
|
51
|
+
if (typeTargetSpecs && typeTargetSpecs.length > 0) {
|
|
52
|
+
ensureTypeTargetBootstrapFile(projectRoot, typeTargetSpecs);
|
|
53
|
+
const bootstrapPath = getTypeTargetBootstrapPath(projectRoot);
|
|
54
|
+
rootNames = [...rootNames, bootstrapPath];
|
|
55
|
+
}
|
|
56
|
+
const sys = ts.sys;
|
|
57
|
+
const compilerOptions = parsed.options;
|
|
58
|
+
const host = {
|
|
59
|
+
getCompilationSettings: () => compilerOptions,
|
|
60
|
+
getScriptFileNames: () => rootNames,
|
|
61
|
+
getScriptVersion: (fileName) => {
|
|
62
|
+
const mtime = sys.getModifiedTime?.(fileName);
|
|
63
|
+
return mtime ? String(mtime.getTime()) : "0";
|
|
64
|
+
},
|
|
65
|
+
getScriptSnapshot: (fileName) => {
|
|
66
|
+
if (!sys.fileExists(fileName))
|
|
67
|
+
return undefined;
|
|
68
|
+
const content = sys.readFile(fileName);
|
|
69
|
+
return content !== undefined ? ts.ScriptSnapshot.fromString(content) : undefined;
|
|
70
|
+
},
|
|
71
|
+
getCurrentDirectory: () => dirname(tsconfigPath),
|
|
72
|
+
getDefaultLibFileName: (opts) => ts.getDefaultLibFilePath(opts),
|
|
73
|
+
directoryExists: sys.directoryExists?.bind(sys),
|
|
74
|
+
fileExists: sys.fileExists?.bind(sys),
|
|
75
|
+
readFile: sys.readFile?.bind(sys),
|
|
76
|
+
readDirectory: sys.readDirectory?.bind(sys),
|
|
77
|
+
};
|
|
78
|
+
const languageService = ts.createLanguageService(host);
|
|
79
|
+
const createTypeInfoApiSessionFn = ({ id: _id, importer: _importer, }) => {
|
|
80
|
+
let session = null;
|
|
81
|
+
let apiUsed = false;
|
|
82
|
+
const getSession = () => {
|
|
83
|
+
if (session)
|
|
84
|
+
return session;
|
|
85
|
+
const program = languageService.getProgram();
|
|
86
|
+
if (!program) {
|
|
87
|
+
throw new Error("TypeInfo session creation failed: Program not yet available from Language Service. Retry when project is loaded.");
|
|
88
|
+
}
|
|
89
|
+
const programWithBootstrap = getProgramWithTypeTargetBootstrap(ts, program, projectRoot, typeTargetSpecs);
|
|
90
|
+
session = createTypeInfoApiSession({
|
|
91
|
+
ts,
|
|
92
|
+
program: programWithBootstrap,
|
|
93
|
+
...(typeTargetSpecs && typeTargetSpecs.length > 0
|
|
94
|
+
? { typeTargetSpecs, failWhenNoTargetsResolved: false }
|
|
95
|
+
: {}),
|
|
96
|
+
});
|
|
97
|
+
return session;
|
|
98
|
+
};
|
|
99
|
+
return {
|
|
100
|
+
api: {
|
|
101
|
+
file: (path, opts) => {
|
|
102
|
+
apiUsed = true;
|
|
103
|
+
return getSession().api.file(path, opts);
|
|
104
|
+
},
|
|
105
|
+
directory: (glob, opts) => {
|
|
106
|
+
apiUsed = true;
|
|
107
|
+
return getSession().api.directory(glob, opts);
|
|
108
|
+
},
|
|
109
|
+
resolveExport: (baseDir, filePath, exportName) => {
|
|
110
|
+
apiUsed = true;
|
|
111
|
+
return getSession().api.resolveExport(baseDir, filePath, exportName);
|
|
112
|
+
},
|
|
113
|
+
isAssignableTo: (node, targetId, projection) => {
|
|
114
|
+
apiUsed = true;
|
|
115
|
+
return getSession().api.isAssignableTo(node, targetId, projection);
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
consumeDependencies: () => (apiUsed ? getSession().consumeDependencies() : []),
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
return createTypeInfoApiSessionFn;
|
|
122
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NodeModulePluginLoadInput, NodeModulePluginLoadResult } from "./types.js";
|
|
2
|
+
export type { NodeModulePluginLoadInput };
|
|
3
|
+
export declare class NodeModulePluginLoader {
|
|
4
|
+
#private;
|
|
5
|
+
load(input: NodeModulePluginLoadInput): NodeModulePluginLoadResult;
|
|
6
|
+
loadMany(inputs: readonly NodeModulePluginLoadInput[]): readonly NodeModulePluginLoadResult[];
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=NodeModulePluginLoader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NodeModulePluginLoader.d.ts","sourceRoot":"","sources":["../src/NodeModulePluginLoader.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAEV,yBAAyB,EACzB,0BAA0B,EAG3B,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,yBAAyB,EAAE,CAAC;AAmF1C,qBAAa,sBAAsB;;IACjC,IAAI,CAAC,KAAK,EAAE,yBAAyB,GAAG,0BAA0B;IAwFlE,QAAQ,CAAC,MAAM,EAAE,SAAS,yBAAyB,EAAE,GAAG,SAAS,0BAA0B,EAAE;CAoF9F"}
|