@nudojs/harvester 0.2.0 → 0.2.2
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/package.json +11 -4
- package/CHANGELOG.md +0 -16
- package/src/__tests__/generated-env.d.ts +0 -8
- package/src/__tests__/harvest.test.ts +0 -108
- package/src/index.ts +0 -752
- package/tsconfig.json +0 -11
package/package.json
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nudojs/harvester",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Harvests TypeScript .d.ts declarations into Nudo env definitions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"main": "
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
8
9
|
"exports": {
|
|
9
|
-
".":
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
10
14
|
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
11
18
|
"repository": {
|
|
12
19
|
"type": "git",
|
|
13
20
|
"url": "git+https://github.com/nudojs/nudo.git",
|
|
@@ -20,7 +27,7 @@
|
|
|
20
27
|
},
|
|
21
28
|
"dependencies": {
|
|
22
29
|
"typescript": "^5.8.0",
|
|
23
|
-
"@nudojs/core": "0.
|
|
30
|
+
"@nudojs/core": "0.4.0"
|
|
24
31
|
},
|
|
25
32
|
"scripts": {
|
|
26
33
|
"build": "tsup src/index.ts --format esm --dts"
|
package/CHANGELOG.md
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# @nudojs/harvester
|
|
2
|
-
|
|
3
|
-
## 0.2.0
|
|
4
|
-
|
|
5
|
-
### Minor Changes
|
|
6
|
-
|
|
7
|
-
- 5786fa5: Auto-inject `@types` during analysis-path harvest and guard oversized dts work.
|
|
8
|
-
|
|
9
|
-
- Analysis can resolve package roots upward and feed harvested Abs into the B module graph.
|
|
10
|
-
- `harvestDts` accepts optional `maxFileBytes` / `maxMs` and skips unreadable or oversized files instead of failing the run.
|
|
11
|
-
|
|
12
|
-
### Patch Changes
|
|
13
|
-
|
|
14
|
-
- Updated dependencies [5786fa5]
|
|
15
|
-
- Updated dependencies [5786fa5]
|
|
16
|
-
- @nudojs/core@0.3.0
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
-
import { rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import nodePath from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { harvestDts, emitEnvModule, type HarvestedEnv } from "../index.ts";
|
|
6
|
-
|
|
7
|
-
const typesNode = nodePath.resolve(
|
|
8
|
-
nodePath.dirname(fileURLToPath(import.meta.url)),
|
|
9
|
-
"../../../..",
|
|
10
|
-
"node_modules/@types/node",
|
|
11
|
-
);
|
|
12
|
-
|
|
13
|
-
const files = [
|
|
14
|
-
"fs.d.ts",
|
|
15
|
-
"path.d.ts",
|
|
16
|
-
"os.d.ts",
|
|
17
|
-
"buffer.d.ts",
|
|
18
|
-
"buffer.buffer.d.ts",
|
|
19
|
-
"url.d.ts",
|
|
20
|
-
"process.d.ts",
|
|
21
|
-
].map((f) => nodePath.join(typesNode, f));
|
|
22
|
-
|
|
23
|
-
let env: HarvestedEnv;
|
|
24
|
-
let code: string;
|
|
25
|
-
|
|
26
|
-
beforeAll(() => {
|
|
27
|
-
env = harvestDts(files);
|
|
28
|
-
code = emitEnvModule(env, "@types/node");
|
|
29
|
-
}, 120_000);
|
|
30
|
-
|
|
31
|
-
describe("harvestDts", () => {
|
|
32
|
-
it("registers both node:-prefixed and bare module keys", () => {
|
|
33
|
-
expect(env.modules["node:fs"]).toBeDefined();
|
|
34
|
-
expect(env.modules.fs).toBeDefined();
|
|
35
|
-
expect(env.modules["node:path"]).toBeDefined();
|
|
36
|
-
expect(env.modules.path).toBeDefined();
|
|
37
|
-
expect(env.modules["node:os"]).toBeDefined();
|
|
38
|
-
expect(env.modules.os).toBeDefined();
|
|
39
|
-
// The bare key aliases the same record as its node: twin.
|
|
40
|
-
expect(env.modules.fs).toBe(env.modules["node:fs"]);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("maps fs.readFileSync to a function returning union(string, Buffer)", () => {
|
|
44
|
-
const fn = env.modules["node:fs"].readFileSync as Record<string, unknown>;
|
|
45
|
-
expect(fn.kind).toBe("function");
|
|
46
|
-
const sig = fn._signature as {
|
|
47
|
-
returnType: { kind: string; members?: Array<{ kind: string; type?: string; className?: string }> };
|
|
48
|
-
};
|
|
49
|
-
expect(sig).toBeDefined();
|
|
50
|
-
expect(sig.returnType.kind).toBe("union");
|
|
51
|
-
const members = sig.returnType.members ?? [];
|
|
52
|
-
const hasString = members.some((m) => m.kind === "primitive" && m.type === "string");
|
|
53
|
-
const hasBuffer = members.some((m) => m.kind === "instance" && m.className === "Buffer");
|
|
54
|
-
expect(hasString).toBe(true);
|
|
55
|
-
expect(hasBuffer).toBe(true);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("maps path.join and os.platform", () => {
|
|
59
|
-
expect(env.modules.path.join.kind).toBe("function");
|
|
60
|
-
const platform = env.modules.os.platform as Record<string, unknown>;
|
|
61
|
-
expect(platform.kind).toBe("function");
|
|
62
|
-
const ret = (platform._signature as { returnType: { kind: string; type?: string; members?: unknown[] } })
|
|
63
|
-
.returnType;
|
|
64
|
-
const isString = ret.kind === "primitive" && ret.type === "string";
|
|
65
|
-
const isStringLiteralUnion =
|
|
66
|
-
ret.kind === "union" &&
|
|
67
|
-
(ret.members ?? []).every(
|
|
68
|
-
(m) => (m as { kind: string; value: unknown }).kind === "literal" && typeof (m as { value: unknown }).value === "string",
|
|
69
|
-
);
|
|
70
|
-
expect(isString || isStringLiteralUnion).toBe(true);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("exposes harvest stats and global-block symbols", () => {
|
|
74
|
-
expect(env.stats.files).toBeGreaterThanOrEqual(3);
|
|
75
|
-
expect(env.stats.files).toBe(files.length);
|
|
76
|
-
expect(env.stats.symbols).toBeGreaterThan(50);
|
|
77
|
-
// process.d.ts declares `global { var process: NodeJS.Process; }`.
|
|
78
|
-
expect(env.globals.process).toBeDefined();
|
|
79
|
-
expect(env.globals.process.kind).toBe("instance");
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe("emitEnvModule", () => {
|
|
84
|
-
const generatedUrl = new URL("./generated-env.tmp.ts", import.meta.url);
|
|
85
|
-
const generatedPath = fileURLToPath(generatedUrl);
|
|
86
|
-
|
|
87
|
-
afterAll(() => {
|
|
88
|
-
rmSync(generatedPath, { force: true });
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("emits importable TS source", () => {
|
|
92
|
-
expect(code).toContain("// Auto-generated by nudo harvest — DO NOT EDIT");
|
|
93
|
-
expect(code).toContain('import { type TypeValue, T } from "@nudojs/core"');
|
|
94
|
-
expect(code).toContain("export function defineEnv");
|
|
95
|
-
expect(code).toContain("T.fnSig");
|
|
96
|
-
expect(code).toContain("T.instanceOf");
|
|
97
|
-
writeFileSync(generatedPath, code);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("emitted module evaluates and defines the env", { timeout: 120_000 }, async () => {
|
|
101
|
-
const mod = await import("./generated-env.tmp.ts");
|
|
102
|
-
const evaluated = mod.defineEnv();
|
|
103
|
-
expect(evaluated.modules.fs).toBeDefined();
|
|
104
|
-
expect(evaluated.modules["node:fs"].readFileSync).toBeDefined();
|
|
105
|
-
expect((evaluated.modules["node:fs"].readFileSync as { kind: string }).kind).toBe("function");
|
|
106
|
-
expect((evaluated.globals.process as { kind: string }).kind).toBe("instance");
|
|
107
|
-
});
|
|
108
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,752 +0,0 @@
|
|
|
1
|
-
import { readFileSync, statSync } from "node:fs";
|
|
2
|
-
import ts from "typescript";
|
|
3
|
-
import { type TypeValue, typeValueEquals, getFnSig, T } from "@nudojs/core";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* The result of harvesting a set of `.d.ts` files: a Nudo env shaped exactly
|
|
7
|
-
* like `EnvDefinition` (`{ globals, modules }`), plus harvest statistics.
|
|
8
|
-
*/
|
|
9
|
-
export type HarvestedEnv = {
|
|
10
|
-
globals: Record<string, TypeValue>;
|
|
11
|
-
modules: Record<string, Record<string, TypeValue>>;
|
|
12
|
-
stats: { files: number; symbols: number; skipped: number };
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
type NamedTypeDecl = ts.InterfaceDeclaration | ts.ClassDeclaration;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Runtime symbol collected in phase 1, materialized to a TypeValue in phase 2
|
|
19
|
-
* once the whole symbol table is populated (cross-file references resolve
|
|
20
|
-
* regardless of file order).
|
|
21
|
-
*/
|
|
22
|
-
type PendingSymbol =
|
|
23
|
-
| { t: "fn"; scope: Scope; name: string; decls: ts.FunctionDeclaration[] }
|
|
24
|
-
| { t: "var"; scope: Scope; name: string; typeNode?: ts.TypeNode }
|
|
25
|
-
/** class / value-position interface re-export; `lookup` is the symbol-table name when the exported name differs */
|
|
26
|
-
| { t: "value"; scope: Scope; name: string; lookup?: string }
|
|
27
|
-
| { t: "enum"; scope: Scope; name: string };
|
|
28
|
-
|
|
29
|
-
interface HarvestContext {
|
|
30
|
-
globals: Record<string, TypeValue>;
|
|
31
|
-
modules: Record<string, Record<string, TypeValue>>;
|
|
32
|
-
/** simple name → interface/class declaration (first wins) */
|
|
33
|
-
interfaces: Map<string, NamedTypeDecl>;
|
|
34
|
-
/** dotted name ("NodeJS.Process") → declaration (first wins) */
|
|
35
|
-
qualifiedInterfaces: Map<string, NamedTypeDecl>;
|
|
36
|
-
/** simple name → type alias declaration (first wins) */
|
|
37
|
-
aliases: Map<string, ts.TypeAliasDeclaration>;
|
|
38
|
-
/** dotted name → type alias declaration (first wins) */
|
|
39
|
-
qualifiedAliases: Map<string, ts.TypeAliasDeclaration>;
|
|
40
|
-
/** simple name → class declaration (runtime values, for `export { X }` re-exports) */
|
|
41
|
-
classes: Map<string, ts.ClassDeclaration>;
|
|
42
|
-
instanceCache: Map<string, TypeValue>;
|
|
43
|
-
expanding: Set<string>;
|
|
44
|
-
/** [aliasModule, targetModule] pairs from `export * from` / `export = x` forms */
|
|
45
|
-
moduleAliases: Array<[alias: string, target: string]>;
|
|
46
|
-
/** `import x = require("mod")` bindings, per scope key */
|
|
47
|
-
importEquals: Map<string, Map<string, string>>;
|
|
48
|
-
declaredModules: Set<string>;
|
|
49
|
-
pending: PendingSymbol[];
|
|
50
|
-
symbols: number;
|
|
51
|
-
skipped: number;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
interface Scope {
|
|
55
|
-
/** enclosing `declare module "..."` name, or undefined for global scope */
|
|
56
|
-
moduleName: string | undefined;
|
|
57
|
-
/** enclosing namespace chain, e.g. ["NodeJS"] */
|
|
58
|
-
ns: string[];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const MAX_ALIAS_DEPTH = 8;
|
|
62
|
-
|
|
63
|
-
function scopeKey(scope: Scope): string {
|
|
64
|
-
return scope.moduleName ?? "\0global";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ---------------------------------------------------------------------------
|
|
68
|
-
// Public API
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
|
|
71
|
-
export function harvestDts(
|
|
72
|
-
files: string[],
|
|
73
|
-
opts?: { maxFileBytes?: number; maxMs?: number },
|
|
74
|
-
): HarvestedEnv {
|
|
75
|
-
const maxBytes = opts?.maxFileBytes ?? 1_500_000;
|
|
76
|
-
const deadline = opts?.maxMs !== undefined ? Date.now() + opts.maxMs : undefined;
|
|
77
|
-
const ctx: HarvestContext = {
|
|
78
|
-
globals: {},
|
|
79
|
-
modules: {},
|
|
80
|
-
interfaces: new Map(),
|
|
81
|
-
qualifiedInterfaces: new Map(),
|
|
82
|
-
aliases: new Map(),
|
|
83
|
-
qualifiedAliases: new Map(),
|
|
84
|
-
classes: new Map(),
|
|
85
|
-
instanceCache: new Map(),
|
|
86
|
-
expanding: new Set(),
|
|
87
|
-
moduleAliases: [],
|
|
88
|
-
importEquals: new Map(),
|
|
89
|
-
declaredModules: new Set(),
|
|
90
|
-
pending: [],
|
|
91
|
-
symbols: 0,
|
|
92
|
-
skipped: 0,
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
// Phase 1: collect declarations into the shared symbol table.
|
|
96
|
-
let parsed = 0;
|
|
97
|
-
for (const file of files) {
|
|
98
|
-
if (deadline !== undefined && Date.now() > deadline) {
|
|
99
|
-
ctx.skipped += files.length - parsed;
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
102
|
-
let content: string;
|
|
103
|
-
try {
|
|
104
|
-
const st = statSync(file);
|
|
105
|
-
if (st.size > maxBytes) {
|
|
106
|
-
ctx.skipped++;
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
content = readFileSync(file, "utf8");
|
|
110
|
-
} catch {
|
|
111
|
-
ctx.skipped++;
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
const sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
|
|
115
|
-
collectStatements(ctx, sourceFile.statements, { moduleName: undefined, ns: [] });
|
|
116
|
-
parsed++;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Phase 2: materialize TypeValues with the complete symbol table available.
|
|
120
|
-
materialize(ctx);
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
globals: ctx.globals,
|
|
124
|
-
modules: ctx.modules,
|
|
125
|
-
stats: { files: parsed, symbols: ctx.symbols, skipped: ctx.skipped },
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function emitEnvModule(env: HarvestedEnv, pkgName: string): string {
|
|
130
|
-
// Module records can be shared (e.g. "fs" aliasing "node:fs"); emit those
|
|
131
|
-
// once as a const and reference it under every key.
|
|
132
|
-
const moduleEntries = Object.entries(env.modules);
|
|
133
|
-
const keysByRecord = new Map<Record<string, TypeValue>, string[]>();
|
|
134
|
-
for (const [moduleName, record] of moduleEntries) {
|
|
135
|
-
const keys = keysByRecord.get(record);
|
|
136
|
-
if (keys) keys.push(moduleName);
|
|
137
|
-
else keysByRecord.set(record, [moduleName]);
|
|
138
|
-
}
|
|
139
|
-
const constNameByRecord = new Map<Record<string, TypeValue>, string>();
|
|
140
|
-
const constLines: string[] = [];
|
|
141
|
-
let constIndex = 0;
|
|
142
|
-
for (const [record, keys] of keysByRecord) {
|
|
143
|
-
if (keys.length < 2) continue;
|
|
144
|
-
const constName = `mod${constIndex++}`;
|
|
145
|
-
constNameByRecord.set(record, constName);
|
|
146
|
-
constLines.push(` const ${constName}: Record<string, TypeValue> = {`);
|
|
147
|
-
emitEntries(constLines, record, 2);
|
|
148
|
-
constLines.push(` };`);
|
|
149
|
-
constLines.push("");
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const lines: string[] = [];
|
|
153
|
-
lines.push("// Auto-generated by nudo harvest — DO NOT EDIT");
|
|
154
|
-
lines.push(`// Source package: ${pkgName}`);
|
|
155
|
-
if (constLines.length > 0) {
|
|
156
|
-
lines.push('import { type TypeValue, T } from "@nudojs/core";');
|
|
157
|
-
} else {
|
|
158
|
-
lines.push(`import { T } from "@nudojs/core";`);
|
|
159
|
-
}
|
|
160
|
-
lines.push("");
|
|
161
|
-
lines.push("export function defineEnv() {");
|
|
162
|
-
lines.push(...constLines);
|
|
163
|
-
lines.push(" return {");
|
|
164
|
-
lines.push(" globals: {");
|
|
165
|
-
emitEntries(lines, env.globals, 3);
|
|
166
|
-
lines.push(" },");
|
|
167
|
-
lines.push(" modules: {");
|
|
168
|
-
for (const [moduleName, record] of moduleEntries) {
|
|
169
|
-
const constName = constNameByRecord.get(record);
|
|
170
|
-
if (constName !== undefined) {
|
|
171
|
-
lines.push(`${indent(3)}${emitKey(moduleName)}: ${constName},`);
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
lines.push(`${indent(3)}${emitKey(moduleName)}: {`);
|
|
175
|
-
emitEntries(lines, record, 4);
|
|
176
|
-
lines.push(`${indent(3)}},`);
|
|
177
|
-
}
|
|
178
|
-
lines.push(" },");
|
|
179
|
-
lines.push(" };");
|
|
180
|
-
lines.push("}");
|
|
181
|
-
lines.push("");
|
|
182
|
-
return lines.join("\n");
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// ---------------------------------------------------------------------------
|
|
186
|
-
// Phase 1: collect declarations
|
|
187
|
-
// ---------------------------------------------------------------------------
|
|
188
|
-
|
|
189
|
-
function recordFor(ctx: HarvestContext, scope: Scope): Record<string, TypeValue> {
|
|
190
|
-
if (scope.moduleName === undefined) return ctx.globals;
|
|
191
|
-
let rec = ctx.modules[scope.moduleName];
|
|
192
|
-
if (!rec) {
|
|
193
|
-
rec = {};
|
|
194
|
-
ctx.modules[scope.moduleName] = rec;
|
|
195
|
-
}
|
|
196
|
-
return rec;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function collectStatements(ctx: HarvestContext, statements: readonly ts.Statement[], scope: Scope): void {
|
|
200
|
-
for (const stmt of statements) {
|
|
201
|
-
collectStatement(ctx, stmt, scope);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function collectStatement(ctx: HarvestContext, stmt: ts.Statement, scope: Scope): void {
|
|
206
|
-
if (ts.isModuleDeclaration(stmt)) {
|
|
207
|
-
const body = stmt.body;
|
|
208
|
-
if (!body || !ts.isModuleBlock(body)) {
|
|
209
|
-
ctx.skipped++;
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
if (ts.isStringLiteral(stmt.name)) {
|
|
213
|
-
// `declare module "node:fs" { ... }`
|
|
214
|
-
ctx.declaredModules.add(stmt.name.text);
|
|
215
|
-
collectStatements(ctx, body.statements, { moduleName: stmt.name.text, ns: [] });
|
|
216
|
-
} else if (stmt.name.text === "global") {
|
|
217
|
-
// `global { ... }` inside a module → globals
|
|
218
|
-
collectStatements(ctx, body.statements, { moduleName: undefined, ns: [] });
|
|
219
|
-
} else {
|
|
220
|
-
// `namespace X { ... }` → types registered with qualified names,
|
|
221
|
-
// runtime symbols flattened into the enclosing scope.
|
|
222
|
-
collectStatements(ctx, body.statements, { moduleName: scope.moduleName, ns: [...scope.ns, stmt.name.text] });
|
|
223
|
-
}
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (ts.isFunctionDeclaration(stmt)) {
|
|
228
|
-
if (!stmt.name) {
|
|
229
|
-
ctx.skipped++;
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
const name = stmt.name.text;
|
|
233
|
-
const existing = ctx.pending.find((p) => p.t === "fn" && p.name === name && scopeKey(p.scope) === scopeKey(scope));
|
|
234
|
-
if (existing && existing.t === "fn") {
|
|
235
|
-
existing.decls.push(stmt); // overload
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
ctx.pending.push({ t: "fn", scope, name, decls: [stmt] });
|
|
239
|
-
ctx.symbols++;
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (ts.isVariableStatement(stmt)) {
|
|
244
|
-
for (const decl of stmt.declarationList.declarations) {
|
|
245
|
-
if (!ts.isIdentifier(decl.name)) {
|
|
246
|
-
ctx.skipped++;
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
ctx.pending.push({ t: "var", scope, name: decl.name.text, typeNode: decl.type });
|
|
250
|
-
ctx.symbols++;
|
|
251
|
-
}
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (ts.isInterfaceDeclaration(stmt)) {
|
|
256
|
-
registerTypeDecl(ctx, scope, stmt.name.text, stmt);
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (ts.isTypeAliasDeclaration(stmt)) {
|
|
261
|
-
registerAlias(ctx, scope, stmt);
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (ts.isClassDeclaration(stmt)) {
|
|
266
|
-
if (!stmt.name) {
|
|
267
|
-
ctx.skipped++;
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
registerTypeDecl(ctx, scope, stmt.name.text, stmt);
|
|
271
|
-
if (!ctx.classes.has(stmt.name.text)) ctx.classes.set(stmt.name.text, stmt);
|
|
272
|
-
// A class is also a runtime value of its scope.
|
|
273
|
-
ctx.pending.push({ t: "value", scope, name: stmt.name.text });
|
|
274
|
-
ctx.symbols++;
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (ts.isEnumDeclaration(stmt)) {
|
|
279
|
-
if (stmt.name) {
|
|
280
|
-
ctx.pending.push({ t: "enum", scope, name: stmt.name.text });
|
|
281
|
-
ctx.symbols++;
|
|
282
|
-
}
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (ts.isImportDeclaration(stmt)) {
|
|
287
|
-
// Type-only imports; names resolve through the shared flat symbol table.
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (ts.isImportEqualsDeclaration(stmt)) {
|
|
292
|
-
// `import path = require("node:path")` — record for `export = path` aliasing.
|
|
293
|
-
if (
|
|
294
|
-
ts.isIdentifier(stmt.name) &&
|
|
295
|
-
stmt.moduleReference &&
|
|
296
|
-
ts.isExternalModuleReference(stmt.moduleReference) &&
|
|
297
|
-
ts.isStringLiteral(stmt.moduleReference.expression)
|
|
298
|
-
) {
|
|
299
|
-
const key = scopeKey(scope);
|
|
300
|
-
let bindings = ctx.importEquals.get(key);
|
|
301
|
-
if (!bindings) {
|
|
302
|
-
bindings = new Map();
|
|
303
|
-
ctx.importEquals.set(key, bindings);
|
|
304
|
-
}
|
|
305
|
-
bindings.set(stmt.name.text, stmt.moduleReference.expression.text);
|
|
306
|
-
}
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (ts.isExportDeclaration(stmt)) {
|
|
311
|
-
const specifier = stmt.moduleSpecifier;
|
|
312
|
-
if (specifier && ts.isStringLiteral(specifier)) {
|
|
313
|
-
if (stmt.exportClause && ts.isNamespaceExport(stmt.exportClause)) {
|
|
314
|
-
// `export * as promises from "..."` — namespace re-export, v1 skip.
|
|
315
|
-
ctx.skipped++;
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
if (!stmt.exportClause) {
|
|
319
|
-
// `export * from "node:fs"` — alias the module.
|
|
320
|
-
if (scope.moduleName !== undefined) {
|
|
321
|
-
ctx.moduleAliases.push([scope.moduleName, specifier.text]);
|
|
322
|
-
}
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
// Named re-exports from another module — v1 skip.
|
|
326
|
-
ctx.skipped++;
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
// `export { Buffer }` — re-export a local runtime value.
|
|
330
|
-
if (stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {
|
|
331
|
-
for (const el of stmt.exportClause.elements) {
|
|
332
|
-
const localName = (el.propertyName ?? el.name).text;
|
|
333
|
-
const exportedName = el.name.text;
|
|
334
|
-
const isValue =
|
|
335
|
-
ctx.classes.has(localName) ||
|
|
336
|
-
(!el.isTypeOnly && (ctx.interfaces.has(localName) || ctx.qualifiedInterfaces.has(localName)));
|
|
337
|
-
if (isValue) {
|
|
338
|
-
ctx.pending.push({ t: "value", scope, name: exportedName, lookup: localName });
|
|
339
|
-
ctx.symbols++;
|
|
340
|
-
} else {
|
|
341
|
-
ctx.skipped++;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
ctx.skipped++;
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
if (ts.isExportAssignment(stmt)) {
|
|
351
|
-
// `export = x` where x is `import x = require("mod")` → module alias.
|
|
352
|
-
if (stmt.expression && ts.isIdentifier(stmt.expression) && scope.moduleName !== undefined) {
|
|
353
|
-
const target = ctx.importEquals.get(scopeKey(scope))?.get(stmt.expression.text);
|
|
354
|
-
if (target !== undefined) {
|
|
355
|
-
ctx.moduleAliases.push([scope.moduleName, target]);
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
ctx.skipped++;
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
ctx.skipped++;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function registerTypeDecl(ctx: HarvestContext, scope: Scope, name: string, decl: NamedTypeDecl): void {
|
|
367
|
-
if (!ctx.interfaces.has(name)) ctx.interfaces.set(name, decl);
|
|
368
|
-
if (ts.isClassDeclaration(decl)) return; // classes register only their simple name
|
|
369
|
-
const qualified = [...scope.ns, name].join(".");
|
|
370
|
-
if (qualified !== name && !ctx.qualifiedInterfaces.has(qualified)) {
|
|
371
|
-
ctx.qualifiedInterfaces.set(qualified, decl);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function registerAlias(ctx: HarvestContext, scope: Scope, stmt: ts.TypeAliasDeclaration): void {
|
|
376
|
-
const name = stmt.name.text;
|
|
377
|
-
if (!ctx.aliases.has(name)) ctx.aliases.set(name, stmt);
|
|
378
|
-
const qualified = [...scope.ns, name].join(".");
|
|
379
|
-
if (qualified !== name && !ctx.qualifiedAliases.has(qualified)) {
|
|
380
|
-
ctx.qualifiedAliases.set(qualified, stmt);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// ---------------------------------------------------------------------------
|
|
385
|
-
// Phase 2: materialize TypeValues
|
|
386
|
-
// ---------------------------------------------------------------------------
|
|
387
|
-
|
|
388
|
-
function materialize(ctx: HarvestContext): void {
|
|
389
|
-
for (const p of ctx.pending) {
|
|
390
|
-
const rec = recordFor(ctx, p.scope);
|
|
391
|
-
if (p.t === "fn") {
|
|
392
|
-
if (rec[p.name] !== undefined) continue; // same name already handled (first wins)
|
|
393
|
-
const value = signatureToFnSig(ctx, p.decls[0]!); // first overload's parameter list
|
|
394
|
-
const sig = getFnSig(value);
|
|
395
|
-
if (sig) {
|
|
396
|
-
// Merge return types across overloads; drop unknown members when a
|
|
397
|
-
// known one exists so unresolved references don't erase information.
|
|
398
|
-
const returns: TypeValue[] = [];
|
|
399
|
-
for (const decl of p.decls) {
|
|
400
|
-
if (!decl.type) continue;
|
|
401
|
-
const ret = mapType(ctx, decl.type, 0);
|
|
402
|
-
if (ret.kind !== "unknown") returns.push(ret);
|
|
403
|
-
}
|
|
404
|
-
sig.returnType =
|
|
405
|
-
returns.length > 0
|
|
406
|
-
? returns.reduce((acc, r) => (typeValueEquals(acc, r) ? acc : T.union(acc, r)))
|
|
407
|
-
: T.unknown;
|
|
408
|
-
}
|
|
409
|
-
rec[p.name] = value;
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
if (rec[p.name] !== undefined) continue;
|
|
413
|
-
switch (p.t) {
|
|
414
|
-
case "var": {
|
|
415
|
-
// 映射为 unknown 的声明零信息量,且会遮蔽 evaluator 内置
|
|
416
|
-
// (如 URL/AbortController 的 createXxxType 构造),跳过更优。
|
|
417
|
-
const mapped = p.typeNode ? mapType(ctx, p.typeNode, 0) : T.unknown;
|
|
418
|
-
if (mapped.kind === "unknown") {
|
|
419
|
-
ctx.skipped++;
|
|
420
|
-
break;
|
|
421
|
-
}
|
|
422
|
-
rec[p.name] = mapped;
|
|
423
|
-
break;
|
|
424
|
-
}
|
|
425
|
-
case "value": {
|
|
426
|
-
// class 作为运行时值 = 构造函数:new X() 走 fnSig → instance。
|
|
427
|
-
// 裸 instance 会被 new 的通用路径当作非函数 callee 退化为 unknown。
|
|
428
|
-
const cls = ctx.classes.get(p.lookup ?? p.name);
|
|
429
|
-
if (cls) {
|
|
430
|
-
const ctor = (cls.members as readonly ts.ClassElement[]).find(ts.isConstructorDeclaration);
|
|
431
|
-
const params: TypeValue[] = [];
|
|
432
|
-
if (ctor) {
|
|
433
|
-
for (const param of ctor.parameters) {
|
|
434
|
-
let type = param.type ? mapType(ctx, param.type, 0) : T.unknown;
|
|
435
|
-
if (param.questionToken) type = T.union(type, T.undefined);
|
|
436
|
-
params.push(type);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
rec[p.name] = T.fnSig(params, instanceFor(ctx, p.lookup ?? p.name));
|
|
440
|
-
} else {
|
|
441
|
-
rec[p.name] = instanceFor(ctx, p.lookup ?? p.name);
|
|
442
|
-
}
|
|
443
|
-
break;
|
|
444
|
-
}
|
|
445
|
-
case "enum":
|
|
446
|
-
rec[p.name] = T.unknown;
|
|
447
|
-
break;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Apply module aliases (`declare module "fs" { export * from "node:fs"; }`
|
|
452
|
-
// and the `export = path` form).
|
|
453
|
-
for (const [alias, target] of ctx.moduleAliases) {
|
|
454
|
-
if (ctx.modules[target] && !ctx.modules[alias]) {
|
|
455
|
-
ctx.modules[alias] = ctx.modules[target]!;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// ---------------------------------------------------------------------------
|
|
461
|
-
// Type mapping
|
|
462
|
-
// ---------------------------------------------------------------------------
|
|
463
|
-
|
|
464
|
-
function typeNameString(name: ts.EntityName): string {
|
|
465
|
-
if (ts.isIdentifier(name)) return name.text;
|
|
466
|
-
return `${typeNameString(name.left)}.${name.right.text}`;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
function memberName(node: { name?: ts.PropertyName }): string | undefined {
|
|
470
|
-
const n = node.name;
|
|
471
|
-
if (!n) return undefined;
|
|
472
|
-
if (ts.isIdentifier(n) || ts.isStringLiteral(n) || ts.isNumericLiteral(n)) return n.text;
|
|
473
|
-
return undefined; // computed / private names
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function instanceFor(ctx: HarvestContext, key: string): TypeValue {
|
|
477
|
-
const cached = ctx.instanceCache.get(key);
|
|
478
|
-
if (cached !== undefined) return cached;
|
|
479
|
-
const decl = ctx.interfaces.get(key) ?? ctx.qualifiedInterfaces.get(key);
|
|
480
|
-
if (!decl) return T.unknown;
|
|
481
|
-
if (ctx.expanding.has(key)) {
|
|
482
|
-
// Recursion guard: reference by name without expanding members.
|
|
483
|
-
return T.instanceOf(decl.name?.text ?? key, {});
|
|
484
|
-
}
|
|
485
|
-
ctx.expanding.add(key);
|
|
486
|
-
const properties: Record<string, TypeValue> = {};
|
|
487
|
-
try {
|
|
488
|
-
const members = decl.members as readonly (ts.TypeElement | ts.ClassElement)[];
|
|
489
|
-
for (const member of members) {
|
|
490
|
-
const modifiers = (member as { modifiers?: readonly { kind: ts.SyntaxKind }[] }).modifiers;
|
|
491
|
-
if (modifiers?.some((mod) => mod.kind === ts.SyntaxKind.StaticKeyword)) continue;
|
|
492
|
-
if (ts.isPropertySignature(member) || ts.isPropertyDeclaration(member)) {
|
|
493
|
-
const name = memberName(member);
|
|
494
|
-
if (name === undefined) continue;
|
|
495
|
-
let type = member.type ? mapType(ctx, member.type, 0) : T.unknown;
|
|
496
|
-
if (member.questionToken) type = T.union(type, T.undefined);
|
|
497
|
-
properties[name] = type;
|
|
498
|
-
} else if (ts.isMethodSignature(member) || ts.isMethodDeclaration(member)) {
|
|
499
|
-
const name = memberName(member);
|
|
500
|
-
if (name === undefined || properties[name] !== undefined) continue; // first overload wins
|
|
501
|
-
properties[name] = signatureToFnSig(ctx, member);
|
|
502
|
-
} else if (ts.isGetAccessorDeclaration(member) || ts.isSetAccessorDeclaration(member)) {
|
|
503
|
-
const name = memberName(member);
|
|
504
|
-
if (name === undefined || properties[name] !== undefined || !member.type) continue;
|
|
505
|
-
properties[name] = mapType(ctx, member.type, 0);
|
|
506
|
-
}
|
|
507
|
-
// call / construct / index signatures are not representable — skipped.
|
|
508
|
-
}
|
|
509
|
-
} finally {
|
|
510
|
-
ctx.expanding.delete(key);
|
|
511
|
-
}
|
|
512
|
-
const value = T.instanceOf(decl.name?.text ?? key, properties);
|
|
513
|
-
ctx.instanceCache.set(key, value);
|
|
514
|
-
return value;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function signatureToFnSig(ctx: HarvestContext, sig: ts.SignatureDeclaration): TypeValue {
|
|
518
|
-
const paramTypes = sig.parameters.map((param) => {
|
|
519
|
-
let type: TypeValue;
|
|
520
|
-
if (param.dotDotDotToken) {
|
|
521
|
-
const inner = param.type && ts.isArrayTypeNode(param.type) ? param.type.elementType : param.type;
|
|
522
|
-
type = inner ? T.array(mapType(ctx, inner, 0)) : T.unknown;
|
|
523
|
-
} else {
|
|
524
|
-
type = param.type ? mapType(ctx, param.type, 0) : T.unknown;
|
|
525
|
-
}
|
|
526
|
-
if (param.questionToken && !param.dotDotDotToken) type = T.union(type, T.undefined);
|
|
527
|
-
return type;
|
|
528
|
-
});
|
|
529
|
-
const returnType = sig.type ? mapType(ctx, sig.type, 0) : T.unknown;
|
|
530
|
-
return T.fnSig(paramTypes, returnType);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
function mapTypeMembers(
|
|
534
|
-
ctx: HarvestContext,
|
|
535
|
-
members: readonly ts.TypeElement[],
|
|
536
|
-
depth: number,
|
|
537
|
-
): Record<string, TypeValue> {
|
|
538
|
-
const properties: Record<string, TypeValue> = {};
|
|
539
|
-
for (const member of members) {
|
|
540
|
-
if (ts.isPropertySignature(member)) {
|
|
541
|
-
const name = memberName(member);
|
|
542
|
-
if (name === undefined) continue;
|
|
543
|
-
let type = member.type ? mapType(ctx, member.type, depth) : T.unknown;
|
|
544
|
-
if (member.questionToken) type = T.union(type, T.undefined);
|
|
545
|
-
properties[name] = type;
|
|
546
|
-
} else if (ts.isMethodSignature(member)) {
|
|
547
|
-
const name = memberName(member);
|
|
548
|
-
if (name === undefined || properties[name] !== undefined) continue; // first overload wins
|
|
549
|
-
properties[name] = signatureToFnSig(ctx, member);
|
|
550
|
-
}
|
|
551
|
-
// index / call / construct signatures skipped
|
|
552
|
-
}
|
|
553
|
-
return properties;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
function mapTypeRef(ctx: HarvestContext, node: ts.TypeReferenceNode, depth: number): TypeValue {
|
|
557
|
-
const name = typeNameString(node.typeName);
|
|
558
|
-
const args = node.typeArguments ?? [];
|
|
559
|
-
if ((name === "Array" || name === "ReadonlyArray") && args.length >= 1) {
|
|
560
|
-
return T.array(mapType(ctx, args[0], depth));
|
|
561
|
-
}
|
|
562
|
-
if (name === "Promise" && args.length >= 1) {
|
|
563
|
-
return T.promise(mapType(ctx, args[0], depth));
|
|
564
|
-
}
|
|
565
|
-
if (name === "Record") {
|
|
566
|
-
// Index-signature objects are approximated as an open object type.
|
|
567
|
-
return T.object({});
|
|
568
|
-
}
|
|
569
|
-
if (ctx.interfaces.has(name) || ctx.qualifiedInterfaces.has(name)) {
|
|
570
|
-
return instanceFor(ctx, name);
|
|
571
|
-
}
|
|
572
|
-
const alias = ctx.aliases.get(name) ?? ctx.qualifiedAliases.get(name);
|
|
573
|
-
if (alias) return mapType(ctx, alias.type, depth + 1);
|
|
574
|
-
return T.unknown;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
function mapType(ctx: HarvestContext, node: ts.TypeNode | undefined, depth: number): TypeValue {
|
|
578
|
-
if (!node || depth > MAX_ALIAS_DEPTH) return T.unknown;
|
|
579
|
-
|
|
580
|
-
if (ts.isParenthesizedTypeNode(node)) return mapType(ctx, node.type, depth);
|
|
581
|
-
if (ts.isTypeReferenceNode(node)) return mapTypeRef(ctx, node, depth);
|
|
582
|
-
|
|
583
|
-
if (ts.isArrayTypeNode(node)) return T.array(mapType(ctx, node.elementType, depth));
|
|
584
|
-
if (ts.isTupleTypeNode(node)) {
|
|
585
|
-
for (const element of node.elements) {
|
|
586
|
-
const inner = ts.isNamedTupleMember(element) ? element.type : element;
|
|
587
|
-
return T.array(mapType(ctx, inner, depth)); // approximate by the first element
|
|
588
|
-
}
|
|
589
|
-
return T.unknown;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
if (ts.isUnionTypeNode(node)) {
|
|
593
|
-
return T.union(...node.types.map((t) => mapType(ctx, t, depth)));
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
if (ts.isIntersectionTypeNode(node)) {
|
|
597
|
-
const members = node.types.map((t) => mapType(ctx, t, depth));
|
|
598
|
-
if (members.length > 0 && members.every((m) => m.kind === "object")) {
|
|
599
|
-
const properties: Record<string, TypeValue> = {};
|
|
600
|
-
for (const member of members) {
|
|
601
|
-
Object.assign(properties, (member as { properties: Record<string, TypeValue> }).properties);
|
|
602
|
-
}
|
|
603
|
-
return T.object(properties);
|
|
604
|
-
}
|
|
605
|
-
return members[0] ?? T.unknown;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
if (ts.isLiteralTypeNode(node)) {
|
|
609
|
-
const lit = node.literal;
|
|
610
|
-
if (ts.isStringLiteral(lit)) return T.literal(lit.text);
|
|
611
|
-
if (ts.isNumericLiteral(lit)) return T.literal(Number(lit.text));
|
|
612
|
-
if (lit.kind === ts.SyntaxKind.TrueKeyword) return T.literal(true);
|
|
613
|
-
if (lit.kind === ts.SyntaxKind.FalseKeyword) return T.literal(false);
|
|
614
|
-
if (
|
|
615
|
-
ts.isPrefixUnaryExpression(lit) &&
|
|
616
|
-
lit.operator === ts.SyntaxKind.MinusToken &&
|
|
617
|
-
ts.isNumericLiteral(lit.operand)
|
|
618
|
-
) {
|
|
619
|
-
return T.literal(-Number(lit.operand.text));
|
|
620
|
-
}
|
|
621
|
-
return T.unknown;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
if (ts.isFunctionTypeNode(node)) return signatureToFnSig(ctx, node);
|
|
625
|
-
|
|
626
|
-
if (ts.isTypeLiteralNode(node)) {
|
|
627
|
-
// 带构造签名的对象字面量类型(如 @types/node 的 var URL: { new(...): URL; ... })
|
|
628
|
-
// 是构造函数形状:new X() 需要 function callee + fnSig 返回实例。
|
|
629
|
-
const ctor = node.members.find(ts.isConstructSignatureDeclaration);
|
|
630
|
-
if (ctor) {
|
|
631
|
-
const params: TypeValue[] = [];
|
|
632
|
-
for (const param of ctor.parameters) {
|
|
633
|
-
let type = param.type ? mapType(ctx, param.type, depth) : T.unknown;
|
|
634
|
-
if (param.questionToken) type = T.union(type, T.undefined);
|
|
635
|
-
params.push(type);
|
|
636
|
-
}
|
|
637
|
-
return T.fnSig(params, ctor.type ? mapType(ctx, ctor.type, depth) : T.unknown);
|
|
638
|
-
}
|
|
639
|
-
return T.object(mapTypeMembers(ctx, node.members, depth));
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
if (ts.isTypeOperatorNode(node)) {
|
|
643
|
-
return node.operator === ts.SyntaxKind.ReadonlyKeyword ? mapType(ctx, node.type, depth) : T.unknown;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
switch (node.kind) {
|
|
647
|
-
case ts.SyntaxKind.StringKeyword:
|
|
648
|
-
return T.string;
|
|
649
|
-
case ts.SyntaxKind.NumberKeyword:
|
|
650
|
-
return T.number;
|
|
651
|
-
case ts.SyntaxKind.BooleanKeyword:
|
|
652
|
-
return T.boolean;
|
|
653
|
-
case ts.SyntaxKind.BigIntKeyword:
|
|
654
|
-
return T.bigint;
|
|
655
|
-
case ts.SyntaxKind.SymbolKeyword:
|
|
656
|
-
return T.symbol;
|
|
657
|
-
case ts.SyntaxKind.AnyKeyword:
|
|
658
|
-
case ts.SyntaxKind.UnknownKeyword:
|
|
659
|
-
case ts.SyntaxKind.ObjectKeyword:
|
|
660
|
-
return T.unknown;
|
|
661
|
-
case ts.SyntaxKind.VoidKeyword:
|
|
662
|
-
case ts.SyntaxKind.UndefinedKeyword:
|
|
663
|
-
return T.undefined;
|
|
664
|
-
case ts.SyntaxKind.NeverKeyword:
|
|
665
|
-
return T.never;
|
|
666
|
-
case ts.SyntaxKind.NullKeyword:
|
|
667
|
-
return T.null;
|
|
668
|
-
default:
|
|
669
|
-
// typeof X, keyof, conditional / mapped / indexed / template-literal /
|
|
670
|
-
// import() types and everything else — conservative degradation.
|
|
671
|
-
return T.unknown;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// ---------------------------------------------------------------------------
|
|
676
|
-
// Emission
|
|
677
|
-
// ---------------------------------------------------------------------------
|
|
678
|
-
|
|
679
|
-
function indent(level: number): string {
|
|
680
|
-
return " ".repeat(level);
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
function emitKey(key: string): string {
|
|
684
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
function emitEntries(lines: string[], record: Record<string, TypeValue>, level: number): void {
|
|
688
|
-
for (const [key, value] of Object.entries(record)) {
|
|
689
|
-
lines.push(`${indent(level)}${emitKey(key)}: ${emitType(value, new Set())},`);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
function emitType(tv: TypeValue, expanding: Set<string>): string {
|
|
694
|
-
switch (tv.kind) {
|
|
695
|
-
case "literal":
|
|
696
|
-
if (tv.value === null) return "T.null";
|
|
697
|
-
if (tv.value === undefined) return "T.undefined";
|
|
698
|
-
return `T.literal(${JSON.stringify(tv.value)})`;
|
|
699
|
-
case "primitive":
|
|
700
|
-
return `T.${tv.type}`;
|
|
701
|
-
case "unknown":
|
|
702
|
-
return "T.unknown";
|
|
703
|
-
case "never":
|
|
704
|
-
return "T.never";
|
|
705
|
-
case "array":
|
|
706
|
-
return `T.array(${emitType(tv.element, expanding)})`;
|
|
707
|
-
case "tuple":
|
|
708
|
-
return `T.tuple([${tv.elements.map((e) => emitType(e, expanding)).join(", ")}])`;
|
|
709
|
-
case "promise":
|
|
710
|
-
return `T.promise(${emitType(tv.value, expanding)})`;
|
|
711
|
-
case "object": {
|
|
712
|
-
const entries = Object.entries(tv.properties).map(
|
|
713
|
-
([key, value]) => `${emitKey(key)}: ${emitType(value, expanding)}`,
|
|
714
|
-
);
|
|
715
|
-
return `T.object({ ${entries.join(", ")} })`;
|
|
716
|
-
}
|
|
717
|
-
case "instance": {
|
|
718
|
-
if (expanding.has(tv.className)) {
|
|
719
|
-
// Recursion guard on the emit side: truncate to a bare named instance.
|
|
720
|
-
return `T.instanceOf(${JSON.stringify(tv.className)}, {})`;
|
|
721
|
-
}
|
|
722
|
-
expanding.add(tv.className);
|
|
723
|
-
try {
|
|
724
|
-
const entries = Object.entries(tv.properties).map(
|
|
725
|
-
([key, value]) => `${emitKey(key)}: ${emitType(value, expanding)}`,
|
|
726
|
-
);
|
|
727
|
-
return `T.instanceOf(${JSON.stringify(tv.className)}, { ${entries.join(", ")} })`;
|
|
728
|
-
} finally {
|
|
729
|
-
expanding.delete(tv.className);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
case "union": {
|
|
733
|
-
if (tv.members.length === 0) return "T.never";
|
|
734
|
-
if (tv.members.length === 1) return emitType(tv.members[0]!, expanding);
|
|
735
|
-
return `T.union(${tv.members.map((m) => emitType(m, expanding)).join(", ")})`;
|
|
736
|
-
}
|
|
737
|
-
case "function": {
|
|
738
|
-
const sig = getFnSig(tv);
|
|
739
|
-
if (!sig) return "T.unknown";
|
|
740
|
-
const params = sig.paramTypes.map((p) => emitType(p, expanding)).join(", ");
|
|
741
|
-
const ret = emitType(sig.returnType, expanding);
|
|
742
|
-
if (sig.throwsType && sig.throwsType.kind !== "never") {
|
|
743
|
-
return `T.fnSig([${params}], ${ret}, ${emitType(sig.throwsType, expanding)})`;
|
|
744
|
-
}
|
|
745
|
-
return `T.fnSig([${params}], ${ret})`;
|
|
746
|
-
}
|
|
747
|
-
case "refined":
|
|
748
|
-
return emitType(tv.base, expanding);
|
|
749
|
-
default:
|
|
750
|
-
return "T.unknown";
|
|
751
|
-
}
|
|
752
|
-
}
|