Skip to content

Commit 5cd2511

Browse files
authored
docs: generate routes interfaces automatically on build (#1308)
1 parent b760be2 commit 5cd2511

File tree

8 files changed

+1158
-23
lines changed

8 files changed

+1158
-23
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,5 @@ docs/*
6666

6767
# djs repo clone
6868
djs
69+
70+
_generated_

.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,5 @@ utils/v8.ts
3838
v8.ts
3939

4040
.yarn/*
41+
djs/*
42+
_generated_

eslint.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ export default config([
250250

251251
'djs/**/*',
252252
'.yarn/*',
253+
254+
'_generated_/**/*',
253255
],
254256
},
255257
commonRuleset,

package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@
9696
"scripts": {
9797
"build:ci": "tsc --noEmit --incremental false",
9898
"build:deno": "node ./scripts/deno.mjs",
99-
"build:node": "tsc && run-p 'esm:*'",
99+
"build:generated": "tsx ./scripts/generate-prettier-routes-interface.ts",
100+
"build:node": "yarn build:generated && tsc && run-p 'esm:*'",
100101
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
101102
"ci:pr": "run-s changelog lint build:deno && node ./scripts/bump-website-version.mjs",
102103
"clean:deno": "rimraf deno/",
@@ -113,7 +114,7 @@
113114
"lint": "prettier --write . && eslint --format=pretty --fix --ext mjs,ts \"{gateway,payloads,rest,rpc,voice,utils}/**/*.ts\" \"{globals,v*}.ts\" \"scripts/**/*.mjs\"",
114115
"postinstallDev": "is-ci || husky",
115116
"prepack": "run-s clean test:lint build:node",
116-
"postpack": "run-s clean:node build:deno",
117+
"postpack": "run-s clean:node build:deno && git checkout -- './deno/**/*.ts' './rest/**/*.ts'",
117118
"test:lint": "prettier --check . && eslint --format=pretty --ext mjs,ts \"{gateway,payloads,rest,rpc,voice,utils}/**/*.ts\" \"{globals,v*}.ts\" \"scripts/**/*.mjs\"",
118119
"test:types": "tsc -p tests"
119120
},
@@ -126,6 +127,7 @@
126127
"author": "Vlad Frangu <[email protected]>",
127128
"license": "MIT",
128129
"files": [
130+
"_generated_/**/*.{js,js.map,d.ts,d.ts.map,mjs}",
129131
"{gateway,payloads,rest,rpc,voice,utils}/**/*.{js,js.map,d.ts,d.ts.map,mjs}",
130132
"{globals,v*}.{js,js.map,d.ts,d.ts.map,mjs}"
131133
],
@@ -155,8 +157,9 @@
155157
"prettier": "^3.5.3",
156158
"pretty-quick": "^4.1.1",
157159
"rimraf": "^6.0.1",
160+
"ts-morph": "^26.0.0",
158161
"tsutils": "^3.21.0",
159-
"tsx": "^4.19.4",
162+
"tsx": "^4.20.3",
160163
"typescript": "^5.8.3",
161164
"typescript-eslint": "^8.33.0"
162165
},
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import { readdirSync } from 'node:fs';
2+
import { join } from 'node:path';
3+
import type {
4+
InterfaceDeclaration,
5+
ObjectLiteralExpression,
6+
ParameterDeclaration,
7+
ParameterDeclarationStructure,
8+
TypeParameterDeclaration,
9+
TypeParameterDeclarationStructure,
10+
} from 'ts-morph';
11+
import { Project, SyntaxKind } from 'ts-morph';
12+
13+
const RoutesInterfaceName = 'RoutesDeclarations';
14+
const CDNRoutesInterfaceName = 'CDNRoutesDeclarations';
15+
16+
const isInWebsite = __dirname.includes('website');
17+
18+
const extraPath = isInWebsite ? '../' : '';
19+
20+
const versions = readdirSync(join(__dirname, extraPath, '../rest')).filter((dir) => /^v\d+$/.exec(dir));
21+
22+
const generatedRestDirectory = join(__dirname, extraPath, '../_generated_/rest');
23+
const globalsFilePath = join(__dirname, extraPath, '../globals.ts');
24+
25+
function parameterDeclarationToStructure(parameter: ParameterDeclaration): ParameterDeclarationStructure {
26+
const obj = parameter.getStructure();
27+
28+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
29+
obj.hasQuestionToken = obj.hasQuestionToken || Boolean(obj.initializer);
30+
31+
delete obj.initializer;
32+
33+
return obj;
34+
}
35+
36+
function typeParameterToStructure(typeParameter: TypeParameterDeclaration): TypeParameterDeclarationStructure {
37+
// eslint-disable-next-line sonarjs/prefer-immediate-return
38+
const obj = typeParameter.getStructure();
39+
40+
return obj;
41+
}
42+
43+
function handleTypeParameter(typeParameter: TypeParameterDeclaration, allSeen: Set<string>) {
44+
const extendsClause = typeParameter.getConstraint();
45+
46+
if (!extendsClause) {
47+
return;
48+
}
49+
50+
const type = extendsClause.getText();
51+
52+
if (allSeen.has(type)) {
53+
return;
54+
}
55+
56+
allSeen.add(type);
57+
58+
if (type === 'StorePageAssetFormat') {
59+
allSeen.add('ImageFormat');
60+
}
61+
}
62+
63+
function handleObject(object: ObjectLiteralExpression, interfaceToAddTo: InterfaceDeclaration) {
64+
const seenTypeParameters = new Set<string>();
65+
66+
for (const property of object.getPropertiesWithComments()) {
67+
const castedMethod = property.asKindOrThrow(SyntaxKind.MethodDeclaration);
68+
69+
const methodName = castedMethod.getName();
70+
const methodParameters = castedMethod.getParameters();
71+
let methodReturnType = castedMethod.getReturnType().getText();
72+
const methodDocs = castedMethod.getJsDocs();
73+
74+
const returnBody = castedMethod
75+
.getChildren()
76+
?.at(-1)
77+
?.getChildren()
78+
?.at(1)
79+
?.getChildren()
80+
?.at(-1)
81+
?.asKindOrThrow(SyntaxKind.ReturnStatement);
82+
83+
const asExpression = returnBody?.getChildrenOfKind(SyntaxKind.AsExpression)[0];
84+
85+
const unionType = asExpression?.getChildrenOfKind(SyntaxKind.UnionType)?.[0];
86+
87+
// Override with union if it exists in the cast
88+
if (unionType) {
89+
methodReturnType = unionType
90+
.getText()
91+
.split('\n')
92+
.map((line) => line.trim())
93+
.join(' ');
94+
}
95+
96+
if (methodReturnType.startsWith('| ')) {
97+
methodReturnType = methodReturnType.slice(2);
98+
}
99+
100+
const typeParameters = castedMethod.getTypeParameters();
101+
102+
for (const typeParameter of typeParameters) {
103+
handleTypeParameter(typeParameter, seenTypeParameters);
104+
}
105+
106+
interfaceToAddTo.addMethod({
107+
name: methodName,
108+
parameters: methodParameters.map(parameterDeclarationToStructure),
109+
typeParameters: typeParameters.map(typeParameterToStructure),
110+
returnType: methodReturnType,
111+
leadingTrivia:
112+
methodDocs
113+
.map((doc) => doc.getText())
114+
.join('\n')
115+
.replaceAll('\t', '') + '\n',
116+
});
117+
118+
for (const overload of castedMethod.getOverloads()) {
119+
const typeParameters = overload.getTypeParameters();
120+
121+
for (const typeParameter of typeParameters) {
122+
handleTypeParameter(typeParameter, seenTypeParameters);
123+
}
124+
125+
interfaceToAddTo.addMethod({
126+
name: overload.getName(),
127+
parameters: overload.getParameters().map(parameterDeclarationToStructure),
128+
typeParameters: typeParameters.map(typeParameterToStructure),
129+
returnType: overload.getReturnType().getText(),
130+
leadingTrivia:
131+
overload
132+
.getJsDocs()
133+
.map((doc) => doc.getText())
134+
.join('\n')
135+
.replaceAll('\t', '') + '\n',
136+
});
137+
}
138+
}
139+
140+
return seenTypeParameters;
141+
}
142+
143+
for (const version of versions) {
144+
console.log(`Generating interfaces for ${version}...`);
145+
146+
const inputFilePath = join(__dirname, extraPath, `../rest/${version}/index.ts`);
147+
const generatedRestInterfacesFilePath = join(__dirname, extraPath, `../_generated_/rest/${version}/interfaces.ts`);
148+
149+
const project = new Project({});
150+
151+
project.addSourceFileAtPathIfExists(inputFilePath);
152+
project.addSourceFileAtPathIfExists(globalsFilePath);
153+
project.createDirectory(generatedRestDirectory);
154+
155+
const generatedRestInterfacesFile = project.createSourceFile(generatedRestInterfacesFilePath, undefined, {
156+
overwrite: true,
157+
});
158+
159+
const routesDeclarationInterface = generatedRestInterfacesFile.addInterface({
160+
name: RoutesInterfaceName,
161+
leadingTrivia: '// Automatically generated interface from the Routes object.\n',
162+
isExported: true,
163+
});
164+
165+
const cdnRoutesDeclarationInterface = generatedRestInterfacesFile.addInterface({
166+
name: CDNRoutesInterfaceName,
167+
leadingTrivia: '// Automatically generated interface from the CDN Routes object.\n',
168+
isExported: true,
169+
});
170+
171+
generatedRestInterfacesFile.addImportDeclaration({
172+
moduleSpecifier: '../../../globals',
173+
namedImports: ['Snowflake'],
174+
isTypeOnly: true,
175+
});
176+
177+
const routesObjectFile = project.getSourceFileOrThrow(inputFilePath);
178+
179+
routesObjectFile.addImportDeclaration({
180+
moduleSpecifier: `../../_generated_/rest/${version}/interfaces`,
181+
isTypeOnly: true,
182+
namedImports: [RoutesInterfaceName, CDNRoutesInterfaceName],
183+
});
184+
185+
routesObjectFile.addExportDeclaration({
186+
isTypeOnly: true,
187+
moduleSpecifier: `../../_generated_/rest/${version}/interfaces`,
188+
leadingTrivia: '// Exports all generated interfaces from the REST API.\n',
189+
});
190+
191+
const routesObject = routesObjectFile.getVariableDeclarationOrThrow('Routes');
192+
const cdnRoutesObject = routesObjectFile.getVariableDeclaration('CDNRoutes');
193+
194+
if (!cdnRoutesObject) {
195+
console.log('Skipping type generation for', version);
196+
continue;
197+
}
198+
199+
const routesObjectChildren = routesObject.getChildren();
200+
const cdnRoutesObjectChildren = cdnRoutesObject.getChildren();
201+
202+
const [routesIdentifier] = routesObjectChildren;
203+
204+
const routesObjectDeclaration = routesObject.getInitializerOrThrow();
205+
const [cdnRoutesIdentifier] = cdnRoutesObjectChildren;
206+
const cdnRoutesObjectDeclaration = cdnRoutesObject.getInitializerOrThrow();
207+
208+
const importsNeededForRoutes = handleObject(
209+
routesObjectDeclaration.asKindOrThrow(SyntaxKind.ObjectLiteralExpression),
210+
routesDeclarationInterface,
211+
);
212+
213+
const importsNeededForCDNRoutes = handleObject(
214+
cdnRoutesObjectDeclaration.asKindOrThrow(SyntaxKind.ObjectLiteralExpression),
215+
cdnRoutesDeclarationInterface,
216+
);
217+
218+
const typesToImportFromOriginalFile = new Set<string>([...importsNeededForRoutes, ...importsNeededForCDNRoutes]);
219+
220+
if (typesToImportFromOriginalFile.size > 0) {
221+
generatedRestInterfacesFile.addImportDeclaration({
222+
moduleSpecifier: `../../../rest/${version}/index`,
223+
isTypeOnly: true,
224+
namedImports: [...typesToImportFromOriginalFile].sort((a, b) => a.localeCompare(b)),
225+
});
226+
}
227+
228+
routesIdentifier.replaceWithText(`Routes: ${RoutesInterfaceName}`);
229+
cdnRoutesIdentifier.replaceWithText(`CDNRoutes: ${CDNRoutesInterfaceName}`);
230+
231+
project.saveSync();
232+
}

0 commit comments

Comments
 (0)