Skip to content

Commit 19bb723

Browse files
committed
Add tests
1 parent 3f45f85 commit 19bb723

File tree

9 files changed

+724
-4
lines changed

9 files changed

+724
-4
lines changed

Diff for: packages/tailwindcss-language-server/tests/utils/configuration.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
type Settings,
44
} from '@tailwindcss/language-service/src/util/state'
55
import { URI } from 'vscode-uri'
6-
import type { DeepPartial } from './types'
6+
import type { DeepPartial } from '@tailwindcss/language-service/src/types'
77
import { CacheMap } from '../../src/cache-map'
88
import deepmerge from 'deepmerge'
99

Diff for: packages/tailwindcss-language-service/package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,15 @@
4848
"@types/picomatch": "^2.3.3",
4949
"@types/stringify-object": "^4.0.5",
5050
"dedent": "^1.5.3",
51+
"deepmerge": "4.2.2",
5152
"esbuild": "^0.25.0",
5253
"esbuild-node-externals": "^1.9.0",
5354
"minimist": "^1.2.8",
5455
"picomatch": "^4.0.1",
56+
"tailwindcss-v4": "npm:[email protected]",
5557
"tslib": "2.2.0",
5658
"typescript": "^5.3.3",
57-
"vitest": "^3.0.9"
59+
"vitest": "^3.0.9",
60+
"vscode-uri": "3.0.2"
5861
}
5962
}
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export class CacheMap<TKey = string, TValue = any> extends Map<TKey, TValue> {
2+
remember(key: TKey, factory: (key: TKey) => TValue): TValue {
3+
let value = super.get(key)
4+
if (!value) {
5+
value = factory(key)
6+
this.set(key, value)
7+
}
8+
return value!
9+
}
10+
}
+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { Settings, State } from '../src'
2+
import postcss from 'postcss'
3+
import { createLanguageService, createState, getDefaultTailwindSettings } from '../src'
4+
import { supportedFeatures } from '../src/features'
5+
import { TextDocument } from 'vscode-languageserver-textdocument'
6+
import { URI, Utils as URIUtils } from 'vscode-uri'
7+
import { createConfiguration } from './configuration'
8+
import { DeepPartial } from '../src/types'
9+
10+
export interface ClientOptions {
11+
config:
12+
| { kind: 'css'; content: string }
13+
| { kind: 'module'; content: any }
14+
| { kind: 'custom'; content: (state: State) => State }
15+
}
16+
17+
export interface DocumentDescriptor {
18+
/**
19+
* The language the document is written in
20+
*/
21+
lang: string
22+
23+
/**
24+
* The content of the document
25+
*/
26+
text: string
27+
28+
/**
29+
* The name or file path to the document
30+
*
31+
* By default a unique path is generated at the root of the workspace
32+
*/
33+
name?: string
34+
35+
/**
36+
* Custom settings / config for this document
37+
*/
38+
settings?: DeepPartial<Settings>
39+
}
40+
41+
export async function createClient(options: ClientOptions) {
42+
if (options.config.kind !== 'css') {
43+
throw new Error('unsupported')
44+
}
45+
46+
let { version } = require('tailwindcss-v4/package.json')
47+
let tailwindcss = await import('tailwindcss-v4')
48+
49+
let design = await tailwindcss.__unstable__loadDesignSystem(options.config.content)
50+
51+
// Step 4: Augment the design system with some additional APIs that the LSP needs
52+
Object.assign(design, {
53+
dependencies: () => [],
54+
55+
// TODOs:
56+
//
57+
// 1. Remove PostCSS parsing — its roughly 60% of the processing time
58+
// ex: compiling 19k classes take 650ms and 400ms of that is PostCSS
59+
//
60+
// - Replace `candidatesToCss` with a `candidatesToAst` API
61+
// First step would be to convert to a PostCSS AST by transforming the nodes directly
62+
// Then it would be to drop the PostCSS AST representation entirely in all v4 code paths
63+
compile(classes: string[]): (postcss.Root | null)[] {
64+
let css = design.candidatesToCss(classes)
65+
let errors: any[] = []
66+
67+
let roots = css.map((str) => {
68+
if (str === null) return postcss.root()
69+
70+
try {
71+
return postcss.parse(str.trimEnd())
72+
} catch (err) {
73+
errors.push(err)
74+
return postcss.root()
75+
}
76+
})
77+
78+
if (errors.length > 0) {
79+
console.error(JSON.stringify(errors))
80+
}
81+
82+
return roots
83+
},
84+
85+
toCss(nodes: postcss.Root | postcss.Node[]): string {
86+
return Array.isArray(nodes)
87+
? postcss.root({ nodes }).toString().trim()
88+
: nodes.toString().trim()
89+
},
90+
})
91+
92+
let config = createConfiguration()
93+
94+
let state = createState({
95+
v4: true,
96+
designSystem: design as any,
97+
features: supportedFeatures(version, tailwindcss),
98+
editor: {
99+
getConfiguration: async (uri) => config.get(uri),
100+
},
101+
})
102+
103+
let service = createLanguageService({
104+
state: () => state,
105+
fs: {
106+
document: async () => null,
107+
resolve: async () => null,
108+
readDirectory: async () => [],
109+
},
110+
})
111+
112+
let index = 0
113+
function open(desc: DocumentDescriptor) {
114+
let uri = URIUtils.resolvePath(
115+
URI.parse('file://projects/root'),
116+
desc.name ? desc.name : `file-${++index}.${desc.lang}`,
117+
).toString()
118+
119+
if (desc.settings) {
120+
config.set(uri, desc.settings)
121+
}
122+
123+
return service.open(TextDocument.create(uri, desc.lang, 1, desc.text))
124+
}
125+
126+
return {
127+
...service,
128+
open,
129+
}
130+
}
+194
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import { test, expect, describe } from 'vitest'
2+
import dedent from 'dedent'
3+
import { createClient } from './client'
4+
5+
const css = dedent
6+
7+
const rgb = (red: number, green: number, blue: number, alpha: number = 1) => ({
8+
red,
9+
green,
10+
blue,
11+
alpha,
12+
})
13+
14+
const range = (startLine: number, startCol: number, endLine: number, endCol: number) => ({
15+
start: { line: startLine, character: startCol },
16+
end: { line: endLine, character: endCol },
17+
})
18+
19+
describe('v4', async () => {
20+
let client = await createClient({
21+
config: {
22+
kind: 'css',
23+
content: css`
24+
@theme {
25+
--color-black: #000;
26+
--color-primary: #f00;
27+
--color-light-dark: light-dark(#ff0000, #0000ff);
28+
}
29+
`,
30+
},
31+
})
32+
33+
test('named', async () => {
34+
let doc = await client.open({
35+
lang: 'html',
36+
text: '<div class="bg-primary">',
37+
})
38+
39+
expect(await doc.documentColors()).toEqual([
40+
//
41+
{ range: range(0, 12, 0, 22), color: rgb(1, 0, 0) },
42+
])
43+
})
44+
45+
test('named + opacity modifier', async () => {
46+
let doc = await client.open({
47+
lang: 'html',
48+
text: '<div class="bg-primary/20">',
49+
})
50+
51+
expect(await doc.documentColors()).toEqual([
52+
//
53+
{ range: range(0, 12, 0, 25), color: rgb(1, 0, 0, 0.2) },
54+
])
55+
})
56+
57+
test('named + arbitrary opacity modifier', async () => {
58+
let doc = await client.open({
59+
lang: 'html',
60+
text: '<div class="bg-primary/[0.125]">',
61+
})
62+
63+
expect(await doc.documentColors()).toEqual([
64+
//
65+
{ range: range(0, 12, 0, 30), color: rgb(1, 0, 0, 0.13) },
66+
])
67+
})
68+
69+
test('arbitrary value', async () => {
70+
let doc = await client.open({
71+
lang: 'html',
72+
text: '<div class="bg-[red]">',
73+
})
74+
75+
expect(await doc.documentColors()).toEqual([
76+
//
77+
{ range: range(0, 12, 0, 20), color: rgb(1, 0, 0) },
78+
])
79+
})
80+
81+
test('arbitrary value + opacity modifier', async () => {
82+
let doc = await client.open({
83+
lang: 'html',
84+
text: '<div class="bg-[red]/20">',
85+
})
86+
87+
expect(await doc.documentColors()).toEqual([
88+
//
89+
{ range: range(0, 12, 0, 23), color: rgb(1, 0, 0, 0.2) },
90+
])
91+
})
92+
93+
test('arbitrary value + arbitrary opacity modifier', async () => {
94+
let doc = await client.open({
95+
lang: 'html',
96+
text: '<div class="bg-[red]/[0.125]">',
97+
})
98+
99+
expect(await doc.documentColors()).toEqual([
100+
//
101+
{ range: range(0, 12, 0, 28), color: rgb(1, 0, 0, 0.13) },
102+
])
103+
})
104+
105+
test('an opacity modifier of zero is ignored', async () => {
106+
let doc = await client.open({
107+
lang: 'html',
108+
text: '<div class="bg-primary/0 bg-[red]/0 bg-primary/[0] bg-[red]/[0]">',
109+
})
110+
111+
expect(await doc.documentColors()).toEqual([])
112+
})
113+
114+
test('oklch colors are supported', async () => {
115+
let doc = await client.open({
116+
lang: 'html',
117+
text: '<div class="bg-[oklch(60%_0.25_25)]',
118+
})
119+
120+
expect(await doc.documentColors()).toEqual([
121+
//
122+
{ range: range(0, 12, 0, 35), color: rgb(0.9475942429386454, 0, 0.14005415620741646) },
123+
])
124+
})
125+
126+
test('gradient colors are supported', async () => {
127+
let doc = await client.open({
128+
lang: 'html',
129+
text: '<div class="from-black from-black/50 via-black via-black/50 to-black to-black/50">',
130+
})
131+
132+
expect(await doc.documentColors()).toEqual([
133+
// from-black from-black/50
134+
{ range: range(0, 12, 0, 22), color: rgb(0, 0, 0) },
135+
{ range: range(0, 23, 0, 36), color: rgb(0, 0, 0, 0.5) },
136+
137+
// via-black via-black/50
138+
{ range: range(0, 37, 0, 46), color: rgb(0, 0, 0) },
139+
{ range: range(0, 47, 0, 59), color: rgb(0, 0, 0, 0.5) },
140+
141+
// to-black to-black/50
142+
{ range: range(0, 60, 0, 68), color: rgb(0, 0, 0) },
143+
{ range: range(0, 69, 0, 80), color: rgb(0, 0, 0, 0.5) },
144+
])
145+
})
146+
147+
test('light-dark() resolves to the light color', async () => {
148+
let doc = await client.open({
149+
lang: 'html',
150+
text: '<div class="bg-light-dark">',
151+
})
152+
153+
let colors = await doc.documentColors()
154+
155+
expect(colors).toEqual([
156+
//
157+
{ range: range(0, 12, 0, 25), color: rgb(1, 0, 0, 1) },
158+
])
159+
})
160+
161+
test('colors are recursively resolved from the theme', async () => {
162+
let client = await createClient({
163+
config: {
164+
kind: 'css',
165+
content: css`
166+
@theme {
167+
--color-primary: #ff0000;
168+
--color-level-1: var(--color-primary);
169+
--color-level-2: var(--color-level-1);
170+
--color-level-3: var(--color-level-2);
171+
--color-level-4: var(--color-level-3);
172+
--color-level-5: var(--color-level-4);
173+
}
174+
`,
175+
},
176+
})
177+
178+
let doc = await client.open({
179+
lang: 'html',
180+
text: '<div class="bg-primary bg-level-1 bg-level-2 bg-level-3 bg-level-4 bg-level-5">',
181+
})
182+
183+
let colors = await doc.documentColors()
184+
185+
expect(colors).toEqual([
186+
{ range: range(0, 12, 0, 22), color: rgb(1, 0, 0, 1) },
187+
{ range: range(0, 23, 0, 33), color: rgb(1, 0, 0, 1) },
188+
{ range: range(0, 34, 0, 44), color: rgb(1, 0, 0, 1) },
189+
{ range: range(0, 45, 0, 55), color: rgb(1, 0, 0, 1) },
190+
{ range: range(0, 56, 0, 66), color: rgb(1, 0, 0, 1) },
191+
{ range: range(0, 67, 0, 77), color: rgb(1, 0, 0, 1) },
192+
])
193+
})
194+
})

0 commit comments

Comments
 (0)