Skip to content

Commit ee50b9b

Browse files
Add cache for known plugins (#301)
* Add cache for known plugins We have to call `require.resolve(…)` ~10 times per call to `parse(…)`. These calls generally take microseconds however if you call `prettier.format(…)` several thousand times the times add up. It also adds up for embedded documents which call `parse(…)` a large number of times. This adds a cache for these calls because generally only a few will ever be installed and Node.js will not cache misses internally. * Update src/plugins.ts Co-authored-by: Robin Malfait <[email protected]> * Update changelog --------- Co-authored-by: Robin Malfait <[email protected]>
1 parent f69f739 commit ee50b9b

File tree

4 files changed

+58
-27
lines changed

4 files changed

+58
-27
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
- Add support for `prettier-plugin-multiline-arrays` ([#299](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/299))
11+
- Add resolution cache for known plugins ([#301](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/301))
1112

1213
## [0.6.5] - 2024-06-17
1314

src/config.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import loadConfigFallback from 'tailwindcss/loadConfig'
1818
import resolveConfigFallback from 'tailwindcss/resolveConfig'
1919
import type { RequiredConfig } from 'tailwindcss/types/config.js'
2020
import { expiringMap } from './expiring-map.js'
21+
import { resolveIn } from './resolve'
2122
import type { ContextContainer } from './types'
2223

2324
let localRequire = createRequire(import.meta.url)
@@ -106,10 +107,7 @@ async function loadTailwindConfig(
106107
let tailwindConfig: RequiredConfig = { content: [] }
107108

108109
try {
109-
let pkgFile = localRequire.resolve('tailwindcss/package.json', {
110-
paths: [baseDir],
111-
})
112-
110+
let pkgFile = resolveIn('tailwindcss/package.json', [baseDir])
113111
let pkgDir = path.dirname(pkgFile)
114112

115113
try {
@@ -155,9 +153,7 @@ async function loadV4(
155153
entryPoint: string | null,
156154
) {
157155
// Import Tailwind — if this is v4 it'll have APIs we can use directly
158-
let pkgPath = localRequire.resolve('tailwindcss', {
159-
paths: [baseDir],
160-
})
156+
let pkgPath = resolveIn('tailwindcss', [baseDir])
161157
let tw = await import(pathToFileURL(pkgPath).toString())
162158

163159
// This is not Tailwind v4

src/plugins.ts

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { createRequire as req } from 'node:module'
21
import type { Parser, ParserOptions, Plugin, Printer } from 'prettier'
32
import './types'
43
import * as prettierParserAcorn from 'prettier/plugins/acorn'
@@ -9,26 +8,22 @@ import * as prettierParserHTML from 'prettier/plugins/html'
98
import * as prettierParserMeriyah from 'prettier/plugins/meriyah'
109
import * as prettierParserPostCSS from 'prettier/plugins/postcss'
1110
import * as prettierParserTypescript from 'prettier/plugins/typescript'
11+
import { loadIfExists, maybeResolve } from './resolve'
1212

1313
interface PluginDetails {
1414
parsers: Record<string, Parser<any>>
1515
printers: Record<string, Printer<any>>
1616
}
1717

1818
async function loadIfExistsESM(name: string): Promise<Plugin<any>> {
19-
try {
20-
if (req(import.meta.url).resolve(name)) {
21-
let mod = await import(name)
22-
return mod.default ?? mod
23-
}
19+
let mod = await loadIfExists<Plugin<any>>(name)
2420

25-
throw new Error('unreachable')
26-
} catch (e) {
27-
return {
28-
parsers: {},
29-
printers: {},
30-
}
21+
mod ??= {
22+
parsers: {},
23+
printers: {},
3124
}
25+
26+
return mod
3227
}
3328

3429
export async function loadPlugins() {
@@ -46,14 +41,6 @@ export async function loadPlugins() {
4641
...thirdparty.printers,
4742
}
4843

49-
function maybeResolve(name: string) {
50-
try {
51-
return req(import.meta.url).resolve(name)
52-
} catch (err) {
53-
return null
54-
}
55-
}
56-
5744
function findEnabledPlugin(
5845
options: ParserOptions<any>,
5946
name: string,

src/resolve.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { createRequire as req } from 'node:module'
2+
import { expiringMap } from './expiring-map'
3+
4+
const localRequire = req(import.meta.url)
5+
6+
// This is a long-lived cache for resolved modules whether they exist or not
7+
// Because we're compatible with a large number of plugins, we need to check
8+
// for the existence of a module before attempting to import it. This cache
9+
// is used to mitigate the cost of that check because Node.js does not cache
10+
// failed module resolutions making repeated checks very expensive.
11+
const resolveCache = expiringMap<string, string | null>(30_000)
12+
13+
export function resolveIn(id: string, paths: string[]) {
14+
return localRequire.resolve(id, {
15+
paths,
16+
})
17+
}
18+
19+
export function maybeResolve(name: string) {
20+
let modpath = resolveCache.get(name)
21+
22+
if (modpath === undefined) {
23+
modpath = freshMaybeResolve(name)
24+
resolveCache.set(name, modpath)
25+
}
26+
27+
return modpath
28+
}
29+
30+
export async function loadIfExists<T>(name: string): Promise<T | null> {
31+
let modpath = maybeResolve(name)
32+
33+
if (modpath) {
34+
let mod = await import(name)
35+
return mod.default ?? mod
36+
}
37+
38+
return null
39+
}
40+
41+
function freshMaybeResolve(name: string) {
42+
try {
43+
return localRequire.resolve(name)
44+
} catch (err) {
45+
return null
46+
}
47+
}

0 commit comments

Comments
 (0)