Skip to content

Commit da51992

Browse files
committed
Lazy load compatible plugins
1 parent aece38f commit da51992

File tree

4 files changed

+200
-266
lines changed

4 files changed

+200
-266
lines changed

src/create-plugin.ts

Lines changed: 128 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
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 } from './types'
77

8-
type Base = Awaited<ReturnType<typeof loadPlugins>>
9-
10-
export function createPlugin(base: Base, transforms: TransformOptions<any>[]) {
8+
export function createPlugin(transforms: TransformOptions<any>[]) {
119
// Prettier parsers and printers may be async functions at definition time.
1210
// They'll be awaited when the plugin is loaded but must also be swapped out
1311
// with the resolved value before returning as later Prettier internals
@@ -20,9 +18,13 @@ export function createPlugin(base: Base, transforms: TransformOptions<any>[]) {
2018
for (let opts of transforms) {
2119
for (let [name, meta] of Object.entries(opts.parsers)) {
2220
parsers[name] = async () => {
23-
parsers[name] = createParser({
24-
base,
25-
parserFormat: name,
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,
2628
opts,
2729
})
2830

@@ -32,9 +34,12 @@ export function createPlugin(base: Base, transforms: TransformOptions<any>[]) {
3234

3335
for (let [name, meta] of Object.entries(opts.printers ?? {})) {
3436
printers[name] = async () => {
37+
let plugin = await loadPlugins(opts.load ?? [])
38+
let original = plugin.printers?.[name]
39+
if (!original) return
40+
3541
printers[name] = createPrinter({
36-
base,
37-
name,
42+
original,
3843
opts,
3944
})
4045

@@ -46,29 +51,48 @@ export function createPlugin(base: Base, transforms: TransformOptions<any>[]) {
4651
return { parsers, printers }
4752
}
4853

49-
function createParser({
50-
//
51-
base,
52-
parserFormat,
54+
async function createParser({
55+
name,
56+
original,
5357
opts,
5458
}: {
55-
base: Base
56-
parserFormat: string
59+
name: string
60+
original: Parser<any>
5761
opts: TransformOptions<any>
5862
}) {
59-
let original = base.parsers[parserFormat]
6063
let parser: Parser<any> = { ...original }
6164

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+
6283
parser.preprocess = (code: string, options: ParserOptions) => {
63-
let original = base.originalParser(parserFormat, options)
84+
let parser = load(options)
6485

65-
return original.preprocess ? original.preprocess(code, options) : code
86+
return parser.preprocess ? parser.preprocess(code, options) : code
6687
}
6788

68-
parser.parse = async (code: string, options: ParserOptions) => {
69-
let original = base.originalParser(parserFormat, options)
89+
parser.parse = async (code, options) => {
90+
let original = load(options)
7091

71-
// @ts-ignore: We pass three options in the case of plugins that support Prettier 2 _and_ 3.
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.
7296
let ast = await original.parse(code, options, options)
7397

7498
let env = await loadTailwindCSS({ opts, options })
@@ -88,20 +112,12 @@ function createParser({
88112
return parser
89113
}
90114

91-
function createPrinter({
92-
//
93-
base,
94-
name,
95-
opts,
96-
}: {
97-
base: Base
98-
name: string
99-
opts: TransformOptions<any>
100-
}): Printer<any> {
101-
let original = base.printers[name]
102-
let printer = { ...original }
115+
function createPrinter({ original, opts }: { original: Printer<any>; opts: TransformOptions<any> }) {
116+
let printer: Printer<any> = { ...original }
117+
103118
let reprint = opts.reprint
104119

120+
// Hook into the preprocessing phase to load the config
105121
if (reprint) {
106122
printer.print = new Proxy(original.print, {
107123
apply(target, thisArg, args) {
@@ -127,6 +143,83 @@ function createPrinter({
127143
return printer
128144
}
129145

146+
async function loadPlugins<T>(fns: string[]) {
147+
let plugin: Plugin<T> = {
148+
parsers: Object.create(null),
149+
printers: Object.create(null),
150+
options: Object.create(null),
151+
defaultOptions: Object.create(null),
152+
languages: [],
153+
}
154+
155+
for (let moduleName of fns) {
156+
try {
157+
let loaded = await loadIfExistsESM(moduleName)
158+
Object.assign(plugin.parsers!, loaded.parsers ?? {})
159+
Object.assign(plugin.printers!, loaded.printers ?? {})
160+
Object.assign(plugin.options!, loaded.options ?? {})
161+
Object.assign(plugin.defaultOptions!, loaded.defaultOptions ?? {})
162+
163+
plugin.languages = [...(plugin.languages ?? []), ...(loaded.languages ?? [])]
164+
} catch (err) {
165+
throw err
166+
}
167+
}
168+
169+
return plugin
170+
}
171+
172+
async function loadIfExistsESM(name: string): Promise<Plugin<any>> {
173+
let mod = await loadIfExists<Plugin<any>>(name)
174+
175+
return (
176+
mod ?? {
177+
parsers: {},
178+
printers: {},
179+
languages: [],
180+
options: {},
181+
defaultOptions: {},
182+
}
183+
)
184+
}
185+
186+
function findEnabledPlugin(options: ParserOptions<any>, name: string, mod: any) {
187+
let path = maybeResolve(name)
188+
189+
for (let plugin of options.plugins) {
190+
if (plugin instanceof URL) {
191+
if (plugin.protocol !== 'file:') continue
192+
if (plugin.hostname !== '') continue
193+
194+
plugin = plugin.pathname
195+
}
196+
197+
if (typeof plugin === 'string') {
198+
if (plugin === name || plugin === path) {
199+
return mod
200+
}
201+
202+
continue
203+
}
204+
205+
// options.plugins.*.name == name
206+
if (plugin.name === name) {
207+
return mod
208+
}
209+
210+
// options.plugins.*.name == path
211+
if (plugin.name === path) {
212+
return mod
213+
}
214+
215+
// basically options.plugins.* == mod
216+
// But that can't work because prettier normalizes plugins which destroys top-level object identity
217+
if (plugin.parsers && mod.parsers && plugin.parsers == mod.parsers) {
218+
return mod
219+
}
220+
}
221+
}
222+
130223
async function loadTailwindCSS<T = any>({
131224
options,
132225
opts,

0 commit comments

Comments
 (0)