aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/packages/turbo-codemod/src/commands/migrate
diff options
context:
space:
mode:
Diffstat (limited to 'packages/turbo-codemod/src/commands/migrate')
-rw-r--r--packages/turbo-codemod/src/commands/migrate/index.ts215
-rw-r--r--packages/turbo-codemod/src/commands/migrate/steps/getCurrentVersion.ts45
-rw-r--r--packages/turbo-codemod/src/commands/migrate/steps/getLatestVersion.ts31
-rw-r--r--packages/turbo-codemod/src/commands/migrate/steps/getTransformsForMigration.ts25
-rw-r--r--packages/turbo-codemod/src/commands/migrate/steps/getTurboUpgradeCommand.ts182
-rw-r--r--packages/turbo-codemod/src/commands/migrate/types.ts9
-rw-r--r--packages/turbo-codemod/src/commands/migrate/utils.ts16
7 files changed, 523 insertions, 0 deletions
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<string | undefined> {
+ 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<Transformer> {
+ 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<PackageManager, string | undefined> {
+ 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 | boolean | undefined>
+ ): 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 };