diff --git a/.circleci/config.yml b/.circleci/config.yml index 6db12bf41959..dac362b5530b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -614,22 +614,22 @@ workflows: requires: - build - create-sandboxes: - parallelism: 36 + parallelism: 35 requires: - build # - smoke-test-sandboxes: # disabled for now # requires: # - create-sandboxes - build-sandboxes: - parallelism: 36 + parallelism: 35 requires: - create-sandboxes - chromatic-sandboxes: - parallelism: 33 + parallelism: 32 requires: - build-sandboxes - e2e-production: - parallelism: 31 + parallelism: 30 requires: - build-sandboxes - e2e-dev: @@ -637,7 +637,7 @@ workflows: requires: - create-sandboxes - test-runner-production: - parallelism: 31 + parallelism: 30 requires: - build-sandboxes # TODO: reenable once we find out the source of flakyness diff --git a/CHANGELOG.md b/CHANGELOG.md index 28d18cedd4ca..ca7fcf921544 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## 7.6.3 + +- Next.js: Fix next/font/local usage in babel mode - [#25045](https://github.com/storybookjs/storybook/pull/25045), thanks [@valentinpalkovic](https://github.com/valentinpalkovic)! + +## 7.6.2 + +- CLI: Improve dependency metadata detection in storybook doctor - [#25037](https://github.com/storybookjs/storybook/pull/25037), thanks [@yannbf](https://github.com/yannbf)! +- React-Docgen: Make error-handling more gentle - [#25055](https://github.com/storybookjs/storybook/pull/25055), thanks [@valentinpalkovic](https://github.com/valentinpalkovic)! + +## 7.6.1 + +- Next.js: Fix AppRouterProvider usage - [#25032](https://github.com/storybookjs/storybook/pull/25032), thanks [@valentinpalkovic](https://github.com/valentinpalkovic)! +- SvelteKit: Fix HMR not working - [#25031](https://github.com/storybookjs/storybook/pull/25031), thanks [@JReinhold](https://github.com/JReinhold)! +- Test: Downgrade @testing-library/user-event to 14.3.0 - [#25004](https://github.com/storybookjs/storybook/pull/25004), thanks [@kasperpeulen](https://github.com/kasperpeulen)! +- Webpack: Fix exclude regex in react-docgen-loader - [#25030](https://github.com/storybookjs/storybook/pull/25030), thanks [@valentinpalkovic](https://github.com/valentinpalkovic)! + ## 7.6.0 Storybook 7.6 is here with increased performance and much more! diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 5ef41fa93384..cae24fdc0f31 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,27 @@ +## 8.0.0-alpha.1 + +- Angular: Drop v14.x support - [#25101](https://github.com/storybookjs/storybook/pull/25101), thanks [@valentinpalkovic](https://github.com/valentinpalkovic)! +- Angular: Fix CSF Plugin - [#25098](https://github.com/storybookjs/storybook/pull/25098), thanks [@valentinpalkovic](https://github.com/valentinpalkovic)! +- Build: Fix Angular sandbox - [#23896](https://github.com/storybookjs/storybook/pull/23896), thanks [@Marklb](https://github.com/Marklb)! +- CLI: Improve dependency metadata detection in storybook doctor - [#25037](https://github.com/storybookjs/storybook/pull/25037), thanks [@yannbf](https://github.com/yannbf)! +- CLI: Point the update-notice to the changelog in the suggested version - [#19911](https://github.com/storybookjs/storybook/pull/19911), thanks [@cprecioso](https://github.com/cprecioso)! +- CLI: Typescript strict mode - [#22254](https://github.com/storybookjs/storybook/pull/22254), thanks [@0916dhkim](https://github.com/0916dhkim)! +- CSF: Autotitle fix multiple dots and handle stories.js - [#21840](https://github.com/storybookjs/storybook/pull/21840), thanks [@agriffis](https://github.com/agriffis)! +- Next.js: Add next/font/local declarations support - [#24983](https://github.com/storybookjs/storybook/pull/24983), thanks [@MauricioRobayo](https://github.com/MauricioRobayo)! +- Next.js: Drop Next.js < v13.5 support - [#25104](https://github.com/storybookjs/storybook/pull/25104), thanks [@valentinpalkovic](https://github.com/valentinpalkovic)! +- Next.js: Fix AppRouterProvider usage - [#25032](https://github.com/storybookjs/storybook/pull/25032), thanks [@valentinpalkovic](https://github.com/valentinpalkovic)! +- Next.js: Fix next/font/local usage in babel mode - [#25045](https://github.com/storybookjs/storybook/pull/25045), thanks [@valentinpalkovic](https://github.com/valentinpalkovic)! +- Next.js: Update validateData function for next/font compatibility - [#25061](https://github.com/storybookjs/storybook/pull/25061), thanks [@kkirby](https://github.com/kkirby)! +- NextJS: Add experimental RSC support - [#25091](https://github.com/storybookjs/storybook/pull/25091), thanks [@shilman](https://github.com/shilman)! +- React-Docgen: Make error-handling more gentle - [#25055](https://github.com/storybookjs/storybook/pull/25055), thanks [@valentinpalkovic](https://github.com/valentinpalkovic)! +- React: Change `StoryFnReactReturnType` to `JSX.Element` - [#23204](https://github.com/storybookjs/storybook/pull/23204), thanks [@chakAs3](https://github.com/chakAs3)! +- React: Set `react-docgen` to default TS docgen - [#24165](https://github.com/storybookjs/storybook/pull/24165), thanks [@shilman](https://github.com/shilman)! +- SvelteKit: Fix HMR not working - [#25031](https://github.com/storybookjs/storybook/pull/25031), thanks [@JReinhold](https://github.com/JReinhold)! +- TypeScript: Migrate `@storybook/docs-tools` to strict TS - [#22567](https://github.com/storybookjs/storybook/pull/22567), thanks [@efrenaragon96](https://github.com/efrenaragon96)! +- UI: Add stricter types to the language property of the SyntaxHighlighter - [#22790](https://github.com/storybookjs/storybook/pull/22790), thanks [@valentinpalkovic](https://github.com/valentinpalkovic)! +- Viewport: Fix viewport dts files - [#25107](https://github.com/storybookjs/storybook/pull/25107), thanks [@kasperpeulen](https://github.com/kasperpeulen)! +- Webpack: Fix exclude regex in react-docgen-loader - [#25030](https://github.com/storybookjs/storybook/pull/25030), thanks [@valentinpalkovic](https://github.com/valentinpalkovic)! + ## 8.0.0-alpha.0 - Addon Viewport: Expose types for user parameter validation - [#24896](https://github.com/storybookjs/storybook/pull/24896), thanks [@piratetaco](https://github.com/piratetaco)! diff --git a/MIGRATION.md b/MIGRATION.md index d116de268ec3..ae382420f682 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -3,11 +3,16 @@ - [From version 7.x to 8.0.0](#from-version-7x-to-800) - [Implicit actions can not be used during rendering (for example in the play function)](#implicit-actions-can-not-be-used-during-rendering-for-example-in-the-play-function) - [Core changes](#core-changes) + - [Autotitle breaking fixes](#autotitle-breaking-fixes) - [React v18 in the manager UI (including addons)](#react-v18-in-the-manager-ui-including-addons) - - [Storyshots has been removed](#storyshots-has-been-removed) + - [Storyshots has been removed](#storyshots-has-been-removed) - [UI layout state has changed shape](#ui-layout-state-has-changed-shape) - [New UI and props for Button and IconButton components](#new-ui-and-props-for-button-and-iconbutton-components) - [Icons is deprecated](#icons-is-deprecated) + - [React-docgen component analysis by default](#react-docgen-component-analysis-by-default) + - [Framework-specific changes](#framework-specific-changes) + - [Angular: Drop support for Angular \< 15](#angular-drop-support-for-angular--15) + - [Next.js: Drop support for version \< 13.5](#nextjs-drop-support-for-version--135) - [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) @@ -320,6 +325,7 @@ - [Packages renaming](#packages-renaming) - [Deprecated embedded addons](#deprecated-embedded-addons) + ## From version 7.x to 8.0.0 ### Implicit actions can not be used during rendering (for example in the play function) @@ -373,6 +379,18 @@ To summarize: ### Core changes +#### Autotitle breaking fixes + +In Storybook 7, the file name `path/to/foo.bar.stories.js` would result in the [autotitle](https://storybook.js.org/docs/react/configure/overview#configure-story-loading) `path/to/foo`. In 8.0, this has been changed to generate `path/to/foo.bar`. We consider this a bugfix but it is also a breaking change if you depended on the old behavior. To get the old titles, you can manually specify the desired title in the default export of your story file. For example: + +```js +export default { + title: 'path/to/foo', +} +``` + +Alternatively, if you need to achieve a different behavior for a large number of files, you can provide a [custom indexer](https://storybook.js.org/docs/7.0/vue/configure/sidebar-and-urls#processing-custom-titles) to generate the titles dynamically. + #### React v18 in the manager UI (including addons) Storybook 7 used React 16 in the manager. In Storybook 8 this is upgraded to react v18. @@ -380,7 +398,7 @@ Addons that inject UI into panels, tools, etc. are possibly affected by this. Addon authors are advised to upgrade to react v18. -##### Storyshots has been removed +#### Storyshots has been removed Storyshots was an addon for storybook which allowed users to turn their stories into automated snapshot-tests. @@ -429,6 +447,32 @@ The `IconButton` doesn't have any deprecated props but it now uses the new `Butt In Storybook 8.0 we are introducing a new icon library available with `@storybook/icons`. We are deprecating the `Icons` component in `@storybook/components` and recommend that addon creators and Storybook maintainers use the new `@storybook/icons` component instead. +#### React-docgen component analysis by default + +In Storybook 7, we used `react-docgen-typescript` to analyze React component props and auto-generate controls. In Storybook 8, we have moved to `react-docgen` as the new default. `react-docgen` is dramatically more efficient, shaving seconds off of dev startup times. However, it only analyzes basic TypeScript constructs. + +We feel `react-docgen` is the right tradeoff for most React projects. However, if you need the full fidelity of `react-docgen-typescript`, you can opt-in using the following setting in `.storybook/main.js`: + +```js +export default { + typescript: { + reactDocgen: 'react-docgen-typescript', + } +} +``` + +For more information see: https://storybook.js.org/docs/react/api/main-config-typescript#reactdocgen + +### Framework-specific changes + +#### Angular: Drop support for Angular \< 15 + +Starting in 8.0, we drop support for Angular < 15 + +#### Next.js: Drop support for version \< 13.5 + +Starting in 8.0, we drop support for Next.js < 13.5. + ## From version 7.5.0 to 7.6.0 #### CommonJS with Vite is deprecated diff --git a/README.md b/README.md index 69fe2fb71cfa..820c663970f0 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ Storybook comes with a lot of [addons](https://storybook.js.org/docs/react/confi ### Community -For additional help, join us in the [Storybook Discord](https://discord.gg/storybook). +For additional help, share your issue in [the repo's GitHub Discussions](https://github.com/storybookjs/storybook/discussions/new?category=help). ## Projects diff --git a/code/addons/controls/README.md b/code/addons/controls/README.md index 0622e23a302c..1d3a8ae3797b 100644 --- a/code/addons/controls/README.md +++ b/code/addons/controls/README.md @@ -44,7 +44,7 @@ Addon-knobs is one of Storybook's most popular addons with over 1M weekly downlo Therefore, rather than deprecating addon-knobs immediately, we will continue to release knobs with the Storybook core distribution until 7.0. This will give us time to improve Controls based on user feedback, and also give knobs users ample time to migrate. -If you are somehow tied to knobs or prefer the knobs interface, we are happy to take on maintainers for the knobs project. If this interests you, hop on our [Discord](https://discord.gg/storybook). +If you are somehow tied to knobs or prefer the knobs interface, we are happy to take on maintainers for the knobs project. If this interests you, please get in touch with us in the [`#contributing`](https://discord.com/channels/486522875931656193/839297503446695956) Discord channel. ### How do I migrate from addon-knobs? diff --git a/code/addons/docs/react/README.md b/code/addons/docs/react/README.md index 1ef7daf97bb5..1642cd8090dc 100644 --- a/code/addons/docs/react/README.md +++ b/code/addons/docs/react/README.md @@ -15,7 +15,7 @@ To learn more about Storybook Docs, read the [general documentation](../README.m - [Props tables](#props-tables) - [MDX](#mdx) - [Inline stories](#inline-stories) -- [TypeScript props with `react-docgen`](#typescript-props-with-react-docgen) +- [TypeScript props with `react-docgen-typescript`](#typescript-props-with-react-docgen-typescript) - [More resources](#more-resources) ## Installation @@ -108,17 +108,17 @@ To do so for all stories, update `.storybook/preview.js`: export const parameters = { docs: { story: { inline: false } } }; ``` -## TypeScript props with `react-docgen` +## TypeScript props with `react-docgen-typescript` -If you're using TypeScript, there are two different options for generating props: `react-docgen-typescript` (default) or `react-docgen`. +If you're using TypeScript, there are two different options for generating props: `react-docgen` (default) or `react-docgen-typescript`. You can add the following lines to your `.storybook/main.js` to switch between the two (or disable docgen): ```js export default { typescript: { - // also valid 'react-docgen-typescript' | false - reactDocgen: 'react-docgen', + // also valid 'react-docgen' | false + reactDocgen: 'react-docgen-typescript', }, }; ``` @@ -129,7 +129,7 @@ Neither option is perfect, so here's everything you should know if you're thinki | --------------- | ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | **Features** | **Great**. The analysis produces great results which gives the best props table experience. | **OK**. React-docgen produces basic results that are fine for most use cases. | | **Performance** | **Slow**. It's doing a lot more work to produce those results, and may also have an inefficient implementation. | **Blazing fast**. Adding it to your project increases build time negligibly. | -| **Bugs** | **Many**. There are a lot of corner cases that are not handled properly, and are annoying for developers. | **Few**. But there's a dealbreaker, which is lack for imported types (see below). | +| **Bugs** | **Some**. There are corner cases that are not handled properly, and are annoying for developers. | **Some**. There are corner cases that are not handled properly, and are annoying for developers. | | **SB docs** | **Good**. Our prop tables have supported `react-docgen-typescript` results from the beginning, so it's relatively stable. | **OK**. There are some obvious improvements to fully support `react-docgen`, and they're coming soon. | **Performance** is a common question, so here are build times from a random project to quantify. Your mileage may vary: @@ -140,34 +140,6 @@ Neither option is perfect, so here's everything you should know if you're thinki | react-docgen | 29s | | none | 28s | -The biggest limitation of `react-docgen` is lack of support for imported types. What that means is that when a component uses a type defined in another file or package, `react-docgen` is unable to extract props information for that type. - -```tsx -import React, { FC } from 'react'; -import SomeType from './someFile'; - -type NewType = SomeType & { foo: string }; -const MyComponent: FC = ... -``` - -So in the previous example, `SomeType` would simply be ignored! There's an [open PR for this in the `react-docgen` repo](https://github.com/reactjs/react-docgen/pull/352) which you can upvote if it affects you. - -Another common pitfall when switching to `react-docgen` is [lack of support for `React.FC`](https://github.com/reactjs/react-docgen/issues/387). This means that the following common pattern **DOESN'T WORK**: - -```tsx -import React, { FC } from 'react'; -interface IProps { ... }; -const MyComponent: FC = ({ ... }) => ... -``` - -Fortunately, the following workaround works: - -```tsx -const MyComponent: FC = ({ ... }: IProps) => ... -``` - -Please upvote [the issue](https://github.com/reactjs/react-docgen/issues/387) if this is affecting your productivity, or better yet, submit a fix! - ## More resources Want to learn more? Here are some more articles on Storybook Docs: diff --git a/code/addons/storysource/package.json b/code/addons/storysource/package.json index 8f657c2e4d86..bc3b9988dfff 100644 --- a/code/addons/storysource/package.json +++ b/code/addons/storysource/package.json @@ -47,6 +47,7 @@ "prep": "node --loader ../../../scripts/node_modules/esbuild-register/loader.js -r ../../../scripts/node_modules/esbuild-register/register.js ../../../scripts/prepare/addon-bundle.ts" }, "dependencies": { + "@storybook/source-loader": "workspace:*", "estraverse": "^5.2.0", "tiny-invariant": "^1.3.1" }, @@ -56,7 +57,6 @@ "@storybook/manager-api": "workspace:*", "@storybook/preview-api": "workspace:*", "@storybook/router": "workspace:*", - "@storybook/source-loader": "workspace:*", "@storybook/theming": "workspace:*", "@types/react": "^18.0.37", "@types/react-syntax-highlighter": "11.0.5", diff --git a/code/addons/viewport/package.json b/code/addons/viewport/package.json index 80d650b26432..6457e61189e0 100644 --- a/code/addons/viewport/package.json +++ b/code/addons/viewport/package.json @@ -30,8 +30,6 @@ "import": "./dist/index.mjs" }, "./manager": "./dist/manager.js", - "./preview": "./dist/preview.ts", - "./register": "./dist/manager.js", "./package.json": "./package.json" }, "main": "dist/index.js", @@ -74,9 +72,6 @@ ], "managerEntries": [ "./src/manager.tsx" - ], - "previewEntries": [ - "./src/preview.ts" ] }, "gitHead": "e6a7fd8a655c69780bc20b9749c2699e44beae17", diff --git a/code/addons/viewport/src/index.ts b/code/addons/viewport/src/index.ts index 832595462640..79e0216d7923 100644 --- a/code/addons/viewport/src/index.ts +++ b/code/addons/viewport/src/index.ts @@ -1,4 +1,2 @@ -export * from './constants'; export * from './defaults'; -export * from './models'; -export * from './shortcuts'; +export type * from './models'; diff --git a/code/addons/viewport/src/models/index.ts b/code/addons/viewport/src/models/index.ts index 0cc15c61f914..7eb7ed1a6942 100644 --- a/code/addons/viewport/src/models/index.ts +++ b/code/addons/viewport/src/models/index.ts @@ -1,2 +1,2 @@ -export * from './Viewport'; -export * from './ViewportAddonParameter'; +export type * from './Viewport'; +export type * from './ViewportAddonParameter'; diff --git a/code/addons/viewport/src/preview.ts b/code/addons/viewport/src/preview.ts deleted file mode 100644 index 05f32144967d..000000000000 --- a/code/addons/viewport/src/preview.ts +++ /dev/null @@ -1 +0,0 @@ -export { INITIAL_VIEWPORTS, DEFAULT_VIEWPORT, MINIMAL_VIEWPORTS } from './defaults'; diff --git a/code/frameworks/angular/package.json b/code/frameworks/angular/package.json index bdc500c756e2..a170d98da10c 100644 --- a/code/frameworks/angular/package.json +++ b/code/frameworks/angular/package.json @@ -65,18 +65,18 @@ "webpack": "5" }, "devDependencies": { - "@angular-devkit/architect": "^0.1600.0-rc.4", - "@angular-devkit/build-angular": "^16.0.0-rc.4", - "@angular-devkit/core": "^16.0.0-rc.4", - "@angular/animations": "^16.0.0-rc.4", - "@angular/cli": "^16.0.0-rc.4", - "@angular/common": "^16.0.0-rc.4", - "@angular/compiler": "^16.0.0-rc.4", - "@angular/compiler-cli": "^16.0.0-rc.4", - "@angular/core": "^16.0.0-rc.4", - "@angular/forms": "^16.0.0-rc.4", - "@angular/platform-browser": "^16.0.0-rc.4", - "@angular/platform-browser-dynamic": "^16.0.0-rc.4", + "@angular-devkit/architect": "^0.1700.5", + "@angular-devkit/build-angular": "^17.0.5", + "@angular-devkit/core": "^17.0.5", + "@angular/animations": "^17.0.5", + "@angular/cli": "^17.0.5", + "@angular/common": "^17.0.5", + "@angular/compiler": "^17.0.5", + "@angular/compiler-cli": "^17.0.5", + "@angular/core": "^17.0.5", + "@angular/forms": "^17.0.5", + "@angular/platform-browser": "^17.0.5", + "@angular/platform-browser-dynamic": "^17.0.5", "@types/cross-spawn": "^6.0.2", "@types/tmp": "^0.2.3", "cross-spawn": "^7.0.3", @@ -86,20 +86,20 @@ "tmp": "^0.2.1", "typescript": "^5.3.2", "webpack": "5", - "zone.js": "^0.13.0" + "zone.js": "^0.14.2" }, "peerDependencies": { - "@angular-devkit/architect": ">=0.1400.0 < 0.1800.0", - "@angular-devkit/build-angular": ">=14.1.0 < 18.0.0", - "@angular-devkit/core": ">=14.1.0 < 18.0.0", - "@angular/cli": ">=14.1.0 < 18.0.0", - "@angular/common": ">=14.1.0 < 18.0.0", - "@angular/compiler": ">=14.1.0 < 18.0.0", - "@angular/compiler-cli": ">=14.1.0 < 18.0.0", - "@angular/core": ">=14.1.0 < 18.0.0", - "@angular/forms": ">=14.1.0 < 18.0.0", - "@angular/platform-browser": ">=14.1.0 < 18.0.0", - "@angular/platform-browser-dynamic": ">=14.1.0 < 18.0.0", + "@angular-devkit/architect": ">=0.1500.0 < 0.1800.0", + "@angular-devkit/build-angular": ">=15.0.0 < 18.0.0", + "@angular-devkit/core": ">=15.0.0 < 18.0.0", + "@angular/cli": ">=15.0.0 < 18.0.0", + "@angular/common": ">=15.0.0 < 18.0.0", + "@angular/compiler": ">=15.0.0 < 18.0.0", + "@angular/compiler-cli": ">=15.0.0 < 18.0.0", + "@angular/core": ">=15.0.0 < 18.0.0", + "@angular/forms": ">=15.0.0 < 18.0.0", + "@angular/platform-browser": ">=15.0.0 < 18.0.0", + "@angular/platform-browser-dynamic": ">=15.0.0 < 18.0.0", "@babel/core": "*", "rxjs": "^6.0.0 || ^7.4.0", "typescript": "^4.0.0 || ^5.0.0", diff --git a/code/frameworks/nextjs/README.md b/code/frameworks/nextjs/README.md index 252fb329167d..5875aa6b92be 100644 --- a/code/frameworks/nextjs/README.md +++ b/code/frameworks/nextjs/README.md @@ -40,6 +40,7 @@ - [Runtime Config](#runtime-config) - [Custom Webpack Config](#custom-webpack-config) - [Typescript](#typescript) + - [Experimental React Server Components (RSC)](#experimental-react-server-components-rsc) - [Notes for Yarn v2 and v3 users](#notes-for-yarn-v2-and-v3-users) - [FAQ](#faq) - [Stories for pages/components which fetch data](#stories-for-pagescomponents-which-fetch-data) @@ -268,7 +269,6 @@ The following features are not supported (yet). Support for these features might - [Support font loaders configuration in next.config.js](https://nextjs.org/docs/basic-features/font-optimization#specifying-a-subset) - [fallback](https://nextjs.org/docs/api-reference/next/font#fallback) option - [adjustFontFallback](https://nextjs.org/docs/api-reference/next/font#adjustfontfallback) option -- [declarations](https://nextjs.org/docs/api-reference/next/font#declarations) option - [preload](https://nextjs.org/docs/api-reference/next/font#preload) option gets ignored. Storybook handles Font loading its own way. - [display](https://nextjs.org/docs/api-reference/next/font#display) option gets ignored. All fonts are loaded with display set to "block" to make Storybook load the font properly. @@ -909,6 +909,39 @@ Storybook handles most [Typescript](https://www.typescriptlang.org/) configurati } ``` +### Experimental React Server Components (RSC) + +If your app uses [React Server Components (RSC)](https://nextjs.org/docs/app/building-your-application/rendering/server-components), Storybook can render them in stories in the browser. + +To enable this set the `experimentalNextRSC` feature flag in your `.storybook/main.js` config: + +```js +// main.js +export default { + features: { + experimentalNextRSC: true, + }, +}; +``` + +Setting this flag automatically wraps your story in a [Suspense](https://react.dev/reference/react/Suspense) wrapper, which is able to render asynchronous components in NextJS's version of React. + +If this wrapper causes problems in any of your existing stories, you can selectively disable it using the `nextjs.rsc` [parameter](https://storybook.js.org/docs/writing-stories/parameters) at the global/component/story level: + +```js +// MyServerComponent.stories.js +export default { + component: MyServerComponent, + parameters: { nextjs: { rsc: false } }, +}; +``` + +Note that wrapping your server components in Suspense does not help if your server components access server-side resources like the file system or Node-specific libraries. To deal work around this, you'll need to mock out your data access layer using [Webpack aliases](https://webpack.js.org/configuration/resolve/#resolvealias) or an addon like [storybook-addon-module-mock](https://storybook.js.org/addons/storybook-addon-module-mock). + +If your server components access data via the network, we recommend using the [MSW Storybook Addon](https://storybook.js.org/addons/msw-storybook-addon) to mock network requests. + +In the future we will provide better mocking support in Storybook and support for [Server Actions](https://nextjs.org/docs/app/api-reference/functions/server-actions). + ### Notes for Yarn v2 and v3 users If you're using [Yarn](https://yarnpkg.com/) v2 or v3, you may run into issues where Storybook can't resolve `style-loader` or `css-loader`. For example, you might get errors like: diff --git a/code/frameworks/nextjs/package.json b/code/frameworks/nextjs/package.json index 3bf4ba086ee8..46fd2faac332 100644 --- a/code/frameworks/nextjs/package.json +++ b/code/frameworks/nextjs/package.json @@ -42,6 +42,7 @@ "import": "./dist/font/webpack/loader/storybook-nextjs-font-loader.mjs" }, "./dist/preview.mjs": "./dist/preview.mjs", + "./dist/previewRSC.mjs": "./dist/previewRSC.mjs", "./next-image-loader-stub.js": { "types": "./dist/next-image-loader-stub.d.ts", "require": "./dist/next-image-loader-stub.js", @@ -127,16 +128,12 @@ "webpack": "^5.65.0" }, "peerDependencies": { - "@next/font": "^13.0.0|| ^14.0.0", - "next": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0", + "next": "^13.5.0 || ^14.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", "webpack": "^5.0.0" }, "peerDependenciesMeta": { - "@next/font": { - "optional": true - }, "typescript": { "optional": true }, @@ -156,9 +153,9 @@ "./src/index.ts", "./src/preset.ts", "./src/preview.tsx", + "./src/previewRSC.tsx", "./src/next-image-loader-stub.ts", "./src/images/decorator.tsx", - "./src/images/next-future-image.tsx", "./src/images/next-legacy-image.tsx", "./src/images/next-image.tsx", "./src/font/webpack/loader/storybook-nextjs-font-loader.ts", diff --git a/code/frameworks/nextjs/src/config/webpack.ts b/code/frameworks/nextjs/src/config/webpack.ts index 9d7c85d8c003..be2a983fceec 100644 --- a/code/frameworks/nextjs/src/config/webpack.ts +++ b/code/frameworks/nextjs/src/config/webpack.ts @@ -1,9 +1,8 @@ import type { Configuration as WebpackConfig } from 'webpack'; -import semver from 'semver'; import type { NextConfig } from 'next'; import { DefinePlugin } from 'webpack'; -import { addScopedAlias, getNextjsVersion, resolveNextConfig } from '../utils'; +import { addScopedAlias, resolveNextConfig } from '../utils'; const tryResolve = (path: string) => { try { @@ -36,8 +35,6 @@ export const configureConfig = async ({ return nextConfig; }; -const version = getNextjsVersion(); - const setupRuntimeConfig = (baseConfig: WebpackConfig, nextConfig: NextConfig): void => { const definePluginConfig: Record = { // this mimics what nextjs does client side @@ -50,21 +47,7 @@ const setupRuntimeConfig = (baseConfig: WebpackConfig, nextConfig: NextConfig): const newNextLinkBehavior = (nextConfig.experimental as any)?.newNextLinkBehavior; - /** - * In Next 13.0.0 - 13.0.5, the `newNextLinkBehavior` option now defaults to truthy (still - * `undefined` in the config), and `next/link` was engineered to opt *out* - * of it - * - */ - if ( - semver.gte(version, '13.0.0') && - semver.lt(version, '13.0.6') && - newNextLinkBehavior !== false - ) { - definePluginConfig['process.env.__NEXT_NEW_LINK_BEHAVIOR'] = true; - } else { - definePluginConfig['process.env.__NEXT_NEW_LINK_BEHAVIOR'] = newNextLinkBehavior; - } + definePluginConfig['process.env.__NEXT_NEW_LINK_BEHAVIOR'] = newNextLinkBehavior; baseConfig.plugins?.push(new DefinePlugin(definePluginConfig)); }; diff --git a/code/frameworks/nextjs/src/dependency-map.ts b/code/frameworks/nextjs/src/dependency-map.ts deleted file mode 100644 index dd848087f3e3..000000000000 --- a/code/frameworks/nextjs/src/dependency-map.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { Configuration as WebpackConfig } from 'webpack'; -import semver from 'semver'; -import { getNextjsVersion, addScopedAlias } from './utils'; - -const mapping: Record> = { - '<11.1.0': { - 'next/dist/next-server/lib/router-context': 'next/dist/next-server/lib/router-context', - }, - '>=11.1.0 <13.5.0': { - 'next/dist/shared/lib/router-context': 'next/dist/shared/lib/router-context', - }, - '>=13.0.2 <13.5.0': { - 'next/dist/shared/lib/hooks-client-context.shared-runtime': - 'next/dist/shared/lib/hooks-client-context', - }, - '<13.5.0': { - 'next/dist/shared/lib/router-context.shared-runtime': 'next/dist/shared/lib/router-context', - 'next/dist/shared/lib/head-manager-context.shared-runtime': - 'next/dist/shared/lib/head-manager-context', - 'next/dist/shared/lib/app-router-context.shared-runtime': - 'next/dist/shared/lib/app-router-context', - }, -}; - -export const configureAliasing = (baseConfig: WebpackConfig): void => { - const version = getNextjsVersion(); - const result: Record = {}; - - Object.keys(mapping).forEach((key) => { - if (semver.intersects(version, key)) { - Object.assign(result, mapping[key]); - } - }); - - Object.entries(result).forEach(([name, alias]) => { - addScopedAlias(baseConfig, name, alias); - }); -}; diff --git a/code/frameworks/nextjs/src/font/webpack/loader/google/get-font-face-declarations.ts b/code/frameworks/nextjs/src/font/webpack/loader/google/get-font-face-declarations.ts index 0f8bfb1957f8..3f9db77ebb16 100644 --- a/code/frameworks/nextjs/src/font/webpack/loader/google/get-font-face-declarations.ts +++ b/code/frameworks/nextjs/src/font/webpack/loader/google/get-font-face-declarations.ts @@ -5,34 +5,28 @@ import { GoogleFontsDownloadError, GoogleFontsLoadingError, } from '@storybook/core-events/server-errors'; +import { validateGoogleFontFunctionCall } from 'next/dist/compiled/@next/font/dist/google/validate-google-font-function-call'; +import { getGoogleFontsUrl } from 'next/dist/compiled/@next/font/dist/google/get-google-fonts-url'; +import { getFontAxes } from 'next/dist/compiled/@next/font/dist/google/get-font-axes'; +import { fetchCSSFromGoogleFonts } from 'next/dist/compiled/@next/font/dist/google/fetch-css-from-google-fonts'; import type { LoaderOptions } from '../types'; -const cssCache = new Map>(); +const cssCache = new Map(); export async function getFontFaceDeclarations(options: LoaderOptions) { - const { - fetchCSSFromGoogleFonts, - getFontAxes, - getUrl, - validateData, - } = require('../utils/google-font-utils'); - - const { fontFamily, weights, styles, selectedVariableAxes, display, variable } = validateData( - options.fontFamily, - [options.props], - null - ); + const { fontFamily, weights, styles, selectedVariableAxes, display, variable } = + validateGoogleFontFunctionCall(options.fontFamily, options.props); const fontAxes = getFontAxes(fontFamily, weights, styles, selectedVariableAxes); - const url = getUrl(fontFamily, fontAxes, display); + const url = getGoogleFontsUrl(fontFamily, fontAxes, display); try { const hasCachedCSS = cssCache.has(url); const fontFaceCSS = hasCachedCSS ? cssCache.get(url) - : await fetchCSSFromGoogleFonts(url, fontFamily).catch(() => null); + : await fetchCSSFromGoogleFonts(url, fontFamily, true).catch(() => null); if (!hasCachedCSS) { - cssCache.set(url, fontFaceCSS); + cssCache.set(url, fontFaceCSS as string); } else { cssCache.delete(url); } diff --git a/code/frameworks/nextjs/src/font/webpack/loader/local/get-font-face-declarations.ts b/code/frameworks/nextjs/src/font/webpack/loader/local/get-font-face-declarations.ts index d7d26ae55a37..f3f35c2fc065 100644 --- a/code/frameworks/nextjs/src/font/webpack/loader/local/get-font-face-declarations.ts +++ b/code/frameworks/nextjs/src/font/webpack/loader/local/get-font-face-declarations.ts @@ -2,22 +2,31 @@ // @ts-expect-error import loaderUtils from 'next/dist/compiled/loader-utils3'; import { getProjectRoot } from '@storybook/core-common'; +import { validateLocalFontFunctionCall } from 'next/dist/compiled/@next/font/dist/local/validate-local-font-function-call'; import path from 'path'; import type { LoaderOptions } from '../types'; type LocalFontSrc = string | Array<{ path: string; weight?: string; style?: string }>; -export async function getFontFaceDeclarations(options: LoaderOptions, rootContext: string) { +export async function getFontFaceDeclarations( + options: LoaderOptions, + rootContext: string, + swcMode: boolean +) { const localFontSrc = options.props.src as LocalFontSrc; // Parent folder relative to the root context - const parentFolder = path - .dirname(path.join(getProjectRoot(), options.filename)) - .replace(rootContext, ''); + const parentFolder = swcMode + ? path.dirname(path.join(getProjectRoot(), options.filename)).replace(rootContext, '') + : path.dirname(options.filename).replace(rootContext, ''); - const { validateData } = require('../utils/local-font-utils'); - const { weight, style, variable } = validateData('', options.props); + const { + weight, + style, + variable, + declarations = [], + } = validateLocalFontFunctionCall('', options.props); const id = `font-${loaderUtils.getHashDigest( Buffer.from(JSON.stringify(localFontSrc)), @@ -26,6 +35,10 @@ export async function getFontFaceDeclarations(options: LoaderOptions, rootContex 6 )}`; + const fontDeclarations = declarations + .map(({ prop, value }: { prop: string; value: string }) => `${prop}: ${value};`) + .join('\n'); + const arePathsWin32Format = /^[a-z]:\\/iu.test(options.filename); const cleanWin32Path = (pathString: string): string => arePathsWin32Format ? pathString.replace(/\\/gu, '/') : pathString; @@ -37,6 +50,7 @@ export async function getFontFaceDeclarations(options: LoaderOptions, rootContex return `@font-face { font-family: ${id}; src: url(.${localFontPath}); + ${fontDeclarations} }`; } return localFontSrc @@ -48,6 +62,7 @@ export async function getFontFaceDeclarations(options: LoaderOptions, rootContex src: url(.${localFontPath}); ${font.weight ? `font-weight: ${font.weight};` : ''} ${font.style ? `font-style: ${font.style};` : ''} + ${fontDeclarations} }`; }) .join(''); diff --git a/code/frameworks/nextjs/src/font/webpack/loader/storybook-nextjs-font-loader.ts b/code/frameworks/nextjs/src/font/webpack/loader/storybook-nextjs-font-loader.ts index 8b7c08894f72..2110701aae61 100644 --- a/code/frameworks/nextjs/src/font/webpack/loader/storybook-nextjs-font-loader.ts +++ b/code/frameworks/nextjs/src/font/webpack/loader/storybook-nextjs-font-loader.ts @@ -15,6 +15,7 @@ type FontFaceDeclaration = { export default async function storybookNextjsFontLoader(this: any) { const loaderOptions = this.getOptions() as LoaderOptions; + let swcMode = false; let options; if (Object.keys(loaderOptions).length > 0) { @@ -23,6 +24,7 @@ export default async function storybookNextjsFontLoader(this: any) { } else { // handles SWC mode const importQuery = JSON.parse(this.resourceQuery.slice(1)); + swcMode = true; options = { filename: importQuery.path, @@ -42,7 +44,7 @@ export default async function storybookNextjsFontLoader(this: any) { } if (options.source.endsWith('next/font/local') || options.source.endsWith('@next/font/local')) { - fontFaceDeclaration = await getLocalFontFaceDeclarations(options, rootCtx); + fontFaceDeclaration = await getLocalFontFaceDeclarations(options, rootCtx, swcMode); } if (typeof fontFaceDeclaration !== 'undefined') { diff --git a/code/frameworks/nextjs/src/font/webpack/loader/utils/google-font-utils.ts b/code/frameworks/nextjs/src/font/webpack/loader/utils/google-font-utils.ts deleted file mode 100644 index 20fe04209947..000000000000 --- a/code/frameworks/nextjs/src/font/webpack/loader/utils/google-font-utils.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* eslint-disable import/no-mutable-exports */ - -import dedent from 'ts-dedent'; - -type FontOptions = { - fontFamily: string; - weights: string[]; - styles: string[]; - display: string; - preload: boolean; - selectedVariableAxes?: string[]; - fallback?: string[]; - adjustFontFallback: boolean; - variable?: string; - subsets: string[]; -}; - -let validateData: (functionName: string, fontData: any, config: any) => FontOptions; - -let getUrl: ( - fontFamily: string, - axes: { - wght?: string[]; - ital?: string[]; - variableAxes?: [string, string][]; - }, - display: string -) => string; - -let getFontAxes: ( - fontFamily: string, - weights: string[], - styles: string[], - selectedVariableAxes?: string[] -) => { - wght?: string[]; - ital?: string[]; - variableAxes?: [string, string][]; -}; - -let fetchCSSFromGoogleFonts: (url: string, fontFamily: string) => Promise; - -// Support @next/font -try { - const fontUtils = require('@next/font/dist/google/utils'); - validateData = fontUtils.validateData; - getUrl = fontUtils.getUrl; - getFontAxes = fontUtils.getFontAxes; - fetchCSSFromGoogleFonts = fontUtils.fetchCSSFromGoogleFonts; -} catch (_) { - // Support next/font prior to v13.2.4 - try { - const fontUtils = require('next/dist/compiled/@next/font/dist/google/utils'); - validateData = fontUtils.validateData; - getUrl = fontUtils.getUrl; - getFontAxes = fontUtils.getFontAxes; - fetchCSSFromGoogleFonts = fontUtils.fetchCSSFromGoogleFonts; - } catch (__) { - // Support next/font since v13.2.4 - try { - validateData = (functionName, fontData, config) => - require('next/dist/compiled/@next/font/dist/google/validate-google-font-function-call').validateGoogleFontFunctionCall( - functionName, - fontData[0], - config - ); - getUrl = - require('next/dist/compiled/@next/font/dist/google/get-google-fonts-url').getGoogleFontsUrl; - getFontAxes = require('next/dist/compiled/@next/font/dist/google/get-font-axes').getFontAxes; - fetchCSSFromGoogleFonts = - require('next/dist/compiled/@next/font/dist/google/fetch-css-from-google-fonts').fetchCSSFromGoogleFonts; - } catch (e) { - throw new Error(dedent` - We are unable to load the helper functions to use next/font/google. - Please downgrade Next.js to version 13.2.4 to continue to use next/font/google in Storybook. - Feel free to open a Github Issue! - `); - } - } -} - -export { validateData, getUrl, getFontAxes, fetchCSSFromGoogleFonts }; diff --git a/code/frameworks/nextjs/src/font/webpack/loader/utils/local-font-utils.ts b/code/frameworks/nextjs/src/font/webpack/loader/utils/local-font-utils.ts deleted file mode 100644 index a7d91f71c73b..000000000000 --- a/code/frameworks/nextjs/src/font/webpack/loader/utils/local-font-utils.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* eslint-disable import/no-mutable-exports */ -import dedent from 'ts-dedent'; - -type FontOptions = { - src: Array<{ - path: string; - weight?: string; - style?: string; - ext: string; - format: string; - }>; - display: string; - weight?: string; - style?: string; - fallback?: string[]; - preload: boolean; - variable?: string; - adjustFontFallback?: string | false; - declarations?: Array<{ - prop: string; - value: string; - }>; -}; - -let validateData: (functionName: string, fontData: any) => FontOptions; - -// Support @next/font -try { - const fontUtils = require('@next/font/dist/local/utils'); - validateData = fontUtils.validateData; -} catch (_) { - // Support next/font prior to v13.2.4 - try { - const fontUtils = require('next/dist/compiled/@next/font/dist/local/utils'); - validateData = fontUtils.validateData; - } catch (__) { - // Support next/font since v13.2.4 - try { - validateData = - require('next/dist/compiled/@next/font/dist/local/validate-local-font-function-call').validateLocalFontFunctionCall; - } catch (e) { - throw new Error(dedent` - We are unable to load the helper functions to use next/font/local. - Please downgrade Next.js to version 13.2.4 to continue to use next/font/local in Storybook. - Feel free to open a Github Issue! - `); - } - } -} - -export { validateData }; diff --git a/code/frameworks/nextjs/src/images/next-future-image.tsx b/code/frameworks/nextjs/src/images/next-future-image.tsx deleted file mode 100644 index 306518079b38..000000000000 --- a/code/frameworks/nextjs/src/images/next-future-image.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import type * as _NextImage from 'next/image'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore import is aliased in webpack config -import OriginalNextFutureImage from 'sb-original/next/future/image'; - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore-error (this only errors during compilation for production) -// eslint-disable-next-line import/no-extraneous-dependencies -import { ImageContext as ImageContextValue } from '@storybook/nextjs/dist/image-context'; -import { type ImageContext as ImageContextType } from '../image-context'; -import { defaultLoader } from './next-image-default-loader'; - -const ImageContext = ImageContextValue as typeof ImageContextType; - -function NextFutureImage({ loader, ...props }: _NextImage.ImageProps) { - const imageParameters = React.useContext(ImageContext); - - return ( - - ); -} - -export default NextFutureImage; diff --git a/code/frameworks/nextjs/src/images/webpack.ts b/code/frameworks/nextjs/src/images/webpack.ts index e80e03545beb..31324d099e48 100644 --- a/code/frameworks/nextjs/src/images/webpack.ts +++ b/code/frameworks/nextjs/src/images/webpack.ts @@ -27,14 +27,6 @@ const configureImageDefaults = (baseConfig: WebpackConfig): void => { 'next/legacy/image': path.resolve(__dirname, './images/next-legacy-image'), }; } - - if (semver.satisfies(version, '^12.2.0')) { - resolve.alias = { - ...resolve.alias, - 'sb-original/next/future/image': require.resolve('next/future/image'), - 'next/future/image': path.resolve(__dirname, './images/next-future-image'), - }; - } }; const configureStaticImageImport = (baseConfig: WebpackConfig, nextConfig: NextConfig): void => { diff --git a/code/frameworks/nextjs/src/nextImport/webpack.ts b/code/frameworks/nextjs/src/nextImport/webpack.ts deleted file mode 100644 index 35f39a7069ed..000000000000 --- a/code/frameworks/nextjs/src/nextImport/webpack.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { Configuration as WebpackConfig } from 'webpack'; -import semver from 'semver'; -import { IgnorePlugin } from 'webpack'; -import { getNextjsVersion } from '../utils'; - -export function configureNextImport(baseConfig: WebpackConfig) { - const nextJSVersion = getNextjsVersion(); - - const isNext12 = semver.satisfies(nextJSVersion, '~12'); - const isNextVersionSmallerThan12dot2 = semver.lt(nextJSVersion, '12.2.0'); - const isNextVersionSmallerThan13 = semver.lt(nextJSVersion, '13.0.0'); - - baseConfig.plugins = baseConfig.plugins ?? []; - - if (!isNext12 || isNextVersionSmallerThan12dot2) { - baseConfig.plugins.push( - new IgnorePlugin({ - resourceRegExp: /next\/future\/image$/, - }) - ); - } - - if (isNextVersionSmallerThan13) { - baseConfig.plugins.push( - new IgnorePlugin({ - // ignore next/dist/shared/lib/hooks-client-context and next/legacy/image imports - resourceRegExp: - /(next\/dist\/shared\/lib\/hooks-client-context|next\/dist\/shared\/lib\/hooks-client-context\.shared-runtime|next\/legacy\/image)$/, - }) - ); - } - - if (isNextVersionSmallerThan12dot2) { - baseConfig.plugins.push( - new IgnorePlugin({ - resourceRegExp: /next\/dist\/shared\/lib\/app-router-context$/, - }) - ); - } -} diff --git a/code/frameworks/nextjs/src/preset.ts b/code/frameworks/nextjs/src/preset.ts index f29290209281..504425df0297 100644 --- a/code/frameworks/nextjs/src/preset.ts +++ b/code/frameworks/nextjs/src/preset.ts @@ -11,12 +11,10 @@ import { configureStyledJsx } from './styledJsx/webpack'; import { configureImages } from './images/webpack'; import { configureRuntimeNextjsVersionResolution } from './utils'; import type { FrameworkOptions, StorybookConfig } from './types'; -import { configureNextImport } from './nextImport/webpack'; import TransformFontImports from './font/babel'; import { configureNextFont } from './font/webpack/configureNextFont'; import nextBabelPreset from './babel/preset'; import { configureNodePolyfills } from './nodePolyfills/webpack'; -import { configureAliasing } from './dependency-map'; import { configureSWCLoader } from './swc/loader'; export const addons: PresetProperty<'addons'> = [ @@ -67,10 +65,17 @@ export const core: PresetProperty<'core'> = async (config, options) => { }; }; -export const previewAnnotations: PresetProperty<'previewAnnotations'> = (entry = []) => [ - ...entry, - join(dirname(require.resolve('@storybook/nextjs/package.json')), 'dist/preview.mjs'), -]; +export const previewAnnotations: PresetProperty<'previewAnnotations'> = ( + entry = [], + { features } +) => { + const nextDir = dirname(require.resolve('@storybook/nextjs/package.json')); + const result = [...entry, join(nextDir, 'dist/preview.mjs')]; + if (features?.experimentalNextRSC) { + result.unshift(join(nextDir, 'dist/previewRSC.mjs')); + } + return result; +}; // Not even sb init - automigrate - running dev // You're using a version of Nextjs prior to v10, which is unsupported by this framework. @@ -142,9 +147,7 @@ export const webpackFinal: StorybookConfig['webpackFinal'] = async (baseConfig, configDir: options.configDir, }); - configureAliasing(baseConfig); configureNextFont(baseConfig, builder?.useSWC); - configureNextImport(baseConfig); configureRuntimeNextjsVersionResolution(baseConfig); configureImports({ baseConfig, configDir: options.configDir }); configureCss(baseConfig, nextConfig); diff --git a/code/frameworks/nextjs/src/previewRSC.tsx b/code/frameworks/nextjs/src/previewRSC.tsx new file mode 100644 index 000000000000..d605a96db980 --- /dev/null +++ b/code/frameworks/nextjs/src/previewRSC.tsx @@ -0,0 +1,10 @@ +import type { Addon_DecoratorFunction } from '@storybook/types'; +import { ServerComponentDecorator } from './rsc/decorator'; + +export const decorators: Addon_DecoratorFunction[] = [ServerComponentDecorator]; + +export const parameters = { + nextjs: { + rsc: true, + }, +}; diff --git a/code/frameworks/nextjs/src/routing/decorator.tsx b/code/frameworks/nextjs/src/routing/decorator.tsx index 059e378c2521..939c3ed0114b 100644 --- a/code/frameworks/nextjs/src/routing/decorator.tsx +++ b/code/frameworks/nextjs/src/routing/decorator.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import type { Addon_StoryContext } from '@storybook/types'; import { action } from '@storybook/addon-actions'; +import { AppRouterProvider } from './app-router-provider'; import { PageRouterProvider } from './page-router-provider'; -import type { AppRouterProvider as TAppRouterProvider } from './app-router-provider'; import type { RouteParams, NextAppDirectory } from './types'; const defaultRouterParams: RouteParams = { @@ -17,19 +17,6 @@ export const RouterDecorator = ( const nextAppDirectory = (parameters.nextjs?.appDirectory as NextAppDirectory | undefined) ?? false; - const [AppRouterProvider, setAppRouterProvider] = React.useState< - typeof TAppRouterProvider | undefined - >(); - - React.useEffect(() => { - if (!nextAppDirectory) { - return; - } - import('./app-router-provider').then((exports) => - setAppRouterProvider(() => exports.AppRouterProvider) - ); - }, [nextAppDirectory]); - if (nextAppDirectory) { if (!AppRouterProvider) { return null; diff --git a/code/frameworks/nextjs/src/rsc/decorator.tsx b/code/frameworks/nextjs/src/rsc/decorator.tsx new file mode 100644 index 000000000000..73b7e7b4d817 --- /dev/null +++ b/code/frameworks/nextjs/src/rsc/decorator.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import type { StoryContext } from '@storybook/react'; + +export const ServerComponentDecorator = ( + Story: React.FC, + { parameters }: StoryContext +): React.ReactNode => + parameters?.nextjs?.rsc ? ( + + + + ) : ( + + ); diff --git a/code/frameworks/nextjs/src/styledJsx/decorator.tsx b/code/frameworks/nextjs/src/styledJsx/decorator.tsx index d358ecf55a15..5e18664c9659 100644 --- a/code/frameworks/nextjs/src/styledJsx/decorator.tsx +++ b/code/frameworks/nextjs/src/styledJsx/decorator.tsx @@ -1,14 +1,5 @@ import * as React from 'react'; - -let StyleRegistry: React.FC; - -try { - // next >= v12 - StyleRegistry = require('styled-jsx').StyleRegistry; -} catch { - // next < v12 - StyleRegistry = React.Fragment; -} +import { StyleRegistry } from 'styled-jsx'; export const StyledJsxDecorator = (Story: React.FC): React.ReactNode => ( diff --git a/code/frameworks/nextjs/src/styledJsx/webpack.ts b/code/frameworks/nextjs/src/styledJsx/webpack.ts index 7594159659a2..891947ba41d7 100644 --- a/code/frameworks/nextjs/src/styledJsx/webpack.ts +++ b/code/frameworks/nextjs/src/styledJsx/webpack.ts @@ -1,17 +1,6 @@ -import semver from 'semver'; import type { Configuration as WebpackConfig } from 'webpack'; -import { addScopedAlias, getNextjsVersion } from '../utils'; +import { addScopedAlias } from '../utils'; export const configureStyledJsx = (baseConfig: WebpackConfig): void => { - const version = getNextjsVersion(); - if (semver.gte(version, '12.0.0')) { - addScopedAlias(baseConfig, 'styled-jsx'); - } else { - addScopedAlias(baseConfig, 'styled-jsx/babel'); - addScopedAlias(baseConfig, 'styled-jsx/css'); - addScopedAlias(baseConfig, 'styled-jsx/macro'); - addScopedAlias(baseConfig, 'styled-jsx/server'); - addScopedAlias(baseConfig, 'styled-jsx/style'); - addScopedAlias(baseConfig, 'styled-jsx/webpack'); - } + addScopedAlias(baseConfig, 'styled-jsx'); }; diff --git a/code/frameworks/nextjs/template/stories/RSC.jsx b/code/frameworks/nextjs/template/stories/RSC.jsx new file mode 100644 index 000000000000..17a98b954919 --- /dev/null +++ b/code/frameworks/nextjs/template/stories/RSC.jsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const RSC = async ({ label }) => <>RSC {label}; + +export const Nested = async ({ children }) => <>Nested {children}; diff --git a/code/frameworks/nextjs/template/stories/RSC.stories.jsx b/code/frameworks/nextjs/template/stories/RSC.stories.jsx new file mode 100644 index 000000000000..e14456b50e58 --- /dev/null +++ b/code/frameworks/nextjs/template/stories/RSC.stories.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { RSC, Nested } from './RSC'; + +export default { + component: RSC, + args: { label: 'label' }, +}; + +export const Default = {}; + +export const DisableRSC = { + tags: ['test-skip'], + parameters: { + chromatic: { disable: true }, + nextjs: { rsc: false }, + }, +}; + +export const Error = { + tags: ['test-skip'], + parameters: { + chromatic: { disable: true }, + }, + render: () => { + throw new Error('RSC Error'); + }, +}; + +export const NestedRSC = { + render: (args) => ( + + + + ), +}; diff --git a/code/frameworks/nextjs/template/stories_nextjs-12-js/ImageFuture.stories.jsx b/code/frameworks/nextjs/template/stories_nextjs-12-js/ImageFuture.stories.jsx deleted file mode 100644 index aad2e8d56ac9..000000000000 --- a/code/frameworks/nextjs/template/stories_nextjs-12-js/ImageFuture.stories.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import Image from 'next/future/image'; - -import Accessibility from '../../assets/accessibility.svg'; - -export default { - component: Image, - args: { - src: Accessibility, - alt: 'Accessibility', - }, -}; - -export const Default = {}; - -export const BlurredPlaceholder = { - args: { - placeholder: 'blur', - }, -}; - -export const BlurredAbsolutePlaceholder = { - args: { - src: 'https://storybook.js.org/images/placeholders/50x50.png', - width: 50, - height: 50, - blurDataURL: - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAABP5JREFUWEeNlwtz2zgMhEGKsv/k9XFp82xe11763yOJVGcXC4m2czM3GYa0E2s/LACSTi+vv1czM/7CvPpqxY/ejPeS3khmFiPHOiVLHKaZi4ux4j18GpMlS6cALupEQBCKQM4BdnGzjIcJgs//QBxAPQAem55f3yL4PWJIdyCyhlMfPdYZot0cwj3Ayg/5JwHA13paen7pADphxr/n5VI8JQsHCCGQ3gVGLLsxQ3h/LYSn5383B05rwNOws3Z576LOTOduvwfrOd5FtVat4akx0uPTrw8BNuUz23vLsY7hmg7i4ipqM2saiAdruNuirh4ff0bNdb3Qy3vkvfAQwrkHoDxTTZtDCOKrC1bMEkdnsQh/PLyetOGHkXeRAgQAQ84efQcBdUhvFofoulpdm9WGGTA+AJEe7l+i37a2c371tCFKs5zzJjxQNBMi1im7OCudP2aNghJuzZaGdSMEZjpwf/t0UgNdg9DOyLGLnY0BUHlzwVtNDkgEQhBeKkb1tUDgQrq7frwAiIJi5BKAeIFgHk5mOpPzvgltOfcoK0Rrs7lWHwsgqtXarK3N0u23h5Ne8+3Cqxn5RYSMfHCAMgDAx4CBWlA9RAGw0GA/ol0gvFB4WjAvBAFUa83SzdUdAbYMqp28uHpxCRefxwAYhksAFBlthxCiXig+zT4TYqkC+Hq7OdAfJv8lPpZiZShWBBIuRP+jspDb2lwcDkzz7OLzbO/zvAHAoXTz5eYMQL0t2yHAiCFcfPY1QDwNFylA5bPoFpsV9fsEiMl8dhcc4PP1CYD3drYcBYdIKQrx0cbRxd2JHSDcQ297/vvoZ5smRC+AyV2AQ+nm03evge08Tyy4jGqXzWWEoIvTgXHU38pWiNgH4ixB/ukAcy/xycXfp4kwdAAAt399W+OCgMjxILQacxvRQ3gEwHgKUIr/rz53CuDFNyP/Eob4+/vEWkBq6AAA/HIi62n/Lk67Q7wDYQ0UpQB7hc54T4E6gACLTYxeAwB0YKZL6U4ATEGIBwCs7qPfQJCCHkCnoK50noJKcXcAojsEAJZZKXhgCoziGKxqWV8IMNp4kP2aC+oB0TMFvhGxDQHQfIPhDrilwKOm/YCZASAHfgBABQjr3f7CyAkA0cPB03AQULRhKd4xAIjzHymo2Gp7gN0FAMAVOoA2fPz03a9ssh/RM7Iz8QKIzYF9HyB0XEZ1xJ4DzNoDOAfAslhDDTyjDfv8A2AcBeCiu/jBHQEgxnYW6Kp6BlCVAkQM8VnieF2Xyr0ivXy+XvsCzKOihwNHCCryw8HrQXVB8dgFeRfAVQiXjMbIIgXINQYB2H7Kf5wF/2Ar7h0AgKKGuAP4zOjhzlkLbpcRXKRZhNUjxG6HIQDOjN47gCn4+fWW3xVS9urPESEEwwHMo9IhAGxS2ISiA1iEnQOoA4hXRAwItp7WzL9Ow18ESJaw/ar4NgeOR49cAHCAnaH8swBhv+6CBGjeBSxEOUAI7HyKHkD4O9xKb3/feQouAI4uLBciHRRHmgbfA7h/xFc9AngNBADthvii1sMOiPwDAFeyt6s7FSFS4PmnA1v0vQvqDqQKAAPE/weAUuEgsj8c+H11Twdw/AKANXA82EDr5cJBEEzB3oI4Mb0AdR3nNw8vQnegWuvqAABwJFJEBwDgNdA7IOs3gL0LhuJdwBY8c4BfNnDdVgooHiOqn/b7JoSW/QODjTHXhU7hMQAAAABJRU5ErkJggg==', - placeholder: 'blur', - }, - parameters: { - // ignoring in Chromatic to avoid inconsistent snapshots - // given that the switch from blur to image is quite fast - chromatic: { disableSnapshot: true }, - }, -}; - -export const FilledParent = { - args: { - fill: true, - }, - decorator: [ - (Story) =>
{Story()}
, - ], -}; - -export const Sized = { - args: { - fill: true, - sizes: '(max-width: 600px) 100vw, 600px', - decorator: [ - (Story) =>
{Story()}
, - ], - }, -}; diff --git a/code/frameworks/nextjs/template/stories_nextjs-12-js/Link.stories.jsx b/code/frameworks/nextjs/template/stories_nextjs-12-js/Link.stories.jsx deleted file mode 100644 index 40631d5cf2f7..000000000000 --- a/code/frameworks/nextjs/template/stories_nextjs-12-js/Link.stories.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import Link from 'next/link'; - -const Component = () => ( - -); - -export default { - component: Component, -}; - -export const Default = {}; diff --git a/code/frameworks/react-vite/src/types.ts b/code/frameworks/react-vite/src/types.ts index fa3e8dec5b24..0f31ea91db51 100644 --- a/code/frameworks/react-vite/src/types.ts +++ b/code/frameworks/react-vite/src/types.ts @@ -42,7 +42,7 @@ type TypescriptOptions = TypescriptOptionsBase & { /** * Sets the type of Docgen when working with React and TypeScript * - * @default `'react-docgen-typescript'` + * @default `'react-docgen'` */ reactDocgen: 'react-docgen-typescript' | 'react-docgen' | false; /** diff --git a/code/frameworks/sveltekit/src/plugins/mock-sveltekit-stores.ts b/code/frameworks/sveltekit/src/plugins/mock-sveltekit-stores.ts index 873ce8bf3517..b5e63f8591e2 100644 --- a/code/frameworks/sveltekit/src/plugins/mock-sveltekit-stores.ts +++ b/code/frameworks/sveltekit/src/plugins/mock-sveltekit-stores.ts @@ -1,17 +1,15 @@ import { resolve } from 'node:path'; -import { mergeConfig, type Plugin } from 'vite'; +import type { Plugin } from 'vite'; export function mockSveltekitStores() { return { name: 'storybook:sveltekit-mock-stores', - enforce: 'post', - config: (config) => - mergeConfig(config, { - resolve: { - alias: { - $app: resolve(__dirname, '../src/mocks/app/'), - }, + config: () => ({ + resolve: { + alias: { + $app: resolve(__dirname, '../src/mocks/app/'), }, - }), + }, + }), } satisfies Plugin; } diff --git a/code/lib/cli/package.json b/code/lib/cli/package.json index 03d37dd63554..5fe6d5d5f40e 100644 --- a/code/lib/cli/package.json +++ b/code/lib/cli/package.json @@ -93,6 +93,7 @@ "simple-update-notifier": "^2.0.0", "strip-json-comments": "^3.0.1", "tempy": "^1.0.1", + "tiny-invariant": "^1.3.1", "ts-dedent": "^2.0.0", "util-deprecate": "^1.0.2" }, diff --git a/code/lib/cli/src/HandledError.ts b/code/lib/cli/src/HandledError.ts index 4a603fed1844..429d9620df3a 100644 --- a/code/lib/cli/src/HandledError.ts +++ b/code/lib/cli/src/HandledError.ts @@ -1,9 +1,9 @@ export class HandledError extends Error { public handled = true; - constructor(messageOrError: string | Error) { - super(typeof messageOrError === 'string' ? messageOrError : messageOrError.message); + constructor(error: unknown) { + super(String(error)); - if (typeof messageOrError !== 'string') this.cause = messageOrError; + if (typeof error !== 'string') this.cause = error; } } diff --git a/code/lib/cli/src/add.ts b/code/lib/cli/src/add.ts index 8728da80ad5f..20e1c42bc811 100644 --- a/code/lib/cli/src/add.ts +++ b/code/lib/cli/src/add.ts @@ -37,7 +37,10 @@ const postinstallAddon = async (addonName: string, options: PostinstallOptions) const getVersionSpecifier = (addon: string) => { const groups = /^(...*)@(.*)$/.exec(addon); - return groups ? [groups[1], groups[2]] : [addon, undefined]; + if (groups) { + return [groups[0], groups[2]] as const; + } + return [addon, undefined] as const; }; const requireMain = (configDir: string) => { @@ -79,6 +82,12 @@ export async function add( 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 (checkInstalled(addon, requireMain(configDir))) { throw new Error(dedent` Addon ${addon} is already installed; we skipped adding it to your ${mainConfig}. diff --git a/code/lib/cli/src/automigrate/fixes/__snapshots__/angular-builders.test.ts.snap b/code/lib/cli/src/automigrate/fixes/__snapshots__/angular-builders.test.ts.snap index d24d6968b01d..4a4f8e5ab107 100644 --- a/code/lib/cli/src/automigrate/fixes/__snapshots__/angular-builders.test.ts.snap +++ b/code/lib/cli/src/automigrate/fixes/__snapshots__/angular-builders.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`is not Nx project angular builders Angular < 14.0.0 should throw an Error 1`] = ` -"❌ Your project uses Angular < 14.0.0. Storybook 7.0 for Angular requires Angular 14.0.0 or higher. -Please upgrade your Angular version to at least version 14.0.0 to use Storybook 7.0 in your project." +exports[`is not Nx project angular builders Angular < 15.0.0 should throw an Error 1`] = ` +"❌ Your project uses Angular < 15.0.0. Storybook 8.0 for Angular requires Angular 15.0.0 or higher. +Please upgrade your Angular version to at least version 15.0.0 to use Storybook 8.0 in your project." `; 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 b9066b9c48f2..c09e3b11e538 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 @@ -29,6 +29,7 @@ jest.mock('../../generators/ANGULAR/helpers', () => ({ })); describe('is Nx project', () => { + // @ts-expect-error (Type 'null' is not comparable) const packageManager = { getPackageVersion: () => { return null; 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 8f1f0650deb6..34f1c1cdccd5 100644 --- a/code/lib/cli/src/automigrate/fixes/angular-builders.test.ts +++ b/code/lib/cli/src/automigrate/fixes/angular-builders.test.ts @@ -68,11 +68,11 @@ describe('is not Nx project', () => { }); }); - describe('Angular < 14.0.0', () => { + describe('Angular < 15.0.0', () => { const packageManager = { getPackageVersion: (packageName: string) => { if (packageName === '@angular/core') { - return Promise.resolve('12.0.0'); + return Promise.resolve('14.0.0'); } return null; @@ -86,11 +86,11 @@ describe('is not Nx project', () => { }); }); - describe('Angular >= 14.0.0', () => { + describe('Angular >= 16.0.0', () => { const packageManager = { getPackageVersion: (packageName) => { if (packageName === '@angular/core') { - return Promise.resolve('15.0.0'); + return Promise.resolve('16.0.0'); } return null; diff --git a/code/lib/cli/src/automigrate/fixes/angular-builders.ts b/code/lib/cli/src/automigrate/fixes/angular-builders.ts index 3bdc9da5062c..c360a4d55b35 100644 --- a/code/lib/cli/src/automigrate/fixes/angular-builders.ts +++ b/code/lib/cli/src/automigrate/fixes/angular-builders.ts @@ -31,10 +31,10 @@ export const angularBuilders: Fix = { return null; } - if (semver.lt(angularVersion, '14.0.0')) { + if (semver.lt(angularVersion, '15.0.0')) { throw new Error(dedent` - ❌ Your project uses Angular < 14.0.0. Storybook 7.0 for Angular requires Angular 14.0.0 or higher. - Please upgrade your Angular version to at least version 14.0.0 to use Storybook 7.0 in your project. + ❌ Your project uses Angular < 15.0.0. Storybook 8.0 for Angular requires Angular 15.0.0 or higher. + Please upgrade your Angular version to at least version 15.0.0 to use Storybook 8.0 in your project. `); } diff --git a/code/lib/cli/src/automigrate/fixes/autodocs-true.ts b/code/lib/cli/src/automigrate/fixes/autodocs-true.ts index c3c2adc59f06..f84c3f7ee778 100644 --- a/code/lib/cli/src/automigrate/fixes/autodocs-true.ts +++ b/code/lib/cli/src/automigrate/fixes/autodocs-true.ts @@ -1,15 +1,13 @@ import chalk from 'chalk'; import { dedent } from 'ts-dedent'; -import type { StorybookConfigRaw } from '@storybook/types'; - import type { Fix } from '../types'; import { updateMainConfig } from '../helpers/mainConfigFile'; const logger = console; interface AutodocsTrueFrameworkRunOptions { - value?: StorybookConfigRaw['docs']['autodocs']; + value?: boolean | 'tag'; } /** @@ -83,7 +81,7 @@ export const autodocsTrue: Fix = { async run({ result: { value }, dryRun, mainConfigPath }) { logger.info(`✅ Setting 'docs.autodocs' to true in main.js`); if (!dryRun) { - await updateMainConfig({ mainConfigPath, dryRun }, async (main) => { + await updateMainConfig({ mainConfigPath, dryRun: !!dryRun }, async (main) => { main.removeField(['docs', 'docsPage']); main.setFieldValue(['docs', 'autodocs'], value ?? true); }); diff --git a/code/lib/cli/src/automigrate/fixes/bare-mdx-stories-glob.ts b/code/lib/cli/src/automigrate/fixes/bare-mdx-stories-glob.ts index b4f81f08a8fb..175e69c23b14 100644 --- a/code/lib/cli/src/automigrate/fixes/bare-mdx-stories-glob.ts +++ b/code/lib/cli/src/automigrate/fixes/bare-mdx-stories-glob.ts @@ -116,7 +116,7 @@ export const bareMdxStoriesGlob: Fix = { ${JSON.stringify(nextStoriesEntries, null, 2)}`); if (!dryRun) { - await updateMainConfig({ mainConfigPath, dryRun }, async (main) => { + await updateMainConfig({ mainConfigPath, dryRun: !!dryRun }, async (main) => { main.setFieldValue(['stories'], nextStoriesEntries); }); } diff --git a/code/lib/cli/src/automigrate/fixes/builder-vite.ts b/code/lib/cli/src/automigrate/fixes/builder-vite.ts index b7ee1317957c..b1d31444e914 100644 --- a/code/lib/cli/src/automigrate/fixes/builder-vite.ts +++ b/code/lib/cli/src/automigrate/fixes/builder-vite.ts @@ -75,7 +75,7 @@ export const builderVite: Fix = { logger.info(`✅ Updating main.js to use vite builder`); if (!dryRun) { - await updateMainConfig({ dryRun, mainConfigPath }, async (main) => { + await updateMainConfig({ dryRun: !!dryRun, mainConfigPath }, async (main) => { const updatedBuilder = typeof builder === 'string' ? '@storybook/builder-vite' diff --git a/code/lib/cli/src/automigrate/fixes/cra5.test.ts b/code/lib/cli/src/automigrate/fixes/cra5.test.ts index 1eedffc0a12f..a2eb150f30ff 100644 --- a/code/lib/cli/src/automigrate/fixes/cra5.test.ts +++ b/code/lib/cli/src/automigrate/fixes/cra5.test.ts @@ -109,6 +109,7 @@ describe('cra5 fix', () => { }); }); describe('no cra dependency', () => { + // @ts-expect-error (Type 'null' is not comparable) const packageManager = { getPackageVersion: () => { return null; diff --git a/code/lib/cli/src/automigrate/fixes/eslint-plugin.test.ts b/code/lib/cli/src/automigrate/fixes/eslint-plugin.test.ts index 54a527848ae6..e896f461f22f 100644 --- a/code/lib/cli/src/automigrate/fixes/eslint-plugin.test.ts +++ b/code/lib/cli/src/automigrate/fixes/eslint-plugin.test.ts @@ -93,9 +93,9 @@ describe('eslint-plugin fix', () => { checkEslint({ packageJson, }) - ).resolves.toMatchObject({ - unsupportedExtension: undefined, - }); + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"warn: Unable to find .eslintrc config file, skipping"` + ); }); it('when .eslintrc is using unsupported extension', async () => { @@ -104,7 +104,9 @@ describe('eslint-plugin fix', () => { packageJson, eslintExtension: 'yml', }) - ).resolves.toMatchObject({ unsupportedExtension: 'yml' }); + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"warn: Unable to find .eslintrc config file, skipping"` + ); }); }); }); diff --git a/code/lib/cli/src/automigrate/fixes/eslint-plugin.ts b/code/lib/cli/src/automigrate/fixes/eslint-plugin.ts index a34f30e78340..25e5dfd03204 100644 --- a/code/lib/cli/src/automigrate/fixes/eslint-plugin.ts +++ b/code/lib/cli/src/automigrate/fixes/eslint-plugin.ts @@ -33,15 +33,15 @@ export const eslintPlugin: Fix = { return null; } - let eslintFile; - let unsupportedExtension; + let eslintFile: string | null = null; + let unsupportedExtension: string | undefined; try { eslintFile = findEslintFile(); } catch (err) { - unsupportedExtension = err.message; + unsupportedExtension = String(err); } - if (!eslintFile && !unsupportedExtension) { + if (!eslintFile || !unsupportedExtension) { logger.warn('Unable to find .eslintrc config file, skipping'); return null; } diff --git a/code/lib/cli/src/automigrate/fixes/index.ts b/code/lib/cli/src/automigrate/fixes/index.ts index 2becfe0b156a..2f71a11255f6 100644 --- a/code/lib/cli/src/automigrate/fixes/index.ts +++ b/code/lib/cli/src/automigrate/fixes/index.ts @@ -19,6 +19,7 @@ import { angularBuilders } from './angular-builders'; import { incompatibleAddons } from './incompatible-addons'; import { angularBuildersMultiproject } from './angular-builders-multiproject'; import { wrapRequire } from './wrap-require'; +import { reactDocgen } from './react-docgen'; export * from '../types'; @@ -42,6 +43,7 @@ export const allFixes: Fix[] = [ angularBuildersMultiproject, angularBuilders, wrapRequire, + reactDocgen, ]; export const initFixes: Fix[] = [missingBabelRc, eslintPlugin]; diff --git a/code/lib/cli/src/automigrate/fixes/mdx-1-to-2.ts b/code/lib/cli/src/automigrate/fixes/mdx-1-to-2.ts index 2ff4c8b4c6eb..108dde3c5aa3 100644 --- a/code/lib/cli/src/automigrate/fixes/mdx-1-to-2.ts +++ b/code/lib/cli/src/automigrate/fixes/mdx-1-to-2.ts @@ -47,7 +47,7 @@ export const mdx1to2: Fix = { async check() { const storiesMdxFiles = await globby('./!(node_modules)**/*.(story|stories).mdx'); - return storiesMdxFiles.length ? { storiesMdxFiles } : undefined; + return storiesMdxFiles.length ? { storiesMdxFiles } : null; }, prompt({ storiesMdxFiles }) { diff --git a/code/lib/cli/src/automigrate/fixes/mdx-gfm.ts b/code/lib/cli/src/automigrate/fixes/mdx-gfm.ts index 1601dce89572..7c3fd0130231 100644 --- a/code/lib/cli/src/automigrate/fixes/mdx-gfm.ts +++ b/code/lib/cli/src/automigrate/fixes/mdx-gfm.ts @@ -33,6 +33,10 @@ export const mdxgfm: Fix = { let pattern; + if (typeof configDir === 'undefined') { + return false; + } + if (typeof item === 'string') { pattern = slash(join(configDir, item)); } else if (typeof item === 'object') { @@ -41,6 +45,10 @@ export const mdxgfm: Fix = { pattern = slash(join(configDir, directory, files)); } + if (!pattern) { + return false; + } + const files = await glob(pattern, commonGlobOptions(pattern)); return files.some((f) => f.endsWith('.mdx')); @@ -94,7 +102,7 @@ export const mdxgfm: Fix = { [`@storybook/addon-mdx-gfm@${versionToInstall}`] ); - await updateMainConfig({ mainConfigPath, dryRun }, async (main) => { + await updateMainConfig({ mainConfigPath, dryRun: !!dryRun }, async (main) => { logger.info(`✅ Adding "@storybook/addon-mdx-gfm" addon`); if (!dryRun) { main.appendValueToArray(['addons'], '@storybook/addon-mdx-gfm'); diff --git a/code/lib/cli/src/automigrate/fixes/missing-babelrc.test.ts b/code/lib/cli/src/automigrate/fixes/missing-babelrc.test.ts index f5306d9b0363..91a3230141fe 100644 --- a/code/lib/cli/src/automigrate/fixes/missing-babelrc.test.ts +++ b/code/lib/cli/src/automigrate/fixes/missing-babelrc.test.ts @@ -69,7 +69,7 @@ describe('missing-babelrc fix', () => { afterEach(jest.restoreAllMocks); it('skips when storybook version < 7.0.0', async () => { - await expect(check({ storybookVersion: '6.3.2' })).resolves.toBeNull(); + await expect(check({ storybookVersion: '6.3.2', main: {} })).resolves.toBeNull(); }); it('skips when babelrc config is present', async () => { diff --git a/code/lib/cli/src/automigrate/fixes/missing-babelrc.ts b/code/lib/cli/src/automigrate/fixes/missing-babelrc.ts index 5c70808657a6..14332f15b5f5 100644 --- a/code/lib/cli/src/automigrate/fixes/missing-babelrc.ts +++ b/code/lib/cli/src/automigrate/fixes/missing-babelrc.ts @@ -52,7 +52,7 @@ export const missingBabelRc: Fix = { filename: '__fake__.js', // somehow needed to detect .babelrc.* files }); - if (!config.config && !config.babelrc && !packageJson.babel) { + if (!config?.config && !config?.babelrc && !packageJson.babel) { return { needsBabelRc: true }; } } diff --git a/code/lib/cli/src/automigrate/fixes/new-frameworks.test.ts b/code/lib/cli/src/automigrate/fixes/new-frameworks.test.ts index 441af07c311f..5e63d6d22fff 100644 --- a/code/lib/cli/src/automigrate/fixes/new-frameworks.test.ts +++ b/code/lib/cli/src/automigrate/fixes/new-frameworks.test.ts @@ -26,6 +26,7 @@ const checkNewFrameworks = async ({ storybookVersion, rendererPackage, configDir: '', + mainConfigPath: ' ', }); }; diff --git a/code/lib/cli/src/automigrate/fixes/new-frameworks.ts b/code/lib/cli/src/automigrate/fixes/new-frameworks.ts index cecf3f852df3..657372d70ea8 100644 --- a/code/lib/cli/src/automigrate/fixes/new-frameworks.ts +++ b/code/lib/cli/src/automigrate/fixes/new-frameworks.ts @@ -4,6 +4,7 @@ import semver from 'semver'; import { frameworkPackages, rendererPackages } from '@storybook/core-common'; import type { Preset } from '@storybook/types'; +import invariant from 'tiny-invariant'; import type { Fix } from '../types'; import { getStorybookVersionSpecifier } from '../../helpers'; import { @@ -26,13 +27,13 @@ interface NewFrameworkRunOptions { dependenciesToRemove: string[]; hasFrameworkInMainConfig: boolean; frameworkPackage: string; - metaFramework: string; + metaFramework: string | undefined; renderer: string; addonsToRemove: string[]; frameworkOptions: Record; rendererOptions: Record; addonOptions: Record; - builderConfig: string | Record; + builderConfig: string | Record | undefined; builderInfo: { name: string; options: Record; @@ -69,13 +70,17 @@ export const newFrameworks: Fix = { return null; } + if (typeof configDir === 'undefined') { + return null; + } + const packageJson = await packageManager.retrievePackageJson(); const frameworkPackageName = getFrameworkPackageName(mainConfig); const rendererPackageName = rendererPackage ?? - (await getRendererPackageNameFromFramework(frameworkPackageName)) ?? + (await getRendererPackageNameFromFramework(frameworkPackageName as string)) ?? (await detectRenderer(packageJson)); let hasFrameworkInMainConfig = !!frameworkPackageName; @@ -220,7 +225,9 @@ export const newFrameworks: Fix = { `); } - return { + invariant(mainConfigPath, 'Missing main config path.'); + + const result: Awaited['check']>> = { mainConfigPath, dependenciesToAdd, dependenciesToRemove, @@ -239,6 +246,7 @@ export const newFrameworks: Fix = { builderConfig, metaFramework, }; + return result; }, prompt({ @@ -453,7 +461,7 @@ export const newFrameworks: Fix = { } } - await updateMainConfig({ mainConfigPath, dryRun }, async (main) => { + await updateMainConfig({ mainConfigPath, dryRun: !!dryRun }, async (main) => { logger.info(`✅ Updating main.js`); logger.info(`✅ Updating "framework" field`); diff --git a/code/lib/cli/src/automigrate/fixes/react-docgen.test.ts b/code/lib/cli/src/automigrate/fixes/react-docgen.test.ts new file mode 100644 index 000000000000..ecfdbe48a432 --- /dev/null +++ b/code/lib/cli/src/automigrate/fixes/react-docgen.test.ts @@ -0,0 +1,77 @@ +import type { StorybookConfig } from '@storybook/types'; +import { reactDocgen } from './react-docgen'; + +const check = async ({ + packageManager, + main: mainConfig, + storybookVersion = '7.0.0', +}: { + packageManager: any; + main: Partial & Record; + storybookVersion?: string; +}) => { + return reactDocgen.check({ + packageManager, + configDir: '', + mainConfig: mainConfig as any, + storybookVersion, + }); +}; + +describe('no-ops', () => { + test('typescript.reactDocgen is already set', async () => { + await expect( + check({ + packageManager: {}, + main: { + typescript: { + // @ts-expect-error assume react + reactDocgen: 'react-docgen-typescript', + }, + }, + }) + ).resolves.toBeFalsy(); + + await expect( + check({ + packageManager: {}, + main: { + typescript: { + // @ts-expect-error assume react + reactDocgen: false, + }, + }, + }) + ).resolves.toBeFalsy(); + }); + test('typescript.reactDocgen and typescript.reactDocgenTypescriptOptions are both unset', async () => { + await expect( + check({ + packageManager: {}, + main: { + typescript: { + // @ts-expect-error assume react + reactDocgen: 'react-docgen-typescript', + reactDocgenTypescriptOptions: undefined, + }, + }, + }) + ).resolves.toBeFalsy(); + }); +}); + +describe('continue', () => { + test('typescript.reactDocgenTypescriptOptions is set', async () => { + await expect( + check({ + packageManager: {}, + main: { + typescript: { + // @ts-expect-error assume react + reactDocgenTypescriptOptions: {}, + }, + }, + }) + ).resolves.toBeTruthy(); + }); +}); diff --git a/code/lib/cli/src/automigrate/fixes/react-docgen.ts b/code/lib/cli/src/automigrate/fixes/react-docgen.ts new file mode 100644 index 000000000000..279c6d455a04 --- /dev/null +++ b/code/lib/cli/src/automigrate/fixes/react-docgen.ts @@ -0,0 +1,49 @@ +import { dedent } from 'ts-dedent'; +import { updateMainConfig } from '../helpers/mainConfigFile'; +import type { Fix } from '../types'; + +const logger = console; + +interface Options { + reactDocgenTypescriptOptions: any; +} + +/** + */ +export const reactDocgen: Fix = { + id: 'react-docgen', + + async check({ mainConfig }) { + // @ts-expect-error assume react + const { reactDocgenTypescriptOptions } = mainConfig.typescript || {}; + + return reactDocgenTypescriptOptions ? { reactDocgenTypescriptOptions } : null; + }, + + prompt() { + return dedent` + You have "typescript.reactDocgenTypescriptOptions" configured in your main.js, + but "typescript.reactDocgen" is unset. + + In Storybook 8.0, we changed the default React docgen analysis from + "react-docgen-typescript" to "react-docgen", which dramatically faster + but doesn't handle all TypeScript constructs. + + We can update your config to continue to use "react-docgen-typescript", + though we recommend giving "react-docgen" for a much faster dev experience. + + https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#react-docgen-component-analysis-by-default + `; + }, + + async run({ dryRun, mainConfigPath }) { + if (!dryRun) { + await updateMainConfig({ mainConfigPath, dryRun: !!dryRun }, async (main) => { + logger.info(`✅ Setting typescript.reactDocgen`); + if (!dryRun) { + main.setFieldValue(['typescript', 'reactDocgen'], 'react-docgen-typescript'); + } + }); + } + }, +}; diff --git a/code/lib/cli/src/automigrate/fixes/sb-scripts.ts b/code/lib/cli/src/automigrate/fixes/sb-scripts.ts index 8471d3c1a6c3..24ac7854ef4d 100644 --- a/code/lib/cli/src/automigrate/fixes/sb-scripts.ts +++ b/code/lib/cli/src/automigrate/fixes/sb-scripts.ts @@ -1,6 +1,7 @@ import chalk from 'chalk'; import { dedent } from 'ts-dedent'; import semver from 'semver'; +import type { PackageJson } from '@storybook/types'; import type { Fix } from '../types'; import type { PackageJsonWithDepsAndDevDeps } from '../../js-package-manager'; @@ -18,10 +19,14 @@ const logger = console; * that do contain the actual sb binary, and not something like "npm run start-storybook" * which could actually be a custom script even though the name matches the legacy binary name */ -export const getStorybookScripts = (allScripts: Record) => { +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 = allScripts[key].split(' '); + const allWordsFromScript = currentScript.split(' '); const newScript = allWordsFromScript .map((currentWord, index) => { const previousWord = allWordsFromScript[index - 1]; @@ -51,7 +56,7 @@ export const getStorybookScripts = (allScripts: Record) => { if (isStorybookScript) { acc[key] = { - before: allScripts[key], + before: currentScript, after: newScript, }; } @@ -90,7 +95,7 @@ export const sbScripts: Fix = { prompt({ storybookVersion, storybookScripts }) { const sbFormatted = chalk.cyan(`Storybook ${storybookVersion}`); - const newScriptsMessage = Object.keys(storybookScripts).reduce((acc, scriptKey) => { + const newScriptsMessage = Object.keys(storybookScripts).reduce((acc: string[], scriptKey) => { acc.push( [ chalk.bold(scriptKey), diff --git a/code/lib/cli/src/automigrate/fixes/vue3.test.ts b/code/lib/cli/src/automigrate/fixes/vue3.test.ts index 6d7f61d77186..cfafca574165 100644 --- a/code/lib/cli/src/automigrate/fixes/vue3.test.ts +++ b/code/lib/cli/src/automigrate/fixes/vue3.test.ts @@ -133,6 +133,7 @@ describe('vue3 fix', () => { }); describe('no vue dependency', () => { it('should no-op', async () => { + // @ts-expect-error (Type 'null' is not comparable) const packageManager = { getPackageVersion: (packageName) => { return null; diff --git a/code/lib/cli/src/automigrate/fixes/webpack5.test.ts b/code/lib/cli/src/automigrate/fixes/webpack5.test.ts index db03eeb57a08..37a601f40c0a 100644 --- a/code/lib/cli/src/automigrate/fixes/webpack5.test.ts +++ b/code/lib/cli/src/automigrate/fixes/webpack5.test.ts @@ -132,6 +132,7 @@ describe('webpack5 fix', () => { }); }); describe('no webpack dependency', () => { + // @ts-expect-error (Type 'null' is not comparable) const packageManager = { getPackageVersion: () => { return null; diff --git a/code/lib/cli/src/automigrate/fixes/webpack5.ts b/code/lib/cli/src/automigrate/fixes/webpack5.ts index bcae50749a1e..8b54ee2d5a8c 100644 --- a/code/lib/cli/src/automigrate/fixes/webpack5.ts +++ b/code/lib/cli/src/automigrate/fixes/webpack5.ts @@ -8,7 +8,7 @@ import { updateMainConfig } from '../helpers/mainConfigFile'; const logger = console; interface Webpack5RunOptions { - webpackVersion: string; + webpackVersion: string | null; storybookVersion: string; } @@ -22,7 +22,7 @@ interface Webpack5RunOptions { * - Add core.builder = 'webpack5' to main.js * - Add 'webpack5' as a project dependency */ -export const webpack5: Fix = { +export const webpack5 = { id: 'webpack5', async check({ configDir, packageManager, mainConfig, storybookVersion }) { @@ -75,9 +75,9 @@ export const webpack5: Fix = { logger.info('✅ Setting `core.builder` to `@storybook/builder-webpack5` in main.js'); if (!dryRun) { - await updateMainConfig({ mainConfigPath, dryRun }, async (main) => { + await updateMainConfig({ mainConfigPath, dryRun: !!dryRun }, async (main) => { main.setFieldValue(['core', 'builder'], '@storybook/builder-webpack5'); }); } }, -}; +} satisfies Fix; 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 c1a33b3119f6..99bf4b8d550c 100644 --- a/code/lib/cli/src/automigrate/fixes/wrap-require-utils.ts +++ b/code/lib/cli/src/automigrate/fixes/wrap-require-utils.ts @@ -85,7 +85,7 @@ export function isRequireWrapperNecessary( if ( t.isArrayExpression(node) && - node.elements.some((element) => isRequireWrapperNecessary(element)) + node.elements.some((element) => element && isRequireWrapperNecessary(element)) ) { cb(node); return true; @@ -164,11 +164,11 @@ export function wrapValueWithRequireWrapper(config: ConfigFile, node: t.Node) { isRequireWrapperNecessary(node, (n) => { if (t.isStringLiteral(n)) { const wrapperNode = getReferenceToRequireWrapper(config, n.value); - Object.keys(n).forEach((k: keyof typeof n) => { - delete n[k]; + Object.keys(n).forEach((k) => { + delete n[k as keyof typeof n]; }); - Object.keys(wrapperNode).forEach((k: keyof typeof wrapperNode) => { - (n as any)[k] = wrapperNode[k]; + Object.keys(wrapperNode).forEach((k) => { + (n as any)[k] = wrapperNode[k as keyof typeof wrapperNode]; }); } diff --git a/code/lib/cli/src/automigrate/fixes/wrap-require.ts b/code/lib/cli/src/automigrate/fixes/wrap-require.ts index 509b51bae989..3651eb2c57a8 100644 --- a/code/lib/cli/src/automigrate/fixes/wrap-require.ts +++ b/code/lib/cli/src/automigrate/fixes/wrap-require.ts @@ -26,6 +26,10 @@ export const wrapRequire: Fix = { const isStorybookInMonorepo = await packageManager.isStorybookInMonorepo(); const isPnp = await detectPnp(); + if (!mainConfigPath) { + return null; + } + const config = await readConfig(mainConfigPath); if (!isStorybookInMonorepo && !isPnp) { @@ -54,7 +58,7 @@ export const wrapRequire: Fix = { async run({ dryRun, mainConfigPath, result }) { return new Promise((resolve, reject) => { - updateMainConfig({ dryRun, mainConfigPath }, (mainConfig) => { + updateMainConfig({ dryRun: !!dryRun, mainConfigPath }, (mainConfig) => { try { getFieldsForRequireWrapper(mainConfig).forEach((node) => { wrapValueWithRequireWrapper(mainConfig, node); @@ -62,10 +66,10 @@ export const wrapRequire: Fix = { if (getRequireWrapperName(mainConfig) === null) { if ( - mainConfig.fileName.endsWith('.cjs') || - mainConfig.fileName.endsWith('.cts') || - mainConfig.fileName.endsWith('.cjsx') || - mainConfig.fileName.endsWith('.ctsx') + mainConfig?.fileName?.endsWith('.cjs') || + mainConfig?.fileName?.endsWith('.cts') || + mainConfig?.fileName?.endsWith('.cjsx') || + mainConfig?.fileName?.endsWith('.ctsx') ) { mainConfig.setRequireImport(['dirname', 'join'], 'path'); } else { diff --git a/code/lib/cli/src/automigrate/helpers/checkWebpack5Builder.test.ts b/code/lib/cli/src/automigrate/helpers/checkWebpack5Builder.test.ts index 89428a48fb2d..06ae7a3d251c 100644 --- a/code/lib/cli/src/automigrate/helpers/checkWebpack5Builder.test.ts +++ b/code/lib/cli/src/automigrate/helpers/checkWebpack5Builder.test.ts @@ -44,6 +44,7 @@ describe('checkWebpack5Builder', () => { it('should return null and log a warning if mainConfig is missing', async () => { const result = await checkWebpack5Builder({ + // @ts-expect-error (Type 'undefined' is not assignable to type 'StorybookConfigRaw'.) mainConfig: undefined, storybookVersion: '6.3.0', }); diff --git a/code/lib/cli/src/automigrate/helpers/eslintPlugin.ts b/code/lib/cli/src/automigrate/helpers/eslintPlugin.ts index 548b0856a74c..992f54d69120 100644 --- a/code/lib/cli/src/automigrate/helpers/eslintPlugin.ts +++ b/code/lib/cli/src/automigrate/helpers/eslintPlugin.ts @@ -47,7 +47,10 @@ export async function extractEslintInfo(packageManager: JsPackageManager): Promi return { hasEslint, isStorybookPluginInstalled, eslintConfigFile }; } -export async function configureEslintPlugin(eslintFile: string, packageManager: JsPackageManager) { +export async function configureEslintPlugin( + eslintFile: string | undefined, + packageManager: JsPackageManager +) { if (eslintFile) { paddedLog(`Configuring Storybook ESLint plugin at ${eslintFile}`); if (eslintFile.endsWith('json')) { @@ -55,7 +58,10 @@ export async function configureEslintPlugin(eslintFile: string, packageManager: const existingConfigValue = Array.isArray(eslintConfig.extends) ? eslintConfig.extends : [eslintConfig.extends].filter(Boolean); - eslintConfig.extends = [...(existingConfigValue || []), 'plugin:storybook/recommended']; + eslintConfig.extends = [ + ...(existingConfigValue || []), + 'plugin:storybook/recommended', + ] as string[]; const eslintFileContents = await readFile(eslintFile, 'utf8'); const spaces = detectIndent(eslintFileContents).amount || 2; diff --git a/code/lib/cli/src/automigrate/helpers/getMigrationSummary.ts b/code/lib/cli/src/automigrate/helpers/getMigrationSummary.ts index 583d1b9170ee..e23c29dcea4d 100644 --- a/code/lib/cli/src/automigrate/helpers/getMigrationSummary.ts +++ b/code/lib/cli/src/automigrate/helpers/getMigrationSummary.ts @@ -57,8 +57,8 @@ export function getMigrationSummary({ }: { fixResults: Record; fixSummary: FixSummary; - installationMetadata: InstallationMetadata; - logFile?: string; + installationMetadata?: InstallationMetadata | null; + logFile: string; }) { const messages = []; messages.push(getGlossaryMessages(fixSummary, fixResults, logFile).join(messageDivider)); @@ -75,7 +75,9 @@ export function getMigrationSummary({ And reach out on Discord if you need help: ${chalk.yellow('https://discord.gg/storybook')} `); - const duplicatedDepsMessage = getDuplicatedDepsWarnings(installationMetadata); + const duplicatedDepsMessage = installationMetadata + ? getDuplicatedDepsWarnings(installationMetadata) + : getDuplicatedDepsWarnings(); if (duplicatedDepsMessage) { messages.push(duplicatedDepsMessage.join(messageDivider)); diff --git a/code/lib/cli/src/automigrate/helpers/mainConfigFile.test.ts b/code/lib/cli/src/automigrate/helpers/mainConfigFile.test.ts index f31ca41f0a0f..1d1d4c2095ac 100644 --- a/code/lib/cli/src/automigrate/helpers/mainConfigFile.test.ts +++ b/code/lib/cli/src/automigrate/helpers/mainConfigFile.test.ts @@ -9,6 +9,7 @@ describe('getBuilderPackageName', () => { const packageName = getBuilderPackageName(undefined); expect(packageName).toBeNull(); + // @ts-expect-error (Argument of type 'null' is not assignable) const packageName2 = getBuilderPackageName(null); expect(packageName2).toBeNull(); }); @@ -76,6 +77,7 @@ describe('getFrameworkPackageName', () => { const packageName = getFrameworkPackageName(undefined); expect(packageName).toBeNull(); + // @ts-expect-error (Argument of type 'null' is not assignable) const packageName2 = getFrameworkPackageName(null); expect(packageName2).toBeNull(); }); @@ -132,6 +134,7 @@ describe('getFrameworkPackageName', () => { describe('getRendererPackageNameFromFramework', () => { it('should return null when given no package name', () => { + // @ts-expect-error (Argument of type 'undefined' is not assignable) const packageName = getRendererPackageNameFromFramework(undefined); expect(packageName).toBeNull(); }); diff --git a/code/lib/cli/src/automigrate/helpers/mainConfigFile.ts b/code/lib/cli/src/automigrate/helpers/mainConfigFile.ts index 188e9bd9d99b..2dd9de625704 100644 --- a/code/lib/cli/src/automigrate/helpers/mainConfigFile.ts +++ b/code/lib/cli/src/automigrate/helpers/mainConfigFile.ts @@ -84,7 +84,7 @@ export const getStorybookData = async ({ configDir: userDefinedConfigDir, }: { packageManager: JsPackageManager; - configDir: string; + configDir?: string; }) => { const packageJson = await packageManager.retrievePackageJson(); const { @@ -102,7 +102,7 @@ export const getStorybookData = async ({ mainConfig = (await loadMainConfig({ configDir, noCache: true })) as StorybookConfigRaw; } catch (err) { throw new Error( - dedent`Unable to find or evaluate ${chalk.blue(mainConfigPath)}: ${err.message}` + dedent`Unable to find or evaluate ${chalk.blue(mainConfigPath)}: ${String(err)}` ); } @@ -178,5 +178,5 @@ export const getAddonNames = (mainConfig: StorybookConfig): string[] => { .replace(/\/preset$/, ''); }); - return addonList.filter(Boolean); + return addonList.filter((item): item is NonNullable => item != null); }; 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 3f7348e0c7a5..e5fba48f2d71 100644 --- a/code/lib/cli/src/automigrate/helpers/new-frameworks-utils.ts +++ b/code/lib/cli/src/automigrate/helpers/new-frameworks-utils.ts @@ -71,13 +71,13 @@ export const detectBuilderInfo = async ({ packageManager: JsPackageManager; }): Promise<{ name: BuilderType; options: any }> => { let builderName: BuilderType; - let builderOrFrameworkName; + let builderOrFrameworkName: string | undefined; const { core = {}, framework } = mainConfig; const { builder } = core; const builderPackageName = getBuilderPackageName(mainConfig); - const frameworkPackageName = getFrameworkPackageName(mainConfig); + const frameworkPackageName = getFrameworkPackageName(mainConfig) as string; let builderOptions = typeof builder !== 'string' ? builder?.options ?? {} : {}; @@ -133,12 +133,12 @@ export const detectBuilderInfo = async ({ if ( builderOrFrameworkName?.includes('vite') || - communityFrameworks.vite.includes(builderOrFrameworkName) + (builderOrFrameworkName && communityFrameworks.vite.includes(builderOrFrameworkName)) ) { builderName = 'vite'; } else if ( builderOrFrameworkName?.includes('webpack') || - communityFrameworks.webpack5.includes(builderOrFrameworkName) + (builderOrFrameworkName && communityFrameworks.webpack5.includes(builderOrFrameworkName)) ) { builderName = 'webpack5'; } else { @@ -154,7 +154,7 @@ export const detectBuilderInfo = async ({ }; }; -export const getNextjsAddonOptions = (addons: Preset[]) => { +export const getNextjsAddonOptions = (addons: Preset[] | undefined) => { const nextjsAddon = addons?.find((addon) => typeof addon === 'string' ? addon === 'storybook-addon-next' diff --git a/code/lib/cli/src/automigrate/index.ts b/code/lib/cli/src/automigrate/index.ts index 3e8e5af0e461..1ef8ec9ce1a2 100644 --- a/code/lib/cli/src/automigrate/index.ts +++ b/code/lib/cli/src/automigrate/index.ts @@ -8,6 +8,7 @@ import dedent from 'ts-dedent'; import { join } from 'path'; import { getStorybookInfo, loadMainConfig } from '@storybook/core-common'; +import invariant from 'tiny-invariant'; import { JsPackageManagerFactory, useNpmWarning } from '../js-package-manager'; import type { PackageManagerName } from '../js-package-manager'; @@ -63,8 +64,8 @@ export const automigrate = async ({ hideMigrationSummary = false, }: FixOptions = {}): Promise<{ fixResults: Record; - preCheckFailure: PreCheckFailure; -}> => { + preCheckFailure?: PreCheckFailure; +} | null> => { if (list) { logAvailableMigrations(); return null; @@ -181,7 +182,8 @@ export async function runFixes({ try { await loadMainConfig({ configDir }); } catch (err) { - if (err.message.includes('No configuration files have been found')) { + const errMessage = String(err); + if (errMessage.includes('No configuration files have been found')) { logger.info( dedent`[Storybook automigrate] Could not find or evaluate your Storybook main.js config directory at ${chalk.blue( configDir @@ -196,7 +198,7 @@ export async function runFixes({ logger.info( dedent`[Storybook automigrate] ❌ Failed trying to evaluate ${chalk.blue( mainConfigPath - )} with the following error: ${err.message}` + )} with the following error: ${errMessage}` ); logger.info('Please fix the error and try again.'); @@ -228,8 +230,10 @@ export async function runFixes({ }); } catch (error) { logger.info(`⚠️ failed to check fix ${chalk.bold(f.id)}`); - logger.error(`\n${error.stack}`); - fixSummary.failed[f.id] = error.message; + if (error instanceof Error) { + logger.error(`\n${error.stack}`); + fixSummary.failed[f.id] = error.message; + } fixResults[f.id] = FixStatus.CHECK_FAILED; } @@ -246,7 +250,7 @@ export async function runFixes({ }) ); - let runAnswer: { fix: boolean }; + let runAnswer: { fix: boolean } | undefined; try { if (dryRun) { @@ -303,8 +307,11 @@ export async function runFixes({ } if (!f.promptOnly) { + invariant(runAnswer, 'runAnswer must be defined if not promptOnly'); if (runAnswer.fix) { try { + invariant(typeof f.run === 'function', 'run method should be available in fix.'); + invariant(mainConfigPath, 'Main config path should be defined to run migration.'); await f.run({ result, packageManager, @@ -318,7 +325,8 @@ export async function runFixes({ fixSummary.succeeded.push(f.id); } catch (error) { fixResults[f.id] = FixStatus.FAILED; - fixSummary.failed[f.id] = error.message; + fixSummary.failed[f.id] = + error instanceof Error ? error.message : 'Failed to run migration'; logger.info(`❌ error when running ${chalk.cyan(f.id)} migration`); logger.info(error); diff --git a/code/lib/cli/src/automigrate/types.ts b/code/lib/cli/src/automigrate/types.ts index a2c124c09629..1befb24bf22c 100644 --- a/code/lib/cli/src/automigrate/types.ts +++ b/code/lib/cli/src/automigrate/types.ts @@ -15,14 +15,14 @@ export interface RunOptions { packageManager: JsPackageManager; result: ResultType; dryRun?: boolean; - mainConfigPath?: string; + mainConfigPath: string; skipInstall?: boolean; } export interface Fix { id: string; promptOnly?: boolean; - check: (options: CheckOptions) => Promise; + check: (options: CheckOptions) => Promise; prompt: (result: ResultType) => string; run?: (options: RunOptions) => Promise; } diff --git a/code/lib/cli/src/build.ts b/code/lib/cli/src/build.ts index 0cffa3a1e75c..1f5f423e3ba5 100644 --- a/code/lib/cli/src/build.ts +++ b/code/lib/cli/src/build.ts @@ -1,8 +1,11 @@ import { sync as readUpSync } from 'read-pkg-up'; import { buildStaticStandalone, withTelemetry } from '@storybook/core-server'; import { cache } from '@storybook/core-common'; +import invariant from 'tiny-invariant'; export const build = async (cliOptions: any) => { + const readUpResult = readUpSync({ cwd: __dirname }); + invariant(readUpResult, 'Failed to find the closest package.json file.'); const options = { ...cliOptions, configDir: cliOptions.configDir || './.storybook', @@ -10,7 +13,7 @@ export const build = async (cliOptions: any) => { ignorePreview: !!cliOptions.previewUrl && !cliOptions.forceBuildPreview, configType: 'PRODUCTION', cache, - packageJson: readUpSync({ cwd: __dirname }).packageJson, + packageJson: readUpResult.packageJson, }; await withTelemetry('build', { cliOptions, presetOptions: options }, () => buildStaticStandalone(options) diff --git a/code/lib/cli/src/detect.ts b/code/lib/cli/src/detect.ts index b62288bc82e5..dc02fbe4325b 100644 --- a/code/lib/cli/src/detect.ts +++ b/code/lib/cli/src/detect.ts @@ -44,7 +44,7 @@ const hasPeerDependency = ( return !!version; }; -type SearchTuple = [string, (version: string) => boolean | undefined]; +type SearchTuple = [string, ((version: string) => boolean) | undefined]; const getFrameworkPreset = ( packageJson: PackageJsonWithMaybeDeps, diff --git a/code/lib/cli/src/dev.ts b/code/lib/cli/src/dev.ts index 216a15595a73..ee82ae2cb7c4 100644 --- a/code/lib/cli/src/dev.ts +++ b/code/lib/cli/src/dev.ts @@ -4,6 +4,7 @@ import { logger, instance as npmLog } from '@storybook/node-logger'; import { buildDevStandalone, withTelemetry } from '@storybook/core-server'; import { cache } from '@storybook/core-common'; import type { CLIOptions } from '@storybook/types'; +import invariant from 'tiny-invariant'; function printError(error: any) { // this is a weird bugfix, somehow 'node-pre-gyp' is polluting the npmLog header @@ -39,13 +40,15 @@ function printError(error: any) { export const dev = async (cliOptions: CLIOptions) => { process.env.NODE_ENV = process.env.NODE_ENV || 'development'; + const readUpResult = readUpSync({ cwd: __dirname }); + invariant(readUpResult, 'Failed to find the closest package.json file.'); const options = { ...cliOptions, configDir: cliOptions.configDir || './.storybook', configType: 'DEVELOPMENT', ignorePreview: !!cliOptions.previewUrl && !cliOptions.forceBuildPreview, cache, - packageJson: readUpSync({ cwd: __dirname }).packageJson, + packageJson: readUpSync({ cwd: __dirname })?.packageJson, } as Parameters[0]; await withTelemetry( diff --git a/code/lib/cli/src/dirs.ts b/code/lib/cli/src/dirs.ts index 1dfc1045b6f5..554b003a97a3 100644 --- a/code/lib/cli/src/dirs.ts +++ b/code/lib/cli/src/dirs.ts @@ -4,6 +4,7 @@ import downloadTarball from '@ndelangen/get-tarball'; import getNpmTarballUrl from 'get-npm-tarball-url'; import * as tempy from 'tempy'; +import invariant from 'tiny-invariant'; import { externalFrameworks } from './project_types'; import type { SupportedFrameworks, SupportedRenderers } from './project_types'; import type { JsPackageManager } from './js-package-manager'; @@ -49,12 +50,14 @@ export async function getRendererDir( }) ); } catch (e) { + invariant(e instanceof Error); errors.push(e); } try { return await resolveUsingBranchInstall(packageManager, frameworkPackageName); } catch (e) { + invariant(e instanceof Error); errors.push(e); } diff --git a/code/lib/cli/src/doctor/getDuplicatedDepsWarnings.ts b/code/lib/cli/src/doctor/getDuplicatedDepsWarnings.ts index 0015798988de..3c72d6c21bc0 100644 --- a/code/lib/cli/src/doctor/getDuplicatedDepsWarnings.ts +++ b/code/lib/cli/src/doctor/getDuplicatedDepsWarnings.ts @@ -44,6 +44,7 @@ export function getDuplicatedDepsWarnings( ): string[] | undefined { try { if ( + !installationMetadata || !installationMetadata?.duplicatedDependencies || Object.keys(installationMetadata.duplicatedDependencies).length === 0 ) { diff --git a/code/lib/cli/src/doctor/getIncompatibleAddons.ts b/code/lib/cli/src/doctor/getIncompatibleAddons.ts index 135865f949c7..465ad661a892 100644 --- a/code/lib/cli/src/doctor/getIncompatibleAddons.ts +++ b/code/lib/cli/src/doctor/getIncompatibleAddons.ts @@ -1,4 +1,5 @@ import type { StorybookConfig } from '@storybook/types'; +import type { SemVer } from 'semver'; import semver from 'semver'; import { getAddonNames } from '../automigrate/helpers/mainConfigFile'; import { JsPackageManagerFactory } from '../js-package-manager'; @@ -36,7 +37,9 @@ export const getIncompatibleAddons = async ( '@storybook/addon-queryparams': '6.2.9', }; - const addons = getAddonNames(mainConfig).filter((addon) => addon in incompatibleList); + const addons = getAddonNames(mainConfig).filter( + (addon): addon is keyof typeof incompatibleList => addon in incompatibleList + ); const dependencies = await packageManager.getAllDependencies(); const storybookPackages = Object.keys(dependencies).filter((dep) => dep.includes('storybook')); @@ -59,11 +62,15 @@ export const getIncompatibleAddons = async ( const incompatibleAddons: { name: string; version: string }[] = []; addonVersions.forEach(({ name, version: installedVersion }) => { - if (installedVersion === null) return; + if (installedVersion === null) { + return; + } const addonVersion = incompatibleList[name]; try { - if (semver.lte(semver.coerce(installedVersion), semver.coerce(addonVersion))) { + if ( + semver.lte(semver.coerce(installedVersion) as SemVer, semver.coerce(addonVersion) as SemVer) + ) { incompatibleAddons.push({ name, version: installedVersion }); } } catch (err) { diff --git a/code/lib/cli/src/doctor/getMismatchingVersionsWarning.ts b/code/lib/cli/src/doctor/getMismatchingVersionsWarning.ts index 9aa0d424e01d..68e93f491075 100644 --- a/code/lib/cli/src/doctor/getMismatchingVersionsWarning.ts +++ b/code/lib/cli/src/doctor/getMismatchingVersionsWarning.ts @@ -4,7 +4,10 @@ import { frameworkPackages } from '@storybook/core-common'; import type { InstallationMetadata } from '../js-package-manager/types'; import storybookCorePackages from '../versions'; -function getPrimaryVersion(name: string, installationMetadata?: InstallationMetadata) { +function getPrimaryVersion(name: string | undefined, installationMetadata?: InstallationMetadata) { + if (!name) { + return undefined; + } const packageMetadata = installationMetadata?.dependencies[name]; if (!packageMetadata) { return undefined; @@ -17,14 +20,20 @@ export function getMismatchingVersionsWarnings( installationMetadata?: InstallationMetadata, allDependencies?: Record ): string | undefined { + if (!installationMetadata) { + return undefined; + } + const messages: string[] = []; try { - const frameworkPackageName = Object.keys(installationMetadata?.dependencies).find( + const frameworkPackageName = Object.keys(installationMetadata?.dependencies || []).find( (packageName) => { return Object.keys(frameworkPackages).includes(packageName); } ); - const cliVersion = getPrimaryVersion('@storybook/cli', installationMetadata); + const cliVersion = + getPrimaryVersion('@storybook/cli', installationMetadata) || + getPrimaryVersion('storybook', installationMetadata); const frameworkVersion = getPrimaryVersion(frameworkPackageName, installationMetadata); if (!cliVersion || !frameworkVersion || semver.eq(cliVersion, frameworkVersion)) { @@ -41,7 +50,7 @@ export function getMismatchingVersionsWarnings( let packageToDisplay: string; if (semver.lt(cliVersion, frameworkVersion)) { versionToCompare = frameworkVersion; - packageToDisplay = frameworkPackageName; + packageToDisplay = frameworkPackageName as string; } else { versionToCompare = cliVersion; packageToDisplay = 'storybook'; @@ -53,7 +62,7 @@ export function getMismatchingVersionsWarnings( )} (from the ${chalk.cyan(packageToDisplay)} package) or higher.` ); - const filteredDependencies = Object.entries(installationMetadata?.dependencies).filter( + const filteredDependencies = Object.entries(installationMetadata?.dependencies || []).filter( ([name, packages]) => { if (Object.keys(storybookCorePackages).includes(name)) { const packageVersion = packages[0].version; @@ -65,15 +74,20 @@ export function getMismatchingVersionsWarnings( ); if (filteredDependencies.length > 0) { + const packageJsonSuffix = '(in your package.json)'; messages.push( `Based on your lockfile, these dependencies should be upgraded:`, filteredDependencies .map( ([name, dep]) => `${chalk.hex('#ff9800')(name)}: ${dep[0].version} ${ - allDependencies[name] ? '(in your package.json)' : '' + allDependencies?.[name] ? packageJsonSuffix : '' }` ) + .sort( + (a, b) => + (b.includes(packageJsonSuffix) ? 1 : 0) - (a.includes(packageJsonSuffix) ? 1 : 0) + ) .join('\n') ); } @@ -82,9 +96,9 @@ export function getMismatchingVersionsWarnings( `You can run ${chalk.cyan( 'npx storybook@latest upgrade' )} to upgrade all of your Storybook packages to the latest version. - + Alternatively you can try manually changing the versions to match in your package.json. We also recommend regenerating your lockfile, or running the following command to possibly deduplicate your Storybook package versions: ${chalk.cyan( - installationMetadata.dedupeCommand + installationMetadata?.dedupeCommand )}` ); diff --git a/code/lib/cli/src/doctor/index.ts b/code/lib/cli/src/doctor/index.ts index cfafb899f3a4..a6a304ce1ea6 100644 --- a/code/lib/cli/src/doctor/index.ts +++ b/code/lib/cli/src/doctor/index.ts @@ -65,7 +65,7 @@ export const doctor = async ({ }); storybookVersion = storybookData.storybookVersion; mainConfig = storybookData.mainConfig; - } catch (err) { + } catch (err: any) { if (err.message.includes('No configuration files have been found')) { logger.info( dedent`[Storybook doctor] Could not find or evaluate your Storybook main.js config directory at ${chalk.blue( @@ -85,6 +85,10 @@ export const doctor = async ({ process.exit(1); } + if (!mainConfig) { + throw new Error('mainConfig is undefined'); + } + const incompatibleAddonList = await getIncompatibleAddons(mainConfig); if (incompatibleAddonList.length > 0) { diagnosticMessages.push(incompatibleAddons.prompt({ incompatibleAddonList })); @@ -95,7 +99,7 @@ export const doctor = async ({ 'storybook', ]); - const allDependencies = await packageManager.getAllDependencies(); + const allDependencies = (await packageManager.getAllDependencies()) as Record; const mismatchingVersionMessage = getMismatchingVersionsWarnings( installationMetadata, allDependencies @@ -103,7 +107,12 @@ export const doctor = async ({ if (mismatchingVersionMessage) { diagnosticMessages.push(mismatchingVersionMessage); } else { - diagnosticMessages.push(getDuplicatedDepsWarnings(installationMetadata)?.join('\n')); + const list = installationMetadata + ? getDuplicatedDepsWarnings(installationMetadata) + : getDuplicatedDepsWarnings(); + if (list) { + diagnosticMessages.push(list?.join('\n')); + } } logger.info(); diff --git a/code/lib/cli/src/generate.ts b/code/lib/cli/src/generate.ts index c1572af2c132..bc396ec428b7 100644 --- a/code/lib/cli/src/generate.ts +++ b/code/lib/cli/src/generate.ts @@ -7,6 +7,7 @@ import { sync as readUpSync } from 'read-pkg-up'; import { logger } from '@storybook/node-logger'; import { addToGlobalContext } from '@storybook/telemetry'; +import invariant from 'tiny-invariant'; import type { CommandOptions } from './generators/types'; import { initiate } from './initiate'; import { add } from './add'; @@ -25,7 +26,9 @@ import { doctor } from './doctor'; addToGlobalContext('cliVersion', versions.storybook); -const pkg = readUpSync({ cwd: __dirname }).packageJson; +const readUpResult = readUpSync({ cwd: __dirname }); +invariant(readUpResult, 'Failed to find the closest package.json file.'); +const pkg = readUpResult.packageJson; const consoleLogger = console; const command = (name: string) => diff --git a/code/lib/cli/src/generators/ANGULAR/index.ts b/code/lib/cli/src/generators/ANGULAR/index.ts index c8c7f3f288b3..4a368e8d0af3 100644 --- a/code/lib/cli/src/generators/ANGULAR/index.ts +++ b/code/lib/cli/src/generators/ANGULAR/index.ts @@ -42,7 +42,7 @@ const generator: Generator<{ projectName: string }> = async ( const { root, projectType } = angularProject; const { projects } = angularJSON; - const useCompodoc = commandOptions.yes ? true : await promptForCompoDocs(); + const useCompodoc = commandOptions?.yes ? true : await promptForCompoDocs(); const storybookFolder = root ? `${root}/.storybook` : '.storybook'; angularJSON.addStorybookEntries({ diff --git a/code/lib/cli/src/generators/REACT_NATIVE/index.ts b/code/lib/cli/src/generators/REACT_NATIVE/index.ts index dc3e14ed0f7e..e3b8dcfa50c0 100644 --- a/code/lib/cli/src/generators/REACT_NATIVE/index.ts +++ b/code/lib/cli/src/generators/REACT_NATIVE/index.ts @@ -34,12 +34,13 @@ const generator = async ( const babelDependencies = await getBabelDependencies(packageManager, packageJson); - const packages = [ - ...babelDependencies, - ...packagesWithFixedVersion, - ...resolvedPackages, - missingReactDom && reactVersion && `react-dom@${reactVersion}`, - ].filter(Boolean); + const packages: string[] = []; + packages.push(...babelDependencies); + packages.push(...packagesWithFixedVersion); + packages.push(...resolvedPackages); + if (missingReactDom && reactVersion) { + packages.push(`react-dom@${reactVersion}`); + } await packageManager.addDependencies({ ...npmOptions, packageJson }, packages); packageManager.addScripts({ diff --git a/code/lib/cli/src/generators/baseGenerator.ts b/code/lib/cli/src/generators/baseGenerator.ts index 6083fae9e873..813ba2d1dd59 100644 --- a/code/lib/cli/src/generators/baseGenerator.ts +++ b/code/lib/cli/src/generators/baseGenerator.ts @@ -2,6 +2,7 @@ import path from 'path'; import fse from 'fs-extra'; import { dedent } from 'ts-dedent'; import ora from 'ora'; +import invariant from 'tiny-invariant'; import type { NpmOptions } from '../NpmOptions'; import type { SupportedRenderers, SupportedFrameworks, Builder } from '../project_types'; import { SupportedLanguage, externalFrameworks, CoreBuilder } from '../project_types'; @@ -52,7 +53,7 @@ const getBuilderDetails = (builder: string) => { return builder; }; -const getExternalFramework = (framework: string) => +const getExternalFramework = (framework?: string) => externalFrameworks.find( (exFramework) => framework !== undefined && @@ -61,7 +62,7 @@ const getExternalFramework = (framework: string) => exFramework?.frameworks?.some?.((item) => item === framework)) ); -const getFrameworkPackage = (framework: string, renderer: string, builder: string) => { +const getFrameworkPackage = (framework: string | undefined, renderer: string, builder: string) => { const externalFramework = getExternalFramework(framework); const storybookBuilder = builder?.replace(/^@storybook\/builder-/, ''); const storybookFramework = framework?.replace(/^@storybook\//, ''); @@ -92,7 +93,7 @@ const getFrameworkPackage = (framework: string, renderer: string, builder: strin return externalFramework.packageName; }; -const getRendererPackage = (framework: string, renderer: string) => { +const getRendererPackage = (framework: string | undefined, renderer: string) => { const externalFramework = getExternalFramework(framework); if (externalFramework !== undefined) return externalFramework.renderer || externalFramework.packageName; @@ -118,12 +119,13 @@ const getFrameworkDetails = ( rendererId: SupportedRenderers; } => { const frameworkPackage = getFrameworkPackage(framework, renderer, builder); + invariant(frameworkPackage, 'Missing framework package.'); const frameworkPackagePath = shouldApplyRequireWrapperOnPackageNames ? applyRequireWrapper(frameworkPackage) : frameworkPackage; - const rendererPackage = getRendererPackage(framework, renderer); + const rendererPackage = getRendererPackage(framework, renderer) as string; const rendererPackagePath = shouldApplyRequireWrapperOnPackageNames ? applyRequireWrapper(rendererPackage) : rendererPackage; @@ -170,7 +172,7 @@ const hasInteractiveStories = (rendererId: SupportedRenderers) => ); const hasFrameworkTemplates = (framework?: SupportedFrameworks) => - ['angular', 'nextjs'].includes(framework); + framework ? ['angular', 'nextjs'].includes(framework) : false; export async function baseGenerator( packageManager: JsPackageManager, @@ -232,7 +234,7 @@ export async function baseGenerator( ...options, }; - const swc = useSWC({ builder }); + const swc = useSWC ? useSWC({ builder }) : false; if (swc) { skipBabel = true; @@ -241,8 +243,8 @@ export async function baseGenerator( const extraAddonsToInstall = typeof extraAddonPackages === 'function' ? await extraAddonPackages({ - builder: builder || builderInclude, - framework: framework || frameworkInclude, + builder: (builder || builderInclude) as string, + framework: (framework || frameworkInclude) as string, }) : extraAddonPackages; @@ -250,7 +252,7 @@ export async function baseGenerator( const addons = [ '@storybook/addon-links', '@storybook/addon-essentials', - ...stripVersions(extraAddonsToInstall), + ...stripVersions(extraAddonsToInstall || []), ].filter(Boolean); // added to package.json @@ -258,7 +260,7 @@ export async function baseGenerator( '@storybook/addon-links', '@storybook/addon-essentials', '@storybook/blocks', - ...extraAddonsToInstall, + ...(extraAddonsToInstall || []), ].filter(Boolean); if (hasInteractiveStories(rendererId)) { @@ -303,8 +305,8 @@ export async function baseGenerator( const extraPackagesToInstall = typeof extraPackages === 'function' ? await extraPackages({ - builder: builder || builderInclude, - framework: framework || frameworkInclude, + builder: (builder || builderInclude) as string, + framework: (framework || frameworkInclude) as string, }) : extraPackages; @@ -313,11 +315,12 @@ export async function baseGenerator( getExternalFramework(rendererId) ? undefined : `@storybook/${rendererId}`, ...frameworkPackages, ...addonPackages, - ...extraPackagesToInstall, + ...(extraPackagesToInstall || []), ].filter(Boolean); const packages = [...new Set(allPackages)].filter( - (packageToInstall) => !installedDependencies.has(getPackageDetails(packageToInstall)[0]) + (packageToInstall) => + !installedDependencies.has(getPackageDetails(packageToInstall as string)[0]) ); logger.log(); @@ -325,7 +328,7 @@ export async function baseGenerator( indent: 2, text: `Getting the correct version of ${packages.length} packages`, }).start(); - const versionedPackages = await packageManager.getVersionedPackages(packages); + const versionedPackages = await packageManager.getVersionedPackages(packages as string[]); versionedPackagesSpinner.succeed(); const depsToInstall = [...versionedPackages]; @@ -371,7 +374,7 @@ export async function baseGenerator( if (hasEslint && !isStorybookPluginInstalled) { if (skipPrompts || (await suggestESLintPlugin())) { depsToInstall.push('eslint-plugin-storybook'); - await configureEslintPlugin(eslintConfigFile, packageManager); + await configureEslintPlugin(eslintConfigFile ?? undefined, packageManager); } } } @@ -447,7 +450,7 @@ export async function baseGenerator( await configurePreview({ frameworkPreviewParts, - storybookConfigFolder, + storybookConfigFolder: storybookConfigFolder as string, language, rendererId, }); @@ -460,6 +463,9 @@ export async function baseGenerator( if (addComponents) { const templateLocation = hasFrameworkTemplates(framework) ? framework : rendererId; + if (!templateLocation) { + throw new Error(`Could not find template location for ${framework} or ${rendererId}`); + } await copyTemplateFiles({ renderer: templateLocation, packageManager, diff --git a/code/lib/cli/src/helpers.test.ts b/code/lib/cli/src/helpers.test.ts index 01897444db11..c15ae604a1a9 100644 --- a/code/lib/cli/src/helpers.test.ts +++ b/code/lib/cli/src/helpers.test.ts @@ -168,4 +168,13 @@ describe('Helpers', () => { }).toThrowError("Couldn't find any official storybook packages in package.json"); }); }); + + describe('coerceSemver', () => { + it(`should throw if the version argument is invalid semver string`, () => { + const invalidSemverString = 'hello, world'; + expect(() => { + helpers.coerceSemver(invalidSemverString); + }).toThrowError(`Could not coerce ${invalidSemverString} into a semver.`); + }); + }); }); diff --git a/code/lib/cli/src/helpers.ts b/code/lib/cli/src/helpers.ts index db5cfb34d1ac..605fab2d1849 100644 --- a/code/lib/cli/src/helpers.ts +++ b/code/lib/cli/src/helpers.ts @@ -3,10 +3,11 @@ import chalk from 'chalk'; import fs from 'fs'; import fse from 'fs-extra'; import path, { join } from 'path'; -import { satisfies } from 'semver'; +import { coerce, satisfies } from 'semver'; import stripJsonComments from 'strip-json-comments'; import findUp from 'find-up'; +import invariant from 'tiny-invariant'; import { getCliDir, getRendererDir } from './dirs'; import type { JsPackageManager, @@ -163,8 +164,14 @@ export function addToDevDependenciesIfNotPresent( name: string, packageVersion: string ) { - if (!packageJson.dependencies[name] && !packageJson.devDependencies[name]) { - packageJson.devDependencies[name] = packageVersion; + if (!packageJson.dependencies?.[name] && !packageJson.devDependencies?.[name]) { + if (packageJson.devDependencies) { + packageJson.devDependencies[name] = packageVersion; + } else { + packageJson.devDependencies = { + [name]: packageVersion, + }; + } } } @@ -287,11 +294,9 @@ export async function adjustTemplate(templatePath: string, templateData: Record< // 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 storybookPackage = Object.keys(allDeps).find( - (name: keyof typeof storybookMonorepoPackages) => { - return storybookMonorepoPackages[name]; - } - ); + const storybookPackage = Object.keys(allDeps).find((name: string) => { + return storybookMonorepoPackages[name as keyof typeof storybookMonorepoPackages]; + }); if (!storybookPackage) { throw new Error(`Couldn't find any official storybook packages in package.json`); @@ -303,3 +308,9 @@ export function getStorybookVersionSpecifier(packageJson: PackageJsonWithDepsAnd export async function isNxProject() { return findUp.sync('nx.json'); } + +export function coerceSemver(version: string) { + const coercedSemver = coerce(version); + invariant(coercedSemver != null, `Could not coerce ${version} into a semver.`); + return coercedSemver; +} diff --git a/code/lib/cli/src/initiate.ts b/code/lib/cli/src/initiate.ts index 3afd703a1e08..8811b88c5d85 100644 --- a/code/lib/cli/src/initiate.ts +++ b/code/lib/cli/src/initiate.ts @@ -9,6 +9,7 @@ import { NxProjectDetectedError } from '@storybook/core-events/server-errors'; import dedent from 'ts-dedent'; import boxen from 'boxen'; import { readdirSync } from 'fs-extra'; +import type { Builder } from './project_types'; import { installableProjectTypes, ProjectType } from './project_types'; import { detect, isStorybookInstantiated, detectLanguage, detectPnp } from './detect'; import { commandLog, codeLog, paddedLog } from './helpers'; @@ -53,10 +54,10 @@ const installStorybook = async ( const generatorOptions: GeneratorOptions = { language, - builder: options.builder, + builder: options.builder as Builder, linkable: !!options.linkable, - pnp: pnp || options.usePnp, - yes: options.yes, + pnp: pnp || (options.usePnp as boolean), + yes: options.yes as boolean, projectType, }; @@ -187,7 +188,7 @@ const installStorybook = async ( try { return await runGenerator(); - } catch (err) { + } catch (err: any) { if (err?.message !== 'Canceled by the user' && err?.stack) { logger.error(`\n ${chalk.red(err.stack)}`); } @@ -336,9 +337,9 @@ async function doInitiate( } } else { try { - projectType = await detect(packageManager, options); + projectType = (await detect(packageManager, options)) as ProjectType; } catch (err) { - done(err.message); + done(String(err)); throw new HandledError(err); } } @@ -430,7 +431,7 @@ export async function initiate(options: CommandOptions, pkg: PackageJson): Promi () => doInitiate(options, pkg) ); - if (initiateResult.shouldRunDev) { + if (initiateResult?.shouldRunDev) { const { projectType, packageManager, storybookCommand } = initiateResult; logger.log('\nRunning Storybook'); diff --git a/code/lib/cli/src/js-package-manager/JsPackageManager.ts b/code/lib/cli/src/js-package-manager/JsPackageManager.ts index 1cb3d93ba8c7..5a3e82be3c43 100644 --- a/code/lib/cli/src/js-package-manager/JsPackageManager.ts +++ b/code/lib/cli/src/js-package-manager/JsPackageManager.ts @@ -6,7 +6,8 @@ import path from 'path'; import fs from 'fs'; import dedent from 'ts-dedent'; -import { readFile, readFileSync, writeFile } from 'fs-extra'; +import { readFile, writeFile, readFileSync } from 'fs-extra'; +import invariant from 'tiny-invariant'; import { commandLog } from '../helpers'; import type { PackageJson, PackageJsonWithDepsAndDevDeps } from './PackageJson'; import storybookPackagesVersions from '../versions'; @@ -143,6 +144,9 @@ export abstract class JsPackageManager { } packageJsonPath(): string { + if (!this.cwd) { + throw new Error('Missing cwd'); + } return path.resolve(this.cwd, 'package.json'); } @@ -191,15 +195,14 @@ export abstract class JsPackageManager { try { packageJson = await this.readPackageJson(); } catch (err) { - if (err.message.includes('Could not read package.json')) { + const errMessage = String(err); + if (errMessage.includes('Could not read package.json')) { await this.initPackageJson(); packageJson = await this.readPackageJson(); } else { throw new Error( dedent` - There was an error while reading the package.json file at ${this.packageJsonPath()}: ${ - err.message - } + There was an error while reading the package.json file at ${this.packageJsonPath()}: ${errMessage} Please fix the error and try again. ` ); @@ -214,7 +217,7 @@ export abstract class JsPackageManager { }; } - public async getAllDependencies(): Promise> { + public async getAllDependencies(): Promise>> { const { dependencies, devDependencies, peerDependencies } = await this.retrievePackageJson(); return { @@ -249,6 +252,7 @@ export abstract class JsPackageManager { if (skipInstall) { const { packageJson } = options; + invariant(packageJson, 'Missing packageJson.'); const dependenciesMap = dependencies.reduce((acc, dep) => { const [packageName, packageVersion] = getPackageDetails(dep); @@ -269,8 +273,8 @@ export abstract class JsPackageManager { await this.writePackageJson(packageJson); } else { try { - await this.runAddDeps(dependencies, options.installAsDevDependencies); - } catch (e) { + await this.runAddDeps(dependencies, Boolean(options.installAsDevDependencies)); + } catch (e: any) { logger.error('\nAn error occurred while installing dependencies:'); logger.log(e.message); throw new HandledError(e); @@ -301,6 +305,7 @@ export abstract class JsPackageManager { if (skipInstall) { const { packageJson } = options; + invariant(packageJson, 'Missing packageJson.'); dependencies.forEach((dep) => { if (packageJson.devDependencies) { delete packageJson.devDependencies[dep]; @@ -316,7 +321,7 @@ export abstract class JsPackageManager { await this.runRemoveDeps(dependencies); } catch (e) { logger.error('An error occurred while removing dependencies.'); - logger.log(e.message); + logger.log(String(e)); throw new HandledError(e); } } @@ -360,7 +365,7 @@ export abstract class JsPackageManager { * @param constraint A valid semver constraint, example: '1.x || >=2.5.0 || 5.0.0 - 7.2.3' */ public async getVersion(packageName: string, constraint?: string): Promise { - let current: string; + let current: string | undefined; if (/(@storybook|^sb$|^storybook$)/.test(packageName)) { // @ts-expect-error (Converted from ts-ignore) @@ -372,11 +377,11 @@ export abstract class JsPackageManager { latest = await this.latestVersion(packageName, constraint); } catch (e) { if (current) { - logger.warn(`\n ${chalk.yellow(e.message)}`); + logger.warn(`\n ${chalk.yellow(String(e))}`); return current; } - logger.error(`\n ${chalk.red(e.message)}`); + logger.error(`\n ${chalk.red(String(e))}`); throw new HandledError(e); } @@ -401,8 +406,14 @@ export abstract class JsPackageManager { const versions = await this.runGetVersions(packageName, true); - // Get the latest version satisfying the constraint - return versions.reverse().find((version) => satisfies(version, constraint)); + const latestVersionSatisfyingTheConstraint = versions + .reverse() + .find((version) => satisfies(version, constraint)); + invariant( + latestVersionSatisfyingTheConstraint != null, + 'No version satisfying the constraint.' + ); + return latestVersionSatisfyingTheConstraint; } public async addStorybookCommandInScripts(options?: { port: number; preCommand?: string }) { diff --git a/code/lib/cli/src/js-package-manager/NPMProxy.ts b/code/lib/cli/src/js-package-manager/NPMProxy.ts index da6a83acec8b..a8db9a836047 100644 --- a/code/lib/cli/src/js-package-manager/NPMProxy.ts +++ b/code/lib/cli/src/js-package-manager/NPMProxy.ts @@ -5,6 +5,7 @@ import { sync as findUpSync } from 'find-up'; import { existsSync, readFileSync } from 'fs'; import path from 'path'; import semver from 'semver'; +import { logger } from '@storybook/node-logger'; import { JsPackageManager } from './JsPackageManager'; import type { PackageJson } from './PackageJson'; import type { InstallationMetadata, PackageMetadata } from './types'; @@ -136,22 +137,34 @@ export class NPMProxy extends JsPackageManager { } public async findInstallations() { - const pipeToNull = platform() === 'win32' ? '2>NUL' : '2>/dev/null'; - const commandResult = await this.executeCommand({ - command: 'npm', - args: ['ls', '--json', '--depth=99', pipeToNull], - // ignore errors, because npm ls will exit with code 1 if there are e.g. unmet peer dependencies - ignoreError: true, - env: { - FORCE_COLOR: 'false', - }, - }); + const exec = async ({ depth }: { depth: number }) => { + const pipeToNull = platform() === 'win32' ? '2>NUL' : '2>/dev/null'; + return this.executeCommand({ + command: 'npm', + args: ['ls', '--json', `--depth=${depth}`, pipeToNull], + env: { + FORCE_COLOR: 'false', + }, + }); + }; try { + const commandResult = await exec({ depth: 99 }); const parsedOutput = JSON.parse(commandResult); + return this.mapDependencies(parsedOutput); } catch (e) { - return undefined; + // when --depth is higher than 0, npm can return a non-zero exit code + // in case the user's project has peer dependency issues. So we try again with no depth + try { + const commandResult = await exec({ depth: 0 }); + const parsedOutput = JSON.parse(commandResult); + + return this.mapDependencies(parsedOutput); + } catch (err) { + logger.warn(`An issue occurred while trying to find dependencies metadata using npm.`); + return undefined; + } } } diff --git a/code/lib/cli/src/js-package-manager/PNPMProxy.ts b/code/lib/cli/src/js-package-manager/PNPMProxy.ts index 33571c0e4dda..765b5bd55063 100644 --- a/code/lib/cli/src/js-package-manager/PNPMProxy.ts +++ b/code/lib/cli/src/js-package-manager/PNPMProxy.ts @@ -137,7 +137,7 @@ export class PNPMProxy extends JsPackageManager { ); return packageJSON; - } catch (error) { + } catch (error: any) { if (error.code !== 'MODULE_NOT_FOUND') { console.error('Error while fetching package version in PNPM PnP mode:', error); } diff --git a/code/lib/cli/src/js-package-manager/Yarn2Proxy.ts b/code/lib/cli/src/js-package-manager/Yarn2Proxy.ts index b1780c474404..f69d7c8d0b3d 100644 --- a/code/lib/cli/src/js-package-manager/Yarn2Proxy.ts +++ b/code/lib/cli/src/js-package-manager/Yarn2Proxy.ts @@ -151,7 +151,7 @@ export class Yarn2Proxy extends JsPackageManager { const virtualPath = path.join(pkg.packageLocation, 'package.json'); return crossFs.readJsonSync(virtualPath); - } catch (error) { + } catch (error: any) { if (error.code !== 'MODULE_NOT_FOUND') { console.error('Error while fetching package version in Yarn PnP mode:', error); } diff --git a/code/lib/cli/src/js-package-manager/util.ts b/code/lib/cli/src/js-package-manager/util.ts index 26d4fe4a8979..ccf07c3f56a2 100644 --- a/code/lib/cli/src/js-package-manager/util.ts +++ b/code/lib/cli/src/js-package-manager/util.ts @@ -1,7 +1,10 @@ // input: @storybook/addon-essentials@npm:7.0.0 // output: { name: '@storybook/addon-essentials', value: { version : '7.0.0', location: '' } } export const parsePackageData = (packageName = '') => { - const [first, second, third] = packageName.trim().split('@'); + const [first, second, third] = packageName + .replace(/[└─├]+/g, '') + .trim() + .split('@'); const version = (third || second).replace('npm:', ''); const name = third ? `@${second}` : first; diff --git a/code/lib/cli/src/link.ts b/code/lib/cli/src/link.ts index 4531256c9960..3af08643874b 100644 --- a/code/lib/cli/src/link.ts +++ b/code/lib/cli/src/link.ts @@ -45,7 +45,9 @@ export const exec = async ( resolve(undefined); } else { logger.error(chalk.red(`An error occurred while executing: \`${command}\``)); - logger.info(errorMessage); + if (errorMessage) { + logger.info(errorMessage); + } reject(new Error(`command exited with code: ${code}: `)); } }); diff --git a/code/lib/cli/src/project_types.ts b/code/lib/cli/src/project_types.ts index 05c64b402423..8d2809f97379 100644 --- a/code/lib/cli/src/project_types.ts +++ b/code/lib/cli/src/project_types.ts @@ -2,12 +2,18 @@ import { minVersion, validRange } from 'semver'; function ltMajor(versionRange: string, major: number) { // Uses validRange to avoid a throw from minVersion if an invalid range gets passed - return validRange(versionRange) && minVersion(versionRange).major < major; + if (validRange(versionRange)) { + return (minVersion(versionRange)?.major ?? Infinity) < major; + } + return false; } function eqMajor(versionRange: string, major: number) { // Uses validRange to avoid a throw from minVersion if an invalid range gets passed - return validRange(versionRange) && minVersion(versionRange).major === major; + if (validRange(versionRange)) { + return minVersion(versionRange)?.major === major; + } + return false; } /** A list of all frameworks that are supported, but use a package outside the storybook monorepo */ @@ -124,7 +130,7 @@ export const supportedTemplates: TemplateConfiguration[] = [ vuetify: (versionRange) => ltMajor(versionRange, 3), }, matcherFunction: ({ dependencies }) => { - return dependencies.some(Boolean); + return dependencies?.some(Boolean) ?? false; }, }, { @@ -135,7 +141,7 @@ export const supportedTemplates: TemplateConfiguration[] = [ nuxt: (versionRange) => ltMajor(versionRange, 3), }, matcherFunction: ({ dependencies }) => { - return dependencies.some(Boolean); + return dependencies?.some(Boolean) ?? false; }, }, { @@ -145,42 +151,42 @@ export const supportedTemplates: TemplateConfiguration[] = [ vue: (versionRange) => versionRange === 'next' || eqMajor(versionRange, 3), }, matcherFunction: ({ dependencies }) => { - return dependencies.some(Boolean); + return dependencies?.some(Boolean) ?? false; }, }, { preset: ProjectType.EMBER, dependencies: ['ember-cli'], matcherFunction: ({ dependencies }) => { - return dependencies.every(Boolean); + return dependencies?.every(Boolean) ?? true; }, }, { preset: ProjectType.NEXTJS, dependencies: ['next'], matcherFunction: ({ dependencies }) => { - return dependencies.every(Boolean); + return dependencies?.every(Boolean) ?? true; }, }, { preset: ProjectType.QWIK, dependencies: ['@builder.io/qwik'], matcherFunction: ({ dependencies }) => { - return dependencies.every(Boolean); + return dependencies?.every(Boolean) ?? true; }, }, { preset: ProjectType.REACT_PROJECT, peerDependencies: ['react'], matcherFunction: ({ peerDependencies }) => { - return peerDependencies.every(Boolean); + return peerDependencies?.every(Boolean) ?? true; }, }, { preset: ProjectType.REACT_NATIVE, dependencies: ['react-native', 'react-native-scripts'], matcherFunction: ({ dependencies }) => { - return dependencies.some(Boolean); + return dependencies?.some(Boolean) ?? false; }, }, { @@ -190,28 +196,28 @@ export const supportedTemplates: TemplateConfiguration[] = [ // For standard CRA projects dependencies: ['react-scripts'], matcherFunction: ({ dependencies, files }) => { - return dependencies.every(Boolean) || files.every(Boolean); + return (dependencies?.every(Boolean) || files?.every(Boolean)) ?? false; }, }, { preset: ProjectType.ANGULAR, dependencies: ['@angular/core'], matcherFunction: ({ dependencies }) => { - return dependencies.every(Boolean); + return dependencies?.every(Boolean) ?? true; }, }, { preset: ProjectType.WEB_COMPONENTS, dependencies: ['lit-element', 'lit-html', 'lit'], matcherFunction: ({ dependencies }) => { - return dependencies.some(Boolean); + return dependencies?.some(Boolean) ?? false; }, }, { preset: ProjectType.PREACT, dependencies: ['preact'], matcherFunction: ({ dependencies }) => { - return dependencies.every(Boolean); + return dependencies?.every(Boolean) ?? true; }, }, { @@ -219,21 +225,21 @@ export const supportedTemplates: TemplateConfiguration[] = [ preset: ProjectType.SVELTEKIT, dependencies: ['@sveltejs/kit'], matcherFunction: ({ dependencies }) => { - return dependencies.every(Boolean); + return dependencies?.every(Boolean) ?? true; }, }, { preset: ProjectType.SVELTE, dependencies: ['svelte'], matcherFunction: ({ dependencies }) => { - return dependencies.every(Boolean); + return dependencies?.every(Boolean) ?? true; }, }, { preset: ProjectType.SOLID, dependencies: ['solid-js'], matcherFunction: ({ dependencies }) => { - return dependencies.every(Boolean); + return dependencies?.every(Boolean) ?? true; }, }, // DO NOT MOVE ANY TEMPLATES BELOW THIS LINE @@ -242,14 +248,14 @@ export const supportedTemplates: TemplateConfiguration[] = [ preset: ProjectType.WEBPACK_REACT, dependencies: ['react', 'webpack'], matcherFunction: ({ dependencies }) => { - return dependencies.every(Boolean); + return dependencies?.every(Boolean) ?? true; }, }, { preset: ProjectType.REACT, dependencies: ['react'], matcherFunction: ({ dependencies }) => { - return dependencies.every(Boolean); + return dependencies?.every(Boolean) ?? true; }, }, ]; @@ -264,7 +270,7 @@ export const unsupportedTemplate: TemplateConfiguration = { nuxt: (versionRange) => eqMajor(versionRange, 3), }, matcherFunction: ({ dependencies }) => { - return dependencies.some(Boolean); + return dependencies?.some(Boolean) ?? false; }, }; diff --git a/code/lib/cli/src/repro-generators/scripts.ts b/code/lib/cli/src/repro-generators/scripts.ts index 2aa59fc2ee40..17241f4b037f 100644 --- a/code/lib/cli/src/repro-generators/scripts.ts +++ b/code/lib/cli/src/repro-generators/scripts.ts @@ -47,7 +47,7 @@ type ExecOptions = globalThis.Parameters[2]; export interface Options extends Parameters { appName: string; creationPath: string; - cwd?: string; + cwd: string; e2e: boolean; pnp: boolean; } @@ -186,7 +186,7 @@ const addAdditionalFiles = async ({ additionalFiles, cwd }: Options) => { logger.info(`⤵️ Adding required files`); await Promise.all( - additionalFiles.map(async (file) => { + (additionalFiles ?? []).map(async (file) => { await outputFile(path.resolve(cwd, file.path), file.contents, { encoding: 'utf-8' }); }) ); diff --git a/code/lib/cli/src/sandbox-templates.ts b/code/lib/cli/src/sandbox-templates.ts index 86b28646fb10..6aeed1d585a3 100644 --- a/code/lib/cli/src/sandbox-templates.ts +++ b/code/lib/cli/src/sandbox-templates.ts @@ -107,16 +107,22 @@ const baseTemplates = { builder: '@storybook/builder-webpack5', }, }, - 'nextjs/12-js': { - name: 'Next.js v12 (Webpack | JavaScript)', + 'nextjs/13-ts': { + name: 'Next.js v13.5 (Webpack | TypeScript)', script: - 'yarn create next-app {{beforeDir}} -e https://github.com/vercel/next.js/tree/next-12-3-2/examples/hello-world && cd {{beforeDir}} && npm pkg set "dependencies.next"="^12.2.0" && yarn && git add . && git commit --amend --no-edit && cd ..', + 'yarn create next-app {{beforeDir}} -e https://github.com/vercel/next.js/tree/next-13/examples/hello-world && cd {{beforeDir}} && npm pkg set "dependencies.next"="^12.2.0" && yarn && git add . && git commit --amend --no-edit && cd ..', expected: { framework: '@storybook/nextjs', renderer: '@storybook/react', builder: '@storybook/builder-webpack5', }, + modifications: { + mainConfig: { + features: { experimentalNextRSC: true }, + }, + }, skipTasks: ['e2e-tests-dev', 'bench'], + inDevelopment: true, }, 'nextjs/default-js': { name: 'Next.js Latest (Webpack | JavaScript)', @@ -127,6 +133,11 @@ const baseTemplates = { renderer: '@storybook/react', builder: '@storybook/builder-webpack5', }, + modifications: { + mainConfig: { + features: { experimentalNextRSC: true }, + }, + }, skipTasks: ['e2e-tests-dev', 'bench'], }, 'nextjs/default-ts': { @@ -138,6 +149,11 @@ const baseTemplates = { renderer: '@storybook/react', builder: '@storybook/builder-webpack5', }, + modifications: { + mainConfig: { + features: { experimentalNextRSC: true }, + }, + }, skipTasks: ['e2e-tests-dev', 'bench'], }, 'nextjs/prerelease': { @@ -149,6 +165,11 @@ const baseTemplates = { renderer: '@storybook/react', builder: '@storybook/builder-webpack5', }, + modifications: { + mainConfig: { + features: { experimentalNextRSC: true }, + }, + }, skipTasks: ['e2e-tests-dev', 'bench'], }, 'react-vite/default-js': { @@ -599,7 +620,7 @@ export const daily: TemplateKey[] = [ 'lit-vite/default-js', 'svelte-kit/skeleton-js', 'svelte-vite/default-js', - 'nextjs/12-js', + 'nextjs/13-ts', 'nextjs/default-js', 'nextjs/prerelease', 'qwik-vite/default-ts', diff --git a/code/lib/cli/src/sandbox.ts b/code/lib/cli/src/sandbox.ts index 2938414adbfa..397fda11830f 100644 --- a/code/lib/cli/src/sandbox.ts +++ b/code/lib/cli/src/sandbox.ts @@ -6,6 +6,7 @@ import { dedent } from 'ts-dedent'; import { downloadTemplate } from 'giget'; import { existsSync, readdir } from 'fs-extra'; +import invariant from 'tiny-invariant'; import type { Template, TemplateKey } from './sandbox-templates'; import { allTemplates as TEMPLATES } from './sandbox-templates'; @@ -105,7 +106,7 @@ export const sandbox = async ({ return; } - selectedConfig = TEMPLATES[selectedTemplate]; + selectedConfig = selectedTemplate ? TEMPLATES[selectedTemplate] : undefined; if (!selectedConfig) { throw new Error('🚨 Sandbox: please specify a valid template type'); @@ -124,7 +125,7 @@ export const sandbox = async ({ type: 'text', message: 'Enter the output directory', name: 'directory', - initial: outputDirectoryName, + initial: outputDirectoryName ?? undefined, validate: async (directoryName) => existsSync(directoryName) ? `${directoryName} already exists. Please choose another name.` @@ -139,6 +140,7 @@ export const sandbox = async ({ ); selectedDirectory = directory; } + invariant(selectedDirectory); try { const templateDestination = path.isAbsolute(selectedDirectory) @@ -166,7 +168,7 @@ export const sandbox = async ({ ); } } catch (err) { - logger.error(`🚨 Failed to download sandbox template: ${err.message}`); + logger.error(`🚨 Failed to download sandbox template: ${String(err)}`); throw err; } diff --git a/code/lib/cli/src/upgrade.ts b/code/lib/cli/src/upgrade.ts index dd31f177df53..aefd6bc8bc07 100644 --- a/code/lib/cli/src/upgrade.ts +++ b/code/lib/cli/src/upgrade.ts @@ -6,7 +6,7 @@ import { withTelemetry } from '@storybook/core-server'; import type { PackageJsonWithMaybeDeps, PackageManagerName } from './js-package-manager'; import { getPackageDetails, JsPackageManagerFactory, useNpmWarning } from './js-package-manager'; -import { commandLog } from './helpers'; +import { coerceSemver, commandLog } from './helpers'; import { automigrate } from './automigrate'; import { isCorePackage } from './utils'; @@ -51,7 +51,7 @@ export const checkVersionConsistency = () => { .split('\n'); const storybookPackages = lines .map(getStorybookVersion) - .filter(Boolean) + .filter((item): item is NonNullable => !!item) .filter((pkg) => isCorePackage(pkg.package)); if (!storybookPackages.length) { logger.warn('No storybook core packages found.'); @@ -97,9 +97,9 @@ export const addExtraFlags = ( (acc, entry) => { const [pattern, extra] = entry; const [pkg, specifier] = getPackageDetails(pattern); - const pkgVersion = dependencies[pkg] || devDependencies[pkg]; + const pkgVersion = dependencies?.[pkg] || devDependencies?.[pkg]; - if (pkgVersion && semver.satisfies(semver.coerce(pkgVersion), specifier)) { + if (pkgVersion && specifier && semver.satisfies(coerceSemver(pkgVersion), specifier)) { return [...acc, ...extra]; } diff --git a/code/lib/cli/tsconfig.json b/code/lib/cli/tsconfig.json index b0f65014db55..4b4e13c55d49 100644 --- a/code/lib/cli/tsconfig.json +++ b/code/lib/cli/tsconfig.json @@ -2,10 +2,10 @@ "extends": "../../tsconfig.json", "compilerOptions": { "types": ["node", "jest"], - "strict": false, + "strict": true, "skipLibCheck": true, - "strictNullChecks": false, - "resolveJsonModule": true + "resolveJsonModule": true, + "noEmit": true }, "include": ["src/**/*"] } diff --git a/code/lib/codemod/package.json b/code/lib/codemod/package.json index d20fa00879b3..1e7650b6b268 100644 --- a/code/lib/codemod/package.json +++ b/code/lib/codemod/package.json @@ -65,7 +65,8 @@ "jscodeshift": "^0.15.1", "lodash": "^4.17.21", "prettier": "^2.8.0", - "recast": "^0.23.1" + "recast": "^0.23.1", + "tiny-invariant": "^1.3.1" }, "devDependencies": { "@types/jscodeshift": "^0.11.10", diff --git a/code/lib/codemod/src/transforms/__testfixtures__/mdx-to-csf/plaintext.input.mdx b/code/lib/codemod/src/transforms/__testfixtures__/mdx-to-csf/plaintext.input.mdx index 7c57125b7c2b..6068d657963b 100644 --- a/code/lib/codemod/src/transforms/__testfixtures__/mdx-to-csf/plaintext.input.mdx +++ b/code/lib/codemod/src/transforms/__testfixtures__/mdx-to-csf/plaintext.input.mdx @@ -1,3 +1,3 @@ import { Story, Meta } from '@storybook/addon-docs'; -Plain text; +Plain text; diff --git a/code/lib/codemod/src/transforms/__testfixtures__/mdx-to-csf/plaintext.output.snapshot b/code/lib/codemod/src/transforms/__testfixtures__/mdx-to-csf/plaintext.output.snapshot index 58ad1ca96af8..e45e137b4308 100644 --- a/code/lib/codemod/src/transforms/__testfixtures__/mdx-to-csf/plaintext.output.snapshot +++ b/code/lib/codemod/src/transforms/__testfixtures__/mdx-to-csf/plaintext.output.snapshot @@ -6,6 +6,6 @@ export default {}; export const Plaintext = () => 'Plain text'; Plaintext.story = { - name: 'plaintext', + name: 'text', };" `; diff --git a/code/lib/codemod/src/transforms/csf-2-to-3.ts b/code/lib/codemod/src/transforms/csf-2-to-3.ts index 945f3542b6e8..72f283df95f3 100644 --- a/code/lib/codemod/src/transforms/csf-2-to-3.ts +++ b/code/lib/codemod/src/transforms/csf-2-to-3.ts @@ -7,6 +7,7 @@ import { loadCsf, printCsf } from '@storybook/csf-tools'; import type { API, FileInfo } from 'jscodeshift'; import type { BabelFile, NodePath } from '@babel/core'; import * as babel from '@babel/core'; +import invariant from 'tiny-invariant'; import { upgradeDeprecatedTypes } from './upgrade-deprecated-types'; const logger = console; @@ -15,7 +16,7 @@ const renameAnnotation = (annotation: string) => { return annotation === 'storyName' ? 'name' : annotation; }; -const getTemplateBindVariable = (init: t.Expression) => +const getTemplateBindVariable = (init: t.Expression | undefined) => t.isCallExpression(init) && t.isMemberExpression(init.callee) && t.isIdentifier(init.callee.object) && @@ -92,7 +93,7 @@ function removeUnusedTemplates(csf: CsfFile) { const references: NodePath[] = []; babel.traverse(csf._ast, { Identifier: (path) => { - if (path.node.name === template) references.push(path); + if (path.node.name === template) references.push(path as NodePath); }, }); // if there is only one reference and this reference is the variable declaration initializing the template @@ -100,7 +101,7 @@ function removeUnusedTemplates(csf: CsfFile) { if (references.length === 1) { const reference = references[0]; if ( - reference.parentPath.isVariableDeclarator() && + reference.parentPath?.isVariableDeclarator() && reference.parentPath.node.init === templateExpression ) { reference.parentPath.remove(); @@ -124,6 +125,7 @@ export default function transform(info: FileInfo, api: API, options: { parser?: // This allows for showing buildCodeFrameError messages // @ts-expect-error File is not yet exposed, see https://github.com/babel/babel/issues/11350#issuecomment-644118606 + const file: BabelFile = new babel.File( { filename: info.path }, { code: info.source, ast: csf._ast } @@ -137,8 +139,9 @@ export default function transform(info: FileInfo, api: API, options: { parser?: return t.objectProperty(t.identifier(renameAnnotation(annotation)), val as t.Expression); }); - if (t.isVariableDeclarator(decl)) { - const { init, id } = decl; + if (t.isVariableDeclarator(decl as t.Node)) { + const { init, id } = decl as any; + invariant(init, 'Inital value should be declared'); // only replace arrow function expressions && template const template = getTemplateBindVariable(init); if (!t.isArrowFunctionExpression(init) && !template) return; @@ -152,10 +155,7 @@ export default function transform(info: FileInfo, api: API, options: { parser?: return; } - let storyFn: t.Expression = template && t.identifier(template); - if (!storyFn) { - storyFn = init; - } + const storyFn: t.Expression = template ? t.identifier(template) : init; // Remove the render function when we can hoist the template // const Template = (args) => ; @@ -178,8 +178,8 @@ export default function transform(info: FileInfo, api: API, options: { parser?: } }); - csf._ast.program.body = csf._ast.program.body.reduce((acc, stmt) => { - const statement = stmt as t.Statement; + csf._ast.program.body = csf._ast.program.body.reduce((acc: t.Statement[], stmt: t.Statement) => { + const statement = stmt; // remove story annotations & template declarations if (isStoryAnnotation(statement, objectExports)) { return acc; @@ -251,8 +251,23 @@ class StorybookImportHelper { } if (!specifier.isImportSpecifier()) return false; const imported = specifier.get('imported'); - if (!imported.isIdentifier()) return false; + if (Array.isArray(imported)) { + return imported.some((importedSpecifier) => { + if (!importedSpecifier.isIdentifier()) return false; + return [ + 'Story', + 'StoryFn', + 'StoryObj', + 'Meta', + 'ComponentStory', + 'ComponentStoryFn', + 'ComponentStoryObj', + 'ComponentMeta', + ].includes(importedSpecifier.node.name); + }); + } + if (!imported.isIdentifier()) return false; return [ 'Story', 'StoryFn', @@ -321,7 +336,7 @@ class StorybookImportHelper { ...id, typeAnnotation: t.tsTypeAnnotation( t.tsTypeReference( - t.identifier(localTypeImport), + t.identifier(localTypeImport ?? ''), id.typeAnnotation.typeAnnotation.typeParameters ) ), diff --git a/code/lib/codemod/src/transforms/mdx-to-csf.ts b/code/lib/codemod/src/transforms/mdx-to-csf.ts index 48e527f237f6..b5f68b8a7b50 100644 --- a/code/lib/codemod/src/transforms/mdx-to-csf.ts +++ b/code/lib/codemod/src/transforms/mdx-to-csf.ts @@ -1,4 +1,4 @@ -/* eslint-disable no-param-reassign,@typescript-eslint/no-shadow */ +/* eslint-disable @typescript-eslint/ban-ts-comment,no-param-reassign,@typescript-eslint/no-shadow */ import type { FileInfo } from 'jscodeshift'; import { babelParse, babelParseExpression } from '@storybook/csf-tools'; import { remark } from 'remark'; @@ -49,7 +49,7 @@ export default function jscodeshift(info: FileInfo) { return mdx; } -export function transform(source: string, baseName: string): [mdx: string, csf: string] { +export function transform(source: string, baseName: string): [string, string] { const root = mdxProcessor.parse(source); const storyNamespaceName = nameToValidExport(`${baseName}Stories`); @@ -70,6 +70,7 @@ export function transform(source: string, baseName: string): [mdx: string, csf: >(); // rewrite addon docs import + // @ts-ignore visit(root, ['mdxjsEsm'], (node: MdxjsEsm) => { node.value = node.value .replaceAll('@storybook/addon-docs/blocks', '@storybook/blocks') @@ -78,6 +79,7 @@ export function transform(source: string, baseName: string): [mdx: string, csf: const file = getEsmAst(root); + // @ts-ignore visit( root, ['mdxJsxFlowElement', 'mdxJsxTextElement'], @@ -134,18 +136,18 @@ export function transform(source: string, baseName: string): [mdx: string, csf: value: `/* ${nodeString} is deprecated, please migrate it to see: https://storybook.js.org/migration-guides/7.0 */`, }; storiesMap.set(idAttribute.value as string, { type: 'id' }); - parent.children.splice(index, 0, newNode); + parent?.children.splice(index as number, 0, newNode); // current index is the new comment, and index + 1 is current node // SKIP traversing current node, and continue with the node after that - return [SKIP, index + 2]; + return [SKIP, (index as number) + 2]; } else if ( storyAttribute?.type === 'mdxJsxAttribute' && typeof storyAttribute.value === 'object' && - storyAttribute.value.type === 'mdxJsxAttributeValueExpression' + storyAttribute.value?.type === 'mdxJsxAttributeValueExpression' ) { // e.g. - const name = storyAttribute.value.value; + const name = storyAttribute.value?.value; node.attributes = [ { type: 'mdxJsxAttribute', @@ -158,9 +160,9 @@ export function transform(source: string, baseName: string): [mdx: string, csf: ]; node.children = []; - storiesMap.set(name, { type: 'reference' }); + storiesMap.set(name ?? '', { type: 'reference' }); } else { - parent.children.splice(index, 1); + parent?.children.splice(index as number, 1); // Do not traverse `node`, continue at the node *now* at `index`. return [SKIP, index]; } @@ -177,7 +179,7 @@ export function transform(source: string, baseName: string): [mdx: string, csf: return [ t.objectProperty( t.identifier(attribute.name), - babelParseExpression(attribute.value.value) as any as t.Expression + babelParseExpression(attribute.value?.value ?? '') as any as t.Expression ), ]; } @@ -193,13 +195,14 @@ export function transform(source: string, baseName: string): [mdx: string, csf: }, // remove exports from csf file ExportNamedDeclaration(path) { + // @ts-ignore path.replaceWith(path.node.declaration); }, }); if (storiesMap.size === 0 && metaAttributes.length === 0) { // A CSF file must have at least one story, so skip migrating if this is the case. - return [mdxProcessor.stringify(root), null]; + return [mdxProcessor.stringify(root), '']; } addStoriesImport(root, baseName, storyNamespaceName); @@ -260,9 +263,7 @@ export function transform(source: string, baseName: string): [mdx: string, csf: } const renderProperty = mapChildrenToRender(value.children); const newObject = t.objectExpression([ - ...(renderProperty - ? [t.objectProperty(t.identifier('render'), mapChildrenToRender(value.children))] - : []), + ...(renderProperty ? [t.objectProperty(t.identifier('render'), renderProperty)] : []), ...value.attributes.flatMap((attribute) => { if (attribute.type === 'mdxJsxAttribute') { if (typeof attribute.value === 'string') { @@ -273,7 +274,7 @@ export function transform(source: string, baseName: string): [mdx: string, csf: return [ t.objectProperty( t.identifier(attribute.name), - babelParseExpression(attribute.value.value) as any as t.Expression + babelParseExpression(attribute.value?.value ?? '') as any as t.Expression ), ]; } @@ -309,12 +310,13 @@ export function transform(source: string, baseName: string): [mdx: string, csf: function getEsmAst(root: Root) { const esm: string[] = []; + // @ts-expect-error (not valid BuildVisitor) visit(root, ['mdxjsEsm'], (node: MdxjsEsm) => { esm.push(node.value); }); const esmSource = `${esm.join('\n\n')}`; - // @ts-expect-error File is not yet exposed, see https://github.com/babel/babel/issues/11350#issuecomment-644118606 + // @ts-expect-error (File is not yet exposed, see https://github.com/babel/babel/issues/11350#issuecomment-644118606) const file: BabelFile = new babel.File( { filename: 'info.path' }, { code: esmSource, ast: babelParse(esmSource) } @@ -324,7 +326,7 @@ function getEsmAst(root: Root) { function addStoriesImport(root: Root, baseName: string, storyNamespaceName: string): void { let found = false; - + // @ts-expect-error (not valid BuildVisitor) visit(root, ['mdxjsEsm'], (node: MdxjsEsm) => { if (!found) { node.value += `\nimport * as ${storyNamespaceName} from './${baseName}.stories';`; diff --git a/code/lib/codemod/tsconfig.json b/code/lib/codemod/tsconfig.json index 3a9315f11d34..0e884ab34b9d 100644 --- a/code/lib/codemod/tsconfig.json +++ b/code/lib/codemod/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "skipLibCheck": true, "allowJs": true, - "strict": false + "strict": true, + "lib": ["ES2021.String"] }, "include": ["src/**/*"], "exclude": ["node_modules", "__testfixtures__", "__tests__"] diff --git a/code/lib/core-server/src/presets/common-preset.ts b/code/lib/core-server/src/presets/common-preset.ts index 3ea66c25743a..f94a6fb365ec 100644 --- a/code/lib/core-server/src/presets/common-preset.ts +++ b/code/lib/core-server/src/presets/common-preset.ts @@ -133,8 +133,8 @@ export const previewBody = async (base: any, { configDir, presets }: Options) => export const typescript = () => ({ check: false, - // 'react-docgen' faster but produces lower quality typescript results - reactDocgen: 'react-docgen-typescript', + // 'react-docgen' faster than `react-docgen-typescript` but produces lower quality results + reactDocgen: 'react-docgen', reactDocgenTypescriptOptions: { shouldExtractLiteralValuesFromEnum: true, shouldRemoveUndefinedFromOptional: true, diff --git a/code/lib/core-server/src/utils/update-check.ts b/code/lib/core-server/src/utils/update-check.ts index 2012921921a0..aa57eb08399d 100644 --- a/code/lib/core-server/src/utils/update-check.ts +++ b/code/lib/core-server/src/utils/update-check.ts @@ -50,7 +50,7 @@ export function createUpdateMessage(updateInfo: VersionCheck, version: string): ${chalk.gray('Upgrade now:')} ${colors.green(upgradeCommand)} ${chalk.gray('Read full changelog:')} ${chalk.gray.underline( - 'https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md' + 'https://github.com/storybookjs/storybook/blob/main/CHANGELOG.md' )} ` : ''; diff --git a/code/lib/csf-plugin/src/index.ts b/code/lib/csf-plugin/src/index.ts index aed7a531ee32..ec0aaa1d52a9 100644 --- a/code/lib/csf-plugin/src/index.ts +++ b/code/lib/csf-plugin/src/index.ts @@ -12,15 +12,18 @@ const logger = console; export const unplugin = createUnplugin((options) => { return { name: 'unplugin-csf', - enforce: 'pre', - loadInclude(id) { + transformInclude(id) { return STORIES_REGEX.test(id); }, - async load(fname) { - const code = await fs.readFile(fname, 'utf-8'); + async transform(code, id) { + const sourceCode = await fs.readFile(id, 'utf-8'); try { - const csf = loadCsf(code, { makeTitle: (userTitle) => userTitle || 'default' }).parse(); - enrichCsf(csf, options); + const makeTitle = (userTitle: string) => userTitle || 'default'; + const csf = loadCsf(code, { makeTitle }).parse(); + const csfSource = loadCsf(sourceCode, { + makeTitle, + }).parse(); + enrichCsf(csf, csfSource, options); return formatCsf(csf, { sourceMaps: true }); } catch (err: any) { // This can be called on legacy storiesOf files, so just ignore diff --git a/code/lib/csf-tools/src/enrichCsf.test.ts b/code/lib/csf-tools/src/enrichCsf.test.ts index a8f5f3aaa09c..d793f730cecc 100644 --- a/code/lib/csf-tools/src/enrichCsf.test.ts +++ b/code/lib/csf-tools/src/enrichCsf.test.ts @@ -11,11 +11,16 @@ expect.addSnapshotSerializer({ test: (val) => true, }); -const enrich = (code: string, options?: EnrichCsfOptions) => { +const enrich = (code: string, originalCode: string, options?: EnrichCsfOptions) => { // we don't actually care about the title - const csf = loadCsf(code, { makeTitle: (userTitle) => userTitle || 'default' }).parse(); - enrichCsf(csf, options); + const csf = loadCsf(code, { + makeTitle: (userTitle) => userTitle || 'default', + }).parse(); + const csfSource = loadCsf(originalCode, { + makeTitle: (userTitle) => userTitle || 'default', + }).parse(); + enrichCsf(csf, csfSource, options); return formatCsf(csf); }; @@ -23,17 +28,28 @@ describe('enrichCsf', () => { describe('source', () => { it('csf1', () => { expect( - enrich(dedent` + enrich( + dedent` + // compiled code export default { title: 'Button', } - export const Basic = () =>