Skip to content

use document.currentScript instead of target option #2221

@Rich-Harris

Description

@Rich-Harris

Describe the problem

This is a super niche problem to have, but it's one I've faced on two recent projects. Currently, SvelteKit knows which bit of markup to hydrate because of tight coupling between the config...

... and the app template:
<div id="svelte">%svelte.body%</div>

The tight coupling is unfortunate in any case, but where this gets really tricky is if you're using SvelteKit to generate markup that will be embedded multiple times on a page, as in this pseudo-code:

// src/app.html wraps the %svelte.head% and %svelte.body% with these tags
const head_start = '<!-- START:HEAD -->';
const head_end = '<!-- END:HEAD -->';
const body_start = '<!-- START:BODY -->';
const body_end = '<!-- END:BODY -->';

// after building app with adapter-static
for (const file of glob('build/**/index.html')) {
  const html = fs.readFileSync(file);

  const head = contents.slice(contents.indexOf(head_start) + head_start.length, contents.indexOf(head_end));
  const body = contents.slice(contents.indexOf(body_start) + body_start.length, contents.indexOf(body_end));

  const slug = file.slice('build/'.length, -'/index.html'.length);
  const metadata = get_metadata(slug);

  await cms.upload({ ...metadata, head, body });
}

The script ends up hydrating the wrong markup unless you use a placeholder selector instead of #svelte and rewrite it to be unique:

-target: '#svelte',
+target: '#PLACEHOLDER',
-<div id="svelte">%svelte.body%</div> 
+<div id="PLACEHOLDER">%svelte.body%</div> 
-const html = fs.readFileSync(file);
+const html = fs.readFileSync(file).replace(/PLACEHOLDER/g, `svelte-${Math.round(Math.random() * 1e9)}`);

That's pretty ugly (and brittle, since there's a non-zero chance of collisions. We're currently using page slugs instead of random numbers, which avoids collisions, but it turns out that sometimes the same embed will be used multiple times in the NYT live blog...), and involves deep knowledge about how SvelteKit works.

Describe the proposed solution

What if <div>%svelte.body%</div> turned into this?

<div>
  <script type="module">
    import { start } from ".../start-81fdb15d.js";
    start({
      target: document.currentScript.parentNode,
      paths: {...},
      // ...
    });
  </script>

  <!-- the server-rendered markup (if using SSR) -->
</div>

Reasons to do this:

  • we're currently polluting the template with an id="svelte" or similar (which is only used for hydration, but you wouldn't necessarily understand that — it looks like something important that you shouldn't muck about with)
  • tight coupling is bad
  • getting rid of config options is good
  • the markup SvelteKit generates is less portable than it should be

Reasons not to do this:

  • the <script> itself will get hydrated away. maybe that's confusing?
  • IE11 doesn't support document.currentScript, so if we intend to support that browser eventually, this would stand in the way. we could continue to support the target option (if unspecified, defaults to document.currentScript.parentNode), but that would be annoying
  • the browser will encounter the import marginally later. in browsers that don't implement modulepreload that could result in a microscopic startup penalty

Alternatives considered

Alternative is to continue using the (perfectly viable, if finicky and annoying) userland solutions.

Importance

nice to have

Additional Information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions