The WebSocket origin check compared the browser's Origin host, which
carries the port, against configured domains, which do not. publicOrigin
only ever matches domains[0], so every other domain fell through to that
comparison and was denied purely on the port: web.localhost:3000 never
matched web.localhost.
The result was a 403 on the HMR upgrade and a client reconnecting
forever, while the page itself loaded fine because HTTP routing resolves
the Host separately.
Compares hostnames now. Unrelated and lookalike-suffix origins are still
denied, and both cases are covered by tests.
Verified through a real gateway: the HMR socket opens on both localhost
and web.localhost, and a live edit reaches the browser.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
window.__wrnI18n was undefined in development: the payload shipped as an
executable inline script, and a document's CSP nonce is fixed at load, so
any such script arriving from a later response is blocked. Client
translations and language switching silently had no data.
The payload is now a type="application/json" block, which the browser
never executes and script-src therefore never applies to. The i18n
runtime, CSR navigation, and HMR all read the block instead of matching
window.__wrnI18n= with a regex.
Pages now render zero executable inline scripts, so an inline script-src
violation is structurally impossible rather than merely unobserved. Zero
framework JavaScript on island-free routes is unaffected: the block is
inert data, and nothing loads to read it unless the page needs it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A document's CSP nonce is fixed at load, so an inline script delivered by
a later response can never carry a nonce that document accepts. The HMR
client is now served at /__wrnexus/hmr-client.js, which script-src 'self'
already covers and which needs no nonce at all.
This removes one of the two inline scripts CSP was blocking in
development. The i18n data script is still blocked and needs the same
treatment; it is shared with the CSR navigation and HMR parsers, so
moving it spans @wrnexus/i18n, csr, and dev-server.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds gatewayWebSocketBackendHeaders so proxied upgrades carry application
identity while Bun keeps ownership of WebSocket framing.
Pre-existing working-tree change, committed as-is rather than authored
here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Component discovery is not utility-source discovery: scanning all
built-in and plugin component directories made Tailwind/Iconify generate
rules for components the app never renders. Packages that need scanning
opt in through styles.source.
Pre-existing working-tree change, committed as-is rather than authored
here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Raises the typescript devDependency across the workspace, bumps package
versions, re-adds ignoreDeprecations, and repoints the @wrnexus registry.
These were pre-existing working-tree changes, committed as-is rather than
authored here. The .npmrc change redirects @wrnexus publishes from
registry.npmjs.org to registry.workroot.in — confirm that is intended
before publishing from this branch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The HMR client script was dead in the browser. HMR_CLIENT_JS is a
TypeScript template literal, so the regex [ \t\r\n] inside it was expanded
into real control characters, producing a regex literal containing a raw
newline — a syntax error that took the whole script down with "Invalid
regular expression: missing /". It now uses \s, and a test asserts the
emitted client parses and holds no control characters inside regex
literals; that test fails if the bug is reintroduced.
HMR also corrupted CSP nonces. A document's nonce is fixed at load, but
morph copied attributes from freshly fetched HTML, overwriting the live
nonce with one the browser will not honour. syncAttrs now leaves nonce
alone, and nodes moved across are re-stamped with the live nonce.
Islands vanished on every HMR update: morph puts the server placeholder
back over the mounted island. The island runtime now remounts on
wrnexus:hmr-updated. Remounting swaps the container for a bare clone —
re-rendering the existing root is a no-op once HMR has wiped the DOM
externally, and unmounting throws asynchronously because the nodes React
wants to remove are already gone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three bugs found by driving the dev server rather than reading code:
1. An island used inside a .wrn component still emitted a component mount
— only the page and nested-page render paths were covered.
2. Editing an island .tsx never rebuilt in dev. The bundle cache was keyed
on source path alone, and page modules are cached after the first
request so no compile runs to notice the change. The cache key now
includes mtime, and the file watcher rebuilds islands whose .tsx
changed.
3. A .wrn cache hit skipped island building entirely, so after a restart
with a warm cache no island bundle was ever produced. Island inputs are
now persisted beside the other artifacts and rebuilt on a cache hit.
The islands manifest is deliberately excluded from the artifact
completeness check: only the async compile path writes it, so requiring it
made the sync path miss the cache on every call.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The island pieces existed but nothing connected .wrn compilation to island
emission. Now:
- codegen emits a data-wrn-island placeholder for component tags bound to
.tsx imports, keeping .wrn components on the normal mount path
- the dev pipeline and static build resolve island imports, thread the
names into codegen, and build the bundles
- collectScripts adds /__wrnexus/islands.js only when island markup is
present, so island-free pages still ship nothing
- island routes classify as static-interactive via hasIslands
Three bugs found by driving a real page in the browser:
1. The mount runtime was never built anywhere, so the bootstrap 404'd and
no island mounted.
2. Building the runtime separately from the islands gave each its own copy
of React: "Cannot read properties of null (reading 'useState')". The
runtime is now an entrypoint of the same build so React stays in one
shared chunk. The existing single-React test only compared bundles
within one build and could not see across build boundaries.
3. Island props arrived as attribute strings, so start={3} was "3" and
incrementing produced "31" then "311". Props now follow JSX semantics:
{…} parses as JSON, quoted values stay strings, and a runtime
expression is a WRN-ISLAND-PROPS build error rather than a silent
wrong value.
island-codegen.ts no longer imports @wrnexus/core. Compiler modules are
bundled into the Node-only VS Code extension, which contains no other
packages, so a runtime import of core broke the editor compiler; the two
helpers are implemented locally and the core dependency is dropped again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds /__wrnexus/islands.js (the bootstrap) and the /__wrnexus/island/
prefix (mount runtime, island bundles, shared chunks) to all three
serving paths.
Dev reuses the browserArtifactPaths registry pattern from pipeline.ts.
Prod mirrors the clientModulesDir handler, including its filename
allowlist, so island names cannot escape the output directory.
The bootstrap is inert without a data-wrn-island marker, so island-free
pages still download nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>