-
Notifications
You must be signed in to change notification settings - Fork 134
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
1 parent
71f41a4
commit 474c344
Showing
9 changed files
with
589 additions
and
420 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}) | ||
}, | ||
} | ||
} |
Oops, something went wrong.