sveltejs / svelte

Cybernetically enhanced web apps
https://svelte.dev
MIT License
78.39k stars 4.1k forks source link

Dynamic loading of cdn-hosted Svelte 5 component #13186

Open stephanboersma opened 1 week ago

stephanboersma commented 1 week ago

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

brunnerh commented 1 week ago

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.

stephanboersma commented 1 week ago

@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.

brunnerh commented 1 week ago

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

paoloricciuti commented 1 week ago

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.

stephanboersma commented 6 days ago

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/es-module-shims@1.7.3/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>
paoloricciuti commented 6 days ago

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

stephanboersma commented 5 days ago

@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.

paoloricciuti commented 5 days ago

@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