Repository navigation
Switch to module nodenext and Node 22.12+ to unblock ESM-only dependencies (precursor to #95) #113
Description
Problem
The package is CommonJS (package.json has no type field, tsconfig.json has module: node16, TypeScript 6.0.3, engines.node >20.0.0). TypeScript refuses to compile a static import of an ESM-only package from a CommonJS module under node16, so every ESM-only dependency major is currently unmergeable:
- chore(deps): bump puppeteer from 24.43.1 to 25.9.0 #108 puppeteer 24.43.1 → 25.9.0: Build fails with 12×
TS1479(7 files insrc/, 4 intest/) plus 1×TS1541insrc/shared/env.ts(type-only import needs aresolution-modeattribute). Run 33741760401. - chore(deps-dev): bump the chai-stack group across 1 directory with 5 updates #102 chai 6.2.2 / chai-as-promised 8.0.2 / sinon-chai 4.0.1 (+
@types): merged, brokemainwith the sameTS1479, reverted in fix(deps-dev): revert chai stack to CommonJS-compatible versions #111. Dependabot now ignores chai-stack majors.
#95 proposes a full dual ESM/CJS build to fix this. That is a large change to the published contract. There is a much smaller first step.
Proposal
tsconfig.json:module: node16→module: nodenext.package.json:engines.node→>=22.12.0.
Node 22.12+ supports require(esm) natively and without a flag, and TypeScript ≥ 5.8 under nodenext type-checks a CommonJS import of an ESM package as a require() call. The emitted output stays CommonJS, main/exports do not change, and no bundler is needed. Puppeteer 25 already declares engines.node >=22.12.0, so the floor bump is unavoidable for #108 regardless.
Verified findings
Environment: Node v22.22.0, npm 10.9.4, TypeScript 6.0.3, macOS. Scratch branch on top of #108 (puppeteer 25.9.0), discarded afterwards.
Build
- Baseline (
module: node16): reproduced all 13 errors from CI. module: nodenext, nothing else changed:npm run buildclean, 0 errors. TheTS1541inenv.tsalso disappears; noresolution-modeattribute needed.- Then re-applied the chore(deps-dev): bump the chai-stack group across 1 directory with 5 updates #102 versions on top (chai 6.2.2, chai-as-promised 8.0.2, sinon-chai 4.0.1, @types/chai-as-promised 8.0.2, @types/sinon-chai 4.0.0; @types/chai left at 4.3.20 as in chore(deps-dev): bump the chai-stack group across 1 directory with 5 updates #102): still 0 errors.
- Emitted
dist/is still CommonJS ("use strict",Object.defineProperty(exports, ...),require("...")). - The
PuppeteerNodeLaunchOptions→LaunchOptionsrename mentioned in Migrate package to ESM (dual build), following apex-parser #95 is already absorbed onmain(src/shared/env.tsimportsLaunchOptions). Nothing to do. - ts-patch transform (
scripts/apexLoader.ts) still runs undernodenext(Inlining apex: .../GovernorLimits.apexprinted, string inlined). typeormexperimentalDecoratorsstill emit viatslib.__decorate.npx typedocstill generates (0 errors, 14 pre-existing warnings).eslint ./src ./test ./test_system: 0 errors, 6 pre-existing warnings.
Runtime require(esm)
- puppeteer 25.9.0 and puppeteer-core 25.9.0 are
"type": "module",engines.node >=22.12.0. Theexports["."]map points bothimportandrequireat the same ESM filelib/puppeteer/puppeteer.js, so it relies onrequire(esm). node -e "require('puppeteer')"on 22.22.0 succeeds,launchis a function, noERR_REQUIRE_ASYNC_MODULE, no warning printed. The entry graph has no top-level await.require('chai'),require('chai-as-promised'),require('sinon-chai')all succeed.npm ls extract-zipis empty with puppeteer 25 (the Dependabot alert dependency is gone from the tree). Remainingnpm auditentries arediffandmocha, unrelated.
Tests (npm test)
- Result with
nodenext+ puppeteer 25, identical with chai 4.5.0 and with chai 6.2.2: 254 passing, 19 pending, 1 failing. Coverage thresholds met. - The failure is real and caused by puppeteer 25 being ESM, not by
nodenextor chai:test/testTemplates/formLoadWithDataLoadingTestTemplate.test.ts:48 stub(puppeteer, 'launch') → TypeError: ES Modules cannot be stubbedrequire(esm)returns a sealed module namespace object, so sinon cannot replacelaunchon it. This is the only place intest/that stubs a puppeteer export. Because it is abeforeEachhook, the rest of thatdescribeis skipped, which is why the count differs from the 257 passing onmain. Fix: routelaunch()insrc/testTemplates/formLoadTestTemplate.tsandformLoadWithDataLoadingTestTemplate.tsthrough a small internal wrapper module and stub that instead. - Loader interaction: the
--loader ts-node/esm --experimental-specifier-resolution=nodeinNODE_OPTIONSis what actually carries the suite. mocha 11 loads test files viaimport(); Node 22 strips types from.tsnatively, so without the loader the run fails immediately withERR_MODULE_NOT_FOUNDon the first extensionless relative import (src/database/alertInfo).ts-node/registerin.mocharc.ymlis effectively redundant for.tsfiles. This is pre-existing and unchanged bynodenext; both work as-is. Node printsExperimentalWarning: --experimental-loader may be removed in the future; instead use register(). Moving to the--import+module.register('ts-node/esm')form is an optional follow-up, not required here.
Versions / CI
@types/nodelatest 22.x is 22.20.1. Currently 20.19.43 and Dependabot ignores majors for it, so it needs one manual bump.Build.ymlandPublish.ymlalready useactions/setup-node@v7withnode-version: 22, which resolves to the latest 22.x (≥ 22.12), so they already satisfy the floor. No change strictly required.lint-staged@17.4.1declaresnode >=22.22.1; npm warnsEBADENGINEon 22.22.0. Unrelated to this change but relevant when picking the floor wording.
Scope of change
| File | Change |
|---|---|
tsconfig.json |
module: "node16" → "nodenext" |
package.json |
engines.node → >=22.12.0; puppeteer 25.9.0 (take #108); chai 6.2.2, chai-as-promised 8.0.2, sinon-chai 4.0.1, @types/chai-as-promised 8.0.2, @types/sinon-chai 4.0.0 (re-apply #102); @types/node → 22.x; version → 8.0.0 (see below) |
package-lock.json |
regenerate |
src/testTemplates/formLoadTestTemplate.ts, formLoadWithDataLoadingTestTemplate.ts |
call launch through an internal wrapper (e.g. src/shared/browser.ts) so tests can stub it |
test/testTemplates/formLoadWithDataLoadingTestTemplate.test.ts |
stub the wrapper instead of puppeteer.launch |
.github/dependabot.yml |
remove the chai-stack major ignore block and its comment; keep the @types/node ignore |
.github/workflows/Build.yml, Publish.yml |
no change needed (node-version: 22 already ≥ 22.12). Optionally pin 22.12 or later explicitly for clarity |
CHANGELOG.md, docs/user |
document the Node 22.12 minimum |
.mocharc.yml / test script |
no change required; optional cleanup of the redundant ts-node/register and the deprecated --loader form |
Out of scope
The dual ESM/CJS build stays in #95. After this change:
- The dependency-unblocking motivation for Migrate package to ESM (dual build), following apex-parser #95 is gone (both blocked stacks build and load).
- The codebase already type-checks under
nodenext,moduleResolutionis settled, and the ESM/CJS import audit for puppeteer/chai is done. - What remains for Migrate package to ESM (dual build), following apex-parser #95 is purely the publishing contract: adding an ESM entry point (
"type"/exports.importcondition, an ESM emit,.d.ts/.d.ctssplit) and reconciling the test runner. Migrate package to ESM (dual build), following apex-parser #95 can be re-scoped to that and reprioritised.
Risks and open questions
- Node 20 consumers cannot take this release.
enginesmoves from>20.0.0to>=22.12.0. Node 20 reached end-of-life on 2026-04-30, but any consumer still on it is pinned to 7.x. This is a breaking change to the supported runtime and should ship as 8.0.0. - Published type declarations import from
puppeteer.dist/types/src/shared/env.d.ts,uiHelper.d.ts,services/metrics.d.ts,services/navigate*.d.tsand both form-load template.d.tsfiles import puppeteer types, andEnv,UiHelperand the templates are re-exported fromindex. A consumer compiling withmodule: node16/commonjs, TypeScript < 5.8 andskipLibCheck: falsewould seeTS1479/TS1541inside our declarations. Consumers on TS ≥ 5.8 withnodenext, or withskipLibCheck: true, are unaffected. Worth a note in the changelog; confirm how downstream projects compile. require(esm)moves a class of failure from compile time to run time. If a future version of puppeteer (or a transitive dependency) introduces top-level await,requirethrowsERR_REQUIRE_ASYNC_MODULEat load rather thantscfailing. The unit tests exercise the imports, so CI still catches it.target: es2020can stay for now; raising it toes2022is a separate, safe follow-up once Node 22 is the floor.- The
--experimental-loaderdeprecation warning is noise today; Node may eventually remove the flag, at which point the--import/register()form becomes mandatory.
Acceptance criteria
-
tsconfig.jsonusesmodule: nodenext;engines.nodeis>=22.12.0; version bumped to 8.0.0 with changelog entry. - chore(deps): bump puppeteer from 24.43.1 to 25.9.0 #108 (puppeteer 25.9.0) rebased onto this change builds green.
- The chai 6 / chai-as-promised 8 / sinon-chai 4 bump (versions from chore(deps-dev): bump the chai-stack group across 1 directory with 5 updates #102) builds green and the Dependabot ignore rules from fix(deps-dev): revert chai stack to CommonJS-compatible versions #111 are removed.
-
npm testpasses fully (257 tests as onmain), including theformLoadWithDataLoadingTestTemplatesuite withlaunchstubbed via a wrapper. -
npm run buildoutput indist/is still CommonJS andnpm run doc:generatesucceeds. - The extract-zip Dependabot alert clears (
npm ls extract-zipempty). -
@types/nodeon 22.x.
Activity