Skip to content

feat: support Node 24 - #88

Open
not-matthias wants to merge 6 commits into
mainfrom
chore/node-24
Open

feat: support Node 24#88
not-matthias wants to merge 6 commits into
mainfrom
chore/node-24

Conversation

@not-matthias

@not-matthias not-matthias commented Aug 26, 2026

Copy link
Copy Markdown
Member

What

Adds Node 24 to CI and moves the repo (and therefore the CodSpeed runs) onto it.

  • .nvmrc and engines.node go to 24.19.0, which switches the CodSpeed workflow (instrumented, simulation, memory, walltime, walltime-macos) and the CI check/release jobs to Node 24
  • the example matrix becomes ["22", "24"], dropping the EOL 18 and 20 entries
  • @types/node moves to ^24

Issues found and fixed

Symptom on Node 24 Cause Fix
every rollup -c rollup.config.ts build fails with SyntaxError: Unexpected identifier 'assert' Rollup defaults output.importAttributesKey to the legacy assert key when bundling the TS config, and Node 24 removed support for it --configImportAttributesKey with in the five build scripts, plus the one config still written with assert
all ESM TypeScript benches fail with Cannot use import statement outside a module esbuild-register's ESM loader no longer intercepts .ts under Node >= 22 module resolution node --import tsx <file> in packages/tinybench-plugin and the four ESM examples; esbuild-register gives way to tsx there
pnpm --filter @codspeed/tinybench-plugin bench fails with ERR_MODULE_NOT_FOUND .../index.ts benches/sample.ts imported "..", and tsx resolves a relative directory import to index.ts instead of the package exports import the plugin by its own name, which still resolves to dist/index.es5.js

CJS examples and @codspeed/benchmark.js-plugin keep node -r esbuild-register; that path works unchanged on 18, 20 and 24.

Why --import tsx and not the tsx CLI

The runner delivers V8 flags with a PATH shim that re-execs $REAL_NODE_PATH $V8_FLAGS "$@", and checkV8Flags reads them back from process.execArgv. The tsx CLI spawns a child through process.execPath carrying only its own loader flags, so that child would bypass the shim and quietly lose --allow-natives-syntax and friends. --import tsx stays in one process.

Verification

On Node 18.20.8, 20.19.2 and 24.19.0:

  • pnpm turbo run build --force: green on all three
  • Node 24: format lint typecheck 24/24 green, including the @types/node bump
  • every bench script the example matrix and the CodSpeed workflow run: green on all three
  • the runner's introspection shim replayed against node --import tsx benches in default and simulation mode: flags harvested and present in process.execArgv, no [CodSpeed] missing required flags warning; the same run without the shim does warn, which confirms the check is live

Wall-time impact of the loader swap is a one-off +23ms at startup (65ms vs 42ms). Bench durations and per-task latencies are unchanged within the local noise floor (repeating one command three times spans 9%).

Release prerequisite

Publishing now authenticates to npm over GitHub OIDC, so a trusted publisher must exist before the next v* tag. npm's token exchange is scoped per package name, so all five published packages need their own entry (npmjs.com → package → Settings → Trusted Publisher → GitHub Actions):

Field Value
Organization or user CodSpeedHQ
Repository codspeed-node
Workflow filename release.yml
Environment name (leave empty)
Allowed actions npm publish

Packages: @codspeed/core, @codspeed/benchmark.js-plugin, @codspeed/tinybench-plugin, @codspeed/vitest-plugin, @codspeed/playwright-plugin.

This needs an npm org owner. Until it lands, a tag push fails at the publish step; re-adding NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} to that step's env restores token publishing.

Note

Unit tests in vitest-plugin and tinybench-plugin fail when the repo is checked out as a git worktree, because getGitDir() looks for a .git directory while a worktree has a .git file, so benchmark URIs pick up an extra path segment. Pre-existing and unrelated to this branch (it reproduces on Node 22 before these commits) and CI is unaffected, so it is left alone here.

`--no-opt` is only an alias for `--no-turbofan`. Maglev is compiled out of
Node < 24 (`v8_enable_maglev=0`), but Node 24 ships it enabled by default, so
hot functions still tier up under the analysis flag set: whole-process
callgrind Ir drops from 167.0M (22.20.0) to 111.9M (24.19.0) on a hot-loop
probe, and steady-state per-iteration cost falls 286.7k -> 64.2k Ir. Adding
`--no-maglev` restores the same tiering state as Node 22
(`%GetOptimizationStatus` 0x8003 on both) and a comparable Ir level (179.5M).

`--scavenge-task` was renamed to `--minor-gc-task` in V8 11.3, so since
Node 20 the analysis flags contained no minor-GC-task suppression at all.
Node 24 dropped support for the legacy `assert { type: "json" }` import
attribute syntax, but rollup still defaults `importAttributesKey` to
`assert` when it bundles the TypeScript config file. That makes every
`rollup -c rollup.config.ts` build fail with
`SyntaxError: Unexpected identifier 'assert'`.

Pass `--configImportAttributesKey with` in the build scripts and
normalize the one config still written with `assert`.
@not-matthias
not-matthias marked this pull request as ready for review August 26, 2026 12:58
@not-matthias

Copy link
Copy Markdown
Member Author

@greptileai review

@greptile-apps

greptile-apps Bot commented Aug 26, 2026

Copy link
Copy Markdown

Greptile Summary

The PR moves repository automation and compatibility coverage to Node 24 while retaining Node 22 example coverage.

  • Migrates ESM TypeScript benchmark entry points from esbuild-register to tsx.
  • Converts Rollup configuration files to native ESM and updates JSON import attributes.
  • Updates Node/V8 runtime flags, native string conversion, CI actions, and npm publication authentication.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains within the follow-up review scope.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/core/src/introspection.ts Extends analysis-mode V8 flags for current Node/V8 garbage collection and Maglev behavior.
packages/core/src/native_core/linux_perf/utils.h Replaces removed V8 string APIs with v8::String::Utf8Value for Node 24 compatibility.
.github/workflows/ci.yml Updates setup actions and moves example compatibility coverage to Node 22 and 24.
.github/workflows/release.yml Updates the release toolchain and switches npm publication from a repository token to trusted-publisher OIDC.
packages/tinybench-plugin/package.json Uses the in-process tsx loader for the plugin’s ESM TypeScript benchmark.
rollup.options.mjs Migrates shared Rollup configuration to native ESM for Node 24-compatible config loading.

Reviews (2): Last reviewed commit: "chore: clear CI warnings" | Re-trigger Greptile

@codspeed-hq

codspeed-hq Bot commented Aug 26, 2026

Copy link
Copy Markdown

Merging this PR will regress 32 benchmarks

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 86 improved benchmarks
❌ 32 regressed benchmarks
✅ 120 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory switch 2 1 B 13,664 B -99.99%
Memory test_iterative_fibo_100 400 B 13,736 B -97.09%
Simulation switch 1 247.4 µs 1,318.5 µs -81.23%
Simulation recursive fibo 10 279.7 µs 1,373 µs -79.63%
WallTime test_recursive_cached_fibo_10 1.1 µs 2.5 µs -57.28%
Simulation short body 2 122.9 µs 215.9 µs -43.09%
Simulation recursive fibo 15 with hooks 381 µs 661.5 µs -42.41%
Simulation recursive fibo 15 with hooks 380.9 µs 661.3 µs -42.4%
Memory test_iterative_fibo_10 400 B 656 B -39.02%
Memory test_recursive_cached_fibo_20 400 B 656 B -39.02%
Memory test_recursive_fibo_20 400 B 656 B -39.02%
Memory test_recursive_cached_fibo_20 400 B 656 B -39.02%
Memory test_recursive_cached_fibo_30 400 B 656 B -39.02%
Memory test_recursive_fibo_20 400 B 656 B -39.02%
Memory test_iterative_fibo_100 400 B 656 B -39.02%
Memory test sync baz 100 400 B 656 B -39.02%
Memory test_recursive_cached_fibo_30 400 B 656 B -39.02%
Memory switch 2 400 B 656 B -39.02%
Memory test sync baz 100 400 B 656 B -39.02%
Simulation test_recursive_cached_fibo_10 52.2 µs 83.4 µs -37.47%
... ... ... ... ... ...

ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing chore/node-24 (534b22a) with main (1999ec9)

Open in CodSpeed

Comment thread .github/workflows/ci.yml Outdated
@not-matthias
not-matthias force-pushed the chore/node-24 branch 2 times, most recently from 6f7391b to 534b22a Compare August 26, 2026 14:28
- bump `.nvmrc` and `engines.node` to 24.19.0, which also moves the
  CodSpeed instrumented, memory and walltime runs to Node 24
- add Node 24 to the example matrix. Its Node 20 entry moves from the
  stale 20.5.1 pin (it only mirrored the old `engines` value) to 20,
  because `node --import` needs `module.register`, added in 20.6.0
- run the ESM TypeScript benches with tsx: esbuild-register's ESM loader
  no longer intercepts `.ts` on Node >= 22, so those benches died with
  `Cannot use import statement outside a module`. `node --import tsx`
  keeps the benchmark in a single process, so the runner's PATH shim
  still delivers the V8 flags through `process.execArgv`
- import the plugin by name in its own bench, since tsx resolves a
  relative directory import to `index.ts` instead of going through the
  package exports
The hardcoded flag list broke on Node 24, which adds --no-maglev; asserting the
plugin wiring instead of duplicating the version-dependent flag list keeps the
test valid across Node majors.
Node 24 bundles npm 11.17.0, clearing the npm >= 11.5.1 floor that made
trusted publishing unavailable on the previous 20.5.1 pin, so publishing no
longer needs a long-lived token.

pnpm 10 shells out to the ambient npm, which performs the OIDC exchange
before any configured token is used, so the publish command itself is
unchanged. Verbose logging keeps the exchange result visible in the release
log.

The exchange is scoped per package name, so every published package needs a
trusted publisher registered for this repository and release.yml.
- bump `actions/setup-node` and `pnpm/action-setup` to the majors that run
  on Node 24, the runners now warn about the Node 20 based ones
- use the `simulation` runner mode in the example matrix, `instrumentation`
  is deprecated
- widen the root `engines.node` to `>=22`, the exact pin made every Node 22
  example job print an unsupported engine warning
- pass `sourceMap`/`outputToFilesystem` to the rollup config plugin, which
  otherwise warns while compiling `rollup.config.ts`
- move the runner mode helpers to their own module: `introspection.ts`
  imported them from the barrel file, so rollup reported a cycle
- ignore the `EVAL` warning for `optimization.ts`, %OptimizeFunctionOnNextCall
  has no non-eval entry point
- build the symbol names with `v8::String::Utf8Value`, `Utf8Length` and
  `WriteUtf8` are deprecated as of the V8 shipped with Node 24
@not-matthias

Copy link
Copy Markdown
Member Author

@greptileai review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants