From 568c1003ad357af92a2f3a3cd6cf9d097d89f624 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 4 Feb 2025 11:24:52 -0500 Subject: [PATCH 1/7] Refactor --- .../src/project-locator.ts | 36 ++++++-------- .../src/version-guesser.ts | 48 +++++++++++++++++++ 2 files changed, 63 insertions(+), 21 deletions(-) create mode 100644 packages/tailwindcss-language-server/src/version-guesser.ts diff --git a/packages/tailwindcss-language-server/src/project-locator.ts b/packages/tailwindcss-language-server/src/project-locator.ts index 132d8cf5..a203aae5 100644 --- a/packages/tailwindcss-language-server/src/project-locator.ts +++ b/packages/tailwindcss-language-server/src/project-locator.ts @@ -16,6 +16,7 @@ import { extractSourceDirectives, resolveCssImports } from './css' import { normalizeDriveLetter, normalizePath, pathToFileURL } from './utils' import postcss from 'postcss' import * as oxide from './oxide' +import { guessTailwindVersion, TailwindVersion } from './version-guesser' export interface ProjectConfig { /** The folder that contains the project */ @@ -324,6 +325,9 @@ export class ProjectLocator { // Read the content of all the CSS files await Promise.all(css.map((entry) => entry.read())) + // Determine what tailwind versions each file might be using + await Promise.all(css.map((entry) => entry.resolvePossibleVersions())) + // Keep track of files that might import or involve Tailwind in some way let imports: FileEntry[] = [] @@ -331,8 +335,9 @@ 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 + // This file doesn't appear to use Tailwind CSS nor any imports + // so we can skip it + if (file.versions.length === 0) continue // Find `@config` directives in CSS files and resolve them to the actual // config file that they point to. This is only relevant for v3 which @@ -642,6 +647,7 @@ class FileEntry { deps: FileEntry[] = [] realpath: string | null sources: string[] = [] + versions: TailwindVersion[] = [] constructor( public type: 'js' | 'css', @@ -709,6 +715,13 @@ class FileEntry { } } + /** + * Determine which Tailwind versions this file might be using + */ + async resolvePossibleVersions() { + this.versions = this.content ? guessTailwindVersion(this.content) : [] + } + /** * 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 @@ -727,25 +740,6 @@ class FileEntry { return normalizePath(path.resolve(path.dirname(this.path), match.groups.config.slice(1, -1))) } - - /** - * 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_TAILWIND = /@tailwind\s*[^;]+;/ - let HAS_DIRECTIVE = /@(theme|plugin|config|utility|variant|apply)\s*[^;{]+[;{]/ - - return ( - HAS_IMPORT.test(this.content) || - HAS_TAILWIND.test(this.content) || - HAS_DIRECTIVE.test(this.content) - ) - } } function requiresPreprocessor(filepath: string) { diff --git a/packages/tailwindcss-language-server/src/version-guesser.ts b/packages/tailwindcss-language-server/src/version-guesser.ts new file mode 100644 index 00000000..8337fdec --- /dev/null +++ b/packages/tailwindcss-language-server/src/version-guesser.ts @@ -0,0 +1,48 @@ +export type TailwindVersion = '3' | '4' + +/** + * Determine the likely Tailwind version used by the given file + * + * This returns an array of possible versions, as a file could contain + * features that make determining the version ambiguous. + * + * The order *does* matter, as the first item is the most likely version. + */ +export function guessTailwindVersion(content: string): TailwindVersion[] { + if (!content) return [] + + // It's likely this is a v4 file if it has a v4 import: + // - `@import "tailwindcss"` + // - `@import "tailwindcss/theme" + // - etc… + let HAS_V4_IMPORT = /@import\s*['"]tailwindcss(?:\/[^'"]+)?['"]/ + if (HAS_V4_IMPORT.test(content)) return ['4'] + + // It's likely this is a v4 file if it has a v4-specific feature: + // - @theme + // - @plugin + // - @utility + // - @variant + // - @custom-variant + let HAS_V4_DIRECTIVE = /@(theme|plugin|utility|custom-variant|variant)\s*[^;{]+[;{]/ + if (HAS_V4_DIRECTIVE.test(content)) return ['4'] + + // If the file contains older `@tailwind` directives, it's likely a v3 file + let HAS_LEGACY_TAILWIND = /@tailwind\s*(base|preflight|components|variants|screens)+;/ + if (HAS_LEGACY_TAILWIND.test(content)) return ['3'] + + // If the file contains other `@tailwind` directives it might be either + let HAS_TAILWIND = /@tailwind\s*[^;]+;/ + if (HAS_TAILWIND.test(content)) return ['4', '3'] + + // If the file contains other `@apply` or `@config` it might be either + let HAS_COMMON_DIRECTIVE = /@(config|apply)\s*[^;{]+[;{]/ + if (HAS_COMMON_DIRECTIVE.test(content)) return ['4', '3'] + + // If it's got imports at all it could be either + let HAS_IMPORT = /@import\s*['"]/ + if (HAS_IMPORT.test(content)) return ['4', '3'] + + // There's chance this file isn't tailwind-related + return [] +} From 220ec9edcb79fe41f57113156fde41ea930a42c4 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 4 Feb 2025 11:24:55 -0500 Subject: [PATCH 2/7] =?UTF-8?q?Skip=20=E2=80=9Croot=E2=80=9D=20CSS=20files?= =?UTF-8?q?=20that=20don=E2=80=99t=20look=20like=20they=20use=20v4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/project-locator.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/tailwindcss-language-server/src/project-locator.ts b/packages/tailwindcss-language-server/src/project-locator.ts index a203aae5..0bb3947d 100644 --- a/packages/tailwindcss-language-server/src/project-locator.ts +++ b/packages/tailwindcss-language-server/src/project-locator.ts @@ -131,6 +131,8 @@ export class ProjectLocator { private async createProject(config: ConfigEntry): Promise { let tailwind = await this.detectTailwindVersion(config) + let possibleVersions = config.entries.flatMap((entry) => entry.versions) + console.log( JSON.stringify({ tailwind, @@ -161,6 +163,15 @@ export class ProjectLocator { return null } + // This config doesn't include any v4 features (even ones that were also in v3) + if (!possibleVersions.includes('4')) { + console.warn( + `The config ${config.path} looks like it might be for a different Tailwind CSS version. Skipping.`, + ) + + return null + } + // v4 does not support .sass, .scss, .less, and .styl files as configs if (requiresPreprocessor(config.path)) { console.warn( From 043d2bbff5290233c0aef416941ccc25b53521ed Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 4 Feb 2025 11:28:11 -0500 Subject: [PATCH 3/7] Update changelog --- packages/vscode-tailwindcss/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index 2ff3cf21..40a00e10 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -2,7 +2,7 @@ ## Prerelease -- Nothing yet! +- Don't create v4 projects for CSS files that don't look like v4 configs [#1164](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1164) ## 0.14.2 From ae80ad2cd5231ed26551557d0db215df90856b03 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 4 Feb 2025 12:44:03 -0500 Subject: [PATCH 4/7] Check for `@reference` --- packages/tailwindcss-language-server/src/version-guesser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss-language-server/src/version-guesser.ts b/packages/tailwindcss-language-server/src/version-guesser.ts index 8337fdec..32d4bf91 100644 --- a/packages/tailwindcss-language-server/src/version-guesser.ts +++ b/packages/tailwindcss-language-server/src/version-guesser.ts @@ -24,7 +24,7 @@ export function guessTailwindVersion(content: string): TailwindVersion[] { // - @utility // - @variant // - @custom-variant - let HAS_V4_DIRECTIVE = /@(theme|plugin|utility|custom-variant|variant)\s*[^;{]+[;{]/ + let HAS_V4_DIRECTIVE = /@(theme|plugin|utility|custom-variant|variant|reference)\s*[^;{]+[;{]/ if (HAS_V4_DIRECTIVE.test(content)) return ['4'] // If the file contains older `@tailwind` directives, it's likely a v3 file From fb39249b614e913c80b03f92e451695638fd658b Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 4 Feb 2025 12:44:12 -0500 Subject: [PATCH 5/7] =?UTF-8?q?Check=20for=20v4=E2=80=99s=20custom=20funct?= =?UTF-8?q?ions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tailwindcss-language-server/src/version-guesser.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/tailwindcss-language-server/src/version-guesser.ts b/packages/tailwindcss-language-server/src/version-guesser.ts index 32d4bf91..5b71ed99 100644 --- a/packages/tailwindcss-language-server/src/version-guesser.ts +++ b/packages/tailwindcss-language-server/src/version-guesser.ts @@ -27,6 +27,13 @@ export function guessTailwindVersion(content: string): TailwindVersion[] { let HAS_V4_DIRECTIVE = /@(theme|plugin|utility|custom-variant|variant|reference)\s*[^;{]+[;{]/ if (HAS_V4_DIRECTIVE.test(content)) return ['4'] + // It's likely this is a v4 file if it's using v4's custom functions: + // - --alpha(…) + // - --spacing(…) + // - --theme(…) + let HAS_V4_FN = /--(alpha|spacing|theme)\(/ + if (HAS_V4_FN.test(content)) return ['4'] + // If the file contains older `@tailwind` directives, it's likely a v3 file let HAS_LEGACY_TAILWIND = /@tailwind\s*(base|preflight|components|variants|screens)+;/ if (HAS_LEGACY_TAILWIND.test(content)) return ['3'] From a218caab58707dfebdf8d6a63cd3f6b49517aada Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 4 Feb 2025 12:45:19 -0500 Subject: [PATCH 6/7] Remove unnvessary check --- packages/tailwindcss-language-server/src/version-guesser.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/tailwindcss-language-server/src/version-guesser.ts b/packages/tailwindcss-language-server/src/version-guesser.ts index 5b71ed99..c0bef0e5 100644 --- a/packages/tailwindcss-language-server/src/version-guesser.ts +++ b/packages/tailwindcss-language-server/src/version-guesser.ts @@ -9,8 +9,6 @@ export type TailwindVersion = '3' | '4' * The order *does* matter, as the first item is the most likely version. */ export function guessTailwindVersion(content: string): TailwindVersion[] { - if (!content) return [] - // It's likely this is a v4 file if it has a v4 import: // - `@import "tailwindcss"` // - `@import "tailwindcss/theme" From 7f55e84776ace16455fd1b551f2aa64e0c34c56b Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 4 Feb 2025 12:51:19 -0500 Subject: [PATCH 7/7] Add tests --- .../tests/env/v4.test.js | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/packages/tailwindcss-language-server/tests/env/v4.test.js b/packages/tailwindcss-language-server/tests/env/v4.test.js index f393f9f1..9f14f06a 100644 --- a/packages/tailwindcss-language-server/tests/env/v4.test.js +++ b/packages/tailwindcss-language-server/tests/env/v4.test.js @@ -225,3 +225,122 @@ defineTest({ expect(completion.items.length).toBe(12288) }, }) + +defineTest({ + name: 'v4, uses npm, does not detect v3 config files as possible roots', + fs: { + 'package.json': json` + { + "dependencies": { + "tailwindcss": "4.0.1" + } + } + `, + // This file MUST be before the v4 CSS file when sorting alphabetically + '_globals.css': css` + @tailwind base; + @tailwind utilities; + @tailwind components; + `, + 'app.css': css` + @import 'tailwindcss'; + + @theme { + --color-primary: #c0ffee; + } + `, + }, + prepare: async ({ root }) => ({ c: await init(root) }), + handle: async ({ c }) => { + let textDocument = await c.openDocument({ + lang: 'html', + text: '
', + }) + + expect(c.project).toMatchObject({ + tailwind: { + version: '4.0.1', + isDefaultVersion: false, + }, + }) + + let hover = await c.sendRequest(HoverRequest.type, { + textDocument, + + //
+ // ^ + position: { line: 0, character: 13 }, + }) + + expect(hover).toEqual({ + contents: { + language: 'css', + value: dedent` + .bg-primary { + background-color: var(--color-primary) /* #c0ffee */; + } + `, + }, + range: { + start: { line: 0, character: 12 }, + end: { line: 0, character: 22 }, + }, + }) + }, +}) + +defineTest({ + name: 'v4, uses fallback, does not detect v3 config files as possible roots', + fs: { + // This file MUST be before the v4 CSS file when sorting alphabetically + '_globals.css': css` + @tailwind base; + @tailwind utilities; + @tailwind components; + `, + 'app.css': css` + @import 'tailwindcss'; + + @theme { + --color-primary: #c0ffee; + } + `, + }, + prepare: async ({ root }) => ({ c: await init(root) }), + handle: async ({ c }) => { + let textDocument = await c.openDocument({ + lang: 'html', + text: '
', + }) + + expect(c.project).toMatchObject({ + tailwind: { + version: '4.0.0', + isDefaultVersion: true, + }, + }) + + let hover = await c.sendRequest(HoverRequest.type, { + textDocument, + + //
+ // ^ + position: { line: 0, character: 13 }, + }) + + expect(hover).toEqual({ + contents: { + language: 'css', + value: dedent` + .bg-primary { + background-color: var(--color-primary) /* #c0ffee */; + } + `, + }, + range: { + start: { line: 0, character: 12 }, + end: { line: 0, character: 22 }, + }, + }) + }, +})