Skip to content

Commit 020e7d9

Browse files
committed
fix: properly handle scoped class injection with spread attributes (#400)
* fix: properly handle scoped class injection when a spread attribute is present * chore(lint): simplify component check
1 parent 5d1ff56 commit 020e7d9

File tree

5 files changed

+74
-4
lines changed

5 files changed

+74
-4
lines changed

.changeset/curvy-peas-buy.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@astrojs/compiler': patch
3+
---
4+
5+
Fix long-standing bug where a `class` attribute inside of a spread prop will cause duplicate `class` attributes

internal/printer/print-to-js.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -423,11 +423,11 @@ func render1(p *printer, n *Node, opts RenderOptions) {
423423
panic(`Element with a slot='...' attribute must be a child of a component or a descendant of a custom element`)
424424
}
425425
if n.Parent.CustomElement {
426-
p.printAttribute(a)
426+
p.printAttribute(a, n)
427427
p.addSourceMapping(n.Loc[0])
428428
}
429429
} else {
430-
p.printAttribute(a)
430+
p.printAttribute(a, n)
431431
p.addSourceMapping(n.Loc[0])
432432
}
433433
}

internal/printer/printer.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ func (p *printer) print(text string) {
4848
p.output = append(p.output, text...)
4949
}
5050

51+
func (p *printer) printf(format string, a ...interface{}) {
52+
p.output = append(p.output, []byte(fmt.Sprintf(format, a...))...)
53+
}
54+
5155
func (p *printer) println(text string) {
5256
p.output = append(p.output, (text + "\n")...)
5357
}
@@ -259,7 +263,7 @@ func (p *printer) printStyleOrScript(opts RenderOptions, n *astro.Node) {
259263
p.print("},\n")
260264
}
261265

262-
func (p *printer) printAttribute(attr astro.Attribute) {
266+
func (p *printer) printAttribute(attr astro.Attribute, n *astro.Node) {
263267
if attr.Key == "define:vars" || attr.Key == "set:text" || attr.Key == "set:html" || attr.Key == "is:raw" {
264268
return
265269
}
@@ -294,10 +298,29 @@ func (p *printer) printAttribute(attr astro.Attribute) {
294298
p.addSourceMapping(attr.KeyLoc)
295299
p.print(`, "` + strings.TrimSpace(attr.Key) + `")}`)
296300
case astro.SpreadAttribute:
301+
injectClass := false
302+
for p := n.Parent; p != nil; p = p.Parent {
303+
if p.Parent == nil && len(p.Styles) != 0 {
304+
injectClass = true
305+
break
306+
}
307+
}
308+
if injectClass {
309+
for _, a := range n.Attr {
310+
if a.Key == "class" || a.Key == "class:list" {
311+
injectClass = false
312+
break
313+
}
314+
}
315+
}
297316
p.print(fmt.Sprintf("${%s(", SPREAD_ATTRIBUTES))
298317
p.addSourceMapping(loc.Loc{Start: attr.KeyLoc.Start - 3})
299318
p.print(strings.TrimSpace(attr.Key))
300-
p.print(`, "` + strings.TrimSpace(attr.Key) + `")}`)
319+
if !injectClass {
320+
p.printf(`,"%s")}`, strings.TrimSpace(attr.Key))
321+
} else {
322+
p.printf(`,"%s",{"class":"astro-%s"})}`, strings.TrimSpace(attr.Key), p.opts.Scope)
323+
}
301324
case astro.ShorthandAttribute:
302325
withoutComments := removeComments(attr.Key)
303326
if len(withoutComments) == 0 {

internal/printer/printer_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1309,6 +1309,37 @@ import { Container, Col, Row } from 'react-bootstrap';
13091309
code: "<head>\n\n\n\n\n\n\n" + RENDER_HEAD_RESULT + "</head>\n<div class=\"astro-LASNTLJA\"></div>",
13101310
},
13111311
},
1312+
{
1313+
name: "class with spread",
1314+
source: `<div class="something" {...Astro.props} />`,
1315+
want: want{
1316+
code: `<div class="something"${$$spreadAttributes(Astro.props,"Astro.props")}></div>`,
1317+
},
1318+
},
1319+
{
1320+
name: "class:list with spread",
1321+
source: `<div class:list="something" {...Astro.props} />`,
1322+
want: want{
1323+
code: `<div class:list="something"${$$spreadAttributes(Astro.props,"Astro.props")}></div>`,
1324+
},
1325+
},
1326+
{
1327+
name: "spread without style or class",
1328+
source: `<div {...Astro.props} />`,
1329+
want: want{
1330+
code: `<div${$$spreadAttributes(Astro.props,"Astro.props")}></div>`,
1331+
},
1332+
},
1333+
{
1334+
name: "spread with style but no explicit class",
1335+
source: `<style>div { color: red; }</style><div {...Astro.props} />`,
1336+
want: want{
1337+
styles: []string{
1338+
"{props:{\"data-astro-id\":\"TN53UTDL\"},children:`div.astro-TN53UTDL{color:red}`}",
1339+
},
1340+
code: `<div${$$spreadAttributes(Astro.props,"Astro.props",{"class":"astro-XXXX"})}></div>`,
1341+
},
1342+
},
13121343
{
13131344
name: "Fragment",
13141345
source: `<body><Fragment><div>Default</div><div>Named</div></Fragment></body>`,

internal/transform/scope-html.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,13 @@ var NeverScopedSelectors map[string]bool = map[string]bool{
3939
}
4040

4141
func injectScopedClass(n *astro.Node, opts TransformOptions) {
42+
hasSpreadAttr := false
4243
for i, attr := range n.Attr {
44+
if !hasSpreadAttr && attr.Type == astro.SpreadAttribute {
45+
// We only handle this special case on built-in elements
46+
hasSpreadAttr = !n.Component
47+
}
48+
4349
// If we find an existing class attribute, append the scoped class
4450
if attr.Key == "class" || (n.Component && attr.Key == "className") {
4551
switch attr.Type {
@@ -90,6 +96,11 @@ func injectScopedClass(n *astro.Node, opts TransformOptions) {
9096
}
9197
}
9298
}
99+
// If there's a spread attribute, `class` might be there, so do not inject `class` here
100+
// `class` will be injected by the `spreadAttributes` helper
101+
if hasSpreadAttr {
102+
return
103+
}
93104
// If we didn't find an existing class attribute, let's add one
94105
n.Attr = append(n.Attr, astro.Attribute{
95106
Key: "class",

0 commit comments

Comments
 (0)