Skip to content

feat(webkit): Angular seam — binding drift gate, SPA assets, CSP + in-tree example - #48

Merged
Snider merged 2 commits into
mainfrom
feat/webkit-angular-seam
Aug 8, 2026
Merged

feat(webkit): Angular seam — binding drift gate, SPA assets, CSP + in-tree example#48
Snider merged 2 commits into
mainfrom
feat/webkit-angular-seam

Conversation

@Snider

@Snider Snider commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

go-render could host an Angular app, but nothing in the repo proved it, and nothing coupled the two sides of the binding seam. This adds three public helpers to display/webkit and an in-tree Angular example that exercises them — with a gate that runs headless in go test ./....

The problem worth fixing

wails resolves Call.ByName through an exact-match map on <pkg path>.<receiver type>.<method> (v3 pkg/application/bindings.go:250 builds the FQN, :173 looks it up). Aliases exist only on the numeric Call.ByID path, so a renamed receiver cannot be papered over at runtime.

That makes the Go struct name part of the wire contract — and the .Service.WailsService rename already seen in the wild invalidates every hardcoded call string in a frontend, with no build-time signal. The only symptom is a promise that rejects when a user clicks something.

What's added

Drift gateBindingNames / BindingName / ScanCallByName / UnresolvedBindingNames. Go reports what it exposes, the scanner extracts the frontend's literals, and the mismatch becomes a failing test. Runtime-assembled names are reported as Dynamic rather than dropped, so the gate cannot claim coverage it lacks.

SPAHandler — embedded build or dev-server proxy, exactly one of the two. Deep links serve the shell; a missing bundle 404s instead of receiving HTML (that fallback is what turns a stale chunk reference into Unexpected token '<' a page-load later, pointing nowhere); /wails/* is refused so a missing WailsHTTPMiddleware fails loudly. Index is served no-store and answered directly rather than via http.FileServer's 301.

CSP / CSPMiddleware — every transport origin contributes both its http:// and ws:// form. Permitting only the first yields a policy that passes every page-load check and then silently kills the event channel — that's lthn/desktop #93, now pinned as TestCSP_Ugly.

The example

go/display/webkit/example/angular — a minimal Angular 20 app carrying both receiver shapes, exercising value/error/slice/struct round-trips, a Go→renderer event, hash routing with a path-routed deep-link fallback, and the dev-vs-built split. seam_test.go needs no npm and no WebView. Its README lists what genuinely does need a real WebView.

Two bugs the example found in my own scanner

Both fixed here with tests — this is what building the example was for:

  1. Call.ByName(${PKG}.T.M) — the natural way to write these once a prefix is factored out — was emitted as a binding name, so the gate failed on a correct call. Now unverifiable.
  2. Comments were read as code, so documenting a binding registered a call to it. The example's own doc comment failed its own gate. Fixed with a string-literal-aware stripper (// inside "https://…" is not a comment) that preserves byte offsets.

Receipts

GOWORK=off go test ./...     58 packages ok, 0 failures
GOWORK=off go vet ./...      clean; gofmt clean
npm run build (ui/)          300.68 kB initial, lazy chunk per route

Real build served by the host:

request result
/, /about, /jobs/build 200, app shell (text/html)
/polyfills-5CFQRCPP.js 200, text/javascript, 34585 B
/chunk-NOTREAL.js 404
/wails/runtime.js 404

Upstream defect recorded

application.NewBindings and Bindings.Add are both exported, but Add nil-derefs the package-global app while logging (bindings.go:156application.go:539), so wails' binding registry can't be unit-tested through its exported API. That's why BindingNames mirrors wails' reflection rules rather than delegating, and why internalBindingMethods has a test that fails loudly if the two drift.

🤖 Generated with Claude Code
Co-Authored-By: Virgil virgil@lethean.io

Snider and others added 2 commits August 8, 2026 09:48
… CSP

Three public helpers for the surface an Angular renderer actually leans
on, each one closing a failure that has already been paid for downstream.

binding_names.go / binding_scan.go — the drift gate. wails resolves
Call.ByName through an exact-match map over
`<pkg path>.<receiver type>.<method>` (v3 pkg/application/bindings.go:250
builds the FQN, :173 looks it up; aliases exist only on the numeric
Call.ByID path). So the Go STRUCT NAME is wire contract: the `.Service.`
→ `.WailsService.` rename seen in the wild silently invalidates every
hardcoded call string, and nothing in either build couples the two sides.
BindingNames/BindingName report what Go exposes, ScanCallByName extracts
the literals a frontend calls, UnresolvedBindingNames names the drift.
Runtime-assembled calls are reported as Dynamic rather than dropped, so
the gate cannot claim coverage it does not have.

assets.go — SPAHandler, the embedded-vs-dev-server split. Deep links
fall back to index.html so a reload survives; a MISSING bundle 404s
instead of receiving HTML, which is what turns a stale chunk reference
into "Unexpected token '<'" a page-load later with nothing pointing at
the cause; /wails/* is refused so a missing WailsHTTPMiddleware fails
loudly rather than feeding HTML to the runtime's script tag. The index is
served no-store (it names the hashed bundles) and answered directly
rather than via http.FileServer's 301.

csp.go — CSP/CSPMiddleware. Each transport origin contributes BOTH its
http:// and its ws:// form, because permitting only the first yields a
policy that passes every page-load check and then kills the event
channel — lthn/desktop #93, encoded as TestCSP_Ugly.

Receipt: GOWORK=off go vet + go test ./display/webkit/ green; 7 Examples
with real // Output: blocks; AX-7 Good/Bad/Ugly on each new symbol.

Co-Authored-By: Virgil <virgil@lethean.io>
An Angular application hosted by display/webkit, in-tree, so the seam a
real consumer depends on can be reproduced and fixed here rather than in
the consumer. A public feature ships with its example; this is the one
for the seam helpers in the previous commit.

The example carries BOTH receiver shapes found in the wild —
RunnerService (`.Service`) and StatsWailsService (`.WailsService`) — and
exercises value, error, slice and struct round-trips, a Go→renderer
event, hash routing with a path-routed deep-link fallback, and the
dev-server vs built-bundle split.

seam_test.go is the gate, and it runs headless in `go test ./...` with no
npm and no WebView: it scans ui/src for Call.ByName literals, resolves
every one against the services the host actually binds, and asserts the
converse too (no bound method is unreachable, no call name is assembled
at runtime). Plus CSP-per-transport, asset routing, middleware
composition, app-scoped window state, and an actionable error for an
unbuilt frontend.

Building it surfaced two bugs in the scanner shipped one commit earlier,
both fixed here with tests:

  - an interpolated template literal — Call.ByName(`${PKG}.T.M`), the
    natural way to write these — was emitted as a binding NAME, so the
    gate would fail on a perfectly correct call. Now reported as
    unverifiable instead.
  - comments were read as code, so DOCUMENTING a binding registered a
    call to it. The example's own doc comment failed its own gate. Now
    stripped by a string-literal-aware pass (a `//` inside "https://…"
    is not a comment) that preserves byte offsets.

Also recorded: application.Bindings.Add nil-derefs the package-global
app while logging (wails v3 bindings.go:156 → application.go:539), so
wails' own binding registry cannot be unit-tested through its exported
API. That is why BindingNames mirrors wails' reflection rules rather than
delegating, and why internalBindingMethods has a test that fails if the
two drift.

Receipts:
  GOWORK=off go test ./...          58 packages ok, 0 failures
  GOWORK=off go vet ./...           clean; gofmt clean
  npm run build (ui/)               300.68 kB initial, lazy chunk per route
  real build served by the host     / and /about and /jobs/build -> 200 shell,
                                    /polyfills-5CFQRCPP.js -> 200 text/javascript,
                                    /chunk-NOTREAL.js and /wails/runtime.js -> 404

Co-Authored-By: Virgil <virgil@lethean.io>
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@Snider, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 22 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b1f47881-fbaf-452e-8db5-a581bc2bf3ae

📥 Commits

Reviewing files that changed from the base of the PR and between 95593e4 and 87419be.

⛔ Files ignored due to path filters (1)
  • go/display/webkit/example/angular/ui/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (29)
  • README.md
  • go/display/webkit/assets.go
  • go/display/webkit/assets_test.go
  • go/display/webkit/binding_names.go
  • go/display/webkit/binding_names_test.go
  • go/display/webkit/binding_scan.go
  • go/display/webkit/binding_scan_test.go
  • go/display/webkit/csp.go
  • go/display/webkit/csp_test.go
  • go/display/webkit/example/angular/README.md
  • go/display/webkit/example/angular/bindings.go
  • go/display/webkit/example/angular/cmd/webkit-angular/main.go
  • go/display/webkit/example/angular/host.go
  • go/display/webkit/example/angular/seam_test.go
  • go/display/webkit/example/angular/ui/.gitignore
  • go/display/webkit/example/angular/ui/angular.json
  • go/display/webkit/example/angular/ui/package.json
  • go/display/webkit/example/angular/ui/src/app/about.page.ts
  • go/display/webkit/example/angular/ui/src/app/app.config.ts
  • go/display/webkit/example/angular/ui/src/app/app.routes.ts
  • go/display/webkit/example/angular/ui/src/app/app.ts
  • go/display/webkit/example/angular/ui/src/app/jobs.page.ts
  • go/display/webkit/example/angular/ui/src/app/wails.service.ts
  • go/display/webkit/example/angular/ui/src/index.html
  • go/display/webkit/example/angular/ui/src/main.ts
  • go/display/webkit/example/angular/ui/src/styles.css
  • go/display/webkit/example/angular/ui/tsconfig.app.json
  • go/display/webkit/example/angular/ui/tsconfig.json
  • go/display/webkit/seam_example_test.go

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


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.

@Snider
Snider merged commit efbd99d into main Aug 8, 2026
2 checks passed
@Snider
Snider deleted the feat/webkit-angular-seam branch August 8, 2026 09:05
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