feat(webkit): Angular seam — binding drift gate, SPA assets, CSP + in-tree example - #48
Conversation
… 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>
|
Warning Review limit reached
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 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (29)
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. Comment |
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/webkitand an in-tree Angular example that exercises them — with a gate that runs headless ingo test ./....The problem worth fixing
wails resolves
Call.ByNamethrough an exact-match map on<pkg path>.<receiver type>.<method>(v3 pkg/application/bindings.go:250builds the FQN,:173looks it up). Aliases exist only on the numericCall.ByIDpath, so a renamed receiver cannot be papered over at runtime.That makes the Go struct name part of the wire contract — and the
.Service→.WailsServicerename 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 gate —
BindingNames/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 asDynamicrather 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 intoUnexpected token '<'a page-load later, pointing nowhere);/wails/*is refused so a missingWailsHTTPMiddlewarefails loudly. Index is served no-store and answered directly rather than viahttp.FileServer's 301.CSP/CSPMiddleware— every transport origin contributes both itshttp://andws://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 asTestCSP_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.goneeds 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:
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.//inside"https://…"is not a comment) that preserves byte offsets.Receipts
Real build served by the host:
/,/about,/jobs/buildtext/html)/polyfills-5CFQRCPP.jstext/javascript, 34585 B/chunk-NOTREAL.js/wails/runtime.jsUpstream defect recorded
application.NewBindingsandBindings.Addare both exported, butAddnil-derefs the package-global app while logging (bindings.go:156→application.go:539), so wails' binding registry can't be unit-tested through its exported API. That's whyBindingNamesmirrors wails' reflection rules rather than delegating, and whyinternalBindingMethodshas a test that fails loudly if the two drift.🤖 Generated with Claude Code
Co-Authored-By: Virgil virgil@lethean.io