Skip to content

Commit 3c937ec

Browse files
authored
Inject polyfills after @import and body-less @layer (#17493)
This PR fixes an issue where polyfills were injected at the top, but they should be after `@import` and body-less `@layer` rules. This is necessary in case you are using Google fonts like this for example: ```css @import url('https://fonts.google.com'); @import "tailwindcss"; ``` While the `@import url(…);` sits above `@import "tailwindcss";` in the final generated CSS we injected the polyfills at the very beginning. This PR will inject the polyfills after the first AST Node that is not: 1. A comment 2. An external import — `@import url(…)` 3. A body-less layer — `@layer foo, bar, baz;` The snapshots look a little confusing, but that's because Lightning CSS is optimizing the output and moving things around a bit: <img width="1482" alt="image" src="https://github.com/user-attachments/assets/a0552c8b-93df-4e1d-ad90-8b8abf9492b1" /> [Lightning CSS Playground](https://lightningcss.dev/playground/index.html#%7B%22minify%22%3Afalse%2C%22customMedia%22%3Atrue%2C%22cssModules%22%3Afalse%2C%22analyzeDependencies%22%3Afalse%2C%22targets%22%3A%7B%22chrome%22%3A6225920%7D%2C%22include%22%3A0%2C%22exclude%22%3A0%2C%22source%22%3A%22%40layer%20theme%2C%20base%2C%20components%2C%20utilities%3B%5Cn%5Cn%40supports%20(((-webkit-hyphens%3A%20none))%20and%20(not%20(margin-trim%3A%20inline)))%20or%20((-moz-orient%3A%20inline)%20and%20(not%20(color%3A%20rgb(from%20red%20r%20g%20b))))%20%7B%5Cn%20%20%40layer%20base%20%7B%5Cn%20%20%20%20*%2C%20%3Abefore%2C%20%3Aafter%2C%20%3A%3Abackdrop%20%7B%5Cn%20%20%20%20%20%20--tw-font-weight%3A%20initial%3B%5Cn%20%20%20%20%7D%5Cn%20%20%7D%5Cn%7D%5Cn%5Cn%40layer%20theme%20%7B%5Cn%20%20%3Aroot%2C%20%3Ahost%20%7B%5Cn%20%20%20%20--font-sans%3A%20ui-sans-serif%2C%20system-ui%2C%20sans-serif%2C%20%5C%22Apple%20Color%20Emoji%5C%22%2C%20%5C%22Segoe%20UI%20Emoji%5C%22%2C%20%5C%22Segoe%20UI%20Symbol%5C%22%2C%20%5C%22Noto%20Color%20Emoji%5C%22%3B%5Cn%20%20%20%20--font-mono%3A%20ui-monospace%2C%20SFMono-Regular%2C%20Menlo%2C%20Monaco%2C%20Consolas%2C%20%5C%22Liberation%20Mono%5C%22%2C%20%5C%22Courier%20New%5C%22%2C%20monospace%3B%5Cn%20%20%20%5Cn%20%20%7D%5Cn%7D%5Cn%5Cn%40layer%20base%20%7B%5Cn%20%20*%2C%20%3Aafter%2C%20%3Abefore%2C%20%3A%3Abackdrop%20%7B%5Cn%20%20%20%20box-sizing%3A%20border-box%3B%5Cn%20%20%20%20border%3A%200%20solid%3B%5Cn%20%20%20%20margin%3A%200%3B%5Cn%20%20%20%20padding%3A%200%3B%5Cn%20%20%7D%5Cn%7D%5Cn%5Cn%40layer%20utilities%20%7B%5Cn%20%20.text-2xl%20%7B%5Cn%20%20%20%20font-size%3A%20var(--text-2xl)%3B%5Cn%20%20%20%20line-height%3A%20var(--tw-leading%2C%20var(--text-2xl--line-height))%3B%5Cn%20%20%7D%5Cn%7D%5Cn%5Cn%40property%20--tw-font-weight%20%7B%5Cn%20%20syntax%3A%20%5C%22*%5C%22%3B%5Cn%20%20inherits%3A%20false%5Cn%7D%22%2C%22visitorEnabled%22%3Afalse%2C%22visitor%22%3A%22%7B%5Cn%20%20Color(color)%20%7B%5Cn%20%20%20%20if%20(color.type%20%3D%3D%3D%20'rgb')%20%7B%5Cn%20%20%20%20%20%20color.g%20%3D%200%3B%5Cn%20%20%20%20%20%20return%20color%3B%5Cn%20%20%20%20%7D%5Cn%20%20%7D%5Cn%7D%22%2C%22unusedSymbols%22%3A%5B%5D%2C%22version%22%3A%22local%22%7D) Fixes: #17494
1 parent b069d7a commit 3c937ec

File tree

4 files changed

+310
-12
lines changed

4 files changed

+310
-12
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111

1212
- Disable padding in `@source inline(…)` brace expansion ([#17491](https://github.com/tailwindlabs/tailwindcss/pull/17491))
13+
- Inject polyfills after `@import` and body-less `@layer` ([#17493](https://github.com/tailwindlabs/tailwindcss/pull/17493))
1314

1415
## [4.1.0] - 2025-04-01
1516

integrations/cli/index.test.ts

+281
Original file line numberDiff line numberDiff line change
@@ -1563,6 +1563,287 @@ test(
15631563
},
15641564
)
15651565

1566+
test(
1567+
'polyfills should be imported after external `@import url(…)` statements',
1568+
{
1569+
fs: {
1570+
'package.json': json`
1571+
{
1572+
"dependencies": {
1573+
"tailwindcss": "workspace:^",
1574+
"@tailwindcss/cli": "workspace:^"
1575+
}
1576+
}
1577+
`,
1578+
'index.css': css`
1579+
@import url('https://fonts.googleapis.com');
1580+
@import 'tailwindcss';
1581+
`,
1582+
'index.html': html`<div class="bg-red-500/50 shadow-md"></div>`,
1583+
},
1584+
},
1585+
async ({ exec, fs, expect }) => {
1586+
await exec('pnpm tailwindcss --input ./index.css --output ./dist/out.css')
1587+
expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(`
1588+
"
1589+
--- ./dist/out.css ---
1590+
@import url('https://fonts.googleapis.com');
1591+
@layer theme, base, components, utilities;
1592+
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
1593+
@layer base {
1594+
*, ::before, ::after, ::backdrop {
1595+
--tw-shadow: 0 0 #0000;
1596+
--tw-shadow-color: initial;
1597+
--tw-shadow-alpha: 100%;
1598+
--tw-inset-shadow: 0 0 #0000;
1599+
--tw-inset-shadow-color: initial;
1600+
--tw-inset-shadow-alpha: 100%;
1601+
--tw-ring-color: initial;
1602+
--tw-ring-shadow: 0 0 #0000;
1603+
--tw-inset-ring-color: initial;
1604+
--tw-inset-ring-shadow: 0 0 #0000;
1605+
--tw-ring-inset: initial;
1606+
--tw-ring-offset-width: 0px;
1607+
--tw-ring-offset-color: #fff;
1608+
--tw-ring-offset-shadow: 0 0 #0000;
1609+
}
1610+
}
1611+
}
1612+
@layer theme {
1613+
:root, :host {
1614+
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
1615+
'Noto Color Emoji';
1616+
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
1617+
monospace;
1618+
--color-red-500: oklch(63.7% 0.237 25.331);
1619+
--default-font-family: var(--font-sans);
1620+
--default-mono-font-family: var(--font-mono);
1621+
}
1622+
}
1623+
@layer base {
1624+
*, ::after, ::before, ::backdrop, ::file-selector-button {
1625+
box-sizing: border-box;
1626+
margin: 0;
1627+
padding: 0;
1628+
border: 0 solid;
1629+
}
1630+
html, :host {
1631+
line-height: 1.5;
1632+
-webkit-text-size-adjust: 100%;
1633+
tab-size: 4;
1634+
font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji');
1635+
font-feature-settings: var(--default-font-feature-settings, normal);
1636+
font-variation-settings: var(--default-font-variation-settings, normal);
1637+
-webkit-tap-highlight-color: transparent;
1638+
}
1639+
hr {
1640+
height: 0;
1641+
color: inherit;
1642+
border-top-width: 1px;
1643+
}
1644+
abbr:where([title]) {
1645+
-webkit-text-decoration: underline dotted;
1646+
text-decoration: underline dotted;
1647+
}
1648+
h1, h2, h3, h4, h5, h6 {
1649+
font-size: inherit;
1650+
font-weight: inherit;
1651+
}
1652+
a {
1653+
color: inherit;
1654+
-webkit-text-decoration: inherit;
1655+
text-decoration: inherit;
1656+
}
1657+
b, strong {
1658+
font-weight: bolder;
1659+
}
1660+
code, kbd, samp, pre {
1661+
font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace);
1662+
font-feature-settings: var(--default-mono-font-feature-settings, normal);
1663+
font-variation-settings: var(--default-mono-font-variation-settings, normal);
1664+
font-size: 1em;
1665+
}
1666+
small {
1667+
font-size: 80%;
1668+
}
1669+
sub, sup {
1670+
font-size: 75%;
1671+
line-height: 0;
1672+
position: relative;
1673+
vertical-align: baseline;
1674+
}
1675+
sub {
1676+
bottom: -0.25em;
1677+
}
1678+
sup {
1679+
top: -0.5em;
1680+
}
1681+
table {
1682+
text-indent: 0;
1683+
border-color: inherit;
1684+
border-collapse: collapse;
1685+
}
1686+
:-moz-focusring {
1687+
outline: auto;
1688+
}
1689+
progress {
1690+
vertical-align: baseline;
1691+
}
1692+
summary {
1693+
display: list-item;
1694+
}
1695+
ol, ul, menu {
1696+
list-style: none;
1697+
}
1698+
img, svg, video, canvas, audio, iframe, embed, object {
1699+
display: block;
1700+
vertical-align: middle;
1701+
}
1702+
img, video {
1703+
max-width: 100%;
1704+
height: auto;
1705+
}
1706+
button, input, select, optgroup, textarea, ::file-selector-button {
1707+
font: inherit;
1708+
font-feature-settings: inherit;
1709+
font-variation-settings: inherit;
1710+
letter-spacing: inherit;
1711+
color: inherit;
1712+
border-radius: 0;
1713+
background-color: transparent;
1714+
opacity: 1;
1715+
}
1716+
:where(select:is([multiple], [size])) optgroup {
1717+
font-weight: bolder;
1718+
}
1719+
:where(select:is([multiple], [size])) optgroup option {
1720+
padding-inline-start: 20px;
1721+
}
1722+
::file-selector-button {
1723+
margin-inline-end: 4px;
1724+
}
1725+
::placeholder {
1726+
opacity: 1;
1727+
}
1728+
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
1729+
::placeholder {
1730+
color: color-mix(in oklab, currentColor 50%, transparent);
1731+
}
1732+
}
1733+
textarea {
1734+
resize: vertical;
1735+
}
1736+
::-webkit-search-decoration {
1737+
-webkit-appearance: none;
1738+
}
1739+
::-webkit-date-and-time-value {
1740+
min-height: 1lh;
1741+
text-align: inherit;
1742+
}
1743+
::-webkit-datetime-edit {
1744+
display: inline-flex;
1745+
}
1746+
::-webkit-datetime-edit-fields-wrapper {
1747+
padding: 0;
1748+
}
1749+
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
1750+
padding-block: 0;
1751+
}
1752+
:-moz-ui-invalid {
1753+
box-shadow: none;
1754+
}
1755+
button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button {
1756+
appearance: button;
1757+
}
1758+
::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
1759+
height: auto;
1760+
}
1761+
[hidden]:where(:not([hidden='until-found'])) {
1762+
display: none !important;
1763+
}
1764+
}
1765+
@layer utilities {
1766+
.bg-red-500\\/50 {
1767+
background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 50%, transparent);
1768+
@supports (color: color-mix(in lab, red, red)) {
1769+
background-color: color-mix(in oklab, var(--color-red-500) 50%, transparent);
1770+
}
1771+
}
1772+
.shadow-md {
1773+
--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
1774+
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
1775+
}
1776+
}
1777+
@property --tw-shadow {
1778+
syntax: "*";
1779+
inherits: false;
1780+
initial-value: 0 0 #0000;
1781+
}
1782+
@property --tw-shadow-color {
1783+
syntax: "*";
1784+
inherits: false;
1785+
}
1786+
@property --tw-shadow-alpha {
1787+
syntax: "<percentage>";
1788+
inherits: false;
1789+
initial-value: 100%;
1790+
}
1791+
@property --tw-inset-shadow {
1792+
syntax: "*";
1793+
inherits: false;
1794+
initial-value: 0 0 #0000;
1795+
}
1796+
@property --tw-inset-shadow-color {
1797+
syntax: "*";
1798+
inherits: false;
1799+
}
1800+
@property --tw-inset-shadow-alpha {
1801+
syntax: "<percentage>";
1802+
inherits: false;
1803+
initial-value: 100%;
1804+
}
1805+
@property --tw-ring-color {
1806+
syntax: "*";
1807+
inherits: false;
1808+
}
1809+
@property --tw-ring-shadow {
1810+
syntax: "*";
1811+
inherits: false;
1812+
initial-value: 0 0 #0000;
1813+
}
1814+
@property --tw-inset-ring-color {
1815+
syntax: "*";
1816+
inherits: false;
1817+
}
1818+
@property --tw-inset-ring-shadow {
1819+
syntax: "*";
1820+
inherits: false;
1821+
initial-value: 0 0 #0000;
1822+
}
1823+
@property --tw-ring-inset {
1824+
syntax: "*";
1825+
inherits: false;
1826+
}
1827+
@property --tw-ring-offset-width {
1828+
syntax: "<length>";
1829+
inherits: false;
1830+
initial-value: 0px;
1831+
}
1832+
@property --tw-ring-offset-color {
1833+
syntax: "*";
1834+
inherits: false;
1835+
initial-value: #fff;
1836+
}
1837+
@property --tw-ring-offset-shadow {
1838+
syntax: "*";
1839+
inherits: false;
1840+
initial-value: 0 0 #0000;
1841+
}
1842+
"
1843+
`)
1844+
},
1845+
)
1846+
15661847
function withBOM(text: string): string {
15671848
return '\uFEFF' + text
15681849
}

packages/@tailwindcss-postcss/src/__snapshots__/index.test.ts.snap

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

33
exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = `
4-
"@supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) {
5-
@layer base {
6-
*, :before, :after, ::backdrop {
7-
--tw-font-weight: initial;
8-
}
9-
}
10-
}
11-
12-
@layer theme {
4+
"@layer theme {
135
:root, :host {
146
--font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
157
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
@@ -289,6 +281,14 @@ exports[`\`@import 'tailwindcss'\` is replaced with the generated CSS 1`] = `
289281
}
290282
}
291283
284+
@supports (((-webkit-hyphens: none)) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) {
285+
@layer base {
286+
*, :before, :after, ::backdrop {
287+
--tw-font-weight: initial;
288+
}
289+
}
290+
}
291+
292292
@property --tw-font-weight {
293293
syntax: "*";
294294
inherits: false

packages/tailwindcss/src/ast.ts

+19-3
Original file line numberDiff line numberDiff line change
@@ -582,10 +582,26 @@ export function optimizeAst(
582582
}
583583

584584
if (fallbackAst.length > 0) {
585-
let firstNonCommentIndex = newAst.findIndex((item) => item.kind !== 'comment')
586-
if (firstNonCommentIndex === -1) firstNonCommentIndex = 0
585+
let firstValidNodeIndex = newAst.findIndex((node) => {
586+
// License comments
587+
if (node.kind === 'comment') return false
588+
589+
if (node.kind === 'at-rule') {
590+
// Charset
591+
if (node.name === '@charset') return false
592+
593+
// External imports
594+
if (node.name === '@import') return false
595+
596+
// Body-less `@layer …;`
597+
if (node.name === '@layer' && node.nodes.length === 0) return false
598+
}
599+
600+
return true
601+
})
602+
587603
newAst.splice(
588-
firstNonCommentIndex,
604+
firstValidNodeIndex < 0 ? newAst.length : firstValidNodeIndex,
589605
0,
590606
atRule(
591607
'@supports',

0 commit comments

Comments
 (0)