Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { Jiti } from 'jiti/lib/types'
import { assets } from './assets'
import { plugins } from './plugins'
import { AstNode, cloneAstNode, parse } from '@tailwindcss/language-service/src/css'
import { walk, WalkAction } from '@tailwindcss/language-service/src/util/walk'

const HAS_V4_IMPORT = /@import\s*(?:'tailwindcss'|"tailwindcss")/
const HAS_V4_THEME = /@theme\s*\{/
Expand Down Expand Up @@ -240,7 +241,27 @@ export async function loadDesignSystem(
let str = css[idx]

if (Array.isArray(str)) {
cache[cls] = str
let ast = str.map(cloneAstNode)

// Rewrite at-rules with zero nodes to act as if they have no body
//
// At a future time we'll only do this conditionally for earlier
// Tailwind CSS v4 versions. We have to clone the AST *first*
// because if the AST was shared with Tailwind CSS internals
// and we mutated it we could break things.
walk(ast, (node) => {
if (node.kind !== 'at-rule') return WalkAction.Continue
if (node.nodes === null) return WalkAction.Continue
if (node.nodes.length !== 0) return WalkAction.Continue

// Treat
node.nodes = null

return WalkAction.Continue
})

cache[cls] = ast

continue
}

Expand Down
103 changes: 5 additions & 98 deletions packages/tailwindcss-language-service/src/css/ast.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { parseAtRule } from './parse'
import type { SourceLocation } from './source'
import type { VisitContext } from '../util/walk'
import { parseAtRule } from './parse'

const AT_SIGN = 0x40

Expand All @@ -17,7 +16,7 @@ export type AtRule = {
kind: 'at-rule'
name: string
params: string
nodes: AstNode[]
nodes: AstNode[] | null

src?: SourceLocation
dst?: SourceLocation
Expand Down Expand Up @@ -70,7 +69,7 @@ export function styleRule(selector: string, nodes: AstNode[] = []): StyleRule {
}
}

export function atRule(name: string, params: string = '', nodes: AstNode[] = []): AtRule {
export function atRule(name: string, params: string = '', nodes: AstNode[] | null = []): AtRule {
return {
kind: 'at-rule',
name,
Expand All @@ -79,12 +78,12 @@ export function atRule(name: string, params: string = '', nodes: AstNode[] = [])
}
}

export function rule(selector: string, nodes: AstNode[] = []): StyleRule | AtRule {
export function rule(selector: string, nodes: AstNode[] | null = []): StyleRule | AtRule {
if (selector.charCodeAt(0) === AT_SIGN) {
return parseAtRule(selector, nodes)
}

return styleRule(selector, nodes)
return styleRule(selector, nodes ?? [])
}

export function decl(property: string, value: string | undefined, important = false): Declaration {
Expand Down Expand Up @@ -117,95 +116,3 @@ export function atRoot(nodes: AstNode[]): AtRoot {
nodes,
}
}

export function cloneAstNode<T extends AstNode>(node: T): T {
switch (node.kind) {
case 'rule':
return {
kind: node.kind,
selector: node.selector,
nodes: node.nodes.map(cloneAstNode),
src: node.src,
dst: node.dst,
} satisfies StyleRule as T

case 'at-rule':
return {
kind: node.kind,
name: node.name,
params: node.params,
nodes: node.nodes.map(cloneAstNode),
src: node.src,
dst: node.dst,
} satisfies AtRule as T

case 'at-root':
return {
kind: node.kind,
nodes: node.nodes.map(cloneAstNode),
src: node.src,
dst: node.dst,
} satisfies AtRoot as T

case 'context':
return {
kind: node.kind,
context: { ...node.context },
nodes: node.nodes.map(cloneAstNode),
src: node.src,
dst: node.dst,
} satisfies Context as T

case 'declaration':
return {
kind: node.kind,
property: node.property,
value: node.value,
important: node.important,
src: node.src,
dst: node.dst,
} satisfies Declaration as T

case 'comment':
return {
kind: node.kind,
value: node.value,
src: node.src,
dst: node.dst,
} satisfies Comment as T

default:
node satisfies never
throw new Error(`Unknown node kind: ${(node as any).kind}`)
}
}

export function cssContext(
ctx: VisitContext<AstNode>,
): VisitContext<AstNode> & { context: Record<string, string | boolean> } {
return {
depth: ctx.depth,
get context() {
let context: Record<string, string | boolean> = {}
for (let child of ctx.path()) {
if (child.kind === 'context') {
Object.assign(context, child.context)
}
}

// Once computed, we never need to compute this again
Object.defineProperty(this, 'context', { value: context })
return context
},
get parent() {
let parent = (this.path().pop() as Extract<AstNode, { nodes: AstNode[] }>) ?? null

// Once computed, we never need to compute this again
Object.defineProperty(this, 'parent', { value: parent })
return parent
},
path() {
return ctx.path().filter((n) => n.kind !== 'context')
},
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function cloneAstNode<T extends AstNode>(node: T): T {
kind: node.kind,
name: node.name,
params: node.params,
nodes: node.nodes.map(cloneAstNode),
nodes: node.nodes?.map(cloneAstNode) ?? null,
src: node.src,
dst: node.dst,
} satisfies AtRule as T
Expand Down
32 changes: 32 additions & 0 deletions packages/tailwindcss-language-service/src/css/css-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { VisitContext } from '../util/walk'
import type { AstNode } from './ast'

export function cssContext(
ctx: VisitContext<AstNode>,
): VisitContext<AstNode> & { context: Record<string, string | boolean> } {
return {
depth: ctx.depth,
get context() {
let context: Record<string, string | boolean> = {}
for (let child of ctx.path()) {
if (child.kind === 'context') {
Object.assign(context, child.context)
}
}

// Once computed, we never need to compute this again
Object.defineProperty(this, 'context', { value: context })
return context
},
get parent() {
let parent = (this.path().pop() as Extract<AstNode, { nodes: AstNode[] }>) ?? null

// Once computed, we never need to compute this again
Object.defineProperty(this, 'parent', { value: parent })
return parent
},
path() {
return ctx.path().filter((n) => n.kind !== 'context')
},
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ export function fromPostCSSAst(root: postcss.Root): AstNode[] {

// AtRule
else if (node.type === 'atrule') {
let astNode = atRule(`@${node.name}`, node.params)
let astNode = atRule(`@${node.name}`, node.params, node.nodes ? [] : null)
astNode.src = toSource(node)
node.each((child) => transform(child, astNode.nodes))
node.each((child) => transform(child, astNode.nodes!))
parent.push(astNode)
}

Expand Down
1 change: 1 addition & 0 deletions packages/tailwindcss-language-service/src/css/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './ast'
export * from './source'
export { parse } from './parse'
export { cloneAstNode } from './clone-ast-node'
export { fromPostCSSAst } from './from-postcss-ast'
export { toPostCSSAst } from './to-postcss-ast'
export { toCss } from './to-css'
8 changes: 7 additions & 1 deletion packages/tailwindcss-language-service/src/css/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ export function parse(input: string, opts?: ParseOptions): AstNode[] {
}

if (parent) {
parent.nodes ??= []
parent.nodes.push(declaration)
} else {
ast.push(declaration)
Expand All @@ -303,6 +304,7 @@ export function parse(input: string, opts?: ParseOptions): AstNode[] {

// At-rule is nested inside of a rule, attach it to the parent.
if (parent) {
parent.nodes ??= []
parent.nodes.push(node)
}

Expand Down Expand Up @@ -343,6 +345,7 @@ export function parse(input: string, opts?: ParseOptions): AstNode[] {
}

if (parent) {
parent.nodes ??= []
parent.nodes.push(declaration)
} else {
ast.push(declaration)
Expand All @@ -369,6 +372,7 @@ export function parse(input: string, opts?: ParseOptions): AstNode[] {

// Attach the rule to the parent in case it's nested.
if (parent) {
parent.nodes ??= []
parent.nodes.push(node)
}

Expand Down Expand Up @@ -421,6 +425,7 @@ export function parse(input: string, opts?: ParseOptions): AstNode[] {

// At-rule is nested inside of a rule, attach it to the parent.
if (parent) {
parent.nodes ??= []
parent.nodes.push(node)
}

Expand Down Expand Up @@ -460,6 +465,7 @@ export function parse(input: string, opts?: ParseOptions): AstNode[] {
node.dst = [source, bufferStart, i]
}

parent.nodes ??= []
parent.nodes.push(node)
}
}
Expand Down Expand Up @@ -548,7 +554,7 @@ export function parse(input: string, opts?: ParseOptions): AstNode[] {
return ast
}

export function parseAtRule(buffer: string, nodes: AstNode[] = []): AtRule {
export function parseAtRule(buffer: string, nodes: AstNode[] | null = []): AtRule {
let name = buffer
let params = ''

Expand Down
2 changes: 1 addition & 1 deletion packages/tailwindcss-language-service/src/css/to-css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export function toCss(ast: AstNode[], track?: boolean): string {
// ```css
// @layer base, components, utilities;
// ```
if (node.nodes.length === 0) {
if (!node.nodes) {
let css = `${indent}${node.name} ${node.params};\n`

if (track) {
Expand Down
10 changes: 7 additions & 3 deletions packages/tailwindcss-language-service/src/css/to-postcss-ast.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as postcss from 'postcss'
import { atRule, comment, decl, styleRule, type AstNode } from './ast'
import type { AstNode } from './ast'
import type { Source, SourceLocation } from './source'
import { DefaultMap } from '../util/default-map'
import { createLineTable, LineTable } from '../util/line-table'
Expand Down Expand Up @@ -85,11 +85,15 @@ export function toPostCSSAst(ast: AstNode[], source?: postcss.Source): postcss.R

// AtRule
else if (node.kind === 'at-rule') {
let astNode = postcss.atRule({ name: node.name.slice(1), params: node.params })
let astNode = postcss.atRule({
name: node.name.slice(1),
params: node.params,
...(node.nodes ? { nodes: [] } : {}),
})
updateSource(astNode, node.src)
astNode.raws.semicolon = true
parent.append(astNode)
for (let child of node.nodes) {
for (let child of node.nodes ?? []) {
transform(child, astNode)
}
}
Expand Down