From 3892f47a5bfd37c40aabb59d1ae2260da0d17c73 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 8 Dec 2020 13:07:25 +0100 Subject: [PATCH 1/7] fix memory leak --- src/processTailwindFeatures.js | 2 ++ src/util/disposables.js | 22 ++++++++++++++++++++++ src/util/useMemo.js | 15 ++++++++++++--- 3 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 src/util/disposables.js diff --git a/src/processTailwindFeatures.js b/src/processTailwindFeatures.js index 2ab557ca15a1..76b335fddd1c 100644 --- a/src/processTailwindFeatures.js +++ b/src/processTailwindFeatures.js @@ -18,6 +18,7 @@ import { issueFlagNotices } from './featureFlags.js' import hash from 'object-hash' import log from './util/log' +import { shared } from './util/disposables' let previousConfig = null let processedPlugins = null @@ -30,6 +31,7 @@ export default function (getConfig) { previousConfig = config if (configChanged) { + shared.dispose() if (config.target) { log.warn([ 'The `target` feature has been removed in Tailwind CSS v2.0.', diff --git a/src/util/disposables.js b/src/util/disposables.js new file mode 100644 index 000000000000..46b61758609b --- /dev/null +++ b/src/util/disposables.js @@ -0,0 +1,22 @@ +export function disposables() { + let disposables = [] + + let api = { + add(cb) { + disposables.push(cb) + + return () => { + let idx = disposables.indexOf(cb) + if (idx !== -1) disposables.splice(idx, 1) + } + }, + dispose() { + disposables.splice(0).forEach((dispose) => dispose()) + }, + } + + return api +} + +// A shared disposables collection +export let shared = disposables() diff --git a/src/util/useMemo.js b/src/util/useMemo.js index a70c397f05fb..e050c9e48b06 100644 --- a/src/util/useMemo.js +++ b/src/util/useMemo.js @@ -1,14 +1,23 @@ +import { shared } from './disposables' + export function useMemo(cb, keyResolver) { - const cache = new Map() + let cache = new Map() + + function clearCache() { + cache.clear() + shared.add(clearCache) + } + + shared.add(clearCache) return (...args) => { - const key = keyResolver(...args) + let key = keyResolver(...args) if (cache.has(key)) { return cache.get(key) } - const result = cb(...args) + let result = cb(...args) cache.set(key, result) return result From 40358cb4e81319e0545c1343dfac610adcdc444d Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 8 Dec 2020 18:24:01 +0100 Subject: [PATCH 2/7] add optional condition to hasAtRule --- src/lib/substituteClassApplyAtRules.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/lib/substituteClassApplyAtRules.js b/src/lib/substituteClassApplyAtRules.js index 7c7c89adc88d..daeabb48e386 100644 --- a/src/lib/substituteClassApplyAtRules.js +++ b/src/lib/substituteClassApplyAtRules.js @@ -11,15 +11,17 @@ import substituteScreenAtRules from './substituteScreenAtRules' import prefixSelector from '../util/prefixSelector' import { useMemo } from '../util/useMemo' -function hasAtRule(css, atRule) { - let foundAtRule = false +function hasAtRule(css, atRule, condition = () => true) { + let found = false - css.walkAtRules(atRule, () => { - foundAtRule = true - return false + css.walkAtRules(atRule, (node) => { + if (condition(node)) { + found = true + return false + } }) - return foundAtRule + return found } function cloneWithoutChildren(node) { From 9206ae26c2d08cef78553418dda8b50b0e944751 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 8 Dec 2020 18:24:08 +0100 Subject: [PATCH 3/7] use known tree to handle `@apply` when required `@tailwind` at rules exists Otherwise we will generate the lookup tree. --- __tests__/applyAtRule.test.js | 6 +++--- src/lib/substituteClassApplyAtRules.js | 10 +++++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/__tests__/applyAtRule.test.js b/__tests__/applyAtRule.test.js index 13f7b8ef907f..37d626d90ed4 100644 --- a/__tests__/applyAtRule.test.js +++ b/__tests__/applyAtRule.test.js @@ -337,7 +337,7 @@ test('you can apply utility classes that do not actually exist as long as they w }) }) -test('the shadow lookup is only used if no @tailwind rules were in the source tree', () => { +test('shadow lookup will be constructed when we have missing @tailwind atrules', () => { const input = ` @tailwind base; .foo { @apply mt-4; } @@ -345,8 +345,8 @@ test('the shadow lookup is only used if no @tailwind rules were in the source tr expect.assertions(1) - return run(input).catch((e) => { - expect(e).toMatchObject({ name: 'CssSyntaxError' }) + return run(input).then((result) => { + expect(result.css).toContain(`.foo { margin-top: 1rem;\n}`) }) }) diff --git a/src/lib/substituteClassApplyAtRules.js b/src/lib/substituteClassApplyAtRules.js index daeabb48e386..8d4892093a91 100644 --- a/src/lib/substituteClassApplyAtRules.js +++ b/src/lib/substituteClassApplyAtRules.js @@ -310,7 +310,15 @@ export default function substituteClassApplyAtRules(config, getProcessedPlugins, } // Tree already contains @tailwind rules, don't prepend default Tailwind tree - if (hasAtRule(css, 'tailwind')) { + let requiredTailwindAtRules = ['utilities'] + if ( + hasAtRule(css, 'tailwind', (node) => { + let idx = requiredTailwindAtRules.indexOf(node.params) + if (idx !== -1) requiredTailwindAtRules.splice(idx, 1) + if (requiredTailwindAtRules.length <= 0) return true + return false + }) + ) { return processApplyAtRules(css, postcss.root(), config) } From 15f837e634dad07c3a4f7942d0f91cac103d9215 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 10 Dec 2020 15:04:29 +0100 Subject: [PATCH 4/7] only generate the missing `@tailwind` atrules when using `@apply` --- src/lib/substituteClassApplyAtRules.js | 45 +++++++++++++++----------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/src/lib/substituteClassApplyAtRules.js b/src/lib/substituteClassApplyAtRules.js index 8d4892093a91..4dbc6aacb444 100644 --- a/src/lib/substituteClassApplyAtRules.js +++ b/src/lib/substituteClassApplyAtRules.js @@ -11,15 +11,23 @@ import substituteScreenAtRules from './substituteScreenAtRules' import prefixSelector from '../util/prefixSelector' import { useMemo } from '../util/useMemo' -function hasAtRule(css, atRule, condition = () => true) { +function hasAtRule(css, atRule, condition) { let found = false - css.walkAtRules(atRule, (node) => { - if (condition(node)) { - found = true - return false - } - }) + css.walkAtRules( + atRule, + condition === undefined + ? () => { + found = true + return false + } + : (node) => { + if (condition(node)) { + found = true + return false + } + } + ) return found } @@ -309,8 +317,7 @@ export default function substituteClassApplyAtRules(config, getProcessedPlugins, return css } - // Tree already contains @tailwind rules, don't prepend default Tailwind tree - let requiredTailwindAtRules = ['utilities'] + let requiredTailwindAtRules = ['base', 'components', 'utilities'] if ( hasAtRule(css, 'tailwind', (node) => { let idx = requiredTailwindAtRules.indexOf(node.params) @@ -319,11 +326,16 @@ export default function substituteClassApplyAtRules(config, getProcessedPlugins, return false }) ) { + // Tree already contains all the at rules (requiredTailwindAtRules) 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