Skip to content

Commit

Permalink
Add cache for known plugins (#301)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
thecrypticace and RobinMalfait committed Jun 29, 2024
1 parent f69f739 commit ee50b9b
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 27 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

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

## [0.6.5] - 2024-06-17

Expand Down
10 changes: 3 additions & 7 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import loadConfigFallback from 'tailwindcss/loadConfig'
import resolveConfigFallback from 'tailwindcss/resolveConfig'
import type { RequiredConfig } from 'tailwindcss/types/config.js'
import { expiringMap } from './expiring-map.js'
import { resolveIn } from './resolve'
import type { ContextContainer } from './types'

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

try {
let pkgFile = localRequire.resolve('tailwindcss/package.json', {
paths: [baseDir],
})

let pkgFile = resolveIn('tailwindcss/package.json', [baseDir])
let pkgDir = path.dirname(pkgFile)

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

// This is not Tailwind v4
Expand Down
27 changes: 7 additions & 20 deletions src/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { createRequire as req } from 'node:module'
import type { Parser, ParserOptions, Plugin, Printer } from 'prettier'
import './types'
import * as prettierParserAcorn from 'prettier/plugins/acorn'
Expand All @@ -9,26 +8,22 @@ import * as prettierParserHTML from 'prettier/plugins/html'
import * as prettierParserMeriyah from 'prettier/plugins/meriyah'
import * as prettierParserPostCSS from 'prettier/plugins/postcss'
import * as prettierParserTypescript from 'prettier/plugins/typescript'
import { loadIfExists, maybeResolve } from './resolve'

interface PluginDetails {
parsers: Record<string, Parser<any>>
printers: Record<string, Printer<any>>
}

async function loadIfExistsESM(name: string): Promise<Plugin<any>> {
try {
if (req(import.meta.url).resolve(name)) {
let mod = await import(name)
return mod.default ?? mod
}
let mod = await loadIfExists<Plugin<any>>(name)

throw new Error('unreachable')
} catch (e) {
return {
parsers: {},
printers: {},
}
mod ??= {
parsers: {},
printers: {},
}

return mod
}

export async function loadPlugins() {
Expand All @@ -46,14 +41,6 @@ export async function loadPlugins() {
...thirdparty.printers,
}

function maybeResolve(name: string) {
try {
return req(import.meta.url).resolve(name)
} catch (err) {
return null
}
}

function findEnabledPlugin(
options: ParserOptions<any>,
name: string,
Expand Down
47 changes: 47 additions & 0 deletions src/resolve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { createRequire as req } from 'node:module'
import { expiringMap } from './expiring-map'

const localRequire = req(import.meta.url)

// This is a long-lived cache for resolved modules whether they exist or not
// Because we're compatible with a large number of plugins, we need to check
// for the existence of a module before attempting to import it. This cache
// is used to mitigate the cost of that check because Node.js does not cache
// failed module resolutions making repeated checks very expensive.
const resolveCache = expiringMap<string, string | null>(30_000)

export function resolveIn(id: string, paths: string[]) {
return localRequire.resolve(id, {
paths,
})
}

export function maybeResolve(name: string) {
let modpath = resolveCache.get(name)

if (modpath === undefined) {
modpath = freshMaybeResolve(name)
resolveCache.set(name, modpath)
}

return modpath
}

export async function loadIfExists<T>(name: string): Promise<T | null> {
let modpath = maybeResolve(name)

if (modpath) {
let mod = await import(name)
return mod.default ?? mod
}

return null
}

function freshMaybeResolve(name: string) {
try {
return localRequire.resolve(name)
} catch (err) {
return null
}
}

0 comments on commit ee50b9b

Please sign in to comment.