diff --git a/code/lib/cli/src/automigrate/fixes/index.ts b/code/lib/cli/src/automigrate/fixes/index.ts index 27f8a80b3140..e08f9b13b0c7 100644 --- a/code/lib/cli/src/automigrate/fixes/index.ts +++ b/code/lib/cli/src/automigrate/fixes/index.ts @@ -20,6 +20,7 @@ import { wrapRequire } from './wrap-require'; import { reactDocgen } from './react-docgen'; import { removeReactDependency } from './prompt-remove-react'; import { storyshotsMigration } from './storyshots-migration'; +import { removeJestTestingLibrary } from './remove-jest-testing-library'; export * from '../types'; @@ -34,6 +35,7 @@ export const allFixes: Fix[] = [ sbBinary, sbScripts, incompatibleAddons, + removeJestTestingLibrary, removedGlobalClientAPIs, mdx1to2, mdxgfm, diff --git a/code/lib/cli/src/automigrate/fixes/remove-jest-testing-library.test.ts b/code/lib/cli/src/automigrate/fixes/remove-jest-testing-library.test.ts new file mode 100644 index 000000000000..60a2c2a97a35 --- /dev/null +++ b/code/lib/cli/src/automigrate/fixes/remove-jest-testing-library.test.ts @@ -0,0 +1,64 @@ +import { expect, it } from 'vitest'; + +import type { StorybookConfig } from '@storybook/types'; +import type { JsPackageManager } from '@storybook/core-common'; +import { removeJestTestingLibrary } from './remove-jest-testing-library'; +import ansiRegex from 'ansi-regex'; + +const check = async ({ + packageManager, + main: mainConfig = {}, + storybookVersion = '8.0.0', +}: { + packageManager: Partial; + main?: Partial & Record; + storybookVersion?: string; +}) => { + return removeJestTestingLibrary.check({ + packageManager: packageManager as any, + configDir: '', + mainConfig: mainConfig as any, + storybookVersion, + }); +}; + +it('should prompt to install the test package and run the codemod', async () => { + const options = await check({ + packageManager: { + getAllDependencies: async () => ({ + '@storybook/jest': '1.0.0', + '@storybook/testing-library': '1.0.0', + }), + }, + main: { addons: ['@storybook/essentials', '@storybook/addon-info'] }, + }); + + await expect(options).toMatchInlineSnapshot(` + { + "incompatiblePackages": [ + "@storybook/jest", + "@storybook/testing-library", + ], + } + `); + + expect.addSnapshotSerializer({ + serialize: (value) => { + const stringVal = typeof value === 'string' ? value : value.toString(); + return stringVal.replace(ansiRegex(), ''); + }, + test: () => true, + }); + + expect(await removeJestTestingLibrary.prompt(options!)).toMatchInlineSnapshot(` + Attention: We've detected that you're using the following packages which are known to be incompatible with Storybook 8: + + - @storybook/jest + - @storybook/testing-library + + Install the replacement for those packages: @storybook/test + + And run the following codemod: + npx storybook migrate migrate-to-test-package --glob="**/*.stories.@(js|jsx|ts|tsx)" + `); +}); diff --git a/code/lib/cli/src/automigrate/fixes/remove-jest-testing-library.ts b/code/lib/cli/src/automigrate/fixes/remove-jest-testing-library.ts new file mode 100644 index 000000000000..87cf964468b3 --- /dev/null +++ b/code/lib/cli/src/automigrate/fixes/remove-jest-testing-library.ts @@ -0,0 +1,32 @@ +import chalk from 'chalk'; +import dedent from 'ts-dedent'; +import type { Fix } from '../types'; + +export const removeJestTestingLibrary: Fix<{ incompatiblePackages: string[] }> = { + id: 'remove-jest-testing-library', + promptOnly: true, + async check({ mainConfig, packageManager }) { + const deps = await packageManager.getAllDependencies(); + + const incompatiblePackages = Object.keys(deps).filter( + (it) => it === '@storybook/jest' || it === '@storybook/testing-library' + ); + return incompatiblePackages.length ? { incompatiblePackages } : null; + }, + prompt({ incompatiblePackages }) { + return dedent` + ${chalk.bold( + 'Attention' + )}: We've detected that you're using the following packages which are known to be incompatible with Storybook 8: + + ${incompatiblePackages.map((name) => `- ${chalk.cyan(`${name}`)}`).join('\n')} + + Install the replacement for those packages: ${chalk.cyan('@storybook/test')} + + And run the following codemod: + ${chalk.cyan( + 'npx storybook migrate migrate-to-test-package --glob="**/*.stories.@(js|jsx|ts|tsx)"' + )} + `; + }, +}; diff --git a/code/lib/codemod/package.json b/code/lib/codemod/package.json index 04fa46508190..259aae0056a3 100644 --- a/code/lib/codemod/package.json +++ b/code/lib/codemod/package.json @@ -31,6 +31,7 @@ "./dist/transforms/csf-hoist-story-annotations.js": "./dist/transforms/csf-hoist-story-annotations.js", "./dist/transforms/move-builtin-addons.js": "./dist/transforms/move-builtin-addons.js", "./dist/transforms/mdx-to-csf.js": "./dist/transforms/mdx-to-csf.js", + "./dist/transforms/migrate-to-test-package.js": "./dist/transforms/migrate-to-test-package.js", "./dist/transforms/storiesof-to-csf.js": "./dist/transforms/storiesof-to-csf.js", "./dist/transforms/update-addon-info.js": "./dist/transforms/update-addon-info.js", "./dist/transforms/update-organisation-name.js": "./dist/transforms/update-organisation-name.js", @@ -93,6 +94,7 @@ "./src/transforms/csf-2-to-3.ts", "./src/transforms/csf-hoist-story-annotations.js", "./src/transforms/mdx-to-csf.ts", + "./src/transforms/migrate-to-test-package.ts", "./src/transforms/move-builtin-addons.js", "./src/transforms/storiesof-to-csf.js", "./src/transforms/update-addon-info.js", diff --git a/code/lib/codemod/src/transforms/__tests__/migrate-to-test-package.test.ts b/code/lib/codemod/src/transforms/__tests__/migrate-to-test-package.test.ts new file mode 100644 index 000000000000..e6acb4b60279 --- /dev/null +++ b/code/lib/codemod/src/transforms/__tests__/migrate-to-test-package.test.ts @@ -0,0 +1,46 @@ +import { expect, test } from 'vitest'; +import transform from '../migrate-to-test-package'; +import dedent from 'ts-dedent'; + +expect.addSnapshotSerializer({ + serialize: (val: any) => (typeof val === 'string' ? val : val.toString()), + test: () => true, +}); + +const tsTransform = async (source: string) => + (await transform({ source, path: 'Component.stories.tsx' })).trim(); + +test('replace jest and testing-library with the test package', async () => { + const input = dedent` + import { expect } from '@storybook/jest'; + import { within, userEvent } from '@storybook/testing-library'; + `; + + expect(await tsTransform(input)).toMatchInlineSnapshot(` + import { expect } from '@storybook/test'; + import { within, userEvent } from '@storybook/test'; + `); +}); + +test('Make jest imports namespace imports', async () => { + const input = dedent` + import { expect, jest } from '@storybook/jest'; + import { within, userEvent } from '@storybook/testing-library'; + + const onFocusMock = jest.fn(); + const onSearchMock = jest.fn(); + + jest.spyOn(window, 'Something'); + `; + + expect(await tsTransform(input)).toMatchInlineSnapshot(` + import { expect } from '@storybook/test'; + import * as test from '@storybook/test'; + import { within, userEvent } from '@storybook/test'; + + const onFocusMock = test.fn(); + const onSearchMock = test.fn(); + + test.spyOn(window, 'Something'); + `); +}); diff --git a/code/lib/codemod/src/transforms/csf-2-to-3.ts b/code/lib/codemod/src/transforms/csf-2-to-3.ts index 38677f07f702..507c38999c88 100644 --- a/code/lib/codemod/src/transforms/csf-2-to-3.ts +++ b/code/lib/codemod/src/transforms/csf-2-to-3.ts @@ -204,16 +204,8 @@ export default async function transform(info: FileInfo, api: API, options: { par let output = printCsf(csf).code; try { - const prettierConfig = (await prettier.resolveConfig(info.path)) ?? { - printWidth: 100, - tabWidth: 2, - bracketSpacing: true, - trailingComma: 'es5', - singleQuote: true, - }; - output = await prettier.format(output, { - ...prettierConfig, + ...(await prettier.resolveConfig(info.path)), filepath: info.path, }); } catch (e) { diff --git a/code/lib/codemod/src/transforms/mdx-to-csf.ts b/code/lib/codemod/src/transforms/mdx-to-csf.ts index 53e2162ab19b..9c657c822e04 100644 --- a/code/lib/codemod/src/transforms/mdx-to-csf.ts +++ b/code/lib/codemod/src/transforms/mdx-to-csf.ts @@ -291,17 +291,10 @@ export async function transform(info: FileInfo, baseName: string): Promise<[stri const newMdx = mdxProcessor.stringify(root); let output = recast.print(file.path.node).code; - const prettierConfig = (await prettier.resolveConfig(`${info.path}.jsx`)) || { - printWidth: 100, - tabWidth: 2, - bracketSpacing: true, - trailingComma: 'es5', - singleQuote: true, - }; - + const path = `${info.path}.jsx`; output = await prettier.format(output.trim(), { - ...prettierConfig, - filepath: `${info.path}.jsx`, + ...(await prettier.resolveConfig(path)), + filepath: path, }); return [newMdx, output]; diff --git a/code/lib/codemod/src/transforms/migrate-to-test-package.ts b/code/lib/codemod/src/transforms/migrate-to-test-package.ts new file mode 100644 index 000000000000..02545aae06af --- /dev/null +++ b/code/lib/codemod/src/transforms/migrate-to-test-package.ts @@ -0,0 +1,63 @@ +/* eslint-disable no-underscore-dangle */ +import type { FileInfo } from 'jscodeshift'; +import { loadCsf, printCsf } from '@storybook/csf-tools'; +import type { BabelFile } from '@babel/core'; +import * as babel from '@babel/core'; +import * as t from '@babel/types'; +import prettier from 'prettier'; + +export default async function transform(info: FileInfo) { + const csf = loadCsf(info.source, { makeTitle: (title) => title }); + const fileNode = csf._ast; + // @ts-expect-error File is not yet exposed, see https://github.com/babel/babel/issues/11350#issuecomment-644118606 + const file: BabelFile = new babel.File( + { filename: info.path }, + { code: info.source, ast: fileNode } + ); + + file.path.traverse({ + ImportDeclaration: (path) => { + if ( + path.node.source.value === '@storybook/jest' || + path.node.source.value === '@storybook/testing-library' + ) { + if (path.node.source.value === '@storybook/jest') { + path.get('specifiers').forEach((specifier) => { + if (specifier.isImportSpecifier()) { + const imported = specifier.get('imported'); + if (!imported.isIdentifier()) return; + if (imported.node.name === 'jest') { + specifier.remove(); + path.insertAfter( + t.importDeclaration( + [t.importNamespaceSpecifier(t.identifier('test'))], + t.stringLiteral('@storybook/test') + ) + ); + } + } + }); + } + path.get('source').replaceWith(t.stringLiteral('@storybook/test')); + } + }, + Identifier: (path) => { + if (path.node.name === 'jest') { + path.replaceWith(t.identifier('test')); + } + }, + }); + + let output = printCsf(csf).code; + try { + output = await prettier.format(output, { + ...(await prettier.resolveConfig(info.path)), + filepath: info.path, + }); + } catch (e) { + console.warn(`Failed applying prettier to ${info.path}.`); + } + return output; +} + +export const parser = 'tsx'; diff --git a/code/lib/codemod/src/transforms/storiesof-to-csf.js b/code/lib/codemod/src/transforms/storiesof-to-csf.js index 83fc7b058a20..993e9ff8c35b 100644 --- a/code/lib/codemod/src/transforms/storiesof-to-csf.js +++ b/code/lib/codemod/src/transforms/storiesof-to-csf.js @@ -265,14 +265,7 @@ export default async function transformer(file, api, options) { let output = source; try { - const prettierConfig = (await prettier.resolveConfig(file.path)) || { - printWidth: 100, - tabWidth: 2, - bracketSpacing: true, - trailingComma: 'es5', - singleQuote: true, - }; - + const prettierConfig = await prettier.resolveConfig(file.path); output = prettier.format(source, { ...prettierConfig, parser: jscodeshiftToPrettierParser(options.parser), diff --git a/code/lib/codemod/src/transforms/upgrade-deprecated-types.ts b/code/lib/codemod/src/transforms/upgrade-deprecated-types.ts index fb4cce571064..5751d139ba97 100644 --- a/code/lib/codemod/src/transforms/upgrade-deprecated-types.ts +++ b/code/lib/codemod/src/transforms/upgrade-deprecated-types.ts @@ -36,15 +36,10 @@ export default async function transform(info: FileInfo, api: API, options: { par let output = printCsf(csf).code; try { - const prettierConfig = (await prettier.resolveConfig(info.path)) || { - printWidth: 100, - tabWidth: 2, - bracketSpacing: true, - trailingComma: 'es5', - singleQuote: true, - }; - - output = await prettier.format(output, { ...prettierConfig, filepath: info.path }); + output = await prettier.format(output, { + ...(await prettier.resolveConfig(info.path)), + filepath: info.path, + }); } catch (e) { logger.log(`Failed applying prettier to ${info.path}.`); } diff --git a/code/nx.json b/code/nx.json index ad6c9a817fe7..b072caaa73ab 100644 --- a/code/nx.json +++ b/code/nx.json @@ -1,11 +1,5 @@ { "$schema": "./node_modules/nx/schemas/nx-schema.json", - "implicitDependencies": { - "package.json": { - "dependencies": "*", - "devDependencies": "*" - } - }, "pluginsConfig": { "@nrwl/js": { "analyzeSourceFiles": false @@ -47,10 +41,21 @@ "dependencies": true } ], - "outputs": ["{projectRoot}/dist"], + "outputs": [ + "{projectRoot}/dist" + ], "cache": true } }, "nxCloudAccessToken": "NGVmYTkxMmItYzY3OS00MjkxLTk1ZDktZDFmYTFmNmVlNGY4fHJlYWQ=", - "parallel": 1 + "namedInputs": { + "default": [ + "{projectRoot}/**/*", + "sharedGlobals" + ], + "sharedGlobals": [], + "production": [ + "default" + ] + } }