remote-modules
v0.37.1
Published
A module manager for distributed applications
Downloads
26
Maintainers
Readme
Remote Modules
Contents
Installation
NOTE: remote-modules
should be installed as a dependency.
Install with yarn:
yarn add remote-modules
Install with npm:
npm install remote-modules
Introduction
remote-modules is a full service JavaScript module manager. It's universal, so it will run both in browser and on server. It's comprised of several components that are used to compile, serve, fetch, and execute modules dynamically, on demand, over HTTP, even server-side.
Remember, never use remote-modules to load untrusted code. It's poor form, and even in a sandbox it can have terrible consequences, especially on your server. Just don't do it.
Why Remote Modules?
UI frameworks come in all shapes and sizes, but they all have one common dependency: context. This makes perfect sense considering browsers are designed to load and execute JavaScript on demand, but it also means UI apps that make use of SSR (server-side rendering) are hopelessly monolithic. To be sure, there are many ways to break things up - you can publish shared components or even entire pages as packages that are installed by the shell, but at the end of the day, even the smallest change means a full deploy.
remote-modules empowers you to build truly distributed, independently deployable applications. Its relevance extends far beyond the UI - any component or plugin based application is a great candidate. We'd love to hear how you're using remote-modules to solve your most frustrating architecture problems.
Stability and Security
remote-modules is still in the very early stages of development, which means there will be big changes coming as we gather feedback and work to stabilize the API. We don't plan on making any breaking changes or adding significant features before the first major release, except for low-hanging fruit or some of the larger pain points. Bugs and vulnerabilities will be patched as they arise, and releases will adhere to semver rules.
Rank | Description ---- | ----------- | Don't use this feature - it's no longer supported and will likely be removed | There may be fundamental changes to this feature | There may be breaking changes to this feature | There may be backward compatible changes to this feature
API Documentation
Example App
Check out the example app to see remote-modules in action.
Getting Started
Before you try any of the examples in the README, be sure to:
- Install remote-modules; and
- Add
node_modules/.bin
to your PATH (you will need to do this in each terminal window):
export PATH=$PATH:$PWD/node_modules/.bin
remote-modules works out of the box with CommonJS or ES6 modules. Let's start with two files:
remote.js
export default 'Hello, world!';
client.js
const Client = require('remote-modules').default;
const client = new Client({ uri: 'http://localhost:3000' });
client.import().then(({ default: hello }) => {
console.log(hello);
});
Open a terminal window and run:
> remote-modules start remote.js
This does two things:
- It compiles your modules, using
remote.js
as the entrypoint; and - It starts the server that will field requests from
client.js
If you're curious, you can open .remote/@default
to see the output (@default
is the scope).
Now, open another terminal window and run:
> node client.js
Hello, world!
🎉 You just loaded your first remote module.
Key Features and Concepts
Hot Swapping
One of the most powerful features of remote-modules is the ability to hot swap server-side code.
remote.js
export default Math.random();
client.js
const Client = require('remote-modules').default;
const client = new Client({
ttl: 0,
uri: 'http://localhost:3000'
});
setInterval(() => {
client.import().then(({ default: n }) => {
console.log(`Got a new random number: ${n}`);
});
}, 100);
By default, the client caches modules for 5m before making a request for fresh content. Setting ttl: 0
forces a new request on each import call.
> node client.js
Got a new random number: 0.7270614311072565
Got a new random number: 0.35143236818184165
Got a new random number: 0.3269304992507207
...
Static Evaluation
remote-modules statically evaluates your modules at compile time to eliminate dead code from the output. In many cases you can significantly reduce payload size by telling the compiler how to evaluate certain expressions.
remote.js
const Path = require('path-browserify');
function getCwdDirname() {
return Path.dirname(process.cwd());
}
let cwdDirname;
if (process.browser) {
cwdDirname = getCwdDirname();
}
if (typeof cwdDirname !== 'undefined') {
require('querystring-es3');
}
export default cwdDirname;
client.js
const Client = require('remote-modules').default;
const client = new Client({ uri: 'http://localhost:3000' });
client.import().then(exports => {
console.log(exports);
});
First, start the server by running:
> remote-modules start remote.js -d 'process.browser=true'
Now, run the client script:
> node client.js
{ default: '/path/to/directory/above/cwd' }
Note that path-browserify
and querystring-es3
are loaded as well. Next, stop the server and restart it with:
> remote-modules start remote.js -d 'process.browser=false'
And run the client script again:
> node client.js
{ default: undefined }
This time no dependencies were loaded. Given process.browser === false
, the compiler had enough information to distill remote.js
down to:
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
Scope
Scope allows you to create cascading configurations and tailor output for a specific runtime. Until now we've been using the CLI to configure remote-modules, but generally you'll want to use a .modulerc
file. You can run remote-modules print-config
to inspect the full config object for each scope.
.modulerc.js
module.exports = ({ Scope }) => ({
entry: 'remote',
[Scope('browser')]: {
preset: 'browser'
},
[Scope('node')]: {
preset: 'node'
}
});
remote.js
export default `Hello from ${process.browser ? 'browser' : 'node'}!`;
client.js
const Client = require('remote-modules').default;
const client = new Client({ uri: 'http://localhost:3000' });
client.import(`<@${process.env.SCOPE}>`).then(exports => {
console.log(exports);
});
This time you can start your server without specifying an entrypoint, since we already did so in .modulerc.js
:
> remote-modules start
Notice there are now two sets of output - one for @browser
and another for @node
. When you run the client this time you'll need to set a SCOPE
environment variable to specify which scope you want to import:
> SCOPE=browser node client.js
{ default: 'Hello from browser!' }
> SCOPE=node node client.js
{ default: 'Hello from node!' }
Also notice the < ... >
in the import request - this is the request format for working with multiple scopes. You can find a more practical use case for namespaces and scopes in the example app. The general format of an import request is:
[<[namespace/]@scope>][request = entrypoint]
Request Attributes
Request attributes are primarily used to reference static assets. Consider a project with the following structure:
project
├─┬ src
│ ├── index.jsx
│ └── styles.css
└─┬ img
└── image.jpg
src/styles.css
.image-scoped {
background-image: url(../img/image.jpg);
}
.image-static {
background-image: url(<static>../img/image.jpg);
}
src/index.jsx
import React from 'react';
import './styles.css';
export default function MyComponent() {
return (
<div>
<img src={import('<href>../img/image.jpg')} />
<img src={import('<static>../img/image.jpg')} />
<div className="image-scoped" />
<div className="image-static" />
</div>
);
}
Both the href
and static
attributes tell the compiler that these import(...)
calls should be replaced with URLs. href
will be transformed to a scoped URL, and static
will always be transformed to the same URL regardless of scope. Note the <href>
is implied when referencing assets from stylesheets. The output might look something like:
function MyComponent() {
return React.createElement("div", null, React.createElement("img", {
src: "/@scope/_/:./img/image.jpg"
}), React.createElement("img", {
src: "/@static/img/image.jpg"
}), React.createElement("div", {
className: "image-scoped"
}), React.createElement("div", {
className: "image-static"
}));
}
FAQ
Does it work with TypeScript?
¯\_(ツ)_/¯
If it doesn't already,
v1
will.In the meantime, assuming you already have @babel/preset-typescript setup, you should be able to enable TypeScript support by adding the following to your
.modulerc
:module.exports = ({ ScriptAdapter }) => ({ adapters: [{ test: ({ extension }) => /\.tsx?$/.test(extension), adapter: ScriptAdapter }], babylon: { plugins: ['typescript'] }, extensions: ['.ts', '.tsx'] });