Optimize rebuilds in long-running processes

This commit is contained in:
Adam Wathan 2020-08-19 09:33:03 -04:00
parent d3606b76dc
commit ef149cfafb
8 changed files with 122 additions and 139 deletions

View File

@ -1,32 +1,15 @@
import postcss from 'postcss'
import substituteClassApplyAtRules from '../src/lib/substituteClassApplyAtRules'
import processPlugins from '../src/util/processPlugins'
import resolveConfig from '../src/util/resolveConfig'
import corePlugins from '../src/corePlugins'
import defaultConfig from '../stubs/defaultConfig.stub.js'
import tailwind from '../src/index'
const resolvedDefaultConfig = resolveConfig([defaultConfig])
const { utilities: defaultUtilities } = processPlugins(
corePlugins(resolvedDefaultConfig),
resolvedDefaultConfig
)
function run(input, config = resolvedDefaultConfig, utilities = defaultUtilities) {
return postcss([
substituteClassApplyAtRules(config, () => ({
utilities,
})),
]).process(input, {
from: undefined,
})
function run(input, config = {}) {
return postcss([tailwind({ ...config })]).process(input, { from: undefined })
}
test("it copies a class's declarations into itself", () => {
test('it copies the declarations from a class into itself', () => {
const output = '.a { color: red; } .b { color: red; }'
return run('.a { color: red; } .b { @apply .a; }').then(result => {
expect(result.css).toEqual(output)
expect(result.css).toMatchCss(output)
expect(result.warnings().length).toBe(0)
})
})
@ -43,7 +26,7 @@ test('selectors with invalid characters do not need to be manually escaped', ()
`
return run(input).then(result => {
expect(result.css).toEqual(expected)
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
@ -60,7 +43,7 @@ test('it removes important from applied classes by default', () => {
`
return run(input).then(result => {
expect(result.css).toEqual(expected)
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
@ -77,7 +60,7 @@ test('applied rules can be made !important', () => {
`
return run(input).then(result => {
expect(result.css).toEqual(expected)
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
@ -103,7 +86,7 @@ test('cssnext custom property sets are preserved', () => {
`
return run(input).then(result => {
expect(result.css).toEqual(expected)
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
@ -196,7 +179,7 @@ test('you can apply utility classes that do not actually exist as long as they w
`
return run(input).then(result => {
expect(result.css).toEqual(expected)
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
@ -210,15 +193,8 @@ test('you can apply utility classes without using the given prefix', () => {
.foo { margin-top: 1rem; margin-bottom: 1rem; }
`
const config = resolveConfig([
{
...defaultConfig,
prefix: 'tw-',
},
])
return run(input, config, processPlugins(corePlugins(config), config).utilities).then(result => {
expect(result.css).toEqual(expected)
return run(input, { prefix: 'tw-' }).then(result => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
@ -232,17 +208,12 @@ test('you can apply utility classes without using the given prefix when using a
.foo { margin-top: 1rem; margin-bottom: 1rem; }
`
const config = resolveConfig([
{
...defaultConfig,
prefix: () => {
return 'tw-'
},
return run(input, {
prefix: () => {
return 'tw-'
},
])
return run(input, config, processPlugins(corePlugins(config), config).utilities).then(result => {
expect(result.css).toEqual(expected)
}).then(result => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
@ -256,15 +227,8 @@ test('you can apply utility classes without specificity prefix even if important
.foo { margin-top: 2rem; margin-bottom: 2rem; }
`
const config = resolveConfig([
{
...defaultConfig,
important: '#app',
},
])
return run(input, config, processPlugins(corePlugins(config), config).utilities).then(result => {
expect(result.css).toEqual(expected)
return run(input, { important: '#app' }).then(result => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
@ -278,16 +242,11 @@ test('you can apply utility classes without using the given prefix even if impor
.foo { margin-top: 1rem; margin-bottom: 1rem; }
`
const config = resolveConfig([
{
...defaultConfig,
prefix: 'tw-',
important: '#app',
},
])
return run(input, config, processPlugins(corePlugins(config), config).utilities).then(result => {
expect(result.css).toEqual(expected)
return run(input, {
prefix: 'tw-',
important: '#app',
}).then(result => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})

View File

@ -1,48 +1,19 @@
import postcss from 'postcss'
import substituteClassApplyAtRules from '../src/lib/substituteClassApplyAtRules'
import processPlugins from '../src/util/processPlugins'
import resolveConfig from '../src/util/resolveConfig'
import corePlugins from '../src/corePlugins'
import defaultConfig from '../stubs/defaultConfig.stub.js'
import cloneNodes from '../src/util/cloneNodes'
import tailwind from '../src/index'
const resolvedDefaultConfig = resolveConfig([defaultConfig])
const defaultProcessedPlugins = processPlugins(
[...corePlugins(resolvedDefaultConfig), ...resolvedDefaultConfig.plugins],
resolvedDefaultConfig
)
const defaultGetProcessedPlugins = function() {
return {
...defaultProcessedPlugins,
base: cloneNodes(defaultProcessedPlugins.base),
components: cloneNodes(defaultProcessedPlugins.components),
utilities: cloneNodes(defaultProcessedPlugins.utilities),
}
}
function run(
input,
config = resolvedDefaultConfig,
getProcessedPlugins = () =>
config === resolvedDefaultConfig
? defaultGetProcessedPlugins()
: processPlugins(corePlugins(config), config)
) {
config.experimental = {
applyComplexClasses: true,
}
return postcss([substituteClassApplyAtRules(config, getProcessedPlugins)]).process(input, {
from: undefined,
})
function run(input, config = {}) {
return postcss([
tailwind({ experimental: { applyComplexClasses: true }, ...config }),
]).process(input, { from: undefined })
}
test('it copies class declarations into itself', () => {
const output = '.a { color: red; } .b { color: red; }'
return run('.a { color: red; } .b { @apply a; }').then(result => {
expect(result.css).toEqual(output)
expect(result.css).toMatchCss(output)
expect(result.warnings().length).toBe(0)
})
})

View File

@ -60,6 +60,7 @@
"lodash": "^4.17.15",
"node-emoji": "^1.8.1",
"normalize.css": "^8.0.1",
"object-hash": "^2.0.3",
"postcss": "^7.0.11",
"postcss-functions": "^3.0.0",
"postcss-js": "^2.0.0",

View File

@ -53,6 +53,10 @@ function futureFlagsAvailable(config) {
}
export function issueFlagNotices(config) {
if (process.env.JEST_WORKER_ID !== undefined) {
return
}
const log = {
info(messages) {
console.log('')

View File

@ -89,10 +89,30 @@ const cloneRuleWithParent = useMemo(
rule => rule
)
function buildUtilityMap(css) {
function buildUtilityMap(css, lookupTree) {
let index = 0
const utilityMap = {}
lookupTree.walkRules(rule => {
const utilityNames = extractUtilityNames(rule.selector)
utilityNames.forEach((utilityName, i) => {
if (utilityMap[utilityName] === undefined) {
utilityMap[utilityName] = []
}
utilityMap[utilityName].push({
index,
utilityName,
classPosition: i,
get rule() {
return cloneRuleWithParent(rule)
},
})
index++
})
})
css.walkRules(rule => {
const utilityNames = extractUtilityNames(rule.selector)
@ -151,8 +171,8 @@ function mergeAdjacentRules(initialRule, rulesToInsert) {
return rulesToInsert.filter(r => r.nodes.length > 0)
}
function makeExtractUtilityRules(css, config) {
const utilityMap = buildUtilityMap(css)
function makeExtractUtilityRules(css, lookupTree, config) {
const utilityMap = buildUtilityMap(css, lookupTree)
return function extractUtilityRules(utilityNames, rule) {
const combined = []
@ -182,7 +202,7 @@ function makeExtractUtilityRules(css, config) {
}
function processApplyAtRules(css, lookupTree, config) {
const extractUtilityRules = makeExtractUtilityRules(lookupTree, config)
const extractUtilityRules = makeExtractUtilityRules(css, lookupTree, config)
do {
css.walkRules(rule => {
@ -259,7 +279,9 @@ function processApplyAtRules(css, lookupTree, config) {
return css
}
export default function applyComplexClasses(config, getProcessedPlugins) {
let defaultTailwindTree = null
export default function applyComplexClasses(config, getProcessedPlugins, configChanged) {
return function(css) {
// We can stop already when we don't have any @apply rules. Vue users: you're welcome!
if (!hasAtRule(css, 'apply')) {
@ -268,31 +290,39 @@ export default function applyComplexClasses(config, getProcessedPlugins) {
// Tree already contains @tailwind rules, don't prepend default Tailwind tree
if (hasAtRule(css, 'tailwind')) {
return processApplyAtRules(css, css, config)
return processApplyAtRules(css, postcss.root(), config)
}
// Tree contains no @tailwind rules, so generate all of Tailwind's styles and
// prepend them to the user's CSS. Important for <style> blocks in Vue components.
return postcss([
substituteTailwindAtRules(config, getProcessedPlugins()),
evaluateTailwindFunctions(config),
substituteVariantsAtRules(config, getProcessedPlugins()),
substituteResponsiveAtRules(config),
convertLayerAtRulesToControlComments(config),
substituteScreenAtRules(config),
])
.process(
`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
{ from: undefined }
)
.then(result => {
// Prepend Tailwind's generated classes to the tree so they are available for `@apply`
const lookupTree = _.tap(result.root, tree => tree.append(css.clone()))
return processApplyAtRules(css, lookupTree, config)
})
const generateLookupTree =
configChanged || defaultTailwindTree === null
? () => {
return postcss([
substituteTailwindAtRules(config, getProcessedPlugins()),
evaluateTailwindFunctions(config),
substituteVariantsAtRules(config, getProcessedPlugins()),
substituteResponsiveAtRules(config),
convertLayerAtRulesToControlComments(config),
substituteScreenAtRules(config),
])
.process(
`
@tailwind base;
@tailwind components;
@tailwind utilities;
`,
{ from: undefined }
)
.then(result => {
defaultTailwindTree = result
return defaultTailwindTree
})
}
: () => Promise.resolve(defaultTailwindTree)
return generateLookupTree().then(result => {
return processApplyAtRules(css, result.root, config)
})
}
}

View File

@ -56,14 +56,19 @@ function findClass(classToApply, classTable, onError) {
return match.clone().nodes
}
export default function(config, getProcessedPlugins) {
let shadowLookup = null
export default function(config, getProcessedPlugins, configChanged) {
if (flagEnabled(config, 'applyComplexClasses')) {
return applyComplexClasses(config, getProcessedPlugins)
return applyComplexClasses(config, getProcessedPlugins, configChanged)
}
return function(css) {
const classLookup = buildClassTable(css)
const shadowLookup = buildShadowTable(getProcessedPlugins().utilities)
shadowLookup =
configChanged || !shadowLookup
? buildShadowTable(getProcessedPlugins().utilities)
: shadowLookup
css.walkRules(rule => {
rule.walkAtRules('apply', atRule => {

View File

@ -16,25 +16,33 @@ import processPlugins from './util/processPlugins'
import cloneNodes from './util/cloneNodes'
import { issueFlagNotices } from './featureFlags.js'
import hash from 'object-hash'
let flagsIssued = null
let previousConfig = null
let processedPlugins = null
let getProcessedPlugins = null
export default function(getConfig) {
return function(css) {
const config = getConfig()
const configChanged = hash(previousConfig) !== hash(config)
previousConfig = config
if (!flagsIssued || !_.isEqual(flagsIssued, _.pick(config, ['future', 'experimental']))) {
flagsIssued = _.pick(config, ['future', 'experimental'])
issueFlagNotices(config)
}
const processedPlugins = processPlugins([...corePlugins(config), ...config.plugins], config)
const getProcessedPlugins = function() {
return {
...processedPlugins,
base: cloneNodes(processedPlugins.base),
components: cloneNodes(processedPlugins.components),
utilities: cloneNodes(processedPlugins.utilities),
if (configChanged) {
processedPlugins = processPlugins([...corePlugins(config), ...config.plugins], config)
getProcessedPlugins = function() {
return {
...processedPlugins,
base: cloneNodes(processedPlugins.base),
components: cloneNodes(processedPlugins.components),
utilities: cloneNodes(processedPlugins.utilities),
}
}
}
@ -45,7 +53,7 @@ export default function(getConfig) {
substituteResponsiveAtRules(config),
convertLayerAtRulesToControlComments(config),
substituteScreenAtRules(config),
substituteClassApplyAtRules(config, getProcessedPlugins),
substituteClassApplyAtRules(config, getProcessedPlugins, configChanged),
applyImportantConfiguration(config),
purgeUnusedStyles(config),
]).process(css, { from: _.get(css, 'source.input.file') })

View File

@ -4122,6 +4122,11 @@ object-copy@^0.1.0:
define-property "^0.2.5"
kind-of "^3.0.3"
object-hash@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea"
integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg==
object-keys@^1.0.11, object-keys@^1.0.12:
version "1.1.1"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"