Skip to content

Commit

Permalink
Merge branch 'main' of github.com:nextui-org/nextui-cli into refactor…
Browse files Browse the repository at this point in the history
…/eng-1847-nextui-cli-to-heroui-cli
  • Loading branch information
winchesHe committed Jan 16, 2025
2 parents e699fe5 + 33fa8e6 commit e821438
Show file tree
Hide file tree
Showing 19 changed files with 310 additions and 57 deletions.
15 changes: 10 additions & 5 deletions packages/codemod/README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
<p align="center">
<a href="https://nextui.org">
<img width="20%" src="https://raw.githubusercontent.com/nextui-org/nextui/main/apps/docs/public/isotipo.png" alt="nextui" />
<a href="https://heroui.com">
<img width="20%" src="https://raw.githubusercontent.com/frontio-ai/heroui/main/apps/docs/public/isotipo.png" alt="nextui" />
<h1 align="center">@heroui/codemod</h1>
</a>
</p>

The CLI provides a comprehensive suite of tools to migrate your codebase from NextUI to heroui.
</br>

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:

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
10 changes: 6 additions & 4 deletions packages/codemod/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
98 changes: 86 additions & 12 deletions packages/codemod/src/actions/migrate-action.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -30,20 +34,21 @@ 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);

// 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');
Expand All @@ -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?'
});
Expand Down Expand Up @@ -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({
Expand All @@ -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!'));
}
2 changes: 2 additions & 0 deletions packages/codemod/src/constants/prefix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
10 changes: 10 additions & 0 deletions packages/codemod/src/helpers/actions/lint-affected-files.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
14 changes: 14 additions & 0 deletions packages/codemod/src/helpers/actions/migrate/migrate-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -13,6 +13,7 @@ export function migrateCssVariables(files: string[]) {
);

writeFileAndUpdateStore(file, 'rawContent', content);
updateAffectedFiles(file);
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 {}
Expand Down
20 changes: 15 additions & 5 deletions packages/codemod/src/helpers/actions/migrate/migrate-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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) {
Expand All @@ -51,6 +60,7 @@ export async function migrateJson(files: string[]) {
const indent = detectIndent(content);

writeFileAndUpdateStore(file, 'rawContent', JSON.stringify(json, null, indent));
updateAffectedFiles(file);
}
})
);
Expand Down
19 changes: 19 additions & 0 deletions packages/codemod/src/helpers/actions/migrate/migrate-left-files.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit e821438

Please sign in to comment.