diff --git a/packages/codemod/README.md b/packages/codemod/README.md index 6816c9e..d34257d 100644 --- a/packages/codemod/README.md +++ b/packages/codemod/README.md @@ -1,15 +1,19 @@
-The CLI provides a comprehensive suite of tools to migrate your codebase from NextUI to heroui. + + +The CLI provides a comprehensive suite of tools to migrate your codebase from NextUI to HeroUI. ## Quick Start > **Note**: The heroui CLI requires [Node.js](https://nodejs.org/en) _18.17.x_ or later +> +> **Note**: If running in monorepo, you need to run the command in the root of your monorepo You can start using @heroui/codemod in one of the following ways: @@ -40,9 +44,10 @@ Options: -v, --version Output the current version -d, --debug Enable debug mode -h, --help Display help for command + -f, --format Format the affected files with Prettier Commands: - migrate [projectPath] Migrate your codebase to use heroui + migrate [projectPath] Migrate your codebase to use heroui ``` ## Codemod Arguments @@ -132,7 +137,7 @@ Example: Migrate your entire codebase from NextUI to heroui. You can choose which codemods to run during the migration process. ```bash -heroui-codemod migrate [projectPath] +heroui-codemod migrate [projectPath] [--format] ``` Example: diff --git a/packages/codemod/package.json b/packages/codemod/package.json index 5a50538..3fb8e15 100644 --- a/packages/codemod/package.json +++ b/packages/codemod/package.json @@ -3,9 +3,9 @@ "private": false, "type": "module", "license": "MIT", - "version": "0.0.2", + "version": "1.1.0", "homepage": "https://github.com/frontio-ai/heroui-cli#readme", - "description": "A CLI tool that modifies your codebase to use the heroui", + "description": "HeroUI Codemod provides transformations to help migrate your codebase from NextUI to HeroUI", "keywords": [ "UI", "CLI", @@ -50,16 +50,18 @@ "lint:fix": "eslint . --max-warnings=0 --fix", "check:prettier": "prettier --check .", "check:types": "tsc --noEmit", - "changelog": "npx conventional-changelog -p angular -i CHANGELOG.md -s --commit-path packages/codemod", - "release": "bumpp --execute='pnpm run changelog' --all --tag='@heroui/codemodv%s'", + "changelog": "npx conventional-changelog -p angular -i CHANGELOG.md -s --commit-path .", + "release": "bumpp --execute='pnpm run changelog' --all --tag '@heroui/codemodv%s'", "prepublishOnly": "pnpm run build" }, "dependencies": { "@clack/prompts": "0.7.0", "async-retry": "1.3.3", "chalk": "5.3.0", + "@winches/prompts": "0.0.6", "cli-progress": "3.12.0", "commander": "11.0.0", + "find-up": "7.0.0", "compare-versions": "6.1.1", "fast-glob": "3.3.2", "gradient-string": "2.0.2", diff --git a/packages/codemod/src/actions/migrate-action.ts b/packages/codemod/src/actions/migrate-action.ts index ae70fdd..7b75d7b 100644 --- a/packages/codemod/src/actions/migrate-action.ts +++ b/packages/codemod/src/actions/migrate-action.ts @@ -1,21 +1,25 @@ import type {Codemods} from '../types'; import * as p from '@clack/prompts'; +import {exec} from '@helpers/exec'; import {Logger} from '@helpers/logger'; import chalk from 'chalk'; import {confirmClack} from 'src/prompts/clack'; -import {NEXTUI_PREFIX} from '../constants/prefix'; +import {EXTRA_FILES} from '../constants/prefix'; +import {lintAffectedFiles} from '../helpers/actions/lint-affected-files'; import {migrateCssVariables} from '../helpers/actions/migrate/migrate-css-variables'; import {migrateImportPackageWithPaths} from '../helpers/actions/migrate/migrate-import'; import {migrateJson} from '../helpers/actions/migrate/migrate-json'; +import {migrateLeftFiles} from '../helpers/actions/migrate/migrate-left-files'; import {migrateNextuiProvider} from '../helpers/actions/migrate/migrate-nextui-provider'; import {migrateNpmrc} from '../helpers/actions/migrate/migrate-npmrc'; import {migrateTailwindcss} from '../helpers/actions/migrate/migrate-tailwindcss'; import {findFiles} from '../helpers/find-files'; -import {getStore, storeParsedContent, storePathsRawContent} from '../helpers/store'; +import {getOptionsValue} from '../helpers/options'; +import {affectedFiles, storeParsedContent, storePathsRawContent} from '../helpers/store'; import {transformPaths} from '../helpers/transform'; -import {getCanRunCodemod} from '../helpers/utils'; +import {filterNextuiFiles, getCanRunCodemod, getInstallCommand} from '../helpers/utils'; process.on('SIGINT', () => { Logger.newLine(); @@ -30,7 +34,10 @@ interface MigrateActionOptions { export async function migrateAction(projectPaths?: string[], options = {} as MigrateActionOptions) { const {codemod} = options; const transformedPaths = transformPaths(projectPaths); - const files = await findFiles(transformedPaths, {ext: '{js,jsx,ts,tsx,json}'}); + const baseFiles = await findFiles(transformedPaths, {ext: '{js,jsx,ts,tsx,json,mjs,cjs}'}); + const dotFiles = await findFiles(transformedPaths, {dot: true}); + const extraFiles = dotFiles.filter((file) => EXTRA_FILES.some((extra) => file.includes(extra))); + const files = [...baseFiles, ...extraFiles]; // Store the raw content of the files storePathsRawContent(files); @@ -38,12 +45,10 @@ export async function migrateAction(projectPaths?: string[], options = {} as Mig // All package.json const packagesJson = files.filter((file) => file.includes('package.json')); // All included nextui - const nextuiFiles = files.filter((file) => - new RegExp(NEXTUI_PREFIX, 'g').test(getStore(file, 'rawContent')) - ); + const nextuiFiles = filterNextuiFiles(files); let step = 1; - p.intro(chalk.inverse(' Starting to migrate nextui to heroui ')); + p.intro(chalk.inverse('Starting to migrate NextUI to HeroUI')); /** ======================== 1. Migrate package.json ======================== */ const runMigratePackageJson = getCanRunCodemod(codemod, 'package-json-package-name'); @@ -64,7 +69,7 @@ export async function migrateAction(projectPaths?: string[], options = {} as Mig const runMigrateImportNextui = getCanRunCodemod(codemod, 'import-heroui'); if (runMigrateImportNextui) { - p.log.step(`${step}. Migrating import "nextui" to "heorui"`); + p.log.step(`${step}. Migrating import "nextui" to "heroui"`); const selectMigrateNextui = await confirmClack({ message: 'Do you want to migrate import nextui to heroui?' }); @@ -129,9 +134,7 @@ export async function migrateAction(projectPaths?: string[], options = {} as Mig const runMigrateNpmrc = getCanRunCodemod(codemod, 'npmrc'); if (runMigrateNpmrc) { - const npmrcFiles = (await findFiles(transformedPaths, {dot: true})).filter((path) => - path.includes('.npmrc') - ); + const npmrcFiles = dotFiles.filter((path) => path.includes('.npmrc')); p.log.step(`${step}. Migrating "npmrc" (Pnpm only)`); const selectMigrateNpmrc = await confirmClack({ @@ -144,5 +147,76 @@ export async function migrateAction(projectPaths?: string[], options = {} as Mig step++; } + /** ======================== 7. Whether need to change left files with @nextui-org ======================== */ + const remainingNextuiFiles = filterNextuiFiles([...affectedFiles]); + const remainingFiles = [ + ...nextuiFiles.filter((file) => !affectedFiles.has(file)), + ...remainingNextuiFiles + ]; + const runCheckLeftFiles = remainingFiles.length > 0; + + // If user not using individual codemod, we need to ask user to replace left files + if (runCheckLeftFiles && !codemod) { + p.log.step(`${step}. Remaining files with "@nextui-org" (${remainingFiles.length})`); + p.log.info(remainingFiles.join('\n')); + const selectMigrateLeftFiles = await confirmClack({ + message: 'Do you want to replace all remaining instances of "@nextui-org" with "@heroui"?' + }); + + if (selectMigrateLeftFiles) { + migrateLeftFiles(remainingFiles); + } + step++; + } + + const format = getOptionsValue('format'); + /** ======================== 8. Formatting affected files (Optional) ======================== */ + const runFormatAffectedFiles = affectedFiles.size > 0; + + // If user using format option, we don't need to use eslint + if (runFormatAffectedFiles && !format) { + p.log.step(`${step}. Formatting affected files (Optional)`); + const selectMigrateNpmrc = await confirmClack({ + message: `Do you want to format affected files? (${affectedFiles.size})` + }); + + if (selectMigrateNpmrc) { + await lintAffectedFiles(); + } + step++; + } + + // Directly linting affected files don't need to ask user + if (format) { + await lintAffectedFiles(); + } + + /** ======================== 9. Reinstall the dependencies ======================== */ + // if package.json is affected, we need to ask user to reinstall the dependencies + const runReinstallDependencies = [...affectedFiles.keys()].some((file) => + file.includes('package.json') + ); + + if (runReinstallDependencies) { + p.log.step(`${step}. Reinstalling the dependencies`); + const selectReinstallDependencies = await confirmClack({ + message: 'Do you want to reinstall the dependencies?' + }); + + if (selectReinstallDependencies) { + const {cmd} = await getInstallCommand(); + + try { + await exec(cmd); + } catch { + p.log.error(`Failed to reinstall dependencies. Please run "${cmd}" manually.`); + } + } else { + // If user doesn't want to reinstall the dependencies automatically, tell them to run it manually + p.note(`Please reinstall the dependencies (e.g., "pnpm install")`, 'Next steps'); + } + step++; + } + p.outro(chalk.green('✅ Migration completed!')); } diff --git a/packages/codemod/src/constants/prefix.ts b/packages/codemod/src/constants/prefix.ts index d2ce36c..46475e0 100644 --- a/packages/codemod/src/constants/prefix.ts +++ b/packages/codemod/src/constants/prefix.ts @@ -9,3 +9,5 @@ export const HEROUI_PLUGIN = 'heroui'; export const NEXTUI_CSS_VARIABLES_PREFIX = '--nextui-'; export const HEROUI_CSS_VARIABLES_PREFIX = '--heroui-'; + +export const EXTRA_FILES = ['.storybook']; diff --git a/packages/codemod/src/helpers/actions/lint-affected-files.ts b/packages/codemod/src/helpers/actions/lint-affected-files.ts new file mode 100644 index 0000000..e331d78 --- /dev/null +++ b/packages/codemod/src/helpers/actions/lint-affected-files.ts @@ -0,0 +1,10 @@ +import {tryLintFile} from '../lint'; +import {affectedFiles} from '../store'; + +export async function lintAffectedFiles() { + try { + await tryLintFile(Array.from(affectedFiles)); + } catch (error) { + return; + } +} diff --git a/packages/codemod/src/helpers/actions/migrate/migrate-common.ts b/packages/codemod/src/helpers/actions/migrate/migrate-common.ts index 9a69548..58d50b7 100644 --- a/packages/codemod/src/helpers/actions/migrate/migrate-common.ts +++ b/packages/codemod/src/helpers/actions/migrate/migrate-common.ts @@ -100,6 +100,20 @@ export function migrateJSXElementName( return dirtyFlag; } +export function migrateByRegex(rawContent: string, match: string, replace: string) { + const regex = new RegExp(match, 'g'); + const dirtyFlag = regex.test(rawContent); + + if (dirtyFlag) { + rawContent = rawContent.replace(regex, replace); + } + + return { + dirtyFlag, + rawContent + }; +} + /** * Migrate the name of the CallExpression * @example diff --git a/packages/codemod/src/helpers/actions/migrate/migrate-css-variables.ts b/packages/codemod/src/helpers/actions/migrate/migrate-css-variables.ts index 3446e87..1bd92a8 100644 --- a/packages/codemod/src/helpers/actions/migrate/migrate-css-variables.ts +++ b/packages/codemod/src/helpers/actions/migrate/migrate-css-variables.ts @@ -1,5 +1,5 @@ import {HEROUI_CSS_VARIABLES_PREFIX, NEXTUI_CSS_VARIABLES_PREFIX} from '../../../constants/prefix'; -import {getStore, writeFileAndUpdateStore} from '../../store'; +import {getStore, updateAffectedFiles, writeFileAndUpdateStore} from '../../store'; export function migrateCssVariables(files: string[]) { for (const file of files) { @@ -13,6 +13,7 @@ export function migrateCssVariables(files: string[]) { ); writeFileAndUpdateStore(file, 'rawContent', content); + updateAffectedFiles(file); } } } diff --git a/packages/codemod/src/helpers/actions/migrate/migrate-import.ts b/packages/codemod/src/helpers/actions/migrate/migrate-import.ts index 1a30010..bfedcbb 100644 --- a/packages/codemod/src/helpers/actions/migrate/migrate-import.ts +++ b/packages/codemod/src/helpers/actions/migrate/migrate-import.ts @@ -1,7 +1,12 @@ import jscodeshift from 'jscodeshift'; import {HEROUI_PREFIX, NEXTUI_PREFIX} from '../../../constants/prefix'; -import {type StoreObject, getStore, writeFileAndUpdateStore} from '../../store'; +import { + type StoreObject, + getStore, + updateAffectedFiles, + writeFileAndUpdateStore +} from '../../store'; /** * Migrate the import package will directly write the file @@ -24,6 +29,7 @@ export function migrateImportPackageWithPaths(paths: string[]) { if (dirtyFlag) { // Write the modified content back to the file writeFileAndUpdateStore(path, 'parsedContent', parsedContent); + updateAffectedFiles(path); } // eslint-disable-next-line no-empty } catch {} diff --git a/packages/codemod/src/helpers/actions/migrate/migrate-json.ts b/packages/codemod/src/helpers/actions/migrate/migrate-json.ts index 9eb9469..6b2e616 100644 --- a/packages/codemod/src/helpers/actions/migrate/migrate-json.ts +++ b/packages/codemod/src/helpers/actions/migrate/migrate-json.ts @@ -5,9 +5,10 @@ import {Logger} from '@helpers/logger'; import {HEROUI_PREFIX, NEXTUI_PREFIX} from '../../../constants/prefix'; import {fetchPackageLatestVersion} from '../../https'; import {safeParseJson} from '../../parse'; -import {getStore, writeFileAndUpdateStore} from '../../store'; +import {getStore, updateAffectedFiles, writeFileAndUpdateStore} from '../../store'; const DEFAULT_INDENT = 2; +const LATEST_VERSION = 'latest'; export function detectIndent(content: string): number { const match = content.match(/^(\s+)/m); @@ -33,14 +34,22 @@ export async function migrateJson(files: string[]) { try { await Promise.all([ ...filterHeroUiPkgs(Object.keys(json.dependencies)).map(async (key) => { - const version = await fetchPackageLatestVersion(key); + try { + const version = await fetchPackageLatestVersion(key); - json.dependencies[key] = version; + json.dependencies[key] = version; + } catch (error) { + json.dependencies[key] = LATEST_VERSION; + } }), ...filterHeroUiPkgs(Object.keys(json.devDependencies)).map(async (key) => { - const version = await fetchPackageLatestVersion(key); + try { + const version = await fetchPackageLatestVersion(key); - json.devDependencies[key] = version; + json.devDependencies[key] = version; + } catch (error) { + json.devDependencies[key] = LATEST_VERSION; + } }) ]); } catch (error) { @@ -51,6 +60,7 @@ export async function migrateJson(files: string[]) { const indent = detectIndent(content); writeFileAndUpdateStore(file, 'rawContent', JSON.stringify(json, null, indent)); + updateAffectedFiles(file); } }) ); diff --git a/packages/codemod/src/helpers/actions/migrate/migrate-left-files.ts b/packages/codemod/src/helpers/actions/migrate/migrate-left-files.ts new file mode 100644 index 0000000..a00f89c --- /dev/null +++ b/packages/codemod/src/helpers/actions/migrate/migrate-left-files.ts @@ -0,0 +1,19 @@ +import { + HEROUI_PLUGIN, + HEROUI_PREFIX, + NEXTUI_PLUGIN, + NEXTUI_PREFIX +} from '../../../constants/prefix'; +import {getStore, updateAffectedFiles, writeFileAndUpdateStore} from '../../store'; + +export function migrateLeftFiles(files: string[]) { + for (const file of files) { + const rawContent = getStore(file, 'rawContent'); + const replaceContent = rawContent + .replaceAll(NEXTUI_PREFIX, HEROUI_PREFIX) + .replaceAll(NEXTUI_PLUGIN, HEROUI_PLUGIN); + + writeFileAndUpdateStore(file, 'rawContent', replaceContent); + updateAffectedFiles(file); + } +} diff --git a/packages/codemod/src/helpers/actions/migrate/migrate-nextui-provider.ts b/packages/codemod/src/helpers/actions/migrate/migrate-nextui-provider.ts index c99be60..862a703 100644 --- a/packages/codemod/src/helpers/actions/migrate/migrate-nextui-provider.ts +++ b/packages/codemod/src/helpers/actions/migrate/migrate-nextui-provider.ts @@ -1,7 +1,7 @@ import {HEROUI_PROVIDER, NEXTUI_PROVIDER} from '../../../constants/prefix'; -import {getStore, writeFileAndUpdateStore} from '../../store'; +import {getStore, updateAffectedFiles, writeFileAndUpdateStore} from '../../store'; -import {migrateImportName, migrateJSXElementName} from './migrate-common'; +import {migrateByRegex} from './migrate-common'; /** * Migrate the NextUIProvider to HeroUIProvider will directly write the file @@ -12,22 +12,21 @@ import {migrateImportName, migrateJSXElementName} from './migrate-common'; export function migrateNextuiProvider(paths: string[]) { for (const path of paths) { try { - const parsedContent = getStore(path, 'parsedContent'); + let rawContent = getStore(path, 'rawContent'); let dirtyFlag = false; - if (!parsedContent) { + if (!rawContent) { continue; } // Replace JSX element NextUIProvider with HeroUIProvider - dirtyFlag = migrateJSXElementName(parsedContent, NEXTUI_PROVIDER, HEROUI_PROVIDER); - // Replace NextUIProvider with HeroUIProvider in import statements - if (dirtyFlag) { - migrateImportName(parsedContent, NEXTUI_PROVIDER, HEROUI_PROVIDER); + ({dirtyFlag, rawContent} = migrateByRegex(rawContent, NEXTUI_PROVIDER, HEROUI_PROVIDER)); + if (dirtyFlag) { // Write the modified content back to the file - writeFileAndUpdateStore(path, 'parsedContent', parsedContent); + writeFileAndUpdateStore(path, 'rawContent', rawContent); + updateAffectedFiles(path); } // eslint-disable-next-line no-empty } catch {} diff --git a/packages/codemod/src/helpers/actions/migrate/migrate-tailwindcss.ts b/packages/codemod/src/helpers/actions/migrate/migrate-tailwindcss.ts index 76c596e..ca6a0b8 100644 --- a/packages/codemod/src/helpers/actions/migrate/migrate-tailwindcss.ts +++ b/packages/codemod/src/helpers/actions/migrate/migrate-tailwindcss.ts @@ -8,7 +8,7 @@ import { NEXTUI_PLUGIN, NEXTUI_PREFIX } from '../../../constants/prefix'; -import {getStore, writeFileAndUpdateStore} from '../../store'; +import {getStore, updateAffectedFiles, writeFileAndUpdateStore} from '../../store'; import {migrateCallExpressionName, migrateImportName} from './migrate-common'; import {migrateImportPackage} from './migrate-import'; @@ -56,6 +56,7 @@ export function migrateTailwindcss(paths: string[]) { if (dirtyFlag) { writeFileAndUpdateStore(path, 'parsedContent', parsedContent); + updateAffectedFiles(path); } } } diff --git a/packages/codemod/src/helpers/https.ts b/packages/codemod/src/helpers/https.ts index f77bbbd..6947b00 100644 --- a/packages/codemod/src/helpers/https.ts +++ b/packages/codemod/src/helpers/https.ts @@ -1,3 +1,4 @@ +import retry from 'async-retry'; import chalk from 'chalk'; import ora from 'ora'; @@ -25,30 +26,28 @@ export async function fetchPackageLatestVersion(packageName: string): Promise