Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/content/1.guide/18.hub-initiate.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ await buildHub({
})
```

A mount served outside the hub base writes to `outDir`'s parent (the deploy root) by its absolute path, so a host can keep the hub at `/__devtools/` while its devframe SPAs and assets stay top-level siblings (`/__inspect/`, `/__devtools-assets/`) rather than children of the hub base. `outDir` holds the hub subtree, its parent holds the whole deploy root, and either directory serves as-is.

Browser-side tools keep working in full: a page script still loads into the host page and talks to its panel over the [in-page channel](/guide/in-page-channel) (the a11y inspector scans a production app exactly as it does in dev). Reads resolve from the baked dump (`static`/`snapshot` RPCs, shared-state snapshots); live writes (messages, command execution) have no server, so the browser clients degrade to local no-ops, and a panel's dock-activation deep links ride a same-origin `BroadcastChannel` instead of the RPC relay.

A devframe whose value is inherently live declares `capabilities.build: false` and silently stays out of the build entirely - no dock, no SPA copy, no RPCs in the dump. The built-in terminals, code-server, and assets devframes declare it, so a hub mounting every built-in bakes only the tools that mean something statically. See the [buildHub options](/references/hub-api#buildhub-options) reference, and [`examples/a11y-messages-playground`](https://github.com/devframes/devframe/tree/main/examples/a11y-messages-playground) for a Vite host whose `vite build` output ships the hub.
Expand Down
23 changes: 14 additions & 9 deletions docs/content/6.errors/DF8006.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,38 @@
---
title: 'DF8006: Static Build Mount Escapes the Hub Base'
description: 'A static hub build can only write mounts under its own base: "{urlBase}" escapes "{base}".'
title: 'DF8006: Static Build Mount Base Is Not Absolute'
description: 'A static hub build writes each mount either under its base ("{base}") or as an absolute-path sibling of it, but "{urlBase}" is neither.'
---

## Message

> A static hub build can only write mounts under its own base: "`{urlBase}`" escapes "`{base}`"
> A static hub build writes each mount either under its base ("`{base}`") or as an absolute-path sibling of it, but "`{urlBase}`" is neither

## Cause

`buildHub` maps every mounted URL base to a directory under its `outDir` (which corresponds to the hub `base` at serve time), so a mount whose base lies outside the hub base has no on-disk location in the output. This happens when a devframe is installed with an explicit base outside the hub base, e.g. `ctx.install(devframe, { base: '/elsewhere/' })` from `configure`.
`buildHub` maps a mount under the hub `base` into its `outDir` (the hub subtree), and any other mount to the deploy root (`outDir`'s parent) by its absolute path, so a devframe SPA or asset dir served as a sibling of the hub base still lands beside it. A mount base that is neither under the hub base nor an absolute path has no on-disk location in the output.

## Example

```ts
await buildHub({
outDir: 'dist/__devframes',
async configure(ctx) {
// ✗ Bad: `/tools/x/` is not under the `/__devframes/` hub base
await ctx.install(myDevframe, { base: '/tools/x/' })
// ✓ Good: under the hub base, written into `outDir`
await ctx.install(a, { base: '/__devframes/a/' })
// ✓ Good: an absolute sibling, written to the deploy root beside the hub
await ctx.install(b, { base: '/tools/b/' })
// ✗ Bad: a relative base resolves against neither
await ctx.install(c, { base: 'tools/c/' })
},
})
```

## Fix

- Drop the `base` override so the devframe mounts at `<hub base><id>/`, or point it somewhere under the hub base.
- Or move the hub `base` up (e.g. `base: '/'`) so it contains every mount.
Give the mount an absolute base (starting with `/`): a base under the hub base writes into `outDir`, and any other absolute base writes to the deploy root by its path.

## Source

- [`packages/hub/src/node/build.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/build.ts): `buildHub()`'s mount-to-disk mapping throws this for any mount base outside the hub base.
- [`packages/hub/src/node/build.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/build.ts): `buildHub()`'s mount-to-disk mapping throws this for a mount base that is not absolute.
</content>
</invoke>
2 changes: 1 addition & 1 deletion docs/content/8.references/6.hub-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ The options of `buildHub()` from `@devframes/hub/build`: [Static builds](/guide/

| Option | Purpose |
|---|---|
| `outDir` | Output directory for the hub subtree; corresponds to `base` at serve time (build `base: '/__devframes/'` into `dist/__devframes`). |
| `outDir` | Output directory for the hub subtree; corresponds to `base` at serve time (build `base: '/__devframes/'` into `dist/__devframes`). A mount served outside the hub base (a devframe SPA or asset dir kept as a sibling of it) is written to this directory's parent (the deploy root) by its absolute path. |
| `base` | Mount base baked into every absolute URL the build emits. Default `/__devframes/`. |
| `context` | An already-mounted `DevframeHubContext` to bake instead of `devframes` (the build counterpart of `initHub({ context })`); reads `ctx.frames` and `ctx.views.buildStaticDirs`. Mutually exclusive with `devframes`. |
| `clean` | Remove `outDir` before writing. Default `true`; set `false` to bake beside an app's own build output. |
Expand Down
22 changes: 17 additions & 5 deletions packages/hub/src/node/__tests__/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,15 +163,27 @@ describe('buildHub', () => {
expect(manifest['alpha:probe']).toMatchObject({ type: 'static' })
})

it('rejects a mount base outside the hub base', async () => {
const outDir = join(mkdtempSync(join(tmpdir(), 'hub-build-out-')), 'hub')
await expect(buildHub({
it('bakes a mount outside the hub base as a deploy-root sibling', async () => {
const deployRoot = mkdtempSync(join(tmpdir(), 'hub-build-out-'))
const outDir = join(deployRoot, '__hub')

await buildHub({
outDir,
base: '/__hub/',
cwd: mkdtempSync(join(tmpdir(), 'hub-build-cwd-')),
async configure(ctx) {
await ctx.install(makeFrame('gamma', { distDir: makeDist('<h1>gamma</h1>') }), { base: '/elsewhere/' })
await ctx.install(makeFrame('gamma', { distDir: makeDist('<h1>gamma</h1>') }), { base: '/gamma/' })
},
})).rejects.toThrow(/escapes "\/__hub\/"/)
})

// The hub subtree lands at `outDir`, while the sibling frame resolves to
// the deploy root (outDir's parent) by its absolute path, beside the hub.
expect(existsSync(join(outDir, '__connection.json'))).toBe(true)
expect(readFileSync(join(deployRoot, 'gamma/index.html'), 'utf-8')).toContain('gamma')
// The sibling frame still gets its per-frame meta pointing back at the hub.
const frameMeta = JSON.parse(readFileSync(join(deployRoot, 'gamma/__connection.json'), 'utf-8'))
expect(frameMeta.baseUrl).toBe('/__hub/__connection.json')
const index = JSON.parse(readFileSync(join(outDir, '__index.json'), 'utf-8'))
expect(index.frames.map((frame: { id: string }) => frame.id)).toEqual(['gamma'])
})
})
21 changes: 17 additions & 4 deletions packages/hub/src/node/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ export interface BuildHubOptions {
* Output directory the hub subtree is written into. It corresponds to the
* hub {@link BuildHubOptions.base} at serve time: building with
* `base: '/__devframes/'` into `dist/__devframes` makes the deployed app's
* `dist/` servable as-is by any static file server.
* `dist/` servable as-is by any static file server. A mount served outside
* the hub base (a devframe SPA or asset dir kept as a sibling of it) is
* written to this directory's parent (the deploy root) by its absolute path,
* so `dist/` still serves the whole layout.
*/
outDir: string
/**
Expand Down Expand Up @@ -123,11 +126,21 @@ export async function buildHub(options: BuildHubOptions): Promise<void> {
await fs.rm(outDir, { recursive: true })
await fs.mkdir(outDir, { recursive: true })

/** Map a hub-base-relative URL base to its on-disk location under `outDir`. */
/**
* Map a served URL base to its on-disk location. A base under the hub
* {@link base} writes into the hub subtree at `outDir` (so `outDir`
* corresponds to the hub base). A base outside it is a deploy-root sibling:
* `outDir`'s parent is the deploy root, and the base resolves under it by its
* absolute path. This is the layout Vite DevTools serves, where devframe SPAs
* and assets sit beside `/__devtools/` rather than under it.
*/
const deployRoot = dirname(outDir)
const resolveOutPath = (urlBase: string): string => {
if (!urlBase.startsWith(base))
if (urlBase.startsWith(base))
return resolve(outDir, urlBase.slice(base.length))
if (!urlBase.startsWith('/'))
throw diagnostics.DF8006({ urlBase, base })
return resolve(outDir, urlBase.slice(base.length))
return resolve(deployRoot, urlBase.slice(1))
}

await copyBuildStatics(ctx, resolveOutPath)
Expand Down
4 changes: 2 additions & 2 deletions packages/hub/src/node/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ export const diagnostics = defineDiagnostics({
fix: 'A hub exposes one aggregate MCP endpoint over every mounted devframe, so per-devframe `mcp` settings are ignored. Drop `mcp: false` from `initHub` (the `\'auto\'` default mounts the aggregate route once agent tools exist) to surface this devframe\'s tools, or drop `mcp` from the devframe to silence this warning.',
},
DF8006: {
why: (p: { urlBase: string, base: string }) => `A static hub build can only write mounts under its own base: "${p.urlBase}" escapes "${p.base}".`,
fix: 'buildHub maps each mount base to a directory under its `outDir`, so every mount must live under the hub base. Drop the `basePath` override (or the `ctx.install` base) that points outside it, or move the hub `base` up so it contains the mount.',
why: (p: { urlBase: string, base: string }) => `A static hub build writes each mount either under its base ("${p.base}") or as an absolute-path sibling of it, but "${p.urlBase}" is neither.`,
fix: 'buildHub maps a mount under the hub base into its `outDir`, and any other mount to the deploy root (`outDir`\'s parent) by its absolute path. Give the mount an absolute base (starting with `/`) so it resolves to one of those.',
},
DF8100: {
why: (p: { id: string }) => `Dock with id "${p.id}" is already registered`,
Expand Down
Loading