Skip to main content

Serenity/JS 3.42: Native ESM Support

ยท 4 min read

Serenity/JS 3.42 brings native ECMAScript Module (ESM) support to all packages, enabling you to use modern import syntax while maintaining full backwards compatibility with CommonJS require(). This release also includes Node.js 24 compatibility fixes and addresses the dual-package hazard that can occur when mixing ESM and CJS in the same project.

What's newโ€‹

Native ESM importsโ€‹

You can now use native ES module imports with Serenity/JS:

import { actorCalled, configure } from '@serenity-js/core';
import { Ensure, equals } from '@serenity-js/assertions';
import { BrowseTheWebWithPlaywright } from '@serenity-js/playwright';

await actorCalled('Alice').attemptsTo(
Ensure.that('hello', equals('hello')),
);

No configuration changes needed โ€” Node.js conditional exports handle the resolution automatically based on whether you're using import or require.

Packages with ESM supportโ€‹

The following packages now ship dual ESM/CJS builds:

  • @serenity-js/core
  • @serenity-js/assertions
  • @serenity-js/rest
  • @serenity-js/cucumber
  • @serenity-js/mocha
  • @serenity-js/jasmine
  • @serenity-js/console-reporter
  • @serenity-js/local-server
  • @serenity-js/web
  • @serenity-js/playwright
  • @serenity-js/playwright-test
  • @serenity-js/webdriverio
  • @serenity-js/webdriverio-8

Two packages remain CJS-only due to their dependencies:

  • @serenity-js/protractor โ€” Protractor itself has no ESM support
  • @serenity-js/serenity-bdd โ€” CLI tooling uses patterns incompatible with ESM

Clean submodule importsโ€‹

Serenity/JS 3.42 also introduces cleaner import paths for submodules:

// New clean paths (recommended)
import { SceneFinished } from '@serenity-js/core/events';
import { FileSystem } from '@serenity-js/core/io';
import { Name } from '@serenity-js/core/model';

// Legacy paths (still work)
import { SceneFinished } from '@serenity-js/core/lib/events';
import { FileSystem } from '@serenity-js/core/lib/io';
import { Name } from '@serenity-js/core/lib/model';

Available clean submodule paths include:

  • @serenity-js/core/adapter
  • @serenity-js/core/config
  • @serenity-js/core/errors
  • @serenity-js/core/events
  • @serenity-js/core/io
  • @serenity-js/core/model
  • @serenity-js/core/screenplay
  • @serenity-js/core/stage
  • @serenity-js/web/scripts
  • @serenity-js/cucumber/adapter
  • @serenity-js/mocha/adapter
  • @serenity-js/jasmine/adapter
  • @serenity-js/playwright-test/events

Dual-package hazard mitigationsโ€‹

When a package is loaded via both import and require in the same Node.js process, two separate module instances are created. This is known as the dual-package hazard and can break instanceof checks and singleton patterns.

This scenario is common with test runners like Cucumber.js, which might load formatters via ESM while support files use CJS.

Serenity/JS 3.42 addresses this at multiple levels:

Global singleton for the serenity instanceโ€‹

The serenity singleton now uses Symbol.for() on globalThis to ensure the same instance is shared regardless of how the module was loaded:

// Both resolve to the same Serenity instance
import { serenity } from '@serenity-js/core'; // ESM
const { serenity } = require('@serenity-js/core'); // CJS

Cross-boundary instanceof checksโ€‹

Custom Symbol.hasInstance implementations ensure that instanceof checks work even when the class constructor comes from a different module instance:

import { Ability } from '@serenity-js/core';

// Works correctly even if MyAbility was loaded from a different module instance
if (ability instanceof Ability) {
// ...
}

This applies to Ability, RuntimeError, Outcome and its subclasses, and Key.

Node.js 24 compatibilityโ€‹

Serenity/JS 3.42 includes fixes for Node.js 24:

  • Updated error message formatting for Symbol values
  • Fixed lazy-loading for modules affected by Node.js 24's removal of the legacy http_parser

Migrating from ts-node to tsxโ€‹

If you're using ts-node with Node.js 22+ and experiencing issues, consider switching to tsx:

# .mocharc.yml
require:
- - ts-node/register
+ - tsx
// package.json
"devDependencies": {
- "ts-node": "10.9.2",
+ "tsx": "4.21.0",
}

Updating tiny-typesโ€‹

If your project has a direct dependency on tiny-types, update it to version 2.x:

"dependencies": {
- "tiny-types": "1.24.3",
+ "tiny-types": "2.0.5",
}

The TinyTypes 2.x API is backwards-compatible with 1.x and includes its own ESM support and Symbol.hasInstance fixes.

No breaking changesโ€‹

This release is fully backwards-compatible:

  • All existing require() calls continue to work
  • All existing deep imports (@serenity-js/core/lib/events) continue to work
  • instanceof checks work across ESM/CJS boundaries
  • The tiny-types peer dependency range accepts both 1.x and 2.x

Updating your projectโ€‹

To update your Serenity/JS project to version 3.42.0, run:

npx -y npm-check-updates '/@serenity-js/' -u

Follow the official Serenity/JS installation guide to learn how to automate your dependency updates.

Learn moreโ€‹

Your feedback matters! If you encounter any issues with the new ESM support, please report them on GitHub.