Skip to content

Commit 921b4b6

Browse files
Use import to load plugins (#14132)
Alternative to #14110 This PR changes the way how we load plugins to be compatible with ES6 async `import`s. This allows us to load plugins even inside the browser but it comes at a downside: We now have to change the `compile` API to return a `Promise`... So most of this PR is rewriting all of the call sites of `compile` to expect a promise instead of the object. --------- Co-authored-by: Jordan Pittman <[email protected]>
1 parent d55852a commit 921b4b6

File tree

15 files changed

+1520
-1223
lines changed

15 files changed

+1520
-1223
lines changed

integrations/postcss/index.test.ts

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import path from 'node:path'
22
import { candidate, css, html, js, json, test, yaml } from '../utils'
33

44
test(
5-
'production build',
5+
'production build (string)',
66
{
77
fs: {
88
'package.json': json`{}`,
@@ -69,6 +69,140 @@ test(
6969
},
7070
)
7171

72+
test(
73+
'production build (ESM)',
74+
{
75+
fs: {
76+
'package.json': json`{}`,
77+
'pnpm-workspace.yaml': yaml`
78+
#
79+
packages:
80+
- project-a
81+
`,
82+
'project-a/package.json': json`
83+
{
84+
"dependencies": {
85+
"postcss": "^8",
86+
"postcss-cli": "^10",
87+
"tailwindcss": "workspace:^",
88+
"@tailwindcss/postcss": "workspace:^"
89+
}
90+
}
91+
`,
92+
'project-a/postcss.config.mjs': js`
93+
import tailwindcss from '@tailwindcss/postcss'
94+
export default {
95+
plugins: [tailwindcss()],
96+
}
97+
`,
98+
'project-a/index.html': html`
99+
<div
100+
class="underline 2xl:font-bold hocus:underline inverted:flex"
101+
></div>
102+
`,
103+
'project-a/plugin.js': js`
104+
module.exports = function ({ addVariant }) {
105+
addVariant('inverted', '@media (inverted-colors: inverted)')
106+
addVariant('hocus', ['&:focus', '&:hover'])
107+
}
108+
`,
109+
'project-a/src/index.css': css`
110+
@import 'tailwindcss/utilities';
111+
@source '../../project-b/src/**/*.js';
112+
@plugin '../plugin.js';
113+
`,
114+
'project-a/src/index.js': js`
115+
const className = "content-['a/src/index.js']"
116+
module.exports = { className }
117+
`,
118+
'project-b/src/index.js': js`
119+
const className = "content-['b/src/index.js']"
120+
module.exports = { className }
121+
`,
122+
},
123+
},
124+
async ({ root, fs, exec }) => {
125+
await exec('pnpm postcss src/index.css --output dist/out.css', {
126+
cwd: path.join(root, 'project-a'),
127+
})
128+
129+
await fs.expectFileToContain('project-a/dist/out.css', [
130+
candidate`underline`,
131+
candidate`content-['a/src/index.js']`,
132+
candidate`content-['b/src/index.js']`,
133+
candidate`inverted:flex`,
134+
candidate`hocus:underline`,
135+
])
136+
},
137+
)
138+
139+
test(
140+
'production build (CJS)',
141+
{
142+
fs: {
143+
'package.json': json`{}`,
144+
'pnpm-workspace.yaml': yaml`
145+
#
146+
packages:
147+
- project-a
148+
`,
149+
'project-a/package.json': json`
150+
{
151+
"dependencies": {
152+
"postcss": "^8",
153+
"postcss-cli": "^10",
154+
"tailwindcss": "workspace:^",
155+
"@tailwindcss/postcss": "workspace:^"
156+
}
157+
}
158+
`,
159+
'project-a/postcss.config.cjs': js`
160+
let tailwindcss = require('@tailwindcss/postcss')
161+
module.exports = {
162+
plugins: [tailwindcss()],
163+
}
164+
`,
165+
'project-a/index.html': html`
166+
<div
167+
class="underline 2xl:font-bold hocus:underline inverted:flex"
168+
></div>
169+
`,
170+
'project-a/plugin.js': js`
171+
module.exports = function ({ addVariant }) {
172+
addVariant('inverted', '@media (inverted-colors: inverted)')
173+
addVariant('hocus', ['&:focus', '&:hover'])
174+
}
175+
`,
176+
'project-a/src/index.css': css`
177+
@import 'tailwindcss/utilities';
178+
@source '../../project-b/src/**/*.js';
179+
@plugin '../plugin.js';
180+
`,
181+
'project-a/src/index.js': js`
182+
const className = "content-['a/src/index.js']"
183+
module.exports = { className }
184+
`,
185+
'project-b/src/index.js': js`
186+
const className = "content-['b/src/index.js']"
187+
module.exports = { className }
188+
`,
189+
},
190+
},
191+
async ({ root, fs, exec }) => {
192+
await exec('pnpm postcss src/index.css --output dist/out.css', {
193+
cwd: path.join(root, 'project-a'),
194+
})
195+
196+
await fs.expectFileToContain('project-a/dist/out.css', [
197+
candidate`underline`,
198+
candidate`content-['a/src/index.js']`,
199+
candidate`content-['b/src/index.js']`,
200+
candidate`inverted:flex`,
201+
candidate`hocus:underline`,
202+
])
203+
},
204+
)
205+
72206
test(
73207
'watch mode',
74208
{

integrations/utils.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,9 @@ export function test(
126126
rejectDisposal?.(new Error(`spawned process (${command}) did not exit in time`)),
127127
ASSERTION_TIMEOUT,
128128
)
129-
disposePromise.finally(() => clearTimeout(timer))
129+
disposePromise.finally(() => {
130+
clearTimeout(timer)
131+
})
130132
return disposePromise
131133
}
132134
disposables.push(dispose)
@@ -294,19 +296,16 @@ export function test(
294296

295297
async function dispose() {
296298
await Promise.all(disposables.map((dispose) => dispose()))
297-
try {
298-
if (debug) return
299-
await fs.rm(root, { recursive: true, maxRetries: 5, force: true })
300-
} catch (err) {
301-
if (!process.env.CI) {
302-
throw err
303-
}
299+
300+
// Skip removing the directory in CI beause it can stall on Windows
301+
if (!process.env.CI && !debug) {
302+
await fs.rm(root, { recursive: true, force: true })
304303
}
305304
}
306305

307306
options.onTestFinished(dispose)
308307

309-
await testCallback(context)
308+
return await testCallback(context)
310309
},
311310
)
312311
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"build": "turbo build --filter=!./playgrounds/* && node ./scripts/pack-packages.mjs",
3838
"dev": "turbo dev --filter=!./playgrounds/*",
3939
"test": "cargo test && vitest run",
40-
"test:integrations": "vitest --root=./integrations",
40+
"test:integrations": "vitest --root=./integrations --no-file-parallelism",
4141
"test:ui": "pnpm run --filter=tailwindcss test:ui",
4242
"tdd": "vitest",
4343
"bench": "vitest bench",

packages/@tailwindcss-cli/src/commands/build/index.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import watcher from '@parcel/watcher'
22
import { clearCache, scanDir, type ChangedContent } from '@tailwindcss/oxide'
33
import fixRelativePathsPlugin from 'internal-postcss-fix-relative-paths'
44
import { Features, transform } from 'lightningcss'
5-
import { createRequire } from 'module'
65
import { existsSync } from 'node:fs'
76
import fs from 'node:fs/promises'
87
import path from 'node:path'
8+
import { pathToFileURL } from 'node:url'
99
import postcss from 'postcss'
1010
import atImport from 'postcss-import'
1111
import * as tailwindcss from 'tailwindcss'
@@ -21,7 +21,6 @@ import {
2121
} from '../../utils/renderer'
2222
import { resolve } from '../../utils/resolve'
2323
import { drainStdin, outputFile } from './utils'
24-
const require = createRequire(import.meta.url)
2524

2625
const css = String.raw
2726

@@ -132,18 +131,20 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
132131

133132
function compile(css: string) {
134133
return tailwindcss.compile(css, {
135-
loadPlugin: (pluginPath) => {
134+
loadPlugin: async (pluginPath) => {
136135
if (pluginPath[0] === '.') {
137-
return require(path.resolve(inputBasePath, pluginPath))
136+
return import(pathToFileURL(path.resolve(inputBasePath, pluginPath)).href).then(
137+
(m) => m.default ?? m,
138+
)
138139
}
139140

140-
return require(pluginPath)
141+
return import(pluginPath).then((m) => m.default ?? m)
141142
},
142143
})
143144
}
144145

145146
// Compile the input
146-
let compiler = compile(input)
147+
let compiler = await compile(input)
147148
let scanDirResult = scanDir({
148149
base, // Root directory, mainly used for auto content detection
149150
sources: compiler.globs.map((pattern) => ({
@@ -208,7 +209,7 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
208209
)
209210

210211
// Create a new compiler, given the new `input`
211-
compiler = compile(input)
212+
compiler = await compile(input)
212213

213214
// Re-scan the directory to get the new `candidates`
214215
scanDirResult = scanDir({

packages/@tailwindcss-postcss/src/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { unlink, writeFile } from 'node:fs/promises'
22
import postcss from 'postcss'
33
import { afterEach, beforeEach, describe, expect, test } from 'vitest'
4+
// @ts-ignore
45
import tailwindcss from './index'
56

67
// We give this file path to PostCSS for processing.

packages/@tailwindcss-postcss/src/index.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { scanDir } from '@tailwindcss/oxide'
22
import fs from 'fs'
33
import fixRelativePathsPlugin from 'internal-postcss-fix-relative-paths'
44
import { Features, transform } from 'lightningcss'
5+
import { pathToFileURL } from 'node:url'
56
import path from 'path'
67
import postcss, { AtRule, type AcceptedPlugin, type PluginCreator } from 'postcss'
78
import postcssImport from 'postcss-import'
@@ -43,7 +44,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
4344
let cache = new DefaultMap(() => {
4445
return {
4546
mtimes: new Map<string, number>(),
46-
compiler: null as null | ReturnType<typeof compile>,
47+
compiler: null as null | Awaited<ReturnType<typeof compile>>,
4748
css: '',
4849
optimizedCss: '',
4950
}
@@ -73,26 +74,30 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
7374
hasTailwind = true
7475
}
7576
},
76-
OnceExit(root, { result }) {
77+
async OnceExit(root, { result }) {
7778
let inputFile = result.opts.from ?? ''
79+
console.log({ inputFile })
7880
let context = cache.get(inputFile)
7981
let inputBasePath = path.dirname(path.resolve(inputFile))
82+
console.log({ inputBasePath })
8083

8184
function createCompiler() {
8285
return compile(root.toString(), {
83-
loadPlugin: (pluginPath) => {
86+
loadPlugin: async (pluginPath) => {
8487
if (pluginPath[0] === '.') {
85-
return require(path.resolve(inputBasePath, pluginPath))
88+
return import(pathToFileURL(path.resolve(inputBasePath, pluginPath)).href).then(
89+
(m) => m.default ?? m,
90+
)
8691
}
8792

88-
return require(pluginPath)
93+
return import(pluginPath).then((m) => m.default ?? m)
8994
},
9095
})
9196
}
9297

9398
// Setup the compiler if it doesn't exist yet. This way we can
9499
// guarantee a `build()` function is available.
95-
context.compiler ??= createCompiler()
100+
context.compiler ??= await createCompiler()
96101

97102
let rebuildStrategy: 'full' | 'incremental' = 'incremental'
98103

@@ -158,7 +163,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
158163
}
159164

160165
if (rebuildStrategy === 'full') {
161-
context.compiler = createCompiler()
166+
context.compiler = await createCompiler()
162167
css = context.compiler.build(hasTailwind ? scanDirResult.candidates : [])
163168
} else if (rebuildStrategy === 'incremental') {
164169
css = context.compiler.build!(scanDirResult.candidates)
@@ -203,4 +208,5 @@ function optimizeCss(
203208
}).code.toString()
204209
}
205210

206-
export default Object.assign(tailwindcss, { postcss: true }) as PluginCreator<PluginOptions>
211+
// This is used instead of `export default` to work around a bug in `postcss-load-config`
212+
module.exports = Object.assign(tailwindcss, { postcss: true }) as PluginCreator<PluginOptions>

packages/@tailwindcss-postcss/tsup.config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ export default defineConfig({
44
format: ['esm', 'cjs'],
55
clean: true,
66
minify: true,
7-
splitting: true,
87
cjsInterop: true,
98
dts: true,
109
entry: ['src/index.ts'],

0 commit comments

Comments
 (0)