Loading TOC...
JavaScript Reference Guide (PDF)

JavaScript Reference Guide — Chapter 4

Converting JavaScript Scripts to Modules

Evaluating a JavaScript program may generate side-effects on the JavaScript global environment; therefore, each JavaScript program is evaluated in a separate v8 context. The overhead of creating such a context is significant, and in the recent v8 version that overhead has increased by roughly 40%.

To compensate for this overhead, it is suggested that you convert your JavaScript scripts to JavaScript modules.

Benefits of JavaScript Modules

In addition to the performance improvements, there are some other side benefits to using JavaScript modules, such as:

  • Modules may be executed any number of times, but are loaded only once, thus improving performance.
  • Module scripts may be shared by multiple applications.
  • Modules help identify and remove naming conflicts. The content page will not load if there are naming clashes across different modules. This helps identify conflicts early in your development cycle.
  • If anything changes in the module dependency chain, the issue is identified quickly during module parsing.
  • Your code may be created as a set of small, maintainable files, which will help with very large projects.
  • For convenience, you may then bring together under all those small modules under the scope of a single module.

Other differences between JavaScript Scripts and Modules

In addition to the performance improvements, there are some other side benefits to using JavaScript modules, such as:

  • Modules are always executed in strict mode, regardless of whether strict mode is declared.
  • Modules may both import and export.
  • Just about any object may be exported, including class, function, let, var or const and any top-level function.
  • Module code is (with the exception of export) regular JavaScript code and can use any object or other functionality available to any other JavaScript program.
  • By default, everything declared in an ES6 module is private, and runs in strict mode. Only functions, classes, variables and constants that are exposed using export are public.
  • Module objects are frozen and there is no way to modify them once they are loaded.
  • All import and export declarations must be made at the top level. So you cannot declare an export inside a function, or as part of a conditional clause. You also cannot export items programmatically by iterating through an array or on demand.
  • There is no way to recover from an import error. Program execution will stop as soon as any module in the dependency tree fails to load. All module dependencies are loaded eagerly, and there is no programmatic way to load a module on demand.

Performance Considerations

Evaluating a JavaScript program may generate side-effects on the JavaScript global environment; therefore, each JavaScript program is evaluated in a separate v8 context. The overhead of creating such a context is significant, and in the recent v8 version that overhead has increased by roughly 40%.

To compensate for this overhead, it is suggested that you convert your JavaScript scripts to JavaScript modules. A JavaScript module program is one that adheres to the following:

  1. A program that uses strict JavaScript syntax and can be compiled as a JavaScript module.
  2. A program that contains a main module with the .mjs extension.
  3. Any ad hoc program that uses import or export syntax.

For further reading on the details of converting script programs into module programs, please see the chapter on modules in the ECMASCRIPT Language Specification.

Creating and Using ES6 Modules

Creating an ES6 module may be accomplished by adding an export statement to any JavaScript script file: This will make the objects being exported available for all scripts to import.

Public variables, functions and classes are exposed using an export statement. By default, all declared objects within an ES6 module are private. The module runs in strict mode with no need for the use strict declaration. Here is a simple example:

// myMathLib.mjs
export const PI = 3.14159265359;
export const E = 2.718281828459;
export const GAMMA = 0.577215;
export const reducer = (accumulator, currentValue) => accumulator + currentValue;
// The following objects are private
const G = 0.915965594177;
const x = 0.110001000000000000000001;
export function adder(arguments) {
  console.log('Grand Total: ', arguments);
  return arguments.reduce(reducer);
}

You may also dispense with all individual export statements and use a single export line to define them all:

// myMathLib.mjs
const PI = 3.14159265359;
const E = 2.718281828459;
const GAMMA = 0.577215;
const reducer = (accumulator, currentValue) => accumulator + currentValue;
// The following objects will remain private
const G = 0.915965594177;
const x = 0.110001000000000000000001;
export function adder(arguments) {
  console.log('Grand Total: ', arguments);
  return arguments.reduce(reducer);
}
export { PI, E, GAMMA, reducer, adder, ... };

You then use an import statement to pull items from a module into another script or module:

// myScript.js
import { adder } from './myMathLib.mjs';
console.log( adder(1,2,3,4,5) );
=> 15

From the import statement, we know that myMathLib.mjs resides in the same directory as myScript.js. In addition to the relative path shown above, you may also use:

  • Full URLs - starting with HTTPS or FILE.
  • Absolute file references - starting with /.

Multiple items may be imported in the same export line:

import { adder, aglomerator } from './myMathLib.mjs';
console.log( adder(1,2,3,4,5) );  // 15
console.log( aglomerator(1,2,3,4,5) ); // 12345

To resolve naming collisions, imported functions may be aliased as follows:

import { adder as sum, aglomerator as glob } from './myMathLib.mjs';
console.log( sum(1,2,3,4,5) );      // 15
console.log( glob(1,2,3,4,5) ); // 12345

Lastly, you are able to import all public items by providing a namespace:

import * as trans from './myMathLib.mjs';
console.log( trans.PI ); // 3.14159265359
console.log( trans.E );  // 2.718281828459
console.log( trans.GAMMA ); // 0.577215

Dynamic Imports are not Allowed

As stated above: All import and export declarations must be made at the top level. So you cannot declare an export inside a function, or as part of a conditional clause. You also cannot export items programmatically by iterating through an array or on demand

import ... from someFunction(); // ERROR: may only import from "string"

The following statements will also not work:

if(some-condition) {
  import ...; // ERROR: can't import conditionally
}
{
  import ...; // ERROR: import is only allowed at the top level
}

The net result is that, by only allowing import at the top level, your code is much more easy to parse, analyze and to bundle. In the process, unused functions will be shakes out, potentially reducing the size of the executable. This is possible because of the strict rules under which modules operate.

Using JavaScript Modules in the Browser

On the web, you can tell browsers to treat a <script> element as a module by setting the type attribute to module.

<script type="module" src="optic.mjs"></script>
<script nomodule src="optic.sjs"></script>

This works because modern browsers understand type="module" and will also ignore scripts with a nomodule attribute. This allows the programmer to serve a module-based payload to module-supporting browsers while providing a failover mode to older browsers. Only older browsers will get the nomodule payload.

New Mimetype for JavaScript Modules

In order to support JavaScript module programs, a new server mimetype has been created. All module URIs must conform to the new mimetype:

  • name: application/vnd.marklogic-js-module
  • Extension: mjs

You may view this new mimetype by navigating to the Admin UI and selecting the Mimetypes from the explorer pane.

The extension for a module URI in the import statement may be omitted. When the module URI in an import statement doesn't contain an extension, an extension mapping to any of the above MIME types must be added to resolve the specified module. For example:

import { square, diag } from 'lib/top'; // map to lib/top.js or lib/top.mjs

Importing MarkLogic Built-In Modules

Based on these Performance Considerations, it is recommended that you import the following modules:

jsearch.mjs instead of jsearch.sjs.

optic.mjs instead of optic.sjs

Here is an example of importing optic.mjs:

'use strict';
import op from '/MarkLogic/optic.mjs';
op.fromLiterals([
            {group:1, val:2}, 
            {group:1, val:4}, 
            {group:2, val:3}, 
            {group:2, val:5}, 
            {group:2, val:7} 
            ])
          .groupBy('group', op.avg('valAvg', 'val'))
          .result();
=>
{"group":1, "valAvg":3}
{"group":2, "valAvg":5}

Here is an example of importing jsearch.mjs:

// Find all documents where the "author" JSON property value is "Mark Twain"
import jsearch from '/MarkLogic/jsearch.mjs';
jsearch.documents()
  .where(jsearch.byExample({author: 'Mark Twain'}))
  .result()
=>
{
"results": ..., 
"estimate": 25
}

Evaluating Variables with ES6 Modules

Sometimes you will need to evaluate a JavaScript snippet from within an XQuery program. Prior to ES6, you could simply pass the names of the required variables as a map of string and value pairs. For instance, given the following program:

hello.sjs:

'use strict';
function specialFunction(
    x, y
){
    return "Hello " + x + " and " + y;
};
module.exports.specialFunction = specialFunction;

You could invoke it via the following code:

xquery version 1.0;
let $x := "'use strict'; var ext=require('/hello.sjs'); ext.specialFunction(x,y);"
return
xdmp:javascript-eval($x,map:map()=>map:with("x","Laurel")=>map:with("y","Hardy"));
=>
Hello Laurel and Hardy

In ES6 modules, the same code will complain about variable x being undefined. This is because in ES6, variables are available as properties on the external global object. So you will need to re-write your module in the following manner:

hello.mjs:

'use strict';
export function specialFunction(x,y)
{
    return "Hello " + x + " and " + y;
};

And your code that imports this module will look like this:

xquery version "1.0-ml";
let $x := "'use strict'; import {specialFunction} from '/hello.mjs';  specialFunction(external.x,external.y);"
return
xdmp:javascript-eval($x,map:map()=>map:with("x","Tom")=>map:with("y","Jerry"));
=>
Hello Tom and Jerry

Note that use of external.x and external.y in the function call. This is the way to reference variables in the external global object.

« Previous chapter