Skip to content

feat(auth-ui): auth ui plugin grafted into _public/_dashboard mounts + composed-SSR share-scope hardening (plan 024) - #134

Draft
elliotBraem wants to merge 19 commits into
mainfrom
feat/auth-ui-plugin
Draft

elliotBraem wants to merge 19 commits into
mainfrom
feat/auth-ui-plugin

Conversation

@elliotBraem

@elliotBraem elliotBraem commented Sep 20, 2026

Copy link
Copy Markdown

Summary

Implements advisor plan 024 (advisor-plans/024-auth-ui-carve-out.md): login and account settings move out of the ui monolith into plugins/auth/ui — the first real consumer of the grafting machinery, proving SSR + client-hydration composition end-to-end and making auth surfaces tenant-overridable via app.auth.ui.

Stacks on #132 (plan 023 — typed mount contract).

What's in here

  • Plugin carve-out (mv-first recipe): git mv of login + settings routes/tests into plugins/auth/ui, git history preserved; dual-target MF remote (remoteEntry.js / remoteEntry.server.js), createUiSharedDeps strict singletons, utilities-only styles.css (zero token CSS), copied component/lib kit
  • Route ids re-based: /_public/login, /_dashboard/settings/* — paths (/login, /settings/*) preserved via the graft
  • Mount semantics change (Option A): core _anon layout deleted; the plugin's _public mount carries the reject-authed gate itself (childless pathless layouts collide with _public/index in the route generator)
  • Config seam: app.auth.ui (flat key) resolves onto the auth runtime entry, which is mirrored into RuntimeConfig.plugins — compose/digest/hydrate/tenant-override and the plugins.auth.ui.* deploy write-back all work unchanged; dedup guards prevent double-spawning the auth backend
  • Plugin ui SSR dev parity: planner allocates + patches plugin uiSsr ports; plugin-ui-ssr:<id> dev service (bun run dev:ssr)
  • Nav seam: pluginPath/pluginHref/pluginSearch for core references to plugin-owned paths
  • Gate-aware core mount resolution: resolveCoreMount disqualifies core layouts claimed by a strictly stronger ancestor mount — the admin area's inner _dashboard no longer competes for the dashboard mount; compose unit tests updated for the new semantics + a regression test
  • Composed-SSR share-scope hardening: ONE shared MF instance (host-ssr-compose) loads the core Router expose and every plugin ./tree expose, with initializeSharing("default") resolved before each load — previously each expose ran through its own instance and self-initialized its own React, crashing composed SSR with Invalid hook call / reading 'useContext' of null
  • Share-scope init is Effect-native: the runtime's initializeSharing returns a promises array; initializeShareScope normalizes through Promise.all inside one Effect.tryPromise, and loadRemoteExpose / loadLocalPluginUiTree are single Effect.gen pipelines (share init → loadRemote → unwrap or yield* fail) with bounded retry + negative caching
  • Dev localPath plugin trees load through the shared instance via the dist/remoteEntry.server.js file path (fallback: native require, with a logged warning), and the auth-ui SSR bundle no longer eager-shares react/react-dom/@TanStack (eager bundled a private second React)
  • Mask-free chunk errors: crossorigin="anonymous" on remoteEntry scripts and rpation crossOriginLoading: "anonymous" on both client builds so async-chunk errors from cross-origin plugin remotes surface with detail instead of Script error.
  • Typing cleanup: compose tests, federation-cache tests re-encoded for the shared-instance invariants (one composition instance per process, in-place registerRemotes on integrity bump), and the 008 migration-runner characterization test typed against the real everything-dev/db exports

Deviations from the plan (recorded)

  1. tree.ts default-exports the raw routeTree with defineUiPlugin as an eager validation call — both consumers (host SSR, client hydrate) unwrap ./tree's default as the tree itself
  2. Mounts are ["public", "dashboard"] (not ["anon", "dashboard"]) — see Option A above
  3. The config shape is the flat app.auth.ui key (matches the pluginUiDeployFields write-back), not the plan's literal plugins.auth.ui

Status

  • typecheck (host clean; two pre-existing plugins/auth/ui errors unrelated to this branch, reproduced by stash)
  • lint/format clean (biome + oxlint Effect rules)
  • compose fixture + fallback tests (host SSR + client) — updated for gate-aware mount resolution
  • host tests 178 green, incl. shared-instance federation-cache invariants; db-run-migrations characterization typed against everything-dev/db
  • changeset (composed-ssr-share-scope.md)
  • root bun run test 492 pass after gate-aware test repair (only-stale-compose-tests)
  • dev smoke: plugin-ui:auth spawn gap under investigation
  • browser regression, bos mf check, built-CSS token inspection

Plan 023. The graft protocol's mount vocabulary becomes a compile-time
contract instead of a stringly convention:

- mount-registry.ts: MOUNT_REGISTRY now satisfies Record<string, MountEntry>
  so MountId derives from its canonical keys; MOUNTS exports the union
  value; MOUNT_ALIASES is typed Record<string, MountId> (moved here from
  types.ts, which re-exports both for surface stability)
- define.ts: defineUiPlugin({ name, mounts, tree }) validates the tree at
  construction — every root child declaring a _mount must resolve to a
  declared canonical mount, else it throws with the child id and a
  closest-mount hint; associations ride a module-scoped WeakMap
- compose.ts: defined modules graft via declared mounts (no
  re-derivation); raw trees keep the deriveMountId fallback, now the
  declaredSegment derivation with a MountId return type
- digest-version.ts: MOUNT_REGISTRY_VERSION -> 2026-09-19.1 (graft input
  semantics changed; all compose digests invalidate)
- README row records the agreed deviation (validation covers _-roots
  only) and the plan-024 note (_template has no ui tree yet)

Verification: everything-dev 489 tests green (compose suite 20 incl. 9
new), host 175 green; no new typecheck or lint findings vs baseline.
…(plan 024)

First carve-out of the ui monolith: login and account settings move to
plugins/auth/ui, loaded via Module Federation and grafted into the core
tree by mount declaration.

Plugin carve-out (mv-first recipe, git history preserved):
- git mv login + settings routes/tests from ui/src/routes into
  plugins/auth/ui (route ids re-based: /_public/login, /_dashboard/settings)
- plugin workspace: dual-target rsbuild (remoteEntry.js + .server),
  createUiSharedDeps strict singletons, utilities-only styles.css (zero
  token CSS), copied components/lib kit, local barrel
- tree.ts validates via defineUiPlugin and default-exports the RAW
  routeTree (both loaders unwrap ./tree's default as the tree)
- core _anon layout deleted; the plugin's _public mount carries the
  reject-authed gate itself (childless pathless layouts collide with
  _public/index at '/' in the route generator)

Config seam: app.auth gains a flat ui key (PluginUiConfigSchema on
ComposableAppEntrySchema; getEntryAssociatedUi reads flat then nested
app.ui); the auth entry resolves its ui surface and is mirrored into
RuntimeConfig.plugins so compose/digest/hydrate/tenant-override and the
deploy write-back (plugins.auth.ui.*) work unchanged; dedup guards in
service-descriptor/dev-session/planner/host entryMap prevent double
spawning the auth backend.

Plugin ui SSR dev parity: planner allocates plugin uiSsr ports, patches
plugin ui url/ssrUrl, service map spawns plugin-ui-ssr:<id> (dev:ssr);
types gen filters the auth mirror.

Core nav to plugin-owned paths: pluginPath/pluginHref/pluginSearch seam
in ui/src/lib/plugin-path.ts (grafted paths are not in the core Register).

Tests: two-mount auth compose fixture + mount-contract rejection
(host), SSR compose-failure -> core-only fallback (ssr-render), client
compose + remote-down + digest-mismatch fallbacks (hydrate).

Known pre-existing tsc findings in packages/everything-dev
(publish.ts strategy, ui/api.ts Headers) are present at the base
commit; bos typecheck passes 9/9.

Verification: bun typecheck 9/9; bun lint clean; ui 322, host 178,
auth-ui plugin 6 tests green.
The dev spawn set derives from plan.serviceDescriptors (planner), while
the launch descriptors live in a separate map — a key must be present in
both to spawn. resolvedPorts.plugins was built only from pluginApiPorts,
so the auth mirror (no api port by design) dropped out of the record
entirely: its allocated ui/uiSsr ports were never patched into the
assigned runtime config, plugin-ui:auth never entered the spawn list,
and the smoke stack silently launched without the plugin ui dev server.

- allocateServices now records every plugin key in resolvedPorts.plugins
  ({ api, ui, uiSsr }) — api stays undefined for the auth mirror and the
  existing p?.api guards keep its backend descriptor suppressed
- buildServiceDescriptors also emits plugin-ui-ssr:<id> (local plugin ui
  + allocated uiSsr port) so the SSR dev server reaches the spawn list
  for any plugin, not just the launch map

Verified via the dev-config probe: resolvedPorts.plugins.auth =
{ ui: 3311, uiSsr: 3312 }, assigned config patches the mirror's
ui.url/ssrUrl, and both plugin-ui:auth and plugin-ui-ssr:auth appear in
plan.serviceDescriptors and the launch map.
# Conflicts:
#	plugins/auth/ui/src/routes/_dashboard/settings/profile.tsx
- graftCopy re-binds TanStack init to the copy (constructor arrow
  closures initialize the ORIGINAL route, leaving copies without
  _id/parentRoute and matches with undefined routeId)
- load local dev plugin SSR trees from disk (writeToDisk server build)
- eager-share react/router deps in plugin SSR bundle
- enable BOS_UI_COMPOSE for the dev regression stack
Core layouts resolve their canonical mount via gate strength, so the
admin area's inner _dashboard no longer claims the dashboard mount —
plugin auth__dashboard grafts under /_layout/_authenticated/_dashboard
(kills the multi-mount warning and the settings/admin gegraffitze).

remoteEntry scripts always carry crossorigin=anonymous in dev so
cross-origin client errors surface with detail instead of 'Script
error.'; BETTER_AUTH_URL set for the dev regression stack to stabilize
Better Auth callback origins.
…errors

Core Router and plugin ui ./tree exposes now load through one shared MF
instance with initializeSharing('default') before each load, so singleton
react/react-dom/@tanstack/* negotiate once — composed SSR no longer mixes
two React copies (fixes 'Invalid hook call / useContext of null').

Local dev plugin trees (plugins.<id>.ui:local) load through the same
instance via the dist/remoteEntry.server.js file path, falling back to
native require with a logged warning on failure. The auth ui SSR bundle
drops the eager react/react-dom/@TanStack shares that bundled a private
React. Core ui + plugin ui client builds emit crossOriginLoading
anonymous so async-chunk errors surface with detail instead of
'Script error.'.

Also reverts a stray duplicated defineUiPlugin registration in
plugins/auth/ui/src/tree.ts and updates the compose tests for
gate-aware core mount resolution (65ce45c).
…e loads

initializeSharing returns a promises array (runtime-core SharedHandler),
so chaining .then on it crashed composed SSR ('initializeShareScope(...).
then is not a function'). initializeShareScope is now an Effect that
normalizes via Promise.all, and loadRemoteExpose/loadLocalPluginUiTree
are single Effect.gen pipelines (share init -> loadRemote -> unwrap or
return yield* fail), retrying around the whole gen.

federation-cache tests updated for the shared-instance invariants:
one composition instance per process; integrity bumps re-register the
remote in place (registerRemotes) instead of minting a second instance.
…hared db types

The leftover Runner alias now uses the real Migration/MigrationDatabase/
MigrationStorage/DatabaseError exports from everything-dev/db, and the
25P2 overlap fixture iterates a typed runners tuple — dropping the
db-as-never / as-never runner-call casts. Silent unused-variable warning
resolved by actually wiring the type. Runners still use the shared
storage journal; behavior unchanged (4 tests green).
@elliotBraem elliotBraem changed the title feat(auth-ui): auth ui plugin grafted into _public/_dashboard mounts (plan 024) feat(auth-ui): auth ui plugin grafted into _public/_dashboard mounts + composed-SSR share-scope hardening (plan 024) Sep 21, 2026
…e; remove native-require fallback

The @module-federation/node runtime plugin requires URL-shaped remote
entries, so the dev dist path crashed the shared-instance load ('Invalid
URL') and silently fell back to a container-local native require — which
bundles its own React copy and breaks composed SSR with an invalid hook
call (observed in regression CI). The dev entry is now normalized with
pathToFileURL before registering on the shared composition instance, and
the native fallback is deleted: instance-share failures propagate as
FederationError so SSR fails loudly instead of paying a second React.
Drop empty spread fallbacks, unneeded array copies, regex caret checks,
and new Array(); add void to floating promises; type test error
channels with Data.TaggedError; yield tagged errors directly per
AGENTS.md conventions.
…ch hook

The node federation entry loader parses file:// entries but fetches them
with native fetch, which cannot read file URLs — dev localPath plugin
trees failed with 'fetch failed' and SSR compose failed hard after the
fallback removal. The composition instance now registers a small runtime
plugin whose fetch loaderHook returns a Response read from disk for
file: URLs (http yields undefined, falling through to native fetch), so
the runtime's vm-script path runs with createRequire anchored at the
entry and dev chunks resolve relative to the plugin dist.

Regression suite gains a bounded composed-SSR readiness probe in global
setup (REGRESSION_SSR_PROBE_TIMEOUT_MS, default 90s): the stack's health
endpoint answers while dev bundles still compile, so a stack that cannot
start now exits early with a pointer to .bos/logs instead of grinding
through 60s webServer test timeouts.
The node federation entry loader only forwards createScriptHook to the
sdk — plugin fetch hooks never fire — so file:// dev entries can never
load through the runtime, and the previous fetch-hook attempt was dead
code. Dev stacks already patch plugins.<id>.ui.ssrUrl to the plugin's
plugin-ui-ssr dev service and compose is gated on that field upstream,
so loadPluginUiTree now checks ssrUrl first and rides the identical
prod path (HTTP entry, digest cache, shared instance). localPath-only
plugins fail hard with an actionable error instead of promising an
EOF-unreachable load, and the file:// machinery (localPath loader,
pathToFileURL, runtime fetch plugin) is deleted — dev === production for
SSR composition, and no dev-without-ssr second-React path can exist.
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