withastro/astro

web components (custom-element) support: Conversion from .astro component to createComponent break when an anchor tag is introduced

schalkneethling opened this issue · 2 comments

Astro Info

I ran into an interesting bug (I believe it to be one) when using web components with Astro. After reducing my test case I can narrow it down to be related to custom elements.

Astro                    v5.0.3
Node                     v22.8.0
System                   macOS (arm64)
Package Manager          npm
Output                   static
Adapter                  none
Integrations             none

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

No response

Describe the Bug

After reducing the test case to the following:

<ul class="card-list">
	{
	users.length &&
		users.map((user: User) => (
		<li>
			<article class="user-card">
			<h2 class="user-card-title">
				{user.firstName} {user.lastName}
			</h2>

			<img
				class="user-card-avatar"
				src={user.avatarURL}
				height="150"
				width="150"
				alt=""
			/>

			<span class="user-card-role">{user.role}</span>

			<span class="user-card-email">
				<span class="visually-hidden">Email:</span>
				<a href={`mailto:${user.email}`}>{user.email}</a>
			</span>
			</article>
		</li>
		))
	}
</ul>

The build works without any trouble. When I wrap this in a template element as follows, everything is still A-OK:

<template shadowrootmode="open">
  <style set:html={style}></style>
  <ul class="card-list">
    {
      users.length &&
        users.map((user: User) => (
          <li>
            // same as above - omitting for brevity
          </li>
        ))
    }
  </ul>
</template>

If I know add my wrapper custom element:

<nimbus-team>
  <template shadowrootmode="open">
    <style set:html={style}></style>
    <ul class="card-list">
      {
        users.length &&
          users.map((user: User) => (
            <li>
              // same as above - omitting for brevity
            </li>
          ))
      }
    </ul>
  </template>
</nimbus-team>

The build fails with:

Stack Trace
20:02:31 ▶ src/pages/index.astro
20:02:31   └─ /index.htmlfile:///Users/schalkneethling/dev/opensource/neo-shadow/dist/pages/index.astro.mjs?time=1733680951649:18
  return renderTemplate`${renderComponent($$result, "nimbus-team", "nimbus-team", {}, { "default": () => renderTemplate` <template shadowrootmode="open"> <style>${unescapeHTML(style)}</style> ${maybeRenderHead()}<ul class="card-list"> ${users.length && users.map((user2) => renderTemplate`<li> <article class="user-card"> <h2 class="user-card-title"> ${user2.firstName} ${user2.lastName} </h2> <img class="user-card-avatar"${addAttribute(user2.avatarURL, "src")} height="150" width="150" alt=""> <span class="user-card-role">${user2.role}</span> <span class="user-card-email"> <span class="visually-hidden">Email:</span> <a${addAttribute(`mailto:${user2.email}`, "href")}>${user2.email}</a> </span> </article> </li>`)} </ul> </template><a${addAttribute(`mailto:${user.email}`, "href")}></a>` })}<a${addAttribute(`mailto:${user.email}`, "href")}> ${renderScript($$result, "/Users/schalkneethling/dev/opensource/neo-shadow/src/components/NimbusTeam.astro?astro&type=script&index=0&lang.ts")}</a>`;
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           ^

ReferenceError: user is not defined
    at default (file:///Users/schalkneethling/dev/opensource/neo-shadow/dist/pages/index.astro.mjs?time=1733680951649:18:764)
    at Object.render (file:///Users/schalkneethling/dev/opensource/neo-shadow/dist/chunks/astro/server_57EaB_XO.mjs:788:70)
    at renderSlotToString (file:///Users/schalkneethling/dev/opensource/neo-shadow/dist/chunks/astro/server_57EaB_XO.mjs:815:24)
    at file:///Users/schalkneethling/dev/opensource/neo-shadow/dist/chunks/astro/server_57EaB_XO.mjs:824:27
    at Array.map (<anonymous>)
    at renderSlots (file:///Users/schalkneethling/dev/opensource/neo-shadow/dist/chunks/astro/server_57EaB_XO.mjs:823:29)
    at renderFrameworkComponent (file:///Users/schalkneethling/dev/opensource/neo-shadow/dist/chunks/astro/server_57EaB_XO.mjs:1228:48)
    at renderComponent (file:///Users/schalkneethling/dev/opensource/neo-shadow/dist/chunks/astro/server_57EaB_XO.mjs:1512:16)
    at file:///Users/schalkneethling/dev/opensource/neo-shadow/dist/pages/index.astro.mjs?time=1733680951649:18:27
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)

Node.js v22.8.0

After referring to the generated intermediary dist/pages/index.astro.mjs one can see that the conversion from the Astro component to JS is going awry:

const $$NimbusTeam = createComponent(async ($$result, $$props, $$slots) => {
  const response = await fetch(
    "https://fictionalfolks.netlify.app/.netlify/functions/users?count=2"
  );
  let users;
  if (response.ok) {
    users = await response.json();
  }
  return renderTemplate`${renderComponent($$result, "nimbus-team", "nimbus-team", {}, { "default": () => renderTemplate` <template shadowrootmode="open"> <style>${unescapeHTML(style)}</style> ${maybeRenderHead()}<ul class="card-list"> ${users.length && users.map((user2) => renderTemplate`<li> <article class="user-card"> <h2 class="user-card-title"> ${user2.firstName} ${user2.lastName} </h2> <img class="user-card-avatar"${addAttribute(user2.avatarURL, "src")} height="150" width="150" alt=""> <span class="user-card-role">${user2.role}</span> <span class="user-card-email"> <span class="visually-hidden">Email:</span> <a${addAttribute(`mailto:${user2.email}`, "href")}>${user2.email}</a> </span> </article> </li>`)} </ul> </template><a${addAttribute(`mailto:${user.email}`, "href")}></a>` })}<a${addAttribute(`mailto:${user.email}`, "href")}> ${renderScript($$result, "/Users/schalkneethling/dev/opensource/neo-shadow/src/components/NimbusTeam.astro?astro&type=script&index=0&lang.ts")}</a>`;
}, "/Users/schalkneethling/dev/opensource/neo-shadow/src/components/NimbusTeam.astro", void 0);

A few standout items for me:

return renderTemplate`${renderComponent($$result, "nimbus-team", "nimbus-team", {}, { "default": () =>

nimbus-team is repeated twice in the signature. Having not looked at the signature, it could be that this is as expected, but it reads a little curious to me.

${users.length && users.map((user2) => renderTemplate

The use of user2 here instead of user as defined in the Astro component seems odd and is the cause of the user is undefined error a bit later in this snippet.

<a${addAttribute(`mailto:${user2.email}`, "href")}>${user2.email}</a> </span> </article> </li>`)} </ul> </template>

The renderTemplate function call seems to end abruptly after the last closing list item even though it starts from <template shadowrootmode="open">.

The anchor element <a${addAttribute(mailto:${user2.email}, "href")}>${user2.email}</a> from a little earlier in the snippet is replicated twice more and in fact, the last of these wraps the script element.

<a${addAttribute(`mailto:${user.email}`, "href")}></a>` })}<a${addAttribute(`mailto:${user.email}`, "href")}> ${renderScript($$result, "/Users/schalkneethling/dev/opensource/neo-shadow/src/components/NimbusTeam.astro?astro&type=script&index=0&lang.ts")}</a>

What is also curious here is that:

  1. These are outside of the template _and
  2. the content ${user2.email} of the anchor element is removed.

This is truly peculiar and I would be happy to assist further in tracking down and fixing whatever is causing this. You can see the full Astro component at the following URL:

https://github.com/schalkneethling/neo-shadow/blob/main/src/components/NimbusTeam.astro

What's the expected result?

The build succeeds and produces the expected HTML.

Link to Minimal Reproducible Example

https://github.com/schalkneethling/neo-shadow/blob/main/src/components/NimbusTeam.astro

Participation

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

BTW, if I do not use declarative ShadowDOM and do not nest the custom element and template element I still get the error, but the stack trace is less detailed:

Astro

<nimbus-team></nimbus-team>

<template>
  <style set:html={style}></style>
  <ul class="card-list">
    {
      users.length &&
        users.map((user: User) => (
          <li>
            <article class="user-card">
              <h2 class="user-card-title">
                {user.firstName} {user.lastName}
              </h2>

              <img
                class="user-card-avatar"
                src={user.avatarURL}
                height="150"
                width="150"
                alt=""
              />

              <span class="user-card-role">{user.role}</span>

              <span class="user-card-email">
                <span class="visually-hidden">Email:</span>
                <a href={`mailto:${user.email}`}>{user.email}</a>
              </span>
            </article>
          </li>
        ))
    }
  </ul>
</template>

Stack Trace

20:18:09 ▶ src/pages/index.astro
20:18:09   └─ /index.htmluser is not defined
  Stack trace:
    at file:///Users/schalkneethling/dev/opensource/neo-shadow/dist/pages/index.astro.mjs?time=1733681889426:18:708

createComponent for the $$NimbusTeam Astro component

const $$NimbusTeam = createComponent(async ($$result, $$props, $$slots) => {
  const response = await fetch(
    "https://fictionalfolks.netlify.app/.netlify/functions/users?count=2"
  );
  let users;
  if (response.ok) {
    users = await response.json();
  }
  return renderTemplate`${renderComponent($$result, "nimbus-team", "nimbus-team", {})} <template> <style>${unescapeHTML(style)}</style> ${maybeRenderHead()}<ul class="card-list"> ${users.length && users.map((user2) => renderTemplate`<li> <article class="user-card"> <h2 class="user-card-title"> ${user2.firstName} ${user2.lastName} </h2> <img class="user-card-avatar"${addAttribute(user2.avatarURL, "src")} height="150" width="150" alt=""> <span class="user-card-role">${user2.role}</span> <span class="user-card-email"> <span class="visually-hidden">Email:</span> <a${addAttribute(`mailto:${user2.email}`, "href")}>${user2.email}</a> </span> </article> </li>`)} </ul> </template><a${addAttribute(`mailto:${user.email}`, "href")}> ${renderScript($$result, "/Users/schalkneethling/dev/opensource/neo-shadow/src/components/NimbusTeam.astro?astro&type=script&index=0&lang.ts")}</a>`;
}, "/Users/schalkneethling/dev/opensource/neo-shadow/src/components/NimbusTeam.astro", void 0);

UPDATE: I was able to "fix" this by changing the data structure and looping over the social platforms to create the list items. I have "fix" in quotations because I am still unsure why having the list items hard-coded inside the template inside the custom element trips up the (I think) compiler so badly. It is most likely still worth investigating (happy to help).

Here is the part that changed:

<ul class="user-card-social">
  {Object.entries(user.social).map(([key, value]) => (
    <li>
      <a
        class={`icon icon-social-${key}`}
        href={value.url}
        target="_blank"
        rel="noopener noreferrer"
      >
        <span class="visually-hidden">
          Follow {user.firstName} on {value.name}
        </span>
      </a>
    </li>
  ))}
</ul>

The entire component can be reviewed here and the commit with the change(s) can be reviewed here.