From 5dc6bd23e9b41a792df22a9cb787211065d02707 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 4 Jun 2025 15:35:49 -0400 Subject: [PATCH 1/7] Fix type errors --- .../src/util/rewriting/index.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/tailwindcss-language-service/src/util/rewriting/index.test.ts b/packages/tailwindcss-language-service/src/util/rewriting/index.test.ts index 3a150a01..5e574498 100644 --- a/packages/tailwindcss-language-service/src/util/rewriting/index.test.ts +++ b/packages/tailwindcss-language-service/src/util/rewriting/index.test.ts @@ -25,6 +25,7 @@ test('replacing CSS variables with their fallbacks (when they have them)', () => let state: State = { enabled: true, + features: [], designSystem: { theme: { prefix: null } as any, resolveThemeValue: (name) => map.get(name) ?? null, @@ -102,6 +103,7 @@ test('recursive theme replacements', () => { let state: State = { enabled: true, + features: [], designSystem: { theme: { prefix: null } as any, resolveThemeValue: (name) => map.get(name) ?? null, @@ -142,6 +144,7 @@ test('recursive theme replacements (inlined)', () => { let state: State = { enabled: true, + features: [], designSystem: { theme: { prefix: null } as any, resolveThemeValue: (name) => map.get(name) ?? null, @@ -184,6 +187,7 @@ test('Inlining calc expressions using the design system', () => { let state: State = { enabled: true, + features: [], designSystem: { theme: { prefix: null } as any, resolveThemeValue: (name) => map.get(name) ?? null, From 23dc7709878156ad39f7bb7a470810e3d8215f4c Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 4 Jun 2025 15:28:14 -0400 Subject: [PATCH 2/7] Refactor --- .../src/project-locator.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/tailwindcss-language-server/src/project-locator.ts b/packages/tailwindcss-language-server/src/project-locator.ts index 99d68505..035991e4 100644 --- a/packages/tailwindcss-language-server/src/project-locator.ts +++ b/packages/tailwindcss-language-server/src/project-locator.ts @@ -382,14 +382,18 @@ export class ProjectLocator { if (indexPath && themePath) graph.connect(indexPath, themePath) if (indexPath && utilitiesPath) graph.connect(indexPath, utilitiesPath) - // Sort the graph so potential "roots" appear first - // The entire concept of roots needs to be rethought because it's not always - // clear what the root of a project is. Even when imports are present a file - // may import a file that is the actual "root" of the project. let roots = Array.from(graph.roots()) roots.sort((a, b) => { - return a.meta.root === b.meta.root ? 0 : a.meta.root ? -1 : 1 + return ( + // Sort the graph so potential "roots" appear first + // The entire concept of roots needs to be rethought because it's not always + // clear what the root of a project is. Even when imports are present a file + // may import a file that is the actual "root" of the project. + Number(b.meta.root) - Number(a.meta.root) || + // Otherwise stylesheets are kept in discovery order + 0 + ) }) for (let root of roots) { From 75ebf0460df30b20fdfbb9f839a8400b238e7d7e Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 4 Jun 2025 15:28:28 -0400 Subject: [PATCH 3/7] Only resolve stylesheet versions once --- packages/tailwindcss-language-server/src/project-locator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tailwindcss-language-server/src/project-locator.ts b/packages/tailwindcss-language-server/src/project-locator.ts index 035991e4..48a92b30 100644 --- a/packages/tailwindcss-language-server/src/project-locator.ts +++ b/packages/tailwindcss-language-server/src/project-locator.ts @@ -729,7 +729,7 @@ class FileEntry { * Determine which Tailwind versions this file might be using */ async resolvePossibleVersions() { - this.meta = this.content ? analyzeStylesheet(this.content) : null + this.meta ??= this.content ? analyzeStylesheet(this.content) : null } /** From 2f6de10e5703898fd86233c459a7fb0b5aa1da7e Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 5 Jun 2025 15:01:11 -0400 Subject: [PATCH 4/7] =?UTF-8?q?Sort=20files=20that=20import=20Tailwind=20C?= =?UTF-8?q?SS=20before=20ones=20that=20don=E2=80=99t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/project-locator.test.ts | 32 ++++++++++++++++ .../src/project-locator.ts | 37 +++++++++++++++++++ .../src/version-guesser.ts | 14 +++++++ 3 files changed, 83 insertions(+) diff --git a/packages/tailwindcss-language-server/src/project-locator.test.ts b/packages/tailwindcss-language-server/src/project-locator.test.ts index 3d5cddbb..d52345ab 100644 --- a/packages/tailwindcss-language-server/src/project-locator.test.ts +++ b/packages/tailwindcss-language-server/src/project-locator.test.ts @@ -499,6 +499,38 @@ testLocator({ ], }) +testLocator({ + name: 'Stylesheets that import Tailwind CSS are picked over ones that dont', + fs: { + 'a/foo.css': css` + @import './bar.css'; + .a { + color: red; + } + `, + 'a/bar.css': css` + .b { + color: red; + } + `, + 'src/app.css': css` + @import 'tailwindcss'; + `, + }, + expected: [ + { + version: '4.1.1 (bundled)', + config: '/src/app.css', + content: [], + }, + { + version: '4.1.1 (bundled)', + config: '/a/foo.css', + content: [], + }, + ], +}) + // --- function testLocator({ diff --git a/packages/tailwindcss-language-server/src/project-locator.ts b/packages/tailwindcss-language-server/src/project-locator.ts index 48a92b30..351302d4 100644 --- a/packages/tailwindcss-language-server/src/project-locator.ts +++ b/packages/tailwindcss-language-server/src/project-locator.ts @@ -345,6 +345,17 @@ export class ProjectLocator { // Resolve all @source directives await Promise.all(imports.map((file) => file.resolveSourceDirectives())) + let byRealPath: Record = {} + for (let file of imports) byRealPath[file.realpath] = file + + // TODO: Link every entry in the import graph + // This breaks things tho + // for (let file of imports) file.deps = file.deps.map((dep) => byRealPath[dep.realpath] ?? dep) + + // Check if each file has a direct or indirect tailwind import + // TODO: Remove the `byRealPath` argument and use linked deps instead + await Promise.all(imports.map((file) => file.resolveImportsTailwind(byRealPath))) + // Create a graph of all the CSS files that might (indirectly) use Tailwind let graph = new Graph() @@ -391,6 +402,8 @@ export class ProjectLocator { // clear what the root of a project is. Even when imports are present a file // may import a file that is the actual "root" of the project. Number(b.meta.root) - Number(a.meta.root) || + // Move stylesheets with an explicit tailwindcss import before others + Number(b.importsTailwind) - Number(a.importsTailwind) || // Otherwise stylesheets are kept in discovery order 0 ) @@ -732,6 +745,30 @@ class FileEntry { this.meta ??= this.content ? analyzeStylesheet(this.content) : null } + /** + * Determine if this entry or any of its dependencies import a Tailwind CSS + * stylesheet + */ + importsTailwind: boolean | null = null + + resolveImportsTailwind(byPath: Record) { + // Already calculated so nothing to do + if (this.importsTailwind !== null) return + + // We import it directly + let self = byPath[this.realpath] + + if (this.meta?.explicitImport || self?.meta?.explicitImport) { + this.importsTailwind = true + return + } + + // Maybe one of our deps does + for (let dep of this.deps) dep.resolveImportsTailwind(byPath) + + this.importsTailwind = this.deps.some((dep) => dep.importsTailwind) + } + /** * 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 diff --git a/packages/tailwindcss-language-server/src/version-guesser.ts b/packages/tailwindcss-language-server/src/version-guesser.ts index a151dea7..86e0d5dd 100644 --- a/packages/tailwindcss-language-server/src/version-guesser.ts +++ b/packages/tailwindcss-language-server/src/version-guesser.ts @@ -10,6 +10,11 @@ export interface TailwindStylesheet { * The likely Tailwind version used by the given file */ versions: TailwindVersion[] + + /** + * Whether or not this stylesheet explicitly imports Tailwind CSS + */ + explicitImport: boolean } // It's likely this is a v4 file if it has a v4 import: @@ -60,6 +65,7 @@ export function analyzeStylesheet(content: string): TailwindStylesheet { return { root: true, versions: ['4'], + explicitImport: true, } } @@ -71,6 +77,7 @@ export function analyzeStylesheet(content: string): TailwindStylesheet { return { root: true, versions: ['4'], + explicitImport: false, } } @@ -78,6 +85,7 @@ export function analyzeStylesheet(content: string): TailwindStylesheet { // This file MUST be imported by another file to be a valid root root: false, versions: ['4'], + explicitImport: false, } } @@ -87,6 +95,7 @@ export function analyzeStylesheet(content: string): TailwindStylesheet { // This file MUST be imported by another file to be a valid root root: false, versions: ['4'], + explicitImport: false, } } @@ -96,6 +105,7 @@ export function analyzeStylesheet(content: string): TailwindStylesheet { // Roots are only a valid concept in v4 root: false, versions: ['3'], + explicitImport: false, } } @@ -104,6 +114,7 @@ export function analyzeStylesheet(content: string): TailwindStylesheet { return { root: true, versions: ['4', '3'], + explicitImport: false, } } @@ -112,6 +123,7 @@ export function analyzeStylesheet(content: string): TailwindStylesheet { return { root: false, versions: ['4', '3'], + explicitImport: false, } } @@ -120,6 +132,7 @@ export function analyzeStylesheet(content: string): TailwindStylesheet { return { root: true, versions: ['4', '3'], + explicitImport: false, } } @@ -127,5 +140,6 @@ export function analyzeStylesheet(content: string): TailwindStylesheet { return { root: false, versions: [], + explicitImport: false, } } From 73330dbf673bdd2e1057941fc1bb4c0a8bddc354 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 4 Jun 2025 15:33:25 -0400 Subject: [PATCH 5/7] Ignore URL imports when considering roots --- .../src/project-locator.test.ts | 22 +++++++++++++++++++ .../src/version-guesser.ts | 5 +++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss-language-server/src/project-locator.test.ts b/packages/tailwindcss-language-server/src/project-locator.test.ts index d52345ab..8ef6a4ab 100644 --- a/packages/tailwindcss-language-server/src/project-locator.test.ts +++ b/packages/tailwindcss-language-server/src/project-locator.test.ts @@ -531,6 +531,28 @@ testLocator({ ], }) +testLocator({ + name: 'Stylesheets that only have URL imports are not considered roots', + fs: { + 'a/fonts.css': css` + @import 'https://example.com/fonts/some-font.css'; + .a { + color: red; + } + `, + 'src/app.css': css` + @import 'tailwindcss'; + `, + }, + expected: [ + { + version: '4.1.1 (bundled)', + config: '/src/app.css', + content: [], + }, + ], +}) + // --- function testLocator({ diff --git a/packages/tailwindcss-language-server/src/version-guesser.ts b/packages/tailwindcss-language-server/src/version-guesser.ts index 86e0d5dd..51b7782b 100644 --- a/packages/tailwindcss-language-server/src/version-guesser.ts +++ b/packages/tailwindcss-language-server/src/version-guesser.ts @@ -49,7 +49,8 @@ const HAS_TAILWIND = /@tailwind\s*[^;]+;/ const HAS_COMMON_DIRECTIVE = /@(config|apply)\s*[^;{]+[;{]/ // If it's got imports at all it could be either -const HAS_IMPORT = /@import\s*['"]/ +// Note: We only care about non-url imports +const HAS_NON_URL_IMPORT = /@import\s*['"](?!([a-z]+:|\/\/))/ /** * Determine the likely Tailwind version used by the given file @@ -128,7 +129,7 @@ export function analyzeStylesheet(content: string): TailwindStylesheet { } // Files that import other files could be either and are potentially roots - if (HAS_IMPORT.test(content)) { + if (HAS_NON_URL_IMPORT.test(content)) { return { root: true, versions: ['4', '3'], From 8946f5026902d7f513aaae3cb5321e5402ed5305 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 5 Jun 2025 15:03:38 -0400 Subject: [PATCH 6/7] Add test --- .../src/project-locator.test.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/tailwindcss-language-server/src/project-locator.test.ts b/packages/tailwindcss-language-server/src/project-locator.test.ts index 8ef6a4ab..7728d795 100644 --- a/packages/tailwindcss-language-server/src/project-locator.test.ts +++ b/packages/tailwindcss-language-server/src/project-locator.test.ts @@ -531,6 +531,41 @@ testLocator({ ], }) +testLocator({ + name: 'Stylesheets that import Tailwind CSS indirectly are picked over ones that dont', + fs: { + 'a/foo.css': css` + @import './bar.css'; + .a { + color: red; + } + `, + 'a/bar.css': css` + .b { + color: red; + } + `, + 'src/app.css': css` + @import './tw.css'; + `, + 'src/tw.css': css` + @import 'tailwindcss'; + `, + }, + expected: [ + { + version: '4.1.1 (bundled)', + config: '/src/app.css', + content: [], + }, + { + version: '4.1.1 (bundled)', + config: '/a/foo.css', + content: [], + }, + ], +}) + testLocator({ name: 'Stylesheets that only have URL imports are not considered roots', fs: { From 6d76ee6164e7114aafb853ce8306a2fe7331aaa0 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 5 Jun 2025 15:22:33 -0400 Subject: [PATCH 7/7] 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 1bbdff03..a0f82d6b 100644 --- a/packages/vscode-tailwindcss/CHANGELOG.md +++ b/packages/vscode-tailwindcss/CHANGELOG.md @@ -5,6 +5,7 @@ - Bump bundled CSS language service ([#1395](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1395)) - Fix infinite loop when resolving completion details with recursive theme keys ([#1400](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1400)) - Simplify completion details for more utilities ([#1397](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1397)) +- Improve project stylesheet detection ([#1401](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1401)) ## 0.14.20