Skip to content

Commit

Permalink
Add preliminary support for v4 (#249)
Browse files Browse the repository at this point in the history
* Refactor

* Add support for loading v4

* Update changelog
  • Loading branch information
thecrypticace committed Feb 28, 2024
1 parent 1fa24c2 commit 81c446e
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 34 deletions.
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 `tailwindcss@3.3.0` 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

0 comments on commit 81c446e

Please sign in to comment.