Skip to content

Commit 2ea73be

Browse files
antfubotopencode
andcommitted
refactor(assets): defer raw byte serving to the host, don't self-serve
The managed public/ directory is already served by whatever host the plugin is mounted into (Vite, Nuxt, any framework serves public/ at /), so the plugin no longer stands up its own /__…-raw/ route. Each asset's publicPath is now baseURL + its path (baseURL default '/'), pointing at the host's own URLs. - Add baseURL option (where the host serves the dir) and serveStatic (self-serve, off by default). setupAssets only calls ctx.views. hostStatic() when serveStatic is set. - The standalone CLI is its own host, so createAssetsCli() enables serveStatic (under a dedicated base so it doesn't collide with the SPA). - Plugin dev server: point Vite's own publicDir at the managed dir so Vite serves the fixtures at /, and drop the ad-hoc raw-serving middleware. Gated to 'serve' so fixtures never bake into dist. - Docs + README + API snapshots updated. Co-authored-by: opencode <noreply@opencode.ai>
1 parent bbc7a60 commit 2ea73be

13 files changed

Lines changed: 100 additions & 63 deletions

File tree

docs/plugins/assets.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { createAssetsDevframe } from '@devframes/plugin-assets'
4444

4545
export default createAssetsDevframe({
4646
dir: 'static', // defaults to `<cwd>/public`
47+
baseURL: '/', // the URL the host serves `dir` at
4748
write: true,
4849
uploadExtensions: ['png', 'jpg', 'svg', 'webp'], // defaults to Nuxt DevTools' own allow-list, or '*' for any
4950
})
@@ -52,10 +53,16 @@ export default createAssetsDevframe({
5253
| Option | Default | Description |
5354
|--------|---------|-------------|
5455
| `dir` | `<cwd>/public` | Directory this devframe manages. |
56+
| `baseURL` | `/` | URL base the host serves `dir` at — each asset's `publicPath` is `baseURL` + its path. Match a non-root deployment base (e.g. Nuxt's `app.baseURL`). |
5557
| `write` | `true` | Enable upload, rename, delete, and folder creation from the UI. |
5658
| `uploadExtensions` | Nuxt DevTools' allow-list | Extensions `upload` accepts, or `'*'` for any. |
59+
| `serveStatic` | `false` | Serve the directory's bytes from this devframe itself. Left off when mounted into a host that already serves `public/`; the standalone CLI turns it on. |
5760
| `build` | `false` | Register the `build` CLI subcommand. See [why it's off by default](#static-export) below. |
5861

62+
## How previews are served
63+
64+
Asset previews (`<img>`, `<video>`, download links) load the files by their **public URL**, and the host the plugin is mounted into serves those files — Vite, Nuxt, and most frameworks already serve their `public/` folder at `/`. The plugin never stands up its own byte-serving route; it just resolves each asset's `publicPath` as `baseURL` + the file's path. Point `baseURL` at wherever the host serves `dir` (the default `/` matches the usual `public/` convention). The standalone CLI (`pnpx @devframes/plugin-assets`) is its own host, so it flips `serveStatic` on and serves the directory under a dedicated base.
65+
5966
## RPC surface
6067

6168
All functions are namespaced `devframes:plugin:assets:*`:
@@ -77,7 +84,7 @@ All functions are namespaced `devframes:plugin:assets:*`:
7784

7885
## Static export
7986

80-
Every devframe's `build` CLI subcommand is disabled here by default (`capabilities: { build: false }`). Real byte serving for previews goes through `ctx.views.hostStatic()`, which only mounts real files under a live adapter (`cli` / `vite` / `embedded`) — a static export can never copy those bytes, and every write action is inherently excluded from a static dump. Rather than ship a broken, preview-less, write-less shell of the tool, the `build` command is simply not registered. Pass `{ build: true }` to `createAssetsDevframe()` (and `{ force: true }` if calling `createBuild()` directly) if that degraded export is still useful to you — the file listing itself still bakes into the static RPC dump.
87+
Every devframe's `build` CLI subcommand is disabled here by default (`capabilities: { build: false }`). A static export has no live host serving the files, and every write action is inherently excluded from a static dump. Rather than ship a broken, preview-less, write-less shell of the tool, the `build` command is simply not registered. Pass `{ build: true }` to `createAssetsDevframe()` (and `{ force: true }` if calling `createBuild()` directly) if that degraded export is still useful to you — the file listing itself still bakes into the static RPC dump.
8188

8289
## Source
8390

plugins/assets/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Browse, preview, upload, rename, and delete the files in a directory — a frame
66
pnpx @devframes/plugin-assets
77
```
88

9-
Manages `<cwd>/public` by default. The package exports `createAssetsDevframe()` for custom definitions and `assetsVitePlugin()` from `@devframes/plugin-assets/vite` for Vite hosts. Pass `{ dir }` to manage a different directory, or `{ write: false }` (`--read-only` on the standalone CLI) for a browse-only deployment.
9+
Manages `<cwd>/public` by default. The package exports `createAssetsDevframe()` for custom definitions and `assetsVitePlugin()` from `@devframes/plugin-assets/vite` for Vite hosts. Pass `{ dir }` to manage a different directory, `{ baseURL }` to match where the host serves it, or `{ write: false }` (`--read-only` on the standalone CLI) for a browse-only deployment.
10+
11+
Asset previews load files by their public URL, served by the host the plugin is mounted into (Vite/Nuxt/etc. already serve `public/` at `/`) — the plugin doesn't serve the bytes itself. The standalone CLI is its own host, so it serves the directory for you.
1012

1113
The standalone server requires devframe's trust handshake by default because it can read, write, and delete real files. The `build` CLI subcommand is disabled by default — see the docs page for why.

plugins/assets/src/cli.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import type { CacHandle } from 'devframe/adapters/cac'
22
import { createCac } from 'devframe/adapters/cac'
3-
import assetsDevframe from './index'
3+
import { createAssetsDevframe } from './index'
44

55
export function createAssetsCli(): CacHandle {
6-
return createCac(assetsDevframe)
6+
// The standalone CLI is its own host, so it serves the managed directory
7+
// itself (under a dedicated base) — unlike the mounted plugin, which
8+
// defers to the Vite/framework dev server it's attached to.
9+
return createCac(createAssetsDevframe({ serveStatic: true }))
710
}

plugins/assets/src/index.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,22 @@ export interface AssetsDevframeOptions {
2828
* own `cwd`.
2929
*/
3030
dir?: string
31+
/**
32+
* URL base the **host** serves the managed directory at, used to build
33+
* each asset's `publicPath`. Defaults to `/` — Vite, Nuxt, and most
34+
* frameworks serve their `public/` folder at the site root. Set this to
35+
* match a non-root deployment base (e.g. Nuxt's `app.baseURL`).
36+
*/
37+
baseURL?: string
38+
/**
39+
* Serve the managed directory's bytes from this devframe itself.
40+
* Defaults to `false`: when mounted into a host (Vite/Nuxt/…) that host
41+
* already serves `public/` at {@link baseURL}, so the plugin references
42+
* those URLs instead of standing up its own route. The standalone CLI
43+
* sets this to `true` (it is its own host), serving the directory under
44+
* a dedicated base so it doesn't collide with the SPA at `/`.
45+
*/
46+
serveStatic?: boolean
3147
basePath?: string
3248
distDir?: string
3349
/** Preferred standalone CLI port (default 9015). */
@@ -50,11 +66,11 @@ export interface AssetsDevframeOptions {
5066
auth?: boolean
5167
/**
5268
* Register the `build` CLI subcommand. Disabled by default: a static
53-
* export can only ever list file metadata from a baked snapshot — real
54-
* previews need `ctx.views.hostStatic()`'s live byte serving, and every
55-
* write action is inherently excluded from a static dump — so the
56-
* command would produce a broken, write-less shell of the tool. Opt
57-
* back in if that degraded export is still useful to you.
69+
* export can only ever list file metadata from a baked snapshot — there
70+
* is no live host serving the files, and every write action is inherently
71+
* excluded from a static dump — so the command would produce a broken,
72+
* write-less shell of the tool. Opt back in if that degraded export is
73+
* still useful to you.
5874
*/
5975
build?: boolean
6076
}
@@ -71,7 +87,11 @@ export function createAssetsDevframe(options: AssetsDevframeOptions = {}): Devfr
7187
const id = options.id ?? DEFAULT_ID
7288
const distDir = options.distDir ?? resolve(PKG_ROOT, 'dist/spa')
7389
const write = options.write ?? true
74-
const rawBase = `/__${id}-raw/`
90+
const serveStatic = options.serveStatic ?? false
91+
// When self-serving (standalone CLI), mount under a dedicated base so the
92+
// asset bytes don't collide with the SPA served at `/`. Otherwise trust the
93+
// host to serve `public/` at `baseURL` (default `/`).
94+
const baseURL = options.baseURL ?? (serveStatic ? `/__${id}-raw/` : '/')
7595

7696
return defineDevframe({
7797
id,
@@ -101,7 +121,8 @@ export function createAssetsDevframe(options: AssetsDevframeOptions = {}): Devfr
101121
dir,
102122
write: readOnlyFlag ? false : write,
103123
uploadExtensions: options.uploadExtensions ?? DEFAULT_ALLOWED_UPLOAD_EXTENSIONS,
104-
rawBase,
124+
baseURL,
125+
serveStatic,
105126
})
106127
},
107128
})

plugins/assets/src/node/context.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ export interface AssetsConfig {
88
write: boolean
99
/** Extensions `upload` accepts, or `'*'` to accept any. */
1010
uploadExtensions: readonly string[] | '*'
11-
/** URL prefix the raw bytes are mounted at (live adapters only). */
12-
rawBase: string
11+
/**
12+
* URL base the managed directory is served at. Normally the host
13+
* (Vite / Nuxt / …) serves `public/` at `/`, so asset `publicPath`s are
14+
* `joinURL(baseURL, path)` and the plugin serves nothing itself.
15+
*/
16+
baseURL: string
1317
/** The upload streaming channel, created once in `setupAssets` when `write` is enabled. */
1418
uploadChannel?: RpcStreamingChannel<Uint8Array>
1519
}
@@ -47,7 +51,7 @@ export function getAssetsContext(ctx: DevframeNodeContext): AssetsContext {
4751
dir,
4852
write: config?.write ?? true,
4953
uploadExtensions: config?.uploadExtensions ?? '*',
50-
rawBase: config?.rawBase ?? '/__devframes_plugin_assets_raw/',
54+
baseURL: config?.baseURL ?? '/',
5155
uploadChannel: config?.uploadChannel,
5256
resolvePath: (relativePath: string) => resolveAssetPath(dir, relativePath),
5357
}

plugins/assets/src/node/index.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,32 @@ export interface SetupAssetsOptions {
1313
write: boolean
1414
/** Extensions `upload` accepts, or `'*'` to accept any. */
1515
uploadExtensions: readonly string[] | '*'
16-
/** URL prefix the raw bytes are mounted at (live adapters only). */
17-
rawBase: string
16+
/**
17+
* URL base the managed directory is served at — the base every asset's
18+
* `publicPath` is resolved against.
19+
*/
20+
baseURL: string
21+
/**
22+
* Whether this devframe should serve the managed directory's bytes
23+
* itself. Left `false` when mounted into a host (Vite / Nuxt / …) that
24+
* already serves `public/` at {@link baseURL}; set `true` only for the
25+
* standalone CLI, which is its own host.
26+
*/
27+
serveStatic: boolean
1828
}
1929

2030
const watchers = new WeakMap<DevframeNodeContext, () => Promise<void>>()
2131

2232
/**
2333
* Register the assets RPC surface on a devframe node context: ensures the
24-
* managed directory exists, hosts it for raw byte access, registers the
25-
* read RPCs (always) and write RPCs (when enabled), and starts a live
26-
* file watcher in dev mode.
34+
* managed directory exists, registers the read RPCs (always) and write
35+
* RPCs (when enabled), and starts a live file watcher in dev mode.
36+
*
37+
* Raw asset bytes (for `<img>`/`<video>`/download previews) are served by
38+
* the **host** the plugin is attached to — Vite, Nuxt, or any framework
39+
* already serves its `public/` dir at {@link SetupAssetsOptions.baseURL}.
40+
* The plugin only serves them itself when `serveStatic` is set (the
41+
* standalone CLI, which is its own host).
2742
*
2843
* Called from the definition's `setup(ctx)` and reusable by host adapters
2944
* that wire their own context.
@@ -38,11 +53,12 @@ export async function setupAssets(ctx: DevframeNodeContext, options: SetupAssets
3853

3954
configureAssets(ctx, { ...options, uploadChannel })
4055

41-
// Real byte serving for `<img>`/`<video>`/`<a download>` etc. Only
42-
// meaningful under a live adapter (cli / vite / embedded) — a no-op
43-
// under `mode: 'build'`, which this plugin opts out of by default via
44-
// `capabilities.build: false` anyway.
45-
ctx.views.hostStatic(options.rawBase, options.dir)
56+
// Only self-serve when this devframe is its own host (the standalone
57+
// CLI). When attached to a Vite/Nuxt/etc. dev server, that host already
58+
// serves the managed directory at `baseURL`, so we reference its URLs
59+
// rather than mounting our own route.
60+
if (options.serveStatic)
61+
ctx.views.hostStatic(options.baseURL, options.dir)
4662

4763
for (const fn of readFunctions)
4864
ctx.rpc.register(fn)

plugins/assets/src/node/scanner.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,31 +26,31 @@ export function guessAssetType(path: string): AssetType {
2626
return 'other'
2727
}
2828

29-
function toPublicPath(rawBase: string, posixPath: string): string {
29+
function toPublicPath(baseURL: string, posixPath: string): string {
3030
const encoded = posixPath.split('/').map(encodeURIComponent).join('/')
31-
return joinURL(rawBase, encoded)
31+
return joinURL(baseURL, encoded)
3232
}
3333

3434
/** Builds an {@link AssetInfo} from an already-resolved `fs.Stats`. */
35-
export function statToAssetInfo(dir: string, rawBase: string, relPath: string, stat: Stats): AssetInfo {
35+
export function statToAssetInfo(dir: string, baseURL: string, relPath: string, stat: Stats): AssetInfo {
3636
const posixPath = relPath.replace(/\\/g, '/')
3737
return {
3838
path: posixPath,
3939
type: guessAssetType(posixPath),
40-
publicPath: toPublicPath(rawBase, posixPath),
40+
publicPath: toPublicPath(baseURL, posixPath),
4141
size: stat.size,
4242
mtime: stat.mtimeMs,
4343
}
4444
}
4545

4646
/** Recursively lists every file under `dir`, sorted alphabetically by path. */
47-
export async function scanAssets(dir: string, rawBase: string): Promise<AssetInfo[]> {
47+
export async function scanAssets(dir: string, baseURL: string): Promise<AssetInfo[]> {
4848
const files = await glob(['**/*'], { cwd: dir, onlyFiles: true, dot: false })
4949

5050
const infos = await Promise.all(files.map(async (relPath): Promise<AssetInfo | undefined> => {
5151
try {
5252
const stat = await fsp.lstat(join(dir, relPath))
53-
return statToAssetInfo(dir, rawBase, relPath, stat)
53+
return statToAssetInfo(dir, baseURL, relPath, stat)
5454
}
5555
catch {
5656
// Removed between the glob scan and the stat call — drop it silently,

plugins/assets/src/rpc/functions/list.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const list = defineAssetsRpc({
3434
// The RPC runtime awaits handlers before validating `returns`; its
3535
// public setup type currently models schema-backed returns as
3636
// synchronous.
37-
handler: (async (): Promise<AssetInfo[]> => scanAssets(assets.dir, assets.rawBase)) as any,
37+
handler: (async (): Promise<AssetInfo[]> => scanAssets(assets.dir, assets.baseURL)) as any,
3838
}
3939
},
4040
})

plugins/assets/src/rpc/functions/rename.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const rename = defineAssetsRpc({
5252

5353
if (from === to) {
5454
const stat = await fsp.lstat(from)
55-
return statToAssetInfo(assets.dir, assets.rawBase, path, stat)
55+
return statToAssetInfo(assets.dir, assets.baseURL, path, stat)
5656
}
5757

5858
const targetExists = await fsp.access(to).then(() => true).catch(() => false)
@@ -70,7 +70,7 @@ export const rename = defineAssetsRpc({
7070
}
7171

7272
const stat = await fsp.lstat(to)
73-
return statToAssetInfo(assets.dir, assets.rawBase, nextRelPath, stat)
73+
return statToAssetInfo(assets.dir, assets.baseURL, nextRelPath, stat)
7474
}) as any,
7575
}
7676
},

plugins/assets/src/rpc/functions/write-text.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const writeText = defineAssetsRpc({
3434
handler: (async ({ path, content }: { path: string, content: string }): Promise<AssetInfo> => {
3535
const absolute = assets.resolvePath(path)
3636
await fsp.writeFile(absolute, content, 'utf-8')
37-
return statToAssetInfo(assets.dir, assets.rawBase, path, await fsp.lstat(absolute))
37+
return statToAssetInfo(assets.dir, assets.baseURL, path, await fsp.lstat(absolute))
3838
}) as any,
3939
}
4040
},

0 commit comments

Comments
 (0)