Overview
extmod
is a CLI, runtime wrapper and API that allows Node applications to dynamically load remote ESM modules at runtime. To put it simply, extmod
enables http(s)-based import statements within Node applications.
extmod
leverages already existing Node machinery, namely ESM module loaders and module permissions, to give both stability and security to the modules
you are loading into your application.
extmod
also updates your modules in real-time, driven by etags or max-age Cache-Control headers, to enable the freshest code to be used, all out of the box.
Use extmod
to:
- Pull remote configuration files, ala feature flagging.
- Leverage shared, common code in your organization.
- Automatically bundle and serve frontend components in SSR/RSC applications, ala Federated Modules.
Getting Started
Node 18.x or greater is required
Installation
Initialization
The next step is to generate a .extmod.json
configuration file.
Allow Remotes
By default, remote modules are not enabled. You need to opt-in URLs that you have deemed safe to run within your application.
To allow a remote URL to be loaded, you can use the config add-remote
command.
Import Remotes
You can now import your remote module in your application as you would any other ESM module.
Application Bootstrap
The final requirement is to leverage the extmod run
command wrapper when you bootstrap your application.
extmod
will pass through anything you add after "run" to Node, however it will inject runtime flags which enable
the extmod
ESM loader as well as apply the module permissions policy you have defined.
Mechanisms of Action
extmod
goes through various stages depending on the modules being requested, their ESM cache status and the module contents.
Resolving
When extmod
finds an import starting with /^https?/
, it will automatically make a HEAD request to that
module in order to determine whether:
- It has seen this specific module before.
- It has a changed
etag
value, indicating it has changed. - It has a
max-age
Cache-Control header.
Depending on the answers to the questions above, extmod
will either load the existing module in the local ESM
cache or it will make a GET request for an updated verison of the module.
If no etag or max-age Cache-Control header is found, once loaded, the remote module will be cached within the ESM cache indefinitely. There is currently no way to "bust" or remove a loaded ESM module from the ESM cache, so the cache will grow unbounded if not restricted.
By default, bare module imports (such as "react" or "lodash") are resolved locally. This aligns with how most bundlers work with "externals" defined.
You should treat bare module imports in your remote modules as being external, where extmod
will look to the local project for that dependency. If you wish for your remote modules to leverage different versions other than what
is supplied locally, you should pre-bundle your module with the dependency.
Permissions
Pulling content from potentially untrusted sources has inherent security risks. extmod
tries to allievate this concern by leveraging Node's
module permission infrastructure, which validates only approved modules when they are loaded by the runtime, and rejects all others.
Remote modules are not enabled by default. You must use the extmod
CLI's config actions to add allowable remote module URLs (or domains) that you trust
in order for them to be loaded into your applications.
See the Configuration section on how to do this.
Loading
After it has been determined a remote module should be loaded, a GET request against the module URL is made. Upon successful request, the remote module is fed through the same import process
Error Handling
The points of failure in the resolution and loading process should be well known in order to guard effectively in your applications. extmod
tries to
make handling these points of failure easier by bubbling up the error into your application code as data, rather than as a bonefide error.
There are two primary classes errors: network errors (both in resolving and loading modules) and module errors (loading a CommonJS vs ESM module for instance).
Handling these errors involves using the extmod
supplied error key and determining whether an error exists within your application code.
Server Side Rendering
Since extmod
is leveraging normal Node import
machinery, all imported modules are assumed to be tailored for a server environment by default.
However, with the rise of Server Side Rendering frameworks, the lines have been blurred on what imported code is being ran on the server and ran
on the client (browser).
Currently, only React 18+ is supported.
When extmod
detects an import is to be used in a browser environment, it will automatically write it and any imported dependencies to disk, bundle
them (via esbuild) and make it available to the browser for rendering.
Import Signaling
There are two ways to trigger client bundling through extmod
, import attributes or the "use client"
directive in the remote module.
Import Attribute
A new feature of ECMAScript allows attributes to be attached to import statements in order to inform the ESM loader of particularities around what
is being imported. extmod
leverages this with a new client
attribute to singal the loader that the imported module should be treated as a client
module.
"use client" Directive
React and frameworks have aligned on the "use client"
Javascript directive to describe the server/client boundry in Server Side Rendering. extmod
looks
for this directive in remotely loaded modules to enforce that boundry and create the client bundle dynamically.
You do not add "use client" in your application code, rather you define it in your remote module and
extmod
will detect it automatically during runtime.
Suspense + Bundling
In order for a seamless UX, extmod
uses React's Suspense
to pause component rendering while the bundling process occurs; the frontend should continue rendering unabated.
When you declare a client import, extmod
immediately returns a default export containing a Suspense
-based component that will resolve into a <script>
tag
containing the location of your bundle on disk.
Client-side Utility
An optional but encouraged step is to use the exported ExtmodSuspense
component. After your client side component has successfully resolved to
a <script>
and the bundle has been loaded into the browser, you need a way to ultimately render that component in-place, including adding an optional
fallback.
ExtmodSuspense
uses a MutationObserver
to watch when a <script>
tag is rendered within it and then replaces that <script>
tag with the
bundle code that was loaded into the browser - all completely automatically.
Configuration
extmod
configuration lives in a .extmod.json
file at the root of your project and drives key features. Because of the complexity of
managing module permissions, it is advised to use the extmod
CLI unless you are versed in how Node module permissions are used. That being said,
extmod
uses an opinionated configuration of the module permission spec speficially tailored for remote modules.
For further information on the mechanics about how Node module permissions work, consult the Node documentation.
The
.extmod.json
configuraiton strictly validated before any CLI command is ran. To avoid errors, do not manually change the configuration file. Rather, use the CLI to perform operations against the configuraiton file.
CLI
The extmod
CLI is helpful in managing it's configuration and running programs to enact said configuration.
init
Create a new .extmod.json
configuration file with defaults. Note, remote modules are disabled by default.
ls
List out the current configuration in an easily consumable format within the console.
flag | default | description |
---|---|---|
-p, --path | process.cwd() | Path to an .extmod.json file |
config
Config subcommands aid in the management of the extmod
configuration file.
add-remote
Add a remote module path. This allows any resources under the given URL to be imported into your application.
flag | default | description |
---|---|---|
-p, --path | process.cwd() | Path to an .extmod.json file |
-a, --alias | Add an optional alias to the remote. Makes working with the remote via the CLI easier | |
-f, --force | false | Force the creation/replacement of the module entry |
update-remote
Update the remote URL via the original URL or the alias.
flag | default | description |
---|---|---|
-p, --path | process.cwd() | Path to an .extmod.json file |
delete-remote
Delete a remote URL via the original URL or the alias.
flag | default | description |
---|---|---|
-p, --path | process.cwd() | Path to an .extmod.json file |
run
Run a Node command with extmod
enabled.
flag | default | description |
---|---|---|
-p, --path | process.cwd() | Path to an .extmod.json file |
-ll, --log-level | info | Set the desired log level emitted from extmod . Possible options: "error", "warn", "info", "debug" |
-lo, --log-output | text | Set the desired log output format. Possible options: "text", "json" |
--cacheDir | process.cwd()/.extmod | The desired location for the extmod cache directory |
--resolverTimeoutMs | 30000 | The amount of time, in ms, to try and resolve a remote module |
--loaderTimeoutMs | 30000 | The amount of time, in ms, to try and load a remote module |
--ignoreWarnings | false | Supress Node experimental warning messages |
validate
Validate an extmod
configuration file. Outputs an error and description if configuration is malformed.
flag | default | description |
---|---|---|
-p, --path | process.cwd() | Path to an .extmod.json file |
API
The externally facing API of extmod
is quite simple and usually not needed.
extmod
The exported extmod()
function is available for situations where using the normal import
Node semantics are not ideal. This is usually
done in cases where the application is using a bundler that transforms import
into require
or statically evalutes import
at build-time
and is not equipt to handle remote imports.
Under the hood, extmod()
simply wraps the normal import
in an eval
to "hide" until runtime, to which things perform as expected.