Require-es
A microfrontend module loader
Installation
Migration from RequireJs
Register a package
Require a package
Override amd dependencies
Require.when - ONLY load after explicit define/register
Define a package
RequireEs options
RequireEs events
WebPack / Rollup bundling for RequireEs
Custom elements support
A microfrontend module loader
Require-es will:
- allow micro-frontends to share dependencies
- help finding compatible package-versions
The project will be structured in 3 parts:
- provide a module loader which:
- loads amd modules
- loads multiple filetypes: js / json / xml / txt / html / css / wasm
- registers multiple versions of a single package
- can set default-versions
- easily migrates from RequireJs
- emits events to allow usage monitoring
- create a require-es-server, where:
- packages can be registered
- usage of package-version can be tracked
- module loading can become predictable
- predictive http2-pushes are possible
- create a webpack/rollup loader
Installation
Add the package to your project
npm install requirees --save
Serve the file in node_modules/requirees/build/requirees.js in the header of your webpage (9Kb Gzipped)
<!DOCTYPE html>
<html>
<head>
<title>Pagetitle</title>
<script src="scripts/requirees.js"></script>
</head>
<body></body>
</html>
Migration from RequireJs
RequireEs allows a soft transition from RequireJs.
This means that most syntax of RequireJs, is also supported in RequireEs:
- define(packageName, [dependencies], factory);
- define([dependencies], factory);
- require(['packageName'], packageInstance => {});
- requirejs(['packageName'], packageInstance => {});
- require('packageName');
- requirejs.config({paths});
In a later version, require.config({shim}) will be supported as well.
More info on RequireJs: https://requirejs.org/
Register a package
Usage
require.register({
packageName1(@)(version)(.filetype)(-default): [
url(.filetype),
url(.filetype),
url(.filetype),
...
],
packageName2(@)(version)(.filetype)(-default): {
versions,
url,
urls
}
});
Key | Description |
---|
PackageName | the name of the package |
Version | the version-number in string format: major.minor(.patch)(.build)(-releaseCandidate) |
Filetype | the file-extension - js/css/txt/xml/json/html/wasm/tag |
Default | indicates the default version |
Versions | array of versions to fill out in the URL (use placeholder ${version} in the url-string) |
Url | single url string |
Urls | multiple urls for 1 package |
Note - Determining the version number, happens in this order:
- Is a 'versions' attribute present in the package-value
- Is '@version' present in the package-name
- Is a version present in the url (cdnjs.com/17.0.2/package.js)
Samples
require.register({
react: 'https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js'
});
require.register({
react: {
versions: ['0.14.9', '15.6.2', '15.7.0'],
url: 'https://cdnjs.cloudflare.com/ajax/libs/react/${version}/react.min.js'
},
'react@16.14.0-default': 'https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js',
'react@17.0.2': 'https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js'
});
require.register({
react: [
'https://cdnjs.cloudflare.com/ajax/libs/react/15.7.0/react.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js',
]
});
require.register({
bootstrap: [
"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js",
"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.css",
"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.6.0/js/bootstrap.bundle.min.js",
"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.6.0/css/bootstrap.min.css"
]
});
Require a package
Usage
const packageInstance = await require('packageName(@)(^)(~)(*)(version)(.filetype)');
const [pckg1, packg2] = await require([
'pckg1(@)(^)(~)(*)(version)(.filetype)',
'pckg2(@)(^)(~)(*)(version)(.filetype)'
]);
const packageInstance = require('packageName(@)(^)(~)(*)(version)(.filetype)');
requirees('packageName(@)(^)(~)(*)(version)(.filetype)').then(packageInstance);
requirees([
'pckg1(@)(^)(~)(*)(version)(.filetype)',
'pckg2(@)(^)(~)(*)(version)(.filetype)'
]).then(
([pckg1, pckg2]) => {}
);
require(['packageName(@)(^)(~)(*)(version)(.filetype)'], package => {});
require([
'pckg1(@)(^)(~)(*)(version)(.filetype)',
'pckg2(@)(^)(~)(*)(version)(.filetype)'
], (pckg1, pckg2) => {});
await require(url);
Key | Description |
---|
PackageName | the name of the package |
Version | the version-number in string format: major.minor(.patch)(.build)(-releaseCandidate) |
Filetype | the file-extension - js/css/txt/xml/json/html/wasm/tag; if no filetype is specified ALL filetypes will be loaded |
- | load highest version
^ | load highest minor-version
~ | load highest patch-version
Note - Determining the version number happens in this order
- Find best version match, if any versionnumber is specified
- Find the default, if no versionnumber is specified
- Take the highest version number if no default, nor versionnumber are specified
Note - If no filetype is specified all registered filetypes will be loaded
Samples
require.register({
bootstrap: [
"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js",
"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.css",
"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.6.0/js/bootstrap.bundle.min.js",
"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.6.0/css/bootstrap.min.css"
]
})
const bootstrap = await require('bootstrap');
const bootstrap = await require('bootstrap@*');
const bootstrapCssTag = await require('bootstrap.css@3.4.1');
const bootstrapJs = await require('bootstrap.js@^3.0.0');
const bootstrap = await require('bootstrap@3.4.2');
const bootstrap = await require('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.6.0/js/bootstrap.bundle.min.js')
Override amd dependencies
Require-es provides more flexibility on loading multiple versions of a given package.
Unfortunately some amd/umd-packages include predefined dependencies. These can lead to unwanted / unexpected behavior.
###Example: react-dom + react
Let's take a look at the conflict below: react 16 + react 18
require.register({
'react': [
'https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.0.0/umd/react-dom.production.min.js'
],
'react-dom': [
'https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.0.0/umd/react-dom.production.min.js'
]
});
const ReactDom = await require('react-dom@^16.0.0');
Why did react 18.0.0 initialize, when react-dom 16.x is required?
The answer can be found in the react-dom source-code itself:
https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js
...nction"===typeof define&&define.amd?define(["exports","react"],ea):(I=I||self,ea(I.ReactDOM={},...
Pay close attention to the dependencies set by the react-dom definition: define(["exports","react"]).
This instructs require-es to load "react" (without version number specified) before the "react-dom" factory runs.
If no "react" version is specified, the default version will be loaded (if no default is specified, the highest version becomes the default)
Fix hardcoded amd-dependencies
To avoid mixing up versions, require-es allows dependency overrides.
This is done by providing the dependencyOverrides object while registering an amd module.
Syntax
require.register({
packageName: {
url: 'urlToDownloadTheAmdModule',
dependencyOverrides: {
dependencyName: 'newDependencyName(@version)'
}
}
})
Example:
require.register({
'react-dom': [
{
url: 'https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.0.0/umd/react-dom.production.min.js',
dependencyOverrides: { react: 'react@18.0.0' }
},
{
url: 'https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js',
dependencyOverrides: { react: 'react@16.14.0' }
}
],
'react': [
'https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.0.0/umd/react-dom.production.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js'
]
});
This will override any hardcoded dependency on "react" to "react@18.0.0" (or "react@16.14.0") in both react-dom packages.
The react-dom factory will now receive a matching react-version.
Looking back at the react-dom source code:
https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js
...nction"===typeof define&&define.amd?define(["exports","react"],ea):(I=I||self,ea(I.ReactDOM={},...
//will become
...nction"===typeof define&&define.amd?define(["exports","react@16.14.0"],ea):(I=I||self,ea(I.ReactDOM={},...
https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.0.0/umd/react-dom.production.min.js
...nction"===typeof define&&define.amd?define(["exports","react"],eb):(M=M||self,eb(M.ReactDOM={},...
//will become
...nction"===typeof define&&define.amd?define(["exports","react@18.0.0"],eb):(M=M||self,eb(M.ReactDOM={},...
Require.when() - only return explicitly defined/registered packages
By default requirees will always try to download a package, even when no registration/definition is available.
Example
require('myUnkownPackage')
Require.when() ensures the requested package are explicitly defined/registered!
If the requested package is not defined/registered yet, require.when will wait for an explicit define/register.
Usage
require.when('packageName(@)(^)(~)(*)(version)(.filetype)').then(packageInstance => {});
require.when([
'pckg1(@)(^)(~)(*)(version)(.filetype)',
'pckg2(@)(^)(~)(*)(version)(.filetype)'
]).then((pckg1, pckg2) => {});
require.when('packageName(@)(^)(~)(*)(version)(.filetype)', callback, failCallback)
Samples
require.when('myUnkownPackage')
.then(p => console.log('my unknown package is defined now', p))
setTimeout(
() => define('myUnkownPackage', () => exports),
5000
);
require.when('three@^0.100.0')
.then(three => console.log('three is registered and usable now', three))
setTimeout(
() => require.register({
three: "https://cdnjs.cloudflare.com/ajax/libs/three.js/0.148.0/three.min.js"
}),
5000
);
Define a package
Usage (follows AMD-pattern)
define(packageName(@)(version)(.filetype)(-default), [dependencies], factory);
define([dependencies], factory);
Key | Description |
---|
PackageName | the name of the package |
Version | the version-number in string format: major.minor(.patch)(.build)(-releaseCandidate) |
Filetype | the file extension - js/css/txt/xml/json/html/wasm/tag |
Default | indicates the default version |
Dependencies | package names (or urls) on which this package is dependent |
Factory | function / json / text / HTMLElement / xml / ... |
Note - The dependency naming rules are equal to the require naming rules:
packageName(@)(^)(~)(*)(version)(.filetype)
Note - The datatype of the factory can be different, depending on the filetype:
- js: function
- json: json-text (gets converted to JSON) / json-object
- css: css-text (gets converted to a script-tag)
- txt: text
- xml: text
- html: html-text (gets converted to an HTMLElement) / HTMLElement-object
Samples
define('hello.js', ['react@^16.0.0'], (react) => {
function fn1(){}
return {fn1}
});
define('hello.js', [], () => {
const react = require('react');
function fn1(){}
module.exports = {fn1};
});
define('hello.json', [], '{"foo": "bar"}');
define('hello.css', [], 'body{background-color: red}');
define('hello.css@1.0.0', [], 'body{background-color: red}');
define('hello.html', ['hello.css@^1.0.0'], '<div>bla</div>');
define('hello.html', [], document.createElement('div'));
define('react', reactFactoryFn);
define('react@17.0.2', react1702FactoryFn);
RequireEs options
require.config({
allowRedefine: false,
invokeNonMatchedDefines: true
});
Key | Description |
---|
allowRedefine | default: false; false: you cannot change the factory, for a given package (after it gets required for the first time) true: the factory can be changed at any time, next require will use the new factory |
invokeNonMatchedDefines | default: false; automatically invoke anonymous defines which could not be matched to any package in the RequireEs register. |
RequireEs events
Usage
require.on(evtName, callback);
require.subscribe(evtName, callback);
require.publish(evtName, payload);
require.addWireTap(callback);
Predefined events
Event | Trigger |
---|
requirees.pre-define | when define gets called, but the factory is not stored into the registry yet |
requirees.define | when a package factory was added to the registry |
requirees.pre-register | when require.register or require.config({paths}) are called, but the package it not added to the registry yet |
requirees.register | when a package was added to the registry |
requirees.pre-file-load | before an actual file/factory load is happening |
requirees.file-load | when a file/factory load has completed |
requirees.wiretaps | on all events |
requirees.scripttag.preadd | triggers before requirees appends a script-tag to the dom |
requirees.scripttag.added | tiggers after requirees appended a script-tag to the dom |
requirees.styletag.preadd | triggers before requirees appends a script-tag to the dom |
requirees.styletag.added | tiggers after requirees appended a style-tag to the dom |
Samples
requirees.subscribe('requirees.pre-file-load', ({package}) => {
console.log(`start loading package: ${package.name}`);
})
requirees.addWireTap(console.log);
requirees.subscribe('requirees.wiretaps', console.log);
requirees.subscribe('hendrik.sayHello', data => {
console.log('hello!!', data)
});
requirees.publish('hendrik.sayHello', {foo: 'bar'});
WebPack / Rollup bundling for RequireEs
Intro
In the examples below project-dependencies 'jquery', 'react' and 'lodash' will be removed from your bundle and loaded using RequireEs.
To make this happen:
- Add RequireEs to the top of your HTML-document
- Register your dependencies on the document (This will happen automatically using RequireEs-server, coming soon)
- Load your AMD (/UMD) bundle through RequireEs (recommended), or using a script-tag
Samples for Webpack and Rollup bundling can be found in the sections below.
require.register({
jquery: 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js',
react: [
'https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/react/16.10.1/umd/react.production.min.js'
],
lodash: [
'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.min.js'
]
});
await require('/scripts/myLib.amd.js');
If your package is added through a script-tag, call require.config({invokeNonMatchedDefines: true}) to invoke the factory immediately:
<script>
requirees.config({
invokeNonMatchedDefines: true
})
</script>
<script src="/scripts/myLib.amd.js"></script>
Build your bundle AMD style
Webpack
module.exports = {
output: {
libraryTarget: 'amd',
filename: 'scripts/myLib.amd.js'
},
externals: {
jquery: 'jquery@*',
react: 'react@^17.0.0',
lodash: 'lodash@^4.17.0'
},
};
Rollup
export default {
output: {
format: 'amd',
file: 'scripts/myLib.amd.js',
external: ['react', 'jquery', 'lodash'],
paths: {
react: 'react@^17.0.0',
jquery: 'jquery@*',
lodash: 'lodash@^4.17.0'
}
},
};
Build your bundle UMD style
Webpack
module.exports = {
output: {
libraryTarget: 'umd',
filename: 'scripts/myLib.amd.js',
library: 'myLib'
},
externals: {
jquery: {
root: '$',
amd: 'jquery@*',
commonjs: 'jquery',
commonjs2: 'jquery'
},
react: {
root: 'React',
amd: 'react@^17.0.0',
commonjs: 'react',
commonjs2: 'react'
},
lodash: {
root: '_',
amd: 'lodash@^4.17.0',
commonjs: 'lodash',
commonjs2: 'lodash'
}
},
};
Rollup
export default {
output: {
format: 'umd',
file: 'scripts/myLib.amd.js',
name: 'myLib',
external: ['react', 'jquery', 'lodash'],
paths: {
react: 'react@^17.0.0',
jquery: 'jquery@*',
lodash: 'lodash@^4.17.0'
},
globals: {
react: 'React',
jquery: '$',
lodash: '_'
}
},
};
Custom elements support (coming soon)