diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bcda15312b0..b8c3e59ed84e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Disallow negative bare values in core utilities and variants ([#14453](https://github.com/tailwindlabs/tailwindcss/pull/14453)) - Preserve explicit shadow color when overriding shadow size ([#14458](https://github.com/tailwindlabs/tailwindcss/pull/14458)) - Preserve explicit transition duration and timing function when overriding transition property ([#14490](https://github.com/tailwindlabs/tailwindcss/pull/14490)) +- Change the implementation for `@import` resolution to speed up initial builds ([#14446](https://github.com/tailwindlabs/tailwindcss/pull/14446)) ## [4.0.0-alpha.24] - 2024-09-11 diff --git a/integrations/postcss/index.test.ts b/integrations/postcss/index.test.ts index d4e4f4d394ba..273f07f73cfd 100644 --- a/integrations/postcss/index.test.ts +++ b/integrations/postcss/index.test.ts @@ -79,6 +79,86 @@ test( }, ) +test( + 'production build with `postcss-import` (string)', + { + fs: { + 'package.json': json`{}`, + 'pnpm-workspace.yaml': yaml` + # + packages: + - project-a + `, + 'project-a/package.json': json` + { + "dependencies": { + "postcss": "^8", + "postcss-cli": "^10", + "postcss-import": "^16", + "tailwindcss": "workspace:^", + "@tailwindcss/postcss": "workspace:^" + } + } + `, + 'project-a/postcss.config.js': js` + module.exports = { + plugins: { + 'postcss-import': {}, + '@tailwindcss/postcss': {}, + }, + } + `, + 'project-a/index.html': html` +
+ `, + 'project-a/plugin.js': js` + module.exports = function ({ addVariant }) { + addVariant('inverted', '@media (inverted-colors: inverted)') + addVariant('hocus', ['&:focus', '&:hover']) + } + `, + 'project-a/tailwind.config.js': js` + module.exports = { + content: ['../project-b/src/**/*.js'], + } + `, + 'project-a/src/index.css': css` + @import 'tailwindcss/utilities'; + @config '../tailwind.config.js'; + @source '../../project-b/src/**/*.html'; + @plugin '../plugin.js'; + `, + 'project-a/src/index.js': js` + const className = "content-['a/src/index.js']" + module.exports = { className } + `, + 'project-b/src/index.html': html` +
+ `, + '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`flex`, + candidate`content-['a/src/index.js']`, + candidate`content-['b/src/index.js']`, + candidate`inverted:flex`, + candidate`hocus:underline`, + ]) + }, +) + test( 'production build (ESM)', { diff --git a/packages/@tailwindcss-cli/package.json b/packages/@tailwindcss-cli/package.json index e36b6e2ada6c..c8dbf8cce678 100644 --- a/packages/@tailwindcss-cli/package.json +++ b/packages/@tailwindcss-cli/package.json @@ -36,12 +36,6 @@ "lightningcss": "catalog:", "mri": "^1.2.0", "picocolors": "^1.0.1", - "postcss-import": "^16.1.0", - "postcss": "^8.4.41", "tailwindcss": "workspace:^" - }, - "devDependencies": { - "@types/postcss-import": "^14.0.3", - "internal-postcss-fix-relative-paths": "workspace:^" } } diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts index 3ca7f4ad0367..daf12294cf74 100644 --- a/packages/@tailwindcss-cli/src/commands/build/index.ts +++ b/packages/@tailwindcss-cli/src/commands/build/index.ts @@ -2,13 +2,10 @@ import watcher from '@parcel/watcher' import { compile } from '@tailwindcss/node' import { clearRequireCache } from '@tailwindcss/node/require-cache' import { Scanner, type ChangedContent } from '@tailwindcss/oxide' -import fixRelativePathsPlugin from 'internal-postcss-fix-relative-paths' import { Features, transform } from 'lightningcss' -import { existsSync, readFileSync } from 'node:fs' +import { existsSync } from 'node:fs' import fs from 'node:fs/promises' import path from 'node:path' -import postcss from 'postcss' -import atImport from 'postcss-import' import type { Arg, Result } from '../../utils/args' import { Disposables } from '../../utils/disposables' import { @@ -19,7 +16,6 @@ import { println, relative, } from '../../utils/renderer' -import { resolveCssId } from '../../utils/resolve' import { drainStdin, outputFile } from './utils' const css = String.raw @@ -83,17 +79,13 @@ export async function handle(args: Result>) { let start = process.hrtime.bigint() - // Resolve the input - let [input, cssImportPaths] = await handleImports( - args['--input'] - ? args['--input'] === '-' - ? await drainStdin() - : await fs.readFile(args['--input'], 'utf-8') - : css` - @import 'tailwindcss'; - `, - args['--input'] ?? base, - ) + let input = args['--input'] + ? args['--input'] === '-' + ? await drainStdin() + : await fs.readFile(args['--input'], 'utf-8') + : css` + @import 'tailwindcss'; + ` let previous = { css: '', @@ -128,7 +120,7 @@ export async function handle(args: Result>) { let inputFile = args['--input'] && args['--input'] !== '-' ? args['--input'] : process.cwd() let inputBasePath = path.dirname(path.resolve(inputFile)) - let fullRebuildPaths: string[] = cssImportPaths.slice() + let fullRebuildPaths: string[] = [] function createCompiler(css: string) { return compile(css, { @@ -143,12 +135,7 @@ export async function handle(args: Result>) { let compiler = await createCompiler(input) let scanner = new Scanner({ detectSources: { base }, - sources: compiler.globs.map(({ origin, pattern }) => ({ - // Ensure the glob is relative to the input CSS file or the config file - // where it is specified. - base: origin ? path.dirname(path.resolve(inputBasePath, origin)) : inputBasePath, - pattern, - })), + sources: compiler.globs, }) // Watch for changes @@ -196,17 +183,16 @@ export async function handle(args: Result>) { // Clear all watchers cleanupWatchers() - // Collect the new `input` and `cssImportPaths`. - ;[input, cssImportPaths] = await handleImports( - args['--input'] - ? await fs.readFile(args['--input'], 'utf-8') - : css` - @import 'tailwindcss'; - `, - args['--input'] ?? base, - ) + // Read the new `input`. + let input = args['--input'] + ? args['--input'] === '-' + ? await drainStdin() + : await fs.readFile(args['--input'], 'utf-8') + : css` + @import 'tailwindcss'; + ` clearRequireCache(resolvedFullRebuildPaths) - fullRebuildPaths = cssImportPaths.slice() + fullRebuildPaths = [] // Create a new compiler, given the new `input` compiler = await createCompiler(input) @@ -214,12 +200,7 @@ export async function handle(args: Result>) { // Re-scan the directory to get the new `candidates` scanner = new Scanner({ detectSources: { base }, - sources: compiler.globs.map(({ origin, pattern }) => ({ - // Ensure the glob is relative to the input CSS file or the - // config file where it is specified. - base: origin ? path.dirname(path.resolve(inputBasePath, origin)) : inputBasePath, - pattern, - })), + sources: compiler.globs, }) // Scan the directory for candidates @@ -367,51 +348,6 @@ async function createWatchers(dirs: string[], cb: (files: string[]) => void) { } } -function handleImports( - input: string, - file: string, -): [css: string, paths: string[]] | Promise<[css: string, paths: string[]]> { - // TODO: Should we implement this ourselves instead of relying on PostCSS? - // - // Relevant specification: - // - CSS Import Resolve: https://csstools.github.io/css-import-resolve/ - - if (!input.includes('@import')) { - return [input, [file]] - } - - return postcss() - .use( - atImport({ - resolve(id, basedir) { - let resolved = resolveCssId(id, basedir) - if (!resolved) { - throw new Error(`Could not resolve ${id} from ${basedir}`) - } - return resolved - }, - load(id) { - // We need to synchronously read the file here because when bundled - // with bun, some of the ids might resolve to files inside the bun - // embedded files root which can only be read by `node:fs` and not - // `node:fs/promises`. - return readFileSync(id, 'utf-8') - }, - }), - ) - .use(fixRelativePathsPlugin()) - .process(input, { from: file }) - .then((result) => [ - result.css, - - // Use `result.messages` to get the imported files. This also includes the - // current file itself. - [file].concat( - result.messages.filter((msg) => msg.type === 'dependency').map((msg) => msg.file), - ), - ]) -} - function optimizeCss( input: string, { file = 'input.css', minify = false }: { file?: string; minify?: boolean } = {}, diff --git a/packages/@tailwindcss-cli/tsup.config.ts b/packages/@tailwindcss-cli/tsup.config.ts index 23628127060d..7d82eee2c882 100644 --- a/packages/@tailwindcss-cli/tsup.config.ts +++ b/packages/@tailwindcss-cli/tsup.config.ts @@ -5,5 +5,4 @@ export default defineConfig({ clean: true, minify: true, entry: ['src/index.ts'], - noExternal: ['internal-postcss-fix-relative-paths'], }) diff --git a/packages/@tailwindcss-node/package.json b/packages/@tailwindcss-node/package.json index 652e65538982..97f63f77e40a 100644 --- a/packages/@tailwindcss-node/package.json +++ b/packages/@tailwindcss-node/package.json @@ -40,6 +40,7 @@ "tailwindcss": "workspace:^" }, "dependencies": { + "enhanced-resolve": "^5.17.1", "jiti": "^2.0.0-beta.3" } } diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index 78c68d9ae06b..02332d578262 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -1,5 +1,8 @@ +import EnhancedResolve from 'enhanced-resolve' import { createJiti, type Jiti } from 'jiti' -import path from 'node:path' +import fs from 'node:fs' +import fsPromises from 'node:fs/promises' +import path, { dirname, extname } from 'node:path' import { pathToFileURL } from 'node:url' import { compile as _compile } from 'tailwindcss' import { getModuleDependencies } from './get-module-dependencies' @@ -9,12 +12,25 @@ export async function compile( { base, onDependency }: { base: string; onDependency: (path: string) => void }, ) { return await _compile(css, { - loadPlugin: async (pluginPath) => { - if (pluginPath[0] !== '.') { - return importModule(pluginPath).then((m) => m.default ?? m) + base, + async loadModule(id, base) { + if (id[0] !== '.') { + let resolvedPath = await resolveJsId(id, base) + if (!resolvedPath) { + throw new Error(`Could not resolve '${id}' from '${base}'`) + } + + let module = await importModule(pathToFileURL(resolvedPath).href) + return { + base: dirname(resolvedPath), + module: module.default ?? module, + } } - let resolvedPath = path.resolve(base, pluginPath) + let resolvedPath = await resolveJsId(id, base) + if (!resolvedPath) { + throw new Error(`Could not resolve '${id}' from '${base}'`) + } let [module, moduleDependencies] = await Promise.all([ importModule(pathToFileURL(resolvedPath).href + '?id=' + Date.now()), getModuleDependencies(resolvedPath), @@ -24,25 +40,31 @@ export async function compile( for (let file of moduleDependencies) { onDependency(file) } - return module.default ?? module + return { + base: dirname(resolvedPath), + module: module.default ?? module, + } }, - loadConfig: async (configPath) => { - if (configPath[0] !== '.') { - return importModule(configPath).then((m) => m.default ?? m) - } + async loadStylesheet(id, basedir) { + let resolvedPath = await resolveCssId(id, basedir) + if (!resolvedPath) throw new Error(`Could not resolve '${id}' from '${basedir}'`) - let resolvedPath = path.resolve(base, configPath) - let [module, moduleDependencies] = await Promise.all([ - importModule(pathToFileURL(resolvedPath).href + '?id=' + Date.now()), - getModuleDependencies(resolvedPath), - ]) + if (typeof globalThis.__tw_readFile === 'function') { + let file = await globalThis.__tw_readFile(resolvedPath, 'utf-8') + if (file) { + return { + base: path.dirname(resolvedPath), + content: file, + } + } + } - onDependency(resolvedPath) - for (let file of moduleDependencies) { - onDependency(file) + let file = await fsPromises.readFile(resolvedPath, 'utf-8') + return { + base: path.dirname(resolvedPath), + content: file, } - return module.default ?? module }, }) } @@ -62,3 +84,58 @@ async function importModule(path: string): Promise { throw error } } + +const cssResolver = EnhancedResolve.ResolverFactory.createResolver({ + fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000), + useSyncFileSystemCalls: true, + extensions: ['.css'], + mainFields: ['style'], + conditionNames: ['style'], +}) +async function resolveCssId(id: string, base: string): Promise { + if (typeof globalThis.__tw_resolve === 'function') { + let resolved = globalThis.__tw_resolve(id, base) + if (resolved) { + return Promise.resolve(resolved) + } + } + + // CSS imports that do not have a dir prefix are considered relative. Since + // the resolver does not account for this, we need to do a first pass with an + // assumed relative import by prefixing `./${path}`. We don't have to do this + // when the path starts with a `.` or when the path has no extension (at which + // case it's likely an npm package and not a relative stylesheet). + let skipRelativeCheck = extname(id) === '' || id.startsWith('.') + + if (!skipRelativeCheck) { + try { + let dotResolved = await runResolver(cssResolver, `./${id}`, base) + if (!dotResolved) throw new Error() + return dotResolved + } catch {} + } + + return runResolver(cssResolver, id, base) +} + +const jsResolver = EnhancedResolve.ResolverFactory.createResolver({ + fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000), + useSyncFileSystemCalls: true, +}) + +function resolveJsId(id: string, base: string): Promise { + return runResolver(jsResolver, id, base) +} + +function runResolver( + resolver: EnhancedResolve.Resolver, + id: string, + base: string, +): Promise { + return new Promise((resolve, reject) => + resolver.resolve({}, base, id, {}, (err, result) => { + if (err) return reject(err) + resolve(result) + }), + ) +} diff --git a/packages/@tailwindcss-node/src/index.cts b/packages/@tailwindcss-node/src/index.cts index a143865efb84..ee0de7ff5503 100644 --- a/packages/@tailwindcss-node/src/index.cts +++ b/packages/@tailwindcss-node/src/index.cts @@ -1,6 +1,7 @@ import * as Module from 'node:module' import { pathToFileURL } from 'node:url' export * from './compile' +export * from './normalize-path' // In Bun, ESM modules will also populate `require.cache`, so the module hook is // not necessary. diff --git a/packages/@tailwindcss-node/src/index.ts b/packages/@tailwindcss-node/src/index.ts index 85b292ed022f..f42c4ff4e40d 100644 --- a/packages/@tailwindcss-node/src/index.ts +++ b/packages/@tailwindcss-node/src/index.ts @@ -1,6 +1,7 @@ import * as Module from 'node:module' import { pathToFileURL } from 'node:url' export * from './compile' +export * from './normalize-path' // In Bun, ESM modules will also populate `require.cache`, so the module hook is // not necessary. diff --git a/packages/internal-postcss-fix-relative-paths/src/normalize-path.ts b/packages/@tailwindcss-node/src/normalize-path.ts similarity index 100% rename from packages/internal-postcss-fix-relative-paths/src/normalize-path.ts rename to packages/@tailwindcss-node/src/normalize-path.ts diff --git a/packages/@tailwindcss-postcss/package.json b/packages/@tailwindcss-postcss/package.json index 94d561f597bc..11da052a9e60 100644 --- a/packages/@tailwindcss-postcss/package.json +++ b/packages/@tailwindcss-postcss/package.json @@ -33,14 +33,13 @@ "@tailwindcss/node": "workspace:^", "@tailwindcss/oxide": "workspace:^", "lightningcss": "catalog:", - "postcss-import": "^16.1.0", "tailwindcss": "workspace:^" }, "devDependencies": { "@types/node": "catalog:", - "@types/postcss-import": "^14.0.3", "postcss": "^8.4.41", - "internal-example-plugin": "workspace:*", - "internal-postcss-fix-relative-paths": "workspace:^" + "postcss-import": "^16.1.0", + "@types/postcss-import": "14.0.3", + "internal-example-plugin": "workspace:*" } } diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 4f9eccbb56ef..881445a8016e 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -2,11 +2,10 @@ import { compile } from '@tailwindcss/node' import { clearRequireCache } from '@tailwindcss/node/require-cache' import { Scanner } from '@tailwindcss/oxide' import fs from 'fs' -import fixRelativePathsPlugin from 'internal-postcss-fix-relative-paths' import { Features, transform } from 'lightningcss' import path from 'path' -import postcss, { AtRule, type AcceptedPlugin, type PluginCreator } from 'postcss' -import postcssImport from 'postcss-import' +import postcss, { type AcceptedPlugin, type PluginCreator } from 'postcss' +import fixRelativePathsPlugin from './postcss-fix-relative-paths' /** * A Map that can generate default values for keys that don't exist. @@ -51,30 +50,16 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } }) - let hasApply: boolean, hasTailwind: boolean - return { postcssPlugin: '@tailwindcss/postcss', plugins: [ - // We need to run `postcss-import` first to handle `@import` rules. - postcssImport(), + // We need to handle the case where `postcss-import` might have run before the Tailwind CSS + // plugin is run. In this case, we need to manually fix relative paths before processing it + // in core. fixRelativePathsPlugin(), { postcssPlugin: 'tailwindcss', - Once() { - // Reset some state between builds - hasApply = false - hasTailwind = false - }, - AtRule(rule: AtRule) { - if (rule.name === 'apply') { - hasApply = true - } else if (rule.name === 'tailwind') { - hasApply = true - hasTailwind = true - } - }, async OnceExit(root, { result }) { let inputFile = result.opts.from ?? '' let context = cache.get(inputFile) @@ -133,23 +118,14 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } } - // Do nothing if neither `@tailwind` nor `@apply` is used - if (!hasTailwind && !hasApply) return - let css = '' // Look for candidates used to generate the CSS let scanner = new Scanner({ detectSources: { base }, - sources: context.compiler.globs.map(({ origin, pattern }) => ({ - // Ensure the glob is relative to the input CSS file or the config - // file where it is specified. - base: origin ? path.dirname(path.resolve(inputBasePath, origin)) : inputBasePath, - pattern, - })), + sources: context.compiler.globs, }) - // let candidates = scanner.scan() // Add all found files as direct dependencies @@ -177,10 +153,8 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { if (rebuildStrategy === 'full') { context.compiler = await createCompiler() - css = context.compiler.build(hasTailwind ? candidates : []) - } else if (rebuildStrategy === 'incremental') { - css = context.compiler.build!(candidates) } + css = context.compiler.build(candidates) // Replace CSS if (css !== context.css && optimize) { diff --git a/packages/internal-postcss-fix-relative-paths/src/fixtures/example-project/src/index.css b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/example-project/src/index.css similarity index 100% rename from packages/internal-postcss-fix-relative-paths/src/fixtures/example-project/src/index.css rename to packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/example-project/src/index.css diff --git a/packages/internal-postcss-fix-relative-paths/src/fixtures/example-project/src/invalid.css b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/example-project/src/invalid.css similarity index 100% rename from packages/internal-postcss-fix-relative-paths/src/fixtures/example-project/src/invalid.css rename to packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/example-project/src/invalid.css diff --git a/packages/internal-postcss-fix-relative-paths/src/fixtures/external-import/src/index.css b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/index.css similarity index 100% rename from packages/internal-postcss-fix-relative-paths/src/fixtures/external-import/src/index.css rename to packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/index.css diff --git a/packages/internal-postcss-fix-relative-paths/src/fixtures/external-import/src/invalid.css b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/invalid.css similarity index 100% rename from packages/internal-postcss-fix-relative-paths/src/fixtures/external-import/src/invalid.css rename to packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/invalid.css diff --git a/packages/internal-postcss-fix-relative-paths/src/fixtures/external-import/src/plugins-in-root.css b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/plugins-in-root.css similarity index 100% rename from packages/internal-postcss-fix-relative-paths/src/fixtures/external-import/src/plugins-in-root.css rename to packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/plugins-in-root.css diff --git a/packages/internal-postcss-fix-relative-paths/src/fixtures/external-import/src/plugins-in-sibling.css b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/plugins-in-sibling.css similarity index 100% rename from packages/internal-postcss-fix-relative-paths/src/fixtures/external-import/src/plugins-in-sibling.css rename to packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/fixtures/external-import/src/plugins-in-sibling.css diff --git a/packages/internal-postcss-fix-relative-paths/src/index.test.ts b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.test.ts similarity index 100% rename from packages/internal-postcss-fix-relative-paths/src/index.test.ts rename to packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.test.ts diff --git a/packages/internal-postcss-fix-relative-paths/src/index.ts b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.ts similarity index 96% rename from packages/internal-postcss-fix-relative-paths/src/index.ts rename to packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.ts index 7a0f92da714a..2b88014a3e85 100644 --- a/packages/internal-postcss-fix-relative-paths/src/index.ts +++ b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.ts @@ -1,12 +1,10 @@ +import { normalizePath } from '@tailwindcss/node' import path from 'node:path' import type { AtRule, Plugin } from 'postcss' -import { normalizePath } from './normalize-path' const SINGLE_QUOTE = "'" const DOUBLE_QUOTE = '"' -export { normalizePath } - export default function fixRelativePathsPlugin(): Plugin { // Retain a list of touched at-rules to avoid infinite loops let touched: WeakSet = new WeakSet() diff --git a/packages/@tailwindcss-postcss/tsup.config.ts b/packages/@tailwindcss-postcss/tsup.config.ts index 76e4fc03b0a0..684c072ac854 100644 --- a/packages/@tailwindcss-postcss/tsup.config.ts +++ b/packages/@tailwindcss-postcss/tsup.config.ts @@ -7,7 +7,6 @@ export default defineConfig([ cjsInterop: true, dts: true, entry: ['src/index.ts'], - noExternal: ['internal-postcss-fix-relative-paths'], }, { format: ['cjs'], @@ -15,6 +14,5 @@ export default defineConfig([ cjsInterop: true, dts: true, entry: ['src/index.cts'], - noExternal: ['internal-postcss-fix-relative-paths'], }, ]) diff --git a/packages/@tailwindcss-standalone/src/index.ts b/packages/@tailwindcss-standalone/src/index.ts index 5dfefaf82928..ae90e2b6efc7 100644 --- a/packages/@tailwindcss-standalone/src/index.ts +++ b/packages/@tailwindcss-standalone/src/index.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs' import { createRequire } from 'node:module' import packageJson from 'tailwindcss/package.json' @@ -42,5 +43,14 @@ globalThis.__tw_resolve = (id, baseDir) => { } } globalThis.__tw_version = packageJson.version +globalThis.__tw_readFile = async (path, encoding) => { + // When reading a file from the `$bunfs`, we need to use the synchronous + // `readFileSync` API + let isEmbeddedFileBase = path.includes('/$bunfs/root') || path.includes(':/~BUN/root') + if (!isEmbeddedFileBase) { + return + } + return fs.readFileSync(path, encoding) +} await import('../../@tailwindcss-cli/src/index.ts') diff --git a/packages/@tailwindcss-standalone/src/types.d.ts b/packages/@tailwindcss-standalone/src/types.d.ts index dfdac2715c0c..e13e4e4275d2 100644 --- a/packages/@tailwindcss-standalone/src/types.d.ts +++ b/packages/@tailwindcss-standalone/src/types.d.ts @@ -5,3 +5,6 @@ declare module '*.css' { declare var __tw_version: string | undefined declare var __tw_resolve: undefined | ((id: string, base?: string) => string | false) +declare var __tw_readFile: + | undefined + | ((path: string, encoding: BufferEncoding) => Promise) diff --git a/packages/@tailwindcss-vite/package.json b/packages/@tailwindcss-vite/package.json index a74b4c3c81d7..20a7de978cea 100644 --- a/packages/@tailwindcss-vite/package.json +++ b/packages/@tailwindcss-vite/package.json @@ -31,14 +31,10 @@ "@tailwindcss/node": "workspace:^", "@tailwindcss/oxide": "workspace:^", "lightningcss": "catalog:", - "postcss": "^8.4.41", - "postcss-import": "^16.1.0", "tailwindcss": "workspace:^" }, "devDependencies": { "@types/node": "catalog:", - "@types/postcss-import": "^14.0.3", - "internal-postcss-fix-relative-paths": "workspace:^", "vite": "catalog:" }, "peerDependencies": { diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 8a1c60932e00..53f2aecffb6b 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -1,13 +1,9 @@ -import { compile } from '@tailwindcss/node' +import { compile, normalizePath } from '@tailwindcss/node' import { clearRequireCache } from '@tailwindcss/node/require-cache' import { Scanner } from '@tailwindcss/oxide' -import fixRelativePathsPlugin, { normalizePath } from 'internal-postcss-fix-relative-paths' import { Features, transform } from 'lightningcss' -import fs from 'node:fs/promises' import path from 'path' -import postcss from 'postcss' -import postcssImport from 'postcss-import' import type { Plugin, ResolvedConfig, Rollup, Update, ViteDevServer } from 'vite' export default function tailwindcss(): Plugin[] { @@ -269,18 +265,6 @@ function isPotentialCssRootFile(id: string) { return isCssFile } -function isCssRootFile(content: string) { - return ( - content.includes('@tailwind') || - content.includes('@config') || - content.includes('@plugin') || - content.includes('@apply') || - content.includes('@theme') || - content.includes('@variant') || - content.includes('@utility') - ) -} - function optimizeCss( input: string, { file = 'input.css', minify = false }: { file?: string; minify?: boolean } = {}, @@ -378,30 +362,7 @@ class Root { clearRequireCache(Array.from(this.dependencies)) this.dependencies = new Set([idToPath(inputPath)]) - let postcssCompiled = await postcss([ - postcssImport({ - load: (path) => { - this.dependencies.add(path) - addWatchFile(path) - return fs.readFile(path, 'utf8') - }, - }), - fixRelativePathsPlugin(), - ]).process(content, { - from: inputPath, - to: inputPath, - }) - let css = postcssCompiled.css - - // This is done inside the Root#generate() method so that we can later use - // information from the Tailwind compiler to determine if the file is a - // CSS root (necessary because we will probably inline the `@import` - // resolution at some point). - if (!isCssRootFile(css)) { - return false - } - - this.compiler = await compile(css, { + this.compiler = await compile(content, { base: inputBase, onDependency: (path) => { addWatchFile(path) @@ -410,12 +371,7 @@ class Root { }) this.scanner = new Scanner({ - sources: this.compiler.globs.map(({ origin, pattern }) => ({ - // Ensure the glob is relative to the input CSS file or the config - // file where it is specified. - base: origin ? path.dirname(path.resolve(inputBase, origin)) : inputBase, - pattern, - })), + sources: this.compiler.globs, }) } diff --git a/packages/@tailwindcss-vite/tsup.config.ts b/packages/@tailwindcss-vite/tsup.config.ts index eaf99e82abae..85bf3149d3f9 100644 --- a/packages/@tailwindcss-vite/tsup.config.ts +++ b/packages/@tailwindcss-vite/tsup.config.ts @@ -6,5 +6,4 @@ export default defineConfig({ minify: true, dts: true, entry: ['src/index.ts'], - noExternal: ['internal-postcss-fix-relative-paths'], }) diff --git a/packages/internal-postcss-fix-relative-paths/package.json b/packages/internal-postcss-fix-relative-paths/package.json deleted file mode 100644 index 893f7466d1d3..000000000000 --- a/packages/internal-postcss-fix-relative-paths/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "internal-postcss-fix-relative-paths", - "version": "0.0.0", - "private": true, - "scripts": { - "lint": "tsc --noEmit", - "build": "tsup-node ./src/index.ts --format cjs,esm --dts --cjsInterop --splitting --minify --clean", - "dev": "pnpm run build -- --watch" - }, - "files": [ - "dist/" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.js" - } - }, - "dependencies": {}, - "devDependencies": { - "@types/node": "catalog:", - "@types/postcss-import": "^14.0.3", - "postcss": "8.4.41", - "postcss-import": "^16.1.0" - } -} diff --git a/packages/internal-postcss-fix-relative-paths/tsconfig.json b/packages/internal-postcss-fix-relative-paths/tsconfig.json deleted file mode 100644 index 6ae022f65bf0..000000000000 --- a/packages/internal-postcss-fix-relative-paths/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../tsconfig.base.json", -} diff --git a/packages/tailwindcss/package.json b/packages/tailwindcss/package.json index 7173e7201608..10e45bdb3aa1 100644 --- a/packages/tailwindcss/package.json +++ b/packages/tailwindcss/package.json @@ -89,6 +89,7 @@ "devDependencies": { "@tailwindcss/oxide": "workspace:^", "@types/node": "catalog:", - "lightningcss": "catalog:" + "lightningcss": "catalog:", + "dedent": "1.5.3" } } diff --git a/packages/tailwindcss/src/ast.test.ts b/packages/tailwindcss/src/ast.test.ts index da0b19204375..21915cba4a4c 100644 --- a/packages/tailwindcss/src/ast.test.ts +++ b/packages/tailwindcss/src/ast.test.ts @@ -1,5 +1,5 @@ import { expect, it } from 'vitest' -import { toCss } from './ast' +import { context, decl, rule, toCss, walk } from './ast' import * as CSS from './css-parser' it('should pretty print an AST', () => { @@ -13,3 +13,54 @@ it('should pretty print an AST', () => { " `) }) + +it('allows the placement of context nodes', () => { + const ast = [ + rule('.foo', [decl('color', 'red')]), + context({ context: 'a' }, [ + rule('.bar', [ + decl('color', 'blue'), + context({ context: 'b' }, [ + // + rule('.baz', [decl('color', 'green')]), + ]), + ]), + ]), + ] + + let redContext + let blueContext + let greenContext + + walk(ast, (node, { context }) => { + if (node.kind !== 'declaration') return + switch (node.value) { + case 'red': + redContext = context + break + case 'blue': + blueContext = context + break + case 'green': + greenContext = context + break + } + }) + + expect(redContext).toEqual({}) + expect(blueContext).toEqual({ context: 'a' }) + expect(greenContext).toEqual({ context: 'b' }) + + expect(toCss(ast)).toMatchInlineSnapshot(` + ".foo { + color: red; + } + .bar { + color: blue; + .baz { + color: green; + } + } + " + `) +}) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index fa5dc6f1b277..afc1888c2659 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -16,7 +16,13 @@ export type Comment = { value: string } -export type AstNode = Rule | Declaration | Comment +export type Context = { + kind: 'context' + context: Record + nodes: AstNode[] +} + +export type AstNode = Rule | Declaration | Comment | Context export function rule(selector: string, nodes: AstNode[]): Rule { return { @@ -42,6 +48,14 @@ export function comment(value: string): Comment { } } +export function context(context: Record, nodes: AstNode[]): Context { + return { + kind: 'context', + context, + nodes, + } +} + export enum WalkAction { /** Continue walking, which is the default */ Continue, @@ -60,12 +74,23 @@ export function walk( utils: { parent: AstNode | null replaceWith(newNode: AstNode | AstNode[]): void + context: Record }, ) => void | WalkAction, parent: AstNode | null = null, + context: Record = {}, ) { for (let i = 0; i < ast.length; i++) { let node = ast[i] + + // We want context nodes to be transparent in walks. This means that + // whenever we encounter one, we immediately walk through its children and + // furthermore we also don't update the parent. + if (node.kind === 'context') { + walk(node.nodes, visit, parent, { ...context, ...node.context }) + continue + } + let status = visit(node, { parent, @@ -76,6 +101,7 @@ export function walk( // will process this position (containing the replaced node) again. i-- }, + context, }) ?? WalkAction.Continue // Stop the walk entirely @@ -85,7 +111,7 @@ export function walk( if (status === WalkAction.Skip) continue if (node.kind === 'rule') { - walk(node.nodes, visit, node) + walk(node.nodes, visit, node, context) } } } @@ -171,6 +197,13 @@ export function toCss(ast: AstNode[]) { css += `${indent}/*${node.value}*/\n` } + // Context Node + else if (node.kind === 'context') { + for (let child of node.nodes) { + css += stringify(child, depth) + } + } + // Declaration else if (node.property !== '--tw-sort' && node.value !== undefined && node.value !== null) { css += `${indent}${node.property}: ${node.value}${node.important ? '!important' : ''};\n` diff --git a/packages/tailwindcss/src/at-import.test.ts b/packages/tailwindcss/src/at-import.test.ts new file mode 100644 index 000000000000..bbd5e6863f26 --- /dev/null +++ b/packages/tailwindcss/src/at-import.test.ts @@ -0,0 +1,572 @@ +import { expect, test, vi } from 'vitest' +import type { Plugin } from './compat/plugin-api' +import { compile, type Config } from './index' +import plugin from './plugin' +import { optimizeCss } from './test-utils/run' + +let css = String.raw + +async function run( + css: string, + { + loadStylesheet = () => Promise.reject(new Error('Unexpected stylesheet')), + loadModule = () => Promise.reject(new Error('Unexpected module')), + candidates = [], + optimize = true, + }: { + loadStylesheet?: (id: string, base: string) => Promise<{ content: string; base: string }> + loadModule?: ( + id: string, + base: string, + resourceHint: 'plugin' | 'config', + ) => Promise<{ module: Config | Plugin; base: string }> + candidates?: string[] + optimize?: boolean + }, +) { + let compiler = await compile(css, { base: '/root', loadStylesheet, loadModule }) + let result = compiler.build(candidates) + return optimize ? optimizeCss(result) : result +} + +test('can resolve relative @imports', async () => { + let loadStylesheet = async (id: string, base: string) => { + expect(base).toBe('/root') + expect(id).toBe('./foo/bar.css') + return { + content: css` + .foo { + color: red; + } + `, + base: '/root/foo', + } + } + + await expect( + run( + css` + @import './foo/bar.css'; + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + ".foo { + color: red; + } + " + `) +}) + +test('can recursively resolve relative @imports', async () => { + let loadStylesheet = async (id: string, base: string) => { + if (base === '/root' && id === './foo/bar.css') { + return { + content: css` + @import './bar/baz.css'; + `, + base: '/root/foo', + } + } else if (base === '/root/foo' && id === './bar/baz.css') { + return { + content: css` + .baz { + color: blue; + } + `, + base: '/root/foo/bar', + } + } + + throw new Error(`Unexpected import: ${id}`) + } + + await expect( + run( + css` + @import './foo/bar.css'; + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + ".baz { + color: #00f; + } + " + `) +}) + +let exampleCSS = css` + a { + color: red; + } +` +let loadStylesheet = async (id: string) => { + if (!id.endsWith('example.css')) throw new Error('Unexpected import: ' + id) + return { + content: exampleCSS, + base: '/root', + } +} + +test('extracts path from @import nodes', async () => { + await expect( + run( + css` + @import 'example.css'; + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "a { + color: red; + } + " + `) + + await expect( + run( + css` + @import './example.css'; + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "a { + color: red; + } + " + `) + + await expect( + run( + css` + @import '/example.css'; + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "a { + color: red; + } + " + `) +}) + +test('url() imports are passed-through', async () => { + await expect( + run( + css` + @import url('example.css'); + `, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, + ), + ).resolves.toMatchInlineSnapshot(` + "@import url('example.css'); + " + `) + + await expect( + run( + css` + @import url('./example.css'); + `, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, + ), + ).resolves.toMatchInlineSnapshot(` + "@import url('./example.css'); + " + `) + + await expect( + run( + css` + @import url('/example.css'); + `, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, + ), + ).resolves.toMatchInlineSnapshot(` + "@import url('/example.css'); + " + `) + + await expect( + run( + css` + @import url(example.css); + `, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, + ), + ).resolves.toMatchInlineSnapshot(` + "@import url(example.css); + " + `) + + await expect( + run( + css` + @import url(./example.css); + `, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, + ), + ).resolves.toMatchInlineSnapshot(` + "@import url(./example.css); + " + `) + + await expect( + run( + css` + @import url(/example.css); + `, + { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, + ), + ).resolves.toMatchInlineSnapshot(` + "@import url(/example.css); + " + `) +}) + +test('handles case-insensitive @import directive', async () => { + await expect( + run( + css` + @import 'example.css'; + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "a { + color: red; + } + " + `) +}) + +test('@media', async () => { + await expect( + run( + css` + @import 'example.css' print; + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "@media print { + a { + color: red; + } + } + " + `) + + await expect( + run( + css` + @import 'example.css' print, screen; + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "@media print, screen { + a { + color: red; + } + } + " + `) + + await expect( + run( + css` + @import 'example.css' screen and (orientation: landscape); + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "@media screen and (orientation: landscape) { + a { + color: red; + } + } + " + `) + + await expect( + run( + css` + @import 'example.css' foo(bar); + `, + { loadStylesheet, optimize: false }, + ), + ).resolves.toMatchInlineSnapshot(` + "@media foo(bar) { + a { + color: red; + } + } + " + `) +}) + +test('@supports', async () => { + await expect( + run( + css` + @import 'example.css' supports(display: grid); + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "@supports (display: grid) { + a { + color: red; + } + } + " + `) + + await expect( + run( + css` + @import 'example.css' supports(display: grid) screen and (max-width: 400px); + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "@supports (display: grid) { + @media screen and (width <= 400px) { + a { + color: red; + } + } + } + " + `) + + await expect( + run( + css` + @import 'example.css' supports((not (display: grid)) and (display: flex)) screen and + (max-width: 400px); + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "@supports (not (display: grid)) and (display: flex) { + @media screen and (width <= 400px) { + a { + color: red; + } + } + } + " + `) + + await expect( + run( + // prettier-ignore + css` + @import 'example.css' + supports((selector(h2 > p)) and (font-tech(color-COLRv1))); + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "@supports selector(h2 > p) and font-tech(color-COLRv1) { + a { + color: red; + } + } + " + `) +}) + +test('@layer', async () => { + await expect( + run( + css` + @import 'example.css' layer(utilities); + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "@layer utilities { + a { + color: red; + } + } + " + `) + + await expect( + run( + css` + @import 'example.css' layer(); + `, + { loadStylesheet }, + ), + ).resolves.toMatchInlineSnapshot(` + "@layer { + a { + color: red; + } + } + " + `) +}) + +test('supports theme(reference) imports', async () => { + expect( + run( + css` + @tailwind utilities; + @import 'example.css' theme(reference); + `, + { + loadStylesheet: () => + Promise.resolve({ + content: css` + @theme { + --color-red-500: red; + } + `, + base: '', + }), + candidates: ['text-red-500'], + }, + ), + ).resolves.toMatchInlineSnapshot(` + ".text-red-500 { + color: var(--color-red-500, red); + } + " + `) +}) + +test('updates the base when loading modules inside nested files', async () => { + let loadStylesheet = () => + Promise.resolve({ + content: css` + @config './nested-config.js'; + @plugin './nested-plugin.js'; + `, + base: '/root/foo', + }) + let loadModule = vi.fn().mockResolvedValue({ base: '', module: () => {} }) + + expect( + ( + await run( + css` + @import './foo/bar.css'; + @config './root-config.js'; + @plugin './root-plugin.js'; + `, + { loadStylesheet, loadModule }, + ) + ).trim(), + ).toBe('') + + expect(loadModule).toHaveBeenNthCalledWith(1, './nested-config.js', '/root/foo', 'config') + expect(loadModule).toHaveBeenNthCalledWith(2, './root-config.js', '/root', 'config') + expect(loadModule).toHaveBeenNthCalledWith(3, './nested-plugin.js', '/root/foo', 'plugin') + expect(loadModule).toHaveBeenNthCalledWith(4, './root-plugin.js', '/root', 'plugin') +}) + +test('emits the right base for @source directives inside nested files', async () => { + let loadStylesheet = () => + Promise.resolve({ + content: css` + @source './nested/**/*.css'; + `, + base: '/root/foo', + }) + + let compiler = await compile( + css` + @import './foo/bar.css'; + @source './root/**/*.css'; + `, + { base: '/root', loadStylesheet }, + ) + + expect(compiler.globs).toEqual([ + { pattern: './nested/**/*.css', base: '/root/foo' }, + { pattern: './root/**/*.css', base: '/root' }, + ]) +}) + +test('emits the right base for @source found inside JS configs and plugins from nested imports', async () => { + let loadStylesheet = () => + Promise.resolve({ + content: css` + @config './nested-config.js'; + @plugin './nested-plugin.js'; + `, + base: '/root/foo', + }) + let loadModule = vi.fn().mockImplementation((id: string) => { + let base = id.includes('nested') ? '/root/foo' : '/root' + if (id.includes('config')) { + let glob = id.includes('nested') ? './nested-config/*.html' : './root-config/*.html' + let pluginGlob = id.includes('nested') + ? './nested-config-plugin/*.html' + : './root-config-plugin/*.html' + return { + module: { + content: [glob], + plugins: [plugin(() => {}, { content: [pluginGlob] })], + } satisfies Config, + base: base + '-config', + } + } else { + let glob = id.includes('nested') ? './nested-plugin/*.html' : './root-plugin/*.html' + return { + module: plugin(() => {}, { content: [glob] }), + base: base + '-plugin', + } + } + }) + + let compiler = await compile( + css` + @import './foo/bar.css'; + @config './root-config.js'; + @plugin './root-plugin.js'; + `, + { base: '/root', loadStylesheet, loadModule }, + ) + + expect(compiler.globs).toEqual([ + { pattern: './nested-plugin/*.html', base: '/root/foo-plugin' }, + { pattern: './root-plugin/*.html', base: '/root-plugin' }, + + { pattern: './nested-config-plugin/*.html', base: '/root/foo-config' }, + { pattern: './nested-config/*.html', base: '/root/foo-config' }, + + { pattern: './root-config-plugin/*.html', base: '/root-config' }, + { pattern: './root-config/*.html', base: '/root-config' }, + ]) +}) + +test('it crashes when inside a cycle', async () => { + let loadStylesheet = () => + Promise.resolve({ + content: css` + @import 'foo.css'; + `, + base: '/root', + }) + + expect( + run( + css` + @import 'foo.css'; + `, + { loadStylesheet }, + ), + ).rejects.toMatchInlineSnapshot( + `[Error: Exceeded maximum recursion depth while resolving \`foo.css\` in \`/root\`)]`, + ) +}) diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts new file mode 100644 index 000000000000..a06000826b05 --- /dev/null +++ b/packages/tailwindcss/src/at-import.ts @@ -0,0 +1,147 @@ +import { context, rule, walk, WalkAction, type AstNode } from './ast' +import * as CSS from './css-parser' +import * as ValueParser from './value-parser' + +type LoadStylesheet = (id: string, basedir: string) => Promise<{ base: string; content: string }> + +export async function substituteAtImports( + ast: AstNode[], + base: string, + loadStylesheet: LoadStylesheet, + recurseCount = 0, +) { + let promises: Promise[] = [] + + walk(ast, (node, { replaceWith }) => { + if ( + node.kind === 'rule' && + node.selector[0] === '@' && + node.selector.toLowerCase().startsWith('@import ') + ) { + try { + let { uri, layer, media, supports } = parseImportParams( + ValueParser.parse(node.selector.slice(8)), + ) + + // Skip importing data or remote URIs + if (uri.startsWith('data:')) return + if (uri.startsWith('http://') || uri.startsWith('https://')) return + + let contextNode = context({}, []) + + promises.push( + (async () => { + // Since we do not have fully resolved paths in core, we can't reliably detect circular + // imports. Instead, we try to limit the recursion depth to a number that is too large + // to be reached in practice. + if (recurseCount > 100) { + throw new Error( + `Exceeded maximum recursion depth while resolving \`${uri}\` in \`${base}\`)`, + ) + } + + const loaded = await loadStylesheet(uri, base) + let ast = CSS.parse(loaded.content) + await substituteAtImports(ast, loaded.base, loadStylesheet, recurseCount + 1) + + contextNode.nodes = buildImportNodes(ast, layer, media, supports) + contextNode.context.base = loaded.base + })(), + ) + + replaceWith(contextNode) + // The resolved Stylesheets already have their transitive @imports + // resolved, so we can skip walking them. + return WalkAction.Skip + } catch (e: any) { + // When an error occurs while parsing the `@import` statement, we skip + // the import. + } + } + }) + + await Promise.all(promises) +} + +// Modified and inlined version of `parse-statements` from +// `postcss-import` +// Copyright (c) 2014 Maxime Thirouin, Jason Campbell & Kevin MÃ¥rtensson +// Released under the MIT License. +function parseImportParams(params: ValueParser.ValueAstNode[]) { + let uri + let layer: string | null = null + let media: string | null = null + let supports: string | null = null + + for (let i = 0; i < params.length; i++) { + const node = params[i] + + if (node.kind === 'separator') continue + + if (node.kind === 'word' && !uri) { + if (!node.value) throw new Error(`Unable to find uri`) + if (node.value[0] !== '"' && node.value[0] !== "'") throw new Error('Unable to find uri') + + uri = node.value.slice(1, -1) + continue + } + + if (node.kind === 'function' && node.value.toLowerCase() === 'url') { + throw new Error('url functions are not supported') + } + + if (!uri) throw new Error('Unable to find uri') + + if ( + (node.kind === 'word' || node.kind === 'function') && + node.value.toLowerCase() === 'layer' + ) { + if (layer) throw new Error('Multiple layers') + if (supports) throw new Error('layers must be defined before support conditions') + + if ('nodes' in node) { + layer = ValueParser.toCss(node.nodes) + } else { + layer = '' + } + + continue + } + + if (node.kind === 'function' && node.value.toLowerCase() === 'supports') { + if (supports) throw new Error('Multiple support conditions') + supports = ValueParser.toCss(node.nodes) + continue + } + + media = ValueParser.toCss(params.slice(i)) + break + } + + if (!uri) throw new Error('Unable to find uri') + + return { uri, layer, media, supports } +} + +function buildImportNodes( + importedAst: AstNode[], + layer: string | null, + media: string | null, + supports: string | null, +): AstNode[] { + let root = importedAst + + if (layer !== null) { + root = [rule('@layer ' + layer, root)] + } + + if (media !== null) { + root = [rule('@media ' + media, root)] + } + + if (supports !== null) { + root = [rule(`@supports ${supports[0] === '(' ? supports : `(${supports})`}`, root)] + } + + return root +} diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index 686b37dc8f6b..4290c136ab5e 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -15,21 +15,25 @@ import { registerThemeVariantOverrides } from './theme-variants' export async function applyCompatibilityHooks({ designSystem, + base, ast, - loadPlugin, - loadConfig, + loadModule, globs, }: { designSystem: DesignSystem + base: string ast: AstNode[] - loadPlugin: (path: string) => Promise - loadConfig: (path: string) => Promise + loadModule: ( + path: string, + base: string, + resourceHint: 'plugin' | 'config', + ) => Promise<{ module: any; base: string }> globs: { origin?: string; pattern: string }[] }) { - let pluginPaths: [string, CssPluginOptions | null][] = [] - let configPaths: string[] = [] + let pluginPaths: [{ id: string; base: string }, CssPluginOptions | null][] = [] + let configPaths: { id: string; base: string }[] = [] - walk(ast, (node, { parent, replaceWith }) => { + walk(ast, (node, { parent, replaceWith, context }) => { if (node.kind !== 'rule' || node.selector[0] !== '@') return // Collect paths from `@plugin` at-rules @@ -86,7 +90,10 @@ export async function applyCompatibilityHooks({ options[decl.property] = parts.length === 1 ? parts[0] : parts } - pluginPaths.push([pluginPath, Object.keys(options).length > 0 ? options : null]) + pluginPaths.push([ + { id: pluginPath, base: context.base }, + Object.keys(options).length > 0 ? options : null, + ]) replaceWith([]) return @@ -102,7 +109,7 @@ export async function applyCompatibilityHooks({ throw new Error('`@config` cannot be nested.') } - configPaths.push(node.selector.slice(9, -1)) + configPaths.push({ id: node.selector.slice(9, -1), base: context.base }) replaceWith([]) return } @@ -142,38 +149,48 @@ export async function applyCompatibilityHooks({ // any additional backwards compatibility hooks. if (!pluginPaths.length && !configPaths.length) return - let configs = await Promise.all( - configPaths.map(async (configPath) => ({ - path: configPath, - config: await loadConfig(configPath), - })), - ) - let pluginDetails = await Promise.all( - pluginPaths.map(async ([pluginPath, pluginOptions]) => ({ - path: pluginPath, - plugin: await loadPlugin(pluginPath), - options: pluginOptions, - })), - ) + let [configs, pluginDetails] = await Promise.all([ + Promise.all( + configPaths.map(async ({ id, base }) => { + let loaded = await loadModule(id, base, 'config') + return { + path: id, + base: loaded.base, + config: loaded.module as UserConfig, + } + }), + ), + Promise.all( + pluginPaths.map(async ([{ id, base }, pluginOptions]) => { + let loaded = await loadModule(id, base, 'plugin') + return { + path: id, + base: loaded.base, + plugin: loaded.module as Plugin, + options: pluginOptions, + } + }), + ), + ]) - let plugins = pluginDetails.map((detail) => { + let pluginConfigs = pluginDetails.map((detail) => { if (!detail.options) { - return detail.plugin + return { config: { plugins: [detail.plugin] }, base: detail.base } } if ('__isOptionsFunction' in detail.plugin) { - return detail.plugin(detail.options) + return { config: { plugins: [detail.plugin(detail.options)] }, base: detail.base } } throw new Error(`The plugin "${detail.path}" does not accept options`) }) - let userConfig = [{ config: { plugins } }, ...configs] + let userConfig = [...pluginConfigs, ...configs] let resolvedConfig = resolveConfig(designSystem, [ - { config: createCompatConfig(designSystem.theme) }, + { config: createCompatConfig(designSystem.theme), base }, ...userConfig, - { config: { plugins: [darkModePlugin] } }, + { config: { plugins: [darkModePlugin] }, base }, ]) let resolvedUserConfig = resolveConfig(designSystem, userConfig) @@ -221,7 +238,7 @@ export async function applyCompatibilityHooks({ ) } - globs.push({ origin: file.base, pattern: file.pattern }) + globs.push(file) } } diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts index af511a25e32f..aad2753b021d 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.test.ts @@ -35,6 +35,7 @@ test('Config values can be merged into the theme', () => { }, }, }, + base: '/root', }, ]) applyConfigToTheme(design, resolvedUserConfig) diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index acfaebc9e411..2b8819a68d83 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest' -import { compile } from '..' +import { compile, type Config } from '..' import plugin from '../plugin' import { flattenColorPalette } from './flatten-color-palette' @@ -12,10 +12,10 @@ test('Config files can add content', async () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ content: ['./file.txt'] }), + loadModule: async () => ({ module: { content: ['./file.txt'] }, base: '/root' }), }) - expect(compiler.globs).toEqual([{ origin: './config.js', pattern: './file.txt' }]) + expect(compiler.globs).toEqual([{ base: '/root', pattern: './file.txt' }]) }) test('Config files can change dark mode (media)', async () => { @@ -25,7 +25,7 @@ test('Config files can change dark mode (media)', async () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ darkMode: 'media' }), + loadModule: async () => ({ module: { darkMode: 'media' }, base: '/root' }), }) expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(` @@ -45,7 +45,7 @@ test('Config files can change dark mode (selector)', async () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ darkMode: 'selector' }), + loadModule: async () => ({ module: { darkMode: 'selector' }, base: '/root' }), }) expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(` @@ -65,7 +65,10 @@ test('Config files can change dark mode (variant)', async () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ darkMode: ['variant', '&:where(:not(.light))'] }), + loadModule: async () => ({ + module: { darkMode: ['variant', '&:where(:not(.light))'] }, + base: '/root', + }), }) expect(compiler.build(['dark:underline'])).toMatchInlineSnapshot(` @@ -85,16 +88,19 @@ test('Config files can add plugins', async () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - plugins: [ - plugin(function ({ addUtilities }) { - addUtilities({ - '.no-scrollbar': { - 'scrollbar-width': 'none', - }, - }) - }), - ], + loadModule: async () => ({ + module: { + plugins: [ + plugin(function ({ addUtilities }) { + addUtilities({ + '.no-scrollbar': { + 'scrollbar-width': 'none', + }, + }) + }), + ], + }, + base: '/root', }), }) @@ -113,12 +119,15 @@ test('Plugins loaded from config files can contribute to the config', async () = ` let compiler = await compile(input, { - loadConfig: async () => ({ - plugins: [ - plugin(() => {}, { - darkMode: ['variant', '&:where(:not(.light))'], - }), - ], + loadModule: async () => ({ + module: { + plugins: [ + plugin(() => {}, { + darkMode: ['variant', '&:where(:not(.light))'], + }), + ], + }, + base: '/root', }), }) @@ -139,12 +148,15 @@ test('Config file presets can contribute to the config', async () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - presets: [ - { - darkMode: ['variant', '&:where(:not(.light))'], - }, - ], + loadModule: async () => ({ + module: { + presets: [ + { + darkMode: ['variant', '&:where(:not(.light))'], + }, + ], + }, + base: '/root', }), }) @@ -165,24 +177,27 @@ test('Config files can affect the theme', async () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - extend: { - colors: { - primary: '#c0ffee', + loadModule: async () => ({ + module: { + theme: { + extend: { + colors: { + primary: '#c0ffee', + }, }, }, - }, - plugins: [ - plugin(function ({ addUtilities, theme }) { - addUtilities({ - '.scrollbar-primary': { - scrollbarColor: theme('colors.primary'), - }, - }) - }), - ], + plugins: [ + plugin(function ({ addUtilities, theme }) { + addUtilities({ + '.scrollbar-primary': { + scrollbarColor: theme('colors.primary'), + }, + }) + }), + ], + }, + base: '/root', }), }) @@ -206,13 +221,16 @@ test('Variants in CSS overwrite variants from plugins', async () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - darkMode: ['variant', '&:is(.dark)'], - plugins: [ - plugin(function ({ addVariant }) { - addVariant('light', '&:is(.light)') - }), - ], + loadModule: async () => ({ + module: { + darkMode: ['variant', '&:is(.dark)'], + plugins: [ + plugin(function ({ addVariant }) { + addVariant('light', '&:is(.light)') + }), + ], + }, + base: '/root', }), }) @@ -253,49 +271,52 @@ describe('theme callbacks', () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - extend: { - fontSize: { - base: ['200rem', { lineHeight: '201rem' }], - md: ['200rem', { lineHeight: '201rem' }], - xl: ['200rem', { lineHeight: '201rem' }], - }, - - // Direct access - lineHeight: ({ theme }) => ({ - base: theme('fontSize.base[1].lineHeight'), - md: theme('fontSize.md[1].lineHeight'), - xl: theme('fontSize.xl[1].lineHeight'), - }), - - // Tuple access - typography: ({ theme }) => ({ - '[class~=lead-base]': { - fontSize: theme('fontSize.base')[0], - ...theme('fontSize.base')[1], - }, - '[class~=lead-md]': { - fontSize: theme('fontSize.md')[0], - ...theme('fontSize.md')[1], + loadModule: async () => ({ + module: { + theme: { + extend: { + fontSize: { + base: ['200rem', { lineHeight: '201rem' }], + md: ['200rem', { lineHeight: '201rem' }], + xl: ['200rem', { lineHeight: '201rem' }], }, - '[class~=lead-xl]': { - fontSize: theme('fontSize.xl')[0], - ...theme('fontSize.xl')[1], - }, - }), + + // Direct access + lineHeight: ({ theme }) => ({ + base: theme('fontSize.base[1].lineHeight'), + md: theme('fontSize.md[1].lineHeight'), + xl: theme('fontSize.xl[1].lineHeight'), + }), + + // Tuple access + typography: ({ theme }) => ({ + '[class~=lead-base]': { + fontSize: theme('fontSize.base')[0], + ...theme('fontSize.base')[1], + }, + '[class~=lead-md]': { + fontSize: theme('fontSize.md')[0], + ...theme('fontSize.md')[1], + }, + '[class~=lead-xl]': { + fontSize: theme('fontSize.xl')[0], + ...theme('fontSize.xl')[1], + }, + }), + }, }, - }, - plugins: [ - plugin(function ({ addUtilities, theme }) { - addUtilities({ - '.prose': { - ...theme('typography'), - }, - }) - }), - ], + plugins: [ + plugin(function ({ addUtilities, theme }) { + addUtilities({ + '.prose': { + ...theme('typography'), + }, + }) + }), + ], + } satisfies Config, + base: '/root', }), }) @@ -361,15 +382,18 @@ describe('theme overrides order', () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - extend: { - colors: { - red: 'very-red', - blue: 'very-blue', + loadModule: async () => ({ + module: { + theme: { + extend: { + colors: { + red: 'very-red', + blue: 'very-blue', + }, }, }, }, + base: '/root', }), }) @@ -404,35 +428,43 @@ describe('theme overrides order', () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - extend: { - colors: { - slate: { - 200: '#200200', - 400: '#200400', - 600: '#200600', - }, - }, - }, - }, - }), - - loadPlugin: async () => { - return plugin(({ matchUtilities, theme }) => { - matchUtilities( - { - 'hover-bg': (value) => { - return { - '&:hover': { - backgroundColor: value, + loadModule: async (id) => { + if (id.includes('config.js')) { + return { + module: { + theme: { + extend: { + colors: { + slate: { + 200: '#200200', + 400: '#200400', + 600: '#200600', + }, }, - } + }, }, - }, - { values: flattenColorPalette(theme('colors')) }, - ) - }) + } satisfies Config, + base: '/root', + } + } else { + return { + module: plugin(({ matchUtilities, theme }) => { + matchUtilities( + { + 'hover-bg': (value) => { + return { + '&:hover': { + backgroundColor: value, + }, + } + }, + }, + { values: flattenColorPalette(theme('colors')) }, + ) + }), + base: '/root', + } + } }, }) @@ -524,12 +556,15 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - fontFamily: { - sans: 'Potato Sans', + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: 'Potato Sans', + }, }, }, + base: '/root', }), }) @@ -560,12 +595,15 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - fontFamily: { - sans: ['Potato Sans', { fontFeatureSettings: '"cv06"' }], + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: ['Potato Sans', { fontFeatureSettings: '"cv06"' }], + }, }, }, + base: '/root', }), }) @@ -597,12 +635,15 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - fontFamily: { - sans: ['Potato Sans', { fontVariationSettings: '"XHGT" 0.7' }], + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: ['Potato Sans', { fontVariationSettings: '"XHGT" 0.7' }], + }, }, }, + base: '/root', }), }) @@ -634,15 +675,18 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - fontFamily: { - sans: [ - 'Potato Sans', - { fontFeatureSettings: '"cv06"', fontVariationSettings: '"XHGT" 0.7' }, - ], + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: [ + 'Potato Sans', + { fontFeatureSettings: '"cv06"', fontVariationSettings: '"XHGT" 0.7' }, + ], + }, }, }, + base: '/root', }), }) @@ -678,12 +722,15 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - fontFamily: { - sans: 'Potato Sans', + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: 'Potato Sans', + }, }, }, + base: '/root', }), }) @@ -715,12 +762,15 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - fontFamily: { - sans: ['Inter', 'system-ui', 'sans-serif'], + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: ['Inter', 'system-ui', 'sans-serif'], + }, }, }, + base: '/root', }), }) @@ -751,12 +801,15 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - fontFamily: { - sans: { foo: 'bar', banana: 'sandwich' }, + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + sans: { foo: 'bar', banana: 'sandwich' }, + }, }, }, + base: '/root', }), }) @@ -782,12 +835,15 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - fontFamily: { - mono: 'Potato Mono', + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + mono: 'Potato Mono', + }, }, }, + base: '/root', }), }) @@ -818,12 +874,15 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - fontFamily: { - mono: ['Potato Mono', { fontFeatureSettings: '"cv06"' }], + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + mono: ['Potato Mono', { fontFeatureSettings: '"cv06"' }], + }, }, }, + base: '/root', }), }) @@ -855,12 +914,15 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - fontFamily: { - mono: ['Potato Mono', { fontVariationSettings: '"XHGT" 0.7' }], + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + mono: ['Potato Mono', { fontVariationSettings: '"XHGT" 0.7' }], + }, }, }, + base: '/root', }), }) @@ -892,15 +954,18 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - fontFamily: { - mono: [ - 'Potato Mono', - { fontFeatureSettings: '"cv06"', fontVariationSettings: '"XHGT" 0.7' }, - ], + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + mono: [ + 'Potato Mono', + { fontFeatureSettings: '"cv06"', fontVariationSettings: '"XHGT" 0.7' }, + ], + }, }, }, + base: '/root', }), }) @@ -936,12 +1001,15 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - fontFamily: { - mono: 'Potato Mono', + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + mono: 'Potato Mono', + }, }, }, + base: '/root', }), }) @@ -973,12 +1041,15 @@ describe('default font family compatibility', () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - fontFamily: { - mono: { foo: 'bar', banana: 'sandwich' }, + loadModule: async () => ({ + module: { + theme: { + fontFamily: { + mono: { foo: 'bar', banana: 'sandwich' }, + }, }, }, + base: '/root', }), }) @@ -1000,21 +1071,24 @@ test('creates variants for `data`, `supports`, and `aria` theme options at the s ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - extend: { - aria: { - polite: 'live="polite"', - }, - supports: { - 'child-combinator': 'selector(h2 > p)', - foo: 'bar', - }, - data: { - checked: 'ui~="checked"', + loadModule: async () => ({ + module: { + theme: { + extend: { + aria: { + polite: 'live="polite"', + }, + supports: { + 'child-combinator': 'selector(h2 > p)', + foo: 'bar', + }, + data: { + checked: 'ui~="checked"', + }, }, }, }, + base: '/root', }), }) @@ -1096,14 +1170,17 @@ test('merges css breakpoints with js config screens', async () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - extend: { - screens: { - sm: '44rem', + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + sm: '44rem', + }, }, }, }, + base: '/root', }), }) diff --git a/packages/tailwindcss/src/compat/config/resolve-config.test.ts b/packages/tailwindcss/src/compat/config/resolve-config.test.ts index 818ff8373d85..895939bcaa4a 100644 --- a/packages/tailwindcss/src/compat/config/resolve-config.test.ts +++ b/packages/tailwindcss/src/compat/config/resolve-config.test.ts @@ -19,6 +19,7 @@ test('top level theme keys are replaced', () => { }, }, }, + base: '/root', }, { config: { @@ -28,6 +29,7 @@ test('top level theme keys are replaced', () => { }, }, }, + base: '/root', }, { config: { @@ -37,6 +39,7 @@ test('top level theme keys are replaced', () => { }, }, }, + base: '/root', }, ]) @@ -68,6 +71,7 @@ test('theme can be extended', () => { }, }, }, + base: '/root', }, { config: { @@ -79,6 +83,7 @@ test('theme can be extended', () => { }, }, }, + base: '/root', }, ]) @@ -112,6 +117,7 @@ test('theme keys can reference other theme keys using the theme function regardl }, }, }, + base: '/root', }, { config: { @@ -124,6 +130,7 @@ test('theme keys can reference other theme keys using the theme function regardl }, }, }, + base: '/root', }, { config: { @@ -135,6 +142,7 @@ test('theme keys can reference other theme keys using the theme function regardl }, }, }, + base: '/root', }, ]) @@ -192,6 +200,7 @@ test('theme keys can read from the CSS theme', () => { }), }, }, + base: '/root', }, ]) diff --git a/packages/tailwindcss/src/compat/config/resolve-config.ts b/packages/tailwindcss/src/compat/config/resolve-config.ts index 268e6883a9dc..1f59b50cf149 100644 --- a/packages/tailwindcss/src/compat/config/resolve-config.ts +++ b/packages/tailwindcss/src/compat/config/resolve-config.ts @@ -12,6 +12,7 @@ import { export interface ConfigFile { path?: string + base: string config: UserConfig } @@ -103,7 +104,7 @@ export interface PluginUtils { theme(keypath: string, defaultValue?: any): any } -function extractConfigs(ctx: ResolutionContext, { config, path }: ConfigFile): void { +function extractConfigs(ctx: ResolutionContext, { config, base, path }: ConfigFile): void { let plugins: PluginWithConfig[] = [] // Normalize plugins so they share the same shape @@ -133,7 +134,7 @@ function extractConfigs(ctx: ResolutionContext, { config, path }: ConfigFile): v } for (let preset of config.presets ?? []) { - extractConfigs(ctx, { path, config: preset }) + extractConfigs(ctx, { path, base, config: preset }) } // Apply configs from plugins @@ -141,7 +142,7 @@ function extractConfigs(ctx: ResolutionContext, { config, path }: ConfigFile): v ctx.plugins.push(plugin) if (plugin.config) { - extractConfigs(ctx, { path, config: plugin.config }) + extractConfigs(ctx, { path, base, config: plugin.config }) } } @@ -150,7 +151,7 @@ function extractConfigs(ctx: ResolutionContext, { config, path }: ConfigFile): v let files = Array.isArray(content) ? content : content.files for (let file of files) { - ctx.content.files.push(typeof file === 'object' ? file : { base: path!, pattern: file }) + ctx.content.files.push(typeof file === 'object' ? file : { base, pattern: file }) } // Then apply the "user" config diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts index f0d0b1102f1d..7d732ca1ccf1 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -15,37 +15,40 @@ describe('theme', async () => { ` let compiler = await compile(input, { - loadPlugin: async () => { - return plugin( - function ({ addBase, theme }) { - addBase({ - '@keyframes enter': theme('keyframes.enter'), - '@keyframes exit': theme('keyframes.exit'), - }) - }, - { - theme: { - extend: { - keyframes: { - enter: { - from: { - opacity: 'var(--tw-enter-opacity, 1)', - transform: - 'translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0) scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))', + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ addBase, theme }) { + addBase({ + '@keyframes enter': theme('keyframes.enter'), + '@keyframes exit': theme('keyframes.exit'), + }) + }, + { + theme: { + extend: { + keyframes: { + enter: { + from: { + opacity: 'var(--tw-enter-opacity, 1)', + transform: + 'translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0) scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))', + }, }, - }, - exit: { - to: { - opacity: 'var(--tw-exit-opacity, 1)', - transform: - 'translate3d(var(--tw-exit-translate-x, 0), var(--tw-exit-translate-y, 0), 0) scale3d(var(--tw-exit-scale, 1), var(--tw-exit-scale, 1), var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))', + exit: { + to: { + opacity: 'var(--tw-exit-opacity, 1)', + transform: + 'translate3d(var(--tw-exit-translate-x, 0), var(--tw-exit-translate-y, 0), 0) scale3d(var(--tw-exit-scale, 1), var(--tw-exit-scale, 1), var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))', + }, }, }, }, }, }, - }, - ) + ), + } }, }) @@ -78,28 +81,31 @@ describe('theme', async () => { ` let compiler = await compile(input, { - loadPlugin: async () => { - return plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { - scrollbar: (value) => ({ 'scrollbar-color': value }), - }, - { - values: theme('colors'), - }, - ) - }, - { - theme: { - extend: { - colors: { - 'russet-700': '#7a4724', + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-color': value }), + }, + { + values: theme('colors'), + }, + ) + }, + { + theme: { + extend: { + colors: { + 'russet-700': '#7a4724', + }, }, }, }, - }, - ) + ), + } }, }) @@ -123,30 +129,33 @@ describe('theme', async () => { ` let compiler = await compile(input, { - loadPlugin: async () => { - return plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { - 'animate-duration': (value) => ({ 'animation-duration': value }), - }, - { - values: theme('animationDuration'), - }, - ) - }, - { - theme: { - extend: { - animationDuration: ({ theme }: { theme: (path: string) => any }) => { - return { - ...theme('transitionDuration'), - } + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + 'animate-duration': (value) => ({ 'animation-duration': value }), + }, + { + values: theme('animationDuration'), + }, + ) + }, + { + theme: { + extend: { + animationDuration: ({ theme }: { theme: (path: string) => any }) => { + return { + ...theme('transitionDuration'), + } + }, }, }, }, - }, - ) + ), + } }, }) @@ -167,32 +176,35 @@ describe('theme', async () => { ` let compiler = await compile(input, { - loadPlugin: async () => { - return plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { - 'animate-duration': (value) => ({ 'animation-duration': value }), - }, - { - values: theme('animationDuration'), - }, - ) - }, - { - theme: { - extend: { - transitionDuration: { - slow: '800ms', + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + 'animate-duration': (value) => ({ 'animation-duration': value }), + }, + { + values: theme('animationDuration'), }, + ) + }, + { + theme: { + extend: { + transitionDuration: { + slow: '800ms', + }, - animationDuration: ({ theme }: { theme: (path: string) => any }) => ({ - ...theme('transitionDuration'), - }), + animationDuration: ({ theme }: { theme: (path: string) => any }) => ({ + ...theme('transitionDuration'), + }), + }, }, }, - }, - ) + ), + } }, }) @@ -218,20 +230,23 @@ describe('theme', async () => { ` let compiler = await compile(input, { - loadPlugin: async () => { - return plugin(function ({ addUtilities, theme }) { - addUtilities({ - '.percentage': { - color: theme('colors.red.500 / 50%'), - }, - '.fraction': { - color: theme('colors.red.500 / 0.5'), - }, - '.variable': { - color: theme('colors.red.500 / var(--opacity)'), - }, - }) - }) + loadModule: async (id, base) => { + return { + base, + module: plugin(function ({ addUtilities, theme }) { + addUtilities({ + '.percentage': { + color: theme('colors.red.500 / 50%'), + }, + '.fraction': { + color: theme('colors.red.500 / 0.5'), + }, + '.variable': { + color: theme('colors.red.500 / var(--opacity)'), + }, + }) + }), + } }, }) @@ -258,36 +273,39 @@ describe('theme', async () => { ` let compiler = await compile(input, { - loadPlugin: async () => { - return plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { - 'animate-delay': (value) => ({ 'animation-delay': value }), - }, - { - values: theme('animationDelay'), - }, - ) - }, - { - theme: { - extend: { - animationDuration: ({ theme }: { theme: (path: string) => any }) => ({ - ...theme('transitionDuration'), - }), + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + 'animate-delay': (value) => ({ 'animation-delay': value }), + }, + { + values: theme('animationDelay'), + }, + ) + }, + { + theme: { + extend: { + animationDuration: ({ theme }: { theme: (path: string) => any }) => ({ + ...theme('transitionDuration'), + }), - animationDelay: ({ theme }: { theme: (path: string) => any }) => ({ - ...theme('animationDuration'), - }), + animationDelay: ({ theme }: { theme: (path: string) => any }) => ({ + ...theme('animationDuration'), + }), - transitionDuration: { - slow: '800ms', + transitionDuration: { + slow: '800ms', + }, }, }, }, - }, - ) + ), + } }, }) @@ -309,28 +327,31 @@ describe('theme', async () => { ` let compiler = await compile(input, { - loadPlugin: async () => { - return plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { - 'animate-duration': (value) => ({ 'animation-delay': value }), - }, - { - values: theme('transitionDuration'), - }, - ) - }, - { - theme: { - extend: { - transitionDuration: { - DEFAULT: '1500ms', + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + 'animate-duration': (value) => ({ 'animation-delay': value }), + }, + { + values: theme('transitionDuration'), + }, + ) + }, + { + theme: { + extend: { + transitionDuration: { + DEFAULT: '1500ms', + }, }, }, }, - }, - ) + ), + } }, }) @@ -353,29 +374,32 @@ describe('theme', async () => { ` let compiler = await compile(input, { - loadPlugin: async () => { - return plugin(function ({ matchUtilities, theme }) { - matchUtilities( - { - animation: (value) => ({ animation: value }), - }, - { - values: theme('animation'), - }, - ) + loadModule: async (id, base) => { + return { + base, + module: plugin(function ({ matchUtilities, theme }) { + matchUtilities( + { + animation: (value) => ({ animation: value }), + }, + { + values: theme('animation'), + }, + ) - matchUtilities( - { - animation2: (value) => ({ animation: value }), - }, - { - values: { - DEFAULT: theme('animation.DEFAULT'), - twist: theme('animation.spin'), + matchUtilities( + { + animation2: (value) => ({ animation: value }), }, - }, - ) - }) + { + values: { + DEFAULT: theme('animation.DEFAULT'), + twist: theme('animation.spin'), + }, + }, + ) + }), + } }, }) @@ -408,28 +432,31 @@ describe('theme', async () => { ` let compiler = await compile(input, { - loadPlugin: async () => { - return plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { - animation: (value) => ({ '--animation': value }), - }, - { - values: theme('animation'), - }, - ) - }, - { - theme: { - extend: { - animation: { - bounce: 'bounce 1s linear infinite', + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + animation: (value) => ({ '--animation': value }), + }, + { + values: theme('animation'), + }, + ) + }, + { + theme: { + extend: { + animation: { + bounce: 'bounce 1s linear infinite', + }, }, }, }, - }, - ) + ), + } }, }) @@ -459,28 +486,31 @@ describe('theme', async () => { ` let compiler = await compile(input, { - loadPlugin: async () => { - return plugin( - function ({ matchUtilities, theme }) { - matchUtilities( - { - animation: (value) => ({ '--animation': value }), - }, - { - values: theme('animation'), - }, - ) - }, - { - theme: { - extend: { - animation: { - DEFAULT: 'twist 1s linear infinite', + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ matchUtilities, theme }) { + matchUtilities( + { + animation: (value) => ({ '--animation': value }), + }, + { + values: theme('animation'), + }, + ) + }, + { + theme: { + extend: { + animation: { + DEFAULT: 'twist 1s linear infinite', + }, }, }, }, - }, - ) + ), + } }, }) @@ -505,21 +535,24 @@ describe('theme', async () => { let fn = vi.fn() await compile(input, { - loadPlugin: async () => { - return plugin( - function ({ theme }) { - fn(theme('animation.simple')) - }, - { - theme: { - extend: { - animation: { - simple: 'simple 1s linear', + loadModule: async (id, base) => { + return { + base, + module: plugin( + function ({ theme }) { + fn(theme('animation.simple')) + }, + { + theme: { + extend: { + animation: { + simple: 'simple 1s linear', + }, }, }, }, - }, - ) + ), + } }, }) @@ -537,58 +570,61 @@ describe('theme', async () => { ` let { build } = await compile(input, { - loadPlugin: async () => { - return plugin(function ({ matchUtilities, theme }) { - function utility(name: string, themeKey: string) { - matchUtilities( - { [name]: (value) => ({ '--value': value }) }, - { values: theme(themeKey) }, - ) - } + loadModule: async (id, base) => { + return { + base, + module: plugin(function ({ matchUtilities, theme }) { + function utility(name: string, themeKey: string) { + matchUtilities( + { [name]: (value) => ({ '--value': value }) }, + { values: theme(themeKey) }, + ) + } - utility('my-aspect', 'aspectRatio') - utility('my-backdrop-brightness', 'backdropBrightness') - utility('my-backdrop-contrast', 'backdropContrast') - utility('my-backdrop-grayscale', 'backdropGrayscale') - utility('my-backdrop-hue-rotate', 'backdropHueRotate') - utility('my-backdrop-invert', 'backdropInvert') - utility('my-backdrop-opacity', 'backdropOpacity') - utility('my-backdrop-saturate', 'backdropSaturate') - utility('my-backdrop-sepia', 'backdropSepia') - utility('my-border-width', 'borderWidth') - utility('my-brightness', 'brightness') - utility('my-columns', 'columns') - utility('my-contrast', 'contrast') - utility('my-divide-width', 'divideWidth') - utility('my-flex-grow', 'flexGrow') - utility('my-flex-shrink', 'flexShrink') - utility('my-gradient-color-stop-positions', 'gradientColorStopPositions') - utility('my-grayscale', 'grayscale') - utility('my-grid-row-end', 'gridRowEnd') - utility('my-grid-row-start', 'gridRowStart') - utility('my-grid-template-columns', 'gridTemplateColumns') - utility('my-grid-template-rows', 'gridTemplateRows') - utility('my-hue-rotate', 'hueRotate') - utility('my-invert', 'invert') - utility('my-line-clamp', 'lineClamp') - utility('my-opacity', 'opacity') - utility('my-order', 'order') - utility('my-outline-offset', 'outlineOffset') - utility('my-outline-width', 'outlineWidth') - utility('my-ring-offset-width', 'ringOffsetWidth') - utility('my-ring-width', 'ringWidth') - utility('my-rotate', 'rotate') - utility('my-saturate', 'saturate') - utility('my-scale', 'scale') - utility('my-sepia', 'sepia') - utility('my-skew', 'skew') - utility('my-stroke-width', 'strokeWidth') - utility('my-text-decoration-thickness', 'textDecorationThickness') - utility('my-text-underline-offset', 'textUnderlineOffset') - utility('my-transition-delay', 'transitionDelay') - utility('my-transition-duration', 'transitionDuration') - utility('my-z-index', 'zIndex') - }) + utility('my-aspect', 'aspectRatio') + utility('my-backdrop-brightness', 'backdropBrightness') + utility('my-backdrop-contrast', 'backdropContrast') + utility('my-backdrop-grayscale', 'backdropGrayscale') + utility('my-backdrop-hue-rotate', 'backdropHueRotate') + utility('my-backdrop-invert', 'backdropInvert') + utility('my-backdrop-opacity', 'backdropOpacity') + utility('my-backdrop-saturate', 'backdropSaturate') + utility('my-backdrop-sepia', 'backdropSepia') + utility('my-border-width', 'borderWidth') + utility('my-brightness', 'brightness') + utility('my-columns', 'columns') + utility('my-contrast', 'contrast') + utility('my-divide-width', 'divideWidth') + utility('my-flex-grow', 'flexGrow') + utility('my-flex-shrink', 'flexShrink') + utility('my-gradient-color-stop-positions', 'gradientColorStopPositions') + utility('my-grayscale', 'grayscale') + utility('my-grid-row-end', 'gridRowEnd') + utility('my-grid-row-start', 'gridRowStart') + utility('my-grid-template-columns', 'gridTemplateColumns') + utility('my-grid-template-rows', 'gridTemplateRows') + utility('my-hue-rotate', 'hueRotate') + utility('my-invert', 'invert') + utility('my-line-clamp', 'lineClamp') + utility('my-opacity', 'opacity') + utility('my-order', 'order') + utility('my-outline-offset', 'outlineOffset') + utility('my-outline-width', 'outlineWidth') + utility('my-ring-offset-width', 'ringOffsetWidth') + utility('my-ring-width', 'ringWidth') + utility('my-rotate', 'rotate') + utility('my-saturate', 'saturate') + utility('my-scale', 'scale') + utility('my-sepia', 'sepia') + utility('my-skew', 'skew') + utility('my-stroke-width', 'strokeWidth') + utility('my-text-decoration-thickness', 'textDecorationThickness') + utility('my-text-underline-offset', 'textUnderlineOffset') + utility('my-transition-delay', 'transitionDelay') + utility('my-transition-duration', 'transitionDuration') + utility('my-z-index', 'zIndex') + }), + } }, }) @@ -781,23 +817,26 @@ describe('theme', async () => { let fn = vi.fn() await compile(input, { - loadPlugin: async () => { - return plugin( - ({ theme }) => { - // The compatibility config specifies that `accentColor` spreads in `colors` - fn(theme('accentColor.primary')) - - // This should even work for theme keys specified in plugin configs - fn(theme('myAccentColor.secondary')) - }, - { - theme: { - extend: { - myAccentColor: ({ theme }) => theme('accentColor'), + loadModule: async (id, base) => { + return { + base, + module: plugin( + ({ theme }) => { + // The compatibility config specifies that `accentColor` spreads in `colors` + fn(theme('accentColor.primary')) + + // This should even work for theme keys specified in plugin configs + fn(theme('myAccentColor.secondary')) + }, + { + theme: { + extend: { + myAccentColor: ({ theme }) => theme('accentColor'), + }, }, }, - }, - ) + ), + } }, }) @@ -820,12 +859,15 @@ describe('theme', async () => { let fn = vi.fn() await compile(input, { - loadPlugin: async () => { - return plugin(({ theme }) => { - fn(theme('transitionTimingFunction.DEFAULT')) - fn(theme('transitionTimingFunction.in')) - fn(theme('transitionTimingFunction.out')) - }) + loadModule: async (id, base) => { + return { + base, + module: plugin(({ theme }) => { + fn(theme('transitionTimingFunction.DEFAULT')) + fn(theme('transitionTimingFunction.in')) + fn(theme('transitionTimingFunction.out')) + }), + } }, }) @@ -848,12 +890,15 @@ describe('theme', async () => { let fn = vi.fn() await compile(input, { - loadPlugin: async () => { - return plugin(({ theme }) => { - fn(theme('color.red.100')) - fn(theme('colors.red.200')) - fn(theme('backgroundColor.red.300')) - }) + loadModule: async (id, base) => { + return { + base, + module: plugin(({ theme }) => { + fn(theme('color.red.100')) + fn(theme('colors.red.200')) + fn(theme('backgroundColor.red.300')) + }), + } }, }) @@ -873,13 +918,16 @@ describe('theme', async () => { let fn = vi.fn() await compile(input, { - loadPlugin: async () => { - return plugin(({ theme }) => { - fn(theme('i.do.not.exist')) - fn(theme('color')) - fn(theme('color', 'magenta')) - fn(theme('colors')) - }) + loadModule: async (id, base) => { + return { + base, + module: plugin(({ theme }) => { + fn(theme('i.do.not.exist')) + fn(theme('color')) + fn(theme('color', 'magenta')) + fn(theme('colors')) + }), + } }, }) @@ -896,34 +944,37 @@ describe('theme', async () => { ` let { build } = await compile(input, { - loadPlugin: async () => { - return plugin(({ addUtilities, matchUtilities }) => { - addUtilities({ - '.foo-bar': { - color: 'red', - }, - }) + loadModule: async (id, base) => { + return { + base, + module: plugin(({ addUtilities, matchUtilities }) => { + addUtilities({ + '.foo-bar': { + color: 'red', + }, + }) - matchUtilities( - { - foo: (value) => ({ - '--my-prop': value, - }), - }, - { - values: { - bar: 'bar-valuer', - baz: 'bar-valuer', + matchUtilities( + { + foo: (value) => ({ + '--my-prop': value, + }), }, - }, - ) + { + values: { + bar: 'bar-valuer', + baz: 'bar-valuer', + }, + }, + ) - addUtilities({ - '.foo-bar': { - backgroundColor: 'red', - }, - }) - }) + addUtilities({ + '.foo-bar': { + backgroundColor: 'red', + }, + }) + }), + } }, }) @@ -948,62 +999,65 @@ describe('theme', async () => { ` let { build } = await compile(input, { - loadPlugin: async () => { - return plugin(function ({ matchUtilities }) { - function utility(name: string, themeKey: string) { - matchUtilities( - { [name]: (value) => ({ '--value': value }) }, - // @ts-ignore - { values: defaultTheme[themeKey] }, - ) - } + loadModule: async (id, base) => { + return { + base, + module: plugin(function ({ matchUtilities }) { + function utility(name: string, themeKey: string) { + matchUtilities( + { [name]: (value) => ({ '--value': value }) }, + // @ts-ignore + { values: defaultTheme[themeKey] }, + ) + } - utility('my-aspect', 'aspectRatio') - // The following keys deliberately doesn't work as these are exported - // as functions from the compat config. - // - // utility('my-backdrop-brightness', 'backdropBrightness') - // utility('my-backdrop-contrast', 'backdropContrast') - // utility('my-backdrop-grayscale', 'backdropGrayscale') - // utility('my-backdrop-hue-rotate', 'backdropHueRotate') - // utility('my-backdrop-invert', 'backdropInvert') - // utility('my-backdrop-opacity', 'backdropOpacity') - // utility('my-backdrop-saturate', 'backdropSaturate') - // utility('my-backdrop-sepia', 'backdropSepia') - // utility('my-divide-width', 'divideWidth') - utility('my-border-width', 'borderWidth') - utility('my-brightness', 'brightness') - utility('my-columns', 'columns') - utility('my-contrast', 'contrast') - utility('my-flex-grow', 'flexGrow') - utility('my-flex-shrink', 'flexShrink') - utility('my-gradient-color-stop-positions', 'gradientColorStopPositions') - utility('my-grayscale', 'grayscale') - utility('my-grid-row-end', 'gridRowEnd') - utility('my-grid-row-start', 'gridRowStart') - utility('my-grid-template-columns', 'gridTemplateColumns') - utility('my-grid-template-rows', 'gridTemplateRows') - utility('my-hue-rotate', 'hueRotate') - utility('my-invert', 'invert') - utility('my-line-clamp', 'lineClamp') - utility('my-opacity', 'opacity') - utility('my-order', 'order') - utility('my-outline-offset', 'outlineOffset') - utility('my-outline-width', 'outlineWidth') - utility('my-ring-offset-width', 'ringOffsetWidth') - utility('my-ring-width', 'ringWidth') - utility('my-rotate', 'rotate') - utility('my-saturate', 'saturate') - utility('my-scale', 'scale') - utility('my-sepia', 'sepia') - utility('my-skew', 'skew') - utility('my-stroke-width', 'strokeWidth') - utility('my-text-decoration-thickness', 'textDecorationThickness') - utility('my-text-underline-offset', 'textUnderlineOffset') - utility('my-transition-delay', 'transitionDelay') - utility('my-transition-duration', 'transitionDuration') - utility('my-z-index', 'zIndex') - }) + utility('my-aspect', 'aspectRatio') + // The following keys deliberately doesn't work as these are exported + // as functions from the compat config. + // + // utility('my-backdrop-brightness', 'backdropBrightness') + // utility('my-backdrop-contrast', 'backdropContrast') + // utility('my-backdrop-grayscale', 'backdropGrayscale') + // utility('my-backdrop-hue-rotate', 'backdropHueRotate') + // utility('my-backdrop-invert', 'backdropInvert') + // utility('my-backdrop-opacity', 'backdropOpacity') + // utility('my-backdrop-saturate', 'backdropSaturate') + // utility('my-backdrop-sepia', 'backdropSepia') + // utility('my-divide-width', 'divideWidth') + utility('my-border-width', 'borderWidth') + utility('my-brightness', 'brightness') + utility('my-columns', 'columns') + utility('my-contrast', 'contrast') + utility('my-flex-grow', 'flexGrow') + utility('my-flex-shrink', 'flexShrink') + utility('my-gradient-color-stop-positions', 'gradientColorStopPositions') + utility('my-grayscale', 'grayscale') + utility('my-grid-row-end', 'gridRowEnd') + utility('my-grid-row-start', 'gridRowStart') + utility('my-grid-template-columns', 'gridTemplateColumns') + utility('my-grid-template-rows', 'gridTemplateRows') + utility('my-hue-rotate', 'hueRotate') + utility('my-invert', 'invert') + utility('my-line-clamp', 'lineClamp') + utility('my-opacity', 'opacity') + utility('my-order', 'order') + utility('my-outline-offset', 'outlineOffset') + utility('my-outline-width', 'outlineWidth') + utility('my-ring-offset-width', 'ringOffsetWidth') + utility('my-ring-width', 'ringWidth') + utility('my-rotate', 'rotate') + utility('my-saturate', 'saturate') + utility('my-scale', 'scale') + utility('my-sepia', 'sepia') + utility('my-skew', 'skew') + utility('my-stroke-width', 'strokeWidth') + utility('my-text-decoration-thickness', 'textDecorationThickness') + utility('my-text-underline-offset', 'textUnderlineOffset') + utility('my-transition-delay', 'transitionDelay') + utility('my-transition-duration', 'transitionDuration') + utility('my-z-index', 'zIndex') + }), + } }, }) @@ -1167,9 +1221,12 @@ describe('addVariant', () => { } `, { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { - addVariant('hocus', '&:hover, &:focus') + loadModule: async (id, base) => { + return { + base, + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', '&:hover, &:focus') + }, } }, }, @@ -1198,9 +1255,12 @@ describe('addVariant', () => { } `, { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { - addVariant('hocus', ['&:hover', '&:focus']) + loadModule: async (id, base) => { + return { + base, + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', ['&:hover', '&:focus']) + }, } }, }, @@ -1230,15 +1290,18 @@ describe('addVariant', () => { } `, { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { - addVariant('hocus', { - '&:hover': '@slot', - '&:focus': '@slot', - }) - } - }, - }, + loadModule: async (id, base) => { + return { + base, + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', { + '&:hover': '@slot', + '&:focus': '@slot', + }) + }, + } + }, + }, ) let compiled = build(['hocus:underline', 'group-hocus:flex']) @@ -1264,14 +1327,17 @@ describe('addVariant', () => { } `, { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { - addVariant('hocus', { - '@media (hover: hover)': { - '&:hover': '@slot', - }, - '&:focus': '@slot', - }) + loadModule: async (id, base) => { + return { + base, + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', { + '@media (hover: hover)': { + '&:hover': '@slot', + }, + '&:focus': '@slot', + }) + }, } }, }, @@ -1312,12 +1378,15 @@ describe('addVariant', () => { } `, { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { - addVariant( - 'potato', - '@media (max-width: 400px) { @supports (font:bold) { &:large-potato } }', - ) + loadModule: async (id, base) => { + return { + base, + module: ({ addVariant }: PluginAPI) => { + addVariant( + 'potato', + '@media (max-width: 400px) { @supports (font:bold) { &:large-potato } }', + ) + }, } }, }, @@ -1354,15 +1423,18 @@ describe('addVariant', () => { } `, { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { - addVariant('hocus', { - '&': { - '--custom-property': '@slot', - '&:hover': '@slot', - '&:focus': '@slot', - }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ addVariant }: PluginAPI) => { + addVariant('hocus', { + '&': { + '--custom-property': '@slot', + '&:hover': '@slot', + '&:focus': '@slot', + }, + }) + }, } }, }, @@ -1393,9 +1465,12 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('potato', (flavor) => `.potato-${flavor} &`) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('potato', (flavor) => `.potato-${flavor} &`) + }, } }, }, @@ -1424,9 +1499,12 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('potato', (flavor) => `@media (potato: ${flavor})`) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('potato', (flavor) => `@media (potato: ${flavor})`) + }, } }, }, @@ -1459,12 +1537,16 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant( - 'potato', - (flavor) => `@media (potato: ${flavor}) { @supports (font:bold) { &:large-potato } }`, - ) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant( + 'potato', + (flavor) => + `@media (potato: ${flavor}) { @supports (font:bold) { &:large-potato } }`, + ) + }, } }, }, @@ -1501,14 +1583,17 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('tooltip', (side) => `&${side}`, { - values: { - bottom: '[data-location="bottom"]', - top: '[data-location="top"]', - }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('tooltip', (side) => `&${side}`, { + values: { + bottom: '[data-location="bottom"]', + top: '[data-location="top"]', + }, + }) + }, } }, }, @@ -1537,16 +1622,19 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('alphabet', (side) => `&${side}`, { - values: { - d: '[data-order="1"]', - a: '[data-order="2"]', - c: '[data-order="3"]', - b: '[data-order="4"]', - }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('alphabet', (side) => `&${side}`, { + values: { + d: '[data-order="1"]', + a: '[data-order="2"]', + c: '[data-order="3"]', + b: '[data-order="4"]', + }, + }) + }, } }, }, @@ -1588,11 +1676,14 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('test', (selector) => - selector.split(',').map((selector) => `&.${selector} > *`), - ) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('test', (selector) => + selector.split(',').map((selector) => `&.${selector} > *`), + ) + }, } }, }, @@ -1617,13 +1708,16 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) + }, } }, }, @@ -1666,16 +1760,19 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - values: { - example: '600px', - }, - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + values: { + example: '600px', + }, + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) + }, } }, }, @@ -1718,19 +1815,22 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) - matchVariant('testmax', (value) => `@media (max-width: ${value})`, { - sort(a, z) { - return parseInt(z.value) - parseInt(a.value) - }, - }) + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { + sort(a, z) { + return parseInt(z.value) - parseInt(a.value) + }, + }) + }, } }, }, @@ -1789,19 +1889,22 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) - matchVariant('testmax', (value) => `@media (max-width: ${value})`, { - sort(a, z) { - return parseInt(z.value) - parseInt(a.value) - }, - }) + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { + sort(a, z) { + return parseInt(z.value) - parseInt(a.value) + }, + }) + }, } }, }, @@ -1842,18 +1945,21 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) - matchVariant('testmax', (value) => `@media (max-width: ${value})`, { - sort(a, z) { - return parseInt(z.value) - parseInt(a.value) - }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { + sort(a, z) { + return parseInt(z.value) - parseInt(a.value) + }, + }) + }, } }, }, @@ -1911,18 +2017,21 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - return parseInt(a.value) - parseInt(z.value) - }, - }) - matchVariant('testmax', (value) => `@media (max-width: ${value})`, { - sort(a, z) { - return parseInt(z.value) - parseInt(a.value) - }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + return parseInt(a.value) - parseInt(z.value) + }, + }) + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { + sort(a, z) { + return parseInt(z.value) - parseInt(a.value) + }, + }) + }, } }, }, @@ -1980,26 +2089,29 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('testmin', (value) => `@media (min-width: ${value})`, { - sort(a, z) { - let lookup = ['100px', '200px'] - if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) { - throw new Error('We are seeing values that should not be there!') - } - return lookup.indexOf(a.value) - lookup.indexOf(z.value) - }, - }) - matchVariant('testmax', (value) => `@media (max-width: ${value})`, { - sort(a, z) { - let lookup = ['300px', '400px'] - if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) { - throw new Error('We are seeing values that should not be there!') - } - return lookup.indexOf(z.value) - lookup.indexOf(a.value) - }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('testmin', (value) => `@media (min-width: ${value})`, { + sort(a, z) { + let lookup = ['100px', '200px'] + if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) { + throw new Error('We are seeing values that should not be there!') + } + return lookup.indexOf(a.value) - lookup.indexOf(z.value) + }, + }) + matchVariant('testmax', (value) => `@media (max-width: ${value})`, { + sort(a, z) { + let lookup = ['300px', '400px'] + if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) { + throw new Error('We are seeing values that should not be there!') + } + return lookup.indexOf(z.value) - lookup.indexOf(a.value) + }, + }) + }, } }, }, @@ -2057,13 +2169,16 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('foo', (value) => `.foo${value} &`, { - values: { - DEFAULT: '.bar', - }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('foo', (value) => `.foo${value} &`, { + values: { + DEFAULT: '.bar', + }, + }) + }, } }, }, @@ -2088,9 +2203,12 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('foo', (value) => `.foo${value} &`) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('foo', (value) => `.foo${value} &`) + }, } }, }, @@ -2109,11 +2227,14 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('foo', (value) => `.foo${value === null ? '-good' : '-bad'} &`, { - values: { DEFAULT: null }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('foo', (value) => `.foo${value === null ? '-good' : '-bad'} &`, { + values: { DEFAULT: null }, + }) + }, } }, }, @@ -2138,11 +2259,14 @@ describe('matchVariant', () => { } `, { - loadPlugin: async () => { - return ({ matchVariant }: PluginAPI) => { - matchVariant('foo', (value) => `.foo${value === undefined ? '-good' : '-bad'} &`, { - values: { DEFAULT: undefined }, - }) + loadModule: async (id, base) => { + return { + base, + module: ({ matchVariant }: PluginAPI) => { + matchVariant('foo', (value) => `.foo${value === undefined ? '-good' : '-bad'} &`, { + values: { DEFAULT: undefined }, + }) + }, } }, }, @@ -2173,14 +2297,17 @@ describe('addUtilities()', () => { } `, { - async loadPlugin() { - return ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.text-trim': { - 'text-box-trim': 'both', - 'text-box-edge': 'cap alphabetic', - }, - }) + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.text-trim': { + 'text-box-trim': 'both', + 'text-box-edge': 'cap alphabetic', + }, + }) + }, } }, }, @@ -2211,13 +2338,19 @@ describe('addUtilities()', () => { @tailwind utilities; `, { - async loadPlugin() { - return ({ addUtilities }: PluginAPI) => { - addUtilities([ - { - '.text-trim': [{ 'text-box-trim': 'both' }, { 'text-box-edge': 'cap alphabetic' }], - }, - ]) + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities([ + { + '.text-trim': [ + { 'text-box-trim': 'both' }, + { 'text-box-edge': 'cap alphabetic' }, + ], + }, + ]) + }, } }, }, @@ -2238,22 +2371,25 @@ describe('addUtilities()', () => { @tailwind utilities; `, { - async loadPlugin() { - return ({ addUtilities }: PluginAPI) => { - addUtilities([ - { - '.text-trim': { - 'text-box-trim': 'both', - 'text-box-edge': 'cap alphabetic', + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities([ + { + '.text-trim': { + 'text-box-trim': 'both', + 'text-box-edge': 'cap alphabetic', + }, }, - }, - { - '.text-trim-2': { - 'text-box-trim': 'both', - 'text-box-edge': 'cap alphabetic', + { + '.text-trim-2': { + 'text-box-trim': 'both', + 'text-box-edge': 'cap alphabetic', + }, }, - }, - ]) + ]) + }, } }, }, @@ -2274,15 +2410,18 @@ describe('addUtilities()', () => { @tailwind utilities; `, { - async loadPlugin() { - return ({ addUtilities }: PluginAPI) => { - addUtilities([ - { - '.outlined': { - outline: ['1px solid ButtonText', '1px auto -webkit-focus-ring-color'], + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities([ + { + '.outlined': { + outline: ['1px solid ButtonText', '1px auto -webkit-focus-ring-color'], + }, }, - }, - ]) + ]) + }, } }, }, @@ -2305,15 +2444,18 @@ describe('addUtilities()', () => { } `, { - async loadPlugin() { - return ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.text-trim': { - WebkitAppearance: 'none', - textBoxTrim: 'both', - textBoxEdge: 'cap alphabetic', - }, - }) + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.text-trim': { + WebkitAppearance: 'none', + textBoxTrim: 'both', + textBoxEdge: 'cap alphabetic', + }, + }) + }, } }, }, @@ -2343,13 +2485,16 @@ describe('addUtilities()', () => { } `, { - async loadPlugin() { - return ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.foo': { - '@apply flex dark:underline': {}, - }, - }) + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.foo': { + '@apply flex dark:underline': {}, + }, + }) + }, } }, }, @@ -2396,14 +2541,17 @@ describe('addUtilities()', () => { } `, { - async loadPlugin() { - return ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.text-trim > *': { - 'text-box-trim': 'both', - 'text-box-edge': 'cap alphabetic', - }, - }) + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.text-trim > *': { + 'text-box-trim': 'both', + 'text-box-edge': 'cap alphabetic', + }, + }) + }, } }, }, @@ -2422,14 +2570,17 @@ describe('addUtilities()', () => { } `, { - async loadPlugin() { - return ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.form-input, .form-textarea': { - appearance: 'none', - 'background-color': '#fff', - }, - }) + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.form-input, .form-textarea': { + appearance: 'none', + 'background-color': '#fff', + }, + }) + }, } }, }, @@ -2462,13 +2613,16 @@ describe('addUtilities()', () => { } `, { - async loadPlugin() { - return ({ addUtilities }: PluginAPI) => { - addUtilities({ - '.form-input, .form-input::placeholder, .form-textarea:hover:focus': { - 'background-color': 'red', - }, - }) + async loadModule(id, base) { + return { + base, + module: ({ addUtilities }: PluginAPI) => { + addUtilities({ + '.form-input, .form-input::placeholder, .form-textarea:hover:focus': { + 'background-color': 'red', + }, + }) + }, } }, }, @@ -2506,20 +2660,24 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, + { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - 'border-block': (value) => ({ 'border-block-width': value }), - }, - { - values: { - DEFAULT: '1px', - '2': '2px', + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + 'border-block': (value) => ({ 'border-block-width': value }), }, - }, - ) + { + values: { + DEFAULT: '1px', + '2': '2px', + }, + }, + ) + }, } }, }, @@ -2582,23 +2740,26 @@ describe('matchUtilities()', () => { @tailwind utilities; `, { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - 'all-but-order-bottom-left-radius': (value) => - [ - { 'border-top-left-radius': value }, - { 'border-top-right-radius': value }, - { 'border-bottom-right-radius': value }, - ] as CssInJs[], - }, - { - values: { - DEFAULT: '1px', + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + 'all-but-order-bottom-left-radius': (value) => + [ + { 'border-top-left-radius': value }, + { 'border-top-right-radius': value }, + { 'border-bottom-right-radius': value }, + ] as CssInJs[], }, - }, - ) + { + values: { + DEFAULT: '1px', + }, + }, + ) + }, } }, }, @@ -2626,25 +2787,29 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, + { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - 'border-block': (value, { modifier }) => ({ - '--my-modifier': modifier ?? 'none', - 'border-block-width': value, - }), - }, - { - values: { - DEFAULT: '1px', - '2': '2px', + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + 'border-block': (value, { modifier }) => ({ + '--my-modifier': modifier ?? 'none', + 'border-block-width': value, + }), }, + { + values: { + DEFAULT: '1px', + '2': '2px', + }, - modifiers: 'any', - }, - ) + modifiers: 'any', + }, + ) + }, } }, }, @@ -2692,27 +2857,31 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, + { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - 'border-block': (value, { modifier }) => ({ - '--my-modifier': modifier ?? 'none', - 'border-block-width': value, - }), - }, - { - values: { - DEFAULT: '1px', - '2': '2px', + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + 'border-block': (value, { modifier }) => ({ + '--my-modifier': modifier ?? 'none', + 'border-block-width': value, + }), }, + { + values: { + DEFAULT: '1px', + '2': '2px', + }, - modifiers: { - foo: 'foo', + modifiers: { + foo: 'foo', + }, }, - }, - ) + ) + }, } }, }, @@ -2762,22 +2931,26 @@ describe('matchUtilities()', () => { @tailwind utilities; @plugin "my-plugin"; `, + { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - scrollbar: (value) => ({ 'scrollbar-color': value }), - }, - { type: ['color', 'any'] }, - ) + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-color': value }), + }, + { type: ['color', 'any'] }, + ) - matchUtilities( - { - scrollbar: (value) => ({ 'scrollbar-width': value }), - }, - { type: ['length'] }, - ) + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-width': value }), + }, + { type: ['length'] }, + ) + }, } }, }, @@ -2813,22 +2986,26 @@ describe('matchUtilities()', () => { @tailwind utilities; @plugin "my-plugin"; `, + { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - scrollbar: (value) => ({ '--scrollbar-angle': value }), - }, - { type: ['angle', 'any'] }, - ) + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + scrollbar: (value) => ({ '--scrollbar-angle': value }), + }, + { type: ['angle', 'any'] }, + ) - matchUtilities( - { - scrollbar: (value) => ({ '--scrollbar-width': value }), - }, - { type: ['length'] }, - ) + matchUtilities( + { + scrollbar: (value) => ({ '--scrollbar-width': value }), + }, + { type: ['length'] }, + ) + }, } }, }, @@ -2847,22 +3024,26 @@ describe('matchUtilities()', () => { @tailwind utilities; @plugin "my-plugin"; `, + { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - scrollbar: (value) => ({ 'scrollbar-color': value }), - }, - { type: ['color', 'any'], modifiers: { foo: 'foo' } }, - ) + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-color': value }), + }, + { type: ['color', 'any'], modifiers: { foo: 'foo' } }, + ) - matchUtilities( - { - scrollbar: (value) => ({ 'scrollbar-width': value }), - }, - { type: ['length'], modifiers: { bar: 'bar' } }, - ) + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-width': value }), + }, + { type: ['length'], modifiers: { bar: 'bar' } }, + ) + }, } }, }, @@ -2887,32 +3068,36 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, + { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - scrollbar: (value) => ({ 'scrollbar-color': value }), - }, - { - type: ['color', 'any'], - values: { - black: 'black', + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-color': value }), }, - }, - ) + { + type: ['color', 'any'], + values: { + black: 'black', + }, + }, + ) - matchUtilities( - { - scrollbar: (value) => ({ 'scrollbar-width': value }), - }, - { - type: ['length'], - values: { - 2: '2px', + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-width': value }), }, - }, - ) + { + type: ['length'], + values: { + 2: '2px', + }, + }, + ) + }, } }, }, @@ -3006,20 +3191,24 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, + { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - scrollbar: (value) => ({ 'scrollbar-color': value }), - }, - { - type: ['color', 'any'], - values: { - black: 'black', + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + scrollbar: (value) => ({ 'scrollbar-color': value }), }, - }, - ) + { + type: ['color', 'any'], + values: { + black: 'black', + }, + }, + ) + }, } }, }, @@ -3079,24 +3268,28 @@ describe('matchUtilities()', () => { --opacity-my-opacity: 0.5; } `, + { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - scrollbar: (value, { modifier }) => ({ - '--modifier': modifier ?? 'none', - 'scrollbar-width': value, - }), - }, - { - type: ['any'], - values: {}, - modifiers: { - foo: 'foo', + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + scrollbar: (value, { modifier }) => ({ + '--modifier': modifier ?? 'none', + 'scrollbar-width': value, + }), }, - }, - ) + { + type: ['any'], + values: {}, + modifiers: { + foo: 'foo', + }, + }, + ) + }, } }, }, @@ -3135,21 +3328,24 @@ describe('matchUtilities()', () => { } `, { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities( - { - foo: (value) => ({ - '--foo': value, - [`@apply flex`]: {}, - }), - }, - { - values: { - bar: 'bar', + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities( + { + foo: (value) => ({ + '--foo': value, + [`@apply flex`]: {}, + }), }, - }, - ) + { + values: { + bar: 'bar', + }, + }, + ) + }, } }, }, @@ -3199,15 +3395,19 @@ describe('matchUtilities()', () => { --breakpoint-lg: 1024px; } `, + { - async loadPlugin() { - return ({ matchUtilities }: PluginAPI) => { - matchUtilities({ - '.text-trim > *': () => ({ - 'text-box-trim': 'both', - 'text-box-edge': 'cap alphabetic', - }), - }) + async loadModule(id, base) { + return { + base, + module: ({ matchUtilities }: PluginAPI) => { + matchUtilities({ + '.text-trim > *': () => ({ + 'text-box-trim': 'both', + 'text-box-edge': 'cap alphabetic', + }), + }) + }, } }, }, @@ -3224,29 +3424,32 @@ describe('addComponents()', () => { @tailwind utilities; `, { - async loadPlugin() { - return ({ addComponents }: PluginAPI) => { - addComponents({ - '.btn': { - padding: '.5rem 1rem', - borderRadius: '.25rem', - fontWeight: '600', - }, - '.btn-blue': { - backgroundColor: '#3490dc', - color: '#fff', - '&:hover': { - backgroundColor: '#2779bd', + async loadModule(id, base) { + return { + base, + module: ({ addComponents }: PluginAPI) => { + addComponents({ + '.btn': { + padding: '.5rem 1rem', + borderRadius: '.25rem', + fontWeight: '600', }, - }, - '.btn-red': { - backgroundColor: '#e3342f', - color: '#fff', - '&:hover': { - backgroundColor: '#cc1f1a', + '.btn-blue': { + backgroundColor: '#3490dc', + color: '#fff', + '&:hover': { + backgroundColor: '#2779bd', + }, }, - }, - }) + '.btn-red': { + backgroundColor: '#e3342f', + color: '#fff', + '&:hover': { + backgroundColor: '#cc1f1a', + }, + }, + }) + }, } }, }, @@ -3289,9 +3492,12 @@ describe('prefix()', () => { @plugin "my-plugin"; `, { - async loadPlugin() { - return ({ prefix }: PluginAPI) => { - fn(prefix('btn')) + async loadModule(id, base) { + return { + base, + module: ({ prefix }: PluginAPI) => { + fn(prefix('btn')) + }, } }, }, diff --git a/packages/tailwindcss/src/compat/screens-config.test.ts b/packages/tailwindcss/src/compat/screens-config.test.ts index 05f725a09b53..5f75504ad966 100644 --- a/packages/tailwindcss/src/compat/screens-config.test.ts +++ b/packages/tailwindcss/src/compat/screens-config.test.ts @@ -20,14 +20,17 @@ test('CSS `--breakpoint-*` merge with JS config `screens`', async () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - extend: { - screens: { - sm: '44rem', + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + sm: '44rem', + }, }, }, }, + base: '/root', }), }) @@ -100,17 +103,20 @@ test('JS config `screens` extend CSS `--breakpoint-*`', async () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - extend: { - screens: { - xs: '30rem', - sm: '40rem', - md: '48rem', - lg: '60rem', + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + xs: '30rem', + sm: '40rem', + md: '48rem', + lg: '60rem', + }, }, }, }, + base: '/root', }), }) @@ -195,14 +201,17 @@ test('JS config `screens` only setup, even if those match the default-theme expo ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - screens: { - sm: '40rem', - md: '48rem', - lg: '64rem', + loadModule: async () => ({ + module: { + theme: { + screens: { + sm: '40rem', + md: '48rem', + lg: '64rem', + }, }, }, + base: '/root', }), }) @@ -271,14 +280,17 @@ test('JS config `screens` overwrite CSS `--breakpoint-*`', async () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - screens: { - mini: '40rem', - midi: '48rem', - maxi: '64rem', + loadModule: async () => ({ + module: { + theme: { + screens: { + mini: '40rem', + midi: '48rem', + maxi: '64rem', + }, }, }, + base: '/root', }), }) @@ -374,16 +386,19 @@ test('JS config with `theme: { extends }` should not include the `default-config ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - extend: { - screens: { - mini: '40rem', - midi: '48rem', - maxi: '64rem', + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + mini: '40rem', + midi: '48rem', + maxi: '64rem', + }, }, }, }, + base: '/root', }), }) @@ -449,22 +464,25 @@ describe('complex screen configs', () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - extend: { - screens: { - sm: { max: '639px' }, - md: [ - // - { min: '668px', max: '767px' }, - '868px', - ], - lg: { min: '868px' }, - xl: { min: '1024px', max: '1279px' }, - tall: { raw: '(min-height: 800px)' }, + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + sm: { max: '639px' }, + md: [ + // + { min: '668px', max: '767px' }, + '868px', + ], + lg: { min: '868px' }, + xl: { min: '1024px', max: '1279px' }, + tall: { raw: '(min-height: 800px)' }, + }, }, }, }, + base: '/root', }), }) @@ -533,15 +551,18 @@ describe('complex screen configs', () => { ` let compiler = await compile(input, { - loadConfig: async () => ({ - theme: { - extend: { - screens: { - sm: '40rem', - portrait: { raw: 'screen and (orientation: portrait)' }, + loadModule: async () => ({ + module: { + theme: { + extend: { + screens: { + sm: '40rem', + portrait: { raw: 'screen and (orientation: portrait)' }, + }, }, }, }, + base: '/root', }), }) diff --git a/packages/tailwindcss/src/css-functions.test.ts b/packages/tailwindcss/src/css-functions.test.ts index b40dc9a2b3ce..521f50009852 100644 --- a/packages/tailwindcss/src/css-functions.test.ts +++ b/packages/tailwindcss/src/css-functions.test.ts @@ -336,7 +336,7 @@ describe('theme function', () => { } `, { - loadConfig: async () => ({}), + loadModule: async () => ({ module: {}, base: '/root' }), }, ) @@ -795,23 +795,26 @@ describe('in plugins', () => { } `, { - async loadPlugin() { - return plugin(({ addBase, addUtilities }) => { - addBase({ - '.my-base-rule': { - color: 'theme(colors.red)', - 'outline-color': 'theme(colors.orange / 15%)', - 'background-color': 'theme(--color-blue)', - 'border-color': 'theme(--color-pink / 10%)', - }, - }) + async loadModule() { + return { + module: plugin(({ addBase, addUtilities }) => { + addBase({ + '.my-base-rule': { + color: 'theme(colors.red)', + 'outline-color': 'theme(colors.orange / 15%)', + 'background-color': 'theme(--color-blue)', + 'border-color': 'theme(--color-pink / 10%)', + }, + }) - addUtilities({ - '.my-utility': { - color: 'theme(colors.red)', - }, - }) - }) + addUtilities({ + '.my-utility': { + color: 'theme(colors.red)', + }, + }) + }), + base: '/root', + } }, }, ) @@ -850,31 +853,34 @@ describe('in JS config files', () => { } `, { - loadConfig: async () => ({ - theme: { - extend: { - colors: { - primary: 'theme(colors.red)', - secondary: 'theme(--color-orange)', + loadModule: async () => ({ + module: { + theme: { + extend: { + colors: { + primary: 'theme(colors.red)', + secondary: 'theme(--color-orange)', + }, }, }, + plugins: [ + plugin(({ addBase, addUtilities }) => { + addBase({ + '.my-base-rule': { + background: 'theme(colors.primary)', + color: 'theme(colors.secondary)', + }, + }) + + addUtilities({ + '.my-utility': { + color: 'theme(colors.red)', + }, + }) + }), + ], }, - plugins: [ - plugin(({ addBase, addUtilities }) => { - addBase({ - '.my-base-rule': { - background: 'theme(colors.primary)', - color: 'theme(colors.secondary)', - }, - }) - - addUtilities({ - '.my-utility': { - color: 'theme(colors.red)', - }, - }) - }), - ], + base: '/root', }), }, ) diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index fb226708e689..3693bbb0e878 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -1429,17 +1429,20 @@ describe('Parsing themes values from CSS', () => { @tailwind utilities; `, { - loadPlugin: async () => { - return plugin(({}) => {}, { - theme: { - extend: { - colors: { - red: 'tomato', - orange: '#f28500', + loadModule: async () => { + return { + module: plugin(({}) => {}, { + theme: { + extend: { + colors: { + red: 'tomato', + orange: '#f28500', + }, }, }, - }, - }) + }), + base: '/root', + } }, }, ) @@ -1472,16 +1475,19 @@ describe('Parsing themes values from CSS', () => { @tailwind utilities; `, { - loadConfig: async () => { + loadModule: async () => { return { - theme: { - extend: { - colors: { - red: 'tomato', - orange: '#f28500', + module: { + theme: { + extend: { + colors: { + red: 'tomato', + orange: '#f28500', + }, }, }, }, + base: '/root', } }, }, @@ -1511,11 +1517,12 @@ describe('plugins', () => { @plugin; `, { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { + loadModule: async () => ({ + module: ({ addVariant }: PluginAPI) => { addVariant('hocus', '&:hover, &:focus') - } - }, + }, + base: '/root', + }), }, ), ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: \`@plugin\` must have a path.]`)) @@ -1527,11 +1534,12 @@ describe('plugins', () => { @plugin ''; `, { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { + loadModule: async () => ({ + module: ({ addVariant }: PluginAPI) => { addVariant('hocus', '&:hover, &:focus') - } - }, + }, + base: '/root', + }), }, ), ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: \`@plugin\` must have a path.]`)) @@ -1545,11 +1553,12 @@ describe('plugins', () => { } `, { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { + loadModule: async () => ({ + module: ({ addVariant }: PluginAPI) => { addVariant('hocus', '&:hover, &:focus') - } - }, + }, + base: '/root', + }), }, ), ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: \`@plugin\` cannot be nested.]`)) @@ -1565,8 +1574,8 @@ describe('plugins', () => { } `, { - loadPlugin: async () => { - return plugin.withOptions((options) => { + loadModule: async () => ({ + module: plugin.withOptions((options) => { expect(options).toEqual({ color: 'red', }) @@ -1578,8 +1587,9 @@ describe('plugins', () => { }, }) } - }) - }, + }), + base: '/root', + }), }, ) @@ -1616,8 +1626,8 @@ describe('plugins', () => { } `, { - loadPlugin: async () => { - return plugin.withOptions((options) => { + loadModule: async () => ({ + module: plugin.withOptions((options) => { expect(options).toEqual({ 'is-null': null, 'is-true': true, @@ -1636,8 +1646,9 @@ describe('plugins', () => { }) return () => {} - }) - }, + }), + base: '/root', + }), }, ) }) @@ -1655,8 +1666,8 @@ describe('plugins', () => { } `, { - loadPlugin: async () => { - return plugin.withOptions((options) => { + loadModule: async () => ({ + module: plugin.withOptions((options) => { return ({ addUtilities }) => { addUtilities({ '.text-primary': { @@ -1664,8 +1675,9 @@ describe('plugins', () => { }, }) } - }) - }, + }), + base: '/root', + }), }, ), ).rejects.toThrowErrorMatchingInlineSnapshot( @@ -1692,15 +1704,16 @@ describe('plugins', () => { } `, { - loadPlugin: async () => { - return plugin(({ addUtilities }) => { + loadModule: async () => ({ + module: plugin(({ addUtilities }) => { addUtilities({ '.text-primary': { color: 'red', }, }) - }) - }, + }), + base: '/root', + }), }, ), ).rejects.toThrowErrorMatchingInlineSnapshot( @@ -1717,7 +1730,7 @@ describe('plugins', () => { } `, { - loadPlugin: async () => plugin(() => {}), + loadModule: async () => ({ module: plugin(() => {}), base: '/root' }), }, ), ).rejects.toThrowErrorMatchingInlineSnapshot( @@ -1738,7 +1751,7 @@ describe('plugins', () => { } `, { - loadPlugin: async () => plugin(() => {}), + loadModule: async () => ({ module: plugin(() => {}), base: '/root' }), }, ), ).rejects.toThrowErrorMatchingInlineSnapshot( @@ -1763,11 +1776,12 @@ describe('plugins', () => { } `, { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { + loadModule: async () => ({ + module: ({ addVariant }: PluginAPI) => { addVariant('hocus', '&:hover, &:focus') - } - }, + }, + base: '/root', + }), }, ) let compiled = build(['hocus:underline', 'group-hocus:flex']) @@ -1794,11 +1808,12 @@ describe('plugins', () => { } `, { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { + loadModule: async () => ({ + module: ({ addVariant }: PluginAPI) => { addVariant('hocus', ['&:hover', '&:focus']) - } - }, + }, + base: '/root', + }), }, ) @@ -1826,14 +1841,15 @@ describe('plugins', () => { } `, { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { + loadModule: async () => ({ + module: ({ addVariant }: PluginAPI) => { addVariant('hocus', { '&:hover': '@slot', '&:focus': '@slot', }) - } - }, + }, + base: '/root', + }), }, ) let compiled = build(['hocus:underline', 'group-hocus:flex']) @@ -1860,16 +1876,17 @@ describe('plugins', () => { } `, { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { + loadModule: async () => ({ + module: ({ addVariant }: PluginAPI) => { addVariant('hocus', { '@media (hover: hover)': { '&:hover': '@slot', }, '&:focus': '@slot', }) - } - }, + }, + base: '/root', + }), }, ) let compiled = build(['hocus:underline', 'group-hocus:flex']) @@ -1908,8 +1925,8 @@ describe('plugins', () => { } `, { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { + loadModule: async () => ({ + module: ({ addVariant }: PluginAPI) => { addVariant('hocus', { '&': { '--custom-property': '@slot', @@ -1917,8 +1934,9 @@ describe('plugins', () => { '&:focus': '@slot', }, }) - } - }, + }, + base: '/root', + }), }, ) let compiled = build(['hocus:underline']) @@ -1944,13 +1962,13 @@ describe('plugins', () => { @tailwind utilities; } `, - { - loadPlugin: async () => { - return ({ addVariant }: PluginAPI) => { + loadModule: async () => ({ + module: ({ addVariant }: PluginAPI) => { addVariant('dark', '&:is([data-theme=dark] *)') - } - }, + }, + base: '/root', + }), }, ) let compiled = build( @@ -1981,20 +1999,29 @@ describe('plugins', () => { describe('@source', () => { test('emits @source files', async () => { - let { globs } = await compile(css` - @source "./foo/bar/*.ts"; - `) + let { globs } = await compile( + css` + @source "./foo/bar/*.ts"; + `, + { base: '/root' }, + ) - expect(globs).toEqual([{ pattern: './foo/bar/*.ts' }]) + expect(globs).toEqual([{ pattern: './foo/bar/*.ts', base: '/root' }]) }) test('emits multiple @source files', async () => { - let { globs } = await compile(css` - @source "./foo/**/*.ts"; - @source "./php/secr3t/smarty.php"; - `) + let { globs } = await compile( + css` + @source "./foo/**/*.ts"; + @source "./php/secr3t/smarty.php"; + `, + { base: '/root' }, + ) - expect(globs).toEqual([{ pattern: './foo/**/*.ts' }, { pattern: './php/secr3t/smarty.php' }]) + expect(globs).toEqual([ + { pattern: './foo/**/*.ts', base: '/root' }, + { pattern: './php/secr3t/smarty.php', base: '/root' }, + ]) }) }) @@ -2513,17 +2540,17 @@ test('addBase', async () => { @tailwind utilities; } `, - { - loadPlugin: async () => { - return ({ addBase }: PluginAPI) => { + loadModule: async () => ({ + module: ({ addBase }: PluginAPI) => { addBase({ body: { 'font-feature-settings': '"tnum"', }, }) - } - }, + }, + base: '/root', + }), }, ) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index ad441084c28c..6ded20cd4dc0 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -1,6 +1,17 @@ import { version } from '../package.json' import { substituteAtApply } from './apply' -import { comment, decl, rule, toCss, walk, WalkAction, type Rule } from './ast' +import { + comment, + context, + decl, + rule, + toCss, + walk, + WalkAction, + type AstNode, + type Rule, +} from './ast' +import { substituteAtImports } from './at-import' import { applyCompatibilityHooks } from './compat/apply-compat-hooks' import type { UserConfig } from './compat/config/types' import { type Plugin } from './compat/plugin-api' @@ -15,16 +26,21 @@ export type Config = UserConfig const IS_VALID_UTILITY_NAME = /^[a-z][a-zA-Z0-9/%._-]*$/ type CompileOptions = { - loadPlugin?: (path: string) => Promise - loadConfig?: (path: string) => Promise + base?: string + loadModule?: ( + id: string, + base: string, + resourceHint: 'plugin' | 'config', + ) => Promise<{ module: Plugin | Config; base: string }> + loadStylesheet?: (id: string, base: string) => Promise<{ content: string; base: string }> } -function throwOnPlugin(): never { - throw new Error('No `loadPlugin` function provided to `compile`') +function throwOnLoadModule(): never { + throw new Error('No `loadModule` function provided to `compile`') } -function throwOnConfig(): never { - throw new Error('No `loadConfig` function provided to `compile`') +function throwOnLoadStylesheet(): never { + throw new Error('No `loadStylesheet` function provided to `compile`') } function parseThemeOptions(selector: string) { @@ -45,9 +61,15 @@ function parseThemeOptions(selector: string) { async function parseCss( css: string, - { loadPlugin = throwOnPlugin, loadConfig = throwOnConfig }: CompileOptions = {}, + { + base = '', + loadModule = throwOnLoadModule, + loadStylesheet = throwOnLoadStylesheet, + }: CompileOptions = {}, ) { - let ast = CSS.parse(css) + let ast = [context({ base }, CSS.parse(css))] as AstNode[] + + await substituteAtImports(ast, base, loadStylesheet) // Find all `@theme` declarations let theme = new Theme() @@ -55,9 +77,9 @@ async function parseCss( let customUtilities: ((designSystem: DesignSystem) => void)[] = [] let firstThemeRule: Rule | null = null let keyframesRules: Rule[] = [] - let globs: { origin?: string; pattern: string }[] = [] + let globs: { base: string; pattern: string }[] = [] - walk(ast, (node, { parent, replaceWith }) => { + walk(ast, (node, { parent, replaceWith, context }) => { if (node.kind !== 'rule') return // Collect custom `@utility` at-rules @@ -104,7 +126,7 @@ async function parseCss( ) { throw new Error('`@source` paths must be quoted.') } - globs.push({ pattern: path.slice(1, -1) }) + globs.push({ base: context.base, pattern: path.slice(1, -1) }) replaceWith([]) return } @@ -234,7 +256,7 @@ async function parseCss( // of random arguments because it really just needs access to "the world" to // do whatever ungodly things it needs to do to make things backwards // compatible without polluting core. - await applyCompatibilityHooks({ designSystem, ast, loadPlugin, loadConfig, globs }) + await applyCompatibilityHooks({ designSystem, base, ast, loadModule, globs }) for (let customVariant of customVariants) { customVariant(designSystem) @@ -316,7 +338,7 @@ export async function compile( css: string, opts: CompileOptions = {}, ): Promise<{ - globs: { origin?: string; pattern: string }[] + globs: { base: string; pattern: string }[] build(candidates: string[]): string }> { let { designSystem, ast, globs } = await parseCss(css, opts) diff --git a/packages/tailwindcss/src/plugin.test.ts b/packages/tailwindcss/src/plugin.test.ts index 5af7ff1d4599..9af3a55fbb58 100644 --- a/packages/tailwindcss/src/plugin.test.ts +++ b/packages/tailwindcss/src/plugin.test.ts @@ -10,15 +10,16 @@ test('plugin', async () => { ` let compiler = await compile(input, { - loadPlugin: async () => { - return plugin(function ({ addBase }) { + loadModule: async () => ({ + module: plugin(function ({ addBase }) { addBase({ body: { margin: '0', }, }) - }) - }, + }), + base: '/root', + }), }) expect(compiler.build([])).toMatchInlineSnapshot(` @@ -37,8 +38,8 @@ test('plugin.withOptions', async () => { ` let compiler = await compile(input, { - loadPlugin: async () => { - return plugin.withOptions(function (opts = { foo: '1px' }) { + loadModule: async () => ({ + module: plugin.withOptions(function (opts = { foo: '1px' }) { return function ({ addBase }) { addBase({ body: { @@ -46,8 +47,9 @@ test('plugin.withOptions', async () => { }, }) } - }) - }, + }), + base: '/root', + }), }) expect(compiler.build([])).toMatchInlineSnapshot(` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3735d13ea5d4..7a1f10b82655 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -156,25 +156,15 @@ importers: picocolors: specifier: ^1.0.1 version: 1.0.1 - postcss: - specifier: ^8.4.41 - version: 8.4.41 - postcss-import: - specifier: ^16.1.0 - version: 16.1.0(postcss@8.4.41) tailwindcss: specifier: workspace:^ version: link:../tailwindcss - devDependencies: - '@types/postcss-import': - specifier: ^14.0.3 - version: 14.0.3 - internal-postcss-fix-relative-paths: - specifier: workspace:^ - version: link:../internal-postcss-fix-relative-paths packages/@tailwindcss-node: dependencies: + enhanced-resolve: + specifier: ^5.17.1 + version: 5.17.1 jiti: specifier: ^2.0.0-beta.3 version: 2.0.0-beta.3 @@ -194,9 +184,6 @@ importers: lightningcss: specifier: 'catalog:' version: 1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4) - postcss-import: - specifier: ^16.1.0 - version: 16.1.0(postcss@8.4.41) tailwindcss: specifier: workspace:^ version: link:../tailwindcss @@ -205,17 +192,17 @@ importers: specifier: 'catalog:' version: 20.14.13 '@types/postcss-import': - specifier: ^14.0.3 + specifier: 14.0.3 version: 14.0.3 internal-example-plugin: specifier: workspace:* version: link:../internal-example-plugin - internal-postcss-fix-relative-paths: - specifier: workspace:^ - version: link:../internal-postcss-fix-relative-paths postcss: specifier: ^8.4.41 version: 8.4.41 + postcss-import: + specifier: ^16.1.0 + version: 16.1.0(postcss@8.4.41) packages/@tailwindcss-standalone: dependencies: @@ -326,12 +313,6 @@ importers: lightningcss: specifier: 'catalog:' version: 1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4) - postcss: - specifier: ^8.4.41 - version: 8.4.41 - postcss-import: - specifier: ^16.1.0 - version: 16.1.0(postcss@8.4.41) tailwindcss: specifier: workspace:^ version: link:../tailwindcss @@ -339,33 +320,12 @@ importers: '@types/node': specifier: 'catalog:' version: 20.14.13 - '@types/postcss-import': - specifier: ^14.0.3 - version: 14.0.3 - internal-postcss-fix-relative-paths: - specifier: workspace:^ - version: link:../internal-postcss-fix-relative-paths vite: specifier: 'catalog:' version: 5.4.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6) packages/internal-example-plugin: {} - packages/internal-postcss-fix-relative-paths: - devDependencies: - '@types/node': - specifier: 'catalog:' - version: 20.14.13 - '@types/postcss-import': - specifier: ^14.0.3 - version: 14.0.3 - postcss: - specifier: 8.4.41 - version: 8.4.41 - postcss-import: - specifier: ^16.1.0 - version: 16.1.0(postcss@8.4.41) - packages/tailwindcss: devDependencies: '@tailwindcss/oxide': @@ -374,6 +334,9 @@ importers: '@types/node': specifier: 'catalog:' version: 20.14.13 + dedent: + specifier: 1.5.3 + version: 1.5.3 lightningcss: specifier: 'catalog:' version: 1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4) @@ -1055,7 +1018,6 @@ packages: '@parcel/watcher-darwin-arm64@2.4.2-alpha.0': resolution: {integrity: sha512-2xH4Ve7OKjIh+4YRfTN3HGJa2W8KTPLOALHZj5fxcbTPwaVxdpIRItDrcikUx2u3AzGAFme7F+AZZXHnf0F15Q==} engines: {node: '>= 10.0.0'} - cpu: [arm64] os: [darwin] '@parcel/watcher-darwin-x64@2.4.1': @@ -1067,7 +1029,6 @@ packages: '@parcel/watcher-darwin-x64@2.4.2-alpha.0': resolution: {integrity: sha512-xtjmXUH4YZVah5+7Q0nb+fpRP5qZn9cFfuPuZ4k77UfUGVwhacgZyIRQgIOwMP3GkgW4TsrKQaw1KIe7L1ZqcQ==} engines: {node: '>= 10.0.0'} - cpu: [x64] os: [darwin] '@parcel/watcher-freebsd-x64@2.4.1': @@ -1091,7 +1052,6 @@ packages: '@parcel/watcher-linux-arm64-glibc@2.4.2-alpha.0': resolution: {integrity: sha512-vIIOcZf+fgsRReIK3Fw0WINvGo9UwiXfisnqYRzfpNByRZvkEPkGTIVe8iiDp72NhPTVmwIvBqM6yKDzIaw8GQ==} engines: {node: '>= 10.0.0'} - cpu: [arm64] os: [linux] '@parcel/watcher-linux-arm64-musl@2.4.1': @@ -1103,7 +1063,6 @@ packages: '@parcel/watcher-linux-arm64-musl@2.4.2-alpha.0': resolution: {integrity: sha512-gXqEAoLG9bBCbQNUgqjSOxHcjpmCZmYT9M8UvrdTMgMYgXgiWcR8igKlPRd40mCIRZSkMpN2ScSy2WjQ0bQZnQ==} engines: {node: '>= 10.0.0'} - cpu: [arm64] os: [linux] '@parcel/watcher-linux-x64-glibc@2.4.1': @@ -1115,7 +1074,6 @@ packages: '@parcel/watcher-linux-x64-glibc@2.4.2-alpha.0': resolution: {integrity: sha512-/WJJ3Y46ubwQW+Z+mzpzK3pvqn/AT7MA63NB0+k9GTLNxJQZNREensMtpJ/FJ+LVIiraEHTY22KQrsx9+DeNbw==} engines: {node: '>= 10.0.0'} - cpu: [x64] os: [linux] '@parcel/watcher-linux-x64-musl@2.4.1': @@ -1127,7 +1085,6 @@ packages: '@parcel/watcher-linux-x64-musl@2.4.2-alpha.0': resolution: {integrity: sha512-1dz4fTM5HaANk3RSRmdhALT+bNqTHawVDL1D77HwV/FuF/kSjlM3rGrJuFaCKwQ5E8CInHCcobqMN8Jh8LYaRg==} engines: {node: '>= 10.0.0'} - cpu: [x64] os: [linux] '@parcel/watcher-win32-arm64@2.4.1': @@ -1151,7 +1108,6 @@ packages: '@parcel/watcher-win32-x64@2.4.2-alpha.0': resolution: {integrity: sha512-U2abMKF7JUiIxQkos19AvTLFcnl2Xn8yIW1kzu+7B0Lux4Gkuu/BUDBroaM1s6+hwgK63NOLq9itX2Y3GwUThg==} engines: {node: '>= 10.0.0'} - cpu: [x64] os: [win32] '@parcel/watcher@2.4.1': @@ -1487,13 +1443,11 @@ packages: bun@1.1.22: resolution: {integrity: sha512-G2HCPhzhjDc2jEDkZsO9vwPlpHrTm7a8UVwx9oNS5bZqo5OcSK5GPuWYDWjj7+37bRk5OVLfeIvUMtSrbKeIjQ==} - cpu: [arm64, x64] os: [darwin, linux, win32] hasBin: true bun@1.1.26: resolution: {integrity: sha512-dWSewAqE7sVbYmflJxgG47dW4vmsbar7VAnQ4ao45y3ulr3n7CwdsMLFnzd28jhPRtF+rsaVK2y4OLIkP3OD4A==} - cpu: [arm64, x64] os: [darwin, linux, win32] hasBin: true @@ -2260,13 +2214,11 @@ packages: lightningcss-darwin-arm64@1.26.0: resolution: {integrity: sha512-n4TIvHO1NY1ondKFYpL2ZX0bcC2y6yjXMD6JfyizgR8BCFNEeArINDzEaeqlfX9bXz73Bpz/Ow0nu+1qiDrBKg==} engines: {node: '>= 12.0.0'} - cpu: [arm64] os: [darwin] lightningcss-darwin-x64@1.26.0: resolution: {integrity: sha512-Rf9HuHIDi1R6/zgBkJh25SiJHF+dm9axUZW/0UoYCW1/8HV0gMI0blARhH4z+REmWiU1yYT/KyNF3h7tHyRXUg==} engines: {node: '>= 12.0.0'} - cpu: [x64] os: [darwin] lightningcss-freebsd-x64@1.26.0: @@ -2284,25 +2236,21 @@ packages: lightningcss-linux-arm64-gnu@1.26.0: resolution: {integrity: sha512-iJmZM7fUyVjH+POtdiCtExG+67TtPUTer7K/5A8DIfmPfrmeGvzfRyBltGhQz13Wi15K1lf2cPYoRaRh6vcwNA==} engines: {node: '>= 12.0.0'} - cpu: [arm64] os: [linux] lightningcss-linux-arm64-musl@1.26.0: resolution: {integrity: sha512-XxoEL++tTkyuvu+wq/QS8bwyTXZv2y5XYCMcWL45b8XwkiS8eEEEej9BkMGSRwxa5J4K+LDeIhLrS23CpQyfig==} engines: {node: '>= 12.0.0'} - cpu: [arm64] os: [linux] lightningcss-linux-x64-gnu@1.26.0: resolution: {integrity: sha512-1dkTfZQAYLj8MUSkd6L/+TWTG8V6Kfrzfa0T1fSlXCXQHrt1HC1/UepXHtKHDt/9yFwyoeayivxXAsApVxn6zA==} engines: {node: '>= 12.0.0'} - cpu: [x64] os: [linux] lightningcss-linux-x64-musl@1.26.0: resolution: {integrity: sha512-yX3Rk9m00JGCUzuUhFEojY+jf/6zHs3XU8S8Vk+FRbnr4St7cjyMXdNjuA2LjiT8e7j8xHRCH8hyZ4H/btRE4A==} engines: {node: '>= 12.0.0'} - cpu: [x64] os: [linux] lightningcss-win32-arm64-msvc@1.26.0: @@ -2314,7 +2262,6 @@ packages: lightningcss-win32-x64-msvc@1.26.0: resolution: {integrity: sha512-pYS3EyGP3JRhfqEFYmfFDiZ9/pVNfy8jVIYtrx9TVNusVyDK3gpW1w/rbvroQ4bDJi7grdUtyrYU6V2xkY/bBw==} engines: {node: '>= 12.0.0'} - cpu: [x64] os: [win32] lightningcss@1.26.0: @@ -4442,7 +4389,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -4466,7 +4413,7 @@ snapshots: enhanced-resolve: 5.17.1 eslint: 8.57.0 eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.6 is-core-module: 2.15.0 @@ -4488,7 +4435,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5