@hoardodile/workbench 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +18 -0
- package/README.md +108 -0
- package/dist/assets/index-DuswH1zS.css +2 -0
- package/dist/assets/index-Rg27XFd4.js +77 -0
- package/dist/index.html +50 -0
- package/dist/mounts.mjs +730 -0
- package/dist/serve.d.mts +156 -0
- package/dist/serve.mjs +139 -0
- package/package.json +65 -0
package/dist/serve.d.mts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { Server } from "node:http"
|
|
2
|
+
import type { ImageVariantQuery } from "@hoardodile/sdk-types/image-variant"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Server-side hook results captured against the selected resource. The
|
|
6
|
+
* workbench pushes these into the plugin iframe context so it renders
|
|
7
|
+
* exactly like the app does. Produced by `hoardodile plugin dev`; the
|
|
8
|
+
* workbench itself never runs a sandbox.
|
|
9
|
+
*/
|
|
10
|
+
export type WorkbenchHookSnapshot = {
|
|
11
|
+
readonly pluginId: string
|
|
12
|
+
readonly detect: {
|
|
13
|
+
readonly ok: boolean
|
|
14
|
+
readonly reasons?: readonly string[]
|
|
15
|
+
}
|
|
16
|
+
readonly sourceMeta: unknown
|
|
17
|
+
readonly searchMeta: unknown
|
|
18
|
+
readonly coverLocal: string | undefined
|
|
19
|
+
readonly files: readonly unknown[] | undefined
|
|
20
|
+
readonly fileStats: {
|
|
21
|
+
readonly count?: number
|
|
22
|
+
readonly sizeBytes?: number
|
|
23
|
+
}
|
|
24
|
+
readonly imageHashes?: readonly unknown[]
|
|
25
|
+
readonly errors: Readonly<Record<string, string>>
|
|
26
|
+
readonly capturedAt: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A resource the workbench can open, as shown in the picker. */
|
|
30
|
+
export type WorkbenchResource = {
|
|
31
|
+
readonly id: string
|
|
32
|
+
readonly name: string
|
|
33
|
+
/** Plugin that owns the resource in the source library, when known. */
|
|
34
|
+
readonly contentPluginId?: string
|
|
35
|
+
readonly fileVersion?: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The plugin-visible slice of a resource's stored state. Used to seed
|
|
40
|
+
* the offline mock host so comments, danmaku, preferences and the
|
|
41
|
+
* per-resource cache are the ones the plugin would really see. Writes
|
|
42
|
+
* never leave the mock.
|
|
43
|
+
*/
|
|
44
|
+
export type WorkbenchResourceState = {
|
|
45
|
+
readonly name?: string
|
|
46
|
+
readonly messages?: readonly unknown[]
|
|
47
|
+
readonly danmaku?: readonly unknown[]
|
|
48
|
+
readonly prefs?: Readonly<Record<string, string>>
|
|
49
|
+
readonly cache?: Readonly<Record<string, string>>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Bytes (or a cached path) plus the content type to serve them as. */
|
|
53
|
+
export type WorkbenchRendered = {
|
|
54
|
+
readonly contentType: string
|
|
55
|
+
readonly bytes?: Uint8Array
|
|
56
|
+
readonly path?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Read-only file access for the selected resource. */
|
|
60
|
+
export type WorkbenchFileProvider = {
|
|
61
|
+
readonly list: (
|
|
62
|
+
resId: string,
|
|
63
|
+
) => Promise<readonly string[]> | readonly string[]
|
|
64
|
+
readonly stat: (
|
|
65
|
+
resId: string,
|
|
66
|
+
path: string,
|
|
67
|
+
) =>
|
|
68
|
+
| Promise<{ readonly sizeBytes: number } | undefined>
|
|
69
|
+
| { readonly sizeBytes: number }
|
|
70
|
+
| undefined
|
|
71
|
+
readonly read: (
|
|
72
|
+
resId: string,
|
|
73
|
+
path: string,
|
|
74
|
+
) => Promise<Uint8Array | undefined> | Uint8Array | undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Everything the workbench page can ask for. Each is optional: without
|
|
79
|
+
* a `preview` provider `?size=preview` (and the generic variant
|
|
80
|
+
* parameters) falls back to the original bytes, without `frame` the
|
|
81
|
+
* seek-preview route stays unmounted.
|
|
82
|
+
*/
|
|
83
|
+
export type WorkbenchProviders = {
|
|
84
|
+
readonly resources: () =>
|
|
85
|
+
| Promise<readonly WorkbenchResource[]>
|
|
86
|
+
| readonly WorkbenchResource[]
|
|
87
|
+
readonly files?: WorkbenchFileProvider
|
|
88
|
+
readonly snapshot?: (
|
|
89
|
+
resId: string,
|
|
90
|
+
) =>
|
|
91
|
+
| Promise<WorkbenchHookSnapshot | undefined>
|
|
92
|
+
| WorkbenchHookSnapshot
|
|
93
|
+
| undefined
|
|
94
|
+
readonly state?: (
|
|
95
|
+
resId: string,
|
|
96
|
+
) =>
|
|
97
|
+
| Promise<WorkbenchResourceState | undefined>
|
|
98
|
+
| WorkbenchResourceState
|
|
99
|
+
| undefined
|
|
100
|
+
readonly preview?: (
|
|
101
|
+
resId: string,
|
|
102
|
+
path: string,
|
|
103
|
+
/**
|
|
104
|
+
* The file route's raw variant query (`size`, `fmt`, `fit`,
|
|
105
|
+
* `area`, `q`) — parsed and validated by the provider, so the
|
|
106
|
+
* workbench mount itself stays dependency-free.
|
|
107
|
+
*/
|
|
108
|
+
variant?: ImageVariantQuery,
|
|
109
|
+
) => Promise<WorkbenchRendered | undefined>
|
|
110
|
+
readonly frame?: (
|
|
111
|
+
resId: string,
|
|
112
|
+
path: string,
|
|
113
|
+
timeMs: number,
|
|
114
|
+
) => Promise<WorkbenchRendered | undefined>
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Options for {@link serveWorkbench}. */
|
|
118
|
+
export type ServeWorkbenchOptions = {
|
|
119
|
+
/** Built plugin dist dir mounted at `/plugin` (manifest + index.html). */
|
|
120
|
+
readonly pluginDir?: string
|
|
121
|
+
/**
|
|
122
|
+
* Data root exposed as a single resource. Shorthand for the
|
|
123
|
+
* directory providers; ignored when `providers` is given.
|
|
124
|
+
*/
|
|
125
|
+
readonly dataDir?: string
|
|
126
|
+
/** Real data sources. `hoardodile plugin dev` supplies these. */
|
|
127
|
+
readonly providers?: WorkbenchProviders
|
|
128
|
+
/**
|
|
129
|
+
* Latest hook snapshot. Called per request so a watch-driven
|
|
130
|
+
* recapture is picked up without restarting. Merged into
|
|
131
|
+
* `providers` when both are given.
|
|
132
|
+
*/
|
|
133
|
+
readonly snapshot?: (
|
|
134
|
+
resId: string,
|
|
135
|
+
) =>
|
|
136
|
+
| Promise<WorkbenchHookSnapshot | undefined>
|
|
137
|
+
| WorkbenchHookSnapshot
|
|
138
|
+
| undefined
|
|
139
|
+
/** Port to listen on. Defaults to 5199. */
|
|
140
|
+
readonly port?: number
|
|
141
|
+
/** Bind host. Defaults to 127.0.0.1. */
|
|
142
|
+
readonly host?: string
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Serve the published workbench SPA with the plugin bundle and the
|
|
147
|
+
* resource data mounted read-only. Resolves once the server is
|
|
148
|
+
* listening.
|
|
149
|
+
*/
|
|
150
|
+
export function serveWorkbench(opts: ServeWorkbenchOptions): Promise<Server>
|
|
151
|
+
|
|
152
|
+
/** Providers over one plain directory, standing in for a single resource. */
|
|
153
|
+
export function createDirectoryProviders(
|
|
154
|
+
dataDir: string,
|
|
155
|
+
resId?: string,
|
|
156
|
+
): WorkbenchProviders
|
package/dist/serve.mjs
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Standalone server for the published workbench bundle: the prebuilt SPA
|
|
4
|
+
* from `dist/` plus the read-only mounts the page needs at runtime. The
|
|
5
|
+
* routing itself lives in `mounts.mjs`, shared with the vite dev server
|
|
6
|
+
* so the two can never drift.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node serve.mjs --plugin <plugin-dist-dir> --data <data-dir> [--port 5199]
|
|
10
|
+
* [--snapshot <hooks.json>]
|
|
11
|
+
*
|
|
12
|
+
* Also exported so the plugin CLI's `dev` subcommand can serve the
|
|
13
|
+
* workbench with richer providers (real storage, preview variants,
|
|
14
|
+
* video frames): `import { serveWorkbench } from "@hoardodile/workbench"`.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readFileSync, statSync } from "node:fs"
|
|
17
|
+
import { createServer } from "node:http"
|
|
18
|
+
import { dirname, join, resolve, sep } from "node:path"
|
|
19
|
+
import { fileURLToPath } from "node:url"
|
|
20
|
+
import {
|
|
21
|
+
contentTypeOf,
|
|
22
|
+
createDirectoryProviders,
|
|
23
|
+
createWorkbenchMounts,
|
|
24
|
+
} from "./mounts.mjs"
|
|
25
|
+
|
|
26
|
+
const DIST_DIR = resolve(dirname(fileURLToPath(import.meta.url)))
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Serve the published workbench on `port`. Pass `providers` for real
|
|
30
|
+
* data (the CLI does), or `dataDir` for the plain-directory default.
|
|
31
|
+
* Returns the http.Server.
|
|
32
|
+
*/
|
|
33
|
+
export function serveWorkbench(opts) {
|
|
34
|
+
const { pluginDir, dataDir, port = 5199, host = "127.0.0.1" } = opts
|
|
35
|
+
if (pluginDir === undefined) {
|
|
36
|
+
console.warn("[workbench] no plugin dir — pass --plugin <dist-dir>")
|
|
37
|
+
}
|
|
38
|
+
const base =
|
|
39
|
+
opts.providers ??
|
|
40
|
+
(dataDir === undefined
|
|
41
|
+
? { resources: () => [] }
|
|
42
|
+
: createDirectoryProviders(dataDir))
|
|
43
|
+
if (opts.providers === undefined && dataDir === undefined) {
|
|
44
|
+
console.warn("[workbench] no data dir — pass --data <data-dir>")
|
|
45
|
+
}
|
|
46
|
+
// A snapshot provider passed on its own (the classic `--snapshot`
|
|
47
|
+
// shape) still works: it is just one more provider.
|
|
48
|
+
const providers =
|
|
49
|
+
opts.snapshot === undefined ? base : { ...base, snapshot: opts.snapshot }
|
|
50
|
+
|
|
51
|
+
// Plugin asset vault: the dev server performs the user-consented
|
|
52
|
+
// downloads (browser fetch would hit CORS) into this local scratch
|
|
53
|
+
// root. The CLI passes `<pluginDir>/.hoardodile/vault`; the
|
|
54
|
+
// standalone default sits next to the data dir.
|
|
55
|
+
const vaultRoot =
|
|
56
|
+
opts.vaultRoot ?? join(resolve(dataDir ?? "."), ".workbench-vault")
|
|
57
|
+
|
|
58
|
+
const mounts = createWorkbenchMounts({
|
|
59
|
+
pluginDir,
|
|
60
|
+
providers,
|
|
61
|
+
vault: vaultRoot,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const server = createServer((req, res) => {
|
|
65
|
+
void (async () => {
|
|
66
|
+
try {
|
|
67
|
+
for (const mount of mounts) {
|
|
68
|
+
if (await mount(req, res)) return
|
|
69
|
+
}
|
|
70
|
+
serveSpa(req, res)
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error("[workbench] request failed:", err)
|
|
73
|
+
if (!res.headersSent) res.statusCode = 500
|
|
74
|
+
res.end("workbench error")
|
|
75
|
+
}
|
|
76
|
+
})()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
return new Promise((resolveStart) => {
|
|
80
|
+
server.listen(port, host, () => {
|
|
81
|
+
console.log(`[workbench] serving on http://${host}:${port}`)
|
|
82
|
+
resolveStart(server)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function serveSpa(req, res) {
|
|
88
|
+
const url = new URL(req.url ?? "/", "http://workbench.local")
|
|
89
|
+
if (url.pathname === "/") {
|
|
90
|
+
res.setHeader("content-type", "text/html; charset=utf-8")
|
|
91
|
+
res.end(readFileSync(join(DIST_DIR, "index.html")))
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
const rel = decodeURIComponent(url.pathname).replace(/^\/+/, "")
|
|
95
|
+
const abs = resolve(DIST_DIR, rel)
|
|
96
|
+
if (
|
|
97
|
+
abs === DIST_DIR ||
|
|
98
|
+
!abs.startsWith(DIST_DIR + sep) ||
|
|
99
|
+
!existsSync(abs) ||
|
|
100
|
+
statSync(abs).isDirectory()
|
|
101
|
+
) {
|
|
102
|
+
res.statusCode = 404
|
|
103
|
+
res.end("not found")
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
res.setHeader("content-type", contentTypeOf(abs))
|
|
107
|
+
res.end(readFileSync(abs))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// CLI entry when run directly.
|
|
111
|
+
const isMain =
|
|
112
|
+
process.argv[1] !== undefined &&
|
|
113
|
+
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
114
|
+
if (isMain) {
|
|
115
|
+
const args = process.argv.slice(2)
|
|
116
|
+
const flagValue = (name) => {
|
|
117
|
+
const i = args.indexOf(name)
|
|
118
|
+
return i !== -1 ? args[i + 1] : undefined
|
|
119
|
+
}
|
|
120
|
+
const snapshotPath = flagValue("--snapshot")
|
|
121
|
+
// Re-read on every request: a `plugin dev` style watcher may rewrite
|
|
122
|
+
// the file as the plugin rebuilds.
|
|
123
|
+
const snapshot =
|
|
124
|
+
snapshotPath === undefined
|
|
125
|
+
? undefined
|
|
126
|
+
: () => {
|
|
127
|
+
try {
|
|
128
|
+
return JSON.parse(readFileSync(resolve(snapshotPath), "utf8"))
|
|
129
|
+
} catch {
|
|
130
|
+
return undefined
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
await serveWorkbench({
|
|
134
|
+
pluginDir: flagValue("--plugin"),
|
|
135
|
+
dataDir: flagValue("--data"),
|
|
136
|
+
port: Number(flagValue("--port") ?? 5199),
|
|
137
|
+
snapshot,
|
|
138
|
+
})
|
|
139
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hoardodile/workbench",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Offline dev workbench for hoardodile content plugins.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"hoardodile",
|
|
8
|
+
"plugin",
|
|
9
|
+
"workbench",
|
|
10
|
+
"dev",
|
|
11
|
+
"mock"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/hoardodile/hoardodile.git",
|
|
16
|
+
"directory": "plugins/workbench"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/serve.d.mts",
|
|
22
|
+
"default": "./dist/serve.mjs"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=24"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@playwright/test": "^1.62.1",
|
|
37
|
+
"@rolldown/plugin-babel": "^0.2.3",
|
|
38
|
+
"@tailwindcss/vite": "^4.3.3",
|
|
39
|
+
"@types/node": "^26.2.0",
|
|
40
|
+
"@types/react": "^19.2.18",
|
|
41
|
+
"@types/react-dom": "^19.2.4",
|
|
42
|
+
"@vitejs/plugin-react": "^6.1.0",
|
|
43
|
+
"babel-plugin-react-compiler": "^1.0.0",
|
|
44
|
+
"i18next": "^26.4.0",
|
|
45
|
+
"react": "^19.2.8",
|
|
46
|
+
"react-dom": "^19.2.8",
|
|
47
|
+
"react-i18next": "^17.0.12",
|
|
48
|
+
"tailwindcss": "^4.3.3",
|
|
49
|
+
"typescript": "^7.0.2",
|
|
50
|
+
"vite": "^8.2.2",
|
|
51
|
+
"vitest": "^4.1.11",
|
|
52
|
+
"zod": "^4.4.3",
|
|
53
|
+
"@hoardodile/host-web": "0.0.0",
|
|
54
|
+
"@hoardodile/ui": "0.0.0",
|
|
55
|
+
"@hoardodile/i18n": "0.0.0",
|
|
56
|
+
"@hoardodile/sdk-web": "0.0.0",
|
|
57
|
+
"@hoardodile/sdk-types": "0.0.0"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"dev": "node scripts/dev.mjs",
|
|
61
|
+
"build": "vite build && node scripts/copy-serve.mjs",
|
|
62
|
+
"lint": "tsc --noEmit",
|
|
63
|
+
"test": "vitest run"
|
|
64
|
+
}
|
|
65
|
+
}
|