create-zenbu-app 0.0.34 → 0.0.36
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/package.json +2 -2
- package/templates/plugin/AGENTS.md +309 -172
- package/templates/tailwind/AGENTS.md +305 -172
- package/templates/vanilla/AGENTS.md +305 -172
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Zenbu.js docs
|
|
2
2
|
|
|
3
|
+
# Plugin rules
|
|
4
|
+
|
|
5
|
+
- do not add this plugin to the host config (the plugins array in zenbu.config.ts, a zenbu.local.ts overlay, or any zenbu.plugins*.jsonc manifest). build it in its own folder and leave it there. the user enables or installs it themselves when they are ready, or will ask you to. do not edit their config files on your own.
|
|
6
|
+
|
|
3
7
|
# Overview
|
|
4
8
|
Source: https://zenbulabs.mintlify.app/api-reference/overview
|
|
5
9
|
|
|
@@ -110,7 +114,7 @@ export class CounterService extends Service.create({
|
|
|
110
114
|
private count = 0
|
|
111
115
|
|
|
112
116
|
evaluate() {
|
|
113
|
-
// Called when the service starts
|
|
117
|
+
// Called when the service starts.
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
increment() {
|
|
@@ -158,7 +162,7 @@ function App() {
|
|
|
158
162
|
const rpc = useRpc()
|
|
159
163
|
|
|
160
164
|
const handleClick = async () => {
|
|
161
|
-
const newCount = await rpc.counter.increment()
|
|
165
|
+
const newCount = await rpc.app.counter.increment()
|
|
162
166
|
console.log(newCount)
|
|
163
167
|
}
|
|
164
168
|
|
|
@@ -203,15 +207,21 @@ function Notifications() {
|
|
|
203
207
|
}
|
|
204
208
|
```
|
|
205
209
|
|
|
210
|
+
## Injections
|
|
211
|
+
|
|
212
|
+
When a plugin wants to add UI to the app, it registers an [**injection**](/core/injections). The host already looks for injections in known places (sidebars, the bottom panel, the title bar, the footer) and renders them. A service registers one with `this.inject({ name, modulePath, meta })`, and any other plugin discovers it with `useInjections({ kind })` or renders it with `<View name="..." />`. The same primitive also covers [advice](/core/advice) and code that just needs to run at boot.
|
|
213
|
+
|
|
206
214
|
|
|
207
215
|
# Advice
|
|
208
216
|
Source: https://zenbulabs.mintlify.app/core/advice
|
|
209
217
|
|
|
210
218
|
|
|
211
219
|
|
|
212
|
-
Advice lets a plugin wrap or replace a function or React component owned by another plugin, without modifying the original source.
|
|
220
|
+
Advice lets a plugin wrap or replace a function or React component owned by another plugin, without modifying the original source. This API is inspired by Emacs, where [`defadvice`](https://www.gnu.org/software/emacs/manual/html_node/elisp/Advising-Functions.html) is used to modify existing functions without editing their source.
|
|
213
221
|
|
|
214
|
-
|
|
222
|
+
<Info>
|
|
223
|
+
Advice is a kind of [injection](/core/injections). `this.advise(...)` is sugar over `this.inject(...)` with `meta.kind: "advice"`.
|
|
224
|
+
</Info>
|
|
215
225
|
|
|
216
226
|
## Registering advice
|
|
217
227
|
|
|
@@ -224,7 +234,6 @@ export class ChromeService extends Service.create({ key: "chrome" }) {
|
|
|
224
234
|
evaluate() {
|
|
225
235
|
this.setup("wrap-counter", () =>
|
|
226
236
|
this.advise({
|
|
227
|
-
view: "entrypoint",
|
|
228
237
|
moduleId: "App.tsx",
|
|
229
238
|
name: "Counter",
|
|
230
239
|
type: "around",
|
|
@@ -236,24 +245,28 @@ export class ChromeService extends Service.create({ key: "chrome" }) {
|
|
|
236
245
|
}
|
|
237
246
|
```
|
|
238
247
|
|
|
239
|
-
| Field
|
|
240
|
-
|
|
|
241
|
-
| `
|
|
242
|
-
| `
|
|
243
|
-
| `
|
|
244
|
-
| `
|
|
245
|
-
| `
|
|
246
|
-
| `
|
|
248
|
+
| Field | Meaning |
|
|
249
|
+
| --------------- | ----------------------------------------------------------------------- |
|
|
250
|
+
| `moduleId` | Suffix of the source file that exports the target. |
|
|
251
|
+
| `name` | Name of the export to advise. |
|
|
252
|
+
| `type` | One of `"replace"`, `"before"`, `"after"`, `"around"`. |
|
|
253
|
+
| `modulePath` | Path to your wrapper module, relative to the plugin root (or absolute). |
|
|
254
|
+
| `exportName` | Named export inside the wrapper module. Defaults to `default`. |
|
|
255
|
+
| `injectionName` | Optional override for the synthetic injection name. |
|
|
247
256
|
|
|
248
|
-
Returns an unregister function.
|
|
257
|
+
Returns an unregister function. Wrap in `this.setup()` for hot-reload cleanup.
|
|
249
258
|
|
|
250
259
|
## Around advice
|
|
251
260
|
|
|
252
|
-
|
|
261
|
+
Around-advice receives the next function in the chain (which is the original target, or the next around-advice if several are stacked) as the **first positional argument**. For React components, since React calls components as `Component(props)`, the signature is `(Original, props)`.
|
|
253
262
|
|
|
254
263
|
```tsx src/content/wrap-counter.tsx theme={null}
|
|
255
|
-
|
|
256
|
-
|
|
264
|
+
import type { ComponentType } from "react"
|
|
265
|
+
|
|
266
|
+
export function WrapCounter<P>(
|
|
267
|
+
Original: ComponentType<P>,
|
|
268
|
+
props: P,
|
|
269
|
+
) {
|
|
257
270
|
return (
|
|
258
271
|
<div className="bordered">
|
|
259
272
|
<Original {...props} />
|
|
@@ -262,7 +275,21 @@ export function WrapCounter(props) {
|
|
|
262
275
|
}
|
|
263
276
|
```
|
|
264
277
|
|
|
265
|
-
|
|
278
|
+
For a plain function `save({ path }: { path: string })`, around-advice would be:
|
|
279
|
+
|
|
280
|
+
```typescript theme={null}
|
|
281
|
+
export function aroundSave(
|
|
282
|
+
next: (args: { path: string }) => void,
|
|
283
|
+
args: { path: string },
|
|
284
|
+
) {
|
|
285
|
+
console.log("before", args.path)
|
|
286
|
+
const result = next(args)
|
|
287
|
+
console.log("after", args.path)
|
|
288
|
+
return result
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Use around-advice when you want to render the original but add structure around it, change its props, or short-circuit it conditionally (skip the `next(...)` call).
|
|
266
293
|
|
|
267
294
|
## Replace advice
|
|
268
295
|
|
|
@@ -276,15 +303,18 @@ export function WrapCounter() {
|
|
|
276
303
|
|
|
277
304
|
## Before / After
|
|
278
305
|
|
|
279
|
-
`before` and `after` advice
|
|
306
|
+
`before` and `after` advice wrap a function without taking over the call.
|
|
307
|
+
|
|
308
|
+
* `before` runs first with the original args (`...originalArgs`); its return value is ignored.
|
|
309
|
+
* `after` runs last with the result followed by the original args (`result, ...originalArgs`). If it returns a value other than `undefined`, that value overrides the result.
|
|
280
310
|
|
|
281
311
|
```typescript theme={null}
|
|
282
|
-
// before: runs
|
|
312
|
+
// before: runs first, then the original
|
|
283
313
|
export function beforeSave(args: { path: string }) {
|
|
284
314
|
console.log("saving", args.path)
|
|
285
315
|
}
|
|
286
316
|
|
|
287
|
-
// after:
|
|
317
|
+
// after: runs last; return a value to override the result
|
|
288
318
|
export function afterSave(result: void, args: { path: string }) {
|
|
289
319
|
console.log("saved", args.path)
|
|
290
320
|
}
|
|
@@ -292,83 +322,7 @@ export function afterSave(result: void, args: { path: string }) {
|
|
|
292
322
|
|
|
293
323
|
## Hot reloading
|
|
294
324
|
|
|
295
|
-
Adding, removing, or editing a `this.advise(...)` call reloads the
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
# Content Scripts
|
|
299
|
-
Source: https://zenbulabs.mintlify.app/core/content-scripts
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
A content script is JavaScript that gets injected into a page at runtime. It runs in the same environment as the page, so it can use React hooks, read from the database, and call RPC methods. Content scripts target views by type, so you can inject into specific views or use `"*"` to inject into all of them.
|
|
304
|
-
|
|
305
|
-
## Use cases
|
|
306
|
-
|
|
307
|
-
* Add UI that appears on every page, like a devtools panel or a chat overlay.
|
|
308
|
-
* Add UI to a specific page without modifying that page's source code.
|
|
309
|
-
* Let a plugin contribute visible elements to the host app.
|
|
310
|
-
|
|
311
|
-
## Registering a content script
|
|
312
|
-
|
|
313
|
-
Call `this.injectContentScript(...)` from a service:
|
|
314
|
-
|
|
315
|
-
```typescript theme={null}
|
|
316
|
-
import { Service } from "@zenbujs/core/runtime"
|
|
317
|
-
|
|
318
|
-
export class DevtoolsService extends Service.create({ key: "devtools" }) {
|
|
319
|
-
evaluate() {
|
|
320
|
-
this.setup("inject", () =>
|
|
321
|
-
this.injectContentScript({
|
|
322
|
-
view: "*",
|
|
323
|
-
modulePath: "src/content/toolbar.tsx",
|
|
324
|
-
}),
|
|
325
|
-
)
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
| Field | Meaning |
|
|
331
|
-
| ------------ | -------------------------------------------------------------------------------------------------------------------------- |
|
|
332
|
-
| `view` | The view type to inject into. Pass `"*"` to inject into every view. |
|
|
333
|
-
| `modulePath` | Path to the script. Relative paths resolve against the plugin's directory; absolute paths are accepted as an escape hatch. |
|
|
334
|
-
|
|
335
|
-
The call returns an unsubscribe function, so wrapping it in `this.setup()` ensures cleanup on hot reload.
|
|
336
|
-
|
|
337
|
-
## Authoring a content script
|
|
338
|
-
|
|
339
|
-
A content script is a module that runs when the page loads. It can mount its own React tree into the DOM. This example adds a toolbar using a shadow root to keep its styles separate from the host page:
|
|
340
|
-
|
|
341
|
-
```tsx src/content/toolbar.tsx theme={null}
|
|
342
|
-
import { StrictMode } from "react"
|
|
343
|
-
import { createRoot } from "react-dom/client"
|
|
344
|
-
import { ZenbuProvider } from "@zenbujs/core/react"
|
|
345
|
-
import { Toolbar } from "./Toolbar"
|
|
346
|
-
|
|
347
|
-
function mount() {
|
|
348
|
-
if (document.body?.dataset.myToolbarMounted === "1") return
|
|
349
|
-
document.body.dataset.myToolbarMounted = "1"
|
|
350
|
-
|
|
351
|
-
const host = document.createElement("div")
|
|
352
|
-
document.body.appendChild(host)
|
|
353
|
-
const shadow = host.attachShadow({ mode: "closed" })
|
|
354
|
-
const reactRoot = document.createElement("div")
|
|
355
|
-
shadow.appendChild(reactRoot)
|
|
356
|
-
|
|
357
|
-
createRoot(reactRoot).render(
|
|
358
|
-
<StrictMode>
|
|
359
|
-
<ZenbuProvider>
|
|
360
|
-
<Toolbar />
|
|
361
|
-
</ZenbuProvider>
|
|
362
|
-
</StrictMode>
|
|
363
|
-
)
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
mount()
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
## Content scripts vs advice
|
|
370
|
-
|
|
371
|
-
Content scripts add new UI alongside the host view's component tree. They cannot replace or wrap existing components. For that, use [advice](/guides/advice-functions) instead.
|
|
325
|
+
Adding, removing, or editing a `this.advise(...)` call reloads the renderer so the new advice takes effect. Edits inside the advice module itself hot-replace through Vite's normal HMR.
|
|
372
326
|
|
|
373
327
|
|
|
374
328
|
# Events
|
|
@@ -441,6 +395,138 @@ The returned function unsubscribes the listener, so return it from your effect's
|
|
|
441
395
|
* **Database** is for state that should persist and drive your UI.
|
|
442
396
|
|
|
443
397
|
|
|
398
|
+
# Injections
|
|
399
|
+
Source: https://zenbulabs.mintlify.app/core/injections
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
An injection is a named value a plugin registers for other code to find. The value can be a React component, a function, or anything else. Other code looks up injections by name, or filters them by optional metadata.
|
|
404
|
+
|
|
405
|
+
## Registering an injection
|
|
406
|
+
|
|
407
|
+
From a service, call `this.inject(...)`:
|
|
408
|
+
|
|
409
|
+
```typescript src/main/services/my-plugin.ts theme={null}
|
|
410
|
+
import { Service } from "@zenbujs/core/runtime"
|
|
411
|
+
|
|
412
|
+
export class MyPluginService extends Service.create({ key: "myPlugin" }) {
|
|
413
|
+
evaluate() {
|
|
414
|
+
this.setup("inject", () =>
|
|
415
|
+
this.inject({
|
|
416
|
+
name: "my-plugin",
|
|
417
|
+
modulePath: "./src/views/my-view.tsx",
|
|
418
|
+
meta: { kind: "left-sidebar", label: "My plugin" },
|
|
419
|
+
}),
|
|
420
|
+
)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
| Field | Required | Description |
|
|
426
|
+
| ------------ | -------- | ------------------------------------------------------------------------ |
|
|
427
|
+
| `name` | yes | Unique key in the registry. Last writer wins. |
|
|
428
|
+
| `modulePath` | yes | Path to the module. Relative paths resolve against the plugin directory. |
|
|
429
|
+
| `exportName` | no | Named export. Defaults to `default`. |
|
|
430
|
+
| `meta` | no | JSON-serializable object. Consumers filter on it. |
|
|
431
|
+
|
|
432
|
+
The call returns an unregister function. Wrap it in `this.setup(...)` so it cleans up on hot reload.
|
|
433
|
+
|
|
434
|
+
## Reading injections
|
|
435
|
+
|
|
436
|
+
`useInjections({ kind })` returns the live list of injections whose `meta.kind` matches.
|
|
437
|
+
|
|
438
|
+
```tsx theme={null}
|
|
439
|
+
import { useInjections } from "@zenbujs/core/react"
|
|
440
|
+
|
|
441
|
+
function LeftSidebar() {
|
|
442
|
+
const tabs = useInjections({ kind: "left-sidebar" })
|
|
443
|
+
return tabs.map((tab) => <Tab key={tab.name} label={tab.meta?.label} />)
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
## Rendering an injection
|
|
448
|
+
|
|
449
|
+
`<View name="...">` looks up the injection and renders its value as a React component. `args` is passed to the component as a prop and is also available via `useViewArgs()` in any child.
|
|
450
|
+
|
|
451
|
+
```tsx theme={null}
|
|
452
|
+
import { View } from "@zenbujs/core/react"
|
|
453
|
+
|
|
454
|
+
<View name="my-plugin" args={{ workspaceId }} fallback={<Loading />} />
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
| Prop | Description |
|
|
458
|
+
| ---------- | ----------------------------------------------------------------------- |
|
|
459
|
+
| `name` | Injection name. Required. |
|
|
460
|
+
| `args` | Object passed to the component as `args`. |
|
|
461
|
+
| `visible` | When `false`, hides the wrapper via `display: none` without unmounting. |
|
|
462
|
+
| `fallback` | Rendered when nothing is registered under `name` yet. |
|
|
463
|
+
|
|
464
|
+
## Meta conventions
|
|
465
|
+
|
|
466
|
+
The framework does not read `meta`. Consumers do. The conventional keys are:
|
|
467
|
+
|
|
468
|
+
| Key | Used for |
|
|
469
|
+
| ------- | ------------------------------------------------------------------------------------------------- |
|
|
470
|
+
| `kind` | Slot discriminator. |
|
|
471
|
+
| `label` | Display text on tabs, palette entries, buttons. |
|
|
472
|
+
| `icon` | Inline SVG. Auto-filled from the plugin's `icons:` map when the key matches the injection `name`. |
|
|
473
|
+
| `order` | Sort hint within a slot. Lower comes first. |
|
|
474
|
+
|
|
475
|
+
## Registering from React
|
|
476
|
+
|
|
477
|
+
`useRegisterInjection` registers an injection while the calling component is mounted, and unregisters on unmount. Use it when the value can only be constructed inside React, for example a CodeMirror extension built from a hook's state.
|
|
478
|
+
|
|
479
|
+
```tsx theme={null}
|
|
480
|
+
import { useRegisterInjection } from "@zenbujs/core/react"
|
|
481
|
+
|
|
482
|
+
function VimSentinel() {
|
|
483
|
+
useRegisterInjection("cm-vim", vim(), {
|
|
484
|
+
kind: "cm.composer-extension-editable",
|
|
485
|
+
})
|
|
486
|
+
return null
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
## Injecting without a value
|
|
491
|
+
|
|
492
|
+
With no `meta`, the framework just imports the module. The module's top-level code runs, but nothing is added to the registry.
|
|
493
|
+
|
|
494
|
+
```typescript theme={null}
|
|
495
|
+
this.inject({
|
|
496
|
+
name: "my-plugin/devtools",
|
|
497
|
+
modulePath: "./src/content/devtools.tsx",
|
|
498
|
+
})
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
## Advice
|
|
502
|
+
|
|
503
|
+
Advice is a kind of injection that replaces or wraps a specific exported function or component anywhere in the app. It is registered with `this.advise(...)`, which targets the export by its source file and export name.
|
|
504
|
+
|
|
505
|
+
```typescript theme={null}
|
|
506
|
+
this.setup("wrap-counter", () =>
|
|
507
|
+
this.advise({
|
|
508
|
+
moduleId: "Counter.tsx",
|
|
509
|
+
name: "Counter",
|
|
510
|
+
type: "around",
|
|
511
|
+
modulePath: "./src/wrap-counter.tsx",
|
|
512
|
+
}),
|
|
513
|
+
)
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
The `type` field controls how the wrapper relates to the original:
|
|
517
|
+
|
|
518
|
+
* `replace` substitutes the original entirely.
|
|
519
|
+
* `around` runs in place of the original. It receives the next function in the chain as its first argument; call it (or don't) to decide whether the original runs.
|
|
520
|
+
* `before` runs first with the original arguments.
|
|
521
|
+
* `after` runs last and receives the result followed by the original arguments. Returning a value other than `undefined` overrides the result.
|
|
522
|
+
|
|
523
|
+
`this.advise(...)` is sugar over `this.inject(...)` with `meta.kind: "advice"`. For the full reference, including how the wrapper module should be written for each `type`, see [Advice](/core/advice).
|
|
524
|
+
|
|
525
|
+
## Hot reloading
|
|
526
|
+
|
|
527
|
+
Adding, removing, or editing a `this.inject(...)` call invalidates the renderer prelude and reloads the window. Edits inside the injection module itself hot-replace through Vite.
|
|
528
|
+
|
|
529
|
+
|
|
444
530
|
# Plugins
|
|
445
531
|
Source: https://zenbulabs.mintlify.app/core/plugins
|
|
446
532
|
|
|
@@ -465,19 +551,24 @@ export default defineConfig({
|
|
|
465
551
|
schema: "./src/main/schema.ts",
|
|
466
552
|
events: "./src/main/events.ts",
|
|
467
553
|
migrations: "./migrations",
|
|
554
|
+
icons: {
|
|
555
|
+
"my-view": '<svg ...>...</svg>',
|
|
556
|
+
},
|
|
468
557
|
}),
|
|
469
558
|
],
|
|
470
559
|
})
|
|
471
560
|
```
|
|
472
561
|
|
|
473
|
-
| Field | Required | Description
|
|
474
|
-
| ------------ | -------- |
|
|
475
|
-
| `name` | yes | Unique plugin identifier. Used as the top-level namespace for the plugin's database section, RPC services, and events (e.g. `db.<name>`, `rpc.<name>.<service>`, `events.<name>.<event>`). Reserved value: `core` belongs to the framework.
|
|
476
|
-
| `services` | yes | Glob patterns for main process service files.
|
|
477
|
-
| `schema` | no | Path to the database schema definition.
|
|
478
|
-
| `events` | no | Path to event type definitions.
|
|
479
|
-
| `migrations` | no | Directory of generated migration files.
|
|
480
|
-
| `
|
|
562
|
+
| Field | Required | Description |
|
|
563
|
+
| ------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
564
|
+
| `name` | yes | Unique plugin identifier. Used as the top-level namespace for the plugin's database section, RPC services, and events (e.g. `db.<name>`, `rpc.<name>.<service>`, `events.<name>.<event>`). Reserved value: `core` belongs to the framework. |
|
|
565
|
+
| `services` | yes | Glob patterns for main process service files. |
|
|
566
|
+
| `schema` | no | Path to the database schema definition. |
|
|
567
|
+
| `events` | no | Path to event type definitions. |
|
|
568
|
+
| `migrations` | no | Directory of generated migration files. |
|
|
569
|
+
| `preload` | no | Path to a renderer preload module. |
|
|
570
|
+
| `icons` | no | Map of inline SVG strings keyed by [injection](/core/injections) `name`. When you call `this.inject({ name })` and a matching key exists, the framework copies the SVG into `meta.icon` for you. Lookup is per-plugin (no cross-plugin fallback). |
|
|
571
|
+
| `dependsOn` | no | Declares which other plugins this plugin needs type definitions from. See [Type dependencies](#type-dependencies). |
|
|
481
572
|
|
|
482
573
|
## Scaffolding a plugin
|
|
483
574
|
|
|
@@ -509,6 +600,39 @@ plugins: [
|
|
|
509
600
|
|
|
510
601
|
Adding or removing a plugin in `zenbu.config.ts` is a hot-reloadable change that takes effect without restarting the app.
|
|
511
602
|
|
|
603
|
+
## Local-only plugins (`localPlugins`)
|
|
604
|
+
|
|
605
|
+
A `zenbu.config.ts` can opt into a per-developer overlay file that's gitignored and loaded only if it exists. Use this when you want to wire a local plugin (e.g. one cloned into `~/.zenbu/plugins/...`) into your app without committing the path:
|
|
606
|
+
|
|
607
|
+
```typescript theme={null}
|
|
608
|
+
export default defineConfig({
|
|
609
|
+
uiEntrypoint: "./src/renderer",
|
|
610
|
+
plugins: [
|
|
611
|
+
"./plugins/app/zenbu.plugin.ts",
|
|
612
|
+
],
|
|
613
|
+
localPlugins: "./zenbu.local.ts", // gitignored; optional
|
|
614
|
+
})
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
```typescript zenbu.local.ts theme={null}
|
|
618
|
+
import type { LocalPluginsDefault } from "@zenbujs/core/config"
|
|
619
|
+
|
|
620
|
+
const plugins: LocalPluginsDefault = [
|
|
621
|
+
"/Users/me/.zenbu/plugins/the-browser-plugin/zenbu.plugin.ts",
|
|
622
|
+
// or an inline definePlugin({...})
|
|
623
|
+
]
|
|
624
|
+
export default plugins
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
Rules:
|
|
628
|
+
|
|
629
|
+
* The default export is a plugin entry, or an array of entries. Same shape as `plugins`.
|
|
630
|
+
* Relative paths inside the overlay anchor to the **overlay file's** directory, not the project root.
|
|
631
|
+
* `localPlugins` accepts a single string or an array of strings (multiple overlays).
|
|
632
|
+
* If the overlay file doesn't exist, the field is silently ignored.
|
|
633
|
+
* Editing the overlay file hot-reloads exactly like editing `zenbu.config.ts`.
|
|
634
|
+
* `zen build:source` / `zen build:electron` / `zen publish:source` **skip** `localPlugins` entirely, so a developer's overlay can never ship in a build artefact.
|
|
635
|
+
|
|
512
636
|
## Plugin capabilities
|
|
513
637
|
|
|
514
638
|
A plugin has the same capabilities as the host application:
|
|
@@ -516,9 +640,7 @@ A plugin has the same capabilities as the host application:
|
|
|
516
640
|
* **Services** run in the main process and expose methods via RPC.
|
|
517
641
|
* **Database schemas** define sections of the shared database.
|
|
518
642
|
* **Events** can be emitted and listened to across plugins.
|
|
519
|
-
* **
|
|
520
|
-
* **Content scripts** inject JavaScript into any view.
|
|
521
|
-
* **Advice** lets a plugin wrap or replace any React component in the renderer.
|
|
643
|
+
* **[Injections](/core/injections)** are the unified renderer-side surface. One primitive (`this.inject(...)` / `useRegisterInjection(...)`) covers React components other plugins can render via `<View>`, plain side-effect modules (the old "content script" pattern), plain functions/values, and [advice](/core/advice) (sugar for wrapping or replacing an existing export).
|
|
522
644
|
|
|
523
645
|
## Type dependencies
|
|
524
646
|
|
|
@@ -571,7 +693,7 @@ Under the hood, communication happens over a WebSocket, which means the same RPC
|
|
|
571
693
|
Services are scoped under their owning plugin's name. For a plugin named `app`, the call site is `rpc.app.<service>.<method>(...)`. Core's own services live under `rpc.core.<service>`.
|
|
572
694
|
|
|
573
695
|
```typescript theme={null}
|
|
574
|
-
// Main process
|
|
696
|
+
// Main process. Declared inside the `app` plugin.
|
|
575
697
|
export class MathService extends Service.create({
|
|
576
698
|
key: "math",
|
|
577
699
|
}) {
|
|
@@ -689,16 +811,18 @@ Services declare dependencies on other services through the `deps` field. The fr
|
|
|
689
811
|
import { Service } from "@zenbujs/core/runtime"
|
|
690
812
|
import { WindowService } from "@zenbujs/core/services"
|
|
691
813
|
|
|
692
|
-
export class
|
|
693
|
-
key: "
|
|
814
|
+
export class InitService extends Service.create({
|
|
815
|
+
key: "init",
|
|
694
816
|
deps: { window: WindowService },
|
|
695
817
|
}) {
|
|
696
818
|
async evaluate() {
|
|
697
|
-
await this.ctx.window.
|
|
819
|
+
await this.ctx.window.openWindow({})
|
|
698
820
|
}
|
|
699
821
|
}
|
|
700
822
|
```
|
|
701
823
|
|
|
824
|
+
`openWindow({})` boots a main window pointed at your `uiEntrypoint`. Pass `injection: "<name>"` to open a window whose entry route renders a specific [injection](/core/injections) instead.
|
|
825
|
+
|
|
702
826
|
By the time `evaluate()` runs, all dependencies in `this.ctx` are fully initialized.
|
|
703
827
|
|
|
704
828
|
## Setups and cleanups
|
|
@@ -841,13 +965,13 @@ Inside `update()`, you mutate the root object directly, the same way you would w
|
|
|
841
965
|
|
|
842
966
|
Regular data fields are always held in memory across every process. Collections are for data that can grow large (like agent messages or logs) and should only be loaded into memory when needed.
|
|
843
967
|
|
|
844
|
-
Define a collection in your schema with `
|
|
968
|
+
Define a collection in your schema with `collection(...)`:
|
|
845
969
|
|
|
846
970
|
```typescript src/main/schema.ts theme={null}
|
|
847
|
-
import { createSchema,
|
|
971
|
+
import { createSchema, collection, z } from "@zenbujs/core/db"
|
|
848
972
|
|
|
849
973
|
export default createSchema({
|
|
850
|
-
messages:
|
|
974
|
+
messages: collection(
|
|
851
975
|
z.object({
|
|
852
976
|
text: z.string(),
|
|
853
977
|
author: z.string(),
|
|
@@ -896,13 +1020,13 @@ Collection data is only loaded into memory when you subscribe or read from it. T
|
|
|
896
1020
|
|
|
897
1021
|
Blobs store binary data (`Uint8Array`) like files or images. Like collections, they live on disk and are only loaded into memory when you read them.
|
|
898
1022
|
|
|
899
|
-
Define a blob in your schema with `
|
|
1023
|
+
Define a blob in your schema with `blob(...)`:
|
|
900
1024
|
|
|
901
1025
|
```typescript src/main/schema.ts theme={null}
|
|
902
|
-
import { createSchema,
|
|
1026
|
+
import { createSchema, blob, z } from "@zenbujs/core/db"
|
|
903
1027
|
|
|
904
1028
|
export default createSchema({
|
|
905
|
-
avatar:
|
|
1029
|
+
avatar: blob({ debugName: "avatar" }),
|
|
906
1030
|
})
|
|
907
1031
|
```
|
|
908
1032
|
|
|
@@ -976,69 +1100,82 @@ Source: https://zenbulabs.mintlify.app/core/views
|
|
|
976
1100
|
|
|
977
1101
|
|
|
978
1102
|
|
|
979
|
-
|
|
1103
|
+
`<View>` renders an [injection](/core/injections) as React. The injection registry is the source of truth. `<View name="...">` looks up the component registered under `name` (by a service's `this.inject(...)` or by `useRegisterInjection(...)`) and mounts it inside the host tree.
|
|
980
1104
|
|
|
981
|
-
|
|
1105
|
+
There's no separate "view registry", no iframes, no per-view Vite server. Everything is one React tree under `<ZenbuProvider>`, sharing the host's RPC, events, DB, theme, and CSS.
|
|
1106
|
+
|
|
1107
|
+
## Embedding a view
|
|
982
1108
|
|
|
983
|
-
|
|
1109
|
+
```tsx theme={null}
|
|
1110
|
+
import { View } from "@zenbujs/core/react"
|
|
984
1111
|
|
|
985
|
-
|
|
1112
|
+
<View
|
|
1113
|
+
name="terminal"
|
|
1114
|
+
args={{ tabId }}
|
|
1115
|
+
fallback={<Spinner />}
|
|
1116
|
+
/>
|
|
1117
|
+
```
|
|
986
1118
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1119
|
+
| Prop | Meaning |
|
|
1120
|
+
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1121
|
+
| `name` | Injection name to render. Required. |
|
|
1122
|
+
| `args` | Object forwarded as the `args` prop and on `useViewArgs()`. |
|
|
1123
|
+
| `visible` | When `false`, hides the wrapper via `display: none` without unmounting. Use it to preserve in-component state across visibility toggles. |
|
|
1124
|
+
| `fallback` | Rendered while no injection has registered under `name`. |
|
|
1125
|
+
| `className`, `style` | Forwarded to the wrapper. |
|
|
990
1126
|
|
|
991
|
-
|
|
1127
|
+
If no injection has registered yet, `<View>` renders the `fallback`
|
|
1128
|
+
(an empty `<span data-zenbu-view-pending={name}>` if you don't pass
|
|
1129
|
+
one). Once a registration lands, it swaps in automatically.
|
|
992
1130
|
|
|
993
|
-
|
|
1131
|
+
## Reading view args
|
|
994
1132
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
import { fileURLToPath } from "node:url"
|
|
998
|
-
import { Service } from "@zenbujs/core/runtime"
|
|
999
|
-
import { ViewRegistryService } from "@zenbujs/core/services"
|
|
1133
|
+
Inside the rendered component, both the `args` prop and the
|
|
1134
|
+
`useViewArgs()` hook are populated with the same object:
|
|
1000
1135
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
}) {
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
type: "terminal",
|
|
1010
|
-
root: viewRoot,
|
|
1011
|
-
configFile: false,
|
|
1012
|
-
meta: { kind: "view", label: "Terminal" },
|
|
1013
|
-
})
|
|
1014
|
-
}
|
|
1136
|
+
```tsx theme={null}
|
|
1137
|
+
import { useViewArgs, type ViewComponentProps } from "@zenbujs/core/react"
|
|
1138
|
+
|
|
1139
|
+
export default function TerminalApp({ args }: ViewComponentProps<{ tabId: string }>) {
|
|
1140
|
+
// Either of these works:
|
|
1141
|
+
const fromProps = args.tabId
|
|
1142
|
+
const fromHook = useViewArgs<{ tabId: string }>().tabId
|
|
1143
|
+
// …
|
|
1015
1144
|
}
|
|
1016
1145
|
```
|
|
1017
1146
|
|
|
1018
|
-
|
|
1147
|
+
`useViewArgs` is the convenient escape hatch for deeply nested children that don't want to thread `args` down manually. The hook re-renders when the parent `<View>` passes a new `args` object.
|
|
1019
1148
|
|
|
1020
|
-
##
|
|
1021
|
-
|
|
1022
|
-
```tsx theme={null}
|
|
1023
|
-
import { View } from "@zenbujs/core/react"
|
|
1149
|
+
## Registering the component a view renders
|
|
1024
1150
|
|
|
1025
|
-
|
|
1026
|
-
```
|
|
1151
|
+
A view is just an injection. Use the API that fits your situation:
|
|
1027
1152
|
|
|
1028
|
-
|
|
1153
|
+
* **Inside a service** (most plugins): `this.inject({ name, modulePath, meta })` from [Injections](/core/injections).
|
|
1154
|
+
* **From the React tree**: `useRegisterInjection(name, Component, meta)`.
|
|
1029
1155
|
|
|
1030
|
-
|
|
1156
|
+
```typescript theme={null}
|
|
1157
|
+
// From a service.
|
|
1158
|
+
this.setup("inject", () =>
|
|
1159
|
+
this.inject({
|
|
1160
|
+
name: "terminal",
|
|
1161
|
+
modulePath: "./src/views/terminal-view.tsx",
|
|
1162
|
+
meta: { kind: "bottom-panel", label: "Terminal" },
|
|
1163
|
+
}),
|
|
1164
|
+
)
|
|
1165
|
+
```
|
|
1031
1166
|
|
|
1032
1167
|
```tsx theme={null}
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
}
|
|
1168
|
+
// From React.
|
|
1169
|
+
useRegisterInjection("terminal", TerminalView, {
|
|
1170
|
+
kind: "bottom-panel",
|
|
1171
|
+
label: "Terminal",
|
|
1172
|
+
})
|
|
1039
1173
|
```
|
|
1040
1174
|
|
|
1041
|
-
|
|
1175
|
+
## See also
|
|
1176
|
+
|
|
1177
|
+
* [Injections](/core/injections) for the full registration surface and `meta` conventions.
|
|
1178
|
+
* [Advice](/core/advice) for wrapping or replacing another plugin's view.
|
|
1042
1179
|
|
|
1043
1180
|
|
|
1044
1181
|
# Releasing to Production
|
|
@@ -1254,15 +1391,15 @@ Source: https://zenbulabs.mintlify.app/introduction
|
|
|
1254
1391
|
|
|
1255
1392
|
|
|
1256
1393
|
|
|
1257
|
-
Zenbu.js is
|
|
1394
|
+
Zenbu.js is the framework for building extensible applications, powering [Zenbu](https://zenbu.dev).
|
|
1258
1395
|
|
|
1259
|
-
Apps built with Zenbu.js are designed to be modified after they
|
|
1396
|
+
Apps and plugins built with Zenbu.js are designed to be modified after they are installed. Users can edit the source code directly, or install plugins that extend the app with new functionality.
|
|
1260
1397
|
|
|
1261
|
-
The
|
|
1398
|
+
The SDK also handles the hard parts of application development, like syncing state between processes, RPC, and hot-reloading everything as you make changes.
|
|
1262
1399
|
|
|
1263
|
-
### Why build
|
|
1400
|
+
### Why build with Zenbu.js
|
|
1264
1401
|
|
|
1265
|
-
* Coding agents can generate and customize software on demand for a specific use case.
|
|
1402
|
+
* Coding agents can generate and customize software on demand for a specific use case. An app built on Zenbu.js gives them full access to do that.
|
|
1266
1403
|
* Letting people modify your app means more directions get explored than you could reach on your own.
|
|
1267
1404
|
* Extensible code tends to be more maintainable, because it's already written to be changed.
|
|
1268
1405
|
|
|
@@ -1386,9 +1523,9 @@ my-app/
|
|
|
1386
1523
|
├── tsconfig.json
|
|
1387
1524
|
└── src/
|
|
1388
1525
|
├── main/
|
|
1389
|
-
│ ├── schema.ts # Zod schema for the database
|
|
1390
1526
|
│ └── services/
|
|
1391
|
-
│
|
|
1527
|
+
│ ├── init.ts # Opens the main window on boot
|
|
1528
|
+
│ └── cwd.ts # RPC service that returns process.cwd()
|
|
1392
1529
|
└── renderer/
|
|
1393
1530
|
├── index.html
|
|
1394
1531
|
├── main.tsx
|
|
@@ -1401,18 +1538,18 @@ The `zenbu.config.ts` file is the entry point. It tells Zenbu.js where everythin
|
|
|
1401
1538
|
import { defineConfig, definePlugin } from "@zenbujs/core/config"
|
|
1402
1539
|
|
|
1403
1540
|
export default defineConfig({
|
|
1404
|
-
db: "./.zenbu/db",
|
|
1405
1541
|
uiEntrypoint: "./src/renderer",
|
|
1406
1542
|
plugins: [
|
|
1407
1543
|
definePlugin({
|
|
1408
1544
|
name: "app",
|
|
1409
1545
|
services: ["./src/main/services/*.ts"],
|
|
1410
|
-
schema: "./src/main/schema.ts",
|
|
1411
1546
|
}),
|
|
1412
1547
|
],
|
|
1413
1548
|
})
|
|
1414
1549
|
```
|
|
1415
1550
|
|
|
1551
|
+
Add a `schema: "./src/main/schema.ts"` field to the plugin when you're ready to introduce a database. The top-level `db` field is optional and defaults to `./.zenbu/db`.
|
|
1552
|
+
|
|
1416
1553
|
## Available scripts
|
|
1417
1554
|
|
|
1418
1555
|
| Script | What it does |
|