Skip to content

Commit 13c3e30

Browse files
committed
Support loading plugins in CSS
1 parent 28c592f commit 13c3e30

File tree

9 files changed

+180
-3
lines changed

9 files changed

+180
-3
lines changed

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

Lines changed: 42 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,14 @@ 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+
}),
4483
})
4584

4685
// Step 4: Augment the design system with some additional APIs that the LSP needs
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
@import 'tailwindcss';
2+
3+
/* Load ESM versions */
4+
@plugin './esm/my-plugin.mjs';
5+
6+
/* Load Common JS versions */
7+
@plugin './cjs/my-plugin.cjs';
8+
9+
/* Load TypeScript versions */
10+
@plugin './ts/my-plugin.ts';
11+
12+
/* Attempt to load files that do not exist */
13+
@plugin './missing-plugin.mjs';
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: 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: 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+
)

packages/tailwindcss-language-server/tests/hover/hover.test.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,3 +242,57 @@ withFixture('v4/basic', (c) => {
242242
},
243243
})
244244
})
245+
246+
withFixture('v4/css-loading-js', (c) => {
247+
async function testHover(name, { text, lang, position, expected, expectedRange, settings }) {
248+
test.concurrent(name, async ({ expect }) => {
249+
let textDocument = await c.openDocument({ text, lang, settings })
250+
let res = await c.sendRequest('textDocument/hover', {
251+
textDocument,
252+
position,
253+
})
254+
255+
expect(res).toEqual(
256+
expected
257+
? {
258+
contents: {
259+
language: 'css',
260+
value: expected,
261+
},
262+
range: expectedRange,
263+
}
264+
: expected,
265+
)
266+
})
267+
}
268+
269+
testHover('Plugins: ESM', {
270+
text: '<div class="bg-esm-from-plugin">',
271+
position: { line: 0, character: 13 },
272+
expected: '.bg-esm-from-plugin {\n background-color: black;\n}',
273+
expectedRange: {
274+
start: { line: 0, character: 12 },
275+
end: { line: 0, character: 30 },
276+
},
277+
})
278+
279+
testHover('Plugins: CJS', {
280+
text: '<div class="bg-cjs-from-plugin">',
281+
position: { line: 0, character: 13 },
282+
expected: '.bg-cjs-from-plugin {\n background-color: black;\n}',
283+
expectedRange: {
284+
start: { line: 0, character: 12 },
285+
end: { line: 0, character: 30 },
286+
},
287+
})
288+
289+
testHover('Plugins: TypeScript', {
290+
text: '<div class="bg-ts-from-plugin">',
291+
position: { line: 0, character: 13 },
292+
expected: '.bg-ts-from-plugin {\n background-color: black;\n}',
293+
expectedRange: {
294+
start: { line: 0, character: 12 },
295+
end: { line: 0, character: 29 },
296+
},
297+
})
298+
})

0 commit comments

Comments
 (0)