upgrade-remix
Version:
Command line utility to upgrade all Remix/React Router dependencies together
280 lines (256 loc) • 7.86 kB
JavaScript
const fs = require("fs");
const path = require("path");
const { parseArgs } = require("node:util");
const childProcess = require("child_process");
const packageJsonPath = path.join(process.cwd(), "package.json");
if (!fs.existsSync(packageJsonPath)) {
throw new Error(
"Could not find a package.json file! " +
"Please run `npx upgrade-remix` from your root React-Router/Remix app directory.",
);
}
const packageJson = require(packageJsonPath);
const isWindows = process.platform === "win32";
const { args, version, implementation, framework } = setup();
if (args["list-versions"]) {
listVersions();
} else {
upgradePackages(args);
}
function setup() {
const { values: args, positionals } = parseArgs({
options: {
"dry-run": {
type: "boolean",
short: "d",
},
list: {
type: "boolean",
short: "l",
},
"package-manager": {
type: "string",
short: "p",
},
"no-sync": {
type: "boolean",
short: "s",
},
},
allowPositionals: true,
});
const version = positionals[0] || "latest";
const implementation = getPackageManagerImplementation(
args["package-manager"],
version,
);
const allDeps = {
...packageJson.dependencies,
...packageJson.devDependencies,
};
const framework = allDeps["@remix-run/react"]
? "remix"
: allDeps["react-router"]
? "react-router"
: null;
console.log(`Detected ${framework} application`);
return { args, version, implementation, framework };
}
function getPackageManagerImplementation(packageManagerFlag, version) {
let isExact = !/^[\^~]/.test(version);
const implementations = {
npm: {
name: "npm",
lockFile: "package-lock.json",
install: (packages, isDev) =>
[
"npm install --force",
isDev ? "--save-dev" : "--save",
isExact ? "--save-exact" : undefined,
packages,
]
.filter((a) => a)
.join(" "),
sync: "npm ci",
list: (package) => `npm ls ${package}`,
remove: (packages) => `npm uninstall ${packages}`,
},
yarn: {
name: "yarn",
lockFile: "yarn.lock",
install: (packages, isDev) =>
[
"yarn add --force",
isDev ? "--dev" : undefined,
isExact ? "--exact" : undefined,
packages,
]
.filter((a) => a)
.join(" "),
sync: "yarn install --frozen-lockfile",
list: (package) => `yarn list --pattern ${package}`,
remove: (packages) => `yarn remove ${packages}`,
},
pnpm: {
name: "pnpm",
lockFile: "pnpm-lock.yaml",
install: (packages, isDev) =>
[
"pnpm add --force",
isDev ? "--save-dev" : undefined,
isExact ? "--save-exact" : undefined,
packages,
]
.filter((a) => a)
.join(" "),
sync: "pnpm install --frozen-lockfile",
list: (package) => `pnpm list ${package}`,
remove: (packages) => `pnpm remove ${packages}`,
},
bun: {
name: "bun",
lockFile: "bun.lockb",
install: (packages, isDev) =>
[
"bun add --force",
isDev ? "--dev" : undefined,
isExact ? "--exact" : undefined,
packages,
]
.filter((a) => a)
.join(" "),
sync: `bun install --frozen-lockfile`,
list: (package) => {
return `bun pm ls | ${isWindows ? "findstr" : "grep"} ${package}`;
},
remove: (packages) => `bun remove ${packages}`,
},
};
const implementation = packageManagerFlag
? [packageManagerFlag, implementations[packageManagerFlag]]
: Object.entries(implementations).find(([name, impl]) => {
if (fs.existsSync(path.join(process.cwd(), impl.lockFile))) {
console.log(`Found ${impl.lockFile}, using ${name}`);
return true;
}
});
if (!implementation) {
throw new Error("Unsupported Package Manager");
}
return implementation[1];
}
function getDeps(deps) {
if (framework === "remix") {
return Object.keys(deps || {}).filter(
(k) =>
(k.startsWith("@remix-run/") || k === "remix") &&
!k.startsWith("@remix-run/v1-") &&
k !== "@remix-run/router",
);
} else if (framework === "react-router") {
return Object.keys(deps || {}).filter(
(k) =>
k.startsWith("@react-router/") ||
k === "react-router" ||
k === "react-router-dom",
);
}
throw new Error("Unable to detect if this is a Remix or a React Router app");
}
function listVersions() {
console.log(`Listing ${framework}} packages in "${packageJsonPath}"`);
const { dependencies, devDependencies } = require(packageJsonPath);
let deps = [
...getDeps(dependencies),
...getDeps(devDependencies),
...(dependencies["react-router"] ? ["react-router"] : []),
...(dependencies["react-router-dom"] ? ["react-router-dom"] : []),
...(dependencies["@remix-run/router"] ? ["@remix-run/router"] : []),
];
deps.forEach((dep) => {
let cmd = implementation.list(dep);
console.log(`Executing: ${cmd}`);
let stdout = childProcess.execSync(cmd).toString();
console.log(stdout);
});
}
function upgradePackages(args) {
if (args.dryRun) {
console.log(`⚠️ Skipping package updates due to --dry-run"`);
console.log(
` - Would have updated ${framework} packages in "${packageJsonPath}" to version "${version}"`,
);
} else {
console.log(
`Updating ${framework} packages in "${packageJsonPath}" to version "${version}"`,
);
}
if (version.startsWith("preview/")) {
if (implementation.name !== "pnpm") {
throw new Error("Preview versions are only supported with pnpm");
}
if (framework === "remix") {
throw new Error("Preview versions are not supported for Remix");
}
}
function installUpdates(_deps, isDev) {
let deps = getDeps(_deps);
if (deps.length === 0) {
console.log(
`No packages to update in ${isDev ? "devDependencies" : "dependencies"}`,
);
return;
}
const packages = deps
.map((k) => {
if (version.startsWith("preview/")) {
let dir = [
"react-router",
"react-router-dom",
"create-react-router",
].includes(k)
? k
: k.replace("@react-router/", "react-router-");
return `"${k}@github:remix-run/react-router#${version}&path:packages/${dir}"`;
} else {
return `${k}@${version}`;
}
})
.join(" ");
const cmd = implementation.install(packages, isDev);
if (args.dryRun) {
console.log(`SKIPPING install command due to --dry-run:`);
console.log(` ${cmd}`);
} else {
console.log(`Executing: ${cmd}`);
childProcess.execSync(cmd);
}
}
if (
!args.dryRun &&
version.startsWith("preview/") &&
packageJson.dependencies["react-router"].startsWith("github:")
) {
console.log(
"Installing a preview build overtop of another preview build can run into pnpm issues.",
);
console.log(
"This will uninstall the current preview builds and then then install the new preview build.",
);
let deps = getDeps({
...packageJson.dependencies,
...packageJson.devDependencies,
});
const cmd = implementation.remove(deps.join(" "));
console.log(`Executing: ${cmd}`);
childProcess.execSync(cmd);
}
installUpdates(packageJson.dependencies, false);
installUpdates(packageJson.devDependencies, true);
const syncCmd = implementation.sync;
if (!args["no-sync"] && !args.dryRun) {
console.log(`Running '${syncCmd}' to sync up all deps`);
childProcess.execSync(syncCmd);
}
}