Skip to content

Commit 5702ca6

Browse files
Improve stylesheet "root" detection (#1401)
Fixes #1388 There's two problems here: - We consider any stylesheet with an `@import` a potential root. This includes *compiled* stylesheets that have URL imports to things like google fonts - We consider all "roots" graphs equally instead of prioritizing ones that import Tailwind CSS.
1 parent 0d4e7c7 commit 5702ca6

File tree

5 files changed

+158
-8
lines changed

5 files changed

+158
-8
lines changed

packages/tailwindcss-language-server/src/project-locator.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,95 @@ testLocator({
499499
],
500500
})
501501

502+
testLocator({
503+
name: 'Stylesheets that import Tailwind CSS are picked over ones that dont',
504+
fs: {
505+
'a/foo.css': css`
506+
@import './bar.css';
507+
.a {
508+
color: red;
509+
}
510+
`,
511+
'a/bar.css': css`
512+
.b {
513+
color: red;
514+
}
515+
`,
516+
'src/app.css': css`
517+
@import 'tailwindcss';
518+
`,
519+
},
520+
expected: [
521+
{
522+
version: '4.1.1 (bundled)',
523+
config: '/src/app.css',
524+
content: [],
525+
},
526+
{
527+
version: '4.1.1 (bundled)',
528+
config: '/a/foo.css',
529+
content: [],
530+
},
531+
],
532+
})
533+
534+
testLocator({
535+
name: 'Stylesheets that import Tailwind CSS indirectly are picked over ones that dont',
536+
fs: {
537+
'a/foo.css': css`
538+
@import './bar.css';
539+
.a {
540+
color: red;
541+
}
542+
`,
543+
'a/bar.css': css`
544+
.b {
545+
color: red;
546+
}
547+
`,
548+
'src/app.css': css`
549+
@import './tw.css';
550+
`,
551+
'src/tw.css': css`
552+
@import 'tailwindcss';
553+
`,
554+
},
555+
expected: [
556+
{
557+
version: '4.1.1 (bundled)',
558+
config: '/src/app.css',
559+
content: [],
560+
},
561+
{
562+
version: '4.1.1 (bundled)',
563+
config: '/a/foo.css',
564+
content: [],
565+
},
566+
],
567+
})
568+
569+
testLocator({
570+
name: 'Stylesheets that only have URL imports are not considered roots',
571+
fs: {
572+
'a/fonts.css': css`
573+
@import 'https://example.com/fonts/some-font.css';
574+
.a {
575+
color: red;
576+
}
577+
`,
578+
'src/app.css': css`
579+
@import 'tailwindcss';
580+
`,
581+
},
582+
expected: [
583+
{
584+
version: '4.1.1 (bundled)',
585+
config: '/src/app.css',
586+
content: [],
587+
},
588+
],
589+
})
590+
502591
// ---
503592

504593
function testLocator({

packages/tailwindcss-language-server/src/project-locator.ts

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,17 @@ export class ProjectLocator {
345345
// Resolve all @source directives
346346
await Promise.all(imports.map((file) => file.resolveSourceDirectives()))
347347

348+
let byRealPath: Record<string, FileEntry> = {}
349+
for (let file of imports) byRealPath[file.realpath] = file
350+
351+
// TODO: Link every entry in the import graph
352+
// This breaks things tho
353+
// for (let file of imports) file.deps = file.deps.map((dep) => byRealPath[dep.realpath] ?? dep)
354+
355+
// Check if each file has a direct or indirect tailwind import
356+
// TODO: Remove the `byRealPath` argument and use linked deps instead
357+
await Promise.all(imports.map((file) => file.resolveImportsTailwind(byRealPath)))
358+
348359
// Create a graph of all the CSS files that might (indirectly) use Tailwind
349360
let graph = new Graph<FileEntry>()
350361

@@ -382,14 +393,20 @@ export class ProjectLocator {
382393
if (indexPath && themePath) graph.connect(indexPath, themePath)
383394
if (indexPath && utilitiesPath) graph.connect(indexPath, utilitiesPath)
384395

385-
// Sort the graph so potential "roots" appear first
386-
// The entire concept of roots needs to be rethought because it's not always
387-
// clear what the root of a project is. Even when imports are present a file
388-
// may import a file that is the actual "root" of the project.
389396
let roots = Array.from(graph.roots())
390397

391398
roots.sort((a, b) => {
392-
return a.meta.root === b.meta.root ? 0 : a.meta.root ? -1 : 1
399+
return (
400+
// Sort the graph so potential "roots" appear first
401+
// The entire concept of roots needs to be rethought because it's not always
402+
// clear what the root of a project is. Even when imports are present a file
403+
// may import a file that is the actual "root" of the project.
404+
Number(b.meta.root) - Number(a.meta.root) ||
405+
// Move stylesheets with an explicit tailwindcss import before others
406+
Number(b.importsTailwind) - Number(a.importsTailwind) ||
407+
// Otherwise stylesheets are kept in discovery order
408+
0
409+
)
393410
})
394411

395412
for (let root of roots) {
@@ -725,7 +742,31 @@ class FileEntry {
725742
* Determine which Tailwind versions this file might be using
726743
*/
727744
async resolvePossibleVersions() {
728-
this.meta = this.content ? analyzeStylesheet(this.content) : null
745+
this.meta ??= this.content ? analyzeStylesheet(this.content) : null
746+
}
747+
748+
/**
749+
* Determine if this entry or any of its dependencies import a Tailwind CSS
750+
* stylesheet
751+
*/
752+
importsTailwind: boolean | null = null
753+
754+
resolveImportsTailwind(byPath: Record<string, FileEntry>) {
755+
// Already calculated so nothing to do
756+
if (this.importsTailwind !== null) return
757+
758+
// We import it directly
759+
let self = byPath[this.realpath]
760+
761+
if (this.meta?.explicitImport || self?.meta?.explicitImport) {
762+
this.importsTailwind = true
763+
return
764+
}
765+
766+
// Maybe one of our deps does
767+
for (let dep of this.deps) dep.resolveImportsTailwind(byPath)
768+
769+
this.importsTailwind = this.deps.some((dep) => dep.importsTailwind)
729770
}
730771

731772
/**

packages/tailwindcss-language-server/src/version-guesser.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ export interface TailwindStylesheet {
1010
* The likely Tailwind version used by the given file
1111
*/
1212
versions: TailwindVersion[]
13+
14+
/**
15+
* Whether or not this stylesheet explicitly imports Tailwind CSS
16+
*/
17+
explicitImport: boolean
1318
}
1419

1520
// It's likely this is a v4 file if it has a v4 import:
@@ -44,7 +49,8 @@ const HAS_TAILWIND = /@tailwind\s*[^;]+;/
4449
const HAS_COMMON_DIRECTIVE = /@(config|apply)\s*[^;{]+[;{]/
4550

4651
// If it's got imports at all it could be either
47-
const HAS_IMPORT = /@import\s*['"]/
52+
// Note: We only care about non-url imports
53+
const HAS_NON_URL_IMPORT = /@import\s*['"](?!([a-z]+:|\/\/))/
4854

4955
/**
5056
* Determine the likely Tailwind version used by the given file
@@ -60,6 +66,7 @@ export function analyzeStylesheet(content: string): TailwindStylesheet {
6066
return {
6167
root: true,
6268
versions: ['4'],
69+
explicitImport: true,
6370
}
6471
}
6572

@@ -71,13 +78,15 @@ export function analyzeStylesheet(content: string): TailwindStylesheet {
7178
return {
7279
root: true,
7380
versions: ['4'],
81+
explicitImport: false,
7482
}
7583
}
7684

7785
return {
7886
// This file MUST be imported by another file to be a valid root
7987
root: false,
8088
versions: ['4'],
89+
explicitImport: false,
8190
}
8291
}
8392

@@ -87,6 +96,7 @@ export function analyzeStylesheet(content: string): TailwindStylesheet {
8796
// This file MUST be imported by another file to be a valid root
8897
root: false,
8998
versions: ['4'],
99+
explicitImport: false,
90100
}
91101
}
92102

@@ -96,6 +106,7 @@ export function analyzeStylesheet(content: string): TailwindStylesheet {
96106
// Roots are only a valid concept in v4
97107
root: false,
98108
versions: ['3'],
109+
explicitImport: false,
99110
}
100111
}
101112

@@ -104,6 +115,7 @@ export function analyzeStylesheet(content: string): TailwindStylesheet {
104115
return {
105116
root: true,
106117
versions: ['4', '3'],
118+
explicitImport: false,
107119
}
108120
}
109121

@@ -112,20 +124,23 @@ export function analyzeStylesheet(content: string): TailwindStylesheet {
112124
return {
113125
root: false,
114126
versions: ['4', '3'],
127+
explicitImport: false,
115128
}
116129
}
117130

118131
// Files that import other files could be either and are potentially roots
119-
if (HAS_IMPORT.test(content)) {
132+
if (HAS_NON_URL_IMPORT.test(content)) {
120133
return {
121134
root: true,
122135
versions: ['4', '3'],
136+
explicitImport: false,
123137
}
124138
}
125139

126140
// Pretty sure it's not related to Tailwind at all
127141
return {
128142
root: false,
129143
versions: [],
144+
explicitImport: false,
130145
}
131146
}

packages/tailwindcss-language-service/src/util/rewriting/index.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ test('replacing CSS variables with their fallbacks (when they have them)', () =>
2525

2626
let state: State = {
2727
enabled: true,
28+
features: [],
2829
designSystem: {
2930
theme: { prefix: null } as any,
3031
resolveThemeValue: (name) => map.get(name) ?? null,
@@ -102,6 +103,7 @@ test('recursive theme replacements', () => {
102103

103104
let state: State = {
104105
enabled: true,
106+
features: [],
105107
designSystem: {
106108
theme: { prefix: null } as any,
107109
resolveThemeValue: (name) => map.get(name) ?? null,
@@ -142,6 +144,7 @@ test('recursive theme replacements (inlined)', () => {
142144

143145
let state: State = {
144146
enabled: true,
147+
features: [],
145148
designSystem: {
146149
theme: { prefix: null } as any,
147150
resolveThemeValue: (name) => map.get(name) ?? null,
@@ -184,6 +187,7 @@ test('Inlining calc expressions using the design system', () => {
184187

185188
let state: State = {
186189
enabled: true,
190+
features: [],
187191
designSystem: {
188192
theme: { prefix: null } as any,
189193
resolveThemeValue: (name) => map.get(name) ?? null,

packages/vscode-tailwindcss/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- Bump bundled CSS language service ([#1395](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1395))
66
- Fix infinite loop when resolving completion details with recursive theme keys ([#1400](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1400))
77
- Simplify completion details for more utilities ([#1397](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1397))
8+
- Improve project stylesheet detection ([#1401](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1401))
89

910
## 0.14.20
1011

0 commit comments

Comments
 (0)