diff options
| author | 2023-04-28 01:36:44 +0800 | |
|---|---|---|
| committer | 2023-04-28 01:36:44 +0800 | |
| commit | dd84b9d64fb98746a230cd24233ff50a562c39c9 (patch) | |
| tree | b583261ef00b3afe72ec4d6dacb31e57779a6faf /packages/turbo-codemod/src/transforms | |
| parent | 0b46fcd72ac34382387b2bcf9095233efbcc52f4 (diff) | |
| download | HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.tar.gz HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.zip | |
Diffstat (limited to 'packages/turbo-codemod/src/transforms')
5 files changed, 459 insertions, 0 deletions
diff --git a/packages/turbo-codemod/src/transforms/README.md b/packages/turbo-codemod/src/transforms/README.md new file mode 100644 index 0000000..8e4430f --- /dev/null +++ b/packages/turbo-codemod/src/transforms/README.md @@ -0,0 +1,36 @@ +# `@turbo/codemod` Transformers + +## Adding new transformers + +Add new transformers using the [plopjs](https://github.com/plopjs/plop) template by running: + +```bash +pnpm add-transformer +``` + +New Transformers will be automatically surfaced to the `transform` CLI command and used by the `migrate` CLI command when appropriate. + +## How it works + +Transformers are loaded automatically from the `src/transforms/` directory via the [`loadTransforms`](../utils/loadTransformers.ts) function. + +All new transformers must contain a default export that matches the [`Transformer`](../types.ts) type: + +```ts +export type Transformer = { + name: string; + value: string; + introducedIn: string; + transformer: (args: TransformerArgs) => TransformerResults; +}; +``` + +## Writing a Transform + +Transforms are ran using the [TransformRunner](../runner/Runner.ts). This class is designed to make writing transforms as simple as possible by abstracting away all of the boilerplate that determines what should be logged, saved, or output as a result. + +To use the TransformRunner: + +1. Transform each file in memory (do not write it back to disk `TransformRunner` takes care of this depending on the options passed in by the user), and pass to `TransformRunner.modifyFile` method. +2. If the transform encounters an unrecoverable error, pass it to the `TransformRunner.abortTransform` method. +3. When all files have been modified and passed to `TransformRunner.modifyFile`, call `TransformRunner.finish` method to write the files to disk (when not running in `dry` mode) and log the results. diff --git a/packages/turbo-codemod/src/transforms/add-package-manager.ts b/packages/turbo-codemod/src/transforms/add-package-manager.ts new file mode 100644 index 0000000..bd6581f --- /dev/null +++ b/packages/turbo-codemod/src/transforms/add-package-manager.ts @@ -0,0 +1,75 @@ +import path from "path"; +import fs from "fs-extra"; + +import getPackageManager from "../utils/getPackageManager"; +import getPackageManagerVersion from "../utils/getPackageManagerVersion"; +import getTransformerHelpers from "../utils/getTransformerHelpers"; +import { TransformerResults } from "../runner"; +import type { TransformerArgs } from "../types"; + +// transformer details +const TRANSFORMER = "add-package-manager"; +const DESCRIPTION = "Set the `packageManager` key in root `package.json` file"; +const INTRODUCED_IN = "1.1.0"; + +export function transformer({ + root, + options, +}: TransformerArgs): TransformerResults { + const { log, runner } = getTransformerHelpers({ + transformer: TRANSFORMER, + rootPath: root, + options, + }); + + log.info(`Set "packageManager" key in root "package.json" file...`); + const packageManager = getPackageManager({ directory: root }); + if (!packageManager) { + return runner.abortTransform({ + reason: `Unable to determine package manager for ${root}`, + }); + } + + // handle workspaces... + let version = null; + try { + version = getPackageManagerVersion(packageManager, root); + } catch (err) { + return runner.abortTransform({ + reason: `Unable to determine package manager version for ${root}`, + }); + } + const pkgManagerString = `${packageManager}@${version}`; + const rootPackageJsonPath = path.join(root, "package.json"); + const rootPackageJson = fs.readJsonSync(rootPackageJsonPath); + const allWorkspaces = [ + { + name: "package.json", + path: root, + packageJson: { + ...rootPackageJson, + packageJsonPath: rootPackageJsonPath, + }, + }, + ]; + + for (const workspace of allWorkspaces) { + const { packageJsonPath, ...pkgJson } = workspace.packageJson; + const newJson = { ...pkgJson, packageManager: pkgManagerString }; + runner.modifyFile({ + filePath: packageJsonPath, + after: newJson, + }); + } + + return runner.finish(); +} + +const transformerMeta = { + name: `${TRANSFORMER}: ${DESCRIPTION}`, + value: TRANSFORMER, + introducedIn: INTRODUCED_IN, + transformer, +}; + +export default transformerMeta; diff --git a/packages/turbo-codemod/src/transforms/create-turbo-config.ts b/packages/turbo-codemod/src/transforms/create-turbo-config.ts new file mode 100644 index 0000000..0e8549a --- /dev/null +++ b/packages/turbo-codemod/src/transforms/create-turbo-config.ts @@ -0,0 +1,70 @@ +import fs from "fs-extra"; +import path from "path"; + +import { TransformerResults } from "../runner"; +import getTransformerHelpers from "../utils/getTransformerHelpers"; +import type { TransformerArgs } from "../types"; + +// transformer details +const TRANSFORMER = "create-turbo-config"; +const DESCRIPTION = + 'Create the `turbo.json` file from an existing "turbo" key in `package.json`'; +const INTRODUCED_IN = "1.1.0"; + +export function transformer({ + root, + options, +}: TransformerArgs): TransformerResults { + const { log, runner } = getTransformerHelpers({ + transformer: TRANSFORMER, + rootPath: root, + options, + }); + + log.info(`Migrating "package.json" "turbo" key to "turbo.json" file...`); + const turboConfigPath = path.join(root, "turbo.json"); + const rootPackageJsonPath = path.join(root, "package.json"); + if (!fs.existsSync(rootPackageJsonPath)) { + return runner.abortTransform({ + reason: `No package.json found at ${root}. Is the path correct?`, + }); + } + + // read files + const rootPackageJson = fs.readJsonSync(rootPackageJsonPath); + let rootTurboJson = null; + try { + rootTurboJson = fs.readJSONSync(turboConfigPath); + } catch (err) { + rootTurboJson = null; + } + + // modify files + let transformedPackageJson = rootPackageJson; + let transformedTurboConfig = rootTurboJson; + if (!rootTurboJson && rootPackageJson["turbo"]) { + const { turbo: turboConfig, ...remainingPkgJson } = rootPackageJson; + transformedTurboConfig = turboConfig; + transformedPackageJson = remainingPkgJson; + } + + runner.modifyFile({ + filePath: turboConfigPath, + after: transformedTurboConfig, + }); + runner.modifyFile({ + filePath: rootPackageJsonPath, + after: transformedPackageJson, + }); + + return runner.finish(); +} + +const transformerMeta = { + name: `${TRANSFORMER}: ${DESCRIPTION}`, + value: TRANSFORMER, + introducedIn: INTRODUCED_IN, + transformer, +}; + +export default transformerMeta; diff --git a/packages/turbo-codemod/src/transforms/migrate-env-var-dependencies.ts b/packages/turbo-codemod/src/transforms/migrate-env-var-dependencies.ts new file mode 100644 index 0000000..ef3a34c --- /dev/null +++ b/packages/turbo-codemod/src/transforms/migrate-env-var-dependencies.ts @@ -0,0 +1,181 @@ +import fs from "fs-extra"; +import path from "path"; +import { getTurboConfigs } from "@turbo/utils"; +import type { Schema, Pipeline } from "@turbo/types"; + +import getTransformerHelpers from "../utils/getTransformerHelpers"; +import { TransformerResults } from "../runner"; +import type { TransformerArgs } from "../types"; + +// transformer details +const TRANSFORMER = "migrate-env-var-dependencies"; +const DESCRIPTION = + 'Migrate environment variable dependencies from "dependsOn" to "env" in `turbo.json`'; +const INTRODUCED_IN = "1.5.0"; + +export function hasLegacyEnvVarDependencies(config: Schema) { + const dependsOn = [ + "extends" in config ? [] : config.globalDependencies, + Object.values(config.pipeline).flatMap( + (pipeline) => pipeline.dependsOn ?? [] + ), + ].flat(); + const envVars = dependsOn.filter((dep) => dep?.startsWith("$")); + return { hasKeys: !!envVars.length, envVars }; +} + +export function migrateDependencies({ + env, + deps, +}: { + env?: string[]; + deps?: string[]; +}) { + const envDeps: Set<string> = new Set(env); + const otherDeps: string[] = []; + deps?.forEach((dep) => { + if (dep.startsWith("$")) { + envDeps.add(dep.slice(1)); + } else { + otherDeps.push(dep); + } + }); + if (envDeps.size) { + return { + deps: otherDeps, + env: Array.from(envDeps), + }; + } else { + return { env, deps }; + } +} + +export function migratePipeline(pipeline: Pipeline) { + const { deps: dependsOn, env } = migrateDependencies({ + env: pipeline.env, + deps: pipeline.dependsOn, + }); + const migratedPipeline = { ...pipeline }; + if (dependsOn) { + migratedPipeline.dependsOn = dependsOn; + } else { + delete migratedPipeline.dependsOn; + } + if (env && env.length) { + migratedPipeline.env = env; + } else { + delete migratedPipeline.env; + } + + return migratedPipeline; +} + +export function migrateGlobal(config: Schema) { + if ("extends" in config) { + return config; + } + + const { deps: globalDependencies, env } = migrateDependencies({ + env: config.globalEnv, + deps: config.globalDependencies, + }); + const migratedConfig = { ...config }; + if (globalDependencies && globalDependencies.length) { + migratedConfig.globalDependencies = globalDependencies; + } else { + delete migratedConfig.globalDependencies; + } + if (env && env.length) { + migratedConfig.globalEnv = env; + } else { + delete migratedConfig.globalEnv; + } + return migratedConfig; +} + +export function migrateConfig(config: Schema) { + let migratedConfig = migrateGlobal(config); + Object.keys(config.pipeline).forEach((pipelineKey) => { + config.pipeline; + if (migratedConfig.pipeline && config.pipeline[pipelineKey]) { + const pipeline = migratedConfig.pipeline[pipelineKey]; + migratedConfig.pipeline[pipelineKey] = { + ...pipeline, + ...migratePipeline(pipeline), + }; + } + }); + return migratedConfig; +} + +export function transformer({ + root, + options, +}: TransformerArgs): TransformerResults { + const { log, runner } = getTransformerHelpers({ + transformer: TRANSFORMER, + rootPath: root, + options, + }); + + log.info( + `Migrating environment variable dependencies from "globalDependencies" and "dependsOn" to "env" in "turbo.json"...` + ); + + // validate we don't have a package.json config + const packageJsonPath = path.join(root, "package.json"); + let packageJSON = {}; + try { + packageJSON = fs.readJSONSync(packageJsonPath); + } catch (e) { + // readJSONSync probably failed because the file doesn't exist + } + + if ("turbo" in packageJSON) { + return runner.abortTransform({ + reason: + '"turbo" key detected in package.json. Run `npx @turbo/codemod transform create-turbo-config` first', + }); + } + + // validate we have a root config + const turboConfigPath = path.join(root, "turbo.json"); + if (!fs.existsSync(turboConfigPath)) { + return runner.abortTransform({ + reason: `No turbo.json found at ${root}. Is the path correct?`, + }); + } + + let turboJson: Schema = fs.readJsonSync(turboConfigPath); + if (hasLegacyEnvVarDependencies(turboJson).hasKeys) { + turboJson = migrateConfig(turboJson); + } + + runner.modifyFile({ + filePath: turboConfigPath, + after: turboJson, + }); + + // find and migrate any workspace configs + const workspaceConfigs = getTurboConfigs(root); + workspaceConfigs.forEach((workspaceConfig) => { + const { config, turboConfigPath } = workspaceConfig; + if (hasLegacyEnvVarDependencies(config).hasKeys) { + runner.modifyFile({ + filePath: turboConfigPath, + after: migrateConfig(config), + }); + } + }); + + return runner.finish(); +} + +const transformerMeta = { + name: `${TRANSFORMER}: ${DESCRIPTION}`, + value: TRANSFORMER, + introducedIn: INTRODUCED_IN, + transformer, +}; + +export default transformerMeta; diff --git a/packages/turbo-codemod/src/transforms/set-default-outputs.ts b/packages/turbo-codemod/src/transforms/set-default-outputs.ts new file mode 100644 index 0000000..44f7fd1 --- /dev/null +++ b/packages/turbo-codemod/src/transforms/set-default-outputs.ts @@ -0,0 +1,97 @@ +import path from "path"; +import fs from "fs-extra"; +import { getTurboConfigs } from "@turbo/utils"; +import type { Schema as TurboJsonSchema } from "@turbo/types"; + +import type { TransformerArgs } from "../types"; +import getTransformerHelpers from "../utils/getTransformerHelpers"; +import { TransformerResults } from "../runner"; + +const DEFAULT_OUTPUTS = ["dist/**", "build/**"]; + +// transformer details +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"; + +function migrateConfig(config: TurboJsonSchema) { + for (const [_, taskDef] of Object.entries(config.pipeline)) { + if (taskDef.cache !== false) { + if (!taskDef.outputs) { + taskDef.outputs = DEFAULT_OUTPUTS; + } else if ( + Array.isArray(taskDef.outputs) && + taskDef.outputs.length === 0 + ) { + delete taskDef.outputs; + } + } + } + + return config; +} + +export function transformer({ + root, + options, +}: TransformerArgs): TransformerResults { + const { log, runner } = getTransformerHelpers({ + transformer: TRANSFORMER, + rootPath: root, + options, + }); + + // If `turbo` key is detected in package.json, require user to run the other codemod first. + const packageJsonPath = path.join(root, "package.json"); + // package.json should always exist, but if it doesn't, it would be a silly place to blow up this codemod + let packageJSON = {}; + + try { + packageJSON = fs.readJSONSync(packageJsonPath); + } catch (e) { + // readJSONSync probably failed because the file doesn't exist + } + + if ("turbo" in packageJSON) { + return runner.abortTransform({ + reason: + '"turbo" key detected in package.json. Run `npx @turbo/codemod transform create-turbo-config` first', + }); + } + + log.info(`Adding default \`outputs\` key into tasks if it doesn't exist`); + const turboConfigPath = path.join(root, "turbo.json"); + if (!fs.existsSync(turboConfigPath)) { + return runner.abortTransform({ + reason: `No turbo.json found at ${root}. Is the path correct?`, + }); + } + + const turboJson: TurboJsonSchema = fs.readJsonSync(turboConfigPath); + runner.modifyFile({ + filePath: turboConfigPath, + after: migrateConfig(turboJson), + }); + + // find and migrate any workspace configs + const workspaceConfigs = getTurboConfigs(root); + workspaceConfigs.forEach((workspaceConfig) => { + const { config, turboConfigPath } = workspaceConfig; + runner.modifyFile({ + filePath: turboConfigPath, + after: migrateConfig(config), + }); + }); + + return runner.finish(); +} + +const transformerMeta = { + name: `${TRANSFORMER}: ${DESCRIPTION}`, + value: TRANSFORMER, + introducedIn: INTRODUCED_IN, + transformer, +}; + +export default transformerMeta; |
