diff --git a/packages/@tailwindcss-cli/src/commands/canonicalize/canonicalize.test.ts b/packages/@tailwindcss-cli/src/commands/canonicalize/canonicalize.test.ts new file mode 100644 index 000000000..8d7ce9b69 --- /dev/null +++ b/packages/@tailwindcss-cli/src/commands/canonicalize/canonicalize.test.ts @@ -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') + }) +}) diff --git a/packages/@tailwindcss-cli/src/commands/canonicalize/fixtures/input.css b/packages/@tailwindcss-cli/src/commands/canonicalize/fixtures/input.css new file mode 100644 index 000000000..d4b507858 --- /dev/null +++ b/packages/@tailwindcss-cli/src/commands/canonicalize/fixtures/input.css @@ -0,0 +1 @@ +@import 'tailwindcss'; diff --git a/packages/@tailwindcss-cli/src/commands/canonicalize/index.ts b/packages/@tailwindcss-cli/src/commands/canonicalize/index.ts new file mode 100644 index 000000000..7748d1599 --- /dev/null +++ b/packages/@tailwindcss-cli/src/commands/canonicalize/index.ts @@ -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 { + 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((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 { + 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>, + 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 + } +} diff --git a/packages/@tailwindcss-cli/src/commands/help/index.ts b/packages/@tailwindcss-cli/src/commands/help/index.ts index a28132062..b557d03e0 100644 --- a/packages/@tailwindcss-cli/src/commands/help/index.ts +++ b/packages/@tailwindcss-cli/src/commands/help/index.ts @@ -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') + } } diff --git a/packages/@tailwindcss-cli/src/index.ts b/packages/@tailwindcss-cli/src/index.ts index 41807f174..4b308e694 100644 --- a/packages/@tailwindcss-cli/src/index.ts +++ b/packages/@tailwindcss-cli/src/index.ts @@ -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() diff --git a/packages/@tailwindcss-cli/src/utils/renderer.test.ts b/packages/@tailwindcss-cli/src/utils/renderer.test.ts index b9950655f..7acef4551 100644 --- a/packages/@tailwindcss-cli/src/utils/renderer.test.ts +++ b/packages/@tailwindcss-cli/src/utils/renderer.test.ts @@ -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"`) diff --git a/packages/@tailwindcss-cli/src/utils/test-helpers.ts b/packages/@tailwindcss-cli/src/utils/test-helpers.ts index 8e61b0a13..30f4fc777 100644 --- a/packages/@tailwindcss-cli/src/utils/test-helpers.ts +++ b/packages/@tailwindcss-cli/src/utils/test-helpers.ts @@ -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 } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ce5acce0..6770d35b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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