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
Draft
elliotBraem wants to merge 19 commits into
elliotBraem wants to merge 19 commits into
Conversation
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).
…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.
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.
Summary
Implements advisor plan 024 (
advisor-plans/024-auth-ui-carve-out.md): login and account settings move out of the ui monolith intoplugins/auth/ui— the first real consumer of the grafting machinery, proving SSR + client-hydration composition end-to-end and making auth surfaces tenant-overridable viaapp.auth.ui.Stacks on #132 (plan 023 — typed mount contract).
What's in here
git mvof login + settings routes/tests intoplugins/auth/ui, git history preserved; dual-target MF remote (remoteEntry.js/remoteEntry.server.js),createUiSharedDepsstrict singletons, utilities-onlystyles.css(zero token CSS), copied component/lib kit/_public/login,/_dashboard/settings/*— paths (/login,/settings/*) preserved via the graft_anonlayout deleted; the plugin's_publicmount carries the reject-authed gate itself (childless pathless layouts collide with_public/indexin the route generator)app.auth.ui(flat key) resolves onto the auth runtime entry, which is mirrored intoRuntimeConfig.plugins— compose/digest/hydrate/tenant-override and theplugins.auth.ui.*deploy write-back all work unchanged; dedup guards prevent double-spawning the auth backenduiSsrports;plugin-ui-ssr:<id>dev service (bun run dev:ssr)pluginPath/pluginHref/pluginSearchfor core references to plugin-owned pathsresolveCoreMountdisqualifies core layouts claimed by a strictly stronger ancestor mount — the admin area's inner_dashboardno longer competes for thedashboardmount; compose unit tests updated for the new semantics + a regression testhost-ssr-compose) loads the coreRouterexpose and every plugin./treeexpose, withinitializeSharing("default")resolved before each load — previously each expose ran through its own instance and self-initialized its own React, crashing composed SSR withInvalid hook call / reading 'useContext' of nullinitializeSharingreturns a promises array;initializeShareScopenormalizes throughPromise.allinside oneEffect.tryPromise, andloadRemoteExpose/loadLocalPluginUiTreeare singleEffect.genpipelines (share init → loadRemote → unwrap oryield*fail) with bounded retry + negative cachingdist/remoteEntry.server.jsfile path (fallback: native require, with a logged warning), and the auth-ui SSR bundle no longereager-shares react/react-dom/@TanStack (eager bundled a private second React)crossorigin="anonymous"on remoteEntry scripts and rpationcrossOriginLoading: "anonymous"on both client builds so async-chunk errors from cross-origin plugin remotes surface with detail instead ofScript error.registerRemoteson integrity bump), and the 008 migration-runner characterization test typed against the realeverything-dev/dbexportsDeviations from the plan (recorded)
tree.tsdefault-exports the raw routeTree withdefineUiPluginas an eager validation call — both consumers (host SSR, client hydrate) unwrap./tree's default as the tree itself["public", "dashboard"](not["anon", "dashboard"]) — see Option A aboveapp.auth.uikey (matches thepluginUiDeployFieldswrite-back), not the plan's literalplugins.auth.uiStatus
plugins/auth/uierrors unrelated to this branch, reproduced by stash)everything-dev/dbcomposed-ssr-share-scope.md)bun run test492 pass after gate-aware test repair (only-stale-compose-tests)bos mf check, built-CSS token inspection