@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 +65 -0
- package/LICENSE +21 -0
- package/README.md +104 -3
- package/dist/index.d.ts +289 -0
- package/dist/index.js +173 -0
- package/dist/index.js.map +1 -0
- package/dist/solid.d.ts +397 -0
- package/dist/solid.js +558 -0
- package/dist/solid.js.map +1 -0
- package/package.json +60 -3
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
|
-
#
|
|
1
|
+
# @andesine/extensions
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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
|