Skip to content

Commit 474c344

Browse files
Speed up formatting (#153)
* Speed up compatible parser loading * Speed up config loading * Cleanup code * Disable pug plugin printer during tests * Simplify fixture tests * Cleanup * Cleanup * Extract plugin compat handling into separate file * Refactor * Add cross-file config reuse * Move config loading * Update changelog
1 parent 71f41a4 commit 474c344

File tree

9 files changed

+589
-420
lines changed

9 files changed

+589
-420
lines changed

CHANGELOG.md

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

1212
- Added support for `prettier-plugin-marko` ([#151](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/151))
1313

14+
### Fixed
15+
16+
- Speed up formatting ([#153](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/153))
17+
1418
## [0.2.8] - 2023-04-28
1519

1620
### Changed

src/compat.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { loadIfExists } from './utils.js'
2+
3+
let compatiblePlugins = [
4+
'@ianvs/prettier-plugin-sort-imports',
5+
'@trivago/prettier-plugin-sort-imports',
6+
'prettier-plugin-organize-imports',
7+
'@prettier/plugin-pug',
8+
'@shopify/prettier-plugin-liquid',
9+
'prettier-plugin-css-order',
10+
'prettier-plugin-import-sort',
11+
'prettier-plugin-jsdoc',
12+
'prettier-plugin-organize-attributes',
13+
'prettier-plugin-style-order',
14+
'prettier-plugin-twig-melody',
15+
]
16+
17+
let additionalParserPlugins = [
18+
'prettier-plugin-astro',
19+
'prettier-plugin-svelte',
20+
'prettier-plugin-twig-melody',
21+
'@prettier/plugin-pug',
22+
'@shopify/prettier-plugin-liquid',
23+
'prettier-plugin-marko',
24+
]
25+
26+
let additionalPrinterPlugins = [
27+
{
28+
pkg: 'prettier-plugin-svelte',
29+
formats: ['svelte-ast'],
30+
},
31+
]
32+
33+
// ---
34+
35+
/** @type {Map<string, any>} */
36+
let parserMap = new Map()
37+
let isTesting = process.env.NODE_ENV === 'test'
38+
39+
export function getCompatibleParser(base, parserFormat, options) {
40+
if (parserMap.has(parserFormat) && !isTesting) {
41+
return parserMap.get(parserFormat)
42+
}
43+
44+
let parser = getFreshCompatibleParser(base, parserFormat, options)
45+
parserMap.set(parserFormat, parser)
46+
return parser
47+
}
48+
49+
function getFreshCompatibleParser(base, parserFormat, options) {
50+
if (!options.plugins) {
51+
return base.parsers[parserFormat]
52+
}
53+
54+
let parser = {
55+
...base.parsers[parserFormat],
56+
}
57+
58+
// Now load parsers from plugins
59+
for (const name of compatiblePlugins) {
60+
let path = null
61+
62+
try {
63+
path = require.resolve(name)
64+
} catch (err) {
65+
continue
66+
}
67+
68+
let plugin = options.plugins.find(
69+
(plugin) => plugin.name === name || plugin.name === path,
70+
)
71+
72+
// The plugin is not loaded
73+
if (!plugin) {
74+
continue
75+
}
76+
77+
Object.assign(parser, plugin.parsers[parserFormat])
78+
}
79+
80+
return parser
81+
}
82+
83+
// We need to load this plugin dynamically because it's not available by default
84+
// And we are not bundling it with the main Prettier plugin
85+
export function getAdditionalParsers() {
86+
let parsers = {}
87+
88+
for (const pkg of additionalParserPlugins) {
89+
Object.assign(parsers, loadIfExists(pkg)?.parsers ?? {})
90+
}
91+
92+
return parsers
93+
}
94+
95+
export function getAdditionalPrinters() {
96+
let printers = {}
97+
98+
for (let { pkg, formats } of additionalPrinterPlugins) {
99+
let pluginPrinters = loadIfExists(pkg)?.printers
100+
for (let format of formats) {
101+
if (format in pluginPrinters) {
102+
printers[format] = pluginPrinters[format]
103+
}
104+
}
105+
}
106+
107+
return printers
108+
}

src/config.js

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// @ts-check
2+
import { expiringMap } from './expiring-map.js'
3+
import clearModule from 'clear-module'
4+
import escalade from 'escalade/sync'
5+
import * as path from 'path'
6+
import prettier from 'prettier'
7+
import resolveFrom from 'resolve-from'
8+
import { generateRules as generateRulesFallback } from 'tailwindcss/lib/lib/generateRules'
9+
import { createContext as createContextFallback } from 'tailwindcss/lib/lib/setupContextUtils'
10+
import loadConfigFallback from 'tailwindcss/loadConfig'
11+
import resolveConfigFallback from 'tailwindcss/resolveConfig'
12+
13+
/**
14+
* @typedef {object} ContextContainer
15+
* @property {any} context
16+
* @property {() => any} generateRules
17+
* @property {any} tailwindConfig
18+
**/
19+
20+
/**
21+
* @typedef {object} PluginOptions
22+
* @property {string} [tailwindConfig]
23+
* @property {string} filepath
24+
**/
25+
26+
/**
27+
* @template K
28+
* @template V
29+
* @typedef {import('./expiring-map.js').ExpiringMap<K,V>} ExpiringMap
30+
**/
31+
32+
/** @type {Map<string, string | null>} */
33+
let sourceToPathMap = new Map()
34+
35+
/** @type {ExpiringMap<string | null, ContextContainer>} */
36+
let pathToContextMap = expiringMap(10_000)
37+
38+
/** @type {ExpiringMap<string, string | null>} */
39+
let prettierConfigCache = expiringMap(10_000)
40+
41+
/**
42+
* @param {PluginOptions} options
43+
* @returns {ContextContainer}
44+
*/
45+
export function getTailwindConfig(options) {
46+
let key = `${options.filepath}:${options.tailwindConfig ?? ''}`
47+
let baseDir = getBaseDir(options)
48+
49+
// Map the source file to it's associated Tailwind config file
50+
let configPath = sourceToPathMap.get(key)
51+
if (configPath === undefined) {
52+
configPath = getConfigPath(options, baseDir)
53+
sourceToPathMap.set(key, configPath)
54+
}
55+
56+
// Now see if we've loaded the Tailwind config file before (and it's still valid)
57+
let existing = pathToContextMap.get(configPath)
58+
if (existing) {
59+
return existing
60+
}
61+
62+
// By this point we know we need to load the Tailwind config file
63+
let result = loadTailwindConfig(baseDir, configPath)
64+
65+
pathToContextMap.set(configPath, result)
66+
67+
return result
68+
}
69+
70+
/**
71+
*
72+
* @param {PluginOptions} options
73+
* @returns {string | null}
74+
*/
75+
function getPrettierConfigPath(options) {
76+
// Locating the config file can be mildly expensive so we cache it temporarily
77+
let existingPath = prettierConfigCache.get(options.filepath)
78+
if (existingPath !== undefined) {
79+
return existingPath
80+
}
81+
82+
let path = prettier.resolveConfigFile.sync(options.filepath)
83+
prettierConfigCache.set(options.filepath, path)
84+
85+
return path
86+
}
87+
88+
/**
89+
* @param {PluginOptions} options
90+
* @returns {string}
91+
*/
92+
function getBaseDir(options) {
93+
let prettierConfigPath = getPrettierConfigPath(options)
94+
95+
if (options.tailwindConfig) {
96+
return prettierConfigPath ? path.dirname(prettierConfigPath) : process.cwd()
97+
}
98+
99+
return prettierConfigPath
100+
? path.dirname(prettierConfigPath)
101+
: options.filepath
102+
? path.dirname(options.filepath)
103+
: process.cwd()
104+
}
105+
106+
/**
107+
*
108+
* @param {string} baseDir
109+
* @param {string | null} tailwindConfigPath
110+
* @returns {ContextContainer}
111+
*/
112+
function loadTailwindConfig(baseDir, tailwindConfigPath) {
113+
let createContext = createContextFallback
114+
let generateRules = generateRulesFallback
115+
let resolveConfig = resolveConfigFallback
116+
let loadConfig = loadConfigFallback
117+
let tailwindConfig = {}
118+
119+
try {
120+
let pkgDir = path.dirname(resolveFrom(baseDir, 'tailwindcss/package.json'))
121+
122+
resolveConfig = require(path.join(pkgDir, 'resolveConfig'))
123+
createContext = require(path.join(
124+
pkgDir,
125+
'lib/lib/setupContextUtils',
126+
)).createContext
127+
generateRules = require(path.join(
128+
pkgDir,
129+
'lib/lib/generateRules',
130+
)).generateRules
131+
132+
// Prior to `[email protected]` this won't exist so we load it last
133+
loadConfig = require(path.join(pkgDir, 'loadConfig'))
134+
} catch {}
135+
136+
if (tailwindConfigPath) {
137+
clearModule(tailwindConfigPath)
138+
const loadedConfig = loadConfig(tailwindConfigPath)
139+
tailwindConfig = loadedConfig.default ?? loadedConfig
140+
}
141+
142+
// suppress "empty content" warning
143+
tailwindConfig.content = ['no-op']
144+
145+
// Create the context
146+
let context = createContext(resolveConfig(tailwindConfig))
147+
148+
return {
149+
context,
150+
tailwindConfig,
151+
generateRules,
152+
}
153+
}
154+
155+
/**
156+
* @param {PluginOptions} options
157+
* @param {string} baseDir
158+
* @returns {string | null}
159+
*/
160+
function getConfigPath(options, baseDir) {
161+
if (options.tailwindConfig) {
162+
return path.resolve(baseDir, options.tailwindConfig)
163+
}
164+
165+
let configPath
166+
try {
167+
configPath = escalade(baseDir, (_dir, names) => {
168+
if (names.includes('tailwind.config.js')) {
169+
return 'tailwind.config.js'
170+
}
171+
if (names.includes('tailwind.config.cjs')) {
172+
return 'tailwind.config.cjs'
173+
}
174+
if (names.includes('tailwind.config.mjs')) {
175+
return 'tailwind.config.mjs'
176+
}
177+
if (names.includes('tailwind.config.ts')) {
178+
return 'tailwind.config.ts'
179+
}
180+
})
181+
} catch {}
182+
183+
if (configPath) {
184+
return configPath
185+
}
186+
187+
return null
188+
}

src/expiring-map.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* @template K
3+
* @template V
4+
* @typedef {object} ExpiringMap
5+
* @property {(key: K) => V | undefined} get
6+
* @property {(key: K, value: V) => void} set
7+
*/
8+
9+
/**
10+
* @template K
11+
* @template V
12+
* @param {number} duration
13+
* @returns {ExpiringMap<K, V>}
14+
*/
15+
export function expiringMap(duration) {
16+
/** @type {Map<K, {value: V, expiration: number}>} */
17+
let map = new Map()
18+
19+
return {
20+
get(key) {
21+
if (map.has(key)) {
22+
let result = map.get(key)
23+
if (result.expiration > new Date()) {
24+
return result.value
25+
}
26+
}
27+
},
28+
set(key, value) {
29+
let expiration = new Date()
30+
expiration.setMilliseconds(expiration.getMilliseconds() + duration)
31+
32+
map.set(key, {
33+
value,
34+
expiration,
35+
})
36+
},
37+
}
38+
}

0 commit comments

Comments
 (0)