@kispace-io/core 0.7.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/api/base-classes.d.ts +7 -0
- package/dist/api/base-classes.d.ts.map +1 -0
- package/dist/api/constants.d.ts +2 -0
- package/dist/api/constants.d.ts.map +1 -0
- package/dist/api/index.d.ts +6 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +80 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/services.d.ts +27 -0
- package/dist/api/services.d.ts.map +1 -0
- package/dist/api/types.d.ts +11 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/commands/files.d.ts +2 -0
- package/dist/commands/files.d.ts.map +1 -0
- package/dist/commands/global.d.ts +1 -0
- package/dist/commands/global.d.ts.map +1 -0
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/version-info.d.ts +2 -0
- package/dist/commands/version-info.d.ts.map +1 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/k-app-selector.d.ts +17 -0
- package/dist/components/k-app-selector.d.ts.map +1 -0
- package/dist/components/k-app-switcher.d.ts +13 -0
- package/dist/components/k-app-switcher.d.ts.map +1 -0
- package/dist/components/k-command.d.ts +31 -0
- package/dist/components/k-command.d.ts.map +1 -0
- package/dist/components/k-extensions.d.ts +32 -0
- package/dist/components/k-extensions.d.ts.map +1 -0
- package/dist/components/k-fastviews.d.ts +34 -0
- package/dist/components/k-fastviews.d.ts.map +1 -0
- package/dist/components/k-filebrowser.d.ts +40 -0
- package/dist/components/k-filebrowser.d.ts.map +1 -0
- package/dist/components/k-language-selector.d.ts +12 -0
- package/dist/components/k-language-selector.d.ts.map +1 -0
- package/dist/components/k-log-terminal.d.ts +36 -0
- package/dist/components/k-log-terminal.d.ts.map +1 -0
- package/dist/components/k-part-name.d.ts +12 -0
- package/dist/components/k-part-name.d.ts.map +1 -0
- package/dist/components/k-tasks.d.ts +13 -0
- package/dist/components/k-tasks.d.ts.map +1 -0
- package/dist/components/k-workspace-name.d.ts +14 -0
- package/dist/components/k-workspace-name.d.ts.map +1 -0
- package/dist/contributions/default-ui-contributions.d.ts +2 -0
- package/dist/contributions/default-ui-contributions.d.ts.map +1 -0
- package/dist/contributions/index.d.ts +1 -0
- package/dist/contributions/index.d.ts.map +1 -0
- package/dist/contributions/marketplace-catalog-contributions.d.ts +2 -0
- package/dist/contributions/marketplace-catalog-contributions.d.ts.map +1 -0
- package/dist/core/app-host-config.d.ts +7 -0
- package/dist/core/app-host-config.d.ts.map +1 -0
- package/dist/core/apploader.d.ts +214 -0
- package/dist/core/apploader.d.ts.map +1 -0
- package/dist/core/appstate.d.ts +12 -0
- package/dist/core/appstate.d.ts.map +1 -0
- package/dist/core/commandregistry.d.ts +79 -0
- package/dist/core/commandregistry.d.ts.map +1 -0
- package/dist/core/config.d.ts +15 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/constants.d.ts +21 -0
- package/dist/core/constants.d.ts.map +1 -0
- package/dist/core/contributionregistry.d.ts +49 -0
- package/dist/core/contributionregistry.d.ts.map +1 -0
- package/dist/core/di.d.ts +18 -0
- package/dist/core/di.d.ts.map +1 -0
- package/dist/core/dialogservice.d.ts +33 -0
- package/dist/core/dialogservice.d.ts.map +1 -0
- package/dist/core/editorregistry.d.ts +73 -0
- package/dist/core/editorregistry.d.ts.map +1 -0
- package/dist/core/esmsh-service.d.ts +40 -0
- package/dist/core/esmsh-service.d.ts.map +1 -0
- package/dist/core/events.d.ts +7 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/events.js +63 -0
- package/dist/core/events.js.map +1 -0
- package/dist/core/extensionregistry.d.ts +98 -0
- package/dist/core/extensionregistry.d.ts.map +1 -0
- package/dist/core/filesys.d.ts +139 -0
- package/dist/core/filesys.d.ts.map +1 -0
- package/dist/core/i18n.d.ts +50 -0
- package/dist/core/i18n.d.ts.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/k-utils.d.ts +2 -0
- package/dist/core/k-utils.d.ts.map +1 -0
- package/dist/core/keybindings.d.ts +67 -0
- package/dist/core/keybindings.d.ts.map +1 -0
- package/dist/core/logger.d.ts +44 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/marketplaceregistry.d.ts +25 -0
- package/dist/core/marketplaceregistry.d.ts.map +1 -0
- package/dist/core/packageinfoservice.d.ts +16 -0
- package/dist/core/packageinfoservice.d.ts.map +1 -0
- package/dist/core/persistenceservice.d.ts +6 -0
- package/dist/core/persistenceservice.d.ts.map +1 -0
- package/dist/core/settingsservice.d.ts +19 -0
- package/dist/core/settingsservice.d.ts.map +1 -0
- package/dist/core/signals.d.ts +3 -0
- package/dist/core/signals.d.ts.map +1 -0
- package/dist/core/taskservice.d.ts +20 -0
- package/dist/core/taskservice.d.ts.map +1 -0
- package/dist/core/toast.d.ts +4 -0
- package/dist/core/toast.d.ts.map +1 -0
- package/dist/core/tree-utils.d.ts +16 -0
- package/dist/core/tree-utils.d.ts.map +1 -0
- package/dist/dialogs/confirm-dialog.d.ts +14 -0
- package/dist/dialogs/confirm-dialog.d.ts.map +1 -0
- package/dist/dialogs/index.d.ts +5 -0
- package/dist/dialogs/index.d.ts.map +1 -0
- package/dist/dialogs/info-dialog.d.ts +13 -0
- package/dist/dialogs/info-dialog.d.ts.map +1 -0
- package/dist/dialogs/navigable-info-dialog.d.ts +33 -0
- package/dist/dialogs/navigable-info-dialog.d.ts.map +1 -0
- package/dist/dialogs/prompt-dialog.d.ts +21 -0
- package/dist/dialogs/prompt-dialog.d.ts.map +1 -0
- package/dist/externals/lit.d.ts +20 -0
- package/dist/externals/lit.d.ts.map +1 -0
- package/dist/externals/lit.js +15 -0
- package/dist/externals/lit.js.map +1 -0
- package/dist/externals/third-party.d.ts +7 -0
- package/dist/externals/third-party.d.ts.map +1 -0
- package/dist/externals/third-party.js +2 -0
- package/dist/externals/third-party.js.map +1 -0
- package/dist/externals/webawesome.d.ts +1 -0
- package/dist/externals/webawesome.d.ts.map +1 -0
- package/dist/externals/webawesome.js +52 -0
- package/dist/externals/webawesome.js.map +1 -0
- package/dist/i18n/extensions.json.d.ts +42 -0
- package/dist/i18n/fastviews.json.d.ts +13 -0
- package/dist/i18n/filebrowser.json.d.ts +35 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/logterminal.json.d.ts +45 -0
- package/dist/i18n/partname.json.d.ts +15 -0
- package/dist/i18n/tasks.json.d.ts +15 -0
- package/dist/i18n/workspace.json.d.ts +15 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +80 -0
- package/dist/index.js.map +1 -0
- package/dist/k-icon-BZC7dQV0.js +492 -0
- package/dist/k-icon-BZC7dQV0.js.map +1 -0
- package/dist/k-nocontent-Bh_yToGh.js +48 -0
- package/dist/k-nocontent-Bh_yToGh.js.map +1 -0
- package/dist/k-resizable-grid-Ch3iWZaL.js +3157 -0
- package/dist/k-resizable-grid-Ch3iWZaL.js.map +1 -0
- package/dist/k-standard-layout-CQ1VZoxa.js +5011 -0
- package/dist/k-standard-layout-CQ1VZoxa.js.map +1 -0
- package/dist/layouts/k-standard-layout.d.ts +16 -0
- package/dist/layouts/k-standard-layout.d.ts.map +1 -0
- package/dist/parts/index.d.ts +1 -0
- package/dist/parts/index.d.ts.map +1 -0
- package/dist/parts/index.js +53 -0
- package/dist/parts/index.js.map +1 -0
- package/dist/parts/k-app.d.ts +11 -0
- package/dist/parts/k-app.d.ts.map +1 -0
- package/dist/parts/k-container.d.ts +4 -0
- package/dist/parts/k-container.d.ts.map +1 -0
- package/dist/parts/k-contextmenu.d.ts +38 -0
- package/dist/parts/k-contextmenu.d.ts.map +1 -0
- package/dist/parts/k-dialog-content.d.ts +9 -0
- package/dist/parts/k-dialog-content.d.ts.map +1 -0
- package/dist/parts/k-element.d.ts +36 -0
- package/dist/parts/k-element.d.ts.map +1 -0
- package/dist/parts/k-part.d.ts +96 -0
- package/dist/parts/k-part.d.ts.map +1 -0
- package/dist/parts/k-resizable-grid.d.ts +31 -0
- package/dist/parts/k-resizable-grid.d.ts.map +1 -0
- package/dist/parts/k-tabs.d.ts +74 -0
- package/dist/parts/k-tabs.d.ts.map +1 -0
- package/dist/parts/k-toolbar.d.ts +21 -0
- package/dist/parts/k-toolbar.d.ts.map +1 -0
- package/dist/widgets/index.d.ts +1 -0
- package/dist/widgets/index.d.ts.map +1 -0
- package/dist/widgets/index.js +3 -0
- package/dist/widgets/index.js.map +1 -0
- package/dist/widgets/k-icon.d.ts +10 -0
- package/dist/widgets/k-icon.d.ts.map +1 -0
- package/dist/widgets/k-nocontent.d.ts +13 -0
- package/dist/widgets/k-nocontent.d.ts.map +1 -0
- package/dist/widgets/k-widget.d.ts +25 -0
- package/dist/widgets/k-widget.d.ts.map +1 -0
- package/package.json +81 -0
- package/src/api/base-classes.ts +10 -0
- package/src/api/constants.ts +3 -0
- package/src/api/index.ts +31 -0
- package/src/api/services.ts +52 -0
- package/src/api/types.ts +46 -0
- package/src/commands/files.ts +829 -0
- package/src/commands/global.ts +225 -0
- package/src/commands/index.ts +4 -0
- package/src/commands/version-info.ts +214 -0
- package/src/components/index.ts +10 -0
- package/src/components/k-app-selector.ts +233 -0
- package/src/components/k-app-switcher.ts +126 -0
- package/src/components/k-command.ts +236 -0
- package/src/components/k-extensions.ts +615 -0
- package/src/components/k-fastviews.ts +314 -0
- package/src/components/k-filebrowser.ts +442 -0
- package/src/components/k-language-selector.ts +166 -0
- package/src/components/k-log-terminal.ts +337 -0
- package/src/components/k-part-name.ts +54 -0
- package/src/components/k-tasks.ts +267 -0
- package/src/components/k-workspace-name.ts +56 -0
- package/src/contributions/default-ui-contributions.ts +51 -0
- package/src/contributions/index.ts +3 -0
- package/src/contributions/marketplace-catalog-contributions.ts +6 -0
- package/src/core/app-host-config.ts +23 -0
- package/src/core/apploader.ts +630 -0
- package/src/core/appstate.ts +15 -0
- package/src/core/commandregistry.ts +210 -0
- package/src/core/config.ts +29 -0
- package/src/core/constants.ts +27 -0
- package/src/core/contributionregistry.ts +77 -0
- package/src/core/di.ts +54 -0
- package/src/core/dialogservice.ts +266 -0
- package/src/core/editorregistry.ts +303 -0
- package/src/core/esmsh-service.ts +404 -0
- package/src/core/events.ts +68 -0
- package/src/core/extensionregistry.ts +399 -0
- package/src/core/filesys.ts +618 -0
- package/src/core/i18n.ts +221 -0
- package/src/core/index.ts +51 -0
- package/src/core/k-utils.ts +11 -0
- package/src/core/keybindings.ts +274 -0
- package/src/core/logger.ts +187 -0
- package/src/core/marketplaceregistry.ts +197 -0
- package/src/core/packageinfoservice.ts +56 -0
- package/src/core/persistenceservice.ts +15 -0
- package/src/core/settingsservice.ts +70 -0
- package/src/core/signals.ts +18 -0
- package/src/core/taskservice.ts +72 -0
- package/src/core/toast.ts +11 -0
- package/src/core/tree-utils.ts +24 -0
- package/src/dialogs/confirm-dialog.ts +72 -0
- package/src/dialogs/index.ts +4 -0
- package/src/dialogs/info-dialog.ts +67 -0
- package/src/dialogs/navigable-info-dialog.ts +256 -0
- package/src/dialogs/prompt-dialog.ts +123 -0
- package/src/externals/lit.ts +26 -0
- package/src/externals/third-party.ts +9 -0
- package/src/externals/webawesome.ts +54 -0
- package/src/i18n/extensions.json +39 -0
- package/src/i18n/fastviews.json +10 -0
- package/src/i18n/filebrowser.json +33 -0
- package/src/i18n/index.ts +25 -0
- package/src/i18n/logterminal.json +42 -0
- package/src/i18n/partname.json +12 -0
- package/src/i18n/tasks.json +12 -0
- package/src/i18n/workspace.json +12 -0
- package/src/icons/icons.txt +3 -0
- package/src/icons/js.svg +6 -0
- package/src/icons/jupyter.svg +18 -0
- package/src/icons/python.svg +15 -0
- package/src/index.ts +3 -0
- package/src/layouts/k-standard-layout.ts +174 -0
- package/src/parts/index.ts +6 -0
- package/src/parts/k-app.ts +29 -0
- package/src/parts/k-container.ts +4 -0
- package/src/parts/k-contextmenu.ts +245 -0
- package/src/parts/k-dialog-content.ts +31 -0
- package/src/parts/k-element.ts +100 -0
- package/src/parts/k-part.ts +158 -0
- package/src/parts/k-resizable-grid.ts +366 -0
- package/src/parts/k-tabs.ts +574 -0
- package/src/parts/k-toolbar.ts +158 -0
- package/src/vite-env.d.ts +2 -0
- package/src/widgets/index.ts +2 -0
- package/src/widgets/k-icon.ts +39 -0
- package/src/widgets/k-nocontent.ts +40 -0
- package/src/widgets/k-widget.ts +90 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export type SubscriptionToken = string;
|
|
2
|
+
type Callback = (data: any) => any;
|
|
3
|
+
|
|
4
|
+
class EventBus {
|
|
5
|
+
private subscriptions: Map<string, Map<SubscriptionToken, Callback>> = new Map();
|
|
6
|
+
private tokenCounter = 0;
|
|
7
|
+
|
|
8
|
+
subscribe(topic: string, callback: Callback): SubscriptionToken {
|
|
9
|
+
if (!this.subscriptions.has(topic)) {
|
|
10
|
+
this.subscriptions.set(topic, new Map());
|
|
11
|
+
}
|
|
12
|
+
const token = `token_${++this.tokenCounter}_${Date.now()}`;
|
|
13
|
+
this.subscriptions.get(topic)!.set(token, callback);
|
|
14
|
+
return token;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
unsubscribe(token: SubscriptionToken): void {
|
|
18
|
+
for (const [topic, callbacks] of this.subscriptions.entries()) {
|
|
19
|
+
if (callbacks.has(token)) {
|
|
20
|
+
callbacks.delete(token);
|
|
21
|
+
if (callbacks.size === 0) {
|
|
22
|
+
this.subscriptions.delete(topic);
|
|
23
|
+
}
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
publish(topic: string, data: any): boolean {
|
|
30
|
+
const callbacks = this.subscriptions.get(topic);
|
|
31
|
+
if (!callbacks || callbacks.size === 0) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
queueMicrotask(() => {
|
|
36
|
+
callbacks.forEach(callback => {
|
|
37
|
+
try {
|
|
38
|
+
callback(data);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error(`Error in event callback for topic "${topic}":`, error);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
clearAllSubscriptions(): void {
|
|
48
|
+
this.subscriptions.clear();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
clearSubscriptions(topic: string): void {
|
|
52
|
+
this.subscriptions.delete(topic);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const eventBus = new EventBus();
|
|
57
|
+
|
|
58
|
+
export const subscribe = (topic: string, callback: Callback): SubscriptionToken => {
|
|
59
|
+
return eventBus.subscribe(topic, callback);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const unsubscribe = (token: SubscriptionToken): void => {
|
|
63
|
+
eventBus.unsubscribe(token);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const publish = (topic: string, data: any): boolean => {
|
|
67
|
+
return eventBus.publish(topic, data);
|
|
68
|
+
}
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import {appSettings, TOPIC_SETTINGS_CHANGED} from "./settingsservice";
|
|
2
|
+
import {publish, subscribe} from "./events";
|
|
3
|
+
import {toastError, toastInfo} from "./toast";
|
|
4
|
+
import {taskService} from "./taskservice";
|
|
5
|
+
import {rootContext, uiContext} from "./di";
|
|
6
|
+
import logger from "./logger";
|
|
7
|
+
import {esmShService} from "./esmsh-service";
|
|
8
|
+
|
|
9
|
+
export const TOPIC_EXTENSIONS_CHANGED = "events/extensionsregistry/extensionsConfigChanged"
|
|
10
|
+
const KEY_EXTENSIONS_CONFIG = "extensions"
|
|
11
|
+
const KEY_EXTERNAL_EXTENSIONS = "extensions.external"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extension definition for the extension registry.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* extensionRegistry.registerExtension({
|
|
19
|
+
* id: "system.myextension",
|
|
20
|
+
* name: "My Extension",
|
|
21
|
+
* description: "An example extension",
|
|
22
|
+
* loader: () => import("./my-extension.ts"),
|
|
23
|
+
* icon: "puzzle-piece",
|
|
24
|
+
* dependencies: ["system.dependency1", "system.dependency2"]
|
|
25
|
+
* })
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
import { UILabel } from "./i18n";
|
|
29
|
+
|
|
30
|
+
export interface Extension {
|
|
31
|
+
/** Unique identifier for the extension (e.g., "system.notebook") */
|
|
32
|
+
id: string;
|
|
33
|
+
|
|
34
|
+
/** Human-readable name of the extension */
|
|
35
|
+
name: UILabel;
|
|
36
|
+
|
|
37
|
+
/** Optional description of what the extension does */
|
|
38
|
+
description?: UILabel;
|
|
39
|
+
|
|
40
|
+
/** Optional URL to load the extension module from */
|
|
41
|
+
url?: string;
|
|
42
|
+
|
|
43
|
+
/** Function that dynamically imports the extension module */
|
|
44
|
+
loader?: () => any;
|
|
45
|
+
|
|
46
|
+
/** Optional icon identifier (FontAwesome or custom icon) */
|
|
47
|
+
icon?: string;
|
|
48
|
+
|
|
49
|
+
/** Whether this extension is marked as experimental */
|
|
50
|
+
experimental?: boolean;
|
|
51
|
+
|
|
52
|
+
/** Optional extension version */
|
|
53
|
+
version?: string;
|
|
54
|
+
|
|
55
|
+
/** Optional extension author */
|
|
56
|
+
author?: string;
|
|
57
|
+
|
|
58
|
+
/** Whether this extension is from an external source (marketplace) */
|
|
59
|
+
external?: boolean;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Optional list of extension IDs that must be loaded before this extension.
|
|
63
|
+
* Dependencies are loaded recursively and automatically when this extension is loaded.
|
|
64
|
+
* The system includes circular dependency detection.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* dependencies: ["system.pythonruntime"]
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
dependencies?: string[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ExtensionSetting {
|
|
75
|
+
id: string;
|
|
76
|
+
enabled: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type ExtensionsConfig = ExtensionSetting[]
|
|
80
|
+
|
|
81
|
+
class ExtensionRegistry {
|
|
82
|
+
private extensionsSettings?: ExtensionsConfig;
|
|
83
|
+
private extensions: { [key: string]: Extension } = {}
|
|
84
|
+
private loadedExtensions: Set<string> = new Set()
|
|
85
|
+
private loadingPromises: Map<string, Promise<void>> = new Map()
|
|
86
|
+
|
|
87
|
+
constructor() {
|
|
88
|
+
subscribe(TOPIC_SETTINGS_CHANGED, () => {
|
|
89
|
+
this.extensionsSettings = undefined
|
|
90
|
+
this.checkExtensionsConfig().then()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Load persisted external extensions first, then load enabled extensions
|
|
94
|
+
this.loadPersistedExternalExtensions().then(() => {
|
|
95
|
+
this.checkExtensionsConfig().then(async () => {
|
|
96
|
+
const loadPromises = this.extensionsSettings
|
|
97
|
+
?.filter(setting => this.isEnabled(setting.id))
|
|
98
|
+
.map(setting =>
|
|
99
|
+
this.load(setting.id).catch(e => {
|
|
100
|
+
toastError("Extension could not be loaded: " + e.message)
|
|
101
|
+
})
|
|
102
|
+
) || []
|
|
103
|
+
|
|
104
|
+
await Promise.all(loadPromises)
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private async loadPersistedExternalExtensions(): Promise<void> {
|
|
110
|
+
try {
|
|
111
|
+
const persisted = await appSettings.get(KEY_EXTERNAL_EXTENSIONS)
|
|
112
|
+
if (persisted && Array.isArray(persisted)) {
|
|
113
|
+
persisted.forEach((ext: Extension) => {
|
|
114
|
+
this.extensions[ext.id] = ext
|
|
115
|
+
})
|
|
116
|
+
logger.debug(`Loaded ${persisted.length} persisted external extensions`)
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
logger.error(`Failed to load persisted external extensions: ${error}`)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async savePersistedExternalExtensions(): Promise<void> {
|
|
124
|
+
try {
|
|
125
|
+
const externalExtensions = Object.values(this.extensions).filter(ext => ext.external)
|
|
126
|
+
await appSettings.set(KEY_EXTERNAL_EXTENSIONS, externalExtensions)
|
|
127
|
+
} catch (error) {
|
|
128
|
+
logger.error(`Failed to save persisted external extensions: ${error}`)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private async checkExtensionsConfig() {
|
|
133
|
+
if (!this.extensionsSettings) {
|
|
134
|
+
this.extensionsSettings = await appSettings.get(KEY_EXTENSIONS_CONFIG)
|
|
135
|
+
if (!this.extensionsSettings) {
|
|
136
|
+
await appSettings.set(KEY_EXTENSIONS_CONFIG, [])
|
|
137
|
+
this.extensionsSettings = await appSettings.get(KEY_EXTENSIONS_CONFIG)
|
|
138
|
+
}
|
|
139
|
+
publish(TOPIC_EXTENSIONS_CHANGED, this.extensionsSettings)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
registerExtension(extension: Extension): void {
|
|
145
|
+
this.extensions[extension.id] = extension;
|
|
146
|
+
|
|
147
|
+
// Persist external extensions
|
|
148
|
+
if (extension.external) {
|
|
149
|
+
this.savePersistedExternalExtensions().catch(err => {
|
|
150
|
+
logger.error(`Failed to persist external extension: ${err}`)
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
publish(TOPIC_EXTENSIONS_CHANGED, this.extensionsSettings);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Load an extension from a URL and register it.
|
|
159
|
+
* The module at the URL must export a default function that receives uiContext.
|
|
160
|
+
* The extension will register its contributions when loaded.
|
|
161
|
+
*
|
|
162
|
+
* Supports:
|
|
163
|
+
* - Direct URLs (http/https)
|
|
164
|
+
* - esm.sh URLs
|
|
165
|
+
* - Source identifiers (npm packages, GitHub repos, JSR packages, PR packages)
|
|
166
|
+
* Examples: 'react@18', 'gh/user/repo', 'jsr/@std/encoding@1.0.0', 'pr/owner/repo@commit'
|
|
167
|
+
*
|
|
168
|
+
* @param url - URL or source identifier to the extension module
|
|
169
|
+
* @param extensionId - Optional extension ID. If not provided, generates one from the URL.
|
|
170
|
+
* @returns Promise that resolves to the extension ID when the extension is loaded
|
|
171
|
+
*/
|
|
172
|
+
async loadExtensionFromUrl(url: string, extensionId?: string): Promise<string> {
|
|
173
|
+
logger.info(`Loading extension from URL: ${url}...`);
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
let finalUrl = url;
|
|
177
|
+
let extensionName = `Extension from ${url}`;
|
|
178
|
+
|
|
179
|
+
if (esmShService.isSourceIdentifier(url)) {
|
|
180
|
+
const packageName = esmShService.extractPackageName(url);
|
|
181
|
+
if (packageName) {
|
|
182
|
+
extensionName = `Extension: ${packageName}`;
|
|
183
|
+
}
|
|
184
|
+
finalUrl = esmShService.normalizeToEsmSh(url);
|
|
185
|
+
logger.debug(`Converted source identifier to esm.sh URL: ${url} -> ${finalUrl}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const id = extensionId || `url:${finalUrl}`;
|
|
189
|
+
|
|
190
|
+
if (this.isEnabled(id)) {
|
|
191
|
+
logger.info(`Extension from URL ${finalUrl} is already enabled`);
|
|
192
|
+
return id;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check if extension is already registered
|
|
196
|
+
if (!this.extensions[id]) {
|
|
197
|
+
const extension: Extension = {
|
|
198
|
+
id: id,
|
|
199
|
+
name: extensionName,
|
|
200
|
+
description: `Extension loaded from: ${url}`,
|
|
201
|
+
url: finalUrl
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
this.registerExtension(extension);
|
|
205
|
+
logger.info(`Registered extension from URL: ${id}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this.enable(id, false);
|
|
209
|
+
|
|
210
|
+
logger.info(`Successfully enabled extension from URL: ${finalUrl}`);
|
|
211
|
+
return id;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
logger.error(`Failed to load extension from URL ${url}: ${error}`);
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
getExtensions(): Extension[] {
|
|
219
|
+
return Object.values(this.extensions)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
public isEnabled(extensionId: string) {
|
|
223
|
+
this.checkExtensionsConfig()
|
|
224
|
+
return !!this.extensionsSettings?.find((setting) => setting.id === extensionId && setting.enabled)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
public isLoaded(extensionId: string) {
|
|
228
|
+
return this.loadedExtensions.has(extensionId)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public enable(extensionId: string, informUser: boolean = false) {
|
|
232
|
+
if (this.isEnabled(extensionId)) {
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
logger.debug(`Loading extension: ${extensionId}`)
|
|
236
|
+
this.load(extensionId).then(() => {
|
|
237
|
+
this.updateEnablement(extensionId, true, informUser)
|
|
238
|
+
}).catch(_e => {
|
|
239
|
+
logger.error(`Could not load extension: ${extensionId}: ${_e}`)
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Loads an extension and all its dependencies.
|
|
245
|
+
*
|
|
246
|
+
* Features:
|
|
247
|
+
* - Automatically loads all dependencies recursively before loading the extension
|
|
248
|
+
* - Ensures each extension is loaded only once (idempotent)
|
|
249
|
+
* - Dependencies are loaded in the order they are declared
|
|
250
|
+
* - If an extension is already being loaded, waits for that load to complete
|
|
251
|
+
* - Detects circular dependencies in the dependency chain
|
|
252
|
+
*
|
|
253
|
+
* @param extensionId - The ID of the extension to load
|
|
254
|
+
* @param loadingChain - Internal parameter to track the dependency chain for circular detection
|
|
255
|
+
* @throws Error if the extension is not found or if a circular dependency is detected
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* ```typescript
|
|
259
|
+
* // This will automatically load system.pythonruntime first
|
|
260
|
+
* await extensionRegistry.load('system.notebook')
|
|
261
|
+
* ```
|
|
262
|
+
*/
|
|
263
|
+
public async load(extensionId: string, loadingChain: string[] = []): Promise<void> {
|
|
264
|
+
// Already loaded, return immediately
|
|
265
|
+
if (this.loadedExtensions.has(extensionId)) {
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Currently loading by another call chain, wait for that promise to complete
|
|
270
|
+
const existingPromise = this.loadingPromises.get(extensionId)
|
|
271
|
+
if (existingPromise) {
|
|
272
|
+
return existingPromise
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check for circular dependency
|
|
276
|
+
if (loadingChain.includes(extensionId)) {
|
|
277
|
+
const chain = [...loadingChain, extensionId].join(' → ')
|
|
278
|
+
throw new Error(`Circular dependency detected: ${chain}`)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const extension = this.extensions[extensionId]
|
|
282
|
+
if (!extension) {
|
|
283
|
+
throw new Error("Extension not found: " + extensionId)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Create and track the loading promise
|
|
287
|
+
const loadingPromise = (async () => {
|
|
288
|
+
try {
|
|
289
|
+
if (extension.dependencies && extension.dependencies.length > 0) {
|
|
290
|
+
logger.debug(`Loading dependencies for ${extensionId}: ${extension.dependencies.join(', ')}`)
|
|
291
|
+
const newChain = [...loadingChain, extensionId]
|
|
292
|
+
for (const depId of extension.dependencies) {
|
|
293
|
+
await this.load(depId, newChain)
|
|
294
|
+
// Enable the dependency if it's not already enabled
|
|
295
|
+
if (!this.isEnabled(depId)) {
|
|
296
|
+
await this.updateEnablementAsync(depId, true, false)
|
|
297
|
+
logger.debug(`Auto-enabled dependency: ${depId}`)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const module = await taskService.runAsync("Loading extension: " + extension.name, async () => {
|
|
303
|
+
if (extension.loader) {
|
|
304
|
+
return extension.loader()
|
|
305
|
+
} else if (extension.url) {
|
|
306
|
+
let finalUrl = extension.url;
|
|
307
|
+
if (esmShService.isSourceIdentifier(extension.url)) {
|
|
308
|
+
finalUrl = esmShService.normalizeToEsmSh(extension.url);
|
|
309
|
+
logger.debug(`Normalized extension URL: ${extension.url} -> ${finalUrl}`);
|
|
310
|
+
}
|
|
311
|
+
return import(/* @vite-ignore */ finalUrl)
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
// Mark as loaded BEFORE executing the module
|
|
316
|
+
this.loadedExtensions.add(extensionId)
|
|
317
|
+
|
|
318
|
+
if (module?.default instanceof Function) {
|
|
319
|
+
logger.debug(`Executing extension function for: ${extensionId}`)
|
|
320
|
+
try {
|
|
321
|
+
module?.default(uiContext.getProxy())
|
|
322
|
+
logger.debug(`Extension function executed successfully: ${extensionId}`)
|
|
323
|
+
} catch (error) {
|
|
324
|
+
logger.error(`Error executing extension function for ${extensionId}: ${error}`)
|
|
325
|
+
throw error
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
logger.warn(`Extension ${extensionId} does not export a default function`)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
logger.debug(`Extension loaded: ${extensionId}`)
|
|
332
|
+
} catch (error) {
|
|
333
|
+
// If loading failed, remove from loaded set
|
|
334
|
+
this.loadedExtensions.delete(extensionId)
|
|
335
|
+
throw error
|
|
336
|
+
} finally {
|
|
337
|
+
// Always clean up the promise
|
|
338
|
+
this.loadingPromises.delete(extensionId)
|
|
339
|
+
}
|
|
340
|
+
})()
|
|
341
|
+
|
|
342
|
+
this.loadingPromises.set(extensionId, loadingPromise)
|
|
343
|
+
return loadingPromise
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
public disable(extensionId: string, informUser: boolean = false) {
|
|
347
|
+
if (!this.isEnabled(extensionId)) {
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
this.updateEnablement(extensionId, false, informUser)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private updateEnablement(extensionId: string, enabled: boolean, informUser: boolean) {
|
|
354
|
+
this.checkExtensionsConfig().then(() => {
|
|
355
|
+
const extension = this.extensionsSettings?.find(e => e.id == extensionId)
|
|
356
|
+
if (extension) {
|
|
357
|
+
extension.enabled = enabled
|
|
358
|
+
} else {
|
|
359
|
+
this.extensionsSettings?.push({id: extensionId, enabled: enabled})
|
|
360
|
+
}
|
|
361
|
+
appSettings.set(KEY_EXTENSIONS_CONFIG, this.extensionsSettings).then(() => {
|
|
362
|
+
if (informUser) {
|
|
363
|
+
const extObj = this.extensions[extensionId]
|
|
364
|
+
if (enabled) {
|
|
365
|
+
toastInfo(extObj.name + " enabled.")
|
|
366
|
+
} else {
|
|
367
|
+
toastInfo(extObj.name + " disabled " + " - Please restart to take effect")
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
publish(TOPIC_EXTENSIONS_CHANGED, this.extensionsSettings)
|
|
371
|
+
})
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private async updateEnablementAsync(extensionId: string, enabled: boolean, informUser: boolean) {
|
|
376
|
+
await this.checkExtensionsConfig()
|
|
377
|
+
const extension = this.extensionsSettings?.find(e => e.id == extensionId)
|
|
378
|
+
if (extension) {
|
|
379
|
+
extension.enabled = enabled
|
|
380
|
+
} else {
|
|
381
|
+
this.extensionsSettings?.push({id: extensionId, enabled: enabled})
|
|
382
|
+
}
|
|
383
|
+
await appSettings.set(KEY_EXTENSIONS_CONFIG, this.extensionsSettings)
|
|
384
|
+
if (informUser) {
|
|
385
|
+
const extObj = this.extensions[extensionId]
|
|
386
|
+
if (enabled) {
|
|
387
|
+
toastInfo(extObj.name + " enabled.")
|
|
388
|
+
} else {
|
|
389
|
+
toastInfo(extObj.name + " disabled " + " - Please restart to take effect")
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
publish(TOPIC_EXTENSIONS_CHANGED, this.extensionsSettings)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
logger.debug('ExtensionRegistry initializing...');
|
|
397
|
+
export const extensionRegistry = new ExtensionRegistry()
|
|
398
|
+
rootContext.put("extensionRegistry", extensionRegistry)
|
|
399
|
+
logger.debug('ExtensionRegistry initialized');
|