Skip to content

Commit 0a3ba6e

Browse files
committed
feat(cli): add code-mod to convert api clients to experimental code-gen
Does not convert all breaking changes, but fixes the worst one to do manually: updating type names.
1 parent 82b4dd7 commit 0a3ba6e

File tree

4 files changed

+256
-1
lines changed

4 files changed

+256
-1
lines changed

packages/cli/src/code-mod/constants.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { cpus } from "os";
2+
import { executeApiClientToExperimentalCodeGen } from "./mods/api-client-to-experimental-code-gen.js";
23
import { executeLintConfigToEslintPlugin } from "./mods/lint-config-to-eslint-plugin.js";
34
import { executeUpdateQueriesSignatureChange } from "./mods/update-queries-signature-change.js";
45

@@ -48,10 +49,34 @@ export const codeModMap = {
4849
exec: executeUpdateQueriesSignatureChange,
4950
},
5051
"lint-config-to-eslint-plugin": {
51-
description: `Converts all known usages of @compas/lint-config to use @compas/eslint-plugin.
52+
description: `Convert all known usages of @compas/lint-config to use @compas/eslint-plugin.
5253
5354
This only updates the configuration files and does not update the code to be consistent with the newly enforced rules.
5455
`,
5556
exec: executeLintConfigToEslintPlugin,
5657
},
58+
"api-client-to-experimental-code-gen": {
59+
description: `Convert the project to use experimental code-gen based on a list of structures in '$project/structures.txt'.
60+
61+
'structures.txt' has the following format;
62+
https://a.remote.compas.backend -- src/generated
63+
./local-openapi.json -- src/generated/foo -- defaultGroup
64+
65+
The code-mode executes the following steps:
66+
- Resolve and validated 'structures.txt'
67+
- Resolve all mentioned structures from 'structures.txt'
68+
- Overwrite 'scripts/generate.mjs'
69+
- Execute 'scripts/generate.mjs'
70+
- Try to overwrite as much type usages as possible based on the cleaner type name generation.
71+
72+
Manual cleanup:
73+
- Remove structures.txt
74+
- Copy-edit & cleanup 'scripts/generate.mjs'
75+
- Use environment variables where appropriate
76+
- Cleanup imports
77+
- Correct 'targetRuntime' when using React-native.
78+
- Go through 'mutation' hooks usage & flatten arguments
79+
`,
80+
exec: executeApiClientToExperimentalCodeGen,
81+
},
5782
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* @param {Logger} logger
3+
* @returns {Promise<void>}
4+
*/
5+
export function executeApiClientToExperimentalCodeGen(
6+
logger: Logger,
7+
): Promise<void>;
8+
/**
9+
* Uppercase first character of the input string
10+
*
11+
* @param {string|undefined} [str] input string
12+
* @returns {string}
13+
*/
14+
export function upperCaseFirst(str?: string | undefined): string;
15+
//# sourceMappingURL=api-client-to-experimental-code-gen.d.ts.map
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
// @ts-nocheck
2+
3+
import { existsSync, readFileSync, writeFileSync } from "fs";
4+
import { exec, processDirectoryRecursiveSync } from "@compas/stdlib";
5+
6+
/**
7+
* @param {Logger} logger
8+
* @returns {Promise<void>}
9+
*/
10+
export async function executeApiClientToExperimentalCodeGen(logger) {
11+
const structureInformation = readStructureInformation();
12+
const loadedStructures = await loadStructures(structureInformation);
13+
logger.info(`Loaded ${loadedStructures.length} structures...`);
14+
15+
writeGenerateFile(structureInformation);
16+
logger.info(`Written scripts/generate.mjs. Executing...`);
17+
await exec(`node ./scripts/generate.mjs`, {});
18+
19+
const nameMap = loadNameMap(loadedStructures);
20+
const fileList = loadFileList(structureInformation);
21+
logger.info(
22+
`Rewriting ${Object.keys(nameMap).length} type names in ${
23+
fileList.length
24+
} files...`,
25+
);
26+
27+
rewriteInFiles(fileList, nameMap);
28+
logger.info(`Done rewriting type names.`);
29+
logger.info(`Manual cleanup:
30+
- Remove structures.txt
31+
- Copy-edit & cleanup 'scripts/generate.mjs'
32+
- Use environment variables where appropriate
33+
- Cleanup imports
34+
- Correct 'targetRuntime' when using React-native.
35+
- Go through 'mutation' hooks usage & flatten arguments
36+
- Update imports from 'generated/common/reactQuery' to 'generated/common/api-client(-wrapper).tsx'
37+
- ...
38+
`);
39+
}
40+
41+
function readStructureInformation() {
42+
const fileContents = readFileSync("./structures.txt", "utf-8");
43+
const lines = fileContents.split("\n").filter((it) => !!it.trim());
44+
45+
return lines.map((it) => {
46+
const [structurePath, outputDirectory, defaultGroup] = it.split(" -- ");
47+
48+
return {
49+
defaultGroup,
50+
structurePath,
51+
outputDirectory,
52+
};
53+
});
54+
}
55+
56+
async function loadStructures(structureInformation) {
57+
const { loadApiStructureFromOpenAPI, loadApiStructureFromRemote } =
58+
await import("@compas/code-gen");
59+
const { default: Axios } = await import("axios");
60+
61+
const result = [];
62+
63+
for (const si of structureInformation) {
64+
if (existsSync(si.structurePath)) {
65+
result.push(
66+
loadApiStructureFromOpenAPI(
67+
si.defaultGroup,
68+
JSON.parse(readFileSync(si.structurePath, "utf-8")),
69+
),
70+
);
71+
} else {
72+
result.push(await loadApiStructureFromRemote(Axios, si.structurePath));
73+
}
74+
}
75+
76+
return result;
77+
}
78+
79+
function writeGenerateFile(structureInformation) {
80+
writeFileSync(
81+
"./scripts/generate.mjs",
82+
`
83+
import { Generator } from "@compas/code-gen/experimental";
84+
import { loadApiStructureFromOpenAPI, loadApiStructureFromRemote } from "@compas/code-gen";
85+
import Axios from "axios";
86+
import { readFileSync } from "node:fs";
87+
import { mainFn } from "@compas/stdlib";
88+
89+
process.env.NODE_ENV = "development";
90+
mainFn(import.meta, main);
91+
92+
async function main() {
93+
${structureInformation
94+
.map((si) => {
95+
if (existsSync(si.structurePath)) {
96+
return `{
97+
const generator = new Generator();
98+
99+
generator.addStructure(loadApiStructureFromOpenAPI("${si.defaultGroup}", JSON.parse(readFileSync("${si.structurePath}", "utf-8"))));
100+
101+
generator.generate({
102+
targetLanguage: "ts",
103+
outputDirectory: "${si.outputDirectory}",
104+
generators: {
105+
apiClient: {
106+
target: {
107+
library: "axios",
108+
targetRuntime: "browser",
109+
// globalClients: true,
110+
includeWrapper: "react-query",
111+
},
112+
},
113+
},
114+
});
115+
}`;
116+
}
117+
118+
return `{
119+
const generator = new Generator();
120+
121+
generator.addStructure(await loadApiStructureFromRemote(Axios, "${si.structurePath}"));
122+
123+
generator.generate({
124+
targetLanguage: "ts",
125+
outputDirectory: "${si.outputDirectory}",
126+
generators: {
127+
apiClient: {
128+
target: {
129+
library: "axios",
130+
targetRuntime: "browser",
131+
// globalClients: true,
132+
includeWrapper: "react-query",
133+
},
134+
},
135+
},
136+
});
137+
}`;
138+
})
139+
.join("\n\n")}
140+
}
141+
`,
142+
);
143+
}
144+
145+
function loadNameMap(structures) {
146+
const result = {};
147+
148+
for (const s of structures) {
149+
for (const group of Object.keys(s)) {
150+
for (const name of Object.keys(s[group])) {
151+
result[`${upperCaseFirst(group)}${upperCaseFirst(name)}Input`] =
152+
upperCaseFirst(group) + upperCaseFirst(name);
153+
result[`${upperCaseFirst(group)}${upperCaseFirst(name)}Api`] =
154+
upperCaseFirst(group) + upperCaseFirst(name);
155+
}
156+
}
157+
}
158+
159+
return result;
160+
}
161+
162+
function loadFileList(structureInformation) {
163+
const fileList = [];
164+
165+
processDirectoryRecursiveSync(process.cwd(), (f) => {
166+
if (structureInformation.find((it) => f.includes(it.outputDirectory))) {
167+
return;
168+
}
169+
170+
if (f.includes("vendor/")) {
171+
return;
172+
}
173+
174+
if (
175+
f.endsWith(".ts") ||
176+
f.endsWith(".js") ||
177+
f.endsWith(".tsx") ||
178+
f.endsWith(".jsx") ||
179+
f.endsWith(".mjs")
180+
) {
181+
fileList.push(f);
182+
}
183+
});
184+
185+
return fileList;
186+
}
187+
188+
function rewriteInFiles(fileList, nameMap) {
189+
for (const file of fileList) {
190+
let contents = readFileSync(file, "utf-8");
191+
let didReplace = false;
192+
193+
for (const [name, replacement] of Object.entries(nameMap)) {
194+
if (contents.includes(name)) {
195+
contents = contents.replaceAll(name, replacement);
196+
didReplace = true;
197+
}
198+
}
199+
200+
if (didReplace) {
201+
writeFileSync(file, contents);
202+
}
203+
}
204+
}
205+
206+
/**
207+
* Uppercase first character of the input string
208+
*
209+
* @param {string|undefined} [str] input string
210+
* @returns {string}
211+
*/
212+
export function upperCaseFirst(str = "") {
213+
return str.length > 0 ? str[0].toUpperCase() + str.substring(1) : "";
214+
}

packages/cli/src/compas/commands/code-mod.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export async function cliExecutor(logger, state) {
8181
${value.description}
8282
8383
Execute with '${state.cli.name} code-mod exec --name ${key}'
84+
8485
`;
8586
}
8687
logger.info(str);

0 commit comments

Comments
 (0)