@lvce-editor/api 9.37.2 → 9.39.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 CHANGED
@@ -67,3 +67,34 @@ const registration = registerPortProvider({
67
67
  ```
68
68
 
69
69
  Ports queries matching extensions with the current workspace URI. Each extension may register one provider per scheme. Return an empty array when no ports are available, and dispose the registration when deactivating. The provider supplies addresses; the API does not create tunnels or change their visibility. Results from disposed registrations are discarded.
70
+
71
+ ## Authentication providers
72
+
73
+ Extensions can contribute a functional authentication provider with session callbacks. The provider's stable `id` and display `label` identify it; requested scopes are passed to the callbacks, and session results include an account, access token, and granted scopes.
74
+
75
+ ```ts
76
+ import { activate, registerAuthenticationProvider } from '@lvce-editor/api'
77
+
78
+ await activate()
79
+ const registration = registerAuthenticationProvider({
80
+ id: 'example',
81
+ label: 'Example Account',
82
+ async getSessions(scopes) {
83
+ return []
84
+ },
85
+ async createSession(scopes) {
86
+ return {
87
+ id: 'account-session',
88
+ accessToken: 'token',
89
+ account: { id: 'account', label: 'Example Account' },
90
+ scopes,
91
+ }
92
+ },
93
+ async removeSession(sessionId) {},
94
+ })
95
+
96
+ // Dispose the contribution when the extension deactivates.
97
+ registration.dispose()
98
+ ```
99
+
100
+ The editor can call `executeAuthenticationProviderGetSessions`, `executeAuthenticationProviderCreateSession`, and `executeAuthenticationProviderRemoveSession` by provider id. Session callbacks are responsible for account selection, consent, token storage, and revocation.
package/dist/index.d.ts CHANGED
@@ -2,6 +2,9 @@ export { activate } from './parts/Activation/Activation.ts';
2
2
  export { getColorThemeNames } from './parts/ColorTheme/ColorTheme.ts';
3
3
  export { markdownToVirtualDom } from './parts/Markdown/Markdown.ts';
4
4
  export { getAccessToken, type GetAccessTokenOptions } from './parts/Authentication/Authentication.ts';
5
+ export { executeAuthenticationProviderCreateSession, executeAuthenticationProviderGetSessions, executeAuthenticationProviderRemoveSession, getAuthenticationProviderRegistrySnapshot, registerAuthenticationProvider, resetAuthenticationProviderRegistry, } from './parts/AuthenticationProviderRegistry/AuthenticationProviderRegistry.ts';
6
+ export type { AuthenticationAccount, AuthenticationProvider, AuthenticationSession } from './parts/AuthenticationProvider/AuthenticationProvider.ts';
7
+ export type { AuthenticationProviderRegistrySnapshot } from './parts/AuthenticationProviderRegistrySnapshot/AuthenticationProviderRegistrySnapshot.ts';
5
8
  export { createElectronWebContentsView } from './parts/ElectronWebContentsView/ElectronWebContentsView.ts';
6
9
  export { executeCommand } from './parts/ExecuteCommand/ExecuteCommand.ts';
7
10
  export { showFileQuickPick, showQuickInput, showQuickPick } from './parts/QuickPick/QuickPick.ts';
@@ -19,6 +22,7 @@ export { exists, getFileHash, getFileHashes, mkdir, readAsObjectUrl, readDirWith
19
22
  export { registerFileChangeHandler, type FileChangeHandler, type FileChanges } from './parts/FileChangeHandler/FileChangeHandler.ts';
20
23
  export { executeFileSystemProviderGetOpenExternalPath, executeFileSystemProviderIsReadonly, executeFileSystemProviderMkdir, executeFileSystemProviderReadDirWithFileTypes, executeFileSystemProviderReadFile, executeFileSystemProviderRemove, executeFileSystemProviderRename, executeFileSystemProviderWriteFile, getFileSystemProviderRegistrySnapshot, registerFileSystemProvider, resetFileSystemProviderRegistry, } from './parts/FileSystemProviderRegistry/FileSystemProviderRegistry.ts';
21
24
  export { closeUri, confirm, getRecentlyOpenedWorkspaceUris, getWorkspaceFolder, getWorkspaceUri, handleWorkspaceRefresh, openUri, setWorkspaceUri, showErrorMessage, showNotification, type ConfirmOptions, type NotificationType, type ShowErrorMessageOptions, } from './parts/Host/Host.ts';
25
+ export { preparePrettyError } from './parts/PrettyError/PrettyError.ts';
22
26
  export { executeHoverProvider, getHoverProviderRegistrySnapshot, registerHoverProvider, resetHoverProviderRegistry } from './parts/Hover/Hover.ts';
23
27
  export { executeSignatureHelpProvider, getSignatureHelpProviderRegistrySnapshot, registerSignatureHelpProvider, resetSignatureHelpProviderRegistry, } from './parts/SignatureHelp/SignatureHelp.ts';
24
28
  export { getPlatform, getUserDataDir, type Platform } from './parts/Platform/Platform.ts';
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ export { activate } from "./parts/Activation/Activation.js";
2
2
  export { getColorThemeNames } from "./parts/ColorTheme/ColorTheme.js";
3
3
  export { markdownToVirtualDom } from "./parts/Markdown/Markdown.js";
4
4
  export { getAccessToken } from "./parts/Authentication/Authentication.js";
5
+ export { executeAuthenticationProviderCreateSession, executeAuthenticationProviderGetSessions, executeAuthenticationProviderRemoveSession, getAuthenticationProviderRegistrySnapshot, registerAuthenticationProvider, resetAuthenticationProviderRegistry, } from "./parts/AuthenticationProviderRegistry/AuthenticationProviderRegistry.js";
5
6
  export { createElectronWebContentsView } from "./parts/ElectronWebContentsView/ElectronWebContentsView.js";
6
7
  export { executeCommand } from "./parts/ExecuteCommand/ExecuteCommand.js";
7
8
  export { showFileQuickPick, showQuickInput, showQuickPick } from "./parts/QuickPick/QuickPick.js";
@@ -19,6 +20,7 @@ export { exists, getFileHash, getFileHashes, mkdir, readAsObjectUrl, readDirWith
19
20
  export { registerFileChangeHandler } from "./parts/FileChangeHandler/FileChangeHandler.js";
20
21
  export { executeFileSystemProviderGetOpenExternalPath, executeFileSystemProviderIsReadonly, executeFileSystemProviderMkdir, executeFileSystemProviderReadDirWithFileTypes, executeFileSystemProviderReadFile, executeFileSystemProviderRemove, executeFileSystemProviderRename, executeFileSystemProviderWriteFile, getFileSystemProviderRegistrySnapshot, registerFileSystemProvider, resetFileSystemProviderRegistry, } from "./parts/FileSystemProviderRegistry/FileSystemProviderRegistry.js";
21
22
  export { closeUri, confirm, getRecentlyOpenedWorkspaceUris, getWorkspaceFolder, getWorkspaceUri, handleWorkspaceRefresh, openUri, setWorkspaceUri, showErrorMessage, showNotification, } from "./parts/Host/Host.js";
23
+ export { preparePrettyError } from "./parts/PrettyError/PrettyError.js";
22
24
  export { executeHoverProvider, getHoverProviderRegistrySnapshot, registerHoverProvider, resetHoverProviderRegistry } from "./parts/Hover/Hover.js";
23
25
  export { executeSignatureHelpProvider, getSignatureHelpProviderRegistrySnapshot, registerSignatureHelpProvider, resetSignatureHelpProviderRegistry, } from "./parts/SignatureHelp/SignatureHelp.js";
24
26
  export { getPlatform, getUserDataDir } from "./parts/Platform/Platform.js";
@@ -0,0 +1,17 @@
1
+ export interface AuthenticationAccount {
2
+ readonly id: string;
3
+ readonly label: string;
4
+ }
5
+ export interface AuthenticationSession {
6
+ readonly accessToken: string;
7
+ readonly account: AuthenticationAccount;
8
+ readonly id: string;
9
+ readonly scopes: readonly string[];
10
+ }
11
+ export interface AuthenticationProvider {
12
+ readonly createSession: (scopes: readonly string[]) => Promise<AuthenticationSession> | AuthenticationSession;
13
+ readonly getSessions: (scopes: readonly string[]) => Promise<readonly AuthenticationSession[]> | readonly AuthenticationSession[];
14
+ readonly id: string;
15
+ readonly label: string;
16
+ readonly removeSession: (sessionId: string) => Promise<void> | void;
17
+ }
@@ -0,0 +1,9 @@
1
+ import type { AuthenticationProvider, AuthenticationSession } from '../AuthenticationProvider/AuthenticationProvider.ts';
2
+ import type { AuthenticationProviderRegistrySnapshot } from '../AuthenticationProviderRegistrySnapshot/AuthenticationProviderRegistrySnapshot.ts';
3
+ import type { Disposable } from '../Disposable/Disposable.ts';
4
+ export declare const registerAuthenticationProvider: (provider: AuthenticationProvider) => Disposable;
5
+ export declare const executeAuthenticationProviderGetSessions: (providerId: string, scopes: readonly string[]) => Promise<readonly AuthenticationSession[]>;
6
+ export declare const executeAuthenticationProviderCreateSession: (providerId: string, scopes: readonly string[]) => Promise<AuthenticationSession>;
7
+ export declare const executeAuthenticationProviderRemoveSession: (providerId: string, sessionId: string) => Promise<void>;
8
+ export declare const getAuthenticationProviderRegistrySnapshot: () => AuthenticationProviderRegistrySnapshot;
9
+ export declare const resetAuthenticationProviderRegistry: () => void;
@@ -0,0 +1,98 @@
1
+ import * as ExtensionApiCommandRegistry from "../ExtensionApiCommandRegistry/ExtensionApiCommandRegistry.js";
2
+ import { ExtensionApiError } from "../ExtensionApiError/ExtensionApiError.js";
3
+ const providers = Object.create(null);
4
+ const assertSession = (session, providerId) => {
5
+ if (!session || typeof session !== 'object') {
6
+ throw new ExtensionApiError(`authentication provider ${providerId} returned an invalid session`);
7
+ }
8
+ const value = session;
9
+ if (typeof value.id !== 'string' || value.id.length === 0) {
10
+ throw new ExtensionApiError(`authentication provider ${providerId} returned a session without an id`);
11
+ }
12
+ if (typeof value.accessToken !== 'string' || value.accessToken.length === 0) {
13
+ throw new ExtensionApiError(`authentication provider ${providerId} returned a session without an access token`);
14
+ }
15
+ if (!Array.isArray(value.scopes) || value.scopes.some((scope) => typeof scope !== 'string' || scope.length === 0)) {
16
+ throw new ExtensionApiError(`authentication provider ${providerId} returned a session with invalid scopes`);
17
+ }
18
+ if (!value.account || typeof value.account !== 'object') {
19
+ throw new ExtensionApiError(`authentication provider ${providerId} returned a session without an account`);
20
+ }
21
+ const account = value.account;
22
+ if (typeof account.id !== 'string' || account.id.length === 0 || typeof account.label !== 'string' || account.label.length === 0) {
23
+ throw new ExtensionApiError(`authentication provider ${providerId} returned a session with an invalid account`);
24
+ }
25
+ return session;
26
+ };
27
+ const assertScopes = (scopes) => {
28
+ if (!Array.isArray(scopes) || scopes.some((scope) => !(typeof scope === 'string' && scope.length > 0))) {
29
+ throw new ExtensionApiError('authentication scopes must be an array of non-empty strings');
30
+ }
31
+ };
32
+ const getProvider = (providerId) => {
33
+ const provider = providers[providerId];
34
+ if (!provider) {
35
+ throw new ExtensionApiError(`authentication provider ${providerId} not found`);
36
+ }
37
+ return provider;
38
+ };
39
+ export const registerAuthenticationProvider = (provider) => {
40
+ if (!provider) {
41
+ throw new ExtensionApiError('authentication provider is not defined');
42
+ }
43
+ const providerRecord = provider;
44
+ if (typeof providerRecord.id !== 'string' || providerRecord.id.length === 0) {
45
+ throw new ExtensionApiError('authentication provider is missing id');
46
+ }
47
+ const { id } = provider;
48
+ if (typeof providerRecord.label !== 'string' || providerRecord.label.length === 0) {
49
+ throw new ExtensionApiError(`authentication provider ${id} is missing label`);
50
+ }
51
+ for (const methodName of ['getSessions', 'createSession', 'removeSession']) {
52
+ if (typeof providerRecord[methodName] !== 'function') {
53
+ throw new ExtensionApiError(`authentication provider ${id} is missing ${methodName} function`);
54
+ }
55
+ }
56
+ if (id in providers) {
57
+ throw new ExtensionApiError(`authentication provider ${id} is already registered`);
58
+ }
59
+ providers[id] = provider;
60
+ ExtensionApiCommandRegistry.registerCommandMap(commandMap);
61
+ return {
62
+ dispose() {
63
+ delete providers[id];
64
+ },
65
+ };
66
+ };
67
+ export const executeAuthenticationProviderGetSessions = async (providerId, scopes) => {
68
+ assertScopes(scopes);
69
+ const sessions = await getProvider(providerId).getSessions(scopes);
70
+ if (!Array.isArray(sessions)) {
71
+ throw new ExtensionApiError(`authentication provider ${providerId} returned invalid sessions`);
72
+ }
73
+ return sessions.map((session) => assertSession(session, providerId));
74
+ };
75
+ export const executeAuthenticationProviderCreateSession = async (providerId, scopes) => {
76
+ assertScopes(scopes);
77
+ return assertSession(await getProvider(providerId).createSession(scopes), providerId);
78
+ };
79
+ export const executeAuthenticationProviderRemoveSession = async (providerId, sessionId) => {
80
+ if (typeof sessionId !== 'string' || sessionId.length === 0) {
81
+ throw new ExtensionApiError('authentication session id is missing');
82
+ }
83
+ await getProvider(providerId).removeSession(sessionId);
84
+ };
85
+ export const getAuthenticationProviderRegistrySnapshot = () => ({
86
+ providers: Object.values(providers).map(({ id, label }) => ({ id, label })),
87
+ });
88
+ export const resetAuthenticationProviderRegistry = () => {
89
+ for (const id of Object.keys(providers)) {
90
+ delete providers[id];
91
+ }
92
+ };
93
+ const commandMap = {
94
+ 'ExtensionApi.executeAuthenticationProviderCreateSession': executeAuthenticationProviderCreateSession,
95
+ 'ExtensionApi.executeAuthenticationProviderGetSessions': executeAuthenticationProviderGetSessions,
96
+ 'ExtensionApi.executeAuthenticationProviderRemoveSession': executeAuthenticationProviderRemoveSession,
97
+ 'ExtensionApi.getAuthenticationProviderRegistrySnapshot': getAuthenticationProviderRegistrySnapshot,
98
+ };
@@ -0,0 +1,6 @@
1
+ export interface AuthenticationProviderRegistrySnapshot {
2
+ readonly providers: readonly {
3
+ readonly id: string;
4
+ readonly label: string;
5
+ }[];
6
+ }
@@ -0,0 +1,7 @@
1
+ export interface PrettyError {
2
+ readonly codeFrame?: string;
3
+ readonly message?: string;
4
+ readonly name?: string;
5
+ readonly stack?: string;
6
+ }
7
+ export declare const preparePrettyError: (error: unknown) => Promise<PrettyError>;
@@ -0,0 +1,35 @@
1
+ import { executeCommand } from "../ExecuteCommand/ExecuteCommand.js";
2
+ const serializeError = (error) => {
3
+ if (error instanceof Error) {
4
+ return {
5
+ code: error.code,
6
+ codeFrame: error.codeFrame,
7
+ constructor: { name: error.constructor.name },
8
+ message: error.message,
9
+ name: error.name,
10
+ stack: error.stack,
11
+ };
12
+ }
13
+ if (error && typeof error === 'object') {
14
+ const value = error;
15
+ const constructorName = Object.hasOwn(value, 'constructor') ? value.constructor?.name : undefined;
16
+ return {
17
+ code: value.code,
18
+ codeFrame: value.codeFrame,
19
+ constructor: constructorName ? { name: constructorName } : undefined,
20
+ message: value.message || value.errorMessage,
21
+ name: value.name || constructorName,
22
+ stack: value.stack || value.errorStack,
23
+ };
24
+ }
25
+ return { message: String(error) };
26
+ };
27
+ export const preparePrettyError = async (error) => {
28
+ const serialized = serializeError(error);
29
+ try {
30
+ return (await executeCommand('ErrorHandling.preparePrettyError', serialized));
31
+ }
32
+ catch {
33
+ return serialized;
34
+ }
35
+ };
@@ -51,6 +51,8 @@ export interface VirtualDomViewInstance {
51
51
  readonly getCss?: () => string | Promise<string>;
52
52
  readonly getMenuEntries?: (menuId: string) => readonly MenuEntry[] | Promise<readonly MenuEntry[]>;
53
53
  readonly handleEvent?: (event: ViewEvent) => unknown;
54
+ /** Whether this document has edits that have not been saved. */
55
+ readonly isDirty?: () => boolean;
54
56
  readonly render: () => readonly VirtualDomNode[] | Promise<readonly VirtualDomNode[]>;
55
57
  readonly renderActions?: () => readonly ViewAction[] | Promise<readonly ViewAction[]>;
56
58
  readonly renderActionsDom?: () => readonly VirtualDomNode[];
@@ -59,6 +61,8 @@ export interface VirtualDomViewInstance {
59
61
  readonly renderSelections?: () => readonly ViewSelection[] | Promise<readonly ViewSelection[]>;
60
62
  readonly renderStatusBarItems?: () => readonly StatusBarItem[] | Promise<readonly StatusBarItem[]>;
61
63
  readonly renderTitle?: () => string | Promise<string>;
64
+ /** Persist committed edits. Reject on failure and retain dirty state until persistence succeeds. */
65
+ readonly save?: () => void | Promise<void>;
62
66
  readonly saveState?: () => unknown;
63
67
  }
64
68
  export type ViewCommand<State = VirtualDomViewInstance, Args extends readonly any[] = readonly any[]> = (state: State, ...args: Args) => State | Promise<State>;
@@ -87,6 +91,7 @@ export interface StatefulView<State = unknown> extends ViewBase<State> {
87
91
  readonly getCss?: (state: State) => string | Promise<string>;
88
92
  readonly getMenuEntries?: (state: State, menuId: string) => readonly MenuEntry[] | Promise<readonly MenuEntry[]>;
89
93
  readonly handleEvent?: (state: State, event: ViewEvent) => State | Promise<State>;
94
+ readonly isDirty?: (state: State) => boolean;
90
95
  readonly render: (state: State) => readonly VirtualDomNode[] | Promise<readonly VirtualDomNode[]>;
91
96
  readonly renderActions?: (state: State) => readonly ViewAction[] | Promise<readonly ViewAction[]>;
92
97
  readonly renderActionsDom?: (state: State) => readonly VirtualDomNode[];
@@ -95,6 +100,7 @@ export interface StatefulView<State = unknown> extends ViewBase<State> {
95
100
  readonly renderSelections?: (state: State) => readonly ViewSelection[] | Promise<readonly ViewSelection[]>;
96
101
  readonly renderStatusBarItems?: (state: State) => readonly StatusBarItem[] | Promise<readonly StatusBarItem[]>;
97
102
  readonly renderTitle?: (state: State) => string | Promise<string>;
103
+ readonly save?: (state: State) => State | Promise<State>;
98
104
  readonly saveState?: (state: State) => unknown;
99
105
  }
100
106
  export type View<State = unknown, ComponentState = unknown> = InstanceView<State, ComponentState> | StatefulView<State>;
@@ -116,6 +122,7 @@ export interface ViewRenderResultDom {
116
122
  readonly css?: string;
117
123
  readonly dom: readonly VirtualDomNode[];
118
124
  readonly focusSelector?: string;
125
+ readonly modified?: boolean;
119
126
  readonly scrollPosition?: ViewScrollPosition;
120
127
  readonly selections?: readonly ViewSelection[];
121
128
  readonly title?: string;
@@ -124,6 +131,7 @@ export interface ViewRenderResultDom {
124
131
  export interface ViewRenderResultPatches {
125
132
  readonly css?: string;
126
133
  readonly focusSelector?: string;
134
+ readonly modified?: boolean;
127
135
  readonly patches: readonly unknown[];
128
136
  readonly scrollPosition?: ViewScrollPosition;
129
137
  readonly selections?: readonly ViewSelection[];
@@ -236,6 +236,16 @@ const createStatefulViewInstance = (view, uid) => {
236
236
  ViewletStates.replace(uid, newState);
237
237
  };
238
238
  }
239
+ if (view.isDirty) {
240
+ instance.isDirty = () => view.isDirty(ViewletStates.get(uid));
241
+ }
242
+ if (view.save) {
243
+ instance.save = async () => {
244
+ const newState = await view.save(ViewletStates.get(uid));
245
+ assertViewState(view.id, newState);
246
+ ViewletStates.replace(uid, newState);
247
+ };
248
+ }
239
249
  if (view.renderActions) {
240
250
  instance.renderActions = () => view.renderActions(ViewletStates.get(uid));
241
251
  }
@@ -506,7 +516,14 @@ const withRenderMetadata = async (uid, viewId, result, instance, contextChange)
506
516
  const resultWithScrollPosition = await withScrollPosition(resultWithSelections, instance);
507
517
  const resultWithTitle = await withTitle(resultWithScrollPosition, instance);
508
518
  await ViewStatusBarItems.renderViewStatusBarItems(uid, viewId, instance);
509
- return resultWithTitle;
519
+ if (typeof instance.isDirty !== 'function') {
520
+ return resultWithTitle;
521
+ }
522
+ const modified = instance.isDirty();
523
+ if (typeof modified !== 'boolean') {
524
+ throw new ExtensionApiError('view isDirty result must be a boolean');
525
+ }
526
+ return { ...resultWithTitle, modified };
510
527
  };
511
528
  const maybeClearContext = async (uid, viewId) => {
512
529
  if (!contexts[uid]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/api",
3
- "version": "9.37.2",
3
+ "version": "9.39.0",
4
4
  "description": "Tree-shakeable extension API for Lvce Editor extensions.",
5
5
  "keywords": [
6
6
  "lvce-editor",