Do not crash when migrating empty classes (#20205)
This PR fixes a crash while using `npx @tailwindcss/upgrade` when
migrating classes with no body inside of an `@layer utilities`.
When running `npx @tailwindcss/upgrade`, one thing we do is migrate the
CSS from:
```css
@layer utilities {
.foo {
color: red;
}
}
```
To:
```css
@utility foo {
color: red;
}
```
We already have some logic that leaves non-classe (IDs, attribute
selectors, ...) alone. But if we are migrating a class that has no body,
then we will migrate that as well:
```css
@layer utilities {
.empty {
}
}
```
Is turned into:
```css
@utility empty {
}
```
But later in the migration process this will result in an error because
a `@utility` has to have at least _some_ nodes.
Ideally, you don't even have CSS that has empty rules since it doesn't
have any effect in the browser (except of making your CSS file bigger),
but it could be that you don't have control over this file, so a fix is
still valid.
This PR solves that by leaving those rules alone, and keep them in an
`@layer utilities`.
Fixes: #20204
## Test plan
1. Added a dedicated test for this usecase
This commit is contained in:
parent
3f58e52e36
commit
68fcdf1f05
3 changed files with 219 additions and 68 deletions
|
|
@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Allow `@variant` to be used inside `addBase` ([#19480](https://github.com/tailwindlabs/tailwindcss/pull/19480))
|
||||
- Ensure `@source` globs with symlinks are preserved ([#20203](https://github.com/tailwindlabs/tailwindcss/pull/20203))
|
||||
- Ensure later `@source` rules can re-include files excluded by earlier `@source not` rules ([#20203](https://github.com/tailwindlabs/tailwindcss/pull/20203))
|
||||
- Upgrade: don't migrate empty class rules to invalid `@utility` rules ([#20205](https://github.com/tailwindlabs/tailwindcss/pull/20205))
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ async function migrate(
|
|||
.use(sortBuckets())
|
||||
.use(formatNodes())
|
||||
.process(stylesheet.root!, { from: expect.getState().testPath })
|
||||
.then((result) => result.css)
|
||||
.then((result) => `\n${result.css.trim()}\n`)
|
||||
}
|
||||
|
||||
it('should migrate simple `@layer utilities` to `@utility`', async () => {
|
||||
|
|
@ -52,9 +52,11 @@ it('should migrate simple `@layer utilities` to `@utility`', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
color: red;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -69,13 +71,15 @@ it('should split multiple selectors in separate utilities', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
color: red;
|
||||
}
|
||||
|
||||
@utility bar {
|
||||
color: red;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -99,14 +103,16 @@ it('should merge `@utility` with the same name', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.bar {
|
||||
color: blue;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -149,7 +155,8 @@ it('should leave non-class utilities alone', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
/* 2. */
|
||||
/* 2.1. */
|
||||
color: red;
|
||||
|
|
@ -182,7 +189,58 @@ it('should leave non-class utilities alone', async () => {
|
|||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
it('should not convert empty classes into `@utility` since that would make them invalid', async () => {
|
||||
expect(
|
||||
await migrate(css`
|
||||
@layer utilities {
|
||||
.good {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.bad-empty {
|
||||
}
|
||||
|
||||
/* Comment above */
|
||||
.bad-with-comment-above {
|
||||
}
|
||||
|
||||
.bad-with-comment-inside {
|
||||
/* Comment inside */
|
||||
}
|
||||
|
||||
.bad-with-comment-below {
|
||||
}
|
||||
/* Comment below */
|
||||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"
|
||||
@utility good {
|
||||
color: red;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.bad-empty {
|
||||
}
|
||||
|
||||
/* Comment above */
|
||||
.bad-with-comment-above {
|
||||
}
|
||||
|
||||
.bad-with-comment-inside {
|
||||
/* Comment inside */
|
||||
}
|
||||
|
||||
.bad-with-comment-below {
|
||||
}
|
||||
/* Comment below */
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -204,7 +262,8 @@ it('should migrate simple `@layer utilities` with nesting to `@utility`', async
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
color: red;
|
||||
|
||||
&:hover {
|
||||
|
|
@ -214,7 +273,8 @@ it('should migrate simple `@layer utilities` with nesting to `@utility`', async
|
|||
&:focus {
|
||||
color: green;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -232,13 +292,15 @@ it('should migrate multiple simple `@layer utilities` to `@utility`', async () =
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
color: red;
|
||||
}
|
||||
|
||||
@utility bar {
|
||||
color: blue;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -260,7 +322,8 @@ it('should not migrate Rules inside of Rules to a `@utility`', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
color: red;
|
||||
}
|
||||
|
||||
|
|
@ -270,7 +333,8 @@ it('should not migrate Rules inside of Rules to a `@utility`', async () => {
|
|||
.baz {
|
||||
color: green;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -286,11 +350,13 @@ it('should invert at-rules to make them migrate-able', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
@media (min-width: 640px) {
|
||||
color: red;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -314,7 +380,8 @@ it('should migrate at-rules with multiple utilities and invert them', async () =
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
@media (min-width: 640px) {
|
||||
color: red;
|
||||
}
|
||||
|
|
@ -324,7 +391,8 @@ it('should migrate at-rules with multiple utilities and invert them', async () =
|
|||
@media (min-width: 640px) {
|
||||
color: blue;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -356,7 +424,8 @@ it('should migrate deeply nested at-rules with multiple utilities and invert the
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
@media (min-width: 640px) {
|
||||
color: red;
|
||||
}
|
||||
|
|
@ -384,7 +453,8 @@ it('should migrate deeply nested at-rules with multiple utilities and invert the
|
|||
}
|
||||
}
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -398,11 +468,13 @@ it('should migrate classes with pseudo elements', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility no-scrollbar {
|
||||
"
|
||||
@utility no-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -416,11 +488,13 @@ it('should migrate classes with attribute selectors', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility no-scrollbar {
|
||||
"
|
||||
@utility no-scrollbar {
|
||||
&[data-checked=''] {
|
||||
display: none;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -434,11 +508,13 @@ it('should migrate classes with element selectors', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility no-scrollbar {
|
||||
"
|
||||
@utility no-scrollbar {
|
||||
& main {
|
||||
display: none;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -452,11 +528,13 @@ it('should migrate classes attached to an element selector', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility no-scrollbar {
|
||||
"
|
||||
@utility no-scrollbar {
|
||||
&main {
|
||||
display: none;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -470,11 +548,13 @@ it('should migrate classes with id selectors', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility no-scrollbar {
|
||||
"
|
||||
@utility no-scrollbar {
|
||||
&#main {
|
||||
display: none;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -488,7 +568,8 @@ it('should migrate classes with another attached class', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility no-scrollbar {
|
||||
"
|
||||
@utility no-scrollbar {
|
||||
&.main {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -498,7 +579,8 @@ it('should migrate classes with another attached class', async () => {
|
|||
&.no-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -512,7 +594,8 @@ it('should migrate a selector with multiple classes to multiple @utility definit
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
& .bar:hover .baz:focus {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -528,7 +611,8 @@ it('should migrate a selector with multiple classes to multiple @utility definit
|
|||
.foo .bar:hover &:focus {
|
||||
display: none;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -548,7 +632,8 @@ it('should merge `@utility` definitions with the same name', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility step {
|
||||
"
|
||||
@utility step {
|
||||
counter-increment: step;
|
||||
|
||||
&:before {
|
||||
|
|
@ -556,7 +641,8 @@ it('should merge `@utility` definitions with the same name', async () => {
|
|||
@apply ml-[-41px];
|
||||
content: counter(step);
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -570,7 +656,8 @@ it('should not migrate nested classes inside a `:not(…)`', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
& .bar:not(.qux):has(.baz) {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -586,7 +673,8 @@ it('should not migrate nested classes inside a `:not(…)`', async () => {
|
|||
.foo .bar:not(.qux):has(&) {
|
||||
display: none;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -620,7 +708,8 @@ it('should migrate advanced combinations', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
@media (width >= 100px) {
|
||||
@supports (display: none) {
|
||||
& .bar:not(.qux):has(.baz) {
|
||||
|
|
@ -659,7 +748,8 @@ it('should migrate advanced combinations', async () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -677,12 +767,14 @@ describe('comments', () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"/* Start of utilities: */
|
||||
"
|
||||
/* Start of utilities: */
|
||||
@utility foo {
|
||||
/* Utility #1 */
|
||||
/* Declarations: */
|
||||
color: red;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -699,7 +791,8 @@ describe('comments', () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"/* Start of utilities: */
|
||||
"
|
||||
/* Start of utilities: */
|
||||
@utility foo {
|
||||
/* Foo & Bar */
|
||||
& .bar {
|
||||
|
|
@ -713,7 +806,8 @@ describe('comments', () => {
|
|||
/* Declarations: */
|
||||
color: red;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -733,7 +827,8 @@ describe('comments', () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"/* Start of utilities: */
|
||||
"
|
||||
/* Start of utilities: */
|
||||
@utility foo {
|
||||
/* Mobile only */
|
||||
@media (width <= 640px) {
|
||||
|
|
@ -741,7 +836,8 @@ describe('comments', () => {
|
|||
/* Declarations: */
|
||||
color: red;
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -780,7 +876,8 @@ describe('comments', () => {
|
|||
/* After */
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"/* Tailwind Utilities: */
|
||||
"
|
||||
/* Tailwind Utilities: */
|
||||
@utility no-scrollbar {
|
||||
/* Chrome, Safari and Opera */
|
||||
/* Second comment */
|
||||
|
|
@ -807,7 +904,8 @@ describe('comments', () => {
|
|||
.after {
|
||||
/* Inside */
|
||||
}
|
||||
/* After */"
|
||||
/* After */
|
||||
"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
|
@ -832,7 +930,8 @@ it('should not lose attribute selectors', async () => {
|
|||
}
|
||||
`),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@layer components {
|
||||
"
|
||||
@layer components {
|
||||
#TableOfContents {
|
||||
.toc a {
|
||||
@apply block max-w-full truncate py-1 pl-2 hover:font-medium hover:no-underline;
|
||||
|
|
@ -845,7 +944,8 @@ it('should not lose attribute selectors', async () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -863,11 +963,13 @@ describe('layered stylesheets', () => {
|
|||
layers: ['utilities'],
|
||||
}),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
/* Utility #1 */
|
||||
/* Declarations: */
|
||||
color: red;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -884,11 +986,13 @@ describe('layered stylesheets', () => {
|
|||
layers: ['components'],
|
||||
}),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
/* Utility #1 */
|
||||
/* Declarations: */
|
||||
color: red;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -905,11 +1009,13 @@ describe('layered stylesheets', () => {
|
|||
layers: ['foo'],
|
||||
}),
|
||||
).toMatchInlineSnapshot(`
|
||||
"/* Utility #1 */
|
||||
"
|
||||
/* Utility #1 */
|
||||
.foo {
|
||||
/* Declarations: */
|
||||
color: red;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -929,7 +1035,8 @@ describe('layered stylesheets', () => {
|
|||
layers: ['utilities'],
|
||||
}),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
/* Utility #1 */
|
||||
/* Declarations: */
|
||||
color: red;
|
||||
|
|
@ -937,7 +1044,8 @@ describe('layered stylesheets', () => {
|
|||
|
||||
#main {
|
||||
color: red;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -978,7 +1086,8 @@ describe('layered stylesheets', () => {
|
|||
layers: ['utilities'],
|
||||
}),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@utility foo {
|
||||
"
|
||||
@utility foo {
|
||||
@layer utilities {
|
||||
@layer utilities {
|
||||
/* Utility #1 */
|
||||
|
|
@ -1011,7 +1120,8 @@ describe('layered stylesheets', () => {
|
|||
|
||||
#secondary {
|
||||
color: red;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -1028,11 +1138,13 @@ describe('layered stylesheets', () => {
|
|||
layers: ['utilities'],
|
||||
}),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@import 'thing';
|
||||
"
|
||||
@import 'thing';
|
||||
|
||||
@utility foo {
|
||||
color: red;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
|
|
@ -1049,11 +1161,13 @@ describe('layered stylesheets', () => {
|
|||
layers: ['utilities'],
|
||||
}),
|
||||
).toMatchInlineSnapshot(`
|
||||
"@charset "utf-8";
|
||||
"
|
||||
@charset "utf-8";
|
||||
|
||||
@utility foo {
|
||||
color: red;
|
||||
}"
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type AtRule, type Comment, type Plugin, type Rule } from 'postcss'
|
||||
import { type AtRule, type Comment, type Container, type Plugin, type Rule } from 'postcss'
|
||||
import SelectorParser from 'postcss-selector-parser'
|
||||
import { segment } from '../../../../tailwindcss/src/utils/segment'
|
||||
import { Stylesheet } from '../../stylesheet'
|
||||
|
|
@ -43,6 +43,7 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin {
|
|||
|
||||
walk(atRule, (node) => {
|
||||
if (node.type !== 'rule') return
|
||||
if (isEmpty(node)) return
|
||||
|
||||
// Find all the classes in the selector
|
||||
SelectorParser((selectors) => {
|
||||
|
|
@ -184,6 +185,18 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin {
|
|||
toRemove.push(node.parent?.nodes.at(i) as Comment)
|
||||
}
|
||||
}
|
||||
|
||||
let commentsAfter: Comment[] = []
|
||||
for (let i = idx + 1; i < (node.parent?.nodes.length ?? 0); i++) {
|
||||
if (node.parent?.nodes.at(i)?.type === 'rule') {
|
||||
commentsAfter = []
|
||||
break
|
||||
}
|
||||
if (node.parent?.nodes.at(i)?.type === 'comment') {
|
||||
commentsAfter.push(node.parent?.nodes.at(i) as Comment)
|
||||
}
|
||||
}
|
||||
toRemove.push(...commentsAfter)
|
||||
}
|
||||
for (let node of toRemove) {
|
||||
node.remove()
|
||||
|
|
@ -231,8 +244,12 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
// Remove empty rules
|
||||
if ((node.type === 'rule' || node.type === 'atrule') && node.nodes?.length === 0) {
|
||||
// Remove empty rules from `@utility` clones and empty wrapper at-rules
|
||||
// from the default `@layer` clone.
|
||||
if (
|
||||
(node.type === 'atrule' && node.nodes?.length === 0) ||
|
||||
(clone !== defaultsAtRule && node.type === 'rule' && node.nodes?.length === 0)
|
||||
) {
|
||||
node.remove()
|
||||
}
|
||||
|
||||
|
|
@ -261,6 +278,11 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin {
|
|||
// Remove empty clones entirely
|
||||
if (clone.nodes?.length === 0) {
|
||||
clones.splice(idx, 1)
|
||||
} else if (clone === defaultsAtRule) {
|
||||
let first = clone.nodes?.[0]
|
||||
if (first) {
|
||||
first.raws.before = first.raws.before?.replace(/\n\s*\n/g, '\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -316,3 +338,17 @@ export function migrateAtLayerUtilities(stylesheet: Stylesheet): Plugin {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
function isEmpty(node: Container): boolean {
|
||||
// We have no body
|
||||
if (node.nodes === undefined) return false
|
||||
|
||||
// We have a body, but no nodes
|
||||
if (node.nodes.length === 0) return true
|
||||
|
||||
// We have nodes, but they are only comments which we consider empty
|
||||
if (node.nodes.every((child) => child.type === 'comment')) return true
|
||||
|
||||
// Not empty
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue