Skip to content

Commit

Permalink
feat(cli/migrate): user-friendly cli output for replace-remix-imports
Browse files Browse the repository at this point in the history
  • Loading branch information
pcattori committed Apr 10, 2022
1 parent 6087b24 commit 6d93edd
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand All @@ -59,16 +90,26 @@ 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: {
...depsToObject(otherDeps),
"@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 }
: {}),
},
Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -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")}`);
};
Original file line number Diff line number Diff line change
@@ -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);

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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}$`
);
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
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 {
Adapter,
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",
Expand All @@ -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 <runtime>` 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<Runtime> => {
// match `remix setup <runtime>` 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 }>([
{
Expand All @@ -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;
};
Expand Down

0 comments on commit 6d93edd

Please sign in to comment.