sveltejs / kit

web development, streamlined
https://svelte.dev/docs/kit
MIT License
18.73k stars 1.95k forks source link

Components that require an external top level await during compilation fail in svelte 5 #13015

Open dsgallups opened 2 days ago

dsgallups commented 2 days ago

Describe the bug

On import of any function binding from WebAssembly will return an error not found when running a vite development build, due to usage of vite-plugin-top-level-await. This is a Sveltekit issue that appears upon usage of Svelte 5 over Svelte 4.

Discovered behavior using the cloudflare adapter. svelte-bug-wasm-2 reproduces the behavior with the auto adapter.

Uses: wasm-pack: 0.13.1, vite-plugin-wasm: 3.3.0 for wasm bindings

Reproduction

https://github.com/dsgallups/svelte-bug-wasm https://github.com/dsgallups/svelte-bug-wasm-2

Branch with working compilation on svelte 4: https://github.com/dsgallups/svelte-bug-wasm-2/tree/svelte-4-working

Logs

TypeError: Pyramid_0 is not a function
    at Root (file:///Users/danielgallups/repo/svelte-bug-wasm-2/.svelte-kit/output/server/chunks/internal.js:510:5)
    at render (file:///Users/danielgallups/repo/svelte-bug-wasm-2/.svelte-kit/output/server/chunks/index3.js:812:3)
    at Function._render [as render] (file:///Users/danielgallups/repo/svelte-bug-wasm-2/.svelte-kit/output/server/chunks/internal.js:472:20)
    at render_response (file:///Users/danielgallups/repo/svelte-bug-wasm-2/.svelte-kit/output/server/index.js:1238:34)
    at async render_page (file:///Users/danielgallups/repo/svelte-bug-wasm-2/.svelte-kit/output/server/index.js:2066:12)
    at async resolve2 (file:///Users/danielgallups/repo/svelte-bug-wasm-2/.svelte-kit/output/server/index.js:2700:24)
    at async respond (file:///Users/danielgallups/repo/svelte-bug-wasm-2/.svelte-kit/output/server/index.js:2592:22)
    at async visit (file:///Users/danielgallups/repo/svelte-bug-wasm-2/node_modules/@sveltejs/kit/src/core/postbuild/prerender.js:208:20)

node:internal/event_target:1094
  process.nextTick(() => { throw err; });
                           ^
Error: 500 /
To suppress or handle this error, implement `handleHttpError` in https://svelte.dev/docs/kit/configuration#prerender
    at file:///Users/danielgallups/repo/svelte-bug-wasm-2/node_modules/@sveltejs/kit/src/core/config/options.js:203:13
    at file:///Users/danielgallups/repo/svelte-bug-wasm-2/node_modules/@sveltejs/kit/src/core/postbuild/prerender.js:71:25
    at save (file:///Users/danielgallups/repo/svelte-bug-wasm-2/node_modules/@sveltejs/kit/src/core/postbuild/prerender.js:419:4)
    at visit (file:///Users/danielgallups/repo/svelte-bug-wasm-2/node_modules/@sveltejs/kit/src/core/postbuild/prerender.js:242:3)
Emitted 'error' event on Worker instance at:
    at [kOnErrorMessage] (node:internal/worker:326:10)
    at [kOnMessage] (node:internal/worker:337:37)
    at MessagePort.<anonymous> (node:internal/worker:232:57)
    at [nodejs.internal.kHybridDispatch] (node:internal/event_target:820:20)
    at MessagePort.<anonymous> (node:internal/per_context/messageport:23:28)

Node.js v20.15.0

System Info

System:
    OS: macOS 15.1
    CPU: (16) arm64 Apple M3 Max
    Memory: 22.27 GB / 48.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 20.15.0 - ~/.nvm/versions/node/v20.15.0/bin/node
    npm: 10.7.0 - ~/.nvm/versions/node/v20.15.0/bin/npm
  Browsers:
    Chrome: 130.0.6723.117
    Chrome Canary: 133.0.6839.1
    Safari: 18.1
  npmPackages:
    svelte: 5.2 => 5.2.0
    vite: ^5.4.10 => 5.4.11

Severity

blocking an upgrade

Additional Information

In .svelte-kit/output/server/entries/pages/_layout.svelte.js the following code is generated in svelte 4:

import { c as create_ssr_component } from "../../chunks/ssr.js";
import { e as escape } from "../../chunks/escape.js";
let Layout;
let __tla = (async () => {
   ...vite-plugin-wasm instantiation code...
 Layout = create_ssr_component(($$result, $$props, $$bindings, slots) => {
    let mountState = "mounting";
    return `<div class="app"><main><div>${escape(mountState)}</div> <div>${slots.default ? slots.default({}) : ``}</div></main></div>`;
  });
})();
export {
  __tla,
  Layout as default
};

Now, svelte-5 will generate:

import { e as escape_html } from "../../chunks/escaping.js";
import { S as pop, Q as push } from "../../chunks/index.js";
let _layout;
let __tla = (async () => {
   ...vite-plugin-wasm instantiation code...
  _layout = function($$payload, $$props) {
    push();
    let { children } = $$props;
    let mountState = "mounting";
    $$payload.out += `<div class="app"><main><div>${escape_html(mountState)}</div> <div>`;
    children($$payload);
    $$payload.out += `<!----></div></main></div>`;
    pop();
  };
})();
export {
  __tla,
  _layout as default
};

The issue is that __tla doesn't complete before _layout is called by render:

_layout is initially undefined. If called before __tla resolves, component (_layout) in 0.js will return undefined leading all the way to Pyramid[0] being the component constructor for the layout page to be undefined. This is why it would sometimes work, because something would eventually process the topLevelAwait.

Why does this work in Svelte 4?

in internal.js the Root function will validate that constructors[0] and constructors[1] have been properly instantiated using validate_component in ssr.js. Svelte 4 has a fallback for this, so things work:

$$rendered = `  ${constructors[1] ? `${validate_component(constructors[0] || missing_component, "svelte:component").$$render(
      $$result,
      { data: data_0, this: components[0] },
      {
        this: ($$value) => {
          components[0] = $$value;
          $$settled = false;
        }
      },
      {
        default: () => {
          return `${validate_component(constructors[1] || missing_component, "svelte:component").$$render(
            $$result,
            { data: data_1, form, this: components[1] },
            {
              this: ($$value) => {
                components[1] = $$value;
                $$settled = false;
              }
            },
            {}
          )}`;
        }
      }
    )}` : `${validate_component(constructors[0] || missing_component, "svelte:component").$$render(
      $$result,
      { data: data_0, form, this: components[0] },
      {
        this: ($$value) => {
          components[0] = $$value;
          $$settled = false;
        }
      },
      {}
    )}`} ${``}`;
  } while (!$$settled);

Compared to the Root function from root.svelte in .svelte-kit/output/server/chunks/internal.js:~516:

  const Pyramid_1 = constructors[1];
  if (constructors[1]) {
    $$payload.out += "<!--[-->";
    const Pyramid_0 = constructors[0];  // <-- There is no guarantee that constructors[0] exists.
    $$payload.out += `<!---->`;
    Pyramid_0($$payload, {
      data: data_0,
      form,
      children: ($$payload2) => {
        $$payload2.out += `<!---->`;
        Pyramid_1($$payload2, { data: data_1, form });
        $$payload2.out += `<!---->`;
      },
      $$slots: { default: true }
    });
    $$payload.out += `<!---->`;
  } else {
    $$payload.out += "<!--[!-->";
    const Pyramid_0 = constructors[0];
    $$payload.out += `<!---->`;
    Pyramid_0($$payload, { data: data_0, form });
    $$payload.out += `<!---->`;
  }

Since these constructors are not validated in Svelte 5, that leads to the error.

This is why I believe wasm won't work: Top level await does not set the component before the component's render() function is called.

Why is this Important?

Because the ES Module Integration proposal for WASM is not supported, the vite docs recommend usage of the vite-plugin-wasm dep. Since Svelte cannot properly compile components that utilize this plugin, the wasm-pack toolchain becomes effectively useless more difficult to use. Examples of a workaround are not available A half-workaround is to use a dynamic import. There are some cases, however, where wasm memory should be loaded alongside the constructor outside of an onMount callback.

Idea for solution

I'd like to readd the validate_component/fallback functionality back to sveltekit in some form. However, I have no clue why it was removed. My hypothesis is that the simplest approach is to adjust write_root with an if check on constructor[0], but not sure what a reasonable fallback would be. Some guidance on this would be super helpful!

dsgallups commented 2 days ago

This is quite an unusual bug. I noticed that in the output you will find

<!-- This file is generated by @sveltejs/kit — do not edit it! -->
<svelte:options runes={true} />
<script>
    import { setContext, onMount, tick } from 'svelte';
    import { browser } from '$app/environment';

    // stores
...

I have provided this output to the example.

It's actually all over the place, sometimes working when revisiting the page during preview of prod build, but not during others. Fails completely when hosted by a cloudflare worker. Scratching my head on this one.

Edit: The first example randomly just started working, so I have posted a new example that should be more dependable. This uses adapter-auto and fails during the prerendering phase. Notably, the wasm generated files + executable are missing from the compilation output.

eltigerchino commented 23 hours ago

I'm not able to reproduce the same error logs when running build on the reproduction. Can you provide another minimal reproduction and a set of steps to run? These are the build logs I get:

x Build failed in 1.16s
error during build:
[vite:load-fallback] Could not load /home/chewteeming/github/svelte-bug-wasm-2/src/lib/wasm/pkg/frontend_wasm (imported by src/routes/+layout.svelte): ENOENT: no such file or directory, open '/home/chewteeming/github/svelte-bug-wasm-2/src/lib/wasm/pkg/frontend_wasm'
Error: Could not load /home/chewteeming/github/svelte-bug-wasm-2/src/lib/wasm/pkg/frontend_wasm (imported by src/routes/+layout.svelte): ENOENT: no such file or directory, open '/home/chewteeming/github/svelte-bug-wasm-2/src/lib/wasm/pkg/frontend_wasm'
 ELIFECYCLE  Command failed with exit code 1.
dsgallups commented 16 hours ago

I'm not able to reproduce the same error logs when running build on the reproduction. Can you provide another minimal reproduction and a set of steps to run?

Absolutely! Apologies, had not added the README.md from the first example. For compilation, you'll want to install rust, but what I'll do is upload the wasm pkg file for this

dsgallups commented 16 hours ago

Alright, should be available now. You should be able to reproduce this error just by running the build command (no rust compilation required). Thank you!

Edit:

I have also updated the readme with steps if you would like to compile the wasm binary directly. This requires the rust toolchain and the wasm host target. This can be added by running rustup target add wasm32-unknown-unknown after installing the toolchain. Finally, run npm run build-wasm, which will compile your binary + bindings out to $lib/pkg/.

When running dev mode, it should run correctly.

When using adapter-auto, it seems like it will prerender the page, causing compilation to fail during the build step. using a host adapter, like cloudflare, it will succeed in compilation, but will render a 500 server error on page access on the first request. On occasion, the page may improperly load after the initial load, but not when running on cloudflare workers. I'm unsure about the other hosts, but will be doing more research today.

dsgallups commented 9 hours ago

FOUND IT!! Did a lot of digging.

Edit: moved most of this comment to issue details

dsgallups commented 3 hours ago

Last time I'm changing the title, wanted to make it more concise.