requirees

1.0.23 • Public • Published

codecov

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

//register react, version '17.0.2'
require.register({
    react: 'https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js'
});

//register react, versions '0.14.9', '15.6.2', '15.7.0', '16.14.0' (set as default), '17.0.2'
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'
});

//register react, versions '15.7.0', '16.14.0', '17.0.2'
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',
    ]
});

//register bootstrap, both css and 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

//async await
const packageInstance = await require('packageName(@)(^)(~)(*)(version)(.filetype)');
//mulitple packages
const [pckg1, packg2] = await require([
    'pckg1(@)(^)(~)(*)(version)(.filetype)',
    'pckg2(@)(^)(~)(*)(version)(.filetype)'
]);

//cjs style (only works when a package is already loaded once, or in a defined factory)
const packageInstance = require('packageName(@)(^)(~)(*)(version)(.filetype)');

//using promises
requirees('packageName(@)(^)(~)(*)(version)(.filetype)').then(packageInstance);
//multiple packages
requirees([
    'pckg1(@)(^)(~)(*)(version)(.filetype)',
    'pckg2(@)(^)(~)(*)(version)(.filetype)'
]).then(
    ([pckg1, pckg2]) => {}
 );

//using callbacks
require(['packageName(@)(^)(~)(*)(version)(.filetype)'], package => {});
//multiple packages
require([
    'pckg1(@)(^)(~)(*)(version)(.filetype)',
    'pckg2(@)(^)(~)(*)(version)(.filetype)'
], (pckg1, pckg2) => {});

//without registration
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

//register bootstrap
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"
    ]
})

//load both css & js from the highest available version (4.6.0)
//4.6.0 will be the default (no default is set)
const bootstrap = await require('bootstrap');
//4.6.0 is the highest registered version
const bootstrap = await require('bootstrap@*');

//load only the css tag from version 3.4.1
const bootstrapCssTag = await require('bootstrap.css@3.4.1'); //fixed version number

//load only the js tag from version 3.x
const bootstrapJs = await require('bootstrap.js@^3.0.0'); //find highest minor

//try to load bootstrap 3.4.2
const bootstrap = await require('bootstrap@3.4.2');
//will result in a console.warn("package not found");
//version 3.4.2 was not registered using define(), require.register, nor require.config({paths})

//download without registration
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');
//react-dom     version 16.14.0 will download/initialize (it's the highest match within major 16)
//react         version 18.0.0 will download/initialize...
//this happens because react-dom has a dependency on 'react' baked into the module itself: define(['react'], factoryReactDom(React){})
//see code-snippet below

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')
//tries to download './myUnkownPackage.js'. 

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

//wait for a single package to get defined/registered
require.when('packageName(@)(^)(~)(*)(version)(.filetype)').then(packageInstance => {});
//wait for multiple packages to get defined/registered
require.when([
  'pckg1(@)(^)(~)(*)(version)(.filetype)',
  'pckg2(@)(^)(~)(*)(version)(.filetype)'
]).then((pckg1, pckg2) => {});
//using callbacks instead of promises
require.when('packageName(@)(^)(~)(*)(version)(.filetype)', callback, failCallback)

Samples

require.when('myUnkownPackage')
        .then(p => console.log('my unknown package is defined now', p))
//waits until someone explicitly defines the package
//the promise above will resolve with the 'myUnknownPackage'-exports after 5 seconds
setTimeout(
    () => define('myUnkownPackage', () => exports),
    5000
);
require.when('three@^0.100.0')
        .then(three => console.log('three is registered and usable now', three))
//waits until someone explicitly registers the package
//
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)

//named define
define(packageName(@)(version)(.filetype)(-default), [dependencies], factory);
//anonymous define
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 an amd module
define('hello.js', ['react@^16.0.0'], (react) => {
   function fn1(){}
   return {fn1} 
});

//define a CJS module
define('hello.js', [], () => {
    const react = require('react');
    function fn1(){}
    module.exports = {fn1};
});

//define a json object
define('hello.json', [], '{"foo": "bar"}');

//define css
define('hello.css', [], 'body{background-color: red}');

//define a specific version of hello.css
define('hello.css@1.0.0', [], 'body{background-color: red}');

//define an html fragment
//gets converted to an HTMLElement when required + will automatically add the CSS to the page
define('hello.html', ['hello.css@^1.0.0'], '<div>bla</div>');
//alternative
define('hello.html', [], document.createElement('div'));

//define r.js style
define('react', reactFactoryFn);
//using version number
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

//subscribe to an event
require.on(evtName, callback);
//require.subscribe is a synonym for require.on
require.subscribe(evtName, callback);

//publish an event
require.publish(evtName, payload);

//spy on all events
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

//listen to all files being loaded
requirees.subscribe('requirees.pre-file-load', ({package}) => {
    console.log(`start loading package: ${package.name}`);
})

//listen to all events being triggered
requirees.addWireTap(console.log);
//alternative:
requirees.subscribe('requirees.wiretaps', console.log);

//create custom events
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 the AMD module
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'
    ]
});

//require the application package
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)

Package Sidebar

Install

npm i requirees

Weekly Downloads

266

Version

1.0.23

License

Beerware

Unpacked Size

204 kB

Total Files

49

Last publish

Collaborators

  • hendrikdp