diff --git a/CHANGELOG.md b/CHANGELOG.md index 26e490d7c6d2..966bf5ce9a52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 7.4.3 + +- CLI: Fix `sb add` adding duplicative entries - [#24229](https://github.com/storybookjs/storybook/pull/24229), thanks [@ndelangen](https://github.com/ndelangen)! +- NextJS: Add compatibility with nextjs `13.5` - [#24239](https://github.com/storybookjs/storybook/pull/24239), thanks [@ndelangen](https://github.com/ndelangen)! +- NextJS: Aliases `react` and `react-dom` like `next.js` does - [#23671](https://github.com/storybookjs/storybook/pull/23671), thanks [@sookmax](https://github.com/sookmax)! +- Types: Allow `null` in value of `experimental_updateStatus` to clear status - [#24206](https://github.com/storybookjs/storybook/pull/24206), thanks [@ndelangen](https://github.com/ndelangen)! + ## 7.4.2 - Addon API: Improve the updateStatus API - [#24007](https://github.com/storybookjs/storybook/pull/24007), thanks [@ndelangen](https://github.com/ndelangen)! diff --git a/code/frameworks/nextjs/src/config/webpack.ts b/code/frameworks/nextjs/src/config/webpack.ts index 13c1a251cebe..f5e72bc360d8 100644 --- a/code/frameworks/nextjs/src/config/webpack.ts +++ b/code/frameworks/nextjs/src/config/webpack.ts @@ -5,6 +5,14 @@ import type { NextConfig } from 'next'; import { DefinePlugin } from 'webpack'; import { addScopedAlias, getNextjsVersion, resolveNextConfig } from '../utils'; +const tryResolve = (path: string) => { + try { + return require.resolve(path); + } catch (err) { + return false; + } +}; + export const configureConfig = async ({ baseConfig, nextConfigPath, @@ -17,8 +25,12 @@ export const configureConfig = async ({ const nextConfig = await resolveNextConfig({ baseConfig, nextConfigPath, configDir }); addScopedAlias(baseConfig, 'next/config'); - addScopedAlias(baseConfig, 'react', 'next/dist/compiled/react'); - addScopedAlias(baseConfig, 'react-dom', 'next/dist/compiled/react-dom'); + if (tryResolve('next/dist/compiled/react')) { + addScopedAlias(baseConfig, 'react', 'next/dist/compiled/react'); + } + if (tryResolve('next/dist/compiled/react-dom')) { + addScopedAlias(baseConfig, 'react-dom', 'next/dist/compiled/react-dom'); + } setupRuntimeConfig(baseConfig, nextConfig); return nextConfig; diff --git a/code/frameworks/nextjs/src/dependency-map.ts b/code/frameworks/nextjs/src/dependency-map.ts new file mode 100644 index 000000000000..70ad2ece94e0 --- /dev/null +++ b/code/frameworks/nextjs/src/dependency-map.ts @@ -0,0 +1,36 @@ +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': { + 'next/dist/shared/lib/router-context': 'next/dist/shared/lib/router-context', + }, + '>=13.5.0': { + 'next/dist/shared/lib/router-context': 'next/dist/shared/lib/router-context.shared-runtime', + 'next/dist/shared/lib/head-manager-context': + 'next/dist/shared/lib/head-manager-context.shared-runtime', + 'next/dist/shared/lib/app-router-context': + 'next/dist/shared/lib/app-router-context.shared-runtime', + 'next/dist/shared/lib/hooks-client-context': + 'next/dist/shared/lib/hooks-client-context.shared-runtime', + }, +}; + +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/preset.ts b/code/frameworks/nextjs/src/preset.ts index 4aa06d54c4d0..db1c276da9b4 100644 --- a/code/frameworks/nextjs/src/preset.ts +++ b/code/frameworks/nextjs/src/preset.ts @@ -7,7 +7,6 @@ import { getProjectRoot } from '@storybook/core-common'; import { configureConfig } from './config/webpack'; import { configureCss } from './css/webpack'; import { configureImports } from './imports/webpack'; -import { configureRouting } from './routing/webpack'; import { configureStyledJsx } from './styledJsx/webpack'; import { configureImages } from './images/webpack'; import { configureRuntimeNextjsVersionResolution } from './utils'; @@ -17,6 +16,7 @@ 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'; export const addons: PresetProperty<'addons', StorybookConfig> = [ dirname(require.resolve(join('@storybook/preset-react-webpack', 'package.json'))), @@ -143,13 +143,13 @@ export const webpackFinal: StorybookConfig['webpackFinal'] = async (baseConfig, configDir: options.configDir, }); + configureAliasing(baseConfig); configureNextFont(baseConfig); configureNextImport(baseConfig); configureRuntimeNextjsVersionResolution(baseConfig); configureImports({ baseConfig, configDir: options.configDir }); configureCss(baseConfig, nextConfig); configureImages(baseConfig, nextConfig); - configureRouting(baseConfig); configureStyledJsx(baseConfig); configureNodePolyfills(baseConfig); diff --git a/code/frameworks/nextjs/src/routing/webpack.tsx b/code/frameworks/nextjs/src/routing/webpack.tsx deleted file mode 100644 index c0d245219742..000000000000 --- a/code/frameworks/nextjs/src/routing/webpack.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { Configuration as WebpackConfig } from 'webpack'; -import semver from 'semver'; -import { addScopedAlias, getNextjsVersion } from '../utils'; - -export const configureRouting = (baseConfig: WebpackConfig): void => { - // here we resolve the router context path with the installed version of Next.js - const routerContextPath = getRouterContextPath(); - addScopedAlias(baseConfig, routerContextPath); -}; - -const getRouterContextPath = () => { - const version = getNextjsVersion(); - if (semver.gte(version, '11.1.0')) { - return 'next/dist/shared/lib/router-context'; - } - - return 'next/dist/next-server/lib/router-context'; -}; diff --git a/code/lib/cli/src/add.ts b/code/lib/cli/src/add.ts index 71437b314b1c..8728da80ad5f 100644 --- a/code/lib/cli/src/add.ts +++ b/code/lib/cli/src/add.ts @@ -1,6 +1,8 @@ -import { getStorybookInfo } from '@storybook/core-common'; +import { getStorybookInfo, serverRequire } from '@storybook/core-common'; import { readConfig, writeConfig } from '@storybook/csf-tools'; +import { isAbsolute, join } from 'path'; import SemVer from 'semver'; +import dedent from 'ts-dedent'; import { JsPackageManagerFactory, @@ -38,6 +40,21 @@ const getVersionSpecifier = (addon: string) => { return groups ? [groups[1], groups[2]] : [addon, undefined]; }; +const requireMain = (configDir: string) => { + const absoluteConfigDir = isAbsolute(configDir) ? configDir : join(process.cwd(), configDir); + const mainFile = join(absoluteConfigDir, 'main'); + + return serverRequire(mainFile) ?? {}; +}; + +const checkInstalled = (addonName: string, main: any) => { + const existingAddon = main.addons?.find((entry: string | { name: string }) => { + const name = typeof entry === 'string' ? entry : entry.name; + return name?.endsWith(addonName); + }); + return !!existingAddon; +}; + /** * Install the given addon package and add it to main.js * @@ -60,9 +77,16 @@ export async function add( } const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr }); const packageJson = await packageManager.retrievePackageJson(); + const { mainConfig, configDir } = getStorybookInfo(packageJson); + + if (checkInstalled(addon, requireMain(configDir))) { + throw new Error(dedent` + Addon ${addon} is already installed; we skipped adding it to your ${mainConfig}. + `); + } + const [addonName, versionSpecifier] = getVersionSpecifier(addon); - const { mainConfig } = getStorybookInfo(packageJson); if (!mainConfig) { logger.error('Unable to find storybook main.js config'); return; diff --git a/code/lib/core-common/src/utils/get-storybook-info.ts b/code/lib/core-common/src/utils/get-storybook-info.ts index fe183d566b7c..8d97fed4d3ed 100644 --- a/code/lib/core-common/src/utils/get-storybook-info.ts +++ b/code/lib/core-common/src/utils/get-storybook-info.ts @@ -100,7 +100,7 @@ const getConfigInfo = (packageJson: PackageJson, configDir?: string) => { } return { - configDir, + configDir: storybookConfigDir, mainConfig: findConfigFile('main', storybookConfigDir), previewConfig: findConfigFile('preview', storybookConfigDir), managerConfig: findConfigFile('manager', storybookConfigDir), diff --git a/code/lib/preview-api/src/modules/preview-web/PreviewWeb.test.ts b/code/lib/preview-api/src/modules/preview-web/PreviewWeb.test.ts index 41e735ebfddb..8123020b86bb 100644 --- a/code/lib/preview-api/src/modules/preview-web/PreviewWeb.test.ts +++ b/code/lib/preview-api/src/modules/preview-web/PreviewWeb.test.ts @@ -3502,6 +3502,19 @@ describe('PreviewWeb', () => { document.location.search = '?id=component-one--docs&viewMode=docs'; const preview = await createAndRenderPreview(); + preview.onKeydown({ + composedPath: jest + .fn() + .mockReturnValue([{ tagName: 'div', getAttribute: jest.fn().mockReturnValue(null) }]), + } as any); + + expect(mockChannel.emit).toHaveBeenCalledWith(PREVIEW_KEYDOWN, expect.objectContaining({})); + }); + + it('emits PREVIEW_KEYDOWN for regular elements, fallback to event.target', async () => { + document.location.search = '?id=component-one--docs&viewMode=docs'; + const preview = await createAndRenderPreview(); + preview.onKeydown({ target: { tagName: 'div', getAttribute: jest.fn().mockReturnValue(null) }, } as any); @@ -3514,7 +3527,9 @@ describe('PreviewWeb', () => { const preview = await createAndRenderPreview(); preview.onKeydown({ - target: { tagName: 'input', getAttribute: jest.fn().mockReturnValue(null) }, + composedPath: jest + .fn() + .mockReturnValue([{ tagName: 'input', getAttribute: jest.fn().mockReturnValue(null) }]), } as any); expect(mockChannel.emit).not.toHaveBeenCalledWith( diff --git a/code/lib/preview-api/src/modules/preview-web/PreviewWithSelection.tsx b/code/lib/preview-api/src/modules/preview-web/PreviewWithSelection.tsx index ca30544d7c49..73b58cc70ead 100644 --- a/code/lib/preview-api/src/modules/preview-web/PreviewWithSelection.tsx +++ b/code/lib/preview-api/src/modules/preview-web/PreviewWithSelection.tsx @@ -45,7 +45,7 @@ import type { StorySpecifier } from '../store/StoryIndexStore'; const globalWindow = globalThis; function focusInInput(event: Event) { - const target = event.target as Element; + const target = ((event.composedPath && event.composedPath()[0]) || event.target) as Element; return /input|textarea/i.test(target.tagName) || target.getAttribute('contenteditable') !== null; }