Add tailwindcss canonicalize sub-command (#19783)

This PR adds a new `tailwindcss canonicalize` sub-command.
This commit is contained in:
Robin Malfait 2026-03-11 20:30:21 +01:00 • committed by GitHub
parent d5717f2307
commit e96909accd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 505 additions and 67 deletions

View file

@ -0,0 +1,134 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, test } from 'vitest'
import { runCommandLine } from '.'
import { normalizeWindowsSeparators } from '../../utils/test-helpers'
let css = normalizeWindowsSeparators(
path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'fixtures/input.css'),
)
describe('runCommandLine', () => {
test('canonicalizes, collapses, and sorts candidate groups from positional arguments', async () => {
let result = await runCommandLine({
argv: ['--css', css, 'py-3 p-1 px-3'],
stdinIsTTY: true,
stdoutIsTTY: false,
})
expect(result).toEqual({
exitCode: 0,
stdout: 'p-3',
stderr: '',
})
})
test('falls back to the default tailwind import when --css is omitted', async () => {
let result = await runCommandLine({
argv: ['py-3 p-1 px-3'],
cwd: path.dirname(css),
stdinIsTTY: true,
stdoutIsTTY: false,
})
expect(result).toEqual({
exitCode: 0,
stdout: 'p-3',
stderr: '',
})
})
test('canonicalizes, collapses, and sorts multiple groups from stdin lines', async () => {
let result = await runCommandLine({
argv: ['--css', css],
stdin: '[display:_flex_] py-3 p-1 px-3\nmt-2 mr-2 mb-2 ml-2 focus:hover:p-3 hover:p-1 py-3\n',
stdinIsTTY: false,
stdoutIsTTY: false,
})
expect(result).toEqual({
exitCode: 0,
stdout: 'flex p-3\nm-2 py-3 hover:p-1 focus:hover:p-3',
stderr: '',
})
})
test('collapses equivalent candidates', async () => {
let result = await runCommandLine({
argv: ['--css', css, 'mt-2 mr-2 mb-2 ml-2'],
stdinIsTTY: true,
stdoutIsTTY: false,
})
expect(result).toEqual({
exitCode: 0,
stdout: 'm-2',
stderr: '',
})
})
test('renders json output for processed candidate groups', async () => {
let result = await runCommandLine({
argv: ['--css', css, '--format', 'json', 'py-3 p-1 px-3'],
stdinIsTTY: true,
stdoutIsTTY: false,
})
expect(result.exitCode).toBe(0)
expect(JSON.parse(result.stdout)).toEqual([
{
input: 'py-3 p-1 px-3',
output: 'p-3',
changed: true,
},
])
expect(result.stderr).toBe('')
})
test('splits candidate lists with segment-aware spacing', async () => {
let result = await runCommandLine({
argv: ['--css', css, '--format', 'json', "content-['hello world'] p-1"],
stdinIsTTY: true,
stdoutIsTTY: false,
})
expect(result.exitCode).toBe(0)
expect(JSON.parse(result.stdout)).toEqual([
{
input: "content-['hello world'] p-1",
output: "p-1 content-['hello_world']",
changed: true,
},
])
expect(result.stderr).toBe('')
})
test('shows a usage error when no candidate groups are provided', async () => {
let result = await runCommandLine({
argv: ['--css', css],
stdinIsTTY: true,
stdoutIsTTY: false,
})
expect(result.exitCode).toBe(1)
expect(result.stderr).toBe('No candidate groups provided')
expect(result.stdout).toContain('Usage:')
})
test('renders text help output', async () => {
let result = await runCommandLine({
argv: ['--help'],
stdinIsTTY: true,
stdoutIsTTY: false,
})
expect(result).toEqual({
exitCode: 0,
stdout: expect.stringContaining('Usage:'),
stderr: '',
})
expect(result.stdout).toContain('tailwindcss canonicalize [classes...]')
expect(result.stdout).toContain('Options:')
expect(result.stdout).not.toContain('markdown output')
})
})

View file

@ -0,0 +1 @@
@import 'tailwindcss';

View file

@ -0,0 +1,276 @@
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
import fs from 'node:fs/promises'
import path from 'node:path'
import { compare } from '../../../../tailwindcss/src/utils/compare'
import { segment } from '../../../../tailwindcss/src/utils/segment'
import { args, type Arg } from '../../utils/args'
import { help } from '../help'
const css = String.raw
export type OutputFormat = 'text' | 'json' | 'jsonl'
export interface CandidateGroupResult {
input: string
output: string
changed: boolean
}
export interface RunCommandLineOptions {
argv?: string[]
cwd?: string
stdin?: string | null
stdinIsTTY?: boolean
stdoutIsTTY?: boolean
}
export interface RunCommandLineResult {
exitCode: number
stdout: string
stderr: string
}
export function usage() {
return 'tailwindcss canonicalize [classes...]'
}
function usageWithCss() {
return 'tailwindcss canonicalize --css input.css [classes...]'
}
export function options() {
return {
'--css': {
type: 'string',
description:
'CSS entry file used to load the Tailwind design system (defaults to `@import "tailwindcss";`)',
},
'--format': {
type: 'string',
description: 'Output format',
default: 'text',
values: ['text', 'json', 'jsonl'],
},
} satisfies Arg
}
const sharedOptions = {
'--help': {
type: 'boolean',
description: 'Display usage information',
alias: '-h',
default: false,
},
} satisfies Arg
export async function runCommandLine({
argv = process.argv.slice(2),
cwd = process.cwd(),
stdin = null,
stdinIsTTY = process.stdin.isTTY,
stdoutIsTTY = process.stdout.isTTY,
}: RunCommandLineOptions = {}): Promise<RunCommandLineResult> {
try {
let flags = args(
{
...options(),
...sharedOptions,
},
argv,
)
if ((stdoutIsTTY && argv.length === 0) || flags['--help']) {
return {
exitCode: 0,
stdout: helpMessage(),
stderr: '',
}
}
let format = parseFormat(flags['--format'])
let inputs = flags._.length > 0 ? flags._ : await readCandidateGroups({ stdin, stdinIsTTY })
if (inputs.length === 0) {
return usageError('No candidate groups provided')
}
let results = await processCandidateGroups({
css: flags['--css'],
cwd,
inputs,
})
return {
exitCode: 0,
stdout: formatCandidateResults(results, format),
stderr: '',
}
} catch (error) {
return {
exitCode: 1,
stdout: '',
stderr: error instanceof Error ? error.message : String(error),
}
}
}
export function readCandidateGroups({
stdin,
stdinIsTTY,
}: {
stdin: string | null
stdinIsTTY: boolean
}) {
if (stdin !== null) {
return Promise.resolve(splitCandidateGroups(stdin))
}
if (stdinIsTTY) {
return Promise.resolve([])
}
return drainStdin().then(splitCandidateGroups)
}
export async function drainStdin() {
return new Promise<string>((resolve, reject) => {
let result = ''
process.stdin.on('data', (chunk) => {
result += chunk
})
process.stdin.on('end', () => resolve(result))
process.stdin.on('error', (err) => reject(err))
})
}
export async function processCandidateGroups({
css,
cwd = process.cwd(),
inputs,
}: {
css: string | null
cwd?: string
inputs: string[]
}): Promise<CandidateGroupResult[]> {
let designSystem = await loadDesignSystem(css, cwd)
return inputs.map((input) => {
let originalCandidates = splitCandidates(input)
let outputCandidates = sortCandidates(
designSystem,
designSystem.canonicalizeCandidates(originalCandidates, {
collapse: true,
logicalToPhysical: true,
}),
)
let output = outputCandidates.join(' ')
return {
input,
output,
changed: output !== input,
}
})
}
export function formatCandidateResults(results: CandidateGroupResult[], format: OutputFormat) {
switch (format) {
case 'json':
return JSON.stringify(results, null, 2)
case 'jsonl':
return results.map((result) => JSON.stringify(result)).join('\n')
case 'text':
return results.map((result) => result.output).join('\n')
}
}
function helpMessage() {
return help({
render: false,
usage: [usage(), usageWithCss()],
options: {
...options(),
...sharedOptions,
},
})
}
async function loadDesignSystem(cssFile: string | null, cwd: string) {
if (cssFile === null) {
return __unstable__loadDesignSystem(
css`
@import 'tailwindcss';
`,
{ base: cwd },
)
}
let resolvedCssFile = path.resolve(cwd, cssFile)
let content = await fs.readFile(resolvedCssFile, 'utf8')
return __unstable__loadDesignSystem(content, {
base: path.dirname(resolvedCssFile),
})
}
function splitCandidateGroups(input: string) {
return input
.split(/\r?\n/g)
.map((line) => line.trim())
.filter((line) => line.length > 0)
}
function parseFormat(input: string): OutputFormat {
if (input === 'text' || input === 'json' || input === 'jsonl') {
return input
}
throw new Error(`Invalid value for --format: ${input}`)
}
function usageError(message: string): RunCommandLineResult {
return {
exitCode: 1,
stdout: helpMessage(),
stderr: message,
}
}
function splitCandidates(input: string) {
let trimmedInput = input.trim()
if (trimmedInput.length === 0) return []
return segment(trimmedInput, ' ')
.map((candidate) => candidate.trim())
.filter((candidate) => candidate.length > 0)
}
function sortCandidates(
designSystem: Awaited<ReturnType<typeof __unstable__loadDesignSystem>>,
candidates: string[],
) {
return defaultSort(designSystem.getClassOrder(candidates))
}
function defaultSort(entries: [string, bigint | null][]) {
return entries
.slice()
.sort(([candidateA, orderA], [candidateZ, orderZ]) => {
if (orderA === orderZ) return compare(candidateA, candidateZ)
if (orderA === null) return -1
if (orderZ === null) return 1
return bigSign(orderA - orderZ) || compare(candidateA, candidateZ)
})
.map(([candidate]) => candidate)
}
function bigSign(value: bigint) {
if (value > 0n) {
return 1
} else if (value === 0n) {
return 0
} else {
return -1
}
}

View file

@ -3,30 +3,34 @@ import type { Arg } from '../../utils/args'
import { UI, header, highlight, indent, println, wordWrap } from '../../utils/renderer'
export function help({
render = true,
invalid,
usage,
options,
}: {
render?: boolean
invalid?: string
usage?: string[]
options?: Arg
}) {
// Available terminal width
let width = process.stdout.columns
let width = process.stdout.columns ?? 80
let lines: string[] = []
let writeLine = render ? println : (value = '') => void lines.push(value)
// Render header
println(header())
writeLine(header())
// Render the invalid command
if (invalid) {
println()
println(`${pc.dim('Invalid command:')} ${invalid}`)
writeLine()
writeLine(`${pc.dim('Invalid command:')} ${invalid}`)
}
// Render usage
if (usage && usage.length > 0) {
println()
println(pc.dim('Usage:'))
writeLine()
writeLine(pc.dim('Usage:'))
for (let [idx, example] of usage.entries()) {
// Split the usage example into the command and its options. This allows
// us to wrap the options based on the available width of the terminal.
@ -65,7 +69,7 @@ export function help({
// tailwindcss other [--watch] [options...]
// ```
if (lines.length > 1 && idx !== 0) {
println()
writeLine()
}
// Print the usage examples based on available width of the terminal.
@ -86,9 +90,9 @@ export function help({
// ```
//
// > Note how the second line is indented to align with the first line.
println(indent(`${command}${lines.shift()}`))
writeLine(indent(`${command}${lines.shift()}`))
for (let line of lines) {
println(indent(line, command.length))
writeLine(indent(line, command.length))
}
}
}
@ -131,8 +135,8 @@ export function help({
maxOptionLength = Math.max(maxOptionLength, option.length)
}
println()
println(pc.dim('Options:'))
writeLine()
writeLine(pc.dim('Options:'))
// The minimum amount of dots between the option and the description.
let minimumGap = 8
@ -160,15 +164,19 @@ export function help({
)
// Print the option, the spacer dots and the start of the description.
println(
writeLine(
indent(`${pc.blue(option)} ${pc.dim(pc.gray('\u00B7')).repeat(dotCount)} ${lines.shift()}`),
)
// Print the remaining lines of the description, indenting them to align
// with the start of the description.
for (let line of lines) {
println(indent(`${' '.repeat(option.length + dotCount + spaces)}${line}`))
writeLine(indent(`${' '.repeat(option.length + dotCount + spaces)}${line}`))
}
}
}
if (!render) {
return lines.join('\n')
}
}

View file

@ -3,46 +3,80 @@
import { args, type Arg } from './utils/args'
import * as build from './commands/build'
import * as canonicalize from './commands/canonicalize'
import { help } from './commands/help'
const sharedOptions = {
'--help': { type: 'boolean', description: 'Display usage information', alias: '-h' },
} satisfies Arg
const flags = args({
...build.options(),
...sharedOptions,
})
const command = flags._[0]
// Right now we don't support any sub-commands. Let's show the help message
// instead.
if (command) {
help({
invalid: command,
usage: ['tailwindcss [options]'],
options: { ...build.options(), ...sharedOptions },
})
process.exit(1)
function buildUsage(command = 'tailwindcss') {
return `${command} [--input input.css] [--output output.css] [--watch] [options…]`
}
// Display main help message if no command is being used.
//
// E.g.:
//
// - `tailwindcss` // should show the help message
//
// E.g.: implicit `build` command
//
// - `tailwindcss -o output.css` // should run the build command, not show the help message
// - `tailwindcss > output.css` // should run the build command, not show the help message
if ((process.stdout.isTTY && process.argv[2] === undefined) || flags['--help']) {
function rootHelp({ invalid }: { invalid?: string } = {}) {
help({
usage: ['tailwindcss [--input input.css] [--output output.css] [--watch] [options…]'],
invalid,
usage: [buildUsage('tailwindcss'), buildUsage('tailwindcss build'), canonicalize.usage()],
commands: {
build: 'Build your CSS',
canonicalize: 'Canonicalize candidate lists',
},
options: { ...build.options(), ...sharedOptions },
})
process.exit(0)
}
// Handle the build command
build.handle(flags)
async function run() {
let argv = process.argv.slice(2)
let command = argv[0]
let rootFlags = args({ ...build.options(), ...sharedOptions }, argv)
if (command === 'build') {
let flags = args({ ...build.options(), ...sharedOptions }, argv.slice(1))
if ((process.stdout.isTTY && argv.length === 1) || flags['--help']) {
help({
usage: [buildUsage('tailwindcss build')],
options: { ...build.options(), ...sharedOptions },
})
process.exit(0)
}
await build.handle(flags)
return
}
if (command === 'canonicalize') {
let result = await canonicalize.runCommandLine({ argv: argv.slice(1) })
if (result.stdout.length > 0) {
process.stdout.write(`${result.stdout}\n`)
}
if (result.stderr.length > 0) {
process.stderr.write(`${result.stderr}\n`)
}
process.exitCode = result.exitCode
return
}
if ((process.stdout.isTTY && command === undefined) || rootFlags['--help']) {
rootHelp()
process.exit(0)
}
if (command && !command.startsWith('-')) {
rootHelp({ invalid: command })
process.exit(1)
}
let flags = args({
...build.options(),
...sharedOptions,
})
await build.handle(flags)
}
await run()

View file

@ -1,29 +1,29 @@
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import { relative, wordWrap } from './renderer'
import { normalizeWindowsSeperators } from './test-helpers'
import { normalizeWindowsSeparators } from './test-helpers'
describe('relative', () => {
it('should print an absolute path relative to the current working directory', () => {
expect(normalizeWindowsSeperators(relative(path.resolve('index.css')))).toMatchInlineSnapshot(
expect(normalizeWindowsSeparators(relative(path.resolve('index.css')))).toMatchInlineSnapshot(
`"./index.css"`,
)
})
it('should prefer the shortest value by default', () => {
// Shortest between absolute and relative paths
expect(normalizeWindowsSeperators(relative('index.css'))).toMatchInlineSnapshot(`"index.css"`)
expect(normalizeWindowsSeparators(relative('index.css'))).toMatchInlineSnapshot(`"index.css"`)
})
it('should be possible to override the current working directory', () => {
expect(normalizeWindowsSeperators(relative('../utils/index.css', '..'))).toMatchInlineSnapshot(
expect(normalizeWindowsSeparators(relative('../utils/index.css', '..'))).toMatchInlineSnapshot(
`"./utils/index.css"`,
)
})
it('should be possible to always prefer the relative path', () => {
expect(
normalizeWindowsSeperators(
normalizeWindowsSeparators(
relative('index.css', process.cwd(), { preferAbsoluteIfShorter: false }),
),
).toMatchInlineSnapshot(`"./index.css"`)

View file

@ -1,5 +1,5 @@
import path from 'node:path'
export function normalizeWindowsSeperators(p: string) {
export function normalizeWindowsSeparators(p: string) {
return path.sep === '\\' ? p.replaceAll('\\', '/') : p
}

21
pnpm-lock.yaml generated
View file

@ -2109,7 +2109,6 @@ packages:
'@parcel/watcher-darwin-arm64@2.5.6':
resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [darwin]
'@parcel/watcher-darwin-x64@2.5.0':
@ -2127,7 +2126,6 @@ packages:
'@parcel/watcher-darwin-x64@2.5.6':
resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [darwin]
'@parcel/watcher-freebsd-x64@2.5.0':
@ -2181,7 +2179,6 @@ packages:
'@parcel/watcher-linux-arm64-glibc@2.5.6':
resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
'@parcel/watcher-linux-arm64-musl@2.5.0':
@ -2199,7 +2196,6 @@ packages:
'@parcel/watcher-linux-arm64-musl@2.5.6':
resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
'@parcel/watcher-linux-x64-glibc@2.5.0':
@ -2217,7 +2213,6 @@ packages:
'@parcel/watcher-linux-x64-glibc@2.5.6':
resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
'@parcel/watcher-linux-x64-musl@2.5.0':
@ -2235,7 +2230,6 @@ packages:
'@parcel/watcher-linux-x64-musl@2.5.6':
resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
'@parcel/watcher-wasm@2.5.0':
@ -2283,7 +2277,6 @@ packages:
'@parcel/watcher-win32-x64@2.5.6':
resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [win32]
'@parcel/watcher@2.5.0':
@ -2815,7 +2808,6 @@ packages:
bun@1.3.9:
resolution: {integrity: sha512-v5hkh1us7sMNjfimWE70flYbD5I1/qWQaqmJ45q2qk5H/7muQVa478LSVRSFyGTBUBog2LsPQnfIRdjyWJRY+A==}
cpu: [arm64, x64]
os: [darwin, linux, win32]
hasBin: true
@ -3740,13 +3732,11 @@ packages:
lightningcss-darwin-arm64@1.31.1:
resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [darwin]
lightningcss-darwin-x64@1.31.1:
resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [darwin]
lightningcss-freebsd-x64@1.31.1:
@ -3764,25 +3754,21 @@ packages:
lightningcss-linux-arm64-gnu@1.31.1:
resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
lightningcss-linux-arm64-musl@1.31.1:
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
lightningcss-linux-x64-gnu@1.31.1:
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
lightningcss-linux-x64-musl@1.31.1:
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
lightningcss-win32-arm64-msvc@1.31.1:
@ -3794,7 +3780,6 @@ packages:
lightningcss-win32-x64-msvc@1.31.1:
resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [win32]
lightningcss@1.31.1:
@ -7277,7 +7262,7 @@ snapshots:
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.47.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.47.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-jsx-a11y: 6.10.1(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react: 7.37.2(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1))
@ -7331,7 +7316,7 @@ snapshots:
is-bun-module: 1.2.1
is-glob: 4.0.3
optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.47.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
transitivePeerDependencies:
- '@typescript-eslint/parser'
- eslint-import-resolver-node
@ -7378,7 +7363,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.39.2(jiti@2.6.1)):
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.47.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.47.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9