@dolphy-app/extension-sdk 0.0.0-stage → 0.2.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 +23 -2
- package/dist/index.d.ts +36 -0
- package/dist/index.js +166 -0
- package/dist/testing.d.ts +40 -0
- package/dist/testing.js +163 -0
- package/package.json +34 -4
package/README.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @dolphy-app/extension-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
SDK автора расширений: defineExtension, defineAnswerElement, помощники тестирования
|
|
4
|
+
|
|
5
|
+
Версия пакетов равна версии приложения Dolphy, из релиза которого они опубликованы (0.2.0).
|
|
6
|
+
|
|
7
|
+
`@dolphy-app/extension-sdk` — код расширения (`defineExtension`,
|
|
8
|
+
`defineExerciseType`), элемент ответа (`defineAnswerElement`) и помощники
|
|
9
|
+
тестов (`@dolphy-app/extension-sdk/testing`).
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { defineExtension } from '@dolphy-app/extension-sdk';
|
|
13
|
+
import { loadExerciseType } from '@dolphy-app/extension-sdk/testing';
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Установка
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install @dolphy-app/extension-sdk
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Документация
|
|
23
|
+
|
|
24
|
+
[Расширения Dolphy](https://github.com/dolphy-app/dolphy/blob/main/docs/design/extensions.md).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AnswerElementProps, ExerciseTypeHandler, ExtensionContext, ExtensionModule, GradePolicyHandler, MarkdownRenderContext, MarkdownRendererModule } from "@dolphy-app/extension-api";
|
|
2
|
+
export * from "@dolphy-app/extension-api";
|
|
3
|
+
//#region packages/extension-sdk/src/define-extension.d.ts
|
|
4
|
+
interface ExtensionDefinition {
|
|
5
|
+
exerciseTypes?: Readonly<Record<string, ExerciseTypeHandler>>;
|
|
6
|
+
gradePolicies?: Readonly<Record<string, GradePolicyHandler>>;
|
|
7
|
+
/** Вызывается после регистрации `exerciseTypes` и `gradePolicies`. */
|
|
8
|
+
activate?(context: ExtensionContext): void | Promise<void>;
|
|
9
|
+
deactivate?(): void | Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export declare const defineExerciseType: <Spec, Answer, View>(handler: ExerciseTypeHandler<Spec, Answer, View>) => ExerciseTypeHandler<Spec, Answer, View>;
|
|
12
|
+
export declare const defineExtension: (definition: ExtensionDefinition) => ExtensionModule;
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region packages/extension-sdk/src/answer-element.d.ts
|
|
15
|
+
interface AnswerElementApi {
|
|
16
|
+
readonly root: ShadowRoot;
|
|
17
|
+
/** `aria-label` хост-элемента, выставленный приложением; `null`, если нет. */
|
|
18
|
+
readonly label: string | null;
|
|
19
|
+
/** Сообщает приложению текущий ответ: событие `dolphy-answer-change`. */
|
|
20
|
+
setAnswer(value: unknown, complete: boolean): void;
|
|
21
|
+
/** Просит приложение отправить ответ: событие `dolphy-answer-submit`. */
|
|
22
|
+
submit(): void;
|
|
23
|
+
}
|
|
24
|
+
interface AnswerElementInstance {
|
|
25
|
+
/** Вызывается при изменении `view`/`value`/`disabled`/`verdict`. */
|
|
26
|
+
update(props: AnswerElementProps): void;
|
|
27
|
+
destroy?(): void;
|
|
28
|
+
}
|
|
29
|
+
type MountAnswerElement = (api: AnswerElementApi, props: AnswerElementProps) => AnswerElementInstance;
|
|
30
|
+
export declare const defineAnswerElement: (tag: string, mount: MountAnswerElement) => void;
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region packages/extension-sdk/src/markdown-renderer.d.ts
|
|
33
|
+
/** `export default defineMarkdownRenderer(...)` в модуле рендерера содержимого. */
|
|
34
|
+
export declare const defineMarkdownRenderer: (render: (source: string, container: HTMLElement, context: MarkdownRenderContext) => void | Promise<void>) => MarkdownRendererModule<HTMLElement>;
|
|
35
|
+
//#endregion
|
|
36
|
+
export type { AnswerElementApi, AnswerElementInstance, ExtensionDefinition, MountAnswerElement };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { ANSWER_EVENT, ELEMENT_NAME_PATTERN } from "@dolphy-app/extension-api";
|
|
2
|
+
|
|
3
|
+
export * from "@dolphy-app/extension-api"
|
|
4
|
+
|
|
5
|
+
//#region packages/extension-sdk/src/define-extension.ts
|
|
6
|
+
const defineExerciseType = (handler) => handler;
|
|
7
|
+
const disposeInReverse = async (registrations) => {
|
|
8
|
+
const errors = [];
|
|
9
|
+
for (const registration of registrations.splice(0).reverse()) try {
|
|
10
|
+
await registration.dispose();
|
|
11
|
+
} catch (error) {
|
|
12
|
+
errors.push(error);
|
|
13
|
+
}
|
|
14
|
+
return errors;
|
|
15
|
+
};
|
|
16
|
+
const defineExtension = (definition) => {
|
|
17
|
+
const registrations = [];
|
|
18
|
+
const register = (context) => {
|
|
19
|
+
const entries = Object.entries(definition.exerciseTypes ?? {});
|
|
20
|
+
for (const [type, handler] of entries) registrations.push(context.registerExerciseType(type, handler));
|
|
21
|
+
for (const [id, handler] of Object.entries(definition.gradePolicies ?? {})) registrations.push(context.registerGradePolicy(id, handler));
|
|
22
|
+
};
|
|
23
|
+
const activate = async (context) => {
|
|
24
|
+
try {
|
|
25
|
+
register(context);
|
|
26
|
+
await definition.activate?.(context);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
const disposalErrors = await disposeInReverse(registrations);
|
|
29
|
+
if (disposalErrors.length === 0) throw error;
|
|
30
|
+
throw new AggregateError([error, ...disposalErrors], "activation failed and rollback was incomplete");
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const deactivate = async () => {
|
|
34
|
+
const failures = [];
|
|
35
|
+
try {
|
|
36
|
+
await definition.deactivate?.();
|
|
37
|
+
} catch (error) {
|
|
38
|
+
failures.push(error);
|
|
39
|
+
}
|
|
40
|
+
const disposalErrors = await disposeInReverse(registrations);
|
|
41
|
+
if (disposalErrors.length > 0) failures.push(new AggregateError(disposalErrors, "failed to dispose contributions"));
|
|
42
|
+
if (failures.length === 1) throw failures[0];
|
|
43
|
+
if (failures.length > 1) throw new AggregateError(failures, "failed to deactivate extension");
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
activate,
|
|
47
|
+
deactivate
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region packages/extension-sdk/src/answer-element.ts
|
|
53
|
+
const logFailure = (tag, message, error) => {
|
|
54
|
+
console.error({
|
|
55
|
+
error,
|
|
56
|
+
tag
|
|
57
|
+
}, message);
|
|
58
|
+
};
|
|
59
|
+
const createAnswerElementClass = (tag, mount) => class AnswerElement extends HTMLElement {
|
|
60
|
+
#root = this.attachShadow({ mode: "open" });
|
|
61
|
+
#props = {
|
|
62
|
+
view: void 0,
|
|
63
|
+
value: void 0,
|
|
64
|
+
disabled: false,
|
|
65
|
+
verdict: null
|
|
66
|
+
};
|
|
67
|
+
#instance = null;
|
|
68
|
+
#isFlushScheduled = false;
|
|
69
|
+
get view() {
|
|
70
|
+
return this.#props.view;
|
|
71
|
+
}
|
|
72
|
+
set view(view) {
|
|
73
|
+
this.#change({ view });
|
|
74
|
+
}
|
|
75
|
+
get value() {
|
|
76
|
+
return this.#props.value;
|
|
77
|
+
}
|
|
78
|
+
set value(value) {
|
|
79
|
+
this.#change({ value });
|
|
80
|
+
}
|
|
81
|
+
get disabled() {
|
|
82
|
+
return this.#props.disabled;
|
|
83
|
+
}
|
|
84
|
+
set disabled(disabled) {
|
|
85
|
+
this.#change({ disabled });
|
|
86
|
+
}
|
|
87
|
+
get verdict() {
|
|
88
|
+
return this.#props.verdict;
|
|
89
|
+
}
|
|
90
|
+
set verdict(verdict) {
|
|
91
|
+
this.#change({ verdict });
|
|
92
|
+
}
|
|
93
|
+
connectedCallback() {
|
|
94
|
+
if (this.#instance !== null) return;
|
|
95
|
+
const readLabel = () => this.getAttribute("aria-label");
|
|
96
|
+
const api = {
|
|
97
|
+
root: this.#root,
|
|
98
|
+
get label() {
|
|
99
|
+
return readLabel();
|
|
100
|
+
},
|
|
101
|
+
setAnswer: (value, complete) => {
|
|
102
|
+
const detail = {
|
|
103
|
+
value,
|
|
104
|
+
complete
|
|
105
|
+
};
|
|
106
|
+
this.#emit(ANSWER_EVENT.change, detail);
|
|
107
|
+
},
|
|
108
|
+
submit: () => this.#emit(ANSWER_EVENT.submit, void 0)
|
|
109
|
+
};
|
|
110
|
+
try {
|
|
111
|
+
this.#instance = mount(api, Object.freeze({ ...this.#props }));
|
|
112
|
+
} catch (error) {
|
|
113
|
+
logFailure(tag, "answer element failed to mount", error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
disconnectedCallback() {
|
|
117
|
+
const instance = this.#instance;
|
|
118
|
+
this.#instance = null;
|
|
119
|
+
if (instance === null) return;
|
|
120
|
+
try {
|
|
121
|
+
instance.destroy?.();
|
|
122
|
+
} catch (error) {
|
|
123
|
+
logFailure(tag, "answer element failed to destroy", error);
|
|
124
|
+
}
|
|
125
|
+
this.#root.replaceChildren();
|
|
126
|
+
}
|
|
127
|
+
#change(patch) {
|
|
128
|
+
this.#props = {
|
|
129
|
+
...this.#props,
|
|
130
|
+
...patch
|
|
131
|
+
};
|
|
132
|
+
if (this.#instance === null || this.#isFlushScheduled) return;
|
|
133
|
+
this.#isFlushScheduled = true;
|
|
134
|
+
queueMicrotask(() => this.#flush());
|
|
135
|
+
}
|
|
136
|
+
#flush() {
|
|
137
|
+
this.#isFlushScheduled = false;
|
|
138
|
+
const instance = this.#instance;
|
|
139
|
+
if (instance === null) return;
|
|
140
|
+
try {
|
|
141
|
+
instance.update(Object.freeze({ ...this.#props }));
|
|
142
|
+
} catch (error) {
|
|
143
|
+
logFailure(tag, "answer element failed to update", error);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
#emit(name, detail) {
|
|
147
|
+
this.dispatchEvent(new CustomEvent(name, {
|
|
148
|
+
detail,
|
|
149
|
+
bubbles: true,
|
|
150
|
+
composed: true
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
const defineAnswerElement = (tag, mount) => {
|
|
155
|
+
if (!ELEMENT_NAME_PATTERN.test(tag)) throw new TypeError(`invalid custom element name '${tag}'`);
|
|
156
|
+
if (customElements.get(tag) !== void 0) return;
|
|
157
|
+
customElements.define(tag, createAnswerElementClass(tag, mount));
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region packages/extension-sdk/src/markdown-renderer.ts
|
|
162
|
+
/** `export default defineMarkdownRenderer(...)` в модуле рендерера содержимого. */
|
|
163
|
+
const defineMarkdownRenderer = (render) => ({ render });
|
|
164
|
+
|
|
165
|
+
//#endregion
|
|
166
|
+
export { defineAnswerElement, defineExerciseType, defineExtension, defineMarkdownRenderer };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ExtensionLogger, ExtensionModule, GradePolicyInput, GradeResult, GradeValue, JsonSchema, LibraryReader } from "@dolphy-app/extension-api";
|
|
2
|
+
//#region packages/extension-sdk/src/testing.d.ts
|
|
3
|
+
export declare const createMemoryLibrary: (files: Readonly<Record<string, string>>) => LibraryReader;
|
|
4
|
+
export declare const createSchemaValidator: (schema: JsonSchema) => (value: unknown) => string[];
|
|
5
|
+
export interface LoadedExerciseType {
|
|
6
|
+
project(spec: unknown, options?: {
|
|
7
|
+
exerciseId?: string;
|
|
8
|
+
}): Promise<unknown>;
|
|
9
|
+
grade(input: {
|
|
10
|
+
spec: unknown;
|
|
11
|
+
answer: unknown;
|
|
12
|
+
exerciseId?: string;
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
authorMode?: boolean;
|
|
15
|
+
}): Promise<GradeResult>;
|
|
16
|
+
referenceAnswer(spec: unknown, options?: {
|
|
17
|
+
exerciseId?: string;
|
|
18
|
+
}): Promise<{
|
|
19
|
+
found: true;
|
|
20
|
+
answer: unknown;
|
|
21
|
+
} | {
|
|
22
|
+
found: false;
|
|
23
|
+
}>;
|
|
24
|
+
/** Деактивирует модуль расширения. */
|
|
25
|
+
dispose(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export declare const loadExerciseType: (module: ExtensionModule, type: string, options?: {
|
|
28
|
+
library?: LibraryReader;
|
|
29
|
+
logger?: ExtensionLogger;
|
|
30
|
+
}) => Promise<LoadedExerciseType>;
|
|
31
|
+
export interface LoadedGradePolicy {
|
|
32
|
+
evaluate(input: GradePolicyInput): Promise<GradeValue | null>;
|
|
33
|
+
/** Деактивирует модуль расширения. */
|
|
34
|
+
dispose(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
export declare const loadGradePolicy: (module: ExtensionModule, id: string, options?: {
|
|
37
|
+
library?: LibraryReader;
|
|
38
|
+
logger?: ExtensionLogger;
|
|
39
|
+
}) => Promise<LoadedGradePolicy>;
|
|
40
|
+
//#endregion
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
2
|
+
|
|
3
|
+
//#region packages/extension-sdk/src/testing.ts
|
|
4
|
+
const MAX_MESSAGES = 6;
|
|
5
|
+
const MAX_REASON_CHARS = 100;
|
|
6
|
+
const MAX_TEXT_CHARS = 4e3;
|
|
7
|
+
const DEFAULT_EXERCISE_ID = "test::lesson::exercise";
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 2e3;
|
|
9
|
+
const silentLogger = {
|
|
10
|
+
debug: () => void 0,
|
|
11
|
+
info: () => void 0,
|
|
12
|
+
warn: () => void 0,
|
|
13
|
+
error: () => void 0
|
|
14
|
+
};
|
|
15
|
+
const createMemoryLibrary = (files) => {
|
|
16
|
+
const entries = new Map(Object.entries(files));
|
|
17
|
+
return {
|
|
18
|
+
readText: async (path) => {
|
|
19
|
+
const text = entries.get(path);
|
|
20
|
+
if (text === void 0) throw new Error(`ENOENT: ${path}`);
|
|
21
|
+
return text;
|
|
22
|
+
},
|
|
23
|
+
stat: async (path) => {
|
|
24
|
+
const text = entries.get(path);
|
|
25
|
+
if (text === void 0) return null;
|
|
26
|
+
return {
|
|
27
|
+
kind: "file",
|
|
28
|
+
bytes: Buffer.byteLength(text),
|
|
29
|
+
mtimeMs: 0
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
const createSchemaValidator = (schema) => {
|
|
35
|
+
const validate = new Ajv2020({
|
|
36
|
+
allErrors: true,
|
|
37
|
+
strict: false
|
|
38
|
+
}).compile(schema);
|
|
39
|
+
return (value) => {
|
|
40
|
+
if (validate(value)) return [];
|
|
41
|
+
return (validate.errors ?? []).slice(0, MAX_MESSAGES).map((error) => `${error.instancePath || "/"} ${error.message}`);
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
45
|
+
const OUTCOME_KEYS = {
|
|
46
|
+
passed: [
|
|
47
|
+
"outcome",
|
|
48
|
+
"feedback",
|
|
49
|
+
"data"
|
|
50
|
+
],
|
|
51
|
+
failed: [
|
|
52
|
+
"outcome",
|
|
53
|
+
"reason",
|
|
54
|
+
"feedback",
|
|
55
|
+
"detail",
|
|
56
|
+
"data"
|
|
57
|
+
],
|
|
58
|
+
error: [
|
|
59
|
+
"outcome",
|
|
60
|
+
"reason",
|
|
61
|
+
"feedback",
|
|
62
|
+
"data"
|
|
63
|
+
]
|
|
64
|
+
};
|
|
65
|
+
const textProblem = (result, key) => {
|
|
66
|
+
const value = result[key];
|
|
67
|
+
if (value === void 0) return null;
|
|
68
|
+
if (typeof value !== "string") return `'${key}' must be a string`;
|
|
69
|
+
if (value.length > MAX_TEXT_CHARS) return `'${key}' is longer than ${MAX_TEXT_CHARS} characters`;
|
|
70
|
+
return null;
|
|
71
|
+
};
|
|
72
|
+
const reasonProblem = (result) => {
|
|
73
|
+
const { reason } = result;
|
|
74
|
+
if (typeof reason !== "string" || reason.length === 0) return `'reason' must be a non-empty string`;
|
|
75
|
+
if (reason.length > MAX_REASON_CHARS) return `'reason' is longer than ${MAX_REASON_CHARS} characters`;
|
|
76
|
+
return null;
|
|
77
|
+
};
|
|
78
|
+
const findGradeResultProblem = (result) => {
|
|
79
|
+
if (!isRecord(result)) return "result must be an object";
|
|
80
|
+
const { outcome } = result;
|
|
81
|
+
if (outcome !== "passed" && outcome !== "failed" && outcome !== "error") return `unknown outcome ${JSON.stringify(outcome)}`;
|
|
82
|
+
const allowed = OUTCOME_KEYS[outcome];
|
|
83
|
+
const extra = Object.keys(result).find((key) => !allowed.includes(key));
|
|
84
|
+
if (extra !== void 0) return `unexpected key '${extra}'`;
|
|
85
|
+
return (outcome === "passed" ? null : reasonProblem(result)) ?? textProblem(result, "feedback") ?? textProblem(result, "detail");
|
|
86
|
+
};
|
|
87
|
+
const loadExerciseType = async (module, type, options = {}) => {
|
|
88
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
89
|
+
const context = {
|
|
90
|
+
extensionId: "test",
|
|
91
|
+
logger: options.logger ?? silentLogger,
|
|
92
|
+
library: options.library ?? createMemoryLibrary({}),
|
|
93
|
+
registerExerciseType: (registeredType, handler) => {
|
|
94
|
+
handlers.set(registeredType, handler);
|
|
95
|
+
return { dispose: () => void handlers.delete(registeredType) };
|
|
96
|
+
},
|
|
97
|
+
registerGradePolicy: () => ({ dispose: () => void 0 })
|
|
98
|
+
};
|
|
99
|
+
await module.activate(context);
|
|
100
|
+
const handler = handlers.get(type);
|
|
101
|
+
if (handler === void 0) throw new Error(`exercise type '${type}' was not registered`);
|
|
102
|
+
return {
|
|
103
|
+
project: async (spec, { exerciseId = DEFAULT_EXERCISE_ID } = {}) => handler.project({
|
|
104
|
+
exerciseId,
|
|
105
|
+
spec
|
|
106
|
+
}),
|
|
107
|
+
grade: async ({ spec, answer, exerciseId = DEFAULT_EXERCISE_ID, timeoutMs = DEFAULT_TIMEOUT_MS, authorMode = false }) => {
|
|
108
|
+
const result = await handler.grade({
|
|
109
|
+
exerciseId,
|
|
110
|
+
spec,
|
|
111
|
+
answer,
|
|
112
|
+
timeoutMs,
|
|
113
|
+
authorMode
|
|
114
|
+
});
|
|
115
|
+
const problem = findGradeResultProblem(result);
|
|
116
|
+
if (problem !== null) throw new Error(`invalid grade result: ${problem}`);
|
|
117
|
+
return result;
|
|
118
|
+
},
|
|
119
|
+
referenceAnswer: async (spec, { exerciseId = DEFAULT_EXERCISE_ID } = {}) => {
|
|
120
|
+
const answer = await handler.referenceAnswer?.({
|
|
121
|
+
exerciseId,
|
|
122
|
+
spec
|
|
123
|
+
});
|
|
124
|
+
return answer === void 0 ? { found: false } : {
|
|
125
|
+
found: true,
|
|
126
|
+
answer
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
dispose: async () => {
|
|
130
|
+
await module.deactivate?.();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
const isGradeValue = (value) => Number.isInteger(value) && value >= 1 && value <= 5;
|
|
135
|
+
const loadGradePolicy = async (module, id, options = {}) => {
|
|
136
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
137
|
+
const context = {
|
|
138
|
+
extensionId: "test",
|
|
139
|
+
logger: options.logger ?? silentLogger,
|
|
140
|
+
library: options.library ?? createMemoryLibrary({}),
|
|
141
|
+
registerExerciseType: () => ({ dispose: () => void 0 }),
|
|
142
|
+
registerGradePolicy: (registeredId, handler) => {
|
|
143
|
+
handlers.set(registeredId, handler);
|
|
144
|
+
return { dispose: () => void handlers.delete(registeredId) };
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
await module.activate(context);
|
|
148
|
+
const handler = handlers.get(id);
|
|
149
|
+
if (handler === void 0) throw new Error(`grade policy '${id}' was not registered`);
|
|
150
|
+
return {
|
|
151
|
+
evaluate: async (input) => {
|
|
152
|
+
const result = await handler(input);
|
|
153
|
+
if (result !== null && !isGradeValue(result)) throw new Error(`invalid grade policy result: ${JSON.stringify(result)} is not an integer 1..5 or null`);
|
|
154
|
+
return result;
|
|
155
|
+
},
|
|
156
|
+
dispose: async () => {
|
|
157
|
+
await module.deactivate?.();
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
//#endregion
|
|
163
|
+
export { createMemoryLibrary, createSchemaValidator, loadExerciseType, loadGradePolicy };
|
package/package.json
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dolphy-app/extension-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "SDK автора расширений: defineExtension, defineAnswerElement, помощники тестирования",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"default": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./testing": {
|
|
12
|
+
"types": "./dist/testing.d.ts",
|
|
13
|
+
"default": "./dist/testing.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@dolphy-app/extension-api": "0.2.0",
|
|
22
|
+
"ajv": "^8"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=22.12"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/dolphy-app/dolphy.git",
|
|
30
|
+
"directory": "packages/extension-sdk"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public",
|
|
34
|
+
"registry": "https://registry.npmjs.org"
|
|
35
|
+
}
|
|
36
|
+
}
|