Search code examples
node.jstypescriptwebpackbundlees6-modules

2 ESM interoperability issue with Node + Typescript + Webpack


I'm straggling on how to make two ESM working together in a browser. I have written a first ESM named 'esm1' written in Typescript that emits both a 'es6' module (with tsc) and a bundle for the browser (with webpack). Everything seems to work since I am able to use the bundle in a web page:

esm1$ npm run webtest

and I am as well able to use the generated 'es6' code from another ts file (

esm1$ npm run test:esm

Now I want to use the 'esm1' module from another module names 'esm2'. esm2 module has the same structure of esm1, but it imports esm1 as dependency in the package.json. In esm2 I have created a Typescript file that uses classes coming from esm1:

// esm2/src/index.ts
import {Hammer} from 'esm1/lib-esm/Hammer.js';
import { BoxObject } from 'esm1/lib-esm/BoxObject.js';
import {Box} from 'esm1/lib-esm/Box-node.js';

export class Interop {

    constructor() {

    }

    doSomethingWithEsm1() {
        console.log("Into doSomethingWithEsm5");
        
        const p = new Hammer("blue");
        console.log(p);
        
        const box = new Box("studio");
        box.addObject(p);
        console.log(box);
        box.getFile("http://skies.esac.esa.int/Herschel/PACS-color/properties").then( (data) => console.log(data));
        
        
    }
}

const ip = new Interop();
ip.doSomethingWithEsm1();

On esm2 if I run

esm2$ npm run test:esm 

everything goes well. The problem raises when I include the esm2 bundle in a web page:

<!-- esm2/webtest/index.html -->
<!doctype html>
<html>

<head>
    <meta charset="utf-8" />
    <title>ESM 6 module</title>
    <base href="."/>
    <!-- <script type="module" src="./my-lib.js"></script> -->
    <script src="./my-lib.js"></script>
</head>

<body>
    <h1>Hello world from ESM6!</h1>
    <h2>Tip: Check your console</h2>
    
    <!-- <script type="module"> -->
    <script>
        document.addEventListener("DOMContentLoaded", () => {
            

        console.log("Hello World from ESM6!");
        const ip = new MyEsm2.Interop();
        const h  = new MyEsm1.Hammer("green");
        ip.doSomethingWithEsm1(h);
        
        });
        
    </script>
</body>

</html>

In that case I get the following error in the browser console:

Uncaught TypeError: esm1_lib_esm_Hammer_js__WEBPACK_IMPORTED_MODULE_0__ is undefined
    doSomethingWithEsm1 index.ts:14
    <anonymous> index.ts:27
    <anonymous> my-lib.js:135
    <anonymous> my-lib.js:138
    webpackUniversalModuleDefinition universalModuleDefinition:9
    <anonymous> universalModuleDefinition:10
index.ts:14:18
Hello World from ESM6! localhost:5001:21:17
Uncaught ReferenceError: MyEsm2 is not defined
    <anonymous> http://localhost:5001/:22
    EventListener.handleEvent* http://localhost:5001/:18

Anybody can help me understanding where I'm mistaking? The code is on github:


Solution

  • At the end I've been able to figure out what was creating issues on the interoperability of the 2 ESM esm1 and esm2. It was node-fetch. I replaced it with cross-fetch and now it seems that everything is working as expected. For anybody facing with the same problem, I am using node:

    (base)esm2$ node -v
    v16.17.1
    

    and below you have configuration (package.json, tsconfig.json and webpack.config.js) for both esm1 and esm2:

    //esm1: package.json
    {
      "name": "emtest1",
      "version": "1.0.0",
      "description": "",
      "type": "module",
      "exports": {
        ".": {
          "types": "./lib-esm/index-node.d.ts",
          "import": "./lib-esm/index-node.js",
          "require": "./_bundles/esm1.js"
        }
      },
      "main": "./lib-esm/index-node.js",
      "types": "./lib-esm/index-node.d.ts",
      "files": [
        "lib-esm/"
      ],
      "scripts": {
        "clean": "shx rm -rf _bundles lib-esm",
        "build:dev": "npm run clean && tsc && webpack --config ./webpack.config.js --mode=development",
        "build:prod": "npm run clean && tsc && webpack --config ./webpack.config.js --mode=production",
        "webtest": "cp _bundles/* webtest/; node server.cjs",
        "test:esm": "tsc -m es6 test.ts; node test.js"
      },
      "engines": {
        "node": ">=16.0.0"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "dependencies": {
        "cross-fetch": "^3.1.5"
      },
      "devDependencies": {
        "@tsconfig/node16": "^1.0.3",
        "@types/node": "^18.7.23",
        "node-static": "^0.7.11",
        "shx": "^0.3.4",
        "ts-loader": "^9.4.0",
        "typescript": "^4.8.3",
        "webpack": "^5.74.0",
        "webpack-cli": "^4.10.0"
      }
    }
    
    //esm1: tsconfig.json
    {
      "compilerOptions": {
        "moduleResolution": "node16",
        "module": "es6",
        "target": "es6",  
        "lib": ["es6", "dom"],
        "outDir": "lib-esm",
        "allowSyntheticDefaultImports": true,
        "suppressImplicitAnyIndexErrors": true,
        "forceConsistentCasingInFileNames": true,
        "sourceMap": true,
        "declaration": true,
        "declarationMap": true
      },
      "include": [
        "src/**/*"
      ],
      "exclude": [
        "node_modules",
        "**/*.spec.ts"
      ],
      "compileOnSave": false,
      "buildOnSave": false
    }
    
    // esm1: webpack.config.js
    import path from 'path';
    import {fileURLToPath} from 'url';
    
    const __filename = fileURLToPath(import.meta.url);
    const __dirname = path.dirname(__filename);
    
    
    const PATHS = {
      entryPoint4Browser: path.resolve(__dirname, 'src/index-node.ts'),
      bundles: path.resolve(__dirname, '_bundles'),
    }
    
    
    var browserConfig = {
      entry: {
        'esm1': [PATHS.entryPoint4Browser],
        'esm1.min': [PATHS.entryPoint4Browser]
      },
      target: 'web',
      externals: {},
      output: {
        path: PATHS.bundles,
        libraryTarget: 'umd',
        library: 'esm1',
        umdNamedDefine: true
      },
      resolve: {
        extensions: ['.ts', '.tsx', '.js'],
        extensionAlias: {
          '.js': ['.ts', '.js'],
          '.mjs': ['.mts', '.mjs']
        }
      },
      devtool: 'source-map',
      plugins: [
      ],
      module: {  
        rules: [
          {
            test: /\.(ts|tsx)$/i,
            use: 'ts-loader',
            exclude: ["/node_modules/","/src/Box-node.ts"],
            
          },
        ],
      }
    }
    
    export default (env, argv) => {
      return [browserConfig];
    };
    
    
    //esm2: package.json
    {
      "name": "emtest2",
      "version": "1.0.0",
      "description": "",
      "type": "module",
      "exports": {
        ".": {
          "types": "./lib-esm/index.d.ts",
          "import": "./lib-esm/index.js",
          "require": "./_bundles/esm2.js"
        }
      },
      "main": "./lib-esm/index.js",
      "types": "./lib-esm/index.d.ts",
      
      "files":[
        "lib-esm/"
      ],
      
      "scripts": {
        "clean": "shx rm -rf _bundles lib-esm",
        "build:dev": "npm run clean && tsc && webpack --config ./webpack.config.js --mode=development",
        "build:prod": "npm run clean && tsc && webpack --config ./webpack.config.js --mode=production",
        "webtest": "cp _bundles/* webtest/; node server.cjs",
        "test:esm": "node lib-esm/index.js"
      },
      "engines" : { 
        "node" : ">=16.0.0"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "devDependencies": {
        "@tsconfig/node16": "^1.0.3",
        "@types/node": "^18.7.23",
        "node-static": "^0.7.11",
        "shx": "^0.3.4",
        "ts-loader": "^9.4.0",
        "typescript": "^4.8.3",
        "webpack": "^5.74.0",
        "webpack-cli": "^4.10.0"
      },
      
      "dependencies": {
        "esm1": "file:../esm1"
        
      }
    }
    
    //esm2: tsconfig.json
    {
      "compilerOptions": {
        "moduleResolution": "node16",
        "module": "es6",
        "target": "es6",
        "lib": [ "es6", "dom" ],
        "outDir": "lib-esm",
        "allowSyntheticDefaultImports": true,
        "suppressImplicitAnyIndexErrors": true,
        "forceConsistentCasingInFileNames": true,
        "sourceMap": true,
        "declaration": true,
        "declarationMap": true
      },
      "include": [
        "src/**/*",
      ],
      "exclude": [
        "node_modules",
        "**/*.spec.ts"
      ],
      "compileOnSave": false,
      "buildOnSave": false
    }
    
    //esm2: webpack.config.js
    import path from 'path';
    import { fileURLToPath } from 'url';
    
    const __filename = fileURLToPath(import.meta.url);
    const __dirname = path.dirname(__filename);
    
    
    const PATHS = {
      entryPoint: path.resolve(__dirname, 'src/index.ts'),
      bundles: path.resolve(__dirname, '_bundles'),
    }
    
    var config = {
      entry: {
        'esm2': [PATHS.entryPoint],
        'esm2.min': [PATHS.entryPoint]
      }, 
      target: ['web'],
      externals: {},
      output: {
        path: PATHS.bundles,
        libraryTarget: 'umd',
        library: 'esm2',
        umdNamedDefine: true
      },
      resolve: {
        symlinks: true,
        extensions: ['.ts', '.tsx', '.js'],
        extensionAlias: {
          '.js': ['.ts', '.js'],
          '.mjs': ['.mts', '.mjs']
        },
      },
      devtool: 'source-map',
      plugins: [],
      module: {
        rules: [{
            test: /\.(ts|tsx)$/i,
            use: 'ts-loader',
            exclude: ["/node_modules/"],
    
          },
        ],
      },
    }
    
    export default config;
    

    Hope that helps someone. Everything is in my github:

    esm1: [https://github.com/fab77/esm1.git][1]
    esm2: [https://github.com/fab77/esm2.git][2]