Electron Stagewright docs

ADR-002: Runtime and Language Choice

Context

Electron Stagewright is an MCP server that drives Electron desktop applications. Before any tool, transport, or plugin can be designed, the project needs a settled answer to four foundational questions:

  1. Which runtime executes the server process? The choices are JavaScript runtimes that can host an MCP SDK and speak to Electron's developer-facing surfaces (the Chrome DevTools Protocol, the Node Inspector protocol, and Playwright's experimental _electron API).
  2. Which language the source is written in. TypeScript vs plain JavaScript is the binary choice; the strictness profile is a second-order choice that materially affects refactor cost.
  3. Which module system the codebase commits to. ESM vs CommonJS is decided once at the root and is expensive to reverse downstream.
  4. Which tooling — package manager, test runner, linter, formatter — the contributor experience is built on. These choices look cosmetic but compound: every contributor pays the cost of every tool in install time and cognitive overhead.

The runtime decision is also entangled with the agent-native UX commitments captured in ADR-007: the principles assume a JavaScript-typed schema layer (Zod), a tested MCP SDK, and a Playwright-style developer surface — all of which are tested-against-Node first. ADR-002 records the runtime + language choice; ADR-007 records the UX shape that runs on top of it.

Closely related is ADR-003: Microsoft's Playwright team has signalled that Playwright's _electron API is experimental and may be deprecated (see Playwright MCP PR #1291). Choosing the runtime that aligns most closely with the MCP SDK and _electron's own testing matrix reduces the blast radius if/when that deprecation lands.

Decision

Runtime

Node.js as the only supported runtime. Minimum version: Node 24. CI exercises both Node 24 and Node 26 so forward-compatibility issues surface before Node 26 becomes the next LTS line.

Language

TypeScript 6+ for all source files. The strict-plus profile committed in tsconfig.base.json enables every relevant safety flag the compiler exposes:

Compiler option Value
strict true
noUncheckedIndexedAccess true
exactOptionalPropertyTypes true
noImplicitOverride true
noFallthroughCasesInSwitch true
noPropertyAccessFromIndexSignature true
verbatimModuleSyntax true
isolatedModules true

Compilation target is ES2023; module resolution is NodeNext.

@types/node tracks the Node 24 major so the type surface matches the runtime floor.

Module system

ESM only. No CommonJS source files, no .cjs entry points, no dual-package shipping. package.json declares "type": "module", every workspace inherits "module": "NodeNext", and path resolution uses import.meta.url rather than __dirname/__filename.

Package manager

pnpm 11+ with workspaces. The lockfile is committed. Contributors enable the version pinned in package.json's packageManager field via corepack enable. The engines.pnpm floor is >=11.0.0 to keep the manifest and this decision in agreement.

Test runner

Vitest 4+ for unit, integration, and example-app tests. Vitest's ESM-first design and vite-bench harness fit the project's module system and the eventual benchmark suite without parallel test runners.

Linter

ESLint 10+ with the flat config format and @typescript-eslint. No legacy .eslintrc files.

Formatter

Prettier 3.8+. Format is decoupled from lint; pnpm format writes, pnpm format:check verifies. No Biome, no dprint at this stage.

Platform matrix

CI exercises the runtime decision on the full cross-platform grid:

Job OS Node
Lint + Typecheck ubuntu-latest 24
Test ubuntu-latest, macos-latest, windows-latest 24, 26
Build ubuntu-latest 24
Example smoke ubuntu-latest 24

The Test job runs the six-cell matrix on every push and PR; Lint, Typecheck, Build, and example smoke are single-cell. The platform commitment is "Linux + macOS + Windows, Node 24 and 26" and is encoded in .github/workflows/ci.yml. Any change to that matrix is a deliberate amendment to this ADR.

Anti-goals

ADR-002 deliberately does not decide the following — those are out-of-scope and tracked elsewhere:

Rationale

Why Node, not Bun or Deno

Why Node 24 floor

Why TypeScript 6 with strict-plus

Why ESM only

Why pnpm 11

Why vitest, eslint flat config, prettier

Alternatives considered

Alternative Why rejected
Bun runtime Faster cold-start and an integrated test runner, but the MCP SDK and Playwright's _electron are tested-against-Node first. Bun's ESM/CommonJS resolver still has known divergences from Node's. Revisit when Bun is the primary upstream runtime for at least one of the two SDKs.
Deno runtime Strong security posture (default-deny capabilities), but requires adapters for the npm ecosystem this project depends on (pnpm workspaces, the MCP SDK, Playwright). The friction is not worth the security upside for a tool whose users will run it from Node-based MCP hosts (Claude Desktop, Cursor, Continue).
Node 20 LTS EOL April 2026 — choosing it now would force a migration within twelve months. Not worth the cost.
TypeScript 5.x Stable, but verbatimModuleSyntax and the latest decorator semantics matter for the plugin model. 6.0+ is mature enough that pinning earlier creates an avoidable upgrade later.
Mixed CJS/ESM Maintenance overhead, dual-package hazard, type-resolution edge cases, and instanceof traps. ESM-only is strictly cleaner.
Jest Slower startup, CommonJS-first, awkward ESM support, and no shared ground with Vite. Vitest dominates on every relevant axis.
Biome (lint+format combined) Promising single-binary tool, but younger than ESLint+Prettier and has fewer rules covering @typescript-eslint-style concerns. Revisit when Biome reaches v2+ stable, or during the next major dependency-refresh window.
npm or yarn (instead of pnpm) Both allow phantom deps. yarn 4 is competitive on speed but lacks pnpm's allowBuilds model. The marginal contributor friction of pnpm is dwarfed by the avoided bug class.

Consequences

Status Update (2026-05-28) — Node 24 LTS floor

The original decision floored the runtime at Node 22 because, at the time of drafting, Node 24 had not yet entered LTS. Node 24 is now the Active LTS line (Node 22 has moved to Maintenance LTS), so the baseline moves up to match the current LTS:

Supersedes note: earlier revisions of this ADR framed Node 22 as the floor and Node 24 as the forward-compat line. The current decision is Node 24 as the floor and Node 26 as the forward-compat test line.

References