From 88ae0755d2e240995e29fce97df2cc41b7f218b9 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 13 Feb 2025 11:14:41 -0500 Subject: [PATCH 1/3] =?UTF-8?q?Require=20=E2=80=9Croot=E2=80=9D=20files=20?= =?UTF-8?q?in=20a=20v4=20project=20to=20use=20certain=20features?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/project-locator.ts | 16 +- .../src/version-guesser.ts | 162 +++++++++++++----- 2 files changed, 131 insertions(+), 47 deletions(-) diff --git a/packages/tailwindcss-language-server/src/project-locator.ts b/packages/tailwindcss-language-server/src/project-locator.ts index e708be0a..f3884fe1 100644 --- a/packages/tailwindcss-language-server/src/project-locator.ts +++ b/packages/tailwindcss-language-server/src/project-locator.ts @@ -16,7 +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' +import { analyzeStylesheet, TailwindStylesheet, TailwindVersion } from './version-guesser' export interface ProjectConfig { /** The folder that contains the project */ @@ -140,7 +140,7 @@ export class ProjectLocator { private async createProject(config: ConfigEntry): Promise { let tailwind = await this.detectTailwindVersion(config) - let possibleVersions = config.entries.flatMap((entry) => entry.versions) + let possibleVersions = config.entries.flatMap((entry) => entry.meta?.versions ?? []) console.log( JSON.stringify({ @@ -354,10 +354,11 @@ export class ProjectLocator { for (let file of css) { // If the CSS file couldn't be read for some reason, skip it if (!file.content) continue + if (!file.meta) continue // This file doesn't appear to use Tailwind CSS nor any imports // so we can skip it - if (file.versions.length === 0) continue + if (file.meta.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 @@ -427,6 +428,11 @@ export class ProjectLocator { if (indexPath && utilitiesPath) graph.connect(indexPath, utilitiesPath) for (let root of graph.roots()) { + if (!root.meta) continue + + // This file is not eligible to act as a root of the CSS graph + if (root.meta.root === false) continue + let config: ConfigEntry = configs.remember(root.path, () => ({ source: 'css', type: 'css', @@ -667,7 +673,7 @@ class FileEntry { deps: FileEntry[] = [] realpath: string | null sources: string[] = [] - versions: TailwindVersion[] = [] + meta: TailwindStylesheet | null = null constructor( public type: 'js' | 'css', @@ -739,7 +745,7 @@ class FileEntry { * Determine which Tailwind versions this file might be using */ async resolvePossibleVersions() { - this.versions = this.content ? guessTailwindVersion(this.content) : [] + this.meta = this.content ? analyzeStylesheet(this.content) : null } /** diff --git a/packages/tailwindcss-language-server/src/version-guesser.ts b/packages/tailwindcss-language-server/src/version-guesser.ts index c0bef0e5..a151dea7 100644 --- a/packages/tailwindcss-language-server/src/version-guesser.ts +++ b/packages/tailwindcss-language-server/src/version-guesser.ts @@ -1,5 +1,51 @@ export type TailwindVersion = '3' | '4' +export interface TailwindStylesheet { + /** + * Whether or not this file can be used as a project root + */ + root: boolean + + /** + * The likely Tailwind version used by the given file + */ + versions: TailwindVersion[] +} + +// It's likely this is a v4 file if it has a v4 import: +// - `@import "tailwindcss"` +// - `@import "tailwindcss/theme" +// - etc… +const HAS_V4_IMPORT = /@import\s*['"]tailwindcss(?:\/[^'"]+)?['"]/ + +// It's likely this is a v4 file if it has a v4-specific feature: +// - @plugin +// - @utility +// - @variant +// - @custom-variant +const HAS_V4_DIRECTIVE = /@(theme|plugin|utility|custom-variant|variant|reference)\s*[^;{]+[;{]/ + +// It's likely this is a v4 file if it's using v4's custom functions: +// - --alpha(…) +// - --spacing(…) +// - --theme(…) +const HAS_V4_FN = /--(alpha|spacing|theme)\(/ + +// If the file contains older `@tailwind` directives, it's likely a v3 file +const HAS_LEGACY_TAILWIND = /@tailwind\s*(base|preflight|components|variants|screens)+;/ + +// If the file contains other `@tailwind` directives it might be either +const HAS_TAILWIND_UTILITIES = /@tailwind\s*utilities\s*[^;]*;/ + +// If the file contains other `@tailwind` directives it might be either +const HAS_TAILWIND = /@tailwind\s*[^;]+;/ + +// If the file contains other `@apply` or `@config` it might be either +const HAS_COMMON_DIRECTIVE = /@(config|apply)\s*[^;{]+[;{]/ + +// If it's got imports at all it could be either +const HAS_IMPORT = /@import\s*['"]/ + /** * Determine the likely Tailwind version used by the given file * @@ -8,46 +54,78 @@ export type TailwindVersion = '3' | '4' * * The order *does* matter, as the first item is the most likely version. */ -export function guessTailwindVersion(content: string): TailwindVersion[] { - // 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|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'] - - // 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 [] +export function analyzeStylesheet(content: string): TailwindStylesheet { + // An import for v4 definitely means it can be a v4 root + if (HAS_V4_IMPORT.test(content)) { + return { + root: true, + versions: ['4'], + } + } + + // Having v4-specific directives means its related but not necessarily a root + // but having `@tailwind utilities` alongside it means it could be + if (HAS_V4_DIRECTIVE.test(content)) { + // Unless it specifically has `@tailwind utilities` in it + if (HAS_TAILWIND_UTILITIES.test(content)) { + return { + root: true, + versions: ['4'], + } + } + + return { + // This file MUST be imported by another file to be a valid root + root: false, + versions: ['4'], + } + } + + // Just having v4 functions doesn't mean it's a v4 root + if (HAS_V4_FN.test(content)) { + return { + // This file MUST be imported by another file to be a valid root + root: false, + versions: ['4'], + } + } + + // Legacy tailwind directives mean it's a v3 file + if (HAS_LEGACY_TAILWIND.test(content)) { + return { + // Roots are only a valid concept in v4 + root: false, + versions: ['3'], + } + } + + // Other tailwind directives could be either (though they're probably invalid) + if (HAS_TAILWIND.test(content)) { + return { + root: true, + versions: ['4', '3'], + } + } + + // Other common directives could be either but don't signal a root file + if (HAS_COMMON_DIRECTIVE.test(content)) { + return { + root: false, + versions: ['4', '3'], + } + } + + // Files that import other files could be either and are potentially roots + if (HAS_IMPORT.test(content)) { + return { + root: true, + versions: ['4', '3'], + } + } + + // Pretty sure it's not related to Tailwind at all + return { + root: false, + versions: [], + } } From 4059f4a8f51cb7c082e4b99a8589cd778d507f28 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 13 Feb 2025 11:22:00 -0500 Subject: [PATCH 2/3] Update changelog --- packages/vscode-tailwindcss/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vscode-tailwindcss/CHANGELOG.md b/packages/vscode-tailwindcss/CHANGELOG.md index af43868d..3a513c0a 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -5,6 +5,7 @@ - Show light color swatch from light-dark() functions ([#1199](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1199)) - Ignore comments when matching class attributes ([#1202](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1202)) - Show source diagnostics when imports contain a layer ([#1204](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1204)) +- Only detect project roots in v4 when using certain CSS features ([#1205](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1205)) ## 0.14.4 From b7be491a17e6001b541170a4fd618f20eceef01f Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 13 Feb 2025 11:29:50 -0500 Subject: [PATCH 3/3] Add test --- .../tests/env/v4.test.js | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/packages/tailwindcss-language-server/tests/env/v4.test.js b/packages/tailwindcss-language-server/tests/env/v4.test.js index 0375cf98..37d83cd1 100644 --- a/packages/tailwindcss-language-server/tests/env/v4.test.js +++ b/packages/tailwindcss-language-server/tests/env/v4.test.js @@ -574,3 +574,50 @@ defineTest({ }) }, }) + +defineTest({ + options: { + only: true, + }, + name: 'what', + fs: { + 'buttons.css': css` + .foo { + @apply bg-black; + } + `, + 'styles.css': css` + @import 'tailwindcss'; + `, + }, + prepare: async ({ root }) => ({ c: await init(root) }), + handle: async ({ c }) => { + let document = await c.openDocument({ + lang: 'html', + text: '
', + }) + + let hover = await c.sendRequest(HoverRequest.type, { + textDocument: document, + + //
+ // ^ + position: { line: 0, character: 13 }, + }) + + expect(hover).toEqual({ + contents: { + language: 'css', + value: dedent` + .bg-black { + background-color: var(--color-black) /* #000 = #000000 */; + } + `, + }, + range: { + start: { line: 0, character: 12 }, + end: { line: 0, character: 20 }, + }, + }) + }, +})