Skip to content

Commit c2c7cc7

Browse files
Support loading plugins in CSS (#1044)
This adds support for `@plugin` and `@config` in v4 which is coming in v4.0.0-alpha.21
1 parent 3c3c193 commit c2c7cc7

File tree

20 files changed

+281
-26
lines changed

20 files changed

+281
-26
lines changed

packages/tailwindcss-language-server/src/lib/env.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Module from 'node:module'
22
import * as path from 'node:path'
3-
import resolveFrom from '../util/resolveFrom'
3+
import { resolveFrom } from '../util/resolveFrom'
44

55
process.env.TAILWIND_MODE = 'build'
66
process.env.TAILWIND_DISABLE_TOUCH = 'true'

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

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { AtRule, Message } from 'postcss'
1111
import { type DocumentSelector, DocumentSelectorPriority } from './projects'
1212
import { CacheMap } from './cache-map'
1313
import { getPackageRoot } from './util/get-package-root'
14-
import resolveFrom from './util/resolveFrom'
14+
import { resolveFrom } from './util/resolveFrom'
1515
import { type Feature, supportedFeatures } from '@tailwindcss/language-service/src/features'
1616
import { extractSourceDirectives, resolveCssImports } from './css'
1717
import { normalizeDriveLetter, normalizePath, pathToFileURL } from './utils'
@@ -132,15 +132,18 @@ export class ProjectLocator {
132132

133133
console.log(JSON.stringify({ tailwind }))
134134

135-
// A JS/TS config file was loaded from an `@config`` directive in a CSS file
135+
// A JS/TS config file was loaded from an `@config` directive in a CSS file
136+
// This is only relevant for v3 projects so we'll do some feature detection
137+
// to verify if this is supported in the current version of Tailwind.
136138
if (config.type === 'js' && config.source === 'css') {
137139
// We only allow local versions of Tailwind to use `@config` directives
138140
if (tailwind.isDefaultVersion) {
139141
return null
140142
}
141143

142-
// This version of Tailwind doesn't support `@config` directives
143-
if (!tailwind.features.includes('css-at-config')) {
144+
// This version of Tailwind doesn't support considering `@config` directives
145+
// as a project on their own.
146+
if (!tailwind.features.includes('css-at-config-as-project')) {
144147
return null
145148
}
146149
}
@@ -310,8 +313,12 @@ export class ProjectLocator {
310313
// If the CSS file couldn't be read for some reason, skip it
311314
if (!file.content) continue
312315

316+
// Look for `@import`, `@tailwind`, `@theme`, `@config`, etc…
317+
if (!file.isMaybeTailwindRelated()) continue
318+
313319
// Find `@config` directives in CSS files and resolve them to the actual
314-
// config file that they point to.
320+
// config file that they point to. This is only relevant for v3 which
321+
// we'll verify after config resolution.
315322
let configPath = file.configPathInCss()
316323
if (configPath) {
317324
// We don't need the content for this file anymore
@@ -327,14 +334,9 @@ export class ProjectLocator {
327334
content: [],
328335
})),
329336
)
330-
continue
331337
}
332338

333-
// Look for `@import` or `@tailwind` directives
334-
if (file.isMaybeTailwindRelated()) {
335-
imports.push(file)
336-
continue
337-
}
339+
imports.push(file)
338340
}
339341

340342
// Resolve imports in all the CSS files
@@ -636,6 +638,9 @@ class FileEntry {
636638
* Look for `@config` directives in a CSS file and return the path to the config
637639
* file that it points to. This path is (possibly) relative to the CSS file so
638640
* it must be resolved to an absolute path before returning.
641+
*
642+
* This is only useful for v3 projects. While v4 can use `@config` directives
643+
* the CSS file is still considered the "config" rather than the JS file.
639644
*/
640645
configPathInCss(): string | null {
641646
if (!this.content) return null
@@ -649,21 +654,21 @@ class FileEntry {
649654
}
650655

651656
/**
652-
* Look for `@import` or `@tailwind` directives in a CSS file. This means that
657+
* Look for tailwind-specific directives in a CSS file. This means that it
653658
* participates in the CSS "graph" for the project and we need to traverse
654659
* the graph to find all the CSS files that are considered entrypoints.
655660
*/
656661
isMaybeTailwindRelated(): boolean {
657662
if (!this.content) return false
658663

659-
let HAS_IMPORT = /@import\s*(?<config>'[^']+'|"[^"]+");/
664+
let HAS_IMPORT = /@import\s*('[^']+'|"[^"]+");/
660665
let HAS_TAILWIND = /@tailwind\s*[^;]+;/
661-
let HAS_THEME = /@theme\s*\{/
666+
let HAS_DIRECTIVE = /@(theme|plugin|config|utility|variant|apply)\s*[^;{]+[;{]/
662667

663668
return (
664669
HAS_IMPORT.test(this.content) ||
665670
HAS_TAILWIND.test(this.content) ||
666-
HAS_THEME.test(this.content)
671+
HAS_DIRECTIVE.test(this.content)
667672
)
668673
}
669674
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import * as path from 'path'
2424
import * as fs from 'fs'
2525
import findUp from 'find-up'
2626
import picomatch from 'picomatch'
27-
import resolveFrom, { setPnpApi } from './util/resolveFrom'
27+
import { resolveFrom, setPnpApi } from './util/resolveFrom'
2828
import type { AtRule, Container, Node, Result } from 'postcss'
2929
import Hook from './lib/hook'
3030
import * as semver from '@tailwindcss/language-service/src/util/semver'

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import normalizePath from 'normalize-path'
3636
import * as path from 'path'
3737
import type * as chokidar from 'chokidar'
3838
import picomatch from 'picomatch'
39-
import resolveFrom from './util/resolveFrom'
39+
import { resolveFrom } from './util/resolveFrom'
4040
import * as parcel from './watcher/index.js'
4141
import { equal } from '@tailwindcss/language-service/src/util/array'
4242
import { CONFIG_GLOB, CSS_GLOB, PACKAGE_LOCK_GLOB } from './lib/constants'

packages/tailwindcss-language-server/src/util/resolveFrom.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function setPnpApi(newPnpApi: any): void {
1616
resolver = recreateResolver()
1717
}
1818

19-
export default function resolveFrom(from?: string, id?: string): string {
19+
export function resolveFrom(from?: string, id?: string): string {
2020
// Network share path on Windows
2121
if (id.startsWith('\\\\')) return id
2222

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

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import type { DesignSystem } from '@tailwindcss/language-service/src/util/v4'
22

33
import postcss from 'postcss'
4+
import * as path from 'node:path'
45
import { resolveCssImports } from '../../css'
6+
import { resolveFrom } from '../resolveFrom'
7+
import { pathToFileURL } from 'tailwindcss-language-server/src/utils'
58

69
const HAS_V4_IMPORT = /@import\s*(?:'tailwindcss'|"tailwindcss")/
710
const HAS_V4_THEME = /@theme\s*\{/
@@ -18,6 +21,37 @@ export async function isMaybeV4(css: string): Promise<boolean> {
1821
return HAS_V4_THEME.test(css) || HAS_V4_IMPORT.test(css)
1922
}
2023

24+
/**
25+
* Create a loader function that can load plugins and config files relative to
26+
* the CSS file that uses them. However, we don't want missing files to prevent
27+
* everything from working so we'll let the error handler decide how to proceed.
28+
*
29+
* @param {object} param0
30+
* @returns
31+
*/
32+
function createLoader<T>({
33+
filepath,
34+
onError,
35+
}: {
36+
filepath: string
37+
onError: (id: string, error: unknown) => T
38+
}) {
39+
let baseDir = path.dirname(filepath)
40+
let cacheKey = `${+Date.now()}`
41+
42+
return async function loadFile(id: string) {
43+
try {
44+
let resolved = resolveFrom(baseDir, id)
45+
let url = pathToFileURL(resolved)
46+
url.searchParams.append('t', cacheKey)
47+
48+
return await import(url.href).then((m) => m.default ?? m)
49+
} catch (err) {
50+
return onError(id, err)
51+
}
52+
}
53+
}
54+
2155
export async function loadDesignSystem(
2256
tailwindcss: any,
2357
filepath: string,
@@ -38,9 +72,23 @@ export async function loadDesignSystem(
3872

3973
// Step 3: Take the resolved CSS and pass it to v4's `loadDesignSystem`
4074
let design: DesignSystem = await tailwindcss.__unstable__loadDesignSystem(resolved.css, {
41-
loadPlugin() {
42-
return () => {}
43-
},
75+
loadPlugin: createLoader({
76+
filepath,
77+
onError(id, err) {
78+
console.error(`Unable to load plugin: ${id}`, err)
79+
80+
return () => {}
81+
},
82+
}),
83+
84+
loadConfig: createLoader({
85+
filepath,
86+
onError(id, err) {
87+
console.error(`Unable to load config: ${id}`, err)
88+
89+
return {}
90+
},
91+
}),
4492
})
4593

4694
// Step 4: Augment the design system with some additional APIs that the LSP needs
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
@import 'tailwindcss';
2+
3+
/* Load ESM versions */
4+
@config './esm/my-config.mjs';
5+
@plugin './esm/my-plugin.mjs';
6+
7+
/* Load Common JS versions */
8+
@config './cjs/my-config.cjs';
9+
@plugin './cjs/my-plugin.cjs';
10+
11+
/* Load TypeScript versions */
12+
@config './ts/my-config.ts';
13+
@plugin './ts/my-plugin.ts';
14+
15+
/* Attempt to load files that do not exist */
16+
@config './missing-confg.mjs';
17+
@plugin './missing-plugin.mjs';
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module.exports = {
2+
theme: {
3+
extend: {
4+
colors: {
5+
'cjs-from-config': 'black',
6+
},
7+
},
8+
},
9+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const plugin = require('tailwindcss/plugin')
2+
3+
module.exports = plugin(
4+
() => {
5+
//
6+
},
7+
{
8+
theme: {
9+
extend: {
10+
colors: {
11+
'cjs-from-plugin': 'black',
12+
},
13+
},
14+
},
15+
},
16+
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export default {
2+
theme: {
3+
extend: {
4+
colors: {
5+
'esm-from-config': 'black',
6+
},
7+
},
8+
},
9+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import plugin from 'tailwindcss/plugin'
2+
3+
export default plugin(
4+
() => {
5+
//
6+
},
7+
{
8+
theme: {
9+
extend: {
10+
colors: {
11+
'esm-from-plugin': 'black',
12+
},
13+
},
14+
},
15+
},
16+
)

packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/package-lock.json

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"dependencies": {
3+
"tailwindcss": "file:tailwindcss.tgz"
4+
}
5+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { Config } from 'tailwindcss'
2+
3+
export default {
4+
theme: {
5+
extend: {
6+
colors: {
7+
'ts-from-config': 'black',
8+
},
9+
},
10+
},
11+
} satisfies Config
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { PluginAPI } from 'tailwindcss'
2+
import plugin from 'tailwindcss/plugin'
3+
4+
export default plugin(
5+
(api: PluginAPI) => {
6+
//
7+
},
8+
{
9+
theme: {
10+
extend: {
11+
colors: {
12+
'ts-from-plugin': 'black',
13+
},
14+
},
15+
},
16+
},
17+
)

0 commit comments

Comments
 (0)