feat: v0.2.0 — DOM-race hardening, tables, iframes, Shadow DOM - #1
Merged
Conversation
* dom-extractor.ts: null-safe `document.body` access. Crash trace was `Cannot read properties of null (reading 'children')` mid- navigation; the wait-strategy didn't always catch the race. * wait-strategy.ts: explicit `waitForFunction(() => document.body)` before observer attaches. Body-existence guards inside the observer script + scroll-to-bottom too. * converter.ts / wait/* / mutation-observer: switch type imports from `playwright` to `playwright-core`. The reference implementation was carrying a peer dep on the larger `playwright` package; consumers using `playwright-core` (lighter, server-side default) hit nominal-type-mismatch errors at the boundary. * tsconfig.lib.json: include DOM lib so `document` references in arrow functions passed to `page.waitForFunction` type-check without leaking `any`. * package.json: peerDep flips `playwright` → `playwright-core`.
Closes the largest "structured DOM data" gap. Tables previously fell
through generic-container handling and emerged as flat cell text with
no row/column structure preserved — LLMs had to *infer* tabular shape
from sequential text.
* dom-extractor.ts: new `emitTable(table)` walker. Picks up:
- <caption> or aria-label as caption
- <thead><tr><th> as headers (or first row if all <th>)
- tbody rows (or direct <tr> children if no tbody)
- cell text from <td>/<th> via .textContent, whitespace-collapsed
- 200 rows × 30 cols cap for snapshot-size safety
* New BodySegment kind: `{ kind: 'table', headers?, rows, caption? }`
* body-builder.ts: renders to GitHub-flavored markdown — pipes,
separator row, escapes `|` inside cells, normalizes newlines to
spaces. LLMs read GFM tables natively.
* Tests for headers, header synthesis, captions, escaping, padding.
Shadow DOM piercing: * In `processNode`, walk `el.shadowRoot.children` before tag-specific handling. Modern web components (Salesforce Lightning, MS 365, every Lit/Polymer/Stencil app) hide interactive content behind shadow roots. Without piercing, the extractor saw an empty `<my-button>` with no text or actions. * Open shadow roots only — closed ones are deliberately unreachable to script (browser security model). Iframe traversal: * Same-origin iframes: walk `contentDocument.body` inline so the consumer sees iframe content as part of the parent. Most embedded forms / docs / maps fit this case. * Cross-origin iframes: `contentDocument` access throws SecurityError; emit an `[IFRAME:n]` tag with src URL + title so the consumer at least knows the iframe exists. (Cross-frame action dispatch is consumer-side concern.) * Adds `IFRAME_OPEN` / `IFRAME_CLOSE` boundary tags around inlined content.
…extractor Three fixes from CI run #24966542292: 1. `typescript` was missing from devDependencies even though the build script runs `tsc`. Added `^5.4.0`. 2. `package-lock.json` didn't exist, breaking `npm ci` in the workflow. Generated. 3. The third commit on this branch (iframes + Shadow DOM) overwrote the table type addition + emitTable helper + processNode branch from the second commit (table support). Re-applied them on top so the BodySegment union, the helpers, and the processNode handler are all in dom-extractor.ts together. Verified locally: `npm run build` is clean, all 90 tests pass (including the 5 new table tests).
`npm ci` fails when the lockfile was generated on a different OS than the CI runner — the rollup native bindings (and other platform-specific optional deps) get omitted from the lockfile and then can't be found at install time. Known npm bug: npm/cli#4828 Switch both CI jobs to `npm install --no-audit --no-fund` which fetches platform-appropriate optional deps. Slightly less reproducible but cross-platform CI works again. Until the npm bug is fixed, this is the standard workaround used by vitest, rollup, esbuild, swc, and many other native-bindings projects.
…und) Standard practice for publishable npm libraries: no committed lockfile, downstream consumers resolve their own deps. Doubly necessary here because of the npm cross-platform optional-deps bug (npm/cli#4828) — lockfiles generated on one OS omit the native binding packages other OSes need (rollup, esbuild, swc), breaking CI when the lockfile + runner OS mismatch. Adding lockfiles to .gitignore alongside node_modules and dist.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
v0.2.0 — three improvements driven by real-world deployment
These changes were proven in production at thinkbrowse.io before being upstreamed. We hit each issue running agentmark in a real agent loop and decided the fixes belong here, in the canonical reference implementation, so other consumers benefit too.
1. DOM-race hardening
document.bodyaccess in the extractor. The crash trace wasCannot read properties of null (reading 'children')mid-navigation —domcontentloadedcan fire before the new body is attached.waitForFunction(() => document.body)in the wait strategy before mutation observers attach.2. Type-import alignment:
playwright→playwright-coreThe library imported
Pagefrom the fullplaywrightpackage. Server-side consumers using the smallerplaywright-corehit a nominal-type mismatch and had to cast throughany. Switched allimport type { Page } from 'playwright'→'playwright-core'. Same runtime, cleaner contract.Peer dep + tsconfig adjusted accordingly (DOM lib added so
() => document.bodyarrow functions type-check without leakingany).3. Semantic table support
Tables previously fell through generic-container handling and emerged as flat cell-text streams. LLMs had to infer rows/columns from sequence — fragile and breaks on empty cells.
New
emitTablewalker recognizes<table>, picks up<thead>/ first-row-of-<th>as headers, walks<tbody>rows, and renders to GitHub-flavored markdown:LLMs read this natively. New
BodySegmentkind:{ kind: 'table', headers?, rows, caption? }. Tests cover: header synthesis, captions, pipe/newline escaping, short-row padding.4. Shadow DOM piercing + iframe handling
processNodewalksel.shadowRoot.childrenbefore tag-specific handling. Web components (Salesforce Lightning, MS 365, every Lit/Polymer app) become visible. Open shadow roots only.contentDocument.bodyinline so consumers see iframe content as part of the parent.contentDocumentaccess throws SecurityError; emit[IFRAME:n]tag with src URL + title so consumers know it's there. NewIFRAME_OPEN/IFRAME_CLOSEboundary tags.Test plan
Versioning
Bumped to
0.2.0(minor — additive features, no breaking changes for existing consumers).