mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
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:
parent
d55852a17c
commit
921b4b673b
@ -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',
|
||||
{
|
||||
|
||||
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -4,7 +4,6 @@ export default defineConfig({
|
||||
format: ['esm', 'cjs'],
|
||||
clean: true,
|
||||
minify: true,
|
||||
splitting: true,
|
||||
cjsInterop: true,
|
||||
dts: true,
|
||||
entry: ['src/index.ts'],
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
@ -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
|
||||
|
||||
|
||||
@ -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
@ -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)),
|
||||
),
|
||||
})
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user