@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.
Files changed (272) hide show
  1. package/dist/api/base-classes.d.ts +7 -0
  2. package/dist/api/base-classes.d.ts.map +1 -0
  3. package/dist/api/constants.d.ts +2 -0
  4. package/dist/api/constants.d.ts.map +1 -0
  5. package/dist/api/index.d.ts +6 -0
  6. package/dist/api/index.d.ts.map +1 -0
  7. package/dist/api/index.js +80 -0
  8. package/dist/api/index.js.map +1 -0
  9. package/dist/api/services.d.ts +27 -0
  10. package/dist/api/services.d.ts.map +1 -0
  11. package/dist/api/types.d.ts +11 -0
  12. package/dist/api/types.d.ts.map +1 -0
  13. package/dist/commands/files.d.ts +2 -0
  14. package/dist/commands/files.d.ts.map +1 -0
  15. package/dist/commands/global.d.ts +1 -0
  16. package/dist/commands/global.d.ts.map +1 -0
  17. package/dist/commands/index.d.ts +1 -0
  18. package/dist/commands/index.d.ts.map +1 -0
  19. package/dist/commands/version-info.d.ts +2 -0
  20. package/dist/commands/version-info.d.ts.map +1 -0
  21. package/dist/components/index.d.ts +1 -0
  22. package/dist/components/index.d.ts.map +1 -0
  23. package/dist/components/k-app-selector.d.ts +17 -0
  24. package/dist/components/k-app-selector.d.ts.map +1 -0
  25. package/dist/components/k-app-switcher.d.ts +13 -0
  26. package/dist/components/k-app-switcher.d.ts.map +1 -0
  27. package/dist/components/k-command.d.ts +31 -0
  28. package/dist/components/k-command.d.ts.map +1 -0
  29. package/dist/components/k-extensions.d.ts +32 -0
  30. package/dist/components/k-extensions.d.ts.map +1 -0
  31. package/dist/components/k-fastviews.d.ts +34 -0
  32. package/dist/components/k-fastviews.d.ts.map +1 -0
  33. package/dist/components/k-filebrowser.d.ts +40 -0
  34. package/dist/components/k-filebrowser.d.ts.map +1 -0
  35. package/dist/components/k-language-selector.d.ts +12 -0
  36. package/dist/components/k-language-selector.d.ts.map +1 -0
  37. package/dist/components/k-log-terminal.d.ts +36 -0
  38. package/dist/components/k-log-terminal.d.ts.map +1 -0
  39. package/dist/components/k-part-name.d.ts +12 -0
  40. package/dist/components/k-part-name.d.ts.map +1 -0
  41. package/dist/components/k-tasks.d.ts +13 -0
  42. package/dist/components/k-tasks.d.ts.map +1 -0
  43. package/dist/components/k-workspace-name.d.ts +14 -0
  44. package/dist/components/k-workspace-name.d.ts.map +1 -0
  45. package/dist/contributions/default-ui-contributions.d.ts +2 -0
  46. package/dist/contributions/default-ui-contributions.d.ts.map +1 -0
  47. package/dist/contributions/index.d.ts +1 -0
  48. package/dist/contributions/index.d.ts.map +1 -0
  49. package/dist/contributions/marketplace-catalog-contributions.d.ts +2 -0
  50. package/dist/contributions/marketplace-catalog-contributions.d.ts.map +1 -0
  51. package/dist/core/app-host-config.d.ts +7 -0
  52. package/dist/core/app-host-config.d.ts.map +1 -0
  53. package/dist/core/apploader.d.ts +214 -0
  54. package/dist/core/apploader.d.ts.map +1 -0
  55. package/dist/core/appstate.d.ts +12 -0
  56. package/dist/core/appstate.d.ts.map +1 -0
  57. package/dist/core/commandregistry.d.ts +79 -0
  58. package/dist/core/commandregistry.d.ts.map +1 -0
  59. package/dist/core/config.d.ts +15 -0
  60. package/dist/core/config.d.ts.map +1 -0
  61. package/dist/core/constants.d.ts +21 -0
  62. package/dist/core/constants.d.ts.map +1 -0
  63. package/dist/core/contributionregistry.d.ts +49 -0
  64. package/dist/core/contributionregistry.d.ts.map +1 -0
  65. package/dist/core/di.d.ts +18 -0
  66. package/dist/core/di.d.ts.map +1 -0
  67. package/dist/core/dialogservice.d.ts +33 -0
  68. package/dist/core/dialogservice.d.ts.map +1 -0
  69. package/dist/core/editorregistry.d.ts +73 -0
  70. package/dist/core/editorregistry.d.ts.map +1 -0
  71. package/dist/core/esmsh-service.d.ts +40 -0
  72. package/dist/core/esmsh-service.d.ts.map +1 -0
  73. package/dist/core/events.d.ts +7 -0
  74. package/dist/core/events.d.ts.map +1 -0
  75. package/dist/core/events.js +63 -0
  76. package/dist/core/events.js.map +1 -0
  77. package/dist/core/extensionregistry.d.ts +98 -0
  78. package/dist/core/extensionregistry.d.ts.map +1 -0
  79. package/dist/core/filesys.d.ts +139 -0
  80. package/dist/core/filesys.d.ts.map +1 -0
  81. package/dist/core/i18n.d.ts +50 -0
  82. package/dist/core/i18n.d.ts.map +1 -0
  83. package/dist/core/index.d.ts +1 -0
  84. package/dist/core/index.d.ts.map +1 -0
  85. package/dist/core/k-utils.d.ts +2 -0
  86. package/dist/core/k-utils.d.ts.map +1 -0
  87. package/dist/core/keybindings.d.ts +67 -0
  88. package/dist/core/keybindings.d.ts.map +1 -0
  89. package/dist/core/logger.d.ts +44 -0
  90. package/dist/core/logger.d.ts.map +1 -0
  91. package/dist/core/marketplaceregistry.d.ts +25 -0
  92. package/dist/core/marketplaceregistry.d.ts.map +1 -0
  93. package/dist/core/packageinfoservice.d.ts +16 -0
  94. package/dist/core/packageinfoservice.d.ts.map +1 -0
  95. package/dist/core/persistenceservice.d.ts +6 -0
  96. package/dist/core/persistenceservice.d.ts.map +1 -0
  97. package/dist/core/settingsservice.d.ts +19 -0
  98. package/dist/core/settingsservice.d.ts.map +1 -0
  99. package/dist/core/signals.d.ts +3 -0
  100. package/dist/core/signals.d.ts.map +1 -0
  101. package/dist/core/taskservice.d.ts +20 -0
  102. package/dist/core/taskservice.d.ts.map +1 -0
  103. package/dist/core/toast.d.ts +4 -0
  104. package/dist/core/toast.d.ts.map +1 -0
  105. package/dist/core/tree-utils.d.ts +16 -0
  106. package/dist/core/tree-utils.d.ts.map +1 -0
  107. package/dist/dialogs/confirm-dialog.d.ts +14 -0
  108. package/dist/dialogs/confirm-dialog.d.ts.map +1 -0
  109. package/dist/dialogs/index.d.ts +5 -0
  110. package/dist/dialogs/index.d.ts.map +1 -0
  111. package/dist/dialogs/info-dialog.d.ts +13 -0
  112. package/dist/dialogs/info-dialog.d.ts.map +1 -0
  113. package/dist/dialogs/navigable-info-dialog.d.ts +33 -0
  114. package/dist/dialogs/navigable-info-dialog.d.ts.map +1 -0
  115. package/dist/dialogs/prompt-dialog.d.ts +21 -0
  116. package/dist/dialogs/prompt-dialog.d.ts.map +1 -0
  117. package/dist/externals/lit.d.ts +20 -0
  118. package/dist/externals/lit.d.ts.map +1 -0
  119. package/dist/externals/lit.js +15 -0
  120. package/dist/externals/lit.js.map +1 -0
  121. package/dist/externals/third-party.d.ts +7 -0
  122. package/dist/externals/third-party.d.ts.map +1 -0
  123. package/dist/externals/third-party.js +2 -0
  124. package/dist/externals/third-party.js.map +1 -0
  125. package/dist/externals/webawesome.d.ts +1 -0
  126. package/dist/externals/webawesome.d.ts.map +1 -0
  127. package/dist/externals/webawesome.js +52 -0
  128. package/dist/externals/webawesome.js.map +1 -0
  129. package/dist/i18n/extensions.json.d.ts +42 -0
  130. package/dist/i18n/fastviews.json.d.ts +13 -0
  131. package/dist/i18n/filebrowser.json.d.ts +35 -0
  132. package/dist/i18n/index.d.ts +2 -0
  133. package/dist/i18n/index.d.ts.map +1 -0
  134. package/dist/i18n/logterminal.json.d.ts +45 -0
  135. package/dist/i18n/partname.json.d.ts +15 -0
  136. package/dist/i18n/tasks.json.d.ts +15 -0
  137. package/dist/i18n/workspace.json.d.ts +15 -0
  138. package/dist/index.d.ts +2 -0
  139. package/dist/index.d.ts.map +1 -0
  140. package/dist/index.js +80 -0
  141. package/dist/index.js.map +1 -0
  142. package/dist/k-icon-BZC7dQV0.js +492 -0
  143. package/dist/k-icon-BZC7dQV0.js.map +1 -0
  144. package/dist/k-nocontent-Bh_yToGh.js +48 -0
  145. package/dist/k-nocontent-Bh_yToGh.js.map +1 -0
  146. package/dist/k-resizable-grid-Ch3iWZaL.js +3157 -0
  147. package/dist/k-resizable-grid-Ch3iWZaL.js.map +1 -0
  148. package/dist/k-standard-layout-CQ1VZoxa.js +5011 -0
  149. package/dist/k-standard-layout-CQ1VZoxa.js.map +1 -0
  150. package/dist/layouts/k-standard-layout.d.ts +16 -0
  151. package/dist/layouts/k-standard-layout.d.ts.map +1 -0
  152. package/dist/parts/index.d.ts +1 -0
  153. package/dist/parts/index.d.ts.map +1 -0
  154. package/dist/parts/index.js +53 -0
  155. package/dist/parts/index.js.map +1 -0
  156. package/dist/parts/k-app.d.ts +11 -0
  157. package/dist/parts/k-app.d.ts.map +1 -0
  158. package/dist/parts/k-container.d.ts +4 -0
  159. package/dist/parts/k-container.d.ts.map +1 -0
  160. package/dist/parts/k-contextmenu.d.ts +38 -0
  161. package/dist/parts/k-contextmenu.d.ts.map +1 -0
  162. package/dist/parts/k-dialog-content.d.ts +9 -0
  163. package/dist/parts/k-dialog-content.d.ts.map +1 -0
  164. package/dist/parts/k-element.d.ts +36 -0
  165. package/dist/parts/k-element.d.ts.map +1 -0
  166. package/dist/parts/k-part.d.ts +96 -0
  167. package/dist/parts/k-part.d.ts.map +1 -0
  168. package/dist/parts/k-resizable-grid.d.ts +31 -0
  169. package/dist/parts/k-resizable-grid.d.ts.map +1 -0
  170. package/dist/parts/k-tabs.d.ts +74 -0
  171. package/dist/parts/k-tabs.d.ts.map +1 -0
  172. package/dist/parts/k-toolbar.d.ts +21 -0
  173. package/dist/parts/k-toolbar.d.ts.map +1 -0
  174. package/dist/widgets/index.d.ts +1 -0
  175. package/dist/widgets/index.d.ts.map +1 -0
  176. package/dist/widgets/index.js +3 -0
  177. package/dist/widgets/index.js.map +1 -0
  178. package/dist/widgets/k-icon.d.ts +10 -0
  179. package/dist/widgets/k-icon.d.ts.map +1 -0
  180. package/dist/widgets/k-nocontent.d.ts +13 -0
  181. package/dist/widgets/k-nocontent.d.ts.map +1 -0
  182. package/dist/widgets/k-widget.d.ts +25 -0
  183. package/dist/widgets/k-widget.d.ts.map +1 -0
  184. package/package.json +81 -0
  185. package/src/api/base-classes.ts +10 -0
  186. package/src/api/constants.ts +3 -0
  187. package/src/api/index.ts +31 -0
  188. package/src/api/services.ts +52 -0
  189. package/src/api/types.ts +46 -0
  190. package/src/commands/files.ts +829 -0
  191. package/src/commands/global.ts +225 -0
  192. package/src/commands/index.ts +4 -0
  193. package/src/commands/version-info.ts +214 -0
  194. package/src/components/index.ts +10 -0
  195. package/src/components/k-app-selector.ts +233 -0
  196. package/src/components/k-app-switcher.ts +126 -0
  197. package/src/components/k-command.ts +236 -0
  198. package/src/components/k-extensions.ts +615 -0
  199. package/src/components/k-fastviews.ts +314 -0
  200. package/src/components/k-filebrowser.ts +442 -0
  201. package/src/components/k-language-selector.ts +166 -0
  202. package/src/components/k-log-terminal.ts +337 -0
  203. package/src/components/k-part-name.ts +54 -0
  204. package/src/components/k-tasks.ts +267 -0
  205. package/src/components/k-workspace-name.ts +56 -0
  206. package/src/contributions/default-ui-contributions.ts +51 -0
  207. package/src/contributions/index.ts +3 -0
  208. package/src/contributions/marketplace-catalog-contributions.ts +6 -0
  209. package/src/core/app-host-config.ts +23 -0
  210. package/src/core/apploader.ts +630 -0
  211. package/src/core/appstate.ts +15 -0
  212. package/src/core/commandregistry.ts +210 -0
  213. package/src/core/config.ts +29 -0
  214. package/src/core/constants.ts +27 -0
  215. package/src/core/contributionregistry.ts +77 -0
  216. package/src/core/di.ts +54 -0
  217. package/src/core/dialogservice.ts +266 -0
  218. package/src/core/editorregistry.ts +303 -0
  219. package/src/core/esmsh-service.ts +404 -0
  220. package/src/core/events.ts +68 -0
  221. package/src/core/extensionregistry.ts +399 -0
  222. package/src/core/filesys.ts +618 -0
  223. package/src/core/i18n.ts +221 -0
  224. package/src/core/index.ts +51 -0
  225. package/src/core/k-utils.ts +11 -0
  226. package/src/core/keybindings.ts +274 -0
  227. package/src/core/logger.ts +187 -0
  228. package/src/core/marketplaceregistry.ts +197 -0
  229. package/src/core/packageinfoservice.ts +56 -0
  230. package/src/core/persistenceservice.ts +15 -0
  231. package/src/core/settingsservice.ts +70 -0
  232. package/src/core/signals.ts +18 -0
  233. package/src/core/taskservice.ts +72 -0
  234. package/src/core/toast.ts +11 -0
  235. package/src/core/tree-utils.ts +24 -0
  236. package/src/dialogs/confirm-dialog.ts +72 -0
  237. package/src/dialogs/index.ts +4 -0
  238. package/src/dialogs/info-dialog.ts +67 -0
  239. package/src/dialogs/navigable-info-dialog.ts +256 -0
  240. package/src/dialogs/prompt-dialog.ts +123 -0
  241. package/src/externals/lit.ts +26 -0
  242. package/src/externals/third-party.ts +9 -0
  243. package/src/externals/webawesome.ts +54 -0
  244. package/src/i18n/extensions.json +39 -0
  245. package/src/i18n/fastviews.json +10 -0
  246. package/src/i18n/filebrowser.json +33 -0
  247. package/src/i18n/index.ts +25 -0
  248. package/src/i18n/logterminal.json +42 -0
  249. package/src/i18n/partname.json +12 -0
  250. package/src/i18n/tasks.json +12 -0
  251. package/src/i18n/workspace.json +12 -0
  252. package/src/icons/icons.txt +3 -0
  253. package/src/icons/js.svg +6 -0
  254. package/src/icons/jupyter.svg +18 -0
  255. package/src/icons/python.svg +15 -0
  256. package/src/index.ts +3 -0
  257. package/src/layouts/k-standard-layout.ts +174 -0
  258. package/src/parts/index.ts +6 -0
  259. package/src/parts/k-app.ts +29 -0
  260. package/src/parts/k-container.ts +4 -0
  261. package/src/parts/k-contextmenu.ts +245 -0
  262. package/src/parts/k-dialog-content.ts +31 -0
  263. package/src/parts/k-element.ts +100 -0
  264. package/src/parts/k-part.ts +158 -0
  265. package/src/parts/k-resizable-grid.ts +366 -0
  266. package/src/parts/k-tabs.ts +574 -0
  267. package/src/parts/k-toolbar.ts +158 -0
  268. package/src/vite-env.d.ts +2 -0
  269. package/src/widgets/index.ts +2 -0
  270. package/src/widgets/k-icon.ts +39 -0
  271. package/src/widgets/k-nocontent.ts +40 -0
  272. 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');