diff --git a/.changeset/shiny-plums-smile.md b/.changeset/shiny-plums-smile.md new file mode 100644 index 000000000..c928011c1 --- /dev/null +++ b/.changeset/shiny-plums-smile.md @@ -0,0 +1,5 @@ +--- +'@astrojs/compiler': minor +--- + +Fixes an edge case that caused `html` and `body` tags with attributes to be ignored when they were wrapped in a component. diff --git a/cmd/astro-wasm/astro-wasm.go b/cmd/astro-wasm/astro-wasm.go index 2930d0ad7..41ed3be0d 100644 --- a/cmd/astro-wasm/astro-wasm.go +++ b/cmd/astro-wasm/astro-wasm.go @@ -232,7 +232,7 @@ func Parse() any { h := handler.NewHandler(source, parseOptions.Filename) var doc *astro.Node - doc, err := astro.ParseWithOptions(strings.NewReader(source), astro.ParseOptionWithHandler(h), astro.ParseOptionEnableLiteral(true)) + doc, err := astro.ParseWithOptions(strings.NewReader(source), astro.ParseOptionEnableLiteral(true), astro.ParseOptionWithHandler(h)) if err != nil { h.AppendError(err) } @@ -256,7 +256,7 @@ func ConvertToTSX() any { h := handler.NewHandler(source, transformOptions.Filename) var doc *astro.Node - doc, err := astro.ParseWithOptions(strings.NewReader(source), astro.ParseOptionWithHandler(h), astro.ParseOptionEnableLiteral(true)) + doc, err := astro.ParseWithOptions(strings.NewReader(source), astro.ParseOptionEnableLiteral(true), astro.ParseOptionWithHandler(h)) if err != nil { h.AppendError(err) } @@ -307,7 +307,7 @@ func Transform() any { } }() - doc, err := astro.ParseWithOptions(strings.NewReader(source), astro.ParseOptionWithHandler(h)) + doc, err := astro.ParseWithOptions(strings.NewReader(source), astro.ParseOptionEnableLiteral(true), astro.ParseOptionWithHandler(h)) if err != nil { reject.Invoke(wasm_utils.ErrorToJSError(h, err)) return diff --git a/internal/parser.go b/internal/parser.go index 18275683a..a413e502c 100644 --- a/internal/parser.go +++ b/internal/parser.go @@ -354,7 +354,7 @@ func (p *parser) addText(text string) { }) } -func (p *parser) addFrontmatter(empty bool) { +func (p *parser) addFrontmatter() { if p.frontmatterState == FrontmatterInitial { if p.doc.FirstChild != nil { p.fm = &Node{ @@ -369,13 +369,8 @@ func (p *parser) addFrontmatter(empty bool) { } p.doc.AppendChild(p.fm) } - if empty { - p.frontmatterState = FrontmatterClosed - p.fm.Attr = append(p.fm.Attr, Attribute{Key: ImplicitNodeMarker, Type: EmptyAttribute}) - } else { - p.frontmatterState = FrontmatterOpen - p.oe = append(p.oe, p.fm) - } + p.frontmatterState = FrontmatterOpen + p.oe = append(p.oe, p.fm) } } @@ -646,9 +641,6 @@ func initialIM(p *parser) bool { p.im = beforeHTMLIM return true } - if p.frontmatterState == FrontmatterInitial { - p.addFrontmatter(true) - } p.quirks = true p.im = beforeHTMLIM return false @@ -1534,9 +1526,6 @@ func inBodyIM(p *parser) bool { } } } - if p.frontmatterState == FrontmatterInitial { - p.addFrontmatter(true) - } return true } @@ -2642,7 +2631,7 @@ func frontmatterIM(p *parser) bool { switch p.tok.Type { case FrontmatterFenceToken: if p.frontmatterState == FrontmatterInitial { - p.addFrontmatter(false) + p.addFrontmatter() return true } else { p.frontmatterState = FrontmatterClosed diff --git a/internal/printer/printer_test.go b/internal/printer/printer_test.go index 938e3c4e8..fc34006c0 100644 --- a/internal/printer/printer_test.go +++ b/internal/printer/printer_test.go @@ -259,7 +259,7 @@ func TestPrinter(t *testing.T) { }, { name: "slot with fallback", - source: `

Hello world!

`, + source: `

Hello world!

`, want: want{ code: `${$$maybeRenderHead($$result)}${$$renderSlot($$result,$$slots["default"],$$render` + BACKTICK + `

Hello world!

` + BACKTICK + `)}`, }, @@ -568,9 +568,9 @@ import type data from "test"

div+h2 ${dummyKey}

-

p+h2 ${dummyKey}

- ` + BACKTICK + ` - ); +

p+h2 ${dummyKey}

+

` + BACKTICK + ` + ); }) } @@ -588,8 +588,8 @@ import type data from "test" want: want{ code: ` ${$$maybeRenderHead($$result)} -${Object.keys(importedAuthors).map(author => $$render` + BACKTICK + `

hello
` + BACKTICK + `)} -${Object.keys(importedAuthors).map(author => $$render` + BACKTICK + `

${author}
` + BACKTICK + `)} +${Object.keys(importedAuthors).map(author => $$render` + BACKTICK + `

hello

` + BACKTICK + `)} +${Object.keys(importedAuthors).map(author => $$render` + BACKTICK + `

${author}

` + BACKTICK + `)} `, }, @@ -1602,6 +1602,13 @@ const name = 'named'; code: "${cond && $$render``}${cond && $$render`My title`}", }, }, + { + name: "top-level component does not drop body attributes", + source: ``, + want: want{ + code: "${$$renderComponent($$result,'Base',Base,{},{\"default\": () => $$render`${$$maybeRenderHead($$result)}${$$renderSlot($$result,$$slots[\"default\"])}`,})}", + }, + }, { name: "custom elements", source: `--- @@ -1661,7 +1668,7 @@ ${$$renderComponent($$result,'my-element','my-element',{"client:load":true,"clie }, { name: "Self-closing script in head works", - source: `` + RENDER_HEAD_RESULT + ``, }, @@ -1682,7 +1689,7 @@ ${$$renderComponent($$result,'my-element','my-element',{"client:load":true,"clie }, { name: "Self-closing components in head can have siblings", - source: ``, + source: ``, want: want{ code: `${$$renderComponent($$result,'BaseHead',BaseHead,{})}` + RENDER_HEAD_RESULT + ``, }, @@ -2082,7 +2089,7 @@ import { Container, Col, Row } from 'react-bootstrap'; name: "Preserve namespaces in expressions", source: ``, want: want{ - code: `${$$maybeRenderHead($$result)}`, + code: `${$$maybeRenderHead($$result)}`, }, }, { @@ -2297,6 +2304,42 @@ const content = "lol"; `, }, }, + { + // ensurethere are no duplicate elements matching the ones in the link below (`` in this test) + // https://github.com/withastro/compiler/blob/a90d99ee8cc3ad92d1b39d73df1f7301011ee970/internal/parser.go#L1490 + name: " tag with expression in table ", + source: `
+ + + + +
{linkURL}
+
`, + want: want{ + code: `${$$maybeRenderHead($$result)}
+ + + + +
${linkURL}
+
`, + }, + }, + { + // makes sure that there are no duplicate elements matching the ones in the link below (`
` in this test) + // https://github.com/withastro/compiler/blob/a90d99ee8cc3ad92d1b39d73df1f7301011ee970/internal/parser.go#L1490 + name: " tag with expression in template", + source: ` +

This should not be a link

`, + want: want{ + code: ` +

This should not be a link

`, + }, + }, { name: "complex table", source: ` @@ -3444,7 +3487,7 @@ const items = ["Dog", "Cat", "Platipus"]; filename: "/projects/app/src/pages/page.astro", transitions: true, want: want{ - code: `${$$maybeRenderHead($$result)}`, + code: `${$$maybeRenderHead($$result)}`, }, }, { @@ -3453,7 +3496,7 @@ const items = ["Dog", "Cat", "Platipus"]; filename: "/projects/app/src/pages/page.astro", transitions: true, want: want{ - code: `${$$maybeRenderHead($$result)}`, + code: `${$$maybeRenderHead($$result)}`, }, }, { @@ -3462,7 +3505,7 @@ const items = ["Dog", "Cat", "Platipus"]; filename: "/projects/app/src/pages/page.astro", transitions: true, want: want{ - code: `${$$maybeRenderHead($$result)}`, + code: `${$$maybeRenderHead($$result)}`, }, }, { @@ -3471,7 +3514,7 @@ const items = ["Dog", "Cat", "Platipus"]; filename: "/projects/app/src/pages/page.astro", transitions: true, want: want{ - code: `${$$renderComponent($$result,'Component',Component,{"class":"bar","data-astro-transition-scope":($$renderTransition($$result, "wkm5vset", "morph", ""))})}`, + code: `${$$renderComponent($$result,'Component',Component,{"class":"bar","data-astro-transition-scope":($$renderTransition($$result, "byigm4lx", "morph", ""))})}`, }, }, { @@ -3479,7 +3522,7 @@ const items = ["Dog", "Cat", "Platipus"]; source: `
`, transitions: true, want: want{ - code: `${$$maybeRenderHead($$result)}`, + code: `${$$maybeRenderHead($$result)}`, }, }, { @@ -3487,7 +3530,7 @@ const items = ["Dog", "Cat", "Platipus"]; source: `
`, transitions: true, want: want{ - code: `${$$maybeRenderHead($$result)}
`, + code: `${$$maybeRenderHead($$result)}
`, }, }, { @@ -3495,7 +3538,7 @@ const items = ["Dog", "Cat", "Platipus"]; source: ``, transitions: true, want: want{ - code: `${$$renderComponent($$result,'my-island','my-island',{"data-astro-transition-persist-props":"false","data-astro-transition-persist":($$createTransitionScope($$result, "otghnj5u"))})}`, + code: `${$$renderComponent($$result,'my-island','my-island',{"data-astro-transition-persist-props":"false","data-astro-transition-persist":($$createTransitionScope($$result, "rho3aldc"))})}`, }, }, { @@ -3505,6 +3548,31 @@ const items = ["Dog", "Cat", "Platipus"]; code: `${$$renderComponent($$result,'Component',Component,{})}${(void 0)}`, }, }, + { + name: "nested head content stays in the head", + source: `--- +const meta = { title: 'My App' }; +--- + + + + + + { + meta && {meta.title} + } + + + + +

My App

+ +`, + want: want{ + frontmatter: []string{"", `const meta = { title: 'My App' };`}, + code: ` ${ meta && $$render` + BACKTICK + `${meta.title}` + BACKTICK + ` } ${$$renderHead($$result)}

My App

`, + }, + }, } for _, tt := range tests { @@ -3520,8 +3588,8 @@ const items = ["Dog", "Cat", "Platipus"]; // transform output from source code := test_utils.Dedent(tt.source) - doc, err := astro.Parse(strings.NewReader(code)) h := handler.NewHandler(code, "") + doc, err := astro.ParseWithOptions(strings.NewReader(code), astro.ParseOptionEnableLiteral(true), astro.ParseOptionWithHandler(h)) if err != nil { t.Error(err) @@ -3535,6 +3603,7 @@ const items = ["Dog", "Cat", "Platipus"]; RenderScript: tt.transformOptions.RenderScript, } transform.Transform(doc, transformOptions, h) // note: we want to test Transform in context here, but more advanced cases could be tested separately + result := PrintToJS(code, doc, 0, transform.TransformOptions{ Scope: "XXXX", InternalURL: "http://localhost:3000/", @@ -3746,6 +3815,11 @@ const c = '\'' source: `

Hello world!

`, want: []ASTNode{{Type: "element", Name: "style"}, {Type: "element", Name: "html", Children: []ASTNode{{Type: "element", Name: "body", Children: []ASTNode{{Type: "element", Name: "h1", Children: []ASTNode{{Type: "text", Value: "Hello world!"}}}}}}}}, }, + { + name: "empty style", + source: ``, + want: []ASTNode{{Type: "element", Name: "style", Attributes: []ASTNode{{Type: "attribute", Kind: "expression", Name: "define:vars", Value: "{ color: \"Gainsboro\" }"}}}}, + }, { name: "style after html", source: `

Hello world!

`, @@ -3776,6 +3850,11 @@ const c = '\'' source: `
`, want: []ASTNode{{Type: "element", Name: "main", Attributes: []ASTNode{{Type: "attribute", Kind: "template-literal", Name: "id", Value: "gotcha", Raw: "`gotcha"}}}}, }, + { + name: "top-level component does not drop body attributes", + source: ``, + want: []ASTNode{{Type: "component", Name: "Base", Attributes: []ASTNode{}, Children: []ASTNode{{Type: "element", Name: "body", Attributes: []ASTNode{{Type: "attribute", Kind: "quoted", Name: "class", Value: "foobar", Raw: "\"foobar\""}}, Children: []ASTNode{{Type: "element", Name: "slot"}}}}}}, + }, } for _, tt := range tests { diff --git a/internal/token_test.go b/internal/token_test.go index 39992f6b9..8ab374fff 100644 --- a/internal/token_test.go +++ b/internal/token_test.go @@ -500,6 +500,11 @@ func TestBasic(t *testing.T) { `

Hello world!

`, + []TokenType{StartTagToken, StartTagToken, StartTagToken, TextToken, EndTagToken, EndTagToken, EndTagToken}, + }, } runTokenTypeTest(t, Basic) diff --git a/internal/transform/transform.go b/internal/transform/transform.go index f83f554df..c0030d1f5 100644 --- a/internal/transform/transform.go +++ b/internal/transform/transform.go @@ -242,7 +242,7 @@ func TrimTrailingSpace(doc *astro.Node) { return } - if doc.LastChild.Type == astro.TextNode { + if doc.LastChild.Type == astro.TextNode && len(doc.LastChild.Data) < len(strings.TrimRightFunc(doc.LastChild.Data, unicode.IsSpace)) { doc.LastChild.Data = strings.TrimRightFunc(doc.LastChild.Data, unicode.IsSpace) return } @@ -254,7 +254,6 @@ func TrimTrailingSpace(doc *astro.Node) { n = n.LastChild continue } else { - n = nil break } } @@ -262,7 +261,11 @@ func TrimTrailingSpace(doc *astro.Node) { // Collapse all trailing text nodes for n != nil && n.Type == astro.TextNode { n.Data = strings.TrimRightFunc(n.Data, unicode.IsSpace) - n = n.PrevSibling + if len(n.Data) > 0 { + break + } else { + n = n.PrevSibling + } } } diff --git a/internal/transform/transform_test.go b/internal/transform/transform_test.go index a52dc11b6..2eaf49e41 100644 --- a/internal/transform/transform_test.go +++ b/internal/transform/transform_test.go @@ -253,13 +253,13 @@ func TestFullTransform(t *testing.T) { }, { name: "Component before html I", - source: `

Astro

`, - want: `

Astro

`, + source: `

Astro

`, + want: `

Astro

`, }, { name: "Component before html II", source: ``, - want: ``, + want: ``, }, { name: "respects explicitly authored elements", @@ -281,6 +281,11 @@ func TestFullTransform(t *testing.T) { source: ``, want: ``, }, + { + name: "top-level component does not drop body attributes", + source: ``, + want: ``, + }, { name: "works with nested components", source: `
`, @@ -302,7 +307,7 @@ func TestFullTransform(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b.Reset() - doc, err := astro.Parse(strings.NewReader(tt.source)) + doc, err := astro.ParseWithOptions(strings.NewReader(tt.source), astro.ParseOptionEnableLiteral(true)) if err != nil { t.Error(err) } @@ -513,11 +518,12 @@ func TestAnnotation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b.Reset() - doc, err := astro.Parse(strings.NewReader(tt.source)) + + h := handler.NewHandler(tt.source, "/src/pages/index.astro") + doc, err := astro.ParseWithOptions(strings.NewReader(tt.source), astro.ParseOptionEnableLiteral(true), astro.ParseOptionWithHandler(h)) if err != nil { t.Error(err) } - h := handler.NewHandler(tt.source, "/src/pages/index.astro") Transform(doc, TransformOptions{ AnnotateSourceFile: true, Filename: "/src/pages/index.astro", diff --git a/packages/compiler/test/basic/body-in-component.ts b/packages/compiler/test/basic/body-in-component.ts new file mode 100644 index 000000000..5776a1c8e --- /dev/null +++ b/packages/compiler/test/basic/body-in-component.ts @@ -0,0 +1,24 @@ +import { test } from 'uvu'; +import * as assert from 'uvu/assert'; +import { transform } from '@astrojs/compiler'; + +const FIXTURE = ` +--- +let value = 'world'; +--- + + +`; + +let result; +test.before(async () => { + result = await transform(FIXTURE); +}); + +test('top-level component does not drop body attributes', () => { + console.log(result.code); + assert.match(result.code, "${$$renderComponent($$result,'Base',Base,{},{\"default\": () => $$render`${$$maybeRenderHead($$result)}${$$renderSlot($$result,$$slots[\"default\"])}`,})}", `Expected body to be included!`); +}); + + +test.run(); diff --git a/packages/compiler/test/basic/trailing-spaces-ii.ts b/packages/compiler/test/basic/trailing-spaces-ii.ts index 198626215..44277dcb2 100644 --- a/packages/compiler/test/basic/trailing-spaces-ii.ts +++ b/packages/compiler/test/basic/trailing-spaces-ii.ts @@ -23,7 +23,7 @@ test.before(async () => { }); test('trailing space', () => { - assert.ok(result.code, 'Expected to compiler'); + assert.ok(result.code, 'Expected to compile'); assert.match( result.code, ` diff --git a/packages/compiler/test/tsx/basic.ts b/packages/compiler/test/tsx/basic.ts index e5f705f6d..d2e7d80b2 100644 --- a/packages/compiler/test/tsx/basic.ts +++ b/packages/compiler/test/tsx/basic.ts @@ -267,8 +267,8 @@ test('return ranges - no frontmatter', async () => { assert.equal(metaRanges, { frontmatter: { - start: 30, - end: 30, + start: 0, + end: 0, }, body: { start: 41,