diff --git a/packages/@tailwindcss-upgrade/README.md b/packages/@tailwindcss-upgrade/README.md
new file mode 100644
index 000000000000..95ec9d87ddcc
--- /dev/null
+++ b/packages/@tailwindcss-upgrade/README.md
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+ A utility-first CSS framework for rapidly building custom user interfaces.
+
+
+
+
+
+
+
+
+
+---
+
+## Documentation
+
+For full documentation, visit [tailwindcss.com](https://tailwindcss.com).
+
+## Community
+
+For help, discussion about best practices, or any other conversation that would benefit from being searchable:
+
+[Discuss Tailwind CSS on GitHub](https://github.com/tailwindcss/tailwindcss/discussions)
+
+For chatting with others using the framework:
+
+[Join the Tailwind CSS Discord Server](https://discord.gg/7NF8GNe)
+
+## Contributing
+
+If you're interested in contributing to Tailwind CSS, please read our [contributing docs](https://github.com/tailwindcss/tailwindcss/blob/next/.github/CONTRIBUTING.md) **before submitting a pull request**.
diff --git a/packages/@tailwindcss-upgrade/package.json b/packages/@tailwindcss-upgrade/package.json
new file mode 100644
index 000000000000..95c3ed453dd1
--- /dev/null
+++ b/packages/@tailwindcss-upgrade/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@tailwindcss/upgrade",
+ "version": "4.0.0-alpha.24",
+ "description": "A utility-first CSS framework for rapidly building custom user interfaces.",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/tailwindlabs/tailwindcss.git",
+ "directory": "packages/@tailwindcss-cli"
+ },
+ "bugs": "https://github.com/tailwindlabs/tailwindcss/issues",
+ "homepage": "https://tailwindcss.com",
+ "scripts": {
+ "lint": "tsc --noEmit",
+ "build": "tsup-node",
+ "dev": "pnpm run build -- --watch"
+ },
+ "bin": {
+ "tailwindcss-upgrade": "./dist/index.mjs"
+ },
+ "exports": {
+ "./package.json": "./package.json"
+ },
+ "files": [
+ "dist"
+ ],
+ "publishConfig": {
+ "provenance": true,
+ "access": "public"
+ },
+ "dependencies": {
+ "enhanced-resolve": "^5.17.1",
+ "fast-glob": "^3.3.2",
+ "mri": "^1.2.0",
+ "picocolors": "^1.0.1",
+ "postcss": "^8.4.41",
+ "postcss-import": "^16.1.0",
+ "tailwindcss": "workspace:^"
+ },
+ "devDependencies": {
+ "@types/node": "catalog:",
+ "@types/postcss-import": "^14.0.3",
+ "dedent": "1.5.3"
+ }
+}
diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.test.ts
new file mode 100644
index 000000000000..f270472d1e65
--- /dev/null
+++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.test.ts
@@ -0,0 +1,70 @@
+import dedent from 'dedent'
+import postcss from 'postcss'
+import { expect, it } from 'vitest'
+import { migrateAtApply } from './migrate-at-apply'
+
+const css = dedent
+
+function migrate(input: string) {
+ return postcss()
+ .use(migrateAtApply())
+ .process(input, { from: expect.getState().testPath })
+ .then((result) => result.css)
+}
+
+it('should not migrate `@apply`, when there are no issues', async () => {
+ expect(
+ await migrate(css`
+ .foo {
+ @apply flex flex-col items-center;
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ ".foo {
+ @apply flex flex-col items-center;
+ }"
+ `)
+})
+
+it('should append `!` to each utility, when using `!important`', async () => {
+ expect(
+ await migrate(css`
+ .foo {
+ @apply flex flex-col !important;
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ ".foo {
+ @apply flex! flex-col!;
+ }"
+ `)
+})
+
+// TODO: Handle SCSS syntax
+it.skip('should append `!` to each utility, when using `#{!important}`', async () => {
+ expect(
+ await migrate(css`
+ .foo {
+ @apply flex flex-col #{!important};
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ ".foo {
+ @apply flex! flex-col!;
+ }"
+ `)
+})
+
+it('should move the legacy `!` prefix, to the new `!` postfix notation', async () => {
+ expect(
+ await migrate(css`
+ .foo {
+ @apply !flex flex-col! hover:!items-start items-center;
+ }
+ `),
+ ).toMatchInlineSnapshot(`
+ ".foo {
+ @apply flex! flex-col! hover:items-start! items-center;
+ }"
+ `)
+})
diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.ts
new file mode 100644
index 000000000000..41ff03ec3323
--- /dev/null
+++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-at-apply.ts
@@ -0,0 +1,43 @@
+import type { AtRule, Plugin } from 'postcss'
+import { segment } from '../../../tailwindcss/src/utils/segment'
+
+export function migrateAtApply(): Plugin {
+ function migrate(atRule: AtRule) {
+ let utilities = atRule.params.split(/(\s+)/)
+ let important =
+ utilities[utilities.length - 1] === '!important' ||
+ utilities[utilities.length - 1] === '#{!important}' // Sass/SCSS
+
+ if (important) utilities.pop() // Remove `!important`
+
+ let params = utilities.map((part) => {
+ // Keep whitespace
+ if (part.trim() === '') return part
+
+ let variants = segment(part, ':')
+ let utility = variants.pop()!
+
+ // Apply the important modifier to all the rules if necessary
+ if (important && utility[0] !== '!' && utility[utility.length - 1] !== '!') {
+ utility += '!'
+ }
+
+ // Migrate the important modifier to the end of the utility
+ if (utility[0] === '!') {
+ utility = `${utility.slice(1)}!`
+ }
+
+ // Reconstruct the utility with the variants
+ return [...variants, utility].join(':')
+ })
+
+ atRule.params = params.join('').trim()
+ }
+
+ return {
+ postcssPlugin: '@tailwindcss/upgrade/migrate-at-apply',
+ AtRule: {
+ apply: migrate,
+ },
+ }
+}
diff --git a/packages/@tailwindcss-upgrade/src/commands/help/index.ts b/packages/@tailwindcss-upgrade/src/commands/help/index.ts
new file mode 100644
index 000000000000..20ee78208440
--- /dev/null
+++ b/packages/@tailwindcss-upgrade/src/commands/help/index.ts
@@ -0,0 +1,170 @@
+import pc from 'picocolors'
+import type { Arg } from '../../utils/args'
+import { UI, header, highlight, indent, println, wordWrap } from '../../utils/renderer'
+
+export function help({
+ invalid,
+ usage,
+ options,
+}: {
+ invalid?: string
+ usage?: string[]
+ options?: Arg
+}) {
+ // Available terminal width
+ let width = process.stdout.columns
+
+ // Render header
+ println(header())
+
+ // Render the invalid command
+ if (invalid) {
+ println()
+ println(`${pc.dim('Invalid command:')} ${invalid}`)
+ }
+
+ // Render usage
+ if (usage && usage.length > 0) {
+ println()
+ println(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.
+ let command = example.slice(0, example.indexOf('['))
+ let options = example.slice(example.indexOf('['))
+
+ // Make the options dimmed, to make them stand out less than the command
+ // itself.
+ options = options.replace(/\[.*?\]/g, (option) => pc.dim(option))
+
+ // The space between the command and the options.
+ let space = 1
+
+ // Wrap the options based on the available width of the terminal.
+ let lines = wordWrap(options, width - UI.indent - command.length - space)
+
+ // Print an empty line between the usage examples if we need to split due
+ // to width constraints. This ensures that the usage examples are visually
+ // separated.
+ //
+ // E.g.: when enough space is available
+ //
+ // ```
+ // Usage:
+ // tailwindcss build [--input input.css] [--output output.css] [--watch] [options...]
+ // tailwindcss other [--watch] [options...]
+ // ```
+ //
+ // E.g.: when not enough space is available
+ //
+ // ```
+ // Usage:
+ // tailwindcss build [--input input.css] [--output output.css]
+ // [--watch] [options...]
+ //
+ // tailwindcss other [--watch] [options...]
+ // ```
+ if (lines.length > 1 && idx !== 0) {
+ println()
+ }
+
+ // Print the usage examples based on available width of the terminal.
+ //
+ // E.g.: when enough space is available
+ //
+ // ```
+ // Usage:
+ // tailwindcss [--input input.css] [--output output.css] [--watch] [options...]
+ // ```
+ //
+ // E.g.: when not enough space is available
+ //
+ // ```
+ // Usage:
+ // tailwindcss [--input input.css] [--output output.css]
+ // [--watch] [options...]
+ // ```
+ //
+ // > Note how the second line is indented to align with the first line.
+ println(indent(`${command}${lines.shift()}`))
+ for (let line of lines) {
+ println(indent(line, command.length))
+ }
+ }
+ }
+
+ // Render options
+ if (options) {
+ // Track the max alias length, this is used to indent the options that don't
+ // have an alias such that everything is aligned properly.
+ let maxAliasLength = 0
+ for (let { alias } of Object.values(options)) {
+ if (alias) {
+ maxAliasLength = Math.max(maxAliasLength, alias.length)
+ }
+ }
+
+ // The option strings, which are the combination of the `alias` and the
+ // `flag`, with the correct spacing.
+ let optionStrings: string[] = []
+
+ // Track the max option length, which is the longest combination of an
+ // `alias` followed by `, ` and followed by the `flag`.
+ let maxOptionLength = 0
+
+ for (let [flag, { alias }] of Object.entries(options)) {
+ // The option string, which is the combination of the alias and the flag
+ // but already properly indented based on the other aliases to ensure
+ // everything is aligned properly.
+ let option = [
+ alias ? `${alias.padStart(maxAliasLength)}` : alias,
+ alias ? flag : ' '.repeat(maxAliasLength + 2 /* `, `.length */) + flag,
+ ]
+ .filter(Boolean)
+ .join(', ')
+
+ optionStrings.push(option)
+ maxOptionLength = Math.max(maxOptionLength, option.length)
+ }
+
+ println()
+ println(pc.dim('Options:'))
+
+ // The minimum amount of dots between the option and the description.
+ let minimumGap = 8
+
+ for (let { description, default: defaultValue = null } of Object.values(options)) {
+ // The option to render
+ let option = optionStrings.shift() as string
+
+ // The amount of dots to show between the option and the description.
+ let dotCount = minimumGap + (maxOptionLength - option.length)
+
+ // To account for the space before and after the dots.
+ let spaces = 2
+
+ // The available width remaining for the description.
+ let availableWidth = width - option.length - dotCount - spaces - UI.indent
+
+ // Wrap the description and the default value (if present), based on the
+ // available width.
+ let lines = wordWrap(
+ defaultValue !== null
+ ? `${description} ${pc.dim(`[default:\u202F${highlight(`${defaultValue}`)}]`)}`
+ : description,
+ availableWidth,
+ )
+
+ // Print the option, the spacer dots and the start of the description.
+ println(
+ 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}`))
+ }
+ }
+ }
+}
diff --git a/packages/@tailwindcss-upgrade/src/index.test.ts b/packages/@tailwindcss-upgrade/src/index.test.ts
new file mode 100644
index 000000000000..a043092d9ae6
--- /dev/null
+++ b/packages/@tailwindcss-upgrade/src/index.test.ts
@@ -0,0 +1,28 @@
+import dedent from 'dedent'
+import { expect, it } from 'vitest'
+import { migrateContents } from './migrate'
+
+const css = dedent
+
+it('should print the input as-is', async () => {
+ expect(
+ await migrateContents(
+ css`
+ /* above */
+ .foo/* after */ {
+ /* above */
+ color: /* before */ red /* after */;
+ /* below */
+ }
+ `,
+ expect.getState().testPath,
+ ),
+ ).toMatchInlineSnapshot(`
+ "/* above */
+ .foo/* after */ {
+ /* above */
+ color: /* before */ red /* after */;
+ /* below */
+ }"
+ `)
+})
diff --git a/packages/@tailwindcss-upgrade/src/index.ts b/packages/@tailwindcss-upgrade/src/index.ts
new file mode 100644
index 000000000000..19513d1cbe54
--- /dev/null
+++ b/packages/@tailwindcss-upgrade/src/index.ts
@@ -0,0 +1,74 @@
+#!/usr/bin/env node
+
+import fastGlob from 'fast-glob'
+import { execSync } from 'node:child_process'
+import path from 'node:path'
+import { help } from './commands/help'
+import { migrate } from './migrate'
+import { args, type Arg } from './utils/args'
+import { eprintln, error, header, highlight, info, success } from './utils/renderer'
+
+const options = {
+ '--help': { type: 'boolean', description: 'Display usage information', alias: '-h' },
+ '--force': { type: 'boolean', description: 'Force the migration', alias: '-f' },
+ '--version': { type: 'boolean', description: 'Display the version number', alias: '-v' },
+} satisfies Arg
+const flags = args(options)
+
+if (flags['--help']) {
+ help({
+ usage: ['npx @tailwindcss/upgrade'],
+ options,
+ })
+ process.exit(0)
+}
+
+async function run() {
+ eprintln(header())
+ eprintln()
+
+ if (!flags['--force']) {
+ let stdout = execSync('git status --porcelain', { encoding: 'utf-8' })
+ if (stdout.trim()) {
+ error('Git directory is not clean. Please stash or commit your changes before migrating.')
+ info(`You may use the ${highlight('--force')} flag to override this safety check.`)
+ process.exit(1)
+ }
+ }
+
+ // Use provided files
+ let files = flags._.map((file) => path.resolve(process.cwd(), file))
+
+ // Discover CSS files in case no files were provided
+ if (files.length === 0) {
+ info(
+ 'No files provided. Searching for CSS files in the current directory and its subdirectories…',
+ )
+
+ files = await fastGlob(['**/*.css'], {
+ absolute: true,
+ ignore: ['**/node_modules', '**/vendor'],
+ })
+ }
+
+ // Ensure we are only dealing with CSS files
+ files = files.filter((file) => file.endsWith('.css'))
+
+ // Migrate each file
+ await Promise.allSettled(files.map((file) => migrate(file)))
+
+ // Figure out if we made any changes
+ let stdout = execSync('git status --porcelain', { encoding: 'utf-8' })
+ if (stdout.trim()) {
+ success('Migration complete. Verify the changes and commit them to your repository.')
+ } else {
+ success('Migration complete. No changes were made to your repository.')
+ }
+}
+
+run()
+ .then(() => process.exit(0))
+ .catch((err) => {
+ console.error(err)
+ process.exit(1)
+ })
diff --git a/packages/@tailwindcss-upgrade/src/migrate.ts b/packages/@tailwindcss-upgrade/src/migrate.ts
new file mode 100644
index 000000000000..68e0d336034a
--- /dev/null
+++ b/packages/@tailwindcss-upgrade/src/migrate.ts
@@ -0,0 +1,18 @@
+import fs from 'node:fs/promises'
+import path from 'node:path'
+import postcss from 'postcss'
+import { migrateAtApply } from './codemods/migrate-at-apply'
+
+export async function migrateContents(contents: string, file?: string) {
+ return postcss()
+ .use(migrateAtApply())
+ .process(contents, { from: file })
+ .then((result) => result.css)
+}
+
+export async function migrate(file: string) {
+ let fullPath = path.resolve(process.cwd(), file)
+ let contents = await fs.readFile(fullPath, 'utf-8')
+
+ await fs.writeFile(fullPath, await migrateContents(contents, fullPath))
+}
diff --git a/packages/@tailwindcss-upgrade/src/utils/args.test.ts b/packages/@tailwindcss-upgrade/src/utils/args.test.ts
new file mode 100644
index 000000000000..51d2c4747241
--- /dev/null
+++ b/packages/@tailwindcss-upgrade/src/utils/args.test.ts
@@ -0,0 +1,123 @@
+import { expect, it } from 'vitest'
+import { args, type Arg } from './args'
+
+it('should be possible to parse a single argument', () => {
+ expect(
+ args(
+ {
+ '--input': { type: 'string', description: 'Input file' },
+ },
+ ['--input', 'input.css'],
+ ),
+ ).toMatchInlineSnapshot(`
+ {
+ "--input": "input.css",
+ "_": [],
+ }
+ `)
+})
+
+it('should fallback to the default value if no flag is passed', () => {
+ expect(
+ args(
+ {
+ '--input': { type: 'string', description: 'Input file', default: 'input.css' },
+ },
+ ['--other'],
+ ),
+ ).toMatchInlineSnapshot(`
+ {
+ "--input": "input.css",
+ "_": [],
+ }
+ `)
+})
+
+it('should fallback to null if no flag is passed and no default value is provided', () => {
+ expect(
+ args(
+ {
+ '--input': { type: 'string', description: 'Input file' },
+ },
+ ['--other'],
+ ),
+ ).toMatchInlineSnapshot(`
+ {
+ "--input": null,
+ "_": [],
+ }
+ `)
+})
+
+it('should be possible to parse a single argument using the shorthand alias', () => {
+ expect(
+ args(
+ {
+ '--input': { type: 'string', description: 'Input file', alias: '-i' },
+ },
+ ['-i', 'input.css'],
+ ),
+ ).toMatchInlineSnapshot(`
+ {
+ "--input": "input.css",
+ "_": [],
+ }
+ `)
+})
+
+it('should convert the incoming value to the correct type', () => {
+ expect(
+ args(
+ {
+ '--input': { type: 'string', description: 'Input file' },
+ '--watch': { type: 'boolean', description: 'Watch mode' },
+ '--retries': { type: 'number', description: 'Amount of retries' },
+ },
+ ['--input', 'input.css', '--watch', '--retries', '3'],
+ ),
+ ).toMatchInlineSnapshot(`
+ {
+ "--input": "input.css",
+ "--retries": 3,
+ "--watch": true,
+ "_": [],
+ }
+ `)
+})
+
+it('should be possible to provide multiple types, and convert the value to that type', () => {
+ let options = {
+ '--retries': { type: 'boolean | number | string', description: 'Retries' },
+ } satisfies Arg
+
+ expect(args(options, ['--retries'])).toMatchInlineSnapshot(`
+ {
+ "--retries": true,
+ "_": [],
+ }
+ `)
+ expect(args(options, ['--retries', 'true'])).toMatchInlineSnapshot(`
+ {
+ "--retries": true,
+ "_": [],
+ }
+ `)
+ expect(args(options, ['--retries', 'false'])).toMatchInlineSnapshot(`
+ {
+ "--retries": false,
+ "_": [],
+ }
+ `)
+ expect(args(options, ['--retries', '5'])).toMatchInlineSnapshot(`
+ {
+ "--retries": 5,
+ "_": [],
+ }
+ `)
+ expect(args(options, ['--retries', 'indefinitely'])).toMatchInlineSnapshot(`
+ {
+ "--retries": "indefinitely",
+ "_": [],
+ }
+ `)
+})
diff --git a/packages/@tailwindcss-upgrade/src/utils/args.ts b/packages/@tailwindcss-upgrade/src/utils/args.ts
new file mode 100644
index 000000000000..81bd847d50d1
--- /dev/null
+++ b/packages/@tailwindcss-upgrade/src/utils/args.ts
@@ -0,0 +1,160 @@
+import parse from 'mri'
+
+// Definition of the arguments for a command in the CLI.
+export type Arg = {
+ [key: `--${string}`]: {
+ type: keyof Types
+ description: string
+ alias?: `-${string}`
+ default?: Types[keyof Types]
+ }
+}
+
+// Each argument will have a type and we want to convert the incoming raw string
+// based value to the correct type. We can't use pure TypeScript types because
+// these don't exist at runtime. Instead, we define a string-based type that
+// maps to a TypeScript type.
+type Types = {
+ boolean: boolean
+ number: number | null
+ string: string | null
+ 'boolean | string': boolean | string | null
+ 'number | string': number | string | null
+ 'boolean | number': boolean | number | null
+ 'boolean | number | string': boolean | number | string | null
+}
+
+// Convert the `Arg` type to a type that can be used at runtime.
+//
+// E.g.:
+//
+// Arg:
+// ```
+// { '--input': { type: 'string', description: 'Input file', alias: '-i' } }
+// ```
+//
+// Command:
+// ```
+// ./tailwindcss -i input.css
+// ./tailwindcss --input input.css
+// ```
+//
+// Result type:
+// ```
+// {
+// _: string[], // All non-flag arguments
+// '--input': string | null // The `--input` flag will be filled with `null`, if the flag is not used.
+// // The `null` type will not be there if `default` is provided.
+// }
+// ```
+//
+// Result runtime object:
+// ```
+// {
+// _: [],
+// '--input': 'input.css'
+// }
+// ```
+export type Result = {
+ [K in keyof T]: T[K] extends { type: keyof Types; default?: any }
+ ? undefined extends T[K]['default']
+ ? Types[T[K]['type']]
+ : NonNullable
+ : never
+} & {
+ // All non-flag arguments
+ _: string[]
+}
+
+export function args(options: T, argv = process.argv.slice(2)): Result {
+ let parsed = parse(argv)
+
+ let result: { _: string[]; [key: string]: unknown } = {
+ _: parsed._,
+ }
+
+ for (let [
+ flag,
+ { type, alias, default: defaultValue = type === 'boolean' ? false : null },
+ ] of Object.entries(options)) {
+ // Start with the default value
+ result[flag] = defaultValue
+
+ // Try to find the `alias`, and map it to long form `flag`
+ if (alias) {
+ let key = alias.slice(1)
+ if (parsed[key] !== undefined) {
+ result[flag] = convert(parsed[key], type)
+ }
+ }
+
+ // Try to find the long form `flag`
+ {
+ let key = flag.slice(2)
+ if (parsed[key] !== undefined) {
+ result[flag] = convert(parsed[key], type)
+ }
+ }
+ }
+
+ return result as Result
+}
+
+// ---
+
+type ArgumentType = string | boolean
+
+// Try to convert the raw incoming `value` (which will be a string or a boolean,
+// this is coming from `mri`'s parse function'), to the correct type based on
+// the `type` of the argument.
+function convert(value: string | boolean, type: T) {
+ switch (type) {
+ case 'string':
+ return convertString(value)
+ case 'boolean':
+ return convertBoolean(value)
+ case 'number':
+ return convertNumber(value)
+ case 'boolean | string':
+ return convertBoolean(value) ?? convertString(value)
+ case 'number | string':
+ return convertNumber(value) ?? convertString(value)
+ case 'boolean | number':
+ return convertBoolean(value) ?? convertNumber(value)
+ case 'boolean | number | string':
+ return convertBoolean(value) ?? convertNumber(value) ?? convertString(value)
+ default:
+ throw new Error(`Unhandled type: ${type}`)
+ }
+}
+
+function convertBoolean(value: ArgumentType) {
+ if (value === true || value === false) {
+ return value
+ }
+
+ if (value === 'true') {
+ return true
+ }
+
+ if (value === 'false') {
+ return false
+ }
+}
+
+function convertNumber(value: ArgumentType) {
+ if (typeof value === 'number') {
+ return value
+ }
+
+ {
+ let valueAsNumber = Number(value)
+ if (!Number.isNaN(valueAsNumber)) {
+ return valueAsNumber
+ }
+ }
+}
+
+function convertString(value: ArgumentType) {
+ return `${value}`
+}
diff --git a/packages/@tailwindcss-upgrade/src/utils/format-ns.test.ts b/packages/@tailwindcss-upgrade/src/utils/format-ns.test.ts
new file mode 100644
index 000000000000..a5382d21772e
--- /dev/null
+++ b/packages/@tailwindcss-upgrade/src/utils/format-ns.test.ts
@@ -0,0 +1,28 @@
+import { expect, it } from 'vitest'
+import { formatNanoseconds } from './format-ns'
+
+it.each([
+ [0, '0ns'],
+ [1, '1ns'],
+ [999, '999ns'],
+ [1000, '1µs'],
+ [1001, '1µs'],
+ [999999, '999µs'],
+ [1000000, '1ms'],
+ [1000001, '1ms'],
+ [999999999, '999ms'],
+ [1000000000, '1s'],
+ [1000000001, '1s'],
+ [59999999999, '59s'],
+ [60000000000, '1m'],
+ [60000000001, '1m'],
+ [3599999999999n, '59m'],
+ [3600000000000n, '1h'],
+ [3600000000001n, '1h'],
+ [86399999999999n, '23h'],
+ [86400000000000n, '1d'],
+ [86400000000001n, '1d'],
+ [8640000000000000n, '100d'],
+])('should format %s nanoseconds as %s', (ns, expected) => {
+ expect(formatNanoseconds(ns)).toBe(expected)
+})
diff --git a/packages/@tailwindcss-upgrade/src/utils/format-ns.ts b/packages/@tailwindcss-upgrade/src/utils/format-ns.ts
new file mode 100644
index 000000000000..39889d401149
--- /dev/null
+++ b/packages/@tailwindcss-upgrade/src/utils/format-ns.ts
@@ -0,0 +1,23 @@
+export function formatNanoseconds(input: bigint | number) {
+ let ns = typeof input === 'number' ? BigInt(input) : input
+
+ if (ns < 1_000n) return `${ns}ns`
+ ns /= 1_000n
+
+ if (ns < 1_000n) return `${ns}µs`
+ ns /= 1_000n
+
+ if (ns < 1_000n) return `${ns}ms`
+ ns /= 1_000n
+
+ if (ns < 60n) return `${ns}s`
+ ns /= 60n
+
+ if (ns < 60n) return `${ns}m`
+ ns /= 60n
+
+ if (ns < 24n) return `${ns}h`
+ ns /= 24n
+
+ return `${ns}d`
+}
diff --git a/packages/@tailwindcss-upgrade/src/utils/renderer.ts b/packages/@tailwindcss-upgrade/src/utils/renderer.ts
new file mode 100644
index 000000000000..b9b9245f7b04
--- /dev/null
+++ b/packages/@tailwindcss-upgrade/src/utils/renderer.ts
@@ -0,0 +1,130 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import { stripVTControlCharacters } from 'node:util'
+import pc from 'picocolors'
+import { resolve } from '../utils/resolve'
+import { formatNanoseconds } from './format-ns'
+
+export const UI = {
+ indent: 2,
+}
+export function header() {
+ return `${pc.italic(pc.bold(pc.blue('\u2248')))} tailwindcss ${pc.blue(`v${getVersion()}`)}`
+}
+
+export function highlight(file: string) {
+ return `${pc.dim(pc.blue('`'))}${pc.blue(file)}${pc.dim(pc.blue('`'))}`
+}
+
+/**
+ * Convert an `absolute` path to a `relative` path from the current working
+ * directory.
+ */
+export function relative(
+ to: string,
+ from = process.cwd(),
+ { preferAbsoluteIfShorter = true } = {},
+) {
+ let result = path.relative(from, to)
+ if (!result.startsWith('..')) {
+ result = `.${path.sep}${result}`
+ }
+
+ if (preferAbsoluteIfShorter && result.length > to.length) {
+ return to
+ }
+
+ return result
+}
+
+/**
+ * Wrap `text` into multiple lines based on the `width`.
+ */
+export function wordWrap(text: string, width: number) {
+ let words = text.split(' ')
+ let lines = []
+
+ let line = ''
+ let lineLength = 0
+ for (let word of words) {
+ let wordLength = stripVTControlCharacters(word).length
+
+ if (lineLength + wordLength + 1 > width) {
+ lines.push(line)
+ line = ''
+ lineLength = 0
+ }
+
+ line += (lineLength ? ' ' : '') + word
+ lineLength += wordLength + (lineLength ? 1 : 0)
+ }
+
+ if (lineLength) {
+ lines.push(line)
+ }
+
+ return lines
+}
+
+/**
+ * Format a duration in nanoseconds to a more human readable format.
+ */
+export function formatDuration(ns: bigint) {
+ let formatted = formatNanoseconds(ns)
+
+ if (ns <= 50 * 1e6) return pc.green(formatted)
+ if (ns <= 300 * 1e6) return pc.blue(formatted)
+ if (ns <= 1000 * 1e6) return pc.yellow(formatted)
+
+ return pc.red(formatted)
+}
+
+export function indent(value: string, offset = 0) {
+ return `${' '.repeat(offset + UI.indent)}${value}`
+}
+
+export function success(message: string, print = eprintln) {
+ wordWrap(message, process.stderr.columns - 3).map((line) => {
+ return print(`${pc.green('\u2502')} ${line}`)
+ })
+ print()
+}
+
+export function info(message: string, print = eprintln) {
+ wordWrap(message, process.stderr.columns - 3).map((line) => {
+ return print(`${pc.blue('\u2502')} ${line}`)
+ })
+ print()
+}
+
+export function error(message: string, print = eprintln) {
+ wordWrap(message, process.stderr.columns - 3).map((line) => {
+ return print(`${pc.red('\u2502')} ${line}`)
+ })
+ print()
+}
+
+export function warn(message: string, print = eprintln) {
+ wordWrap(message, process.stderr.columns - 3).map((line) => {
+ return print(`${pc.yellow('\u2502')} ${line}`)
+ })
+ print()
+}
+
+// Rust inspired functions to print to the console:
+
+export function eprintln(value = '') {
+ process.stderr.write(`${value}\n`)
+}
+
+export function println(value = '') {
+ process.stdout.write(`${value}\n`)
+}
+
+function getVersion(): string {
+ if (typeof globalThis.__tw_version === 'string') {
+ return globalThis.__tw_version
+ }
+ let { version } = JSON.parse(fs.readFileSync(resolve('tailwindcss/package.json'), 'utf-8'))
+ return version
+}
diff --git a/packages/@tailwindcss-upgrade/src/utils/resolve.ts b/packages/@tailwindcss-upgrade/src/utils/resolve.ts
new file mode 100644
index 000000000000..e9197e8741ab
--- /dev/null
+++ b/packages/@tailwindcss-upgrade/src/utils/resolve.ts
@@ -0,0 +1,32 @@
+import EnhancedResolve from 'enhanced-resolve'
+import fs from 'node:fs'
+import { createRequire } from 'node:module'
+
+const localResolve = createRequire(import.meta.url).resolve
+export function resolve(id: string) {
+ if (typeof globalThis.__tw_resolve === 'function') {
+ let resolved = globalThis.__tw_resolve(id)
+ if (resolved) {
+ return resolved
+ }
+ }
+ return localResolve(id)
+}
+
+const resolver = EnhancedResolve.ResolverFactory.createResolver({
+ fileSystem: new EnhancedResolve.CachedInputFileSystem(fs, 4000),
+ useSyncFileSystemCalls: true,
+ extensions: ['.css'],
+ mainFields: ['style'],
+ conditionNames: ['style'],
+})
+export function resolveCssId(id: string, base: string) {
+ if (typeof globalThis.__tw_resolve === 'function') {
+ let resolved = globalThis.__tw_resolve(id, base)
+ if (resolved) {
+ return resolved
+ }
+ }
+
+ return resolver.resolveSync({}, base, id)
+}
diff --git a/packages/@tailwindcss-upgrade/tsconfig.json b/packages/@tailwindcss-upgrade/tsconfig.json
new file mode 100644
index 000000000000..6ae022f65bf0
--- /dev/null
+++ b/packages/@tailwindcss-upgrade/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../tsconfig.base.json",
+}
diff --git a/packages/@tailwindcss-upgrade/tsup.config.ts b/packages/@tailwindcss-upgrade/tsup.config.ts
new file mode 100644
index 000000000000..7d82eee2c882
--- /dev/null
+++ b/packages/@tailwindcss-upgrade/tsup.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from 'tsup'
+
+export default defineConfig({
+ format: ['esm'],
+ clean: true,
+ minify: true,
+ entry: ['src/index.ts'],
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 08d91bab8c94..ba22273e2f77 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -281,6 +281,40 @@ importers:
specifier: ^1.25.1
version: 1.26.0
+ packages/@tailwindcss-upgrade:
+ dependencies:
+ enhanced-resolve:
+ specifier: ^5.17.1
+ version: 5.17.1
+ fast-glob:
+ specifier: ^3.3.2
+ version: 3.3.2
+ mri:
+ specifier: ^1.2.0
+ version: 1.2.0
+ picocolors:
+ specifier: ^1.0.1
+ version: 1.0.1
+ postcss:
+ specifier: ^8.4.41
+ version: 8.4.41
+ postcss-import:
+ specifier: ^16.1.0
+ version: 16.1.0(postcss@8.4.41)
+ tailwindcss:
+ specifier: workspace:^
+ version: link:../tailwindcss
+ devDependencies:
+ '@types/node':
+ specifier: 'catalog:'
+ version: 20.14.13
+ '@types/postcss-import':
+ specifier: ^14.0.3
+ version: 14.0.3
+ dedent:
+ specifier: 1.5.3
+ version: 1.5.3
+
packages/@tailwindcss-vite:
dependencies:
'@tailwindcss/node':
@@ -391,7 +425,7 @@ importers:
version: link:../../packages/@tailwindcss-vite
'@vitejs/plugin-react':
specifier: ^4.3.1
- version: 4.3.1(vite@5.4.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6))
+ version: 4.3.1(vite@5.4.0(@types/node@22.5.4)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6))
react:
specifier: ^18.3.1
version: 18.3.1
@@ -413,10 +447,10 @@ importers:
version: 1.1.22
vite:
specifier: 'catalog:'
- version: 5.4.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6)
+ version: 5.4.0(@types/node@22.5.4)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6)
vite-plugin-handlebars:
specifier: ^2.0.0
- version: 2.0.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6)
+ version: 2.0.0(@types/node@22.5.4)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6)
packages:
@@ -1021,6 +1055,7 @@ packages:
'@parcel/watcher-darwin-arm64@2.4.2-alpha.0':
resolution: {integrity: sha512-2xH4Ve7OKjIh+4YRfTN3HGJa2W8KTPLOALHZj5fxcbTPwaVxdpIRItDrcikUx2u3AzGAFme7F+AZZXHnf0F15Q==}
engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
os: [darwin]
'@parcel/watcher-darwin-x64@2.4.1':
@@ -1032,6 +1067,7 @@ packages:
'@parcel/watcher-darwin-x64@2.4.2-alpha.0':
resolution: {integrity: sha512-xtjmXUH4YZVah5+7Q0nb+fpRP5qZn9cFfuPuZ4k77UfUGVwhacgZyIRQgIOwMP3GkgW4TsrKQaw1KIe7L1ZqcQ==}
engines: {node: '>= 10.0.0'}
+ cpu: [x64]
os: [darwin]
'@parcel/watcher-freebsd-x64@2.4.1':
@@ -1055,6 +1091,7 @@ packages:
'@parcel/watcher-linux-arm64-glibc@2.4.2-alpha.0':
resolution: {integrity: sha512-vIIOcZf+fgsRReIK3Fw0WINvGo9UwiXfisnqYRzfpNByRZvkEPkGTIVe8iiDp72NhPTVmwIvBqM6yKDzIaw8GQ==}
engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
os: [linux]
'@parcel/watcher-linux-arm64-musl@2.4.1':
@@ -1066,6 +1103,7 @@ packages:
'@parcel/watcher-linux-arm64-musl@2.4.2-alpha.0':
resolution: {integrity: sha512-gXqEAoLG9bBCbQNUgqjSOxHcjpmCZmYT9M8UvrdTMgMYgXgiWcR8igKlPRd40mCIRZSkMpN2ScSy2WjQ0bQZnQ==}
engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
os: [linux]
'@parcel/watcher-linux-x64-glibc@2.4.1':
@@ -1077,6 +1115,7 @@ packages:
'@parcel/watcher-linux-x64-glibc@2.4.2-alpha.0':
resolution: {integrity: sha512-/WJJ3Y46ubwQW+Z+mzpzK3pvqn/AT7MA63NB0+k9GTLNxJQZNREensMtpJ/FJ+LVIiraEHTY22KQrsx9+DeNbw==}
engines: {node: '>= 10.0.0'}
+ cpu: [x64]
os: [linux]
'@parcel/watcher-linux-x64-musl@2.4.1':
@@ -1088,6 +1127,7 @@ packages:
'@parcel/watcher-linux-x64-musl@2.4.2-alpha.0':
resolution: {integrity: sha512-1dz4fTM5HaANk3RSRmdhALT+bNqTHawVDL1D77HwV/FuF/kSjlM3rGrJuFaCKwQ5E8CInHCcobqMN8Jh8LYaRg==}
engines: {node: '>= 10.0.0'}
+ cpu: [x64]
os: [linux]
'@parcel/watcher-win32-arm64@2.4.1':
@@ -1111,6 +1151,7 @@ packages:
'@parcel/watcher-win32-x64@2.4.2-alpha.0':
resolution: {integrity: sha512-U2abMKF7JUiIxQkos19AvTLFcnl2Xn8yIW1kzu+7B0Lux4Gkuu/BUDBroaM1s6+hwgK63NOLq9itX2Y3GwUThg==}
engines: {node: '>= 10.0.0'}
+ cpu: [x64]
os: [win32]
'@parcel/watcher@2.4.1':
@@ -1239,6 +1280,9 @@ packages:
'@types/node@20.14.13':
resolution: {integrity: sha512-+bHoGiZb8UiQ0+WEtmph2IWQCjIqg8MDZMAV+ppRRhUZnquF5mQkP/9vpSwJClEiSM/C7fZZExPzfU0vJTyp8w==}
+ '@types/node@22.5.4':
+ resolution: {integrity: sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==}
+
'@types/postcss-import@14.0.3':
resolution: {integrity: sha512-raZhRVTf6Vw5+QbmQ7LOHSDML71A5rj4+EqDzAbrZPfxfoGzFxMHRCq16VlddGIZpHELw0BG4G0YE2ANkdZiIQ==}
@@ -1442,11 +1486,13 @@ packages:
bun@1.1.22:
resolution: {integrity: sha512-G2HCPhzhjDc2jEDkZsO9vwPlpHrTm7a8UVwx9oNS5bZqo5OcSK5GPuWYDWjj7+37bRk5OVLfeIvUMtSrbKeIjQ==}
+ cpu: [arm64, x64]
os: [darwin, linux, win32]
hasBin: true
bun@1.1.26:
resolution: {integrity: sha512-dWSewAqE7sVbYmflJxgG47dW4vmsbar7VAnQ4ao45y3ulr3n7CwdsMLFnzd28jhPRtF+rsaVK2y4OLIkP3OD4A==}
+ cpu: [arm64, x64]
os: [darwin, linux, win32]
hasBin: true
@@ -2209,11 +2255,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:
@@ -2231,21 +2279,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:
@@ -2257,6 +2309,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:
@@ -3000,6 +3053,9 @@ packages:
undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
+ undici-types@6.19.8:
+ resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
+
update-browserslist-db@1.1.0:
resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==}
hasBin: true
@@ -3736,6 +3792,10 @@ snapshots:
dependencies:
undici-types: 5.26.5
+ '@types/node@22.5.4':
+ dependencies:
+ undici-types: 6.19.8
+
'@types/postcss-import@14.0.3':
dependencies:
postcss: 8.4.41
@@ -3753,7 +3813,7 @@ snapshots:
'@types/ws@8.5.12':
dependencies:
- '@types/node': 20.14.13
+ '@types/node': 22.5.4
'@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4)':
dependencies:
@@ -3797,14 +3857,14 @@ snapshots:
'@ungap/structured-clone@1.2.0': {}
- '@vitejs/plugin-react@4.3.1(vite@5.4.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6))':
+ '@vitejs/plugin-react@4.3.1(vite@5.4.0(@types/node@22.5.4)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6))':
dependencies:
'@babel/core': 7.25.2
'@babel/plugin-transform-react-jsx-self': 7.24.7(@babel/core@7.25.2)
'@babel/plugin-transform-react-jsx-source': 7.24.7(@babel/core@7.25.2)
'@types/babel__core': 7.20.5
react-refresh: 0.14.2
- vite: 5.4.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6)
+ vite: 5.4.0(@types/node@22.5.4)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6)
transitivePeerDependencies:
- supports-color
@@ -4370,7 +4430,7 @@ snapshots:
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0)
- eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
+ eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0)
eslint-plugin-react: 7.35.0(eslint@8.57.0)
eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0)
@@ -4394,7 +4454,7 @@ snapshots:
enhanced-resolve: 5.17.1
eslint: 8.57.0
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
- eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
+ eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
fast-glob: 3.3.2
get-tsconfig: 4.7.6
is-core-module: 2.15.0
@@ -4416,7 +4476,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
+ eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
dependencies:
array-includes: 3.1.8
array.prototype.findlastindex: 1.2.5
@@ -5759,6 +5819,8 @@ snapshots:
undici-types@5.26.5: {}
+ undici-types@6.19.8: {}
+
update-browserslist-db@1.1.0(browserslist@4.23.2):
dependencies:
browserslist: 4.23.2
@@ -5787,10 +5849,10 @@ snapshots:
- supports-color
- terser
- vite-plugin-handlebars@2.0.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6):
+ vite-plugin-handlebars@2.0.0(@types/node@22.5.4)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6):
dependencies:
handlebars: 4.7.8
- vite: 5.4.0(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6)
+ vite: 5.4.0(@types/node@22.5.4)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6)
transitivePeerDependencies:
- '@types/node'
- less
@@ -5812,6 +5874,17 @@ snapshots:
lightningcss: 1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4)
terser: 5.31.6
+ vite@5.4.0(@types/node@22.5.4)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6):
+ dependencies:
+ esbuild: 0.21.5
+ postcss: 8.4.41
+ rollup: 4.20.0
+ optionalDependencies:
+ '@types/node': 22.5.4
+ fsevents: 2.3.3
+ lightningcss: 1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4)
+ terser: 5.31.6
+
vitest@2.0.5(@types/node@20.14.13)(lightningcss@1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4))(terser@5.31.6):
dependencies:
'@ampproject/remapping': 2.3.0