From 1cafff74b4b0e7e569f35665141716b78dccb6d5 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Fri, 10 Oct 2025 11:19:27 +0200 Subject: [PATCH 1/5] Enhance PostCSS configuration handling - Added `lilconfig` dependency to manage PostCSS configurations. - Introduced `find-postcss-config.ts` to locate and load PostCSS config files with a fallback mechanism. - Updated `preset.ts` to utilize the new PostCSS loading functionality, improving error handling for incompatible configurations. --- code/frameworks/nextjs-vite/package.json | 1 + .../nextjs-vite/src/find-postcss-config.ts | 120 ++++++++++++++++++ code/frameworks/nextjs-vite/src/preset.ts | 20 +-- code/yarn.lock | 3 +- 4 files changed, 129 insertions(+), 15 deletions(-) create mode 100644 code/frameworks/nextjs-vite/src/find-postcss-config.ts diff --git a/code/frameworks/nextjs-vite/package.json b/code/frameworks/nextjs-vite/package.json index 676a7b589aea..5f5c44e08334 100644 --- a/code/frameworks/nextjs-vite/package.json +++ b/code/frameworks/nextjs-vite/package.json @@ -87,6 +87,7 @@ }, "devDependencies": { "@types/node": "^22.0.0", + "lilconfig": "^3.0.0", "next": "^15.2.3", "postcss-load-config": "^6.0.1", "semver": "^7.3.5", diff --git a/code/frameworks/nextjs-vite/src/find-postcss-config.ts b/code/frameworks/nextjs-vite/src/find-postcss-config.ts new file mode 100644 index 000000000000..64308a0cf750 --- /dev/null +++ b/code/frameworks/nextjs-vite/src/find-postcss-config.ts @@ -0,0 +1,120 @@ +// @ts-check +import { readFile, writeFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; + +import { getProjectRoot } from 'storybook/internal/common'; +import { IncompatiblePostCssConfigError } from 'storybook/internal/server-errors'; + +import config from 'lilconfig'; +import postCssLoadConfig from 'postcss-load-config'; +import yaml from 'yaml'; + +type Options = import('lilconfig').Options; + +const require = createRequire(import.meta.url); + +async function loader(filepath: string) { + return require(filepath); +} + +async function yamlLoader(_: string, content: string) { + return yaml.parse(content); +} + +const withLoaders = (options: Options = {}) => { + const moduleName = 'postcss'; + + return { + ...options, + loaders: { + ...options.loaders, + '.cjs': loader, + '.cts': loader, + '.js': loader, + '.mjs': loader, + '.mts': loader, + '.ts': loader, + '.yaml': yamlLoader, + '.yml': yamlLoader, + }, + searchPlaces: [ + ...(options.searchPlaces ?? []), + 'package.json', + `.${moduleName}rc`, + `.${moduleName}rc.json`, + `.${moduleName}rc.yaml`, + `.${moduleName}rc.yml`, + `.${moduleName}rc.ts`, + `.${moduleName}rc.cts`, + `.${moduleName}rc.mts`, + `.${moduleName}rc.js`, + `.${moduleName}rc.cjs`, + `.${moduleName}rc.mjs`, + `${moduleName}.config.ts`, + `${moduleName}.config.cts`, + `${moduleName}.config.mts`, + `${moduleName}.config.js`, + `${moduleName}.config.cjs`, + `${moduleName}.config.mjs`, + ], + } satisfies Options; +}; + +/** + * Find PostCSS config file path (without loading the config) + * + * @param {String} path Config Path + * @param {Object} options Config Options + * @returns {Promise} Config file path or null if not found + */ +export async function postCssFindConfig(path: string, options: Options = {}) { + const result = await config.lilconfig('postcss', withLoaders(options)).search(path); + + return result ? result.filepath : null; +} + +export { postCssLoadConfig }; + +/** Handle PostCSS config loading with fallback mechanism */ +export const loadPostCssConfigWithFallback = async (searchPath: string): Promise => { + const configPath = await postCssFindConfig(searchPath); + if (!configPath) { + return true; + } + + let error: any; + + // First attempt: try loading config as-is + try { + await postCssLoadConfig({}, searchPath, { stopDir: getProjectRoot() }); + return true; // Success! + } catch (e: any) { + error = e; + } + + // No config found is not an error we need to handle + if (error.message.includes('No PostCSS Config found')) { + return true; + } + + // NextJS uses an incompatible format for PostCSS plugins, we make an attempt to fix it + if (error.message.includes('Invalid PostCSS Plugin found')) { + // Second attempt: try with modified config + try { + const originalContent = await readFile(configPath, 'utf8'); + const modifiedContent = originalContent.replace(/FROM/g, 'TO'); + + // Write the modified content + await writeFile(configPath, modifiedContent, 'utf8'); + + // Retry loading the config + await postCssLoadConfig({}, searchPath, { stopDir: getProjectRoot() }); + return true; // Success with modified config! + } catch (e: any) { + // We were unable to fix the config, so we throw an error + throw new IncompatiblePostCssConfigError({ error }); + } + } + + return false; +}; diff --git a/code/frameworks/nextjs-vite/src/preset.ts b/code/frameworks/nextjs-vite/src/preset.ts index b34294dd7977..46caf9b58875 100644 --- a/code/frameworks/nextjs-vite/src/preset.ts +++ b/code/frameworks/nextjs-vite/src/preset.ts @@ -3,16 +3,14 @@ import { createRequire } from 'node:module'; import { dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { getProjectRoot } from 'storybook/internal/common'; -import { IncompatiblePostCssConfigError } from 'storybook/internal/server-errors'; import type { PresetProperty } from 'storybook/internal/types'; import type { StorybookConfigVite } from '@storybook/builder-vite'; import { viteFinal as reactViteFinal } from '@storybook/react-vite/preset'; -import postCssLoadConfig from 'postcss-load-config'; import semver from 'semver'; +import { loadPostCssConfigWithFallback } from './find-postcss-config'; import type { FrameworkOptions } from './types'; import { getNextjsVersion } from './utils'; @@ -63,17 +61,11 @@ export const optimizeViteDeps = [ export const viteFinal: StorybookConfigVite['viteFinal'] = async (config, options) => { const reactConfig = await reactViteFinal(config, options); - try { - const inlineOptions = config.css?.postcss; - const searchPath = typeof inlineOptions === 'string' ? inlineOptions : config.root; - await postCssLoadConfig({}, searchPath, { stopDir: getProjectRoot() }); - } catch (e: any) { - if (!e.message.includes('No PostCSS Config found')) { - // This is a custom error that we throw when the PostCSS config is invalid - if (e.message.includes('Invalid PostCSS Plugin found')) { - throw new IncompatiblePostCssConfigError({ error: e }); - } - } + const inlineOptions = config.css?.postcss; + const searchPath = typeof inlineOptions === 'string' ? inlineOptions : config.root; + + if (searchPath) { + await loadPostCssConfigWithFallback(searchPath); } const { nextConfigPath } = await options.presets.apply('frameworkOptions'); diff --git a/code/yarn.lock b/code/yarn.lock index 9f5cdb31e37b..fbc0b812a177 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6564,6 +6564,7 @@ __metadata: "@storybook/react": "workspace:*" "@storybook/react-vite": "workspace:*" "@types/node": "npm:^22.0.0" + lilconfig: "npm:^3.0.0" next: "npm:^15.2.3" postcss-load-config: "npm:^6.0.1" semver: "npm:^7.3.5" @@ -18126,7 +18127,7 @@ __metadata: languageName: node linkType: hard -"lilconfig@npm:^3.1.1": +"lilconfig@npm:^3.0.0, lilconfig@npm:^3.1.1": version: 3.1.3 resolution: "lilconfig@npm:3.1.3" checksum: 10c0/f5604e7240c5c275743561442fbc5abf2a84ad94da0f5adc71d25e31fa8483048de3dcedcb7a44112a942fed305fd75841cdf6c9681c7f640c63f1049e9a5dcc From 0dc191c89065c740bec09fe49ccbda16f58ed59f Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Fri, 10 Oct 2025 11:39:13 +0200 Subject: [PATCH 2/5] replace the incorrect postcss config file syntax with the correct one --- code/frameworks/nextjs-vite/src/find-postcss-config.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/code/frameworks/nextjs-vite/src/find-postcss-config.ts b/code/frameworks/nextjs-vite/src/find-postcss-config.ts index 64308a0cf750..d5e06b1af765 100644 --- a/code/frameworks/nextjs-vite/src/find-postcss-config.ts +++ b/code/frameworks/nextjs-vite/src/find-postcss-config.ts @@ -102,7 +102,10 @@ export const loadPostCssConfigWithFallback = async (searchPath: string): Promise // Second attempt: try with modified config try { const originalContent = await readFile(configPath, 'utf8'); - const modifiedContent = originalContent.replace(/FROM/g, 'TO'); + const modifiedContent = originalContent.replace( + 'plugins: ["@tailwindcss/postcss"]', + 'plugins: { "@tailwindcss/postcss": {} }' + ); // Write the modified content await writeFile(configPath, modifiedContent, 'utf8'); From d86e68ad155869f32c511daea08e571e1ec0d91b Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Fri, 10 Oct 2025 12:48:24 +0200 Subject: [PATCH 3/5] Update PostCSS configuration handling and add YAML dependency - Refactored PostCSS configuration loading by renaming `loadPostCssConfigWithFallback` to `normalizePostCssConfig` for clarity. - Enhanced error handling in `normalizePostCssConfig` to manage incompatible configurations more effectively. - Added `yaml` dependency version `2.8.1` to `package.json` and updated `yarn.lock` accordingly. --- code/frameworks/nextjs-vite/package.json | 3 +- .../nextjs-vite/src/find-postcss-config.ts | 36 +++++++++++++++---- code/frameworks/nextjs-vite/src/preset.ts | 4 +-- code/yarn.lock | 3 +- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/code/frameworks/nextjs-vite/package.json b/code/frameworks/nextjs-vite/package.json index 5f5c44e08334..11fa6d96bb21 100644 --- a/code/frameworks/nextjs-vite/package.json +++ b/code/frameworks/nextjs-vite/package.json @@ -91,7 +91,8 @@ "next": "^15.2.3", "postcss-load-config": "^6.0.1", "semver": "^7.3.5", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "yaml": "^2.8.1" }, "peerDependencies": { "next": "^14.1.0 || ^15.0.0", diff --git a/code/frameworks/nextjs-vite/src/find-postcss-config.ts b/code/frameworks/nextjs-vite/src/find-postcss-config.ts index d5e06b1af765..f67af903bbde 100644 --- a/code/frameworks/nextjs-vite/src/find-postcss-config.ts +++ b/code/frameworks/nextjs-vite/src/find-postcss-config.ts @@ -67,7 +67,7 @@ const withLoaders = (options: Options = {}) => { * @param {Object} options Config Options * @returns {Promise} Config file path or null if not found */ -export async function postCssFindConfig(path: string, options: Options = {}) { +async function postCssFindConfig(path: string, options: Options = {}) { const result = await config.lilconfig('postcss', withLoaders(options)).search(path); return result ? result.filepath : null; @@ -75,21 +75,45 @@ export async function postCssFindConfig(path: string, options: Options = {}) { export { postCssLoadConfig }; -/** Handle PostCSS config loading with fallback mechanism */ -export const loadPostCssConfigWithFallback = async (searchPath: string): Promise => { +/** + * Normalizes PostCSS configuration for NextJS compatibility. + * + * This function handles the incompatibility between NextJS's PostCSS plugin format and Storybook's + * requirements. NextJS uses array format for plugins while Storybook expects object format. + * + * Process: + * + * 1. First attempts to load the config as-is + * 2. If that fails due to "Invalid PostCSS Plugin found" error, modifies the config file to convert + * array format to object format (e.g., ["@tailwindcss/postcss"] becomes { + * "@tailwindcss/postcss": {} }) + * 3. Retries loading with the modified config + * + * @param searchPath - Directory path to search for PostCSS config + * @returns Promise - true if config loads successfully (or no config found), false if + * config exists but cannot be loaded + * @throws {IncompatiblePostCssConfigError} - When config cannot be fixed automatically + * @sideEffect Modifies the PostCSS config file on disk when fixing plugin format + */ export const normalizePostCssConfig = async (searchPath: string): Promise => { const configPath = await postCssFindConfig(searchPath); if (!configPath) { return true; } - let error: any; + let error: Error | undefined; // First attempt: try loading config as-is try { await postCssLoadConfig({}, searchPath, { stopDir: getProjectRoot() }); return true; // Success! - } catch (e: any) { - error = e; + } catch (e: unknown) { + if (e instanceof Error) { + error = e; + } + } + + if (!error) { + return true; } // No config found is not an error we need to handle diff --git a/code/frameworks/nextjs-vite/src/preset.ts b/code/frameworks/nextjs-vite/src/preset.ts index 46caf9b58875..cc1ed65d4b65 100644 --- a/code/frameworks/nextjs-vite/src/preset.ts +++ b/code/frameworks/nextjs-vite/src/preset.ts @@ -10,7 +10,7 @@ import { viteFinal as reactViteFinal } from '@storybook/react-vite/preset'; import semver from 'semver'; -import { loadPostCssConfigWithFallback } from './find-postcss-config'; +import { normalizePostCssConfig } from './find-postcss-config'; import type { FrameworkOptions } from './types'; import { getNextjsVersion } from './utils'; @@ -65,7 +65,7 @@ export const viteFinal: StorybookConfigVite['viteFinal'] = async (config, option const searchPath = typeof inlineOptions === 'string' ? inlineOptions : config.root; if (searchPath) { - await loadPostCssConfigWithFallback(searchPath); + await normalizePostCssConfig(searchPath); } const { nextConfigPath } = await options.presets.apply('frameworkOptions'); diff --git a/code/yarn.lock b/code/yarn.lock index fbc0b812a177..706309d2aa09 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6571,6 +6571,7 @@ __metadata: styled-jsx: "npm:5.1.6" typescript: "npm:^5.8.3" vite-plugin-storybook-nextjs: "npm:^2.0.7" + yaml: "npm:^2.8.1" peerDependencies: next: ^14.1.0 || ^15.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -27551,7 +27552,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.0.0, yaml@npm:^2.3.1, yaml@npm:^2.6.0, yaml@npm:^2.8.0": +"yaml@npm:^2.0.0, yaml@npm:^2.3.1, yaml@npm:^2.6.0, yaml@npm:^2.8.0, yaml@npm:^2.8.1": version: 2.8.1 resolution: "yaml@npm:2.8.1" bin: From c874228e5006a38d050f0f7e1e7169bce25ec92d Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Fri, 10 Oct 2025 13:48:18 +0200 Subject: [PATCH 4/5] Refactor PostCSS config loader by removing YAML support - Eliminated the YAML loader and related search places from the PostCSS configuration handling. - Simplified the code structure in `find-postcss-config.ts` for better maintainability. - Ensured compatibility with existing PostCSS configurations while enhancing error handling. --- .../nextjs-vite/src/find-postcss-config.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/code/frameworks/nextjs-vite/src/find-postcss-config.ts b/code/frameworks/nextjs-vite/src/find-postcss-config.ts index f67af903bbde..5f1a31a4c1ea 100644 --- a/code/frameworks/nextjs-vite/src/find-postcss-config.ts +++ b/code/frameworks/nextjs-vite/src/find-postcss-config.ts @@ -1,4 +1,3 @@ -// @ts-check import { readFile, writeFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; @@ -7,7 +6,6 @@ import { IncompatiblePostCssConfigError } from 'storybook/internal/server-errors import config from 'lilconfig'; import postCssLoadConfig from 'postcss-load-config'; -import yaml from 'yaml'; type Options = import('lilconfig').Options; @@ -17,10 +15,6 @@ async function loader(filepath: string) { return require(filepath); } -async function yamlLoader(_: string, content: string) { - return yaml.parse(content); -} - const withLoaders = (options: Options = {}) => { const moduleName = 'postcss'; @@ -34,16 +28,12 @@ const withLoaders = (options: Options = {}) => { '.mjs': loader, '.mts': loader, '.ts': loader, - '.yaml': yamlLoader, - '.yml': yamlLoader, }, searchPlaces: [ ...(options.searchPlaces ?? []), 'package.json', `.${moduleName}rc`, `.${moduleName}rc.json`, - `.${moduleName}rc.yaml`, - `.${moduleName}rc.yml`, `.${moduleName}rc.ts`, `.${moduleName}rc.cts`, `.${moduleName}rc.mts`, @@ -124,8 +114,8 @@ export { postCssLoadConfig }; // NextJS uses an incompatible format for PostCSS plugins, we make an attempt to fix it if (error.message.includes('Invalid PostCSS Plugin found')) { // Second attempt: try with modified config + const originalContent = await readFile(configPath, 'utf8'); try { - const originalContent = await readFile(configPath, 'utf8'); const modifiedContent = originalContent.replace( 'plugins: ["@tailwindcss/postcss"]', 'plugins: { "@tailwindcss/postcss": {} }' @@ -138,7 +128,9 @@ export { postCssLoadConfig }; await postCssLoadConfig({}, searchPath, { stopDir: getProjectRoot() }); return true; // Success with modified config! } catch (e: any) { - // We were unable to fix the config, so we throw an error + // We were unable to fix the config, so we change the file back to the original content + await writeFile(configPath, originalContent, 'utf8'); + // and throw an error throw new IncompatiblePostCssConfigError({ error }); } } From 857bfc470b08424953c08be681952fc2985d1fff Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Fri, 10 Oct 2025 13:50:07 +0200 Subject: [PATCH 5/5] Remove YAML dependency from Next.js Vite package configuration and update yarn.lock accordingly --- code/frameworks/nextjs-vite/package.json | 3 +-- code/yarn.lock | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/code/frameworks/nextjs-vite/package.json b/code/frameworks/nextjs-vite/package.json index 11fa6d96bb21..5f5c44e08334 100644 --- a/code/frameworks/nextjs-vite/package.json +++ b/code/frameworks/nextjs-vite/package.json @@ -91,8 +91,7 @@ "next": "^15.2.3", "postcss-load-config": "^6.0.1", "semver": "^7.3.5", - "typescript": "^5.8.3", - "yaml": "^2.8.1" + "typescript": "^5.8.3" }, "peerDependencies": { "next": "^14.1.0 || ^15.0.0", diff --git a/code/yarn.lock b/code/yarn.lock index 706309d2aa09..fbc0b812a177 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6571,7 +6571,6 @@ __metadata: styled-jsx: "npm:5.1.6" typescript: "npm:^5.8.3" vite-plugin-storybook-nextjs: "npm:^2.0.7" - yaml: "npm:^2.8.1" peerDependencies: next: ^14.1.0 || ^15.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -27552,7 +27551,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.0.0, yaml@npm:^2.3.1, yaml@npm:^2.6.0, yaml@npm:^2.8.0, yaml@npm:^2.8.1": +"yaml@npm:^2.0.0, yaml@npm:^2.3.1, yaml@npm:^2.6.0, yaml@npm:^2.8.0": version: 2.8.1 resolution: "yaml@npm:2.8.1" bin: