Skip to content

Commit 82482f2

Browse files
Require “root” files in a v4 project to use certain features (#1205)
Fixes #1137
1 parent 34fb4b5 commit 82482f2

File tree

4 files changed

+179
-47
lines changed

4 files changed

+179
-47
lines changed

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

+11-5
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { extractSourceDirectives, resolveCssImports } from './css'
1616
import { normalizeDriveLetter, normalizePath, pathToFileURL } from './utils'
1717
import postcss from 'postcss'
1818
import * as oxide from './oxide'
19-
import { guessTailwindVersion, TailwindVersion } from './version-guesser'
19+
import { analyzeStylesheet, TailwindStylesheet, TailwindVersion } from './version-guesser'
2020

2121
export interface ProjectConfig {
2222
/** The folder that contains the project */
@@ -140,7 +140,7 @@ export class ProjectLocator {
140140
private async createProject(config: ConfigEntry): Promise<ProjectConfig | null> {
141141
let tailwind = await this.detectTailwindVersion(config)
142142

143-
let possibleVersions = config.entries.flatMap((entry) => entry.versions)
143+
let possibleVersions = config.entries.flatMap((entry) => entry.meta?.versions ?? [])
144144

145145
console.log(
146146
JSON.stringify({
@@ -354,10 +354,11 @@ export class ProjectLocator {
354354
for (let file of css) {
355355
// If the CSS file couldn't be read for some reason, skip it
356356
if (!file.content) continue
357+
if (!file.meta) continue
357358

358359
// This file doesn't appear to use Tailwind CSS nor any imports
359360
// so we can skip it
360-
if (file.versions.length === 0) continue
361+
if (file.meta.versions.length === 0) continue
361362

362363
// Find `@config` directives in CSS files and resolve them to the actual
363364
// config file that they point to. This is only relevant for v3 which
@@ -427,6 +428,11 @@ export class ProjectLocator {
427428
if (indexPath && utilitiesPath) graph.connect(indexPath, utilitiesPath)
428429

429430
for (let root of graph.roots()) {
431+
if (!root.meta) continue
432+
433+
// This file is not eligible to act as a root of the CSS graph
434+
if (root.meta.root === false) continue
435+
430436
let config: ConfigEntry = configs.remember(root.path, () => ({
431437
source: 'css',
432438
type: 'css',
@@ -667,7 +673,7 @@ class FileEntry {
667673
deps: FileEntry[] = []
668674
realpath: string | null
669675
sources: string[] = []
670-
versions: TailwindVersion[] = []
676+
meta: TailwindStylesheet | null = null
671677

672678
constructor(
673679
public type: 'js' | 'css',
@@ -739,7 +745,7 @@ class FileEntry {
739745
* Determine which Tailwind versions this file might be using
740746
*/
741747
async resolvePossibleVersions() {
742-
this.versions = this.content ? guessTailwindVersion(this.content) : []
748+
this.meta = this.content ? analyzeStylesheet(this.content) : null
743749
}
744750

745751
/**
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,51 @@
11
export type TailwindVersion = '3' | '4'
22

3+
export interface TailwindStylesheet {
4+
/**
5+
* Whether or not this file can be used as a project root
6+
*/
7+
root: boolean
8+
9+
/**
10+
* The likely Tailwind version used by the given file
11+
*/
12+
versions: TailwindVersion[]
13+
}
14+
15+
// It's likely this is a v4 file if it has a v4 import:
16+
// - `@import "tailwindcss"`
17+
// - `@import "tailwindcss/theme"
18+
// - etc…
19+
const HAS_V4_IMPORT = /@import\s*['"]tailwindcss(?:\/[^'"]+)?['"]/
20+
21+
// It's likely this is a v4 file if it has a v4-specific feature:
22+
// - @plugin
23+
// - @utility
24+
// - @variant
25+
// - @custom-variant
26+
const HAS_V4_DIRECTIVE = /@(theme|plugin|utility|custom-variant|variant|reference)\s*[^;{]+[;{]/
27+
28+
// It's likely this is a v4 file if it's using v4's custom functions:
29+
// - --alpha(…)
30+
// - --spacing(…)
31+
// - --theme(…)
32+
const HAS_V4_FN = /--(alpha|spacing|theme)\(/
33+
34+
// If the file contains older `@tailwind` directives, it's likely a v3 file
35+
const HAS_LEGACY_TAILWIND = /@tailwind\s*(base|preflight|components|variants|screens)+;/
36+
37+
// If the file contains other `@tailwind` directives it might be either
38+
const HAS_TAILWIND_UTILITIES = /@tailwind\s*utilities\s*[^;]*;/
39+
40+
// If the file contains other `@tailwind` directives it might be either
41+
const HAS_TAILWIND = /@tailwind\s*[^;]+;/
42+
43+
// If the file contains other `@apply` or `@config` it might be either
44+
const HAS_COMMON_DIRECTIVE = /@(config|apply)\s*[^;{]+[;{]/
45+
46+
// If it's got imports at all it could be either
47+
const HAS_IMPORT = /@import\s*['"]/
48+
349
/**
450
* Determine the likely Tailwind version used by the given file
551
*
@@ -8,46 +54,78 @@ export type TailwindVersion = '3' | '4'
854
*
955
* The order *does* matter, as the first item is the most likely version.
1056
*/
11-
export function guessTailwindVersion(content: string): TailwindVersion[] {
12-
// It's likely this is a v4 file if it has a v4 import:
13-
// - `@import "tailwindcss"`
14-
// - `@import "tailwindcss/theme"
15-
// - etc…
16-
let HAS_V4_IMPORT = /@import\s*['"]tailwindcss(?:\/[^'"]+)?['"]/
17-
if (HAS_V4_IMPORT.test(content)) return ['4']
18-
19-
// It's likely this is a v4 file if it has a v4-specific feature:
20-
// - @theme
21-
// - @plugin
22-
// - @utility
23-
// - @variant
24-
// - @custom-variant
25-
let HAS_V4_DIRECTIVE = /@(theme|plugin|utility|custom-variant|variant|reference)\s*[^;{]+[;{]/
26-
if (HAS_V4_DIRECTIVE.test(content)) return ['4']
27-
28-
// It's likely this is a v4 file if it's using v4's custom functions:
29-
// - --alpha(…)
30-
// - --spacing(…)
31-
// - --theme(…)
32-
let HAS_V4_FN = /--(alpha|spacing|theme)\(/
33-
if (HAS_V4_FN.test(content)) return ['4']
34-
35-
// If the file contains older `@tailwind` directives, it's likely a v3 file
36-
let HAS_LEGACY_TAILWIND = /@tailwind\s*(base|preflight|components|variants|screens)+;/
37-
if (HAS_LEGACY_TAILWIND.test(content)) return ['3']
38-
39-
// If the file contains other `@tailwind` directives it might be either
40-
let HAS_TAILWIND = /@tailwind\s*[^;]+;/
41-
if (HAS_TAILWIND.test(content)) return ['4', '3']
42-
43-
// If the file contains other `@apply` or `@config` it might be either
44-
let HAS_COMMON_DIRECTIVE = /@(config|apply)\s*[^;{]+[;{]/
45-
if (HAS_COMMON_DIRECTIVE.test(content)) return ['4', '3']
46-
47-
// If it's got imports at all it could be either
48-
let HAS_IMPORT = /@import\s*['"]/
49-
if (HAS_IMPORT.test(content)) return ['4', '3']
50-
51-
// There's chance this file isn't tailwind-related
52-
return []
57+
export function analyzeStylesheet(content: string): TailwindStylesheet {
58+
// An import for v4 definitely means it can be a v4 root
59+
if (HAS_V4_IMPORT.test(content)) {
60+
return {
61+
root: true,
62+
versions: ['4'],
63+
}
64+
}
65+
66+
// Having v4-specific directives means its related but not necessarily a root
67+
// but having `@tailwind utilities` alongside it means it could be
68+
if (HAS_V4_DIRECTIVE.test(content)) {
69+
// Unless it specifically has `@tailwind utilities` in it
70+
if (HAS_TAILWIND_UTILITIES.test(content)) {
71+
return {
72+
root: true,
73+
versions: ['4'],
74+
}
75+
}
76+
77+
return {
78+
// This file MUST be imported by another file to be a valid root
79+
root: false,
80+
versions: ['4'],
81+
}
82+
}
83+
84+
// Just having v4 functions doesn't mean it's a v4 root
85+
if (HAS_V4_FN.test(content)) {
86+
return {
87+
// This file MUST be imported by another file to be a valid root
88+
root: false,
89+
versions: ['4'],
90+
}
91+
}
92+
93+
// Legacy tailwind directives mean it's a v3 file
94+
if (HAS_LEGACY_TAILWIND.test(content)) {
95+
return {
96+
// Roots are only a valid concept in v4
97+
root: false,
98+
versions: ['3'],
99+
}
100+
}
101+
102+
// Other tailwind directives could be either (though they're probably invalid)
103+
if (HAS_TAILWIND.test(content)) {
104+
return {
105+
root: true,
106+
versions: ['4', '3'],
107+
}
108+
}
109+
110+
// Other common directives could be either but don't signal a root file
111+
if (HAS_COMMON_DIRECTIVE.test(content)) {
112+
return {
113+
root: false,
114+
versions: ['4', '3'],
115+
}
116+
}
117+
118+
// Files that import other files could be either and are potentially roots
119+
if (HAS_IMPORT.test(content)) {
120+
return {
121+
root: true,
122+
versions: ['4', '3'],
123+
}
124+
}
125+
126+
// Pretty sure it's not related to Tailwind at all
127+
return {
128+
root: false,
129+
versions: [],
130+
}
53131
}

packages/tailwindcss-language-server/tests/env/v4.test.js

+47
Original file line numberDiff line numberDiff line change
@@ -574,3 +574,50 @@ defineTest({
574574
})
575575
},
576576
})
577+
578+
defineTest({
579+
options: {
580+
only: true,
581+
},
582+
name: 'what',
583+
fs: {
584+
'buttons.css': css`
585+
.foo {
586+
@apply bg-black;
587+
}
588+
`,
589+
'styles.css': css`
590+
@import 'tailwindcss';
591+
`,
592+
},
593+
prepare: async ({ root }) => ({ c: await init(root) }),
594+
handle: async ({ c }) => {
595+
let document = await c.openDocument({
596+
lang: 'html',
597+
text: '<div class="bg-black">',
598+
})
599+
600+
let hover = await c.sendRequest(HoverRequest.type, {
601+
textDocument: document,
602+
603+
// <div class="bg-black">
604+
// ^
605+
position: { line: 0, character: 13 },
606+
})
607+
608+
expect(hover).toEqual({
609+
contents: {
610+
language: 'css',
611+
value: dedent`
612+
.bg-black {
613+
background-color: var(--color-black) /* #000 = #000000 */;
614+
}
615+
`,
616+
},
617+
range: {
618+
start: { line: 0, character: 12 },
619+
end: { line: 0, character: 20 },
620+
},
621+
})
622+
},
623+
})

packages/vscode-tailwindcss/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- Show light color swatch from light-dark() functions ([#1199](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1199))
66
- Ignore comments when matching class attributes ([#1202](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1202))
77
- Show source diagnostics when imports contain a layer ([#1204](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1204))
8+
- Only detect project roots in v4 when using certain CSS features ([#1205](https://github.com/tailwindlabs/tailwindcss-intellisense/pull/1205))
89

910
## 0.14.4
1011

0 commit comments

Comments
 (0)