@dolphy-app/extension-sdk 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,210 @@
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
+ The helpers of `@dolphy-app/extension-sdk/testing` run your handlers in plain
10
+ Node, with a debugger and `console.log` available. Most mistakes (wrong result
11
+ shape, a setting read before it is set, a handler that throws) show up there.
12
+
13
+ - `loadCommands`, `loadEvents`, `loadExerciseType` and `loadGradePolicy` take a
14
+ `logger` option: pass an object with `debug`, `info`, `warn` and `error`
15
+ methods to see what `ctx.logger` receives.
16
+ - The helpers check what the host checks: the shape of a verdict, a grade of
17
+ 1–5 or `null`, a command result (`notify` text of 1–500 characters, a JSON
18
+ value of at most 64 KiB), a declared id. A message from a helper usually names
19
+ the rule.
20
+ - They do not swallow a handler's failure and do not count the host's time
21
+ limits, so a test that passes does not prove a handler is fast enough: an
22
+ event handler gets 2 seconds, `activate` gets 10.
23
+ - They do not restrict permissions (see section 5).
24
+
25
+ ## 2. Run the checks
26
+
27
+ ```sh
28
+ pnpm typecheck # ids against extension.json: a misspelt or missing id
29
+ pnpm validate # the manifest the way the app parses it
30
+ pnpm lint # metadata and bundle findings the catalog review also sees
31
+ ```
32
+
33
+ `pnpm validate` parses the built manifest with the same code as the app and
34
+ exits with code 1 on a problem.
35
+ `pnpm lint` prints `warning <id> <RULE> <field>: <message>` lines; a rule that
36
+ looks wrong for your code (for example `eval` in a bundled dependency) is a hint
37
+ for the reviewer, not a failure.
38
+
39
+ ## 3. The app: development loop
40
+
41
+ `dolphy-ext dev` builds the project in watch mode and starts the installed
42
+ Dolphy app on the result:
43
+
44
+ ```sh
45
+ pnpm exec dolphy-ext dev # the project in the current directory
46
+ pnpm exec dolphy-ext dev ../my-ext # another project
47
+ ```
48
+
49
+ It prints the path of the app it started and the directory it passes to it
50
+ (`DOLPHY_DEV_EXTENSIONS=<project>/dist-ext`). Press Ctrl+C to stop the build and
51
+ the app; the exit code is 0. The app is looked up in this order:
52
+
53
+ 1. `--app <path>`: a macOS `.app` bundle or an executable file.
54
+ 2. The `DOLPHY_APP` environment variable (same kind of path).
55
+ 3. The standard place of your system: macOS `/Applications/Dolphy.app`, then
56
+ `~/Applications/Dolphy.app`; Windows
57
+ `%LOCALAPPDATA%\Programs\Dolphy\Dolphy.exe`; Linux the newest
58
+ `~/Applications/Dolphy-Linux-*.AppImage` (Linux has no fixed place: keep the
59
+ AppImage there or use `--app`).
60
+
61
+ When none exists the command exits with code 2 and lists the places it looked
62
+ at. A path given with `--app` or `DOLPHY_APP` that does not exist is an error as
63
+ well, not a silent fallback.
64
+
65
+ If you prefer to run the two parts yourself, start `pnpm dev`
66
+ (`dolphy-ext build --watch`) and start Dolphy with
67
+ `DOLPHY_DEV_EXTENSIONS=<project>/dist-ext`; from a checkout of the Dolphy
68
+ repository that is `DOLPHY_DEV_EXTENSIONS=<project>/dist-ext pnpm dev`. The app
69
+ rereads the extensions on every file change and applies the change live. Things
70
+ to know:
71
+
72
+ - The app has one instance. If Dolphy is already running, a second start exits
73
+ at once and the variable is lost. `dolphy-ext dev` notices an app that quits
74
+ within 5 seconds and prints "Dolphy is probably already running: quit it and
75
+ run again" (exit code 1): quit the running app and start again.
76
+ - The extension appears in Settings → Extensions with the origin
77
+ "Development". A manifest or load problem is shown there next to the
78
+ extension instead of the extension working: start with that text.
79
+ - If the id is also in the bundled set or installed from the catalog, the
80
+ development copy wins.
81
+ - Answer inputs of extensions in development are recreated on a change, so
82
+ their state can be lost.
83
+
84
+ ## 4. DevTools
85
+
86
+ While `DOLPHY_DEV_EXTENSIONS` is set (so under `dolphy-ext dev` too), in any
87
+ build of the app, including an installed one, these keys toggle the DevTools of
88
+ the main window: `F12`, `Cmd+Alt+I` (macOS) and `Ctrl+Shift+I`. Without the
89
+ variable the keys do nothing and an installed app has no DevTools.
90
+
91
+ - Views, panels and markdown blocks of an extension run in frames with the
92
+ address `dolphy-ext://<id>/__dolphy/frame.html`. In the console, pick that
93
+ frame in the context drop-down (the one that says "top") to evaluate code in
94
+ your view; in "Elements" the frame is an `<iframe>` with that address.
95
+ - `dolphy-ext dev` and `dolphy-ext build --watch` put an inline source map
96
+ (`//# sourceMappingURL=data:application/json…`) into every bundle, so
97
+ "Sources" shows your TypeScript (`src/index.ts` and the files it imports) for
98
+ views, panels and renderers: set a breakpoint there, `debugger;` works too.
99
+ A plain `dolphy-ext build` (`pnpm build`) and the catalog build never write
100
+ source maps, and the catalog check rejects a submission that has one.
101
+ - The code that runs in the extension process (`main.mjs`: commands, events,
102
+ `activate`) is not in this window: see section 7 and the log.
103
+
104
+ ## 5. The log
105
+
106
+ `ctx.logger` has `debug`, `info`, `warn` and `error`; each takes an object of
107
+ fields and an optional message:
108
+
109
+ ```text
110
+ ctx.logger.info({ id: change.id, value: change.value }, 'setting changed');
111
+ ```
112
+
113
+ The output goes to the log of the app, not to a console of the window. Log
114
+ structured fields, not secrets or the learner's answers. The host writes to the
115
+ same log: an exception in a handler, a handler that outlives its time limit, an
116
+ event dropped from a full queue, and a warning after activation about an id the
117
+ manifest declares but the code did not register.
118
+
119
+ ### Reading the log in the app
120
+
121
+ 1. Open Settings → Extensions. In the "Diagnostics" block press "Log" to see
122
+ everything, or press "Log" in the row of your extension to see only its
123
+ entries (the dialog opens with the id of the extension in the filter).
124
+ 2. The dialog lists the last 500 entries, the newest at the bottom. Each entry
125
+ shows the time, the level, the source (`main`, `engine` or `ext-host`), the
126
+ id of the extension that wrote it and the message. "Details" opens the other
127
+ fields of the entry as JSON.
128
+ 3. Filter by extension: type or pick an id in "Extension" (an id that is not in
129
+ the list is accepted). Filter by level: "Minimum level" hides entries below
130
+ it; the default, "Debug", shows all of them.
131
+ 4. The dialog does not update by itself: press "Refresh" after you triggered
132
+ the code you are watching. "No entries match the filters." means the filters
133
+ hide everything, or nothing was logged yet.
134
+
135
+ The labels the dialog shows, with the keys of the app's messages (ru and en):
136
+
137
+ | Where | English | Русский | Message key |
138
+ | -------------------------- | -------------------------------- | ----------------------------- | ------------------------------------------ |
139
+ | Settings section | Extensions | Расширения | `settings.extensions.title` |
140
+ | Origin of a dev extension | Development | Разработка | `settings.extensions.origin.dev` |
141
+ | Trust switch | Trust (no isolation) | Доверять (без изоляции) | `settings.extensions.trustLabel` |
142
+ | Block with the log button | Diagnostics | Диагностика | `settings.extensions.support.title` |
143
+ | Button of the block | Log | Журнал | `settings.extensions.support.openLog` |
144
+ | Action in an extension row | Log | Журнал | `settings.extensions.log.rowAction` |
145
+ | Dialog title | Log | Журнал | `settings.extensions.log.title` |
146
+ | Extension filter | Extension | Расширение | `settings.extensions.log.filterExtension` |
147
+ | Extension filter, empty | All extensions | Все расширения | `settings.extensions.log.filterExtensionHint` |
148
+ | Level filter | Minimum level | Минимальный уровень | `settings.extensions.log.filterLevel` |
149
+ | Level | Debug | Отладка | `settings.extensions.log.level.debug` |
150
+ | Level | Info | Инфо | `settings.extensions.log.level.info` |
151
+ | Level | Warning | Предупреждение | `settings.extensions.log.level.warn` |
152
+ | Level | Error | Ошибка | `settings.extensions.log.level.error` |
153
+ | Other fields of an entry | Details | Подробности | `settings.extensions.log.details` |
154
+ | Reread the log | Refresh | Обновить | `settings.extensions.log.refresh` |
155
+ | Filters match nothing | No entries match the filters. | Нет записей, подходящих под условия. | `settings.extensions.log.empty` |
156
+
157
+ ### Output of the restricted process
158
+
159
+ An extension that is not trusted runs in a restricted process (section 6). What
160
+ it prints with `console.log`, `console.error` or an uncaught error goes to the
161
+ log as `warn` entries with the extension id and `stream` (`stdout` or `stderr`)
162
+ in "Details". Use `ctx.logger` for anything you want to filter by level; use
163
+ `console` only for a quick look. The output is limited so that one extension
164
+ cannot flood the log: at most 64 KiB in 60 seconds per extension; the rest is
165
+ dropped and one entry "output truncated" with `droppedBytes` closes the window.
166
+ A process that sends a message larger than 1 MiB or more than 200 messages in
167
+ a second is stopped, with an `error` entry whose reason is `ipc-size` or
168
+ `ipc-rate`.
169
+
170
+ ### The file
171
+
172
+ The entries are also in `logs/dolphy-YYYY-MM-DD.log` in the app's data folder,
173
+ one JSON object per line (`level`, `source`, `message`, `at` and the other
174
+ fields). A new file starts every day and at 2 MiB; files older than 7 days are
175
+ removed, and the oldest go first while the folder is over 10 MiB. Attach the
176
+ relevant lines, or the text of "Copy diagnostics" in the "Diagnostics" block (it
177
+ has no paths of your home folder, library content or learning data), to a bug
178
+ report.
179
+
180
+ ## 6. Permissions and the restricted process
181
+
182
+ An extension that is not bundled with the app and not trusted runs in a
183
+ restricted process, and what its `permissions` do not declare is unavailable:
184
+ `ctx.library` throws `PermissionError` without `library.read`; spawning a
185
+ process, a worker thread or a native module fails with `ERR_ACCESS_DENIED`.
186
+ Test helpers do not reproduce this, so a feature that needs a permission must be
187
+ tried in the app. Extensions in development get the permissions their manifest
188
+ declares. To debug without the restrictions, turn on "Trust (no isolation)" for
189
+ the extension in Settings → Extensions; turn it off again before you release,
190
+ because your users will not have it on.
191
+
192
+ ## 7. Reading a stack trace
193
+
194
+ The code of the extension process is `dist-ext/<id>/main.mjs`, readable and not
195
+ minified. A watch build (`dolphy-ext dev`, `dolphy-ext build --watch`) appends
196
+ an inline source map to it, but the app does not turn on source maps for that
197
+ process, so a stack trace in the log still points to lines of `main.mjs`: open
198
+ that file to find the place and search it for the name from the trace. The
199
+ map is for the browser files of section 4. A plain `dolphy-ext build` writes no
200
+ maps at all.
201
+
202
+ ## Which limit did I hit?
203
+
204
+ | Symptom | Limit |
205
+ | ----------------------------------------------- | -------------------------------------------------------- |
206
+ | activation fails with `activation-timeout` | `activate` must finish in 10 seconds |
207
+ | an event handler stops mid-way | 2 seconds per event; the queue holds 100 events |
208
+ | `StorageQuotaError` | key 128 characters, value 64 KiB, 256 keys, 1 MiB total |
209
+ | a command result is rejected | `notify` text 1–500 characters; a result up to 64 KiB |
210
+ | a panel cannot load an image or open a socket | the frame loads only from its own extension, no network |
@@ -0,0 +1,99 @@
1
+ # Without a build
2
+
3
+ An extension is a directory with an `extension.json` and, if it has code, an ES
4
+ module. You do not need TypeScript, a `package.json` or `dolphy-ext` to write
5
+ one. This page shows the smallest case: one palette command, two hand-written
6
+ files. Use it for a quick experiment or when the code is a few lines; switch to
7
+ the [quick start](quick-start.md) layout when you want typed ids, tests and
8
+ bundling.
9
+
10
+ ## The files
11
+
12
+ File `extension.json` (no build):
13
+
14
+ ```json
15
+ {
16
+ "id": "acme.plain",
17
+ "version": "0.1.0",
18
+ "apiVersion": 1,
19
+ "name": "Plain hello",
20
+ "description": "A palette command written by hand, without a build step.",
21
+ "author": "your-github-login",
22
+ "tags": ["productivity"],
23
+ "contributes": {
24
+ "commands": [{ "id": "acme.plain.hello", "title": "Say hello" }]
25
+ }
26
+ }
27
+ ```
28
+
29
+ File `main.mjs` (no build):
30
+
31
+ ```js
32
+ export default {
33
+ activate(ctx) {
34
+ ctx.commands.register('acme.plain.hello', () => ({
35
+ notify: `Hello from ${ctx.extensionId}!`,
36
+ }));
37
+ },
38
+ };
39
+ ```
40
+
41
+ - `main` is left out: for an extension with commands it defaults to `./main.mjs`,
42
+ so the module sits next to the manifest under that name.
43
+ - `export default { activate(ctx) }` is the whole contract of the module. `ctx`
44
+ is the same context as in the typed projects (`commands`, `settings`,
45
+ `storage`, `events`, `logger`, `library`); `deactivate()` is optional.
46
+ - There are no helpers, so a command returns the result object itself:
47
+ `{ notify: text }` is what `notify(text)` makes (the SDK helper does nothing
48
+ more). Every id must be
49
+ declared in the manifest, and a declared id nobody registers produces a
50
+ warning in the log.
51
+ - The module must be plain JavaScript that Node runs as it is: no TypeScript, no
52
+ bare imports of packages (nothing is installed next to it).
53
+
54
+ ## Check and try
55
+
56
+ Put both files into a directory named exactly as the extension id,
57
+ `acme.plain/` (the app takes the id from the directory name and rejects a
58
+ manifest that disagrees), and check it:
59
+
60
+ ```sh
61
+ npx --package @dolphy-app/extension-tools dolphy-ext validate ./acme.plain
62
+ ```
63
+
64
+ `validate` parses the manifest the way the app does and exits with code 1 on a
65
+ problem. To try the extension, put the directory into a folder and start the
66
+ app with `DOLPHY_DEV_EXTENSIONS` pointing at that folder (the folder, not the
67
+ extension directory: it holds one directory per extension), or copy the
68
+ directory to `<userData>/extensions/` and restart the app. Edits to the files are
69
+ picked up without a restart when you use `DOLPHY_DEV_EXTENSIONS`.
70
+
71
+ To test the module without the app, activate it with the SDK helper:
72
+
73
+ ```sh
74
+ npm install --save-dev @dolphy-app/extension-sdk vitest
75
+ ```
76
+
77
+ <!-- fragment -->
78
+
79
+ ```js
80
+ import { loadCommands } from '@dolphy-app/extension-sdk/testing';
81
+ import extension from './main.mjs';
82
+
83
+ const commands = await loadCommands(extension, {
84
+ declaredCommands: ['acme.plain.hello'],
85
+ });
86
+ console.log(await commands.run('acme.plain.hello'));
87
+ // { kind: 'notify', text: 'Hello from …!' }
88
+ ```
89
+
90
+ `loadCommands` accepts the default export as it is, because the module has the
91
+ shape of a host module (`activate`, `deactivate`).
92
+
93
+ ## Publishing
94
+
95
+ The extension catalog reviews a project, not a bare directory: it requires a
96
+ `package.json` with a lock file and a `README.md` that explains what each
97
+ permission is for (`dolphy-ext catalog check`). A hand-written directory is for
98
+ yourself or for handing a folder to someone. To publish, generate a project with
99
+ `create-dolphy-extension` and move the code into it.
@@ -0,0 +1,172 @@
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, no code.
10
+ - [Command and panel](recipe-command-panel.md): palette commands and a screen in an isolated frame.
11
+ - [Events and storage](recipe-event-storage.md): react to learning events and keep data.
12
+ - [Settings](recipe-settings.md): let the user configure the extension.
13
+ - [Without a build](no-build.md): a hand-written `extension.json` and `main.mjs`, no TypeScript.
14
+ - [Debugging](debugging.md): where to look when something does not work.
15
+
16
+ ## What you need
17
+
18
+ - Node.js 22.12 or newer and pnpm.
19
+ - The Dolphy app, to see the extension work. Everything else (build, type
20
+ check, tests) runs without it.
21
+
22
+ ## 1. Create the project
23
+
24
+ ```sh
25
+ npx @dolphy-app/create-extension my-extension --id acme.hello --template blank
26
+ cd my-extension
27
+ pnpm install
28
+ ```
29
+
30
+ `--id` is the extension id: lowercase letters, digits, dots and hyphens. Every
31
+ id the extension declares (commands, panels, settings…) starts with it, so pick
32
+ one that is yours (a publisher prefix, then a name). Without `--id` the id is the
33
+ directory name in kebab-case. The `--template` values:
34
+
35
+ | Template | What you get |
36
+ | --------------- | ------------------------------------------------------------------------------ |
37
+ | `exercise` | a task type with an answer input and a setting (the default) |
38
+ | `theme` | a color theme, no code |
39
+ | `command-panel` | palette commands and a panel |
40
+ | `events` | a learning event handler, storage, commands and a panel |
41
+ | `blank` | one palette command |
42
+
43
+ An unknown name exits with code 2 and lists the available ones.
44
+
45
+ ## 2. The files
46
+
47
+ The project is two files that matter. The manifest declares what the extension
48
+ adds, the code implements it.
49
+
50
+ File `extension.json` (quick start):
51
+
52
+ ```json
53
+ {
54
+ "$schema": "./node_modules/@dolphy-app/extension-api/dist/extension.schema.json",
55
+ "id": "acme.hello",
56
+ "version": "0.1.0",
57
+ "apiVersion": 1,
58
+ "name": "Hello command",
59
+ "description": "A command-palette command that shows a notification.",
60
+ "author": "your-github-login",
61
+ "tags": ["productivity"],
62
+ "contributes": {
63
+ "commands": [{ "id": "acme.hello.hello", "title": "Say hello" }]
64
+ }
65
+ }
66
+ ```
67
+
68
+ `$schema` gives editors completion and checking. `id` is the identity of the
69
+ extension in the app and the catalog; it never changes after the first release.
70
+ Replace `your-github-login` in `author` with your GitHub login before you
71
+ publish. `contributes.commands` declares the command that appears in the palette.
72
+
73
+ File `src/index.ts` (quick start):
74
+
75
+ ```ts
76
+ import { defineExtension, notify } from '@dolphy-app/extension-sdk';
77
+
78
+ // extension code: runs in the extension process of the app
79
+ // the command id comes from extension.json: a misspelt id or a declared id
80
+ // without a handler fails `pnpm typecheck`
81
+ export const host = defineExtension({
82
+ commands: {
83
+ 'acme.hello.hello': () => notify('Hello from acme.hello!'),
84
+ },
85
+ });
86
+ ```
87
+
88
+ `host` is the code that runs in the extension process of the app.
89
+ `defineExtension` takes a handler for every command the manifest declares:
90
+ `notify` returns a notification to the app. The ids are types: `pnpm typecheck`
91
+ (and every build) writes `.dolphy/ids.d.ts` from `extension.json`, so a misspelt
92
+ id, or a declared command without a handler, does not compile.
93
+
94
+ File `test/index.test.ts` (quick start):
95
+
96
+ ```ts
97
+ import { loadCommands } from '@dolphy-app/extension-sdk/testing';
98
+ import { expect, it } from 'vitest';
99
+ import { host } from '../src/index.ts';
100
+
101
+ it('the hello command notifies', async () => {
102
+ const commands = await loadCommands(host, {
103
+ declaredCommands: ['acme.hello.hello'],
104
+ });
105
+ expect(await commands.run('acme.hello.hello')).toEqual({
106
+ kind: 'notify',
107
+ text: 'Hello from acme.hello!',
108
+ });
109
+ await commands.dispose();
110
+ });
111
+ ```
112
+
113
+ The test runs the command the way the host does, without the app:
114
+ `@dolphy-app/extension-sdk/testing` activates `host` with in-memory storage,
115
+ settings and commands.
116
+
117
+ ## 3. Build, check, test
118
+
119
+ ```sh
120
+ pnpm build # dolphy-ext build: writes dist-ext/acme.hello
121
+ pnpm validate # parses the built manifest the way the app does
122
+ pnpm lint # metadata and bundle checks the catalog review also runs
123
+ pnpm typecheck # dolphy-ext types, then tsc
124
+ pnpm test # vitest
125
+ ```
126
+
127
+ `pnpm build` turns `src/index.ts` into the files the app loads. With this
128
+ template that is `dist-ext/acme.hello/extension.json` and `main.mjs`. Keep
129
+ `dist-ext` and `.dolphy` out of git; the generated `.gitignore` does it.
130
+
131
+ ## 4. Try it in the app
132
+
133
+ One command builds the project in watch mode and starts the installed Dolphy
134
+ app on the result:
135
+
136
+ ```sh
137
+ pnpm exec dolphy-ext dev
138
+ ```
139
+
140
+ The extension appears in Settings → Extensions with the origin "development",
141
+ and the command appears in the command palette. Press Ctrl+C to stop the build
142
+ and the app. Quit a running Dolphy first: the app has one instance, a second
143
+ start does not pick up the extension directory. Where the app is looked up and
144
+ what to do when it is not found: [debugging](debugging.md), section 3.
145
+
146
+ To run the two parts yourself, start the build in watch mode with `pnpm dev`
147
+ (`dolphy-ext build --watch`) and start Dolphy with the variable
148
+ `DOLPHY_DEV_EXTENSIONS` set to the absolute path of `dist-ext` of your project.
149
+ It adds a developer root with the highest priority.
150
+
151
+ ```sh
152
+ # from a checkout of the Dolphy repository
153
+ DOLPHY_DEV_EXTENSIONS=/path/to/my-extension/dist-ext pnpm dev
154
+ ```
155
+
156
+ Save a file: the build writes the new files and the app applies them without a
157
+ restart. The window does not reload; answer inputs of extensions in development
158
+ are recreated, so their state can be lost.
159
+
160
+ An extension that is not bundled with the app and not trusted runs in a
161
+ restricted process: what the `permissions` of the manifest do not declare is
162
+ unavailable. The test helpers do not reproduce that; try permission-dependent
163
+ code in the app.
164
+
165
+ ## 5. Next
166
+
167
+ - Pick the recipe closest to your task and generate that template.
168
+ - Before a pull request to the extension catalog run `pnpm build`,
169
+ `pnpm validate`, `pnpm lint`, `pnpm typecheck` and `pnpm test`: all must pass.
170
+ The generated `.github/workflows/ci.yml` runs the same steps on every push.
171
+ - Write in `README.md` what the extension does and what each permission is for;
172
+ the catalog review reads it.