From 5461f739e21869d595f4210619d60e65e1c5b108 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Fri, 27 Sep 2024 15:45:25 +0200 Subject: [PATCH 01/28] support statsJson in angular schemas --- .../src/builders/build-storybook/schema.json | 35 +++++++++++++++---- .../src/builders/start-storybook/schema.json | 35 +++++++++++++++---- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/code/frameworks/angular/src/builders/build-storybook/schema.json b/code/frameworks/angular/src/builders/build-storybook/schema.json index dbc2a734417e..afd42aa4de07 100644 --- a/code/frameworks/angular/src/builders/build-storybook/schema.json +++ b/code/frameworks/angular/src/builders/build-storybook/schema.json @@ -67,16 +67,30 @@ "compodocArgs": { "type": "array", "description": "Compodoc options : https://compodoc.app/guides/options.html. Options `-p` with tsconfig path and `-d` with workspace root is always given.", - "default": ["-e", "json"], + "default": [ + "-e", + "json" + ], "items": { "type": "string" } }, "webpackStatsJson": { - "type": ["boolean", "string"], + "type": [ + "boolean", + "string" + ], "description": "Write Webpack Stats JSON to disk", "default": false }, + "statsJson": { + "type": [ + "boolean", + "string" + ], + "description": "Write stats JSON to disk", + "default": false + }, "previewUrl": { "type": "string", "description": "Disables the default storybook preview and lets you use your own" @@ -113,7 +127,10 @@ } }, "sourceMap": { - "type": ["boolean", "object"], + "type": [ + "boolean", + "object" + ], "description": "Configure sourcemaps. See: https://angular.io/guide/workspace-config#source-map-configuration", "default": false } @@ -151,7 +168,11 @@ } }, "additionalProperties": false, - "required": ["glob", "input", "output"] + "required": [ + "glob", + "input", + "output" + ] }, { "type": "string" @@ -179,7 +200,9 @@ } }, "additionalProperties": false, - "required": ["input"] + "required": [ + "input" + ] }, { "type": "string", @@ -188,4 +211,4 @@ ] } } -} +} \ No newline at end of file diff --git a/code/frameworks/angular/src/builders/start-storybook/schema.json b/code/frameworks/angular/src/builders/start-storybook/schema.json index a2eef03e4b08..a96182ea1b1e 100644 --- a/code/frameworks/angular/src/builders/start-storybook/schema.json +++ b/code/frameworks/angular/src/builders/start-storybook/schema.json @@ -94,7 +94,10 @@ "compodocArgs": { "type": "array", "description": "Compodoc options : https://compodoc.app/guides/options.html. Options `-p` with tsconfig path and `-d` with workspace root is always given.", - "default": ["-e", "json"], + "default": [ + "-e", + "json" + ], "items": { "type": "string" } @@ -135,10 +138,21 @@ "description": "URL path to be appended when visiting Storybook for the first time" }, "webpackStatsJson": { - "type": ["boolean", "string"], + "type": [ + "boolean", + "string" + ], "description": "Write Webpack Stats JSON to disk", "default": false }, + "statsJson": { + "type": [ + "boolean", + "string" + ], + "description": "Write stats JSON to disk", + "default": false + }, "previewUrl": { "type": "string", "description": "Disables the default storybook preview and lets you use your own" @@ -149,7 +163,10 @@ "pattern": "(silly|verbose|info|warn|silent)" }, "sourceMap": { - "type": ["boolean", "object"], + "type": [ + "boolean", + "object" + ], "description": "Configure sourcemaps. See: https://angular.io/guide/workspace-config#source-map-configuration", "default": false } @@ -187,7 +204,11 @@ } }, "additionalProperties": false, - "required": ["glob", "input", "output"] + "required": [ + "glob", + "input", + "output" + ] }, { "type": "string" @@ -215,7 +236,9 @@ } }, "additionalProperties": false, - "required": ["input"] + "required": [ + "input" + ] }, { "type": "string", @@ -224,4 +247,4 @@ ] } } -} +} \ No newline at end of file From 2a02e134805f9659481f90237478a39516322c44 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Tue, 19 Nov 2024 18:14:41 +1100 Subject: [PATCH 02/28] Fix webpack fsCache not working with @storybook/nextjs Next.js overrides certain internal webpack packages (particularly `webpack-sources`), which are involved with filesystem caching, with its own versions from next/dist/compiled. For filesystem caching to work, Next.js must be allowed to perform these overrides before webpack is first initialized by @storybook/builder-webpack5. If it is not, the objects to be serialized to disk in the caching process will be instantiated using the original (non-Next.js) modules, but the serializers will be created using the Next.js modules. This mismatch between the objects to be cached and the serializers that write the filesystem cache prevents the cache from being written; instead, webpack outputs a warning message to the console for every object that it tries and fails to find a matching serializer for. This fix works by invoking Next.js to configure webpack in the `core` hook of @storybook/nextjs/preset, immediately before loading @storybook/builder-webpack5. We don't actually use this configuration that Next.js creates; the actual configuration that will be used in the build is still generated in `webpackFinal` as before. `fsCache` has a large impact on Storybook build performance. Even in a minimal project with a single story, enabling it reduces build time by 66%! This is therefore a very valuable option to be able to enable. Fixes #29621 --- code/frameworks/nextjs/src/preset.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/code/frameworks/nextjs/src/preset.ts b/code/frameworks/nextjs/src/preset.ts index 3463910175e1..84255fc62e20 100644 --- a/code/frameworks/nextjs/src/preset.ts +++ b/code/frameworks/nextjs/src/preset.ts @@ -36,6 +36,19 @@ export const addons: PresetProperty<'addons'> = [ export const core: PresetProperty<'core'> = async (config, options) => { const framework = await options.presets.apply('framework'); + // Load the Next.js configuration before we need it in webpackFinal (below). + // This gives Next.js an opportunity to override some of webpack's internals + // (see next/dist/server/config-utils.js) before @storybook/builder-webpack5 + // starts to use it. Without this, webpack's file system cache (fsCache: true) + // does not work. + const { nextConfigPath } = await options.presets.apply('frameworkOptions'); + await configureConfig({ + // Pass in a dummy webpack config object for now, since we don't want to + // modify the real one yet. We pass in the real one in webpackFinal. + baseConfig: {}, + nextConfigPath, + }); + return { ...config, builder: { From 0a3e05bceafe0ad4aae8a2d3e19984c2d337b420 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Wed, 27 Nov 2024 11:33:40 +1100 Subject: [PATCH 03/28] Fix crash when framework is not specified with options --- code/frameworks/nextjs/src/preset.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/code/frameworks/nextjs/src/preset.ts b/code/frameworks/nextjs/src/preset.ts index 84255fc62e20..f10550acc900 100644 --- a/code/frameworks/nextjs/src/preset.ts +++ b/code/frameworks/nextjs/src/preset.ts @@ -34,19 +34,18 @@ export const addons: PresetProperty<'addons'> = [ ]; export const core: PresetProperty<'core'> = async (config, options) => { - const framework = await options.presets.apply('framework'); + const framework = await options.presets.apply('framework'); // Load the Next.js configuration before we need it in webpackFinal (below). // This gives Next.js an opportunity to override some of webpack's internals // (see next/dist/server/config-utils.js) before @storybook/builder-webpack5 // starts to use it. Without this, webpack's file system cache (fsCache: true) // does not work. - const { nextConfigPath } = await options.presets.apply('frameworkOptions'); await configureConfig({ // Pass in a dummy webpack config object for now, since we don't want to // modify the real one yet. We pass in the real one in webpackFinal. baseConfig: {}, - nextConfigPath, + nextConfigPath: typeof framework === 'string' ? undefined : framework.options.nextConfigPath, }); return { From 83a1f24f02c50a0b5ed4a03c6b248100183a3b01 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Tue, 3 Dec 2024 12:27:09 +1100 Subject: [PATCH 04/28] Defer all imports from webpack until after Next.js has loaded its internal instance --- code/frameworks/nextjs/src/config/webpack.ts | 14 +++++--- code/frameworks/nextjs/src/preset.ts | 35 +++++++++++--------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/code/frameworks/nextjs/src/config/webpack.ts b/code/frameworks/nextjs/src/config/webpack.ts index 76edac25c81c..030da6b2ab77 100644 --- a/code/frameworks/nextjs/src/config/webpack.ts +++ b/code/frameworks/nextjs/src/config/webpack.ts @@ -1,8 +1,7 @@ import type { NextConfig } from 'next'; import type { Configuration as WebpackConfig } from 'webpack'; -import { DefinePlugin } from 'webpack'; -import { addScopedAlias, resolveNextConfig, setAlias } from '../utils'; +import { addScopedAlias, resolveNextConfig } from '../utils'; const tryResolve = (path: string) => { try { @@ -48,12 +47,15 @@ export const configureConfig = async ({ addScopedAlias(baseConfig, 'react-dom/server', 'next/dist/compiled/react-dom/server'); } - setupRuntimeConfig(baseConfig, nextConfig); + await setupRuntimeConfig(baseConfig, nextConfig); return nextConfig; }; -const setupRuntimeConfig = (baseConfig: WebpackConfig, nextConfig: NextConfig): void => { +const setupRuntimeConfig = async ( + baseConfig: WebpackConfig, + nextConfig: NextConfig +): Promise => { const definePluginConfig: Record = { // this mimics what nextjs does client side // https://github.com/vercel/next.js/blob/57702cb2a9a9dba4b552e0007c16449cf36cfb44/packages/next/client/index.tsx#L101 @@ -67,5 +69,7 @@ const setupRuntimeConfig = (baseConfig: WebpackConfig, nextConfig: NextConfig): definePluginConfig['process.env.__NEXT_NEW_LINK_BEHAVIOR'] = newNextLinkBehavior; - baseConfig.plugins?.push(new DefinePlugin(definePluginConfig)); + // Load DefinePlugin with a dynamic import to ensure that Next.js can first + // replace webpack with its own internal instance, and we get that here. + baseConfig.plugins?.push(new (await import('webpack')).DefinePlugin(definePluginConfig)); }; diff --git a/code/frameworks/nextjs/src/preset.ts b/code/frameworks/nextjs/src/preset.ts index f10550acc900..473e24655848 100644 --- a/code/frameworks/nextjs/src/preset.ts +++ b/code/frameworks/nextjs/src/preset.ts @@ -10,24 +10,10 @@ import type { ConfigItem, PluginItem, TransformOptions } from '@babel/core'; import { loadPartialConfig } from '@babel/core'; import semver from 'semver'; -import { configureAliases } from './aliases/webpack'; -import { configureBabelLoader } from './babel/loader'; import nextBabelPreset from './babel/preset'; -import { configureCompatibilityAliases } from './compatibility/compatibility-map'; import { configureConfig } from './config/webpack'; -import { configureCss } from './css/webpack'; -import { configureNextExportMocks } from './export-mocks/webpack'; -import { configureFastRefresh } from './fastRefresh/webpack'; import TransformFontImports from './font/babel'; -import { configureNextFont } from './font/webpack/configureNextFont'; -import { configureImages } from './images/webpack'; -import { configureImports } from './imports/webpack'; -import { configureNodePolyfills } from './nodePolyfills/webpack'; -import { configureRSC } from './rsc/webpack'; -import { configureStyledJsx } from './styledJsx/webpack'; -import { configureSWCLoader } from './swc/loader'; import type { FrameworkOptions, StorybookConfig } from './types'; -import { configureRuntimeNextjsVersionResolution, getNextjsVersion } from './utils'; export const addons: PresetProperty<'addons'> = [ dirname(require.resolve(join('@storybook/preset-react-webpack', 'package.json'))), @@ -156,6 +142,22 @@ export const webpackFinal: StorybookConfig['webpackFinal'] = async (baseConfig, nextConfigPath, }); + // Use dynamic imports to ensure these modules that use webpack load after + // Next.js has been configured (above), and has replaced webpack with its precompiled + // version. + const { configureNextFont } = await import('./font/webpack/configureNextFont'); + const { configureRuntimeNextjsVersionResolution, getNextjsVersion } = await import('./utils'); + const { configureImports } = await import('./imports/webpack'); + const { configureCss } = await import('./css/webpack'); + const { configureImages } = await import('./images/webpack'); + const { configureStyledJsx } = await import('./styledJsx/webpack'); + const { configureNodePolyfills } = await import('./nodePolyfills/webpack'); + const { configureAliases } = await import('./aliases/webpack'); + const { configureFastRefresh } = await import('./fastRefresh/webpack'); + const { configureRSC } = await import('./rsc/webpack'); + const { configureSWCLoader } = await import('./swc/loader'); + const { configureBabelLoader } = await import('./babel/loader'); + const babelRCPath = join(getProjectRoot(), '.babelrc'); const babelConfigPath = join(getProjectRoot(), 'babel.config.js'); const hasBabelConfig = existsSync(babelRCPath) || existsSync(babelConfigPath); @@ -168,7 +170,10 @@ export const webpackFinal: StorybookConfig['webpackFinal'] = async (baseConfig, configureNextFont(baseConfig, useSWC); configureRuntimeNextjsVersionResolution(baseConfig); - configureImports({ baseConfig, configDir: options.configDir }); + configureImports({ + baseConfig, + configDir: options.configDir, + }); configureCss(baseConfig, nextConfig); configureImages(baseConfig, nextConfig); configureStyledJsx(baseConfig); From c00c054199d73cd88e6f53b713c3b944422c72c9 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Wed, 4 Dec 2024 10:42:43 +1100 Subject: [PATCH 05/28] Undo unintentional formatting change --- code/frameworks/nextjs/src/preset.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/code/frameworks/nextjs/src/preset.ts b/code/frameworks/nextjs/src/preset.ts index 473e24655848..6b90ae543a22 100644 --- a/code/frameworks/nextjs/src/preset.ts +++ b/code/frameworks/nextjs/src/preset.ts @@ -170,10 +170,7 @@ export const webpackFinal: StorybookConfig['webpackFinal'] = async (baseConfig, configureNextFont(baseConfig, useSWC); configureRuntimeNextjsVersionResolution(baseConfig); - configureImports({ - baseConfig, - configDir: options.configDir, - }); + configureImports({ baseConfig, configDir: options.configDir }); configureCss(baseConfig, nextConfig); configureImages(baseConfig, nextConfig); configureStyledJsx(baseConfig); From a91b0ba3a6c2f885d8f965e23cbd8c7bfdb42f11 Mon Sep 17 00:00:00 2001 From: Lars Rickert Date: Tue, 1 Oct 2024 11:47:43 +0200 Subject: [PATCH 06/28] feat: add code snippet panel to single story view mode --- code/addons/docs/package.json | 13 ++++++- code/addons/docs/src/manager.tsx | 38 +++++++++++++++++++ code/addons/essentials/package.json | 5 +++ code/addons/essentials/src/docs/manager.ts | 2 + code/addons/essentials/src/index.ts | 2 +- .../vue3/src/docs/sourceDecorator.ts | 6 +-- 6 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 code/addons/docs/src/manager.tsx create mode 100644 code/addons/essentials/src/docs/manager.ts diff --git a/code/addons/docs/package.json b/code/addons/docs/package.json index f5834a822026..fbfa158c43d1 100644 --- a/code/addons/docs/package.json +++ b/code/addons/docs/package.json @@ -71,7 +71,12 @@ "./angular": "./angular/index.js", "./angular/index.js": "./angular/index.js", "./web-components/index.js": "./web-components/index.js", - "./package.json": "./package.json" + "./package.json": "./package.json", + "./manager": { + "types": "./dist/manager.d.ts", + "import": "./dist/manager.mjs", + "require": "./dist/manager.js" + } }, "main": "dist/index.js", "module": "dist/index.mjs", @@ -129,7 +134,11 @@ "./src/preview.ts", "./src/blocks.ts", "./src/shims/mdx-react-shim.ts", - "./src/mdx-loader.ts" + "./src/mdx-loader.ts", + "./src/manager.tsx" + ], + "managerEntries": [ + "./src/manager.tsx" ] }, "gitHead": "e6a7fd8a655c69780bc20b9749c2699e44beae16", diff --git a/code/addons/docs/src/manager.tsx b/code/addons/docs/src/manager.tsx new file mode 100644 index 000000000000..9b8cdafb7799 --- /dev/null +++ b/code/addons/docs/src/manager.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +import { AddonPanel, type SyntaxHighlighterFormatTypes } from 'storybook/internal/components'; +import { ADDON_ID, PANEL_ID, PARAM_KEY, SNIPPET_RENDERED } from 'storybook/internal/docs-tools'; +import { addons, types, useAddonState, useChannel } from 'storybook/internal/manager-api'; + +import { Source } from '@storybook/blocks'; + +addons.register(ADDON_ID, (api) => { + addons.add(PANEL_ID, { + title: 'Code', + type: types.PANEL, + paramKey: PARAM_KEY, + match: ({ viewMode }) => viewMode === 'story', + render: ({ active }) => { + const [codeSnippet, setSourceCode] = useAddonState<{ + source: string; + format: SyntaxHighlighterFormatTypes; + }>(ADDON_ID, { + source: '', + format: 'html', + }); + + useChannel({ + [SNIPPET_RENDERED]: ({ source, format }) => { + setSourceCode({ source, format: format ?? 'html' }); + console.log('SOURCE CODE CHANGED', codeSnippet.source); + }, + }); + + return ( + + + + ); + }, + }); +}); diff --git a/code/addons/essentials/package.json b/code/addons/essentials/package.json index 4f7ad700ec0d..73e1cbcb6f9f 100644 --- a/code/addons/essentials/package.json +++ b/code/addons/essentials/package.json @@ -40,6 +40,7 @@ }, "./backgrounds/manager": "./dist/backgrounds/manager.js", "./controls/manager": "./dist/controls/manager.js", + "./docs/manager": "./dist/docs/manager.js", "./docs/preview": { "types": "./dist/docs/preview.d.ts", "import": "./dist/docs/preview.mjs", @@ -114,10 +115,14 @@ "./src/docs/preset.ts", "./src/docs/mdx-react-shim.ts" ], + "entries": [ + "./src/docs/manager.ts" + ], "managerEntries": [ "./src/actions/manager.ts", "./src/backgrounds/manager.ts", "./src/controls/manager.ts", + "./src/docs/manager.ts", "./src/measure/manager.ts", "./src/outline/manager.ts", "./src/toolbars/manager.ts", diff --git a/code/addons/essentials/src/docs/manager.ts b/code/addons/essentials/src/docs/manager.ts new file mode 100644 index 000000000000..6101f7d79261 --- /dev/null +++ b/code/addons/essentials/src/docs/manager.ts @@ -0,0 +1,2 @@ +// @ts-expect-error (no types needed for this) +export * from '@storybook/addon-docs/manager'; diff --git a/code/addons/essentials/src/index.ts b/code/addons/essentials/src/index.ts index 5809420bc1b8..a72554227ba2 100644 --- a/code/addons/essentials/src/index.ts +++ b/code/addons/essentials/src/index.ts @@ -88,9 +88,9 @@ export function addons(options: PresetOptions) { // NOTE: The order of these addons is important. return [ - 'docs', 'controls', 'actions', + 'docs', 'backgrounds', 'viewport', 'toolbars', diff --git a/code/renderers/vue3/src/docs/sourceDecorator.ts b/code/renderers/vue3/src/docs/sourceDecorator.ts index 7eb8734305af..cc2f922f63f1 100644 --- a/code/renderers/vue3/src/docs/sourceDecorator.ts +++ b/code/renderers/vue3/src/docs/sourceDecorator.ts @@ -107,7 +107,6 @@ ${template}`; * Checks if the source code generation should be skipped for the given Story context. Will be true * if one of the following is true: * - * - View mode is not "docs" * - Story is no arg story * - Story has set custom source code via parameters.docs.source.code * - Story has set source type to "code" via parameters.docs.source.type @@ -120,13 +119,10 @@ export const shouldSkipSourceCodeGeneration = (context: StoryContext): boolean = } const isArgsStory = context?.parameters.__isArgsStory; - const isDocsViewMode = context?.viewMode === 'docs'; // never render if the user is forcing the block to render code, or // if the user provides code, or if it's not an args story. - return ( - !isDocsViewMode || !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE - ); + return !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE; }; /** From 107c40bf71a437b72f7c1d1755e10da0b7926a04 Mon Sep 17 00:00:00 2001 From: Lars Rickert Date: Tue, 1 Oct 2024 12:03:33 +0200 Subject: [PATCH 07/28] remove console log --- code/addons/docs/src/manager.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/code/addons/docs/src/manager.tsx b/code/addons/docs/src/manager.tsx index 9b8cdafb7799..ed3823ed4492 100644 --- a/code/addons/docs/src/manager.tsx +++ b/code/addons/docs/src/manager.tsx @@ -24,7 +24,6 @@ addons.register(ADDON_ID, (api) => { useChannel({ [SNIPPET_RENDERED]: ({ source, format }) => { setSourceCode({ source, format: format ?? 'html' }); - console.log('SOURCE CODE CHANGED', codeSnippet.source); }, }); From a283cea33680e379625bbc57dbd8b52c46c9ddcf Mon Sep 17 00:00:00 2001 From: Lars Rickert Date: Mon, 25 Nov 2024 10:04:25 +0100 Subject: [PATCH 08/28] use dark theme --- code/addons/docs/src/manager.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/addons/docs/src/manager.tsx b/code/addons/docs/src/manager.tsx index ed3823ed4492..212956b99ebb 100644 --- a/code/addons/docs/src/manager.tsx +++ b/code/addons/docs/src/manager.tsx @@ -29,7 +29,7 @@ addons.register(ADDON_ID, (api) => { return ( - + ); }, From 1fa0e7da9125544912704155fcea4d8e1b115b0f Mon Sep 17 00:00:00 2001 From: Lars Rickert Date: Mon, 25 Nov 2024 11:15:44 +0100 Subject: [PATCH 09/28] feat: support `docs.source.addonPanel` parameter --- code/addons/docs/src/manager.tsx | 37 ++++++++++++++++++++++++++++- code/addons/essentials/src/index.ts | 2 +- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/code/addons/docs/src/manager.tsx b/code/addons/docs/src/manager.tsx index 212956b99ebb..09c371be238b 100644 --- a/code/addons/docs/src/manager.tsx +++ b/code/addons/docs/src/manager.tsx @@ -1,12 +1,27 @@ import React from 'react'; import { AddonPanel, type SyntaxHighlighterFormatTypes } from 'storybook/internal/components'; +import { FORCE_RE_RENDER, PRELOAD_ENTRIES } from 'storybook/internal/core-events'; import { ADDON_ID, PANEL_ID, PARAM_KEY, SNIPPET_RENDERED } from 'storybook/internal/docs-tools'; import { addons, types, useAddonState, useChannel } from 'storybook/internal/manager-api'; import { Source } from '@storybook/blocks'; -addons.register(ADDON_ID, (api) => { +addons.register(ADDON_ID, async (api) => { + // at this point, the parameters are not yet defined so we can not check whether the addon panel should + // be added or not. The "PRELOAD_ENTRIES" event seems to be the earliest point in time where the parameters + // are available + const isDisabled = await new Promise((resolve) => { + api.once(PRELOAD_ENTRIES, () => { + const parameter = api.getCurrentParameter(PARAM_KEY); + resolve(shouldDisableAddonPanel(parameter)); + }); + }); + + if (isDisabled) { + return; + } + addons.add(PANEL_ID, { title: 'Code', type: types.PANEL, @@ -34,4 +49,24 @@ addons.register(ADDON_ID, (api) => { ); }, }); + + api.emit(FORCE_RE_RENDER); }); + +const isObject = (value: unknown): value is object => { + return value != null && typeof value === 'object'; +}; + +/** + * Checks whether the addon panel should be disabled by checking the parameter.source.addonPanel + * property. + */ +const shouldDisableAddonPanel = (parameter: unknown) => { + return ( + isObject(parameter) && + 'source' in parameter && + isObject(parameter.source) && + 'addonPanel' in parameter.source && + parameter.source.addonPanel === false + ); +}; diff --git a/code/addons/essentials/src/index.ts b/code/addons/essentials/src/index.ts index a72554227ba2..5809420bc1b8 100644 --- a/code/addons/essentials/src/index.ts +++ b/code/addons/essentials/src/index.ts @@ -88,9 +88,9 @@ export function addons(options: PresetOptions) { // NOTE: The order of these addons is important. return [ + 'docs', 'controls', 'actions', - 'docs', 'backgrounds', 'viewport', 'toolbars', From 01e11e5bb9f0e8fa8626d1c8a98230210af93670 Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Tue, 3 Dec 2024 22:25:15 +0800 Subject: [PATCH 10/28] Addon-docs: Fix and document source panel disabling --- MIGRATION.md | 20 ++++++++- code/addons/docs/src/manager.tsx | 42 ++----------------- .../stories/sourcePanel/index.stories.tsx | 15 +++++++ 3 files changed, 38 insertions(+), 39 deletions(-) create mode 100644 code/addons/docs/template/stories/sourcePanel/index.stories.tsx diff --git a/MIGRATION.md b/MIGRATION.md index 2e2e735fb21f..07aecbbe3973 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,5 +1,7 @@

Migration

+- [From version 8.4.x to 8.5.x](#from-version-84x-to-85x) + - [Added source code pnael to docs](#added-source-code-pnael-to-docs) - [From version 8.2.x to 8.3.x](#from-version-82x-to-83x) - [Removed `experimental_SIDEBAR_BOTTOM` and deprecated `experimental_SIDEBAR_TOP` addon types](#removed-experimental_sidebar_bottom-and-deprecated-experimental_sidebar_top-addon-types) - [New parameters format for addon backgrounds](#new-parameters-format-for-addon-backgrounds) @@ -167,7 +169,7 @@ - [Angular: Drop support for calling Storybook directly](#angular-drop-support-for-calling-storybook-directly) - [Angular: Application providers and ModuleWithProviders](#angular-application-providers-and-modulewithproviders) - [Angular: Removed legacy renderer](#angular-removed-legacy-renderer) - - [Angular: initializer functions](#angular-initializer-functions) + - [Angular: Initializer functions](#angular-initializer-functions) - [Next.js: use the `@storybook/nextjs` framework](#nextjs-use-the-storybooknextjs-framework) - [SvelteKit: needs the `@storybook/sveltekit` framework](#sveltekit-needs-the-storybooksveltekit-framework) - [Vue3: replaced app export with setup](#vue3-replaced-app-export-with-setup) @@ -419,6 +421,22 @@ - [Packages renaming](#packages-renaming) - [Deprecated embedded addons](#deprecated-embedded-addons) +## From version 8.4.x to 8.5.x + +### Added source code pnael to docs + +Starting in 8.5, Storybook Docs (`@storybook/addon-docs`) automatically adds a new addon panel to stories that displays a source snippet beneath each story. This works similarly to the existing [source snippet doc block](https://storybook.js.org/docs/writing-docs/doc-blocks#source), but in the story view. It is intended to replace the [Storysource addon](https://storybook.js.org/addons/@storybook/addon-storysource). + +If you wish to disable this panel globally, add the following line to your `.storybook/preview.js` project configuration. You can also selectively disable/enable at the story level. + +```js +export default { + parameters: { + docsSourcePanel: { disable: true }, + }, +}; +``` + ## From version 8.2.x to 8.3.x ### Removed `experimental_SIDEBAR_BOTTOM` and deprecated `experimental_SIDEBAR_TOP` addon types diff --git a/code/addons/docs/src/manager.tsx b/code/addons/docs/src/manager.tsx index 09c371be238b..fdbe0375d369 100644 --- a/code/addons/docs/src/manager.tsx +++ b/code/addons/docs/src/manager.tsx @@ -1,31 +1,17 @@ import React from 'react'; import { AddonPanel, type SyntaxHighlighterFormatTypes } from 'storybook/internal/components'; -import { FORCE_RE_RENDER, PRELOAD_ENTRIES } from 'storybook/internal/core-events'; -import { ADDON_ID, PANEL_ID, PARAM_KEY, SNIPPET_RENDERED } from 'storybook/internal/docs-tools'; +import { ADDON_ID, PANEL_ID, SNIPPET_RENDERED } from 'storybook/internal/docs-tools'; import { addons, types, useAddonState, useChannel } from 'storybook/internal/manager-api'; import { Source } from '@storybook/blocks'; -addons.register(ADDON_ID, async (api) => { - // at this point, the parameters are not yet defined so we can not check whether the addon panel should - // be added or not. The "PRELOAD_ENTRIES" event seems to be the earliest point in time where the parameters - // are available - const isDisabled = await new Promise((resolve) => { - api.once(PRELOAD_ENTRIES, () => { - const parameter = api.getCurrentParameter(PARAM_KEY); - resolve(shouldDisableAddonPanel(parameter)); - }); - }); - - if (isDisabled) { - return; - } - +addons.register(ADDON_ID, (api) => { addons.add(PANEL_ID, { title: 'Code', type: types.PANEL, - paramKey: PARAM_KEY, + // disable this with `docsSourcePanel: { disable: true }` + paramKey: 'docsSourcePanel', match: ({ viewMode }) => viewMode === 'story', render: ({ active }) => { const [codeSnippet, setSourceCode] = useAddonState<{ @@ -49,24 +35,4 @@ addons.register(ADDON_ID, async (api) => { ); }, }); - - api.emit(FORCE_RE_RENDER); }); - -const isObject = (value: unknown): value is object => { - return value != null && typeof value === 'object'; -}; - -/** - * Checks whether the addon panel should be disabled by checking the parameter.source.addonPanel - * property. - */ -const shouldDisableAddonPanel = (parameter: unknown) => { - return ( - isObject(parameter) && - 'source' in parameter && - isObject(parameter.source) && - 'addonPanel' in parameter.source && - parameter.source.addonPanel === false - ); -}; diff --git a/code/addons/docs/template/stories/sourcePanel/index.stories.tsx b/code/addons/docs/template/stories/sourcePanel/index.stories.tsx new file mode 100644 index 000000000000..dd4d2208fe6e --- /dev/null +++ b/code/addons/docs/template/stories/sourcePanel/index.stories.tsx @@ -0,0 +1,15 @@ +export default { + component: globalThis.Components.Button, + tags: ['autodocs'], + parameters: { + chromatic: { disable: true }, + docsSourcePanel: { disable: true }, + }, +}; + +export const One = { args: { label: 'One' } }; +export const Two = { args: { label: 'Two' } }; +export const WithSource = { + args: { label: 'Three' }, + parameters: { docsSourcePanel: { disable: false } }, +}; From 2be176923b7a2e00216d26b61142704851e82a92 Mon Sep 17 00:00:00 2001 From: Lars Rickert Date: Wed, 4 Dec 2024 08:10:31 +0100 Subject: [PATCH 11/28] fix typos --- MIGRATION.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 07aecbbe3973..db4d3b2d15c2 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,7 +1,7 @@

Migration

- [From version 8.4.x to 8.5.x](#from-version-84x-to-85x) - - [Added source code pnael to docs](#added-source-code-pnael-to-docs) + - [Added source code panel to docs](#added-source-code-panel-to-docs) - [From version 8.2.x to 8.3.x](#from-version-82x-to-83x) - [Removed `experimental_SIDEBAR_BOTTOM` and deprecated `experimental_SIDEBAR_TOP` addon types](#removed-experimental_sidebar_bottom-and-deprecated-experimental_sidebar_top-addon-types) - [New parameters format for addon backgrounds](#new-parameters-format-for-addon-backgrounds) @@ -423,7 +423,7 @@ ## From version 8.4.x to 8.5.x -### Added source code pnael to docs +### Added source code panel to docs Starting in 8.5, Storybook Docs (`@storybook/addon-docs`) automatically adds a new addon panel to stories that displays a source snippet beneath each story. This works similarly to the existing [source snippet doc block](https://storybook.js.org/docs/writing-docs/doc-blocks#source), but in the story view. It is intended to replace the [Storysource addon](https://storybook.js.org/addons/@storybook/addon-storysource). From 33ce8f93c8e9039c4566cc3e2ce484579282040d Mon Sep 17 00:00:00 2001 From: Lars Rickert Date: Wed, 4 Dec 2024 10:18:25 +0100 Subject: [PATCH 12/28] move docs source panel order --- code/addons/essentials/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/addons/essentials/src/index.ts b/code/addons/essentials/src/index.ts index 5809420bc1b8..a72554227ba2 100644 --- a/code/addons/essentials/src/index.ts +++ b/code/addons/essentials/src/index.ts @@ -88,9 +88,9 @@ export function addons(options: PresetOptions) { // NOTE: The order of these addons is important. return [ - 'docs', 'controls', 'actions', + 'docs', 'backgrounds', 'viewport', 'toolbars', From db146c34dcc878e321b167bdd743e1ec6d396ce9 Mon Sep 17 00:00:00 2001 From: Lars Rickert Date: Wed, 4 Dec 2024 10:54:04 +0100 Subject: [PATCH 13/28] fix react code snippet format --- code/addons/docs/src/manager.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/addons/docs/src/manager.tsx b/code/addons/docs/src/manager.tsx index fdbe0375d369..a02fe22f0764 100644 --- a/code/addons/docs/src/manager.tsx +++ b/code/addons/docs/src/manager.tsx @@ -24,7 +24,7 @@ addons.register(ADDON_ID, (api) => { useChannel({ [SNIPPET_RENDERED]: ({ source, format }) => { - setSourceCode({ source, format: format ?? 'html' }); + setSourceCode({ source, format }); }, }); From 75f235a4bd67d2db69d98324236b9082992cbe50 Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Mon, 9 Dec 2024 17:13:12 +0800 Subject: [PATCH 14/28] Fix merge conflict remnants --- MIGRATION.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 2b5ff2bcb9c6..7e46fa5bb1a2 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,10 +1,8 @@

Migration

- [From version 8.4.x to 8.5.x](#from-version-84x-to-85x) - <<<<<<< HEAD - - # [Added source code panel to docs](#added-source-code-panel-to-docs) + - [Added source code panel to docs](#added-source-code-panel-to-docs) - [Indexing behavior of @storybook/experimental-addon-test is changed](#indexing-behavior-of-storybookexperimental-addon-test-is-changed) - > > > > > > > next - [From version 8.2.x to 8.3.x](#from-version-82x-to-83x) - [Removed `experimental_SIDEBAR_BOTTOM` and deprecated `experimental_SIDEBAR_TOP` addon types](#removed-experimental_sidebar_bottom-and-deprecated-experimental_sidebar_top-addon-types) - [New parameters format for addon backgrounds](#new-parameters-format-for-addon-backgrounds) From d749986384f29891b24d45d76a86bcb27001b4ba Mon Sep 17 00:00:00 2001 From: Lars Rickert Date: Thu, 12 Dec 2024 10:15:45 +0100 Subject: [PATCH 15/28] refactor: update parameter to disable code panel --- MIGRATION.md | 4 ++- code/addons/docs/src/manager.tsx | 25 ++++++++++++++++--- .../stories/sourcePanel/index.stories.tsx | 12 +++++++-- code/core/src/manager/container/Panel.tsx | 6 +++++ code/core/src/types/modules/addons.ts | 4 +-- 5 files changed, 43 insertions(+), 8 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index f3fa585591e4..6e3f43cc5a48 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -435,7 +435,9 @@ If you wish to disable this panel globally, add the following line to your `.sto ```js export default { parameters: { - docsSourcePanel: { disable: true }, + docs: { + codePanel: false, + }, }, }; ``` diff --git a/code/addons/docs/src/manager.tsx b/code/addons/docs/src/manager.tsx index a02fe22f0764..0b84cd6e36f8 100644 --- a/code/addons/docs/src/manager.tsx +++ b/code/addons/docs/src/manager.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { AddonPanel, type SyntaxHighlighterFormatTypes } from 'storybook/internal/components'; -import { ADDON_ID, PANEL_ID, SNIPPET_RENDERED } from 'storybook/internal/docs-tools'; +import { ADDON_ID, PANEL_ID, PARAM_KEY, SNIPPET_RENDERED } from 'storybook/internal/docs-tools'; import { addons, types, useAddonState, useChannel } from 'storybook/internal/manager-api'; import { Source } from '@storybook/blocks'; @@ -10,8 +10,27 @@ addons.register(ADDON_ID, (api) => { addons.add(PANEL_ID, { title: 'Code', type: types.PANEL, - // disable this with `docsSourcePanel: { disable: true }` - paramKey: 'docsSourcePanel', + paramKey: PARAM_KEY, + /** + * This code panel can be disabled by the user by adding this parameter: + * + * @example + * + * ```ts + * parameters: { + * docs: { + * codePanel: false, + * }, + * }, + * ``` + */ + disabled: (parameters) => { + return ( + !!parameters && + typeof parameters[PARAM_KEY] === 'object' && + parameters[PARAM_KEY].codePanel === false + ); + }, match: ({ viewMode }) => viewMode === 'story', render: ({ active }) => { const [codeSnippet, setSourceCode] = useAddonState<{ diff --git a/code/addons/docs/template/stories/sourcePanel/index.stories.tsx b/code/addons/docs/template/stories/sourcePanel/index.stories.tsx index dd4d2208fe6e..9958096cb815 100644 --- a/code/addons/docs/template/stories/sourcePanel/index.stories.tsx +++ b/code/addons/docs/template/stories/sourcePanel/index.stories.tsx @@ -3,13 +3,21 @@ export default { tags: ['autodocs'], parameters: { chromatic: { disable: true }, - docsSourcePanel: { disable: true }, + docs: { + codePanel: false, + }, }, }; export const One = { args: { label: 'One' } }; + export const Two = { args: { label: 'Two' } }; + export const WithSource = { args: { label: 'Three' }, - parameters: { docsSourcePanel: { disable: false } }, + parameters: { + docs: { + codePanel: true, + }, + }, }; diff --git a/code/core/src/manager/container/Panel.tsx b/code/core/src/manager/container/Panel.tsx index c81e489d8f68..f8cc2877cef0 100644 --- a/code/core/src/manager/container/Panel.tsx +++ b/code/core/src/manager/container/Panel.tsx @@ -32,6 +32,12 @@ const getPanels = (api: API) => { if (paramKey && parameters && parameters[paramKey] && parameters[paramKey].disable) { return; } + if ( + panel.disabled === true || + (typeof panel.disabled === 'function' && panel.disabled(parameters)) + ) { + return; + } filteredPanels[id] = panel; }); diff --git a/code/core/src/types/modules/addons.ts b/code/core/src/types/modules/addons.ts index 47f5aec6412b..b2c7b4ac2f3c 100644 --- a/code/core/src/types/modules/addons.ts +++ b/code/core/src/types/modules/addons.ts @@ -5,7 +5,7 @@ import type { TestProviderConfig, TestingModuleProgressReportProgress } from '.. import type { RenderData as RouterData } from '../../router/types'; import type { ThemeVars } from '../../theming/types'; import type { API_SidebarOptions } from './api'; -import type { API_HashEntry, API_StatusState, API_StatusUpdate } from './api-stories'; +import type { API_HashEntry, API_StoryEntry } from './api-stories'; import type { Args, ArgsStoryFn as ArgsStoryFnForFramework, @@ -392,7 +392,7 @@ export interface Addon_BaseType { /** @unstable */ paramKey?: string; /** @unstable */ - disabled?: boolean; + disabled?: boolean | ((parameters: API_StoryEntry['parameters']) => boolean); /** @unstable */ hidden?: boolean; } From 9c4d4f06150af2e910b7ed31e3c2684315707c84 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Thu, 12 Dec 2024 12:49:32 +0100 Subject: [PATCH 16/28] Experimental Nextjs: Add docgen types to main config --- .../experimental-nextjs-vite/src/types.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/code/frameworks/experimental-nextjs-vite/src/types.ts b/code/frameworks/experimental-nextjs-vite/src/types.ts index 8de91a4430d9..588caf0919bf 100644 --- a/code/frameworks/experimental-nextjs-vite/src/types.ts +++ b/code/frameworks/experimental-nextjs-vite/src/types.ts @@ -1,10 +1,13 @@ import type { CompatibleString, StorybookConfig as StorybookConfigBase, + TypescriptOptions as TypescriptOptionsBase, } from 'storybook/internal/types'; import type { BuilderOptions, StorybookConfigVite } from '@storybook/builder-vite'; +import type docgenTypescript from '@joshwooding/vite-plugin-react-docgen-typescript'; + type FrameworkName = CompatibleString<'@storybook/experimental-nextjs-vite'>; type BuilderName = CompatibleString<'@storybook/builder-vite'>; @@ -31,10 +34,23 @@ type StorybookConfigFramework = { }; }; +type TypescriptOptions = TypescriptOptionsBase & { + /** + * Sets the type of Docgen when working with React and TypeScript + * + * @default `'react-docgen'` + */ + reactDocgen: 'react-docgen-typescript' | 'react-docgen' | false; + /** Configures `@joshwooding/vite-plugin-react-docgen-typescript` */ + reactDocgenTypescriptOptions: Parameters[0]; +}; + /** The interface for Storybook configuration in `main.ts` files. */ export type StorybookConfig = Omit< StorybookConfigBase, - keyof StorybookConfigVite | keyof StorybookConfigFramework + keyof StorybookConfigVite | keyof StorybookConfigFramework | 'typescript' > & StorybookConfigVite & - StorybookConfigFramework & {}; + StorybookConfigFramework & { + typescript?: Partial; + }; From bb323de1fc7413a0a7a318e76ba0286cc8c00b46 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Fri, 13 Dec 2024 11:21:30 +0100 Subject: [PATCH 17/28] simplify config types in experimental-nextjs-vite by reutilizing react-vite ones --- .../experimental-nextjs-vite/src/types.ts | 34 ++++--------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/code/frameworks/experimental-nextjs-vite/src/types.ts b/code/frameworks/experimental-nextjs-vite/src/types.ts index 588caf0919bf..0221787dccb6 100644 --- a/code/frameworks/experimental-nextjs-vite/src/types.ts +++ b/code/frameworks/experimental-nextjs-vite/src/types.ts @@ -1,12 +1,7 @@ -import type { - CompatibleString, - StorybookConfig as StorybookConfigBase, - TypescriptOptions as TypescriptOptionsBase, -} from 'storybook/internal/types'; +import type { CompatibleString } from 'storybook/internal/types'; -import type { BuilderOptions, StorybookConfigVite } from '@storybook/builder-vite'; - -import type docgenTypescript from '@joshwooding/vite-plugin-react-docgen-typescript'; +import type { BuilderOptions } from '@storybook/builder-vite'; +import type { StorybookConfig as StorybookConfigReactVite } from '@storybook/react-vite'; type FrameworkName = CompatibleString<'@storybook/experimental-nextjs-vite'>; type BuilderName = CompatibleString<'@storybook/builder-vite'>; @@ -24,7 +19,7 @@ type StorybookConfigFramework = { name: FrameworkName; options: FrameworkOptions; }; - core?: StorybookConfigBase['core'] & { + core?: StorybookConfigReactVite['core'] & { builder?: | BuilderName | { @@ -34,23 +29,6 @@ type StorybookConfigFramework = { }; }; -type TypescriptOptions = TypescriptOptionsBase & { - /** - * Sets the type of Docgen when working with React and TypeScript - * - * @default `'react-docgen'` - */ - reactDocgen: 'react-docgen-typescript' | 'react-docgen' | false; - /** Configures `@joshwooding/vite-plugin-react-docgen-typescript` */ - reactDocgenTypescriptOptions: Parameters[0]; -}; - /** The interface for Storybook configuration in `main.ts` files. */ -export type StorybookConfig = Omit< - StorybookConfigBase, - keyof StorybookConfigVite | keyof StorybookConfigFramework | 'typescript' -> & - StorybookConfigVite & - StorybookConfigFramework & { - typescript?: Partial; - }; +export type StorybookConfig = Omit & + StorybookConfigFramework; From 902dc18344bbc209804de0715a523b9529d8717c Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Fri, 13 Dec 2024 12:51:52 +0100 Subject: [PATCH 18/28] Fix webpack DefinePlugin import to use default export --- code/frameworks/nextjs/src/config/webpack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/frameworks/nextjs/src/config/webpack.ts b/code/frameworks/nextjs/src/config/webpack.ts index 030da6b2ab77..4b10922c9df9 100644 --- a/code/frameworks/nextjs/src/config/webpack.ts +++ b/code/frameworks/nextjs/src/config/webpack.ts @@ -71,5 +71,5 @@ const setupRuntimeConfig = async ( // Load DefinePlugin with a dynamic import to ensure that Next.js can first // replace webpack with its own internal instance, and we get that here. - baseConfig.plugins?.push(new (await import('webpack')).DefinePlugin(definePluginConfig)); + baseConfig.plugins?.push(new (await import('webpack')).default.DefinePlugin(definePluginConfig)); }; From 6dc27904eabb0fbfc9a4be3c8dbebe877104c571 Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Thu, 12 Dec 2024 17:56:42 +0800 Subject: [PATCH 19/28] UI: Fix controls and parameters on tag-filtered stories --- code/core/src/manager-api/lib/stories.ts | 1 + code/core/src/manager-api/modules/refs.ts | 26 +++++- code/core/src/manager-api/modules/stories.ts | 79 +++++++++++++------ code/core/src/manager-api/tests/refs.test.ts | 28 +++++-- .../src/manager-api/tests/stories.test.ts | 20 +++-- .../components/sidebar/Explorer.stories.tsx | 10 +-- .../components/sidebar/Refs.stories.tsx | 24 +++--- .../src/manager/components/sidebar/Refs.tsx | 2 +- .../components/sidebar/Sidebar.stories.tsx | 6 +- .../manager/components/sidebar/Sidebar.tsx | 1 + code/core/src/manager/container/Sidebar.tsx | 2 +- code/core/src/types/modules/api.ts | 2 + 12 files changed, 136 insertions(+), 65 deletions(-) diff --git a/code/core/src/manager-api/lib/stories.ts b/code/core/src/manager-api/lib/stories.ts index 59b59f070a10..28fbc190853b 100644 --- a/code/core/src/manager-api/lib/stories.ts +++ b/code/core/src/manager-api/lib/stories.ts @@ -342,6 +342,7 @@ export const transformStoryIndexToStoriesHash = ( .reduce(addItem, orphanHash); }; +/** Now we need to patch in the existing prepared stories */ export const addPreparedStories = (newHash: API_IndexHash, oldHash?: API_IndexHash) => { if (!oldHash) { return newHash; diff --git a/code/core/src/manager-api/modules/refs.ts b/code/core/src/manager-api/modules/refs.ts index 166c04786adc..cb9b8916ce29 100644 --- a/code/core/src/manager-api/modules/refs.ts +++ b/code/core/src/manager-api/modules/refs.ts @@ -179,7 +179,15 @@ export const init: ModuleFn = ( }, changeRefVersion: async (id, url) => { const { versions, title } = api.getRefs()[id]; - const ref: API_SetRefData = { id, url, versions, title, index: {}, expanded: true }; + const ref: API_SetRefData = { + id, + url, + versions, + title, + index: {}, + filteredIndex: {}, + expanded: true, + }; await api.setRef(id, { ...ref, type: 'unknown' }, false); await api.checkRef(ref); @@ -292,6 +300,7 @@ export const init: ModuleFn = ( // eslint-disable-next-line @typescript-eslint/naming-convention let internal_index: StoryIndex | undefined; let index: API_IndexHash | undefined; + let filteredIndex: API_IndexHash | undefined; const { filters } = store.getState(); const { storyMapper = defaultStoryMapper } = provider.getConfig(); const ref = api.getRefs()[id]; @@ -304,19 +313,28 @@ export const init: ModuleFn = ( : storyIndex; // @ts-expect-error (could be undefined) - index = transformStoryIndexToStoriesHash(storyIndex, { + filteredIndex = transformStoryIndexToStoriesHash(storyIndex, { provider, docsOptions, filters, status: {}, }); + // @ts-expect-error (could be undefined) + index = transformStoryIndexToStoriesHash(storyIndex, { + provider, + docsOptions, + filters: {}, + status: {}, + }); } if (index) { index = addRefIds(index, ref); } - - await api.updateRef(id, { ...ref, ...rest, index, internal_index }); + if (filteredIndex) { + filteredIndex = addRefIds(filteredIndex, ref); + } + await api.updateRef(id, { ...ref, ...rest, index, filteredIndex, internal_index }); }, updateRef: async (id, data) => { diff --git a/code/core/src/manager-api/modules/stories.ts b/code/core/src/manager-api/modules/stories.ts index 3c5aac769c6a..fa295bc20567 100644 --- a/code/core/src/manager-api/modules/stories.ts +++ b/code/core/src/manager-api/modules/stories.ts @@ -568,41 +568,61 @@ export const init: ModuleFn = ({ // The story index we receive on fetchStoryIndex is not, but all the prepared fields are optional // so we can cast one to the other easily enough setIndex: async (input) => { - const { index: oldHash, status, filters } = store.getState(); - const newHash = transformStoryIndexToStoriesHash(input, { + const { filteredIndex: oldFilteredHash, index: oldHash, status, filters } = store.getState(); + const newFilteredHash = transformStoryIndexToStoriesHash(input, { provider, docsOptions, status, filters, }); + const newHash = transformStoryIndexToStoriesHash(input, { + provider, + docsOptions, + status, + filters: {}, + }); - // Now we need to patch in the existing prepared stories - const output = addPreparedStories(newHash, oldHash); - - await store.setState({ internal_index: input, index: output, indexError: undefined }); + await store.setState({ + internal_index: input, + filteredIndex: addPreparedStories(newFilteredHash, oldFilteredHash), + index: addPreparedStories(newHash, oldHash), + indexError: undefined, + }); }, + // FIXME: is there a bug where filtered stories get added back in on updateStory??? updateStory: async ( storyId: StoryId, update: StoryUpdate, ref?: API_ComposedRef ): Promise => { if (!ref) { - const { index } = store.getState(); - if (!index) { - return; + const { index, filteredIndex } = store.getState(); + if (index) { + index[storyId] = { + ...index[storyId], + ...update, + } as API_StoryEntry; + } + if (filteredIndex) { + filteredIndex[storyId] = { + ...filteredIndex[storyId], + ...update, + } as API_StoryEntry; + } + if (index || filteredIndex) { + await store.setState({ index, filteredIndex }); } + } else { + const { id: refId, index, filteredIndex }: any = ref; index[storyId] = { ...index[storyId], ...update, } as API_StoryEntry; - await store.setState({ index }); - } else { - const { id: refId, index }: any = ref; - index[storyId] = { - ...index[storyId], + filteredIndex[storyId] = { + ...filteredIndex[storyId], ...update, } as API_StoryEntry; - await fullAPI.updateRef(refId, { index }); + await fullAPI.updateRef(refId, { index, filteredIndex }); } }, updateDocs: async ( @@ -611,22 +631,33 @@ export const init: ModuleFn = ({ ref?: API_ComposedRef ): Promise => { if (!ref) { - const { index } = store.getState(); - if (!index) { - return; + const { index, filteredIndex } = store.getState(); + if (index) { + index[docsId] = { + ...index[docsId], + ...update, + } as API_DocsEntry; } + if (filteredIndex) { + filteredIndex[docsId] = { + ...filteredIndex[docsId], + ...update, + } as API_DocsEntry; + } + if (index || filteredIndex) { + await store.setState({ index, filteredIndex }); + } + } else { + const { id: refId, index, filteredIndex }: any = ref; index[docsId] = { ...index[docsId], ...update, } as API_DocsEntry; - await store.setState({ index }); - } else { - const { id: refId, index }: any = ref; - index[docsId] = { - ...index[docsId], + filteredIndex[docsId] = { + ...filteredIndex[docsId], ...update, } as API_DocsEntry; - await fullAPI.updateRef(refId, { index }); + await fullAPI.updateRef(refId, { index, filteredIndex }); } }, setPreviewInitialized: async (ref) => { diff --git a/code/core/src/manager-api/tests/refs.test.ts b/code/core/src/manager-api/tests/refs.test.ts index c8d65baafc57..b9d39ee6a88b 100644 --- a/code/core/src/manager-api/tests/refs.test.ts +++ b/code/core/src/manager-api/tests/refs.test.ts @@ -291,6 +291,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": undefined, "id": "fake", "index": undefined, "indexError": { @@ -360,6 +361,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": undefined, "id": "fake", "index": undefined, "indexError": { @@ -504,6 +506,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": {}, "id": "fake", "index": {}, "internal_index": { @@ -522,6 +525,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": {}, "id": "fake", "index": {}, "internal_index": { @@ -601,6 +605,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": {}, "id": "fake", "index": {}, "internal_index": { @@ -682,6 +687,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": {}, "id": "fake", "index": {}, "internal_index": { @@ -763,6 +769,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": undefined, "id": "fake", "index": undefined, "internal_index": undefined, @@ -905,6 +912,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": undefined, "id": "fake", "index": undefined, "internal_index": undefined, @@ -987,6 +995,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": {}, "id": "fake", "index": {}, "internal_index": { @@ -1068,6 +1077,7 @@ describe('Refs API', () => { { "refs": { "fake": { + "filteredIndex": {}, "id": "fake", "index": {}, "internal_index": { @@ -1227,18 +1237,20 @@ describe('Refs API', () => { }, }; + const transformOptions = { + provider: provider as any, + docsOptions: {}, + filters: {}, + status: {}, + }; const initialState: Partial = { refs: { fake: { id: 'fake', url: 'https://example.com', previewInitialized: true, - index: transformStoryIndexToStoriesHash(index, { - provider: provider as any, - docsOptions: {}, - filters: {}, - status: {}, - }), + index: transformStoryIndexToStoriesHash(index, transformOptions), + filteredIndex: transformStoryIndexToStoriesHash(index, transformOptions), internal_index: index, }, }, @@ -1261,10 +1273,10 @@ describe('Refs API', () => { await api.setRef('fake', { storyIndex: index }); - await expect(api.getRefs().fake.index).toEqual( + await expect(api.getRefs().fake.filteredIndex).toEqual( expect.objectContaining({ 'a--1': expect.anything() }) ); - await expect(api.getRefs().fake.index).not.toEqual( + await expect(api.getRefs().fake.filteredIndex).not.toEqual( expect.objectContaining({ 'a--2': expect.anything() }) ); }); diff --git a/code/core/src/manager-api/tests/stories.test.ts b/code/core/src/manager-api/tests/stories.test.ts index b652c87cd7a1..6f480061d9ec 100644 --- a/code/core/src/manager-api/tests/stories.test.ts +++ b/code/core/src/manager-api/tests/stories.test.ts @@ -765,10 +765,15 @@ describe('stories API', () => { source: '', sourceLocation: '', type: '', - ref: { id: 'refId', index: { 'a--1': { args: { a: 'b' } } } } as any, + ref: { + id: 'refId', + index: { 'a--1': { args: { a: 'b' } } }, + filteredIndex: { 'a--1': { args: { a: 'b' } } }, + } as any, }); provider.channel.emit(STORY_ARGS_UPDATED, { storyId: 'a--1', args: { foo: 'bar' } }); expect(fullAPI.updateRef).toHaveBeenCalledWith('refId', { + filteredIndex: { 'a--1': { args: { foo: 'bar' } } }, index: { 'a--1': { args: { foo: 'bar' } } }, }); }); @@ -1539,6 +1544,7 @@ describe('stories API', () => { }) ); }); + it('updates state', async () => { const moduleArgs = createMockModuleArgs({}); const { api } = initStories(moduleArgs as unknown as ModuleArgs); @@ -1565,9 +1571,9 @@ describe('stories API', () => { await api.setIndex({ v: 5, entries: navigationEntries }); await api.experimental_setFilter('myCustomFilter', (item: any) => item.id.startsWith('a')); - const { index } = store.getState(); + const { filteredIndex } = store.getState(); - expect(index).toMatchInlineSnapshot(` + expect(filteredIndex).toMatchInlineSnapshot(` { "a": { "children": [ @@ -1624,7 +1630,7 @@ describe('stories API', () => { ); // empty, because there are no stories with status - expect(store.getState().index).toMatchInlineSnapshot('{}'); + expect(store.getState().filteredIndex).toMatchInlineSnapshot('{}'); // setting status should update the index await api.experimental_updateStatus('a-addon-id', { @@ -1636,7 +1642,7 @@ describe('stories API', () => { 'a--2': { status: 'success', title: 'a addon title', description: '' }, }); - expect(store.getState().index).toMatchInlineSnapshot(` + expect(store.getState().filteredIndex).toMatchInlineSnapshot(` { "a": { "children": [ @@ -1676,9 +1682,9 @@ describe('stories API', () => { await api.setIndex({ v: 5, entries: navigationEntries }); - const { index } = store.getState(); + const { filteredIndex } = store.getState(); - expect(index).toMatchInlineSnapshot(` + expect(filteredIndex).toMatchInlineSnapshot(` { "a": { "children": [ diff --git a/code/core/src/manager/components/sidebar/Explorer.stories.tsx b/code/core/src/manager/components/sidebar/Explorer.stories.tsx index 908ead2be9f3..34adad0a32a0 100644 --- a/code/core/src/manager/components/sidebar/Explorer.stories.tsx +++ b/code/core/src/manager/components/sidebar/Explorer.stories.tsx @@ -34,7 +34,7 @@ const simple: Record = { url: 'iframe.html', previewInitialized: true, // @ts-expect-error (invalid input) - index: mockDataset.withRoot, + filteredIndex: mockDataset.withRoot, }, }; @@ -47,7 +47,7 @@ const withRefs: Record = { previewInitialized: true, type: 'auto-inject', // @ts-expect-error (invalid input) - index: mockDataset.noRoot, + filteredIndex: mockDataset.noRoot, }, injected: { id: 'injected', @@ -56,7 +56,7 @@ const withRefs: Record = { previewInitialized: false, type: 'auto-inject', // @ts-expect-error (invalid input) - index: mockDataset.noRoot, + filteredIndex: mockDataset.noRoot, }, unknown: { id: 'unknown', @@ -65,7 +65,7 @@ const withRefs: Record = { previewInitialized: true, type: 'unknown', // @ts-expect-error (invalid input) - index: mockDataset.noRoot, + filteredIndex: mockDataset.noRoot, }, lazy: { id: 'lazy', @@ -74,7 +74,7 @@ const withRefs: Record = { previewInitialized: false, type: 'lazy', // @ts-expect-error (invalid input) - index: mockDataset.withRoot, + filteredIndex: mockDataset.withRoot, }, }; diff --git a/code/core/src/manager/components/sidebar/Refs.stories.tsx b/code/core/src/manager/components/sidebar/Refs.stories.tsx index a042970beadc..3b6e37efce50 100644 --- a/code/core/src/manager/components/sidebar/Refs.stories.tsx +++ b/code/core/src/manager/components/sidebar/Refs.stories.tsx @@ -37,11 +37,11 @@ export default { }; const { menu } = standardHeaderData; -const index = mockDataset.withRoot; +const filteredIndex = mockDataset.withRoot; const storyId = '1-12-121'; -export const simpleData = { menu, index, storyId }; -export const loadingData = { menu, index: {} }; +export const simpleData = { menu, filteredIndex, storyId }; +export const loadingData = { menu, filteredIndex: {} }; // @ts-expect-error (non strict) const indexError: Error = (() => { @@ -60,14 +60,14 @@ const refs: Record = { previewInitialized: false, type: 'lazy', // @ts-expect-error (invalid input) - index, + filteredIndex, }, empty: { id: 'empty', title: 'It is empty because no stories were loaded', url: 'https://example.com', type: 'lazy', - index: {}, + filteredIndex: {}, previewInitialized: false, }, startInjected_unknown: { @@ -77,7 +77,7 @@ const refs: Record = { type: 'unknown', previewInitialized: false, // @ts-expect-error (invalid input) - index, + filteredIndex, }, startInjected_loading: { id: 'startInjected_loading', @@ -86,7 +86,7 @@ const refs: Record = { type: 'auto-inject', previewInitialized: false, // @ts-expect-error (invalid input) - index, + filteredIndex, }, startInjected_ready: { id: 'startInjected_ready', @@ -95,7 +95,7 @@ const refs: Record = { type: 'auto-inject', previewInitialized: true, // @ts-expect-error (invalid input) - index, + filteredIndex, }, versions: { id: 'versions', @@ -103,7 +103,7 @@ const refs: Record = { url: 'https://example.com', type: 'lazy', // @ts-expect-error (invalid input) - index, + filteredIndex, versions: { '1.0.0': 'https://example.com/v1', '2.0.0': 'https://example.com' }, previewInitialized: true, }, @@ -113,7 +113,7 @@ const refs: Record = { url: 'https://example.com', type: 'lazy', // @ts-expect-error (invalid input) - index, + filteredIndex, versions: { '1.0.0': 'https://example.com/v1', '2.0.0': 'https://example.com/v2' }, previewInitialized: true, }, @@ -138,7 +138,7 @@ const refs: Record = { title: 'This storybook has a very very long name for some reason', url: 'https://example.com', // @ts-expect-error (invalid input) - index, + filteredIndex, type: 'lazy', versions: { '111.111.888-new': 'https://example.com/new', @@ -154,7 +154,7 @@ const refs: Record = { previewInitialized: false, type: 'lazy', // @ts-expect-error (invalid input) - index, + filteredIndex, }, }; diff --git a/code/core/src/manager/components/sidebar/Refs.tsx b/code/core/src/manager/components/sidebar/Refs.tsx index 7412a627e889..b4d2a79ca351 100644 --- a/code/core/src/manager/components/sidebar/Refs.tsx +++ b/code/core/src/manager/components/sidebar/Refs.tsx @@ -81,7 +81,7 @@ export const Ref: FC = React. const { docsOptions } = useStorybookState(); const api = useStorybookApi(); const { - index, + filteredIndex: index, id: refId, title = refId, isLoading: isLoadingMain, diff --git a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx index 53f22957f17d..0e2df01f6aed 100644 --- a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx +++ b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx @@ -111,7 +111,7 @@ const refs: Record = { title: 'This is a ref', url: 'https://example.com', type: 'lazy', - index, + filteredIndex: index, previewInitialized: true, }, }; @@ -123,7 +123,7 @@ const refsError = { optimized: { ...refs.optimized, // @ts-expect-error (non strict) - index: undefined as IndexHash, + filteredIndex: undefined as IndexHash, indexError, }, }; @@ -132,7 +132,7 @@ const refsEmpty = { optimized: { ...refs.optimized, // type: 'auto-inject', - index: {} as IndexHash, + filteredIndex: {} as IndexHash, }, }; diff --git a/code/core/src/manager/components/sidebar/Sidebar.tsx b/code/core/src/manager/components/sidebar/Sidebar.tsx index d4784cc8c562..dfec7fba7826 100644 --- a/code/core/src/manager/components/sidebar/Sidebar.tsx +++ b/code/core/src/manager/components/sidebar/Sidebar.tsx @@ -93,6 +93,7 @@ const useCombination = ( () => ({ [DEFAULT_REF_ID]: { index, + filteredIndex: index, indexError, previewInitialized, status, diff --git a/code/core/src/manager/container/Sidebar.tsx b/code/core/src/manager/container/Sidebar.tsx index bc05d1713b59..723d9989ac10 100755 --- a/code/core/src/manager/container/Sidebar.tsx +++ b/code/core/src/manager/container/Sidebar.tsx @@ -27,7 +27,7 @@ const Sidebar = React.memo(function Sideber({ onMenuClick }: SidebarProps) { // is actually the stories hash. We should fix this up and make it consistent. // eslint-disable-next-line @typescript-eslint/naming-convention internal_index, - index, + filteredIndex: index, status, indexError, previewInitialized, diff --git a/code/core/src/types/modules/api.ts b/code/core/src/types/modules/api.ts index 30edbe36ad29..f5e2d6f32100 100644 --- a/code/core/src/types/modules/api.ts +++ b/code/core/src/types/modules/api.ts @@ -155,6 +155,7 @@ export type API_StoryMapper = (ref: API_ComposedRef, story: SetStoriesStory) => export interface API_LoadedRefData { index?: API_IndexHash; + filteredIndex?: API_IndexHash; indexError?: Error; previewInitialized: boolean; } @@ -180,6 +181,7 @@ export type API_ComposedRefUpdate = Partial< | 'type' | 'expanded' | 'index' + | 'filteredIndex' | 'versions' | 'loginUrl' | 'version' From 632402769c40be7c979a0095e16ee5c30667900b Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Fri, 13 Dec 2024 13:12:20 +0100 Subject: [PATCH 20/28] Build: Improve waiting detection on E2E to fix flake --- code/e2e-tests/util.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/code/e2e-tests/util.ts b/code/e2e-tests/util.ts index 057dc91f715d..0e8c6d0b93d8 100644 --- a/code/e2e-tests/util.ts +++ b/code/e2e-tests/util.ts @@ -112,6 +112,10 @@ export class SbPage { const storyLoadingPage = root.locator('.sb-preparing-story'); await docsLoadingPage.waitFor({ state: 'hidden' }); await storyLoadingPage.waitFor({ state: 'hidden' }); + await this.page.waitForFunction(() => { + const storybookRoot = document.querySelector('#storybook-root'); + return storybookRoot && storybookRoot.children.length > 0; + }); } previewIframe() { From cf809b93e46286e5a446153e6d567bf1428d52bd Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Fri, 13 Dec 2024 13:15:51 +0100 Subject: [PATCH 21/28] add deps optimization in react kitchen sink --- .../react/.storybook/main.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/test-storybooks/portable-stories-kitchen-sink/react/.storybook/main.ts b/test-storybooks/portable-stories-kitchen-sink/react/.storybook/main.ts index 0a76f80b4d25..57b96d2e8d1e 100644 --- a/test-storybooks/portable-stories-kitchen-sink/react/.storybook/main.ts +++ b/test-storybooks/portable-stories-kitchen-sink/react/.storybook/main.ts @@ -12,9 +12,21 @@ const config: StorybookConfig = { options: {}, }, core: { - disableWhatsNewNotifications: true + disableWhatsNewNotifications: true, }, - previewHead: (head = '') => `${head} + viteFinal: (config) => ({ + ...config, + optimizeDeps: { + ...config.optimizeDeps, + include: [ + ...(config.optimizeDeps?.include || []), + "react-dom/test-utils", + "@storybook/react/**", + "@storybook/experimental-addon-test/preview", + ], + }, + }), + previewHead: (head = "") => `${head}