diff --git a/packages/tailwindcss-language-server/src/project-locator.test.ts b/packages/tailwindcss-language-server/src/project-locator.test.ts index 3d5cddbb..7728d795 100644 --- a/packages/tailwindcss-language-server/src/project-locator.test.ts +++ b/packages/tailwindcss-language-server/src/project-locator.test.ts @@ -499,6 +499,95 @@ 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: [], + }, + ], +}) + +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: { + '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/project-locator.ts b/packages/tailwindcss-language-server/src/project-locator.ts index 99d68505..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() @@ -382,14 +393,20 @@ 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) || + // Move stylesheets with an explicit tailwindcss import before others + Number(b.importsTailwind) - Number(a.importsTailwind) || + // Otherwise stylesheets are kept in discovery order + 0 + ) }) for (let root of roots) { @@ -725,7 +742,31 @@ 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 + } + + /** + * 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) } /** diff --git a/packages/tailwindcss-language-server/src/version-guesser.ts b/packages/tailwindcss-language-server/src/version-guesser.ts index a151dea7..51b7782b 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: @@ -44,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 @@ -60,6 +66,7 @@ export function analyzeStylesheet(content: string): TailwindStylesheet { return { root: true, versions: ['4'], + explicitImport: true, } } @@ -71,6 +78,7 @@ export function analyzeStylesheet(content: string): TailwindStylesheet { return { root: true, versions: ['4'], + explicitImport: false, } } @@ -78,6 +86,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 +96,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 +106,7 @@ export function analyzeStylesheet(content: string): TailwindStylesheet { // Roots are only a valid concept in v4 root: false, versions: ['3'], + explicitImport: false, } } @@ -104,6 +115,7 @@ export function analyzeStylesheet(content: string): TailwindStylesheet { return { root: true, versions: ['4', '3'], + explicitImport: false, } } @@ -112,14 +124,16 @@ export function analyzeStylesheet(content: string): TailwindStylesheet { return { root: false, versions: ['4', '3'], + explicitImport: false, } } // 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'], + explicitImport: false, } } @@ -127,5 +141,6 @@ export function analyzeStylesheet(content: string): TailwindStylesheet { return { root: false, versions: [], + explicitImport: false, } } 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, 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