Skip to content

Commit

Permalink
feat(codemod): add logic for major version bump (#8260)
Browse files Browse the repository at this point in the history
### 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).
  • Loading branch information
chris-olszewski authored May 31, 2024
1 parent 80b8e48 commit 35a0708
Show file tree
Hide file tree
Showing 13 changed files with 153 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "turbo-1",
"version": "1.0.0",
"dependencies": {},
"devDependencies": {
"turbo": "1.7.1"
}
}
Original file line number Diff line number Diff line change
@@ -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
}
90 changes: 90 additions & 0 deletions packages/turbo-codemod/__tests__/migrate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: "[email protected]",
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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,21 @@ export function getTransformsForMigration({
fromVersion: string;
toVersion: string;
}): Array<Transformer> {
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).
Expand Down
2 changes: 1 addition & 1 deletion packages/turbo-codemod/src/transforms/add-package-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 10 additions & 5 deletions packages/turbo-codemod/src/transforms/migrate-dot-env.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
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";

// 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 ?? [];
Expand All @@ -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$"];
Expand All @@ -32,7 +37,7 @@ function migrateConfig(config: TurboJsonSchema) {
}
delete taskDef.dotEnv;
}
}
});

return config;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
2 changes: 1 addition & 1 deletion packages/turbo-codemod/src/transforms/rename-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions packages/turbo-codemod/src/transforms/set-default-outputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -92,6 +93,7 @@ const transformerMeta: Transformer = {
name: TRANSFORMER,
description: DESCRIPTION,
introducedIn: INTRODUCED_IN,
idempotent: IDEMPOTENT,
transformer,
};

Expand Down
2 changes: 1 addition & 1 deletion packages/turbo-codemod/src/transforms/stabilize-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/turbo-codemod/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface Transformer {
name: string;
description: string;
introducedIn: string;
idempotent?: boolean;
transformer: (
args: TransformerArgs
) => Promise<TransformerResults> | TransformerResults;
Expand Down
2 changes: 1 addition & 1 deletion packages/turbo-utils/src/getTurboConfigs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 5 additions & 1 deletion packages/turbo-utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down

0 comments on commit 35a0708

Please sign in to comment.