Skip to content

refactor(client): inline the ANSI conversion, drop a dependency - #2442

Merged
alexander-akait merged 2 commits into
mainfrom
refactor/inline-ansi-html
Sep 30, 2026
Merged

alexander-akait merged 2 commits into
mainfrom
refactor/inline-ansi-html

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Four production dependencies instead of five. Independent of #2439 — branches
off main.

What we used of it

ansi-html-community is 176 lines, of which this project touched about a
third: the sequence regex, the open/close tag maps, the replace loop, and
building those maps from a palette. The rest was surface it never called — the
tags getters with their pre-ES5 defineProperty fallback, a reset()
nobody used, the validation branches a fixed palette literal cannot reach, and
a bin/. All of it shipped into every consumer's browser bundle, from a
package whose last release was 0.0.8 in April 2022, itself a fork of the
abandoned ansi-html.

It is client-src/utils/ansi-html.js now, at 248 lines including the
documentation of why each difference exists.

Verified against it, not assumed

I compared the two over a corpus before replacing anything: identical for
every sequence a build actually produces
. And I checked what that is rather
than guessing — a real babel-loader failure with colour forced emits
[0m, [1m, [22m, [31m, [33m, [36m, [39m, [90m, all
single-parameter, all handled the same way.

Four things it got wrong

  • "transparent" became color:#transparent. That is how the overlay's
    palette says to leave the page's own colour alone, and it was written into a
    hex slot. Not a colour — so the reset worked only because browsers drop an
    invalid declaration, and \u001b[7m (inverse) did nothing whatsoever. The
    declaration is now left out.
  • A multi-parameter sequence matched nothing. \u001b[1;31m stayed in the
    output as literal text, in front of the reader, and the following [0m then
    opened a span that wrapped the remainder. Parameters are applied one by one.
  • \u001b[m — \u001b[0m written short — was left the same way. Read as a
    reset.
  • A closing sequence with nothing open emitted an unmatched </span>. The
    highlighters run after this and wrap their own spans around its output, so a
    stray close could end one of theirs early. Two corpus cases hit it:
    [99m…[39m and [31ma[31mb[39mc, the latter giving …a</span>b</span>c.

The trap

The package initialised its tag tables at module load, via reset().
setColors here is reached only when ansiColors is configured — so without
an unconditional call at load, ANSI would have silently stopped converting
for everyone who never set a palette. The corpus comparison could not have
caught it, because the harness configures the palette itself. There is now a
call at load, where the palette is declared.

Tests

The conversion had no test of its own while it was a dependency — the only
ANSI-adjacent test passed ansiColors and checked the option was honoured,
never feeding a sequence through. This is the one part of the overlay with real
edge cases, so it has 19 now: nesting, backgrounds, tag-style codes, an
unclosed sequence, an unknown parameter, a non-SGR escape left alone, each of
the four fixes, and a palette of one's own including hex, non-hex, one-value
reset and a missing colour.

overlay + ansi suites 71 pass; the full run before splitting this out was e2e
146 across 12 suites and unit 6941 across 18. Lint, both typecheck passes,
spelling, the precompiled schema check and the full build clean.

Worth noting ansi-html-community is a direct dependency of webpack-dev-server
too, for the overlay webpack/webpack-dev-server#5750 deletes — so it drops from
there as well once that lands.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA


Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved rendering of ANSI colors and text styles in the error overlay, including transparent colors, combined formatting codes, and short or multi-parameter reset sequences.
    • Corrected handling of nested, repeated, unmatched, and crossed formatting tags so displayed output closes styles in the proper order.
    • Preserved plain text and non-color escape sequences without applying unintended formatting.

`ansi-html-community` was a third used and two thirds unused: the `tags`
getters and their pre-ES5 fallback, a `reset()` nobody called, and the
validation branches a fixed palette cannot reach. All of it shipped into every
consumer's browser bundle, from a package whose last release was 0.0.8 in
April 2022 — itself a fork of the abandoned `ansi-html`. Four production
dependencies now instead of five.

Checked against the package over a corpus before replacing it: identical for
every sequence a build actually produces. I confirmed what that is rather than
assuming — a real babel-loader failure with colour forced emits
`[0m [1m [22m [31m [33m [36m [39m [90m]`, all single-parameter.

Four things it got wrong, each now tested:

  * `"transparent"`, which is how the palette says to leave the page's own
    colour alone, became `color:#transparent`. Not a colour — so the reset
    worked only because a browser drops an invalid declaration, and the
    inverse sequence did nothing whatsoever.
  * A sequence with more than one parameter (`\u001b[1;31m`) matched nothing,
    leaving the escape in the output as text in front of the reader.
  * `\u001b[m`, which is `\u001b[0m` written short, was left the same way.
  * A closing sequence with nothing open emitted an unmatched `</span>`. The
    highlighters wrap their own spans around this output, so a stray close
    could end one of theirs early.

One trap worth recording: the package initialised its tag tables at module
load via `reset()`, and `setColors` here is only reached when `ansiColors` is
configured. Without an unconditional call at load, ANSI would have silently
stopped converting for everyone who never set a palette — which the corpus
comparison could not have caught, since it configures the palette itself.

The conversion had no test while it was a dependency. It has 19.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
@changeset-bot

changeset-bot Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 464d401

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
webpack-dev-middleware Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 7249dce1-a6dd-4ca0-b0be-b95f39ad2ce4

📥 Commits

Reviewing files that changed from the base of the PR and between b82b504 and 464d401.

📒 Files selected for processing (4)
  • .changeset/refactor-inline-ansi-html.md
  • client-src/utils/ansi-html.js
  • test/ansi-html.test.js
  • types/client/utils/ansi-html.d.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • .changeset/refactor-inline-ansi-html.md
  • types/client/utils/ansi-html.d.ts
  • test/ansi-html.test.js

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.


Walkthrough

The change adds a local ANSI SGR-to-HTML converter with configurable colors and declares its TypeScript interface. The overlay initializes the converter’s default palette and updates it when configureOverlay receives ansiColors. Tests cover conversion behavior, including multiple parameters, resets, unavailable colors, and unclosed styles. The change also removes ansi-html-community from runtime dependencies.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 464d4

The local converter replaces the dependency while preserving overlay escaping and palette integration. No concrete merge-blocking issue is established; merge after normal checks pass.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 464d4

Problem text remains escaped before conversion, and the new converter keeps formatting state local to each message. No introduced security vulnerability was established. Palette values enter generated HTML separately from problem text, however, and their validation behavior has not been fully compared with the removed dependency.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The material exposure is HTML rendered in the consuming browser’s overlay. The inspected reporter keeps problem-message data separate from palette configuration; the evidence does not establish a message-controlled route to palette authority or additional service privileges.

Trust Boundaries and Controls

  • observed — Problem text has HTML-significant characters encoded before ANSI processing. Palette values do not pass through that control: non-hex values are retained and interpolated into generated style attributes. This establishes a configuration-sensitive HTML boundary, but not an introduced vulnerability without attacker reachability and a baseline comparison.
  • observed — The inspected client parses overlay configuration from its webpack resource query and also exposes a JavaScript configuration API. These are distinct from received problem payloads; downstream integrations’ handling of untrusted configuration remains unresolved.

Resilience and Maintainability Implications

  • observed — setColors constructs replacement tables before publishing them, preventing ordinary synchronous updates from exposing partially built tables. The surrounding configuration function mutates its palette first, however, so table publication is not a transaction covering both representations if construction fails.

Hardening Proposals

  • proposed — Define an explicit trusted-palette contract or validate and safely serialize accepted color values before generating HTML. Staging palette validation before publishing configuration and tables would also make rejection and recovery predictable. This is hardening, not a verified PR regression.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main changes: inlining the ANSI conversion and removing the dependency.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 4 files. (1 skipped: 1 …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 2305a1cd-214b-45a9-bf14-e9f1bb583269

📥 Commits

Reviewing files that changed from the base of the PR and between 717a436 and b82b504.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • .changeset/refactor-inline-ansi-html.md
  • client-src/overlay.js
  • client-src/utils/ansi-html.js
  • package.json
  • test/ansi-html.test.js
  • types/client/utils/ansi-html.d.ts
💤 Files with no reviewable changes (1)
  • package.json

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.

Comment thread client-src/utils/ansi-html.js
@codecov

codecov Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.33%. Comparing base (cc0b244) to head (464d401).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2442      +/-   ##
==========================================
+ Coverage   96.22%   96.33%   +0.10%     
==========================================
  Files          20       21       +1     
  Lines        2200     2265      +65     
==========================================
+ Hits         2117     2182      +65     
  Misses         83       83              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The stack held SGR parameters and every close emitted `</span>`. So an
element opened as a tag was closed as a span — `\u001b[3mx` gave `<i>x</span>`
— and an interleaved sequence crossed its tags: `\u001b[3m\u001b[31mx\u001b[23m`
gave `<i><span style="…">x</i></span>`.

Both are inherited from the package this replaced, identically, but leaving
them sat badly next to a changeset claiming to fix unmatched closes. This is
the same class of defect.

Each open element now carries its own closing tag. A close emits the innermost
one, a repeated parameter closes back through everything opened inside it, and
the final cleanup unwinds in reverse. Which parameter a closer belongs to is
still not tracked — `[3m[31mx[23m` cannot close the italic without crossing
the colour span, so the italic runs to the end of the message — but nothing
this produces fails to nest, which is what matters when the highlighters wrap
their own spans around it afterwards.

Found by CodeRabbit on #2442.

Five cases for the reported shapes, and one that walks every three-sequence
combination of twelve sequences and checks each closing tag matches what is
innermost. Six of the 25 fail without this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
@alexander-akait
alexander-akait merged commit 018e219 into main Sep 30, 2026
22 checks passed
@alexander-akait
alexander-akait deleted the refactor/inline-ansi-html branch September 30, 2026 12:34
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.

1 participant