Fix native ESM config loading in v3 (#18938)

Unfortunately for backwards compatibility purposes (with `loadConfig` at
least) we can't switch things to use `import(…)` because there's baked
in knowledge that the config is loaded synchronously for v3.

This PR does two things:
- Defers to `require(…)` which allows newer versions that support
`require(esm)` to work natively. This works around the need to switch to
`import(…)` for those versions.
- Allows newer versions of `postcss-load-config` enabling better
ESM+TypeScript support for PostCSS configs in the CLI.

We support v4, v5, and v6 of `postcss-load-config` simultaneously so any
of those versions should work. I've verified that newer node versions
seem to install v6 while earlier ones like Node v14 install v4 of
`postcss-load-config`. So this should be a backwards compatible change.

- [x] needs tests for `import.meta.resolve(…)`
- [x] needs tests for ESM postcss configs

Fixes #14152
Fixes #14423
This commit is contained in:
Jordan Pittman 2025-09-16 14:03:09 -04:00 committed by GitHub
parent ff52f8caf2
commit ba55a445cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 164 additions and 45 deletions

View File

@ -27,7 +27,7 @@ jobs:
strategy:
matrix:
node-version: [14, 18]
node-version: [14, 18, 20, 22, 24]
steps:
- uses: actions/checkout@v3

View File

@ -24,7 +24,7 @@ jobs:
strategy:
matrix:
node-version: [14, 18]
node-version: [14, 18, 20, 22, 24]
steps:
- uses: actions/checkout@v3

View File

@ -31,7 +31,7 @@ jobs:
- vite
- webpack-4
- webpack-5
node-version: [18]
node-version: [18, 20]
fail-fast: false
steps:

View File

@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improve support for raw `supports-[…]` queries in arbitrary values ([#13605](https://github.com/tailwindlabs/tailwindcss/pull/13605))
- Fix `require.cache` error when loaded through a TypeScript file in Node 22.18+ ([#18665](https://github.com/tailwindlabs/tailwindcss/pull/18665))
- Support `import.meta.resolve(…)` in configs for new enough Node.js versions ([#18938](https://github.com/tailwindlabs/tailwindcss/pull/18938))
- Allow using newer versions of `postcss-load-config` for better ESM and TypeScript PostCSS config support with the CLI ([#18938](https://github.com/tailwindlabs/tailwindcss/pull/18938))
## [3.4.17] - 2024-12-17

View File

@ -207,6 +207,52 @@ describe('Build command', () => {
)
})
test('configs support import.meta', async () => {
// Skip this test in Node 18 as this only works with
// `require(esm)` in Node 20.19+
if (process.versions.node.startsWith('18.')) {
expect(true).toBe(true)
return
}
await writeInputFile('index.html', html`<div class="font-bold"></div>`)
let customConfig = `
console.log(import.meta.url)
console.log(import.meta.resolve('./tailwind.config.mjs'))
export default ${JSON.stringify(
{
content: ['./src/index.html'],
theme: {
extend: {
fontWeight: {
bold: 'BOLD',
},
},
},
corePlugins: {
preflight: false,
},
plugins: [],
},
null,
2
)}
`
await writeInputFile('../tailwind.config.mjs', customConfig)
await $(`${EXECUTABLE} --output ./dist/main.css --config ./tailwind.config.mjs`)
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.font-bold {
font-weight: BOLD;
}
`
)
})
test('--content', async () => {
await writeInputFile('other.html', html`<div class="font-bold"></div>`)
@ -391,6 +437,79 @@ describe('Build command', () => {
expect(contents).toContain(`/*# sourceMappingURL`)
})
test('--postcss supports ESM configs', async () => {
await writeInputFile('index.html', html`<div class="font-bold"></div>`)
let customConfig = javascript`
import * as path from 'path'
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
export default {
map: { inline: true },
plugins: [
function tailwindcss() {
return require(path.resolve('..', '..'))
},
],
}
`
await removeFile('./postcss.config.js')
await writeInputFile('../postcss.config.mjs', customConfig)
await $(`${EXECUTABLE} --output ./dist/main.css --postcss`)
let contents = await readOutputFile('main.css')
expect(contents).toIncludeCss(
css`
.font-bold {
font-weight: 700;
}
`
)
expect(contents).toContain(`/*# sourceMappingURL`)
})
test('--postcss supports TS configs', async () => {
await writeInputFile('index.html', html`<div class="font-bold"></div>`)
let customConfig = javascript`
import * as path from 'path'
import { createRequire } from 'module'
import type { AcceptedPlugin } from 'postcss'
const require = createRequire(import.meta.url)
export default {
map: { inline: true },
plugins: [
function tailwindcss() {
return require(path.resolve('..', '..'))
} as AcceptedPlugin,
],
}
`
await removeFile('./postcss.config.js')
await writeInputFile('../postcss.config.ts', customConfig)
await $(`${EXECUTABLE} --output ./dist/main.css --postcss`)
let contents = await readOutputFile('main.css')
expect(contents).toIncludeCss(
css`
.font-bold {
font-weight: 700;
}
`
)
expect(contents).toContain(`/*# sourceMappingURL`)
})
test('postcss-import is supported by default', async () => {
cleanupFile('src/test.css')

54
package-lock.json generated
View File

@ -17,7 +17,7 @@
"fast-glob": "^3.3.2",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.21.6",
"jiti": "^1.21.7",
"lilconfig": "^3.1.3",
"micromatch": "^4.0.8",
"normalize-path": "^3.0.0",
@ -26,7 +26,7 @@
"postcss": "^8.4.47",
"postcss-import": "^15.1.0",
"postcss-js": "^4.0.1",
"postcss-load-config": "^4.0.2",
"postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
"postcss-nested": "^6.2.0",
"postcss-selector-parser": "^6.1.2",
"resolve": "^1.22.8",
@ -5446,9 +5446,9 @@
}
},
"node_modules/jiti": {
"version": "1.21.6",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
"integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==",
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"license": "MIT",
"bin": {
"jiti": "bin/jiti.js"
@ -6518,9 +6518,9 @@
}
},
"node_modules/postcss-load-config": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
"integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
"funding": [
{
"type": "opencollective",
@ -6533,21 +6533,28 @@
],
"license": "MIT",
"dependencies": {
"lilconfig": "^3.0.0",
"yaml": "^2.3.4"
"lilconfig": "^3.1.1"
},
"engines": {
"node": ">= 14"
"node": ">= 18"
},
"peerDependencies": {
"jiti": ">=1.21.0",
"postcss": ">=8.0.9",
"ts-node": ">=9.0.0"
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"jiti": {
"optional": true
},
"postcss": {
"optional": true
},
"ts-node": {
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
@ -8186,6 +8193,8 @@
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz",
"integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==",
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
@ -11832,9 +11841,9 @@
}
},
"jiti": {
"version": "1.21.6",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
"integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w=="
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="
},
"js-tokens": {
"version": "4.0.0",
@ -12464,12 +12473,11 @@
}
},
"postcss-load-config": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
"integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
"requires": {
"lilconfig": "^3.0.0",
"yaml": "^2.3.4"
"lilconfig": "^3.1.1"
}
},
"postcss-merge-longhand": {
@ -13506,7 +13514,9 @@
"yaml": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz",
"integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ=="
"integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==",
"optional": true,
"peer": true
},
"yargs": {
"version": "17.7.2",

View File

@ -72,7 +72,7 @@
"fast-glob": "^3.3.2",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.21.6",
"jiti": "^1.21.7",
"lilconfig": "^3.1.3",
"micromatch": "^4.0.8",
"normalize-path": "^3.0.0",
@ -81,7 +81,7 @@
"postcss": "^8.4.47",
"postcss-import": "^15.1.0",
"postcss-js": "^4.0.1",
"postcss-load-config": "^4.0.2",
"postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
"postcss-nested": "^6.2.0",
"postcss-selector-parser": "^6.1.2",
"resolve": "^1.22.8",

View File

@ -43,10 +43,13 @@ async function loadPostCssPlugins(customPostCssPath) {
config.plugins = []
}
// We have to await these because in v5 and v6 of postcss-load-config
// these functions return promises while they don't in v4. Awaiting a
// non-promise is basically a no-op so this is safe to do.
return {
file,
plugins: loadPlugins(config, file),
options: loadOptions(config, file),
plugins: await loadPlugins(config, file),
options: await loadOptions(config, file),
}
})()
: await postcssrc()

View File

@ -35,23 +35,8 @@ export function loadConfig(path: string): Config {
let config = (function () {
if (!path) return {}
// Always use jiti for now. There is a a bug that occurs in Node v22.12+
// where imported files return invalid results
return lazyJiti()(path)
// Always use jiti for ESM or TS files
if (
path &&
(path.endsWith('.mjs') ||
path.endsWith('.ts') ||
path.endsWith('.cts') ||
path.endsWith('.mts'))
) {
return lazyJiti()(path)
}
try {
return path ? require(path) : {}
return require(path)
} catch {
return lazyJiti()(path)
}