Skip to content

Commit 737f9e3

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

File tree

6 files changed

+113
-39
lines changed

6 files changed

+113
-39
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

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,72 @@ describe('postcss detection', () => {
2727
const {isUsingPostCss} = await import('../../css-tools/postcss')
2828
expect(isUsingPostCss('/p')).toBe(true)
2929
})
30+
31+
it('uses file-based postcss config discovered from project path', 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+
const {maybeUsePostCss} = await import('../../css-tools/postcss')
44+
const rule = await maybeUsePostCss('/p', {mode: 'development'})
45+
// Ensure loader configured
46+
expect(rule.loader).toBeDefined()
47+
const opts = rule.options?.postcssOptions
48+
expect(opts?.config).toBe('/p')
49+
// We don't pre-supply plugins; discovery will happen at runtime from /p
50+
expect(opts?.plugins).toBeUndefined()
51+
})
52+
53+
it('supports postcss.config.mjs discovered from project path', async () => {
54+
// Simulate presence of postcss.config.mjs; still use createRequire path in tests
55+
vi.doMock('fs', async () => {
56+
const actual = await vi.importActual<any>('fs')
57+
return {
58+
...actual,
59+
existsSync: (p: string) =>
60+
String(p).endsWith('postcss.config.mjs') || actual.existsSync(p),
61+
readFileSync: actual.readFileSync
62+
}
63+
})
64+
65+
const {maybeUsePostCss} = await import('../../css-tools/postcss')
66+
const rule = await maybeUsePostCss('/p', {mode: 'production'})
67+
const opts = rule.options?.postcssOptions
68+
expect(opts?.config).toBe('/p')
69+
expect(opts?.plugins).toBeUndefined()
70+
})
71+
72+
it('uses project-root discovery when only package.json contains postcss config', async () => {
73+
vi.doMock('fs', async () => {
74+
const actual = await vi.importActual<any>('fs')
75+
return {
76+
...actual,
77+
existsSync: (p: string) => {
78+
// No file configs
79+
if (String(p).includes('postcss.config')) return false
80+
return actual.existsSync(p)
81+
},
82+
readFileSync: (p: string, enc: string) => {
83+
if (String(p).endsWith('package.json')) {
84+
return JSON.stringify({postcss: {}})
85+
}
86+
return (actual as any).readFileSync(p, enc)
87+
}
88+
}
89+
})
90+
91+
const {maybeUsePostCss} = await import('../../css-tools/postcss')
92+
const rule = await maybeUsePostCss('/p', {mode: 'development'})
93+
const opts = rule.options?.postcssOptions
94+
expect(opts?.config).toBe('/p')
95+
// We don't pre-supply plugins
96+
expect(opts?.plugins).toBeUndefined()
97+
})
3098
})

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(res.options?.postcssOptions?.plugins).toBeUndefined()
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

Lines changed: 35 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const postCssConfigFiles = [
2525
'.postcssrc.json',
2626
'.postcssrc.yaml',
2727
'.postcssrc.yml',
28+
'postcss.config.mjs',
2829
'.postcssrc.js',
2930
'.postcssrc.cjs',
3031
'postcss.config.js',
@@ -83,35 +84,33 @@ export async function maybeUsePostCss(
8384
projectPath: string,
8485
opts: StyleLoaderOptions
8586
): Promise<Record<string, any>> {
86-
if (!isUsingPostCss(projectPath)) return {}
87-
8887
const userPostCssConfig = findPostCssConfig(projectPath)
8988

90-
// Resolve the project's own PostCSS implementation to avoid resolving from the toolchain
91-
function getProjectPostcssImpl(): any {
89+
function hasPostCssInPackageJson(p: string): boolean {
9290
try {
93-
const req = createRequire(path.join(projectPath, 'package.json'))
94-
return req('postcss')
91+
const raw = fs.readFileSync(path.join(p, 'package.json'), 'utf8')
92+
const pkg = JSON.parse(raw || '{}')
93+
return !!pkg?.postcss
9594
} catch {
96-
try {
97-
// Fallback to loader's postcss if not found in project
98-
// eslint-disable-next-line @typescript-eslint/no-var-requires
99-
return require('postcss')
100-
} catch {
101-
return undefined
102-
}
95+
return false
10396
}
10497
}
10598

99+
const pkgHasPostCss = hasPostCssInPackageJson(projectPath)
100+
const tailwindPresent = isUsingTailwind(projectPath)
101+
102+
// Only add postcss-loader when there's a clear signal of usage
103+
if (!userPostCssConfig && !pkgHasPostCss && !tailwindPresent) {
104+
return {}
105+
}
106+
106107
try {
107108
require.resolve('postcss-loader')
108109
} catch (e) {
109110
// SASS and LESS will install PostCSS as a dependency
110111
// so we don't need to check for it here.
111112
if (!isUsingSass(projectPath) && !isUsingLess(projectPath)) {
112-
const postCssDependencies = userPostCssConfig
113-
? ['postcss', 'postcss-loader']
114-
: ['postcss', 'postcss-loader', 'postcss-preset-env']
113+
const postCssDependencies = ['postcss', 'postcss-loader']
115114

116115
await installOptionalDependencies('PostCSS', postCssDependencies)
117116
}
@@ -120,6 +119,22 @@ export async function maybeUsePostCss(
120119
process.exit(0)
121120
}
122121

122+
// Resolve the project's own PostCSS implementation to avoid resolving from the toolchain
123+
function getProjectPostcssImpl(): any {
124+
try {
125+
const req = createRequire(path.join(projectPath, 'package.json'))
126+
return req('postcss')
127+
} catch {
128+
try {
129+
// Fallback to loader's postcss if not found in project
130+
// eslint-disable-next-line @typescript-eslint/no-var-requires
131+
return require('postcss')
132+
} catch {
133+
return undefined
134+
}
135+
}
136+
}
137+
123138
return {
124139
test: /\.css$/,
125140
type: 'css',
@@ -129,24 +144,10 @@ export async function maybeUsePostCss(
129144
implementation: getProjectPostcssImpl(),
130145
postcssOptions: {
131146
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)
147+
// Ensure resolution and discovery happen from the project root
148+
cwd: projectPath,
149+
// Let postcss-load-config discover config/plugins from the package path
150+
config: projectPath
150151
},
151152
sourceMap: opts.mode === 'development'
152153
}

programs/develop/webpack/plugin-css/css-tools/tailwind.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import {hasDependency} from '../css-lib/integrations'
1313
let userMessageDelivered = false
1414

1515
export function isUsingTailwind(projectPath: string) {
16-
const isUsingTailwind = hasDependency(projectPath, 'tailwindcss')
16+
const isUsingTailwind =
17+
hasDependency(projectPath, 'tailwindcss') ||
18+
hasDependency(projectPath, '@tailwindcss/postcss')
1719

1820
if (isUsingTailwind) {
1921
if (!userMessageDelivered) {

0 commit comments

Comments
 (0)