@dolphy-app/extension-sdk 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -12
- package/dist/client.d.ts +40 -0
- package/dist/client.js +67 -0
- package/dist/define-entry-BTz2F3qv.js +22 -0
- package/dist/define-entry-lsuxzKdD.d.ts +77 -0
- package/dist/index-I_CXC6Lj.d.ts +1958 -0
- package/dist/index.d.ts +7 -103
- package/dist/index.js +5 -80
- package/dist/react.d.ts +51 -0
- package/dist/react.js +112 -0
- package/dist/rpc.d.ts +28 -0
- package/dist/rpc.js +37 -0
- package/dist/testing.d.ts +248 -121
- package/dist/testing.js +779 -241
- package/docs/debugging.md +228 -0
- package/docs/no-build.md +135 -0
- package/docs/quick-start.md +194 -0
- package/docs/recipe-command-panel.md +355 -0
- package/docs/recipe-event-storage.md +326 -0
- package/docs/recipe-exercise-type.md +467 -0
- package/docs/recipe-hooks.md +158 -0
- package/docs/recipe-import-export.md +236 -0
- package/docs/recipe-mountable.md +322 -0
- package/docs/recipe-react.md +352 -0
- package/docs/recipe-rpc-and-app.md +366 -0
- package/docs/recipe-settings.md +183 -0
- package/docs/recipe-theme.md +148 -0
- package/docs/recipe-when-dependencies.md +264 -0
- package/package.json +31 -8
- package/dist/answer-element-BOQcuxYh.js +0 -114
- package/dist/answer-view-D6wnyThb.d.ts +0 -28
- package/dist/runtime.d.ts +0 -14
- package/dist/runtime.js +0 -18
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Debugging
|
|
2
|
+
|
|
3
|
+
Where to look when an extension does not do what you expect, from the cheapest
|
|
4
|
+
check to the running app. Commands are those of a generated project; see the
|
|
5
|
+
[quick start](quick-start.md).
|
|
6
|
+
|
|
7
|
+
## 1. Reproduce it in a test
|
|
8
|
+
|
|
9
|
+
`createTestServer` and `createTestClient` of `@dolphy-app/extension-sdk/testing`
|
|
10
|
+
run your entries in plain Node, with a debugger and `console.log` available.
|
|
11
|
+
Most mistakes (a wrong result shape, a setting read before it is set, a handler
|
|
12
|
+
that throws, an id without the extension prefix) show up there.
|
|
13
|
+
|
|
14
|
+
- `createTestServer(server, { extensionId, logger })` takes a `logger` option:
|
|
15
|
+
pass an object with `debug`, `info`, `warn` and `error` methods to see what
|
|
16
|
+
`server.logger` receives.
|
|
17
|
+
- With `extensionId` set the harness checks that every id you register is the
|
|
18
|
+
extension id or starts with `<id>.`, the way the host does. It also checks what
|
|
19
|
+
the host checks about a result: the shape of a verdict, a grade of 1–5 or
|
|
20
|
+
`null`, a command result (`notify` text of 1–500 characters, a JSON value of
|
|
21
|
+
at most 64 KiB), an importer's or exporter's result. A message from the harness
|
|
22
|
+
usually names the rule.
|
|
23
|
+
- An error in `server` fails `createTestServer` itself, as it fails the load in
|
|
24
|
+
the app: nothing is registered. A handler's failure is not swallowed but
|
|
25
|
+
rejects the promise.
|
|
26
|
+
- The harness does not count the host's time limits, so a test that passes does
|
|
27
|
+
not prove a handler is fast enough: an event handler gets 2 seconds, a command
|
|
28
|
+
or a schedule handler 10, an importer or an exporter 30, and the registration
|
|
29
|
+
(`server`) 10.
|
|
30
|
+
|
|
31
|
+
## 2. Run the checks
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
pnpm typecheck # tsc
|
|
35
|
+
pnpm validate # the manifest the way the app parses it
|
|
36
|
+
pnpm lint # metadata and bundle findings the catalog review also sees
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`pnpm validate` parses the built manifest with the same code as the app and
|
|
40
|
+
exits with code 1 on a problem; a key the manifest does not have (a command, a
|
|
41
|
+
setting, a panel written into `extension.json`) is one: the manifest holds the
|
|
42
|
+
identity of the extension, and the code registers everything else. The build
|
|
43
|
+
also fails when `server` imports `vue`, `vuetify` or a component, or `client`
|
|
44
|
+
imports a `node:*` module, and names the file.
|
|
45
|
+
`pnpm lint` prints `warning <id> <RULE> <field>: <message>` lines; a rule that
|
|
46
|
+
looks wrong for your code (for example `eval` in a bundled dependency) is a hint
|
|
47
|
+
for the reviewer, not a failure.
|
|
48
|
+
|
|
49
|
+
## 3. The app: development loop
|
|
50
|
+
|
|
51
|
+
`dolphy-ext dev` builds the project in watch mode and starts the installed
|
|
52
|
+
Dolphy app on the result:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
pnpm exec dolphy-ext dev # the project in the current directory
|
|
56
|
+
pnpm exec dolphy-ext dev ../my-ext # another project
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
It prints the path of the app it started and the directory it passes to it
|
|
60
|
+
(`DOLPHY_DEV_EXTENSIONS=<project>/dist-ext`). Press Ctrl+C to stop the build and
|
|
61
|
+
the app; the exit code is 0. The app is looked up in this order:
|
|
62
|
+
|
|
63
|
+
1. `--app <path>`: a macOS `.app` bundle or an executable file.
|
|
64
|
+
2. The `DOLPHY_APP` environment variable (same kind of path).
|
|
65
|
+
3. The standard place of your system: macOS `/Applications/Dolphy.app`, then
|
|
66
|
+
`~/Applications/Dolphy.app`; Windows
|
|
67
|
+
`%LOCALAPPDATA%\Programs\Dolphy\Dolphy.exe`; Linux the newest
|
|
68
|
+
`~/Applications/Dolphy-Linux-*.AppImage` (Linux has no fixed place: keep the
|
|
69
|
+
AppImage there or use `--app`).
|
|
70
|
+
|
|
71
|
+
When none exists the command exits with code 2 and lists the places it looked
|
|
72
|
+
at. A path given with `--app` or `DOLPHY_APP` that does not exist is an error as
|
|
73
|
+
well, not a silent fallback.
|
|
74
|
+
|
|
75
|
+
If you prefer to run the two parts yourself, start `pnpm dev`
|
|
76
|
+
(`dolphy-ext build --watch`) and start Dolphy with
|
|
77
|
+
`DOLPHY_DEV_EXTENSIONS=<project>/dist-ext`; from a checkout of the Dolphy
|
|
78
|
+
repository that is `DOLPHY_DEV_EXTENSIONS=<project>/dist-ext pnpm dev`. The app
|
|
79
|
+
rereads the extensions on every file change and applies the change live: the
|
|
80
|
+
extension host runs `server` again and the window loads `client.mjs` again, and
|
|
81
|
+
the window itself does not reload. Things to know:
|
|
82
|
+
|
|
83
|
+
- The app has one instance. If Dolphy is already running, a second start exits
|
|
84
|
+
at once and the variable is lost. `dolphy-ext dev` notices an app that quits
|
|
85
|
+
within 5 seconds and prints "Dolphy is probably already running: quit it and
|
|
86
|
+
run again" (exit code 1): quit the running app and start again.
|
|
87
|
+
- The extension appears in Settings → Extensions with the origin
|
|
88
|
+
"Development". A manifest or load problem is shown there next to the
|
|
89
|
+
extension instead of the extension working: start with that text.
|
|
90
|
+
- If the id is also in the bundled set or installed from the catalog, the
|
|
91
|
+
development copy wins.
|
|
92
|
+
- The components of extensions in development are redrawn on a change, so their
|
|
93
|
+
state can be lost.
|
|
94
|
+
|
|
95
|
+
### `load-failed`
|
|
96
|
+
|
|
97
|
+
When `server` throws, registers something invalid (an id that is taken or does
|
|
98
|
+
not carry the extension prefix, a bad `when`, an invalid setting definition), or
|
|
99
|
+
does not finish in 10 seconds, the extension host registers
|
|
100
|
+
nothing from it. Registration is all or nothing: the commands, settings and
|
|
101
|
+
event handlers `server` managed to add before the failure are removed too. The
|
|
102
|
+
extension row in Settings → Extensions shows the diagnostic "Could not load the
|
|
103
|
+
extension: {reason}" with the message of the error, and the log has the entry
|
|
104
|
+
`extension registration failed` with the cause (`activation-failed`, or
|
|
105
|
+
`activation-timeout` for the 10 seconds). `server` is called once per load, so
|
|
106
|
+
do slow work (a network request, a big file) in a handler and not in `server`
|
|
107
|
+
itself.
|
|
108
|
+
|
|
109
|
+
The client part has its own failure. When `client.mjs` does not import, `client`
|
|
110
|
+
throws, or a registration is refused (an id that is taken, a bad injection
|
|
111
|
+
target), the window shows "The extension client part failed to load" on the
|
|
112
|
+
extension and keeps no contribution of the client part; the other extensions
|
|
113
|
+
and the server part of this one keep working. The reason is in the DevTools
|
|
114
|
+
console of section 4. After you fix the file, the next rebuild loads it again.
|
|
115
|
+
|
|
116
|
+
## 4. DevTools
|
|
117
|
+
|
|
118
|
+
While `DOLPHY_DEV_EXTENSIONS` is set (so under `dolphy-ext dev` too), in any
|
|
119
|
+
build of the app, including an installed one, these keys toggle the DevTools of
|
|
120
|
+
the main window: `F12`, `Cmd+Alt+I` (macOS) and `Ctrl+Shift+I`. Without the
|
|
121
|
+
variable the keys do nothing and an installed app has no DevTools.
|
|
122
|
+
|
|
123
|
+
- Panels, injected components, answer views and markdown blocks of an extension
|
|
124
|
+
are Vue components in the page of the app window itself. "Elements" shows
|
|
125
|
+
their markup in the page and the Vue DevTools show the component tree; the
|
|
126
|
+
console evaluates in the same page as the app and shows what the client part
|
|
127
|
+
and its components print with `console`.
|
|
128
|
+
- `dolphy-ext dev` and `dolphy-ext build --watch` put an inline source map
|
|
129
|
+
(`//# sourceMappingURL=data:application/json…`) into every bundle, so
|
|
130
|
+
"Sources" shows your TypeScript (`src/client.ts` and the files it imports)
|
|
131
|
+
for the client part: set a breakpoint there, `debugger;` works too. A plain
|
|
132
|
+
`dolphy-ext build` (`pnpm build`) and the catalog build never write source
|
|
133
|
+
maps, and the catalog check rejects a submission that has one.
|
|
134
|
+
- The code that runs in the extension host (`main.mjs`: `server`, command and
|
|
135
|
+
event handlers, schedules, importers and exporters) is not in this window: see
|
|
136
|
+
section 6 and the log.
|
|
137
|
+
|
|
138
|
+
## 5. The log
|
|
139
|
+
|
|
140
|
+
`server.logger` has `debug`, `info`, `warn` and `error`; each takes an object of
|
|
141
|
+
fields and an optional message:
|
|
142
|
+
|
|
143
|
+
```text
|
|
144
|
+
s.logger.info({ id: change.id, value: change.value }, 'setting changed');
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The output goes to the log of the app, not to a console of the window. Log
|
|
148
|
+
structured fields, not secrets or the learner's answers. The host writes to the
|
|
149
|
+
same log: an exception in a handler, a handler that outlives its time limit, an
|
|
150
|
+
event dropped from a full queue, and the failure of a registration. The client
|
|
151
|
+
part has no logger: use `console` and the DevTools.
|
|
152
|
+
|
|
153
|
+
### Reading the log in the app
|
|
154
|
+
|
|
155
|
+
1. Open Settings → Extensions. In the "Diagnostics" block press "Log" to see
|
|
156
|
+
everything, or press "Log" in the row of your extension to see only its
|
|
157
|
+
entries (the dialog opens with the id of the extension in the filter).
|
|
158
|
+
2. The dialog lists the last 500 entries, the newest at the bottom. Each entry
|
|
159
|
+
shows the time, the level, the source (`main`, `engine` or `ext-host`), the
|
|
160
|
+
id of the extension that wrote it and the message. "Details" opens the other
|
|
161
|
+
fields of the entry as JSON.
|
|
162
|
+
3. Filter by extension: type or pick an id in "Extension" (an id that is not in
|
|
163
|
+
the list is accepted). Filter by level: "Minimum level" hides entries below
|
|
164
|
+
it; the default, "Debug", shows all of them.
|
|
165
|
+
4. The dialog does not update by itself: press "Refresh" after you triggered
|
|
166
|
+
the code you are watching. "No entries match the filters." means the filters
|
|
167
|
+
hide everything, or nothing was logged yet.
|
|
168
|
+
|
|
169
|
+
The labels the dialog shows, with the keys of the app's messages (ru and en):
|
|
170
|
+
|
|
171
|
+
| Where | English | Русский | Message key |
|
|
172
|
+
| -------------------------- | -------------------------------- | ----------------------------- | ------------------------------------------ |
|
|
173
|
+
| Settings section | Extensions | Расширения | `settings.extensions.title` |
|
|
174
|
+
| Origin of a dev extension | Development | Разработка | `settings.extensions.origin.dev` |
|
|
175
|
+
| Load failure of the client part | The extension client part failed to load | Клиентская часть расширения не загрузилась | `settings.extensions.clientFailed.title` |
|
|
176
|
+
| Block with the log button | Diagnostics | Диагностика | `settings.extensions.support.title` |
|
|
177
|
+
| Button of the block | Log | Журнал | `settings.extensions.support.openLog` |
|
|
178
|
+
| Action in an extension row | Log | Журнал | `settings.extensions.log.rowAction` |
|
|
179
|
+
| Dialog title | Log | Журнал | `settings.extensions.log.title` |
|
|
180
|
+
| Extension filter | Extension | Расширение | `settings.extensions.log.filterExtension` |
|
|
181
|
+
| Extension filter, empty | All extensions | Все расширения | `settings.extensions.log.filterExtensionHint` |
|
|
182
|
+
| Level filter | Minimum level | Минимальный уровень | `settings.extensions.log.filterLevel` |
|
|
183
|
+
| Level | Debug | Отладка | `settings.extensions.log.level.debug` |
|
|
184
|
+
| Level | Info | Инфо | `settings.extensions.log.level.info` |
|
|
185
|
+
| Level | Warning | Предупреждение | `settings.extensions.log.level.warn` |
|
|
186
|
+
| Level | Error | Ошибка | `settings.extensions.log.level.error` |
|
|
187
|
+
| Other fields of an entry | Details | Подробности | `settings.extensions.log.details` |
|
|
188
|
+
| Reread the log | Refresh | Обновить | `settings.extensions.log.refresh` |
|
|
189
|
+
| Filters match nothing | No entries match the filters. | Нет записей, подходящих под условия. | `settings.extensions.log.empty` |
|
|
190
|
+
|
|
191
|
+
### Output of the extension host
|
|
192
|
+
|
|
193
|
+
What `server` and its handlers print with `console.log`, `console.error` or an
|
|
194
|
+
uncaught error goes to the log as `warn` entries of the extension host in
|
|
195
|
+
"Details". Use `s.logger` for anything you want to filter by level (its entries
|
|
196
|
+
carry the extension id); use `console` only for a quick look.
|
|
197
|
+
|
|
198
|
+
### The file
|
|
199
|
+
|
|
200
|
+
The entries are also in `logs/dolphy-YYYY-MM-DD.log` in the app's data folder,
|
|
201
|
+
one JSON object per line (`level`, `source`, `message`, `at` and the other
|
|
202
|
+
fields). A new file starts every day and at 2 MiB; files older than 7 days are
|
|
203
|
+
removed, and the oldest go first while the folder is over 10 MiB. Attach the
|
|
204
|
+
relevant lines, or the text of "Copy diagnostics" in the "Diagnostics" block (it
|
|
205
|
+
has no paths of your home folder, library content or learning data), to a bug
|
|
206
|
+
report.
|
|
207
|
+
|
|
208
|
+
## 6. Reading a stack trace
|
|
209
|
+
|
|
210
|
+
The code of the extension host is `dist-ext/<id>/main.mjs`, readable and not
|
|
211
|
+
minified. A watch build (`dolphy-ext dev`, `dolphy-ext build --watch`) appends
|
|
212
|
+
an inline source map to it, but the app does not turn on source maps for that
|
|
213
|
+
process, so a stack trace in the log still points to lines of `main.mjs`: open
|
|
214
|
+
that file to find the place and search it for the name from the trace. The
|
|
215
|
+
map is for the browser file of section 4. A plain `dolphy-ext build` writes no
|
|
216
|
+
maps at all.
|
|
217
|
+
|
|
218
|
+
## Which limit did I hit?
|
|
219
|
+
|
|
220
|
+
| Symptom | Limit |
|
|
221
|
+
| ----------------------------------------------- | -------------------------------------------------------- |
|
|
222
|
+
| the extension shows `load-failed` with a timeout | `server` must finish in 10 seconds |
|
|
223
|
+
| an event handler stops mid-way | 2 seconds per event; the queue holds 100 events |
|
|
224
|
+
| a command or a schedule handler stops mid-way | 10 seconds |
|
|
225
|
+
| an importer or an exporter stops mid-way | 30 seconds |
|
|
226
|
+
| `StorageQuotaError` | key 128 characters, value 64 KiB, 256 keys, 1 MiB total |
|
|
227
|
+
| a command result is rejected | `notify` text 1–500 characters; a result up to 64 KiB |
|
|
228
|
+
| a panel or a view fails while it draws | the card "Retry" replaces it; the window stays up |
|
package/docs/no-build.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Without a build
|
|
2
|
+
|
|
3
|
+
An extension is a directory with an `extension.json` and, if it has code, one or
|
|
4
|
+
two ES modules: `main.mjs` for the `server` entry and `client.mjs` for the
|
|
5
|
+
`client` entry. `dolphy-ext build` only produces such a directory from a
|
|
6
|
+
TypeScript project; you can write it by hand. You do not need TypeScript, a
|
|
7
|
+
`package.json` or `dolphy-ext` for that. This page shows the smallest case: one
|
|
8
|
+
palette command and a panel, three hand-written files. Use it for a quick
|
|
9
|
+
experiment or when the code is a few lines; switch to the
|
|
10
|
+
[quick start](quick-start.md) layout when you want types, tests and bundling.
|
|
11
|
+
|
|
12
|
+
## The files
|
|
13
|
+
|
|
14
|
+
File `extension.json` (no build):
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"id": "acme.plain",
|
|
19
|
+
"version": "0.1.0",
|
|
20
|
+
"apiVersion": 1,
|
|
21
|
+
"main": "./main.mjs",
|
|
22
|
+
"client": "./client.mjs",
|
|
23
|
+
"name": "Plain hello",
|
|
24
|
+
"description": "A palette command and a panel written by hand, without a build step.",
|
|
25
|
+
"author": "your-github-login",
|
|
26
|
+
"tags": ["productivity"]
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
File `main.mjs` (no build):
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
export const server = (s) => {
|
|
34
|
+
s.registerCommand({
|
|
35
|
+
id: 'acme.plain.hello',
|
|
36
|
+
title: 'Say hello',
|
|
37
|
+
run: () => ({ notify: `Hello from ${s.extensionId}!` }),
|
|
38
|
+
});
|
|
39
|
+
s.registerCommand({
|
|
40
|
+
id: 'acme.plain.open',
|
|
41
|
+
title: 'Open the plain panel',
|
|
42
|
+
run: () => ({ openPanel: 'acme.plain.view' }),
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
File `client.mjs` (no build):
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
// `vue` is the app's own instance: the window gives it through globalThis.__dolphy
|
|
51
|
+
const { defineComponent, h } = await globalThis.__dolphy.require('vue');
|
|
52
|
+
|
|
53
|
+
export const client = (c) => {
|
|
54
|
+
c.addPanel({
|
|
55
|
+
id: 'acme.plain.view',
|
|
56
|
+
title: 'Plain hello',
|
|
57
|
+
component: defineComponent({
|
|
58
|
+
setup: () => () => h('p', 'Hello from a panel'),
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- `main` and `client` are written in the manifest: nothing fills them in. A part
|
|
65
|
+
that is left out (`null` or no key) does not exist, so an extension may have
|
|
66
|
+
only `main.mjs` or only `client.mjs`. Each is a path inside the directory that
|
|
67
|
+
ends with `.mjs`.
|
|
68
|
+
- The contract of `main.mjs` is `export const server = (s) => { … }`, the same
|
|
69
|
+
function `defineServer` takes in a project; `s` is the `ServerContext`
|
|
70
|
+
(`registerCommand`, `registerSettings`, `on`, `storage`, `logger`, …). The
|
|
71
|
+
contract of `client.mjs` is `export const client = (c) => { … }`
|
|
72
|
+
(`addPanel`, `addInjection`, `addAnswerView`, `addMarkdownRenderer`,
|
|
73
|
+
`addTheme`, `addCommand`). Either may return a cleanup function. `async`
|
|
74
|
+
functions work. `server` is called when the host loads the extension; if it
|
|
75
|
+
throws or takes more than 10 seconds the extension shows `load-failed` and
|
|
76
|
+
registers nothing.
|
|
77
|
+
- Without a build there is no `import 'vue'`: `client.mjs` reads `vue` (and
|
|
78
|
+
`vuetify`, `vuetify/components`, `vuetify/directives`) from
|
|
79
|
+
`globalThis.__dolphy.require(name)`, which resolves to the app's own instance.
|
|
80
|
+
Write the components with `h` or a render function: there is no template
|
|
81
|
+
compiler. `main.mjs` never needs `vue`.
|
|
82
|
+
- There are no helpers, so a command returns the result object itself:
|
|
83
|
+
`{ notify: text }` is what `notify(text)` makes and `{ openPanel: id, props }`
|
|
84
|
+
is what `openPanel(id, props)` makes. Ids are written in the code and must be
|
|
85
|
+
the extension id or start with `<id>.`.
|
|
86
|
+
- The modules must be plain JavaScript that the app runs as it is: no
|
|
87
|
+
TypeScript, no bare imports of packages (nothing is installed next to them),
|
|
88
|
+
and no `node:*` imports in `client.mjs`.
|
|
89
|
+
|
|
90
|
+
## Check and try
|
|
91
|
+
|
|
92
|
+
Put the files into a directory named exactly as the extension id,
|
|
93
|
+
`acme.plain/` (the app takes the id from the directory name and rejects a
|
|
94
|
+
manifest that disagrees), and check it:
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
npx --package @dolphy-app/extension-tools dolphy-ext validate ./acme.plain
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`validate` parses the manifest the way the app does, checks that `main` and
|
|
101
|
+
`client` are files, and exits with code 1 on a problem. To try the extension,
|
|
102
|
+
put the directory into a folder and start the app with `DOLPHY_DEV_EXTENSIONS`
|
|
103
|
+
pointing at that folder (the folder, not the extension directory: it holds one
|
|
104
|
+
directory per extension), or copy the directory to `<userData>/extensions/` and
|
|
105
|
+
restart the app. Edits to the files are picked up without a restart when you use
|
|
106
|
+
`DOLPHY_DEV_EXTENSIONS`.
|
|
107
|
+
|
|
108
|
+
To test the server module without the app, run its entry with the SDK helper:
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
npm install --save-dev @dolphy-app/extension-sdk vitest
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
<!-- fragment -->
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
import { createTestServer } from '@dolphy-app/extension-sdk/testing';
|
|
118
|
+
import { server } from './main.mjs';
|
|
119
|
+
|
|
120
|
+
const running = await createTestServer(server, { extensionId: 'acme.plain' });
|
|
121
|
+
console.log(await running.commands.run('acme.plain.hello'));
|
|
122
|
+
// { kind: 'notify', text: 'Hello from acme.plain!' }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`createTestServer` accepts the export of the module as it is, because it has the
|
|
126
|
+
shape of a server entry. `createTestClient(client, { extensionId })` does the
|
|
127
|
+
same for `client.mjs` and lists the panels it added.
|
|
128
|
+
|
|
129
|
+
## Publishing
|
|
130
|
+
|
|
131
|
+
The extension catalog reviews a project, not a bare directory: it requires a
|
|
132
|
+
`package.json` with a lock file and a `README.md` that explains what the
|
|
133
|
+
extension does (`dolphy-ext catalog check`). A hand-written directory is for
|
|
134
|
+
yourself or for handing a folder to someone. To publish, generate a project with
|
|
135
|
+
`create-dolphy-extension` and move the code into it.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Quick start
|
|
2
|
+
|
|
3
|
+
This guide takes you from nothing to a working Dolphy extension: a command in
|
|
4
|
+
the command palette that shows a notification. It is the smallest project the
|
|
5
|
+
generator makes (`--template blank`); the recipes next to this file build on the
|
|
6
|
+
same layout:
|
|
7
|
+
|
|
8
|
+
- [Exercise type](recipe-exercise-type.md): a new kind of task with its own answer input.
|
|
9
|
+
- [Theme](recipe-theme.md): colors, registered by the client part.
|
|
10
|
+
- [Command and panel](recipe-command-panel.md): palette commands and a panel, a Vue single-file component (`.vue`) in the app window.
|
|
11
|
+
- [A panel in React](recipe-react.md): the same panel drawn with React: `"frameworks": ["react"]`, `reactComponent`, hooks, tests with `mountForTest`.
|
|
12
|
+
- [A component of any framework](recipe-mountable.md): `defineMountable` on plain DOM, the base for Svelte, Solid or Lit, and what `ctx` gives it.
|
|
13
|
+
- [Events and storage](recipe-event-storage.md): react to learning events and keep data.
|
|
14
|
+
- [Hooks](recipe-hooks.md): change or cancel a session start and the batch of exercises before the engine acts.
|
|
15
|
+
- [Settings](recipe-settings.md): let the user configure the extension.
|
|
16
|
+
- [Visibility conditions and dependencies](recipe-when-dependencies.md): show a command only where it makes sense, a widget in the daily plan, require another extension.
|
|
17
|
+
- [Importer and exporter](recipe-import-export.md): bring a file in as a course, write a course out.
|
|
18
|
+
- [Calls, engine and window](recipe-rpc-and-app.md): `defineRpc` between the parts, `engine` access, `useApp`.
|
|
19
|
+
- [Without a build](no-build.md): a hand-written `extension.json`, `main.mjs` and `client.mjs`, no TypeScript.
|
|
20
|
+
- [Debugging](debugging.md): where to look when something does not work.
|
|
21
|
+
|
|
22
|
+
## What you need
|
|
23
|
+
|
|
24
|
+
- Node.js 22.12 or newer and pnpm.
|
|
25
|
+
- The Dolphy app, to see the extension work. Everything else (build, type
|
|
26
|
+
check, tests) runs without it.
|
|
27
|
+
|
|
28
|
+
## 1. Create the project
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
npx @dolphy-app/create-extension my-extension --id acme.hello --template blank
|
|
32
|
+
cd my-extension
|
|
33
|
+
pnpm install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`--id` is the extension id: lowercase letters, digits, dots and hyphens. Every
|
|
37
|
+
id the extension registers (commands, panels, settings…) is the extension id or
|
|
38
|
+
starts with it and a dot, so pick one that is yours (a publisher prefix, then a
|
|
39
|
+
name). Without `--id` the id is the directory name in kebab-case. The
|
|
40
|
+
`--template` values:
|
|
41
|
+
|
|
42
|
+
| Template | What you get |
|
|
43
|
+
| --------------- | ----------------------------------------------------------------- |
|
|
44
|
+
| `exercise` | a task type with an answer input and a setting (the default) |
|
|
45
|
+
| `theme` | a color theme |
|
|
46
|
+
| `command-panel` | palette commands and a panel as a Vue single-file component |
|
|
47
|
+
| `react-panel` | palette commands and a panel drawn with React |
|
|
48
|
+
| `events` | a learning event handler, storage, commands and a panel |
|
|
49
|
+
| `blank` | one palette command |
|
|
50
|
+
|
|
51
|
+
An unknown name exits with code 2 and lists the available ones.
|
|
52
|
+
|
|
53
|
+
## 2. The files
|
|
54
|
+
|
|
55
|
+
The project is two files that matter. The manifest says who the extension is,
|
|
56
|
+
the code says what it adds.
|
|
57
|
+
|
|
58
|
+
File `extension.json` (quick start):
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"$schema": "./node_modules/@dolphy-app/extension-api/dist/extension.schema.json",
|
|
63
|
+
"id": "acme.hello",
|
|
64
|
+
"version": "0.1.0",
|
|
65
|
+
"apiVersion": 1,
|
|
66
|
+
"name": "Hello command",
|
|
67
|
+
"description": "A command-palette command that shows a notification.",
|
|
68
|
+
"author": "your-github-login",
|
|
69
|
+
"tags": ["productivity"]
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`$schema` gives editors completion and checking. `id` is the identity of the
|
|
74
|
+
extension in the app and the catalog; it never changes after the first release.
|
|
75
|
+
Replace `your-github-login` in `author` with your GitHub login before you
|
|
76
|
+
publish. The manifest declares no commands, panels or settings: the code
|
|
77
|
+
registers them. The other keys are `platforms`, `minAppVersion`, `icon` and
|
|
78
|
+
`dependencies`. `main` and `client` are written by the build.
|
|
79
|
+
|
|
80
|
+
File `src/index.ts` (quick start):
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { defineServer, notify } from '@dolphy-app/extension-sdk';
|
|
84
|
+
|
|
85
|
+
// runs in the extension host: every call registers a contribution
|
|
86
|
+
export const server = defineServer((s) => {
|
|
87
|
+
s.registerCommand({
|
|
88
|
+
id: 'acme.hello.hello',
|
|
89
|
+
title: { en: 'Say hello', ru: 'Поздороваться' },
|
|
90
|
+
run: () => notify('Hello from acme.hello!'),
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`src/index.ts` exports up to two entries. `server` runs in the extension host
|
|
96
|
+
(Node) and registers commands, exercise types, settings, event handlers,
|
|
97
|
+
schedules, importers and exporters; `client` runs in the app window and adds
|
|
98
|
+
panels, injected components, answer views, markdown renderers, themes and
|
|
99
|
+
client commands. This project has only `server`. The host calls it when it
|
|
100
|
+
loads the extension; if it throws, or does not finish in 10 seconds, the
|
|
101
|
+
extension shows `load-failed` in Settings → Extensions and registers nothing
|
|
102
|
+
(see [debugging](debugging.md)).
|
|
103
|
+
|
|
104
|
+
`notify` returns a notification to the app. A text the user sees is a
|
|
105
|
+
`LocalizedText`: a plain string, or `{ en, ru }` as here. An id is written in
|
|
106
|
+
the code and must be the extension id or start with `acme.hello.`; the host
|
|
107
|
+
refuses an id that is taken or does not carry the prefix.
|
|
108
|
+
|
|
109
|
+
When both entries exist, keep each in its own file and re-export them from
|
|
110
|
+
`src/index.ts` (the recipes do): `server` must not import `vue`, `vuetify` or a
|
|
111
|
+
component, `client` must not import `node:*` modules, and the build reports a
|
|
112
|
+
violation with the file and the rule.
|
|
113
|
+
|
|
114
|
+
File `test/index.test.ts` (quick start):
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { createTestServer } from '@dolphy-app/extension-sdk/testing';
|
|
118
|
+
import { expect, it } from 'vitest';
|
|
119
|
+
import { server } from '../src/index.ts';
|
|
120
|
+
|
|
121
|
+
it('the hello command notifies', async () => {
|
|
122
|
+
const running = await createTestServer(server, { extensionId: 'acme.hello' });
|
|
123
|
+
expect(await running.commands.run('acme.hello.hello')).toEqual({
|
|
124
|
+
kind: 'notify',
|
|
125
|
+
text: 'Hello from acme.hello!',
|
|
126
|
+
});
|
|
127
|
+
await running.dispose();
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The test runs the command the way the host does, without the app:
|
|
132
|
+
`createTestServer` from `@dolphy-app/extension-sdk/testing` starts `server` on
|
|
133
|
+
in-memory storage, settings and library, and `running.commands.run` applies the
|
|
134
|
+
rules the host applies to a command result. `createTestClient` does the same for
|
|
135
|
+
`client` (see the [command and panel recipe](recipe-command-panel.md)).
|
|
136
|
+
|
|
137
|
+
## 3. Build, check, test
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
pnpm build # dolphy-ext build: writes dist-ext/acme.hello
|
|
141
|
+
pnpm validate # parses the built manifest the way the app does
|
|
142
|
+
pnpm lint # metadata and bundle checks the catalog review also runs
|
|
143
|
+
pnpm typecheck # tsc
|
|
144
|
+
pnpm test # vitest
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`pnpm build` turns `src/index.ts` into the files the app loads. With this
|
|
148
|
+
template that is `dist-ext/acme.hello/extension.json` and `main.mjs`; the built
|
|
149
|
+
manifest names it in `main`. A project that exports `client` also gets
|
|
150
|
+
`client.mjs` and the `client` key. Keep `dist-ext` out of git; the generated
|
|
151
|
+
`.gitignore` does it.
|
|
152
|
+
|
|
153
|
+
## 4. Try it in the app
|
|
154
|
+
|
|
155
|
+
One command builds the project in watch mode and starts the installed Dolphy
|
|
156
|
+
app on the result:
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
pnpm exec dolphy-ext dev
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The extension appears in Settings → Extensions with the origin "Development",
|
|
163
|
+
and the command appears in the command palette. Press Ctrl+C to stop the build
|
|
164
|
+
and the app. Quit a running Dolphy first: the app has one instance, a second
|
|
165
|
+
start does not pick up the extension directory. Where the app is looked up and
|
|
166
|
+
what to do when it is not found: [debugging](debugging.md), section 3.
|
|
167
|
+
|
|
168
|
+
To run the two parts yourself, start the build in watch mode with `pnpm dev`
|
|
169
|
+
(`dolphy-ext build --watch`) and start Dolphy with the variable
|
|
170
|
+
`DOLPHY_DEV_EXTENSIONS` set to the absolute path of `dist-ext` of your project.
|
|
171
|
+
It adds a developer root with the highest priority.
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
# from a checkout of the Dolphy repository
|
|
175
|
+
DOLPHY_DEV_EXTENSIONS=/path/to/my-extension/dist-ext pnpm dev
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Save a file: the build writes the new files and the app applies them without a
|
|
179
|
+
restart. The window does not reload; the components of extensions in
|
|
180
|
+
development are recreated, so their state can be lost.
|
|
181
|
+
|
|
182
|
+
The extension code runs without restrictions: files, processes, threads and the
|
|
183
|
+
network are available. The test helpers run a handler in your process and do not
|
|
184
|
+
reproduce the host (time limits, the process boundary); try such code in the
|
|
185
|
+
app.
|
|
186
|
+
|
|
187
|
+
## 5. Next
|
|
188
|
+
|
|
189
|
+
- Pick the recipe closest to your task and generate that template.
|
|
190
|
+
- Before a pull request to the extension catalog run `pnpm build`,
|
|
191
|
+
`pnpm validate`, `pnpm lint`, `pnpm typecheck` and `pnpm test`: all must pass.
|
|
192
|
+
The generated `.github/workflows/ci.yml` runs the same steps on every push.
|
|
193
|
+
- Write in `README.md` what the extension does; the catalog
|
|
194
|
+
review reads it.
|