@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,1211 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, 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 } from "vitest";
|
|
7
|
+
import type { TypeNode } from "./types.js";
|
|
8
|
+
import {
|
|
9
|
+
createTypeInfoApiSession,
|
|
10
|
+
createTypeTargetBootstrapContent,
|
|
11
|
+
resolveTypeTargetsFromSpecs,
|
|
12
|
+
serializeTypeForTest,
|
|
13
|
+
} from "./TypeInfoApi.js";
|
|
14
|
+
|
|
15
|
+
const tempDirs: string[] = [];
|
|
16
|
+
|
|
17
|
+
const createTempDir = (): string => {
|
|
18
|
+
const dir = mkdtempSync(join(tmpdir(), "typed-vm-typeinfo-"));
|
|
19
|
+
tempDirs.push(dir);
|
|
20
|
+
return dir;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
while (tempDirs.length > 0) {
|
|
25
|
+
const dir = tempDirs.pop();
|
|
26
|
+
if (dir) {
|
|
27
|
+
rmSync(dir, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("createTypeTargetBootstrapContent", () => {
|
|
33
|
+
it("generates imports for each unique module in specs", () => {
|
|
34
|
+
const specs = [
|
|
35
|
+
{ id: "Effect", module: "effect/Effect", exportName: "Effect" },
|
|
36
|
+
{ id: "Route", module: "@typed/router", exportName: "Route" },
|
|
37
|
+
{ id: "Schema", module: "effect/Schema", exportName: "Schema" },
|
|
38
|
+
];
|
|
39
|
+
const content = createTypeTargetBootstrapContent(specs);
|
|
40
|
+
expect(content).toContain('import * as _effect_Effect from "effect/Effect";');
|
|
41
|
+
expect(content).toContain('import * as __typed_router from "@typed/router";');
|
|
42
|
+
expect(content).toContain('import * as _effect_Schema from "effect/Schema";');
|
|
43
|
+
expect(content).toContain("export {};");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("dedupes modules when multiple specs share the same module", () => {
|
|
47
|
+
const specs = [
|
|
48
|
+
{ id: "A", module: "pkg/mod", exportName: "A" },
|
|
49
|
+
{ id: "B", module: "pkg/mod", exportName: "B" },
|
|
50
|
+
];
|
|
51
|
+
const content = createTypeTargetBootstrapContent(specs);
|
|
52
|
+
const importCount = (content.match(/import \* as/g) ?? []).length;
|
|
53
|
+
expect(importCount).toBe(1);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("resolveTypeTargetsFromSpecs with typeMember", () => {
|
|
58
|
+
it("resolves typeMember for generic base (Route.Any) and assignableTo passes for instantiation", () => {
|
|
59
|
+
const dir = createTempDir();
|
|
60
|
+
const routeMod = join(dir, "route.ts");
|
|
61
|
+
const bootstrap = join(dir, "bootstrap.ts");
|
|
62
|
+
const main = join(dir, "main.ts");
|
|
63
|
+
writeFileSync(
|
|
64
|
+
routeMod,
|
|
65
|
+
`
|
|
66
|
+
export interface Route<P, S> { readonly path: P; readonly schema: S }
|
|
67
|
+
export namespace Route {
|
|
68
|
+
export type Any = Route<any, any>;
|
|
69
|
+
export const Parse = <P extends string>(path: P): Route<P, any> =>
|
|
70
|
+
({ path, schema: {} } as Route<P, any>);
|
|
71
|
+
}
|
|
72
|
+
`,
|
|
73
|
+
"utf8",
|
|
74
|
+
);
|
|
75
|
+
writeFileSync(bootstrap, `import * as Route from "./route.js"; void Route; export {};`, "utf8");
|
|
76
|
+
writeFileSync(
|
|
77
|
+
main,
|
|
78
|
+
`import * as Route from "./route.js"; export const r = Route.Parse("/status");`,
|
|
79
|
+
"utf8",
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const program = makeProgram([bootstrap, main]);
|
|
83
|
+
const specs = [
|
|
84
|
+
{ id: "Route", module: "./route.js", exportName: "Route", typeMember: "Any" },
|
|
85
|
+
] as const;
|
|
86
|
+
const targets = resolveTypeTargetsFromSpecs(program, ts, specs);
|
|
87
|
+
expect(targets).toHaveLength(1);
|
|
88
|
+
expect(targets[0]!.id).toBe("Route");
|
|
89
|
+
|
|
90
|
+
const session = createTypeInfoApiSession({
|
|
91
|
+
ts,
|
|
92
|
+
program,
|
|
93
|
+
typeTargetSpecs: specs,
|
|
94
|
+
failWhenNoTargetsResolved: false,
|
|
95
|
+
});
|
|
96
|
+
const result = session.api.file("./main.ts", { baseDir: dir });
|
|
97
|
+
expect(result.ok).toBe(true);
|
|
98
|
+
if (!result.ok) return;
|
|
99
|
+
const rExport = result.snapshot.exports.find((e) => e.name === "r");
|
|
100
|
+
expect(rExport).toBeDefined();
|
|
101
|
+
expect(session.api.isAssignableTo(rExport!.type, "Route")).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const makeProgram = (rootFiles: readonly string[]): ts.Program =>
|
|
106
|
+
ts.createProgram(rootFiles, {
|
|
107
|
+
strict: true,
|
|
108
|
+
target: ts.ScriptTarget.ESNext,
|
|
109
|
+
module: ts.ModuleKind.ESNext,
|
|
110
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
111
|
+
skipLibCheck: true,
|
|
112
|
+
noEmit: true,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("createTypeInfoApiSession", () => {
|
|
116
|
+
it("serializes rich structural type information", () => {
|
|
117
|
+
const dir = createTempDir();
|
|
118
|
+
const filePath = join(dir, "types.ts");
|
|
119
|
+
writeFileSync(
|
|
120
|
+
filePath,
|
|
121
|
+
`
|
|
122
|
+
export type U = string | number;
|
|
123
|
+
export interface Box { readonly value: U; optional?: number }
|
|
124
|
+
export const tuple = [1, "x"] as const;
|
|
125
|
+
export const fn = (input: Box): U => input.value;
|
|
126
|
+
`,
|
|
127
|
+
"utf8",
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const program = makeProgram([filePath]);
|
|
131
|
+
const session = createTypeInfoApiSession({ ts, program });
|
|
132
|
+
const result = session.api.file("./types.ts", { baseDir: dir });
|
|
133
|
+
expect(result.ok).toBe(true);
|
|
134
|
+
if (!result.ok) return;
|
|
135
|
+
const snapshot = result.snapshot;
|
|
136
|
+
const byName = new Map(snapshot.exports.map((entry) => [entry.name, entry]));
|
|
137
|
+
expect(byName.get("U")?.type.kind).toBe("union");
|
|
138
|
+
expect(byName.get("tuple")?.type.kind).toBe("tuple");
|
|
139
|
+
expect(byName.get("fn")?.type.kind).toBe("function");
|
|
140
|
+
expect(byName.get("Box")?.type.kind).toBe("object");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("supports relativeGlobs directory queries and watch descriptors", () => {
|
|
144
|
+
const dir = createTempDir();
|
|
145
|
+
writeFileSync(join(dir, "a.ts"), `export const a = 1;`, "utf8");
|
|
146
|
+
writeFileSync(join(dir, "b.ts"), `export const b = 2;`, "utf8");
|
|
147
|
+
mkdirSync(join(dir, "nested"), { recursive: true });
|
|
148
|
+
writeFileSync(join(dir, "nested", "nested.ts"), `export const nested = 3;`, "utf8");
|
|
149
|
+
|
|
150
|
+
const program = makeProgram([
|
|
151
|
+
join(dir, "a.ts"),
|
|
152
|
+
join(dir, "b.ts"),
|
|
153
|
+
join(dir, "nested", "nested.ts"),
|
|
154
|
+
]);
|
|
155
|
+
const session = createTypeInfoApiSession({ ts, program });
|
|
156
|
+
|
|
157
|
+
const nonRecursive = session.api.directory("*.ts", {
|
|
158
|
+
baseDir: dir,
|
|
159
|
+
recursive: false,
|
|
160
|
+
watch: true,
|
|
161
|
+
});
|
|
162
|
+
const recursive = session.api.directory("*.ts", {
|
|
163
|
+
baseDir: dir,
|
|
164
|
+
recursive: true,
|
|
165
|
+
watch: true,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(nonRecursive.length).toBe(2);
|
|
169
|
+
expect(recursive.length).toBeGreaterThan(nonRecursive.length);
|
|
170
|
+
|
|
171
|
+
const dependencies = session.consumeDependencies();
|
|
172
|
+
expect(dependencies.some((descriptor) => descriptor.type === "glob")).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("returns path-escapes-base when relativePath escapes baseDir", () => {
|
|
176
|
+
const dir = createTempDir();
|
|
177
|
+
const filePath = join(dir, "types.ts");
|
|
178
|
+
writeFileSync(filePath, "export const x = 1;", "utf8");
|
|
179
|
+
const program = makeProgram([filePath]);
|
|
180
|
+
const session = createTypeInfoApiSession({ ts, program });
|
|
181
|
+
|
|
182
|
+
const result = session.api.file("../../../other/types.ts", { baseDir: dir });
|
|
183
|
+
expect(result.ok).toBe(false);
|
|
184
|
+
if (result.ok) return;
|
|
185
|
+
expect(result.error).toBe("path-escapes-base");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("returns path-escapes-base when path resolves via symlink outside baseDir", () => {
|
|
189
|
+
const baseDir = createTempDir();
|
|
190
|
+
const outsideDir = createTempDir();
|
|
191
|
+
const outsideFile = join(outsideDir, "outside.ts");
|
|
192
|
+
writeFileSync(outsideFile, "export const x = 1;", "utf8");
|
|
193
|
+
const linkInsideBase = join(baseDir, "link-to-outside");
|
|
194
|
+
let symlinkCreated = false;
|
|
195
|
+
try {
|
|
196
|
+
symlinkSync(outsideDir, linkInsideBase);
|
|
197
|
+
symlinkCreated = true;
|
|
198
|
+
} catch {
|
|
199
|
+
// Symlinks may require privileges on some platforms (e.g. Windows); skip if unavailable
|
|
200
|
+
}
|
|
201
|
+
if (!symlinkCreated) return;
|
|
202
|
+
|
|
203
|
+
const program = makeProgram([join(baseDir, "dummy.ts")]);
|
|
204
|
+
writeFileSync(join(baseDir, "dummy.ts"), "export const d = 1;", "utf8");
|
|
205
|
+
const session = createTypeInfoApiSession({ ts, program });
|
|
206
|
+
|
|
207
|
+
const result = session.api.file("./link-to-outside/outside.ts", { baseDir });
|
|
208
|
+
expect(result.ok).toBe(false);
|
|
209
|
+
if (result.ok) return;
|
|
210
|
+
expect(result.error).toBe("path-escapes-base");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("returns file-not-in-program when file is not in the program", () => {
|
|
214
|
+
const dir = createTempDir();
|
|
215
|
+
const inProgram = join(dir, "in-program.ts");
|
|
216
|
+
const notInProgram = join(dir, "not-in-program.ts");
|
|
217
|
+
writeFileSync(inProgram, "export const a = 1;", "utf8");
|
|
218
|
+
writeFileSync(notInProgram, "export const b = 2;", "utf8");
|
|
219
|
+
const program = makeProgram([inProgram]);
|
|
220
|
+
|
|
221
|
+
const session = createTypeInfoApiSession({ ts, program });
|
|
222
|
+
const result = session.api.file("./not-in-program.ts", { baseDir: dir });
|
|
223
|
+
expect(result.ok).toBe(false);
|
|
224
|
+
if (result.ok) return;
|
|
225
|
+
expect(result.error).toBe("file-not-in-program");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("returns empty array from directory() when baseDir is invalid", () => {
|
|
229
|
+
const dir = createTempDir();
|
|
230
|
+
const filePath = join(dir, "types.ts");
|
|
231
|
+
writeFileSync(filePath, "export const x = 1;", "utf8");
|
|
232
|
+
const program = makeProgram([filePath]);
|
|
233
|
+
const session = createTypeInfoApiSession({ ts, program });
|
|
234
|
+
|
|
235
|
+
const emptyBase = session.api.directory("*.ts", { baseDir: "", recursive: false });
|
|
236
|
+
expect(emptyBase).toEqual([]);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("caches directory() results: identical calls return the same array reference", () => {
|
|
240
|
+
const dir = createTempDir();
|
|
241
|
+
const filePath = join(dir, "types.ts");
|
|
242
|
+
writeFileSync(filePath, "export const x = 1;", "utf8");
|
|
243
|
+
const program = makeProgram([filePath]);
|
|
244
|
+
const session = createTypeInfoApiSession({ ts, program });
|
|
245
|
+
|
|
246
|
+
const first = session.api.directory("*.ts", { baseDir: dir, recursive: false });
|
|
247
|
+
const second = session.api.directory("*.ts", { baseDir: dir, recursive: false });
|
|
248
|
+
expect(first).toBe(second);
|
|
249
|
+
expect(first.length).toBeGreaterThan(0);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("directory() cache is keyed by baseDir and globs: different query yields different result", () => {
|
|
253
|
+
const dir = createTempDir();
|
|
254
|
+
const tsPath = join(dir, "a.ts");
|
|
255
|
+
writeFileSync(tsPath, "export const x = 1;", "utf8");
|
|
256
|
+
const program = makeProgram([tsPath]);
|
|
257
|
+
const session = createTypeInfoApiSession({ ts, program });
|
|
258
|
+
|
|
259
|
+
const withTs = session.api.directory("*.ts", { baseDir: dir, recursive: false });
|
|
260
|
+
const withTsx = session.api.directory("*.tsx", { baseDir: dir, recursive: false });
|
|
261
|
+
expect(withTs).not.toBe(withTsx);
|
|
262
|
+
expect(withTs.length).toBeGreaterThan(0);
|
|
263
|
+
expect(withTsx.length).toBe(0);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("returns invalid-input for empty baseDir or invalid relativePath", () => {
|
|
267
|
+
const dir = createTempDir();
|
|
268
|
+
const filePath = join(dir, "types.ts");
|
|
269
|
+
writeFileSync(filePath, "export const x = 1;", "utf8");
|
|
270
|
+
const program = makeProgram([filePath]);
|
|
271
|
+
const session = createTypeInfoApiSession({ ts, program });
|
|
272
|
+
|
|
273
|
+
const emptyBase = session.api.file("./types.ts", { baseDir: "" });
|
|
274
|
+
expect(emptyBase.ok).toBe(false);
|
|
275
|
+
if (emptyBase.ok) return;
|
|
276
|
+
expect(emptyBase.error).toBe("invalid-input");
|
|
277
|
+
|
|
278
|
+
const nullByte = session.api.file("types.ts\0", { baseDir: dir });
|
|
279
|
+
expect(nullByte.ok).toBe(false);
|
|
280
|
+
if (nullByte.ok) return;
|
|
281
|
+
expect(nullByte.error).toBe("invalid-input");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("resolves re-exports to the type from the target file", () => {
|
|
285
|
+
const dir = createTempDir();
|
|
286
|
+
const routeShape = `export const route = { ast: null, path: "/", paramsSchema: null, pathSchema: null, querySchema: null };`;
|
|
287
|
+
const bPath = join(dir, "b.ts");
|
|
288
|
+
const aPath = join(dir, "a.ts");
|
|
289
|
+
writeFileSync(bPath, routeShape, "utf8");
|
|
290
|
+
writeFileSync(aPath, 'export { route } from "./b";', "utf8");
|
|
291
|
+
const program = makeProgram([aPath, bPath]);
|
|
292
|
+
const session = createTypeInfoApiSession({ ts, program });
|
|
293
|
+
const result = session.api.file("./a.ts", { baseDir: dir });
|
|
294
|
+
expect(result.ok).toBe(true);
|
|
295
|
+
if (!result.ok) return;
|
|
296
|
+
const routeExport = result.snapshot.exports.find((e) => e.name === "route");
|
|
297
|
+
expect(routeExport).toBeDefined();
|
|
298
|
+
expect(routeExport!.type.kind).toBe("object");
|
|
299
|
+
const obj = routeExport!.type as {
|
|
300
|
+
kind: "object";
|
|
301
|
+
properties: ReadonlyArray<{ name: string }>;
|
|
302
|
+
};
|
|
303
|
+
const names = new Set(obj.properties.map((p) => p.name));
|
|
304
|
+
expect(names.has("ast")).toBe(true);
|
|
305
|
+
expect(names.has("path")).toBe(true);
|
|
306
|
+
expect(names.has("paramsSchema")).toBe(true);
|
|
307
|
+
expect(names.has("pathSchema")).toBe(true);
|
|
308
|
+
expect(names.has("querySchema")).toBe(true);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("resolveExport returns export by name from a file", () => {
|
|
312
|
+
const dir = createTempDir();
|
|
313
|
+
const filePath = join(dir, "mod.ts");
|
|
314
|
+
writeFileSync(filePath, "export const foo = 1; export type Bar = string;", "utf8");
|
|
315
|
+
const program = makeProgram([filePath]);
|
|
316
|
+
const session = createTypeInfoApiSession({ ts, program });
|
|
317
|
+
|
|
318
|
+
const foo = session.api.resolveExport(dir, "./mod.ts", "foo");
|
|
319
|
+
expect(foo).toBeDefined();
|
|
320
|
+
expect(foo!.name).toBe("foo");
|
|
321
|
+
expect(["primitive", "literal"]).toContain(foo!.type.kind);
|
|
322
|
+
|
|
323
|
+
const bar = session.api.resolveExport(dir, "./mod.ts", "Bar");
|
|
324
|
+
expect(bar).toBeDefined();
|
|
325
|
+
expect(bar!.name).toBe("Bar");
|
|
326
|
+
|
|
327
|
+
const missing = session.api.resolveExport(dir, "./mod.ts", "nonexistent");
|
|
328
|
+
expect(missing).toBeUndefined();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("includes imports in file snapshot when present", () => {
|
|
332
|
+
const dir = createTempDir();
|
|
333
|
+
const filePath = join(dir, "mod.ts");
|
|
334
|
+
writeFileSync(
|
|
335
|
+
filePath,
|
|
336
|
+
'import { a, b } from "./other"; import * as ns from "./ns"; export const x = a;',
|
|
337
|
+
"utf8",
|
|
338
|
+
);
|
|
339
|
+
const otherPath = join(dir, "other.ts");
|
|
340
|
+
writeFileSync(otherPath, "export const a = 1; export const b = 2;", "utf8");
|
|
341
|
+
const nsPath = join(dir, "ns.ts");
|
|
342
|
+
writeFileSync(nsPath, "export const y = 3;", "utf8");
|
|
343
|
+
const program = makeProgram([filePath, otherPath, nsPath]);
|
|
344
|
+
const session = createTypeInfoApiSession({ ts, program });
|
|
345
|
+
const result = session.api.file("./mod.ts", { baseDir: dir });
|
|
346
|
+
expect(result.ok).toBe(true);
|
|
347
|
+
if (!result.ok) return;
|
|
348
|
+
expect(result.snapshot.imports).toBeDefined();
|
|
349
|
+
expect(result.snapshot.imports!.length).toBe(2);
|
|
350
|
+
const named = result.snapshot.imports!.find((i) => i.importedNames);
|
|
351
|
+
expect(named?.moduleSpecifier).toBe("./other");
|
|
352
|
+
expect(named?.importedNames).toEqual(["a", "b"]);
|
|
353
|
+
const ns = result.snapshot.imports!.find((i) => i.namespaceImport);
|
|
354
|
+
expect(ns?.moduleSpecifier).toBe("./ns");
|
|
355
|
+
expect(ns?.namespaceImport).toBe("ns");
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("accepts typeTargetSpecs and resolves them for assignableTo", () => {
|
|
359
|
+
const dir = createTempDir();
|
|
360
|
+
const filePath = join(dir, "mod.ts");
|
|
361
|
+
writeFileSync(filePath, "export const x: number = 1;", "utf8");
|
|
362
|
+
const program = makeProgram([filePath]);
|
|
363
|
+
const session = createTypeInfoApiSession({
|
|
364
|
+
ts,
|
|
365
|
+
program,
|
|
366
|
+
typeTargetSpecs: [{ id: "Num", module: "effect/Number", exportName: "Number" }],
|
|
367
|
+
failWhenNoTargetsResolved: false,
|
|
368
|
+
});
|
|
369
|
+
const result = session.api.file("./mod.ts", { baseDir: dir });
|
|
370
|
+
expect(result.ok).toBe(true);
|
|
371
|
+
if (!result.ok) return;
|
|
372
|
+
const xExport = result.snapshot.exports.find((e) => e.name === "x");
|
|
373
|
+
expect(xExport).toBeDefined();
|
|
374
|
+
expect(xExport!.type.kind).toBe("primitive");
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
describe("isAssignableTo dynamic API", () => {
|
|
378
|
+
it("returns true for direct assignability check", () => {
|
|
379
|
+
const dir = createTempDir();
|
|
380
|
+
const routeMod = join(dir, "route.ts");
|
|
381
|
+
const bootstrap = join(dir, "bootstrap.ts");
|
|
382
|
+
const main = join(dir, "main.ts");
|
|
383
|
+
writeFileSync(
|
|
384
|
+
routeMod,
|
|
385
|
+
`
|
|
386
|
+
export interface Route<P, S> { readonly path: P; readonly schema: S }
|
|
387
|
+
export namespace Route {
|
|
388
|
+
export type Any = Route<any, any>;
|
|
389
|
+
export const Parse = <P extends string>(path: P): Route<P, any> =>
|
|
390
|
+
({ path, schema: {} } as Route<P, any>);
|
|
391
|
+
}
|
|
392
|
+
`,
|
|
393
|
+
"utf8",
|
|
394
|
+
);
|
|
395
|
+
writeFileSync(bootstrap, `import * as Route from "./route.js"; void Route; export {};`, "utf8");
|
|
396
|
+
writeFileSync(
|
|
397
|
+
main,
|
|
398
|
+
`import * as Route from "./route.js"; export const r = Route.Parse("/status");`,
|
|
399
|
+
"utf8",
|
|
400
|
+
);
|
|
401
|
+
const program = makeProgram([bootstrap, main]);
|
|
402
|
+
const specs = [
|
|
403
|
+
{ id: "Route", module: "./route.js", exportName: "Route", typeMember: "Any" },
|
|
404
|
+
] as const;
|
|
405
|
+
const session = createTypeInfoApiSession({
|
|
406
|
+
ts,
|
|
407
|
+
program,
|
|
408
|
+
typeTargetSpecs: specs,
|
|
409
|
+
failWhenNoTargetsResolved: false,
|
|
410
|
+
});
|
|
411
|
+
const result = session.api.file("./main.ts", { baseDir: dir });
|
|
412
|
+
expect(result.ok).toBe(true);
|
|
413
|
+
if (!result.ok) return;
|
|
414
|
+
const routeExport = result.snapshot.exports.find((e) => e.name === "r");
|
|
415
|
+
expect(routeExport).toBeDefined();
|
|
416
|
+
expect(session.api.isAssignableTo(routeExport!.type, "Route")).toBe(true);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("returns false for non-matching target", () => {
|
|
420
|
+
const dir = createTempDir();
|
|
421
|
+
const routeMod = join(dir, "route.ts");
|
|
422
|
+
const bootstrap = join(dir, "bootstrap.ts");
|
|
423
|
+
const main = join(dir, "main.ts");
|
|
424
|
+
writeFileSync(
|
|
425
|
+
routeMod,
|
|
426
|
+
`
|
|
427
|
+
export interface Route<P, S> { readonly path: P; readonly schema: S }
|
|
428
|
+
export namespace Route {
|
|
429
|
+
export type Any = Route<any, any>;
|
|
430
|
+
export const Parse = <P extends string>(path: P): Route<P, any> =>
|
|
431
|
+
({ path, schema: {} } as Route<P, any>);
|
|
432
|
+
}
|
|
433
|
+
`,
|
|
434
|
+
"utf8",
|
|
435
|
+
);
|
|
436
|
+
writeFileSync(bootstrap, `import * as Route from "./route.js"; void Route; export {};`, "utf8");
|
|
437
|
+
writeFileSync(
|
|
438
|
+
main,
|
|
439
|
+
`import * as Route from "./route.js"; export const r = Route.Parse("/status"); export const n: number = 1;`,
|
|
440
|
+
"utf8",
|
|
441
|
+
);
|
|
442
|
+
const program = makeProgram([bootstrap, main]);
|
|
443
|
+
const specs = [
|
|
444
|
+
{ id: "Route", module: "./route.js", exportName: "Route", typeMember: "Any" },
|
|
445
|
+
] as const;
|
|
446
|
+
const session = createTypeInfoApiSession({
|
|
447
|
+
ts,
|
|
448
|
+
program,
|
|
449
|
+
typeTargetSpecs: specs,
|
|
450
|
+
failWhenNoTargetsResolved: false,
|
|
451
|
+
});
|
|
452
|
+
const result = session.api.file("./main.ts", { baseDir: dir });
|
|
453
|
+
expect(result.ok).toBe(true);
|
|
454
|
+
if (!result.ok) return;
|
|
455
|
+
const nExport = result.snapshot.exports.find((e) => e.name === "n");
|
|
456
|
+
expect(nExport).toBeDefined();
|
|
457
|
+
expect(session.api.isAssignableTo(nExport!.type, "Route")).toBe(false);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("returns true for projected check (returnType)", () => {
|
|
461
|
+
const dir = createTempDir();
|
|
462
|
+
const routeMod = join(dir, "route.ts");
|
|
463
|
+
const bootstrap = join(dir, "bootstrap.ts");
|
|
464
|
+
const main = join(dir, "main.ts");
|
|
465
|
+
writeFileSync(
|
|
466
|
+
routeMod,
|
|
467
|
+
`
|
|
468
|
+
export interface Route<P, S> { readonly path: P; readonly schema: S }
|
|
469
|
+
export namespace Route {
|
|
470
|
+
export type Any = Route<any, any>;
|
|
471
|
+
export const Parse = <P extends string>(path: P): Route<P, any> =>
|
|
472
|
+
({ path, schema: {} } as Route<P, any>);
|
|
473
|
+
}
|
|
474
|
+
`,
|
|
475
|
+
"utf8",
|
|
476
|
+
);
|
|
477
|
+
writeFileSync(bootstrap, `import * as Route from "./route.js"; void Route; export {};`, "utf8");
|
|
478
|
+
writeFileSync(
|
|
479
|
+
main,
|
|
480
|
+
`import * as Route from "./route.js";
|
|
481
|
+
export const r = Route.Parse("/status");
|
|
482
|
+
export function getRoute(): Route.Route<string, any> { return r; }`,
|
|
483
|
+
"utf8",
|
|
484
|
+
);
|
|
485
|
+
const program = makeProgram([bootstrap, main]);
|
|
486
|
+
const specs = [
|
|
487
|
+
{ id: "Route", module: "./route.js", exportName: "Route", typeMember: "Any" },
|
|
488
|
+
] as const;
|
|
489
|
+
const session = createTypeInfoApiSession({
|
|
490
|
+
ts,
|
|
491
|
+
program,
|
|
492
|
+
typeTargetSpecs: specs,
|
|
493
|
+
failWhenNoTargetsResolved: false,
|
|
494
|
+
});
|
|
495
|
+
const result = session.api.file("./main.ts", { baseDir: dir });
|
|
496
|
+
expect(result.ok).toBe(true);
|
|
497
|
+
if (!result.ok) return;
|
|
498
|
+
const fnExport = result.snapshot.exports.find((e) => e.name === "getRoute");
|
|
499
|
+
expect(fnExport).toBeDefined();
|
|
500
|
+
expect(
|
|
501
|
+
session.api.isAssignableTo(fnExport!.type, "Route", [{ kind: "returnType" }]),
|
|
502
|
+
).toBe(true);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("returns true for sub-node passed directly", () => {
|
|
506
|
+
const dir = createTempDir();
|
|
507
|
+
const routeMod = join(dir, "route.ts");
|
|
508
|
+
const bootstrap = join(dir, "bootstrap.ts");
|
|
509
|
+
const main = join(dir, "main.ts");
|
|
510
|
+
writeFileSync(
|
|
511
|
+
routeMod,
|
|
512
|
+
`
|
|
513
|
+
export interface Route<P, S> { readonly path: P; readonly schema: S }
|
|
514
|
+
export namespace Route {
|
|
515
|
+
export type Any = Route<any, any>;
|
|
516
|
+
export const Parse = <P extends string>(path: P): Route<P, any> =>
|
|
517
|
+
({ path, schema: {} } as Route<P, any>);
|
|
518
|
+
}
|
|
519
|
+
`,
|
|
520
|
+
"utf8",
|
|
521
|
+
);
|
|
522
|
+
writeFileSync(bootstrap, `import * as Route from "./route.js"; void Route; export {};`, "utf8");
|
|
523
|
+
writeFileSync(
|
|
524
|
+
main,
|
|
525
|
+
`import * as Route from "./route.js";
|
|
526
|
+
export function getRoute(): Route.Route<string, any> { return Route.Parse("/status"); }`,
|
|
527
|
+
"utf8",
|
|
528
|
+
);
|
|
529
|
+
const program = makeProgram([bootstrap, main]);
|
|
530
|
+
const specs = [
|
|
531
|
+
{ id: "Route", module: "./route.js", exportName: "Route", typeMember: "Any" },
|
|
532
|
+
] as const;
|
|
533
|
+
const session = createTypeInfoApiSession({
|
|
534
|
+
ts,
|
|
535
|
+
program,
|
|
536
|
+
typeTargetSpecs: specs,
|
|
537
|
+
failWhenNoTargetsResolved: false,
|
|
538
|
+
});
|
|
539
|
+
const result = session.api.file("./main.ts", { baseDir: dir });
|
|
540
|
+
expect(result.ok).toBe(true);
|
|
541
|
+
if (!result.ok) return;
|
|
542
|
+
const fnExport = result.snapshot.exports.find((e) => e.name === "getRoute");
|
|
543
|
+
expect(fnExport).toBeDefined();
|
|
544
|
+
expect(fnExport!.type.kind).toBe("function");
|
|
545
|
+
const fnNode = fnExport!.type as { kind: "function"; returnType: TypeNode };
|
|
546
|
+
expect(session.api.isAssignableTo(fnNode.returnType, "Route")).toBe(true);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("returns false for projection on wrong shape", () => {
|
|
550
|
+
const dir = createTempDir();
|
|
551
|
+
const routeMod = join(dir, "route.ts");
|
|
552
|
+
const bootstrap = join(dir, "bootstrap.ts");
|
|
553
|
+
const main = join(dir, "main.ts");
|
|
554
|
+
writeFileSync(
|
|
555
|
+
routeMod,
|
|
556
|
+
`
|
|
557
|
+
export interface Route<P, S> { readonly path: P; readonly schema: S }
|
|
558
|
+
export namespace Route {
|
|
559
|
+
export type Any = Route<any, any>;
|
|
560
|
+
export const Parse = <P extends string>(path: P): Route<P, any> =>
|
|
561
|
+
({ path, schema: {} } as Route<P, any>);
|
|
562
|
+
}
|
|
563
|
+
`,
|
|
564
|
+
"utf8",
|
|
565
|
+
);
|
|
566
|
+
writeFileSync(bootstrap, `import * as Route from "./route.js"; void Route; export {};`, "utf8");
|
|
567
|
+
writeFileSync(
|
|
568
|
+
main,
|
|
569
|
+
`import * as Route from "./route.js"; export const r = Route.Parse("/status");`,
|
|
570
|
+
"utf8",
|
|
571
|
+
);
|
|
572
|
+
const program = makeProgram([bootstrap, main]);
|
|
573
|
+
const specs = [
|
|
574
|
+
{ id: "Route", module: "./route.js", exportName: "Route", typeMember: "Any" },
|
|
575
|
+
] as const;
|
|
576
|
+
const session = createTypeInfoApiSession({
|
|
577
|
+
ts,
|
|
578
|
+
program,
|
|
579
|
+
typeTargetSpecs: specs,
|
|
580
|
+
failWhenNoTargetsResolved: false,
|
|
581
|
+
});
|
|
582
|
+
const result = session.api.file("./main.ts", { baseDir: dir });
|
|
583
|
+
expect(result.ok).toBe(true);
|
|
584
|
+
if (!result.ok) return;
|
|
585
|
+
const rExport = result.snapshot.exports.find((e) => e.name === "r");
|
|
586
|
+
expect(rExport).toBeDefined();
|
|
587
|
+
expect(
|
|
588
|
+
session.api.isAssignableTo(rExport!.type, "Route", [{ kind: "returnType" }]),
|
|
589
|
+
).toBe(false);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it("returns false for unregistered node", () => {
|
|
593
|
+
const dir = createTempDir();
|
|
594
|
+
const routeMod = join(dir, "route.ts");
|
|
595
|
+
const bootstrap = join(dir, "bootstrap.ts");
|
|
596
|
+
const main = join(dir, "main.ts");
|
|
597
|
+
writeFileSync(
|
|
598
|
+
routeMod,
|
|
599
|
+
`
|
|
600
|
+
export interface Route<P, S> { readonly path: P; readonly schema: S }
|
|
601
|
+
export namespace Route {
|
|
602
|
+
export type Any = Route<any, any>;
|
|
603
|
+
export const Parse = <P extends string>(path: P): Route<P, any> =>
|
|
604
|
+
({ path, schema: {} } as Route<P, any>);
|
|
605
|
+
}
|
|
606
|
+
`,
|
|
607
|
+
"utf8",
|
|
608
|
+
);
|
|
609
|
+
writeFileSync(bootstrap, `import * as Route from "./route.js"; void Route; export {};`, "utf8");
|
|
610
|
+
writeFileSync(
|
|
611
|
+
main,
|
|
612
|
+
`import * as Route from "./route.js"; export const r = Route.Parse("/status");`,
|
|
613
|
+
"utf8",
|
|
614
|
+
);
|
|
615
|
+
const program = makeProgram([bootstrap, main]);
|
|
616
|
+
const specs = [
|
|
617
|
+
{ id: "Route", module: "./route.js", exportName: "Route", typeMember: "Any" },
|
|
618
|
+
] as const;
|
|
619
|
+
const session = createTypeInfoApiSession({
|
|
620
|
+
ts,
|
|
621
|
+
program,
|
|
622
|
+
typeTargetSpecs: specs,
|
|
623
|
+
failWhenNoTargetsResolved: false,
|
|
624
|
+
});
|
|
625
|
+
const result = session.api.file("./main.ts", { baseDir: dir });
|
|
626
|
+
expect(result.ok).toBe(true);
|
|
627
|
+
if (!result.ok) return;
|
|
628
|
+
const unregisteredNode = { kind: "primitive" as const, text: "number" } as TypeNode;
|
|
629
|
+
expect(session.api.isAssignableTo(unregisteredNode, "Route")).toBe(false);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it("ensure passes and continues chain - (...) => Effect<Option<*>> pattern", () => {
|
|
633
|
+
const dir = createTempDir();
|
|
634
|
+
const effectMod = join(dir, "effect.ts");
|
|
635
|
+
const optionMod = join(dir, "option.ts");
|
|
636
|
+
const bootstrap = join(dir, "bootstrap.ts");
|
|
637
|
+
const main = join(dir, "main.ts");
|
|
638
|
+
writeFileSync(
|
|
639
|
+
effectMod,
|
|
640
|
+
`
|
|
641
|
+
export interface Effect<A, E, R> { readonly _tag: "Effect"; readonly _A: A; readonly _E: E; readonly _R: R }
|
|
642
|
+
export namespace Effect {
|
|
643
|
+
export type Any = Effect<any, any, any>;
|
|
644
|
+
}
|
|
645
|
+
`,
|
|
646
|
+
"utf8",
|
|
647
|
+
);
|
|
648
|
+
writeFileSync(
|
|
649
|
+
optionMod,
|
|
650
|
+
`
|
|
651
|
+
export interface Option<A> { readonly _tag: "Option"; readonly _A: A }
|
|
652
|
+
export namespace Option {
|
|
653
|
+
export type Any = Option<any>;
|
|
654
|
+
}
|
|
655
|
+
`,
|
|
656
|
+
"utf8",
|
|
657
|
+
);
|
|
658
|
+
writeFileSync(
|
|
659
|
+
bootstrap,
|
|
660
|
+
`import * as Effect from "./effect.js"; import * as Option from "./option.js"; void Effect; void Option; export {};`,
|
|
661
|
+
"utf8",
|
|
662
|
+
);
|
|
663
|
+
writeFileSync(
|
|
664
|
+
main,
|
|
665
|
+
`import * as Effect from "./effect.js"; import * as Option from "./option.js";
|
|
666
|
+
export function fn(): Effect.Effect<Option.Option<string>, never, never> {
|
|
667
|
+
return {} as Effect.Effect<Option.Option<string>, never, never>;
|
|
668
|
+
}`,
|
|
669
|
+
"utf8",
|
|
670
|
+
);
|
|
671
|
+
const program = makeProgram([bootstrap, main]);
|
|
672
|
+
const specs = [
|
|
673
|
+
{ id: "Effect", module: "./effect.js", exportName: "Effect", typeMember: "Any" },
|
|
674
|
+
{ id: "Option", module: "./option.js", exportName: "Option", typeMember: "Any" },
|
|
675
|
+
] as const;
|
|
676
|
+
const session = createTypeInfoApiSession({
|
|
677
|
+
ts,
|
|
678
|
+
program,
|
|
679
|
+
typeTargetSpecs: specs,
|
|
680
|
+
failWhenNoTargetsResolved: false,
|
|
681
|
+
});
|
|
682
|
+
const result = session.api.file("./main.ts", { baseDir: dir });
|
|
683
|
+
expect(result.ok).toBe(true);
|
|
684
|
+
if (!result.ok) return;
|
|
685
|
+
const fnExport = result.snapshot.exports.find((e) => e.name === "fn");
|
|
686
|
+
expect(fnExport).toBeDefined();
|
|
687
|
+
expect(
|
|
688
|
+
session.api.isAssignableTo(fnExport!.type, "Option", [
|
|
689
|
+
{ kind: "returnType" },
|
|
690
|
+
{ kind: "ensure", targetId: "Effect" },
|
|
691
|
+
{ kind: "typeArg", index: 0 },
|
|
692
|
+
]),
|
|
693
|
+
).toBe(true);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it("ensure fails when return type is not Effect", () => {
|
|
697
|
+
const dir = createTempDir();
|
|
698
|
+
const effectMod = join(dir, "effect.ts");
|
|
699
|
+
const optionMod = join(dir, "option.ts");
|
|
700
|
+
const bootstrap = join(dir, "bootstrap.ts");
|
|
701
|
+
const main = join(dir, "main.ts");
|
|
702
|
+
writeFileSync(
|
|
703
|
+
effectMod,
|
|
704
|
+
`
|
|
705
|
+
export interface Effect<A, E, R> { readonly _tag: "Effect"; readonly _A: A; readonly _E: E; readonly _R: R }
|
|
706
|
+
export namespace Effect { export type Any = Effect<any, any, any>; }
|
|
707
|
+
`,
|
|
708
|
+
"utf8",
|
|
709
|
+
);
|
|
710
|
+
writeFileSync(
|
|
711
|
+
optionMod,
|
|
712
|
+
`
|
|
713
|
+
export interface Option<A> { readonly _tag: "Option"; readonly _A: A }
|
|
714
|
+
export namespace Option { export type Any = Option<any>; }
|
|
715
|
+
`,
|
|
716
|
+
"utf8",
|
|
717
|
+
);
|
|
718
|
+
writeFileSync(
|
|
719
|
+
bootstrap,
|
|
720
|
+
`import * as Effect from "./effect.js"; import * as Option from "./option.js"; void Effect; void Option; export {};`,
|
|
721
|
+
"utf8",
|
|
722
|
+
);
|
|
723
|
+
writeFileSync(
|
|
724
|
+
main,
|
|
725
|
+
`import * as Option from "./option.js";
|
|
726
|
+
export function fn(): Option.Option<string> {
|
|
727
|
+
return {} as Option.Option<string>;
|
|
728
|
+
}`,
|
|
729
|
+
"utf8",
|
|
730
|
+
);
|
|
731
|
+
const program = makeProgram([bootstrap, main]);
|
|
732
|
+
const specs = [
|
|
733
|
+
{ id: "Effect", module: "./effect.js", exportName: "Effect", typeMember: "Any" },
|
|
734
|
+
{ id: "Option", module: "./option.js", exportName: "Option", typeMember: "Any" },
|
|
735
|
+
] as const;
|
|
736
|
+
const session = createTypeInfoApiSession({
|
|
737
|
+
ts,
|
|
738
|
+
program,
|
|
739
|
+
typeTargetSpecs: specs,
|
|
740
|
+
failWhenNoTargetsResolved: false,
|
|
741
|
+
});
|
|
742
|
+
const result = session.api.file("./main.ts", { baseDir: dir });
|
|
743
|
+
expect(result.ok).toBe(true);
|
|
744
|
+
if (!result.ok) return;
|
|
745
|
+
const fnExport = result.snapshot.exports.find((e) => e.name === "fn");
|
|
746
|
+
expect(fnExport).toBeDefined();
|
|
747
|
+
expect(
|
|
748
|
+
session.api.isAssignableTo(fnExport!.type, "Option", [
|
|
749
|
+
{ kind: "returnType" },
|
|
750
|
+
{ kind: "ensure", targetId: "Effect" },
|
|
751
|
+
{ kind: "typeArg", index: 0 },
|
|
752
|
+
]),
|
|
753
|
+
).toBe(false);
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
it("ensure with unknown targetId returns false", () => {
|
|
757
|
+
const dir = createTempDir();
|
|
758
|
+
const routeMod = join(dir, "route.ts");
|
|
759
|
+
const bootstrap = join(dir, "bootstrap.ts");
|
|
760
|
+
const main = join(dir, "main.ts");
|
|
761
|
+
writeFileSync(
|
|
762
|
+
routeMod,
|
|
763
|
+
`export interface Route<P, S> { readonly path: P; readonly schema: S }
|
|
764
|
+
export namespace Route { export type Any = Route<any, any>; export const Parse = (p: string) => ({} as Route<any, any>); }`,
|
|
765
|
+
"utf8",
|
|
766
|
+
);
|
|
767
|
+
writeFileSync(bootstrap, `import * as Route from "./route.js"; void Route; export {};`, "utf8");
|
|
768
|
+
writeFileSync(
|
|
769
|
+
main,
|
|
770
|
+
`import * as Route from "./route.js";
|
|
771
|
+
export function getRoute(): Route.Route<string, any> { return Route.Parse("/") as Route.Route<string, any>; }`,
|
|
772
|
+
"utf8",
|
|
773
|
+
);
|
|
774
|
+
const program = makeProgram([bootstrap, main]);
|
|
775
|
+
const specs = [
|
|
776
|
+
{ id: "Route", module: "./route.js", exportName: "Route", typeMember: "Any" },
|
|
777
|
+
] as const;
|
|
778
|
+
const session = createTypeInfoApiSession({
|
|
779
|
+
ts,
|
|
780
|
+
program,
|
|
781
|
+
typeTargetSpecs: specs,
|
|
782
|
+
failWhenNoTargetsResolved: false,
|
|
783
|
+
});
|
|
784
|
+
const result = session.api.file("./main.ts", { baseDir: dir });
|
|
785
|
+
expect(result.ok).toBe(true);
|
|
786
|
+
if (!result.ok) return;
|
|
787
|
+
const fnExport = result.snapshot.exports.find((e) => e.name === "getRoute");
|
|
788
|
+
expect(fnExport).toBeDefined();
|
|
789
|
+
expect(
|
|
790
|
+
session.api.isAssignableTo(fnExport!.type, "Route", [
|
|
791
|
+
{ kind: "returnType" },
|
|
792
|
+
{ kind: "ensure", targetId: "UnknownTarget" },
|
|
793
|
+
]),
|
|
794
|
+
).toBe(false);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it("predicate on node kind passes", () => {
|
|
798
|
+
const dir = createTempDir();
|
|
799
|
+
const routeMod = join(dir, "route.ts");
|
|
800
|
+
const bootstrap = join(dir, "bootstrap.ts");
|
|
801
|
+
const main = join(dir, "main.ts");
|
|
802
|
+
writeFileSync(
|
|
803
|
+
routeMod,
|
|
804
|
+
`export interface Route<P, S> { readonly path: P; readonly schema: S }
|
|
805
|
+
export namespace Route { export type Any = Route<any, any>; export const Parse = (p: string) => ({} as Route<any, any>); }`,
|
|
806
|
+
"utf8",
|
|
807
|
+
);
|
|
808
|
+
writeFileSync(bootstrap, `import * as Route from "./route.js"; void Route; export {};`, "utf8");
|
|
809
|
+
writeFileSync(
|
|
810
|
+
main,
|
|
811
|
+
`import * as Route from "./route.js";
|
|
812
|
+
export function getRoute(): Route.Route<string, any> { return Route.Parse("/") as Route.Route<string, any>; }`,
|
|
813
|
+
"utf8",
|
|
814
|
+
);
|
|
815
|
+
const program = makeProgram([bootstrap, main]);
|
|
816
|
+
const specs = [
|
|
817
|
+
{ id: "Route", module: "./route.js", exportName: "Route", typeMember: "Any" },
|
|
818
|
+
] as const;
|
|
819
|
+
const session = createTypeInfoApiSession({
|
|
820
|
+
ts,
|
|
821
|
+
program,
|
|
822
|
+
typeTargetSpecs: specs,
|
|
823
|
+
failWhenNoTargetsResolved: false,
|
|
824
|
+
});
|
|
825
|
+
const result = session.api.file("./main.ts", { baseDir: dir });
|
|
826
|
+
expect(result.ok).toBe(true);
|
|
827
|
+
if (!result.ok) return;
|
|
828
|
+
const fnExport = result.snapshot.exports.find((e) => e.name === "getRoute");
|
|
829
|
+
expect(fnExport).toBeDefined();
|
|
830
|
+
expect(
|
|
831
|
+
session.api.isAssignableTo(fnExport!.type, "Route", [
|
|
832
|
+
{ kind: "returnType" },
|
|
833
|
+
{ kind: "predicate", fn: (n) => n.kind === "reference" || n.kind === "object" },
|
|
834
|
+
]),
|
|
835
|
+
).toBe(true);
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
it("predicate returns false - whole check fails", () => {
|
|
839
|
+
const dir = createTempDir();
|
|
840
|
+
const routeMod = join(dir, "route.ts");
|
|
841
|
+
const bootstrap = join(dir, "bootstrap.ts");
|
|
842
|
+
const main = join(dir, "main.ts");
|
|
843
|
+
writeFileSync(
|
|
844
|
+
routeMod,
|
|
845
|
+
`export interface Route<P, S> { readonly path: P; readonly schema: S }
|
|
846
|
+
export namespace Route { export type Any = Route<any, any>; export const Parse = (p: string) => ({} as Route<any, any>); }`,
|
|
847
|
+
"utf8",
|
|
848
|
+
);
|
|
849
|
+
writeFileSync(bootstrap, `import * as Route from "./route.js"; void Route; export {};`, "utf8");
|
|
850
|
+
writeFileSync(
|
|
851
|
+
main,
|
|
852
|
+
`import * as Route from "./route.js";
|
|
853
|
+
export function getRoute(): Route.Route<string, any> { return Route.Parse("/") as Route.Route<string, any>; }`,
|
|
854
|
+
"utf8",
|
|
855
|
+
);
|
|
856
|
+
const program = makeProgram([bootstrap, main]);
|
|
857
|
+
const specs = [
|
|
858
|
+
{ id: "Route", module: "./route.js", exportName: "Route", typeMember: "Any" },
|
|
859
|
+
] as const;
|
|
860
|
+
const session = createTypeInfoApiSession({
|
|
861
|
+
ts,
|
|
862
|
+
program,
|
|
863
|
+
typeTargetSpecs: specs,
|
|
864
|
+
failWhenNoTargetsResolved: false,
|
|
865
|
+
});
|
|
866
|
+
const result = session.api.file("./main.ts", { baseDir: dir });
|
|
867
|
+
expect(result.ok).toBe(true);
|
|
868
|
+
if (!result.ok) return;
|
|
869
|
+
const fnExport = result.snapshot.exports.find((e) => e.name === "getRoute");
|
|
870
|
+
expect(fnExport).toBeDefined();
|
|
871
|
+
expect(
|
|
872
|
+
session.api.isAssignableTo(fnExport!.type, "Route", [
|
|
873
|
+
{ kind: "returnType" },
|
|
874
|
+
{ kind: "predicate", fn: () => false },
|
|
875
|
+
]),
|
|
876
|
+
).toBe(false);
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
it("predicate with typeArguments cardinality check", () => {
|
|
880
|
+
const dir = createTempDir();
|
|
881
|
+
const effectMod = join(dir, "effect.ts");
|
|
882
|
+
const bootstrap = join(dir, "bootstrap.ts");
|
|
883
|
+
const main = join(dir, "main.ts");
|
|
884
|
+
writeFileSync(
|
|
885
|
+
effectMod,
|
|
886
|
+
`export interface Effect<A, E, R> { readonly _tag: "Effect"; readonly _A: A; readonly _E: E; readonly _R: R }
|
|
887
|
+
export namespace Effect { export type Any = Effect<any, any, any>; }`,
|
|
888
|
+
"utf8",
|
|
889
|
+
);
|
|
890
|
+
writeFileSync(bootstrap, `import * as Effect from "./effect.js"; void Effect; export {};`, "utf8");
|
|
891
|
+
writeFileSync(
|
|
892
|
+
main,
|
|
893
|
+
`import * as Effect from "./effect.js";
|
|
894
|
+
export function fn(): Effect.Effect<string, never, never> {
|
|
895
|
+
return {} as Effect.Effect<string, never, never>;
|
|
896
|
+
}`,
|
|
897
|
+
"utf8",
|
|
898
|
+
);
|
|
899
|
+
const program = makeProgram([bootstrap, main]);
|
|
900
|
+
const specs = [
|
|
901
|
+
{ id: "Effect", module: "./effect.js", exportName: "Effect", typeMember: "Any" },
|
|
902
|
+
] as const;
|
|
903
|
+
const session = createTypeInfoApiSession({
|
|
904
|
+
ts,
|
|
905
|
+
program,
|
|
906
|
+
typeTargetSpecs: specs,
|
|
907
|
+
failWhenNoTargetsResolved: false,
|
|
908
|
+
});
|
|
909
|
+
const result = session.api.file("./main.ts", { baseDir: dir });
|
|
910
|
+
expect(result.ok).toBe(true);
|
|
911
|
+
if (!result.ok) return;
|
|
912
|
+
const fnExport = result.snapshot.exports.find((e) => e.name === "fn");
|
|
913
|
+
expect(fnExport).toBeDefined();
|
|
914
|
+
expect(
|
|
915
|
+
session.api.isAssignableTo(fnExport!.type, "Effect", [
|
|
916
|
+
{ kind: "returnType" },
|
|
917
|
+
{ kind: "predicate", fn: (n) => n.kind === "reference" && (n.typeArguments?.length ?? 0) >= 2 },
|
|
918
|
+
]),
|
|
919
|
+
).toBe(true);
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
it("applies maxDepth when serializing deep types", () => {
|
|
924
|
+
const dir = createTempDir();
|
|
925
|
+
const filePath = join(dir, "deep.ts");
|
|
926
|
+
writeFileSync(
|
|
927
|
+
filePath,
|
|
928
|
+
`
|
|
929
|
+
type L0 = { a: number };
|
|
930
|
+
type L1 = { b: L0 };
|
|
931
|
+
export type Deep = L1;
|
|
932
|
+
`,
|
|
933
|
+
"utf8",
|
|
934
|
+
);
|
|
935
|
+
const program = makeProgram([filePath]);
|
|
936
|
+
const session = createTypeInfoApiSession({ ts, program, maxTypeDepth: 0 });
|
|
937
|
+
const result = session.api.file("./deep.ts", { baseDir: dir });
|
|
938
|
+
expect(result.ok).toBe(true);
|
|
939
|
+
if (!result.ok) return;
|
|
940
|
+
const deepExport = result.snapshot.exports.find((e) => e.name === "Deep");
|
|
941
|
+
expect(deepExport).toBeDefined();
|
|
942
|
+
expect(deepExport!.type).toBeDefined();
|
|
943
|
+
const type = deepExport!.type as { kind: string; members?: Array<{ type?: unknown }> };
|
|
944
|
+
if (type.kind === "object" && type.members?.length) {
|
|
945
|
+
const firstMemberType = type.members[0]?.type as { kind: string } | undefined;
|
|
946
|
+
expect(firstMemberType?.kind === "reference").toBe(true);
|
|
947
|
+
} else {
|
|
948
|
+
expect(type.kind === "reference" || type.kind === "object").toBe(true);
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
it("serializes conditional types when unresolved", () => {
|
|
953
|
+
const dir = createTempDir();
|
|
954
|
+
const filePath = join(dir, "conditional.ts");
|
|
955
|
+
writeFileSync(
|
|
956
|
+
filePath,
|
|
957
|
+
`
|
|
958
|
+
export type Cond<T> = T extends number ? "a" : "b";
|
|
959
|
+
export type Instantiated = Cond<string>;
|
|
960
|
+
`,
|
|
961
|
+
"utf8",
|
|
962
|
+
);
|
|
963
|
+
const program = makeProgram([filePath]);
|
|
964
|
+
const session = createTypeInfoApiSession({ ts, program });
|
|
965
|
+
const result = session.api.file("./conditional.ts", { baseDir: dir });
|
|
966
|
+
expect(result.ok).toBe(true);
|
|
967
|
+
if (!result.ok) return;
|
|
968
|
+
const cond = result.snapshot.exports.find((e) => e.name === "Cond");
|
|
969
|
+
const inst = result.snapshot.exports.find((e) => e.name === "Instantiated");
|
|
970
|
+
expect(cond).toBeDefined();
|
|
971
|
+
expect(inst).toBeDefined();
|
|
972
|
+
expect(["reference", "conditional", "function"]).toContain(cond!.type.kind);
|
|
973
|
+
expect(["literal", "conditional", "reference"]).toContain(inst!.type.kind);
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
it("serializes indexed access or resolved result", () => {
|
|
977
|
+
const dir = createTempDir();
|
|
978
|
+
const filePath = join(dir, "indexed.ts");
|
|
979
|
+
writeFileSync(
|
|
980
|
+
filePath,
|
|
981
|
+
`
|
|
982
|
+
interface Obj { a: string; b: number }
|
|
983
|
+
export type A = Obj["a"];
|
|
984
|
+
export type B = Obj["b"];
|
|
985
|
+
`,
|
|
986
|
+
"utf8",
|
|
987
|
+
);
|
|
988
|
+
const program = makeProgram([filePath]);
|
|
989
|
+
const session = createTypeInfoApiSession({ ts, program });
|
|
990
|
+
const result = session.api.file("./indexed.ts", { baseDir: dir });
|
|
991
|
+
expect(result.ok).toBe(true);
|
|
992
|
+
if (!result.ok) return;
|
|
993
|
+
const a = result.snapshot.exports.find((e) => e.name === "A");
|
|
994
|
+
const b = result.snapshot.exports.find((e) => e.name === "B");
|
|
995
|
+
expect(a?.type.kind).toBeDefined();
|
|
996
|
+
expect(b?.type.kind).toBeDefined();
|
|
997
|
+
expect(["indexedAccess", "primitive", "literal", "reference"]).toContain(a!.type.kind);
|
|
998
|
+
expect(["indexedAccess", "primitive", "literal", "reference"]).toContain(b!.type.kind);
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
it("serializes keyof of concrete object as union of string literals", () => {
|
|
1002
|
+
const dir = createTempDir();
|
|
1003
|
+
const filePath = join(dir, "keyof.ts");
|
|
1004
|
+
writeFileSync(filePath, `export type K = keyof { a: 1; b: 2 };`, "utf8");
|
|
1005
|
+
const program = makeProgram([filePath]);
|
|
1006
|
+
const session = createTypeInfoApiSession({ ts, program });
|
|
1007
|
+
const result = session.api.file("./keyof.ts", { baseDir: dir });
|
|
1008
|
+
expect(result.ok).toBe(true);
|
|
1009
|
+
if (!result.ok) return;
|
|
1010
|
+
const k = result.snapshot.exports.find((e) => e.name === "K");
|
|
1011
|
+
expect(k).toBeDefined();
|
|
1012
|
+
expect(k!.type.kind).toBe("union");
|
|
1013
|
+
const union = k!.type as { kind: "union"; elements: readonly { kind: string; text: string }[] };
|
|
1014
|
+
const texts = new Set(union.elements.map((e) => e.text));
|
|
1015
|
+
expect(texts).toEqual(new Set(['"a"', '"b"']));
|
|
1016
|
+
expect(union.elements.every((e) => e.kind === "literal")).toBe(true);
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
it("serializes keyof type node as typeOperator (IndexType)", () => {
|
|
1020
|
+
const dir = createTempDir();
|
|
1021
|
+
const filePath = join(dir, "keyof.ts");
|
|
1022
|
+
writeFileSync(filePath, `type KeysOf<T> = keyof T;`, "utf8");
|
|
1023
|
+
const program = makeProgram([filePath]);
|
|
1024
|
+
const checker = program.getTypeChecker();
|
|
1025
|
+
const sourceFile = program.getSourceFile(filePath);
|
|
1026
|
+
expect(sourceFile).toBeDefined();
|
|
1027
|
+
const typeAlias = sourceFile!.statements.find(
|
|
1028
|
+
(s): s is ts.TypeAliasDeclaration => ts.isTypeAliasDeclaration(s),
|
|
1029
|
+
);
|
|
1030
|
+
expect(typeAlias).toBeDefined();
|
|
1031
|
+
const keyofTypeNode = typeAlias!.type;
|
|
1032
|
+
if (!ts.isTypeOperatorNode(keyofTypeNode)) {
|
|
1033
|
+
throw new Error("Expected type alias RHS to be TypeOperatorNode (keyof T)");
|
|
1034
|
+
}
|
|
1035
|
+
const keyofType = checker.getTypeFromTypeNode(keyofTypeNode);
|
|
1036
|
+
expect((keyofType.flags & ts.TypeFlags.Index) !== 0).toBe(true);
|
|
1037
|
+
const serialized = serializeTypeForTest(keyofType, checker, ts);
|
|
1038
|
+
expect(serialized.kind).toBe("typeOperator");
|
|
1039
|
+
const op = serialized as { kind: "typeOperator"; operator: string; type: unknown };
|
|
1040
|
+
expect(op.operator).toBe("keyof");
|
|
1041
|
+
expect(op.type).toBeDefined();
|
|
1042
|
+
expect(typeof op.type === "object" && op.type !== null && "kind" in (op.type as object)).toBe(true);
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
it("serializes enum object type as object (enum declaration)", () => {
|
|
1046
|
+
const dir = createTempDir();
|
|
1047
|
+
const filePath = join(dir, "enum.ts");
|
|
1048
|
+
writeFileSync(filePath, `export enum E { A = 1, B = 2 };`, "utf8");
|
|
1049
|
+
const program = makeProgram([filePath]);
|
|
1050
|
+
const session = createTypeInfoApiSession({ ts, program });
|
|
1051
|
+
const result = session.api.file("./enum.ts", { baseDir: dir });
|
|
1052
|
+
expect(result.ok).toBe(true);
|
|
1053
|
+
if (!result.ok) return;
|
|
1054
|
+
const e = result.snapshot.exports.find((ex) => ex.name === "E");
|
|
1055
|
+
expect(e).toBeDefined();
|
|
1056
|
+
expect(e!.type.kind).toBe("object");
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
it("serializes enum type as enum with members when TypeFlags.Enum is present", () => {
|
|
1060
|
+
const dir = createTempDir();
|
|
1061
|
+
const filePath = join(dir, "enum.ts");
|
|
1062
|
+
writeFileSync(filePath, `enum E { A = 1, B = 2 }`, "utf8");
|
|
1063
|
+
const program = makeProgram([filePath]);
|
|
1064
|
+
const checker = program.getTypeChecker();
|
|
1065
|
+
const sourceFile = program.getSourceFile(filePath);
|
|
1066
|
+
const enumDecl = sourceFile!.statements.find(
|
|
1067
|
+
(s): s is ts.EnumDeclaration => ts.isEnumDeclaration(s),
|
|
1068
|
+
)!;
|
|
1069
|
+
const enumSymbol = checker.getSymbolAtLocation(enumDecl.name);
|
|
1070
|
+
const enumType = enumSymbol && checker.getDeclaredTypeOfSymbol(enumSymbol);
|
|
1071
|
+
if (!enumType || (enumType.flags & ts.TypeFlags.Enum) === 0) {
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
const serialized = serializeTypeForTest(enumType, checker, ts);
|
|
1075
|
+
expect(serialized.kind).toBe("enum");
|
|
1076
|
+
const enumNode = serialized as {
|
|
1077
|
+
kind: "enum";
|
|
1078
|
+
members?: readonly { name: string; value?: string | number }[];
|
|
1079
|
+
};
|
|
1080
|
+
expect(enumNode.members?.some((m) => m.name === "A" && m.value === 1)).toBe(true);
|
|
1081
|
+
expect(enumNode.members?.some((m) => m.name === "B" && m.value === 2)).toBe(true);
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
it("serializes enum exports and enum-typed values", () => {
|
|
1085
|
+
const dir = createTempDir();
|
|
1086
|
+
const filePath = join(dir, "enum.ts");
|
|
1087
|
+
writeFileSync(
|
|
1088
|
+
filePath,
|
|
1089
|
+
`export enum E { A = 1, B = 2 }
|
|
1090
|
+
export enum S { X = "x", Y = "y" }
|
|
1091
|
+
export const eVal: E = E.A;
|
|
1092
|
+
export const sVal: S = S.X;`,
|
|
1093
|
+
"utf8",
|
|
1094
|
+
);
|
|
1095
|
+
const program = makeProgram([filePath]);
|
|
1096
|
+
const session = createTypeInfoApiSession({ ts, program });
|
|
1097
|
+
const result = session.api.file("./enum.ts", { baseDir: dir });
|
|
1098
|
+
expect(result.ok).toBe(true);
|
|
1099
|
+
if (!result.ok) return;
|
|
1100
|
+
const e = result.snapshot.exports.find((ex) => ex.name === "E");
|
|
1101
|
+
const eVal = result.snapshot.exports.find((ex) => ex.name === "eVal");
|
|
1102
|
+
const sValExport = result.snapshot.exports.find((ex) => ex.name === "sVal");
|
|
1103
|
+
expect(e?.type.kind).toBe("object");
|
|
1104
|
+
expect(eVal!.type.kind).toBe("union");
|
|
1105
|
+
const eValUnion = eVal!.type as { kind: "union"; elements: readonly { kind: string; text: string }[] };
|
|
1106
|
+
const eValTexts = new Set(eValUnion.elements.map((el) => el.text));
|
|
1107
|
+
expect(eValTexts).toContain("E.A");
|
|
1108
|
+
expect(eValTexts).toContain("E.B");
|
|
1109
|
+
expect(sValExport!.type.kind).toBe("union");
|
|
1110
|
+
const sValUnion = sValExport!.type as { kind: "union"; elements: readonly { kind: string; text: string }[] };
|
|
1111
|
+
const sValTexts = new Set(sValUnion.elements.map((el) => el.text));
|
|
1112
|
+
expect(sValTexts).toContain("S.X");
|
|
1113
|
+
expect(sValTexts).toContain("S.Y");
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
it("serializes objects with index signature", () => {
|
|
1117
|
+
const dir = createTempDir();
|
|
1118
|
+
const filePath = join(dir, "indexSig.ts");
|
|
1119
|
+
writeFileSync(filePath, `export type Dict = { [k: string]: number };`, "utf8");
|
|
1120
|
+
const program = makeProgram([filePath]);
|
|
1121
|
+
const session = createTypeInfoApiSession({ ts, program });
|
|
1122
|
+
const result = session.api.file("./indexSig.ts", { baseDir: dir });
|
|
1123
|
+
expect(result.ok).toBe(true);
|
|
1124
|
+
if (!result.ok) return;
|
|
1125
|
+
const dict = result.snapshot.exports.find((e) => e.name === "Dict");
|
|
1126
|
+
expect(dict?.type.kind).toBe("object");
|
|
1127
|
+
const obj = dict!.type as { kind: string; indexSignature?: unknown };
|
|
1128
|
+
expect(obj.indexSignature).toBeDefined();
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
it("serializes overload sets", () => {
|
|
1132
|
+
const dir = createTempDir();
|
|
1133
|
+
const filePath = join(dir, "overloads.ts");
|
|
1134
|
+
writeFileSync(
|
|
1135
|
+
filePath,
|
|
1136
|
+
`
|
|
1137
|
+
export function fn(x: string): number;
|
|
1138
|
+
export function fn(x: number): string;
|
|
1139
|
+
export function fn(x: string | number): number | string {
|
|
1140
|
+
return typeof x === "string" ? x.length : String(x);
|
|
1141
|
+
}
|
|
1142
|
+
`,
|
|
1143
|
+
"utf8",
|
|
1144
|
+
);
|
|
1145
|
+
const program = makeProgram([filePath]);
|
|
1146
|
+
const session = createTypeInfoApiSession({ ts, program });
|
|
1147
|
+
const result = session.api.file("./overloads.ts", { baseDir: dir });
|
|
1148
|
+
expect(result.ok).toBe(true);
|
|
1149
|
+
if (!result.ok) return;
|
|
1150
|
+
const fn = result.snapshot.exports.find((e) => e.name === "fn");
|
|
1151
|
+
expect(["function", "overloadSet"]).toContain(fn?.type.kind);
|
|
1152
|
+
if (fn?.type.kind === "overloadSet") {
|
|
1153
|
+
expect(fn.type.signatures.length).toBeGreaterThanOrEqual(2);
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
it("assignabilityMode strict uses checker only", () => {
|
|
1158
|
+
const dir = createTempDir();
|
|
1159
|
+
const routeMod = join(dir, "route.ts");
|
|
1160
|
+
const bootstrap = join(dir, "bootstrap.ts");
|
|
1161
|
+
const main = join(dir, "main.ts");
|
|
1162
|
+
writeFileSync(
|
|
1163
|
+
routeMod,
|
|
1164
|
+
`
|
|
1165
|
+
export interface Route<P, S> { readonly path: P; readonly schema: S }
|
|
1166
|
+
export namespace Route {
|
|
1167
|
+
export type Any = Route<any, any>;
|
|
1168
|
+
export const Parse = <P extends string>(path: P): Route<P, any> =>
|
|
1169
|
+
({ path, schema: {} } as Route<P, any>);
|
|
1170
|
+
}
|
|
1171
|
+
`,
|
|
1172
|
+
"utf8",
|
|
1173
|
+
);
|
|
1174
|
+
writeFileSync(bootstrap, `import * as Route from "./route.js"; void Route; export {};`, "utf8");
|
|
1175
|
+
writeFileSync(
|
|
1176
|
+
main,
|
|
1177
|
+
`import * as Route from "./route.js"; export const r = Route.Parse("/status");`,
|
|
1178
|
+
"utf8",
|
|
1179
|
+
);
|
|
1180
|
+
|
|
1181
|
+
const program = makeProgram([bootstrap, main]);
|
|
1182
|
+
const specs = [
|
|
1183
|
+
{ id: "Route", module: "./route.js", exportName: "Route", typeMember: "Any" },
|
|
1184
|
+
] as const;
|
|
1185
|
+
const sessionCompat = createTypeInfoApiSession({
|
|
1186
|
+
ts,
|
|
1187
|
+
program,
|
|
1188
|
+
typeTargetSpecs: specs,
|
|
1189
|
+
failWhenNoTargetsResolved: false,
|
|
1190
|
+
assignabilityMode: "compatibility",
|
|
1191
|
+
});
|
|
1192
|
+
const sessionStrict = createTypeInfoApiSession({
|
|
1193
|
+
ts,
|
|
1194
|
+
program,
|
|
1195
|
+
typeTargetSpecs: specs,
|
|
1196
|
+
failWhenNoTargetsResolved: false,
|
|
1197
|
+
assignabilityMode: "strict",
|
|
1198
|
+
});
|
|
1199
|
+
const resCompat = sessionCompat.api.file("./main.ts", { baseDir: dir });
|
|
1200
|
+
const resStrict = sessionStrict.api.file("./main.ts", { baseDir: dir });
|
|
1201
|
+
expect(resCompat.ok && resStrict.ok).toBe(true);
|
|
1202
|
+
const rCompat = resCompat.ok
|
|
1203
|
+
? resCompat.snapshot.exports.find((e) => e.name === "r")
|
|
1204
|
+
: undefined;
|
|
1205
|
+
const rStrict = resStrict.ok
|
|
1206
|
+
? resStrict.snapshot.exports.find((e) => e.name === "r")
|
|
1207
|
+
: undefined;
|
|
1208
|
+
expect(sessionCompat.api.isAssignableTo(rCompat!.type, "Route")).toBe(true);
|
|
1209
|
+
expect(sessionStrict.api.isAssignableTo(rStrict!.type, "Route")).toBe(true);
|
|
1210
|
+
});
|
|
1211
|
+
});
|