Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions code/frameworks/nextjs-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
123 changes: 123 additions & 0 deletions code/frameworks/nextjs-vite/src/find-postcss-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// @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<string | null>} 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<boolean> => {
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(
'plugins: ["@tailwindcss/postcss"]',
'plugins: { "@tailwindcss/postcss": {} }'
);
Comment on lines +119 to +122
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach will not work for yml files.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think we need it.

I had to copy code from here:
https://github.com/postcss/postcss-load-config/blob/main/src/index.js

And i wanted to keep it close to the original code.

yaml is already included in the bundle.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't support modifying YAML-based PostCSS files, we should clean up the code above (searching for YAML files, registering a YAML config loader,...). Then we can also actually remove the yaml dependency.

Copy link
Member Author

@ndelangen ndelangen Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You would rather not keep the code similar to where we got it from? Okay.

Their code seems to prefer yaml config files, so our code skipping/not looking for them, might cause their code to find different files, if the user has many different ones, possibly on multiple levels.

The replacement code, also does not "work" for JSON files, should I remove handling for those as well?


// 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;
};
20 changes: 6 additions & 14 deletions code/frameworks/nextjs-vite/src/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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>('frameworkOptions');
Expand Down
3 changes: 2 additions & 1 deletion code/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
Loading