diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000000..3317e750e0c5 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,4 @@ +34e364a0ca1d93555d36a7367d78e8e229493de8 +c0896915fb7fb9a8dd416b9aebca17abd909d1c1 +a41c227037e7e7249b8b376f838f4f8bcc3e3e59 +13c46e6c0b7f3dd8cf4ba42d1cfd6714f4777d54 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a2458954ccd9..5180c2c1d556 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 7.6.8 + +- Addon-actions: Fix module resolution for react-native - [#25296](https://github.com/storybookjs/storybook/pull/25296), thanks [@dannyhw](https://github.com/dannyhw)! +- Storysource: Fix import error - [#25391](https://github.com/storybookjs/storybook/pull/25391), thanks [@unional](https://github.com/unional)! + ## 7.6.7 - Core: Skip no-framework error when ignorePreview=true - [#25286](https://github.com/storybookjs/storybook/pull/25286), thanks [@ndelangen](https://github.com/ndelangen)! diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 9932596fed36..26255568cc17 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,28 @@ +## 8.0.0-alpha.10 + +- API: Remove deprecations from manager and preview api - [#25536](https://github.com/storybookjs/storybook/pull/25536), thanks [@valentinpalkovic](https://github.com/valentinpalkovic)! +- Addon Controls: Remove unused hideNoControlsWarning type - [#25417](https://github.com/storybookjs/storybook/pull/25417), thanks [@yannbf](https://github.com/yannbf)! +- Addon Remark-GFM: Upgrade remark-gfm - [#25301](https://github.com/storybookjs/storybook/pull/25301), thanks [@yannbf](https://github.com/yannbf)! +- Addon-actions: Fix module resolution for react-native - [#25296](https://github.com/storybookjs/storybook/pull/25296), thanks [@dannyhw](https://github.com/dannyhw)! +- Angular: Remove deprecated Story type - [#25558](https://github.com/storybookjs/storybook/pull/25558), thanks [@valentinpalkovic](https://github.com/valentinpalkovic)! +- CLI: Add addon `remove` command - [#25538](https://github.com/storybookjs/storybook/pull/25538), thanks [@shilman](https://github.com/shilman)! +- CLI: Check optionalDependencies for storybook versions - [#25406](https://github.com/storybookjs/storybook/pull/25406), thanks [@reyronald](https://github.com/reyronald)! +- CLI: Sandbox script should use current version to init - [#25560](https://github.com/storybookjs/storybook/pull/25560), thanks [@ndelangen](https://github.com/ndelangen)! +- CLI: Versioned installation of monorepo packages - [#25517](https://github.com/storybookjs/storybook/pull/25517), thanks [@ndelangen](https://github.com/ndelangen)! +- CLI: Versioned upgrade of monorepo packages - [#25553](https://github.com/storybookjs/storybook/pull/25553), thanks [@JReinhold](https://github.com/JReinhold)! +- Core: Prevent stories lookup in node_modules - [#25214](https://github.com/storybookjs/storybook/pull/25214), thanks [@valentinpalkovic](https://github.com/valentinpalkovic)! +- Core: Refactor preview and deprecate story store - [#24926](https://github.com/storybookjs/storybook/pull/24926), thanks [@tmeasday](https://github.com/tmeasday)! +- Doc blocks: Remove deprecated props from Primary block - [#25461](https://github.com/storybookjs/storybook/pull/25461), thanks [@yannbf](https://github.com/yannbf)! +- Doc blocks: Remove deprecated props from Source block - [#25459](https://github.com/storybookjs/storybook/pull/25459), thanks [@yannbf](https://github.com/yannbf)! +- Doc blocks: Remove deprecated props from Story block - [#25460](https://github.com/storybookjs/storybook/pull/25460), thanks [@yannbf](https://github.com/yannbf)! +- Maintenance: Pin TS to >= 4.2 as typefest 2 requires it - [#25548](https://github.com/storybookjs/storybook/pull/25548), thanks [@kasperpeulen](https://github.com/kasperpeulen)! +- Maintenance: Upgrade to prettier 3 - [#25524](https://github.com/storybookjs/storybook/pull/25524), thanks [@kasperpeulen](https://github.com/kasperpeulen)! +- Remove deprecated properties from manager-api - [#25578](https://github.com/storybookjs/storybook/pull/25578), thanks [@valentinpalkovic](https://github.com/valentinpalkovic)! +- Test: Fix user event being inlined by tsup by using an interface - [#25547](https://github.com/storybookjs/storybook/pull/25547), thanks [@kasperpeulen](https://github.com/kasperpeulen)! +- Test: Upgrade test package to vitest 1.1.3 - [#25576](https://github.com/storybookjs/storybook/pull/25576), thanks [@kasperpeulen](https://github.com/kasperpeulen)! +- UI: Add configurable tags-based exclusion from sidebar/autodocs - [#25328](https://github.com/storybookjs/storybook/pull/25328), thanks [@shilman](https://github.com/shilman)! +- Webpack: Remove deprecated standalone webpackConfig option - [#25481](https://github.com/storybookjs/storybook/pull/25481), thanks [@yannbf](https://github.com/yannbf)! + ## 8.0.0-alpha.9 - AutoTitle: Fix case-insensitive trailing duplicate - [#25452](https://github.com/storybookjs/storybook/pull/25452), thanks [@ksugawara61](https://github.com/ksugawara61)! diff --git a/MIGRATION.md b/MIGRATION.md index 7d850150fc19..547996e8aba6 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -42,7 +42,11 @@ - [Web Components](#web-components) - [Dropping default babel plugins in Webpack5-based projects](#dropping-default-babel-plugins-in-webpack5-based-projects) - [Deprecations which are now removed](#deprecations-which-are-now-removed) + - [Methods and properties from AddonStore](#methods-and-properties-from-addonstore) + - [Methods and properties from PreviewAPI](#methods-and-properties-from-previewapi) + - [Removals in @storybook/types](#removals-in-storybooktypes) - [--use-npm flag in storybook CLI](#--use-npm-flag-in-storybook-cli) + - [hideNoControlsWarning parameter from addon controls](#hidenocontrolswarning-parameter-from-addon-controls) - [`setGlobalConfig` from `@storybook/react`](#setglobalconfig-from-storybookreact) - [StorybookViteConfig type from @storybook/builder-vite](#storybookviteconfig-type-from-storybookbuilder-vite) - [props from WithTooltipComponent from @storybook/components](#props-from-withtooltipcomponent-from-storybookcomponents) @@ -53,8 +57,13 @@ - [storyIndexers](#storyindexers) - [Deprecated docs parameters](#deprecated-docs-parameters) - [Description Doc block properties](#description-doc-block-properties) + - [Story Doc block properties](#story-doc-block-properties) - [Manager API expandAll and collapseAll methods](#manager-api-expandall-and-collapseall-methods) + - [Source Doc block properties](#source-doc-block-properties) + - [Canvas Doc block properties](#canvas-doc-block-properties) + - [`Primary` Doc block properties](#primary-doc-block-properties) - [`createChannel` from `@storybook/postmessage` and `@storybook/channel-websocket`](#createchannel-from-storybookpostmessage-and--storybookchannel-websocket) + - [StoryStore and methods deprecated](#storystore-and-methods-deprecated) - [From version 7.5.0 to 7.6.0](#from-version-750-to-760) - [CommonJS with Vite is deprecated](#commonjs-with-vite-is-deprecated) - [Using implicit actions during rendering is deprecated](#using-implicit-actions-during-rendering-is-deprecated) @@ -66,6 +75,7 @@ - [`storyIndexers` is replaced with `experimental_indexers`](#storyindexers-is-replaced-with-experimental_indexers) - [From version 7.0.0 to 7.2.0](#from-version-700-to-720) - [Addon API is more type-strict](#addon-api-is-more-type-strict) + - [Addon-controls hideNoControlsWarning parameter is deprecated](#addon-controls-hidenocontrolswarning-parameter-is-deprecated) - [From version 6.5.x to 7.0.0](#from-version-65x-to-700) - [7.0 breaking changes](#70-breaking-changes) - [Dropped support for Node 15 and below](#dropped-support-for-node-15-and-below) @@ -789,10 +799,48 @@ Until the 8.0 release, Storybook provided the `@babel/preset-env` preset for Web ### Deprecations which are now removed +#### Methods and properties from AddonStore + +The following methods and properties from the class `AddonStore` in `@storybook/manager-api` are now removed: + +- `serverChannel` -> Use `channel` instead +- `getServerChannel` -> Use `getChannel` instead +- `setServerChannel` -> Use `setChannel` instead +- `hasServerChannel` -> Use `hasChannel` instead +- `addPanel` + +The following methods and properties from the class `AddonStore` in `@storybook/preview-api` are now removed: + +- `serverChannel` -> Use `channel` instead +- `getServerChannel` -> Use `getChannel` instead +- `setServerChannel` -> Use `setChannel` instead +- `hasServerChannel` -> Use `hasChannel` instead + +#### Methods and properties from PreviewAPI + +The following exports from `@storybook/preview-api` are now removed: + +- `useSharedState` +- `useAddonState` + +Please file an issue if you need these APIs. + +#### Removals in @storybook/types + +The following exports from `@storybook/types` are now removed: + +- `API_ADDON` -> Use `Addon_Type` instead +- `API_COLLECTION` -> Use `Addon_Collection` instead +- `API_Panels` + #### --use-npm flag in storybook CLI The `--use-npm` is now removed. Use `--package-manager=npm` instead. [More info here](#cli-option---use-npm-deprecated). +#### hideNoControlsWarning parameter from addon controls + +The `hideNoControlsWarning` parameter is now removed. [More info here](#addon-controls-hidenocontrolswarning-parameter-is-deprecated). + #### `setGlobalConfig` from `@storybook/react` The `setGlobalConfig` (used for reusing stories in your tests) is now removed in favor of `setProjectAnnotations`. @@ -886,6 +934,12 @@ More info [here](#autodocs-changes) and [here](#source-block). `children`, `markdown` and `type` are now removed in favor of the `of` property. [More info](#doc-blocks). +#### Story Doc block properties + +The `story` prop is now removed in favor of the `of` property. [More info](#doc-blocks). + +Additionally, given that CSF in MDX is not supported anymore, the following props are also removed: `args`, `argTypes`, `decorators`, `loaders`, `name`, `parameters`, `play`, `render`, and `storyName`. [More info](#dropping-support-for-storiesmdx-csf-in-mdx-format-and-mdx1-support). + #### Manager API expandAll and collapseAll methods The `collapseAll` and `expandAll` APIs (possibly used by addons) are now removed. Please emit events for these actions instead: @@ -899,12 +953,43 @@ api.collapseAll() // becomes api.emit(STORIES_COLLAPSE_ALL) api.expandAll() // becomes api.emit(STORIES_EXPAND_ALL) ``` +#### Source Doc block properties + +`id` and `ids` are now removed in favor of the `of` property. [More info](#doc-blocks). + +#### Canvas Doc block properties + +The following properties were removed from the Canvas Doc block: + +- children +- isColumn +- columns +- withSource +- mdxSource + +[More info](#doc-blocks). + +#### `Primary` Doc block properties + +The `name` prop is now removed in favor of the `of` property. [More info](#doc-blocks). + #### `createChannel` from `@storybook/postmessage` and `@storybook/channel-websocket` The `createChannel` APIs from both `@storybook/channel-websocket` and `@storybook/postmessage` are now removed. Please use `createBrowserChannel` instead, from the `@storybook/channels` package. Additionally, the `PostmsgTransport` type is now removed in favor of `PostMessageTransport`. + +#### StoryStore and methods deprecated + +The StoryStore (`__STORYBOOK_STORY_STORE__` and `__STORYBOOK_PREVIEW__.storyStore`) are deprecated, and will no longer be accessible in Storybook 9.0. + +In particular, the following methods on the `StoryStore` are deprecated and will be removed in 9.0: + - `store.fromId()` - please use `preview.loadStory({ storyId })` instead. + - `store.raw()` - please use `preview.extract()` instead. + +Note that both these methods require initialization, so you should await `preview.ready()`. + ## From version 7.5.0 to 7.6.0 #### CommonJS with Vite is deprecated @@ -1085,6 +1170,18 @@ The API: `addons.addPanel()` is now deprecated, and will be removed in 8.0. Plea The `render` method can now be a `React.FunctionComponent` (without the `children` prop). Storybook will now render it, rather than calling it as a function. +#### Addon-controls hideNoControlsWarning parameter is deprecated + +The `hideNoControlsWarning` parameter is now unused and deprecated, given that the UI of the Controls addon changed in a way that does not display that message anymore. + +```ts +export const Primary = { + parameters: { + controls: { hideNoControlsWarning: true }, // this parameter is now unnecessary + }, +}; +``` + ## From version 6.5.x to 7.0.0 A number of these changes can be made automatically by the Storybook CLI. To take advantage of these "automigrations", run `npx storybook@latest upgrade --prerelease` or `pnpx dlx storybook@latest upgrade --prerelease`. diff --git a/code/.eslintrc.js b/code/.eslintrc.js index 06b9f06ee759..6acd1c798be1 100644 --- a/code/.eslintrc.js +++ b/code/.eslintrc.js @@ -27,6 +27,7 @@ module.exports = { 'jest/no-standalone-expect': 'off', 'jest/no-done-callback': 'off', 'jest/no-deprecated-functions': 'off', + 'jest/valid-expect': 'off', 'eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }], 'eslint-comments/no-unused-disable': 'error', diff --git a/code/.yarn/patches/@vitest-expect-npm-0.34.5-8031508efe.patch b/code/.yarn/patches/@vitest-expect-npm-1.1.3-2062bf533f.patch similarity index 67% rename from code/.yarn/patches/@vitest-expect-npm-0.34.5-8031508efe.patch rename to code/.yarn/patches/@vitest-expect-npm-1.1.3-2062bf533f.patch index 175c8fbcc343..ea5e834a06df 100644 --- a/code/.yarn/patches/@vitest-expect-npm-0.34.5-8031508efe.patch +++ b/code/.yarn/patches/@vitest-expect-npm-1.1.3-2062bf533f.patch @@ -1,8 +1,8 @@ diff --git a/dist/index.js b/dist/index.js -index 5a61947ad50426d27390b4e82533179323ad3ba1..32bfc45909b645cb31cec2e204c8baa23f21fdd2 100644 +index 974d6b26f626024fc9904908100c9ecaa54f43e1..5be2d35267e7f0525c6588758dbebe72599f88a9 100644 --- a/dist/index.js +++ b/dist/index.js -@@ -6,23 +6,29 @@ import { processError } from '@vitest/utils/error'; +@@ -6,31 +6,37 @@ import { processError } from '@vitest/utils/error'; import { util } from 'chai'; const MATCHERS_OBJECT = Symbol.for("matchers-object"); @@ -11,15 +11,19 @@ index 5a61947ad50426d27390b4e82533179323ad3ba1..32bfc45909b645cb31cec2e204c8baa2 +// Otherwise, vitest will override global jest matchers, and crash. +const JEST_MATCHERS_OBJECT = Symbol.for("$$jest-matchers-object-storybook"); const GLOBAL_EXPECT = Symbol.for("expect-global"); + const ASYMMETRIC_MATCHERS_OBJECT = Symbol.for("asymmetric-matchers-object"); if (!Object.prototype.hasOwnProperty.call(globalThis, MATCHERS_OBJECT)) { const globalState = /* @__PURE__ */ new WeakMap(); - const matchers = /* @__PURE__ */ Object.create(null); + const assymetricMatchers = /* @__PURE__ */ Object.create(null); Object.defineProperty(globalThis, MATCHERS_OBJECT, { get: () => globalState }); ++ Object.defineProperty(globalThis, ASYMMETRIC_MATCHERS_OBJECT, { ++ get: () => assymetricMatchers ++ }); +} -+ +if (!Object.prototype.hasOwnProperty.call(globalThis, JEST_MATCHERS_OBJECT)) { + const matchers = /* @__PURE__ */ Object.create(null); Object.defineProperty(globalThis, JEST_MATCHERS_OBJECT, { @@ -30,8 +34,15 @@ index 5a61947ad50426d27390b4e82533179323ad3ba1..32bfc45909b645cb31cec2e204c8baa2 matchers }) }); +- Object.defineProperty(globalThis, ASYMMETRIC_MATCHERS_OBJECT, { +- get: () => assymetricMatchers +- }); } + function getState(expect) { return globalThis[MATCHERS_OBJECT].get(expect); } ++ + function setState(state, expect) { + const map = globalThis[MATCHERS_OBJECT]; + const current = map.get(expect) || {}; diff --git a/code/addons/a11y/src/a11yRunner.ts b/code/addons/a11y/src/a11yRunner.ts index fb32e0f543a0..ec33803558c3 100644 --- a/code/addons/a11y/src/a11yRunner.ts +++ b/code/addons/a11y/src/a11yRunner.ts @@ -3,7 +3,7 @@ import { addons } from '@storybook/preview-api'; import { EVENTS } from './constants'; import type { A11yParameters } from './params'; -const { document, window: globalWindow } = global; +const { document } = global; const channel = addons.getChannel(); // Holds axe core running state @@ -11,22 +11,21 @@ let active = false; // Holds latest story we requested a run let activeStoryId: string | undefined; +const defaultParameters = { config: {}, options: {} }; + /** * Handle A11yContext events. * Because the event are sent without manual check, we split calls */ -const handleRequest = async (storyId: string) => { - const { manual } = await getParams(storyId); - if (!manual) { - await run(storyId); +const handleRequest = async (storyId: string, input: A11yParameters = defaultParameters) => { + if (!input?.manual) { + await run(storyId, input); } }; -const run = async (storyId: string) => { +const run = async (storyId: string, input: A11yParameters = defaultParameters) => { activeStoryId = storyId; try { - const input = await getParams(storyId); - if (!active) { active = true; channel.emit(EVENTS.RUNNING); @@ -69,17 +68,5 @@ const run = async (storyId: string) => { } }; -/** Returns story parameters or default ones. */ -const getParams = async (storyId: string): Promise => { - const { parameters } = - (await globalWindow.__STORYBOOK_STORY_STORE__.loadStory({ storyId })) || {}; - return ( - parameters.a11y || { - config: {}, - options: {}, - } - ); -}; - channel.on(EVENTS.REQUEST, handleRequest); channel.on(EVENTS.MANUAL, run); diff --git a/code/addons/a11y/src/components/A11YPanel.tsx b/code/addons/a11y/src/components/A11YPanel.tsx index 9552b7951e9d..5f1cf4627abe 100644 --- a/code/addons/a11y/src/components/A11YPanel.tsx +++ b/code/addons/a11y/src/components/A11YPanel.tsx @@ -6,7 +6,12 @@ import { ActionBar, ScrollArea } from '@storybook/components'; import { SyncIcon, CheckIcon } from '@storybook/icons'; import type { AxeResults } from 'axe-core'; -import { useChannel, useParameter, useStorybookState } from '@storybook/manager-api'; +import { + useChannel, + useParameter, + useStorybookApi, + useStorybookState, +} from '@storybook/manager-api'; import { Report } from './Report'; @@ -59,6 +64,7 @@ export const A11YPanel: React.FC = () => { const [error, setError] = React.useState(undefined); const { setResults, results } = useA11yContext(); const { storyId } = useStorybookState(); + const api = useStorybookApi(); React.useEffect(() => { setStatus(manual ? 'manual' : 'initial'); @@ -92,7 +98,7 @@ export const A11YPanel: React.FC = () => { const handleManual = useCallback(() => { setStatus('running'); - emit(EVENTS.MANUAL, storyId); + emit(EVENTS.MANUAL, storyId, api.getParameters(storyId, 'a11y')); }, [storyId]); const manualActionItems = useMemo( diff --git a/code/addons/a11y/src/components/A11yContext.test.tsx b/code/addons/a11y/src/components/A11yContext.test.tsx index 5cfde1886953..2269b8071908 100644 --- a/code/addons/a11y/src/components/A11yContext.test.tsx +++ b/code/addons/a11y/src/components/A11yContext.test.tsx @@ -57,6 +57,7 @@ describe('A11YPanel', () => { }); const getCurrentStoryData = vi.fn(); + const getParameters = vi.fn(); beforeEach(() => { mockedApi.useChannel.mockReset(); mockedApi.useStorybookApi.mockReset(); @@ -65,7 +66,8 @@ describe('A11YPanel', () => { mockedApi.useAddonState.mockImplementation((_, defaultState) => React.useState(defaultState)); mockedApi.useChannel.mockReturnValue(vi.fn()); getCurrentStoryData.mockReset().mockReturnValue({ id: storyId, type: 'story' }); - mockedApi.useStorybookApi.mockReturnValue({ getCurrentStoryData } as any); + getParameters.mockReturnValue({}); + mockedApi.useStorybookApi.mockReturnValue({ getCurrentStoryData, getParameters } as any); }); it('should render children', () => { @@ -94,7 +96,7 @@ describe('A11YPanel', () => { mockedApi.useChannel.mockReturnValue(emit); const { rerender } = render(); rerender(); - expect(emit).toHaveBeenLastCalledWith(EVENTS.REQUEST, storyId); + expect(emit).toHaveBeenLastCalledWith(EVENTS.REQUEST, storyId, {}); }); it('should emit highlight with no values when inactive', () => { diff --git a/code/addons/a11y/src/components/A11yContext.tsx b/code/addons/a11y/src/components/A11yContext.tsx index 8410a646ce65..01e9c68c32ba 100644 --- a/code/addons/a11y/src/components/A11yContext.tsx +++ b/code/addons/a11y/src/components/A11yContext.tsx @@ -70,7 +70,7 @@ export const A11yContextProvider: React.FC { - emit(EVENTS.REQUEST, renderedStoryId); + emit(EVENTS.REQUEST, renderedStoryId, api.getParameters(renderedStoryId, 'a11y')); }; const handleClearHighlights = React.useCallback(() => setHighlighted([]), []); const handleSetTab = React.useCallback((index: number) => { diff --git a/code/addons/a11y/src/components/Report/HighlightToggle.tsx b/code/addons/a11y/src/components/Report/HighlightToggle.tsx index cff5ec7ceb99..90a81b26c065 100644 --- a/code/addons/a11y/src/components/Report/HighlightToggle.tsx +++ b/code/addons/a11y/src/components/Report/HighlightToggle.tsx @@ -31,8 +31,8 @@ function areAllRequiredElementsHighlighted( return highlightedCount === 0 ? CheckBoxStates.UNCHECKED : highlightedCount === elementsToHighlight.length - ? CheckBoxStates.CHECKED - : CheckBoxStates.INDETERMINATE; + ? CheckBoxStates.CHECKED + : CheckBoxStates.INDETERMINATE; } const HighlightToggle: React.FC = ({ toggleId, elementsToHighlight = [] }) => { diff --git a/code/addons/actions/package.json b/code/addons/actions/package.json index 2a2c9d4df7e0..1ebc8b64e43e 100644 --- a/code/addons/actions/package.json +++ b/code/addons/actions/package.json @@ -40,6 +40,7 @@ }, "main": "dist/index.js", "module": "dist/index.mjs", + "react-native": "dist/index.mjs", "types": "dist/index.d.ts", "typesVersions": { "*": { diff --git a/code/addons/backgrounds/src/containers/BackgroundSelector.tsx b/code/addons/backgrounds/src/containers/BackgroundSelector.tsx index 87bf6e84f133..c8d4bcb9019d 100644 --- a/code/addons/backgrounds/src/containers/BackgroundSelector.tsx +++ b/code/addons/backgrounds/src/containers/BackgroundSelector.tsx @@ -37,40 +37,24 @@ const createBackgroundSelectorItem = memoize(1000)( }) ); -const getDisplayedItems = memoize(10)( - ( - backgrounds: Background[], - selectedBackgroundColor: string | null, - change: (arg: { selected: string; name: string }) => void - ) => { - const backgroundSelectorItems = backgrounds.map(({ name, value }) => - createBackgroundSelectorItem( - null, - name, - value, - true, - change, - value === selectedBackgroundColor - ) - ); - - if (selectedBackgroundColor !== 'transparent') { - return [ - createBackgroundSelectorItem( - 'reset', - 'Clear background', - 'transparent', - null, - change, - false - ), - ...backgroundSelectorItems, - ]; - } +const getDisplayedItems = memoize(10)(( + backgrounds: Background[], + selectedBackgroundColor: string | null, + change: (arg: { selected: string; name: string }) => void +) => { + const backgroundSelectorItems = backgrounds.map(({ name, value }) => + createBackgroundSelectorItem(null, name, value, true, change, value === selectedBackgroundColor) + ); - return backgroundSelectorItems; + if (selectedBackgroundColor !== 'transparent') { + return [ + createBackgroundSelectorItem('reset', 'Clear background', 'transparent', null, change, false), + ...backgroundSelectorItems, + ]; } -); + + return backgroundSelectorItems; +}); const DEFAULT_BACKGROUNDS_CONFIG: BackgroundsParameter = { default: null, diff --git a/code/addons/controls/src/ControlsPanel.tsx b/code/addons/controls/src/ControlsPanel.tsx index fc7c2085abd4..09d3bc45e44e 100644 --- a/code/addons/controls/src/ControlsPanel.tsx +++ b/code/addons/controls/src/ControlsPanel.tsx @@ -16,9 +16,6 @@ interface ControlsParameters { sort?: SortType; expanded?: boolean; presetColors?: PresetColor[]; - - /** @deprecated No longer used, will be removed in Storybook 8.0 */ - hideNoControlsWarning?: boolean; } export const ControlsPanel: FC = () => { diff --git a/code/addons/docs/docs/props-tables.md b/code/addons/docs/docs/props-tables.md index 7bc30bcd0dcb..cdc704e2bd65 100644 --- a/code/addons/docs/docs/props-tables.md +++ b/code/addons/docs/docs/props-tables.md @@ -12,7 +12,6 @@ Storybook Docs automatically generates props tables for components in supported - [Controls](#controls) - [Customization](#customization) - [Customizing ArgTypes](#customizing-argtypes) - - [Custom ArgTypes in MDX](#custom-argtypes-in-mdx) - [Reporting a bug](#reporting-a-bug) - [Known limitations](#known-limitations) - [More resources](#more-resources) @@ -60,7 +59,7 @@ Starting in SB 6.0, the `ArgsTable` block has built-in `Controls` (formerly know
-These controls are implemented appear automatically in the props table when your story accepts [Storybook Args](https://storybook.js.org/docs/react/api/csf#args-story-inputs) as its input. This is done slightly differently depending on whether you're using `DocsPage` or `MDX`. +These controls are implemented to appear automatically in the props table when your story accepts [Storybook Args](https://storybook.js.org/docs/react/api/csf#args-story-inputs) as its input. This is done slightly differently depending on whether you're using `DocsPage` or `MDX`. **DocsPage.** In [DocsPage](./docspage.md), simply write your story to consume args and the auto-generated props table will display controls in the right-most column: @@ -202,32 +201,6 @@ Here are the possible customizations for the rest of the prop table: | `table.defaultValue.detail` | A longer version of the default value (if it's a complex value) | | `control` | See [`addon-controls` README](https://storybook.js.org/docs/react/essentials/controls#configuration) | -### Custom ArgTypes in MDX - -To do the equivalent of the above customization [in MDX](./mdx.md), use the following. - -Overriding at the component level: - -```jsx - -``` - -And at the story level: - -```jsx - - {/* story contents */} - -``` - ## Reporting a bug Extracting component properties from source is a tricky problem with thousands of corner cases. We've designed this package and its tests to accurately isolate problems, since the cause could either be in this package or (likely) one of the packages it depends on. diff --git a/code/addons/docs/src/preview.ts b/code/addons/docs/src/preview.ts index 0d1183bd0cf1..991a7811b472 100644 --- a/code/addons/docs/src/preview.ts +++ b/code/addons/docs/src/preview.ts @@ -1,8 +1,30 @@ +import type { PreparedStory } from '@storybook/types'; +import { global } from '@storybook/global'; + +const excludeTags = Object.entries(global.TAGS_OPTIONS ?? {}).reduce( + (acc, entry) => { + const [tag, option] = entry; + if ((option as any).excludeFromDocsStories) { + acc[tag] = true; + } + return acc; + }, + {} as Record +); + export const parameters: any = { docs: { renderer: async () => { const { DocsRenderer } = (await import('./DocsRenderer')) as any; return new DocsRenderer(); }, + stories: { + filter: (story: PreparedStory) => { + const tags = story.tags || []; + return ( + tags.filter((tag) => excludeTags[tag]).length === 0 && !story.parameters.docs?.disable + ); + }, + }, }, }; diff --git a/code/addons/docs/src/typings.d.ts b/code/addons/docs/src/typings.d.ts index cfa3c4639f8e..a3efeb653c83 100644 --- a/code/addons/docs/src/typings.d.ts +++ b/code/addons/docs/src/typings.d.ts @@ -11,3 +11,5 @@ declare module 'sveltedoc-parser' { declare var FEATURES: import('@storybook/types').StorybookConfigRaw['features']; declare var LOGLEVEL: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent' | undefined; + +declare var TAGS_OPTIONS: import('@storybook/types').TagsOptions; diff --git a/code/addons/gfm/package.json b/code/addons/gfm/package.json index 8987354f48a6..e1cf2aaaaff2 100644 --- a/code/addons/gfm/package.json +++ b/code/addons/gfm/package.json @@ -45,7 +45,7 @@ }, "dependencies": { "@storybook/node-logger": "workspace:*", - "remark-gfm": "^3.0.1", + "remark-gfm": "^4.0.0", "ts-dedent": "^2.0.0" }, "devDependencies": { diff --git a/code/addons/gfm/src/index.ts b/code/addons/gfm/src/index.ts index ecfc725fc231..fc1a225b9e1c 100644 --- a/code/addons/gfm/src/index.ts +++ b/code/addons/gfm/src/index.ts @@ -11,7 +11,7 @@ export const mdxLoaderOptions = async (config: any) => { }; deprecate(dedent` - The "@storybook/addon-mdx-gfm" addon is meant as a migration assistant for Storybook 7.0; and will likely be removed in a future version. + The "@storybook/addon-mdx-gfm" addon is meant as a migration assistant for Storybook 8.0; and will likely be removed in a future version. It's recommended you read this document: https://storybook.js.org/docs/react/writing-docs/mdx#lack-of-github-flavored-markdown-gfm diff --git a/code/addons/links/src/react/components/link.test.tsx b/code/addons/links/src/react/components/link.test.tsx index e49ad02ede9f..872c29d5b898 100644 --- a/code/addons/links/src/react/components/link.test.tsx +++ b/code/addons/links/src/react/components/link.test.tsx @@ -17,10 +17,6 @@ vi.mock('@storybook/global', () => ({ search: 'search', }, }, - window: global, - __STORYBOOK_STORY_STORE__: { - fromId: vi.fn(() => ({})), - }, }, })); diff --git a/code/addons/viewport/src/Tool.tsx b/code/addons/viewport/src/Tool.tsx index f365fb65d040..53e7bce1c451 100644 --- a/code/addons/viewport/src/Tool.tsx +++ b/code/addons/viewport/src/Tool.tsx @@ -35,21 +35,24 @@ const responsiveViewport: ViewportItem = { const baseViewports: ViewportItem[] = [responsiveViewport]; -const toLinks = memoize(50)( - (list: ViewportItem[], active: LinkBase, updateGlobals, close): Link[] => { - return list - .filter((i) => i.id !== responsiveViewport.id || active.id !== i.id) - .map((i) => { - return { - ...i, - onClick: () => { - updateGlobals({ viewport: i.id }); - close(); - }, - }; - }); - } -); +const toLinks = memoize(50)(( + list: ViewportItem[], + active: LinkBase, + updateGlobals, + close +): Link[] => { + return list + .filter((i) => i.id !== responsiveViewport.id || active.id !== i.id) + .map((i) => { + return { + ...i, + onClick: () => { + updateGlobals({ viewport: i.id }); + close(); + }, + }; + }); +}); interface LinkBase { id: string; diff --git a/code/builders/builder-manager/src/index.ts b/code/builders/builder-manager/src/index.ts index b7923a64a2ba..d55a8bb2d898 100644 --- a/code/builders/builder-manager/src/index.ts +++ b/code/builders/builder-manager/src/index.ts @@ -138,6 +138,7 @@ const starter: StarterFunction = async function* starterGeneratorFn({ title, logLevel, docsOptions, + tagsOptions, } = await getData(options); yield; @@ -175,6 +176,7 @@ const starter: StarterFunction = async function* starterGeneratorFn({ refs, logLevel, docsOptions, + tagsOptions, options ); @@ -222,6 +224,7 @@ const builder: BuilderFunction = async function* builderGeneratorFn({ startTime, title, logLevel, docsOptions, + tagsOptions, } = await getData(options); yield; @@ -262,6 +265,7 @@ const builder: BuilderFunction = async function* builderGeneratorFn({ startTime, refs, logLevel, docsOptions, + tagsOptions, options ); diff --git a/code/builders/builder-manager/src/utils/data.ts b/code/builders/builder-manager/src/utils/data.ts index eb6754e7635a..2b7fc15e137d 100644 --- a/code/builders/builder-manager/src/utils/data.ts +++ b/code/builders/builder-manager/src/utils/data.ts @@ -14,6 +14,7 @@ export const getData = async (options: Options) => { const logLevel = options.presets.apply('logLevel'); const title = options.presets.apply('title'); const docsOptions = options.presets.apply('docs', {}); + const tagsOptions = options.presets.apply('tags', {}); const template = readTemplate('template.ejs'); const customHead = options.presets.apply('managerHead'); @@ -35,5 +36,6 @@ export const getData = async (options: Options) => { config, logLevel, favicon, + tagsOptions, }; }; diff --git a/code/builders/builder-manager/src/utils/template.ts b/code/builders/builder-manager/src/utils/template.ts index 0d7b67a1dff3..4ccb2d50864a 100644 --- a/code/builders/builder-manager/src/utils/template.ts +++ b/code/builders/builder-manager/src/utils/template.ts @@ -3,7 +3,7 @@ import fs from 'fs-extra'; import { render } from 'ejs'; -import type { DocsOptions, Options, Ref } from '@storybook/types'; +import type { DocsOptions, TagsOptions, Options, Ref } from '@storybook/types'; export const getTemplatePath = async (template: string) => { return join( @@ -34,6 +34,7 @@ export const renderHTML = async ( refs: Promise>, logLevel: Promise, docsOptions: Promise, + tagsOptions: Promise, { versionCheck, previewUrl, configType, ignorePreview }: Options ) => { const titleRef = await title; @@ -52,6 +53,7 @@ export const renderHTML = async ( // These two need to be double stringified because the UI expects a string VERSIONCHECK: JSON.stringify(JSON.stringify(versionCheck), null, 2), PREVIEW_URL: JSON.stringify(previewUrl, null, 2), // global preview URL + TAGS_OPTIONS: JSON.stringify(await tagsOptions, null, 2), }, head: (await customHead) || '', ignorePreview, diff --git a/code/builders/builder-vite/input/iframe.html b/code/builders/builder-vite/input/iframe.html index 867a16a4a223..dd976d6c4ab4 100644 --- a/code/builders/builder-vite/input/iframe.html +++ b/code/builders/builder-vite/input/iframe.html @@ -21,6 +21,7 @@ window.FEATURES = '[FEATURES HERE]'; window.STORIES = '[STORIES HERE]'; window.DOCS_OPTIONS = '[DOCS_OPTIONS HERE]'; + window.TAGS_OPTIONS = '[TAGS_OPTIONS HERE]'; ('OTHER_GLOBLALS HERE'); 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 c30c533dbd8d..39109c148df4 100644 --- a/code/builders/builder-vite/src/codegen-modern-iframe-script.ts +++ b/code/builders/builder-vite/src/codegen-modern-iframe-script.ts @@ -66,10 +66,9 @@ export async function generateModernIframeScriptCode(options: Options, projectRo ${getPreviewAnnotationsFunction} - window.__STORYBOOK_PREVIEW__ = window.__STORYBOOK_PREVIEW__ || new PreviewWeb(); + window.__STORYBOOK_PREVIEW__ = window.__STORYBOOK_PREVIEW__ || new PreviewWeb(importFn, getProjectAnnotations); window.__STORYBOOK_STORY_STORE__ = window.__STORYBOOK_STORY_STORE__ || window.__STORYBOOK_PREVIEW__.storyStore; - window.__STORYBOOK_PREVIEW__.initialize({ importFn, getProjectAnnotations }); ${generateHMRHandler(frameworkName)}; `.trim(); diff --git a/code/builders/builder-vite/src/transform-iframe-html.ts b/code/builders/builder-vite/src/transform-iframe-html.ts index 8c0546125162..a4a482b2f119 100644 --- a/code/builders/builder-vite/src/transform-iframe-html.ts +++ b/code/builders/builder-vite/src/transform-iframe-html.ts @@ -1,5 +1,5 @@ import { normalizeStories } from '@storybook/core-common'; -import type { DocsOptions, Options } from '@storybook/types'; +import type { DocsOptions, TagsOptions, Options } from '@storybook/types'; export type PreviewHtml = string | undefined; @@ -11,6 +11,7 @@ export async function transformIframeHtml(html: string, options: Options) { const bodyHtmlSnippet = await presets.apply('previewBody'); const logLevel = await presets.apply('logLevel', undefined); const docsOptions = await presets.apply('docs'); + const tagsOptions = await presets.apply('tags'); const coreOptions = await presets.apply('core'); const stories = normalizeStories(await options.presets.apply('stories', [], options), { @@ -42,6 +43,7 @@ export async function transformIframeHtml(html: string, options: Options) { .replace(`'[FEATURES HERE]'`, JSON.stringify(features || {})) .replace(`'[STORIES HERE]'`, JSON.stringify(stories || {})) .replace(`'[DOCS_OPTIONS HERE]'`, JSON.stringify(docsOptions || {})) + .replace(`'[TAGS_OPTIONS HERE]'`, JSON.stringify(tagsOptions || {})) .replace('', headHtmlSnippet || '') .replace('', bodyHtmlSnippet || ''); } diff --git a/code/builders/builder-vite/src/vite-config.test.ts b/code/builders/builder-vite/src/vite-config.test.ts index 0dfd0534ee39..8f34e65277e7 100644 --- a/code/builders/builder-vite/src/vite-config.test.ts +++ b/code/builders/builder-vite/src/vite-config.test.ts @@ -25,7 +25,7 @@ const dummyOptions: Options = { builder: {}, }, options: {}, - }[key]), + })[key], } as Presets, presetsList: [], }; diff --git a/code/builders/builder-webpack5/src/presets/custom-webpack-preset.ts b/code/builders/builder-webpack5/src/presets/custom-webpack-preset.ts index 5d921943012c..4068100019e0 100644 --- a/code/builders/builder-webpack5/src/presets/custom-webpack-preset.ts +++ b/code/builders/builder-webpack5/src/presets/custom-webpack-preset.ts @@ -2,14 +2,11 @@ import * as webpackReal from 'webpack'; import { logger } from '@storybook/node-logger'; import type { Options } from '@storybook/types'; import type { Configuration } from 'webpack'; -import deprecate from 'util-deprecate'; -import { dedent } from 'ts-dedent'; import { loadCustomWebpackConfig } from '@storybook/core-webpack'; import { createDefaultWebpackConfig } from '../preview/base-webpack.config'; export async function webpack(config: Configuration, options: Options) { - // @ts-expect-error (Converted from ts-ignore) - const { configDir, configType, presets, webpackConfig } = options; + const { configDir, configType, presets } = options; const coreOptions = await presets.apply('core'); @@ -20,16 +17,6 @@ export async function webpack(config: Configuration, options: Options) { const finalDefaultConfig = await presets.apply('webpackFinal', defaultConfig, options); - // through standalone webpackConfig option - if (webpackConfig) { - return deprecate( - webpackConfig, - dedent` - You've provided a webpack config directly in CallOptions, this is not recommended. Please use presets instead. This feature will be removed in 7.0 - ` - )(finalDefaultConfig); - } - // Check whether user has a custom webpack config file and // return the (extended) base configuration if it's not available. const customConfig = loadCustomWebpackConfig(configDir); diff --git a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts index 97cde4815b6b..ef510ef2378f 100644 --- a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts +++ b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts @@ -82,6 +82,7 @@ export default async ( nonNormalizedStories, modulesCount = 1000, build, + tagsOptions, ] = await Promise.all([ presets.apply('core'), presets.apply('frameworkOptions'), @@ -95,6 +96,7 @@ export default async ( presets.apply('stories', []), options.cache?.get('modulesCount').catch(() => {}), options.presets.apply('build'), + presets.apply('tags', {}), ]); const stories = normalizeStories(nonNormalizedStories, { @@ -129,9 +131,8 @@ export default async ( externals['@storybook/blocks'] = '__STORYBOOK_BLOCKS_EMPTY_MODULE__'; } - const { virtualModules: virtualModuleMapping, entries: dynamicEntries } = await getVirtualModules( - options - ); + const { virtualModules: virtualModuleMapping, entries: dynamicEntries } = + await getVirtualModules(options); return { name: 'preview', @@ -185,6 +186,7 @@ export default async ( importPathMatcher: specifier.importPathMatcher.source, })), DOCS_OPTIONS: docsOptions, + TAGS_OPTIONS: tagsOptions, ...(build?.test?.disableBlocks ? { __STORYBOOK_BLOCKS_EMPTY_MODULE__: {} } : {}), }, headHtmlSnippet, @@ -215,6 +217,7 @@ export default async ( rules: [ { test: /\.stories\.([tj])sx?$|(stories|story)\.mdx$/, + exclude: /node_modules/, enforce: 'post', use: [ { diff --git a/code/builders/builder-webpack5/templates/virtualModuleModernEntry.js.handlebars b/code/builders/builder-webpack5/templates/virtualModuleModernEntry.js.handlebars index 20d420c825f6..1224d3d015df 100644 --- a/code/builders/builder-webpack5/templates/virtualModuleModernEntry.js.handlebars +++ b/code/builders/builder-webpack5/templates/virtualModuleModernEntry.js.handlebars @@ -15,14 +15,12 @@ if (global.CONFIG_TYPE === 'DEVELOPMENT'){ window.__STORYBOOK_SERVER_CHANNEL__ = channel; } -const preview = new PreviewWeb(); +const preview = new PreviewWeb(importFn, getProjectAnnotations); window.__STORYBOOK_PREVIEW__ = preview; window.__STORYBOOK_STORY_STORE__ = preview.storyStore; window.__STORYBOOK_ADDONS_CHANNEL__ = channel; -preview.initialize({ importFn, getProjectAnnotations }); - if (import.meta.webpackHot) { import.meta.webpackHot.accept('./{{storiesFilename}}', () => { // importFn has changed so we need to patch the new one in diff --git a/code/e2e-tests/tags.spec.ts b/code/e2e-tests/tags.spec.ts new file mode 100644 index 000000000000..37fb76fb814c --- /dev/null +++ b/code/e2e-tests/tags.spec.ts @@ -0,0 +1,62 @@ +import { test, expect } from '@playwright/test'; +import { SbPage } from './util'; + +const storybookUrl = process.env.STORYBOOK_URL || 'http://localhost:8001'; + +test.describe('tags', () => { + test.beforeEach(async ({ page }) => { + await page.goto(storybookUrl); + await new SbPage(page).waitUntilLoaded(); + }); + + test('should correctly filter dev-only, docs-only, test-only stories', async ({ page }) => { + const sbPage = new SbPage(page); + + await sbPage.navigateToStory('lib/preview-api/tags', 'docs'); + + // Sidebar should include dev-only and exclude docs-only and test-only + const devOnlyEntry = await page.locator('#lib-preview-api-tags--dev-only').all(); + expect(devOnlyEntry.length).toBe(1); + + const docsOnlyEntry = await page.locator('#lib-preview-api-tags--docs-only').all(); + expect(docsOnlyEntry.length).toBe(0); + + const testOnlyEntry = await page.locator('#lib-preview-api-tags--test-only').all(); + expect(testOnlyEntry.length).toBe(0); + + // Autodocs should include docs-only and exclude dev-only and test-only + const root = sbPage.previewRoot(); + + const devOnlyAnchor = await root.locator('#anchor--lib-preview-api-tags--dev-only').all(); + expect(devOnlyAnchor.length).toBe(0); + + const docsOnlyAnchor = await root.locator('#anchor--lib-preview-api-tags--docs-only').all(); + expect(docsOnlyAnchor.length).toBe(1); + + const testOnlyAnchor = await root.locator('#anchor--lib-preview-api-tags--test-only').all(); + expect(testOnlyAnchor.length).toBe(0); + }); + + test('should correctly filter out test-only autodocs pages', async ({ page }) => { + const sbPage = new SbPage(page); + + await sbPage.selectToolbar('#lib-preview-api'); + + // Sidebar should exclude test-only stories and their docs + const componentEntry = await page.locator('#lib-preview-api-test-only-tag').all(); + expect(componentEntry.length).toBe(0); + + // Even though test-only autodocs not sidebar, it is still in the preview + // Even though the test-only story is filtered out of the stories, it is still the primary story (should it be?) + await sbPage.deepLinkToStory(storybookUrl, 'lib/preview-api/test-only-tag', 'docs'); + await sbPage.waitUntilLoaded(); + const docsButton = await sbPage.previewRoot().locator('button', { hasText: 'Button' }); + await expect(docsButton).toBeVisible(); + + // Even though test-only story not sidebar, it is still in the preview + await sbPage.deepLinkToStory(storybookUrl, 'lib/preview-api/test-only-tag', 'default'); + await sbPage.waitUntilLoaded(); + const storyButton = await sbPage.previewRoot().locator('button', { hasText: 'Button' }); + await expect(storyButton).toBeVisible(); + }); +}); diff --git a/code/e2e-tests/util.ts b/code/e2e-tests/util.ts index 38b0e78cb4c8..ce4cd6b09af0 100644 --- a/code/e2e-tests/util.ts +++ b/code/e2e-tests/util.ts @@ -33,6 +33,9 @@ export class SbPage { const storyLinkId = `${titleId}--${storyId}`; const viewMode = name === 'docs' ? 'docs' : 'story'; await this.page.goto(`${baseURL}/?path=/${viewMode}/${storyLinkId}`); + + await this.page.waitForURL((url) => url.search.includes(`path=/${viewMode}/${storyLinkId}`)); + await this.previewRoot(); } /** diff --git a/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.ts b/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.ts index a4462ea1a3d5..e75354a4bf9c 100644 --- a/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.ts +++ b/code/frameworks/angular/src/client/angular-beta/ComputesTemplateFromComponent.ts @@ -172,7 +172,7 @@ const buildTemplate = ( const firstSelector = selector.split(',')[0]; const templateReplacers: [ string | RegExp, - string | ((substring: string, ...args: any[]) => string) + string | ((substring: string, ...args: any[]) => string), ][] = [ [/(^.*?)(?=[,])/, '$1'], [/(^\..+)/, 'div$1'], diff --git a/code/frameworks/angular/src/client/angular-beta/utils/PropertyExtractor.ts b/code/frameworks/angular/src/client/angular-beta/utils/PropertyExtractor.ts index e6db7384488f..f7bb907831c5 100644 --- a/code/frameworks/angular/src/client/angular-beta/utils/PropertyExtractor.ts +++ b/code/frameworks/angular/src/client/angular-beta/utils/PropertyExtractor.ts @@ -41,7 +41,10 @@ export class PropertyExtractor implements NgModuleMetadata { applicationProviders?: Array>; /* eslint-enable @typescript-eslint/lines-between-class-members */ - constructor(private metadata: NgModuleMetadata, private component?: any) { + constructor( + private metadata: NgModuleMetadata, + private component?: any + ) { this.init(); } diff --git a/code/frameworks/angular/src/client/public-types.ts b/code/frameworks/angular/src/client/public-types.ts index ae7c6773de12..02571ddf1411 100644 --- a/code/frameworks/angular/src/client/public-types.ts +++ b/code/frameworks/angular/src/client/public-types.ts @@ -37,17 +37,6 @@ export type StoryFn = AnnotatedStoryFn = StoryAnnotations>; -/** - * @deprecated Use `StoryFn` instead. - * Use `StoryObj` if you want to migrate to CSF3, which uses objects instead of functions to represent stories. - * You can read more about the CSF3 format here: https://storybook.js.org/blog/component-story-format-3-0/ - * - * Story function that represents a CSFv2 component example. - * - * @see [Named Story exports](https://storybook.js.org/docs/formats/component-story-format/#named-story-exports) - */ -export type Story = StoryFn; - export type Decorator = DecoratorFunction; export type Loader = LoaderFunction; export type StoryContext = GenericStoryContext; diff --git a/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/attribute-selector.component.ts b/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/attribute-selector.component.ts index 58618870358f..af0827439144 100644 --- a/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/attribute-selector.component.ts +++ b/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/attribute-selector.component.ts @@ -11,7 +11,10 @@ export class AttributeSelectorComponent { selectors!: string; - constructor(public el: ElementRef, private resolver: ComponentFactoryResolver) { + constructor( + public el: ElementRef, + private resolver: ComponentFactoryResolver + ) { const factory = this.resolver.resolveComponentFactory(AttributeSelectorComponent); this.selectors = factory.selector; this.generatedTemplate = el.nativeElement.outerHTML; diff --git a/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/class-selector.component.ts b/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/class-selector.component.ts index 3e81b5c5074d..74dd70d7251d 100644 --- a/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/class-selector.component.ts +++ b/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/class-selector.component.ts @@ -11,7 +11,10 @@ export class ClassSelectorComponent { selectors!: string; - constructor(public el: ElementRef, private resolver: ComponentFactoryResolver) { + constructor( + public el: ElementRef, + private resolver: ComponentFactoryResolver + ) { const factory = this.resolver.resolveComponentFactory(ClassSelectorComponent); this.selectors = factory.selector; this.generatedTemplate = el.nativeElement.outerHTML; diff --git a/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/multiple-selector.component.ts b/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/multiple-selector.component.ts index 00a4b98c3bfa..88d7020da50e 100644 --- a/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/multiple-selector.component.ts +++ b/code/frameworks/angular/template/stories/basics/component-with-complex-selectors/multiple-selector.component.ts @@ -11,7 +11,10 @@ export class MultipleSelectorComponent { selectors!: string; - constructor(public el: ElementRef, private resolver: ComponentFactoryResolver) { + constructor( + public el: ElementRef, + private resolver: ComponentFactoryResolver + ) { const factory = this.resolver.resolveComponentFactory(MultipleClassSelectorComponent); this.selectors = factory.selector; this.generatedTemplate = el.nativeElement.outerHTML; @@ -29,7 +32,10 @@ export class MultipleClassSelectorComponent { selectors!: string; - constructor(public el: ElementRef, private resolver: ComponentFactoryResolver) { + constructor( + public el: ElementRef, + private resolver: ComponentFactoryResolver + ) { const factory = this.resolver.resolveComponentFactory(MultipleClassSelectorComponent); this.selectors = factory.selector; this.generatedTemplate = el.nativeElement.outerHTML; diff --git a/code/frameworks/nextjs/README.md b/code/frameworks/nextjs/README.md index cb00d48ff9df..ab6a8fc9cad5 100644 --- a/code/frameworks/nextjs/README.md +++ b/code/frameworks/nextjs/README.md @@ -99,7 +99,7 @@ npx storybook@latest init This framework is designed to work with Storybook 7. If you’re not already using v7, upgrade with this command: ```bash -npx storybook@latest upgrade --prerelease +npx storybook@latest upgrade ``` #### Automatic migration diff --git a/code/frameworks/nextjs/src/babel/plugins/jsx-pragma.ts b/code/frameworks/nextjs/src/babel/plugins/jsx-pragma.ts index 419b178d576f..76e1eec9c66a 100644 --- a/code/frameworks/nextjs/src/babel/plugins/jsx-pragma.ts +++ b/code/frameworks/nextjs/src/babel/plugins/jsx-pragma.ts @@ -78,9 +78,9 @@ export default function jsxPragma({ types: t }: { types: typeof BabelTypes }): P ? // import { $import as _pragma } from '$module' t.importSpecifier(importAs, t.identifier(state.opts.import)) : state.opts.importNamespace - ? t.importNamespaceSpecifier(importAs) - : // import _pragma from '$module' - t.importDefaultSpecifier(importAs), + ? t.importNamespaceSpecifier(importAs) + : // import _pragma from '$module' + t.importDefaultSpecifier(importAs), ], t.stringLiteral(state.opts.module || 'react') ); diff --git a/code/frameworks/nextjs/src/babel/plugins/next-ssg-transform.ts b/code/frameworks/nextjs/src/babel/plugins/next-ssg-transform.ts index 5fa80c7e1250..4c97b90e470d 100644 --- a/code/frameworks/nextjs/src/babel/plugins/next-ssg-transform.ts +++ b/code/frameworks/nextjs/src/babel/plugins/next-ssg-transform.ts @@ -198,10 +198,10 @@ export default function nextTransformSsg({ p.node.type === 'ObjectProperty' ? 'value' : p.node.type === 'RestElement' - ? 'argument' - : (function () { - throw new Error('invariant'); - })() + ? 'argument' + : (function () { + throw new Error('invariant'); + })() ) as NodePath; if (isIdentifierReferenced(local)) { variableState.refs.add(local); @@ -360,10 +360,10 @@ export default function nextTransformSsg({ p.node.type === 'ObjectProperty' ? 'value' : p.node.type === 'RestElement' - ? 'argument' - : (function () { - throw new Error('invariant'); - })() + ? 'argument' + : (function () { + throw new Error('invariant'); + })() ) as NodePath; if (refs.has(local) && !isIdentifierReferenced(local)) { diff --git a/code/frameworks/preact-vite/README.md b/code/frameworks/preact-vite/README.md index e418166a3b54..1e7d742e1674 100644 --- a/code/frameworks/preact-vite/README.md +++ b/code/frameworks/preact-vite/README.md @@ -22,7 +22,7 @@ npx storybook@latest init This framework is designed to work with Storybook 7. If you’re not already using v7, upgrade with this command: ```bash -npx storybook@latest upgrade --prerelease +npx storybook@latest upgrade ``` #### Manual migration diff --git a/code/frameworks/react-webpack5/package.json b/code/frameworks/react-webpack5/package.json index 7da971ae7c83..79e388b9d25a 100644 --- a/code/frameworks/react-webpack5/package.json +++ b/code/frameworks/react-webpack5/package.json @@ -55,7 +55,7 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", - "typescript": "*" + "typescript": ">= 4.2.x" }, "peerDependenciesMeta": { "typescript": { diff --git a/code/frameworks/sveltekit/README.md b/code/frameworks/sveltekit/README.md index c3c01c771897..0243fde57b23 100644 --- a/code/frameworks/sveltekit/README.md +++ b/code/frameworks/sveltekit/README.md @@ -17,7 +17,6 @@ Check out our [Frameworks API](https://storybook.js.org/blog/framework-api/) ann - [Mocking links](#mocking-links) - [Troubleshooting](#troubleshooting) - [Error: `ERR! SyntaxError: Identifier '__esbuild_register_import_meta_url__' has already been declared` when starting Storybook](#error-err-syntaxerror-identifier-__esbuild_register_import_meta_url__-has-already-been-declared-when-starting-storybook) - - [Error: `Cannot read properties of undefined (reading 'disable_scroll_handling')` in preview](#error-cannot-read-properties-of-undefined-reading-disable_scroll_handling-in-preview) - [Acknowledgements](#acknowledgements) ## Supported features @@ -64,7 +63,7 @@ npx storybook@latest init This framework is designed to work with Storybook 7. If you’re not already using v7, upgrade with this command: ```bash -npx storybook@latest upgrade --prerelease +npx storybook@latest upgrade ``` #### Automatic migration diff --git a/code/lib/cli/package.json b/code/lib/cli/package.json index c5745ac3dd7f..216181e7d4bc 100644 --- a/code/lib/cli/package.json +++ b/code/lib/cli/package.json @@ -85,11 +85,10 @@ "jscodeshift": "^0.15.1", "leven": "^3.1.0", "ora": "^5.4.1", - "prettier": "^2.8.0", + "prettier": "^3.1.1", "prompts": "^2.4.0", "read-pkg-up": "^7.0.1", "semver": "^7.3.7", - "simple-update-notifier": "^2.0.0", "strip-json-comments": "^3.0.1", "tempy": "^1.0.1", "tiny-invariant": "^1.3.1", diff --git a/code/lib/cli/src/automigrate/fixes/angular-builders-multiproject.test.ts b/code/lib/cli/src/automigrate/fixes/angular-builders-multiproject.test.ts index 5d5b36f0c52a..50844068d13d 100644 --- a/code/lib/cli/src/automigrate/fixes/angular-builders-multiproject.test.ts +++ b/code/lib/cli/src/automigrate/fixes/angular-builders-multiproject.test.ts @@ -104,7 +104,7 @@ describe('is not Nx project', () => { () => ({ hasStorybookBuilder: true, - } as any) + }) as any ); }); @@ -129,7 +129,7 @@ describe('is not Nx project', () => { project1: { root: 'project1', architect: {} }, }, rootProject: 'project1', - } as any) + }) as any ); }); @@ -155,7 +155,7 @@ describe('is not Nx project', () => { project2: { root: 'project2', architect: {} }, }, rootProject: null, - } as any) + }) as any ); }); diff --git a/code/lib/cli/src/automigrate/fixes/angular-builders.test.ts b/code/lib/cli/src/automigrate/fixes/angular-builders.test.ts index 7c9e63bd4a5f..c527b6434b3a 100644 --- a/code/lib/cli/src/automigrate/fixes/angular-builders.test.ts +++ b/code/lib/cli/src/automigrate/fixes/angular-builders.test.ts @@ -107,7 +107,7 @@ describe('is not Nx project', () => { () => ({ hasStorybookBuilder: true, - } as any) + }) as any ); }); @@ -133,7 +133,7 @@ describe('is not Nx project', () => { project2: { root: 'project2', architect: {} }, }, rootProject: null, - } as any) + }) as any ); }); @@ -158,7 +158,7 @@ describe('is not Nx project', () => { project1: { root: 'project1', architect: {} }, }, rootProject: 'project1', - } as any) + }) as any ); }); diff --git a/code/lib/cli/src/automigrate/fixes/builder-vite.ts b/code/lib/cli/src/automigrate/fixes/builder-vite.ts index b1d31444e914..6a46f16f15bd 100644 --- a/code/lib/cli/src/automigrate/fixes/builder-vite.ts +++ b/code/lib/cli/src/automigrate/fixes/builder-vite.ts @@ -6,6 +6,7 @@ import { writeConfig } from '@storybook/csf-tools'; import type { Fix } from '../types'; import type { PackageJson } from '../../js-package-manager'; import { updateMainConfig } from '../helpers/mainConfigFile'; +import { getStorybookVersionSpecifier } from '../../helpers'; const logger = console; @@ -68,8 +69,11 @@ export const builderVite: Fix = { logger.info(`✅ Adding '@storybook/builder-vite' as dev dependency`); if (!dryRun) { + const versionToInstall = getStorybookVersionSpecifier( + await packageManager.retrievePackageJson() + ); await packageManager.addDependencies({ installAsDevDependencies: true }, [ - '@storybook/builder-vite', + `@storybook/builder-vite@${versionToInstall}`, ]); } diff --git a/code/lib/cli/src/automigrate/fixes/new-frameworks.ts b/code/lib/cli/src/automigrate/fixes/new-frameworks.ts index 657372d70ea8..f335d654050b 100644 --- a/code/lib/cli/src/automigrate/fixes/new-frameworks.ts +++ b/code/lib/cli/src/automigrate/fixes/new-frameworks.ts @@ -218,10 +218,10 @@ export const newFrameworks: Fix = { ❌ Your project should be upgraded to use the framework package ${chalk.bold( newFrameworkPackage )}, but we detected that you are using Vite ${chalk.bold( - viteVersion - )}, which is unsupported in ${chalk.bold( - 'Storybook 7.0' - )}. Please upgrade Vite to ${chalk.bold('3.0.0 or higher')} and rerun this migration. + viteVersion + )}, which is unsupported in ${chalk.bold( + 'Storybook 7.0' + )}. Please upgrade Vite to ${chalk.bold('3.0.0 or higher')} and rerun this migration. `); } @@ -351,8 +351,8 @@ export const newFrameworks: Fix = { This migration is set to update your project to use the ${chalk.magenta( '@storybook/react-vite' )} framework, but Storybook provides a framework package specifically for Next.js projects: ${chalk.magenta( - '@storybook/nextjs' - )}. + '@storybook/nextjs' + )}. This package provides a better, out of the box experience for Next.js users, however it is only compatible with the Webpack 5 builder, so we can't automigrate for you, as you are using the Vite builder. If you switch this project to use Webpack 5 and rerun this migration, we can update your project. @@ -379,8 +379,8 @@ export const newFrameworks: Fix = { This migration is set to update your project to use the ${chalk.magenta( '@storybook/svelte-webpack5' )} framework, but Storybook provides a framework package specifically for SvelteKit projects: ${chalk.magenta( - '@storybook/sveltekit' - )}. + '@storybook/sveltekit' + )}. This package provides a better experience for SvelteKit users, however it is only compatible with the Vite builder, so we can't automigrate for you, as you are using the Webpack builder. diff --git a/code/lib/cli/src/automigrate/fixes/nodejs-requirement.ts b/code/lib/cli/src/automigrate/fixes/nodejs-requirement.ts index 4c3006b7a367..cf82bceb9bac 100644 --- a/code/lib/cli/src/automigrate/fixes/nodejs-requirement.ts +++ b/code/lib/cli/src/automigrate/fixes/nodejs-requirement.ts @@ -32,8 +32,8 @@ export const nodeJsRequirement: Fix = { We've detected that you're using Node ${chalk.bold( nodeVersion )} but Storybook 7 only supports Node ${chalk.bold( - 'v16.0.0' - )} and higher. You will either need to upgrade your Node version or keep using an older version of Storybook. + 'v16.0.0' + )} and higher. You will either need to upgrade your Node version or keep using an older version of Storybook. Please see the migration guide for more information: ${chalk.yellow( diff --git a/code/lib/cli/src/automigrate/fixes/sb-scripts.ts b/code/lib/cli/src/automigrate/fixes/sb-scripts.ts index 24ac7854ef4d..5192ccd4e187 100644 --- a/code/lib/cli/src/automigrate/fixes/sb-scripts.ts +++ b/code/lib/cli/src/automigrate/fixes/sb-scripts.ts @@ -20,49 +20,52 @@ const logger = console; * which could actually be a custom script even though the name matches the legacy binary name */ export const getStorybookScripts = (allScripts: NonNullable) => { - return Object.keys(allScripts).reduce((acc, key) => { - const currentScript = allScripts[key]; - if (currentScript == null) { - return acc; - } - let isStorybookScript = false; - const allWordsFromScript = currentScript.split(' '); - const newScript = allWordsFromScript - .map((currentWord, index) => { - const previousWord = allWordsFromScript[index - 1]; - - // full word check, rather than regex which could be faulty - const isSbBinary = - currentWord === 'build-storybook' || - currentWord === 'start-storybook' || - currentWord === 'sb'; - - // in case people have scripts like `yarn start-storybook` - const isPrependedByPkgManager = - previousWord && - ['npx', 'run', 'yarn', 'pnpx', 'pnpm dlx'].some((cmd) => previousWord.includes(cmd)); - - if (isSbBinary && !isPrependedByPkgManager) { - isStorybookScript = true; - return currentWord - .replace('sb', 'storybook') - .replace('start-storybook', 'storybook dev') - .replace('build-storybook', 'storybook build'); - } - - return currentWord; - }) - .join(' '); - - if (isStorybookScript) { - acc[key] = { - before: currentScript, - after: newScript, - }; - } + return Object.keys(allScripts).reduce( + (acc, key) => { + const currentScript = allScripts[key]; + if (currentScript == null) { + return acc; + } + let isStorybookScript = false; + const allWordsFromScript = currentScript.split(' '); + const newScript = allWordsFromScript + .map((currentWord, index) => { + const previousWord = allWordsFromScript[index - 1]; + + // full word check, rather than regex which could be faulty + const isSbBinary = + currentWord === 'build-storybook' || + currentWord === 'start-storybook' || + currentWord === 'sb'; + + // in case people have scripts like `yarn start-storybook` + const isPrependedByPkgManager = + previousWord && + ['npx', 'run', 'yarn', 'pnpx', 'pnpm dlx'].some((cmd) => previousWord.includes(cmd)); + + if (isSbBinary && !isPrependedByPkgManager) { + isStorybookScript = true; + return currentWord + .replace('sb', 'storybook') + .replace('start-storybook', 'storybook dev') + .replace('build-storybook', 'storybook build'); + } + + return currentWord; + }) + .join(' '); + + if (isStorybookScript) { + acc[key] = { + before: currentScript, + after: newScript, + }; + } - return acc; - }, {} as Record); + return acc; + }, + {} as Record + ); }; /** @@ -111,10 +114,10 @@ export const sbScripts: Fix = { return dedent` We've detected you are using ${sbFormatted} with scripts from previous versions of Storybook. Starting in Storybook 7, the ${chalk.yellow('start-storybook')} and ${chalk.yellow( - 'build-storybook' - )} binaries have changed to ${chalk.magenta('storybook dev')} and ${chalk.magenta( - 'storybook build' - )} respectively. + 'build-storybook' + )} binaries have changed to ${chalk.magenta('storybook dev')} and ${chalk.magenta( + 'storybook build' + )} respectively. In order to work with ${sbFormatted}, your storybook scripts have to be adjusted to use the binary. We can adjust them for you: ${newScriptsMessage.join('\n\n')} @@ -129,10 +132,13 @@ export const sbScripts: Fix = { logger.info(`✅ Updating scripts in package.json`); logger.log(); if (!dryRun) { - const newScripts = Object.keys(storybookScripts).reduce((acc, scriptKey) => { - acc[scriptKey] = storybookScripts[scriptKey].after; - return acc; - }, {} as Record); + const newScripts = Object.keys(storybookScripts).reduce( + (acc, scriptKey) => { + acc[scriptKey] = storybookScripts[scriptKey].after; + return acc; + }, + {} as Record + ); logger.log(); diff --git a/code/lib/cli/src/automigrate/fixes/wrap-require-utils.ts b/code/lib/cli/src/automigrate/fixes/wrap-require-utils.ts index 99bf4b8d550c..ef06f654a651 100644 --- a/code/lib/cli/src/automigrate/fixes/wrap-require-utils.ts +++ b/code/lib/cli/src/automigrate/fixes/wrap-require-utils.ts @@ -47,8 +47,8 @@ export function getRequireWrapperName(config: ConfigFile) { doesVariableOrFunctionDeclarationExist(node, 'wrapForPnp') ? ['wrapForPnp'] : doesVariableOrFunctionDeclarationExist(node, defaultRequireWrapperName) - ? [defaultRequireWrapperName] - : [] + ? [defaultRequireWrapperName] + : [] ); if (declarationName.length) { diff --git a/code/lib/cli/src/automigrate/helpers/checkWebpack5Builder.ts b/code/lib/cli/src/automigrate/helpers/checkWebpack5Builder.ts index 860b68fb0718..a350b23fd82b 100644 --- a/code/lib/cli/src/automigrate/helpers/checkWebpack5Builder.ts +++ b/code/lib/cli/src/automigrate/helpers/checkWebpack5Builder.ts @@ -20,9 +20,11 @@ export const checkWebpack5Builder = async ({ To upgrade to the latest stable release, run this from your project directory: - ${chalk.cyan('npx storybook upgrade')} + ${chalk.cyan('npx storybook@latest upgrade')} - Add the ${chalk.cyan('--prerelease')} flag to get the latest prerelease. + To upgrade to the latest pre-release, run this from your project directory: + + ${chalk.cyan('npx storybook@next upgrade')} `.trim() ); return null; diff --git a/code/lib/cli/src/automigrate/helpers/getMigrationSummary.ts b/code/lib/cli/src/automigrate/helpers/getMigrationSummary.ts index e23c29dcea4d..4c51a53224eb 100644 --- a/code/lib/cli/src/automigrate/helpers/getMigrationSummary.ts +++ b/code/lib/cli/src/automigrate/helpers/getMigrationSummary.ts @@ -92,8 +92,8 @@ export function getMigrationSummary({ const title = hasNoFixes ? 'No migrations were applicable to your project' : hasFailures - ? 'Migration check ran with failures' - : 'Migration check ran successfully'; + ? 'Migration check ran with failures' + : 'Migration check ran successfully'; return boxen(messages.filter(Boolean).join(segmentDivider), { borderStyle: 'round', diff --git a/code/lib/cli/src/automigrate/helpers/new-frameworks-utils.ts b/code/lib/cli/src/automigrate/helpers/new-frameworks-utils.ts index 7f62392c6839..2232993d9aaf 100644 --- a/code/lib/cli/src/automigrate/helpers/new-frameworks-utils.ts +++ b/code/lib/cli/src/automigrate/helpers/new-frameworks-utils.ts @@ -107,12 +107,10 @@ export const detectBuilderInfo = async ({ // if builder is still not detected, rely on package dependencies if (!builderOrFrameworkName) { - const storybookBuilderViteVersion = await packageManager.getPackageVersion( - '@storybook/builder-vite' - ); - const storybookBuilderVite2Version = await packageManager.getPackageVersion( - 'storybook-builder-vite' - ); + const storybookBuilderViteVersion = + await packageManager.getPackageVersion('@storybook/builder-vite'); + const storybookBuilderVite2Version = + await packageManager.getPackageVersion('storybook-builder-vite'); const storybookBuilderWebpack5Version = await packageManager.getPackageVersion( '@storybook/builder-webpack5' ); diff --git a/code/lib/cli/src/detect.ts b/code/lib/cli/src/detect.ts index 0e6878c43c80..1138b84e016b 100644 --- a/code/lib/cli/src/detect.ts +++ b/code/lib/cli/src/detect.ts @@ -185,9 +185,8 @@ export async function detectLanguage(packageManager: JsPackageManager) { '@typescript-eslint/parser' ); - const eslintPluginStorybookVersion = await packageManager.getPackageVersion( - 'eslint-plugin-storybook' - ); + const eslintPluginStorybookVersion = + await packageManager.getPackageVersion('eslint-plugin-storybook'); if (isTypescriptDirectDependency && typescriptVersion) { if ( diff --git a/code/lib/cli/src/doctor/getIncompatibleAddons.ts b/code/lib/cli/src/doctor/getIncompatibleAddons.ts index 465ad661a892..0a0fc5dc2d19 100644 --- a/code/lib/cli/src/doctor/getIncompatibleAddons.ts +++ b/code/lib/cli/src/doctor/getIncompatibleAddons.ts @@ -52,7 +52,7 @@ export const getIncompatibleAddons = async ( ({ name: addon, version: await packageManager.getPackageVersion(addon), - } as { name: keyof typeof incompatibleList; version: string }) + }) as { name: keyof typeof incompatibleList; version: string } ) ); diff --git a/code/lib/cli/src/generate.ts b/code/lib/cli/src/generate.ts index a91fd0517687..c514dfb35f29 100644 --- a/code/lib/cli/src/generate.ts +++ b/code/lib/cli/src/generate.ts @@ -11,6 +11,7 @@ import invariant from 'tiny-invariant'; import type { CommandOptions } from './generators/types'; import { initiate } from './initiate'; import { add } from './add'; +import { remove } from './remove'; import { migrate } from './migrate'; import { upgrade, type UpgradeOptions } from './upgrade'; import { sandbox } from './sandbox'; @@ -66,16 +67,22 @@ command('add ') .option('-s --skip-postinstall', 'Skip package specific postinstall config modifications') .action((addonName: string, options: any) => add(addonName, options)); +command('remove ') + .description('Remove an addon from your Storybook') + .option( + '--package-manager ', + 'Force package manager for installing dependencies' + ) + .action((addonName: string, options: any) => remove(addonName, options)); + command('upgrade') - .description('Upgrade your Storybook packages to the latest') + .description(`Upgrade your Storybook packages to v${versions.storybook}`) .option( '--package-manager ', 'Force package manager for installing dependencies' ) .option('-y --yes', 'Skip prompting the user') .option('-n --dry-run', 'Only check for upgrades, do not install') - .option('-t --tag ', 'Upgrade to a certain npm dist-tag (e.g. next, prerelease)') - .option('-p --prerelease', 'Upgrade to the pre-release packages') .option('-s --skip-check', 'Skip postinstall version and automigration checks') .option('-c, --config-dir ', 'Directory where to load Storybook configurations from') .action(async (options: UpgradeOptions) => upgrade(options).catch(() => process.exit(1))); @@ -134,10 +141,9 @@ command('sandbox [filterValue]') .alias('repro') // for backwards compatibility .description('Create a sandbox from a set of possible templates') .option('-o --output ', 'Define an output directory') - .option('-b --branch ', 'Define the branch to download from', 'next') .option('--no-init', 'Whether to download a template without an initialized Storybook', false) .action((filterValue, options) => - sandbox({ filterValue, ...options }).catch((e) => { + sandbox({ filterValue, ...options }, pkg).catch((e) => { logger.error(e); process.exit(1); }) diff --git a/code/lib/cli/src/generators/NEXTJS/index.ts b/code/lib/cli/src/generators/NEXTJS/index.ts index 40f91b3e8c66..fe6672b669a7 100644 --- a/code/lib/cli/src/generators/NEXTJS/index.ts +++ b/code/lib/cli/src/generators/NEXTJS/index.ts @@ -15,7 +15,7 @@ const generator: Generator = async (packageManager, npmOptions, options) => { 'react', { staticDir, - extraAddons: ['@storybook/addon-onboarding'], + extraAddons: ['@storybook/addon-onboarding@^1.0.0'], webpackCompiler: ({ builder }) => undefined, }, 'nextjs' diff --git a/code/lib/cli/src/generators/REACT/index.ts b/code/lib/cli/src/generators/REACT/index.ts index e19a55ce1adc..967ed7b5531b 100644 --- a/code/lib/cli/src/generators/REACT/index.ts +++ b/code/lib/cli/src/generators/REACT/index.ts @@ -11,7 +11,7 @@ const generator: Generator = async (packageManager, npmOptions, options) => { await baseGenerator(packageManager, npmOptions, options, 'react', { extraPackages, webpackCompiler: ({ builder }) => (builder === CoreBuilder.Webpack5 ? 'swc' : undefined), - extraAddons: ['@storybook/addon-onboarding'], + extraAddons: ['@storybook/addon-onboarding@^1.0.0'], }); }; diff --git a/code/lib/cli/src/generators/REACT_NATIVE/index.ts b/code/lib/cli/src/generators/REACT_NATIVE/index.ts index e3b8dcfa50c0..d87cd187e55b 100644 --- a/code/lib/cli/src/generators/REACT_NATIVE/index.ts +++ b/code/lib/cli/src/generators/REACT_NATIVE/index.ts @@ -30,14 +30,14 @@ const generator = async ( '@storybook/addon-controls@^6.5.16', ]; - const resolvedPackages = await packageManager.getVersionedPackages(packagesToResolve); + const versionedPackages = await packageManager.getVersionedPackages(packagesToResolve); const babelDependencies = await getBabelDependencies(packageManager, packageJson); const packages: string[] = []; packages.push(...babelDependencies); packages.push(...packagesWithFixedVersion); - packages.push(...resolvedPackages); + packages.push(...versionedPackages); if (missingReactDom && reactVersion) { packages.push(`react-dom@${reactVersion}`); } diff --git a/code/lib/cli/src/generators/WEBPACK_REACT/index.ts b/code/lib/cli/src/generators/WEBPACK_REACT/index.ts index d894be17b34e..a6f0293248f4 100644 --- a/code/lib/cli/src/generators/WEBPACK_REACT/index.ts +++ b/code/lib/cli/src/generators/WEBPACK_REACT/index.ts @@ -4,7 +4,7 @@ import type { Generator } from '../types'; const generator: Generator = async (packageManager, npmOptions, options) => { await baseGenerator(packageManager, npmOptions, options, 'react', { - extraAddons: ['@storybook/addon-onboarding'], + extraAddons: ['@storybook/addon-onboarding@^1.0.0'], webpackCompiler: ({ builder }) => (builder === CoreBuilder.Webpack5 ? 'swc' : undefined), }); }; diff --git a/code/lib/cli/src/generators/baseGenerator.ts b/code/lib/cli/src/generators/baseGenerator.ts index 2cf1b8c75687..51b5397008fe 100644 --- a/code/lib/cli/src/generators/baseGenerator.ts +++ b/code/lib/cli/src/generators/baseGenerator.ts @@ -298,16 +298,13 @@ export async function baseGenerator( const versionedPackages = await packageManager.getVersionedPackages(packages as string[]); versionedPackagesSpinner.succeed(); - const depsToInstall = [...versionedPackages]; - try { if (process.env.CI !== 'true') { - const { hasEslint, isStorybookPluginInstalled, eslintConfigFile } = await extractEslintInfo( - packageManager - ); + const { hasEslint, isStorybookPluginInstalled, eslintConfigFile } = + await extractEslintInfo(packageManager); if (hasEslint && !isStorybookPluginInstalled) { - depsToInstall.push('eslint-plugin-storybook'); + versionedPackages.push('eslint-plugin-storybook'); await configureEslintPlugin(eslintConfigFile ?? undefined, packageManager); } } @@ -315,12 +312,13 @@ export async function baseGenerator( // any failure regarding configuring the eslint plugin should not fail the whole generator } - if (depsToInstall.length > 0) { + if (versionedPackages.length > 0) { const addDependenciesSpinner = ora({ indent: 2, text: 'Installing Storybook dependencies', }).start(); - await packageManager.addDependencies({ ...npmOptions, packageJson }, depsToInstall); + + await packageManager.addDependencies({ ...npmOptions, packageJson }, versionedPackages); addDependenciesSpinner.succeed(); } diff --git a/code/lib/cli/src/generators/configure.ts b/code/lib/cli/src/generators/configure.ts index 38cd265df83c..2d0a3e1f5fc8 100644 --- a/code/lib/cli/src/generators/configure.ts +++ b/code/lib/cli/src/generators/configure.ts @@ -105,8 +105,8 @@ export async function configureMain({ try { const prettier = (await import('prettier')).default; - mainJsContents = prettier.format(dedent(mainJsContents), { - ...prettier.resolveConfig.sync(process.cwd()), + mainJsContents = await prettier.format(dedent(mainJsContents), { + ...(await prettier.resolveConfig(mainPath)), filepath: mainPath, }); } catch { @@ -168,8 +168,8 @@ export async function configurePreview(options: ConfigurePreviewOptions) { try { const prettier = (await import('prettier')).default; - preview = prettier.format(preview, { - ...prettier.resolveConfig.sync(process.cwd()), + preview = await prettier.format(preview, { + ...(await prettier.resolveConfig(previewPath)), filepath: previewPath, }); } catch { diff --git a/code/lib/cli/src/helpers.ts b/code/lib/cli/src/helpers.ts index 33c7b6c6ccd9..ed54bd765986 100644 --- a/code/lib/cli/src/helpers.ts +++ b/code/lib/cli/src/helpers.ts @@ -295,7 +295,11 @@ export async function adjustTemplate(templatePath: string, templateData: Record< // Given a package.json, finds any official storybook package within it // and if it exists, returns the version of that package from the specified package.json export function getStorybookVersionSpecifier(packageJson: PackageJsonWithDepsAndDevDeps) { - const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies }; + const allDeps = { + ...packageJson.dependencies, + ...packageJson.devDependencies, + ...packageJson.optionalDependencies, + }; const storybookPackage = Object.keys(allDeps).find((name: string) => { return storybookMonorepoPackages[name as keyof typeof storybookMonorepoPackages]; }); diff --git a/code/lib/cli/src/initiate.ts b/code/lib/cli/src/initiate.ts index c0dbf5a3c7b3..175bbcb7e189 100644 --- a/code/lib/cli/src/initiate.ts +++ b/code/lib/cli/src/initiate.ts @@ -8,6 +8,7 @@ import { NxProjectDetectedError } from '@storybook/core-events/server-errors'; import dedent from 'ts-dedent'; import boxen from 'boxen'; +import { lt, prerelease } from 'semver'; import type { Builder } from './project_types'; import { installableProjectTypes, ProjectType } from './project_types'; import { detect, isStorybookInstantiated, detectLanguage, detectPnp } from './detect'; @@ -223,7 +224,7 @@ const projectTypeInquirer = async ( process.exit(0); }; -async function doInitiate( +export async function doInitiate( options: CommandOptions, pkg: PackageJson ): Promise< @@ -241,15 +242,33 @@ async function doInitiate( force: pkgMgr, }); - const welcomeMessage = 'storybook init - the simplest way to add a Storybook to your project.'; - logger.log(chalk.inverse(`\n ${welcomeMessage} \n`)); + const latestVersion = await packageManager.latestVersion('@storybook/cli'); + const currentVersion = versions['@storybook/cli']; + const isPrerelease = prerelease(currentVersion); + const isOutdated = lt(currentVersion, latestVersion); + const borderColor = isOutdated ? '#FC521F' : '#F1618C'; + + const messages = { + welcome: `Adding Storybook version ${chalk.bold(currentVersion)} to your project..`, + notLatest: chalk.red(dedent` + This version is behind the latest release, which is: ${chalk.bold(latestVersion)}! + You likely ran the init command through npx, which can use a locally cached version, to get the latest please run: + ${chalk.bold('npx storybook@latest init')} + + You may want to CTRL+C to stop, and run with the latest version instead. + `), + prelease: chalk.yellow('This is a pre-release version.'), + }; - // Update notify code. - const { default: updateNotifier } = await import('simple-update-notifier'); - await updateNotifier({ - pkg: pkg as any, - updateCheckInterval: 1000 * 60 * 60, // every hour (we could increase this later on.) - }); + logger.log( + boxen( + [messages.welcome] + .concat(isOutdated && !isPrerelease ? [messages.notLatest] : []) + .concat(isPrerelease ? [messages.prelease] : []) + .join('\n'), + { borderStyle: 'round', padding: 1, borderColor } + ) + ); // Check if the current directory is empty. if (options.force !== true && currentDirectoryIsEmpty(packageManager.type)) { diff --git a/code/lib/cli/src/js-package-manager/JsPackageManager.ts b/code/lib/cli/src/js-package-manager/JsPackageManager.ts index 5a3e82be3c43..9555d08a90b2 100644 --- a/code/lib/cli/src/js-package-manager/JsPackageManager.ts +++ b/code/lib/cli/src/js-package-manager/JsPackageManager.ts @@ -134,6 +134,8 @@ export abstract class JsPackageManager { done = commandLog('Installing dependencies'); + logger.log(); + try { await this.runInstall(); done(); @@ -330,13 +332,29 @@ export abstract class JsPackageManager { /** * Return an array of strings matching following format: `@` * + * For packages in the storybook monorepo, when the latest version is equal to the version of the current CLI + * the version is not added to the string. + * + * When a package is in the monorepo, and the version is not equal to the CLI version, the version is taken from the versions.ts file and added to the string. + * * @param packages */ public getVersionedPackages(packages: string[]): Promise { return Promise.all( packages.map(async (pkg) => { const [packageName, packageVersion] = getPackageDetails(pkg); - return `${packageName}@${await this.getVersion(packageName, packageVersion)}`; + const latestInRange = await this.latestVersion(packageName, packageVersion); + + const k = packageName as keyof typeof storybookPackagesVersions; + const currentVersion = storybookPackagesVersions[k]; + + if (currentVersion === latestInRange) { + return `${packageName}`; + } + if (currentVersion) { + return `${packageName}@${currentVersion}`; + } + return `${packageName}@^${latestInRange}`; }) ); } diff --git a/code/lib/cli/src/js-package-manager/Yarn1Proxy.ts b/code/lib/cli/src/js-package-manager/Yarn1Proxy.ts index 2dd3251ead61..c57de55e1d20 100644 --- a/code/lib/cli/src/js-package-manager/Yarn1Proxy.ts +++ b/code/lib/cli/src/js-package-manager/Yarn1Proxy.ts @@ -192,7 +192,7 @@ export class Yarn1Proxy extends JsPackageManager { const existingVersions: Record = {}; const duplicatedDependencies: Record = {}; - const recurse = (tree: typeof trees[0]) => { + const recurse = (tree: (typeof trees)[0]) => { const { children } = tree; const { name, value } = parsePackageData(tree.name); if (!name || !name.includes('storybook')) return; diff --git a/code/lib/cli/src/remove.ts b/code/lib/cli/src/remove.ts new file mode 100644 index 000000000000..47c556eb578f --- /dev/null +++ b/code/lib/cli/src/remove.ts @@ -0,0 +1,46 @@ +import { getStorybookInfo } from '@storybook/core-common'; +import { readConfig, writeConfig } from '@storybook/csf-tools'; +import dedent from 'ts-dedent'; + +import { JsPackageManagerFactory, type PackageManagerName } from './js-package-manager'; + +const logger = console; + +/** + * Remove the given addon package and remove it from main.js + * + * Usage: + * - sb remove @storybook/addon-links + */ +export async function remove(addon: string, options: { packageManager: PackageManagerName }) { + const { packageManager: pkgMgr } = options; + + const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr }); + const packageJson = await packageManager.retrievePackageJson(); + const { mainConfig, configDir } = getStorybookInfo(packageJson); + + if (typeof configDir === 'undefined') { + throw new Error(dedent` + Unable to find storybook config directory + `); + } + + if (!mainConfig) { + logger.error('Unable to find storybook main.js config'); + return; + } + const main = await readConfig(mainConfig); + + // remove from package.json + logger.log(`Uninstalling ${addon}`); + await packageManager.removeDependencies({ packageJson }, [addon]); + + // add to main.js + logger.log(`Removing '${addon}' from main.js addons field.`); + try { + main.removeEntryFromArray(['addons'], addon); + await writeConfig(main); + } catch (err) { + logger.warn(`Failed to remove '${addon}' from main.js addons field.`); + } +} diff --git a/code/lib/cli/src/sandbox.ts b/code/lib/cli/src/sandbox.ts index 397fda11830f..8bc306d39049 100644 --- a/code/lib/cli/src/sandbox.ts +++ b/code/lib/cli/src/sandbox.ts @@ -7,8 +7,13 @@ import { downloadTemplate } from 'giget'; import { existsSync, readdir } from 'fs-extra'; import invariant from 'tiny-invariant'; +import { lt, prerelease } from 'semver'; import type { Template, TemplateKey } from './sandbox-templates'; import { allTemplates as TEMPLATES } from './sandbox-templates'; +import type { PackageJson, PackageManagerName } from './js-package-manager'; +import { JsPackageManagerFactory } from './js-package-manager'; +import versions from './versions'; +import { doInitiate } from './initiate'; const logger = console; @@ -17,20 +22,60 @@ interface SandboxOptions { output?: string; branch?: string; init?: boolean; + packageManager: PackageManagerName; } type Choice = keyof typeof TEMPLATES; const toChoices = (c: Choice): prompts.Choice => ({ title: TEMPLATES[c].name, value: c }); -export const sandbox = async ({ - output: outputDirectory, - filterValue, - branch, - init, -}: SandboxOptions) => { +export const sandbox = async ( + { output: outputDirectory, filterValue, init, ...options }: SandboxOptions, + pkg: PackageJson +) => { // Either get a direct match when users pass a template id, or filter through all templates let selectedConfig: Template | undefined = TEMPLATES[filterValue as TemplateKey]; - let selectedTemplate: Choice | null = selectedConfig ? (filterValue as TemplateKey) : null; + let templateId: Choice | null = selectedConfig ? (filterValue as TemplateKey) : null; + + const { packageManager: pkgMgr } = options; + + const packageManager = JsPackageManagerFactory.getPackageManager({ + force: pkgMgr, + }); + const latestVersion = await packageManager.latestVersion('@storybook/cli'); + const nextVersion = await packageManager.latestVersion('@storybook/cli@next'); + const currentVersion = versions['@storybook/cli']; + const isPrerelease = prerelease(currentVersion); + const isOutdated = lt(currentVersion, isPrerelease ? nextVersion : latestVersion); + const borderColor = isOutdated ? '#FC521F' : '#F1618C'; + + const downloadType = !isOutdated && init ? 'after-storybook' : 'before-storybook'; + const branch = isPrerelease ? 'next' : 'main'; + + const messages = { + welcome: `Creating a Storybook ${chalk.bold(currentVersion)} sandbox..`, + notLatest: chalk.red(dedent` + This version is behind the latest release, which is: ${chalk.bold(latestVersion)}! + You likely ran the init command through npx, which can use a locally cached version, to get the latest please run: + ${chalk.bold('npx storybook@latest sandbox')} + + You may want to CTRL+C to stop, and run with the latest version instead. + `), + longInitTime: chalk.yellow( + 'The creation of the sandbox will take longer, because we will need to run init.' + ), + prerelease: chalk.yellow('This is a pre-release version.'), + }; + + logger.log( + boxen( + [messages.welcome] + .concat(isOutdated && !isPrerelease ? [messages.notLatest] : []) + .concat(init && (isOutdated || isPrerelease) ? [messages.longInitTime] : []) + .concat(isPrerelease ? [messages.prerelease] : []) + .join('\n'), + { borderStyle: 'round', padding: 1, borderColor } + ) + ); if (!selectedConfig) { const filterRegex = new RegExp(`^${filterValue || ''}`, 'i'); @@ -79,7 +124,7 @@ export const sandbox = async ({ } if (choices.length === 1) { - [selectedTemplate] = choices; + [templateId] = choices; } else { logger.info( boxen( @@ -97,16 +142,16 @@ export const sandbox = async ({ ) ); - selectedTemplate = await promptSelectedTemplate(choices); + templateId = await promptSelectedTemplate(choices); } - const hasSelectedTemplate = !!(selectedTemplate ?? null); + const hasSelectedTemplate = !!(templateId ?? null); if (!hasSelectedTemplate) { logger.error('Somehow we got no templates. Please rerun this command!'); return; } - selectedConfig = selectedTemplate ? TEMPLATES[selectedTemplate] : undefined; + selectedConfig = templateId ? TEMPLATES[templateId] : undefined; if (!selectedConfig) { throw new Error('🚨 Sandbox: please specify a valid template type'); @@ -114,7 +159,7 @@ export const sandbox = async ({ } let selectedDirectory = outputDirectory; - const outputDirectoryName = outputDirectory || selectedTemplate; + const outputDirectoryName = outputDirectory || templateId; if (selectedDirectory && existsSync(`${selectedDirectory}`)) { logger.info(`⚠️ ${selectedDirectory} already exists! Overwriting...`); } @@ -149,22 +194,35 @@ export const sandbox = async ({ logger.info(`🏃 Adding ${selectedConfig.name} into ${templateDestination}`); - logger.log('📦 Downloading sandbox template...'); + logger.log(`📦 Downloading sandbox template (${chalk.bold(downloadType)})...`); try { - const templateType = init ? 'after-storybook' : 'before-storybook'; // Download the sandbox based on subfolder "after-storybook" and selected branch - const gitPath = `github:storybookjs/sandboxes/${selectedTemplate}/${templateType}#${branch}`; + const gitPath = `github:storybookjs/sandboxes/${templateId}/${downloadType}#${branch}`; await downloadTemplate(gitPath, { force: true, dir: templateDestination, }); // throw an error if templateDestination is an empty directory using fs-extra if ((await readdir(templateDestination)).length === 0) { - throw new Error( - dedent`Template downloaded from ${chalk.blue(gitPath)} is empty. - Are you use it exists? Or did you want to set ${chalk.yellow( - selectedTemplate - )} to inDevelopment first?` + const selected = chalk.yellow(templateId); + throw new Error(dedent` + Template downloaded from ${chalk.blue(gitPath)} is empty. + Are you use it exists? Or did you want to set ${selected} to inDevelopment first? + `); + } + + // when user wanted an sandbox that has been initiated, but force-downloaded the before-storybook directory + // then we need to initiate the sandbox + // this is to ensure we DO get the latest version of the template (output of the generator), but we initialize using the version of storybook that the CLI is. + // we warned the user about the fact they are running an old version of storybook + // we warned the user the sandbox step would take longer + if ((isOutdated || isPrerelease) && init) { + // we run doInitiate, instead of initiate, to avoid sending this init event to telemetry, because it's not a real world project + await doInitiate( + { + ...options, + }, + pkg ); } } catch (err) { @@ -173,7 +231,10 @@ export const sandbox = async ({ } const initMessage = init - ? chalk.yellow(`yarn install\nyarn storybook`) + ? chalk.yellow(dedent` + yarn install + yarn storybook + `) : `Recreate your setup, then ${chalk.yellow(`npx storybook@latest init`)}`; logger.info( diff --git a/code/lib/cli/src/upgrade.test.ts b/code/lib/cli/src/upgrade.test.ts index 6fcea5f84596..be025028c495 100644 --- a/code/lib/cli/src/upgrade.test.ts +++ b/code/lib/cli/src/upgrade.test.ts @@ -1,5 +1,25 @@ -import { describe, it, expect } from 'vitest'; -import { addExtraFlags, addNxPackagesToReject, getStorybookVersion } from './upgrade'; +import { describe, it, expect, vi } from 'vitest'; +import { getStorybookCoreVersion } from '@storybook/telemetry'; +import { + UpgradeStorybookToLowerVersionError, + UpgradeStorybookToSameVersionError, +} from '@storybook/core-events/server-errors'; +import { doUpgrade, getStorybookVersion } from './upgrade'; +import type versions from './versions'; + +vi.mock('@storybook/telemetry'); +vi.mock('./versions', async (importOriginal) => { + const originalVersions = ((await importOriginal()) as { default: typeof versions }).default; + return { + default: Object.keys(originalVersions).reduce( + (acc, key) => { + acc[key] = '8.0.0'; + return acc; + }, + {} as Record + ), + }; +}); describe.each([ ['│ │ │ ├── @babel/code-frame@7.10.3 deduped', null], @@ -22,68 +42,15 @@ describe.each([ }); }); -describe('extra flags', () => { - const extraFlags = { - 'react-scripts@<5': ['--foo'], - }; - const devDependencies = {}; - it('package matches constraints', () => { - expect( - addExtraFlags(extraFlags, [], { dependencies: { 'react-scripts': '4' }, devDependencies }) - ).toEqual(['--foo']); - }); - it('package prerelease matches constraints', () => { - expect( - addExtraFlags(extraFlags, [], { - dependencies: { 'react-scripts': '4.0.0-alpha.0' }, - devDependencies, - }) - ).toEqual(['--foo']); - }); - it('package not matches constraints', () => { - expect( - addExtraFlags(extraFlags, [], { - dependencies: { 'react-scripts': '5.0.0-alpha.0' }, - devDependencies, - }) - ).toEqual([]); - }); - it('no package not matches constraints', () => { - expect( - addExtraFlags(extraFlags, [], { - dependencies: {}, - devDependencies, - }) - ).toEqual([]); - }); -}); +describe('Upgrade errors', () => { + it('should throw an error when upgrading to a lower version number', async () => { + vi.mocked(getStorybookCoreVersion).mockResolvedValue('8.1.0'); -describe('addNxPackagesToReject', () => { - it('reject exists and is in regex pattern', () => { - const flags = ['--reject', '/preset-create-react-app/', '--some-flag', 'hello']; - expect(addNxPackagesToReject(flags)).toMatchObject([ - '--reject', - '"/(preset-create-react-app|@nrwl/storybook|@nx/storybook)/"', - '--some-flag', - 'hello', - ]); + await expect(doUpgrade({} as any)).rejects.toThrowError(UpgradeStorybookToLowerVersionError); }); - it('reject exists and is in unknown pattern', () => { - const flags = ['--some-flag', 'hello', '--reject', '@storybook/preset-create-react-app']; - expect(addNxPackagesToReject(flags)).toMatchObject([ - '--some-flag', - 'hello', - '--reject', - '@storybook/preset-create-react-app,@nrwl/storybook,@nx/storybook', - ]); - }); - it('reject does not exist', () => { - const flags = ['--some-flag', 'hello']; - expect(addNxPackagesToReject(flags)).toMatchObject([ - '--some-flag', - 'hello', - '--reject', - '@nrwl/storybook,@nx/storybook', - ]); + it('should throw an error when upgrading to the same version number', async () => { + vi.mocked(getStorybookCoreVersion).mockResolvedValue('8.0.0'); + + await expect(doUpgrade({} as any)).rejects.toThrowError(UpgradeStorybookToSameVersionError); }); }); diff --git a/code/lib/cli/src/upgrade.ts b/code/lib/cli/src/upgrade.ts index f42cfd13f5cc..8738f1bfea6a 100644 --- a/code/lib/cli/src/upgrade.ts +++ b/code/lib/cli/src/upgrade.ts @@ -1,18 +1,22 @@ import { sync as spawnSync } from 'cross-spawn'; import { telemetry, getStorybookCoreVersion } from '@storybook/telemetry'; -import semver from 'semver'; +import semver, { eq, lt, prerelease } from 'semver'; import { logger } from '@storybook/node-logger'; import { withTelemetry } from '@storybook/core-server'; import { - ConflictingVersionTagsError, - UpgradeStorybookPackagesError, + UpgradeStorybookToLowerVersionError, + UpgradeStorybookToSameVersionError, } from '@storybook/core-events/server-errors'; -import type { PackageJsonWithMaybeDeps, PackageManagerName } from './js-package-manager'; -import { getPackageDetails, JsPackageManagerFactory } from './js-package-manager'; +import chalk from 'chalk'; +import dedent from 'ts-dedent'; +import boxen from 'boxen'; +import type { PackageManagerName } from './js-package-manager'; +import { JsPackageManagerFactory } from './js-package-manager'; import { coerceSemver, commandLog } from './helpers'; import { automigrate } from './automigrate'; import { isCorePackage } from './utils'; +import versions from './versions'; type Package = { package: string; @@ -87,57 +91,7 @@ export const checkVersionConsistency = () => { }); }; -type ExtraFlags = Record; -const EXTRA_FLAGS: ExtraFlags = { - 'react-scripts@<5': ['--reject', '/preset-create-react-app/'], -}; - -export const addExtraFlags = ( - extraFlags: ExtraFlags, - flags: string[], - { dependencies, devDependencies }: PackageJsonWithMaybeDeps -) => { - return Object.entries(extraFlags).reduce( - (acc, entry) => { - const [pattern, extra] = entry; - const [pkg, specifier] = getPackageDetails(pattern); - const pkgVersion = dependencies?.[pkg] || devDependencies?.[pkg]; - - if (pkgVersion && specifier && semver.satisfies(coerceSemver(pkgVersion), specifier)) { - return [...acc, ...extra]; - } - - return acc; - }, - [...flags] - ); -}; - -export const addNxPackagesToReject = (flags: string[]) => { - const newFlags = [...flags]; - const index = flags.indexOf('--reject'); - if (index > -1) { - // Try to understand if it's in the format of a regex pattern - if (newFlags[index + 1].endsWith('/') && newFlags[index + 1].startsWith('/')) { - // Remove last and first slash so that I can add the parentheses - newFlags[index + 1] = newFlags[index + 1].substring(1, newFlags[index + 1].length - 1); - newFlags[index + 1] = `"/(${newFlags[index + 1]}|@nrwl/storybook|@nx/storybook)/"`; - } else { - // Adding the two packages as comma-separated values - // If the existing rejects are in regex format, they will be ignored. - // Maybe we need to find a more robust way to treat rejects? - newFlags[index + 1] = `${newFlags[index + 1]},@nrwl/storybook,@nx/storybook`; - } - } else { - newFlags.push('--reject'); - newFlags.push('@nrwl/storybook,@nx/storybook'); - } - return newFlags; -}; - export interface UpgradeOptions { - tag: string; - prerelease: boolean; skipCheck: boolean; packageManager: PackageManagerName; dryRun: boolean; @@ -147,8 +101,6 @@ export interface UpgradeOptions { } export const doUpgrade = async ({ - tag, - prerelease, skipCheck, packageManager: pkgMgr, dryRun, @@ -158,66 +110,88 @@ export const doUpgrade = async ({ }: UpgradeOptions) => { const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr }); + const currentVersion = versions['@storybook/cli']; const beforeVersion = await getStorybookCoreVersion(); - commandLog(`Checking for latest versions of '@storybook/*' packages\n`); - - if (tag && prerelease) { - throw new ConflictingVersionTagsError(); + if (lt(currentVersion, beforeVersion)) { + throw new UpgradeStorybookToLowerVersionError({ beforeVersion, currentVersion }); } - - let target = 'latest'; - if (prerelease) { - // '@next' is storybook's convention for the latest prerelease tag. - // This used to be 'greatest', but that was not reliable and could pick canaries, etc. - // and random releases of other packages with storybook in their name. - target = '@next'; - } else if (tag) { - target = `@${tag}`; + if (eq(currentVersion, beforeVersion)) { + throw new UpgradeStorybookToSameVersionError({ beforeVersion }); } - let flags = []; - if (!dryRun) flags.push('--upgrade'); - flags.push('--target'); - flags.push(target); - flags = addExtraFlags(EXTRA_FLAGS, flags, await packageManager.retrievePackageJson()); - flags = addNxPackagesToReject(flags); - - const command = 'npx'; - const checkArgs = ['npm-check-updates@latest', '/storybook/', ...flags]; - const check = spawnSync(command, checkArgs, { - stdio: 'pipe', - shell: true, - }); + const latestVersion = await packageManager.latestVersion('@storybook/cli'); + const isOutdated = lt(currentVersion, latestVersion); + const isPrerelease = prerelease(currentVersion) !== null; + + const borderColor = isOutdated ? '#FC521F' : '#F1618C'; + + const messages = { + welcome: `Upgrading Storybook from version ${chalk.bold(beforeVersion)} to version ${chalk.bold( + currentVersion + )}..`, + notLatest: chalk.red(dedent` + This version is behind the latest release, which is: ${chalk.bold(latestVersion)}! + You likely ran the upgrade command through npx, which can use a locally cached version, to upgrade to the latest version please run: + ${chalk.bold('npx storybook@latest upgrade')} + + You may want to CTRL+C to stop, and run with the latest version instead. + `), + prelease: chalk.yellow('This is a pre-release version.'), + }; - if (check.stderr && check.stderr.toString().includes('npm ERR')) { - throw new UpgradeStorybookPackagesError({ - command, - args: checkArgs, - errorMessage: check.stderr.toString(), - }); - } + logger.plain( + boxen( + [messages.welcome] + .concat(isOutdated && !isPrerelease ? [messages.notLatest] : []) + .concat(isPrerelease ? [messages.prelease] : []) + .join('\n'), + { borderStyle: 'round', padding: 1, borderColor } + ) + ); - logger.info(check.stdout.toString()); + const packageJson = await packageManager.retrievePackageJson(); + + const toUpgradedDependencies = (deps: Record) => { + const monorepoDependencies = Object.keys(deps || {}).filter((dependency) => { + // don't upgrade @storybook/preset-create-react-app if react-scripts is < v5 + if (dependency === '@storybook/preset-create-react-app') { + const reactScriptsVersion = + packageJson.dependencies['react-scripts'] ?? packageJson.devDependencies['react-scripts']; + if (reactScriptsVersion && lt(coerceSemver(reactScriptsVersion), '5.0.0')) { + return false; + } + } - const checkSbArgs = ['npm-check-updates@latest', 'sb', ...flags]; - const checkSb = spawnSync(command, checkSbArgs, { - stdio: 'pipe', - shell: true, - }); - logger.info(checkSb.stdout.toString()); - logger.info(checkSb.stderr.toString()); - - if (checkSb.stderr && checkSb.stderr.toString().includes('npm ERR')) { - throw new UpgradeStorybookPackagesError({ - command, - args: checkSbArgs, - errorMessage: checkSb.stderr.toString(), - }); - } + // only upgrade packages that are in the monorepo + return dependency in versions; + }) as Array; + return monorepoDependencies.map( + (dependency) => + // add ^ modifier to the version if this is the latest and stable version + // example output: @storybook/react@^8.0.0 + `${dependency}@${!isOutdated || isPrerelease ? '^' : ''}${versions[dependency]}` + ); + }; + + const upgradedDependencies = toUpgradedDependencies(packageJson.dependencies); + const upgradedDevDependencies = toUpgradedDependencies(packageJson.devDependencies); if (!dryRun) { - commandLog(`Installing upgrades`); + commandLog(`Updating dependencies in ${chalk.cyan('package.json')}..`); + logger.plain(''); + if (upgradedDependencies.length > 0) { + await packageManager.addDependencies( + { installAsDevDependencies: false, skipInstall: true, packageJson }, + upgradedDependencies + ); + } + if (upgradedDevDependencies.length > 0) { + await packageManager.addDependencies( + { installAsDevDependencies: true, skipInstall: true, packageJson }, + upgradedDevDependencies + ); + } await packageManager.installDependencies(); } @@ -234,8 +208,6 @@ export const doUpgrade = async ({ automigrationPreCheckFailure: preCheckFailure || null, }; telemetry('upgrade', { - prerelease, - tag, beforeVersion, afterVersion, ...automigrationTelemetry, diff --git a/code/lib/codemod/package.json b/code/lib/codemod/package.json index dfa22e0aae7a..6c4ac7e72bbb 100644 --- a/code/lib/codemod/package.json +++ b/code/lib/codemod/package.json @@ -64,22 +64,22 @@ "globby": "^11.0.2", "jscodeshift": "^0.15.1", "lodash": "^4.17.21", - "prettier": "^2.8.0", + "prettier": "^3.1.1", "recast": "^0.23.1", "tiny-invariant": "^1.3.1" }, "devDependencies": { "@types/jscodeshift": "^0.11.10", "ansi-regex": "^5.0.1", - "mdast-util-mdx-jsx": "^2.1.2", - "mdast-util-mdxjs-esm": "^1.3.1", - "remark": "^14.0.2", - "remark-mdx": "^2.3.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.1", + "remark": "^15.0.1", + "remark-mdx": "^3.0.0", "ts-dedent": "^2.2.0", "typescript": "^5.3.2", - "unist-util-is": "^5.2.0", - "unist-util-select": "^4.0.3", - "unist-util-visit": "^4.1.2", + "unist-util-is": "^6.0.0", + "unist-util-select": "^5.1.0", + "unist-util-visit": "^5.0.0", "util": "^0.12.4", "vfile": "^5.3.7" }, diff --git a/code/lib/codemod/src/transforms/__tests__/csf-2-to-3.test.ts b/code/lib/codemod/src/transforms/__tests__/csf-2-to-3.test.ts index 65f77bb4ec2e..a91946f345bc 100644 --- a/code/lib/codemod/src/transforms/__tests__/csf-2-to-3.test.ts +++ b/code/lib/codemod/src/transforms/__tests__/csf-2-to-3.test.ts @@ -9,21 +9,25 @@ expect.addSnapshotSerializer({ test: () => true, }); -const jsTransform = (source: string) => - _transform({ source, path: 'Component.stories.js' }, {} as API, {}).trim(); -const tsTransform = (source: string) => - _transform({ source, path: 'Component.stories.ts' }, {} as API, { parser: 'tsx' }).trim(); +const jsTransform = async (source: string) => + (await _transform({ source, path: 'Component.stories.jsx' }, {} as API, {})).trim(); +const tsTransform = async (source: string) => + ( + await _transform({ source, path: 'Component.stories.tsx' }, {} as API, { + parser: 'tsx', + }) + ).trim(); describe('csf-2-to-3', () => { describe('javascript', () => { - it('should replace non-simple function exports with objects', () => { - expect( + it('should replace non-simple function exports with objects', async () => { + await expect( jsTransform(dedent` export default { title: 'Cat' }; export const A = () => ; export const B = (args) => - ), + import { Button } from './button'; + export default {}; - name: 'Primary', - }; + export const Primary = { + render: () => ( + + ), - `); + name: 'Primary', + }; + `); }); -it('story child is CSF3', () => { +it('story child is CSF3', async () => { const input = dedent` import { Story } from '@storybook/addon-docs'; import { Button } from './button'; @@ -547,7 +529,7 @@ it('story child is CSF3', () => { } args={{label: 'Hello' }} /> `; - jscodeshift({ source: input, path: 'Foobar.stories.mdx' }); + await jscodeshift({ source: input, path: 'Foobar.stories.mdx' }); const [, csf] = fs.writeFileSync.mock.calls[0]; @@ -563,11 +545,10 @@ it('story child is CSF3', () => { label: 'Hello', }, }; - `); }); -it('story child is arrow function', () => { +it('story child is arrow function', async () => { const input = dedent` import { Canvas, Meta, Story } from '@storybook/addon-docs'; import { Button } from './button'; @@ -577,23 +558,22 @@ it('story child is arrow function', () => { `; - jscodeshift({ source: input, path: 'Foobar.stories.mdx' }); + await jscodeshift({ source: input, path: 'Foobar.stories.mdx' }); const [, csf] = fs.writeFileSync.mock.calls[0]; expect(csf).toMatchInlineSnapshot(` - import { Button } from './button'; - export default {}; - - export const Primary = { - render: (args) =>