From e67bb417113b2173432af5859e497fc59f8b5bbc Mon Sep 17 00:00:00 2001 From: winches <329487092@qq.com> Date: Fri, 5 Apr 2024 19:04:43 +0800 Subject: [PATCH 01/15] feat: init add command --- src/actions/add-action.ts | 210 ++++++++++++++++++++++++++++++++++ src/actions/doctor-action.ts | 16 ++- src/actions/env-action.ts | 2 +- src/actions/list-action.ts | 2 +- src/constants/component.ts | 16 ++- src/constants/components.json | 113 ++++++++++++------ src/constants/required.ts | 67 ++++++++++- src/constants/templates.ts | 38 ++++++ src/helpers/check.ts | 19 +-- src/helpers/exec.ts | 36 ++++++ src/helpers/fix.ts | 117 +++++++++++++++++++ src/helpers/match.ts | 28 +++++ src/helpers/math-diff.ts | 34 ++++++ src/helpers/package.ts | 2 +- src/helpers/type.ts | 2 +- src/helpers/utils.ts | 6 +- src/index.ts | 16 +++ src/prompts/index.ts | 16 +++ src/scripts/helpers.ts | 7 +- 19 files changed, 679 insertions(+), 68 deletions(-) create mode 100644 src/actions/add-action.ts create mode 100644 src/helpers/exec.ts create mode 100644 src/helpers/fix.ts create mode 100644 src/helpers/math-diff.ts diff --git a/src/actions/add-action.ts b/src/actions/add-action.ts new file mode 100644 index 0000000..cd38137 --- /dev/null +++ b/src/actions/add-action.ts @@ -0,0 +1,210 @@ +import type {SAFE_ANY} from '@helpers/type'; + +import {writeFileSync} from 'fs'; + +import chalk from 'chalk'; + +import {checkApp, checkPnpm, checkRequiredContentInstalled, checkTailwind} from '@helpers/check'; +import {detect} from '@helpers/detect'; +import {exec} from '@helpers/exec'; +import {fixPnpm, fixProvider, fixTailwind} from '@helpers/fix'; +import {Logger} from '@helpers/logger'; +import {findMostMatchText} from '@helpers/math-diff'; +import {getPackageInfo} from '@helpers/package'; +import {findFiles} from '@helpers/utils'; +import { + nextUIComponents, + nextUIComponentsKeys, + nextUIComponentsKeysSet, + nextUIComponentsMap +} from 'src/constants/component'; +import {resolver} from 'src/constants/path'; +import {individualTailwindRequired} from 'src/constants/required'; +import {tailwindTemplate} from 'src/constants/templates'; +import {getAutocompleteMultiselect} from 'src/prompts'; + +interface AddActionOptions { + all?: boolean; + prettier?: boolean; + packagePath?: string; + tailwindPath?: string; + appPath?: string; +} + +export async function addAction(components: string[], options: AddActionOptions) { + const { + all = false, + appPath = findFiles('**/App.(j|t)sx')[0], + packagePath = resolver('package.json'), + prettier = false, + tailwindPath = findFiles('**/tailwind.config.(j|t)s')[0] + } = options; + + if (!components && !all) { + components = await getAutocompleteMultiselect( + 'Select the NextUI components to add', + nextUIComponents.map((component) => { + return { + description: component.description, + title: component.name, + value: component.name + }; + }) + ); + } else if (all) { + components = nextUIComponentsKeys; + } + + /** ======================== Add judge whether illegal component exist ======================== */ + const illegalList: [string, null | string][] = []; + + for (const component of components) { + if (!nextUIComponentsKeysSet.has(component)) { + const matchComponent = findMostMatchText(nextUIComponentsKeys, component); + + illegalList.push([component, matchComponent]); + } + } + + if (illegalList.length) { + const [illegalComponents, matchComponents] = illegalList.reduce( + (acc, [illegalComponent, matchComponent]) => { + return [ + acc[0] + chalk.underline(illegalComponent) + ', ', + acc[1] + (matchComponent ? chalk.underline(matchComponent) + ', ' : '') + ]; + }, + ['', ''] + ); + + Logger.prefix( + 'error', + `Illegal NextUI components: ${illegalComponents.replace(/, $/, '')}${ + matchComponents + ? `\n${''.padEnd(12)}It may be a typo, did you mean ${matchComponents.replace( + /, $/, + '' + )}?` + : '' + }` + ); + + return; + } + + // Check whether have added the NextUI components + const {allDependenciesKeys, currentComponents} = getPackageInfo(packagePath); + + const currentComponentsKeys = currentComponents.map((c) => c.name); + const filterCurrentComponents = components.filter((c) => currentComponentsKeys.includes(c)); + + if (filterCurrentComponents.length) { + Logger.prefix( + 'error', + `❌ You have added the NextUI components: ${filterCurrentComponents + .map((c) => chalk.underline(c)) + .join(', ')}` + ); + + return; + } + + // Check whether the App.tsx file exists + if (!appPath) { + Logger.prefix( + 'error', + "❌ Cannot find the App.(j|t)sx file\nYou should specify appPath through 'add --appPath=yourAppPath'" + ); + + return; + } + + const currentPkgManager = await detect(); + + /** ======================== Step 1 Add dependencies required ======================== */ + if (all) { + const [, ...missingDependencies] = checkRequiredContentInstalled('all', allDependenciesKeys); + + if (missingDependencies.length) { + Logger.info( + `Adding the required dependencies: ${[...missingDependencies] + .map((c) => chalk.underline(c)) + .join(', ')}` + ); + + await exec(`${currentPkgManager} add ${[...missingDependencies].join(' ')}`); + } + } else { + const [, ..._missingDependencies] = checkRequiredContentInstalled( + 'partial', + allDependenciesKeys + ); + const missingDependencies = [ + ..._missingDependencies, + ...components.map((c) => nextUIComponentsMap[c]!.package) + ]; + + Logger.info( + `Adding the required dependencies: ${[...missingDependencies] + .map((c) => chalk.underline(c)) + .join(', ')}` + ); + + await exec( + `${currentPkgManager} ${currentPkgManager === 'npm' ? 'install' : 'add'} ${[ + ...missingDependencies + ].join(' ')}` + ); + } + + /** ======================== Step 2 Tailwind CSS Setup ======================== */ + const type: SAFE_ANY = all ? 'all' : 'partial'; + + const isPnpm = currentPkgManager === 'pnpm'; + + if (!tailwindPath) { + const individualContent = individualTailwindRequired.content(currentComponents, isPnpm); + const template = tailwindTemplate(type, individualContent); + const tailwindPath = resolver('tailwind.config.js'); + + writeFileSync(tailwindPath, template, 'utf-8'); + + Logger.newLine(); + Logger.info(`Added the tailwind.config.js file: ${tailwindPath}`); + } else { + const [, ...errorInfoList] = checkTailwind(type, tailwindPath, currentComponents, isPnpm); + + fixTailwind(type, {errorInfoList, format: prettier, tailwindPath}); + + Logger.newLine(); + Logger.info(`Added the required tailwind content in file: ${tailwindPath}`); + } + + /** ======================== Step 3 Provider ======================== */ + const [isCorrectProvider] = checkApp(type, appPath); + + if (!isCorrectProvider) { + fixProvider(appPath, {format: prettier}); + + Logger.newLine(); + Logger.info(`Added the NextUIProvider in file: ${appPath}`); + Logger.warn('You need to check the NextUIProvider whether in the correct place'); + } + + /** ======================== Step 4 Setup Pnpm ======================== */ + if (currentPkgManager === 'pnpm') { + const npmrcPath = resolver('.npmrc'); + + const [isCorrectPnpm] = checkPnpm(npmrcPath); + + if (!isCorrectPnpm) { + fixPnpm(npmrcPath); + } + } + + // Finish adding the NextUI components + Logger.newLine(); + Logger.success( + '✅ All the NextUI components have been added\nNow you can use the component you installed in your application' + ); +} diff --git a/src/actions/doctor-action.ts b/src/actions/doctor-action.ts index a24ece5..316d9b1 100644 --- a/src/actions/doctor-action.ts +++ b/src/actions/doctor-action.ts @@ -33,7 +33,7 @@ export interface ProblemRecord { export async function doctorAction(options: DoctorActionOptions) { const { - appPath = findFiles('**/App.tsx')[0], + appPath = findFiles('**/App.(j|t)sx')[0], checkApp: _enableCheckApp = true, checkPnpm: _enableCheckPnpm = true, checkTailwind: _enableCheckTailwind = true, @@ -45,8 +45,7 @@ export async function doctorAction(options: DoctorActionOptions) { const enableCheckTailwind = transformOption(_enableCheckTailwind); const tailwindPaths = [tailwindPath].flat(); - const {allDependenciesKeys, currentComponents, isAllComponents} = - await getPackageInfo(packagePath); + const {allDependenciesKeys, currentComponents, isAllComponents} = getPackageInfo(packagePath); /** ======================== Output when there is no components installed ======================== */ if (!currentComponents.length && !isAllComponents) { @@ -95,7 +94,7 @@ export async function doctorAction(options: DoctorActionOptions) { level: 'error', name: 'missingApp', outputFn: () => { - Logger.error('Cannot find the App.tsx file'); + Logger.error('Cannot find the App.(j|t)sx file'); Logger.error("You should specify appPath through 'doctor --appPath=yourAppPath'"); } }); @@ -147,11 +146,14 @@ export async function doctorAction(options: DoctorActionOptions) { // Check whether tailwind.config file is correct if (enableCheckTailwind) { + const isPnpm = (await detect()) === 'pnpm'; + for (const tailwindPath of tailwindPaths) { const [isCorrectTailwind, ...errorInfo] = checkTailwind( 'partial', tailwindPath, - currentComponents + currentComponents, + isPnpm ); if (!isCorrectTailwind) { @@ -177,7 +179,9 @@ export async function doctorAction(options: DoctorActionOptions) { const currentPkgManager = await detect(); if (currentPkgManager === 'pnpm') { - const [isCorrect, ...errorInfo] = checkPnpm(); + const npmrcPath = resolver('.npmrc'); + + const [isCorrect, ...errorInfo] = checkPnpm(npmrcPath); if (!isCorrect) { problemRecord.push({ diff --git a/src/actions/env-action.ts b/src/actions/env-action.ts index 5b83208..8843cb4 100644 --- a/src/actions/env-action.ts +++ b/src/actions/env-action.ts @@ -9,7 +9,7 @@ interface EnvActionOptions { export async function envAction(options: EnvActionOptions) { const {packagePath = resolver('package.json')} = options; - const {currentComponents} = await getPackageInfo(packagePath); + const {currentComponents} = getPackageInfo(packagePath); /** ======================== Output the current components ======================== */ outputComponents(currentComponents); diff --git a/src/actions/list-action.ts b/src/actions/list-action.ts index 8d1b7b3..0c67762 100644 --- a/src/actions/list-action.ts +++ b/src/actions/list-action.ts @@ -18,7 +18,7 @@ export async function listAction(options: ListActionOptions) { try { /** ======================== Get the installed components ======================== */ if (current) { - const {currentComponents} = await getPackageInfo(packagePath); + const {currentComponents} = getPackageInfo(packagePath); components = currentComponents; } diff --git a/src/constants/component.ts b/src/constants/component.ts index 9afe3aa..7433ad7 100644 --- a/src/constants/component.ts +++ b/src/constants/component.ts @@ -2,13 +2,27 @@ import {getComponents} from 'src/scripts/helpers'; export const nextUIComponents = (await getComponents()).components; +export const nextUIComponentsKeys = nextUIComponents.map((component) => component.name); +export const nextUIcomponentsPackages = nextUIComponents.map((component) => component.package); + +export const nextUIComponentsKeysSet = new Set(nextUIComponentsKeys); + +export const nextUIComponentsMap = nextUIComponents.reduce( + (acc, component) => { + acc[component.name] = component; + + return acc; + }, + {} as Record +); + export const orderNextUIComponentKeys = ['package', 'version', 'status', 'docs'] as const; export const colorNextUIComponentKeys = ['package', 'version', 'status']; export type NextUIComponentStatus = 'stable' | 'updated' | 'newPost'; -type NextUIComponent = (typeof nextUIComponents)[0]; +export type NextUIComponent = (typeof nextUIComponents)[0]; export type NextUIComponents = (Omit & { status: NextUIComponentStatus; diff --git a/src/constants/components.json b/src/constants/components.json index 40bb89d..71ec227 100644 --- a/src/constants/components.json +++ b/src/constants/components.json @@ -6,7 +6,8 @@ "version": "2.0.28", "docs": "https://nextui.org/docs/components/accordion", "description": "Collapse display a list of high-level options that can expand/collapse to reveal more information.", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "autocomplete", @@ -14,7 +15,8 @@ "version": "2.0.10", "docs": "https://nextui.org/docs/components/autocomplete", "description": "An autocomplete combines a text input with a listbox, allowing users to filter a list of options to items matching a query.", - "status": "newPost" + "status": "newPost", + "style": "" }, { "name": "avatar", @@ -22,7 +24,8 @@ "version": "2.0.24", "docs": "https://nextui.org/docs/components/avatar", "description": "The Avatar component is used to represent a user, and displays the profile picture, initials or fallback icon.", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "badge", @@ -30,7 +33,8 @@ "version": "2.0.24", "docs": "https://nextui.org/docs/components/badge", "description": "Badges are used as a small numerical value or status descriptor for UI elements.", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "breadcrumbs", @@ -38,7 +42,8 @@ "version": "2.0.4", "docs": "https://nextui.org/docs/components/breadcrumbs", "description": "Breadcrumbs display a hierarchy of links to the current page or resource in an application.", - "status": "newPost" + "status": "newPost", + "style": "" }, { "name": "button", @@ -46,7 +51,8 @@ "version": "2.0.27", "docs": "https://nextui.org/docs/components/button", "description": "Buttons allow users to perform actions and choose with a single tap.", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "card", @@ -54,7 +60,8 @@ "version": "2.0.24", "docs": "https://nextui.org/docs/components/card", "description": "Card is a container for text, photos, and actions in the context of a single subject.", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "checkbox", @@ -62,7 +69,8 @@ "version": "2.0.25", "docs": "https://nextui.org/docs/components/checkbox", "description": "Checkboxes allow users to select multiple items from a list of individual items, or to mark one individual item as selected.", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "chip", @@ -70,7 +78,8 @@ "version": "2.0.25", "docs": "https://nextui.org/docs/components/chip", "description": "Chips help people enter information, make selections, filter content, or trigger actions.", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "code", @@ -78,7 +87,8 @@ "version": "2.0.24", "docs": "https://nextui.org/docs/components/code", "description": "Code is a component used to display inline code.", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "divider", @@ -86,7 +96,8 @@ "version": "2.0.25", "docs": "https://nextui.org/docs/components/divider", "description": ". A separator is a visual divider between two groups of content", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "dropdown", @@ -94,7 +105,8 @@ "version": "2.1.17", "docs": "https://nextui.org/docs/components/dropdown", "description": "A dropdown displays a list of actions or options that a user can choose.", - "status": "updated" + "status": "updated", + "style": "" }, { "name": "image", @@ -102,7 +114,8 @@ "version": "2.0.24", "docs": "https://nextui.org/docs/components/image", "description": "A simple image component", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "input", @@ -110,7 +123,8 @@ "version": "2.1.17", "docs": "https://nextui.org/docs/components/input", "description": "The input component is designed for capturing user input within a text field.", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "kbd", @@ -118,7 +132,8 @@ "version": "2.0.25", "docs": "https://nextui.org/docs/components/kbd", "description": "The keyboard key components indicates which key or set of keys used to execute a specificv action", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "link", @@ -126,7 +141,8 @@ "version": "2.0.26", "docs": "https://nextui.org/docs/components/link", "description": "Links allow users to click their way from page to page. This component is styled to resemble a hyperlink and semantically renders an <a>", - "status": "updated" + "status": "updated", + "style": "" }, { "name": "listbox", @@ -134,7 +150,8 @@ "version": "2.1.16", "docs": "https://nextui.org/docs/components/listbox", "description": "A listbox displays a list of options and allows a user to select one or more of them.", - "status": "updated" + "status": "updated", + "style": "" }, { "name": "menu", @@ -142,7 +159,8 @@ "version": "2.0.17", "docs": "https://nextui.org/docs/components/menu", "description": "A menu displays a list of options and allows a user to select one or more of them.", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "modal", @@ -150,7 +168,8 @@ "version": "2.0.29", "docs": "https://nextui.org/docs/components/modal", "description": "Displays a dialog with a custom content that requires attention or provides additional information.", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "navbar", @@ -158,7 +177,8 @@ "version": "2.0.27", "docs": "https://nextui.org/docs/components/navbar", "description": "A responsive navigation header positioned on top side of your page that includes support for branding, links, navigation, collapse and more.", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "pagination", @@ -166,7 +186,8 @@ "version": "2.0.27", "docs": "https://nextui.org/docs/components/pagination", "description": "The Pagination component allows you to display active page and navigate between multiple pages.", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "popover", @@ -174,7 +195,8 @@ "version": "2.1.15", "docs": "https://nextui.org/docs/components/popover", "description": "A popover is an overlay element positioned relative to a trigger.", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "progress", @@ -182,7 +204,8 @@ "version": "2.0.25", "docs": "https://nextui.org/docs/components/progress", "description": "Progress bars show either determinate or indeterminate progress of an operation over time.", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "radio", @@ -190,7 +213,8 @@ "version": "2.0.25", "docs": "https://nextui.org/docs/components/radio", "description": "Radios allow users to select a single option from a list of mutually exclusive options.", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "ripple", @@ -198,7 +222,8 @@ "version": "2.0.24", "docs": "https://nextui.org/docs/components/ripple", "description": "A simple implementation to display a ripple animation when the source component is clicked", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "scroll-shadow", @@ -206,7 +231,8 @@ "version": "2.1.13", "docs": "https://nextui.org/docs/components/scroll-shadow", "description": "A component that applies top and bottom shadows when content overflows on scroll.", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "select", @@ -214,7 +240,8 @@ "version": "2.1.21", "docs": "https://nextui.org/docs/components/select", "description": "A select displays a collapsible list of options and allows a user to select one of them.", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "skeleton", @@ -222,7 +249,8 @@ "version": "2.0.24", "docs": "https://nextui.org/docs/components/skeleton", "description": "Skeleton is used to display the loading state of some component.", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "slider", @@ -230,7 +258,8 @@ "version": "2.2.6", "docs": "https://nextui.org/docs/components/slider", "description": "A slider allows a user to select one or more values within a range.", - "status": "newPost" + "status": "newPost", + "style": "" }, { "name": "snippet", @@ -238,7 +267,8 @@ "version": "2.0.31", "docs": "https://nextui.org/docs/components/snippet", "description": "Display a snippet of copyable code for the command line.", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "spacer", @@ -246,7 +276,8 @@ "version": "2.0.24", "docs": "https://nextui.org/docs/components/spacer", "description": "A flexible spacer component designed to create consistent spacing and maintain alignment in your layout.", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "spinner", @@ -254,7 +285,8 @@ "version": "2.0.25", "docs": "https://nextui.org/docs/components/spinner", "description": "Loaders express an unspecified wait time or display the length of a process.", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "switch", @@ -262,7 +294,8 @@ "version": "2.0.25", "docs": "https://nextui.org/docs/components/switch", "description": "A switch is similar to a checkbox, but represents on/off values as opposed to selection.", - "status": "stable" + "status": "stable", + "style": "toggle" }, { "name": "table", @@ -270,7 +303,8 @@ "version": "2.0.28", "docs": "https://nextui.org/docs/components/table", "description": "Tables are used to display tabular data using rows and columns. ", - "status": "stable" + "status": "stable", + "style": "" }, { "name": "tabs", @@ -278,7 +312,8 @@ "version": "2.0.26", "docs": "https://nextui.org/docs/components/tabs", "description": "Tabs organize content into multiple sections and allow users to navigate between them.", - "status": "updated" + "status": "updated", + "style": "" }, { "name": "tooltip", @@ -286,7 +321,8 @@ "version": "2.0.30", "docs": "https://nextui.org/docs/components/tooltip", "description": "A React Component for rendering dynamically positioned Tooltips", - "status": "stable" + "status": "stable", + "style": "popover" }, { "name": "user", @@ -294,8 +330,9 @@ "version": "2.0.25", "docs": "https://nextui.org/docs/components/user", "description": "Flexible User Profile Component.", - "status": "stable" + "status": "stable", + "style": "" } ], - "version": "0.0.0-dev-v2-20240331183656" + "version": "0.0.0-dev-v2-20240402181757" } diff --git a/src/constants/required.ts b/src/constants/required.ts index d5fe516..d650854 100644 --- a/src/constants/required.ts +++ b/src/constants/required.ts @@ -1,4 +1,11 @@ -import type {NextUIComponents} from './component'; +import {existsSync} from 'fs'; + +import fg from 'fast-glob'; + +import {getPackageInfo} from '@helpers/package'; + +import {type NextUIComponent, type NextUIComponents} from './component'; +import {resolver} from './path'; export const FRAMER_MOTION = 'framer-motion'; export const TAILWINDCSS = 'tailwindcss'; @@ -21,13 +28,27 @@ export const tailwindRequired = { } as const; export const individualTailwindRequired = { - content: (currentComponents: NextUIComponents) => { - if (currentComponents.length === 1) { - return `@nextui-org/theme/dist/components/${currentComponents[0]!.name}.js`; + content: (currentComponents: NextUIComponents, isPnpm: boolean) => { + currentComponents.forEach((component) => { + const walkDeps = walkDepComponents(component, isPnpm) as NextUIComponents; + + currentComponents.push(...walkDeps); + }); + + const outputComponents = [ + ...new Set( + currentComponents.map((component) => { + return component.style || component.name; + }) + ) + ]; + + if (outputComponents.length === 1) { + return `@nextui-org/theme/dist/components/${currentComponents[0]}.js`; } - const requiredContent = currentComponents + const requiredContent = outputComponents .reduce((acc, component) => { - return (acc += `${component.name}|`); + return (acc += `${component}|`); }, '') .replace(/\|$/, ''); @@ -43,3 +64,37 @@ export const appRequired = { export const pnpmRequired = { content: 'public-hoist-pattern[]=*@nextui-org/*' } as const; + +export function walkDepComponents(nextUIComponent: NextUIComponent, isPnpm: boolean) { + const component = nextUIComponent.name; + let componentPath = resolver(`node_modules/@nextui-org/${component}`); + const components = [nextUIComponent]; + + if (!existsSync(componentPath) && isPnpm) { + const pnpmDir = resolver('node_modules/.pnpm'); + + const file = fg.sync(`**/@nextui-org/${component}`, { + absolute: true, + cwd: pnpmDir, + onlyDirectories: true + })[0]; + + if (file) { + componentPath = file; + } else { + return components; + } + } + + const {currentComponents} = getPackageInfo(`${componentPath}/package.json`); + + if (currentComponents.length) { + for (const component of currentComponents) { + const result = walkDepComponents(component, isPnpm); + + components.push(...result); + } + } + + return components; +} diff --git a/src/constants/templates.ts b/src/constants/templates.ts index a60e1e1..00f7b18 100644 --- a/src/constants/templates.ts +++ b/src/constants/templates.ts @@ -1,5 +1,43 @@ +import type {CheckType} from '@helpers/check'; + export const APP_REPO = 'https://codeload.github.com/nextui-org/next-app-template/tar.gz/main'; export const PAGES_REPO = 'https://codeload.github.com/nextui-org/next-pages-template/tar.gz/main'; export const APP_DIR = 'next-app-template-main'; export const PAGES_DIR = 'next-pages-template-main'; + +export function tailwindTemplate(type: 'all', content?: string): string; +export function tailwindTemplate(type: 'partial', content: string): string; +export function tailwindTemplate(type: CheckType, content?: string) { + if (type === 'all') { + return `// tailwind.config.js +const {nextui} = require("@nextui-org/react"); + +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + darkMode: "class", + plugins: [nextui()], +};`; + } else { + return `// tailwind.config.js +const {nextui} = require("@nextui-org/theme"); + +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + ${content}, + ], + theme: { + extend: {}, + }, + darkMode: "class", + plugins: [nextui()], +};`; + } +} diff --git a/src/helpers/check.ts b/src/helpers/check.ts index 7a72f73..90d157e 100644 --- a/src/helpers/check.ts +++ b/src/helpers/check.ts @@ -4,7 +4,6 @@ import type {NextUIComponents} from 'src/constants/component'; import {readFileSync} from 'fs'; -import {resolver} from 'src/constants/path'; import { DOCS_INSTALLED, DOCS_TAILWINDCSS_SETUP, @@ -147,17 +146,20 @@ export function checkRequiredContentInstalled( export function checkTailwind( type: 'all', tailwindPath: string, - currentComponents?: NextUIComponents + currentComponents?: NextUIComponents, + isPnpm?: boolean ): CheckResult; export function checkTailwind( type: 'partial', tailwindPath: string, - currentComponents: NextUIComponents + currentComponents: NextUIComponents, + isPnpm: boolean ): CheckResult; export function checkTailwind( type: CheckType, tailwindPath: string, - currentComponents?: NextUIComponents + currentComponents?: NextUIComponents, + isPnpm?: boolean ): CheckResult { const result = [] as unknown as CheckResult; @@ -168,7 +170,7 @@ export function checkTailwind( if (type === 'all') { // Check if the required content is added Detail: https://nextui.org/docs/guide/installation#global-installation - const isDarkModeCorrect = new RegExp(tailwindRequired.darkMode).test(tailwindContent); + const isDarkModeCorrect = tailwindContent.match(/darkMode: ["']\w/); const isContentCorrect = contentMatch.some((content) => content.includes(tailwindRequired.content) ); @@ -183,8 +185,8 @@ export function checkTailwind( !isContentCorrect && result.push(tailwindRequired.content); !isPluginsCorrect && result.push(tailwindRequired.plugins); } else if (type === 'partial') { - const individualContent = individualTailwindRequired.content(currentComponents!); - const isContentCorrect = contentMatch.some((content) => individualContent.includes(content)); + const individualContent = individualTailwindRequired.content(currentComponents!, isPnpm!); + const isContentCorrect = contentMatch.some((content) => content.includes(individualContent)); const isPluginsCorrect = pluginsMatch.some((plugins) => plugins.includes(tailwindRequired.plugins) ); @@ -218,9 +220,8 @@ export function checkApp(type: CheckType, appPath: string): CheckResult { return [false, ...result]; } -export function checkPnpm(): CheckResult { +export function checkPnpm(npmrcPath: string): CheckResult { const result = [] as unknown as CheckResult; - const npmrcPath = resolver('.npmrc'); let content: string; diff --git a/src/helpers/exec.ts b/src/helpers/exec.ts new file mode 100644 index 0000000..a6fa72c --- /dev/null +++ b/src/helpers/exec.ts @@ -0,0 +1,36 @@ +import type {AppendKeyValue} from './type'; + +import {type CommonExecOptions, execSync} from 'child_process'; + +import {Logger} from './logger'; +import {omit} from './utils'; + +export async function exec( + cmd: string, + commonExecOptions?: AppendKeyValue +) { + return new Promise((resolve, reject) => { + try { + const {logCmd = true} = commonExecOptions || {}; + + if (logCmd) { + Logger.newLine(); + Logger.log(`${cmd}`); + } + + const stdout = execSync(cmd, { + stdio: 'inherit', + ...(commonExecOptions ? omit(commonExecOptions, ['logCmd']) : {}) + }); + + if (stdout) { + const output = stdout.toString(); + + resolve(output); + } + resolve(''); + } catch (error) { + reject(error); + } + }); +} diff --git a/src/helpers/fix.ts b/src/helpers/fix.ts new file mode 100644 index 0000000..79a8849 --- /dev/null +++ b/src/helpers/fix.ts @@ -0,0 +1,117 @@ +import type {CheckType} from './check'; + +import {execSync} from 'node:child_process'; +import {readFileSync, writeFileSync} from 'node:fs'; + +import {pnpmRequired, tailwindRequired} from 'src/constants/required'; + +import {Logger} from './logger'; +import {getMatchArray, replaceMatchArray} from './match'; + +interface FixTailwind { + errorInfoList: string[]; + tailwindPath: string; + write?: boolean; + format?: boolean; +} + +interface FixProvider { + write?: boolean; + format?: boolean; +} + +export function fixProvider(appPath: string, options: FixProvider) { + const {format = false, write = true} = options; + let appContent = readFileSync(appPath, 'utf-8'); + + appContent = `import {NextUIProvider} from "@nextui-org/react";\n${appContent}`; + + appContent = wrapWithNextUIProvider(appContent); + + write && writeFileSync(appPath, appContent, 'utf-8'); + format && execSync(`npx prettier --write ${appPath}`, {stdio: 'ignore'}); +} + +function wrapWithNextUIProvider(content: string) { + const returnRegex = /return\s*\(([\s\S]*?)\);/g; + const wrappedCode = content.replace(returnRegex, (_, p1) => { + return `return ( + + ${p1.trim()} + + );`; + }); + + return wrappedCode; +} + +export function fixTailwind(type: CheckType, options: FixTailwind) { + const {errorInfoList, format = false, tailwindPath, write = true} = options; + + if (!errorInfoList.length) { + return; + } + + let tailwindContent = readFileSync(tailwindPath, 'utf-8'); + const contentMatch = getMatchArray('content', tailwindContent); + const pluginsMatch = getMatchArray('plugins', tailwindContent); + + for (const errorInfo of errorInfoList) { + const [errorType, info] = transformErrorInfo(errorInfo); + + if (errorType === 'content') { + contentMatch.push(info); + tailwindContent = replaceMatchArray('content', tailwindContent, contentMatch); + } else if (errorType === 'plugins') { + pluginsMatch.push(info); + tailwindContent = replaceMatchArray('plugins', tailwindContent, pluginsMatch); + } + + if (type === 'all' && errorType === 'darkMode') { + // Add darkMode under the plugins content in tailwindContent + const darkModeIndex = tailwindContent.indexOf('plugins') - 1; + const darkModeContent = tailwindRequired.darkMode; + + tailwindContent = `${tailwindContent.slice( + 0, + darkModeIndex + )}${darkModeContent},\n${tailwindContent.slice(darkModeIndex)}`; + } + } + + write && writeFileSync(tailwindPath, tailwindContent, 'utf-8'); + + if (format) { + try { + execSync(`npx prettier --write ${tailwindPath}`, {stdio: 'ignore'}); + } catch (error) { + Logger.warn(`Prettier failed to format ${tailwindPath}`); + } + } +} + +function transformErrorInfo(errorInfo: string): [keyof typeof tailwindRequired, string] { + if (tailwindRequired.darkMode.includes(errorInfo)) { + return ['darkMode', errorInfo]; + } else if (tailwindRequired.plugins.includes(errorInfo)) { + return ['plugins', errorInfo]; + } else { + return ['content', errorInfo]; + } +} + +export function fixPnpm(npmrcPath: string, write = true, runInstall = true) { + let content = readFileSync(npmrcPath, 'utf-8'); + + content = `${pnpmRequired.content}\n${npmrcPath}`; + + write && writeFileSync(npmrcPath, content, 'utf-8'); + Logger.newLine(); + Logger.info(`Added the required content in file: ${npmrcPath}`); + + if (runInstall) { + Logger.newLine(); + Logger.info('Pnpm install will be run now'); + runInstall && execSync('pnpm install', {stdio: 'inherit'}); + } +} diff --git a/src/helpers/match.ts b/src/helpers/match.ts index 84f8285..8d2bd00 100644 --- a/src/helpers/match.ts +++ b/src/helpers/match.ts @@ -43,3 +43,31 @@ export function getMatchArray(key: string, target: string) { return []; } + +/** + * Replace the array content of the key in the target string. + * @example replaceMatchArray('key', 'key: [a, b, c]', ['d', 'e', 'f']) => 'key: [d, e, f]' + * @param key + * @param target + * @param value + */ +export function replaceMatchArray(key: string, target: string, value: string[]) { + const mixinReg = new RegExp(`\\s*${key}:\\s\\[([\\w\\W]*?)\\]\\s*`); + const replaceValue = value.map((v) => JSON.stringify(v)).join(', '); + + if (mixinReg.test(target)) { + return target.replace(mixinReg, `\n ${key}: [${replaceValue}]`); + } + + // If the key does not exist, add the key and value to the end of the target + const targetArray = target.split('\n'); + const contentIndex = targetArray.findIndex((item) => item.includes('content:')); + const moduleIndex = targetArray.findIndex((item) => item.includes('module.exports =')); + const insertIndex = contentIndex !== -1 ? contentIndex : moduleIndex !== -1 ? moduleIndex : 0; + + key === 'content' + ? targetArray.splice(insertIndex + 1, 0, ` ${key}: [${replaceValue}],`) + : targetArray.splice(insertIndex + 1, 0, ` ${key}: [${value}],`); + + return targetArray.join('\n'); +} diff --git a/src/helpers/math-diff.ts b/src/helpers/math-diff.ts new file mode 100644 index 0000000..aa2e464 --- /dev/null +++ b/src/helpers/math-diff.ts @@ -0,0 +1,34 @@ +function matchTextScore(text: string, pattern: string) { + let score = 0; + const textLength = text.length; + const patternLength = pattern.length; + let i = 0; + let j = 0; + + while (i < textLength && j < patternLength) { + if (text[i] === pattern[j]) { + score++; + j++; + } + + i++; + } + + return score; +} + +export function findMostMatchText(list: string[], pattern: string) { + let maxScore = 0; + let result = ''; + + for (const text of list) { + const score = matchTextScore(text, pattern); + + if (score > maxScore) { + maxScore = score; + result = text; + } + } + + return result !== '' ? result : null; +} diff --git a/src/helpers/package.ts b/src/helpers/package.ts index 8c1bb82..db8e8eb 100644 --- a/src/helpers/package.ts +++ b/src/helpers/package.ts @@ -9,7 +9,7 @@ import {Logger} from './logger'; * Get the package information * @param packagePath string */ -export async function getPackageInfo(packagePath: string) { +export function getPackageInfo(packagePath: string) { let pkg; try { diff --git a/src/helpers/type.ts b/src/helpers/type.ts index 2a66e36..c054827 100644 --- a/src/helpers/type.ts +++ b/src/helpers/type.ts @@ -9,7 +9,7 @@ export type PascalCase = T extends `${infer F}-${infer R}` export type SAFE_ANY = any; export type AppendKeyValue = { - [P in keyof T | K]: P extends keyof T ? T[P] : P extends K ? V : never; + [P in keyof T | K]?: P extends keyof T ? T[P] : P extends K ? V : never; }; export type CommandName = 'init' | 'list' | 'env' | 'upgrade' | 'remove' | 'add'; diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts index 045870a..4267aa0 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -1,4 +1,4 @@ -import type {PascalCase} from './type'; +import type {PascalCase, SAFE_ANY} from './type'; import fg, {type Options} from 'fast-glob'; @@ -47,3 +47,7 @@ export function transformOption(options: boolean | 'false') { return !!options; } + +export function omit(obj: Record, keys: string[]) { + return Object.fromEntries(Object.entries(obj).filter(([key]) => !keys.includes(key))); +} diff --git a/src/index.ts b/src/index.ts index 9d49d21..2dd4847 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import {getCommandDescAndLog} from '@helpers/utils'; import pkg from '../package.json'; +import {addAction} from './actions/add-action'; import {doctorAction} from './actions/doctor-action'; import {envAction} from './actions/env-action'; import {initAction} from './actions/init-action'; @@ -32,6 +33,21 @@ nextui // .option('-p --package [string]', 'The package manager to use for the new project') .action(initAction); +nextui + .command('add') + .description('Add NextUI components to your project') + .argument('[components...]', 'The name of the NextUI components to add') + .option('-a --all [boolean]', 'Add all the NextUI components', false) + .option('-p --packagePath [string]', 'The path to the package.json file') + .option('-tw --tailwindPath [string]', 'The path to the tailwind.config file file') + .option('-app --appPath [string]', 'The path to the App.tsx file') + .option( + '--prettier [boolean]', + 'Add prettier format in the add content which required installed prettier', + false + ) + .action(addAction); + nextui .command('list') .description('List all the components status, description, version, etc') diff --git a/src/prompts/index.ts b/src/prompts/index.ts index 1ad5232..6530a44 100644 --- a/src/prompts/index.ts +++ b/src/prompts/index.ts @@ -23,6 +23,22 @@ export async function getInput(message: string, choices?: prompts.Choice[]) { return result.value; } +export async function getAutocompleteMultiselect(message: string, choices?: prompts.Choice[]) { + const result = await prompts( + { + hint: '- Space to select. Return to submit', + message, + min: 1, + name: 'value', + type: 'autocompleteMultiselect', + ...(choices ? {choices} : {}) + }, + defaultPromptOptions + ); + + return result.value; +} + export async function getSelect(message: string, choices: prompts.Choice[]) { const result = await prompts( { diff --git a/src/scripts/helpers.ts b/src/scripts/helpers.ts index aeea0ee..b12c0bc 100644 --- a/src/scripts/helpers.ts +++ b/src/scripts/helpers.ts @@ -13,6 +13,7 @@ export type Components = { docs: string; description: string; status: string; + style: string; }[]; export type ComponentsJson = { @@ -29,7 +30,7 @@ export async function updateComponents() { } // const latestVersion = await getLatestVersion('@nextui-org/react'); // TODO:(winches) Remove this after the NextUI first release - const latestVersion = '0.0.0-dev-v2-20240331183656'; + const latestVersion = '0.0.0-dev-v2-20240402181757'; const components = JSON.parse(readFileSync(COMPONENTS_PATH, 'utf-8')) as ComponentsJson; const currentVersion = components.version; @@ -73,14 +74,14 @@ const getUnpkgUrl = (version: string) => export async function autoUpdateComponents() { // TODO:(winches) Remove this after the NextUI first release - const url = getUnpkgUrl('0.0.0-dev-v2-20240331183656'); + const url = getUnpkgUrl('0.0.0-dev-v2-20240402181757'); const components = await downloadFile(url); const componentsJson = { components, // TODO:(winches) Remove this after the NextUI first release - version: '0.0.0-dev-v2-20240331183656' + version: '0.0.0-dev-v2-20240402181757' }; writeFileSync(COMPONENTS_PATH, JSON.stringify(componentsJson, null, 2), 'utf-8'); From f56697aa9cddfb4fad43df1ae7d9febae7f53432 Mon Sep 17 00:00:00 2001 From: winches <329487092@qq.com> Date: Fri, 5 Apr 2024 19:13:30 +0800 Subject: [PATCH 02/15] fix: judge repair --- src/actions/add-action.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/actions/add-action.ts b/src/actions/add-action.ts index cd38137..1e8e510 100644 --- a/src/actions/add-action.ts +++ b/src/actions/add-action.ts @@ -40,7 +40,7 @@ export async function addAction(components: string[], options: AddActionOptions) tailwindPath = findFiles('**/tailwind.config.(j|t)s')[0] } = options; - if (!components && !all) { + if (!components.length && !all) { components = await getAutocompleteMultiselect( 'Select the NextUI components to add', nextUIComponents.map((component) => { From 06a6130450d1764d2aabeb8310474bdff0e76e8d Mon Sep 17 00:00:00 2001 From: winches <329487092@qq.com> Date: Fri, 5 Apr 2024 19:29:42 +0800 Subject: [PATCH 03/15] fix: add type --- src/actions/add-action.ts | 71 +++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/src/actions/add-action.ts b/src/actions/add-action.ts index 1e8e510..e5b4de7 100644 --- a/src/actions/add-action.ts +++ b/src/actions/add-action.ts @@ -19,7 +19,7 @@ import { nextUIComponentsMap } from 'src/constants/component'; import {resolver} from 'src/constants/path'; -import {individualTailwindRequired} from 'src/constants/required'; +import {NEXT_UI, individualTailwindRequired} from 'src/constants/required'; import {tailwindTemplate} from 'src/constants/templates'; import {getAutocompleteMultiselect} from 'src/prompts'; @@ -52,44 +52,46 @@ export async function addAction(components: string[], options: AddActionOptions) }) ); } else if (all) { - components = nextUIComponentsKeys; + components = [NEXT_UI]; } /** ======================== Add judge whether illegal component exist ======================== */ - const illegalList: [string, null | string][] = []; + if (!all) { + const illegalList: [string, null | string][] = []; - for (const component of components) { - if (!nextUIComponentsKeysSet.has(component)) { - const matchComponent = findMostMatchText(nextUIComponentsKeys, component); + for (const component of components) { + if (!nextUIComponentsKeysSet.has(component)) { + const matchComponent = findMostMatchText(nextUIComponentsKeys, component); - illegalList.push([component, matchComponent]); + illegalList.push([component, matchComponent]); + } } - } - if (illegalList.length) { - const [illegalComponents, matchComponents] = illegalList.reduce( - (acc, [illegalComponent, matchComponent]) => { - return [ - acc[0] + chalk.underline(illegalComponent) + ', ', - acc[1] + (matchComponent ? chalk.underline(matchComponent) + ', ' : '') - ]; - }, - ['', ''] - ); + if (illegalList.length) { + const [illegalComponents, matchComponents] = illegalList.reduce( + (acc, [illegalComponent, matchComponent]) => { + return [ + acc[0] + chalk.underline(illegalComponent) + ', ', + acc[1] + (matchComponent ? chalk.underline(matchComponent) + ', ' : '') + ]; + }, + ['', ''] + ); - Logger.prefix( - 'error', - `Illegal NextUI components: ${illegalComponents.replace(/, $/, '')}${ - matchComponents - ? `\n${''.padEnd(12)}It may be a typo, did you mean ${matchComponents.replace( - /, $/, - '' - )}?` - : '' - }` - ); + Logger.prefix( + 'error', + `Illegal NextUI components: ${illegalComponents.replace(/, $/, '')}${ + matchComponents + ? `\n${''.padEnd(12)}It may be a typo, did you mean ${matchComponents.replace( + /, $/, + '' + )}?` + : '' + }` + ); - return; + return; + } } // Check whether have added the NextUI components @@ -120,6 +122,7 @@ export async function addAction(components: string[], options: AddActionOptions) } const currentPkgManager = await detect(); + const runCmd = currentPkgManager === 'npm' ? 'install' : 'add'; /** ======================== Step 1 Add dependencies required ======================== */ if (all) { @@ -132,7 +135,7 @@ export async function addAction(components: string[], options: AddActionOptions) .join(', ')}` ); - await exec(`${currentPkgManager} add ${[...missingDependencies].join(' ')}`); + await exec(`${currentPkgManager} ${runCmd} ${[...missingDependencies].join(' ')}`); } } else { const [, ..._missingDependencies] = checkRequiredContentInstalled( @@ -150,11 +153,7 @@ export async function addAction(components: string[], options: AddActionOptions) .join(', ')}` ); - await exec( - `${currentPkgManager} ${currentPkgManager === 'npm' ? 'install' : 'add'} ${[ - ...missingDependencies - ].join(' ')}` - ); + await exec(`${currentPkgManager} ${runCmd} ${[...missingDependencies].join(' ')}`); } /** ======================== Step 2 Tailwind CSS Setup ======================== */ From 7506fd70c940350c0985cd3e6be3376723e48939 Mon Sep 17 00:00:00 2001 From: winches <329487092@qq.com> Date: Fri, 5 Apr 2024 19:45:16 +0800 Subject: [PATCH 04/15] fix: pnpm setup --- src/actions/add-action.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/actions/add-action.ts b/src/actions/add-action.ts index e5b4de7..f2a784c 100644 --- a/src/actions/add-action.ts +++ b/src/actions/add-action.ts @@ -19,7 +19,7 @@ import { nextUIComponentsMap } from 'src/constants/component'; import {resolver} from 'src/constants/path'; -import {NEXT_UI, individualTailwindRequired} from 'src/constants/required'; +import {NEXT_UI, individualTailwindRequired, pnpmRequired} from 'src/constants/required'; import {tailwindTemplate} from 'src/constants/templates'; import {getAutocompleteMultiselect} from 'src/prompts'; @@ -194,10 +194,14 @@ export async function addAction(components: string[], options: AddActionOptions) if (currentPkgManager === 'pnpm') { const npmrcPath = resolver('.npmrc'); - const [isCorrectPnpm] = checkPnpm(npmrcPath); + if (!npmrcPath) { + writeFileSync(resolver('.npmrc'), pnpmRequired.content, 'utf-8'); + } else { + const [isCorrectPnpm] = checkPnpm(npmrcPath); - if (!isCorrectPnpm) { - fixPnpm(npmrcPath); + if (!isCorrectPnpm) { + fixPnpm(npmrcPath); + } } } From 350e26e9aa2c146a6abee059358ebd71cb3d3c97 Mon Sep 17 00:00:00 2001 From: winches <329487092@qq.com> Date: Fri, 5 Apr 2024 19:54:59 +0800 Subject: [PATCH 05/15] fix: update dep --- src/actions/add-action.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/actions/add-action.ts b/src/actions/add-action.ts index f2a784c..4401167 100644 --- a/src/actions/add-action.ts +++ b/src/actions/add-action.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-var */ import type {SAFE_ANY} from '@helpers/type'; import {writeFileSync} from 'fs'; @@ -95,7 +96,7 @@ export async function addAction(components: string[], options: AddActionOptions) } // Check whether have added the NextUI components - const {allDependenciesKeys, currentComponents} = getPackageInfo(packagePath); + var {allDependenciesKeys, currentComponents} = getPackageInfo(packagePath); const currentComponentsKeys = currentComponents.map((c) => c.name); const filterCurrentComponents = components.filter((c) => currentComponentsKeys.includes(c)); @@ -156,6 +157,9 @@ export async function addAction(components: string[], options: AddActionOptions) await exec(`${currentPkgManager} ${runCmd} ${[...missingDependencies].join(' ')}`); } + // After install the required dependencies, get the latest package information + var {allDependenciesKeys, currentComponents} = getPackageInfo(packagePath); + /** ======================== Step 2 Tailwind CSS Setup ======================== */ const type: SAFE_ANY = all ? 'all' : 'partial'; From 4bcf3793c4f58dd422e365f55e829cd19bc316db Mon Sep 17 00:00:00 2001 From: winches <329487092@qq.com> Date: Fri, 5 Apr 2024 19:58:07 +0800 Subject: [PATCH 06/15] fix: partial tailwind output --- src/constants/templates.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants/templates.ts b/src/constants/templates.ts index 00f7b18..1c58222 100644 --- a/src/constants/templates.ts +++ b/src/constants/templates.ts @@ -31,7 +31,7 @@ const {nextui} = require("@nextui-org/theme"); /** @type {import('tailwindcss').Config} */ module.exports = { content: [ - ${content}, + ${JSON.stringify(content)}, ], theme: { extend: {}, From ebde8b878d5af0a4dc13cfc4668d1acd48142389 Mon Sep 17 00:00:00 2001 From: winches <329487092@qq.com> Date: Fri, 5 Apr 2024 19:59:33 +0800 Subject: [PATCH 07/15] fix: pnpm setup problem --- src/actions/add-action.ts | 4 ++-- src/helpers/fix.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/actions/add-action.ts b/src/actions/add-action.ts index 4401167..31bd1ad 100644 --- a/src/actions/add-action.ts +++ b/src/actions/add-action.ts @@ -1,7 +1,7 @@ /* eslint-disable no-var */ import type {SAFE_ANY} from '@helpers/type'; -import {writeFileSync} from 'fs'; +import {existsSync, writeFileSync} from 'fs'; import chalk from 'chalk'; @@ -198,7 +198,7 @@ export async function addAction(components: string[], options: AddActionOptions) if (currentPkgManager === 'pnpm') { const npmrcPath = resolver('.npmrc'); - if (!npmrcPath) { + if (!existsSync(npmrcPath)) { writeFileSync(resolver('.npmrc'), pnpmRequired.content, 'utf-8'); } else { const [isCorrectPnpm] = checkPnpm(npmrcPath); diff --git a/src/helpers/fix.ts b/src/helpers/fix.ts index 79a8849..42f56d6 100644 --- a/src/helpers/fix.ts +++ b/src/helpers/fix.ts @@ -103,7 +103,7 @@ function transformErrorInfo(errorInfo: string): [keyof typeof tailwindRequired, export function fixPnpm(npmrcPath: string, write = true, runInstall = true) { let content = readFileSync(npmrcPath, 'utf-8'); - content = `${pnpmRequired.content}\n${npmrcPath}`; + content = `${pnpmRequired.content}\n${content}`; write && writeFileSync(npmrcPath, content, 'utf-8'); Logger.newLine(); From 17761db30100d8cec76250540c5cb5aa6fa3f204 Mon Sep 17 00:00:00 2001 From: winches <329487092@qq.com> Date: Fri, 5 Apr 2024 20:10:10 +0800 Subject: [PATCH 08/15] fix: output-info type error --- src/helpers/output-info.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/helpers/output-info.ts b/src/helpers/output-info.ts index d275e64..4a578fc 100644 --- a/src/helpers/output-info.ts +++ b/src/helpers/output-info.ts @@ -3,6 +3,7 @@ import type {CommandName} from './type'; import chalk from 'chalk'; import { + type NextUIComponent, type NextUIComponents, colorNextUIComponentKeys, orderNextUIComponentKeys @@ -42,12 +43,13 @@ export function outputComponents( return; } - const componentKeyLengthMap: Record = { + const componentKeyLengthMap: Record = { description: 0, docs: 0, name: 0, package: 0, status: 0, + style: 0, version: 0 }; From 746ec0932efba4f1e070f8637905f45538fc9438 Mon Sep 17 00:00:00 2001 From: winches <329487092@qq.com> Date: Mon, 8 Apr 2024 12:16:50 +0800 Subject: [PATCH 09/15] fix: review problem --- src/actions/add-action.ts | 91 ++++++++++++++++++++++++++++++--------- src/constants/required.ts | 7 +-- src/helpers/fix.ts | 22 +++++++++- src/helpers/match.ts | 11 +++-- src/index.ts | 1 + 5 files changed, 104 insertions(+), 28 deletions(-) diff --git a/src/actions/add-action.ts b/src/actions/add-action.ts index 31bd1ad..978a987 100644 --- a/src/actions/add-action.ts +++ b/src/actions/add-action.ts @@ -20,7 +20,12 @@ import { nextUIComponentsMap } from 'src/constants/component'; import {resolver} from 'src/constants/path'; -import {NEXT_UI, individualTailwindRequired, pnpmRequired} from 'src/constants/required'; +import { + DOCS_PROVIDER_SETUP, + NEXT_UI, + individualTailwindRequired, + pnpmRequired +} from 'src/constants/required'; import {tailwindTemplate} from 'src/constants/templates'; import {getAutocompleteMultiselect} from 'src/prompts'; @@ -30,10 +35,12 @@ interface AddActionOptions { packagePath?: string; tailwindPath?: string; appPath?: string; + addApp?: boolean; } export async function addAction(components: string[], options: AddActionOptions) { const { + addApp = false, all = false, appPath = findFiles('**/App.(j|t)sx')[0], packagePath = resolver('package.json'), @@ -41,16 +48,43 @@ export async function addAction(components: string[], options: AddActionOptions) tailwindPath = findFiles('**/tailwind.config.(j|t)s')[0] } = options; + var {allDependenciesKeys, currentComponents} = getPackageInfo(packagePath); + + // Check whether the user has installed the All NextUI components + if (allDependenciesKeys.has(NEXT_UI)) { + Logger.prefix( + 'error', + `❌ You have installed all the NextUI components (@nextui-org/react)\nYou can use 'nextui list' to view the current installed components` + ); + + // Check whether have added redundant dependencies + if (currentComponents.length) { + Logger.newLine(); + Logger.warn('You have installed redundant dependencies, please remove them'); + Logger.warn('The redundant dependencies are:'); + currentComponents.forEach((component) => { + Logger.info(`- ${component.package}`); + }); + } + + return; + } + if (!components.length && !all) { components = await getAutocompleteMultiselect( 'Select the NextUI components to add', - nextUIComponents.map((component) => { - return { - description: component.description, - title: component.name, - value: component.name - }; - }) + nextUIComponents + .filter( + (component) => + !currentComponents.some((currentComponent) => currentComponent.name === component.name) + ) + .map((component) => { + return { + description: component.description, + title: component.name, + value: component.name + }; + }) ); } else if (all) { components = [NEXT_UI]; @@ -96,8 +130,6 @@ export async function addAction(components: string[], options: AddActionOptions) } // Check whether have added the NextUI components - var {allDependenciesKeys, currentComponents} = getPackageInfo(packagePath); - const currentComponentsKeys = currentComponents.map((c) => c.name); const filterCurrentComponents = components.filter((c) => currentComponentsKeys.includes(c)); @@ -113,7 +145,7 @@ export async function addAction(components: string[], options: AddActionOptions) } // Check whether the App.tsx file exists - if (!appPath) { + if (addApp && !appPath) { Logger.prefix( 'error', "❌ Cannot find the App.(j|t)sx file\nYou should specify appPath through 'add --appPath=yourAppPath'" @@ -183,15 +215,17 @@ export async function addAction(components: string[], options: AddActionOptions) Logger.info(`Added the required tailwind content in file: ${tailwindPath}`); } - /** ======================== Step 3 Provider ======================== */ - const [isCorrectProvider] = checkApp(type, appPath); + /** ======================== Step 3 Provider Need Manually Open ======================== */ + if (addApp && appPath && existsSync(appPath)) { + const [isCorrectProvider] = checkApp(type, appPath); - if (!isCorrectProvider) { - fixProvider(appPath, {format: prettier}); + if (!isCorrectProvider) { + fixProvider(appPath, {format: prettier}); - Logger.newLine(); - Logger.info(`Added the NextUIProvider in file: ${appPath}`); - Logger.warn('You need to check the NextUIProvider whether in the correct place'); + Logger.newLine(); + Logger.info(`Added the NextUIProvider in file: ${appPath}`); + Logger.warn('You need to check the NextUIProvider whether in the correct place'); + } } /** ======================== Step 4 Setup Pnpm ======================== */ @@ -211,7 +245,24 @@ export async function addAction(components: string[], options: AddActionOptions) // Finish adding the NextUI components Logger.newLine(); - Logger.success( - '✅ All the NextUI components have been added\nNow you can use the component you installed in your application' + Logger.success('✅ All the NextUI components have been added\n'); + + // Warn the user to check the NextUIProvider whether in the correct place + Logger.warn( + `Please check the ${chalk.bold( + 'NextUIProvider' + )} whether in the correct place (ignore if added)\nSee more info here: ${DOCS_PROVIDER_SETUP}` ); + + // Add warn check when installed all the NextUI components + if (all && currentComponents.length) { + Logger.newLine(); + Logger.warn('You have installed redundant dependencies, please remove them'); + Logger.warn('The redundant dependencies are:'); + currentComponents.forEach((component) => { + Logger.info(`- ${component.package}`); + }); + } + + return; } diff --git a/src/constants/required.ts b/src/constants/required.ts index d650854..36ed052 100644 --- a/src/constants/required.ts +++ b/src/constants/required.ts @@ -19,10 +19,11 @@ export const DOCS_TAILWINDCSS_SETUP = 'https://nextui.org/docs/guide/installation#tailwind-css-setup'; export const DOCS_APP_SETUP = 'https://nextui.org/docs/guide/installation#provider-setup'; export const DOCS_PNPM_SETUP = 'https://nextui.org/docs/guide/installation#setup-pnpm-optional'; +export const DOCS_PROVIDER_SETUP = 'https://nextui.org/docs/guide/installation#provider-setup'; // Record the required content of tailwind.config file export const tailwindRequired = { - content: '@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}', + content: './node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}', darkMode: 'darkMode: "class"', plugins: 'nextui()' } as const; @@ -44,7 +45,7 @@ export const individualTailwindRequired = { ]; if (outputComponents.length === 1) { - return `@nextui-org/theme/dist/components/${currentComponents[0]}.js`; + return `./node_modules/@nextui-org/theme/dist/components/${currentComponents[0]}.js`; } const requiredContent = outputComponents .reduce((acc, component) => { @@ -52,7 +53,7 @@ export const individualTailwindRequired = { }, '') .replace(/\|$/, ''); - return `@nextui-org/theme/dist/components/(${requiredContent}).js`; + return `./node_modules/@nextui-org/theme/dist/components/(${requiredContent}).js`; }, plugins: 'nextui()' } as const; diff --git a/src/helpers/fix.ts b/src/helpers/fix.ts index 42f56d6..d25f709 100644 --- a/src/helpers/fix.ts +++ b/src/helpers/fix.ts @@ -53,15 +53,33 @@ export function fixTailwind(type: CheckType, options: FixTailwind) { } let tailwindContent = readFileSync(tailwindPath, 'utf-8'); - const contentMatch = getMatchArray('content', tailwindContent); + let contentMatch = getMatchArray('content', tailwindContent); const pluginsMatch = getMatchArray('plugins', tailwindContent); for (const errorInfo of errorInfoList) { const [errorType, info] = transformErrorInfo(errorInfo); if (errorType === 'content') { + contentMatch = contentMatch.filter((content) => !content.includes('@nextui-org/theme/dist/')); contentMatch.push(info); - tailwindContent = replaceMatchArray('content', tailwindContent, contentMatch); + tailwindContent = replaceMatchArray( + 'content', + tailwindContent, + contentMatch, + contentMatch + .map((v, index, arr) => { + // Add 4 spaces before the content + if (index === 0) { + return `\n ${JSON.stringify(v)}`; + } + if (arr.length - 1 === index) { + return ` ${JSON.stringify(v)}\n`; + } + + return ` ${JSON.stringify(v)}`; + }) + .join(',\n') + ); } else if (errorType === 'plugins') { pluginsMatch.push(info); tailwindContent = replaceMatchArray('plugins', tailwindContent, pluginsMatch); diff --git a/src/helpers/match.ts b/src/helpers/match.ts index 8d2bd00..c7d4947 100644 --- a/src/helpers/match.ts +++ b/src/helpers/match.ts @@ -37,7 +37,7 @@ export function getMatchArray(key: string, target: string) { target .match(mixinReg)?.[1] ?.split(/,\n/) - .map((i) => i.trim()) + .map((i) => i.trim().replace(/[`'"]/g, '')) .filter(Boolean) ?? [] ); @@ -51,9 +51,14 @@ export function getMatchArray(key: string, target: string) { * @param target * @param value */ -export function replaceMatchArray(key: string, target: string, value: string[]) { +export function replaceMatchArray( + key: string, + target: string, + value: string[], + _replaceValue?: string +) { const mixinReg = new RegExp(`\\s*${key}:\\s\\[([\\w\\W]*?)\\]\\s*`); - const replaceValue = value.map((v) => JSON.stringify(v)).join(', '); + const replaceValue = _replaceValue ?? value.map((v) => JSON.stringify(v)).join(', '); if (mixinReg.test(target)) { return target.replace(mixinReg, `\n ${key}: [${replaceValue}]`); diff --git a/src/index.ts b/src/index.ts index 2dd4847..6542e9e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -46,6 +46,7 @@ nextui 'Add prettier format in the add content which required installed prettier', false ) + .option('--addApp [boolean]', 'Add App.tsx file content which required provider', false) .action(addAction); nextui From d86f33ab4c45bbc7a044ae39490c4b190af8084b Mon Sep 17 00:00:00 2001 From: winches <329487092@qq.com> Date: Mon, 8 Apr 2024 22:11:33 +0800 Subject: [PATCH 10/15] fix: add command remove error about add and redundant --- src/actions/add-action.ts | 7 ------- src/helpers/fix.ts | 9 +++++++++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/actions/add-action.ts b/src/actions/add-action.ts index 978a987..49e51d4 100644 --- a/src/actions/add-action.ts +++ b/src/actions/add-action.ts @@ -52,11 +52,6 @@ export async function addAction(components: string[], options: AddActionOptions) // Check whether the user has installed the All NextUI components if (allDependenciesKeys.has(NEXT_UI)) { - Logger.prefix( - 'error', - `❌ You have installed all the NextUI components (@nextui-org/react)\nYou can use 'nextui list' to view the current installed components` - ); - // Check whether have added redundant dependencies if (currentComponents.length) { Logger.newLine(); @@ -66,8 +61,6 @@ export async function addAction(components: string[], options: AddActionOptions) Logger.info(`- ${component.package}`); }); } - - return; } if (!components.length && !all) { diff --git a/src/helpers/fix.ts b/src/helpers/fix.ts index d25f709..c94ca6d 100644 --- a/src/helpers/fix.ts +++ b/src/helpers/fix.ts @@ -60,6 +60,11 @@ export function fixTailwind(type: CheckType, options: FixTailwind) { const [errorType, info] = transformErrorInfo(errorInfo); if (errorType === 'content') { + // Check if all the required content is added then skip + const allPublic = contentMatch.includes(tailwindRequired.content); + + if (allPublic) continue; + contentMatch = contentMatch.filter((content) => !content.includes('@nextui-org/theme/dist/')); contentMatch.push(info); tailwindContent = replaceMatchArray( @@ -70,6 +75,10 @@ export function fixTailwind(type: CheckType, options: FixTailwind) { .map((v, index, arr) => { // Add 4 spaces before the content if (index === 0) { + if (arr.length === 1) { + return `\n ${JSON.stringify(v)}\n`; + } + return `\n ${JSON.stringify(v)}`; } if (arr.length - 1 === index) { From f24b22f859cd6556120a741903d7a216cb145c68 Mon Sep 17 00:00:00 2001 From: winches <329487092@qq.com> Date: Mon, 8 Apr 2024 22:14:47 +0800 Subject: [PATCH 11/15] fix: add command warn logger optimize --- src/actions/add-action.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/actions/add-action.ts b/src/actions/add-action.ts index 49e51d4..d0551e2 100644 --- a/src/actions/add-action.ts +++ b/src/actions/add-action.ts @@ -55,7 +55,9 @@ export async function addAction(components: string[], options: AddActionOptions) // Check whether have added redundant dependencies if (currentComponents.length) { Logger.newLine(); - Logger.warn('You have installed redundant dependencies, please remove them'); + Logger.warn( + 'You do not need the `@nextui-org/react` package when using individual components\nWe suggest to use individual components for smaller bundle sizes' + ); Logger.warn('The redundant dependencies are:'); currentComponents.forEach((component) => { Logger.info(`- ${component.package}`); From d416f888b20053759ac8c83aaba2c87dcebb5271 Mon Sep 17 00:00:00 2001 From: winches <329487092@qq.com> Date: Mon, 8 Apr 2024 22:16:15 +0800 Subject: [PATCH 12/15] fix: remove redundant return command --- src/actions/add-action.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/actions/add-action.ts b/src/actions/add-action.ts index d0551e2..700c037 100644 --- a/src/actions/add-action.ts +++ b/src/actions/add-action.ts @@ -258,6 +258,4 @@ export async function addAction(components: string[], options: AddActionOptions) Logger.info(`- ${component.package}`); }); } - - return; } From 4dc1fd0616fc7628027d5f81ae9bfd01d6e8a11e Mon Sep 17 00:00:00 2001 From: winches <329487092@qq.com> Date: Mon, 8 Apr 2024 22:23:45 +0800 Subject: [PATCH 13/15] fix: optimize logger --- src/actions/add-action.ts | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/src/actions/add-action.ts b/src/actions/add-action.ts index 700c037..5d11d85 100644 --- a/src/actions/add-action.ts +++ b/src/actions/add-action.ts @@ -50,21 +50,6 @@ export async function addAction(components: string[], options: AddActionOptions) var {allDependenciesKeys, currentComponents} = getPackageInfo(packagePath); - // Check whether the user has installed the All NextUI components - if (allDependenciesKeys.has(NEXT_UI)) { - // Check whether have added redundant dependencies - if (currentComponents.length) { - Logger.newLine(); - Logger.warn( - 'You do not need the `@nextui-org/react` package when using individual components\nWe suggest to use individual components for smaller bundle sizes' - ); - Logger.warn('The redundant dependencies are:'); - currentComponents.forEach((component) => { - Logger.info(`- ${component.package}`); - }); - } - } - if (!components.length && !all) { components = await getAutocompleteMultiselect( 'Select the NextUI components to add', @@ -249,10 +234,13 @@ export async function addAction(components: string[], options: AddActionOptions) )} whether in the correct place (ignore if added)\nSee more info here: ${DOCS_PROVIDER_SETUP}` ); - // Add warn check when installed all the NextUI components - if (all && currentComponents.length) { + // Check whether the user has installed the All NextUI components + if ((allDependenciesKeys.has(NEXT_UI) || all) && currentComponents.length) { + // Check whether have added redundant dependencies Logger.newLine(); - Logger.warn('You have installed redundant dependencies, please remove them'); + Logger.warn( + 'You do not need the `@nextui-org/react` package when using individual components\nWe suggest to use individual components for smaller bundle sizes' + ); Logger.warn('The redundant dependencies are:'); currentComponents.forEach((component) => { Logger.info(`- ${component.package}`); From e0154573a11e10b064f6fd43421bffe9853393c5 Mon Sep 17 00:00:00 2001 From: winches <329487092@qq.com> Date: Mon, 8 Apr 2024 22:44:28 +0800 Subject: [PATCH 14/15] fix: optimize all option tailwind log --- src/actions/add-action.ts | 6 +++++- src/helpers/fix.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/actions/add-action.ts b/src/actions/add-action.ts index 5d11d85..5a68224 100644 --- a/src/actions/add-action.ts +++ b/src/actions/add-action.ts @@ -111,7 +111,9 @@ export async function addAction(components: string[], options: AddActionOptions) // Check whether have added the NextUI components const currentComponentsKeys = currentComponents.map((c) => c.name); - const filterCurrentComponents = components.filter((c) => currentComponentsKeys.includes(c)); + const filterCurrentComponents = components.filter( + (c) => currentComponentsKeys.includes(c) || allDependenciesKeys[c] + ); if (filterCurrentComponents.length) { Logger.prefix( @@ -246,4 +248,6 @@ export async function addAction(components: string[], options: AddActionOptions) Logger.info(`- ${component.package}`); }); } + + process.exit(0); } diff --git a/src/helpers/fix.ts b/src/helpers/fix.ts index c94ca6d..7968d66 100644 --- a/src/helpers/fix.ts +++ b/src/helpers/fix.ts @@ -82,7 +82,7 @@ export function fixTailwind(type: CheckType, options: FixTailwind) { return `\n ${JSON.stringify(v)}`; } if (arr.length - 1 === index) { - return ` ${JSON.stringify(v)}\n`; + return ` ${JSON.stringify(v)}\n `; } return ` ${JSON.stringify(v)}`; From 18a0d891dff841d446fd1bc6fe74e997d3160785 Mon Sep 17 00:00:00 2001 From: winches <329487092@qq.com> Date: Thu, 11 Apr 2024 15:45:58 +0800 Subject: [PATCH 15/15] fix: tailwind plugin content error --- src/helpers/match.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/helpers/match.ts b/src/helpers/match.ts index c7d4947..462613c 100644 --- a/src/helpers/match.ts +++ b/src/helpers/match.ts @@ -61,7 +61,9 @@ export function replaceMatchArray( const replaceValue = _replaceValue ?? value.map((v) => JSON.stringify(v)).join(', '); if (mixinReg.test(target)) { - return target.replace(mixinReg, `\n ${key}: [${replaceValue}]`); + const _value = key === 'content' ? `\n ${key}: [${replaceValue}]` : `\n ${key}: [${value}]`; + + return target.replace(mixinReg, _value); } // If the key does not exist, add the key and value to the end of the target