@lvce-editor/api 9.12.0 → 9.14.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.
@@ -1,14 +1,21 @@
1
1
  import { type Rpc } from '@lvce-editor/rpc';
2
- export interface CreateRpcOptions {
2
+ interface CreateRpcBaseOptions {
3
3
  readonly commandMap?: Record<string, unknown>;
4
- readonly contentSecurityPolicy?: string;
4
+ }
5
+ interface CreateDeclaredRpcOptions extends CreateRpcBaseOptions {
6
+ readonly id: string;
7
+ }
8
+ interface CreateLegacyRpcOptions extends CreateRpcBaseOptions {
9
+ readonly contentSecurityPolicy?: readonly string[] | string;
5
10
  readonly name?: string;
6
11
  readonly url: string;
7
12
  }
13
+ export type CreateRpcOptions = CreateDeclaredRpcOptions | CreateLegacyRpcOptions;
8
14
  export interface CreateNodeRpcOptions {
9
15
  readonly id: string;
10
16
  readonly legacyName?: string;
11
17
  readonly legacyPath?: string;
12
18
  }
13
- export declare const createRpc: ({ commandMap, contentSecurityPolicy, name, url }: CreateRpcOptions) => Promise<Rpc>;
19
+ export declare const createRpc: (options: CreateRpcOptions) => Promise<Rpc>;
14
20
  export declare const createNodeRpc: (options: CreateNodeRpcOptions) => Promise<Rpc>;
21
+ export {};
@@ -1,5 +1,23 @@
1
1
  import { MessagePortRpcParent, WebSocketRpcParent } from '@lvce-editor/rpc';
2
2
  import { ExtensionManagementWorker } from '@lvce-editor/rpc-registry';
3
+ const resolveRpcOptions = async (options) => {
4
+ if ('id' in options && options.id) {
5
+ const info = (await ExtensionManagementWorker.invoke('Extensions.getRpcInfo', options.id));
6
+ return {
7
+ contentSecurityPolicy: info.contentSecurityPolicy || '',
8
+ name: info.name || '',
9
+ url: info.url,
10
+ };
11
+ }
12
+ if ('url' in options && options.url) {
13
+ return {
14
+ contentSecurityPolicy: options.contentSecurityPolicy || '',
15
+ name: options.name || '',
16
+ url: options.url,
17
+ };
18
+ }
19
+ throw new TypeError('createRpc requires an id or url');
20
+ };
3
21
  const sendMessagePortToWebWorker = async (port, contentSecurityPolicy, name, url) => {
4
22
  await ExtensionManagementWorker.invokeAndTransfer('Extensions.createWebViewWorkerRpc2', { contentSecurityPolicy, name, url }, port);
5
23
  };
@@ -14,7 +32,9 @@ const createMessagePortRpc = async (commandMap, send) => {
14
32
  await send(port1);
15
33
  return rpcPromise;
16
34
  };
17
- export const createRpc = async ({ commandMap = {}, contentSecurityPolicy = '', name = '', url }) => {
35
+ export const createRpc = async (options) => {
36
+ const { commandMap = {} } = options;
37
+ const { contentSecurityPolicy, name, url } = await resolveRpcOptions(options);
18
38
  return createMessagePortRpc(commandMap, (port) => sendMessagePortToWebWorker(port, contentSecurityPolicy, name, url));
19
39
  };
20
40
  const isMissingCommand = (error, command) => {
@@ -1,4 +1,5 @@
1
1
  import type { VirtualDomNode } from '@lvce-editor/virtual-dom-worker';
2
+ import type { StatusBarItem } from '../StatusBarItem/StatusBarItem.ts';
2
3
  export type ViewKind = 'virtualDom';
3
4
  export type ViewPreferredLocation = 'preview' | 'sideBar';
4
5
  export interface ViewContext {
@@ -56,6 +57,7 @@ export interface VirtualDomViewInstance {
56
57
  readonly renderFocus?: (oldContext: Readonly<Record<string, boolean>>, newContext: Readonly<Record<string, boolean>>) => string | Promise<string>;
57
58
  readonly renderScrollPosition?: () => readonly [] | ViewScrollPosition | Promise<readonly [] | ViewScrollPosition>;
58
59
  readonly renderSelections?: () => readonly ViewSelection[] | Promise<readonly ViewSelection[]>;
60
+ readonly renderStatusBarItems?: () => readonly StatusBarItem[] | Promise<readonly StatusBarItem[]>;
59
61
  readonly renderTitle?: () => string | Promise<string>;
60
62
  readonly saveState?: () => unknown;
61
63
  }
@@ -3,6 +3,7 @@ import { diffTree } from '@lvce-editor/virtual-dom-worker';
3
3
  import { registerCommand } from "../CommandRegistry/CommandRegistry.js";
4
4
  import * as ExtensionApiCommandRegistry from "../ExtensionApiCommandRegistry/ExtensionApiCommandRegistry.js";
5
5
  import { ExtensionApiError } from "../ExtensionApiError/ExtensionApiError.js";
6
+ import * as ViewStatusBarItems from "../ViewStatusBarItems/ViewStatusBarItems.js";
6
7
  const views = Object.create(null);
7
8
  const instances = Object.create(null);
8
9
  const instanceUidsByView = Object.create(null);
@@ -420,12 +421,14 @@ const withScrollPosition = async (result, instance) => {
420
421
  scrollPosition,
421
422
  };
422
423
  };
423
- const withRenderMetadata = async (result, instance, contextChange) => {
424
+ const withRenderMetadata = async (uid, viewId, result, instance, contextChange) => {
424
425
  const resultWithCss = await withCss(result, instance);
425
426
  const resultWithFocus = await withFocusSelector(resultWithCss, instance, contextChange);
426
427
  const resultWithSelections = await withSelections(resultWithFocus, instance);
427
428
  const resultWithScrollPosition = await withScrollPosition(resultWithSelections, instance);
428
- return withTitle(resultWithScrollPosition, instance);
429
+ const resultWithTitle = await withTitle(resultWithScrollPosition, instance);
430
+ await ViewStatusBarItems.renderViewStatusBarItems(uid, viewId, instance);
431
+ return resultWithTitle;
429
432
  };
430
433
  const maybeClearContext = async (uid, viewId) => {
431
434
  if (!contexts[uid]) {
@@ -469,7 +472,7 @@ export const createViewInstance = async (viewId, uid, context) => {
469
472
  type: 'setDom',
470
473
  };
471
474
  const contextChange = await maybeNotifyContextChanged(uid, viewId, instance);
472
- return withRenderMetadata(result, instance, contextChange);
475
+ return withRenderMetadata(uid, viewId, result, instance, contextChange);
473
476
  };
474
477
  export const dispatchViewEvent = async (uid, event) => {
475
478
  const instance = getVirtualDomInstance(uid);
@@ -488,29 +491,38 @@ export const dispatchViewEvent = async (uid, event) => {
488
491
  }
489
492
  const result = await renderPatches(uid, instance);
490
493
  const contextChange = await maybeNotifyContextChanged(uid, contextViewIds[uid], instance);
491
- return withRenderMetadata(result, instance, contextChange);
494
+ return withRenderMetadata(uid, contextViewIds[uid], result, instance, contextChange);
492
495
  };
493
496
  export const renderViewInstance = async (uid) => {
494
497
  const instance = getVirtualDomInstance(uid);
495
498
  const result = await renderPatches(uid, instance);
496
499
  const contextChange = await maybeNotifyContextChanged(uid, contextViewIds[uid], instance);
497
- return withRenderMetadata(result, instance, contextChange);
500
+ return withRenderMetadata(uid, contextViewIds[uid], result, instance, contextChange);
498
501
  };
499
502
  export const disposeViewInstance = async (uid) => {
500
503
  const instance = instances[uid];
501
- if (instance && typeof instance.dispose === 'function') {
502
- await instance.dispose();
503
- }
504
- await maybeClearContext(uid, contextViewIds[uid]);
505
504
  const viewId = contextViewIds[uid];
506
- const instanceUids = instanceUidsByView[viewId];
507
- instanceUids?.delete(uid);
508
- if (instanceUids?.size === 0) {
509
- delete instanceUidsByView[viewId];
505
+ try {
506
+ if (instance && typeof instance.dispose === 'function') {
507
+ await instance.dispose();
508
+ }
509
+ }
510
+ finally {
511
+ ViewStatusBarItems.disposeViewStatusBarItems(uid);
512
+ try {
513
+ await maybeClearContext(uid, viewId);
514
+ }
515
+ finally {
516
+ const instanceUids = instanceUidsByView[viewId];
517
+ instanceUids?.delete(uid);
518
+ if (instanceUids?.size === 0) {
519
+ delete instanceUidsByView[viewId];
520
+ }
521
+ delete instances[uid];
522
+ delete renderedDoms[uid];
523
+ delete contextViewIds[uid];
524
+ }
510
525
  }
511
- delete instances[uid];
512
- delete renderedDoms[uid];
513
- delete contextViewIds[uid];
514
526
  };
515
527
  export const saveViewInstanceState = async (uid) => {
516
528
  const instance = getVirtualDomInstance(uid);
@@ -563,6 +575,7 @@ const commandMap = {
563
575
  'ExtensionApi.saveViewInstanceState': saveViewInstanceState,
564
576
  };
565
577
  export const resetViewRegistry = () => {
578
+ ViewStatusBarItems.resetViewStatusBarItems();
566
579
  for (const id of Object.keys(views)) {
567
580
  for (const disposable of viewCommandDisposables[id] || []) {
568
581
  disposable.dispose();
@@ -0,0 +1,4 @@
1
+ import type { VirtualDomViewInstance } from '../View/View.ts';
2
+ export declare const renderViewStatusBarItems: (uid: number, viewId: string, instance: VirtualDomViewInstance) => Promise<void>;
3
+ export declare const disposeViewStatusBarItems: (uid: number) => void;
4
+ export declare const resetViewStatusBarItems: () => void;
@@ -0,0 +1,90 @@
1
+ import { ExtensionApiError } from "../ExtensionApiError/ExtensionApiError.js";
2
+ import { registerStatusBarItemProvider } from "../StatusBarItemProviderRegistry/StatusBarItemProviderRegistry.js";
3
+ const handlesByUid = Object.create(null);
4
+ const itemsByUid = Object.create(null);
5
+ const stringProperties = ['ariaLabel', 'icon', 'name', 'onClick', 'text', 'title'];
6
+ const normalizeStatusBarItem = (item, index) => {
7
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
8
+ throw new ExtensionApiError(`view status bar item ${index} must be an object`);
9
+ }
10
+ const statusBarItem = item;
11
+ for (const property of stringProperties) {
12
+ if (statusBarItem[property] !== undefined && typeof statusBarItem[property] !== 'string') {
13
+ throw new ExtensionApiError(`view status bar item ${index} has invalid ${property}`);
14
+ }
15
+ }
16
+ if (statusBarItem.spinning !== undefined && typeof statusBarItem.spinning !== 'boolean') {
17
+ throw new ExtensionApiError(`view status bar item ${index} has invalid spinning`);
18
+ }
19
+ return {
20
+ ...(statusBarItem.ariaLabel !== undefined && { ariaLabel: statusBarItem.ariaLabel }),
21
+ ...(statusBarItem.icon !== undefined && { icon: statusBarItem.icon }),
22
+ ...(statusBarItem.name !== undefined && { name: statusBarItem.name }),
23
+ ...(statusBarItem.onClick !== undefined && { onClick: statusBarItem.onClick }),
24
+ ...(statusBarItem.spinning !== undefined && { spinning: statusBarItem.spinning }),
25
+ ...(statusBarItem.text !== undefined && { text: statusBarItem.text }),
26
+ ...(statusBarItem.title !== undefined && { title: statusBarItem.title }),
27
+ };
28
+ };
29
+ const normalizeStatusBarItems = (items) => {
30
+ if (!Array.isArray(items)) {
31
+ throw new ExtensionApiError('view renderStatusBarItems result must be an array');
32
+ }
33
+ return items.map(normalizeStatusBarItem);
34
+ };
35
+ const areItemsEqual = (oldItems, newItems) => {
36
+ if (oldItems.length !== newItems.length) {
37
+ return false;
38
+ }
39
+ return oldItems.every((oldItem, index) => {
40
+ const newItem = newItems[index];
41
+ return (oldItem.ariaLabel === newItem.ariaLabel &&
42
+ oldItem.icon === newItem.icon &&
43
+ oldItem.name === newItem.name &&
44
+ oldItem.onClick === newItem.onClick &&
45
+ oldItem.spinning === newItem.spinning &&
46
+ oldItem.text === newItem.text &&
47
+ oldItem.title === newItem.title);
48
+ });
49
+ };
50
+ const disposeHandles = (uid) => {
51
+ for (const handle of handlesByUid[uid] || []) {
52
+ handle.dispose();
53
+ }
54
+ delete handlesByUid[uid];
55
+ };
56
+ const replaceItems = (uid, viewId, items) => {
57
+ disposeHandles(uid);
58
+ itemsByUid[uid] = items;
59
+ handlesByUid[uid] = items.map((_item, index) => registerStatusBarItemProvider({
60
+ getStatusBarItem() {
61
+ return itemsByUid[uid]?.[index];
62
+ },
63
+ id: `view:${viewId}:${uid}:${index}`,
64
+ }));
65
+ };
66
+ export const renderViewStatusBarItems = async (uid, viewId, instance) => {
67
+ if (typeof instance.renderStatusBarItems !== 'function') {
68
+ return;
69
+ }
70
+ const items = normalizeStatusBarItems(await instance.renderStatusBarItems());
71
+ const oldItems = itemsByUid[uid] || [];
72
+ if (areItemsEqual(oldItems, items)) {
73
+ return;
74
+ }
75
+ if ((handlesByUid[uid]?.length || 0) !== items.length) {
76
+ replaceItems(uid, viewId, items);
77
+ return;
78
+ }
79
+ itemsByUid[uid] = items;
80
+ await Promise.all((handlesByUid[uid] || []).map((handle) => handle.refresh()));
81
+ };
82
+ export const disposeViewStatusBarItems = (uid) => {
83
+ disposeHandles(uid);
84
+ delete itemsByUid[uid];
85
+ };
86
+ export const resetViewStatusBarItems = () => {
87
+ for (const uid of Object.keys(itemsByUid)) {
88
+ disposeViewStatusBarItems(Number(uid));
89
+ }
90
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/api",
3
- "version": "9.12.0",
3
+ "version": "9.14.0",
4
4
  "description": "Tree-shakeable extension API for Lvce Editor extensions.",
5
5
  "keywords": [
6
6
  "lvce-editor",