Skip to content

Commit d7c8448

Browse files
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
1 parent a3aec17 commit d7c8448

File tree

6 files changed

+127
-52
lines changed

6 files changed

+127
-52
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2525
- Improve performance and memory usage ([#15529](https://github.com/tailwindlabs/tailwindcss/pull/15529))
2626
- Ensure `@apply` rules are processed in the correct order ([#15542](https://github.com/tailwindlabs/tailwindcss/pull/15542))
2727
- Allow negative utility names in `@utilty` ([#15573](https://github.com/tailwindlabs/tailwindcss/pull/15573))
28+
- Remove all `@keyframes` contributed by JavaScript plugins when using `@reference` imports ([#15581](https://github.com/tailwindlabs/tailwindcss/pull/15581))
2829
- _Upgrade (experimental)_: Do not extract class names from functions (e.g. `shadow` in `filter: 'drop-shadow(…)'`) ([#15566](https://github.com/tailwindlabs/tailwindcss/pull/15566))
2930

3031
### Changed

packages/tailwindcss/src/ast.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,11 @@ export function optimizeAst(ast: AstNode[]) {
325325

326326
// Context
327327
else if (node.kind === 'context') {
328+
// Remove reference imports from printing
329+
if (node.context.reference) {
330+
return
331+
}
332+
328333
for (let child of node.nodes) {
329334
transform(child, parent, depth)
330335
}

packages/tailwindcss/src/compat/apply-compat-hooks.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -276,14 +276,27 @@ function upgradeToFullPluginSupport({
276276
}
277277
}
278278

279-
let pluginApi = buildPluginApi(designSystem, ast, resolvedConfig, {
280-
set current(value: number) {
281-
features |= value
279+
let pluginApiConfig = {
280+
designSystem,
281+
ast,
282+
resolvedConfig,
283+
featuresRef: {
284+
set current(value: number) {
285+
features |= value
286+
},
282287
},
283-
})
288+
}
289+
290+
let pluginApi = buildPluginApi({ ...pluginApiConfig, referenceMode: false })
291+
let referenceModePluginApi = undefined
284292

285293
for (let { handler, reference } of resolvedConfig.plugins) {
286-
handler(reference ? { ...pluginApi, addBase: () => {} } : pluginApi)
294+
if (reference) {
295+
referenceModePluginApi ||= buildPluginApi({ ...pluginApiConfig, referenceMode: true })
296+
handler(referenceModePluginApi)
297+
} else {
298+
handler(pluginApi)
299+
}
287300
}
288301

289302
// Merge the user-configured theme keys into the design system. The compat

packages/tailwindcss/src/compat/plugin-api.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,22 @@ export type PluginAPI = {
8686

8787
const IS_VALID_UTILITY_NAME = /^[a-z@][a-zA-Z0-9/%._-]*$/
8888

89-
export function buildPluginApi(
90-
designSystem: DesignSystem,
91-
ast: AstNode[],
92-
resolvedConfig: ResolvedConfig,
93-
featuresRef: { current: Features },
94-
): PluginAPI {
89+
export function buildPluginApi({
90+
designSystem,
91+
ast,
92+
resolvedConfig,
93+
featuresRef,
94+
referenceMode,
95+
}: {
96+
designSystem: DesignSystem
97+
ast: AstNode[]
98+
resolvedConfig: ResolvedConfig
99+
featuresRef: { current: Features }
100+
referenceMode: boolean
101+
}): PluginAPI {
95102
let api: PluginAPI = {
96103
addBase(css) {
104+
if (referenceMode) return
97105
let baseNodes = objectToAst(css)
98106
featuresRef.current |= substituteFunctions(baseNodes, designSystem)
99107
ast.push(atRule('@layer', 'base', baseNodes))
@@ -212,7 +220,9 @@ export function buildPluginApi(
212220

213221
for (let [name, css] of entries) {
214222
if (name.startsWith('@keyframes ')) {
215-
ast.push(rule(name, objectToAst(css)))
223+
if (!referenceMode) {
224+
ast.push(rule(name, objectToAst(css)))
225+
}
216226
continue
217227
}
218228

packages/tailwindcss/src/index.test.ts

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3212,7 +3212,7 @@ describe('`@import "…" reference`', () => {
32123212
{ loadStylesheet },
32133213
)
32143214

3215-
expect(build(['text-underline', 'border']).trim()).toMatchInlineSnapshot(`"@layer utilities;"`)
3215+
expect(build(['text-underline', 'border']).trim()).toMatchInlineSnapshot(`""`)
32163216
})
32173217

32183218
test('removes styles when the import resolver was handled outside of Tailwind CSS', async () => {
@@ -3241,13 +3241,91 @@ describe('`@import "…" reference`', () => {
32413241
[],
32423242
),
32433243
).resolves.toMatchInlineSnapshot(`
3244-
"@layer theme;
3245-
3246-
@media (width >= 48rem) {
3244+
"@media (width >= 48rem) {
32473245
.bar:hover, .bar:focus {
32483246
color: red;
32493247
}
32503248
}"
32513249
`)
32523250
})
3251+
3252+
test('removes all @keyframes, even those contributed by JavasScript plugins', async () => {
3253+
await expect(
3254+
compileCss(
3255+
css`
3256+
@media reference {
3257+
@layer theme, base, components, utilities;
3258+
@layer theme {
3259+
@theme {
3260+
--animate-spin: spin 1s linear infinite;
3261+
--animate-ping: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
3262+
@keyframes spin {
3263+
to {
3264+
transform: rotate(360deg);
3265+
}
3266+
}
3267+
}
3268+
}
3269+
@layer base {
3270+
@keyframes ping {
3271+
75%,
3272+
100% {
3273+
transform: scale(2);
3274+
opacity: 0;
3275+
}
3276+
}
3277+
}
3278+
@plugin "my-plugin";
3279+
}
3280+
3281+
.bar {
3282+
@apply animate-spin;
3283+
}
3284+
`,
3285+
['animate-spin', 'match-utility-initial', 'match-components-initial'],
3286+
{
3287+
loadModule: async () => ({
3288+
module: ({
3289+
addBase,
3290+
addUtilities,
3291+
addComponents,
3292+
matchUtilities,
3293+
matchComponents,
3294+
}: PluginAPI) => {
3295+
addBase({
3296+
'@keyframes base': { '100%': { opacity: '0' } },
3297+
})
3298+
addUtilities({
3299+
'@keyframes utilities': { '100%': { opacity: '0' } },
3300+
})
3301+
addComponents({
3302+
'@keyframes components ': { '100%': { opacity: '0' } },
3303+
})
3304+
matchUtilities(
3305+
{
3306+
'match-utility': (value) => ({
3307+
'@keyframes match-utilities': { '100%': { opacity: '0' } },
3308+
}),
3309+
},
3310+
{ values: { initial: 'initial' } },
3311+
)
3312+
matchComponents(
3313+
{
3314+
'match-components': (value) => ({
3315+
'@keyframes match-components': { '100%': { opacity: '0' } },
3316+
}),
3317+
},
3318+
{ values: { initial: 'initial' } },
3319+
)
3320+
},
3321+
base: '/root',
3322+
}),
3323+
},
3324+
),
3325+
).resolves.toMatchInlineSnapshot(`
3326+
".bar {
3327+
animation: var(--animate-spin);
3328+
}"
3329+
`)
3330+
})
32533331
})

packages/tailwindcss/src/index.ts

Lines changed: 4 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -362,42 +362,6 @@ async function parseCss(
362362

363363
// Handle `@import "…" reference`
364364
else if (param === 'reference') {
365-
walk(node.nodes, (child, { replaceWith }) => {
366-
if (child.kind !== 'at-rule') {
367-
replaceWith([])
368-
return WalkAction.Skip
369-
}
370-
switch (child.name) {
371-
case '@theme': {
372-
let themeParams = segment(child.params, ' ')
373-
if (!themeParams.includes('reference')) {
374-
child.params = (child.params === '' ? '' : ' ') + 'reference'
375-
}
376-
return WalkAction.Skip
377-
}
378-
case '@import':
379-
case '@config':
380-
case '@plugin':
381-
case '@variant':
382-
case '@utility': {
383-
return WalkAction.Skip
384-
}
385-
386-
case '@media':
387-
case '@supports':
388-
case '@layer': {
389-
// These rules should be recursively traversed as these might be
390-
// inserted by the `@import` resolution.
391-
return
392-
}
393-
394-
default: {
395-
replaceWith([])
396-
return WalkAction.Skip
397-
}
398-
}
399-
})
400-
401365
node.nodes = [contextNode({ reference: true }, node.nodes)]
402366
}
403367

@@ -420,6 +384,10 @@ async function parseCss(
420384
if (node.name === '@theme') {
421385
let [themeOptions, themePrefix] = parseThemeOptions(node.params)
422386

387+
if (context.reference) {
388+
themeOptions |= ThemeOptions.REFERENCE
389+
}
390+
423391
if (themePrefix) {
424392
if (!IS_VALID_PREFIX.test(themePrefix)) {
425393
throw new Error(

0 commit comments

Comments
 (0)