diff --git a/code/addons/a11y/src/postinstall.ts b/code/addons/a11y/src/postinstall.ts index 79822fbdc7d3..81a4cc2ff027 100644 --- a/code/addons/a11y/src/postinstall.ts +++ b/code/addons/a11y/src/postinstall.ts @@ -1,17 +1,7 @@ -// eslint-disable-next-line depend/ban-dependencies -import { execa } from 'execa'; +import { runAutomigrate } from 'storybook/internal/cli'; import type { PostinstallOptions } from '../../../lib/cli-storybook/src/add'; -const $ = execa({ - preferLocal: true, - stdio: 'inherit', - // we stream the stderr to the console - reject: false, -}); - export default async function postinstall(options: PostinstallOptions) { - await $({ - stdio: 'inherit', - })`storybook automigrate addonA11yAddonTest ${options.yes ? '--yes' : ''}`; + await runAutomigrate('addonA11yAddonTest', options); } diff --git a/code/addons/vitest/src/postinstall.ts b/code/addons/vitest/src/postinstall.ts index 1e8b43744223..7a715b1b614f 100644 --- a/code/addons/vitest/src/postinstall.ts +++ b/code/addons/vitest/src/postinstall.ts @@ -3,6 +3,7 @@ import * as fs from 'node:fs/promises'; import { writeFile } from 'node:fs/promises'; import { babelParse, generate, traverse } from 'storybook/internal/babel'; +import { runAutomigrate } from 'storybook/internal/cli'; import { JsPackageManagerFactory, extractProperFrameworkName, @@ -18,8 +19,6 @@ import { import { readConfig, writeConfig } from 'storybook/internal/csf-tools'; import { colors, logger } from 'storybook/internal/node-logger'; -// eslint-disable-next-line depend/ban-dependencies -import { $ } from 'execa'; import { findUp } from 'find-up'; import { dirname, extname, join, relative, resolve } from 'pathe'; import picocolors from 'picocolors'; @@ -335,9 +334,7 @@ export default async function postInstall(options: PostinstallOptions) { if (a11yAddon) { try { logger.plain(`${step} Setting up ${addonA11yName} for @storybook/addon-vitest:`); - await $({ - stdio: 'inherit', - })`storybook automigrate addonA11yAddonTest ${options.yes ? '--yes' : ''}`; + await runAutomigrate('addonA11yAddonTest', options); } catch (e) { printError( '🚨 Oh no!', diff --git a/code/core/src/cli/index.ts b/code/core/src/cli/index.ts index c99751ac2c1b..e1635c42efe8 100644 --- a/code/core/src/cli/index.ts +++ b/code/core/src/cli/index.ts @@ -5,3 +5,4 @@ export * from './dirs'; export * from './project_types'; export * from './NpmOptions'; export * from './eslintPlugin'; +export * from './runner'; diff --git a/code/core/src/cli/runner.ts b/code/core/src/cli/runner.ts new file mode 100644 index 000000000000..5cc26d3acd41 --- /dev/null +++ b/code/core/src/cli/runner.ts @@ -0,0 +1,171 @@ +import type { AddOptions } from '../../../lib/cli-storybook/src/add'; +import type { AutofixOptionsFromCLI } from '../../../lib/cli-storybook/src/automigrate/fixes'; +import type { FixesIDs, allFixes } from '../../../lib/cli-storybook/src/automigrate/fixes'; +import type { DoctorOptions } from '../../../lib/cli-storybook/src/doctor'; +import { JsPackageManagerFactory } from '../common'; +import type { RemoveAddonOptions } from '../common/utils/remove-addon'; + +// Common options shared across multiple commands +interface CommonOptions { + packageManager?: 'npm' | 'pnpm' | 'yarn1' | 'yarn2' | 'bun'; + cwd?: string; +} + +/** + * Execute a Storybook CLI command + * + * @private + * @param args The command arguments to pass to Storybook CLI + * @returns A promise that resolves when the command completes + */ +const executeCommand = async (args: string[], options: CommonOptions = {}) => { + const { packageManager: pkgMgr } = options; + + const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr }, options.cwd); + await packageManager.runPackageCommand('storybook', args, options.cwd, 'inherit'); +}; + +/** + * Run the 'add' command to add an addon to Storybook + * + * @example + * + * ```ts + * await runAdd('@storybook/addon-a11y', { + * yes: true, + * configDir: 'config', + * packageManager: 'npm', + * }); + * ``` + */ +export const runAdd = async (addonName: string, options: AddOptions = {}) => { + const args = ['add', addonName]; + + if (options.yes) { + args.push('--yes'); + } + + if (options.configDir) { + args.push('--config-dir', options.configDir); + } + + if (options.packageManager) { + args.push('--package-manager', options.packageManager); + } + + if (options.skipPostinstall) { + args.push('--skip-postinstall'); + } + + return executeCommand(args); +}; + +/** + * Run the 'remove' command to remove an addon from Storybook + * + * @example + * + * ```ts + * await runRemove('@storybook/addon-a11y', { + * configDir: 'config', + * packageManager: 'npm', + * }); + * ``` + */ +export const runRemove = async (addonName: string, options: RemoveAddonOptions = {}) => { + const args = ['remove', addonName]; + + if (options.configDir) { + args.push('--config-dir', options.configDir); + } + + if (options.packageManager) { + args.push('--package-manager', options.packageManager); + } + + if (options.cwd) { + args.push('--cwd', options.cwd); + } + + return executeCommand(args); +}; + +/** + * Run the 'automigrate' command to check and fix incompatibilities + * + * @example + * + * ```ts + * await runAutomigrate('addon-a11y-parameters', { + * yes: true, + * configDir: 'config', + * packageManager: 'npm', + * }); + * ``` + */ +export const runAutomigrate = async ( + fixId?: FixesIDs, + options: AutofixOptionsFromCLI = {} +) => { + const args = ['automigrate']; + + if (fixId) { + args.push(fixId); + } + + if (options.yes) { + args.push('--yes'); + } + + if (options.dryRun) { + args.push('--dry-run'); + } + + if (options.configDir) { + args.push('--config-dir', options.configDir); + } + + if (options.packageManager) { + args.push('--package-manager', options.packageManager); + } + + if (options.list) { + args.push('--list'); + } + + if (options.skipInstall) { + args.push('--skip-install'); + } + + if (options.renderer) { + args.push('--renderer', options.renderer); + } + + return executeCommand(args); +}; + +/** + * Run the 'doctor' command to check for problems and get suggestions + * + * @example + * + * ```ts + * await runDoctor({ + * configDir: 'config', + * packageManager: 'npm', + * }); + * ``` + */ +export const runDoctor = async (options: DoctorOptions = {}) => { + const args = ['doctor']; + + if (options.configDir) { + args.push('--config-dir', options.configDir); + } + + if (options.packageManager) { + args.push('--package-manager', options.packageManager); + } + + return executeCommand(args, options); +}; diff --git a/code/core/src/common/index.ts b/code/core/src/common/index.ts index 35cbc5a51e86..84932788a9aa 100644 --- a/code/core/src/common/index.ts +++ b/code/core/src/common/index.ts @@ -30,7 +30,6 @@ export * from './utils/log-config'; export * from './utils/normalize-stories'; export * from './utils/paths'; export * from './utils/readTemplate'; -export * from './utils/remove'; export * from './utils/resolve-path-in-sb-cache'; export * from './utils/symlinks'; export * from './utils/template'; @@ -46,6 +45,7 @@ export * from './utils/sync-main-preview-addons'; export * from './js-package-manager'; export * from './utils/scan-and-transform-files'; export * from './utils/transform-imports'; +export * from './utils/addons'; export { versions }; diff --git a/code/core/src/common/utils/addons.ts b/code/core/src/common/utils/addons.ts new file mode 100644 index 000000000000..de2726bb306c --- /dev/null +++ b/code/core/src/common/utils/addons.ts @@ -0,0 +1,236 @@ +import { readConfig, writeConfig } from 'storybook/internal/csf-tools'; + +import { dedent } from 'ts-dedent'; + +import type { PackageManagerName } from '../js-package-manager'; +import { JsPackageManagerFactory } from '../js-package-manager'; +import { getStorybookInfo } from './get-storybook-info'; +import { + JsPackageManagerFactory, + type PackageManagerName, + syncStorybookAddons, + versions, +} from 'storybook/internal/common'; +import { readConfig, writeConfig } from 'storybook/internal/csf-tools'; +import type { StorybookConfigRaw } from 'storybook/internal/types'; + +import prompts from 'prompts'; +import SemVer from 'semver'; +import { dedent } from 'ts-dedent'; + +import { + getRequireWrapperName, + wrapValueWithRequireWrapper, +} from './automigrate/fixes/wrap-require-utils'; +import { getStorybookData } from './automigrate/helpers/mainConfigFile'; +import { postinstallAddon } from './postinstallAddon'; + +export interface PostinstallOptions { + packageManager: PackageManagerName; + configDir: string; + yes?: boolean; +} + +/** + * Extract the addon name and version specifier from the input string + * + * @example + * + * ```ts + * getVersionSpecifier('@storybook/addon-docs@7.0.1') => ['@storybook/addon-docs', '7.0.1'] + * ``` + * + * @param addon - The input string + * @returns {undefined} AddonName, versionSpecifier + */ +export const getVersionSpecifier = (addon: string) => { + const groups = /^(@{0,1}[^@]+)(?:@(.+))?$/.exec(addon); + if (groups) { + return [groups[1], groups[2]] as const; + } + return [addon, undefined] as const; +}; + +const checkInstalled = (addonName: string, main: StorybookConfigRaw) => { + const existingAddon = main.addons?.find((entry: string | { name: string }) => { + const name = typeof entry === 'string' ? entry : entry.name; + return name?.endsWith(addonName); + }); + return !!existingAddon; +}; + +const isCoreAddon = (addonName: string) => Object.hasOwn(versions, addonName); + +export type AddOptions = { + packageManager?: PackageManagerName; + configDir?: string; + skipPostinstall?: boolean; + yes?: boolean; +}; + + + +const logger = console; + +export type RemoveAddonOptions = { + packageManager?: PackageManagerName; + cwd?: string; + configDir?: string; +}; + +/** + * Remove the given addon package and remove it from main.js + * + * @example + * + * ```sh + * sb remove @storybook/addon-links + * ``` + */ +export async function removeAddon(addon: string, options: RemoveAddonOptions = {}) { + const { packageManager: pkgMgr } = options; + + const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr }, options.cwd); + const packageJson = await packageManager.retrievePackageJson(); + const { mainConfig, configDir } = getStorybookInfo(packageJson, options.configDir); + + if (typeof configDir === 'undefined') { + throw new Error(dedent` + Unable to find storybook config directory + `); + } + + if (!mainConfig) { + logger.error('Unable to find storybook main.js config'); + return; + } + const main = await readConfig(mainConfig); + + // remove from package.json + logger.log(`Uninstalling ${addon}`); + await packageManager.removeDependencies({ packageJson }, [addon]); + + // add to main.js + logger.log(`Removing '${addon}' from main.js addons field.`); + try { + main.removeEntryFromArray(['addons'], addon); + await writeConfig(main); + } catch (err) { + logger.warn(`Failed to remove '${addon}' from main.js addons field.`); + } +} + +/** + * Install the given addon package and add it to main.js + * + * @example + * + * ```sh + * sb add "@storybook/addon-docs" + * sb add "@storybook/addon-vitest@9.0.1" + * ``` + * + * If there is no version specifier and it's a Storybook addon, it will try to use the version + * specifier matching your current Storybook install version. + */ +export async function add( + addon: string, + { packageManager: pkgMgr, skipPostinstall, configDir: userSpecifiedConfigDir, yes }: AddOptions, + logger = console +) { + const [addonName, inputVersion] = getVersionSpecifier(addon); + + const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr }); + const { mainConfig, mainConfigPath, configDir, previewConfigPath, storybookVersion } = + await getStorybookData({ + packageManager, + configDir: userSpecifiedConfigDir, + }); + + if (typeof configDir === 'undefined') { + throw new Error(dedent` + Unable to find storybook config directory. Please specify your Storybook config directory with the --config-dir flag. + `); + } + + if (!mainConfigPath) { + logger.error('Unable to find Storybook main.js config'); + return; + } + + let shouldAddToMain = true; + if (checkInstalled(addonName, mainConfig)) { + shouldAddToMain = false; + if (!yes) { + logger.log(`The Storybook addon "${addonName}" is already present in ${mainConfigPath}.`); + const { shouldForceInstall } = await prompts({ + type: 'confirm', + name: 'shouldForceInstall', + message: `Do you wish to install it again?`, + }); + + if (!shouldForceInstall) { + return; + } + } + } + + const main = await readConfig(mainConfigPath); + logger.log(`Verifying ${addonName}`); + + let version = inputVersion; + + if (!version && isCoreAddon(addonName) && storybookVersion) { + version = storybookVersion; + } + if (!version) { + version = await packageManager.latestVersion(addonName); + } + + if (isCoreAddon(addonName) && version !== storybookVersion) { + logger.warn( + `The version of ${addonName} (${version}) you are installing is not the same as the version of Storybook you are using (${storybookVersion}). This may lead to unexpected behavior.` + ); + } + + const addonWithVersion = + isValidVersion(version) && !version.includes('-pr-') + ? `${addonName}@^${version}` + : `${addonName}@${version}`; + + logger.log(`Installing ${addonWithVersion}`); + await packageManager.addDependencies( + { installAsDevDependencies: true, writeOutputToFile: false }, + [addonWithVersion] + ); + + if (shouldAddToMain) { + logger.log(`Adding '${addon}' to the "addons" field in ${mainConfigPath}`); + + 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); + } + + // TODO: remove try/catch once CSF factories is shipped, for now gracefully handle any error + try { + await syncStorybookAddons(mainConfig, previewConfigPath!); + } catch (e) { + // + } + + if (!skipPostinstall && isCoreAddon(addonName)) { + await postinstallAddon(addonName, { packageManager: packageManager.type, configDir, yes }); + } +} + +function isValidVersion(version: string) { + return SemVer.valid(version) || version.match(/^\d+$/); +} diff --git a/code/core/src/common/utils/remove.ts b/code/core/src/common/utils/remove.ts deleted file mode 100644 index 91dda44dc357..000000000000 --- a/code/core/src/common/utils/remove.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { readConfig, writeConfig } from 'storybook/internal/csf-tools'; - -import { dedent } from 'ts-dedent'; - -import type { PackageManagerName } from '../js-package-manager'; -import { JsPackageManagerFactory } from '../js-package-manager'; -import { getStorybookInfo } from './get-storybook-info'; - -const logger = console; - -/** - * Remove the given addon package and remove it from main.js - * - * @example - * - * ```sh - * sb remove @storybook/addon-links - * ``` - */ -export async function removeAddon( - addon: string, - options: { packageManager?: PackageManagerName; cwd?: string; configDir?: string } = {} -) { - const { packageManager: pkgMgr } = options; - - const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr }, options.cwd); - const packageJson = await packageManager.retrievePackageJson(); - const { mainConfig, configDir, ...rest } = getStorybookInfo(packageJson, options.configDir); - - if (typeof configDir === 'undefined') { - throw new Error(dedent` - Unable to find storybook config directory - `); - } - - if (!mainConfig) { - logger.error('Unable to find storybook main.js config'); - return; - } - const main = await readConfig(mainConfig); - - // remove from package.json - logger.log(`Uninstalling ${addon}`); - await packageManager.removeDependencies({ packageJson }, [addon]); - - // add to main.js - logger.log(`Removing '${addon}' from main.js addons field.`); - try { - main.removeEntryFromArray(['addons'], addon); - await writeConfig(main); - } catch (err) { - logger.warn(`Failed to remove '${addon}' from main.js addons field.`); - } -} diff --git a/code/core/src/core-server/presets/common-preset.ts b/code/core/src/core-server/presets/common-preset.ts index f0d9dc2c1a73..9f8ef726cea3 100644 --- a/code/core/src/core-server/presets/common-preset.ts +++ b/code/core/src/core-server/presets/common-preset.ts @@ -186,17 +186,6 @@ const optionalEnvToBoolean = (input: string | undefined): boolean | undefined => return undefined; }; -export const experimental_serverAPI = (extension: Record, options: Options) => { - let removeAddon = removeAddonBase; - if (!options.disableTelemetry) { - removeAddon = async (id: string, opts: any) => { - await telemetry('remove', { addon: id, source: 'api' }); - return removeAddonBase(id, opts); - }; - } - return { ...extension, removeAddon }; -}; - /** * If for some reason this config is not applied, the reason is that likely there is an addon that * does `export core = () => ({ someConfig })`, instead of `export core = (existing) => ({ diff --git a/code/core/src/telemetry/types.ts b/code/core/src/telemetry/types.ts index 078318447c12..89bc07053d48 100644 --- a/code/core/src/telemetry/types.ts +++ b/code/core/src/telemetry/types.ts @@ -19,6 +19,7 @@ export type EventType = | 'version-update' | 'core-config' | 'remove' + | 'add-addon' | 'save-story' | 'create-new-story-file' | 'create-new-story-file-search' diff --git a/code/lib/cli-storybook/src/add.ts b/code/lib/cli-storybook/src/add.ts index b9f2a01fdfe0..54d1177dc012 100644 --- a/code/lib/cli-storybook/src/add.ts +++ b/code/lib/cli-storybook/src/add.ts @@ -1,9 +1,6 @@ -import { isAbsolute, join } from 'node:path'; - import { JsPackageManagerFactory, type PackageManagerName, - serverRequire, syncStorybookAddons, versions, } from 'storybook/internal/common'; @@ -47,13 +44,6 @@ export const getVersionSpecifier = (addon: string) => { return [addon, undefined] as const; }; -const requireMain = (configDir: string) => { - const absoluteConfigDir = isAbsolute(configDir) ? configDir : join(process.cwd(), configDir); - const mainFile = join(absoluteConfigDir, 'main'); - - return serverRequire(mainFile) ?? {}; -}; - const checkInstalled = (addonName: string, main: StorybookConfigRaw) => { const existingAddon = main.addons?.find((entry: string | { name: string }) => { const name = typeof entry === 'string' ? entry : entry.name; @@ -64,10 +54,10 @@ const checkInstalled = (addonName: string, main: StorybookConfigRaw) => { const isCoreAddon = (addonName: string) => Object.hasOwn(versions, addonName); -type CLIOptions = { +export type AddOptions = { packageManager?: PackageManagerName; configDir?: string; - skipPostinstall: boolean; + skipPostinstall?: boolean; yes?: boolean; }; @@ -86,7 +76,7 @@ type CLIOptions = { */ export async function add( addon: string, - { packageManager: pkgMgr, skipPostinstall, configDir: userSpecifiedConfigDir, yes }: CLIOptions, + { packageManager: pkgMgr, skipPostinstall, configDir: userSpecifiedConfigDir, yes }: AddOptions, logger = console ) { const [addonName, inputVersion] = getVersionSpecifier(addon); @@ -181,6 +171,7 @@ export async function add( await postinstallAddon(addonName, { packageManager: packageManager.type, configDir, yes }); } } + function isValidVersion(version: string) { return SemVer.valid(version) || version.match(/^\d+$/); } diff --git a/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.ts b/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.ts index 4fd12eebfcd6..57ccfb061df1 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.ts @@ -39,7 +39,7 @@ interface AddonA11yAddonTestOptions { * - `.storybook/preview.` to set up tags. * - If we can't transform the files automatically, we'll prompt the user to do it manually. */ -export const addonA11yAddonTest: Fix = { +export const addonA11yAddonTest: Fix = { id: 'addonA11yAddonTest', versionRange: ['<9.0.0', '^9.0.0-0 || ^9.0.0'], diff --git a/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-parameters.ts b/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-parameters.ts index 885fe7a61665..dd573298a4d6 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-parameters.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-parameters.ts @@ -21,7 +21,7 @@ interface A11yOptions { const logger = console; -export const addonA11yParameters: Fix = { +export const addonA11yParameters: Fix = { id: 'addon-a11y-parameters', versionRange: ['<9.0.0', '^9.0.0-0 || ^9.0.0'], diff --git a/code/lib/cli-storybook/src/automigrate/fixes/addon-essentials-remove-docs.ts b/code/lib/cli-storybook/src/automigrate/fixes/addon-essentials-remove-docs.ts index 4434dd02b7e6..93a33921206d 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/addon-essentials-remove-docs.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/addon-essentials-remove-docs.ts @@ -34,7 +34,7 @@ const consolidatedAddons = { * - If user had docs enabled (default): Install @storybook/addon-docs and add to main.ts * - If user had docs disabled: Skip addon-docs installation */ -export const addonEssentialsRemoveDocs: Fix = { +export const addonEssentialsRemoveDocs: Fix = { id: 'addon-essentials-remove-docs', versionRange: ['<9.0.0', '^9.0.0-0 || ^9.0.0'], diff --git a/code/lib/cli-storybook/src/automigrate/fixes/addon-experimental-test.ts b/code/lib/cli-storybook/src/automigrate/fixes/addon-experimental-test.ts index 4d4c246cf518..2580cbdb3db6 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/addon-experimental-test.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/addon-experimental-test.ts @@ -19,7 +19,7 @@ interface AddonExperimentalTestOptions { * project files * - Update package.json dependencies if needed */ -export const addonExperimentalTest: Fix = { +export const addonExperimentalTest: Fix = { id: 'addon-experimental-test', versionRange: ['<9.0.0', '^9.0.0-0 || ^9.0.0'], diff --git a/code/lib/cli-storybook/src/automigrate/fixes/addon-mdx-gfm-remove.ts b/code/lib/cli-storybook/src/automigrate/fixes/addon-mdx-gfm-remove.ts index 2f9dec4a99a2..bfb13644cf9c 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/addon-mdx-gfm-remove.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/addon-mdx-gfm-remove.ts @@ -14,7 +14,7 @@ interface AddonMdxGfmOptions { * * - Remove @storybook/addon-mdx-gfm from main.ts and package.json */ -export const addonMdxGfmRemove: Fix = { +export const addonMdxGfmRemove: Fix = { id: 'addon-mdx-gfm-remove', versionRange: ['<9.0.0', '^9.0.0-0 || ^9.0.0'], diff --git a/code/lib/cli-storybook/src/automigrate/fixes/addon-storysource-remove.ts b/code/lib/cli-storybook/src/automigrate/fixes/addon-storysource-remove.ts index 21b510d84831..becae93f4b68 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/addon-storysource-remove.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/addon-storysource-remove.ts @@ -15,7 +15,7 @@ interface AddonStorysourceOptions { * * - Remove @storybook/addon-storysource from main.ts and package.json */ -export const addonStorysourceRemove: Fix = { +export const addonStorysourceRemove: Fix = { id: 'addon-storysource-remove', versionRange: ['<9.0.0', '^9.0.0-0 || ^9.0.0'], diff --git a/code/lib/cli-storybook/src/automigrate/fixes/consolidated-imports.ts b/code/lib/cli-storybook/src/automigrate/fixes/consolidated-imports.ts index 6857c12c713a..cc01a8e8806a 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/consolidated-imports.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/consolidated-imports.ts @@ -97,7 +97,7 @@ export const transformPackageJsonFiles = async (files: string[], dryRun: boolean return errors; }; -export const consolidatedImports: Fix = { +export const consolidatedImports: Fix = { id: 'consolidated-imports', versionRange: ['<9.0.0', '^9.0.0-0 || ^9.0.0'], check: async () => { diff --git a/code/lib/cli-storybook/src/automigrate/fixes/eslint-plugin.ts b/code/lib/cli-storybook/src/automigrate/fixes/eslint-plugin.ts index 74369781e8d2..7d722d8a62da 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/eslint-plugin.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/eslint-plugin.ts @@ -24,7 +24,7 @@ interface EslintPluginRunOptions { * * - Install it, and if possible configure it */ -export const eslintPlugin: Fix = { +export const eslintPlugin: Fix = { id: 'eslintPlugin', versionRange: ['*', '*'], diff --git a/code/lib/cli-storybook/src/automigrate/fixes/index.ts b/code/lib/cli-storybook/src/automigrate/fixes/index.ts index 22dc5cb4ad4f..63e333ecdd3a 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/index.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/index.ts @@ -18,7 +18,7 @@ import { wrapRequire } from './wrap-require'; export * from '../types'; -export const allFixes: Fix[] = [ +export const allFixes = [ eslintPlugin, wrapRequire, addonMdxGfmRemove, @@ -34,10 +34,12 @@ export const allFixes: Fix[] = [ addonEssentialsRemoveDocs, addonA11yParameters, removeDocsAutodocs, -]; +] satisfies Fix[]; -export const initFixes: Fix[] = [eslintPlugin]; +export type FixesIDs[]> = F[number]['id']; + +export const initFixes = [eslintPlugin] satisfies Fix[]; // These are specific fixes that only occur when triggered on command, and are hidden otherwise. // e.g. npx storybook automigrate csf-factories -export const commandFixes: CommandFix[] = [csfFactories]; +export const commandFixes = [csfFactories] satisfies CommandFix[]; diff --git a/code/lib/cli-storybook/src/automigrate/fixes/initial-globals.ts b/code/lib/cli-storybook/src/automigrate/fixes/initial-globals.ts index fde14f7e1381..64379ebc82da 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/initial-globals.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/initial-globals.ts @@ -19,7 +19,7 @@ interface Options { } /** Rename preview.js globals to initialGlobals */ -export const initialGlobals: Fix = { +export const initialGlobals: Fix = { id: 'initial-globals', versionRange: ['<9.0.0', '^9.0.0-0 || ^9.0.0'], async check({ previewConfigPath }) { diff --git a/code/lib/cli-storybook/src/automigrate/fixes/remove-addon-interactions.ts b/code/lib/cli-storybook/src/automigrate/fixes/remove-addon-interactions.ts index a4d684a3eae0..b7804a8091b7 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/remove-addon-interactions.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/remove-addon-interactions.ts @@ -6,7 +6,7 @@ import { dedent } from 'ts-dedent'; import type { Fix } from '../types'; /** Remove @storybook/addon-interactions since it's now part of Storybook core. */ -export const removeAddonInteractions: Fix<{}> = { +export const removeAddonInteractions: Fix<{}, 'removeAddonInteractions'> = { id: 'removeAddonInteractions', versionRange: ['<9.0.0', '^9.0.0-0 || ^9.0.0'], diff --git a/code/lib/cli-storybook/src/automigrate/fixes/remove-docs-autodocs.ts b/code/lib/cli-storybook/src/automigrate/fixes/remove-docs-autodocs.ts index 58982a242a49..9f86732d794a 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/remove-docs-autodocs.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/remove-docs-autodocs.ts @@ -22,7 +22,7 @@ interface RemoveDocsAutodocsOptions { * Migration to remove the docs.autodocs field from main.ts config This field was deprecated in * Storybook 7-8 and removed in Storybook 9 */ -export const removeDocsAutodocs: Fix = { +export const removeDocsAutodocs: Fix = { id: 'remove-docs-autodocs', versionRange: ['<9.0.0', '^9.0.0-0 || ^9.0.0'], diff --git a/code/lib/cli-storybook/src/automigrate/fixes/renderer-to-framework.ts b/code/lib/cli-storybook/src/automigrate/fixes/renderer-to-framework.ts index dc96b42cd3a2..553841ab05ad 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/renderer-to-framework.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/renderer-to-framework.ts @@ -138,7 +138,7 @@ const checkPackageJson = async ( return { frameworks, renderers }; }; -export const rendererToFramework: Fix = { +export const rendererToFramework: Fix = { id: 'renderer-to-framework', versionRange: ['<9.0.0', '^9.0.0-0'], promptType: 'auto', diff --git a/code/lib/cli-storybook/src/automigrate/fixes/rnstorybook-config.ts b/code/lib/cli-storybook/src/automigrate/fixes/rnstorybook-config.ts index 60ab6a992520..a3ddd2166963 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/rnstorybook-config.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/rnstorybook-config.ts @@ -35,7 +35,7 @@ const getDotStorybookReferences = async () => { } }; -export const rnstorybookConfig: Fix = { +export const rnstorybookConfig: Fix = { id: 'rnstorybook-config', versionRange: ['<9.0.0', '^9.0.0-0 || ^9.0.0'], diff --git a/code/lib/cli-storybook/src/automigrate/fixes/upgrade-storybook-related-dependencies.ts b/code/lib/cli-storybook/src/automigrate/fixes/upgrade-storybook-related-dependencies.ts index 40200ab9ac41..e44e77be0d7e 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/upgrade-storybook-related-dependencies.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/upgrade-storybook-related-dependencies.ts @@ -40,7 +40,10 @@ async function getLatestVersions( * * See: https://github.com/storybookjs/storybook/issues/25731#issuecomment-1977346398 */ -export const upgradeStorybookRelatedDependencies = { +export const upgradeStorybookRelatedDependencies: Fix< + Options, + 'upgradeStorybookRelatedDependencies' +> = { id: 'upgradeStorybookRelatedDependencies', versionRange: ['*.*.*', '*.*.*'], promptType: 'auto', @@ -149,4 +152,4 @@ export const upgradeStorybookRelatedDependencies = { } console.log(); }, -} satisfies Fix; +}; diff --git a/code/lib/cli-storybook/src/automigrate/fixes/wrap-require.ts b/code/lib/cli-storybook/src/automigrate/fixes/wrap-require.ts index 7a2b09e3315a..ff9b4e47f7b1 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/wrap-require.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/wrap-require.ts @@ -21,7 +21,7 @@ interface WrapRequireRunOptions { isConfigTypescript: boolean; } -export const wrapRequire: Fix = { +export const wrapRequire: Fix = { id: 'wrap-require', versionRange: ['*', '*'], diff --git a/code/lib/cli-storybook/src/automigrate/index.ts b/code/lib/cli-storybook/src/automigrate/index.ts index 9184a56eb69f..207b59cde342 100644 --- a/code/lib/cli-storybook/src/automigrate/index.ts +++ b/code/lib/cli-storybook/src/automigrate/index.ts @@ -164,7 +164,7 @@ export const automigrate = async ({ return null; } - const selectedFixes: Fix[] = + const selectedFixes: Fix[] = inputFixes || allFixes.filter((fix) => { // we only allow this automigration when the user explicitly asks for it, or they are upgrading to the latest version of storybook @@ -178,7 +178,9 @@ export const automigrate = async ({ return true; }); - const fixes: Fix[] = fixId ? selectedFixes.filter((f) => f.id === fixId) : selectedFixes; + const fixes: Fix[] = fixId + ? selectedFixes.filter((f) => f.id === fixId) + : selectedFixes; if (fixId && fixes.length === 0) { logger.info(`📭 No migrations found for ${picocolors.magenta(fixId)}.`); @@ -252,7 +254,7 @@ export async function runFixes({ beforeVersion, isUpgrade, }: { - fixes: Fix[]; + fixes: Fix[]; yes?: boolean; dryRun?: boolean; rendererPackage?: string; @@ -275,7 +277,7 @@ export async function runFixes({ const fixSummary: FixSummary = { succeeded: [], failed: {}, manual: [], skipped: [] }; for (let i = 0; i < fixes.length; i += 1) { - const f = fixes[i] as Fix; + const f = fixes[i] as Fix; let result; try { diff --git a/code/lib/cli-storybook/src/automigrate/types.ts b/code/lib/cli-storybook/src/automigrate/types.ts index 61e7da45a09a..5a8e363fd0c3 100644 --- a/code/lib/cli-storybook/src/automigrate/types.ts +++ b/code/lib/cli-storybook/src/automigrate/types.ts @@ -34,8 +34,8 @@ export interface RunOptions { */ export type Prompt = 'auto' | 'manual' | 'notification' | 'command'; -type BaseFix = { - id: string; +type BaseFix = { + id: Key; /** * The from/to version range of Storybook that this fix applies to. The strings are semver ranges. * The versionRange will only be checked if the automigration is part of an upgrade. If the @@ -52,20 +52,20 @@ type PromptType = | T | ((result: ResultType) => Promise | Prompt); -export type Fix = +export type Fix = | ({ promptType?: PromptType; run: (options: RunOptions) => Promise; - } & BaseFix) + } & BaseFix) | ({ promptType: PromptType; run?: never; - } & BaseFix); + } & BaseFix); -export type CommandFix = { +export type CommandFix = { promptType: PromptType; run: (options: RunOptions) => Promise; -} & Omit, 'versionRange' | 'check' | 'prompt'>; +} & Omit, 'versionRange' | 'check' | 'prompt'>; export type FixId = string; @@ -87,15 +87,16 @@ export interface AutofixOptions extends Omit[]; yes?: boolean; packageManager?: PackageManagerName; dryRun?: boolean; - configDir: string; + configDir?: string; renderer?: string; skipInstall?: boolean; hideMigrationSummary?: boolean; diff --git a/code/lib/cli-storybook/src/bin/index.ts b/code/lib/cli-storybook/src/bin/index.ts index 4b199ffb4eb6..b74867179b37 100644 --- a/code/lib/cli-storybook/src/bin/index.ts +++ b/code/lib/cli-storybook/src/bin/index.ts @@ -1,8 +1,4 @@ -import { - JsPackageManagerFactory, - removeAddon as remove, - versions, -} from 'storybook/internal/common'; +import { JsPackageManagerFactory, removeAddon, versions } from 'storybook/internal/common'; import { withTelemetry } from 'storybook/internal/core-server'; import { logger } from 'storybook/internal/node-logger'; import { addToGlobalContext, telemetry } from 'storybook/internal/telemetry'; @@ -67,7 +63,15 @@ command('add ') .option('-c, --config-dir ', 'Directory where to load Storybook configurations from') .option('-s --skip-postinstall', 'Skip package specific postinstall config modifications') .option('-y --yes', 'Skip prompting the user') - .action((addonName: string, options: any) => add(addonName, options)); + .action((addonName: string, options: any) => { + withTelemetry('add-addon', { cliOptions: options }, async () => { + await add(addonName, options)); + if (!options.disableTelemetry) { + await telemetry('add', { addon: addonName, source: 'cli' }); + } + }) + + } command('remove ') .description('Remove an addon from your Storybook') @@ -78,7 +82,7 @@ command('remove ') .option('-c, --config-dir ', 'Directory where to load Storybook configurations from') .action((addonName: string, options: any) => withTelemetry('remove', { cliOptions: options }, async () => { - await remove(addonName, options); + await removeAddon(addonName, options); if (!options.disableTelemetry) { await telemetry('remove', { addon: addonName, source: 'cli' }); } diff --git a/code/lib/cli-storybook/src/doctor/index.ts b/code/lib/cli-storybook/src/doctor/index.ts index 1c7a03477c80..1230c9485d4e 100644 --- a/code/lib/cli-storybook/src/doctor/index.ts +++ b/code/lib/cli-storybook/src/doctor/index.ts @@ -44,7 +44,7 @@ const cleanup = () => { process.stderr.write = originalStdErrWrite; }; -type DoctorOptions = { +export type DoctorOptions = { configDir?: string; packageManager?: PackageManagerName; }; diff --git a/code/lib/cli-storybook/src/remove.ts b/code/lib/cli-storybook/src/remove.ts new file mode 100644 index 000000000000..710044f6705a --- /dev/null +++ b/code/lib/cli-storybook/src/remove.ts @@ -0,0 +1,11 @@ +import { withTelemetry } from 'storybook/internal/core-server'; +import { telemetry } from 'storybook/internal/telemetry'; + +export function remove(addonName: string, options: RemoveOptions) { + withTelemetry('remove', { cliOptions: options }, async () => { + await remove(addonName, options); + if (!options.disableTelemetry) { + await telemetry('remove', { addon: addonName, source: 'cli' }); + } + }); +}