diff --git a/.changeset/postcss-app-root-plugins.md b/.changeset/postcss-app-root-plugins.md new file mode 100644 index 000000000000..9e146c9ea475 --- /dev/null +++ b/.changeset/postcss-app-root-plugins.md @@ -0,0 +1,5 @@ +--- +'@modern-js/builder': patch +--- + +fix: resolve PostCSS plugins from the app root when needed diff --git a/packages/cli/builder/src/plugins/postcss.ts b/packages/cli/builder/src/plugins/postcss.ts index f07af799e979..c845efdded82 100644 --- a/packages/cli/builder/src/plugins/postcss.ts +++ b/packages/cli/builder/src/plugins/postcss.ts @@ -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; +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; + } + } + + throw firstError; +}; + +const importPostcssPlugin = (name: string, appRootPath: string) => + Promise.resolve(loadPostcssPlugin(name, appRootPath)) as Promise; export interface PluginPostcssOptions { autoprefixer?: ToolsAutoprefixerConfig; @@ -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)); diff --git a/packages/cli/builder/tests/postcssLegacy.test.ts b/packages/cli/builder/tests/postcssLegacy.test.ts index a54800593f21..d952ed63dd9c 100644 --- a/packages/cli/builder/tests/postcssLegacy.test.ts +++ b/packages/cli/builder/tests/postcssLegacy.test.ts @@ -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 () => {