From d5931e8eefad1cf115da541cc2a5fc912aae7d8e Mon Sep 17 00:00:00 2001 From: winches <329487092@qq.com> Date: Sat, 27 Apr 2024 01:35:30 +0800 Subject: [PATCH] feat: upgrade command add peerDep upgrade (#30) * feat: upgrade command add peerDep upgrade * feat: optimize upgrade peer output * feat: optimize upgrade all dep output * feat: optimize upgrade losing to missing * fix: ignore lose * fix: some optimize * refactor: upgrade command all option include others components (#42) * feat: update components.json * feat: typo * fix: output lose alignment * fix: output component lose alignment * fix: upgrade error about all select * feat: add peerDep warn about min version required * feat: get the missing dep inside and add install * feat: optimize output log --- src/actions/upgrade-action.ts | 74 +++++++-- src/constants/components.json | 86 +++++----- src/helpers/exec.ts | 13 +- src/helpers/output-info.ts | 1 + src/helpers/package.ts | 42 +++-- src/helpers/remove.ts | 7 +- src/helpers/upgrade.ts | 288 ++++++++++++++++++++++++++++------ src/helpers/utils.ts | 6 +- src/prompts/index.ts | 16 ++ src/scripts/helpers.ts | 9 +- 10 files changed, 409 insertions(+), 133 deletions(-) diff --git a/src/actions/upgrade-action.ts b/src/actions/upgrade-action.ts index eb3855a..ba6a35b 100644 --- a/src/actions/upgrade-action.ts +++ b/src/actions/upgrade-action.ts @@ -6,6 +6,7 @@ import {checkIllegalComponents} from '@helpers/check'; import {detect} from '@helpers/detect'; import {exec} from '@helpers/exec'; import {Logger} from '@helpers/logger'; +import {colorMatchRegex} from '@helpers/output-info'; import {getPackageInfo} from '@helpers/package'; import {upgrade} from '@helpers/upgrade'; import {getColorVersion, getPackageManagerInfo} from '@helpers/utils'; @@ -13,8 +14,8 @@ import {type NextUIComponents} from 'src/constants/component'; import {resolver} from 'src/constants/path'; import {NEXT_UI} from 'src/constants/required'; import {store} from 'src/constants/store'; -import {getAutocompleteMultiselect, getSelect} from 'src/prompts'; -import {getLatestVersion} from 'src/scripts/helpers'; +import {getAutocompleteMultiselect, getMultiselect, getSelect} from 'src/prompts'; +import {compareVersions, getLatestVersion} from 'src/scripts/helpers'; interface UpgradeActionOptions { packagePath?: string; @@ -41,7 +42,7 @@ export async function upgradeAction(components: string[], options: UpgradeAction transformComponents.push({ ...component, - isLatest: component.version === latestVersion, + isLatest: compareVersions(component.version, latestVersion) >= 0, latestVersion }); } @@ -53,9 +54,7 @@ export async function upgradeAction(components: string[], options: UpgradeAction return; } - if (isNextUIAll) { - components = [NEXT_UI]; - } else if (all) { + if (all) { components = currentComponents.map((component) => component.package); } else if (!components.length) { components = await getAutocompleteMultiselect( @@ -95,11 +94,13 @@ export async function upgradeAction(components: string[], options: UpgradeAction /** ======================== Upgrade ======================== */ const upgradeOptionList = transformComponents.filter((c) => components.includes(c.package)); - const result = await upgrade({ + let result = await upgrade({ + all, allDependencies, isNextUIAll, upgradeOptionList }); + let ignoreList: string[] = []; if (result.length) { const isExecute = await getSelect('Would you like to proceed with the upgrade?', [ @@ -107,19 +108,60 @@ export async function upgradeAction(components: string[], options: UpgradeAction title: 'Yes', value: true }, - {title: 'No', value: false} + { + description: 'Select this if you wish to exclude certain packages from the upgrade', + title: 'No', + value: false + } ]); - if (isExecute) { - const packageManager = await detect(); - const {install} = getPackageManagerInfo(packageManager); - - await exec( - `${packageManager} ${install} ${result.reduce((acc, component) => { - return `${acc} ${component.package}@${component.latestVersion}`; - }, '')}` + const packageManager = await detect(); + const {install} = getPackageManagerInfo(packageManager); + + if (!isExecute) { + // Ask whether need to remove some package not to upgrade + const isNeedRemove = await getSelect( + 'Would you like to exclude any packages from the upgrade?', + [ + { + description: 'Select this to choose packages to exclude', + title: 'Yes', + value: true + }, + { + description: 'Select this to proceed without excluding any packages', + title: 'No', + value: false + } + ] ); + + if (isNeedRemove) { + ignoreList = await getMultiselect( + 'Select the packages you want to exclude from the upgrade:', + result.map((c) => { + return { + description: `${c.version} -> ${getColorVersion(c.version, c.latestVersion)}`, + title: c.package, + value: c.package + }; + }) + ); + } } + + // Remove the components that need to be ignored + result = result.filter((r) => { + return !ignoreList.some((ignore) => r.package === ignore); + }); + + await exec( + `${packageManager} ${install} ${result.reduce((acc, component, index) => { + return `${acc}${index === 0 ? '' : ' '}${ + component.package + }@${component.latestVersion.replace(colorMatchRegex, '')}`; + }, '')}` + ); } Logger.newLine(); diff --git a/src/constants/components.json b/src/constants/components.json index 3cdd9a1..2c6f41e 100644 --- a/src/constants/components.json +++ b/src/constants/components.json @@ -3,7 +3,7 @@ { "name": "accordion", "package": "@nextui-org/accordion", - "version": "2.0.29", + "version": "2.0.32", "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", @@ -20,7 +20,7 @@ { "name": "autocomplete", "package": "@nextui-org/autocomplete", - "version": "2.0.11", + "version": "2.0.16", "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": "stable", @@ -37,7 +37,7 @@ { "name": "avatar", "package": "@nextui-org/avatar", - "version": "2.0.25", + "version": "2.0.27", "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", @@ -53,7 +53,7 @@ { "name": "badge", "package": "@nextui-org/badge", - "version": "2.0.25", + "version": "2.0.27", "docs": "https://nextui.org/docs/components/badge", "description": "Badges are used as a small numerical value or status descriptor for UI elements.", "status": "stable", @@ -68,7 +68,7 @@ { "name": "breadcrumbs", "package": "@nextui-org/breadcrumbs", - "version": "2.0.5", + "version": "2.0.7", "docs": "https://nextui.org/docs/components/breadcrumbs", "description": "Breadcrumbs display a hierarchy of links to the current page or resource in an application.", "status": "stable", @@ -84,7 +84,7 @@ { "name": "button", "package": "@nextui-org/button", - "version": "2.0.28", + "version": "2.0.31", "docs": "https://nextui.org/docs/components/button", "description": "Buttons allow users to perform actions and choose with a single tap.", "status": "stable", @@ -101,7 +101,7 @@ { "name": "calendar", "package": "@nextui-org/calendar", - "version": "2.0.1", + "version": "2.0.4", "docs": "https://nextui.org/docs/components/calendar", "description": "A calendar displays one or more date grids and allows users to select a single date.", "status": "new", @@ -117,7 +117,7 @@ { "name": "card", "package": "@nextui-org/card", - "version": "2.0.25", + "version": "2.0.28", "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", @@ -134,23 +134,23 @@ { "name": "checkbox", "package": "@nextui-org/checkbox", - "version": "2.0.26", + "version": "2.0.29", "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", "style": "", "peerDependencies": { + "@nextui-org/system": ">=2.0.0", + "@nextui-org/theme": ">=2.1.0", "react": ">=18", "react-dom": ">=18", - "@nextui-org/theme": ">=2.1.0", - "@nextui-org/system": ">=2.0.0", "tailwindcss": ">=3.4.0" } }, { "name": "chip", "package": "@nextui-org/chip", - "version": "2.0.26", + "version": "2.0.28", "docs": "https://nextui.org/docs/components/chip", "description": "Chips help people enter information, make selections, filter content, or trigger actions.", "status": "stable", @@ -166,7 +166,7 @@ { "name": "code", "package": "@nextui-org/code", - "version": "2.0.25", + "version": "2.0.27", "docs": "https://nextui.org/docs/components/code", "description": "Code is a component used to display inline code.", "status": "stable", @@ -181,7 +181,7 @@ { "name": "date-input", "package": "@nextui-org/date-input", - "version": "2.0.1", + "version": "2.0.3", "docs": "https://nextui.org/docs/components/date-input", "description": "A date input allows users to enter and edit date and time values using a keyboard.", "status": "new", @@ -197,7 +197,7 @@ { "name": "date-picker", "package": "@nextui-org/date-picker", - "version": "2.0.1", + "version": "2.0.7", "docs": "https://nextui.org/docs/components/date-picker", "description": "A date picker combines a DateInput and a Calendar popover to allow users to enter or select a date and time value.", "status": "new", @@ -213,7 +213,7 @@ { "name": "divider", "package": "@nextui-org/divider", - "version": "2.0.26", + "version": "2.0.27", "docs": "https://nextui.org/docs/components/divider", "description": ". A separator is a visual divider between two groups of content", "status": "stable", @@ -228,7 +228,7 @@ { "name": "dropdown", "package": "@nextui-org/dropdown", - "version": "2.1.18", + "version": "2.1.23", "docs": "https://nextui.org/docs/components/dropdown", "description": "A dropdown displays a list of actions or options that a user can choose.", "status": "stable", @@ -245,7 +245,7 @@ { "name": "image", "package": "@nextui-org/image", - "version": "2.0.25", + "version": "2.0.27", "docs": "https://nextui.org/docs/components/image", "description": "A simple image component", "status": "stable", @@ -261,7 +261,7 @@ { "name": "input", "package": "@nextui-org/input", - "version": "2.1.18", + "version": "2.1.21", "docs": "https://nextui.org/docs/components/input", "description": "The input component is designed for capturing user input within a text field.", "status": "stable", @@ -277,7 +277,7 @@ { "name": "kbd", "package": "@nextui-org/kbd", - "version": "2.0.26", + "version": "2.0.28", "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", @@ -292,7 +292,7 @@ { "name": "link", "package": "@nextui-org/link", - "version": "2.0.27", + "version": "2.0.29", "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": "stable", @@ -308,7 +308,7 @@ { "name": "listbox", "package": "@nextui-org/listbox", - "version": "2.1.17", + "version": "2.1.19", "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": "stable", @@ -324,7 +324,7 @@ { "name": "menu", "package": "@nextui-org/menu", - "version": "2.0.18", + "version": "2.0.22", "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", @@ -340,7 +340,7 @@ { "name": "modal", "package": "@nextui-org/modal", - "version": "2.0.30", + "version": "2.0.33", "docs": "https://nextui.org/docs/components/modal", "description": "Displays a dialog with a custom content that requires attention or provides additional information.", "status": "stable", @@ -357,7 +357,7 @@ { "name": "navbar", "package": "@nextui-org/navbar", - "version": "2.0.28", + "version": "2.0.30", "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", @@ -374,7 +374,7 @@ { "name": "pagination", "package": "@nextui-org/pagination", - "version": "2.0.28", + "version": "2.0.30", "docs": "https://nextui.org/docs/components/pagination", "description": "The Pagination component allows you to display active page and navigate between multiple pages.", "status": "stable", @@ -390,7 +390,7 @@ { "name": "popover", "package": "@nextui-org/popover", - "version": "2.1.16", + "version": "2.1.21", "docs": "https://nextui.org/docs/components/popover", "description": "A popover is an overlay element positioned relative to a trigger.", "status": "stable", @@ -407,7 +407,7 @@ { "name": "progress", "package": "@nextui-org/progress", - "version": "2.0.26", + "version": "2.0.28", "docs": "https://nextui.org/docs/components/progress", "description": "Progress bars show either determinate or indeterminate progress of an operation over time.", "status": "stable", @@ -423,7 +423,7 @@ { "name": "radio", "package": "@nextui-org/radio", - "version": "2.0.26", + "version": "2.0.28", "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", @@ -439,7 +439,7 @@ { "name": "ripple", "package": "@nextui-org/ripple", - "version": "2.0.25", + "version": "2.0.28", "docs": "https://nextui.org/docs/components/ripple", "description": "A simple implementation to display a ripple animation when the source component is clicked", "status": "stable", @@ -456,7 +456,7 @@ { "name": "scroll-shadow", "package": "@nextui-org/scroll-shadow", - "version": "2.1.14", + "version": "2.1.16", "docs": "https://nextui.org/docs/components/scroll-shadow", "description": "A component that applies top and bottom shadows when content overflows on scroll.", "status": "stable", @@ -472,7 +472,7 @@ { "name": "select", "package": "@nextui-org/select", - "version": "2.1.22", + "version": "2.1.27", "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", @@ -489,7 +489,7 @@ { "name": "skeleton", "package": "@nextui-org/skeleton", - "version": "2.0.25", + "version": "2.0.27", "docs": "https://nextui.org/docs/components/skeleton", "description": "Skeleton is used to display the loading state of some component.", "status": "stable", @@ -504,7 +504,7 @@ { "name": "slider", "package": "@nextui-org/slider", - "version": "2.2.7", + "version": "2.2.9", "docs": "https://nextui.org/docs/components/slider", "description": "A slider allows a user to select one or more values within a range.", "status": "stable", @@ -520,7 +520,7 @@ { "name": "snippet", "package": "@nextui-org/snippet", - "version": "2.0.32", + "version": "2.0.35", "docs": "https://nextui.org/docs/components/snippet", "description": "Display a snippet of copyable code for the command line.", "status": "stable", @@ -537,7 +537,7 @@ { "name": "spacer", "package": "@nextui-org/spacer", - "version": "2.0.25", + "version": "2.0.27", "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", @@ -552,7 +552,7 @@ { "name": "spinner", "package": "@nextui-org/spinner", - "version": "2.0.26", + "version": "2.0.28", "docs": "https://nextui.org/docs/components/spinner", "description": "Loaders express an unspecified wait time or display the length of a process.", "status": "stable", @@ -567,7 +567,7 @@ { "name": "switch", "package": "@nextui-org/switch", - "version": "2.0.26", + "version": "2.0.28", "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", @@ -583,7 +583,7 @@ { "name": "table", "package": "@nextui-org/table", - "version": "2.0.29", + "version": "2.0.33", "docs": "https://nextui.org/docs/components/table", "description": "Tables are used to display tabular data using rows and columns. ", "status": "stable", @@ -599,7 +599,7 @@ { "name": "tabs", "package": "@nextui-org/tabs", - "version": "2.0.27", + "version": "2.0.29", "docs": "https://nextui.org/docs/components/tabs", "description": "Tabs organize content into multiple sections and allow users to navigate between them.", "status": "updated", @@ -616,7 +616,7 @@ { "name": "tooltip", "package": "@nextui-org/tooltip", - "version": "2.0.31", + "version": "2.0.33", "docs": "https://nextui.org/docs/components/tooltip", "description": "A React Component for rendering dynamically positioned Tooltips", "status": "stable", @@ -633,7 +633,7 @@ { "name": "user", "package": "@nextui-org/user", - "version": "2.0.26", + "version": "2.0.28", "docs": "https://nextui.org/docs/components/user", "description": "Flexible User Profile Component.", "status": "stable", @@ -647,5 +647,5 @@ } } ], - "version": "2.3.0" + "version": "2.3.6" } diff --git a/src/helpers/exec.ts b/src/helpers/exec.ts index c7b12a8..31f5ef4 100644 --- a/src/helpers/exec.ts +++ b/src/helpers/exec.ts @@ -5,13 +5,21 @@ import {type CommonExecOptions, execSync} from 'node:child_process'; import {Logger} from './logger'; import {omit} from './utils'; +const execCache = new Map(); + export async function exec( cmd: string, - commonExecOptions?: AppendKeyValue + commonExecOptions?: AppendKeyValue & { + cache?: boolean; + } ) { return new Promise((resolve, reject) => { try { - const {logCmd = true} = commonExecOptions || {}; + const {cache = true, logCmd = true} = commonExecOptions || {}; + + if (execCache.has(cmd) && cache) { + resolve(execCache.get(cmd)); + } if (logCmd) { Logger.newLine(); @@ -27,6 +35,7 @@ export async function exec( const output = stdout.toString(); resolve(output); + execCache.set(cmd, output); } resolve(''); } catch (error) { diff --git a/src/helpers/output-info.ts b/src/helpers/output-info.ts index 1ee3bc3..6e1e394 100644 --- a/src/helpers/output-info.ts +++ b/src/helpers/output-info.ts @@ -52,6 +52,7 @@ export function outputComponents({ name: 0, originVersion: 0, package: 0, + peerDependencies: 0, status: 0, style: 0, version: 0 diff --git a/src/helpers/package.ts b/src/helpers/package.ts index 635a3c5..e8d3336 100644 --- a/src/helpers/package.ts +++ b/src/helpers/package.ts @@ -1,3 +1,5 @@ +import type {UpgradeOption} from './upgrade'; + import {readFileSync} from 'node:fs'; import {type NextUIComponents} from 'src/constants/component'; @@ -7,6 +9,7 @@ import {getLatestVersion} from 'src/scripts/helpers'; import {exec} from './exec'; import {Logger} from './logger'; +import {colorMatchRegex} from './output-info'; import {getVersionAndMode} from './utils'; /** @@ -28,22 +31,25 @@ export function getPackageInfo(packagePath: string, transformVersion = true) { const allDependencies = {...devDependencies, ...dependencies}; const allDependenciesKeys = new Set(Object.keys(allDependencies)); - const currentComponents = (store.nextUIComponents as unknown as NextUIComponents).filter( - (component) => { - if (allDependenciesKeys.has(component.package)) { - const {currentVersion, versionMode} = getVersionAndMode(allDependencies, component.package); + const currentComponents = (store.nextUIComponents as unknown as NextUIComponents) + .map((component) => { + let version = component.version; + let versionMode = component.versionMode; - component.version = transformVersion - ? `${currentVersion} new: ${component.version}` - : currentVersion; - component.versionMode = versionMode; + if (allDependenciesKeys.has(component.package)) { + const data = getVersionAndMode(allDependencies, component.package); - return true; + version = transformVersion ? `${data.currentVersion} new: ${version}` : data.currentVersion; + versionMode = data.versionMode; } - return false; - } - ) as NextUIComponents; + return { + ...component, + version, + versionMode + }; + }) + .filter((component) => allDependenciesKeys.has(component.package)) as NextUIComponents; const isAllComponents = allDependenciesKeys.has(NEXT_UI); return { @@ -104,6 +110,7 @@ export async function transformPackageDetail( docs: docs || '', name: component, package: component, + peerDependencies: {}, status: 'stable', style: '', version: currentVersion, @@ -115,3 +122,14 @@ export async function transformPackageDetail( return result; } + +/** + * Get the complete version + * @example getCompleteVersion({latestVersion: '1.0.0', versionMode: '^'}) --> '^1.0.0' + */ +export function getCompleteVersion(upgradeOption: UpgradeOption) { + return `${upgradeOption.versionMode || ''}${upgradeOption.latestVersion.replace( + colorMatchRegex, + '' + )}`; +} diff --git a/src/helpers/remove.ts b/src/helpers/remove.ts index cd46066..fdc95c6 100644 --- a/src/helpers/remove.ts +++ b/src/helpers/remove.ts @@ -15,12 +15,7 @@ import {getPackageManagerInfo} from './utils'; export async function removeDependencies(components: string[], packageManager: Agent) { const {remove} = getPackageManagerInfo(packageManager); - await exec( - `${packageManager} ${remove} ${components.reduce( - (acc, component) => `${acc} ${component}`, - '' - )}` - ); + await exec(`${packageManager} ${remove} ${components.join(' ')}`); return; } diff --git a/src/helpers/upgrade.ts b/src/helpers/upgrade.ts index 4038a9c..94d3406 100644 --- a/src/helpers/upgrade.ts +++ b/src/helpers/upgrade.ts @@ -2,23 +2,97 @@ import type {RequiredKey, SAFE_ANY} from './type'; import chalk from 'chalk'; -import {NEXT_UI} from 'src/constants/required'; +import {NEXT_UI, THEME_UI} from 'src/constants/required'; import {store} from 'src/constants/store'; +import {type Dependencies, compareVersions, getLatestVersion} from 'src/scripts/helpers'; -import {outputBox} from './output-info'; -import {getColorVersion, getVersionAndMode} from './utils'; +import {exec} from './exec'; +import {Logger} from './logger'; +import {colorMatchRegex, outputBox} from './output-info'; +import { + fillAnsiLength, + getColorVersion, + getVersionAndMode, + transformPeerVersion, + versionModeRegex +} from './utils'; -interface UpgradeOption { +export interface UpgradeOption { package: string; version: string; latestVersion: string; isLatest: boolean; versionMode: string; + peerDependencies?: Dependencies; } const DEFAULT_SPACE = ''.padEnd(7); -export function getUpgradeVersion(upgradeOptionList: UpgradeOption[]) { +interface Upgrade { + isNextUIAll: boolean; + allDependencies?: Record; + upgradeOptionList?: UpgradeOption[]; + all?: boolean; +} + +type ExtractUpgrade = T extends {isNextUIAll: infer U} + ? U extends true + ? RequiredKey + : RequiredKey + : T; + +export async function upgrade(options: ExtractUpgrade) { + const {all, allDependencies, isNextUIAll, upgradeOptionList} = options as Required; + let result: UpgradeOption[] = []; + const missingDepSet = new Set(); + + const allOutputData = await getAllOutputData(all, isNextUIAll, allDependencies, missingDepSet); + + const transformUpgradeOptionList = upgradeOptionList.map((c) => ({ + ...c, + latestVersion: getColorVersion(c.version, c.latestVersion) + })); + + const upgradePeerList = await Promise.all( + upgradeOptionList.map((upgradeOption) => + getPackagePeerDep( + upgradeOption.package, + allDependencies, + missingDepSet, + upgradeOption.peerDependencies + ) + ) + ); + + const missingDepList = await getPackageUpgradeData([...missingDepSet]); + + const outputList = [...transformUpgradeOptionList, ...allOutputData.allOutputList]; + const peerDepList = [ + ...upgradePeerList.flat(), + ...allOutputData.allPeerDepList, + ...missingDepList + ].filter( + (upgradeOption, index, arr) => + index === arr.findIndex((c) => c.package === upgradeOption.package) + ); + + // Output dependencies box + outputDependencies(outputList, peerDepList); + + result = [...outputList, ...peerDepList].filter( + (upgradeOption, index, arr) => + !upgradeOption.isLatest && index === arr.findIndex((c) => c.package === upgradeOption.package) + ); + + return result; +} + +/** + * Get upgrade version + * @param upgradeOptionList + * @param peer Use for peerDependencies change the latest to fulfillment + */ +export function getUpgradeVersion(upgradeOptionList: UpgradeOption[], peer = false) { if (!upgradeOptionList.length) { return ''; } @@ -33,12 +107,31 @@ export function getUpgradeVersion(upgradeOptionList: UpgradeOption[]) { for (const upgradeOption of upgradeOptionList) { for (const key in upgradeOption) { - optionMaxLenMap[key] = Math.max(optionMaxLenMap[key], upgradeOption[key].length); + if (!Object.prototype.hasOwnProperty.call(upgradeOption, key) || !upgradeOption[key]) { + continue; + } + + if (key === 'version') { + // Remove the duplicate character '^' + upgradeOption[key] = upgradeOption[key].replace(versionModeRegex, ''); + } + + const compareLength = + key === 'version' + ? upgradeOption[key].replace(colorMatchRegex, '').length + : upgradeOption[key].length; + + optionMaxLenMap[key] = Math.max(optionMaxLenMap[key], compareLength); } } for (const upgradeOption of upgradeOptionList) { if (upgradeOption.isLatest) { + if (peer) { + // If it is peerDependencies, then skip output the latest version + continue; + } + output.push( ` ${chalk.white( `${`${upgradeOption.package}@${upgradeOption.versionMode || ''}${ @@ -52,7 +145,8 @@ export function getUpgradeVersion(upgradeOptionList: UpgradeOption[]) { ` ${chalk.white( `${upgradeOption.package.padEnd( optionMaxLenMap.package + DEFAULT_SPACE.length - )}${DEFAULT_SPACE}${upgradeOption.versionMode || ''}${upgradeOption.version.padStart( + )}${DEFAULT_SPACE}${fillAnsiLength( + `${upgradeOption.versionMode || ''}${upgradeOption.version}`, optionMaxLenMap.version )} -> ${upgradeOption.versionMode || ''}${upgradeOption.latestVersion}` )}${DEFAULT_SPACE}` @@ -62,59 +156,149 @@ export function getUpgradeVersion(upgradeOptionList: UpgradeOption[]) { return output.join('\n'); } -interface Upgrade { - isNextUIAll: boolean; - allDependencies?: Record; - upgradeOptionList?: UpgradeOption[]; +async function getPackagePeerDep( + packageName: string, + allDependencies: Dependencies, + missingDepList: Set, + peerDependencies?: Dependencies +): Promise { + peerDependencies = + peerDependencies || + JSON.parse( + (await exec(`npm show ${packageName} peerDependencies --json`, { + logCmd: false, + stdio: 'pipe' + })) as SAFE_ANY + ) || + {}; + + if (!peerDependencies || !Object.keys(peerDependencies).length) { + return []; + } + + const upgradeOptionList: UpgradeOption[] = []; + + for (const [peerPackage, peerVersion] of Object.entries(peerDependencies)) { + if (upgradeOptionList.some((c) => c.package === peerPackage)) { + // Avoid duplicate + continue; + } + + const currentVersion = allDependencies[peerPackage]; + + if (!currentVersion) { + missingDepList.add(peerPackage); + continue; + } + + const {versionMode} = getVersionAndMode(allDependencies, peerPackage); + let formatPeerVersion = transformPeerVersion(peerVersion); + const isLatest = compareVersions(currentVersion, formatPeerVersion) >= 0; + + if (isLatest) { + formatPeerVersion = transformPeerVersion(currentVersion); + } + + upgradeOptionList.push({ + isLatest, + latestVersion: isLatest + ? formatPeerVersion + : getColorVersion(currentVersion, formatPeerVersion), + package: peerPackage, + version: currentVersion, + versionMode + }); + } + + return upgradeOptionList; } -type ExtractUpgrade = T extends {isNextUIAll: infer U} - ? U extends true - ? RequiredKey - : RequiredKey - : T; +function outputDependencies(outputList: UpgradeOption[], peerDepList: UpgradeOption[]) { + const componentName = outputList.length === 1 ? 'Component' : 'Components'; + const outputDefault = { + components: {color: 'blue', text: '', title: chalk.blue(componentName)}, + peerDependencies: {color: 'yellow', text: '', title: chalk.yellow('PeerDependencies')} + } as const; + + const outputInfo = getUpgradeVersion(outputList); + const outputPeerDepInfo = getUpgradeVersion(peerDepList, true); + + outputInfo.length && outputBox({...outputDefault.components, text: outputInfo}); + Logger.newLine(); + Logger.log( + chalk.gray( + `Required min version: ${peerDepList + .filter((c) => !c.isLatest) + .map((c) => { + return `${c.package}>=${c.latestVersion.replace(colorMatchRegex, '')}`; + }) + .join(', ')}` + ) + ); + outputPeerDepInfo.length && + outputBox({...outputDefault.peerDependencies, text: outputPeerDepInfo}); +} + +/** + * Get all output data + * @example + * getAllOutputData(true, allDependencies, missingDepSet) --> {allOutputList: [], allPeerDepList: []} + */ +export async function getAllOutputData( + all: boolean, + isNextUIAll: boolean, + allDependencies: Record, + missingDepSet: Set +) { + if (!all || !isNextUIAll) { + return { + allOutputList: [], + allPeerDepList: [] + }; + } -export async function upgrade(options: ExtractUpgrade) { - const {allDependencies, isNextUIAll} = options as Required; - const {upgradeOptionList} = options as Required; - let result: UpgradeOption[] = []; const latestVersion = store.latestVersion; - if (isNextUIAll) { - const {currentVersion, versionMode} = getVersionAndMode(allDependencies, NEXT_UI); - const colorVersion = getColorVersion(currentVersion, latestVersion); - const isLatest = latestVersion === currentVersion; - const outputInfo = getUpgradeVersion([ - { - isLatest, - latestVersion: colorVersion, - package: NEXT_UI, - version: currentVersion, - versionMode - } - ]); - - outputBox({text: outputInfo}); - - if (!isLatest) { - result.push({ - isLatest, - latestVersion, - package: NEXT_UI, - version: currentVersion, - versionMode - }); + const {currentVersion, versionMode} = getVersionAndMode(allDependencies, NEXT_UI); + const colorVersion = getColorVersion(currentVersion, latestVersion); + const isLatest = compareVersions(currentVersion, latestVersion) >= 0; + + const nextUIPeerDepList = await getPackagePeerDep(NEXT_UI, allDependencies, missingDepSet); + const nextUIThemePeerDepList = await getPackagePeerDep(THEME_UI, allDependencies, missingDepSet); + + const allOutputList = [ + { + isLatest, + latestVersion: colorVersion, + package: NEXT_UI, + version: currentVersion, + versionMode } - } else { - const outputUpgradeOptionList = upgradeOptionList.map((c) => ({ - ...c, - latestVersion: getColorVersion(c.version, c.latestVersion) - })); - const outputInfo = getUpgradeVersion(outputUpgradeOptionList); + ]; + const allPeerDepList = [...nextUIPeerDepList, ...nextUIThemePeerDepList]; + const allOutputData = { + allOutputList, + allPeerDepList + }; + + return allOutputData; +} + +export async function getPackageUpgradeData(packageNameList: string[]) { + const result: UpgradeOption[] = []; + + for (const packageName of packageNameList) { + const latestVersion = await getLatestVersion(packageName); - outputBox({text: outputInfo}); + const allOutputList = { + isLatest: false, + latestVersion, + package: packageName, + version: chalk.red('Missing'), + versionMode: '' + }; - result = upgradeOptionList.filter((upgradeOption) => !upgradeOption.isLatest); + result.push(allOutputList); } return result; diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts index 8c97045..95a9b10 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -9,6 +9,8 @@ import {ROOT} from 'src/constants/path'; import {Logger} from './logger'; import {colorMatchRegex} from './output-info'; +export const versionModeRegex = /([\^~])/; + export function getCommandDescAndLog(log: string, desc: string) { Logger.gradient(log); @@ -72,6 +74,9 @@ export function getUpgradeType({ } export function getColorVersion(currentVersion: string, latestVersion: string) { + currentVersion = transformPeerVersion(currentVersion); + latestVersion = transformPeerVersion(latestVersion); + if (isMajorUpdate(currentVersion, latestVersion)) { return isMajorUpdate(currentVersion, latestVersion); } else if (isMinorUpdate(currentVersion, latestVersion)) { @@ -121,7 +126,6 @@ export function isPatchUpdate(currentVersion: string, latestVersion: string) { } export function getVersionAndMode(allDependencies: Record, packageName: string) { - const versionModeRegex = /([\^~])/; const currentVersion = allDependencies[packageName].replace(versionModeRegex, ''); const versionMode = allDependencies[packageName].match(versionModeRegex)?.[1] || ''; diff --git a/src/prompts/index.ts b/src/prompts/index.ts index 44cee8a..7bfe644 100644 --- a/src/prompts/index.ts +++ b/src/prompts/index.ts @@ -67,3 +67,19 @@ export async function getSelect(message: string, choices: prompts.Choice[]) { return result.value; } + +export async function getMultiselect(message: string, choices?: prompts.Choice[]) { + const result = await prompts( + { + hint: '- Space to select. Return to submit', + message, + min: 1, + name: 'value', + type: 'multiselect', + ...(choices ? {choices} : {}) + }, + defaultPromptOptions + ); + + return result.value; +} diff --git a/src/scripts/helpers.ts b/src/scripts/helpers.ts index 1f212f5..daa6aaa 100644 --- a/src/scripts/helpers.ts +++ b/src/scripts/helpers.ts @@ -6,9 +6,12 @@ import chalk from 'chalk'; import {oraPromise} from 'ora'; import {Logger} from '@helpers/logger'; +import {transformPeerVersion} from '@helpers/utils'; import {COMPONENTS_PATH} from 'src/constants/path'; import {getStore} from 'src/constants/store'; +export type Dependencies = Record; + export type Components = { name: string; package: string; @@ -17,6 +20,7 @@ export type Components = { description: string; status: string; style: string; + peerDependencies: Dependencies; }[]; export type ComponentsJson = { @@ -33,6 +37,9 @@ export type ComponentsJson = { * @param version2 */ export function compareVersions(version1: string, version2: string) { + version1 = transformPeerVersion(version1); + version2 = transformPeerVersion(version2); + const parts1 = version1.split('.').map(Number); const parts2 = version2.split('.').map(Number); @@ -145,7 +152,7 @@ export async function downloadFile(url: string): Promise { process.exit(1); }, successText: (() => { - return chalk.greenBright('Components data update successfully!\n'); + return chalk.greenBright('Components data updated successfully!\n'); })(), text: 'Fetching components data...' }