Cleanup code a bit

This commit is contained in:
Jordan Pittman 2025-04-03 14:26:02 -04:00
parent 5c5ae04db6
commit a4cd44d426
13 changed files with 153 additions and 94 deletions

View file

@ -39,7 +39,7 @@
"dependencies": {
"enhanced-resolve": "^5.18.1",
"jiti": "^2.4.2",
"tailwindcss": "workspace:*",
"lightningcss": "catalog:"
"lightningcss": "catalog:",
"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,
@ -125,7 +125,7 @@ export async function loadModule(
let module = await importModule(pathToFileURL(resolvedPath).href)
return {
base: dirname(resolvedPath),
base: path.dirname(resolvedPath),
module: module.default ?? module,
}
}
@ -144,7 +144,7 @@ export async function loadModule(
onDependency(file)
}
return {
base: dirname(resolvedPath),
base: path.dirname(resolvedPath),
module: module.default ?? module,
}
}

View file

@ -34,7 +34,12 @@ export default function tailwindcss(): Plugin[] {
function customJsResolver(id: string, base: string) {
return jsResolver(id, base, true, isSSR)
}
return new Root(id, config!.root, customCssResolver, customJsResolver)
return new Root(
id,
config!.root,
customCssResolver,
customJsResolver,
)
})
return [
@ -68,14 +73,14 @@ export default function tailwindcss(): Plugin[] {
let root = roots.get(id)
let generated = await root.generate(src, (file) => this.addWatchFile(file), I)
if (!generated) {
let result = await root.generate(src, (file) => this.addWatchFile(file), I)
if (!result) {
roots.delete(id)
return src
}
DEBUG && I.end('[@tailwindcss/vite] Generate CSS (serve)')
return { code: generated }
return result
},
},
@ -93,18 +98,18 @@ export default function tailwindcss(): Plugin[] {
let root = roots.get(id)
let generated = await root.generate(src, (file) => this.addWatchFile(file), I)
if (!generated) {
let result = await root.generate(src, (file) => this.addWatchFile(file), I)
if (!result) {
roots.delete(id)
return src
}
DEBUG && I.end('[@tailwindcss/vite] Generate CSS (build)')
DEBUG && I.start('[@tailwindcss/vite] Optimize CSS')
generated = optimize(generated, { minify })
result.code = optimize(result.code, { minify })
DEBUG && I.end('[@tailwindcss/vite] Optimize CSS')
return { code: generated }
return result
},
},
] satisfies Plugin[]
@ -183,7 +188,12 @@ class Root {
content: string,
_addWatchFile: (file: string) => void,
I: Instrumentation,
): Promise<string | false> {
): Promise<
| {
code: string
}
| false
> {
let inputPath = idToPath(this.id)
function addWatchFile(file: string) {
@ -313,10 +323,12 @@ class Root {
}
DEBUG && I.start('Build CSS')
let result = this.compiler.build([...this.candidates])
let code = this.compiler.build([...this.candidates])
DEBUG && I.end('Build CSS')
return result
return {
code,
}
}
private async addBuildDependency(path: string) {

View file

@ -161,11 +161,13 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
{
// Parse the candidates to an AST that we can replace the `@apply` rule
// with.
let candidateAst = compileCandidates(candidates, designSystem, {
let compiled = compileCandidates(candidates, designSystem, {
onInvalidCandidate: (candidate) => {
throw new Error(`Cannot apply unknown utility class: ${candidate}`)
},
}).astNodes
})
let candidateAst = compiled.astNodes
// Collect the nodes to insert in place of the `@apply` rule. When a rule
// was used, we want to insert its children instead of the rule because we

View file

@ -378,10 +378,12 @@ export function optimizeAst(
}
}
let fallback = decl(property, initialValue ?? 'initial')
if (inherits) {
propertyFallbacksRoot.push(decl(property, initialValue ?? 'initial'))
propertyFallbacksRoot.push(fallback)
} else {
propertyFallbacksUniversal.push(decl(property, initialValue ?? 'initial'))
propertyFallbacksUniversal.push(fallback)
}
}
@ -632,11 +634,13 @@ export function optimizeAst(
let fallbackAst = []
if (propertyFallbacksRoot.length > 0) {
fallbackAst.push(rule(':root, :host', propertyFallbacksRoot))
let wrapper = rule(':root, :host', propertyFallbacksRoot)
fallbackAst.push(wrapper)
}
if (propertyFallbacksUniversal.length > 0) {
fallbackAst.push(rule('*, ::before, ::after, ::backdrop', propertyFallbacksUniversal))
let wrapper = rule('*, ::before, ::after, ::backdrop', propertyFallbacksUniversal)
fallbackAst.push(wrapper)
}
if (fallbackAst.length > 0) {
@ -658,23 +662,25 @@ export function optimizeAst(
return true
})
let layerPropertiesStatement = atRule('@layer', 'properties', [])
newAst.splice(
firstValidNodeIndex < 0 ? newAst.length : firstValidNodeIndex,
0,
atRule('@layer', 'properties', []),
layerPropertiesStatement,
)
newAst.push(
rule('@layer properties', [
atRule(
'@supports',
// We can't write a supports query for `@property` directly so we have to test for
// features that are added around the same time in Mozilla and Safari.
'((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b))))',
fallbackAst,
),
]),
)
let block = rule('@layer properties', [
atRule(
'@supports',
// We can't write a supports query for `@property` directly so we have to test for
// features that are added around the same time in Mozilla and Safari.
'((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b))))',
fallbackAst,
),
])
newAst.push(block)
}
}
@ -710,7 +716,8 @@ export function toCss(ast: AstNode[]) {
// @layer base, components, utilities;
// ```
if (node.nodes.length === 0) {
return `${indent}${node.name} ${node.params};\n`
let css = `${indent}${node.name} ${node.params};\n`
return css
}
css += `${indent}${node.name}${node.params ? ` ${node.params} ` : ' '}{\n`
@ -725,7 +732,7 @@ export function toCss(ast: AstNode[]) {
css += `${indent}/*${node.value}*/\n`
}
// These should've been handled already by `prepareAstForPrinting` which
// These should've been handled already by `optimizeAst` which
// means we can safely ignore them here. We return an empty string
// immediately to signal that something went wrong.
else if (node.kind === 'context' || node.kind === 'at-root') {
@ -743,10 +750,7 @@ export function toCss(ast: AstNode[]) {
let css = ''
for (let node of ast) {
let result = stringify(node)
if (result !== '') {
css += result
}
css += stringify(node, 0)
}
return css

View file

@ -3,7 +3,13 @@ import { atRule, context, walk, WalkAction, type AstNode } from './ast'
import * as CSS from './css-parser'
import * as ValueParser from './value-parser'
type LoadStylesheet = (id: string, basedir: string) => Promise<{ base: string; content: string }>
type LoadStylesheet = (
id: string,
basedir: string,
) => Promise<{
base: string
content: string
}>
export async function substituteAtImports(
ast: AstNode[],
@ -148,15 +154,18 @@ function buildImportNodes(
let root = importedAst
if (layer !== null) {
root = [atRule('@layer', layer, root)]
let node = atRule('@layer', layer, root)
root = [node]
}
if (media !== null) {
root = [atRule('@media', media, root)]
let node = atRule('@media', media, root)
root = [node]
}
if (supports !== null) {
root = [atRule('@supports', supports[0] === '(' ? supports : `(${supports})`, root)]
let node = atRule('@supports', supports[0] === '(' ? supports : `(${supports})`, root)
root = [node]
}
return root

View file

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

View file

@ -1508,10 +1508,10 @@ describe('addBase', () => {
},
async loadStylesheet() {
return {
base: '',
content: css`
@plugin "inside";
`,
base: '',
}
},
})
@ -1533,6 +1533,7 @@ describe('addBase', () => {
let compiler = await compile(input, {
loadModule: async () => ({
base: '/root',
module: plugin(function ({ addBase }) {
addBase({
':root': {
@ -1542,7 +1543,6 @@ describe('addBase', () => {
},
})
}),
base: '/root',
}),
})

View file

@ -412,8 +412,8 @@ describe('--theme(…)', () => {
[],
{
loadModule: async () => ({
module: () => {},
base: '/root',
module: () => {},
}),
},
),
@ -771,7 +771,10 @@ describe('theme(…)', () => {
}
`,
{
loadModule: async () => ({ module: {}, base: '/root' }),
loadModule: async () => ({
base: '/root',
module: {},
}),
},
)
@ -1196,6 +1199,7 @@ describe('in plugins', () => {
{
async loadModule() {
return {
base: '/root',
module: plugin(({ addBase, addUtilities }) => {
addBase({
'.my-base-rule': {
@ -1212,7 +1216,6 @@ describe('in plugins', () => {
},
})
}),
base: '/root',
}
},
},
@ -1253,6 +1256,7 @@ describe('in JS config files', () => {
`,
{
loadModule: async () => ({
base: '/root',
module: {
theme: {
extend: {
@ -1279,7 +1283,6 @@ describe('in JS config files', () => {
}),
],
},
base: '/root',
}),
},
)

View file

@ -31,8 +31,11 @@ const AT_SIGN = 0x40
const EXCLAMATION_MARK = 0x21
export function parse(input: string) {
if (input[0] === '\uFEFF') input = input.slice(1)
input = input.replaceAll('\r\n', '\n')
// Note: it is important that any transformations of the input string
// *before* processing do NOT change the length of the string. This
// would invalidate the mechanism used to track source locations.
if (input[0] === '\uFEFF') input = ' ' + input.slice(1)
input = input.replaceAll('\r\n', ' \n')
let ast: AstNode[] = []
let licenseComments: Comment[] = []
@ -104,7 +107,8 @@ export function parse(input: string) {
// Collect all license comments so that we can hoist them to the top of
// the AST.
if (commentString.charCodeAt(2) === EXCLAMATION_MARK) {
licenseComments.push(comment(commentString.slice(2, -2)))
let node = comment(commentString.slice(2, -2))
licenseComments.push(node)
}
}
@ -503,7 +507,9 @@ export function parse(input: string) {
// means that we have an at-rule that is not terminated with a semicolon at
// the end of the input.
if (buffer.charCodeAt(0) === AT_SIGN) {
ast.push(parseAtRule(buffer))
let node = parseAtRule(buffer)
ast.push(node)
}
// When we are done parsing then everything should be balanced. If we still
@ -525,6 +531,9 @@ export function parse(input: string) {
}
export function parseAtRule(buffer: string, nodes: AstNode[] = []): AtRule {
let name = buffer
let params = ''
// Assumption: The smallest at-rule in CSS right now is `@page`, this means
// that we can always skip the first 5 characters and start at the
// sixth (at index 5).
@ -545,13 +554,13 @@ export function parseAtRule(buffer: string, nodes: AstNode[] = []): AtRule {
for (let i = 5 /* '@page'.length */; i < buffer.length; i++) {
let currentChar = buffer.charCodeAt(i)
if (currentChar === SPACE || currentChar === OPEN_PAREN) {
let name = buffer.slice(0, i).trim()
let params = buffer.slice(i).trim()
return atRule(name, params, nodes)
name = buffer.slice(0, i)
params = buffer.slice(i)
break
}
}
return atRule(buffer.trim(), '', nodes)
return atRule(name.trim(), params.trim(), nodes)
}
function parseDeclaration(

View file

@ -126,11 +126,11 @@ describe('compiling CSS', () => {
{
async loadStylesheet(id) {
return {
base: '',
content: fs.readFileSync(
path.resolve(__dirname, '..', id === 'tailwindcss' ? 'index.css' : id),
'utf-8',
),
base: '',
}
},
},
@ -2320,6 +2320,7 @@ describe('Parsing theme values from CSS', () => {
{
async loadStylesheet() {
return {
base: '',
content: css`
@theme {
--color-tomato: #e10c04;
@ -2329,7 +2330,6 @@ describe('Parsing theme values from CSS', () => {
@tailwind utilities;
`,
base: '',
}
},
},
@ -2407,6 +2407,7 @@ describe('Parsing theme values from CSS', () => {
{
async loadStylesheet() {
return {
base: '',
content: css`
@theme {
--color-tomato: #e10c04;
@ -2416,7 +2417,6 @@ describe('Parsing theme values from CSS', () => {
@tailwind utilities;
`,
base: '',
}
},
},
@ -2704,6 +2704,7 @@ describe('Parsing theme values from CSS', () => {
{
loadModule: async () => {
return {
base: '/root',
module: plugin(({}) => {}, {
theme: {
extend: {
@ -2714,7 +2715,6 @@ describe('Parsing theme values from CSS', () => {
},
},
}),
base: '/root',
}
},
},
@ -2750,6 +2750,7 @@ describe('Parsing theme values from CSS', () => {
{
loadModule: async () => {
return {
base: '/root',
module: {
theme: {
extend: {
@ -2760,7 +2761,6 @@ describe('Parsing theme values from CSS', () => {
},
},
},
base: '/root',
}
},
},
@ -2839,10 +2839,10 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', '&:hover, &:focus')
},
base: '/root',
}),
},
),
@ -2857,10 +2857,10 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', '&:hover, &:focus')
},
base: '/root',
}),
},
),
@ -2877,10 +2877,10 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', '&:hover, &:focus')
},
base: '/root',
}),
},
),
@ -2899,6 +2899,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
base: '/root',
module: plugin.withOptions((options) => {
expect(options).toEqual({
color: 'red',
@ -2912,7 +2913,6 @@ describe('plugins', () => {
})
}
}),
base: '/root',
}),
},
)
@ -2951,6 +2951,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
base: '/root',
module: plugin.withOptions((options) => {
expect(options).toEqual({
'is-null': null,
@ -2971,7 +2972,6 @@ describe('plugins', () => {
return () => {}
}),
base: '/root',
}),
},
)
@ -2991,6 +2991,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
base: '/root',
module: plugin.withOptions((options) => {
return ({ addUtilities }) => {
addUtilities({
@ -3000,7 +3001,6 @@ describe('plugins', () => {
})
}
}),
base: '/root',
}),
},
),
@ -3029,6 +3029,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
base: '/root',
module: plugin(({ addUtilities }) => {
addUtilities({
'.text-primary': {
@ -3036,7 +3037,6 @@ describe('plugins', () => {
},
})
}),
base: '/root',
}),
},
),
@ -3054,7 +3054,10 @@ describe('plugins', () => {
}
`,
{
loadModule: async () => ({ module: plugin(() => {}), base: '/root' }),
loadModule: async () => ({
base: '/root',
module: plugin(() => {}),
}),
},
),
).rejects.toThrowErrorMatchingInlineSnapshot(
@ -3075,7 +3078,10 @@ describe('plugins', () => {
}
`,
{
loadModule: async () => ({ module: plugin(() => {}), base: '/root' }),
loadModule: async () => ({
base: '/root',
module: plugin(() => {}),
}),
},
),
).rejects.toThrowErrorMatchingInlineSnapshot(`
@ -3099,10 +3105,10 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', '&:hover, &:focus')
},
base: '/root',
}),
},
)
@ -3131,10 +3137,10 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', ['&:hover', '&:focus'])
},
base: '/root',
}),
},
)
@ -3164,13 +3170,13 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', {
'&:hover': '@slot',
'&:focus': '@slot',
})
},
base: '/root',
}),
},
)
@ -3199,6 +3205,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', {
'@media (hover: hover)': {
@ -3207,7 +3214,6 @@ describe('plugins', () => {
'&:focus': '@slot',
})
},
base: '/root',
}),
},
)
@ -3248,6 +3254,7 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('hocus', {
'&': {
@ -3257,7 +3264,6 @@ describe('plugins', () => {
},
})
},
base: '/root',
}),
},
)
@ -3286,10 +3292,10 @@ describe('plugins', () => {
`,
{
loadModule: async () => ({
base: '/root',
module: ({ addVariant }: PluginAPI) => {
addVariant('dark', '&:is([data-theme=dark] *)')
},
base: '/root',
}),
},
)
@ -4142,6 +4148,7 @@ test('addBase', async () => {
`,
{
loadModule: async () => ({
base: '/root',
module: ({ addBase }: PluginAPI) => {
addBase({
body: {
@ -4149,7 +4156,6 @@ test('addBase', async () => {
},
})
},
base: '/root',
}),
},
)
@ -4201,6 +4207,7 @@ describe('`@reference "…" imports`', () => {
let loadStylesheet = async (id: string, base: string) => {
if (id === './foo/baz.css') {
return {
base: '/root/foo',
content: css`
.foo {
color: red;
@ -4213,14 +4220,13 @@ describe('`@reference "…" imports`', () => {
}
@custom-variant hocus (&:hover, &:focus);
`,
base: '/root/foo',
}
}
return {
base: '/root/foo',
content: css`
@import './foo/baz.css';
`,
base: '/root/foo',
}
}
@ -4249,19 +4255,19 @@ describe('`@reference "…" imports`', () => {
let loadStylesheet = async (id: string, base: string) => {
if (id === './foo/baz.css') {
return {
base: '/root/foo',
content: css`
@layer utilities {
@tailwind utilities;
}
`,
base: '/root/foo',
}
}
return {
base: '/root/foo',
content: css`
@import './foo/baz.css';
`,
base: '/root/foo',
}
}
@ -4311,6 +4317,7 @@ describe('`@reference "…" imports`', () => {
['animate-spin', 'match-utility-initial', 'match-components-initial'],
{
loadModule: async () => ({
base: '/root',
module: ({
addBase,
addUtilities,
@ -4344,7 +4351,6 @@ describe('`@reference "…" imports`', () => {
{ values: { initial: 'initial' } },
)
},
base: '/root',
}),
},
),
@ -4366,22 +4372,23 @@ describe('`@reference "…" imports`', () => {
switch (id) {
case './one.css': {
return {
base: '/root',
content: css`
@import './two.css' layer(two);
`,
base: '/root',
}
}
case './two.css': {
return {
base: '/root',
content: css`
@import './three.css' layer(three);
`,
base: '/root',
}
}
case './three.css': {
return {
base: '/root',
content: css`
.foo {
color: red;
@ -4400,10 +4407,11 @@ describe('`@reference "…" imports`', () => {
}
}
`,
base: '/root',
}
}
}
throw new Error('unreachable')
}
await expect(
@ -4438,6 +4446,7 @@ describe('`@reference "…" imports`', () => {
test('supports `@import "…" reference` syntax', async () => {
let loadStylesheet = async () => {
return {
base: '/root/foo',
content: css`
.foo {
color: red;
@ -4450,7 +4459,6 @@ describe('`@reference "…" imports`', () => {
}
@custom-variant hocus (&:hover, &:focus);
`,
base: '/root/foo',
}
}

View file

@ -56,8 +56,17 @@ type CompileOptions = {
id: string,
base: string,
resourceHint: 'plugin' | 'config',
) => Promise<{ module: Plugin | Config; base: string }>
loadStylesheet?: (id: string, base: string) => Promise<{ content: string; base: string }>
) => Promise<{
base: string
module: Plugin | Config
}>
loadStylesheet?: (
id: string,
base: string,
) => Promise<{
base: string
content: string
}>
}
function throwOnLoadModule(): never {
@ -593,8 +602,8 @@ async function parseCss(
for (let [key, value] of designSystem.theme.entries()) {
if (value.options & ThemeOptions.REFERENCE) continue
nodes.push(decl(escape(key), value.value))
let node = decl(escape(key), value.value)
nodes.push(node)
}
let keyframesRules = designSystem.theme.getKeyframes()

View file

@ -2526,7 +2526,7 @@ test('matchVariant sorts deterministically', async () => {
for (let classList of classLists) {
let output = await compileCss('@tailwind utilities; @plugin "./plugin.js";', classList, {
loadModule(id: string) {
async loadModule(id: string) {
return {
base: '/',
module: createPlugin(({ matchVariant }) => {