@typed/virtual-modules-ts-plugin 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 +109 -0
- package/dist/plugin.d.ts +16 -0
- package/dist/plugin.js +2475 -0
- package/package.json +40 -0
- package/src/plugin.d.ts +16 -0
- package/src/plugin.test.ts +419 -0
- package/src/plugin.ts +410 -0
- package/src/sample-project.integration.test.ts +39 -0
- package/src/types.ts +18 -0
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
// oxlint-disable typescript/unbound-method
|
|
2
|
+
/**
|
|
3
|
+
* TypeScript Language Service plugin that integrates @typed/virtual-modules.
|
|
4
|
+
* Resolves virtual modules (e.g. virtual:foo) during editor type-checking.
|
|
5
|
+
*
|
|
6
|
+
* Preferred setup:
|
|
7
|
+
* 1) Define plugins/resolver in vmc.config.ts in the project root.
|
|
8
|
+
* 2) Enable this TS plugin in tsconfig.json.
|
|
9
|
+
*
|
|
10
|
+
* tsconfig.json:
|
|
11
|
+
* {
|
|
12
|
+
* "compilerOptions": {
|
|
13
|
+
* "plugins": [{
|
|
14
|
+
* "name": "@typed/virtual-modules-ts-plugin",
|
|
15
|
+
* "debounceMs": 50
|
|
16
|
+
* }]
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* Use the package name; path-style names (e.g. "../") often fail when the workspace
|
|
21
|
+
* root is a monorepo parent.
|
|
22
|
+
*
|
|
23
|
+
* Plugin definitions are loaded from vmc.config.ts.
|
|
24
|
+
*/
|
|
25
|
+
import {
|
|
26
|
+
attachLanguageServiceAdapter,
|
|
27
|
+
createTypeInfoApiSession,
|
|
28
|
+
ensureTypeTargetBootstrapFile,
|
|
29
|
+
getProgramWithTypeTargetBootstrap,
|
|
30
|
+
getTypeTargetBootstrapPath,
|
|
31
|
+
loadResolverFromVmcConfig,
|
|
32
|
+
// @ts-expect-error It's ESM being imported by CJS
|
|
33
|
+
} from "@typed/virtual-modules";
|
|
34
|
+
import { existsSync } from "node:fs";
|
|
35
|
+
import { dirname, join, resolve } from "node:path";
|
|
36
|
+
import ts, { DirectoryWatcherCallback, FileWatcherCallback } from "typescript";
|
|
37
|
+
import type { PluginCreateInfo } from "./types.js";
|
|
38
|
+
|
|
39
|
+
interface VirtualModulesTsPluginConfig {
|
|
40
|
+
readonly debounceMs?: number;
|
|
41
|
+
readonly vmcConfigPath?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type LoadedVirtualResolver = import(
|
|
45
|
+
"@typed/virtual-modules",
|
|
46
|
+
{ with: { "resolution-mode": "import" } }
|
|
47
|
+
).VirtualModuleResolver;
|
|
48
|
+
|
|
49
|
+
function findTsconfig(fromDir: string): string | undefined {
|
|
50
|
+
let dir = resolve(fromDir);
|
|
51
|
+
const root = resolve(dir, "/");
|
|
52
|
+
while (dir !== root) {
|
|
53
|
+
const candidate = join(dir, "tsconfig.json");
|
|
54
|
+
if (existsSync(candidate)) return candidate;
|
|
55
|
+
const parent = dirname(dir);
|
|
56
|
+
if (parent === dir) break;
|
|
57
|
+
dir = parent;
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createFallbackProgram(
|
|
63
|
+
tsMod: typeof import("typescript"),
|
|
64
|
+
projectRoot: string,
|
|
65
|
+
log: (msg: string) => void,
|
|
66
|
+
tsconfigPath?: string,
|
|
67
|
+
typeTargetSpecs?: ReadonlyArray<
|
|
68
|
+
import("@typed/virtual-modules", { with: { "resolution-mode": "import" } }).TypeTargetSpec
|
|
69
|
+
>,
|
|
70
|
+
): import("typescript").Program | undefined {
|
|
71
|
+
const configPath = tsconfigPath ?? findTsconfig(projectRoot);
|
|
72
|
+
if (!configPath) {
|
|
73
|
+
log(`fallback program: no tsconfig found from ${projectRoot}`);
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const configFile = tsMod.readConfigFile(configPath, tsMod.sys.readFile);
|
|
78
|
+
if (configFile.error) {
|
|
79
|
+
log(`fallback program: tsconfig read error: ${configFile.error.messageText}`);
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const configDir = dirname(configPath);
|
|
83
|
+
const parsed = tsMod.parseJsonConfigFileContent(
|
|
84
|
+
configFile.config,
|
|
85
|
+
tsMod.sys,
|
|
86
|
+
configDir,
|
|
87
|
+
undefined,
|
|
88
|
+
configPath,
|
|
89
|
+
);
|
|
90
|
+
if (parsed.errors.length > 0) {
|
|
91
|
+
log(`fallback program: tsconfig parse errors: ${parsed.errors.map((e) => e.messageText).join(", ")}`);
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
let rootNames = parsed.fileNames;
|
|
95
|
+
if (typeTargetSpecs && typeTargetSpecs.length > 0) {
|
|
96
|
+
ensureTypeTargetBootstrapFile(projectRoot, typeTargetSpecs);
|
|
97
|
+
const bootstrapPath = getTypeTargetBootstrapPath(projectRoot);
|
|
98
|
+
rootNames = [...rootNames, bootstrapPath];
|
|
99
|
+
log(`fallback program: added bootstrap ${bootstrapPath}`);
|
|
100
|
+
}
|
|
101
|
+
const program = tsMod.createProgram(
|
|
102
|
+
rootNames,
|
|
103
|
+
parsed.options,
|
|
104
|
+
tsMod.createCompilerHost(parsed.options),
|
|
105
|
+
);
|
|
106
|
+
log(`fallback program: created with ${rootNames.length} root files`);
|
|
107
|
+
return program;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
log(`fallback program: exception: ${err instanceof Error ? err.message : String(err)}`);
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function init(modules: { typescript: typeof import("typescript") }): {
|
|
115
|
+
create: (info: PluginCreateInfo) => import("typescript").LanguageService;
|
|
116
|
+
} {
|
|
117
|
+
const ts = modules.typescript;
|
|
118
|
+
|
|
119
|
+
function create(info: PluginCreateInfo) {
|
|
120
|
+
const config = (info.config ?? {}) as VirtualModulesTsPluginConfig;
|
|
121
|
+
const logger = (
|
|
122
|
+
info.project as { projectService?: { logger?: { info?: (s: string) => void } } }
|
|
123
|
+
)?.projectService?.logger;
|
|
124
|
+
const log = (msg: string) =>
|
|
125
|
+
logger?.info?.(`[@typed/virtual-modules-ts-plugin] ${msg}`);
|
|
126
|
+
|
|
127
|
+
const project = info.project as {
|
|
128
|
+
getCurrentDirectory?: () => string;
|
|
129
|
+
configFilePath?: string;
|
|
130
|
+
};
|
|
131
|
+
const projectRoot =
|
|
132
|
+
typeof project.configFilePath === "string" && project.configFilePath.length > 0
|
|
133
|
+
? dirname(project.configFilePath)
|
|
134
|
+
: typeof project.getCurrentDirectory === "function"
|
|
135
|
+
? project.getCurrentDirectory()
|
|
136
|
+
: process.cwd();
|
|
137
|
+
|
|
138
|
+
log(`create: projectRoot=${projectRoot}`);
|
|
139
|
+
|
|
140
|
+
const debounceMs =
|
|
141
|
+
typeof config.debounceMs === "number" &&
|
|
142
|
+
Number.isFinite(config.debounceMs) &&
|
|
143
|
+
config.debounceMs >= 0
|
|
144
|
+
? config.debounceMs
|
|
145
|
+
: 50;
|
|
146
|
+
if (
|
|
147
|
+
config.debounceMs !== undefined &&
|
|
148
|
+
(typeof config.debounceMs !== "number" || !Number.isFinite(config.debounceMs))
|
|
149
|
+
) {
|
|
150
|
+
log("Ignoring invalid debounceMs; expected finite number");
|
|
151
|
+
}
|
|
152
|
+
const vmcConfigPath =
|
|
153
|
+
typeof config.vmcConfigPath === "string" && config.vmcConfigPath.trim().length > 0
|
|
154
|
+
? config.vmcConfigPath
|
|
155
|
+
: undefined;
|
|
156
|
+
if (config.vmcConfigPath !== undefined && vmcConfigPath === undefined) {
|
|
157
|
+
log("Ignoring invalid vmcConfigPath; expected non-empty string");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let resolver: LoadedVirtualResolver | undefined;
|
|
161
|
+
const loadedResolver = loadResolverFromVmcConfig({
|
|
162
|
+
projectRoot,
|
|
163
|
+
ts,
|
|
164
|
+
...(vmcConfigPath ? { configPath: vmcConfigPath } : {}),
|
|
165
|
+
});
|
|
166
|
+
log(`vmc: status=${loadedResolver.status}`);
|
|
167
|
+
if (loadedResolver.status === "error") {
|
|
168
|
+
log(`vmc error: ${loadedResolver.message}`);
|
|
169
|
+
return info.languageService;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (loadedResolver.status === "loaded") {
|
|
173
|
+
for (const pluginLoadError of loadedResolver.pluginLoadErrors) {
|
|
174
|
+
log(`plugin load error: "${pluginLoadError.specifier}": ${pluginLoadError.message}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
resolver = loadedResolver.resolver as LoadedVirtualResolver | undefined;
|
|
178
|
+
if (!resolver) {
|
|
179
|
+
log(`${loadedResolver.path} has no resolver/plugins`);
|
|
180
|
+
return info.languageService;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!resolver || loadedResolver.status === "not-found") {
|
|
185
|
+
log("vmc.config.ts not found; virtual module resolution disabled");
|
|
186
|
+
return info.languageService;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
log("Virtual module resolver initialized");
|
|
190
|
+
|
|
191
|
+
const typeTargetSpecs = loadedResolver.typeTargetSpecs ?? [];
|
|
192
|
+
log(`typeTargetSpecs: ${typeTargetSpecs.length} specs`);
|
|
193
|
+
|
|
194
|
+
const projectConfigPath = (info.project as { configFilePath?: string }).configFilePath;
|
|
195
|
+
const tsconfigPath =
|
|
196
|
+
typeof projectConfigPath === "string" && projectConfigPath.length > 0
|
|
197
|
+
? projectConfigPath
|
|
198
|
+
: undefined;
|
|
199
|
+
|
|
200
|
+
let cachedFallbackProgram: ts.Program | undefined = createFallbackProgram(
|
|
201
|
+
ts,
|
|
202
|
+
projectRoot,
|
|
203
|
+
log,
|
|
204
|
+
tsconfigPath,
|
|
205
|
+
typeTargetSpecs,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Pre-validate that TypeInfoApiSession can be created from the fallback program.
|
|
209
|
+
// This catches issues early (missing type targets, checker errors) and caches the result.
|
|
210
|
+
let preCreatedSession: ReturnType<typeof createTypeInfoApiSession> | undefined;
|
|
211
|
+
if (cachedFallbackProgram) {
|
|
212
|
+
try {
|
|
213
|
+
const programWithBootstrap = getProgramWithTypeTargetBootstrap(
|
|
214
|
+
ts,
|
|
215
|
+
cachedFallbackProgram,
|
|
216
|
+
projectRoot,
|
|
217
|
+
typeTargetSpecs,
|
|
218
|
+
);
|
|
219
|
+
preCreatedSession = createTypeInfoApiSession({
|
|
220
|
+
ts,
|
|
221
|
+
program: programWithBootstrap,
|
|
222
|
+
...(typeTargetSpecs.length > 0
|
|
223
|
+
? { typeTargetSpecs, failWhenNoTargetsResolved: false }
|
|
224
|
+
: {}),
|
|
225
|
+
});
|
|
226
|
+
log("pre-created TypeInfoApiSession OK");
|
|
227
|
+
} catch (err) {
|
|
228
|
+
log(`pre-created TypeInfoApiSession failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const getProgramForTypeInfo = (): ts.Program | undefined => {
|
|
233
|
+
const fromLS = info.languageService.getProgram();
|
|
234
|
+
if (fromLS !== undefined) return fromLS;
|
|
235
|
+
const projectLike = info.project as { getProgram?: () => ts.Program };
|
|
236
|
+
const fromProject = projectLike.getProgram?.();
|
|
237
|
+
if (fromProject !== undefined) return fromProject;
|
|
238
|
+
if (cachedFallbackProgram !== undefined) return cachedFallbackProgram;
|
|
239
|
+
const fallback = createFallbackProgram(ts, projectRoot, log, tsconfigPath, typeTargetSpecs);
|
|
240
|
+
if (fallback !== undefined) cachedFallbackProgram = fallback;
|
|
241
|
+
return fallback;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const createTypeInfoApiSessionFactory = ({
|
|
245
|
+
id: _id,
|
|
246
|
+
importer: _importer,
|
|
247
|
+
}: {
|
|
248
|
+
id: string;
|
|
249
|
+
importer: string;
|
|
250
|
+
}) => {
|
|
251
|
+
let session: ReturnType<typeof createTypeInfoApiSession> | null = null;
|
|
252
|
+
let apiUsed = false;
|
|
253
|
+
|
|
254
|
+
const getSession = () => {
|
|
255
|
+
if (session) return session;
|
|
256
|
+
|
|
257
|
+
// Prefer the LS/project program once available, but fall back to the
|
|
258
|
+
// pre-created session from the fallback program.
|
|
259
|
+
const program = getProgramForTypeInfo();
|
|
260
|
+
if (program === undefined) {
|
|
261
|
+
if (preCreatedSession) {
|
|
262
|
+
session = preCreatedSession;
|
|
263
|
+
return session;
|
|
264
|
+
}
|
|
265
|
+
log(`getSession: no program available for ${_id}`);
|
|
266
|
+
throw new Error(
|
|
267
|
+
"TypeInfo session creation failed: Program not yet available. Retry when project is loaded.",
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const programWithBootstrap = getProgramWithTypeTargetBootstrap(
|
|
273
|
+
ts,
|
|
274
|
+
program,
|
|
275
|
+
projectRoot,
|
|
276
|
+
typeTargetSpecs,
|
|
277
|
+
);
|
|
278
|
+
session = createTypeInfoApiSession({
|
|
279
|
+
ts,
|
|
280
|
+
program: programWithBootstrap,
|
|
281
|
+
...(typeTargetSpecs.length > 0
|
|
282
|
+
? { typeTargetSpecs, failWhenNoTargetsResolved: false }
|
|
283
|
+
: {}),
|
|
284
|
+
});
|
|
285
|
+
} catch (err) {
|
|
286
|
+
// If session creation from the real program fails, fall back to the
|
|
287
|
+
// pre-created session (from the fallback program).
|
|
288
|
+
if (preCreatedSession) {
|
|
289
|
+
log(`getSession: real program session failed, using pre-created session: ${err instanceof Error ? err.message : String(err)}`);
|
|
290
|
+
session = preCreatedSession;
|
|
291
|
+
return session;
|
|
292
|
+
}
|
|
293
|
+
throw err;
|
|
294
|
+
}
|
|
295
|
+
return session;
|
|
296
|
+
};
|
|
297
|
+
return {
|
|
298
|
+
api: {
|
|
299
|
+
file: (
|
|
300
|
+
path: string,
|
|
301
|
+
opts: Parameters<ReturnType<typeof createTypeInfoApiSession>["api"]["file"]>[1],
|
|
302
|
+
) => {
|
|
303
|
+
apiUsed = true;
|
|
304
|
+
return getSession().api.file(path, opts);
|
|
305
|
+
},
|
|
306
|
+
directory: (
|
|
307
|
+
glob: string | readonly string[],
|
|
308
|
+
opts: Parameters<ReturnType<typeof createTypeInfoApiSession>["api"]["directory"]>[1],
|
|
309
|
+
) => {
|
|
310
|
+
apiUsed = true;
|
|
311
|
+
return getSession().api.directory(glob, opts);
|
|
312
|
+
},
|
|
313
|
+
resolveExport: (
|
|
314
|
+
baseDir: string,
|
|
315
|
+
filePath: string,
|
|
316
|
+
exportName: string,
|
|
317
|
+
) => {
|
|
318
|
+
apiUsed = true;
|
|
319
|
+
return getSession().api.resolveExport(baseDir, filePath, exportName);
|
|
320
|
+
},
|
|
321
|
+
isAssignableTo: (node: unknown, targetId: string, projection?: readonly unknown[]) => {
|
|
322
|
+
apiUsed = true;
|
|
323
|
+
return getSession().api.isAssignableTo(node as never, targetId, projection as never);
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
consumeDependencies: () => (apiUsed ? getSession().consumeDependencies() : ([] as const)),
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const projectWithWatch = info.project as {
|
|
331
|
+
watchFile?: (
|
|
332
|
+
path: string,
|
|
333
|
+
callback: (fileName: string, eventKind: ts.FileWatcherEventKind) => void,
|
|
334
|
+
) => ts.FileWatcher;
|
|
335
|
+
watchDirectory?: (
|
|
336
|
+
path: string,
|
|
337
|
+
callback: (fileName: string) => void,
|
|
338
|
+
recursive?: boolean,
|
|
339
|
+
) => ts.FileWatcher;
|
|
340
|
+
};
|
|
341
|
+
const sys = ts.sys;
|
|
342
|
+
const projectWatchFile = projectWithWatch.watchFile;
|
|
343
|
+
const projectWatchDirectory = projectWithWatch.watchDirectory;
|
|
344
|
+
const sysWatchFile = sys?.watchFile;
|
|
345
|
+
const sysWatchDirectory = sys?.watchDirectory;
|
|
346
|
+
const watchHost =
|
|
347
|
+
typeof projectWatchFile === "function"
|
|
348
|
+
? {
|
|
349
|
+
watchFile: (path: string, callback: FileWatcherCallback) =>
|
|
350
|
+
projectWatchFile!(path, callback),
|
|
351
|
+
watchDirectory:
|
|
352
|
+
typeof projectWatchDirectory === "function"
|
|
353
|
+
? (path: string, callback: DirectoryWatcherCallback, recursive?: boolean) =>
|
|
354
|
+
projectWatchDirectory!(path, callback, recursive)
|
|
355
|
+
: undefined,
|
|
356
|
+
}
|
|
357
|
+
: typeof sysWatchFile === "function"
|
|
358
|
+
? {
|
|
359
|
+
watchFile: (path: string, callback: FileWatcherCallback) =>
|
|
360
|
+
sysWatchFile!(path, callback),
|
|
361
|
+
watchDirectory:
|
|
362
|
+
typeof sysWatchDirectory === "function"
|
|
363
|
+
? (path: string, callback: DirectoryWatcherCallback, recursive?: boolean) =>
|
|
364
|
+
sysWatchDirectory!(path, callback, recursive)
|
|
365
|
+
: undefined,
|
|
366
|
+
}
|
|
367
|
+
: undefined;
|
|
368
|
+
|
|
369
|
+
attachLanguageServiceAdapter({
|
|
370
|
+
ts,
|
|
371
|
+
languageService: info.languageService,
|
|
372
|
+
languageServiceHost: info.project as import("typescript").LanguageServiceHost,
|
|
373
|
+
resolver,
|
|
374
|
+
projectRoot,
|
|
375
|
+
createTypeInfoApiSession: createTypeInfoApiSessionFactory,
|
|
376
|
+
watchHost,
|
|
377
|
+
debounceMs,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// Force program rebuild so resolution uses our patched host.
|
|
381
|
+
const projectWithDirty = info.project as {
|
|
382
|
+
markAsDirty?: () => void;
|
|
383
|
+
invalidateResolutionsOfFailedLookupLocations?: () => void;
|
|
384
|
+
};
|
|
385
|
+
if (typeof projectWithDirty.invalidateResolutionsOfFailedLookupLocations === "function") {
|
|
386
|
+
projectWithDirty.invalidateResolutionsOfFailedLookupLocations();
|
|
387
|
+
} else if (typeof projectWithDirty.markAsDirty === "function") {
|
|
388
|
+
projectWithDirty.markAsDirty();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Schedule a deferred invalidation: if any virtual modules failed during the initial
|
|
392
|
+
// graph build (e.g., TypeInfoApi not ready yet), this second pass picks them up after
|
|
393
|
+
// the program is fully built.
|
|
394
|
+
setTimeout(() => {
|
|
395
|
+
log("deferred retry: invalidating failed lookups");
|
|
396
|
+
if (typeof projectWithDirty.invalidateResolutionsOfFailedLookupLocations === "function") {
|
|
397
|
+
projectWithDirty.invalidateResolutionsOfFailedLookupLocations();
|
|
398
|
+
}
|
|
399
|
+
if (typeof projectWithDirty.markAsDirty === "function") {
|
|
400
|
+
projectWithDirty.markAsDirty();
|
|
401
|
+
}
|
|
402
|
+
}, 200);
|
|
403
|
+
|
|
404
|
+
return info.languageService;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return { create };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
module.exports = init;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests that run against the sample-project fixture.
|
|
3
|
+
* Validates vmc works with the documented setup. Note: tsc does not load LS plugins
|
|
4
|
+
* (the ts-plugin is for editors/tsserver), so we only test vmc here.
|
|
5
|
+
*/
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { describe, expect, it, beforeAll } from "vitest";
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const sampleProjectDir = join(__dirname, "..", "sample-project");
|
|
13
|
+
const vmcCliPath = join(__dirname, "..", "..", "virtual-modules-compiler", "dist", "cli.js");
|
|
14
|
+
|
|
15
|
+
describe("sample-project integration", () => {
|
|
16
|
+
beforeAll(() => {
|
|
17
|
+
const build = spawnSync("pnpm", ["run", "build:plugins"], {
|
|
18
|
+
cwd: sampleProjectDir,
|
|
19
|
+
encoding: "utf8",
|
|
20
|
+
timeout: 15_000,
|
|
21
|
+
});
|
|
22
|
+
if (build.status !== 0) {
|
|
23
|
+
throw new Error(`build:plugins failed: ${build.stderr || build.stdout}`);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("vmc --noEmit passes (mirrors tsc with virtual modules)", () => {
|
|
28
|
+
const result = spawnSync("node", [vmcCliPath, "--noEmit"], {
|
|
29
|
+
cwd: sampleProjectDir,
|
|
30
|
+
encoding: "utf8",
|
|
31
|
+
timeout: 10_000,
|
|
32
|
+
});
|
|
33
|
+
if (result.status !== 0) {
|
|
34
|
+
const out = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
35
|
+
expect.fail(`vmc --noEmit failed (exit ${result.status}):\n${out || "(no output)"}`);
|
|
36
|
+
}
|
|
37
|
+
expect(result.stderr).toBe("");
|
|
38
|
+
});
|
|
39
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type * as ts from "typescript";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal PluginCreateInfo shape used when the plugin is loaded by tsserver.
|
|
5
|
+
* The full interface is defined in typescript/lib/tsserverlibrary.
|
|
6
|
+
*/
|
|
7
|
+
export interface PluginCreateInfo {
|
|
8
|
+
readonly languageService: ts.LanguageService;
|
|
9
|
+
readonly project: ts.LanguageServiceHost & {
|
|
10
|
+
getCurrentDirectory?(): string;
|
|
11
|
+
watchFile?(path: string, callback: () => void): ts.FileWatcher;
|
|
12
|
+
watchDirectory?(path: string, callback: () => void, recursive?: boolean): ts.FileWatcher;
|
|
13
|
+
projectService?: {
|
|
14
|
+
logger?: { info?(message: string): void };
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
readonly config?: unknown;
|
|
18
|
+
}
|