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 <jordan@cryptica.me>
This commit is contained in:
Philipp Spiess 2024-08-08 17:49:06 +02:00 committed by GitHub
parent d55852a17c
commit 921b4b673b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1520 additions and 1223 deletions

View File

@ -2,7 +2,7 @@ import path from 'node:path'
import { candidate, css, html, js, json, test, yaml } from '../utils'
test(
'production build',
'production build (string)',
{
fs: {
'package.json': json`{}`,
@ -69,6 +69,140 @@ test(
},
)
test(
'production build (ESM)',
{
fs: {
'package.json': json`{}`,
'pnpm-workspace.yaml': yaml`
#
packages:
- project-a
`,
'project-a/package.json': json`
{
"dependencies": {
"postcss": "^8",
"postcss-cli": "^10",
"tailwindcss": "workspace:^",
"@tailwindcss/postcss": "workspace:^"
}
}
`,
'project-a/postcss.config.mjs': js`
import tailwindcss from '@tailwindcss/postcss'
export default {
plugins: [tailwindcss()],
}
`,
'project-a/index.html': html`
<div
class="underline 2xl:font-bold hocus:underline inverted:flex"
></div>
`,
'project-a/plugin.js': js`
module.exports = function ({ addVariant }) {
addVariant('inverted', '@media (inverted-colors: inverted)')
addVariant('hocus', ['&:focus', '&:hover'])
}
`,
'project-a/src/index.css': css`
@import 'tailwindcss/utilities';
@source '../../project-b/src/**/*.js';
@plugin '../plugin.js';
`,
'project-a/src/index.js': js`
const className = "content-['a/src/index.js']"
module.exports = { className }
`,
'project-b/src/index.js': js`
const className = "content-['b/src/index.js']"
module.exports = { className }
`,
},
},
async ({ root, fs, exec }) => {
await exec('pnpm postcss src/index.css --output dist/out.css', {
cwd: path.join(root, 'project-a'),
})
await fs.expectFileToContain('project-a/dist/out.css', [
candidate`underline`,
candidate`content-['a/src/index.js']`,
candidate`content-['b/src/index.js']`,
candidate`inverted:flex`,
candidate`hocus:underline`,
])
},
)
test(
'production build (CJS)',
{
fs: {
'package.json': json`{}`,
'pnpm-workspace.yaml': yaml`
#
packages:
- project-a
`,
'project-a/package.json': json`
{
"dependencies": {
"postcss": "^8",
"postcss-cli": "^10",
"tailwindcss": "workspace:^",
"@tailwindcss/postcss": "workspace:^"
}
}
`,
'project-a/postcss.config.cjs': js`
let tailwindcss = require('@tailwindcss/postcss')
module.exports = {
plugins: [tailwindcss()],
}
`,
'project-a/index.html': html`
<div
class="underline 2xl:font-bold hocus:underline inverted:flex"
></div>
`,
'project-a/plugin.js': js`
module.exports = function ({ addVariant }) {
addVariant('inverted', '@media (inverted-colors: inverted)')
addVariant('hocus', ['&:focus', '&:hover'])
}
`,
'project-a/src/index.css': css`
@import 'tailwindcss/utilities';
@source '../../project-b/src/**/*.js';
@plugin '../plugin.js';
`,
'project-a/src/index.js': js`
const className = "content-['a/src/index.js']"
module.exports = { className }
`,
'project-b/src/index.js': js`
const className = "content-['b/src/index.js']"
module.exports = { className }
`,
},
},
async ({ root, fs, exec }) => {
await exec('pnpm postcss src/index.css --output dist/out.css', {
cwd: path.join(root, 'project-a'),
})
await fs.expectFileToContain('project-a/dist/out.css', [
candidate`underline`,
candidate`content-['a/src/index.js']`,
candidate`content-['b/src/index.js']`,
candidate`inverted:flex`,
candidate`hocus:underline`,
])
},
)
test(
'watch mode',
{

View File

@ -126,7 +126,9 @@ export function test(
rejectDisposal?.(new Error(`spawned process (${command}) did not exit in time`)),
ASSERTION_TIMEOUT,
)
disposePromise.finally(() => clearTimeout(timer))
disposePromise.finally(() => {
clearTimeout(timer)
})
return disposePromise
}
disposables.push(dispose)
@ -294,19 +296,16 @@ export function test(
async function dispose() {
await Promise.all(disposables.map((dispose) => dispose()))
try {
if (debug) return
await fs.rm(root, { recursive: true, maxRetries: 5, force: true })
} catch (err) {
if (!process.env.CI) {
throw err
}
// Skip removing the directory in CI beause it can stall on Windows
if (!process.env.CI && !debug) {
await fs.rm(root, { recursive: true, force: true })
}
}
options.onTestFinished(dispose)
await testCallback(context)
return await testCallback(context)
},
)
}

View File

@ -37,7 +37,7 @@
"build": "turbo build --filter=!./playgrounds/* && node ./scripts/pack-packages.mjs",
"dev": "turbo dev --filter=!./playgrounds/*",
"test": "cargo test && vitest run",
"test:integrations": "vitest --root=./integrations",
"test:integrations": "vitest --root=./integrations --no-file-parallelism",
"test:ui": "pnpm run --filter=tailwindcss test:ui",
"tdd": "vitest",
"bench": "vitest bench",

View File

@ -2,10 +2,10 @@ import watcher from '@parcel/watcher'
import { clearCache, scanDir, type ChangedContent } from '@tailwindcss/oxide'
import fixRelativePathsPlugin from 'internal-postcss-fix-relative-paths'
import { Features, transform } from 'lightningcss'
import { createRequire } from 'module'
import { existsSync } from 'node:fs'
import fs from 'node:fs/promises'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
import postcss from 'postcss'
import atImport from 'postcss-import'
import * as tailwindcss from 'tailwindcss'
@ -21,7 +21,6 @@ import {
} from '../../utils/renderer'
import { resolve } from '../../utils/resolve'
import { drainStdin, outputFile } from './utils'
const require = createRequire(import.meta.url)
const css = String.raw
@ -132,18 +131,20 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
function compile(css: string) {
return tailwindcss.compile(css, {
loadPlugin: (pluginPath) => {
loadPlugin: async (pluginPath) => {
if (pluginPath[0] === '.') {
return require(path.resolve(inputBasePath, pluginPath))
return import(pathToFileURL(path.resolve(inputBasePath, pluginPath)).href).then(
(m) => m.default ?? m,
)
}
return require(pluginPath)
return import(pluginPath).then((m) => m.default ?? m)
},
})
}
// Compile the input
let compiler = compile(input)
let compiler = await compile(input)
let scanDirResult = scanDir({
base, // Root directory, mainly used for auto content detection
sources: compiler.globs.map((pattern) => ({
@ -208,7 +209,7 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
)
// Create a new compiler, given the new `input`
compiler = compile(input)
compiler = await compile(input)
// Re-scan the directory to get the new `candidates`
scanDirResult = scanDir({

View File

@ -1,6 +1,7 @@
import { unlink, writeFile } from 'node:fs/promises'
import postcss from 'postcss'
import { afterEach, beforeEach, describe, expect, test } from 'vitest'
// @ts-ignore
import tailwindcss from './index'
// We give this file path to PostCSS for processing.

View File

@ -2,6 +2,7 @@ import { scanDir } from '@tailwindcss/oxide'
import fs from 'fs'
import fixRelativePathsPlugin from 'internal-postcss-fix-relative-paths'
import { Features, transform } from 'lightningcss'
import { pathToFileURL } from 'node:url'
import path from 'path'
import postcss, { AtRule, type AcceptedPlugin, type PluginCreator } from 'postcss'
import postcssImport from 'postcss-import'
@ -43,7 +44,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
let cache = new DefaultMap(() => {
return {
mtimes: new Map<string, number>(),
compiler: null as null | ReturnType<typeof compile>,
compiler: null as null | Awaited<ReturnType<typeof compile>>,
css: '',
optimizedCss: '',
}
@ -73,26 +74,30 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
hasTailwind = true
}
},
OnceExit(root, { result }) {
async OnceExit(root, { result }) {
let inputFile = result.opts.from ?? ''
console.log({ inputFile })
let context = cache.get(inputFile)
let inputBasePath = path.dirname(path.resolve(inputFile))
console.log({ inputBasePath })
function createCompiler() {
return compile(root.toString(), {
loadPlugin: (pluginPath) => {
loadPlugin: async (pluginPath) => {
if (pluginPath[0] === '.') {
return require(path.resolve(inputBasePath, pluginPath))
return import(pathToFileURL(path.resolve(inputBasePath, pluginPath)).href).then(
(m) => m.default ?? m,
)
}
return require(pluginPath)
return import(pluginPath).then((m) => m.default ?? m)
},
})
}
// Setup the compiler if it doesn't exist yet. This way we can
// guarantee a `build()` function is available.
context.compiler ??= createCompiler()
context.compiler ??= await createCompiler()
let rebuildStrategy: 'full' | 'incremental' = 'incremental'
@ -158,7 +163,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
}
if (rebuildStrategy === 'full') {
context.compiler = createCompiler()
context.compiler = await createCompiler()
css = context.compiler.build(hasTailwind ? scanDirResult.candidates : [])
} else if (rebuildStrategy === 'incremental') {
css = context.compiler.build!(scanDirResult.candidates)
@ -203,4 +208,5 @@ function optimizeCss(
}).code.toString()
}
export default Object.assign(tailwindcss, { postcss: true }) as PluginCreator<PluginOptions>
// This is used instead of `export default` to work around a bug in `postcss-load-config`
module.exports = Object.assign(tailwindcss, { postcss: true }) as PluginCreator<PluginOptions>

View File

@ -4,7 +4,6 @@ export default defineConfig({
format: ['esm', 'cjs'],
clean: true,
minify: true,
splitting: true,
cjsInterop: true,
dts: true,
entry: ['src/index.ts'],

View File

@ -1,11 +1,14 @@
import { scanDir } from '@tailwindcss/oxide'
import fixRelativePathsPlugin, { normalizePath } from 'internal-postcss-fix-relative-paths'
import { Features, transform } from 'lightningcss'
import { fileURLToPath } from 'node:url'
import path from 'path'
import postcssrc from 'postcss-load-config'
import { compile } from 'tailwindcss'
import type { Plugin, ResolvedConfig, Rollup, Update, ViteDevServer } from 'vite'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
export default function tailwindcss(): Plugin[] {
let server: ViteDevServer | null = null
let config: ResolvedConfig | null = null
@ -81,15 +84,15 @@ export default function tailwindcss(): Plugin[] {
return updated
}
function generateCss(css: string, inputPath: string, addWatchFile: (file: string) => void) {
async function generateCss(css: string, inputPath: string, addWatchFile: (file: string) => void) {
let inputBasePath = path.dirname(path.resolve(inputPath))
let { build, globs } = compile(css, {
loadPlugin: (pluginPath) => {
let { build, globs } = await compile(css, {
loadPlugin: async (pluginPath) => {
if (pluginPath[0] === '.') {
return require(path.resolve(inputBasePath, pluginPath))
return import(path.resolve(inputBasePath, pluginPath)).then((m) => m.default ?? m)
}
return require(pluginPath)
return import(pluginPath).then((m) => m.default ?? m)
},
})
@ -131,12 +134,12 @@ export default function tailwindcss(): Plugin[] {
return build(Array.from(candidates))
}
function generateOptimizedCss(
async function generateOptimizedCss(
css: string,
inputPath: string,
addWatchFile: (file: string) => void,
) {
return optimizeCss(generateCss(css, inputPath, addWatchFile), { minify })
return optimizeCss(await generateCss(css, inputPath, addWatchFile), { minify })
}
// Manually run the transform functions of non-Tailwind plugins on the given CSS
@ -301,7 +304,7 @@ export default function tailwindcss(): Plugin[] {
let code = await transformWithPlugins(
this,
id,
generateCss(src, id, (file) => this.addWatchFile(file)),
await generateCss(src, id, (file) => this.addWatchFile(file)),
)
return { code }
},
@ -326,7 +329,7 @@ export default function tailwindcss(): Plugin[] {
continue
}
let css = generateOptimizedCss(file.content, id, (file) => this.addWatchFile(file))
let css = await generateOptimizedCss(file.content, id, (file) => this.addWatchFile(file))
// These plugins have side effects which, during build, results in CSS
// being written to the output dir. We need to run them here to ensure

View File

@ -9,7 +9,8 @@ const css = String.raw
bench('compile', async () => {
let { candidates } = scanDir({ base: root })
compile(css`
let { build } = await compile(css`
@tailwind utilities;
`).build(candidates)
`)
build(candidates)
})

File diff suppressed because it is too large Load Diff

View File

@ -26,7 +26,7 @@ type PluginAPI = {
type Plugin = (api: PluginAPI) => void
type CompileOptions = {
loadPlugin?: (path: string) => Plugin
loadPlugin?: (path: string) => Promise<Plugin>
}
function throwOnPlugin(): never {
@ -48,13 +48,13 @@ function parseThemeOptions(selector: string) {
return { isReference, isInline }
}
export function compile(
export async function compile(
css: string,
{ loadPlugin = throwOnPlugin }: CompileOptions = {},
): {
): Promise<{
globs: string[]
build(candidates: string[]): string
} {
}> {
let ast = CSS.parse(css)
if (process.env.NODE_ENV !== 'test') {
@ -69,7 +69,7 @@ export function compile(
// Find all `@theme` declarations
let theme = new Theme()
let plugins: Plugin[] = []
let pluginLoaders: Promise<Plugin>[] = []
let customVariants: ((designSystem: DesignSystem) => void)[] = []
let customUtilities: ((designSystem: DesignSystem) => void)[] = []
let firstThemeRule: Rule | null = null
@ -89,7 +89,7 @@ export function compile(
throw new Error('`@plugin` cannot be nested.')
}
plugins.push(loadPlugin(node.selector.slice(9, -1)))
pluginLoaders.push(loadPlugin(node.selector.slice(9, -1)))
replaceWith([])
return
}
@ -334,9 +334,7 @@ export function compile(
},
}
for (let plugin of plugins) {
plugin(api)
}
await Promise.all(pluginLoaders.map((loader) => loader.then((plugin) => plugin(api))))
let tailwindUtilitiesNode: Rule | null = null

View File

@ -1,12 +1,14 @@
import { Features, transform } from 'lightningcss'
import { compile } from '..'
export function compileCss(css: string, candidates: string[] = []) {
return optimizeCss(compile(css).build(candidates)).trim()
export async function compileCss(css: string, candidates: string[] = []) {
let { build } = await compile(css)
return optimizeCss(build(candidates)).trim()
}
export function run(candidates: string[]) {
return optimizeCss(compile('@tailwind utilities;').build(candidates)).trim()
export async function run(candidates: string[]) {
let { build } = await compile('@tailwind utilities;')
return optimizeCss(build(candidates)).trim()
}
export function optimizeCss(

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -292,21 +292,23 @@ test('content-none persists when conditionally styling a pseudo-element', async
const preflight = fs.readFileSync(path.resolve(__dirname, '..', 'preflight.css'), 'utf-8')
const defaultTheme = fs.readFileSync(path.resolve(__dirname, '..', 'theme.css'), 'utf-8')
async function render(page: Page, content: string) {
let { build } = await compile(css`
@layer theme, base, components, utilities;
@layer theme {
${defaultTheme}
}
@layer base {
${preflight}
}
@layer utilities {
@tailwind utilities;
}
`)
await page.setContent(content)
await page.addStyleTag({
content: optimizeCss(
compile(css`
@layer theme, base, components, utilities;
@layer theme {
${defaultTheme}
}
@layer base {
${preflight}
}
@layer utilities {
@tailwind utilities;
}
`).build(scanFiles([{ content, extension: 'html' }], IO.Sequential | Parsing.Sequential)),
build(scanFiles([{ content, extension: 'html' }], IO.Sequential | Parsing.Sequential)),
),
})