diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a0c93a6..5a79df24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/config.ts b/src/config.ts index 289adb32..4161226f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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) @@ -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 { @@ -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 diff --git a/src/plugins.ts b/src/plugins.ts index 82d2f984..1fae06f9 100644 --- a/src/plugins.ts +++ b/src/plugins.ts @@ -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' @@ -9,6 +8,7 @@ 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> @@ -16,19 +16,14 @@ interface PluginDetails { } async function loadIfExistsESM(name: string): Promise> { - try { - if (req(import.meta.url).resolve(name)) { - let mod = await import(name) - return mod.default ?? mod - } + let mod = await loadIfExists>(name) - throw new Error('unreachable') - } catch (e) { - return { - parsers: {}, - printers: {}, - } + mod ??= { + parsers: {}, + printers: {}, } + + return mod } export async function loadPlugins() { @@ -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, name: string, diff --git a/src/resolve.ts b/src/resolve.ts new file mode 100644 index 00000000..80a095ff --- /dev/null +++ b/src/resolve.ts @@ -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(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(name: string): Promise { + 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 + } +}