From fe9fc9abba33bae24f1cbe7ae2bdf66bafd989fa Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 19 Nov 2024 18:39:49 +0100 Subject: [PATCH] Use `resolveJsId` when resolving `tailwindcss/package.json` (#15041) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- CHANGELOG.md | 2 + integrations/cli/index.test.ts | 157 +++++++++++++++- integrations/postcss/index.test.ts | 173 +++++++++++++++++- packages/@tailwindcss-node/src/compile.ts | 13 +- .../src/utils/package-version.ts | 7 +- .../@tailwindcss-upgrade/src/utils/resolve.ts | 29 ++- 6 files changed, 365 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 976f56df0..29b94cc89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/integrations/cli/index.test.ts b/integrations/cli/index.test.ts index db4a895bb..c6eac6fa7 100644 --- a/integrations/cli/index.test.ts +++ b/integrations/cli/index.test.ts @@ -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` +
+ `, + '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` +
+ `, + '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( diff --git a/integrations/postcss/index.test.ts b/integrations/postcss/index.test.ts index 93b757d46..883bc5615 100644 --- a/integrations/postcss/index.test.ts +++ b/integrations/postcss/index.test.ts @@ -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` +
+ `, + '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` +
+ `, + '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', { diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index 60ddecff2..396b7116e 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -174,11 +174,18 @@ async function resolveCssId(id: string, base: string): Promise { @@ -188,7 +195,7 @@ function resolveJsId(id: string, base: string): Promise runResolver(cjsResolver, id, base)) } function runResolver( diff --git a/packages/@tailwindcss-upgrade/src/utils/package-version.ts b/packages/@tailwindcss-upgrade/src/utils/package-version.ts index b41ad6fb0..833ccadf2 100644 --- a/packages/@tailwindcss-upgrade/src/utils/package-version.ts +++ b/packages/@tailwindcss-upgrade/src/utils/package-version.ts @@ -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 { 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 { diff --git a/packages/@tailwindcss-upgrade/src/utils/resolve.ts b/packages/@tailwindcss-upgrade/src/utils/resolve.ts index e9197e874..58f5834ba 100644 --- a/packages/@tailwindcss-upgrade/src/utils/resolve.ts +++ b/packages/@tailwindcss-upgrade/src/utils/resolve.ts @@ -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) }