docs: generate routes interfaces automatically on build (#1308)

This commit is contained in:
Vlad Frangu
2025-07-23 00:15:59 +03:00
committed by GitHub
parent b760be279a
commit 5cd2511798
8 changed files with 1158 additions and 23 deletions

2
.gitignore vendored
View File

@@ -66,3 +66,5 @@ docs/*
# djs repo clone
djs
_generated_

View File

@@ -38,3 +38,5 @@ utils/v8.ts
v8.ts
.yarn/*
djs/*
_generated_

View File

@@ -250,6 +250,8 @@ export default config([
'djs/**/*',
'.yarn/*',
'_generated_/**/*',
],
},
commonRuleset,

View File

@@ -96,7 +96,8 @@
"scripts": {
"build:ci": "tsc --noEmit --incremental false",
"build:deno": "node ./scripts/deno.mjs",
"build:node": "tsc && run-p 'esm:*'",
"build:generated": "tsx ./scripts/generate-prettier-routes-interface.ts",
"build:node": "yarn build:generated && tsc && run-p 'esm:*'",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"ci:pr": "run-s changelog lint build:deno && node ./scripts/bump-website-version.mjs",
"clean:deno": "rimraf deno/",
@@ -113,7 +114,7 @@
"lint": "prettier --write . && eslint --format=pretty --fix --ext mjs,ts \"{gateway,payloads,rest,rpc,voice,utils}/**/*.ts\" \"{globals,v*}.ts\" \"scripts/**/*.mjs\"",
"postinstallDev": "is-ci || husky",
"prepack": "run-s clean test:lint build:node",
"postpack": "run-s clean:node build:deno",
"postpack": "run-s clean:node build:deno && git checkout -- './deno/**/*.ts' './rest/**/*.ts'",
"test:lint": "prettier --check . && eslint --format=pretty --ext mjs,ts \"{gateway,payloads,rest,rpc,voice,utils}/**/*.ts\" \"{globals,v*}.ts\" \"scripts/**/*.mjs\"",
"test:types": "tsc -p tests"
},
@@ -126,6 +127,7 @@
"author": "Vlad Frangu <me@vladfrangu.dev>",
"license": "MIT",
"files": [
"_generated_/**/*.{js,js.map,d.ts,d.ts.map,mjs}",
"{gateway,payloads,rest,rpc,voice,utils}/**/*.{js,js.map,d.ts,d.ts.map,mjs}",
"{globals,v*}.{js,js.map,d.ts,d.ts.map,mjs}"
],
@@ -155,8 +157,9 @@
"prettier": "^3.5.3",
"pretty-quick": "^4.1.1",
"rimraf": "^6.0.1",
"ts-morph": "^26.0.0",
"tsutils": "^3.21.0",
"tsx": "^4.19.4",
"tsx": "^4.20.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.33.0"
},

View File

@@ -0,0 +1,232 @@
import { readdirSync } from 'node:fs';
import { join } from 'node:path';
import type {
InterfaceDeclaration,
ObjectLiteralExpression,
ParameterDeclaration,
ParameterDeclarationStructure,
TypeParameterDeclaration,
TypeParameterDeclarationStructure,
} from 'ts-morph';
import { Project, SyntaxKind } from 'ts-morph';
const RoutesInterfaceName = 'RoutesDeclarations';
const CDNRoutesInterfaceName = 'CDNRoutesDeclarations';
const isInWebsite = __dirname.includes('website');
const extraPath = isInWebsite ? '../' : '';
const versions = readdirSync(join(__dirname, extraPath, '../rest')).filter((dir) => /^v\d+$/.exec(dir));
const generatedRestDirectory = join(__dirname, extraPath, '../_generated_/rest');
const globalsFilePath = join(__dirname, extraPath, '../globals.ts');
function parameterDeclarationToStructure(parameter: ParameterDeclaration): ParameterDeclarationStructure {
const obj = parameter.getStructure();
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
obj.hasQuestionToken = obj.hasQuestionToken || Boolean(obj.initializer);
delete obj.initializer;
return obj;
}
function typeParameterToStructure(typeParameter: TypeParameterDeclaration): TypeParameterDeclarationStructure {
// eslint-disable-next-line sonarjs/prefer-immediate-return
const obj = typeParameter.getStructure();
return obj;
}
function handleTypeParameter(typeParameter: TypeParameterDeclaration, allSeen: Set<string>) {
const extendsClause = typeParameter.getConstraint();
if (!extendsClause) {
return;
}
const type = extendsClause.getText();
if (allSeen.has(type)) {
return;
}
allSeen.add(type);
if (type === 'StorePageAssetFormat') {
allSeen.add('ImageFormat');
}
}
function handleObject(object: ObjectLiteralExpression, interfaceToAddTo: InterfaceDeclaration) {
const seenTypeParameters = new Set<string>();
for (const property of object.getPropertiesWithComments()) {
const castedMethod = property.asKindOrThrow(SyntaxKind.MethodDeclaration);
const methodName = castedMethod.getName();
const methodParameters = castedMethod.getParameters();
let methodReturnType = castedMethod.getReturnType().getText();
const methodDocs = castedMethod.getJsDocs();
const returnBody = castedMethod
.getChildren()
?.at(-1)
?.getChildren()
?.at(1)
?.getChildren()
?.at(-1)
?.asKindOrThrow(SyntaxKind.ReturnStatement);
const asExpression = returnBody?.getChildrenOfKind(SyntaxKind.AsExpression)[0];
const unionType = asExpression?.getChildrenOfKind(SyntaxKind.UnionType)?.[0];
// Override with union if it exists in the cast
if (unionType) {
methodReturnType = unionType
.getText()
.split('\n')
.map((line) => line.trim())
.join(' ');
}
if (methodReturnType.startsWith('| ')) {
methodReturnType = methodReturnType.slice(2);
}
const typeParameters = castedMethod.getTypeParameters();
for (const typeParameter of typeParameters) {
handleTypeParameter(typeParameter, seenTypeParameters);
}
interfaceToAddTo.addMethod({
name: methodName,
parameters: methodParameters.map(parameterDeclarationToStructure),
typeParameters: typeParameters.map(typeParameterToStructure),
returnType: methodReturnType,
leadingTrivia:
methodDocs
.map((doc) => doc.getText())
.join('\n')
.replaceAll('\t', '') + '\n',
});
for (const overload of castedMethod.getOverloads()) {
const typeParameters = overload.getTypeParameters();
for (const typeParameter of typeParameters) {
handleTypeParameter(typeParameter, seenTypeParameters);
}
interfaceToAddTo.addMethod({
name: overload.getName(),
parameters: overload.getParameters().map(parameterDeclarationToStructure),
typeParameters: typeParameters.map(typeParameterToStructure),
returnType: overload.getReturnType().getText(),
leadingTrivia:
overload
.getJsDocs()
.map((doc) => doc.getText())
.join('\n')
.replaceAll('\t', '') + '\n',
});
}
}
return seenTypeParameters;
}
for (const version of versions) {
console.log(`Generating interfaces for ${version}...`);
const inputFilePath = join(__dirname, extraPath, `../rest/${version}/index.ts`);
const generatedRestInterfacesFilePath = join(__dirname, extraPath, `../_generated_/rest/${version}/interfaces.ts`);
const project = new Project({});
project.addSourceFileAtPathIfExists(inputFilePath);
project.addSourceFileAtPathIfExists(globalsFilePath);
project.createDirectory(generatedRestDirectory);
const generatedRestInterfacesFile = project.createSourceFile(generatedRestInterfacesFilePath, undefined, {
overwrite: true,
});
const routesDeclarationInterface = generatedRestInterfacesFile.addInterface({
name: RoutesInterfaceName,
leadingTrivia: '// Automatically generated interface from the Routes object.\n',
isExported: true,
});
const cdnRoutesDeclarationInterface = generatedRestInterfacesFile.addInterface({
name: CDNRoutesInterfaceName,
leadingTrivia: '// Automatically generated interface from the CDN Routes object.\n',
isExported: true,
});
generatedRestInterfacesFile.addImportDeclaration({
moduleSpecifier: '../../../globals',
namedImports: ['Snowflake'],
isTypeOnly: true,
});
const routesObjectFile = project.getSourceFileOrThrow(inputFilePath);
routesObjectFile.addImportDeclaration({
moduleSpecifier: `../../_generated_/rest/${version}/interfaces`,
isTypeOnly: true,
namedImports: [RoutesInterfaceName, CDNRoutesInterfaceName],
});
routesObjectFile.addExportDeclaration({
isTypeOnly: true,
moduleSpecifier: `../../_generated_/rest/${version}/interfaces`,
leadingTrivia: '// Exports all generated interfaces from the REST API.\n',
});
const routesObject = routesObjectFile.getVariableDeclarationOrThrow('Routes');
const cdnRoutesObject = routesObjectFile.getVariableDeclaration('CDNRoutes');
if (!cdnRoutesObject) {
console.log('Skipping type generation for', version);
continue;
}
const routesObjectChildren = routesObject.getChildren();
const cdnRoutesObjectChildren = cdnRoutesObject.getChildren();
const [routesIdentifier] = routesObjectChildren;
const routesObjectDeclaration = routesObject.getInitializerOrThrow();
const [cdnRoutesIdentifier] = cdnRoutesObjectChildren;
const cdnRoutesObjectDeclaration = cdnRoutesObject.getInitializerOrThrow();
const importsNeededForRoutes = handleObject(
routesObjectDeclaration.asKindOrThrow(SyntaxKind.ObjectLiteralExpression),
routesDeclarationInterface,
);
const importsNeededForCDNRoutes = handleObject(
cdnRoutesObjectDeclaration.asKindOrThrow(SyntaxKind.ObjectLiteralExpression),
cdnRoutesDeclarationInterface,
);
const typesToImportFromOriginalFile = new Set<string>([...importsNeededForRoutes, ...importsNeededForCDNRoutes]);
if (typesToImportFromOriginalFile.size > 0) {
generatedRestInterfacesFile.addImportDeclaration({
moduleSpecifier: `../../../rest/${version}/index`,
isTypeOnly: true,
namedImports: [...typesToImportFromOriginalFile].sort((a, b) => a.localeCompare(b)),
});
}
routesIdentifier.replaceWithText(`Routes: ${RoutesInterfaceName}`);
cdnRoutesIdentifier.replaceWithText(`CDNRoutes: ${CDNRoutesInterfaceName}`);
project.saveSync();
}

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@
"scripts": {
"docusaurus": "docusaurus",
"start": "npm run clean && docusaurus start",
"build": "npm run clean && cross-env NODE_OPTIONS=\"--max_old_space_size=7500\" docusaurus build",
"build": "npm run clean && cp ../scripts/generate-prettier-routes-interface.ts ./scripts/generate-prettier-routes-interface.ts && npx tsx ./scripts/generate-prettier-routes-interface.ts && cross-env NODE_OPTIONS=\"--max_old_space_size=7500\" docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
@@ -47,7 +47,8 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"sass": "^1.81.0",
"swr": "^2.2.5"
"swr": "^2.2.5",
"ts-morph": "^26.0.0"
},
"devDependencies": {
"@apify/docusaurus-plugin-typedoc-api": "^4.3.1",
@@ -66,6 +67,7 @@
"patch-package": "^8.0.0",
"prettier": "^3.4.1",
"pretty-quick": "^4.0.0",
"tsx": "^4.20.3",
"typedoc": "^0.27.1",
"typedoc-plugin-djs-links": "^2.2.1",
"typedoc-plugin-markdown": "^4.3.0",

View File

@@ -2535,6 +2535,17 @@ __metadata:
languageName: node
linkType: hard
"@ts-morph/common@npm:~0.27.0":
version: 0.27.0
resolution: "@ts-morph/common@npm:0.27.0"
dependencies:
fast-glob: "npm:^3.3.3"
minimatch: "npm:^10.0.1"
path-browserify: "npm:^1.0.1"
checksum: 10c0/3daa267bd78114ff504eb064c5215da6e46589e775b781ec0da4998d999b0d7130eff287e70d6e13e0a0a897ea16e9387f4cd885b4b9d6d628f318cecb81d473
languageName: node
linkType: hard
"@tybys/wasm-util@npm:^0.10.0":
version: 0.10.0
resolution: "@tybys/wasm-util@npm:0.10.0"
@@ -3772,6 +3783,13 @@ __metadata:
languageName: node
linkType: hard
"code-block-writer@npm:^13.0.3":
version: 13.0.3
resolution: "code-block-writer@npm:13.0.3"
checksum: 10c0/87db97b37583f71cfd7eced8bf3f0a0a0ca53af912751a734372b36c08cd27f3e8a4878ec05591c0cd9ae11bea8add1423e132d660edd86aab952656dd41fd66
languageName: node
linkType: hard
"color-convert@npm:^2.0.1":
version: 2.0.1
resolution: "color-convert@npm:2.0.1"
@@ -4196,8 +4214,9 @@ __metadata:
prettier: "npm:^3.5.3"
pretty-quick: "npm:^4.1.1"
rimraf: "npm:^6.0.1"
ts-morph: "npm:^26.0.0"
tsutils: "npm:^3.21.0"
tsx: "npm:^4.19.4"
tsx: "npm:^4.20.3"
typescript: "npm:^5.8.3"
typescript-eslint: "npm:^8.33.0"
languageName: unknown
@@ -7463,7 +7482,7 @@ __metadata:
languageName: node
linkType: hard
"minimatch@npm:^10.0.3, minimatch@npm:^9.0.3 || ^10.0.1":
"minimatch@npm:^10.0.1, minimatch@npm:^10.0.3, minimatch@npm:^9.0.3 || ^10.0.1":
version: 10.0.3
resolution: "minimatch@npm:10.0.3"
dependencies:
@@ -8103,6 +8122,13 @@ __metadata:
languageName: node
linkType: hard
"path-browserify@npm:^1.0.1":
version: 1.0.1
resolution: "path-browserify@npm:1.0.1"
checksum: 10c0/8b8c3fd5c66bd340272180590ae4ff139769e9ab79522e2eb82e3d571a89b8117c04147f65ad066dccfb42fcad902e5b7d794b3d35e0fd840491a8ddbedf8c66
languageName: node
linkType: hard
"path-exists@npm:^4.0.0":
version: 4.0.0
resolution: "path-exists@npm:4.0.0"
@@ -9373,6 +9399,16 @@ __metadata:
languageName: node
linkType: hard
"ts-morph@npm:^26.0.0":
version: 26.0.0
resolution: "ts-morph@npm:26.0.0"
dependencies:
"@ts-morph/common": "npm:~0.27.0"
code-block-writer: "npm:^13.0.3"
checksum: 10c0/c6880d90a1eefe0ce6555bf8c11cc104b1f36f84bd36a37a82b9ae0b974f51fe6b1bc91bb0ec42550158dc1c812329d6433e1237cba64f1ef515c129b321dd5d
languageName: node
linkType: hard
"tslib@npm:^1.8.1":
version: 1.14.1
resolution: "tslib@npm:1.14.1"
@@ -9414,7 +9450,7 @@ __metadata:
languageName: node
linkType: hard
"tsx@npm:^4.19.4":
"tsx@npm:^4.20.3":
version: 4.20.3
resolution: "tsx@npm:4.20.3"
dependencies: