11// @ts -check
2+ import * as fs from 'fs/promises'
3+ import { createRequire } from 'module'
4+ import * as path from 'path'
25import clearModule from 'clear-module'
36import escalade from 'escalade/sync'
4- import * as path from 'path'
7+ import postcss from 'postcss'
8+ import postcssImport from 'postcss-import'
59import prettier from 'prettier'
6- import resolveFrom from 'resolve-from'
710// @ts -ignore
811import { generateRules as generateRulesFallback } from 'tailwindcss/lib/lib/generateRules'
912// @ts -ignore
@@ -12,6 +15,8 @@ import loadConfigFallback from 'tailwindcss/loadConfig'
1215import resolveConfigFallback from 'tailwindcss/resolveConfig'
1316import { expiringMap } from './expiring-map.js'
1417
18+ let localRequire = createRequire ( import . meta. url )
19+
1520/** @typedef {import('prettier').ParserOptions } ParserOptions **/
1621/** @typedef {import('./types.js').ContextContainer } ContextContainer **/
1722
@@ -24,6 +29,9 @@ import { expiringMap } from './expiring-map.js'
2429/** @type {Map<string, string | null> } */
2530let sourceToPathMap = new Map ( )
2631
32+ /** @type {Map<string, string | null> } */
33+ let sourceToEntryMap = new Map ( )
34+
2735/** @type {ExpiringMap<string | null, ContextContainer> } */
2836let pathToContextMap = expiringMap ( 10_000 )
2937
@@ -35,7 +43,7 @@ let prettierConfigCache = expiringMap(10_000)
3543 * @returns {Promise<ContextContainer> }
3644 */
3745export async function getTailwindConfig ( options ) {
38- let key = `${ options . filepath } :${ options . tailwindConfig ?? '' } `
46+ let key = `${ options . filepath } :${ options . tailwindConfig ?? '' } : ${ options . tailwindEntryPoint ?? '' } `
3947 let baseDir = await getBaseDir ( options )
4048
4149 // Map the source file to it's associated Tailwind config file
@@ -45,16 +53,23 @@ export async function getTailwindConfig(options) {
4553 sourceToPathMap . set ( key , configPath )
4654 }
4755
56+ let entryPoint = sourceToEntryMap . get ( key )
57+ if ( entryPoint === undefined ) {
58+ entryPoint = getEntryPoint ( options , baseDir )
59+ sourceToEntryMap . set ( key , entryPoint )
60+ }
61+
4862 // 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 )
5065 if ( existing ) {
5166 return existing
5267 }
5368
5469 // 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 )
5671
57- pathToContextMap . set ( configPath , result )
72+ pathToContextMap . set ( contextKey , result )
5873
5974 return result
6075}
@@ -88,38 +103,51 @@ async function getBaseDir(options) {
88103 return prettierConfigPath ? path . dirname ( prettierConfigPath ) : process . cwd ( )
89104 }
90105
106+ if ( options . tailwindEntryPoint ) {
107+ return prettierConfigPath ? path . dirname ( prettierConfigPath ) : process . cwd ( )
108+ }
109+
91110 return prettierConfigPath
92111 ? path . dirname ( prettierConfigPath )
93112 : options . filepath
94- ? path . dirname ( options . filepath )
95- : process . cwd ( )
113+ ? path . dirname ( options . filepath )
114+ : process . cwd ( )
96115}
97116
98117/**
99- *
100118 * @param {string } baseDir
101119 * @param {string | null } tailwindConfigPath
102- * @returns {ContextContainer }
120+ * @param {string | null } entryPoint
121+ * @returns {Promise<ContextContainer> }
103122 */
104- function loadTailwindConfig ( baseDir , tailwindConfigPath ) {
123+ async function loadTailwindConfig ( baseDir , tailwindConfigPath , entryPoint ) {
105124 let createContext = createContextFallback
106125 let generateRules = generateRulesFallback
107126 let resolveConfig = resolveConfigFallback
108127 let loadConfig = loadConfigFallback
109128 let tailwindConfig = { }
110129
111130 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 { }
113143
114144 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
123151
124152 // Prior to `[email protected] ` this won't exist so we load it last 125153 loadConfig = require ( path . join ( pkgDir , 'loadConfig' ) )
@@ -139,11 +167,53 @@ function loadTailwindConfig(baseDir, tailwindConfigPath) {
139167
140168 return {
141169 context,
142- tailwindConfig,
143170 generateRules,
144171 }
145172}
146173
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+
147217/**
148218 * @param {ParserOptions } options
149219 * @param {string } baseDir
@@ -178,3 +248,16 @@ function getConfigPath(options, baseDir) {
178248
179249 return null
180250}
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