Skip to content

Instantly share code, notes, and snippets.

@azu
Created April 2, 2025 00:16
Show Gist options
  • Save azu/9a6c957da8c5aef0426873df7e732eca to your computer and use it in GitHub Desktop.
Save azu/9a6c957da8c5aef0426873df7e732eca to your computer and use it in GitHub Desktop.
migrate to pnpm from npm/yarn
#!/usr/bin/env node --experimental-strip-types
// # npm/yarn を pnpm に移行するスクリプト
// ## 制限
// Node.jsのコアパッケージのみを利用する
// - fsのglob
// https://nodejs.org/api/fs.html#fspromisesglobpattern-options
// - util.parseArgv
// https://nodejs.org/api/util.html#utilparseargsconfig
// ## 変更箇所
// ### package.json
// - `corepack use pnpm`を実行して、pnpmを有効化する
// - `scripts`フィールドで`yarn`を`pnpm`コマンドに置換
// ### lockfile
// - `pnpm import`を実行して、lockfileをpnpmに変換する
// https://pnpm.io/cli/import
// ### .github/workflows/*.{yml,yaml}
// - pnpm/action-setup@v4がなければ、actions/checkoutの後に追加
// - `yarn` コマンドを`pnpm`に置換/`npm` コマンドを`pnpm`に置換
// https://github.com/azu/ni.zsh#command-table を参考にして置換
// - actions/setup-nodeのcacheの設定を pnpm に変更
// https://github.com/actions/setup-node
// ### .githooks/pre-commit
// `#!/usr/bin/env node`となっている場合は
// ```
// #!/bin/sh
// npx --no-install lint-staged
// ```
// に置換
const USAGE = `
Usage: migrate-to-pnpm [options]
Options:
--cwd <path> Change working directory to <path>
--help Show this help message
`;
import { promises as fs } from "fs";
import * as path from "path";
import * as util from "util";
import { exec as execCallback } from "child_process";
// child_processのexecをPromise化
const exec = util.promisify(execCallback);
// コマンドラインオプションの解析
const parseArgs = () => {
try {
const options = util.parseArgs({
options: {
cwd: {
type: "string",
},
help: {
type: "boolean",
},
},
});
if (options.values.help) {
console.log(USAGE);
process.exit(0);
}
return {
cwd: options.values.cwd ?? process.cwd(),
};
} catch (error) {
console.error(
`エラー: ${error instanceof Error ? error.message : String(error)}`
);
console.log(USAGE);
process.exit(1);
}
};
/**
* package.jsonを更新します
* @param {string} cwd - 作業ディレクトリ
*/
const updatePackageJson = async (cwd: string) => {
try {
const packageJsonPath = path.join(cwd, "package.json");
const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
const packageJson = JSON.parse(packageJsonContent);
// scriptsフィールドで `yarn` コマンドを `pnpm` に置換
if (packageJson.scripts) {
for (const [key, value] of Object.entries(packageJson.scripts)) {
if (typeof value === "string") {
// yarn コマンドを pnpm に置換
packageJson.scripts[key] = value.replace(/\byarn\b/g, "pnpm");
}
}
}
// 更新したpackage.jsonを書き込み
await fs.writeFile(
packageJsonPath,
JSON.stringify(packageJson, null, 2) + "\n",
"utf-8"
);
console.log("✅ package.jsonを更新しました");
} catch (error) {
console.error(
`❌ package.jsonの更新に失敗しました: ${
error instanceof Error ? error.message : String(error)
}`
);
throw error;
}
};
/**
* corerpackを使ってpnpmを有効化します
* @param {string} cwd - 作業ディレクトリ
*/
const enablePnpmWithCorepack = async (cwd: string) => {
try {
console.log("🔧 corepack use pnpmを実行しています...");
await exec("corepack use pnpm", { cwd });
console.log("✅ pnpmを有効化しました");
} catch (error) {
console.error(
`❌ pnpmの有効化に失敗しました: ${
error instanceof Error ? error.message : String(error)
}`
);
throw error;
}
};
/**
* lockfileをpnpmに変換します
* @param {string} cwd - 作業ディレクトリ
*/
const convertLockfile = async (cwd: string) => {
try {
// yarn.lockかpackage-lock.jsonがあるか確認
const hasYarnLock = await fileExists(path.join(cwd, "yarn.lock"));
const hasNpmLock = await fileExists(path.join(cwd, "package-lock.json"));
if (!hasYarnLock && !hasNpmLock) {
console.log(
"⚠️ yarn.lockもpackage-lock.jsonも見つかりませんでした。lockfileの変換をスキップします。"
);
return;
}
console.log("🔧 pnpm importを実行しています...");
await exec("pnpm import", { cwd });
console.log("✅ lockfileを変換しました");
} catch (error) {
console.error(
`❌ lockfileの変換に失敗しました: ${
error instanceof Error ? error.message : String(error)
}`
);
throw error;
}
};
/**
* ファイルが存在するか確認します
* @param {string} filePath - ファイルパス
* @returns {Promise<boolean>} ファイルが存在する場合はtrue
*/
const fileExists = async (filePath: string): Promise<boolean> => {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
};
/**
* GitHub Actionsのワークフローファイルを更新します
* @param {string} cwd - 作業ディレクトリ
*/
const updateGitHubWorkflows = async (cwd: string) => {
try {
const workflowsDir = path.join(cwd, ".github", "workflows");
// .github/workflowsディレクトリが存在するか確認
if (!(await fileExists(workflowsDir))) {
console.log(
"⚠️ .github/workflowsディレクトリが見つかりませんでした。GitHub Actionsの更新をスキップします。"
);
return;
}
// .github/workflows/*.{yml,yaml}ファイルを取得
// absoluteオプションがないため、相対パスで取得して後から絶対パスに変換
const relativeWorkflowFiles = await Array.fromAsync(
fs.glob("**/*.{yml,yaml}", {
cwd: workflowsDir,
})
);
if (relativeWorkflowFiles.length === 0) {
console.log(
"⚠️ ワークフローファイルが見つかりませんでした。GitHub Actionsの更新をスキップします。"
);
return;
}
// 相対パスを絶対パスに変換
const workflowFiles = relativeWorkflowFiles.map((relPath) =>
path.join(workflowsDir, relPath)
);
for (const workflowFile of workflowFiles) {
await updateWorkflowFile(workflowFile);
}
console.log(
`✅ ${workflowFiles.length}個のGitHub Actionsワークフローファイルを更新しました`
);
} catch (error) {
console.error(
`❌ GitHub Actionsワークフローの更新に失敗しました: ${
error instanceof Error ? error.message : String(error)
}`
);
throw error;
}
};
/**
* ワークフローファイルを更新します
* @param {string} filePath - ワークフローファイルのパス
*/
const updateWorkflowFile = async (filePath: string) => {
try {
let content = await fs.readFile(filePath, "utf-8");
// yarn/npmコマンドをpnpmに置換
// https://github.com/azu/ni.zsh#command-table を参考に置換
const replacements: [RegExp, string][] = [
[/\byarn\b/g, "pnpm"],
[/\byarn add\b/g, "pnpm add"],
[/\byarn add -D\b/g, "pnpm add -D"],
[/\byarn install\b/g, "pnpm install"],
[/\byarn remove\b/g, "pnpm remove"],
[/\byarn run\b/g, "pnpm run"],
[/\bnpm\b/g, "pnpm"],
[/\bnpm i\b/g, "pnpm install"],
[/\bnpm install\b/g, "pnpm install"],
[/\bnpm uninstall\b/g, "pnpm remove"],
[/\bnpm run\b/g, "pnpm run"],
];
for (const [pattern, replacement] of replacements) {
content = content.replace(pattern, replacement);
}
// actions/setup-nodeのcacheの設定を変更
const nodeSetupRegex = /uses:\s+actions\/setup-node@/;
if (nodeSetupRegex.test(content)) {
// すでにcacheがある場合は置換する
if (/cache:\s+['"](?:npm|yarn)['"]/.test(content)) {
content = content.replace(
/cache:\s+['"](?:npm|yarn)['"]/,
"cache: 'pnpm'"
);
}
}
// pnpm/action-setup@v4を追加(ない場合のみ)
if (!content.includes("uses: pnpm/action-setup@")) {
// actions/checkoutを探す
const checkoutPattern = /(uses:\s+actions\/checkout@[^\s]+)/;
if (checkoutPattern.test(content)) {
content = content.replace(
checkoutPattern,
"$1\n\n - name: Install pnpm\n uses: pnpm/action-setup@v4"
);
}
}
// 変更を保存
await fs.writeFile(filePath, content, "utf-8");
console.log(` 📝 ${path.basename(filePath)}を更新しました`);
} catch (error) {
console.error(
` ❌ ${path.basename(filePath)}の更新に失敗しました: ${
error instanceof Error ? error.message : String(error)
}`
);
throw error;
}
};
/**
* .githooks/pre-commitファイルを更新します
* @param {string} cwd - 作業ディレクトリ
*/
const updateGitHooks = async (cwd: string) => {
try {
const preCommitPath = path.join(cwd, ".githooks", "pre-commit");
// .githooks/pre-commitファイルが存在するか確認
if (!await fileExists(preCommitPath)) {
console.log("⚠️ .githooks/pre-commitファイルが見つかりませんでした。Githooksの更新をスキップします。");
return;
}
// ファイルの内容を読み込み
const content = await fs.readFile(preCommitPath, "utf-8");
// #!/usr/bin/env nodeで始まるかチェック
if (content.trimStart().startsWith("#!/usr/bin/env node")) {
console.log("🔧 .githooks/pre-commitを更新しています...");
// 置換後の内容
const newContent = `#!/bin/sh
npx --no-install lint-staged
`;
// ファイルを書き込み
await fs.writeFile(preCommitPath, newContent, "utf-8");
// 実行権限を付与
await fs.chmod(preCommitPath, 0o755);
console.log("✅ .githooks/pre-commitを更新しました");
} else {
console.log("⚠️ .githooks/pre-commitは既に更新されているか、形式が異なります。スキップします。");
}
} catch (error) {
console.error(`❌ .githooks/pre-commitの更新に失敗しました: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
};
/**
* メイン処理
*/
const main = async () => {
try {
const { cwd } = parseArgs();
console.log(`🚀 '${cwd}'でpnpm移行プロセスを開始します`);
// 各タスクを順番に実行
await enablePnpmWithCorepack(cwd);
await updatePackageJson(cwd);
await convertLockfile(cwd);
await updateGitHubWorkflows(cwd);
await updateGitHooks(cwd);
console.log("🎉 pnpmへの移行が完了しました!");
} catch (error) {
console.error(`❌ pnpmへの移行に失敗しました: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
};
// スクリプトの実行
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment