Search code examples
node.jsvue.jswebpackes6-modulescommonjs

Webpack / Vue.js: generate module code at compile-time using ESM dependencies


Environment: webpack 5.44 + vue.js 3.0 + node 12.21

I'm trying to generate a module at compile-time, in order to avoid a costly computation at run-time (as well as 10Mb of dependencies that will never be used except during said computation). Basically run this at compile-time:

import * as BigModule from "big-module";
function extract_info(module) { ... }
export default extract_info(BigModule);

which will be imported at run-time as:

export default [ /* static info */ ];

I tried using val-loader (latest 4.0) which seems designed exactly for this use case.

Problem: big-module is an ESM, but val-loader apparently only supports CJS. So I can neither import ("Cannot use import statement outside a module" error) nor require ("Unexpected token 'export'" error).

Is there any way to make val-loader somehow load the ESM module? Note that I'm not bent on using val-loader, any other technique that achieves the same goal is just as welcome.


Solution

  • After learning way more than I wanted about this issue and node/webpack internals, there seems to be two possible approaches to import ESM from CJS:

    1. Use dynamic import(). But it is asynchronous which makes it unfit here, as val-loader requires a synchronous result.

    2. Transpile the ESM into CJS, which is the approach I took.

    In my case, full transpiling is overkill and rewriting imports/exports is sufficient, so I'm using ascjs to rewrite the ESM files, along with eval to safely evaluate the resulting string.

    All in all:

    // require-esm.js
    
    const fs = require('fs');
    const ascjs = require('ascjs');
    const _eval = require('eval');
    function requireESM(file) {
      file = require.resolve(file);
      return _eval(ascjs(fs.readFileSync(file)), file, { require: requireESM }, true);
    }
    module.exports = requireESM;
    
    // val-loader-target.js
    
    const requireESM = require('./require-esm');
    const BigModule = requireESM('big-module');
    function extract_info(module) { ... }
    module.exports = extract_info(BigModule);
    

    Note that:

    • ascjs is safe to use on CJS modules, since it only rewrites ESM imports/exports. So it's OK for big-module or its dependencies to require CJS files.
    • the third argument to _eval enables recursive rewriting, otherwise only the top-level file (the one passed to requireESM) is translated.