From 6d93edd6fd7127424e744c6d2a59dd85f621377a Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Sun, 10 Apr 2022 10:07:47 -0400 Subject: [PATCH] feat(cli/migrate): user-friendly cli output for `replace-remix-imports` --- .../migrations/replace-remix-imports/index.ts | 83 ++++++++++++++++--- .../replace-remix-imports/messages.ts | 7 ++ .../replace-remix-imports/postinstall.ts | 1 - .../replace-remix-imports/remixSetup.ts | 6 ++ .../resolveTransformOptions.ts | 81 +++++++++++++----- 5 files changed, 143 insertions(+), 35 deletions(-) create mode 100644 packages/remix-dev/cli/migrate/migrations/replace-remix-imports/messages.ts delete mode 100644 packages/remix-dev/cli/migrate/migrations/replace-remix-imports/postinstall.ts create mode 100644 packages/remix-dev/cli/migrate/migrations/replace-remix-imports/remixSetup.ts diff --git a/packages/remix-dev/cli/migrate/migrations/replace-remix-imports/index.ts b/packages/remix-dev/cli/migrate/migrations/replace-remix-imports/index.ts index adea3a3205a..1c72ce2d10b 100644 --- a/packages/remix-dev/cli/migrate/migrations/replace-remix-imports/index.ts +++ b/packages/remix-dev/cli/migrate/migrations/replace-remix-imports/index.ts @@ -3,43 +3,74 @@ import NpmCliPackageJson from "@npmcli/package-json"; import { join } from "path"; import glob from "fast-glob"; import { maxBy } from "lodash"; +import chalk from "chalk"; import { readConfig } from "../../../../config"; +import * as colors from "../../../../colors"; import * as jscodeshift from "../../jscodeshift"; import type { MigrationFunction } from "../../types"; import { resolveTransformOptions } from "./resolveTransformOptions"; import type { Options } from "./transform"; import type { Dependency } from "./dependency"; import { depsToObject, isRemixPackage, depsToEntries } from "./dependency"; -import { remixSetupPattern } from "./postinstall"; +import { + onlyRemixSetup, + onlyRemixSetupRuntime, + remixSetup, +} from "./remixSetup"; +import { because, detected } from "./messages"; const TRANSFORM_PATH = join(__dirname, "transform"); const getRemixVersionSpec = (remixDeps: Dependency[]): string => { let candidate = maxBy(remixDeps, (dep) => semver.minVersion(dep.versionSpec)); if (candidate === undefined) { - console.error("TODO"); + console.error("āŒ I couldn't find versions for your Remix packages."); process.exit(1); } let candidateMin = semver.minVersion(candidate.versionSpec); if (candidateMin === null) { - console.error("TODO"); + console.error("āŒ I couldn't find versions for your Remix packages."); process.exit(1); } - return semver.lt(candidateMin, "1.3.3") ? "^1.3.3" : candidateMin.raw; + if (semver.lt(candidateMin, "1.3.3")) { + console.log("ā¬†ļø I'm upgrading your Remix dependencies"); + console.log(because("this migration requires v1.3.3 or newer.")); + return "^1.3.3"; + } + console.log( + detected( + `\`${chalk.blue( + candidate.versionSpec + )}\` as the best Remix version to use` + ) + ); + console.log(because("you're already using a compatible Remix version.")); + return candidateMin.raw; }; const shouldKeepPostinstall = (original?: string): boolean => { if (original === undefined) return false; - let hasRemixSetup = new RegExp(remixSetupPattern).test(original); - if (!hasRemixSetup) return true; + if (onlyRemixSetup.test(original) || onlyRemixSetupRuntime.test(original)) { + console.log( + "šŸ—‘ I'm removing `remix setup` from your `postinstall` script." + ); + return false; + } - let isOnlyRemixSetup = new RegExp(`^${remixSetupPattern}$`).test(original); - if (isOnlyRemixSetup) return false; + let hasRemixSetup = remixSetup.test(original); + if (hasRemixSetup) { + console.warn( + "āš ļø I couldn't remove `remix setup` from your `postinstall script" + ); + console.log(because("your `postinstall` script is too complex.")); + console.warn( + "šŸ‘‰ You need to manually remove `remix setup` from your `postinstall` script." + ); + } - console.warn("TODO"); return true; }; @@ -59,7 +90,18 @@ export const replaceRemixImports: MigrationFunction = async ({ let remixDevDeps = devDeps.filter(({ name }) => isRemixPackage(name)); let otherDevDeps = devDeps.filter(({ name }) => !isRemixPackage(name)); + let remixServeInstalled = remixDeps + .map(({ name }) => name) + .includes("@remix-run/serve"); + if (remixServeInstalled) { + let servePackage = colors.hint("@remix-run/serve"); + console.log(detected(`\`${servePackage}\` as your Remix server`)); + console.log(because("it is in your dependencies.")); + } + // 1. upgrade Remix package, remove unused Remix packages + console.log("\nšŸ’æ I'm checking your Remix dependencies"); + console.log(because("the `remix` package is deprecated.")); let remixVersionSpec = getRemixVersionSpec([...remixDeps, ...remixDevDeps]); pkg.update({ dependencies: { @@ -67,8 +109,7 @@ export const replaceRemixImports: MigrationFunction = async ({ "@remix-run/react": remixVersionSpec, [`@remix-run/${runtime}`]: remixVersionSpec, ...(adapter ? { [`@remix-run/${adapter}`]: remixVersionSpec } : {}), - // keep @remix-run/serve - ...(remixDeps.map(({ name }) => name).includes("@remix-run/serve") + ...(remixServeInstalled ? { [`@remix-run/serve`]: remixVersionSpec } : {}), }, @@ -82,8 +123,11 @@ export const replaceRemixImports: MigrationFunction = async ({ ), }, }); + console.log("āœ… Your Remix dependencies look good!"); // 2. Remove `remix setup` from postinstall + console.log("\nšŸ’æ I'm checking your `package.json` scripts"); + console.log(because("calling `remix setup` in `postinstall` is deprecated.")); if (!shouldKeepPostinstall(pkg.content.scripts?.postinstall)) { pkg.update({ scripts: Object.fromEntries( @@ -93,11 +137,14 @@ export const replaceRemixImports: MigrationFunction = async ({ ), }); } + console.log("āœ… Your `package.json` scripts looks good!"); // write updates to package.json await pkg.save(); // 3. Run codemod + console.log("\nšŸ’æ I'm replacing any `remix` imports"); + console.log(because("importing from `remix` is deprecated.")); let config = await readConfig(projectDir); let files = glob.sync("**/*.+(js|jsx|ts|tsx)", { cwd: config.appDirectory, @@ -109,4 +156,18 @@ export const replaceRemixImports: MigrationFunction = async ({ flags, transformOptions: { runtime, adapter }, }); + if (!codemodOk) { + console.error("āŒ I couldn't replace all of your `remix` imports."); + if (!flags.debug) { + console.log("šŸ‘‰ Try again with the `--debug` flag to see what failed."); + } + process.exit(1); + } + console.log("āœ… Your Remix imports look good!"); + + console.log("\nšŸšš I've successfully migrated your project! šŸŽ‰"); + console.log( + "\nšŸ‘‰ Reinstall from your new `package.json` to update your lockfile" + ); + console.log(` ${chalk.blue("npm install")}`); }; diff --git a/packages/remix-dev/cli/migrate/migrations/replace-remix-imports/messages.ts b/packages/remix-dev/cli/migrate/migrations/replace-remix-imports/messages.ts new file mode 100644 index 00000000000..bc3a1e04cf5 --- /dev/null +++ b/packages/remix-dev/cli/migrate/migrations/replace-remix-imports/messages.ts @@ -0,0 +1,7 @@ +import chalk from "chalk"; + +export const detected = (message: string) => + chalk.gray("šŸ•µļø I detected " + message); + +export const because = (message: string) => + chalk.gray(" ...because " + message); diff --git a/packages/remix-dev/cli/migrate/migrations/replace-remix-imports/postinstall.ts b/packages/remix-dev/cli/migrate/migrations/replace-remix-imports/postinstall.ts deleted file mode 100644 index 0c80fd46be0..00000000000 --- a/packages/remix-dev/cli/migrate/migrations/replace-remix-imports/postinstall.ts +++ /dev/null @@ -1 +0,0 @@ -export const remixSetupPattern = "remix\\s+setup(\\s*|\\s+(\\w+)\\s*)"; diff --git a/packages/remix-dev/cli/migrate/migrations/replace-remix-imports/remixSetup.ts b/packages/remix-dev/cli/migrate/migrations/replace-remix-imports/remixSetup.ts new file mode 100644 index 00000000000..8e0522288fd --- /dev/null +++ b/packages/remix-dev/cli/migrate/migrations/replace-remix-imports/remixSetup.ts @@ -0,0 +1,6 @@ +export const remixSetup = /\s*remix\s+setup\s*/; +export const remixSetupRuntime = /\s*remix\s+setup\s+(\w+)\s*/; +export const onlyRemixSetup = new RegExp(`^${remixSetup.source}$`); +export const onlyRemixSetupRuntime = new RegExp( + `^${remixSetupRuntime.source}$` +); diff --git a/packages/remix-dev/cli/migrate/migrations/replace-remix-imports/resolveTransformOptions.ts b/packages/remix-dev/cli/migrate/migrations/replace-remix-imports/resolveTransformOptions.ts index 09963269ee3..13551914803 100644 --- a/packages/remix-dev/cli/migrate/migrations/replace-remix-imports/resolveTransformOptions.ts +++ b/packages/remix-dev/cli/migrate/migrations/replace-remix-imports/resolveTransformOptions.ts @@ -1,7 +1,7 @@ import inquirer from "inquirer"; import type { PackageJson } from "@npmcli/package-json"; +import chalk from "chalk"; -import { error, hint } from "../../../../logging"; import type { Options } from "./transform"; import { runtimes, isRuntime, isAdapter } from "./transform"; import type { @@ -9,7 +9,8 @@ import type { Runtime, } from "./transform/mapNormalizedImports/packageExports"; import { depsToEntries, isRemixPackage } from "./dependency"; -import { remixSetupPattern } from "./postinstall"; +import { remixSetup, remixSetupRuntime } from "./remixSetup"; +import { because, detected } from "./messages"; const adapterToRuntime = { architect: "node", @@ -20,32 +21,65 @@ const adapterToRuntime = { vercel: "node", } as const; +const autoDetectPostinstallRuntime = ( + packageJson: PackageJson +): Runtime | undefined => { + let postinstall = packageJson.scripts?.postinstall; + if (postinstall === undefined) return undefined; + if (postinstall.match(remixSetup) === null) return undefined; + + // match `remix setup ` in `postinstall` script + let runtimeMatch = postinstall.match(remixSetupRuntime); + if (runtimeMatch === null) return "node"; + let runtime = runtimeMatch[1]; + if (isRuntime(runtime)) return runtime; + console.warn( + `ļøāš ļø You have \`${runtime}\` in your \`postinstall\` script, but \`${runtime}\` is not a valid Remix server runtime.` + ); + return undefined; +}; + +const detectedRuntime = (runtime: string) => { + let runtimePackage = chalk.blue(`@remix-run/${runtime}`); + return detected(`\`${runtimePackage}\` as your Remix server runtime`); +}; + const resolveRuntime = async ( packageJson: PackageJson, adapter?: Adapter ): Promise => { // match `remix setup ` in `postinstall` script - let remixSetupMatch = - packageJson.scripts?.postinstall?.match(remixSetupPattern); - if (remixSetupMatch && remixSetupMatch.length >= 2) { - // `remix setup` defaults to `node - if (remixSetupMatch[1] === undefined) return "node"; + let postinstallRuntime = autoDetectPostinstallRuntime(packageJson); + if (postinstallRuntime) { + console.log(detectedRuntime(postinstallRuntime)); + console.log( + because( + `you had \`remix setup ${postinstallRuntime}\` in your \`postinstall\` script.` + ) + ); + return postinstallRuntime; + } - let postinstallRuntime = remixSetupMatch[1].trim(); - if (isRuntime(postinstallRuntime)) { - return postinstallRuntime; - } + // infer runtime from adapter + if (adapter) { + let runtime = adapterToRuntime[adapter]; + console.log(detectedRuntime(runtime)); + let adapterPackage = chalk.blue(`@remix-run/${adapter}`); + console.log(because(`you have \`${adapterPackage}\` installed.`)); + return runtime; } // @remix-run/serve uses node let deps = depsToEntries(packageJson.dependencies); let remixDeps = deps.filter(({ name }) => isRemixPackage(name)); if (remixDeps.map(({ name }) => name).includes("@remix-run/serve")) { - return "node"; + let runtime = "node" as const; + console.log(detectedRuntime(runtime)); + console.log(because("you have `@remix-run/serve` installed.")); + return runtime; } - // infer runtime from adapter - if (adapter) return adapterToRuntime[adapter]; + console.log("šŸ•µļø I couldn't infer your Remix server runtime."); // otherwise, ask user for runtime let { runtime } = await inquirer.prompt<{ runtime?: Runtime }>([ { @@ -68,21 +102,22 @@ const resolveAdapter = (packageJson: PackageJson): Adapter | undefined => { if (adapters.length > 1) { console.error( - error( - `Found multiple Remix server adapters in dependencies: ${adapters.join( - "," - )}` - ) + "āŒ I found more than one Remix server adapter your in dependencies:" ); console.log( - hint( - "You should only need one Remix server adapter. Uninstall unused server adapter packages and try again." - ) + adapters.map((adapter) => ` - @remix-run/${adapter}`).join("\n") ); + console.log("šŸ‘‰ Uninstall unused adapters and try again."); process.exit(1); } - if (adapters.length === 1) return adapters[0]; + if (adapters.length === 1) { + let adapter = adapters[0]; + let adapterPackage = chalk.blue(`@remix-run/${adapter}`); + console.log(detected(`\`${adapterPackage}\` as your Remix server adapter`)); + console.log(because("it's in your dependencies.")); + return adapter; + } return undefined; };