@rigee/plugin-api 0.0.0-stage → 0.2.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/README.md CHANGED
@@ -1,3 +1,138 @@
1
- # Temporary Holding Version
1
+ # @rigee/plugin-api
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The contract a [Rigee](https://github.com/Qyuan-Tech/rigee) plugin is written against.
4
+
5
+ ```bash
6
+ npm install --save-dev @rigee/plugin-api
7
+ ```
8
+
9
+ It is a development dependency on purpose: a renderer half leaves it out of its bundle and gets
10
+ Rigee's own copy at runtime (see Building), so the version you build against is for types and for
11
+ `minAppVersion`, not something that ships.
12
+
13
+ A plugin is up to two halves, because Electron leaves no choice: a PDF renders in the sandboxed
14
+ renderer, and the bytes it renders can only be read in the main process. Each half default-exports
15
+ a [cordis](https://github.com/cordiverse/cordis) plugin, and the two share an id and talk over one
16
+ IPC channel.
17
+
18
+ ```
19
+ my-plugin/
20
+ package.json the manifest, under a "rigee" key
21
+ dist/main.js ESM. Node. Optional.
22
+ dist/renderer.js ESM. DOM. Optional.
23
+ ```
24
+
25
+ ```jsonc
26
+ {
27
+ "name": "rigee-plugin-hello",
28
+ "version": "1.0.0",
29
+ "rigee": {
30
+ "id": "hello",
31
+ "version": "1.0.0",
32
+ "displayName": "Hello",
33
+ "apiVersion": 1,
34
+ "minAppVersion": "1.1.0",
35
+ "permissions": ["files:read"],
36
+ "main": "dist/main.js",
37
+ "renderer": "dist/renderer.js",
38
+ "contributes": {
39
+ "views": [{ "id": "hello.panel", "title": "Hello", "slot": "left", "icon": "plugin" }]
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ ```tsx
46
+ import { icons, openFile, type PluginViewProps } from '@rigee/plugin-api'
47
+ import type { Context, Plugin } from 'cordis'
48
+
49
+ function Panel({ onCollapse }: PluginViewProps) {
50
+ return <button onClick={onCollapse}><icons.CollapseLeft size={14} /></button>
51
+ }
52
+
53
+ const plugin: Plugin = {
54
+ name: 'hello',
55
+ inject: ['contributions'],
56
+ apply(ctx: Context) {
57
+ ctx.contributions.view('hello.panel', Panel)
58
+ }
59
+ }
60
+ export default plugin
61
+ ```
62
+
63
+ ## Four entry points
64
+
65
+ | import | contains | realm |
66
+ |---|---|---|
67
+ | `@rigee/plugin-api` | everything | renderer half |
68
+ | `@rigee/plugin-api/manifest` | the declarative contract only — no React, no DOM | either |
69
+ | `@rigee/plugin-api/data` | the shared data types only | either |
70
+ | `@rigee/plugin-api/context` | types only: what `ctx.contributions`, `ctx.rpc`, `ctx.commands` and `ctx.shell` are | either |
71
+
72
+ The main half must not import the root entry: it would pull React into a process that cannot
73
+ render. It gets the `ctx` typings with `import type {} from '@rigee/plugin-api/context'`; a plugin
74
+ with a renderer half has them already, through the root entry.
75
+
76
+ ## Building
77
+
78
+ **The renderer half** leaves exactly `react`, `react-dom`, `react/jsx-runtime` and
79
+ `@rigee/plugin-api` external, as ESM. The shell resolves those through an import map to its own
80
+ copies — two Reacts in one window means broken hooks, and a second copy of this package means
81
+ talking to a host nobody installed. Bundle everything else, including anything you import from
82
+ `@rigee/plugin-api/manifest`: the import map names the package, not its subpaths.
83
+
84
+ **The main half** is one bundled ESM file for Node, with only `electron` and Node's built-ins left
85
+ external. One file because Rigee re-imports it after an update under a new URL, and a second file
86
+ it imported relatively would come back from Node's module cache as the old one.
87
+
88
+ **Styles** cannot be a `<link>`: the window's policy does not load stylesheets from a plugin.
89
+ Inline your CSS into the bundle and add it as a `<style>` element when the module loads.
90
+
91
+ **Workers** cannot be started from a plugin's own files — a window only starts workers from its
92
+ own origin. Start one through the shell instead, from a self-contained script (a worker has no
93
+ import map):
94
+
95
+ ```ts
96
+ new Worker(workerUrl(new URL('./parse.worker.js', import.meta.url)), { type: 'module' })
97
+ ```
98
+
99
+ **Code highlighting** comes from the text editor's own tokenizers: `tokenizeCode(text, 'ts')`
100
+ returns the tokens of each line, so a plugin need not ship a highlighter of its own.
101
+
102
+ ## Publishing, so that Rigee can find it
103
+
104
+ Rigee's plugins view searches public GitHub. A repository is found and installable when:
105
+
106
+ | | the rule |
107
+ |---|---|
108
+ | found by | the topic `rigee-plugin` |
109
+ | named | `rigee-plugin-<id>` — the id in lower-case letters, digits and hyphens |
110
+ | released | its latest release (not a draft or pre-release) is tagged `v<version>` |
111
+ | packaged | that release carries `rigee-plugin-<id>-<version>.tgz`, exactly as `npm pack` writes it |
112
+ | agreeing | `name` is the repository's name; the tag, `version` and `rigee.version` are one version |
113
+
114
+ Rigee downloads the tarball, checks it against the SHA-256 GitHub reports for it, unpacks it
115
+ (regular files and directories under `package/`, nothing else), checks the manifest, and only then
116
+ asks the user — saying where the plugin came from, that it was found by searching, and that Rigee
117
+ has not reviewed it. Turn on immutable releases if you can: a release that cannot be changed after
118
+ it is published is one a user can trust to stay what they approved.
119
+
120
+ ## What `apiVersion` and `minAppVersion` are for
121
+
122
+ `apiVersion` is the generation of this contract the plugin was built against. Rigee refuses a
123
+ plugin whose `apiVersion` it does not implement, at discovery, with that as the stated reason —
124
+ rather than letting it fail later as a missing export in a stack trace nobody can act on. Adding a
125
+ function does not bump it; removing or reshaping one does.
126
+
127
+ `minAppVersion` is the oldest Rigee the plugin runs in. A function added to this package exists
128
+ only in Rigee releases from then on, so a plugin that calls one should say so — `tokenizeCode` and
129
+ `workerUrl` arrived in Rigee 1.1.0. An older Rigee then refuses the plugin by name instead of
130
+ failing to link it.
131
+
132
+ ## Permissions, and what they are not
133
+
134
+ `permissions` is an integrity boundary: the grant is recorded per plugin, and a new version that
135
+ asks for more has to ask again. It is **not** a sandbox. A renderer half runs in the shell's own
136
+ JavaScript context, so a hostile plugin can reach whatever the shell can reach regardless of what
137
+ it declared. Installing one is a decision to run its code with the application's privileges, and
138
+ Rigee's install dialog says so in those words.
@@ -0,0 +1,130 @@
1
+ /**
2
+ * The services a plugin's halves are given on `ctx`, as a plugin sees them.
3
+ *
4
+ * The shell implements each one as a cordis Service, and used to declare them on `Context` from its
5
+ * own source — which only a plugin compiled INSIDE this repository could see. A plugin from another
6
+ * repository wrote `ctx.contributions.editor(…)` against a Context that, as far as its compiler
7
+ * knew, had no such property. So the shapes live here, with the augmentation, and the shell's
8
+ * classes implement them: one declaration, and a plugin anywhere types against it.
9
+ *
10
+ * Types only — this module emits nothing. It is reached through the root entry, which a renderer
11
+ * half imports anyway, and on its own as `@rigee/plugin-api/context`: a plugin with only a main
12
+ * half gets the typings with `import type {} from '@rigee/plugin-api/context'`, which costs
13
+ * nothing at runtime and keeps React out of a process that cannot render.
14
+ *
15
+ * Each service has to be named in a half's `inject` before it can be read — `inject: ['commands']`
16
+ * — or cordis throws, and a half whose injection cannot be met never runs at all.
17
+ *
18
+ * BIND A METHOD BEFORE PASSING IT AROUND: `const shell = ctx.shell; const emit = shell.emit.bind(shell)`.
19
+ * cordis tells the shell WHICH plugin is calling by swapping `this` when a method is called on the
20
+ * service itself, so a bare `const emit = ctx.shell.emit` arrives with no `this` and throws. Bound,
21
+ * it is also some eighty times cheaper per call than going through `ctx` each time.
22
+ */
23
+ import type { Context } from 'cordis';
24
+ import type { BrowserWindow, WebContents } from 'electron';
25
+ import type { PluginId, ViewType } from './manifest';
26
+ import type { PluginEditorComponent, PluginOverlayComponent, PluginStatusItemComponent, PluginTabComponent, PluginTabFactory, PluginViewComponent } from './ui';
27
+ /**
28
+ * `ctx.contributions`: the components a manifest promised.
29
+ *
30
+ * Every id must be one the manifest declares, under the plugin's own prefix — `pdf.preview` for
31
+ * the plugin `pdf` — or the bind throws. Each bind is revoked with the plugin, so a tab showing a
32
+ * component of a plugin that was switched off falls back to the shell's notice.
33
+ */
34
+ export interface PluginContributionsService {
35
+ /** A viewer for the files an `editors` contribution claims. */
36
+ editor(viewType: ViewType, component: PluginEditorComponent): void;
37
+ /** A tab type the plugin owns outright. */
38
+ tab(viewType: ViewType, component: PluginTabComponent): void;
39
+ /** What a fresh tab of a `creatable` tab type starts as. */
40
+ tabFactory(viewType: ViewType, factory: PluginTabFactory): void;
41
+ /** A side-bar panel. */
42
+ view(id: ViewType, component: PluginViewComponent): void;
43
+ statusItem(id: ViewType, component: PluginStatusItemComponent): void;
44
+ /** Mounted at the shell's root while the plugin is active. */
45
+ overlay(id: ViewType, component: PluginOverlayComponent): void;
46
+ }
47
+ /**
48
+ * `ctx.rpc`: the renderer half's line to its own main half, and nobody else's. A command or event
49
+ * name without the plugin's own prefix — `pdf:read` for `pdf` — throws locally.
50
+ */
51
+ export interface PluginRpcService {
52
+ /** Call one of this plugin's main-half commands. Rejects with whatever the handler threw. */
53
+ invoke<T = unknown>(command: string, payload?: unknown): Promise<T>;
54
+ /** Listen for one of this plugin's own events — `status`, not `scm:status`. Revoked with the plugin. */
55
+ on<T = unknown>(event: string, listener: (payload: T) => void): void;
56
+ }
57
+ /** `ctx.commands`: what the renderer half can call. */
58
+ export interface PluginCommandsService {
59
+ /**
60
+ * Answer `name`, which must carry the plugin's own prefix: `pdf:read`. What the handler returns
61
+ * — or the promise it returns resolves to — is what the renderer's `invoke` resolves to, so it
62
+ * must survive structured cloning: no functions, no class instances. Revoked with the plugin.
63
+ */
64
+ register(name: string, handler: (payload: unknown) => unknown): void;
65
+ }
66
+ /** A question for `ctx.shell.ask`: Electron's message box, cut down to what a question needs. */
67
+ export interface AskOptions {
68
+ type?: 'none' | 'info' | 'error' | 'question' | 'warning';
69
+ message: string;
70
+ detail?: string;
71
+ buttons?: string[];
72
+ /** The button Return presses. */
73
+ defaultId?: number;
74
+ /** The button Escape presses, and the answer when the dialog is dismissed. */
75
+ cancelId?: number;
76
+ }
77
+ /** `ctx.shell`: what a main half is given of the shell — the window, the renderer, and the user. */
78
+ export interface PluginShellService {
79
+ /** The app window, or null before it exists and after it is gone. */
80
+ window(): BrowserWindow | null;
81
+ /** The shell's own WebContents: the React renderer, not a browser tab. */
82
+ webContents(): WebContents | null;
83
+ /** Push an event to this plugin's renderer half. Plugin-local name: `status`, not `scm:status`. */
84
+ emit(event: string, payload: unknown): void;
85
+ /**
86
+ * A URL the renderer can stream this file from, Range requests and all — what a <video> needs.
87
+ * It names a token, not the path, and works until `releaseFile` or until the plugin is disposed.
88
+ */
89
+ serveFile(path: string, mime: string): string;
90
+ /** Stop serving a URL `serveFile` returned. Unknown or already released is not an error. */
91
+ releaseFile(url: string): void;
92
+ /**
93
+ * Ask the user something in a native dialog over the window: the index of the button pressed.
94
+ * Through the shell rather than Electron's `dialog`, so that Rigee's own tests can answer it.
95
+ */
96
+ ask(options: AskOptions): Promise<number>;
97
+ /**
98
+ * Offer to take keyboard focus instead of the shell — return true if you took it. For a plugin
99
+ * that composites a native view over the window, which the window never focuses by itself.
100
+ */
101
+ claimFocus(handler: () => boolean): void;
102
+ }
103
+ /**
104
+ * The Context both halves receive, with the services above declared on it.
105
+ *
106
+ * Also what keeps `cordis` imported in the published declarations: the augmentation below merges
107
+ * into cordis's own `Context` only when this file imports the module, and a type import that
108
+ * nothing used would be dropped from the emitted .d.ts — leaving a declaration that shadows cordis's
109
+ * Context instead of extending it.
110
+ */
111
+ export type PluginContext = Context;
112
+ declare module 'cordis' {
113
+ interface Context {
114
+ /** Renderer half. */
115
+ contributions: PluginContributionsService;
116
+ /** Renderer half. */
117
+ rpc: PluginRpcService;
118
+ /** Main half. */
119
+ commands: PluginCommandsService;
120
+ /** Main half. */
121
+ shell: PluginShellService;
122
+ /**
123
+ * Which plugin this Context belongs to, set by the host on each plugin's own Context.
124
+ *
125
+ * The `?` is a TypeScript fiction over a Proxy that THROWS on an unknown property read; both
126
+ * hosts install an accessor answering undefined on the root, so reading it is always safe.
127
+ */
128
+ pluginId?: PluginId;
129
+ }
130
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The services a plugin's halves are given on `ctx`, as a plugin sees them.
3
+ *
4
+ * The shell implements each one as a cordis Service, and used to declare them on `Context` from its
5
+ * own source — which only a plugin compiled INSIDE this repository could see. A plugin from another
6
+ * repository wrote `ctx.contributions.editor(…)` against a Context that, as far as its compiler
7
+ * knew, had no such property. So the shapes live here, with the augmentation, and the shell's
8
+ * classes implement them: one declaration, and a plugin anywhere types against it.
9
+ *
10
+ * Types only — this module emits nothing. It is reached through the root entry, which a renderer
11
+ * half imports anyway, and on its own as `@rigee/plugin-api/context`: a plugin with only a main
12
+ * half gets the typings with `import type {} from '@rigee/plugin-api/context'`, which costs
13
+ * nothing at runtime and keeps React out of a process that cannot render.
14
+ *
15
+ * Each service has to be named in a half's `inject` before it can be read — `inject: ['commands']`
16
+ * — or cordis throws, and a half whose injection cannot be met never runs at all.
17
+ *
18
+ * BIND A METHOD BEFORE PASSING IT AROUND: `const shell = ctx.shell; const emit = shell.emit.bind(shell)`.
19
+ * cordis tells the shell WHICH plugin is calling by swapping `this` when a method is called on the
20
+ * service itself, so a bare `const emit = ctx.shell.emit` arrives with no `this` and throws. Bound,
21
+ * it is also some eighty times cheaper per call than going through `ctx` each time.
22
+ */
23
+ export {};
package/dist/data.d.ts ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The shell's data types that appear on the plugin boundary.
3
+ *
4
+ * Moved here from the shell's own `types.ts` rather than copied, and the direction is the point: a
5
+ * plugin compiled against this package must be looking at the SAME `TabId` and `ViewBounds` the
6
+ * shell hands it, or the two agree only by coincidence and drift the first time either side edits
7
+ * a field. `src/shared/types.ts` re-exports these, so the shell still reads as if they were local.
8
+ *
9
+ * Only what actually crosses the boundary lives here. The tab union, the group row and the
10
+ * persisted document stay in the shell: a plugin receives its own state and its own tab id, never
11
+ * the shape the shell keeps them in, and publishing those would freeze the shell's internals into
12
+ * a contract nobody asked for.
13
+ */
14
+ export type WorkspaceId = string;
15
+ export type RepoId = string;
16
+ export type TabId = string;
17
+ /** One root folder inside a workspace. A workspace may hold several, VSCode-style. */
18
+ export interface WorkspaceFolder {
19
+ /** Absolute path. */
20
+ path: string;
21
+ /** Display name; defaults to basename(path) but the user may rename it. */
22
+ name: string;
23
+ /** Whether `path` is (or is inside) a git work tree. Resolved at add time and on refresh. */
24
+ isGitRepo: boolean;
25
+ /**
26
+ * Absolute path to the repository root, when `isGitRepo`. May differ from `path` if the user
27
+ * added a subdirectory of a repo. This, not `path`, is the identity of a repo.
28
+ */
29
+ repoRoot: string | null;
30
+ }
31
+ export interface Workspace {
32
+ id: WorkspaceId;
33
+ name: string;
34
+ folders: WorkspaceFolder[];
35
+ createdAt: number;
36
+ lastOpenedAt: number;
37
+ /** Where terminals start when the workspace has no folders. Only set for Scratch. */
38
+ defaultCwd?: string;
39
+ /**
40
+ * The `.code-workspace` file this workspace was opened from, when it came from one.
41
+ *
42
+ * Absent for a workspace that is just a folder — which is the majority, and is exactly VS Code's
43
+ * distinction between a folder and a saved multi-root workspace. Its presence is what makes
44
+ * "Save Workspace As" an overwrite rather than a new file.
45
+ */
46
+ filePath?: string | null;
47
+ }
48
+ /** One entry in a directory listing. */
49
+ export interface FileStat {
50
+ path: string;
51
+ name: string;
52
+ isDir: boolean;
53
+ /** True when a .gitignore in scope excludes this entry; the tree dims it. */
54
+ ignored: boolean;
55
+ }
56
+ /** Bounds of a DOM placeholder, in CSS pixels relative to the window's content area. */
57
+ export interface ViewBounds {
58
+ x: number;
59
+ y: number;
60
+ width: number;
61
+ height: number;
62
+ }
package/dist/data.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The shell's data types that appear on the plugin boundary.
3
+ *
4
+ * Moved here from the shell's own `types.ts` rather than copied, and the direction is the point: a
5
+ * plugin compiled against this package must be looking at the SAME `TabId` and `ViewBounds` the
6
+ * shell hands it, or the two agree only by coincidence and drift the first time either side edits
7
+ * a field. `src/shared/types.ts` re-exports these, so the shell still reads as if they were local.
8
+ *
9
+ * Only what actually crosses the boundary lives here. The tab union, the group row and the
10
+ * persisted document stay in the shell: a plugin receives its own state and its own tab id, never
11
+ * the shape the shell keeps them in, and publishing those would freeze the shell's internals into
12
+ * a contract nobody asked for.
13
+ */
14
+ export {};