Compare commits

...
Sign in to create a new pull request.

41 commits

Author SHA1 Message Date
Jordan Pittman
be8cc9538c wip 2025-05-07 18:53:04 -04:00
Jordan Pittman
3551daad37 wip visualize souce maps in tests 2025-05-07 18:52:57 -04:00
Jordan Pittman
ec0453dfec Improve sorting of mappings 2025-05-07 18:40:22 -04:00
Jordan Pittman
83e2cd296d Clarify perf comment
The actual unoptimized bytecode contains one additional instruction but this is definitely a bit faster in benchmarks
2025-05-07 16:04:45 -04:00
Jordan Pittman
f3fc8137f2 Add line table benchmark 2025-05-07 15:49:45 -04:00
Jordan Pittman
347d9e2971 Remove unncessary settings from test 2025-05-07 15:49:03 -04:00
Jordan Pittman
e3561fba8e Update type 2025-05-06 11:29:11 -04:00
Jordan Pittman
58932d5a25 Inline function to push mappings 2025-05-06 11:28:17 -04:00
Jordan Pittman
1992ba55ab Add comment clarifying magic number 2025-05-06 11:27:12 -04:00
Jordan Pittman
8fdb8baaa1 Increment unknown source ID when generating maps 2025-05-06 11:26:44 -04:00
Jordan Pittman
db5d97403e Fix typo 2025-05-06 11:26:06 -04:00
Jordan Pittman
78915dc156 Remove leftover logs 2025-05-06 11:25:29 -04:00
Jordan Pittman
2e8d099475 Move walk into map fn 2025-05-06 11:23:27 -04:00
Jordan Pittman
5e2fd98f60 Don’t double clone nodes 2025-05-06 11:14:07 -04:00
Jordan Pittman
65e2c17627 Optimize LineTable#find implementation 2025-05-06 10:36:15 -04:00
Jordan Pittman
c9cb8a7deb Fix UI tests 2025-05-05 17:56:23 -04:00
Jordan Pittman
502bda71b8 Update lockfile 2025-05-05 14:06:10 -04:00
Jordan Pittman
00358d0686 Add source map support to PostCSS 2025-05-05 14:05:36 -04:00
Jordan Pittman
bb78c59770 Add source map support to Vite 2025-05-05 14:05:36 -04:00
Jordan Pittman
2f25e3ec3a Add source map support to the CLI 2025-05-05 14:05:36 -04:00
Jordan Pittman
acf5f3182e Update source maps when optimizing output CSS 2025-05-05 14:05:31 -04:00
Jordan Pittman
2302668b48 Add integration test helpers for source maps 2025-05-05 14:05:31 -04:00
Jordan Pittman
b191165f44 Prep for source maps in build tools 2025-05-05 14:05:31 -04:00
Jordan Pittman
a0a9a33952 Use offset information to generate source maps 2025-05-05 14:05:31 -04:00
Jordan Pittman
9a9b47a1fa Track utilities per-candidate in @apply 2025-05-05 13:47:25 -04:00
Jordan Pittman
6b9ace2679 Track utilities generated by @apply 2025-05-05 13:47:25 -04:00
Jordan Pittman
6febfd966a Track locations when printing 2025-05-05 13:47:25 -04:00
Jordan Pittman
fcd4e2d2bc Track locations when optimizing 2025-05-05 13:47:25 -04:00
Jordan Pittman
386e72158c Track generated utilities 2025-05-05 13:47:25 -04:00
Jordan Pittman
3143eb6e7a Track theme variables 2025-05-05 13:47:25 -04:00
Jordan Pittman
3bc80cda67 Track wrapping at-rules created by imports 2025-05-05 13:47:25 -04:00
Jordan Pittman
c129f48cae Track locations inside @import 2025-05-05 13:47:25 -04:00
Jordan Pittman
e95e6e239f Track locations when parsing 2025-05-05 13:47:25 -04:00
Jordan Pittman
f08f89d7ba Track source locations in the AST
These are stored as character offsets for the original input (`src`) as well as the printed output (`dst`)
2025-05-05 13:47:25 -04:00
Jordan Pittman
8319bd83b3 Update CSS parser tests to preserve CRLF where needed 2025-05-05 13:47:25 -04:00
Jordan Pittman
65409a98d9 Don’t replace CRLF with LF before parsing CSS
Now that we can handle CRLF correctly in the parser this is no longer necessary
2025-05-05 13:47:25 -04:00
Jordan Pittman
da86e231d4 Handle CRLF in strings 2025-05-05 13:47:25 -04:00
Jordan Pittman
e7652b7a71 Check for CRLF when scanning consecutive whitespace 2025-05-05 13:47:25 -04:00
Jordan Pittman
c53f344871 Skip over the CR in CRLF when parsing CSS 2025-05-05 13:47:25 -04:00
Jordan Pittman
68ce361ef4 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.
2025-05-05 13:47:25 -04:00
Jordan Pittman
a4cd44d426 Cleanup code a bit 2025-05-05 13:47:25 -04:00
44 changed files with 4013 additions and 180 deletions

View file

@ -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,194 @@ describe.each([
])
},
)
test(
'production build + inline source maps',
{
fs: {
'package.json': json`
{
"dependencies": {
"tailwindcss": "workspace:^",
"@tailwindcss/cli": "workspace:^"
}
}
`,
'ssrc/index.html': html`
<div class="flex"></div>
`,
'src/index.css': css`
@import 'tailwindcss/utilities';
/* */
`,
},
},
async ({ exec, expect, fs, parseSourceMap }) => {
await exec(`${command} --input src/index.css --output dist/out.css --map`)
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`
<div class="flex"></div>
`,
'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`
<div class="flex"></div>
`,
'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`])
// 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(

View file

@ -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"
}
}

View file

@ -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`
<div class="flex"></div>
`,
'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: '<no 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: '}...',
})
},
)

View file

@ -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<string>
spawn(command: string, options?: ChildProcessOptions): Promise<SpawnedProcess>
parseSourceMap(opts: string | SourceMapOptions): SourceMap
fs: {
write(filePath: string, content: string, encoding?: BufferEncoding): Promise<void>
create(filePaths: string[]): Promise<void>
@ -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<string> {
}
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() + '...',
}
},
}
}

View file

@ -0,0 +1,93 @@
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,
},
})
`,
'index.html': html`
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<div class="flex">Hello, world!</div>
</body>
`,
'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: '}...',
})
},
)

View file

@ -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,
}

View file

@ -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<ReturnType<typeof options>>) {
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,27 +147,48 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
optimizedCss: '',
}
async function write(css: string, args: Result<ReturnType<typeof options>>, I: Instrumentation) {
async function write(
css: string,
map: SourceMap | null,
args: Result<ReturnType<typeof options>>,
I: Instrumentation,
) {
let output = css
// Optimize the output
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,
map: map?.raw ?? undefined,
})
DEBUG && I.end('Optimize CSS')
previous.css = css
previous.optimizedCss = optimizedCss
output = optimizedCss
previous.optimizedCss = optimized.code
if (optimized.map) {
map = toSourceMap(optimized.map)
}
output = optimized.code
} else {
output = previous.optimizedCss
}
}
// 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<ReturnType<typeof options>>) {
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<ReturnType<typeof options>>) {
// 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<ReturnType<typeof options>>) {
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<ReturnType<typeof options>>) {
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<ReturnType<typeof options>>) {
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())

View file

@ -37,9 +37,12 @@
}
},
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"enhanced-resolve": "^5.18.1",
"jiti": "^2.4.2",
"tailwindcss": "workspace:*",
"lightningcss": "catalog:"
"lightningcss": "catalog:",
"magic-string": "^0.30.17",
"source-map-js": "^1.2.1",
"tailwindcss": "workspace:*"
}
}

View file

@ -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,
@ -21,6 +21,7 @@ export type Resolver = (id: string, base: string) => Promise<string | false | un
export interface CompileOptions {
base: string
from?: string
onDependency: (path: string) => 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)
},
@ -125,7 +128,8 @@ export async function loadModule(
let module = await importModule(pathToFileURL(resolvedPath).href)
return {
base: dirname(resolvedPath),
path: resolvedPath,
base: path.dirname(resolvedPath),
module: module.default ?? module,
}
}
@ -144,7 +148,8 @@ export async function loadModule(
onDependency(file)
}
return {
base: dirname(resolvedPath),
path: resolvedPath,
base: path.dirname(resolvedPath),
module: module.default ?? module,
}
}
@ -164,6 +169,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 +178,7 @@ async function loadStylesheet(
let file = await fsPromises.readFile(resolvedPath, 'utf-8')
return {
path: resolvedPath,
base: path.dirname(resolvedPath),
content: file,
}

View file

@ -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

View file

@ -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

View file

@ -1,15 +1,42 @@
import remapping from '@ampproject/remapping'
import { Features, transform } from 'lightningcss'
import MagicString from 'magic-string'
export interface OptimizeOptions {
/**
* The file being transformed
*/
file?: string
/**
* Enabled minified output
*/
minify?: boolean
/**
* The output source map before optimization
*
* If omitted a 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 }: { file?: string; minify?: boolean } = {},
): string {
function optimize(code: Buffer | Uint8Array) {
{ file = 'input.css', minify = false, map }: OptimizeOptions = {},
): TransformResult {
function optimize(code: Buffer | Uint8Array, map: string | undefined) {
return transform({
filename: file,
code: code as any,
code,
minify,
sourceMap: false,
sourceMap: typeof map !== 'undefined',
inputSourceMap: map,
drafts: {
customMedia: true,
},
@ -25,16 +52,39 @@ 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), map)
map = result.map?.toString()
result = optimize(result.code, map)
map = result.map?.toString()
let code = result.code.toString()
// 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 magic = new MagicString(code)
magic.replaceAll('@media not (', '@media not all and (')
return out
// 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,
}
}

View file

@ -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 ?? `<unknown ${id++}>`,
content: src?.content ?? '<none>',
}
})
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
},
}
}

View file

@ -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<Source, Input>((src) => {
return new Input(src.code, {
map: source?.input.map,
from: src.file ?? undefined,
})
})
let lineTables = new DefaultMap<Source, LineTable>((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, Source>((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<AstNode, { nodes: AstNode[] }>['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

View file

@ -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),
@ -282,13 +283,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')

View file

@ -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'
@ -34,7 +42,15 @@ 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,
// 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,
)
})
return [
@ -68,14 +84,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 +109,21 @@ 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 = optimize(result.code, {
minify,
map: result.map,
})
DEBUG && I.end('[@tailwindcss/vite] Optimize CSS')
return { code: generated }
return result
},
},
] satisfies Plugin[]
@ -173,6 +192,7 @@ class Root {
private id: string,
private base: string,
private enableSourceMaps: boolean,
private customCssResolver: (id: string, base: string) => Promise<string | false | undefined>,
private customJsResolver: (id: string, base: string) => Promise<string | false | undefined>,
) {}
@ -183,7 +203,13 @@ class Root {
content: string,
_addWatchFile: (file: string) => void,
I: Instrumentation,
): Promise<string | false> {
): Promise<
| {
code: string
map: string | undefined
}
| false
> {
let inputPath = idToPath(this.id)
function addWatchFile(file: string) {
@ -215,6 +241,7 @@ class Root {
DEBUG && I.start('Setup compiler')
let addBuildDependenciesPromises: Promise<void>[] = []
this.compiler = await compile(content, {
from: this.enableSourceMaps ? this.id : undefined,
base: inputBase,
shouldRewriteUrls: true,
onDependency: (path) => {
@ -313,10 +340,17 @@ 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
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,
}
}
private async addBuildDependency(path: string) {

View file

@ -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"
}
}

View file

@ -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,17 +156,59 @@ 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<string, number> = {}
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 candidateAst = compileCandidates(candidates, designSystem, {
let candidates = Object.keys(candidateOffsets)
let compiled = compileCandidates(candidates, designSystem, {
onInvalidCandidate: (candidate) => {
throw new Error(`Cannot apply unknown utility class: ${candidate}`)
},
}).astNodes
})
let src = node.src
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) {
// 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]]
candidateSrc[1] += 7 /* '@apply '.length */ + candidateOffset
candidateSrc[2] = candidateSrc[1] + candidate.length
// 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 = candidateSrc
})
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

View file

@ -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)
})

View file

@ -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<string, string | boolean>
nodes: AstNode[]
src?: undefined
dst?: undefined
}
export type AtRoot = {
kind: 'at-root'
nodes: AstNode[]
src?: undefined
dst?: undefined
}
export type Rule = StyleRule | AtRule
@ -378,10 +397,13 @@ export function optimizeAst(
}
}
let fallback = decl(property, initialValue ?? 'initial')
fallback.src = node.src
if (inherits) {
propertyFallbacksRoot.push(decl(property, initialValue ?? 'initial'))
propertyFallbacksRoot.push(fallback)
} else {
propertyFallbacksUniversal.push(decl(property, initialValue ?? 'initial'))
propertyFallbacksUniversal.push(fallback)
}
}
@ -623,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)
}
}
@ -632,11 +655,15 @@ export function optimizeAst(
let fallbackAst = []
if (propertyFallbacksRoot.length > 0) {
fallbackAst.push(rule(':root, :host', propertyFallbacksRoot))
let wrapper = rule(':root, :host', propertyFallbacksRoot)
wrapper.src = propertyFallbacksRoot[0].src
fallbackAst.push(wrapper)
}
if (propertyFallbacksUniversal.length > 0) {
fallbackAst.push(rule('*, ::before, ::after, ::backdrop', propertyFallbacksUniversal))
let wrapper = rule('*, ::before, ::after, ::backdrop', propertyFallbacksUniversal)
wrapper.src = propertyFallbacksUniversal[0].src
fallbackAst.push(wrapper)
}
if (fallbackAst.length > 0) {
@ -658,30 +685,43 @@ export function optimizeAst(
return true
})
let layerPropertiesStatement = atRule('@layer', 'properties', [])
layerPropertiesStatement.src = fallbackAst[0].src
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,
),
])
block.src = fallbackAst[0].src
block.nodes[0].src = fallbackAst[0].src
newAst.push(block)
}
}
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)
@ -689,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
@ -710,22 +805,97 @@ 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`
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 `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,12 +913,11 @@ export function toCss(ast: AstNode[]) {
let css = ''
for (let node of ast) {
let result = stringify(node)
if (result !== '') {
css += result
}
css += stringify(node, 0)
}
source.code = css
return css
}

View file

@ -3,13 +3,21 @@ 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<{
path: string
base: string
content: string
}>
export async function substituteAtImports(
ast: AstNode[],
base: string,
loadStylesheet: LoadStylesheet,
recurseCount = 0,
track = false,
) {
let features = Features.None
let promises: Promise<void>[] = []
@ -45,10 +53,11 @@ 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(
node,
[context({ base: loaded.base }, ast)],
layer,
media,
@ -140,6 +149,7 @@ export function parseImportParams(params: ValueParser.ValueAstNode[]) {
}
function buildImportNodes(
importNode: AstNode,
importedAst: AstNode[],
layer: string | null,
media: string | null,
@ -148,15 +158,21 @@ function buildImportNodes(
let root = importedAst
if (layer !== null) {
root = [atRule('@layer', layer, root)]
let node = atRule('@layer', layer, root)
node.src = importNode.src
root = [node]
}
if (media !== null) {
root = [atRule('@media', media, root)]
let node = atRule('@media', media, root)
node.src = importNode.src
root = [node]
}
if (supports !== null) {
root = [atRule('@supports', supports[0] === '(' ? supports : `(${supports})`, root)]
let node = atRule('@supports', supports[0] === '(' ? supports : `(${supports})`, root)
node.src = importNode.src
root = [node]
}
return root

View file

@ -30,7 +30,11 @@ export async function applyCompatibilityHooks({
path: string,
base: string,
resourceHint: 'plugin' | 'config',
) => Promise<{ module: any; base: string }>
) => Promise<{
path: string
base: string
module: any
}>
sources: { base: string; pattern: string; negated: boolean }[]
}) {
let features = Features.None

View file

@ -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()

View file

@ -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,10 +1533,11 @@ describe('addBase', () => {
},
async loadStylesheet() {
return {
path: '',
base: '',
content: css`
@plugin "inside";
`,
base: '',
}
},
})
@ -1533,6 +1559,8 @@ describe('addBase', () => {
let compiler = await compile(input, {
loadModule: async () => ({
path: '',
base: '/root',
module: plugin(function ({ addBase }) {
addBase({
':root': {
@ -1542,7 +1570,6 @@ describe('addBase', () => {
},
})
}),
base: '/root',
}),
})
@ -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'))

View file

@ -412,8 +412,9 @@ describe('--theme(…)', () => {
[],
{
loadModule: async () => ({
module: () => {},
path: '',
base: '/root',
module: () => {},
}),
},
),
@ -771,7 +772,11 @@ describe('theme(…)', () => {
}
`,
{
loadModule: async () => ({ module: {}, base: '/root' }),
loadModule: async () => ({
path: '',
base: '/root',
module: {},
}),
},
)
@ -1196,6 +1201,8 @@ describe('in plugins', () => {
{
async loadModule() {
return {
path: '',
base: '/root',
module: plugin(({ addBase, addUtilities }) => {
addBase({
'.my-base-rule': {
@ -1212,7 +1219,6 @@ describe('in plugins', () => {
},
})
}),
base: '/root',
}
},
},
@ -1253,6 +1259,8 @@ describe('in JS config files', () => {
`,
{
loadModule: async () => ({
path: '',
base: '/root',
module: {
theme: {
extend: {
@ -1279,7 +1287,6 @@ describe('in JS config files', () => {
}),
],
},
base: '/root',
}),
},
)
@ -1314,6 +1321,7 @@ test('replaces CSS theme() function with values inside imported stylesheets', as
{
async loadStylesheet() {
return {
path: '',
base: '/bar.css',
content: css`
.red {

View file

@ -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' })
})

View file

@ -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,
},
])

View file

@ -9,6 +9,7 @@ import {
type Declaration,
type Rule,
} from './ast'
import type { Source } from './source-maps/source'
const BACKSLASH = 0x5c
const SLASH = 0x2f
@ -18,6 +19,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
@ -30,9 +32,17 @@ const DASH = 0x2d
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')
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.
if (input[0] === '\uFEFF') input = ' ' + input.slice(1)
let ast: AstNode[] = []
let licenseComments: Comment[] = []
@ -45,11 +55,22 @@ 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++) {
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.
//
@ -61,6 +82,7 @@ export function parse(input: string) {
// ```
//
if (currentChar === BACKSLASH) {
if (buffer === '') bufferStart = i
buffer += input.slice(i, i + 2)
i += 1
}
@ -104,7 +126,13 @@ 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)
if (source) {
node.src = [source, start, i + 1]
node.dst = [source, start, i + 1]
}
}
}
@ -146,7 +174,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)}`,
)
@ -162,7 +194,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)}`,
)
@ -178,7 +213,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
}
@ -289,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 {
@ -309,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)
@ -345,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 {
@ -364,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)
@ -410,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)
@ -446,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)
}
}
@ -495,6 +567,8 @@ export function parse(input: string) {
continue
}
if (buffer === '') bufferStart = i
buffer += String.fromCharCode(currentChar)
}
}
@ -503,7 +577,15 @@ 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)
// Track the source location for source maps
if (source) {
node.src = [source, bufferStart, input.length]
node.dst = [source, bufferStart, input.length]
}
ast.push(node)
}
// When we are done parsing then everything should be balanced. If we still
@ -525,6 +607,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 +630,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(

View file

@ -126,11 +126,12 @@ describe('compiling CSS', () => {
{
async loadStylesheet(id) {
return {
path: '',
base: '',
content: fs.readFileSync(
path.resolve(__dirname, '..', id === 'tailwindcss' ? 'index.css' : id),
'utf-8',
),
base: '',
}
},
},
@ -401,6 +402,7 @@ describe('@apply', () => {
{
async loadStylesheet() {
return {
path: '',
base: '/bar.css',
content: css`
.foo {
@ -2320,6 +2322,8 @@ describe('Parsing theme values from CSS', () => {
{
async loadStylesheet() {
return {
path: '',
base: '',
content: css`
@theme {
--color-tomato: #e10c04;
@ -2329,7 +2333,6 @@ describe('Parsing theme values from CSS', () => {
@tailwind utilities;
`,
base: '',
}
},
},
@ -2407,6 +2410,8 @@ describe('Parsing theme values from CSS', () => {
{
async loadStylesheet() {
return {
path: '',
base: '',
content: css`
@theme {
--color-tomato: #e10c04;
@ -2416,7 +2421,6 @@ describe('Parsing theme values from CSS', () => {
@tailwind utilities;
`,
base: '',
}
},
},
@ -2704,6 +2708,8 @@ describe('Parsing theme values from CSS', () => {
{
loadModule: async () => {
return {
path: '',
base: '/root',
module: plugin(({}) => {}, {
theme: {
extend: {
@ -2714,7 +2720,6 @@ describe('Parsing theme values from CSS', () => {
},
},
}),
base: '/root',
}
},
},
@ -2750,6 +2755,8 @@ describe('Parsing theme values from CSS', () => {
{
loadModule: async () => {
return {
path: '',
base: '/root',
module: {
theme: {
extend: {
@ -2760,7 +2767,6 @@ describe('Parsing theme values from CSS', () => {
},
},
},
base: '/root',
}
},
},
@ -2839,10 +2845,11 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
path: '',
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', '&:hover, &:focus')
},
base: '/root',
}),
},
),
@ -2857,10 +2864,11 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
path: '',
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', '&:hover, &:focus')
},
base: '/root',
}),
},
),
@ -2877,10 +2885,11 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
path: '',
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', '&:hover, &:focus')
},
base: '/root',
}),
},
),
@ -2899,6 +2908,8 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
path: '',
base: '/root',
module: plugin.withOptions((options) => {
expect(options).toEqual({
color: 'red',
@ -2912,7 +2923,6 @@ describe('plugins', () => {
})
}
}),
base: '/root',
}),
},
)
@ -2951,6 +2961,8 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
path: '',
base: '/root',
module: plugin.withOptions((options) => {
expect(options).toEqual({
'is-null': null,
@ -2971,7 +2983,6 @@ describe('plugins', () => {
return () => {}
}),
base: '/root',
}),
},
)
@ -2991,6 +3002,8 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
path: '',
base: '/root',
module: plugin.withOptions((options) => {
return ({ addUtilities }) => {
addUtilities({
@ -3000,7 +3013,6 @@ describe('plugins', () => {
})
}
}),
base: '/root',
}),
},
),
@ -3029,6 +3041,8 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
path: '',
base: '/root',
module: plugin(({ addUtilities }) => {
addUtilities({
'.text-primary': {
@ -3036,7 +3050,6 @@ describe('plugins', () => {
},
})
}),
base: '/root',
}),
},
),
@ -3054,7 +3067,11 @@ describe('plugins', () => {
}
`,
{
loadModule: async () => ({ module: plugin(() => {}), base: '/root' }),
loadModule: async () => ({
path: '',
base: '/root',
module: plugin(() => {}),
}),
},
),
).rejects.toThrowErrorMatchingInlineSnapshot(
@ -3075,7 +3092,11 @@ describe('plugins', () => {
}
`,
{
loadModule: async () => ({ module: plugin(() => {}), base: '/root' }),
loadModule: async () => ({
path: '',
base: '/root',
module: plugin(() => {}),
}),
},
),
).rejects.toThrowErrorMatchingInlineSnapshot(`
@ -3099,10 +3120,11 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
path: '',
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', '&:hover, &:focus')
},
base: '/root',
}),
},
)
@ -3131,10 +3153,11 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
path: '',
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', ['&:hover', '&:focus'])
},
base: '/root',
}),
},
)
@ -3164,13 +3187,14 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
path: '',
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', {
'&:hover': '@slot',
'&:focus': '@slot',
})
},
base: '/root',
}),
},
)
@ -3199,6 +3223,8 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
path: '',
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', {
'@media (hover: hover)': {
@ -3207,7 +3233,6 @@ describe('plugins', () => {
'&:focus': '@slot',
})
},
base: '/root',
}),
},
)
@ -3248,6 +3273,8 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
path: '',
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', {
'&': {
@ -3257,7 +3284,6 @@ describe('plugins', () => {
},
})
},
base: '/root',
}),
},
)
@ -3286,10 +3312,11 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
path: '',
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('dark', '&:is([data-theme=dark] *)')
},
base: '/root',
}),
},
)
@ -4142,6 +4169,8 @@ test('addBase', async () => {
`,
{
loadModule: async () => ({
path: '',
base: '/root',
module: ({ addBase }: PluginAPI) => {
addBase({
body: {
@ -4149,7 +4178,6 @@ test('addBase', async () => {
},
})
},
base: '/root',
}),
},
)
@ -4181,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 {
@ -4201,6 +4230,8 @@ describe('`@reference "…" imports`', () => {
let loadStylesheet = async (id: string, base: string) => {
if (id === './foo/baz.css') {
return {
path: '',
base: '/root/foo',
content: css`
.foo {
color: red;
@ -4213,14 +4244,14 @@ describe('`@reference "…" imports`', () => {
}
@custom-variant hocus (&:hover, &:focus);
`,
base: '/root/foo',
}
}
return {
path: '',
base: '/root/foo',
content: css`
@import './foo/baz.css';
`,
base: '/root/foo',
}
}
@ -4249,19 +4280,21 @@ describe('`@reference "…" imports`', () => {
let loadStylesheet = async (id: string, base: string) => {
if (id === './foo/baz.css') {
return {
path: '',
base: '/root/foo',
content: css`
@layer utilities {
@tailwind utilities;
}
`,
base: '/root/foo',
}
}
return {
path: '',
base: '/root/foo',
content: css`
@import './foo/baz.css';
`,
base: '/root/foo',
}
}
@ -4311,6 +4344,8 @@ describe('`@reference "…" imports`', () => {
['animate-spin', 'match-utility-initial', 'match-components-initial'],
{
loadModule: async () => ({
path: '',
base: '/root',
module: ({
addBase,
addUtilities,
@ -4344,7 +4379,6 @@ describe('`@reference "…" imports`', () => {
{ values: { initial: 'initial' } },
)
},
base: '/root',
}),
},
),
@ -4366,22 +4400,26 @@ describe('`@reference "…" imports`', () => {
switch (id) {
case './one.css': {
return {
path: '',
base: '/root',
content: css`
@import './two.css' layer(two);
`,
base: '/root',
}
}
case './two.css': {
return {
path: '',
base: '/root',
content: css`
@import './three.css' layer(three);
`,
base: '/root',
}
}
case './three.css': {
return {
path: '',
base: '/root',
content: css`
.foo {
color: red;
@ -4400,10 +4438,11 @@ describe('`@reference "…" imports`', () => {
}
}
`,
base: '/root',
}
}
}
throw new Error('unreachable')
}
await expect(
@ -4438,6 +4477,8 @@ describe('`@reference "…" imports`', () => {
test('supports `@import "…" reference` syntax', async () => {
let loadStylesheet = async () => {
return {
path: '',
base: '/root/foo',
content: css`
.foo {
color: red;
@ -4450,7 +4491,6 @@ describe('`@reference "…" imports`', () => {
}
@custom-variant hocus (&:hover, &:focus);
`,
base: '/root/foo',
}
}

View file

@ -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'
@ -51,13 +52,25 @@ export const enum Polyfills {
type CompileOptions = {
base?: string
from?: string
polyfills?: Polyfills
loadModule?: (
id: string,
base: string,
resourceHint: 'plugin' | 'config',
) => Promise<{ module: Plugin | Config; base: string }>
loadStylesheet?: (id: string, base: string) => Promise<{ content: string; base: string }>
) => Promise<{
path: string
base: string
module: Plugin | Config
}>
loadStylesheet?: (
id: string,
base: string,
) => Promise<{
path: string
base: string
content: string
}>
}
function throwOnLoadModule(): never {
@ -125,6 +138,7 @@ async function parseCss(
ast: AstNode[],
{
base = '',
from,
loadModule = throwOnLoadModule,
loadStylesheet = throwOnLoadStylesheet,
}: CompileOptions = {},
@ -132,7 +146,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()
@ -528,7 +542,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
}
@ -546,6 +560,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([])
@ -593,8 +608,9 @@ 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)
node.src = value.src
nodes.push(node)
}
let keyframesRules = designSystem.theme.getKeyframes()
@ -748,6 +764,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.
@ -766,6 +792,8 @@ export async function compileAst(
}
}
export type { DecodedSourceMap }
export async function compile(
css: string,
opts: CompileOptions = {},
@ -774,8 +802,9 @@ export async function compile(
root: Root
features: Features
build(candidates: string[]): string
buildSourceMap(): DecodedSourceMap
}> {
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
@ -789,11 +818,17 @@ export async function compile(
return compiledCss
}
compiledCss = toCss(newAst)
compiledCss = toCss(newAst, !!opts.from)
compiledAst = newAst
return compiledCss
},
buildSourceMap() {
return createSourceMap({
ast: compiledAst,
})
},
}
}

View file

@ -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;',
}),

View file

@ -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 {

View file

@ -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)
})

View file

@ -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)
})

View file

@ -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` improves performance (in V8 at least)
let mid = (count | 0) >> 1
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,
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,206 @@
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<Source, LineTable>((src) => createLineTable(src.code))
let sourceTable = new DefaultMap<Source, DecodedSource>((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
walk(ast, (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,
})
})
// Populate
for (let source of lineTables.keys()) {
map.sources.push(sourceTable.get(source))
}
// 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 (
aGenerated.line - bGenerated.line ||
aGenerated.column - bGenerated.column ||
aSource - bSource ||
aOriginal.line - bOriginal.line ||
aOriginal.column - bGenerated.column
)
})
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 = [
originalStart: Position,
originalEnd: Position,
generatedStart: Position | null,
generatedEnd: 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
}
}

View file

@ -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]

View file

@ -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<DecodedSource, DefaultMap<number, DecodedMapping[]>>(
() => new DefaultMap<number, DecodedMapping[]>(() => []),
)
let mappingIds = new Map<DecodedMapping, number>()
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
}

View file

@ -7,12 +7,14 @@ export async function compileCss(
options: Parameters<typeof compile>[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
}

View file

@ -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<string, { value: string; options: ThemeOptions }>(),
private values = new Map<
string,
{
value: string
options: ThemeOptions
src: Declaration['src']
}
>(),
private keyframes = new Set<AtRule>([]),
) {}
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 })
}
}

View file

@ -2526,8 +2526,9 @@ 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 {
path: '',
base: '/',
module: createPlugin(({ matchVariant }) => {
matchVariant('is-data', (value) => `&:is([data-${value}])`, {

View file

@ -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 = `<style type="text/css">${styles}</style>${content}`
await page.setContent(content)

66
pnpm-lock.yaml generated
View file

@ -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