From d7c8448eecabbc5c1493de91452ea5636d5843af Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 9 Jan 2025 17:14:07 +0100 Subject: [PATCH] Remove all `@keyframes` in reference import mode (#15581) This PR fixes an issue where JavaScript plugins were still able to contribute `@keyframes` when loaded inside an `@reference` import. This was possible because we only gated the `addBase` API and not the `addUtilities` one which also has a special branch to handle `@keyframe` rules. To make this work, we have to create a new instance of the plugin API that has awareness of wether the plugin accessing it is inside reference import mode. ## Test plan Added a unit test that reproduces the issue observed via #15544 --- CHANGELOG.md | 1 + packages/tailwindcss/src/ast.ts | 5 ++ .../src/compat/apply-compat-hooks.ts | 23 +++-- packages/tailwindcss/src/compat/plugin-api.ts | 24 ++++-- packages/tailwindcss/src/index.test.ts | 86 ++++++++++++++++++- packages/tailwindcss/src/index.ts | 40 +-------- 6 files changed, 127 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49612ac2149d..547fe1dfaee2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improve performance and memory usage ([#15529](https://github.com/tailwindlabs/tailwindcss/pull/15529)) - Ensure `@apply` rules are processed in the correct order ([#15542](https://github.com/tailwindlabs/tailwindcss/pull/15542)) - Allow negative utility names in `@utilty` ([#15573](https://github.com/tailwindlabs/tailwindcss/pull/15573)) +- Remove all `@keyframes` contributed by JavaScript plugins when using `@reference` imports ([#15581](https://github.com/tailwindlabs/tailwindcss/pull/15581)) - _Upgrade (experimental)_: Do not extract class names from functions (e.g. `shadow` in `filter: 'drop-shadow(…)'`) ([#15566](https://github.com/tailwindlabs/tailwindcss/pull/15566)) ### Changed diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 2fe9d6901a4f..28d89a87cdbd 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -325,6 +325,11 @@ export function optimizeAst(ast: AstNode[]) { // Context else if (node.kind === 'context') { + // Remove reference imports from printing + if (node.context.reference) { + return + } + for (let child of node.nodes) { transform(child, parent, depth) } diff --git a/packages/tailwindcss/src/compat/apply-compat-hooks.ts b/packages/tailwindcss/src/compat/apply-compat-hooks.ts index c40b83f9910d..3b5cde1954f0 100644 --- a/packages/tailwindcss/src/compat/apply-compat-hooks.ts +++ b/packages/tailwindcss/src/compat/apply-compat-hooks.ts @@ -276,14 +276,27 @@ function upgradeToFullPluginSupport({ } } - let pluginApi = buildPluginApi(designSystem, ast, resolvedConfig, { - set current(value: number) { - features |= value + let pluginApiConfig = { + designSystem, + ast, + resolvedConfig, + featuresRef: { + set current(value: number) { + features |= value + }, }, - }) + } + + let pluginApi = buildPluginApi({ ...pluginApiConfig, referenceMode: false }) + let referenceModePluginApi = undefined for (let { handler, reference } of resolvedConfig.plugins) { - handler(reference ? { ...pluginApi, addBase: () => {} } : pluginApi) + if (reference) { + referenceModePluginApi ||= buildPluginApi({ ...pluginApiConfig, referenceMode: true }) + handler(referenceModePluginApi) + } else { + handler(pluginApi) + } } // Merge the user-configured theme keys into the design system. The compat diff --git a/packages/tailwindcss/src/compat/plugin-api.ts b/packages/tailwindcss/src/compat/plugin-api.ts index 05ce811bba43..e746b08468fc 100644 --- a/packages/tailwindcss/src/compat/plugin-api.ts +++ b/packages/tailwindcss/src/compat/plugin-api.ts @@ -86,14 +86,22 @@ export type PluginAPI = { const IS_VALID_UTILITY_NAME = /^[a-z@][a-zA-Z0-9/%._-]*$/ -export function buildPluginApi( - designSystem: DesignSystem, - ast: AstNode[], - resolvedConfig: ResolvedConfig, - featuresRef: { current: Features }, -): PluginAPI { +export function buildPluginApi({ + designSystem, + ast, + resolvedConfig, + featuresRef, + referenceMode, +}: { + designSystem: DesignSystem + ast: AstNode[] + resolvedConfig: ResolvedConfig + featuresRef: { current: Features } + referenceMode: boolean +}): PluginAPI { let api: PluginAPI = { addBase(css) { + if (referenceMode) return let baseNodes = objectToAst(css) featuresRef.current |= substituteFunctions(baseNodes, designSystem) ast.push(atRule('@layer', 'base', baseNodes)) @@ -212,7 +220,9 @@ export function buildPluginApi( for (let [name, css] of entries) { if (name.startsWith('@keyframes ')) { - ast.push(rule(name, objectToAst(css))) + if (!referenceMode) { + ast.push(rule(name, objectToAst(css))) + } continue } diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 679d3f1700ed..677f33593454 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -3212,7 +3212,7 @@ describe('`@import "…" reference`', () => { { loadStylesheet }, ) - expect(build(['text-underline', 'border']).trim()).toMatchInlineSnapshot(`"@layer utilities;"`) + expect(build(['text-underline', 'border']).trim()).toMatchInlineSnapshot(`""`) }) test('removes styles when the import resolver was handled outside of Tailwind CSS', async () => { @@ -3241,13 +3241,91 @@ describe('`@import "…" reference`', () => { [], ), ).resolves.toMatchInlineSnapshot(` - "@layer theme; - - @media (width >= 48rem) { + "@media (width >= 48rem) { .bar:hover, .bar:focus { color: red; } }" `) }) + + test('removes all @keyframes, even those contributed by JavasScript plugins', async () => { + await expect( + compileCss( + css` + @media reference { + @layer theme, base, components, utilities; + @layer theme { + @theme { + --animate-spin: spin 1s linear infinite; + --animate-ping: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite; + @keyframes spin { + to { + transform: rotate(360deg); + } + } + } + } + @layer base { + @keyframes ping { + 75%, + 100% { + transform: scale(2); + opacity: 0; + } + } + } + @plugin "my-plugin"; + } + + .bar { + @apply animate-spin; + } + `, + ['animate-spin', 'match-utility-initial', 'match-components-initial'], + { + loadModule: async () => ({ + module: ({ + addBase, + addUtilities, + addComponents, + matchUtilities, + matchComponents, + }: PluginAPI) => { + addBase({ + '@keyframes base': { '100%': { opacity: '0' } }, + }) + addUtilities({ + '@keyframes utilities': { '100%': { opacity: '0' } }, + }) + addComponents({ + '@keyframes components ': { '100%': { opacity: '0' } }, + }) + matchUtilities( + { + 'match-utility': (value) => ({ + '@keyframes match-utilities': { '100%': { opacity: '0' } }, + }), + }, + { values: { initial: 'initial' } }, + ) + matchComponents( + { + 'match-components': (value) => ({ + '@keyframes match-components': { '100%': { opacity: '0' } }, + }), + }, + { values: { initial: 'initial' } }, + ) + }, + base: '/root', + }), + }, + ), + ).resolves.toMatchInlineSnapshot(` + ".bar { + animation: var(--animate-spin); + }" + `) + }) }) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 794caeaaa847..856240ce2cec 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -362,42 +362,6 @@ async function parseCss( // Handle `@import "…" reference` else if (param === 'reference') { - walk(node.nodes, (child, { replaceWith }) => { - if (child.kind !== 'at-rule') { - replaceWith([]) - return WalkAction.Skip - } - switch (child.name) { - case '@theme': { - let themeParams = segment(child.params, ' ') - if (!themeParams.includes('reference')) { - child.params = (child.params === '' ? '' : ' ') + 'reference' - } - return WalkAction.Skip - } - case '@import': - case '@config': - case '@plugin': - case '@variant': - case '@utility': { - return WalkAction.Skip - } - - case '@media': - case '@supports': - case '@layer': { - // These rules should be recursively traversed as these might be - // inserted by the `@import` resolution. - return - } - - default: { - replaceWith([]) - return WalkAction.Skip - } - } - }) - node.nodes = [contextNode({ reference: true }, node.nodes)] } @@ -420,6 +384,10 @@ async function parseCss( if (node.name === '@theme') { let [themeOptions, themePrefix] = parseThemeOptions(node.params) + if (context.reference) { + themeOptions |= ThemeOptions.REFERENCE + } + if (themePrefix) { if (!IS_VALID_PREFIX.test(themePrefix)) { throw new Error(