sveltejs / svelte

web development for the rest of us
https://svelte.dev
MIT License
79.96k stars 4.25k forks source link

Migrating Remotely Bundled Component setup from Svelte 4 to 5 #14293

Closed LazerJesus closed 2 days ago

LazerJesus commented 2 days ago

Describe the bug

I have a server on port 8000 that bundles and hosts svelte components, which are imported into a sveltekit app running on port 5173. This setup used to work fine on svelte 4, but the migration isnt as straight forward as i'd hoped. Let me show first how the old setup used to work server and client side, and then what ive tried so far.

The server had a esbuild bundler, which had its outputFiles hosted on a http get request.

export default async function bundle (entry) {
  const build = await esbuild.build({
    entryPoints: [entry],
    mainFields: ["svelte", "browser", "module", "main"],
    conditions: ["svelte", "browser"],
    target: "es6",
    format: "esm",
    write: false,
    treeShaking: true,
    sourcemap: config.isDev ? "inline" : false,
    minify: true,
    bundle: true,
    outdir: dirname(entry),
    outExtension: { ".js": ".svelte" },
    plugins: [
      cache(svelteImportMap),
      sveltePlugin({ },
        compilerOptions: {
          filename: basename(entry),
          css: "injected",
        },
      }),
    ],
  });
  return build.outputFiles;
}
const svelte = "https://esm.sh/svelte@4.2.18";

const svelteImportMap = {
  importmap: {
    imports: {
      svelte,
      "@vivalence/ui": `../../../../packages/ui/mod.js`,
      "svelte/store": `${svelte}/store`,
      "svelte/motion": `${svelte}/motion`,
      "svelte/internal": `${svelte}/internal`,
      "svelte/internal/disclose-version": `${svelte}/internal/disclose-version`,
    },
  },
};

....
  // serve:
  bundler.serve = () => async (ctx) => {
    const path = join(dirname(input.path), ctx.params.filename);
    const bundle = await bundler(path);
    if (bundle) {
      ctx.response.body = bundle;
      ctx.response.type = "application/javascript";
    }
  };

This was consumed by the client in two steps. The Widget functioned as the Sveltekits Universal interface/ loader.

<script>
  import Component from "./Component.svelte";
  import { onMount } from "svelte";

  export let bundle;
  export let data;

  let component = null;

  async function fetchAndCompileAST() {
    const response = await locals.call.raw(bundle, null, { method: "GET" });
    const text = await response.text();
    const blob = new Blob([text], { type: "application/javascript" });
    const url = URL.createObjectURL(blob);
    const { default: Widget } = await import(/* @vite-ignore */ url);
    component = Widget;
  }

  onMount(() => {
    fetchAndCompileAST();
  });
</script>

{#if Component}
  <Component this="{component}" {...data}  />
{:else}
  <p>Loading component...</p>
{/if}

The referenced Component:

<script>
  import { onDestroy } from 'svelte'

  let component
  export { component as this }

  let target
  let cmp

  const create = () => {
    cmp = new component({
      target,
      props: $$restProps,
    })
  }

  const cleanup = () => {
    if (!cmp) return
    cmp.$destroy()
    cmp = null
  }

  $: if (component && target) {
    cleanup()
    create()
  }

  $: if (cmp) {
    cmp.$set($$restProps)
  }

  onDestroy(cleanup)
</script>

<div id="game-container" bind:this={target} />

The component thats gettings built by the server is currently an empty demo.

<script>
  console.log("Hello World from Component");
</script>

<h1 class="text-palette-white">My Heading</h1>

this setup worked like a CHARM! given, its a bit much, but once i had figured it out, it never had any hickups.

but as you can see, it relied on instantiating the components as new component classes.

now ive updated the build svelte dependency to 5.1.9 which is the same my main sveltekit app uses. on the client ive tried a few different approaches like:

replace new component with cmp = createClassComponent({component:Component, target }); and cmp = mount(Component, { target, props: payload,});

but nothing works. I get various error messages like:

Uncaught TypeError: Cannot read properties of undefined (reading 'call')

    in Component.svelte
    in Widget.svelte
    in GameBoard.svelte
    in +page.svelte
    in layout.svelte
    in +layout.svelte
    in root.svelte

    at get_first_child (operations.js:77:28)
    at template.js:48:50
    at Flashcards (Flashcards.svelte:3:44)
    at render.js:228:16
    at update_reaction (runtime.js:317:53)
    at update_effect (runtime.js:443:18)
    at create_effect (effects.js:125:4)
    at branch (effects.js:346:9)
    at render.js:210:3
    at update_reaction (:5173/.vite/deps/chunk-6CDLSX2F.js?v=6ab23a08:1714:23)TypeError: Cannot read properties of undefined (reading 'call')
    at get_first_child (operations.js:77:28)
    at template.js:48:50
    at Flashcards (Flashcards.svelte:3:44)
    at render.js:228:16
    at update_reaction (runtime.js:317:53)
    at update_effect (runtime.js:443:18)
    at create_effect (effects.js:125:4)
    at branch (effects.js:346:9)
    at render.js:210:3
    at update_reaction (runtime.js:317:53)

any help would be welcome. i am very stuck and have no clue what levers to try next.

Reproduction

above

Logs

No response

System Info

System:
    OS: macOS 14.5
    CPU: (8) arm64 Apple M1 Pro
    Memory: 161.06 MB / 16.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 20.17.0 - ~/.nvm/versions/node/v20.17.0/bin/node
    Yarn: 4.5.0 - ~/.nvm/versions/node/v20.17.0/bin/yarn
    npm: 10.8.2 - ~/.nvm/versions/node/v20.17.0/bin/npm
    pnpm: 9.11.0 - ~/Library/pnpm/pnpm
  Browsers:
    Chrome: 130.0.6723.117

Severity

blocking an upgrade

dummdidumm commented 2 days ago

Please provide a reproduction repository

LazerJesus commented 2 days ago

of the system that worked or the one that doesnt? gonna take me till tmrw

LazerJesus commented 2 days ago

@dummdidumm https://github.com/vivalence/Svelte-Widget-Demo

LazerJesus commented 2 days ago

This the version that doesnt work. with svelte 4 and just a few adjustments this setup worked like a charm.

I am pretty sure the problem is somewhere on the client with how the component is instantiated and mounted. I think that because thats the thing that changed most and the :server/bundle/Test.svelte component's console.log() statement actually executes and prints to the client console.

only once the component tries to attach to the dom, some problem arrises.

also the bundling is successfull, and from what i can tell, looks exactly like before. you can view the bundle by visiting http://localhost:3000/bundle/Test.svelte once the server is running.

dummdidumm commented 2 days ago

The problem is that your setup does not deduplicate the Svelte runtime, it exists multiple times: once for your main app, once for your each of your bundles. If you can externalize them somehow that would be the ideal outcome, since it means you also save on bundle size. If that's not possible for some reason, you have to make sure to call the mount method of your bundled component, not the one of your main app. In other words, you likely need a wrapper that does the mounting inside the bundle.

I quickly tried that like this

<script module>
    import Test from './Test.svelte'
    import { mount as _mount } from "svelte";

    export function mount(target, props) {
        return _mount(Test, { target, props})
    }
</script>

<script>
 let { ...data } = $props();
 console.log("Hello World from Test.svelte");
 console.log("Check these amazing props", data);
</script>

<h1 class="text-palette-white">My Test Heading</h1>

(and adjusting the place where the component is instantiated to use the exported mount method)

...but it still produces two versions of the Svelte runtime inside the Test.svelte bundle. I don't know why that is, but if you fix that then that would work (but as I said above ideally you would externalize the whole Svelte runtime).

Converting this to a discussion because this is not a bug in Svelte.