Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

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

## [0.5.11] - 2024-01-05

Expand Down
41 changes: 30 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
"license-checker": "^25.0.1",
"line-column": "^1.0.2",
"marko": "^5.31.18",
"postcss": "^8.4.35",
"postcss-import": "^16.0.1",
"prettier": "^3.2",
"prettier-plugin-astro": "^0.12.2",
"prettier-plugin-css-order": "^2.0.0",
Expand Down
125 changes: 104 additions & 21 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// @ts-check
import * as fs from 'fs/promises'
import { createRequire } from 'module'
import * as path from 'path'
import clearModule from 'clear-module'
import escalade from 'escalade/sync'
import * as path from 'path'
import postcss from 'postcss'
import postcssImport from 'postcss-import'
import prettier from 'prettier'
import resolveFrom from 'resolve-from'
// @ts-ignore
import { generateRules as generateRulesFallback } from 'tailwindcss/lib/lib/generateRules'
// @ts-ignore
Expand All @@ -12,6 +15,8 @@ import loadConfigFallback from 'tailwindcss/loadConfig'
import resolveConfigFallback from 'tailwindcss/resolveConfig'
import { expiringMap } from './expiring-map.js'

let localRequire = createRequire(import.meta.url)

/** @typedef {import('prettier').ParserOptions} ParserOptions **/
/** @typedef {import('./types.js').ContextContainer} ContextContainer **/

Expand All @@ -24,6 +29,9 @@ import { expiringMap } from './expiring-map.js'
/** @type {Map<string, string | null>} */
let sourceToPathMap = new Map()

/** @type {Map<string, string | null>} */
let sourceToEntryMap = new Map()

/** @type {ExpiringMap<string | null, ContextContainer>} */
let pathToContextMap = expiringMap(10_000)

Expand All @@ -35,7 +43,7 @@ let prettierConfigCache = expiringMap(10_000)
* @returns {Promise<ContextContainer>}
*/
export async function getTailwindConfig(options) {
let key = `${options.filepath}:${options.tailwindConfig ?? ''}`
let key = `${options.filepath}:${options.tailwindConfig ?? ''}:${options.tailwindEntryPoint ?? ''}`
let baseDir = await getBaseDir(options)

// Map the source file to it's associated Tailwind config file
Expand All @@ -45,16 +53,23 @@ export async function getTailwindConfig(options) {
sourceToPathMap.set(key, configPath)
}

let entryPoint = sourceToEntryMap.get(key)
if (entryPoint === undefined) {
entryPoint = getEntryPoint(options, baseDir)
sourceToEntryMap.set(key, entryPoint)
}

// Now see if we've loaded the Tailwind config file before (and it's still valid)
let existing = pathToContextMap.get(configPath)
let contextKey = `${configPath}:${entryPoint}`
let existing = pathToContextMap.get(contextKey)
if (existing) {
return existing
}

// By this point we know we need to load the Tailwind config file
let result = loadTailwindConfig(baseDir, configPath)
let result = await loadTailwindConfig(baseDir, configPath, entryPoint)

pathToContextMap.set(configPath, result)
pathToContextMap.set(contextKey, result)

return result
}
Expand Down Expand Up @@ -88,38 +103,51 @@ async function getBaseDir(options) {
return prettierConfigPath ? path.dirname(prettierConfigPath) : process.cwd()
}

if (options.tailwindEntryPoint) {
return prettierConfigPath ? path.dirname(prettierConfigPath) : process.cwd()
}

return prettierConfigPath
? path.dirname(prettierConfigPath)
: options.filepath
? path.dirname(options.filepath)
: process.cwd()
? path.dirname(options.filepath)
: process.cwd()
}

/**
*
* @param {string} baseDir
* @param {string | null} tailwindConfigPath
* @returns {ContextContainer}
* @param {string | null} entryPoint
* @returns {Promise<ContextContainer>}
*/
function loadTailwindConfig(baseDir, tailwindConfigPath) {
async function loadTailwindConfig(baseDir, tailwindConfigPath, entryPoint) {
let createContext = createContextFallback
let generateRules = generateRulesFallback
let resolveConfig = resolveConfigFallback
let loadConfig = loadConfigFallback
let tailwindConfig = {}

try {
let pkgDir = path.dirname(resolveFrom(baseDir, 'tailwindcss/package.json'))
let pkgFile = localRequire.resolve('tailwindcss/package.json', {
paths: [baseDir],
})

let pkgDir = path.dirname(pkgFile)

try {
let v4 = await loadV4(baseDir, pkgDir, entryPoint)
if (v4) {
return v4
}
} catch {}

resolveConfig = require(path.join(pkgDir, 'resolveConfig'))
createContext = require(path.join(
pkgDir,
'lib/lib/setupContextUtils',
)).createContext
generateRules = require(path.join(
pkgDir,
'lib/lib/generateRules',
)).generateRules
createContext = require(
path.join(pkgDir, 'lib/lib/setupContextUtils'),
).createContext
generateRules = require(
path.join(pkgDir, 'lib/lib/generateRules'),
).generateRules

// Prior to `[email protected]` this won't exist so we load it last
loadConfig = require(path.join(pkgDir, 'loadConfig'))
Expand All @@ -139,11 +167,53 @@ function loadTailwindConfig(baseDir, tailwindConfigPath) {

return {
context,
tailwindConfig,
generateRules,
}
}

/**
* @param {string} baseDir
* @param {string} pkgDir
* @param {string | null} entryPoint
*/
async function loadV4(baseDir, pkgDir, entryPoint) {
// Import Tailwind — if this is v4 it'll have APIs we can use directly
let pkgPath = localRequire.resolve('tailwindcss', {
paths: [baseDir],
})

let tw = await import(pkgPath)

// This is not Tailwind v4
if (!tw.loadDesignSystem) {
return null
}

// If the user doesn't define an entrypoint then we use the default theme
entryPoint = entryPoint ?? `${pkgDir}/theme.css`

// Resolve imports in the entrypoint to a flat CSS tree
let css = await fs.readFile(entryPoint, 'utf-8')
let resolveImports = postcss([postcssImport()])
let result = await resolveImports.process(css, { from: entryPoint })

// Load the design system and set up a compatible context object that is
// usable by the rest of the plugin
let design = tw.loadDesignSystem(result.css)

return {
context: {
/**
* @param {string[]} classList
*/
getClassOrder: (classList) => design.getClassOrder(classList),
},

// Stubs that are not needed for v4
generateRules: () => [],
}
}

/**
* @param {ParserOptions} options
* @param {string} baseDir
Expand Down Expand Up @@ -178,3 +248,16 @@ function getConfigPath(options, baseDir) {

return null
}

/**
* @param {ParserOptions} options
* @param {string} baseDir
* @returns {string | null}
*/
function getEntryPoint(options, baseDir) {
if (options.tailwindEntryPoint) {
return path.resolve(baseDir, options.tailwindEntryPoint)
}

return null
}
7 changes: 6 additions & 1 deletion src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Parser, Printer, SupportOption } from 'prettier';
import { Parser, Printer, SupportOption } from 'prettier'

export interface PluginOptions {
/**
* Path to the Tailwind config file.
*/
tailwindConfig?: string

/**
* Path to the Tailwind entry point (v4+)
*/
tailwindEntryPoint?: string

/**
* List of custom function and tag names that contain classes.
*/
Expand Down
6 changes: 6 additions & 0 deletions src/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ export const options = {
category: 'Tailwind CSS',
description: 'Path to Tailwind configuration file',
},
tailwindEntryPoint: {
since: '0.0.0',
type: 'string',
category: 'Tailwind CSS',
description: 'Path to the CSS entrypoint in your Tailwind project (v4+)',
},
tailwindAttributes: {
since: '0.3.0',
type: 'string',
Expand Down
1 change: 0 additions & 1 deletion src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export interface TransformerEnv {
export interface ContextContainer {
context: any
generateRules: () => any
tailwindConfig: any
}

export interface InternalOptions {
Expand Down