Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
LazerJesus opened this issue Nov 13, 2024 · 5 comments
Closed

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

LazerJesus opened this issue Nov 13, 2024 · 5 comments
Labels
awaiting submitter needs a reproduction, or clarification

Comments

@LazerJesus
Copy link

LazerJesus commented Nov 13, 2024

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/[email protected]";

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
Copy link
Member

Please provide a reproduction repository

@dummdidumm dummdidumm added the awaiting submitter needs a reproduction, or clarification label Nov 13, 2024
@LazerJesus
Copy link
Author

LazerJesus commented Nov 13, 2024

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

@LazerJesus
Copy link
Author

@LazerJesus
Copy link
Author

LazerJesus commented Nov 14, 2024

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
Copy link
Member

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.

@sveltejs sveltejs locked and limited conversation to collaborators Nov 14, 2024
@dummdidumm dummdidumm converted this issue into discussion #14298 Nov 14, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
awaiting submitter needs a reproduction, or clarification
Projects
None yet
Development

No branches or pull requests

2 participants