Skip to content

Commit 4a77374

Browse files
committed
refactor: type generators
1 parent 3467a7f commit 4a77374

14 files changed

+569
-409
lines changed

.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
node_modules/
22
dist/
3+
build/api/out.d.ts
34

45
!.eslintrc.js
56
!.eslintplugin.js

build/api/enums.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as tsm from "ts-morph";
2+
import * as ts from "typescript";
3+
import { fn } from "./util";
4+
5+
export const buildEnums = () => {
6+
const project = new tsm.Project({
7+
tsConfigFilePath: fn("./tsconfig.json"),
8+
compilerOptions: {
9+
outDir: fn("./packages"),
10+
outFile: fn("./packages/enums.d.ts"),
11+
isolatedModules: false,
12+
module: tsm.ModuleKind.CommonJS,
13+
target: ts.ScriptTarget.ESNext,
14+
allowJs: true,
15+
emitDeclarationOnly: true,
16+
declaration: true,
17+
},
18+
skipAddingFilesFromTsConfig: true,
19+
});
20+
project.addSourceFileAtPath(fn("./src/common/enums/index.ts"));
21+
project.emit({ emitOnlyDtsFiles: true });
22+
};
23+
24+
buildEnums();

build/api/extract-types.ts

+284
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
import * as tsm from "ts-morph";
2+
import {
3+
emitFileDeclaration,
4+
fn,
5+
getIdentifierDefinitions,
6+
TSMProcessedNodeCache,
7+
TSMValidNodes,
8+
} from "./util";
9+
import { readFileSync } from "fs";
10+
import { basename } from "path";
11+
12+
// reads files and concatenates them into a single string
13+
const concat = (files: string[]) => {
14+
return files.map(fn).map((file) => ({
15+
filename: basename(file),
16+
content: readFileSync(file, "utf8"),
17+
}));
18+
};
19+
20+
export type Diagnostic = {
21+
nodeModules: string[];
22+
external: string[];
23+
notFound: string[];
24+
nodes: string[];
25+
files: any[];
26+
tree: FileTree[];
27+
};
28+
29+
type FileTree = {
30+
parent: string;
31+
child: string;
32+
childType: string;
33+
node?: string;
34+
};
35+
36+
type InputFile = {
37+
file: string;
38+
// types?: string[];
39+
ignoreReferences?: string[];
40+
};
41+
42+
export const extractTypesFromFiles = async ({
43+
files,
44+
globalIgnore,
45+
prependFiles,
46+
}: {
47+
files: InputFile[];
48+
globalIgnore: string[];
49+
prependFiles: string[];
50+
}) => {
51+
const inProject = new tsm.Project({
52+
tsConfigFilePath: fn("./tsconfig.json"),
53+
compilerOptions: {
54+
declaration: true,
55+
emitDeclarationOnly: true,
56+
declarationDir: "./outdir",
57+
allowJs: false,
58+
},
59+
});
60+
61+
const fileSystem = new tsm.InMemoryFileSystemHost();
62+
63+
const outProject = new tsm.Project({
64+
skipAddingFilesFromTsConfig: true,
65+
compilerOptions: {
66+
...inProject.getCompilerOptions(),
67+
declaration: true,
68+
emitDeclarationOnly: true,
69+
declarationDir: "/outdir",
70+
allowJs: false,
71+
},
72+
fileSystem,
73+
});
74+
75+
const processed = new TSMProcessedNodeCache();
76+
77+
let result = {
78+
content: "",
79+
diagnostics: {
80+
nodeModules: [],
81+
external: [],
82+
notFound: [],
83+
nodes: [],
84+
tree: [],
85+
} as Diagnostic,
86+
};
87+
const writeContent = (str) => (result.content += str + "\n");
88+
89+
prependFiles.map(fn).forEach((file) => {
90+
const sourceFile = inProject.getSourceFiles(file)[0];
91+
const defFile = emitFileDeclaration(inProject, sourceFile, outProject);
92+
writeContent(defFile.declFile.getFullText());
93+
});
94+
95+
const workNodes: TSMValidNodes[] = [];
96+
97+
const declMap = new Map<
98+
tsm.SourceFile,
99+
{
100+
declFile: tsm.SourceFile;
101+
mapFile: tsm.SourceFile;
102+
}
103+
>();
104+
105+
const addWork = (child: TSMValidNodes) => {
106+
workNodes.push(child);
107+
};
108+
109+
for (const file of files) {
110+
const sourceFile = inProject.getSourceFiles(file.file)[0];
111+
for (const childNode of [
112+
...sourceFile.getTypeAliases(),
113+
...sourceFile.getInterfaces(),
114+
...sourceFile.getClasses(),
115+
]) {
116+
addWork(childNode);
117+
}
118+
}
119+
120+
// gather identifiers
121+
// gather identiifer definitions
122+
123+
for (const workNode of workNodes) {
124+
if (processed.containsNode(workNode)) {
125+
continue;
126+
}
127+
128+
const workItemSourceFile = workNode.getSourceFile();
129+
130+
if (workItemSourceFile.isFromExternalLibrary()) {
131+
if (
132+
!result.diagnostics.external.includes(workItemSourceFile.getFilePath())
133+
) {
134+
result.diagnostics.external.push(workItemSourceFile.getFilePath());
135+
}
136+
continue;
137+
}
138+
if (workItemSourceFile.isInNodeModules()) {
139+
if (
140+
!result.diagnostics.nodeModules.includes(
141+
workItemSourceFile.getFilePath()
142+
)
143+
) {
144+
result.diagnostics.nodeModules.push(workItemSourceFile.getFilePath());
145+
}
146+
continue;
147+
}
148+
if (!declMap.has(workItemSourceFile)) {
149+
declMap.set(
150+
workItemSourceFile,
151+
emitFileDeclaration(inProject, workItemSourceFile, outProject)
152+
);
153+
}
154+
155+
let declNode: TSMValidNodes;
156+
for (const _declNode of declMap
157+
.get(workItemSourceFile)
158+
.declFile.getChildrenOfKind(workNode.getKind())) {
159+
//TODO: map this by pos not by id if we want to support other types of nodes
160+
if ((_declNode as TSMValidNodes).getName() === workNode.getName()) {
161+
if (declNode) {
162+
throw new Error("Duplicate node found");
163+
}
164+
declNode = _declNode as TSMValidNodes;
165+
}
166+
}
167+
if (!declNode) {
168+
throw new Error("Node not found");
169+
}
170+
processed.addNode(workNode, declNode);
171+
172+
const p = workNode.getParent();
173+
//@ts-ignore
174+
const pn = p?.getName ? p.getName() : "<>";
175+
176+
result.diagnostics.tree.push({
177+
// parent: workNode.getParent() ? workNode.getParent().getName() : "<root>",
178+
parent: pn,
179+
child: workNode.getSourceFile().getFilePath(),
180+
childType: workNode.getKindName(),
181+
node: workNode.getName(),
182+
});
183+
184+
// go down the tree and find all referenced types so we can add them to our d.ts file
185+
const references = workNode
186+
.getDescendantsOfKind(tsm.SyntaxKind.TypeReference)
187+
.map((node) => {
188+
const query = node.getFirstChildByKind(tsm.SyntaxKind.TypeQuery);
189+
const definitions = getIdentifierDefinitions(node);
190+
return {
191+
node,
192+
query,
193+
definitions,
194+
};
195+
});
196+
197+
for (const node of references) {
198+
for (const d of node.definitions) {
199+
if (
200+
d.getKind() === tsm.SyntaxKind.TypeAliasDeclaration ||
201+
d.getKind() === tsm.SyntaxKind.InterfaceDeclaration ||
202+
d.getKind() === tsm.SyntaxKind.ClassDeclaration
203+
) {
204+
addWork(d as TSMValidNodes);
205+
}
206+
}
207+
}
208+
209+
if (workNode.getKind() === tsm.SyntaxKind.TypeAliasDeclaration) {
210+
// writeContent(
211+
// `export type ${root.getName()} = ${printNode(
212+
// inProject.getTypeChecker(),
213+
// root.getType()
214+
// )}`
215+
// );
216+
} else if (workNode.getKind() === tsm.SyntaxKind.InterfaceDeclaration) {
217+
workNode
218+
.asKind(tsm.SyntaxKind.InterfaceDeclaration)
219+
.getBaseDeclarations()
220+
.forEach(addWork);
221+
} else if (workNode.getKind() === tsm.SyntaxKind.ClassDeclaration) {
222+
const base = workNode
223+
.asKind(tsm.SyntaxKind.ClassDeclaration)
224+
.getBaseClass();
225+
if (base) {
226+
addWork(base);
227+
}
228+
} else {
229+
throw new Error(`Unknown kind ${workNode.getKindName()}`);
230+
}
231+
}
232+
233+
let mashedOut = "";
234+
235+
for (const decl of declMap) {
236+
decl[1].declFile.forEachChild((node) => {
237+
if (tsm.Node.isImportDeclaration(node)) {
238+
return;
239+
}
240+
mashedOut += node.getFullText();
241+
});
242+
}
243+
for (const node of processed.getAllNodes()) {
244+
const id = node.getFirstChildByKind(tsm.SyntaxKind.Identifier);
245+
if (!id) {
246+
debugger;
247+
}
248+
// id.getDefinitionNodes().forEach((d) => {
249+
// d.print()
250+
251+
// mashedOut = mashedOut + "\n" + id.getType().getText();
252+
// const defs = inProject
253+
// .getLanguageService()
254+
// .compilerObject.getDefinitionAtPosition(
255+
// node.compilerNode.getSourceFile().fileName,
256+
// id.compilerNode.pos
257+
// );
258+
// console.log(
259+
// node.compilerNode.getSourceFile().fileName,
260+
// id.compilerNode.pos,
261+
// id.compilerNode.text,
262+
// defs
263+
// );
264+
// for (const def of) {
265+
// mashedOut = mashedOut + "\n" + def.textSpan;
266+
// }
267+
// mashedOut = mashedOut + "\n" + processed.getDefNode(node).print();
268+
}
269+
270+
result.content = mashedOut;
271+
272+
result.diagnostics.files = outProject.getSourceFiles().map((file) => {
273+
return {
274+
filePath: file.getFilePath(),
275+
aliases: file.getTypeAliases().map((t) => t.getName()),
276+
interfaces: file.getInterfaces().map((t) => t.getName()),
277+
classes: file.getClasses().map((t) => t.getName()),
278+
variables: file.getVariableDeclarations().map((t) => t.getName()),
279+
};
280+
});
281+
result.diagnostics.nodes = processed.getAllNodes().map((node) => node.getName());
282+
283+
return result;
284+
};

build/api/host.ts

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { writeFile } from "fs/promises";
2+
import { fn } from "./util";
3+
import { extractTypesFromFiles } from "./extract-types";
4+
5+
(async () => {
6+
writeFile(
7+
fn("./src/common/types/declarations/titan-reactor-host.d.ts"),
8+
`declare module "titan-reactor/host" {\n` +
9+
(await extractTypesFromFiles({
10+
files: [
11+
{
12+
file: "**/types/plugin.ts",
13+
types: ["PluginMetaData", "SceneInputHandler"],
14+
},
15+
{
16+
file: "**/bwdat/bw-dat.ts",
17+
},
18+
{
19+
file: "**/minimap-dimensions.ts",
20+
},
21+
{
22+
file: "**/parse-replay-header.ts",
23+
types: ["ReplayPlayer"],
24+
},
25+
{
26+
file: "**/plugin-system-ui.ts",
27+
types: ["PluginStateMessage", "ReplayPlayer"],
28+
},
29+
{
30+
file: "**/plugin-system-native.ts",
31+
types: ["PluginProto"],
32+
},
33+
{
34+
file: "**/scenes/scene.ts",
35+
types: ["SceneStateID"],
36+
},
37+
{
38+
file: "**/assets.ts",
39+
types: ["UIStateAssets"],
40+
ignoreReferences: ["Assets", "Pick"],
41+
},
42+
{
43+
file: "**/icons.ts",
44+
},
45+
{
46+
file: "**/core/unit.ts",
47+
},
48+
],
49+
globalIgnore: ["SpritesBufferView", "Vector2"],
50+
extraTypes: [
51+
"type Vector2 = {x:number,y:number}",
52+
// ,
53+
// `
54+
// export class PluginBase implements NativePlugin {
55+
// id: string;
56+
// name: string;
57+
// $$permissions: NativePlugin["$$permissions"];
58+
// $$config: NativePlugin["$$config"];
59+
// $$meta: NativePlugin["$$meta"];
60+
// sendUIMessage: NativePlugin["sendUIMessage"];
61+
// setConfig(key: string, value: any, persist?: boolean): void
62+
// }
63+
// `
64+
],
65+
prependFiles: [fn("./src/renderer/plugins/events.ts")],
66+
})) +
67+
"\n}",
68+
{ encoding: "utf8" }
69+
);
70+
})();

0 commit comments

Comments
 (0)