1

I am attempting to migrate our single-page application from RequireJS to Webpack. As part of this transition, I am gradually rewriting our hundreds of modules from ASM define() syntax to modern ESM import / export syntax. Because of the size of the codebase, it's not feasible to refactor every file at once.

In doing so, I've ran into a fundamental difference between ASM / ES modules exporting: the ASM modules always export a single value, while ES modules support either named exports and/or a single export default value. Our codebase is already set up to use single module exports, so I think the default exports are more appropriate for the modules we have.

However, this means that when importing these ES modules into legacy ASM modules, and also fetching them with require() or import(), we always have to extract the default property from them.

Examples:

// Refactored ESM module
// Both ASM and ESM dependencies can be imported with this syntax
import LegacyDependency from "legacyDependency";
import EsmDependency from "esmDependency";
// Legacy ASM module
// ASM dependencies come in as-is; ESM dependencies come in as modules with default property
define(["legacyDependency", "esmDependency"],
    function(LegacyDependency, {default: EsmDependency}) {
});
// Async import
// Both ASM and ESM come in as modules with default property
const {default: LegacyDependency} = import("legacyDependency");
const {default: EsmDependency} = import("esmDependency");

Not only is this a lot of code to refactor, it also requires us to know what kind of module each of our files is (and then go change all references to it after it gets refactored into ESM.

Is there a way to override Webpack's importing behavior so that the import() method will always return the module's default export, if one exists? Even if this isn't consistent with how a "pure" ESM app would work (if it were using the browser's native import feature), it would stop a lot of headaches in the short-term during this transitional period.

Ultimately I'd like it if we could adjust Webpack's interpreter so that the above examples could be rewritten as this:

// Legacy ASM module (no destructuring)
define(["legacyDependency", "esmDependency", "namedDependency"],
    function(LegacyDependency, EsmDependency, {namedFn, namedProperty}) {

});
// Async import (no destructuring)
const LegacyDependency = import("legacyDependency");
const EsmDependency = import("esmDependency");
const {namedFn, namedProperty} = import("namedDependency");

Is the above possible with Webpack's configuration?

2 Answers 2

1

The first thing which come in my mind is to use Webpack babel-loader with custom babel plugin.
This custom plugin could, for example, perform the following transformation:

//your source code:                                
const foo = await import('./mod'); 

|
|
|
v

//code after Babel:  
const foo = (await import('./mod')).default;

You can write your code as if the import() syntax returns a single value equal to the default export of the module. However, after Webpack builds your code, it will be transformed into the correct JavaScript syntax.

Example of a custom Babel plugin for the described transformation. Please treat this snippet more as a concept or guide, as I may have missed some corner cases:

module.exports = function ({ types: t }) {
    return {
        visitor: {
            AwaitExpression(path) {
                const arg = path.node.argument;
                // Only transform await import(...)
                if (
                    t.isCallExpression(arg) &&
                    t.isImport(arg.callee)
                ) {
                    // Avoid re-transforming transformed nodes
                    if (
                        t.isIdentifier(path.parentPath.parentPath.node.property, { name: 'default' })
                    ) {
                        return;
                    }

                    // Build: (await import(...)).default
                    const clone = t.awaitExpression(t.cloneNode(arg));
                    const wrapped = t.parenthesizedExpression(clone);
                    const memberExpr = t.memberExpression(wrapped, t.identifier('default'));


                    path.replaceWith(memberExpr);
                }
            }
        }
    };
};
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for the suggestion; it looks promising! I won't be able to test it for a week so I wanted you to know I saw it.
Is it possible to use this concept to work on the AMD imports as well? Those would be the higher priority for me, since every legacy file uses those. It would need to convert those into imports that return the default property if one exists (if it's an ES module) or the original object otherwise (if it's an AMD module).
1

We ultimately decided to bite the bullet and refactor all of our files before merging them all at once. We used this tool to automatically rewrite the define() headers into import statements in bulk; after that we just need to rewrite our async imports manually.

https://github.com/prantlf/requirejs-esm-converter

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.