@lvce-editor/extension-host-worker 8.33.0 → 8.35.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/dist/extension-api/index.js +175 -9
- package/dist/extension-api/parts/ExtensionApiCommandMap/ExtensionApiCommandMap.js +4 -0
- package/dist/extension-api/parts/ViewRegistry/ViewRegistry.js +175 -9
- package/extension-api/dist/index.d.ts +2 -2
- package/extension-api/dist/index.js +1 -1
- package/extension-api/dist/parts/CommandMap/CommandMap.d.ts +2 -0
- package/extension-api/dist/parts/ExtensionApiCommandMap/ExtensionApiCommandMap.d.ts +2 -0
- package/extension-api/dist/parts/ExtensionApiCommandMap/ExtensionApiCommandMap.js +3 -1
- package/extension-api/dist/parts/ExtensionApiWorkerCommandMap/ExtensionApiWorkerCommandMap.d.ts +2 -0
- package/extension-api/dist/parts/View/View.d.ts +27 -0
- package/extension-api/dist/parts/ViewRegistry/ViewRegistry.d.ts +3 -1
- package/extension-api/dist/parts/ViewRegistry/ViewRegistry.js +174 -9
- package/extension-api/package.json +1 -1
- package/extension-api/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { VirtualDomNode } from '@lvce-editor/virtual-dom-worker';
|
|
2
2
|
export type ViewKind = 'virtualDom';
|
|
3
3
|
export interface ViewContext {
|
|
4
|
+
readonly requestRerender: () => Promise<void>;
|
|
5
|
+
readonly showContextMenu: (menuId: string, x: number, y: number) => Promise<void>;
|
|
4
6
|
readonly state?: unknown;
|
|
5
7
|
readonly uid: number;
|
|
6
8
|
readonly viewId: string;
|
|
@@ -9,24 +11,49 @@ export interface ViewEvent {
|
|
|
9
11
|
readonly name?: string;
|
|
10
12
|
readonly type: string;
|
|
11
13
|
readonly value?: unknown;
|
|
14
|
+
readonly x?: number;
|
|
15
|
+
readonly y?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface MenuEntry {
|
|
18
|
+
readonly args?: readonly unknown[];
|
|
19
|
+
readonly command: string;
|
|
20
|
+
readonly flags?: number;
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly label: string;
|
|
23
|
+
}
|
|
24
|
+
export interface DomEventListener {
|
|
25
|
+
readonly capture?: boolean;
|
|
26
|
+
readonly name: string | number;
|
|
27
|
+
readonly params: readonly string[];
|
|
28
|
+
readonly passive?: boolean;
|
|
29
|
+
readonly preventDefault?: boolean;
|
|
30
|
+
readonly stopPropagation?: boolean;
|
|
12
31
|
}
|
|
13
32
|
export interface VirtualDomViewInstance {
|
|
14
33
|
readonly dispose?: () => unknown;
|
|
34
|
+
readonly getContext?: () => Readonly<Record<string, boolean>>;
|
|
35
|
+
readonly getMenuEntries?: (menuId: string) => readonly MenuEntry[] | Promise<readonly MenuEntry[]>;
|
|
15
36
|
readonly handleEvent?: (event: ViewEvent) => unknown;
|
|
16
37
|
readonly render: () => readonly VirtualDomNode[] | Promise<readonly VirtualDomNode[]>;
|
|
17
38
|
readonly saveState?: () => unknown;
|
|
18
39
|
}
|
|
19
40
|
export interface View {
|
|
20
41
|
readonly create: (context?: ViewContext) => unknown;
|
|
42
|
+
readonly displayName?: string;
|
|
43
|
+
readonly eventListeners?: readonly DomEventListener[];
|
|
21
44
|
readonly icon?: string;
|
|
22
45
|
readonly id: string;
|
|
23
46
|
readonly kind?: ViewKind;
|
|
47
|
+
readonly name?: string;
|
|
24
48
|
readonly title?: string;
|
|
25
49
|
}
|
|
26
50
|
export interface RegisteredView {
|
|
51
|
+
readonly displayName?: string;
|
|
52
|
+
readonly eventListeners?: readonly DomEventListener[];
|
|
27
53
|
readonly icon?: string;
|
|
28
54
|
readonly id: string;
|
|
29
55
|
readonly kind?: ViewKind;
|
|
56
|
+
readonly name?: string;
|
|
30
57
|
readonly title?: string;
|
|
31
58
|
}
|
|
32
59
|
export interface ViewRegistrySnapshot {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Disposable } from '../Disposable/Disposable.ts';
|
|
2
|
-
import type { View, ViewContext, ViewEvent, ViewRegistrySnapshot, ViewRenderResult } from '../View/View.ts';
|
|
2
|
+
import type { MenuEntry, View, ViewContext, ViewEvent, ViewRegistrySnapshot, ViewRenderResult } from '../View/View.ts';
|
|
3
3
|
export declare const registerView: (view: View) => Disposable;
|
|
4
4
|
export declare const executeViewProvider: (id: string) => unknown;
|
|
5
5
|
export declare const createViewInstance: (viewId: string, uid: number, context?: ViewContext) => Promise<ViewRenderResult>;
|
|
6
6
|
export declare const dispatchViewEvent: (uid: number, event: ViewEvent) => Promise<ViewRenderResult>;
|
|
7
|
+
export declare const renderViewInstance: (uid: number) => Promise<ViewRenderResult>;
|
|
7
8
|
export declare const disposeViewInstance: (uid: number) => Promise<void>;
|
|
8
9
|
export declare const saveViewInstanceState: (uid: number) => Promise<unknown>;
|
|
10
|
+
export declare const getViewMenuEntries: (uid: number, menuId: string) => Promise<readonly MenuEntry[]>;
|
|
9
11
|
export declare const getViewRegistrySnapshot: () => ViewRegistrySnapshot;
|
|
10
12
|
export declare const resetViewRegistry: () => void;
|
|
@@ -1,8 +1,53 @@
|
|
|
1
|
+
import { ExtensionManagementWorker } from '@lvce-editor/rpc-registry';
|
|
1
2
|
import { diffTree } from '@lvce-editor/virtual-dom-worker';
|
|
2
3
|
import { ExtensionApiError } from "../ExtensionApiError/ExtensionApiError.js";
|
|
3
4
|
const views = Object.create(null);
|
|
4
5
|
const instances = Object.create(null);
|
|
5
6
|
const renderedDoms = Object.create(null);
|
|
7
|
+
const contexts = Object.create(null);
|
|
8
|
+
const contextViewIds = Object.create(null);
|
|
9
|
+
const assertBoolean = (value, message) => {
|
|
10
|
+
if (value !== undefined && typeof value !== 'boolean') {
|
|
11
|
+
throw new ExtensionApiError(message);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const assertNumber = (value, message) => {
|
|
15
|
+
if (typeof value !== 'number') {
|
|
16
|
+
throw new ExtensionApiError(message);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const assertString = (value, message) => {
|
|
20
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
21
|
+
throw new ExtensionApiError(message);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const assertEventListener = (viewId, listener, index) => {
|
|
25
|
+
if (!listener || typeof listener !== 'object') {
|
|
26
|
+
throw new ExtensionApiError(`view ${viewId} event listener ${index} must be an object`);
|
|
27
|
+
}
|
|
28
|
+
const eventListener = listener;
|
|
29
|
+
if (typeof eventListener.name !== 'string' && typeof eventListener.name !== 'number') {
|
|
30
|
+
throw new ExtensionApiError(`view ${viewId} event listener ${index} is missing name`);
|
|
31
|
+
}
|
|
32
|
+
if (!Array.isArray(eventListener.params) || eventListener.params.some((param) => typeof param !== 'string')) {
|
|
33
|
+
throw new ExtensionApiError(`view ${viewId} event listener ${index} is missing params`);
|
|
34
|
+
}
|
|
35
|
+
assertBoolean(eventListener.capture, `view ${viewId} event listener ${index} has invalid capture`);
|
|
36
|
+
assertBoolean(eventListener.passive, `view ${viewId} event listener ${index} has invalid passive`);
|
|
37
|
+
assertBoolean(eventListener.preventDefault, `view ${viewId} event listener ${index} has invalid preventDefault`);
|
|
38
|
+
assertBoolean(eventListener.stopPropagation, `view ${viewId} event listener ${index} has invalid stopPropagation`);
|
|
39
|
+
};
|
|
40
|
+
const assertEventListeners = (view) => {
|
|
41
|
+
if (view.eventListeners === undefined) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (!Array.isArray(view.eventListeners)) {
|
|
45
|
+
throw new ExtensionApiError(`view ${view.id} eventListeners must be an array`);
|
|
46
|
+
}
|
|
47
|
+
for (const [index, listener] of view.eventListeners.entries()) {
|
|
48
|
+
assertEventListener(view.id, listener, index);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
6
51
|
const assertView = (view) => {
|
|
7
52
|
if (!view) {
|
|
8
53
|
throw new ExtensionApiError('view is not defined');
|
|
@@ -16,12 +61,17 @@ const assertView = (view) => {
|
|
|
16
61
|
if (view.id in views) {
|
|
17
62
|
throw new ExtensionApiError(`view ${view.id} is already registered`);
|
|
18
63
|
}
|
|
64
|
+
assertEventListeners(view);
|
|
19
65
|
};
|
|
20
66
|
const toRegisteredView = (view) => {
|
|
67
|
+
const displayName = view.displayName || view.name || view.title;
|
|
21
68
|
const registeredView = {
|
|
69
|
+
displayName,
|
|
70
|
+
...(view.eventListeners && { eventListeners: view.eventListeners }),
|
|
22
71
|
icon: view.icon,
|
|
23
72
|
id: view.id,
|
|
24
|
-
|
|
73
|
+
name: view.name,
|
|
74
|
+
title: displayName,
|
|
25
75
|
};
|
|
26
76
|
if (view.kind) {
|
|
27
77
|
return {
|
|
@@ -69,6 +119,93 @@ const renderDom = async (instance) => {
|
|
|
69
119
|
}
|
|
70
120
|
return dom;
|
|
71
121
|
};
|
|
122
|
+
const normalizeMenuEntry = (entry, index) => {
|
|
123
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
124
|
+
throw new ExtensionApiError(`menu entry ${index} must be an object`);
|
|
125
|
+
}
|
|
126
|
+
const menuEntry = entry;
|
|
127
|
+
assertString(menuEntry.id, `menu entry ${index} is missing id`);
|
|
128
|
+
assertString(menuEntry.label, `menu entry ${index} is missing label`);
|
|
129
|
+
assertString(menuEntry.command, `menu entry ${index} is missing command`);
|
|
130
|
+
if (menuEntry.flags !== undefined && typeof menuEntry.flags !== 'number') {
|
|
131
|
+
throw new ExtensionApiError(`menu entry ${index} has invalid flags`);
|
|
132
|
+
}
|
|
133
|
+
if (menuEntry.args !== undefined && !Array.isArray(menuEntry.args)) {
|
|
134
|
+
throw new ExtensionApiError(`menu entry ${index} has invalid args`);
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
command: menuEntry.command,
|
|
138
|
+
flags: menuEntry.flags ?? 0,
|
|
139
|
+
id: menuEntry.id,
|
|
140
|
+
label: menuEntry.label,
|
|
141
|
+
...(menuEntry.args && { args: menuEntry.args }),
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
const normalizeMenuEntries = (entries) => {
|
|
145
|
+
if (!Array.isArray(entries)) {
|
|
146
|
+
throw new ExtensionApiError('view menu entries must be an array');
|
|
147
|
+
}
|
|
148
|
+
return entries.map(normalizeMenuEntry);
|
|
149
|
+
};
|
|
150
|
+
const renderPatches = async (uid, instance) => {
|
|
151
|
+
const oldDom = renderedDoms[uid] || [];
|
|
152
|
+
const newDom = await renderDom(instance);
|
|
153
|
+
renderedDoms[uid] = newDom;
|
|
154
|
+
const patches = diffTree(oldDom, newDom);
|
|
155
|
+
return {
|
|
156
|
+
patches,
|
|
157
|
+
type: 'setPatches',
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
const normalizeContext = (context) => {
|
|
161
|
+
if (!context || typeof context !== 'object' || Array.isArray(context)) {
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
const normalized = {};
|
|
165
|
+
for (const [key, value] of Object.entries(context)) {
|
|
166
|
+
if (typeof value === 'boolean') {
|
|
167
|
+
normalized[key] = value;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return normalized;
|
|
171
|
+
};
|
|
172
|
+
const isSameContext = (oldContext, newContext) => {
|
|
173
|
+
const oldKeys = Object.keys(oldContext);
|
|
174
|
+
const newKeys = Object.keys(newContext);
|
|
175
|
+
if (oldKeys.length !== newKeys.length) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
for (const key of oldKeys) {
|
|
179
|
+
if (oldContext[key] !== newContext[key]) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return true;
|
|
184
|
+
};
|
|
185
|
+
const maybeNotifyContextChanged = async (uid, viewId, instance) => {
|
|
186
|
+
if (typeof instance.getContext !== 'function') {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const oldContext = contexts[uid] || {};
|
|
190
|
+
const newContext = normalizeContext(instance.getContext());
|
|
191
|
+
if (isSameContext(oldContext, newContext)) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (Object.keys(newContext).length === 0) {
|
|
195
|
+
delete contexts[uid];
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
contexts[uid] = newContext;
|
|
199
|
+
}
|
|
200
|
+
await ExtensionManagementWorker.invoke('Extensions.handleViewContextChange', uid, viewId, newContext);
|
|
201
|
+
};
|
|
202
|
+
const maybeClearContext = async (uid, viewId) => {
|
|
203
|
+
if (!contexts[uid]) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
delete contexts[uid];
|
|
207
|
+
await ExtensionManagementWorker.invoke('Extensions.handleViewContextChange', uid, viewId, {});
|
|
208
|
+
};
|
|
72
209
|
export const createViewInstance = async (viewId, uid, context) => {
|
|
73
210
|
const view = views[viewId];
|
|
74
211
|
if (!view) {
|
|
@@ -79,13 +216,24 @@ export const createViewInstance = async (viewId, uid, context) => {
|
|
|
79
216
|
}
|
|
80
217
|
const instance = await view.create({
|
|
81
218
|
...context,
|
|
219
|
+
requestRerender() {
|
|
220
|
+
return ExtensionManagementWorker.invoke('Extensions.requestViewRerender', uid);
|
|
221
|
+
},
|
|
222
|
+
showContextMenu(menuId, x, y) {
|
|
223
|
+
assertString(menuId, 'menuId must be a string');
|
|
224
|
+
assertNumber(x, 'x must be a number');
|
|
225
|
+
assertNumber(y, 'y must be a number');
|
|
226
|
+
return ExtensionManagementWorker.invoke('Extensions.showViewContextMenu', uid, viewId, menuId, x, y);
|
|
227
|
+
},
|
|
82
228
|
uid,
|
|
83
229
|
viewId,
|
|
84
230
|
});
|
|
85
231
|
assertVirtualDomViewInstance(viewId, instance);
|
|
86
232
|
instances[uid] = instance;
|
|
233
|
+
contextViewIds[uid] = viewId;
|
|
87
234
|
const dom = await renderDom(instance);
|
|
88
235
|
renderedDoms[uid] = dom;
|
|
236
|
+
await maybeNotifyContextChanged(uid, viewId, instance);
|
|
89
237
|
return {
|
|
90
238
|
dom,
|
|
91
239
|
type: 'setDom',
|
|
@@ -96,22 +244,25 @@ export const dispatchViewEvent = async (uid, event) => {
|
|
|
96
244
|
if (typeof instance.handleEvent === 'function') {
|
|
97
245
|
await instance.handleEvent(event);
|
|
98
246
|
}
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
247
|
+
const result = await renderPatches(uid, instance);
|
|
248
|
+
await maybeNotifyContextChanged(uid, contextViewIds[uid], instance);
|
|
249
|
+
return result;
|
|
250
|
+
};
|
|
251
|
+
export const renderViewInstance = async (uid) => {
|
|
252
|
+
const instance = getVirtualDomInstance(uid);
|
|
253
|
+
const result = await renderPatches(uid, instance);
|
|
254
|
+
await maybeNotifyContextChanged(uid, contextViewIds[uid], instance);
|
|
255
|
+
return result;
|
|
107
256
|
};
|
|
108
257
|
export const disposeViewInstance = async (uid) => {
|
|
109
258
|
const instance = instances[uid];
|
|
110
259
|
if (instance && typeof instance.dispose === 'function') {
|
|
111
260
|
await instance.dispose();
|
|
112
261
|
}
|
|
262
|
+
await maybeClearContext(uid, contextViewIds[uid]);
|
|
113
263
|
delete instances[uid];
|
|
114
264
|
delete renderedDoms[uid];
|
|
265
|
+
delete contextViewIds[uid];
|
|
115
266
|
};
|
|
116
267
|
export const saveViewInstanceState = async (uid) => {
|
|
117
268
|
const instance = getVirtualDomInstance(uid);
|
|
@@ -120,6 +271,14 @@ export const saveViewInstanceState = async (uid) => {
|
|
|
120
271
|
}
|
|
121
272
|
return instance.saveState();
|
|
122
273
|
};
|
|
274
|
+
export const getViewMenuEntries = async (uid, menuId) => {
|
|
275
|
+
assertString(menuId, 'menuId must be a string');
|
|
276
|
+
const instance = getVirtualDomInstance(uid);
|
|
277
|
+
if (typeof instance.getMenuEntries !== 'function') {
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
return normalizeMenuEntries(await instance.getMenuEntries(menuId));
|
|
281
|
+
};
|
|
123
282
|
export const getViewRegistrySnapshot = () => {
|
|
124
283
|
return {
|
|
125
284
|
views: Object.values(views).map(toRegisteredView),
|
|
@@ -135,4 +294,10 @@ export const resetViewRegistry = () => {
|
|
|
135
294
|
for (const uid of Object.keys(renderedDoms)) {
|
|
136
295
|
delete renderedDoms[Number(uid)];
|
|
137
296
|
}
|
|
297
|
+
for (const uid of Object.keys(contexts)) {
|
|
298
|
+
delete contexts[Number(uid)];
|
|
299
|
+
}
|
|
300
|
+
for (const uid of Object.keys(contextViewIds)) {
|
|
301
|
+
delete contextViewIds[Number(uid)];
|
|
302
|
+
}
|
|
138
303
|
};
|