From c3cf4c561821ec226656840907db787ab31d3edd Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 1 Dec 2025 15:23:56 -0500 Subject: [PATCH 01/16] Update types --- src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index e0802d77..7eb6ba25 100644 --- a/src/config.ts +++ b/src/config.ts @@ -13,7 +13,7 @@ import { loadV4 } from './versions/v4' let pathToApiMap = expiringMap>(10_000) -export async function getTailwindConfig(options: ParserOptions): Promise { +export async function getTailwindConfig(options: ParserOptions): Promise { let cwd = process.cwd() // Locate the file being processed From dd48bcfd78140fde96a421467bc105149eca2bb8 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 1 Dec 2025 13:23:27 -0500 Subject: [PATCH 02/16] Handle CSS imports when running tests --- vitest.config.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/vitest.config.ts b/vitest.config.ts index d94857a4..9b6b73ab 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,5 +3,18 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { testTimeout: 10000, + css: true, }, + + plugins: [ + { + name: 'force-inline-css', + enforce: 'pre', + resolveId(id) { + if (!id.endsWith('.css')) return + if (id.includes('?raw')) return + return this.resolve(`${id}?raw`) + }, + }, + ], }) From 3c5f6965961957d701ccd8a120400987b7cc28b0 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 1 Dec 2025 13:26:25 -0500 Subject: [PATCH 03/16] Remove old `pluginSearchDirs` setting It stopped being used with Prettier v3 --- tests/utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/utils.ts b/tests/utils.ts index 1e3ec5ab..5a638382 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -41,7 +41,6 @@ export let pluginPath = path.resolve(__dirname, '../dist/index.mjs') export async function format(str: string, options: prettier.Options = {}) { let result = await prettier.format(str, { - pluginSearchDirs: [__dirname], // disable plugin autoload semi: false, singleQuote: true, printWidth: 9999, From dc0a80fe4b0c6f786299b74050032ca03cdb3e1b Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 1 Dec 2025 13:26:56 -0500 Subject: [PATCH 04/16] Rerun tests when editing source files --- tests/utils.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/utils.ts b/tests/utils.ts index 5a638382..164b6981 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -40,13 +40,20 @@ export function t(strings: TemplateStringsArray, ...values: string[]): TestEntry export let pluginPath = path.resolve(__dirname, '../dist/index.mjs') export async function format(str: string, options: prettier.Options = {}) { + let plugin: prettier.Plugin = (await import('../src/index.ts')) as any + let result = await prettier.format(str, { semi: false, singleQuote: true, printWidth: 9999, parser: 'html', ...options, - plugins: [...(options.plugins ?? []), pluginPath], + plugins: [ + // + ...(options.plugins ?? []), + // plugin, + pluginPath, + ], }) return result.trim() From 021f4aca293bddab1aa4d0ed8900db7f24dea831 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 1 Dec 2025 15:23:57 -0500 Subject: [PATCH 05/16] Refactor sorting code --- src/index.ts | 5 ++-- src/sorting.ts | 71 +++++++++++++++++++++++++------------------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/index.ts b/src/index.ts index 6d29d2ba..2e4bf2d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -961,8 +961,9 @@ function transformPug(ast: any, { env }: TransformerContext) { for (const [startIdx, endIdx] of ranges) { const classes = ast.tokens.slice(startIdx, endIdx + 1).map((token: any) => token.val) - const { classList } = sortClassList(classes, { - env, + const { classList } = sortClassList({ + classList: classes, + api: env.context, removeDuplicates: false, }) diff --git a/src/sorting.ts b/src/sorting.ts index f11ad33c..ecceb504 100644 --- a/src/sorting.ts +++ b/src/sorting.ts @@ -1,19 +1,11 @@ -import type { TransformerEnv } from './types' +import type { TransformerEnv, UnifiedApi } from './types' import { bigSign } from './utils' -function reorderClasses(classList: string[], { env }: { env: TransformerEnv }) { - let orderedClasses = env.context.getClassOrder(classList) - - return orderedClasses.sort(([nameA, a], [nameZ, z]) => { - // Move `...` to the end of the list - if (nameA === '...' || nameA === '…') return 1 - if (nameZ === '...' || nameZ === '…') return -1 - - if (a === z) return 0 - if (a === null) return -1 - if (z === null) return 1 - return bigSign(a - z) - }) +export interface SortOptions { + ignoreFirst?: boolean + ignoreLast?: boolean + removeDuplicates?: boolean + collapseWhitespace?: false | { start: boolean; end: boolean } } export function sortClasses( @@ -24,12 +16,8 @@ export function sortClasses( ignoreLast = false, removeDuplicates = true, collapseWhitespace = { start: true, end: true }, - }: { + }: SortOptions & { env: TransformerEnv - ignoreFirst?: boolean - ignoreLast?: boolean - removeDuplicates?: boolean - collapseWhitespace?: false | { start: boolean; end: boolean } }, ): string { if (typeof classStr !== 'string' || classStr === '') { @@ -46,6 +34,10 @@ export function sortClasses( collapseWhitespace = false } + if (env.options.tailwindPreserveDuplicates) { + removeDuplicates = false + } + // This class list is purely whitespace // Collapse it to a single space if the option is enabled if (/^[\t\r\f\n ]+$/.test(classStr) && collapseWhitespace) { @@ -75,8 +67,9 @@ export function sortClasses( suffix = `${whitespace.pop() ?? ''}${classes.pop() ?? ''}` } - let { classList, removedIndices } = sortClassList(classes, { - env, + let { classList, removedIndices } = sortClassList({ + classList: classes, + api: env.context, removeDuplicates, }) @@ -99,24 +92,30 @@ export function sortClasses( return prefix + result + suffix } -export function sortClassList( - classList: string[], - { - env, - removeDuplicates, - }: { - env: TransformerEnv - removeDuplicates: boolean - }, -) { +export function sortClassList({ + classList, + api, + removeDuplicates, +}: { + classList: string[] + api: UnifiedApi + removeDuplicates: boolean +}) { // Re-order classes based on the Tailwind CSS configuration - let orderedClasses = reorderClasses(classList, { env }) + let orderedClasses = api.getClassOrder(classList) - // Remove duplicate Tailwind classes - if (env.options.tailwindPreserveDuplicates) { - removeDuplicates = false - } + orderedClasses.sort(([nameA, a], [nameZ, z]) => { + // Move `...` to the end of the list + if (nameA === '...' || nameA === '…') return 1 + if (nameZ === '...' || nameZ === '…') return -1 + + if (a === z) return 0 + if (a === null) return -1 + if (z === null) return 1 + return bigSign(a - z) + }) + // Remove duplicate Tailwind classes let removedIndices = new Set() if (removeDuplicates) { From 65a499520ecb15b4a7b59cf30171e3b0d96d104a Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 1 Dec 2025 14:30:57 -0500 Subject: [PATCH 06/16] Combine `TransformerContext` and `TransformerEnv` --- src/index.ts | 55 +++++++++++++++++++++++++++------------------------- src/types.ts | 6 +----- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2e4bf2d8..7da9a357 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,7 @@ import { getTailwindConfig } from './config.js' import { createMatcher, type Matcher } from './options.js' import { loadPlugins } from './plugins.js' import { sortClasses, sortClassList } from './sorting.js' -import type { Customizations, StringChange, TransformerContext, TransformerEnv, TransformerMetadata } from './types' +import type { Customizations, StringChange, TransformerEnv, TransformerMetadata } from './types' import { spliceChangesIntoString, visit, type Path } from './utils.js' let base = await loadPlugins() @@ -23,7 +23,7 @@ const ESCAPE_SEQUENCE_PATTERN = /\\(['"\\nrtbfv0-7xuU])/g function createParser( parserFormat: string, - transform: (ast: any, context: TransformerContext) => void, + transform: (ast: any, env: TransformerEnv) => void, meta: TransformerMetadata = {}, ) { let customizationDefaults: Customizations = { @@ -54,15 +54,18 @@ function createParser( let matcher = createMatcher(options, parserFormat, customizationDefaults) - let changes: any[] = [] + let env: TransformerEnv = { + context, + matcher, + parsers: {}, + options, + changes: [], + } - transform(ast, { - env: { context, matcher, parsers: {}, options }, - changes, - }) + transform(ast, env) if (parserFormat === 'svelte') { - ast.changes = changes + ast.changes = env.changes } return ast @@ -271,7 +274,7 @@ function transformDynamicJsAttribute(attr: any, env: TransformerEnv) { } } -function transformHtml(ast: any, { env, changes }: TransformerContext) { +function transformHtml(ast: any, env: TransformerEnv) { let { matcher } = env let { parser } = env.options @@ -292,11 +295,11 @@ function transformHtml(ast: any, { env, changes }: TransformerContext) { } for (let child of ast.children ?? []) { - transformHtml(child, { env, changes }) + transformHtml(child, env) } } -function transformGlimmer(ast: any, { env }: TransformerContext) { +function transformGlimmer(ast: any, env: TransformerEnv) { let { matcher } = env visit(ast, { @@ -352,7 +355,7 @@ function transformGlimmer(ast: any, { env }: TransformerContext) { }) } -function transformLiquid(ast: any, { env }: TransformerContext) { +function transformLiquid(ast: any, env: TransformerEnv) { let { matcher } = env function isClassAttr(node: { name: string | { type: string; value: string }[] }) { @@ -651,7 +654,7 @@ function canCollapseWhitespaceIn(path: Path) { // // We cross several parsers that share roughly the same shape so things are // good enough. The actual AST we should be using is probably estree + ts. -function transformJavaScript(ast: import('@babel/types').Node, { env }: TransformerContext) { +function transformJavaScript(ast: import('@babel/types').Node, env: TransformerEnv) { let { matcher } = env function sortInside(ast: import('@babel/types').Node) { @@ -722,7 +725,7 @@ function transformJavaScript(ast: import('@babel/types').Node, { env }: Transfor }) } -function transformCss(ast: any, { env }: TransformerContext) { +function transformCss(ast: any, env: TransformerEnv) { // `parseValue` inside Prettier's CSS parser is private API so we have to // produce the same result by parsing an import statement with the same params function tryParseAtRuleParams(name: string, params: any) { @@ -790,7 +793,7 @@ function transformCss(ast: any, { env }: TransformerContext) { }) } -function transformAstro(ast: any, { env, changes }: TransformerContext) { +function transformAstro(ast: any, env: TransformerEnv) { let { matcher } = env if (ast.type === 'element' || ast.type === 'custom-element' || ast.type === 'component') { @@ -811,11 +814,11 @@ function transformAstro(ast: any, { env, changes }: TransformerContext) { } for (let child of ast.children ?? []) { - transformAstro(child, { env, changes }) + transformAstro(child, env) } } -function transformMarko(ast: any, { env }: TransformerContext) { +function transformMarko(ast: any, env: TransformerEnv) { let { matcher } = env const nodesToVisit = [ast] @@ -857,11 +860,11 @@ function transformMarko(ast: any, { env }: TransformerContext) { } } -function transformTwig(ast: any, { env, changes }: TransformerContext) { +function transformTwig(ast: any, env: TransformerEnv) { let { matcher } = env for (let child of ast.expressions ?? []) { - transformTwig(child, { env, changes }) + transformTwig(child, env) } visit(ast, { @@ -918,7 +921,7 @@ function transformTwig(ast: any, { env, changes }: TransformerContext) { }) } -function transformPug(ast: any, { env }: TransformerContext) { +function transformPug(ast: any, env: TransformerEnv) { let { matcher } = env // This isn't optimal @@ -973,8 +976,8 @@ function transformPug(ast: any, { env }: TransformerContext) { } } -function transformSvelte(ast: any, { env, changes }: TransformerContext) { - let { matcher } = env +function transformSvelte(ast: any, env: TransformerEnv) { + let { matcher, changes } = env for (let attr of ast.attributes ?? []) { if (!matcher.hasStaticAttr(attr.name) || attr.type !== 'Attribute') { @@ -1047,12 +1050,12 @@ function transformSvelte(ast: any, { env, changes }: TransformerContext) { } for (let child of ast.children ?? []) { - transformSvelte(child, { env, changes }) + transformSvelte(child, env) } if (ast.type === 'IfBlock') { for (let child of ast.else?.children ?? []) { - transformSvelte(child, { env, changes }) + transformSvelte(child, env) } } @@ -1060,12 +1063,12 @@ function transformSvelte(ast: any, { env, changes }: TransformerContext) { let nodes = [ast.pending, ast.then, ast.catch] for (let child of nodes) { - transformSvelte(child, { env, changes }) + transformSvelte(child, env) } } if (ast.html) { - transformSvelte(ast.html, { env, changes }) + transformSvelte(ast.html, env) } } diff --git a/src/types.ts b/src/types.ts index cdefec88..3fcad226 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,11 +17,6 @@ export interface Customizations { functionsRegex: RegExp[] } -export interface TransformerContext { - env: TransformerEnv - changes: StringChange[] -} - export interface UnifiedApi { getClassOrder(classList: string[]): [string, bigint | null][] } @@ -31,6 +26,7 @@ export interface TransformerEnv { matcher: Matcher parsers: any options: ParserOptions + changes: StringChange[] } export interface StringChange { From 630e7143cecdc804f9fdc14eaf81f2e4d3ac6f6b Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 1 Dec 2025 14:33:59 -0500 Subject: [PATCH 07/16] Refactor angular attribute parsing --- src/index.ts | 25 ++++++------------------- src/types.ts | 1 - 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7da9a357..dcb34a28 100644 --- a/src/index.ts +++ b/src/index.ts @@ -57,7 +57,6 @@ function createParser( let env: TransformerEnv = { context, matcher, - parsers: {}, options, changes: [], } @@ -74,25 +73,13 @@ function createParser( } function tryParseAngularAttribute(value: string, env: TransformerEnv) { - let parsers = [ - // Try parsing as an angular directive - prettierParserAngular.parsers.__ng_directive, - - // If this fails we fall back to arbitrary parsing of a JS expression - { parse: env.parsers.__js_expression }, - ] - - let errors: unknown[] = [] - for (const parser of parsers) { - try { - return parser.parse(value, env.parsers, env.options) - } catch (err) { - errors.push(err) - } + try { + return prettierParserAngular.parsers.__ng_directive.parse(value, env.options) + } catch (err) { + console.warn('prettier-plugin-tailwindcss: Unable to parse angular directive') + console.warn(err) + return null } - - console.warn('prettier-plugin-tailwindcss: Unable to parse angular directive') - errors.forEach((err) => console.warn(err)) } function transformDynamicAngularAttribute(attr: any, env: TransformerEnv) { diff --git a/src/types.ts b/src/types.ts index 3fcad226..833b8fb2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -24,7 +24,6 @@ export interface UnifiedApi { export interface TransformerEnv { context: UnifiedApi matcher: Matcher - parsers: any options: ParserOptions changes: StringChange[] } From 113a98be9a242806f7ca2c76a43ee3d9e81376a2 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 9 Dec 2025 15:48:54 -0500 Subject: [PATCH 08/16] Refactor parser setup --- src/create-plugin.ts | 114 +++++++++++++++++++ src/index.ts | 263 ++++++++++++++++--------------------------- src/transform.ts | 29 +++++ 3 files changed, 240 insertions(+), 166 deletions(-) create mode 100644 src/create-plugin.ts create mode 100644 src/transform.ts diff --git a/src/create-plugin.ts b/src/create-plugin.ts new file mode 100644 index 00000000..6249e821 --- /dev/null +++ b/src/create-plugin.ts @@ -0,0 +1,114 @@ +import type { Parser, ParserOptions } from 'prettier' +import { getTailwindConfig } from './config' +import { createMatcher } from './options' +import type { loadPlugins } from './plugins' +import type { TransformOptions } from './transform' +import type { Customizations, TransformerEnv, TransformerMetadata } from './types' + +type Base = Awaited> + +export function createPlugin(base: Base, transforms: TransformOptions[]) { + let parsers: Record> = Object.create(null) + + for (let opts of transforms) { + for (let [name, meta] of Object.entries(opts.parsers)) { + parsers[name] = createParser({ + base, + parserFormat: name, + opts, + }) + } + } + + return { parsers } +} + +function createParser({ + // + base, + parserFormat, + opts, +}: { + base: Base + parserFormat: string + opts: TransformOptions +}) { + return { + ...base.parsers[parserFormat], + + preprocess(code: string, options: ParserOptions) { + let original = base.originalParser(parserFormat, options) + + return original.preprocess ? original.preprocess(code, options) : code + }, + + async parse(text: string, options: ParserOptions) { + let original = base.originalParser(parserFormat, options) + + // @ts-ignore: We pass three options in the case of plugins that support Prettier 2 _and_ 3. + let ast = await original.parse(text, options, options) + + let env = await loadTailwindCSS({ opts, options }) + + transformAst({ + ast, + env, + opts, + options, + }) + + return ast + }, + } +} + +async function loadTailwindCSS({ + options, + opts, +}: { + options: ParserOptions + opts: TransformOptions +}): Promise { + let parsers = opts.parsers + let parser = options.parser as string + + let context = await getTailwindConfig(options) + + let matcher = createMatcher(options, parser, { + staticAttrs: new Set(parsers[parser]?.staticAttrs ?? []), + dynamicAttrs: new Set(parsers[parser]?.dynamicAttrs ?? []), + functions: new Set(), + staticAttrsRegex: [], + dynamicAttrsRegex: [], + functionsRegex: [], + }) + + return { + context, + matcher, + options, + changes: [], + } +} + +function transformAst({ + ast, + options, + env, + opts, +}: { + ast: T + env: TransformerEnv + options: ParserOptions + opts: TransformOptions +}) { + let transform = opts.transform + if (transform) { + transform(ast, env) + } + + if (options.parser === 'svelte') { + // @ts-ignore + ast.changes = env.changes + } +} diff --git a/src/index.ts b/src/index.ts index dcb34a28..bb44d8e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,73 +5,22 @@ import * as astTypes from 'ast-types' import jsesc from 'jsesc' // @ts-ignore import lineColumn from 'line-column' -import type { Parser, ParserOptions, Printer } from 'prettier' +import type { Printer } from 'prettier' import * as prettierParserAngular from 'prettier/plugins/angular' import * as prettierParserBabel from 'prettier/plugins/babel' // @ts-ignore import * as recast from 'recast' -import { getTailwindConfig } from './config.js' -import { createMatcher, type Matcher } from './options.js' +import { createPlugin } from './create-plugin.js' +import type { Matcher } from './options.js' import { loadPlugins } from './plugins.js' import { sortClasses, sortClassList } from './sorting.js' -import type { Customizations, StringChange, TransformerEnv, TransformerMetadata } from './types' +import type { StringChange, TransformerEnv } from './types' import { spliceChangesIntoString, visit, type Path } from './utils.js' let base = await loadPlugins() const ESCAPE_SEQUENCE_PATTERN = /\\(['"\\nrtbfv0-7xuU])/g -function createParser( - parserFormat: string, - transform: (ast: any, env: TransformerEnv) => void, - meta: TransformerMetadata = {}, -) { - let customizationDefaults: Customizations = { - staticAttrs: new Set(meta.staticAttrs ?? []), - dynamicAttrs: new Set(meta.dynamicAttrs ?? []), - functions: new Set(meta.functions ?? []), - staticAttrsRegex: [], - dynamicAttrsRegex: [], - functionsRegex: [], - } - - return { - ...base.parsers[parserFormat], - - preprocess(code: string, options: ParserOptions) { - let original = base.originalParser(parserFormat, options) - - return original.preprocess ? original.preprocess(code, options) : code - }, - - async parse(text: string, options: ParserOptions) { - let context = await getTailwindConfig(options) - - let original = base.originalParser(parserFormat, options) - - // @ts-ignore: We pass three options in the case of plugins that support Prettier 2 _and_ 3. - let ast = await original.parse(text, options, options) - - let matcher = createMatcher(options, parserFormat, customizationDefaults) - - let env: TransformerEnv = { - context, - matcher, - options, - changes: [], - } - - transform(ast, env) - - if (parserFormat === 'svelte') { - ast.changes = env.changes - } - - return ast - }, - } -} - function tryParseAngularAttribute(value: string, env: TransformerEnv) { try { return prettierParserAngular.parsers.__ng_directive.parse(value, env.options) @@ -1116,123 +1065,105 @@ export const printers: Record = (function () { return printers })() -export const parsers: Record = { - html: createParser('html', transformHtml, { - staticAttrs: ['class'], - }), - glimmer: createParser('glimmer', transformGlimmer, { - staticAttrs: ['class'], - }), - lwc: createParser('lwc', transformHtml, { - staticAttrs: ['class'], - }), - angular: createParser('angular', transformHtml, { - staticAttrs: ['class'], - dynamicAttrs: ['[ngClass]'], - }), - vue: createParser('vue', transformHtml, { - staticAttrs: ['class'], - dynamicAttrs: [':class', 'v-bind:class'], - }), - - css: createParser('css', transformCss), - scss: createParser('scss', transformCss), - less: createParser('less', transformCss), - babel: createParser('babel', transformJavaScript, { - staticAttrs: ['class', 'className'], - }), - - 'babel-flow': createParser('babel-flow', transformJavaScript, { - staticAttrs: ['class', 'className'], - }), - - flow: createParser('flow', transformJavaScript, { - staticAttrs: ['class', 'className'], - }), - - hermes: createParser('hermes', transformJavaScript, { - staticAttrs: ['class', 'className'], - }), - - typescript: createParser('typescript', transformJavaScript, { - staticAttrs: ['class', 'className'], - }), - - 'babel-ts': createParser('babel-ts', transformJavaScript, { - staticAttrs: ['class', 'className'], - }), - - oxc: createParser('oxc', transformJavaScript, { - staticAttrs: ['class', 'className'], - }), - 'oxc-ts': createParser('oxc-ts', transformJavaScript, { - staticAttrs: ['class', 'className'], - }), - - acorn: createParser('acorn', transformJavaScript, { - staticAttrs: ['class', 'className'], - }), - - meriyah: createParser('meriyah', transformJavaScript, { - staticAttrs: ['class', 'className'], - }), - - __js_expression: createParser('__js_expression', transformJavaScript, { - staticAttrs: ['class', 'className'], - }), - +export const { parsers } = createPlugin(base, [ + { + transform: transformHtml, + parsers: { + html: { staticAttrs: ['class'] }, + lwc: { staticAttrs: ['class'] }, + angular: { staticAttrs: ['class'], dynamicAttrs: ['[ngClass]'] }, + vue: { staticAttrs: ['class'], dynamicAttrs: [':class', 'v-bind:class'] }, + }, + }, + { + transform: transformGlimmer, + parsers: { + glimmer: { staticAttrs: ['class'] }, + }, + }, + { + transform: transformCss, + parsers: { + css: {}, + scss: {}, + less: {}, + }, + }, + { + transform: transformJavaScript, + parsers: { + babel: { staticAttrs: ['class', 'className'] }, + 'babel-flow': { staticAttrs: ['class', 'className'] }, + flow: { staticAttrs: ['class', 'className'] }, + hermes: { staticAttrs: ['class', 'className'] }, + typescript: { staticAttrs: ['class', 'className'] }, + 'babel-ts': { staticAttrs: ['class', 'className'] }, + oxc: { staticAttrs: ['class', 'className'] }, + 'oxc-ts': { staticAttrs: ['class', 'className'] }, + acorn: { staticAttrs: ['class', 'className'] }, + meriyah: { staticAttrs: ['class', 'className'] }, + __js_expression: { staticAttrs: ['class', 'className'] }, + ...(base.parsers.astroExpressionParser + ? { astroExpressionParser: { staticAttrs: ['class'], dynamicAttrs: ['class:list'] } } + : {}), + }, + }, ...(base.parsers.svelte - ? { - svelte: createParser('svelte', transformSvelte, { - staticAttrs: ['class'], - }), - } - : {}), + ? [ + { + transform: transformSvelte, + parsers: { + svelte: { staticAttrs: ['class'] }, + }, + }, + ] + : []), ...(base.parsers.astro - ? { - astro: createParser('astro', transformAstro, { - staticAttrs: ['class', 'className'], - dynamicAttrs: ['class:list', 'className'], - }), - } - : {}), - ...(base.parsers.astroExpressionParser - ? { - astroExpressionParser: createParser('astroExpressionParser', transformJavaScript, { - staticAttrs: ['class'], - dynamicAttrs: ['class:list'], - }), - } - : {}), + ? [ + { + transform: transformAstro, + parsers: { + astro: { + staticAttrs: ['class', 'className'], + dynamicAttrs: ['class:list', 'className'], + }, + }, + }, + ] + : []), ...(base.parsers.marko - ? { - marko: createParser('marko', transformMarko, { - staticAttrs: ['class'], - }), - } - : {}), + ? [ + { + transform: transformMarko, + parsers: { marko: { staticAttrs: ['class'] } }, + }, + ] + : []), ...(base.parsers.twig - ? { - twig: createParser('twig', transformTwig, { - staticAttrs: ['class'], - }), - } - : {}), + ? [ + { + transform: transformTwig, + parsers: { twig: { staticAttrs: ['class'] } }, + }, + ] + : []), ...(base.parsers.pug - ? { - pug: createParser('pug', transformPug, { - staticAttrs: ['class'], - }), - } - : {}), + ? [ + { + transform: transformPug, + parsers: { pug: { staticAttrs: ['class'] } }, + }, + ] + : []), ...(base.parsers['liquid-html'] - ? { - 'liquid-html': createParser('liquid-html', transformLiquid, { - staticAttrs: ['class'], - }), - } - : {}), -} + ? [ + { + transform: transformLiquid, + parsers: { 'liquid-html': { staticAttrs: ['class'] } }, + }, + ] + : []), +]) export interface PluginOptions { /** diff --git a/src/transform.ts b/src/transform.ts new file mode 100644 index 00000000..36b0fa7b --- /dev/null +++ b/src/transform.ts @@ -0,0 +1,29 @@ +import type { TransformerEnv } from './types' + +export interface TransformOptions { + /** + * A list of supported parser names + */ + parsers: Record< + string, + { + /** + * Static attributes that are supported by default + */ + staticAttrs?: string[] + + /** + * Dynamic / expression attributes that are supported by default + */ + dynamicAttrs?: string[] + } + > + + /** + * Transform entire ASTs + * + * @param ast The AST to transform + * @param env Provides options and mechanisms to sort classes + */ + transform(ast: T, env: TransformerEnv): void +} From 33742d6ad8d7a6a107ed85ed175dcec3e21abe48 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 1 Dec 2025 16:17:47 -0500 Subject: [PATCH 09/16] Lift transform definitions --- src/index.ts | 193 ++++++++++++++++++++++++----------------------- src/transform.ts | 4 + 2 files changed, 104 insertions(+), 93 deletions(-) diff --git a/src/index.ts b/src/index.ts index bb44d8e8..086caf24 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ import { createPlugin } from './create-plugin.js' import type { Matcher } from './options.js' import { loadPlugins } from './plugins.js' import { sortClasses, sortClassList } from './sorting.js' +import { defineTransform, type TransformOptions } from './transform.js' import type { StringChange, TransformerEnv } from './types' import { spliceChangesIntoString, visit, type Path } from './utils.js' @@ -1065,104 +1066,110 @@ export const printers: Record = (function () { return printers })() -export const { parsers } = createPlugin(base, [ - { - transform: transformHtml, - parsers: { - html: { staticAttrs: ['class'] }, - lwc: { staticAttrs: ['class'] }, - angular: { staticAttrs: ['class'], dynamicAttrs: ['[ngClass]'] }, - vue: { staticAttrs: ['class'], dynamicAttrs: [':class', 'v-bind:class'] }, - }, +let html = defineTransform({ + parsers: { + html: { staticAttrs: ['class'] }, + lwc: { staticAttrs: ['class'] }, + angular: { staticAttrs: ['class'], dynamicAttrs: ['[ngClass]'] }, + vue: { staticAttrs: ['class'], dynamicAttrs: [':class', 'v-bind:class'] }, }, - { - transform: transformGlimmer, - parsers: { - glimmer: { staticAttrs: ['class'] }, - }, + + transform: transformHtml, +}) + +let glimmer = defineTransform({ + parsers: { + glimmer: { staticAttrs: ['class'] }, }, - { - transform: transformCss, - parsers: { - css: {}, - scss: {}, - less: {}, - }, + + transform: transformGlimmer, +}) + +let css = defineTransform({ + parsers: { + css: {}, + scss: {}, + less: {}, }, - { - transform: transformJavaScript, - parsers: { - babel: { staticAttrs: ['class', 'className'] }, - 'babel-flow': { staticAttrs: ['class', 'className'] }, - flow: { staticAttrs: ['class', 'className'] }, - hermes: { staticAttrs: ['class', 'className'] }, - typescript: { staticAttrs: ['class', 'className'] }, - 'babel-ts': { staticAttrs: ['class', 'className'] }, - oxc: { staticAttrs: ['class', 'className'] }, - 'oxc-ts': { staticAttrs: ['class', 'className'] }, - acorn: { staticAttrs: ['class', 'className'] }, - meriyah: { staticAttrs: ['class', 'className'] }, - __js_expression: { staticAttrs: ['class', 'className'] }, - ...(base.parsers.astroExpressionParser - ? { astroExpressionParser: { staticAttrs: ['class'], dynamicAttrs: ['class:list'] } } - : {}), + + transform: transformCss, +}) + +let js = defineTransform({ + parsers: { + babel: { staticAttrs: ['class', 'className'] }, + 'babel-flow': { staticAttrs: ['class', 'className'] }, + flow: { staticAttrs: ['class', 'className'] }, + hermes: { staticAttrs: ['class', 'className'] }, + typescript: { staticAttrs: ['class', 'className'] }, + 'babel-ts': { staticAttrs: ['class', 'className'] }, + oxc: { staticAttrs: ['class', 'className'] }, + 'oxc-ts': { staticAttrs: ['class', 'className'] }, + acorn: { staticAttrs: ['class', 'className'] }, + meriyah: { staticAttrs: ['class', 'className'] }, + __js_expression: { staticAttrs: ['class', 'className'] }, + ...(base.parsers.astroExpressionParser + ? { astroExpressionParser: { staticAttrs: ['class'], dynamicAttrs: ['class:list'] } } + : {}), + }, + + transform: transformJavaScript, +}) + +let svelte = defineTransform({ + parsers: { + svelte: { staticAttrs: ['class'] }, + }, + + transform: transformSvelte, +}) + +let astro = defineTransform({ + parsers: { + astro: { + staticAttrs: ['class', 'className'], + dynamicAttrs: ['class:list', 'className'], }, }, - ...(base.parsers.svelte - ? [ - { - transform: transformSvelte, - parsers: { - svelte: { staticAttrs: ['class'] }, - }, - }, - ] - : []), - ...(base.parsers.astro - ? [ - { - transform: transformAstro, - parsers: { - astro: { - staticAttrs: ['class', 'className'], - dynamicAttrs: ['class:list', 'className'], - }, - }, - }, - ] - : []), - ...(base.parsers.marko - ? [ - { - transform: transformMarko, - parsers: { marko: { staticAttrs: ['class'] } }, - }, - ] - : []), - ...(base.parsers.twig - ? [ - { - transform: transformTwig, - parsers: { twig: { staticAttrs: ['class'] } }, - }, - ] - : []), - ...(base.parsers.pug - ? [ - { - transform: transformPug, - parsers: { pug: { staticAttrs: ['class'] } }, - }, - ] - : []), - ...(base.parsers['liquid-html'] - ? [ - { - transform: transformLiquid, - parsers: { 'liquid-html': { staticAttrs: ['class'] } }, - }, - ] - : []), + + transform: transformAstro, +}) + +let marko = defineTransform({ + parsers: { marko: { staticAttrs: ['class'] } }, + + transform: transformMarko, +}) + +let twig = defineTransform({ + parsers: { twig: { staticAttrs: ['class'] } }, + + transform: transformTwig, +}) + +let pug = defineTransform({ + parsers: { pug: { staticAttrs: ['class'] } }, + + transform: transformPug, +}) + +let liquid = defineTransform({ + parsers: { 'liquid-html': { staticAttrs: ['class'] } }, + + transform: transformLiquid, +}) + +export const { parsers } = createPlugin(base, [ + html, + glimmer, + css, + js, + ...(base.parsers.svelte ? [svelte] : []), + ...(base.parsers.astro ? [astro] : []), + ...(base.parsers.marko ? [marko] : []), + ...(base.parsers.twig ? [twig] : []), + ...(base.parsers.pug ? [pug] : []), + ...(base.parsers['liquid-html'] ? [liquid] : []), ]) export interface PluginOptions { diff --git a/src/transform.ts b/src/transform.ts index 36b0fa7b..afb5cfb7 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -1,5 +1,9 @@ import type { TransformerEnv } from './types' +export function defineTransform(opts: TransformOptions) { + return opts +} + export interface TransformOptions { /** * A list of supported parser names From 9dce827f4b31e8f1a216dc4354fcc90731054993 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 9 Dec 2025 15:50:00 -0500 Subject: [PATCH 10/16] Move Svelte AST printer into transform definition --- src/create-plugin.ts | 64 +++++++++++++++++++++++++++++------ src/index.ts | 79 ++++++++++++-------------------------------- src/transform.ts | 14 ++++++++ src/types.ts | 9 ++++- 4 files changed, 97 insertions(+), 69 deletions(-) diff --git a/src/create-plugin.ts b/src/create-plugin.ts index 6249e821..d57436b8 100644 --- a/src/create-plugin.ts +++ b/src/create-plugin.ts @@ -1,4 +1,4 @@ -import type { Parser, ParserOptions } from 'prettier' +import type { AstPath, Parser, ParserOptions, Printer } from 'prettier' import { getTailwindConfig } from './config' import { createMatcher } from './options' import type { loadPlugins } from './plugins' @@ -9,6 +9,7 @@ type Base = Awaited> export function createPlugin(base: Base, transforms: TransformOptions[]) { let parsers: Record> = Object.create(null) + let printers: Record> = Object.create(null) for (let opts of transforms) { for (let [name, meta] of Object.entries(opts.parsers)) { @@ -18,9 +19,17 @@ export function createPlugin(base: Base, transforms: TransformOptions[]) { opts, }) } + + for (let [name, meta] of Object.entries(opts.printers ?? {})) { + printers[name] = createPrinter({ + base, + name, + opts, + }) + } } - return { parsers } + return { parsers, printers } } function createParser({ @@ -57,11 +66,52 @@ function createParser({ options, }) + options.__tailwindcss__ = env + return ast }, } } +function createPrinter({ + // + base, + name, + opts, +}: { + base: Base + name: string + opts: TransformOptions +}): Printer { + let original = base.printers[name] + let printer = { ...original } + let reprint = opts.reprint + + if (reprint) { + printer.print = new Proxy(original.print, { + apply(target, thisArg, args) { + let [path, options] = args as Parameters + let env = options.__tailwindcss__ as TransformerEnv + reprint(path, { ...env, options: options }) + return Reflect.apply(target, thisArg, args) + }, + }) + + if (original.embed) { + printer.embed = new Proxy(original.embed, { + apply(target, thisArg, args) { + let [path, options] = args as Parameters + let env = options.__tailwindcss__ as TransformerEnv + reprint(path, { ...env, options: options as any }) + return Reflect.apply(target, thisArg, args) + }, + }) + } + } + + return printer +} + async function loadTailwindCSS({ options, opts, @@ -93,7 +143,6 @@ async function loadTailwindCSS({ function transformAst({ ast, - options, env, opts, }: { @@ -103,12 +152,5 @@ function transformAst({ opts: TransformOptions }) { let transform = opts.transform - if (transform) { - transform(ast, env) - } - - if (options.parser === 'svelte') { - // @ts-ignore - ast.changes = env.changes - } + if (transform) transform(ast, env) } diff --git a/src/index.ts b/src/index.ts index 086caf24..f66de1c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,6 @@ import * as astTypes from 'ast-types' import jsesc from 'jsesc' // @ts-ignore import lineColumn from 'line-column' -import type { Printer } from 'prettier' import * as prettierParserAngular from 'prettier/plugins/angular' import * as prettierParserBabel from 'prettier/plugins/babel' // @ts-ignore @@ -1011,61 +1010,6 @@ function transformSvelte(ast: any, env: TransformerEnv) { export { options } from './options.js' -export const printers: Record = (function () { - let printers: Record = {} - - if (base.printers['svelte-ast']) { - function mutateOriginalText(path: any, options: any) { - if (options.__mutatedOriginalText) { - return - } - - options.__mutatedOriginalText = true - - let changes: any[] = path.stack[0].changes - - if (changes?.length) { - let finder = lineColumn(options.originalText) - - changes = changes.map((change) => { - return { - ...change, - start: finder.toIndex(change.start.line, change.start.column + 1), - end: finder.toIndex(change.end.line, change.end.column + 1), - } - }) - - options.originalText = spliceChangesIntoString(options.originalText, changes) - } - } - - let original = base.printers['svelte-ast'] - let printer = { ...original } - - printer.print = new Proxy(original.print, { - apply(target, thisArg, args) { - let [path, options] = args as Parameters - mutateOriginalText(path, options) - return Reflect.apply(target, thisArg, args) - }, - }) - - if (original.embed) { - printer.embed = new Proxy(original.embed, { - apply(target, thisArg, args) { - let [path, options] = args as Parameters - mutateOriginalText(path, options) - return Reflect.apply(target, thisArg, args) - }, - }) - } - - printers['svelte-ast'] = printer - } - - return printers -})() - let html = defineTransform({ parsers: { html: { staticAttrs: ['class'] }, @@ -1121,7 +1065,28 @@ let svelte = defineTransform({ svelte: { staticAttrs: ['class'] }, }, + printers: { + 'svelte-ast': {}, + }, + transform: transformSvelte, + + reprint(path, { options, changes }) { + if (options.__mutatedOriginalText) return + options.__mutatedOriginalText = true + + if (!changes?.length) return + + let finder = lineColumn(options.originalText) + + let stringChanges: StringChange[] = changes.map((change) => ({ + ...change, + start: finder.toIndex(change.start.line, change.start.column + 1), + end: finder.toIndex(change.end.line, change.end.column + 1), + })) + + options.originalText = spliceChangesIntoString(options.originalText, stringChanges) + }, }) let astro = defineTransform({ @@ -1159,7 +1124,7 @@ let liquid = defineTransform({ transform: transformLiquid, }) -export const { parsers } = createPlugin(base, [ +export const { parsers, printers } = createPlugin(base, [ html, glimmer, css, diff --git a/src/transform.ts b/src/transform.ts index afb5cfb7..7baf3922 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -1,3 +1,4 @@ +import type { AstPath, ParserOptions } from 'prettier' import type { TransformerEnv } from './types' export function defineTransform(opts: TransformOptions) { @@ -23,6 +24,11 @@ export interface TransformOptions { } > + /** + * A list of supported parser names + */ + printers?: Record + /** * Transform entire ASTs * @@ -30,4 +36,12 @@ export interface TransformOptions { * @param env Provides options and mechanisms to sort classes */ transform(ast: T, env: TransformerEnv): void + + /** + * Transform entire ASTs + * + * @param ast The AST to transform + * @param env Provides options and mechanisms to sort classes + */ + reprint?(path: AstPath, options: TransformerEnv): void } diff --git a/src/types.ts b/src/types.ts index 833b8fb2..96d8463c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -25,7 +25,14 @@ export interface TransformerEnv { context: UnifiedApi matcher: Matcher options: ParserOptions - changes: StringChange[] + changes: StringChangePositional[] +} + +export interface StringChangePositional { + start: { line: number; column: number } + end: { line: number; column: number } + before: string + after: string } export interface StringChange { From ad0cf3b031516a9092472604450efbf78ac10bcc Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 1 Dec 2025 16:51:31 -0500 Subject: [PATCH 11/16] Add types --- src/index.ts | 100 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 87 insertions(+), 13 deletions(-) diff --git a/src/index.ts b/src/index.ts index f66de1c2..b3036255 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ // @ts-ignore -import type { AttrDoubleQuoted, AttrSingleQuoted } from '@shopify/prettier-plugin-liquid/dist/types.js' +import type * as Liquid from '@shopify/prettier-plugin-liquid/dist/types.js' import * as astTypes from 'ast-types' // @ts-ignore import jsesc from 'jsesc' @@ -311,7 +311,7 @@ function transformLiquid(ast: any, env: TransformerEnv) { let changes: StringChange[] = [] - function sortAttribute(attr: AttrSingleQuoted | AttrDoubleQuoted) { + function sortAttribute(attr: Liquid.AttrSingleQuoted | Liquid.AttrDoubleQuoted) { for (let i = 0; i < attr.value.length; i++) { let node = attr.value[i] if (node.type === 'TextNode') { @@ -1010,7 +1010,9 @@ function transformSvelte(ast: any, env: TransformerEnv) { export { options } from './options.js' -let html = defineTransform({ +type HtmlNode = { type: 'attribute'; name: string; value: string } | { kind: 'attribute'; name: string; value: string } + +let html = defineTransform({ parsers: { html: { staticAttrs: ['class'] }, lwc: { staticAttrs: ['class'] }, @@ -1021,7 +1023,14 @@ let html = defineTransform({ transform: transformHtml, }) -let glimmer = defineTransform({ +type GlimmerNode = + | { type: 'TextNode'; chars: string } + | { type: 'StringLiteral'; value: string } + | { type: 'ConcatStatement'; parts: GlimmerNode[] } + | { type: 'SubExpression'; path: { original: string } } + | { type: 'AttrNode'; name: string; value: GlimmerNode } + +let glimmer = defineTransform({ parsers: { glimmer: { staticAttrs: ['class'] }, }, @@ -1029,7 +1038,14 @@ let glimmer = defineTransform({ transform: transformGlimmer, }) -let css = defineTransform({ +type CssValueNode = { type: 'value-*'; name: string; params: string } +type CssNode = { + type: 'css-atrule' + name: string + params: string | CssValueNode +} + +let css = defineTransform({ parsers: { css: {}, scss: {}, @@ -1039,7 +1055,7 @@ let css = defineTransform({ transform: transformCss, }) -let js = defineTransform({ +let js = defineTransform({ parsers: { babel: { staticAttrs: ['class', 'className'] }, 'babel-flow': { staticAttrs: ['class', 'className'] }, @@ -1053,14 +1069,23 @@ let js = defineTransform({ meriyah: { staticAttrs: ['class', 'className'] }, __js_expression: { staticAttrs: ['class', 'className'] }, ...(base.parsers.astroExpressionParser - ? { astroExpressionParser: { staticAttrs: ['class'], dynamicAttrs: ['class:list'] } } + ? { + astroExpressionParser: { + staticAttrs: ['class'], + dynamicAttrs: ['class:list'], + }, + } : {}), }, transform: transformJavaScript, }) -let svelte = defineTransform({ +type SvelteNode = import('svelte/compiler').AST.SvelteNode & { + changes: StringChange[] +} + +let svelte = defineTransform({ parsers: { svelte: { staticAttrs: ['class'] }, }, @@ -1089,7 +1114,20 @@ let svelte = defineTransform({ }, }) -let astro = defineTransform({ +type AstroNode = + | { type: 'element'; attributes: Extract[] } + | { + type: 'custom-element' + attributes: Extract[] + } + | { + type: 'component' + attributes: Extract[] + } + | { type: 'attribute'; kind: 'quoted'; name: string; value: string } + | { type: 'attribute'; kind: 'expression'; name: string; value: unknown } + +let astro = defineTransform({ parsers: { astro: { staticAttrs: ['class', 'className'], @@ -1100,25 +1138,61 @@ let astro = defineTransform({ transform: transformAstro, }) -let marko = defineTransform({ +type MarkoNode = import('@marko/compiler').types.Node + +let marko = defineTransform({ parsers: { marko: { staticAttrs: ['class'] } }, transform: transformMarko, }) -let twig = defineTransform({ +type TwigIdentifier = { type: 'Identifier'; name: string } + +type TwigMemberExpression = { + type: 'MemberExpression' + property: TwigIdentifier | TwigCallExpression | TwigMemberExpression +} + +type TwigCallExpression = { + type: 'CallExpression' + callee: TwigIdentifier | TwigCallExpression | TwigMemberExpression +} + +type TwigNode = + | { type: 'Attribute'; name: TwigIdentifier } + | { type: 'StringLiteral'; value: string } + | { type: 'BinaryConcatExpression' } + | { type: 'BinaryAddExpression' } + | TwigIdentifier + | TwigMemberExpression + | TwigCallExpression + +let twig = defineTransform({ parsers: { twig: { staticAttrs: ['class'] } }, transform: transformTwig, }) -let pug = defineTransform({ +interface PugNode { + content: string + tokens: import('pug-lexer').Token[] +} + +let pug = defineTransform({ parsers: { pug: { staticAttrs: ['class'] } }, transform: transformPug, }) -let liquid = defineTransform({ +type LiquidNode = + | Liquid.TextNode + | Liquid.AttributeNode + | Liquid.LiquidTag + | Liquid.HtmlElement + | Liquid.DocumentNode + | Liquid.LiquidExpression + +let liquid = defineTransform({ parsers: { 'liquid-html': { staticAttrs: ['class'] } }, transform: transformLiquid, From 969680a431efb8eb5da15c13a3a0a821ae6c0adf Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 9 Dec 2025 13:50:03 -0500 Subject: [PATCH 12/16] Hoist common lists of attributes --- src/create-plugin.ts | 4 +-- src/index.ts | 73 +++++++++++++++++++++++++++++--------------- src/transform.ts | 10 ++++++ 3 files changed, 60 insertions(+), 27 deletions(-) diff --git a/src/create-plugin.ts b/src/create-plugin.ts index d57436b8..617511bd 100644 --- a/src/create-plugin.ts +++ b/src/create-plugin.ts @@ -125,8 +125,8 @@ async function loadTailwindCSS({ let context = await getTailwindConfig(options) let matcher = createMatcher(options, parser, { - staticAttrs: new Set(parsers[parser]?.staticAttrs ?? []), - dynamicAttrs: new Set(parsers[parser]?.dynamicAttrs ?? []), + staticAttrs: new Set(parsers[parser]?.staticAttrs ?? opts.staticAttrs ?? []), + dynamicAttrs: new Set(parsers[parser]?.dynamicAttrs ?? opts.dynamicAttrs ?? []), functions: new Set(), staticAttrsRegex: [], dynamicAttrsRegex: [], diff --git a/src/index.ts b/src/index.ts index b3036255..e6462551 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1013,11 +1013,13 @@ export { options } from './options.js' type HtmlNode = { type: 'attribute'; name: string; value: string } | { kind: 'attribute'; name: string; value: string } let html = defineTransform({ + staticAttrs: ['class'], + parsers: { - html: { staticAttrs: ['class'] }, - lwc: { staticAttrs: ['class'] }, - angular: { staticAttrs: ['class'], dynamicAttrs: ['[ngClass]'] }, - vue: { staticAttrs: ['class'], dynamicAttrs: [':class', 'v-bind:class'] }, + html: {}, + lwc: {}, + angular: { dynamicAttrs: ['[ngClass]'] }, + vue: { dynamicAttrs: [':class', 'v-bind:class'] }, }, transform: transformHtml, @@ -1031,8 +1033,10 @@ type GlimmerNode = | { type: 'AttrNode'; name: string; value: GlimmerNode } let glimmer = defineTransform({ + staticAttrs: ['class'], + parsers: { - glimmer: { staticAttrs: ['class'] }, + glimmer: {}, }, transform: transformGlimmer, @@ -1056,18 +1060,21 @@ let css = defineTransform({ }) let js = defineTransform({ + staticAttrs: ['class', 'className'], + parsers: { - babel: { staticAttrs: ['class', 'className'] }, - 'babel-flow': { staticAttrs: ['class', 'className'] }, - flow: { staticAttrs: ['class', 'className'] }, - hermes: { staticAttrs: ['class', 'className'] }, - typescript: { staticAttrs: ['class', 'className'] }, - 'babel-ts': { staticAttrs: ['class', 'className'] }, - oxc: { staticAttrs: ['class', 'className'] }, - 'oxc-ts': { staticAttrs: ['class', 'className'] }, - acorn: { staticAttrs: ['class', 'className'] }, - meriyah: { staticAttrs: ['class', 'className'] }, - __js_expression: { staticAttrs: ['class', 'className'] }, + babel: {}, + 'babel-flow': {}, + 'babel-ts': {}, + __js_expression: {}, + typescript: {}, + meriyah: {}, + acorn: {}, + flow: {}, + oxc: {}, + 'oxc-ts': {}, + hermes: {}, + ...(base.parsers.astroExpressionParser ? { astroExpressionParser: { @@ -1086,8 +1093,10 @@ type SvelteNode = import('svelte/compiler').AST.SvelteNode & { } let svelte = defineTransform({ + staticAttrs: ['class'], + parsers: { - svelte: { staticAttrs: ['class'] }, + svelte: {}, }, printers: { @@ -1128,11 +1137,11 @@ type AstroNode = | { type: 'attribute'; kind: 'expression'; name: string; value: unknown } let astro = defineTransform({ + staticAttrs: ['class', 'className'], + dynamicAttrs: ['class:list', 'className'], + parsers: { - astro: { - staticAttrs: ['class', 'className'], - dynamicAttrs: ['class:list', 'className'], - }, + astro: {}, }, transform: transformAstro, @@ -1141,7 +1150,11 @@ let astro = defineTransform({ type MarkoNode = import('@marko/compiler').types.Node let marko = defineTransform({ - parsers: { marko: { staticAttrs: ['class'] } }, + staticAttrs: ['class'], + + parsers: { + marko: {}, + }, transform: transformMarko, }) @@ -1168,7 +1181,11 @@ type TwigNode = | TwigCallExpression let twig = defineTransform({ - parsers: { twig: { staticAttrs: ['class'] } }, + staticAttrs: ['class'], + + parsers: { + twig: {}, + }, transform: transformTwig, }) @@ -1179,7 +1196,11 @@ interface PugNode { } let pug = defineTransform({ - parsers: { pug: { staticAttrs: ['class'] } }, + staticAttrs: ['class'], + + parsers: { + pug: {}, + }, transform: transformPug, }) @@ -1193,7 +1214,9 @@ type LiquidNode = | Liquid.LiquidExpression let liquid = defineTransform({ - parsers: { 'liquid-html': { staticAttrs: ['class'] } }, + staticAttrs: ['class'], + + parsers: { 'liquid-html': {} }, transform: transformLiquid, }) diff --git a/src/transform.ts b/src/transform.ts index 7baf3922..69bd8eeb 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -6,6 +6,16 @@ export function defineTransform(opts: TransformOptions) { } export interface TransformOptions { + /** + * Static attributes that are supported by default + */ + staticAttrs?: string[] + + /** + * Dynamic / expression attributes that are supported by default + */ + dynamicAttrs?: string[] + /** * A list of supported parser names */ From 02384394c1bec27fbf8f1c0774ad365dfef42c75 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 9 Dec 2025 13:57:15 -0500 Subject: [PATCH 13/16] Refactor --- src/create-plugin.ts | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/create-plugin.ts b/src/create-plugin.ts index 617511bd..1f7a9bf9 100644 --- a/src/create-plugin.ts +++ b/src/create-plugin.ts @@ -42,35 +42,36 @@ function createParser({ parserFormat: string opts: TransformOptions }) { - return { - ...base.parsers[parserFormat], + let original = base.parsers[parserFormat] + let parser: Parser = { ...original } - preprocess(code: string, options: ParserOptions) { - let original = base.originalParser(parserFormat, options) + parser.preprocess = (code: string, options: ParserOptions) => { + let original = base.originalParser(parserFormat, options) - return original.preprocess ? original.preprocess(code, options) : code - }, + return original.preprocess ? original.preprocess(code, options) : code + } - async parse(text: string, options: ParserOptions) { - let original = base.originalParser(parserFormat, options) + parser.parse = async (code: string, options: ParserOptions) => { + let original = base.originalParser(parserFormat, options) - // @ts-ignore: We pass three options in the case of plugins that support Prettier 2 _and_ 3. - let ast = await original.parse(text, options, options) + // @ts-ignore: We pass three options in the case of plugins that support Prettier 2 _and_ 3. + let ast = await original.parse(code, options, options) - let env = await loadTailwindCSS({ opts, options }) + let env = await loadTailwindCSS({ opts, options }) - transformAst({ - ast, - env, - opts, - options, - }) + transformAst({ + ast, + env, + opts, + options, + }) - options.__tailwindcss__ = env + options.__tailwindcss__ = env - return ast - }, + return ast } + + return parser } function createPrinter({ From aece38f4166e29e25d8da369ab854c6dba1d2c73 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 9 Dec 2025 13:59:00 -0500 Subject: [PATCH 14/16] Prep for async plugin loading --- src/create-plugin.ts | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/create-plugin.ts b/src/create-plugin.ts index 1f7a9bf9..1e4a199c 100644 --- a/src/create-plugin.ts +++ b/src/create-plugin.ts @@ -8,24 +8,38 @@ import type { Customizations, TransformerEnv, TransformerMetadata } from './type type Base = Awaited> export function createPlugin(base: Base, transforms: TransformOptions[]) { - let parsers: Record> = Object.create(null) - let printers: Record> = Object.create(null) + // Prettier parsers and printers may be async functions at definition time. + // They'll be awaited when the plugin is loaded but must also be swapped out + // with the resolved value before returning as later Prettier internals + // assume that parsers and printers are objects and not functions. + type Init = (() => Promise) | T | undefined + + let parsers: Record>> = Object.create(null) + let printers: Record>> = Object.create(null) for (let opts of transforms) { for (let [name, meta] of Object.entries(opts.parsers)) { - parsers[name] = createParser({ - base, - parserFormat: name, - opts, - }) + parsers[name] = async () => { + parsers[name] = createParser({ + base, + parserFormat: name, + opts, + }) + + return parsers[name] + } } for (let [name, meta] of Object.entries(opts.printers ?? {})) { - printers[name] = createPrinter({ - base, - name, - opts, - }) + printers[name] = async () => { + printers[name] = createPrinter({ + base, + name, + opts, + }) + + return printers[name] + } } } From da51992926ab415f8ebc914f9232a267e778ff30 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 9 Dec 2025 14:00:43 -0500 Subject: [PATCH 15/16] Lazy load compatible plugins --- src/create-plugin.ts | 163 +++++++++++++++++++++++++++-------- src/index.ts | 83 +++++++++++------- src/plugins.ts | 199 ------------------------------------------- src/transform.ts | 21 ++++- 4 files changed, 200 insertions(+), 266 deletions(-) delete mode 100644 src/plugins.ts diff --git a/src/create-plugin.ts b/src/create-plugin.ts index 1e4a199c..27d872a8 100644 --- a/src/create-plugin.ts +++ b/src/create-plugin.ts @@ -1,13 +1,11 @@ -import type { AstPath, Parser, ParserOptions, Printer } from 'prettier' +import type { Parser, ParserOptions, Plugin, Printer } from 'prettier' import { getTailwindConfig } from './config' import { createMatcher } from './options' -import type { loadPlugins } from './plugins' +import { loadIfExists, maybeResolve } from './resolve' import type { TransformOptions } from './transform' -import type { Customizations, TransformerEnv, TransformerMetadata } from './types' +import type { TransformerEnv } from './types' -type Base = Awaited> - -export function createPlugin(base: Base, transforms: TransformOptions[]) { +export function createPlugin(transforms: TransformOptions[]) { // Prettier parsers and printers may be async functions at definition time. // They'll be awaited when the plugin is loaded but must also be swapped out // with the resolved value before returning as later Prettier internals @@ -20,9 +18,13 @@ export function createPlugin(base: Base, transforms: TransformOptions[]) { for (let opts of transforms) { for (let [name, meta] of Object.entries(opts.parsers)) { parsers[name] = async () => { - parsers[name] = createParser({ - base, - parserFormat: name, + let plugin = await loadPlugins(meta.load ?? opts.load ?? []) + let original = plugin.parsers?.[name] + if (!original) return + + parsers[name] = await createParser({ + name, + original, opts, }) @@ -32,9 +34,12 @@ export function createPlugin(base: Base, transforms: TransformOptions[]) { for (let [name, meta] of Object.entries(opts.printers ?? {})) { printers[name] = async () => { + let plugin = await loadPlugins(opts.load ?? []) + let original = plugin.printers?.[name] + if (!original) return + printers[name] = createPrinter({ - base, - name, + original, opts, }) @@ -46,29 +51,48 @@ export function createPlugin(base: Base, transforms: TransformOptions[]) { return { parsers, printers } } -function createParser({ - // - base, - parserFormat, +async function createParser({ + name, + original, opts, }: { - base: Base - parserFormat: string + name: string + original: Parser opts: TransformOptions }) { - let original = base.parsers[parserFormat] let parser: Parser = { ...original } + let compatible: { pluginName: string; mod: unknown }[] = [] + + for (let pluginName of opts.compatible ?? []) { + let mod = await loadIfExistsESM(pluginName) + compatible.push({ pluginName, mod }) + } + + function load(options: ParserOptions) { + let parser: Parser = { ...original } + + for (let { pluginName, mod } of compatible) { + let plugin = findEnabledPlugin(options, pluginName, mod) + if (plugin) Object.assign(parser, plugin.parsers[name]) + } + + return parser + } + parser.preprocess = (code: string, options: ParserOptions) => { - let original = base.originalParser(parserFormat, options) + let parser = load(options) - return original.preprocess ? original.preprocess(code, options) : code + return parser.preprocess ? parser.preprocess(code, options) : code } - parser.parse = async (code: string, options: ParserOptions) => { - let original = base.originalParser(parserFormat, options) + parser.parse = async (code, options) => { + let original = load(options) - // @ts-ignore: We pass three options in the case of plugins that support Prettier 2 _and_ 3. + // @ts-expect-error: `options` is passed twice for compat with older plugins that were written + // for Prettier v2 but still work with v3. + // + // Currently only the Twig plugin requires this. let ast = await original.parse(code, options, options) let env = await loadTailwindCSS({ opts, options }) @@ -88,20 +112,12 @@ function createParser({ return parser } -function createPrinter({ - // - base, - name, - opts, -}: { - base: Base - name: string - opts: TransformOptions -}): Printer { - let original = base.printers[name] - let printer = { ...original } +function createPrinter({ original, opts }: { original: Printer; opts: TransformOptions }) { + let printer: Printer = { ...original } + let reprint = opts.reprint + // Hook into the preprocessing phase to load the config if (reprint) { printer.print = new Proxy(original.print, { apply(target, thisArg, args) { @@ -127,6 +143,83 @@ function createPrinter({ return printer } +async function loadPlugins(fns: string[]) { + let plugin: Plugin = { + parsers: Object.create(null), + printers: Object.create(null), + options: Object.create(null), + defaultOptions: Object.create(null), + languages: [], + } + + for (let moduleName of fns) { + try { + let loaded = await loadIfExistsESM(moduleName) + Object.assign(plugin.parsers!, loaded.parsers ?? {}) + Object.assign(plugin.printers!, loaded.printers ?? {}) + Object.assign(plugin.options!, loaded.options ?? {}) + Object.assign(plugin.defaultOptions!, loaded.defaultOptions ?? {}) + + plugin.languages = [...(plugin.languages ?? []), ...(loaded.languages ?? [])] + } catch (err) { + throw err + } + } + + return plugin +} + +async function loadIfExistsESM(name: string): Promise> { + let mod = await loadIfExists>(name) + + return ( + mod ?? { + parsers: {}, + printers: {}, + languages: [], + options: {}, + defaultOptions: {}, + } + ) +} + +function findEnabledPlugin(options: ParserOptions, name: string, mod: any) { + let path = maybeResolve(name) + + for (let plugin of options.plugins) { + if (plugin instanceof URL) { + if (plugin.protocol !== 'file:') continue + if (plugin.hostname !== '') continue + + plugin = plugin.pathname + } + + if (typeof plugin === 'string') { + if (plugin === name || plugin === path) { + return mod + } + + continue + } + + // options.plugins.*.name == name + if (plugin.name === name) { + return mod + } + + // options.plugins.*.name == path + if (plugin.name === path) { + return mod + } + + // basically options.plugins.* == mod + // But that can't work because prettier normalizes plugins which destroys top-level object identity + if (plugin.parsers && mod.parsers && plugin.parsers == mod.parsers) { + return mod + } + } +} + async function loadTailwindCSS({ options, opts, diff --git a/src/index.ts b/src/index.ts index e6462551..30e29742 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,18 +7,16 @@ import jsesc from 'jsesc' import lineColumn from 'line-column' import * as prettierParserAngular from 'prettier/plugins/angular' import * as prettierParserBabel from 'prettier/plugins/babel' +import * as prettierParserCss from 'prettier/plugins/postcss' // @ts-ignore import * as recast from 'recast' import { createPlugin } from './create-plugin.js' import type { Matcher } from './options.js' -import { loadPlugins } from './plugins.js' import { sortClasses, sortClassList } from './sorting.js' import { defineTransform, type TransformOptions } from './transform.js' import type { StringChange, TransformerEnv } from './types' import { spliceChangesIntoString, visit, type Path } from './utils.js' -let base = await loadPlugins() - const ESCAPE_SEQUENCE_PATTERN = /\\(['"\\nrtbfv0-7xuU])/g function tryParseAngularAttribute(value: string, env: TransformerEnv) { @@ -672,7 +670,7 @@ function transformCss(ast: any, env: TransformerEnv) { // Otherwise we let prettier re-parse the params into its custom value AST // based on postcss-value parser. try { - let parser = base.parsers.css + let parser = prettierParserCss.parsers.css let root = parser.parse(`@import ${params};`, { // We can't pass env.options directly because css.parse overwrites @@ -1015,6 +1013,9 @@ type HtmlNode = { type: 'attribute'; name: string; value: string } | { kind: 'at let html = defineTransform({ staticAttrs: ['class'], + load: ['prettier/plugins/html'], + compatible: ['prettier-plugin-organize-attributes'], + parsers: { html: {}, lwc: {}, @@ -1034,6 +1035,7 @@ type GlimmerNode = let glimmer = defineTransform({ staticAttrs: ['class'], + load: ['prettier/plugins/glimmer'], parsers: { glimmer: {}, @@ -1050,6 +1052,9 @@ type CssNode = { } let css = defineTransform({ + load: ['prettier/plugins/postcss'], + compatible: ['prettier-plugin-css-order'], + parsers: { css: {}, scss: {}, @@ -1061,28 +1066,37 @@ let css = defineTransform({ let js = defineTransform({ staticAttrs: ['class', 'className'], + compatible: [ + // The following plugins must come *before* the jsdoc plugin for it to + // function correctly. Additionally `multiline-arrays` usually needs to be + // placed before import sorting plugins. + // + // https://github.com/electrovir/prettier-plugin-multiline-arrays#compatibility + 'prettier-plugin-multiline-arrays', + '@ianvs/prettier-plugin-sort-imports', + '@trivago/prettier-plugin-sort-imports', + 'prettier-plugin-organize-imports', + 'prettier-plugin-sort-imports', + 'prettier-plugin-jsdoc', + ], parsers: { - babel: {}, - 'babel-flow': {}, - 'babel-ts': {}, - __js_expression: {}, - typescript: {}, - meriyah: {}, - acorn: {}, - flow: {}, - oxc: {}, - 'oxc-ts': {}, - hermes: {}, - - ...(base.parsers.astroExpressionParser - ? { - astroExpressionParser: { - staticAttrs: ['class'], - dynamicAttrs: ['class:list'], - }, - } - : {}), + babel: { load: ['prettier/plugins/babel'] }, + 'babel-flow': { load: ['prettier/plugins/babel'] }, + 'babel-ts': { load: ['prettier/plugins/babel'] }, + __js_expression: { load: ['prettier/plugins/babel'] }, + typescript: { load: ['prettier/plugins/typescript'] }, + meriyah: { load: ['prettier/plugins/meriyah'] }, + acorn: { load: ['prettier/plugins/acorn'] }, + flow: { load: ['prettier/plugins/flow'] }, + oxc: { load: ['@prettier/plugin-oxc'] }, + 'oxc-ts': { load: ['@prettier/plugin-oxc'] }, + hermes: { load: ['@prettier/plugin-hermes'] }, + astroExpressionParser: { + load: ['prettier-plugin-astro'], + staticAttrs: ['class'], + dynamicAttrs: ['class:list'], + }, }, transform: transformJavaScript, @@ -1094,6 +1108,7 @@ type SvelteNode = import('svelte/compiler').AST.SvelteNode & { let svelte = defineTransform({ staticAttrs: ['class'], + load: ['prettier-plugin-svelte'], parsers: { svelte: {}, @@ -1139,6 +1154,7 @@ type AstroNode = let astro = defineTransform({ staticAttrs: ['class', 'className'], dynamicAttrs: ['class:list', 'className'], + load: ['prettier-plugin-astro'], parsers: { astro: {}, @@ -1151,6 +1167,7 @@ type MarkoNode = import('@marko/compiler').types.Node let marko = defineTransform({ staticAttrs: ['class'], + load: ['prettier-plugin-marko'], parsers: { marko: {}, @@ -1182,6 +1199,7 @@ type TwigNode = let twig = defineTransform({ staticAttrs: ['class'], + load: ['@zackad/prettier-plugin-twig'], parsers: { twig: {}, @@ -1197,6 +1215,7 @@ interface PugNode { let pug = defineTransform({ staticAttrs: ['class'], + load: ['@prettier/plugin-pug'], parsers: { pug: {}, @@ -1215,23 +1234,25 @@ type LiquidNode = let liquid = defineTransform({ staticAttrs: ['class'], + load: ['@shopify/prettier-plugin-liquid'], parsers: { 'liquid-html': {} }, transform: transformLiquid, }) -export const { parsers, printers } = createPlugin(base, [ +export const { parsers, printers } = createPlugin([ + // html, glimmer, css, js, - ...(base.parsers.svelte ? [svelte] : []), - ...(base.parsers.astro ? [astro] : []), - ...(base.parsers.marko ? [marko] : []), - ...(base.parsers.twig ? [twig] : []), - ...(base.parsers.pug ? [pug] : []), - ...(base.parsers['liquid-html'] ? [liquid] : []), + svelte, + astro, + marko, + twig, + pug, + liquid, ]) export interface PluginOptions { diff --git a/src/plugins.ts b/src/plugins.ts deleted file mode 100644 index 0c0c9c07..00000000 --- a/src/plugins.ts +++ /dev/null @@ -1,199 +0,0 @@ -import type { Parser, ParserOptions, Plugin, Printer } from 'prettier' -import './types' -import * as prettierParserAcorn from 'prettier/plugins/acorn' -import * as prettierParserBabel from 'prettier/plugins/babel' -import * as prettierParserFlow from 'prettier/plugins/flow' -import * as prettierParserGlimmer from 'prettier/plugins/glimmer' -import * as prettierParserHTML from 'prettier/plugins/html' -import * as prettierParserMeriyah from 'prettier/plugins/meriyah' -import * as prettierParserPostCSS from 'prettier/plugins/postcss' -import * as prettierParserTypescript from 'prettier/plugins/typescript' -import { loadIfExists, maybeResolve } from './resolve' - -interface PluginDetails { - parsers: Record> - printers: Record> -} - -async function loadIfExistsESM(name: string): Promise> { - let mod = await loadIfExists>(name) - - mod ??= { - parsers: {}, - printers: {}, - } - - return mod -} - -export async function loadPlugins() { - const builtin = await loadBuiltinPlugins() - const thirdparty = await loadThirdPartyPlugins() - const compatible = await loadCompatiblePlugins() - - let parsers = { - ...builtin.parsers, - ...thirdparty.parsers, - } - - let printers = { - ...builtin.printers, - ...thirdparty.printers, - } - - function findEnabledPlugin(options: ParserOptions, name: string, mod: any) { - let path = maybeResolve(name) - - for (let plugin of options.plugins) { - if (plugin instanceof URL) { - if (plugin.protocol !== 'file:') continue - if (plugin.hostname !== '') continue - - plugin = plugin.pathname - } - - if (typeof plugin === 'string') { - if (plugin === name || plugin === path) { - return mod - } - - continue - } - - // options.plugins.*.name == name - if (plugin.name === name) { - return mod - } - - // options.plugins.*.name == path - if (plugin.name === path) { - return mod - } - - // basically options.plugins.* == mod - // But that can't work because prettier normalizes plugins which destroys top-level object identity - if (plugin.parsers && mod.parsers && plugin.parsers == mod.parsers) { - return mod - } - } - - return null - } - - return { - parsers, - printers, - - originalParser(format: string, options: ParserOptions) { - if (!options.plugins) { - return parsers[format] - } - - let parser = { ...parsers[format] } - - // Now load parsers from "compatible" plugins if any - for (const { name, mod } of compatible) { - let plugin = findEnabledPlugin(options, name, mod) - if (plugin) { - Object.assign(parser, plugin.parsers[format]) - } - } - - return parser - }, - } -} - -async function loadBuiltinPlugins(): Promise { - return { - parsers: { - html: prettierParserHTML.parsers.html, - glimmer: prettierParserGlimmer.parsers.glimmer, - lwc: prettierParserHTML.parsers.lwc, - angular: prettierParserHTML.parsers.angular, - vue: prettierParserHTML.parsers.vue, - css: prettierParserPostCSS.parsers.css, - scss: prettierParserPostCSS.parsers.scss, - less: prettierParserPostCSS.parsers.less, - babel: prettierParserBabel.parsers.babel, - 'babel-flow': prettierParserBabel.parsers['babel-flow'], - flow: prettierParserFlow.parsers.flow, - typescript: prettierParserTypescript.parsers.typescript, - 'babel-ts': prettierParserBabel.parsers['babel-ts'], - acorn: prettierParserAcorn.parsers.acorn, - meriyah: prettierParserMeriyah.parsers.meriyah, - __js_expression: prettierParserBabel.parsers.__js_expression, - }, - printers: { - // - }, - } -} - -async function loadThirdPartyPlugins(): Promise { - // These plugins *must* be loaded sequentially. Race conditions are possible - // when using await import(…), require(esm), and Promise.all(…). - let astro = await loadIfExistsESM('prettier-plugin-astro') - let liquid = await loadIfExistsESM('@shopify/prettier-plugin-liquid') - let marko = await loadIfExistsESM('prettier-plugin-marko') - let twig = await loadIfExistsESM('@zackad/prettier-plugin-twig') - let hermes = await loadIfExistsESM('@prettier/plugin-hermes') - let oxc = await loadIfExistsESM('@prettier/plugin-oxc') - let pug = await loadIfExistsESM('@prettier/plugin-pug') - let svelte = await loadIfExistsESM('prettier-plugin-svelte') - - return { - parsers: { - ...astro.parsers, - ...liquid.parsers, - ...marko.parsers, - ...twig.parsers, - ...hermes.parsers, - ...oxc.parsers, - ...pug.parsers, - ...svelte.parsers, - }, - printers: { - ...hermes.printers, - ...oxc.printers, - ...svelte.printers, - }, - } -} - -async function loadCompatiblePlugins() { - // Plugins are loaded in a specific order for proper interoperability - let plugins = [ - 'prettier-plugin-css-order', - 'prettier-plugin-organize-attributes', - - // The following plugins must come *before* the jsdoc plugin for it to - // function correctly. Additionally `multiline-arrays` usually needs to be - // placed before import sorting plugins. - // - // https://github.com/electrovir/prettier-plugin-multiline-arrays#compatibility - 'prettier-plugin-multiline-arrays', - '@ianvs/prettier-plugin-sort-imports', - '@trivago/prettier-plugin-sort-imports', - 'prettier-plugin-organize-imports', - 'prettier-plugin-sort-imports', - - 'prettier-plugin-jsdoc', - ] - - let list: { name: string; mod: unknown }[] = [] - - // Load all the available compatible plugins up front. These are wrapped in - // try/catch internally so failure doesn't cause issues. - // - // We're always executing these plugins even if they're not enabled. Sadly, - // there is no way around this currently. - // - // These plugins *must* be loaded sequentially. Race conditions are possible - // when using await import(…), require(esm), and Promise.all(…). - for (let name of plugins) { - list.push({ name, mod: await loadIfExistsESM(name) }) - } - - return list -} diff --git a/src/transform.ts b/src/transform.ts index 69bd8eeb..d8021723 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -16,12 +16,31 @@ export interface TransformOptions { */ dynamicAttrs?: string[] + /** + * Load the given plugins for the parsers and printers + */ + load?: string[] + + /** + * A list of compatible, third-party plugins for this transformation step + * + * The loading of these is delayed until the actual parse call as + * using the parse() function from these plugins may cause errors + * if they haven't already been loaded by Prettier. + */ + compatible?: string[] + /** * A list of supported parser names */ parsers: Record< string, { + /** + * Load the given plugins for the parsers and printers + */ + load?: string[] + /** * Static attributes that are supported by default */ @@ -45,7 +64,7 @@ export interface TransformOptions { * @param ast The AST to transform * @param env Provides options and mechanisms to sort classes */ - transform(ast: T, env: TransformerEnv): void + transform?(ast: T, env: TransformerEnv): void /** * Transform entire ASTs From 0fc721ebb46b0277426f07d5ae6e73bbc27e6925 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 9 Dec 2025 16:29:51 -0500 Subject: [PATCH 16/16] Update changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf5be4a9..1dc56a40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- Nothing yet! +### Changed + +- Remove top-level await ([#420](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/420)) +- Improve load-time performance ([#420](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/420)) ## [0.7.2] - 2025-12-01