From eb70971fa1c9b45156e9c7cff42b56b5883b3f9e Mon Sep 17 00:00:00 2001 From: Barys <55663639+bnn1@users.noreply.github.com> Date: Mon, 25 Dec 2023 09:59:23 +0400 Subject: [PATCH] add TS support for storybook preview tsx config extension (#9309) Co-authored-by: Daniel Choudhury --- __fixtures__/test-project/web/tsconfig.json | 1 + .../src/commands/setup/i18n/i18nHandler.js | 2 +- ...emplate => storybook.preview.tsx.template} | 12 +++--- .../commands/setup/ui/libraries/chakra-ui.js | 2 +- .../commands/setup/ui/libraries/mantine.js | 5 ++- ... => chakra.storybook.preview.tsx.template} | 8 +++- ...=> mantine.storybook.preview.tsx.template} | 6 ++- .../cli/src/lib/__tests__/mergeBasics.test.js | 30 ++++++++++++++ packages/cli/src/lib/configureStorybook.js | 41 ++++++++++++++----- packages/cli/src/lib/merge/index.js | 3 +- .../templates/storybook.preview.js.template | 16 -------- .../templates/storybook.preview.tsx.template | 18 ++++++++ .../templates/js/web/jsconfig.json | 1 + .../templates/ts/web/tsconfig.json | 1 + .../src/__tests__/paths.test.ts | 28 ++----------- packages/project-config/src/paths.ts | 10 ++--- packages/testing/config/storybook/main.js | 11 ++--- 17 files changed, 120 insertions(+), 75 deletions(-) rename packages/cli/src/commands/setup/i18n/templates/{storybook.preview.js.template => storybook.preview.tsx.template} (75%) rename packages/cli/src/commands/setup/ui/templates/{chakra.storybook.preview.js.template => chakra.storybook.preview.tsx.template} (58%) rename packages/cli/src/commands/setup/ui/templates/{mantine.storybook.preview.js.template => mantine.storybook.preview.tsx.template} (65%) delete mode 100644 packages/cli/src/lib/templates/storybook.preview.js.template create mode 100644 packages/cli/src/lib/templates/storybook.preview.tsx.template diff --git a/__fixtures__/test-project/web/tsconfig.json b/__fixtures__/test-project/web/tsconfig.json index 8b5649abe5a4..5dc323bff5f4 100644 --- a/__fixtures__/test-project/web/tsconfig.json +++ b/__fixtures__/test-project/web/tsconfig.json @@ -31,6 +31,7 @@ }, "include": [ "src", + "config", "../.redwood/types/includes/all-*", "../.redwood/types/includes/web-*", "../types", diff --git a/packages/cli/src/commands/setup/i18n/i18nHandler.js b/packages/cli/src/commands/setup/i18n/i18nHandler.js index 362f49749b71..0f87b015d16a 100644 --- a/packages/cli/src/commands/setup/i18n/i18nHandler.js +++ b/packages/cli/src/commands/setup/i18n/i18nHandler.js @@ -171,7 +171,7 @@ export const handler = async ({ force }) => { skip: () => fileIncludes(rwPaths.web.storybookConfig, 'withI18n'), task: async () => extendStorybookConfiguration( - path.join(__dirname, 'templates', 'storybook.preview.js.template') + path.join(__dirname, 'templates', 'storybook.preview.tsx.template') ), }, { diff --git a/packages/cli/src/commands/setup/i18n/templates/storybook.preview.js.template b/packages/cli/src/commands/setup/i18n/templates/storybook.preview.tsx.template similarity index 75% rename from packages/cli/src/commands/setup/i18n/templates/storybook.preview.js.template rename to packages/cli/src/commands/setup/i18n/templates/storybook.preview.tsx.template index 512bcf7b67b3..a481f82c015a 100644 --- a/packages/cli/src/commands/setup/i18n/templates/storybook.preview.js.template +++ b/packages/cli/src/commands/setup/i18n/templates/storybook.preview.tsx.template @@ -1,9 +1,11 @@ import * as React from 'react' import { I18nextProvider } from 'react-i18next' +import type { GlobalTypes } from '@storybook/csf' +import type { StoryFn, StoryContext } from '@storybook/react' import i18n from 'web/src/i18n' /** @type { import("@storybook/csf").GlobalTypes } */ -export const globalTypes = { +export const globalTypes: GlobalTypes = { locale: { name: 'Locale', description: 'Internationalization locale', @@ -23,12 +25,10 @@ export const globalTypes = { * https://github.com/storybookjs/addon-kit/blob/main/src/withGlobals.ts * Unfortunately that will make eslint complain, so we have to disable it when * using a hook below - * - * @param { import("@storybook/addons").StoryFn} StoryFn - * @param { import("@storybook/addons").StoryContext} context - * @returns a story wrapped in an I18nextProvider + * @param { import("@storybook/react").StoryFn} StoryFn + * @param { import("@storybook/react").StoryContext} context */ -const withI18n = (StoryFn, context) => { +const withI18n = (StoryFn: StoryFn, context: StoryContext) => { // eslint-disable-next-line react-hooks/rules-of-hooks React.useEffect(() => { i18n.changeLanguage(context.globals.locale) diff --git a/packages/cli/src/commands/setup/ui/libraries/chakra-ui.js b/packages/cli/src/commands/setup/ui/libraries/chakra-ui.js index e088883d4177..b34ea1a3fb57 100644 --- a/packages/cli/src/commands/setup/ui/libraries/chakra-ui.js +++ b/packages/cli/src/commands/setup/ui/libraries/chakra-ui.js @@ -108,7 +108,7 @@ export async function handler({ force, install }) { __dirname, '..', 'templates', - 'chakra.storybook.preview.js.template' + 'chakra.storybook.preview.tsx.template' ) ), }, diff --git a/packages/cli/src/commands/setup/ui/libraries/mantine.js b/packages/cli/src/commands/setup/ui/libraries/mantine.js index d63158285f0b..c77466d9bd66 100644 --- a/packages/cli/src/commands/setup/ui/libraries/mantine.js +++ b/packages/cli/src/commands/setup/ui/libraries/mantine.js @@ -158,14 +158,15 @@ export async function handler({ force, install, packages }) { }, { title: 'Configure Storybook...', - skip: () => fileIncludes(rwPaths.web.storybookConfig, 'withMantine'), + skip: () => + fileIncludes(rwPaths.web.storybookPreviewConfig, 'withMantine'), task: async () => extendStorybookConfiguration( path.join( __dirname, '..', 'templates', - 'mantine.storybook.preview.js.template' + 'mantine.storybook.preview.tsx.template' ) ), }, diff --git a/packages/cli/src/commands/setup/ui/templates/chakra.storybook.preview.js.template b/packages/cli/src/commands/setup/ui/templates/chakra.storybook.preview.tsx.template similarity index 58% rename from packages/cli/src/commands/setup/ui/templates/chakra.storybook.preview.js.template rename to packages/cli/src/commands/setup/ui/templates/chakra.storybook.preview.tsx.template index b6afe7aa1769..627ffd2f02fc 100644 --- a/packages/cli/src/commands/setup/ui/templates/chakra.storybook.preview.js.template +++ b/packages/cli/src/commands/setup/ui/templates/chakra.storybook.preview.tsx.template @@ -1,11 +1,15 @@ import * as React from 'react' import { ChakraProvider, extendTheme } from '@chakra-ui/react' -import * as theme from 'config/chakra.config' +import type { StoryFn } from '@storybook/react' +import theme from 'config/chakra.config' const extendedTheme = extendTheme(theme) -const withChakra = (StoryFn) => { +/** + * @param { import("@storybook/react").StoryFn} StoryFn + */ +const withChakra = (StoryFn: StoryFn) => { return ( diff --git a/packages/cli/src/commands/setup/ui/templates/mantine.storybook.preview.js.template b/packages/cli/src/commands/setup/ui/templates/mantine.storybook.preview.tsx.template similarity index 65% rename from packages/cli/src/commands/setup/ui/templates/mantine.storybook.preview.js.template rename to packages/cli/src/commands/setup/ui/templates/mantine.storybook.preview.tsx.template index 1e235f622ca0..08c50d47856e 100644 --- a/packages/cli/src/commands/setup/ui/templates/mantine.storybook.preview.js.template +++ b/packages/cli/src/commands/setup/ui/templates/mantine.storybook.preview.tsx.template @@ -1,11 +1,15 @@ import * as React from 'react' import { MantineProvider } from '@mantine/core' +import type { StoryFn } from '@storybook/react' import theme from 'config/mantine.config' import '@mantine/core/styles.css' -const withMantine = (StoryFn) => { +/** + * @param { import("@storybook/react").StoryFn} StoryFn + */ +const withMantine = (StoryFn: StoryFn) => { return ( diff --git a/packages/cli/src/lib/__tests__/mergeBasics.test.js b/packages/cli/src/lib/__tests__/mergeBasics.test.js index 28845dbc5aef..cac630c55e73 100644 --- a/packages/cli/src/lib/__tests__/mergeBasics.test.js +++ b/packages/cli/src/lib/__tests__/mergeBasics.test.js @@ -35,6 +35,36 @@ describe('the basics', () => { { ArrayExpression: concatUnique } ) }) + it('Merges JSX strings', () => { + const componentA = 'const ComponentA = (props) =>
Hello
' + const componentB = 'const ComponentB = (props) =>
Bye
' + expectTrivialConcat(componentA, componentB) + }) + it('Merges TSX strings', () => { + const componentA = + 'const ComponentA: MyComponent = (props) =>
Hello
' + const componentB = + 'const ComponentB: MyComponent = (props) =>
Bye
' + expectTrivialConcat(componentA, componentB) + }) + it('Merges TS strings', () => { + expectMerged( + `\ + const x: string = 'x' + const list: string[] = [x] + `, + `\ + const y: string = 'y' + const list: string[] = [y] + `, + `\ + const x: string = 'x' + const y: string = 'y' + const list: string[] = [x, y] + `, + { ArrayExpression: concatUnique } + ) + }) }) describe('Import behavior', () => { diff --git a/packages/cli/src/lib/configureStorybook.js b/packages/cli/src/lib/configureStorybook.js index cccb7ba18276..3f79e0abb238 100644 --- a/packages/cli/src/lib/configureStorybook.js +++ b/packages/cli/src/lib/configureStorybook.js @@ -1,5 +1,4 @@ import path from 'path' -import util from 'util' import fse from 'fs-extra' import prettier from 'prettier' @@ -11,24 +10,46 @@ import { keepBoth, keepBothStatementParents, } from './merge/strategy' +import { isTypeScriptProject } from './project' -import { getPaths } from '.' +import { getPaths, transformTSToJS, writeFile } from '.' +/** + * Extends the Storybook configuration file with the new configuration file + * @param {string} newConfigPath - The path to the new configuration file + */ export default async function extendStorybookConfiguration( newConfigPath = undefined ) { - const sbPreviewConfigPath = getPaths().web.storybookPreviewConfig + const webPaths = getPaths().web + const ts = isTypeScriptProject() + const sbPreviewConfigPath = + webPaths.storybookPreviewConfig ?? + `${webPaths.config}/storybook.preview.${ts ? 'tsx' : 'js'}` + const read = (path) => fse.readFileSync(path, { encoding: 'utf-8' }) + if (!fse.existsSync(sbPreviewConfigPath)) { - await util.promisify(fse.cp)( - path.join(__dirname, 'templates', 'storybook.preview.js.template'), - sbPreviewConfigPath + // If the Storybook preview config file doesn't exist, create it from the template + const templateContent = read( + path.resolve(__dirname, 'templates', 'storybook.preview.tsx.template') ) + const storybookPreviewContent = ts + ? templateContent + : transformTSToJS(sbPreviewConfigPath, templateContent) + + await writeFile(sbPreviewConfigPath, storybookPreviewContent) } + const storybookPreviewContent = read(sbPreviewConfigPath) + if (newConfigPath) { - const read = (path) => fse.readFileSync(path, { encoding: 'utf-8' }) - const write = (path, data) => fse.writeFileSync(path, data) - const merged = merge(read(sbPreviewConfigPath), read(newConfigPath), { + // If the new config file path is provided, merge it with the Storybook preview config file + const newConfigTemplate = read(newConfigPath) + const newConfigContent = ts + ? newConfigTemplate + : transformTSToJS(newConfigPath, newConfigTemplate) + + const merged = merge(storybookPreviewContent, newConfigContent, { ImportDeclaration: interleave, ArrayExpression: concatUnique, ObjectExpression: concatUnique, @@ -41,6 +62,6 @@ export default async function extendStorybookConfiguration( ...(await prettier.resolveConfig(sbPreviewConfigPath)), }) - write(sbPreviewConfigPath, formatted) + writeFile(sbPreviewConfigPath, formatted, { overwriteExisting: true }) } } diff --git a/packages/cli/src/lib/merge/index.js b/packages/cli/src/lib/merge/index.js index cee121f97d34..bc0637484540 100644 --- a/packages/cli/src/lib/merge/index.js +++ b/packages/cli/src/lib/merge/index.js @@ -212,7 +212,8 @@ function mergeAST(baseAST, extAST, strategy = {}) { export function merge(base, extension, strategy) { function parseReact(code) { return parse(code, { - presets: ['@babel/preset-react'], + filename: 'merged.tsx', // required to prevent babel error. The .tsx is relevant + presets: ['@babel/preset-typescript'], }) } diff --git a/packages/cli/src/lib/templates/storybook.preview.js.template b/packages/cli/src/lib/templates/storybook.preview.js.template deleted file mode 100644 index 8241d88fab17..000000000000 --- a/packages/cli/src/lib/templates/storybook.preview.js.template +++ /dev/null @@ -1,16 +0,0 @@ -import * as React from 'react' - -/** @type { import("@storybook/csf").GlobalTypes } */ -export const globalTypes = {} - -/** - * An example, no-op storybook decorator. Use a function like this to create decorators. - * @param { import("@storybook/addons").StoryFn} StoryFn - * @param { import("@storybook/addons").StoryContext} context - * @returns StoryFn, unmodified. - */ -const _exampleDecorator = (StoryFn, _context) => { - return -} - -export const decorators = [] diff --git a/packages/cli/src/lib/templates/storybook.preview.tsx.template b/packages/cli/src/lib/templates/storybook.preview.tsx.template new file mode 100644 index 000000000000..042497ff6196 --- /dev/null +++ b/packages/cli/src/lib/templates/storybook.preview.tsx.template @@ -0,0 +1,18 @@ +import * as React from 'react' + +import type { GlobalTypes } from '@storybook/csf' +import type { StoryFn, StoryContext } from '@storybook/react' + +/** @type { import("@storybook/csf").GlobalTypes } */ +export const globalTypes: GlobalTypes = {} + +/** + * An example, no-op storybook decorator. Use a function like this to create decorators. + * @param { import("@storybook/react").StoryFn} StoryFn + * @param { import("@storybook/react").StoryContext} context +*/ +const _exampleDecorator = (StoryFn: StoryFn, _context: StoryContext) => { + return +} + +export const decorators = [] diff --git a/packages/create-redwood-app/templates/js/web/jsconfig.json b/packages/create-redwood-app/templates/js/web/jsconfig.json index 0ee3041e4b2f..3f5be910a4f3 100644 --- a/packages/create-redwood-app/templates/js/web/jsconfig.json +++ b/packages/create-redwood-app/templates/js/web/jsconfig.json @@ -43,6 +43,7 @@ }, "include": [ "src", + "config", "../.redwood/types/includes/all-*", "../.redwood/types/includes/web-*", "../types", diff --git a/packages/create-redwood-app/templates/ts/web/tsconfig.json b/packages/create-redwood-app/templates/ts/web/tsconfig.json index 8b5649abe5a4..5dc323bff5f4 100644 --- a/packages/create-redwood-app/templates/ts/web/tsconfig.json +++ b/packages/create-redwood-app/templates/ts/web/tsconfig.json @@ -31,6 +31,7 @@ }, "include": [ "src", + "config", "../.redwood/types/includes/all-*", "../.redwood/types/includes/web-*", "../types", diff --git a/packages/project-config/src/__tests__/paths.test.ts b/packages/project-config/src/__tests__/paths.test.ts index 7d9f4a471ec6..a4367a18ddfa 100644 --- a/packages/project-config/src/__tests__/paths.test.ts +++ b/packages/project-config/src/__tests__/paths.test.ts @@ -123,12 +123,7 @@ describe('paths', () => { 'config', 'storybook.config.js' ), - storybookPreviewConfig: path.join( - FIXTURE_BASEDIR, - 'web', - 'config', - 'storybook.preview.js' - ), + storybookPreviewConfig: null, storybookManagerConfig: path.join( FIXTURE_BASEDIR, 'web', @@ -363,12 +358,7 @@ describe('paths', () => { 'config', 'storybook.config.js' ), - storybookPreviewConfig: path.join( - FIXTURE_BASEDIR, - 'web', - 'config', - 'storybook.preview.js' - ), + storybookPreviewConfig: null, storybookManagerConfig: path.join( FIXTURE_BASEDIR, 'web', @@ -649,12 +639,7 @@ describe('paths', () => { 'config', 'storybook.config.js' ), - storybookPreviewConfig: path.join( - FIXTURE_BASEDIR, - 'web', - 'config', - 'storybook.preview.js' - ), + storybookPreviewConfig: null, storybookManagerConfig: path.join( FIXTURE_BASEDIR, 'web', @@ -892,12 +877,7 @@ describe('paths', () => { 'config', 'storybook.config.js' ), - storybookPreviewConfig: path.join( - FIXTURE_BASEDIR, - 'web', - 'config', - 'storybook.preview.js' - ), + storybookPreviewConfig: null, storybookManagerConfig: path.join( FIXTURE_BASEDIR, 'web', diff --git a/packages/project-config/src/paths.ts b/packages/project-config/src/paths.ts index 7d607b0e94cb..9ec1246aa606 100644 --- a/packages/project-config/src/paths.ts +++ b/packages/project-config/src/paths.ts @@ -42,7 +42,7 @@ export interface WebPaths { entryClient: string | null postcss: string storybookConfig: string - storybookPreviewConfig: string + storybookPreviewConfig: string | null storybookManagerConfig: string dist: string types: string @@ -105,9 +105,8 @@ const PATH_WEB_DIR_ENTRY_CLIENT = 'web/src/entry.client' // .jsx,.tsx const PATH_WEB_DIR_CONFIG_POSTCSS = 'web/config/postcss.config.js' const PATH_WEB_DIR_CONFIG_STORYBOOK_CONFIG = 'web/config/storybook.config.js' -const PATH_WEB_DIR_CONFIG_STORYBOOK_PREVIEW = 'web/config/storybook.preview.js' +const PATH_WEB_DIR_CONFIG_STORYBOOK_PREVIEW = 'web/config/storybook.preview' // .js, .tsx const PATH_WEB_DIR_CONFIG_STORYBOOK_MANAGER = 'web/config/storybook.manager.js' - const PATH_WEB_DIR_DIST = 'web/dist' /** @@ -205,9 +204,8 @@ export const getPaths = (BASE_DIR: string = getBaseDir()): Paths => { BASE_DIR, PATH_WEB_DIR_CONFIG_STORYBOOK_CONFIG ), - storybookPreviewConfig: path.join( - BASE_DIR, - PATH_WEB_DIR_CONFIG_STORYBOOK_PREVIEW + storybookPreviewConfig: resolveFile( + path.join(BASE_DIR, PATH_WEB_DIR_CONFIG_STORYBOOK_PREVIEW) ), storybookManagerConfig: path.join( BASE_DIR, diff --git a/packages/testing/config/storybook/main.js b/packages/testing/config/storybook/main.js index 2a098541103b..9319ea4d5b9f 100644 --- a/packages/testing/config/storybook/main.js +++ b/packages/testing/config/storybook/main.js @@ -74,11 +74,12 @@ const baseConfig = { } } - const userPreviewPath = fs.existsSync( - redwoodProjectPaths.web.storybookPreviewConfig - ) - ? redwoodProjectPaths.web.storybookPreviewConfig - : './preview.example.js' + let userPreviewPath = './preview.example.js' + + if (redwoodProjectPaths.storybookPreviewConfig) { + userPreviewPath = redwoodProjectPaths.storybookPreviewConfig + } + sbConfig.resolve.alias['~__REDWOOD__USER_STORYBOOK_PREVIEW_CONFIG'] = userPreviewPath