From a4cd44d426016b83377a5771bdb1e3d12c5b8990 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Thu, 3 Apr 2025 14:26:02 -0400
Subject: [PATCH 01/41] Cleanup code a bit
---
packages/@tailwindcss-node/package.json | 4 +-
packages/@tailwindcss-node/src/compile.ts | 6 +-
packages/@tailwindcss-vite/src/index.ts | 34 +++++++---
packages/tailwindcss/src/apply.ts | 6 +-
packages/tailwindcss/src/ast.ts | 48 +++++++------
packages/tailwindcss/src/at-import.ts | 17 +++--
.../src/compat/apply-compat-hooks.ts | 5 +-
.../tailwindcss/src/compat/plugin-api.test.ts | 4 +-
.../tailwindcss/src/css-functions.test.ts | 11 +--
packages/tailwindcss/src/css-parser.ts | 25 ++++---
packages/tailwindcss/src/index.test.ts | 68 +++++++++++--------
packages/tailwindcss/src/index.ts | 17 +++--
packages/tailwindcss/src/variants.test.ts | 2 +-
13 files changed, 153 insertions(+), 94 deletions(-)
diff --git a/packages/@tailwindcss-node/package.json b/packages/@tailwindcss-node/package.json
index d3631b758b24..0844c232416f 100644
--- a/packages/@tailwindcss-node/package.json
+++ b/packages/@tailwindcss-node/package.json
@@ -39,7 +39,7 @@
"dependencies": {
"enhanced-resolve": "^5.18.1",
"jiti": "^2.4.2",
- "tailwindcss": "workspace:*",
- "lightningcss": "catalog:"
+ "lightningcss": "catalog:",
+ "tailwindcss": "workspace:*"
}
}
diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts
index ef9dfbcba57d..85c4bd42f948 100644
--- a/packages/@tailwindcss-node/src/compile.ts
+++ b/packages/@tailwindcss-node/src/compile.ts
@@ -2,7 +2,7 @@ import EnhancedResolve from 'enhanced-resolve'
import { createJiti, type Jiti } from 'jiti'
import fs from 'node:fs'
import fsPromises from 'node:fs/promises'
-import path, { dirname } from 'node:path'
+import path from 'node:path'
import { pathToFileURL } from 'node:url'
import {
__unstable__loadDesignSystem as ___unstable__loadDesignSystem,
@@ -125,7 +125,7 @@ export async function loadModule(
let module = await importModule(pathToFileURL(resolvedPath).href)
return {
- base: dirname(resolvedPath),
+ base: path.dirname(resolvedPath),
module: module.default ?? module,
}
}
@@ -144,7 +144,7 @@ export async function loadModule(
onDependency(file)
}
return {
- base: dirname(resolvedPath),
+ base: path.dirname(resolvedPath),
module: module.default ?? module,
}
}
diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts
index a484583a3ec3..a62929069b13 100644
--- a/packages/@tailwindcss-vite/src/index.ts
+++ b/packages/@tailwindcss-vite/src/index.ts
@@ -34,7 +34,12 @@ export default function tailwindcss(): Plugin[] {
function customJsResolver(id: string, base: string) {
return jsResolver(id, base, true, isSSR)
}
- return new Root(id, config!.root, customCssResolver, customJsResolver)
+ return new Root(
+ id,
+ config!.root,
+ customCssResolver,
+ customJsResolver,
+ )
})
return [
@@ -68,14 +73,14 @@ export default function tailwindcss(): Plugin[] {
let root = roots.get(id)
- let generated = await root.generate(src, (file) => this.addWatchFile(file), I)
- if (!generated) {
+ let result = await root.generate(src, (file) => this.addWatchFile(file), I)
+ if (!result) {
roots.delete(id)
return src
}
DEBUG && I.end('[@tailwindcss/vite] Generate CSS (serve)')
- return { code: generated }
+ return result
},
},
@@ -93,18 +98,18 @@ export default function tailwindcss(): Plugin[] {
let root = roots.get(id)
- let generated = await root.generate(src, (file) => this.addWatchFile(file), I)
- if (!generated) {
+ let result = await root.generate(src, (file) => this.addWatchFile(file), I)
+ if (!result) {
roots.delete(id)
return src
}
DEBUG && I.end('[@tailwindcss/vite] Generate CSS (build)')
DEBUG && I.start('[@tailwindcss/vite] Optimize CSS')
- generated = optimize(generated, { minify })
+ result.code = optimize(result.code, { minify })
DEBUG && I.end('[@tailwindcss/vite] Optimize CSS')
- return { code: generated }
+ return result
},
},
] satisfies Plugin[]
@@ -183,7 +188,12 @@ class Root {
content: string,
_addWatchFile: (file: string) => void,
I: Instrumentation,
- ): Promise {
+ ): Promise<
+ | {
+ code: string
+ }
+ | false
+ > {
let inputPath = idToPath(this.id)
function addWatchFile(file: string) {
@@ -313,10 +323,12 @@ class Root {
}
DEBUG && I.start('Build CSS')
- let result = this.compiler.build([...this.candidates])
+ let code = this.compiler.build([...this.candidates])
DEBUG && I.end('Build CSS')
- return result
+ return {
+ code,
+ }
}
private async addBuildDependency(path: string) {
diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts
index 676b4f2eee19..6038c4b8dfbb 100644
--- a/packages/tailwindcss/src/apply.ts
+++ b/packages/tailwindcss/src/apply.ts
@@ -161,11 +161,13 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
{
// Parse the candidates to an AST that we can replace the `@apply` rule
// with.
- let candidateAst = compileCandidates(candidates, designSystem, {
+ let compiled = compileCandidates(candidates, designSystem, {
onInvalidCandidate: (candidate) => {
throw new Error(`Cannot apply unknown utility class: ${candidate}`)
},
- }).astNodes
+ })
+
+ let candidateAst = compiled.astNodes
// Collect the nodes to insert in place of the `@apply` rule. When a rule
// was used, we want to insert its children instead of the rule because we
diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts
index 3db0432dd3a9..4c5cd40da1ef 100644
--- a/packages/tailwindcss/src/ast.ts
+++ b/packages/tailwindcss/src/ast.ts
@@ -378,10 +378,12 @@ export function optimizeAst(
}
}
+ let fallback = decl(property, initialValue ?? 'initial')
+
if (inherits) {
- propertyFallbacksRoot.push(decl(property, initialValue ?? 'initial'))
+ propertyFallbacksRoot.push(fallback)
} else {
- propertyFallbacksUniversal.push(decl(property, initialValue ?? 'initial'))
+ propertyFallbacksUniversal.push(fallback)
}
}
@@ -632,11 +634,13 @@ export function optimizeAst(
let fallbackAst = []
if (propertyFallbacksRoot.length > 0) {
- fallbackAst.push(rule(':root, :host', propertyFallbacksRoot))
+ let wrapper = rule(':root, :host', propertyFallbacksRoot)
+ fallbackAst.push(wrapper)
}
if (propertyFallbacksUniversal.length > 0) {
- fallbackAst.push(rule('*, ::before, ::after, ::backdrop', propertyFallbacksUniversal))
+ let wrapper = rule('*, ::before, ::after, ::backdrop', propertyFallbacksUniversal)
+ fallbackAst.push(wrapper)
}
if (fallbackAst.length > 0) {
@@ -658,23 +662,25 @@ export function optimizeAst(
return true
})
+ let layerPropertiesStatement = atRule('@layer', 'properties', [])
+
newAst.splice(
firstValidNodeIndex < 0 ? newAst.length : firstValidNodeIndex,
0,
- atRule('@layer', 'properties', []),
+ layerPropertiesStatement,
)
- newAst.push(
- rule('@layer properties', [
- atRule(
- '@supports',
- // We can't write a supports query for `@property` directly so we have to test for
- // features that are added around the same time in Mozilla and Safari.
- '((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b))))',
- fallbackAst,
- ),
- ]),
- )
+ let block = rule('@layer properties', [
+ atRule(
+ '@supports',
+ // We can't write a supports query for `@property` directly so we have to test for
+ // features that are added around the same time in Mozilla and Safari.
+ '((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b))))',
+ fallbackAst,
+ ),
+ ])
+
+ newAst.push(block)
}
}
@@ -710,7 +716,8 @@ export function toCss(ast: AstNode[]) {
// @layer base, components, utilities;
// ```
if (node.nodes.length === 0) {
- return `${indent}${node.name} ${node.params};\n`
+ let css = `${indent}${node.name} ${node.params};\n`
+ return css
}
css += `${indent}${node.name}${node.params ? ` ${node.params} ` : ' '}{\n`
@@ -725,7 +732,7 @@ export function toCss(ast: AstNode[]) {
css += `${indent}/*${node.value}*/\n`
}
- // These should've been handled already by `prepareAstForPrinting` which
+ // These should've been handled already by `optimizeAst` which
// means we can safely ignore them here. We return an empty string
// immediately to signal that something went wrong.
else if (node.kind === 'context' || node.kind === 'at-root') {
@@ -743,10 +750,7 @@ export function toCss(ast: AstNode[]) {
let css = ''
for (let node of ast) {
- let result = stringify(node)
- if (result !== '') {
- css += result
- }
+ css += stringify(node, 0)
}
return css
diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts
index effd33e5b203..343e9fbaea53 100644
--- a/packages/tailwindcss/src/at-import.ts
+++ b/packages/tailwindcss/src/at-import.ts
@@ -3,7 +3,13 @@ import { atRule, context, walk, WalkAction, type AstNode } from './ast'
import * as CSS from './css-parser'
import * as ValueParser from './value-parser'
-type LoadStylesheet = (id: string, basedir: string) => Promise<{ base: string; content: string }>
+type LoadStylesheet = (
+ id: string,
+ basedir: string,
+) => Promise<{
+ base: string
+ content: string
+}>
export async function substituteAtImports(
ast: AstNode[],
@@ -148,15 +154,18 @@ function buildImportNodes(
let root = importedAst
if (layer !== null) {
- root = [atRule('@layer', layer, root)]
+ let node = atRule('@layer', layer, root)
+ root = [node]
}
if (media !== null) {
- root = [atRule('@media', media, root)]
+ let node = atRule('@media', media, root)
+ root = [node]
}
if (supports !== null) {
- root = [atRule('@supports', supports[0] === '(' ? supports : `(${supports})`, root)]
+ let node = atRule('@supports', supports[0] === '(' ? supports : `(${supports})`, root)
+ root = [node]
}
return root
diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts
index a781cdcb0c78..eb23cec18d07 100644
--- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts
+++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts
@@ -30,7 +30,10 @@ export async function applyCompatibilityHooks({
path: string,
base: string,
resourceHint: 'plugin' | 'config',
- ) => Promise<{ module: any; base: string }>
+ ) => Promise<{
+ base: string
+ module: any
+ }>
sources: { base: string; pattern: string; negated: boolean }[]
}) {
let features = Features.None
diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts
index ca7d96dee648..6a8f95c58a5b 100644
--- a/packages/tailwindcss/src/compat/plugin-api.test.ts
+++ b/packages/tailwindcss/src/compat/plugin-api.test.ts
@@ -1508,10 +1508,10 @@ describe('addBase', () => {
},
async loadStylesheet() {
return {
+ base: '',
content: css`
@plugin "inside";
`,
- base: '',
}
},
})
@@ -1533,6 +1533,7 @@ describe('addBase', () => {
let compiler = await compile(input, {
loadModule: async () => ({
+ base: '/root',
module: plugin(function ({ addBase }) {
addBase({
':root': {
@@ -1542,7 +1543,6 @@ describe('addBase', () => {
},
})
}),
- base: '/root',
}),
})
diff --git a/packages/tailwindcss/src/css-functions.test.ts b/packages/tailwindcss/src/css-functions.test.ts
index f63682f63e01..968bb7f432f4 100644
--- a/packages/tailwindcss/src/css-functions.test.ts
+++ b/packages/tailwindcss/src/css-functions.test.ts
@@ -412,8 +412,8 @@ describe('--theme(…)', () => {
[],
{
loadModule: async () => ({
- module: () => {},
base: '/root',
+ module: () => {},
}),
},
),
@@ -771,7 +771,10 @@ describe('theme(…)', () => {
}
`,
{
- loadModule: async () => ({ module: {}, base: '/root' }),
+ loadModule: async () => ({
+ base: '/root',
+ module: {},
+ }),
},
)
@@ -1196,6 +1199,7 @@ describe('in plugins', () => {
{
async loadModule() {
return {
+ base: '/root',
module: plugin(({ addBase, addUtilities }) => {
addBase({
'.my-base-rule': {
@@ -1212,7 +1216,6 @@ describe('in plugins', () => {
},
})
}),
- base: '/root',
}
},
},
@@ -1253,6 +1256,7 @@ describe('in JS config files', () => {
`,
{
loadModule: async () => ({
+ base: '/root',
module: {
theme: {
extend: {
@@ -1279,7 +1283,6 @@ describe('in JS config files', () => {
}),
],
},
- base: '/root',
}),
},
)
diff --git a/packages/tailwindcss/src/css-parser.ts b/packages/tailwindcss/src/css-parser.ts
index df10fa850035..c492ed353ce4 100644
--- a/packages/tailwindcss/src/css-parser.ts
+++ b/packages/tailwindcss/src/css-parser.ts
@@ -31,8 +31,11 @@ const AT_SIGN = 0x40
const EXCLAMATION_MARK = 0x21
export function parse(input: string) {
- if (input[0] === '\uFEFF') input = input.slice(1)
- input = input.replaceAll('\r\n', '\n')
+ // Note: it is important that any transformations of the input string
+ // *before* processing do NOT change the length of the string. This
+ // would invalidate the mechanism used to track source locations.
+ if (input[0] === '\uFEFF') input = ' ' + input.slice(1)
+ input = input.replaceAll('\r\n', ' \n')
let ast: AstNode[] = []
let licenseComments: Comment[] = []
@@ -104,7 +107,8 @@ export function parse(input: string) {
// Collect all license comments so that we can hoist them to the top of
// the AST.
if (commentString.charCodeAt(2) === EXCLAMATION_MARK) {
- licenseComments.push(comment(commentString.slice(2, -2)))
+ let node = comment(commentString.slice(2, -2))
+ licenseComments.push(node)
}
}
@@ -503,7 +507,9 @@ export function parse(input: string) {
// means that we have an at-rule that is not terminated with a semicolon at
// the end of the input.
if (buffer.charCodeAt(0) === AT_SIGN) {
- ast.push(parseAtRule(buffer))
+ let node = parseAtRule(buffer)
+
+ ast.push(node)
}
// When we are done parsing then everything should be balanced. If we still
@@ -525,6 +531,9 @@ export function parse(input: string) {
}
export function parseAtRule(buffer: string, nodes: AstNode[] = []): AtRule {
+ let name = buffer
+ let params = ''
+
// Assumption: The smallest at-rule in CSS right now is `@page`, this means
// that we can always skip the first 5 characters and start at the
// sixth (at index 5).
@@ -545,13 +554,13 @@ export function parseAtRule(buffer: string, nodes: AstNode[] = []): AtRule {
for (let i = 5 /* '@page'.length */; i < buffer.length; i++) {
let currentChar = buffer.charCodeAt(i)
if (currentChar === SPACE || currentChar === OPEN_PAREN) {
- let name = buffer.slice(0, i).trim()
- let params = buffer.slice(i).trim()
- return atRule(name, params, nodes)
+ name = buffer.slice(0, i)
+ params = buffer.slice(i)
+ break
}
}
- return atRule(buffer.trim(), '', nodes)
+ return atRule(name.trim(), params.trim(), nodes)
}
function parseDeclaration(
diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts
index 00e73a90b405..22dd6b053a0f 100644
--- a/packages/tailwindcss/src/index.test.ts
+++ b/packages/tailwindcss/src/index.test.ts
@@ -126,11 +126,11 @@ describe('compiling CSS', () => {
{
async loadStylesheet(id) {
return {
+ base: '',
content: fs.readFileSync(
path.resolve(__dirname, '..', id === 'tailwindcss' ? 'index.css' : id),
'utf-8',
),
- base: '',
}
},
},
@@ -2320,6 +2320,7 @@ describe('Parsing theme values from CSS', () => {
{
async loadStylesheet() {
return {
+ base: '',
content: css`
@theme {
--color-tomato: #e10c04;
@@ -2329,7 +2330,6 @@ describe('Parsing theme values from CSS', () => {
@tailwind utilities;
`,
- base: '',
}
},
},
@@ -2407,6 +2407,7 @@ describe('Parsing theme values from CSS', () => {
{
async loadStylesheet() {
return {
+ base: '',
content: css`
@theme {
--color-tomato: #e10c04;
@@ -2416,7 +2417,6 @@ describe('Parsing theme values from CSS', () => {
@tailwind utilities;
`,
- base: '',
}
},
},
@@ -2704,6 +2704,7 @@ describe('Parsing theme values from CSS', () => {
{
loadModule: async () => {
return {
+ base: '/root',
module: plugin(({}) => {}, {
theme: {
extend: {
@@ -2714,7 +2715,6 @@ describe('Parsing theme values from CSS', () => {
},
},
}),
- base: '/root',
}
},
},
@@ -2750,6 +2750,7 @@ describe('Parsing theme values from CSS', () => {
{
loadModule: async () => {
return {
+ base: '/root',
module: {
theme: {
extend: {
@@ -2760,7 +2761,6 @@ describe('Parsing theme values from CSS', () => {
},
},
},
- base: '/root',
}
},
},
@@ -2839,10 +2839,10 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', '&:hover, &:focus')
},
- base: '/root',
}),
},
),
@@ -2857,10 +2857,10 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', '&:hover, &:focus')
},
- base: '/root',
}),
},
),
@@ -2877,10 +2877,10 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', '&:hover, &:focus')
},
- base: '/root',
}),
},
),
@@ -2899,6 +2899,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ base: '/root',
module: plugin.withOptions((options) => {
expect(options).toEqual({
color: 'red',
@@ -2912,7 +2913,6 @@ describe('plugins', () => {
})
}
}),
- base: '/root',
}),
},
)
@@ -2951,6 +2951,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ base: '/root',
module: plugin.withOptions((options) => {
expect(options).toEqual({
'is-null': null,
@@ -2971,7 +2972,6 @@ describe('plugins', () => {
return () => {}
}),
- base: '/root',
}),
},
)
@@ -2991,6 +2991,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ base: '/root',
module: plugin.withOptions((options) => {
return ({ addUtilities }) => {
addUtilities({
@@ -3000,7 +3001,6 @@ describe('plugins', () => {
})
}
}),
- base: '/root',
}),
},
),
@@ -3029,6 +3029,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ base: '/root',
module: plugin(({ addUtilities }) => {
addUtilities({
'.text-primary': {
@@ -3036,7 +3037,6 @@ describe('plugins', () => {
},
})
}),
- base: '/root',
}),
},
),
@@ -3054,7 +3054,10 @@ describe('plugins', () => {
}
`,
{
- loadModule: async () => ({ module: plugin(() => {}), base: '/root' }),
+ loadModule: async () => ({
+ base: '/root',
+ module: plugin(() => {}),
+ }),
},
),
).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -3075,7 +3078,10 @@ describe('plugins', () => {
}
`,
{
- loadModule: async () => ({ module: plugin(() => {}), base: '/root' }),
+ loadModule: async () => ({
+ base: '/root',
+ module: plugin(() => {}),
+ }),
},
),
).rejects.toThrowErrorMatchingInlineSnapshot(`
@@ -3099,10 +3105,10 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', '&:hover, &:focus')
},
- base: '/root',
}),
},
)
@@ -3131,10 +3137,10 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', ['&:hover', '&:focus'])
},
- base: '/root',
}),
},
)
@@ -3164,13 +3170,13 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', {
'&:hover': '@slot',
'&:focus': '@slot',
})
},
- base: '/root',
}),
},
)
@@ -3199,6 +3205,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', {
'@media (hover: hover)': {
@@ -3207,7 +3214,6 @@ describe('plugins', () => {
'&:focus': '@slot',
})
},
- base: '/root',
}),
},
)
@@ -3248,6 +3254,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', {
'&': {
@@ -3257,7 +3264,6 @@ describe('plugins', () => {
},
})
},
- base: '/root',
}),
},
)
@@ -3286,10 +3292,10 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('dark', '&:is([data-theme=dark] *)')
},
- base: '/root',
}),
},
)
@@ -4142,6 +4148,7 @@ test('addBase', async () => {
`,
{
loadModule: async () => ({
+ base: '/root',
module: ({ addBase }: PluginAPI) => {
addBase({
body: {
@@ -4149,7 +4156,6 @@ test('addBase', async () => {
},
})
},
- base: '/root',
}),
},
)
@@ -4201,6 +4207,7 @@ describe('`@reference "…" imports`', () => {
let loadStylesheet = async (id: string, base: string) => {
if (id === './foo/baz.css') {
return {
+ base: '/root/foo',
content: css`
.foo {
color: red;
@@ -4213,14 +4220,13 @@ describe('`@reference "…" imports`', () => {
}
@custom-variant hocus (&:hover, &:focus);
`,
- base: '/root/foo',
}
}
return {
+ base: '/root/foo',
content: css`
@import './foo/baz.css';
`,
- base: '/root/foo',
}
}
@@ -4249,19 +4255,19 @@ describe('`@reference "…" imports`', () => {
let loadStylesheet = async (id: string, base: string) => {
if (id === './foo/baz.css') {
return {
+ base: '/root/foo',
content: css`
@layer utilities {
@tailwind utilities;
}
`,
- base: '/root/foo',
}
}
return {
+ base: '/root/foo',
content: css`
@import './foo/baz.css';
`,
- base: '/root/foo',
}
}
@@ -4311,6 +4317,7 @@ describe('`@reference "…" imports`', () => {
['animate-spin', 'match-utility-initial', 'match-components-initial'],
{
loadModule: async () => ({
+ base: '/root',
module: ({
addBase,
addUtilities,
@@ -4344,7 +4351,6 @@ describe('`@reference "…" imports`', () => {
{ values: { initial: 'initial' } },
)
},
- base: '/root',
}),
},
),
@@ -4366,22 +4372,23 @@ describe('`@reference "…" imports`', () => {
switch (id) {
case './one.css': {
return {
+ base: '/root',
content: css`
@import './two.css' layer(two);
`,
- base: '/root',
}
}
case './two.css': {
return {
+ base: '/root',
content: css`
@import './three.css' layer(three);
`,
- base: '/root',
}
}
case './three.css': {
return {
+ base: '/root',
content: css`
.foo {
color: red;
@@ -4400,10 +4407,11 @@ describe('`@reference "…" imports`', () => {
}
}
`,
- base: '/root',
}
}
}
+
+ throw new Error('unreachable')
}
await expect(
@@ -4438,6 +4446,7 @@ describe('`@reference "…" imports`', () => {
test('supports `@import "…" reference` syntax', async () => {
let loadStylesheet = async () => {
return {
+ base: '/root/foo',
content: css`
.foo {
color: red;
@@ -4450,7 +4459,6 @@ describe('`@reference "…" imports`', () => {
}
@custom-variant hocus (&:hover, &:focus);
`,
- base: '/root/foo',
}
}
diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts
index e8f34e366327..792f7452fe95 100644
--- a/packages/tailwindcss/src/index.ts
+++ b/packages/tailwindcss/src/index.ts
@@ -56,8 +56,17 @@ type CompileOptions = {
id: string,
base: string,
resourceHint: 'plugin' | 'config',
- ) => Promise<{ module: Plugin | Config; base: string }>
- loadStylesheet?: (id: string, base: string) => Promise<{ content: string; base: string }>
+ ) => Promise<{
+ base: string
+ module: Plugin | Config
+ }>
+ loadStylesheet?: (
+ id: string,
+ base: string,
+ ) => Promise<{
+ base: string
+ content: string
+ }>
}
function throwOnLoadModule(): never {
@@ -593,8 +602,8 @@ async function parseCss(
for (let [key, value] of designSystem.theme.entries()) {
if (value.options & ThemeOptions.REFERENCE) continue
-
- nodes.push(decl(escape(key), value.value))
+ let node = decl(escape(key), value.value)
+ nodes.push(node)
}
let keyframesRules = designSystem.theme.getKeyframes()
diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts
index 5da1dcac7572..e1a1388f6912 100644
--- a/packages/tailwindcss/src/variants.test.ts
+++ b/packages/tailwindcss/src/variants.test.ts
@@ -2526,7 +2526,7 @@ test('matchVariant sorts deterministically', async () => {
for (let classList of classLists) {
let output = await compileCss('@tailwind utilities; @plugin "./plugin.js";', classList, {
- loadModule(id: string) {
+ async loadModule(id: string) {
return {
base: '/',
module: createPlugin(({ matchVariant }) => {
From 68ce361ef4eaa548d7decd137e48dbd7ab3f187d Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Fri, 18 Apr 2025 10:40:54 -0400
Subject: [PATCH 02/41] Return resolved path in `loadStylesheet` and
`loadModule`
Source map support needs this in loadStylesheet but it makes more sense to be consistent and require it for both.
---
packages/@tailwindcss-browser/src/index.ts | 4 +
packages/@tailwindcss-node/src/compile.ts | 4 +
packages/tailwindcss/src/at-import.ts | 1 +
.../src/compat/apply-compat-hooks.ts | 1 +
.../tailwindcss/src/compat/config.test.ts | 11 +++
.../tailwindcss/src/compat/plugin-api.test.ts | 83 +++++++++++++++++++
.../tailwindcss/src/css-functions.test.ts | 5 ++
packages/tailwindcss/src/index.test.ts | 32 +++++++
packages/tailwindcss/src/index.ts | 2 +
packages/tailwindcss/src/intellisense.test.ts | 11 +++
packages/tailwindcss/src/prefix.test.ts | 5 ++
packages/tailwindcss/src/variants.test.ts | 1 +
12 files changed, 160 insertions(+)
diff --git a/packages/@tailwindcss-browser/src/index.ts b/packages/@tailwindcss-browser/src/index.ts
index e7688a1219e7..74992157f5d5 100644
--- a/packages/@tailwindcss-browser/src/index.ts
+++ b/packages/@tailwindcss-browser/src/index.ts
@@ -116,6 +116,7 @@ async function loadStylesheet(id: string, base: string) {
function load() {
if (id === 'tailwindcss') {
return {
+ path: 'virtual:tailwindcss/index.css',
base,
content: assets.css.index,
}
@@ -125,6 +126,7 @@ async function loadStylesheet(id: string, base: string) {
id === './preflight.css'
) {
return {
+ path: 'virtual:tailwindcss/preflight.css',
base,
content: assets.css.preflight,
}
@@ -134,6 +136,7 @@ async function loadStylesheet(id: string, base: string) {
id === './theme.css'
) {
return {
+ path: 'virtual:tailwindcss/theme.css',
base,
content: assets.css.theme,
}
@@ -143,6 +146,7 @@ async function loadStylesheet(id: string, base: string) {
id === './utilities.css'
) {
return {
+ path: 'virtual:tailwindcss/utilities.css',
base,
content: assets.css.utilities,
}
diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts
index 85c4bd42f948..02c488afd0ba 100644
--- a/packages/@tailwindcss-node/src/compile.ts
+++ b/packages/@tailwindcss-node/src/compile.ts
@@ -125,6 +125,7 @@ export async function loadModule(
let module = await importModule(pathToFileURL(resolvedPath).href)
return {
+ path: resolvedPath,
base: path.dirname(resolvedPath),
module: module.default ?? module,
}
@@ -144,6 +145,7 @@ export async function loadModule(
onDependency(file)
}
return {
+ path: resolvedPath,
base: path.dirname(resolvedPath),
module: module.default ?? module,
}
@@ -164,6 +166,7 @@ async function loadStylesheet(
let file = await globalThis.__tw_readFile(resolvedPath, 'utf-8')
if (file) {
return {
+ path: resolvedPath,
base: path.dirname(resolvedPath),
content: file,
}
@@ -172,6 +175,7 @@ async function loadStylesheet(
let file = await fsPromises.readFile(resolvedPath, 'utf-8')
return {
+ path: resolvedPath,
base: path.dirname(resolvedPath),
content: file,
}
diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts
index 343e9fbaea53..2d2682ddd3b0 100644
--- a/packages/tailwindcss/src/at-import.ts
+++ b/packages/tailwindcss/src/at-import.ts
@@ -7,6 +7,7 @@ type LoadStylesheet = (
id: string,
basedir: string,
) => Promise<{
+ path: string
base: string
content: string
}>
diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts
index eb23cec18d07..fb619561bc13 100644
--- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts
+++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts
@@ -31,6 +31,7 @@ export async function applyCompatibilityHooks({
base: string,
resourceHint: 'plugin' | 'config',
) => Promise<{
+ path: string
base: string
module: any
}>
diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts
index 9e6c3b58a389..ea64fe3a1239 100644
--- a/packages/tailwindcss/src/compat/config.test.ts
+++ b/packages/tailwindcss/src/compat/config.test.ts
@@ -1156,6 +1156,7 @@ test('utilities must be prefixed', async () => {
let compiler = await compile(input, {
loadModule: async (id, base) => ({
+ path: '',
base,
module: { prefix: 'tw' },
}),
@@ -1183,6 +1184,7 @@ test('utilities must be prefixed', async () => {
// Non-prefixed utilities are ignored
compiler = await compile(input, {
loadModule: async (id, base) => ({
+ path: '',
base,
module: { prefix: 'tw' },
}),
@@ -1202,6 +1204,7 @@ test('utilities used in @apply must be prefixed', async () => {
`,
{
loadModule: async (id, base) => ({
+ path: '',
base,
module: { prefix: 'tw' },
}),
@@ -1228,6 +1231,7 @@ test('utilities used in @apply must be prefixed', async () => {
`,
{
loadModule: async (id, base) => ({
+ path: '',
base,
module: { prefix: 'tw' },
}),
@@ -1258,6 +1262,7 @@ test('Prefixes configured in CSS take precedence over those defined in JS config
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: { prefix: 'tw' },
}
@@ -1282,6 +1287,7 @@ test('a prefix must be letters only', async () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: { prefix: '__' },
}
@@ -1305,6 +1311,7 @@ test('important: `#app`', async () => {
let compiler = await compile(input, {
loadModule: async (_, base) => ({
+ path: '',
base,
module: { important: '#app' },
}),
@@ -1342,6 +1349,7 @@ test('important: true', async () => {
let compiler = await compile(input, {
loadModule: async (_, base) => ({
+ path: '',
base,
module: { important: true },
}),
@@ -1378,6 +1386,7 @@ test('blocklisted candidates are not generated', async () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: {
blocklist: ['bg-white'],
@@ -1421,6 +1430,7 @@ test('blocklisted candidates cannot be used with `@apply`', async () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: {
blocklist: ['bg-white'],
@@ -1473,6 +1483,7 @@ test('old theme values are merged with their renamed counterparts in the CSS the
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: plugin(function ({ theme }) {
didCallPluginFn()
diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts
index 6a8f95c58a5b..465974679646 100644
--- a/packages/tailwindcss/src/compat/plugin-api.test.ts
+++ b/packages/tailwindcss/src/compat/plugin-api.test.ts
@@ -17,6 +17,7 @@ describe('theme', async () => {
let compiler = await compile(input, {
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: plugin(
function ({ addBase, theme }) {
@@ -80,6 +81,7 @@ describe('theme', async () => {
let compiler = await compile(input, {
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: plugin(function ({ addUtilities, theme }) {
addUtilities({
@@ -116,6 +118,7 @@ describe('theme', async () => {
let compiler = await compile(input, {
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: plugin(
function ({ matchUtilities, theme }) {
@@ -164,6 +167,7 @@ describe('theme', async () => {
let compiler = await compile(input, {
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: plugin(
function ({ matchUtilities, theme }) {
@@ -211,6 +215,7 @@ describe('theme', async () => {
let compiler = await compile(input, {
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: plugin(
function ({ matchUtilities, theme }) {
@@ -265,6 +270,7 @@ describe('theme', async () => {
let compiler = await compile(input, {
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: plugin(function ({ addUtilities, theme }) {
addUtilities({
@@ -313,6 +319,7 @@ describe('theme', async () => {
let compiler = await compile(input, {
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: plugin(
function ({ addUtilities, theme }) {
@@ -398,6 +405,7 @@ describe('theme', async () => {
let compiler = await compile(input, {
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: plugin(
function ({ matchUtilities, theme }) {
@@ -452,6 +460,7 @@ describe('theme', async () => {
let compiler = await compile(input, {
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: plugin(
function ({ matchUtilities, theme }) {
@@ -499,6 +508,7 @@ describe('theme', async () => {
let compiler = await compile(input, {
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: plugin(function ({ matchUtilities, theme }) {
matchUtilities(
@@ -557,6 +567,7 @@ describe('theme', async () => {
let compiler = await compile(input, {
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: plugin(
function ({ matchUtilities, theme }) {
@@ -611,6 +622,7 @@ describe('theme', async () => {
let compiler = await compile(input, {
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: plugin(
function ({ matchUtilities, theme }) {
@@ -660,6 +672,7 @@ describe('theme', async () => {
await compile(input, {
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: plugin(
function ({ theme }) {
@@ -695,6 +708,7 @@ describe('theme', async () => {
let { build } = await compile(input, {
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: plugin(function ({ matchUtilities, theme }) {
function utility(name: string, themeKey: string) {
@@ -942,6 +956,7 @@ describe('theme', async () => {
await compile(input, {
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: plugin(
({ theme }) => {
@@ -984,6 +999,7 @@ describe('theme', async () => {
await compile(input, {
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: plugin(({ theme }) => {
fn(theme('transitionTimingFunction.DEFAULT'))
@@ -1015,6 +1031,7 @@ describe('theme', async () => {
await compile(input, {
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: plugin(({ theme }) => {
fn(theme('color.red.100'))
@@ -1043,6 +1060,7 @@ describe('theme', async () => {
await compile(input, {
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: plugin(({ theme }) => {
fn(theme('i.do.not.exist'))
@@ -1069,6 +1087,7 @@ describe('theme', async () => {
let { build } = await compile(input, {
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: plugin(({ addUtilities, matchUtilities }) => {
addUtilities({
@@ -1124,6 +1143,7 @@ describe('theme', async () => {
let { build } = await compile(input, {
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: plugin(function ({ matchUtilities }) {
function utility(name: string, themeKey: string) {
@@ -1342,6 +1362,7 @@ describe('theme', async () => {
let compiler = await compile(input, {
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: plugin(
function ({ matchUtilities, theme }) {
@@ -1400,6 +1421,7 @@ describe('theme', async () => {
let compiler = await compile(input, {
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: plugin(function ({ matchUtilities, theme }) {
matchUtilities(
@@ -1446,6 +1468,7 @@ describe('theme', async () => {
let compiler = await compile(input, {
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: plugin(
function ({ matchUtilities, theme }) {
@@ -1493,6 +1516,7 @@ describe('addBase', () => {
loadModule: async (id, base) => {
if (id === 'inside') {
return {
+ path: '',
base,
module: plugin(function ({ addBase }) {
addBase({ inside: { color: 'red' } })
@@ -1500,6 +1524,7 @@ describe('addBase', () => {
}
}
return {
+ path: '',
base,
module: plugin(function ({ addBase }) {
addBase({ outside: { color: 'red' } })
@@ -1508,6 +1533,7 @@ describe('addBase', () => {
},
async loadStylesheet() {
return {
+ path: '',
base: '',
content: css`
@plugin "inside";
@@ -1533,6 +1559,7 @@ describe('addBase', () => {
let compiler = await compile(input, {
loadModule: async () => ({
+ path: '',
base: '/root',
module: plugin(function ({ addBase }) {
addBase({
@@ -1571,6 +1598,7 @@ describe('addVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', '&:hover, &:focus')
@@ -1605,6 +1633,7 @@ describe('addVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', ['&:hover', '&:focus'])
@@ -1640,6 +1669,7 @@ describe('addVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', {
@@ -1677,6 +1707,7 @@ describe('addVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', {
@@ -1728,6 +1759,7 @@ describe('addVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ addVariant }: PluginAPI) => {
addVariant(
@@ -1769,6 +1801,7 @@ describe('addVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', {
@@ -1811,6 +1844,7 @@ describe('matchVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ matchVariant }: PluginAPI) => {
matchVariant('potato', (flavor) => `.potato-${flavor} &`)
@@ -1845,6 +1879,7 @@ describe('matchVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ matchVariant }: PluginAPI) => {
matchVariant('potato', (flavor) => `@media (potato: ${flavor})`)
@@ -1883,6 +1918,7 @@ describe('matchVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ matchVariant }: PluginAPI) => {
matchVariant(
@@ -1929,6 +1965,7 @@ describe('matchVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ matchVariant }: PluginAPI) => {
matchVariant('tooltip', (side) => `&${side}`, {
@@ -1968,6 +2005,7 @@ describe('matchVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ matchVariant }: PluginAPI) => {
matchVariant('alphabet', (side) => `&${side}`, {
@@ -2010,6 +2048,7 @@ describe('matchVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ matchVariant }: PluginAPI) => {
matchVariant('test', (selector) =>
@@ -2042,6 +2081,7 @@ describe('matchVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ matchVariant }: PluginAPI) => {
matchVariant('testmin', (value) => `@media (min-width: ${value})`, {
@@ -2094,6 +2134,7 @@ describe('matchVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ matchVariant }: PluginAPI) => {
matchVariant('testmin', (value) => `@media (min-width: ${value})`, {
@@ -2149,6 +2190,7 @@ describe('matchVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ matchVariant }: PluginAPI) => {
matchVariant('testmin', (value) => `@media (min-width: ${value})`, {
@@ -2221,6 +2263,7 @@ describe('matchVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ matchVariant }: PluginAPI) => {
matchVariant('testmin', (value) => `@media (min-width: ${value})`, {
@@ -2275,6 +2318,7 @@ describe('matchVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ matchVariant }: PluginAPI) => {
matchVariant('testmin', (value) => `@media (min-width: ${value})`, {
@@ -2347,6 +2391,7 @@ describe('matchVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ matchVariant }: PluginAPI) => {
matchVariant('testmin', (value) => `@media (min-width: ${value})`, {
@@ -2415,6 +2460,7 @@ describe('matchVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ matchVariant }: PluginAPI) => {
matchVariant('testmin', (value) => `@media (min-width: ${value})`, {
@@ -2495,6 +2541,7 @@ describe('matchVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ matchVariant }: PluginAPI) => {
matchVariant('foo', (value) => `.foo${value} &`, {
@@ -2529,6 +2576,7 @@ describe('matchVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ matchVariant }: PluginAPI) => {
matchVariant('foo', (value) => `.foo${value} &`)
@@ -2553,6 +2601,7 @@ describe('matchVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ matchVariant }: PluginAPI) => {
matchVariant('foo', (value) => `.foo${value === null ? '-good' : '-bad'} &`, {
@@ -2585,6 +2634,7 @@ describe('matchVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ matchVariant }: PluginAPI) => {
matchVariant('foo', (value) => `.foo${value === undefined ? '-good' : '-bad'} &`, {
@@ -2615,6 +2665,7 @@ describe('matchVariant', () => {
{
loadModule: async (id, base) => {
return {
+ path: '',
base,
module: ({ matchVariant }: PluginAPI) => {
matchVariant('my-container', (value, { modifier }) => {
@@ -2669,6 +2720,7 @@ describe('addUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ addUtilities }: PluginAPI) => {
addUtilities({
@@ -2710,6 +2762,7 @@ describe('addUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ addUtilities }: PluginAPI) => {
addUtilities([
@@ -2743,6 +2796,7 @@ describe('addUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ addUtilities }: PluginAPI) => {
addUtilities([
@@ -2782,6 +2836,7 @@ describe('addUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ addUtilities }: PluginAPI) => {
addUtilities([
@@ -2816,6 +2871,7 @@ describe('addUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ addUtilities }: PluginAPI) => {
addUtilities({
@@ -2857,6 +2913,7 @@ describe('addUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ addUtilities }: PluginAPI) => {
addUtilities({
@@ -2913,6 +2970,7 @@ describe('addUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ addUtilities }: PluginAPI) => {
addUtilities({
@@ -2942,6 +3000,7 @@ describe('addUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ addUtilities }: PluginAPI) => {
addUtilities({
@@ -2985,6 +3044,7 @@ describe('addUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ addUtilities }: PluginAPI) => {
addUtilities({
@@ -3026,6 +3086,7 @@ describe('addUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ addUtilities }: PluginAPI) => {
addUtilities({
@@ -3122,6 +3183,7 @@ describe('addUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ addUtilities }: PluginAPI) => {
addUtilities({
@@ -3175,6 +3237,7 @@ describe('addUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ addUtilities }: PluginAPI) => {
addUtilities({
@@ -3240,6 +3303,7 @@ describe('matchUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ matchUtilities }: PluginAPI) => {
matchUtilities(
@@ -3320,6 +3384,7 @@ describe('matchUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ matchUtilities }: PluginAPI) => {
matchUtilities(
@@ -3361,6 +3426,7 @@ describe('matchUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ matchUtilities }: PluginAPI) => {
matchUtilities(
@@ -3410,6 +3476,7 @@ describe('matchUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ matchUtilities }: PluginAPI) => {
matchUtilities(
@@ -3480,6 +3547,7 @@ describe('matchUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ matchUtilities }: PluginAPI) => {
matchUtilities(
@@ -3554,6 +3622,7 @@ describe('matchUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ matchUtilities }: PluginAPI) => {
matchUtilities(
@@ -3609,6 +3678,7 @@ describe('matchUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ matchUtilities }: PluginAPI) => {
matchUtilities(
@@ -3647,6 +3717,7 @@ describe('matchUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ matchUtilities }: PluginAPI) => {
matchUtilities(
@@ -3691,6 +3762,7 @@ describe('matchUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ matchUtilities }: PluginAPI) => {
matchUtilities(
@@ -3818,6 +3890,7 @@ describe('matchUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ matchUtilities }: PluginAPI) => {
matchUtilities(
@@ -3903,6 +3976,7 @@ describe('matchUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ matchUtilities }: PluginAPI) => {
matchUtilities(
@@ -3961,6 +4035,7 @@ describe('matchUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ matchUtilities }: PluginAPI) => {
matchUtilities(
@@ -4028,6 +4103,7 @@ describe('matchUtilities()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ matchUtilities }: PluginAPI) => {
matchUtilities({
@@ -4056,6 +4132,7 @@ describe('matchUtilities()', () => {
{
async loadModule(base) {
return {
+ path: '',
base,
module: ({ matchUtilities }: PluginAPI) => {
matchUtilities(
@@ -4122,6 +4199,7 @@ describe('addComponents()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ addComponents }: PluginAPI) => {
addComponents({
@@ -4190,6 +4268,7 @@ describe('matchComponents()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ matchComponents }: PluginAPI) => {
matchComponents(
@@ -4235,6 +4314,7 @@ describe('prefix()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ prefix }: PluginAPI) => {
fn(prefix('btn'))
@@ -4258,6 +4338,7 @@ describe('config()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ config }: PluginAPI) => {
fn(config())
@@ -4285,6 +4366,7 @@ describe('config()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ config }: PluginAPI) => {
fn(config('theme'))
@@ -4310,6 +4392,7 @@ describe('config()', () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: ({ config }: PluginAPI) => {
fn(config('somekey', 'defaultvalue'))
diff --git a/packages/tailwindcss/src/css-functions.test.ts b/packages/tailwindcss/src/css-functions.test.ts
index 968bb7f432f4..9c7502b8d4e5 100644
--- a/packages/tailwindcss/src/css-functions.test.ts
+++ b/packages/tailwindcss/src/css-functions.test.ts
@@ -412,6 +412,7 @@ describe('--theme(…)', () => {
[],
{
loadModule: async () => ({
+ path: '',
base: '/root',
module: () => {},
}),
@@ -772,6 +773,7 @@ describe('theme(…)', () => {
`,
{
loadModule: async () => ({
+ path: '',
base: '/root',
module: {},
}),
@@ -1199,6 +1201,7 @@ describe('in plugins', () => {
{
async loadModule() {
return {
+ path: '',
base: '/root',
module: plugin(({ addBase, addUtilities }) => {
addBase({
@@ -1256,6 +1259,7 @@ describe('in JS config files', () => {
`,
{
loadModule: async () => ({
+ path: '',
base: '/root',
module: {
theme: {
@@ -1317,6 +1321,7 @@ test('replaces CSS theme() function with values inside imported stylesheets', as
{
async loadStylesheet() {
return {
+ path: '',
base: '/bar.css',
content: css`
.red {
diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts
index 22dd6b053a0f..cc22114d33a8 100644
--- a/packages/tailwindcss/src/index.test.ts
+++ b/packages/tailwindcss/src/index.test.ts
@@ -126,6 +126,7 @@ describe('compiling CSS', () => {
{
async loadStylesheet(id) {
return {
+ path: '',
base: '',
content: fs.readFileSync(
path.resolve(__dirname, '..', id === 'tailwindcss' ? 'index.css' : id),
@@ -401,6 +402,7 @@ describe('@apply', () => {
{
async loadStylesheet() {
return {
+ path: '',
base: '/bar.css',
content: css`
.foo {
@@ -2320,6 +2322,7 @@ describe('Parsing theme values from CSS', () => {
{
async loadStylesheet() {
return {
+ path: '',
base: '',
content: css`
@theme {
@@ -2407,6 +2410,7 @@ describe('Parsing theme values from CSS', () => {
{
async loadStylesheet() {
return {
+ path: '',
base: '',
content: css`
@theme {
@@ -2704,6 +2708,7 @@ describe('Parsing theme values from CSS', () => {
{
loadModule: async () => {
return {
+ path: '',
base: '/root',
module: plugin(({}) => {}, {
theme: {
@@ -2750,6 +2755,7 @@ describe('Parsing theme values from CSS', () => {
{
loadModule: async () => {
return {
+ path: '',
base: '/root',
module: {
theme: {
@@ -2839,6 +2845,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ path: '',
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', '&:hover, &:focus')
@@ -2857,6 +2864,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ path: '',
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', '&:hover, &:focus')
@@ -2877,6 +2885,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ path: '',
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', '&:hover, &:focus')
@@ -2899,6 +2908,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ path: '',
base: '/root',
module: plugin.withOptions((options) => {
expect(options).toEqual({
@@ -2951,6 +2961,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ path: '',
base: '/root',
module: plugin.withOptions((options) => {
expect(options).toEqual({
@@ -2991,6 +3002,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ path: '',
base: '/root',
module: plugin.withOptions((options) => {
return ({ addUtilities }) => {
@@ -3029,6 +3041,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ path: '',
base: '/root',
module: plugin(({ addUtilities }) => {
addUtilities({
@@ -3055,6 +3068,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ path: '',
base: '/root',
module: plugin(() => {}),
}),
@@ -3079,6 +3093,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ path: '',
base: '/root',
module: plugin(() => {}),
}),
@@ -3105,6 +3120,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ path: '',
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', '&:hover, &:focus')
@@ -3137,6 +3153,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ path: '',
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', ['&:hover', '&:focus'])
@@ -3170,6 +3187,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ path: '',
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', {
@@ -3205,6 +3223,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ path: '',
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', {
@@ -3254,6 +3273,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ path: '',
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', {
@@ -3292,6 +3312,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
+ path: '',
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('dark', '&:is([data-theme=dark] *)')
@@ -4148,6 +4169,7 @@ test('addBase', async () => {
`,
{
loadModule: async () => ({
+ path: '',
base: '/root',
module: ({ addBase }: PluginAPI) => {
addBase({
@@ -4187,6 +4209,7 @@ it("should error when `layer(…)` is used, but it's not the first param", async
{
async loadStylesheet() {
return {
+ path: '',
base: '/bar.css',
content: css`
.foo {
@@ -4207,6 +4230,7 @@ describe('`@reference "…" imports`', () => {
let loadStylesheet = async (id: string, base: string) => {
if (id === './foo/baz.css') {
return {
+ path: '',
base: '/root/foo',
content: css`
.foo {
@@ -4223,6 +4247,7 @@ describe('`@reference "…" imports`', () => {
}
}
return {
+ path: '',
base: '/root/foo',
content: css`
@import './foo/baz.css';
@@ -4255,6 +4280,7 @@ describe('`@reference "…" imports`', () => {
let loadStylesheet = async (id: string, base: string) => {
if (id === './foo/baz.css') {
return {
+ path: '',
base: '/root/foo',
content: css`
@layer utilities {
@@ -4264,6 +4290,7 @@ describe('`@reference "…" imports`', () => {
}
}
return {
+ path: '',
base: '/root/foo',
content: css`
@import './foo/baz.css';
@@ -4317,6 +4344,7 @@ describe('`@reference "…" imports`', () => {
['animate-spin', 'match-utility-initial', 'match-components-initial'],
{
loadModule: async () => ({
+ path: '',
base: '/root',
module: ({
addBase,
@@ -4372,6 +4400,7 @@ describe('`@reference "…" imports`', () => {
switch (id) {
case './one.css': {
return {
+ path: '',
base: '/root',
content: css`
@import './two.css' layer(two);
@@ -4380,6 +4409,7 @@ describe('`@reference "…" imports`', () => {
}
case './two.css': {
return {
+ path: '',
base: '/root',
content: css`
@import './three.css' layer(three);
@@ -4388,6 +4418,7 @@ describe('`@reference "…" imports`', () => {
}
case './three.css': {
return {
+ path: '',
base: '/root',
content: css`
.foo {
@@ -4446,6 +4477,7 @@ describe('`@reference "…" imports`', () => {
test('supports `@import "…" reference` syntax', async () => {
let loadStylesheet = async () => {
return {
+ path: '',
base: '/root/foo',
content: css`
.foo {
diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts
index 792f7452fe95..541ec11a07c0 100644
--- a/packages/tailwindcss/src/index.ts
+++ b/packages/tailwindcss/src/index.ts
@@ -57,6 +57,7 @@ type CompileOptions = {
base: string,
resourceHint: 'plugin' | 'config',
) => Promise<{
+ path: string
base: string
module: Plugin | Config
}>
@@ -64,6 +65,7 @@ type CompileOptions = {
id: string,
base: string,
) => Promise<{
+ path: string
base: string
content: string
}>
diff --git a/packages/tailwindcss/src/intellisense.test.ts b/packages/tailwindcss/src/intellisense.test.ts
index 95d5d954d2ce..04a6ed7961b9 100644
--- a/packages/tailwindcss/src/intellisense.test.ts
+++ b/packages/tailwindcss/src/intellisense.test.ts
@@ -172,10 +172,12 @@ test('Utilities do not show wrapping selector in intellisense', async () => {
let design = await __unstable__loadDesignSystem(input, {
loadStylesheet: async (_, base) => ({
+ path: '',
base,
content: '@tailwind utilities;',
}),
loadModule: async () => ({
+ path: '',
base: '',
module: {
important: '#app',
@@ -208,6 +210,7 @@ test('Utilities, when marked as important, show as important in intellisense', a
let design = await __unstable__loadDesignSystem(input, {
loadStylesheet: async (_, base) => ({
+ path: '',
base,
content: '@tailwind utilities;',
}),
@@ -239,10 +242,12 @@ test('Static utilities from plugins are listed in hovers and completions', async
let design = await __unstable__loadDesignSystem(input, {
loadStylesheet: async (_, base) => ({
+ path: '',
base,
content: '@tailwind utilities;',
}),
loadModule: async () => ({
+ path: '',
base: '',
module: plugin(({ addUtilities }) => {
addUtilities({
@@ -274,10 +279,12 @@ test('Functional utilities from plugins are listed in hovers and completions', a
let design = await __unstable__loadDesignSystem(input, {
loadStylesheet: async (_, base) => ({
+ path: '',
base,
content: '@tailwind utilities;',
}),
loadModule: async () => ({
+ path: '',
base: '',
module: plugin(({ matchUtilities }) => {
matchUtilities(
@@ -420,10 +427,12 @@ test('Custom at-rule variants do not show up as a value under `group`', async ()
let design = await __unstable__loadDesignSystem(input, {
loadStylesheet: async (_, base) => ({
+ path: '',
base,
content: '@tailwind utilities;',
}),
loadModule: async () => ({
+ path: '',
base: '',
module: plugin(({ addVariant }) => {
addVariant('variant-3', '@media baz')
@@ -510,6 +519,7 @@ test('Custom functional @utility', async () => {
let design = await __unstable__loadDesignSystem(input, {
loadStylesheet: async (_, base) => ({
+ path: '',
base,
content: '@tailwind utilities;',
}),
@@ -587,6 +597,7 @@ test('Theme keys with underscores are suggested with underscores', async () => {
let design = await __unstable__loadDesignSystem(input, {
loadStylesheet: async (_, base) => ({
+ path: '',
base,
content: '@tailwind utilities;',
}),
diff --git a/packages/tailwindcss/src/prefix.test.ts b/packages/tailwindcss/src/prefix.test.ts
index 9556c34e3213..de426ab459fa 100644
--- a/packages/tailwindcss/src/prefix.test.ts
+++ b/packages/tailwindcss/src/prefix.test.ts
@@ -169,6 +169,7 @@ test('JS theme functions do not use the prefix', async () => {
{
async loadModule(id, base) {
return {
+ path: '',
base,
module: plugin(({ addUtilities, theme }) => {
addUtilities({
@@ -206,6 +207,7 @@ test('a prefix can be configured via @import theme(…)', async () => {
let compiler = await compile(input, {
async loadStylesheet(id, base) {
return {
+ path: '',
base,
content: css`
@theme {
@@ -250,6 +252,7 @@ test('a prefix can be configured via @import theme(…)', async () => {
compiler = await compile(input, {
async loadStylesheet(id, base) {
return {
+ path: '',
base,
content: css`
@theme {
@@ -275,6 +278,7 @@ test('a prefix can be configured via @import prefix(…)', async () => {
let compiler = await compile(input, {
async loadStylesheet(id, base) {
return {
+ path: '',
base,
content: css`
@theme {
@@ -314,6 +318,7 @@ test('a prefix can be configured via @import prefix(…)', async () => {
compiler = await compile(input, {
async loadStylesheet(id, base) {
return {
+ path: '',
base,
content: css`
@theme {
diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts
index e1a1388f6912..016191dac0a7 100644
--- a/packages/tailwindcss/src/variants.test.ts
+++ b/packages/tailwindcss/src/variants.test.ts
@@ -2528,6 +2528,7 @@ test('matchVariant sorts deterministically', async () => {
let output = await compileCss('@tailwind utilities; @plugin "./plugin.js";', classList, {
async loadModule(id: string) {
return {
+ path: '',
base: '/',
module: createPlugin(({ matchVariant }) => {
matchVariant('is-data', (value) => `&:is([data-${value}])`, {
From c53f34487155df956f2f6a08de233f6f5c7ee0d2 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Tue, 29 Apr 2025 11:18:47 -0400
Subject: [PATCH 03/41] Skip over the CR in CRLF when parsing CSS
---
packages/tailwindcss/src/css-parser.ts | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/packages/tailwindcss/src/css-parser.ts b/packages/tailwindcss/src/css-parser.ts
index c492ed353ce4..69eb1fca0a05 100644
--- a/packages/tailwindcss/src/css-parser.ts
+++ b/packages/tailwindcss/src/css-parser.ts
@@ -18,6 +18,7 @@ const SINGLE_QUOTE = 0x27
const COLON = 0x3a
const SEMICOLON = 0x3b
const LINE_BREAK = 0x0a
+const CARRIAGE_RETURN = 0xd
const SPACE = 0x20
const TAB = 0x09
const OPEN_CURLY = 0x7b
@@ -53,6 +54,14 @@ export function parse(input: string) {
for (let i = 0; i < input.length; i++) {
let currentChar = input.charCodeAt(i)
+ // Skip over the CR in CRLF. This allows code below to only check for a line
+ // break even if we're looking at a Windows newline. Peeking the input still
+ // has to check for CRLF but that happens less often.
+ if (currentChar === CARRIAGE_RETURN) {
+ peekChar = input.charCodeAt(i + 1)
+ if (peekChar === LINE_BREAK) continue
+ }
+
// Current character is a `\` therefore the next character is escaped,
// consume it together with the next character and continue.
//
From e7652b7a71aa67ad755aea7099dc5304ed26e769 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Tue, 29 Apr 2025 11:25:26 -0400
Subject: [PATCH 04/41] Check for CRLF when scanning consecutive whitespace
---
packages/tailwindcss/src/css-parser.ts | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/packages/tailwindcss/src/css-parser.ts b/packages/tailwindcss/src/css-parser.ts
index 69eb1fca0a05..e98f5382080d 100644
--- a/packages/tailwindcss/src/css-parser.ts
+++ b/packages/tailwindcss/src/css-parser.ts
@@ -191,7 +191,12 @@ export function parse(input: string) {
else if (
(currentChar === SPACE || currentChar === LINE_BREAK || currentChar === TAB) &&
(peekChar = input.charCodeAt(i + 1)) &&
- (peekChar === SPACE || peekChar === LINE_BREAK || peekChar === TAB)
+ (peekChar === SPACE ||
+ peekChar === LINE_BREAK ||
+ peekChar === TAB ||
+ (peekChar === CARRIAGE_RETURN &&
+ (peekChar = input.charCodeAt(i + 2)) &&
+ peekChar == LINE_BREAK))
) {
continue
}
From da86e231d49f681f0d2867e42b6baabe15beac27 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Tue, 29 Apr 2025 11:27:11 -0400
Subject: [PATCH 05/41] Handle CRLF in strings
---
packages/tailwindcss/src/css-parser.ts | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/packages/tailwindcss/src/css-parser.ts b/packages/tailwindcss/src/css-parser.ts
index e98f5382080d..e458ec124de9 100644
--- a/packages/tailwindcss/src/css-parser.ts
+++ b/packages/tailwindcss/src/css-parser.ts
@@ -159,7 +159,11 @@ export function parse(input: string) {
// ^ Missing "
// }
// ```
- else if (peekChar === SEMICOLON && input.charCodeAt(j + 1) === LINE_BREAK) {
+ else if (
+ peekChar === SEMICOLON &&
+ (input.charCodeAt(j + 1) === LINE_BREAK ||
+ (input.charCodeAt(j + 1) === CARRIAGE_RETURN && input.charCodeAt(j + 2) === LINE_BREAK))
+ ) {
throw new Error(
`Unterminated string: ${input.slice(start, j + 1) + String.fromCharCode(currentChar)}`,
)
@@ -175,7 +179,10 @@ export function parse(input: string) {
// ^ Missing "
// }
// ```
- else if (peekChar === LINE_BREAK) {
+ else if (
+ peekChar === LINE_BREAK ||
+ (peekChar === CARRIAGE_RETURN && input.charCodeAt(j + 1) === LINE_BREAK)
+ ) {
throw new Error(
`Unterminated string: ${input.slice(start, j) + String.fromCharCode(currentChar)}`,
)
From 65409a98d90f6862ea9d403d5479123cc5e7accd Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Tue, 29 Apr 2025 11:28:41 -0400
Subject: [PATCH 06/41] =?UTF-8?q?Don=E2=80=99t=20replace=20CRLF=20with=20L?=
=?UTF-8?q?F=20before=20parsing=20CSS?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Now that we can handle CRLF correctly in the parser this is no longer necessary
---
packages/tailwindcss/src/css-parser.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/packages/tailwindcss/src/css-parser.ts b/packages/tailwindcss/src/css-parser.ts
index e458ec124de9..89b06da5c9fd 100644
--- a/packages/tailwindcss/src/css-parser.ts
+++ b/packages/tailwindcss/src/css-parser.ts
@@ -36,7 +36,6 @@ export function parse(input: string) {
// *before* processing do NOT change the length of the string. This
// would invalidate the mechanism used to track source locations.
if (input[0] === '\uFEFF') input = ' ' + input.slice(1)
- input = input.replaceAll('\r\n', ' \n')
let ast: AstNode[] = []
let licenseComments: Comment[] = []
From 8319bd83b3d760d6e158de6af8b90d08442ab058 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Tue, 29 Apr 2025 11:29:05 -0400
Subject: [PATCH 07/41] Update CSS parser tests to preserve CRLF where needed
---
packages/tailwindcss/src/css-parser.test.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/packages/tailwindcss/src/css-parser.test.ts b/packages/tailwindcss/src/css-parser.test.ts
index 379b4b942793..f7ee47a61145 100644
--- a/packages/tailwindcss/src/css-parser.test.ts
+++ b/packages/tailwindcss/src/css-parser.test.ts
@@ -86,7 +86,7 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
kind: 'comment',
value: `!
* License #2
- `,
+ `.replaceAll(/\r?\n/g, lineEndings === 'Windows' ? '\r\n' : '\n'),
},
])
})
@@ -368,7 +368,7 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
background-color: red;
/* A comment */
content: 'Hello, world!';
- }`,
+ }`.replaceAll(/\r?\n/g, lineEndings === 'Windows' ? '\r\n' : '\n'),
important: false,
},
])
@@ -396,7 +396,7 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
background-color: red;
/* A comment ; */
content: 'Hello, world!';
- }`,
+ }`.replaceAll(/\r?\n/g, lineEndings === 'Windows' ? '\r\n' : '\n'),
important: false,
},
{
@@ -406,7 +406,7 @@ describe.each(['Unix', 'Windows'])('Line endings: %s', (lineEndings) => {
background-color: red;
/* A comment } */
content: 'Hello, world!';
- }`,
+ }`.replaceAll(/\r?\n/g, lineEndings === 'Windows' ? '\r\n' : '\n'),
important: false,
},
])
From f08f89d7ba7cfc15bb2b3df798700dd3231da338 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Wed, 16 Apr 2025 17:47:20 -0400
Subject: [PATCH 08/41] Track source locations in the AST
These are stored as character offsets for the original input (`src`) as well as the printed output (`dst`)
---
packages/tailwindcss/src/ast.ts | 19 +++++++++++++
.../tailwindcss/src/source-maps/source.ts | 27 +++++++++++++++++++
2 files changed, 46 insertions(+)
create mode 100644 packages/tailwindcss/src/source-maps/source.ts
diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts
index 4c5cd40da1ef..5f3032b54b0b 100644
--- a/packages/tailwindcss/src/ast.ts
+++ b/packages/tailwindcss/src/ast.ts
@@ -1,6 +1,7 @@
import { Polyfills } from '.'
import { parseAtRule } from './css-parser'
import type { DesignSystem } from './design-system'
+import type { Source, SourceLocation } from './source-maps/source'
import { Theme, ThemeOptions } from './theme'
import { DefaultMap } from './utils/default-map'
import { extractUsedVariables } from './utils/variables'
@@ -12,6 +13,9 @@ export type StyleRule = {
kind: 'rule'
selector: string
nodes: AstNode[]
+
+ src?: SourceLocation
+ dst?: SourceLocation
}
export type AtRule = {
@@ -19,6 +23,9 @@ export type AtRule = {
name: string
params: string
nodes: AstNode[]
+
+ src?: SourceLocation
+ dst?: SourceLocation
}
export type Declaration = {
@@ -26,22 +33,34 @@ export type Declaration = {
property: string
value: string | undefined
important: boolean
+
+ src?: SourceLocation
+ dst?: SourceLocation
}
export type Comment = {
kind: 'comment'
value: string
+
+ src?: SourceLocation
+ dst?: SourceLocation
}
export type Context = {
kind: 'context'
context: Record
nodes: AstNode[]
+
+ src?: undefined
+ dst?: undefined
}
export type AtRoot = {
kind: 'at-root'
nodes: AstNode[]
+
+ src?: undefined
+ dst?: undefined
}
export type Rule = StyleRule | AtRule
diff --git a/packages/tailwindcss/src/source-maps/source.ts b/packages/tailwindcss/src/source-maps/source.ts
new file mode 100644
index 000000000000..c3a4f8988488
--- /dev/null
+++ b/packages/tailwindcss/src/source-maps/source.ts
@@ -0,0 +1,27 @@
+/**
+ * The source code for one or more nodes in the AST
+ *
+ * This generally corresponds to a stylesheet
+ */
+export interface Source {
+ /**
+ * The path to the file that contains the referenced source code
+ *
+ * If this references the *output* source code, this is `null`.
+ */
+ file: string | null
+
+ /**
+ * The referenced source code
+ */
+ code: string
+}
+
+/**
+ * The file and offsets within it that this node covers
+ *
+ * This can represent either:
+ * - A location in the original CSS which caused this node to be created
+ * - A location in the output CSS where this node resides
+ */
+export type SourceLocation = [source: Source, start: number, end: number]
From e95e6e239f0cfa7aa69ef29592e5e4a32d1089ef Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Wed, 16 Apr 2025 17:36:57 -0400
Subject: [PATCH 09/41] Track locations when parsing
---
packages/@tailwindcss-node/src/compile.ts | 3 +
packages/tailwindcss/src/css-parser.bench.ts | 4 ++
packages/tailwindcss/src/css-parser.ts | 58 +++++++++++++++++++-
packages/tailwindcss/src/index.ts | 4 +-
4 files changed, 67 insertions(+), 2 deletions(-)
diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts
index 02c488afd0ba..5ce6299af6a2 100644
--- a/packages/@tailwindcss-node/src/compile.ts
+++ b/packages/@tailwindcss-node/src/compile.ts
@@ -21,6 +21,7 @@ export type Resolver = (id: string, base: string) => Promise void
shouldRewriteUrls?: boolean
polyfills?: Polyfills
@@ -31,6 +32,7 @@ export interface CompileOptions {
function createCompileOptions({
base,
+ from,
polyfills,
onDependency,
shouldRewriteUrls,
@@ -41,6 +43,7 @@ function createCompileOptions({
return {
base,
polyfills,
+ from,
async loadModule(id: string, base: string) {
return loadModule(id, base, onDependency, customJsResolver)
},
diff --git a/packages/tailwindcss/src/css-parser.bench.ts b/packages/tailwindcss/src/css-parser.bench.ts
index ab490e103849..d48f88ab3841 100644
--- a/packages/tailwindcss/src/css-parser.bench.ts
+++ b/packages/tailwindcss/src/css-parser.bench.ts
@@ -10,3 +10,7 @@ const cssFile = readFileSync(currentFolder + './preflight.css', 'utf-8')
bench('css-parser on preflight.css', () => {
CSS.parse(cssFile)
})
+
+bench('CSS with sourcemaps', () => {
+ CSS.parse(cssFile, { from: 'input.css' })
+})
diff --git a/packages/tailwindcss/src/css-parser.ts b/packages/tailwindcss/src/css-parser.ts
index 89b06da5c9fd..6a5ce83204bd 100644
--- a/packages/tailwindcss/src/css-parser.ts
+++ b/packages/tailwindcss/src/css-parser.ts
@@ -9,6 +9,7 @@ import {
type Declaration,
type Rule,
} from './ast'
+import type { Source } from './source-maps/source'
const BACKSLASH = 0x5c
const SLASH = 0x2f
@@ -31,7 +32,13 @@ const DASH = 0x2d
const AT_SIGN = 0x40
const EXCLAMATION_MARK = 0x21
-export function parse(input: string) {
+export interface ParseOptions {
+ from?: string
+}
+
+export function parse(input: string, opts?: ParseOptions) {
+ let source: Source | null = opts?.from ? { file: opts.from, code: input } : null
+
// Note: it is important that any transformations of the input string
// *before* processing do NOT change the length of the string. This
// would invalidate the mechanism used to track source locations.
@@ -48,6 +55,9 @@ export function parse(input: string) {
let buffer = ''
let closingBracketStack = ''
+ // The start of the first non-whitespace character in the buffer
+ let bufferStart = 0
+
let peekChar
for (let i = 0; i < input.length; i++) {
@@ -72,6 +82,7 @@ export function parse(input: string) {
// ```
//
if (currentChar === BACKSLASH) {
+ if (buffer === '') bufferStart = i
buffer += input.slice(i, i + 2)
i += 1
}
@@ -117,6 +128,11 @@ export function parse(input: string) {
if (commentString.charCodeAt(2) === EXCLAMATION_MARK) {
let node = comment(commentString.slice(2, -2))
licenseComments.push(node)
+
+ if (source) {
+ node.src = [source, start, i + 1]
+ node.dst = [source, start, i + 1]
+ }
}
}
@@ -313,6 +329,11 @@ export function parse(input: string) {
let declaration = parseDeclaration(buffer, colonIdx)
if (!declaration) throw new Error(`Invalid custom property, expected a value`)
+ if (source) {
+ declaration.src = [source, start, i]
+ declaration.dst = [source, start, i]
+ }
+
if (parent) {
parent.nodes.push(declaration)
} else {
@@ -333,6 +354,11 @@ export function parse(input: string) {
else if (currentChar === SEMICOLON && buffer.charCodeAt(0) === AT_SIGN) {
node = parseAtRule(buffer)
+ if (source) {
+ node.src = [source, bufferStart, i]
+ node.dst = [source, bufferStart, i]
+ }
+
// At-rule is nested inside of a rule, attach it to the parent.
if (parent) {
parent.nodes.push(node)
@@ -369,6 +395,11 @@ export function parse(input: string) {
throw new Error(`Invalid declaration: \`${buffer.trim()}\``)
}
+ if (source) {
+ declaration.src = [source, bufferStart, i]
+ declaration.dst = [source, bufferStart, i]
+ }
+
if (parent) {
parent.nodes.push(declaration)
} else {
@@ -388,6 +419,12 @@ export function parse(input: string) {
// At this point `buffer` should resemble a selector or an at-rule.
node = rule(buffer.trim())
+ // Track the source location for source maps
+ if (source) {
+ node.src = [source, bufferStart, i]
+ node.dst = [source, bufferStart, i]
+ }
+
// Attach the rule to the parent in case it's nested.
if (parent) {
parent.nodes.push(node)
@@ -434,6 +471,12 @@ export function parse(input: string) {
if (buffer.charCodeAt(0) === AT_SIGN) {
node = parseAtRule(buffer)
+ // Track the source location for source maps
+ if (source) {
+ node.src = [source, bufferStart, i]
+ node.dst = [source, bufferStart, i]
+ }
+
// At-rule is nested inside of a rule, attach it to the parent.
if (parent) {
parent.nodes.push(node)
@@ -470,6 +513,11 @@ export function parse(input: string) {
let node = parseDeclaration(buffer, colonIdx)
if (!node) throw new Error(`Invalid declaration: \`${buffer.trim()}\``)
+ if (source) {
+ node.src = [source, bufferStart, i]
+ node.dst = [source, bufferStart, i]
+ }
+
parent.nodes.push(node)
}
}
@@ -519,6 +567,8 @@ export function parse(input: string) {
continue
}
+ if (buffer === '') bufferStart = i
+
buffer += String.fromCharCode(currentChar)
}
}
@@ -529,6 +579,12 @@ export function parse(input: string) {
if (buffer.charCodeAt(0) === AT_SIGN) {
let node = parseAtRule(buffer)
+ // Track the source location for source maps
+ if (source) {
+ node.src = [source, bufferStart, input.length]
+ node.dst = [source, bufferStart, input.length]
+ }
+
ast.push(node)
}
diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts
index 541ec11a07c0..10dba2386d61 100644
--- a/packages/tailwindcss/src/index.ts
+++ b/packages/tailwindcss/src/index.ts
@@ -51,6 +51,7 @@ export const enum Polyfills {
type CompileOptions = {
base?: string
+ from?: string
polyfills?: Polyfills
loadModule?: (
id: string,
@@ -136,6 +137,7 @@ async function parseCss(
ast: AstNode[],
{
base = '',
+ from,
loadModule = throwOnLoadModule,
loadStylesheet = throwOnLoadStylesheet,
}: CompileOptions = {},
@@ -786,7 +788,7 @@ export async function compile(
features: Features
build(candidates: string[]): string
}> {
- let ast = CSS.parse(css)
+ let ast = CSS.parse(css, { from: opts.from })
let api = await compileAst(ast, opts)
let compiledAst = ast
let compiledCss = css
From c129f48caeb949b37c3e639434e52943dff562ff Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Thu, 3 Apr 2025 13:29:55 -0400
Subject: [PATCH 10/41] Track locations inside `@import`
---
packages/tailwindcss/src/at-import.ts | 5 +++--
packages/tailwindcss/src/index.ts | 2 +-
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts
index 2d2682ddd3b0..8853a9e5fdcc 100644
--- a/packages/tailwindcss/src/at-import.ts
+++ b/packages/tailwindcss/src/at-import.ts
@@ -17,6 +17,7 @@ export async function substituteAtImports(
base: string,
loadStylesheet: LoadStylesheet,
recurseCount = 0,
+ track = false,
) {
let features = Features.None
let promises: Promise[] = []
@@ -52,8 +53,8 @@ export async function substituteAtImports(
}
let loaded = await loadStylesheet(uri, base)
- let ast = CSS.parse(loaded.content)
- await substituteAtImports(ast, loaded.base, loadStylesheet, recurseCount + 1)
+ let ast = CSS.parse(loaded.content, { from: track ? loaded.path : undefined })
+ await substituteAtImports(ast, loaded.base, loadStylesheet, recurseCount + 1, track)
contextNode.nodes = buildImportNodes(
[context({ base: loaded.base }, ast)],
diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts
index 10dba2386d61..6adb8fffc792 100644
--- a/packages/tailwindcss/src/index.ts
+++ b/packages/tailwindcss/src/index.ts
@@ -145,7 +145,7 @@ async function parseCss(
let features = Features.None
ast = [contextNode({ base }, ast)] as AstNode[]
- features |= await substituteAtImports(ast, base, loadStylesheet)
+ features |= await substituteAtImports(ast, base, loadStylesheet, 0, from !== undefined)
let important = null as boolean | null
let theme = new Theme()
From 3bc80cda67a4f3eb2eefff4e5ae38fa36ad9a166 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Fri, 18 Apr 2025 13:12:09 -0400
Subject: [PATCH 11/41] Track wrapping at-rules created by imports
---
packages/tailwindcss/src/at-import.ts | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/packages/tailwindcss/src/at-import.ts b/packages/tailwindcss/src/at-import.ts
index 8853a9e5fdcc..5f13cce99c51 100644
--- a/packages/tailwindcss/src/at-import.ts
+++ b/packages/tailwindcss/src/at-import.ts
@@ -57,6 +57,7 @@ export async function substituteAtImports(
await substituteAtImports(ast, loaded.base, loadStylesheet, recurseCount + 1, track)
contextNode.nodes = buildImportNodes(
+ node,
[context({ base: loaded.base }, ast)],
layer,
media,
@@ -148,6 +149,7 @@ export function parseImportParams(params: ValueParser.ValueAstNode[]) {
}
function buildImportNodes(
+ importNode: AstNode,
importedAst: AstNode[],
layer: string | null,
media: string | null,
@@ -157,16 +159,19 @@ function buildImportNodes(
if (layer !== null) {
let node = atRule('@layer', layer, root)
+ node.src = importNode.src
root = [node]
}
if (media !== null) {
let node = atRule('@media', media, root)
+ node.src = importNode.src
root = [node]
}
if (supports !== null) {
let node = atRule('@supports', supports[0] === '(' ? supports : `(${supports})`, root)
+ node.src = importNode.src
root = [node]
}
From 3143eb6e7ad176470ebf9e1387951aaebfc8b83c Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Wed, 16 Apr 2025 17:26:37 -0400
Subject: [PATCH 12/41] Track theme variables
---
packages/tailwindcss/src/index.ts | 4 +++-
packages/tailwindcss/src/theme.ts | 15 +++++++++++----
2 files changed, 14 insertions(+), 5 deletions(-)
diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts
index 6adb8fffc792..2e1968d405d2 100644
--- a/packages/tailwindcss/src/index.ts
+++ b/packages/tailwindcss/src/index.ts
@@ -541,7 +541,7 @@ async function parseCss(
if (child.kind === 'comment') return
if (child.kind === 'declaration' && child.property.startsWith('--')) {
- theme.add(unescape(child.property), child.value ?? '', themeOptions)
+ theme.add(unescape(child.property), child.value ?? '', themeOptions, child.src)
return
}
@@ -559,6 +559,7 @@ async function parseCss(
// theme later, and delete any other `@theme` rules.
if (!firstThemeRule) {
firstThemeRule = styleRule(':root, :host', [])
+ firstThemeRule.src = node.src
replaceWith([firstThemeRule])
} else {
replaceWith([])
@@ -607,6 +608,7 @@ async function parseCss(
for (let [key, value] of designSystem.theme.entries()) {
if (value.options & ThemeOptions.REFERENCE) continue
let node = decl(escape(key), value.value)
+ node.src = value.src
nodes.push(node)
}
diff --git a/packages/tailwindcss/src/theme.ts b/packages/tailwindcss/src/theme.ts
index 74375b6f723d..2928afd3bd55 100644
--- a/packages/tailwindcss/src/theme.ts
+++ b/packages/tailwindcss/src/theme.ts
@@ -1,4 +1,4 @@
-import { type AtRule } from './ast'
+import { type AtRule, type Declaration } from './ast'
import { escape, unescape } from './utils/escape'
export const enum ThemeOptions {
@@ -40,11 +40,18 @@ export class Theme {
public prefix: string | null = null
constructor(
- private values = new Map(),
+ private values = new Map<
+ string,
+ {
+ value: string
+ options: ThemeOptions
+ src: Declaration['src']
+ }
+ >(),
private keyframes = new Set([]),
) {}
- add(key: string, value: string, options = ThemeOptions.NONE): void {
+ add(key: string, value: string, options = ThemeOptions.NONE, src?: Declaration['src']): void {
if (key.endsWith('-*')) {
if (value !== 'initial') {
throw new Error(`Invalid theme value \`${value}\` for namespace \`${key}\``)
@@ -68,7 +75,7 @@ export class Theme {
if (value === 'initial') {
this.values.delete(key)
} else {
- this.values.set(key, { value, options })
+ this.values.set(key, { value, options, src })
}
}
From 386e72158ccd9d80f63547a639c15e788d19d13d Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Wed, 16 Apr 2025 17:40:49 -0400
Subject: [PATCH 13/41] Track generated utilities
---
packages/tailwindcss/src/index.ts | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts
index 2e1968d405d2..04a99b927d85 100644
--- a/packages/tailwindcss/src/index.ts
+++ b/packages/tailwindcss/src/index.ts
@@ -763,6 +763,16 @@ export async function compileAst(
onInvalidCandidate,
}).astNodes
+ if (opts.from) {
+ walk(newNodes, (node) => {
+ // We do this conditionally to preserve source locations from both
+ // `@utility` and `@custom-variant`. Even though generated nodes are
+ // cached this should be fine because `utilitiesNode.src` should not
+ // change without a full rebuild which destroys the cache.
+ node.src ??= utilitiesNode.src
+ })
+ }
+
// If no new ast nodes were generated, then we can return the original
// CSS. This currently assumes that we only add new ast nodes and never
// remove any.
From fcd4e2d2bc7c8cc174bbcd8e573f894ba74df32b Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Wed, 16 Apr 2025 17:48:31 -0400
Subject: [PATCH 14/41] Track locations when optimizing
---
packages/tailwindcss/src/ast.ts | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts
index 5f3032b54b0b..a3f1835b0aef 100644
--- a/packages/tailwindcss/src/ast.ts
+++ b/packages/tailwindcss/src/ast.ts
@@ -398,6 +398,7 @@ export function optimizeAst(
}
let fallback = decl(property, initialValue ?? 'initial')
+ fallback.src = node.src
if (inherits) {
propertyFallbacksRoot.push(fallback)
@@ -644,6 +645,7 @@ export function optimizeAst(
value: ValueParser.toCss(ast),
}
let colorMixQuery = rule('@supports (color: color-mix(in lab, red, red))', [declaration])
+ colorMixQuery.src = declaration.src
parent.splice(idx, 1, fallback, colorMixQuery)
}
}
@@ -654,11 +656,13 @@ export function optimizeAst(
if (propertyFallbacksRoot.length > 0) {
let wrapper = rule(':root, :host', propertyFallbacksRoot)
+ wrapper.src = propertyFallbacksRoot[0].src
fallbackAst.push(wrapper)
}
if (propertyFallbacksUniversal.length > 0) {
let wrapper = rule('*, ::before, ::after, ::backdrop', propertyFallbacksUniversal)
+ wrapper.src = propertyFallbacksUniversal[0].src
fallbackAst.push(wrapper)
}
@@ -682,6 +686,7 @@ export function optimizeAst(
})
let layerPropertiesStatement = atRule('@layer', 'properties', [])
+ layerPropertiesStatement.src = fallbackAst[0].src
newAst.splice(
firstValidNodeIndex < 0 ? newAst.length : firstValidNodeIndex,
@@ -699,6 +704,9 @@ export function optimizeAst(
),
])
+ block.src = fallbackAst[0].src
+ block.nodes[0].src = fallbackAst[0].src
+
newAst.push(block)
}
}
From 6febfd966a4c2c5f16a5301dfb86e619ab1f19cd Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Wed, 16 Apr 2025 17:48:26 -0400
Subject: [PATCH 15/41] Track locations when printing
---
packages/tailwindcss/src/ast.bench.ts | 28 ++++++
packages/tailwindcss/src/ast.ts | 140 +++++++++++++++++++++++++-
packages/tailwindcss/src/index.ts | 2 +-
3 files changed, 168 insertions(+), 2 deletions(-)
create mode 100644 packages/tailwindcss/src/ast.bench.ts
diff --git a/packages/tailwindcss/src/ast.bench.ts b/packages/tailwindcss/src/ast.bench.ts
new file mode 100644
index 000000000000..a2d8b28920f2
--- /dev/null
+++ b/packages/tailwindcss/src/ast.bench.ts
@@ -0,0 +1,28 @@
+import { bench } from 'vitest'
+import { toCss } from './ast'
+import * as CSS from './css-parser'
+
+const css = String.raw
+const input = css`
+ @theme {
+ --color-primary: #333;
+ }
+ @tailwind utilities;
+ .foo {
+ color: red;
+ /* comment */
+ &:hover {
+ color: blue;
+ @apply font-bold;
+ }
+ }
+`
+const ast = CSS.parse(input)
+
+bench('toCss', () => {
+ toCss(ast)
+})
+
+bench('toCss with source maps', () => {
+ toCss(ast, true)
+})
diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts
index a3f1835b0aef..e4b5d5a9ab52 100644
--- a/packages/tailwindcss/src/ast.ts
+++ b/packages/tailwindcss/src/ast.ts
@@ -714,7 +714,14 @@ export function optimizeAst(
return newAst
}
-export function toCss(ast: AstNode[]) {
+export function toCss(ast: AstNode[], track?: boolean) {
+ let pos = 0
+
+ let source: Source = {
+ file: null,
+ code: '',
+ }
+
function stringify(node: AstNode, depth = 0): string {
let css = ''
let indent = ' '.repeat(depth)
@@ -722,15 +729,70 @@ export function toCss(ast: AstNode[]) {
// Declaration
if (node.kind === 'declaration') {
css += `${indent}${node.property}: ${node.value}${node.important ? ' !important' : ''};\n`
+
+ if (track) {
+ // indent
+ pos += indent.length
+
+ // node.property
+ let start = pos
+ pos += node.property.length
+
+ // `: `
+ pos += 2
+
+ // node.value
+ pos += node.value?.length ?? 0
+
+ // !important
+ if (node.important) {
+ pos += 11
+ }
+
+ let end = pos
+
+ // `;\n`
+ pos += 2
+
+ node.dst = [source, start, end]
+ }
}
// Rule
else if (node.kind === 'rule') {
css += `${indent}${node.selector} {\n`
+
+ if (track) {
+ // indent
+ pos += indent.length
+
+ // node.selector
+ let start = pos
+ pos += node.selector.length
+
+ // ` `
+ pos += 1
+
+ let end = pos
+ node.dst = [source, start, end]
+
+ // `{\n`
+ pos += 2
+ }
+
for (let child of node.nodes) {
css += stringify(child, depth + 1)
}
+
css += `${indent}}\n`
+
+ if (track) {
+ // indent
+ pos += indent.length
+
+ // `}\n`
+ pos += 2
+ }
}
// AtRule
@@ -744,19 +806,93 @@ export function toCss(ast: AstNode[]) {
// ```
if (node.nodes.length === 0) {
let css = `${indent}${node.name} ${node.params};\n`
+
+ if (track) {
+ // indent
+ pos += indent.length
+
+ // node.name
+ let start = pos
+ pos += node.name.length
+
+ // ` `
+ pos += 1
+
+ // node.params
+ pos += node.params.length
+ let end = pos
+
+ // `;\n`
+ pos += 2
+
+ node.dst = [source, start, end]
+ }
+
return css
}
css += `${indent}${node.name}${node.params ? ` ${node.params} ` : ' '}{\n`
+
+ if (track) {
+ // indent
+ pos += indent.length
+
+ // node.name
+ let start = pos
+ pos += node.name.length
+
+ if (node.params) {
+ // ` `
+ pos += 1
+
+ // node.params
+ pos += node.params.length
+ }
+
+ // ` `
+ pos += 1
+
+ let end = pos
+ node.dst = [source, start, end]
+
+ // `{\n`
+ pos += 2
+ }
+
for (let child of node.nodes) {
css += stringify(child, depth + 1)
}
+
css += `${indent}}\n`
+
+ if (track) {
+ // indent
+ pos += indent.length
+
+ // `}\n`
+ pos += 2
+ }
}
// Comment
else if (node.kind === 'comment') {
css += `${indent}/*${node.value}*/\n`
+
+ if (track) {
+ // indent
+ pos += indent.length
+
+ // The comment itself. We do this instead of just the inside because
+ // it seems more useful to have the entire comment span tracked.
+ let start = pos
+ pos += 2 + node.value.length + 2
+ let end = pos
+
+ node.dst = [source, start, end]
+
+ // `\n`
+ pos += 1
+ }
}
// These should've been handled already by `optimizeAst` which
@@ -780,6 +916,8 @@ export function toCss(ast: AstNode[]) {
css += stringify(node, 0)
}
+ source.code = css
+
return css
}
diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts
index 04a99b927d85..b109713ee265 100644
--- a/packages/tailwindcss/src/index.ts
+++ b/packages/tailwindcss/src/index.ts
@@ -814,7 +814,7 @@ export async function compile(
return compiledCss
}
- compiledCss = toCss(newAst)
+ compiledCss = toCss(newAst, !!opts.from)
compiledAst = newAst
return compiledCss
From 6b9ace26799f5dbcdefb64a6175fc52cf205a4da Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Wed, 16 Apr 2025 18:04:03 -0400
Subject: [PATCH 16/41] Track utilities generated by `@apply`
---
packages/tailwindcss/src/apply.ts | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts
index 6038c4b8dfbb..0b7b3ad16f24 100644
--- a/packages/tailwindcss/src/apply.ts
+++ b/packages/tailwindcss/src/apply.ts
@@ -167,8 +167,13 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
},
})
+ let src = node.src
let candidateAst = compiled.astNodes
+ walk(candidateAst, (node) => {
+ node.src = src
+ })
+
// Collect the nodes to insert in place of the `@apply` rule. When a rule
// was used, we want to insert its children instead of the rule because we
// don't want the wrapping selector.
From 9a9b47a1faf2339f172dd329e35fa3e272c77201 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Wed, 16 Apr 2025 18:05:04 -0400
Subject: [PATCH 17/41] Track utilities per-candidate in `@apply`
---
packages/tailwindcss/src/apply.ts | 41 ++++++++++++++++++++++++++++---
1 file changed, 37 insertions(+), 4 deletions(-)
diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts
index 0b7b3ad16f24..71cfc1e039ce 100644
--- a/packages/tailwindcss/src/apply.ts
+++ b/packages/tailwindcss/src/apply.ts
@@ -2,6 +2,7 @@ import { Features } from '.'
import { rule, toCss, walk, WalkAction, type AstNode } from './ast'
import { compileCandidates } from './compile'
import type { DesignSystem } from './design-system'
+import type { SourceLocation } from './source-maps/source'
import { DefaultMap } from './utils/default-map'
export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
@@ -155,12 +156,20 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
let node = parent.nodes[i]
if (node.kind !== 'at-rule' || node.name !== '@apply') continue
- let candidates = node.params.split(/\s+/g)
+ let parts = node.params.split(/(\s+)/g)
+ let candidateOffsets: Record = {}
+
+ let offset = 0
+ for (let [idx, part] of parts.entries()) {
+ if (idx % 2 === 0) candidateOffsets[part] = offset
+ offset += part.length
+ }
// Replace the `@apply` rule with the actual utility classes
{
// Parse the candidates to an AST that we can replace the `@apply` rule
// with.
+ let candidates = Object.keys(candidateOffsets)
let compiled = compileCandidates(candidates, designSystem, {
onInvalidCandidate: (candidate) => {
throw new Error(`Cannot apply unknown utility class: ${candidate}`)
@@ -168,12 +177,36 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
})
let src = node.src
- let candidateAst = compiled.astNodes
- walk(candidateAst, (node) => {
- node.src = src
+ let details = compiled.astNodes.map((node) => {
+ let candidate = compiled.nodeSorting.get(node)?.candidate
+ let candidateOffset = candidate ? candidateOffsets[candidate] : undefined
+
+ node = structuredClone(node)
+
+ if (!src || !candidate || candidateOffset === undefined) {
+ return { node, src }
+ }
+
+ let candidateSrc: SourceLocation = [src[0], src[1], src[2]]
+
+ candidateSrc[1] += 7 + candidateOffset
+ candidateSrc[2] = candidateSrc[1] + candidate.length
+
+ return { node: structuredClone(node), src: candidateSrc }
})
+ for (let { node, src } of details) {
+ // While the original nodes may have come from an `@utility` we still
+ // want to replace the source because the `@apply` is ultimately the
+ // reason the node was emitted into the AST.
+ walk([node], (node) => {
+ node.src = src
+ })
+ }
+
+ let candidateAst = details.map((d) => d.node)
+
// Collect the nodes to insert in place of the `@apply` rule. When a rule
// was used, we want to insert its children instead of the rule because we
// don't want the wrapping selector.
From a0a9a33952cd8716d08e8561481818fb6f3366b3 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Sun, 12 Jan 2025 22:41:25 -0500
Subject: [PATCH 18/41] Use offset information to generate source maps
---
packages/tailwindcss/package.json | 5 +-
packages/tailwindcss/src/index.ts | 10 +
.../src/source-maps/line-table.test.ts | 87 ++++
.../tailwindcss/src/source-maps/line-table.ts | 100 +++++
.../src/source-maps/source-map.test.ts | 421 ++++++++++++++++++
.../tailwindcss/src/source-maps/source-map.ts | 194 ++++++++
.../src/source-maps/translation-map.test.ts | 279 ++++++++++++
7 files changed, 1095 insertions(+), 1 deletion(-)
create mode 100644 packages/tailwindcss/src/source-maps/line-table.test.ts
create mode 100644 packages/tailwindcss/src/source-maps/line-table.ts
create mode 100644 packages/tailwindcss/src/source-maps/source-map.test.ts
create mode 100644 packages/tailwindcss/src/source-maps/source-map.ts
create mode 100644 packages/tailwindcss/src/source-maps/translation-map.test.ts
diff --git a/packages/tailwindcss/package.json b/packages/tailwindcss/package.json
index 92110f1ca2c3..91c05a22b5dc 100644
--- a/packages/tailwindcss/package.json
+++ b/packages/tailwindcss/package.json
@@ -127,9 +127,12 @@
"utilities.css"
],
"devDependencies": {
+ "@ampproject/remapping": "^2.3.0",
"@tailwindcss/oxide": "workspace:^",
"@types/node": "catalog:",
+ "dedent": "1.5.3",
"lightningcss": "catalog:",
- "dedent": "1.5.3"
+ "magic-string": "^0.30.17",
+ "source-map-js": "^1.2.1"
}
}
diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts
index b109713ee265..6e493c3689d0 100644
--- a/packages/tailwindcss/src/index.ts
+++ b/packages/tailwindcss/src/index.ts
@@ -26,6 +26,7 @@ import { applyVariant, compileCandidates } from './compile'
import { substituteFunctions } from './css-functions'
import * as CSS from './css-parser'
import { buildDesignSystem, type DesignSystem } from './design-system'
+import { createSourceMap, type DecodedSourceMap } from './source-maps/source-map'
import { Theme, ThemeOptions } from './theme'
import { createCssUtility } from './utilities'
import { expand } from './utils/brace-expansion'
@@ -791,6 +792,8 @@ export async function compileAst(
}
}
+export type { DecodedSourceMap }
+
export async function compile(
css: string,
opts: CompileOptions = {},
@@ -799,6 +802,7 @@ export async function compile(
root: Root
features: Features
build(candidates: string[]): string
+ buildSourceMap(): DecodedSourceMap
}> {
let ast = CSS.parse(css, { from: opts.from })
let api = await compileAst(ast, opts)
@@ -819,6 +823,12 @@ export async function compile(
return compiledCss
},
+
+ buildSourceMap() {
+ return createSourceMap({
+ ast: compiledAst,
+ })
+ },
}
}
diff --git a/packages/tailwindcss/src/source-maps/line-table.test.ts b/packages/tailwindcss/src/source-maps/line-table.test.ts
new file mode 100644
index 000000000000..23c46243b726
--- /dev/null
+++ b/packages/tailwindcss/src/source-maps/line-table.test.ts
@@ -0,0 +1,87 @@
+import dedent from 'dedent'
+import { expect, test } from 'vitest'
+import { createLineTable } from './line-table'
+
+const css = dedent
+
+test('line tables', () => {
+ let text = css`
+ .foo {
+ color: red;
+ }
+ `
+
+ let table = createLineTable(`${text}\n`)
+
+ // Line 1: `.foo {\n`
+ expect(table.find(0)).toEqual({ line: 1, column: 0 })
+ expect(table.find(1)).toEqual({ line: 1, column: 1 })
+ expect(table.find(2)).toEqual({ line: 1, column: 2 })
+ expect(table.find(3)).toEqual({ line: 1, column: 3 })
+ expect(table.find(4)).toEqual({ line: 1, column: 4 })
+ expect(table.find(5)).toEqual({ line: 1, column: 5 })
+ expect(table.find(6)).toEqual({ line: 1, column: 6 })
+
+ // Line 2: ` color: red;\n`
+ expect(table.find(6 + 1)).toEqual({ line: 2, column: 0 })
+ expect(table.find(6 + 2)).toEqual({ line: 2, column: 1 })
+ expect(table.find(6 + 3)).toEqual({ line: 2, column: 2 })
+ expect(table.find(6 + 4)).toEqual({ line: 2, column: 3 })
+ expect(table.find(6 + 5)).toEqual({ line: 2, column: 4 })
+ expect(table.find(6 + 6)).toEqual({ line: 2, column: 5 })
+ expect(table.find(6 + 7)).toEqual({ line: 2, column: 6 })
+ expect(table.find(6 + 8)).toEqual({ line: 2, column: 7 })
+ expect(table.find(6 + 9)).toEqual({ line: 2, column: 8 })
+ expect(table.find(6 + 10)).toEqual({ line: 2, column: 9 })
+ expect(table.find(6 + 11)).toEqual({ line: 2, column: 10 })
+ expect(table.find(6 + 12)).toEqual({ line: 2, column: 11 })
+ expect(table.find(6 + 13)).toEqual({ line: 2, column: 12 })
+
+ // Line 3: `}\n`
+ expect(table.find(20 + 1)).toEqual({ line: 3, column: 0 })
+ expect(table.find(20 + 2)).toEqual({ line: 3, column: 1 })
+
+ // After the new line
+ expect(table.find(22 + 1)).toEqual({ line: 4, column: 0 })
+})
+
+test('line tables findOffset', () => {
+ let text = css`
+ .foo {
+ color: red;
+ }
+ `
+
+ let table = createLineTable(`${text}\n`)
+
+ // Line 1: `.foo {\n`
+ expect(table.findOffset({ line: 1, column: 0 })).toEqual(0)
+ expect(table.findOffset({ line: 1, column: 1 })).toEqual(1)
+ expect(table.findOffset({ line: 1, column: 2 })).toEqual(2)
+ expect(table.findOffset({ line: 1, column: 3 })).toEqual(3)
+ expect(table.findOffset({ line: 1, column: 4 })).toEqual(4)
+ expect(table.findOffset({ line: 1, column: 5 })).toEqual(5)
+ expect(table.findOffset({ line: 1, column: 6 })).toEqual(6)
+
+ // Line 2: ` color: red;\n`
+ expect(table.findOffset({ line: 2, column: 0 })).toEqual(6 + 1)
+ expect(table.findOffset({ line: 2, column: 1 })).toEqual(6 + 2)
+ expect(table.findOffset({ line: 2, column: 2 })).toEqual(6 + 3)
+ expect(table.findOffset({ line: 2, column: 3 })).toEqual(6 + 4)
+ expect(table.findOffset({ line: 2, column: 4 })).toEqual(6 + 5)
+ expect(table.findOffset({ line: 2, column: 5 })).toEqual(6 + 6)
+ expect(table.findOffset({ line: 2, column: 6 })).toEqual(6 + 7)
+ expect(table.findOffset({ line: 2, column: 7 })).toEqual(6 + 8)
+ expect(table.findOffset({ line: 2, column: 8 })).toEqual(6 + 9)
+ expect(table.findOffset({ line: 2, column: 9 })).toEqual(6 + 10)
+ expect(table.findOffset({ line: 2, column: 10 })).toEqual(6 + 11)
+ expect(table.findOffset({ line: 2, column: 11 })).toEqual(6 + 12)
+ expect(table.findOffset({ line: 2, column: 12 })).toEqual(6 + 13)
+
+ // Line 3: `}\n`
+ expect(table.findOffset({ line: 3, column: 0 })).toEqual(20 + 1)
+ expect(table.findOffset({ line: 3, column: 1 })).toEqual(20 + 2)
+
+ // After the new line
+ expect(table.findOffset({ line: 4, column: 0 })).toEqual(22 + 1)
+})
diff --git a/packages/tailwindcss/src/source-maps/line-table.ts b/packages/tailwindcss/src/source-maps/line-table.ts
new file mode 100644
index 000000000000..d42be8055206
--- /dev/null
+++ b/packages/tailwindcss/src/source-maps/line-table.ts
@@ -0,0 +1,100 @@
+/**
+ * Line offset tables are the key to generating our source maps. They allow us
+ * to store indexes with our AST nodes and later convert them into positions as
+ * when given the source that the indexes refer to.
+ */
+
+const LINE_BREAK = 0x0a
+
+/**
+ * A position in source code
+ *
+ * https://tc39.es/ecma426/#sec-position-record-type
+ */
+export interface Position {
+ /** The line number, one-based */
+ line: number
+
+ /** The column/character number, one-based */
+ column: number
+}
+
+/**
+ * A table that lets you turn an offset into a line number and column
+ */
+export interface LineTable {
+ /**
+ * Find the line/column position in the source code for a given offset
+ *
+ * Searching for a given offset takes O(log N) time where N is the number of
+ * lines of code.
+ *
+ * @param offset The index for which to find the position
+ */
+ find(offset: number): Position
+
+ /**
+ * Find the most likely byte offset for given a position
+ *
+ * @param offset The position for which to find the byte offset
+ */
+ findOffset(pos: Position): number
+}
+
+/**
+ * Compute a lookup table to allow for efficient line/column lookups based on
+ * offsets in the source code.
+ *
+ * Creating this table is an O(N) operation where N is the length of the source
+ */
+export function createLineTable(source: string): LineTable {
+ let table: number[] = [0]
+
+ // Compute the offsets for the start of each line
+ for (let i = 0; i < source.length; i++) {
+ if (source.charCodeAt(i) === LINE_BREAK) {
+ table.push(i + 1)
+ }
+ }
+
+ function find(offset: number) {
+ // Based on esbuild's binary search for line numbers
+ let line = 0
+ let count = table.length
+ while (count > 0) {
+ // `| 0` causes integer division
+ let mid = (count / 2) | 0
+ let i = line + mid
+ if (table[i] <= offset) {
+ line = i + 1
+ count = count - mid - 1
+ } else {
+ count = mid
+ }
+ }
+
+ line -= 1
+
+ let column = offset - table[line]
+
+ return {
+ line: line + 1,
+ column: column,
+ }
+ }
+
+ function findOffset({ line, column }: Position) {
+ line -= 1
+ line = Math.min(Math.max(line, 0), table.length - 1)
+
+ let offsetA = table[line]
+ let offsetB = table[line + 1] ?? offsetA
+
+ return Math.min(Math.max(offsetA + column, 0), offsetB)
+ }
+
+ return {
+ find,
+ findOffset,
+ }
+}
diff --git a/packages/tailwindcss/src/source-maps/source-map.test.ts b/packages/tailwindcss/src/source-maps/source-map.test.ts
new file mode 100644
index 000000000000..7a6e0471f8b5
--- /dev/null
+++ b/packages/tailwindcss/src/source-maps/source-map.test.ts
@@ -0,0 +1,421 @@
+import remapping from '@ampproject/remapping'
+import dedent from 'dedent'
+import MagicString from 'magic-string'
+import * as fs from 'node:fs/promises'
+import * as path from 'node:path'
+import { SourceMapConsumer, SourceMapGenerator, type RawSourceMap } from 'source-map-js'
+import { test } from 'vitest'
+import { compile } from '..'
+import createPlugin from '../plugin'
+import { DefaultMap } from '../utils/default-map'
+import type { DecodedSource, DecodedSourceMap } from './source-map'
+const css = dedent
+
+interface RunOptions {
+ input: string
+ candidates?: string[]
+ options?: Parameters[1]
+}
+
+async function run({ input, candidates, options }: RunOptions) {
+ let source = new MagicString(input)
+ let root = path.resolve(__dirname, '../..')
+
+ let compiler = await compile(source.toString(), {
+ from: 'input.css',
+ async loadStylesheet(id, base) {
+ let resolvedPath = path.resolve(root, id === 'tailwindcss' ? 'index.css' : id)
+
+ return {
+ path: path.relative(root, resolvedPath),
+ base,
+ content: await fs.readFile(resolvedPath, 'utf-8'),
+ }
+ },
+ ...options,
+ })
+
+ let css = compiler.build(candidates ?? [])
+ let decoded = compiler.buildSourceMap()
+ let rawMap = toRawSourceMap(decoded)
+ let combined = remapping(rawMap, () => null)
+ let map = JSON.parse(rawMap.toString()) as RawSourceMap
+
+ let sources = combined.sources
+ let annotations = formattedMappings(map)
+
+ return { css, map, sources, annotations }
+}
+
+function toRawSourceMap(map: DecodedSourceMap): string {
+ let generator = new SourceMapGenerator()
+
+ let id = 1
+ let sourceTable = new DefaultMap<
+ DecodedSource | null,
+ {
+ url: string
+ content: string
+ }
+ >((src) => {
+ return {
+ url: src?.url ?? ``,
+ content: src?.content ?? '',
+ }
+ })
+
+ for (let mapping of map.mappings) {
+ let original = sourceTable.get(mapping.originalPosition?.source ?? null)
+
+ generator.addMapping({
+ generated: mapping.generatedPosition,
+ original: mapping.originalPosition,
+ source: original.url,
+ name: mapping.name ?? undefined,
+ })
+
+ generator.setSourceContent(original.url, original.content)
+ }
+
+ return generator.toString()
+}
+
+/**
+ * An string annotation that represents a source map
+ *
+ * It's not meant to be exhaustive just enough to
+ * verify that the source map is working and that
+ * lines are mapped back to the original source
+ *
+ * Including when using @apply with multiple classes
+ */
+function formattedMappings(map: RawSourceMap) {
+ const smc = new SourceMapConsumer(map)
+ const annotations: Record<
+ number,
+ {
+ original: { start: [number, number]; end: [number, number] }
+ generated: { start: [number, number]; end: [number, number] }
+ source: string
+ }
+ > = {}
+
+ smc.eachMapping((mapping) => {
+ let annotation = (annotations[mapping.generatedLine] = annotations[mapping.generatedLine] || {
+ ...mapping,
+
+ original: {
+ start: [mapping.originalLine, mapping.originalColumn],
+ end: [mapping.originalLine, mapping.originalColumn],
+ },
+
+ generated: {
+ start: [mapping.generatedLine, mapping.generatedColumn],
+ end: [mapping.generatedLine, mapping.generatedColumn],
+ },
+
+ source: mapping.source,
+ })
+
+ annotation.generated.end[0] = mapping.generatedLine
+ annotation.generated.end[1] = mapping.generatedColumn
+
+ annotation.original.end[0] = mapping.originalLine!
+ annotation.original.end[1] = mapping.originalColumn!
+ })
+
+ return Object.values(annotations).map((annotation) => {
+ return `${annotation.source}: ${formatRange(annotation.generated)} <- ${formatRange(annotation.original)}`
+ })
+}
+
+function formatRange(range: { start: [number, number]; end: [number, number] }) {
+ if (range.start[0] === range.end[0]) {
+ // This range is on the same line
+ // and the columns are the same
+ if (range.start[1] === range.end[1]) {
+ return `${range.start[0]}:${range.start[1]}`
+ }
+
+ // This range is on the same line
+ // but the columns are different
+ return `${range.start[0]}:${range.start[1]}-${range.end[1]}`
+ }
+
+ // This range spans multiple lines
+ return `${range.start[0]}:${range.start[1]}-${range.end[0]}:${range.end[1]}`
+}
+
+test('source maps trace back to @import location', async ({ expect }) => {
+ let { sources, annotations } = await run({
+ input: css`
+ @import 'tailwindcss';
+
+ .foo {
+ @apply underline;
+ }
+ `,
+ })
+
+ // All CSS should be mapped back to the original source file
+ expect(sources).toEqual([
+ //
+ 'index.css',
+ 'theme.css',
+ 'preflight.css',
+ 'input.css',
+ ])
+ expect(sources.length).toBe(4)
+
+ // The output CSS should include annotations linking back to:
+ // 1. The class definition `.foo`
+ // 2. The `@apply underline` line inside of it
+ expect(annotations).toEqual([
+ 'index.css: 1:0-41 <- 1:0-41',
+ 'index.css: 2:0-13 <- 3:0-34',
+ 'theme.css: 3:2-15 <- 1:0-15',
+ 'theme.css: 4:4 <- 2:2-4:0',
+ 'theme.css: 5:22 <- 4:22',
+ 'theme.css: 6:4 <- 6:2-8:0',
+ 'theme.css: 7:13 <- 8:13',
+ 'theme.css: 8:4-43 <- 446:2-54',
+ 'theme.css: 9:4-48 <- 449:2-59',
+ 'index.css: 12:0-12 <- 4:0-37',
+ 'preflight.css: 13:2-59 <- 7:0-11:23',
+ 'preflight.css: 14:4-26 <- 12:2-24',
+ 'preflight.css: 15:4-13 <- 13:2-11',
+ 'preflight.css: 16:4-14 <- 14:2-12',
+ 'preflight.css: 17:4-19 <- 15:2-17',
+ 'preflight.css: 19:2-14 <- 28:0-29:6',
+ 'preflight.css: 20:4-20 <- 30:2-18',
+ 'preflight.css: 21:4-34 <- 31:2-32',
+ 'preflight.css: 22:4-15 <- 32:2-13',
+ 'preflight.css: 23:4-159 <- 33:2-42:3',
+ 'preflight.css: 24:4-71 <- 43:2-73',
+ 'preflight.css: 25:4-75 <- 44:2-77',
+ 'preflight.css: 26:4-44 <- 45:2-42',
+ 'preflight.css: 28:2-5 <- 54:0-3',
+ 'preflight.css: 29:4-13 <- 55:2-11',
+ 'preflight.css: 30:4-18 <- 56:2-16',
+ 'preflight.css: 31:4-25 <- 57:2-23',
+ 'preflight.css: 33:2-22 <- 64:0-20',
+ 'preflight.css: 34:4-45 <- 65:2-43',
+ 'preflight.css: 35:4-37 <- 66:2-35',
+ 'preflight.css: 37:2-25 <- 73:0-78:3',
+ 'preflight.css: 38:4-22 <- 79:2-20',
+ 'preflight.css: 39:4-24 <- 80:2-22',
+ 'preflight.css: 41:2-4 <- 87:0-2',
+ 'preflight.css: 42:4-18 <- 88:2-16',
+ 'preflight.css: 43:4-36 <- 89:2-34',
+ 'preflight.css: 44:4-28 <- 90:2-26',
+ 'preflight.css: 46:2-12 <- 97:0-98:7',
+ 'preflight.css: 47:4-23 <- 99:2-21',
+ 'preflight.css: 49:2-23 <- 109:0-112:4',
+ 'preflight.css: 50:4-148 <- 113:2-123:3',
+ 'preflight.css: 51:4-76 <- 124:2-78',
+ 'preflight.css: 52:4-80 <- 125:2-82',
+ 'preflight.css: 53:4-18 <- 126:2-16',
+ 'preflight.css: 55:2-8 <- 133:0-6',
+ 'preflight.css: 56:4-18 <- 134:2-16',
+ 'preflight.css: 58:2-11 <- 141:0-142:4',
+ 'preflight.css: 59:4-18 <- 143:2-16',
+ 'preflight.css: 60:4-18 <- 144:2-16',
+ 'preflight.css: 61:4-22 <- 145:2-20',
+ 'preflight.css: 62:4-28 <- 146:2-26',
+ 'preflight.css: 64:2-6 <- 149:0-4',
+ 'preflight.css: 65:4-19 <- 150:2-17',
+ 'preflight.css: 67:2-6 <- 153:0-4',
+ 'preflight.css: 68:4-15 <- 154:2-13',
+ 'preflight.css: 70:2-8 <- 163:0-6',
+ 'preflight.css: 71:4-18 <- 164:2-16',
+ 'preflight.css: 72:4-25 <- 165:2-23',
+ 'preflight.css: 73:4-29 <- 166:2-27',
+ 'preflight.css: 75:2-18 <- 173:0-16',
+ 'preflight.css: 76:4-17 <- 174:2-15',
+ 'preflight.css: 78:2-11 <- 181:0-9',
+ 'preflight.css: 79:4-28 <- 182:2-26',
+ 'preflight.css: 81:2-10 <- 189:0-8',
+ 'preflight.css: 82:4-22 <- 190:2-20',
+ 'preflight.css: 84:2-15 <- 197:0-199:5',
+ 'preflight.css: 85:4-20 <- 200:2-18',
+ 'preflight.css: 87:2-56 <- 209:0-216:7',
+ 'preflight.css: 88:4-18 <- 217:2-16',
+ 'preflight.css: 89:4-26 <- 218:2-24',
+ 'preflight.css: 91:2-13 <- 225:0-226:6',
+ 'preflight.css: 92:4-19 <- 227:2-17',
+ 'preflight.css: 93:4-16 <- 228:2-14',
+ 'preflight.css: 95:2-68 <- 238:0-243:23',
+ 'preflight.css: 96:4-17 <- 244:2-15',
+ 'preflight.css: 97:4-34 <- 245:2-32',
+ 'preflight.css: 98:4-36 <- 246:2-34',
+ 'preflight.css: 99:4-27 <- 247:2-25',
+ 'preflight.css: 100:4-18 <- 248:2-16',
+ 'preflight.css: 101:4-20 <- 249:2-18',
+ 'preflight.css: 102:4-33 <- 250:2-31',
+ 'preflight.css: 103:4-14 <- 251:2-12',
+ 'preflight.css: 105:2-49 <- 258:0-47',
+ 'preflight.css: 106:4-23 <- 259:2-21',
+ 'preflight.css: 108:2-56 <- 266:0-54',
+ 'preflight.css: 109:4-30 <- 267:2-28',
+ 'preflight.css: 111:2-25 <- 274:0-23',
+ 'preflight.css: 112:4-26 <- 275:2-24',
+ 'preflight.css: 114:2-16 <- 282:0-14',
+ 'preflight.css: 115:4-14 <- 283:2-12',
+ 'preflight.css: 117:2-92 <- 291:0-292:49',
+ 'preflight.css: 118:4-18 <- 293:2-16',
+ 'preflight.css: 119:6-25 <- 294:4-61',
+ 'preflight.css: 120:6-53 <- 294:4-61',
+ 'preflight.css: 121:8-65 <- 294:4-61',
+ 'preflight.css: 125:2-11 <- 302:0-9',
+ 'preflight.css: 126:4-20 <- 303:2-18',
+ 'preflight.css: 128:2-30 <- 310:0-28',
+ 'preflight.css: 129:4-28 <- 311:2-26',
+ 'preflight.css: 131:2-32 <- 319:0-30',
+ 'preflight.css: 132:4-19 <- 320:2-17',
+ 'preflight.css: 133:4-23 <- 321:2-21',
+ 'preflight.css: 135:2-26 <- 328:0-24',
+ 'preflight.css: 136:4-24 <- 329:2-22',
+ 'preflight.css: 138:2-41 <- 336:0-39',
+ 'preflight.css: 139:4-14 <- 337:2-12',
+ 'preflight.css: 141:2-329 <- 340:0-348:39',
+ 'preflight.css: 142:4-20 <- 349:2-18',
+ 'preflight.css: 144:2-19 <- 356:0-17',
+ 'preflight.css: 145:4-20 <- 357:2-18',
+ 'preflight.css: 147:2-96 <- 364:0-366:23',
+ 'preflight.css: 148:4-22 <- 367:2-20',
+ 'preflight.css: 150:2-59 <- 374:0-375:28',
+ 'preflight.css: 151:4-16 <- 376:2-14',
+ 'preflight.css: 153:2-47 <- 383:0-45',
+ 'preflight.css: 154:4-28 <- 384:2-26',
+ 'index.css: 157:0-16 <- 5:0-42',
+ 'input.css: 158:0-5 <- 3:0-5',
+ 'input.css: 159:2-33 <- 4:9-18',
+ ])
+})
+
+test('source maps are generated for utilities', async ({ expect }) => {
+ let {
+ sources,
+ css: output,
+ annotations,
+ } = await run({
+ input: css`
+ @import './utilities.css';
+ @plugin "./plugin.js";
+ @utility custom {
+ color: orange;
+ }
+ `,
+ candidates: ['custom', 'custom-js', 'flex'],
+ options: {
+ loadModule: async (_, base) => ({
+ path: '',
+ base,
+ module: createPlugin(({ addUtilities }) => {
+ addUtilities({ '.custom-js': { color: 'blue' } })
+ }),
+ }),
+ },
+ })
+
+ // All CSS should be mapped back to the original source file
+ expect(sources).toEqual(['utilities.css', 'input.css'])
+ expect(sources.length).toBe(2)
+
+ // The output CSS should include annotations linking back to:
+ expect(annotations).toEqual([
+ // @tailwind utilities
+ 'utilities.css: 1:0-6 <- 1:0-19',
+ 'utilities.css: 2:2-15 <- 1:0-19',
+ 'utilities.css: 4:0-8 <- 1:0-19',
+ // color: orange
+ 'input.css: 5:2-15 <- 4:2-15',
+ // @tailwind utilities
+ 'utilities.css: 7:0-11 <- 1:0-19',
+ 'utilities.css: 8:2-13 <- 1:0-19',
+ ])
+
+ expect(output).toMatchInlineSnapshot(`
+ ".flex {
+ display: flex;
+ }
+ .custom {
+ color: orange;
+ }
+ .custom-js {
+ color: blue;
+ }
+ "
+ `)
+})
+
+test('utilities have source maps pointing to the utilities node', async ({ expect }) => {
+ let { sources, annotations } = await run({
+ input: `@tailwind utilities;`,
+ candidates: [
+ //
+ 'underline',
+ ],
+ })
+
+ expect(sources).toEqual(['input.css'])
+
+ expect(annotations).toEqual([
+ //
+ 'input.css: 1:0-11 <- 1:0-19',
+ 'input.css: 2:2-33 <- 1:0-19',
+ ])
+})
+
+test('@apply generates source maps', async ({ expect }) => {
+ let { sources, annotations } = await run({
+ input: css`
+ .foo {
+ color: blue;
+ @apply text-[#000] hover:text-[#f00];
+ @apply underline;
+ color: red;
+ }
+ `,
+ })
+
+ expect(sources).toEqual(['input.css'])
+
+ expect(annotations).toEqual([
+ 'input.css: 1:0-5 <- 1:0-5',
+ 'input.css: 2:2-13 <- 2:2-13',
+ 'input.css: 3:2-13 <- 3:9-20',
+ 'input.css: 4:2-10 <- 3:21-38',
+ 'input.css: 5:4-26 <- 3:21-38',
+ 'input.css: 6:6-17 <- 3:21-38',
+ 'input.css: 9:2-33 <- 4:9-18',
+ 'input.css: 10:2-12 <- 5:2-12',
+ ])
+})
+
+test('license comments preserve source locations', async ({ expect }) => {
+ let { sources, annotations } = await run({
+ input: `/*! some comment */`,
+ })
+
+ expect(sources).toEqual(['input.css'])
+
+ expect(annotations).toEqual([
+ //
+ 'input.css: 1:0-19 <- 1:0-19',
+ ])
+})
+
+test('license comments with new lines preserve source locations', async ({ expect }) => {
+ let { sources, annotations, css } = await run({
+ input: `/*! some \n comment */`,
+ })
+
+ expect(sources).toEqual(['input.css'])
+
+ expect(annotations).toEqual([
+ //
+ 'input.css: 1:0 <- 1:0-2:0',
+ 'input.css: 2:11 <- 2:11',
+ ])
+})
diff --git a/packages/tailwindcss/src/source-maps/source-map.ts b/packages/tailwindcss/src/source-maps/source-map.ts
new file mode 100644
index 000000000000..1e048cdd00e3
--- /dev/null
+++ b/packages/tailwindcss/src/source-maps/source-map.ts
@@ -0,0 +1,194 @@
+import { walk, type AstNode } from '../ast'
+import { DefaultMap } from '../utils/default-map'
+import { createLineTable, type LineTable, type Position } from './line-table'
+import type { Source } from './source'
+
+// https://tc39.es/ecma426/#sec-original-position-record-type
+export interface OriginalPosition extends Position {
+ source: DecodedSource
+}
+
+/**
+ * A "decoded" sourcemap
+ *
+ * @see https://tc39.es/ecma426/#decoded-source-map-record
+ */
+export interface DecodedSourceMap {
+ file: string | null
+ sources: DecodedSource[]
+ mappings: DecodedMapping[]
+}
+
+/**
+ * A "decoded" source
+ *
+ * @see https://tc39.es/ecma426/#decoded-source-record
+ */
+export interface DecodedSource {
+ url: string | null
+ content: string | null
+ ignore: boolean
+}
+
+/**
+ * A "decoded" mapping
+ *
+ * @see https://tc39.es/ecma426/#decoded-mapping-record
+ */
+export interface DecodedMapping {
+ // https://tc39.es/ecma426/#sec-original-position-record-type
+ originalPosition: OriginalPosition | null
+
+ // https://tc39.es/ecma426/#sec-position-record-type
+ generatedPosition: Position
+
+ name: string | null
+}
+
+/**
+ * Build a source map from the given AST.
+ *
+ * Our AST is build from flat CSS strings but there are many because we handle
+ * `@import`. This means that different nodes can have a different source.
+ *
+ * Instead of taking an input source map, we take the input CSS string we were
+ * originally given, as well as the source text for any imported files, and
+ * use that to generate a source map.
+ *
+ * We then require the use of other tools that can translate one or more
+ * "input" source maps into a final output source map. For example,
+ * `@ampproject/remapping` can be used to handle this.
+ *
+ * This also ensures that tools that expect "local" source maps are able to
+ * consume the source map we generate.
+ *
+ * The source map type we generate may be a bit different from "raw" source maps
+ * that the `source-map-js` package uses. It's a "decoded" source map that is
+ * represented by an object graph. It's identical to "decoded" source map from
+ * the ECMA-426 spec for source maps.
+ *
+ * Note that the spec itself is still evolving which means our implementation
+ * may need to evolve to match it.
+ *
+ * This can easily be converted to a "raw" source map by any tool that needs to.
+ **/
+export function createSourceMap({ ast }: { ast: AstNode[] }) {
+ // Compute line tables for both the original and generated source lazily so we
+ // don't have to do it during parsing or printing.
+ let lineTables = new DefaultMap((src) => createLineTable(src.code))
+ let sourceTable = new DefaultMap((src) => ({
+ url: src.file,
+ content: src.code,
+ ignore: false,
+ }))
+
+ // Convert each mapping to a set of positions
+ let map: DecodedSourceMap = {
+ file: null,
+ sources: [],
+ mappings: [],
+ }
+
+ // Get all the indexes from the mappings
+ function add(node: AstNode) {
+ if (!node.src || !node.dst) return
+
+ let originalSource = sourceTable.get(node.src[0])
+ if (!originalSource.content) return
+
+ let originalTable = lineTables.get(node.src[0])
+ let generatedTable = lineTables.get(node.dst[0])
+
+ let originalSlice = originalSource.content.slice(node.src[1], node.src[2])
+
+ // Source maps only encode single locations — not multi-line ranges
+ // So to properly emulate this we'll scan the original text for multiple
+ // lines and create mappings for each of those lines that point to the
+ // destination node (whether it spans multiple lines or not)
+ //
+ // This is not 100% accurate if both the source and destination preserve
+ // their newlines but this only happens in the case of custom properties
+ //
+ // This is _good enough_
+ let offset = 0
+ for (let line of originalSlice.split('\n')) {
+ if (line.trim() !== '') {
+ let originalStart = originalTable.find(node.src[1] + offset)
+ let generatedStart = generatedTable.find(node.dst[1])
+
+ map.mappings.push({
+ name: null,
+ originalPosition: {
+ source: originalSource,
+ ...originalStart,
+ },
+ generatedPosition: generatedStart,
+ })
+ }
+
+ offset += line.length
+ offset += 1
+ }
+
+ let originalEnd = originalTable.find(node.src[2])
+ let generatedEnd = generatedTable.find(node.dst[2])
+
+ map.mappings.push({
+ name: null,
+ originalPosition: {
+ source: originalSource,
+ ...originalEnd,
+ },
+ generatedPosition: generatedEnd,
+ })
+ }
+
+ walk(ast, add)
+
+ // Populate
+ for (let source of lineTables.keys()) {
+ map.sources.push(sourceTable.get(source))
+ }
+
+ // Sort the mappings in ascending order
+ map.mappings.sort((a, b) => {
+ return (
+ a.generatedPosition.line - b.generatedPosition.line ||
+ a.generatedPosition.column - b.generatedPosition.column ||
+ (a.originalPosition?.line ?? 0) - (b.originalPosition?.line ?? 0) ||
+ (a.originalPosition?.column ?? 0) - (b.originalPosition?.column ?? 0)
+ )
+ })
+
+ return map
+}
+
+export function createTranslationMap({
+ original,
+ generated,
+}: {
+ original: string
+ generated: string
+}) {
+ // Compute line tables for both the original and generated source lazily so we
+ // don't have to do it during parsing or printing.
+ let originalTable = createLineTable(original)
+ let generatedTable = createLineTable(generated)
+
+ type Translation = [Position, Position, Position | null, Position | null]
+
+ return (node: AstNode) => {
+ if (!node.src) return []
+
+ let translations: Translation[] = []
+
+ translations.push([
+ originalTable.find(node.src[1]),
+ originalTable.find(node.src[2]),
+ node.dst ? generatedTable.find(node.dst[1]) : null,
+ node.dst ? generatedTable.find(node.dst[2]) : null,
+ ])
+
+ return translations
+ }
+}
diff --git a/packages/tailwindcss/src/source-maps/translation-map.test.ts b/packages/tailwindcss/src/source-maps/translation-map.test.ts
new file mode 100644
index 000000000000..ad0a9f435154
--- /dev/null
+++ b/packages/tailwindcss/src/source-maps/translation-map.test.ts
@@ -0,0 +1,279 @@
+import dedent from 'dedent'
+import { assert, expect, test } from 'vitest'
+import { toCss, type AstNode } from '../ast'
+import * as CSS from '../css-parser'
+import { createTranslationMap } from './source-map'
+
+async function analyze(input: string) {
+ let ast = CSS.parse(input, { from: 'input.css' })
+ let css = toCss(ast, true)
+ let translate = createTranslationMap({
+ original: input,
+ generated: css,
+ })
+
+ function format(node: AstNode) {
+ let lines: string[] = []
+
+ for (let [oStart, oEnd, gStart, gEnd] of translate(node)) {
+ let src = `${oStart.line}:${oStart.column}-${oEnd.line}:${oEnd.column}`
+
+ let dst = '(none)'
+
+ if (gStart && gEnd) {
+ dst = `${gStart.line}:${gStart.column}-${gEnd.line}:${gEnd.column}`
+ }
+
+ lines.push(`${dst} <- ${src}`)
+ }
+
+ return lines
+ }
+
+ return { ast, css, format }
+}
+
+test('comment, single line', async () => {
+ let { ast, css, format } = await analyze(`/*! foo */`)
+
+ assert(ast[0].kind === 'comment')
+ expect(format(ast[0])).toMatchInlineSnapshot(`
+ [
+ "1:0-1:10 <- 1:0-1:10",
+ ]
+ `)
+
+ expect(css).toMatchInlineSnapshot(`
+ "/*! foo */
+ "
+ `)
+})
+
+test('comment, multi line', async () => {
+ let { ast, css, format } = await analyze(`/*! foo \n bar */`)
+
+ assert(ast[0].kind === 'comment')
+ expect(format(ast[0])).toMatchInlineSnapshot(`
+ [
+ "1:0-2:7 <- 1:0-2:7",
+ ]
+ `)
+
+ expect(css).toMatchInlineSnapshot(`
+ "/*! foo
+ bar */
+ "
+ `)
+})
+
+test('declaration, normal property, single line', async () => {
+ let { ast, css, format } = await analyze(`.foo { color: red; }`)
+
+ assert(ast[0].kind === 'rule')
+ assert(ast[0].nodes[0].kind === 'declaration')
+ expect(format(ast[0].nodes[0])).toMatchInlineSnapshot(`
+ [
+ "2:2-2:12 <- 1:7-1:17",
+ ]
+ `)
+
+ expect(css).toMatchInlineSnapshot(`
+ ".foo {
+ color: red;
+ }
+ "
+ `)
+})
+
+test('declaration, normal property, multi line', async () => {
+ // Works, no changes needed
+ let { ast, css, format } = await analyze(dedent`
+ .foo {
+ grid-template-areas:
+ "a b c"
+ "d e f"
+ "g h i";
+ }
+ `)
+
+ assert(ast[0].kind === 'rule')
+ assert(ast[0].nodes[0].kind === 'declaration')
+ expect(format(ast[0].nodes[0])).toMatchInlineSnapshot(`
+ [
+ "2:2-2:46 <- 2:2-5:11",
+ ]
+ `)
+
+ expect(css).toMatchInlineSnapshot(`
+ ".foo {
+ grid-template-areas: "a b c" "d e f" "g h i";
+ }
+ "
+ `)
+})
+
+test('declaration, custom property, single line', async () => {
+ let { ast, css, format } = await analyze(`.foo { --foo: bar; }`)
+
+ assert(ast[0].kind === 'rule')
+ assert(ast[0].nodes[0].kind === 'declaration')
+ expect(format(ast[0].nodes[0])).toMatchInlineSnapshot(`
+ [
+ "2:2-2:12 <- 1:7-1:17",
+ ]
+ `)
+
+ expect(css).toMatchInlineSnapshot(`
+ ".foo {
+ --foo: bar;
+ }
+ "
+ `)
+})
+
+test('declaration, custom property, multi line', async () => {
+ let { ast, css, format } = await analyze(dedent`
+ .foo {
+ --foo: bar\nbaz;
+ }
+ `)
+
+ assert(ast[0].kind === 'rule')
+ assert(ast[0].nodes[0].kind === 'declaration')
+ expect(format(ast[0].nodes[0])).toMatchInlineSnapshot(`
+ [
+ "2:2-3:3 <- 2:2-3:3",
+ ]
+ `)
+
+ expect(css).toMatchInlineSnapshot(`
+ ".foo {
+ --foo: bar
+ baz;
+ }
+ "
+ `)
+})
+
+test('at rules, bodyless, single line', async () => {
+ // This intentionally has extra spaces
+ let { ast, css, format } = await analyze(`@layer foo, bar;`)
+
+ assert(ast[0].kind === 'at-rule')
+ expect(format(ast[0])).toMatchInlineSnapshot(`
+ [
+ "1:0-1:15 <- 1:0-1:19",
+ ]
+ `)
+
+ expect(css).toMatchInlineSnapshot(`
+ "@layer foo, bar;
+ "
+ `)
+})
+
+test('at rules, bodyless, multi line', async () => {
+ let { ast, css, format } = await analyze(dedent`
+ @layer
+ foo,
+ bar
+ ;
+ `)
+
+ assert(ast[0].kind === 'at-rule')
+ expect(format(ast[0])).toMatchInlineSnapshot(`
+ [
+ "1:0-1:15 <- 1:0-4:0",
+ ]
+ `)
+
+ expect(css).toMatchInlineSnapshot(`
+ "@layer foo, bar;
+ "
+ `)
+})
+
+test('at rules, body, single line', async () => {
+ let { ast, css, format } = await analyze(`@layer foo { color: red; }`)
+
+ assert(ast[0].kind === 'at-rule')
+ expect(format(ast[0])).toMatchInlineSnapshot(`
+ [
+ "1:0-1:11 <- 1:0-1:11",
+ ]
+ `)
+
+ expect(css).toMatchInlineSnapshot(`
+ "@layer foo {
+ color: red;
+ }
+ "
+ `)
+})
+
+test('at rules, body, multi line', async () => {
+ let { ast, css, format } = await analyze(dedent`
+ @layer
+ foo
+ {
+ color: baz;
+ }
+ `)
+
+ assert(ast[0].kind === 'at-rule')
+ expect(format(ast[0])).toMatchInlineSnapshot(`
+ [
+ "1:0-1:11 <- 1:0-3:0",
+ ]
+ `)
+
+ expect(css).toMatchInlineSnapshot(`
+ "@layer foo {
+ color: baz;
+ }
+ "
+ `)
+})
+
+test('style rules, body, single line', async () => {
+ let { ast, css, format } = await analyze(`.foo:is(.bar) { color: red; }`)
+
+ assert(ast[0].kind === 'rule')
+ expect(format(ast[0])).toMatchInlineSnapshot(`
+ [
+ "1:0-1:14 <- 1:0-1:14",
+ ]
+ `)
+
+ expect(css).toMatchInlineSnapshot(`
+ ".foo:is(.bar) {
+ color: red;
+ }
+ "
+ `)
+})
+
+test('style rules, body, multi line', async () => {
+ // Works, no changes needed
+ let { ast, css, format } = await analyze(dedent`
+ .foo:is(
+ .bar
+ ) {
+ color: red;
+ }
+ `)
+
+ assert(ast[0].kind === 'rule')
+ expect(format(ast[0])).toMatchInlineSnapshot(`
+ [
+ "1:0-1:16 <- 1:0-3:2",
+ ]
+ `)
+
+ expect(css).toMatchInlineSnapshot(`
+ ".foo:is( .bar ) {
+ color: red;
+ }
+ "
+ `)
+})
From b191165f44112a6df5b7c367ea9d9c4d1a4d6ebe Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Tue, 29 Apr 2025 19:18:59 -0400
Subject: [PATCH 19/41] Prep for source maps in build tools
---
.../src/commands/build/index.ts | 6 +-
packages/@tailwindcss-node/package.json | 1 +
packages/@tailwindcss-node/src/index.cts | 1 +
packages/@tailwindcss-node/src/index.ts | 1 +
packages/@tailwindcss-node/src/optimize.ts | 36 ++++++++---
packages/@tailwindcss-node/src/source-maps.ts | 59 +++++++++++++++++++
packages/@tailwindcss-postcss/src/index.ts | 4 +-
packages/@tailwindcss-vite/src/index.ts | 4 +-
packages/tailwindcss/src/test-utils/run.ts | 8 ++-
9 files changed, 102 insertions(+), 18 deletions(-)
create mode 100644 packages/@tailwindcss-node/src/source-maps.ts
diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts
index 409c398e9939..5e8ecdbae5d2 100644
--- a/packages/@tailwindcss-cli/src/commands/build/index.ts
+++ b/packages/@tailwindcss-cli/src/commands/build/index.ts
@@ -127,14 +127,14 @@ export async function handle(args: Result>) {
if (args['--minify'] || args['--optimize']) {
if (css !== previous.css) {
DEBUG && I.start('Optimize CSS')
- let optimizedCss = optimize(css, {
+ let optimized = optimize(css, {
file: args['--input'] ?? 'input.css',
minify: args['--minify'] ?? false,
})
DEBUG && I.end('Optimize CSS')
previous.css = css
- previous.optimizedCss = optimizedCss
- output = optimizedCss
+ previous.optimizedCss = optimized.code
+ output = optimized.code
} else {
output = previous.optimizedCss
}
diff --git a/packages/@tailwindcss-node/package.json b/packages/@tailwindcss-node/package.json
index 0844c232416f..5fc6727eea21 100644
--- a/packages/@tailwindcss-node/package.json
+++ b/packages/@tailwindcss-node/package.json
@@ -40,6 +40,7 @@
"enhanced-resolve": "^5.18.1",
"jiti": "^2.4.2",
"lightningcss": "catalog:",
+ "source-map-js": "^1.2.1",
"tailwindcss": "workspace:*"
}
}
diff --git a/packages/@tailwindcss-node/src/index.cts b/packages/@tailwindcss-node/src/index.cts
index b9284a0c8628..4bca0a5e11de 100644
--- a/packages/@tailwindcss-node/src/index.cts
+++ b/packages/@tailwindcss-node/src/index.cts
@@ -5,6 +5,7 @@ export * from './compile'
export * from './instrumentation'
export * from './normalize-path'
export * from './optimize'
+export * from './source-maps'
export { env }
// In Bun, ESM modules will also populate `require.cache`, so the module hook is
diff --git a/packages/@tailwindcss-node/src/index.ts b/packages/@tailwindcss-node/src/index.ts
index 83d603429388..a5e7bf5b4bc3 100644
--- a/packages/@tailwindcss-node/src/index.ts
+++ b/packages/@tailwindcss-node/src/index.ts
@@ -5,6 +5,7 @@ export * from './compile'
export * from './instrumentation'
export * from './normalize-path'
export * from './optimize'
+export * from './source-maps'
export { env }
// In Bun, ESM modules will also populate `require.cache`, so the module hook is
diff --git a/packages/@tailwindcss-node/src/optimize.ts b/packages/@tailwindcss-node/src/optimize.ts
index bf7ab674a4f8..168b3f832d5d 100644
--- a/packages/@tailwindcss-node/src/optimize.ts
+++ b/packages/@tailwindcss-node/src/optimize.ts
@@ -1,13 +1,29 @@
import { Features, transform } from 'lightningcss'
+export interface OptimizeOptions {
+ /**
+ * The file being transformed
+ */
+ file?: string
+
+ /**
+ * Enabled minified output
+ */
+ minify?: boolean
+}
+
+export interface TransformResult {
+ code: string
+}
+
export function optimize(
input: string,
- { file = 'input.css', minify = false }: { file?: string; minify?: boolean } = {},
-): string {
+ { file = 'input.css', minify = false }: OptimizeOptions = {},
+): TransformResult {
function optimize(code: Buffer | Uint8Array) {
return transform({
filename: file,
- code: code as any,
+ code,
minify,
sourceMap: false,
drafts: {
@@ -25,16 +41,18 @@ export function optimize(
chrome: 111 << 16,
},
errorRecovery: true,
- }).code
+ })
}
// Running Lightning CSS twice to ensure that adjacent rules are merged after
// nesting is applied. This creates a more optimized output.
- let out = optimize(optimize(Buffer.from(input))).toString()
+ let result = optimize(Buffer.from(input))
+ result = optimize(result.code)
- // Work around an issue where the media query range syntax transpilation
- // generates code that is invalid with `@media` queries level 3.
- out = out.replaceAll('@media not (', '@media not all and (')
+ let code = result.code.toString()
+ code = code.replaceAll('@media not (', '@media not all and (')
- return out
+ return {
+ code,
+ }
}
diff --git a/packages/@tailwindcss-node/src/source-maps.ts b/packages/@tailwindcss-node/src/source-maps.ts
new file mode 100644
index 000000000000..88ba54e2f258
--- /dev/null
+++ b/packages/@tailwindcss-node/src/source-maps.ts
@@ -0,0 +1,59 @@
+import { SourceMapGenerator } from 'source-map-js'
+import type { DecodedSource, DecodedSourceMap } from '../../tailwindcss/src/source-maps/source-map'
+import { DefaultMap } from '../../tailwindcss/src/utils/default-map'
+
+export type { DecodedSource, DecodedSourceMap }
+export interface SourceMap {
+ readonly raw: string
+ readonly inline: string
+}
+
+function serializeSourceMap(map: DecodedSourceMap): string {
+ let generator = new SourceMapGenerator()
+
+ let id = 1
+ let sourceTable = new DefaultMap<
+ DecodedSource | null,
+ {
+ url: string
+ content: string
+ }
+ >((src) => {
+ return {
+ url: src?.url ?? ``,
+ content: src?.content ?? '',
+ }
+ })
+
+ for (let mapping of map.mappings) {
+ let original = sourceTable.get(mapping.originalPosition?.source ?? null)
+
+ generator.addMapping({
+ generated: mapping.generatedPosition,
+ original: mapping.originalPosition,
+ source: original.url,
+ name: mapping.name,
+ })
+
+ generator.setSourceContent(original.url, original.content)
+ }
+
+ return generator.toString()
+}
+
+export function toSourceMap(map: DecodedSourceMap | string): SourceMap {
+ let raw = typeof map === 'string' ? map : serializeSourceMap(map)
+
+ return {
+ raw,
+ get inline() {
+ let tmp = ''
+
+ tmp += '/*# sourceMappingURL=data:application/json;base64,'
+ tmp += Buffer.from(raw, 'utf-8').toString('base64')
+ tmp += ' */\n'
+
+ return tmp
+ },
+ }
+}
diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts
index b06c15cb237b..79e51d2aa574 100644
--- a/packages/@tailwindcss-postcss/src/index.ts
+++ b/packages/@tailwindcss-postcss/src/index.ts
@@ -282,13 +282,13 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
DEBUG && I.end('AST -> CSS')
DEBUG && I.start('Lightning CSS')
- let ast = optimizeCss(css, {
+ let optimized = optimizeCss(css, {
minify: typeof optimize === 'object' ? optimize.minify : true,
})
DEBUG && I.end('Lightning CSS')
DEBUG && I.start('CSS -> PostCSS AST')
- context.optimizedPostCssAst = postcss.parse(ast, result.opts)
+ context.optimizedPostCssAst = postcss.parse(optimized.code, result.opts)
DEBUG && I.end('CSS -> PostCSS AST')
DEBUG && I.end('Optimization')
diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts
index a62929069b13..0a053291a162 100644
--- a/packages/@tailwindcss-vite/src/index.ts
+++ b/packages/@tailwindcss-vite/src/index.ts
@@ -106,7 +106,9 @@ export default function tailwindcss(): Plugin[] {
DEBUG && I.end('[@tailwindcss/vite] Generate CSS (build)')
DEBUG && I.start('[@tailwindcss/vite] Optimize CSS')
- result.code = optimize(result.code, { minify })
+ result = optimize(result.code, {
+ minify,
+ })
DEBUG && I.end('[@tailwindcss/vite] Optimize CSS')
return result
diff --git a/packages/tailwindcss/src/test-utils/run.ts b/packages/tailwindcss/src/test-utils/run.ts
index 3ecf1a10fd36..5b4b1ca993c8 100644
--- a/packages/tailwindcss/src/test-utils/run.ts
+++ b/packages/tailwindcss/src/test-utils/run.ts
@@ -7,12 +7,14 @@ export async function compileCss(
options: Parameters[1] = {},
) {
let { build } = await compile(css, options)
- return optimize(build(candidates)).trim()
+ return optimize(build(candidates)).code.trim()
}
export async function run(candidates: string[]) {
let { build } = await compile('@tailwind utilities;')
- return optimize(build(candidates)).trim()
+ return optimize(build(candidates)).code.trim()
}
-export const optimizeCss = optimize
+export function optimizeCss(input: string) {
+ return optimize(input).code
+}
From 2302668b48f109aa70a956b8beeb0556ec314703 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Tue, 29 Apr 2025 18:54:43 -0400
Subject: [PATCH 20/41] Add integration test helpers for source maps
---
integrations/package.json | 3 +-
integrations/utils.ts | 87 ++++++++++++++++++++++++++++++++++++++-
2 files changed, 88 insertions(+), 2 deletions(-)
diff --git a/integrations/package.json b/integrations/package.json
index 62f458c77480..a3c1025f916e 100644
--- a/integrations/package.json
+++ b/integrations/package.json
@@ -4,6 +4,7 @@
"private": true,
"devDependencies": {
"dedent": "1.5.3",
- "fast-glob": "^3.3.3"
+ "fast-glob": "^3.3.3",
+ "source-map-js": "^1.2.1"
}
}
diff --git a/integrations/utils.ts b/integrations/utils.ts
index 8affff3b4a83..f13e8581eafb 100644
--- a/integrations/utils.ts
+++ b/integrations/utils.ts
@@ -5,7 +5,9 @@ import fs from 'node:fs/promises'
import { platform, tmpdir } from 'node:os'
import path from 'node:path'
import { stripVTControlCharacters } from 'node:util'
+import { RawSourceMap, SourceMapConsumer } from 'source-map-js'
import { test as defaultTest, type ExpectStatic } from 'vitest'
+import { createLineTable } from '../packages/tailwindcss/src/source-maps/line-table'
import { escape } from '../packages/tailwindcss/src/utils/escape'
const REPO_ROOT = path.join(__dirname, '..')
@@ -42,6 +44,7 @@ interface TestContext {
expect: ExpectStatic
exec(command: string, options?: ChildProcessOptions, execOptions?: ExecOptions): Promise
spawn(command: string, options?: ChildProcessOptions): Promise
+ parseSourceMap(opts: string | SourceMapOptions): SourceMap
fs: {
write(filePath: string, content: string, encoding?: BufferEncoding): Promise
create(filePaths: string[]): Promise
@@ -104,6 +107,7 @@ export function test(
let context = {
root,
expect: options.expect,
+ parseSourceMap,
async exec(
command: string,
childProcessOptions: ChildProcessOptions = {},
@@ -591,7 +595,9 @@ export async function fetchStyles(base: string, path = '/'): Promise {
}
return stylesheets.reduce((acc, css) => {
- return acc + '\n' + css
+ if (acc.length > 0) acc += '\n'
+ acc += css
+ return acc
}, '')
}
@@ -603,3 +609,82 @@ async function gracefullyRemove(dir: string) {
})
}
}
+
+const SOURCE_MAP_COMMENT = /^\/\*# sourceMappingURL=data:application\/json;base64,(.*) \*\/$/
+
+export interface SourceMap {
+ at(
+ line: number,
+ column: number,
+ ): {
+ source: string | null
+ original: string
+ generated: string
+ }
+}
+
+interface SourceMapOptions {
+ /**
+ * A raw source map
+ *
+ * This may be a string or an object. Strings will be decoded.
+ */
+ map: string | object
+
+ /**
+ * The content of the generated file the source map is for
+ */
+ content: string
+
+ /**
+ * The encoding of the source map
+ *
+ * Can be used to decode a base64 map (e.g. an inline source map URI)
+ */
+ encoding?: BufferEncoding
+}
+
+function parseSourceMap(opts: string | SourceMapOptions): SourceMap {
+ if (typeof opts === 'string') {
+ let lines = opts.trimEnd().split('\n')
+ let comment = lines.at(-1) ?? ''
+ let map = String(comment).match(SOURCE_MAP_COMMENT)?.[1] ?? null
+ if (!map) throw new Error('No source map comment found')
+
+ return parseSourceMap({
+ map,
+ content: lines.slice(0, -1).join('\n'),
+ encoding: 'base64',
+ })
+ }
+
+ let rawMap: RawSourceMap
+ let content = opts.content
+
+ if (typeof opts.map === 'object') {
+ rawMap = opts.map as RawSourceMap
+ } else {
+ rawMap = JSON.parse(Buffer.from(opts.map, opts.encoding ?? 'utf-8').toString())
+ }
+
+ let map = new SourceMapConsumer(rawMap)
+ let generatedTable = createLineTable(content)
+
+ return {
+ at(line: number, column: number) {
+ let pos = map.originalPositionFor({ line, column })
+ let source = pos.source ? map.sourceContentFor(pos.source) : null
+ let originalTable = createLineTable(source ?? '')
+ let originalOffset = originalTable.findOffset(pos)
+ let generatedOffset = generatedTable.findOffset({ line, column })
+
+ return {
+ source: pos.source,
+ original: source
+ ? source.slice(originalOffset, originalOffset + 10).trim() + '...'
+ : '(none)',
+ generated: content.slice(generatedOffset, generatedOffset + 10).trim() + '...',
+ }
+ },
+ }
+}
From acf5f3182efc0a00ae7fff097c220c301a544315 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Tue, 29 Apr 2025 19:13:42 -0400
Subject: [PATCH 21/41] Update source maps when optimizing output CSS
---
packages/@tailwindcss-node/package.json | 2 +
packages/@tailwindcss-node/src/optimize.ts | 44 +++++++++++++++++++---
2 files changed, 40 insertions(+), 6 deletions(-)
diff --git a/packages/@tailwindcss-node/package.json b/packages/@tailwindcss-node/package.json
index 5fc6727eea21..a7c867013202 100644
--- a/packages/@tailwindcss-node/package.json
+++ b/packages/@tailwindcss-node/package.json
@@ -37,9 +37,11 @@
}
},
"dependencies": {
+ "@ampproject/remapping": "^2.3.0",
"enhanced-resolve": "^5.18.1",
"jiti": "^2.4.2",
"lightningcss": "catalog:",
+ "magic-string": "^0.30.17",
"source-map-js": "^1.2.1",
"tailwindcss": "workspace:*"
}
diff --git a/packages/@tailwindcss-node/src/optimize.ts b/packages/@tailwindcss-node/src/optimize.ts
index 168b3f832d5d..9ebffae98508 100644
--- a/packages/@tailwindcss-node/src/optimize.ts
+++ b/packages/@tailwindcss-node/src/optimize.ts
@@ -1,4 +1,6 @@
+import remapping from '@ampproject/remapping'
import { Features, transform } from 'lightningcss'
+import MagicString from 'magic-string'
export interface OptimizeOptions {
/**
@@ -10,22 +12,31 @@ export interface OptimizeOptions {
* Enabled minified output
*/
minify?: boolean
+
+ /**
+ * The output source map before optimization
+ *
+ * If omitted an resulting source map will not be available
+ */
+ map?: string
}
export interface TransformResult {
code: string
+ map: string | undefined
}
export function optimize(
input: string,
- { file = 'input.css', minify = false }: OptimizeOptions = {},
+ { file = 'input.css', minify = false, map }: OptimizeOptions = {},
): TransformResult {
- function optimize(code: Buffer | Uint8Array) {
+ function optimize(code: Buffer | Uint8Array, map: string | undefined) {
return transform({
filename: file,
code,
minify,
- sourceMap: false,
+ sourceMap: typeof map !== 'undefined',
+ inputSourceMap: map,
drafts: {
customMedia: true,
},
@@ -46,13 +57,34 @@ export function optimize(
// Running Lightning CSS twice to ensure that adjacent rules are merged after
// nesting is applied. This creates a more optimized output.
- let result = optimize(Buffer.from(input))
- result = optimize(result.code)
+ let result = optimize(Buffer.from(input), map)
+ map = result.map?.toString()
+
+ result = optimize(result.code, map)
+ map = result.map?.toString()
let code = result.code.toString()
- code = code.replaceAll('@media not (', '@media not all and (')
+
+ // Work around an issue where the media query range syntax transpilation
+ // generates code that is invalid with `@media` queries level 3.
+ let magic = new MagicString(code)
+ magic.replaceAll('@media not (', '@media not all and (')
+
+ // We have to use a source-map-preserving method of replacing the content
+ // which requires the use of Magic String + remapping(…) to make sure
+ // the resulting map is correct
+ if (map !== undefined && magic.hasChanged()) {
+ let magicMap = magic.generateMap({ source: 'original', hires: 'boundary' }).toString()
+
+ let remapped = remapping([magicMap, map], () => null)
+
+ map = remapped.toString()
+ }
+
+ code = magic.toString()
return {
code,
+ map,
}
}
From 2f25e3ec3aa65a0c94cfc3062e2fd5ff0a3c7972 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Tue, 29 Apr 2025 19:15:47 -0400
Subject: [PATCH 22/41] Add source map support to the CLI
---
integrations/cli/index.test.ts | 194 +++++++++++++++++-
.../src/commands/build/index.ts | 79 ++++++-
2 files changed, 268 insertions(+), 5 deletions(-)
diff --git a/integrations/cli/index.test.ts b/integrations/cli/index.test.ts
index d30f6e24bb37..22d3a9c6aea0 100644
--- a/integrations/cli/index.test.ts
+++ b/integrations/cli/index.test.ts
@@ -23,7 +23,7 @@ describe.each([
'Standalone CLI',
path.resolve(__dirname, `../../packages/@tailwindcss-standalone/dist/${STANDALONE_BINARY}`),
],
-])('%s', (_, command) => {
+])('%s', (kind, command) => {
test(
'production build',
{
@@ -628,6 +628,198 @@ describe.each([
])
},
)
+
+ test(
+ 'production build + inline source maps',
+ {
+ fs: {
+ 'package.json': json`
+ {
+ "dependencies": {
+ "tailwindcss": "workspace:^",
+ "@tailwindcss/cli": "workspace:^"
+ }
+ }
+ `,
+ 'ssrc/index.html': html`
+
+ `,
+ 'src/index.css': css`
+ @import 'tailwindcss/utilities';
+ /* */
+ `,
+ },
+ },
+ async ({ exec, expect, fs, parseSourceMap }) => {
+ await exec(`${command} --input src/index.css --output dist/out.css --map`)
+
+ console.log(await fs.read('dist/out.css'))
+
+ await fs.expectFileToContain('dist/out.css', [candidate`flex`])
+
+ // Make sure we can find a source map
+ let map = parseSourceMap(await fs.read('dist/out.css'))
+
+ expect(map.at(1, 0)).toMatchObject({
+ source: null,
+ original: '(none)',
+ generated: '/*! tailwi...',
+ })
+
+ expect(map.at(2, 0)).toMatchObject({
+ source:
+ kind === 'CLI'
+ ? expect.stringContaining('node_modules/tailwindcss/utilities.css')
+ : expect.stringMatching(/\/utilities-\w+\.css$/),
+ original: '@tailwind...',
+ generated: '.flex {...',
+ })
+
+ expect(map.at(3, 2)).toMatchObject({
+ source:
+ kind === 'CLI'
+ ? expect.stringContaining('node_modules/tailwindcss/utilities.css')
+ : expect.stringMatching(/\/utilities-\w+\.css$/),
+ original: '@tailwind...',
+ generated: 'display: f...',
+ })
+
+ expect(map.at(4, 0)).toMatchObject({
+ source: null,
+ original: '(none)',
+ generated: '}...',
+ })
+ },
+ )
+
+ test(
+ 'production build + separate source maps',
+ {
+ fs: {
+ 'package.json': json`
+ {
+ "dependencies": {
+ "tailwindcss": "workspace:^",
+ "@tailwindcss/cli": "workspace:^"
+ }
+ }
+ `,
+ 'ssrc/index.html': html`
+
+ `,
+ 'src/index.css': css`
+ @import 'tailwindcss/utilities';
+ /* */
+ `,
+ },
+ },
+ async ({ exec, expect, fs, parseSourceMap }) => {
+ await exec(`${command} --input src/index.css --output dist/out.css --map dist/out.css.map`)
+
+ await fs.expectFileToContain('dist/out.css', [candidate`flex`])
+
+ // Make sure we can find a source map
+ let map = parseSourceMap({
+ map: await fs.read('dist/out.css.map'),
+ content: await fs.read('dist/out.css'),
+ })
+
+ expect(map.at(1, 0)).toMatchObject({
+ source: null,
+ original: '(none)',
+ generated: '/*! tailwi...',
+ })
+
+ expect(map.at(2, 0)).toMatchObject({
+ source:
+ kind === 'CLI'
+ ? expect.stringContaining('node_modules/tailwindcss/utilities.css')
+ : expect.stringMatching(/\/utilities-\w+\.css$/),
+ original: '@tailwind...',
+ generated: '.flex {...',
+ })
+
+ expect(map.at(3, 2)).toMatchObject({
+ source:
+ kind === 'CLI'
+ ? expect.stringContaining('node_modules/tailwindcss/utilities.css')
+ : expect.stringMatching(/\/utilities-\w+\.css$/),
+ original: '@tailwind...',
+ generated: 'display: f...',
+ })
+
+ expect(map.at(4, 0)).toMatchObject({
+ source: null,
+ original: '(none)',
+ generated: '}...',
+ })
+ },
+ )
+
+ // Skipped because Lightning CSS has a bug with source maps containing
+ // license comments and it breaks stuff.
+ test.skip(
+ 'production build + minify + source maps',
+ {
+ fs: {
+ 'package.json': json`
+ {
+ "dependencies": {
+ "tailwindcss": "workspace:^",
+ "@tailwindcss/cli": "workspace:^"
+ }
+ }
+ `,
+ 'ssrc/index.html': html`
+
+ `,
+ 'src/index.css': css`
+ @import 'tailwindcss/utilities';
+ /* */
+ `,
+ },
+ },
+ async ({ exec, expect, fs, parseSourceMap }) => {
+ await exec(`${command} --input src/index.css --output dist/out.css --minify --map`)
+
+ await fs.expectFileToContain('dist/out.css', [candidate`flex`])
+
+ console.log(await fs.read('dist/out.css'))
+
+ // Make sure we can find a source map
+ let map = parseSourceMap(await fs.read('dist/out.css'))
+
+ expect(map.at(1, 0)).toMatchObject({
+ source: null,
+ original: '(none)',
+ generated: '/*! tailwi...',
+ })
+
+ expect(map.at(2, 0)).toMatchObject({
+ source:
+ kind === 'CLI'
+ ? expect.stringContaining('node_modules/tailwindcss/utilities.css')
+ : expect.stringMatching(/\/utilities-\w+\.css$/),
+ original: '@tailwind...',
+ generated: '.flex {...',
+ })
+
+ expect(map.at(3, 2)).toMatchObject({
+ source:
+ kind === 'CLI'
+ ? expect.stringContaining('node_modules/tailwindcss/utilities.css')
+ : expect.stringMatching(/\/utilities-\w+\.css$/),
+ original: '@tailwind...',
+ generated: 'display: f...',
+ })
+
+ expect(map.at(4, 0)).toMatchObject({
+ source: null,
+ original: '(none)',
+ generated: '}...',
+ })
+ },
+ )
})
test(
diff --git a/packages/@tailwindcss-cli/src/commands/build/index.ts b/packages/@tailwindcss-cli/src/commands/build/index.ts
index 5e8ecdbae5d2..936540dddca6 100644
--- a/packages/@tailwindcss-cli/src/commands/build/index.ts
+++ b/packages/@tailwindcss-cli/src/commands/build/index.ts
@@ -1,5 +1,12 @@
import watcher from '@parcel/watcher'
-import { compile, env, Instrumentation, optimize } from '@tailwindcss/node'
+import {
+ compile,
+ env,
+ Instrumentation,
+ optimize,
+ toSourceMap,
+ type SourceMap,
+} from '@tailwindcss/node'
import { clearRequireCache } from '@tailwindcss/node/require-cache'
import { Scanner, type ChangedContent } from '@tailwindcss/oxide'
import { existsSync, type Stats } from 'node:fs'
@@ -52,6 +59,11 @@ export function options() {
description: 'The current working directory',
default: '.',
},
+ '--map': {
+ type: 'boolean | string',
+ description: 'Generate a source map',
+ default: false,
+ },
} satisfies Arg
}
@@ -105,6 +117,21 @@ export async function handle(args: Result>) {
process.exit(1)
}
+ // If the user passes `{bin} build --map -` then this likely means they want to output the map inline
+ // this is the default behavior of `{bin build} --map` to inform the user of that
+ if (args['--map'] === '-') {
+ eprintln(header())
+ eprintln()
+ eprintln(`Use --map without a value to inline the source map`)
+ process.exit(1)
+ }
+
+ // Resolve the map as an absolute path. If the output is true then we
+ // don't need to resolve it because it'll be an inline source map
+ if (args['--map'] && args['--map'] !== true) {
+ args['--map'] = path.resolve(base, args['--map'])
+ }
+
let start = process.hrtime.bigint()
let input = args['--input']
@@ -120,7 +147,12 @@ export async function handle(args: Result>) {
optimizedCss: '',
}
- async function write(css: string, args: Result>, I: Instrumentation) {
+ async function write(
+ css: string,
+ map: SourceMap | null,
+ args: Result>,
+ I: Instrumentation,
+ ) {
let output = css
// Optimize the output
@@ -130,10 +162,14 @@ export async function handle(args: Result>) {
let optimized = optimize(css, {
file: args['--input'] ?? 'input.css',
minify: args['--minify'] ?? false,
+ map: map?.raw ?? undefined,
})
DEBUG && I.end('Optimize CSS')
previous.css = css
previous.optimizedCss = optimized.code
+ if (optimized.map) {
+ map = toSourceMap(optimized.map)
+ }
output = optimized.code
} else {
output = previous.optimizedCss
@@ -141,6 +177,18 @@ export async function handle(args: Result>) {
}
// Write the output
+ if (map) {
+ // Inline the source map
+ if (args['--map'] === true) {
+ output += `\n`
+ output += map.inline
+ } else if (typeof args['--map'] === 'string') {
+ DEBUG && I.start('Write source map')
+ await outputFile(args['--map'], map.raw)
+ DEBUG && I.end('Write source map')
+ }
+ }
+
DEBUG && I.start('Write output')
if (args['--output'] && args['--output'] !== '-') {
await outputFile(args['--output'], output)
@@ -160,6 +208,7 @@ export async function handle(args: Result>) {
async function createCompiler(css: string, I: Instrumentation) {
DEBUG && I.start('Setup compiler')
let compiler = await compile(css, {
+ from: args['--output'] ? (inputFilePath ?? 'stdin.css') : undefined,
base: inputBasePath,
onDependency(path) {
fullRebuildPaths.push(path)
@@ -231,6 +280,7 @@ export async function handle(args: Result>) {
// Track the compiled CSS
let compiledCss = ''
+ let compiledMap: SourceMap | null = null
// Scan the entire `base` directory for full rebuilds.
if (rebuildStrategy === 'full') {
@@ -269,6 +319,12 @@ export async function handle(args: Result>) {
DEBUG && I.start('Build CSS')
compiledCss = compiler.build(candidates)
DEBUG && I.end('Build CSS')
+
+ if (args['--map']) {
+ DEBUG && I.start('Build Source Map')
+ compiledMap = compiler.buildSourceMap() as any
+ DEBUG && I.end('Build Source Map')
+ }
}
// Scan changed files only for incremental rebuilds.
@@ -288,9 +344,15 @@ export async function handle(args: Result>) {
DEBUG && I.start('Build CSS')
compiledCss = compiler.build(newCandidates)
DEBUG && I.end('Build CSS')
+
+ if (args['--map']) {
+ DEBUG && I.start('Build Source Map')
+ compiledMap = compiler.buildSourceMap() as any
+ DEBUG && I.end('Build Source Map')
+ }
}
- await write(compiledCss, args, I)
+ await write(compiledCss, compiledMap, args, I)
let end = process.hrtime.bigint()
eprintln(`Done in ${formatDuration(end - start)}`)
@@ -325,7 +387,16 @@ export async function handle(args: Result>) {
DEBUG && I.start('Build CSS')
let output = await handleError(() => compiler.build(candidates))
DEBUG && I.end('Build CSS')
- await write(output, args, I)
+
+ let map: SourceMap | null = null
+
+ if (args['--map']) {
+ DEBUG && I.start('Build Source Map')
+ map = await handleError(() => toSourceMap(compiler.buildSourceMap()))
+ DEBUG && I.end('Build Source Map')
+ }
+
+ await write(output, map, args, I)
let end = process.hrtime.bigint()
eprintln(header())
From bb78c597704935304b7d7633fdbaab6a25e9052a Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Tue, 29 Apr 2025 19:16:49 -0400
Subject: [PATCH 23/41] Add source map support to Vite
---
integrations/vite/source-maps.test.ts | 96 +++++++++++++++++++++++++
packages/@tailwindcss-vite/src/index.ts | 22 +++++-
2 files changed, 117 insertions(+), 1 deletion(-)
create mode 100644 integrations/vite/source-maps.test.ts
diff --git a/integrations/vite/source-maps.test.ts b/integrations/vite/source-maps.test.ts
new file mode 100644
index 000000000000..299cd11676be
--- /dev/null
+++ b/integrations/vite/source-maps.test.ts
@@ -0,0 +1,96 @@
+import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils'
+
+test(
+ `dev build`,
+ {
+ fs: {
+ 'package.json': json`
+ {
+ "type": "module",
+ "dependencies": {
+ "@tailwindcss/vite": "workspace:^",
+ "tailwindcss": "workspace:^"
+ },
+ "devDependencies": {
+ "lightningcss": "^1.26.0",
+ "vite": "^6"
+ }
+ }
+ `,
+ 'vite.config.ts': ts`
+ import tailwindcss from '@tailwindcss/vite'
+ import { defineConfig } from 'vite'
+
+ export default defineConfig({
+ plugins: [tailwindcss()],
+ css: {
+ devSourcemap: true,
+ },
+ build: {
+ sourcemap: true,
+ },
+ })
+ `,
+ 'index.html': html`
+
+
+
+
+ Hello, world!
+
+ `,
+ 'src/index.css': css`
+ @import 'tailwindcss/utilities';
+ /* */
+ `,
+ },
+ },
+ async ({ fs, spawn, expect, parseSourceMap }) => {
+ // Source maps only work in development mode in Vite
+ let process = await spawn('pnpm vite dev')
+ await process.onStdout((m) => m.includes('ready in'))
+
+ let url = ''
+ await process.onStdout((m) => {
+ let match = /Local:\s*(http.*)\//.exec(m)
+ if (match) url = match[1]
+ return Boolean(url)
+ })
+
+ let styles = await retryAssertion(async () => {
+ let styles = await fetchStyles(url, '/index.html')
+
+ // Wait until we have the right CSS
+ expect(styles).toContain(candidate`flex`)
+
+ return styles
+ })
+
+ // Make sure we can find a source map
+ let map = parseSourceMap(styles)
+
+ expect(map.at(1, 0)).toMatchObject({
+ source: null,
+ original: '(none)',
+ generated: '/*! tailwi...',
+ })
+
+ expect(map.at(2, 0)).toMatchObject({
+ source: expect.stringContaining('node_modules/tailwindcss/utilities.css'),
+ original: '@tailwind...',
+ generated: '.flex {...',
+ })
+
+ expect(map.at(3, 2)).toMatchObject({
+ source: expect.stringContaining('node_modules/tailwindcss/utilities.css'),
+ original: '@tailwind...',
+ generated: 'display: f...',
+ })
+
+ expect(map.at(4, 0)).toMatchObject({
+ source: null,
+ original: '(none)',
+ generated: '}...',
+ })
+ },
+)
diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts
index 0a053291a162..00ba92db4c83 100644
--- a/packages/@tailwindcss-vite/src/index.ts
+++ b/packages/@tailwindcss-vite/src/index.ts
@@ -1,4 +1,12 @@
-import { compile, env, Features, Instrumentation, normalizePath, optimize } from '@tailwindcss/node'
+import {
+ compile,
+ env,
+ Features,
+ Instrumentation,
+ normalizePath,
+ optimize,
+ toSourceMap,
+} from '@tailwindcss/node'
import { clearRequireCache } from '@tailwindcss/node/require-cache'
import { Scanner } from '@tailwindcss/oxide'
import fs from 'node:fs/promises'
@@ -37,6 +45,9 @@ export default function tailwindcss(): Plugin[] {
return new Root(
id,
config!.root,
+ // Currently, Vite only supports CSS source maps in development and they
+ // are off by default. Check to see if we need them or not.
+ config?.css.devSourcemap ?? false,
customCssResolver,
customJsResolver,
)
@@ -108,6 +119,7 @@ export default function tailwindcss(): Plugin[] {
DEBUG && I.start('[@tailwindcss/vite] Optimize CSS')
result = optimize(result.code, {
minify,
+ map: result.map,
})
DEBUG && I.end('[@tailwindcss/vite] Optimize CSS')
@@ -180,6 +192,7 @@ class Root {
private id: string,
private base: string,
+ private enableSourceMaps: boolean,
private customCssResolver: (id: string, base: string) => Promise,
private customJsResolver: (id: string, base: string) => Promise,
) {}
@@ -193,6 +206,7 @@ class Root {
): Promise<
| {
code: string
+ map: string | undefined
}
| false
> {
@@ -227,6 +241,7 @@ class Root {
DEBUG && I.start('Setup compiler')
let addBuildDependenciesPromises: Promise[] = []
this.compiler = await compile(content, {
+ from: this.enableSourceMaps ? this.id : undefined,
base: inputBase,
shouldRewriteUrls: true,
onDependency: (path) => {
@@ -328,8 +343,13 @@ class Root {
let code = this.compiler.build([...this.candidates])
DEBUG && I.end('Build CSS')
+ DEBUG && I.start('Build Source Map')
+ let map = this.enableSourceMaps ? toSourceMap(this.compiler.buildSourceMap()).raw : undefined
+ DEBUG && I.end('Build Source Map')
+
return {
code,
+ map,
}
}
From 00358d0686e7783eb2da56c5b9c2a1661dc5ed0b Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Thu, 3 Apr 2025 13:29:46 -0400
Subject: [PATCH 24/41] Add source map support to PostCSS
---
integrations/postcss/index.test.ts | 65 +++++++++++++++++++
packages/@tailwindcss-postcss/src/ast.ts | 72 ++++++++++++++++++++--
packages/@tailwindcss-postcss/src/index.ts | 1 +
3 files changed, 132 insertions(+), 6 deletions(-)
diff --git a/integrations/postcss/index.test.ts b/integrations/postcss/index.test.ts
index 7a263ba92d51..3e81195eef80 100644
--- a/integrations/postcss/index.test.ts
+++ b/integrations/postcss/index.test.ts
@@ -712,3 +712,68 @@ test(
await retryAssertion(async () => expect(await fs.read('dist/out.css')).toEqual(''))
},
)
+
+test(
+ 'dev mode + source maps',
+ {
+ fs: {
+ 'package.json': json`
+ {
+ "dependencies": {
+ "postcss": "^8",
+ "postcss-cli": "^11",
+ "tailwindcss": "workspace:^",
+ "@tailwindcss/postcss": "workspace:^"
+ }
+ }
+ `,
+ 'postcss.config.js': js`
+ module.exports = {
+ map: { inline: true },
+ plugins: {
+ '@tailwindcss/postcss': {},
+ },
+ }
+ `,
+ 'src/index.html': html`
+
+ `,
+ 'src/index.css': css`
+ @import 'tailwindcss/utilities';
+ @source not inline("inline");
+ /* */
+ `,
+ },
+ },
+ async ({ fs, exec, expect, parseSourceMap }) => {
+ await exec('pnpm postcss src/index.css --output dist/out.css')
+
+ await fs.expectFileToContain('dist/out.css', [candidate`flex`])
+
+ let map = parseSourceMap(await fs.read('dist/out.css'))
+
+ expect(map.at(1, 0)).toMatchObject({
+ source: '',
+ original: '(none)',
+ generated: '/*! tailwi...',
+ })
+
+ expect(map.at(2, 0)).toMatchObject({
+ source: expect.stringContaining('node_modules/tailwindcss/utilities.css'),
+ original: '@tailwind...',
+ generated: '.flex {...',
+ })
+
+ expect(map.at(3, 2)).toMatchObject({
+ source: expect.stringContaining('node_modules/tailwindcss/utilities.css'),
+ original: '@tailwind...',
+ generated: 'display: f...',
+ })
+
+ expect(map.at(4, 0)).toMatchObject({
+ source: expect.stringContaining('node_modules/tailwindcss/utilities.css'),
+ original: ';...',
+ generated: '}...',
+ })
+ },
+)
diff --git a/packages/@tailwindcss-postcss/src/ast.ts b/packages/@tailwindcss-postcss/src/ast.ts
index 201dd4bf8ac9..a2e1535e8cec 100644
--- a/packages/@tailwindcss-postcss/src/ast.ts
+++ b/packages/@tailwindcss-postcss/src/ast.ts
@@ -1,17 +1,54 @@
import postcss, {
+ Input,
type ChildNode as PostCssChildNode,
type Container as PostCssContainerNode,
type Root as PostCssRoot,
type Source as PostcssSource,
} from 'postcss'
import { atRule, comment, decl, rule, type AstNode } from '../../tailwindcss/src/ast'
+import { createLineTable, type LineTable } from '../../tailwindcss/src/source-maps/line-table'
+import type { Source, SourceLocation } from '../../tailwindcss/src/source-maps/source'
+import { DefaultMap } from '../../tailwindcss/src/utils/default-map'
const EXCLAMATION_MARK = 0x21
export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undefined): PostCssRoot {
+ let inputMap = new DefaultMap((src) => {
+ return new Input(src.code, {
+ map: source?.input.map,
+ from: src.file ?? undefined,
+ })
+ })
+
+ let lineTables = new DefaultMap((src) => createLineTable(src.code))
+
let root = postcss.root()
root.source = source
+ function toSource(loc: SourceLocation | undefined): PostcssSource | undefined {
+ // Use the fallback if this node has no location info in the AST
+ if (!loc) return
+ if (!loc[0]) return
+
+ let table = lineTables.get(loc[0])
+ let start = table.find(loc[1])
+ let end = table.find(loc[2])
+
+ return {
+ input: inputMap.get(loc[0]),
+ start: {
+ line: start.line,
+ column: start.column + 1,
+ offset: loc[1],
+ },
+ end: {
+ line: end.line,
+ column: end.column + 1,
+ offset: loc[2],
+ },
+ }
+ }
+
function transform(node: AstNode, parent: PostCssContainerNode) {
// Declaration
if (node.kind === 'declaration') {
@@ -20,14 +57,14 @@ export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undef
value: node.value ?? '',
important: node.important,
})
- astNode.source = source
+ astNode.source = toSource(node.src)
parent.append(astNode)
}
// Rule
else if (node.kind === 'rule') {
let astNode = postcss.rule({ selector: node.selector })
- astNode.source = source
+ astNode.source = toSource(node.src)
astNode.raws.semicolon = true
parent.append(astNode)
for (let child of node.nodes) {
@@ -38,7 +75,7 @@ export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undef
// AtRule
else if (node.kind === 'at-rule') {
let astNode = postcss.atRule({ name: node.name.slice(1), params: node.params })
- astNode.source = source
+ astNode.source = toSource(node.src)
astNode.raws.semicolon = true
parent.append(astNode)
for (let child of node.nodes) {
@@ -53,7 +90,7 @@ export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undef
// spaces.
astNode.raws.left = ''
astNode.raws.right = ''
- astNode.source = source
+ astNode.source = toSource(node.src)
parent.append(astNode)
}
@@ -75,18 +112,38 @@ export function cssAstToPostCssAst(ast: AstNode[], source: PostcssSource | undef
}
export function postCssAstToCssAst(root: PostCssRoot): AstNode[] {
+ let inputMap = new DefaultMap((input) => ({
+ file: input.file ?? input.id ?? null,
+ code: input.css,
+ }))
+
+ function toSource(node: PostCssChildNode): SourceLocation | undefined {
+ let source = node.source
+ if (!source) return
+
+ let input = source.input
+ if (!input) return
+ if (source.start === undefined) return
+ if (source.end === undefined) return
+
+ return [inputMap.get(input), source.start.offset, source.end.offset]
+ }
+
function transform(
node: PostCssChildNode,
parent: Extract['nodes'],
) {
// Declaration
if (node.type === 'decl') {
- parent.push(decl(node.prop, node.value, node.important))
+ let astNode = decl(node.prop, node.value, node.important)
+ astNode.src = toSource(node)
+ parent.push(astNode)
}
// Rule
else if (node.type === 'rule') {
let astNode = rule(node.selector)
+ astNode.src = toSource(node)
node.each((child) => transform(child, astNode.nodes))
parent.push(astNode)
}
@@ -94,6 +151,7 @@ export function postCssAstToCssAst(root: PostCssRoot): AstNode[] {
// AtRule
else if (node.type === 'atrule') {
let astNode = atRule(`@${node.name}`, node.params)
+ astNode.src = toSource(node)
node.each((child) => transform(child, astNode.nodes))
parent.push(astNode)
}
@@ -101,7 +159,9 @@ export function postCssAstToCssAst(root: PostCssRoot): AstNode[] {
// Comment
else if (node.type === 'comment') {
if (node.text.charCodeAt(0) !== EXCLAMATION_MARK) return
- parent.push(comment(node.text))
+ let astNode = comment(node.text)
+ astNode.src = toSource(node)
+ parent.push(astNode)
}
// Unknown
diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts
index 79e51d2aa574..8a37f542c94f 100644
--- a/packages/@tailwindcss-postcss/src/index.ts
+++ b/packages/@tailwindcss-postcss/src/index.ts
@@ -121,6 +121,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
DEBUG && I.start('Create compiler')
let compiler = await compileAst(ast, {
+ from: result.opts.from,
base: inputBasePath,
shouldRewriteUrls: true,
onDependency: (path) => context.fullRebuildPaths.push(path),
From 502bda71b82a5e3db8e84fd93de7147e8233f455 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Wed, 16 Apr 2025 17:39:57 -0400
Subject: [PATCH 25/41] Update lockfile
---
pnpm-lock.yaml | 66 +++++++++++++++++++++++++-------------------------
1 file changed, 33 insertions(+), 33 deletions(-)
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9bd72710aa0e..e381f6f6ab5d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -188,6 +188,9 @@ importers:
fast-glob:
specifier: ^3.3.3
version: 3.3.3
+ source-map-js:
+ specifier: ^1.2.1
+ version: 1.2.1
packages/@tailwindcss-browser:
devDependencies:
@@ -227,6 +230,9 @@ importers:
packages/@tailwindcss-node:
dependencies:
+ '@ampproject/remapping':
+ specifier: ^2.3.0
+ version: 2.3.0
enhanced-resolve:
specifier: ^5.18.1
version: 5.18.1
@@ -236,6 +242,12 @@ importers:
lightningcss:
specifier: 'catalog:'
version: 1.29.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm)
+ magic-string:
+ specifier: ^0.30.17
+ version: 0.30.17
+ source-map-js:
+ specifier: ^1.2.1
+ version: 1.2.1
tailwindcss:
specifier: workspace:*
version: link:../tailwindcss
@@ -437,6 +449,9 @@ importers:
packages/tailwindcss:
devDependencies:
+ '@ampproject/remapping':
+ specifier: ^2.3.0
+ version: 2.3.0
'@tailwindcss/oxide':
specifier: workspace:^
version: link:../../crates/node
@@ -449,6 +464,12 @@ importers:
lightningcss:
specifier: 'catalog:'
version: 1.29.2(patch_hash=tzyxy3asfxcqc7ihrooumyi5fm)
+ magic-string:
+ specifier: ^0.30.17
+ version: 0.30.17
+ source-map-js:
+ specifier: ^1.2.1
+ version: 1.2.1
playgrounds/nextjs:
dependencies:
@@ -2068,7 +2089,6 @@ packages:
'@parcel/watcher-darwin-arm64@2.5.1':
resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==}
engines: {node: '>= 10.0.0'}
- cpu: [arm64]
os: [darwin]
'@parcel/watcher-darwin-x64@2.5.0':
@@ -2080,7 +2100,6 @@ packages:
'@parcel/watcher-darwin-x64@2.5.1':
resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==}
engines: {node: '>= 10.0.0'}
- cpu: [x64]
os: [darwin]
'@parcel/watcher-freebsd-x64@2.5.0':
@@ -2128,7 +2147,6 @@ packages:
'@parcel/watcher-linux-arm64-glibc@2.5.1':
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
engines: {node: '>= 10.0.0'}
- cpu: [arm64]
os: [linux]
'@parcel/watcher-linux-arm64-musl@2.5.0':
@@ -2140,7 +2158,6 @@ packages:
'@parcel/watcher-linux-arm64-musl@2.5.1':
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
engines: {node: '>= 10.0.0'}
- cpu: [arm64]
os: [linux]
'@parcel/watcher-linux-x64-glibc@2.5.0':
@@ -2152,7 +2169,6 @@ packages:
'@parcel/watcher-linux-x64-glibc@2.5.1':
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
engines: {node: '>= 10.0.0'}
- cpu: [x64]
os: [linux]
'@parcel/watcher-linux-x64-musl@2.5.0':
@@ -2164,7 +2180,6 @@ packages:
'@parcel/watcher-linux-x64-musl@2.5.1':
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
engines: {node: '>= 10.0.0'}
- cpu: [x64]
os: [linux]
'@parcel/watcher-wasm@2.5.0':
@@ -2206,7 +2221,6 @@ packages:
'@parcel/watcher-win32-x64@2.5.1':
resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==}
engines: {node: '>= 10.0.0'}
- cpu: [x64]
os: [win32]
'@parcel/watcher@2.5.0':
@@ -2621,7 +2635,6 @@ packages:
bun@1.2.11:
resolution: {integrity: sha512-9brVfsp6/TYVsE3lCl1MUxoyKhvljqyL1MNPErgwsOaS9g4Gzi2nY+W5WtRAXGzLrgz5jzsoGHHwyH/rTeRCIg==}
- cpu: [arm64, x64, aarch64]
os: [darwin, linux, win32]
hasBin: true
@@ -3480,13 +3493,11 @@ packages:
lightningcss-darwin-arm64@1.29.2:
resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==}
engines: {node: '>= 12.0.0'}
- cpu: [arm64]
os: [darwin]
lightningcss-darwin-x64@1.29.2:
resolution: {integrity: sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==}
engines: {node: '>= 12.0.0'}
- cpu: [x64]
os: [darwin]
lightningcss-freebsd-x64@1.29.2:
@@ -3504,25 +3515,21 @@ packages:
lightningcss-linux-arm64-gnu@1.29.2:
resolution: {integrity: sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==}
engines: {node: '>= 12.0.0'}
- cpu: [arm64]
os: [linux]
lightningcss-linux-arm64-musl@1.29.2:
resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==}
engines: {node: '>= 12.0.0'}
- cpu: [arm64]
os: [linux]
lightningcss-linux-x64-gnu@1.29.2:
resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==}
engines: {node: '>= 12.0.0'}
- cpu: [x64]
os: [linux]
lightningcss-linux-x64-musl@1.29.2:
resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==}
engines: {node: '>= 12.0.0'}
- cpu: [x64]
os: [linux]
lightningcss-win32-arm64-msvc@1.29.2:
@@ -3534,7 +3541,6 @@ packages:
lightningcss-win32-x64-msvc@1.29.2:
resolution: {integrity: sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==}
engines: {node: '>= 12.0.0'}
- cpu: [x64]
os: [win32]
lightningcss@1.29.2:
@@ -3592,8 +3598,8 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
- magic-string@0.30.11:
- resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==}
+ magic-string@0.30.17:
+ resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
@@ -4113,10 +4119,6 @@ packages:
resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==}
engines: {node: '>=14.16'}
- source-map-js@1.2.0:
- resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
- engines: {node: '>=0.10.0'}
-
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -6156,7 +6158,7 @@ snapshots:
'@vitest/snapshot@2.0.5':
dependencies:
'@vitest/pretty-format': 2.0.5
- magic-string: 0.30.11
+ magic-string: 0.30.17
pathe: 1.1.2
'@vitest/spy@2.0.5':
@@ -6817,7 +6819,7 @@ snapshots:
debug: 4.4.0
enhanced-resolve: 5.18.1
eslint: 9.25.1(jiti@2.4.2)
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.25.1(jiti@2.4.2))
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2))
fast-glob: 3.3.3
get-tsconfig: 4.8.1
is-bun-module: 1.2.1
@@ -6836,7 +6838,7 @@ snapshots:
debug: 4.4.0
enhanced-resolve: 5.18.1
eslint: 9.25.1(jiti@2.4.2)
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.25.1(jiti@2.4.2))
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2))
fast-glob: 3.3.3
get-tsconfig: 4.8.1
is-bun-module: 1.2.1
@@ -6849,7 +6851,7 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
- eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.25.1(jiti@2.4.2)):
+ eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)):
dependencies:
debug: 3.2.7
optionalDependencies:
@@ -6860,7 +6862,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.25.1(jiti@2.4.2)):
+ eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)):
dependencies:
debug: 3.2.7
optionalDependencies:
@@ -6882,7 +6884,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.25.1(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.25.1(jiti@2.4.2))
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2))
hasown: 2.0.2
is-core-module: 2.15.1
is-glob: 4.0.3
@@ -6911,7 +6913,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.25.1(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.25.1(jiti@2.4.2))
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2))
hasown: 2.0.2
is-core-module: 2.15.1
is-glob: 4.0.3
@@ -7560,7 +7562,7 @@ snapshots:
dependencies:
yallist: 3.1.1
- magic-string@0.30.11:
+ magic-string@0.30.17:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
@@ -7865,7 +7867,7 @@ snapshots:
dependencies:
nanoid: 3.3.7
picocolors: 1.1.1
- source-map-js: 1.2.0
+ source-map-js: 1.2.1
postcss@8.4.47:
dependencies:
@@ -8087,8 +8089,6 @@ snapshots:
slash@5.1.0: {}
- source-map-js@1.2.0: {}
-
source-map-js@1.2.1: {}
source-map-support@0.5.21:
@@ -8523,7 +8523,7 @@ snapshots:
chai: 5.1.1
debug: 4.3.6
execa: 8.0.1
- magic-string: 0.30.11
+ magic-string: 0.30.17
pathe: 1.1.2
std-env: 3.7.0
tinybench: 2.9.0
From c9cb8a7debb0c057574946534dd0ddac3143c14c Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Mon, 5 May 2025 17:56:23 -0400
Subject: [PATCH 26/41] Fix UI tests
---
packages/tailwindcss/tests/ui.spec.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/tailwindcss/tests/ui.spec.ts b/packages/tailwindcss/tests/ui.spec.ts
index a4efc6468ac3..0c3534efd748 100644
--- a/packages/tailwindcss/tests/ui.spec.ts
+++ b/packages/tailwindcss/tests/ui.spec.ts
@@ -2216,7 +2216,7 @@ async function render(page: Page, content: string, extraCss: string = '') {
let scanner = new Scanner({})
let candidates = scanner.scanFiles([{ content, extension: 'html' }])
- let styles = optimize(build(candidates))
+ let { code: styles } = optimize(build(candidates))
content = `${content}`
await page.setContent(content)
From 65e2c176272591dc8aaee2953b0772db0747bfdb Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Tue, 6 May 2025 10:36:15 -0400
Subject: [PATCH 27/41] Optimize `LineTable#find` implementation
---
packages/tailwindcss/src/source-maps/line-table.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/tailwindcss/src/source-maps/line-table.ts b/packages/tailwindcss/src/source-maps/line-table.ts
index d42be8055206..e34ec8fd4680 100644
--- a/packages/tailwindcss/src/source-maps/line-table.ts
+++ b/packages/tailwindcss/src/source-maps/line-table.ts
@@ -62,8 +62,8 @@ export function createLineTable(source: string): LineTable {
let line = 0
let count = table.length
while (count > 0) {
- // `| 0` causes integer division
- let mid = (count / 2) | 0
+ // `| 0` forces integer bytecode generation
+ let mid = (count | 0) >> 1
let i = line + mid
if (table[i] <= offset) {
line = i + 1
From 5e2fd98f601b24a19cae08f771d4ba1cccd1dbbb Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Tue, 6 May 2025 11:14:07 -0400
Subject: [PATCH 28/41] =?UTF-8?q?Don=E2=80=99t=20double=20clone=20nodes?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/tailwindcss/src/apply.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts
index 71cfc1e039ce..d0de38a3d78d 100644
--- a/packages/tailwindcss/src/apply.ts
+++ b/packages/tailwindcss/src/apply.ts
@@ -193,7 +193,7 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
candidateSrc[1] += 7 + candidateOffset
candidateSrc[2] = candidateSrc[1] + candidate.length
- return { node: structuredClone(node), src: candidateSrc }
+ return { node, src: candidateSrc }
})
for (let { node, src } of details) {
From 2e8d099475ea0c9e11383c09ed5c468b91dd5722 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Tue, 6 May 2025 11:23:27 -0400
Subject: [PATCH 29/41] Move walk into map fn
---
packages/tailwindcss/src/apply.ts | 21 ++++++++++++---------
1 file changed, 12 insertions(+), 9 deletions(-)
diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts
index d0de38a3d78d..6785b8759204 100644
--- a/packages/tailwindcss/src/apply.ts
+++ b/packages/tailwindcss/src/apply.ts
@@ -178,14 +178,21 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
let src = node.src
- let details = compiled.astNodes.map((node) => {
+ let candidateAst = compiled.astNodes.map((node) => {
let candidate = compiled.nodeSorting.get(node)?.candidate
let candidateOffset = candidate ? candidateOffsets[candidate] : undefined
node = structuredClone(node)
if (!src || !candidate || candidateOffset === undefined) {
- return { node, src }
+ // While the original nodes may have come from an `@utility` we still
+ // want to replace the source because the `@apply` is ultimately the
+ // reason the node was emitted into the AST.
+ walk([node], (node) => {
+ node.src = src
+ })
+
+ return node
}
let candidateSrc: SourceLocation = [src[0], src[1], src[2]]
@@ -193,19 +200,15 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
candidateSrc[1] += 7 + candidateOffset
candidateSrc[2] = candidateSrc[1] + candidate.length
- return { node, src: candidateSrc }
- })
-
- for (let { node, src } of details) {
// While the original nodes may have come from an `@utility` we still
// want to replace the source because the `@apply` is ultimately the
// reason the node was emitted into the AST.
walk([node], (node) => {
- node.src = src
+ node.src = candidateSrc
})
- }
- let candidateAst = details.map((d) => d.node)
+ return node
+ })
// Collect the nodes to insert in place of the `@apply` rule. When a rule
// was used, we want to insert its children instead of the rule because we
From 78915dc156f527859cb27573bad2254d2c10f8e5 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Tue, 6 May 2025 11:25:29 -0400
Subject: [PATCH 30/41] Remove leftover logs
---
integrations/cli/index.test.ts | 4 ----
1 file changed, 4 deletions(-)
diff --git a/integrations/cli/index.test.ts b/integrations/cli/index.test.ts
index 22d3a9c6aea0..5c5cd1b29036 100644
--- a/integrations/cli/index.test.ts
+++ b/integrations/cli/index.test.ts
@@ -653,8 +653,6 @@ describe.each([
async ({ exec, expect, fs, parseSourceMap }) => {
await exec(`${command} --input src/index.css --output dist/out.css --map`)
- console.log(await fs.read('dist/out.css'))
-
await fs.expectFileToContain('dist/out.css', [candidate`flex`])
// Make sure we can find a source map
@@ -784,8 +782,6 @@ describe.each([
await fs.expectFileToContain('dist/out.css', [candidate`flex`])
- console.log(await fs.read('dist/out.css'))
-
// Make sure we can find a source map
let map = parseSourceMap(await fs.read('dist/out.css'))
From db5d97403e8ee4cf34357583cd08c777dac09a89 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Tue, 6 May 2025 11:26:06 -0400
Subject: [PATCH 31/41] Fix typo
---
packages/@tailwindcss-node/src/optimize.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/@tailwindcss-node/src/optimize.ts b/packages/@tailwindcss-node/src/optimize.ts
index 9ebffae98508..4024d15b8e0a 100644
--- a/packages/@tailwindcss-node/src/optimize.ts
+++ b/packages/@tailwindcss-node/src/optimize.ts
@@ -16,7 +16,7 @@ export interface OptimizeOptions {
/**
* The output source map before optimization
*
- * If omitted an resulting source map will not be available
+ * If omitted a resulting source map will not be available
*/
map?: string
}
From 8fdb8baaa11a9d31b15a27d236e33e44690763c4 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Tue, 6 May 2025 11:26:44 -0400
Subject: [PATCH 32/41] Increment unknown source ID when generating maps
---
packages/@tailwindcss-node/src/source-maps.ts | 2 +-
packages/tailwindcss/src/source-maps/source-map.test.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/@tailwindcss-node/src/source-maps.ts b/packages/@tailwindcss-node/src/source-maps.ts
index 88ba54e2f258..3f02efb24c00 100644
--- a/packages/@tailwindcss-node/src/source-maps.ts
+++ b/packages/@tailwindcss-node/src/source-maps.ts
@@ -20,7 +20,7 @@ function serializeSourceMap(map: DecodedSourceMap): string {
}
>((src) => {
return {
- url: src?.url ?? ``,
+ url: src?.url ?? ``,
content: src?.content ?? '',
}
})
diff --git a/packages/tailwindcss/src/source-maps/source-map.test.ts b/packages/tailwindcss/src/source-maps/source-map.test.ts
index 7a6e0471f8b5..0f5ec5483827 100644
--- a/packages/tailwindcss/src/source-maps/source-map.test.ts
+++ b/packages/tailwindcss/src/source-maps/source-map.test.ts
@@ -59,7 +59,7 @@ function toRawSourceMap(map: DecodedSourceMap): string {
}
>((src) => {
return {
- url: src?.url ?? ``,
+ url: src?.url ?? ``,
content: src?.content ?? '',
}
})
From 1992ba55ab9015eb8ab08248ffcb0675492e6a58 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Tue, 6 May 2025 11:27:12 -0400
Subject: [PATCH 33/41] Add comment clarifying magic number
---
packages/tailwindcss/src/apply.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts
index 6785b8759204..897e569b88a1 100644
--- a/packages/tailwindcss/src/apply.ts
+++ b/packages/tailwindcss/src/apply.ts
@@ -197,7 +197,7 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
let candidateSrc: SourceLocation = [src[0], src[1], src[2]]
- candidateSrc[1] += 7 + candidateOffset
+ candidateSrc[1] += 7 /* '@apply '.length */ + candidateOffset
candidateSrc[2] = candidateSrc[1] + candidate.length
// While the original nodes may have come from an `@utility` we still
From 58932d5a25e6c6d15085521dcc58a0cafe3c18a3 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Tue, 6 May 2025 11:28:17 -0400
Subject: [PATCH 34/41] Inline function to push mappings
---
packages/tailwindcss/src/source-maps/source-map.ts | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/packages/tailwindcss/src/source-maps/source-map.ts b/packages/tailwindcss/src/source-maps/source-map.ts
index 1e048cdd00e3..8073b78a0792 100644
--- a/packages/tailwindcss/src/source-maps/source-map.ts
+++ b/packages/tailwindcss/src/source-maps/source-map.ts
@@ -90,7 +90,7 @@ export function createSourceMap({ ast }: { ast: AstNode[] }) {
}
// Get all the indexes from the mappings
- function add(node: AstNode) {
+ walk(ast, (node: AstNode) => {
if (!node.src || !node.dst) return
let originalSource = sourceTable.get(node.src[0])
@@ -141,9 +141,7 @@ export function createSourceMap({ ast }: { ast: AstNode[] }) {
},
generatedPosition: generatedEnd,
})
- }
-
- walk(ast, add)
+ })
// Populate
for (let source of lineTables.keys()) {
From e3561fba8e578cda823b9f5f779e8c304f0c14e3 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Tue, 6 May 2025 11:29:11 -0400
Subject: [PATCH 35/41] Update type
---
packages/tailwindcss/src/source-maps/source-map.ts | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/packages/tailwindcss/src/source-maps/source-map.ts b/packages/tailwindcss/src/source-maps/source-map.ts
index 8073b78a0792..58f544568363 100644
--- a/packages/tailwindcss/src/source-maps/source-map.ts
+++ b/packages/tailwindcss/src/source-maps/source-map.ts
@@ -173,7 +173,12 @@ export function createTranslationMap({
let originalTable = createLineTable(original)
let generatedTable = createLineTable(generated)
- type Translation = [Position, Position, Position | null, Position | null]
+ type Translation = [
+ originalStart: Position,
+ originalEnd: Position,
+ generatedStart: Position | null,
+ generatedEnd: Position | null,
+ ]
return (node: AstNode) => {
if (!node.src) return []
From 347d9e2971c986ea51ebf80134791c44368a2f2b Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Wed, 7 May 2025 09:10:36 -0400
Subject: [PATCH 36/41] Remove unncessary settings from test
---
integrations/vite/source-maps.test.ts | 3 ---
1 file changed, 3 deletions(-)
diff --git a/integrations/vite/source-maps.test.ts b/integrations/vite/source-maps.test.ts
index 299cd11676be..5a4b4ab79bb4 100644
--- a/integrations/vite/source-maps.test.ts
+++ b/integrations/vite/source-maps.test.ts
@@ -26,9 +26,6 @@ test(
css: {
devSourcemap: true,
},
- build: {
- sourcemap: true,
- },
})
`,
'index.html': html`
From f3fc8137f25a3e7430e4d50e7530f0bc1d4ded91 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Wed, 7 May 2025 09:10:40 -0400
Subject: [PATCH 37/41] Add line table benchmark
---
.../tailwindcss/src/source-maps/line-table.bench.ts | 13 +++++++++++++
1 file changed, 13 insertions(+)
create mode 100644 packages/tailwindcss/src/source-maps/line-table.bench.ts
diff --git a/packages/tailwindcss/src/source-maps/line-table.bench.ts b/packages/tailwindcss/src/source-maps/line-table.bench.ts
new file mode 100644
index 000000000000..862e2d398297
--- /dev/null
+++ b/packages/tailwindcss/src/source-maps/line-table.bench.ts
@@ -0,0 +1,13 @@
+import { readFileSync } from 'node:fs'
+import { fileURLToPath } from 'node:url'
+
+import { bench } from 'vitest'
+import { createLineTable } from './line-table'
+
+const currentFolder = fileURLToPath(new URL('..', import.meta.url))
+const cssFile = readFileSync(currentFolder + '../preflight.css', 'utf-8')
+const table = createLineTable(cssFile)
+
+bench('line table lookups', () => {
+ for (let i = 0; i < cssFile.length; ++i) table.find(i)
+})
From 83e2cd296dab547fdb97f614596ea6cbaa2edbc7 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Wed, 7 May 2025 16:04:45 -0400
Subject: [PATCH 38/41] Clarify perf comment
The actual unoptimized bytecode contains one additional instruction but this is definitely a bit faster in benchmarks
---
packages/tailwindcss/src/source-maps/line-table.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/tailwindcss/src/source-maps/line-table.ts b/packages/tailwindcss/src/source-maps/line-table.ts
index e34ec8fd4680..e16051762786 100644
--- a/packages/tailwindcss/src/source-maps/line-table.ts
+++ b/packages/tailwindcss/src/source-maps/line-table.ts
@@ -62,7 +62,7 @@ export function createLineTable(source: string): LineTable {
let line = 0
let count = table.length
while (count > 0) {
- // `| 0` forces integer bytecode generation
+ // `| 0` improves performance (in V8 at least)
let mid = (count | 0) >> 1
let i = line + mid
if (table[i] <= offset) {
From ec0453dfecb4d572afc9e894211c6109931a80a4 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Wed, 7 May 2025 18:40:22 -0400
Subject: [PATCH 39/41] Improve sorting of mappings
---
.../tailwindcss/src/source-maps/source-map.ts | 17 +++++++++++++----
1 file changed, 13 insertions(+), 4 deletions(-)
diff --git a/packages/tailwindcss/src/source-maps/source-map.ts b/packages/tailwindcss/src/source-maps/source-map.ts
index 58f544568363..a7afc54fac49 100644
--- a/packages/tailwindcss/src/source-maps/source-map.ts
+++ b/packages/tailwindcss/src/source-maps/source-map.ts
@@ -150,11 +150,20 @@ export function createSourceMap({ ast }: { ast: AstNode[] }) {
// Sort the mappings in ascending order
map.mappings.sort((a, b) => {
+ let aOriginal = a.originalPosition!
+ let aGenerated = a.generatedPosition!
+ let bOriginal = b.originalPosition!
+ let bGenerated = b.generatedPosition!
+
+ let aSource = map.sources.indexOf(aOriginal.source)
+ let bSource = map.sources.indexOf(bOriginal.source)
+
return (
- a.generatedPosition.line - b.generatedPosition.line ||
- a.generatedPosition.column - b.generatedPosition.column ||
- (a.originalPosition?.line ?? 0) - (b.originalPosition?.line ?? 0) ||
- (a.originalPosition?.column ?? 0) - (b.originalPosition?.column ?? 0)
+ aGenerated.line - bGenerated.line ||
+ aGenerated.column - bGenerated.column ||
+ aSource - bSource ||
+ aOriginal.line - bOriginal.line ||
+ aOriginal.column - bGenerated.column
)
})
From 3551daad373ac9d9123170b5d0b9bd9f32436efe Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Wed, 7 May 2025 18:52:57 -0400
Subject: [PATCH 40/41] wip visualize souce maps in tests
---
.../src/source-maps/source-map.test.ts | 2217 ++++++++++++++---
.../tailwindcss/src/source-maps/visualizer.ts | 120 +
2 files changed, 2019 insertions(+), 318 deletions(-)
create mode 100644 packages/tailwindcss/src/source-maps/visualizer.ts
diff --git a/packages/tailwindcss/src/source-maps/source-map.test.ts b/packages/tailwindcss/src/source-maps/source-map.test.ts
index 0f5ec5483827..b833a84e9908 100644
--- a/packages/tailwindcss/src/source-maps/source-map.test.ts
+++ b/packages/tailwindcss/src/source-maps/source-map.test.ts
@@ -1,14 +1,13 @@
-import remapping from '@ampproject/remapping'
import dedent from 'dedent'
-import MagicString from 'magic-string'
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
-import { SourceMapConsumer, SourceMapGenerator, type RawSourceMap } from 'source-map-js'
-import { test } from 'vitest'
+import { expect, test } from 'vitest'
import { compile } from '..'
+import { toCss } from '../ast'
+import * as CSS from '../css-parser'
import createPlugin from '../plugin'
-import { DefaultMap } from '../utils/default-map'
-import type { DecodedSource, DecodedSourceMap } from './source-map'
+import { createSourceMap } from './source-map'
+import { visualize } from './visualizer'
const css = dedent
interface RunOptions {
@@ -18,10 +17,9 @@ interface RunOptions {
}
async function run({ input, candidates, options }: RunOptions) {
- let source = new MagicString(input)
let root = path.resolve(__dirname, '../..')
- let compiler = await compile(source.toString(), {
+ let compiler = await compile(input, {
from: 'input.css',
async loadStylesheet(id, base) {
let resolvedPath = path.resolve(root, id === 'tailwindcss' ? 'index.css' : id)
@@ -37,117 +35,19 @@ async function run({ input, candidates, options }: RunOptions) {
let css = compiler.build(candidates ?? [])
let decoded = compiler.buildSourceMap()
- let rawMap = toRawSourceMap(decoded)
- let combined = remapping(rawMap, () => null)
- let map = JSON.parse(rawMap.toString()) as RawSourceMap
- let sources = combined.sources
- let annotations = formattedMappings(map)
-
- return { css, map, sources, annotations }
+ return visualize(css, decoded)
}
-function toRawSourceMap(map: DecodedSourceMap): string {
- let generator = new SourceMapGenerator()
-
- let id = 1
- let sourceTable = new DefaultMap<
- DecodedSource | null,
- {
- url: string
- content: string
- }
- >((src) => {
- return {
- url: src?.url ?? ``,
- content: src?.content ?? '',
- }
- })
-
- for (let mapping of map.mappings) {
- let original = sourceTable.get(mapping.originalPosition?.source ?? null)
-
- generator.addMapping({
- generated: mapping.generatedPosition,
- original: mapping.originalPosition,
- source: original.url,
- name: mapping.name ?? undefined,
- })
-
- generator.setSourceContent(original.url, original.content)
- }
-
- return generator.toString()
+async function analyze({ input }: RunOptions) {
+ let ast = CSS.parse(input, { from: 'input.css' })
+ let css = toCss(ast, true)
+ let decoded = createSourceMap({ ast })
+ return visualize(css, decoded)
}
-/**
- * An string annotation that represents a source map
- *
- * It's not meant to be exhaustive just enough to
- * verify that the source map is working and that
- * lines are mapped back to the original source
- *
- * Including when using @apply with multiple classes
- */
-function formattedMappings(map: RawSourceMap) {
- const smc = new SourceMapConsumer(map)
- const annotations: Record<
- number,
- {
- original: { start: [number, number]; end: [number, number] }
- generated: { start: [number, number]; end: [number, number] }
- source: string
- }
- > = {}
-
- smc.eachMapping((mapping) => {
- let annotation = (annotations[mapping.generatedLine] = annotations[mapping.generatedLine] || {
- ...mapping,
-
- original: {
- start: [mapping.originalLine, mapping.originalColumn],
- end: [mapping.originalLine, mapping.originalColumn],
- },
-
- generated: {
- start: [mapping.generatedLine, mapping.generatedColumn],
- end: [mapping.generatedLine, mapping.generatedColumn],
- },
-
- source: mapping.source,
- })
-
- annotation.generated.end[0] = mapping.generatedLine
- annotation.generated.end[1] = mapping.generatedColumn
-
- annotation.original.end[0] = mapping.originalLine!
- annotation.original.end[1] = mapping.originalColumn!
- })
-
- return Object.values(annotations).map((annotation) => {
- return `${annotation.source}: ${formatRange(annotation.generated)} <- ${formatRange(annotation.original)}`
- })
-}
-
-function formatRange(range: { start: [number, number]; end: [number, number] }) {
- if (range.start[0] === range.end[0]) {
- // This range is on the same line
- // and the columns are the same
- if (range.start[1] === range.end[1]) {
- return `${range.start[0]}:${range.start[1]}`
- }
-
- // This range is on the same line
- // but the columns are different
- return `${range.start[0]}:${range.start[1]}-${range.end[1]}`
- }
-
- // This range spans multiple lines
- return `${range.start[0]}:${range.start[1]}-${range.end[0]}:${range.end[1]}`
-}
-
-test('source maps trace back to @import location', async ({ expect }) => {
- let { sources, annotations } = await run({
+test('source maps trace back to @import location', async () => {
+ let visualized = await run({
input: css`
@import 'tailwindcss';
@@ -157,148 +57,1386 @@ test('source maps trace back to @import location', async ({ expect }) => {
`,
})
- // All CSS should be mapped back to the original source file
- expect(sources).toEqual([
- //
- 'index.css',
- 'theme.css',
- 'preflight.css',
- 'input.css',
- ])
- expect(sources.length).toBe(4)
-
- // The output CSS should include annotations linking back to:
- // 1. The class definition `.foo`
- // 2. The `@apply underline` line inside of it
- expect(annotations).toEqual([
- 'index.css: 1:0-41 <- 1:0-41',
- 'index.css: 2:0-13 <- 3:0-34',
- 'theme.css: 3:2-15 <- 1:0-15',
- 'theme.css: 4:4 <- 2:2-4:0',
- 'theme.css: 5:22 <- 4:22',
- 'theme.css: 6:4 <- 6:2-8:0',
- 'theme.css: 7:13 <- 8:13',
- 'theme.css: 8:4-43 <- 446:2-54',
- 'theme.css: 9:4-48 <- 449:2-59',
- 'index.css: 12:0-12 <- 4:0-37',
- 'preflight.css: 13:2-59 <- 7:0-11:23',
- 'preflight.css: 14:4-26 <- 12:2-24',
- 'preflight.css: 15:4-13 <- 13:2-11',
- 'preflight.css: 16:4-14 <- 14:2-12',
- 'preflight.css: 17:4-19 <- 15:2-17',
- 'preflight.css: 19:2-14 <- 28:0-29:6',
- 'preflight.css: 20:4-20 <- 30:2-18',
- 'preflight.css: 21:4-34 <- 31:2-32',
- 'preflight.css: 22:4-15 <- 32:2-13',
- 'preflight.css: 23:4-159 <- 33:2-42:3',
- 'preflight.css: 24:4-71 <- 43:2-73',
- 'preflight.css: 25:4-75 <- 44:2-77',
- 'preflight.css: 26:4-44 <- 45:2-42',
- 'preflight.css: 28:2-5 <- 54:0-3',
- 'preflight.css: 29:4-13 <- 55:2-11',
- 'preflight.css: 30:4-18 <- 56:2-16',
- 'preflight.css: 31:4-25 <- 57:2-23',
- 'preflight.css: 33:2-22 <- 64:0-20',
- 'preflight.css: 34:4-45 <- 65:2-43',
- 'preflight.css: 35:4-37 <- 66:2-35',
- 'preflight.css: 37:2-25 <- 73:0-78:3',
- 'preflight.css: 38:4-22 <- 79:2-20',
- 'preflight.css: 39:4-24 <- 80:2-22',
- 'preflight.css: 41:2-4 <- 87:0-2',
- 'preflight.css: 42:4-18 <- 88:2-16',
- 'preflight.css: 43:4-36 <- 89:2-34',
- 'preflight.css: 44:4-28 <- 90:2-26',
- 'preflight.css: 46:2-12 <- 97:0-98:7',
- 'preflight.css: 47:4-23 <- 99:2-21',
- 'preflight.css: 49:2-23 <- 109:0-112:4',
- 'preflight.css: 50:4-148 <- 113:2-123:3',
- 'preflight.css: 51:4-76 <- 124:2-78',
- 'preflight.css: 52:4-80 <- 125:2-82',
- 'preflight.css: 53:4-18 <- 126:2-16',
- 'preflight.css: 55:2-8 <- 133:0-6',
- 'preflight.css: 56:4-18 <- 134:2-16',
- 'preflight.css: 58:2-11 <- 141:0-142:4',
- 'preflight.css: 59:4-18 <- 143:2-16',
- 'preflight.css: 60:4-18 <- 144:2-16',
- 'preflight.css: 61:4-22 <- 145:2-20',
- 'preflight.css: 62:4-28 <- 146:2-26',
- 'preflight.css: 64:2-6 <- 149:0-4',
- 'preflight.css: 65:4-19 <- 150:2-17',
- 'preflight.css: 67:2-6 <- 153:0-4',
- 'preflight.css: 68:4-15 <- 154:2-13',
- 'preflight.css: 70:2-8 <- 163:0-6',
- 'preflight.css: 71:4-18 <- 164:2-16',
- 'preflight.css: 72:4-25 <- 165:2-23',
- 'preflight.css: 73:4-29 <- 166:2-27',
- 'preflight.css: 75:2-18 <- 173:0-16',
- 'preflight.css: 76:4-17 <- 174:2-15',
- 'preflight.css: 78:2-11 <- 181:0-9',
- 'preflight.css: 79:4-28 <- 182:2-26',
- 'preflight.css: 81:2-10 <- 189:0-8',
- 'preflight.css: 82:4-22 <- 190:2-20',
- 'preflight.css: 84:2-15 <- 197:0-199:5',
- 'preflight.css: 85:4-20 <- 200:2-18',
- 'preflight.css: 87:2-56 <- 209:0-216:7',
- 'preflight.css: 88:4-18 <- 217:2-16',
- 'preflight.css: 89:4-26 <- 218:2-24',
- 'preflight.css: 91:2-13 <- 225:0-226:6',
- 'preflight.css: 92:4-19 <- 227:2-17',
- 'preflight.css: 93:4-16 <- 228:2-14',
- 'preflight.css: 95:2-68 <- 238:0-243:23',
- 'preflight.css: 96:4-17 <- 244:2-15',
- 'preflight.css: 97:4-34 <- 245:2-32',
- 'preflight.css: 98:4-36 <- 246:2-34',
- 'preflight.css: 99:4-27 <- 247:2-25',
- 'preflight.css: 100:4-18 <- 248:2-16',
- 'preflight.css: 101:4-20 <- 249:2-18',
- 'preflight.css: 102:4-33 <- 250:2-31',
- 'preflight.css: 103:4-14 <- 251:2-12',
- 'preflight.css: 105:2-49 <- 258:0-47',
- 'preflight.css: 106:4-23 <- 259:2-21',
- 'preflight.css: 108:2-56 <- 266:0-54',
- 'preflight.css: 109:4-30 <- 267:2-28',
- 'preflight.css: 111:2-25 <- 274:0-23',
- 'preflight.css: 112:4-26 <- 275:2-24',
- 'preflight.css: 114:2-16 <- 282:0-14',
- 'preflight.css: 115:4-14 <- 283:2-12',
- 'preflight.css: 117:2-92 <- 291:0-292:49',
- 'preflight.css: 118:4-18 <- 293:2-16',
- 'preflight.css: 119:6-25 <- 294:4-61',
- 'preflight.css: 120:6-53 <- 294:4-61',
- 'preflight.css: 121:8-65 <- 294:4-61',
- 'preflight.css: 125:2-11 <- 302:0-9',
- 'preflight.css: 126:4-20 <- 303:2-18',
- 'preflight.css: 128:2-30 <- 310:0-28',
- 'preflight.css: 129:4-28 <- 311:2-26',
- 'preflight.css: 131:2-32 <- 319:0-30',
- 'preflight.css: 132:4-19 <- 320:2-17',
- 'preflight.css: 133:4-23 <- 321:2-21',
- 'preflight.css: 135:2-26 <- 328:0-24',
- 'preflight.css: 136:4-24 <- 329:2-22',
- 'preflight.css: 138:2-41 <- 336:0-39',
- 'preflight.css: 139:4-14 <- 337:2-12',
- 'preflight.css: 141:2-329 <- 340:0-348:39',
- 'preflight.css: 142:4-20 <- 349:2-18',
- 'preflight.css: 144:2-19 <- 356:0-17',
- 'preflight.css: 145:4-20 <- 357:2-18',
- 'preflight.css: 147:2-96 <- 364:0-366:23',
- 'preflight.css: 148:4-22 <- 367:2-20',
- 'preflight.css: 150:2-59 <- 374:0-375:28',
- 'preflight.css: 151:4-16 <- 376:2-14',
- 'preflight.css: 153:2-47 <- 383:0-45',
- 'preflight.css: 154:4-28 <- 384:2-26',
- 'index.css: 157:0-16 <- 5:0-42',
- 'input.css: 158:0-5 <- 3:0-5',
- 'input.css: 159:2-33 <- 4:9-18',
- ])
+ expect(visualized).toMatchInlineSnapshot(`
+ "
+ SOURCES
+ - index.css
+ - theme.css
+ - preflight.css
+ - input.css
+
+ VISUALIZATION
+ /* input: index.css */
+ @layer theme, base, components, utilities;
+ #1 -----------------------------------------
+
+ @import './theme.css' layer(theme);
+ #2 ----------------------------------
+ @import './preflight.css' layer(base);
+ #10 -------------------------------------
+ @import './utilities.css' layer(utilities);
+ #147 ------------------------------------------
+
+ /* input: theme.css */
+ @theme default {
+ #3 ---------------
+ --font-sans:
+ #4 ^
+ ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
+ #4 ^
+ 'Noto Color Emoji';
+ #5 ----------------------
+ --font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
+ --font-mono:
+ #6 ^
+ ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
+ #6 ^
+ monospace;
+ #7 -------------
+
+ --color-red-50: oklch(97.1% 0.013 17.38);
+ --color-red-100: oklch(93.6% 0.032 17.717);
+ --color-red-200: oklch(88.5% 0.062 18.334);
+ --color-red-300: oklch(80.8% 0.114 19.571);
+ --color-red-400: oklch(70.4% 0.191 22.216);
+ --color-red-500: oklch(63.7% 0.237 25.331);
+ --color-red-600: oklch(57.7% 0.245 27.325);
+ --color-red-700: oklch(50.5% 0.213 27.518);
+ --color-red-800: oklch(44.4% 0.177 26.899);
+ --color-red-900: oklch(39.6% 0.141 25.723);
+ --color-red-950: oklch(25.8% 0.092 26.042);
+
+ --color-orange-50: oklch(98% 0.016 73.684);
+ --color-orange-100: oklch(95.4% 0.038 75.164);
+ --color-orange-200: oklch(90.1% 0.076 70.697);
+ --color-orange-300: oklch(83.7% 0.128 66.29);
+ --color-orange-400: oklch(75% 0.183 55.934);
+ --color-orange-500: oklch(70.5% 0.213 47.604);
+ --color-orange-600: oklch(64.6% 0.222 41.116);
+ --color-orange-700: oklch(55.3% 0.195 38.402);
+ --color-orange-800: oklch(47% 0.157 37.304);
+ --color-orange-900: oklch(40.8% 0.123 38.172);
+ --color-orange-950: oklch(26.6% 0.079 36.259);
+
+ --color-amber-50: oklch(98.7% 0.022 95.277);
+ --color-amber-100: oklch(96.2% 0.059 95.617);
+ --color-amber-200: oklch(92.4% 0.12 95.746);
+ --color-amber-300: oklch(87.9% 0.169 91.605);
+ --color-amber-400: oklch(82.8% 0.189 84.429);
+ --color-amber-500: oklch(76.9% 0.188 70.08);
+ --color-amber-600: oklch(66.6% 0.179 58.318);
+ --color-amber-700: oklch(55.5% 0.163 48.998);
+ --color-amber-800: oklch(47.3% 0.137 46.201);
+ --color-amber-900: oklch(41.4% 0.112 45.904);
+ --color-amber-950: oklch(27.9% 0.077 45.635);
+
+ --color-yellow-50: oklch(98.7% 0.026 102.212);
+ --color-yellow-100: oklch(97.3% 0.071 103.193);
+ --color-yellow-200: oklch(94.5% 0.129 101.54);
+ --color-yellow-300: oklch(90.5% 0.182 98.111);
+ --color-yellow-400: oklch(85.2% 0.199 91.936);
+ --color-yellow-500: oklch(79.5% 0.184 86.047);
+ --color-yellow-600: oklch(68.1% 0.162 75.834);
+ --color-yellow-700: oklch(55.4% 0.135 66.442);
+ --color-yellow-800: oklch(47.6% 0.114 61.907);
+ --color-yellow-900: oklch(42.1% 0.095 57.708);
+ --color-yellow-950: oklch(28.6% 0.066 53.813);
+
+ --color-lime-50: oklch(98.6% 0.031 120.757);
+ --color-lime-100: oklch(96.7% 0.067 122.328);
+ --color-lime-200: oklch(93.8% 0.127 124.321);
+ --color-lime-300: oklch(89.7% 0.196 126.665);
+ --color-lime-400: oklch(84.1% 0.238 128.85);
+ --color-lime-500: oklch(76.8% 0.233 130.85);
+ --color-lime-600: oklch(64.8% 0.2 131.684);
+ --color-lime-700: oklch(53.2% 0.157 131.589);
+ --color-lime-800: oklch(45.3% 0.124 130.933);
+ --color-lime-900: oklch(40.5% 0.101 131.063);
+ --color-lime-950: oklch(27.4% 0.072 132.109);
+
+ --color-green-50: oklch(98.2% 0.018 155.826);
+ --color-green-100: oklch(96.2% 0.044 156.743);
+ --color-green-200: oklch(92.5% 0.084 155.995);
+ --color-green-300: oklch(87.1% 0.15 154.449);
+ --color-green-400: oklch(79.2% 0.209 151.711);
+ --color-green-500: oklch(72.3% 0.219 149.579);
+ --color-green-600: oklch(62.7% 0.194 149.214);
+ --color-green-700: oklch(52.7% 0.154 150.069);
+ --color-green-800: oklch(44.8% 0.119 151.328);
+ --color-green-900: oklch(39.3% 0.095 152.535);
+ --color-green-950: oklch(26.6% 0.065 152.934);
+
+ --color-emerald-50: oklch(97.9% 0.021 166.113);
+ --color-emerald-100: oklch(95% 0.052 163.051);
+ --color-emerald-200: oklch(90.5% 0.093 164.15);
+ --color-emerald-300: oklch(84.5% 0.143 164.978);
+ --color-emerald-400: oklch(76.5% 0.177 163.223);
+ --color-emerald-500: oklch(69.6% 0.17 162.48);
+ --color-emerald-600: oklch(59.6% 0.145 163.225);
+ --color-emerald-700: oklch(50.8% 0.118 165.612);
+ --color-emerald-800: oklch(43.2% 0.095 166.913);
+ --color-emerald-900: oklch(37.8% 0.077 168.94);
+ --color-emerald-950: oklch(26.2% 0.051 172.552);
+
+ --color-teal-50: oklch(98.4% 0.014 180.72);
+ --color-teal-100: oklch(95.3% 0.051 180.801);
+ --color-teal-200: oklch(91% 0.096 180.426);
+ --color-teal-300: oklch(85.5% 0.138 181.071);
+ --color-teal-400: oklch(77.7% 0.152 181.912);
+ --color-teal-500: oklch(70.4% 0.14 182.503);
+ --color-teal-600: oklch(60% 0.118 184.704);
+ --color-teal-700: oklch(51.1% 0.096 186.391);
+ --color-teal-800: oklch(43.7% 0.078 188.216);
+ --color-teal-900: oklch(38.6% 0.063 188.416);
+ --color-teal-950: oklch(27.7% 0.046 192.524);
+
+ --color-cyan-50: oklch(98.4% 0.019 200.873);
+ --color-cyan-100: oklch(95.6% 0.045 203.388);
+ --color-cyan-200: oklch(91.7% 0.08 205.041);
+ --color-cyan-300: oklch(86.5% 0.127 207.078);
+ --color-cyan-400: oklch(78.9% 0.154 211.53);
+ --color-cyan-500: oklch(71.5% 0.143 215.221);
+ --color-cyan-600: oklch(60.9% 0.126 221.723);
+ --color-cyan-700: oklch(52% 0.105 223.128);
+ --color-cyan-800: oklch(45% 0.085 224.283);
+ --color-cyan-900: oklch(39.8% 0.07 227.392);
+ --color-cyan-950: oklch(30.2% 0.056 229.695);
+
+ --color-sky-50: oklch(97.7% 0.013 236.62);
+ --color-sky-100: oklch(95.1% 0.026 236.824);
+ --color-sky-200: oklch(90.1% 0.058 230.902);
+ --color-sky-300: oklch(82.8% 0.111 230.318);
+ --color-sky-400: oklch(74.6% 0.16 232.661);
+ --color-sky-500: oklch(68.5% 0.169 237.323);
+ --color-sky-600: oklch(58.8% 0.158 241.966);
+ --color-sky-700: oklch(50% 0.134 242.749);
+ --color-sky-800: oklch(44.3% 0.11 240.79);
+ --color-sky-900: oklch(39.1% 0.09 240.876);
+ --color-sky-950: oklch(29.3% 0.066 243.157);
+
+ --color-blue-50: oklch(97% 0.014 254.604);
+ --color-blue-100: oklch(93.2% 0.032 255.585);
+ --color-blue-200: oklch(88.2% 0.059 254.128);
+ --color-blue-300: oklch(80.9% 0.105 251.813);
+ --color-blue-400: oklch(70.7% 0.165 254.624);
+ --color-blue-500: oklch(62.3% 0.214 259.815);
+ --color-blue-600: oklch(54.6% 0.245 262.881);
+ --color-blue-700: oklch(48.8% 0.243 264.376);
+ --color-blue-800: oklch(42.4% 0.199 265.638);
+ --color-blue-900: oklch(37.9% 0.146 265.522);
+ --color-blue-950: oklch(28.2% 0.091 267.935);
+
+ --color-indigo-50: oklch(96.2% 0.018 272.314);
+ --color-indigo-100: oklch(93% 0.034 272.788);
+ --color-indigo-200: oklch(87% 0.065 274.039);
+ --color-indigo-300: oklch(78.5% 0.115 274.713);
+ --color-indigo-400: oklch(67.3% 0.182 276.935);
+ --color-indigo-500: oklch(58.5% 0.233 277.117);
+ --color-indigo-600: oklch(51.1% 0.262 276.966);
+ --color-indigo-700: oklch(45.7% 0.24 277.023);
+ --color-indigo-800: oklch(39.8% 0.195 277.366);
+ --color-indigo-900: oklch(35.9% 0.144 278.697);
+ --color-indigo-950: oklch(25.7% 0.09 281.288);
+
+ --color-violet-50: oklch(96.9% 0.016 293.756);
+ --color-violet-100: oklch(94.3% 0.029 294.588);
+ --color-violet-200: oklch(89.4% 0.057 293.283);
+ --color-violet-300: oklch(81.1% 0.111 293.571);
+ --color-violet-400: oklch(70.2% 0.183 293.541);
+ --color-violet-500: oklch(60.6% 0.25 292.717);
+ --color-violet-600: oklch(54.1% 0.281 293.009);
+ --color-violet-700: oklch(49.1% 0.27 292.581);
+ --color-violet-800: oklch(43.2% 0.232 292.759);
+ --color-violet-900: oklch(38% 0.189 293.745);
+ --color-violet-950: oklch(28.3% 0.141 291.089);
+
+ --color-purple-50: oklch(97.7% 0.014 308.299);
+ --color-purple-100: oklch(94.6% 0.033 307.174);
+ --color-purple-200: oklch(90.2% 0.063 306.703);
+ --color-purple-300: oklch(82.7% 0.119 306.383);
+ --color-purple-400: oklch(71.4% 0.203 305.504);
+ --color-purple-500: oklch(62.7% 0.265 303.9);
+ --color-purple-600: oklch(55.8% 0.288 302.321);
+ --color-purple-700: oklch(49.6% 0.265 301.924);
+ --color-purple-800: oklch(43.8% 0.218 303.724);
+ --color-purple-900: oklch(38.1% 0.176 304.987);
+ --color-purple-950: oklch(29.1% 0.149 302.717);
+
+ --color-fuchsia-50: oklch(97.7% 0.017 320.058);
+ --color-fuchsia-100: oklch(95.2% 0.037 318.852);
+ --color-fuchsia-200: oklch(90.3% 0.076 319.62);
+ --color-fuchsia-300: oklch(83.3% 0.145 321.434);
+ --color-fuchsia-400: oklch(74% 0.238 322.16);
+ --color-fuchsia-500: oklch(66.7% 0.295 322.15);
+ --color-fuchsia-600: oklch(59.1% 0.293 322.896);
+ --color-fuchsia-700: oklch(51.8% 0.253 323.949);
+ --color-fuchsia-800: oklch(45.2% 0.211 324.591);
+ --color-fuchsia-900: oklch(40.1% 0.17 325.612);
+ --color-fuchsia-950: oklch(29.3% 0.136 325.661);
+
+ --color-pink-50: oklch(97.1% 0.014 343.198);
+ --color-pink-100: oklch(94.8% 0.028 342.258);
+ --color-pink-200: oklch(89.9% 0.061 343.231);
+ --color-pink-300: oklch(82.3% 0.12 346.018);
+ --color-pink-400: oklch(71.8% 0.202 349.761);
+ --color-pink-500: oklch(65.6% 0.241 354.308);
+ --color-pink-600: oklch(59.2% 0.249 0.584);
+ --color-pink-700: oklch(52.5% 0.223 3.958);
+ --color-pink-800: oklch(45.9% 0.187 3.815);
+ --color-pink-900: oklch(40.8% 0.153 2.432);
+ --color-pink-950: oklch(28.4% 0.109 3.907);
+
+ --color-rose-50: oklch(96.9% 0.015 12.422);
+ --color-rose-100: oklch(94.1% 0.03 12.58);
+ --color-rose-200: oklch(89.2% 0.058 10.001);
+ --color-rose-300: oklch(81% 0.117 11.638);
+ --color-rose-400: oklch(71.2% 0.194 13.428);
+ --color-rose-500: oklch(64.5% 0.246 16.439);
+ --color-rose-600: oklch(58.6% 0.253 17.585);
+ --color-rose-700: oklch(51.4% 0.222 16.935);
+ --color-rose-800: oklch(45.5% 0.188 13.697);
+ --color-rose-900: oklch(41% 0.159 10.272);
+ --color-rose-950: oklch(27.1% 0.105 12.094);
+
+ --color-slate-50: oklch(98.4% 0.003 247.858);
+ --color-slate-100: oklch(96.8% 0.007 247.896);
+ --color-slate-200: oklch(92.9% 0.013 255.508);
+ --color-slate-300: oklch(86.9% 0.022 252.894);
+ --color-slate-400: oklch(70.4% 0.04 256.788);
+ --color-slate-500: oklch(55.4% 0.046 257.417);
+ --color-slate-600: oklch(44.6% 0.043 257.281);
+ --color-slate-700: oklch(37.2% 0.044 257.287);
+ --color-slate-800: oklch(27.9% 0.041 260.031);
+ --color-slate-900: oklch(20.8% 0.042 265.755);
+ --color-slate-950: oklch(12.9% 0.042 264.695);
+
+ --color-gray-50: oklch(98.5% 0.002 247.839);
+ --color-gray-100: oklch(96.7% 0.003 264.542);
+ --color-gray-200: oklch(92.8% 0.006 264.531);
+ --color-gray-300: oklch(87.2% 0.01 258.338);
+ --color-gray-400: oklch(70.7% 0.022 261.325);
+ --color-gray-500: oklch(55.1% 0.027 264.364);
+ --color-gray-600: oklch(44.6% 0.03 256.802);
+ --color-gray-700: oklch(37.3% 0.034 259.733);
+ --color-gray-800: oklch(27.8% 0.033 256.848);
+ --color-gray-900: oklch(21% 0.034 264.665);
+ --color-gray-950: oklch(13% 0.028 261.692);
+
+ --color-zinc-50: oklch(98.5% 0 0);
+ --color-zinc-100: oklch(96.7% 0.001 286.375);
+ --color-zinc-200: oklch(92% 0.004 286.32);
+ --color-zinc-300: oklch(87.1% 0.006 286.286);
+ --color-zinc-400: oklch(70.5% 0.015 286.067);
+ --color-zinc-500: oklch(55.2% 0.016 285.938);
+ --color-zinc-600: oklch(44.2% 0.017 285.786);
+ --color-zinc-700: oklch(37% 0.013 285.805);
+ --color-zinc-800: oklch(27.4% 0.006 286.033);
+ --color-zinc-900: oklch(21% 0.006 285.885);
+ --color-zinc-950: oklch(14.1% 0.005 285.823);
+
+ --color-neutral-50: oklch(98.5% 0 0);
+ --color-neutral-100: oklch(97% 0 0);
+ --color-neutral-200: oklch(92.2% 0 0);
+ --color-neutral-300: oklch(87% 0 0);
+ --color-neutral-400: oklch(70.8% 0 0);
+ --color-neutral-500: oklch(55.6% 0 0);
+ --color-neutral-600: oklch(43.9% 0 0);
+ --color-neutral-700: oklch(37.1% 0 0);
+ --color-neutral-800: oklch(26.9% 0 0);
+ --color-neutral-900: oklch(20.5% 0 0);
+ --color-neutral-950: oklch(14.5% 0 0);
+
+ --color-stone-50: oklch(98.5% 0.001 106.423);
+ --color-stone-100: oklch(97% 0.001 106.424);
+ --color-stone-200: oklch(92.3% 0.003 48.717);
+ --color-stone-300: oklch(86.9% 0.005 56.366);
+ --color-stone-400: oklch(70.9% 0.01 56.259);
+ --color-stone-500: oklch(55.3% 0.013 58.071);
+ --color-stone-600: oklch(44.4% 0.011 73.639);
+ --color-stone-700: oklch(37.4% 0.01 67.558);
+ --color-stone-800: oklch(26.8% 0.007 34.298);
+ --color-stone-900: oklch(21.6% 0.006 56.043);
+ --color-stone-950: oklch(14.7% 0.004 49.25);
+
+ --color-black: #000;
+ --color-white: #fff;
+
+ --spacing: 0.25rem;
+
+ --breakpoint-sm: 40rem;
+ --breakpoint-md: 48rem;
+ --breakpoint-lg: 64rem;
+ --breakpoint-xl: 80rem;
+ --breakpoint-2xl: 96rem;
+
+ --container-3xs: 16rem;
+ --container-2xs: 18rem;
+ --container-xs: 20rem;
+ --container-sm: 24rem;
+ --container-md: 28rem;
+ --container-lg: 32rem;
+ --container-xl: 36rem;
+ --container-2xl: 42rem;
+ --container-3xl: 48rem;
+ --container-4xl: 56rem;
+ --container-5xl: 64rem;
+ --container-6xl: 72rem;
+ --container-7xl: 80rem;
+
+ --text-xs: 0.75rem;
+ --text-xs--line-height: calc(1 / 0.75);
+ --text-sm: 0.875rem;
+ --text-sm--line-height: calc(1.25 / 0.875);
+ --text-base: 1rem;
+ --text-base--line-height: calc(1.5 / 1);
+ --text-lg: 1.125rem;
+ --text-lg--line-height: calc(1.75 / 1.125);
+ --text-xl: 1.25rem;
+ --text-xl--line-height: calc(1.75 / 1.25);
+ --text-2xl: 1.5rem;
+ --text-2xl--line-height: calc(2 / 1.5);
+ --text-3xl: 1.875rem;
+ --text-3xl--line-height: calc(2.25 / 1.875);
+ --text-4xl: 2.25rem;
+ --text-4xl--line-height: calc(2.5 / 2.25);
+ --text-5xl: 3rem;
+ --text-5xl--line-height: 1;
+ --text-6xl: 3.75rem;
+ --text-6xl--line-height: 1;
+ --text-7xl: 4.5rem;
+ --text-7xl--line-height: 1;
+ --text-8xl: 6rem;
+ --text-8xl--line-height: 1;
+ --text-9xl: 8rem;
+ --text-9xl--line-height: 1;
+
+ --font-weight-thin: 100;
+ --font-weight-extralight: 200;
+ --font-weight-light: 300;
+ --font-weight-normal: 400;
+ --font-weight-medium: 500;
+ --font-weight-semibold: 600;
+ --font-weight-bold: 700;
+ --font-weight-extrabold: 800;
+ --font-weight-black: 900;
+
+ --tracking-tighter: -0.05em;
+ --tracking-tight: -0.025em;
+ --tracking-normal: 0em;
+ --tracking-wide: 0.025em;
+ --tracking-wider: 0.05em;
+ --tracking-widest: 0.1em;
+
+ --leading-tight: 1.25;
+ --leading-snug: 1.375;
+ --leading-normal: 1.5;
+ --leading-relaxed: 1.625;
+ --leading-loose: 2;
+
+ --radius-xs: 0.125rem;
+ --radius-sm: 0.25rem;
+ --radius-md: 0.375rem;
+ --radius-lg: 0.5rem;
+ --radius-xl: 0.75rem;
+ --radius-2xl: 1rem;
+ --radius-3xl: 1.5rem;
+ --radius-4xl: 2rem;
+
+ --shadow-2xs: 0 1px rgb(0 0 0 / 0.05);
+ --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05);
+ --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
+ --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
+ --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
+
+ --inset-shadow-2xs: inset 0 1px rgb(0 0 0 / 0.05);
+ --inset-shadow-xs: inset 0 1px 1px rgb(0 0 0 / 0.05);
+ --inset-shadow-sm: inset 0 2px 4px rgb(0 0 0 / 0.05);
+
+ --drop-shadow-xs: 0 1px 1px rgb(0 0 0 / 0.05);
+ --drop-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.15);
+ --drop-shadow-md: 0 3px 3px rgb(0 0 0 / 0.12);
+ --drop-shadow-lg: 0 4px 4px rgb(0 0 0 / 0.15);
+ --drop-shadow-xl: 0 9px 7px rgb(0 0 0 / 0.1);
+ --drop-shadow-2xl: 0 25px 25px rgb(0 0 0 / 0.15);
+
+ --text-shadow-2xs: 0px 1px 0px rgb(0 0 0 / 0.15);
+ --text-shadow-xs: 0px 1px 1px rgb(0 0 0 / 0.2);
+ --text-shadow-sm:
+ 0px 1px 0px rgb(0 0 0 / 0.075), 0px 1px 1px rgb(0 0 0 / 0.075), 0px 2px 2px rgb(0 0 0 / 0.075);
+ --text-shadow-md:
+ 0px 1px 1px rgb(0 0 0 / 0.1), 0px 1px 2px rgb(0 0 0 / 0.1), 0px 2px 4px rgb(0 0 0 / 0.1);
+ --text-shadow-lg:
+ 0px 1px 2px rgb(0 0 0 / 0.1), 0px 3px 2px rgb(0 0 0 / 0.1), 0px 4px 8px rgb(0 0 0 / 0.1);
+
+ --ease-in: cubic-bezier(0.4, 0, 1, 1);
+ --ease-out: cubic-bezier(0, 0, 0.2, 1);
+ --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
+
+ --animate-spin: spin 1s linear infinite;
+ --animate-ping: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+ --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+ --animate-bounce: bounce 1s infinite;
+
+ @keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+ }
+
+ @keyframes ping {
+ 75%,
+ 100% {
+ transform: scale(2);
+ opacity: 0;
+ }
+ }
+
+ @keyframes pulse {
+ 50% {
+ opacity: 0.5;
+ }
+ }
+
+ @keyframes bounce {
+ 0%,
+ 100% {
+ transform: translateY(-25%);
+ animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
+ }
+
+ 50% {
+ transform: none;
+ animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
+ }
+ }
+
+ --blur-xs: 4px;
+ --blur-sm: 8px;
+ --blur-md: 12px;
+ --blur-lg: 16px;
+ --blur-xl: 24px;
+ --blur-2xl: 40px;
+ --blur-3xl: 64px;
+
+ --perspective-dramatic: 100px;
+ --perspective-near: 300px;
+ --perspective-normal: 500px;
+ --perspective-midrange: 800px;
+ --perspective-distant: 1200px;
+
+ --aspect-video: 16 / 9;
+
+ --default-transition-duration: 150ms;
+ --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ --default-font-family: --theme(--font-sans, initial);
+ #8 ----------------------------------------------------
+ --default-font-feature-settings: --theme(--font-sans--font-feature-settings, initial);
+ --default-font-variation-settings: --theme(--font-sans--font-variation-settings, initial);
+ --default-mono-font-family: --theme(--font-mono, initial);
+ #9 ---------------------------------------------------------
+ --default-mono-font-feature-settings: --theme(--font-mono--font-feature-settings, initial);
+ --default-mono-font-variation-settings: --theme(--font-mono--font-variation-settings, initial);
+ }
+
+ /* Deprecated */
+ @theme default inline reference {
+ --blur: 8px;
+ --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
+ --shadow-inner: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);
+ --drop-shadow: 0 1px 2px rgb(0 0 0 / 0.1), 0 1px 1px rgb(0 0 0 / 0.06);
+ --radius: 0.25rem;
+ --max-width-prose: 65ch;
+ }
+
+ /* input: preflight.css */
+ /*
+ 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
+ 2. Remove default margins and padding
+ 3. Reset all borders.
+ */
+
+ *,
+ #11 ^
+ ::after,
+ #11 ^
+ ::before,
+ #12 ^
+ ::backdrop,
+ #12 ^
+ ::file-selector-button {
+ #13 -----------------------
+ box-sizing: border-box; /* 1 */
+ #14 ----------------------
+ margin: 0; /* 2 */
+ #15 ---------
+ padding: 0; /* 2 */
+ #16 ----------
+ border: 0 solid; /* 3 */
+ #17 ---------------
+ }
+
+ /*
+ 1. Use a consistent sensible line-height in all browsers.
+ 2. Prevent adjustments of font size after orientation changes in iOS.
+ 3. Use a more readable tab size.
+ 4. Use the user's configured \`sans\` font-family by default.
+ 5. Use the user's configured \`sans\` font-feature-settings by default.
+ 6. Use the user's configured \`sans\` font-variation-settings by default.
+ 7. Disable tap highlights on iOS.
+ */
+
+ html,
+ #18 ^
+ :host {
+ #18 ------
+ line-height: 1.5; /* 1 */
+ #19 ----------------
+ -webkit-text-size-adjust: 100%; /* 2 */
+ #20 ------------------------------
+ tab-size: 4; /* 3 */
+ #21 -----------
+ font-family: --theme(
+ #22 ^
+ --default-font-family,
+ #23 ^
+ ui-sans-serif,
+ #23 ^
+ system-ui,
+ #24 ^
+ sans-serif,
+ #24 ^
+ 'Apple Color Emoji',
+ #25 ^
+ 'Segoe UI Emoji',
+ #25 ^
+ 'Segoe UI Symbol',
+ #26 ^
+ 'Noto Color Emoji'
+ #26 ^
+ ); /* 4 */
+ #27 ---
+ font-feature-settings: --theme(--default-font-feature-settings, normal); /* 5 */
+ #28 -----------------------------------------------------------------------
+ font-variation-settings: --theme(--default-font-variation-settings, normal); /* 6 */
+ #29 ---------------------------------------------------------------------------
+ -webkit-tap-highlight-color: transparent; /* 7 */
+ #30 ----------------------------------------
+ }
+
+ /*
+ 1. Add the correct height in Firefox.
+ 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
+ 3. Reset the default border style to a 1px solid border.
+ */
+
+ hr {
+ #31 ---
+ height: 0; /* 1 */
+ #32 ---------
+ color: inherit; /* 2 */
+ #33 --------------
+ border-top-width: 1px; /* 3 */
+ #34 ---------------------
+ }
+
+ /*
+ Add the correct text decoration in Chrome, Edge, and Safari.
+ */
+
+ abbr:where([title]) {
+ #35 --------------------
+ -webkit-text-decoration: underline dotted;
+ #36 -----------------------------------------
+ text-decoration: underline dotted;
+ #37 ---------------------------------
+ }
+
+ /*
+ Remove the default font size and weight for headings.
+ */
+
+ h1,
+ #38 ^
+ h2,
+ #38 ^
+ h3,
+ #39 ^
+ h4,
+ #39 ^
+ h5,
+ #40 ^
+ h6 {
+ #40 ---
+ font-size: inherit;
+ #41 ------------------
+ font-weight: inherit;
+ #42 --------------------
+ }
+
+ /*
+ Reset links to optimize for opt-in styling instead of opt-out.
+ */
+
+ a {
+ #43 --
+ color: inherit;
+ #44 --------------
+ -webkit-text-decoration: inherit;
+ #45 --------------------------------
+ text-decoration: inherit;
+ #46 ------------------------
+ }
+
+ /*
+ Add the correct font weight in Edge and Safari.
+ */
+
+ b,
+ #47 ^
+ strong {
+ #48 -------
+ font-weight: bolder;
+ #49 -------------------
+ }
+
+ /*
+ 1. Use the user's configured \`mono\` font-family by default.
+ 2. Use the user's configured \`mono\` font-feature-settings by default.
+ 3. Use the user's configured \`mono\` font-variation-settings by default.
+ 4. Correct the odd \`em\` font sizing in all browsers.
+ */
+
+ code,
+ #50 ^
+ kbd,
+ #50 ^
+ samp,
+ #51 ^
+ pre {
+ #51 ----
+ font-family: --theme(
+ #52 ^
+ --default-mono-font-family,
+ #53 ^
+ ui-monospace,
+ #53 ^
+ SFMono-Regular,
+ #54 ^
+ Menlo,
+ #54 ^
+ Monaco,
+ #55 ^
+ Consolas,
+ #55 ^
+ 'Liberation Mono',
+ #56 ^
+ 'Courier New',
+ #56 ^
+ monospace
+ #57 ^
+ ); /* 1 */
+ #57 ---
+ font-feature-settings: --theme(--default-mono-font-feature-settings, normal); /* 2 */
+ #58 ----------------------------------------------------------------------------
+ font-variation-settings: --theme(--default-mono-font-variation-settings, normal); /* 3 */
+ #59 --------------------------------------------------------------------------------
+ font-size: 1em; /* 4 */
+ #60 --------------
+ }
+
+ /*
+ Add the correct font size in all browsers.
+ */
+
+ small {
+ #61 ------
+ font-size: 80%;
+ #62 --------------
+ }
+
+ /*
+ Prevent \`sub\` and \`sup\` elements from affecting the line height in all browsers.
+ */
+
+ sub,
+ #63 ^
+ sup {
+ #64 ----
+ font-size: 75%;
+ #65 --------------
+ line-height: 0;
+ #66 --------------
+ position: relative;
+ #67 ------------------
+ vertical-align: baseline;
+ #68 ------------------------
+ }
+
+ sub {
+ #69 ----
+ bottom: -0.25em;
+ #70 ---------------
+ }
+
+ sup {
+ #71 ----
+ top: -0.5em;
+ #72 -----------
+ }
+
+ /*
+ 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
+ 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
+ 3. Remove gaps between table borders by default.
+ */
+
+ table {
+ #73 ------
+ text-indent: 0; /* 1 */
+ #74 --------------
+ border-color: inherit; /* 2 */
+ #75 ---------------------
+ border-collapse: collapse; /* 3 */
+ #76 -------------------------
+ }
+
+ /*
+ Use the modern Firefox focus style for all focusable elements.
+ */
+
+ :-moz-focusring {
+ #77 ----------------
+ outline: auto;
+ #78 -------------
+ }
+
+ /*
+ Add the correct vertical alignment in Chrome and Firefox.
+ */
+
+ progress {
+ #79 ---------
+ vertical-align: baseline;
+ #80 ------------------------
+ }
+
+ /*
+ Add the correct display in Chrome and Safari.
+ */
+
+ summary {
+ #81 --------
+ display: list-item;
+ #82 ------------------
+ }
+
+ /*
+ Make lists unstyled by default.
+ */
+
+ ol,
+ #83 ^
+ ul,
+ #83 ^
+ menu {
+ #84 -----
+ list-style: none;
+ #85 ----------------
+ }
+
+ /*
+ 1. Make replaced elements \`display: block\` by default. (https://github.com/mozdevs/cssremedy/issues/14)
+ 2. Add \`vertical-align: middle\` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
+ This can trigger a poorly considered lint error in some tools but is included by design.
+ */
+
+ img,
+ #86 ^
+ svg,
+ #86 ^
+ video,
+ #87 ^
+ canvas,
+ #87 ^
+ audio,
+ #88 ^
+ iframe,
+ #88 ^
+ embed,
+ #89 ^
+ object {
+ #89 -------
+ display: block; /* 1 */
+ #90 --------------
+ vertical-align: middle; /* 2 */
+ #91 ----------------------
+ }
+
+ /*
+ Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
+ */
+
+ img,
+ #92 ^
+ video {
+ #93 ------
+ max-width: 100%;
+ #94 ---------------
+ height: auto;
+ #95 ------------
+ }
+
+ /*
+ 1. Inherit font styles in all browsers.
+ 2. Remove border radius in all browsers.
+ 3. Remove background color in all browsers.
+ 4. Ensure consistent opacity for disabled states in all browsers.
+ */
+
+ button,
+ #96 ^
+ input,
+ #96 ^
+ select,
+ #97 ^
+ optgroup,
+ #97 ^
+ textarea,
+ #98 ^
+ ::file-selector-button {
+ #98 -----------------------
+ font: inherit; /* 1 */
+ #99 -------------
+ font-feature-settings: inherit; /* 1 */
+ #100 ------------------------------
+ font-variation-settings: inherit; /* 1 */
+ #101 --------------------------------
+ letter-spacing: inherit; /* 1 */
+ #102 -----------------------
+ color: inherit; /* 1 */
+ #103 --------------
+ border-radius: 0; /* 2 */
+ #104 ----------------
+ background-color: transparent; /* 3 */
+ #105 -----------------------------
+ opacity: 1; /* 4 */
+ #106 ----------
+ }
+
+ /*
+ Restore default font weight.
+ */
+
+ :where(select:is([multiple], [size])) optgroup {
+ #107 -----------------------------------------------
+ font-weight: bolder;
+ #108 -------------------
+ }
+
+ /*
+ Restore indentation.
+ */
+
+ :where(select:is([multiple], [size])) optgroup option {
+ #109 ------------------------------------------------------
+ padding-inline-start: 20px;
+ #110 --------------------------
+ }
+
+ /*
+ Restore space after button.
+ */
+
+ ::file-selector-button {
+ #111 -----------------------
+ margin-inline-end: 4px;
+ #112 ----------------------
+ }
+
+ /*
+ Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
+ */
+
+ ::placeholder {
+ #113 --------------
+ opacity: 1;
+ #114 ----------
+ }
+
+ /*
+ Set the default placeholder color to a semi-transparent version of the current text color in browsers that do not
+ crash when using \`color-mix(…)\` with \`currentcolor\`. (https://github.com/tailwindlabs/tailwindcss/issues/17194)
+ */
+
+ @supports (not (-webkit-appearance: -apple-pay-button)) /* Not Safari */ or
+ #115 ^
+ (contain-intrinsic-size: 1px) /* Safari 17+ */ {
+ #116 -------------------------------------------------
+ ::placeholder {
+ #117 --------------
+ color: color-mix(in oklab, currentcolor 50%, transparent);
+ #118 ---------------------------------------------------------
+ #119 ---------------------------------------------------------
+ #120 ---------------------------------------------------------
+ }
+ }
+
+ /*
+ Prevent resizing textareas horizontally by default.
+ */
+
+ textarea {
+ #121 ---------
+ resize: vertical;
+ #122 ----------------
+ }
+
+ /*
+ Remove the inner padding in Chrome and Safari on macOS.
+ */
+
+ ::-webkit-search-decoration {
+ #123 ----------------------------
+ -webkit-appearance: none;
+ #124 ------------------------
+ }
+
+ /*
+ 1. Ensure date/time inputs have the same height when empty in iOS Safari.
+ 2. Ensure text alignment can be changed on date/time inputs in iOS Safari.
+ */
+
+ ::-webkit-date-and-time-value {
+ #125 ------------------------------
+ min-height: 1lh; /* 1 */
+ #126 ---------------
+ text-align: inherit; /* 2 */
+ #127 -------------------
+ }
+
+ /*
+ Prevent height from changing on date/time inputs in macOS Safari when the input is set to \`display: block\`.
+ */
+
+ ::-webkit-datetime-edit {
+ #128 ------------------------
+ display: inline-flex;
+ #129 --------------------
+ }
+
+ /*
+ Remove excess padding from pseudo-elements in date/time inputs to ensure consistent height across browsers.
+ */
+
+ ::-webkit-datetime-edit-fields-wrapper {
+ #130 ---------------------------------------
+ padding: 0;
+ #131 ----------
+ }
+
+ ::-webkit-datetime-edit,
+ #132 ^
+ ::-webkit-datetime-edit-year-field,
+ #132 ^
+ ::-webkit-datetime-edit-month-field,
+ #133 ^
+ ::-webkit-datetime-edit-day-field,
+ #133 ^
+ ::-webkit-datetime-edit-hour-field,
+ #134 ^
+ ::-webkit-datetime-edit-minute-field,
+ #134 ^
+ ::-webkit-datetime-edit-second-field,
+ #135 ^
+ ::-webkit-datetime-edit-millisecond-field,
+ #135 ^
+ ::-webkit-datetime-edit-meridiem-field {
+ #136 ---------------------------------------
+ padding-block: 0;
+ #137 ----------------
+ }
+
+ /*
+ Remove the additional \`:invalid\` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
+ */
+
+ :-moz-ui-invalid {
+ #138 -----------------
+ box-shadow: none;
+ #139 ----------------
+ }
+
+ /*
+ Correct the inability to style the border radius in iOS Safari.
+ */
+
+ button,
+ #140 ^
+ input:where([type='button'], [type='reset'], [type='submit']),
+ #140 ^
+ ::file-selector-button {
+ #141 -----------------------
+ appearance: button;
+ #142 ------------------
+ }
+
+ /*
+ Correct the cursor style of increment and decrement buttons in Safari.
+ */
+
+ ::-webkit-inner-spin-button,
+ #143 ^
+ ::-webkit-outer-spin-button {
+ #143 ----------------------------
+ height: auto;
+ #144 ------------
+ }
+
+ /*
+ Make elements with the HTML hidden attribute stay hidden by default.
+ */
+
+ [hidden]:where(:not([hidden='until-found'])) {
+ #145 ---------------------------------------------
+ display: none !important;
+ #146 ------------------------
+ }
+
+ /* input: input.css */
+ @import 'tailwindcss';
+
+ .foo {
+ #148 -----
+ @apply underline;
+ #149 ---------
+ }
+ /* output */
+ @layer theme, base, components, utilities;
+ #1 -----------------------------------------
+ @layer theme {
+ #2 -------------
+ :root, :host {
+ #3 -------------
+ --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
+ #4
+ #5 ^
+ 'Noto Color Emoji';
+ #5 ^
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
+ #6
+ #7 ^
+ monospace;
+ #7 ^
+ --default-font-family: var(--font-sans);
+ #8 ---------------------------------------
+ --default-mono-font-family: var(--font-mono);
+ #9 --------------------------------------------
+ }
+ }
+ @layer base {
+ #10 ------------
+ *, ::after, ::before, ::backdrop, ::file-selector-button {
+ #11
+ #12
+ #13 ---------------------------------------------------------
+ box-sizing: border-box;
+ #14 ----------------------
+ margin: 0;
+ #15 ---------
+ padding: 0;
+ #16 ----------
+ border: 0 solid;
+ #17 ---------------
+ }
+ html, :host {
+ #18
+ #19 ^
+ line-height: 1.5;
+ #19 ----------------
+ -webkit-text-size-adjust: 100%;
+ #20 ------------------------------
+ tab-size: 4;
+ #21 -----------
+ font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji');
+ #22
+ #23
+ #24
+ #25
+ #26
+ #27 ^
+ font-feature-settings: var(--default-font-feature-settings, normal);
+ #28 -------------------------------------------------------------------
+ font-variation-settings: var(--default-font-variation-settings, normal);
+ #29 -----------------------------------------------------------------------
+ -webkit-tap-highlight-color: transparent;
+ #30 ----------------------------------------
+ }
+ hr {
+ #31 ---
+ height: 0;
+ #32 ---------
+ color: inherit;
+ #33 --------------
+ border-top-width: 1px;
+ #34 ---------------------
+ }
+ abbr:where([title]) {
+ #35 --------------------
+ -webkit-text-decoration: underline dotted;
+ #36 -----------------------------------------
+ text-decoration: underline dotted;
+ #37 ---------------------------------
+ }
+ h1, h2, h3, h4, h5, h6 {
+ #38
+ #39
+ #40
+ #41 ^
+ font-size: inherit;
+ #41 ------------------
+ font-weight: inherit;
+ #42 --------------------
+ }
+ a {
+ #43 --
+ color: inherit;
+ #44 --------------
+ -webkit-text-decoration: inherit;
+ #45 --------------------------------
+ text-decoration: inherit;
+ #46 ------------------------
+ }
+ b, strong {
+ #47
+ #48 ^
+ font-weight: bolder;
+ #49 -------------------
+ }
+ code, kbd, samp, pre {
+ #50
+ #51
+ #52 ^
+ font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace);
+ #52
+ #53
+ #54
+ #55
+ #56
+ #57 ------------------------------------------------------------------------------------------------------------------------------------------------
+ font-feature-settings: var(--default-mono-font-feature-settings, normal);
+ #58 ------------------------------------------------------------------------
+ font-variation-settings: var(--default-mono-font-variation-settings, normal);
+ #59 ----------------------------------------------------------------------------
+ font-size: 1em;
+ #60 --------------
+ }
+ small {
+ #61 ------
+ font-size: 80%;
+ #62 --------------
+ }
+ sub, sup {
+ #63
+ #64 ^
+ font-size: 75%;
+ #65 --------------
+ line-height: 0;
+ #66 --------------
+ position: relative;
+ #67 ------------------
+ vertical-align: baseline;
+ #68 ------------------------
+ }
+ sub {
+ #69 ----
+ bottom: -0.25em;
+ #70 ---------------
+ }
+ sup {
+ #71 ----
+ top: -0.5em;
+ #72 -----------
+ }
+ table {
+ #73 ------
+ text-indent: 0;
+ #74 --------------
+ border-color: inherit;
+ #75 ---------------------
+ border-collapse: collapse;
+ #76 -------------------------
+ }
+ :-moz-focusring {
+ #77 ----------------
+ outline: auto;
+ #78 -------------
+ }
+ progress {
+ #79 ---------
+ vertical-align: baseline;
+ #80 ------------------------
+ }
+ summary {
+ #81 --------
+ display: list-item;
+ #82 ------------------
+ }
+ ol, ul, menu {
+ #83
+ #84 -------------
+ list-style: none;
+ #85 ----------------
+ }
+ img, svg, video, canvas, audio, iframe, embed, object {
+ #86
+ #87
+ #88
+ #89
+ #90 ^
+ display: block;
+ #90 --------------
+ vertical-align: middle;
+ #91 ----------------------
+ }
+ img, video {
+ #92
+ #93 ^
+ max-width: 100%;
+ #94 ---------------
+ height: auto;
+ #95 ------------
+ }
+ button, input, select, optgroup, textarea, ::file-selector-button {
+ #96
+ #97
+ #98
+ #99 ^
+ font: inherit;
+ #99 -------------
+ font-feature-settings: inherit;
+ #100 ------------------------------
+ font-variation-settings: inherit;
+ #101 --------------------------------
+ letter-spacing: inherit;
+ #102 -----------------------
+ color: inherit;
+ #103 --------------
+ border-radius: 0;
+ #104 ----------------
+ background-color: transparent;
+ #105 -----------------------------
+ opacity: 1;
+ #106 ----------
+ }
+ :where(select:is([multiple], [size])) optgroup {
+ #107 -----------------------------------------------
+ font-weight: bolder;
+ #108 -------------------
+ }
+ :where(select:is([multiple], [size])) optgroup option {
+ #109 ------------------------------------------------------
+ padding-inline-start: 20px;
+ #110 --------------------------
+ }
+ ::file-selector-button {
+ #111 -----------------------
+ margin-inline-end: 4px;
+ #112 ----------------------
+ }
+ ::placeholder {
+ #113 --------------
+ opacity: 1;
+ #114 ----------
+ }
+ @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
+ #115
+ #116 ^
+ ::placeholder {
+ #117 --------------
+ color: currentcolor;
+ #118 -------------------
+ @supports (color: color-mix(in lab, red, red)) {
+ #119 -----------------------------------------------
+ color: color-mix(in oklab, currentcolor 50%, transparent);
+ #120 ---------------------------------------------------------
+ }
+ }
+ }
+ textarea {
+ #121 ---------
+ resize: vertical;
+ #122 ----------------
+ }
+ ::-webkit-search-decoration {
+ #123 ----------------------------
+ -webkit-appearance: none;
+ #124 ------------------------
+ }
+ ::-webkit-date-and-time-value {
+ #125 ------------------------------
+ min-height: 1lh;
+ #126 ---------------
+ text-align: inherit;
+ #127 -------------------
+ }
+ ::-webkit-datetime-edit {
+ #128 ------------------------
+ display: inline-flex;
+ #129 --------------------
+ }
+ ::-webkit-datetime-edit-fields-wrapper {
+ #130 ---------------------------------------
+ padding: 0;
+ #131 ----------
+ }
+ ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
+ #132
+ #133
+ #134
+ #135
+ #136 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ padding-block: 0;
+ #137 ----------------
+ }
+ :-moz-ui-invalid {
+ #138 -----------------
+ box-shadow: none;
+ #139 ----------------
+ }
+ button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button {
+ #140
+ #141 ----------------------------------------------------------------------------------------------
+ appearance: button;
+ #142 ------------------
+ }
+ ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
+ #143
+ #144 ^
+ height: auto;
+ #144 ------------
+ }
+ [hidden]:where(:not([hidden='until-found'])) {
+ #145 ---------------------------------------------
+ display: none !important;
+ #146 ------------------------
+ }
+ }
+ @layer utilities;
+ #147 ----------------
+ .foo {
+ #148 -----
+ text-decoration-line: underline;
+ #149 -------------------------------
+ }
+
+ "
+ `)
})
-test('source maps are generated for utilities', async ({ expect }) => {
- let {
- sources,
- css: output,
- annotations,
- } = await run({
+test('source maps are generated for utilities', async () => {
+ let visualized = await run({
input: css`
@import './utilities.css';
@plugin "./plugin.js";
@@ -318,39 +1456,51 @@ test('source maps are generated for utilities', async ({ expect }) => {
},
})
- // All CSS should be mapped back to the original source file
- expect(sources).toEqual(['utilities.css', 'input.css'])
- expect(sources.length).toBe(2)
-
- // The output CSS should include annotations linking back to:
- expect(annotations).toEqual([
- // @tailwind utilities
- 'utilities.css: 1:0-6 <- 1:0-19',
- 'utilities.css: 2:2-15 <- 1:0-19',
- 'utilities.css: 4:0-8 <- 1:0-19',
- // color: orange
- 'input.css: 5:2-15 <- 4:2-15',
- // @tailwind utilities
- 'utilities.css: 7:0-11 <- 1:0-19',
- 'utilities.css: 8:2-13 <- 1:0-19',
- ])
-
- expect(output).toMatchInlineSnapshot(`
- ".flex {
- display: flex;
- }
- .custom {
- color: orange;
- }
- .custom-js {
- color: blue;
- }
+ expect(visualized).toMatchInlineSnapshot(`
+ "
+ SOURCES
+ - utilities.css
+ - input.css
+
+ VISUALIZATION
+ /* input: utilities.css */
+ @tailwind utilities;
+ #1 -------------------
+ #2 -------------------
+ #3 -------------------
+ #5 -------------------
+ #6 -------------------
+
+ /* input: input.css */
+ @import './utilities.css';
+ @plugin "./plugin.js";
+ @utility custom {
+ color: orange;
+ #4 -------------
+ }
+ /* output */
+ .flex {
+ #1 ------
+ display: flex;
+ #2 -------------
+ }
+ .custom {
+ #3 --------
+ color: orange;
+ #4 -------------
+ }
+ .custom-js {
+ #5 -----------
+ color: blue;
+ #6 -----------
+ }
+
"
`)
})
-test('utilities have source maps pointing to the utilities node', async ({ expect }) => {
- let { sources, annotations } = await run({
+test('utilities have source maps pointing to the utilities node', async () => {
+ let visualized = await run({
input: `@tailwind utilities;`,
candidates: [
//
@@ -358,17 +1508,29 @@ test('utilities have source maps pointing to the utilities node', async ({ expec
],
})
- expect(sources).toEqual(['input.css'])
+ expect(visualized).toMatchInlineSnapshot(`
+ "
+ SOURCES
+ - input.css
- expect(annotations).toEqual([
- //
- 'input.css: 1:0-11 <- 1:0-19',
- 'input.css: 2:2-33 <- 1:0-19',
- ])
+ VISUALIZATION
+ /* input: input.css */
+ @tailwind utilities;
+ #1 -------------------
+ #2 -------------------
+ /* output */
+ .underline {
+ #1 -----------
+ text-decoration-line: underline;
+ #2 -------------------------------
+ }
+
+ "
+ `)
})
-test('@apply generates source maps', async ({ expect }) => {
- let { sources, annotations } = await run({
+test('@apply generates source maps', async () => {
+ let visualized = await run({
input: css`
.foo {
color: blue;
@@ -379,43 +1541,462 @@ test('@apply generates source maps', async ({ expect }) => {
`,
})
- expect(sources).toEqual(['input.css'])
-
- expect(annotations).toEqual([
- 'input.css: 1:0-5 <- 1:0-5',
- 'input.css: 2:2-13 <- 2:2-13',
- 'input.css: 3:2-13 <- 3:9-20',
- 'input.css: 4:2-10 <- 3:21-38',
- 'input.css: 5:4-26 <- 3:21-38',
- 'input.css: 6:6-17 <- 3:21-38',
- 'input.css: 9:2-33 <- 4:9-18',
- 'input.css: 10:2-12 <- 5:2-12',
- ])
+ expect(visualized).toMatchInlineSnapshot(`
+ "
+ SOURCES
+ - input.css
+
+ VISUALIZATION
+ /* input: input.css */
+ .foo {
+ #1 -----
+ color: blue;
+ #2 -----------
+ @apply text-[#000] hover:text-[#f00];
+ #3 -----------
+ #4 -----------------
+ #5 -----------------
+ #6 -----------------
+ @apply underline;
+ #7 ---------
+ color: red;
+ #8 ----------
+ }
+ /* output */
+ .foo {
+ #1 -----
+ color: blue;
+ #2 -----------
+ color: #000;
+ #3 -----------
+ &:hover {
+ #4 --------
+ @media (hover: hover) {
+ #5 ----------------------
+ color: #f00;
+ #6 -----------
+ }
+ }
+ text-decoration-line: underline;
+ #7 -------------------------------
+ color: red;
+ #8 ----------
+ }
+
+ "
+ `)
})
-test('license comments preserve source locations', async ({ expect }) => {
- let { sources, annotations } = await run({
+test('license comments preserve source locations', async () => {
+ let visualized = await run({
input: `/*! some comment */`,
})
- expect(sources).toEqual(['input.css'])
+ expect(visualized).toMatchInlineSnapshot(`
+ "
+ SOURCES
+ - input.css
- expect(annotations).toEqual([
- //
- 'input.css: 1:0-19 <- 1:0-19',
- ])
+ VISUALIZATION
+ /* input: input.css */
+ /*! some comment */
+ #1 -------------------
+ /* output */
+ /*! some comment */
+ #1 -------------------
+ "
+ `)
})
-test('license comments with new lines preserve source locations', async ({ expect }) => {
- let { sources, annotations, css } = await run({
+test('license comments with new lines preserve source locations', async () => {
+ let visualized = await run({
input: `/*! some \n comment */`,
})
- expect(sources).toEqual(['input.css'])
+ expect(visualized).toMatchInlineSnapshot(`
+ "
+ SOURCES
+ - input.css
+
+ VISUALIZATION
+ /* input: input.css */
+ /*! some
+ #1 ^
+ comment */
+ #1 -----------
+ /* output */
+ /*! some
+ #1
+ comment */
+ #2 ^
+ "
+ `)
+})
+
+test('comment, single line', async () => {
+ let visualized = await analyze({
+ input: `/*! foo */`,
+ })
+
+ expect(visualized).toMatchInlineSnapshot(`
+ "
+ SOURCES
+ - input.css
+
+ VISUALIZATION
+ /* input: input.css */
+ /*! foo */
+ #1 ----------
+ /* output */
+ /*! foo */
+ #1 ----------
+
+ "
+ `)
+})
+
+test('comment, multi line', async () => {
+ let visualized = await analyze({
+ input: `/*! foo \n bar */`,
+ })
+
+ expect(visualized).toMatchInlineSnapshot(`
+ "
+ SOURCES
+ - input.css
+
+ VISUALIZATION
+ /* input: input.css */
+ /*! foo
+ #1 ^
+ bar */
+ #1 -------
+ /* output */
+ /*! foo
+ #1
+ bar */
+ #2 ^
+
+ "
+ `)
+})
+
+test('declaration, normal property, single line', async () => {
+ let visualized = await analyze({
+ input: `.foo { color: red; }`,
+ })
+
+ expect(visualized).toMatchInlineSnapshot(`
+ "
+ SOURCES
+ - input.css
+
+ VISUALIZATION
+ /* input: input.css */
+ .foo { color: red; }
+ #1 -----
+ #2 ----------
+ /* output */
+ .foo {
+ #1 -----
+ color: red;
+ #2 ----------
+ }
+
+ "
+ `)
+})
+
+test('declaration, normal property, multi line', async () => {
+ // Works, no changes needed
+ let visualized = await analyze({
+ input: dedent`
+ .foo {
+ grid-template-areas:
+ "a b c"
+ "d e f"
+ "g h i";
+ }
+ `,
+ })
+
+ expect(visualized).toMatchInlineSnapshot(`
+ "
+ SOURCES
+ - input.css
+
+ VISUALIZATION
+ /* input: input.css */
+ .foo {
+ #1 -----
+ grid-template-areas:
+ #2 ^
+ "a b c"
+ #2 ^
+ "d e f"
+ #3 ^
+ "g h i";
+ #3 -----------
+ }
+ /* output */
+ .foo {
+ #1 -----
+ grid-template-areas: "a b c" "d e f" "g h i";
+ #2
+ #3
+ #4 ^
+ }
+
+ "
+ `)
+})
+
+test('declaration, custom property, single line', async () => {
+ let visualized = await analyze({
+ input: `.foo { --foo: bar; }`,
+ })
+
+ expect(visualized).toMatchInlineSnapshot(`
+ "
+ SOURCES
+ - input.css
+
+ VISUALIZATION
+ /* input: input.css */
+ .foo { --foo: bar; }
+ #1 -----
+ #2 ----------
+ /* output */
+ .foo {
+ #1 -----
+ --foo: bar;
+ #2 ----------
+ }
+
+ "
+ `)
+})
+
+test('declaration, custom property, multi line', async () => {
+ let visualized = await analyze({
+ input: dedent`
+ .foo {
+ --foo: bar\nbaz;
+ }
+ `,
+ })
+
+ expect(visualized).toMatchInlineSnapshot(`
+ "
+ SOURCES
+ - input.css
+
+ VISUALIZATION
+ /* input: input.css */
+ .foo {
+ #1 -----
+ --foo: bar
+ #2 ^
+ baz;
+ #2 ---
+ }
+ /* output */
+ .foo {
+ #1 -----
+ --foo: bar
+ #2
+ baz;
+ #3 ^
+ }
+
+ "
+ `)
+})
+
+test('at rules, bodyless, single line', async () => {
+ // This intentionally has extra spaces
+ let visualized = await analyze({
+ input: `@layer foo, bar;`,
+ })
+
+ expect(visualized).toMatchInlineSnapshot(`
+ "
+ SOURCES
+ - input.css
+
+ VISUALIZATION
+ /* input: input.css */
+ @layer foo, bar;
+ #1 -------------------
+ /* output */
+ @layer foo, bar;
+ #1 ---------------
+
+ "
+ `)
+})
+
+test('at rules, bodyless, multi line', async () => {
+ let visualized = await analyze({
+ input: dedent`
+ @layer
+ foo,
+ bar
+ ;
+ `,
+ })
+
+ expect(visualized).toMatchInlineSnapshot(`
+ "
+ SOURCES
+ - input.css
+
+ VISUALIZATION
+ /* input: input.css */
+ @layer
+ #1 ^
+ foo,
+ #1 ^
+ bar
+ #2 ^
+ ;
+ #2 ^
+ /* output */
+ @layer foo, bar;
+ #1
+ #2 ---------------
+
+ "
+ `)
+})
+
+test('at rules, body, single line', async () => {
+ let visualized = await analyze({
+ input: `@layer foo { color: red; }`,
+ })
+
+ expect(visualized).toMatchInlineSnapshot(`
+ "
+ SOURCES
+ - input.css
+
+ VISUALIZATION
+ /* input: input.css */
+ @layer foo { color: red; }
+ #1 -----------
+ #2 ----------
+ /* output */
+ @layer foo {
+ #1 -----------
+ color: red;
+ #2 ----------
+ }
+
+ "
+ `)
+})
+
+test('at rules, body, multi line', async () => {
+ let visualized = await analyze({
+ input: dedent`
+ @layer
+ foo
+ {
+ color: baz;
+ }
+ `,
+ })
+
+ expect(visualized).toMatchInlineSnapshot(`
+ "
+ SOURCES
+ - input.css
+
+ VISUALIZATION
+ /* input: input.css */
+ @layer
+ #1 ^
+ foo
+ #1 ^
+ {
+ #2 ^
+ color: baz;
+ #2 ----------
+ }
+ /* output */
+ @layer foo {
+ #1
+ #2 ^
+ color: baz;
+ #2 ----------
+ }
+
+ "
+ `)
+})
- expect(annotations).toEqual([
- //
- 'input.css: 1:0 <- 1:0-2:0',
- 'input.css: 2:11 <- 2:11',
- ])
+test('style rules, body, single line', async () => {
+ let visualized = await analyze({
+ input: `.foo:is(.bar) { color: red; }`,
+ })
+
+ expect(visualized).toMatchInlineSnapshot(`
+ "
+ SOURCES
+ - input.css
+
+ VISUALIZATION
+ /* input: input.css */
+ .foo:is(.bar) { color: red; }
+ #1 --------------
+ #2 ----------
+ /* output */
+ .foo:is(.bar) {
+ #1 --------------
+ color: red;
+ #2 ----------
+ }
+
+ "
+ `)
+})
+
+test('style rules, body, multi line', async () => {
+ // Works, no changes needed
+ let visualized = await analyze({
+ input: dedent`
+ .foo:is(
+ .bar
+ ) {
+ color: red;
+ }
+ `,
+ })
+
+ expect(visualized).toMatchInlineSnapshot(`
+ "
+ SOURCES
+ - input.css
+
+ VISUALIZATION
+ /* input: input.css */
+ .foo:is(
+ #1 ^
+ .bar
+ #1 ^
+ ) {
+ #2 --
+ color: red;
+ #3 ----------
+ }
+ /* output */
+ .foo:is( .bar ) {
+ #1
+ #2 ----------------
+ color: red;
+ #3 ----------
+ }
+
+ "
+ `)
})
diff --git a/packages/tailwindcss/src/source-maps/visualizer.ts b/packages/tailwindcss/src/source-maps/visualizer.ts
new file mode 100644
index 000000000000..9401d3f39327
--- /dev/null
+++ b/packages/tailwindcss/src/source-maps/visualizer.ts
@@ -0,0 +1,120 @@
+import { DefaultMap } from '../utils/default-map'
+import type { DecodedMapping, DecodedSource, DecodedSourceMap } from './source-map'
+
+interface RangeMapping {
+ id: number
+ source: DecodedSource
+ line: number
+ start: number
+ end: number | null
+}
+
+export function visualize(generated: string, map: DecodedSourceMap) {
+ let outputSource: DecodedSource = {
+ url: 'output.css',
+ content: generated,
+ ignore: false,
+ }
+
+ // Group mappings by source file
+ let bySource = new DefaultMap>(
+ () => new DefaultMap(() => []),
+ )
+
+ let mappingIds = new Map()
+
+ let nextId = 1
+ for (let mapping of map.mappings) {
+ let pos = mapping.originalPosition
+ if (!pos) continue
+ let source = pos.source
+ if (!source) continue
+ bySource.get(source).get(pos.line).push(mapping)
+ mappingIds.set(mapping, nextId++)
+ }
+
+ for (let mapping of map.mappings) {
+ let pos = mapping.generatedPosition
+ if (!pos) continue
+ bySource.get(outputSource).get(pos.line).push(mapping)
+ }
+
+ let maxIdSize = Math.ceil(Math.log10(Math.max(...mappingIds.values())))
+ // `#number `
+ let gutterSize = 3 + maxIdSize
+
+ let output = ''
+ output += '\n'
+ output += 'SOURCES\n'
+
+ for (let source of bySource.keys()) {
+ if (source === outputSource) continue
+ output += '- '
+ output += source.url ?? 'unknown'
+ output += '\n'
+ }
+
+ output += '\n'
+ output += 'VISUALIZATION\n'
+
+ for (let [source, byLine] of bySource) {
+ if (!source.content) continue
+
+ output += ' '.repeat(gutterSize)
+ output += `/* `
+ if (source === outputSource) {
+ output += `output`
+ } else {
+ output += `input: `
+ output += source.url ?? 'unknown'
+ }
+ output += ` */\n`
+
+ let lines = source.content.split('\n').entries()
+ for (let [lineNum, line] of lines) {
+ output += ' '.repeat(gutterSize)
+ output += line
+ output += '\n'
+
+ let pairs: DecodedMapping[][] = []
+
+ // Get all mappings for this line
+ let lineMappings = byLine.get(lineNum + 1)
+
+ // Group consecutive mappings into pairs
+ for (let i = 0; i < lineMappings.length; i += 2) {
+ let pair = [lineMappings[i]]
+ if (i + 1 < lineMappings.length) {
+ pair.push(lineMappings[i + 1])
+ }
+
+ pairs.push(pair)
+ }
+
+ for (let [start, end] of pairs) {
+ let id = mappingIds.get(start)
+ if (!id) continue
+
+ let startPos = source === outputSource ? start.generatedPosition : start.originalPosition
+ if (!startPos) continue
+
+ let endPos = source === outputSource ? end?.generatedPosition : end?.originalPosition
+
+ output += '#'
+ output += `${Math.floor((id + 1) / 2)}`.padEnd(maxIdSize, ' ')
+ output += ' '
+ output += ' '.repeat(startPos.column)
+
+ if (endPos) {
+ output += '-'.repeat(Math.max(0, endPos.column - startPos.column))
+ } else {
+ output += '^'
+ }
+
+ output += '\n'
+ }
+ }
+ }
+
+ return output
+}
From be8cc9538c347e3a4510a8b84939f6b414bc62d0 Mon Sep 17 00:00:00 2001
From: Jordan Pittman
Date: Wed, 7 May 2025 18:53:04 -0400
Subject: [PATCH 41/41] wip
---
.../src/source-maps/translation-map.test.ts | 279 ------------------
1 file changed, 279 deletions(-)
delete mode 100644 packages/tailwindcss/src/source-maps/translation-map.test.ts
diff --git a/packages/tailwindcss/src/source-maps/translation-map.test.ts b/packages/tailwindcss/src/source-maps/translation-map.test.ts
deleted file mode 100644
index ad0a9f435154..000000000000
--- a/packages/tailwindcss/src/source-maps/translation-map.test.ts
+++ /dev/null
@@ -1,279 +0,0 @@
-import dedent from 'dedent'
-import { assert, expect, test } from 'vitest'
-import { toCss, type AstNode } from '../ast'
-import * as CSS from '../css-parser'
-import { createTranslationMap } from './source-map'
-
-async function analyze(input: string) {
- let ast = CSS.parse(input, { from: 'input.css' })
- let css = toCss(ast, true)
- let translate = createTranslationMap({
- original: input,
- generated: css,
- })
-
- function format(node: AstNode) {
- let lines: string[] = []
-
- for (let [oStart, oEnd, gStart, gEnd] of translate(node)) {
- let src = `${oStart.line}:${oStart.column}-${oEnd.line}:${oEnd.column}`
-
- let dst = '(none)'
-
- if (gStart && gEnd) {
- dst = `${gStart.line}:${gStart.column}-${gEnd.line}:${gEnd.column}`
- }
-
- lines.push(`${dst} <- ${src}`)
- }
-
- return lines
- }
-
- return { ast, css, format }
-}
-
-test('comment, single line', async () => {
- let { ast, css, format } = await analyze(`/*! foo */`)
-
- assert(ast[0].kind === 'comment')
- expect(format(ast[0])).toMatchInlineSnapshot(`
- [
- "1:0-1:10 <- 1:0-1:10",
- ]
- `)
-
- expect(css).toMatchInlineSnapshot(`
- "/*! foo */
- "
- `)
-})
-
-test('comment, multi line', async () => {
- let { ast, css, format } = await analyze(`/*! foo \n bar */`)
-
- assert(ast[0].kind === 'comment')
- expect(format(ast[0])).toMatchInlineSnapshot(`
- [
- "1:0-2:7 <- 1:0-2:7",
- ]
- `)
-
- expect(css).toMatchInlineSnapshot(`
- "/*! foo
- bar */
- "
- `)
-})
-
-test('declaration, normal property, single line', async () => {
- let { ast, css, format } = await analyze(`.foo { color: red; }`)
-
- assert(ast[0].kind === 'rule')
- assert(ast[0].nodes[0].kind === 'declaration')
- expect(format(ast[0].nodes[0])).toMatchInlineSnapshot(`
- [
- "2:2-2:12 <- 1:7-1:17",
- ]
- `)
-
- expect(css).toMatchInlineSnapshot(`
- ".foo {
- color: red;
- }
- "
- `)
-})
-
-test('declaration, normal property, multi line', async () => {
- // Works, no changes needed
- let { ast, css, format } = await analyze(dedent`
- .foo {
- grid-template-areas:
- "a b c"
- "d e f"
- "g h i";
- }
- `)
-
- assert(ast[0].kind === 'rule')
- assert(ast[0].nodes[0].kind === 'declaration')
- expect(format(ast[0].nodes[0])).toMatchInlineSnapshot(`
- [
- "2:2-2:46 <- 2:2-5:11",
- ]
- `)
-
- expect(css).toMatchInlineSnapshot(`
- ".foo {
- grid-template-areas: "a b c" "d e f" "g h i";
- }
- "
- `)
-})
-
-test('declaration, custom property, single line', async () => {
- let { ast, css, format } = await analyze(`.foo { --foo: bar; }`)
-
- assert(ast[0].kind === 'rule')
- assert(ast[0].nodes[0].kind === 'declaration')
- expect(format(ast[0].nodes[0])).toMatchInlineSnapshot(`
- [
- "2:2-2:12 <- 1:7-1:17",
- ]
- `)
-
- expect(css).toMatchInlineSnapshot(`
- ".foo {
- --foo: bar;
- }
- "
- `)
-})
-
-test('declaration, custom property, multi line', async () => {
- let { ast, css, format } = await analyze(dedent`
- .foo {
- --foo: bar\nbaz;
- }
- `)
-
- assert(ast[0].kind === 'rule')
- assert(ast[0].nodes[0].kind === 'declaration')
- expect(format(ast[0].nodes[0])).toMatchInlineSnapshot(`
- [
- "2:2-3:3 <- 2:2-3:3",
- ]
- `)
-
- expect(css).toMatchInlineSnapshot(`
- ".foo {
- --foo: bar
- baz;
- }
- "
- `)
-})
-
-test('at rules, bodyless, single line', async () => {
- // This intentionally has extra spaces
- let { ast, css, format } = await analyze(`@layer foo, bar;`)
-
- assert(ast[0].kind === 'at-rule')
- expect(format(ast[0])).toMatchInlineSnapshot(`
- [
- "1:0-1:15 <- 1:0-1:19",
- ]
- `)
-
- expect(css).toMatchInlineSnapshot(`
- "@layer foo, bar;
- "
- `)
-})
-
-test('at rules, bodyless, multi line', async () => {
- let { ast, css, format } = await analyze(dedent`
- @layer
- foo,
- bar
- ;
- `)
-
- assert(ast[0].kind === 'at-rule')
- expect(format(ast[0])).toMatchInlineSnapshot(`
- [
- "1:0-1:15 <- 1:0-4:0",
- ]
- `)
-
- expect(css).toMatchInlineSnapshot(`
- "@layer foo, bar;
- "
- `)
-})
-
-test('at rules, body, single line', async () => {
- let { ast, css, format } = await analyze(`@layer foo { color: red; }`)
-
- assert(ast[0].kind === 'at-rule')
- expect(format(ast[0])).toMatchInlineSnapshot(`
- [
- "1:0-1:11 <- 1:0-1:11",
- ]
- `)
-
- expect(css).toMatchInlineSnapshot(`
- "@layer foo {
- color: red;
- }
- "
- `)
-})
-
-test('at rules, body, multi line', async () => {
- let { ast, css, format } = await analyze(dedent`
- @layer
- foo
- {
- color: baz;
- }
- `)
-
- assert(ast[0].kind === 'at-rule')
- expect(format(ast[0])).toMatchInlineSnapshot(`
- [
- "1:0-1:11 <- 1:0-3:0",
- ]
- `)
-
- expect(css).toMatchInlineSnapshot(`
- "@layer foo {
- color: baz;
- }
- "
- `)
-})
-
-test('style rules, body, single line', async () => {
- let { ast, css, format } = await analyze(`.foo:is(.bar) { color: red; }`)
-
- assert(ast[0].kind === 'rule')
- expect(format(ast[0])).toMatchInlineSnapshot(`
- [
- "1:0-1:14 <- 1:0-1:14",
- ]
- `)
-
- expect(css).toMatchInlineSnapshot(`
- ".foo:is(.bar) {
- color: red;
- }
- "
- `)
-})
-
-test('style rules, body, multi line', async () => {
- // Works, no changes needed
- let { ast, css, format } = await analyze(dedent`
- .foo:is(
- .bar
- ) {
- color: red;
- }
- `)
-
- assert(ast[0].kind === 'rule')
- expect(format(ast[0])).toMatchInlineSnapshot(`
- [
- "1:0-1:16 <- 1:0-3:2",
- ]
- `)
-
- expect(css).toMatchInlineSnapshot(`
- ".foo:is( .bar ) {
- color: red;
- }
- "
- `)
-})