Skip to content

Commit 81c446e

Browse files
Add preliminary support for v4 (#249)
* Refactor * Add support for loading v4 * Update changelog
1 parent 1fa24c2 commit 81c446e

File tree

7 files changed

+149
-34
lines changed

7 files changed

+149
-34
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- Add support for `prettier-plugin-sort-imports` ([#241](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/241))
13+
- Add support for Tailwind CSS v4.0 ([#249](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/249))
1314

1415
## [0.5.11] - 2024-01-05
1516

package-lock.json

Lines changed: 30 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
"license-checker": "^25.0.1",
4646
"line-column": "^1.0.2",
4747
"marko": "^5.31.18",
48+
"postcss": "^8.4.35",
49+
"postcss-import": "^16.0.1",
4850
"prettier": "^3.2",
4951
"prettier-plugin-astro": "^0.12.2",
5052
"prettier-plugin-css-order": "^2.0.0",

src/config.js

Lines changed: 104 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
// @ts-check
2+
import * as fs from 'fs/promises'
3+
import { createRequire } from 'module'
4+
import * as path from 'path'
25
import clearModule from 'clear-module'
36
import escalade from 'escalade/sync'
4-
import * as path from 'path'
7+
import postcss from 'postcss'
8+
import postcssImport from 'postcss-import'
59
import prettier from 'prettier'
6-
import resolveFrom from 'resolve-from'
710
// @ts-ignore
811
import { generateRules as generateRulesFallback } from 'tailwindcss/lib/lib/generateRules'
912
// @ts-ignore
@@ -12,6 +15,8 @@ import loadConfigFallback from 'tailwindcss/loadConfig'
1215
import resolveConfigFallback from 'tailwindcss/resolveConfig'
1316
import { 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>} */
2530
let sourceToPathMap = new Map()
2631

32+
/** @type {Map<string, string | null>} */
33+
let sourceToEntryMap = new Map()
34+
2735
/** @type {ExpiringMap<string | null, ContextContainer>} */
2836
let pathToContextMap = expiringMap(10_000)
2937

@@ -35,7 +43,7 @@ let prettierConfigCache = expiringMap(10_000)
3543
* @returns {Promise<ContextContainer>}
3644
*/
3745
export 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+
}

src/index.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
import { Parser, Printer, SupportOption } from 'prettier';
1+
import { Parser, Printer, SupportOption } from 'prettier'
22

33
export interface PluginOptions {
44
/**
55
* Path to the Tailwind config file.
66
*/
77
tailwindConfig?: string
88

9+
/**
10+
* Path to the Tailwind entry point (v4+)
11+
*/
12+
tailwindEntryPoint?: string
13+
914
/**
1015
* List of custom function and tag names that contain classes.
1116
*/

src/options.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ export const options = {
66
category: 'Tailwind CSS',
77
description: 'Path to Tailwind configuration file',
88
},
9+
tailwindEntryPoint: {
10+
since: '0.0.0',
11+
type: 'string',
12+
category: 'Tailwind CSS',
13+
description: 'Path to the CSS entrypoint in your Tailwind project (v4+)',
14+
},
915
tailwindAttributes: {
1016
since: '0.3.0',
1117
type: 'string',

src/types.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ export interface TransformerEnv {
2929
export interface ContextContainer {
3030
context: any
3131
generateRules: () => any
32-
tailwindConfig: any
3332
}
3433

3534
export interface InternalOptions {

0 commit comments

Comments
 (0)