From 2b9abf08c8f783615075d4970f4b3ff039871e11 Mon Sep 17 00:00:00 2001 From: asier Date: Tue, 16 Dec 2025 17:54:17 +0100 Subject: [PATCH 1/5] feat(migrate): add migration generators and utilities Includes detection logic, cleanup helpers, and Biome configuration inference based on existing ESLint/Prettier settings. --- package-lock.json | 9 + package.json | 2 + src/generators/cleanup.ts | 161 +++++++++ src/generators/migrate-eslint.ts | 36 ++ src/generators/migrate-prettier.ts | 36 ++ src/types/index.ts | 26 ++ src/utils/biome-config.ts | 344 +++++++++++++++++++ src/utils/detect-project.ts | 76 ++++ src/utils/git-safety.ts | 51 +++ src/utils/validate-migration.ts | 107 ++++++ tests/fixtures/eslint-project/.eslintrc.json | 25 ++ tests/fixtures/eslint-project/package.json | 13 + tests/fixtures/prettier-project/.prettierrc | 7 + tests/fixtures/prettier-project/package.json | 11 + tests/unit/cleanup.test.ts | 114 ++++++ tests/unit/detect-project.test.ts | 125 +++++++ 16 files changed, 1143 insertions(+) create mode 100644 src/generators/cleanup.ts create mode 100644 src/generators/migrate-eslint.ts create mode 100644 src/generators/migrate-prettier.ts create mode 100644 src/utils/biome-config.ts create mode 100644 src/utils/detect-project.ts create mode 100644 src/utils/git-safety.ts create mode 100644 src/utils/validate-migration.ts create mode 100644 tests/fixtures/eslint-project/.eslintrc.json create mode 100644 tests/fixtures/eslint-project/package.json create mode 100644 tests/fixtures/prettier-project/.prettierrc create mode 100644 tests/fixtures/prettier-project/package.json create mode 100644 tests/unit/cleanup.test.ts create mode 100644 tests/unit/detect-project.test.ts diff --git a/package-lock.json b/package-lock.json index caa3a79..9cd9d36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,9 @@ "@biomejs/biome": "^2.0.6", "@types/fs-extra": "^11.0.4", "@types/node": "^22.10.1", + "@types/semver": "^7.7.1", "@vitest/coverage-v8": "^4.0.15", + "semver": "^7.7.3", "tsup": "^8.3.5", "tsx": "^4.19.2", "typescript": "^5.7.2", @@ -1146,6 +1148,13 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, "node_modules/@vitest/coverage-v8": { "version": "4.0.15", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.15.tgz", diff --git a/package.json b/package.json index 2bf9c09..99aec31 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,9 @@ "@biomejs/biome": "^2.0.6", "@types/fs-extra": "^11.0.4", "@types/node": "^22.10.1", + "@types/semver": "^7.7.1", "@vitest/coverage-v8": "^4.0.15", + "semver": "^7.7.3", "tsup": "^8.3.5", "tsx": "^4.19.2", "typescript": "^5.7.2", diff --git a/src/generators/cleanup.ts b/src/generators/cleanup.ts new file mode 100644 index 0000000..29a1d26 --- /dev/null +++ b/src/generators/cleanup.ts @@ -0,0 +1,161 @@ +import path from "node:path"; +import { confirm, spinner } from "@clack/prompts"; +import { execa } from "execa"; +import fs from "fs-extra"; +import pc from "picocolors"; +import type { PackageManager, ProjectInfo } from "../types/index.js"; +import { getESLintConfigs, getPrettierConfigs } from "../utils/detect-project.js"; + +// Regex patterns to detect ESLint/Prettier related packages +const ESLINT_PACKAGE_PATTERNS = [ + /^eslint$/, + /^eslint-/, + /^@eslint\//, + /^@typescript-eslint\//, + /eslint-plugin-/, + /eslint-config-/, +]; + +const PRETTIER_PACKAGE_PATTERNS = [/^prettier$/, /^prettier-/, /^@prettier\//]; + +function matchesPatterns(packageName: string, patterns: RegExp[]): boolean { + return patterns.some((pattern) => pattern.test(packageName)); +} + +export function findDepsToRemove(pkg: Record): string[] { + const allDeps = [ + ...Object.keys((pkg["dependencies"] as Record) || {}), + ...Object.keys((pkg["devDependencies"] as Record) || {}), + ]; + + return allDeps.filter( + (dep) => + matchesPatterns(dep, ESLINT_PACKAGE_PATTERNS) || + matchesPatterns(dep, PRETTIER_PACKAGE_PATTERNS), + ); +} + +export async function uninstallDeps( + deps: string[], + pm: PackageManager, + cwd: string, +): Promise { + if (deps.length === 0) return; + + const uninstallCmd = pm === "npm" ? "uninstall" : "remove"; + await execa(pm, [uninstallCmd, ...deps], { cwd }); +} + +export async function removeConfigFiles(_info: ProjectInfo, cwd: string): Promise { + const filesToRemove: string[] = []; + + // Collect ESLint config files + for (const file of getESLintConfigs()) { + const filePath = path.join(cwd, file); + if (await fs.pathExists(filePath)) { + filesToRemove.push(file); + } + } + + // ESLint ignore file + if (await fs.pathExists(path.join(cwd, ".eslintignore"))) { + filesToRemove.push(".eslintignore"); + } + + // Collect Prettier config files + for (const file of getPrettierConfigs()) { + const filePath = path.join(cwd, file); + if (await fs.pathExists(filePath)) { + filesToRemove.push(file); + } + } + + // Prettier ignore file + if (await fs.pathExists(path.join(cwd, ".prettierignore"))) { + filesToRemove.push(".prettierignore"); + } + + // Remove files + for (const file of filesToRemove) { + await fs.remove(path.join(cwd, file)); + } + + return filesToRemove; +} + +export async function updatePackageScripts( + cwd: string, +): Promise<{ updated: boolean; scripts: Record }> { + const pkgPath = path.join(cwd, "package.json"); + const pkg = await fs.readJson(pkgPath); + + const newScripts: Record = { + lint: "biome check .", + "lint:fix": "biome check --write .", + format: "biome format --write .", + }; + + const shouldUpdate = await confirm({ + message: "Update package.json scripts to use Biome?", + initialValue: true, + }); + + if (shouldUpdate === true) { + pkg.scripts = { ...pkg.scripts, ...newScripts }; + await fs.writeJson(pkgPath, pkg, { spaces: 2 }); + return { updated: true, scripts: newScripts }; + } + + return { updated: false, scripts: newScripts }; +} + +export async function cleanupOldDeps( + info: ProjectInfo, + cwd: string = process.cwd(), + dryRun = false, +): Promise<{ depsRemoved: string[]; filesRemoved: string[] }> { + const s = spinner(); + s.start("Analyzing dependencies to remove..."); + + // Read package.json to find deps + const pkgPath = path.join(cwd, "package.json"); + const pkg = await fs.readJson(pkgPath); + const depsToRemove = findDepsToRemove(pkg); + + s.stop(`Found ${depsToRemove.length} ESLint/Prettier packages`); + + if (depsToRemove.length > 0) { + console.log(pc.dim(` Packages: ${depsToRemove.join(", ")}`)); + } + + if (dryRun) { + // In dry-run, just return what would be removed + const configFiles = [...getESLintConfigs(), ...getPrettierConfigs()].filter((f) => + fs.existsSync(path.join(cwd, f)), + ); + return { depsRemoved: depsToRemove, filesRemoved: configFiles }; + } + + // Confirm before removing + if (depsToRemove.length > 0) { + const shouldRemove = await confirm({ + message: `Remove ${depsToRemove.length} ESLint/Prettier packages?`, + initialValue: true, + }); + + if (shouldRemove === true) { + const s2 = spinner(); + s2.start("Removing packages..."); + await uninstallDeps(depsToRemove, info.packageManager, cwd); + s2.stop(pc.green(`✓ Removed ${depsToRemove.length} packages`)); + } + } + + // Remove config files + const filesRemoved = await removeConfigFiles(info, cwd); + if (filesRemoved.length > 0) { + console.log(pc.dim(` Removed config files: ${filesRemoved.join(", ")}`)); + } + + return { depsRemoved: depsToRemove, filesRemoved }; +} diff --git a/src/generators/migrate-eslint.ts b/src/generators/migrate-eslint.ts new file mode 100644 index 0000000..ceece10 --- /dev/null +++ b/src/generators/migrate-eslint.ts @@ -0,0 +1,36 @@ +import { spinner } from "@clack/prompts"; +import { execa } from "execa"; +import pc from "picocolors"; + +export async function migrateESLint( + cwd: string = process.cwd(), + dryRun = false, +): Promise<{ success: boolean; message: string }> { + const s = spinner(); + s.start("Migrating ESLint config to Biome..."); + + try { + const args = ["@biomejs/biome", "migrate", "eslint", "--include-inspired"]; + + if (!dryRun) { + args.push("--write"); + } + + const { stdout, stderr } = await execa("npx", args, { cwd }); + + s.stop(pc.green("✓ ESLint config migrated")); + + return { + success: true, + message: stdout || stderr || "ESLint configuration migrated successfully", + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + s.stop(pc.red("✗ Failed to migrate ESLint")); + + return { + success: false, + message: `ESLint migration failed: ${errorMessage}`, + }; + } +} diff --git a/src/generators/migrate-prettier.ts b/src/generators/migrate-prettier.ts new file mode 100644 index 0000000..0b3886a --- /dev/null +++ b/src/generators/migrate-prettier.ts @@ -0,0 +1,36 @@ +import { spinner } from "@clack/prompts"; +import { execa } from "execa"; +import pc from "picocolors"; + +export async function migratePrettier( + cwd: string = process.cwd(), + dryRun = false, +): Promise<{ success: boolean; message: string }> { + const s = spinner(); + s.start("Migrating Prettier config to Biome..."); + + try { + const args = ["@biomejs/biome", "migrate", "prettier"]; + + if (!dryRun) { + args.push("--write"); + } + + const { stdout, stderr } = await execa("npx", args, { cwd }); + + s.stop(pc.green("✓ Prettier config migrated")); + + return { + success: true, + message: stdout || stderr || "Prettier configuration migrated successfully", + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + s.stop(pc.red("✗ Failed to migrate Prettier")); + + return { + success: false, + message: `Prettier migration failed: ${errorMessage}`, + }; + } +} diff --git a/src/types/index.ts b/src/types/index.ts index ff34f19..eaee991 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -15,3 +15,29 @@ export interface ProjectContext { options: CreateOptions; packageManager: PackageManager; } + +// Migrate command types +export interface MigrateOptions { + skipInstall: boolean; + skipCleanup: boolean; + skipGit: boolean; + dryRun: boolean; +} + +export interface ProjectInfo { + hasESLint: boolean; + hasPrettier: boolean; + hasPackageJson: boolean; + hasBiome: boolean; + eslintConfig?: string; + prettierConfig?: string; + packageManager: PackageManager; +} + +export interface MigrationResult { + eslintMigrated: boolean; + prettierMigrated: boolean; + depsRemoved: string[]; + filesRemoved: string[]; + scriptsUpdated: boolean; +} diff --git a/src/utils/biome-config.ts b/src/utils/biome-config.ts new file mode 100644 index 0000000..6ad552f --- /dev/null +++ b/src/utils/biome-config.ts @@ -0,0 +1,344 @@ +import path from "node:path"; +import { confirm, select, spinner } from "@clack/prompts"; +import fs from "fs-extra"; +import pc from "picocolors"; + +interface PrettierConfig { + semi?: boolean; + singleQuote?: boolean; + trailingComma?: "none" | "es5" | "all"; + printWidth?: number; + tabWidth?: number; + useTabs?: boolean; + bracketSpacing?: boolean; + arrowParens?: "always" | "avoid"; + endOfLine?: "lf" | "crlf" | "cr" | "auto"; +} + +interface BiomeFormatterSettings { + quoteStyle: "single" | "double"; + trailingCommas: "none" | "all"; + lineWidth: number; + indentWidth: number; + indentStyle: "space" | "tab"; + semicolons: "always" | "asNeeded"; + bracketSpacing: boolean; + arrowParentheses: "always" | "asNeeded"; + lineEnding: "lf" | "crlf" | "cr"; +} + +const GENERATED_PATTERNS = [ + "!**/gen/**", + "!**/generated/**", + "!**/*.generated.*", + "!**/*.d.ts", + "!**/dist/**", + "!**/build/**", + "!**/node_modules/**", +]; + +/** + * Read and parse Prettier configuration from the project + */ +export async function readPrettierConfig(cwd: string): Promise { + const configFiles = [ + ".prettierrc", + ".prettierrc.json", + ".prettierrc.js", + ".prettierrc.cjs", + ".prettierrc.mjs", + "prettier.config.js", + "prettier.config.cjs", + "prettier.config.mjs", + ]; + + // Try JSON config files first + for (const file of configFiles) { + const configPath = path.join(cwd, file); + if (await fs.pathExists(configPath)) { + try { + if (file.endsWith(".json") || file === ".prettierrc") { + const content = await fs.readFile(configPath, "utf-8"); + return JSON.parse(content); + } + // For JS configs, we can't easily parse them, skip + } catch { + // Invalid config, continue + } + } + } + + // Check package.json for prettier config + const pkgPath = path.join(cwd, "package.json"); + if (await fs.pathExists(pkgPath)) { + try { + const pkg = await fs.readJson(pkgPath); + if (pkg.prettier && typeof pkg.prettier === "object") { + return pkg.prettier; + } + } catch { + // Invalid package.json + } + } + + return null; +} + +/** + * Read ESLint ignorePatterns from the project + * Note: This only works with JSON-based configs, not JS configs + */ +export async function readESLintIgnorePatterns(cwd: string): Promise { + const configFiles = [".eslintrc", ".eslintrc.json"]; + + // Try JSON config files + for (const file of configFiles) { + const configPath = path.join(cwd, file); + if (await fs.pathExists(configPath)) { + try { + const content = await fs.readFile(configPath, "utf-8"); + const config = JSON.parse(content); + if (Array.isArray(config.ignorePatterns)) { + return config.ignorePatterns; + } + } catch { + // Invalid config, continue + } + } + } + + // Check package.json for eslintConfig + const pkgPath = path.join(cwd, "package.json"); + if (await fs.pathExists(pkgPath)) { + try { + const pkg = await fs.readJson(pkgPath); + if (pkg.eslintConfig?.ignorePatterns && Array.isArray(pkg.eslintConfig.ignorePatterns)) { + return pkg.eslintConfig.ignorePatterns; + } + } catch { + // Invalid package.json + } + } + + // Try reading .eslintignore file + const ignorePath = path.join(cwd, ".eslintignore"); + if (await fs.pathExists(ignorePath)) { + try { + const content = await fs.readFile(ignorePath, "utf-8"); + return content + .split("\n") + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith("#")); + } catch { + // Invalid file + } + } + + return []; +} + +/** + * Convert Prettier config to Biome formatter settings + * Note: trailingCommas "es5" is not supported in Biome, caller must resolve it first + */ +export function mapPrettierToBiome( + prettier: PrettierConfig, + trailingCommasOverride?: "none" | "all", +): BiomeFormatterSettings { + // If es5, require override; otherwise map directly + let trailingCommas: "none" | "all" = "all"; + if (trailingCommasOverride) { + trailingCommas = trailingCommasOverride; + } else if (prettier.trailingComma === "none") { + trailingCommas = "none"; + } else if (prettier.trailingComma === "all") { + trailingCommas = "all"; + } + + return { + quoteStyle: prettier.singleQuote ? "single" : "double", + trailingCommas, + lineWidth: prettier.printWidth || 80, + indentWidth: prettier.tabWidth || 2, + indentStyle: prettier.useTabs ? "tab" : "space", + semicolons: prettier.semi === false ? "asNeeded" : "always", + bracketSpacing: prettier.bracketSpacing !== false, + arrowParentheses: prettier.arrowParens === "avoid" ? "asNeeded" : "always", + lineEnding: prettier.endOfLine === "crlf" ? "crlf" : prettier.endOfLine === "cr" ? "cr" : "lf", + }; +} + +/** + * Prompt user to choose trailing commas when Prettier has "es5" + */ +export async function promptTrailingCommasForEs5(): Promise<"none" | "all"> { + const result = await select({ + message: + 'Your Prettier uses "es5" trailing commas. Biome only supports "none" or "all". Choose:', + options: [ + { value: "none", label: "No trailing commas", hint: "Cleaner, no extra commas" }, + { value: "all", label: "All trailing commas", hint: "Better git diffs" }, + ], + initialValue: "none", + }); + + return result as "none" | "all"; +} + +/** + * Detect generated folders in the project + */ +export async function detectGeneratedFolders(cwd: string): Promise { + const detected: string[] = []; + const foldersToCheck = [ + "src/api/gen", + "src/generated", + "generated", + "gen", + "src/types/generated", + ]; + + for (const folder of foldersToCheck) { + if (await fs.pathExists(path.join(cwd, folder))) { + detected.push(`!**/${folder}/**`); + } + } + + return detected; +} + +/** + * Apply Prettier-compatible settings to Biome config + */ +export async function applyPrettierCompatibility( + cwd: string, + prettierConfig: PrettierConfig | null, + relaxedRules: boolean, +): Promise { + const s = spinner(); + s.start("Customizing Biome configuration..."); + + const biomeConfigPath = path.join(cwd, "biome.json"); + + if (!(await fs.pathExists(biomeConfigPath))) { + s.stop(pc.yellow("⚠ biome.json not found, skipping customization")); + return; + } + + const config = await fs.readJson(biomeConfigPath); + + // Collect all ignore patterns from various sources + const generatedFolders = await detectGeneratedFolders(cwd); + const eslintIgnorePatterns = await readESLintIgnorePatterns(cwd); + + // Convert ESLint ignore patterns to Biome negated includes + const eslintExcludes = eslintIgnorePatterns + .filter((p) => p && !p.startsWith("!")) // Skip negations + .map((p) => { + // Normalize pattern for Biome + const normalized = p.replace(/\/$/, ""); // Remove trailing slash + return normalized.startsWith("!") ? normalized : `!${normalized}`; + }); + + // Combine all exclusion patterns + const allExcludes = [...new Set([...generatedFolders, ...eslintExcludes, ...GENERATED_PATTERNS])]; + + if (allExcludes.length > 0) { + config.files = config.files || {}; + const existingIncludes = config.files.includes || []; + const hasWildcard = existingIncludes.some((p: string) => p === "**" || p === "**/*"); + const basePatterns = hasWildcard ? existingIncludes : ["**", ...existingIncludes]; + config.files.includes = [...new Set([...basePatterns, ...allExcludes])]; + } + + // Apply Prettier-compatible formatting settings + if (prettierConfig) { + // Handle es5 trailing commas - Biome doesn't support it + let trailingCommasOverride: "none" | "all" | undefined; + if (prettierConfig.trailingComma === "es5") { + s.stop(pc.yellow("⚠ Biome doesn't support 'es5' trailing commas")); + trailingCommasOverride = await promptTrailingCommasForEs5(); + s.start("Continuing configuration..."); + } + + const biomeSettings = mapPrettierToBiome(prettierConfig, trailingCommasOverride); + + config.formatter = config.formatter || {}; + config.formatter.indentStyle = biomeSettings.indentStyle; + config.formatter.indentWidth = biomeSettings.indentWidth; + config.formatter.lineWidth = biomeSettings.lineWidth; + config.formatter.lineEnding = biomeSettings.lineEnding; + config.formatter.bracketSpacing = biomeSettings.bracketSpacing; + + config.javascript = config.javascript || {}; + config.javascript.formatter = config.javascript.formatter || {}; + config.javascript.formatter.quoteStyle = biomeSettings.quoteStyle; + config.javascript.formatter.trailingCommas = biomeSettings.trailingCommas; + config.javascript.formatter.semicolons = biomeSettings.semicolons; + config.javascript.formatter.arrowParentheses = biomeSettings.arrowParentheses; + } + + // Apply relaxed linting rules if requested + if (relaxedRules) { + config.linter = config.linter || {}; + config.linter.rules = config.linter.rules || {}; + + // Relax suspicious rules + config.linter.rules.suspicious = config.linter.rules.suspicious || {}; + config.linter.rules.suspicious.noExplicitAny = "off"; + config.linter.rules.suspicious.noConsole = "off"; + + // Relax style rules + config.linter.rules.style = config.linter.rules.style || {}; + config.linter.rules.style.noNonNullAssertion = "off"; + + // Relax a11y rules (can be very strict) + config.linter.rules.a11y = config.linter.rules.a11y || {}; + config.linter.rules.a11y.noRedundantRoles = "off"; + config.linter.rules.a11y.useSemanticElements = "off"; + config.linter.rules.a11y.useAriaPropsSupportedByRole = "off"; + + // Relax correctness rules that may be too strict + config.linter.rules.correctness = config.linter.rules.correctness || {}; + config.linter.rules.correctness.useExhaustiveDependencies = "warn"; + } + + // Add Jest/Vitest globals for test files + config.javascript = config.javascript || {}; + config.javascript.globals = [ + // Jest globals + "jest", + "describe", + "it", + "test", + "expect", + "beforeEach", + "afterEach", + "beforeAll", + "afterAll", + // Vitest globals + "vi", + ]; + + // Disable organize imports assistant (can be disruptive during migration) + config.assist = config.assist || {}; + config.assist.actions = config.assist.actions || {}; + config.assist.actions.source = config.assist.actions.source || {}; + config.assist.actions.source.organizeImports = "off"; + + await fs.writeJson(biomeConfigPath, config, { spaces: 2 }); + + s.stop(pc.green("✓ Biome configuration customized")); +} + +/** + * Prompt for relaxed rules only (the one thing Prettier doesn't configure) + */ +export async function promptRelaxedRules(): Promise { + const result = await confirm({ + message: "Use relaxed lint rules? (disable noExplicitAny, noConsole, strict a11y)", + initialValue: true, + }); + + return result === true; +} diff --git a/src/utils/detect-project.ts b/src/utils/detect-project.ts new file mode 100644 index 0000000..e981473 --- /dev/null +++ b/src/utils/detect-project.ts @@ -0,0 +1,76 @@ +import path from "node:path"; +import fs from "fs-extra"; +import type { ProjectInfo } from "../types/index.js"; +import { detectPackageManager } from "./detect-pm.js"; + +const ESLINT_CONFIGS = [ + ".eslintrc.js", + ".eslintrc.cjs", + ".eslintrc.json", + ".eslintrc.yaml", + ".eslintrc.yml", + ".eslintrc", + "eslint.config.js", + "eslint.config.mjs", + "eslint.config.cjs", +]; + +const PRETTIER_CONFIGS = [ + ".prettierrc", + ".prettierrc.json", + ".prettierrc.yaml", + ".prettierrc.yml", + ".prettierrc.js", + ".prettierrc.cjs", + ".prettierrc.mjs", + "prettier.config.js", + "prettier.config.cjs", + "prettier.config.mjs", +]; + +export async function detectProject(cwd: string = process.cwd()): Promise { + // Detect ESLint config + const eslintConfig = ESLINT_CONFIGS.find((file) => fs.existsSync(path.join(cwd, file))); + + // Check for ESLint config in package.json + let hasESLintInPkg = false; + const pkgPath = path.join(cwd, "package.json"); + if (await fs.pathExists(pkgPath)) { + const pkg = await fs.readJson(pkgPath); + hasESLintInPkg = !!pkg.eslintConfig; + } + + // Detect Prettier config + const prettierConfig = PRETTIER_CONFIGS.find((file) => fs.existsSync(path.join(cwd, file))); + + // Check for Prettier config in package.json + let hasPrettierInPkg = false; + if (await fs.pathExists(pkgPath)) { + const pkg = await fs.readJson(pkgPath); + hasPrettierInPkg = !!pkg.prettier; + } + + // Detect existing Biome config + const hasBiome = fs.existsSync(path.join(cwd, "biome.json")); + + // Detect package.json + const hasPackageJson = await fs.pathExists(pkgPath); + + return { + hasESLint: !!eslintConfig || hasESLintInPkg, + hasPrettier: !!prettierConfig || hasPrettierInPkg, + hasPackageJson, + hasBiome, + eslintConfig, + prettierConfig, + packageManager: detectPackageManager(cwd), + }; +} + +export function getESLintConfigs(): string[] { + return ESLINT_CONFIGS; +} + +export function getPrettierConfigs(): string[] { + return PRETTIER_CONFIGS; +} diff --git a/src/utils/git-safety.ts b/src/utils/git-safety.ts new file mode 100644 index 0000000..702998f --- /dev/null +++ b/src/utils/git-safety.ts @@ -0,0 +1,51 @@ +import { confirm } from "@clack/prompts"; +import { execa } from "execa"; + +export async function isGitRepo(cwd: string = process.cwd()): Promise { + try { + await execa("git", ["rev-parse", "--is-inside-work-tree"], { cwd }); + return true; + } catch { + return false; + } +} + +export async function hasUncommittedChanges(cwd: string = process.cwd()): Promise { + try { + const { stdout } = await execa("git", ["status", "--porcelain"], { cwd }); + return stdout.trim().length > 0; + } catch { + return false; + } +} + +export async function createSafetyCommit(cwd: string = process.cwd()): Promise { + // Check if it's a git repo + if (!(await isGitRepo(cwd))) { + return false; + } + + // Check for uncommitted changes + if (!(await hasUncommittedChanges(cwd))) { + return false; + } + + const shouldCommit = await confirm({ + message: "You have uncommitted changes. Create safety commit before migrating?", + initialValue: true, + }); + + if (shouldCommit === true) { + try { + await execa("git", ["add", "."], { cwd }); + await execa("git", ["commit", "-m", "chore: backup before Biome migration", "--no-verify"], { + cwd, + }); + return true; + } catch { + return false; + } + } + + return false; +} diff --git a/src/utils/validate-migration.ts b/src/utils/validate-migration.ts new file mode 100644 index 0000000..908353a --- /dev/null +++ b/src/utils/validate-migration.ts @@ -0,0 +1,107 @@ +import path from "node:path"; +import { note, spinner } from "@clack/prompts"; +import { execa } from "execa"; +import fs from "fs-extra"; +import pc from "picocolors"; +import semver from "semver"; + +const MINIMUM_BIOME_VERSION = "1.7.0"; + +export async function getBiomeVersion(cwd: string = process.cwd()): Promise { + try { + const { stdout } = await execa("npx", ["@biomejs/biome", "--version"], { cwd }); + // Output is like "Version: 1.9.0" or just "1.9.0" + const match = stdout.match(/(\d+\.\d+\.\d+)/); + return match?.[1] ?? null; + } catch { + return null; + } +} + +export async function validateBiomeVersion(cwd: string = process.cwd()): Promise<{ + valid: boolean; + version: string | null; + message: string; +}> { + const version = await getBiomeVersion(cwd); + + if (!version) { + return { + valid: false, + version: null, + message: "Biome is not installed or version could not be determined", + }; + } + + if (!semver.gte(version, MINIMUM_BIOME_VERSION)) { + return { + valid: false, + version, + message: `Biome version ${version} is too old. Minimum required: ${MINIMUM_BIOME_VERSION}`, + }; + } + + return { + valid: true, + version, + message: `Biome ${version} detected`, + }; +} + +export async function validateMigration(cwd: string = process.cwd()): Promise<{ + success: boolean; + issues: string[]; +}> { + const s = spinner(); + s.start("Validating migration..."); + + const issues: string[] = []; + + // 1. Verify that biome.json exists + const biomeConfigPath = path.join(cwd, "biome.json"); + if (!(await fs.pathExists(biomeConfigPath))) { + issues.push("biome.json not found"); + } + + // 2. Verify that @biomejs/biome is installed + const pkgPath = path.join(cwd, "package.json"); + if (await fs.pathExists(pkgPath)) { + const pkg = await fs.readJson(pkgPath); + const hasBiomeDep = + pkg.devDependencies?.["@biomejs/biome"] || pkg.dependencies?.["@biomejs/biome"]; + + if (!hasBiomeDep) { + issues.push("@biomejs/biome not found in dependencies"); + } + } + + // 3. Check if scripts are updated + if (await fs.pathExists(pkgPath)) { + const pkg = await fs.readJson(pkgPath); + const lintScript = pkg.scripts?.lint; + + if (!lintScript?.includes("biome")) { + issues.push('package.json "lint" script does not use Biome'); + } + } + + // 4. Run biome check to verify it works + try { + await execa("npx", ["@biomejs/biome", "check", ".", "--max-diagnostics=0"], { cwd }); + } catch { + // Biome check might have lint errors, but that's OK + // We only care that it runs + } + + if (issues.length > 0) { + s.stop(pc.yellow("⚠ Migration completed with warnings")); + note(issues.map((i) => `⚠ ${i}`).join("\n"), "Issues found"); + } else { + s.stop(pc.green("✓ Migration validated successfully")); + } + + return { + success: issues.length === 0, + issues, + }; +} diff --git a/tests/fixtures/eslint-project/.eslintrc.json b/tests/fixtures/eslint-project/.eslintrc.json new file mode 100644 index 0000000..63de70d --- /dev/null +++ b/tests/fixtures/eslint-project/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint", + "react" + ], + "rules": { + "no-unused-vars": "warn", + "no-console": "off", + "@typescript-eslint/no-explicit-any": "error" + } +} \ No newline at end of file diff --git a/tests/fixtures/eslint-project/package.json b/tests/fixtures/eslint-project/package.json new file mode 100644 index 0000000..b9ba215 --- /dev/null +++ b/tests/fixtures/eslint-project/package.json @@ -0,0 +1,13 @@ +{ + "name": "eslint-fixture-project", + "version": "1.0.0", + "scripts": { + "lint": "eslint ." + }, + "devDependencies": { + "eslint": "^8.0.0", + "@typescript-eslint/parser": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "eslint-plugin-react": "^7.0.0" + } +} \ No newline at end of file diff --git a/tests/fixtures/prettier-project/.prettierrc b/tests/fixtures/prettier-project/.prettierrc new file mode 100644 index 0000000..1f4c4bb --- /dev/null +++ b/tests/fixtures/prettier-project/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 100 +} diff --git a/tests/fixtures/prettier-project/package.json b/tests/fixtures/prettier-project/package.json new file mode 100644 index 0000000..0fa7f95 --- /dev/null +++ b/tests/fixtures/prettier-project/package.json @@ -0,0 +1,11 @@ +{ + "name": "prettier-fixture-project", + "version": "1.0.0", + "scripts": { + "format": "prettier --write ." + }, + "devDependencies": { + "prettier": "^3.0.0", + "prettier-plugin-tailwindcss": "^0.5.0" + } +} \ No newline at end of file diff --git a/tests/unit/cleanup.test.ts b/tests/unit/cleanup.test.ts new file mode 100644 index 0000000..237ec32 --- /dev/null +++ b/tests/unit/cleanup.test.ts @@ -0,0 +1,114 @@ +import { describe, expect, it } from "vitest"; +import { findDepsToRemove } from "../../src/generators/cleanup.js"; + +describe("findDepsToRemove", () => { + it("finds eslint package", () => { + const pkg = { + devDependencies: { + eslint: "^8.0.0", + typescript: "^5.0.0", + }, + }; + + const result = findDepsToRemove(pkg); + + expect(result).toContain("eslint"); + expect(result).not.toContain("typescript"); + }); + + it("finds prettier package", () => { + const pkg = { + devDependencies: { + prettier: "^3.0.0", + typescript: "^5.0.0", + }, + }; + + const result = findDepsToRemove(pkg); + + expect(result).toContain("prettier"); + }); + + it("finds @typescript-eslint packages", () => { + const pkg = { + devDependencies: { + "@typescript-eslint/parser": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + }, + }; + + const result = findDepsToRemove(pkg); + + expect(result).toContain("@typescript-eslint/parser"); + expect(result).toContain("@typescript-eslint/eslint-plugin"); + }); + + it("finds eslint-plugin-* packages", () => { + const pkg = { + devDependencies: { + "eslint-plugin-react": "^7.0.0", + "eslint-plugin-jsx-a11y": "^6.0.0", + }, + }; + + const result = findDepsToRemove(pkg); + + expect(result).toContain("eslint-plugin-react"); + expect(result).toContain("eslint-plugin-jsx-a11y"); + }); + + it("finds eslint-config-* packages", () => { + const pkg = { + devDependencies: { + "eslint-config-prettier": "^9.0.0", + "eslint-config-airbnb": "^19.0.0", + }, + }; + + const result = findDepsToRemove(pkg); + + expect(result).toContain("eslint-config-prettier"); + expect(result).toContain("eslint-config-airbnb"); + }); + + it("finds prettier-* packages", () => { + const pkg = { + devDependencies: { + "prettier-plugin-tailwindcss": "^0.5.0", + }, + }; + + const result = findDepsToRemove(pkg); + + expect(result).toContain("prettier-plugin-tailwindcss"); + }); + + it("returns empty array when no matching deps", () => { + const pkg = { + devDependencies: { + typescript: "^5.0.0", + vite: "^5.0.0", + }, + }; + + const result = findDepsToRemove(pkg); + + expect(result).toHaveLength(0); + }); + + it("checks both dependencies and devDependencies", () => { + const pkg = { + dependencies: { + eslint: "^8.0.0", + }, + devDependencies: { + prettier: "^3.0.0", + }, + }; + + const result = findDepsToRemove(pkg); + + expect(result).toContain("eslint"); + expect(result).toContain("prettier"); + }); +}); diff --git a/tests/unit/detect-project.test.ts b/tests/unit/detect-project.test.ts new file mode 100644 index 0000000..6d00c79 --- /dev/null +++ b/tests/unit/detect-project.test.ts @@ -0,0 +1,125 @@ +import os from "node:os"; +import path from "node:path"; +import fs from "fs-extra"; +import { beforeEach, describe, expect, it } from "vitest"; +import { + detectProject, + getESLintConfigs, + getPrettierConfigs, +} from "../../src/utils/detect-project.js"; + +describe("detectProject", () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "zsb-detect-")); + // Create a basic package.json + await fs.writeJson(path.join(tempDir, "package.json"), { + name: "test-project", + version: "1.0.0", + }); + }); + + it("detects ESLint .eslintrc.json config", async () => { + await fs.writeJson(path.join(tempDir, ".eslintrc.json"), { rules: {} }); + + const result = await detectProject(tempDir); + + expect(result.hasESLint).toBe(true); + expect(result.eslintConfig).toBe(".eslintrc.json"); + }); + + it("detects ESLint eslint.config.js (flat config)", async () => { + await fs.writeFile(path.join(tempDir, "eslint.config.js"), "export default []"); + + const result = await detectProject(tempDir); + + expect(result.hasESLint).toBe(true); + expect(result.eslintConfig).toBe("eslint.config.js"); + }); + + it("detects ESLint config in package.json", async () => { + await fs.writeJson(path.join(tempDir, "package.json"), { + name: "test-project", + eslintConfig: { rules: {} }, + }); + + const result = await detectProject(tempDir); + + expect(result.hasESLint).toBe(true); + }); + + it("detects Prettier .prettierrc config", async () => { + await fs.writeJson(path.join(tempDir, ".prettierrc"), { semi: true }); + + const result = await detectProject(tempDir); + + expect(result.hasPrettier).toBe(true); + expect(result.prettierConfig).toBe(".prettierrc"); + }); + + it("detects Prettier config in package.json", async () => { + await fs.writeJson(path.join(tempDir, "package.json"), { + name: "test-project", + prettier: { semi: true }, + }); + + const result = await detectProject(tempDir); + + expect(result.hasPrettier).toBe(true); + }); + + it("detects existing Biome config", async () => { + await fs.writeJson(path.join(tempDir, "biome.json"), { linter: { enabled: true } }); + + const result = await detectProject(tempDir); + + expect(result.hasBiome).toBe(true); + }); + + it("returns false when no configs exist", async () => { + const result = await detectProject(tempDir); + + expect(result.hasESLint).toBe(false); + expect(result.hasPrettier).toBe(false); + expect(result.hasBiome).toBe(false); + }); + + it("detects package manager from lockfile when no user agent", async () => { + // Clear user agent to force lockfile detection + const originalUserAgent = process.env["npm_config_user_agent"]; + delete process.env["npm_config_user_agent"]; + + await fs.writeFile(path.join(tempDir, "pnpm-lock.yaml"), ""); + + const result = await detectProject(tempDir); + + // Restore user agent + if (originalUserAgent) { + process.env["npm_config_user_agent"] = originalUserAgent; + } + + expect(result.packageManager).toBe("pnpm"); + }); +}); + +describe("getESLintConfigs", () => { + it("returns all supported ESLint config filenames", () => { + const configs = getESLintConfigs(); + + expect(configs).toContain(".eslintrc.js"); + expect(configs).toContain(".eslintrc.json"); + expect(configs).toContain("eslint.config.js"); + expect(configs).toContain("eslint.config.mjs"); + }); +}); + +describe("getPrettierConfigs", () => { + it("returns all supported Prettier config filenames", () => { + const configs = getPrettierConfigs(); + + expect(configs).toContain(".prettierrc"); + expect(configs).toContain(".prettierrc.json"); + expect(configs).toContain("prettier.config.js"); + }); +}); From cd7c3620947d09934dfc58dbf6b0bfbcee98ac44 Mon Sep 17 00:00:00 2001 From: asier Date: Tue, 16 Dec 2025 17:54:43 +0100 Subject: [PATCH 2/5] feat(migrate): register migrate command Adds the migrate command to the CLI, orchestrating the migration process including detection, installation, config migration, and cleanup. --- .github/workflows/ci.yml | 2 +- src/cli.ts | 17 +++ src/commands/migrate.ts | 233 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 src/commands/migrate.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 342f276..eeeb202 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18, 20, 22] + node-version: [22] steps: - uses: actions/checkout@v4 diff --git a/src/cli.ts b/src/cli.ts index b3d38dd..ecf1fc3 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,5 +1,6 @@ import { Command } from "commander"; import { runCreate } from "./commands/create.js"; +import { runMigrate } from "./commands/migrate.js"; import type { PackageManager, Template } from "./types/index.js"; import { TEMPLATES, VERSION } from "./utils/constants.js"; @@ -34,4 +35,20 @@ program }); }); +program + .command("migrate") + .description("Migrate existing project from ESLint/Prettier to Biome") + .option("--skip-install", "Skip installing Biome", false) + .option("--skip-cleanup", "Keep ESLint/Prettier configs and dependencies", false) + .option("--skip-git", "Skip creating safety git commit", false) + .option("--dry-run", "Show changes without applying them", false) + .action(async (opts) => { + await runMigrate({ + skipInstall: opts.skipInstall, + skipCleanup: opts.skipCleanup, + skipGit: opts.skipGit, + dryRun: opts.dryRun, + }); + }); + program.parse(); diff --git a/src/commands/migrate.ts b/src/commands/migrate.ts new file mode 100644 index 0000000..668819a --- /dev/null +++ b/src/commands/migrate.ts @@ -0,0 +1,233 @@ +import { cancel, confirm, intro, isCancel, note, outro, spinner } from "@clack/prompts"; +import { execa } from "execa"; +import pc from "picocolors"; +import { cleanupOldDeps, updatePackageScripts } from "../generators/cleanup.js"; +import { migrateESLint } from "../generators/migrate-eslint.js"; +import { migratePrettier } from "../generators/migrate-prettier.js"; +import type { MigrateOptions, MigrationResult } from "../types/index.js"; +import { + applyPrettierCompatibility, + promptRelaxedRules, + readPrettierConfig, +} from "../utils/biome-config.js"; +import { detectProject } from "../utils/detect-project.js"; +import { createSafetyCommit } from "../utils/git-safety.js"; +import { validateBiomeVersion, validateMigration } from "../utils/validate-migration.js"; + +export async function runMigrate(options: MigrateOptions): Promise { + const cwd = process.cwd(); + + intro(pc.bgMagenta(pc.black(" Migrate to Biome "))); + + // 1. Detect project setup + const s = spinner(); + s.start("Detecting project setup..."); + const projectInfo = await detectProject(cwd); + s.stop("Project analyzed"); + + // Check if there's something to migrate + if (!projectInfo.hasESLint && !projectInfo.hasPrettier) { + outro(pc.yellow("No ESLint or Prettier configuration detected. Nothing to migrate.")); + return; + } + + // Show what was found + const detectedItems: string[] = []; + if (projectInfo.hasESLint) { + detectedItems.push( + `✓ ESLint${projectInfo.eslintConfig ? ` (${projectInfo.eslintConfig})` : ""}`, + ); + } + if (projectInfo.hasPrettier) { + detectedItems.push( + `✓ Prettier${projectInfo.prettierConfig ? ` (${projectInfo.prettierConfig})` : ""}`, + ); + } + if (projectInfo.hasBiome) { + detectedItems.push("✓ Biome (already configured)"); + } + detectedItems.push(`✓ Package manager: ${projectInfo.packageManager}`); + + note(detectedItems.join("\n"), "Detected setup"); + + // 2. Dry run mode + if (options.dryRun) { + note( + [ + projectInfo.hasESLint ? "• Migrate ESLint rules to biome.json" : null, + projectInfo.hasPrettier ? "• Migrate Prettier config to biome.json" : null, + !options.skipInstall ? "• Install @biomejs/biome" : null, + !options.skipCleanup ? "• Remove ESLint/Prettier dependencies" : null, + !options.skipCleanup ? "• Remove old config files" : null, + "• Update package.json scripts", + ] + .filter(Boolean) + .join("\n"), + "Dry run - Changes that would be made", + ); + outro(pc.dim("Run without --dry-run to apply changes")); + return; + } + + // 3. Confirm migration + const shouldContinue = await confirm({ + message: "Ready to migrate to Biome?", + initialValue: true, + }); + + if (isCancel(shouldContinue) || !shouldContinue) { + cancel("Migration cancelled"); + return; + } + + // 4. Create safety commit + if (!options.skipGit) { + const committed = await createSafetyCommit(cwd); + if (committed) { + console.log(pc.dim(" Created safety commit")); + } + } + + // Track results + const result: MigrationResult = { + eslintMigrated: false, + prettierMigrated: false, + depsRemoved: [], + filesRemoved: [], + scriptsUpdated: false, + }; + + // 5. Install Biome if not already installed + if (!options.skipInstall && !projectInfo.hasBiome) { + const s2 = spinner(); + s2.start("Installing @biomejs/biome..."); + try { + const installCmd = projectInfo.packageManager === "npm" ? "install" : "add"; + await execa(projectInfo.packageManager, [installCmd, "-D", "@biomejs/biome"], { cwd }); + s2.stop(pc.green("✓ Installed @biomejs/biome")); + } catch (error) { + s2.stop(pc.red("✗ Failed to install Biome")); + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + cancel(`Failed to install Biome: ${errorMessage}`); + return; + } + } + + // Validate Biome version + const versionCheck = await validateBiomeVersion(cwd); + if (!versionCheck.valid) { + cancel(versionCheck.message); + return; + } + console.log(pc.dim(` ${versionCheck.message}`)); + + // 6. Initialize biome.json if it doesn't exist (required for migration) + if (!projectInfo.hasBiome) { + const s3 = spinner(); + s3.start("Initializing Biome configuration..."); + try { + await execa("npx", ["@biomejs/biome", "init"], { cwd }); + s3.stop(pc.green("✓ Created biome.json")); + } catch (error) { + s3.stop(pc.red("✗ Failed to initialize Biome")); + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + cancel(`Failed to initialize Biome: ${errorMessage}`); + return; + } + } + + // 7. Migrate ESLint + if (projectInfo.hasESLint) { + const eslintResult = await migrateESLint(cwd); + result.eslintMigrated = eslintResult.success; + if (!eslintResult.success) { + note(eslintResult.message, "ESLint migration warning"); + } + } + + // 8. Migrate Prettier + if (projectInfo.hasPrettier) { + const prettierResult = await migratePrettier(cwd); + result.prettierMigrated = prettierResult.success; + if (!prettierResult.success) { + note(prettierResult.message, "Prettier migration warning"); + } + } + + // 9. Read Prettier config BEFORE cleanup (so we can preserve settings) + const prettierConfig = await readPrettierConfig(cwd); + if (prettierConfig) { + console.log(pc.dim(" Read Prettier settings to preserve formatting preferences")); + } + + // 10. Ask about relaxed lint rules (only question needed) + const relaxedRules = await promptRelaxedRules(); + + // 11. Apply Prettier-compatible settings to Biome config + await applyPrettierCompatibility(cwd, prettierConfig, relaxedRules); + + // 12. Validate migration before cleanup + const _validation = await validateMigration(cwd); + + // 13. Cleanup old dependencies (only if migration was successful) + if (!options.skipCleanup) { + const cleanup = await cleanupOldDeps(projectInfo, cwd); + result.depsRemoved = cleanup.depsRemoved; + result.filesRemoved = cleanup.filesRemoved; + } + + // 14. Update scripts + const scriptsResult = await updatePackageScripts(cwd); + result.scriptsUpdated = scriptsResult.updated; + + if (!scriptsResult.updated) { + note( + Object.entries(scriptsResult.scripts) + .map(([key, val]) => `"${key}": "${val}"`) + .join("\n"), + "Add these scripts to package.json", + ); + } + + // 15. Ask if user wants to apply Biome formatting now + const applyFormatting = await confirm({ + message: + "Biome may format code differently than Prettier.\n" + + "Apply Biome formatting now? (recommended, creates one reformatting commit)", + initialValue: true, + }); + + if (applyFormatting === true) { + const formatSpinner = spinner(); + formatSpinner.start("Applying Biome formatting to all files..."); + try { + await execa("npx", ["@biomejs/biome", "check", "--write", "."], { cwd }); + formatSpinner.stop(pc.green("✓ Code formatted with Biome")); + } catch { + // biome check --write may exit with non-zero if there are unfixable issues + formatSpinner.stop(pc.yellow("⚠ Formatting applied (some issues may remain)")); + } + } + + // 16. Show summary + const summary: string[] = []; + if (result.eslintMigrated) summary.push("✓ ESLint rules migrated"); + if (result.prettierMigrated) summary.push("✓ Prettier config migrated"); + if (result.depsRemoved.length > 0) + summary.push(`✓ Removed ${result.depsRemoved.length} packages`); + if (result.filesRemoved.length > 0) + summary.push(`✓ Removed ${result.filesRemoved.length} config files`); + if (result.scriptsUpdated) summary.push("✓ Updated package.json scripts"); + if (applyFormatting === true) summary.push("✓ Code reformatted with Biome"); + + if (summary.length > 0) { + note(summary.join("\n"), "Migration summary"); + } + + outro( + pc.green("✓ Migration complete!\n\n") + + pc.dim("Run ") + + pc.cyan("npm run lint") + + pc.dim(" to test Biome (22x faster than ESLint)"), + ); +} From 1de9eeabd19a6a59185fca5df2952d5a4b7fa5f9 Mon Sep 17 00:00:00 2001 From: asier Date: Tue, 16 Dec 2025 17:56:01 +0100 Subject: [PATCH 3/5] docs: update migrate command documentation Updates README and CHANGELOG with details about the new migrate command features and usage. --- CHANGELOG.md | 18 +++++++++++++++++- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 473c541..e4a953d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [0.1.0] - 2024-XX-XX +### Added + +- **`migrate` command** — Migrate existing projects from ESLint/Prettier to Biome + - Automatic ESLint config migration (uses Biome's official `biome migrate eslint`) + - Automatic Prettier config migration (`biome migrate prettier`) + - **Preserves Prettier settings** (quotes, semicolons, trailing commas, line width) + - **Auto-detects and excludes generated folders** (`gen/`, `dist/`, `build/`) + - **Reads ESLint ignorePatterns** and applies them to Biome + - **Jest/Vitest globals** automatically configured + - **Optional auto-format** to apply Biome formatting in one commit + - Smart dependency cleanup (detects all ESLint/Prettier plugins with regex) + - Git safety commit before migration + - Interactive prompts for relaxed rules, script updates, and cleanup + - `--dry-run` mode for previewing changes + - Project detection for ESLint, Prettier, and existing Biome configs + - Biome version validation (requires ≥1.7) + ### Added diff --git a/README.md b/README.md index f24a3e2..1b70eb7 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,21 @@ ## Quick Start +### Create a New Project + ```bash npx zero-setup-biome my-app cd my-app npm run dev ``` +### Migrate Existing Project + +```bash +cd your-existing-project +npx zero-setup-biome migrate +``` + That's it. No ESLint/Prettier conflicts. No configuration hell. Just code. ## What You Get @@ -20,20 +29,48 @@ That's it. No ESLint/Prettier conflicts. No configuration hell. Just code. - 🎨 **VSCode Integration** — Format on save, auto-organize imports - 🔒 **Zero Conflicts** — No ESLint vs Prettier wars -## Options +## Commands + +### Create ```bash npx zero-setup-biome [options] Options: -t, --template