|
1 | | -import type { AstPath, Parser, ParserOptions, Printer } from 'prettier' |
| 1 | +import type { Parser, ParserOptions, Plugin, Printer } from 'prettier' |
2 | 2 | import { getTailwindConfig } from './config' |
3 | 3 | import { createMatcher } from './options' |
4 | | -import type { loadPlugins } from './plugins' |
| 4 | +import { loadIfExists, maybeResolve } from './resolve' |
5 | 5 | import type { TransformOptions } from './transform' |
6 | | -import type { Customizations, TransformerEnv, TransformerMetadata } from './types' |
| 6 | +import type { TransformerEnv, TransformerMetadata } from './types' |
7 | 7 |
|
8 | | -type Base = Awaited<ReturnType<typeof loadPlugins>> |
| 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 |
9 | 14 |
|
10 | | -export function createPlugin(base: Base, transforms: TransformOptions<any>[]) { |
11 | | - let parsers: Record<string, Parser<any>> = Object.create(null) |
12 | | - let printers: Record<string, Printer<any>> = Object.create(null) |
| 15 | + let parsers: Record<string, Init<Parser<any>>> = Object.create(null) |
| 16 | + let printers: Record<string, Init<Printer<any>>> = Object.create(null) |
13 | 17 |
|
14 | 18 | for (let opts of transforms) { |
15 | 19 | for (let [name, meta] of Object.entries(opts.parsers)) { |
16 | | - parsers[name] = createParser(base, name, opts.transform, { |
17 | | - staticAttrs: meta.staticAttrs ?? opts.staticAttrs ?? [], |
18 | | - dynamicAttrs: meta.dynamicAttrs ?? opts.dynamicAttrs ?? [], |
19 | | - }) |
| 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 | + // Now load parsers from "compatible" plugins if any |
| 26 | + let compatible: { pluginName: string; mod: Plugin<any> }[] = [] |
| 27 | + |
| 28 | + for (let pluginName of opts.compatible ?? []) { |
| 29 | + compatible.push({ |
| 30 | + pluginName, |
| 31 | + mod: await loadIfExistsESM(pluginName), |
| 32 | + }) |
| 33 | + } |
| 34 | + |
| 35 | + // TODO: Find a way to drop this. We have to do this for compatible |
| 36 | + // plugins that are intended to override builtin ones |
| 37 | + parsers[name] = await createParser({ |
| 38 | + original, |
| 39 | + transform: opts.transform, |
| 40 | + meta: { |
| 41 | + staticAttrs: meta.staticAttrs ?? opts.staticAttrs ?? [], |
| 42 | + dynamicAttrs: meta.dynamicAttrs ?? opts.dynamicAttrs ?? [], |
| 43 | + }, |
| 44 | + |
| 45 | + loadCompatible(options) { |
| 46 | + let parser: Parser<any> = { ...original } |
| 47 | + |
| 48 | + for (let { pluginName, mod } of compatible) { |
| 49 | + let plugin = findEnabledPlugin(options, pluginName, mod) |
| 50 | + if (plugin) Object.assign(parser, plugin.parsers[name]) |
| 51 | + } |
| 52 | + |
| 53 | + return parser |
| 54 | + }, |
| 55 | + }) |
| 56 | + |
| 57 | + return parsers[name] |
| 58 | + } |
20 | 59 | } |
21 | 60 |
|
22 | 61 | for (let [name, meta] of Object.entries(opts.printers ?? {})) { |
23 | 62 | if (!opts.reprint) continue |
24 | 63 |
|
25 | | - printers[name] = createPrinter(base, name, opts.reprint) |
| 64 | + printers[name] = async () => { |
| 65 | + let plugin = await loadPlugins(meta.load ?? opts.load ?? []) |
| 66 | + let original = plugin.printers?.[name] |
| 67 | + if (!original) return |
| 68 | + |
| 69 | + printers[name] = createPrinter({ |
| 70 | + original, |
| 71 | + reprint: opts.reprint!, |
| 72 | + }) |
| 73 | + |
| 74 | + return printers[name] |
| 75 | + } |
26 | 76 | } |
27 | 77 | } |
28 | 78 |
|
29 | 79 | return { parsers, printers } |
30 | 80 | } |
31 | 81 |
|
32 | | -function createParser( |
33 | | - base: Base, |
34 | | - parserFormat: string, |
35 | | - transform: (ast: any, env: TransformerEnv) => void, |
36 | | - meta: TransformerMetadata = {}, |
37 | | -) { |
38 | | - let customizationDefaults: Customizations = { |
39 | | - staticAttrs: new Set(meta.staticAttrs ?? []), |
40 | | - dynamicAttrs: new Set(meta.dynamicAttrs ?? []), |
41 | | - functions: new Set(meta.functions ?? []), |
42 | | - staticAttrsRegex: [], |
43 | | - dynamicAttrsRegex: [], |
44 | | - functionsRegex: [], |
| 82 | +async function loadPlugins<T>(fns: string[]) { |
| 83 | + let plugin: Plugin<T> = { |
| 84 | + parsers: Object.create(null), |
| 85 | + printers: Object.create(null), |
| 86 | + options: Object.create(null), |
| 87 | + defaultOptions: Object.create(null), |
| 88 | + languages: [], |
45 | 89 | } |
46 | 90 |
|
47 | | - return { |
48 | | - ...base.parsers[parserFormat], |
| 91 | + for (let moduleName of fns) { |
| 92 | + try { |
| 93 | + let loaded = await loadIfExistsESM(moduleName) |
| 94 | + Object.assign(plugin.parsers!, loaded.parsers ?? {}) |
| 95 | + Object.assign(plugin.printers!, loaded.printers ?? {}) |
| 96 | + Object.assign(plugin.options!, loaded.options ?? {}) |
| 97 | + Object.assign(plugin.defaultOptions!, loaded.defaultOptions ?? {}) |
| 98 | + |
| 99 | + plugin.languages = [...(plugin.languages ?? []), ...(loaded.languages ?? [])] |
| 100 | + } catch (err) { |
| 101 | + throw err |
| 102 | + } |
| 103 | + } |
49 | 104 |
|
50 | | - preprocess(code: string, options: ParserOptions) { |
51 | | - let original = base.originalParser(parserFormat, options) |
| 105 | + return plugin |
| 106 | +} |
52 | 107 |
|
53 | | - return original.preprocess ? original.preprocess(code, options) : code |
54 | | - }, |
| 108 | +async function loadIfExistsESM(name: string): Promise<Plugin<any>> { |
| 109 | + let mod = await loadIfExists<Plugin<any>>(name) |
55 | 110 |
|
56 | | - async parse(text: string, options: ParserOptions) { |
57 | | - let context = await getTailwindConfig(options) |
| 111 | + return ( |
| 112 | + mod ?? { |
| 113 | + parsers: {}, |
| 114 | + printers: {}, |
| 115 | + languages: [], |
| 116 | + options: {}, |
| 117 | + defaultOptions: {}, |
| 118 | + } |
| 119 | + ) |
| 120 | +} |
58 | 121 |
|
59 | | - let original = base.originalParser(parserFormat, options) |
| 122 | +function findEnabledPlugin(options: ParserOptions<any>, name: string, mod: any) { |
| 123 | + let path = maybeResolve(name) |
60 | 124 |
|
61 | | - // @ts-ignore: We pass three options in the case of plugins that support Prettier 2 _and_ 3. |
62 | | - let ast = await original.parse(text, options, options) |
| 125 | + for (let plugin of options.plugins) { |
| 126 | + if (plugin instanceof URL) { |
| 127 | + if (plugin.protocol !== 'file:') continue |
| 128 | + if (plugin.hostname !== '') continue |
63 | 129 |
|
64 | | - let matcher = createMatcher(options, parserFormat, customizationDefaults) |
| 130 | + plugin = plugin.pathname |
| 131 | + } |
65 | 132 |
|
66 | | - let env: TransformerEnv = { |
67 | | - context, |
68 | | - matcher, |
69 | | - options, |
70 | | - changes: [], |
| 133 | + if (typeof plugin === 'string') { |
| 134 | + if (plugin === name || plugin === path) { |
| 135 | + return mod |
71 | 136 | } |
72 | 137 |
|
73 | | - transform(ast, env) |
| 138 | + continue |
| 139 | + } |
74 | 140 |
|
75 | | - if (parserFormat === 'svelte') { |
76 | | - ast.changes = env.changes |
77 | | - } |
| 141 | + // options.plugins.*.name == name |
| 142 | + if (plugin.name === name) { |
| 143 | + return mod |
| 144 | + } |
78 | 145 |
|
79 | | - return ast |
80 | | - }, |
| 146 | + // options.plugins.*.name == path |
| 147 | + if (plugin.name === path) { |
| 148 | + return mod |
| 149 | + } |
| 150 | + |
| 151 | + // basically options.plugins.* == mod |
| 152 | + // But that can't work because prettier normalizes plugins which destroys top-level object identity |
| 153 | + if (plugin.parsers && mod.parsers && plugin.parsers == mod.parsers) { |
| 154 | + return mod |
| 155 | + } |
81 | 156 | } |
82 | 157 | } |
83 | 158 |
|
84 | | -function createPrinter( |
85 | | - base: Base, |
86 | | - name: string, |
87 | | - reprint: (path: AstPath<any>, options: ParserOptions<any>) => void, |
88 | | -): Printer<any> { |
89 | | - let original = base.printers[name] |
90 | | - let printer = { ...original } |
| 159 | +async function createParser({ |
| 160 | + original, |
| 161 | + loadCompatible, |
| 162 | + meta, |
| 163 | + transform, |
| 164 | +}: { |
| 165 | + original: Parser<any> |
| 166 | + meta: TransformerMetadata |
| 167 | + loadCompatible: (options: ParserOptions) => Parser<any> |
| 168 | + transform: NonNullable<TransformOptions<any>['transform']> |
| 169 | +}) { |
| 170 | + let parser: Parser<any> = { ...original } |
| 171 | + |
| 172 | + // TODO: Prettier v3.6.2+ allows preprocess to be async however this breaks |
| 173 | + // - Astro |
| 174 | + // - prettier-plugin-multiline-arrays |
| 175 | + // - @trivago/prettier-plugin-sort-imports |
| 176 | + // - prettier-plugin-jsdoc |
| 177 | + parser.preprocess = (code: string, options: ParserOptions) => { |
| 178 | + let parser = loadCompatible(options) |
| 179 | + |
| 180 | + return parser.preprocess ? parser.preprocess(code, options) : code |
| 181 | + } |
| 182 | + |
| 183 | + parser.parse = async (code, options) => { |
| 184 | + let original = loadCompatible(options) |
| 185 | + |
| 186 | + // @ts-expect-error: `options` is passed twice for compat with older plugins that were written |
| 187 | + // for Prettier v2 but still work with v3. |
| 188 | + // |
| 189 | + // Currently only the Twig plugin requires this. |
| 190 | + let ast = await original.parse(code, options, options) |
| 191 | + |
| 192 | + let context = await getTailwindConfig(options) |
| 193 | + |
| 194 | + let matcher = createMatcher(options, options.parser as string, { |
| 195 | + staticAttrs: new Set(meta.staticAttrs ?? []), |
| 196 | + dynamicAttrs: new Set(meta.dynamicAttrs ?? []), |
| 197 | + functions: new Set(), |
| 198 | + staticAttrsRegex: [], |
| 199 | + dynamicAttrsRegex: [], |
| 200 | + functionsRegex: [], |
| 201 | + }) |
| 202 | + |
| 203 | + let env: TransformerEnv = { |
| 204 | + context, |
| 205 | + matcher, |
| 206 | + options, |
| 207 | + changes: [], |
| 208 | + } |
| 209 | + |
| 210 | + transform(ast, env) |
| 211 | + |
| 212 | + if (options.parser === 'svelte') { |
| 213 | + ast.changes = env.changes |
| 214 | + } |
| 215 | + |
| 216 | + return ast |
| 217 | + } |
| 218 | + |
| 219 | + return parser |
| 220 | +} |
| 221 | + |
| 222 | +function createPrinter({ |
| 223 | + original, |
| 224 | + reprint, |
| 225 | +}: { |
| 226 | + original: Printer<any> |
| 227 | + reprint: NonNullable<TransformOptions<any>['reprint']> |
| 228 | +}) { |
| 229 | + let printer: Printer<any> = { ...original } |
91 | 230 |
|
| 231 | + // Hook into the preprocessing phase to load the config |
92 | 232 | printer.print = new Proxy(original.print, { |
93 | 233 | apply(target, thisArg, args) { |
94 | 234 | let [path, options] = args as Parameters<typeof original.print> |
|
0 commit comments