Skip to content

Commit 11ccd31

Browse files
committed
Lazy load compatible plugins
1 parent b515e95 commit 11ccd31

File tree

4 files changed

+275
-286
lines changed

4 files changed

+275
-286
lines changed

src/create-plugin.ts

Lines changed: 195 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,234 @@
1-
import type { AstPath, Parser, ParserOptions, Printer } from 'prettier'
1+
import type { Parser, ParserOptions, Plugin, Printer } from 'prettier'
22
import { getTailwindConfig } from './config'
33
import { createMatcher } from './options'
4-
import type { loadPlugins } from './plugins'
4+
import { loadIfExists, maybeResolve } from './resolve'
55
import type { TransformOptions } from './transform'
6-
import type { Customizations, TransformerEnv, TransformerMetadata } from './types'
6+
import type { TransformerEnv, TransformerMetadata } from './types'
77

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
914

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)
1317

1418
for (let opts of transforms) {
1519
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+
}
2059
}
2160

2261
for (let [name, meta] of Object.entries(opts.printers ?? {})) {
2362
if (!opts.reprint) continue
2463

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+
}
2676
}
2777
}
2878

2979
return { parsers, printers }
3080
}
3181

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: [],
4589
}
4690

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+
}
49104

50-
preprocess(code: string, options: ParserOptions) {
51-
let original = base.originalParser(parserFormat, options)
105+
return plugin
106+
}
52107

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)
55110

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+
}
58121

59-
let original = base.originalParser(parserFormat, options)
122+
function findEnabledPlugin(options: ParserOptions<any>, name: string, mod: any) {
123+
let path = maybeResolve(name)
60124

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
63129

64-
let matcher = createMatcher(options, parserFormat, customizationDefaults)
130+
plugin = plugin.pathname
131+
}
65132

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
71136
}
72137

73-
transform(ast, env)
138+
continue
139+
}
74140

75-
if (parserFormat === 'svelte') {
76-
ast.changes = env.changes
77-
}
141+
// options.plugins.*.name == name
142+
if (plugin.name === name) {
143+
return mod
144+
}
78145

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+
}
81156
}
82157
}
83158

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 }
91230

231+
// Hook into the preprocessing phase to load the config
92232
printer.print = new Proxy(original.print, {
93233
apply(target, thisArg, args) {
94234
let [path, options] = args as Parameters<typeof original.print>

0 commit comments

Comments
 (0)