diff --git a/CHANGELOG.md b/CHANGELOG.md index fee74c24c..2a75ef940 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 a7aaf3e64..f7e353751 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 eb287ae82..d11771290 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 eb42d84be..9588f21ab 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 345590271..02ab23bea 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 73d3f55bf..6720dfea0 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 c589e6950..b83e5add1 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 4f34b13a0..f94bdebb4 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 e14b84cf7..cc89775ea 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 - - if (root !== 'none' && root !== null) { - let basePath = normalizePath(path.resolve(root.base, root.pattern)) - - let isDir = await fs.stat(basePath).then( - (stats) => stats.isDirectory(), - () => false, - ) - - if (!isDir) { - throw new Error( - `The path given to \`source(…)\` must be a directory but got \`source(${basePath})\` instead.`, - ) + 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) - this.basePath = basePath - } else if (root === null) { - this.basePath = null + addWatchFile(path.posix.join(relative, glob.pattern)) + + let root = this.compiler.root + + if (root !== 'none' && root !== null) { + let basePath = normalizePath(path.resolve(root.base, root.pattern)) + + let isDir = await fs.stat(basePath).then( + (stats) => stats.isDirectory(), + () => false, + ) + + 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 90e7478c2..7af9cf10d 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 1c1f162c9..97f4a72aa 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 7315bb09b..5245db2df 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 6686dbe49..dafb8af07 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 75b2f7b59..4a585202d 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 e53a253ee..841a1c2e2 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 f833d8a66..ad46e3b35 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 f2ff408d9..7dc0b82f8 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 e55405ddc..fe259a0a2 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 04e9bb99d..9678cc592 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 18ee3e404..d51b793cb 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 0acd31faa..1dc5628b2 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