diff --git a/code/lib/cli/src/automigrate/fixes/index.ts b/code/lib/cli/src/automigrate/fixes/index.ts index 924c558d331..1fd9ba4c55e 100644 --- a/code/lib/cli/src/automigrate/fixes/index.ts +++ b/code/lib/cli/src/automigrate/fixes/index.ts @@ -17,6 +17,7 @@ import { autodocsTrue } from './autodocs-true'; import { sveltekitFramework } from './sveltekit-framework'; import { addReact } from './add-react'; import { nodeJsRequirement } from './nodejs-requirement'; +import { missingBabelRc } from './missing-babelrc'; export * from '../types'; @@ -38,4 +39,5 @@ export const fixes: Fix[] = [ mdx1to2, autodocsTrue, addReact, + missingBabelRc, ]; diff --git a/code/lib/cli/src/automigrate/fixes/missing-babelrc.test.ts b/code/lib/cli/src/automigrate/fixes/missing-babelrc.test.ts new file mode 100644 index 00000000000..9aa0d770e4c --- /dev/null +++ b/code/lib/cli/src/automigrate/fixes/missing-babelrc.test.ts @@ -0,0 +1,91 @@ +/* eslint-disable no-underscore-dangle */ +/// ; + +import path from 'path'; +import type { JsPackageManager } from '../../js-package-manager'; +import { missingBabelRc } from './missing-babelrc'; + +// eslint-disable-next-line global-require, jest/no-mocks-import +jest.mock('fs-extra', () => require('../../../../../__mocks__/fs-extra')); + +const babelContent = JSON.stringify({ + sourceType: 'unambiguous', + presets: [ + [ + '@babel/preset-env', + { + targets: { + chrome: 100, + }, + }, + ], + '@babel/preset-typescript', + '@babel/preset-react', + ], + plugins: [], +}); + +const check = async ({ packageJson = {}, main = {}, extraFiles }: any) => { + if (extraFiles) { + // eslint-disable-next-line global-require + require('fs-extra').__setMockFiles({ + [path.join('.storybook', 'main.js')]: `module.exports = ${JSON.stringify(main)};`, + ...extraFiles, + }); + } + const packageManager = { + retrievePackageJson: () => ({ dependencies: {}, devDependencies: {}, ...packageJson }), + } as JsPackageManager; + return missingBabelRc.check({ packageManager }); +}; + +describe('missing-babelrc fix', () => { + it('skips when babelrc config is present', async () => { + const packageJson = { + devDependencies: { + '@storybook/react': '^7.0.0', + '@storybook/react-webpack5': '^7.0.0', + }, + }; + + // different babel extensions + await expect( + check({ extraFiles: { '.babelrc': babelContent }, packageJson }) + ).resolves.toBeNull(); + await expect( + check({ extraFiles: { '.babelrc.json': babelContent }, packageJson }) + ).resolves.toBeNull(); + await expect( + check({ extraFiles: { 'babel.config.json': babelContent }, packageJson }) + ).resolves.toBeNull(); + + // babel field in package.json + await expect( + check({ packageJson: { ...packageJson, babel: babelContent } }) + ).resolves.toBeNull(); + }); + + it('skips when using a framework that provides babel config', async () => { + const packageJson = { + devDependencies: { + '@storybook/react': '^7.0.0', + '@storybook/nextjs': '^7.0.0', + }, + }; + + await expect(check({ packageJson })).resolves.toBeNull(); + }); + + it('prompts when babelrc file is missing and framework does not provide babel config', async () => { + const packageJson = { + devDependencies: { + '@storybook/react': '^7.0.0', + '@storybook/react-webpack5': '^7.0.0', + }, + }; + + await expect(check({ packageJson })).resolves.toBe({ + needsBabelRc: true, + }); + }); +}); diff --git a/code/lib/cli/src/automigrate/fixes/missing-babelrc.ts b/code/lib/cli/src/automigrate/fixes/missing-babelrc.ts new file mode 100644 index 00000000000..10b64d67858 --- /dev/null +++ b/code/lib/cli/src/automigrate/fixes/missing-babelrc.ts @@ -0,0 +1,91 @@ +import chalk from 'chalk'; +import dedent from 'ts-dedent'; +import semver from 'semver'; +import { getStorybookInfo } from '@storybook/core-common'; +import { loadPartialConfigAsync } from '@babel/core'; +import { readConfig } from '@storybook/csf-tools'; +import type { Fix } from '../types'; + +interface MissingBabelRcOptions { + needsBabelRc: boolean; +} + +const logger = console; + +const frameworksThatNeedBabelConfig = [ + '@storybook/angular', + '@storybook/react-webpack5', + '@storybook/vue-webpack5', + '@storybook/vue3-webpack5', + '@storybook/preact-webpack5', + '@storybook/html-webpack5', + '@storybook/react-vite', + '@storybook/vue-vite', + '@storybook/vue3-vite', + '@storybook/preact-vite', + '@storybook/html-vite', +]; + +export const missingBabelRc: Fix = { + id: 'missing-babelrc', + promptOnly: true, + + async check({ packageManager }) { + const packageJson = packageManager.retrievePackageJson(); + const { mainConfig, version: storybookVersion } = getStorybookInfo(packageJson); + + const storybookCoerced = storybookVersion && semver.coerce(storybookVersion)?.version; + if (!storybookCoerced) { + throw new Error(dedent` + ❌ Unable to determine storybook version. + 🤔 Are you running automigrate from your project directory? + `); + } + + if (!semver.gte(storybookCoerced, '7.0.0')) { + return null; + } + + if (!mainConfig) { + logger.warn('Unable to find storybook main.js config, skipping'); + return null; + } + + const main = await readConfig(mainConfig); + + const frameworkField = main.getFieldValue(['framework']); + const frameworkPackage = + typeof frameworkField === 'string' ? frameworkField : frameworkField?.name; + + if (frameworksThatNeedBabelConfig.includes(frameworkPackage)) { + const config = await loadPartialConfigAsync(); + if (!config.config && !packageJson.babel) { + return { needsBabelRc: true }; + } + } + + return null; + }, + prompt() { + return dedent` + ${chalk.bold( + chalk.red('Attention') + )}: We could not automatically make this change. You'll need to do it manually. + + Storybook now uses Babel mode v7 exclusively. In 6.x, Storybook provided its own babel settings out of the box. Now, Storybook's uses your project's babel settings (.babelrc, babel.config.js, etc.) instead. + + In the new mode, Storybook expects you to provide a configuration file. If you want a configuration file that's equivalent to the 6.x default, you can run the following command in your project directory: + + ${chalk.blue('npx sb@next babelrc')} + + This will create a ${chalk.blue( + '.babelrc.json' + )} file with some basic configuration and add new package devDependencies accordingly. + + Please see the migration guide for more information: + ${chalk.yellow( + 'https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#babel-mode-v7-exclusively' + )} + `; + }, +};