Add tailwindcss canonicalize sub-command (#19783)
This PR adds a new `tailwindcss canonicalize` sub-command.
This commit is contained in:
parent
d5717f2307
commit
e96909accd
8 changed files with 505 additions and 67 deletions
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1 @@
|
|||
@import 'tailwindcss';
|
||||
276
packages/@tailwindcss-cli/src/commands/canonicalize/index.ts
Normal file
276
packages/@tailwindcss-cli/src/commands/canonicalize/index.ts
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"`)
|
||||
|
|
|
|||
|
|
@ -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
21
pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue