@velora-cms/plugin-sdk 0.9.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 +202 -0
- package/README.md +27 -0
- package/dist/admin-section-registry.d.ts +4 -0
- package/dist/admin-section-registry.js +17 -0
- package/dist/built-in-serializers.d.ts +3 -0
- package/dist/built-in-serializers.js +18 -0
- package/dist/built-in-settings-schemas.d.ts +6 -0
- package/dist/built-in-settings-schemas.js +37 -0
- package/dist/built-ins.d.ts +1 -0
- package/dist/built-ins.js +142 -0
- package/dist/data-type-plugin.d.ts +21 -0
- package/dist/data-type-plugin.js +1 -0
- package/dist/datatypes-entry.d.ts +2 -0
- package/dist/datatypes-entry.js +10 -0
- package/dist/define-plugin.d.ts +25 -0
- package/dist/define-plugin.js +51 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +27 -0
- package/dist/manifest-entry.d.ts +6 -0
- package/dist/manifest-entry.js +9 -0
- package/dist/manifest-schema.d.ts +728 -0
- package/dist/manifest-schema.js +348 -0
- package/dist/manifest.d.ts +370 -0
- package/dist/manifest.js +12 -0
- package/dist/plugin-context.d.ts +30 -0
- package/dist/plugin-context.js +58 -0
- package/dist/registry.d.ts +4 -0
- package/dist/registry.js +17 -0
- package/dist/simple-storage-value-provider.d.ts +9 -0
- package/dist/simple-storage-value-provider.js +58 -0
- package/dist/structured-storage-types.d.ts +53 -0
- package/dist/structured-storage-types.js +11 -0
- package/dist/views/image-view.d.ts +3 -0
- package/dist/views/image-view.js +108 -0
- package/dist/views/rich-text-view.d.ts +2 -0
- package/dist/views/rich-text-view.js +58 -0
- package/package.json +58 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import type { ComponentType } from "react";
|
|
2
|
+
import type { CreateDocumentTypeRequest } from "@velora-cms/api-schemas";
|
|
3
|
+
import type { ThemeTokenOverrides } from "@velora-cms/design-tokens";
|
|
4
|
+
import type { ZodType } from "zod";
|
|
5
|
+
import type { DataTypePlugin } from "./data-type-plugin.js";
|
|
6
|
+
import type { PluginApiRouteContext, PublishedContent, ScopedStructuredStorage } from "./structured-storage-types.js";
|
|
7
|
+
/**
|
|
8
|
+
* The seven plugin types. A plugin's type determines which contributions
|
|
9
|
+
* it may declare — see the contribution matrix in docs/PROJECT_CONTEXT.md
|
|
10
|
+
* (Plugin System), which the per-type manifest variants below encode
|
|
11
|
+
* structurally: TypeScript rejects a disallowed contribution at compile
|
|
12
|
+
* time, and the Zod manifest validation (Session 60) enforces the same
|
|
13
|
+
* matrix at runtime for non-TS authors.
|
|
14
|
+
*/
|
|
15
|
+
export type PluginType = "datatype" | "section" | "template" | "theme" | "integration" | "utility" | "bundle";
|
|
16
|
+
/** The types whose contribution-matrix rows can carry browser-rendered
|
|
17
|
+
* contributions (views, widgets, settings pages, toolbar/tree items) —
|
|
18
|
+
* exactly these ship a Module Federation client bundle
|
|
19
|
+
* (remoteEntry.js). theme/template/bundle are data-only: their packages
|
|
20
|
+
* carry manifest.json + server/plugin.js (the manifest carrier) and
|
|
21
|
+
* nothing client-side. Single source of truth for velora-plugin's
|
|
22
|
+
* build/validate, apps/server's install path, and (as a documented
|
|
23
|
+
* static mirror) the marketplace's submission checks. */
|
|
24
|
+
export declare const CLIENT_BUNDLE_TYPES: readonly ["datatype", "section", "integration", "utility"];
|
|
25
|
+
export declare function hasClientBundle(type: PluginType): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Databases a plugin can declare support for. Deliberately NOT
|
|
28
|
+
* db-adapter's DatabaseDialect: that union includes 'mock' (the in-memory
|
|
29
|
+
* test adapter), which no plugin may target — and the SDK is a published
|
|
30
|
+
* package that must not depend on the unpublished adapter internals.
|
|
31
|
+
*/
|
|
32
|
+
export type DatabaseDialect = "postgresql" | "mysql" | "sqlite" | "mssql" | "mongodb";
|
|
33
|
+
/**
|
|
34
|
+
* How the plugin stores data and where it can run. Required on every
|
|
35
|
+
* manifest (plugin-sdk.md) — the marketplace pre-filters by the site's
|
|
36
|
+
* active database and install is blocked with a clear reason, never a
|
|
37
|
+
* silent failure.
|
|
38
|
+
*/
|
|
39
|
+
export interface DatabaseCompatibility {
|
|
40
|
+
/** 'simple' = JSON key-value SimpleStorageAPI (default, ~80% of
|
|
41
|
+
* plugins); 'structured' = relational StructuredStorageAPI, which
|
|
42
|
+
* requires needsStructuredData and manual marketplace review. */
|
|
43
|
+
storageType: "simple" | "structured";
|
|
44
|
+
supported: DatabaseDialect[];
|
|
45
|
+
/** Optional human-readable reason per UNSUPPORTED dialect (shown on the
|
|
46
|
+
* blocked-install screen) — hence Partial: supported dialects carry no
|
|
47
|
+
* reason. */
|
|
48
|
+
unsupportedReason?: Partial<Record<DatabaseDialect, string>>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Capability declarations surfaced to the user at install time (and, for
|
|
52
|
+
* needsStructuredData, triggering manual marketplace review). Required on
|
|
53
|
+
* every manifest — validation rejects a manifest missing it.
|
|
54
|
+
*/
|
|
55
|
+
export interface PluginPermissions {
|
|
56
|
+
needsStructuredData: boolean;
|
|
57
|
+
needsExternalNetwork: boolean;
|
|
58
|
+
needsFileSystemAccess: boolean;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The storage surface a plugin migration runs against. Deliberately empty
|
|
62
|
+
* for now: plugins never construct raw SQL (non-negotiable), so this is
|
|
63
|
+
* NOT db-adapter's TransactionContext — Month 6's plugin-storage work
|
|
64
|
+
* fills it in (defineTable etc.). Widening a parameter the plugin
|
|
65
|
+
* receives is backward-compatible for plugin authors.
|
|
66
|
+
*/
|
|
67
|
+
export interface PluginMigrationContext {
|
|
68
|
+
}
|
|
69
|
+
/** A database change applied at plugin install/update time. */
|
|
70
|
+
export interface PluginMigration {
|
|
71
|
+
id: string;
|
|
72
|
+
name: string;
|
|
73
|
+
up: (ctx: PluginMigrationContext) => void | Promise<void>;
|
|
74
|
+
down: (ctx: PluginMigrationContext) => void | Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
/** Result of a plugin's validateMigration hook. */
|
|
77
|
+
export interface PluginMigrationValidation {
|
|
78
|
+
compatible: boolean;
|
|
79
|
+
issues: string[];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Participation in a site-wide dialect migration (e.g. SQLite →
|
|
83
|
+
* PostgreSQL, the v1.x wizard): a plugin with its own stored data can
|
|
84
|
+
* veto, transform rows in flight, and run before/after steps.
|
|
85
|
+
*/
|
|
86
|
+
export interface PluginMigrationHooks {
|
|
87
|
+
beforeMigration?: (from: DatabaseDialect, to: DatabaseDialect) => void | Promise<void>;
|
|
88
|
+
transformRow?: (table: string, row: Record<string, unknown>, from: DatabaseDialect, to: DatabaseDialect) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
89
|
+
afterMigration?: (from: DatabaseDialect, to: DatabaseDialect) => void | Promise<void>;
|
|
90
|
+
validateMigration?: (from: DatabaseDialect, to: DatabaseDialect) => PluginMigrationValidation | Promise<PluginMigrationValidation>;
|
|
91
|
+
}
|
|
92
|
+
/** The events a plugin can hook. The hook engine (S159) actually FIRES
|
|
93
|
+
* only three of these — content:afterPublish (including rollbacks),
|
|
94
|
+
* content:afterTrash, content:afterRestore — with the
|
|
95
|
+
* ContentLifecycleHookPayload shape declared below; every other event in
|
|
96
|
+
* this catalog is declared for forward compatibility and stays inert. */
|
|
97
|
+
export type HookEvent = "content:beforeSave" | "content:afterSave" | "content:beforePublish" | "content:afterPublish" | "content:afterTrash" | "content:afterRestore" | "content:beforeDelete" | "media:afterUpload" | "user:afterLogin" | "api:beforeResponse";
|
|
98
|
+
export interface ContentLifecycleHookPayload {
|
|
99
|
+
event: "content:afterPublish" | "content:afterTrash" | "content:afterRestore";
|
|
100
|
+
contentId: string;
|
|
101
|
+
path: string;
|
|
102
|
+
documentTypeId: string;
|
|
103
|
+
occurredAt: string;
|
|
104
|
+
}
|
|
105
|
+
export interface PluginHookContext {
|
|
106
|
+
structuredStorage: ScopedStructuredStorage;
|
|
107
|
+
getPublishedContent(contentId: string): Promise<PublishedContent | null>;
|
|
108
|
+
}
|
|
109
|
+
export type HookHandler = (payload: unknown, context?: PluginHookContext) => void | Promise<void>;
|
|
110
|
+
export type PluginHooks = Partial<Record<HookEvent, HookHandler>>;
|
|
111
|
+
/** A major admin feature area with its own app-bar section (e.g. the
|
|
112
|
+
* Commerce plugin's Orders). First consumer: Month 8. */
|
|
113
|
+
export interface AdminSectionContribution {
|
|
114
|
+
id: string;
|
|
115
|
+
name: string;
|
|
116
|
+
icon: string;
|
|
117
|
+
view: ComponentType;
|
|
118
|
+
/** When true, only admin-role users see this section in the app bar and
|
|
119
|
+
* may reach its route — the same gate built-in Settings/Users use
|
|
120
|
+
* (Session 91 closed the plugin bypass in Session 108). Default (unset)
|
|
121
|
+
* = visible to any authenticated user. */
|
|
122
|
+
adminOnly?: boolean;
|
|
123
|
+
}
|
|
124
|
+
export interface DashboardWidgetContribution {
|
|
125
|
+
id: string;
|
|
126
|
+
name: string;
|
|
127
|
+
view: ComponentType;
|
|
128
|
+
}
|
|
129
|
+
/** Mounted under /api/plugins/:pluginId/… so two plugins can never
|
|
130
|
+
* collide on a path (api-design.md) — the host dispatches to this exact
|
|
131
|
+
* (method, path) pair at request time (Month 8, Session 89: the first
|
|
132
|
+
* real server-side consumer). `handler` stays untyped at the SDK
|
|
133
|
+
* boundary (same cross-boundary erasure as view ComponentType fields)
|
|
134
|
+
* — the host casts to real Fastify types internally, since it always
|
|
135
|
+
* passes real FastifyRequest/FastifyReply instances. */
|
|
136
|
+
export interface ApiRouteContribution {
|
|
137
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
138
|
+
/** Relative to /api/plugins/:pluginId/ — must start with "/". */
|
|
139
|
+
path: string;
|
|
140
|
+
/** Who may call this route.
|
|
141
|
+
* - "admin" (the default when omitted) requires a JWT admin session —
|
|
142
|
+
* for admin-facing plugin UIs like Commerce Orders or the Forms
|
|
143
|
+
* builder.
|
|
144
|
+
* - "apiKey" requires a valid public API key — for credentialed
|
|
145
|
+
* headless callers like a storefront checkout.
|
|
146
|
+
* - "public" requires NO credentials — for genuinely anonymous
|
|
147
|
+
* browser traffic such as a contact-form submission. Session 108
|
|
148
|
+
* deliberately deferred this tier ("a webhook/public level can be
|
|
149
|
+
* added when a real consumer needs it"); Session 160's Forms
|
|
150
|
+
* plugin is that consumer. A public route owns its own abuse
|
|
151
|
+
* controls (honeypot, per-IP bucket, body caps) and only reaches
|
|
152
|
+
* browsers cross-origin when the operator allowlists their origin
|
|
153
|
+
* in VELORA_CORS_ORIGINS.
|
|
154
|
+
* Anything unrecognized falls back to "admin" — secure by default. */
|
|
155
|
+
access?: "admin" | "apiKey" | "public";
|
|
156
|
+
/** Optional request-side validation. The handler's response is NOT
|
|
157
|
+
* schema-validated — trusted, same as SimpleStorageAPI's handlers
|
|
158
|
+
* today. */
|
|
159
|
+
schema?: {
|
|
160
|
+
body?: ZodType;
|
|
161
|
+
querystring?: ZodType;
|
|
162
|
+
params?: ZodType;
|
|
163
|
+
};
|
|
164
|
+
/** Third argument added Month 8, Session 90 — the first server-side
|
|
165
|
+
* consumer needing privileged host access. Existing two-argument (or
|
|
166
|
+
* fewer) handlers remain perfectly valid: TypeScript allows a function
|
|
167
|
+
* with fewer declared parameters to satisfy a type expecting more. */
|
|
168
|
+
handler: (request: unknown, reply: unknown, context: PluginApiRouteContext) => unknown;
|
|
169
|
+
}
|
|
170
|
+
export interface ScheduledTaskContribution {
|
|
171
|
+
id: string;
|
|
172
|
+
name: string;
|
|
173
|
+
/** Standard 5-field cron expression. */
|
|
174
|
+
cron: string;
|
|
175
|
+
run: () => void | Promise<void>;
|
|
176
|
+
}
|
|
177
|
+
/** Media pipeline processor (thumbnails, optimization). The file/result
|
|
178
|
+
* contract firms up when the BullMQ media pipeline lands. */
|
|
179
|
+
export interface MediaProcessorContribution {
|
|
180
|
+
id: string;
|
|
181
|
+
name: string;
|
|
182
|
+
process: (file: unknown) => unknown | Promise<unknown>;
|
|
183
|
+
}
|
|
184
|
+
export interface SettingsPageContribution {
|
|
185
|
+
id: string;
|
|
186
|
+
name: string;
|
|
187
|
+
view: ComponentType;
|
|
188
|
+
}
|
|
189
|
+
export interface ContentTreeActionContribution {
|
|
190
|
+
id: string;
|
|
191
|
+
label: string;
|
|
192
|
+
onSelect: (nodeId: string) => void | Promise<void>;
|
|
193
|
+
}
|
|
194
|
+
/** A button in the rich-text editor's toolbar. TipTap wiring comes with
|
|
195
|
+
* the first consumer. */
|
|
196
|
+
export interface EditorToolbarContribution {
|
|
197
|
+
id: string;
|
|
198
|
+
label: string;
|
|
199
|
+
icon: string;
|
|
200
|
+
onClick: () => void | Promise<void>;
|
|
201
|
+
}
|
|
202
|
+
/** A service an integration plugin exposes for other plugins to consume
|
|
203
|
+
* (e.g. a Stripe client). Shape intentionally opaque until the
|
|
204
|
+
* cross-plugin service registry exists. */
|
|
205
|
+
export interface ExposedServiceContribution {
|
|
206
|
+
id: string;
|
|
207
|
+
name: string;
|
|
208
|
+
service: unknown;
|
|
209
|
+
}
|
|
210
|
+
/** A ready-made frontend starter shipped alongside a plugin (e.g. the
|
|
211
|
+
* Commerce plugin's "Online Store" Next.js starter) — one marketplace
|
|
212
|
+
* listing, working site immediately. Packaging lands Month 13. */
|
|
213
|
+
export interface BundledTemplateContribution {
|
|
214
|
+
name: string;
|
|
215
|
+
description?: string;
|
|
216
|
+
/** Where the starter's source lives until marketplace packaging exists. */
|
|
217
|
+
repository?: string;
|
|
218
|
+
}
|
|
219
|
+
export interface SerializerContext {
|
|
220
|
+
/** The public core item, post field-serialization. */
|
|
221
|
+
item: {
|
|
222
|
+
id: string;
|
|
223
|
+
documentTypeId: string;
|
|
224
|
+
locale: string;
|
|
225
|
+
publishedAt: string | null;
|
|
226
|
+
version: number;
|
|
227
|
+
};
|
|
228
|
+
/** SERIALIZED field data — contributions run after core serialization. */
|
|
229
|
+
data: Record<string, unknown>;
|
|
230
|
+
documentType: {
|
|
231
|
+
id: string;
|
|
232
|
+
name: string;
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
export interface CustomFormatContribution {
|
|
236
|
+
/** Plugin-scoped name, /^[a-z0-9-]+$/ — addressed as `<pluginId>:<name>`. */
|
|
237
|
+
name: string;
|
|
238
|
+
build: (context: SerializerContext) => unknown | Promise<unknown>;
|
|
239
|
+
}
|
|
240
|
+
export interface SerializerContribution {
|
|
241
|
+
extendContentResponse?: (context: SerializerContext) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
242
|
+
customFormats?: CustomFormatContribution[];
|
|
243
|
+
}
|
|
244
|
+
export interface DataTypePluginContributions {
|
|
245
|
+
/** The Month 3 datatype contract, referenced verbatim — a datatype
|
|
246
|
+
* plugin can ship several field types (id/version on each entry are the
|
|
247
|
+
* datatype's own, distinct from the plugin's). */
|
|
248
|
+
dataTypes?: DataTypePlugin[];
|
|
249
|
+
mediaProcessors?: MediaProcessorContribution[];
|
|
250
|
+
settingsPages?: SettingsPageContribution[];
|
|
251
|
+
}
|
|
252
|
+
export interface SectionPluginContributions {
|
|
253
|
+
dataTypes?: DataTypePlugin[];
|
|
254
|
+
adminSections?: AdminSectionContribution[];
|
|
255
|
+
dashboardWidgets?: DashboardWidgetContribution[];
|
|
256
|
+
apiRoutes?: ApiRouteContribution[];
|
|
257
|
+
hooks?: PluginHooks;
|
|
258
|
+
scheduledTasks?: ScheduledTaskContribution[];
|
|
259
|
+
settingsPages?: SettingsPageContribution[];
|
|
260
|
+
/** Document types the plugin provisions at install — the creation shape
|
|
261
|
+
* (server mints ids), same schema the admin's builder POSTs. */
|
|
262
|
+
documentTypes?: CreateDocumentTypeRequest[];
|
|
263
|
+
bundledTemplate?: BundledTemplateContribution;
|
|
264
|
+
}
|
|
265
|
+
export interface TemplatePluginContributions {
|
|
266
|
+
/** A template plugin IS a bundled template (per the matrix), so there
|
|
267
|
+
* is no separate bundledTemplate key — it contributes the document
|
|
268
|
+
* types its starter needs. */
|
|
269
|
+
documentTypes?: CreateDocumentTypeRequest[];
|
|
270
|
+
}
|
|
271
|
+
export interface ThemePluginContributions {
|
|
272
|
+
/** Tokens only, never components — the same ThemeTokenOverrides shape
|
|
273
|
+
* the built-in Light/Dark themes use (color scales + radius, enforced
|
|
274
|
+
* structurally by @velora-cms/design-tokens). */
|
|
275
|
+
themeTokens?: ThemeTokenOverrides;
|
|
276
|
+
}
|
|
277
|
+
export interface IntegrationPluginContributions {
|
|
278
|
+
hooks?: PluginHooks;
|
|
279
|
+
settingsPages?: SettingsPageContribution[];
|
|
280
|
+
exposedServices?: ExposedServiceContribution[];
|
|
281
|
+
serializers?: SerializerContribution;
|
|
282
|
+
}
|
|
283
|
+
export interface UtilityPluginContributions {
|
|
284
|
+
dashboardWidgets?: DashboardWidgetContribution[];
|
|
285
|
+
hooks?: PluginHooks;
|
|
286
|
+
scheduledTasks?: ScheduledTaskContribution[];
|
|
287
|
+
mediaProcessors?: MediaProcessorContribution[];
|
|
288
|
+
settingsPages?: SettingsPageContribution[];
|
|
289
|
+
contentTreeActions?: ContentTreeActionContribution[];
|
|
290
|
+
editorToolbar?: EditorToolbarContribution[];
|
|
291
|
+
serializers?: SerializerContribution;
|
|
292
|
+
}
|
|
293
|
+
export interface BundlePluginContributions {
|
|
294
|
+
/** Plugin ids installed together — resolved by the marketplace. */
|
|
295
|
+
bundledPlugins?: string[];
|
|
296
|
+
bundledTemplate?: BundledTemplateContribution;
|
|
297
|
+
}
|
|
298
|
+
/** Fields shared by every plugin type. */
|
|
299
|
+
export interface PluginManifestBase {
|
|
300
|
+
/** Reverse domain, e.g. 'com.yourname.googlemaps'. */
|
|
301
|
+
id: string;
|
|
302
|
+
name: string;
|
|
303
|
+
/** The plugin's own semver. */
|
|
304
|
+
version: string;
|
|
305
|
+
type: PluginType;
|
|
306
|
+
description: string;
|
|
307
|
+
author: string;
|
|
308
|
+
license: string;
|
|
309
|
+
/** Marketplace price; 0 or absent = free. */
|
|
310
|
+
price?: number;
|
|
311
|
+
/**
|
|
312
|
+
* Marketplace verification hash. Optional BY DESIGN, not oversight:
|
|
313
|
+
* per the Trust model, sideloaded/local dev plugins are unsigned by
|
|
314
|
+
* definition (Tier 2 'unverified') — only marketplace-signed plugins
|
|
315
|
+
* carry one. Signing is authenticity, not a sandbox; the safety layer
|
|
316
|
+
* is identical for both tiers.
|
|
317
|
+
*/
|
|
318
|
+
signature?: string;
|
|
319
|
+
/** Semver range of compatible CMS versions, e.g. '^1.0.0'. */
|
|
320
|
+
cmsVersion: string;
|
|
321
|
+
/** Ids of plugins this one requires. */
|
|
322
|
+
pluginDependencies?: string[];
|
|
323
|
+
databaseCompatibility: DatabaseCompatibility;
|
|
324
|
+
permissions: PluginPermissions;
|
|
325
|
+
/** DB changes applied on install (through plugin storage — never raw
|
|
326
|
+
* SQL). */
|
|
327
|
+
migrations?: PluginMigration[];
|
|
328
|
+
onInstall?: () => void | Promise<void>;
|
|
329
|
+
onUninstall?: () => void | Promise<void>;
|
|
330
|
+
onActivate?: () => void | Promise<void>;
|
|
331
|
+
onDeactivate?: () => void | Promise<void>;
|
|
332
|
+
onUpdate?: (previousVersion: string) => void | Promise<void>;
|
|
333
|
+
migrationHooks?: PluginMigrationHooks;
|
|
334
|
+
}
|
|
335
|
+
export interface DataTypePluginManifest extends PluginManifestBase {
|
|
336
|
+
type: "datatype";
|
|
337
|
+
contributions: DataTypePluginContributions;
|
|
338
|
+
}
|
|
339
|
+
export interface SectionPluginManifest extends PluginManifestBase {
|
|
340
|
+
type: "section";
|
|
341
|
+
contributions: SectionPluginContributions;
|
|
342
|
+
}
|
|
343
|
+
export interface TemplatePluginManifest extends PluginManifestBase {
|
|
344
|
+
type: "template";
|
|
345
|
+
contributions: TemplatePluginContributions;
|
|
346
|
+
}
|
|
347
|
+
export interface ThemePluginManifest extends PluginManifestBase {
|
|
348
|
+
type: "theme";
|
|
349
|
+
contributions: ThemePluginContributions;
|
|
350
|
+
}
|
|
351
|
+
export interface IntegrationPluginManifest extends PluginManifestBase {
|
|
352
|
+
type: "integration";
|
|
353
|
+
contributions: IntegrationPluginContributions;
|
|
354
|
+
}
|
|
355
|
+
export interface UtilityPluginManifest extends PluginManifestBase {
|
|
356
|
+
type: "utility";
|
|
357
|
+
contributions: UtilityPluginContributions;
|
|
358
|
+
}
|
|
359
|
+
export interface BundlePluginManifest extends PluginManifestBase {
|
|
360
|
+
type: "bundle";
|
|
361
|
+
contributions: BundlePluginContributions;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* The full plugin manifest — a discriminated union on `type`, so the
|
|
365
|
+
* contribution matrix is enforced by the compiler for TypeScript authors
|
|
366
|
+
* (a 'datatype' plugin declaring adminSections is a type error, not a
|
|
367
|
+
* runtime surprise). Built-ins and Module Federation remotes both
|
|
368
|
+
* register through this same contract.
|
|
369
|
+
*/
|
|
370
|
+
export type CMSPlugin = DataTypePluginManifest | SectionPluginManifest | TemplatePluginManifest | ThemePluginManifest | IntegrationPluginManifest | UtilityPluginManifest | BundlePluginManifest;
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** The types whose contribution-matrix rows can carry browser-rendered
|
|
2
|
+
* contributions (views, widgets, settings pages, toolbar/tree items) —
|
|
3
|
+
* exactly these ship a Module Federation client bundle
|
|
4
|
+
* (remoteEntry.js). theme/template/bundle are data-only: their packages
|
|
5
|
+
* carry manifest.json + server/plugin.js (the manifest carrier) and
|
|
6
|
+
* nothing client-side. Single source of truth for velora-plugin's
|
|
7
|
+
* build/validate, apps/server's install path, and (as a documented
|
|
8
|
+
* static mirror) the marketplace's submission checks. */
|
|
9
|
+
export const CLIENT_BUNDLE_TYPES = ["datatype", "section", "integration", "utility"];
|
|
10
|
+
export function hasClientBundle(type) {
|
|
11
|
+
return CLIENT_BUNDLE_TYPES.includes(type);
|
|
12
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
interface PluginValueProviderProps {
|
|
3
|
+
value: unknown;
|
|
4
|
+
onChange: (value: unknown) => void;
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
export declare function PluginValueProvider({ value, onChange, children }: PluginValueProviderProps): import("react").JSX.Element;
|
|
8
|
+
export declare function usePluginValue<T = unknown>(): {
|
|
9
|
+
value: T;
|
|
10
|
+
onChange: (value: T) => void;
|
|
11
|
+
};
|
|
12
|
+
interface PluginSettingsProviderProps {
|
|
13
|
+
settings: Record<string, unknown>;
|
|
14
|
+
onSettingsChange?: (settings: Record<string, unknown>) => void;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
export declare function PluginSettingsProvider({ settings, onSettingsChange, children }: PluginSettingsProviderProps): import("react").JSX.Element;
|
|
18
|
+
export declare function usePluginSettings<T extends Record<string, unknown> = Record<string, unknown>>(): T;
|
|
19
|
+
export declare function usePluginSettingsValue<T extends Record<string, unknown> = Record<string, unknown>>(): {
|
|
20
|
+
value: T;
|
|
21
|
+
onChange: (value: T) => void;
|
|
22
|
+
};
|
|
23
|
+
export type PluginFetch = (input: string, init?: RequestInit) => Promise<Response>;
|
|
24
|
+
interface PluginApiProviderProps {
|
|
25
|
+
fetch: PluginFetch;
|
|
26
|
+
children: ReactNode;
|
|
27
|
+
}
|
|
28
|
+
export declare function PluginApiProvider({ fetch, children }: PluginApiProviderProps): import("react").JSX.Element;
|
|
29
|
+
export declare function usePluginApi(): PluginFetch;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
const PluginValueContext = createContext(null);
|
|
4
|
+
export function PluginValueProvider({ value, onChange, children }) {
|
|
5
|
+
return _jsx(PluginValueContext.Provider, { value: { value, onChange }, children: children });
|
|
6
|
+
}
|
|
7
|
+
// A view calling this outside a host-rendered PluginValueProvider is a
|
|
8
|
+
// real bug (the host forgot to wrap it) — throw loudly, don't hand back
|
|
9
|
+
// a silently-undefined value/no-op onChange.
|
|
10
|
+
export function usePluginValue() {
|
|
11
|
+
const ctx = useContext(PluginValueContext);
|
|
12
|
+
if (!ctx) {
|
|
13
|
+
throw new Error("usePluginValue must be called from a view rendered inside a PluginValueProvider");
|
|
14
|
+
}
|
|
15
|
+
return ctx;
|
|
16
|
+
}
|
|
17
|
+
const PluginSettingsContext = createContext(null);
|
|
18
|
+
export function PluginSettingsProvider({ settings, onSettingsChange, children }) {
|
|
19
|
+
return (_jsx(PluginSettingsContext.Provider, { value: { settings, onSettingsChange }, children: children }));
|
|
20
|
+
}
|
|
21
|
+
// Returns settings directly (not {settings, onChange}) — this is the
|
|
22
|
+
// READ-ONLY path used by input/readOnly/preview views (Session 69). For a
|
|
23
|
+
// settings-EDITING view (the "settings" DataTypePluginViews slot), use
|
|
24
|
+
// usePluginSettingsValue() below instead.
|
|
25
|
+
export function usePluginSettings() {
|
|
26
|
+
const ctx = useContext(PluginSettingsContext);
|
|
27
|
+
if (!ctx) {
|
|
28
|
+
throw new Error("usePluginSettings must be called from a view rendered inside a PluginSettingsProvider");
|
|
29
|
+
}
|
|
30
|
+
return ctx.settings;
|
|
31
|
+
}
|
|
32
|
+
// Mirrors usePluginValue()'s {value, onChange} shape, for settings-EDITING
|
|
33
|
+
// views only (Session 69) — the host (e.g. the Data Types editor) must
|
|
34
|
+
// supply onSettingsChange via PluginSettingsProvider, or this throws:
|
|
35
|
+
// a settings view rendered in a read-only context is a host bug, same
|
|
36
|
+
// "throw loudly" philosophy as usePluginValue().
|
|
37
|
+
export function usePluginSettingsValue() {
|
|
38
|
+
const ctx = useContext(PluginSettingsContext);
|
|
39
|
+
if (!ctx) {
|
|
40
|
+
throw new Error("usePluginSettingsValue must be called from a view rendered inside a PluginSettingsProvider");
|
|
41
|
+
}
|
|
42
|
+
if (!ctx.onSettingsChange) {
|
|
43
|
+
throw new Error("usePluginSettingsValue must be called from a settings view rendered inside a PluginSettingsProvider " +
|
|
44
|
+
"that supplies onSettingsChange (read-only contexts should use usePluginSettings() instead)");
|
|
45
|
+
}
|
|
46
|
+
return { value: ctx.settings, onChange: ctx.onSettingsChange };
|
|
47
|
+
}
|
|
48
|
+
const PluginApiContext = createContext(null);
|
|
49
|
+
export function PluginApiProvider({ fetch, children }) {
|
|
50
|
+
return _jsx(PluginApiContext.Provider, { value: fetch, children: children });
|
|
51
|
+
}
|
|
52
|
+
export function usePluginApi() {
|
|
53
|
+
const ctx = useContext(PluginApiContext);
|
|
54
|
+
if (!ctx) {
|
|
55
|
+
throw new Error("usePluginApi must be called from a view rendered inside a PluginApiProvider");
|
|
56
|
+
}
|
|
57
|
+
return ctx;
|
|
58
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DataTypePlugin } from "./data-type-plugin.js";
|
|
2
|
+
export declare function registerDataType(plugin: DataTypePlugin): void;
|
|
3
|
+
export declare function getDataType(id: string): DataTypePlugin | undefined;
|
|
4
|
+
export declare function listDataTypes(): DataTypePlugin[];
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Static registry: built-ins register into this exact same map at module
|
|
2
|
+
// load (Core vs Plugin — built-ins are statically registered, dynamically
|
|
3
|
+
// loaded marketplace plugins are a second registration path into this same
|
|
4
|
+
// registry, not a different kind of plugin).
|
|
5
|
+
const dataTypes = new Map();
|
|
6
|
+
export function registerDataType(plugin) {
|
|
7
|
+
if (dataTypes.has(plugin.id)) {
|
|
8
|
+
throw new Error(`A datatype with id "${plugin.id}" is already registered`);
|
|
9
|
+
}
|
|
10
|
+
dataTypes.set(plugin.id, plugin);
|
|
11
|
+
}
|
|
12
|
+
export function getDataType(id) {
|
|
13
|
+
return dataTypes.get(id);
|
|
14
|
+
}
|
|
15
|
+
export function listDataTypes() {
|
|
16
|
+
return [...dataTypes.values()];
|
|
17
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
interface SimpleStorageValueProviderProps {
|
|
3
|
+
contentId: string;
|
|
4
|
+
fieldId: string;
|
|
5
|
+
pluginId: string;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
export declare function SimpleStorageValueProvider({ contentId, fieldId, pluginId, children, }: SimpleStorageValueProviderProps): import("react").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { PluginValueProvider, usePluginApi } from "./plugin-context.js";
|
|
4
|
+
// The only SDK code path that ever hits /api/plugins/:pluginId/storage/...
|
|
5
|
+
// (SimpleStorageAPI, Session 68) — a plugin's view code (and usePluginValue()
|
|
6
|
+
// itself) never touches the storage API directly, matching plugin-sdk.md's
|
|
7
|
+
// "SDK handles all host communication" rule. Fetches the current value on
|
|
8
|
+
// mount and feeds it into the SAME PluginValueContext usePluginValue()
|
|
9
|
+
// already reads, so a view wrapped in this provider is indistinguishable
|
|
10
|
+
// from one wrapped in a plain PluginValueProvider — swap this in wherever a
|
|
11
|
+
// field's value should live in content_field_values instead of the local
|
|
12
|
+
// content blob (see document-type-form.tsx for the blob-backed default).
|
|
13
|
+
//
|
|
14
|
+
// Shape mirrors views/image-view.tsx's useMediaPreview(): usePluginApi(),
|
|
15
|
+
// a cancelled-guarded effect, local value/error state. A GET returning
|
|
16
|
+
// {value: null} is the normal "never set" state, not an error.
|
|
17
|
+
export function SimpleStorageValueProvider({ contentId, fieldId, pluginId, children, }) {
|
|
18
|
+
const fetchApi = usePluginApi();
|
|
19
|
+
const [value, setValue] = useState(null);
|
|
20
|
+
const [error, setError] = useState(null);
|
|
21
|
+
const path = `/api/plugins/${pluginId}/storage/${contentId}/${fieldId}`;
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
let cancelled = false;
|
|
24
|
+
setError(null);
|
|
25
|
+
fetchApi(path)
|
|
26
|
+
.then(async (response) => {
|
|
27
|
+
if (!response.ok)
|
|
28
|
+
throw new Error(`Failed to load plugin value (${response.status})`);
|
|
29
|
+
const body = (await response.json());
|
|
30
|
+
if (!cancelled)
|
|
31
|
+
setValue(body.value);
|
|
32
|
+
})
|
|
33
|
+
.catch((err) => {
|
|
34
|
+
if (!cancelled)
|
|
35
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
36
|
+
});
|
|
37
|
+
return () => {
|
|
38
|
+
cancelled = true;
|
|
39
|
+
};
|
|
40
|
+
}, [fetchApi, path]);
|
|
41
|
+
function onChange(next) {
|
|
42
|
+
setValue(next); // optimistic — the view should feel instant
|
|
43
|
+
fetchApi(path, {
|
|
44
|
+
method: "PUT",
|
|
45
|
+
headers: { "Content-Type": "application/json" },
|
|
46
|
+
body: JSON.stringify({ value: next }),
|
|
47
|
+
})
|
|
48
|
+
.then((response) => {
|
|
49
|
+
if (!response.ok)
|
|
50
|
+
throw new Error(`Failed to save plugin value (${response.status})`);
|
|
51
|
+
setError(null);
|
|
52
|
+
})
|
|
53
|
+
.catch((err) => {
|
|
54
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return (_jsxs(_Fragment, { children: [error && (_jsx("p", { role: "alert", className: "text-xs text-danger-600", children: error })), _jsx(PluginValueProvider, { value: value, onChange: onChange, children: children })] }));
|
|
58
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type PluginColumnType = "text" | "integer" | "boolean" | "json" | "timestamp";
|
|
2
|
+
export interface PluginColumnSchema {
|
|
3
|
+
name: string;
|
|
4
|
+
type: PluginColumnType;
|
|
5
|
+
primaryKey?: boolean;
|
|
6
|
+
notNull?: boolean;
|
|
7
|
+
unique?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface PluginIndexSchema {
|
|
10
|
+
columns: string[];
|
|
11
|
+
unique?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface PluginTableSchema {
|
|
14
|
+
tableName: string;
|
|
15
|
+
columns: PluginColumnSchema[];
|
|
16
|
+
indexes?: PluginIndexSchema[];
|
|
17
|
+
}
|
|
18
|
+
export interface StructuredQueryBuilder<T = Record<string, unknown>> extends PromiseLike<T[]> {
|
|
19
|
+
where(conditions: Record<string, unknown>): this;
|
|
20
|
+
orderBy(column: string, direction?: "asc" | "desc"): this;
|
|
21
|
+
limit(n: number): this;
|
|
22
|
+
paginate(options: {
|
|
23
|
+
cursor?: string;
|
|
24
|
+
limit: number;
|
|
25
|
+
}): this;
|
|
26
|
+
}
|
|
27
|
+
export interface StructuredStorageTransaction {
|
|
28
|
+
insert(tableName: string, data: Record<string, unknown>): Promise<void>;
|
|
29
|
+
update(tableName: string, where: Record<string, unknown>, data: Record<string, unknown>): Promise<number>;
|
|
30
|
+
delete(tableName: string, where: Record<string, unknown>): Promise<number>;
|
|
31
|
+
query<T = Record<string, unknown>>(tableName: string): StructuredQueryBuilder<T>;
|
|
32
|
+
}
|
|
33
|
+
export interface ScopedStructuredStorage {
|
|
34
|
+
defineTable(schema: PluginTableSchema): Promise<void>;
|
|
35
|
+
insert(tableName: string, data: Record<string, unknown>): Promise<void>;
|
|
36
|
+
update(tableName: string, where: Record<string, unknown>, data: Record<string, unknown>): Promise<number>;
|
|
37
|
+
delete(tableName: string, where: Record<string, unknown>): Promise<number>;
|
|
38
|
+
query<T = Record<string, unknown>>(tableName: string): StructuredQueryBuilder<T>;
|
|
39
|
+
transaction(fn: (tx: StructuredStorageTransaction) => Promise<void>): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
export interface PublishedContent {
|
|
42
|
+
id: string;
|
|
43
|
+
documentTypeId: string;
|
|
44
|
+
data: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
export interface PluginApiRouteContext {
|
|
47
|
+
structuredStorage: ScopedStructuredStorage;
|
|
48
|
+
/** Null if the content id doesn't exist OR isn't published (a
|
|
49
|
+
* draft-only save never satisfies this) — deliberately conflates
|
|
50
|
+
* "missing" and "not yet purchasable," since callers treat both the
|
|
51
|
+
* same way. */
|
|
52
|
+
getPublishedContent(contentId: string): Promise<PublishedContent | null>;
|
|
53
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Local, SDK-owned duplicates of packages/db-adapter's structured-storage
|
|
2
|
+
// shapes and apps/server's StructuredStorageAPI/StructuredQueryBuilder/
|
|
3
|
+
// StructuredStorageTransaction classes — never imported directly, since
|
|
4
|
+
// @velora-cms/plugin-sdk is a published package that must not depend on
|
|
5
|
+
// unpublished adapter/server internals (same reasoning manifest.ts's
|
|
6
|
+
// DatabaseDialect duplication already uses). TypeScript's structural
|
|
7
|
+
// typing means the real apps/server classes satisfy these interfaces
|
|
8
|
+
// without any cast, as long as their member shapes match (they do —
|
|
9
|
+
// this is exactly the public contract apps/docs/src/content/docs/v1/plugin-sdk/storage.md
|
|
10
|
+
// already documents).
|
|
11
|
+
export {};
|