esm.sh icon indicating copy to clipboard operation
esm.sh copied to clipboard

Package that expose multiple entrypoints with a shared module do not externalize that module

Open paoloricciuti opened this issue 1 year ago • 3 comments

Sorry if i'm not following a template but this is a bit of a special case...the import actually succeed but i think the resulting JS file is unproperly bundled.

So what i'm doing is i'm compiling a svelte component into a JS file. It looks something like this.

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

var root = $.template(`<h1> </h1>`);

export default function App($$anchor, $$props) {
	var h1 = root();
	var text = $.child(h1);

	$.reset(h1);
	$.template_effect(() => $.set_text(text, `Hello ${$$props.greet ?? ""}`));
	$.append($$anchor, h1);
}

then i'm importing this component and hydrate from svelte in my main js file

import Component from "./Component.js";
import { hydrate } from "svelte";

hydrate(Component, {
    target: document.getElementById("app"),
    props: { greet: "world" },
});

i can setup an importmap to map this work like this

<script type="importmap">
    {
        "imports": {
            "svelte": "https://esm.sh/svelte@next",
            "svelte/": "https://esm.sh/svelte@next/"
        }
    }
</script>

however here's where the problem arise. As you can see my main js file it's importing hydrate from svelte (which is the root entry point exposed by svelte) and the Component.js it's importing the client runtime from svelte/internal/client, another entry point exposed by svelte.

Those two modules exposed by svelte both import a third module that expose an export hydrating and some other utilities. However when accessing the module bundled by esm.sh this third shared module is bundled separately in each module so the aforementioned hydrating variable is just a local variable for both modules. This obviously breaks a lot of stuff and the whole thing cease to work.

I can fix this with a slight change to the import map

<script type="importmap">
    {
        "imports": {
            "svelte": "https://esm.sh/svelte@next?no-bundle",
            "svelte/": "https://esm.sh/svelte@next&no-bundle/"
        }
    }
</script>

but this

  1. it's not ideal
  2. ships the library completely unbundled leading to a lot of http requests.

The best option would be for esm.sh to smartly externalize a module if it's shared by multiple exposed entry points of a library. Do you think it could be feasible? Do you see any other solution to this problem?

paoloricciuti avatar Sep 25 '24 20:09 paoloricciuti

The best option would be for esm.sh to smartly externalize a module if it's shared by multiple exposed entry points of a library.

i like this idea, we can decide whether a module should be bundled by checking the dependency tree, i will look into it

ije avatar Sep 27 '24 07:09 ije

I think it's probably simpler than checking the dependency tree — just pass the entries in package.exports to esbuild as entryPoints and enable splitting: true, and it should do the work for you

Rich-Harris avatar Sep 28 '24 02:09 Rich-Harris

@Rich-Harris the exports is alreay externalized by default, for example, entries esm.sh/svelte and esm.sh/internal/client share the same module https://esm.sh/stable/[email protected]/es2022/internal/client.js, and entry esm.sh/svelte imports another module https://esm.sh/stable/[email protected]/es2022/svelte.mjs.

I think the issue @paoloricciuti got is, same modules with side-effect are required by esm.sh/svelte and esm.sh/internal/client are not externalized.

here i found this file( https://github.com/sveltejs/svelte/blob/35ebbe61adfb09a780806d70f5c0067e6ebd78f7/packages/svelte/src/internal/client/render.js#L171) should be externalized, but it's not declared in the exports field. (it's bundle both in the client.js and svelte.mjs)

Screenshot 2024-09-28 at 13 51 56 Screenshot 2024-09-28 at 13 52 10

ije avatar Sep 28 '24 06:09 ije