node.js - ERR_REQUIRE_ESM från webpacks import av ett ESM-paket


Jag bygger en app med Nx för modularisering och Geckos för serverdelen. Med Nxs standard Webpack 5-konfiguration för Node får jag detta fel när jag kör appen:

/Users/hgrzeskowiak/hugo-game/fresh-nx-geckos/nx-geckos-test/dist/apps/geckos-server-app/main.js:8
module.exports = require("@geckos.io/server");
                 ^
Error [ERR_REQUIRE_ESM]: require() of ES Module /Users/hgrzeskowiak/hugo-game/fresh-nx-geckos/nx-geckos-test/node_modules/@geckos.io/server/lib/index.js from /Users/hgrzeskowiak/hugo-game/fresh-nx-geckos/nx-geckos-test/dist/apps/geckos-server-app/main.js not supported.
Instead change the require of index.js in /Users/hgrzeskowiak/hugo-game/fresh-nx-geckos/nx-geckos-test/dist/apps/geckos-server-app/main.js to a dynamic import() which is available in all CommonJS modules.
(...)

Här är stegen för att återskapa:

npx create-nx-workspace@latest nx-geckos-test
Need to install the following packages:
  create-nx-workspace@latest
Ok to proceed? (y) y
✔ What to create in the new workspace · express
✔ Application name                    · geckos-server-app
✔ Use Nx Cloud? (It's free and doesn't require registration.) · No

>  NX  Nx is creating your v13.4.1 workspace.

  To make sure the command works reliably in all environments, and that the preset is applied correctly,
  Nx will run "npm install" several times. Please wait.

✔ Installing dependencies with npm
✔ Nx has successfully created the workspace.
npm run start

> [email  start
> nx serve

> nx run geckos-server-app:serve
chunk (runtime: main) main.js (main) 552 bytes [entry] [rendered]
webpack compiled successfully (26ac6f288b082854)
Debugger listening on ws://localhost:9229/70a1d9b4-af2b-4fc9-90f7-f3502e5fdf2e
Debugger listening on ws://localhost:9229/70a1d9b4-af2b-4fc9-90f7-f3502e5fdf2e
For help, see: https://nodejs.org/en/docs/inspector
Issues checking in progress...
Listening at http://localhost:3333/api

I detta skede bygger och körs standard Express-appen.

Nu ersätter jag innehållet i apps/geckos-server-app/src/main.ts med Geckos-serverexemplet från README:

import geckos from '@geckos.io/server'

const io = geckos()

io.listen(3000) // default port is 9208

io.onConnection(channel => {
  channel.onDisconnect(() => {
    console.log(`${channel.id} got disconnected`)
  })

  channel.on('chat message', data => {
    console.log(`got ${data} from "chat message"`)
    // emit the "chat message" data to all channels in the same room
    io.room(channel.roomId).emit('chat message', data)
  })
})

...och installera geckos-serverpaketet med hjälp av

npm install @geckos.io/server

added 4 packages, and audited 785 packages in 7s

87 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Okej, dags att köra det igen:

npm run start

> [email  start
> nx serve


> nx run geckos-server-app:serve
chunk (runtime: main) main.js (main) 610 bytes [entry] [rendered]
webpack compiled successfully (90920432a9d246ea)
Debugger listening on ws://localhost:9229/dc6d9e1a-1a15-432f-a46c-d2ac83b923ea
Debugger listening on ws://localhost:9229/dc6d9e1a-1a15-432f-a46c-d2ac83b923ea
For help, see: https://nodejs.org/en/docs/inspector

/Users/hgrzeskowiak/hugo-game/fresh-nx-geckos/nx-geckos-test/dist/apps/geckos-server-app/main.js:8
module.exports = require("@geckos.io/server");
                 ^
Error [ERR_REQUIRE_ESM]: require() of ES Module /Users/hgrzeskowiak/hugo-game/fresh-nx-geckos/nx-geckos-test/node_modules/@geckos.io/server/lib/index.js from /Users/hgrzeskowiak/hugo-game/fresh-nx-geckos/nx-geckos-test/dist/apps/geckos-server-app/main.js not supported.
Instead change the require of index.js in /Users/hgrzeskowiak/hugo-game/fresh-nx-geckos/nx-geckos-test/dist/apps/geckos-server-app/main.js to a dynamic import() which is available in all CommonJS modules.
    at [email /server (/Users/hgrzeskowiak/hugo-game/fresh-nx-geckos/nx-geckos-test/dist/apps/geckos-server-app/main.js:8:18)
    at __webpack_require__ (/Users/hgrzeskowiak/hugo-game/fresh-nx-geckos/nx-geckos-test/dist/apps/geckos-server-app/main.js:32:41)
    at /Users/hgrzeskowiak/hugo-game/fresh-nx-geckos/nx-geckos-test/dist/apps/geckos-server-app/main.js:45:18
    at /Users/hgrzeskowiak/hugo-game/fresh-nx-geckos/nx-geckos-test/dist/apps/geckos-server-app/main.js:59:3
    at Object.<anonymous> (/Users/hgrzeskowiak/hugo-game/fresh-nx-geckos/nx-geckos-test/dist/apps/geckos-server-app/main.js:64:12)

Jag testade ganska många Webpack-konfigurationsöverskridningar och att använda type: "module" i package.json, men utan resultat. Den genererade main.js ser mestadels likadan ut oavsett vilka webpack-inställningar jag ändrar. Saker jag försökte ändra i appens webpack-överskridningskonfiguration:

module.exports = function(webpackConfig, context) {
  webpackConfig.target = "async-node16";
  webpackConfig.output.libraryTarget = "module";
  webpackConfig.output.library = {type: "module"};
  webpackConfig.output.chunkFormat = "module";
  webpackConfig.experiments.outputModule = true;
  console.log("webpack config:");
  console.log(webpackConfig);
  console.log("module rules:");
  console.log(webpackConfig.module.rules);
  return webpackConfig;
}

Konsolloggningarna i konfigurationen verifierar att skriptet körs.

Appen fungerar när jag manuellt ändrar require till import i den transpilerade koden, men det är inte en hållbar lösning.

Jag misstänker att felet har något att göra med att Geckos är ett ESM-paket, och att Webpack endast använder require istället för import som det borde, men jag kunde inte hitta något sätt att ändra det beteendet.

Några idéer?

  • Node-version 16.13.0
  • Nx version 13.4.1
  • Webpack (transitivt beroende av Nx): 5.65.0

Svar #1

Här är den renaste lösningen jag kunde hitta som fungerar med Type Script-versioner äldre än 4.6 (som borde lägga till stöd för denna funktion). Detta är i princip att patcha in ESM-stöd i WebPack med äldre TypeScript-versioner (inklusive den senaste vid tidpunkten för att skriva detta).

Ett anpassat Webpack-plugin som ändrar typen av den externa modulen till import.

// TypeScript before version 4.6 does not support transpiling ESM imports to
// `import()`, but uses `require()` instead. NodeJS does not support the use
// of `require() for ECMAScript modules (ESM).
//
// Good reads:
// https://www.typescriptlang.org/docs/handbook/esm-node.html
// https://devblogs.microsoft.com/typescript/announcing-typescript-4-5/#esm-nodejs

/**
 * A Webpack 5 plugin that can be passed a list of packages that are of type
 * ESM. The typescript compiler will then be instructed to use the `import`
 * external type.
 */
class ESMLoader {
  static defaultOptions = {
    esmPackages: "all"
  };

  constructor(options = {}) {
    this.options = { ...ESMLoader.defaultOptions, ...options };
  }

  apply(compiler) {
    compiler.hooks.compilation.tap(
      "ECMAScript Module (ESM) Loader. Turns require() into import()",
      (
        compilation
      ) => {
        compilation.hooks.buildModule.tap("Hello World Plugin", (module) => {
            if (
              module.type === "javascript/dynamic" &&
              (
                this.options.esmPackages === "all" ||
                this.options.esmPackages.includes(module.request)
              )
            ) {
              // All types documented at
              // https://webpack.js.org/configuration/externals/#externalstype
              module.externalType = "import";
            }
          }
        )
        ;
      }
    );
  }
}

module.exports = function(webpackConfig, context) {
  // NodeJS supports dynamic imports since version 12.
  webpackConfig.target = "node16";
  webpackConfig.plugins.push(
    // Manually specify the ESM modules or leave blank for using `import()`
    // on all packages.
    //new ESMLoader()
    new ESMLoader({esmPackages: "@geckos.io/server"})
  );
  return webpackConfig;
};

Den anpassade webpack-konfigurationen kan ställas in på en Nx-app genom project.json:

{
  "targets": {
    "build": {
      "executor": "@nrwl/node:build",
      "options": {
       ...
        "webpackConfig": "apps/your-app/webpackConfig.js"
      },
      ...

Denna lösning kräver också att tsconfig.json har satt modultypen till något mer modernt än commonjs, t.ex. es2020:

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "../../dist/out-tsc",
    "module": "es2020",
    "types": ["node"],
  },
  "exclude": ["**/*.spec.ts", "**/*.test.ts"],
  "include": ["**/*.ts"]
}

När detta är gjort kan den byggda filen köras med det vanliga nx serve.

Relaterade artiklar