@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
package/dist/index.d.ts
CHANGED
|
@@ -1,108 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { t as ExtensionEngine } from "./index-I_CXC6Lj.js";
|
|
2
|
+
import { a as MountContext, c as ServerContext, d as defineExerciseType, f as defineMountable, i as InjectionRegistration, l as ServerEntry, n as ClientContext, o as Mountable, p as defineServer, r as ClientEntry, s as PanelRegistration, t as AppApi, u as defineClient } from "./define-entry-lsuxzKdD.js";
|
|
3
|
+
import { RpcTarget, callRpc, defineRpc } from "./rpc.js";
|
|
4
|
+
import { JsonValue, NotifyEffect, OpenPanelEffect } from "@dolphy-app/extension-api";
|
|
3
5
|
export * from "@dolphy-app/extension-api";
|
|
4
|
-
//#region packages/extension-sdk/src/ids.d.ts
|
|
5
|
-
/**
|
|
6
|
-
* The ids declared in `extension.json`. Empty here: `dolphy-ext types` (and
|
|
7
|
-
* every `dolphy-ext build`) writes `.dolphy/ids.d.ts`, which augments this
|
|
8
|
-
* interface with one key per kind of id:
|
|
9
|
-
*
|
|
10
|
-
* ```ts
|
|
11
|
-
* declare module '@dolphy-app/extension-sdk' {
|
|
12
|
-
* interface ExtensionIds {
|
|
13
|
-
* exerciseTypes: 'acme.echo';
|
|
14
|
-
* commands: 'acme.a' | 'acme.b';
|
|
15
|
-
* settings: { 'acme.goal': number };
|
|
16
|
-
* // …
|
|
17
|
-
* }
|
|
18
|
-
* }
|
|
19
|
-
* ```
|
|
20
|
-
*
|
|
21
|
-
* While the interface is empty (no generated file) every id is a plain
|
|
22
|
-
* `string` and `defineExtension` does not require any record.
|
|
23
|
-
*/
|
|
24
|
-
interface ExtensionIds {}
|
|
25
|
-
type Declared<K extends keyof ExtensionIdSet> = K extends keyof ExtensionIds ? ExtensionIds[K] extends ExtensionIdSet[K] ? ExtensionIds[K] : ExtensionIdSet[K] : ExtensionIdSet[K];
|
|
26
|
-
/** `ExtensionIds` with the plain-string fallback for the kinds it does not declare. */
|
|
27
|
-
type ResolvedIds = { [K in keyof ExtensionIdSet]: Declared<K>; };
|
|
28
|
-
/** Whether the generated declarations are part of the program. */
|
|
29
|
-
type HasGeneratedIds = [keyof ExtensionIds] extends [never] ? false : true;
|
|
30
|
-
/**
|
|
31
|
-
* `ExtensionContext` of this extension: `settings`, `events`, `commands` and
|
|
32
|
-
* the `register…` methods accept the declared ids only.
|
|
33
|
-
*/
|
|
34
|
-
type ExtensionContext = ExtensionContext$1<ResolvedIds>;
|
|
35
|
-
/** Context of a panel module; `call` accepts the declared command ids only. */
|
|
36
|
-
type PanelContext = PanelContext$1<ResolvedIds['commands']>;
|
|
37
|
-
/**
|
|
38
|
-
* A record that holds exactly the declared ids: a missing and an extra key are
|
|
39
|
-
* both compile errors. With no generated declarations any keys are accepted;
|
|
40
|
-
* with none declared of this kind no key is accepted.
|
|
41
|
-
*/
|
|
42
|
-
type Exact<Id extends string, Value> = [HasGeneratedIds] extends [false] ? {
|
|
43
|
-
readonly [key: string]: Value;
|
|
44
|
-
} : [Id] extends [never] ? {
|
|
45
|
-
readonly [key: string]: never;
|
|
46
|
-
} : { readonly [K in Id]: Value; };
|
|
47
|
-
/** `export const views = { … } satisfies ExtensionViews`: one `defineAnswerView` per declared exercise type. */
|
|
48
|
-
type ExtensionViews = Exact<ResolvedIds['exerciseTypes'], AnswerView>;
|
|
49
|
-
/** `export const panels = { … } satisfies ExtensionPanels`: one `defineExtensionPanel` per declared panel. */
|
|
50
|
-
type ExtensionPanels = Exact<ResolvedIds['panels'], PanelModule<HTMLElement, ResolvedIds['commands']>>;
|
|
51
|
-
/** `export const markdown = { … } satisfies ExtensionMarkdown`: one `defineMarkdownRenderer` per declared language. */
|
|
52
|
-
type ExtensionMarkdown = Exact<ResolvedIds['markdownLanguages'], MarkdownRendererModule<HTMLElement>>;
|
|
53
|
-
//#endregion
|
|
54
6
|
//#region packages/extension-sdk/src/commands.d.ts
|
|
55
7
|
/** A command result: the app shows a notification (1–500 characters, as is, no markup). */
|
|
56
8
|
export declare const notify: (text: string) => NotifyEffect;
|
|
57
|
-
/** A command result: the app opens a panel of this extension (a
|
|
58
|
-
export declare const openPanel: (panelId:
|
|
59
|
-
//#endregion
|
|
60
|
-
//#region packages/extension-sdk/src/define-extension.d.ts
|
|
61
|
-
declare const inActivateBrand: unique symbol;
|
|
62
|
-
/** Type of `inActivate`. */
|
|
63
|
-
interface InActivate {
|
|
64
|
-
readonly [inActivateBrand]: true;
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* A record value in `defineExtension` that says "this id is registered in
|
|
68
|
-
* `activate`" (`ctx.commands.register`, `ctx.events.on`,
|
|
69
|
-
* `ctx.registerExerciseType`, `ctx.registerGradePolicy`) rather than by a
|
|
70
|
-
* handler in the record. Needed because the records must name every declared
|
|
71
|
-
* id: a handler that needs `ctx` is written in `activate`, and its id gets this
|
|
72
|
-
* marker in the record.
|
|
73
|
-
*/
|
|
74
|
-
export declare const inActivate: InActivate;
|
|
75
|
-
/** Without generated declarations a record may name any subset: an index signature already does, a record keyed by the event names needs `Partial`. */
|
|
76
|
-
type Lenient<Entries> = string extends keyof Entries ? Entries : Partial<Entries>;
|
|
77
|
-
/**
|
|
78
|
-
* The definition record of one kind of id. With generated declarations the
|
|
79
|
-
* record is required (when the manifest declares any id of the kind) and holds
|
|
80
|
-
* exactly the declared ids: a missing and an extra key are compile errors.
|
|
81
|
-
* Without them every key is accepted and the record is optional.
|
|
82
|
-
*/
|
|
83
|
-
type Section<Name extends string, Id extends string, Entries> = [HasGeneratedIds] extends [false] ? { readonly [N in Name]?: Lenient<Entries>; } : [Id] extends [never] ? { readonly [N in Name]?: never; } : { readonly [N in Name]: Entries; };
|
|
84
|
-
type Ids = ResolvedIds;
|
|
85
|
-
/** Learning-event handlers by event name; the events must be declared in `contributes.events`, the `learning.events` permission is needed. */
|
|
86
|
-
type EventHandlers = { readonly [N in Ids['events']]: LearningEventHandler<N> | InActivate; };
|
|
87
|
-
/**
|
|
88
|
-
* What `defineExtension` takes. `exerciseTypes`, `gradePolicies`, `events` and
|
|
89
|
-
* `commands` name every id `extension.json` declares for them, exactly: a
|
|
90
|
-
* handler, or `inActivate` for an id that `activate` registers.
|
|
91
|
-
*/
|
|
92
|
-
type ExtensionDefinition = Section<'exerciseTypes', Ids['exerciseTypes'], { readonly [K in Ids['exerciseTypes']]: ExerciseTypeHandler | InActivate; }> & Section<'gradePolicies', Ids['gradePolicies'], { readonly [K in Ids['gradePolicies']]: GradePolicyHandler | InActivate; }> & Section<'events', Ids['events'], EventHandlers> & Section<'commands', Ids['commands'], { readonly [K in Ids['commands']]: CommandHandler | InActivate; }> & {
|
|
93
|
-
/** Runs after the records are registered. */
|
|
94
|
-
activate?(context: ExtensionContext): void | Promise<void>;
|
|
95
|
-
deactivate?(): void | Promise<void>;
|
|
96
|
-
};
|
|
97
|
-
export declare const defineExerciseType: <Spec, Answer, View>(handler: ExerciseTypeHandler<Spec, Answer, View>) => ExerciseTypeHandler<Spec, Answer, View>;
|
|
98
|
-
export declare const defineExtension: (declared: ExtensionDefinition) => ExtensionModule;
|
|
99
|
-
//#endregion
|
|
100
|
-
//#region packages/extension-sdk/src/markdown-renderer.d.ts
|
|
101
|
-
/** Entry `markdown[<language>]` in `src/index.ts` (`contributes.markdownRenderers`). */
|
|
102
|
-
export declare const defineMarkdownRenderer: (render: (source: string, container: HTMLElement, context: MarkdownRenderContext) => void | Promise<void>) => MarkdownRendererModule<HTMLElement>;
|
|
103
|
-
//#endregion
|
|
104
|
-
//#region packages/extension-sdk/src/panel.d.ts
|
|
105
|
-
/** An entry of `panels[<panel id>]` in `src/index.ts` (`contributes.panels`); `ctx.call` accepts the declared command ids. */
|
|
106
|
-
export declare const defineExtensionPanel: (module: PanelModule<HTMLElement, ResolvedIds["commands"]>) => PanelModule<HTMLElement, ResolvedIds["commands"]>;
|
|
9
|
+
/** A command result: the app opens a panel of this extension (a panel id of `client.addPanel`); `props` reach the panel as `usePanel().props`. */
|
|
10
|
+
export declare const openPanel: (panelId: string, props?: JsonValue) => OpenPanelEffect;
|
|
107
11
|
//#endregion
|
|
108
|
-
export { type
|
|
12
|
+
export { type AppApi, type ClientContext, type ClientEntry, type ExtensionEngine, type InjectionRegistration, type MountContext, type Mountable, type PanelRegistration, type RpcTarget, type ServerContext, type ServerEntry, callRpc, defineClient, defineExerciseType, defineMountable, defineRpc, defineServer };
|
package/dist/index.js
CHANGED
|
@@ -1,91 +1,16 @@
|
|
|
1
|
+
import { i as defineServer, n as defineExerciseType, r as defineMountable, t as defineClient } from "./define-entry-BTz2F3qv.js";
|
|
2
|
+
import { callRpc, defineRpc } from "./rpc.js";
|
|
3
|
+
|
|
1
4
|
export * from "@dolphy-app/extension-api"
|
|
2
5
|
|
|
3
6
|
//#region packages/extension-sdk/src/commands.ts
|
|
4
7
|
/** A command result: the app shows a notification (1–500 characters, as is, no markup). */
|
|
5
8
|
const notify = (text) => ({ notify: text });
|
|
6
|
-
/** A command result: the app opens a panel of this extension (a
|
|
9
|
+
/** A command result: the app opens a panel of this extension (a panel id of `client.addPanel`); `props` reach the panel as `usePanel().props`. */
|
|
7
10
|
const openPanel = (panelId, props) => props === void 0 ? { openPanel: panelId } : {
|
|
8
11
|
openPanel: panelId,
|
|
9
12
|
props
|
|
10
13
|
};
|
|
11
14
|
|
|
12
15
|
//#endregion
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* A record value in `defineExtension` that says "this id is registered in
|
|
16
|
-
* `activate`" (`ctx.commands.register`, `ctx.events.on`,
|
|
17
|
-
* `ctx.registerExerciseType`, `ctx.registerGradePolicy`) rather than by a
|
|
18
|
-
* handler in the record. Needed because the records must name every declared
|
|
19
|
-
* id: a handler that needs `ctx` is written in `activate`, and its id gets this
|
|
20
|
-
* marker in the record.
|
|
21
|
-
*/
|
|
22
|
-
const inActivate = /*#__PURE__*/ Object.freeze({});
|
|
23
|
-
const defineExerciseType = /* @__NO_SIDE_EFFECTS__ */ (handler) => handler;
|
|
24
|
-
const disposeInReverse = async (registrations) => {
|
|
25
|
-
const errors = [];
|
|
26
|
-
for (const registration of registrations.splice(0).reverse()) try {
|
|
27
|
-
await registration.dispose();
|
|
28
|
-
} catch (error) {
|
|
29
|
-
errors.push(error);
|
|
30
|
-
}
|
|
31
|
-
return errors;
|
|
32
|
-
};
|
|
33
|
-
/** The entries of a record that carry a handler (not `inActivate`). */
|
|
34
|
-
const handlersOf = (record) => Object.entries(record ?? {}).filter((entry) => entry[1] !== inActivate);
|
|
35
|
-
const defineExtension = /* @__NO_SIDE_EFFECTS__ */ (declared) => {
|
|
36
|
-
const definition = declared;
|
|
37
|
-
const registrations = [];
|
|
38
|
-
const register = (context) => {
|
|
39
|
-
for (const [type, handler] of handlersOf(definition.exerciseTypes)) registrations.push(context.registerExerciseType(type, handler));
|
|
40
|
-
for (const [id, handler] of handlersOf(definition.gradePolicies)) registrations.push(context.registerGradePolicy(id, handler));
|
|
41
|
-
for (const [name, handler] of handlersOf(definition.events)) registrations.push(context.events.on(name, handler));
|
|
42
|
-
for (const [id, handler] of handlersOf(definition.commands)) registrations.push(context.commands.register(id, handler));
|
|
43
|
-
};
|
|
44
|
-
const activate = async (context) => {
|
|
45
|
-
try {
|
|
46
|
-
register(context);
|
|
47
|
-
await definition.activate?.(context);
|
|
48
|
-
} catch (error) {
|
|
49
|
-
const disposalErrors = await disposeInReverse(registrations);
|
|
50
|
-
if (disposalErrors.length === 0) throw error;
|
|
51
|
-
throw new AggregateError([error, ...disposalErrors], "activation failed and rollback was incomplete");
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
const deactivate = async () => {
|
|
55
|
-
const failures = [];
|
|
56
|
-
try {
|
|
57
|
-
await definition.deactivate?.();
|
|
58
|
-
} catch (error) {
|
|
59
|
-
failures.push(error);
|
|
60
|
-
}
|
|
61
|
-
const disposalErrors = await disposeInReverse(registrations);
|
|
62
|
-
if (disposalErrors.length > 0) failures.push(new AggregateError(disposalErrors, "failed to dispose contributions"));
|
|
63
|
-
if (failures.length === 1) throw failures[0];
|
|
64
|
-
if (failures.length > 1) throw new AggregateError(failures, "failed to deactivate extension");
|
|
65
|
-
};
|
|
66
|
-
return {
|
|
67
|
-
activate,
|
|
68
|
-
deactivate
|
|
69
|
-
};
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
//#endregion
|
|
73
|
-
//#region packages/extension-sdk/src/answer-view.ts
|
|
74
|
-
/**
|
|
75
|
-
* Entry `views[<exercise kind id>]` in `src/index.ts`. Registers nothing:
|
|
76
|
-
* the custom element with the manifest tag is defined by the build's browser file.
|
|
77
|
-
*/
|
|
78
|
-
const defineAnswerView = /* @__NO_SIDE_EFFECTS__ */ (mount) => ({ mount });
|
|
79
|
-
|
|
80
|
-
//#endregion
|
|
81
|
-
//#region packages/extension-sdk/src/markdown-renderer.ts
|
|
82
|
-
/** Entry `markdown[<language>]` in `src/index.ts` (`contributes.markdownRenderers`). */
|
|
83
|
-
const defineMarkdownRenderer = /* @__NO_SIDE_EFFECTS__ */ (render) => ({ render });
|
|
84
|
-
|
|
85
|
-
//#endregion
|
|
86
|
-
//#region packages/extension-sdk/src/panel.ts
|
|
87
|
-
/** An entry of `panels[<panel id>]` in `src/index.ts` (`contributes.panels`); `ctx.call` accepts the declared command ids. */
|
|
88
|
-
const defineExtensionPanel = /* @__NO_SIDE_EFFECTS__ */ (module) => module;
|
|
89
|
-
|
|
90
|
-
//#endregion
|
|
91
|
-
export { defineAnswerView, defineExerciseType, defineExtension, defineExtensionPanel, defineMarkdownRenderer, inActivate, notify, openPanel };
|
|
16
|
+
export { callRpc, defineClient, defineExerciseType, defineMountable, defineRpc, defineServer, notify, openPanel };
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { t as ExtensionEngine } from "./index-I_CXC6Lj.js";
|
|
2
|
+
import { a as MountContext$1, o as Mountable, t as AppApi$1 } from "./define-entry-lsuxzKdD.js";
|
|
3
|
+
import { AppLocale, AppTheme, InjectionHandle, PanelHandle, RpcContract } from "@dolphy-app/extension-api";
|
|
4
|
+
import { ComponentType } from "react";
|
|
5
|
+
//#region packages/extension-sdk/src/react.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* The `MountContext` of the component drawn by `reactComponent`. `Props` and
|
|
8
|
+
* `Handle` are the ones of the surface: pick them by the way the component is
|
|
9
|
+
* registered. Only inside a component that `reactComponent` draws.
|
|
10
|
+
*/
|
|
11
|
+
export declare const useMountContext: <Props = unknown, Handle = undefined>() => MountContext$1<Props, Handle>;
|
|
12
|
+
/** The window API, the same object as `useApp()` of a Vue component. Only inside a component that `reactComponent` draws. */
|
|
13
|
+
export declare const useApp: () => AppApi$1;
|
|
14
|
+
/** The engine client of the window, the same object as `useEngine()` of a Vue component. Only inside a component that `reactComponent` draws. */
|
|
15
|
+
export declare const useEngine: () => ExtensionEngine;
|
|
16
|
+
/**
|
|
17
|
+
* Binds the contract to the server part of this extension, as `useRpc` of a
|
|
18
|
+
* Vue component does: the returned function validates the input and the
|
|
19
|
+
* answer with the contract and rejects with an `Error` on a failure. The
|
|
20
|
+
* function is stable while the contract is. Only inside a component that
|
|
21
|
+
* `reactComponent` draws.
|
|
22
|
+
*/
|
|
23
|
+
export declare const useRpc: <Input, Output>(contract: RpcContract<Input, Output>) => ((input: Input) => Promise<Output>);
|
|
24
|
+
/**
|
|
25
|
+
* The handle of the panel the component is drawn for: `panelId`, `props`,
|
|
26
|
+
* `context` (the current values; the component renders again when they change)
|
|
27
|
+
* and `call` for the commands of this extension. Only inside a panel.
|
|
28
|
+
*/
|
|
29
|
+
export declare const usePanel: <Commands extends string = string>() => PanelHandle<Commands>;
|
|
30
|
+
/** The handle of the injected component: the `target` element and the `position`. Only inside a component that `client.addInjection` registered. */
|
|
31
|
+
export declare const useInjection: () => InjectionHandle;
|
|
32
|
+
/** The theme the window shows; the component renders again when it changes. Only inside a component that `reactComponent` draws. */
|
|
33
|
+
export declare const useTheme: () => AppTheme;
|
|
34
|
+
/** The language of the window; the component renders again when it changes. Only inside a component that `reactComponent` draws. */
|
|
35
|
+
export declare const useLocale: () => AppLocale;
|
|
36
|
+
export interface ReactComponentOptions {
|
|
37
|
+
/** Draws the component inside `React.StrictMode`; default `false`. */
|
|
38
|
+
strictMode?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Wraps a React component into a `Mountable`: the app gives it an element, the
|
|
42
|
+
* adapter draws `<Component {...ctx.props} />` into it with `createRoot`, draws
|
|
43
|
+
* it again when the props, the theme or the language change and unmounts the
|
|
44
|
+
* root on cleanup. An error of the render goes to `ctx.reportError`. Inside
|
|
45
|
+
* the component `useApp`, `useEngine`, `useRpc`, `usePanel`, `useInjection`,
|
|
46
|
+
* `useTheme`, `useLocale` and `useMountContext` work. `Props` and `Handle` are
|
|
47
|
+
* those of the surface the component is registered for, such as
|
|
48
|
+
* `PanelProps` and `PanelHandle`.
|
|
49
|
+
*/
|
|
50
|
+
export declare const reactComponent: <Props extends object, Handle = undefined>(component: ComponentType<Props>, options?: ReactComponentOptions) => Mountable<Props, Handle>;
|
|
51
|
+
//#endregion
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { r as defineMountable } from "./define-entry-BTz2F3qv.js";
|
|
2
|
+
import { Component, StrictMode, createContext, createElement, useContext, useMemo } from "react";
|
|
3
|
+
import { flushSync } from "react-dom";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
|
|
6
|
+
//#region packages/extension-sdk/src/react.ts
|
|
7
|
+
const DolphyContext = createContext(null);
|
|
8
|
+
const useDolphyState = (hook) => {
|
|
9
|
+
const state = useContext(DolphyContext);
|
|
10
|
+
if (state === null) throw new Error(`${hook}() works inside a component that reactComponent() draws, there is no Dolphy context here`);
|
|
11
|
+
return state;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The `MountContext` of the component drawn by `reactComponent`. `Props` and
|
|
15
|
+
* `Handle` are the ones of the surface: pick them by the way the component is
|
|
16
|
+
* registered. Only inside a component that `reactComponent` draws.
|
|
17
|
+
*/
|
|
18
|
+
const useMountContext = () => {
|
|
19
|
+
const { ctx } = useDolphyState("useMountContext");
|
|
20
|
+
return ctx;
|
|
21
|
+
};
|
|
22
|
+
/** The window API, the same object as `useApp()` of a Vue component. Only inside a component that `reactComponent` draws. */
|
|
23
|
+
const useApp = () => useDolphyState("useApp").ctx.app;
|
|
24
|
+
/** The engine client of the window, the same object as `useEngine()` of a Vue component. Only inside a component that `reactComponent` draws. */
|
|
25
|
+
const useEngine = () => useDolphyState("useEngine").ctx.engine;
|
|
26
|
+
/**
|
|
27
|
+
* Binds the contract to the server part of this extension, as `useRpc` of a
|
|
28
|
+
* Vue component does: the returned function validates the input and the
|
|
29
|
+
* answer with the contract and rejects with an `Error` on a failure. The
|
|
30
|
+
* function is stable while the contract is. Only inside a component that
|
|
31
|
+
* `reactComponent` draws.
|
|
32
|
+
*/
|
|
33
|
+
const useRpc = (contract) => {
|
|
34
|
+
const { ctx } = useDolphyState("useRpc");
|
|
35
|
+
return useMemo(() => (input) => ctx.callRpc(contract, input), [ctx, contract]);
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The handle of the panel the component is drawn for: `panelId`, `props`,
|
|
39
|
+
* `context` (the current values; the component renders again when they change)
|
|
40
|
+
* and `call` for the commands of this extension. Only inside a panel.
|
|
41
|
+
*/
|
|
42
|
+
const usePanel = () => {
|
|
43
|
+
const { handle } = useDolphyState("usePanel").ctx;
|
|
44
|
+
if (typeof handle !== "object" || handle === null || !("panelId" in handle)) throw new Error("usePanel() works inside a panel component that the app draws, there is no panel here");
|
|
45
|
+
return handle;
|
|
46
|
+
};
|
|
47
|
+
/** The handle of the injected component: the `target` element and the `position`. Only inside a component that `client.addInjection` registered. */
|
|
48
|
+
const useInjection = () => {
|
|
49
|
+
const { handle } = useDolphyState("useInjection").ctx;
|
|
50
|
+
if (typeof handle !== "object" || handle === null || !("target" in handle) || !("position" in handle)) throw new Error("useInjection() works inside an injected component that the app draws, there is no injection here");
|
|
51
|
+
return handle;
|
|
52
|
+
};
|
|
53
|
+
/** The theme the window shows; the component renders again when it changes. Only inside a component that `reactComponent` draws. */
|
|
54
|
+
const useTheme = () => useDolphyState("useTheme").theme;
|
|
55
|
+
/** The language of the window; the component renders again when it changes. Only inside a component that `reactComponent` draws. */
|
|
56
|
+
const useLocale = () => useDolphyState("useLocale").locale;
|
|
57
|
+
/** Catches the errors of the render, reports them to the app and draws nothing: the app shows its own card in place of the element. */
|
|
58
|
+
var ErrorBoundary = class extends Component {
|
|
59
|
+
state = { failed: false };
|
|
60
|
+
static getDerivedStateFromError() {
|
|
61
|
+
return { failed: true };
|
|
62
|
+
}
|
|
63
|
+
componentDidCatch(error) {
|
|
64
|
+
this.props.onError(error);
|
|
65
|
+
}
|
|
66
|
+
render() {
|
|
67
|
+
return this.state.failed ? null : this.props.children;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Wraps a React component into a `Mountable`: the app gives it an element, the
|
|
72
|
+
* adapter draws `<Component {...ctx.props} />` into it with `createRoot`, draws
|
|
73
|
+
* it again when the props, the theme or the language change and unmounts the
|
|
74
|
+
* root on cleanup. An error of the render goes to `ctx.reportError`. Inside
|
|
75
|
+
* the component `useApp`, `useEngine`, `useRpc`, `usePanel`, `useInjection`,
|
|
76
|
+
* `useTheme`, `useLocale` and `useMountContext` work. `Props` and `Handle` are
|
|
77
|
+
* those of the surface the component is registered for, such as
|
|
78
|
+
* `PanelProps` and `PanelHandle`.
|
|
79
|
+
*/
|
|
80
|
+
const reactComponent = /* @__NO_SIDE_EFFECTS__ */ (component, options = {}) => /* @__PURE__ */ defineMountable((el, ctx) => {
|
|
81
|
+
let alive = true;
|
|
82
|
+
const root = createRoot(el, {
|
|
83
|
+
onCaughtError: () => void 0,
|
|
84
|
+
onUncaughtError: (error) => ctx.reportError(error)
|
|
85
|
+
});
|
|
86
|
+
const render = () => {
|
|
87
|
+
if (!alive) return;
|
|
88
|
+
const state = {
|
|
89
|
+
ctx,
|
|
90
|
+
theme: ctx.theme,
|
|
91
|
+
locale: ctx.locale
|
|
92
|
+
};
|
|
93
|
+
const tree = createElement(DolphyContext.Provider, { value: state }, createElement(ErrorBoundary, { onError: ctx.reportError }, createElement(component, ctx.props)));
|
|
94
|
+
flushSync(() => {
|
|
95
|
+
root.render(options.strictMode === true ? createElement(StrictMode, null, tree) : tree);
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
render();
|
|
99
|
+
const stops = [
|
|
100
|
+
ctx.onProps(render),
|
|
101
|
+
ctx.onTheme(render),
|
|
102
|
+
ctx.onLocale(render)
|
|
103
|
+
];
|
|
104
|
+
return () => {
|
|
105
|
+
alive = false;
|
|
106
|
+
for (const stop of stops) stop();
|
|
107
|
+
root.unmount();
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
//#endregion
|
|
112
|
+
export { reactComponent, useApp, useEngine, useInjection, useLocale, useMountContext, usePanel, useRpc, useTheme };
|
package/dist/rpc.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { t as ExtensionEngine } from "./index-I_CXC6Lj.js";
|
|
2
|
+
import { RpcContract } from "@dolphy-app/extension-api";
|
|
3
|
+
//#region packages/extension-sdk/src/rpc.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* `export const sayHello = defineRpc({ name, input, output })`: a typed call
|
|
6
|
+
* between the client and the server part of an extension. Put the contract in a
|
|
7
|
+
* module both parts import; the server answers it with `server.handle`, a
|
|
8
|
+
* component calls it with `useRpc`. Returns `contract` as is; it checks the
|
|
9
|
+
* types and that `name` matches `RPC_NAME_PATTERN` and is at most
|
|
10
|
+
* `EXTENSION_RPC_LIMITS.nameLength` characters. Needs neither Vue nor the
|
|
11
|
+
* engine, so server code imports it.
|
|
12
|
+
*/
|
|
13
|
+
export declare const defineRpc: <Input, Output>(contract: RpcContract<Input, Output>) => RpcContract<Input, Output>;
|
|
14
|
+
/** What `callRpc` needs from a context: `ClientContext` and `MountContext` have it. */
|
|
15
|
+
export interface RpcTarget {
|
|
16
|
+
readonly extensionId: string;
|
|
17
|
+
readonly engine: ExtensionEngine;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Calls the server part of the extension without Vue: validates `input` with
|
|
21
|
+
* `contract.input`, calls the handler of `server.handle` through
|
|
22
|
+
* `engine.extensions.invokeRpc` and validates the answer with
|
|
23
|
+
* `contract.output`. A schema violation, an error of the handler and an
|
|
24
|
+
* unavailable server reject the promise with an `Error` that carries the
|
|
25
|
+
* message. `useRpc` and `MountContext.callRpc` do the same.
|
|
26
|
+
*/
|
|
27
|
+
export declare const callRpc: <Input, Output>({ engine, extensionId }: RpcTarget, contract: RpcContract<Input, Output>, input: Input) => Promise<Output>;
|
|
28
|
+
//#endregion
|
package/dist/rpc.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { EXTENSION_RPC_LIMITS, RPC_NAME_PATTERN } from "@dolphy-app/extension-api";
|
|
2
|
+
|
|
3
|
+
//#region packages/extension-sdk/src/rpc.ts
|
|
4
|
+
/**
|
|
5
|
+
* `export const sayHello = defineRpc({ name, input, output })`: a typed call
|
|
6
|
+
* between the client and the server part of an extension. Put the contract in a
|
|
7
|
+
* module both parts import; the server answers it with `server.handle`, a
|
|
8
|
+
* component calls it with `useRpc`. Returns `contract` as is; it checks the
|
|
9
|
+
* types and that `name` matches `RPC_NAME_PATTERN` and is at most
|
|
10
|
+
* `EXTENSION_RPC_LIMITS.nameLength` characters. Needs neither Vue nor the
|
|
11
|
+
* engine, so server code imports it.
|
|
12
|
+
*/
|
|
13
|
+
const defineRpc = (contract) => {
|
|
14
|
+
const { name } = contract;
|
|
15
|
+
if (typeof name !== "string" || name.length > EXTENSION_RPC_LIMITS.nameLength || !RPC_NAME_PATTERN.test(name)) throw new Error(`rpc name '${String(name)}' must match ${RPC_NAME_PATTERN.source} and be at most ${EXTENSION_RPC_LIMITS.nameLength} characters`);
|
|
16
|
+
return contract;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Calls the server part of the extension without Vue: validates `input` with
|
|
20
|
+
* `contract.input`, calls the handler of `server.handle` through
|
|
21
|
+
* `engine.extensions.invokeRpc` and validates the answer with
|
|
22
|
+
* `contract.output`. A schema violation, an error of the handler and an
|
|
23
|
+
* unavailable server reject the promise with an `Error` that carries the
|
|
24
|
+
* message. `useRpc` and `MountContext.callRpc` do the same.
|
|
25
|
+
*/
|
|
26
|
+
const callRpc = async ({ engine, extensionId }, contract, input) => {
|
|
27
|
+
contract.input.parse(input);
|
|
28
|
+
const result = await engine.extensions.invokeRpc({
|
|
29
|
+
extensionId,
|
|
30
|
+
name: contract.name,
|
|
31
|
+
input
|
|
32
|
+
});
|
|
33
|
+
return contract.output.parse(result);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
export { callRpc, defineRpc };
|