Improve performance of @tailwindcss/postcss and @tailwindcss/vite (#15226)

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 <adam.wathan@gmail.com>
This commit is contained in:
Robin Malfait 2024-11-29 16:59:29 +01:00 committed by GitHub
parent 6abd8086c3
commit 99b73ee368
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 325 additions and 165 deletions

View File

@ -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

View File

@ -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<string | false | undefined>
export async function compile(

View File

@ -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 }

View File

@ -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"
}

View File

@ -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;
}"
`)
})

View File

@ -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),

View File

@ -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,

View File

@ -1,4 +1,4 @@
export enum WalkAction {
export const enum WalkAction {
// Continue walking the tree. Default behavior.
Continue,

View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -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",
}
`)
})

View File

@ -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
}
}
}
}

View File

@ -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 () => {

View File

@ -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<void>[] = []
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

View File

@ -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
}

View File

@ -213,7 +213,7 @@ function isValidThemeTuple(value: unknown): value is [string, Record<string, str
return true
}
enum WalkAction {
const enum WalkAction {
/** Continue walking, which is the default */
Continue,
@ -241,10 +241,12 @@ function walk(
let result = callback(value, keyPath) ?? WalkAction.Continue
if (result === WalkAction.Skip) continue
if (result === WalkAction.Stop) break
if (result === WalkAction.Stop) return WalkAction.Stop
if (!Array.isArray(value) && typeof value !== 'object') continue
walk(value as any, keyPath, callback)
if (walk(value as any, keyPath, callback) === WalkAction.Stop) {
return WalkAction.Stop
}
}
}

View File

@ -1,3 +1,4 @@
import type { Features } from '..'
import { substituteAtApply } from '../apply'
import { atRule, decl, rule, walk, type AstNode } from '../ast'
import type { Candidate, CandidateModifier, NamedUtilityValue } from '../candidate'
@ -83,11 +84,12 @@ export function buildPluginApi(
designSystem: DesignSystem,
ast: AstNode[],
resolvedConfig: ResolvedConfig,
featuresRef: { current: Features },
): PluginAPI {
let api: PluginAPI = {
addBase(css) {
let baseNodes = objectToAst(css)
substituteFunctions(baseNodes, api.theme)
featuresRef.current |= substituteFunctions(baseNodes, api.theme)
ast.push(atRule('@layer', 'base', baseNodes))
},
@ -260,7 +262,7 @@ export function buildPluginApi(
designSystem.utilities.static(className, () => {
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
}
}

View File

@ -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
}
}
}
}

View File

@ -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(

View File

@ -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<string>()
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

View File

@ -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
}
}
}
}

42
pnpm-lock.yaml generated
View File

@ -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