|
| 1 | +import type { Parser, ParserOptions, Plugin, Printer } from 'prettier' |
| 2 | +import { getTailwindConfig } from './config' |
| 3 | +import { createMatcher } from './options' |
| 4 | +import { loadIfExists, maybeResolve } from './resolve' |
| 5 | +import type { TransformOptions } from './transform' |
| 6 | +import type { TransformerEnv } from './types' |
| 7 | + |
| 8 | +export function createPlugin(transforms: TransformOptions<any>[]) { |
| 9 | + // Prettier parsers and printers may be async functions at definition time. |
| 10 | + // They'll be awaited when the plugin is loaded but must also be swapped out |
| 11 | + // with the resolved value before returning as later Prettier internals |
| 12 | + // assume that parsers and printers are objects and not functions. |
| 13 | + type Init<T> = (() => Promise<T | undefined>) | T | undefined |
| 14 | + |
| 15 | + let parsers: Record<string, Init<Parser<any>>> = Object.create(null) |
| 16 | + let printers: Record<string, Init<Printer<any>>> = Object.create(null) |
| 17 | + |
| 18 | + for (let opts of transforms) { |
| 19 | + for (let [name, meta] of Object.entries(opts.parsers)) { |
| 20 | + parsers[name] = async () => { |
| 21 | + let plugin = await loadPlugins(meta.load ?? opts.load ?? []) |
| 22 | + let original = plugin.parsers?.[name] |
| 23 | + if (!original) return |
| 24 | + |
| 25 | + parsers[name] = await createParser({ |
| 26 | + name, |
| 27 | + original, |
| 28 | + opts, |
| 29 | + }) |
| 30 | + |
| 31 | + return parsers[name] |
| 32 | + } |
| 33 | + } |
| 34 | + |
| 35 | + for (let [name, meta] of Object.entries(opts.printers ?? {})) { |
| 36 | + printers[name] = async () => { |
| 37 | + let plugin = await loadPlugins(opts.load ?? []) |
| 38 | + let original = plugin.printers?.[name] |
| 39 | + if (!original) return |
| 40 | + |
| 41 | + printers[name] = createPrinter({ |
| 42 | + original, |
| 43 | + opts, |
| 44 | + }) |
| 45 | + |
| 46 | + return printers[name] |
| 47 | + } |
| 48 | + } |
| 49 | + } |
| 50 | + |
| 51 | + return { parsers, printers } |
| 52 | +} |
| 53 | + |
| 54 | +async function createParser({ |
| 55 | + name, |
| 56 | + original, |
| 57 | + opts, |
| 58 | +}: { |
| 59 | + name: string |
| 60 | + original: Parser<any> |
| 61 | + opts: TransformOptions<any> |
| 62 | +}) { |
| 63 | + let parser: Parser<any> = { ...original } |
| 64 | + |
| 65 | + let compatible: { pluginName: string; mod: unknown }[] = [] |
| 66 | + |
| 67 | + for (let pluginName of opts.compatible ?? []) { |
| 68 | + let mod = await loadIfExistsESM(pluginName) |
| 69 | + compatible.push({ pluginName, mod }) |
| 70 | + } |
| 71 | + |
| 72 | + function load(options: ParserOptions<any>) { |
| 73 | + let parser: Parser<any> = { ...original } |
| 74 | + |
| 75 | + for (let { pluginName, mod } of compatible) { |
| 76 | + let plugin = findEnabledPlugin(options, pluginName, mod) |
| 77 | + if (plugin) Object.assign(parser, plugin.parsers[name]) |
| 78 | + } |
| 79 | + |
| 80 | + return parser |
| 81 | + } |
| 82 | + |
| 83 | + parser.preprocess = (code: string, options: ParserOptions) => { |
| 84 | + let parser = load(options) |
| 85 | + |
| 86 | + return parser.preprocess ? parser.preprocess(code, options) : code |
| 87 | + } |
| 88 | + |
| 89 | + parser.parse = async (code, options) => { |
| 90 | + let original = load(options) |
| 91 | + |
| 92 | + // @ts-expect-error: `options` is passed twice for compat with older plugins that were written |
| 93 | + // for Prettier v2 but still work with v3. |
| 94 | + // |
| 95 | + // Currently only the Twig plugin requires this. |
| 96 | + let ast = await original.parse(code, options, options) |
| 97 | + |
| 98 | + let env = await loadTailwindCSS({ opts, options }) |
| 99 | + |
| 100 | + transformAst({ |
| 101 | + ast, |
| 102 | + env, |
| 103 | + opts, |
| 104 | + options, |
| 105 | + }) |
| 106 | + |
| 107 | + options.__tailwindcss__ = env |
| 108 | + |
| 109 | + return ast |
| 110 | + } |
| 111 | + |
| 112 | + return parser |
| 113 | +} |
| 114 | + |
| 115 | +function createPrinter({ |
| 116 | + original, |
| 117 | + opts, |
| 118 | +}: { |
| 119 | + original: Printer<any> |
| 120 | + opts: TransformOptions<any> |
| 121 | +}) { |
| 122 | + let printer: Printer<any> = { ...original } |
| 123 | + |
| 124 | + let reprint = opts.reprint |
| 125 | + |
| 126 | + // Hook into the preprocessing phase to load the config |
| 127 | + if (reprint) { |
| 128 | + printer.print = new Proxy(original.print, { |
| 129 | + apply(target, thisArg, args) { |
| 130 | + let [path, options] = args as Parameters<typeof original.print> |
| 131 | + let env = options.__tailwindcss__ as TransformerEnv |
| 132 | + reprint(path, { ...env, options: options }) |
| 133 | + return Reflect.apply(target, thisArg, args) |
| 134 | + }, |
| 135 | + }) |
| 136 | + |
| 137 | + if (original.embed) { |
| 138 | + printer.embed = new Proxy(original.embed, { |
| 139 | + apply(target, thisArg, args) { |
| 140 | + let [path, options] = args as Parameters<typeof original.embed> |
| 141 | + let env = options.__tailwindcss__ as TransformerEnv |
| 142 | + reprint(path, { ...env, options: options as any }) |
| 143 | + return Reflect.apply(target, thisArg, args) |
| 144 | + }, |
| 145 | + }) |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + return printer |
| 150 | +} |
| 151 | + |
| 152 | +async function loadPlugins<T>(fns: string[]) { |
| 153 | + let plugin: Plugin<T> = { |
| 154 | + parsers: Object.create(null), |
| 155 | + printers: Object.create(null), |
| 156 | + options: Object.create(null), |
| 157 | + defaultOptions: Object.create(null), |
| 158 | + languages: [], |
| 159 | + } |
| 160 | + |
| 161 | + for (let moduleName of fns) { |
| 162 | + try { |
| 163 | + let loaded = await loadIfExistsESM(moduleName) |
| 164 | + Object.assign(plugin.parsers!, loaded.parsers ?? {}) |
| 165 | + Object.assign(plugin.printers!, loaded.printers ?? {}) |
| 166 | + Object.assign(plugin.options!, loaded.options ?? {}) |
| 167 | + Object.assign(plugin.defaultOptions!, loaded.defaultOptions ?? {}) |
| 168 | + |
| 169 | + plugin.languages = [...(plugin.languages ?? []), ...(loaded.languages ?? [])] |
| 170 | + } catch (err) { |
| 171 | + throw err |
| 172 | + } |
| 173 | + } |
| 174 | + |
| 175 | + return plugin |
| 176 | +} |
| 177 | + |
| 178 | +async function loadIfExistsESM(name: string): Promise<Plugin<any>> { |
| 179 | + let mod = await loadIfExists<Plugin<any>>(name) |
| 180 | + |
| 181 | + return ( |
| 182 | + mod ?? { |
| 183 | + parsers: {}, |
| 184 | + printers: {}, |
| 185 | + languages: [], |
| 186 | + options: {}, |
| 187 | + defaultOptions: {}, |
| 188 | + } |
| 189 | + ) |
| 190 | +} |
| 191 | + |
| 192 | +function findEnabledPlugin(options: ParserOptions<any>, name: string, mod: any) { |
| 193 | + let path = maybeResolve(name) |
| 194 | + |
| 195 | + for (let plugin of options.plugins) { |
| 196 | + if (plugin instanceof URL) { |
| 197 | + if (plugin.protocol !== 'file:') continue |
| 198 | + if (plugin.hostname !== '') continue |
| 199 | + |
| 200 | + plugin = plugin.pathname |
| 201 | + } |
| 202 | + |
| 203 | + if (typeof plugin === 'string') { |
| 204 | + if (plugin === name || plugin === path) { |
| 205 | + return mod |
| 206 | + } |
| 207 | + |
| 208 | + continue |
| 209 | + } |
| 210 | + |
| 211 | + // options.plugins.*.name == name |
| 212 | + if (plugin.name === name) { |
| 213 | + return mod |
| 214 | + } |
| 215 | + |
| 216 | + // options.plugins.*.name == path |
| 217 | + if (plugin.name === path) { |
| 218 | + return mod |
| 219 | + } |
| 220 | + |
| 221 | + // basically options.plugins.* == mod |
| 222 | + // But that can't work because prettier normalizes plugins which destroys top-level object identity |
| 223 | + if (plugin.parsers && mod.parsers && plugin.parsers == mod.parsers) { |
| 224 | + return mod |
| 225 | + } |
| 226 | + } |
| 227 | +} |
| 228 | + |
| 229 | +async function loadTailwindCSS<T = any>({ |
| 230 | + options, |
| 231 | + opts, |
| 232 | +}: { |
| 233 | + options: ParserOptions<T> |
| 234 | + opts: TransformOptions<T> |
| 235 | +}): Promise<TransformerEnv> { |
| 236 | + let parsers = opts.parsers |
| 237 | + let parser = options.parser as string |
| 238 | + |
| 239 | + let context = await getTailwindConfig(options) |
| 240 | + |
| 241 | + let matcher = createMatcher(options, parser, { |
| 242 | + staticAttrs: new Set(parsers[parser]?.staticAttrs ?? opts.staticAttrs ?? []), |
| 243 | + dynamicAttrs: new Set(parsers[parser]?.dynamicAttrs ?? opts.dynamicAttrs ?? []), |
| 244 | + functions: new Set(), |
| 245 | + staticAttrsRegex: [], |
| 246 | + dynamicAttrsRegex: [], |
| 247 | + functionsRegex: [], |
| 248 | + }) |
| 249 | + |
| 250 | + return { |
| 251 | + context, |
| 252 | + matcher, |
| 253 | + options, |
| 254 | + changes: [], |
| 255 | + } |
| 256 | +} |
| 257 | + |
| 258 | +function transformAst<T = any>({ |
| 259 | + ast, |
| 260 | + env, |
| 261 | + opts, |
| 262 | +}: { |
| 263 | + ast: T |
| 264 | + env: TransformerEnv |
| 265 | + options: ParserOptions<T> |
| 266 | + opts: TransformOptions<T> |
| 267 | +}) { |
| 268 | + let transform = opts.transform |
| 269 | + if (transform) transform(ast, env) |
| 270 | +} |
0 commit comments