From 99b73ee368a1deb9466687fc8723dd07d33d2b5b Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 29 Nov 2024 16:59:29 +0100 Subject: [PATCH] Improve performance of `@tailwindcss/postcss` and `@tailwindcss/vite` (#15226) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR improves the performance of the `@tailwindcss/postcss` and `@tailwindcss/vite` implementations. The issue is that in some scenarios, if you have multiple `.css` files, then all of the CSS files are ran through the Tailwind CSS compiler. The issue with this is that in a lot of cases, the CSS files aren't even related to Tailwind CSS at all. E.g.: in a Next.js project, if you use the `next/font/local` tool, then every font you used will be in a separate CSS file. This means that we run Tailwind CSS in all these files as well. That said, running Tailwind CSS on these files isn't the end of the world because we still need to handle `@import` in case `@tailwind utilities` is being used. However, we also run the auto source detection logic for every CSS file in the system. This part is bad. To solve this, this PR introduces an internal `features` to collect what CSS features are used throughout the system (`@import`, `@plugin`, `@apply`, `@tailwind utilities`, etc…) The `@tailwindcss/postcss` and `@tailwindcss/vite` plugin can use that information to decide if they can take some shortcuts or not. --- Overall, this means that we don't run the slow parts of Tailwind CSS if we don't need to. --------- Co-authored-by: Adam Wathan --- CHANGELOG.md | 5 +- packages/@tailwindcss-node/src/compile.ts | 3 + packages/@tailwindcss-node/src/index.ts | 2 +- packages/@tailwindcss-postcss/package.json | 1 + .../@tailwindcss-postcss/src/index.test.ts | 50 ++++++++--- packages/@tailwindcss-postcss/src/index.ts | 83 ++++++++++--------- .../src/template/codemods/theme-to-var.ts | 2 +- .../@tailwindcss-upgrade/src/utils/walk.ts | 2 +- packages/@tailwindcss-vite/src/index.ts | 81 ++++++++++-------- packages/tailwindcss/src/apply.ts | 4 + packages/tailwindcss/src/ast.test.ts | 32 ++++++- packages/tailwindcss/src/ast.ts | 14 +++- packages/tailwindcss/src/at-import.test.ts | 33 ++------ packages/tailwindcss/src/at-import.ts | 10 ++- .../src/compat/apply-compat-hooks.ts | 20 ++++- .../src/compat/apply-config-to-theme.ts | 8 +- packages/tailwindcss/src/compat/plugin-api.ts | 8 +- .../tailwindcss/src/compat/selector-parser.ts | 8 +- packages/tailwindcss/src/css-functions.ts | 5 ++ packages/tailwindcss/src/index.ts | 63 ++++++++++---- packages/tailwindcss/src/value-parser.ts | 8 +- pnpm-lock.yaml | 42 +++++++--- 22 files changed, 322 insertions(+), 162 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fee74c24cc41..2a75ef940f67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- Nothing yet! +### Fixed + +- Don't scan source files for utilities unless `@tailwind utilities` is present in the CSS in `@tailwindcss/postcss` and `@tailwindcss/vite` ([#15226](https://github.com/tailwindlabs/tailwindcss/pull/15226)) +- Skip reserializing CSS files that don't use Tailwind features in `@tailwindcss/postcss` and `@tailwindcss/vite` ([#15226](https://github.com/tailwindlabs/tailwindcss/pull/15226)) ## [4.0.0-beta.3] - 2024-11-27 diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index a7aaf3e64849..f7e3537515c0 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -7,10 +7,13 @@ import { pathToFileURL } from 'node:url' import { __unstable__loadDesignSystem as ___unstable__loadDesignSystem, compile as _compile, + Features, } from 'tailwindcss' import { getModuleDependencies } from './get-module-dependencies' import { rewriteUrls } from './urls' +export { Features } + export type Resolver = (id: string, base: string) => Promise export async function compile( diff --git a/packages/@tailwindcss-node/src/index.ts b/packages/@tailwindcss-node/src/index.ts index eb287ae8203b..d11771290317 100644 --- a/packages/@tailwindcss-node/src/index.ts +++ b/packages/@tailwindcss-node/src/index.ts @@ -1,7 +1,7 @@ import * as Module from 'node:module' import { pathToFileURL } from 'node:url' import * as env from './env' -export { __unstable__loadDesignSystem, compile } from './compile' +export { __unstable__loadDesignSystem, compile, Features } from './compile' export * from './normalize-path' export { env } diff --git a/packages/@tailwindcss-postcss/package.json b/packages/@tailwindcss-postcss/package.json index eb42d84be1d8..9588f21ab217 100644 --- a/packages/@tailwindcss-postcss/package.json +++ b/packages/@tailwindcss-postcss/package.json @@ -40,6 +40,7 @@ "devDependencies": { "@types/node": "catalog:", "@types/postcss-import": "14.0.3", + "dedent": "1.5.3", "internal-example-plugin": "workspace:*", "postcss-import": "^16.1.0" } diff --git a/packages/@tailwindcss-postcss/src/index.test.ts b/packages/@tailwindcss-postcss/src/index.test.ts index 3455902718d5..02ab23bea23d 100644 --- a/packages/@tailwindcss-postcss/src/index.test.ts +++ b/packages/@tailwindcss-postcss/src/index.test.ts @@ -1,3 +1,4 @@ +import dedent from 'dedent' import { unlink, writeFile } from 'node:fs/promises' import postcss from 'postcss' import { afterEach, beforeEach, describe, expect, test } from 'vitest' @@ -9,16 +10,20 @@ import tailwindcss from './index' // We place it in packages/ because Vitest runs in the monorepo root, // and packages/tailwindcss must be a sub-folder for // @import 'tailwindcss' to work. -const INPUT_CSS_PATH = `${__dirname}/fixtures/example-project/input.css` +function inputCssFilePath() { + // Including the current test name to ensure that the cache is invalidated per + // test otherwise the cache will be used across tests. + return `${__dirname}/fixtures/example-project/input.css?test=${expect.getState().currentTestName}` +} -const css = String.raw +const css = dedent test("`@import 'tailwindcss'` is replaced with the generated CSS", async () => { let processor = postcss([ tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), ]) - let result = await processor.process(`@import 'tailwindcss'`, { from: INPUT_CSS_PATH }) + let result = await processor.process(`@import 'tailwindcss'`, { from: inputCssFilePath() }) expect(result.css.trim()).toMatchSnapshot() @@ -49,8 +54,6 @@ test('output is optimized by Lightning CSS', async () => { tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), ]) - // `@apply` is used because Lightning is skipped if neither `@tailwind` nor - // `@apply` is used. let result = await processor.process( css` @layer utilities { @@ -65,7 +68,7 @@ test('output is optimized by Lightning CSS', async () => { } } `, - { from: INPUT_CSS_PATH }, + { from: inputCssFilePath() }, ) expect(result.css.trim()).toMatchInlineSnapshot(` @@ -86,8 +89,6 @@ test('@apply can be used without emitting the theme in the CSS file', async () = tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), ]) - // `@apply` is used because Lightning is skipped if neither `@tailwind` nor - // `@apply` is used. let result = await processor.process( css` @import 'tailwindcss/theme.css' theme(reference); @@ -95,7 +96,7 @@ test('@apply can be used without emitting the theme in the CSS file', async () = @apply text-red-500; } `, - { from: INPUT_CSS_PATH }, + { from: inputCssFilePath() }, ) expect(result.css.trim()).toMatchInlineSnapshot(` @@ -116,7 +117,7 @@ describe('processing without specifying a base path', () => { test('the current working directory is used by default', async () => { let processor = postcss([tailwindcss({ optimize: { minify: false } })]) - let result = await processor.process(`@import "tailwindcss"`, { from: INPUT_CSS_PATH }) + let result = await processor.process(`@import "tailwindcss"`, { from: inputCssFilePath() }) expect(result.css).toContain( ".md\\:\\[\\&\\:hover\\]\\:content-\\[\\'testing_default_base_path\\'\\]", @@ -142,7 +143,7 @@ describe('plugins', () => { @import 'tailwindcss/utilities'; @plugin './plugin.js'; `, - { from: INPUT_CSS_PATH }, + { from: inputCssFilePath() }, ) expect(result.css.trim()).toMatchInlineSnapshot(` @@ -202,7 +203,7 @@ describe('plugins', () => { @import 'tailwindcss/utilities'; @plugin 'internal-example-plugin'; `, - { from: INPUT_CSS_PATH }, + { from: inputCssFilePath() }, ) expect(result.css.trim()).toMatchInlineSnapshot(` @@ -222,3 +223,28 @@ describe('plugins', () => { `) }) }) + +test('bail early when Tailwind is not used', async () => { + let processor = postcss([ + tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), + ]) + + let result = await processor.process( + css` + .custom-css { + color: red; + } + `, + { from: inputCssFilePath() }, + ) + + // `fixtures/example-project` includes an `underline` candidate. But since we + // didn't use `@tailwind utilities` we didn't scan for utilities. + expect(result.css).not.toContain('.underline {') + + expect(result.css.trim()).toMatchInlineSnapshot(` + ".custom-css { + color: red; + }" + `) +}) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 73d3f55bfd84..6720dfea0fd8 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -1,8 +1,8 @@ import QuickLRU from '@alloc/quick-lru' -import { compile, env } from '@tailwindcss/node' +import { compile, env, Features } from '@tailwindcss/node' import { clearRequireCache } from '@tailwindcss/node/require-cache' import { Scanner } from '@tailwindcss/oxide' -import { Features, transform } from 'lightningcss' +import { Features as LightningCssFeatures, transform } from 'lightningcss' import fs from 'node:fs' import path from 'node:path' import postcss, { type AcceptedPlugin, type PluginCreator } from 'postcss' @@ -63,7 +63,9 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { async function createCompiler() { env.DEBUG && console.time('[@tailwindcss/postcss] Setup compiler') - clearRequireCache(context.fullRebuildPaths) + if (context.fullRebuildPaths.length > 0 && !isInitialBuild) { + clearRequireCache(context.fullRebuildPaths) + } context.fullRebuildPaths = [] @@ -86,6 +88,10 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { // guarantee a `build()` function is available. context.compiler ??= await createCompiler() + if (context.compiler.features === Features.None) { + return + } + let rebuildStrategy: 'full' | 'incremental' = 'incremental' // Track file modification times to CSS files @@ -154,46 +160,49 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } env.DEBUG && console.time('[@tailwindcss/postcss] Scan for candidates') - let candidates = context.scanner.scan() + let candidates = + context.compiler.features & Features.Utilities ? context.scanner.scan() : [] env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Scan for candidates') - // Add all found files as direct dependencies - for (let file of context.scanner.files) { - result.messages.push({ - type: 'dependency', - plugin: '@tailwindcss/postcss', - file, - parent: result.opts.from, - }) - } - - // Register dependencies so changes in `base` cause a rebuild while - // giving tools like Vite or Parcel a glob that can be used to limit - // the files that cause a rebuild to only those that match it. - for (let { base: globBase, pattern } of context.scanner.globs) { - // Avoid adding a dependency on the base directory itself, since it - // causes Next.js to start an endless recursion if the `distDir` is - // configured to anything other than the default `.next` dir. - if (pattern === '*' && base === globBase) { - continue - } - - if (pattern === '') { + if (context.compiler.features & Features.Utilities) { + // Add all found files as direct dependencies + for (let file of context.scanner.files) { result.messages.push({ type: 'dependency', plugin: '@tailwindcss/postcss', - file: globBase, - parent: result.opts.from, - }) - } else { - result.messages.push({ - type: 'dir-dependency', - plugin: '@tailwindcss/postcss', - dir: globBase, - glob: pattern, + file, parent: result.opts.from, }) } + + // Register dependencies so changes in `base` cause a rebuild while + // giving tools like Vite or Parcel a glob that can be used to limit + // the files that cause a rebuild to only those that match it. + for (let { base: globBase, pattern } of context.scanner.globs) { + // Avoid adding a dependency on the base directory itself, since it + // causes Next.js to start an endless recursion if the `distDir` is + // configured to anything other than the default `.next` dir. + if (pattern === '*' && base === globBase) { + continue + } + + if (pattern === '') { + result.messages.push({ + type: 'dependency', + plugin: '@tailwindcss/postcss', + file: globBase, + parent: result.opts.from, + }) + } else { + result.messages.push({ + type: 'dir-dependency', + plugin: '@tailwindcss/postcss', + dir: globBase, + glob: pattern, + parent: result.opts.from, + }) + } + } } env.DEBUG && console.time('[@tailwindcss/postcss] Build CSS') @@ -237,8 +246,8 @@ function optimizeCss( nonStandard: { deepSelectorCombinator: true, }, - include: Features.Nesting, - exclude: Features.LogicalProperties, + include: LightningCssFeatures.Nesting, + exclude: LightningCssFeatures.LogicalProperties, targets: { safari: (16 << 16) | (4 << 8), ios_saf: (16 << 16) | (4 << 8), diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.ts b/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.ts index c589e6950d6f..b83e5add16e8 100644 --- a/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.ts +++ b/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.ts @@ -13,7 +13,7 @@ import { toKeyPath } from '../../../../tailwindcss/src/utils/to-key-path' import * as ValueParser from '../../../../tailwindcss/src/value-parser' import { printCandidate } from '../candidates' -export enum Convert { +export const enum Convert { All = 0, MigrateModifier = 1 << 0, MigrateThemeOnly = 1 << 1, diff --git a/packages/@tailwindcss-upgrade/src/utils/walk.ts b/packages/@tailwindcss-upgrade/src/utils/walk.ts index 4f34b13a09a4..f94bdebb48d6 100644 --- a/packages/@tailwindcss-upgrade/src/utils/walk.ts +++ b/packages/@tailwindcss-upgrade/src/utils/walk.ts @@ -1,4 +1,4 @@ -export enum WalkAction { +export const enum WalkAction { // Continue walking the tree. Default behavior. Continue, diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index e14b84cf7f2d..cc89775eae32 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -1,7 +1,7 @@ -import { compile, env, normalizePath } from '@tailwindcss/node' +import { compile, env, Features, normalizePath } from '@tailwindcss/node' import { clearRequireCache } from '@tailwindcss/node/require-cache' import { Scanner } from '@tailwindcss/oxide' -import { Features, transform } from 'lightningcss' +import { Features as LightningCssFeatures, transform } from 'lightningcss' import fs from 'node:fs/promises' import path from 'node:path' import { sveltePreprocess } from 'svelte-preprocess' @@ -360,8 +360,8 @@ function optimizeCss( nonStandard: { deepSelectorCombinator: true, }, - include: Features.Nesting, - exclude: Features.LogicalProperties, + include: LightningCssFeatures.Nesting, + exclude: LightningCssFeatures.LogicalProperties, targets: { safari: (16 << 16) | (4 << 8), ios_saf: (16 << 16) | (4 << 8), @@ -497,7 +497,16 @@ class Root { this.scanner = new Scanner({ sources }) } - if (!this.overwriteCandidates) { + if ( + !( + this.compiler.features & + (Features.AtApply | Features.JsPluginCompat | Features.ThemeFunction | Features.Utilities) + ) + ) { + return false + } + + if (!this.overwriteCandidates || this.compiler.features & Features.Utilities) { // This should not be here, but right now the Vite plugin is setup where we // setup a new scanner and compiler every time we request the CSS file // (regardless whether it actually changed or not). @@ -508,44 +517,46 @@ class Root { env.DEBUG && console.timeEnd('[@tailwindcss/vite] Scan for candidates') } - // Watch individual files found via custom `@source` paths - for (let file of this.scanner.files) { - addWatchFile(file) - } - - // Watch globs found via custom `@source` paths - for (let glob of this.scanner.globs) { - if (glob.pattern[0] === '!') continue - - let relative = path.relative(this.base, glob.base) - if (relative[0] !== '.') { - relative = './' + relative + if (this.compiler.features & Features.Utilities) { + // Watch individual files found via custom `@source` paths + for (let file of this.scanner.files) { + addWatchFile(file) } - // Ensure relative is a posix style path since we will merge it with the - // glob. - relative = normalizePath(relative) - addWatchFile(path.posix.join(relative, glob.pattern)) + // Watch globs found via custom `@source` paths + for (let glob of this.scanner.globs) { + if (glob.pattern[0] === '!') continue - let root = this.compiler.root + let relative = path.relative(this.base, glob.base) + if (relative[0] !== '.') { + relative = './' + relative + } + // Ensure relative is a posix style path since we will merge it with the + // glob. + relative = normalizePath(relative) + + addWatchFile(path.posix.join(relative, glob.pattern)) - if (root !== 'none' && root !== null) { - let basePath = normalizePath(path.resolve(root.base, root.pattern)) + let root = this.compiler.root - let isDir = await fs.stat(basePath).then( - (stats) => stats.isDirectory(), - () => false, - ) + if (root !== 'none' && root !== null) { + let basePath = normalizePath(path.resolve(root.base, root.pattern)) - if (!isDir) { - throw new Error( - `The path given to \`source(…)\` must be a directory but got \`source(${basePath})\` instead.`, + let isDir = await fs.stat(basePath).then( + (stats) => stats.isDirectory(), + () => false, ) - } - this.basePath = basePath - } else if (root === null) { - this.basePath = null + if (!isDir) { + throw new Error( + `The path given to \`source(…)\` must be a directory but got \`source(${basePath})\` instead.`, + ) + } + + this.basePath = basePath + } else if (root === null) { + this.basePath = null + } } } diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts index 90e7478c23a0..7af9cf10d9ff 100644 --- a/packages/tailwindcss/src/apply.ts +++ b/packages/tailwindcss/src/apply.ts @@ -1,9 +1,11 @@ +import { Features } from '.' import { walk, WalkAction, type AstNode } from './ast' import { compileCandidates } from './compile' import type { DesignSystem } from './design-system' import { escape } from './utils/escape' export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { + let features = Features.None walk(ast, (node, { replaceWith }) => { if (node.kind !== 'at-rule') return @@ -18,6 +20,7 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { } if (node.name !== '@apply') return + features |= Features.AtApply let candidates = node.params.split(/\s+/g) @@ -75,4 +78,5 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { replaceWith(newNodes) } }) + return features } diff --git a/packages/tailwindcss/src/ast.test.ts b/packages/tailwindcss/src/ast.test.ts index 1c1f162c9256..97f4a72aa253 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 { context, decl, styleRule, toCss, walk } from './ast' +import { context, decl, styleRule, toCss, walk, WalkAction } from './ast' import * as CSS from './css-parser' it('should pretty print an AST', () => { @@ -64,3 +64,33 @@ it('allows the placement of context nodes', () => { " `) }) + +it('should stop walking when returning `WalkAction.Stop`', () => { + let ast = [ + styleRule('.foo', [styleRule('.nested', [styleRule('.bail', [decl('color', 'red')])])]), + styleRule('.bar'), + styleRule('.baz'), + styleRule('.qux'), + ] + + let seen = new Set() + + walk(ast, (node) => { + if (node.kind === 'rule') { + seen.add(node.selector) + } + + if (node.kind === 'rule' && node.selector === '.bail') { + return WalkAction.Stop + } + }) + + // We do not want to see `.bar`, `.baz`, or `.qux` because we bailed early + expect(seen).toMatchInlineSnapshot(` + Set { + ".foo", + ".nested", + ".bail", + } + `) +}) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 7315bb09bfe0..5245db2df0d1 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -97,7 +97,7 @@ export function atRoot(nodes: AstNode[]): AtRoot { } } -export enum WalkAction { +export const enum WalkAction { /** Continue walking, which is the default */ Continue, @@ -131,7 +131,11 @@ export function walk( // 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, parentPath, { ...context, ...node.context }) + if ( + walk(node.nodes, visit, parentPath, { ...context, ...node.context }) === WalkAction.Stop + ) { + return WalkAction.Stop + } continue } @@ -150,13 +154,15 @@ export function walk( }) ?? WalkAction.Continue // Stop the walk entirely - if (status === WalkAction.Stop) return + if (status === WalkAction.Stop) return WalkAction.Stop // Skip visiting the children of this node if (status === WalkAction.Skip) continue if (node.kind === 'rule' || node.kind === 'at-rule') { - walk(node.nodes, visit, path, context) + if (walk(node.nodes, visit, path, context) === WalkAction.Stop) { + return WalkAction.Stop + } } } } diff --git a/packages/tailwindcss/src/at-import.test.ts b/packages/tailwindcss/src/at-import.test.ts index 6686dbe49ec2..dafb8af07fd5 100644 --- a/packages/tailwindcss/src/at-import.test.ts +++ b/packages/tailwindcss/src/at-import.test.ts @@ -1,10 +1,11 @@ +import dedent from 'dedent' 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' -const css = String.raw +const css = dedent async function run( css: string, @@ -161,10 +162,7 @@ test('url() imports are passed-through', async () => { `, { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, ), - ).resolves.toMatchInlineSnapshot(` - "@import url('example.css'); - " - `) + ).resolves.toMatchInlineSnapshot(`"@import url('example.css');"`) await expect( run( @@ -173,10 +171,7 @@ test('url() imports are passed-through', async () => { `, { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, ), - ).resolves.toMatchInlineSnapshot(` - "@import url('./example.css'); - " - `) + ).resolves.toMatchInlineSnapshot(`"@import url('./example.css');"`) await expect( run( @@ -185,10 +180,7 @@ test('url() imports are passed-through', async () => { `, { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, ), - ).resolves.toMatchInlineSnapshot(` - "@import url('/example.css'); - " - `) + ).resolves.toMatchInlineSnapshot(`"@import url('/example.css');"`) await expect( run( @@ -197,10 +189,7 @@ test('url() imports are passed-through', async () => { `, { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, ), - ).resolves.toMatchInlineSnapshot(` - "@import url(example.css); - " - `) + ).resolves.toMatchInlineSnapshot(`"@import url(example.css);"`) await expect( run( @@ -209,10 +198,7 @@ test('url() imports are passed-through', async () => { `, { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, ), - ).resolves.toMatchInlineSnapshot(` - "@import url(./example.css); - " - `) + ).resolves.toMatchInlineSnapshot(`"@import url(./example.css);"`) await expect( run( @@ -221,10 +207,7 @@ test('url() imports are passed-through', async () => { `, { loadStylesheet: () => Promise.reject(new Error('Unexpected stylesheet')), optimize: false }, ), - ).resolves.toMatchInlineSnapshot(` - "@import url(/example.css); - " - `) + ).resolves.toMatchInlineSnapshot(`"@import url(/example.css);"`) }) test('handles case-insensitive @import directive', async () => { diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts index 75b2f7b594f8..4a585202d531 100644 --- a/packages/tailwindcss/src/at-import.ts +++ b/packages/tailwindcss/src/at-import.ts @@ -1,3 +1,4 @@ +import { Features } from '.' import { atRule, context, walk, WalkAction, type AstNode } from './ast' import * as CSS from './css-parser' import * as ValueParser from './value-parser' @@ -10,6 +11,7 @@ export async function substituteAtImports( loadStylesheet: LoadStylesheet, recurseCount = 0, ) { + let features = Features.None let promises: Promise[] = [] walk(ast, (node, { replaceWith }) => { @@ -17,6 +19,8 @@ export async function substituteAtImports( let parsed = parseImportParams(ValueParser.parse(node.params)) if (parsed === null) return + features |= Features.AtImport + let { uri, layer, media, supports } = parsed // Skip importing data or remote URIs @@ -58,7 +62,11 @@ export async function substituteAtImports( } }) - await Promise.all(promises) + if (promises.length > 0) { + await Promise.all(promises) + } + + return features } // Modified and inlined version of `parse-statements` from diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index e53a253ee8be..841a1c2e2860 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -1,3 +1,4 @@ +import { Features } from '..' import { styleRule, toCss, walk, WalkAction, type AstNode } from '../ast' import type { DesignSystem } from '../design-system' import { segment } from '../utils/segment' @@ -32,6 +33,7 @@ export async function applyCompatibilityHooks({ ) => Promise<{ module: any; base: string }> globs: { origin?: string; pattern: string }[] }) { + let features = Features.None let pluginPaths: [{ id: string; base: string }, CssPluginOptions | null][] = [] let configPaths: { id: string; base: string }[] = [] @@ -98,6 +100,7 @@ export async function applyCompatibilityHooks({ ]) replaceWith([]) + features |= Features.JsPluginCompat return } @@ -113,6 +116,7 @@ export async function applyCompatibilityHooks({ configPaths.push({ id: node.params.slice(1, -1), base: context.base }) replaceWith([]) + features |= Features.JsPluginCompat return } }) @@ -132,7 +136,7 @@ export async function applyCompatibilityHooks({ // If the theme value is not found in the simple resolver, we upgrade to the full backward // compatibility support implementation of the `resolveThemeValue` function. - upgradeToFullPluginSupport({ + features |= upgradeToFullPluginSupport({ designSystem, base, ast, @@ -145,7 +149,7 @@ export async function applyCompatibilityHooks({ // If there are no plugins or configs registered, we don't need to register // any additional backwards compatibility hooks. - if (!pluginPaths.length && !configPaths.length) return + if (!pluginPaths.length && !configPaths.length) return Features.None let [configs, pluginDetails] = await Promise.all([ Promise.all( @@ -171,7 +175,7 @@ export async function applyCompatibilityHooks({ ), ]) - upgradeToFullPluginSupport({ + features |= upgradeToFullPluginSupport({ designSystem, base, ast, @@ -179,6 +183,8 @@ export async function applyCompatibilityHooks({ configs, pluginDetails, }) + + return features } function upgradeToFullPluginSupport({ @@ -205,6 +211,7 @@ function upgradeToFullPluginSupport({ options: CssPluginOptions | null }[] }) { + let features = Features.None let pluginConfigs = pluginDetails.map((detail) => { if (!detail.options) { return { config: { plugins: [detail.plugin] }, base: detail.base } @@ -229,7 +236,11 @@ function upgradeToFullPluginSupport({ userConfig, ) - let pluginApi = buildPluginApi(designSystem, ast, resolvedConfig) + let pluginApi = buildPluginApi(designSystem, ast, resolvedConfig, { + set current(value: number) { + features |= value + }, + }) for (let { handler } of resolvedConfig.plugins) { handler(pluginApi) @@ -323,4 +334,5 @@ function upgradeToFullPluginSupport({ globs.push(file) } + return features } diff --git a/packages/tailwindcss/src/compat/apply-config-to-theme.ts b/packages/tailwindcss/src/compat/apply-config-to-theme.ts index f833d8a66de5..ad46e3b3544e 100644 --- a/packages/tailwindcss/src/compat/apply-config-to-theme.ts +++ b/packages/tailwindcss/src/compat/apply-config-to-theme.ts @@ -213,7 +213,7 @@ function isValidThemeTuple(value: unknown): value is [string, Record { let clonedAst = structuredClone(ast) - substituteAtApply(clonedAst, designSystem) + featuresRef.current |= substituteAtApply(clonedAst, designSystem) return clonedAst }) } @@ -382,7 +384,7 @@ export function buildPluginApi( } let ast = objectToAst(fn(value, { modifier })) - substituteAtApply(ast, designSystem) + featuresRef.current |= substituteAtApply(ast, designSystem) return ast } } diff --git a/packages/tailwindcss/src/compat/selector-parser.ts b/packages/tailwindcss/src/compat/selector-parser.ts index f2ff408d9294..7dc0b82f8f1a 100644 --- a/packages/tailwindcss/src/compat/selector-parser.ts +++ b/packages/tailwindcss/src/compat/selector-parser.ts @@ -68,7 +68,7 @@ function value(value: string): SelectorValueNode { } } -export enum SelectorWalkAction { +export const enum SelectorWalkAction { /** Continue walking, which is the default */ Continue, @@ -105,13 +105,15 @@ export function walk( }) ?? SelectorWalkAction.Continue // Stop the walk entirely - if (status === SelectorWalkAction.Stop) return + if (status === SelectorWalkAction.Stop) return SelectorWalkAction.Stop // Skip visiting the children of this node if (status === SelectorWalkAction.Skip) continue if (node.kind === 'function') { - walk(node.nodes, visit, node) + if (walk(node.nodes, visit, node) === SelectorWalkAction.Stop) { + return SelectorWalkAction.Stop + } } } } diff --git a/packages/tailwindcss/src/css-functions.ts b/packages/tailwindcss/src/css-functions.ts index e55405ddc8a7..fe259a0a2ed6 100644 --- a/packages/tailwindcss/src/css-functions.ts +++ b/packages/tailwindcss/src/css-functions.ts @@ -1,3 +1,4 @@ +import { Features } from '.' import { walk, type AstNode } from './ast' import * as ValueParser from './value-parser' import { type ValueAstNode } from './value-parser' @@ -7,9 +8,11 @@ export const THEME_FUNCTION_INVOCATION = 'theme(' type ResolveThemeValue = (path: string) => string | undefined export function substituteFunctions(ast: AstNode[], resolveThemeValue: ResolveThemeValue) { + let features = Features.None walk(ast, (node) => { // Find all declaration values if (node.kind === 'declaration' && node.value?.includes(THEME_FUNCTION_INVOCATION)) { + features |= Features.ThemeFunction node.value = substituteFunctionsInValue(node.value, resolveThemeValue) return } @@ -23,10 +26,12 @@ export function substituteFunctions(ast: AstNode[], resolveThemeValue: ResolveTh node.name === '@supports') && node.params.includes(THEME_FUNCTION_INVOCATION) ) { + features |= Features.ThemeFunction node.params = substituteFunctionsInValue(node.params, resolveThemeValue) } } }) + return features } export function substituteFunctionsInValue( diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 04e9bb99d9e4..9678cc592020 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -69,6 +69,35 @@ function parseThemeOptions(params: string) { return [options, prefix] as const } +type Root = + // Unknown root + | null + + // Explicitly no root specified via `source(none)` + | 'none' + + // Specified via `source(…)`, relative to the `base` + | { base: string; pattern: string } + +export const enum Features { + None = 0, + + // `@apply` was used + AtApply = 1 << 0, + + // `@import` was used + AtImport = 1 << 1, + + // `@plugin` or `@config` was used + JsPluginCompat = 1 << 2, + + // `theme(…)` was used + ThemeFunction = 1 << 3, + + // `@tailwind utilities` was used + Utilities = 1 << 4, +} + async function parseCss( css: string, { @@ -77,9 +106,10 @@ async function parseCss( loadStylesheet = throwOnLoadStylesheet, }: CompileOptions = {}, ) { + let features = Features.None let ast = [contextNode({ base }, CSS.parse(css))] as AstNode[] - await substituteAtImports(ast, base, loadStylesheet) + features |= await substituteAtImports(ast, base, loadStylesheet) let important = null as boolean | null let theme = new Theme() @@ -88,11 +118,7 @@ async function parseCss( let firstThemeRule = null as StyleRule | null let utilitiesNode = null as AtRule | null let globs: { base: string; pattern: string }[] = [] - let root: - | null // Unknown root - | 'none' // Explicitly no root specified via `source(none)` - // Specified via `source(…)`, relative to the `base` - | { base: string; pattern: string } = null + let root = null as Root // Handle at-rules walk(ast, (node, { parent, replaceWith, context }) => { @@ -138,6 +164,7 @@ async function parseCss( } utilitiesNode = node + features |= Features.Utilities } // Collect custom `@utility` at-rules @@ -414,7 +441,13 @@ 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, base, ast, loadModule, globs }) + features |= await applyCompatibilityHooks({ + designSystem, + base, + ast, + loadModule, + globs, + }) for (let customVariant of customVariants) { customVariant(designSystem) @@ -464,9 +497,9 @@ async function parseCss( } // Replace `@apply` rules with the actual utility classes. - substituteAtApply(ast, designSystem) + features |= substituteAtApply(ast, designSystem) - substituteFunctions(ast, designSystem.resolveThemeValue) + features |= substituteFunctions(ast, designSystem.resolveThemeValue) // Remove `@utility`, we couldn't replace it before yet because we had to // handle the nested `@apply` at-rules first. @@ -488,6 +521,7 @@ async function parseCss( globs, root, utilitiesNode, + features, } } @@ -496,13 +530,11 @@ export async function compile( opts: CompileOptions = {}, ): Promise<{ globs: { base: string; pattern: string }[] - root: - | null // Unknown root - | 'none' // Explicitly no root specified via `source(none)` - | { base: string; pattern: string } // Specified via `source(…)`, relative to the `base` + root: Root + features: Features build(candidates: string[]): string }> { - let { designSystem, ast, globs, root, utilitiesNode } = await parseCss(css, opts) + let { designSystem, ast, globs, root, utilitiesNode, features } = await parseCss(css, opts) if (process.env.NODE_ENV !== 'test') { ast.unshift(comment(`! tailwindcss v${version} | MIT License | https://tailwindcss.com `)) @@ -517,12 +549,13 @@ export async function compile( // resulted in a generated AST Node. All the other `rawCandidates` are invalid // and should be ignored. let allValidCandidates = new Set() - let compiledCss = toCss(ast) + let compiledCss = features !== Features.None ? toCss(ast) : css let previousAstNodeCount = 0 return { globs, root, + features, build(newRawCandidates: string[]) { let didChange = false diff --git a/packages/tailwindcss/src/value-parser.ts b/packages/tailwindcss/src/value-parser.ts index 18ee3e404747..d51b793cb254 100644 --- a/packages/tailwindcss/src/value-parser.ts +++ b/packages/tailwindcss/src/value-parser.ts @@ -39,7 +39,7 @@ function separator(value: string): ValueSeparatorNode { } } -export enum ValueWalkAction { +export const enum ValueWalkAction { /** Continue walking, which is the default */ Continue, @@ -76,13 +76,15 @@ export function walk( }) ?? ValueWalkAction.Continue // Stop the walk entirely - if (status === ValueWalkAction.Stop) return + if (status === ValueWalkAction.Stop) return ValueWalkAction.Stop // Skip visiting the children of this node if (status === ValueWalkAction.Skip) continue if (node.kind === 'function') { - walk(node.nodes, visit, node) + if (walk(node.nodes, visit, node) === ValueWalkAction.Stop) { + return ValueWalkAction.Stop + } } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0acd31faa44c..1dc5628b24ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -204,6 +204,9 @@ importers: '@types/postcss-import': specifier: 14.0.3 version: 14.0.3 + dedent: + specifier: 1.5.3 + version: 1.5.3 internal-example-plugin: specifier: workspace:* version: link:../internal-example-plugin @@ -1477,11 +1480,13 @@ packages: '@parcel/watcher-darwin-arm64@2.5.0': resolution: {integrity: sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==} engines: {node: '>= 10.0.0'} + cpu: [arm64] os: [darwin] '@parcel/watcher-darwin-x64@2.5.0': resolution: {integrity: sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==} engines: {node: '>= 10.0.0'} + cpu: [x64] os: [darwin] '@parcel/watcher-freebsd-x64@2.5.0': @@ -1505,21 +1510,25 @@ packages: '@parcel/watcher-linux-arm64-glibc@2.5.0': resolution: {integrity: sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==} engines: {node: '>= 10.0.0'} + cpu: [arm64] os: [linux] '@parcel/watcher-linux-arm64-musl@2.5.0': resolution: {integrity: sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==} engines: {node: '>= 10.0.0'} + cpu: [arm64] os: [linux] '@parcel/watcher-linux-x64-glibc@2.5.0': resolution: {integrity: sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==} engines: {node: '>= 10.0.0'} + cpu: [x64] os: [linux] '@parcel/watcher-linux-x64-musl@2.5.0': resolution: {integrity: sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==} engines: {node: '>= 10.0.0'} + cpu: [x64] os: [linux] '@parcel/watcher-win32-arm64@2.5.0': @@ -1537,6 +1546,7 @@ packages: '@parcel/watcher-win32-x64@2.5.0': resolution: {integrity: sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==} engines: {node: '>= 10.0.0'} + cpu: [x64] os: [win32] '@parcel/watcher@2.5.0': @@ -2032,6 +2042,7 @@ packages: bun@1.1.29: resolution: {integrity: sha512-SKhpyKNZtgxrVel9ec9xon3LDv8mgpiuFhARgcJo1YIbggY2PBrKHRNiwQ6Qlb+x3ivmRurfuwWgwGexjpgBRg==} + cpu: [arm64, x64] os: [darwin, linux, win32] hasBin: true @@ -2854,11 +2865,13 @@ 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: @@ -2876,21 +2889,25 @@ 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: @@ -2902,6 +2919,7 @@ 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: @@ -5685,7 +5703,7 @@ snapshots: eslint: 9.15.0(jiti@2.4.0) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.15.0(jiti@2.4.0)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.5.4))(eslint@9.15.0(jiti@2.4.0)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@9.15.0(jiti@2.4.0)) eslint-plugin-jsx-a11y: 6.10.1(eslint@9.15.0(jiti@2.4.0)) eslint-plugin-react: 7.37.2(eslint@9.15.0(jiti@2.4.0)) eslint-plugin-react-hooks: 5.0.0(eslint@9.15.0(jiti@2.4.0)) @@ -5705,7 +5723,7 @@ snapshots: eslint: 9.15.0(jiti@2.4.0) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.15.0(jiti@2.4.0)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.15.0(jiti@2.4.0)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.6.3))(eslint@9.15.0(jiti@2.4.0)) eslint-plugin-jsx-a11y: 6.10.1(eslint@9.15.0(jiti@2.4.0)) eslint-plugin-react: 7.37.2(eslint@9.15.0(jiti@2.4.0)) eslint-plugin-react-hooks: 5.0.0(eslint@9.15.0(jiti@2.4.0)) @@ -5730,13 +5748,13 @@ snapshots: debug: 4.3.7 enhanced-resolve: 5.17.1 eslint: 9.15.0(jiti@2.4.0) - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.15.0(jiti@2.4.0)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.15.0(jiti@2.4.0)))(eslint@9.15.0(jiti@2.4.0)) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.5.4))(eslint@9.15.0(jiti@2.4.0)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@9.15.0(jiti@2.4.0)) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node @@ -5749,20 +5767,20 @@ snapshots: debug: 4.3.7 enhanced-resolve: 5.17.1 eslint: 9.15.0(jiti@2.4.0) - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.15.0(jiti@2.4.0)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.15.0(jiti@2.4.0)))(eslint@9.15.0(jiti@2.4.0)) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.15.0(jiti@2.4.0)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.6.3))(eslint@9.15.0(jiti@2.4.0)) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.15.0(jiti@2.4.0)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.15.0(jiti@2.4.0)))(eslint@9.15.0(jiti@2.4.0)): dependencies: debug: 3.2.7 optionalDependencies: @@ -5773,7 +5791,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.15.0(jiti@2.4.0)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.15.0(jiti@2.4.0)))(eslint@9.15.0(jiti@2.4.0)): dependencies: debug: 3.2.7 optionalDependencies: @@ -5784,7 +5802,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.5.4))(eslint@9.15.0(jiti@2.4.0)): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@9.15.0(jiti@2.4.0)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -5795,7 +5813,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.15.0(jiti@2.4.0) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.15.0(jiti@2.4.0)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.15.0(jiti@2.4.0)))(eslint@9.15.0(jiti@2.4.0)) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -5813,7 +5831,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.15.0(jiti@2.4.0)): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.6.3))(eslint@9.15.0(jiti@2.4.0)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -5824,7 +5842,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.15.0(jiti@2.4.0) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.15.0(jiti@2.4.0)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.15.0(jiti@2.4.0))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.15.0(jiti@2.4.0)))(eslint@9.15.0(jiti@2.4.0)) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3