-
-
Notifications
You must be signed in to change notification settings - Fork 11
feat(backroad): embed apps on a sub-route of Express or Hono (+ jscpd gate) #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sudo-vaibhav
wants to merge
26
commits into
main
Choose a base branch
from
feat/pluggable-mount
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
bd86346
feat(backroad): mount apps on a sub-route of Express or Hono
sudo-vaibhav eb35b4b
Merge remote-tracking branch 'origin' into feat/pluggable-mount
sudo-vaibhav b524590
fix(review): disable credential persistence in duplication checkout (…
sudo-vaibhav 60a23ea
fix(review): make hono base-path stripping boundary-safe (#44)
sudo-vaibhav 844e4e9
fix(review): use Map for socket registry to avoid prototype pollution…
sudo-vaibhav b81deb0
fix(review): normalize '/' basePath and harden /api/uploads handler (…
sudo-vaibhav 3a2accd
fix(review): use Map for session storage to avoid prototype pollution…
sudo-vaibhav b53ddae
fix(review): fail closed when jscpd report is unreadable (#44)
sudo-vaibhav 267707e
fix(ci): ignore orphaned @ai-hero/sandcastle devDependency in knip (#44)
sudo-vaibhav e22ea4b
fix(review): fail closed on invalid jscpd percentage (#44)
sudo-vaibhav d33cc67
fix(review): handle query/hash in withBasePath already-prefixed check…
sudo-vaibhav 1ba2e3e
fix(review): use nullish fallback for port to preserve explicit 0 (#44)
sudo-vaibhav 0ba4f79
fix(review): unregister socket on disconnect to prevent memory leak (…
sudo-vaibhav e02d48c
fix(review): restrict skipped allowance to duplication job only (#44)
sudo-vaibhav 41939a9
Merge remote-tracking branch 'origin/main' into feat/pluggable-mount
sudo-vaibhav 767927c
fix(review): only unregister socket when it is the mapped one (#44)
sudo-vaibhav 4d13124
fix(review): add #getSocket convenience method on RenderQueue (#44)
sudo-vaibhav 3b00c36
fix(review): extract shared lazyRequire helper for adapters (#44)
sudo-vaibhav c571498
fix(e2e): fire download confirmation as a persistent toast (#44)
sudo-vaibhav 8102a6a
fix(review): only swallow MODULE_NOT_FOUND in lazyRequire (#44)
sudo-vaibhav aa8e05c
fix(review): add shared base-path-aware backroadFetch wrapper (#44)
sudo-vaibhav af314f4
fix(review): split server/build.ts into focused modules (#44)
sudo-vaibhav ebc538f
fix(review): keep standalone runner agnostic of express (#44)
sudo-vaibhav 437981b
test(review): add integration tests for express and hono outlets (#44)
sudo-vaibhav f2f1d18
fix(review): guard missing socket during async render flush (#44)
sudo-vaibhav 741158e
fix(review): 404 unknown /api GET routes instead of SPA fallback (#44)
sudo-vaibhav File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| { | ||
| "mode": "strict", | ||
| "minTokens": 70, | ||
| "minLines": 8, | ||
| "format": ["typescript", "tsx", "javascript", "jsx"], | ||
| "absolute": false, | ||
| "gitignore": true, | ||
| "pattern": "{apps,libs,examples,tools}/**/*.{ts,tsx,js,jsx,mjs}", | ||
| "threshold": 0, | ||
| "ignore": [ | ||
| "**/node_modules/**", | ||
| "**/dist/**", | ||
| "**/out-tsc/**", | ||
| "**/coverage/**", | ||
| "**/storybook-static/**", | ||
| "**/.storybook/**", | ||
| "**/.docusaurus/**", | ||
| "**/build/**", | ||
| "**/.sandcastle/**", | ||
| "**/*.test.ts", | ||
| "**/*.test.tsx", | ||
| "**/*.spec.ts", | ||
| "**/*.spec.tsx", | ||
| "**/*.stories.tsx", | ||
| "**/*.gen.ts", | ||
| "**/*.d.ts" | ||
| ] | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| --- | ||
| title: Embedding in Express / Hono | ||
| sidebar_position: 7 | ||
| --- | ||
|
|
||
| # Embedding a Backroad app | ||
|
|
||
| `run()` starts a standalone server that owns a whole port. Sometimes you | ||
| instead want a Backroad app to live on a **sub-route of an existing server** — | ||
| e.g. an internal dashboard at `/backroad` next to your own API. The adapters | ||
| let you mount a Backroad app as a sub-app of an Express or Hono server. | ||
|
|
||
| ```ts | ||
| // Instead of taking a whole port… | ||
| run(executor); | ||
|
|
||
| // …mount under a sub-route of an app you already have: | ||
| app.use('/backroad', backroadExpress(executor, { basePath: '/backroad' })); | ||
| ``` | ||
|
|
||
| The `basePath` option must match the path you mount at. It's how the prebuilt | ||
| frontend learns where it lives: the server injects it into the served HTML at | ||
| runtime, and every URL the client builds (asset paths, the Socket.IO handshake, | ||
| `/api/*` calls, the router) is prefixed with it. One published bundle therefore | ||
| works from the domain root **or** any sub-path. | ||
|
|
||
| ## Express | ||
|
|
||
| ```ts | ||
| import express from 'express'; | ||
| import { backroadExpress } from '@backroad/backroad'; | ||
|
|
||
| const app = express(); | ||
|
|
||
| app.get('/', (_req, res) => res.send('my own home page')); | ||
|
|
||
| app.use( | ||
| '/backroad', | ||
| backroadExpress( | ||
| async (br) => { | ||
| const n = br.button({ label: 'Click me' }); | ||
| br.write({ body: n ? 'Clicked!' : 'Not yet.' }); | ||
| }, | ||
| { basePath: '/backroad' } | ||
| ) | ||
| ); | ||
|
|
||
| app.listen(3000); | ||
| // Backroad UI: http://localhost:3000/backroad | ||
| // Your route: http://localhost:3000/ | ||
| ``` | ||
|
|
||
| That's the whole integration. Socket.IO **auto-attaches** to the underlying | ||
| server on the first request, so there's no extra wiring step. | ||
|
|
||
| ## Hono | ||
|
|
||
| Hono speaks the Web Fetch API, so run it on `@hono/node-server` and pass the | ||
| returned server to `attach()` (see [WebSockets](#websockets-and-attach) below): | ||
|
|
||
| ```ts | ||
| import { serve } from '@hono/node-server'; | ||
| import { Hono } from 'hono'; | ||
| import { backroadHono } from '@backroad/backroad'; | ||
|
|
||
| const app = new Hono(); | ||
| app.get('/', (c) => c.text('my own home page')); | ||
|
|
||
| const br = backroadHono( | ||
| async (br) => { | ||
| const n = br.button({ label: 'Click me' }); | ||
| br.write({ body: n ? 'Clicked!' : 'Not yet.' }); | ||
| }, | ||
| { basePath: '/backroad' } | ||
| ); | ||
| app.route('/backroad', br); | ||
|
|
||
| const server = serve({ fetch: app.fetch, port: 3000 }); | ||
| br.attach(server); // bind Socket.IO to the Node server | ||
| ``` | ||
|
|
||
| Install the optional peer deps for the Hono adapter: | ||
|
|
||
| ```bash | ||
| pnpm add hono @hono/node-server | ||
| ``` | ||
|
|
||
| ## WebSockets and `attach` | ||
|
|
||
| Backroad uses Socket.IO, which binds at the **HTTP server** level — below the | ||
| request/response middleware that `app.use()` deals with. There are two ways to | ||
| give it the server: | ||
|
|
||
| - **Auto-attach (default).** The handler grabs the server off the first HTTP | ||
| request it sees and attaches Socket.IO then. The browser always loads the | ||
| page before it opens the websocket, so this is in place in time. This is the | ||
| one-liner shown in the Express example — nothing else to do. | ||
|
|
||
| - **Explicit `attach(server)`.** Both adapters also expose `.attach(server)` for | ||
| when the auto-attach can't reach the server — Hono on `@hono/node-server`, or | ||
| any setup behind a proxy that hides the underlying server: | ||
|
|
||
| ```ts | ||
| const br = backroadExpress(executor, { basePath: '/backroad' }); | ||
| app.use('/backroad', br); | ||
| const server = app.listen(3000); | ||
| br.attach(server); // explicit — safe to also call; the first wins | ||
| ``` | ||
|
|
||
| Calling `attach()` after auto-attach has already run is a harmless no-op. | ||
|
|
||
| ## Multiple apps in one process | ||
|
|
||
| Each mounted app gets its own sessions and sockets, so you can run several side | ||
| by side. Give each a distinct `basePath`: | ||
|
|
||
| ```ts | ||
| app.use('/admin', backroadExpress(adminApp, { basePath: '/admin' })); | ||
| app.use('/reports', backroadExpress(reportsApp, { basePath: '/reports' })); | ||
| ``` | ||
|
|
||
| State set in one never leaks into the other. | ||
|
|
||
| ## Auth under a sub-path | ||
|
|
||
| If you enable [auth](./auth.md) on an embedded app, point better-auth's | ||
| `baseURL` at the mounted path so its callback URLs match where the handler | ||
| actually lives, e.g. `https://yourhost/backroad`. The Backroad frontend already | ||
| prefixes the mount path onto its `/api/auth/*` calls. | ||
|
|
||
| ## Still want a standalone server? | ||
|
|
||
| `run()` is unchanged — it's exactly this machinery with `basePath: ''` on a | ||
| server it creates for you. Reach for the adapters only when you need to embed | ||
| into an existing app. |
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
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
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
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
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
11 changes: 8 additions & 3 deletions
11
libs/backroad-components/src/lib/socket/auth-synchronizers.ts
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,24 +1,29 @@ | ||
| /** Wires server-driven auth events (redirect, sign-out) to browser navigation. */ | ||
| import { withBasePath } from './base-path'; | ||
| import { backroadFetch } from './fetch'; | ||
| import { socket } from './client'; | ||
|
|
||
| export const registerAuthSynchronizers = (): void => { | ||
| // Server-built auth URLs (from br.login/logout) are root-relative and don't | ||
| // know the mount sub-path, so withBasePath prefixes it (and leaves absolute | ||
| // or already-prefixed URLs alone). | ||
| socket.on('auth_redirect', ({ url }) => { | ||
| window.location.assign(url); | ||
| window.location.assign(withBasePath(url)); | ||
| }); | ||
|
|
||
| // br.logout() flows through here. Hit better-auth's sign-out endpoint | ||
| // directly (POST + cookies) so backroad-components stays free of the | ||
| // better-auth client SDK, then navigate to the React /auth/signin route. | ||
| socket.on('auth_signout', async () => { | ||
| try { | ||
| await fetch('/api/auth/sign-out', { | ||
| await backroadFetch('/api/auth/sign-out', { | ||
| method: 'POST', | ||
| credentials: 'include', | ||
| }); | ||
| } catch (err) { | ||
| console.error('Sign-out request failed', err); | ||
| } finally { | ||
| window.location.assign('/auth/signin'); | ||
| window.location.assign(withBasePath('/auth/signin')); | ||
| } | ||
| }); | ||
| }; |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.