From 35bf291fbfee14bcf4d64539fb349216a74c97c7 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 19 Feb 2024 11:45:54 -0500 Subject: [PATCH 1/3] Refactor --- src/config.js | 28 +++++++++++++--------------- src/types.d.ts | 1 - 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/config.js b/src/config.js index 9f2ce8b..a154d46 100644 --- a/src/config.js +++ b/src/config.js @@ -52,7 +52,7 @@ export async function getTailwindConfig(options) { } // By this point we know we need to load the Tailwind config file - let result = loadTailwindConfig(baseDir, configPath) + let result = await loadTailwindConfig(baseDir, configPath) pathToContextMap.set(configPath, result) @@ -91,17 +91,17 @@ async function getBaseDir(options) { 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} + * @returns {Promise} */ -function loadTailwindConfig(baseDir, tailwindConfigPath) { +async function loadTailwindConfig(baseDir, tailwindConfigPath) { let createContext = createContextFallback let generateRules = generateRulesFallback let resolveConfig = resolveConfigFallback @@ -109,17 +109,16 @@ function loadTailwindConfig(baseDir, tailwindConfigPath) { let tailwindConfig = {} try { - let pkgDir = path.dirname(resolveFrom(baseDir, 'tailwindcss/package.json')) + let pkgFile = resolveFrom(baseDir, 'tailwindcss/package.json') + let pkgDir = path.dirname(pkgFile) 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')) @@ -139,7 +138,6 @@ function loadTailwindConfig(baseDir, tailwindConfigPath) { return { context, - tailwindConfig, generateRules, } } diff --git a/src/types.d.ts b/src/types.d.ts index 12a038e..3a0b571 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -29,7 +29,6 @@ export interface TransformerEnv { export interface ContextContainer { context: any generateRules: () => any - tailwindConfig: any } export interface InternalOptions { From 36f77922f22cb43142e3a3f0aa486adf89a76ffd Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 19 Feb 2024 11:53:37 -0500 Subject: [PATCH 2/3] Add support for loading v4 --- package-lock.json | 41 +++++++++++++----- package.json | 2 + src/config.js | 103 ++++++++++++++++++++++++++++++++++++++++++---- src/index.d.ts | 7 +++- src/options.js | 6 +++ 5 files changed, 138 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4ec3659..6e154ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,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", @@ -7117,9 +7119,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, "funding": [ { @@ -7557,9 +7559,9 @@ } }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "dev": true, "funding": [ { @@ -7576,7 +7578,7 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -7585,9 +7587,9 @@ } }, "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-16.0.1.tgz", + "integrity": "sha512-i2Pci0310NaLHr/5JUFSw1j/8hf1CzwMY13g6ZDxgOavmRHQi2ba3PmUHoihO+sjaum+KmCNzskNsw7JDrg03g==", "dev": true, "dependencies": { "postcss-value-parser": "^4.0.0", @@ -7595,7 +7597,7 @@ "resolve": "^1.1.7" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" }, "peerDependencies": { "postcss": "^8.0.0" @@ -8812,6 +8814,23 @@ "node": ">= 6" } }, + "node_modules/tailwindcss/node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", diff --git a/package.json b/package.json index 46779e8..cd39b19 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/config.js b/src/config.js index a154d46..4be210b 100644 --- a/src/config.js +++ b/src/config.js @@ -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 @@ -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 **/ @@ -24,6 +29,9 @@ import { expiringMap } from './expiring-map.js' /** @type {Map} */ let sourceToPathMap = new Map() +/** @type {Map} */ +let sourceToEntryMap = new Map() + /** @type {ExpiringMap} */ let pathToContextMap = expiringMap(10_000) @@ -35,7 +43,7 @@ let prettierConfigCache = expiringMap(10_000) * @returns {Promise} */ 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 @@ -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 = await loadTailwindConfig(baseDir, configPath) + let result = await loadTailwindConfig(baseDir, configPath, entryPoint) - pathToContextMap.set(configPath, result) + pathToContextMap.set(contextKey, result) return result } @@ -88,6 +103,10 @@ 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 @@ -96,12 +115,12 @@ async function getBaseDir(options) { } /** - * * @param {string} baseDir * @param {string | null} tailwindConfigPath + * @param {string | null} entryPoint * @returns {Promise} */ -async function loadTailwindConfig(baseDir, tailwindConfigPath) { +async function loadTailwindConfig(baseDir, tailwindConfigPath, entryPoint) { let createContext = createContextFallback let generateRules = generateRulesFallback let resolveConfig = resolveConfigFallback @@ -109,9 +128,19 @@ async function loadTailwindConfig(baseDir, tailwindConfigPath) { let tailwindConfig = {} try { - let pkgFile = 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'), @@ -142,6 +171,49 @@ async function loadTailwindConfig(baseDir, tailwindConfigPath) { } } +/** + * @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 @@ -176,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 +} diff --git a/src/index.d.ts b/src/index.d.ts index 45957be..3f1a176 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,4 +1,4 @@ -import { Parser, Printer, SupportOption } from 'prettier'; +import { Parser, Printer, SupportOption } from 'prettier' export interface PluginOptions { /** @@ -6,6 +6,11 @@ export interface PluginOptions { */ tailwindConfig?: string + /** + * Path to the Tailwind entry point (v4+) + */ + tailwindEntryPoint?: string + /** * List of custom function and tag names that contain classes. */ diff --git a/src/options.js b/src/options.js index 37aa417..b08cfbe 100644 --- a/src/options.js +++ b/src/options.js @@ -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', From a3a7ec5593906a40da2e689dff61e8479b5782f8 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Wed, 28 Feb 2024 12:34:25 -0500 Subject: [PATCH 3/3] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc2daef..ebdf4d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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