Updating Module Syntax from AMD to ESM
Most entry points for legacy modules that have recently been migrated from RequireJS to Webpack as part of the RequireJS to Webpack Migration are eligible for this update.
See the Historical Background on Module Patterns for a more detailed discussion of module types. As a quick refresher, here are some definitions:
- Modified AMD (Asynchronous Module Definition)
The legacy module type used for older JavaScript modules on HQ, identified by having an
hqDefine
statement near the top of the file. AMD was the only module type compatible with RequireJS, our first JavaScript bundler. It is still needed as a format for modules required by No-Bundler pages.- ESM (ES Modules)
The newest module type with updated powerful import and export syntax. This is the module format that you will see referenced by documentation in modern javascript frameworks. This is quickly identified by the
import
statements at the top used for including dependencies.
The different types of modules you will encounter are:
- Entry Point Modules
Modules that are included directly on a page using a bundler template tag, like
js_entry
. These are the modules that the bundler (Webpack) uses to build a dependency graph so that it knows what bundle of javascript dependencies and page-specific code is needed to render that page / entry point.- Dependency Modules
These are modules that are never referenced by
js_entry
and are only in the list of dependencies for other modules. Often these modules are used as utility modules or a way to organize JavaScript for a page that is very front-end heavy.
Step 1: Determine if the Module is Eligible for a Syntax Update
The HQ AMD-style module will look something like:
hqDefine('hqwebapp/js/my_module', [
'jquery',
'knockout',
'underscore',
'hqwebapp/js/initial_page_data',
'hqwebapp/js/assert_properties',
'hqwebapp/js/bootstrap5/knockout_bindings.ko',
'commcarehq',
], function (
$,
ko,
_,
initialPageData,
assertProperties
) {
...
});
Entry Points
If this module is a webpack entry point, then it is eligible for an update. In the example above, you would find
hqwebapp/js/my_module
used on a page with the following:
{% js_entry "hqwebapp/js/my_module %}
The entry point can also be specified with js_entry_b3
if the module is part of the Bootstrap 3 build
of Webpack.
If this module is inside a requirejs_main
or requirejs_main_b5
tag, then it is NOT eligible for an update.
Instead, please first
migrate this module from RequireJS to Webpack
Dependency Modules
If this module is a dependency of any modules that are requirejs_main
entry points,
then this module is not eligible for migration. If a module’s syntax is updated when it’s still
required by RequireJS modules, then it will result in a RequireJS build failure on deploy.
You can check the status of a dependency module’s RequireJS usage by looking at the Bootstrap 3 and Bootstrap 5 module list.
If this module is referenced by any hqImport
calls (for instance hqImport('hqwebapp/js/my_module')
),
then this module is NOT yet eligible, and must continue using the older AMD-style syntax until
the hqImport
statements are no longer needed. See the
JS Bundler Migration Guide for
how to proceed in this case.
Slightly Different Syntax
If the AMD-style module looks a bit different than the syntax above–for instance, the list of dependencies are missing or
hqImport
and/or global variables can be found in the main body of the module–then this module must be
migrated to use a JS Bundler.
Step 2: Update the Module Syntax
Key Points
ESM no longer needs to define the module name within the module itself. Instead, Webpack (our bundler) is configured to know how to reference this module by its filename and relative path within an application.
By default, you can use the same dependency names with the
import
syntax. If theimport
statement results in a Webpack Build error, look atwebpack.common.js
because it might require an alias. If you still have a problem, checkrequirejs_config.js
, because there might have been an alias defined there that hasn’t been added towebpack.common.js
.
Example Structural Change
This is a rough example of what the changes will look like:
hqDefine('hqwebapp/js/my_module', [
'jquery',
'knockout',
'underscore',
'hqwebapp/js/initial_page_data',
'hqwebapp/js/assert_properties',
'hqwebapp/js/bootstrap5/knockout_bindings.ko',
'commcarehq',
], function (
$,
ko,
_,
initialPageData,
assertProperties
) {
...
});
to
import "commcarehq"; // Note: moved to top
// named yarn/npm dependencies
import $ from "jquery";
import ko from "knockout";
import _ from "underscore";
// named internal dependencies:
import initialPageData from "hqwebapp/js/initial_page_data";
import assertProperties from "hqwebapp/js/assert_properties";
// unnamed internal dependencies:
import "hqwebapp/js/bootstrap3/knockout_bindings.ko";
// module specific code...
...
Note that import "commcarehq";
has been moved to the top of the file. The ordering is
for consistency purposes, but it’s important that either import "commcarehq";
is present in the list
of imports for Webpack Entry Point modules. If this import is not present in an entry point,
then site-wide navigation, notifications, modals, and other global widgets will not
work on that page.
Remember, an Entry Point is any module that is included directly on a page using the
js_entry
or js_entry_b3
template tags.
Modules that are not entry points are not required to have this import. If you are updating the syntax of a dependency (non-entry point) module, do not worry about including this import if it is not already present.
Step 4: Other Code Updates
If this module is an entry point, then the rest of the module-specific code can remain as is, with the indentation level updated. However, some entry points are also dependencies of other entry points. If that’s the case, proceed to the next part.
If this module is a dependency module, meaning it is referenced by other modules,
then the return
line at the end of the module should follow the appropriate export
syntax
needed by the modules that depend on this module.
The most likely change is to replace return
with export
and leave everything else as is.
Otherwise, see the
export documentation
for details and inspiration in case you want to do some additional refactoring.