Namespaces in JavaScript

I am very impressed by namespaces in programming languages ​​such as Java and PHP. So much so that I even somehow wrote an article about them on Habré. Almost two years have passed since then, but namespaces have not appeared in JavaScript during this time. " And if I did namespaces in JS for myself, what would they be? " - I thought. Under the cut - my thoughts, what namespaces I need in JavaScript.

Introductory

All my reasoning below applies to ES6 modules and does not touch on other formats (AMD, UMD, CommonJS) simply because I'm interested in seeing where JavaScript is going, not where it was . Also, in my practice, I somehow ran into GWT quite closely, after which I developed a persistent rejection of various transpilers (as well as, to a heap, minifiers and obfuscators). Therefore vanilla JS and no TS. Well, I have such items.

ES6 modules

An ES module is a separate source file that explicitly defines the elements available outside the module:

export function fn() {/*...*/}

Thus, to begin with, you need to somehow address individual ES modules within the entire application.

Packages

Modern applications are made up of individual packages, the dependencies between which are managed by package managers. And the packages themselves consist of separate modules. An individual vendor can split his code into multiple packages, using the same packages in his various applications. The source code of modules is usually placed in a separate directory within the package (for example ./src).

Package managers put all packages for one application into a directory node_modules. Thus, the structure of a nodejs application compiled from packages of a certain developer may look something like this:

* node_modules
    * @vendor
        * package1
            * src
                * module1.js
                * ...
                * moduleN.js
        * ...
        * packageN
            * src
                * module1.mjs
                * ...
                * moduleN.mjs

Module addressing

In the file structure, the source code of any ES module is addressed by the path to the file relative to the application root:

./node_modules/@vendor/package1/src/module1.js
...
./node_modules/@vendor/packageN/src/moduleN.mjs

As part of a nodejs app module loader, part  ./node_modules/ goes away:

import SomeThing from '@vendor/package1/src/module1.js';

, , :

import SomeThing from './module1.js';

web- , web-  node_modules, web- ES-, , nodejs:

<script type="module">
    import {fn} from './@vendor/package1/src/module1.js'
    fn();
</script>

:

<script>
    import('./@vendor/package1/src/module1.js').then((mod) => {
        mod.fn();
    });
</script>

, web'  ./  . :

import {fn} from '@vendor/package1/src/module1.js'

:

Uncaught TypeError: Failed to resolve module specifier "@vendor/package1/src/module1.js". Relative references must start with either "/", "./", or "../".

, ES-:

  • ( ): ./module1.js

  • (nodejs): @vendor/package1/src/module1.js

  • (web): ./@vendor/package1/src/module1.js

./ nodejs-, ./ .

, JS- , , ( - ) , ( ).

" " ( , namespace'), ES- ( ), ES- , , nodejs, .

,  ./, , ( , ):

@vendor/package1/src/module1

- : ./src/./lib/./dist/. - , , :

@vendor/package1/module1

, , .

Namespace mapping

, , . - web-,  node_modules  web- ( - ./packages/):

const node = {
    '@vendor/package1': {path: '/.../node_modules/@vendor/package1/src', ext: 'js'},
    '@vendor/packageN': {path: '/.../node_modules/@vendor/packageN/src', ext: 'mjs'},
};
const browser = {
    '@vendor/package1': {path: 'https://.../packages/@vendor/package1/src', ext: 'js'},
    '@vendor/packageN': {path: 'https://.../packages/@vendor/packageN/src', ext: 'mjs'},
};

Module loader

, '' ( @vendor/package1/module1) ( - ) (node ):

@vendor/package1/module1 => /.../node_modules/@vendor/package1/src/module1.js       // node
@vendor/packageN/moduleN => https://.../packages/@vendor/packageN/src/moduleN.mjs   // browser

and use it to dynamically import modules. Of course, there is no need to map every module in the package - you just need to map the root of the package. The output is something like this:

const loader = new ModuleLoader();
loader.addNamespace('@vendor/package1', {path: '/.../node_modules/@vendor/package1/src', ext: 'js'});
// ...
loader.addNamespace('@vendor/packageN', {path: '/.../node_modules/@vendor/packageN/src', ext: 'js'});
const module1 = await loader.import('@vendor/package1/module1');

The import of modules must be asynchronous, since an asynchronous function will be used inside import().

Summary

In such an elegant way, it would be possible to switch from physical addressing of ES modules during import to their logical addressing (namespaces) and use the same modules both for nodejs applications and in the browser. Nothing new has been invented here ( something similar has already been done in PHP, where this idea is stolen from).




All Articles