SystemJS

A hookable, standards-based module loader that runs System.register modules and import maps in browsers back to IE11.

Library
npm
v6.15.1
13,092stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
60/100Good
Development Activity20
Maintenance44
Community76
Maturity60
Momentum40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
73/100Good
Architecture78
Code Quality65
Innovation82
Learning Curve65

SystemJS is a dynamic module loader that runs code written in the System.register format across modern and legacy browsers alike, supporting live bindings, circular references, dynamic import, top-level await, and import maps back to IE11. It ships as three purpose-built builds: a 2.8KB s.js loader for minimal production footprints, a 4.2KB system.js loader with tracing hooks and pluggable module type support (CSS, JSON, Wasm), and a system-node.cjs build for running System modules server-side in Node.js SSR workflows.

Because every loader hook is overridable, SystemJS underpins several other tools rather than competing with them directly: jspm.org uses it as the backwards-compatibility layer for its package manager, single-spa relies on it to load framework-agnostic microfrontends, and Rollup’s code-splitting output can target the System.register format directly for production-grade dynamic loading in older browsers.

What You Get

  • Three loader builds (s.js, system.js, system-node.cjs) sized for different deployment targets, from a 2.8KB CSP-friendly loader to a full Node.js SSR loader
  • Import maps support via <script type="systemjs-importmap">, including scoped and dynamic import maps
  • Pluggable extras: AMD interop, named-register bundles, global-script loading, and CSS/JSON/Wasm module types
  • Tracing hooks (System.onload) and a registry deletion API for hot-reload and reloading workflows
  • IE11 compatibility for live bindings, circular references, and top-level await, given Promise/fetch polyfills

Common Use Cases

  • Loading Rollup code-splitting builds in production without needing native ES module support
  • Providing the backwards-compatibility loader beneath package managers like jspm
  • Loading independently-deployed microfrontends framework-agnostically (single-spa)
  • Server-side rendering System modules in Node.js via the system-node.cjs build

Under The Hood

Architecture SystemJS’s execution flow begins in System.import() (src/system-core.js), which resolves an id through the pluggable loader.resolve hook, creates or reuses a Load record, and drives it through instantiate/link/execute stages that support circular references, live bindings, and function hoisting; system-core.js intentionally ships with no resolve or instantiate implementation, deferring both to override points installed by other modules. Each capability — script/fetch loading, URL/import-map resolution, worker loading, dependency pre-caching, global-script interop, AMD, named-register, module-type parsing — lives in its own file under src/features/ or src/extras/ and simply monkey-patches methods onto the shared systemJSPrototype exported from system-core.js, so the three shipped builds (s.js, system.js, system-node.cjs) are just different combinations of these feature imports assembled by src/s.js, src/system.js, and src/system-node.js and bundled per target in chompfile.toml. This gives a genuinely pluggable, hook-based architecture with a clean core/feature separation, though it relies on global prototype mutation and Symbol-keyed instance state rather than explicit composition, so changing the shape of systemJSPrototype or the Load record in system-core.js would ripple through every feature file that patches it.

Tech Stack SystemJS ships zero runtime dependencies — package.json lists no dependencies, only devDependencies for the build/test pipeline (rollup@2, @rollup/plugin-json/node-resolve/replace, rollup-plugin-terser/terser@5 for minification, @vercel/ncc@0.34 to bundle the Node build, mocha@7 for tests, bluebird and whatwg-fetch/node-fetch as legacy-environment polyfills for testing IE11 code paths). All of it is orchestrated by Chomp (chompbuild.com) via chompfile.toml, which defines rollup targets per build (dist/system.js, dist/s.js, dist/extras/*.js as IIFE bundles with process.env.SYSTEM_PRODUCTION/SYSTEM_BROWSER replacements), a terser task for the .min.js/.map outputs (ES5 target, Safari 10 safe), and an ncc task that produces the single-file dist/system-node.cjs for Node.js SSR use. There is no TypeScript, no application framework, and no database — the entire deployment target is static browser/Node JS files published straight from dist/.

Code Quality Tests are split between Mocha-based unit tests (test/system-core.mjs, test/url-resolution.mjs, test/import-map.mjs, test/system-node.mjs, run via Node’s built-in assert module) and browser integration tests (test/browser/*.js, served through a custom test/server.mjs and loaded via test.html/test-base-href.html) covering AMD, dynamic import maps, named exports/register, and worker loading; CI (.github/workflows) runs on Windows and Ubuntu on every push/PR, plus a separate size-impact-reporting job. There is no TypeScript and no ESLint/Prettier config in the repo, so type safety and lint enforcement are absent; error handling instead centers on a small, deliberate convention (src/err-msg.js) that throws Error objects annotated with a stable numeric code and a link into docs/errors.md, letting production builds strip verbose messages (process.env.SYSTEM_PRODUCTION) while keeping errors traceable. Naming is terse and consistent with the vanilla ES5-target style (function-based prototypes, no classes), appropriate for a size-budgeted loader but not enforced by any static tooling.

API Design The public surface is deliberately small: System.import(), System.register(), and a handful of overridable hooks (resolve, instantiate, locate, fetch, createContext) that every extra and feature builds on rather than exposing a bespoke plugin API of its own. Getting started requires no build step at all — drop in a script tag and call System.import() — while advanced usage (custom hooks, import maps, module types) is documented with runnable HTML/JS snippets across docs/api.md, docs/hooks.md, docs/import-maps.md, and docs/system-register.md. The tradeoff for that minimalism is that error messages are terse numeric codes (errMsg) requiring a trip to docs/errors.md to fully diagnose, and there’s no TypeScript typings shipped in the package itself.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search