Starting with the LuciadRIA 2023.0 release, LuciadRIA modules fully comply with the ECMAScript Modules (ESM) resolution algorithm used by browsers, module bundlers and NodeJS. This means that browsers can now load LuciadRIA modules with or without the use of a bundler. For consistency, we converted all NodeJS build scripts in the LuciadRIA release to ESM. This article first outlines the benefits of using ES Modules. Then, it describes the changes in LuciadRIA, and how you can upgrade to LuciadRIA 2023.0 in your project.

Benefits of ES Modules in the browser

Most browsers support the ES module syntax — import and export — for quite some time now, but they still lacked some features for the loading of those modules until recently. One missing feature was import-maps. The import-maps feature allows you to control what URLs get fetched by JavaScript import statements and import() expressions. For example, you can use an import map to map @luciad/ria/…​ imports to LuciadRIA modules hosted on a server:

<!DOCTYPE html>
<html lang="en">
<head>
  <script type="importmap">
  {
    "imports": {
      "@luciad/ria/": "/lib/ria/"
    }
  }
  </script>
</head>
<body style="margin: 0; padding: 0;">
<div id="map" style="width: 100vw; height: 100vh;"></div>
<script type="module" src="./main.js"></script>
</body>
</html>
import {WebGLMap} from "@luciad/ria/view/WebGLMap.js";

const map = new WebGLMap("map", {reference: "EPSG:4978"});

Note that the import identifier includes a file extension .js. Unlike bundlers, that use the NodeJS CommonJS module resolution, browsers require you to include the file extension in your imports. As of 2023.0, LuciadRIA modules include the file extension in the API and in samples. This allows the native loading of these modules by the browser, even without using a bundler. Note that bundlers, such as webpack, can also handle import identifiers that end with a file extension. This means that you can use the same main.js code in the browser, with or without a bundler.

Being able to load ES modules in the browser natively is great, but the use of bundlers still offers some benefits. Loading many small modules over the network can be slower than loading a single, bundled script, because each network request has a bit of overhead. For production usage, you may want to bundle your modules into a single file. Bundling tools also offer other features like:

  • Code-splitting, to help you load parts of your application on demand.

  • Tree-shaking, to reduce the size of your application JavaScript bundles.

  • Loading of non-JS resources, to import CSS or images directly in JS.

  • Transpilation, to develop applications in another language, such as TypeScript or SASS.

  • Automatic polyfilling, to expand the browser compatibility of your application.

ES Modules in NodeJS

NodeJS supports two module systems: CommonJS (CJS) and ECMAScript Modules (ESM). For consistency with the LuciadRIA API and sample modules, we converted the NodeJS build scripts in a LuciadRIA release to ESM in 2023.0.

LuciadRIA’s API is intended for use in a browser environment, not in NodeJS. LuciadRIA assumes the presence of browser APIs, like the DOM API, WebGL, and others. These aren’t available in NodeJS.

The adoption of ECMAScript Modules by Node.js reflects a broader shift in the JavaScript ecosystem towards ESM. Several factors motivated this shift:

  • Standardization: ESM is the official standard for JavaScript modules as specified by ECMAScript, the standardized specification of JavaScript. By supporting ESM, Node.js aligns itself with this standard, which helps improve interoperability and reduces fragmentation in the JavaScript ecosystem.

  • Static Analysis: ESM supports static imports and exports, which can be analyzed at compile-time, as opposed to at run-time with CommonJS. This can lead to performance improvements, because tools can perform optimizations like tree-shaking to remove unused code. It can also lead to better tooling support in IDEs for features such as auto-imports and "go to definition" for example.

  • Browser Compatibility: Modern browsers support ESM. Therefore, using ESM can make it easier to write isomorphic code — code that can run both in Node.js and in the browser — without needing a build step or special tooling to convert module formats.

  • Asynchronous Loading: ESM has built-in support for asynchronous loading and can handle circular dependencies more predictably than CommonJS.

  • Features: ESM supports features that CommonJS doesn’t, such as top-level await and importing JSON or native modules.

These benefits motivated Node.js to add support for ESM, but it’s important to note that Node.js didn’t remove support for CommonJS. As of now, Node.js supports both module systems to maintain backward compatibility and to allow developers to choose the system that best suits their needs.

What changed in LuciadRIA 2023.0?

As of LuciadRIA 2023.0, all JavaScript modules in the release are in ESM format. This includes API modules, sample modules, and NodeJS build scripts.

This is an overview of the changes:

  • All imports in the API and in sample code now include the file extension. This allows both bundlers like webpack and the browser’s native module loader to load LuciadRIA modules.

  • All package.json files now declare "type": "module". This declaration makes NodeJS assume that .js files are ES modules, instead of CommonJS modules.

  • All NodeJS build scripts are now ES modules instead of CommonJS files. This includes webpack configuration files (webpack.config.js). To maintain compatibility with CommonJS projects that use samples/common/webpack.config.js, we ensured that this file can still be imported with require().

  • The TypeScript configuration in sample code now uses the nodenext module resolution.

Upgrading to 2023.0

When upgrading to 2023.0, you can choose to stick with CommonJS modules, or switch to the new ES modules.

If you’ve based your project on a LuciadRIA sample, or if you’re using @luciad/ria-sample-common, then you should keep your sample in-sync with the @luciad/ria-sample-common version: either keep using the old @luciad/ria-sample-common version, or upgrade your entire application to the 2023.0 sample framework. This includes increasing the versions of dependencies like webpack and react in both @luciad/ria-sample-common and your sample-based application, so that both packages use the same version. Concretely, verify that your sample’s package.json, tsconfig.json and webpack.config.js are in-sync with a 2023.0 sample when you’re upgrading.

Stick with CommonJS modules

Sticking with CommonJS modules is relatively straightforward. Upgrading to a new version of the API itself shouldn’t give any issues.

If you’re using TypeScript and:

  • You’re extending from 2023.0’s @luciad/ria-sample-common/tsconfig.json

  • You still want to use imports without an extension

you must revert back to the old node module resolution. Otherwise, TypeScript will complain about not being able to resolve imports that don’t have an extension. Update your tsconfig.json to:

{
  "extends": "@luciad/ria-sample-common/tsconfig.json",
  "compilerOptions": {
    "moduleResolution": "node"
  }
}

Switch your project to ES Modules

If you also want to switch to ES modules in your project, you can:

  • Update your package.json: ensure that the "type" field in your package.json is set to "module".This tells Node.js to treat .js files as ES modules.

  • Convert your NodeJS scripts to ES modules: replace require() imports with import statements, and replace module.exports declarations with export. You can check out the LuciadRIA 2023.0 sample NodeJS scripts for inspiration, for example sample/common/webpack.config.js.

    You can use a tool like cjstoesm to automate part of the CJS-to-ESM conversion.

  • Add file extensions to your imports: File extensions are mandatory when you’re using ES Modules resolution.Update your imports to include file extensions. For example:

    import {getReference} from "@luciad/ria/reference/ReferenceProvider";
    import {SingleMapSample} from "@luciad/ria-sample-common-ui/SingleMapSample";

    becomes

    import {getReference} from "@luciad/ria/reference/ReferenceProvider.js";
    import {SingleMapSample} from "@luciad/ria-sample-common-ui/map/SingleMapSample.jsx";

    Even in TypeScript, the file extension of the imported module is .js or .jsx (not .ts or .tsx).

You can use the LuciadRIA 2023.0 sample code as a reference. Specifically, you can look at the package.json, the updated imports, and the updated webpack.config.js files.