# js-modules **Repository Path**: mirrors_dsyer/js-modules ## Basic Information - **Project Name**: js-modules - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-03-14 - **Last Updated**: 2026-03-07 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # JavaScript Modules Primer A module system is a tool for encapsulating library code into self-contained chunks that explicitly and deliberately export their public API. JavaScript has had a few different module systems over the years, but things seem to have settled down a bit, and while some of the older attempts are sometimes still seen, there are 3 main options to consider if you are writing a JavaScript library. 1. The newest and shiniest module system is the standard one that is part of the language now: [ECMAScript Modules](https://nodejs.org/api/esm.html), sometimes referred to as ESM, or ES6. It is supported by all modern browsers and recent versions of Node.js. 2. Before ES6 existed a lot of effort went into [CommonJS](https://nodejs.org/api/modules.html). It was *the* module system for Node.js for some time, and nearly all JavaScript libraries support it now. It is not supported natively in the browser. 3. The last option which is worth considering is "no module system". It might upset the purists but often works pretty well in the browser, and a lot of libraries ship this way. We are going to look at a simple example of each of those options, just to see how they work, and then we are going to change the focus to a specific challenge of supporting all 3 in the same library. To make it more interesting, the example for that exercise will have an asynchronous initializer - some work that has to be done before the library API works, but has to be done asynchronously (e.g. by loading some data from a file or remote endpoint). This will lead to some additional unpleasantness and compromises. - [JavaScript Modules Primer](#javascript-modules-primer) - [Simple ES6 Example](#simple-es6-example) - [File Name Hack](#file-name-hack) - [Import Maps](#import-maps) - [Hello CommonJS](#hello-commonjs) - [File Name Hack](#file-name-hack-1) - [Browser Globals](#browser-globals) - [CommonJS and Browser Globals Together](#commonjs-and-browser-globals-together) - [An Asynchronous Initializer](#an-asynchronous-initializer) - [Implementation of Initializer](#implementation-of-initializer) - [Something That Works with CommonJS](#something-that-works-with-commonjs) - [Sort Of...](#sort-of) - [Push the Async Concerns up the Stack](#push-the-async-concerns-up-the-stack) - [Expose the Initializer](#expose-the-initializer) - [CommonJS with an ES6 Wrapper](#commonjs-with-an-es6-wrapper) - [Browserify](#browserify) - [ES6 Core with a CommonJS Wrapper](#es6-core-with-a-commonjs-wrapper) - [Conclusions](#conclusions) ## Simple ES6 Example Here is a simple ES6 module - it defines a function and exports it. We could put it in a file and call it `hello.js`: ```javascript let hello = function() { return 'Hello World'; } export hello ``` You can use it in Node.js as it is but only if you run with a `package.json` that has `type=module`: ```javascript $ cat package.json {"type":"module"} $ node > var hello = await import('./hello.js') > hello() 'Hello World' ``` If you make a `main.js` you can import the module, choosing which of its public API features to expose, and then use it. You don't need the `await` that we used in the REPL because ES6 module imports are by definition asynchronous: ```javascript import hello from './hello.js' console.log(hello()) ``` and we then run it on the command line: ``` $ node main.js Hello World ``` You can also use this simple library in the browser in exactly the same way (start a webserver with `python -m http.server 8000` if you need it): ```html

Month

``` ### File Name Hack With Node.js if you don't have a `package.json` you can use `.mjs` as a file extension. So rename all your `.js` files as `.mjs` and things just work with ES6. But not with CommonJS (it's one or the other). ### Import Maps Modern browsers don't justy support ES6, they also have a way to map the library script locations to their names. This means they can be used with the same `import` in Node.js and in the browser. An example for our simple library would be: ```html

Month

``` It doesn't look all that impressive yet because the module is not defined as 'hello' in Node.js. We could publish `hello.js` as `hello` by moving it to `node_modules/hello/index.js`, and then the exact same code can be used in the browser and on the server. That's what most JavaScript libraries do - they have module names not file paths, and Node.js searches for the right file to load at runtime. ## Hello CommonJS Node.js provides a global `require()` function that you use to load a library using the CommonJS module loader. It wraps the module code with a [wrapper](https://nodejs.org/api/modules.html#the-module-wrapper) and then calls it: ```javascript (function(exports, require, module, __filename, __dirname) { // Module code actually lives in here }); ``` The parameters are: * `exports` are the public API of the module * `require` can be used to pull in other dependencies * `module` is a subset of `exports` * `__filename` and `__dirname` are convenience variables containing the path to the module being loaded We have to re-write our `hello.js`. One simple way to do that would be to add the `hello` function to the `exports`: ```javascript let hello = function() { return 'Hello World'; } exports.hello = hello ``` Using it looks like this: ```javascript $ node > var hello = require('./hello.js'); > hello.hello() 'Hello World' ``` You can't use it in its current form in the browser because there is no `exports` global variable in the browser. ### File Name Hack Node.js recognises `.cjs` as a file extension for CommonJS modules. Thus you can override the behaviour in `package.json` and make sure that a modules can be loaded with `require()` and not `import`. ## Browser Globals What many libraries do in the browser is simply add their public API to the global ("window") namespace. For example: ```javascript let hello = function() { return 'Hello World'; } ``` The `hello` function is in the global namespace so it can be used in a different ` ``` To encapsulate its internal details a library will often wrap its code in a function and then call it. Then you can be more selective about which parts to expose globally. For example: ```javascript (function() { let msg = 'Hello World'; let hello = function () { return msg; } this.hello = hello; })() ``` The assignment to `this` is to the window (global) namespace so it can be used in another ` ``` The `init()` is asynchronous, so `monthFromDate()` is not available until it has finished: ``` [x] Uncaught TypeError: monthFromDate is not a function at month.html:6 (anonymous) @ month.html:6 Initialized months January > monthFromDate('2022-03-23') 'March' ``` No doubt we could find a way to wait for the initialization to finish, but that would expose the details of our library to its users. We had the same problem in Node.js but we didn't see it in the REPL because of the delay while we typed the call to `monthFromDate()`: ```javascript $ cat > main.js var month = require('./months.js') month.monthFromDate('2022-03-23') $ node main.js /home/dsyer/dev/scratch/js-modules/main.js:2 console.log(month.monthFromDate(dateString)); ^ TypeError: month.monthFromDate is not a function at Object. (/home/dsyer/dev/scratch/js-modules/month.js:3:19) at Module._compile (node:internal/modules/cjs/loader:1101:14) at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10) at Module.load (node:internal/modules/cjs/loader:981:32) at Function.Module._load (node:internal/modules/cjs/loader:822:12) at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12) at node:internal/main/run_main_module:17:47 ``` ### Push the Async Concerns up the Stack One solution might be to make `monthFromDate()` asynchronous. E.g. ```javascript $ node Welcome to Node.js v16.13.1. Type ".help" for more information. > var month = require('./months.js') undefined > await month.monthFromDate('2022-03-23') Initialized months January 'March' ``` and in the browser: ```html

Month

``` That forces the user of the library to face the facts, but it seems like it should be unnecessary because `monthFromDate()` as originally implemented is not asynchronous. ### Expose the Initializer We have an asynchronous `init()` function, and the user is going to have to know about it. Sigh: ```javascript let initFunc = async function(exports) { // ... all the code from the old library return init().then( () => { exports.monthFromDate = monthFromDate; return exports; } ) } if (typeof module !== 'undefined' && module.exports) module.exports.init = () => initFunc(exports) else this.init = () => initFunc(this) ``` The user code now looks like this in the REPL: ```javascript > var month = require('./months.js') undefined > await month.init().then(() => console.log(month.monthFromDate('2022-03-23'))) Initialized months January March ``` or in a script: ``` $ cat main.js var month = require('./months.js'); const dateString = process.argv[2] ?? null; month.init().then(() => console.log(month.monthFromDate(dateString))); $ node main.js 2022-03-23 Initialized months January March ``` ## CommonJS with an ES6 Wrapper Starting with the CommonJS version of `months.js`, which already works in Node.js and in the browser like this: ```html

Month

``` We can try and wrap it in an ES6 module: ```javascript var month = require('./months.js') await month.init(); let monthFromDate = month.monthFromDate; export {monthFromDate}; export default monthFromDate ``` but it doesn't work because there is no `require()` function in an ES6 module. There's one last thing to try -- we can implement a module loader. Here is the code to read the CommonJS module as a raw string, and use `Function` to evaluate it: ```javascript let month = {}; var script; if (typeof fetch === 'undefined') { script = await import('fs').then(fs => fs.readFileSync('./months.js', {encoding: 'utf8'})); } else { script = await fetch('./months.js').then(response => response.text())); } Function('return function(module, exports) {\n' + script + '\n}')()({exports:month}, month); await month.init(); let monthFromDate = month.monthFromDate; export {monthFromDate}; export default monthFromDate ``` This works in `main.mjs` and in the browser: ```javascript

Month

``` It would break if the module we required itself needed to `require()` another module. We could try and fix that by defining our own `require()` function using the code above, but sadly this will not work because it has to be asynchronous (as can be seen from the presence of `await`), while CommonJS `require()` is synchronous. In Node.js there is a "module" built-in package that saves the day: ```javascript await import('module').then(module => globalThis.require = module.createRequire(import.meta.url)); let month = require('./months.js'); await month.init(); let monthFromDate = month.monthFromDate; export {monthFromDate}; export default monthFromDate ``` but this won't work in the browser. ### Browserify There is a Node.js tool called [browserify](https://github.com/browserify/browserify) that we can use to build a `require()` function for the browser. ``` $ npm install --no-save browserify $ node_modules/browserify/bin/cmd.js -r ./months.js -s bundle > bundle.js ``` Then in a browser you could do this: ```html

Bundle

``` It works because the generated `bundle.js` defines a `require()` function to load the named modules by running the code in the input source files. We can use that `bundle.js` in our ES6 wrapper: ```javascript let month; if (typeof fetch === 'undefined') { await import('module').then(module => globalThis.require = module.createRequire(import.meta.url)); month = require('./months.js'); } else { await fetch('./bundle.js').then(response => response.text()).then(script => month = Function(script + ';\nreturn bundle;')() ); } await month.init(); let monthFromDate = month.monthFromDate; export {monthFromDate}; export default monthFromDate ``` This will work in Node.js and in the browser. Another approach that works is to use browserify to define a global `require()` in the browser: ``` $ node_modules/browserify/bin/cmd.js -r ./months.js -r ./hello.js | sed -e 's,"/,"./,g' > bundle.js ``` > NOTE: We had to fix the generated code with `sed` because it strips the leading `.` from relative module names. and then in `months.mjs`: ```javascript if (typeof fetch === 'undefined') { await import('module').then(module => globalThis.require = module.createRequire(import.meta.url)); } else { await fetch('./bundle.js').then(response => response.text()).then(script => globalThis.require = Function(script + ';\nreturn require;')() ); } let month = require('./months.js'); await month.init(); let monthFromDate = month.monthFromDate; export {monthFromDate}; export default monthFromDate ``` ## ES6 Core with a CommonJS Wrapper The slightly good news is that ES6 `import` is asynchronous by definition so if we re-arrange the library into an ES6 module `months.mjs` (same as at the beginning) users can just import it. I.e. ES6 users can still use it just like before but the common folk will need their own wrapper (`months.js`): ```javascript let initFunc = async function (exports) { return import('./months.mjs').then( months => { exports.monthFromDate = months.monthFromDate; return exports; } ) } if (typeof module !== 'undefined' && module.exports) module.exports.init = () => initFunc(exports) else this.init = () => initFunc(window) ``` Then they can deal with the asynchronous initializer explicitly: ```javascript > var month = require('./months.js') > await month.init().then(() => console.log(month.monthFromDate('2022-03-23'))) Initialized months January March ``` It works in the browser with the global namespace: ```html

Month

``` but it also works (better) as an ES6 module: ```html

Month

``` ## Conclusions We learned about 3 different ways to load modules in Javascript, and saw how each of them works (or doesn't work) in Node.js and in the browser. ES6 is part of the language specification, and is fully asynchronous, which turns out to be an advantage. CommonJS and plain browser globals are synchronous and that creates problems for libraries with asynchronous initializers. To create such a library that works in both Node.js and the browser is a challenge, and involves compomises. We also looked at 2 different ways of supporting the same library for all 3 module loaders - it is preferable to write the main library in ES6 and provide a wrapper into the CommonJS and browser globals, otherwise some extra tooling has to be introduced to generate code for the browser.