diff --git a/code/addons/test/package.json b/code/addons/test/package.json index 2b16dc1b0bfe..153695d12966 100644 --- a/code/addons/test/package.json +++ b/code/addons/test/package.json @@ -73,8 +73,11 @@ "@storybook/csf": "^0.1.11" }, "devDependencies": { + "@types/semver": "^7", "@vitest/browser": "^2.0.0", + "boxen": "^8.0.1", "find-up": "^7.0.0", + "semver": "^7.6.3", "tinyrainbow": "^1.2.0", "ts-dedent": "^2.2.0", "vitest": "^2.0.0" diff --git a/code/addons/test/project.json b/code/addons/test/project.json index f9278931287c..96314d1efc94 100644 --- a/code/addons/test/project.json +++ b/code/addons/test/project.json @@ -5,4 +5,4 @@ "targets": { "build": {} } -} \ No newline at end of file +} diff --git a/code/addons/test/src/postinstall-logger.ts b/code/addons/test/src/postinstall-logger.ts new file mode 100644 index 000000000000..8bb7df027c0b --- /dev/null +++ b/code/addons/test/src/postinstall-logger.ts @@ -0,0 +1,34 @@ +import { colors, logger } from 'storybook/internal/node-logger'; + +import boxen, { type Options } from 'boxen'; + +const fancy = + process.platform !== 'win32' || process.env.CI || process.env.TERM === 'xterm-256color'; + +export const step = colors.gray('›'); +export const info = colors.blue(fancy ? 'ℹ' : 'i'); +export const success = colors.green(fancy ? '✔' : '√'); +export const warning = colors.orange(fancy ? '⚠' : '‼'); +export const error = colors.red(fancy ? '✖' : '×'); + +const baseOptions: Options = { + borderStyle: 'round', + padding: 1, +}; + +export const print = (message: string, options: Options) => { + logger.line(1); + logger.plain(boxen(message, { ...baseOptions, ...options })); +}; + +export const printInfo = (title: string, message: string, options?: Options) => + print(message, { borderColor: 'blue', title, ...options }); + +export const printWarning = (title: string, message: string, options?: Options) => + print(message, { borderColor: 'yellow', title, ...options }); + +export const printError = (title: string, message: string, options?: Options) => + print(message, { borderColor: 'red', title, ...options }); + +export const printSuccess = (title: string, message: string, options?: Options) => + print(message, { borderColor: 'green', title, ...options }); diff --git a/code/addons/test/src/postinstall.ts b/code/addons/test/src/postinstall.ts index 3ecbff0432da..c1d8cf54709b 100644 --- a/code/addons/test/src/postinstall.ts +++ b/code/addons/test/src/postinstall.ts @@ -1,7 +1,7 @@ import { existsSync } from 'node:fs'; import * as fs from 'node:fs/promises'; import { writeFile } from 'node:fs/promises'; -import { dirname, join, relative, resolve } from 'node:path'; +import { dirname, join, relative } from 'node:path'; import * as path from 'node:path'; import { @@ -11,32 +11,44 @@ import { loadMainConfig, validateFrameworkName, } from 'storybook/internal/common'; -import { logger } from 'storybook/internal/node-logger'; +import { colors, logger } from 'storybook/internal/node-logger'; import { findUp } from 'find-up'; +import { coerce, satisfies } from 'semver'; import c from 'tinyrainbow'; -import dedent from 'ts-dedent'; +import { dedent } from 'ts-dedent'; import { type PostinstallOptions } from '../../../lib/cli-storybook/src/add'; +import { printError, printInfo, printSuccess, step } from './postinstall-logger'; -const extensions = ['.js', '.jsx', '.ts', '.tsx', '.cts', '.mts', '.cjs', '.mjs']; +const ADDON_NAME = '@storybook/experimental-addon-test' as const; +const EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.cts', '.mts', '.cjs', '.mjs'] as const; + +const findFile = async (basename: string) => findUp(EXTENSIONS.map((ext) => basename + ext)); export default async function postInstall(options: PostinstallOptions) { + printSuccess( + '👋 Howdy!', + dedent` + I'm the installation helper for ${colors.pink.bold(ADDON_NAME)} + + Hold on for a moment while I look at your project and get it set up... + ` + ); + const packageManager = JsPackageManagerFactory.getPackageManager({ force: options.packageManager, }); const info = await getFrameworkInfo(options); - - if ( - info.frameworkPackageName !== '@storybook/nextjs' && - info.builderPackageName !== '@storybook/builder-vite' - ) { - logger.info( - 'The Vitest addon can only be used with a Vite-based Storybook framework or Next.js.' - ); - return; - } + const allDeps = await packageManager.getAllDependencies(); + // only install these dependencies if they are not already installed + const dependencies = ['vitest', '@vitest/browser', 'playwright'].filter((p) => !allDeps[p]); + const vitestVersionSpecifier = + allDeps.vitest || (await packageManager.getInstalledVersion('vitest')); + const coercedVitestVersion = vitestVersionSpecifier ? coerce(vitestVersionSpecifier) : null; + // if Vitest is installed, we use the same version to keep consistency across Vitest packages + const vitestVersionToInstall = vitestVersionSpecifier ?? 'latest'; const annotationsImport = [ '@storybook/nextjs', @@ -50,28 +62,100 @@ export default async function postInstall(options: PostinstallOptions) { ) ? info.rendererPackageName : null; + const isRendererSupported = !!annotationsImport; + + const prerequisiteCheck = async () => { + const reasons = []; + + if ( + info.frameworkPackageName !== '@storybook/nextjs' && + info.builderPackageName !== '@storybook/builder-vite' + ) { + reasons.push( + '• The addon can only be used with a Vite-based Storybook framework or Next.js.' + ); + } + + if (!isRendererSupported) { + reasons.push(dedent` + • The addon cannot yet be used with ${colors.pink.bold(info.frameworkPackageName)} + `); + } + + if (coercedVitestVersion && !satisfies(coercedVitestVersion, '>=2.0.0')) { + reasons.push(` + • The addon requires Vitest 2.0.0 or later. You are currently using ${vitestVersionSpecifier}. + Please update your ${colors.pink.bold('vitest')} dependency and try again. + `); + } + + if (info.frameworkPackageName === '@storybook/nextjs') { + const nextVersion = await packageManager.getInstalledVersion('next'); + if (!nextVersion) { + reasons.push(dedent` + • You are using ${colors.pink.bold('@storybook/nextjs')} without having ${colors.pink.bold('next')} installed. + Please install "next" or use a different Storybook framework integration and try again. + `); + } + } + + if (reasons.length > 0) { + reasons.unshift( + `Storybook Test's automated setup failed due to the following package incompatibilities:` + ); + reasons.push( + dedent` + To roll back the installation, remove ${colors.pink.bold(ADDON_NAME)} from the "addons" array + in your main Storybook config file and remove the dependency from your package.json file. + ` + ); - if (!annotationsImport) { - logger.info('The Vitest addon cannot yet be used with: ' + info.frameworkPackageName); + if (!isRendererSupported) { + reasons.push( + dedent` + Please check the documentation for more information about its requirements and installation: + ${c.cyan`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest`} + ` + ); + } else { + reasons.push( + dedent` + Fear not, however, you can follow the manual installation process instead at: + ${c.cyan`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest#manual`} + ` + ); + } + + return reasons.map((r) => r.trim()).join('\n\n'); + } + + return null; + }; + + const result = await prerequisiteCheck(); + + if (result) { + printError('⛔️ Sorry!', result); + logger.line(1); return; } const vitestInfo = getVitestPluginInfo(info.frameworkPackageName); - const packages = ['vitest@latest', '@vitest/browser@latest', 'playwright@latest']; - if (info.frameworkPackageName === '@storybook/nextjs') { - logger.info( + printInfo( + '🍿 Just so you know...', dedent` - We detected that you're using Next.js. - We will configure the @storybook/experimental-nextjs-vite/vite-plugin to allow you to run tests in Vitest. + It looks like you're using Next.js. + + Adding ${colors.pink.bold(`@storybook/experimental-nextjs-vite/vite-plugin`)} so you can use it with Vitest. + + More info about the plugin at: ${c.cyan`https://github.com/storybookjs/vite-plugin-storybook-nextjs`} ` ); - try { const storybookVersion = await packageManager.getInstalledVersion('storybook'); - - packages.push(`@storybook/experimental-nextjs-vite@^${storybookVersion}`); + dependencies.push(`@storybook/experimental-nextjs-vite@^${storybookVersion}`); } catch (e) { console.error( 'Failed to install @storybook/experimental-nextjs-vite. Please install it manually' @@ -79,103 +163,185 @@ export default async function postInstall(options: PostinstallOptions) { } } - logger.info(c.bold('Installing packages...')); - logger.info(packages.join(', ')); - await packageManager.addDependencies({ installAsDevDependencies: true }, packages); + const versionedDependencies = dependencies.map((p) => { + if (p.includes('vitest')) { + return `${p}@${vitestVersionToInstall ?? 'latest'}`; + } + + return p; + }); + + if (versionedDependencies.length > 0) { + logger.line(1); + logger.plain(`${step} Installing dependencies:`); + logger.plain(colors.gray(' ' + versionedDependencies.join(', '))); + + await packageManager.addDependencies({ installAsDevDependencies: true }, versionedDependencies); + } + + logger.line(1); + logger.plain(`${step} Configuring Playwright with Chromium (this might take some time):`); + logger.plain(colors.gray(' npx playwright install chromium --with-deps')); - logger.info(c.bold('Executing npx playwright install chromium --with-deps ...')); await packageManager.executeCommand({ command: 'npx', args: ['playwright', 'install', 'chromium', '--with-deps'], }); - logger.info(c.bold('Writing .storybook/vitest.setup.ts file...')); + const vitestSetupFile = path.resolve(options.configDir, 'vitest.setup.ts'); + if (existsSync(vitestSetupFile)) { + printError( + '🚨 Oh no!', + dedent` + Found an existing Vitest setup file: + ${colors.gray(vitestSetupFile)} - const previewExists = extensions - .map((ext) => path.resolve(options.configDir, `preview${ext}`)) - .some((config) => existsSync(config)); + Please refer to the documentation to complete the setup manually: + ${c.cyan`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest#manual`} + ` + ); + logger.line(1); + return; + } + + logger.line(1); + logger.plain(`${step} Creating a Vitest setup file for Storybook:`); + logger.plain(colors.gray(` ${vitestSetupFile}`)); + + const previewExists = EXTENSIONS.map((ext) => + path.resolve(options.configDir, `preview${ext}`) + ).some((config) => existsSync(config)); await writeFile( - resolve(options.configDir, 'vitest.setup.ts'), + vitestSetupFile, dedent` - import { beforeAll } from 'vitest' - import { setProjectAnnotations } from '${annotationsImport}' - ${previewExists ? `import * as projectAnnotations from './preview'` : ''} + import { beforeAll } from 'vitest'; + import { setProjectAnnotations } from '${annotationsImport}'; + ${previewExists ? `import * as projectAnnotations from './preview';` : ''} - const project = setProjectAnnotations(${previewExists ? 'projectAnnotations' : '[]'}) + // This is an important step to apply the right configuration when testing your stories. + // More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations + const project = setProjectAnnotations(${previewExists ? '[projectAnnotations]' : '[]'}); - beforeAll(project.beforeAll) + beforeAll(project.beforeAll); ` ); - // Check for an existing config file. Can be from Vitest (preferred) or Vite (with `test` option). - const viteConfigFiles = extensions.map((ext) => 'vite.config' + ext); - const viteConfig = await findUp(viteConfigFiles, { cwd: process.cwd() }); - const vitestConfigFiles = extensions.map((ext) => 'vitest.config' + ext); - const rootConfig = (await findUp(vitestConfigFiles, { cwd: process.cwd() })) || viteConfig; + // Check for existing Vitest workspace. We can't extend it so manual setup is required. + const vitestWorkspaceFile = await findFile('vitest.workspace'); + if (vitestWorkspaceFile) { + printError( + '🚨 Oh no!', + dedent` + Found an existing Vitest workspace file: + ${colors.gray(vitestWorkspaceFile)} - if (rootConfig) { - // If there's an existing config, we create a workspace file so we can run Storybook tests alongside. - const extname = path.extname(rootConfig); - const browserWorkspaceFile = resolve(dirname(rootConfig), `vitest.workspace${extname}`); - if (existsSync(browserWorkspaceFile)) { - logger.info( + I was able to configure most of the addon but could not safely extend + your existing workspace file automatically, you must do it yourself. This was the last step. + + Please refer to the documentation to complete the setup manually: + ${c.cyan`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest#manual`} + ` + ); + logger.line(1); + return; + } + + // Check for an existing config file. Can be from Vitest (preferred) or Vite (with `test` option). + const viteConfigFile = await findFile('vite.config'); + if (viteConfigFile) { + const viteConfig = await fs.readFile(viteConfigFile, 'utf8'); + if (viteConfig.match(/\Wtest:\s*{/)) { + printError( + '🚨 Oh no!', dedent` - We can not automatically setup the plugin when you use Vitest with workspaces. + You seem to have an existing test configuration in your Vite config file: + ${colors.gray(vitestWorkspaceFile)} + + I was able to configure most of the addon but could not safely extend + your existing workspace file automatically, you must do it yourself. This was the last step. + Please refer to the documentation to complete the setup manually: - https://storybook.js.org/docs/writing-tests/test-runner-with-vitest#manual + ${c.cyan`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest#manual`} ` ); - } else { - logger.info(c.bold('Writing vitest.workspace.ts file...')); - await writeFile( - browserWorkspaceFile, - dedent` - import { defineWorkspace } from 'vitest/config'; - import { storybookTest } from '@storybook/experimental-addon-test/vite-plugin'; - ${vitestInfo.frameworkPluginImport ? vitestInfo.frameworkPluginImport + '\n' : ''} - export default defineWorkspace([ - '${relative(dirname(browserWorkspaceFile), rootConfig)}', - { - extends: '${viteConfig ? relative(dirname(browserWorkspaceFile), viteConfig) : ''}', - plugins: [ - storybookTest(),${vitestInfo.frameworkPluginCall ? '\n ' + vitestInfo.frameworkPluginCall : ''} - ], - test: { - browser: { - enabled: true, - headless: true, - name: 'chromium', - provider: 'playwright', - }, - include: ['**/*.stories.?(m)[jt]s?(x)'], - setupFiles: ['./.storybook/vitest.setup.ts'], + logger.line(1); + return; + } + } + + const vitestConfigFile = await findFile('vitest.config'); + const rootConfig = vitestConfigFile || viteConfigFile; + + if (rootConfig) { + // If there's an existing config, we create a workspace file so we can run Storybook tests alongside. + const extname = path.extname(rootConfig); + const browserWorkspaceFile = path.resolve(dirname(rootConfig), `vitest.workspace${extname}`); + + logger.line(1); + logger.plain(`${step} Creating a Vitest project workspace file:`); + logger.plain(colors.gray(` ${browserWorkspaceFile}`)); + + await writeFile( + browserWorkspaceFile, + dedent` + import { defineWorkspace } from 'vitest/config'; + import { storybookTest } from '@storybook/experimental-addon-test/vite-plugin';${vitestInfo.frameworkPluginImport} + + // More info at: https://storybook.js.org/docs/writing-tests/test-runner-with-vitest + export default defineWorkspace([ + '${relative(dirname(browserWorkspaceFile), rootConfig)}', + { + extends: '${viteConfigFile ? relative(dirname(browserWorkspaceFile), viteConfigFile) : ''}', + plugins: [ + // See options at: https://storybook.js.org/docs/writing-tests/test-runner-with-vitest#storybooktest + storybookTest(),${vitestInfo.frameworkPluginDocs + vitestInfo.frameworkPluginCall} + ], + test: { + name: 'storybook', + browser: { + enabled: true, + headless: true, + name: 'chromium', + provider: 'playwright', }, + // Make sure to adjust this pattern to match your stories files. + include: ['**/*.stories.?(m)[jt]s?(x)'], + setupFiles: ['./.storybook/vitest.setup.ts'], }, - ]); - `.replace(/\s+extends: '',/, '') - ); - } + }, + ]); + `.replace(/\s+extends: '',/, '') + ); } else { // If there's no existing Vitest/Vite config, we create a new Vitest config file. - logger.info(c.bold('Writing vitest.config.ts file...')); + const newVitestConfigFile = path.resolve('vitest.config.ts'); + + logger.line(1); + logger.plain(`${step} Creating a Vitest project config file:`); + logger.plain(colors.gray(` ${newVitestConfigFile}`)); + await writeFile( - resolve('vitest.config.ts'), + newVitestConfigFile, dedent` - import { defineConfig } from "vitest/config"; - import { storybookTest } from "@storybook/experimental-addon-test/vite-plugin"; - ${vitestInfo.frameworkPluginImport ? vitestInfo.frameworkPluginImport + '\n' : ''} + import { defineConfig } from 'vitest/config'; + import { storybookTest } from '@storybook/experimental-addon-test/vite-plugin';${vitestInfo.frameworkPluginImport} + + // More info at: https://storybook.js.org/docs/writing-tests/test-runner-with-vitest export default defineConfig({ plugins: [ - storybookTest(),${vitestInfo.frameworkPluginCall ? '\n' + vitestInfo.frameworkPluginCall : ''} + // See options at: https://storybook.js.org/docs/writing-tests/test-runner-with-vitest#storybooktest + storybookTest(),${vitestInfo.frameworkPluginDocs + vitestInfo.frameworkPluginCall} ], test: { + name: 'storybook', browser: { enabled: true, headless: true, name: 'chromium', provider: 'playwright', }, + // Make sure to adjust this pattern to match your stories files. include: ['**/*.stories.?(m)[jt]s?(x)'], setupFiles: ['./.storybook/vitest.setup.ts'], }, @@ -184,37 +350,55 @@ export default async function postInstall(options: PostinstallOptions) { ); } - logger.info( + const runCommand = rootConfig ? `npx vitest --project=storybook` : `npx vitest`; + + printSuccess( + '🎉 All done!', dedent` - The Vitest addon is now configured and you're ready to run your tests! + The Storybook Test addon is now configured and you're ready to run your tests! + + Here are a couple of tips to get you started: + • You can run tests with ${colors.gray(runCommand)} + • When using the Vitest extension in your editor, all of your stories will be shown as tests! + Check the documentation for more information about its features and options at: - https://storybook.js.org/docs/writing-tests/test-runner-with-vitest + ${c.cyan`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest`} ` ); + logger.line(1); } const getVitestPluginInfo = (framework: string) => { let frameworkPluginImport = ''; let frameworkPluginCall = ''; + let frameworkPluginDocs = ''; if (framework === '@storybook/nextjs') { frameworkPluginImport = - "import { storybookNextJsPlugin } from '@storybook/experimental-nextjs-vite/vite-plugin'"; + "import { storybookNextJsPlugin } from '@storybook/experimental-nextjs-vite/vite-plugin';"; + frameworkPluginDocs = + '// More info at: https://github.com/storybookjs/vite-plugin-storybook-nextjs'; frameworkPluginCall = 'storybookNextJsPlugin()'; } if (framework === '@storybook/sveltekit') { frameworkPluginImport = - "import { storybookSveltekitPlugin } from '@storybook/sveltekit/vite-plugin'"; + "import { storybookSveltekitPlugin } from '@storybook/sveltekit/vite-plugin';"; frameworkPluginCall = 'storybookSveltekitPlugin()'; } if (framework === '@storybook/vue3-vite') { - frameworkPluginImport = "import { storybookVuePlugin } from '@storybook/vue3-vite/vite-plugin'"; + frameworkPluginImport = + "import { storybookVuePlugin } from '@storybook/vue3-vite/vite-plugin';"; frameworkPluginCall = 'storybookVuePlugin()'; } - return { frameworkPluginImport, frameworkPluginCall }; + // spaces for file identation + frameworkPluginImport = `\n${frameworkPluginImport}`; + frameworkPluginDocs = frameworkPluginDocs ? `\n ${frameworkPluginDocs}` : ''; + frameworkPluginCall = frameworkPluginCall ? `\n ${frameworkPluginCall},` : ''; + + return { frameworkPluginImport, frameworkPluginCall, frameworkPluginDocs }; }; async function getFrameworkInfo({ configDir, packageManager: pkgMgr }: PostinstallOptions) { diff --git a/code/lib/cli-storybook/src/add.ts b/code/lib/cli-storybook/src/add.ts index d0e64efdf443..661c4fb7d064 100644 --- a/code/lib/cli-storybook/src/add.ts +++ b/code/lib/cli-storybook/src/add.ts @@ -10,6 +10,7 @@ import { } from 'storybook/internal/common'; import { readConfig, writeConfig } from 'storybook/internal/csf-tools'; +import prompts from 'prompts'; import SemVer from 'semver'; import { dedent } from 'ts-dedent'; @@ -97,19 +98,28 @@ export async function add( if (typeof configDir === 'undefined') { throw new Error(dedent` - Unable to find storybook config directory + Unable to find storybook config directory. Please specify your Storybook config directory with the --config-dir flag. `); } if (!mainConfig) { - logger.error('Unable to find storybook main.js config'); + logger.error('Unable to find Storybook main.js config'); return; } + let shouldAddToMain = true; if (checkInstalled(addonName, requireMain(configDir))) { - throw new Error(dedent` - Addon ${addonName} is already installed; we skipped adding it to your ${mainConfig}. - `); + const { shouldForceInstall } = await prompts({ + type: 'confirm', + name: 'shouldForceInstall', + message: `The Storybook addon "${addonName}" is already present in ${mainConfig}. Do you wish to install it again?`, + }); + + if (!shouldForceInstall) { + return; + } + + shouldAddToMain = false; } const main = await readConfig(mainConfig); @@ -139,18 +149,20 @@ export async function add( logger.log(`Installing ${addonWithVersion}`); await packageManager.addDependencies({ installAsDevDependencies: true }, [addonWithVersion]); - logger.log(`Adding '${addon}' to main.js addons field.`); + if(shouldAddToMain) { + logger.log(`Adding '${addon}' to the "addons" field in ${mainConfig}`); - const mainConfigAddons = main.getFieldNode(['addons']); - if (mainConfigAddons && getRequireWrapperName(main) !== null) { - const addonNode = main.valueToNode(addonName); - main.appendNodeToArray(['addons'], addonNode as any); - wrapValueWithRequireWrapper(main, addonNode as any); - } else { - main.appendValueToArray(['addons'], addonName); - } + const mainConfigAddons = main.getFieldNode(['addons']); + if (mainConfigAddons && getRequireWrapperName(main) !== null) { + const addonNode = main.valueToNode(addonName); + main.appendNodeToArray(['addons'], addonNode as any); + wrapValueWithRequireWrapper(main, addonNode as any); + } else { + main.appendValueToArray(['addons'], addonName); + } - await writeConfig(main); + await writeConfig(main); + } if (!skipPostinstall && isCoreAddon(addonName)) { await postinstallAddon(addonName, { packageManager: packageManager.type, configDir }); diff --git a/code/yarn.lock b/code/yarn.lock index 83b4b2ba2bdc..20ab3fbc5783 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6131,8 +6131,11 @@ __metadata: resolution: "@storybook/experimental-addon-test@workspace:addons/test" dependencies: "@storybook/csf": "npm:^0.1.11" + "@types/semver": "npm:^7" "@vitest/browser": "npm:^2.0.0" + boxen: "npm:^8.0.1" find-up: "npm:^7.0.0" + semver: "npm:^7.6.3" tinyrainbow: "npm:^1.2.0" ts-dedent: "npm:^2.2.0" vitest: "npm:^2.0.0" @@ -8175,7 +8178,7 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:^7.3.12, @types/semver@npm:^7.3.4, @types/semver@npm:^7.5.0, @types/semver@npm:^7.5.6, @types/semver@npm:^7.5.8": +"@types/semver@npm:^7, @types/semver@npm:^7.3.12, @types/semver@npm:^7.3.4, @types/semver@npm:^7.5.0, @types/semver@npm:^7.5.6, @types/semver@npm:^7.5.8": version: 7.5.8 resolution: "@types/semver@npm:7.5.8" checksum: 10c0/8663ff927234d1c5fcc04b33062cb2b9fcfbe0f5f351ed26c4d1e1581657deebd506b41ff7fdf89e787e3d33ce05854bc01686379b89e9c49b564c4cfa988efa @@ -9760,7 +9763,7 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0": +"ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1": version: 6.2.1 resolution: "ansi-styles@npm:6.2.1" checksum: 10c0/5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c @@ -10716,6 +10719,22 @@ __metadata: languageName: node linkType: hard +"boxen@npm:^8.0.1": + version: 8.0.1 + resolution: "boxen@npm:8.0.1" + dependencies: + ansi-align: "npm:^3.0.1" + camelcase: "npm:^8.0.0" + chalk: "npm:^5.3.0" + cli-boxes: "npm:^3.0.0" + string-width: "npm:^7.2.0" + type-fest: "npm:^4.21.0" + widest-line: "npm:^5.0.0" + wrap-ansi: "npm:^9.0.0" + checksum: 10c0/8c54f9797bf59eec0b44c9043d9cb5d5b2783dc673e4650235e43a5155c43334e78ec189fd410cf92056c1054aee3758279809deed115b49e68f1a1c6b3faa32 + languageName: node + linkType: hard + "bplist-parser@npm:^0.2.0": version: 0.2.0 resolution: "bplist-parser@npm:0.2.0" @@ -13621,6 +13640,13 @@ __metadata: languageName: node linkType: hard +"emoji-regex@npm:^10.3.0": + version: 10.4.0 + resolution: "emoji-regex@npm:10.4.0" + checksum: 10c0/a3fcedfc58bfcce21a05a5f36a529d81e88d602100145fcca3dc6f795e3c8acc4fc18fe773fbf9b6d6e9371205edb3afa2668ec3473fa2aa7fd47d2a9d46482d + languageName: node + linkType: hard + "emoji-regex@npm:^8.0.0": version: 8.0.0 resolution: "emoji-regex@npm:8.0.0" @@ -15922,6 +15948,13 @@ __metadata: languageName: node linkType: hard +"get-east-asian-width@npm:^1.0.0": + version: 1.2.0 + resolution: "get-east-asian-width@npm:1.2.0" + checksum: 10c0/914b1e217cf38436c24b4c60b4c45289e39a45bf9e65ef9fd343c2815a1a02b8a0215aeec8bf9c07c516089004b6e3826332481f40a09529fcadbf6e579f286b + languageName: node + linkType: hard + "get-func-name@npm:^2.0.1, get-func-name@npm:^2.0.2": version: 2.0.2 resolution: "get-func-name@npm:2.0.2" @@ -25335,6 +25368,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.6.3": + version: 7.6.3 + resolution: "semver@npm:7.6.3" + bin: + semver: bin/semver.js + checksum: 10c0/88f33e148b210c153873cb08cfe1e281d518aaa9a666d4d148add6560db5cd3c582f3a08ccb91f38d5f379ead256da9931234ed122057f40bb5766e65e58adaf + languageName: node + linkType: hard + "send@npm:0.18.0": version: 0.18.0 resolution: "send@npm:0.18.0" @@ -26231,6 +26273,17 @@ __metadata: languageName: node linkType: hard +"string-width@npm:^7.0.0, string-width@npm:^7.2.0": + version: 7.2.0 + resolution: "string-width@npm:7.2.0" + dependencies: + emoji-regex: "npm:^10.3.0" + get-east-asian-width: "npm:^1.0.0" + strip-ansi: "npm:^7.1.0" + checksum: 10c0/eb0430dd43f3199c7a46dcbf7a0b34539c76fe3aa62763d0b0655acdcbdf360b3f66f3d58ca25ba0205f42ea3491fa00f09426d3b7d3040e506878fc7664c9b9 + languageName: node + linkType: hard + "string.prototype.matchall@npm:^4.0.8": version: 4.0.10 resolution: "string.prototype.matchall@npm:4.0.10" @@ -29254,6 +29307,15 @@ __metadata: languageName: node linkType: hard +"widest-line@npm:^5.0.0": + version: 5.0.0 + resolution: "widest-line@npm:5.0.0" + dependencies: + string-width: "npm:^7.0.0" + checksum: 10c0/6bd6cca8cda502ef50e05353fd25de0df8c704ffc43ada7e0a9cf9a5d4f4e12520485d80e0b77cec8a21f6c3909042fcf732aa9281e5dbb98cc9384a138b2578 + languageName: node + linkType: hard + "wildcard@npm:^2.0.0": version: 2.0.1 resolution: "wildcard@npm:2.0.1" @@ -29333,6 +29395,17 @@ __metadata: languageName: node linkType: hard +"wrap-ansi@npm:^9.0.0": + version: 9.0.0 + resolution: "wrap-ansi@npm:9.0.0" + dependencies: + ansi-styles: "npm:^6.2.1" + string-width: "npm:^7.0.0" + strip-ansi: "npm:^7.1.0" + checksum: 10c0/a139b818da9573677548dd463bd626a5a5286271211eb6e4e82f34a4f643191d74e6d4a9bb0a3c26ec90e6f904f679e0569674ac099ea12378a8b98e20706066 + languageName: node + linkType: hard + "wrappy@npm:1": version: 1.0.2 resolution: "wrappy@npm:1.0.2"