From 35a07081be1ca99879c7415e3a68a979ead78414 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Thu, 30 May 2024 20:17:49 -0700 Subject: [PATCH] feat(codemod): add logic for major version bump (#8260) ### Description Add migration logic for the 1->2 move. For the major bump we run all existing transforms that are safe to be run twice to ensure that `turbo.json` is in the desired state. A few things to note: - We change the introduction version for the 2.0 codemods to the canary to allow for easier testing - I updated `migrate-dot-env` to work with either `pipeline` or `tasks` as there isn't a great way to order transforms that are introduced in the same version. - Add an idempotent flag to transformers that defaults to `true`. This is used to mark a transformation as one to avoid re-running. ### Testing Instructions Added unit test for major version migration. Manual testing on repos via `turbo build --filter='@turbo/codemod'` and then using `node ~/code/turbo/packages/turbo-codemod/dist/cli.js migrate --to 2.0.0-canary.0` (canary is necessary due to 2.0.0 not existing in npm yet). --- .../__fixtures__/migrate/turbo-1/package.json | 8 ++ .../__fixtures__/migrate/turbo-1/turbo.json | 19 ++++ .../turbo-codemod/__tests__/migrate.test.ts | 90 +++++++++++++++++++ .../steps/getTransformsForMigration.ts | 16 +++- .../src/transforms/add-package-names.ts | 2 +- .../src/transforms/migrate-dot-env.ts | 15 ++-- .../src/transforms/rename-output-mode.ts | 2 +- .../src/transforms/rename-pipeline.ts | 2 +- .../src/transforms/set-default-outputs.ts | 2 + .../src/transforms/stabilize-ui.ts | 2 +- packages/turbo-codemod/src/types.ts | 1 + packages/turbo-utils/src/getTurboConfigs.ts | 2 +- packages/turbo-utils/src/index.ts | 6 +- 13 files changed, 153 insertions(+), 14 deletions(-) create mode 100644 packages/turbo-codemod/__tests__/__fixtures__/migrate/turbo-1/package.json create mode 100644 packages/turbo-codemod/__tests__/__fixtures__/migrate/turbo-1/turbo.json diff --git a/packages/turbo-codemod/__tests__/__fixtures__/migrate/turbo-1/package.json b/packages/turbo-codemod/__tests__/__fixtures__/migrate/turbo-1/package.json new file mode 100644 index 0000000000000..24a20e7c3b688 --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/migrate/turbo-1/package.json @@ -0,0 +1,8 @@ +{ + "name": "turbo-1", + "version": "1.0.0", + "dependencies": {}, + "devDependencies": { + "turbo": "1.7.1" + } +} diff --git a/packages/turbo-codemod/__tests__/__fixtures__/migrate/turbo-1/turbo.json b/packages/turbo-codemod/__tests__/__fixtures__/migrate/turbo-1/turbo.json new file mode 100644 index 0000000000000..f8fdb8a585cfd --- /dev/null +++ b/packages/turbo-codemod/__tests__/__fixtures__/migrate/turbo-1/turbo.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://turbo.build/schema.json", + "pipeline": { + "build": { + "outputs": [".next/**", "!.next/cache/**"] + }, + "lint": { + "dotEnv": [".env.local"], + "outputs": [] + }, + "test": { + "outputMode": "errors-only" + }, + "dev": { + "cache": false + } + }, + "experimentalUI": true +} diff --git a/packages/turbo-codemod/__tests__/migrate.test.ts b/packages/turbo-codemod/__tests__/migrate.test.ts index 1a6171b5e9d62..1583d521a1af7 100644 --- a/packages/turbo-codemod/__tests__/migrate.test.ts +++ b/packages/turbo-codemod/__tests__/migrate.test.ts @@ -924,4 +924,94 @@ describe("migrate", () => { // restore mocks mockedCheckGitStatus.mockRestore(); }); + + it("migrates across majors with all required codemods", async () => { + const { root, readJson } = useFixture({ + fixture: "turbo-1", + }); + + const packageManager = "pnpm"; + const packageManagerVersion = "1.2.3"; + + // setup mocks + const mockedCheckGitStatus = jest + .spyOn(checkGitStatus, "checkGitStatus") + .mockReturnValue(undefined); + const mockedGetCurrentVersion = jest + .spyOn(getCurrentVersion, "getCurrentVersion") + .mockReturnValue("1.99.99"); + const mockedGetLatestVersion = jest + .spyOn(getLatestVersion, "getLatestVersion") + .mockResolvedValue("2.0.0"); + const mockedGetTurboUpgradeCommand = jest + .spyOn(getTurboUpgradeCommand, "getTurboUpgradeCommand") + .mockResolvedValue("pnpm install -g turbo@latest"); + const mockedGetAvailablePackageManagers = jest + .spyOn(turboUtils, "getAvailablePackageManagers") + .mockResolvedValue({ + pnpm: packageManagerVersion, + npm: undefined, + yarn: undefined, + bun: undefined, + }); + const mockedGetWorkspaceDetails = jest + .spyOn(turboWorkspaces, "getWorkspaceDetails") + .mockResolvedValue( + getWorkspaceDetailsMockReturnValue({ + root, + packageManager, + }) + ); + + await migrate(root, { + force: false, + dry: false, + print: false, + install: false, + }); + + expect(readJson("package.json")).toStrictEqual({ + dependencies: {}, + devDependencies: { + turbo: "1.7.1", + }, + name: "turbo-1", + packageManager: "pnpm@1.2.3", + version: "1.0.0", + }); + expect(readJson("turbo.json")).toStrictEqual({ + $schema: "https://turbo.build/schema.json", + tasks: { + build: { + outputs: [".next/**", "!.next/cache/**"], + }, + dev: { + cache: false, + }, + lint: { + inputs: ["$TURBO_DEFAULT$", ".env.local"], + outputs: [], + }, + test: { + outputLogs: "errors-only", + }, + }, + }); + + // verify mocks were called + expect(mockedCheckGitStatus).toHaveBeenCalled(); + expect(mockedGetCurrentVersion).toHaveBeenCalled(); + expect(mockedGetLatestVersion).toHaveBeenCalled(); + expect(mockedGetTurboUpgradeCommand).toHaveBeenCalled(); + expect(mockedGetAvailablePackageManagers).toHaveBeenCalled(); + expect(mockedGetWorkspaceDetails).toHaveBeenCalled(); + + // restore mocks + mockedCheckGitStatus.mockRestore(); + mockedGetCurrentVersion.mockRestore(); + mockedGetLatestVersion.mockRestore(); + mockedGetTurboUpgradeCommand.mockRestore(); + mockedGetAvailablePackageManagers.mockRestore(); + mockedGetWorkspaceDetails.mockRestore(); + }); }); diff --git a/packages/turbo-codemod/src/commands/migrate/steps/getTransformsForMigration.ts b/packages/turbo-codemod/src/commands/migrate/steps/getTransformsForMigration.ts index d36493b8b8bdf..12ad1410c3bf6 100644 --- a/packages/turbo-codemod/src/commands/migrate/steps/getTransformsForMigration.ts +++ b/packages/turbo-codemod/src/commands/migrate/steps/getTransformsForMigration.ts @@ -12,11 +12,21 @@ export function getTransformsForMigration({ fromVersion: string; toVersion: string; }): Array { + const fromMajor = fromVersion.split(".")[0]; + // if migrating "from" to "to" spans a major, floor "from" to ensure all required codemods are run + const isMajorBump = fromMajor !== toVersion.split(".")[0]; + const resolvedFromVersion = isMajorBump ? `${fromMajor}.0.0` : fromVersion; const transforms = loadTransformers().filter((transformer) => { - return ( + const inOriginalRange = gt(transformer.introducedIn, fromVersion) && - lte(transformer.introducedIn, toVersion) - ); + lte(transformer.introducedIn, toVersion); + // If a transform is only in the expanded range, then we should only perform it + // if it is idempotent. + const idempotentAndInExpandedRange = + (transformer.idempotent ?? true) && + gt(transformer.introducedIn, resolvedFromVersion) && + lte(transformer.introducedIn, toVersion); + return inOriginalRange || idempotentAndInExpandedRange; }); // Sort the transforms from oldest (1.0) to newest (1.10). diff --git a/packages/turbo-codemod/src/transforms/add-package-names.ts b/packages/turbo-codemod/src/transforms/add-package-names.ts index c58dd6b19a12f..a34b3df3ec91a 100644 --- a/packages/turbo-codemod/src/transforms/add-package-names.ts +++ b/packages/turbo-codemod/src/transforms/add-package-names.ts @@ -8,7 +8,7 @@ import { getTransformerHelpers } from "../utils/getTransformerHelpers"; // transformer details const TRANSFORMER = "add-package-names"; const DESCRIPTION = "Ensure all packages have a name in their package.json"; -const INTRODUCED_IN = "2.0.0"; +const INTRODUCED_IN = "2.0.0-canary.0"; interface PartialPackageJson { name?: string; diff --git a/packages/turbo-codemod/src/transforms/migrate-dot-env.ts b/packages/turbo-codemod/src/transforms/migrate-dot-env.ts index 97e21dd31de0d..3aff219c6871b 100644 --- a/packages/turbo-codemod/src/transforms/migrate-dot-env.ts +++ b/packages/turbo-codemod/src/transforms/migrate-dot-env.ts @@ -1,7 +1,12 @@ import path from "node:path"; import { readJsonSync, existsSync } from "fs-extra"; -import { type PackageJson, getTurboConfigs } from "@turbo/utils"; +import { + type PackageJson, + getTurboConfigs, + forEachTaskDef, +} from "@turbo/utils"; import type { Schema as TurboJsonSchema } from "@turbo/types"; +import type { LegacySchema } from "@turbo/types/src/types/config"; import type { Transformer, TransformerArgs } from "../types"; import { getTransformerHelpers } from "../utils/getTransformerHelpers"; import type { TransformerResults } from "../runner"; @@ -9,9 +14,9 @@ import type { TransformerResults } from "../runner"; // transformer details const TRANSFORMER = "migrate-dot-env"; const DESCRIPTION = 'Migrate the "dotEnv" entries to "inputs" in `turbo.json`'; -const INTRODUCED_IN = "2.0.0"; +const INTRODUCED_IN = "2.0.0-canary.0"; -function migrateConfig(config: TurboJsonSchema) { +function migrateConfig(config: LegacySchema) { if ("globalDotEnv" in config) { if (config.globalDotEnv) { config.globalDependencies = config.globalDependencies ?? []; @@ -22,7 +27,7 @@ function migrateConfig(config: TurboJsonSchema) { delete config.globalDotEnv; } - for (const [_, taskDef] of Object.entries(config.tasks)) { + forEachTaskDef(config, ([_, taskDef]) => { if ("dotEnv" in taskDef) { if (taskDef.dotEnv) { taskDef.inputs = taskDef.inputs ?? ["$TURBO_DEFAULT$"]; @@ -32,7 +37,7 @@ function migrateConfig(config: TurboJsonSchema) { } delete taskDef.dotEnv; } - } + }); return config; } diff --git a/packages/turbo-codemod/src/transforms/rename-output-mode.ts b/packages/turbo-codemod/src/transforms/rename-output-mode.ts index df372718c4bdf..d10257cb6dd62 100644 --- a/packages/turbo-codemod/src/transforms/rename-output-mode.ts +++ b/packages/turbo-codemod/src/transforms/rename-output-mode.ts @@ -11,7 +11,7 @@ import type { TransformerResults } from "../runner"; const TRANSFORMER = "rename-output-mode"; const DESCRIPTION = 'Rename the "outputMode" key to "outputLogs" in `turbo.json`'; -const INTRODUCED_IN = "2.0.0"; +const INTRODUCED_IN = "2.0.0-canary.0"; function migrateConfig(config: SchemaV1) { for (const [_, taskDef] of Object.entries(config.pipeline)) { diff --git a/packages/turbo-codemod/src/transforms/rename-pipeline.ts b/packages/turbo-codemod/src/transforms/rename-pipeline.ts index 5217631bbb59d..bf375b498a096 100644 --- a/packages/turbo-codemod/src/transforms/rename-pipeline.ts +++ b/packages/turbo-codemod/src/transforms/rename-pipeline.ts @@ -9,7 +9,7 @@ import type { TransformerResults } from "../runner"; // transformer details const TRANSFORMER = "rename-pipeline"; const DESCRIPTION = 'Rename the "pipeline" key to "tasks" in `turbo.json`'; -const INTRODUCED_IN = "2.0.0"; +const INTRODUCED_IN = "2.0.0-canary.0"; function migrateConfig(config: SchemaV1): Schema { const { pipeline, ...rest } = config; diff --git a/packages/turbo-codemod/src/transforms/set-default-outputs.ts b/packages/turbo-codemod/src/transforms/set-default-outputs.ts index 5de94265c2859..552f6cdbb108e 100644 --- a/packages/turbo-codemod/src/transforms/set-default-outputs.ts +++ b/packages/turbo-codemod/src/transforms/set-default-outputs.ts @@ -13,6 +13,7 @@ const TRANSFORMER = "set-default-outputs"; const DESCRIPTION = 'Add the "outputs" key with defaults where it is missing in `turbo.json`'; const INTRODUCED_IN = "1.7.0"; +const IDEMPOTENT = false; function migrateConfig(config: SchemaV1) { for (const [_, taskDef] of Object.entries(config.pipeline)) { @@ -92,6 +93,7 @@ const transformerMeta: Transformer = { name: TRANSFORMER, description: DESCRIPTION, introducedIn: INTRODUCED_IN, + idempotent: IDEMPOTENT, transformer, }; diff --git a/packages/turbo-codemod/src/transforms/stabilize-ui.ts b/packages/turbo-codemod/src/transforms/stabilize-ui.ts index ecd3bb819b639..a44186c2b0c88 100644 --- a/packages/turbo-codemod/src/transforms/stabilize-ui.ts +++ b/packages/turbo-codemod/src/transforms/stabilize-ui.ts @@ -8,7 +8,7 @@ import type { TransformerResults } from "../runner"; // transformer details const TRANSFORMER = "stabilize-ui"; const DESCRIPTION = 'Rename the "experimentalUI" key to "ui" in `turbo.json`'; -const INTRODUCED_IN = "2.0.0"; +const INTRODUCED_IN = "2.0.0-canary.0"; interface ExperimentalSchema extends RootSchema { experimentalUI?: boolean; diff --git a/packages/turbo-codemod/src/types.ts b/packages/turbo-codemod/src/types.ts index 625c46e828867..8af661141a050 100644 --- a/packages/turbo-codemod/src/types.ts +++ b/packages/turbo-codemod/src/types.ts @@ -4,6 +4,7 @@ export interface Transformer { name: string; description: string; introducedIn: string; + idempotent?: boolean; transformer: ( args: TransformerArgs ) => Promise | TransformerResults; diff --git a/packages/turbo-utils/src/getTurboConfigs.ts b/packages/turbo-utils/src/getTurboConfigs.ts index a7cdd185e21db..17ff5c31baa4c 100644 --- a/packages/turbo-utils/src/getTurboConfigs.ts +++ b/packages/turbo-utils/src/getTurboConfigs.ts @@ -208,7 +208,7 @@ export function getWorkspaceConfigs( export function forEachTaskDef( config: LegacySchema, f: (value: [string, Pipeline]) => void -) { +): void { if ("pipeline" in config) { Object.entries(config.pipeline).forEach(f); } else { diff --git a/packages/turbo-utils/src/index.ts b/packages/turbo-utils/src/index.ts index 0908872cd35b5..118c91fb74f95 100644 --- a/packages/turbo-utils/src/index.ts +++ b/packages/turbo-utils/src/index.ts @@ -1,6 +1,10 @@ // utils export { getTurboRoot } from "./getTurboRoot"; -export { getTurboConfigs, getWorkspaceConfigs } from "./getTurboConfigs"; +export { + getTurboConfigs, + getWorkspaceConfigs, + forEachTaskDef, +} from "./getTurboConfigs"; export { searchUp } from "./searchUp"; export { getAvailablePackageManagers,