@abraca/plugin 2.3.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.
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Plugin source specification — the user-typed shorthand that resolves to a
3
+ * loadable URL.
4
+ *
5
+ * Today the registry server (Phase C) hosts artifacts at canonical URLs.
6
+ * Until then, both `cou-shell` and `@abraca/nuxt` accept three forms:
7
+ *
8
+ * - `npm:<package>[@version]` → jsDelivr npm CDN
9
+ * - `github:<user>/<repo>[@ref]` → jsDelivr GitHub CDN
10
+ * - any other string → returned as-is (must be `https://` or `http://localhost`)
11
+ *
12
+ * Once Phase C ships we add:
13
+ *
14
+ * - `abra:<plugin-id>[@version]` → the official registry
15
+ *
16
+ * The output URL points at the plugin's bundle entry. Convention is
17
+ * `<root>/dist/plugin.js`.
18
+ */
19
+
20
+ /** A locally-installed external plugin record (persisted to host storage). */
21
+ export interface ExternalPluginEntry {
22
+ /** Resolved fetch URL — the output of `normalizePluginUrl`. */
23
+ url: string;
24
+ /** Plugin `name` from the loaded bundle's manifest / default export. */
25
+ name: string;
26
+ label?: string;
27
+ version?: string;
28
+ description?: string;
29
+ enabled: boolean;
30
+ /** Most recent load error, if any. Cleared on next successful load. */
31
+ error?: string;
32
+ installedAt: number;
33
+ /**
34
+ * Integrity hash from the plugin's manifest, if known. Verified on every
35
+ * load — a silent change indicates supply-chain tampering and the host
36
+ * should refuse to instantiate the plugin.
37
+ */
38
+ integrity?: string;
39
+ }
40
+
41
+ /**
42
+ * Resolve a user-typed plugin spec to a fetchable URL.
43
+ *
44
+ * - `npm:foo@1.2.3` → `https://cdn.jsdelivr.net/npm/foo@1.2.3/dist/plugin.js`
45
+ * - `github:org/repo@main` → `https://cdn.jsdelivr.net/gh/org/repo@main/dist/plugin.js`
46
+ * - anything else → returned trimmed, unchanged
47
+ */
48
+ export function normalizePluginUrl(input: string): string {
49
+ const trimmed = input.trim();
50
+ if (trimmed.startsWith("npm:")) {
51
+ const pkg = trimmed.slice(4);
52
+ return `https://cdn.jsdelivr.net/npm/${pkg}/dist/plugin.js`;
53
+ }
54
+ if (trimmed.startsWith("github:")) {
55
+ const repo = trimmed.slice(7);
56
+ return `https://cdn.jsdelivr.net/gh/${repo}/dist/plugin.js`;
57
+ }
58
+ return trimmed;
59
+ }
60
+
61
+ /**
62
+ * Reject any URL that isn't HTTPS or localhost over HTTP. Hosts should call
63
+ * this before importing — `normalizePluginUrl` does not enforce the policy
64
+ * (npm:/github: shorthand always produces HTTPS, so callers only need this
65
+ * for raw third-party URLs).
66
+ */
67
+ export function isSafePluginUrl(url: string): boolean {
68
+ try {
69
+ const u = new URL(url);
70
+ if (u.protocol === "https:") return true;
71
+ if (
72
+ u.protocol === "http:" &&
73
+ (u.hostname === "localhost" || u.hostname === "127.0.0.1")
74
+ ) {
75
+ return true;
76
+ }
77
+ return false;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
package/src/types.ts ADDED
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Shared plugin contract for cou-shell and `@abraca/nuxt`.
3
+ *
4
+ * `AbraPlugin` is the universal interface every plugin satisfies. App-specific
5
+ * supersets (`CouPlugin`, `AbracadabraPlugin`) extend or intersect this with
6
+ * tightened contribution shapes — TipTap toolbar items, Nuxt UI dropdown
7
+ * items, page-type renderers, file importers, etc.
8
+ *
9
+ * Zero peer-dep types. `Y.Doc`, `@tiptap/core`'s `Extension`, and Vue's
10
+ * `Component` are all referenced via local structural markers (`AbraYDoc`,
11
+ * `AbraTiptapExtension`, `unknown`) so this package compiles in isolation
12
+ * and can be consumed by hosts on any TipTap / yjs / Vue version without
13
+ * dragging duplicate-type fights along.
14
+ */
15
+
16
+ /**
17
+ * Structural marker for `yjs`'s `Y.Doc`. Defined locally — and intentionally
18
+ * permissive — so the plugin contract doesn't pin a specific yjs version. Any
19
+ * `Y.Doc` from any minor yjs version (13.6.29, 13.6.30, …) is structurally
20
+ * compatible. Consumers cast back to their own `Y.Doc` as needed.
21
+ */
22
+ export interface AbraYDoc {
23
+ readonly clientID?: number;
24
+ }
25
+
26
+ /**
27
+ * Structural marker for `@tiptap/core`'s `Extension`. Defined locally — and
28
+ * intentionally permissive — so the plugin contract doesn't pin a specific
29
+ * TipTap version. Any `Extension` from any minor TipTap version (3.22.5,
30
+ * 3.23.2, …) is structurally compatible. Consumers cast back to their own
31
+ * `Extension` as needed.
32
+ */
33
+ export interface AbraTiptapExtension {
34
+ readonly name?: string;
35
+ readonly type?: string;
36
+ }
37
+
38
+ // ── Plugin definition ──────────────────────────────────────────────────────────
39
+
40
+ /**
41
+ * The universal plugin shape. Every plugin in the Abracadabra ecosystem
42
+ * (built-in or third-party, in cou-shell or any `@abraca/nuxt` consumer)
43
+ * satisfies this interface.
44
+ *
45
+ * Contribution fields use widened shapes (`unknown`, `Record<string, unknown>`)
46
+ * where the concrete type is consumer-defined. App-level plugin aliases
47
+ * (e.g. `CouPlugin`, `AbracadabraPlugin`) narrow them.
48
+ */
49
+ export interface AbraPlugin {
50
+ /** Unique slug. Must be stable across versions, e.g. `core`, `kanban`, `my-plugin`. */
51
+ name: string;
52
+ /** Human-readable label shown in plugin browsers. Falls back to `name`. */
53
+ label?: string;
54
+ /** Plugin version. Should follow semver. */
55
+ version?: string;
56
+ /** One-line plugin description shown in browsers and scorecards. */
57
+ description?: string;
58
+ /**
59
+ * Whether a built-in plugin is enabled by default. `true` if omitted.
60
+ * Only consulted for plugins that ship as built-ins; external plugins
61
+ * are opt-in regardless.
62
+ */
63
+ defaultEnabled?: boolean;
64
+
65
+ // ── Editor contributions ────────────────────────────────────────────────────
66
+
67
+ /** TipTap extensions (nodes, marks, extensions) contributed by this plugin. */
68
+ extensions?: () => AbraTiptapExtension[];
69
+
70
+ /**
71
+ * Optional promise that resolves when this plugin's extensions are fully
72
+ * loaded. Used by `PluginRegistry.waitForExtensions()` so the editor
73
+ * doesn't initialise before async extension bundles are ready.
74
+ */
75
+ extensionsReady?: Promise<void>;
76
+
77
+ /** Page-type renderers keyed by type slug. Concrete shape is consumer-defined. */
78
+ pageTypes?: Record<string, unknown>;
79
+
80
+ /**
81
+ * Custom editor handlers (e.g. `table`, `fileUpload`). Each entry merges
82
+ * into the host's handler map. Handler signatures are consumer-defined —
83
+ * cou-shell types them as `EditorHandler` from `@nuxt/ui` (which uses
84
+ * specific arg counts that wouldn't match `(...args: any[]) => any`), the
85
+ * module uses a different shape; the shared contract just promises a
86
+ * record of any values.
87
+ */
88
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
89
+ customHandlers?: () => Record<string, any>;
90
+
91
+ /**
92
+ * Editor toolbar / menu / slash-command / drag-handle contributions.
93
+ *
94
+ * The context shape and item-group shape are both consumer-defined. Method
95
+ * shorthand syntax is used deliberately — interface-method positions are
96
+ * bivariant in TypeScript, so app-specific aliases (`AbracadabraPlugin`,
97
+ * `CouPlugin`) can narrow `ctx` to their own tightened context types
98
+ * without `Omit`-and-rewrite gymnastics.
99
+ */
100
+ toolbarItems?(ctx: EditorPluginCtx): readonly (readonly unknown[])[];
101
+
102
+ bubbleMenuItems?(ctx: EditorPluginCtx): readonly (readonly unknown[])[];
103
+
104
+ suggestionItems?(ctx: EditorPluginCtx): readonly (readonly unknown[])[];
105
+
106
+ dragHandleItems?(ctx: DragHandlePluginCtx): readonly (readonly unknown[])[];
107
+
108
+ /** Mention (@) providers contributing items to the mention popup. */
109
+ mentionProviders?: AbraMentionProvider[];
110
+
111
+ /** Awareness field declarations — keys this plugin reads/writes on awareness. */
112
+ awarenessContributions?: AbraAwarenessContribution[];
113
+
114
+ /** Global command-palette (⌘K) items. */
115
+ commandPaletteItems?(
116
+ ctx: CommandPaletteCtx,
117
+ ): readonly AbraCommandItem[] | Promise<readonly AbraCommandItem[]>;
118
+
119
+ /** Extra tabs rendered in the child-document slideover panel. */
120
+ nodePanelSlots?: AbraNodePanelSlot[];
121
+
122
+ /** Settings panel contributed to the global settings view. */
123
+ settingsPanel?: AbraSettingsPanel;
124
+
125
+ /** Global keyboard shortcuts (document-level, not inside TipTap scope). */
126
+ keyboardShortcuts?: AbraKeyboardShortcut[];
127
+
128
+ /**
129
+ * Client-side setup hook — called once after the client + provider are
130
+ * ready. May return a teardown function called on app unmount.
131
+ */
132
+ clientSetup?(
133
+ ctx: ClientPluginCtx,
134
+ ):
135
+ | undefined
136
+ | void
137
+ | (() => void)
138
+ | Promise<undefined | void | (() => void)>;
139
+
140
+ // ── Server-side contributions ───────────────────────────────────────────────
141
+
142
+ /** TipTap extensions safe for server-side Y.Doc → HTML rendering (no Vue NodeViews). */
143
+ serverExtensions?: () => AbraTiptapExtension[];
144
+
145
+ /**
146
+ * Custom cache renderer for non-doc page types. Receives the Y.Doc and
147
+ * returns serialisable data for the host's cache. Method shorthand —
148
+ * bivariant, so consumer plugins can narrow the `ydoc` parameter to a
149
+ * real `Y.Doc` from their own yjs version.
150
+ */
151
+ serverCacheRenderer?(docId: string, ydoc: AbraYDoc): unknown;
152
+
153
+ /**
154
+ * Server-side background runners (Nitro context, service-role identity).
155
+ *
156
+ * Typed as `unknown[]` in the shared contract because each host wraps
157
+ * the runner ctx with its own `AbracadabraClient` / `AbracadabraProvider`
158
+ * / `DocCacheAPI` / etc. — a structural override here would force every
159
+ * consumer to either match the shared base structure exactly or fight
160
+ * input-contravariance. Consumers narrow this to their own
161
+ * `ServerRunnerDefinition[]` in their `CouPlugin` / `AbracadabraPlugin`
162
+ * aliases.
163
+ */
164
+ serverRunners?: readonly unknown[];
165
+ }
166
+
167
+ // ── Context objects ────────────────────────────────────────────────────────────
168
+
169
+ /**
170
+ * Context passed to editor-scoped plugin hooks.
171
+ *
172
+ * `editor` is the TipTap editor wrapped in whatever ref-like the host uses
173
+ * (`ShallowRef<Editor | null>` in Vue 3 hosts). Typed as a structural ref to
174
+ * avoid pulling `vue` into the type closure of plugin consumers that don't
175
+ * use Vue.
176
+ */
177
+ export interface EditorPluginCtx<TEditor = unknown> {
178
+ editor: EditorRefLike<TEditor>;
179
+ docId: string;
180
+ }
181
+
182
+ /**
183
+ * Context passed to drag-handle plugin hooks. Carries the currently-selected
184
+ * node plus its document position.
185
+ */
186
+ export interface DragHandlePluginCtx<TEditor = unknown, TNode = unknown>
187
+ extends EditorPluginCtx<TEditor> {
188
+ selectedNode: RefLike<
189
+ { node: TNode | null; pos: number } | undefined
190
+ >;
191
+ }
192
+
193
+ /**
194
+ * Context passed to `clientSetup`. `abracadabra` is the host's full reactive
195
+ * state — typed as `unknown` here; app-specific plugin aliases tighten this
196
+ * to `AbracadabraState` / `ReturnType<typeof useAbracadabra>` / similar.
197
+ */
198
+ export interface ClientPluginCtx<TState = unknown> {
199
+ abracadabra: TState;
200
+ }
201
+
202
+ /** Context passed to command-palette plugin hooks. */
203
+ export interface CommandPaletteCtx<TState = unknown, TTree = unknown> {
204
+ docId: string | null;
205
+ /** `useRouter()`'s return value. Typed as `unknown` to avoid a `vue-router` peer dep. */
206
+ router: unknown;
207
+ abracadabra: TState;
208
+ tree: TTree | null;
209
+ }
210
+
211
+ /** Context passed to mention provider hooks. */
212
+ export interface MentionPluginCtx {
213
+ docId: string;
214
+ connectedUsers: readonly CollaborationUser[];
215
+ rootDoc: AbraYDoc;
216
+ }
217
+
218
+ // ── Ref-like primitives ────────────────────────────────────────────────────────
219
+
220
+ /**
221
+ * Minimal shape of a Vue `Ref`/`ShallowRef`/Solid signal. Defined locally so
222
+ * the plugin contract doesn't depend on a specific reactivity library.
223
+ */
224
+ export interface RefLike<T> {
225
+ value: T;
226
+ }
227
+
228
+ /** A read-mostly ref for the TipTap editor instance. */
229
+ export type EditorRefLike<TEditor = unknown> = RefLike<TEditor | null>;
230
+
231
+ // ── Contribution types ────────────────────────────────────────────────────────
232
+
233
+ export interface AbraMentionProvider {
234
+ /** Group label shown in the suggestion popup. */
235
+ label: string;
236
+ /**
237
+ * Method shorthand (not arrow property) — TS treats this position as
238
+ * bivariant, so consumer plugins can narrow `ctx` to their own tightened
239
+ * mention-context type without contravariance fights.
240
+ */
241
+ getItems(
242
+ query: string,
243
+ ctx: MentionPluginCtx,
244
+ ): readonly AbraMentionItem[] | Promise<readonly AbraMentionItem[]>;
245
+ }
246
+
247
+ export interface AbraMentionItem {
248
+ id: string;
249
+ label: string;
250
+ avatar?: string;
251
+ color?: string;
252
+ attrs?: Record<string, unknown>;
253
+ }
254
+
255
+ export interface AbraAwarenessContribution {
256
+ /** Namespaced keys this plugin writes, e.g. `['cursor', 'presence:kanban']`. */
257
+ keys: readonly string[];
258
+ /**
259
+ * Optional Vue component rendered as a presence overlay. Typed as
260
+ * `unknown` so the plugin contract stays Vue-free at the type level.
261
+ * Concrete consumers cast to `Component` / `DefineComponent` as needed.
262
+ */
263
+ cursorComponent?: unknown;
264
+ }
265
+
266
+ export interface AbraCommandItem {
267
+ id: string;
268
+ label: string;
269
+ description?: string;
270
+ icon?: string;
271
+ group?: string;
272
+ shortcut?: string;
273
+ /** Method shorthand — bivariant, so consumer types can narrow `ctx`. */
274
+ when?(ctx: CommandPaletteCtx): boolean;
275
+ /** Method shorthand — bivariant, so consumer types can narrow `ctx`. */
276
+ handler(ctx: CommandPaletteCtx): void | Promise<void>;
277
+ }
278
+
279
+ export interface AbraNodePanelSlot {
280
+ id: string;
281
+ label: string;
282
+ icon: string;
283
+ /** Method shorthand — bivariant, so consumer types can narrow `ctx`. */
284
+ when(ctx: NodePanelCtx): boolean;
285
+ component: unknown;
286
+ }
287
+
288
+ export interface NodePanelCtx {
289
+ childId: string;
290
+ childDoc: AbraYDoc | null;
291
+ parentDocId: string;
292
+ parentType: string | undefined;
293
+ meta: Record<string, unknown>;
294
+ tree: unknown;
295
+ }
296
+
297
+ export interface AbraSettingsPanel {
298
+ label: string;
299
+ icon: string;
300
+ component: unknown;
301
+ }
302
+
303
+ export interface AbraKeyboardShortcut {
304
+ /** Key spec, e.g. `'Meta+Shift+K'`, `'Control+Alt+P'`. */
305
+ key: string;
306
+ description: string;
307
+ handler: () => void | Promise<void>;
308
+ }
309
+
310
+ // ── Collaboration / presence ──────────────────────────────────────────────────
311
+
312
+ export interface CollaborationUser {
313
+ name: string;
314
+ color: string;
315
+ publicKey?: string;
316
+ docId?: string;
317
+ }
318
+
319
+ // ── Server runners ────────────────────────────────────────────────────────────
320
+
321
+ /**
322
+ * Cleanup function returned by a runner's `start()`. Called on graceful
323
+ * server shutdown.
324
+ */
325
+ export type RunnerCleanup =
326
+ | (() => void | Promise<void>)
327
+ | undefined
328
+ | void;
329
+
330
+ /**
331
+ * A named background task that runs inside the host's server process
332
+ * (Nitro, in `@abraca/nuxt`'s case).
333
+ *
334
+ * The runner context is parameterised so each host can extend it with its
335
+ * own client/provider/storage shapes without the plugin contract dragging
336
+ * `@abraca/dabra` along.
337
+ */
338
+ export interface ServerRunnerDefinition<TCtx = ServerRunnerContextBase> {
339
+ name: string;
340
+ /** Method shorthand — bivariant, so consumer types can narrow `ctx`. */
341
+ start(ctx: TCtx): Promise<RunnerCleanup> | RunnerCleanup;
342
+ }
343
+
344
+ /**
345
+ * Minimum context every server runner receives. Hosts extend this with
346
+ * `client`, `wsp`, `rootProvider`, etc.
347
+ */
348
+ export interface ServerRunnerContextBase {
349
+ /** Root Y.Doc (the world / hub doc, or the space root doc for space-scoped runners). */
350
+ rootDoc: AbraYDoc;
351
+ /** Space ID if this runner is scoped to a space; `null` for hub-scoped runners. */
352
+ spaceId?: string | null;
353
+ }