Skip to content

Commit c2bb1a3

Browse files
committed
Lazy load compatible plugins
1 parent 5a1e4ec commit c2bb1a3

File tree

4 files changed

+190
-270
lines changed

4 files changed

+190
-270
lines changed

src/create-plugin.ts

Lines changed: 118 additions & 39 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 () => {
21+
let plugin = await loadPlugins(meta.load ?? opts.load ?? [])
22+
let original = plugin.parsers?.[name]
23+
if (!original) return
24+
2325
parsers[name] = createParser({
24-
base,
25-
parserFormat: name,
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,34 @@ export function createPlugin(base: Base, transforms: TransformOptions<any>[]) {
4651
return { parsers, printers }
4752
}
4853

49-
function createParser({
50-
//
51-
base,
52-
parserFormat,
53-
opts,
54-
}: {
55-
base: Base
56-
parserFormat: string
57-
opts: TransformOptions<any>
58-
}) {
59-
let original = base.parsers[parserFormat]
54+
function createParser({ name, original, opts }: { name: string; original: Parser<any>; opts: TransformOptions<any> }) {
6055
let parser: Parser<any> = { ...original }
6156

62-
parser.preprocess = (code: string, options: ParserOptions) => {
63-
let original = base.originalParser(parserFormat, options)
57+
async function load(options: ParserOptions<any>) {
58+
let parser: Parser<any> = { ...original }
59+
60+
for (let pluginName of opts.compatible ?? []) {
61+
let mod = await loadIfExistsESM(pluginName)
62+
let plugin = findEnabledPlugin(options, pluginName, mod)
63+
if (plugin) Object.assign(parser, plugin.parsers[name])
64+
}
65+
66+
return parser
67+
}
68+
69+
parser.preprocess = async (code: string, options: ParserOptions) => {
70+
let parser = await load(options)
6471

65-
return original.preprocess ? original.preprocess(code, options) : code
72+
return parser.preprocess ? await parser.preprocess(code, options) : code
6673
}
6774

68-
parser.parse = async (code: string, options: ParserOptions) => {
69-
let original = base.originalParser(parserFormat, options)
75+
parser.parse = async (code, options) => {
76+
let original = await load(options)
7077

71-
// @ts-ignore: We pass three options in the case of plugins that support Prettier 2 _and_ 3.
78+
// @ts-expect-error: `options` is passed twice for compat with older plugins that were written
79+
// for Prettier v2 but still work with v3.
80+
//
81+
// Currently only the Twig plugin requires this.
7282
let ast = await original.parse(code, options, options)
7383

7484
await transformAst({
@@ -83,20 +93,12 @@ function createParser({
8393
return parser
8494
}
8595

86-
function createPrinter({
87-
//
88-
base,
89-
name,
90-
opts,
91-
}: {
92-
base: Base
93-
name: string
94-
opts: TransformOptions<any>
95-
}): Printer<any> {
96-
let original = base.printers[name]
97-
let printer = { ...original }
96+
function createPrinter({ original, opts }: { original: Printer<any>; opts: TransformOptions<any> }) {
97+
let printer: Printer<any> = { ...original }
98+
9899
let reprint = opts.reprint
99100

101+
// Hook into the preprocessing phase to load the config
100102
if (reprint) {
101103
printer.print = new Proxy(original.print, {
102104
apply(target, thisArg, args) {
@@ -120,6 +122,83 @@ function createPrinter({
120122
return printer
121123
}
122124

125+
async function loadPlugins<T>(fns: string[]) {
126+
let plugin: Plugin<T> = {
127+
parsers: Object.create(null),
128+
printers: Object.create(null),
129+
options: Object.create(null),
130+
defaultOptions: Object.create(null),
131+
languages: [],
132+
}
133+
134+
for (let moduleName of fns) {
135+
try {
136+
let loaded = await loadIfExistsESM(moduleName)
137+
Object.assign(plugin.parsers!, loaded.parsers ?? {})
138+
Object.assign(plugin.printers!, loaded.printers ?? {})
139+
Object.assign(plugin.options!, loaded.options ?? {})
140+
Object.assign(plugin.defaultOptions!, loaded.defaultOptions ?? {})
141+
142+
plugin.languages = [...(plugin.languages ?? []), ...(loaded.languages ?? [])]
143+
} catch (err) {
144+
throw err
145+
}
146+
}
147+
148+
return plugin
149+
}
150+
151+
async function loadIfExistsESM(name: string): Promise<Plugin<any>> {
152+
let mod = await loadIfExists<Plugin<any>>(name)
153+
154+
return (
155+
mod ?? {
156+
parsers: {},
157+
printers: {},
158+
languages: [],
159+
options: {},
160+
defaultOptions: {},
161+
}
162+
)
163+
}
164+
165+
function findEnabledPlugin(options: ParserOptions<any>, name: string, mod: any) {
166+
let path = maybeResolve(name)
167+
168+
for (let plugin of options.plugins) {
169+
if (plugin instanceof URL) {
170+
if (plugin.protocol !== 'file:') continue
171+
if (plugin.hostname !== '') continue
172+
173+
plugin = plugin.pathname
174+
}
175+
176+
if (typeof plugin === 'string') {
177+
if (plugin === name || plugin === path) {
178+
return mod
179+
}
180+
181+
continue
182+
}
183+
184+
// options.plugins.*.name == name
185+
if (plugin.name === name) {
186+
return mod
187+
}
188+
189+
// options.plugins.*.name == path
190+
if (plugin.name === path) {
191+
return mod
192+
}
193+
194+
// basically options.plugins.* == mod
195+
// But that can't work because prettier normalizes plugins which destroys top-level object identity
196+
if (plugin.parsers && mod.parsers && plugin.parsers == mod.parsers) {
197+
return mod
198+
}
199+
}
200+
}
201+
123202
async function transformAst<T = any>({
124203
ast,
125204
options,

src/index.ts

Lines changed: 52 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,16 @@ import jsesc from 'jsesc'
77
import lineColumn from 'line-column'
88
import * as prettierParserAngular from 'prettier/plugins/angular'
99
import * as prettierParserBabel from 'prettier/plugins/babel'
10+
import * as prettierParserCss from 'prettier/plugins/postcss'
1011
// @ts-ignore
1112
import * as recast from 'recast'
1213
import { createPlugin } from './create-plugin.js'
1314
import type { Matcher } from './options.js'
14-
import { loadPlugins } from './plugins.js'
1515
import { sortClasses, sortClassList } from './sorting.js'
1616
import { defineTransform, type TransformOptions } from './transform.js'
1717
import type { StringChange, TransformerEnv } from './types'
1818
import { spliceChangesIntoString, visit, type Path } from './utils.js'
1919

20-
let base = await loadPlugins()
21-
2220
const ESCAPE_SEQUENCE_PATTERN = /\\(['"\\nrtbfv0-7xuU])/g
2321

2422
function tryParseAngularAttribute(value: string, env: TransformerEnv) {
@@ -672,7 +670,7 @@ function transformCss(ast: any, env: TransformerEnv) {
672670
// Otherwise we let prettier re-parse the params into its custom value AST
673671
// based on postcss-value parser.
674672
try {
675-
let parser = base.parsers.css
673+
let parser = prettierParserCss.parsers.css
676674

677675
let root = parser.parse(`@import ${params};`, {
678676
// 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
10151013
let html = defineTransform<HtmlNode>({
10161014
staticAttrs: ['class'],
10171015

1016+
load: ['prettier/plugins/html'],
1017+
compatible: ['prettier-plugin-organize-attributes'],
1018+
10181019
parsers: {
10191020
html: {},
10201021
lwc: {},
@@ -1034,6 +1035,7 @@ type GlimmerNode =
10341035

10351036
let glimmer = defineTransform<GlimmerNode>({
10361037
staticAttrs: ['class'],
1038+
load: ['prettier/plugins/glimmer'],
10371039

10381040
parsers: {
10391041
glimmer: {},
@@ -1050,6 +1052,9 @@ type CssNode = {
10501052
}
10511053

10521054
let css = defineTransform<CssNode>({
1055+
load: ['prettier/plugins/postcss'],
1056+
compatible: ['prettier-plugin-css-order'],
1057+
10531058
parsers: {
10541059
css: {},
10551060
scss: {},
@@ -1061,28 +1066,37 @@ let css = defineTransform<CssNode>({
10611066

10621067
let js = defineTransform<import('@babel/types').Node>({
10631068
staticAttrs: ['class', 'className'],
1069+
compatible: [
1070+
// The following plugins must come *before* the jsdoc plugin for it to
1071+
// function correctly. Additionally `multiline-arrays` usually needs to be
1072+
// placed before import sorting plugins.
1073+
//
1074+
// https://github.com/electrovir/prettier-plugin-multiline-arrays#compatibility
1075+
'prettier-plugin-multiline-arrays',
1076+
'@ianvs/prettier-plugin-sort-imports',
1077+
'@trivago/prettier-plugin-sort-imports',
1078+
'prettier-plugin-organize-imports',
1079+
'prettier-plugin-sort-imports',
1080+
'prettier-plugin-jsdoc',
1081+
],
10641082

10651083
parsers: {
1066-
babel: {},
1067-
'babel-flow': {},
1068-
'babel-ts': {},
1069-
__js_expression: {},
1070-
typescript: {},
1071-
meriyah: {},
1072-
acorn: {},
1073-
flow: {},
1074-
oxc: {},
1075-
'oxc-ts': {},
1076-
hermes: {},
1077-
1078-
...(base.parsers.astroExpressionParser
1079-
? {
1080-
astroExpressionParser: {
1081-
staticAttrs: ['class'],
1082-
dynamicAttrs: ['class:list'],
1083-
},
1084-
}
1085-
: {}),
1084+
babel: { load: ['prettier/plugins/babel'] },
1085+
'babel-flow': { load: ['prettier/plugins/babel'] },
1086+
'babel-ts': { load: ['prettier/plugins/babel'] },
1087+
__js_expression: { load: ['prettier/plugins/babel'] },
1088+
typescript: { load: ['prettier/plugins/typescript'] },
1089+
meriyah: { load: ['prettier/plugins/meriyah'] },
1090+
acorn: { load: ['prettier/plugins/acorn'] },
1091+
flow: { load: ['prettier/plugins/flow'] },
1092+
oxc: { load: ['@prettier/plugin-oxc'] },
1093+
'oxc-ts': { load: ['@prettier/plugin-oxc'] },
1094+
hermes: { load: ['@prettier/plugin-hermes'] },
1095+
astroExpressionParser: {
1096+
load: ['prettier-plugin-astro'],
1097+
staticAttrs: ['class'],
1098+
dynamicAttrs: ['class:list'],
1099+
},
10861100
},
10871101

10881102
transform: transformJavaScript,
@@ -1094,6 +1108,7 @@ type SvelteNode = import('svelte/compiler').AST.SvelteNode & {
10941108

10951109
let svelte = defineTransform<SvelteNode>({
10961110
staticAttrs: ['class'],
1111+
load: ['prettier-plugin-svelte'],
10971112

10981113
parsers: {
10991114
svelte: {},
@@ -1140,6 +1155,7 @@ type AstroNode =
11401155
let astro = defineTransform<AstroNode>({
11411156
staticAttrs: ['class', 'className'],
11421157
dynamicAttrs: ['class:list', 'className'],
1158+
load: ['prettier-plugin-astro'],
11431159

11441160
parsers: {
11451161
astro: {},
@@ -1152,6 +1168,7 @@ type MarkoNode = import('@marko/compiler').types.Node
11521168

11531169
let marko = defineTransform<MarkoNode>({
11541170
staticAttrs: ['class'],
1171+
load: ['prettier-plugin-marko'],
11551172

11561173
parsers: {
11571174
marko: {},
@@ -1183,6 +1200,7 @@ type TwigNode =
11831200

11841201
let twig = defineTransform<TwigNode>({
11851202
staticAttrs: ['class'],
1203+
load: ['@zackad/prettier-plugin-twig'],
11861204

11871205
parsers: {
11881206
twig: {},
@@ -1198,6 +1216,7 @@ interface PugNode {
11981216

11991217
let pug = defineTransform<PugNode>({
12001218
staticAttrs: ['class'],
1219+
load: ['@prettier/plugin-pug'],
12011220

12021221
parsers: {
12031222
pug: {},
@@ -1216,23 +1235,25 @@ type LiquidNode =
12161235

12171236
let liquid = defineTransform<LiquidNode>({
12181237
staticAttrs: ['class'],
1238+
load: ['@shopify/prettier-plugin-liquid'],
12191239

12201240
parsers: { 'liquid-html': {} },
12211241

12221242
transform: transformLiquid,
12231243
})
12241244

1225-
export const { parsers, printers } = createPlugin(base, [
1245+
export const { parsers, printers } = createPlugin([
1246+
//
12261247
html,
12271248
glimmer,
12281249
css,
12291250
js,
1230-
...(base.parsers.svelte ? [svelte] : []),
1231-
...(base.parsers.astro ? [astro] : []),
1232-
...(base.parsers.marko ? [marko] : []),
1233-
...(base.parsers.twig ? [twig] : []),
1234-
...(base.parsers.pug ? [pug] : []),
1235-
...(base.parsers['liquid-html'] ? [liquid] : []),
1251+
svelte,
1252+
astro,
1253+
marko,
1254+
twig,
1255+
pug,
1256+
liquid,
12361257
])
12371258

12381259
export interface PluginOptions {

0 commit comments

Comments
 (0)