jspm

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!

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/pkgLoad the main entry point of a package at the latest version.
jspm.dev/pkg@1Load the latest ^1 release of the package (includes prereleases).
jspm.dev/pkg@1.2Load 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@tagLoad 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/subpathLoad 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:

{
  "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:

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:

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.

Edit