@andesine/extensions 0.0.0-stage → 0.6.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/CHANGELOG.md ADDED
@@ -0,0 +1,65 @@
1
+ # Changelog
2
+
3
+ ## 0.6.0
4
+
5
+ - `Button` and `IconButton` take one `variant`: `primary`, `secondary`, `ghost`, `link`, or
6
+ `danger` (`color` is removed). Buttons default to `secondary` and icon buttons to `ghost`.
7
+ `Input` renders with the outlined look of Andesine's own fields.
8
+ - Remove `settingsSections` from the manifest: the host-rendered configuration form
9
+ covers settings, and custom extension UI belongs in panels.
10
+ - Add `description` and `icon` to element views; Andesine shows them in the editor's
11
+ slash menu and in the extension's settings.
12
+ - Block action views decide their UI: a view that renders nothing runs without UI
13
+ (the block menu shows a spinner until it closes), and one root `Dialog` or
14
+ `Menu` opens a modal or a menu where the block menu was.
15
+ - Add `Dialog` and `Menu` for any view. `Menu` is a dropdown menu with
16
+ `MenuTrigger` (or a right-click area with `contextMenu`), `MenuItem` (with
17
+ submenus and a `loading` spinner), `MenuGroup`, `MenuSeparator`, and custom
18
+ `MenuContent` items. Outside block actions, a dialog needs a recent interaction.
19
+ - Add `notify(text, type?)`, a host action that shows a short notification.
20
+ - Block action edits accept elements as `{ type: "element", attrs: { name, props } }`;
21
+ Andesine derives their tag.
22
+ - Fix block action and element view edits built from view props: `editor.replace`,
23
+ `editor.insertAfter`, and `editor.setElementProps` now send plain JSON instead of
24
+ store proxies, which could not be sent and stopped the extension.
25
+
26
+ ## 0.5.0
27
+
28
+ - First release, versioned with `@andesine/sdk` and the `andesine` CLI.
29
+ - Add the root entry for `andesine.config.ts`: `defineExtension` (the manifest; a Zod
30
+ `configuration` becomes its JSON Schema subset) and `defineBuild` (the frontend module
31
+ and custom icon sets for `andesine extensions build`).
32
+ - Add `@andesine/extensions/solid`: the worker runtime for extension frontends
33
+ (`startExtension`), a `solid-js/universal` renderer that sends serialized view
34
+ patches to Andesine, and the host components: layout (`Stack`, `Grid`, `Box`,
35
+ `Divider`, `ScrollArea`), text (`Text`, `Heading`, `Code`, `Link`, `Badge`,
36
+ `Icon`), controls (`Button`, `IconButton`, `Input`, `Textarea`, `Select`,
37
+ `Combobox`, `Checkbox`, `Toggle`, `ToggleGroup`, `ColorInput`, `Tooltip`),
38
+ settings (`Setting`, `List`), and data display (`Card`, `Tree`, `Tabs`, `Tab`,
39
+ `Disclosure`, `Callout`, `Steps`, `Step`, `Figure`, `CodeBlock`, `TimeAgo`,
40
+ `Spinner`, `Skeleton`).
41
+ - Layout and text components (`Stack`, `Grid`, `Box`, `Text`, `Heading`) accept
42
+ `class` with utility classes of the Andesine UnoCSS preset. Icons are icon
43
+ classes (`i-lucide:rocket`; `?bg` for colored icons) from the default Lucide
44
+ and Tabler sets, installed Iconify sets, or custom sets.
45
+ - Add `ContentSlot` for element views: it shows the element's editable content,
46
+ which Andesine owns. Element views receive `{ element, props }` with the
47
+ element name and its props.
48
+ - Add host actions: `copyText`, `downloadFile`, and `openURL`. They run shortly
49
+ after a user interaction in the extension's view and reject with
50
+ `ExtensionRequestError` (`code`) otherwise.
51
+ - Add `useEditor()` for views: block action views can `replace` the selected
52
+ blocks, `insertAfter` them, and `close`; element views can `setElementProps`.
53
+ Andesine validates each edit and applies it as the member's edit.
54
+ - Add `usePermissions()` and `hasPermission()` for the member's effective
55
+ permissions, `apiFetch` and `EXTENSION_API_URL` to use the Andesine SDK from
56
+ the frontend (requests go through Andesine with the member's session), and
57
+ `backend.fetch` for the extension's own backend with a session token.
58
+ - Add `useConfiguration()` for the configuration's non-secret values, with
59
+ defaults for unset fields. It updates when a manager saves the configuration.
60
+ - Add `storage` for the extension's JSON key-value storage, which its backend
61
+ shares: `get`, `set`, `delete`, and `list` (in key order, with an optional
62
+ prefix).
63
+ - `backend.fetch` also sends the `Andesine-Extension` and `Andesine-Instance`
64
+ headers, which `verifySession` from `@andesine/sdk/extensions` needs. The
65
+ backend's CORS configuration must allow them with `Authorization`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vrite, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,4 +1,105 @@
1
- # Temporary Holding Version
1
+ # @andesine/extensions
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.
4
- If no other versions are published within 30 days, this package and version will be deleted.
3
+ Authoring tools for Andesine extensions. Extension frontends are Solid components
4
+ that run in a sandboxed worker. Andesine renders its own components from the
5
+ serialized view tree; extension code has no DOM access.
6
+
7
+ ## Solid entry
8
+
9
+ Compile extension JSX for the universal renderer:
10
+
11
+ ```js
12
+ // babel-preset-solid options
13
+ { generate: "universal", moduleName: "@andesine/extensions/solid" }
14
+ ```
15
+
16
+ Bundle for the `browser` export condition (Solid's `worker` condition selects its
17
+ non-reactive server build). Then register the views that the manifest names:
18
+
19
+ ```tsx
20
+ import { createSignal } from "solid-js";
21
+ import { Button, Stack, Text, startExtension } from "@andesine/extensions/solid";
22
+
23
+ const Counter = () => {
24
+ const [count, setCount] = createSignal(0);
25
+
26
+ return (
27
+ <Stack direction="row" gap="small">
28
+ <Text>{count()}</Text>
29
+ <Button onClick={() => setCount(count() + 1)}>Add</Button>
30
+ </Stack>
31
+ );
32
+ };
33
+
34
+ startExtension({ Counter });
35
+ ```
36
+
37
+ Only the exported host components render. Props must be JSON values; functions
38
+ become callbacks that the host calls with plain values.
39
+
40
+ ## Element views
41
+
42
+ An element view renders a document element (for example `<Accordion>`) and
43
+ receives `{ element, props }`. `ContentSlot` shows the element's editable
44
+ content; Andesine keeps that content when the slot is hidden. A view's CSS does
45
+ not apply inside its slot.
46
+
47
+ ## Host actions
48
+
49
+ `copyText(text)`, `downloadFile(name, text, type?)`, and `openURL(url)` ask
50
+ Andesine to act for the extension. Call them from an interaction, for example a
51
+ button's `onClick`; otherwise they reject with `ExtensionRequestError` and the
52
+ code `user_interaction_required`. `openURL` asks the member to confirm URLs
53
+ outside the manifest's declared URLs. `notify(text, type?)` shows a short
54
+ notification with the extension's name and needs no interaction.
55
+
56
+ ## Block actions and edits
57
+
58
+ A block action view receives `{ blocks }`: the selected blocks as editor JSON.
59
+ `useEditor()` returns `replace(content)`, `insertAfter(content)`, and `close()`
60
+ for block action views, and `setElementProps(props)` for element views (call it
61
+ from an interaction). Content must be blocks that are allowed where the
62
+ selection is; edits fail when the selected blocks changed meanwhile. Elements
63
+ are `{ type: "element", attrs: { name, props }, content }`; Andesine derives
64
+ their tag.
65
+
66
+ The view decides its UI at its root. A view that renders nothing runs without
67
+ UI: the block menu shows a spinner until the view calls `close()`, and Andesine
68
+ stops it after 30 seconds. One root `<Dialog title>` opens a modal, and one root
69
+ `<Menu>` opens a menu where the block menu was; anything else at the root stops
70
+ the action with an error. Escape, an outside click, or the close button close
71
+ them like `close()`; after a menu choice, the action runs on until it closes.
72
+ Choosing the action counts as an interaction, so host actions work right away.
73
+
74
+ ## Dialogs and menus
75
+
76
+ `Dialog` and `Menu` work in any view. A dialog is shown while it is rendered and
77
+ calls `onClose` when dismissed; outside block actions, render it right after an
78
+ interaction, e.g. from `onClick`, otherwise it is not shown. A `Menu` is a
79
+ dropdown menu: `MenuTrigger` holds its trigger (with `contextMenu`, the area
80
+ that opens it on a right click), and its items are `MenuItem` (nested items
81
+ make a submenu; `loading` shows a spinner), `MenuGroup`, `MenuSeparator`, and
82
+ custom `MenuContent` with any content. `opened` and `onOpenChange` control it.
83
+
84
+ ## API, permissions, and backend
85
+
86
+ ```ts
87
+ import { createClient } from "@andesine/sdk";
88
+ import { EXTENSION_API_URL, apiFetch, backend, hasPermission } from "@andesine/extensions/solid";
89
+
90
+ const client = createClient({ baseURL: EXTENSION_API_URL, fetch: apiFetch });
91
+ const entries = hasPermission("read:entries") ? await client.entries.list({}) : [];
92
+ const response = await backend.fetch("/publish", { method: "POST" });
93
+ ```
94
+
95
+ API requests use the member's session and are limited to the member's
96
+ permissions that the extension was granted (`usePermissions()`); other
97
+ operations answer with HTTP 403. `backend.fetch` calls paths of the manifest's
98
+ backend URL with a short-lived session token in `Authorization: Bearer`, which
99
+ the backend verifies with Andesine.
100
+
101
+ ## Examples
102
+
103
+ `npx andesine extensions init` creates a project with samples of the main features: element
104
+ views, a block action, a panel that calls its backend, settings, and webhooks. Published
105
+ extensions in the public registry show complete projects.
@@ -0,0 +1,289 @@
1
+ import * as z from "zod";
2
+ //#region ../../private/contracts/src/extensions/configuration.d.ts
3
+ type ExtensionConfiguration = z.output<typeof extensionConfigurationType>;
4
+ /** Flat JSON Schema 2020-12 subset; the host validates it without running extension code. */
5
+ declare const extensionConfigurationType: z.ZodObject<{
6
+ $schema: z.ZodOptional<z.ZodLiteral<"https://json-schema.org/draft/2020-12/schema">>;
7
+ type: z.ZodLiteral<"object">;
8
+ properties: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
9
+ type: z.ZodLiteral<"string">;
10
+ minLength: z.ZodOptional<z.ZodInt>;
11
+ maxLength: z.ZodOptional<z.ZodInt>;
12
+ pattern: z.ZodOptional<z.ZodString>;
13
+ format: z.ZodOptional<z.ZodEnum<{
14
+ uri: "uri";
15
+ email: "email";
16
+ }>>;
17
+ default: z.ZodOptional<z.ZodString>;
18
+ secret: z.ZodOptional<z.ZodLiteral<true>>;
19
+ control: z.ZodOptional<z.ZodLiteral<"textarea">>;
20
+ title: z.ZodOptional<z.ZodString>;
21
+ description: z.ZodOptional<z.ZodString>;
22
+ }, z.core.$strict>, z.ZodObject<{
23
+ type: z.ZodLiteral<"string">;
24
+ enum: z.ZodArray<z.ZodString>;
25
+ default: z.ZodOptional<z.ZodString>;
26
+ title: z.ZodOptional<z.ZodString>;
27
+ description: z.ZodOptional<z.ZodString>;
28
+ }, z.core.$strict>, z.ZodObject<{
29
+ type: z.ZodEnum<{
30
+ number: "number";
31
+ integer: "integer";
32
+ }>;
33
+ minimum: z.ZodOptional<z.ZodNumber>;
34
+ maximum: z.ZodOptional<z.ZodNumber>;
35
+ exclusiveMinimum: z.ZodOptional<z.ZodNumber>;
36
+ exclusiveMaximum: z.ZodOptional<z.ZodNumber>;
37
+ multipleOf: z.ZodOptional<z.ZodNumber>;
38
+ default: z.ZodOptional<z.ZodNumber>;
39
+ title: z.ZodOptional<z.ZodString>;
40
+ description: z.ZodOptional<z.ZodString>;
41
+ }, z.core.$strict>, z.ZodObject<{
42
+ type: z.ZodLiteral<"boolean">;
43
+ default: z.ZodOptional<z.ZodBoolean>;
44
+ title: z.ZodOptional<z.ZodString>;
45
+ description: z.ZodOptional<z.ZodString>;
46
+ }, z.core.$strict>, z.ZodObject<{
47
+ type: z.ZodLiteral<"array">;
48
+ items: z.ZodUnion<readonly [z.ZodObject<{
49
+ type: z.ZodLiteral<"string">;
50
+ minLength: z.ZodOptional<z.ZodInt>;
51
+ maxLength: z.ZodOptional<z.ZodInt>;
52
+ pattern: z.ZodOptional<z.ZodString>;
53
+ format: z.ZodOptional<z.ZodEnum<{
54
+ uri: "uri";
55
+ email: "email";
56
+ }>>;
57
+ }, z.core.$strict>, z.ZodObject<{
58
+ type: z.ZodLiteral<"string">;
59
+ enum: z.ZodArray<z.ZodString>;
60
+ }, z.core.$strict>, z.ZodObject<{
61
+ type: z.ZodEnum<{
62
+ number: "number";
63
+ integer: "integer";
64
+ }>;
65
+ minimum: z.ZodOptional<z.ZodNumber>;
66
+ maximum: z.ZodOptional<z.ZodNumber>;
67
+ exclusiveMinimum: z.ZodOptional<z.ZodNumber>;
68
+ exclusiveMaximum: z.ZodOptional<z.ZodNumber>;
69
+ multipleOf: z.ZodOptional<z.ZodNumber>;
70
+ }, z.core.$strict>, z.ZodObject<{
71
+ type: z.ZodLiteral<"boolean">;
72
+ }, z.core.$strict>]>;
73
+ minItems: z.ZodOptional<z.ZodInt>;
74
+ maxItems: z.ZodOptional<z.ZodInt>;
75
+ uniqueItems: z.ZodOptional<z.ZodBoolean>;
76
+ default: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
77
+ title: z.ZodOptional<z.ZodString>;
78
+ description: z.ZodOptional<z.ZodString>;
79
+ }, z.core.$strict>]>>;
80
+ required: z.ZodOptional<z.ZodArray<z.ZodString>>;
81
+ additionalProperties: z.ZodLiteral<false>;
82
+ }, z.core.$strict>;
83
+ //#endregion
84
+ //#region ../../private/contracts/src/extensions/manifest.d.ts
85
+ declare const extensionManifestType: z.ZodObject<{
86
+ elementViews: z.ZodDefault<z.ZodArray<z.ZodObject<{
87
+ id: z.ZodString;
88
+ name: z.ZodString;
89
+ description: z.ZodOptional<z.ZodString>;
90
+ icon: z.ZodOptional<z.ZodString>;
91
+ element: z.ZodString;
92
+ entry: z.ZodString;
93
+ descendants: z.ZodDefault<z.ZodArray<z.ZodObject<{
94
+ element: z.ZodString;
95
+ entry: z.ZodString;
96
+ }, z.core.$strict>>>;
97
+ }, z.core.$strict>>>;
98
+ blockActions: z.ZodDefault<z.ZodArray<z.ZodObject<{
99
+ id: z.ZodString;
100
+ label: z.ZodString;
101
+ icon: z.ZodOptional<z.ZodString>;
102
+ blocks: z.ZodArray<z.ZodString>;
103
+ entry: z.ZodString;
104
+ }, z.core.$strict>>>;
105
+ panels: z.ZodDefault<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
106
+ side: z.ZodLiteral<"left">;
107
+ entry: z.ZodString;
108
+ id: z.ZodString;
109
+ name: z.ZodString;
110
+ icon: z.ZodString;
111
+ }, z.core.$strict>, z.ZodObject<{
112
+ side: z.ZodLiteral<"right">;
113
+ context: z.ZodEnum<{
114
+ entry: "entry";
115
+ workspace: "workspace";
116
+ collection: "collection";
117
+ }>;
118
+ entry: z.ZodString;
119
+ id: z.ZodString;
120
+ name: z.ZodString;
121
+ icon: z.ZodString;
122
+ }, z.core.$strict>], "side">>>;
123
+ name: z.ZodString;
124
+ version: z.ZodString;
125
+ displayName: z.ZodString;
126
+ description: z.ZodString;
127
+ icon: z.ZodOptional<z.ZodString>;
128
+ apiVersion: z.ZodLiteral<1>;
129
+ permissions: z.ZodDefault<z.ZodArray<z.ZodEnum<{
130
+ "read:restricted_collections": "read:restricted_collections";
131
+ entries: "entries";
132
+ "read:entries": "read:entries";
133
+ versions: "versions";
134
+ "read:versions": "read:versions";
135
+ publishing: "publishing";
136
+ "read:publishing": "read:publishing";
137
+ collections: "collections";
138
+ "read:collections": "read:collections";
139
+ memberships: "memberships";
140
+ "read:memberships": "read:memberships";
141
+ roles: "roles";
142
+ "read:roles": "read:roles";
143
+ webhooks: "webhooks";
144
+ "read:webhooks": "read:webhooks";
145
+ "ai-answers": "ai-answers";
146
+ }>>>;
147
+ requests: z.ZodDefault<z.ZodArray<z.ZodString>>;
148
+ backend: z.ZodOptional<z.ZodObject<{
149
+ url: z.ZodString;
150
+ keys: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
151
+ kid: z.ZodString;
152
+ kty: z.ZodLiteral<"OKP">;
153
+ alg: z.ZodLiteral<"EdDSA">;
154
+ crv: z.ZodLiteral<"Ed25519">;
155
+ x: z.ZodString;
156
+ }, z.core.$strict>, z.ZodObject<{
157
+ kid: z.ZodString;
158
+ kty: z.ZodLiteral<"EC">;
159
+ alg: z.ZodLiteral<"ES256">;
160
+ crv: z.ZodLiteral<"P-256">;
161
+ x: z.ZodString;
162
+ y: z.ZodString;
163
+ }, z.core.$strict>], "alg">>;
164
+ }, z.core.$strict>>;
165
+ webhooks: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
166
+ events: z.ZodArray<z.ZodEnum<{
167
+ "entry.created": "entry.created";
168
+ "entry.updated": "entry.updated";
169
+ "entry.content_saved": "entry.content_saved";
170
+ "entry.moved": "entry.moved";
171
+ "entry.deleted": "entry.deleted";
172
+ "entry.restored": "entry.restored";
173
+ "collection.created": "collection.created";
174
+ "collection.updated": "collection.updated";
175
+ "collection.moved": "collection.moved";
176
+ "collection.deleted": "collection.deleted";
177
+ "collection.restored": "collection.restored";
178
+ "publishing.channel_advanced": "publishing.channel_advanced";
179
+ "publishing.channel_created": "publishing.channel_created";
180
+ "publishing.channel_deleted": "publishing.channel_deleted";
181
+ "extension.installed": "extension.installed";
182
+ "extension.enabled": "extension.enabled";
183
+ "extension.disabled": "extension.disabled";
184
+ "extension.updated": "extension.updated";
185
+ "extension.configured": "extension.configured";
186
+ "extension.uninstalled": "extension.uninstalled";
187
+ }>>;
188
+ path: z.ZodString;
189
+ channels: z.ZodDefault<z.ZodArray<z.ZodString>>;
190
+ }, z.core.$strict>>>;
191
+ configuration: z.ZodOptional<z.ZodObject<{
192
+ $schema: z.ZodOptional<z.ZodLiteral<"https://json-schema.org/draft/2020-12/schema">>;
193
+ type: z.ZodLiteral<"object">;
194
+ properties: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
195
+ type: z.ZodLiteral<"string">;
196
+ minLength: z.ZodOptional<z.ZodInt>;
197
+ maxLength: z.ZodOptional<z.ZodInt>;
198
+ pattern: z.ZodOptional<z.ZodString>;
199
+ format: z.ZodOptional<z.ZodEnum<{
200
+ uri: "uri";
201
+ email: "email";
202
+ }>>;
203
+ default: z.ZodOptional<z.ZodString>;
204
+ secret: z.ZodOptional<z.ZodLiteral<true>>;
205
+ control: z.ZodOptional<z.ZodLiteral<"textarea">>;
206
+ title: z.ZodOptional<z.ZodString>;
207
+ description: z.ZodOptional<z.ZodString>;
208
+ }, z.core.$strict>, z.ZodObject<{
209
+ type: z.ZodLiteral<"string">;
210
+ enum: z.ZodArray<z.ZodString>;
211
+ default: z.ZodOptional<z.ZodString>;
212
+ title: z.ZodOptional<z.ZodString>;
213
+ description: z.ZodOptional<z.ZodString>;
214
+ }, z.core.$strict>, z.ZodObject<{
215
+ type: z.ZodEnum<{
216
+ number: "number";
217
+ integer: "integer";
218
+ }>;
219
+ minimum: z.ZodOptional<z.ZodNumber>;
220
+ maximum: z.ZodOptional<z.ZodNumber>;
221
+ exclusiveMinimum: z.ZodOptional<z.ZodNumber>;
222
+ exclusiveMaximum: z.ZodOptional<z.ZodNumber>;
223
+ multipleOf: z.ZodOptional<z.ZodNumber>;
224
+ default: z.ZodOptional<z.ZodNumber>;
225
+ title: z.ZodOptional<z.ZodString>;
226
+ description: z.ZodOptional<z.ZodString>;
227
+ }, z.core.$strict>, z.ZodObject<{
228
+ type: z.ZodLiteral<"boolean">;
229
+ default: z.ZodOptional<z.ZodBoolean>;
230
+ title: z.ZodOptional<z.ZodString>;
231
+ description: z.ZodOptional<z.ZodString>;
232
+ }, z.core.$strict>, z.ZodObject<{
233
+ type: z.ZodLiteral<"array">;
234
+ items: z.ZodUnion<readonly [z.ZodObject<{
235
+ type: z.ZodLiteral<"string">;
236
+ minLength: z.ZodOptional<z.ZodInt>;
237
+ maxLength: z.ZodOptional<z.ZodInt>;
238
+ pattern: z.ZodOptional<z.ZodString>;
239
+ format: z.ZodOptional<z.ZodEnum<{
240
+ uri: "uri";
241
+ email: "email";
242
+ }>>;
243
+ }, z.core.$strict>, z.ZodObject<{
244
+ type: z.ZodLiteral<"string">;
245
+ enum: z.ZodArray<z.ZodString>;
246
+ }, z.core.$strict>, z.ZodObject<{
247
+ type: z.ZodEnum<{
248
+ number: "number";
249
+ integer: "integer";
250
+ }>;
251
+ minimum: z.ZodOptional<z.ZodNumber>;
252
+ maximum: z.ZodOptional<z.ZodNumber>;
253
+ exclusiveMinimum: z.ZodOptional<z.ZodNumber>;
254
+ exclusiveMaximum: z.ZodOptional<z.ZodNumber>;
255
+ multipleOf: z.ZodOptional<z.ZodNumber>;
256
+ }, z.core.$strict>, z.ZodObject<{
257
+ type: z.ZodLiteral<"boolean">;
258
+ }, z.core.$strict>]>;
259
+ minItems: z.ZodOptional<z.ZodInt>;
260
+ maxItems: z.ZodOptional<z.ZodInt>;
261
+ uniqueItems: z.ZodOptional<z.ZodBoolean>;
262
+ default: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
263
+ title: z.ZodOptional<z.ZodString>;
264
+ description: z.ZodOptional<z.ZodString>;
265
+ }, z.core.$strict>]>>;
266
+ required: z.ZodOptional<z.ZodArray<z.ZodString>>;
267
+ additionalProperties: z.ZodLiteral<false>;
268
+ }, z.core.$strict>>;
269
+ }, z.core.$strict>;
270
+ //#endregion
271
+ //#region src/index.d.ts
272
+ interface ExtensionDefinition extends Omit<ExtensionManifestInput, "configuration"> {
273
+ /** A Zod object of flat fields, or the configuration schema it converts to. */
274
+ configuration?: z.ZodObject | ExtensionConfiguration;
275
+ }
276
+ /** Options of `andesine extensions build`, exported from the config file as `build`. */
277
+ interface ExtensionBuildOptions {
278
+ /** The module exporting the manifest's view entries (default: `src/frontend/index.tsx`). */
279
+ frontend?: string;
280
+ /** Custom icon sets: a set prefix and a directory of SVG files, e.g. `{ acme: "./icons" }`. */
281
+ icons?: Record<string, string>;
282
+ }
283
+ type ExtensionManifestInput = z.input<typeof extensionManifestType>;
284
+ /** Defines the `andesine.config.ts` manifest; a Zod configuration becomes JSON Schema. */
285
+ declare const defineExtension: (definition: ExtensionDefinition) => ExtensionManifestInput;
286
+ declare const defineBuild: (options: ExtensionBuildOptions) => ExtensionBuildOptions;
287
+ //#endregion
288
+ export { type ExtensionBuildOptions, type ExtensionDefinition, type ExtensionManifestInput, defineBuild, defineExtension };
289
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,173 @@
1
+ import * as z from "zod";
2
+ //#region ../../private/contracts/src/primitives/unique.ts
3
+ const uniqueItems = (values) => new Set(values).size === values.length;
4
+ //#endregion
5
+ //#region ../../private/contracts/src/extensions/configuration.ts
6
+ const JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
7
+ const MAX_CONFIGURATION_STRING_LENGTH = 1e4;
8
+ const MAX_CONFIGURATION_ARRAY_ITEMS = 100;
9
+ const MAX_CONFIGURATION_ENUM_VALUES = 100;
10
+ const MAX_CONFIGURATION_PATTERN_LENGTH = 500;
11
+ const isValidPattern = (pattern) => {
12
+ try {
13
+ new RegExp(pattern);
14
+ return true;
15
+ } catch {
16
+ return false;
17
+ }
18
+ };
19
+ const fieldKeyType = z.string().regex(/^[A-Za-z][A-Za-z0-9_]{0,63}$/, "Use a letter, then letters, digits, or underscores");
20
+ const lengthType = z.int().min(0).max(MAX_CONFIGURATION_STRING_LENGTH);
21
+ const metadata = {
22
+ title: z.string().min(1).max(100).optional(),
23
+ description: z.string().min(1).max(500).optional()
24
+ };
25
+ const stringItemType = z.strictObject({
26
+ type: z.literal("string"),
27
+ minLength: lengthType.optional(),
28
+ maxLength: lengthType.optional(),
29
+ pattern: z.string().min(1).max(MAX_CONFIGURATION_PATTERN_LENGTH).refine(isValidPattern, "Use a valid regular expression").optional(),
30
+ format: z.enum(["uri", "email"]).optional()
31
+ });
32
+ const enumItemType = z.strictObject({
33
+ type: z.literal("string"),
34
+ enum: z.array(z.string().min(1).max(200)).min(1).max(MAX_CONFIGURATION_ENUM_VALUES).refine(uniqueItems, "Options must be unique")
35
+ });
36
+ const numberItemType = z.strictObject({
37
+ type: z.enum(["number", "integer"]),
38
+ minimum: z.number().optional(),
39
+ maximum: z.number().optional(),
40
+ exclusiveMinimum: z.number().optional(),
41
+ exclusiveMaximum: z.number().optional(),
42
+ multipleOf: z.number().positive().optional()
43
+ });
44
+ const booleanItemType = z.strictObject({ type: z.literal("boolean") });
45
+ const stringFormatTypes = {
46
+ uri: z.url(),
47
+ email: z.email()
48
+ };
49
+ const itemType = z.union([
50
+ stringItemType,
51
+ enumItemType,
52
+ numberItemType,
53
+ booleanItemType
54
+ ]);
55
+ const fieldType = z.union([
56
+ stringItemType.extend({
57
+ ...metadata,
58
+ default: z.string().optional(),
59
+ secret: z.literal(true).optional(),
60
+ control: z.literal("textarea").optional()
61
+ }),
62
+ enumItemType.extend({
63
+ ...metadata,
64
+ default: z.string().optional()
65
+ }),
66
+ numberItemType.extend({
67
+ ...metadata,
68
+ default: z.number().optional()
69
+ }),
70
+ booleanItemType.extend({
71
+ ...metadata,
72
+ default: z.boolean().optional()
73
+ }),
74
+ z.strictObject({
75
+ ...metadata,
76
+ type: z.literal("array"),
77
+ items: itemType,
78
+ minItems: z.int().min(0).max(MAX_CONFIGURATION_ARRAY_ITEMS).optional(),
79
+ maxItems: z.int().min(0).max(MAX_CONFIGURATION_ARRAY_ITEMS).optional(),
80
+ uniqueItems: z.boolean().optional(),
81
+ default: z.array(z.union([
82
+ z.string(),
83
+ z.number(),
84
+ z.boolean()
85
+ ])).optional()
86
+ })
87
+ ]);
88
+ /** Flat JSON Schema 2020-12 subset; the host validates it without running extension code. */
89
+ const extensionConfigurationType = z.strictObject({
90
+ $schema: z.literal(JSON_SCHEMA_DIALECT).optional(),
91
+ type: z.literal("object"),
92
+ properties: z.record(fieldKeyType, fieldType).refine((properties) => Object.keys(properties).length <= 50, `Use at most 50 fields`),
93
+ required: z.array(fieldKeyType).refine(uniqueItems, "Fields must be unique").optional(),
94
+ additionalProperties: z.literal(false)
95
+ }).superRefine((configuration, context) => {
96
+ for (const [index, key] of (configuration.required || []).entries()) if (!configuration.properties[key]) context.addIssue({
97
+ code: "custom",
98
+ path: ["required", index],
99
+ message: "Unknown field"
100
+ });
101
+ for (const [key, field] of Object.entries(configuration.properties)) if ("secret" in field && field.secret && field.default !== void 0) context.addIssue({
102
+ code: "custom",
103
+ path: [
104
+ "properties",
105
+ key,
106
+ "default"
107
+ ],
108
+ message: "Secret fields cannot have defaults"
109
+ });
110
+ else if (field.default !== void 0) {
111
+ if (!createConfigurationValueType(field).safeParse(field.default).success) context.addIssue({
112
+ code: "custom",
113
+ path: [
114
+ "properties",
115
+ key,
116
+ "default"
117
+ ],
118
+ message: "The default does not match the field"
119
+ });
120
+ }
121
+ });
122
+ const createConfigurationValueType = (field) => {
123
+ if (field.type === "boolean") return z.boolean();
124
+ if (field.type === "array") {
125
+ const maxItems = Math.min(field.maxItems ?? MAX_CONFIGURATION_ARRAY_ITEMS, MAX_CONFIGURATION_ARRAY_ITEMS);
126
+ const items = z.array(createConfigurationValueType(field.items)).min(field.minItems ?? 0).max(maxItems);
127
+ return field.uniqueItems ? items.refine(uniqueItems, "Values must be unique") : items;
128
+ }
129
+ if ("enum" in field) return z.enum(field.enum);
130
+ if (field.type === "string") {
131
+ const maxLength = Math.min(field.maxLength ?? MAX_CONFIGURATION_STRING_LENGTH, MAX_CONFIGURATION_STRING_LENGTH);
132
+ const value = (field.format ? stringFormatTypes[field.format] : z.string()).min(field.minLength ?? 0).max(maxLength);
133
+ return field.pattern ? value.regex(new RegExp(field.pattern)) : value;
134
+ }
135
+ let value = field.type === "integer" ? z.int() : z.number();
136
+ if (field.minimum !== void 0) value = value.gte(field.minimum);
137
+ if (field.maximum !== void 0) value = value.lte(field.maximum);
138
+ if (field.exclusiveMinimum !== void 0) value = value.gt(field.exclusiveMinimum);
139
+ if (field.exclusiveMaximum !== void 0) value = value.lt(field.exclusiveMaximum);
140
+ if (field.multipleOf !== void 0) value = value.multipleOf(field.multipleOf);
141
+ return value;
142
+ };
143
+ /** Converts an authoring Zod object to the configuration subset; unsupported constructs throw. */
144
+ const toExtensionConfiguration = (schema) => {
145
+ const jsonSchema = z.toJSONSchema(schema, {
146
+ io: "input",
147
+ target: "draft-2020-12",
148
+ unrepresentable: "throw"
149
+ });
150
+ const result = extensionConfigurationType.safeParse({
151
+ additionalProperties: false,
152
+ ...jsonSchema
153
+ });
154
+ if (!result.success) throw new Error(`Unsupported configuration schema:\n${z.prettifyError(result.error)}`);
155
+ return result.data;
156
+ };
157
+ //#endregion
158
+ //#region src/index.ts
159
+ const isZodObject = (value) => "_zod" in value;
160
+ /** Defines the `andesine.config.ts` manifest; a Zod configuration becomes JSON Schema. */
161
+ const defineExtension = (definition) => {
162
+ const { configuration, ...manifest } = definition;
163
+ if (!configuration) return manifest;
164
+ return {
165
+ ...manifest,
166
+ configuration: isZodObject(configuration) ? toExtensionConfiguration(configuration) : configuration
167
+ };
168
+ };
169
+ const defineBuild = (options) => options;
170
+ //#endregion
171
+ export { defineBuild, defineExtension };
172
+
173
+ //# sourceMappingURL=index.js.map