Skip to content

Broken ESM imports in xterm-headless #5448

@iSuslov

Description

@iSuslov

Following up #5441

Details

  • xterm.js version: 5.6.0-beta.140

Seems like #5442 won't fix the core problem and ESM imports would still fail in NodeJs.

Investigation

TL;DR: packages should use package.json exports field, otherwise named imports may fail in Node runtime and tree-shaking might work unpredictable.

I've tried the same fix from #5442 and my code still failed by running it with npx tsx ./src/snippet.ts:

import {Terminal} from "@xterm/headless" // no typescript error here
const terminal = new Terminal()
console.log("cols:", terminal.cols)

That seem to be a module resolution problem inside tsx I thought, and tried to run the same script like this:

node -e 'import {Terminal} from "@xterm/headless"
const terminal = new Terminal()
console.log("cols:", terminal.cols)'

output:

import {Terminal} from "@xterm/headless"
        ^^^^^^^^
SyntaxError: Named export 'Terminal' not found. The requested module '@xterm/headless' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from '@xterm/headless';
const {Terminal} = pkg;

Turns out the "module" field in package.json isn't supported by NodeJs and won't be in the future. That means module resolution silently falls back to CommonJs version.

As per my research all popular bundlers seem to support "module" field right now, however webpack uses https://github.com/webpack/enhanced-resolve which by default doesn't respect it (webpack overrides this defaults).

Why it's bad

If a runtime or a bundler doesn't support "module" field in package.json and:

  • module compiled to commonjs -> typescript won't raise any error for named imports -> Runtime error
  • module compiled to umd (like other add-ons) -> Tree-shaking silently skipped
  • ...probably more uncertainties?

Proposed solution

We should adopt de-facto standard exports field in package.json. More specifically Conditional exports. This won't introduce any compatibility issues, exports and main+module fields can live together:

// ...
 "main": "lib-headless/xterm-headless.js",
 "module": "lib-headless/xterm-headless.mjs",
 "types": "typings/xterm-headless.d.ts",
 "exports": {
       "types": "./typings/xterm-headless.d.ts", // goes first
       "import": "./lib-headless/xterm-headless.mjs",
       "require": "./lib-headless/xterm-headless.js"
      }
// ...

This approach can provide further benefits like precise exports for browser/node or test/production environments etc.. More: https://nodejs.org/api/packages.html#resolving-user-conditions

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions