Skip to content

Unexpected Script Inlining Behavior With Slotted Scripts, Async Frontmatter, and Templates #13801

@san4d

Description

@san4d

Astro Info

Astro                    v5.7.13
Node                     v20.19.0
System                   macOS (arm64)
Package Manager          pnpm
Output                   server
Adapter                  astro-sst
Integrations             astro-icon
                         htmx
                         posthogJs
                         @astrojs/partytown

If this issue only occurs in one browser, which browser is a problem?

Chrome, Brave, likely all

Describe the Bug

Problem

Consider two Astro components such that the InnerComponent is placed inside OuterComponent using slots. Suppose the InnerComponent has at least one script associated with it.

<!-- OuterComponent.astro -->
<outer-component>
  <h2>OuterComponent</h2>
  <slot />
</outer-component>

<script>
  console.log('OuterComponent connected')
</script>

<!-- InnerComponent.astro -->
<inner-component>
  <h3>InnerComponent {Astro.props.idx}</h3>
</inner-component>

<script>
    console.log('InnerComponent connected')
</script>

The script inlining behavior does not following expected ordering rules in two usage scenarios, both of which involve async execution and templates.

Scenario 1: async string

---
import OuterComponent from '../comps/OuterComponent.astro'
import InnerComponent from '../comps/InnerComponent.astro'

// Any word containing 'a w a i t' (no spaces) as a substring causes the bug to appear.
// Editing the string below, without removing the comment, or deleing the line
// results in the disappearance of the bug:
// await
---
<Layout>
  <OuterComponent>
    <InnerComponent idx={1} />
  </OuterComponent>
  
  <template>
    <InnerComponent idx={2}/>
  </template>
</Layout>

I noticed this scenario first in a more realistic situation: awaiting server data. In the process of debugging, I realized that I could reproduce this bug with only the text async inside a comment.

Scenario 2: Promise block

---
import OuterComponent from '../comps/OuterComponent.astro'
import InnerComponent from '../comps/InnerComponent.astro'
---
<Layout>
  <OuterComponent>
    {Promise.resolve(1).then((i) => <InnerComponent idx={i} />)}
  </OuterComponent>
  
  <template>
    <InnerComponent idx={2}/>
  </template>
</Layout>

The Result (Either Scenario)

The script is inlined within the template tag, instead of after first usage of the component. As a result, the scripts do not run.
Image

Context

I ran into this when using web components in a nested list. Because of this issue my web component never registers, meaning items outside the template have no interactivity. I also noticed the icons inside the impacted component failed to load.

This is related to #13795 in that async behavior appears to cause the inlining to occur in the template tag.

Workaround

The simplest workaround is to leave the async in the frontmatter and load the template scripts into the body once the page has loaded:

const location = window.location.href
document.addEventListener('astro:page-load', () => {
  if (location === window.location.href) {
      const itemTempl = document.getElementById('account-item-template') as HTMLTemplateElement
      const frag = itemTempl.content.cloneNode(true) as DocumentFragment
      frag.querySelectorAll('script').forEach((s) => {
          document.body.appendChild(s)
      })
  }
})

Alternatively, you can refactor your frontmatter to work with promises (removing the async keyword) and then include a hidden instance of your component before the async promise block. In this scenario, the hidden component will provide the inlined script.

Analysis

From what I can tell, the inlining will not occur as expected if all of he following are true:

  • this component is in a slot
  • the string 'await' appears in parent component (frontmatter or html, before or after component usage) or the component is used inside an Promise (ex. somePromise.then((p) => <MyComponent ...p />)
  • the component used in a template

The async in the comment in a frontmatter was very odd behavior. I don't know enough about the project internals, but that makes me think it may be a compiler issue. Paired with the Promise behavior, it seems like the component script inlining behavior is connected to async blocks.

What's the expected result?

The expected behavior is hard to discuss without also discussing #13795. I expect that in the following setups my scripts are inlined such that the component scripts are run at least once.

Await Scenario

---
const data = await serverData()
---
<Layout>
  <OuterComponent />
    <InnerComponent data={data} />
  <OuterComponent />
  <template>
    <InnerComponent />
  </template>
</Layout>

Promise Scenario

---
const dataPromise = serverData()
---
<Layout>
  <OuterComponent />
    {dataPromise.then((data) => <InnerComponent data={data} />)}
  <OuterComponent />
  <template>
    <InnerComponent />
  </template>
</Layout>

Link to Minimal Reproducible Example

https://stackblitz.com/edit/github-x42aukcv?file=src%2Fpages%2Findex.astro

Participation

  • I am willing to submit a pull request for this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    - P3: minor bugAn edge case that only affects very specific usage (priority)

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions