Skip to content

Commit 619431e

Browse files
committed
Best-effort attempt to fix PostCSS path resolution
1 parent 10f87e1 commit 619431e

File tree

8 files changed

+355
-29
lines changed

8 files changed

+355
-29
lines changed

programs/develop/webpack/plugin-css/__spec__/common-style-loaders.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ vi.mock('../css-tools/less', () => ({
1313
}))
1414

1515
vi.mock('../css-tools/postcss', () => ({
16+
isUsingPostCss: vi.fn(() => false),
1617
maybeUsePostCss: vi.fn(async () => ({}))
1718
}))
1819

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {describe, it, expect, vi, beforeEach} from 'vitest'
2+
3+
describe('loadPluginsFromUserConfig helper', () => {
4+
beforeEach(() => {
5+
vi.resetModules()
6+
vi.clearAllMocks()
7+
})
8+
9+
it('normalizes array, tuple, map and postcss property forms', async () => {
10+
vi.doMock('module', () => ({
11+
createRequire: (_: string) => {
12+
const req = (id: string) => {
13+
if (id.endsWith('postcss.config.js')) {
14+
return {
15+
plugins: [
16+
'plugin-a',
17+
['plugin-b', {b: true}],
18+
{postcss: () => 'plugin-c-fn'},
19+
{'plugin-d': true, 'plugin-e': {e: 1}, 'plugin-f': false}
20+
]
21+
}
22+
}
23+
if (id === 'plugin-a') return () => 'plugin-a-fn'
24+
if (id === 'plugin-b') return () => 'plugin-b-fn'
25+
if (id === 'plugin-d') return () => 'plugin-d-fn'
26+
if (id === 'plugin-e') return () => 'plugin-e-fn'
27+
throw new Error('unexpected require: ' + id)
28+
}
29+
return req as any
30+
}
31+
}))
32+
33+
const {loadPluginsFromUserConfig} = await import(
34+
'../../css-tools/postcss/load-plugins-from-user-config'
35+
)
36+
const res = await loadPluginsFromUserConfig(
37+
'/p',
38+
'/p/postcss.config.js',
39+
'development'
40+
)
41+
expect(res && res.length).toBe(5)
42+
})
43+
44+
it('returns empty list when plugins missing', async () => {
45+
vi.doMock('module', () => ({
46+
createRequire: (_: string) => {
47+
const req = (id: string) => {
48+
if (id.endsWith('postcss.config.js')) {
49+
return {}
50+
}
51+
throw new Error('unexpected require: ' + id)
52+
}
53+
return req as any
54+
}
55+
}))
56+
57+
const {loadPluginsFromUserConfig} = await import(
58+
'../../css-tools/postcss/load-plugins-from-user-config'
59+
)
60+
const res = await loadPluginsFromUserConfig(
61+
'/p',
62+
'/p/postcss.config.js',
63+
'production'
64+
)
65+
expect(Array.isArray(res)).toBe(true)
66+
expect(res?.length).toBe(0)
67+
})
68+
})

programs/develop/webpack/plugin-css/__spec__/css-tools/postcss.spec.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,119 @@ describe('postcss detection', () => {
2727
const {isUsingPostCss} = await import('../../css-tools/postcss')
2828
expect(isUsingPostCss('/p')).toBe(true)
2929
})
30+
31+
it('loads plugins from file-based postcss config and disables config discovery', async () => {
32+
// Simulate presence of postcss.config.js
33+
vi.doMock('fs', async () => {
34+
const actual = await vi.importActual<any>('fs')
35+
return {
36+
...actual,
37+
existsSync: (p: string) =>
38+
String(p).endsWith('postcss.config.js') || actual.existsSync(p),
39+
readFileSync: actual.readFileSync
40+
}
41+
})
42+
43+
// Mock createRequire to return our config and plugin modules
44+
vi.doMock('module', () => ({
45+
createRequire: (_: string) => {
46+
const req = (id: string) => {
47+
if (id.endsWith('postcss.config.js')) {
48+
// Return mixed plugin shapes: string, tuple, map
49+
return {
50+
plugins: [
51+
'plugin-a',
52+
['plugin-b', {b: true}],
53+
{postcss: () => 'plugin-c-fn'},
54+
{'plugin-d': true, 'plugin-e': {e: 1}, 'plugin-f': false}
55+
]
56+
}
57+
}
58+
if (id === 'plugin-a') return () => 'plugin-a-fn'
59+
if (id === 'plugin-b') return () => 'plugin-b-fn'
60+
if (id === 'plugin-d') return () => 'plugin-d-fn'
61+
if (id === 'plugin-e') return () => 'plugin-e-fn'
62+
throw new Error('unexpected require: ' + id)
63+
}
64+
return req as any
65+
}
66+
}))
67+
68+
const {maybeUsePostCss} = await import('../../css-tools/postcss')
69+
const rule = await maybeUsePostCss('/p', {mode: 'development'})
70+
// Ensure loader configured
71+
expect(rule.loader).toBeDefined()
72+
const opts = rule.options?.postcssOptions
73+
expect(opts?.config).toBe(false)
74+
// Expect normalized plugins present (5 entries: a, b, c, d, e; f=false is skipped)
75+
expect(Array.isArray(opts?.plugins)).toBe(true)
76+
expect((opts?.plugins as any[]).length).toBe(5)
77+
})
78+
79+
it('supports postcss.config.mjs presence and disables config discovery', async () => {
80+
// Simulate presence of postcss.config.mjs; still use createRequire path in tests
81+
vi.doMock('fs', async () => {
82+
const actual = await vi.importActual<any>('fs')
83+
return {
84+
...actual,
85+
existsSync: (p: string) =>
86+
String(p).endsWith('postcss.config.mjs') || actual.existsSync(p),
87+
readFileSync: actual.readFileSync
88+
}
89+
})
90+
91+
vi.doMock('module', () => ({
92+
createRequire: (_: string) => {
93+
const req = (id: string) => {
94+
if (id.endsWith('postcss.config.mjs')) {
95+
return {
96+
plugins: {
97+
'plugin-x': true,
98+
'plugin-y': {y: 2}
99+
}
100+
}
101+
}
102+
if (id === 'plugin-x') return () => 'x'
103+
if (id === 'plugin-y') return () => 'y'
104+
throw new Error('unexpected require: ' + id)
105+
}
106+
return req as any
107+
}
108+
}))
109+
110+
const {maybeUsePostCss} = await import('../../css-tools/postcss')
111+
const rule = await maybeUsePostCss('/p', {mode: 'production'})
112+
const opts = rule.options?.postcssOptions
113+
expect(opts?.config).toBe(false)
114+
expect(Array.isArray(opts?.plugins)).toBe(true)
115+
expect((opts?.plugins as any[]).length).toBe(2)
116+
})
117+
118+
it('uses project-root discovery when only package.json contains postcss config', async () => {
119+
vi.doMock('fs', async () => {
120+
const actual = await vi.importActual<any>('fs')
121+
return {
122+
...actual,
123+
existsSync: (p: string) => {
124+
// No file configs
125+
if (String(p).includes('postcss.config')) return false
126+
return actual.existsSync(p)
127+
},
128+
readFileSync: (p: string, enc: string) => {
129+
if (String(p).endsWith('package.json')) {
130+
return JSON.stringify({postcss: {}})
131+
}
132+
return (actual as any).readFileSync(p, enc)
133+
}
134+
}
135+
})
136+
137+
const {maybeUsePostCss} = await import('../../css-tools/postcss')
138+
const rule = await maybeUsePostCss('/p', {mode: 'development'})
139+
const opts = rule.options?.postcssOptions
140+
expect(opts?.config).toBe('/p')
141+
// When using user config via discovery, we don't pre-supply plugins
142+
expect(Array.isArray(opts?.plugins)).toBe(true)
143+
expect((opts?.plugins as any[]).length).toBe(0)
144+
})
30145
})

programs/develop/webpack/plugin-css/__spec__/tools.spec.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,9 @@ describe('css tools additional coverage', () => {
141141
const res = await maybeUsePostCss('/project', {mode: 'development'})
142142
expect(res.loader).toBeDefined()
143143
expect(String(res.loader)).toContain('postcss-loader')
144-
expect(
145-
res.options?.postcssOptions?.config?.endsWith('postcss.config.js')
146-
).toBe(true)
144+
// Since the test doesn't provide a real config module to load,
145+
// we fall back to discovery from the project root
146+
expect(res.options?.postcssOptions?.config).toBe('/project')
147+
expect(Array.isArray(res.options?.postcssOptions?.plugins)).toBe(true)
147148
})
148149
})

programs/develop/webpack/plugin-css/common-style-loaders.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {type RuleSetRule} from '@rspack/core'
22
import {isUsingTailwind} from './css-tools/tailwind'
33
import {isUsingSass} from './css-tools/sass'
44
import {isUsingLess} from './css-tools/less'
5-
import {maybeUsePostCss} from './css-tools/postcss'
5+
import {isUsingPostCss, maybeUsePostCss} from './css-tools/postcss'
66
import {type DevOptions} from '../webpack-types'
77

88
export interface StyleLoaderOptions {
@@ -19,6 +19,7 @@ export async function commonStyleLoaders(
1919

2020
// Handle PostCSS for Tailwind, Sass, or Less
2121
if (
22+
isUsingPostCss(projectPath) ||
2223
isUsingTailwind(projectPath) ||
2324
isUsingSass(projectPath) ||
2425
isUsingLess(projectPath)

programs/develop/webpack/plugin-css/css-tools/postcss.ts renamed to programs/develop/webpack/plugin-css/css-tools/postcss/index.ts

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@
88
import * as path from 'path'
99
import * as fs from 'fs'
1010
import {createRequire} from 'module'
11-
import * as messages from '../css-lib/messages'
11+
import * as messages from '../../css-lib/messages'
1212
import {
1313
installOptionalDependencies,
1414
hasDependency
15-
} from '../css-lib/integrations'
16-
import {isUsingTailwind} from './tailwind'
17-
import {isUsingSass} from './sass'
18-
import {isUsingLess} from './less'
19-
import type {StyleLoaderOptions} from '../common-style-loaders'
15+
} from '../../css-lib/integrations'
16+
import {isUsingTailwind} from '../tailwind'
17+
import {isUsingSass} from '../sass'
18+
import {isUsingLess} from '../less'
19+
import type {StyleLoaderOptions} from '../../common-style-loaders'
20+
import {loadPluginsFromUserConfig} from './load-plugins-from-user-config'
2021

2122
let userMessageDelivered = false
2223

@@ -25,6 +26,7 @@ const postCssConfigFiles = [
2526
'.postcssrc.json',
2627
'.postcssrc.yaml',
2728
'.postcssrc.yml',
29+
'postcss.config.mjs',
2830
'.postcssrc.js',
2931
'.postcssrc.cjs',
3032
'postcss.config.js',
@@ -87,6 +89,16 @@ export async function maybeUsePostCss(
8789

8890
const userPostCssConfig = findPostCssConfig(projectPath)
8991

92+
function hasPostCssInPackageJson(p: string): boolean {
93+
try {
94+
const raw = fs.readFileSync(path.join(p, 'package.json'), 'utf8')
95+
const pkg = JSON.parse(raw || '{}')
96+
return !!pkg?.postcss
97+
} catch {
98+
return false
99+
}
100+
}
101+
90102
// Resolve the project's own PostCSS implementation to avoid resolving from the toolchain
91103
function getProjectPostcssImpl(): any {
92104
try {
@@ -120,6 +132,14 @@ export async function maybeUsePostCss(
120132
process.exit(0)
121133
}
122134

135+
const pkgHasPostCss = hasPostCssInPackageJson(projectPath)
136+
const loadedPlugins = await loadPluginsFromUserConfig(
137+
projectPath,
138+
userPostCssConfig,
139+
opts.mode
140+
)
141+
const useUserConfig = !!userPostCssConfig || pkgHasPostCss
142+
123143
return {
124144
test: /\.css$/,
125145
type: 'css',
@@ -129,24 +149,31 @@ export async function maybeUsePostCss(
129149
implementation: getProjectPostcssImpl(),
130150
postcssOptions: {
131151
ident: 'postcss',
132-
// When the user has a config, pass the string path (tests rely on this being a string)
133-
// Otherwise, disable auto discovery to avoid resolving outside the project.
134-
config: userPostCssConfig ? userPostCssConfig : false,
135-
// If the user has their own PostCSS config, defer entirely to it.
136-
// Otherwise, apply a sensible default with postcss-preset-env.
137-
plugins: userPostCssConfig
138-
? []
139-
: [
140-
[
141-
'postcss-preset-env',
142-
{
143-
autoprefixer: {
144-
flexbox: 'no-2009'
145-
},
146-
stage: 3
147-
}
148-
]
149-
].filter(Boolean)
152+
// Ensure resolution happens from the project root, never the toolchain/cache
153+
cwd: projectPath,
154+
// If we successfully loaded a file config, disable rediscovery and pass plugins directly.
155+
// Else if there's a package.json "postcss", allow discovery starting from the project path.
156+
// Otherwise disable config discovery and fall back to defaults.
157+
config: loadedPlugins ? false : useUserConfig ? projectPath : false,
158+
// If the user has a config (file or package.json), let it drive plugins.
159+
// When we loaded a file config ourselves, provide the normalized plugins explicitly.
160+
// Otherwise, provide a default preset.
161+
plugins:
162+
loadedPlugins !== undefined
163+
? loadedPlugins
164+
: useUserConfig
165+
? []
166+
: [
167+
[
168+
'postcss-preset-env',
169+
{
170+
autoprefixer: {
171+
flexbox: 'no-2009'
172+
},
173+
stage: 3
174+
}
175+
]
176+
].filter(Boolean)
150177
},
151178
sourceMap: opts.mode === 'development'
152179
}

0 commit comments

Comments
 (0)