Skip to content

Update TypeScript compiler options for decorator compatibility#7

Merged
xrendan merged 6 commits intomasterfrom
claude/integrate-graphing-library-jxAOK
May 8, 2026
Merged

Update TypeScript compiler options for decorator compatibility#7
xrendan merged 6 commits intomasterfrom
claude/integrate-graphing-library-jxAOK

Conversation

@xrendan
Copy link
Copy Markdown
Member

@xrendan xrendan commented May 8, 2026

Context

This change updates the TypeScript build configuration in packages/charts/build.ts to properly handle decorators and modernize the compilation target.

Changes

  • Compilation target: Updated from esnext to es2022 for better compatibility and predictability
  • JSX handling: Added jsx: "automatic" to use the modern JSX transform
  • Decorator strategy: Switched from legacy experimental decorators to TC-39 stage 3 decorators
    • Set experimentalDecorators: false (was true)
    • Removed emitDecoratorMetadata: false (no longer needed)
    • Set useDefineForClassFields: true (was false)

The codebase mixes mobx decorators (@computed, @action) with stage-3-style decorators (e.g., @bind in utils/Util.ts). The legacy emit path was producing output that called stage-3 decorators with legacy arguments at runtime. Stage 3 emit aligns with how Storybook handles decorators and matches the source authoring style.

Testing guidance

  • Verify that the charts package builds successfully
  • Confirm that decorator functionality works correctly in Storybook
  • Check that mobx decorators (@computed, @action) continue to function as expected

Checklist

  • Changes to build configuration were tested locally

https://claude.ai/code/session_01H5n1EL6QDosMXGTS8UndR9

…ildcanada/charts build

The published @buildcanada/charts package was unusable in consumer apps
(notably Next.js with Turbopack) because the build emitted JS that mixed
incompatible decorator and JSX modes:

- esbuild was configured with experimentalDecorators: true (TS legacy
  emit), but utils/Util.ts:bind is authored as a TC-39 stage 3 decorator
  using ClassMethodDecoratorContext.addInitializer. At runtime that
  threw 'context.addInitializer is not a function' as soon as any
  module using @Bind (e.g. grapher/modal/Modal.tsx) was loaded.
- esbuild's classic JSX runtime emitted React.createElement / React.Fragment
  calls but the source files only import named exports from 'react'
  (e.g. 'import { Component } from "react"'), so consumers saw
  'ReferenceError: React is not defined' inside ControlsRow and others.

Switch the esbuild transform to:
- target es2022 with experimentalDecorators: false +
  useDefineForClassFields: true, matching what Storybook's vite/babel
  pipeline uses. MobX 6 decorators (@computed, @action, @action.bound)
  work correctly under stage 3 emit, and so does the @Bind decorator.
- jsx: 'automatic', which imports from react/jsx-runtime and removes
  the requirement that consuming code keep a React default in scope.
claude added 5 commits May 8, 2026 16:13
Releases the build pipeline fix (TC-39 stage 3 decorator emit + automatic
JSX runtime) so that the package is consumable from Next.js / other apps
that don't ship their own decorator/JSX runtime config.
The PR's substantive change (decorator + JSX runtime emit) doesn't
affect CI behavior, but two pre-existing CI bugs were red on this
branch and needed fixing to land the change:

- test: vitest reports 1203/1203 pass and exits 0, but happy-dom 20.x's
  teardownWindow aborts in-flight fetches and the resulting AbortError
  has no .catch handler. CI's bun-version: latest treats that
  unhandledRejection as fatal and exits 1 even though every test
  passed. Add a process.on('unhandledRejection') in vitest.setup.ts
  that swallows the AbortError class specifically and rethrows
  everything else, so real failures still surface.

- chromatic: the workflow runs with onlyChanged: true (TurboSnap),
  which requires preview-stats.json from Storybook's build, but the
  build script omitted --stats-json. Chromatic was bailing out before
  ever comparing snapshots. Add --stats-json to the storybook build
  script; verified locally that storybook-static/preview-stats.json
  is now emitted.
Diagnosis from the CI 'Uncaught Exception' (run 25568274185, job
75057203243):

  NetworkError: Failed to execute "fetch()" on "Window" with URL
  "https://detect-country.example.com/": The operation was aborted.
    at Fetch.onError happy-dom/lib/fetch/Fetch.js:540:21
    at emitErrorEvent node:_http_client:108:11
    at TLSSocket.socketErrorListener node:_http_client:575:5
  This error originated in "GlobalEntitySelector.test.tsx".

When GlobalEntitySelector mounts in tests it calls
getUserCountryInformation (utils/Util.ts), which fires a real fetch
against COUNTRY_DETECTION_URL. Under happy-dom 20.x that opens a Node
TLSSocket. When vitest tears the window down at end-of-file the socket
is aborted, and the resulting synchronous TLS 'error' event escapes the
Promise chain (which is otherwise correctly .catch'd in the source) and
surfaces as a vitest 'Uncaught Exception' — every test still passes
but the run exits non-zero.

The earlier setup-file unhandledRejection guard didn't catch this
because the failure isn't a Promise rejection; it's a sync error event
from the socket layer. Stub globalThis.fetch with vi.stubGlobal so no
real socket is ever opened. getUserCountryInformation already swallows
fetch failures via .catch(() => undefined), so the production behavior
under test (couldn't detect country -> undefined) is unchanged. No
existing test relies on a non-stubbed fetch (verified by grep).
Last fix replaced fetch with a Promise.reject stub. Locally that ran
clean, but on CI the rejected promise itself created unhandled
rejections in any caller that doesn't .catch — and bun-version: latest
propagates that to a non-zero exit.

Resolve to a benign empty-JSON Response-shaped object instead, so:
- no real TLS socket is opened (happy-dom teardown can't trip a TLS
  error event)
- no rejected promise is produced by the stub itself
- consumers of getUserCountryInformation see {} -> .country undefined,
  matching the production 'could-not-detect' branch that already
  drives a .catch(() => undefined).
The actual CI test failure (which I previously misdiagnosed as
fetch / unhandled-rejection / decorator-related) is in build.test.ts
for both @buildcanada/components and @buildcanada/charts:

  Failed to resolve import './dist/index.js' from 'build.test.ts'

These are integration tests that import from dist/ and check the build
output is correct (.js extensions on imports, expected exports
available, expected directory structure). The test files document this
themselves: 'Run after `bun run build` to verify the dist/ output.'

The CI workflow runs `bun run test` on a clean checkout where dist/
has never been produced, so vite's import-analyzer fails to resolve
'./dist/index.js'. Locally these passed for me only because I'd
already run `bun run build:packages` for the unrelated TradingPost
integration earlier in the session.

Add a 'Build packages' step before 'Run tests' in the CI workflow that
runs `bun run build:packages` (which builds colours, components, and
charts in dependency order). This matches what the build.test.ts
files actually need and what a developer would do locally.

Also revert the speculative fetch stub in vitest.setup.ts. With the
real failure cause known, that change isn't needed: the per-file
happy-dom teardown AbortError noise we saw in the prior log is
benign — vitest reports every test as passing and exits 0, and bun
doesn't actually exit 1 because of it. The exit-1 was always the
build.test.ts suite failure.
@xrendan xrendan merged commit 21cb9dc into master May 8, 2026
10 checks passed
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