From dd84b9d64fb98746a230cd24233ff50a562c39c9 Mon Sep 17 00:00:00 2001 From: 简律纯 Date: Fri, 28 Apr 2023 01:36:44 +0800 Subject: --- packages/turbo-codemod/src/commands/index.ts | 11 ++ .../turbo-codemod/src/commands/migrate/index.ts | 215 +++++++++++++++++++++ .../commands/migrate/steps/getCurrentVersion.ts | 45 +++++ .../src/commands/migrate/steps/getLatestVersion.ts | 31 +++ .../migrate/steps/getTransformsForMigration.ts | 25 +++ .../migrate/steps/getTurboUpgradeCommand.ts | 182 +++++++++++++++++ .../turbo-codemod/src/commands/migrate/types.ts | 9 + .../turbo-codemod/src/commands/migrate/utils.ts | 16 ++ .../turbo-codemod/src/commands/transform/index.ts | 101 ++++++++++ .../turbo-codemod/src/commands/transform/types.ts | 7 + 10 files changed, 642 insertions(+) create mode 100644 packages/turbo-codemod/src/commands/index.ts create mode 100644 packages/turbo-codemod/src/commands/migrate/index.ts create mode 100644 packages/turbo-codemod/src/commands/migrate/steps/getCurrentVersion.ts create mode 100644 packages/turbo-codemod/src/commands/migrate/steps/getLatestVersion.ts create mode 100644 packages/turbo-codemod/src/commands/migrate/steps/getTransformsForMigration.ts create mode 100644 packages/turbo-codemod/src/commands/migrate/steps/getTurboUpgradeCommand.ts create mode 100644 packages/turbo-codemod/src/commands/migrate/types.ts create mode 100644 packages/turbo-codemod/src/commands/migrate/utils.ts create mode 100644 packages/turbo-codemod/src/commands/transform/index.ts create mode 100644 packages/turbo-codemod/src/commands/transform/types.ts (limited to 'packages/turbo-codemod/src/commands') diff --git a/packages/turbo-codemod/src/commands/index.ts b/packages/turbo-codemod/src/commands/index.ts new file mode 100644 index 0000000..a7aeee6 --- /dev/null +++ b/packages/turbo-codemod/src/commands/index.ts @@ -0,0 +1,11 @@ +export { default as migrate } from "./migrate"; +export { default as transform } from "./transform"; + +export type { + TransformCommandArgument, + TransformCommandOptions, +} from "./transform/types"; +export type { + MigrateCommandArgument, + MigrateCommandOptions, +} from "./migrate/types"; diff --git a/packages/turbo-codemod/src/commands/migrate/index.ts b/packages/turbo-codemod/src/commands/migrate/index.ts new file mode 100644 index 0000000..c4c6d02 --- /dev/null +++ b/packages/turbo-codemod/src/commands/migrate/index.ts @@ -0,0 +1,215 @@ +import chalk from "chalk"; +import os from "os"; +import inquirer from "inquirer"; +import { execSync } from "child_process"; + +import getCurrentVersion from "./steps/getCurrentVersion"; +import getLatestVersion from "./steps/getLatestVersion"; +import getCodemodsForMigration from "./steps/getTransformsForMigration"; +import checkGitStatus from "../../utils/checkGitStatus"; +import directoryInfo from "../../utils/directoryInfo"; +import getTurboUpgradeCommand from "./steps/getTurboUpgradeCommand"; +import Runner from "../../runner/Runner"; +import type { MigrateCommandArgument, MigrateCommandOptions } from "./types"; +import looksLikeRepo from "../../utils/looksLikeRepo"; + +function endMigration({ + message, + success, +}: { + message?: string; + success: boolean; +}) { + if (success) { + console.log(chalk.bold(chalk.green("Migration completed"))); + if (message) { + console.log(message); + } + return process.exit(0); + } + + console.log(chalk.bold(chalk.red("Migration failed"))); + if (message) { + console.log(message); + } + return process.exit(1); +} + +/** +Migration is done in 4 steps: + -- gather information + 1. find the version (x) of turbo to migrate from (if not specified) + 2. find the version (y) of turbo to migrate to (if not specified) + 3. determine which codemods need to be run to move from version x to version y + -- action + 4. execute the codemods (serially, and in order) + 5. update the turbo version (optionally) +**/ +export default async function migrate( + directory: MigrateCommandArgument, + options: MigrateCommandOptions +) { + // check git status + if (!options.dry) { + checkGitStatus({ directory, force: options.force }); + } + + const answers = await inquirer.prompt<{ + directoryInput?: string; + }>([ + { + type: "input", + name: "directoryInput", + message: "Where is the root of the repo to migrate?", + when: !directory, + default: ".", + validate: (directory: string) => { + const { exists, absolute } = directoryInfo({ directory }); + if (exists) { + return true; + } else { + return `Directory ${chalk.dim(`(${absolute})`)} does not exist`; + } + }, + filter: (directory: string) => directory.trim(), + }, + ]); + + const { directoryInput: selectedDirectory = directory as string } = answers; + const { exists, absolute: root } = directoryInfo({ + directory: selectedDirectory, + }); + if (!exists) { + return endMigration({ + success: false, + message: `Directory ${chalk.dim(`(${root})`)} does not exist`, + }); + } + + if (!looksLikeRepo({ directory: root })) { + return endMigration({ + success: false, + message: `Directory (${chalk.dim( + root + )}) does not appear to be a repository`, + }); + } + + // step 1 + const fromVersion = getCurrentVersion(selectedDirectory, options); + if (!fromVersion) { + return endMigration({ + success: false, + message: `Unable to infer the version of turbo being used by ${directory}`, + }); + } + + // step 2 + let toVersion = options.to; + try { + toVersion = await getLatestVersion(options); + } catch (err) { + let message = "UNKNOWN_ERROR"; + if (err instanceof Error) { + message = err.message; + } + return endMigration({ + success: false, + message, + }); + } + + if (!toVersion) { + return endMigration({ + success: false, + message: `Unable to fetch the latest version of turbo`, + }); + } + + if (fromVersion === toVersion) { + return endMigration({ + success: true, + message: `Nothing to do, current version (${chalk.bold( + fromVersion + )}) is the same as the requested version (${chalk.bold(toVersion)})`, + }); + } + + // step 3 + const codemods = getCodemodsForMigration({ fromVersion, toVersion }); + if (codemods.length === 0) { + console.log( + `No codemods required to migrate from ${fromVersion} to ${toVersion}`, + os.EOL + ); + } + + // step 4 + console.log( + `Upgrading turbo from ${chalk.bold(fromVersion)} to ${chalk.bold( + toVersion + )} (${ + codemods.length === 0 + ? "no codemods required" + : `${codemods.length} required codemod${ + codemods.length === 1 ? "" : "s" + }` + })`, + os.EOL + ); + const results = codemods.map((codemod, idx) => { + console.log( + `(${idx + 1}/${codemods.length}) ${chalk.bold( + `Running ${codemod.value}` + )}` + ); + + const result = codemod.transformer({ root: selectedDirectory, options }); + Runner.logResults(result); + return result; + }); + + const hasTransformError = results.some( + (result) => + result.fatalError || + Object.keys(result.changes).some((key) => result.changes[key].error) + ); + + if (hasTransformError) { + return endMigration({ + success: false, + message: `Could not complete migration due to codemod errors. Please fix the errors and try again.`, + }); + } + + // step 5 + const upgradeCommand = getTurboUpgradeCommand({ + directory: selectedDirectory, + to: options.to, + }); + + if (!upgradeCommand) { + return endMigration({ + success: false, + message: "Unable to determine upgrade command", + }); + } + + if (options.install) { + if (options.dry) { + console.log( + `Upgrading turbo with ${chalk.bold(upgradeCommand)} ${chalk.dim( + "(dry run)" + )}`, + os.EOL + ); + } else { + console.log(`Upgrading turbo with ${chalk.bold(upgradeCommand)}`, os.EOL); + execSync(upgradeCommand, { cwd: selectedDirectory }); + } + } else { + console.log(`Upgrade turbo with ${chalk.bold(upgradeCommand)}`, os.EOL); + } + + endMigration({ success: true }); +} diff --git a/packages/turbo-codemod/src/commands/migrate/steps/getCurrentVersion.ts b/packages/turbo-codemod/src/commands/migrate/steps/getCurrentVersion.ts new file mode 100644 index 0000000..3644f8b --- /dev/null +++ b/packages/turbo-codemod/src/commands/migrate/steps/getCurrentVersion.ts @@ -0,0 +1,45 @@ +import path from "path"; +import { existsSync } from "fs-extra"; + +import getPackageManager from "../../../utils/getPackageManager"; +import { exec } from "../utils"; +import type { MigrateCommandOptions } from "../types"; + +function getCurrentVersion( + directory: string, + opts: MigrateCommandOptions +): string | undefined { + const { from } = opts; + if (from) { + return from; + } + + // try global first + const turboVersionFromGlobal = exec(`turbo --version`, { cwd: directory }); + + if (turboVersionFromGlobal) { + return turboVersionFromGlobal; + } + + // try to use the package manager to find the version + const packageManager = getPackageManager({ directory }); + if (packageManager) { + if (packageManager === "yarn") { + return exec(`yarn turbo --version`, { cwd: directory }); + } + if (packageManager === "pnpm") { + return exec(`pnpm turbo --version`, { cwd: directory }); + } else { + // this doesn't work for npm, so manually build the binary path + const turboBin = path.join(directory, "node_modules", ".bin", "turbo"); + if (existsSync(turboBin)) { + return exec(`${turboBin} --version`, { cwd: directory }); + } + } + } + + // unable to determine local version, + return undefined; +} + +export default getCurrentVersion; diff --git a/packages/turbo-codemod/src/commands/migrate/steps/getLatestVersion.ts b/packages/turbo-codemod/src/commands/migrate/steps/getLatestVersion.ts new file mode 100644 index 0000000..a6ab7e6 --- /dev/null +++ b/packages/turbo-codemod/src/commands/migrate/steps/getLatestVersion.ts @@ -0,0 +1,31 @@ +import axios from "axios"; + +import type { MigrateCommandOptions } from "../types"; + +const REGISTRY = "https://registry.npmjs.org"; + +async function getPackageDetails({ packageName }: { packageName: string }) { + try { + const result = await axios.get(`${REGISTRY}/${packageName}`); + return result.data; + } catch (err) { + throw new Error(`Unable to fetch the latest version of ${packageName}`); + } +} + +export default async function getLatestVersion({ + to, +}: MigrateCommandOptions): Promise { + const packageDetails = await getPackageDetails({ packageName: "turbo" }); + const { "dist-tags": tags, versions } = packageDetails; + + if (to) { + if (tags[to] || versions[to]) { + return to; + } else { + throw new Error(`turbo@${to} does not exist`); + } + } + + return tags.latest as string; +} diff --git a/packages/turbo-codemod/src/commands/migrate/steps/getTransformsForMigration.ts b/packages/turbo-codemod/src/commands/migrate/steps/getTransformsForMigration.ts new file mode 100644 index 0000000..2224c06 --- /dev/null +++ b/packages/turbo-codemod/src/commands/migrate/steps/getTransformsForMigration.ts @@ -0,0 +1,25 @@ +import { gt, lte } from "semver"; + +import loadTransformers from "../../../utils/loadTransformers"; +import type { Transformer } from "../../../types"; + +/** + Returns all transformers introduced after fromVersion, but before or equal to toVersion +**/ +function getTransformsForMigration({ + fromVersion, + toVersion, +}: { + fromVersion: string; + toVersion: string; +}): Array { + const transforms = loadTransformers(); + return transforms.filter((transformer) => { + return ( + gt(transformer.introducedIn, fromVersion) && + lte(transformer.introducedIn, toVersion) + ); + }); +} + +export default getTransformsForMigration; diff --git a/packages/turbo-codemod/src/commands/migrate/steps/getTurboUpgradeCommand.ts b/packages/turbo-codemod/src/commands/migrate/steps/getTurboUpgradeCommand.ts new file mode 100644 index 0000000..8fd5972 --- /dev/null +++ b/packages/turbo-codemod/src/commands/migrate/steps/getTurboUpgradeCommand.ts @@ -0,0 +1,182 @@ +import os from "os"; +import path from "path"; +import fs from "fs-extra"; +import { gte } from "semver"; + +import { exec } from "../utils"; +import getPackageManager, { + PackageManager, +} from "../../../utils/getPackageManager"; +import getPackageManagerVersion from "../../../utils/getPackageManagerVersion"; + +type InstallType = "dependencies" | "devDependencies"; + +function getGlobalBinaryPaths(): Record { + return { + // we run these from a tmpdir to avoid corepack interference + yarn: exec(`yarn global bin`, { cwd: os.tmpdir() }), + npm: exec(`npm bin --global`, { cwd: os.tmpdir() }), + pnpm: exec(`pnpm bin --global`, { cwd: os.tmpdir() }), + }; +} + +function getGlobalUpgradeCommand( + packageManager: PackageManager, + to: string = "latest" +) { + switch (packageManager) { + case "yarn": + return `yarn global add turbo@${to}`; + case "npm": + return `npm install turbo@${to} --global`; + case "pnpm": + return `pnpm install turbo@${to} --global`; + } +} + +function getLocalUpgradeCommand({ + packageManager, + packageManagerVersion, + installType, + isUsingWorkspaces, + to = "latest", +}: { + packageManager: PackageManager; + packageManagerVersion: string; + installType: InstallType; + isUsingWorkspaces?: boolean; + to?: string; +}) { + const renderCommand = ( + command: Array + ): string => command.filter(Boolean).join(" "); + switch (packageManager) { + // yarn command differs depending on the version + case "yarn": + // yarn 2.x and 3.x (berry) + if (gte(packageManagerVersion, "2.0.0")) { + return renderCommand([ + "yarn", + "add", + `turbo@${to}`, + installType === "devDependencies" && "--dev", + ]); + // yarn 1.x + } else { + return renderCommand([ + "yarn", + "add", + `turbo@${to}`, + installType === "devDependencies" && "--dev", + isUsingWorkspaces && "-W", + ]); + } + case "npm": + return renderCommand([ + "npm", + "install", + `turbo@${to}`, + installType === "devDependencies" && "--save-dev", + ]); + case "pnpm": + return renderCommand([ + "pnpm", + "install", + `turbo@${to}`, + installType === "devDependencies" && "--save-dev", + isUsingWorkspaces && "-w", + ]); + } +} + +function getInstallType({ directory }: { directory: string }): { + installType?: InstallType; + isUsingWorkspaces?: boolean; +} { + // read package.json to make sure we have a reference to turbo + const packageJsonPath = path.join(directory, "package.json"); + const pnpmWorkspaceConfig = path.join(directory, "pnpm-workspace.yaml"); + const isPnpmWorkspaces = fs.existsSync(pnpmWorkspaceConfig); + + if (!fs.existsSync(packageJsonPath)) { + console.error(`Unable to find package.json at ${packageJsonPath}`); + return { installType: undefined, isUsingWorkspaces: undefined }; + } + + const packageJson = fs.readJsonSync(packageJsonPath); + const isDevDependency = + packageJson.devDependencies && "turbo" in packageJson.devDependencies; + const isDependency = + packageJson.dependencies && "turbo" in packageJson.dependencies; + let isUsingWorkspaces = "workspaces" in packageJson || isPnpmWorkspaces; + + if (isDependency || isDevDependency) { + return { + installType: isDependency ? "dependencies" : "devDependencies", + isUsingWorkspaces, + }; + } + + return { + installType: undefined, + isUsingWorkspaces, + }; +} + +/** + Finding the correct command to upgrade depends on two things: + 1. The package manager + 2. The install method (local or global) + + We try global first to let turbo handle the inference, then we try local. +**/ +export default function getTurboUpgradeCommand({ + directory, + to, +}: { + directory: string; + to?: string; +}) { + const turboBinaryPathFromGlobal = exec(`turbo bin`, { + cwd: directory, + stdio: "pipe", + }); + const packageManagerGlobalBinaryPaths = getGlobalBinaryPaths(); + + const globalPackageManager = Object.keys( + packageManagerGlobalBinaryPaths + ).find((packageManager) => { + const packageManagerBinPath = + packageManagerGlobalBinaryPaths[packageManager as PackageManager]; + if (packageManagerBinPath && turboBinaryPathFromGlobal) { + return turboBinaryPathFromGlobal.includes(packageManagerBinPath); + } + + return false; + }) as PackageManager; + + if (turboBinaryPathFromGlobal && globalPackageManager) { + // figure which package manager we need to upgrade + return getGlobalUpgradeCommand(globalPackageManager, to); + } else { + const packageManager = getPackageManager({ directory }); + // we didn't find a global install, so we'll try to find a local one + const { installType, isUsingWorkspaces } = getInstallType({ directory }); + if (packageManager && installType) { + const packageManagerVersion = getPackageManagerVersion( + packageManager, + directory + ); + + return getLocalUpgradeCommand({ + packageManager, + packageManagerVersion, + installType, + isUsingWorkspaces, + to, + }); + } + } + + return undefined; +} diff --git a/packages/turbo-codemod/src/commands/migrate/types.ts b/packages/turbo-codemod/src/commands/migrate/types.ts new file mode 100644 index 0000000..ae90965 --- /dev/null +++ b/packages/turbo-codemod/src/commands/migrate/types.ts @@ -0,0 +1,9 @@ +import { TransformerOptions } from "../../types"; + +export type MigrateCommandArgument = "string" | undefined; + +export interface MigrateCommandOptions extends TransformerOptions { + from?: string; + to?: string; + install: boolean; +} diff --git a/packages/turbo-codemod/src/commands/migrate/utils.ts b/packages/turbo-codemod/src/commands/migrate/utils.ts new file mode 100644 index 0000000..512d78b --- /dev/null +++ b/packages/turbo-codemod/src/commands/migrate/utils.ts @@ -0,0 +1,16 @@ +import { execSync, ExecSyncOptions } from "child_process"; + +function exec( + command: string, + opts: ExecSyncOptions, + fallback?: string +): string | undefined { + try { + const rawResult = execSync(command, opts); + return rawResult.toString("utf8").trim(); + } catch (err) { + return fallback || undefined; + } +} + +export { exec }; diff --git a/packages/turbo-codemod/src/commands/transform/index.ts b/packages/turbo-codemod/src/commands/transform/index.ts new file mode 100644 index 0000000..e3b86aa --- /dev/null +++ b/packages/turbo-codemod/src/commands/transform/index.ts @@ -0,0 +1,101 @@ +import chalk from "chalk"; +import inquirer from "inquirer"; + +import loadTransformers from "../../utils/loadTransformers"; +import checkGitStatus from "../../utils/checkGitStatus"; +import directoryInfo from "../../utils/directoryInfo"; +import type { + TransformCommandOptions, + TransformCommandArgument, +} from "./types"; +import { Runner } from "../../runner"; + +export default async function transform( + transform: TransformCommandArgument, + directory: TransformCommandArgument, + options: TransformCommandOptions +) { + const transforms = loadTransformers(); + if (options.list) { + console.log( + transforms + .map((transform) => `- ${chalk.cyan(transform.value)}`) + .join("\n") + ); + return process.exit(0); + } + + // check git status + if (!options.dry) { + checkGitStatus({ directory, force: options.force }); + } + + const answers = await inquirer.prompt<{ + directoryInput?: string; + transformerInput?: string; + }>([ + { + type: "input", + name: "directoryInput", + message: "Where is the root of the repo where the transform should run?", + when: !directory, + default: ".", + validate: (directory: string) => { + const { exists, absolute } = directoryInfo({ directory }); + if (exists) { + return true; + } else { + return `Directory ${chalk.dim(`(${absolute})`)} does not exist`; + } + }, + filter: (directory: string) => directory.trim(), + }, + { + type: "list", + name: "transformerInput", + message: "Which transform would you like to apply?", + when: !transform, + pageSize: transforms.length, + choices: transforms, + }, + ]); + + const { + directoryInput: selectedDirectory = directory as string, + transformerInput: selectedTransformer = transform as string, + } = answers; + const { exists, absolute: root } = directoryInfo({ + directory: selectedDirectory, + }); + if (!exists) { + console.error(`Directory ${chalk.dim(`(${root})`)} does not exist`); + return process.exit(1); + } + + const transformKeys = transforms.map((transform) => transform.value); + const transformData = transforms.find( + (transform) => transform.value === selectedTransformer + ); + + // validate transforms + if (!transformData) { + console.error( + `Invalid transform choice ${chalk.dim(`(${transform})`)}, pick one of:` + ); + console.error(transformKeys.map((key) => `- ${key}`).join("\n")); + return process.exit(1); + } + + // run the transform + const result = transformData.transformer({ + root, + options, + }); + + if (result.fatalError) { + // Runner already logs this, so we can just exit + return process.exit(1); + } + + Runner.logResults(result); +} diff --git a/packages/turbo-codemod/src/commands/transform/types.ts b/packages/turbo-codemod/src/commands/transform/types.ts new file mode 100644 index 0000000..9ac2db0 --- /dev/null +++ b/packages/turbo-codemod/src/commands/transform/types.ts @@ -0,0 +1,7 @@ +import { TransformerOptions } from "../../types"; + +export type TransformCommandArgument = "string" | undefined; + +export interface TransformCommandOptions extends TransformerOptions { + list: boolean; +} -- cgit v1.2.3-70-g09d2