diff --git a/packages/tailwindcss-language-server/src/lib/env.ts b/packages/tailwindcss-language-server/src/lib/env.ts index 21942860..16df926a 100644 --- a/packages/tailwindcss-language-server/src/lib/env.ts +++ b/packages/tailwindcss-language-server/src/lib/env.ts @@ -1,6 +1,6 @@ import Module from 'node:module' import * as path from 'node:path' -import resolveFrom from '../util/resolveFrom' +import { resolveFrom } from '../util/resolveFrom' process.env.TAILWIND_MODE = 'build' process.env.TAILWIND_DISABLE_TOUCH = 'true' diff --git a/packages/tailwindcss-language-server/src/project-locator.ts b/packages/tailwindcss-language-server/src/project-locator.ts index 9e39f67d..e76033d3 100644 --- a/packages/tailwindcss-language-server/src/project-locator.ts +++ b/packages/tailwindcss-language-server/src/project-locator.ts @@ -11,7 +11,7 @@ import type { AtRule, Message } from 'postcss' import { type DocumentSelector, DocumentSelectorPriority } from './projects' import { CacheMap } from './cache-map' import { getPackageRoot } from './util/get-package-root' -import resolveFrom from './util/resolveFrom' +import { resolveFrom } from './util/resolveFrom' import { type Feature, supportedFeatures } from '@tailwindcss/language-service/src/features' import { extractSourceDirectives, resolveCssImports } from './css' import { normalizeDriveLetter, normalizePath, pathToFileURL } from './utils' @@ -132,15 +132,18 @@ export class ProjectLocator { console.log(JSON.stringify({ tailwind })) - // A JS/TS config file was loaded from an `@config`` directive in a CSS file + // A JS/TS config file was loaded from an `@config` directive in a CSS file + // This is only relevant for v3 projects so we'll do some feature detection + // to verify if this is supported in the current version of Tailwind. if (config.type === 'js' && config.source === 'css') { // We only allow local versions of Tailwind to use `@config` directives if (tailwind.isDefaultVersion) { return null } - // This version of Tailwind doesn't support `@config` directives - if (!tailwind.features.includes('css-at-config')) { + // This version of Tailwind doesn't support considering `@config` directives + // as a project on their own. + if (!tailwind.features.includes('css-at-config-as-project')) { return null } } @@ -310,8 +313,12 @@ export class ProjectLocator { // If the CSS file couldn't be read for some reason, skip it if (!file.content) continue + // Look for `@import`, `@tailwind`, `@theme`, `@config`, etc… + if (!file.isMaybeTailwindRelated()) continue + // Find `@config` directives in CSS files and resolve them to the actual - // config file that they point to. + // config file that they point to. This is only relevant for v3 which + // we'll verify after config resolution. let configPath = file.configPathInCss() if (configPath) { // We don't need the content for this file anymore @@ -327,14 +334,9 @@ export class ProjectLocator { content: [], })), ) - continue } - // Look for `@import` or `@tailwind` directives - if (file.isMaybeTailwindRelated()) { - imports.push(file) - continue - } + imports.push(file) } // Resolve imports in all the CSS files @@ -636,6 +638,9 @@ class FileEntry { * Look for `@config` directives in a CSS file and return the path to the config * file that it points to. This path is (possibly) relative to the CSS file so * it must be resolved to an absolute path before returning. + * + * This is only useful for v3 projects. While v4 can use `@config` directives + * the CSS file is still considered the "config" rather than the JS file. */ configPathInCss(): string | null { if (!this.content) return null @@ -649,21 +654,21 @@ class FileEntry { } /** - * Look for `@import` or `@tailwind` directives in a CSS file. This means that + * Look for tailwind-specific directives in a CSS file. This means that it * participates in the CSS "graph" for the project and we need to traverse * the graph to find all the CSS files that are considered entrypoints. */ isMaybeTailwindRelated(): boolean { if (!this.content) return false - let HAS_IMPORT = /@import\s*(?'[^']+'|"[^"]+");/ + let HAS_IMPORT = /@import\s*('[^']+'|"[^"]+");/ let HAS_TAILWIND = /@tailwind\s*[^;]+;/ - let HAS_THEME = /@theme\s*\{/ + let HAS_DIRECTIVE = /@(theme|plugin|config|utility|variant|apply)\s*[^;{]+[;{]/ return ( HAS_IMPORT.test(this.content) || HAS_TAILWIND.test(this.content) || - HAS_THEME.test(this.content) + HAS_DIRECTIVE.test(this.content) ) } } diff --git a/packages/tailwindcss-language-server/src/projects.ts b/packages/tailwindcss-language-server/src/projects.ts index 6704c871..09d4196a 100644 --- a/packages/tailwindcss-language-server/src/projects.ts +++ b/packages/tailwindcss-language-server/src/projects.ts @@ -24,7 +24,7 @@ import * as path from 'path' import * as fs from 'fs' import findUp from 'find-up' import picomatch from 'picomatch' -import resolveFrom, { setPnpApi } from './util/resolveFrom' +import { resolveFrom, setPnpApi } from './util/resolveFrom' import type { AtRule, Container, Node, Result } from 'postcss' import Hook from './lib/hook' import * as semver from '@tailwindcss/language-service/src/util/semver' diff --git a/packages/tailwindcss-language-server/src/tw.ts b/packages/tailwindcss-language-server/src/tw.ts index 08536ae5..f95e438a 100644 --- a/packages/tailwindcss-language-server/src/tw.ts +++ b/packages/tailwindcss-language-server/src/tw.ts @@ -36,7 +36,7 @@ import normalizePath from 'normalize-path' import * as path from 'path' import type * as chokidar from 'chokidar' import picomatch from 'picomatch' -import resolveFrom from './util/resolveFrom' +import { resolveFrom } from './util/resolveFrom' import * as parcel from './watcher/index.js' import { equal } from '@tailwindcss/language-service/src/util/array' import { CONFIG_GLOB, CSS_GLOB, PACKAGE_LOCK_GLOB } from './lib/constants' diff --git a/packages/tailwindcss-language-server/src/util/resolveFrom.ts b/packages/tailwindcss-language-server/src/util/resolveFrom.ts index 8fbc37fe..f81cccf7 100644 --- a/packages/tailwindcss-language-server/src/util/resolveFrom.ts +++ b/packages/tailwindcss-language-server/src/util/resolveFrom.ts @@ -16,7 +16,7 @@ export function setPnpApi(newPnpApi: any): void { resolver = recreateResolver() } -export default function resolveFrom(from?: string, id?: string): string { +export function resolveFrom(from?: string, id?: string): string { // Network share path on Windows if (id.startsWith('\\\\')) return id diff --git a/packages/tailwindcss-language-server/src/util/v4/design-system.ts b/packages/tailwindcss-language-server/src/util/v4/design-system.ts index 07cc3766..e4c5bd84 100644 --- a/packages/tailwindcss-language-server/src/util/v4/design-system.ts +++ b/packages/tailwindcss-language-server/src/util/v4/design-system.ts @@ -1,7 +1,10 @@ import type { DesignSystem } from '@tailwindcss/language-service/src/util/v4' import postcss from 'postcss' +import * as path from 'node:path' import { resolveCssImports } from '../../css' +import { resolveFrom } from '../resolveFrom' +import { pathToFileURL } from 'tailwindcss-language-server/src/utils' const HAS_V4_IMPORT = /@import\s*(?:'tailwindcss'|"tailwindcss")/ const HAS_V4_THEME = /@theme\s*\{/ @@ -18,6 +21,37 @@ export async function isMaybeV4(css: string): Promise { return HAS_V4_THEME.test(css) || HAS_V4_IMPORT.test(css) } +/** + * Create a loader function that can load plugins and config files relative to + * the CSS file that uses them. However, we don't want missing files to prevent + * everything from working so we'll let the error handler decide how to proceed. + * + * @param {object} param0 + * @returns + */ +function createLoader({ + filepath, + onError, +}: { + filepath: string + onError: (id: string, error: unknown) => T +}) { + let baseDir = path.dirname(filepath) + let cacheKey = `${+Date.now()}` + + return async function loadFile(id: string) { + try { + let resolved = resolveFrom(baseDir, id) + let url = pathToFileURL(resolved) + url.searchParams.append('t', cacheKey) + + return await import(url.href).then((m) => m.default ?? m) + } catch (err) { + return onError(id, err) + } + } +} + export async function loadDesignSystem( tailwindcss: any, filepath: string, @@ -38,9 +72,23 @@ export async function loadDesignSystem( // Step 3: Take the resolved CSS and pass it to v4's `loadDesignSystem` let design: DesignSystem = await tailwindcss.__unstable__loadDesignSystem(resolved.css, { - loadPlugin() { - return () => {} - }, + loadPlugin: createLoader({ + filepath, + onError(id, err) { + console.error(`Unable to load plugin: ${id}`, err) + + return () => {} + }, + }), + + loadConfig: createLoader({ + filepath, + onError(id, err) { + console.error(`Unable to load config: ${id}`, err) + + return {} + }, + }), }) // Step 4: Augment the design system with some additional APIs that the LSP needs diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/app.css b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/app.css new file mode 100644 index 00000000..dc86c622 --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/app.css @@ -0,0 +1,17 @@ +@import 'tailwindcss'; + +/* Load ESM versions */ +@config './esm/my-config.mjs'; +@plugin './esm/my-plugin.mjs'; + +/* Load Common JS versions */ +@config './cjs/my-config.cjs'; +@plugin './cjs/my-plugin.cjs'; + +/* Load TypeScript versions */ +@config './ts/my-config.ts'; +@plugin './ts/my-plugin.ts'; + +/* Attempt to load files that do not exist */ +@config './missing-confg.mjs'; +@plugin './missing-plugin.mjs'; diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/cjs/my-config.cjs b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/cjs/my-config.cjs new file mode 100644 index 00000000..9d2eec31 --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/cjs/my-config.cjs @@ -0,0 +1,9 @@ +module.exports = { + theme: { + extend: { + colors: { + 'cjs-from-config': 'black', + }, + }, + }, +} diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/cjs/my-plugin.cjs b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/cjs/my-plugin.cjs new file mode 100644 index 00000000..9b8634e8 --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/cjs/my-plugin.cjs @@ -0,0 +1,16 @@ +const plugin = require('tailwindcss/plugin') + +module.exports = plugin( + () => { + // + }, + { + theme: { + extend: { + colors: { + 'cjs-from-plugin': 'black', + }, + }, + }, + }, +) diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/esm/my-config.mjs b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/esm/my-config.mjs new file mode 100644 index 00000000..0862924f --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/esm/my-config.mjs @@ -0,0 +1,9 @@ +export default { + theme: { + extend: { + colors: { + 'esm-from-config': 'black', + }, + }, + }, +} diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/esm/my-plugin.mjs b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/esm/my-plugin.mjs new file mode 100644 index 00000000..f08ad2ca --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/esm/my-plugin.mjs @@ -0,0 +1,16 @@ +import plugin from 'tailwindcss/plugin' + +export default plugin( + () => { + // + }, + { + theme: { + extend: { + colors: { + 'esm-from-plugin': 'black', + }, + }, + }, + }, +) diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/package-lock.json b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/package-lock.json new file mode 100644 index 00000000..63c4cb68 --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/package-lock.json @@ -0,0 +1,17 @@ +{ + "name": "css-loading-js", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "tailwindcss": "file:tailwindcss.tgz" + } + }, + "node_modules/tailwindcss": { + "version": "4.0.0-alpha.20", + "resolved": "file:tailwindcss.tgz", + "integrity": "sha512-BlNEPI7xonARxqHaNmjWSwBonwu/d8UcVzznUZqekpCIJaWNVlPVqAlOHghSbXju3NHAmUKh6PrMa1ijUMdj+g==" + } + } +} diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/package.json b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/package.json new file mode 100644 index 00000000..444d4230 --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "tailwindcss": "file:tailwindcss.tgz" + } +} diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/tailwindcss.tgz b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/tailwindcss.tgz new file mode 100644 index 00000000..7544562f Binary files /dev/null and b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/tailwindcss.tgz differ diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/ts/my-config.ts b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/ts/my-config.ts new file mode 100644 index 00000000..a7262293 --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/ts/my-config.ts @@ -0,0 +1,11 @@ +import type { Config } from 'tailwindcss' + +export default { + theme: { + extend: { + colors: { + 'ts-from-config': 'black', + }, + }, + }, +} satisfies Config diff --git a/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/ts/my-plugin.ts b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/ts/my-plugin.ts new file mode 100644 index 00000000..2ba29694 --- /dev/null +++ b/packages/tailwindcss-language-server/tests/fixtures/v4/css-loading-js/ts/my-plugin.ts @@ -0,0 +1,17 @@ +import type { PluginAPI } from 'tailwindcss' +import plugin from 'tailwindcss/plugin' + +export default plugin( + (api: PluginAPI) => { + // + }, + { + theme: { + extend: { + colors: { + 'ts-from-plugin': 'black', + }, + }, + }, + }, +) diff --git a/packages/tailwindcss-language-server/tests/hover/hover.test.js b/packages/tailwindcss-language-server/tests/hover/hover.test.js index 93c3c7a4..0b380cbe 100644 --- a/packages/tailwindcss-language-server/tests/hover/hover.test.js +++ b/packages/tailwindcss-language-server/tests/hover/hover.test.js @@ -242,3 +242,87 @@ withFixture('v4/basic', (c) => { }, }) }) + +withFixture('v4/css-loading-js', (c) => { + async function testHover(name, { text, lang, position, expected, expectedRange, settings }) { + test.concurrent(name, async ({ expect }) => { + let textDocument = await c.openDocument({ text, lang, settings }) + let res = await c.sendRequest('textDocument/hover', { + textDocument, + position, + }) + + expect(res).toEqual( + expected + ? { + contents: { + language: 'css', + value: expected, + }, + range: expectedRange, + } + : expected, + ) + }) + } + + testHover('Plugins: ESM', { + text: '
', + position: { line: 0, character: 13 }, + expected: '.bg-esm-from-plugin {\n background-color: black;\n}', + expectedRange: { + start: { line: 0, character: 12 }, + end: { line: 0, character: 30 }, + }, + }) + + testHover('Plugins: CJS', { + text: '
', + position: { line: 0, character: 13 }, + expected: '.bg-cjs-from-plugin {\n background-color: black;\n}', + expectedRange: { + start: { line: 0, character: 12 }, + end: { line: 0, character: 30 }, + }, + }) + + testHover('Plugins: TypeScript', { + text: '
', + position: { line: 0, character: 13 }, + expected: '.bg-ts-from-plugin {\n background-color: black;\n}', + expectedRange: { + start: { line: 0, character: 12 }, + end: { line: 0, character: 29 }, + }, + }) + + testHover('Configs: ESM', { + text: '
', + position: { line: 0, character: 13 }, + expected: '.bg-esm-from-config {\n background-color: black;\n}', + expectedRange: { + start: { line: 0, character: 12 }, + end: { line: 0, character: 30 }, + }, + }) + + testHover('Configs: CJS', { + text: '
', + position: { line: 0, character: 13 }, + expected: '.bg-cjs-from-config {\n background-color: black;\n}', + expectedRange: { + start: { line: 0, character: 12 }, + end: { line: 0, character: 30 }, + }, + }) + + testHover('Configs: TypeScript', { + text: '
', + position: { line: 0, character: 13 }, + expected: '.bg-ts-from-config {\n background-color: black;\n}', + expectedRange: { + start: { line: 0, character: 12 }, + end: { line: 0, character: 29 }, + }, + }) +}) diff --git a/packages/tailwindcss-language-server/tsconfig.json b/packages/tailwindcss-language-server/tsconfig.json index 5e2a7c8c..f69c060b 100755 --- a/packages/tailwindcss-language-server/tsconfig.json +++ b/packages/tailwindcss-language-server/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es6", + "target": "ES2018", "lib": ["ES2022"], "rootDir": "..", "sourceMap": true, diff --git a/packages/tailwindcss-language-service/src/features.ts b/packages/tailwindcss-language-service/src/features.ts index b24a4615..1306bcf3 100644 --- a/packages/tailwindcss-language-service/src/features.ts +++ b/packages/tailwindcss-language-service/src/features.ts @@ -4,7 +4,7 @@ export type Feature = | 'layer:preflight' | 'layer:base' | 'css-at-theme' - | 'css-at-config' + | 'css-at-config-as-project' | 'transpiled-configs' | 'relative-content-paths' | 'browserslist-in-plugins' @@ -77,7 +77,7 @@ export function supportedFeatures(version: string): Feature[] { if (semver.gte(version, '3.2.0')) { // Support for the `@config` directive in CSS to customize the located config file - features.push('css-at-config') + features.push('css-at-config-as-project') // Support for relative content paths features.push('relative-content-paths') diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index 042bd8d9..8cded156 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -2,7 +2,8 @@ ## Prerelease -- Nothing yet! +- Support plugins loaded via `@plugin` in v4 ([#1044](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1044)) +- Support configs loaded via `@config` in v4 ([#1044](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1044)) ## 0.12.8