Skip to content

Commit c39497f

Browse files
Remove top-level await from plugin (#420)
1 parent fa6e410 commit c39497f

10 files changed

Lines changed: 686 additions & 509 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- Remove top-level await ([#420](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/420))
13+
- Improve load-time performance ([#420](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/420))
14+
1015
### Fixed
1116

1217
- Collapse whitespace in template literals with adjacent quasis ([#427](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/427))

src/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function cacheForDirs<V>(
4848

4949
let pathToApiMap = expiringMap<string | null, Promise<UnifiedApi>>(10_000)
5050

51-
export async function getTailwindConfig(options: ParserOptions): Promise<any> {
51+
export async function getTailwindConfig(options: ParserOptions): Promise<UnifiedApi> {
5252
let cwd = process.cwd()
5353

5454
// Locate the file being processed

src/create-plugin.ts

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
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

Comments
 (0)