Skip to content

Commit 3fdd806

Browse files
committed
Add support for v4 fallback
This does not currently support importing / requiring JS APIs when configs and/or plugins are used.
1 parent 1b7a786 commit 3fdd806

File tree

9 files changed

+412
-2
lines changed

9 files changed

+412
-2
lines changed

packages/tailwindcss-language-server/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"color-name": "1.1.4",
6060
"culori": "^4.0.1",
6161
"debounce": "1.2.0",
62+
"dedent": "^1.5.3",
6263
"deepmerge": "4.2.2",
6364
"dlv": "1.1.3",
6465
"dset": "3.1.2",
@@ -81,6 +82,7 @@
8182
"rimraf": "3.0.2",
8283
"stack-trace": "0.0.10",
8384
"tailwindcss": "3.4.17",
85+
"tailwindcss-v4": "npm:[email protected]",
8486
"tsconfck": "^3.1.4",
8587
"tsconfig-paths": "^4.2.0",
8688
"typescript": "5.3.3",

packages/tailwindcss-language-server/src/project-locator.ts

+18
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,24 @@ export class ProjectLocator {
454454
}
455455
} catch {}
456456

457+
// A local version of Tailwind CSS was not found so we need to use the
458+
// fallback bundled with the language server. This is especially important
459+
// for projects using the standalone CLI.
460+
461+
// This is a v4-style CSS config
462+
if (config.type === 'css') {
463+
let { version } = require('tailwindcss-v4/package.json')
464+
// @ts-ignore
465+
let mod = await import('tailwindcss-v4')
466+
let features = supportedFeatures(version, mod)
467+
468+
return {
469+
version,
470+
features,
471+
isDefaultVersion: true,
472+
}
473+
}
474+
457475
let { version } = require('tailwindcss/package.json')
458476
let mod = require('tailwindcss')
459477
let features = supportedFeatures(version, mod)

packages/tailwindcss-language-server/src/projects.ts

+29-2
Original file line numberDiff line numberDiff line change
@@ -489,8 +489,8 @@ export async function createProjectService(
489489
log('CSS-based configuration is not supported before Tailwind CSS v4')
490490
state.enabled = false
491491
enabled = false
492-
// CSS-based configuration is not supported before Tailwind CSS v4 so bail
493-
// TODO: Fall back to built-in version of v4
492+
493+
// The fallback to a bundled v4 is in the catch block
494494
return
495495
}
496496

@@ -673,6 +673,31 @@ export async function createProjectService(
673673
} catch (_) {}
674674
}
675675
} catch (error) {
676+
if (projectConfig.config.source === 'css') {
677+
// @ts-ignore
678+
let tailwindcss = await import('tailwindcss-v4')
679+
let tailwindcssVersion = require('tailwindcss-v4/package.json').version
680+
let features = supportedFeatures(tailwindcssVersion, tailwindcss)
681+
682+
log('Failed to load workspace modules.')
683+
log(`Using bundled version of \`tailwindcss\`: v${tailwindcssVersion}`)
684+
685+
state.configPath = configPath
686+
state.version = tailwindcssVersion
687+
state.isCssConfig = true
688+
state.v4 = true
689+
state.v4Fallback = true
690+
state.jit = true
691+
state.modules = {
692+
tailwindcss: { version: tailwindcssVersion, module: tailwindcss },
693+
postcss: { version: null, module: null },
694+
resolveConfig: { module: null },
695+
loadConfig: { module: null },
696+
}
697+
698+
return tryRebuild()
699+
}
700+
676701
let util = await import('node:util')
677702

678703
console.error(util.format(error))
@@ -786,6 +811,7 @@ export async function createProjectService(
786811
state.modules.tailwindcss.module,
787812
state.configPath,
788813
css,
814+
state.v4Fallback ?? false,
789815
)
790816

791817
state.designSystem = designSystem
@@ -1063,6 +1089,7 @@ export async function createProjectService(
10631089
state.modules.tailwindcss.module,
10641090
state.configPath,
10651091
css,
1092+
state.v4Fallback ?? false,
10661093
)
10671094
} catch (err) {
10681095
console.error(err)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { afterAll, onTestFinished, test, TestOptions } from 'vitest'
2+
import * as fs from 'node:fs/promises'
3+
import * as path from 'node:path'
4+
import * as proc from 'node:child_process'
5+
import dedent from 'dedent'
6+
7+
export interface TestUtils {
8+
/** The "cwd" for this test */
9+
root: string
10+
}
11+
12+
export interface Storage {
13+
/** A list of files and their content */
14+
[filePath: string]: string | Uint8Array
15+
}
16+
17+
export interface TestConfig<Extras extends {}> {
18+
name: string
19+
fs: Storage
20+
prepare?(utils: TestUtils): Promise<Extras>
21+
handle(utils: TestUtils & Extras): void | Promise<void>
22+
23+
options?: TestOptions
24+
}
25+
26+
export function defineTest<T>(config: TestConfig<T>) {
27+
return test(config.name, config.options ?? {}, async ({ expect }) => {
28+
let utils = await setup(config)
29+
let extras = await config.prepare?.(utils)
30+
31+
await config.handle({
32+
...utils,
33+
...extras,
34+
})
35+
})
36+
}
37+
38+
async function setup<T>(config: TestConfig<T>): Promise<TestUtils> {
39+
let randomId = Math.random().toString(36).substring(7)
40+
41+
let baseDir = path.resolve(process.cwd(), `../../.debug/${randomId}`)
42+
let doneDir = path.resolve(process.cwd(), `../../.debug/${randomId}-done`)
43+
44+
await fs.mkdir(baseDir, { recursive: true })
45+
46+
await prepareFileSystem(baseDir, config.fs)
47+
await installDependencies(baseDir, config.fs)
48+
49+
onTestFinished(async (result) => {
50+
// Once done, move all the files to a new location
51+
await fs.rename(baseDir, doneDir)
52+
53+
if (result.state === 'fail') return
54+
55+
if (path.sep === '\\') return
56+
57+
// Remove the directory on *nix systems. Recursive removal on Windows will
58+
// randomly fail b/c its slow and buggy.
59+
await fs.rm(doneDir, { recursive: true })
60+
})
61+
62+
return {
63+
root: baseDir,
64+
}
65+
}
66+
67+
async function prepareFileSystem(base: string, storage: Storage) {
68+
// Create a temporary directory to store the test files
69+
await fs.mkdir(base, { recursive: true })
70+
71+
// Write the files to disk
72+
for (let [filepath, content] of Object.entries(storage)) {
73+
let fullPath = path.resolve(base, filepath)
74+
await fs.mkdir(path.dirname(fullPath), { recursive: true })
75+
await fs.writeFile(fullPath, content, { encoding: 'utf-8' })
76+
}
77+
}
78+
79+
async function installDependencies(base: string, storage: Storage) {
80+
for (let filepath of Object.keys(storage)) {
81+
if (!filepath.endsWith('package.json')) continue
82+
83+
let pkgDir = path.dirname(filepath)
84+
let basePath = path.resolve(pkgDir, base)
85+
86+
await installDependenciesIn(basePath)
87+
}
88+
}
89+
90+
async function installDependenciesIn(dir: string) {
91+
console.log(`Installing dependencies in ${dir}`)
92+
93+
await new Promise((resolve, reject) => {
94+
proc.exec('npm install --package-lock=false', { cwd: dir }, (err, res) => {
95+
if (err) {
96+
reject(err)
97+
} else {
98+
resolve(res)
99+
}
100+
})
101+
})
102+
}
103+
104+
export const css = dedent
105+
export const html = dedent
106+
export const js = dedent
107+
export const json = dedent
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import index from 'tailwindcss-v4/index.css'
2+
import preflight from 'tailwindcss-v4/preflight.css'
3+
import theme from 'tailwindcss-v4/theme.css'
4+
import utilities from 'tailwindcss-v4/utilities.css'
5+
6+
export const assets = {
7+
tailwindcss: index,
8+
'tailwindcss/index': index,
9+
'tailwindcss/index.css': index,
10+
11+
'tailwindcss/preflight': preflight,
12+
'tailwindcss/preflight.css': preflight,
13+
14+
'tailwindcss/theme': theme,
15+
'tailwindcss/theme.css': theme,
16+
17+
'tailwindcss/utilities': utilities,
18+
'tailwindcss/utilities.css': utilities,
19+
}

packages/tailwindcss-language-server/src/util/v4/design-system.ts

+8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { resolveCssImports } from '../../css'
88
import { Resolver } from '../../resolver'
99
import { pathToFileURL } from '../../utils'
1010
import type { Jiti } from 'jiti/lib/types'
11+
import { assets } from './assets'
1112

1213
const HAS_V4_IMPORT = /@import\s*(?:'tailwindcss'|"tailwindcss")/
1314
const HAS_V4_THEME = /@theme\s*\{/
@@ -79,6 +80,7 @@ export async function loadDesignSystem(
7980
tailwindcss: any,
8081
filepath: string,
8182
css: string,
83+
isFallback: boolean,
8284
): Promise<DesignSystem | null> {
8385
// This isn't a v4 project
8486
if (!tailwindcss.__unstable__loadDesignSystem) return null
@@ -151,6 +153,12 @@ export async function loadDesignSystem(
151153
content: await fs.readFile(resolved, 'utf-8'),
152154
}
153155
} catch (err) {
156+
if (isFallback && id in assets) {
157+
console.error(`Loading fallback stylesheet for: ${id}`)
158+
159+
return { base, content: assets[id] }
160+
}
161+
154162
console.error(`Unable to load stylesheet: ${id}`, err)
155163
return { base, content: '' }
156164
}

0 commit comments

Comments
 (0)