Skip to content

Commit 79b5e48

Browse files
antfubotopencodewebfansplz
authored
feat(assets): port Nuxt DevTools' Assets tab as a framework-neutral plugin (#138)
Co-authored-by: opencode <noreply@opencode.ai> Co-authored-by: arlo <webfansplz@gmail.com>
1 parent bb8e962 commit 79b5e48

126 files changed

Lines changed: 5029 additions & 11 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎alias.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ export const alias = {
108108
'@devframes/plugin-messages/cli': p('messages/src/cli.ts'),
109109
'@devframes/plugin-messages/vite': p('messages/src/vite.ts'),
110110
'@devframes/plugin-messages': p('messages/src/index.ts'),
111+
'@devframes/plugin-assets/client': p('assets/src/client/index.ts'),
112+
'@devframes/plugin-assets/node': p('assets/src/node/index.ts'),
113+
'@devframes/plugin-assets/rpc': p('assets/src/rpc/index.ts'),
114+
'@devframes/plugin-assets/cli': p('assets/src/cli.ts'),
115+
'@devframes/plugin-assets/vite': p('assets/src/vite.ts'),
116+
'@devframes/plugin-assets': p('assets/src/index.ts'),
111117
}
112118

113119
// update tsconfig.base.json — CSS aliases exist for Vite resolution only;

‎docs/.vitepress/config.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ function pluginsItems(prefix: string) {
7171
{ text: 'Git', link: `${prefix}/plugins/git` },
7272
{ text: 'Terminals', link: `${prefix}/plugins/terminals` },
7373
{ text: 'Code Server', link: `${prefix}/plugins/code-server` },
74+
{ text: 'Assets', link: `${prefix}/plugins/assets` },
7475
] satisfies DefaultTheme.NavItemWithLink[]
7576
}
7677

‎docs/errors/DF0042.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# DF0042: Static Build Disabled By The Definition
6+
7+
## Message
8+
9+
> "`{id}`" declares `capabilities.build: false` — its static export is not meaningful (writes are excluded and any live-served data won't be there).
10+
11+
## Cause
12+
13+
`createBuild` runs unconditionally when called directly, but a definition can opt out of static export via `capabilities.build: false` — typically because the devframe is inherently live (it manages real files on disk, spawns a process, etc.) and a `mode: 'build'` export would only ever produce a broken, write-less shell of the tool. `createCac` already skips registering the `build` subcommand for such a definition; this diagnostic covers the remaining path — a caller invoking `createBuild()` directly, bypassing the CLI.
14+
15+
## Example
16+
17+
```ts
18+
// ✗ Bad — builds a devframe that opted out of static export
19+
await createBuild(assetsDevframe) // throws DF0042
20+
21+
// ✓ Good — the degraded export is still useful to you
22+
await createBuild(assetsDevframe, { force: true })
23+
```
24+
25+
## Fix
26+
27+
- Pass `{ force: true }` to `createBuild()` if the degraded export is still useful to you.
28+
- Otherwise, drop `capabilities.build: false` on the definition if a static export should be supported after all.
29+
30+
## Source
31+
32+
- [`packages/devframe/src/adapters/build.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/adapters/build.ts)`createBuild()` throws this when `capabilities.build` is `false` and `force` isn't set.

‎docs/plugins/assets.md‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# Assets
6+
7+
Browse, preview, upload, rename, and delete the files in a directory, built as a **Vue** SPA on `@antfu/design` — a framework-neutral port of Nuxt DevTools' Assets tab.
8+
9+
Package: `@devframes/plugin-assets` · framework: **Vue + @antfu/design**
10+
11+
## What it does
12+
13+
Search by name and filter by type from an inline chip row, switch between a thumbnail grid (grouped by folder) and a file tree, and open a resizable right-hand details panel with a live preview (image, video, audio, font, or text), file metadata, and ready-to-copy usage snippets (`<img>`, CSS `background-image`, `@font-face`, a download link). Upload files with the toolbar button (native file picker) or by dropping them anywhere on the frame, and select multiple assets to delete them together. A live file watcher keeps every connected client's listing in sync with changes made outside the UI.
14+
15+
The standalone server requires devframe's trust handshake by default because it can read, write, and delete real files. Uploads, renames, deletes, and folder creation are enabled by default — pass `{ write: false }` (or `--read-only` on the standalone CLI) for a browse-only deployment.
16+
17+
## Standalone
18+
19+
```sh
20+
pnpx @devframes/plugin-assets # manages <cwd>/public
21+
pnpx @devframes/plugin-assets --read-only # disable upload / rename / delete / mkdir
22+
```
23+
24+
## Mount into a Vite host
25+
26+
```ts
27+
// vite.config.ts
28+
import { assetsVitePlugin } from '@devframes/plugin-assets/vite'
29+
import { defineConfig } from 'vite'
30+
31+
export default defineConfig({
32+
plugins: [
33+
assetsVitePlugin(),
34+
],
35+
})
36+
```
37+
38+
## Programmatic
39+
40+
`createAssetsDevframe(options)` returns a definition you can deploy through any adapter:
41+
42+
```ts
43+
import { createAssetsDevframe } from '@devframes/plugin-assets'
44+
45+
export default createAssetsDevframe({
46+
dir: 'static', // defaults to `<cwd>/public`
47+
baseURL: '/', // the URL the host serves `dir` at
48+
write: true,
49+
uploadExtensions: ['png', 'jpg', 'svg', 'webp'], // defaults to Nuxt DevTools' own allow-list, or '*' for any
50+
})
51+
```
52+
53+
| Option | Default | Description |
54+
|--------|---------|-------------|
55+
| `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`). |
57+
| `write` | `true` | Enable upload, rename, delete, and folder creation from the UI. |
58+
| `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. |
60+
| `build` | `false` | Register the `build` CLI subcommand. See [why it's off by default](#static-export) below. |
61+
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+
66+
## RPC surface
67+
68+
All functions are namespaced `devframes:plugin:assets:*`:
69+
70+
| Function | Type | Notes |
71+
|----------|------|-------|
72+
| `list` | `query`, `snapshot: true` | Every file under the managed directory, with type, size, and last-modified time. |
73+
| `capabilities` | `query`, `snapshot: true` | Whether write actions are enabled, and the upload allow-list — lets the UI gate itself proactively. |
74+
| `read-image-meta` | `query` | Width, height, and orientation for an image asset. |
75+
| `read-text` | `query` | Truncated text content, for preview. |
76+
| `upload` | `action` | Allocates a streaming upload slot; the client pipes the file's bytes over the paired channel. |
77+
| `rename` | `action` | Renames an asset within its folder, preserving its extension. |
78+
| `delete` | `action` | Deletes one or more assets in a single call. |
79+
| `mkdir` | `action` | Creates a folder, including missing parents. |
80+
| `open-in-editor` / `reveal-in-folder` | `action` | Launch the asset in your editor, or reveal its containing folder in the OS file manager. Always registered, regardless of `write`. |
81+
82+
`upload` / `rename` / `delete` / `mkdir` are registered only when `write` is enabled.
83+
84+
## Static export
85+
86+
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.
87+
88+
## Source
89+
90+
[`plugins/assets`](https://github.com/devframes/devframe/tree/main/plugins/assets)

‎docs/plugins/index.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Each plugin is built with a **different UI framework**. That is deliberate: devf
1717
| [Git](./git) | React (Next.js) | A repository dashboard — status, a commit graph, branches, and diffs, with optional staging and committing. |
1818
| [Terminals](./terminals) | Svelte | Stream read-only command output and run fully interactive PTY shells in the browser. |
1919
| [Code Server](./code-server) | Vue | Launch VS Code in the browser (code-server, `code serve-web`, or a tunnel) on demand and embed it in an auto-authenticated iframe. |
20+
| [Assets](./assets) | Vue | Browse, preview, upload, rename, and delete the files in a managed directory. |
2021

2122
## One client, any framework
2223

@@ -32,6 +33,7 @@ Most plugins publish a `bin`, so the quickest path is `pnpx`:
3233
pnpx @devframes/plugin-inspect # the Devframe Inspector, standalone
3334
pnpx @devframes/plugin-og # inspect Open Graph metadata and social cards
3435
pnpx @devframes/plugin-git # the Git dashboard against the current repo
36+
pnpx @devframes/plugin-assets # manage the files under <cwd>/public
3537
```
3638

3739
Each also exports a `create…Devframe` factory (or, for the Accessibility Inspector, a ready-made definition) you can drive through any adapter — see the individual pages for the factory name, options, and host-mount snippets.

‎examples/next-devframe-hub/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@devframes/json-render": "workspace:*",
1717
"@devframes/next": "workspace:*",
1818
"@devframes/plugin-a11y": "workspace:*",
19+
"@devframes/plugin-assets": "workspace:*",
1920
"@devframes/plugin-code-server": "workspace:*",
2021
"@devframes/plugin-git": "workspace:*",
2122
"@devframes/plugin-inspect": "workspace:*",

‎examples/next-devframe-hub/src/client/devframe/next-devframe-hub.ts‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { StartedServer } from 'devframe/node'
44
import type { ConnectionMeta, DevframeDefinition } from 'devframe/types'
55
import { homedir } from 'node:os'
66
import process from 'node:process'
7+
import { fileURLToPath } from 'node:url'
78
import { defineHubRpcFunction } from '@devframes/hub'
89
import { createHubContext, mountDevframe } from '@devframes/hub/node'
910
import { toJsonRenderDockEntry } from '@devframes/json-render/hub'
@@ -45,6 +46,24 @@ async function loadBuiltinPlugins(): Promise<DevframeDefinition[]> {
4546
return mods.map(mod => mod.default as DevframeDefinition)
4647
}
4748

49+
/**
50+
* Load the assets plugin and point its managed directory at this Next app's
51+
* `public/` (`src/client/public`) — the exact directory Next serves at `/`,
52+
* so the tab's previews resolve to real host URLs. Loaded through the same
53+
* bundler-ignored dynamic `import()` as the other plugins (its node code and
54+
* `import.meta.url`-based SPA-dist lookup don't survive static bundling), and
55+
* mounted with `watch: false` so no background file watcher lingers when the
56+
* hub is booted in a short-lived context (e.g. the example's own test).
57+
*/
58+
async function loadAssetsDevframe(): Promise<DevframeDefinition> {
59+
const mod = await import(/* webpackIgnore: true */ /* turbopackIgnore: true */ '@devframes/plugin-assets')
60+
// String path ops on the module path (not `new URL('../public', …)`, which
61+
// Turbopack eagerly resolves as an asset import and fails on a directory) —
62+
// `src/client/devframe/` → `src/client/public`, the dir Next serves at `/`.
63+
const dir = join(dirname(fileURLToPath(import.meta.url)), '../public')
64+
return (mod.createAssetsDevframe as (options: { dir: string, watch: boolean }) => DevframeDefinition)({ dir, watch: false })
65+
}
66+
4867
/** URL base the a11y agent module is served under (same-origin, catch-all route). */
4968
const A11Y_AGENT_MOUNT_BASE = '/__df-a11y-agent/'
5069

@@ -190,7 +209,7 @@ export async function nextDevframeHub(
190209

191210
// Demo devframes alongside the dogfooded built-in plugin packages.
192211
const devframes = options.devframes
193-
?? [demoDevframe, demoDevframeB, ...await loadBuiltinPlugins()]
212+
?? [demoDevframe, demoDevframeB, ...await loadBuiltinPlugins(), await loadAssetsDevframe()]
194213

195214
await context.messages.add({
196215
level: 'success',
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Next hub assets
2+
3+
These files live in the Next app's `public/` directory (`src/client/public`),
4+
served by Next at the site root (`/`). The **Assets** dock is mounted with its
5+
`dir` pointed here, so previews load from Next's real URLs (`/logo.svg`,
6+
`/images/…`) and write actions round-trip to disk.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "Devframe Assets",
3+
"description": "Sample fixtures for the assets plugin dev server.",
4+
"theme": {
5+
"primary": "#3a6a45",
6+
"surface": "#ffffff"
7+
},
8+
"nav": [
9+
{ "label": "Home", "href": "/" },
10+
{ "label": "Docs", "href": "/docs" }
11+
]
12+
}
Lines changed: 4 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)