Skip to content

Commit 62acb86

Browse files
authored
feat(plugin-inspect): serve the SPA from a lockstep assets package (#239)
* feat(plugin-inspect): serve the SPA from a lockstep client-assets package Pilot of the deferred-client-assets pattern: the inspector's Vue SPA now ships in a new @devframes/plugin-inspect-client package instead of inside the node tarball, which drops from ~409 KB (90% SPA) to ~18 KB. Its Vite build emits into plugins/inspect/client-pkg/dist, and cli.distDir points at a RemoteAssets declaration (package + version + resolveFrom). In this monorepo the client package is a dev-only workspace link, so resolveFrom short-circuits to its built dist (zero network); published consumers get the assets on demand through devframe's caching CDN back-proxy, or by installing the client package for an offline/air-gap UI. Wires the nested workspace glob (plugins/*/client-pkg), turbo outputs, a knip override for the runtime-string dependency, and updates the plugin's test harness to resolve the source. * refactor(plugin-inspect): name the assets package -assets, not -client 'client' is overloaded in devframe (devframe/client is the RPC client; each plugin's ./client export is its panel script), so a package holding prebuilt browser assets reads wrong as '…-client'. Rename @devframes/plugin-inspect-client -> @devframes/plugin-inspect-assets and the directory client-pkg -> assets-pkg, and adopt '${pkg.name}-assets' as the convention for deferred SPA-asset packages. * refactor(plugin-inspect): double-dash the assets package suffix Separate the '--assets' companion suffix from the plugin's own name segments: @devframes/plugin-inspect-assets -> @devframes/plugin-inspect--assets. The '--' makes the auto-derived assets package visually distinct from a plugin whose name legitimately ends in a single-dash segment. Convention is now '${pkg.name}--assets'; the directory stays plugins/*/assets-pkg.
1 parent acb344e commit 62acb86

9 files changed

Lines changed: 95 additions & 16 deletions

File tree

knip.jsonc

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,24 @@
156156
"src/{client,node,rpc,inject,engine,registry}/index.ts"
157157
]
158158
},
159+
"plugins/inspect": {
160+
// The lockstep assets package is referenced only as a runtime string
161+
// (`${pkg.name}--assets` in `cli.distDir`), never imported, so knip
162+
// can't see the dev-only workspace link that makes it resolvable in
163+
// the monorepo. Repeat the `plugins/*` entry glob (a workspace config
164+
// replaces, not merges, it).
165+
"entry": [
166+
"src/{index,cli,vite,constants,types}.ts",
167+
"src/{client,node,rpc,inject,engine,registry}/index.ts"
168+
],
169+
"ignoreDependencies": ["@devframes/plugin-inspect--assets"]
170+
},
171+
"plugins/inspect/assets-pkg": {
172+
// Assets-only package: no source, just a prebuilt `dist` produced by
173+
// the sibling plugin's Vite build.
174+
"entry": [],
175+
"project": []
176+
},
159177
"plugins/a11y": {
160178
// `storybook-solidjs-vite` (not an official `@storybook/*` framework
161179
// package) doesn't match knip's Storybook plugin trigger, so it never
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "@devframes/plugin-inspect--assets",
3+
"type": "module",
4+
"version": "0.9.0-beta.4",
5+
"description": "Prebuilt browser assets (SPA) for @devframes/plugin-inspect, served on demand through devframe's remote-assets back-proxy.",
6+
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
7+
"license": "MIT",
8+
"homepage": "https://github.com/devframes/devframe#readme",
9+
"repository": {
10+
"directory": "plugins/inspect/assets-pkg",
11+
"type": "git",
12+
"url": "git+https://github.com/devframes/devframe.git"
13+
},
14+
"bugs": "https://github.com/devframes/devframe/issues",
15+
"keywords": [
16+
"devframe",
17+
"devframe-plugin",
18+
"devtools",
19+
"client-assets"
20+
],
21+
"exports": {
22+
"./package.json": "./package.json"
23+
},
24+
"files": [
25+
"dist"
26+
],
27+
"scripts": {
28+
"prepack": "pnpm --filter @devframes/plugin-inspect run build"
29+
}
30+
}

plugins/inspect/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"devDependencies": {
6464
"@antfu/design": "catalog:frontend",
6565
"@devframes/hub": "workspace:*",
66+
"@devframes/plugin-inspect--assets": "workspace:*",
6667
"@iconify-json/ph": "catalog:frontend",
6768
"@standard-schema/spec": "catalog:deps",
6869
"@storybook/addon-docs": "catalog:storybook",

plugins/inspect/src/index.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
1-
import type { DevframeDefinition } from 'devframe'
2-
import { existsSync } from 'node:fs'
3-
import { fileURLToPath } from 'node:url'
1+
import type { DevframeDefinition, RemoteAssets } from 'devframe'
42
import { defineDevframe } from 'devframe'
53
import pkg from '../package.json' with { type: 'json' }
64
import { setupInspect } from './node/index'
75

86
/** Default devframe id — drives the hosted mount path `/__<id>/`. */
97
const DEFAULT_ID = 'devframes_plugin_inspect'
108

11-
// The Vue SPA is built (by Vite) into `dist/spa`. From both the source
12-
// entry (`src/index.ts`, via the workspace alias) and the published
13-
// entry (`dist/index.mjs`), `../dist/spa` resolves to `<pkg>/dist/spa`.
14-
const distDir = fileURLToPath(new URL('../dist/spa', import.meta.url))
9+
// The Vue SPA ships in the lockstep-versioned `@devframes/plugin-inspect--assets`
10+
// package rather than inside this (slim) node package. `resolveFrom` lets a
11+
// locally installed copy (a workspace link in this monorepo, or an explicit
12+
// `npm install` for air-gapped setups) be served with zero network; otherwise
13+
// the assets stream on demand through devframe's caching CDN back-proxy.
14+
const distDir: RemoteAssets = {
15+
package: `${pkg.name}--assets`,
16+
version: pkg.version,
17+
resolveFrom: import.meta.url,
18+
}
1519

1620
export interface InspectDevframeOptions {
1721
/** Override the devframe id (and default CLI command / mount path). */
@@ -58,7 +62,7 @@ export function createInspectDevframe(options: InspectDevframeOptions = {}): Dev
5862
cli: {
5963
command: id,
6064
port: options.port ?? 9012,
61-
distDir: existsSync(distDir) ? distDir : undefined,
65+
distDir,
6266
// Gate the standalone server by default; `maybeOpenBrowser` folds the
6367
// current OTP into the `--open` URL so the tab lands already trusted.
6468
// Hosted adapters (Vite/hub) supply their own auth layer and ignore this.

plugins/inspect/src/spa/vite.config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ export default defineConfig({
2222
// SFCs instead of esbuild pre-bundling them.
2323
optimizeDeps: { exclude: ['@antfu/design'] },
2424
build: {
25-
outDir: fileURLToPath(new URL('../../dist/spa', import.meta.url)),
25+
// Emit into the sibling `@devframes/plugin-inspect--assets` package, which
26+
// ships these assets to npm; the node package stays slim and serves them
27+
// on demand through devframe's remote-assets back-proxy.
28+
outDir: fileURLToPath(new URL('../../assets-pkg/dist', import.meta.url)),
2629
emptyOutDir: true,
2730
},
2831
})

plugins/inspect/test/_utils.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { DevframeHubContext } from '@devframes/hub/node'
22
import type { DevframeNodeContext } from 'devframe'
33
import type { StartedServer } from 'devframe/internal'
44
import { existsSync } from 'node:fs'
5+
import os from 'node:os'
56
import path from 'node:path'
67
import process from 'node:process'
78
import { createHubContext } from '@devframes/hub/node'
@@ -10,22 +11,38 @@ import { DEVFRAME_CONNECTION_META_FILENAME } from 'devframe/constants'
1011
import { createH3DevframeHost } from 'devframe/internal'
1112
import { createHostContext } from 'devframe/node'
1213
import { resolveBasePath } from 'devframe/node/hub-internals'
14+
import { resolveStaticAssetsSource } from 'devframe/utils/remote-assets'
1315
import { mountStaticHandler } from 'devframe/utils/serve-static'
1416
import { getPort } from 'get-port-please'
1517
import { H3 } from 'h3'
1618
import { serveTestContext } from '../../../tests/helpers/serve-test-context'
1719

18-
const SPA_DIST = inspectDevframe.cli!.distDir!
20+
/**
21+
* Resolve the inspector's SPA to a local directory. Its `distDir` is a
22+
* remote-assets declaration; in this monorepo the lockstep
23+
* `@devframes/plugin-inspect--assets` package is workspace-linked, so
24+
* resolution short-circuits to its built `dist`. A store (rather than a
25+
* string) means that build hasn't run.
26+
*/
27+
function localSpaDir(): string {
28+
const resolved = resolveStaticAssetsSource(inspectDevframe.cli!.distDir!, path.join(os.tmpdir(), 'devframes_plugin_inspect-test'))
29+
if (typeof resolved !== 'string') {
30+
throw new TypeError(
31+
'[devframes_plugin_inspect] client SPA missing — run `pnpm -C plugins/inspect run build` first.',
32+
)
33+
}
34+
return resolved
35+
}
1936

2037
/**
2138
* Assert the Vue SPA has been built. The dev-server and static-build
22-
* tests mount / copy `dist/spa`; a missing build produces a loud, fixable
23-
* failure rather than an opaque 404.
39+
* tests mount / copy the client SPA; a missing build produces a loud,
40+
* fixable failure rather than an opaque 404.
2441
*/
2542
export function assertSpaBuilt(): void {
26-
if (!existsSync(path.join(SPA_DIST, 'index.html'))) {
43+
if (!existsSync(path.join(localSpaDir(), 'index.html'))) {
2744
throw new Error(
28-
'[devframes_plugin_inspect] dist/spa missing — run `pnpm -C plugins/inspect run build` first.',
45+
'[devframes_plugin_inspect] client SPA missing — run `pnpm -C plugins/inspect run build` first.',
2946
)
3047
}
3148
}
@@ -51,7 +68,7 @@ interface BootOptions {
5168
* context exercises the no-hub path (empty list, thrown diagnostic).
5269
*/
5370
async function boot(options: BootOptions): Promise<InspectorServer> {
54-
const distDir = inspectDevframe.cli!.distDir!
71+
const distDir = localSpaDir()
5572
const basePath = resolveBasePath(inspectDevframe, 'standalone')
5673
const host = '127.0.0.1'
5774
const port = await getPort({ host, random: true })

pnpm-lock.yaml

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ trustPolicyExclude:
2626
packages:
2727
- packages/*
2828
- plugins/*
29+
- plugins/*/assets-pkg
2930
- examples/*
3031
- storybook
3132
- docs

turbo.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
"@devframes/plugin-inspect#build": {
6666
"outputLogs": "new-only",
6767
"dependsOn": ["devframe#build", "@devframes/vite#build"],
68-
"outputs": ["dist/**"]
68+
"outputs": ["dist/**", "assets-pkg/dist/**"]
6969
},
7070
"@devframes/plugin-og#build": {
7171
"outputLogs": "new-only",

0 commit comments

Comments
 (0)