Skip to content

Commit

Permalink
Remove all @keyframes in reference import mode (#15581)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
philipp-spiess authored Jan 9, 2025
1 parent a3aec17 commit d7c8448
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 52 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions packages/tailwindcss/src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
23 changes: 18 additions & 5 deletions packages/tailwindcss/src/compat/apply-compat-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 17 additions & 7 deletions packages/tailwindcss/src/compat/plugin-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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
}

Expand Down
86 changes: 82 additions & 4 deletions packages/tailwindcss/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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);
}"
`)
})
})
40 changes: 4 additions & 36 deletions packages/tailwindcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
}

Expand All @@ -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(
Expand Down

0 comments on commit d7c8448

Please sign in to comment.