diff --git a/packages/cli/src/code-mod/constants.js b/packages/cli/src/code-mod/constants.js index 2e75487a1c..7af9bff390 100644 --- a/packages/cli/src/code-mod/constants.js +++ b/packages/cli/src/code-mod/constants.js @@ -1,7 +1,4 @@ import { cpus } from "node:os"; -import { executeApiClientToExperimentalCodeGen } from "./mods/api-client-to-experimental-code-gen.js"; -import { executeLintConfigToEslintPlugin } from "./mods/lint-config-to-eslint-plugin.js"; -import { executeUpdateQueriesSignatureChange } from "./mods/update-queries-signature-change.js"; export const PARALLEL_COUNT = Math.max(cpus().length - 1, 1); @@ -11,72 +8,4 @@ export const PARALLEL_COUNT = Math.max(cpus().length - 1, 1); * exec: (logger: Logger) => Promise * }>} */ -export const codeModMap = { - "update-queries-signature-change": { - description: `Convert arguments in call sites of generated 'queries.entityUpdate' to pass in the named 'where' and 'update' parameters. - It also adds 'returning: "*"' if it detects that the result is used, or if it is unable to detect that the return value is unused. - - Inline update & where; - // old - await queries.entityUpdate(sql, { /* inline update */ }, { /* inline where */}); - // new - await queries.entityUpdate(sql, { - update: { /* inline update */ }, - where: { /* inline where */ }, - }); - - Referenced update & where: - // old - await queries.entityUpdate(sql, update, where); - // new - await queries.entityUpdate(sql, { - update, - where, - }); - - Infer that result is used: - // old - const [updatedEntity] = await queries.entityUpdate(sql, update, where); - // new - const [updatedEntity] = await queries.entityUpdate(sql, { - update, - where, - returning: "*", - }); - - Or any combination of the above. -`, - exec: executeUpdateQueriesSignatureChange, - }, - "lint-config-to-eslint-plugin": { - description: `Convert all known usages of @compas/lint-config to use @compas/eslint-plugin. - - This only updates the configuration files and does not update the code to be consistent with the newly enforced rules. -`, - exec: executeLintConfigToEslintPlugin, - }, - "api-client-to-experimental-code-gen": { - description: `Convert the project to use experimental code-gen based on a list of structures in '$project/structures.txt'. - - 'structures.txt' has the following format; - https://a.remote.compas.backend -- src/generated - ./local-openapi.json -- src/generated/foo -- defaultGroup - - The code-mode executes the following steps: - - Resolve and validated 'structures.txt' - - Resolve all mentioned structures from 'structures.txt' - - Overwrite 'scripts/generate.mjs' - - Execute 'scripts/generate.mjs' - - Try to overwrite as much type usages as possible based on the cleaner type name generation. - - Manual cleanup: - - Remove structures.txt - - Copy-edit & cleanup 'scripts/generate.mjs' - - Use environment variables where appropriate - - Cleanup imports - - Correct 'targetRuntime' when using React-native. - - Go through 'mutation' hooks usage & flatten arguments - `, - exec: executeApiClientToExperimentalCodeGen, - }, -}; +export const codeModMap = {}; diff --git a/packages/cli/src/code-mod/mods/api-client-to-experimental-code-gen.d.ts b/packages/cli/src/code-mod/mods/api-client-to-experimental-code-gen.d.ts deleted file mode 100644 index 49a99953ad..0000000000 --- a/packages/cli/src/code-mod/mods/api-client-to-experimental-code-gen.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @param {Logger} logger - * @returns {Promise} - */ -export function executeApiClientToExperimentalCodeGen( - logger: Logger, -): Promise; -/** - * Uppercase first character of the input string - * - * @param {string|undefined} [str] input string - * @returns {string} - */ -export function upperCaseFirst(str?: string | undefined): string; -//# sourceMappingURL=api-client-to-experimental-code-gen.d.ts.map diff --git a/packages/cli/src/code-mod/mods/api-client-to-experimental-code-gen.js b/packages/cli/src/code-mod/mods/api-client-to-experimental-code-gen.js deleted file mode 100644 index 00fb68d433..0000000000 --- a/packages/cli/src/code-mod/mods/api-client-to-experimental-code-gen.js +++ /dev/null @@ -1,213 +0,0 @@ -// @ts-nocheck - -import { existsSync, readFileSync, writeFileSync } from "node:fs"; -import { exec, processDirectoryRecursiveSync } from "@compas/stdlib"; - -/** - * @param {Logger} logger - * @returns {Promise} - */ -export async function executeApiClientToExperimentalCodeGen(logger) { - const structureInformation = readStructureInformation(); - const loadedStructures = await loadStructures(structureInformation); - logger.info(`Loaded ${loadedStructures.length} structures...`); - - writeGenerateFile(structureInformation); - logger.info(`Written scripts/generate.mjs. Executing...`); - await exec(`node ./scripts/generate.mjs`, {}); - - const nameMap = loadNameMap(loadedStructures); - const fileList = loadFileList(structureInformation); - logger.info( - `Rewriting ${Object.keys(nameMap).length} type names in ${ - fileList.length - } files...`, - ); - - rewriteInFiles(fileList, nameMap); - logger.info(`Done rewriting type names.`); - logger.info(`Manual cleanup: - - Remove structures.txt - - Copy-edit & cleanup 'scripts/generate.mjs' - - Use environment variables where appropriate - - Cleanup imports - - Correct 'targetRuntime' when using React-native. - - Go through 'mutation' hooks usage & flatten arguments - - Update imports from 'generated/common/reactQuery' to 'generated/common/api-client(-wrapper).tsx' - - ... -`); -} - -function readStructureInformation() { - const fileContents = readFileSync("./structures.txt", "utf-8"); - const lines = fileContents.split("\n").filter((it) => !!it.trim()); - - return lines.map((it) => { - const [structurePath, outputDirectory, defaultGroup] = it.split(" -- "); - - return { - defaultGroup, - structurePath, - outputDirectory, - }; - }); -} - -async function loadStructures(structureInformation) { - const { loadApiStructureFromOpenAPI, loadApiStructureFromRemote } = - await import("@compas/code-gen"); - const { default: Axios } = await import("axios"); - - const result = []; - - for (const si of structureInformation) { - if (existsSync(si.structurePath)) { - result.push( - loadApiStructureFromOpenAPI( - si.defaultGroup, - JSON.parse(readFileSync(si.structurePath, "utf-8")), - ), - ); - } else { - result.push(await loadApiStructureFromRemote(Axios, si.structurePath)); - } - } - - return result; -} - -function writeGenerateFile(structureInformation) { - writeFileSync( - "./scripts/generate.mjs", - ` -import { Generator, loadApiStructureFromOpenAPI, loadApiStructureFromRemote } from "@compas/code-gen"; -import Axios from "axios"; -import { readFileSync } from "node:fs"; -import { mainFn } from "@compas/stdlib"; - -process.env.NODE_ENV = "development"; -mainFn(import.meta, main); - -async function main() { -${structureInformation - .map((si) => { - if (existsSync(si.structurePath)) { - return `{ - const generator = new Generator(); - - generator.addStructure(loadApiStructureFromOpenAPI("${si.defaultGroup}", JSON.parse(readFileSync("${si.structurePath}", "utf-8")))); - - generator.generate({ - targetLanguage: "ts", - outputDirectory: "${si.outputDirectory}", - generators: { - apiClient: { - target: { - library: "axios", - targetRuntime: "browser", - // globalClients: true, - includeWrapper: "react-query", - }, - }, - }, - }); - }`; - } - - return `{ - const generator = new Generator(); - - generator.addStructure(await loadApiStructureFromRemote(Axios, "${si.structurePath}")); - - generator.generate({ - targetLanguage: "ts", - outputDirectory: "${si.outputDirectory}", - generators: { - apiClient: { - target: { - library: "axios", - targetRuntime: "browser", - // globalClients: true, - includeWrapper: "react-query", - }, - }, - }, - }); - }`; - }) - .join("\n\n")} -} -`, - ); -} - -function loadNameMap(structures) { - const result = {}; - - for (const s of structures) { - for (const group of Object.keys(s)) { - for (const name of Object.keys(s[group])) { - result[`${upperCaseFirst(group)}${upperCaseFirst(name)}Input`] = - upperCaseFirst(group) + upperCaseFirst(name); - result[`${upperCaseFirst(group)}${upperCaseFirst(name)}Api`] = - upperCaseFirst(group) + upperCaseFirst(name); - } - } - } - - return result; -} - -function loadFileList(structureInformation) { - const fileList = []; - - processDirectoryRecursiveSync(process.cwd(), (f) => { - if (structureInformation.find((it) => f.includes(it.outputDirectory))) { - return; - } - - if (f.includes("vendor/")) { - return; - } - - if ( - f.endsWith(".ts") || - f.endsWith(".js") || - f.endsWith(".tsx") || - f.endsWith(".jsx") || - f.endsWith(".mjs") - ) { - fileList.push(f); - } - }); - - return fileList; -} - -function rewriteInFiles(fileList, nameMap) { - for (const file of fileList) { - let contents = readFileSync(file, "utf-8"); - let didReplace = false; - - for (const [name, replacement] of Object.entries(nameMap)) { - if (contents.includes(name)) { - contents = contents.replaceAll(name, replacement); - didReplace = true; - } - } - - if (didReplace) { - writeFileSync(file, contents); - } - } -} - -/** - * Uppercase first character of the input string - * - * @param {string|undefined} [str] input string - * @returns {string} - */ -export function upperCaseFirst(str = "") { - return str.length > 0 ? str[0].toUpperCase() + str.substring(1) : ""; -} diff --git a/packages/cli/src/code-mod/mods/lint-config-to-eslint-plugin.d.ts b/packages/cli/src/code-mod/mods/lint-config-to-eslint-plugin.d.ts deleted file mode 100644 index ddcc5fa018..0000000000 --- a/packages/cli/src/code-mod/mods/lint-config-to-eslint-plugin.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Convert all known usages from @compas/lint-config to eslint-plugin - * - * @param {Logger} logger - * @returns {Promise} - */ -export function executeLintConfigToEslintPlugin(logger: Logger): Promise; -//# sourceMappingURL=lint-config-to-eslint-plugin.d.ts.map diff --git a/packages/cli/src/code-mod/mods/lint-config-to-eslint-plugin.js b/packages/cli/src/code-mod/mods/lint-config-to-eslint-plugin.js deleted file mode 100644 index 00037b49a8..0000000000 --- a/packages/cli/src/code-mod/mods/lint-config-to-eslint-plugin.js +++ /dev/null @@ -1,47 +0,0 @@ -import { readFile, rm, writeFile } from "node:fs/promises"; - -/** - * Convert all known usages from @compas/lint-config to eslint-plugin - * - * @param {Logger} logger - * @returns {Promise} - */ -export async function executeLintConfigToEslintPlugin(logger) { - await Promise.all([updatePackageJson(), updateEslintRc()]); - - logger.info( - "Updated all configuration files. Run 'npm install && npx compas lint' to see if the newly applied rules report any issues.", - ); -} - -async function updatePackageJson() { - const pkgJson = JSON.parse(await readFile("./package.json", "utf-8")); - - const hasLintConfig = pkgJson.devDependencies["@compas/lint-config"]; - if (!hasLintConfig && !pkgJson.prettier) { - return; - } - - if (hasLintConfig) { - delete pkgJson.devDependencies["@compas/lint-config"]; - - pkgJson.devDependencies["@compas/eslint-plugin"] = - pkgJson.dependencies["@compas/cli"] ?? - pkgJson.dependencies["@compas/cli"]; - } - - pkgJson.prettier = "@compas/eslint-plugin/prettierrc"; - - await writeFile("./package.json", `${JSON.stringify(pkgJson, null, 2)}\n`); -} - -async function updateEslintRc() { - await rm("./.eslintrc", { force: true }); - await rm("./.eslintrc.js", { force: true }); - await rm("./.eslintrc.cjs", { force: true }); - - await writeFile( - "./.eslintrc", - `${JSON.stringify({ extends: ["plugin:@compas/full"] }, null, 2)}\n`, - ); -} diff --git a/packages/cli/src/code-mod/mods/update-queries-signature-change.d.ts b/packages/cli/src/code-mod/mods/update-queries-signature-change.d.ts deleted file mode 100644 index 9239e4b46f..0000000000 --- a/packages/cli/src/code-mod/mods/update-queries-signature-change.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Convert `queries.entitySelect(sql, { ...where })` to `queryEntity({ where: { ...where - * } }).exec(sql)` - * - * @param {Logger} logger - * @returns {Promise} - */ -export function executeUpdateQueriesSignatureChange( - logger: Logger, -): Promise; -//# sourceMappingURL=update-queries-signature-change.d.ts.map diff --git a/packages/cli/src/code-mod/mods/update-queries-signature-change.js b/packages/cli/src/code-mod/mods/update-queries-signature-change.js deleted file mode 100644 index 2ff687725f..0000000000 --- a/packages/cli/src/code-mod/mods/update-queries-signature-change.js +++ /dev/null @@ -1,280 +0,0 @@ -// @ts-nocheck - -import { readFile, writeFile } from "node:fs/promises"; -import { AppError, processDirectoryRecursive } from "@compas/stdlib"; -import { PARALLEL_COUNT } from "../constants.js"; - -/** - * Convert `queries.entitySelect(sql, { ...where })` to `queryEntity({ where: { ...where - * } }).exec(sql)` - * - * @param {Logger} logger - * @returns {Promise} - */ -export async function executeUpdateQueriesSignatureChange(logger) { - const generatedFileList = await listGeneratedEntityFiles(); - const fullFileList = await listAllJavaScriptFiles(generatedFileList); - - const { callCount, fileCount, possibleInvalidCalls } = await modTheFiles( - fullFileList, - ); - - logger.info(`Converted ${callCount} occurrences in ${fileCount} files.`); - - if (possibleInvalidCalls.length > 0) { - for (const invalidCall of possibleInvalidCalls) { - logger.info( - `Found a possible invalid usage that is not transformed at '${invalidCall}'.`, - ); - } - } -} - -/** - * Find generated database files - * - * @returns {Promise} - */ -async function listGeneratedEntityFiles() { - const possibleFileList = []; - - await processDirectoryRecursive(process.cwd(), (file) => { - if (file.match(/database\/\w+\.js$/gi) && !file.endsWith("index.js")) { - possibleFileList.push(file); - } - }); - - const result = []; - - // Double check that these files are generated - while (possibleFileList.length) { - const partial = possibleFileList.splice(0, PARALLEL_COUNT); - - await Promise.all( - partial.map(async (file) => { - const contents = await readFile(file, "utf-8"); - - if (contents.startsWith("// Generated by @compas/code-gen")) { - result.push(file); - } - }), - ); - } - - return result; -} - -/** - * Find all js files, excluding the generated ones - * - * @param {string[]} generatedFiles - * @returns {Promise} - */ -async function listAllJavaScriptFiles(generatedFiles) { - const result = []; - - await processDirectoryRecursive(process.cwd(), (file) => { - if (!file.endsWith(".js")) { - return; - } - - if (generatedFiles.includes(file)) { - return; - } - - result.push(file); - }); - - return result; -} - -/** - * Replace the call-sites - * - * @param {string[]} fileList - * @returns {Promise<{ callCount: number, fileCount: number, possibleInvalidCalls: - * string[] }>} - */ -async function modTheFiles(fileList) { - const recast = await import("recast"); - const builders = recast.types.builders; - - const filesToWrite = {}; - let replaceCount = 0; - let modifiedFileCount = 0; - const possibleInvalidCalls = []; - - while (fileList.length) { - const partial = fileList.splice(0, PARALLEL_COUNT); - - await Promise.all( - partial.map(async (file) => { - let didReplace = false; - const contents = await readFile(file, "utf-8"); - - // We only supported destructured 'queries' imports, so skip parsing if the file - // doesn't include such an instance. - if (!contents.includes("queries")) { - return; - } - - const ast = await parseFile(file, contents); - - // Cases; - // Input: await queries.bbbUpdate(sql, update, where); - // Output: await queries.bbbUpdate(sql, { update, where }); - - // Input: const result = await queries.bbbUpdate(sql, update, where); - // Output: const result = await queries.bbbUpdate(sql, {update, where, returning: - // "*" }); - recast.visit(ast, { - visitCallExpression(path) { - if (path.node.callee.type !== "MemberExpression") { - this.traverse(path); - return; - } - - // Check if object is a 'queries' - if (path.node.callee.object.name !== "queries") { - this.traverse(path); - return; - } - - // Check if .property is an entityUpdate() - if (!/^\w+Update$/g.test(path.node.callee.property.name)) { - this.traverse(path); - return; - } - - if (path.node.arguments.length !== 3) { - possibleInvalidCalls.push( - `${file}:${path.node.loc.start.line}:${path.node.loc.start.column}`, - ); - - this.traverse(path); - return; - } - - const resultIsUsed = - path.parent.parent.value.type === "VariableDeclarator" || - path.parent.parent.value.type !== "ExpressionStatement"; - - // Replace call - const sql = path.node.arguments[0]; - let update = path.node.arguments[1]; - let where = path.node.arguments[2]; - - if (update.type === "ObjectExpression") { - update = builders.objectExpression(update.properties); - } - if (where.type === "ObjectExpression") { - where = builders.objectExpression(where.properties); - } - - path.replace( - builders.callExpression(path.node.callee, [ - sql, - builders.objectExpression([ - builders.objectProperty( - builders.identifier("update"), - update, - false, - update.type === "Identifier" && update.name === "update", - ), - builders.objectProperty( - builders.identifier("where"), - where, - false, - where.type === "Identifier" && where.name === "where", - ), - ...(resultIsUsed - ? [ - builders.objectProperty( - builders.identifier("returning"), - builders.stringLiteral("*"), - false, - false, - ), - ] - : []), - ]), - ]), - ); - - replaceCount++; - didReplace = true; - - this.traverse(path); - }, - }); - - if (didReplace) { - modifiedFileCount++; - filesToWrite[file] = recast.print(ast).code; - } - }), - ); - } - - // Only write if no error is thrown - for (const [file, source] of Object.entries(filesToWrite)) { - await writeFile(file, source, "utf-8"); - } - - return { - callCount: replaceCount, - fileCount: modifiedFileCount, - possibleInvalidCalls, - }; -} - -/** - * Parse a file, enabling typescript and jsx parsers when necessary - * - * @param {string} file - * @param {string} contents - * @returns {Promise} - */ -async function parseFile(file, contents) { - let babel; - - try { - babel = await import("@babel/parser"); - } catch (e) { - throw new AppError("cli.codeMod.failedToLoadBabel", 500, { - message: - "Please install @compas/eslint-plugin, or @babel/parser directly.", - }); - } - - const recast = await import("recast"); - const babelOpts = await import("recast/parsers/_babel_options.js"); - const opts = babelOpts.default.default(); - - try { - return recast.parse(contents, { - parser: { - parse(code) { - if (file.endsWith(".ts")) { - opts.plugins.push("typescript"); - } else if (file.endsWith(".tsx")) { - opts.plugins.push("jsx", "typescript"); - } else if (file.endsWith(".jsx")) { - opts.plugins.push("jsx"); - } - - return babel.parse(code, opts); - }, - }, - }); - } catch (e) { - throw new AppError( - "cli.codeMod.failedToParseFile", - 500, - { - file, - }, - e, - ); - } -}