Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .changeset/postcss-app-root-plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modern-js/builder': patch
---

fix: resolve PostCSS plugins from the app root when needed
73 changes: 53 additions & 20 deletions packages/cli/builder/src/plugins/postcss.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
import { createRequire } from 'node:module';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import { applyOptionsChain, isProd } from '@modern-js/utils';
import { type RsbuildPlugin, logger } from '@rsbuild/core';
import type { Options } from 'cssnano';
import { getCssSupport } from '../shared/getCssSupport';
import type { ToolsAutoprefixerConfig } from '../types';

const require = createRequire(import.meta.url);
const builderRequire = createRequire(import.meta.url);

const importPostcssPlugin = (name: string) =>
Promise.resolve(require(name)) as Promise<any>;
const createRootRequire = (rootPath: string) =>
createRequire(pathToFileURL(path.join(rootPath, 'package.json')).href);

export const loadPostcssPlugin = (name: string, appRootPath: string) => {
const resolvers = [
builderRequire,
createRootRequire(appRootPath),
createRootRequire(process.cwd()),
];

let firstError: unknown = null;

for (const resolveWith of resolvers) {
try {
return resolveWith(name);
} catch (error) {
firstError ??= error;
}
Comment on lines +27 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Re-throw plugin load errors instead of falling back

When a resolver actually finds the package but the package throws during evaluation (for example an app-root PostCSS plugin has an ESM-only entry or a missing transitive dependency), this catch treats it the same as a lookup miss and tries the next roots. In the new app-root fallback path that can load a different copy from process.cwd() or, if none exists, throw the saved builder MODULE_NOT_FOUND, hiding the real failure. Please only continue on MODULE_NOT_FOUND for the requested name and rethrow other load errors.

Useful? React with 👍 / 👎.

}

throw firstError;
};

const importPostcssPlugin = (name: string, appRootPath: string) =>
Promise.resolve(loadPostcssPlugin(name, appRootPath)) as Promise<any>;

export interface PluginPostcssOptions {
autoprefixer?: ToolsAutoprefixerConfig;
Expand Down Expand Up @@ -54,29 +79,37 @@ export const pluginPostcss = (
};

const plugins = await Promise.all([
importPostcssPlugin('postcss-flexbugs-fixes'),
importPostcssPlugin('postcss-flexbugs-fixes', api.context.rootPath),
!cssSupport.customProperties &&
importPostcssPlugin('postcss-custom-properties'),
!cssSupport.initial && importPostcssPlugin('postcss-initial'),
!cssSupport.pageBreak && importPostcssPlugin('postcss-page-break'),
!cssSupport.fontVariant && importPostcssPlugin('postcss-font-variant'),
!cssSupport.mediaMinmax && importPostcssPlugin('postcss-media-minmax'),
importPostcssPlugin('postcss-nesting'),
importPostcssPlugin(
'postcss-custom-properties',
api.context.rootPath,
),
!cssSupport.initial &&
importPostcssPlugin('postcss-initial', api.context.rootPath),
!cssSupport.pageBreak &&
importPostcssPlugin('postcss-page-break', api.context.rootPath),
!cssSupport.fontVariant &&
importPostcssPlugin('postcss-font-variant', api.context.rootPath),
!cssSupport.mediaMinmax &&
importPostcssPlugin('postcss-media-minmax', api.context.rootPath),
importPostcssPlugin('postcss-nesting', api.context.rootPath),
enableCssMinify &&
importPostcssPlugin('cssnano').then(cssnano =>
importPostcssPlugin('cssnano', api.context.rootPath).then(cssnano =>
cssnano(cssnanoOptions),
),
// The last insert autoprefixer
importPostcssPlugin('autoprefixer').then(autoprefixerPlugin =>
autoprefixerPlugin(
applyOptionsChain(
{
flexbox: 'no-2009',
overrideBrowserslist: config.output.overrideBrowserslist!,
},
autoprefixer,
importPostcssPlugin('autoprefixer', api.context.rootPath).then(
autoprefixerPlugin =>
autoprefixerPlugin(
applyOptionsChain(
{
flexbox: 'no-2009',
overrideBrowserslist: config.output.overrideBrowserslist!,
},
autoprefixer,
),
),
),
),
]).then(results => results.filter(Boolean));

Expand Down
37 changes: 36 additions & 1 deletion packages/cli/builder/tests/postcssLegacy.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,45 @@
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, it, rs } from '@rstest/core';
import { createBuilder } from '../src';
import { loadPostcssPlugin } from '../src/plugins/postcss';
import { matchRules, unwrapConfig } from './helper';

const tempDirs: string[] = [];

describe('plugin-postcssLegacy', () => {
afterEach(() => {
afterEach(async () => {
rs.unstubAllEnvs();
await Promise.all(
tempDirs.map(dir => rm(dir, { recursive: true, force: true })),
);
tempDirs.length = 0;
});

it('should resolve postcss plugin from app root', async () => {
const pluginName = 'postcss-app-root-plugin';
const appRoot = await mkdtemp(path.join(tmpdir(), 'builder-postcss-'));
tempDirs.push(appRoot);

const pluginDir = path.join(appRoot, 'node_modules', pluginName);
await mkdir(pluginDir, { recursive: true });
await writeFile(
path.join(appRoot, 'package.json'),
JSON.stringify({ name: 'app-root' }),
);
await writeFile(
path.join(pluginDir, 'package.json'),
JSON.stringify({ name: pluginName, main: 'index.js' }),
);
await writeFile(
path.join(pluginDir, 'index.js'),
"module.exports = { postcssPlugin: 'postcss-app-root-plugin' };",
);

expect(loadPostcssPlugin(pluginName, appRoot)).toEqual({
postcssPlugin: pluginName,
});
});

it('should register postcss plugin by browserslist', async () => {
Expand Down