Use resolveJsId when resolving tailwindcss/package.json (#15041)

This PR uses the `enhanced-resolve` instead of
`createRequire(…).resolve` which improves the usability when running the
upgrade tool locally using Bun.

While testing, we also noticed that it is not possible to use a
`cjs`-only plugin inside of an `esm` project. It was also not possible
to use an `esm`-only plugin inside of a `cjs` project.

# Test plan

We added integration tests in both the CLI (the CLI is an mjs project)
and in the PostCSS (where we can configure a `cjs` and `esm` PostCSS
config) integration tests where we created an `esm` and `cjs` based
project with 4 plugins (`cjs`-only, `esm`-only, and TypeScript based
plugins: `cts`-only and `mts`-only).
This commit is contained in:
Robin Malfait 2024-11-19 18:39:49 +01:00 committed by GitHub
parent e4bfa8c9b7
commit fe9fc9abba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 365 additions and 16 deletions

View File

@ -23,11 +23,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Ensure `flex` is suggested ([#15014](https://github.com/tailwindlabs/tailwindcss/pull/15014))
- Improve module resolution for `cjs`-only and `esm`-only plugins ([#15041](https://github.com/tailwindlabs/tailwindcss/pull/15041))
- _Upgrade (experimental)_: Resolve imports when specifying a CSS entry point on the command-line ([#15010](https://github.com/tailwindlabs/tailwindcss/pull/15010))
- _Upgrade (experimental)_: Resolve nearest Tailwind config file when CSS file does not contain `@config` ([#15001](https://github.com/tailwindlabs/tailwindcss/pull/15001))
- _Upgrade (experimental)_: Improve output when CSS imports can not be found ([#15038](https://github.com/tailwindlabs/tailwindcss/pull/15038))
- _Upgrade (experimental)_: Ignore analyzing imports with external URLs (e.g.: `@import "https://fonts.google.com"`) ([#15040](https://github.com/tailwindlabs/tailwindcss/pull/15040))
- _Upgrade (experimental)_: Ignore analyzing imports with `url(…)` (e.g.: `@import url("https://fonts.google.com")`) ([#15040](https://github.com/tailwindlabs/tailwindcss/pull/15040))
- _Upgrade (experimental)_: Use `resolveJsId` when resolving `tailwindcss/package.json` ([#15041](https://github.com/tailwindlabs/tailwindcss/pull/15041))
### Changed

View File

@ -2,7 +2,7 @@ import dedent from 'dedent'
import os from 'node:os'
import path from 'node:path'
import { describe, expect } from 'vitest'
import { candidate, css, html, js, json, test, yaml } from '../utils'
import { candidate, css, html, js, json, test, ts, yaml } from '../utils'
const STANDALONE_BINARY = (() => {
switch (os.platform()) {
@ -257,6 +257,161 @@ describe.each([
await fs.expectFileToContain('dist/out.css', [candidate`underline`])
},
)
test(
'module resolution using CJS, ESM, CTS, and MTS',
{
fs: {
'package.json': json`{}`,
'pnpm-workspace.yaml': yaml`
#
packages:
- project-cjs
- project-esm
- plugin-cjs
- plugin-esm
- plugin-cts
- plugin-mts
`,
'project-cjs/package.json': json`
{
"type": "commonjs",
"dependencies": {
"tailwindcss": "workspace:^",
"@tailwindcss/cli": "workspace:^",
"plugin-cjs": "workspace:*",
"plugin-esm": "workspace:*",
"plugin-cts": "workspace:*",
"plugin-mts": "workspace:*"
}
}
`,
'project-cjs/index.html': html`
<div class="cjs esm cts mts"></div>
`,
'project-cjs/src/index.css': css`
@import 'tailwindcss/utilities';
@plugin 'plugin-cjs';
@plugin 'plugin-esm';
@plugin 'plugin-cts';
@plugin 'plugin-mts';
`,
'project-esm/package.json': json`
{
"type": "module",
"dependencies": {
"tailwindcss": "workspace:^",
"@tailwindcss/cli": "workspace:^",
"plugin-cjs": "workspace:*",
"plugin-esm": "workspace:*",
"plugin-cts": "workspace:*",
"plugin-mts": "workspace:*"
}
}
`,
'project-esm/index.html': html`
<div class="cjs esm cts mts"></div>
`,
'project-esm/src/index.css': css`
@import 'tailwindcss/utilities';
@plugin 'plugin-cjs';
@plugin 'plugin-esm';
@plugin 'plugin-cts';
@plugin 'plugin-mts';
`,
'plugin-cjs/package.json': json`
{
"name": "plugin-cjs",
"type": "commonjs",
"exports": {
".": {
"require": "./index.cjs"
}
}
}
`,
'plugin-cjs/index.cjs': js`
module.exports = function ({ addUtilities }) {
addUtilities({ '.cjs': { content: '"cjs"' } })
}
`,
'plugin-esm/package.json': json`
{
"name": "plugin-esm",
"type": "module",
"exports": {
".": {
"import": "./index.mjs"
}
}
}
`,
'plugin-esm/index.mjs': js`
export default function ({ addUtilities }) {
addUtilities({ '.esm': { content: '"esm"' } })
}
`,
'plugin-cts/package.json': json`
{
"name": "plugin-cts",
"type": "commonjs",
"exports": {
".": {
"require": "./index.cts"
}
}
}
`,
'plugin-cts/index.cts': ts`
export default function ({ addUtilities }) {
addUtilities({ '.cts': { content: '"cts"' as const } })
}
`,
'plugin-mts/package.json': json`
{
"name": "plugin-mts",
"type": "module",
"exports": {
".": {
"import": "./index.mts"
}
}
}
`,
'plugin-mts/index.mts': ts`
export default function ({ addUtilities }) {
addUtilities({ '.mts': { content: '"mts"' as const } })
}
`,
},
},
async ({ root, fs, exec }) => {
await exec(`${command} --input src/index.css --output dist/out.css`, {
cwd: path.join(root, 'project-cjs'),
})
await exec(`${command} --input src/index.css --output dist/out.css`, {
cwd: path.join(root, 'project-esm'),
})
await fs.expectFileToContain('./project-cjs/dist/out.css', [
candidate`cjs`,
candidate`esm`,
candidate`cts`,
candidate`mts`,
])
await fs.expectFileToContain('./project-esm/dist/out.css', [
candidate`cjs`,
candidate`esm`,
candidate`cts`,
candidate`mts`,
])
},
)
})
test(

View File

@ -1,7 +1,7 @@
import dedent from 'dedent'
import path from 'node:path'
import { expect } from 'vitest'
import { candidate, css, html, js, json, test, yaml } from '../utils'
import { candidate, css, html, js, json, test, ts, yaml } from '../utils'
test(
'production build (string)',
@ -315,6 +315,177 @@ test(
},
)
test(
'module resolution using CJS, ESM, CTS, and MTS',
{
fs: {
'package.json': json`{}`,
'pnpm-workspace.yaml': yaml`
#
packages:
- project-cjs
- project-esm
- plugin-cjs
- plugin-esm
- plugin-cts
- plugin-mts
`,
'project-cjs/package.json': json`
{
"type": "commonjs",
"dependencies": {
"@tailwindcss/postcss": "workspace:^",
"plugin-cjs": "workspace:*",
"plugin-cts": "workspace:*",
"plugin-esm": "workspace:*",
"plugin-mts": "workspace:*",
"postcss": "^8",
"postcss-cli": "^10",
"tailwindcss": "workspace:^"
}
}
`,
'project-cjs/postcss.config.cjs': js`
let tailwindcss = require('@tailwindcss/postcss')
module.exports = {
plugins: [tailwindcss()],
}
`,
'project-cjs/index.html': html`
<div class="cjs esm cts mts"></div>
`,
'project-cjs/src/index.css': css`
@import 'tailwindcss/utilities';
@plugin 'plugin-cjs';
@plugin 'plugin-esm';
@plugin 'plugin-cts';
@plugin 'plugin-mts';
`,
'project-esm/package.json': json`
{
"type": "module",
"dependencies": {
"@tailwindcss/postcss": "workspace:^",
"plugin-cjs": "workspace:*",
"plugin-cts": "workspace:*",
"plugin-esm": "workspace:*",
"plugin-mts": "workspace:*",
"postcss": "^8",
"postcss-cli": "^10",
"tailwindcss": "workspace:^"
}
}
`,
'project-esm/postcss.config.mjs': js`
import tailwindcss from '@tailwindcss/postcss'
export default {
plugins: [tailwindcss()],
}
`,
'project-esm/index.html': html`
<div class="cjs esm cts mts"></div>
`,
'project-esm/src/index.css': css`
@import 'tailwindcss/utilities';
@plugin 'plugin-cjs';
@plugin 'plugin-esm';
@plugin 'plugin-cts';
@plugin 'plugin-mts';
`,
'plugin-cjs/package.json': json`
{
"name": "plugin-cjs",
"type": "commonjs",
"exports": {
".": {
"require": "./index.cjs"
}
}
}
`,
'plugin-cjs/index.cjs': js`
module.exports = function ({ addUtilities }) {
addUtilities({ '.cjs': { content: '"cjs"' } })
}
`,
'plugin-esm/package.json': json`
{
"name": "plugin-esm",
"type": "module",
"exports": {
".": {
"import": "./index.mjs"
}
}
}
`,
'plugin-esm/index.mjs': js`
export default function ({ addUtilities }) {
addUtilities({ '.esm': { content: '"esm"' } })
}
`,
'plugin-cts/package.json': json`
{
"name": "plugin-cts",
"type": "commonjs",
"exports": {
".": {
"require": "./index.cts"
}
}
}
`,
'plugin-cts/index.cts': ts`
export default function ({ addUtilities }) {
addUtilities({ '.cts': { content: '"cts"' as const } })
}
`,
'plugin-mts/package.json': json`
{
"name": "plugin-mts",
"type": "module",
"exports": {
".": {
"import": "./index.mts"
}
}
}
`,
'plugin-mts/index.mts': ts`
export default function ({ addUtilities }) {
addUtilities({ '.mts': { content: '"mts"' as const } })
}
`,
},
},
async ({ root, fs, exec }) => {
await exec(`pnpm postcss src/index.css --output dist/out.css`, {
cwd: path.join(root, 'project-cjs'),
})
await exec(`pnpm postcss src/index.css --output dist/out.css`, {
cwd: path.join(root, 'project-esm'),
})
await fs.expectFileToContain('./project-cjs/dist/out.css', [
candidate`cjs`,
candidate`esm`,
candidate`cts`,
candidate`mts`,
])
await fs.expectFileToContain('./project-esm/dist/out.css', [
candidate`cjs`,
candidate`esm`,
candidate`cts`,
candidate`mts`,
])
},
)
test(
'watch mode',
{

View File

@ -174,11 +174,18 @@ async function resolveCssId(id: string, base: string): Promise<string | false |
return runResolver(cssResolver, id, base)
}
const jsResolver = EnhancedResolve.ResolverFactory.createResolver({
const esmResolver = EnhancedResolve.ResolverFactory.createResolver({
fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000),
useSyncFileSystemCalls: true,
extensions: ['.js', '.json', '.node', '.ts'],
conditionNames: import.meta.url ? ['node', 'import'] : ['node', 'require'],
conditionNames: ['node', 'import'],
})
const cjsResolver = EnhancedResolve.ResolverFactory.createResolver({
fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000),
useSyncFileSystemCalls: true,
extensions: ['.js', '.json', '.node', '.ts'],
conditionNames: ['node', 'require'],
})
function resolveJsId(id: string, base: string): Promise<string | false | undefined> {
@ -188,7 +195,7 @@ function resolveJsId(id: string, base: string): Promise<string | false | undefin
return Promise.resolve(resolved)
}
}
return runResolver(jsResolver, id, base)
return runResolver(esmResolver, id, base).catch(() => runResolver(cjsResolver, id, base))
}
function runResolver(

View File

@ -1,7 +1,5 @@
import fs from 'node:fs/promises'
import { createRequire } from 'node:module'
const localResolve = createRequire(import.meta.url).resolve
import { resolveJsId } from './resolve'
/**
* Resolves the version string of an npm dependency installed in the based
@ -9,7 +7,8 @@ const localResolve = createRequire(import.meta.url).resolve
*/
export async function getPackageVersion(pkg: string, base: string): Promise<string | null> {
try {
let packageJson = localResolve(`${pkg}/package.json`, { paths: [base] })
let packageJson = resolveJsId(`${pkg}/package.json`, base)
if (!packageJson) return null
let { version } = JSON.parse(await fs.readFile(packageJson, 'utf8'))
return version
} catch {

View File

@ -13,6 +13,28 @@ export function resolve(id: string) {
return localResolve(id)
}
const esmResolver = EnhancedResolve.ResolverFactory.createResolver({
fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000),
useSyncFileSystemCalls: true,
extensions: ['.js', '.json', '.node', '.ts'],
conditionNames: ['node', 'import'],
})
const cjsResolver = EnhancedResolve.ResolverFactory.createResolver({
fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000),
useSyncFileSystemCalls: true,
extensions: ['.js', '.json', '.node', '.ts'],
conditionNames: ['node', 'require'],
})
export function resolveJsId(id: string, base: string) {
try {
return esmResolver.resolveSync({}, base, id)
} catch {
return cjsResolver.resolveSync({}, base, id)
}
}
const resolver = EnhancedResolve.ResolverFactory.createResolver({
fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000),
useSyncFileSystemCalls: true,
@ -21,12 +43,5 @@ const resolver = EnhancedResolve.ResolverFactory.createResolver({
conditionNames: ['style'],
})
export function resolveCssId(id: string, base: string) {
if (typeof globalThis.__tw_resolve === 'function') {
let resolved = globalThis.__tw_resolve(id, base)
if (resolved) {
return resolved
}
}
return resolver.resolveSync({}, base, id)
}