From 79794744a9cc4ece01b2ab067171fcba7868d642 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Mon, 23 Sep 2024 17:05:55 +0200 Subject: [PATCH] Resolve `@import` in core (#14446) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR brings `@import` resolution into Tailwind CSS core. This means that our clients (PostCSS, Vite, and CLI) no longer need to depend on `postcss` and `postcss-import` to resolve `@import`. Furthermore this simplifies the handling of relative paths for `@source`, `@plugin`, or `@config` in transitive CSS files (where the relative root should always be relative to the CSS file that contains the directive). This PR also fixes a plugin resolution bug where non-relative imports (e.g. directly importing node modules like `@plugin '@tailwindcss/typography';`) would not work in CSS files that are based in a different npm package. ### Resolving `@import` The core of the `@import` resolution is inside `packages/tailwindcss/src/at-import.ts`. There, to keep things performant, we do a two-step process to resolve imports. Imagine the following input CSS file: ```css @import "tailwindcss/theme.css"; @import "tailwindcss/utilities.css"; ``` Since our AST walks are synchronous, we will do a first traversal where we start a loading request for each `@import` directive. Once all loads are started, we will await the promise and do a second walk where we actually replace the AST nodes with their resolved stylesheets. All of this is recursive, so that `@import`-ed files can again `@import` other files. The core `@import` resolver also includes extensive test cases for [various combinations of media query and supports conditionals as well als layered imports](https://developer.mozilla.org/en-US/docs/Web/CSS/@import). When the same file is imported multiple times, the AST nodes are duplicated but duplicate I/O is avoided on a per-file basis, so this will only load one file, but include the `@theme` rules twice: ```css @import "tailwindcss/theme.css"; @import "tailwindcss/theme.css"; ``` ### Adding a new `context` node to the AST One limitation we had when working with the `postcss-import` plugin was the need to do an additional traversal to rewrite relative `@source`, `@plugin`, and `@config` directives. This was needed because we want these paths to be relative to the CSS file that defines the directive but when flattening a CSS file, this information is no longer part of the stringifed CSS representation. We worked around this by rewriting the content of these directives to be relative to the input CSS file, which resulted in added complexity and caused a lot of issues with Windows paths in the beginning. Now that we are doing the `@import` resolution in core, we can use a different data structure to persist this information. This PR adds a new `context` node so that we can store arbitrary context like this inside the Ast directly. This allows us to share information with the sub tree _while doing the Ast walk_. Here's an example of how the new `context` node can be used to share information with subtrees: ```ts const ast = [ rule('.foo', [decl('color', 'red')]), context({ value: 'a' }, [ rule('.bar', [ decl('color', 'blue'), context({ value: 'b' }, [ rule('.baz', [decl('color', 'green')]), ]), ]), ]), ] walk(ast, (node, { context }) => { if (node.kind !== 'declaration') return switch (node.value) { case 'red': assert(context.value === undefined) case 'blue': assert(context.value === 'a') case 'green': assert(context.value === 'b') } }) ``` In core, we use this new Ast node specifically to persist the `base` path of the current CSS file. We put the input CSS file `base` at the root of the Ast and then overwrite the `base` on every `@import` substitution. ### Removing the dependency on `postcss-import` Now that we support `@import` resolution in core, our clients no longer need a dependency on `postcss-import`. Furthermore, most dependencies also don't need to know about `postcss` at all anymore (except the PostCSS client, of course!). This also means that our workaround for rewriting `@source`, the `postcss-fix-relative-paths` plugin, can now go away as a shared dependency between all of our clients. Note that we still have it for the PostCSS plugin only, where it's possible that users already have `postcss-import` running _before_ the `@tailwindcss/postcss` plugin. Here's an example of the changes to the dependencies for our Vite client ✨ : Screenshot 2024-09-19 at 16 59 45 ### Performance Since our Vite and CLI clients now no longer need to use `postcss` at all, we have also measured a significant improvement to the initial build times. For a small test setup that contains only a hand full of files (nothing super-complex), we measured an improvement in the **3.5x** range: Screenshot 2024-09-19 at 14 52 49 The code for this is in the commit history if you want to reproduce the results. The test was based on the Vite client. ### Caveats One thing to note is that we previously relied on finding specific symbols in the input CSS to _bail out of Tailwind processing completely_. E.g. if a file does not contain a `@tailwind` or `@apply` directive, it can never be a Tailwind file. Since we no longer have a string representation of the flattened CSS file, we can no longer do this check. However, the current implementation was already inconsistent with differences on the allowed symbol list between our clients. Ideally, Tailwind CSS should figure out wether a CSS file is a Tailwind CSS file. This, however, is left as an improvement for a future API since it goes hand-in-hand with our planned API changes for the core `tailwindcss` package. --------- Co-authored-by: Jordan Pittman --- CHANGELOG.md | 1 + integrations/postcss/index.test.ts | 80 + packages/@tailwindcss-cli/package.json | 6 - .../src/commands/build/index.ts | 104 +- packages/@tailwindcss-cli/tsup.config.ts | 1 - packages/@tailwindcss-node/package.json | 1 + packages/@tailwindcss-node/src/compile.ts | 115 +- packages/@tailwindcss-node/src/index.cts | 1 + packages/@tailwindcss-node/src/index.ts | 1 + .../src/normalize-path.ts | 0 packages/@tailwindcss-postcss/package.json | 7 +- packages/@tailwindcss-postcss/src/index.ts | 40 +- .../fixtures/example-project/src/index.css | 0 .../fixtures/example-project/src/invalid.css | 0 .../fixtures/external-import/src/index.css | 0 .../fixtures/external-import/src/invalid.css | 0 .../external-import/src/plugins-in-root.css | 0 .../src/plugins-in-sibling.css | 0 .../postcss-fix-relative-paths}/index.test.ts | 0 .../src/postcss-fix-relative-paths}/index.ts | 4 +- packages/@tailwindcss-postcss/tsup.config.ts | 2 - packages/@tailwindcss-standalone/src/index.ts | 10 + .../@tailwindcss-standalone/src/types.d.ts | 3 + packages/@tailwindcss-vite/package.json | 4 - packages/@tailwindcss-vite/src/index.ts | 50 +- packages/@tailwindcss-vite/tsup.config.ts | 1 - .../package.json | 27 - .../tsconfig.json | 3 - packages/tailwindcss/package.json | 3 +- packages/tailwindcss/src/ast.test.ts | 53 +- packages/tailwindcss/src/ast.ts | 37 +- packages/tailwindcss/src/at-import.test.ts | 572 +++++ packages/tailwindcss/src/at-import.ts | 147 ++ .../src/compat/apply-compat-hooks.ts | 75 +- .../src/compat/apply-config-to-theme.test.ts | 1 + .../tailwindcss/src/compat/config.test.ts | 473 +++-- .../src/compat/config/resolve-config.test.ts | 9 + .../src/compat/config/resolve-config.ts | 9 +- .../tailwindcss/src/compat/plugin-api.test.ts | 1888 +++++++++-------- .../src/compat/screens-config.test.ts | 123 +- .../tailwindcss/src/css-functions.test.ts | 84 +- packages/tailwindcss/src/index.test.ts | 195 +- packages/tailwindcss/src/index.ts | 50 +- packages/tailwindcss/src/plugin.test.ts | 18 +- pnpm-lock.yaml | 79 +- 45 files changed, 2705 insertions(+), 1572 deletions(-) rename packages/{internal-postcss-fix-relative-paths => @tailwindcss-node}/src/normalize-path.ts (100%) rename packages/{internal-postcss-fix-relative-paths/src => @tailwindcss-postcss/src/postcss-fix-relative-paths}/fixtures/example-project/src/index.css (100%) rename packages/{internal-postcss-fix-relative-paths/src => @tailwindcss-postcss/src/postcss-fix-relative-paths}/fixtures/example-project/src/invalid.css (100%) rename packages/{internal-postcss-fix-relative-paths/src => @tailwindcss-postcss/src/postcss-fix-relative-paths}/fixtures/external-import/src/index.css (100%) rename packages/{internal-postcss-fix-relative-paths/src => @tailwindcss-postcss/src/postcss-fix-relative-paths}/fixtures/external-import/src/invalid.css (100%) rename packages/{internal-postcss-fix-relative-paths/src => @tailwindcss-postcss/src/postcss-fix-relative-paths}/fixtures/external-import/src/plugins-in-root.css (100%) rename packages/{internal-postcss-fix-relative-paths/src => @tailwindcss-postcss/src/postcss-fix-relative-paths}/fixtures/external-import/src/plugins-in-sibling.css (100%) rename packages/{internal-postcss-fix-relative-paths/src => @tailwindcss-postcss/src/postcss-fix-relative-paths}/index.test.ts (100%) rename packages/{internal-postcss-fix-relative-paths/src => @tailwindcss-postcss/src/postcss-fix-relative-paths}/index.ts (96%) delete mode 100644 packages/internal-postcss-fix-relative-paths/package.json delete mode 100644 packages/internal-postcss-fix-relative-paths/tsconfig.json create mode 100644 packages/tailwindcss/src/at-import.test.ts create mode 100644 packages/tailwindcss/src/at-import.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bcda1531..b8c3e59ed 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 d4e4f4d39..273f07f73 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 e36b6e2ad..c8dbf8cce 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 3ca7f4ad0..daf12294c 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 236281270..7d82eee2c 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 652e65538..97f63f77e 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 78c68d9ae..02332d578 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}'`) + + if (typeof globalThis.__tw_readFile === 'function') { + let file = await globalThis.__tw_readFile(resolvedPath, 'utf-8') + if (file) { + return { + base: path.dirname(resolvedPath), + content: file, + } + } } - let resolvedPath = path.resolve(base, configPath) - let [module, moduleDependencies] = await Promise.all([ - importModule(pathToFileURL(resolvedPath).href + '?id=' + Date.now()), - getModuleDependencies(resolvedPath), - ]) - - 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 a143865ef..ee0de7ff5 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 85b292ed0..f42c4ff4e 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 94d561f59..11da052a9 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 4f9eccbb5..881445a80 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 7a0f92da7..2b88014a3 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 76e4fc03b..684c072ac 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 5dfefaf82..ae90e2b6e 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 dfdac2715..e13e4e427 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 a74b4c3c8..20a7de978 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 8a1c60932..53f2aecff 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 eaf99e82a..85bf3149d 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 893f7466d..000000000 --- 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 6ae022f65..000000000 --- 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 7173e7201..10e45bdb3 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 da0b19204..21915cba4 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 fa5dc6f1b..afc1888c2 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 000000000..bbd5e6863 --- /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 000000000..a06000826 --- /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 686b37dc8..4290c136a 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 af511a25e..aad2753b0 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 acfaebc9e..2b8819a68 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' }], + loadModule: async () => ({ + module: { + 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], + }, + '[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 818ff8373..895939bca 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 268e6883a..1f59b50cf 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 f0d0b1102..7d732ca1c 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'), - }, - ) - - matchUtilities( - { - animation2: (value) => ({ animation: value }), - }, - { - values: { - DEFAULT: theme('animation.DEFAULT'), - twist: theme('animation.spin'), + 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'), + }, + }, + ) + }), + } }, }) @@ -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')) + 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'), + // 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', - }, - }) - - matchUtilities( - { - foo: (value) => ({ - '--my-prop': value, - }), - }, - { - values: { - bar: 'bar-valuer', - baz: 'bar-valuer', + loadModule: async (id, base) => { + return { + base, + module: plugin(({ addUtilities, matchUtilities }) => { + addUtilities({ + '.foo-bar': { + color: 'red', }, - }, - ) + }) - addUtilities({ - '.foo-bar': { - backgroundColor: 'red', - }, - }) - }) + matchUtilities( + { + foo: (value) => ({ + '--my-prop': value, + }), + }, + { + values: { + bar: 'bar-valuer', + baz: 'bar-valuer', + }, + }, + ) + + 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,12 +1290,15 @@ 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', + }) + }, } }, }, @@ -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', - }, - modifiers: 'any', - }, - ) + { + 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', + }, + ) + }, } }, }, @@ -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', - }, - modifiers: { - foo: 'foo', + { + 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', + }, + }, + ) + }, } }, }, @@ -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'] }, - ) - matchUtilities( - { - scrollbar: (value) => ({ 'scrollbar-width': value }), - }, - { type: ['length'] }, - ) + { + 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'] }, + ) + }, } }, }, @@ -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'] }, - ) - matchUtilities( - { - scrollbar: (value) => ({ '--scrollbar-width': value }), - }, - { type: ['length'] }, - ) + { + 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'] }, + ) + }, } }, }, @@ -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' } }, - ) - matchUtilities( - { - scrollbar: (value) => ({ 'scrollbar-width': value }), - }, - { type: ['length'], modifiers: { bar: 'bar' } }, - ) + { + 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' } }, + ) + }, } }, }, @@ -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', - }, - }, - ) - matchUtilities( - { - scrollbar: (value) => ({ 'scrollbar-width': value }), - }, - { - type: ['length'], - values: { - 2: '2px', + { + 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', + }, + }, + ) + }, } }, }, @@ -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 05f725a09..5f75504ad 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 b40dc9a2b..521f50009 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)', - }, - }) + plugins: [ + plugin(({ addBase, addUtilities }) => { + addBase({ + '.my-base-rule': { + background: 'theme(colors.primary)', + color: 'theme(colors.secondary)', + }, + }) - addUtilities({ - '.my-utility': { - color: 'theme(colors.red)', - }, - }) - }), - ], + 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 fb226708e..3693bbb0e 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 ad441084c..6ded20cd4 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 5af7ff1d4..9af3a55fb 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 3735d13ea..7a1f10b82 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