svelte icon indicating copy to clipboard operation
svelte copied to clipboard

Dynamic loading of cdn-hosted Svelte 5 component

Open stephanboersma opened this issue 1 year ago • 21 comments

Describe the bug

Hi team,

I am not sure whether this is a bug, not supported or me not having proper configuration. I hope that you or someone is able to help me with clarification.

The requirement I am trying to satisfy is externally compiled and hosted Svelte widgets that can be loaded into my SvelteKit app but when I try to dynamically import and load the Svelte widgets, I get the following error.

image

I can see that the dynamically loaded component taps into the svelte runtime within the sveltekit project, so I got that going for me :)

Looking forward to hear from someone.

Reproduction

1st Repo with Component

  1. Create a barebone vite & svelte 5 project
  2. Create Counter component at src/lib/Counter.svelte.
  3. npm run build
  4. use http-server to host server counter.js: http-server dist --cors

Counter.svelte

<script lang="ts">
  let count: number = $state(0);
  const increment = () => {
    count += 1;
  };
</script>

<button onclick={increment}>
  count is {count}
</button>

vite.config.js

import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';

export default defineConfig({
  plugins: [svelte()],
  build: {
    lib: {
      entry: './src/lib/Counter.svelte',
      formats: ['es'],
      fileName: 'counter',
    },
    rollupOptions: {
      external: ['svelte', 'svelte/internal'],
      output: {
        globals: {
          svelte: 'Svelte',
        },
      },
    },
  },
});

2nd Repo with barebone SvelteKit setup

  1. Create DynamicComponent at src/lib/DynamicComponent.svelte
  2. Render DyanamicComponent in routes/+page.svelte: <DynamicComponent componentUrl="http://localhost:8080/counter.js" />
  3. Start dev server: npm run dev

DynamicaComponent.svelte

<script lang="ts">
	export let componentUrl: string;
</script>

{#await import(componentUrl)}
	<div>Loading...</div>
{:then Component}
	<!-- Dynamically loaded component -->
	<Component.default />
{:catch error}
	<div>Error loading component: {error.message}</div>
{/await}

Both repos have svelte 5 installed.

Logs

No response

System Info

Repo 1:
"devDependencies": {
    "@sveltejs/vite-plugin-svelte": "^4.0.0-next.6",
    "@tsconfig/svelte": "^5.0.4",
    "svelte": "^5.0.0-next.244",
    "svelte-check": "^3.8.5",
    "tslib": "^2.6.3",
    "typescript": "^5.5.3",
    "vite": "^5.4.1"
  }

Repo 2
"devDependencies": {
		"@sveltejs/adapter-auto": "^3.0.0",
		"@sveltejs/kit": "^2.0.0",
		"@sveltejs/vite-plugin-svelte": "^4.0.0-next.6",
		"@types/eslint": "^9.6.0",
		"eslint": "^9.0.0",
		"eslint-config-prettier": "^9.1.0",
		"eslint-plugin-svelte": "^2.36.0",
		"globals": "^15.0.0",
		"prettier": "^3.1.1",
		"prettier-plugin-svelte": "^3.1.2",
		"svelte": "^5.0.0-next.1",
		"svelte-check": "^4.0.0",
		"typescript": "^5.0.0",
		"typescript-eslint": "^8.0.0",
		"vite": "^5.0.3",
		"vitest": "^2.0.0"
	},

Severity

blocking all usage of svelte

stephanboersma avatar Sep 10 '24 14:09 stephanboersma

Unless you disable SSR, you need two builds: One for the server (generate: 'ssr') and one for the client (generate: 'dom'). hydratable should also be set. (See compile options.)

Likewise you would need to change the import to get the correct file accordingly.

brunnerh avatar Sep 10 '24 14:09 brunnerh

@brunnerh Thank you for pointing that out. The sveltekit app is a SPA and has no SSR. /routes/+page.ts contains export const prerender = false.

Unfortunately, the error persists.

stephanboersma avatar Sep 10 '24 14:09 stephanboersma

SSR is controlled by the ssr setting, prerender is a separate thing.

brunnerh avatar Sep 10 '24 14:09 brunnerh

So i've explored this a bit...the error that you see is caused by this function https://github.com/sveltejs/svelte/blob/56f41e1d960fec60ba9235711435ac7ddede372e/packages/svelte/src/internal/client/dom/operations.js#L35 being undefined.

The reason because it's undefined is because this get's initialised inside this function https://github.com/sveltejs/svelte/blob/56f41e1d960fec60ba9235711435ac7ddede372e/packages/svelte/src/internal/client/dom/operations.js#L23C17-L23C32

which is called during hydrate or mount.

However by bundling the svelte component as a library you are bundling and minifying those functions too...so while in the actual runtime (of the sveltekit application) first_child_getter is defined the function inside your prebundled component is invoking the first_child_getter that was bundled with it (which was never initialised and thus undefined)

I'm not sure if there's a way to bundle your component so that it uses the importer svelte runtime (i guess you need to externalize svelte) so either this is something we definitely need to document or we might want to fix something...this is if we actually want the ability to bundle a svelte component as standalone library and give the users the ability to import it like so.

Consider that by doing this you are basically freezing in time the compiled output to that of the version you compiled it with.

paoloricciuti avatar Sep 10 '24 16:09 paoloricciuti

Hi @paoloricciuti ,

Thanks for taking the time! I think you are right in the need of externalizing svelte. I have tried to do that now and I feel like I am a little closer.

My goal was to bundle all svelte dependencies together such that I can create an import map which will be used by the dynamically imported widgets. I see the svelte-bundle.js is successfully compiled in the server but when the counter.js file is loaded, it gives an error that "template" is not a function. This indicates to me that the import map is correctly resolving to the svelte-bundle.js but maybe it is not bundled correct because something is missing.

Do you have any ideas what I might be missing?

Vite config for bundling the component:

import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';

export default defineConfig({
  plugins: [
    svelte(),
  ],
  build: {
    lib: {
      entry: './src/lib/Counter.svelte',
      formats: ['es'],
      fileName: 'counter',
    },
    rollupOptions: {
      external: [
        'svelte',
        'svelte/internal',
        'svelte/internal/client',
      ],
    },
  },
});

Output


import * as t from "svelte/internal/client";

const c = "5";
typeof window < "u" && (window.__svelte || (window.__svelte = { v: /* @__PURE__ */ new Set() })).v.add(c);
const i = (o, e) => {
  t.set(e, t.get(e) + 1);
};
var d = t.template("<button> </button>");
function r(o) {
  let e = t.state(0);
  var n = d();
  n.__click = [i, e];
  var a = t.child(n);
  t.reset(n), t.template_effect(() => t.set_text(a, `count is ${t.get(e) ?? ""}`)), t.append(o, n);
}
t.delegate(["click"]);
export {
  r as default
};

Vite config in sveltekit app

import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';

export default defineConfig({
	plugins: [sveltekit()],

	test: {
		include: ['src/**/*.{test,spec}.{js,ts}']
	},
	build: {
		rollupOptions: {
			output: {
				manualChunks: {
					'svelte-bundle': ['svelte', 'svelte/internal', 'svelte/internal/client']
				}
			}
		}
	},
});

app.html in sveltekit

<!doctype html>
<html lang="en">

<head>
	<meta charset="utf-8" />
	<link rel="icon" href="%sveltekit.assets%/favicon.png" />
	<meta name="viewport" content="width=device-width, initial-scale=1" />
	<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
	<script type="importmap">
		{
		  "imports": {
			"svelte": "/.svelte-kit/output/server/chunks/svelte-bundle.js",
			"svelte/internal": "/.svelte-kit/output/server/chunks/svelte-bundle.js",
			"svelte/internal/client": "/.svelte-kit/output/server/chunks/svelte-bundle.js"
		  }
		}
	  </script>

	%sveltekit.head%
</head>

<body data-sveltekit-preload-data="hover">
	<div style="display: contents">%sveltekit.body%</div>
</body>

</html>

stephanboersma avatar Sep 11 '24 16:09 stephanboersma

Uh i actually never used manual chunks so i'd have to explore that but my first guess would be that the built chunk is minified so template is not there anymore but it has been renamed to something else

paoloricciuti avatar Sep 11 '24 17:09 paoloricciuti

@paoloricciuti, do you think the solution mentioned here is still possible for svelte 5?

https://github.com/sveltejs/svelte/issues/3671#issuecomment-1644848898

It seems that the compiler emits an error when someone tries to import svelte/internal.

stephanboersma avatar Sep 12 '24 05:09 stephanboersma

@paoloricciuti, do you think the solution mentioned here is still possible for svelte 5?

#3671 (comment)

It seems that the compiler emits an error when someone tries to import svelte/internal.

Yeah It does error on purpose...accessing internals should not be done because are not stable (we could change those In a minor).

I'm not sure that solution is what you are looking for

paoloricciuti avatar Sep 12 '24 06:09 paoloricciuti

Hi, I have a usecase which also is affected by this. In short, I'm writing a CMS that should allow for user defined input components when editing documents, and the best solution I can think to achieve that is retrieving a pre compiled component and using it at runtime, which in my research on if this is even possible lead me to here.

murl-digital avatar Nov 26 '24 02:11 murl-digital

Also having this issue, additionally if I try to compile my Svelte 5 lib into iife - here is the result (notice that first_child_getter is never assigned)

const PUBLIC_VERSION = "5";
if (typeof window !== "undefined")
  (window.__svelte || (window.__svelte = { v: /* @__PURE__ */ new Set() })).v.add(PUBLIC_VERSION);
let active_effect = null;
var first_child_getter;
// @__NO_SIDE_EFFECTS__
function get_first_child(node) {
  return first_child_getter.call(node);
}
function create_fragment_from_html(html) {
  var elem = document.createElement("template");
  elem.innerHTML = html;
  return elem.content;
}
function assign_nodes(start, end) {
  var effect = (
    /** @type {Effect} */
    active_effect
  );
  if (effect.nodes_start === null) {
    effect.nodes_start = start;
    effect.nodes_end = end;
  }
}
// @__NO_SIDE_EFFECTS__
function template(content, flags) {
  var node;
  var has_start = !content.startsWith("<!>");
  return () => {
    if (node === void 0) {
      node = create_fragment_from_html(has_start ? content : "<!>" + content);
      node = /** @type {Node} */
      /* @__PURE__ */ get_first_child(node);
    }
    var clone = (
      /** @type {TemplateNode} */
      node.cloneNode(true)
    );
    {
      assign_nodes(clone, clone);
    }
    return clone;
  };
}
function append(anchor, dom) {
  if (anchor === null) {
    return;
  }
  anchor.before(
    /** @type {Node} */
    dom
  );
}
var root = /* @__PURE__ */ template(`<div class="mt-6 mb-20 mx-auto w-11/12 text-center sm:text-left text-secondary-light">All Systems Go</div>`);
function upload($$anchor) {
  var div = root();
  append($$anchor, div);
}
export {
  upload as default
};

Any solutions to this?

vdenisenko-waverley avatar Dec 02 '24 13:12 vdenisenko-waverley

reading through this again i can't tell if this is something that svelte isn't designed for or if this is just an edge case. it just seems like component initialization expects things to be done a certain way and there isn't a clear way to pull it apart. i don't know enough about build options and svelte's internals to have any idea about what needs to be done.

murl-digital avatar Dec 20 '24 11:12 murl-digital

I was wondering if there is any update to the issue? @stephanboersma did you manage to find a solution that is working?

TheKili avatar Jan 09 '25 15:01 TheKili

@TheKili Unfortunately not.

stephanboersma avatar Jan 09 '25 17:01 stephanboersma

@stephanboersma to make sure i understand (because this thread's a bit hard for me to follow), where are you getting stuck?

murl-digital avatar Jan 09 '25 22:01 murl-digital

Hi @murl-digital,

In my latest attempt, I tried to use vite module federation since this seems easier than a webpack config. However, I end up at the same error as described in the issue.

image

See this repo: https://github.com/stephanboersma/svelte-cdn

The remote exposes App.Svelte and the Host tries load it. See the module federation config in vite.config.js.

I suspect that this isn't possible to solve using vite or webpack but that it is about the javascript that the svelte compiler ultimately outputs.

stephanboersma avatar Jan 10 '25 06:01 stephanboersma

I'm looking for the same thing. My reason being me not wanting to do a full site deploy and just update the component that's on the cdn.

kakarlus avatar Feb 10 '25 05:02 kakarlus

In my experience this happens when you're trying to mount a precompiled Svelte component. Module federation and single-spa fall under this case.

For Svelte 3 and Svelte 4:

As these are uncontrolled components, you mount them like this

<script>
    import { SimpleComponent } from 'remote/SimpleComponent';

    let element;

    onMount(() => {
        new SimpleComponent({ target: element }, props: {propName: 'my prop value'}); // you might need to use a dynamic import here and use the default property of that imported module
    });
</script>

<div bind:this={element} />

You can pass props as you instantiate the component, but it looks like you can't pass slots from the consumer app to the remotely (dynamic) loaded component.

For Svelte 5:

It's not a solution, but if you enable compatibility with Svelte 4 in your producer app's bundler config you can instantiate the component exactly as you did with Svelte 4

  compilerOptions: {
    compatibility: {
      componentApi: 4,
    },
  },

Related:

  • https://github.com/sveltejs/svelte/issues/6584

yairkukielka avatar Feb 11 '25 13:02 yairkukielka

Just tried the same using esbuild to build the component, same result obviously. As Svelte 5 has been out for a while it would be nice to get that working somehow, not having to downgrade to Svelte 4.

inzanez avatar Mar 18 '25 10:03 inzanez

I would like to provide a solution that may not relate to the code, but the server.

[!IMPORTANT] TLDR: Disable CDN/Server Cache


So this problem occurred to me today out of nowhere. It caused all the svelte components (layout, page, etc.) disappear from the dom after the JS is loaded, only app.html content stays. If I disable csr this problem will go away but I don't think this is the correct solution.

The weird thing is, the problem doesn't always appear. I tried to revert my changes, reinstall dependencies, disable browser cache and switch browser. Nothing works, it went away and came back to stay. Not until I started a new svelte-kit project and the problem is still there. Now I realize the problem is from the server.

In my case, I do all my development on my dev server. localhost:5173 is exposed to one of my domains via Cloudflare Tunnel. By default the Cloudflare CDN cached the content, and the bugged js chunks from the previous compilation are cached and send to my browser even after I started a new project.

If you're also using Cloudflare, you can go to your domain and turn on Development Mode, or create a Page Rule that bypass Cloudflare's cache if you're using a subdomain. If you use other CDN, try to disable or refresh the cache. Hope this helps!

let-lc avatar Apr 21 '25 05:04 let-lc

However by bundling the svelte component as a library you are bundling and minifying those functions too...so while in the actual runtime (of the sveltekit application) first_child_getter is defined the function inside your prebundled component is invoking the first_child_getter that was bundled with it (which was never initialised and thus undefined)

I'm not sure if there's a way to bundle your component so that it uses the importer svelte runtime (i guess you need to externalize svelte) so either this is something we definitely need to document or we might want to fix something...this is if we actually want the ability to bundle a svelte component as standalone library and give the users the ability to import it like so.

One workaround to get the first_child_getter initialised is by making the mount operation happen in the context of the library component instead of the actual runtime:

App.svelte

<script>
  import svelteLogo from "./assets/svelte.svg";
  import viteLogo from "/vite.svg";
  import Counter from "./lib/Counter.svelte";
  import { getContext } from "svelte";

  const ctx = getContext("key");

  export const testFn = () => {
    console.log(ctx);
  };
</script>
...

lib.js

import { mount as $mount, unmount as $unmount } from 'svelte'
import './app.css'
import App from './App.svelte'

let app

export function mount(options) {
  return (app = $mount(App, options))
}

export function unmount(options) {
  $unmount(app, options);
}

vite.config.js

import { resolve } from 'path'
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'

// https://vite.dev/config/
export default defineConfig({
  plugins: [svelte()],
  build: {
    minify: false,
    lib: {
      formats: ['es'],
      entry: {
        'app': resolve(__dirname, 'src/lib.js'),
      },
      cssFileName: 'style'
    },
  }
})

App.svelte (importer)

<script>
  import { onDestroy, onMount } from "svelte";
  import * as App from "http://localhost:8080/app.js";

  let container = $state();

  onMount(() => {
    const instance = App.mount({
      target: container,
      context: new Map([["key", "value"]]),
    });
    instance.testFn();
  });

  onDestroy(() => {
    App.unmount();
  });
</script>

<link rel="stylesheet" href="http://localhost:8080/style.css" />

<h1>Imported app:</h1>
<div bind:this={container}></div>

<style>
  div {
    border: 0.25em solid magenta;
  }
</style>

Image

Though, I'm not sure if there are any issues like rendering snippets inside the library component that are passed by the runtime. Worth giving that a try.

5chulzi avatar May 17 '25 14:05 5chulzi

We're using a pre-compiled runtime with svelte 4 so we can selectively load components at runtime. I need to update it to work with svelte 5. Originally I created a small toy repo to get this working. After lots of messing I seem to have this working with 5 too. I've made this repo public incase it helps anyone else here.

https://github.com/crisward/svelte-shared

crisward avatar May 25 '25 10:05 crisward

@crisward Your solution for external Svelte runtime shared by both host app and dynamically loaded components looks like the best, thanks. I'm attempting to apply it within a SvelteKit / vite setup.

tstewart-klaudhaus avatar Aug 29 '25 07:08 tstewart-klaudhaus