diff --git a/.circleci/config.yml b/.circleci/config.yml index f7620794f87b..850d4f0175d9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -206,7 +206,7 @@ jobs: name: Knip command: | cd code - yarn knip --no-exit-code + yarn knip --no-exit-code - report-workflow-on-failure - cancel-workflow-on-failure bench-packages: diff --git a/code/.storybook/main.ts b/code/.storybook/main.ts index 43a8f3269c86..89f1b6a36226 100644 --- a/code/.storybook/main.ts +++ b/code/.storybook/main.ts @@ -1,12 +1,12 @@ import { join } from 'node:path'; -import type { StorybookConfig } from '../frameworks/react-vite'; +import { defineMain } from '../frameworks/react-vite/src/node'; const componentsPath = join(__dirname, '../core/src/components'); const managerApiPath = join(__dirname, '../core/src/manager-api'); const imageContextPath = join(__dirname, '../frameworks/nextjs/src/image-context.ts'); -const config: StorybookConfig = { +const config = defineMain({ stories: [ './*.stories.@(js|jsx|ts|tsx)', { @@ -170,6 +170,6 @@ const config: StorybookConfig = { } satisfies typeof viteConfig); }, // logLevel: 'debug', -}; +}); export default config; diff --git a/code/.storybook/preview.tsx b/code/.storybook/preview.tsx index cc4481a56d1c..49997225fef7 100644 --- a/code/.storybook/preview.tsx +++ b/code/.storybook/preview.tsx @@ -18,7 +18,20 @@ import { DocsContext } from '@storybook/blocks'; import { global } from '@storybook/global'; import type { Decorator, Loader, ReactRenderer } from '@storybook/react'; +// TODO add empty preview +// import * as storysource from '@storybook/addon-storysource'; +// import * as designs from '@storybook/addon-designs/preview'; +import addonTest from '@storybook/experimental-addon-test'; +import { definePreview } from '@storybook/react-vite'; + +import addonA11y from '@storybook/addon-a11y'; +import addonEssentials from '@storybook/addon-essentials'; +import addonThemes from '@storybook/addon-themes'; + +import * as addonsPreview from '../addons/toolbars/template/stories/preview'; +import * as templatePreview from '../core/template/stories/preview'; import { DocsPageWrapper } from '../lib/blocks/src/components'; +import '../renderers/react/template/components/index'; import { isChromatic } from './isChromatic'; const { document } = global; @@ -120,7 +133,7 @@ const ThemedSetRoot = () => { // eslint-disable-next-line no-underscore-dangle const preview = (window as any).__STORYBOOK_PREVIEW__ as PreviewWeb | undefined; const channel = (window as any).__STORYBOOK_ADDONS_CHANNEL__ as Channel | undefined; -export const loaders = [ +const loaders = [ /** * This loader adds a DocsContext to the story, which is required for the most Blocks to work. A * story will specify which stories they need in the index with: @@ -169,7 +182,7 @@ export const loaders = [ }, ] as Loader[]; -export const decorators = [ +const decorators = [ // This decorator adds the DocsContext created in the loader above (Story, { loaded: { docsContext } }) => docsContext ? ( @@ -307,11 +320,7 @@ export const decorators = [ }, ] satisfies Decorator[]; -export const parameters = { - options: { - storySort: (a, b) => - a.title === b.title ? 0 : a.id.localeCompare(b.id, undefined, { numeric: true }), - }, +const parameters = { docs: { theme: themes.light, toc: {}, @@ -360,4 +369,17 @@ export const parameters = { }, }; -export const tags = ['test', 'vitest']; +export default definePreview({ + addons: [ + addonThemes(), + addonEssentials(), + addonA11y(), + addonTest(), + addonsPreview, + templatePreview, + ], + decorators, + loaders, + tags: ['test', 'vitest'], + parameters, +}); diff --git a/code/.storybook/storybook.setup.ts b/code/.storybook/storybook.setup.ts index ce62499fa0a6..80160218a314 100644 --- a/code/.storybook/storybook.setup.ts +++ b/code/.storybook/storybook.setup.ts @@ -3,25 +3,12 @@ import { beforeAll, vi, expect as vitestExpect } from 'vitest'; import { setProjectAnnotations } from '@storybook/react'; import { userEvent as storybookEvent, expect as storybookExpect } from '@storybook/test'; -// eslint-disable-next-line import/namespace -import * as testAnnotations from '@storybook/experimental-addon-test/preview'; - -import * as a11yAddonAnnotations from '@storybook/addon-a11y/preview'; - -import * as coreAnnotations from '../addons/toolbars/template/stories/preview'; -import * as componentAnnotations from '../core/template/stories/preview'; -// register global components used in many stories -import '../renderers/react/template/components'; -import * as projectAnnotations from './preview'; +import preview from './preview'; vi.spyOn(console, 'warn').mockImplementation((...args) => console.log(...args)); const annotations = setProjectAnnotations([ - a11yAddonAnnotations, - projectAnnotations, - componentAnnotations, - coreAnnotations, - testAnnotations, + preview.composed, { // experiment with injecting Vitest's interactivity API over our userEvent while tests run in browser mode // https://vitest.dev/guide/browser/interactivity-api.html diff --git a/code/addons/a11y/src/index.ts b/code/addons/a11y/src/index.ts index ce28f952df01..775cfe3181d2 100644 --- a/code/addons/a11y/src/index.ts +++ b/code/addons/a11y/src/index.ts @@ -1,2 +1,9 @@ +import { definePreview } from 'storybook/internal/preview-api'; + +import * as addonAnnotations from './preview'; + export { PARAM_KEY } from './constants'; export * from './params'; +export type { A11yParameters } from './types'; + +export default () => definePreview(addonAnnotations); diff --git a/code/addons/a11y/src/params.ts b/code/addons/a11y/src/params.ts index e66a0813a42c..72b3dd39dde6 100644 --- a/code/addons/a11y/src/params.ts +++ b/code/addons/a11y/src/params.ts @@ -1,4 +1,4 @@ -import type { ElementContext, ImpactValue, RunOptions, Spec } from 'axe-core'; +import type { ElementContext, RunOptions, Spec } from 'axe-core'; export interface Setup { element?: ElementContext; diff --git a/code/addons/a11y/src/types.ts b/code/addons/a11y/src/types.ts index 9e116e5aab2b..b05b75ca1833 100644 --- a/code/addons/a11y/src/types.ts +++ b/code/addons/a11y/src/types.ts @@ -1,3 +1,49 @@ -import type { AxeResults } from 'axe-core'; +import type { AxeResults, ElementContext, RunOptions, Spec } from 'axe-core'; export type A11YReport = AxeResults | { error: Error }; + +export interface A11yParameters { + /** + * Accessibility configuration + * + * @see https://storybook.js.org/docs/writing-tests/accessibility-testing + */ + a11y?: { + /** Manual configuration for specific elements */ + element?: ElementContext; + + /** + * Configuration for the accessibility rules + * + * @see https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#api-name-axeconfigure + */ + config?: Spec; + + /** + * Options for the accessibility checks To learn more about the available options, + * + * @see https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#options-parameter + */ + options?: RunOptions; + + /** Remove the addon panel and disable the addon's behavior */ + disable?: boolean; + }; +} + +export interface A11yGlobals { + /** + * Accessibility configuration + * + * @see https://storybook.js.org/docs/writing-tests/accessibility-testing + */ + a11y: { + /** + * Prevent the addon from executing automated accessibility checks upon visiting a story. You + * can still trigger the checks from the addon panel. + * + * @see https://storybook.js.org/docs/writing-tests/accessibility-testing#turn-off-automated-a11y-tests + */ + manual?: boolean; + }; +} diff --git a/code/addons/actions/src/index.ts b/code/addons/actions/src/index.ts index d2d3261dc960..567dd618392b 100644 --- a/code/addons/actions/src/index.ts +++ b/code/addons/actions/src/index.ts @@ -1,3 +1,11 @@ +import { definePreview } from 'storybook/internal/preview-api'; + +import * as addonAnnotations from './preview'; + export * from './constants'; export * from './models'; export * from './runtime'; + +export default () => definePreview(addonAnnotations); + +export type { ActionsParameters } from './types'; diff --git a/code/addons/actions/src/runtime/action.ts b/code/addons/actions/src/runtime/action.ts index 6fea0cb90a74..81e6d6002634 100644 --- a/code/addons/actions/src/runtime/action.ts +++ b/code/addons/actions/src/runtime/action.ts @@ -74,7 +74,7 @@ export function action(name: string, options: ActionOptions = {}): HandlerFuncti ); if (storyRenderer) { - const deprecated = !window?.FEATURES?.disallowImplicitActionsInRenderV8; + const deprecated = !globalThis?.FEATURES?.disallowImplicitActionsInRenderV8; const error = new ImplicitActionsDuringRendering({ phase: storyRenderer.phase!, name, diff --git a/code/addons/actions/src/types.ts b/code/addons/actions/src/types.ts new file mode 100644 index 000000000000..47b3bb9ddd84 --- /dev/null +++ b/code/addons/actions/src/types.ts @@ -0,0 +1,38 @@ +export interface ActionsParameters { + /** + * Actions configuration + * + * @see https://storybook.js.org/docs/essentials/actions#parameters + */ + actions: { + /** + * Create actions for each arg that matches the regex. (**NOT recommended, see below**) + * + * This is quite useful when your component has dozens (or hundreds) of methods and you do not + * want to manually apply the fn utility for each of those methods. However, this is not the + * recommended way of writing actions. That's because automatically inferred args are not + * available as spies in your play function. If you use argTypesRegex and your stories have play + * functions, you will need to also define args with the fn utility to test them in your play + * function. + * + * @example `argTypesRegex: '^on.*'` + */ + argTypesRegex?: string; + + /** Remove the addon panel and disable the addon's behavior */ + disable?: boolean; + + /** + * Binds a standard HTML event handler to the outermost HTML element rendered by your component + * and triggers an action when the event is called for a given selector. The format is + * ` `. The selector is optional; it defaults to all elements. + * + * **To enable this feature, you must use the `withActions` decorator.** + * + * @example `handles: ['mouseover', 'click .btn']` + * + * @see https://storybook.js.org/docs/essentials/actions#action-event-handlers + */ + handles?: string[]; + }; +} diff --git a/code/addons/backgrounds/package.json b/code/addons/backgrounds/package.json index 24d309d59f2f..e021d34c3ee3 100644 --- a/code/addons/backgrounds/package.json +++ b/code/addons/backgrounds/package.json @@ -43,6 +43,16 @@ "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "dist/index.d.ts" + ], + "preview": [ + "dist/preview.d.ts" + ] + } + }, "files": [ "dist/**/*", "README.md", diff --git a/code/addons/backgrounds/src/decorator.ts b/code/addons/backgrounds/src/decorator.ts index 9fa8419d8eb0..9806cdd546f6 100644 --- a/code/addons/backgrounds/src/decorator.ts +++ b/code/addons/backgrounds/src/decorator.ts @@ -1,9 +1,5 @@ import { useEffect } from 'storybook/internal/preview-api'; -import type { - Renderer, - StoryContext, - PartialStoryFn as StoryFunction, -} from 'storybook/internal/types'; +import type { DecoratorFunction } from 'storybook/internal/types'; import { PARAM_KEY as KEY } from './constants'; import { DEFAULT_BACKGROUNDS } from './defaults'; @@ -21,10 +17,7 @@ const GRID_SELECTOR_BASE = 'addon-backgrounds-grid'; const transitionStyle = isReduceMotionEnabled() ? '' : 'transition: background-color 0.3s;'; -export const withBackgroundAndGrid = ( - StoryFn: StoryFunction, - context: StoryContext -) => { +export const withBackgroundAndGrid: DecoratorFunction = (StoryFn, context) => { const { globals, parameters, viewMode, id } = context; const { options = DEFAULT_BACKGROUNDS, diff --git a/code/addons/backgrounds/src/index.ts b/code/addons/backgrounds/src/index.ts index dafa948eda6c..1d169dff54f5 100644 --- a/code/addons/backgrounds/src/index.ts +++ b/code/addons/backgrounds/src/index.ts @@ -1,2 +1,7 @@ -// make it work with --isolatedModules -export default {}; +import { definePreview } from 'storybook/internal/preview-api'; + +import * as addonAnnotations from './preview'; + +export default () => definePreview(addonAnnotations); + +export type { BackgroundsParameters, BackgroundsGlobals } from './types'; diff --git a/code/addons/backgrounds/src/legacy/withBackgroundLegacy.ts b/code/addons/backgrounds/src/legacy/withBackgroundLegacy.ts index a7d42e9d46b0..0223f2bd0e59 100644 --- a/code/addons/backgrounds/src/legacy/withBackgroundLegacy.ts +++ b/code/addons/backgrounds/src/legacy/withBackgroundLegacy.ts @@ -1,18 +1,11 @@ import { useEffect, useMemo } from 'storybook/internal/preview-api'; -import type { - Renderer, - StoryContext, - PartialStoryFn as StoryFunction, -} from 'storybook/internal/types'; +import type { DecoratorFunction } from 'storybook/internal/types'; import { PARAM_KEY as BACKGROUNDS_PARAM_KEY } from '../constants'; import { addBackgroundStyle, clearStyles, isReduceMotionEnabled } from '../utils'; import { getBackgroundColorByName } from './getBackgroundColorByName'; -export const withBackground = ( - StoryFn: StoryFunction, - context: StoryContext -) => { +export const withBackground: DecoratorFunction = (StoryFn, context) => { const { globals, parameters } = context; const globalsBackgroundColor = globals[BACKGROUNDS_PARAM_KEY]?.value; const backgroundsConfig = parameters[BACKGROUNDS_PARAM_KEY]; diff --git a/code/addons/backgrounds/src/legacy/withGridLegacy.ts b/code/addons/backgrounds/src/legacy/withGridLegacy.ts index 3fb711c772e2..ed98d1985854 100644 --- a/code/addons/backgrounds/src/legacy/withGridLegacy.ts +++ b/code/addons/backgrounds/src/legacy/withGridLegacy.ts @@ -1,14 +1,10 @@ import { useEffect, useMemo } from 'storybook/internal/preview-api'; -import type { - Renderer, - StoryContext, - PartialStoryFn as StoryFunction, -} from 'storybook/internal/types'; +import type { DecoratorFunction } from 'storybook/internal/types'; import { PARAM_KEY as BACKGROUNDS_PARAM_KEY } from '../constants'; import { addGridStyle, clearStyles } from '../utils'; -export const withGrid = (StoryFn: StoryFunction, context: StoryContext) => { +export const withGrid: DecoratorFunction = (StoryFn, context) => { const { globals, parameters } = context; const gridParameters = parameters[BACKGROUNDS_PARAM_KEY].grid; const isActive = globals[BACKGROUNDS_PARAM_KEY]?.grid === true && gridParameters.disable !== true; diff --git a/code/addons/backgrounds/src/preview.ts b/code/addons/backgrounds/src/preview.ts index 212a88c0bec2..27f9e258a7d8 100644 --- a/code/addons/backgrounds/src/preview.ts +++ b/code/addons/backgrounds/src/preview.ts @@ -1,5 +1,3 @@ -import type { Addon_DecoratorFunction } from 'storybook/internal/types'; - import { PARAM_KEY as KEY } from './constants'; import { withBackgroundAndGrid } from './decorator'; import { DEFAULT_BACKGROUNDS } from './defaults'; @@ -7,7 +5,7 @@ import { withBackground } from './legacy/withBackgroundLegacy'; import { withGrid } from './legacy/withGridLegacy'; import type { Config, GlobalState } from './types'; -export const decorators: Addon_DecoratorFunction[] = FEATURES?.backgroundsStoryGlobals +export const decorators = globalThis.FEATURES?.backgroundsStoryGlobals ? [withBackgroundAndGrid] : [withGrid, withBackground]; @@ -20,7 +18,7 @@ export const parameters = { }, disable: false, // TODO: remove in 9.0 - ...(!FEATURES?.backgroundsStoryGlobals && { + ...(!globalThis.FEATURES?.backgroundsStoryGlobals && { values: Object.values(DEFAULT_BACKGROUNDS), }), } satisfies Partial, @@ -30,4 +28,6 @@ const modern: Record = { [KEY]: { value: undefined, grid: false }, }; -export const initialGlobals = FEATURES?.backgroundsStoryGlobals ? modern : { [KEY]: null }; +export const initialGlobals = globalThis.FEATURES?.backgroundsStoryGlobals + ? modern + : { [KEY]: null }; diff --git a/code/addons/backgrounds/src/types.ts b/code/addons/backgrounds/src/types.ts index 8f6c66b20a85..e9d4fd846089 100644 --- a/code/addons/backgrounds/src/types.ts +++ b/code/addons/backgrounds/src/types.ts @@ -21,3 +21,33 @@ export interface Config { export type GlobalState = { value: string | undefined; grid: boolean }; export type GlobalStateUpdate = Partial; + +export interface BackgroundsParameters { + /** + * Backgrounds configuration + * + * @see https://storybook.js.org/docs/essentials/backgrounds#parameters + */ + backgrounds: { + /** Default background color */ + default?: string; + + /** Remove the addon panel and disable the addon's behavior */ + disable?: boolean; + + /** Configuration for the background grid */ + grid?: Partial; + + /** Available background colors */ + values?: Array; + }; +} + +export interface BackgroundsGlobals { + /** + * Backgrounds configuration + * + * @see https://storybook.js.org/docs/essentials/backgrounds#globals + */ + backgrounds: GlobalState; +} diff --git a/code/addons/controls/src/index.ts b/code/addons/controls/src/index.ts index 0fe41f8142f4..50e8392a4da5 100644 --- a/code/addons/controls/src/index.ts +++ b/code/addons/controls/src/index.ts @@ -1 +1,7 @@ +import { definePreview } from 'storybook/internal/preview-api'; + export { PARAM_KEY } from './constants'; + +export default () => definePreview({}); + +export type { ControlsParameters } from './types'; diff --git a/code/addons/controls/src/types.ts b/code/addons/controls/src/types.ts new file mode 100644 index 000000000000..d12dc06ad802 --- /dev/null +++ b/code/addons/controls/src/types.ts @@ -0,0 +1,37 @@ +export interface ControlsParameters { + /** + * Controls configuration + * + * @see https://storybook.js.org/docs/essentials/controls#parameters-1 + */ + controls: { + /** Remove the addon panel and disable the addon's behavior */ + disable?: boolean; + + /** Disable the ability to create or edit stories from the Controls panel */ + disableSaveFromUI?: boolean; + + /** Exclude specific properties from the Controls panel */ + exclude?: string[] | RegExp; + + /** + * Show the full documentation for each property in the Controls addon panel, including the + * description and default value. + */ + expanded?: boolean; + + /** Exclude only specific properties in the Controls panel */ + include?: string[] | RegExp; + + /** + * Preset color swatches for the color picker control + * + * @example PresetColors: [{ color: '#ff4785', title: 'Coral' }, 'rgba(0, 159, 183, 1)', + * '#fe4a49'] + */ + presetColors?: Array; + + /** Controls sorting order */ + sort?: 'none' | 'alpha' | 'requiredFirst'; + }; +} diff --git a/code/addons/docs/ember/index.d.ts b/code/addons/docs/ember/index.d.ts new file mode 100644 index 000000000000..18986c0c909f --- /dev/null +++ b/code/addons/docs/ember/index.d.ts @@ -0,0 +1 @@ +export declare const setJSONDoc: (jsonDoc: any) => void; diff --git a/code/addons/docs/package.json b/code/addons/docs/package.json index 41c76b9e04d1..e38011c5853e 100644 --- a/code/addons/docs/package.json +++ b/code/addons/docs/package.json @@ -81,6 +81,25 @@ "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "dist/index.d.ts" + ], + "angular": [ + "angular/index.d.ts" + ], + "blocks": [ + "dist/blocks.d.ts" + ], + "ember": [ + "ember/index.d.ts" + ], + "preview": [ + "dist/preview.d.ts" + ] + } + }, "files": [ "dist/**/*", "angular/**/*", diff --git a/code/addons/docs/src/index.ts b/code/addons/docs/src/index.ts index b74399955f12..a37140d58493 100644 --- a/code/addons/docs/src/index.ts +++ b/code/addons/docs/src/index.ts @@ -1,2 +1,9 @@ +import { definePreview } from 'storybook/internal/preview-api'; + +import * as addonAnnotations from './preview'; + export * from '@storybook/blocks'; export { DocsRenderer } from './DocsRenderer'; +export type { DocsParameters } from './types'; + +export default () => definePreview(addonAnnotations); diff --git a/code/addons/docs/src/types.ts b/code/addons/docs/src/types.ts new file mode 100644 index 000000000000..880044841577 --- /dev/null +++ b/code/addons/docs/src/types.ts @@ -0,0 +1,219 @@ +import type { ModuleExport, ModuleExports } from '@storybook/types'; + +type StoryBlockParameters = { + /** Whether a story's play function runs when shown in docs page */ + autoplay?: boolean; + /** + * Set a minimum height (note for an iframe this is the actual height) when rendering a story in + * an iframe or inline. This overrides `parameters.docs.story.iframeHeight` for iframes. + */ + height?: string; + /** IFrame configuration */ + iframeHeight?: string; + /** + * Whether the story is rendered inline (in the same browser frame as the other docs content) or + * in an iframe + */ + inline?: boolean; + /** Specifies the CSF file to which the story is associated */ + meta: ModuleExports; + /** + * Specifies which story is rendered by the Story block. If no `of` is defined and the MDX file is + * attached, the primary (first) story will be rendered. + */ + of: ModuleExport; +}; + +type ControlsBlockParameters = { + /** Exclude specific properties from the Controls panel */ + exclude?: string[] | RegExp; + + /** Exclude only specific properties in the Controls panel */ + include?: string[] | RegExp; + + /** Controls sorting order */ + sort?: 'none' | 'alpha' | 'requiredFirst'; +}; + +type ArgTypesBlockParameters = { + /** Exclude specific arg types from the args table */ + exclude?: string[] | RegExp; + + /** Exclude only specific arg types from the args table */ + include?: string[] | RegExp; + + /** + * Specifies which story to get the arg types from. If a CSF file exports is provided, it will use + * the primary (first) story in the file. + */ + of: ModuleExport | ModuleExports; + + /** + * Controls arg types order + * + * @see https://storybook.js.org/docs/api/doc-blocks/doc-block-argtypes#sort + */ + sort?: 'none' | 'alpha' | 'requiredFirst'; +}; + +type CanvasBlockParameters = { + /** + * Provides any additional custom actions to show in the bottom right corner. These are simple + * buttons that do anything you specify in the onClick function. + */ + additionalActions?: { + className?: string; + disabled?: boolean; + onClick: () => void; + title: string | JSX.Element; + }[]; + /** Provide HTML class(es) to the preview element, for custom styling. */ + className?: string; + /** + * Specify how the canvas should layout the story. + * + * @see https://storybook.js.org/docs/api/doc-blocks/doc-block-canvas#layout + */ + layout?: 'centered' | 'fullscreen' | 'padded'; + /** Specifies which story is rendered */ + of: ModuleExport; + /** Show story source code */ + sourceState?: 'hidden' | 'shown'; + /** + * Story configuration + * + * @see https://storybook.js.org/docs/api/doc-blocks/doc-block-canvas#story + */ + story?: StoryBlockParameters; + /** Disable story source code */ + withSource?: 'open' | 'closed' | 'none'; + /** Whether to render a toolbar containing tools to interact with the story. */ + withToolbar?: 'open' | 'closed' | 'none'; +}; + +type DescriptionBlockParameters = { + /** Component description */ + component?: string; + /** Story description */ + story?: string; +}; + +type SourceBlockParameters = { + /** The source code to be rendered. Will be inferred if not passed */ + code?: string; + /** Whether to render the code in dark mode */ + dark?: boolean; + /** Determines if decorators are rendered in the source code snippet. */ + excludeDecorators?: boolean; + /** + * The formatting used on source code. Both true and 'dedent' have the same effect of removing any + * extraneous indentation. Supports all valid prettier parser names. + * + * @see https://storybook.js.org/docs/api/doc-blocks/doc-block-source#format + */ + format?: boolean | 'dedent' | string; + // TODO: We could try to extract types from 'SupportedLanguages' in SyntaxHighlihter, but for now we inline them + /** Source code language */ + language?: + | 'bash' + | 'css' + | 'graphql' + | 'html' + | 'json' + | 'jsextra' + | 'jsx' + | 'md' + | 'text' + | 'tsx' + | 'typescript' + | 'yml'; + /** + * Specifies which story is rendered by the Source block. If no of is defined and the MDX file is + * attached, the primary (first) story will be rendered. + */ + of: ModuleExport; + /** Source code transformations */ + transform?: (code: string, storyContext: any) => string; + /** + * Specifies how the source code is rendered. + * + * @default 'auto' + * @see https://storybook.js.org/docs/api/doc-blocks/doc-block-source#type + */ + type?: 'auto' | 'code' | 'dynamic'; +}; + +export interface DocsParameters { + /** + * Docs configuration + * + * @see https://storybook.js.org/docs/writing-docs + */ + docs?: { + /** + * The subtitle displayed when shown in docs page + * + * @see https://storybook.js.org/docs/api/doc-blocks/doc-block-argtypes + */ + argTypes?: ArgTypesBlockParameters; + + /** + * Canvas configuration when shown in docs page + * + * @see https://storybook.js.org/docs/api/doc-blocks/doc-block-canvas + */ + canvas?: CanvasBlockParameters; + + /** + * Controls block configuration + * + * @see https://storybook.js.org/docs/api/doc-blocks/doc-block-controls + */ + controls?: ControlsBlockParameters; + + /** + * Component/story description when shown in docs page + * + * @see https://storybook.js.org/docs/api/doc-blocks/doc-block-description#writing-descriptions + */ + description?: DescriptionBlockParameters; + + /** Remove the addon panel and disable the addon's behavior */ + disable?: boolean; + + /** + * Replace the default documentation template used by Storybook with your own + * + * @see https://storybook.js.org/docs/writing-docs/autodocs#write-a-custom-template + */ + page?: unknown; + + /** + * Source code configuration when shown in docs page + * + * @see https://storybook.js.org/docs/api/doc-blocks/doc-block-source + */ + source?: SourceBlockParameters; + + /** + * Story configuration + * + * @see https://storybook.js.org/docs/api/doc-blocks/doc-block-story + */ + story?: StoryBlockParameters; + + /** + * The subtitle displayed when shown in docs page + * + * @see https://storybook.js.org/docs/api/doc-blocks/doc-block-subtitle + */ + subtitle?: string; + + /** + * The title displayed when shown in docs page + * + * @see https://storybook.js.org/docs/api/doc-blocks/doc-block-title + */ + title?: string; + }; +} diff --git a/code/addons/docs/template/stories/docs2/resolved-react.stories.ts b/code/addons/docs/template/stories/docs2/resolved-react.stories.ts index 00fed804cebb..75e3d480ca98 100644 --- a/code/addons/docs/template/stories/docs2/resolved-react.stories.ts +++ b/code/addons/docs/template/stories/docs2/resolved-react.stories.ts @@ -62,15 +62,15 @@ export const Story = { const actualReactDomVersion = (await canvas.findByTestId('react-dom')).textContent; const actualReactDomServerVersion = (await canvas.findByTestId('react-dom-server')).textContent; - step('Expect React packages to all resolve to the same version', () => { + step('Expect React packages to all resolve to the same version', async () => { // react-dom has a bug in its production build, reporting version 18.2.0-next-9e3b772b8-20220608 even though version 18.2.0 is installed. - expect(actualReactDomVersion!.startsWith(actualReactVersion!)).toBeTruthy(); + await expect(actualReactDomVersion!.startsWith(actualReactVersion!)).toBeTruthy(); if (parameters.renderer === 'preact') { // the preact/compat alias doesn't have a version export in react-dom/server return; } - expect(actualReactDomServerVersion).toBe(actualReactVersion); + await expect(actualReactDomServerVersion).toBe(actualReactVersion); }); }, }; diff --git a/code/addons/essentials/package.json b/code/addons/essentials/package.json index c39d825978c7..79943768de39 100644 --- a/code/addons/essentials/package.json +++ b/code/addons/essentials/package.json @@ -27,6 +27,11 @@ "import": "./dist/index.mjs", "require": "./dist/index.js" }, + "./preview": { + "types": "./dist/preview.d.ts", + "import": "./dist/preview.mjs", + "require": "./dist/preview.js" + }, "./actions/preview": { "types": "./dist/actions/preview.d.ts", "import": "./dist/actions/preview.mjs", @@ -72,11 +77,22 @@ "import": "./dist/viewport/preview.mjs", "require": "./dist/viewport/preview.js" }, + "./preset": "./dist/preset.js", "./package.json": "./package.json" }, "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "dist/index.d.ts" + ], + "preview": [ + "dist/preview.d.ts" + ] + } + }, "files": [ "dist/**/*", "README.md", @@ -111,10 +127,13 @@ }, "bundler": { "nodeEntries": [ - "./src/index.ts", + "./src/preset.ts", "./src/docs/preset.ts", "./src/docs/mdx-react-shim.ts" ], + "exportEntries": [ + "./src/index.ts" + ], "entries": [ "./src/docs/manager.ts" ], @@ -129,6 +148,7 @@ "./src/viewport/manager.ts" ], "previewEntries": [ + "./src/preview.ts", "./src/actions/preview.ts", "./src/backgrounds/preview.ts", "./src/docs/preview.ts", diff --git a/code/addons/essentials/src/backgrounds/manager.ts b/code/addons/essentials/src/backgrounds/manager.ts index 9da6a432be39..930d5ee38181 100644 --- a/code/addons/essentials/src/backgrounds/manager.ts +++ b/code/addons/essentials/src/backgrounds/manager.ts @@ -1,2 +1 @@ -// @ts-expect-error (no types needed for this) export * from '@storybook/addon-backgrounds/manager'; diff --git a/code/addons/essentials/src/backgrounds/preview.ts b/code/addons/essentials/src/backgrounds/preview.ts index cf24112788f3..2d01bf61bb6a 100644 --- a/code/addons/essentials/src/backgrounds/preview.ts +++ b/code/addons/essentials/src/backgrounds/preview.ts @@ -1,2 +1 @@ -// @ts-expect-error (no types needed for this) export * from '@storybook/addon-backgrounds/preview'; diff --git a/code/addons/essentials/src/docs/manager.ts b/code/addons/essentials/src/docs/manager.ts index 6101f7d79261..9f14a38904c4 100644 --- a/code/addons/essentials/src/docs/manager.ts +++ b/code/addons/essentials/src/docs/manager.ts @@ -1,2 +1 @@ -// @ts-expect-error (no types needed for this) export * from '@storybook/addon-docs/manager'; diff --git a/code/addons/essentials/src/highlight/preview.ts b/code/addons/essentials/src/highlight/preview.ts index e124e7a1374a..c57b34aafd63 100644 --- a/code/addons/essentials/src/highlight/preview.ts +++ b/code/addons/essentials/src/highlight/preview.ts @@ -1,2 +1 @@ -// @ts-expect-error (no types needed for this) export * from '@storybook/addon-highlight/preview'; diff --git a/code/addons/essentials/src/index.ts b/code/addons/essentials/src/index.ts index a72554227ba2..3ccfb15a26aa 100644 --- a/code/addons/essentials/src/index.ts +++ b/code/addons/essentials/src/index.ts @@ -1,107 +1,5 @@ -import { isAbsolute, join } from 'node:path'; +import { definePreview } from 'storybook/internal/preview-api'; -import { serverRequire } from 'storybook/internal/common'; -import { logger } from 'storybook/internal/node-logger'; +import addonAnnotations from './preview'; -interface PresetOptions { - /** - * Allow to use @storybook/addon-actions - * - * @default true - * @see https://storybook.js.org/addons/@storybook/addon-actions - */ - actions?: boolean; - /** - * Allow to use @storybook/addon-backgrounds - * - * @default true - * @see https://storybook.js.org/addons/@storybook/addon-backgrounds - */ - backgrounds?: boolean; - configDir: string; - /** - * Allow to use @storybook/addon-controls - * - * @default true - * @see https://storybook.js.org/addons/@storybook/addon-controls - */ - controls?: boolean; - /** - * Allow to use @storybook/addon-docs - * - * @default true - * @see https://storybook.js.org/addons/@storybook/addon-docs - */ - docs?: boolean; - /** - * Allow to use @storybook/addon-measure - * - * @default true - * @see https://storybook.js.org/addons/@storybook/addon-measure - */ - measure?: boolean; - /** - * Allow to use @storybook/addon-outline - * - * @default true - * @see https://storybook.js.org/addons/@storybook/addon-outline - */ - outline?: boolean; - themes?: boolean; - /** - * Allow to use @storybook/addon-toolbars - * - * @default true - * @see https://storybook.js.org/addons/@storybook/addon-toolbars - */ - toolbars?: boolean; - /** - * Allow to use @storybook/addon-viewport - * - * @default true - * @see https://storybook.js.org/addons/@storybook/addon-viewport - */ - viewport?: boolean; -} - -const requireMain = (configDir: string) => { - const absoluteConfigDir = isAbsolute(configDir) ? configDir : join(process.cwd(), configDir); - const mainFile = join(absoluteConfigDir, 'main'); - - return serverRequire(mainFile) ?? {}; -}; - -export function addons(options: PresetOptions) { - const checkInstalled = (addonName: string, main: any) => { - const addon = `@storybook/addon-${addonName}`; - const existingAddon = main.addons?.find((entry: string | { name: string }) => { - const name = typeof entry === 'string' ? entry : entry.name; - return name?.startsWith(addon); - }); - if (existingAddon) { - logger.info(`Found existing addon ${JSON.stringify(existingAddon)}, skipping.`); - } - return !!existingAddon; - }; - - const main = requireMain(options.configDir); - - // NOTE: The order of these addons is important. - return [ - 'controls', - 'actions', - 'docs', - 'backgrounds', - 'viewport', - 'toolbars', - 'measure', - 'outline', - 'highlight', - ] - .filter((key) => (options as any)[key] !== false) - .filter((addon) => !checkInstalled(addon, main)) - .map((addon) => { - // We point to the re-export from addon-essentials to support yarn pnp and pnpm. - return `@storybook/addon-essentials/${addon}`; - }); -} +export default () => definePreview(addonAnnotations); diff --git a/code/addons/essentials/src/outline/manager.ts b/code/addons/essentials/src/outline/manager.ts index d3a29db6d98b..9f46ef8cbae4 100644 --- a/code/addons/essentials/src/outline/manager.ts +++ b/code/addons/essentials/src/outline/manager.ts @@ -1,2 +1 @@ -// @ts-expect-error (no types needed for this) export * from '@storybook/addon-outline/manager'; diff --git a/code/addons/essentials/src/outline/preview.ts b/code/addons/essentials/src/outline/preview.ts index 3fe09381fe8f..16cc2faa0397 100644 --- a/code/addons/essentials/src/outline/preview.ts +++ b/code/addons/essentials/src/outline/preview.ts @@ -1,2 +1 @@ -// @ts-expect-error (no types needed for this) export * from '@storybook/addon-outline/preview'; diff --git a/code/addons/essentials/src/preset.ts b/code/addons/essentials/src/preset.ts new file mode 100644 index 000000000000..a72554227ba2 --- /dev/null +++ b/code/addons/essentials/src/preset.ts @@ -0,0 +1,107 @@ +import { isAbsolute, join } from 'node:path'; + +import { serverRequire } from 'storybook/internal/common'; +import { logger } from 'storybook/internal/node-logger'; + +interface PresetOptions { + /** + * Allow to use @storybook/addon-actions + * + * @default true + * @see https://storybook.js.org/addons/@storybook/addon-actions + */ + actions?: boolean; + /** + * Allow to use @storybook/addon-backgrounds + * + * @default true + * @see https://storybook.js.org/addons/@storybook/addon-backgrounds + */ + backgrounds?: boolean; + configDir: string; + /** + * Allow to use @storybook/addon-controls + * + * @default true + * @see https://storybook.js.org/addons/@storybook/addon-controls + */ + controls?: boolean; + /** + * Allow to use @storybook/addon-docs + * + * @default true + * @see https://storybook.js.org/addons/@storybook/addon-docs + */ + docs?: boolean; + /** + * Allow to use @storybook/addon-measure + * + * @default true + * @see https://storybook.js.org/addons/@storybook/addon-measure + */ + measure?: boolean; + /** + * Allow to use @storybook/addon-outline + * + * @default true + * @see https://storybook.js.org/addons/@storybook/addon-outline + */ + outline?: boolean; + themes?: boolean; + /** + * Allow to use @storybook/addon-toolbars + * + * @default true + * @see https://storybook.js.org/addons/@storybook/addon-toolbars + */ + toolbars?: boolean; + /** + * Allow to use @storybook/addon-viewport + * + * @default true + * @see https://storybook.js.org/addons/@storybook/addon-viewport + */ + viewport?: boolean; +} + +const requireMain = (configDir: string) => { + const absoluteConfigDir = isAbsolute(configDir) ? configDir : join(process.cwd(), configDir); + const mainFile = join(absoluteConfigDir, 'main'); + + return serverRequire(mainFile) ?? {}; +}; + +export function addons(options: PresetOptions) { + const checkInstalled = (addonName: string, main: any) => { + const addon = `@storybook/addon-${addonName}`; + const existingAddon = main.addons?.find((entry: string | { name: string }) => { + const name = typeof entry === 'string' ? entry : entry.name; + return name?.startsWith(addon); + }); + if (existingAddon) { + logger.info(`Found existing addon ${JSON.stringify(existingAddon)}, skipping.`); + } + return !!existingAddon; + }; + + const main = requireMain(options.configDir); + + // NOTE: The order of these addons is important. + return [ + 'controls', + 'actions', + 'docs', + 'backgrounds', + 'viewport', + 'toolbars', + 'measure', + 'outline', + 'highlight', + ] + .filter((key) => (options as any)[key] !== false) + .filter((addon) => !checkInstalled(addon, main)) + .map((addon) => { + // We point to the re-export from addon-essentials to support yarn pnp and pnpm. + return `@storybook/addon-essentials/${addon}`; + }); +} diff --git a/code/addons/essentials/src/preview.ts b/code/addons/essentials/src/preview.ts new file mode 100644 index 000000000000..1f624f279c53 --- /dev/null +++ b/code/addons/essentials/src/preview.ts @@ -0,0 +1,22 @@ +import { composeConfigs } from 'storybook/internal/preview-api'; + +import actionsAddon from '@storybook/addon-actions'; +import backgroundsAddon from '@storybook/addon-backgrounds'; +// We can't use docs as function yet because of the --test flag. Once we figure out disabling docs properly in CSF4, we can change this +// eslint-disable-next-line import/namespace +import * as docsAddon from '@storybook/addon-docs/preview'; +import highlightAddon from '@storybook/addon-highlight'; +import measureAddon from '@storybook/addon-measure'; +import outlineAddon from '@storybook/addon-outline'; +import viewportAddon from '@storybook/addon-viewport'; + +export default composeConfigs([ + actionsAddon(), + // TODO: we can't use this as function because of the --test flag + docsAddon, + backgroundsAddon(), + viewportAddon(), + measureAddon(), + outlineAddon(), + highlightAddon(), +]); diff --git a/code/addons/essentials/src/types.ts b/code/addons/essentials/src/types.ts new file mode 100644 index 000000000000..23b33ce66750 --- /dev/null +++ b/code/addons/essentials/src/types.ts @@ -0,0 +1,16 @@ +import type { ActionsParameters } from '@storybook/addon-actions'; +import type { BackgroundsParameters } from '@storybook/addon-backgrounds'; +import type { DocsParameters } from '@storybook/addon-docs'; +import type { HighlightParameters } from '@storybook/addon-highlight'; +import type { MeasureParameters } from '@storybook/addon-measure'; +import type { OutlineParameters } from '@storybook/addon-outline'; +import type { ViewportParameters } from '@storybook/addon-viewport'; + +export interface EssentialsParameters + extends ActionsParameters, + BackgroundsParameters, + DocsParameters, + HighlightParameters, + MeasureParameters, + OutlineParameters, + ViewportParameters {} diff --git a/code/addons/essentials/src/viewport/manager.ts b/code/addons/essentials/src/viewport/manager.ts index 48bc7a850de6..ccbe283d4101 100644 --- a/code/addons/essentials/src/viewport/manager.ts +++ b/code/addons/essentials/src/viewport/manager.ts @@ -1,2 +1 @@ -// @ts-expect-error (no types needed for this) export * from '@storybook/addon-viewport/manager'; diff --git a/code/addons/essentials/src/viewport/preview.ts b/code/addons/essentials/src/viewport/preview.ts index 34ee7de45614..b039b3bfa870 100644 --- a/code/addons/essentials/src/viewport/preview.ts +++ b/code/addons/essentials/src/viewport/preview.ts @@ -1,2 +1 @@ -// @ts-expect-error (no types needed for this) export * from '@storybook/addon-viewport/preview'; diff --git a/code/addons/highlight/package.json b/code/addons/highlight/package.json index 3d30774c84c7..1ec3158e45ae 100644 --- a/code/addons/highlight/package.json +++ b/code/addons/highlight/package.json @@ -39,6 +39,16 @@ "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "dist/index.d.ts" + ], + "preview": [ + "dist/preview.d.ts" + ] + } + }, "files": [ "dist/**/*", "README.md", diff --git a/code/addons/highlight/src/index.ts b/code/addons/highlight/src/index.ts index 6849e07e6184..16ab5dbd64a3 100644 --- a/code/addons/highlight/src/index.ts +++ b/code/addons/highlight/src/index.ts @@ -1,4 +1,8 @@ +import { definePreview } from 'storybook/internal/preview-api'; + +import './preview'; + export { HIGHLIGHT, RESET_HIGHLIGHT } from './constants'; +export type { HighlightParameters } from './types'; -// make it work with --isolatedModules -export default {}; +export default () => definePreview({}); diff --git a/code/addons/highlight/src/types.ts b/code/addons/highlight/src/types.ts new file mode 100644 index 000000000000..3613b23fb9b5 --- /dev/null +++ b/code/addons/highlight/src/types.ts @@ -0,0 +1,11 @@ +export interface HighlightParameters { + /** + * Highlight configuration + * + * @see https://storybook.js.org/docs/essentials/highlight#parameters + */ + highlight: { + /** Remove the addon panel and disable the addon's behavior */ + disable?: boolean; + }; +} diff --git a/code/addons/interactions/package.json b/code/addons/interactions/package.json index 53ad3a1b0edb..539322c3e363 100644 --- a/code/addons/interactions/package.json +++ b/code/addons/interactions/package.json @@ -40,6 +40,16 @@ "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "dist/index.d.ts" + ], + "preview": [ + "dist/preview.d.ts" + ] + } + }, "files": [ "dist/**/*", "README.md", diff --git a/code/addons/interactions/src/index.ts b/code/addons/interactions/src/index.ts index dafa948eda6c..0e536df78dad 100644 --- a/code/addons/interactions/src/index.ts +++ b/code/addons/interactions/src/index.ts @@ -1,2 +1,5 @@ -// make it work with --isolatedModules -export default {}; +import { definePreview } from 'storybook/internal/preview-api'; + +import * as addonAnnotations from './preview'; + +export default () => definePreview(addonAnnotations); diff --git a/code/addons/interactions/src/preview.ts b/code/addons/interactions/src/preview.ts index 482b6933279f..42c419390239 100644 --- a/code/addons/interactions/src/preview.ts +++ b/code/addons/interactions/src/preview.ts @@ -1,11 +1,13 @@ -import type { PlayFunction, StepLabel, StoryContext } from 'storybook/internal/types'; +import type { PlayFunction, StepLabel, StepRunner, StoryContext } from 'storybook/internal/types'; import { instrument } from '@storybook/instrumenter'; // This makes sure that storybook test loaders are always loaded when addon-interactions is used // For 9.0 we want to merge storybook/test and addon-interactions into one addon. import '@storybook/test'; -export const { step: runStep } = instrument( +import type { InteractionsParameters } from './types'; + +export const runStep = instrument( { // It seems like the label is unused, but the instrumenter has access to it // The context will be bounded later in StoryRender, so that the user can write just: @@ -15,8 +17,9 @@ export const { step: runStep } = instrument( step: (label: StepLabel, play: PlayFunction, context: StoryContext) => play(context), }, { intercept: true } -); + // perhaps csf types need to be updated? StepRunner expects Promise and not Promise | void +).step as StepRunner; -export const parameters = { +export const parameters: InteractionsParameters['test'] = { throwPlayFunctionExceptions: false, }; diff --git a/code/addons/interactions/src/types.ts b/code/addons/interactions/src/types.ts new file mode 100644 index 000000000000..9e21dc4e1521 --- /dev/null +++ b/code/addons/interactions/src/types.ts @@ -0,0 +1,14 @@ +export interface InteractionsParameters { + /** + * Interactions configuration + * + * @see https://storybook.js.org/docs/essentials/interactions + */ + test: { + /** Ignore unhandled errors during test execution */ + dangerouslyIgnoreUnhandledErrors?: boolean; + + /** Whether to throw exceptions coming from the play function */ + throwPlayFunctionExceptions?: boolean; + }; +} diff --git a/code/addons/jest/src/shared.ts b/code/addons/jest/src/shared.ts index 32107bdf23ef..24a705e39724 100644 --- a/code/addons/jest/src/shared.ts +++ b/code/addons/jest/src/shared.ts @@ -2,6 +2,8 @@ import type { StorybookInternalParameters } from 'storybook/internal/types'; import invariant from 'tiny-invariant'; +import type { JestParameters } from './types'; + // addons, panels and events get unique names using a prefix export const PARAM_KEY = 'test'; export const ADDON_ID = 'storybookjs/test'; @@ -9,11 +11,9 @@ export const PANEL_ID = `${ADDON_ID}/panel`; export const ADD_TESTS = `${ADDON_ID}/add_tests`; -interface AddonParameters extends StorybookInternalParameters { - jest?: string | string[] | { disabled: true }; -} - -export function defineJestParameter(parameters: AddonParameters): string[] | null { +export function defineJestParameter( + parameters: JestParameters & StorybookInternalParameters +): string[] | null { const { jest, fileName: filePath } = parameters; if (typeof jest === 'string') { diff --git a/code/addons/jest/src/types.ts b/code/addons/jest/src/types.ts new file mode 100644 index 000000000000..998c0254d83f --- /dev/null +++ b/code/addons/jest/src/types.ts @@ -0,0 +1,8 @@ +export interface JestParameters { + /** + * Jest configuration + * + * @see https://github.com/storybookjs/storybook/blob/next/code/addons/jest/README.md#usage + */ + jest?: string | string[] | { disabled: true }; +} diff --git a/code/addons/links/package.json b/code/addons/links/package.json index 1c37255fcb26..6fb7d1c4575a 100644 --- a/code/addons/links/package.json +++ b/code/addons/links/package.json @@ -48,6 +48,9 @@ "*": [ "dist/index.d.ts" ], + "preview": [ + "dist/preview.d.ts" + ], "react": [ "dist/react/index.d.ts" ] diff --git a/code/addons/links/src/index.ts b/code/addons/links/src/index.ts index 524558abc6c6..4bb40898a9ac 100644 --- a/code/addons/links/src/index.ts +++ b/code/addons/links/src/index.ts @@ -1 +1,7 @@ +import { definePreview } from 'storybook/internal/preview-api'; + +import * as addonAnnotations from './preview'; + export { linkTo, hrefTo, withLinks, navigate } from './utils'; + +export default () => definePreview(addonAnnotations); diff --git a/code/addons/links/src/preview.ts b/code/addons/links/src/preview.ts index 6270d133ab77..1527786e81af 100644 --- a/code/addons/links/src/preview.ts +++ b/code/addons/links/src/preview.ts @@ -1,5 +1,3 @@ -import type { Addon_DecoratorFunction } from 'storybook/internal/types'; - import { withLinks } from './index'; -export const decorators: Addon_DecoratorFunction[] = [withLinks]; +export const decorators = [withLinks]; diff --git a/code/addons/measure/src/index.ts b/code/addons/measure/src/index.ts index dafa948eda6c..40009898f0db 100644 --- a/code/addons/measure/src/index.ts +++ b/code/addons/measure/src/index.ts @@ -1,2 +1,7 @@ -// make it work with --isolatedModules -export default {}; +import { definePreview } from 'storybook/internal/preview-api'; + +import * as addonAnnotations from './preview'; + +export type { MeasureParameters } from './types'; + +export default () => definePreview(addonAnnotations); diff --git a/code/addons/measure/src/preview.tsx b/code/addons/measure/src/preview.tsx index 8898eb58dd6b..a97aefdff659 100644 --- a/code/addons/measure/src/preview.tsx +++ b/code/addons/measure/src/preview.tsx @@ -1,9 +1,7 @@ -import type { Addon_DecoratorFunction } from 'storybook/internal/types'; - import { PARAM_KEY } from './constants'; import { withMeasure } from './withMeasure'; -export const decorators: Addon_DecoratorFunction[] = [withMeasure]; +export const decorators = [withMeasure]; export const initialGlobals = { [PARAM_KEY]: false, diff --git a/code/addons/measure/src/types.ts b/code/addons/measure/src/types.ts new file mode 100644 index 000000000000..e51cf69775b5 --- /dev/null +++ b/code/addons/measure/src/types.ts @@ -0,0 +1,11 @@ +export interface MeasureParameters { + /** + * Measure configuration + * + * @see https://storybook.js.org/docs/essentials/measure-and-outline#parameters + */ + measure: { + /** Remove the addon panel and disable the addon's behavior */ + disable?: boolean; + }; +} diff --git a/code/addons/measure/src/withMeasure.ts b/code/addons/measure/src/withMeasure.ts index 8524a7f71fce..bc94d6ce2529 100644 --- a/code/addons/measure/src/withMeasure.ts +++ b/code/addons/measure/src/withMeasure.ts @@ -1,10 +1,6 @@ /* eslint-env browser */ import { useEffect } from 'storybook/internal/preview-api'; -import type { - Renderer, - StoryContext, - PartialStoryFn as StoryFunction, -} from 'storybook/internal/types'; +import type { DecoratorFunction } from 'storybook/internal/types'; import { destroy, init, rescale } from './box-model/canvas'; import { drawSelectedElement } from './box-model/visualizer'; @@ -18,7 +14,7 @@ function findAndDrawElement(x: number, y: number) { drawSelectedElement(nodeAtPointerRef); } -export const withMeasure = (StoryFn: StoryFunction, context: StoryContext) => { +export const withMeasure: DecoratorFunction = (StoryFn, context) => { const { measureEnabled } = context.globals; useEffect(() => { diff --git a/code/addons/outline/package.json b/code/addons/outline/package.json index da2ad1bddf14..7769eeaba99e 100644 --- a/code/addons/outline/package.json +++ b/code/addons/outline/package.json @@ -45,6 +45,16 @@ "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "dist/index.d.ts" + ], + "preview": [ + "dist/preview.d.ts" + ] + } + }, "files": [ "dist/**/*", "README.md", @@ -80,7 +90,7 @@ "./src/manager.tsx" ], "previewEntries": [ - "./src/preview.tsx" + "./src/preview.ts" ] }, "gitHead": "e6a7fd8a655c69780bc20b9749c2699e44beae16", diff --git a/code/addons/outline/src/index.ts b/code/addons/outline/src/index.ts index dafa948eda6c..459b096ff9b9 100644 --- a/code/addons/outline/src/index.ts +++ b/code/addons/outline/src/index.ts @@ -1,2 +1,7 @@ -// make it work with --isolatedModules -export default {}; +import { definePreview } from 'storybook/internal/preview-api'; + +import * as addonAnnotations from './preview'; + +export type { OutlineParameters } from './types'; + +export default () => definePreview(addonAnnotations); diff --git a/code/addons/outline/src/preview.tsx b/code/addons/outline/src/preview.ts similarity index 50% rename from code/addons/outline/src/preview.tsx rename to code/addons/outline/src/preview.ts index 19deb3a9afe0..a1c337467c8b 100644 --- a/code/addons/outline/src/preview.tsx +++ b/code/addons/outline/src/preview.ts @@ -1,9 +1,7 @@ -import type { Addon_DecoratorFunction } from 'storybook/internal/types'; - import { PARAM_KEY } from './constants'; import { withOutline } from './withOutline'; -export const decorators: Addon_DecoratorFunction[] = [withOutline]; +export const decorators = [withOutline]; export const initialGlobals = { [PARAM_KEY]: false, diff --git a/code/addons/outline/src/types.ts b/code/addons/outline/src/types.ts new file mode 100644 index 000000000000..b5b3d4b3d663 --- /dev/null +++ b/code/addons/outline/src/types.ts @@ -0,0 +1,11 @@ +export interface OutlineParameters { + /** + * Outline configuration + * + * @see https://storybook.js.org/docs/essentials/measure-and-outline#parameters + */ + outline: { + /** Remove the addon panel and disable the addon's behavior */ + disable?: boolean; + }; +} diff --git a/code/addons/outline/src/withOutline.ts b/code/addons/outline/src/withOutline.ts index 219b93a3acd2..0bc29c106f87 100644 --- a/code/addons/outline/src/withOutline.ts +++ b/code/addons/outline/src/withOutline.ts @@ -1,15 +1,11 @@ import { useEffect, useMemo } from 'storybook/internal/preview-api'; -import type { - Renderer, - StoryContext, - PartialStoryFn as StoryFunction, -} from 'storybook/internal/types'; +import type { DecoratorFunction } from 'storybook/internal/types'; import { PARAM_KEY } from './constants'; import { addOutlineStyles, clearStyles } from './helpers'; import outlineCSS from './outlineCSS'; -export const withOutline = (StoryFn: StoryFunction, context: StoryContext) => { +export const withOutline: DecoratorFunction = (StoryFn, context) => { const { globals } = context; const isActive = [true, 'true'].includes(globals[PARAM_KEY]); const isInDocs = context.viewMode === 'docs'; diff --git a/code/addons/storysource/src/index.ts b/code/addons/storysource/src/index.ts index 55221ffd2535..3daf3fe3d64e 100644 --- a/code/addons/storysource/src/index.ts +++ b/code/addons/storysource/src/index.ts @@ -1,3 +1,4 @@ import { ADDON_ID, PANEL_ID } from './events'; export { ADDON_ID, PANEL_ID }; +export type { StorySourceParameters } from './types'; diff --git a/code/addons/storysource/src/types.ts b/code/addons/storysource/src/types.ts new file mode 100644 index 000000000000..1a350264c6d1 --- /dev/null +++ b/code/addons/storysource/src/types.ts @@ -0,0 +1,38 @@ +export interface StorySourceParameters { + /** + * Storysource addon configuration + * + * @see https://github.com/storybookjs/storybook/tree/next/code/addons/storysource + */ + storySource?: { + /** Dark mode for source code */ + dark?: boolean; + + /** Remove the addon panel and disable the addon's behavior */ + disable?: boolean; + + /** Source code formatting options */ + format?: 'jsx' | 'typescript' | 'javascript'; + + /** Source code language */ + language?: string; + + /** Source code loader options */ + loaderOptions?: { + /** Ignore specific patterns */ + ignore?: string[]; + /** Include specific patterns */ + include?: string[]; + /** Parser options */ + parser?: string; + /** Pretty print source code */ + prettierConfig?: object; + }; + + /** Show story source code */ + showCode?: boolean; + + /** Source code transformations */ + transformSource?: (source: string, storyContext: any) => string; + }; +} diff --git a/code/addons/test/package.json b/code/addons/test/package.json index d67203ca92a8..788662e8e70a 100644 --- a/code/addons/test/package.json +++ b/code/addons/test/package.json @@ -66,6 +66,16 @@ "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "dist/index.d.ts" + ], + "preview": [ + "dist/preview.d.ts" + ] + } + }, "files": [ "dist/**/*", "templates/**/*", diff --git a/code/addons/test/src/index.ts b/code/addons/test/src/index.ts index 883e969b4088..001c02864cb0 100644 --- a/code/addons/test/src/index.ts +++ b/code/addons/test/src/index.ts @@ -1,7 +1,11 @@ +import { definePreview } from 'storybook/internal/preview-api'; + +import * as addonAnnotations from './preview'; import type { storybookTest as storybookTestImport } from './vitest-plugin'; -// make it work with --isolatedModules -export default {}; +export default () => definePreview(addonAnnotations); + +export type { TestParameters } from './types'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-error - this is a hack to make the module's sub-path augmentable diff --git a/code/addons/test/src/types.ts b/code/addons/test/src/types.ts new file mode 100644 index 000000000000..1e36b89e08ff --- /dev/null +++ b/code/addons/test/src/types.ts @@ -0,0 +1,14 @@ +export interface TestParameters { + /** + * Test addon configuration + * + * @see https://storybook.js.org/docs/writing-tests/test-addon + */ + test: { + /** Ignore unhandled errors during test execution */ + dangerouslyIgnoreUnhandledErrors?: boolean; + + /** Whether to throw exceptions coming from the play function */ + throwPlayFunctionExceptions?: boolean; + }; +} diff --git a/code/addons/test/src/vitest-plugin/test-utils.ts b/code/addons/test/src/vitest-plugin/test-utils.ts index a776637216a7..f8259d4445fb 100644 --- a/code/addons/test/src/vitest-plugin/test-utils.ts +++ b/code/addons/test/src/vitest-plugin/test-utils.ts @@ -3,7 +3,11 @@ /* eslint-disable no-underscore-dangle */ import { type RunnerTask, type TaskMeta, type TestContext } from 'vitest'; -import { type Report, composeStory } from 'storybook/internal/preview-api'; +import { + type Report, + composeStory, + getCsfFactoryAnnotations, +} from 'storybook/internal/preview-api'; import type { ComponentAnnotations, ComposedStoryFn } from 'storybook/internal/types'; import { server } from '@vitest/browser/context'; @@ -26,13 +30,15 @@ export const testStory = ( skipTags: string[] ) => { return async (context: TestContext & { story: ComposedStoryFn }) => { + const annotations = getCsfFactoryAnnotations(story, meta); const composedStory = composeStory( - story, - meta, + annotations.story, + annotations.meta!, { initialGlobals: (await getInitialGlobals?.()) ?? {}, tags: await getTags?.() }, - undefined, + annotations.preview, exportName ); + if (composedStory === undefined || skipTags?.some((tag) => composedStory.tags.includes(tag))) { context.skip(); } diff --git a/code/addons/themes/package.json b/code/addons/themes/package.json index ff16998228ef..558a6f95d38f 100644 --- a/code/addons/themes/package.json +++ b/code/addons/themes/package.json @@ -44,6 +44,16 @@ "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "dist/index.d.ts" + ], + "preview": [ + "dist/preview.d.ts" + ] + } + }, "files": [ "dist/**/*", "README.md", diff --git a/code/addons/themes/src/constants.ts b/code/addons/themes/src/constants.ts index 654cc57ea893..b1f84fea67b6 100644 --- a/code/addons/themes/src/constants.ts +++ b/code/addons/themes/src/constants.ts @@ -1,24 +1,16 @@ +import type { ThemeAddonState, ThemesParameters } from './types'; + export const PARAM_KEY = 'themes' as const; export const ADDON_ID = `storybook/${PARAM_KEY}` as const; export const GLOBAL_KEY = 'theme' as const; export const THEME_SWITCHER_ID = `${ADDON_ID}/theme-switcher` as const; -export interface ThemeAddonState { - themesList: string[]; - themeDefault?: string; -} - export const DEFAULT_ADDON_STATE: ThemeAddonState = { themesList: [], themeDefault: undefined, }; -export interface ThemeParameters { - themeOverride?: string; - disable?: boolean; -} - -export const DEFAULT_THEME_PARAMETERS: ThemeParameters = {}; +export const DEFAULT_THEME_PARAMETERS: ThemesParameters['themes'] = {}; export const THEMING_EVENTS = { REGISTER_THEMES: `${ADDON_ID}/REGISTER_THEMES`, diff --git a/code/addons/themes/src/decorators/helpers.ts b/code/addons/themes/src/decorators/helpers.ts index 97c70dd1f0a4..b3c6a6eff1ed 100644 --- a/code/addons/themes/src/decorators/helpers.ts +++ b/code/addons/themes/src/decorators/helpers.ts @@ -4,8 +4,10 @@ import type { StoryContext } from 'storybook/internal/types'; import dedent from 'ts-dedent'; -import type { ThemeParameters } from '../constants'; import { DEFAULT_THEME_PARAMETERS, GLOBAL_KEY, PARAM_KEY, THEMING_EVENTS } from '../constants'; +import type { ThemesParameters as Parameters } from '../types'; + +type ThemesParameters = Parameters['themes']; /** * @param StoryContext @@ -15,7 +17,7 @@ export function pluckThemeFromContext({ globals }: StoryContext): string { return globals[GLOBAL_KEY] || ''; } -export function useThemeParameters(context?: StoryContext): ThemeParameters { +export function useThemeParameters(context?: StoryContext): ThemesParameters { deprecate( dedent`The useThemeParameters function is deprecated. Please access parameters via the context directly instead e.g. - const { themeOverride } = context.parameters.themes ?? {}; @@ -23,7 +25,7 @@ export function useThemeParameters(context?: StoryContext): ThemeParameters { ); if (!context) { - return useParameter(PARAM_KEY, DEFAULT_THEME_PARAMETERS) as ThemeParameters; + return useParameter(PARAM_KEY, DEFAULT_THEME_PARAMETERS) as ThemesParameters; } return context.parameters[PARAM_KEY] ?? DEFAULT_THEME_PARAMETERS; diff --git a/code/addons/themes/src/index.ts b/code/addons/themes/src/index.ts index b89aa12deba9..44aac9406bae 100644 --- a/code/addons/themes/src/index.ts +++ b/code/addons/themes/src/index.ts @@ -1,3 +1,9 @@ -// make it work with --isolatedModules -export default {}; +import { definePreview } from 'storybook/internal/preview-api'; + +import * as addonAnnotations from './preview'; + +export type { ThemesGlobals, ThemesParameters } from './types'; + +export default () => definePreview(addonAnnotations); + export * from './decorators'; diff --git a/code/addons/themes/src/theme-switcher.tsx b/code/addons/themes/src/theme-switcher.tsx index d8e24f4afc08..9ca44ba016be 100644 --- a/code/addons/themes/src/theme-switcher.tsx +++ b/code/addons/themes/src/theme-switcher.tsx @@ -13,7 +13,6 @@ import { styled } from 'storybook/internal/theming'; import { PaintBrushIcon } from '@storybook/icons'; -import type { ThemeAddonState, ThemeParameters } from './constants'; import { DEFAULT_ADDON_STATE, DEFAULT_THEME_PARAMETERS, @@ -22,6 +21,9 @@ import { THEME_SWITCHER_ID, THEMING_EVENTS, } from './constants'; +import type { ThemesParameters as Parameters, ThemeAddonState } from './types'; + +type ThemesParameters = Parameters['themes']; const IconButtonLabel = styled.div(({ theme }) => ({ fontSize: theme.typography.size.s2 - 1, @@ -31,10 +33,10 @@ const hasMultipleThemes = (themesList: ThemeAddonState['themesList']) => themesL const hasTwoThemes = (themesList: ThemeAddonState['themesList']) => themesList.length === 2; export const ThemeSwitcher = React.memo(function ThemeSwitcher() { - const { themeOverride, disable } = useParameter( + const { themeOverride, disable } = useParameter( PARAM_KEY, DEFAULT_THEME_PARAMETERS - ) as ThemeParameters; + ) as ThemesParameters; const [{ theme: selected }, updateGlobals, storyGlobals] = useGlobals(); const channel = addons.getChannel(); diff --git a/code/addons/themes/src/types.ts b/code/addons/themes/src/types.ts new file mode 100644 index 000000000000..3a825e37983e --- /dev/null +++ b/code/addons/themes/src/types.ts @@ -0,0 +1,23 @@ +export interface ThemeAddonState { + themesList: string[]; + themeDefault?: string; +} + +export interface ThemesParameters { + /** + * Themes configuration + * + * @see https://github.com/storybookjs/storybook/blob/next/code/addons/themes/README.md + */ + themes: { + /** Remove the addon panel and disable the addon's behavior */ + disable?: boolean; + /** Which theme to override for the story */ + themeOverride?: string; + }; +} + +export interface ThemesGlobals { + /** Which theme to override for the story */ + theme?: string; +} diff --git a/code/addons/viewport/package.json b/code/addons/viewport/package.json index b61a803818ea..57f813e08135 100644 --- a/code/addons/viewport/package.json +++ b/code/addons/viewport/package.json @@ -39,6 +39,16 @@ "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "dist/index.d.ts" + ], + "preview": [ + "dist/preview.d.ts" + ] + } + }, "files": [ "dist/**/*", "README.md", diff --git a/code/addons/viewport/src/index.ts b/code/addons/viewport/src/index.ts index fac593a2f2dd..3deffc965b6d 100644 --- a/code/addons/viewport/src/index.ts +++ b/code/addons/viewport/src/index.ts @@ -1,2 +1,8 @@ +import { definePreview } from 'storybook/internal/preview-api'; + +import * as addonAnnotations from './preview'; + export * from './defaults'; export type * from './types'; + +export default () => definePreview(addonAnnotations); diff --git a/code/addons/viewport/src/preview.ts b/code/addons/viewport/src/preview.ts index 9c8ddc9cfb0e..448909996c78 100644 --- a/code/addons/viewport/src/preview.ts +++ b/code/addons/viewport/src/preview.ts @@ -1,5 +1,4 @@ import { PARAM_KEY as KEY } from './constants'; -import { MINIMAL_VIEWPORTS } from './defaults'; import type { GlobalState } from './types'; const modern: Record = { @@ -9,4 +8,4 @@ const modern: Record = { // TODO: remove in 9.0 const legacy = { viewport: 'reset', viewportRotated: false }; -export const initialGlobals = FEATURES?.viewportStoryGlobals ? modern : legacy; +export const initialGlobals = globalThis.FEATURES?.viewportStoryGlobals ? modern : legacy; diff --git a/code/addons/viewport/src/types.ts b/code/addons/viewport/src/types.ts index 33f186c2c3c1..0a99d209f642 100644 --- a/code/addons/viewport/src/types.ts +++ b/code/addons/viewport/src/types.ts @@ -24,5 +24,63 @@ export interface Config { disable: boolean; } -export type GlobalState = { value: string | undefined; isRotated: boolean }; +export type GlobalState = { + /** + * When set, the viewport is applied and cannot be changed using the toolbar. Must match the key + * of one of the available viewports. + */ + value: string | undefined; + + /** + * When true the viewport applied will be rotated 90°, e.g. it will rotate from portrait to + * landscape orientation. + */ + isRotated: boolean; +}; export type GlobalStateUpdate = Partial; + +export interface ViewportParameters { + /** + * Viewport configuration + * + * @see https://storybook.js.org/docs/essentials/viewport#parameters + */ + viewport: { + /** + * Specifies the default orientation used when viewing a story. Only available if you haven't + * enabled the globals API. + */ + defaultOrientation?: 'landscape' | 'portrait'; + + /** + * Specifies the default viewport used when viewing a story. Must match a key in the viewports + * (or options) object. + */ + defaultViewport?: string; + + /** + * Remove the addon panel and disable the addon's behavior . If you wish to turn off this addon + * for the entire Storybook, you should do so when registering addon-essentials + * + * @see https://storybook.js.org/docs/essentials/index#disabling-addons + */ + disabled?: boolean; + + /** + * Specify the available viewports. The width and height values must include the unit, e.g. + * '320px'. + */ + viewports?: Viewport; // TODO: use ModernViewport in 9.0 + }; +} + +export interface ViewportGlobals { + /** + * Viewport configuration + * + * @see https://storybook.js.org/docs/essentials/viewport#globals + */ + viewport: { + [key: string]: GlobalState; + }; +} diff --git a/code/builders/builder-vite/src/codegen-modern-iframe-script.ts b/code/builders/builder-vite/src/codegen-modern-iframe-script.ts index 10066686dd47..1801cb7bec31 100644 --- a/code/builders/builder-vite/src/codegen-modern-iframe-script.ts +++ b/code/builders/builder-vite/src/codegen-modern-iframe-script.ts @@ -14,7 +14,7 @@ export async function generateModernIframeScriptCode(options: Options, projectRo [], options ); - const previewAnnotationURLs = [...previewAnnotations, previewOrConfigFile] + const [previewFileUrl, ...previewAnnotationURLs] = [previewOrConfigFile, ...previewAnnotations] .filter(Boolean) .map((path) => processPreviewAnnotation(path, projectRoot)); @@ -23,6 +23,12 @@ export async function generateModernIframeScriptCode(options: Options, projectRo // modules are provided, the rest are null. We can just re-import everything again in that case. const getPreviewAnnotationsFunction = ` const getProjectAnnotations = async (hmrPreviewAnnotationModules = []) => { + const preview = await import('${previewFileUrl}'); + + if (isPreview(preview.default)) { + return preview.default.composed; + } + const configs = await Promise.all([${previewAnnotationURLs .map( (previewAnnotation, index) => @@ -30,7 +36,7 @@ export async function generateModernIframeScriptCode(options: Options, projectRo `hmrPreviewAnnotationModules[${index}] ?? import('${previewAnnotation}')` ) .join(',\n')}]) - return composeConfigs(configs); + return composeConfigs([...configs, preview]); }`; // eslint-disable-next-line @typescript-eslint/no-shadow @@ -73,6 +79,7 @@ export async function generateModernIframeScriptCode(options: Options, projectRo setup(); import { composeConfigs, PreviewWeb, ClientApi } from 'storybook/internal/preview-api'; + import { isPreview } from 'storybook/internal/csf'; import { importFn } from '${SB_VIRTUAL_FILES.VIRTUAL_STORIES_FILE}'; diff --git a/code/builders/builder-vite/src/optimizeDeps.ts b/code/builders/builder-vite/src/optimizeDeps.ts index 74dd0090be42..696d523086df 100644 --- a/code/builders/builder-vite/src/optimizeDeps.ts +++ b/code/builders/builder-vite/src/optimizeDeps.ts @@ -17,6 +17,7 @@ const INCLUDE_CANDIDATES = [ '@storybook/addon-backgrounds/preview', '@storybook/addon-designs/blocks', '@storybook/addon-docs/preview', + '@storybook/addon-essentials/preview', '@storybook/addon-essentials/actions/preview', '@storybook/addon-essentials/actions/preview', '@storybook/addon-essentials/backgrounds/preview', diff --git a/code/builders/builder-vite/src/vite-server.ts b/code/builders/builder-vite/src/vite-server.ts index e58f6c98028d..e50935d77758 100644 --- a/code/builders/builder-vite/src/vite-server.ts +++ b/code/builders/builder-vite/src/vite-server.ts @@ -31,9 +31,7 @@ export async function createViteServer(options: Options, devServer: Server) { const ipRegex = /^(?:\d{1,3}\.){3}\d{1,3}$|^(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}$/; - // @ts-expect-error (does not exist) config.server.allowedHosts = - // @ts-expect-error (does not exist) commonCfg.server?.allowedHosts ?? (options.host && !ipRegex.test(options.host) ? [options.host.toLowerCase()] : true); diff --git a/code/builders/builder-webpack5/templates/virtualModuleModernEntry.js b/code/builders/builder-webpack5/templates/virtualModuleModernEntry.js index 4ae082c43b2e..0ed2fe15fa85 100644 --- a/code/builders/builder-webpack5/templates/virtualModuleModernEntry.js +++ b/code/builders/builder-webpack5/templates/virtualModuleModernEntry.js @@ -1,11 +1,22 @@ import { createBrowserChannel } from 'storybook/internal/channels'; +import { isPreview } from 'storybook/internal/csf'; import { PreviewWeb, addons, composeConfigs } from 'storybook/internal/preview-api'; import { global } from '@storybook/global'; import { importFn } from '{{storiesFilename}}'; -const getProjectAnnotations = () => composeConfigs(['{{previewAnnotations_requires}}']); +const getProjectAnnotations = () => { + const previewAnnotations = ['{{previewAnnotations_requires}}']; + // the last one in this array is the user preview + const userPreview = previewAnnotations[previewAnnotations.length - 1]?.default; + + if (isPreview(userPreview)) { + return userPreview.composed; + } + + return composeConfigs(previewAnnotations); +}; const channel = createBrowserChannel({ page: 'preview' }); addons.setChannel(channel); diff --git a/code/core/src/common/index.ts b/code/core/src/common/index.ts index 6166e285ab05..6700cdf14fad 100644 --- a/code/core/src/common/index.ts +++ b/code/core/src/common/index.ts @@ -41,6 +41,8 @@ export * from './utils/strip-abs-node-modules-path'; export * from './utils/formatter'; export * from './utils/get-story-id'; export * from './utils/posix'; +export * from './utils/get-addon-names'; +export * from './utils/sync-main-preview-addons'; export * from './js-package-manager'; export { versions }; diff --git a/code/core/src/common/utils/get-addon-annotations.test.ts b/code/core/src/common/utils/get-addon-annotations.test.ts new file mode 100644 index 000000000000..b60f9f512947 --- /dev/null +++ b/code/core/src/common/utils/get-addon-annotations.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest'; + +import { getAnnotationsName } from './get-addon-annotations'; + +describe('getAnnotationsName', () => { + it('should handle @storybook namespace and camel case conversion', () => { + expect(getAnnotationsName('@storybook/addon-essentials')).toBe('addonEssentials'); + }); + + it('should handle other namespaces and camel case conversion', () => { + expect(getAnnotationsName('@kudos-components/testing/module')).toBe( + 'kudosComponentsTestingModule' + ); + }); + + it('should handle strings without namespaces', () => { + expect(getAnnotationsName('plain-text/example')).toBe('plainTextExample'); + }); + + it('should handle strings with multiple special characters', () => { + expect(getAnnotationsName('@storybook/multi-part/example-test')).toBe('multiPartExampleTest'); + }); +}); diff --git a/code/core/src/common/utils/get-addon-annotations.ts b/code/core/src/common/utils/get-addon-annotations.ts new file mode 100644 index 000000000000..eb99bdcd110d --- /dev/null +++ b/code/core/src/common/utils/get-addon-annotations.ts @@ -0,0 +1,46 @@ +import path from 'node:path'; + +import { isCorePackage } from './cli'; + +/** + * Get the name of the annotations object for a given addon. + * + * Input: '@storybook/addon-essentials' + * + * Output: 'addonEssentialsAnnotations' + */ +export function getAnnotationsName(addonName: string): string { + // remove @storybook namespace, split by special characters, convert to camelCase + const cleanedUpName = addonName + .replace(/^@storybook\//, '') + .split(/[^a-zA-Z0-9]+/) + .map((word, index) => + index === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() + ) + .join('') + .replace(/^./, (char) => char.toLowerCase()); + + return cleanedUpName; +} + +export async function getAddonAnnotations(addon: string) { + try { + const data = { + // core addons will have a function as default export in index entrypoint + importPath: addon, + importName: getAnnotationsName(addon), + isCoreAddon: isCorePackage(addon), + }; + + // for backwards compatibility, if it's not a core addon we use /preview entrypoint + if (!data.isCoreAddon) { + data.importPath = `@storybook/${addon}/preview`; + } + + require.resolve(path.join(addon, 'preview')); + + return data; + } catch (err) {} + + return null; +} diff --git a/code/core/src/common/utils/get-addon-names.test.ts b/code/core/src/common/utils/get-addon-names.test.ts new file mode 100644 index 000000000000..c7769063afe3 --- /dev/null +++ b/code/core/src/common/utils/get-addon-names.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from 'vitest'; + +import { getAddonNames } from './get-addon-names'; + +describe('getAddonNames', () => { + it('should extract addon names from simple strings', () => { + const config = { + stories: [], + addons: ['@storybook/addon-actions', '@storybook/addon-outline'], + }; + const result = getAddonNames(config); + expect(result).toEqual(['@storybook/addon-actions', '@storybook/addon-outline']); + }); + + it('should extract addon names from object notation', () => { + const config = { + stories: [], + addons: [{ name: '@storybook/addon-actions' }, { name: '@storybook/addon-outline' }], + }; + const result = getAddonNames(config); + expect(result).toEqual(['@storybook/addon-actions', '@storybook/addon-outline']); + }); + + it('should filter out relative paths for local addons', () => { + const config = { + stories: [], + addons: ['./local-addon', { name: './another-local-addon' }], + }; + const result = getAddonNames(config); + expect(result).toEqual([]); + }); + + it('should extract addon names from absolute paths', () => { + const config = { + stories: [], + addons: [ + '/sandbox/react-vite-default-ts/node_modules/@storybook/addon-actions', + '/sandbox/react-vite-default-ts/node_modules/@storybook/addon-outline', + ], + }; + const result = getAddonNames(config); + expect(result).toEqual(['@storybook/addon-actions', '@storybook/addon-outline']); + }); + + it('should extract addon names from pnpm paths', () => { + const config = { + stories: [], + addons: [ + '/Users/xxx/node_modules/.pnpm/@storybook+addon-essentials@8.5.0-beta.5_@types+react@18.2.33_storybook@8.5.0-beta.5_prettier@3.2.5_/node_modules/@storybook/addon-essentials', + ], + }; + const result = getAddonNames(config); + expect(result).toEqual(['@storybook/addon-essentials']); + }); + + it('should extract addon names from yarn pnp paths', () => { + const config = { + stories: [], + addons: [ + '/Users/xxx/.yarn/__virtual__/@storybook-addon-essentials-virtual-5c3b9b3005/3/.yarn/berry/cache/@storybook-addon-essentials-npm-8.5.0-bbaf03c190-10c0.zip/node_modules/@storybook/addon-essentials', + ], + }; + const result = getAddonNames(config); + expect(result).toEqual(['@storybook/addon-essentials']); + }); + + it('should handle mixed addon configurations', () => { + const config = { + stories: [], + addons: [ + '@storybook/addon-actions', + { name: '@storybook/addon-outline' }, + './local-addon', + '/sandbox/react-vite-default-ts/node_modules/@storybook/addon-controls', + ], + }; + const result = getAddonNames(config); + expect(result).toEqual([ + '@storybook/addon-actions', + '@storybook/addon-outline', + '@storybook/addon-controls', + ]); + }); +}); diff --git a/code/core/src/common/utils/get-addon-names.ts b/code/core/src/common/utils/get-addon-names.ts new file mode 100644 index 000000000000..d3081a729438 --- /dev/null +++ b/code/core/src/common/utils/get-addon-names.ts @@ -0,0 +1,31 @@ +import type { StorybookConfig } from '@storybook/types'; + +export const getAddonNames = (mainConfig: StorybookConfig): string[] => { + const addons = mainConfig.addons || []; + const addonList = addons.map((addon) => { + let name = ''; + if (typeof addon === 'string') { + name = addon; + } else if (typeof addon === 'object') { + name = addon.name; + } + + if (name.startsWith('.')) { + return undefined; + } + + // For absolute paths, pnpm and yarn pnp, + // Remove everything before and including "node_modules/" + name = name.replace(/.*node_modules\//, ''); + + // Further clean up package names + return name + .replace(/\/dist\/.*$/, '') + .replace(/\.[mc]?[tj]?sx?$/, '') + .replace(/\/register$/, '') + .replace(/\/manager$/, '') + .replace(/\/preset$/, ''); + }); + + return addonList.filter((item): item is NonNullable => item != null); +}; diff --git a/code/core/src/common/utils/sync-main-preview-addons.test.ts b/code/core/src/common/utils/sync-main-preview-addons.test.ts new file mode 100644 index 000000000000..774ace2af674 --- /dev/null +++ b/code/core/src/common/utils/sync-main-preview-addons.test.ts @@ -0,0 +1,153 @@ +import type { Mock } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; + +import type { StorybookConfigRaw } from '@storybook/types'; + +import { loadConfig, printConfig } from '@storybook/core/csf-tools'; + +import { dedent } from 'ts-dedent'; + +import { getAddonAnnotations } from './get-addon-annotations'; +import { getSyncedStorybookAddons } from './sync-main-preview-addons'; + +vi.mock('./get-addon-annotations'); + +expect.addSnapshotSerializer({ + serialize: (val: any) => (typeof val === 'string' ? dedent(val) : dedent(val.toString())), + test: () => true, +}); + +describe('getSyncedStorybookAddons', () => { + const mainConfig: StorybookConfigRaw = { + stories: [], + addons: ['custom-addon', '@storybook/addon-a11y'], + }; + + it('should sync addons between main and preview', async () => { + const preview = loadConfig(` + import * as myAddonAnnotations from "custom-addon/preview"; + import { definePreview } from "@storybook/react/preview"; + + export default definePreview({ + addons: [myAddonAnnotations], + }); + `).parse(); + + (getAddonAnnotations as Mock).mockImplementation(() => { + return { importName: 'addonA11yAnnotations', importPath: '@storybook/addon-a11y/preview' }; + }); + + const result = await getSyncedStorybookAddons(mainConfig, preview); + expect(printConfig(result).code).toMatchInlineSnapshot(` + import * as addonA11yAnnotations from "@storybook/addon-a11y/preview"; + import * as myAddonAnnotations from "custom-addon/preview"; + import { definePreview } from "@storybook/react/preview"; + + export default definePreview({ + addons: [myAddonAnnotations, addonA11yAnnotations], + }); + `); + }); + + it('should sync addons as functions when they are core packages', async () => { + const preview = loadConfig(` + import * as myAddonAnnotations from "custom-addon/preview"; + import { definePreview } from "@storybook/react/preview"; + + export default definePreview({ + addons: [myAddonAnnotations], + }); + `).parse(); + + (getAddonAnnotations as Mock).mockImplementation(() => { + return { + importName: 'addonA11yAnnotations', + importPath: '@storybook/addon-a11y', + isCoreAddon: true, + }; + }); + + const result = await getSyncedStorybookAddons(mainConfig, preview); + expect(printConfig(result).code).toMatchInlineSnapshot(` + import addonA11yAnnotations from "@storybook/addon-a11y"; + import * as myAddonAnnotations from "custom-addon/preview"; + import { definePreview } from "@storybook/react/preview"; + + export default definePreview({ + addons: [myAddonAnnotations, addonA11yAnnotations()], + }); + `); + }); + + it('should add addons if the preview has no addons field', async () => { + const originalCode = dedent` + import { definePreview } from "@storybook/react/preview"; + + export default definePreview({ + tags: [] + }); + `; + const preview = loadConfig(originalCode).parse(); + + (getAddonAnnotations as Mock).mockImplementation(() => { + return { importName: 'addonA11yAnnotations', importPath: '@storybook/addon-a11y/preview' }; + }); + + const result = await getSyncedStorybookAddons(mainConfig, preview); + expect(printConfig(result).code).toMatchInlineSnapshot(` + import * as addonA11yAnnotations from "@storybook/addon-a11y/preview"; + import { definePreview } from "@storybook/react/preview"; + + export default definePreview({ + tags: [], + addons: [addonA11yAnnotations] + }); + `); + }); + + // necessary for windows and unix output to match in the assertions + const normalizeLineBreaks = (str: string) => str.replace(/\r/g, '').trim(); + it('should not add an addon if its annotations path has already been imported', async () => { + const originalCode = dedent` + import * as addonA11yAnnotations from "@storybook/addon-a11y/preview"; + import * as myAddonAnnotations from "custom-addon/preview"; + import { definePreview } from "@storybook/react/preview"; + const extraAddons = [addonA11yAnnotations] + export default definePreview({ + addons: [myAddonAnnotations, ...extraAddons], + }); + `; + const preview = loadConfig(originalCode).parse(); + + (getAddonAnnotations as Mock).mockImplementation(() => { + return { importName: 'addonA11yAnnotations', importPath: '@storybook/addon-a11y/preview' }; + }); + + const result = await getSyncedStorybookAddons(mainConfig, preview); + const transformedCode = normalizeLineBreaks(printConfig(result).code); + + expect(transformedCode).toMatch(originalCode); + }); + + it('should not modify the code if all addons are already synced', async () => { + const originalCode = dedent` + import * as addonA11yAnnotations from "@storybook/addon-a11y/preview"; + import * as myAddonAnnotations from "custom-addon/preview"; + import { definePreview } from "@storybook/react/preview"; + + export default definePreview({ + addons: [myAddonAnnotations, addonA11yAnnotations], + }); + `; + const preview = loadConfig(originalCode).parse(); + + (getAddonAnnotations as Mock).mockImplementation(() => { + return { importName: 'addonA11yAnnotations', importPath: '@storybook/addon-a11y/preview' }; + }); + + const result = await getSyncedStorybookAddons(mainConfig, preview); + const transformedCode = normalizeLineBreaks(printConfig(result).code); + + expect(transformedCode).toMatch(originalCode); + }); +}); diff --git a/code/core/src/common/utils/sync-main-preview-addons.ts b/code/core/src/common/utils/sync-main-preview-addons.ts new file mode 100644 index 000000000000..462b9554adc7 --- /dev/null +++ b/code/core/src/common/utils/sync-main-preview-addons.ts @@ -0,0 +1,89 @@ +/* eslint-disable no-underscore-dangle */ +import { types as t } from '@storybook/core/babel'; +import type { StorybookConfig } from '@storybook/types'; + +import { + type ConfigFile, + isCsfFactoryPreview, + readConfig, + writeConfig, +} from '@storybook/core/csf-tools'; + +import picocolors from 'picocolors'; + +import { getAddonAnnotations } from './get-addon-annotations'; +import { getAddonNames } from './get-addon-names'; + +const logger = console; + +export async function syncStorybookAddons(mainConfig: StorybookConfig, previewConfigPath: string) { + const previewConfig = await readConfig(previewConfigPath!); + const modifiedConfig = await getSyncedStorybookAddons(mainConfig, previewConfig); + + await writeConfig(modifiedConfig); +} + +export async function getSyncedStorybookAddons( + mainConfig: StorybookConfig, + previewConfig: ConfigFile +): Promise { + const isCsfFactory = isCsfFactoryPreview(previewConfig); + + if (!isCsfFactory) { + return previewConfig; + } + + const addons = getAddonNames(mainConfig); + if (!addons) { + return previewConfig; + } + + const syncedAddons: string[] = []; + const existingAddons = previewConfig.getFieldNode(['addons']); + /** + * This goes through all mainConfig.addons, read their package.json and check whether they have an + * exports map called preview, if so add to the array + */ + await addons.forEach(async (addon) => { + const annotations = await getAddonAnnotations(addon); + if (annotations) { + const hasAlreadyImportedAddonAnnotations = previewConfig._ast.program.body.find( + (node) => t.isImportDeclaration(node) && node.source.value === annotations.importPath + ); + + if (!!hasAlreadyImportedAddonAnnotations) { + return; + } + + if ( + !existingAddons || + (t.isArrayExpression(existingAddons) && + !existingAddons.elements.some( + (element) => t.isIdentifier(element) && element.name === annotations.importName + )) + ) { + syncedAddons.push(addon); + if (annotations.isCoreAddon) { + // import addonName from 'addon'; + addonName() + previewConfig.setImport(annotations.importName, annotations.importPath); + previewConfig.appendNodeToArray( + ['addons'], + t.callExpression(t.identifier(annotations.importName), []) + ); + } else { + // import * as addonName from 'addon/preview'; + addonName + previewConfig.setImport({ namespace: annotations.importName }, annotations.importPath); + previewConfig.appendNodeToArray(['addons'], t.identifier(annotations.importName)); + } + } + } + }); + + if (syncedAddons.length > 0) { + logger.info( + `Synchronizing addons from main config in ${picocolors.cyan(previewConfig.fileName)}:\n${syncedAddons.map(picocolors.magenta).join(', ')}` + ); + } + + return previewConfig; +} diff --git a/code/core/src/components/components/Button/Button.stories.tsx b/code/core/src/components/components/Button/Button.stories.tsx index 93407ce8cc8d..78e061d5d1a7 100644 --- a/code/core/src/components/components/Button/Button.stories.tsx +++ b/code/core/src/components/components/Button/Button.stories.tsx @@ -2,18 +2,16 @@ import type { ReactNode } from 'react'; import React from 'react'; import { FaceHappyIcon } from '@storybook/icons'; -import type { Meta, StoryObj } from '@storybook/react'; +import preview from '../../../../../.storybook/preview'; import { Button } from './Button'; -const meta = { +const meta = preview.meta({ + id: 'button-component', title: 'Button', component: Button, args: { children: 'Button' }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; +}); const Stack = ({ children }: { children: ReactNode }) => (
{children}
@@ -23,9 +21,9 @@ const Row = ({ children }: { children: ReactNode }) => (
{children}
); -export const Base: Story = {}; +export const Base = meta.story({}); -export const Variants: Story = { +export const Variants = meta.story({ render: (args) => ( @@ -63,9 +61,9 @@ export const Variants: Story = { ), -}; +}); -export const Active: Story = { +export const Active = meta.story({ args: { active: true, children: ( @@ -82,9 +80,9 @@ export const Active: Story = { ), -}; +}); -export const Disabled: Story = { +export const Disabled = meta.story({ args: { disabled: true, children: 'Disabled Button', }, -}; +}); -export const WithHref: Story = { +export const WithHref = meta.story({ render: () => ( @@ -141,9 +139,9 @@ export const WithHref: Story = { ), -}; +}); -export const Animated: Story = { +export const Animated = meta.story({ args: { variant: 'outline', }, @@ -184,4 +182,4 @@ export const Animated: Story = { ), -}; +}); diff --git a/code/core/src/components/components/Button/Docs.mdx b/code/core/src/components/components/Button/Docs.mdx index 3872c9a3fab0..83dbb3c95a2a 100644 --- a/code/core/src/components/components/Button/Docs.mdx +++ b/code/core/src/components/components/Button/Docs.mdx @@ -28,10 +28,10 @@ import { FaceHappyIcon, HeartIcon } from '@storybook/icons' // Using the onClick event handler -// Using the asChild prop to render a custom child - + // Using the asChild prop to render a custom child + `} /> diff --git a/code/core/src/core-server/build-dev.ts b/code/core/src/core-server/build-dev.ts index c8b336a22e09..be55dc10ff09 100644 --- a/code/core/src/core-server/build-dev.ts +++ b/code/core/src/core-server/build-dev.ts @@ -9,6 +9,7 @@ import { resolveAddonName, resolvePathInStorybookCache, serverResolve, + syncStorybookAddons, validateFrameworkName, versions, } from '@storybook/core/common'; @@ -113,6 +114,10 @@ export async function buildDevStandalone( console.warn('Storybook failed to check addon compatibility', e); } + try { + await syncStorybookAddons(config, previewConfigPath!); + } catch (e) {} + try { await warnWhenUsingArgTypesRegex(previewConfigPath, config); } catch (e) {} diff --git a/code/core/src/core-server/utils/StoryIndexGenerator.test.ts b/code/core/src/core-server/utils/StoryIndexGenerator.test.ts index 495cc2010e6a..544c24375f39 100644 --- a/code/core/src/core-server/utils/StoryIndexGenerator.test.ts +++ b/code/core/src/core-server/utils/StoryIndexGenerator.test.ts @@ -95,6 +95,7 @@ describe('StoryIndexGenerator', () => { expect(stats).toMatchInlineSnapshot(` { "beforeEach": 0, + "factory": 0, "globals": 0, "loaders": 0, "moduleMock": 0, @@ -463,6 +464,7 @@ describe('StoryIndexGenerator', () => { expect(stats).toMatchInlineSnapshot(` { "beforeEach": 1, + "factory": 0, "globals": 0, "loaders": 1, "moduleMock": 0, @@ -728,6 +730,7 @@ describe('StoryIndexGenerator', () => { expect(stats).toMatchInlineSnapshot(` { "beforeEach": 1, + "factory": 0, "globals": 0, "loaders": 1, "moduleMock": 0, diff --git a/code/core/src/core-server/utils/get-new-story-file.ts b/code/core/src/core-server/utils/get-new-story-file.ts index 570ab8aecf7e..df8276484693 100644 --- a/code/core/src/core-server/utils/get-new-story-file.ts +++ b/code/core/src/core-server/utils/get-new-story-file.ts @@ -1,8 +1,10 @@ import { existsSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; import { basename, dirname, extname, join } from 'node:path'; import { extractProperRendererNameFromFramework, + findConfigFile, getFrameworkName, getProjectRoot, rendererPackages, @@ -10,7 +12,10 @@ import { import type { Options } from '@storybook/core/types'; import type { CreateNewStoryRequestPayload } from '@storybook/core/core-events'; +import { isCsfFactoryPreview } from '@storybook/core/csf-tools'; +import { loadConfig } from '../../csf-tools'; +import { getCsfFactoryTemplateForNewStoryFile } from './new-story-templates/csf-factory-template'; import { getJavaScriptTemplateForNewStoryFile } from './new-story-templates/javascript'; import { getTypeScriptTemplateForNewStoryFile } from './new-story-templates/typescript'; @@ -42,21 +47,42 @@ export async function getNewStoryFile( const exportedStoryName = 'Default'; - const storyFileContent = - isTypescript && rendererPackage - ? await getTypeScriptTemplateForNewStoryFile({ - basenameWithoutExtension, - componentExportName, - componentIsDefaultExport, - rendererPackage, - exportedStoryName, - }) - : await getJavaScriptTemplateForNewStoryFile({ - basenameWithoutExtension, - componentExportName, - componentIsDefaultExport, - exportedStoryName, - }); + let useCsfFactory = false; + try { + const previewConfig = findConfigFile('preview', options.configDir); + if (previewConfig) { + const previewContent = await readFile(previewConfig, 'utf-8'); + useCsfFactory = isCsfFactoryPreview(loadConfig(previewContent)); + } + } catch (err) { + // TODO: improve this later on, for now while CSF factories are experimental, just fallback to CSF3 + } + + let storyFileContent = ''; + if (useCsfFactory) { + storyFileContent = await getCsfFactoryTemplateForNewStoryFile({ + basenameWithoutExtension, + componentExportName, + componentIsDefaultExport, + exportedStoryName, + }); + } else { + storyFileContent = + isTypescript && rendererPackage + ? await getTypeScriptTemplateForNewStoryFile({ + basenameWithoutExtension, + componentExportName, + componentIsDefaultExport, + rendererPackage, + exportedStoryName, + }) + : await getJavaScriptTemplateForNewStoryFile({ + basenameWithoutExtension, + componentExportName, + componentIsDefaultExport, + exportedStoryName, + }); + } const storyFilePath = doesStoryFileExist(join(cwd, dir), storyFileName) && componentExportCount > 1 diff --git a/code/core/src/core-server/utils/new-story-templates/csf-factory-template.test.ts b/code/core/src/core-server/utils/new-story-templates/csf-factory-template.test.ts new file mode 100644 index 000000000000..2c41ce689062 --- /dev/null +++ b/code/core/src/core-server/utils/new-story-templates/csf-factory-template.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; + +import { getCsfFactoryTemplateForNewStoryFile } from './csf-factory-template'; + +describe('csf-factories', () => { + it('should return a CSF factories template with a default import', async () => { + const result = await getCsfFactoryTemplateForNewStoryFile({ + basenameWithoutExtension: 'foo', + componentExportName: 'default', + componentIsDefaultExport: true, + exportedStoryName: 'Default', + }); + + expect(result).toMatchInlineSnapshot(` + "import preview from '#.storybook/preview'; + + import Foo from './foo'; + + const meta = preview.meta({ + component: Foo, + }); + + export const Default = meta.story({});" + `); + }); +}); diff --git a/code/core/src/core-server/utils/new-story-templates/csf-factory-template.ts b/code/core/src/core-server/utils/new-story-templates/csf-factory-template.ts new file mode 100644 index 000000000000..33af96bb6e66 --- /dev/null +++ b/code/core/src/core-server/utils/new-story-templates/csf-factory-template.ts @@ -0,0 +1,33 @@ +import { dedent } from 'ts-dedent'; + +import { getComponentVariableName } from '../get-component-variable-name'; + +interface CsfFactoryTemplateData { + /** The components file name without the extension */ + basenameWithoutExtension: string; + componentExportName: string; + componentIsDefaultExport: boolean; + /** The exported name of the default story */ + exportedStoryName: string; +} + +export async function getCsfFactoryTemplateForNewStoryFile(data: CsfFactoryTemplateData) { + const importName = data.componentIsDefaultExport + ? await getComponentVariableName(data.basenameWithoutExtension) + : data.componentExportName; + const importStatement = data.componentIsDefaultExport + ? `import ${importName} from './${data.basenameWithoutExtension}';` + : `import { ${importName} } from './${data.basenameWithoutExtension}';`; + const previewImport = `import preview from '#.storybook/preview';`; + return dedent` + ${previewImport} + + ${importStatement} + + const meta = preview.meta({ + component: ${importName}, + }); + + export const ${data.exportedStoryName} = meta.story({}); + `; +} diff --git a/code/core/src/core-server/utils/save-story/duplicate-story-with-new-name.test.ts b/code/core/src/core-server/utils/save-story/duplicate-story-with-new-name.test.ts index 5cd500c88dc3..92b2baa09a0e 100644 --- a/code/core/src/core-server/utils/save-story/duplicate-story-with-new-name.test.ts +++ b/code/core/src/core-server/utils/save-story/duplicate-story-with-new-name.test.ts @@ -15,6 +15,7 @@ const makeTitle = (userTitle: string) => userTitle; const FILES = { csfVariances: join(__dirname, 'mocks/csf-variances.stories.tsx'), + csf4Variances: join(__dirname, 'mocks/csf4-variances.stories.tsx'), unsupportedCsfVariances: join(__dirname, 'mocks/unsupported-csf-variances.stories.tsx'), typescriptConstructs: join(__dirname, 'mocks/typescript-constructs.stories.tsx'), }; @@ -77,6 +78,38 @@ describe('success', () => { + " `); }); + test('CSF4 Variances', async () => { + const before = await format(await readFile(FILES.csf4Variances, 'utf-8'), { + parser: 'typescript', + }); + const CSF = await readCsf(FILES.csf4Variances, { makeTitle }); + + const parsed = CSF.parse(); + const names = Object.keys(parsed._stories); + + names.forEach((name) => { + duplicateStoryWithNewName(parsed, name, name + 'Duplicated'); + }); + + const after = await format(printCsf(parsed).code, { + parser: 'typescript', + }); + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + foo: "bar", + }, + }); + + + export const EmptyDuplicated = meta.story({}); + + export const WithArgsDuplicated = meta.story({}); + + " + `); + }); test('Unsupported CSF Variances', async () => { const CSF = await readCsf(FILES.unsupportedCsfVariances, { makeTitle }); diff --git a/code/core/src/core-server/utils/save-story/duplicate-story-with-new-name.ts b/code/core/src/core-server/utils/save-story/duplicate-story-with-new-name.ts index fe85e2cb6aa0..cf20ccfcaeb1 100644 --- a/code/core/src/core-server/utils/save-story/duplicate-story-with-new-name.ts +++ b/code/core/src/core-server/utils/save-story/duplicate-story-with-new-name.ts @@ -37,8 +37,17 @@ export const duplicateStoryWithNewName = (csfFile: In, storyName: string, newSto noScope: true, }); + const isCsf4Story = + t.isCallExpression(cloned.init) && + t.isMemberExpression(cloned.init.callee) && + t.isIdentifier(cloned.init.callee.property) && + cloned.init.callee.property.name === 'story'; + // detect CSF2 and throw - if (t.isArrowFunctionExpression(cloned.init) || t.isCallExpression(cloned.init)) { + if ( + !isCsf4Story && + (t.isArrowFunctionExpression(cloned.init) || t.isCallExpression(cloned.init)) + ) { throw new SaveStoryError(`Creating a new story based on a CSF2 story is not supported`); } diff --git a/code/core/src/core-server/utils/save-story/mocks/csf-variances.stories.tsx b/code/core/src/core-server/utils/save-story/mocks/csf-variances.stories.tsx index 35081f422ae3..674605e2ac4e 100644 --- a/code/core/src/core-server/utils/save-story/mocks/csf-variances.stories.tsx +++ b/code/core/src/core-server/utils/save-story/mocks/csf-variances.stories.tsx @@ -49,7 +49,7 @@ export const RenderExistingArgs = { render: (args) => , } satisfies Story; -// The order of both the properties of the story and the order or args should be preserved +// The order of both the properties of the story and the order of args should be preserved export const OrderedArgs = { args: { bordered: true, @@ -59,7 +59,7 @@ export const OrderedArgs = { render: (args) => , } satisfies Story; -// The order of both the properties of the story and the order or args should be preserved +// The order of both the properties of the story and the order of args should be preserved export const HasPlayFunction = { args: { bordered: true, diff --git a/code/core/src/core-server/utils/save-story/mocks/csf4-variances.stories.tsx b/code/core/src/core-server/utils/save-story/mocks/csf4-variances.stories.tsx new file mode 100644 index 000000000000..51f78d90ed92 --- /dev/null +++ b/code/core/src/core-server/utils/save-story/mocks/csf4-variances.stories.tsx @@ -0,0 +1,15 @@ +// @ts-expect-error this is just a mock file +import preview from '#.storybook/preview'; + +const meta = preview.meta({ + title: 'MyComponent', + args: { + initial: 'foo', + }, +}); +export const Empty = meta.story({}); +export const WithArgs = meta.story({ + args: { + foo: 'bar', + }, +}); diff --git a/code/core/src/core-server/utils/save-story/update-args-in-csf-file.test.ts b/code/core/src/core-server/utils/save-story/update-args-in-csf-file.test.ts index d094a82aff13..502443c0a692 100644 --- a/code/core/src/core-server/utils/save-story/update-args-in-csf-file.test.ts +++ b/code/core/src/core-server/utils/save-story/update-args-in-csf-file.test.ts @@ -16,6 +16,7 @@ const makeTitle = (userTitle: string) => userTitle; const FILES = { typescriptConstructs: join(__dirname, 'mocks/typescript-constructs.stories.tsx'), csfVariances: join(__dirname, 'mocks/csf-variances.stories.tsx'), + csf4Variances: join(__dirname, 'mocks/csf4-variances.stories.tsx'), unsupportedCsfVariances: join(__dirname, 'mocks/unsupported-csf-variances.stories.tsx'), exportVariances: join(__dirname, 'mocks/export-variances.stories.tsx'), dataVariances: join(__dirname, 'mocks/data-variances.stories.tsx'), @@ -204,7 +205,7 @@ describe('success', () => { render: (args) => , } satisfies Story; - // The order of both the properties of the story and the order or args should be preserved + // The order of both the properties of the story and the order of args should be preserved export const OrderedArgs = { args: { bordered: true, @@ -233,6 +234,58 @@ describe('success', () => { ..." `); }); + test('CSF4 Variances', async () => { + const newArgs = { bordered: true, initial: 'test1' }; + + const before = await format(await readFile(FILES.csf4Variances, 'utf-8'), { + parser: 'typescript', + }); + const CSF = await readCsf(FILES.csf4Variances, { makeTitle }); + + const parsed = CSF.parse(); + const names = Object.keys(parsed._stories); + const nodes = names.map((name) => CSF.getStoryExport(name)); + + nodes.forEach((node) => { + updateArgsInCsfFile(node, newArgs); + }); + + const after = await format(printCsf(parsed).code, { + parser: 'typescript', + }); + + // check if the code was updated at all + expect(after).not.toBe(before); + + // check if the code was updated correctly + // TODO, the comment is not preserved!!! + expect(getDiff(before, after)).toMatchInlineSnapshot(` + " ... + initial: "foo", + }, + }); + + - export const Empty = meta.story({}); + - + + export const Empty = meta.story({ + + args: { + + bordered: true, + + initial: "test1", + + }, + + }); + + + export const WithArgs = meta.story({ + args: { + foo: "bar", + + + bordered: true, + + initial: "test1", + + + }, + }); + " + `); + }); test('Export Variances', async () => { const newArgs = { bordered: true, initial: 'test1' }; diff --git a/code/core/src/core-server/utils/save-story/update-args-in-csf-file.ts b/code/core/src/core-server/utils/save-story/update-args-in-csf-file.ts index 7e1f30e7e961..d391d9c6a296 100644 --- a/code/core/src/core-server/utils/save-story/update-args-in-csf-file.ts +++ b/code/core/src/core-server/utils/save-story/update-args-in-csf-file.ts @@ -11,8 +11,14 @@ export const updateArgsInCsfFile = async (node: t.Node, input: Record { ).toEqual('bar'); }); }); + + describe('factory config', () => { + it('parses correctly', () => { + const source = dedent` + import { definePreview } from '@storybook/react-vite'; + + const config = definePreview({ + framework: 'foo', + }); + export default config; + `; + const config = loadConfig(source).parse(); + expect(config.getNameFromPath(['framework'])).toEqual('foo'); + }); + it('found scalar', () => { + expect( + getField( + ['core', 'builder'], + dedent` + import { definePreview } from '@storybook/react-vite'; + export const foo = definePreview({ core: { builder: 'webpack5' } }); + ` + ) + ).toEqual('webpack5'); + }); + it('tags', () => { + expect( + getField( + ['tags'], + dedent` + import { definePreview } from '@storybook/react-vite'; + const parameters = {}; + export const config = definePreview({ + parameters, + tags: ['test', 'vitest', '!a11ytest'], + }); + ` + ) + ).toEqual(['test', 'vitest', '!a11ytest']); + }); + }); }); describe('setField', () => { @@ -479,6 +520,73 @@ describe('ConfigFile', () => { `); }); }); + + describe('factory config', () => { + it('missing export', () => { + expect( + setField( + ['core', 'builder'], + 'webpack5', + dedent` + import { definePreview } from '@storybook/react-vite'; + export const foo = definePreview({ + addons: [], + }); + ` + ) + ).toMatchInlineSnapshot(` + import { definePreview } from '@storybook/react-vite'; + export const foo = definePreview({ + addons: [], + + core: { + builder: 'webpack5' + } + }); + `); + }); + it('missing field', () => { + expect( + setField( + ['core', 'builder'], + 'webpack5', + dedent` + import { definePreview } from '@storybook/react-vite'; + export const foo = definePreview({ + core: { foo: 'bar' }, + }); + ` + ) + ).toMatchInlineSnapshot(` + import { definePreview } from '@storybook/react-vite'; + export const foo = definePreview({ + core: { + foo: 'bar', + builder: 'webpack5' + }, + }); + `); + }); + it('found scalar', () => { + expect( + setField( + ['core', 'builder'], + 'webpack5', + dedent` + import { definePreview } from '@storybook/react-vite'; + export const foo = definePreview({ + core: { builder: 'webpack4' }, + }); + ` + ) + ).toMatchInlineSnapshot(` + import { definePreview } from '@storybook/react-vite'; + export const foo = definePreview({ + core: { builder: 'webpack5' }, + }); + `); + }); + }); }); describe('appendToArray', () => { @@ -921,6 +1029,19 @@ describe('ConfigFile', () => { expect(config.getNameFromPath(['otherField'])).toEqual('foo'); }); + it(`supports pnp wrapped names`, () => { + const source = dedent` + import type { StorybookConfig } from '@storybook/react-webpack5'; + + const config: StorybookConfig = { + framework: getAbsolutePath('foo'), + } + export default config; + `; + const config = loadConfig(source).parse(); + expect(config.getNameFromPath(['framework'])).toEqual('foo'); + }); + it(`returns undefined when accessing a field that does not exist`, () => { const source = dedent` import type { StorybookConfig } from '@storybook/react-webpack5'; @@ -1345,5 +1466,16 @@ describe('ConfigFile', () => { expect(config._exportDecls['path']).toBe(undefined); expect(config._exports['path']).toBe(undefined); }); + + it('detects const and function export declarations', () => { + const source = dedent` + export function normalFunction() { }; + export const value = ['@storybook/addon-essentials']; + export async function asyncFunction() { }; + `; + const config = loadConfig(source).parse(); + + expect(Object.keys(config._exportDecls)).toHaveLength(3); + }); }); }); diff --git a/code/core/src/csf-tools/ConfigFile.ts b/code/core/src/csf-tools/ConfigFile.ts index 19907bddd962..605e37675866 100644 --- a/code/core/src/csf-tools/ConfigFile.ts +++ b/code/core/src/csf-tools/ConfigFile.ts @@ -25,18 +25,8 @@ const getCsfParsingErrorMessage = ({ foundType: string | undefined; node: any | undefined; }) => { - let nodeInfo = ''; - if (node) { - try { - nodeInfo = JSON.stringify(node); - } catch (e) { - // - } - } - return dedent` CSF Parsing error: Expected '${expectedType}' but found '${foundType}' instead in '${node?.type}'. - ${nodeInfo} `; }; @@ -171,7 +161,7 @@ export class ConfigFile { // FIXME: this is a hack. this is only used in the case where the user is // modifying a named export that's a scalar. The _exports map is not suitable // for that. But rather than refactor the whole thing, we just use this as a stopgap. - _exportDecls: Record = {}; + _exportDecls: Record = {}; _exportsObject: t.ObjectExpression | undefined; @@ -187,6 +177,20 @@ export class ConfigFile { this.fileName = fileName; } + _parseExportsObject(exportsObject: t.ObjectExpression) { + this._exportsObject = exportsObject; + (exportsObject.properties as t.ObjectProperty[]).forEach((p) => { + const exportName = propKey(p); + if (exportName) { + let exportVal = p.value; + if (t.isIdentifier(exportVal)) { + exportVal = _findVarInitialization(exportVal.name, this._ast.program) as any; + } + this._exports[exportName] = exportVal as t.Expression; + } + }); + } + parse() { // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; @@ -201,18 +205,13 @@ export class ConfigFile { decl = unwrap(decl); + // csf factory + if (t.isCallExpression(decl) && t.isObjectExpression(decl.arguments[0])) { + decl = decl.arguments[0]; + } + if (t.isObjectExpression(decl)) { - self._exportsObject = decl; - (decl.properties as t.ObjectProperty[]).forEach((p) => { - const exportName = propKey(p); - if (exportName) { - let exportVal = p.value; - if (t.isIdentifier(exportVal)) { - exportVal = _findVarInitialization(exportVal.name, parent as t.Program) as any; - } - self._exports[exportName] = exportVal as t.Expression; - } - }); + self._parseExportsObject(decl); } else { logger.warn( getCsfParsingErrorMessage({ @@ -239,6 +238,13 @@ export class ConfigFile { self._exportDecls[exportName] = decl; } }); + } else if (t.isFunctionDeclaration(node.declaration)) { + // export function X() {...}; + const decl = node.declaration; + if (t.isIdentifier(decl.id)) { + const { name: exportName } = decl.id; + self._exportDecls[exportName] = decl; + } } else if (node.specifiers) { // export { X }; node.specifiers.forEach((spec) => { @@ -315,6 +321,18 @@ export class ConfigFile { } }, }, + CallExpression: { + enter: ({ node }) => { + if ( + t.isIdentifier(node.callee) && + node.callee.name === 'definePreview' && + node.arguments.length === 1 && + t.isObjectExpression(node.arguments[0]) + ) { + self._parseExportsObject(node.arguments[0]); + } + }, + }, }); return self; } @@ -369,7 +387,9 @@ export class ConfigFile { _updateExportNode(rest, expr, exportNode); } else if (exportNode && rest.length === 0 && this._exportDecls[path[0]]) { const decl = this._exportDecls[path[0]]; - decl.init = _makeObjectExpression([], expr); + if (t.isVariableDeclarator(decl)) { + decl.init = _makeObjectExpression([], expr); + } } else if (this.hasDefaultExport) { // This means the main.js of the user has a default export that is not an object expression, therefore we can'types change the AST. throw new Error( @@ -483,6 +503,8 @@ export class ConfigFile { value = prop.value.value; } }); + } else if (t.isCallExpression(node)) { + value = this._getPnpWrappedValue(node); } if (!value) { @@ -948,3 +970,20 @@ export const writeConfig = async (config: ConfigFile, fileName?: string) => { } await writeFile(fname, formatConfig(config)); }; + +export const isCsfFactoryPreview = (previewConfig: ConfigFile) => { + const program = previewConfig._ast.program; + return !!program.body.find((node) => { + return ( + t.isImportDeclaration(node) && + node.source.value.includes('@storybook') && + node.specifiers.some((specifier) => { + return ( + t.isImportSpecifier(specifier) && + t.isIdentifier(specifier.imported) && + specifier.imported.name === 'definePreview' + ); + }) + ); + }); +}; diff --git a/code/core/src/csf-tools/CsfFile.test.ts b/code/core/src/csf-tools/CsfFile.test.ts index d7194edcdf2e..639e1a6dbfc6 100644 --- a/code/core/src/csf-tools/CsfFile.test.ts +++ b/code/core/src/csf-tools/CsfFile.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it, vi } from 'vitest'; import yaml from 'js-yaml'; import { dedent } from 'ts-dedent'; -import { type CsfOptions, formatCsf, isModuleMock, loadCsf } from './CsfFile'; +import { type CsfOptions, formatCsf, isModuleMock, isValidPreviewPath, loadCsf } from './CsfFile'; expect.addSnapshotSerializer({ print: (val: any) => yaml.dump(val).trimEnd(), @@ -37,6 +37,7 @@ describe('CsfFile', () => { const parsed = loadCsf(code, { makeTitle }).parse(); expect(Object.keys(parsed._stories)).toEqual(['validStory']); }); + it('filters out non-story exports', () => { const code = ` export default { title: 'foo/bar', excludeStories: ['invalidStory'] }; @@ -48,12 +49,13 @@ describe('CsfFile', () => { const parsed = loadCsf(code, { makeTitle }).parse(); expect(Object.keys(parsed._stories)).toEqual(['A', 'B']); }); + it('transforms inline default exports to constant declarations', () => { expect( transform( dedent` - export default { title: 'foo/bar' }; - `, + export default { title: 'foo/bar' }; + `, { transformInlineMeta: true } ) ).toMatchInlineSnapshot(` @@ -129,6 +131,7 @@ describe('CsfFile', () => { __isArgsStory: false __id: foo-bar--basic __stats: + factory: false play: false render: false loaders: false @@ -160,6 +163,7 @@ describe('CsfFile', () => { - id: foo-bar--a name: A __stats: + factory: false play: false render: false loaders: false @@ -189,6 +193,7 @@ describe('CsfFile', () => { - id: foo-bar--include-a name: Include A __stats: + factory: false play: false render: false loaders: false @@ -216,6 +221,7 @@ describe('CsfFile', () => { - id: foo-bar--a name: Some story __stats: + factory: false play: false render: false loaders: false @@ -244,6 +250,7 @@ describe('CsfFile', () => { - id: default-title--a name: A __stats: + factory: false play: false render: false loaders: false @@ -255,6 +262,7 @@ describe('CsfFile', () => { - id: default-title--b name: B __stats: + factory: false play: false render: false loaders: false @@ -283,6 +291,7 @@ describe('CsfFile', () => { - id: custom-id--a name: A __stats: + factory: false play: false render: false loaders: false @@ -294,6 +303,7 @@ describe('CsfFile', () => { - id: custom-id--b name: B __stats: + factory: false play: false render: false loaders: false @@ -322,6 +332,7 @@ describe('CsfFile', () => { - id: custom-meta-id--just-custom-meta-id name: Just Custom Meta Id __stats: + factory: false play: false render: false loaders: false @@ -333,6 +344,7 @@ describe('CsfFile', () => { - id: custom-id name: Custom Paremeters Id __stats: + factory: false play: false render: false loaders: false @@ -362,6 +374,7 @@ describe('CsfFile', () => { - id: foo-bar-baz--a name: A __stats: + factory: false play: false render: false loaders: false @@ -373,6 +386,7 @@ describe('CsfFile', () => { - id: foo-bar-baz--b name: B __stats: + factory: false play: false render: false loaders: false @@ -406,6 +420,7 @@ describe('CsfFile', () => { __isArgsStory: true __id: foo-bar--a __stats: + factory: false play: false render: false loaders: false @@ -420,6 +435,7 @@ describe('CsfFile', () => { __isArgsStory: true __id: foo-bar--b __stats: + factory: false play: false render: false loaders: false @@ -453,6 +469,7 @@ describe('CsfFile', () => { __isArgsStory: true __id: foo-bar--a __stats: + factory: false play: false render: false loaders: false @@ -467,6 +484,7 @@ describe('CsfFile', () => { __isArgsStory: true __id: foo-bar--b __stats: + factory: false play: false render: false loaders: false @@ -497,6 +515,7 @@ describe('CsfFile', () => { - id: foo-bar-baz--a name: A __stats: + factory: false play: false render: false loaders: false @@ -508,6 +527,7 @@ describe('CsfFile', () => { - id: foo-bar-baz--b name: B __stats: + factory: false play: false render: false loaders: false @@ -538,6 +558,7 @@ describe('CsfFile', () => { - id: foo-bar-baz--a name: A __stats: + factory: false play: false render: false loaders: false @@ -549,6 +570,7 @@ describe('CsfFile', () => { - id: foo-bar-baz--b name: B __stats: + factory: false play: false render: false loaders: false @@ -577,6 +599,7 @@ describe('CsfFile', () => { - id: default-title--a name: A __stats: + factory: false play: false render: false loaders: false @@ -588,6 +611,7 @@ describe('CsfFile', () => { - id: default-title--b name: B __stats: + factory: false play: false render: false loaders: false @@ -620,6 +644,7 @@ describe('CsfFile', () => { __isArgsStory: true __id: foo-bar--a __stats: + factory: false play: false render: false loaders: false @@ -651,6 +676,7 @@ describe('CsfFile', () => { __isArgsStory: false __id: foo-bar--a __stats: + factory: false play: false render: false loaders: false @@ -683,6 +709,7 @@ describe('CsfFile', () => { __id: foo-bar--page docsOnly: true __stats: + factory: false play: false render: false loaders: false @@ -720,6 +747,7 @@ describe('CsfFile', () => { __id: foo-bar--page docsOnly: true __stats: + factory: false play: false render: false loaders: false @@ -752,6 +780,7 @@ describe('CsfFile', () => { __isArgsStory: false __id: foo-bar--a __stats: + factory: false play: false render: false loaders: false @@ -766,6 +795,7 @@ describe('CsfFile', () => { __isArgsStory: true __id: foo-bar--b __stats: + factory: false play: false render: false loaders: false @@ -838,6 +868,7 @@ describe('CsfFile', () => { __isArgsStory: true __id: foo-bar--b __stats: + factory: false play: false render: false loaders: false @@ -852,6 +883,7 @@ describe('CsfFile', () => { __isArgsStory: false __id: foo-bar--a __stats: + factory: false play: false render: false loaders: false @@ -966,6 +998,7 @@ describe('CsfFile', () => { - id: default-title--a name: A __stats: + factory: false play: false render: false loaders: false @@ -977,6 +1010,7 @@ describe('CsfFile', () => { - id: default-title--b name: B __stats: + factory: false play: false render: false loaders: false @@ -1030,6 +1064,7 @@ describe('CsfFile', () => { - id: foo-bar--a name: A __stats: + factory: false play: false render: false loaders: false @@ -1041,6 +1076,7 @@ describe('CsfFile', () => { - id: foo-bar--b name: B __stats: + factory: false play: false render: false loaders: false @@ -1118,6 +1154,7 @@ describe('CsfFile', () => { __isArgsStory: false __id: foo-bar--a __stats: + factory: false play: false render: true loaders: false @@ -1150,6 +1187,7 @@ describe('CsfFile', () => { __isArgsStory: true __id: foo-bar--a __stats: + factory: false play: false render: true loaders: false @@ -1180,6 +1218,7 @@ describe('CsfFile', () => { __isArgsStory: true __id: foo-bar--a __stats: + factory: false play: false render: false loaders: false @@ -1212,6 +1251,7 @@ describe('CsfFile', () => { __isArgsStory: true __id: foo-bar--a __stats: + factory: false play: false render: false loaders: false @@ -1293,6 +1333,7 @@ describe('CsfFile', () => { - id: foo-bar--a name: A __stats: + factory: false play: false render: false loaders: false @@ -1326,6 +1367,7 @@ describe('CsfFile', () => { - id: foo-bar--a name: A __stats: + factory: false play: false render: true loaders: false @@ -1361,6 +1403,7 @@ describe('CsfFile', () => { - id: foo-bar--a name: A __stats: + factory: false play: false render: true loaders: false @@ -1421,6 +1464,7 @@ describe('CsfFile', () => { - id: foo-bar--a name: A __stats: + factory: false play: true render: false loaders: false @@ -1456,6 +1500,7 @@ describe('CsfFile', () => { - id: foo-bar--a name: A __stats: + factory: false play: true render: true loaders: false @@ -1487,6 +1532,7 @@ describe('CsfFile', () => { - id: foo-bar--a name: A __stats: + factory: false play: true render: false loaders: false @@ -1517,6 +1563,7 @@ describe('CsfFile', () => { - id: foo-bar--a name: A __stats: + factory: false play: true render: false loaders: false @@ -1550,6 +1597,7 @@ describe('CsfFile', () => { - id: foo-bar--a name: A __stats: + factory: false play: true render: false loaders: false @@ -1583,6 +1631,7 @@ describe('CsfFile', () => { - id: foo-bar--a name: A __stats: + factory: false play: true render: true loaders: true @@ -1615,6 +1664,7 @@ describe('CsfFile', () => { - id: foo-bar--a name: A __stats: + factory: false play: true render: false loaders: false @@ -1665,6 +1715,7 @@ describe('CsfFile', () => { - play-fn __id: component-id--a __stats: + factory: false play: true render: false loaders: false @@ -1685,6 +1736,7 @@ describe('CsfFile', () => { - play-fn __id: component-id--b __stats: + factory: false play: true render: false loaders: false @@ -1723,6 +1775,7 @@ describe('CsfFile', () => { - component-tag __id: custom-story-id __stats: + factory: false play: false render: false loaders: false @@ -1766,6 +1819,7 @@ describe('CsfFile', () => { - inherit-tag-dup __id: custom-foo-title--a __stats: + factory: false play: false render: false loaders: false @@ -1824,6 +1878,7 @@ describe('CsfFile', () => { tags: [] __id: custom-foo-title--a __stats: + factory: false play: false render: true loaders: false @@ -1861,6 +1916,7 @@ describe('CsfFile', () => { tags: [] __id: custom-foo-title--a __stats: + factory: false play: false render: true loaders: false @@ -1898,6 +1954,7 @@ describe('CsfFile', () => { tags: [] __id: custom-foo-title--a __stats: + factory: false play: false render: true loaders: false @@ -1935,6 +1992,7 @@ describe('CsfFile', () => { tags: [] __id: custom-foo-title--a __stats: + factory: false play: false render: true loaders: false @@ -1965,6 +2023,7 @@ describe('CsfFile', () => { - id: foo-bar--a name: A __stats: + factory: false play: false render: false loaders: false @@ -1995,6 +2054,7 @@ describe('CsfFile', () => { - id: foo-bar--a name: A __stats: + factory: false play: false render: false loaders: false @@ -2024,6 +2084,7 @@ describe('CsfFile', () => { - id: foo-bar--a name: A __stats: + factory: false play: false render: false loaders: false @@ -2050,6 +2111,7 @@ describe('CsfFile', () => { - id: foo-bar--a name: A __stats: + factory: false play: false render: false loaders: false @@ -2061,6 +2123,343 @@ describe('CsfFile', () => { `); }); }); + + describe('csf factories', () => { + describe('normal', () => { + it('meta variable', () => { + expect( + parse( + dedent` + import { config } from '#.storybook/preview' + const meta = config.meta({ component: 'foo' }); + export const A = meta.story({}) + export const B = meta.story({}) + ` + ) + ).toMatchInlineSnapshot(` + meta: + component: '''foo''' + title: Default Title + stories: + - id: default-title--a + name: A + __stats: + factory: true + play: false + render: false + loaders: false + beforeEach: false + globals: false + storyFn: false + mount: false + moduleMock: false + - id: default-title--b + name: B + __stats: + factory: true + play: false + render: false + loaders: false + beforeEach: false + globals: false + storyFn: false + mount: false + moduleMock: false + `); + }); + + it('meta variable with renamed factory', () => { + expect( + parse( + dedent` + import { boo as moo } from '#.storybook/preview' + const meta = moo.meta({ component: 'foo' }); + export const A = meta.story({}) + ` + ) + ).toMatchInlineSnapshot(` + meta: + component: '''foo''' + title: Default Title + stories: + - id: default-title--a + name: A + __stats: + factory: true + play: false + render: false + loaders: false + beforeEach: false + globals: false + storyFn: false + mount: false + moduleMock: false + `); + }); + + it('meta default export', () => { + expect( + parse( + dedent` + import { config } from '#.storybook/preview' + const meta = config.meta({ component: 'foo' }); + export default meta; + export const A = meta.story({}) + export const B = meta.story({}) + ` + ) + ).toMatchInlineSnapshot(` + meta: + component: '''foo''' + title: Default Title + stories: + - id: default-title--a + name: A + __stats: + factory: true + play: false + render: false + loaders: false + beforeEach: false + globals: false + storyFn: false + mount: false + moduleMock: false + - id: default-title--b + name: B + __stats: + factory: true + play: false + render: false + loaders: false + beforeEach: false + globals: false + storyFn: false + mount: false + moduleMock: false + `); + }); + + it('story name', () => { + expect( + parse( + dedent` + import { config } from '#.storybook/preview' + const meta = config.meta({ component: 'foo' }); + export const A = meta.story({ name: 'bar'}) + ` + ) + ).toMatchInlineSnapshot(` + meta: + component: '''foo''' + title: Default Title + stories: + - id: default-title--a + name: bar + __stats: + factory: true + play: false + render: false + loaders: false + beforeEach: false + globals: false + storyFn: false + mount: false + moduleMock: false + `); + }); + + it('Object export with no-args render', () => { + expect( + parse( + dedent` + import { config } from '#.storybook/preview' + const meta = config.meta({ title: 'foo/bar' }); + export const A = meta.story({ + render: () => {} + }) + `, + true + ) + ).toMatchInlineSnapshot(` + meta: + title: foo/bar + stories: + - id: foo-bar--a + name: A + parameters: + __isArgsStory: false + __id: foo-bar--a + __stats: + factory: true + play: false + render: true + loaders: false + beforeEach: false + globals: false + storyFn: false + mount: false + moduleMock: false + `); + }); + + it('Object export with args render', () => { + expect( + parse( + dedent` + import { config } from '#.storybook/preview' + const meta = config.meta({ title: 'foo/bar' }); + export const A = meta.story({ + render: (args) => {} + }); + `, + true + ) + ).toMatchInlineSnapshot(` + meta: + title: foo/bar + stories: + - id: foo-bar--a + name: A + parameters: + __isArgsStory: true + __id: foo-bar--a + __stats: + factory: true + play: false + render: true + loaders: false + beforeEach: false + globals: false + storyFn: false + mount: false + moduleMock: false + `); + }); + }); + describe('errors', () => { + it('multiple meta variables', () => { + expect(() => + parse( + dedent` + import { config } from '#.storybook/preview' + const foo = config.meta({ component: 'foo' }); + export const A = foo.story({}) + const bar = config.meta({ component: 'bar' }); + export const B = bar.story({}) + ` + ) + ).toThrowErrorMatchingInlineSnapshot(` + [MultipleMetaError: CSF: multiple meta objects (line 4, col 24) + + More info: https://storybook.js.org/docs/writing-stories#default-export] + `); + }); + + it('default export and meta', () => { + expect(() => + parse( + dedent` + import { config } from '#.storybook/preview' + export default { title: 'atoms/foo' }; + const meta = config.meta({ component: 'foo' }); + export const A = meta.story({}) + export const B = meta.story({}) + ` + ) + ).toThrowErrorMatchingInlineSnapshot(` + [MultipleMetaError: CSF: multiple meta objects (line 3, col 25) + + More info: https://storybook.js.org/docs/writing-stories#default-export] + `); + }); + + it('meta and default export', () => { + expect(() => + parse( + dedent` + import { config } from '#.storybook/preview' + const meta = config.meta({ component: 'foo' }); + export default { title: 'atoms/foo' }; + export const A = meta.story({}) + export const B = meta.story({}) + ` + ) + ).toThrowErrorMatchingInlineSnapshot(` + [MultipleMetaError: CSF: multiple meta objects + + More info: https://storybook.js.org/docs/writing-stories#default-export] + `); + }); + + it('bad preview import', () => { + expect(() => + parse( + dedent` + import { config } from '#.storybook/bad-preview' + const meta = config.meta({ component: 'foo' }); + export const A = meta.story({}) + ` + ) + ).toThrowErrorMatchingInlineSnapshot(` + [BadMetaError: CSF: meta() factory must be imported from .storybook/preview configuration (line 1, col 0) + + More info: https://storybook.js.org/docs/writing-stories#default-export] + `); + }); + + it('local defineConfig', () => { + expect(() => + parse( + dedent` + import { defineConfig } from '@storybook/react/preview'; + const config = defineConfig({ }); + const meta = config.meta({ component: 'foo' }); + export const A = meta.story({}) + ` + ) + ).toThrowErrorMatchingInlineSnapshot(` + [BadMetaError: CSF: meta() factory must be imported from .storybook/preview configuration (line 4, col 28) + + More info: https://storybook.js.org/docs/writing-stories#default-export] + `); + }); + + it('mixed factories and non-factories', () => { + expect(() => + parse( + dedent` + import { config } from '#.storybook/preview' + const meta = config.meta({ component: 'foo' }); + export const A = meta.story({}) + export const B = {} + ` + ) + ).toThrowErrorMatchingInlineSnapshot(` + [MixedFactoryError: CSF: expected factory story (line 4, col 17) + + More info: https://storybook.js.org/docs/writing-stories#default-export] + `); + }); + + it('factory stories in non-factory file', () => { + expect(() => + parse( + dedent` + import { meta } from 'somewhere'; + export default { title: 'atoms/foo' }; + export const A = {} + export const B = meta.story({}) + ` + ) + ).toThrowErrorMatchingInlineSnapshot(` + [MixedFactoryError: CSF: expected non-factory story (line 4, col 28) + + More info: https://storybook.js.org/docs/writing-stories#default-export] + `); + }); + }); + }); }); describe('isModuleMock', () => { @@ -2082,3 +2481,22 @@ describe('isModuleMock', () => { expect(isModuleMock('#foo.mock.test.ts')).toBe(false); }); }); + +describe('isValidPreviewPath', () => { + it.each([ + ['#.storybook/preview', true], + ['../../.storybook/preview', true], + ['/path/to/.storybook/preview', true], + ['./preview', true], + ['./preview.ts', true], + ['./preview.tsx', true], + ['./preview.js', true], + ['./preview.jsx', true], + ['./preview.mjs', true], + ['foo', false], + ['#.storybook/bad-preview', false], + ['preview', false], + ])('isValidPreviewPath("%s") === %s', (path, expected) => { + expect(isValidPreviewPath(path)).toBe(expected); + }); +}); diff --git a/code/core/src/csf-tools/CsfFile.ts b/code/core/src/csf-tools/CsfFile.ts index 3465ef090f71..528abdab2323 100644 --- a/code/core/src/csf-tools/CsfFile.ts +++ b/code/core/src/csf-tools/CsfFile.ts @@ -4,6 +4,7 @@ import { readFile, writeFile } from 'node:fs/promises'; import { BabelFileClass, type GeneratorOptions, + type NodePath, type RecastOptions, babelParse, generate, @@ -40,6 +41,9 @@ interface BabelFile { code: string; } +const PREVIEW_FILE_REGEX = /\/preview(.(js|jsx|mjs|ts|tsx))?$/; +export const isValidPreviewPath = (filepath: string) => PREVIEW_FILE_REGEX.test(filepath); + function parseIncludeExclude(prop: t.Node) { if (t.isArrayExpression(prop)) { return prop.elements.map((e) => { @@ -75,8 +79,12 @@ function parseTags(prop: t.Node) { } const formatLocation = (node: t.Node, fileName?: string) => { - const { line, column } = node.loc?.start || {}; - return `${fileName || ''} (line ${line}, col ${column})`.trim(); + let loc = ''; + if (node.loc) { + const { line, column } = node.loc?.start || {}; + loc = `(line ${line}, col ${column})`; + } + return `${fileName || ''} ${loc}`.trim(); }; export const isModuleMock = (importPath: string) => MODULE_MOCK_REGEX.test(importPath); @@ -171,9 +179,46 @@ export interface CsfOptions { export class NoMetaError extends Error { constructor(message: string, ast: t.Node, fileName?: string) { + const msg = ``.trim(); super(dedent` CSF: ${message} ${formatLocation(ast, fileName)} + + More info: https://storybook.js.org/docs/writing-stories#default-export + `); + this.name = this.constructor.name; + } +} +export class MultipleMetaError extends Error { + constructor(message: string, ast: t.Node, fileName?: string) { + const msg = `${message} ${formatLocation(ast, fileName)}`.trim(); + super(dedent` + CSF: ${message} ${formatLocation(ast, fileName)} + + More info: https://storybook.js.org/docs/writing-stories#default-export + `); + this.name = this.constructor.name; + } +} + +export class MixedFactoryError extends Error { + constructor(message: string, ast: t.Node, fileName?: string) { + const msg = `${message} ${formatLocation(ast, fileName)}`.trim(); + super(dedent` + CSF: ${message} ${formatLocation(ast, fileName)} + + More info: https://storybook.js.org/docs/writing-stories#default-export + `); + this.name = this.constructor.name; + } +} + +export class BadMetaError extends Error { + constructor(message: string, ast: t.Node, fileName?: string) { + const msg = ``.trim(); + super(dedent` + CSF: ${message} ${formatLocation(ast, fileName)} + More info: https://storybook.js.org/docs/writing-stories#default-export `); this.name = this.constructor.name; @@ -211,12 +256,18 @@ export class CsfFile { _storyExports: Record = {}; + _storyPaths: Record> = {}; + _metaStatement: t.Statement | undefined; _metaNode: t.Expression | undefined; + _metaPath: NodePath | undefined; + _metaVariableName: string | undefined; + _metaIsFactory: boolean | undefined; + _storyStatements: Record = {}; _storyAnnotations: Record> = {}; @@ -263,6 +314,10 @@ export class CsfFile { } _parseMeta(declaration: t.ObjectExpression, program: t.Program) { + if (this._metaNode) { + throw new MultipleMetaError('multiple meta objects', declaration, this._options.fileName); + } + this._metaNode = declaration; const meta: StaticMeta = {}; (declaration.properties as t.ObjectProperty[]).forEach((p) => { if (t.isIdentifier(p.key)) { @@ -339,6 +394,17 @@ export class CsfFile { const { node, parent } = path; const isVariableReference = t.isIdentifier(node.declaration) && t.isProgram(parent); + /** + * Transform inline default exports into a constant declaration as it is needed for the + * Vitest plugin to compose stories using CSF1 through CSF3 should not be needed at all + * once we move to CSF4 entirely + * + * `export default {};` + * + * Becomes + * + * `const _meta = {}; export default _meta;` + */ if ( self._options.transformInlineMeta && !isVariableReference && @@ -354,6 +420,10 @@ export class CsfFile { // Preserve sourcemaps location nodes.forEach((_node: t.Node) => (_node.loc = path.node.loc)); path.replaceWithMultiple(nodes); + + // This is a bit brittle because it assumes that we will hit the inserted default export + // as the traversal continues. + return; } let metaNode: t.ObjectExpression | undefined; @@ -390,8 +460,7 @@ export class CsfFile { metaNode = decl.expression; } - if (!self._meta && metaNode && t.isProgram(parent)) { - self._metaNode = metaNode; + if (metaNode && t.isProgram(parent)) { self._parseMeta(metaNode, parent); } @@ -402,10 +471,13 @@ export class CsfFile { self._options.fileName ); } + + self._metaPath = path; }, }, ExportNamedDeclaration: { - enter({ node, parent }) { + enter(path) { + const { node, parent } = path; let declarations; if (t.isVariableDeclaration(node.declaration)) { declarations = node.declaration.declarations.filter((d) => t.isVariableDeclarator(d)); @@ -416,12 +488,14 @@ export class CsfFile { // export const X = ...; declarations.forEach((decl: t.VariableDeclarator | t.FunctionDeclaration) => { if (t.isIdentifier(decl.id)) { + let storyIsFactory = false; const { name: exportName } = decl.id; if (exportName === '__namedExportsOrder' && t.isVariableDeclarator(decl)) { self._namedExportsOrder = parseExportsOrder(decl.init as t.Expression); return; } self._storyExports[exportName] = decl; + self._storyPaths[exportName] = path; self._storyStatements[exportName] = node; let name = storyNameFromExport(exportName); if (self._storyAnnotations[exportName]) { @@ -440,6 +514,36 @@ export class CsfFile { } else { storyNode = decl; } + if ( + t.isCallExpression(storyNode) && + t.isMemberExpression(storyNode.callee) && + t.isIdentifier(storyNode.callee.property) && + storyNode.callee.property.name === 'story' + ) { + storyIsFactory = true; + storyNode = storyNode.arguments[0]; + } + if (self._metaIsFactory && !storyIsFactory) { + throw new MixedFactoryError( + 'expected factory story', + storyNode as t.Node, + self._options.fileName + ); + } else if (!self._metaIsFactory && storyIsFactory) { + if (self._metaNode) { + throw new MixedFactoryError( + 'expected non-factory story', + storyNode as t.Node, + self._options.fileName + ); + } else { + throw new BadMetaError( + 'meta() factory must be imported from .storybook/preview configuration', + storyNode as t.Node, + self._options.fileName + ); + } + } const parameters: { [key: string]: any } = {}; if (t.isObjectExpression(storyNode)) { parameters.__isArgsStory = true; // assume default render is an args story @@ -480,7 +584,9 @@ export class CsfFile { id: 'FIXME', name, parameters, - __stats: {}, + __stats: { + factory: storyIsFactory, + }, }; } }); @@ -508,12 +614,13 @@ export class CsfFile { metaNode = decl.expression; } - if (!self._meta && metaNode && t.isProgram(parent)) { + if (metaNode && t.isProgram(parent)) { self._parseMeta(metaNode, parent); } } else { self._storyAnnotations[exportName] = {}; self._storyStatements[exportName] = decl; + self._storyPaths[exportName] = path; self._stories[exportName] = { id: 'FIXME', name: exportName, @@ -570,7 +677,8 @@ export class CsfFile { }, }, CallExpression: { - enter({ node }) { + enter(path) { + const { node } = path; const { callee } = node; if (t.isIdentifier(callee) && callee.name === 'storiesOf') { throw new Error(dedent` @@ -579,6 +687,30 @@ export class CsfFile { SB8 does not support \`storiesOf\`. `); } + if ( + t.isMemberExpression(callee) && + t.isIdentifier(callee.property) && + callee.property.name === 'meta' && + t.isIdentifier(callee.object) && + node.arguments.length > 0 + ) { + const configCandidate = path.scope.getBinding(callee.object.name); + const configParent = configCandidate?.path?.parentPath?.node; + if (t.isImportDeclaration(configParent)) { + if (isValidPreviewPath(configParent.source.value)) { + const metaNode = node.arguments[0] as t.ObjectExpression; + self._metaVariableName = callee.property.name; + self._metaIsFactory = true; + self._parseMeta(metaNode, self._ast.program); + } else { + throw new BadMetaError( + 'meta() factory must be imported from .storybook/preview configuration', + configParent, + self._options.fileName + ); + } + } + } }, }, ImportDeclaration: { diff --git a/code/core/src/csf-tools/enrichCsf.test.ts b/code/core/src/csf-tools/enrichCsf.test.ts index e8c0ce6250eb..a54a6373f489 100644 --- a/code/core/src/csf-tools/enrichCsf.test.ts +++ b/code/core/src/csf-tools/enrichCsf.test.ts @@ -149,6 +149,51 @@ describe('enrichCsf', () => { }; `); }); + it('csf factories', () => { + expect( + enrich( + dedent` + // compiled code + import {config} from "/.storybook/preview.ts"; + const meta = config.meta({ + args: { + label: "Hello world!" + } + }); + export const Story = meta.story({}); + `, + dedent` + // original code + import {config} from "#.storybook/preview.ts"; + const meta = config.meta({ + args: { + label: "Hello world!" + } + }); + export const Story = meta.story({}); + ` + ) + ).toMatchInlineSnapshot(` + // compiled code + import { config } from "/.storybook/preview.ts"; + const meta = config.meta({ + args: { + label: "Hello world!" + } + }); + export const Story = meta.story({}); + Story.input.parameters = { + ...Story.input.parameters, + docs: { + ...Story.input.parameters?.docs, + source: { + originalSource: "meta.story({})", + ...Story.input.parameters?.docs?.source + } + } + }; + `); + }); it('multiple stories', () => { expect( enrich( diff --git a/code/core/src/csf-tools/enrichCsf.ts b/code/core/src/csf-tools/enrichCsf.ts index aa4a205e6bf3..e920a2e816da 100644 --- a/code/core/src/csf-tools/enrichCsf.ts +++ b/code/core/src/csf-tools/enrichCsf.ts @@ -15,11 +15,20 @@ export const enrichCsfStory = ( options?: EnrichCsfOptions ) => { const storyExport = csfSource.getStoryExport(key); + const isCsfFactory = + t.isCallExpression(storyExport) && + t.isMemberExpression(storyExport.callee) && + t.isIdentifier(storyExport.callee.object) && + storyExport.callee.object.name === 'meta'; const source = !options?.disableSource && extractSource(storyExport); const description = !options?.disableDescription && extractDescription(csfSource._storyStatements[key]); const parameters = []; - const originalParameters = t.memberExpression(t.identifier(key), t.identifier('parameters')); + // in csf 1/2/3 use Story.parameters; CSF factories use Story.input.parameters + const baseStoryObject = isCsfFactory + ? t.memberExpression(t.identifier(key), t.identifier('input')) + : t.identifier(key); + const originalParameters = t.memberExpression(baseStoryObject, t.identifier('parameters')); parameters.push(t.spreadElement(originalParameters)); const optionalDocs = t.optionalMemberExpression( originalParameters, diff --git a/code/core/src/csf-tools/getStorySortParameter.test.ts b/code/core/src/csf-tools/getStorySortParameter.test.ts index 565e0e10fdf2..ea468abd19ba 100644 --- a/code/core/src/csf-tools/getStorySortParameter.test.ts +++ b/code/core/src/csf-tools/getStorySortParameter.test.ts @@ -477,6 +477,51 @@ export default { } `); }); + describe('csf factories', () => { + it('inline storysort in default export', () => { + expect( + getStorySortParameter(dedent` + export default definePreview({ + parameters: { + options: { + storySort: { + order: ['General'] + } + }, + }, + }); + `) + ).toMatchInlineSnapshot(` + { + "order": [ + "General", + ], + } + `); + }); + it('variable reference in default export', () => { + expect( + getStorySortParameter(dedent` + const parameters = { + options: { + storySort: { + order: ['General'] + } + }, + }; + export default definePreview({ + parameters, + }); + `) + ).toMatchInlineSnapshot(` + { + "order": [ + "General", + ], + } + `); + }); + }); }); describe('unsupported', () => { it('bad default export', () => { diff --git a/code/core/src/csf-tools/getStorySortParameter.ts b/code/core/src/csf-tools/getStorySortParameter.ts index 4615ca64ce0e..4659859e1c77 100644 --- a/code/core/src/csf-tools/getStorySortParameter.ts +++ b/code/core/src/csf-tools/getStorySortParameter.ts @@ -131,7 +131,10 @@ export const getStorySortParameter = (previewCode: string) => { defaultObj = findVarInitialization(defaultObj.name, ast.program); } defaultObj = stripTSModifiers(defaultObj); - if (t.isObjectExpression(defaultObj)) { + // parse the call arg when using definePreview({ ... }) + if (t.isCallExpression(defaultObj) && t.isObjectExpression(defaultObj.arguments?.[0])) { + storySort = parseDefault(defaultObj.arguments[0], ast.program); + } else if (t.isObjectExpression(defaultObj)) { storySort = parseDefault(defaultObj, ast.program); } else { unsupported('default', false); diff --git a/code/core/src/csf-tools/vitest-plugin/transformer.test.ts b/code/core/src/csf-tools/vitest-plugin/transformer.test.ts index 5b030ac19c73..b475380df2e4 100644 --- a/code/core/src/csf-tools/vitest-plugin/transformer.test.ts +++ b/code/core/src/csf-tools/vitest-plugin/transformer.test.ts @@ -58,280 +58,653 @@ describe('transformer', () => { }); }); - describe('default exports (meta)', () => { - it('should add title to inline default export if not present', async () => { - const code = ` - export default { - component: Button, - }; - export const Story = {}; - `; + describe('CSF v1/v2/v3', () => { + describe('default exports (meta)', () => { + it('should add title to inline default export if not present', async () => { + const code = ` + export default { + component: Button, + }; + export const Story = {}; + `; - const result = await transform({ code }); + const result = await transform({ code }); - expect(getStoryTitle).toHaveBeenCalled(); + expect(getStoryTitle).toHaveBeenCalled(); - expect(result.code).toMatchInlineSnapshot(` - import { test as _test, expect as _expect } from "vitest"; - import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; - const _meta = { - component: Button, - title: "automatic/calculated/title" - }; - export default _meta; - export const Story = {}; - const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); - if (_isRunningFromThisFile) { - _test("Story", _testStory("Story", Story, _meta, [])); - } - `); + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + const _meta = { + component: Button, + title: "automatic/calculated/title" + }; + export default _meta; + export const Story = {}; + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("Story", _testStory("Story", Story, _meta, [])); + } + `); + }); + + it('should overwrite title to inline default export if already present', async () => { + const code = ` + export default { + title: 'Button', + component: Button, + }; + export const Story = {}; + `; + + const result = await transform({ code }); + + expect(getStoryTitle).toHaveBeenCalled(); + + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + const _meta = { + title: "automatic/calculated/title", + component: Button + }; + export default _meta; + export const Story = {}; + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("Story", _testStory("Story", Story, _meta, [])); + } + `); + }); + + it('should add title to const declared default export if not present', async () => { + const code = ` + const meta = { + component: Button, + }; + export default meta; + + export const Story = {}; + `; + + const result = await transform({ code }); + + expect(getStoryTitle).toHaveBeenCalled(); + + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + const meta = { + component: Button, + title: "automatic/calculated/title" + }; + export default meta; + export const Story = {}; + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("Story", _testStory("Story", Story, meta, [])); + } + `); + }); + + it('should overwrite title to const declared default export if already present', async () => { + const code = ` + const meta = { + title: 'Button', + component: Button, + }; + export default meta; + + export const Story = {}; + `; + + const result = await transform({ code }); + + expect(getStoryTitle).toHaveBeenCalled(); + + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + const meta = { + title: "automatic/calculated/title", + component: Button + }; + export default meta; + export const Story = {}; + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("Story", _testStory("Story", Story, meta, [])); + } + `); + }); }); - it('should overwrite title to inline default export if already present', async () => { - const code = ` - export default { - title: 'Button', - component: Button, - }; - export const Story = {}; - `; + describe('named exports (stories)', () => { + it('should add test statement to inline exported stories', async () => { + const code = ` + export default { + component: Button, + } + export const Primary = { + args: { + label: 'Primary Button', + }, + }; + `; - const result = await transform({ code }); + const result = await transform({ code }); - expect(getStoryTitle).toHaveBeenCalled(); + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + const _meta = { + component: Button, + title: "automatic/calculated/title" + }; + export default _meta; + export const Primary = { + args: { + label: 'Primary Button' + } + }; + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("Primary", _testStory("Primary", Primary, _meta, [])); + } + `); + }); - expect(result.code).toMatchInlineSnapshot(` - import { test as _test, expect as _expect } from "vitest"; - import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; - const _meta = { - title: "automatic/calculated/title", - component: Button - }; - export default _meta; - export const Story = {}; - const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); - if (_isRunningFromThisFile) { - _test("Story", _testStory("Story", Story, _meta, [])); - } - `); + describe("use the story's name as test title", () => { + it('should support CSF v3 via name property', async () => { + const code = ` + export default { component: Button } + export const Primary = { name: "custom name" };`; + const result = await transform({ code }); + + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + const _meta = { + component: Button, + title: "automatic/calculated/title" + }; + export default _meta; + export const Primary = { + name: "custom name" + }; + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("custom name", _testStory("Primary", Primary, _meta, [])); + } + `); + }); + + it('should support CSF v1/v2 via storyName property', async () => { + const code = ` + export default { component: Button } + export const Story = () => {} + Story.storyName = 'custom name';`; + const result = await transform({ code: code }); + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + const _meta = { + component: Button, + title: "automatic/calculated/title" + }; + export default _meta; + export const Story = () => {}; + Story.storyName = 'custom name'; + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("custom name", _testStory("Story", Story, _meta, [])); + } + `); + }); + }); + + it('should add test statement to const declared exported stories', async () => { + const code = ` + export default {}; + const Primary = { + args: { + label: 'Primary Button', + }, + }; + + export { Primary }; + `; + + const result = await transform({ code }); + + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + const _meta = { + title: "automatic/calculated/title" + }; + export default _meta; + const Primary = { + args: { + label: 'Primary Button' + } + }; + export { Primary }; + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("Primary", _testStory("Primary", Primary, _meta, [])); + } + `); + }); + + it('should add test statement to const declared renamed exported stories', async () => { + const code = ` + export default {}; + const Primary = { + args: { + label: 'Primary Button', + }, + }; + + export { Primary as PrimaryStory }; + `; + + const result = await transform({ code }); + + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + const _meta = { + title: "automatic/calculated/title" + }; + export default _meta; + const Primary = { + args: { + label: 'Primary Button' + } + }; + export { Primary as PrimaryStory }; + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("PrimaryStory", _testStory("PrimaryStory", Primary, _meta, [])); + } + `); + }); + + it('should add tests for multiple stories', async () => { + const code = ` + export default {}; + const Primary = { + args: { + label: 'Primary Button', + }, + }; + + export const Secondary = {} + + export { Primary }; + `; + + const result = await transform({ code }); + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + const _meta = { + title: "automatic/calculated/title" + }; + export default _meta; + const Primary = { + args: { + label: 'Primary Button' + } + }; + export const Secondary = {}; + export { Primary }; + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("Secondary", _testStory("Secondary", Secondary, _meta, [])); + _test("Primary", _testStory("Primary", Primary, _meta, [])); + } + `); + }); + + it('should exclude exports via excludeStories', async () => { + const code = ` + export default { + title: 'Button', + component: Button, + excludeStories: ['nonStory'], + } + export const Story = {}; + export const nonStory = 123 + `; + + const result = await transform({ code }); + + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + const _meta = { + title: "automatic/calculated/title", + component: Button, + excludeStories: ['nonStory'] + }; + export default _meta; + export const Story = {}; + export const nonStory = 123; + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("Story", _testStory("Story", Story, _meta, [])); + } + `); + }); + + it('should return a describe with skip if there are no valid stories', async () => { + const code = ` + export default { + title: 'Button', + component: Button, + tags: ['!test'] + } + export const Story = {} + `; + const result = await transform({ code }); + + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, describe as _describe } from "vitest"; + const _meta = { + title: "automatic/calculated/title", + component: Button, + tags: ['!test'] + }; + export default _meta; + export const Story = {}; + _describe.skip("No valid tests found"); + `); + }); }); - it('should add title to const declared default export if not present', async () => { - const code = ` - const meta = { - component: Button, - }; - export default meta; + describe('tags filtering mechanism', () => { + it('should only include stories from tags.include', async () => { + const code = ` + export default {}; + export const Included = { tags: ['include-me'] }; + + export const NotIncluded = {} + `; - export const Story = {}; - `; + const result = await transform({ + code, + tagsFilter: { include: ['include-me'], exclude: [], skip: [] }, + }); - const result = await transform({ code }); + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + const _meta = { + title: "automatic/calculated/title" + }; + export default _meta; + export const Included = { + tags: ['include-me'] + }; + export const NotIncluded = {}; + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("Included", _testStory("Included", Included, _meta, [])); + } + `); + }); - expect(getStoryTitle).toHaveBeenCalled(); + it('should exclude stories from tags.exclude', async () => { + const code = ` + export default {}; + export const Included = {}; + + export const NotIncluded = { tags: ['exclude-me'] } + `; - expect(result.code).toMatchInlineSnapshot(` - import { test as _test, expect as _expect } from "vitest"; - import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; - const meta = { - component: Button, - title: "automatic/calculated/title" - }; - export default meta; - export const Story = {}; - const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); - if (_isRunningFromThisFile) { - _test("Story", _testStory("Story", Story, meta, [])); - } - `); + const result = await transform({ + code, + tagsFilter: { include: ['test'], exclude: ['exclude-me'], skip: [] }, + }); + + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + const _meta = { + title: "automatic/calculated/title" + }; + export default _meta; + export const Included = {}; + export const NotIncluded = { + tags: ['exclude-me'] + }; + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("Included", _testStory("Included", Included, _meta, [])); + } + `); + }); + + it('should pass skip tags to testStory call using tags.skip', async () => { + const code = ` + export default {}; + export const Skipped = { tags: ['skip-me'] }; + `; + + const result = await transform({ + code, + tagsFilter: { include: ['test'], exclude: [], skip: ['skip-me'] }, + }); + + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + const _meta = { + title: "automatic/calculated/title" + }; + export default _meta; + export const Skipped = { + tags: ['skip-me'] + }; + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("Skipped", _testStory("Skipped", Skipped, _meta, ["skip-me"])); + } + `); + }); }); - it('should overwrite title to const declared default export if already present', async () => { - const code = ` - const meta = { - title: 'Button', - component: Button, - }; - export default meta; - - export const Story = {}; - `; + describe('source map calculation', () => { + it('should remap the location of an inline named export to its relative testStory function', async () => { + const originalCode = ` + const meta = { + title: 'Button', + component: Button, + } + export default meta; + export const Primary = {}; + `; - const result = await transform({ code }); + const { code: transformedCode, map } = await transform({ + code: originalCode, + }); - expect(getStoryTitle).toHaveBeenCalled(); + expect(transformedCode).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + const meta = { + title: "automatic/calculated/title", + component: Button + }; + export default meta; + export const Primary = {}; + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("Primary", _testStory("Primary", Primary, meta, [])); + } + `); - expect(result.code).toMatchInlineSnapshot(` - import { test as _test, expect as _expect } from "vitest"; - import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; - const meta = { - title: "automatic/calculated/title", - component: Button - }; - export default meta; - export const Story = {}; - const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); - if (_isRunningFromThisFile) { - _test("Story", _testStory("Story", Story, meta, [])); - } - `); + const consumer = await new SourceMapConsumer(map as unknown as RawSourceMap); + + // Locate `__test("Primary"...` in the transformed code + const testPrimaryLine = + transformedCode.split('\n').findIndex((line) => line.includes('_test("Primary"')) + 1; + const testPrimaryColumn = transformedCode + .split('\n') + [testPrimaryLine - 1].indexOf('_test("Primary"'); + + // Get the original position from the source map for `__test("Primary"...` + const originalPosition = consumer.originalPositionFor({ + line: testPrimaryLine, + column: testPrimaryColumn, + }); + + // Locate `export const Primary` in the original code + const originalPrimaryLine = + originalCode.split('\n').findIndex((line) => line.includes('export const Primary')) + 1; + const originalPrimaryColumn = originalCode + .split('\n') + [originalPrimaryLine - 1].indexOf('export const Primary'); + + // The original locations of the transformed code should match with the ones of the original code + expect(originalPosition.line, 'original line location').toBe(originalPrimaryLine); + expect(originalPosition.column, 'original column location').toBe(originalPrimaryColumn); + }); }); }); - describe('named exports (stories)', () => { - it('should add test statement to inline exported stories', async () => { - const code = ` - export default { - component: Button, - } - export const Primary = { - args: { - label: 'Primary Button', - }, - }; + describe('CSF Factories', () => { + describe('default exports (meta)', () => { + it('should add title to inline default export if not present', async () => { + const code = ` + import { config } from '#.storybook/preview'; + const meta = config.meta({ component: Button }); + export const Story = meta.story({}); `; - const result = await transform({ code }); - - expect(result.code).toMatchInlineSnapshot(` - import { test as _test, expect as _expect } from "vitest"; - import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; - const _meta = { - component: Button, - title: "automatic/calculated/title" - }; - export default _meta; - export const Primary = { - args: { - label: 'Primary Button' - } - }; - const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); - if (_isRunningFromThisFile) { - _test("Primary", _testStory("Primary", Primary, _meta, [])); - } - `); - }); - - describe("use the story's name as test title", () => { - it('should support CSF v3 via name property', async () => { - const code = ` - export default { component: Button } - export const Primary = { name: "custom name" };`; const result = await transform({ code }); + expect(getStoryTitle).toHaveBeenCalled(); + expect(result.code).toMatchInlineSnapshot(` import { test as _test, expect as _expect } from "vitest"; import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; - const _meta = { + import { config } from '#.storybook/preview'; + const meta = config.meta({ component: Button, title: "automatic/calculated/title" - }; - export default _meta; - export const Primary = { - name: "custom name" - }; + }); + export const Story = meta.story({}); const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); if (_isRunningFromThisFile) { - _test("custom name", _testStory("Primary", Primary, _meta, [])); + _test("Story", _testStory("Story", Story, meta, [])); } `); }); + }); - it('should support CSF v1/v2 via storyName property', async () => { + describe('named exports (stories)', () => { + it("should use the story's name as test title", async () => { const code = ` - export default { component: Button } - export const Story = () => {} - Story.storyName = 'custom name';`; - const result = await transform({ code: code }); + import { config } from '#.storybook/preview'; + const meta = config.meta({ component: Button }); + export const Primary = meta.story({ name: "custom name" });`; + const result = await transform({ code }); + expect(result.code).toMatchInlineSnapshot(` import { test as _test, expect as _expect } from "vitest"; import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; - const _meta = { + import { config } from '#.storybook/preview'; + const meta = config.meta({ component: Button, title: "automatic/calculated/title" - }; - export default _meta; - export const Story = () => {}; - Story.storyName = 'custom name'; + }); + export const Primary = meta.story({ + name: "custom name" + }); const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); if (_isRunningFromThisFile) { - _test("custom name", _testStory("Story", Story, _meta, [])); + _test("custom name", _testStory("Primary", Primary, meta, [])); } `); }); - }); - it('should add test statement to const declared exported stories', async () => { - const code = ` - export default {}; - const Primary = { + it('should add test statement to const declared exported stories', async () => { + const code = ` + import { config } from '#.storybook/preview'; + const meta = config.meta({ component: Button }); + const Primary = meta.story({ args: { label: 'Primary Button', - }, - }; + } + }); export { Primary }; `; - const result = await transform({ code }); + const result = await transform({ code }); - expect(result.code).toMatchInlineSnapshot(` - import { test as _test, expect as _expect } from "vitest"; - import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; - const _meta = { - title: "automatic/calculated/title" - }; - export default _meta; - const Primary = { - args: { - label: 'Primary Button' + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + import { config } from '#.storybook/preview'; + const meta = config.meta({ + component: Button, + title: "automatic/calculated/title" + }); + const Primary = meta.story({ + args: { + label: 'Primary Button' + } + }); + export { Primary }; + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("Primary", _testStory("Primary", Primary, meta, [])); } - }; - export { Primary }; - const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); - if (_isRunningFromThisFile) { - _test("Primary", _testStory("Primary", Primary, _meta, [])); - } - `); - }); + `); + }); - it('should add test statement to const declared renamed exported stories', async () => { - const code = ` - export default {}; - const Primary = { + it('should add test statement to const declared renamed exported stories', async () => { + const code = ` + import { config } from '#.storybook/preview'; + const meta = config.meta({ component: Button }); + const Primary = meta.story({ args: { label: 'Primary Button', - }, - }; + } + }); export { Primary as PrimaryStory }; `; - const result = await transform({ code }); + const result = await transform({ code }); - expect(result.code).toMatchInlineSnapshot(` - import { test as _test, expect as _expect } from "vitest"; - import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; - const _meta = { - title: "automatic/calculated/title" - }; - export default _meta; - const Primary = { - args: { - label: 'Primary Button' + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + import { config } from '#.storybook/preview'; + const meta = config.meta({ + component: Button, + title: "automatic/calculated/title" + }); + const Primary = meta.story({ + args: { + label: 'Primary Button' + } + }); + export { Primary as PrimaryStory }; + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("PrimaryStory", _testStory("PrimaryStory", Primary, meta, [])); } - }; - export { Primary as PrimaryStory }; - const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); - if (_isRunningFromThisFile) { - _test("PrimaryStory", _testStory("PrimaryStory", Primary, _meta, [])); - } - `); - }); + `); + }); - it('should add tests for multiple stories', async () => { - const code = ` + it('should add tests for multiple stories', async () => { + const code = ` export default {}; const Primary = { args: { @@ -344,8 +717,8 @@ describe('transformer', () => { export { Primary }; `; - const result = await transform({ code }); - expect(result.code).toMatchInlineSnapshot(` + const result = await transform({ code }); + expect(result.code).toMatchInlineSnapshot(` import { test as _test, expect as _expect } from "vitest"; import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; const _meta = { @@ -365,10 +738,10 @@ describe('transformer', () => { _test("Primary", _testStory("Primary", Primary, _meta, [])); } `); - }); + }); - it('should exclude exports via excludeStories', async () => { - const code = ` + it('should exclude exports via excludeStories', async () => { + const code = ` export default { title: 'Button', component: Button, @@ -378,9 +751,9 @@ describe('transformer', () => { export const nonStory = 123 `; - const result = await transform({ code }); + const result = await transform({ code }); - expect(result.code).toMatchInlineSnapshot(` + expect(result.code).toMatchInlineSnapshot(` import { test as _test, expect as _expect } from "vitest"; import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; const _meta = { @@ -396,10 +769,10 @@ describe('transformer', () => { _test("Story", _testStory("Story", Story, _meta, [])); } `); - }); + }); - it('should return a describe with skip if there are no valid stories', async () => { - const code = ` + it('should return a describe with skip if there are no valid stories', async () => { + const code = ` export default { title: 'Button', component: Button, @@ -407,9 +780,9 @@ describe('transformer', () => { } export const Story = {} `; - const result = await transform({ code }); + const result = await transform({ code }); - expect(result.code).toMatchInlineSnapshot(` + expect(result.code).toMatchInlineSnapshot(` import { test as _test, describe as _describe } from "vitest"; const _meta = { title: "automatic/calculated/title", @@ -420,156 +793,156 @@ describe('transformer', () => { export const Story = {}; _describe.skip("No valid tests found"); `); + }); }); - }); - describe('tags filtering mechanism', () => { - it('should only include stories from tags.include', async () => { - const code = ` - export default {}; - export const Included = { tags: ['include-me'] }; + describe('tags filtering mechanism', () => { + it('should only include stories from tags.include', async () => { + const code = ` + import { config } from '#.storybook/preview'; + const meta = config.meta({}); + export const Included = meta.story({ tags: ['include-me'] }); - export const NotIncluded = {} + export const NotIncluded = meta.story({}); `; - const result = await transform({ - code, - tagsFilter: { include: ['include-me'], exclude: [], skip: [] }, + const result = await transform({ + code, + tagsFilter: { include: ['include-me'], exclude: [], skip: [] }, + }); + + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + import { config } from '#.storybook/preview'; + const meta = config.meta({ + title: "automatic/calculated/title" + }); + export const Included = meta.story({ + tags: ['include-me'] + }); + export const NotIncluded = meta.story({}); + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("Included", _testStory("Included", Included, meta, [])); + } + `); }); - expect(result.code).toMatchInlineSnapshot(` - import { test as _test, expect as _expect } from "vitest"; - import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; - const _meta = { - title: "automatic/calculated/title" - }; - export default _meta; - export const Included = { - tags: ['include-me'] - }; - export const NotIncluded = {}; - const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); - if (_isRunningFromThisFile) { - _test("Included", _testStory("Included", Included, _meta, [])); - } - `); - }); + it('should exclude stories from tags.exclude', async () => { + const code = ` + import { config } from '#.storybook/preview'; + const meta = config.meta({}); + export const Included = meta.story({}); - it('should exclude stories from tags.exclude', async () => { - const code = ` - export default {}; - export const Included = {}; + export const NotIncluded = meta.story({ tags: ['exclude-me'] }); + `; - export const NotIncluded = { tags: ['exclude-me'] } - `; + const result = await transform({ + code, + tagsFilter: { include: ['test'], exclude: ['exclude-me'], skip: [] }, + }); - const result = await transform({ - code, - tagsFilter: { include: ['test'], exclude: ['exclude-me'], skip: [] }, + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + import { config } from '#.storybook/preview'; + const meta = config.meta({ + title: "automatic/calculated/title" + }); + export const Included = meta.story({}); + export const NotIncluded = meta.story({ + tags: ['exclude-me'] + }); + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("Included", _testStory("Included", Included, meta, [])); + } + `); }); - expect(result.code).toMatchInlineSnapshot(` - import { test as _test, expect as _expect } from "vitest"; - import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; - const _meta = { - title: "automatic/calculated/title" - }; - export default _meta; - export const Included = {}; - export const NotIncluded = { - tags: ['exclude-me'] - }; - const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); - if (_isRunningFromThisFile) { - _test("Included", _testStory("Included", Included, _meta, [])); - } - `); - }); - - it('should pass skip tags to testStory call using tags.skip', async () => { - const code = ` - export default {}; - export const Skipped = { tags: ['skip-me'] }; + it('should pass skip tags to testStory call using tags.skip', async () => { + const code = ` + import { config } from '#.storybook/preview'; + const meta = config.meta({}); + export const Skipped = meta.story({ tags: ['skip-me'] }); `; - const result = await transform({ - code, - tagsFilter: { include: ['test'], exclude: [], skip: ['skip-me'] }, - }); + const result = await transform({ + code, + tagsFilter: { include: ['test'], exclude: [], skip: ['skip-me'] }, + }); - expect(result.code).toMatchInlineSnapshot(` - import { test as _test, expect as _expect } from "vitest"; - import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; - const _meta = { - title: "automatic/calculated/title" - }; - export default _meta; - export const Skipped = { - tags: ['skip-me'] - }; - const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); - if (_isRunningFromThisFile) { - _test("Skipped", _testStory("Skipped", Skipped, _meta, ["skip-me"])); - } - `); + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + import { config } from '#.storybook/preview'; + const meta = config.meta({ + title: "automatic/calculated/title" + }); + export const Skipped = meta.story({ + tags: ['skip-me'] + }); + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("Skipped", _testStory("Skipped", Skipped, meta, ["skip-me"])); + } + `); + }); }); - }); - describe('source map calculation', () => { - it('should remap the location of an inline named export to its relative testStory function', async () => { - const originalCode = ` - const meta = { - title: 'Button', - component: Button, - } - export default meta; - export const Primary = {}; + describe('source map calculation', () => { + it('should remap the location of an inline named export to its relative testStory function', async () => { + const originalCode = ` + import { config } from '#.storybook/preview'; + const meta = config.meta({}); + export const Primary = meta.story({}); `; - const { code: transformedCode, map } = await transform({ - code: originalCode, - }); - - expect(transformedCode).toMatchInlineSnapshot(` - import { test as _test, expect as _expect } from "vitest"; - import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; - const meta = { - title: "automatic/calculated/title", - component: Button - }; - export default meta; - export const Primary = {}; - const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); - if (_isRunningFromThisFile) { - _test("Primary", _testStory("Primary", Primary, meta, [])); - } - `); - - const consumer = await new SourceMapConsumer(map as unknown as RawSourceMap); + const { code: transformedCode, map } = await transform({ + code: originalCode, + }); - // Locate `__test("Primary"...` in the transformed code - const testPrimaryLine = - transformedCode.split('\n').findIndex((line) => line.includes('_test("Primary"')) + 1; - const testPrimaryColumn = transformedCode - .split('\n') - [testPrimaryLine - 1].indexOf('_test("Primary"'); + expect(transformedCode).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + import { config } from '#.storybook/preview'; + const meta = config.meta({ + title: "automatic/calculated/title" + }); + export const Primary = meta.story({}); + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("Primary", _testStory("Primary", Primary, meta, [])); + } + `); - // Get the original position from the source map for `__test("Primary"...` - const originalPosition = consumer.originalPositionFor({ - line: testPrimaryLine, - column: testPrimaryColumn, + const consumer = await new SourceMapConsumer(map as unknown as RawSourceMap); + + // Locate `__test("Primary"...` in the transformed code + const testPrimaryLine = + transformedCode.split('\n').findIndex((line) => line.includes('_test("Primary"')) + 1; + const testPrimaryColumn = transformedCode + .split('\n') + [testPrimaryLine - 1].indexOf('_test("Primary"'); + + // Get the original position from the source map for `__test("Primary"...` + const originalPosition = consumer.originalPositionFor({ + line: testPrimaryLine, + column: testPrimaryColumn, + }); + + // Locate `export const Primary` in the original code + const originalPrimaryLine = + originalCode.split('\n').findIndex((line) => line.includes('export const Primary')) + 1; + const originalPrimaryColumn = originalCode + .split('\n') + [originalPrimaryLine - 1].indexOf('export const Primary'); + + // The original locations of the transformed code should match with the ones of the original code + expect(originalPosition.line, 'original line location').toBe(originalPrimaryLine); + expect(originalPosition.column, 'original column location').toBe(originalPrimaryColumn); }); - - // Locate `export const Primary` in the original code - const originalPrimaryLine = - originalCode.split('\n').findIndex((line) => line.includes('export const Primary')) + 1; - const originalPrimaryColumn = originalCode - .split('\n') - [originalPrimaryLine - 1].indexOf('export const Primary'); - - // The original locations of the transformed code should match with the ones of the original code - expect(originalPosition.line, 'original line location').toBe(originalPrimaryLine); - expect(originalPosition.column, 'original column location').toBe(originalPrimaryColumn); }); }); diff --git a/code/core/src/csf/csf-factories.ts b/code/core/src/csf/csf-factories.ts new file mode 100644 index 000000000000..0c0ea4cbe21a --- /dev/null +++ b/code/core/src/csf/csf-factories.ts @@ -0,0 +1,96 @@ +/* eslint-disable no-underscore-dangle */ +import type { + Args, + ComponentAnnotations, + NormalizedComponentAnnotations, + NormalizedProjectAnnotations, + NormalizedStoryAnnotations, + ProjectAnnotations, + Renderer, + StoryAnnotations, +} from '@storybook/core/types'; + +import { composeConfigs, normalizeProjectAnnotations } from '@storybook/core/preview-api'; + +export interface Preview { + readonly _tag: 'Preview'; + input: ProjectAnnotations; + composed: NormalizedProjectAnnotations; + + meta(input: ComponentAnnotations): Meta; +} + +export function definePreview( + preview: Preview['input'] +): Preview { + return { + _tag: 'Preview', + input: preview, + get composed() { + const { addons, ...rest } = preview; + return normalizeProjectAnnotations(composeConfigs([...(addons ?? []), rest])); + }, + meta(meta: ComponentAnnotations) { + return defineMeta(meta, this); + }, + }; +} + +export function isPreview(input: unknown): input is Preview { + return input != null && typeof input === 'object' && '_tag' in input && input?._tag === 'Preview'; +} + +export interface Meta { + readonly _tag: 'Meta'; + input: ComponentAnnotations; + composed: NormalizedComponentAnnotations; + preview: Preview; + + story(input: ComponentAnnotations): Story; +} + +export function isMeta(input: unknown): input is Meta { + return input != null && typeof input === 'object' && '_tag' in input && input?._tag === 'Meta'; +} + +function defineMeta( + input: ComponentAnnotations, + preview: Preview +): Meta { + return { + _tag: 'Meta', + input, + preview, + get composed(): never { + throw new Error('Not implemented'); + }, + story(story: StoryAnnotations) { + return defineStory(story, this); + }, + }; +} + +export interface Story { + readonly _tag: 'Story'; + input: StoryAnnotations; + composed: NormalizedStoryAnnotations; + meta: Meta; +} + +function defineStory( + input: ComponentAnnotations, + meta: Meta +): Story { + return { + _tag: 'Story', + input, + meta, + get composed(): never { + throw new Error('Not implemented'); + }, + }; +} + +export function isStory(input: unknown): input is Story { + return input != null && typeof input === 'object' && '_tag' in input && input?._tag === 'Story'; +} diff --git a/code/core/src/csf/index.ts b/code/core/src/csf/index.ts index 4c8bc2b44a01..4997de1b3705 100644 --- a/code/core/src/csf/index.ts +++ b/code/core/src/csf/index.ts @@ -88,3 +88,4 @@ export const combineTags = (...tags: string[]): string[] => { export { includeConditionalArg } from './includeConditionalArg'; export * from './story'; +export * from './csf-factories'; diff --git a/code/core/src/preview-api/index.ts b/code/core/src/preview-api/index.ts index bc57ddbe6ceb..f011f9f500ee 100644 --- a/code/core/src/preview-api/index.ts +++ b/code/core/src/preview-api/index.ts @@ -27,6 +27,9 @@ export { makeDecorator } from './addons'; */ export { addons, mockChannel } from './addons'; +/** ADDON ANNOTATIONS TYPE HELPER */ +export { definePreview } from './addons'; + export { UniversalStore as experimental_UniversalStore } from '../shared/universal-store'; export { useUniversalStore as experimental_useUniversalStore } from '../shared/universal-store/use-universal-store-preview'; @@ -56,9 +59,11 @@ export { userOrAutoTitleFromSpecifier, userOrAutoTitle, sortStoriesV7, + normalizeProjectAnnotations, } from './store'; -export { createPlaywrightTest } from './modules/store/csf/portable-stories'; +/** CSF API */ +export { createPlaywrightTest, getCsfFactoryAnnotations } from './modules/store/csf'; export type { PropDescriptor } from './store'; diff --git a/code/core/src/preview-api/modules/addons/definePreview.ts b/code/core/src/preview-api/modules/addons/definePreview.ts new file mode 100644 index 000000000000..da17e62f9f48 --- /dev/null +++ b/code/core/src/preview-api/modules/addons/definePreview.ts @@ -0,0 +1,5 @@ +import type { ProjectAnnotations, Renderer } from '@storybook/core/types'; + +export function definePreview(config: ProjectAnnotations): ProjectAnnotations { + return config; +} diff --git a/code/core/src/preview-api/modules/addons/index.ts b/code/core/src/preview-api/modules/addons/index.ts index b32933b0c1c9..db494490d7c1 100644 --- a/code/core/src/preview-api/modules/addons/index.ts +++ b/code/core/src/preview-api/modules/addons/index.ts @@ -1,4 +1,5 @@ export * from './main'; +export * from './definePreview'; export * from './hooks'; export * from './make-decorator'; export * from './storybook-channel-mock'; diff --git a/code/core/src/preview-api/modules/preview-web/docs-context/DocsContext.ts b/code/core/src/preview-api/modules/preview-web/docs-context/DocsContext.ts index 6e39643859db..d8d83bed4deb 100644 --- a/code/core/src/preview-api/modules/preview-web/docs-context/DocsContext.ts +++ b/code/core/src/preview-api/modules/preview-web/docs-context/DocsContext.ts @@ -1,4 +1,5 @@ import type { Channel } from '@storybook/core/channels'; +import { isStory } from '@storybook/core/csf'; import type { CSFFile, ModuleExport, @@ -13,7 +14,7 @@ import type { import { dedent } from 'ts-dedent'; -import type { StoryStore } from '../../store'; +import { type StoryStore } from '../../store'; import type { DocsContextProps } from './DocsContextProps'; export class DocsContext implements DocsContextProps { @@ -162,7 +163,9 @@ export class DocsContext implements DocsContextProps return { type: 'meta', csfFile } as TResolvedExport; } - const story = this.exportToStory.get(moduleExportOrType); + const story = this.exportToStory.get( + isStory(moduleExportOrType) ? moduleExportOrType.input : moduleExportOrType + ); if (story) { return { type: 'story', story } as TResolvedExport; diff --git a/code/core/src/preview-api/modules/preview-web/render/mount-utils.ts b/code/core/src/preview-api/modules/preview-web/render/mount-utils.ts index 00510d0f5edb..23fee127a9df 100644 --- a/code/core/src/preview-api/modules/preview-web/render/mount-utils.ts +++ b/code/core/src/preview-api/modules/preview-web/render/mount-utils.ts @@ -1,9 +1,10 @@ // Inspired by Vitest fixture implementation: // https://github.com/vitest-dev/vitest/blob/200a4349a2f85686bc7005dce686d9d1b48b84d2/packages/runner/src/fixture.ts +import type { PlayFunction } from '@storybook/core/csf'; import { type PreparedStory, type Renderer } from '@storybook/core/types'; export function mountDestructured( - playFunction: PreparedStory['playFunction'] + playFunction?: PlayFunction ): boolean { return playFunction != null && getUsedProps(playFunction).includes('mount'); } diff --git a/code/core/src/preview-api/modules/store/StoryStore.ts b/code/core/src/preview-api/modules/store/StoryStore.ts index 9b6362739039..5a9231dd742e 100644 --- a/code/core/src/preview-api/modules/store/StoryStore.ts +++ b/code/core/src/preview-api/modules/store/StoryStore.ts @@ -215,7 +215,7 @@ export class StoryStore { const story = this.prepareStoryWithCache( storyAnnotations, componentAnnotations, - this.projectAnnotations + csfFile.projectAnnotations ?? this.projectAnnotations ); this.args.setInitial(story); this.hooks[story.id] = this.hooks[story.id] || new HooksContext(); diff --git a/code/core/src/preview-api/modules/store/csf/csf-factory-utils.ts b/code/core/src/preview-api/modules/store/csf/csf-factory-utils.ts new file mode 100644 index 000000000000..741a1842d4a5 --- /dev/null +++ b/code/core/src/preview-api/modules/store/csf/csf-factory-utils.ts @@ -0,0 +1,25 @@ +import { isStory } from '@storybook/core/csf'; +import type { + Args, + ComponentAnnotations, + LegacyStoryAnnotationsOrFn, + ProjectAnnotations, + Renderer, +} from '@storybook/core/types'; + +export function getCsfFactoryAnnotations< + TRenderer extends Renderer = Renderer, + TArgs extends Args = Args, +>( + story: LegacyStoryAnnotationsOrFn, + meta?: ComponentAnnotations, + projectAnnotations?: ProjectAnnotations +) { + return isStory(story) + ? { + story: story.input, + meta: story.meta.input, + preview: story.meta.preview.composed, + } + : { story, meta, preview: projectAnnotations }; +} diff --git a/code/core/src/preview-api/modules/store/csf/index.ts b/code/core/src/preview-api/modules/store/csf/index.ts index 3ce7ca25109e..6d997c90903a 100644 --- a/code/core/src/preview-api/modules/store/csf/index.ts +++ b/code/core/src/preview-api/modules/store/csf/index.ts @@ -8,3 +8,4 @@ export * from './getValuesFromArgTypes'; export * from './composeConfigs'; export * from './stepRunners'; export * from './portable-stories'; +export * from './csf-factory-utils'; diff --git a/code/core/src/preview-api/modules/store/csf/portable-stories.ts b/code/core/src/preview-api/modules/store/csf/portable-stories.ts index 3c5c987fb955..e4e43e390a3d 100644 --- a/code/core/src/preview-api/modules/store/csf/portable-stories.ts +++ b/code/core/src/preview-api/modules/store/csf/portable-stories.ts @@ -28,6 +28,7 @@ import { dedent } from 'ts-dedent'; import { HooksContext } from '../../../addons'; import { ReporterAPI } from '../reporter-api'; import { composeConfigs } from './composeConfigs'; +import { getCsfFactoryAnnotations } from './csf-factory-utils'; import { getValuesFromArgTypes } from './getValuesFromArgTypes'; import { normalizeComponentAnnotations } from './normalizeComponentAnnotations'; import { normalizeProjectAnnotations } from './normalizeProjectAnnotations'; @@ -275,22 +276,27 @@ export function composeStories( globalConfig: ProjectAnnotations, composeStoryFn: ComposeStoryFn = defaultComposeStory ) { - const { default: meta, __esModule, __namedExportsOrder, ...stories } = storiesImport; - const composedStories = Object.entries(stories).reduce((storiesMap, [exportsName, story]) => { - if (!isExportStory(exportsName, meta)) { - return storiesMap; - } + const { default: metaExport, __esModule, __namedExportsOrder, ...stories } = storiesImport; + let meta = metaExport; + + const composedStories = Object.entries(stories).reduce( + (storiesMap, [exportsName, story]: [string, any]) => { + const { story: storyAnnotations, meta: componentAnnotations } = + getCsfFactoryAnnotations(story); + if (!meta && componentAnnotations) { + meta = componentAnnotations; + } - const result = Object.assign(storiesMap, { - [exportsName]: composeStoryFn( - story as LegacyStoryAnnotationsOrFn, - meta, - globalConfig, - exportsName - ), - }); - return result; - }, {}); + if (!isExportStory(exportsName, meta)) { + return storiesMap; + } + const result = Object.assign(storiesMap, { + [exportsName]: composeStoryFn(storyAnnotations, meta, globalConfig, exportsName), + }); + return result; + }, + {} + ); return composedStories; } diff --git a/code/core/src/preview-api/modules/store/csf/processCSFFile.ts b/code/core/src/preview-api/modules/store/csf/processCSFFile.ts index d17db9bea3ed..c7c9456ae927 100644 --- a/code/core/src/preview-api/modules/store/csf/processCSFFile.ts +++ b/code/core/src/preview-api/modules/store/csf/processCSFFile.ts @@ -1,4 +1,4 @@ -import { isExportStory } from '@storybook/core/csf'; +import { isExportStory, isStory } from '@storybook/core/csf'; import type { ComponentTitle, Parameters, Path, Renderer } from '@storybook/core/types'; import type { CSFFile, ModuleExports, NormalizedComponentAnnotations } from '@storybook/core/types'; @@ -46,6 +46,28 @@ export function processCSFFile( // eslint-disable-next-line @typescript-eslint/naming-convention const { default: defaultExport, __namedExportsOrder, ...namedExports } = moduleExports; + const firstStory = Object.values(namedExports)[0]; + if (isStory(firstStory)) { + const meta: NormalizedComponentAnnotations = + normalizeComponentAnnotations(firstStory.meta.input, title, importPath); + checkDisallowedParameters(meta.parameters); + + const csfFile: CSFFile = { meta, stories: {}, moduleExports }; + + Object.keys(namedExports).forEach((key) => { + if (isExportStory(key, meta)) { + const storyMeta = normalizeStory(key, namedExports[key].input, meta); + checkDisallowedParameters(storyMeta.parameters); + + csfFile.stories[storyMeta.id] = storyMeta; + } + }); + + csfFile.projectAnnotations = firstStory.meta.preview.composed; + + return csfFile; + } + const meta: NormalizedComponentAnnotations = normalizeComponentAnnotations( defaultExport, title, diff --git a/code/core/src/types/modules/indexer.ts b/code/core/src/types/modules/indexer.ts index 042eb1791a28..12a1e3628b67 100644 --- a/code/core/src/types/modules/indexer.ts +++ b/code/core/src/types/modules/indexer.ts @@ -91,6 +91,7 @@ export interface IndexInputStats { beforeEach?: boolean; moduleMock?: boolean; globals?: boolean; + factory?: boolean; } /** The base input for indexing a story or docs entry. */ diff --git a/code/core/src/types/modules/story.ts b/code/core/src/types/modules/story.ts index a1322174b737..187bf3af7674 100644 --- a/code/core/src/types/modules/story.ts +++ b/code/core/src/types/modules/story.ts @@ -43,6 +43,7 @@ export type RenderToCanvas = ( export interface ProjectAnnotations extends BaseProjectAnnotations { + addons?: ProjectAnnotations[]; testingLibraryRender?: (...args: never[]) => { unmount: () => void }; renderToCanvas?: RenderToCanvas; /* @deprecated use renderToCanvas */ @@ -95,6 +96,7 @@ export type NormalizedStoryAnnotations = export type CSFFile = { meta: NormalizedComponentAnnotations; stories: Record>; + projectAnnotations?: NormalizedProjectAnnotations; moduleExports: ModuleExports; }; diff --git a/code/core/template/stories/preview.ts b/code/core/template/stories/preview.ts index bba2716864bc..483bb2edd360 100644 --- a/code/core/template/stories/preview.ts +++ b/code/core/template/stories/preview.ts @@ -1,5 +1,6 @@ /* eslint-disable no-underscore-dangle */ import type { PartialStoryFn, StoryContext } from '@storybook/core/types'; +import type { ReactRenderer } from '@storybook/react'; declare global { interface Window { @@ -30,7 +31,7 @@ export const parameters = { export const loaders = [async () => ({ projectValue: 2 })]; -const testProjectDecorator = (storyFn: PartialStoryFn, context: StoryContext) => { +const testProjectDecorator = (storyFn: PartialStoryFn, context: StoryContext) => { if (context.parameters.useProjectDecorator) { return storyFn({ args: { ...context.args, text: `project ${context.args.text}` } }); } diff --git a/code/e2e-tests/util.ts b/code/e2e-tests/util.ts index c4f1d8c6848a..b4b69d7f176d 100644 --- a/code/e2e-tests/util.ts +++ b/code/e2e-tests/util.ts @@ -87,6 +87,9 @@ export class SbPage { async waitForStoryLoaded() { try { + // wait for the story to be visited + await this.page.waitForURL((url) => url.search.includes(`path`)); + const root = this.previewRoot(); // Wait until there is at least one child (a story element) in the preview iframe await root.locator(':scope > *').first().waitFor({ diff --git a/code/frameworks/angular/package.json b/code/frameworks/angular/package.json index 6a08f97f2c35..0e029b5559ab 100644 --- a/code/frameworks/angular/package.json +++ b/code/frameworks/angular/package.json @@ -20,6 +20,22 @@ "url": "https://opencollective.com/storybook" }, "license": "MIT", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "node": "./dist/index.js", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./preset": "./preset.js", + "./node": { + "types": "./dist/node/index.d.ts", + "node": "./dist/node/index.js", + "import": "./dist/node/index.js", + "require": "./dist/node/index.js" + }, + "./package.json": "./package.json" + }, "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", diff --git a/code/frameworks/angular/src/node/index.ts b/code/frameworks/angular/src/node/index.ts new file mode 100644 index 000000000000..16fcde688ae7 --- /dev/null +++ b/code/frameworks/angular/src/node/index.ts @@ -0,0 +1,5 @@ +import { StorybookConfig } from '../types'; + +export function defineMain(config: StorybookConfig) { + return config; +} diff --git a/code/frameworks/ember/package.json b/code/frameworks/ember/package.json index cedabd16beea..cc12dce5d31d 100644 --- a/code/frameworks/ember/package.json +++ b/code/frameworks/ember/package.json @@ -16,6 +16,22 @@ "url": "https://opencollective.com/storybook" }, "license": "MIT", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "node": "./dist/index.js", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./preset": "./preset.js", + "./node": { + "types": "./dist/node/index.d.ts", + "node": "./dist/node/index.js", + "import": "./dist/node/index.js", + "require": "./dist/node/index.js" + }, + "./package.json": "./package.json" + }, "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", diff --git a/code/frameworks/ember/src/node/index.ts b/code/frameworks/ember/src/node/index.ts new file mode 100644 index 000000000000..bbfba66cc964 --- /dev/null +++ b/code/frameworks/ember/src/node/index.ts @@ -0,0 +1,5 @@ +import type { StorybookConfig } from '../types'; + +export function defineMain(config: StorybookConfig) { + return config; +} diff --git a/code/frameworks/experimental-nextjs-vite/package.json b/code/frameworks/experimental-nextjs-vite/package.json index 507aa6a8ecb2..7f110ad81e7c 100644 --- a/code/frameworks/experimental-nextjs-vite/package.json +++ b/code/frameworks/experimental-nextjs-vite/package.json @@ -58,6 +58,12 @@ "import": "./dist/vite-plugin/index.mjs", "require": "./dist/vite-plugin/index.js" }, + "./node": { + "types": "./dist/node/index.d.ts", + "node": "./dist/node/index.js", + "import": "./dist/node/index.mjs", + "require": "./dist/node/index.js" + }, "./package.json": "./package.json" }, "main": "dist/index.js", @@ -79,6 +85,9 @@ ], "navigation.mock": [ "dist/export-mocks/navigation/index.d.ts" + ], + "node": [ + "dist/node/index.d.ts" ] } }, @@ -134,6 +143,7 @@ "./src/index.ts", "./src/vite-plugin/index.ts", "./src/preset.ts", + "./src/node/index.ts", "./src/preview.tsx", "./src/export-mocks/cache/index.ts", "./src/export-mocks/headers/index.ts", diff --git a/code/frameworks/experimental-nextjs-vite/src/index.ts b/code/frameworks/experimental-nextjs-vite/src/index.ts index 32476387c88c..f620bc6df0a0 100644 --- a/code/frameworks/experimental-nextjs-vite/src/index.ts +++ b/code/frameworks/experimental-nextjs-vite/src/index.ts @@ -1,5 +1,10 @@ +import type { ReactPreview } from '@storybook/react'; +import { definePreview as definePreviewBase } from '@storybook/react'; + import type vitePluginStorybookNextJs from 'vite-plugin-storybook-nextjs'; +import * as nextPreview from './preview'; + export * from './types'; export * from './portable-stories'; @@ -8,3 +13,12 @@ export * from './portable-stories'; declare module '@storybook/experimental-nextjs-vite/vite-plugin' { export const storybookNextJsPlugin: typeof vitePluginStorybookNextJs; } + +export function definePreview(preview: NextPreview['input']) { + return definePreviewBase({ + ...preview, + addons: [nextPreview, ...(preview.addons ?? [])], + }) as NextPreview; +} + +interface NextPreview extends ReactPreview {} diff --git a/code/frameworks/experimental-nextjs-vite/src/node/index.ts b/code/frameworks/experimental-nextjs-vite/src/node/index.ts new file mode 100644 index 000000000000..bbfba66cc964 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/node/index.ts @@ -0,0 +1,5 @@ +import type { StorybookConfig } from '../types'; + +export function defineMain(config: StorybookConfig) { + return config; +} diff --git a/code/frameworks/experimental-nextjs-vite/src/types.ts b/code/frameworks/experimental-nextjs-vite/src/types.ts index 0221787dccb6..f9e5659d423a 100644 --- a/code/frameworks/experimental-nextjs-vite/src/types.ts +++ b/code/frameworks/experimental-nextjs-vite/src/types.ts @@ -3,6 +3,8 @@ import type { CompatibleString } from 'storybook/internal/types'; import type { BuilderOptions } from '@storybook/builder-vite'; import type { StorybookConfig as StorybookConfigReactVite } from '@storybook/react-vite'; +import type { NextRouter } from 'next/router'; + type FrameworkName = CompatibleString<'@storybook/experimental-nextjs-vite'>; type BuilderName = CompatibleString<'@storybook/builder-vite'>; @@ -32,3 +34,27 @@ type StorybookConfigFramework = { /** The interface for Storybook configuration in `main.ts` files. */ export type StorybookConfig = Omit & StorybookConfigFramework; + +export interface NextJsParameters { + /** + * Next.js framework configuration + * + * @see https://storybook.js.org/docs/get-started/frameworks/nextjs + */ + nextjs?: { + /** + * Enable App Directory features If your story imports components that use next/navigation, you + * need to set this parameter to true + */ + appDirectory?: boolean; + + /** + * Next.js navigation configuration when using `next/navigation`. Please note that it can only + * be used in components/pages in the app directory. + */ + navigation?: NextRouter; + + /** Next.js router configuration */ + router?: NextRouter; + }; +} diff --git a/code/frameworks/html-vite/package.json b/code/frameworks/html-vite/package.json index ee39f2ef1254..961ee7ef0e2c 100644 --- a/code/frameworks/html-vite/package.json +++ b/code/frameworks/html-vite/package.json @@ -30,6 +30,12 @@ "types": "./dist/preset.d.ts", "require": "./dist/preset.js" }, + "./node": { + "types": "./dist/node/index.d.ts", + "node": "./dist/node/index.js", + "import": "./dist/node/index.mjs", + "require": "./dist/node/index.js" + }, "./package.json": "./package.json" }, "main": "dist/index.js", @@ -67,7 +73,8 @@ "bundler": { "entries": [ "./src/index.ts", - "./src/preset.ts" + "./src/preset.ts", + "./src/node/index.ts" ], "platform": "node" }, diff --git a/code/frameworks/html-vite/src/node/index.ts b/code/frameworks/html-vite/src/node/index.ts new file mode 100644 index 000000000000..bbfba66cc964 --- /dev/null +++ b/code/frameworks/html-vite/src/node/index.ts @@ -0,0 +1,5 @@ +import type { StorybookConfig } from '../types'; + +export function defineMain(config: StorybookConfig) { + return config; +} diff --git a/code/frameworks/html-webpack5/package.json b/code/frameworks/html-webpack5/package.json index ec95f517f32d..3d8b15bf669a 100644 --- a/code/frameworks/html-webpack5/package.json +++ b/code/frameworks/html-webpack5/package.json @@ -30,6 +30,12 @@ "types": "./dist/preset.d.ts", "require": "./dist/preset.js" }, + "./node": { + "types": "./dist/node/index.d.ts", + "node": "./dist/node/index.js", + "import": "./dist/node/index.mjs", + "require": "./dist/node/index.js" + }, "./package.json": "./package.json" }, "main": "dist/index.js", @@ -68,7 +74,8 @@ "bundler": { "entries": [ "./src/index.ts", - "./src/preset.ts" + "./src/preset.ts", + "./src/node/index.ts" ], "platform": "node" }, diff --git a/code/frameworks/html-webpack5/src/node/index.ts b/code/frameworks/html-webpack5/src/node/index.ts new file mode 100644 index 000000000000..bbfba66cc964 --- /dev/null +++ b/code/frameworks/html-webpack5/src/node/index.ts @@ -0,0 +1,5 @@ +import type { StorybookConfig } from '../types'; + +export function defineMain(config: StorybookConfig) { + return config; +} diff --git a/code/frameworks/nextjs/package.json b/code/frameworks/nextjs/package.json index 1914a2334268..4e1c970c069c 100644 --- a/code/frameworks/nextjs/package.json +++ b/code/frameworks/nextjs/package.json @@ -87,6 +87,12 @@ "import": "./dist/export-mocks/router/index.mjs", "require": "./dist/export-mocks/router/index.js" }, + "./node": { + "types": "./dist/node/index.d.ts", + "node": "./dist/node/index.js", + "import": "./dist/node/index.mjs", + "require": "./dist/node/index.js" + }, "./package.json": "./package.json" }, "main": "dist/index.js", @@ -114,6 +120,9 @@ ], "navigation.mock": [ "dist/export-mocks/navigation/index.d.ts" + ], + "node": [ + "dist/node/index.d.ts" ] } }, @@ -209,6 +218,7 @@ "./src/image-context.ts", "./src/index.ts", "./src/preset.ts", + "./src/node/index.ts", "./src/preview.tsx", "./src/export-mocks/index.ts", "./src/export-mocks/cache/index.ts", diff --git a/code/frameworks/nextjs/src/index.ts b/code/frameworks/nextjs/src/index.ts index a904f93ec89d..41f13da3975f 100644 --- a/code/frameworks/nextjs/src/index.ts +++ b/code/frameworks/nextjs/src/index.ts @@ -1,2 +1,16 @@ +import type { ReactPreview } from '@storybook/react'; +import { definePreview as definePreviewBase } from '@storybook/react'; + +import * as nextPreview from './preview'; + export * from './types'; export * from './portable-stories'; + +export function definePreview(preview: NextPreview['input']) { + return definePreviewBase({ + ...preview, + addons: [nextPreview, ...(preview.addons ?? [])], + }) as NextPreview; +} + +interface NextPreview extends ReactPreview {} diff --git a/code/frameworks/nextjs/src/node/index.ts b/code/frameworks/nextjs/src/node/index.ts new file mode 100644 index 000000000000..bbfba66cc964 --- /dev/null +++ b/code/frameworks/nextjs/src/node/index.ts @@ -0,0 +1,5 @@ +import type { StorybookConfig } from '../types'; + +export function defineMain(config: StorybookConfig) { + return config; +} diff --git a/code/frameworks/nextjs/src/types.ts b/code/frameworks/nextjs/src/types.ts index 42148ef8517b..986c1a542746 100644 --- a/code/frameworks/nextjs/src/types.ts +++ b/code/frameworks/nextjs/src/types.ts @@ -12,6 +12,7 @@ import type { } from '@storybook/preset-react-webpack'; import type * as NextImage from 'next/image'; +import type { NextRouter } from 'next/router'; type FrameworkName = CompatibleString<'@storybook/nextjs'>; type BuilderName = CompatibleString<'@storybook/builder-webpack5'>; @@ -48,3 +49,27 @@ export type StorybookConfig = Omit< > & StorybookConfigWebpack & StorybookConfigFramework; + +export interface NextJsParameters { + /** + * Next.js framework configuration + * + * @see https://storybook.js.org/docs/get-started/frameworks/nextjs + */ + nextjs?: { + /** + * Enable App Directory features If your story imports components that use next/navigation, you + * need to set this parameter to true + */ + appDirectory?: boolean; + + /** + * Next.js navigation configuration when using `next/navigation`. Please note that it can only + * be used in components/pages in the app directory. + */ + navigation?: NextRouter; + + /** Next.js router configuration */ + router?: NextRouter; + }; +} diff --git a/code/frameworks/preact-vite/package.json b/code/frameworks/preact-vite/package.json index edc34da8abbf..a42f0cf41a5c 100644 --- a/code/frameworks/preact-vite/package.json +++ b/code/frameworks/preact-vite/package.json @@ -29,6 +29,12 @@ "types": "./dist/preset.d.ts", "require": "./dist/preset.js" }, + "./node": { + "types": "./dist/node/index.d.ts", + "node": "./dist/node/index.js", + "import": "./dist/node/index.mjs", + "require": "./dist/node/index.js" + }, "./package.json": "./package.json" }, "main": "dist/index.js", diff --git a/code/frameworks/preact-vite/src/node/index.ts b/code/frameworks/preact-vite/src/node/index.ts new file mode 100644 index 000000000000..bbfba66cc964 --- /dev/null +++ b/code/frameworks/preact-vite/src/node/index.ts @@ -0,0 +1,5 @@ +import type { StorybookConfig } from '../types'; + +export function defineMain(config: StorybookConfig) { + return config; +} diff --git a/code/frameworks/preact-webpack5/package.json b/code/frameworks/preact-webpack5/package.json index 8f8b26d89bbc..ccce216625f3 100644 --- a/code/frameworks/preact-webpack5/package.json +++ b/code/frameworks/preact-webpack5/package.json @@ -30,6 +30,12 @@ "types": "./dist/preset.d.ts", "require": "./dist/preset.js" }, + "./node": { + "types": "./dist/node/index.d.ts", + "node": "./dist/node/index.js", + "import": "./dist/node/index.mjs", + "require": "./dist/node/index.js" + }, "./package.json": "./package.json" }, "main": "dist/index.js", @@ -69,7 +75,8 @@ "bundler": { "entries": [ "./src/index.ts", - "./src/preset.ts" + "./src/preset.ts", + "./src/node/index.ts" ], "platform": "node" }, diff --git a/code/frameworks/preact-webpack5/src/node/index.ts b/code/frameworks/preact-webpack5/src/node/index.ts new file mode 100644 index 000000000000..bbfba66cc964 --- /dev/null +++ b/code/frameworks/preact-webpack5/src/node/index.ts @@ -0,0 +1,5 @@ +import type { StorybookConfig } from '../types'; + +export function defineMain(config: StorybookConfig) { + return config; +} diff --git a/code/frameworks/react-native-web-vite/package.json b/code/frameworks/react-native-web-vite/package.json index aaf62d208e9c..599f1baa618c 100644 --- a/code/frameworks/react-native-web-vite/package.json +++ b/code/frameworks/react-native-web-vite/package.json @@ -35,6 +35,12 @@ "import": "./dist/vite-plugin.mjs", "require": "./dist/vite-plugin.js" }, + "./node": { + "types": "./dist/node/index.d.ts", + "node": "./dist/node/index.js", + "import": "./dist/node/index.mjs", + "require": "./dist/node/index.js" + }, "./package.json": "./package.json" }, "main": "dist/index.js", @@ -87,7 +93,8 @@ "entries": [ "./src/index.ts", "./src/preset.ts", - "./src/vite-plugin.ts" + "./src/vite-plugin.ts", + "./src/node/index.ts" ], "platform": "node" }, diff --git a/code/frameworks/react-native-web-vite/src/node/index.ts b/code/frameworks/react-native-web-vite/src/node/index.ts new file mode 100644 index 000000000000..bbfba66cc964 --- /dev/null +++ b/code/frameworks/react-native-web-vite/src/node/index.ts @@ -0,0 +1,5 @@ +import type { StorybookConfig } from '../types'; + +export function defineMain(config: StorybookConfig) { + return config; +} diff --git a/code/frameworks/react-vite/package.json b/code/frameworks/react-vite/package.json index 65fd8a0ee4c2..5833f5fdfeaf 100644 --- a/code/frameworks/react-vite/package.json +++ b/code/frameworks/react-vite/package.json @@ -30,6 +30,12 @@ "types": "./dist/preset.d.ts", "require": "./dist/preset.js" }, + "./node": { + "types": "./dist/node/index.d.ts", + "node": "./dist/node/index.js", + "import": "./dist/node/index.mjs", + "require": "./dist/node/index.js" + }, "./package.json": "./package.json" }, "main": "dist/index.js", @@ -42,6 +48,9 @@ ], "preset": [ "dist/preset.d.ts" + ], + "node": [ + "dist/node/index.d.ts" ] } }, @@ -93,7 +102,8 @@ "bundler": { "entries": [ "./src/index.ts", - "./src/preset.ts" + "./src/preset.ts", + "./src/node/index.ts" ], "platform": "node" }, diff --git a/code/frameworks/react-vite/src/index.ts b/code/frameworks/react-vite/src/index.ts index fcb073fefcd6..54688d096160 100644 --- a/code/frameworks/react-vite/src/index.ts +++ b/code/frameworks/react-vite/src/index.ts @@ -1 +1,3 @@ +export { definePreview } from '@storybook/react'; + export * from './types'; diff --git a/code/frameworks/react-vite/src/node/index.ts b/code/frameworks/react-vite/src/node/index.ts new file mode 100644 index 000000000000..bbfba66cc964 --- /dev/null +++ b/code/frameworks/react-vite/src/node/index.ts @@ -0,0 +1,5 @@ +import type { StorybookConfig } from '../types'; + +export function defineMain(config: StorybookConfig) { + return config; +} diff --git a/code/frameworks/react-webpack5/package.json b/code/frameworks/react-webpack5/package.json index 2cd966fd1948..8b455e23e367 100644 --- a/code/frameworks/react-webpack5/package.json +++ b/code/frameworks/react-webpack5/package.json @@ -30,6 +30,12 @@ "types": "./dist/preset.d.ts", "require": "./dist/preset.js" }, + "./node": { + "types": "./dist/node/index.d.ts", + "node": "./dist/node/index.js", + "import": "./dist/node/index.mjs", + "require": "./dist/node/index.js" + }, "./package.json": "./package.json" }, "main": "dist/index.js", @@ -74,7 +80,8 @@ "bundler": { "entries": [ "./src/index.ts", - "./src/preset.ts" + "./src/preset.ts", + "./src/node/index.ts" ], "platform": "node" }, diff --git a/code/frameworks/react-webpack5/src/index.ts b/code/frameworks/react-webpack5/src/index.ts index fcb073fefcd6..84081fa8f85d 100644 --- a/code/frameworks/react-webpack5/src/index.ts +++ b/code/frameworks/react-webpack5/src/index.ts @@ -1 +1,2 @@ export * from './types'; +export { definePreview } from '@storybook/react'; diff --git a/code/frameworks/react-webpack5/src/node/index.ts b/code/frameworks/react-webpack5/src/node/index.ts new file mode 100644 index 000000000000..bbfba66cc964 --- /dev/null +++ b/code/frameworks/react-webpack5/src/node/index.ts @@ -0,0 +1,5 @@ +import type { StorybookConfig } from '../types'; + +export function defineMain(config: StorybookConfig) { + return config; +} diff --git a/code/frameworks/server-webpack5/package.json b/code/frameworks/server-webpack5/package.json index efe067e0a1f3..af6f2e8224ed 100644 --- a/code/frameworks/server-webpack5/package.json +++ b/code/frameworks/server-webpack5/package.json @@ -30,6 +30,12 @@ "types": "./dist/preset.d.ts", "require": "./dist/preset.js" }, + "./node": { + "types": "./dist/node/index.d.ts", + "node": "./dist/node/index.js", + "import": "./dist/node/index.mjs", + "require": "./dist/node/index.js" + }, "./package.json": "./package.json" }, "main": "dist/index.js", @@ -67,7 +73,8 @@ "bundler": { "entries": [ "./src/index.ts", - "./src/preset.ts" + "./src/preset.ts", + "./src/node/index.ts" ], "platform": "node" }, diff --git a/code/frameworks/server-webpack5/src/node/index.ts b/code/frameworks/server-webpack5/src/node/index.ts new file mode 100644 index 000000000000..bbfba66cc964 --- /dev/null +++ b/code/frameworks/server-webpack5/src/node/index.ts @@ -0,0 +1,5 @@ +import type { StorybookConfig } from '../types'; + +export function defineMain(config: StorybookConfig) { + return config; +} diff --git a/code/frameworks/svelte-vite/package.json b/code/frameworks/svelte-vite/package.json index f2742c65fa79..d2b972c8bd34 100644 --- a/code/frameworks/svelte-vite/package.json +++ b/code/frameworks/svelte-vite/package.json @@ -30,6 +30,12 @@ "types": "./dist/preset.d.ts", "require": "./dist/preset.js" }, + "./node": { + "types": "./dist/node/index.d.ts", + "node": "./dist/node/index.js", + "import": "./dist/node/index.mjs", + "require": "./dist/node/index.js" + }, "./package.json": "./package.json" }, "main": "dist/index.js", @@ -78,7 +84,8 @@ "bundler": { "entries": [ "./src/index.ts", - "./src/preset.ts" + "./src/preset.ts", + "./src/node/index.ts" ], "platform": "node" }, diff --git a/code/frameworks/svelte-vite/src/node/index.ts b/code/frameworks/svelte-vite/src/node/index.ts new file mode 100644 index 000000000000..bbfba66cc964 --- /dev/null +++ b/code/frameworks/svelte-vite/src/node/index.ts @@ -0,0 +1,5 @@ +import type { StorybookConfig } from '../types'; + +export function defineMain(config: StorybookConfig) { + return config; +} diff --git a/code/frameworks/svelte-webpack5/package.json b/code/frameworks/svelte-webpack5/package.json index 9490cd3a5bac..c807b8edfea1 100644 --- a/code/frameworks/svelte-webpack5/package.json +++ b/code/frameworks/svelte-webpack5/package.json @@ -30,6 +30,12 @@ "types": "./dist/preset.d.ts", "require": "./dist/preset.js" }, + "./node": { + "types": "./dist/node/index.d.ts", + "node": "./dist/node/index.js", + "import": "./dist/node/index.mjs", + "require": "./dist/node/index.js" + }, "./package.json": "./package.json" }, "main": "dist/index.js", @@ -70,7 +76,8 @@ "bundler": { "entries": [ "./src/index.ts", - "./src/preset.ts" + "./src/preset.ts", + "./src/node/index.ts" ], "platform": "node" }, diff --git a/code/frameworks/svelte-webpack5/src/node/index.ts b/code/frameworks/svelte-webpack5/src/node/index.ts new file mode 100644 index 000000000000..bbfba66cc964 --- /dev/null +++ b/code/frameworks/svelte-webpack5/src/node/index.ts @@ -0,0 +1,5 @@ +import type { StorybookConfig } from '../types'; + +export function defineMain(config: StorybookConfig) { + return config; +} diff --git a/code/frameworks/sveltekit/package.json b/code/frameworks/sveltekit/package.json index 9afa86e498e4..f41a6dbb87d8 100644 --- a/code/frameworks/sveltekit/package.json +++ b/code/frameworks/sveltekit/package.json @@ -41,6 +41,12 @@ "require": "./dist/vite-plugin.js", "import": "./dist/vite-plugin.mjs" }, + "./node": { + "types": "./dist/node/index.d.ts", + "node": "./dist/node/index.js", + "import": "./dist/node/index.mjs", + "require": "./dist/node/index.js" + }, "./package.json": "./package.json" }, "main": "dist/index.js", @@ -84,7 +90,8 @@ "./src/index.ts", "./src/preview.ts", "./src/preset.ts", - "./src/vite-plugin.ts" + "./src/vite-plugin.ts", + "./src/node/index.ts" ], "platform": "node" }, diff --git a/code/frameworks/sveltekit/src/node/index.ts b/code/frameworks/sveltekit/src/node/index.ts new file mode 100644 index 000000000000..bbfba66cc964 --- /dev/null +++ b/code/frameworks/sveltekit/src/node/index.ts @@ -0,0 +1,5 @@ +import type { StorybookConfig } from '../types'; + +export function defineMain(config: StorybookConfig) { + return config; +} diff --git a/code/frameworks/vue3-vite/package.json b/code/frameworks/vue3-vite/package.json index 2929fe642ae7..79d181ce9333 100644 --- a/code/frameworks/vue3-vite/package.json +++ b/code/frameworks/vue3-vite/package.json @@ -35,6 +35,12 @@ "require": "./dist/vite-plugin.js", "import": "./dist/vite-plugin.mjs" }, + "./node": { + "types": "./dist/node/index.d.ts", + "node": "./dist/node/index.js", + "import": "./dist/node/index.mjs", + "require": "./dist/node/index.js" + }, "./package.json": "./package.json" }, "main": "dist/index.js", @@ -80,7 +86,8 @@ "entries": [ "./src/index.ts", "./src/preset.ts", - "./src/vite-plugin.ts" + "./src/vite-plugin.ts", + "./src/node/index.ts" ], "platform": "node" }, diff --git a/code/frameworks/vue3-vite/src/node/index.ts b/code/frameworks/vue3-vite/src/node/index.ts new file mode 100644 index 000000000000..bbfba66cc964 --- /dev/null +++ b/code/frameworks/vue3-vite/src/node/index.ts @@ -0,0 +1,5 @@ +import type { StorybookConfig } from '../types'; + +export function defineMain(config: StorybookConfig) { + return config; +} diff --git a/code/frameworks/vue3-webpack5/package.json b/code/frameworks/vue3-webpack5/package.json index 17ecaa899bd4..aec2f18cf0f3 100644 --- a/code/frameworks/vue3-webpack5/package.json +++ b/code/frameworks/vue3-webpack5/package.json @@ -30,6 +30,12 @@ "types": "./dist/preset.d.ts", "require": "./dist/preset.js" }, + "./node": { + "types": "./dist/node/index.d.ts", + "node": "./dist/node/index.js", + "import": "./dist/node/index.mjs", + "require": "./dist/node/index.js" + }, "./package.json": "./package.json" }, "main": "dist/index.js", @@ -71,7 +77,8 @@ "bundler": { "entries": [ "./src/index.ts", - "./src/preset.ts" + "./src/preset.ts", + "./src/node/index.ts" ], "platform": "node" }, diff --git a/code/frameworks/vue3-webpack5/src/node/index.ts b/code/frameworks/vue3-webpack5/src/node/index.ts new file mode 100644 index 000000000000..bbfba66cc964 --- /dev/null +++ b/code/frameworks/vue3-webpack5/src/node/index.ts @@ -0,0 +1,5 @@ +import type { StorybookConfig } from '../types'; + +export function defineMain(config: StorybookConfig) { + return config; +} diff --git a/code/frameworks/web-components-vite/package.json b/code/frameworks/web-components-vite/package.json index 4cc036bca8c1..d050d805a5d5 100644 --- a/code/frameworks/web-components-vite/package.json +++ b/code/frameworks/web-components-vite/package.json @@ -30,6 +30,12 @@ "types": "./dist/preset.d.ts", "require": "./dist/preset.js" }, + "./node": { + "types": "./dist/node/index.d.ts", + "node": "./dist/node/index.js", + "import": "./dist/node/index.mjs", + "require": "./dist/node/index.js" + }, "./package.json": "./package.json" }, "main": "dist/index.js", @@ -66,6 +72,7 @@ }, "bundler": { "entries": [ + "./src/node/index.ts", "./src/index.ts", "./src/preset.ts" ], diff --git a/code/frameworks/web-components-vite/src/node/index.ts b/code/frameworks/web-components-vite/src/node/index.ts new file mode 100644 index 000000000000..bbfba66cc964 --- /dev/null +++ b/code/frameworks/web-components-vite/src/node/index.ts @@ -0,0 +1,5 @@ +import type { StorybookConfig } from '../types'; + +export function defineMain(config: StorybookConfig) { + return config; +} diff --git a/code/frameworks/web-components-webpack5/package.json b/code/frameworks/web-components-webpack5/package.json index ac66bb1741de..d462071171f6 100644 --- a/code/frameworks/web-components-webpack5/package.json +++ b/code/frameworks/web-components-webpack5/package.json @@ -33,6 +33,12 @@ "types": "./dist/preset.d.ts", "require": "./dist/preset.js" }, + "./node": { + "types": "./dist/node/index.d.ts", + "node": "./dist/node/index.js", + "import": "./dist/node/index.mjs", + "require": "./dist/node/index.js" + }, "./package.json": "./package.json" }, "main": "dist/index.js", @@ -71,7 +77,8 @@ "bundler": { "entries": [ "./src/index.ts", - "./src/preset.ts" + "./src/preset.ts", + "./src/node/index.ts" ], "platform": "node" }, diff --git a/code/frameworks/web-components-webpack5/src/node/index.ts b/code/frameworks/web-components-webpack5/src/node/index.ts new file mode 100644 index 000000000000..bbfba66cc964 --- /dev/null +++ b/code/frameworks/web-components-webpack5/src/node/index.ts @@ -0,0 +1,5 @@ +import type { StorybookConfig } from '../types'; + +export function defineMain(config: StorybookConfig) { + return config; +} diff --git a/code/lib/cli-storybook/package.json b/code/lib/cli-storybook/package.json index c3ea289b4385..cb4bc353771e 100644 --- a/code/lib/cli-storybook/package.json +++ b/code/lib/cli-storybook/package.json @@ -56,6 +56,7 @@ "globby": "^14.0.1", "jscodeshift": "^0.15.1", "leven": "^3.1.0", + "p-limit": "^6.2.0", "prompts": "^2.4.0", "semver": "^7.3.7", "storybook": "workspace:*", diff --git a/code/lib/cli-storybook/src/add.test.ts b/code/lib/cli-storybook/src/add.test.ts index 1bbf88275afc..d685798d4d22 100644 --- a/code/lib/cli-storybook/src/add.test.ts +++ b/code/lib/cli-storybook/src/add.test.ts @@ -47,13 +47,16 @@ vi.mock('./postinstallAddon', () => { vi.mock('./automigrate/fixes/wrap-require-utils', () => { return MockWrapRequireUtils; }); +vi.mock('./codemod/helpers/csf-factories-utils'); vi.mock('storybook/internal/common', () => { return { getStorybookInfo: vi.fn(() => ({ mainConfig: {}, configDir: '' })), serverRequire: vi.fn(() => ({})), + loadMainConfig: vi.fn(() => ({})), JsPackageManagerFactory: { getPackageManager: vi.fn(() => MockedPackageManager), }, + syncStorybookAddons: vi.fn(), getCoercedStorybookVersion: vi.fn(() => '8.0.0'), versions: { '@storybook/addon-docs': '^8.0.0', diff --git a/code/lib/cli-storybook/src/add.ts b/code/lib/cli-storybook/src/add.ts index d9e24dd61314..381ae170e906 100644 --- a/code/lib/cli-storybook/src/add.ts +++ b/code/lib/cli-storybook/src/add.ts @@ -3,13 +3,14 @@ import { isAbsolute, join } from 'node:path'; import { JsPackageManagerFactory, type PackageManagerName, - getCoercedStorybookVersion, - getStorybookInfo, serverRequire, + syncStorybookAddons, versions, } from 'storybook/internal/common'; import { readConfig, writeConfig } from 'storybook/internal/csf-tools'; +import type { StorybookConfigRaw } from '@storybook/types'; + import prompts from 'prompts'; import SemVer from 'semver'; import { dedent } from 'ts-dedent'; @@ -18,6 +19,7 @@ import { getRequireWrapperName, wrapValueWithRequireWrapper, } from './automigrate/fixes/wrap-require-utils'; +import { getStorybookData } from './automigrate/helpers/mainConfigFile'; import { postinstallAddon } from './postinstallAddon'; export interface PostinstallOptions { @@ -53,7 +55,7 @@ const requireMain = (configDir: string) => { return serverRequire(mainFile) ?? {}; }; -const checkInstalled = (addonName: string, main: any) => { +const checkInstalled = (addonName: string, main: StorybookConfigRaw) => { const existingAddon = main.addons?.find((entry: string | { name: string }) => { const name = typeof entry === 'string' ? entry : entry.name; return name?.endsWith(addonName); @@ -91,12 +93,11 @@ export async function add( const [addonName, inputVersion] = getVersionSpecifier(addon); const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr }); - const packageJson = await packageManager.retrievePackageJson(); - const { mainConfig, configDir: inferredConfigDir } = getStorybookInfo( - packageJson, - userSpecifiedConfigDir - ); - const configDir = userSpecifiedConfigDir || inferredConfigDir || '.storybook'; + const { mainConfig, mainConfigPath, configDir, previewConfigPath, storybookVersion } = + await getStorybookData({ + packageManager, + configDir: userSpecifiedConfigDir, + }); if (typeof configDir === 'undefined') { throw new Error(dedent` @@ -104,16 +105,16 @@ export async function add( `); } - if (!mainConfig) { + if (!mainConfigPath) { logger.error('Unable to find Storybook main.js config'); return; } let shouldAddToMain = true; - if (checkInstalled(addonName, requireMain(configDir))) { + if (checkInstalled(addonName, mainConfig)) { shouldAddToMain = false; if (!yes) { - logger.log(`The Storybook addon "${addonName}" is already present in ${mainConfig}.`); + logger.log(`The Storybook addon "${addonName}" is already present in ${mainConfigPath}.`); const { shouldForceInstall } = await prompts({ type: 'confirm', name: 'shouldForceInstall', @@ -126,11 +127,9 @@ export async function add( } } - const main = await readConfig(mainConfig); + const main = await readConfig(mainConfigPath); logger.log(`Verifying ${addonName}`); - const storybookVersion = await getCoercedStorybookVersion(packageManager); - let version = inputVersion; if (!version && isCoreAddon(addonName) && storybookVersion) { @@ -155,7 +154,7 @@ export async function add( await packageManager.addDependencies({ installAsDevDependencies: true }, [addonWithVersion]); if (shouldAddToMain) { - logger.log(`Adding '${addon}' to the "addons" field in ${mainConfig}`); + logger.log(`Adding '${addon}' to the "addons" field in ${mainConfigPath}`); const mainConfigAddons = main.getFieldNode(['addons']); if (mainConfigAddons && getRequireWrapperName(main) !== null) { @@ -169,6 +168,8 @@ export async function add( await writeConfig(main); } + await syncStorybookAddons(mainConfig, previewConfigPath!); + if (!skipPostinstall && isCoreAddon(addonName)) { await postinstallAddon(addonName, { packageManager: packageManager.type, configDir, yes }); } diff --git a/code/lib/cli-storybook/src/automigrate/codemod.ts b/code/lib/cli-storybook/src/automigrate/codemod.ts new file mode 100644 index 000000000000..1cb937d73398 --- /dev/null +++ b/code/lib/cli-storybook/src/automigrate/codemod.ts @@ -0,0 +1,108 @@ +import os from 'node:os'; + +import { formatFileContent } from 'storybook/internal/common'; + +import { promises as fs } from 'fs'; +import picocolors from 'picocolors'; +import slash from 'slash'; + +const logger = console; + +export const maxConcurrentTasks = Math.max(1, os.cpus().length - 1); + +export interface FileInfo { + path: string; + source: string; + [key: string]: any; +} + +/** + * Runs a codemod transformation on files matching the specified glob pattern. + * + * The function processes each file matching the glob pattern, applies the transform function, and + * writes the transformed source back to the file if it has changed. + * + * @example + * + * ``` + * await runCodemod('*.stories.tsx', async (fileInfo) => { + * // Transform the file source return + * return fileInfo.source.replace(/foo/g, 'bar'); + * }); + * ``` + */ +export async function runCodemod( + globPattern: string = '**/*.stories.*', + transform: (source: FileInfo, ...rest: any) => Promise, + { dryRun = false, skipFormatting = false }: { dryRun?: boolean; skipFormatting?: boolean } = {} +) { + let modifiedCount = 0; + let unmodifiedCount = 0; + let errorCount = 0; + + // Dynamically import these packages because they are pure ESM modules + // eslint-disable-next-line depend/ban-dependencies + const { globby } = await import('globby'); + + // glob only supports forward slashes + const files = await globby(slash(globPattern), { + followSymbolicLinks: true, + ignore: ['node_modules/**', 'dist/**', 'storybook-static/**', 'build/**'], + }); + + if (!files.length) { + logger.error( + `No files found for glob pattern "${globPattern}".\nPlease try a different pattern.\n` + ); + // eslint-disable-next-line local-rules/no-uncategorized-errors + throw new Error('No files matched'); + } + + try { + const pLimit = (await import('p-limit')).default; + + const limit = pLimit(maxConcurrentTasks); + + await Promise.all( + files.map((file) => + limit(async () => { + try { + const source = await fs.readFile(file, 'utf-8'); + const fileInfo: FileInfo = { path: file, source }; + const transformedSource = await transform(fileInfo); + + if (transformedSource !== source) { + if (!dryRun) { + const fileContent = skipFormatting + ? transformedSource + : await formatFileContent(file, transformedSource); + await fs.writeFile(file, fileContent, 'utf-8'); + } + modifiedCount++; + } else { + unmodifiedCount++; + } + } catch (fileError) { + logger.error(`Error processing file ${file}:`, fileError); + errorCount++; + } + }) + ) + ); + } catch (error) { + logger.error('Error applying transform:', error); + errorCount++; + } + + logger.log( + `Summary: ${picocolors.green(`${modifiedCount} transformed`)}, ${picocolors.yellow(`${unmodifiedCount} unmodified`)}, ${picocolors.red(`${errorCount} errors`)}` + ); + + if (dryRun) { + logger.log( + picocolors.bold( + `This was a dry run. Run without --dry-run to apply the transformation to ${modifiedCount} files.` + ) + ); + } +} diff --git a/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.test.ts b/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.test.ts index 3de227e2f765..cfb38417ff1b 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.test.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.test.ts @@ -1,18 +1,19 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { getAddonNames } from 'storybook/internal/common'; + import { existsSync, readFileSync, writeFileSync } from 'fs'; import * as jscodeshift from 'jscodeshift'; import path from 'path'; import dedent from 'ts-dedent'; -import { getAddonNames } from '../helpers/mainConfigFile'; import { addonA11yAddonTest, transformPreviewFile, transformSetupFile, } from './addon-a11y-addon-test'; -vi.mock('../helpers/mainConfigFile', async (importOriginal) => { +vi.mock('storybook/internal/common', async (importOriginal) => { const mod = (await importOriginal()) as any; return { ...mod, diff --git a/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.ts b/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.ts index 80b1ccbf5658..29b07c54b064 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/addon-a11y-addon-test.ts @@ -1,4 +1,4 @@ -import { formatFileContent, rendererPackages } from 'storybook/internal/common'; +import { formatFileContent, getAddonNames, rendererPackages } from 'storybook/internal/common'; import { formatConfig, loadConfig } from 'storybook/internal/csf-tools'; import { type ArrayExpression } from '@babel/types'; @@ -13,7 +13,7 @@ import { SUPPORTED_FRAMEWORKS, SUPPORTED_RENDERERS, } from '../../../../../addons/test/src/constants'; -import { getAddonNames, getFrameworkPackageName, getRendererName } from '../helpers/mainConfigFile'; +import { getFrameworkPackageName, getRendererName } from '../helpers/mainConfigFile'; import type { Fix } from '../types'; export const fileExtensions = [ diff --git a/code/lib/cli-storybook/src/automigrate/fixes/addon-postcss.ts b/code/lib/cli-storybook/src/automigrate/fixes/addon-postcss.ts index d8bd135da2f9..42047be06132 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/addon-postcss.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/addon-postcss.ts @@ -1,7 +1,8 @@ +import { getAddonNames } from 'storybook/internal/common'; + import picocolors from 'picocolors'; import { dedent } from 'ts-dedent'; -import { getAddonNames } from '../helpers/mainConfigFile'; import type { Fix } from '../types'; interface AddonPostcssRunOptions { diff --git a/code/lib/cli-storybook/src/automigrate/fixes/index.ts b/code/lib/cli-storybook/src/automigrate/fixes/index.ts index dfd43100665c..c66b0106ce5d 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/index.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/index.ts @@ -1,4 +1,5 @@ -import type { Fix } from '../types'; +import { csfFactories } from '../../codemod/csf-factories'; +import type { CommandFix, Fix } from '../types'; import { addonA11yAddonTest } from './addon-a11y-addon-test'; import { addonPostCSS } from './addon-postcss'; import { addonsAPI } from './addons-api'; @@ -70,3 +71,7 @@ export const allFixes: Fix[] = [ ]; export const initFixes: Fix[] = [eslintPlugin]; + +// These are specific fixes that only occur when triggered on command, and are hidden otherwise. +// e.g. npx storybook automigrate csf-factories +export const commandFixes: CommandFix[] = [csfFactories]; diff --git a/code/lib/cli-storybook/src/automigrate/fixes/missing-storybook-dependencies.test.ts b/code/lib/cli-storybook/src/automigrate/fixes/missing-storybook-dependencies.test.ts index 2729cfb1da16..b39926413778 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/missing-storybook-dependencies.test.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/missing-storybook-dependencies.test.ts @@ -109,6 +109,8 @@ describe('missingStorybookDependencies', () => { await missingStorybookDependencies.run!({ result: { packageUsage }, dryRun, + packageJson: {}, + mainConfig: { stories: [] }, packageManager: mockPackageManager as JsPackageManager, mainConfigPath: 'path/to/main-config.js', }); diff --git a/code/lib/cli-storybook/src/automigrate/fixes/vta.ts b/code/lib/cli-storybook/src/automigrate/fixes/vta.ts index 47cd1a9fe0ff..0b9d4f481e5b 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/vta.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/vta.ts @@ -1,7 +1,9 @@ +import { getAddonNames } from 'storybook/internal/common'; + import picocolors from 'picocolors'; import { dedent } from 'ts-dedent'; -import { getAddonNames, updateMainConfig } from '../helpers/mainConfigFile'; +import { updateMainConfig } from '../helpers/mainConfigFile'; import type { Fix } from '../types'; const logger = console; diff --git a/code/lib/cli-storybook/src/automigrate/fixes/webpack5-compiler-setup.ts b/code/lib/cli-storybook/src/automigrate/fixes/webpack5-compiler-setup.ts index e0c5f0e9cfc7..531da92a827a 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/webpack5-compiler-setup.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/webpack5-compiler-setup.ts @@ -5,7 +5,7 @@ import { builderNameToCoreBuilder, compilerNameToCoreCompiler, } from 'storybook/internal/cli'; -import { frameworkPackages } from 'storybook/internal/common'; +import { frameworkPackages, getAddonNames } from 'storybook/internal/common'; import type { SupportedFrameworks } from 'storybook/internal/types'; import picocolors from 'picocolors'; @@ -14,7 +14,6 @@ import { dedent } from 'ts-dedent'; import { add } from '../../add'; import { - getAddonNames, getBuilderPackageName, getFrameworkOptions, getFrameworkPackageName, diff --git a/code/lib/cli-storybook/src/automigrate/helpers/mainConfigFile.ts b/code/lib/cli-storybook/src/automigrate/helpers/mainConfigFile.ts index 57aa4bf7ce07..6a93538da497 100644 --- a/code/lib/cli-storybook/src/automigrate/helpers/mainConfigFile.ts +++ b/code/lib/cli-storybook/src/automigrate/helpers/mainConfigFile.ts @@ -13,7 +13,7 @@ import type { JsPackageManager } from 'storybook/internal/common'; import { getCoercedStorybookVersion } from 'storybook/internal/common'; import type { ConfigFile } from 'storybook/internal/csf-tools'; import { readConfig, writeConfig as writeConfigFile } from 'storybook/internal/csf-tools'; -import type { StorybookConfig, StorybookConfigRaw } from 'storybook/internal/types'; +import type { StorybookConfigRaw } from 'storybook/internal/types'; import picocolors from 'picocolors'; import { dedent } from 'ts-dedent'; @@ -157,6 +157,7 @@ export const getStorybookData = async ({ storybookVersion, mainConfigPath, previewConfigPath, + packageJson, }; }; export type GetStorybookData = typeof getStorybookData; @@ -201,28 +202,3 @@ export const updateMainConfig = async ( ); } }; - -export const getAddonNames = (mainConfig: StorybookConfig): string[] => { - const addons = mainConfig.addons || []; - const addonList = addons.map((addon) => { - let name = ''; - if (typeof addon === 'string') { - name = addon; - } else if (typeof addon === 'object') { - name = addon.name; - } - - if (name.startsWith('.')) { - return undefined; - } - - return name - .replace(/\/dist\/.*/, '') - .replace(/\.[mc]?[tj]?s[x]?$/, '') - .replace(/\/register$/, '') - .replace(/\/manager$/, '') - .replace(/\/preset$/, ''); - }); - - return addonList.filter((item): item is NonNullable => item != null); -}; diff --git a/code/lib/cli-storybook/src/automigrate/index.test.ts b/code/lib/cli-storybook/src/automigrate/index.test.ts index 9bc05affbacc..32a4a512cbc4 100644 --- a/code/lib/cli-storybook/src/automigrate/index.test.ts +++ b/code/lib/cli-storybook/src/automigrate/index.test.ts @@ -89,6 +89,8 @@ const runFixWrapper = async ({ fixes, dryRun, yes, + packageJson: {}, + mainConfig: { stories: [] }, rendererPackage, skipInstall, configDir, @@ -134,15 +136,17 @@ describe('runFixes', () => { expect(fixResults).toEqual({ 'fix-1': 'succeeded', }); - expect(run1).toHaveBeenCalledWith({ - dryRun, - mainConfigPath, - packageManager, - result: { - some: 'result', - }, - skipInstall, - }); + expect(run1).toHaveBeenCalledWith( + expect.objectContaining({ + dryRun, + mainConfigPath, + packageManager, + result: { + some: 'result', + }, + skipInstall, + }) + ); }); it('should fail if an error is thrown', async () => { diff --git a/code/lib/cli-storybook/src/automigrate/index.ts b/code/lib/cli-storybook/src/automigrate/index.ts index 9d98d97d7013..4dd8e7445895 100644 --- a/code/lib/cli-storybook/src/automigrate/index.ts +++ b/code/lib/cli-storybook/src/automigrate/index.ts @@ -2,13 +2,13 @@ import { createWriteStream } from 'node:fs'; import { rename, rm } from 'node:fs/promises'; import { join } from 'node:path'; +import type { PackageJson } from 'storybook/internal/common'; import { type JsPackageManager, JsPackageManagerFactory, - getCoercedStorybookVersion, - getStorybookInfo, temporaryFile, } from 'storybook/internal/common'; +import type { StorybookConfigRaw } from 'storybook/internal/types'; import boxen from 'boxen'; import picocolors from 'picocolors'; @@ -27,7 +27,7 @@ import type { PreCheckFailure, Prompt, } from './fixes'; -import { FixStatus, allFixes } from './fixes'; +import { FixStatus, allFixes, commandFixes } from './fixes'; import { upgradeStorybookRelatedDependencies } from './fixes/upgrade-storybook-related-dependencies'; import { cleanLog } from './helpers/cleanLog'; import { getMigrationSummary } from './helpers/getMigrationSummary'; @@ -60,7 +60,7 @@ const cleanup = () => { }; const logAvailableMigrations = () => { - const availableFixes = allFixes + const availableFixes = [...allFixes, ...commandFixes] .map((f) => picocolors.yellow(f.id)) .map((x) => `- ${x}`) .join('\n'); @@ -77,16 +77,17 @@ export const doAutomigrate = async (options: AutofixOptionsFromCLI) => { force: options.packageManager, }); - const [packageJson, storybookVersion] = await Promise.all([ - packageManager.retrievePackageJson(), - getCoercedStorybookVersion(packageManager), - ]); - - const { configDir: inferredConfigDir, mainConfig: mainConfigPath } = getStorybookInfo( + const { + mainConfig, + mainConfigPath, + previewConfigPath, + storybookVersion, + configDir, packageJson, - options.configDir - ); - const configDir = options.configDir || inferredConfigDir || '.storybook'; + } = await getStorybookData({ + configDir: options.configDir, + packageManager, + }); if (!storybookVersion) { throw new Error('Could not determine Storybook version'); @@ -98,10 +99,13 @@ export const doAutomigrate = async (options: AutofixOptionsFromCLI) => { const outcome = await automigrate({ ...options, + packageJson, packageManager, storybookVersion, beforeVersion: storybookVersion, mainConfigPath, + mainConfig, + previewConfigPath, configDir, isUpgrade: false, isLatest: false, @@ -118,9 +122,12 @@ export const automigrate = async ({ dryRun, yes, packageManager, + packageJson, list, configDir, + mainConfig, mainConfigPath, + previewConfigPath, storybookVersion, beforeVersion, renderer: rendererPackage, @@ -137,6 +144,24 @@ export const automigrate = async ({ return null; } + // if an on-command migration is triggered, run it and bail + const commandFix = commandFixes.find((f) => f.id === fixId); + if (commandFix) { + logger.info(`🔎 Running migration ${picocolors.magenta(fixId)}..`); + + await commandFix.run({ + mainConfigPath, + previewConfigPath, + packageManager, + packageJson, + dryRun, + mainConfig, + result: null, + }); + + return null; + } + const selectedFixes: Fix[] = inputFixes || allFixes.filter((fix) => { @@ -166,9 +191,12 @@ export const automigrate = async ({ const { fixResults, fixSummary, preCheckFailure } = await runFixes({ fixes, packageManager, + packageJson, rendererPackage, skipInstall, configDir, + previewConfigPath, + mainConfig, mainConfigPath, storybookVersion, beforeVersion, @@ -214,7 +242,10 @@ export async function runFixes({ skipInstall, configDir, packageManager, + packageJson, + mainConfig, mainConfigPath, + previewConfigPath, storybookVersion, beforeVersion, isUpgrade, @@ -226,7 +257,10 @@ export async function runFixes({ skipInstall?: boolean; configDir: string; packageManager: JsPackageManager; + packageJson: PackageJson; mainConfigPath: string; + previewConfigPath?: string; + mainConfig: StorybookConfigRaw; storybookVersion: string; beforeVersion: string; isUpgrade?: boolean; @@ -243,11 +277,6 @@ export async function runFixes({ let result; try { - const { mainConfig, previewConfigPath } = await getStorybookData({ - configDir, - packageManager, - }); - if ( (isUpgrade && semver.satisfies(beforeVersion, f.versionRange[0], { includePrerelease: true }) && @@ -383,6 +412,9 @@ export async function runFixes({ packageManager, dryRun, mainConfigPath, + previewConfigPath, + packageJson, + mainConfig, skipInstall, }); logger.info(`✅ ran ${picocolors.cyan(f.id)} migration`); diff --git a/code/lib/cli-storybook/src/automigrate/types.ts b/code/lib/cli-storybook/src/automigrate/types.ts index 737d8f9018f7..f4eb22d9e740 100644 --- a/code/lib/cli-storybook/src/automigrate/types.ts +++ b/code/lib/cli-storybook/src/automigrate/types.ts @@ -1,4 +1,4 @@ -import type { JsPackageManager, PackageManagerName } from 'storybook/internal/common'; +import type { JsPackageManager, PackageJson, PackageManagerName } from 'storybook/internal/common'; import type { StorybookConfigRaw } from 'storybook/internal/types'; export interface CheckOptions { @@ -13,9 +13,12 @@ export interface CheckOptions { export interface RunOptions { packageManager: JsPackageManager; + packageJson: PackageJson; result: ResultType; dryRun?: boolean; mainConfigPath: string; + previewConfigPath?: string; + mainConfig: StorybookConfigRaw; skipInstall?: boolean; } @@ -25,8 +28,9 @@ export interface RunOptions { * - Auto: the fix will be applied automatically * - Manual: the user will be prompted to apply the fix * - Notification: the user will be notified about some changes. A fix isn't required, though + * - Command: the fix will only be applied when specified directly by its id */ -export type Prompt = 'auto' | 'manual' | 'notification'; +export type Prompt = 'auto' | 'manual' | 'notification' | 'command'; type BaseFix = { id: string; @@ -46,17 +50,20 @@ type PromptType = | T | ((result: ResultType) => Promise | Prompt); -export type Fix = ( - | { +export type Fix = + | ({ promptType?: PromptType; run: (options: RunOptions) => Promise; - } - | { + } & BaseFix) + | ({ promptType: PromptType; run?: never; - } -) & - BaseFix; + } & BaseFix); + +export type CommandFix = { + promptType: PromptType; + run: (options: RunOptions) => Promise; +} & Omit, 'versionRange' | 'check' | 'prompt'>; export type FixId = string; @@ -68,7 +75,10 @@ export enum PreCheckFailure { export interface AutofixOptions extends Omit { packageManager: JsPackageManager; + packageJson: PackageJson; mainConfigPath: string; + previewConfigPath?: string; + mainConfig: StorybookConfigRaw; /** The version of Storybook before the migration. */ beforeVersion: string; storybookVersion: string; diff --git a/code/lib/cli-storybook/src/codemod/csf-factories.ts b/code/lib/cli-storybook/src/codemod/csf-factories.ts new file mode 100644 index 000000000000..eeae5bdab993 --- /dev/null +++ b/code/lib/cli-storybook/src/codemod/csf-factories.ts @@ -0,0 +1,153 @@ +import { type JsPackageManager, syncStorybookAddons } from 'storybook/internal/common'; + +import picocolors from 'picocolors'; +import prompts from 'prompts'; +import { dedent } from 'ts-dedent'; + +import { runCodemod } from '../automigrate/codemod'; +import { getFrameworkPackageName } from '../automigrate/helpers/mainConfigFile'; +import type { CommandFix } from '../automigrate/types'; +import { printBoxedMessage } from '../util'; +import { configToCsfFactory } from './helpers/config-to-csf-factory'; +import { storyToCsfFactory } from './helpers/story-to-csf-factory'; + +export const logger = console; + +async function runStoriesCodemod(options: { + dryRun: boolean | undefined; + packageManager: JsPackageManager; + useSubPathImports: boolean; + previewConfigPath: string; +}) { + const { dryRun, packageManager, ...codemodOptions } = options; + try { + let globString = 'src/**/*.stories.*'; + if (!process.env.IN_STORYBOOK_SANDBOX) { + logger.log('Please enter the glob for your stories to migrate'); + globString = ( + await prompts( + { + type: 'text', + name: 'glob', + message: 'glob', + initial: globString, + }, + { + onCancel: () => process.exit(0), + } + ) + ).glob; + } + + logger.log('\n🛠️ Applying codemod on your stories, this might take some time...'); + + // TODO: Move the csf-2-to-3 codemod into automigrations + await packageManager.executeCommand({ + command: `${packageManager.getRemoteRunCommand()} storybook migrate csf-2-to-3 --glob=${globString}`, + args: [], + stdio: 'ignore', + ignoreError: true, + }); + + await runCodemod(globString, (info) => storyToCsfFactory(info, codemodOptions), { + dryRun, + }); + } catch (err: any) { + if (err.message === 'No files matched') { + await runStoriesCodemod(options); + } else { + throw err; + } + } +} + +export const csfFactories: CommandFix = { + id: 'csf-factories', + promptType: 'command', + async run({ + dryRun, + mainConfig, + mainConfigPath, + previewConfigPath, + packageJson, + packageManager, + }) { + let useSubPathImports = true; + if (!process.env.IN_STORYBOOK_SANDBOX) { + // prompt whether the user wants to use imports map + logger.log( + printBoxedMessage(dedent` + The CSF factories format benefits from subpath imports (the imports property in your \`package.json\`), which is a node standard for module resolution. This makes it more convenient to import the preview config in your story files. + + However, please note that this might not work if you have an outdated tsconfig, use custom paths, or have type alias plugins configured in your project. You can always rerun this codemod and select another option to update your code later. + + More info: ${picocolors.yellow('https://storybook.js.org/docs/api/csf/csf-factories#subpath-imports')} + + As we modify your story files, we can create two types of imports: + + - ${picocolors.bold('Subpath imports (recommended):')} ${picocolors.cyan("`import preview from '#.storybook/preview'`")} + - ${picocolors.bold('Relative imports:')} ${picocolors.cyan("`import preview from '../../.storybook/preview'`")} + `) + ); + useSubPathImports = ( + await prompts( + { + type: 'select', + name: 'useSubPathImports', + message: 'Which would you like to use?', + choices: [ + { title: 'Subpath imports', value: true }, + { title: 'Relative imports', value: false }, + ], + initial: 0, + }, + { + onCancel: () => process.exit(0), + } + ) + ).useSubPathImports; + } + + if (useSubPathImports && !packageJson.imports?.['#*']) { + logger.log(`🗺️ Adding imports map in ${picocolors.cyan(packageManager.packageJsonPath())}`); + packageJson.imports = { + ...packageJson.imports, + // @ts-expect-error we need to upgrade type-fest + '#*': ['./*', './*.ts', './*.tsx', './*.js', './*.jsx'], + }; + await packageManager.writePackageJson(packageJson); + } + + await runStoriesCodemod({ + dryRun, + packageManager, + useSubPathImports, + previewConfigPath: previewConfigPath!, + }); + + logger.log('\n🛠️ Applying codemod on your main config...'); + const frameworkPackage = + getFrameworkPackageName(mainConfig) || '@storybook/your-framework-here'; + await runCodemod(mainConfigPath, (fileInfo) => + configToCsfFactory(fileInfo, { configType: 'main', frameworkPackage }, { dryRun }) + ); + + logger.log('\n🛠️ Applying codemod on your preview config...'); + await runCodemod(previewConfigPath, (fileInfo) => + configToCsfFactory(fileInfo, { configType: 'preview', frameworkPackage }, { dryRun }) + ); + + await syncStorybookAddons(mainConfig, previewConfigPath!); + + logger.log( + printBoxedMessage( + dedent` + You can now run Storybook with the new CSF factories format. + + For more info, check out the docs: + ${picocolors.yellow('https://storybook.js.org/docs/api/csf/csf-factories')} + ` + ) + ); + }, +}; diff --git a/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.test.ts b/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.test.ts new file mode 100644 index 000000000000..0da113d41a4d --- /dev/null +++ b/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.test.ts @@ -0,0 +1,195 @@ +import { describe, expect, it } from 'vitest'; + +import { dedent } from 'ts-dedent'; + +import { configToCsfFactory } from './config-to-csf-factory'; + +expect.addSnapshotSerializer({ + serialize: (val: any) => (typeof val === 'string' ? val : val.toString()), + test: () => true, +}); + +describe('main/preview codemod: general parsing functionality', () => { + const transform = async (source: string) => + ( + await configToCsfFactory( + { source, path: 'main.ts' }, + { configType: 'main', frameworkPackage: '@storybook/react-vite' } + ) + ).trim(); + + it('should wrap defineMain call from inline default export', async () => { + await expect( + transform(dedent` + export default { + stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'], + addons: ['@storybook/addon-essentials'], + framework: '@storybook/react-vite', + }; + `) + ).resolves.toMatchInlineSnapshot(` + import { defineMain } from '@storybook/react-vite/node'; + + export default defineMain({ + stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'], + addons: ['@storybook/addon-essentials'], + framework: '@storybook/react-vite', + }); + `); + }); + it('should wrap defineMain call from const declared default export', async () => { + await expect( + transform(dedent` + const config = { + stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'], + addons: ['@storybook/addon-essentials'], + framework: '@storybook/react-vite', + }; + + export default config; + `) + ).resolves.toMatchInlineSnapshot(` + import { defineMain } from '@storybook/react-vite/node'; + + export default defineMain({ + stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'], + addons: ['@storybook/addon-essentials'], + framework: '@storybook/react-vite', + }); + `); + }); + + it('should wrap defineMain call from const declared default export and default export mix', async () => { + await expect( + transform(dedent` + export const tags = []; + export async function viteFinal(config) { return config }; + const config = { + framework: '@storybook/react-vite', + }; + + export default config; + `) + ).resolves.toMatchInlineSnapshot(` + import { defineMain } from '@storybook/react-vite/node'; + + const config = { + framework: '@storybook/react-vite', + tags: [], + viteFinal: () => { + return config; + }, + }; + + export default config; + `); + }); + it('should wrap defineMain call from named exports format', async () => { + await expect( + transform(dedent` + export function stories() { return ['../src/**/*.stories.@(js|jsx|ts|tsx)'] }; + export const addons = ['@storybook/addon-essentials']; + export async function viteFinal(config) { return config }; + export const framework = '@storybook/react-vite'; + `) + ).resolves.toMatchInlineSnapshot(` + import { defineMain } from '@storybook/react-vite/node'; + + export default defineMain({ + stories: () => { + return ['../src/**/*.stories.@(js|jsx|ts|tsx)']; + }, + addons: ['@storybook/addon-essentials'], + viteFinal: () => { + return config; + }, + framework: '@storybook/react-vite', + }); + `); + }); + it('should not add additional imports if there is already one', async () => { + const transformed = await transform(dedent` + import { defineMain } from '@storybook/react-vite/node'; + const config = {}; + + export default config; + `); + expect( + transformed.match(/import { defineMain } from '@storybook\/react-vite\/node'/g) + ).toHaveLength(1); + }); + + it('should leave already transformed code as is', async () => { + const original = dedent` + import { defineMain } from '@storybook/react-vite/node'; + + export default defineMain({}); + `; + const transformed = await transform(original); + expect(transformed).toEqual(original); + }); + + it('should remove legacy main config type imports', async () => { + await expect( + transform(dedent` + import { type StorybookConfig } from '@storybook/react-vite' + + const config: StorybookConfig = { + stories: [] + }; + export default config; + `) + ).resolves.toMatchInlineSnapshot(` + import { defineMain } from '@storybook/react-vite/node'; + + export default defineMain({ + stories: [], + }); + `); + }); +}); + +describe('preview specific functionality', () => { + const transform = async (source: string) => + ( + await configToCsfFactory( + { source, path: 'preview.ts' }, + { configType: 'preview', frameworkPackage: '@storybook/react-vite' } + ) + ).trim(); + + it('should contain a named config export', async () => { + await expect( + transform(dedent` + export default { + tags: ['test'], + }; + `) + ).resolves.toMatchInlineSnapshot(` + import { definePreview } from '@storybook/react-vite'; + + export default definePreview({ + tags: ['test'], + }); + `); + }); + + it('should remove legacy preview type imports', async () => { + await expect( + transform(dedent` + import type { Preview } from '@storybook/react-vite' + + const preview: Preview = { + tags: [] + }; + export default preview; + `) + ).resolves.toMatchInlineSnapshot(` + import { definePreview } from '@storybook/react-vite'; + + export default definePreview({ + tags: [], + }); + `); + }); +}); diff --git a/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.ts b/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.ts new file mode 100644 index 000000000000..9cf2bef83443 --- /dev/null +++ b/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.ts @@ -0,0 +1,170 @@ +/* eslint-disable no-underscore-dangle */ +import { types as t } from 'storybook/internal/babel'; +import { formatFileContent } from 'storybook/internal/common'; +import { loadConfig, printConfig } from 'storybook/internal/csf-tools'; + +import picocolors from 'picocolors'; + +import type { FileInfo } from '../../automigrate/codemod'; +import { logger } from '../csf-factories'; +import { + cleanupTypeImports, + getConfigProperties, + removeExportDeclarations, +} from './csf-factories-utils'; + +export async function configToCsfFactory( + info: FileInfo, + { configType, frameworkPackage }: { configType: 'main' | 'preview'; frameworkPackage: string }, + { dryRun = false, skipFormatting = false }: { dryRun?: boolean; skipFormatting?: boolean } = {} +) { + const config = loadConfig(info.source); + try { + config.parse(); + } catch (err) { + logger.log(`Error when parsing ${info.path}, skipping:\n${err}`); + return info.source; + } + + const methodName = configType === 'main' ? 'defineMain' : 'definePreview'; + const programNode = config._ast.program; + const hasNamedExports = Object.keys(config._exportDecls).length > 0; + + /** + * Scenario 1: Mixed exports + * + * ``` + * export const tags = []; + * export default { + * parameters: {}, + * }; + * ``` + * + * Transform into: `export default defineMain({ tags: [], parameters: {} })` + */ + if (config._exportsObject && hasNamedExports) { + const exportDecls = config._exportDecls; + + const defineConfigProps = getConfigProperties(exportDecls); + config._exportsObject.properties.push(...defineConfigProps); + + programNode.body = removeExportDeclarations(programNode, exportDecls); + } else if (config._exportsObject) { + /** + * Scenario 2: Default exports + * + * - Syntax 1: `default export const config = {}; export default config;` + * - Syntax 2: `export default {};` + * + * Transform into: `export default defineMain({})` + */ + const defineConfigCall = t.callExpression(t.identifier(methodName), [config._exportsObject]); + + let exportDefaultNode = null as any as t.ExportDefaultDeclaration; + let declarationNodeIndex = -1; + + programNode.body.forEach((node) => { + // Detect Syntax 1 + if (t.isExportDefaultDeclaration(node) && t.isIdentifier(node.declaration)) { + const declarationName = node.declaration.name; + + declarationNodeIndex = programNode.body.findIndex( + (n) => + t.isVariableDeclaration(n) && + n.declarations.some( + (d) => + t.isIdentifier(d.id) && + d.id.name === declarationName && + t.isObjectExpression(d.init) + ) + ); + + if (declarationNodeIndex !== -1) { + exportDefaultNode = node; + // remove the original declaration as it will become a default export + const declarationNode = programNode.body[declarationNodeIndex]; + if (t.isVariableDeclaration(declarationNode)) { + const id = declarationNode.declarations[0].id; + const variableName = t.isIdentifier(id) && id.name; + + if (variableName) { + programNode.body.splice(declarationNodeIndex, 1); + } + } + } + } else if (t.isExportDefaultDeclaration(node) && t.isObjectExpression(node.declaration)) { + // Detect Syntax 2 + exportDefaultNode = node; + } + }); + + if (exportDefaultNode !== null) { + exportDefaultNode.declaration = defineConfigCall; + } + } else if (hasNamedExports) { + /** + * Scenario 3: Named exports export const foo = {}; export bar = ''; + * + * Transform into: export default defineMain({ foo: {}, bar: '' }); + */ + const exportDecls = config._exportDecls; + const defineConfigProps = getConfigProperties(exportDecls); + + // Construct the `define` call + const defineConfigCall = t.callExpression(t.identifier(methodName), [ + t.objectExpression(defineConfigProps), + ]); + + // Remove all related named exports + programNode.body = removeExportDeclarations(programNode, exportDecls); + + // Add the new export default declaration + programNode.body.push(t.exportDefaultDeclaration(defineConfigCall)); + } + + const configImport = t.importDeclaration( + [t.importSpecifier(t.identifier(methodName), t.identifier(methodName))], + t.stringLiteral(frameworkPackage + `${configType === 'main' ? '/node' : ''}`) + ); + + // Check whether @storybook/framework import already exists + const existingImport = programNode.body.find( + (node) => + t.isImportDeclaration(node) && + node.importKind !== 'type' && + node.source.value === configImport.source.value + ); + + if (existingImport && t.isImportDeclaration(existingImport)) { + // If it does, check whether defineMain/definePreview is already imported + // and only add it if it's not + const hasMethodName = existingImport.specifiers.some( + (specifier) => + t.isImportSpecifier(specifier) && + t.isIdentifier(specifier.imported) && + specifier.imported.name === methodName + ); + + if (!hasMethodName) { + existingImport.specifiers.push( + t.importSpecifier(t.identifier(methodName), t.identifier(methodName)) + ); + } + } else { + // if not, add import { defineMain } from '@storybook/framework' + programNode.body.unshift(configImport); + } + + // Remove type imports – now inferred – from @storybook/* packages + const disallowList = ['StorybookConfig', 'Preview']; + programNode.body = cleanupTypeImports(programNode, disallowList); + + const output = printConfig(config).code; + + if (dryRun) { + logger.log(`Would write to ${picocolors.yellow(info.path)}:\n${picocolors.green(output)}`); + return info.source; + } + + return skipFormatting ? output : formatFileContent(info.path, output); +} diff --git a/code/lib/cli-storybook/src/codemod/helpers/csf-factories-utils.ts b/code/lib/cli-storybook/src/codemod/helpers/csf-factories-utils.ts new file mode 100644 index 000000000000..2d32386bb4ca --- /dev/null +++ b/code/lib/cli-storybook/src/codemod/helpers/csf-factories-utils.ts @@ -0,0 +1,78 @@ +import { types as t } from 'storybook/internal/babel'; + +export function cleanupTypeImports(programNode: t.Program, disallowList: string[]) { + return programNode.body.filter((node) => { + if (t.isImportDeclaration(node)) { + const { source, specifiers } = node; + + if (source.value.startsWith('@storybook/')) { + const allowedSpecifiers = specifiers.filter((specifier) => { + if (t.isImportSpecifier(specifier) && t.isIdentifier(specifier.imported)) { + return !disallowList.includes(specifier.imported.name); + } + // Retain non-specifier imports (e.g., namespace imports) + return true; + }); + + // Remove the entire import if no specifiers are left + if (allowedSpecifiers.length > 0) { + node.specifiers = allowedSpecifiers; + return true; + } + + // Remove the import if no specifiers remain + return false; + } + } + + // Retain all other nodes + return true; + // @TODO adding any for now, unsure how to fix the following error: + // error TS4058: Return type of exported function has or is using name 'BlockStatement' from external module "/code/core/dist/babel/index" but cannot be named + }) as any; +} + +export function removeExportDeclarations( + programNode: t.Program, + exportDecls: Record +) { + return programNode.body.filter((node) => { + if (t.isExportNamedDeclaration(node) && node.declaration) { + if (t.isVariableDeclaration(node.declaration)) { + // Handle variable declarations + node.declaration.declarations = node.declaration.declarations.filter( + (decl) => t.isIdentifier(decl.id) && !exportDecls[decl.id.name] + ); + return node.declaration.declarations.length > 0; + } else if (t.isFunctionDeclaration(node.declaration)) { + // Handle function declarations + const funcDecl = node.declaration; + return t.isIdentifier(funcDecl.id) && !exportDecls[funcDecl.id.name]; + } + } + return true; + // @TODO adding any for now, unsure how to fix the following error: + // error TS4058: Return type of exported function has or is using name 'ObjectProperty' from external module "/tmp/storybook/code/core/dist/babel/index" but cannot be named. + }) as any; +} + +export function getConfigProperties( + exportDecls: Record +) { + const properties = []; + + // Collect properties from named exports + for (const [name, decl] of Object.entries(exportDecls)) { + if (t.isVariableDeclarator(decl) && decl.init) { + properties.push(t.objectProperty(t.identifier(name), decl.init)); + } else if (t.isFunctionDeclaration(decl)) { + properties.push( + t.objectProperty(t.identifier(name), t.arrowFunctionExpression([], decl.body)) + ); + } + } + + // @TODO adding any for now, unsure how to fix the following error: + // error TS4058: Return type of exported function has or is using name 'ObjectProperty' from external module "/tmp/storybook/code/core/dist/babel/index" but cannot be named. + return properties as any; +} diff --git a/code/lib/cli-storybook/src/codemod/helpers/story-to-csf-factory.test.ts b/code/lib/cli-storybook/src/codemod/helpers/story-to-csf-factory.test.ts new file mode 100644 index 000000000000..12e920a772fd --- /dev/null +++ b/code/lib/cli-storybook/src/codemod/helpers/story-to-csf-factory.test.ts @@ -0,0 +1,513 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { formatFileContent } from '@storybook/core/common'; + +import path from 'path'; +import { dedent } from 'ts-dedent'; + +import { storyToCsfFactory } from './story-to-csf-factory'; + +expect.addSnapshotSerializer({ + serialize: (val: any) => (typeof val === 'string' ? val : val.toString()), + test: () => true, +}); + +describe('stories codemod', () => { + const transform = async (source: string) => + formatFileContent( + 'Component.stories.tsx', + await storyToCsfFactory( + { source, path: 'Component.stories.tsx' }, + { previewConfigPath: '#.storybook/preview', useSubPathImports: true } + ) + ); + describe('javascript', () => { + it('should wrap const declared meta', async () => { + await expect( + transform(dedent` + const meta = { title: 'Component' }; + export default meta; + `) + ).resolves.toMatchInlineSnapshot(` + import preview from '#.storybook/preview'; + + const meta = preview.meta({ title: 'Component' }); + `); + }); + + it('should transform and wrap inline default exported meta', async () => { + await expect( + transform(dedent` + export default { title: 'Component' }; + `) + ).resolves.toMatchInlineSnapshot(` + import preview from '#.storybook/preview'; + + const meta = preview.meta({ + title: 'Component', + }); + `); + }); + + it('should rename meta object to meta if it has a different name', async () => { + await expect( + transform(dedent` + const componentMeta = { title: 'Component' }; + export default componentMeta; + `) + ).resolves.toMatchInlineSnapshot(` + import preview from '#.storybook/preview'; + + const meta = preview.meta({ title: 'Component' }); + `); + }); + + it('should wrap stories in a meta.story method', async () => { + await expect( + transform(dedent` + const componentMeta = { title: 'Component' }; + export default componentMeta; + export const A = { + args: { primary: true }, + render: (args) => + }; + `) + ).resolves.toMatchInlineSnapshot(` + import preview from '#.storybook/preview'; + + const meta = preview.meta({ title: 'Component' }); + export const A = meta.story({ + args: { primary: true }, + render: (args) => , + }); + `); + }); + + it('should respect existing config imports', async () => { + await expect( + transform(dedent` + import { decorators } from "#.storybook/preview"; + const componentMeta = { title: 'Component' }; + export default componentMeta; + export const A = { + args: { primary: true }, + render: (args) => + }; + `) + ).resolves.toMatchInlineSnapshot(` + import preview, { decorators } from '#.storybook/preview'; + + const meta = preview.meta({ title: 'Component' }); + export const A = meta.story({ + args: { primary: true }, + render: (args) => , + }); + `); + }); + + it('should reuse existing default config import name', async () => { + await expect( + transform(dedent` + import previewConfig from "#.storybook/preview"; + const componentMeta = { title: 'Component' }; + export default componentMeta; + export const A = { + args: { primary: true }, + render: (args) => + }; + `) + ).resolves.toMatchInlineSnapshot(` + import previewConfig from '#.storybook/preview'; + + const meta = previewConfig.meta({ title: 'Component' }); + export const A = meta.story({ + args: { primary: true }, + render: (args) => , + }); + `); + }); + + it('if there is an existing local constant called preview, rename storybook preview import', async () => { + await expect( + transform(dedent` + const componentMeta = { title: 'Component' }; + export default componentMeta; + const preview = {}; + export const A = { + args: { primary: true }, + render: (args) => + }; + `) + ).resolves.toMatchInlineSnapshot(` + import storybookPreview from '#.storybook/preview'; + + const meta = storybookPreview.meta({ title: 'Component' }); + const preview = {}; + export const A = meta.story({ + args: { primary: true }, + render: (args) => , + }); + `); + }); + + it('migrate reused properties of other stories from `Story.xyz` to `Story.input.xyz`', async () => { + await expect( + transform(dedent` + export default { title: 'Component' }; + const someData = {}; + + export const A = {}; + + export const B = { + ...A, + args: { + ...A.args, + ...someData, + }, + }; + export const C = { + render: async () => { + return JSON.stringify({ + ...A.argTypes, + ...B, + }) + } + }; + `) + ).resolves.toMatchInlineSnapshot(` + import preview from '#.storybook/preview'; + + const meta = preview.meta({ + title: 'Component', + }); + + const someData = {}; + + export const A = meta.story({}); + + export const B = meta.story({ + ...A.input, + args: { + ...A.input.args, + ...someData, + }, + }); + export const C = meta.story({ + render: async () => { + return JSON.stringify({ + ...A.input.argTypes, + ...B.input, + }); + }, + }); + `); + }); + + it('does not migrate reused properties from disallowed list', async () => { + await expect( + transform(dedent` + export default { title: 'Component' }; + export const A = {}; + export const B = { + play: async () => { + await A.play(); + } + }; + export const C = A.run; + export const D = A.extends({}); + `) + ).resolves.toMatchInlineSnapshot(` + import preview from '#.storybook/preview'; + + const meta = preview.meta({ + title: 'Component', + }); + + export const A = meta.story({}); + export const B = meta.story({ + play: async () => { + await A.play(); + }, + }); + export const C = A.run; + export const D = A.extends({}); + `); + }); + + it('should support non-conventional formats (INCOMPLETE)', async () => { + const transformed = await transform(dedent` + import { A as Component } from './Button'; + import * as Stories from './Other.stories'; + import someData from './fixtures' + export default { + component: Component, + // not supported yet (story coming from another file) + args: Stories.A.args + }; + const data = {}; + export const A = () => {}; + // not supported yet (story as function) + export function B() { }; + // not supported yet (story redeclared) + const C = { ...A, args: data, }; + export { C }; + `); + + expect(transformed).toContain('A = meta.story'); + // @TODO: when we support these, uncomment these lines + // expect(transformed).toContain('B = meta.story'); + // expect(transformed).toContain('C = meta.story'); + }); + + it('converts the preview import path based on useSubPathImports flag', async () => { + const relativeMock = vi.spyOn(path, 'relative').mockReturnValue('../../preview.ts'); + + try { + await expect( + formatFileContent( + 'Component.stories.tsx', + await storyToCsfFactory( + { + source: dedent` + import preview, { extra } from '../../../.storybook/preview'; + export default {}; + `, + path: 'Component.stories.tsx', + }, + { previewConfigPath: '#.storybook/preview', useSubPathImports: true } + ) + ) + ).resolves.toMatchInlineSnapshot(` + import preview, { extra } from '#.storybook/preview'; + + const meta = preview.meta({}); + `); + + await expect( + formatFileContent( + 'Component.stories.tsx', + await storyToCsfFactory( + { + source: dedent` + import preview, { extra } from '#.storybook/preview'; + export default {}; + `, + path: 'Component.stories.tsx', + }, + { previewConfigPath: '#.storybook/preview', useSubPathImports: false } + ) + ) + ).resolves.toMatchInlineSnapshot(` + import preview, { extra } from '../../preview'; + + const meta = preview.meta({}); + `); + } finally { + relativeMock.mockRestore(); + } + }); + + it('converts CSF1 into CSF4 with render', async () => { + await expect( + transform(dedent` + const meta = { title: 'Component' }; + export default meta; + export const CSF1Story = () =>
Hello
; + `) + ).resolves.toMatchInlineSnapshot(` + import preview from '#.storybook/preview'; + + const meta = preview.meta({ title: 'Component' }); + export const CSF1Story = meta.story({ + render: () =>
Hello
, + }); + `); + }); + }); + + describe('typescript', () => { + const inlineMetaSatisfies = dedent` + import { Meta, StoryObj as CSF3 } from '@storybook/react'; + import { ComponentProps } from './Component'; + + export default { title: 'Component', component: Component } satisfies Meta; + + export const A: CSF3 = { + args: { primary: true } + }; + `; + it('meta satisfies syntax', async () => { + await expect(transform(inlineMetaSatisfies)).resolves.toMatchInlineSnapshot(` + import preview from '#.storybook/preview'; + + import { ComponentProps } from './Component'; + + const meta = preview.meta({ title: 'Component', component: Component }); + + export const A = meta.story({ + args: { primary: true }, + }); + `); + }); + + const inlineMetaAs = dedent` + import { Meta, StoryObj as CSF3 } from '@storybook/react'; + import { ComponentProps } from './Component'; + + export default { title: 'Component', component: Component } as Meta; + + export const A: CSF3 = { + args: { primary: true } + }; + `; + it('meta as syntax', async () => { + await expect(transform(inlineMetaAs)).resolves.toMatchInlineSnapshot(` + import preview from '#.storybook/preview'; + + import { ComponentProps } from './Component'; + + const meta = preview.meta({ title: 'Component', component: Component }); + + export const A = meta.story({ + args: { primary: true }, + }); + `); + }); + const metaSatisfies = dedent` + import { Meta, StoryObj as CSF3 } from '@storybook/react'; + import { ComponentProps } from './Component'; + + const meta = { title: 'Component', component: Component } satisfies Meta + export default meta; + + export const A: CSF3 = { + args: { primary: true } + }; + `; + it('meta satisfies syntax', async () => { + await expect(transform(metaSatisfies)).resolves.toMatchInlineSnapshot(` + import preview from '#.storybook/preview'; + + import { ComponentProps } from './Component'; + + const meta = preview.meta({ title: 'Component', component: Component }); + + export const A = meta.story({ + args: { primary: true }, + }); + `); + }); + + const metaAs = dedent` + import { Meta, StoryObj as CSF3 } from '@storybook/react'; + import { ComponentProps } from './Component'; + + const meta = { title: 'Component', component: Component } as Meta + export default meta; + + export const A: CSF3 = { + args: { primary: true } + }; + `; + it('meta as syntax', async () => { + await expect(transform(metaAs)).resolves.toMatchInlineSnapshot(` + import preview from '#.storybook/preview'; + + import { ComponentProps } from './Component'; + + const meta = preview.meta({ title: 'Component', component: Component }); + + export const A = meta.story({ + args: { primary: true }, + }); + `); + }); + + const storySatisfies = dedent` + import { Meta, StoryObj as CSF3 } from '@storybook/react'; + import { ComponentProps } from './Component'; + + const meta = { title: 'Component', component: Component } as Meta + export default meta; + + export const A = { + args: { primary: true } + } satisfies CSF3; + `; + it('story satisfies syntax', async () => { + await expect(transform(storySatisfies)).resolves.toMatchInlineSnapshot(` + import preview from '#.storybook/preview'; + + import { ComponentProps } from './Component'; + + const meta = preview.meta({ title: 'Component', component: Component }); + + export const A = meta.story({ + args: { primary: true }, + }); + `); + }); + + const storyAs = dedent` + import { Meta, StoryObj as CSF3 } from '@storybook/react'; + import { ComponentProps } from './Component'; + + const meta = { title: 'Component', component: Component } as Meta + export default meta; + + export const A = { + args: { primary: true } + } as CSF3; + `; + it('story as syntax', async () => { + await expect(transform(storyAs)).resolves.toMatchInlineSnapshot(` + import preview from '#.storybook/preview'; + + import { ComponentProps } from './Component'; + + const meta = preview.meta({ title: 'Component', component: Component }); + + export const A = meta.story({ + args: { primary: true }, + }); + `); + }); + + it('should yield the same result to all syntaxes', async () => { + const allSnippets = await Promise.all([ + transform(inlineMetaSatisfies), + transform(inlineMetaAs), + transform(metaSatisfies), + transform(metaAs), + transform(storySatisfies), + transform(storyAs), + ]); + + allSnippets.forEach((result) => { + expect(result).toEqual(allSnippets[0]); + }); + }); + + it('should remove unused Story types', async () => { + await expect( + transform( + `import { Meta, StoryObj as CSF3 } from '@storybook/react'; + import { ComponentProps } from './Component'; + + export default {}; + type Story = StoryObj; + + export const A: Story = {};` + ) + ).resolves.toMatchInlineSnapshot(` + import preview from '#.storybook/preview'; + + import { ComponentProps } from './Component'; + + const meta = preview.meta({}); + + export const A = meta.story({}); + `); + }); + }); +}); diff --git a/code/lib/cli-storybook/src/codemod/helpers/story-to-csf-factory.ts b/code/lib/cli-storybook/src/codemod/helpers/story-to-csf-factory.ts new file mode 100644 index 000000000000..a419dfb53fce --- /dev/null +++ b/code/lib/cli-storybook/src/codemod/helpers/story-to-csf-factory.ts @@ -0,0 +1,293 @@ +/* eslint-disable no-underscore-dangle */ +import { types as t, traverse } from 'storybook/internal/babel'; +import { isValidPreviewPath, loadCsf, printCsf } from 'storybook/internal/csf-tools'; + +import path from 'path'; + +import type { FileInfo } from '../../automigrate/codemod'; +import { logger } from '../csf-factories'; +import { cleanupTypeImports } from './csf-factories-utils'; + +// Name of properties that should not be renamed to `Story.input.xyz` +const reuseDisallowList = ['play', 'run', 'extends']; + +// Name of types that should be removed from the import list +const typesDisallowList = [ + 'Story', + 'StoryFn', + 'StoryObj', + 'Meta', + 'MetaObj', + 'ComponentStory', + 'ComponentMeta', +]; + +type Options = { previewConfigPath: string; useSubPathImports: boolean }; + +export async function storyToCsfFactory( + info: FileInfo, + { previewConfigPath, useSubPathImports }: Options +) { + const csf = loadCsf(info.source, { makeTitle: () => 'FIXME' }); + try { + csf.parse(); + } catch (err) { + logger.log(`Error when parsing ${info.path}, skipping:\n${err}`); + return info.source; + } + + const metaVariableName = 'meta'; + + /** + * Add the preview import if it doesn't exist yet: + * + * `import preview from '#.storybook/preview'`; + */ + const programNode = csf._ast.program; + let previewImport: t.ImportDeclaration | undefined; + + // Check if a root-level constant named 'preview' exists + const hasRootLevelConfig = programNode.body.some( + (n) => + t.isVariableDeclaration(n) && + n.declarations.some((declaration) => t.isIdentifier(declaration.id, { name: 'preview' })) + ); + + let previewPath = '#.storybook/preview'; + if (!useSubPathImports) { + // calculate relative path from story file to preview file + const relativePath = path.relative(path.dirname(info.path), previewConfigPath); + const { dir, name } = path.parse(relativePath); + + // Construct the path manually and replace Windows backslashes + previewPath = `${dir ? `${dir}/` : ''}${name}`; + + // account for stories in the same path as preview file + if (!previewPath.startsWith('.')) { + previewPath = `./${previewPath}`; + } + + // Convert Windows backslashes to forward slashes + previewPath = previewPath.replace(/\\/g, '/'); + } + + let sbConfigImportName = hasRootLevelConfig ? 'storybookPreview' : 'preview'; + + const sbConfigImportSpecifier = t.importDefaultSpecifier(t.identifier(sbConfigImportName)); + + programNode.body.forEach((node) => { + if (t.isImportDeclaration(node) && isValidPreviewPath(node.source.value)) { + const defaultImportSpecifier = node.specifiers.find((specifier) => + t.isImportDefaultSpecifier(specifier) + ); + + if (!defaultImportSpecifier) { + node.specifiers.push(sbConfigImportSpecifier); + } else if (defaultImportSpecifier.local.name !== sbConfigImportName) { + sbConfigImportName = defaultImportSpecifier.local.name; + } + + previewImport = node; + } + }); + + const hasMeta = !!csf._meta; + + // @TODO: Support unconventional formats: + // `export function Story() { };` and `export { Story }; + // These are not part of csf._storyExports but rather csf._storyStatements and are tricky to support. + Object.entries(csf._storyExports).forEach(([_key, decl]) => { + const id = decl.id; + const declarator = decl as t.VariableDeclarator; + let init = t.isVariableDeclarator(declarator) ? declarator.init : undefined; + + if (t.isIdentifier(id) && init) { + // Remove type annotations e.g. A in `const Story: A = {};` + if (id.typeAnnotation) { + id.typeAnnotation = null; + } + + // Remove type annotations e.g. A in `const Story = {} satisfies A;` + if (t.isTSSatisfiesExpression(init) || t.isTSAsExpression(init)) { + init = init.expression; + } + + if (t.isObjectExpression(init)) { + // Wrap the object in `meta.story()` + declarator.init = t.callExpression( + t.memberExpression(t.identifier(metaVariableName), t.identifier('story')), + [init] + ); + } else if (t.isArrowFunctionExpression(init)) { + // Transform CSF1 to meta.story({ render: }) + const renderProperty = t.objectProperty(t.identifier('render'), init); + + const objectExpression = t.objectExpression([renderProperty]); + + declarator.init = t.callExpression( + t.memberExpression(t.identifier(metaVariableName), t.identifier('story')), + [objectExpression] + ); + } + } + }); + + const storyExportDecls = new Map( + Object.entries(csf._storyExports).filter( + ( + entry + ): entry is [string, Exclude<(typeof csf._storyExports)[string], t.FunctionDeclaration>] => + !t.isFunctionDeclaration(entry[1]) + ) + ); + + // For each story, replace any reference of story reuse e.g. + // Story.args -> Story.input.args + traverse(csf._ast, { + Identifier(nodePath) { + const binding = nodePath.scope.getBinding(nodePath.node.name); + + // Check if the identifier corresponds to a story export + if (binding && storyExportDecls.has(binding.identifier.name)) { + const parent = nodePath.parent; + + // Skip declarations (e.g., `const Story = {};`) + if (t.isVariableDeclarator(parent) && parent.id === nodePath.node) { + return; + } + + // Skip import statements e.g.`import { X as Story }` + if (t.isImportSpecifier(parent)) { + return; + } + + // Skip export statements e.g.`export const Story` or `export { Story }` + if (t.isExportSpecifier(parent)) { + return; + } + + // Skip if it's already `Story.input` + if (t.isMemberExpression(parent) && t.isIdentifier(parent.property, { name: 'input' })) { + return; + } + // Check if the property name is in the disallow list + if ( + t.isMemberExpression(parent) && + t.isIdentifier(parent.property) && + reuseDisallowList.includes(parent.property.name) + ) { + return; + } + + try { + // Replace the identifier with `Story.input` + nodePath.replaceWith( + t.memberExpression(t.identifier(nodePath.node.name), t.identifier('input')) + ); + } catch (err: any) { + // This is a tough one to support, we just skip for now. + // Relates to `Stories.Story.args` where Stories is coming from another file. We can't know whether it should be transformed or not. + if (err.message.includes(`instead got "MemberExpression"`)) { + return; + } else { + throw err; + } + } + } + }, + }); + + // modify meta + if (csf._metaPath) { + let declaration = csf._metaPath.node.declaration; + if (t.isTSSatisfiesExpression(declaration) || t.isTSAsExpression(declaration)) { + declaration = declaration.expression; + } + + if (t.isObjectExpression(declaration)) { + const metaVariable = t.variableDeclaration('const', [ + t.variableDeclarator( + t.identifier(metaVariableName), + t.callExpression( + t.memberExpression(t.identifier(sbConfigImportName), t.identifier('meta')), + [declaration] + ) + ), + ]); + csf._metaPath.replaceWith(metaVariable); + } else if (t.isIdentifier(declaration)) { + /** + * Transform const declared metas: + * + * `const meta = {}; export default meta;` + * + * Into a meta call: + * + * `const meta = preview.meta({ title: 'A' });` + */ + const binding = csf._metaPath.scope.getBinding(declaration.name); + if (binding && binding.path.isVariableDeclarator()) { + const originalName = declaration.name; + + // Always rename the meta variable to 'meta' + binding.path.node.id = t.identifier(metaVariableName); + + let init = binding.path.node.init; + if (t.isTSSatisfiesExpression(init) || t.isTSAsExpression(init)) { + init = init.expression; + } + if (t.isObjectExpression(init)) { + binding.path.node.init = t.callExpression( + t.memberExpression(t.identifier(sbConfigImportName), t.identifier('meta')), + [init] + ); + } + + // Update all references to the original name + csf._metaPath.scope.rename(originalName, metaVariableName); + } + + // Remove the default export, it's not needed anymore + csf._metaPath.remove(); + } + } + + if (previewImport) { + // If there is alerady an import, just update the path. This is useful for users + // who rerun the codemod to change the preview import to use (or not) subpaths + if (previewImport.source.value !== previewPath) { + previewImport.source = t.stringLiteral(previewPath); + } + } else if (hasMeta) { + // If the import doesn't exist, create a new one + const configImport = t.importDeclaration( + [t.importDefaultSpecifier(t.identifier(sbConfigImportName))], + t.stringLiteral(previewPath) + ); + programNode.body.unshift(configImport); + } + + // Remove type imports – now inferred – from @storybook/* packages + programNode.body = cleanupTypeImports(programNode, typesDisallowList); + + // Remove unused type aliases e.g. `type Story = StoryObj;` + programNode.body.forEach((node, index) => { + if (t.isTSTypeAliasDeclaration(node)) { + const isUsed = programNode.body.some((otherNode) => { + if (t.isVariableDeclaration(otherNode)) { + return otherNode.declarations.some( + (declaration) => + t.isIdentifier(declaration.init) && declaration.init.name === node.id.name + ); + } + return false; + }); + + if (!isUsed) { + programNode.body.splice(index, 1); + } + } + }); + + return printCsf(csf).code; +} diff --git a/code/lib/cli-storybook/src/migrate.ts b/code/lib/cli-storybook/src/migrate.ts index e985971b5a05..9075a9d9e8b2 100644 --- a/code/lib/cli-storybook/src/migrate.ts +++ b/code/lib/cli-storybook/src/migrate.ts @@ -9,6 +9,7 @@ import { listCodemods, runCodemod } from '@storybook/codemod'; import { runFixes } from './automigrate'; import { mdxToCSF } from './automigrate/fixes/mdx-to-csf'; +import { getStorybookData } from './automigrate/helpers/mainConfigFile'; const logger = console; @@ -33,15 +34,11 @@ export async function migrate( if (migration === 'mdx-to-csf' && !dryRun) { const packageManager = JsPackageManagerFactory.getPackageManager(); - const [packageJson, storybookVersion] = await Promise.all([ - packageManager.retrievePackageJson(), - getCoercedStorybookVersion(packageManager), - ]); - const { configDir: inferredConfigDir, mainConfig: mainConfigPath } = getStorybookInfo( - packageJson, - userSpecifiedConfigDir - ); - const configDir = userSpecifiedConfigDir || inferredConfigDir || '.storybook'; + const { configDir, mainConfig, mainConfigPath, storybookVersion, packageJson } = + await getStorybookData({ + packageManager, + configDir: userSpecifiedConfigDir, + }); // GUARDS if (!storybookVersion) { @@ -57,6 +54,8 @@ export async function migrate( configDir, mainConfigPath, packageManager, + mainConfig, + packageJson, storybookVersion, beforeVersion: storybookVersion, isUpgrade: false, diff --git a/code/lib/cli-storybook/src/sandbox-templates.ts b/code/lib/cli-storybook/src/sandbox-templates.ts index f70449318095..17f3e2982da6 100644 --- a/code/lib/cli-storybook/src/sandbox-templates.ts +++ b/code/lib/cli-storybook/src/sandbox-templates.ts @@ -75,6 +75,7 @@ export type Template = { disableDocs?: boolean; extraDependencies?: string[]; editAddons?: (addons: string[]) => string[]; + useCsfFactory?: boolean; }; /** * Flag to indicate that this template is a secondary template, which is used mainly to test @@ -106,6 +107,7 @@ export const baseTemplates = { skipTasks: ['e2e-tests-dev', 'bench', 'vitest-integration'], modifications: { + useCsfFactory: true, extraDependencies: ['prop-types'], mainConfig: (config) => { const stories = config.getFieldValue>(['stories']); @@ -136,6 +138,7 @@ export const baseTemplates = { builder: '@storybook/builder-webpack5', }, modifications: { + useCsfFactory: true, extraDependencies: ['prop-types'], }, }, @@ -149,6 +152,7 @@ export const baseTemplates = { builder: '@storybook/builder-webpack5', }, modifications: { + useCsfFactory: true, mainConfig: { features: { experimentalRSC: true, @@ -169,6 +173,7 @@ export const baseTemplates = { builder: '@storybook/builder-webpack5', }, modifications: { + useCsfFactory: true, mainConfig: { features: { experimentalRSC: true, @@ -189,6 +194,7 @@ export const baseTemplates = { builder: '@storybook/builder-webpack5', }, modifications: { + useCsfFactory: true, mainConfig: { features: { experimentalRSC: true, @@ -209,6 +215,7 @@ export const baseTemplates = { builder: '@storybook/builder-webpack5', }, modifications: { + useCsfFactory: true, mainConfig: { features: { experimentalRSC: true, @@ -229,6 +236,7 @@ export const baseTemplates = { builder: '@storybook/builder-vite', }, modifications: { + useCsfFactory: true, mainConfig: { framework: '@storybook/experimental-nextjs-vite', features: { @@ -255,6 +263,7 @@ export const baseTemplates = { builder: '@storybook/builder-vite', }, modifications: { + useCsfFactory: true, mainConfig: { framework: '@storybook/experimental-nextjs-vite', features: { @@ -280,6 +289,7 @@ export const baseTemplates = { builder: '@storybook/builder-vite', }, modifications: { + useCsfFactory: true, extraDependencies: ['prop-types'], mainConfig: { features: { @@ -298,6 +308,7 @@ export const baseTemplates = { builder: '@storybook/builder-vite', }, modifications: { + useCsfFactory: true, extraDependencies: ['prop-types'], mainConfig: { features: { @@ -329,6 +340,7 @@ export const baseTemplates = { builder: '@storybook/builder-vite', }, modifications: { + useCsfFactory: true, extraDependencies: ['prop-types'], mainConfig: { features: { @@ -347,6 +359,7 @@ export const baseTemplates = { builder: '@storybook/builder-webpack5', }, modifications: { + useCsfFactory: true, extraDependencies: ['prop-types'], }, skipTasks: ['e2e-tests-dev', 'bench', 'vitest-integration'], @@ -361,6 +374,7 @@ export const baseTemplates = { builder: '@storybook/builder-webpack5', }, modifications: { + useCsfFactory: true, extraDependencies: ['prop-types'], }, skipTasks: ['e2e-tests-dev', 'bench', 'vitest-integration'], @@ -385,6 +399,7 @@ export const baseTemplates = { builder: '@storybook/builder-webpack5', }, modifications: { + useCsfFactory: true, extraDependencies: ['prop-types'], }, skipTasks: ['e2e-tests-dev', 'bench', 'vitest-integration'], diff --git a/code/lib/cli-storybook/src/sandbox.ts b/code/lib/cli-storybook/src/sandbox.ts index d334b9a78534..03ba2178b1b0 100644 --- a/code/lib/cli-storybook/src/sandbox.ts +++ b/code/lib/cli-storybook/src/sandbox.ts @@ -1,5 +1,5 @@ import { existsSync } from 'node:fs'; -import { readdir } from 'node:fs/promises'; +import { readdir, rm } from 'node:fs/promises'; import { isAbsolute, join } from 'node:path'; import type { PackageManagerName } from 'storybook/internal/common'; @@ -166,6 +166,7 @@ export const sandbox = async ({ const outputDirectoryName = outputDirectory || templateId; if (selectedDirectory && existsSync(`${selectedDirectory}`)) { logger.info(`⚠️ ${selectedDirectory} already exists! Overwriting...`); + await rm(selectedDirectory, { recursive: true, force: true }); } if (!selectedDirectory) { diff --git a/code/lib/cli-storybook/src/upgrade.ts b/code/lib/cli-storybook/src/upgrade.ts index 6657fb0ee691..5a8bd0a4efb0 100644 --- a/code/lib/cli-storybook/src/upgrade.ts +++ b/code/lib/cli-storybook/src/upgrade.ts @@ -24,6 +24,7 @@ import semver, { clean, eq, lt, prerelease } from 'semver'; import { dedent } from 'ts-dedent'; import { autoblock } from './autoblock/index'; +import { getStorybookData } from './automigrate/helpers/mainConfigFile'; import { automigrate } from './automigrate/index'; type Package = { @@ -157,10 +158,7 @@ export const doUpgrade = async ({ logger.warn(new UpgradeStorybookToSameVersionError({ beforeVersion }).message); } - const [latestCLIVersionOnNPM, packageJson] = await Promise.all([ - packageManager.latestVersion('storybook'), - packageManager.retrievePackageJson(), - ]); + const latestCLIVersionOnNPM = await packageManager.latestVersion('storybook'); const isCLIOutdated = lt(currentCLIVersion, latestCLIVersionOnNPM); const isCLIExactLatest = currentCLIVersion === latestCLIVersionOnNPM; @@ -198,13 +196,11 @@ export const doUpgrade = async ({ let results; - const { configDir: inferredConfigDir, mainConfig: mainConfigPath } = getStorybookInfo( - packageJson, - userSpecifiedConfigDir - ); - const configDir = userSpecifiedConfigDir || inferredConfigDir || '.storybook'; - - const mainConfig = await loadMainConfig({ configDir }); + const { configDir, mainConfig, mainConfigPath, previewConfigPath, packageJson } = + await getStorybookData({ + packageManager, + configDir: userSpecifiedConfigDir, + }); // GUARDS if (!beforeVersion) { @@ -277,7 +273,10 @@ export const doUpgrade = async ({ dryRun, yes, packageManager, + packageJson, + mainConfig, configDir, + previewConfigPath, mainConfigPath, beforeVersion, storybookVersion: currentCLIVersion, diff --git a/code/lib/cli-storybook/src/util.ts b/code/lib/cli-storybook/src/util.ts new file mode 100644 index 000000000000..f8fa3f3d6f3a --- /dev/null +++ b/code/lib/cli-storybook/src/util.ts @@ -0,0 +1,4 @@ +import boxen, { type Options } from 'boxen'; + +export const printBoxedMessage = (message: string, style?: Options) => + boxen(message, { borderStyle: 'round', padding: 1, borderColor: '#F1618C', ...style }); diff --git a/code/lib/cli-storybook/tsconfig.json b/code/lib/cli-storybook/tsconfig.json index d0c5371602ac..c541cfa91e50 100644 --- a/code/lib/cli-storybook/tsconfig.json +++ b/code/lib/cli-storybook/tsconfig.json @@ -3,5 +3,9 @@ "compilerOptions": { "resolveJsonModule": true }, - "include": ["src/**/*"] + "include": [ + "src/**/*", + "../../core/src/common/utils/get-addon-annotations.test.ts", + "../../core/src/common/utils/get-addon-annotations.ts" + ] } diff --git a/code/lib/cli/package.json b/code/lib/cli/package.json index c4808dff99d0..832afd5c2cb0 100644 --- a/code/lib/cli/package.json +++ b/code/lib/cli/package.json @@ -99,6 +99,11 @@ "import": "./core/types/index.js", "require": "./core/types/index.cjs" }, + "./internal/csf": { + "types": "./core/csf/index.d.ts", + "import": "./core/csf/index.js", + "require": "./core/csf/index.cjs" + }, "./internal/csf-tools": { "types": "./core/csf-tools/index.d.ts", "import": "./core/csf-tools/index.js", @@ -189,11 +194,6 @@ }, "./internal/preview/runtime": { "import": "./core/preview/runtime.js" - }, - "./internal/csf": { - "types": "./core/csf/index.d.ts", - "import": "./core/csf/index.js", - "require": "./core/csf/index.cjs" } }, "main": "dist/index.cjs", diff --git a/code/lib/codemod/package.json b/code/lib/codemod/package.json index e2d9774792ef..2d591ed90aec 100644 --- a/code/lib/codemod/package.json +++ b/code/lib/codemod/package.json @@ -89,14 +89,14 @@ "bundler": { "entries": [ "./src/index.ts", - "./src/transforms/add-component-parameters.js", + "./src/transforms/storiesof-to-csf.js", + "./src/transforms/mdx-to-csf.ts", "./src/transforms/csf-2-to-3.ts", "./src/transforms/csf-hoist-story-annotations.js", "./src/transforms/find-implicit-spies.ts", - "./src/transforms/mdx-to-csf.ts", + "./src/transforms/add-component-parameters.js", "./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", "./src/transforms/update-organisation-name.js", "./src/transforms/upgrade-deprecated-types.ts", diff --git a/code/package.json b/code/package.json index d313e7a7b8a5..c935363273d0 100644 --- a/code/package.json +++ b/code/package.json @@ -197,7 +197,7 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-local-rules": "portal:../scripts/eslint-plugin-local-rules", "eslint-plugin-playwright": "^1.6.2", - "eslint-plugin-storybook": "^0.8.0", + "eslint-plugin-storybook": "0.11.3--canary.187.1af857a.0", "github-release-from-changelog": "^2.1.1", "glob": "^10.0.0", "happy-dom": "^14.12.0", diff --git a/code/renderers/react/package.json b/code/renderers/react/package.json index 0eb9a2ff0caa..65985cf1d193 100644 --- a/code/renderers/react/package.json +++ b/code/renderers/react/package.json @@ -25,6 +25,11 @@ "import": "./dist/index.mjs", "require": "./dist/index.js" }, + "./preview": { + "types": "./dist/preview.d.ts", + "import": "./dist/preview.mjs", + "require": "./dist/preview.js" + }, "./experimental-playwright": { "types": "./dist/playwright.d.ts", "import": "./dist/playwright.mjs", @@ -44,6 +49,9 @@ "*": [ "dist/index.d.ts" ], + "preview": [ + "dist/preview.d.ts" + ], "experimental-playwright": [ "dist/playwright.d.ts" ] @@ -116,6 +124,7 @@ "entries": [ "./src/index.ts", "./src/preset.ts", + "./src/preview.tsx", "./src/entry-preview.tsx", "./src/entry-preview-docs.ts", "./src/entry-preview-rsc.tsx", diff --git a/code/renderers/react/src/__test__/Button.csf4.stories.tsx b/code/renderers/react/src/__test__/Button.csf4.stories.tsx new file mode 100644 index 000000000000..6a92532ab691 --- /dev/null +++ b/code/renderers/react/src/__test__/Button.csf4.stories.tsx @@ -0,0 +1,298 @@ +import React, { useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; + +import { expect, fn, mocked, userEvent, within } from '@storybook/test'; + +import { action } from '@storybook/addon-actions'; + +import { definePreview } from '../preview'; +import { Button } from './Button'; + +const preview = definePreview({}); + +const meta = preview.meta({ + id: 'button-component', + title: 'Example/CSF4/Button', + component: Button, + argTypes: { + backgroundColor: { control: 'color' }, + }, + args: { + children: 'Children coming from meta args', + }, +}); + +export const CSF2Secondary = meta.story({ + render: (args) => { + return + + ); + }, + name: 'WithLocale', +}); + +export const CSF2StoryWithParamsAndDecorator = meta.story({ + render: (args) => { + return + + ); + }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement); + await step('Step label', async () => { + const inputEl = canvas.getByTestId('input'); + const buttonEl = canvas.getByRole('button'); + await userEvent.click(buttonEl); + await userEvent.type(inputEl, 'Hello world!'); + + await expect(inputEl).toHaveValue('Hello world!'); + await expect(buttonEl).toHaveTextContent('I am clicked'); + }); + }, +}); + +export const CSF3InputFieldFilled = meta.story({ + render: () => { + return ; + }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement); + await step('Step label', async () => { + const inputEl = canvas.getByTestId('input'); + await userEvent.type(inputEl, 'Hello world!'); + await expect(inputEl).toHaveValue('Hello world!'); + }); + }, +}); + +const mockFn = fn(); +export const LoaderStory = meta.story({ + args: { + // @ts-expect-error TODO: add a way to provide custom args/argTypes + mockFn, + }, + loaders: [ + async () => { + mockFn.mockReturnValueOnce('mockFn return value'); + return { + value: 'loaded data', + }; + }, + ], + render: (args: any & { mockFn: (val: string) => string }, { loaded }) => { + const data = args.mockFn('render'); + return ( +
+
{loaded.value}
+
{String(data)}
+
+ ); + }, + play: async () => { + expect(mockFn).toHaveBeenCalledWith('render'); + }, +}); + +export const MountInPlayFunction = meta.story({ + args: { + // @ts-expect-error TODO: add a way to provide custom args/argTypes + mockFn: fn(), + }, + play: async ({ args, mount, context }) => { + // equivalent of loaders + const loadedData = await Promise.resolve('loaded data'); + // @ts-expect-error TODO: add a way to provide custom args/argTypes + mocked(args.mockFn).mockReturnValueOnce('mockFn return value'); + // equivalent of render + // @ts-expect-error TODO: add a way to provide custom args/argTypes + const data = args.mockFn('render'); + // TODO refactor this in the mount args PR + context.originalStoryFn = () => ( +
+
{loadedData}
+
{String(data)}
+
+ ); + await mount(); + + // equivalent of play + // @ts-expect-error TODO: add a way to provide custom args/argTypes + expect(args.mockFn).toHaveBeenCalledWith('render'); + }, +}); + +export const MountInPlayFunctionThrow = meta.story({ + play: async () => { + throw new Error('Error thrown in play'); + }, +}); + +export const WithActionArg = meta.story({ + args: { + // @ts-expect-error TODO: add a way to provide custom args/argTypes + someActionArg: action('some-action-arg'), + }, + render: (args) => { + // @ts-expect-error TODO: add a way to provide custom args/argTypes + args.someActionArg('in render'); + return ( + + , + modalContainer + ) + : null; + + return ( + <> + + {modalContent} + + ); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const openModalButton = await canvas.getByRole('button', { name: /open modal/i }); + await userEvent.click(openModalButton); + await expect(within(document.body).getByRole('dialog')).toBeInTheDocument(); + }, +}); diff --git a/code/renderers/react/src/__test__/__snapshots__/portable-stories-factory.test.tsx.snap b/code/renderers/react/src/__test__/__snapshots__/portable-stories-factory.test.tsx.snap new file mode 100644 index 000000000000..3f00ff746281 --- /dev/null +++ b/code/renderers/react/src/__test__/__snapshots__/portable-stories-factory.test.tsx.snap @@ -0,0 +1,185 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Renders CSF2Secondary story 1`] = ` + +
+ +
+ +`; + +exports[`Renders CSF2StoryWithParamsAndDecorator story 1`] = ` + +
+ +
+ +`; + +exports[`Renders CSF3Button story 1`] = ` + +
+ +
+ +`; + +exports[`Renders CSF3ButtonWithRender story 1`] = ` + +
+
+

+ I am a custom render function +

+ +
+
+ +`; + +exports[`Renders CSF3InputFieldFilled story 1`] = ` + +
+ +
+ +`; + +exports[`Renders CSF3Primary story 1`] = ` + +
+ +
+ +`; + +exports[`Renders HooksStory story 1`] = ` + +
+ +
+ +
+ +`; + +exports[`Renders LoaderStory story 1`] = ` + +
+
+
+ loaded data +
+
+ mockFn return value +
+
+
+ +`; + +exports[`Renders Modal story 1`] = ` + +
+ +
+ + +`; + +exports[`Renders MountInPlayFunction story 1`] = ` + +
+
+
+ loaded data +
+
+ mockFn return value +
+
+
+ +`; + +exports[`Renders WithActionArg story 1`] = ` + +
+
+ +`; + +exports[`Renders WithActionArgType story 1`] = ` + +
+
+ nothing +
+
+ +`; diff --git a/code/renderers/react/src/__test__/portable-stories-factory.test.tsx b/code/renderers/react/src/__test__/portable-stories-factory.test.tsx new file mode 100644 index 000000000000..ce1ba2f74c9a --- /dev/null +++ b/code/renderers/react/src/__test__/portable-stories-factory.test.tsx @@ -0,0 +1,258 @@ +// @vitest-environment happy-dom + +/* eslint-disable import/namespace */ +import { cleanup, render, screen } from '@testing-library/react'; +import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; + +import React from 'react'; + +import type { ProjectAnnotations } from 'storybook/internal/csf'; +import { addons } from 'storybook/internal/preview-api'; + +import type { Meta, ReactRenderer } from '@storybook/react'; + +import * as addonActionsPreview from '@storybook/addon-actions/preview'; + +import { expectTypeOf } from 'expect-type'; + +import { composeStories, composeStory, setProjectAnnotations } from '..'; +import type { Button } from './Button'; +import * as ButtonStories from './Button.csf4.stories'; +import * as ComponentWithErrorStories from './ComponentWithError.stories'; + +const HooksStory = composeStory( + ButtonStories.HooksStory.input, + ButtonStories.CSF3Primary.meta.input +); + +const projectAnnotations = setProjectAnnotations([]); + +// example with composeStories, returns an object with all stories composed with args/decorators +// @ts-expect-error TODO: add a way to provide custom args/argTypes +// eslint-disable-next-line prettier/prettier +const { CSF3Primary, LoaderStory, MountInPlayFunction, MountInPlayFunctionThrow } = composeStories(ButtonStories); +const { ThrowsError } = composeStories(ComponentWithErrorStories); + +beforeAll(async () => { + await projectAnnotations.beforeAll?.(); +}); + +afterEach(() => { + cleanup(); +}); + +// example with composeStory, returns a single story composed with args/decorators +const Secondary = composeStory( + ButtonStories.CSF2Secondary.input, + ButtonStories.CSF3Primary.meta.input +); +describe('renders', () => { + it('renders primary button', () => { + render(Hello world); + const buttonElement = screen.getByText(/Hello world/i); + expect(buttonElement).not.toBeNull(); + }); + + it('reuses args from composed story', () => { + render(); + const buttonElement = screen.getByRole('button'); + expect(buttonElement.textContent).toEqual(Secondary.args.children); + }); + + it('onclick handler is called', async () => { + const onClickSpy = vi.fn(); + render(); + const buttonElement = screen.getByRole('button'); + buttonElement.click(); + expect(onClickSpy).toHaveBeenCalled(); + }); + + it('reuses args from composeStories', () => { + const { getByText } = render(); + const buttonElement = getByText(/foo/i); + expect(buttonElement).not.toBeNull(); + }); + + it('should throw error when rendering a component with a render error', async () => { + await expect(() => ThrowsError.run()).rejects.toThrowError('Error in render'); + }); + + it('should render component mounted in play function', async () => { + await MountInPlayFunction.run(); + + expect(screen.getByTestId('spy-data').textContent).toEqual('mockFn return value'); + expect(screen.getByTestId('loaded-data').textContent).toEqual('loaded data'); + }); + + it('should throw an error in play function', async () => { + await expect(() => MountInPlayFunctionThrow.run()).rejects.toThrowError('Error thrown in play'); + }); + + it('should call and compose loaders data', async () => { + await LoaderStory.load(); + const { getByTestId } = render(); + expect(getByTestId('spy-data').textContent).toEqual('mockFn return value'); + expect(getByTestId('loaded-data').textContent).toEqual('loaded data'); + // spy assertions happen in the play function and should work + await LoaderStory.run!(); + }); +}); + +describe('projectAnnotations', () => { + it('renders with default projectAnnotations', () => { + setProjectAnnotations([ + { + parameters: { injected: true }, + globalTypes: { + locale: { defaultValue: 'en' }, + }, + }, + ]); + const WithEnglishText = composeStory( + ButtonStories.CSF2StoryWithLocale.input, + ButtonStories.CSF3Primary.meta.input + ); + const { getByText } = render(); + const buttonElement = getByText('Hello!'); + expect(buttonElement).not.toBeNull(); + expect(WithEnglishText.parameters?.injected).toBe(true); + }); + + it('renders with custom projectAnnotations via composeStory params', () => { + const WithPortugueseText = composeStory( + ButtonStories.CSF2StoryWithLocale.input, + ButtonStories.CSF3Primary.meta.input, + { + initialGlobals: { locale: 'pt' }, + } + ); + const { getByText } = render(); + const buttonElement = getByText('Olá!'); + expect(buttonElement).not.toBeNull(); + }); + + it('has action arg from argTypes when addon-actions annotations are added', () => { + const Story = composeStory( + ButtonStories.WithActionArgType.input, + ButtonStories.CSF3Primary.meta.input, + addonActionsPreview as ProjectAnnotations + ); + + // TODO: add a way to provide custom args/argTypes, right now it's type any + expect(Story.args.someActionArg).toHaveProperty('isAction', true); + }); +}); + +describe('CSF3', () => { + it('renders with inferred globalRender', () => { + const Primary = composeStory( + ButtonStories.CSF3Button.input, + ButtonStories.CSF3Primary.meta.input + ); + + render(Hello world); + const buttonElement = screen.getByText(/Hello world/i); + expect(buttonElement).not.toBeNull(); + }); + + it('renders with custom render function', () => { + const Primary = composeStory( + ButtonStories.CSF3ButtonWithRender.input, + ButtonStories.CSF3Primary.meta.input + ); + + render(); + expect(screen.getByTestId('custom-render')).not.toBeNull(); + }); + + it('renders with play function without canvas element', async () => { + const CSF3InputFieldFilled = composeStory( + ButtonStories.CSF3InputFieldFilled.input, + ButtonStories.CSF3Primary.meta.input + ); + await CSF3InputFieldFilled.run(); + + const input = screen.getByTestId('input') as HTMLInputElement; + expect(input.value).toEqual('Hello world!'); + }); + + it('renders with play function with canvas element', async () => { + const CSF3InputFieldFilled = composeStory( + ButtonStories.CSF3InputFieldFilled.input, + ButtonStories.CSF3Primary.meta.input + ); + + let divElement; + try { + divElement = document.createElement('div'); + document.body.appendChild(divElement); + + await CSF3InputFieldFilled.run({ canvasElement: divElement }); + + const input = screen.getByTestId('input') as HTMLInputElement; + expect(input.value).toEqual('Hello world!'); + } finally { + if (divElement) { + document.body.removeChild(divElement); + } + } + }); + + it('renders with hooks', async () => { + await HooksStory.run(); + + const input = screen.getByTestId('input') as HTMLInputElement; + expect(input.value).toEqual('Hello world!'); + }); +}); + +// common in addons that need to communicate between manager and preview +it('should pass with decorators that need addons channel', () => { + const PrimaryWithChannels = composeStory( + ButtonStories.CSF3Primary.input, + ButtonStories.CSF3Primary.meta.input, + { + decorators: [ + (StoryFn: any) => { + addons.getChannel(); + return StoryFn(); + }, + ], + } + ); + render(Hello world); + const buttonElement = screen.getByText(/Hello world/i); + expect(buttonElement).not.toBeNull(); +}); + +describe('ComposeStories types', () => { + // this file tests Typescript types that's why there are no assertions + it('Should support typescript operators', () => { + type ComposeStoriesParam = Parameters[0]; + + expectTypeOf({ + ...ButtonStories, + default: ButtonStories.CSF3Primary.meta.input as Meta, + }).toMatchTypeOf(); + + expectTypeOf({ + ...ButtonStories, + default: ButtonStories.CSF3Primary.meta.input satisfies Meta, + }).toMatchTypeOf(); + }); +}); + +// @ts-expect-error TODO: fix the types for this +const testCases = Object.values(composeStories(ButtonStories)).map( + // @ts-expect-error TODO: fix the types for this + (Story) => [Story.storyName, Story] as [string, typeof Story] +); +it.each(testCases)('Renders %s story', async (_storyName, Story) => { + if (_storyName === 'CSF2StoryWithLocale' || _storyName === 'MountInPlayFunctionThrow') { + return; + } + + // @ts-expect-error TODO: fix the types for this + await Story.run(); + expect(document.body).toMatchSnapshot(); +}); diff --git a/code/renderers/react/src/csf-factories.test.tsx b/code/renderers/react/src/csf-factories.test.tsx new file mode 100644 index 000000000000..27fe0515298c --- /dev/null +++ b/code/renderers/react/src/csf-factories.test.tsx @@ -0,0 +1,254 @@ +// @vitest-environment happy-dom +// this file tests Typescript types that's why there are no assertions +import { describe, it } from 'vitest'; +import { expect, test } from 'vitest'; + +import type { KeyboardEventHandler, ReactElement, ReactNode } from 'react'; +import React from 'react'; + +import type { Canvas } from 'storybook/internal/csf'; +import type { Args, StrictArgs } from 'storybook/internal/types'; + +import type { Mock } from '@storybook/test'; +import { fn } from '@storybook/test'; + +import { expectTypeOf } from 'expect-type'; + +import { definePreview } from './preview'; +import type { Decorator } from './public-types'; + +type ButtonProps = { label: string; disabled: boolean }; +const Button: (props: ButtonProps) => ReactElement = () => <>; + +const preview = definePreview({}); + +test('csf factories', () => { + const config = definePreview({ + addons: [ + { + decorators: [], + }, + ], + }); + + const meta = config.meta({ component: Button, args: { disabled: true } }); + + const MyStory = meta.story({ + args: { + label: 'Hello world', + }, + }); + + expect(MyStory.input.args?.label).toBe('Hello world'); +}); + +describe('Args can be provided in multiple ways', () => { + it('✅ All required args may be provided in meta', () => { + const meta = preview.meta({ + component: Button, + args: { label: 'good', disabled: false }, + }); + + const Basic = meta.story({}); + }); + + it('✅ Required args may be provided partial in meta and the story', () => { + const meta = preview.meta({ + component: Button, + args: { label: 'good' }, + }); + const Basic = meta.story({ + args: { disabled: false }, + }); + }); + + it('❌ The combined shape of meta args and story args must match the required args.', () => { + { + const meta = preview.meta({ component: Button }); + const Basic = meta.story({ + // @ts-expect-error disabled not provided ❌ + args: { label: 'good' }, + }); + } + { + const meta = preview.meta({ + component: Button, + args: { label: 'good' }, + }); + // @ts-expect-error disabled not provided ❌ + const Basic = meta.story({}); + } + { + const meta = preview.meta({ component: Button }); + const Basic = meta.story({ + // @ts-expect-error disabled not provided ❌ + args: { label: 'good' }, + }); + } + }); +}); + +it('✅ Void functions are not changed', () => { + interface CmpProps { + label: string; + disabled: boolean; + onClick(): void; + onKeyDown: KeyboardEventHandler; + onLoading: (s: string) => ReactElement; + submitAction(): void; + } + + const Cmp: (props: CmpProps) => ReactElement = () => <>; + + const meta = preview.meta({ + component: Cmp, + args: { label: 'good' }, + }); + + const Basic = meta.story({ + args: { + disabled: false, + onLoading: () =>
Loading...
, + onKeyDown: fn(), + onClick: fn(), + submitAction: fn(), + }, + }); +}); + +type ThemeData = 'light' | 'dark'; +declare const Theme: (props: { theme: ThemeData; children?: ReactNode }) => ReactElement; + +describe('Story args can be inferred', () => { + it('Correct args are inferred when type is widened for render function', () => { + const meta = preview.meta({ + component: Button, + args: { disabled: false }, + render: (args: ButtonProps & { theme: ThemeData }, { component }) => { + // component is not null as it is provided in meta + + const Component = component!; + return ( + + + + ); + }, + }); + + const Basic = meta.story({ args: { theme: 'light', label: 'good' } }); + }); + + const withDecorator: Decorator<{ decoratorArg: number }> = (Story, { args }) => ( + <> + Decorator: {args.decoratorArg} + + + ); + + it('Correct args are inferred when type is widened for decorators', () => { + const meta = preview.meta({ + component: Button, + args: { disabled: false }, + decorators: [withDecorator], + }); + + const Basic = meta.story({ args: { decoratorArg: 0, label: 'good' } }); + }); + + it('Correct args are inferred when type is widened for multiple decorators', () => { + type Props = ButtonProps & { decoratorArg: number; decoratorArg2: string }; + + const secondDecorator: Decorator<{ decoratorArg2: string }> = (Story, { args }) => ( + <> + Decorator: {args.decoratorArg2} + + + ); + + // decorator is not using args + const thirdDecorator: Decorator = (Story) => ( + <> + + + ); + + // decorator is not using args + const fourthDecorator: Decorator = (Story) => ( + <> + + + ); + + const meta = preview.meta({ + component: Button, + args: { disabled: false }, + decorators: [withDecorator, secondDecorator, thirdDecorator, fourthDecorator], + }); + + const Basic = meta.story({ + args: { decoratorArg: 0, decoratorArg2: '', label: 'good' }, + }); + }); +}); + +it('Components without Props can be used, issue #21768', () => { + const Component = () => <>Foo; + const withDecorator: Decorator = (Story) => ( + <> + + + ); + + const meta = preview.meta({ + component: Component, + decorators: [withDecorator], + }); + + const Basic = meta.story({}); +}); + +it('Meta is broken when using discriminating types, issue #23629', () => { + type TestButtonProps = { + text: string; + } & ( + | { + id?: string; + onClick?: (e: unknown, id: string | undefined) => void; + } + | { + id: string; + onClick: (e: unknown, id: string) => void; + } + ); + const TestButton: React.FC = ({ text }) => { + return

{text}

; + }; + + preview.meta({ + title: 'Components/Button', + component: TestButton, + args: { + text: 'Button', + }, + }); +}); + +it('Infer mock function given to args in meta.', () => { + type Props = { label: string; onClick: () => void; onRender: () => JSX.Element }; + const TestButton = (props: Props) => <>; + + const meta = preview.meta({ + component: TestButton, + args: { label: 'label', onClick: fn(), onRender: () => <>some jsx }, + }); + + const Basic = meta.story({ + play: async ({ args, mount }) => { + const canvas = await mount(); + expectTypeOf(canvas).toEqualTypeOf(); + expectTypeOf(args.onClick).toEqualTypeOf(); + expectTypeOf(args.onRender).toEqualTypeOf<() => JSX.Element>(); + }, + }); +}); diff --git a/code/renderers/react/src/index.ts b/code/renderers/react/src/index.ts index 263d7546f10e..0734155b57e2 100644 --- a/code/renderers/react/src/index.ts +++ b/code/renderers/react/src/index.ts @@ -5,6 +5,10 @@ export * from './public-types'; export * from './portable-stories'; +export * from './preview'; + +export type { ReactParameters } from './types'; + // optimization: stop HMR propagation in webpack // optimization: stop HMR propagation in webpack diff --git a/code/renderers/react/src/preview.tsx b/code/renderers/react/src/preview.tsx new file mode 100644 index 000000000000..498521c241f0 --- /dev/null +++ b/code/renderers/react/src/preview.tsx @@ -0,0 +1,72 @@ +import type { ComponentType } from 'react'; + +import { definePreview as definePreviewBase } from 'storybook/internal/csf'; +import type { Meta, Preview, Story } from 'storybook/internal/csf'; +import type { + Args, + ArgsStoryFn, + ComponentAnnotations, + DecoratorFunction, + Renderer, + StoryAnnotations, +} from 'storybook/internal/types'; + +import type { AddMocks } from 'src/public-types'; +import type { RemoveIndexSignature, SetOptional, Simplify, UnionToIntersection } from 'type-fest'; + +import * as reactAnnotations from './entry-preview'; +import * as reactDocsAnnotations from './entry-preview-docs'; +import type { ReactRenderer } from './types'; + +export function definePreview(preview: ReactPreview['input']) { + return definePreviewBase({ + ...preview, + addons: [reactAnnotations, reactDocsAnnotations, ...(preview.addons ?? [])], + }) as ReactPreview; +} + +export interface ReactPreview extends Preview { + meta< + TArgs extends Args, + Decorators extends DecoratorFunction, + // Try to make Exact, TMetaArgs> work + TMetaArgs extends Partial, + >( + meta: { + render?: ArgsStoryFn; + component?: ComponentType; + decorators?: Decorators | Decorators[]; + args?: TMetaArgs; + } & Omit, 'decorators'> + ): ReactMeta< + { + args: Simplify< + TArgs & Simplify>> + >; + }, + { args: Partial extends TMetaArgs ? {} : TMetaArgs } + >; +} + +type DecoratorsArgs = UnionToIntersection< + Decorators extends DecoratorFunction ? TArgs : unknown +>; +interface ReactMeta< + Context extends { args: Args }, + MetaInput extends ComponentAnnotations, +> extends Meta { + story< + const TInput extends Simplify< + StoryAnnotations< + ReactRenderer, + // TODO: infer mocks from story itself as well + AddMocks, + SetOptional + > + >, + >( + story: TInput + ): ReactStory; +} + +interface ReactStory extends Story {} diff --git a/code/renderers/react/src/public-types.ts b/code/renderers/react/src/public-types.ts index 8a2f7003ec94..2f2b2d1de816 100644 --- a/code/renderers/react/src/public-types.ts +++ b/code/renderers/react/src/public-types.ts @@ -66,7 +66,7 @@ export type StoryObj = [TMetaOrCmpOrArgs] extends [ : StoryAnnotations; // This performs a downcast to function types that are mocks, when a mock fn is given to meta args. -type AddMocks = Simplify<{ +export type AddMocks = Simplify<{ [T in keyof TArgs]: T extends keyof DefaultArgs ? // eslint-disable-next-line @typescript-eslint/ban-types DefaultArgs[T] extends (...args: any) => any & { mock: {} } // allow any function with a mock object diff --git a/code/renderers/react/src/types.ts b/code/renderers/react/src/types.ts index 7b3eeb648afe..269e5897f52e 100644 --- a/code/renderers/react/src/types.ts +++ b/code/renderers/react/src/types.ts @@ -15,4 +15,21 @@ export interface ShowErrorArgs { description: string; } +export interface ReactParameters { + /** React renderer configuration */ + react?: { + /** + * Whether to enable React Server Components + * + * @see https://storybook.js.org/docs/get-started/frameworks/nextjs#react-server-components-rsc + */ + rsc?: boolean; + /** Options passed to React root creation */ + rootOptions?: { + /** Custom error handler for caught errors */ + onCaughtError?: (error: unknown) => void; + }; + }; +} + export type StoryFnReactReturnType = JSX.Element; diff --git a/code/renderers/react/template/stories/csf4.mdx b/code/renderers/react/template/stories/csf4.mdx new file mode 100644 index 000000000000..48c16cabb07e --- /dev/null +++ b/code/renderers/react/template/stories/csf4.mdx @@ -0,0 +1,8 @@ +import * as StoriesModule from './csf4.stories' +import { Meta, Stories } from '@storybook/blocks' + + + +# CSF4 in MDX + + \ No newline at end of file diff --git a/code/renderers/react/template/stories/csf4.stories.tsx b/code/renderers/react/template/stories/csf4.stories.tsx new file mode 100644 index 000000000000..40b9e7898d4d --- /dev/null +++ b/code/renderers/react/template/stories/csf4.stories.tsx @@ -0,0 +1,12 @@ +// @ts-expect-error this will be part of the package.json of the sandbox +import preview from '#.storybook/preview'; + +const meta = preview.meta({ + // @ts-expect-error fix globalThis.Components type not existing later + component: globalThis.Components.Button, + args: { + label: 'Hello world!', + }, +}); + +export const Story = meta.story({}); diff --git a/code/yarn.lock b/code/yarn.lock index f481c35cd9fe..2fa2a8f90859 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6402,6 +6402,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm-eabi@npm:4.29.1": + version: 4.29.1 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.29.1" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@rollup/rollup-android-arm-eabi@npm:4.30.1": version: 4.30.1 resolution: "@rollup/rollup-android-arm-eabi@npm:4.30.1" @@ -6409,6 +6416,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm64@npm:4.29.1": + version: 4.29.1 + resolution: "@rollup/rollup-android-arm64@npm:4.29.1" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-android-arm64@npm:4.30.1": version: 4.30.1 resolution: "@rollup/rollup-android-arm64@npm:4.30.1" @@ -6416,6 +6430,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-arm64@npm:4.29.1": + version: 4.29.1 + resolution: "@rollup/rollup-darwin-arm64@npm:4.29.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-arm64@npm:4.30.1": version: 4.30.1 resolution: "@rollup/rollup-darwin-arm64@npm:4.30.1" @@ -6423,6 +6444,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-x64@npm:4.29.1": + version: 4.29.1 + resolution: "@rollup/rollup-darwin-x64@npm:4.29.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-x64@npm:4.30.1": version: 4.30.1 resolution: "@rollup/rollup-darwin-x64@npm:4.30.1" @@ -6430,6 +6458,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-freebsd-arm64@npm:4.29.1": + version: 4.29.1 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.29.1" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-freebsd-arm64@npm:4.30.1": version: 4.30.1 resolution: "@rollup/rollup-freebsd-arm64@npm:4.30.1" @@ -6437,6 +6472,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-freebsd-x64@npm:4.29.1": + version: 4.29.1 + resolution: "@rollup/rollup-freebsd-x64@npm:4.29.1" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-freebsd-x64@npm:4.30.1": version: 4.30.1 resolution: "@rollup/rollup-freebsd-x64@npm:4.30.1" @@ -6444,6 +6486,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-gnueabihf@npm:4.29.1": + version: 4.29.1 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.29.1" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-gnueabihf@npm:4.30.1": version: 4.30.1 resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.30.1" @@ -6451,6 +6500,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-musleabihf@npm:4.29.1": + version: 4.29.1 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.29.1" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-musleabihf@npm:4.30.1": version: 4.30.1 resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.30.1" @@ -6458,6 +6514,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-gnu@npm:4.29.1": + version: 4.29.1 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.29.1" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-gnu@npm:4.30.1": version: 4.30.1 resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.30.1" @@ -6465,6 +6528,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-musl@npm:4.29.1": + version: 4.29.1 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.29.1" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-musl@npm:4.30.1": version: 4.30.1 resolution: "@rollup/rollup-linux-arm64-musl@npm:4.30.1" @@ -6472,6 +6542,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-loongarch64-gnu@npm:4.29.1": + version: 4.29.1 + resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.29.1" + conditions: os=linux & cpu=loong64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-loongarch64-gnu@npm:4.30.1": version: 4.30.1 resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.30.1" @@ -6479,6 +6556,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-powerpc64le-gnu@npm:4.29.1": + version: 4.29.1 + resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.29.1" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-powerpc64le-gnu@npm:4.30.1": version: 4.30.1 resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.30.1" @@ -6486,6 +6570,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-riscv64-gnu@npm:4.29.1": + version: 4.29.1 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.29.1" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-riscv64-gnu@npm:4.30.1": version: 4.30.1 resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.30.1" @@ -6493,6 +6584,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-s390x-gnu@npm:4.29.1": + version: 4.29.1 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.29.1" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-s390x-gnu@npm:4.30.1": version: 4.30.1 resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.30.1" @@ -6500,6 +6598,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-gnu@npm:4.29.1": + version: 4.29.1 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.29.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-gnu@npm:4.30.1": version: 4.30.1 resolution: "@rollup/rollup-linux-x64-gnu@npm:4.30.1" @@ -6507,6 +6612,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-musl@npm:4.29.1": + version: 4.29.1 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.29.1" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-musl@npm:4.30.1": version: 4.30.1 resolution: "@rollup/rollup-linux-x64-musl@npm:4.30.1" @@ -6514,6 +6626,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-arm64-msvc@npm:4.29.1": + version: 4.29.1 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.29.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-win32-arm64-msvc@npm:4.30.1": version: 4.30.1 resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.30.1" @@ -6521,6 +6640,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-ia32-msvc@npm:4.29.1": + version: 4.29.1 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.29.1" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@rollup/rollup-win32-ia32-msvc@npm:4.30.1": version: 4.30.1 resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.30.1" @@ -6528,6 +6654,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-x64-msvc@npm:4.29.1": + version: 4.29.1 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.29.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-win32-x64-msvc@npm:4.30.1": version: 4.30.1 resolution: "@rollup/rollup-win32-x64-msvc@npm:4.30.1" @@ -7219,6 +7352,7 @@ __metadata: globby: "npm:^14.0.1" jscodeshift: "npm:^0.15.1" leven: "npm:^3.1.0" + p-limit: "npm:^6.2.0" picocolors: "npm:^1.1.0" prompts: "npm:^2.4.0" semver: "npm:^7.3.7" @@ -7493,12 +7627,12 @@ __metadata: languageName: node linkType: hard -"@storybook/csf@npm:^0.0.1": - version: 0.0.1 - resolution: "@storybook/csf@npm:0.0.1" +"@storybook/csf@npm:^0.1.11": + version: 0.1.13 + resolution: "@storybook/csf@npm:0.1.13" dependencies: - lodash: "npm:^4.17.15" - checksum: 10c0/7b0f75763415f9147692a460b44417ee56ea9639433716a1fd4d1df4c8b0221cbc71b8da0fbed4dcecb3ccd6c7ed64be39f5c255c713539a6088a1d6488aaa24 + type-fest: "npm:^2.19.0" + checksum: 10c0/7c57b531ac95ca45239f498d419483d675e58cd8d549e0bac623519cc1ef4f3c9c6b75ec3873aa51cc2872728012db5dd5e1f2c2d8085014241eb4b896480996 languageName: node linkType: hard @@ -8274,7 +8408,7 @@ __metadata: eslint-plugin-import: "npm:^2.29.1" eslint-plugin-local-rules: "portal:../scripts/eslint-plugin-local-rules" eslint-plugin-playwright: "npm:^1.6.2" - eslint-plugin-storybook: "npm:^0.8.0" + eslint-plugin-storybook: "npm:0.11.3--canary.187.1af857a.0" github-release-from-changelog: "npm:^2.1.1" glob: "npm:^10.0.0" happy-dom: "npm:^14.12.0" @@ -9143,6 +9277,16 @@ __metadata: languageName: node linkType: hard +"@types/eslint-scope@npm:^3.7.3": + version: 3.7.5 + resolution: "@types/eslint-scope@npm:3.7.5" + dependencies: + "@types/eslint": "npm:*" + "@types/estree": "npm:*" + checksum: 10c0/9ade676030067a14d34acb4a48362bcf16632e867d059e734cf082e0523362415ed698e3776f8fad7e346019078d63a5264992b33054182607ce20ad9eaeec80 + languageName: node + linkType: hard + "@types/eslint-scope@npm:^3.7.7": version: 3.7.7 resolution: "@types/eslint-scope@npm:3.7.7" @@ -9632,7 +9776,7 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:^7, @types/semver@npm:^7.3.12, @types/semver@npm:^7.3.4, @types/semver@npm:^7.5.0, @types/semver@npm:^7.5.6, @types/semver@npm:^7.5.8": +"@types/semver@npm:^7, @types/semver@npm:^7.3.4, @types/semver@npm:^7.5.0, @types/semver@npm:^7.5.6, @types/semver@npm:^7.5.8": version: 7.5.8 resolution: "@types/semver@npm:7.5.8" checksum: 10c0/8663ff927234d1c5fcc04b33062cb2b9fcfbe0f5f351ed26c4d1e1581657deebd506b41ff7fdf89e787e3d33ce05854bc01686379b89e9c49b564c4cfa988efa @@ -9923,16 +10067,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/scope-manager@npm:5.62.0" - dependencies: - "@typescript-eslint/types": "npm:5.62.0" - "@typescript-eslint/visitor-keys": "npm:5.62.0" - checksum: 10c0/861253235576c1c5c1772d23cdce1418c2da2618a479a7de4f6114a12a7ca853011a1e530525d0931c355a8fd237b9cd828fac560f85f9623e24054fd024726f - languageName: node - linkType: hard - "@typescript-eslint/scope-manager@npm:6.18.1": version: 6.18.1 resolution: "@typescript-eslint/scope-manager@npm:6.18.1" @@ -9963,6 +10097,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:8.19.1": + version: 8.19.1 + resolution: "@typescript-eslint/scope-manager@npm:8.19.1" + dependencies: + "@typescript-eslint/types": "npm:8.19.1" + "@typescript-eslint/visitor-keys": "npm:8.19.1" + checksum: 10c0/7dca0c28ad27a0c7e26499e0f584f98efdcf34087f46aadc661b36c310484b90655e83818bafd249b5a28c7094a69c54d553f6cd403869bf134f95a9148733f5 + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:6.21.0": version: 6.21.0 resolution: "@typescript-eslint/type-utils@npm:6.21.0" @@ -9997,13 +10141,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/types@npm:5.62.0" - checksum: 10c0/7febd3a7f0701c0b927e094f02e82d8ee2cada2b186fcb938bc2b94ff6fbad88237afc304cbaf33e82797078bbbb1baf91475f6400912f8b64c89be79bfa4ddf - languageName: node - linkType: hard - "@typescript-eslint/types@npm:6.18.1": version: 6.18.1 resolution: "@typescript-eslint/types@npm:6.18.1" @@ -10025,21 +10162,10 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/typescript-estree@npm:5.62.0" - dependencies: - "@typescript-eslint/types": "npm:5.62.0" - "@typescript-eslint/visitor-keys": "npm:5.62.0" - debug: "npm:^4.3.4" - globby: "npm:^11.1.0" - is-glob: "npm:^4.0.3" - semver: "npm:^7.3.7" - tsutils: "npm:^3.21.0" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10c0/d7984a3e9d56897b2481940ec803cb8e7ead03df8d9cfd9797350be82ff765dfcf3cfec04e7355e1779e948da8f02bc5e11719d07a596eb1cb995c48a95e38cf +"@typescript-eslint/types@npm:8.19.1": + version: 8.19.1 + resolution: "@typescript-eslint/types@npm:8.19.1" + checksum: 10c0/e907bf096d5ed7a812a1e537a98dd881ab5d2d47e072225bfffaa218c1433115a148b27a15744db8374b46dac721617c6d13a1da255fdeb369cf193416533f6e languageName: node linkType: hard @@ -10100,6 +10226,24 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:8.19.1": + version: 8.19.1 + resolution: "@typescript-eslint/typescript-estree@npm:8.19.1" + dependencies: + "@typescript-eslint/types": "npm:8.19.1" + "@typescript-eslint/visitor-keys": "npm:8.19.1" + debug: "npm:^4.3.4" + fast-glob: "npm:^3.3.2" + is-glob: "npm:^4.0.3" + minimatch: "npm:^9.0.4" + semver: "npm:^7.6.0" + ts-api-utils: "npm:^2.0.0" + peerDependencies: + typescript: ">=4.8.4 <5.8.0" + checksum: 10c0/549d9d565a58a25fc8397a555506f2e8d29a740f5b6ed9105479e22de5aab89d9d535959034a8e9d4115adb435de09ee6987d28e8922052eea577842ddce1a7a + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:6.21.0": version: 6.21.0 resolution: "@typescript-eslint/utils@npm:6.21.0" @@ -10131,31 +10275,18 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:^5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/utils@npm:5.62.0" +"@typescript-eslint/utils@npm:^8.8.1": + version: 8.19.1 + resolution: "@typescript-eslint/utils@npm:8.19.1" dependencies: - "@eslint-community/eslint-utils": "npm:^4.2.0" - "@types/json-schema": "npm:^7.0.9" - "@types/semver": "npm:^7.3.12" - "@typescript-eslint/scope-manager": "npm:5.62.0" - "@typescript-eslint/types": "npm:5.62.0" - "@typescript-eslint/typescript-estree": "npm:5.62.0" - eslint-scope: "npm:^5.1.1" - semver: "npm:^7.3.7" + "@eslint-community/eslint-utils": "npm:^4.4.0" + "@typescript-eslint/scope-manager": "npm:8.19.1" + "@typescript-eslint/types": "npm:8.19.1" + "@typescript-eslint/typescript-estree": "npm:8.19.1" peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: 10c0/f09b7d9952e4a205eb1ced31d7684dd55cee40bf8c2d78e923aa8a255318d97279825733902742c09d8690f37a50243f4c4d383ab16bd7aefaf9c4b438f785e1 - languageName: node - linkType: hard - -"@typescript-eslint/visitor-keys@npm:5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/visitor-keys@npm:5.62.0" - dependencies: - "@typescript-eslint/types": "npm:5.62.0" - eslint-visitor-keys: "npm:^3.3.0" - checksum: 10c0/7c3b8e4148e9b94d9b7162a596a1260d7a3efc4e65199693b8025c71c4652b8042501c0bc9f57654c1e2943c26da98c0f77884a746c6ae81389fcb0b513d995d + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <5.8.0" + checksum: 10c0/f7d2fe9a2bd8cb3ae6fafe5e465882a6784b2acf81d43d194c579381b92651c2ffc0fca69d2a35eee119f539622752a0e9ec063aaec7576d5d2bfe68b441980d languageName: node linkType: hard @@ -10189,6 +10320,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.19.1": + version: 8.19.1 + resolution: "@typescript-eslint/visitor-keys@npm:8.19.1" + dependencies: + "@typescript-eslint/types": "npm:8.19.1" + eslint-visitor-keys: "npm:^4.2.0" + checksum: 10c0/117537450a099f51f3f0d39186f248ae370bdc1b7f6975dbdbffcfc89e6e1aa47c1870db790d4f778a48f2c1f6cd9c269b63867c12afaa424367c63dabee8fd0 + languageName: node + linkType: hard + "@ungap/structured-clone@npm:^1.0.0, @ungap/structured-clone@npm:^1.2.0": version: 1.2.0 resolution: "@ungap/structured-clone@npm:1.2.0" @@ -10876,6 +11017,16 @@ __metadata: languageName: node linkType: hard +"@webassemblyjs/ast@npm:1.11.6, @webassemblyjs/ast@npm:^1.11.5": + version: 1.11.6 + resolution: "@webassemblyjs/ast@npm:1.11.6" + dependencies: + "@webassemblyjs/helper-numbers": "npm:1.11.6" + "@webassemblyjs/helper-wasm-bytecode": "npm:1.11.6" + checksum: 10c0/e28476a183c8a1787adcf0e5df1d36ec4589467ab712c674fe4f6769c7fb19d1217bfb5856b3edd0f3e0a148ebae9e4bbb84110cee96664966dfef204d9c31fb + languageName: node + linkType: hard + "@webassemblyjs/ast@npm:1.14.1, @webassemblyjs/ast@npm:^1.14.1": version: 1.14.1 resolution: "@webassemblyjs/ast@npm:1.14.1" @@ -10886,6 +11037,13 @@ __metadata: languageName: node linkType: hard +"@webassemblyjs/floating-point-hex-parser@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/floating-point-hex-parser@npm:1.11.6" + checksum: 10c0/37fe26f89e18e4ca0e7d89cfe3b9f17cfa327d7daf906ae01400416dbb2e33c8a125b4dc55ad7ff405e5fcfb6cf0d764074c9bc532b9a31a71e762be57d2ea0a + languageName: node + linkType: hard + "@webassemblyjs/floating-point-hex-parser@npm:1.13.2": version: 1.13.2 resolution: "@webassemblyjs/floating-point-hex-parser@npm:1.13.2" @@ -10893,6 +11051,13 @@ __metadata: languageName: node linkType: hard +"@webassemblyjs/helper-api-error@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/helper-api-error@npm:1.11.6" + checksum: 10c0/a681ed51863e4ff18cf38d223429f414894e5f7496856854d9a886eeddcee32d7c9f66290f2919c9bb6d2fc2b2fae3f989b6a1e02a81e829359738ea0c4d371a + languageName: node + linkType: hard + "@webassemblyjs/helper-api-error@npm:1.13.2": version: 1.13.2 resolution: "@webassemblyjs/helper-api-error@npm:1.13.2" @@ -10900,6 +11065,13 @@ __metadata: languageName: node linkType: hard +"@webassemblyjs/helper-buffer@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/helper-buffer@npm:1.11.6" + checksum: 10c0/55b5d67db95369cdb2a505ae7ebdf47194d49dfc1aecb0f5403277dcc899c7d3e1f07e8d279646adf8eafd89959272db62ca66fbe803321661ab184176ddfd3a + languageName: node + linkType: hard + "@webassemblyjs/helper-buffer@npm:1.14.1": version: 1.14.1 resolution: "@webassemblyjs/helper-buffer@npm:1.14.1" @@ -10907,6 +11079,17 @@ __metadata: languageName: node linkType: hard +"@webassemblyjs/helper-numbers@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/helper-numbers@npm:1.11.6" + dependencies: + "@webassemblyjs/floating-point-hex-parser": "npm:1.11.6" + "@webassemblyjs/helper-api-error": "npm:1.11.6" + "@xtuc/long": "npm:4.2.2" + checksum: 10c0/c7d5afc0ff3bd748339b466d8d2f27b908208bf3ff26b2e8e72c39814479d486e0dca6f3d4d776fd9027c1efe05b5c0716c57a23041eb34473892b2731c33af3 + languageName: node + linkType: hard + "@webassemblyjs/helper-numbers@npm:1.13.2": version: 1.13.2 resolution: "@webassemblyjs/helper-numbers@npm:1.13.2" @@ -10918,6 +11101,13 @@ __metadata: languageName: node linkType: hard +"@webassemblyjs/helper-wasm-bytecode@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/helper-wasm-bytecode@npm:1.11.6" + checksum: 10c0/79d2bebdd11383d142745efa32781249745213af8e022651847382685ca76709f83e1d97adc5f0d3c2b8546bf02864f8b43a531fdf5ca0748cb9e4e0ef2acaa5 + languageName: node + linkType: hard + "@webassemblyjs/helper-wasm-bytecode@npm:1.13.2": version: 1.13.2 resolution: "@webassemblyjs/helper-wasm-bytecode@npm:1.13.2" @@ -10925,6 +11115,18 @@ __metadata: languageName: node linkType: hard +"@webassemblyjs/helper-wasm-section@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/helper-wasm-section@npm:1.11.6" + dependencies: + "@webassemblyjs/ast": "npm:1.11.6" + "@webassemblyjs/helper-buffer": "npm:1.11.6" + "@webassemblyjs/helper-wasm-bytecode": "npm:1.11.6" + "@webassemblyjs/wasm-gen": "npm:1.11.6" + checksum: 10c0/b79b19a63181f32e5ee0e786fa8264535ea5360276033911fae597d2de15e1776f028091d08c5a813a3901fd2228e74cd8c7e958fded064df734f00546bef8ce + languageName: node + linkType: hard + "@webassemblyjs/helper-wasm-section@npm:1.14.1": version: 1.14.1 resolution: "@webassemblyjs/helper-wasm-section@npm:1.14.1" @@ -10937,6 +11139,15 @@ __metadata: languageName: node linkType: hard +"@webassemblyjs/ieee754@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/ieee754@npm:1.11.6" + dependencies: + "@xtuc/ieee754": "npm:^1.2.0" + checksum: 10c0/59de0365da450322c958deadade5ec2d300c70f75e17ae55de3c9ce564deff5b429e757d107c7ec69bd0ba169c6b6cc2ff66293ab7264a7053c829b50ffa732f + languageName: node + linkType: hard + "@webassemblyjs/ieee754@npm:1.13.2": version: 1.13.2 resolution: "@webassemblyjs/ieee754@npm:1.13.2" @@ -10946,6 +11157,15 @@ __metadata: languageName: node linkType: hard +"@webassemblyjs/leb128@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/leb128@npm:1.11.6" + dependencies: + "@xtuc/long": "npm:4.2.2" + checksum: 10c0/cb344fc04f1968209804de4da018679c5d4708a03b472a33e0fa75657bb024978f570d3ccf9263b7f341f77ecaa75d0e051b9cd4b7bb17a339032cfd1c37f96e + languageName: node + linkType: hard + "@webassemblyjs/leb128@npm:1.13.2": version: 1.13.2 resolution: "@webassemblyjs/leb128@npm:1.13.2" @@ -10955,6 +11175,13 @@ __metadata: languageName: node linkType: hard +"@webassemblyjs/utf8@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/utf8@npm:1.11.6" + checksum: 10c0/14d6c24751a89ad9d801180b0d770f30a853c39f035a15fbc96266d6ac46355227abd27a3fd2eeaa97b4294ced2440a6b012750ae17bafe1a7633029a87b6bee + languageName: node + linkType: hard + "@webassemblyjs/utf8@npm:1.13.2": version: 1.13.2 resolution: "@webassemblyjs/utf8@npm:1.13.2" @@ -10962,6 +11189,22 @@ __metadata: languageName: node linkType: hard +"@webassemblyjs/wasm-edit@npm:^1.11.5": + version: 1.11.6 + resolution: "@webassemblyjs/wasm-edit@npm:1.11.6" + dependencies: + "@webassemblyjs/ast": "npm:1.11.6" + "@webassemblyjs/helper-buffer": "npm:1.11.6" + "@webassemblyjs/helper-wasm-bytecode": "npm:1.11.6" + "@webassemblyjs/helper-wasm-section": "npm:1.11.6" + "@webassemblyjs/wasm-gen": "npm:1.11.6" + "@webassemblyjs/wasm-opt": "npm:1.11.6" + "@webassemblyjs/wasm-parser": "npm:1.11.6" + "@webassemblyjs/wast-printer": "npm:1.11.6" + checksum: 10c0/9a56b6bf635cf7aa5d6e926eaddf44c12fba050170e452a8e17ab4e1b937708678c03f5817120fb9de1e27167667ce693d16ce718d41e5a16393996a6017ab73 + languageName: node + linkType: hard + "@webassemblyjs/wasm-edit@npm:^1.14.1": version: 1.14.1 resolution: "@webassemblyjs/wasm-edit@npm:1.14.1" @@ -10978,6 +11221,19 @@ __metadata: languageName: node linkType: hard +"@webassemblyjs/wasm-gen@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/wasm-gen@npm:1.11.6" + dependencies: + "@webassemblyjs/ast": "npm:1.11.6" + "@webassemblyjs/helper-wasm-bytecode": "npm:1.11.6" + "@webassemblyjs/ieee754": "npm:1.11.6" + "@webassemblyjs/leb128": "npm:1.11.6" + "@webassemblyjs/utf8": "npm:1.11.6" + checksum: 10c0/ce9a39d3dab2eb4a5df991bc9f3609960daa4671d25d700f4617152f9f79da768547359f817bee10cd88532c3e0a8a1714d383438e0a54217eba53cb822bd5ad + languageName: node + linkType: hard + "@webassemblyjs/wasm-gen@npm:1.14.1": version: 1.14.1 resolution: "@webassemblyjs/wasm-gen@npm:1.14.1" @@ -10991,6 +11247,18 @@ __metadata: languageName: node linkType: hard +"@webassemblyjs/wasm-opt@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/wasm-opt@npm:1.11.6" + dependencies: + "@webassemblyjs/ast": "npm:1.11.6" + "@webassemblyjs/helper-buffer": "npm:1.11.6" + "@webassemblyjs/wasm-gen": "npm:1.11.6" + "@webassemblyjs/wasm-parser": "npm:1.11.6" + checksum: 10c0/82788408054171688e9f12883b693777219366d6867003e34dccc21b4a0950ef53edc9d2b4d54cabdb6ee869cf37c8718401b4baa4f70a7f7dd3867c75637298 + languageName: node + linkType: hard + "@webassemblyjs/wasm-opt@npm:1.14.1": version: 1.14.1 resolution: "@webassemblyjs/wasm-opt@npm:1.14.1" @@ -11003,6 +11271,20 @@ __metadata: languageName: node linkType: hard +"@webassemblyjs/wasm-parser@npm:1.11.6, @webassemblyjs/wasm-parser@npm:^1.11.5": + version: 1.11.6 + resolution: "@webassemblyjs/wasm-parser@npm:1.11.6" + dependencies: + "@webassemblyjs/ast": "npm:1.11.6" + "@webassemblyjs/helper-api-error": "npm:1.11.6" + "@webassemblyjs/helper-wasm-bytecode": "npm:1.11.6" + "@webassemblyjs/ieee754": "npm:1.11.6" + "@webassemblyjs/leb128": "npm:1.11.6" + "@webassemblyjs/utf8": "npm:1.11.6" + checksum: 10c0/7a97a5f34f98bdcfd812157845a06d53f3d3f67dbd4ae5d6bf66e234e17dc4a76b2b5e74e5dd70b4cab9778fc130194d50bbd6f9a1d23e15ed1ed666233d6f5f + languageName: node + linkType: hard + "@webassemblyjs/wasm-parser@npm:1.14.1, @webassemblyjs/wasm-parser@npm:^1.14.1": version: 1.14.1 resolution: "@webassemblyjs/wasm-parser@npm:1.14.1" @@ -11017,6 +11299,16 @@ __metadata: languageName: node linkType: hard +"@webassemblyjs/wast-printer@npm:1.11.6": + version: 1.11.6 + resolution: "@webassemblyjs/wast-printer@npm:1.11.6" + dependencies: + "@webassemblyjs/ast": "npm:1.11.6" + "@xtuc/long": "npm:4.2.2" + checksum: 10c0/916b90fa3a8aadd95ca41c21d4316d0a7582cf6d0dcf6d9db86ab0de823914df513919fba60ac1edd227ff00e93a66b927b15cbddd36b69d8a34c8815752633c + languageName: node + linkType: hard + "@webassemblyjs/wast-printer@npm:1.14.1": version: 1.14.1 resolution: "@webassemblyjs/wast-printer@npm:1.14.1" @@ -11133,6 +11425,15 @@ __metadata: languageName: node linkType: hard +"acorn-import-assertions@npm:^1.9.0": + version: 1.9.0 + resolution: "acorn-import-assertions@npm:1.9.0" + peerDependencies: + acorn: ^8 + checksum: 10c0/3b4a194e128efdc9b86c2b1544f623aba4c1aa70d638f8ab7dc3971a5b4aa4c57bd62f99af6e5325bb5973c55863b4112e708a6f408bad7a138647ca72283afe + languageName: node + linkType: hard + "acorn-jsx@npm:^5.0.0, acorn-jsx@npm:^5.3.1, acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" @@ -11174,7 +11475,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.0.0, acorn@npm:^8.11.2, acorn@npm:^8.12.1, acorn@npm:^8.4.1, acorn@npm:^8.6.0, acorn@npm:^8.8.2, acorn@npm:^8.9.0": +"acorn@npm:^8.0.0, acorn@npm:^8.11.2, acorn@npm:^8.12.1, acorn@npm:^8.4.1, acorn@npm:^8.6.0, acorn@npm:^8.7.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": version: 8.12.1 resolution: "acorn@npm:8.12.1" bin: @@ -11218,6 +11519,15 @@ __metadata: languageName: node linkType: hard +"agent-base@npm:^7.0.2": + version: 7.1.0 + resolution: "agent-base@npm:7.1.0" + dependencies: + debug: "npm:^4.3.4" + checksum: 10c0/fc974ab57ffdd8421a2bc339644d312a9cca320c20c3393c9d8b1fd91731b9bbabdb985df5fc860f5b79d81c3e350daa3fcb31c5c07c0bb385aafc817df004ce + languageName: node + linkType: hard + "agent-base@npm:^7.1.0, agent-base@npm:^7.1.2": version: 7.1.3 resolution: "agent-base@npm:7.1.3" @@ -11292,7 +11602,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:8.17.1, ajv@npm:^8.0.0, ajv@npm:^8.2.0, ajv@npm:^8.9.0": +"ajv@npm:8.17.1": version: 8.17.1 resolution: "ajv@npm:8.17.1" dependencies: @@ -11316,6 +11626,18 @@ __metadata: languageName: node linkType: hard +"ajv@npm:^8.0.0, ajv@npm:^8.2.0, ajv@npm:^8.9.0": + version: 8.12.0 + resolution: "ajv@npm:8.12.0" + dependencies: + fast-deep-equal: "npm:^3.1.1" + json-schema-traverse: "npm:^1.0.0" + require-from-string: "npm:^2.0.2" + uri-js: "npm:^4.2.2" + checksum: 10c0/ac4f72adf727ee425e049bc9d8b31d4a57e1c90da8d28bcd23d60781b12fcd6fc3d68db5df16994c57b78b94eed7988f5a6b482fd376dc5b084125e20a0a622e + languageName: node + linkType: hard + "alien-signals@npm:^0.4.9": version: 0.4.12 resolution: "alien-signals@npm:0.4.12" @@ -12757,6 +13079,20 @@ __metadata: languageName: node linkType: hard +"browserslist@npm:^4.21.10": + version: 4.24.2 + resolution: "browserslist@npm:4.24.2" + dependencies: + caniuse-lite: "npm:^1.0.30001669" + electron-to-chromium: "npm:^1.5.41" + node-releases: "npm:^2.0.18" + update-browserslist-db: "npm:^1.1.1" + bin: + browserslist: cli.js + checksum: 10c0/d747c9fb65ed7b4f1abcae4959405707ed9a7b835639f8a9ba0da2911995a6ab9b0648fd05baf2a4d4e3cf7f9fdbad56d3753f91881e365992c1d49c8d88ff7a + languageName: node + linkType: hard + "browserslist@npm:^4.21.5, browserslist@npm:^4.23.0, browserslist@npm:^4.23.3, browserslist@npm:^4.24.0, browserslist@npm:^4.24.3": version: 4.24.4 resolution: "browserslist@npm:4.24.4" @@ -13024,7 +13360,14 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001579, caniuse-lite@npm:^1.0.30001646, caniuse-lite@npm:^1.0.30001688": +"caniuse-lite@npm:^1.0.30001579, caniuse-lite@npm:^1.0.30001669": + version: 1.0.30001677 + resolution: "caniuse-lite@npm:1.0.30001677" + checksum: 10c0/22b4aa738b213b5d0bc820c26ba23fa265ca90a5c59776e1a686b9ab6fff9120d0825fd920c0a601a4b65056ef40d01548405feb95c8dd6083255f50c71a0864 + languageName: node + linkType: hard + +"caniuse-lite@npm:^1.0.30001646, caniuse-lite@npm:^1.0.30001688": version: 1.0.30001692 resolution: "caniuse-lite@npm:1.0.30001692" checksum: 10c0/fca5105561ea12f3de593f3b0f062af82f7d07519e8dbcb97f34e7fd23349bcef1b1622a9a6cd2164d98e3d2f20059ef7e271edae46567aef88caf4c16c7708a @@ -14420,15 +14763,15 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.6, debug@npm:^4.3.7, debug@npm:^4.4.0": - version: 4.4.0 - resolution: "debug@npm:4.4.0" +"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.7": + version: 4.3.7 + resolution: "debug@npm:4.3.7" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10c0/db94f1a182bf886f57b4755f85b3a74c39b5114b9377b7ab375dc2cfa3454f09490cc6c30f829df3fc8042bc8b8995f6567ce5cd96f3bc3688bd24027197d9de + checksum: 10c0/1471db19c3b06d485a622d62f65947a19a23fbd0dd73f7fd3eafb697eec5360cde447fb075919987899b1a2096e85d35d4eb5a4de09a57600ac9cf7e6c8e768b languageName: node linkType: hard @@ -14453,6 +14796,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.3.6, debug@npm:^4.4.0": + version: 4.4.0 + resolution: "debug@npm:4.4.0" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/db94f1a182bf886f57b4755f85b3a74c39b5114b9377b7ab375dc2cfa3454f09490cc6c30f829df3fc8042bc8b8995f6567ce5cd96f3bc3688bd24027197d9de + languageName: node + linkType: hard + "decamelize@npm:^1.2.0": version: 1.2.0 resolution: "decamelize@npm:1.2.0" @@ -15151,6 +15506,13 @@ __metadata: languageName: node linkType: hard +"electron-to-chromium@npm:^1.5.41": + version: 1.5.50 + resolution: "electron-to-chromium@npm:1.5.50" + checksum: 10c0/8b77b18ae833bfe2173e346ac33b8d66b5b5acf0cf5de65df9799f4d482334c938aa0950e4d01391d5fab8994f46c0e9059f4517843e7b8d861f9b0c49eb4c5d + languageName: node + linkType: hard + "electron-to-chromium@npm:^1.5.73": version: 1.5.83 resolution: "electron-to-chromium@npm:1.5.83" @@ -15410,6 +15772,16 @@ __metadata: languageName: node linkType: hard +"enhanced-resolve@npm:^5.15.0": + version: 5.15.0 + resolution: "enhanced-resolve@npm:5.15.0" + dependencies: + graceful-fs: "npm:^4.2.4" + tapable: "npm:^2.2.0" + checksum: 10c0/69984a7990913948b4150855aed26a84afb4cb1c5a94fb8e3a65bd00729a73fc2eaff6871fb8e345377f294831afe349615c93560f2f54d61b43cdfdf668f19a + languageName: node + linkType: hard + "enquirer@npm:^2.3.5": version: 2.4.1 resolution: "enquirer@npm:2.4.1" @@ -16164,21 +16536,20 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-storybook@npm:^0.8.0": - version: 0.8.0 - resolution: "eslint-plugin-storybook@npm:0.8.0" +"eslint-plugin-storybook@npm:0.11.3--canary.187.1af857a.0": + version: 0.11.3--canary.187.1af857a.0 + resolution: "eslint-plugin-storybook@npm:0.11.3--canary.187.1af857a.0" dependencies: - "@storybook/csf": "npm:^0.0.1" - "@typescript-eslint/utils": "npm:^5.62.0" - requireindex: "npm:^1.2.0" + "@storybook/csf": "npm:^0.1.11" + "@typescript-eslint/utils": "npm:^8.8.1" ts-dedent: "npm:^2.2.0" peerDependencies: - eslint: ">=6" - checksum: 10c0/c76f6decdd4c826cd6a8bb613085e0cde804f4648093a0464a39867cc0ba4e1d34be15ff91eed827730da5efbbf55ae5e71af648bb0b461946d5e41384669ab8 + eslint: ">=8" + checksum: 10c0/1d9de1e8478fda4c0054271baf4444fa8c401c58bfb9752f5160dc83110ceff9dd0c36321a4473142e6a88cb4bda4cdbe3945cf91dc26990026a526747b2707b languageName: node linkType: hard -"eslint-scope@npm:5.1.1, eslint-scope@npm:^5.1.1": +"eslint-scope@npm:5.1.1": version: 5.1.1 resolution: "eslint-scope@npm:5.1.1" dependencies: @@ -16223,6 +16594,13 @@ __metadata: languageName: node linkType: hard +"eslint-visitor-keys@npm:^4.2.0": + version: 4.2.0 + resolution: "eslint-visitor-keys@npm:4.2.0" + checksum: 10c0/2ed81c663b147ca6f578312919483eb040295bbab759e5a371953456c636c5b49a559883e2677112453728d66293c0a4c90ab11cab3428cf02a0236d2e738269 + languageName: node + linkType: hard + "eslint@npm:8.4.1": version: 8.4.1 resolution: "eslint@npm:8.4.1" @@ -16796,7 +17174,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:3.3.3, fast-glob@npm:^3.0.3, fast-glob@npm:^3.2.2, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.1, fast-glob@npm:^3.3.2, fast-glob@npm:^3.3.3": +"fast-glob@npm:3.3.3, fast-glob@npm:^3.3.3": version: 3.3.3 resolution: "fast-glob@npm:3.3.3" dependencies: @@ -16809,6 +17187,19 @@ __metadata: languageName: node linkType: hard +"fast-glob@npm:^3.0.3, fast-glob@npm:^3.2.2, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.1, fast-glob@npm:^3.3.2": + version: 3.3.2 + resolution: "fast-glob@npm:3.3.2" + dependencies: + "@nodelib/fs.stat": "npm:^2.0.2" + "@nodelib/fs.walk": "npm:^1.2.3" + glob-parent: "npm:^5.1.2" + merge2: "npm:^1.3.0" + micromatch: "npm:^4.0.4" + checksum: 10c0/42baad7b9cd40b63e42039132bde27ca2cb3a4950d0a0f9abe4639ea1aa9d3e3b40f98b1fe31cbc0cc17b664c9ea7447d911a152fa34ec5b72977b125a6fc845 + languageName: node + linkType: hard + "fast-json-parse@npm:^1.0.3": version: 1.0.3 resolution: "fast-json-parse@npm:1.0.3" @@ -18117,7 +18508,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.3, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": +"graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.3, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 @@ -18847,7 +19238,7 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:7.0.6, https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.2": +"https-proxy-agent@npm:7.0.6": version: 7.0.6 resolution: "https-proxy-agent@npm:7.0.6" dependencies: @@ -18867,6 +19258,16 @@ __metadata: languageName: node linkType: hard +"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.2": + version: 7.0.4 + resolution: "https-proxy-agent@npm:7.0.4" + dependencies: + agent-base: "npm:^7.0.2" + debug: "npm:4" + checksum: 10c0/bc4f7c38da32a5fc622450b6cb49a24ff596f9bd48dcedb52d2da3fa1c1a80e100fb506bd59b326c012f21c863c69b275c23de1a01d0b84db396822fdf25e52b + languageName: node + linkType: hard + "human-signals@npm:^2.1.0": version: 2.1.0 resolution: "human-signals@npm:2.1.0" @@ -20390,13 +20791,20 @@ __metadata: languageName: node linkType: hard -"jsonc-parser@npm:3.3.1, jsonc-parser@npm:^3.0.0": +"jsonc-parser@npm:3.3.1": version: 3.3.1 resolution: "jsonc-parser@npm:3.3.1" checksum: 10c0/269c3ae0a0e4f907a914bf334306c384aabb9929bd8c99f909275ebd5c2d3bc70b9bcd119ad794f339dec9f24b6a4ee9cd5a8ab2e6435e730ad4075388fc2ab6 languageName: node linkType: hard +"jsonc-parser@npm:^3.0.0": + version: 3.2.1 + resolution: "jsonc-parser@npm:3.2.1" + checksum: 10c0/ada66dec143d7f9cb0e2d0d29c69e9ce40d20f3a4cb96b0c6efb745025ac7f9ba647d7ac0990d0adfc37a2d2ae084a12009a9c833dbdbeadf648879a99b9df89 + languageName: node + linkType: hard + "jsonexport@npm:^3.0.1": version: 3.2.0 resolution: "jsonexport@npm:3.2.0" @@ -23415,6 +23823,13 @@ __metadata: languageName: node linkType: hard +"node-releases@npm:^2.0.18": + version: 2.0.18 + resolution: "node-releases@npm:2.0.18" + checksum: 10c0/786ac9db9d7226339e1dc84bbb42007cb054a346bd9257e6aa154d294f01bc6a6cddb1348fa099f079be6580acbb470e3c048effd5f719325abd0179e566fd27 + languageName: node + linkType: hard + "node-releases@npm:^2.0.19": version: 2.0.19 resolution: "node-releases@npm:2.0.19" @@ -24205,6 +24620,15 @@ __metadata: languageName: node linkType: hard +"p-limit@npm:^6.2.0": + version: 6.2.0 + resolution: "p-limit@npm:6.2.0" + dependencies: + yocto-queue: "npm:^1.1.1" + checksum: 10c0/448bf55a1776ca1444594d53b3c731e68cdca00d44a6c8df06a2f6e506d5bbd540ebb57b05280f8c8bff992a630ed782a69612473f769a7473495d19e2270166 + languageName: node + linkType: hard + "p-locate@npm:^2.0.0": version: 2.0.0 resolution: "p-locate@npm:2.0.0" @@ -24756,7 +25180,14 @@ __metadata: languageName: node linkType: hard -"picocolors@npm:^1.0.0, picocolors@npm:^1.0.1, picocolors@npm:^1.1.0, picocolors@npm:^1.1.1": +"picocolors@npm:^1.0.0, picocolors@npm:^1.1.0": + version: 1.1.0 + resolution: "picocolors@npm:1.1.0" + checksum: 10c0/86946f6032148801ef09c051c6fb13b5cf942eaf147e30ea79edb91dd32d700934edebe782a1078ff859fb2b816792e97ef4dab03d7f0b804f6b01a0df35e023 + languageName: node + linkType: hard + +"picocolors@npm:^1.0.1, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 @@ -27326,7 +27757,7 @@ __metadata: languageName: node linkType: hard -"rollup@npm:4.30.1, rollup@npm:^4.23.0": +"rollup@npm:4.30.1": version: 4.30.1 resolution: "rollup@npm:4.30.1" dependencies: @@ -27412,6 +27843,78 @@ __metadata: languageName: node linkType: hard +"rollup@npm:^4.23.0": + version: 4.29.1 + resolution: "rollup@npm:4.29.1" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.29.1" + "@rollup/rollup-android-arm64": "npm:4.29.1" + "@rollup/rollup-darwin-arm64": "npm:4.29.1" + "@rollup/rollup-darwin-x64": "npm:4.29.1" + "@rollup/rollup-freebsd-arm64": "npm:4.29.1" + "@rollup/rollup-freebsd-x64": "npm:4.29.1" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.29.1" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.29.1" + "@rollup/rollup-linux-arm64-gnu": "npm:4.29.1" + "@rollup/rollup-linux-arm64-musl": "npm:4.29.1" + "@rollup/rollup-linux-loongarch64-gnu": "npm:4.29.1" + "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.29.1" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.29.1" + "@rollup/rollup-linux-s390x-gnu": "npm:4.29.1" + "@rollup/rollup-linux-x64-gnu": "npm:4.29.1" + "@rollup/rollup-linux-x64-musl": "npm:4.29.1" + "@rollup/rollup-win32-arm64-msvc": "npm:4.29.1" + "@rollup/rollup-win32-ia32-msvc": "npm:4.29.1" + "@rollup/rollup-win32-x64-msvc": "npm:4.29.1" + "@types/estree": "npm:1.0.6" + fsevents: "npm:~2.3.2" + dependenciesMeta: + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-freebsd-arm64": + optional: true + "@rollup/rollup-freebsd-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm-musleabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-loongarch64-gnu": + optional: true + "@rollup/rollup-linux-powerpc64le-gnu": + optional: true + "@rollup/rollup-linux-riscv64-gnu": + optional: true + "@rollup/rollup-linux-s390x-gnu": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: 10c0/fcd0321df78fdc74b36858e92c4b73ebf5aa8f0b9cf7c446f008e0dc3c5c4ed855d662dc44e5a09c7794bbe91017b4dd7be88b619c239f0494f9f0fbfa67c557 + languageName: node + linkType: hard + "rsvp@npm:^3.0.14, rsvp@npm:^3.0.18": version: 3.6.2 resolution: "rsvp@npm:3.6.2" @@ -27736,6 +28239,27 @@ __metadata: languageName: node linkType: hard +"send@npm:0.18.0": + version: 0.18.0 + resolution: "send@npm:0.18.0" + dependencies: + debug: "npm:2.6.9" + depd: "npm:2.0.0" + destroy: "npm:1.2.0" + encodeurl: "npm:~1.0.2" + escape-html: "npm:~1.0.3" + etag: "npm:~1.8.1" + fresh: "npm:0.5.2" + http-errors: "npm:2.0.0" + mime: "npm:1.6.0" + ms: "npm:2.1.3" + on-finished: "npm:2.4.1" + range-parser: "npm:~1.2.1" + statuses: "npm:2.0.1" + checksum: 10c0/0eb134d6a51fc13bbcb976a1f4214ea1e33f242fae046efc311e80aff66c7a43603e26a79d9d06670283a13000e51be6e0a2cb80ff0942eaf9f1cd30b7ae736a + languageName: node + linkType: hard + "send@npm:0.19.0": version: 0.19.0 resolution: "send@npm:0.19.0" @@ -27781,7 +28305,7 @@ __metadata: languageName: node linkType: hard -"serve-static@npm:1.16.2, serve-static@npm:^1.14.1": +"serve-static@npm:1.16.2": version: 1.16.2 resolution: "serve-static@npm:1.16.2" dependencies: @@ -27793,6 +28317,18 @@ __metadata: languageName: node linkType: hard +"serve-static@npm:^1.14.1": + version: 1.15.0 + resolution: "serve-static@npm:1.15.0" + dependencies: + encodeurl: "npm:~1.0.2" + escape-html: "npm:~1.0.3" + parseurl: "npm:~1.3.3" + send: "npm:0.18.0" + checksum: 10c0/fa9f0e21a540a28f301258dfe1e57bb4f81cd460d28f0e973860477dd4acef946a1f41748b5bd41c73b621bea2029569c935faa38578fd34cd42a9b4947088ba + languageName: node + linkType: hard + "set-blocking@npm:^2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" @@ -29257,7 +29793,7 @@ __metadata: languageName: node linkType: hard -"terser@npm:5.37.0, terser@npm:^5.10.0, terser@npm:^5.26.0": +"terser@npm:5.37.0": version: 5.37.0 resolution: "terser@npm:5.37.0" dependencies: @@ -29271,6 +29807,20 @@ __metadata: languageName: node linkType: hard +"terser@npm:^5.10.0, terser@npm:^5.26.0": + version: 5.29.1 + resolution: "terser@npm:5.29.1" + dependencies: + "@jridgewell/source-map": "npm:^0.3.3" + acorn: "npm:^8.8.2" + commander: "npm:^2.20.0" + source-map-support: "npm:~0.5.20" + bin: + terser: bin/terser + checksum: 10c0/5f50762d0804bf906dab4f8102811b0b94b8bceebe0f5f6186ee902200a089f06445c10f0f9bfd0cf3e118a5dd149a7cf625ec008cb880235be6901b43280833 + languageName: node + linkType: hard + "test-exclude@npm:^6.0.0": version: 6.0.0 resolution: "test-exclude@npm:6.0.0" @@ -29633,6 +30183,15 @@ __metadata: languageName: node linkType: hard +"ts-api-utils@npm:^2.0.0": + version: 2.0.0 + resolution: "ts-api-utils@npm:2.0.0" + peerDependencies: + typescript: ">=4.8.4" + checksum: 10c0/6165e29a5b75bd0218e3cb0f9ee31aa893dbd819c2e46dbb086c841121eb0436ed47c2c18a20cb3463d74fd1fb5af62e2604ba5971cc48e5b38ebbdc56746dfc + languageName: node + linkType: hard + "ts-dedent@npm:^2.0.0, ts-dedent@npm:^2.2.0": version: 2.2.0 resolution: "ts-dedent@npm:2.2.0" @@ -29776,28 +30335,24 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2.8.1, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2": +"tslib@npm:2.8.1": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 languageName: node linkType: hard -"tslib@npm:^1.13.0, tslib@npm:^1.8.1, tslib@npm:^1.9.3": +"tslib@npm:^1.13.0, tslib@npm:^1.9.3": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: 10c0/69ae09c49eea644bc5ebe1bca4fa4cc2c82b7b3e02f43b84bd891504edf66dbc6b2ec0eef31a957042de2269139e4acff911e6d186a258fb14069cd7f6febce2 languageName: node linkType: hard -"tsutils@npm:^3.21.0": - version: 3.21.0 - resolution: "tsutils@npm:3.21.0" - dependencies: - tslib: "npm:^1.8.1" - peerDependencies: - typescript: ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - checksum: 10c0/02f19e458ec78ead8fffbf711f834ad8ecd2cc6ade4ec0320790713dccc0a412b99e7fd907c4cda2a1dc602c75db6f12e0108e87a5afad4b2f9e90a24cabd5a2 +"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2": + version: 2.6.2 + resolution: "tslib@npm:2.6.2" + checksum: 10c0/e03a8a4271152c8b26604ed45535954c0a45296e32445b4b87f8a5abdb2421f40b59b4ca437c4346af0f28179780d604094eb64546bee2019d903d01c6c19bdb languageName: node linkType: hard @@ -30947,8 +31502,8 @@ __metadata: linkType: hard "vite@npm:^4.0.0, vite@npm:^4.0.4": - version: 4.5.1 - resolution: "vite@npm:4.5.1" + version: 4.5.9 + resolution: "vite@npm:4.5.9" dependencies: esbuild: "npm:^0.18.10" fsevents: "npm:~2.3.2" @@ -30982,7 +31537,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/352a94b13f793e4bcbc424d680a32507343223eeda8917fde0f23c1fa1ba3db7c806dade8461ca5cfb270154ddb8895a219fdd4384519fe9b8e46d1cf491a890 + checksum: 10c0/d51b9da32fddc6079333a16306c4c70d6ea6b253267931b5cd5d1c521bcfbee926297dc6878da79b0f1e058b7eef72555226be701fae376c2dfae9f83bc5699a languageName: node linkType: hard @@ -31210,9 +31765,9 @@ __metadata: linkType: hard "vue-component-type-helpers@npm:latest": - version: 2.2.0 - resolution: "vue-component-type-helpers@npm:2.2.0" - checksum: 10c0/3e95ddc38e01accdc1f0ae4bdeb3b5a32fd9ad3f7f10100d84c2752492d4f757d305d3026d26244b574d921eb35123a5f1cfa6f2e2c943a4ecd7deeaf777843d + version: 1.8.15 + resolution: "vue-component-type-helpers@npm:1.8.15" + checksum: 10c0/3397faf50844e31d92a29a44616088408cf6eb8b6c88b7deb8841c4bc78e8e258fafbb7052b100e52ebea5d1c4e826b7247b2c48f6f3c330addd53132db141f9 languageName: node linkType: hard @@ -31392,7 +31947,7 @@ __metadata: languageName: node linkType: hard -"watchpack@npm:2.4.2, watchpack@npm:^2.2.0, watchpack@npm:^2.4.1": +"watchpack@npm:2.4.2, watchpack@npm:^2.4.1": version: 2.4.2 resolution: "watchpack@npm:2.4.2" dependencies: @@ -31402,6 +31957,16 @@ __metadata: languageName: node linkType: hard +"watchpack@npm:^2.2.0, watchpack@npm:^2.4.0": + version: 2.4.0 + resolution: "watchpack@npm:2.4.0" + dependencies: + glob-to-regexp: "npm:^0.4.1" + graceful-fs: "npm:^4.1.2" + checksum: 10c0/c5e35f9fb9338d31d2141d9835643c0f49b5f9c521440bb648181059e5940d93dd8ed856aa8a33fbcdd4e121dad63c7e8c15c063cf485429cd9d427be197fe62 + languageName: node + linkType: hard + "wbuf@npm:^1.1.0, wbuf@npm:^1.7.3": version: 1.7.3 resolution: "wbuf@npm:1.7.3" @@ -31598,7 +32163,44 @@ __metadata: languageName: node linkType: hard -"webpack@npm:5, webpack@npm:5.97.1, webpack@npm:^5, webpack@npm:^5.65.0": +"webpack@npm:5, webpack@npm:^5, webpack@npm:^5.65.0": + version: 5.90.3 + resolution: "webpack@npm:5.90.3" + dependencies: + "@types/eslint-scope": "npm:^3.7.3" + "@types/estree": "npm:^1.0.5" + "@webassemblyjs/ast": "npm:^1.11.5" + "@webassemblyjs/wasm-edit": "npm:^1.11.5" + "@webassemblyjs/wasm-parser": "npm:^1.11.5" + acorn: "npm:^8.7.1" + acorn-import-assertions: "npm:^1.9.0" + browserslist: "npm:^4.21.10" + chrome-trace-event: "npm:^1.0.2" + enhanced-resolve: "npm:^5.15.0" + es-module-lexer: "npm:^1.2.1" + eslint-scope: "npm:5.1.1" + events: "npm:^3.2.0" + glob-to-regexp: "npm:^0.4.1" + graceful-fs: "npm:^4.2.9" + json-parse-even-better-errors: "npm:^2.3.1" + loader-runner: "npm:^4.2.0" + mime-types: "npm:^2.1.27" + neo-async: "npm:^2.6.2" + schema-utils: "npm:^3.2.0" + tapable: "npm:^2.1.1" + terser-webpack-plugin: "npm:^5.3.10" + watchpack: "npm:^2.4.0" + webpack-sources: "npm:^3.2.3" + peerDependenciesMeta: + webpack-cli: + optional: true + bin: + webpack: bin/webpack.js + checksum: 10c0/f737aa871cadbbae89833eb85387f1bf9ee0768f039100a3c8134f2fdcc78c3230ca775c373b1aa467b272f74c6831e119f7a8a1c14dcac97327212be9c93eeb + languageName: node + linkType: hard + +"webpack@npm:5.97.1": version: 5.97.1 resolution: "webpack@npm:5.97.1" dependencies: @@ -32115,6 +32717,13 @@ __metadata: languageName: node linkType: hard +"yocto-queue@npm:^1.1.1": + version: 1.1.1 + resolution: "yocto-queue@npm:1.1.1" + checksum: 10c0/cb287fe5e6acfa82690acb43c283de34e945c571a78a939774f6eaba7c285bacdf6c90fbc16ce530060863984c906d2b4c6ceb069c94d1e0a06d5f2b458e2a92 + languageName: node + linkType: hard + "yoctocolors-cjs@npm:^2.1.2": version: 2.1.2 resolution: "yoctocolors-cjs@npm:2.1.2" diff --git a/scripts/package.json b/scripts/package.json index 7f8216b2c86f..083e0a231cd6 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -125,7 +125,7 @@ "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-react": "^7.34.2", "eslint-plugin-react-hooks": "^4.6.2", - "eslint-plugin-storybook": "^0.8.0", + "eslint-plugin-storybook": "0.11.3--canary.187.1af857a.0", "execa": "^6.1.0", "fast-folder-size": "^2.2.0", "fast-glob": "^3.3.2", diff --git a/scripts/prepare/addon-bundle.ts b/scripts/prepare/addon-bundle.ts index 21e7f1b0bbc8..b5d2b97b3b5e 100755 --- a/scripts/prepare/addon-bundle.ts +++ b/scripts/prepare/addon-bundle.ts @@ -130,6 +130,7 @@ const run = async ({ cwd, flags }: { cwd: string; flags: string[] }) => { }; const commonExternals = [ + '@storybook/csf', name, ...extraExternals, ...Object.keys(dependencies || {}), diff --git a/scripts/tasks/sandbox-parts.ts b/scripts/tasks/sandbox-parts.ts index f9a41abb7069..20f783ff4a67 100644 --- a/scripts/tasks/sandbox-parts.ts +++ b/scripts/tasks/sandbox-parts.ts @@ -20,7 +20,7 @@ import { join, relative, resolve, sep } from 'path'; import slash from 'slash'; import dedent from 'ts-dedent'; -import { babelParse } from '../../code/core/src/babel/babelParse'; +import { babelParse, types as t } from '../../code/core/src/babel'; import { detectLanguage } from '../../code/core/src/cli/detect'; import { SupportedLanguage } from '../../code/core/src/cli/project_types'; import { JsPackageManagerFactory, versions as storybookPackages } from '../../code/core/src/common'; @@ -418,33 +418,50 @@ export async function setupVitest(details: TemplateDetails, options: PassedOptio ? template.expected.framework : template.expected.renderer; - await writeFile( - join(sandboxDir, '.storybook/vitest.setup.ts'), - dedent`import { beforeAll } from 'vitest' - import { setProjectAnnotations } from '${storybookPackage}' - import * as rendererDocsAnnotations from '${template.expected.renderer}/dist/entry-preview-docs.mjs' - import * as addonA11yAnnotations from '@storybook/addon-a11y/preview' - import * as addonActionsAnnotations from '@storybook/addon-actions/preview' - import * as addonTestAnnotations from '@storybook/experimental-addon-test/preview' - import '../src/stories/components' - import * as coreAnnotations from '../template-stories/core/preview' - import * as toolbarAnnotations from '../template-stories/addons/toolbars/preview' - import * as projectAnnotations from './preview' - ${isVue ? 'import * as vueAnnotations from "../src/stories/renderers/vue3/preview.js"' : ''} - - const annotations = setProjectAnnotations([ - ${isVue ? 'vueAnnotations,' : ''} - rendererDocsAnnotations, - coreAnnotations, - toolbarAnnotations, - addonActionsAnnotations, - addonTestAnnotations, - addonA11yAnnotations, - projectAnnotations, - ]) - - beforeAll(annotations.beforeAll)` - ); + const setupFilePath = join(sandboxDir, '.storybook/vitest.setup.ts'); + + const shouldUseCsf4 = template.expected.framework === '@storybook/react-vite'; + if (shouldUseCsf4) { + await writeFile( + setupFilePath, + dedent`import { beforeAll } from 'vitest' + import { setProjectAnnotations } from '${storybookPackage}' + import projectAnnotations from './preview' + + // setProjectAnnotations still kept to support non-CSF4 story tests + const annotations = setProjectAnnotations(projectAnnotations.composed) + beforeAll(annotations.beforeAll) + ` + ); + } else { + await writeFile( + setupFilePath, + dedent`import { beforeAll } from 'vitest' + import { setProjectAnnotations } from '${storybookPackage}' + import * as rendererDocsAnnotations from '${template.expected.renderer}/dist/entry-preview-docs.mjs' + import * as addonA11yAnnotations from '@storybook/addon-a11y/preview' + import * as addonActionsAnnotations from '@storybook/addon-actions/preview' + import * as addonTestAnnotations from '@storybook/experimental-addon-test/preview' + import '../src/stories/components' + import * as coreAnnotations from '../template-stories/core/preview' + import * as toolbarAnnotations from '../template-stories/addons/toolbars/preview' + import * as projectAnnotations from './preview' + ${isVue ? 'import * as vueAnnotations from "../src/stories/renderers/vue3/preview.js"' : ''} + + const annotations = setProjectAnnotations([ + ${isVue ? 'vueAnnotations,' : ''} + rendererDocsAnnotations, + coreAnnotations, + toolbarAnnotations, + addonActionsAnnotations, + addonTestAnnotations, + addonA11yAnnotations, + projectAnnotations, + ]) + + beforeAll(annotations.beforeAll)` + ); + } const opts = { cwd: sandboxDir }; const viteConfigFile = await findFirstPath(['vite.config.ts', 'vite.config.js'], opts); @@ -883,6 +900,17 @@ export const extendPreview: Task['run'] = async ({ template, sandboxDir }) => { logger.log('📝 Extending preview.js'); const previewConfig = await readConfig({ cwd: sandboxDir, fileName: 'preview' }); + if (template.modifications?.useCsfFactory) { + previewConfig.setImport(null, '../src/stories/components'); + previewConfig.setImport({ namespace: 'coreAnnotations' }, '../template-stories/core/preview'); + previewConfig.setImport( + { namespace: 'toolbarAnnotations' }, + '../template-stories/addons/toolbars/preview' + ); + previewConfig.appendNodeToArray(['addons'], t.identifier('coreAnnotations')); + previewConfig.appendNodeToArray(['addons'], t.identifier('toolbarAnnotations')); + } + if (template.expected.builder.includes('vite')) { previewConfig.setFieldValue(['tags'], ['vitest', '!a11y-test']); } @@ -890,6 +918,17 @@ export const extendPreview: Task['run'] = async ({ template, sandboxDir }) => { await writeConfig(previewConfig); }; +export const runMigrations: Task['run'] = async ({ sandboxDir, template }, { dryRun, debug }) => { + if (template.modifications?.useCsfFactory) { + await executeCLIStep(steps.automigrate, { + cwd: sandboxDir, + argument: 'csf-factories', + dryRun, + debug, + }); + } +}; + export async function setImportMap(cwd: string) { const packageJson = await readJson(join(cwd, 'package.json')); diff --git a/scripts/tasks/sandbox.ts b/scripts/tasks/sandbox.ts index c9ae7fea8473..850522e2fb4c 100644 --- a/scripts/tasks/sandbox.ts +++ b/scripts/tasks/sandbox.ts @@ -67,6 +67,7 @@ export const sandbox: Task = { addExtraDependencies, setImportMap, setupVitest, + runMigrations, } = await import('./sandbox-parts'); const extraDeps = [ @@ -146,8 +147,6 @@ export const sandbox: Task = { await extendMain(details, options); - await extendPreview(details, options); - await setImportMap(details.sandboxDir); const { JsPackageManagerFactory } = await import('../../code/core/src/common'); @@ -157,6 +156,10 @@ export const sandbox: Task = { await remove(path.join(details.sandboxDir, 'node_modules')); await packageManager.installDependencies(); + await runMigrations(details, options); + + await extendPreview(details, options); + logger.info(`✅ Storybook sandbox created at ${details.sandboxDir}`); }, }; diff --git a/scripts/utils/cli-step.ts b/scripts/utils/cli-step.ts index 188cb1a68449..d7933400fc39 100644 --- a/scripts/utils/cli-step.ts +++ b/scripts/utils/cli-step.ts @@ -74,6 +74,22 @@ export const steps = { icon: '🖥 ', options: createOptions({}), }, + migrate: { + command: 'migrate', + hasArgument: true, + description: 'Run codemods', + icon: '🚀', + options: createOptions({ + glob: { type: 'string' }, + }), + }, + automigrate: { + command: 'automigrate', + hasArgument: true, + description: 'Run automigrations', + icon: '🤖', + options: createOptions({}), + }, }; export async function executeCLIStep( diff --git a/scripts/utils/options.test.ts b/scripts/utils/options.test.ts index 2cf5dfecf7f4..bbe9b5e59a8b 100644 --- a/scripts/utils/options.test.ts +++ b/scripts/utils/options.test.ts @@ -150,7 +150,7 @@ describe('getCommand', () => { }); it('works with string options', () => { - expect(getCommand('node foo', { third }, { third: 'one' })).toBe('node foo --third one'); + expect(getCommand('node foo', { third }, { third: 'one' })).toBe(`node foo --third 'one'`); }); it('works with multiple string options', () => { @@ -162,7 +162,7 @@ describe('getCommand', () => { // This is for convenience it('works with partial options', () => { expect(getCommand('node foo', allOptions, { third: 'one' })).toBe( - 'node foo --no-second --third one' + `node foo --no-second --third 'one'` ); }); @@ -174,6 +174,6 @@ describe('getCommand', () => { third: 'one', fifth: ['a', 'b'], }) - ).toBe('node foo --first --no-second --third one --fifth a --fifth b'); + ).toBe(`node foo --first --no-second --third 'one' --fifth a --fifth b`); }); }); diff --git a/scripts/utils/options.ts b/scripts/utils/options.ts index d83131181f5a..c0b7d44a0bc9 100644 --- a/scripts/utils/options.ts +++ b/scripts/utils/options.ts @@ -296,7 +296,7 @@ function getFlag( if (option.type === 'string') { if (value) { - return `--${longFlag(key, option)} ${value}`; + return `--${longFlag(key, option)} '${value}'`; } return ''; } diff --git a/scripts/yarn.lock b/scripts/yarn.lock index aa22dcbf53a9..17cd2b97de0b 100644 --- a/scripts/yarn.lock +++ b/scripts/yarn.lock @@ -49,7 +49,7 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.0, @babel/code-frame@npm:^7.21.4, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.24.2, @babel/code-frame@npm:^7.24.7, @babel/code-frame@npm:^7.25.9": +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.0, @babel/code-frame@npm:^7.21.4, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.24.2": version: 7.26.2 resolution: "@babel/code-frame@npm:7.26.2" dependencies: @@ -60,6 +60,16 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/code-frame@npm:7.24.7" + dependencies: + "@babel/highlight": "npm:^7.24.7" + picocolors: "npm:^1.0.0" + checksum: 10c0/ab0af539473a9f5aeaac7047e377cb4f4edd255a81d84a76058595f8540784cc3fbe8acf73f1e073981104562490aabfb23008cd66dc677a456a4ed5390fdde6 + languageName: node + linkType: hard + "@babel/generator@npm:7.17.7": version: 7.17.7 resolution: "@babel/generator@npm:7.17.7" @@ -72,15 +82,14 @@ __metadata: linkType: hard "@babel/generator@npm:^7.23.0, @babel/generator@npm:^7.23.6, @babel/generator@npm:^7.25.0": - version: 7.26.3 - resolution: "@babel/generator@npm:7.26.3" + version: 7.25.0 + resolution: "@babel/generator@npm:7.25.0" dependencies: - "@babel/parser": "npm:^7.26.3" - "@babel/types": "npm:^7.26.3" + "@babel/types": "npm:^7.25.0" "@jridgewell/gen-mapping": "npm:^0.3.5" "@jridgewell/trace-mapping": "npm:^0.3.25" - jsesc: "npm:^3.0.2" - checksum: 10c0/54f260558e3e4ec8942da3cde607c35349bb983c3a7c5121243f96893fba3e8cd62e1f1773b2051f936f8c8a10987b758d5c7d76dbf2784e95bb63ab4843fa00 + jsesc: "npm:^2.5.1" + checksum: 10c0/d0e2dfcdc8bdbb5dded34b705ceebf2e0bc1b06795a1530e64fb6a3ccf313c189db7f60c1616effae48114e1a25adc75855bc4496f3779a396b3377bae718ce7 languageName: node linkType: hard @@ -119,6 +128,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.24.8": + version: 7.24.8 + resolution: "@babel/helper-string-parser@npm:7.24.8" + checksum: 10c0/6361f72076c17fabf305e252bf6d580106429014b3ab3c1f5c4eb3e6d465536ea6b670cc0e9a637a77a9ad40454d3e41361a2909e70e305116a23d68ce094c08 + languageName: node + linkType: hard + "@babel/helper-string-parser@npm:^7.25.9": version: 7.25.9 resolution: "@babel/helper-string-parser@npm:7.25.9" @@ -133,7 +149,26 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.20.5, @babel/parser@npm:^7.22.5, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.25.3, @babel/parser@npm:^7.25.4, @babel/parser@npm:^7.25.9, @babel/parser@npm:^7.26.3": +"@babel/helper-validator-identifier@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/helper-validator-identifier@npm:7.24.7" + checksum: 10c0/87ad608694c9477814093ed5b5c080c2e06d44cb1924ae8320474a74415241223cc2a725eea2640dd783ff1e3390e5f95eede978bc540e870053152e58f1d651 + languageName: node + linkType: hard + +"@babel/highlight@npm:^7.24.7": + version: 7.24.7 + resolution: "@babel/highlight@npm:7.24.7" + dependencies: + "@babel/helper-validator-identifier": "npm:^7.24.7" + chalk: "npm:^2.4.2" + js-tokens: "npm:^4.0.0" + picocolors: "npm:^1.0.0" + checksum: 10c0/674334c571d2bb9d1c89bdd87566383f59231e16bcdcf5bb7835babdf03c9ae585ca0887a7b25bdf78f303984af028df52831c7989fecebb5101cc132da9393a + languageName: node + linkType: hard + +"@babel/parser@npm:^7.20.5, @babel/parser@npm:^7.22.5, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.25.4": version: 7.26.5 resolution: "@babel/parser@npm:7.26.5" dependencies: @@ -144,6 +179,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.25.0, @babel/parser@npm:^7.25.3": + version: 7.25.3 + resolution: "@babel/parser@npm:7.25.3" + dependencies: + "@babel/types": "npm:^7.25.2" + bin: + parser: ./bin/babel-parser.js + checksum: 10c0/874b01349aedb805d6694f867a752fdc7469778fad76aca4548d2cc6ce96087c3ba5fb917a6f8d05d2d1a74aae309b5f50f1a4dba035f5a2c9fcfe6e106d2c4e + languageName: node + linkType: hard + "@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.23.2": version: 7.23.2 resolution: "@babel/runtime@npm:7.23.2" @@ -154,13 +200,13 @@ __metadata: linkType: hard "@babel/template@npm:^7.24.7, @babel/template@npm:^7.25.0": - version: 7.25.9 - resolution: "@babel/template@npm:7.25.9" + version: 7.25.0 + resolution: "@babel/template@npm:7.25.0" dependencies: - "@babel/code-frame": "npm:^7.25.9" - "@babel/parser": "npm:^7.25.9" - "@babel/types": "npm:^7.25.9" - checksum: 10c0/ebe677273f96a36c92cc15b7aa7b11cc8bc8a3bb7a01d55b2125baca8f19cae94ff3ce15f1b1880fb8437f3a690d9f89d4e91f16fc1dc4d3eb66226d128983ab + "@babel/code-frame": "npm:^7.24.7" + "@babel/parser": "npm:^7.25.0" + "@babel/types": "npm:^7.25.0" + checksum: 10c0/4e31afd873215744c016e02b04f43b9fa23205d6d0766fb2e93eb4091c60c1b88897936adb895fb04e3c23de98dfdcbe31bc98daaa1a4e0133f78bb948e1209b languageName: node linkType: hard @@ -207,7 +253,28 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.17.0, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.24.7, @babel/types@npm:^7.25.2, @babel/types@npm:^7.25.4, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.3, @babel/types@npm:^7.26.5": +"@babel/types@npm:^7.17.0, @babel/types@npm:^7.23.0, @babel/types@npm:^7.24.7": + version: 7.26.3 + resolution: "@babel/types@npm:7.26.3" + dependencies: + "@babel/helper-string-parser": "npm:^7.25.9" + "@babel/helper-validator-identifier": "npm:^7.25.9" + checksum: 10c0/966c5242c5e55c8704bf7a7418e7be2703a0afa4d19a8480999d5a4ef13d095dd60686615fe5983cb7593b4b06ba3a7de8d6ca501c1d78bdd233a10d90be787b + languageName: node + linkType: hard + +"@babel/types@npm:^7.22.5, @babel/types@npm:^7.25.0, @babel/types@npm:^7.25.2": + version: 7.25.2 + resolution: "@babel/types@npm:7.25.2" + dependencies: + "@babel/helper-string-parser": "npm:^7.24.8" + "@babel/helper-validator-identifier": "npm:^7.24.7" + to-fast-properties: "npm:^2.0.0" + checksum: 10c0/e489435856be239f8cc1120c90a197e4c2865385121908e5edb7223cfdff3768cba18f489adfe0c26955d9e7bbb1fb10625bc2517505908ceb0af848989bd864 + languageName: node + linkType: hard + +"@babel/types@npm:^7.25.4, @babel/types@npm:^7.26.5": version: 7.26.5 resolution: "@babel/types@npm:7.26.5" dependencies: @@ -1388,12 +1455,12 @@ __metadata: languageName: node linkType: hard -"@storybook/csf@npm:^0.0.1": - version: 0.0.1 - resolution: "@storybook/csf@npm:0.0.1" +"@storybook/csf@npm:^0.1.11": + version: 0.1.13 + resolution: "@storybook/csf@npm:0.1.13" dependencies: - lodash: "npm:^4.17.15" - checksum: 10c0/7b0f75763415f9147692a460b44417ee56ea9639433716a1fd4d1df4c8b0221cbc71b8da0fbed4dcecb3ccd6c7ed64be39f5c255c713539a6088a1d6488aaa24 + type-fest: "npm:^2.19.0" + checksum: 10c0/7c57b531ac95ca45239f498d419483d675e58cd8d549e0bac623519cc1ef4f3c9c6b75ec3873aa51cc2872728012db5dd5e1f2c2d8085014241eb4b896480996 languageName: node linkType: hard @@ -1502,7 +1569,7 @@ __metadata: eslint-plugin-prettier: "npm:^5.1.3" eslint-plugin-react: "npm:^7.34.2" eslint-plugin-react-hooks: "npm:^4.6.2" - eslint-plugin-storybook: "npm:^0.8.0" + eslint-plugin-storybook: "npm:0.11.3--canary.187.1af857a.0" execa: "npm:^6.1.0" fast-folder-size: "npm:^2.2.0" fast-glob: "npm:^3.3.2" @@ -2245,6 +2312,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:8.19.1": + version: 8.19.1 + resolution: "@typescript-eslint/scope-manager@npm:8.19.1" + dependencies: + "@typescript-eslint/types": "npm:8.19.1" + "@typescript-eslint/visitor-keys": "npm:8.19.1" + checksum: 10c0/7dca0c28ad27a0c7e26499e0f584f98efdcf34087f46aadc661b36c310484b90655e83818bafd249b5a28c7094a69c54d553f6cd403869bf134f95a9148733f5 + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:6.18.1": version: 6.18.1 resolution: "@typescript-eslint/type-utils@npm:6.18.1" @@ -2300,6 +2377,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:8.19.1": + version: 8.19.1 + resolution: "@typescript-eslint/types@npm:8.19.1" + checksum: 10c0/e907bf096d5ed7a812a1e537a98dd881ab5d2d47e072225bfffaa218c1433115a148b27a15744db8374b46dac721617c6d13a1da255fdeb369cf193416533f6e + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/typescript-estree@npm:5.62.0" @@ -2356,7 +2440,25 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:5.62.0, @typescript-eslint/utils@npm:^5.62.0": +"@typescript-eslint/typescript-estree@npm:8.19.1": + version: 8.19.1 + resolution: "@typescript-eslint/typescript-estree@npm:8.19.1" + dependencies: + "@typescript-eslint/types": "npm:8.19.1" + "@typescript-eslint/visitor-keys": "npm:8.19.1" + debug: "npm:^4.3.4" + fast-glob: "npm:^3.3.2" + is-glob: "npm:^4.0.3" + minimatch: "npm:^9.0.4" + semver: "npm:^7.6.0" + ts-api-utils: "npm:^2.0.0" + peerDependencies: + typescript: ">=4.8.4 <5.8.0" + checksum: 10c0/549d9d565a58a25fc8397a555506f2e8d29a740f5b6ed9105479e22de5aab89d9d535959034a8e9d4115adb435de09ee6987d28e8922052eea577842ddce1a7a + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/utils@npm:5.62.0" dependencies: @@ -2405,6 +2507,21 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:^8.8.1": + version: 8.19.1 + resolution: "@typescript-eslint/utils@npm:8.19.1" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.4.0" + "@typescript-eslint/scope-manager": "npm:8.19.1" + "@typescript-eslint/types": "npm:8.19.1" + "@typescript-eslint/typescript-estree": "npm:8.19.1" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <5.8.0" + checksum: 10c0/f7d2fe9a2bd8cb3ae6fafe5e465882a6784b2acf81d43d194c579381b92651c2ffc0fca69d2a35eee119f539622752a0e9ec063aaec7576d5d2bfe68b441980d + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/visitor-keys@npm:5.62.0" @@ -2435,6 +2552,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.19.1": + version: 8.19.1 + resolution: "@typescript-eslint/visitor-keys@npm:8.19.1" + dependencies: + "@typescript-eslint/types": "npm:8.19.1" + eslint-visitor-keys: "npm:^4.2.0" + checksum: 10c0/117537450a099f51f3f0d39186f248ae370bdc1b7f6975dbdbffcfc89e6e1aa47c1870db790d4f778a48f2c1f6cd9c269b63867c12afaa424367c63dabee8fd0 + languageName: node + linkType: hard + "@ungap/structured-clone@npm:^1.0.0, @ungap/structured-clone@npm:^1.2.0": version: 1.2.1 resolution: "@ungap/structured-clone@npm:1.2.1" @@ -3185,14 +3312,14 @@ __metadata: linkType: hard "array.prototype.flatmap@npm:^1.3.2": - version: 1.3.3 - resolution: "array.prototype.flatmap@npm:1.3.3" + version: 1.3.2 + resolution: "array.prototype.flatmap@npm:1.3.2" dependencies: - call-bind: "npm:^1.0.8" - define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.5" - es-shim-unscopables: "npm:^1.0.2" - checksum: 10c0/ba899ea22b9dc9bf276e773e98ac84638ed5e0236de06f13d63a90b18ca9e0ec7c97d622d899796e3773930b946cd2413d098656c0c5d8cc58c6f25c21e6bd54 + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + es-shim-unscopables: "npm:^1.0.0" + checksum: 10c0/67b3f1d602bb73713265145853128b1ad77cc0f9b833c7e1e056b323fbeac41a4ff1c9c99c7b9445903caea924d9ca2450578d9011913191aa88cc3c3a4b54f4 languageName: node linkType: hard @@ -3782,7 +3909,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^2.3.0": +"chalk@npm:^2.3.0, chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" dependencies: @@ -3813,14 +3940,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^5.0.0": - version: 5.4.1 - resolution: "chalk@npm:5.4.1" - checksum: 10c0/b23e88132c702f4855ca6d25cb5538b1114343e41472d5263ee8a37cccfccd9c4216d111e1097c6a27830407a1dc81fecdf2a56f2c63033d4dbbd88c10b0dcef - languageName: node - linkType: hard - -"chalk@npm:~5.3.0": +"chalk@npm:^5.0.0, chalk@npm:~5.3.0": version: 5.3.0 resolution: "chalk@npm:5.3.0" checksum: 10c0/8297d436b2c0f95801103ff2ef67268d362021b8210daf8ddbe349695333eb3610a71122172ff3b0272f1ef2cf7cc2c41fdaa4715f52e49ffe04c56340feed09 @@ -4465,15 +4585,15 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.4.0": - version: 4.4.0 - resolution: "debug@npm:4.4.0" +"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:~4.3.4": + version: 4.3.7 + resolution: "debug@npm:4.3.7" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10c0/db94f1a182bf886f57b4755f85b3a74c39b5114b9377b7ab375dc2cfa3454f09490cc6c30f829df3fc8042bc8b8995f6567ce5cd96f3bc3688bd24027197d9de + checksum: 10c0/1471db19c3b06d485a622d62f65947a19a23fbd0dd73f7fd3eafb697eec5360cde447fb075919987899b1a2096e85d35d4eb5a4de09a57600ac9cf7e6c8e768b languageName: node linkType: hard @@ -4498,15 +4618,15 @@ __metadata: languageName: node linkType: hard -"debug@npm:~4.3.4": - version: 4.3.7 - resolution: "debug@npm:4.3.7" +"debug@npm:^4.4.0": + version: 4.4.0 + resolution: "debug@npm:4.4.0" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10c0/1471db19c3b06d485a622d62f65947a19a23fbd0dd73f7fd3eafb697eec5360cde447fb075919987899b1a2096e85d35d4eb5a4de09a57600ac9cf7e6c8e768b + checksum: 10c0/db94f1a182bf886f57b4755f85b3a74c39b5114b9377b7ab375dc2cfa3454f09490cc6c30f829df3fc8042bc8b8995f6567ce5cd96f3bc3688bd24027197d9de languageName: node linkType: hard @@ -5675,17 +5795,16 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-storybook@npm:^0.8.0": - version: 0.8.0 - resolution: "eslint-plugin-storybook@npm:0.8.0" +"eslint-plugin-storybook@npm:0.11.3--canary.187.1af857a.0": + version: 0.11.3--canary.187.1af857a.0 + resolution: "eslint-plugin-storybook@npm:0.11.3--canary.187.1af857a.0" dependencies: - "@storybook/csf": "npm:^0.0.1" - "@typescript-eslint/utils": "npm:^5.62.0" - requireindex: "npm:^1.2.0" + "@storybook/csf": "npm:^0.1.11" + "@typescript-eslint/utils": "npm:^8.8.1" ts-dedent: "npm:^2.2.0" peerDependencies: - eslint: ">=6" - checksum: 10c0/c76f6decdd4c826cd6a8bb613085e0cde804f4648093a0464a39867cc0ba4e1d34be15ff91eed827730da5efbbf55ae5e71af648bb0b461946d5e41384669ab8 + eslint: ">=8" + checksum: 10c0/1d9de1e8478fda4c0054271baf4444fa8c401c58bfb9752f5160dc83110ceff9dd0c36321a4473142e6a88cb4bda4cdbe3945cf91dc26990026a526747b2707b languageName: node linkType: hard @@ -5716,6 +5835,13 @@ __metadata: languageName: node linkType: hard +"eslint-visitor-keys@npm:^4.2.0": + version: 4.2.0 + resolution: "eslint-visitor-keys@npm:4.2.0" + checksum: 10c0/2ed81c663b147ca6f578312919483eb040295bbab759e5a371953456c636c5b49a559883e2677112453728d66293c0a4c90ab11cab3428cf02a0236d2e738269 + languageName: node + linkType: hard + "eslint@npm:^8.57.0": version: 8.57.0 resolution: "eslint@npm:8.57.0" @@ -6158,7 +6284,7 @@ __metadata: languageName: node linkType: hard -"fastq@npm:^1.15.0, fastq@npm:^1.6.0": +"fastq@npm:^1.15.0": version: 1.19.0 resolution: "fastq@npm:1.19.0" dependencies: @@ -6167,6 +6293,15 @@ __metadata: languageName: node linkType: hard +"fastq@npm:^1.6.0": + version: 1.15.0 + resolution: "fastq@npm:1.15.0" + dependencies: + reusify: "npm:^1.0.4" + checksum: 10c0/5ce4f83afa5f88c9379e67906b4d31bc7694a30826d6cc8d0f0473c966929017fda65c2174b0ec89f064ede6ace6c67f8a4fe04cef42119b6a55b0d465554c24 + languageName: node + linkType: hard + "fd-package-json@npm:^1.2.0": version: 1.2.0 resolution: "fd-package-json@npm:1.2.0" @@ -6344,13 +6479,13 @@ __metadata: linkType: hard "form-data@npm:^4.0.0": - version: 4.0.1 - resolution: "form-data@npm:4.0.1" + version: 4.0.0 + resolution: "form-data@npm:4.0.0" dependencies: asynckit: "npm:^0.4.0" combined-stream: "npm:^1.0.8" mime-types: "npm:^2.1.12" - checksum: 10c0/bb102d570be8592c23f4ea72d7df9daa50c7792eb0cf1c5d7e506c1706e7426a4e4ae48a35b109e91c85f1c0ec63774a21ae252b66f4eb981cb8efef7d0463c8 + checksum: 10c0/cb6f3ac49180be03ff07ba3ff125f9eba2ff0b277fb33c7fc47569fc5e616882c5b1c69b9904c4c4187e97dd0419dd03b134174756f296dec62041e6527e2c6e languageName: node linkType: hard @@ -7429,13 +7564,10 @@ __metadata: languageName: node linkType: hard -"ip-address@npm:^9.0.5": - version: 9.0.5 - resolution: "ip-address@npm:9.0.5" - dependencies: - jsbn: "npm:1.1.0" - sprintf-js: "npm:^1.1.3" - checksum: 10c0/331cd07fafcb3b24100613e4b53e1a2b4feab11e671e655d46dc09ee233da5011284d09ca40c4ecbdfe1d0004f462958675c224a804259f2f78d2465a87824bc +"ip@npm:^2.0.0": + version: 2.0.0 + resolution: "ip@npm:2.0.0" + checksum: 10c0/8d186cc5585f57372847ae29b6eba258c68862055e18a75cc4933327232cb5c107f89800ce29715d542eef2c254fbb68b382e780a7414f9ee7caf60b7a473958 languageName: node linkType: hard @@ -7559,11 +7691,11 @@ __metadata: linkType: hard "is-core-module@npm:^2.13.0, is-core-module@npm:^2.13.1": - version: 2.16.1 - resolution: "is-core-module@npm:2.16.1" + version: 2.13.1 + resolution: "is-core-module@npm:2.13.1" dependencies: - hasown: "npm:^2.0.2" - checksum: 10c0/898443c14780a577e807618aaae2b6f745c8538eca5c7bc11388a3f2dc6de82b9902bcc7eb74f07be672b11bbe82dd6a6edded44a00cb3d8f933d0459905eedd + hasown: "npm:^2.0.0" + checksum: 10c0/2cba9903aaa52718f11c4896dabc189bab980870aae86a62dc0d5cedb546896770ee946fb14c84b7adf0735f5eaea4277243f1b95f5cefa90054f92fbcac2518 languageName: node linkType: hard @@ -7865,7 +7997,16 @@ __metadata: languageName: node linkType: hard -"is-typed-array@npm:^1.1.13, is-typed-array@npm:^1.1.14, is-typed-array@npm:^1.1.15, is-typed-array@npm:^1.1.3": +"is-typed-array@npm:^1.1.13, is-typed-array@npm:^1.1.3": + version: 1.1.13 + resolution: "is-typed-array@npm:1.1.13" + dependencies: + which-typed-array: "npm:^1.1.14" + checksum: 10c0/fa5cb97d4a80e52c2cc8ed3778e39f175a1a2ae4ddf3adae3187d69586a1fd57cfa0b095db31f66aa90331e9e3da79184cea9c6abdcd1abc722dc3c3edd51cca + languageName: node + linkType: hard + +"is-typed-array@npm:^1.1.14, is-typed-array@npm:^1.1.15": version: 1.1.15 resolution: "is-typed-array@npm:1.1.15" dependencies: @@ -8179,13 +8320,6 @@ __metadata: languageName: node linkType: hard -"jsbn@npm:1.1.0": - version: 1.1.0 - resolution: "jsbn@npm:1.1.0" - checksum: 10c0/4f907fb78d7b712e11dea8c165fe0921f81a657d3443dde75359ed52eb2b5d33ce6773d97985a089f09a65edd80b11cb75c767b57ba47391fee4c969f7215c96 - languageName: node - linkType: hard - "jsbn@npm:~0.1.0": version: 0.1.1 resolution: "jsbn@npm:0.1.1" @@ -8202,15 +8336,6 @@ __metadata: languageName: node linkType: hard -"jsesc@npm:^3.0.2": - version: 3.1.0 - resolution: "jsesc@npm:3.1.0" - bin: - jsesc: bin/jsesc - checksum: 10c0/531779df5ec94f47e462da26b4cbf05eb88a83d9f08aac2ba04206508fc598527a153d08bd462bae82fc78b3eaa1a908e1a4a79f886e9238641c4cdefaf118b1 - languageName: node - linkType: hard - "json-bigint@npm:^1.0.0": version: 1.0.0 resolution: "json-bigint@npm:1.0.0" @@ -8729,7 +8854,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:4, lodash@npm:4.17.21, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.21": +"lodash@npm:4, lodash@npm:4.17.21, lodash@npm:^4.17.14, lodash@npm:^4.17.21": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: 10c0/d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c @@ -9654,7 +9779,17 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:^4.0.4, micromatch@npm:^4.0.8, micromatch@npm:~4.0.7": +"micromatch@npm:^4.0.4, micromatch@npm:~4.0.7": + version: 4.0.7 + resolution: "micromatch@npm:4.0.7" + dependencies: + braces: "npm:^3.0.3" + picomatch: "npm:^2.3.1" + checksum: 10c0/58fa99bc5265edec206e9163a1d2cec5fabc46a5b473c45f4a700adce88c2520456ae35f2b301e4410fb3afb27e9521fb2813f6fc96be0a48a89430e0916a772 + languageName: node + linkType: hard + +"micromatch@npm:^4.0.8": version: 4.0.8 resolution: "micromatch@npm:4.0.8" dependencies: @@ -10312,7 +10447,7 @@ __metadata: languageName: node linkType: hard -"object.values@npm:^1.1.6, object.values@npm:^1.1.7, object.values@npm:^1.2.0": +"object.values@npm:^1.1.6, object.values@npm:^1.2.0": version: 1.2.1 resolution: "object.values@npm:1.2.1" dependencies: @@ -10324,6 +10459,17 @@ __metadata: languageName: node linkType: hard +"object.values@npm:^1.1.7": + version: 1.2.0 + resolution: "object.values@npm:1.2.0" + dependencies: + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-object-atoms: "npm:^1.0.0" + checksum: 10c0/15809dc40fd6c5529501324fec5ff08570b7d70fb5ebbe8e2b3901afec35cf2b3dc484d1210c6c642cd3e7e0a5e18dd1d6850115337fef46bdae14ab0cb18ac3 + languageName: node + linkType: hard + "on-exit-leak-free@npm:^0.2.0": version: 0.2.0 resolution: "on-exit-leak-free@npm:0.2.0" @@ -12959,12 +13105,12 @@ __metadata: linkType: hard "socks@npm:^2.6.2, socks@npm:^2.7.1": - version: 2.8.3 - resolution: "socks@npm:2.8.3" + version: 2.7.1 + resolution: "socks@npm:2.7.1" dependencies: - ip-address: "npm:^9.0.5" + ip: "npm:^2.0.0" smart-buffer: "npm:^4.2.0" - checksum: 10c0/d54a52bf9325165770b674a67241143a3d8b4e4c8884560c4e0e078aace2a728dffc7f70150660f51b85797c4e1a3b82f9b7aa25e0a0ceae1a243365da5c51a7 + checksum: 10c0/43f69dbc9f34fc8220bc51c6eea1c39715ab3cfdb115d6e3285f6c7d1a603c5c75655668a5bbc11e3c7e2c99d60321fb8d7ab6f38cda6a215fadd0d6d0b52130 languageName: node linkType: hard @@ -13114,13 +13260,6 @@ __metadata: languageName: node linkType: hard -"sprintf-js@npm:^1.1.3": - version: 1.1.3 - resolution: "sprintf-js@npm:1.1.3" - checksum: 10c0/09270dc4f30d479e666aee820eacd9e464215cdff53848b443964202bf4051490538e5dd1b42e1a65cf7296916ca17640aebf63dae9812749c7542ee5f288dec - languageName: node - linkType: hard - "sprintf-js@npm:~1.0.2": version: 1.0.3 resolution: "sprintf-js@npm:1.0.3" @@ -13216,9 +13355,9 @@ __metadata: linkType: hard "stream-shift@npm:^1.0.0": - version: 1.0.3 - resolution: "stream-shift@npm:1.0.3" - checksum: 10c0/939cd1051ca750d240a0625b106a2b988c45fb5a3be0cebe9a9858cb01bc1955e8c7b9fac17a9462976bea4a7b704e317c5c2200c70f0ca715a3363b9aa4fd3b + version: 1.0.1 + resolution: "stream-shift@npm:1.0.1" + checksum: 10c0/b63a0d178cde34b920ad93e2c0c9395b840f408d36803b07c61416edac80ef9e480a51910e0ceea0d679cec90921bcd2cccab020d3a9fa6c73a98b0fbec132fd languageName: node linkType: hard @@ -13872,6 +14011,15 @@ __metadata: languageName: node linkType: hard +"ts-api-utils@npm:^2.0.0": + version: 2.0.0 + resolution: "ts-api-utils@npm:2.0.0" + peerDependencies: + typescript: ">=4.8.4" + checksum: 10c0/6165e29a5b75bd0218e3cb0f9ee31aa893dbd819c2e46dbb086c841121eb0436ed47c2c18a20cb3463d74fd1fb5af62e2604ba5971cc48e5b38ebbdc56746dfc + languageName: node + linkType: hard + "ts-dedent@npm:^2.2.0": version: 2.2.0 resolution: "ts-dedent@npm:2.2.0" @@ -15115,6 +15263,19 @@ __metadata: languageName: node linkType: hard +"which-typed-array@npm:^1.1.14": + version: 1.1.15 + resolution: "which-typed-array@npm:1.1.15" + dependencies: + available-typed-arrays: "npm:^1.0.7" + call-bind: "npm:^1.0.7" + for-each: "npm:^0.3.3" + gopd: "npm:^1.0.1" + has-tostringtag: "npm:^1.0.2" + checksum: 10c0/4465d5348c044032032251be54d8988270e69c6b7154f8fcb2a47ff706fe36f7624b3a24246b8d9089435a8f4ec48c1c1025c5d6b499456b9e5eff4f48212983 + languageName: node + linkType: hard + "which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.18, which-typed-array@npm:^1.1.2": version: 1.1.18 resolution: "which-typed-array@npm:1.1.18"