jspm
jspm provides a module CDN allowing any package from npm to be directly loaded
in the browser and other JS environments as a fully optimized native JavaScript module.
June 19th 2020: The new jspm.dev CDN has been released - read the release post or scroll down for documentation.
Load any npm library in the browser with module scripts:
<script type="module">
// Statically:
import babel from 'https://jspm.dev/@babel/core';
console.log(babel);
// Dynamically:
(async () => {
console.log(await import('//jspm.dev/lodash@4/clone'));
})();
</script>
Try this example in the online sandbox.
Features
jspm provides an alternative to traditional JS build tooling without getting dragged down into npm installs and build configurations. Just import packages directly from JS and start hacking!
- All modules on npm are converted into ES modules handling full CommonJS compatibility including strict mode conversions.
- Packages are served heavily optimized with RollupJS code splitting and dependency inlining and served over Google Cloud CDN for global edge caching.
- All Node.js module loading semantics are supported including the new package exports field.
- Exact versions are cached with far-future expires for optimal loading. Non-exact versions redirect to exact versions, with the redirects refreshed from the edge CDN to pick up version updates every few minutes.
- High performance CDN, using Google Cloud CDN and Cloud Storage - no custom code lies between Google's Cloud CDN, HTTPS Load Balancer and Storage - the uptime guarantees are the direct Google Cloud uptime guarantees. See for example any popular CDN comparison to see how Google is faster than other cloud providers here.
URL Patterns
All packages from npm are precomputed and served through jspm.dev and are available at their corresponding URLs.
Versions
To specify a specific package version target, the following URL versioning patterns are supported:
jspm.dev/pkg | Load the main entry point of a package at the latest version. |
---|---|
jspm.dev/pkg@1 | Load the latest ^1 release of the package (includes prereleases). |
jspm.dev/pkg@1.2 | Load the latest ~1.2 release of the package (including prereleases). |
jspm.dev/pkg@ | Load the edge version of a package. This is the highest possible semver version including prereleases. |
jspm.dev/pkg@tag | Load a tagged package version. |
jspm.dev/npm:pkg@1.2.3 jspm.dev/pkg@1.2.3 | Load an exact version of a package. The explicit `npm:` registry identifier is optional, to avoid the automatic redirect that is added for forwards compatibility with new registries in future. |
Subpaths
Full subpath support is also provided for packages. It is a recommended best-practice to use package subpaths where possible to load specific package features instead of loading all package code when some of it might be unused:
jspm.dev/pkg/subpath | Load a subpath of a package - applies to all version patterns above. |
---|
Packages that have an exports field defined will expose the subpaths corresponding to the exports field. For packages without an exports field, a statistical analysis process is used to determine the subpaths of a package in code splitting optimization.
Import Maps
Including full URLs in every import, like import 'https://jspm.dev/svelte@3'
, can become repetitive to maintain. Package import maps are a specification allowing for defining package URLs in the browser.
With import maps you can define packages with the following HTML:
<script type="importmap">
{
"imports": {
"svelte": "https://jspm.dev/svelte@3"
}
}
</script>
then any module within that HTML page can import the package by name:
// Statically:
import svelte from 'svelte';
// Or Dynamically:
import('svelte').then(react => console.log(react));
For packages with subpath modules, import maps also allow defining subpath maps:
<script type="importmap">
{
"imports": {
"svelte": "https://jspm.dev/svelte@3",
"svelte/": "https://jspm.dev/svelte@3/",
}
}
</script>
which will then allow any subpaths to be imported by name:
import store from 'svelte/store';
import compiler from 'svelte/compiler';
Enabling Import Maps in Chrome
To enable import maps in a Chrome / Chromium browser, navigate to chrome://flags
, or copy the URL below:
chrome://flags/#enable-experimental-web-platform-features
Select the Experimental Web Platform Features feature to Enabled, relaunch, and you're good to go.
Polyfilling Import Maps with ES Module Shims
To support import maps in all modern browsers, ES Module Shims provides a performant shim based on a Web Assembly lexer for fast import specifier rewriting.
This can be included from jspm.dev with the followinng HTML, and the import map defined instead by "importmap-shim"
:
<script type="module" src="https://jspm.dev/es-module-shims"></script>
<script type="importmap-shim">
{
"imports": {
"svelte": "https://jspm.dev/svelte@3",
"svelte/": "https://jspm.dev/svelte@3/",
}
}
</script>
When using ES Module Shims, modules can be imported statically with "type": "module-shim"
or dynamically with importShim()
:
<script type="module-shim">
// Statically:
import svelte from 'svelte';
// Dynamically:
importShim('svelte/store').then(store => console.log(store));
</script>
ES Module Shims uses a very fast Wasm-based lexer for rewriting JS import statements only and will know to skip jspm.dev source processing, resulting in a minimal performance cost.
Package Optimization
All packages on jspm.dev are optimized served with a RollupJS code splitting build.
Packages with only a main entry point will be loaded as a single module served at the direct URL of the package - https://jspm.dev/npm:pkg@x.y.z
.
For packages with multiple entry points or subpaths, each of those package subpaths are optimized, with private non-public internal modules combined into chunks to minimize the number of dependencies loaded.
Source maps are included to map back to the unoptimized file structure.
To control which entry points are exposed in this way, the "exports"
field can be used to define what is optimized by jspm.dev.
Packages without an "exports"
field get their exports inferred by a statistical analysis approach. Whenever possible the "exports"
field is the preferred way to define subpaths for published packages.
Exports Field
Libraries published to npm can use the "exports"
field to define what entry points to expose and to which environments, and jspm.dev will optimize these with a RollupJS code splitting build.
Exports support in jspm follows the exact features of the Node.js ECMAScript modules implementation.
Main Entry Point
The base case is to define the main entry point in exports in the package.json file via:
{
"exports": "./main.js"
}
If not using "exports"
, jspm.dev will fall back to the "main"
, like in Node.js.
Both the leading
./
and the explicit file extension are important to include with the exports field.
Multiple Entry Points
If there are multiple entry points, these can be defined as a map, with the "."
export for the main:
{
"exports": {
".": "./main.js",
"./feature": "./feature.js"
}
}
The above will support import 'pkg'
and import 'pkg/feature'
for consumers in Node.js and the browser (or via //jspm.dev/pkg
and //jspm.dev/pkg/feature
if not using import maps with jspm.dev), and these separate entry points will then be optimized in a RollupJS code splitting build on jspm.dev.
Any entry points not explicitly defined in
"exports"
will throw when attempting to be imported in Node.js. That is, the"exports"
field fully encapsulates the package. It is exactly this encapsulation of the private modules of the package that makes it possible to safely optimize the package by merging these internal modules with a RollupJS code splitting build.
Conditional Exports
To use a different main entry point between Node.js and other environments this can be written:
{
"exports": {
"node": "./main-node.js",
"default": "./main-not-node.js"
}
}
There is also a "browser"
condition, but the benefit of using a "default"
fallback above is that it can also work in e.g. Deno, or other JS environments.
Conditional exports also apply to multiple entry points:
{
"exports": {
".": {
"node": "./main-node.js",
"default": "./main-not-node.js"
}
"./feature": {
"node": "./feature.js",
"default": "./feature-not-node.js"
}
}
}
Other conditions that can be used include "browser"
, "react-native"
, "development"
, "production"
, "require"
and "import"
.
jspm.dev will always resolve to the "browser"
, "development"
, "default"
conditions in exports. "require"
and "import"
as appropriate, as these are defined for Node.js.
Development Workflows
If you have Node.js installed, a local server can be run with npx http-server
, which is then the only step necessary to get going on a simple web application development workflow with native modules - no other tooling is required apart from a text editor.
Alternatively, by using new browsers like Beaker Browser it can be possible to develop web applications in the browser itself without even needing any local CLI tooling at all (and of course you can do this with online editors too, but the decentralized web is far more fun).
These approaches can be a huge saving in avoiding wasted time on complex build tool configurations or starter projects in the early development phase of a web application.
The jspm online sandbox is entirely developed with this type of workflow using Import Maps and the jspm CDN (source).
TypeScript Workflow
TypeScript can be notoriously difficult to get to play nice with native modules in Node.js and browsers. This isn't meant to be a TypeScript tutorial, but here are some brief suggestions to make this process run more smoothly:
npm install typescript
and runtsc -p .
ortsc -p . --watch
to compile, storing this command in the package.json"scripts": { "build": "tsc -p ." }
field to execute vianpm run build
, or however else you want to run it.- Install the type for any dependency via
npm install @types/dep
or create asrc/deps.d.ts
file with manualdeclare module 'dep';
entries to avoid dependency compilation errors. - Set
allowSyntheticDefaultImports: true
for TypeScript to support importing CommonJS modules asimport cjs from 'cjs'
, which is the way they are recommended to be imported in Node.js and jspm.dev. - When importing one TypeScript module from another, use an explicit
.js
file extension likeimport './feature.js'
so that the output file references the exact file to work natively in Node.js and browsers (even though the file is actually atlib/feature.ts
). - Set up a
tsconfig.json
that compiles from asrc
dir to alib
dir for modern syntax with a configuration something like:
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"target": "es2017",
"module": "esnext",
"outDir": "lib"
},
"include": ["src/**/*.ts"]
}
Then run the lib
version directly in the browser or Node.js for a fast universal workflow with good file watching support.
Note: Using the CommonJS "module"
output from TypeScript requires using completely different interop handling. In this case it's advisable to simply forget about trying to get any parity with native ES module semantics and focus on getting the CommonJS build to work, as it's almost impossible to do both with the same inputs. Pick one target or the other and stick with it.
This same sort of src -> lib compilation workflow works well for Babel (
babel src --out-dir lib
) and other compilers. The benefit of single file-to-file transformation is that it supports ideal caching since the file mtimes can be checked to avoid rebuilds, unlike monolithic build processes which require custom cache stores for this.
The other great thing about this type of workflow is that by following simple module semantics its very easy to add RollupJS as an optimization at the end while actually getting the optimal overall build time performance.
Universal Module Semantics
When publishing packages to npm for support on jspm.dev, the basic rule for module semantics is that if it works in Node.js or in a browser then it should work on jspm.dev when published to npm.
Some guidelines for writing universal native ES modules:
- Use explicit file extensions when loading one module from another -
import './dep.js'
instead ofimport './dep'
. - When supporting Node.js, use the
.mjs
extension or set the"type": "module"
field in the package.json for native modules support. - Use the package.json
"exports"
field to define the main entry point and other entry points of the package. - When
"exports"
is not set, the"main"
will be used, just like in Node.js."module"
is not supported as the semantics aren't tested against Node.js module semantics and would likely break many packages (eg due to named exports usage and interop scenarios that work in bundlers but do not work natively). - It is recommended to import CommonJS modules as the default export -
import cjs from 'cjs'
. Named exports likeimport { name } from 'cjs'
are supported for some CommonJS modules on jspm.dev, but whether Node.js will also support these is still being discussed. - To reference asset files relative to the current module, use
new URL('./file.ext', import.meta.url)
to get its URL. This works in Node.js and browsers (and Deno). - When accessing environment-specific globals like
process
in Node.js, always use a guard liketypeof process !== 'undefined'
as they won't necessarily be available in other environments. Ideally, rather import these modules where possible.
Only CommonJS modules will go through a conversion process on jspm.dev - ECMAScript module sources are left entirely as-is (although they will still be fully optimized with RollupJS code splitting).
Modules are resolved as URLs, with the package.json "dependencies"
field used to determine version ranges of package dependencies. Node.js builtin imports like util
are replaced with optimized Browserify library references.
Only dependencies on npm are supported - for other registry types custom private registry installations could be requested.
Assets
jspm.dev will serve the readme, license and typing files as assets.
All other non-JavaScript assets will only be included if they are explicitly referenced using the "exports"
field which will then make them availabile on the CDN, although assets do not support versioned redirects like JS modules so the exact version reference needs to be used (https://jspm.dev/npm:pkg@x.y.z/path/to/asset
).
Folder exports (exports entries ending in /
) also support asset inclusion.
CommonJS Compatibility
Any module which is not an ECMAScript module is treated as CommonJS. ECMAScript modules are detected as files ending in .mjs
, .js
files in a "type": "module"
package.json boundary, or any .js
file with import
or export
syntax.
The following CommonJS compatibility features are provided by the conversion process:
- All CommonJS modules are effectively converted into
export default module.exports
as an ECMAScript module. That is, they should always be imported asimport cjs from 'cjs'
, the default import sugar. - Named exports for CommonJS modules are detected based on applying CJS Module Lexer. This uses a static analysis approach to determine the named exports of a CommonJS module. The
default
export will always remain themodule.exports
instance, even with this named exports assignment process. - CommonJS modules in a cycle get a function-wrapper-based transform that ensures that the cycle references work out according to the CommonJS semantics.
- Comprehensive strict-mode conversion is applied to all CommonJS modules.
Buffer
andprocess
globals are updated to reference the Browserify libraries for these.- Any reference to
global
is rewritten to the actual environment global. __filename
and__dirname
references are rewritten using anew URL('.', import.meta.url)
style expression.- Dynamic
require()
andrequire.resolve
rewriting is not currently supported. - The
"browser"
field is supported as it is in Browserify, but is not supported when the"exports"
field is set.
CommonJS should work the same as it does in Browserify or Webpack. Any bugs can be reported to the main project issue tracker.
For questions or further discussion about jspm, join jspm on Discord.