Skip to content

Commit

Permalink
Speed up formatting (#153)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
thecrypticace committed May 3, 2023
1 parent 71f41a4 commit 474c344
Show file tree
Hide file tree
Showing 9 changed files with 589 additions and 420 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

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

### Fixed

- Speed up formatting ([#153](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/153))

## [0.2.8] - 2023-04-28

### Changed
Expand Down
108 changes: 108 additions & 0 deletions src/compat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { loadIfExists } from './utils.js'

let compatiblePlugins = [
'@ianvs/prettier-plugin-sort-imports',
'@trivago/prettier-plugin-sort-imports',
'prettier-plugin-organize-imports',
'@prettier/plugin-pug',
'@shopify/prettier-plugin-liquid',
'prettier-plugin-css-order',
'prettier-plugin-import-sort',
'prettier-plugin-jsdoc',
'prettier-plugin-organize-attributes',
'prettier-plugin-style-order',
'prettier-plugin-twig-melody',
]

let additionalParserPlugins = [
'prettier-plugin-astro',
'prettier-plugin-svelte',
'prettier-plugin-twig-melody',
'@prettier/plugin-pug',
'@shopify/prettier-plugin-liquid',
'prettier-plugin-marko',
]

let additionalPrinterPlugins = [
{
pkg: 'prettier-plugin-svelte',
formats: ['svelte-ast'],
},
]

// ---

/** @type {Map<string, any>} */
let parserMap = new Map()
let isTesting = process.env.NODE_ENV === 'test'

export function getCompatibleParser(base, parserFormat, options) {
if (parserMap.has(parserFormat) && !isTesting) {
return parserMap.get(parserFormat)
}

let parser = getFreshCompatibleParser(base, parserFormat, options)
parserMap.set(parserFormat, parser)
return parser
}

function getFreshCompatibleParser(base, parserFormat, options) {
if (!options.plugins) {
return base.parsers[parserFormat]
}

let parser = {
...base.parsers[parserFormat],
}

// Now load parsers from plugins
for (const name of compatiblePlugins) {
let path = null

try {
path = require.resolve(name)
} catch (err) {
continue
}

let plugin = options.plugins.find(
(plugin) => plugin.name === name || plugin.name === path,
)

// The plugin is not loaded
if (!plugin) {
continue
}

Object.assign(parser, plugin.parsers[parserFormat])
}

return parser
}

// We need to load this plugin dynamically because it's not available by default
// And we are not bundling it with the main Prettier plugin
export function getAdditionalParsers() {
let parsers = {}

for (const pkg of additionalParserPlugins) {
Object.assign(parsers, loadIfExists(pkg)?.parsers ?? {})
}

return parsers
}

export function getAdditionalPrinters() {
let printers = {}

for (let { pkg, formats } of additionalPrinterPlugins) {
let pluginPrinters = loadIfExists(pkg)?.printers
for (let format of formats) {
if (format in pluginPrinters) {
printers[format] = pluginPrinters[format]
}
}
}

return printers
}
188 changes: 188 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// @ts-check
import { expiringMap } from './expiring-map.js'
import clearModule from 'clear-module'
import escalade from 'escalade/sync'
import * as path from 'path'
import prettier from 'prettier'
import resolveFrom from 'resolve-from'
import { generateRules as generateRulesFallback } from 'tailwindcss/lib/lib/generateRules'
import { createContext as createContextFallback } from 'tailwindcss/lib/lib/setupContextUtils'
import loadConfigFallback from 'tailwindcss/loadConfig'
import resolveConfigFallback from 'tailwindcss/resolveConfig'

/**
* @typedef {object} ContextContainer
* @property {any} context
* @property {() => any} generateRules
* @property {any} tailwindConfig
**/

/**
* @typedef {object} PluginOptions
* @property {string} [tailwindConfig]
* @property {string} filepath
**/

/**
* @template K
* @template V
* @typedef {import('./expiring-map.js').ExpiringMap<K,V>} ExpiringMap
**/

/** @type {Map<string, string | null>} */
let sourceToPathMap = new Map()

/** @type {ExpiringMap<string | null, ContextContainer>} */
let pathToContextMap = expiringMap(10_000)

/** @type {ExpiringMap<string, string | null>} */
let prettierConfigCache = expiringMap(10_000)

/**
* @param {PluginOptions} options
* @returns {ContextContainer}
*/
export function getTailwindConfig(options) {
let key = `${options.filepath}:${options.tailwindConfig ?? ''}`
let baseDir = getBaseDir(options)

// Map the source file to it's associated Tailwind config file
let configPath = sourceToPathMap.get(key)
if (configPath === undefined) {
configPath = getConfigPath(options, baseDir)
sourceToPathMap.set(key, configPath)
}

// Now see if we've loaded the Tailwind config file before (and it's still valid)
let existing = pathToContextMap.get(configPath)
if (existing) {
return existing
}

// By this point we know we need to load the Tailwind config file
let result = loadTailwindConfig(baseDir, configPath)

pathToContextMap.set(configPath, result)

return result
}

/**
*
* @param {PluginOptions} options
* @returns {string | null}
*/
function getPrettierConfigPath(options) {
// Locating the config file can be mildly expensive so we cache it temporarily
let existingPath = prettierConfigCache.get(options.filepath)
if (existingPath !== undefined) {
return existingPath
}

let path = prettier.resolveConfigFile.sync(options.filepath)
prettierConfigCache.set(options.filepath, path)

return path
}

/**
* @param {PluginOptions} options
* @returns {string}
*/
function getBaseDir(options) {
let prettierConfigPath = getPrettierConfigPath(options)

if (options.tailwindConfig) {
return prettierConfigPath ? path.dirname(prettierConfigPath) : process.cwd()
}

return prettierConfigPath
? path.dirname(prettierConfigPath)
: options.filepath
? path.dirname(options.filepath)
: process.cwd()
}

/**
*
* @param {string} baseDir
* @param {string | null} tailwindConfigPath
* @returns {ContextContainer}
*/
function loadTailwindConfig(baseDir, tailwindConfigPath) {
let createContext = createContextFallback
let generateRules = generateRulesFallback
let resolveConfig = resolveConfigFallback
let loadConfig = loadConfigFallback
let tailwindConfig = {}

try {
let pkgDir = path.dirname(resolveFrom(baseDir, 'tailwindcss/package.json'))

resolveConfig = require(path.join(pkgDir, 'resolveConfig'))
createContext = require(path.join(
pkgDir,
'lib/lib/setupContextUtils',
)).createContext
generateRules = require(path.join(
pkgDir,
'lib/lib/generateRules',
)).generateRules

// Prior to `tailwindcss@3.3.0` this won't exist so we load it last
loadConfig = require(path.join(pkgDir, 'loadConfig'))
} catch {}

if (tailwindConfigPath) {
clearModule(tailwindConfigPath)
const loadedConfig = loadConfig(tailwindConfigPath)
tailwindConfig = loadedConfig.default ?? loadedConfig
}

// suppress "empty content" warning
tailwindConfig.content = ['no-op']

// Create the context
let context = createContext(resolveConfig(tailwindConfig))

return {
context,
tailwindConfig,
generateRules,
}
}

/**
* @param {PluginOptions} options
* @param {string} baseDir
* @returns {string | null}
*/
function getConfigPath(options, baseDir) {
if (options.tailwindConfig) {
return path.resolve(baseDir, options.tailwindConfig)
}

let configPath
try {
configPath = escalade(baseDir, (_dir, names) => {
if (names.includes('tailwind.config.js')) {
return 'tailwind.config.js'
}
if (names.includes('tailwind.config.cjs')) {
return 'tailwind.config.cjs'
}
if (names.includes('tailwind.config.mjs')) {
return 'tailwind.config.mjs'
}
if (names.includes('tailwind.config.ts')) {
return 'tailwind.config.ts'
}
})
} catch {}

if (configPath) {
return configPath
}

return null
}
38 changes: 38 additions & 0 deletions src/expiring-map.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* @template K
* @template V
* @typedef {object} ExpiringMap
* @property {(key: K) => V | undefined} get
* @property {(key: K, value: V) => void} set
*/

/**
* @template K
* @template V
* @param {number} duration
* @returns {ExpiringMap<K, V>}
*/
export function expiringMap(duration) {
/** @type {Map<K, {value: V, expiration: number}>} */
let map = new Map()

return {
get(key) {
if (map.has(key)) {
let result = map.get(key)
if (result.expiration > new Date()) {
return result.value
}
}
},
set(key, value) {
let expiration = new Date()
expiration.setMilliseconds(expiration.getMilliseconds() + duration)

map.set(key, {
value,
expiration,
})
},
}
}

0 comments on commit 474c344

Please sign in to comment.