@dolphy-app/extension-sdk 0.4.0 → 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/docs/debugging.md CHANGED
@@ -6,32 +6,42 @@ check to the running app. Commands are those of a generated project; see the
6
6
 
7
7
  ## 1. Reproduce it in a test
8
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).
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.
24
30
 
25
31
  ## 2. Run the checks
26
32
 
27
33
  ```sh
28
- pnpm typecheck # ids against extension.json: a misspelt or missing id
34
+ pnpm typecheck # tsc
29
35
  pnpm validate # the manifest the way the app parses it
30
36
  pnpm lint # metadata and bundle findings the catalog review also sees
31
37
  ```
32
38
 
33
39
  `pnpm validate` parses the built manifest with the same code as the app and
34
- exits with code 1 on a problem.
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.
35
45
  `pnpm lint` prints `warning <id> <RULE> <field>: <message>` lines; a rule that
36
46
  looks wrong for your code (for example `eval` in a bundled dependency) is a hint
37
47
  for the reviewer, not a failure.
@@ -66,8 +76,9 @@ If you prefer to run the two parts yourself, start `pnpm dev`
66
76
  (`dolphy-ext build --watch`) and start Dolphy with
67
77
  `DOLPHY_DEV_EXTENSIONS=<project>/dist-ext`; from a checkout of the Dolphy
68
78
  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:
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:
71
82
 
72
83
  - The app has one instance. If Dolphy is already running, a second start exits
73
84
  at once and the variable is lost. `dolphy-ext dev` notices an app that quits
@@ -78,8 +89,29 @@ to know:
78
89
  extension instead of the extension working: start with that text.
79
90
  - If the id is also in the bundled set or installed from the catalog, the
80
91
  development copy wins.
81
- - Answer inputs of extensions in development are recreated on a change, so
82
- their state can be lost.
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.
83
115
 
84
116
  ## 4. DevTools
85
117
 
@@ -88,33 +120,35 @@ build of the app, including an installed one, these keys toggle the DevTools of
88
120
  the main window: `F12`, `Cmd+Alt+I` (macOS) and `Ctrl+Shift+I`. Without the
89
121
  variable the keys do nothing and an installed app has no DevTools.
90
122
 
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.
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`.
95
128
  - `dolphy-ext dev` and `dolphy-ext build --watch` put an inline source map
96
129
  (`//# 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.
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.
103
137
 
104
138
  ## 5. The log
105
139
 
106
- `ctx.logger` has `debug`, `info`, `warn` and `error`; each takes an object of
140
+ `server.logger` has `debug`, `info`, `warn` and `error`; each takes an object of
107
141
  fields and an optional message:
108
142
 
109
143
  ```text
110
- ctx.logger.info({ id: change.id, value: change.value }, 'setting changed');
144
+ s.logger.info({ id: change.id, value: change.value }, 'setting changed');
111
145
  ```
112
146
 
113
147
  The output goes to the log of the app, not to a console of the window. Log
114
148
  structured fields, not secrets or the learner's answers. The host writes to the
115
149
  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.
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.
118
152
 
119
153
  ### Reading the log in the app
120
154
 
@@ -138,7 +172,7 @@ The labels the dialog shows, with the keys of the app's messages (ru and en):
138
172
  | -------------------------- | -------------------------------- | ----------------------------- | ------------------------------------------ |
139
173
  | Settings section | Extensions | Расширения | `settings.extensions.title` |
140
174
  | Origin of a dev extension | Development | Разработка | `settings.extensions.origin.dev` |
141
- | Trust switch | Trust (no isolation) | Доверять (без изоляции) | `settings.extensions.trustLabel` |
175
+ | Load failure of the client part | The extension client part failed to load | Клиентская часть расширения не загрузилась | `settings.extensions.clientFailed.title` |
142
176
  | Block with the log button | Diagnostics | Диагностика | `settings.extensions.support.title` |
143
177
  | Button of the block | Log | Журнал | `settings.extensions.support.openLog` |
144
178
  | Action in an extension row | Log | Журнал | `settings.extensions.log.rowAction` |
@@ -154,18 +188,12 @@ The labels the dialog shows, with the keys of the app's messages (ru and en):
154
188
  | Reread the log | Refresh | Обновить | `settings.extensions.log.refresh` |
155
189
  | Filters match nothing | No entries match the filters. | Нет записей, подходящих под условия. | `settings.extensions.log.empty` |
156
190
 
157
- ### Output of the restricted process
191
+ ### Output of the extension host
158
192
 
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`.
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.
169
197
 
170
198
  ### The file
171
199
 
@@ -177,34 +205,24 @@ relevant lines, or the text of "Copy diagnostics" in the "Diagnostics" block (it
177
205
  has no paths of your home folder, library content or learning data), to a bug
178
206
  report.
179
207
 
180
- ## 6. Permissions and the restricted process
208
+ ## 6. Reading a stack trace
181
209
 
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
210
+ The code of the extension host is `dist-ext/<id>/main.mjs`, readable and not
195
211
  minified. A watch build (`dolphy-ext dev`, `dolphy-ext build --watch`) appends
196
212
  an inline source map to it, but the app does not turn on source maps for that
197
213
  process, so a stack trace in the log still points to lines of `main.mjs`: open
198
214
  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
215
+ map is for the browser file of section 4. A plain `dolphy-ext build` writes no
200
216
  maps at all.
201
217
 
202
218
  ## Which limit did I hit?
203
219
 
204
220
  | Symptom | Limit |
205
221
  | ----------------------------------------------- | -------------------------------------------------------- |
206
- | activation fails with `activation-timeout` | `activate` must finish in 10 seconds |
222
+ | the extension shows `load-failed` with a timeout | `server` must finish in 10 seconds |
207
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 |
208
226
  | `StorageQuotaError` | key 128 characters, value 64 KiB, 256 keys, 1 MiB total |
209
227
  | 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 |
228
+ | a panel or a view fails while it draws | the card "Retry" replaces it; the window stays up |
package/docs/no-build.md CHANGED
@@ -1,11 +1,13 @@
1
1
  # Without a build
2
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.
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.
9
11
 
10
12
  ## The files
11
13
 
@@ -16,44 +18,78 @@ File `extension.json` (no build):
16
18
  "id": "acme.plain",
17
19
  "version": "0.1.0",
18
20
  "apiVersion": 1,
21
+ "main": "./main.mjs",
22
+ "client": "./client.mjs",
19
23
  "name": "Plain hello",
20
- "description": "A palette command written by hand, without a build step.",
24
+ "description": "A palette command and a panel written by hand, without a build step.",
21
25
  "author": "your-github-login",
22
- "tags": ["productivity"],
23
- "contributes": {
24
- "commands": [{ "id": "acme.plain.hello", "title": "Say hello" }]
25
- }
26
+ "tags": ["productivity"]
26
27
  }
27
28
  ```
28
29
 
29
30
  File `main.mjs` (no build):
30
31
 
31
32
  ```js
32
- export default {
33
- activate(ctx) {
34
- ctx.commands.register('acme.plain.hello', () => ({
35
- notify: `Hello from ${ctx.extensionId}!`,
36
- }));
37
- },
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
+ });
38
44
  };
39
45
  ```
40
46
 
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.
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`.
46
82
  - 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).
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`.
53
89
 
54
90
  ## Check and try
55
91
 
56
- Put both files into a directory named exactly as the extension id,
92
+ Put the files into a directory named exactly as the extension id,
57
93
  `acme.plain/` (the app takes the id from the directory name and rejects a
58
94
  manifest that disagrees), and check it:
59
95
 
@@ -61,14 +97,15 @@ manifest that disagrees), and check it:
61
97
  npx --package @dolphy-app/extension-tools dolphy-ext validate ./acme.plain
62
98
  ```
63
99
 
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`.
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`.
70
107
 
71
- To test the module without the app, activate it with the SDK helper:
108
+ To test the server module without the app, run its entry with the SDK helper:
72
109
 
73
110
  ```sh
74
111
  npm install --save-dev @dolphy-app/extension-sdk vitest
@@ -77,23 +114,22 @@ npm install --save-dev @dolphy-app/extension-sdk vitest
77
114
  <!-- fragment -->
78
115
 
79
116
  ```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 …!' }
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!' }
88
123
  ```
89
124
 
90
- `loadCommands` accepts the default export as it is, because the module has the
91
- shape of a host module (`activate`, `deactivate`).
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.
92
128
 
93
129
  ## Publishing
94
130
 
95
131
  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
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
98
134
  yourself or for handing a folder to someone. To publish, generate a project with
99
135
  `create-dolphy-extension` and move the code into it.