Skip to content
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

Introducing esm.run - A New-Age CDN for JavaScript modules #18263

Open
MartinKolarik opened this issue Oct 30, 2020 · 60 comments
Open

Introducing esm.run - A New-Age CDN for JavaScript modules #18263

MartinKolarik opened this issue Oct 30, 2020 · 60 comments

Comments

@MartinKolarik
Copy link
Member

MartinKolarik commented Oct 30, 2020

Updated as of Sep 2022:

We are excited to announce a long-awaited feature - improved support for packages distributed as ES modules 🎉

It is currently in beta, which means we encourage you to try it out, but not use it in production applications yet. This issue will cover all technical details and can be also used to provide feedback or report bugs if you find any.

TL;DR

  1. Demo and essential information: https://www.jsdelivr.com/esm
  2. You can load any ESM file like this:
    • https://cdn.jsdelivr.net/npm/uuid/+esm (default file)
    • https://cdn.jsdelivr.net/npm/uuid/dist/esm-browser/index.js/+esm (explicit file).
  3. There's a new domain that you can use for shorter and prettier links. It redirects everything to our main domain and is not meant to be used in production:
    • https://esm.run/uuid
    • https://esm.run/uuid/dist/esm-browser/index.js

How this works

Locating entry points

To find the default file, we use exports, module, and jsnext:main fields from package.json. These fields are currently used by all existing tooling and by most package authors.

Handling imports

Resolving

  1. Absolute URLs like https://example.com/foo are always kept in their original form.
  2. Relative imports are first resolved via the browser field if it exists (this applies to the entry point as well) and then resolved using node's experimental resolution algorithm, which supports extension resolution and importing from directories that include an index file. The supported extensions are: .mjs, .js, and .json.

Bundling

  1. Internal imports (pointing to files in the same package):

    • are bundled with the requested file if they are static, e.g., import foo from './foo.js',
    • are rewritten to their jsDelivr URL if they are dynamic, e.g., import('./foo.js').
  2. External imports (pointing to files in other packages):

    • are always rewritten to their jsDelivr URL, whether they are static or dynamic.

This means that one npm package roughly equals one bundle and one HTTP request, which allows good caching and doesn't require too many HTTP requests.

External URLs are always generated with fully resolved dependency versions based on package.json dependencies (or latest if the version is not specified there). This further improves caching and guarantees stable versions even for transitive dependencies.

Performance

  • The generated files are automatically minified.
  • Source maps are available for easy debugging.
  • Once generated, the files are stored in our permanent storage and served from there.
  • In supported browsers, HTTP preloading is used to start fetching external static dependencies as soon as possible.

What doesn't work (but may in the future)

  • Packages that are written in CJS, or a mix of ESM and CJS. CJS packages are now transformed to ESM.
  • Packages that import core node.js modules. Most core node.js modules are now polyfilled.
  • Packages that import files in formats other than JS/JSON. CSS is now supported too.
  • Packages larger than 50 MiB.
  • search on esm.run lists all packages. This will soon be changed to list only the supported ones.

The esm.run domain

This domain is introduced as an easier to remember and type alternative. It doesn't run on our multi-cdn architecture and simply redirects all requests to the main cdn domain, so it's not meant to be used in production.

@Finesse
Copy link

Finesse commented Dec 9, 2020

Is there a URL option to bundle all imports inside a single file? Comments in files produced by jsDeliver say that Rollup is used so it won't be a problem.

@lubomirblazekcz
Copy link

Will the current URL scheme change to me more friendly? Currently what I see in devtools/network is bunch of +esm files

I guess this https://cdn.jsdelivr.net/npm/d3@6.3.1/+esm/d3.js would be better than this https://cdn.jsdelivr.net/npm/d3@6.3.1/+esm

Skypack and esm.sh have similar approach for production urls
https://cdn.skypack.dev/pin/react@v17.0.1-tOtrZxBRexARODgO0jli/min/react.js
https://cdn.esm.sh/react@17.0.1/esnext/react.js

@MartinKolarik
Copy link
Member Author

@evromalarkey I did notice this problem but you can easily configure devtools to show full paths by right-clicking the column:

image

Alternatively, you could also add the filename as a query string, which we ignore: https://cdn.jsdelivr.net/npm/d3@6.3.1/+esm?d3

@FredKSchott
Copy link

Congrats on the release! I've run your benchmarks myself a few times now, and can't reproduce your numbers across a few different devices. Here's what I see on my laptop:

Screen Shot 2021-01-25 at 7 41 36 PM

I don't know if this is out of date or just bad, but the data appears outdated across both Skypack and https://esm.run.

Additionally, your benchmark imports are to https://cdn.jsdelivr.net/npm/d3/+esm, and not https://esm.run. That's fine if you want to measure the "optimized" use-case, but then that should really be compared against Skypack's optimized URL as well: https://docs.skypack.dev/skypack-cdn/api-reference/pinned-urls-optimized

Can you update these stats to better represent things to your users?

@MartinKolarik
Copy link
Member Author

Hi @FredKSchott, the performance may of course change over time as each of the services develops. We would have preferred showing realtime results but that wasn't possible so we went with numbers that were accurate at the time this was launched. We'll recheck and update if the results are consistently different now.

Regarding the pinned URLs, those are not functionally equivalent because they lock down a specific version while jsDelivr (even on our primary domain) and unpkg resolve to latest.

@vp2177
Copy link

vp2177 commented Jan 30, 2021

I'm not sure this is the appropriate place to report this, I've tried this service with lit-element and while there are no errors logged, their html function doesn't seem to work.

Here is a repro: https://vp2177.github.io/js-utils/?jsdelivr-lit-element-issue

I have also tried loading lit-element from SkyPack and in that case it works, you can try by uncommenting that import.

@MartinKolarik
Copy link
Member Author

Thanks for the report @vp2177. I don't immediately see where's the problem as L.html returns the correct object but we'll check this further later.

@MartinKolarik
Copy link
Member Author

@vp2177 after closer inspection we found the problem but not yet sure how we'll be able to fix this.

lit-element imports main lit-html file here and it also imports another nested file shady-render.js from lit-html here. The problem is shady-render.js imports the main lit-html file as well.

When we get a request for shady-render.js we include all of its imports, including the main lib-html file (this is by design - as explained in the first post here, internal imports are always bundled).

Then when lib-element imports the main lib-html file in a separate request, it's loaded a second time. This would not be a breaking problem in most cases but the library relies on instanceof checks and those don't work if two versions of the code are loaded.

@MartinKolarik
Copy link
Member Author

@FredKSchott it seems Skypack is now consistently faster than it was before so we decided to update the numbers.

@lubomirblazekcz
Copy link

https://esm.run/@fullcalendar/daygrid fails to load, you should check all @fullcalendar plugins though, I think it's because import css (eg. https://cdn.jsdelivr.net/npm/@fullcalendar/timegrid/main.js)

Another problem is https://esm.run/dayjs (works on https://esm.run/dayjs/esm though)

@lubomirblazekcz
Copy link

Also I see a potential problem with resolving subfolders (only with production url) when using importmaps

{
  "imports": {
    "dayjs": "https://cdn.jsdelivr.net/npm/dayjs/esm/+esm",
    "dayjs/": "https://cdn.jsdelivr.net/npm/dayjs/esm/+esm/"
  }
}

import isBetween from "dayjs/plugin/isBetween.js" - this wouldn't work

it would work with esm.run url though

{
  "imports": {
    "dayjs": "https://esm.run/dayjs/esm",
    "dayjs/": "https://esm.run/dayjs/esm/"
  }
}

It would work with https://cdn.jsdelivr.net/npm/dayjs/esm/, but that's only because this library has esm version builded on npm. The same problem could be in many other libraries with subfolders.

@MartinKolarik
Copy link
Member Author

I think it's because import css

Yes, that is not supported yet, as mentioned in the original post.

Another problem is https://esm.run/dayjs (works on https://esm.run/dayjs/esm though)

dayjs removed the "module" package.json entry in the recent versions - I'm not sure why but there isn't much we can do in this case.

Regarding the import maps - will check this closer.

@mikabytes
Copy link

mikabytes commented Apr 22, 2021

Would it be possible to add HTTP2 push support, and an option to disable bundling?

With bundling, we get this issue:

Imagine having A.js imports C.js. We run an SPA. On initial fetch only C.js is imported as the rest aren't needed. Later on, A.js is imported.

Result:

  • C.js has been downloaded twice
  • C.js has been parsed and evaluated twice
  • The two imports of C.js are not the same. They are completely different files, so references are broken

Bundling is a dinosaur performance thing. We better avoid it when possible.

@MartinKolarik
Copy link
Member Author

@mikabytes indeed, that is basically the issue described in #18263 (comment) and we might end up introducing some options to fix it.

@Xeevis
Copy link

Xeevis commented Jun 13, 2021

Hello, I have trouble importing libraries that use process.env.NODE_ENV to differentiate development and production builds. For example

import tippy from "https://cdn.jsdelivr.net/npm/tippy.js/+esm"; 

throws in browser

- ReferenceError: process is not defined
- at https://cdn.jsdelivr.net/npm/tippy.js/+esm:12:2691

In skypack, non-production code (including the expression) is stripped away.

https://cdn.skypack.dev/-/tippy.js@v6.3.1-HeICNxUmzNoshMxx5q78/dist=es2020,mode=imports/optimized/tippyjs.js
(no process.env.NODE_ENV)

vs

https://cdn.jsdelivr.net/npm/tippy.js@6.3.1/+esm
(plenty of process.env.NODE_ENV)

Since esm.run is using Rollup, this should be easy to solve with @rollup/plugin-replace plugin (which is also what Tippy suggests).

import replace from '@rollup/plugin-replace';

export default {
  plugins: [
    replace({
      'process.env.NODE_ENV': JSON.stringify('production')     
    })
  ]
};

@MartinKolarik
Copy link
Member Author

We're aware of this pattern for CJS files but I'd say it's a bad idea to assume process exists in ESM code. One of the key advantages of ESM is that it should work in a browser without bundling so the check is usually written in a safe way as process && process.env && process.env.NODE_ENV

Still, if some modules rely on it, we'll look into replacing NODE_ENV to improve the compatibility.

@MaximKing1
Copy link

Congrats on the release! I've run your benchmarks myself a few times now, and can't reproduce your numbers across a few different devices. Here's what I see on my laptop:

Screen Shot 2021-01-25 at 7 41 36 PM

I don't know if this is out of date or just bad, but the data appears outdated across both Skypack and https://esm.run.

Additionally, your benchmark imports are to https://cdn.jsdelivr.net/npm/d3/+esm, and not https://esm.run. That's fine if you want to measure the "optimized" use-case, but then that should really be compared against Skypack's optimized URL as well: https://docs.skypack.dev/skypack-cdn/api-reference/pinned-urls-optimized

Can you update these stats to better represent things to your users?

Just a question where did you get that comparison table from? Did you make it using something like photoshop or a service looks nice 👍

@zarianec
Copy link

@MaximKing1 this is regular benchmark results from https://www.jsdelivr.com/esm. You just need to resize your browser's width until the page gets redrawn for smaller devices.

@IgorNovozhilov
Copy link

IgorNovozhilov commented Sep 28, 2021

@MartinKolarik

@vp2177 after closer inspection we found the problem but not yet sure how we'll be able to fix this.

lit-element imports main lit-html file here and it also imports another nested file shady-render.js from lit-html here. The problem is shady-render.js imports the main lit-html file as well.

... This would not be a breaking problem in most cases but the library relies on instanceof checks and those don't work if two versions of the code are loaded.

Found a similar problem in Material Web Components.
mwc-tab-bar-base.ts imports file mwc-tab-base.ts as import {TabBase} from '@material/mwc-tab/mwc-tab-base';
At the same time file mwc-tab-base.ts imported as nested file in mwc-tab.ts, which is also imported from mwc-tab-bar-base.ts as import {Tab} from '@material/mwc-tab';.
And as a result, this check does not work: .filter((e: Node) => e instanceof TabBase, because the class declaration is duplicated

Proposal

There is a suggestion how to solve this problem via package.json#exports

  1. (For unambiguity of import and restoring order) For "+esm" mode, if package.json contains exports block external import from CDN on the path to the file not specified in exports

Like in CommonJS, module files within packages can be accessed by appending a path to the package name unless the package’s package.json contains an "exports" field, in which case files within packages can only be accessed via the paths defined in "exports".
(с) https://nodejs.org/dist/latest-v16.x/docs/api/esm.html#esm_terminology

  1. Export paths from exports always provide the independent URI's, and replace the import of nested files to inside a bundled file to external paths, if they are finally resolved in the ways specified in exports

For example, for the above case
Both files (https://cdn.jsdelivr.net/npm/@material/mwc-tab/+esm, https://cdn.jsdelivr.net/npm/@material/mwc-tab-bar/+esm) will contain:
import{TabBase as c}from"/npm/@material/mwc-tab@0.25.1/mwc-tab-base/+esm";

Perhaps this will be another headache for modules authors , but it will develop people's desire for a thoughtful declaration of dependencies and exported paths for the module. imho 🐈

FAQ
Anticipating the question that this will increase the number of downloaded files, I will answer that this problem is solved module authors by generalizing the export to one point. It is their decision whether to allocate independent access points, or leave a single file.

I can notice that even directly in "Node.js", importing from different export points can periodically cause problems with duplicating code downloads with different versions. (Tested by personal bitter experience 🤯)

@MartinKolarik
Copy link
Member Author

MartinKolarik commented Sep 28, 2021

@IgorNovozhilov the problem is definitely fixable by package authors writing the imports in a different way but our goal is to make as many packages as possible work without requiring specific patterns.

@IgorNovozhilov
Copy link

make as many packages as possible work

In this case, if you just add replacement nested file imports to external ones, it will definitely increase the number of packages that will work correctly when importing from your CDN 🙃

without requiring specific patterns

package.json#exports is gradually becoming more popular, it has much more possibilities for use in combination with ES modules. In my opinion, I would still consider exports as basic definition of available export points, and not as specific pattern for "+esm" mode

@claviska
Copy link

This is fantastic! May I suggest using ?module or ?esm instead of /+esm in the URL? Aside from dev tools showing tons of +esm entries, it feels very strange using a virtual folder that could potentially collide with real folders.

@MartinKolarik
Copy link
Member Author

Thanks for reporting @nihgwu! Turns out we didn't properly purge the old files versions in some cases. This should be fixed soon.

@nihgwu
Copy link

nihgwu commented Mar 14, 2022

@MartinKolarik forgot to mention another issue, I got two requests for the same react version, which will cause this error, I double checked the request url is exactly the same, don't know why, unless I explicitly use https://cdn.jsdelivr.net/npm/react@17.0.2/+esm and https://cdn.jsdelivr.net/npm/react-dom@17.0.2/+esm

@MartinKolarik
Copy link
Member Author

@nihgwu please check now.

@nihgwu
Copy link

nihgwu commented Mar 14, 2022

@MartinKolarik thank you for your quick response, really appreciated. It almost works, now I got TypeError: Cannot read properties of null (reading 'createSvgIcon') error, it will work if I don't import the icon, reproduce demo

And this issue is still there, which is the biggest problem for me, I can't use https://cdn.jsdelivr.net/npm/react@17.0.2/+esm as alias as then jsx-runtime will be resolved to https://cdn.jsdelivr.net/npm/react@17.0.2/+esm/jsx-runtime which is wrong, and I don't want to change all of my react import to remote version as in that way I won't get typings

@MartinKolarik
Copy link
Member Author

And #18263 (comment) is still there, which is the biggest problem for me, I can't use https://cdn.jsdelivr.net/npm/react@17.0.2/+esm as alias as then jsx-runtime will be resolved to https://cdn.jsdelivr.net/npm/react@17.0.2/+esm/jsx-runtime which is wrong, and I don't want to change all of my react import to remote version as in that way I won't get typings

I don't follow here, I don't see any duplicate requests in the jsFiddle.

It almost works, now I got TypeError: Cannot read properties of null (reading 'createSvgIcon') error, it will work if I don't import the icon, reproduce demo

Indeed, I don't see why yet, if you got a chance to pinpoint what exactly is going wrong and where it would greatly help (there's many nested imports so we need to find which file exactly is getting transformed incorrectly if it's supposed to work).

@nihgwu
Copy link

nihgwu commented Mar 14, 2022

@MartinKolarik I tried to workaround the dup react issue here, and I think probably you should use the same strategy, and in that way we will get better debug experience, right now they are all +esm in the network panel, so I propose the following format

// response for `https://esm.run/react`, instead of redirecting
export * from 'https://cdn.jsdelivr.net/npm/react@17.0.2/+esm'
export { default } from 'https://cdn.jsdelivr.net/npm/react@17.0.2/+esm'

in this way we can ensure only one version of React will be imported

You can play with this deployment

the current url format is insane
image

@nihgwu
Copy link

nihgwu commented Mar 15, 2022

@MartinKolarik Regarding the duplicated requests for React, repro demo in jsFiddle

The first and forth requests are importing the same content, and for my case, the request urls are also the same
image

@nihgwu
Copy link

nihgwu commented Mar 15, 2022

@MartinKolarik And for the icon issue, here is the problem, the import result is null

@MartinKolarik
Copy link
Member Author

@MartinKolarik Regarding the duplicated requests for React, repro demo in jsFiddle

The first and forth requests are importing the same content, and for my case, the request urls are also the same image

I believe here it would help if you loaded https://esm.run/react@17.0.2 in your code but the versions handling is indeed a little problematic, I'll see if we can make it more intuitive.

@MartinKolarik
Copy link
Member Author

MartinKolarik commented Mar 15, 2022

@MartinKolarik And for the icon issue, here is the problem, the import result is null

The file doesn't have a default export even in the original version so that seems right: https://cdn.jsdelivr.net/npm/@mui/material@5.5.1/utils/index.js

Btw we should probably move this to a separate issue, feel free to open one.

@nihgwu
Copy link

nihgwu commented Mar 15, 2022

@MartinKolarik in my case the duplicated urls are both https://cdn.jsdelivr.net/npm/react@17.0.2/+esm, OK I can reproduce it with your suggestion https://jsfiddle.net/sq0xLmz4/

Seems it's related to react-dom, it will happen only if I import react-dom at the same time, but it's OK with other packages I'm wrong on this, it happens to other packages as well

What about my suggestion to response instead of redirecting? That's also what SkypackCDN and esm.sh do

@MartinKolarik
Copy link
Member Author

Indeed, we may need to do the reexport thing for this case.

@MartinKolarik
Copy link
Member Author

Also regarding the paths, there's a reasonable workaround #18263 (comment) but we're still considering other options too.

@nihgwu
Copy link

nihgwu commented Mar 15, 2022

Indeed, we may need to do the reexport thing for this case.

Yes I just tested, it's not only for react but any package with a dependent, the dependency will always be requested twice, we should reexport to solve it

@JosefJezek
Copy link

I found issue with Lit lib.

https://cdn.jsdelivr.net/npm/lit@2.2.3/+esm

https://jsfiddle.net/startweb/ko73cjaq/

File lit-element@3.2.0/lit-element.js imports older reactive-element@1.3.0 and lit-html@2.2.0

https://cdn.jsdelivr.net/npm/lit-element@3.2.0/lit-element.js/+esm

@jimmywarting
Copy link

When i try to import cbor-x that have some optional node:buffer support to speed up certain things then it likes to inline nodejs buffer into the package.

Here is a cjs example that you can see do not have any require('buffer') syntax
https://cdn.jsdelivr.net/npm/cbor-x@1.4.1/dist/index.js

but when i add /+esm then it includes the Buffer module which it dose not need to function correctly. Buffer is optional
https://cdn.jsdelivr.net/npm/cbor-x@1.4.1/dist/index.js/+esm

I really dislike that NodeJS added the Buffer module onto the global namespace it the first place... it should have been just like any other core module... ppl should really be using import Buffer from 'node:buffer' if they intend to use it...

is there any way to circumvent this? i don't want or need the buffer module...

@MartinKolarik
Copy link
Member Author

This is intentional because, unfortunately, many packages rely on the global Buffer and don't work without it. If a global Buffer reference is found it the code, we add the polyfill.

@MartinKolarik
Copy link
Member Author

MartinKolarik commented Dec 10, 2022

The package could opt-out from this by using an import Buffer from 'node:buffer' and then excluding this import from browser builds via imports or browser fields.

@jimmywarting
Copy link

jimmywarting commented Dec 10, 2022

can i (they) do something like const Buffer = globalThis.Buffer at the top of the file? or even possible to something like

  • const Buffer = globalThis['Buffer'] or
  • const Buffer = globalThis['Buf'+'fer'] or
  • function getThingFromGlobal(thing) { return globalThis[thing] }; getThingFromGlobal('Buffer')

what makes the parse think it needs to polyfill the Buffer?

@MartinKolarik
Copy link
Member Author

Importing it as a module and then adding this to package.json is the best solution (docs):

"browser": {
    "node:buffer": false
}

Alternatively https://nodejs.org/dist/latest-v19.x/docs/api/packages.html#subpath-imports can be used and for browser it can resolve to an empty file.

const Buffer = globalThis['Buf'+'fer] would likely work too, just not as clean as the other solutions.

@jimmywarting
Copy link

just hope that it don't do minifications first and resolve 'Buf'+'fer' into 'Buffer' before it tries to polyfill stuff. but i will pass your suggestion along to the other repo, thanks for your help!

@MartinKolarik
Copy link
Member Author

Minification si the last step so that shouldn't be an issue.

@mon-jai
Copy link

mon-jai commented Jan 21, 2023

Will the esm.run server be open sourced?

@porfirioribeiro
Copy link

Support for dependency version?

I am currently aliasing vue and vue-i18n to esm.run

vue: "https://esm.run/vue@3",
"vue-i18n": "https://esm.run/vue-i18n@9",

But this will fail, as vue@3 resolves to vue@3.2.47, but vue-i18n imports vue@3.2.45
That means it will download 2 versions of vue and will not work.

Of course i could change my dependency to be vue@3.2.45, and that will work, but it will break as soon as i have other dependency that requires vue@3.2.46 for example.

esm.sh solves this by adding a dep option to allow specifying a specific version.

vue: "https://esm.sh/vue@3",
"vue-i18n": "https://esm.sh/vue-i18n@9?deps=vue@3",

Having something like this in esm.run would be really nice

@Potherca
Copy link

Playing around with /+esm, I noticed that it does not work together with /combine.

The error I get is:

Uncaught SyntaxError: Identifier 't' has already been declared (at +esm:30:20)

(Where t is sometimes e or another letter)

Is this a bug, or is combining ESM not supported?

@MartinKolarik
Copy link
Member Author

Yes, combine for esm is not supported as it would quite easily result in conflicts in the exports.

@Potherca
Copy link

Potherca commented May 11, 2023

When I try to /combine/ different formats, I get an error message that this isn't allowed.

For clarity, it might be worth adding a similar message that /+esm is disallowed?
(As it currently does create a JS file as output, but the file errors out).

@mistic100
Copy link
Contributor

mistic100 commented Aug 31, 2023

Support for dependency version?

I am currently aliasing vue and vue-i18n to esm.run

vue: "https://esm.run/vue@3",
"vue-i18n": "https://esm.run/vue-i18n@9",

But this will fail, as vue@3 resolves to vue@3.2.47, but vue-i18n imports vue@3.2.45 That means it will download 2 versions of vue and will not work.

Of course i could change my dependency to be vue@3.2.45, and that will work, but it will break as soon as i have other dependency that requires vue@3.2.46 for example.

esm.sh solves this by adding a dep option to allow specifying a specific version.

vue: "https://esm.sh/vue@3",
"vue-i18n": "https://esm.sh/vue-i18n@9?deps=vue@3",

Having something like this in esm.run would be really nice

I think I have a similar need :

  • @photo-sphere-viewer/core excplicitely declare three in its dependencies, thus it imports /npm/three@0.155.0/+esm
  • @photo-sphere-viewer/markers-plugin depends on @photo-sphere-viewer/core but not on three (assumed to be there transitively) thus it imports /npm/three/+esm

of course I could declare three as dependencies of all @photo-sphere-viewer packages, but is this the way to go ?


Similarly I cannot import @photo-sphere-viewer/core@5 with @photo-sphere-viewer/markers-plugin@5 because it will load version 5.3.0 of the core twice, once as /npm/@photo-sphere-viewer/core@5/+esm and once as /npm/@photo-sphere-viewer/core@5.3.0/+esm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests