From 2a02e134805f9659481f90237478a39516322c44 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Tue, 19 Nov 2024 18:14:41 +1100 Subject: [PATCH 1/5] Fix webpack fsCache not working with @storybook/nextjs Next.js overrides certain internal webpack packages (particularly `webpack-sources`), which are involved with filesystem caching, with its own versions from next/dist/compiled. For filesystem caching to work, Next.js must be allowed to perform these overrides before webpack is first initialized by @storybook/builder-webpack5. If it is not, the objects to be serialized to disk in the caching process will be instantiated using the original (non-Next.js) modules, but the serializers will be created using the Next.js modules. This mismatch between the objects to be cached and the serializers that write the filesystem cache prevents the cache from being written; instead, webpack outputs a warning message to the console for every object that it tries and fails to find a matching serializer for. This fix works by invoking Next.js to configure webpack in the `core` hook of @storybook/nextjs/preset, immediately before loading @storybook/builder-webpack5. We don't actually use this configuration that Next.js creates; the actual configuration that will be used in the build is still generated in `webpackFinal` as before. `fsCache` has a large impact on Storybook build performance. Even in a minimal project with a single story, enabling it reduces build time by 66%! This is therefore a very valuable option to be able to enable. Fixes #29621 --- code/frameworks/nextjs/src/preset.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/code/frameworks/nextjs/src/preset.ts b/code/frameworks/nextjs/src/preset.ts index 3463910175e1..84255fc62e20 100644 --- a/code/frameworks/nextjs/src/preset.ts +++ b/code/frameworks/nextjs/src/preset.ts @@ -36,6 +36,19 @@ export const addons: PresetProperty<'addons'> = [ export const core: PresetProperty<'core'> = async (config, options) => { const framework = await options.presets.apply('framework'); + // Load the Next.js configuration before we need it in webpackFinal (below). + // This gives Next.js an opportunity to override some of webpack's internals + // (see next/dist/server/config-utils.js) before @storybook/builder-webpack5 + // starts to use it. Without this, webpack's file system cache (fsCache: true) + // does not work. + const { nextConfigPath } = await options.presets.apply('frameworkOptions'); + await configureConfig({ + // Pass in a dummy webpack config object for now, since we don't want to + // modify the real one yet. We pass in the real one in webpackFinal. + baseConfig: {}, + nextConfigPath, + }); + return { ...config, builder: { From 0a3e05bceafe0ad4aae8a2d3e19984c2d337b420 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Wed, 27 Nov 2024 11:33:40 +1100 Subject: [PATCH 2/5] Fix crash when framework is not specified with options --- code/frameworks/nextjs/src/preset.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/code/frameworks/nextjs/src/preset.ts b/code/frameworks/nextjs/src/preset.ts index 84255fc62e20..f10550acc900 100644 --- a/code/frameworks/nextjs/src/preset.ts +++ b/code/frameworks/nextjs/src/preset.ts @@ -34,19 +34,18 @@ export const addons: PresetProperty<'addons'> = [ ]; export const core: PresetProperty<'core'> = async (config, options) => { - const framework = await options.presets.apply('framework'); + const framework = await options.presets.apply('framework'); // Load the Next.js configuration before we need it in webpackFinal (below). // This gives Next.js an opportunity to override some of webpack's internals // (see next/dist/server/config-utils.js) before @storybook/builder-webpack5 // starts to use it. Without this, webpack's file system cache (fsCache: true) // does not work. - const { nextConfigPath } = await options.presets.apply('frameworkOptions'); await configureConfig({ // Pass in a dummy webpack config object for now, since we don't want to // modify the real one yet. We pass in the real one in webpackFinal. baseConfig: {}, - nextConfigPath, + nextConfigPath: typeof framework === 'string' ? undefined : framework.options.nextConfigPath, }); return { From 83a1f24f02c50a0b5ed4a03c6b248100183a3b01 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Tue, 3 Dec 2024 12:27:09 +1100 Subject: [PATCH 3/5] Defer all imports from webpack until after Next.js has loaded its internal instance --- code/frameworks/nextjs/src/config/webpack.ts | 14 +++++--- code/frameworks/nextjs/src/preset.ts | 35 +++++++++++--------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/code/frameworks/nextjs/src/config/webpack.ts b/code/frameworks/nextjs/src/config/webpack.ts index 76edac25c81c..030da6b2ab77 100644 --- a/code/frameworks/nextjs/src/config/webpack.ts +++ b/code/frameworks/nextjs/src/config/webpack.ts @@ -1,8 +1,7 @@ import type { NextConfig } from 'next'; import type { Configuration as WebpackConfig } from 'webpack'; -import { DefinePlugin } from 'webpack'; -import { addScopedAlias, resolveNextConfig, setAlias } from '../utils'; +import { addScopedAlias, resolveNextConfig } from '../utils'; const tryResolve = (path: string) => { try { @@ -48,12 +47,15 @@ export const configureConfig = async ({ addScopedAlias(baseConfig, 'react-dom/server', 'next/dist/compiled/react-dom/server'); } - setupRuntimeConfig(baseConfig, nextConfig); + await setupRuntimeConfig(baseConfig, nextConfig); return nextConfig; }; -const setupRuntimeConfig = (baseConfig: WebpackConfig, nextConfig: NextConfig): void => { +const setupRuntimeConfig = async ( + baseConfig: WebpackConfig, + nextConfig: NextConfig +): Promise => { const definePluginConfig: Record = { // this mimics what nextjs does client side // https://github.com/vercel/next.js/blob/57702cb2a9a9dba4b552e0007c16449cf36cfb44/packages/next/client/index.tsx#L101 @@ -67,5 +69,7 @@ const setupRuntimeConfig = (baseConfig: WebpackConfig, nextConfig: NextConfig): definePluginConfig['process.env.__NEXT_NEW_LINK_BEHAVIOR'] = newNextLinkBehavior; - baseConfig.plugins?.push(new DefinePlugin(definePluginConfig)); + // Load DefinePlugin with a dynamic import to ensure that Next.js can first + // replace webpack with its own internal instance, and we get that here. + baseConfig.plugins?.push(new (await import('webpack')).DefinePlugin(definePluginConfig)); }; diff --git a/code/frameworks/nextjs/src/preset.ts b/code/frameworks/nextjs/src/preset.ts index f10550acc900..473e24655848 100644 --- a/code/frameworks/nextjs/src/preset.ts +++ b/code/frameworks/nextjs/src/preset.ts @@ -10,24 +10,10 @@ import type { ConfigItem, PluginItem, TransformOptions } from '@babel/core'; import { loadPartialConfig } from '@babel/core'; import semver from 'semver'; -import { configureAliases } from './aliases/webpack'; -import { configureBabelLoader } from './babel/loader'; import nextBabelPreset from './babel/preset'; -import { configureCompatibilityAliases } from './compatibility/compatibility-map'; import { configureConfig } from './config/webpack'; -import { configureCss } from './css/webpack'; -import { configureNextExportMocks } from './export-mocks/webpack'; -import { configureFastRefresh } from './fastRefresh/webpack'; import TransformFontImports from './font/babel'; -import { configureNextFont } from './font/webpack/configureNextFont'; -import { configureImages } from './images/webpack'; -import { configureImports } from './imports/webpack'; -import { configureNodePolyfills } from './nodePolyfills/webpack'; -import { configureRSC } from './rsc/webpack'; -import { configureStyledJsx } from './styledJsx/webpack'; -import { configureSWCLoader } from './swc/loader'; import type { FrameworkOptions, StorybookConfig } from './types'; -import { configureRuntimeNextjsVersionResolution, getNextjsVersion } from './utils'; export const addons: PresetProperty<'addons'> = [ dirname(require.resolve(join('@storybook/preset-react-webpack', 'package.json'))), @@ -156,6 +142,22 @@ export const webpackFinal: StorybookConfig['webpackFinal'] = async (baseConfig, nextConfigPath, }); + // Use dynamic imports to ensure these modules that use webpack load after + // Next.js has been configured (above), and has replaced webpack with its precompiled + // version. + const { configureNextFont } = await import('./font/webpack/configureNextFont'); + const { configureRuntimeNextjsVersionResolution, getNextjsVersion } = await import('./utils'); + const { configureImports } = await import('./imports/webpack'); + const { configureCss } = await import('./css/webpack'); + const { configureImages } = await import('./images/webpack'); + const { configureStyledJsx } = await import('./styledJsx/webpack'); + const { configureNodePolyfills } = await import('./nodePolyfills/webpack'); + const { configureAliases } = await import('./aliases/webpack'); + const { configureFastRefresh } = await import('./fastRefresh/webpack'); + const { configureRSC } = await import('./rsc/webpack'); + const { configureSWCLoader } = await import('./swc/loader'); + const { configureBabelLoader } = await import('./babel/loader'); + const babelRCPath = join(getProjectRoot(), '.babelrc'); const babelConfigPath = join(getProjectRoot(), 'babel.config.js'); const hasBabelConfig = existsSync(babelRCPath) || existsSync(babelConfigPath); @@ -168,7 +170,10 @@ export const webpackFinal: StorybookConfig['webpackFinal'] = async (baseConfig, configureNextFont(baseConfig, useSWC); configureRuntimeNextjsVersionResolution(baseConfig); - configureImports({ baseConfig, configDir: options.configDir }); + configureImports({ + baseConfig, + configDir: options.configDir, + }); configureCss(baseConfig, nextConfig); configureImages(baseConfig, nextConfig); configureStyledJsx(baseConfig); From c00c054199d73cd88e6f53b713c3b944422c72c9 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Wed, 4 Dec 2024 10:42:43 +1100 Subject: [PATCH 4/5] Undo unintentional formatting change --- code/frameworks/nextjs/src/preset.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/code/frameworks/nextjs/src/preset.ts b/code/frameworks/nextjs/src/preset.ts index 473e24655848..6b90ae543a22 100644 --- a/code/frameworks/nextjs/src/preset.ts +++ b/code/frameworks/nextjs/src/preset.ts @@ -170,10 +170,7 @@ export const webpackFinal: StorybookConfig['webpackFinal'] = async (baseConfig, configureNextFont(baseConfig, useSWC); configureRuntimeNextjsVersionResolution(baseConfig); - configureImports({ - baseConfig, - configDir: options.configDir, - }); + configureImports({ baseConfig, configDir: options.configDir }); configureCss(baseConfig, nextConfig); configureImages(baseConfig, nextConfig); configureStyledJsx(baseConfig); From 902dc18344bbc209804de0715a523b9529d8717c Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Fri, 13 Dec 2024 12:51:52 +0100 Subject: [PATCH 5/5] Fix webpack DefinePlugin import to use default export --- code/frameworks/nextjs/src/config/webpack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/frameworks/nextjs/src/config/webpack.ts b/code/frameworks/nextjs/src/config/webpack.ts index 030da6b2ab77..4b10922c9df9 100644 --- a/code/frameworks/nextjs/src/config/webpack.ts +++ b/code/frameworks/nextjs/src/config/webpack.ts @@ -71,5 +71,5 @@ const setupRuntimeConfig = async ( // Load DefinePlugin with a dynamic import to ensure that Next.js can first // replace webpack with its own internal instance, and we get that here. - baseConfig.plugins?.push(new (await import('webpack')).DefinePlugin(definePluginConfig)); + baseConfig.plugins?.push(new (await import('webpack')).default.DefinePlugin(definePluginConfig)); };