1
1
// @ts -check
2
+ import * as fs from 'fs/promises'
3
+ import { createRequire } from 'module'
4
+ import * as path from 'path'
2
5
import clearModule from 'clear-module'
3
6
import escalade from 'escalade/sync'
4
- import * as path from 'path'
7
+ import postcss from 'postcss'
8
+ import postcssImport from 'postcss-import'
5
9
import prettier from 'prettier'
6
- import resolveFrom from 'resolve-from'
7
10
// @ts -ignore
8
11
import { generateRules as generateRulesFallback } from 'tailwindcss/lib/lib/generateRules'
9
12
// @ts -ignore
@@ -12,6 +15,8 @@ import loadConfigFallback from 'tailwindcss/loadConfig'
12
15
import resolveConfigFallback from 'tailwindcss/resolveConfig'
13
16
import { expiringMap } from './expiring-map.js'
14
17
18
+ let localRequire = createRequire ( import . meta. url )
19
+
15
20
/** @typedef {import('prettier').ParserOptions } ParserOptions **/
16
21
/** @typedef {import('./types.js').ContextContainer } ContextContainer **/
17
22
@@ -24,6 +29,9 @@ import { expiringMap } from './expiring-map.js'
24
29
/** @type {Map<string, string | null> } */
25
30
let sourceToPathMap = new Map ( )
26
31
32
+ /** @type {Map<string, string | null> } */
33
+ let sourceToEntryMap = new Map ( )
34
+
27
35
/** @type {ExpiringMap<string | null, ContextContainer> } */
28
36
let pathToContextMap = expiringMap ( 10_000 )
29
37
@@ -35,7 +43,7 @@ let prettierConfigCache = expiringMap(10_000)
35
43
* @returns {Promise<ContextContainer> }
36
44
*/
37
45
export async function getTailwindConfig ( options ) {
38
- let key = `${ options . filepath } :${ options . tailwindConfig ?? '' } `
46
+ let key = `${ options . filepath } :${ options . tailwindConfig ?? '' } : ${ options . tailwindEntryPoint ?? '' } `
39
47
let baseDir = await getBaseDir ( options )
40
48
41
49
// Map the source file to it's associated Tailwind config file
@@ -45,16 +53,23 @@ export async function getTailwindConfig(options) {
45
53
sourceToPathMap . set ( key , configPath )
46
54
}
47
55
56
+ let entryPoint = sourceToEntryMap . get ( key )
57
+ if ( entryPoint === undefined ) {
58
+ entryPoint = getEntryPoint ( options , baseDir )
59
+ sourceToEntryMap . set ( key , entryPoint )
60
+ }
61
+
48
62
// Now see if we've loaded the Tailwind config file before (and it's still valid)
49
- let existing = pathToContextMap . get ( configPath )
63
+ let contextKey = `${ configPath } :${ entryPoint } `
64
+ let existing = pathToContextMap . get ( contextKey )
50
65
if ( existing ) {
51
66
return existing
52
67
}
53
68
54
69
// By this point we know we need to load the Tailwind config file
55
- let result = loadTailwindConfig ( baseDir , configPath )
70
+ let result = await loadTailwindConfig ( baseDir , configPath , entryPoint )
56
71
57
- pathToContextMap . set ( configPath , result )
72
+ pathToContextMap . set ( contextKey , result )
58
73
59
74
return result
60
75
}
@@ -88,38 +103,51 @@ async function getBaseDir(options) {
88
103
return prettierConfigPath ? path . dirname ( prettierConfigPath ) : process . cwd ( )
89
104
}
90
105
106
+ if ( options . tailwindEntryPoint ) {
107
+ return prettierConfigPath ? path . dirname ( prettierConfigPath ) : process . cwd ( )
108
+ }
109
+
91
110
return prettierConfigPath
92
111
? path . dirname ( prettierConfigPath )
93
112
: options . filepath
94
- ? path . dirname ( options . filepath )
95
- : process . cwd ( )
113
+ ? path . dirname ( options . filepath )
114
+ : process . cwd ( )
96
115
}
97
116
98
117
/**
99
- *
100
118
* @param {string } baseDir
101
119
* @param {string | null } tailwindConfigPath
102
- * @returns {ContextContainer }
120
+ * @param {string | null } entryPoint
121
+ * @returns {Promise<ContextContainer> }
103
122
*/
104
- function loadTailwindConfig ( baseDir , tailwindConfigPath ) {
123
+ async function loadTailwindConfig ( baseDir , tailwindConfigPath , entryPoint ) {
105
124
let createContext = createContextFallback
106
125
let generateRules = generateRulesFallback
107
126
let resolveConfig = resolveConfigFallback
108
127
let loadConfig = loadConfigFallback
109
128
let tailwindConfig = { }
110
129
111
130
try {
112
- let pkgDir = path . dirname ( resolveFrom ( baseDir , 'tailwindcss/package.json' ) )
131
+ let pkgFile = localRequire . resolve ( 'tailwindcss/package.json' , {
132
+ paths : [ baseDir ] ,
133
+ } )
134
+
135
+ let pkgDir = path . dirname ( pkgFile )
136
+
137
+ try {
138
+ let v4 = await loadV4 ( baseDir , pkgDir , entryPoint )
139
+ if ( v4 ) {
140
+ return v4
141
+ }
142
+ } catch { }
113
143
114
144
resolveConfig = require ( path . join ( pkgDir , 'resolveConfig' ) )
115
- createContext = require ( path . join (
116
- pkgDir ,
117
- 'lib/lib/setupContextUtils' ,
118
- ) ) . createContext
119
- generateRules = require ( path . join (
120
- pkgDir ,
121
- 'lib/lib/generateRules' ,
122
- ) ) . generateRules
145
+ createContext = require (
146
+ path . join ( pkgDir , 'lib/lib/setupContextUtils' ) ,
147
+ ) . createContext
148
+ generateRules = require (
149
+ path . join ( pkgDir , 'lib/lib/generateRules' ) ,
150
+ ) . generateRules
123
151
124
152
// Prior to `[email protected] ` this won't exist so we load it last
125
153
loadConfig = require ( path . join ( pkgDir , 'loadConfig' ) )
@@ -139,11 +167,53 @@ function loadTailwindConfig(baseDir, tailwindConfigPath) {
139
167
140
168
return {
141
169
context,
142
- tailwindConfig,
143
170
generateRules,
144
171
}
145
172
}
146
173
174
+ /**
175
+ * @param {string } baseDir
176
+ * @param {string } pkgDir
177
+ * @param {string | null } entryPoint
178
+ */
179
+ async function loadV4 ( baseDir , pkgDir , entryPoint ) {
180
+ // Import Tailwind — if this is v4 it'll have APIs we can use directly
181
+ let pkgPath = localRequire . resolve ( 'tailwindcss' , {
182
+ paths : [ baseDir ] ,
183
+ } )
184
+
185
+ let tw = await import ( pkgPath )
186
+
187
+ // This is not Tailwind v4
188
+ if ( ! tw . loadDesignSystem ) {
189
+ return null
190
+ }
191
+
192
+ // If the user doesn't define an entrypoint then we use the default theme
193
+ entryPoint = entryPoint ?? `${ pkgDir } /theme.css`
194
+
195
+ // Resolve imports in the entrypoint to a flat CSS tree
196
+ let css = await fs . readFile ( entryPoint , 'utf-8' )
197
+ let resolveImports = postcss ( [ postcssImport ( ) ] )
198
+ let result = await resolveImports . process ( css , { from : entryPoint } )
199
+
200
+ // Load the design system and set up a compatible context object that is
201
+ // usable by the rest of the plugin
202
+ let design = tw . loadDesignSystem ( result . css )
203
+
204
+ return {
205
+ context : {
206
+ /**
207
+ * @param {string[] } classList
208
+ */
209
+ getClassOrder : ( classList ) => design . getClassOrder ( classList ) ,
210
+ } ,
211
+
212
+ // Stubs that are not needed for v4
213
+ generateRules : ( ) => [ ] ,
214
+ }
215
+ }
216
+
147
217
/**
148
218
* @param {ParserOptions } options
149
219
* @param {string } baseDir
@@ -178,3 +248,16 @@ function getConfigPath(options, baseDir) {
178
248
179
249
return null
180
250
}
251
+
252
+ /**
253
+ * @param {ParserOptions } options
254
+ * @param {string } baseDir
255
+ * @returns {string | null }
256
+ */
257
+ function getEntryPoint ( options , baseDir ) {
258
+ if ( options . tailwindEntryPoint ) {
259
+ return path . resolve ( baseDir , options . tailwindEntryPoint )
260
+ }
261
+
262
+ return null
263
+ }
0 commit comments