@theia/plugin-ext 1.76.0-next.7 → 1.76.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 (221) hide show
  1. package/lib/common/paths-util.d.ts.map +1 -1
  2. package/lib/common/paths-util.js +9 -2
  3. package/lib/common/paths-util.js.map +1 -1
  4. package/lib/common/paths-util.spec.d.ts +2 -0
  5. package/lib/common/paths-util.spec.d.ts.map +1 -0
  6. package/lib/common/paths-util.spec.js +42 -0
  7. package/lib/common/paths-util.spec.js.map +1 -0
  8. package/lib/common/plugin-api-rpc.d.ts +14 -0
  9. package/lib/common/plugin-api-rpc.d.ts.map +1 -1
  10. package/lib/common/plugin-api-rpc.js +1 -1
  11. package/lib/common/plugin-api-rpc.js.map +1 -1
  12. package/lib/common/plugin-protocol.d.ts +2 -2
  13. package/lib/common/plugin-protocol.d.ts.map +1 -1
  14. package/lib/common/plugin-protocol.js +2 -1
  15. package/lib/common/plugin-protocol.js.map +1 -1
  16. package/lib/hosted/browser/hosted-plugin.d.ts +2 -0
  17. package/lib/hosted/browser/hosted-plugin.d.ts.map +1 -1
  18. package/lib/hosted/browser/hosted-plugin.js +9 -3
  19. package/lib/hosted/browser/hosted-plugin.js.map +1 -1
  20. package/lib/hosted/browser/worker/worker-main.js +22 -12
  21. package/lib/hosted/browser/worker/worker-main.js.map +1 -1
  22. package/lib/hosted/browser/worker/worker-plugin-module.d.ts.map +1 -1
  23. package/lib/hosted/browser/worker/worker-plugin-module.js +2 -0
  24. package/lib/hosted/browser/worker/worker-plugin-module.js.map +1 -1
  25. package/lib/hosted/browser-only/browser-only-hosted-plugin-server.d.ts +37 -0
  26. package/lib/hosted/browser-only/browser-only-hosted-plugin-server.d.ts.map +1 -0
  27. package/lib/hosted/browser-only/browser-only-hosted-plugin-server.js +107 -0
  28. package/lib/hosted/browser-only/browser-only-hosted-plugin-server.js.map +1 -0
  29. package/lib/hosted/browser-only/browser-only-hosted-plugin-server.spec.d.ts +2 -0
  30. package/lib/hosted/browser-only/browser-only-hosted-plugin-server.spec.d.ts.map +1 -0
  31. package/lib/hosted/browser-only/browser-only-hosted-plugin-server.spec.js +106 -0
  32. package/lib/hosted/browser-only/browser-only-hosted-plugin-server.spec.js.map +1 -0
  33. package/lib/hosted/browser-only/browser-only-plugin-file-system-provider.d.ts +38 -0
  34. package/lib/hosted/browser-only/browser-only-plugin-file-system-provider.d.ts.map +1 -0
  35. package/lib/hosted/browser-only/browser-only-plugin-file-system-provider.js +124 -0
  36. package/lib/hosted/browser-only/browser-only-plugin-file-system-provider.js.map +1 -0
  37. package/lib/hosted/browser-only/browser-only-plugin-file-system-provider.spec.d.ts +2 -0
  38. package/lib/hosted/browser-only/browser-only-plugin-file-system-provider.spec.d.ts.map +1 -0
  39. package/lib/hosted/browser-only/browser-only-plugin-file-system-provider.spec.js +137 -0
  40. package/lib/hosted/browser-only/browser-only-plugin-file-system-provider.spec.js.map +1 -0
  41. package/lib/hosted/browser-only/browser-only-plugin-paths-service.d.ts +43 -0
  42. package/lib/hosted/browser-only/browser-only-plugin-paths-service.d.ts.map +1 -0
  43. package/lib/hosted/browser-only/browser-only-plugin-paths-service.js +145 -0
  44. package/lib/hosted/browser-only/browser-only-plugin-paths-service.js.map +1 -0
  45. package/lib/hosted/browser-only/browser-only-plugin-paths-service.spec.d.ts +2 -0
  46. package/lib/hosted/browser-only/browser-only-plugin-paths-service.spec.d.ts.map +1 -0
  47. package/lib/hosted/browser-only/browser-only-plugin-paths-service.spec.js +165 -0
  48. package/lib/hosted/browser-only/browser-only-plugin-paths-service.spec.js.map +1 -0
  49. package/lib/hosted/browser-only/browser-only-plugin-server.d.ts +54 -0
  50. package/lib/hosted/browser-only/browser-only-plugin-server.d.ts.map +1 -0
  51. package/lib/hosted/browser-only/browser-only-plugin-server.js +155 -0
  52. package/lib/hosted/browser-only/browser-only-plugin-server.js.map +1 -0
  53. package/lib/hosted/browser-only/browser-only-plugin-server.spec.d.ts +2 -0
  54. package/lib/hosted/browser-only/browser-only-plugin-server.spec.d.ts.map +1 -0
  55. package/lib/hosted/browser-only/browser-only-plugin-server.spec.js +190 -0
  56. package/lib/hosted/browser-only/browser-only-plugin-server.spec.js.map +1 -0
  57. package/lib/hosted/browser-only/test/navigator-locks-test-util.d.ts +24 -0
  58. package/lib/hosted/browser-only/test/navigator-locks-test-util.d.ts.map +1 -0
  59. package/lib/hosted/browser-only/test/navigator-locks-test-util.js +46 -0
  60. package/lib/hosted/browser-only/test/navigator-locks-test-util.js.map +1 -0
  61. package/lib/hosted/common/hosted-plugin.d.ts +2 -2
  62. package/lib/hosted/common/hosted-plugin.d.ts.map +1 -1
  63. package/lib/hosted/common/hosted-plugin.js +2 -2
  64. package/lib/hosted/common/hosted-plugin.js.map +1 -1
  65. package/lib/hosted/node/plugin-host-module.d.ts.map +1 -1
  66. package/lib/hosted/node/plugin-host-module.js +2 -0
  67. package/lib/hosted/node/plugin-host-module.js.map +1 -1
  68. package/lib/hosted/node/plugin-host-rpc.d.ts +3 -0
  69. package/lib/hosted/node/plugin-host-rpc.d.ts.map +1 -1
  70. package/lib/hosted/node/plugin-host-rpc.js +9 -3
  71. package/lib/hosted/node/plugin-host-rpc.js.map +1 -1
  72. package/lib/main/browser/editors-and-documents-main.d.ts +7 -0
  73. package/lib/main/browser/editors-and-documents-main.d.ts.map +1 -1
  74. package/lib/main/browser/editors-and-documents-main.js +19 -9
  75. package/lib/main/browser/editors-and-documents-main.js.map +1 -1
  76. package/lib/main/browser/editors-and-documents-main.spec.d.ts +2 -0
  77. package/lib/main/browser/editors-and-documents-main.spec.d.ts.map +1 -0
  78. package/lib/main/browser/editors-and-documents-main.spec.js +117 -0
  79. package/lib/main/browser/editors-and-documents-main.spec.js.map +1 -0
  80. package/lib/main/browser/languages-main.d.ts +7 -0
  81. package/lib/main/browser/languages-main.d.ts.map +1 -1
  82. package/lib/main/browser/languages-main.js +31 -9
  83. package/lib/main/browser/languages-main.js.map +1 -1
  84. package/lib/main/browser/languages-main.spec.d.ts +2 -0
  85. package/lib/main/browser/languages-main.spec.d.ts.map +1 -0
  86. package/lib/main/browser/languages-main.spec.js +82 -0
  87. package/lib/main/browser/languages-main.spec.js.map +1 -0
  88. package/lib/main/browser/main-context.d.ts.map +1 -1
  89. package/lib/main/browser/main-context.js +3 -0
  90. package/lib/main/browser/main-context.js.map +1 -1
  91. package/lib/main/browser/menus/vscode-theia-menu-mappings.d.ts +5 -0
  92. package/lib/main/browser/menus/vscode-theia-menu-mappings.d.ts.map +1 -1
  93. package/lib/main/browser/menus/vscode-theia-menu-mappings.js +5 -0
  94. package/lib/main/browser/menus/vscode-theia-menu-mappings.js.map +1 -1
  95. package/lib/main/browser/plugin-ext-frontend-module.d.ts.map +1 -1
  96. package/lib/main/browser/plugin-ext-frontend-module.js +1 -0
  97. package/lib/main/browser/plugin-ext-frontend-module.js.map +1 -1
  98. package/lib/main/browser/plugin-icon-service.d.ts +6 -0
  99. package/lib/main/browser/plugin-icon-service.d.ts.map +1 -1
  100. package/lib/main/browser/plugin-icon-service.js +14 -2
  101. package/lib/main/browser/plugin-icon-service.js.map +1 -1
  102. package/lib/main/browser/plugin-icon-service.spec.d.ts +2 -0
  103. package/lib/main/browser/plugin-icon-service.spec.d.ts.map +1 -0
  104. package/lib/main/browser/plugin-icon-service.spec.js +83 -0
  105. package/lib/main/browser/plugin-icon-service.spec.js.map +1 -0
  106. package/lib/main/browser/plugin-icon-theme-service.d.ts.map +1 -1
  107. package/lib/main/browser/plugin-icon-theme-service.js +2 -1
  108. package/lib/main/browser/plugin-icon-theme-service.js.map +1 -1
  109. package/lib/main/browser/plugin-icon-theme-service.spec.d.ts +2 -0
  110. package/lib/main/browser/plugin-icon-theme-service.spec.d.ts.map +1 -0
  111. package/lib/main/browser/plugin-icon-theme-service.spec.js +91 -0
  112. package/lib/main/browser/plugin-icon-theme-service.spec.js.map +1 -0
  113. package/lib/main/browser/telemetry-main.d.ts +20 -0
  114. package/lib/main/browser/telemetry-main.d.ts.map +1 -0
  115. package/lib/main/browser/telemetry-main.js +43 -0
  116. package/lib/main/browser/telemetry-main.js.map +1 -0
  117. package/lib/main/browser/telemetry-main.spec.d.ts +2 -0
  118. package/lib/main/browser/telemetry-main.spec.d.ts.map +1 -0
  119. package/lib/main/browser/telemetry-main.spec.js +66 -0
  120. package/lib/main/browser/telemetry-main.spec.js.map +1 -0
  121. package/lib/main/browser/webview/webview-context-keys.d.ts +3 -2
  122. package/lib/main/browser/webview/webview-context-keys.d.ts.map +1 -1
  123. package/lib/main/browser/webview/webview-context-keys.js +16 -2
  124. package/lib/main/browser/webview/webview-context-keys.js.map +1 -1
  125. package/lib/main/browser/webview/webview-environment.d.ts.map +1 -1
  126. package/lib/main/browser/webview/webview-environment.js +2 -1
  127. package/lib/main/browser/webview/webview-environment.js.map +1 -1
  128. package/lib/main/common/paths/const.d.ts +13 -0
  129. package/lib/main/common/paths/const.d.ts.map +1 -0
  130. package/lib/main/common/paths/const.js +32 -0
  131. package/lib/main/common/paths/const.js.map +1 -0
  132. package/lib/main/node/paths/const.d.ts +5 -5
  133. package/lib/main/node/paths/const.d.ts.map +1 -1
  134. package/lib/main/node/paths/const.js +6 -6
  135. package/lib/main/node/paths/const.js.map +1 -1
  136. package/lib/main/node/paths/plugin-paths-service.js +1 -1
  137. package/lib/main/node/paths/plugin-paths-service.js.map +1 -1
  138. package/lib/main/node/plugin-cli-contribution.d.ts +2 -2
  139. package/lib/main/node/plugin-cli-contribution.d.ts.map +1 -1
  140. package/lib/main/node/plugin-cli-contribution.js +4 -3
  141. package/lib/main/node/plugin-cli-contribution.js.map +1 -1
  142. package/lib/main/node/plugins-key-value-storage.js +1 -1
  143. package/lib/main/node/plugins-key-value-storage.js.map +1 -1
  144. package/lib/main/node/resolvers/local-directory-plugin-deployer-resolver.d.ts +5 -0
  145. package/lib/main/node/resolvers/local-directory-plugin-deployer-resolver.d.ts.map +1 -1
  146. package/lib/main/node/resolvers/local-directory-plugin-deployer-resolver.js +12 -1
  147. package/lib/main/node/resolvers/local-directory-plugin-deployer-resolver.js.map +1 -1
  148. package/lib/main/node/resolvers/local-directory-plugin-deployer-resolver.spec.d.ts +2 -0
  149. package/lib/main/node/resolvers/local-directory-plugin-deployer-resolver.spec.d.ts.map +1 -0
  150. package/lib/main/node/resolvers/local-directory-plugin-deployer-resolver.spec.js +54 -0
  151. package/lib/main/node/resolvers/local-directory-plugin-deployer-resolver.spec.js.map +1 -0
  152. package/lib/plugin/plugin-context.d.ts +2 -1
  153. package/lib/plugin/plugin-context.d.ts.map +1 -1
  154. package/lib/plugin/plugin-context.js +2 -3
  155. package/lib/plugin/plugin-context.js.map +1 -1
  156. package/lib/plugin/plugin-manager.d.ts +2 -0
  157. package/lib/plugin/plugin-manager.d.ts.map +1 -1
  158. package/lib/plugin/plugin-manager.js +12 -1
  159. package/lib/plugin/plugin-manager.js.map +1 -1
  160. package/lib/plugin/relative-pattern.spec.d.ts +2 -0
  161. package/lib/plugin/relative-pattern.spec.d.ts.map +1 -0
  162. package/lib/plugin/relative-pattern.spec.js +51 -0
  163. package/lib/plugin/relative-pattern.spec.js.map +1 -0
  164. package/lib/plugin/telemetry-ext.d.ts +23 -7
  165. package/lib/plugin/telemetry-ext.d.ts.map +1 -1
  166. package/lib/plugin/telemetry-ext.js +60 -33
  167. package/lib/plugin/telemetry-ext.js.map +1 -1
  168. package/lib/plugin/telemetry-ext.spec.js +121 -16
  169. package/lib/plugin/telemetry-ext.spec.js.map +1 -1
  170. package/lib/plugin-ext-frontend-only-module.d.ts +4 -0
  171. package/lib/plugin-ext-frontend-only-module.d.ts.map +1 -0
  172. package/lib/plugin-ext-frontend-only-module.js +34 -0
  173. package/lib/plugin-ext-frontend-only-module.js.map +1 -0
  174. package/package.json +36 -32
  175. package/src/common/paths-util.spec.ts +47 -0
  176. package/src/common/paths-util.ts +11 -2
  177. package/src/common/plugin-api-rpc.ts +15 -1
  178. package/src/common/plugin-protocol.ts +2 -0
  179. package/src/hosted/browser/hosted-plugin.ts +9 -4
  180. package/src/hosted/browser/worker/worker-main.ts +23 -12
  181. package/src/hosted/browser/worker/worker-plugin-module.ts +2 -0
  182. package/src/hosted/browser-only/browser-only-hosted-plugin-server.spec.ts +133 -0
  183. package/src/hosted/browser-only/browser-only-hosted-plugin-server.ts +122 -0
  184. package/src/hosted/browser-only/browser-only-plugin-file-system-provider.spec.ts +178 -0
  185. package/src/hosted/browser-only/browser-only-plugin-file-system-provider.ts +129 -0
  186. package/src/hosted/browser-only/browser-only-plugin-paths-service.spec.ts +199 -0
  187. package/src/hosted/browser-only/browser-only-plugin-paths-service.ts +145 -0
  188. package/src/hosted/browser-only/browser-only-plugin-server.spec.ts +228 -0
  189. package/src/hosted/browser-only/browser-only-plugin-server.ts +165 -0
  190. package/src/hosted/browser-only/test/navigator-locks-test-util.ts +54 -0
  191. package/src/hosted/common/hosted-plugin.ts +4 -4
  192. package/src/hosted/node/plugin-host-module.ts +2 -0
  193. package/src/hosted/node/plugin-host-rpc.ts +10 -4
  194. package/src/main/browser/editors-and-documents-main.spec.ts +140 -0
  195. package/src/main/browser/editors-and-documents-main.ts +19 -7
  196. package/src/main/browser/languages-main.spec.ts +101 -0
  197. package/src/main/browser/languages-main.ts +31 -9
  198. package/src/main/browser/main-context.ts +4 -0
  199. package/src/main/browser/menus/vscode-theia-menu-mappings.ts +5 -0
  200. package/src/main/browser/plugin-ext-frontend-module.ts +2 -1
  201. package/src/main/browser/plugin-icon-service.spec.ts +101 -0
  202. package/src/main/browser/plugin-icon-service.ts +16 -3
  203. package/src/main/browser/plugin-icon-theme-service.spec.ts +103 -0
  204. package/src/main/browser/plugin-icon-theme-service.ts +2 -1
  205. package/src/main/browser/telemetry-main.spec.ts +77 -0
  206. package/src/main/browser/telemetry-main.ts +46 -0
  207. package/src/main/browser/webview/webview-context-keys.ts +20 -4
  208. package/src/main/browser/webview/webview-environment.ts +2 -1
  209. package/src/main/common/paths/const.ts +28 -0
  210. package/src/main/node/paths/const.ts +5 -5
  211. package/src/main/node/paths/plugin-paths-service.ts +1 -1
  212. package/src/main/node/plugin-cli-contribution.ts +4 -3
  213. package/src/main/node/plugins-key-value-storage.ts +1 -1
  214. package/src/main/node/resolvers/local-directory-plugin-deployer-resolver.spec.ts +61 -0
  215. package/src/main/node/resolvers/local-directory-plugin-deployer-resolver.ts +14 -1
  216. package/src/plugin/plugin-context.ts +3 -2
  217. package/src/plugin/plugin-manager.ts +11 -1
  218. package/src/plugin/relative-pattern.spec.ts +55 -0
  219. package/src/plugin/telemetry-ext.spec.ts +158 -17
  220. package/src/plugin/telemetry-ext.ts +63 -33
  221. package/src/plugin-ext-frontend-only-module.ts +34 -0
@@ -0,0 +1,228 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 Robert Jandow
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { expect } from 'chai';
18
+ import { ILogger } from '@theia/core';
19
+ import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
20
+ import { Container } from '@theia/core/shared/inversify';
21
+ import { LocalStorageService, StorageService } from '@theia/core/lib/browser/storage-service';
22
+ import { PluginPathsService } from '../../main/common/plugin-paths-protocol';
23
+ import { BrowserOnlyPluginServer } from './browser-only-plugin-server';
24
+ import { installFakeLockManager as installNavigatorLocks } from './test/navigator-locks-test-util';
25
+
26
+ class InMemoryStorageService implements StorageService {
27
+ readonly data = new Map<string, string>();
28
+
29
+ async setData<T>(key: string, data: T): Promise<void> {
30
+ this.data.set(key, JSON.stringify(data));
31
+ }
32
+
33
+ async getData<T>(key: string, defaultValue?: T): Promise<T | undefined> {
34
+ const stored = this.data.get(key);
35
+ return stored === undefined ? defaultValue : JSON.parse(stored);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Stands for `@theia/workspace`'s `WorkspaceStorageService`, to which `@theia/workspace` rebinds
41
+ * the plain `StorageService` symbol, prefixing every key with the current workspace.
42
+ */
43
+ class PrefixingStorageService implements StorageService {
44
+
45
+ constructor(private readonly delegate: InMemoryStorageService, private readonly prefix: string) { }
46
+
47
+ setData<T>(key: string, data: T): Promise<void> {
48
+ return this.delegate.setData(`${this.prefix}:${key}`, data);
49
+ }
50
+
51
+ getData<T>(key: string, defaultValue?: T): Promise<T | undefined> {
52
+ return this.delegate.getData(`${this.prefix}:${key}`, defaultValue);
53
+ }
54
+ }
55
+
56
+ const pluginPathsService: PluginPathsService = {
57
+ getHostLogPath: async () => '/logs/host',
58
+ getHostStoragePath: async workspaceUri => workspaceUri && `/workspace-storage/${workspaceUri}`
59
+ };
60
+
61
+ /**
62
+ * Stand-in for the browser's `LockManager`. Like the real one, it runs requests for the same
63
+ * lock name one after another.
64
+ */
65
+ class FakeLockManager {
66
+ private readonly queues = new Map<string, Promise<unknown>>();
67
+
68
+ request<T>(name: string, callback: () => T | PromiseLike<T>): Promise<T> {
69
+ const previous = this.queues.get(name) ?? Promise.resolve();
70
+ const run = (async () => {
71
+ await previous.catch(() => undefined);
72
+ return callback();
73
+ })();
74
+ this.queues.set(name, run.catch(() => undefined));
75
+ return run;
76
+ }
77
+ }
78
+
79
+ /** Installs a {@link FakeLockManager} as `navigator.locks`; see {@link installNavigatorLocks}. */
80
+ function installFakeLockManager(): () => void {
81
+ return installNavigatorLocks(new FakeLockManager());
82
+ }
83
+
84
+ describe('BrowserOnlyPluginServer', () => {
85
+
86
+ let storageService: InMemoryStorageService;
87
+
88
+ function createServer(): BrowserOnlyPluginServer {
89
+ const container = new Container();
90
+ container.bind(BrowserOnlyPluginServer).toSelf().inSingletonScope();
91
+ container.bind(ILogger).to(MockLogger);
92
+ container.bind<StorageService>(LocalStorageService).toConstantValue(storageService);
93
+ container.bind(PluginPathsService).toConstantValue(pluginPathsService);
94
+ return container.get(BrowserOnlyPluginServer);
95
+ }
96
+
97
+ beforeEach(() => {
98
+ storageService = new InMemoryStorageService();
99
+ });
100
+
101
+ it('keeps global state across sessions', async () => {
102
+ expect(await createServer().setStorageValue('my.plugin', { count: 1 }, undefined)).to.be.true;
103
+
104
+ // a fresh server stands for the next session, reading from the same browser storage
105
+ expect(await createServer().getStorageValue('my.plugin', undefined)).to.deep.equal({ count: 1 });
106
+ });
107
+
108
+ it('keeps global state independent of a workspace-scoped StorageService binding', async () => {
109
+ const container = new Container();
110
+ container.bind(BrowserOnlyPluginServer).toSelf().inSingletonScope();
111
+ container.bind(ILogger).to(MockLogger);
112
+ container.bind<StorageService>(LocalStorageService).toConstantValue(storageService);
113
+ // simulates `@theia/workspace` rebinding the plain `StorageService` symbol; `BrowserOnlyPluginServer`
114
+ // must ignore it and inject `LocalStorageService` directly, or global state would end up
115
+ // scoped per workspace too
116
+ container.bind(StorageService).toConstantValue(new PrefixingStorageService(storageService, 'file:///a'));
117
+ container.bind(PluginPathsService).toConstantValue(pluginPathsService);
118
+ const server = container.get(BrowserOnlyPluginServer);
119
+
120
+ await server.setStorageValue('my.plugin', { count: 1 }, undefined);
121
+
122
+ expect(storageService.data.has('plugin-storage:global')).to.be.true;
123
+ expect(storageService.data.has('file:///a:plugin-storage:global')).to.be.false;
124
+ });
125
+
126
+ it('keeps workspace state per workspace', async () => {
127
+ const server = createServer();
128
+ await server.setStorageValue('my.plugin', { count: 1 }, { workspace: 'file:///a', roots: [] });
129
+ await server.setStorageValue('my.plugin', { count: 2 }, { workspace: 'file:///b', roots: [] });
130
+
131
+ expect(await server.getStorageValue('my.plugin', { workspace: 'file:///a', roots: [] })).to.deep.equal({ count: 1 });
132
+ expect(await server.getStorageValue('my.plugin', { workspace: 'file:///b', roots: [] })).to.deep.equal({ count: 2 });
133
+ });
134
+
135
+ it('keeps global and workspace state apart', async () => {
136
+ const server = createServer();
137
+ await server.setStorageValue('my.plugin', { scope: 'global' }, undefined);
138
+ await server.setStorageValue('my.plugin', { scope: 'workspace' }, { workspace: 'file:///a', roots: [] });
139
+
140
+ expect(await server.getStorageValue('my.plugin', undefined)).to.deep.equal({ scope: 'global' });
141
+ expect(await server.getAllStorageValues({ workspace: 'file:///a', roots: [] })).to.deep.equal({ 'my.plugin': { scope: 'workspace' } });
142
+ });
143
+
144
+ it('cannot keep workspace state while no workspace is open', async () => {
145
+ const server = createServer();
146
+
147
+ expect(await server.setStorageValue('my.plugin', { count: 1 }, { workspace: undefined, roots: [] })).to.be.false;
148
+ expect(await server.getAllStorageValues({ workspace: undefined, roots: [] })).to.deep.equal({});
149
+ });
150
+
151
+ it('does not drop what another host stored in the meantime', async () => {
152
+ const one = createServer();
153
+ // stands for another tab of the application, running its own plugin host over the same browser storage
154
+ const other = createServer();
155
+ await one.setStorageValue('plugin.one', { count: 1 }, undefined);
156
+ // as the plugin host does on start up, before the other tab writes
157
+ await one.getAllStorageValues(undefined);
158
+
159
+ await other.setStorageValue('plugin.other', { count: 2 }, undefined);
160
+ await one.setStorageValue('plugin.one', { count: 3 }, undefined);
161
+
162
+ expect(await createServer().getAllStorageValues(undefined)).to.deep.equal({
163
+ 'plugin.one': { count: 3 },
164
+ 'plugin.other': { count: 2 }
165
+ });
166
+ });
167
+
168
+ describe('concurrent writes', () => {
169
+
170
+ async function assertNeitherUpdateIsLost(one: BrowserOnlyPluginServer, other: BrowserOnlyPluginServer): Promise<void> {
171
+ // start both writes before awaiting either, so they actually overlap
172
+ await Promise.all([
173
+ one.setStorageValue('plugin.one', { count: 1 }, undefined),
174
+ other.setStorageValue('plugin.other', { count: 2 }, undefined)
175
+ ]);
176
+
177
+ expect(await createServer().getAllStorageValues(undefined)).to.deep.equal({
178
+ 'plugin.one': { count: 1 },
179
+ 'plugin.other': { count: 2 }
180
+ });
181
+ }
182
+
183
+ it('does not lose either update from two tabs, serialized via the Web Locks API', async () => {
184
+ const restore = installFakeLockManager();
185
+ try {
186
+ // one server per tab, sharing the same browser storage
187
+ await assertNeitherUpdateIsLost(createServer(), createServer());
188
+ } finally {
189
+ restore();
190
+ }
191
+ });
192
+
193
+ it('does not lose either update within one tab when the Web Locks API is unavailable', async () => {
194
+ const server = createServer();
195
+ await assertNeitherUpdateIsLost(server, server);
196
+ });
197
+ });
198
+
199
+ it('drops a value that is set to nothing', async () => {
200
+ const server = createServer();
201
+ await server.setStorageValue('my.plugin', { count: 1 }, undefined);
202
+
203
+ await server.setStorageValue('my.plugin', {}, undefined);
204
+
205
+ expect(await createServer().getAllStorageValues(undefined)).to.deep.equal({});
206
+ });
207
+
208
+ it('answers with an empty value for an unknown key', async () => {
209
+ expect(await createServer().getStorageValue('my.plugin', undefined)).to.deep.equal({});
210
+ });
211
+
212
+ it('rejects changing the deployed plugins', async () => {
213
+ const server = createServer();
214
+
215
+ await server.install('my.plugin').then(
216
+ () => expect.fail('should have rejected'),
217
+ error => expect(error.message).to.contain('browser-only'));
218
+ await server.uninstall('theia.a@1.0.0').then(
219
+ () => expect.fail('should have rejected'),
220
+ error => expect(error.message).to.contain('browser-only'));
221
+ await server.enablePlugin('theia.a').then(
222
+ () => expect.fail('should have rejected'),
223
+ error => expect(error.message).to.contain('browser-only'));
224
+ await server.disablePlugin('theia.a').then(
225
+ () => expect.fail('should have rejected'),
226
+ error => expect(error.message).to.contain('browser-only'));
227
+ });
228
+ });
@@ -0,0 +1,165 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 Robert Jandow
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { inject, injectable, named } from '@theia/core/shared/inversify';
18
+ import { Mutex } from 'async-mutex';
19
+ import { ILogger, nls } from '@theia/core';
20
+ import { LocalStorageService, StorageService } from '@theia/core/lib/browser/storage-service';
21
+ import { PluginDeployOptions, PluginIdentifiers, PluginServer, PluginStorageKind, PluginType } from '../../common';
22
+ import { KeysToAnyValues, KeysToKeysToAnyValue } from '../../common/types';
23
+ import { PluginPathsService } from '../../main/common/plugin-paths-protocol';
24
+
25
+ const GLOBAL_STORAGE_KEY = 'plugin-storage:global';
26
+ const WORKSPACE_STORAGE_KEY_PREFIX = 'plugin-storage:workspace:';
27
+ const LOCK_NAME_PREFIX = 'theia:plugin-storage:';
28
+
29
+ /**
30
+ * Plugins of a browser-only application are deployed at build time, so they can't be installed,
31
+ * uninstalled, enabled or disabled at runtime. The queries just report an empty result instead
32
+ * of failing, so callers like the plugin view still have something to render.
33
+ *
34
+ * The plugin key-value storage backing `ExtensionContext.globalState` and
35
+ * `ExtensionContext.workspaceState` lives in the browser's local storage.
36
+ */
37
+ @injectable()
38
+ export class BrowserOnlyPluginServer implements PluginServer {
39
+
40
+ @inject(ILogger) @named('plugin-ext:BrowserOnlyPluginServer')
41
+ protected readonly logger: ILogger;
42
+
43
+ // `@theia/workspace` rebinds `StorageService` to `WorkspaceStorageService`, which prefixes every
44
+ // key with the current workspace URI. That would scope `GLOBAL_STORAGE_KEY` per workspace too,
45
+ // so `LocalStorageService` is injected directly instead - the workspace store key already
46
+ // encodes the workspace via `PluginPathsService.getHostStoragePath`.
47
+ @inject(LocalStorageService)
48
+ protected readonly storageService: StorageService;
49
+
50
+ @inject(PluginPathsService)
51
+ protected readonly pluginPathsService: PluginPathsService;
52
+
53
+ /** Fallback for {@link withStoreLock} when the Web Locks API is unavailable. */
54
+ protected readonly localLock = new Mutex();
55
+ protected missingLocksWarned = false;
56
+
57
+ async install(pluginEntry: string, type?: PluginType, options?: PluginDeployOptions): Promise<void> {
58
+ throw new Error(nls.localize('theia/plugin-ext/browserOnlyInstallUnsupported', 'Installing plugins is not supported in a browser-only application.'));
59
+ }
60
+
61
+ async uninstall(pluginId: PluginIdentifiers.VersionedId): Promise<void> {
62
+ throw new Error(nls.localize('theia/plugin-ext/browserOnlyUninstallUnsupported', 'Uninstalling plugins is not supported in a browser-only application.'));
63
+ }
64
+
65
+ async enablePlugin(pluginId: PluginIdentifiers.UnversionedId): Promise<boolean> {
66
+ throw new Error(nls.localize('theia/plugin-ext/browserOnlyEnableUnsupported', 'Enabling plugins is not supported in a browser-only application.'));
67
+ }
68
+
69
+ async disablePlugin(pluginId: PluginIdentifiers.UnversionedId): Promise<boolean> {
70
+ throw new Error(nls.localize('theia/plugin-ext/browserOnlyDisableUnsupported', 'Disabling plugins is not supported in a browser-only application.'));
71
+ }
72
+
73
+ async getInstalledPlugins(): Promise<readonly PluginIdentifiers.VersionedId[]> {
74
+ return [];
75
+ }
76
+
77
+ async getUninstalledPlugins(): Promise<readonly PluginIdentifiers.VersionedId[]> {
78
+ return [];
79
+ }
80
+
81
+ async getDisabledPlugins(): Promise<readonly PluginIdentifiers.UnversionedId[]> {
82
+ return [];
83
+ }
84
+
85
+ async setStorageValue(key: string, value: KeysToAnyValues, kind: PluginStorageKind): Promise<boolean> {
86
+ const storeKey = await this.getStoreKey(kind);
87
+ if (!storeKey) {
88
+ this.logger.warn('Cannot save plugin data: no opened workspace.');
89
+ return false;
90
+ }
91
+ // the browser storage is shared with this application's other tabs, each running its own
92
+ // plugin host, so the read and write below need to be atomic - otherwise a concurrent
93
+ // update from another tab could land in between and get overwritten by this one
94
+ await this.withStoreLock(storeKey, async () => {
95
+ const store = await this.getStore(storeKey);
96
+ if (value === undefined || Object.keys(value).length === 0) {
97
+ delete store[key];
98
+ } else {
99
+ store[key] = value;
100
+ }
101
+ await this.storageService.setData(storeKey, store);
102
+ });
103
+ return true;
104
+ }
105
+
106
+ async getStorageValue(key: string, kind: PluginStorageKind): Promise<KeysToAnyValues> {
107
+ return (await this.getAllStorageValues(kind))[key] ?? {};
108
+ }
109
+
110
+ async getAllStorageValues(kind: PluginStorageKind): Promise<KeysToKeysToAnyValue> {
111
+ const storeKey = await this.getStoreKey(kind);
112
+ return storeKey ? this.getStore(storeKey) : {};
113
+ }
114
+
115
+ protected getStore(storeKey: string): Promise<KeysToKeysToAnyValue> {
116
+ return this.storageService.getData<KeysToKeysToAnyValue>(storeKey, {});
117
+ }
118
+
119
+ /**
120
+ * Runs `task` while holding the cross-tab lock for `storeKey`, via the Web Locks API, so a
121
+ * concurrent {@link setStorageValue} on this or another tab can't interleave with it.
122
+ */
123
+ protected withStoreLock<T>(storeKey: string, task: () => Promise<T>): Promise<T> {
124
+ const locks = this.getWebLocks();
125
+ if (locks) {
126
+ return this.requestLock(locks, `${LOCK_NAME_PREFIX}${storeKey}`, task);
127
+ }
128
+ // no Web Locks API (insecure context, older browser): fall back to serializing writes
129
+ // within this tab. A write from another tab can still race and get lost.
130
+ if (!this.missingLocksWarned) {
131
+ this.missingLocksWarned = true;
132
+ this.logger.warn('Web Locks API unavailable: plugin storage updates from different tabs may race.');
133
+ }
134
+ return this.localLock.runExclusive(task);
135
+ }
136
+
137
+ /** The Web Locks API of this browsing context, or `undefined` in an insecure context or an older browser. */
138
+ protected getWebLocks(): LockManager | undefined {
139
+ return typeof navigator === 'object' ? navigator.locks : undefined;
140
+ }
141
+
142
+ /**
143
+ * Requests `name` from `locks` and runs `callback` once granted, same as `LockManager.request()`.
144
+ *
145
+ * `LockGrantedCallback` is typed as `(lock: Lock | null) => T`, which doesn't account for an
146
+ * async or never-settling callback even though the API itself supports and awaits one, hence
147
+ * the cast. Kept as its own method so a subclass or test can swap the lock implementation.
148
+ */
149
+ protected requestLock<T>(locks: LockManager, name: string, callback: () => T | PromiseLike<T>): Promise<T> {
150
+ return locks.request<T>(name, callback as unknown as LockGrantedCallback<T>);
151
+ }
152
+
153
+ /**
154
+ * The browser storage key holding the given kind of values, or `undefined` if there's
155
+ * nowhere to keep them - e.g. workspace state while no workspace is open.
156
+ */
157
+ protected async getStoreKey(kind: PluginStorageKind): Promise<string | undefined> {
158
+ if (!kind) {
159
+ return GLOBAL_STORAGE_KEY;
160
+ }
161
+ // derived from the storage path so workspace state follows `ExtensionContext.storageUri`
162
+ const storagePath = await this.pluginPathsService.getHostStoragePath(kind.workspace, kind.roots);
163
+ return storagePath && `${WORKSPACE_STORAGE_KEY_PREFIX}${storagePath}`;
164
+ }
165
+ }
@@ -0,0 +1,54 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 Robert Jandow
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ /**
18
+ * The subset of `LockManager` that the fakes in these tests actually implement. The real
19
+ * `LockManager.request()` is overloaded with an optional `options` parameter that none of our
20
+ * fakes need, so typing {@link installFakeLockManager} directly against the DOM `LockManager`
21
+ * interface would reject every fake as an incompatible overload - we cast to the real type just
22
+ * once instead, at the point where the fake gets installed as `navigator.locks`.
23
+ */
24
+ export interface FakeLockManager {
25
+ request<T>(name: string, callback: () => T | PromiseLike<T>): Promise<T>;
26
+ query?(): Promise<{ held?: Array<{ name?: string }> }>;
27
+ }
28
+
29
+ /**
30
+ * Installs `manager` as `navigator.locks` and returns a function that restores whatever was
31
+ * there before. Doesn't assume `navigator` is even defined: `enableJSDOM()`/`disableJSDOM()`
32
+ * (see `packages/core/src/browser/test/jsdom.ts`) deletes the global `navigator` that Node
33
+ * normally provides as a side effect of disabling jsdom, and depending on which spec files ran
34
+ * earlier in this mocha process, it may already be gone by the time a test runs.
35
+ */
36
+ export function installFakeLockManager(manager: FakeLockManager): () => void {
37
+ const target = globalThis as { navigator?: { locks?: unknown } };
38
+ const hadNavigator = typeof navigator !== 'undefined';
39
+ if (!hadNavigator) {
40
+ Object.defineProperty(target, 'navigator', { value: {}, configurable: true });
41
+ }
42
+ const originalLocks = Object.getOwnPropertyDescriptor(navigator, 'locks');
43
+ Object.defineProperty(navigator, 'locks', { value: manager as unknown as LockManager, configurable: true });
44
+
45
+ return () => {
46
+ if (!hadNavigator) {
47
+ delete target.navigator;
48
+ } else if (originalLocks) {
49
+ Object.defineProperty(navigator, 'locks', originalLocks);
50
+ } else {
51
+ delete (navigator as { locks?: unknown }).locks;
52
+ }
53
+ };
54
+ }
@@ -23,7 +23,7 @@
23
23
 
24
24
  import debounce = require('@theia/core/shared/lodash.debounce');
25
25
  import { injectable, inject, interfaces, named, postConstruct, unmanaged } from '@theia/core/shared/inversify';
26
- import { PluginMetadata, HostedPluginServer, DeployedPlugin, PluginServer, PluginIdentifiers } from '../../common/plugin-protocol';
26
+ import { PluginMetadata, HostedPluginServer, DeployedPlugin, PluginServer, PluginIdentifiers, PLUGIN_HOST_FRONTEND } from '../../common/plugin-protocol';
27
27
  import { AbstractPluginManagerExt, ConfigStorage } from '../../common/plugin-api-rpc';
28
28
  import {
29
29
  Disposable, DisposableCollection, Emitter,
@@ -37,7 +37,7 @@ import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
37
37
  import { environment } from '@theia/core/shared/@theia/application-package/lib/environment';
38
38
  import { Measurement, Stopwatch } from '@theia/core/lib/common';
39
39
 
40
- export type PluginHost = 'frontend' | string;
40
+ export type PluginHost = typeof PLUGIN_HOST_FRONTEND | string;
41
41
 
42
42
  export const ALL_ACTIVATION_EVENT = '*';
43
43
 
@@ -336,7 +336,7 @@ export abstract class AbstractHostedPluginSupport<PM extends AbstractPluginManag
336
336
 
337
337
  if (contributions.state === PluginContributions.State.LOADED) {
338
338
  contributions.state = PluginContributions.State.STARTING;
339
- const host = plugin.model.entryPoint.frontend ? 'frontend' : plugin.host;
339
+ const host = plugin.model.entryPoint.frontend ? PLUGIN_HOST_FRONTEND : plugin.host;
340
340
  const dynamicContributions = hostContributions.get(host) || [];
341
341
  dynamicContributions.push(contributions);
342
342
  hostContributions.set(host, dynamicContributions);
@@ -381,7 +381,7 @@ export abstract class AbstractHostedPluginSupport<PM extends AbstractPluginManag
381
381
 
382
382
  for (const [host, hostContributions] of contributionsByHost) {
383
383
  // do not start plugins for electron browser
384
- if (host === 'frontend' && environment.electron.is()) {
384
+ if (host === PLUGIN_HOST_FRONTEND && environment.electron.is()) {
385
385
  continue;
386
386
  }
387
387
 
@@ -39,6 +39,7 @@ import { setupPluginHostLogger } from './plugin-host-logger';
39
39
  import { LmExtImpl } from '../../plugin/lm-ext';
40
40
  import { LanguageModelToolsExtImpl } from '../../plugin/lm-tool-ext';
41
41
  import { EncodingService } from '@theia/core/lib/common/encoding-service';
42
+ import { TelemetryExtImpl } from '../../plugin/telemetry-ext';
42
43
 
43
44
  export default new ContainerModule(bind => {
44
45
  const channel = new IPCChannel();
@@ -76,4 +77,5 @@ export default new ContainerModule(bind => {
76
77
  bind(WebviewsExtImpl).toSelf().inSingletonScope();
77
78
  bind(MinimalTerminalServiceExt).toService(TerminalServiceExtImpl);
78
79
  bind(TerminalServiceExtImpl).toSelf().inSingletonScope();
80
+ bind(TelemetryExtImpl).toSelf().inSingletonScope();
79
81
  });
@@ -31,6 +31,7 @@ import {
31
31
  import { PluginMetadata, PluginModel, PluginPackage } from '../../common/plugin-protocol';
32
32
  import { createAPIFactory } from '../../plugin/plugin-context';
33
33
  import { EnvExtImpl } from '../../plugin/env';
34
+ import { TelemetryExtImpl } from '../../plugin/telemetry-ext';
34
35
  import { PreferenceRegistryExtImpl } from '../../plugin/preference-registry';
35
36
  import { ExtPluginApi, ExtPluginApiBackendInitializationFn } from '../../common/plugin-ext-api-contribution';
36
37
  import { DebugExtImpl } from '../../plugin/debug/debug-ext';
@@ -64,7 +65,8 @@ export interface ExtInterfaces {
64
65
  webviewExt: WebviewsExtImpl,
65
66
  terminalServiceExt: TerminalServiceExtImpl,
66
67
  secretsExt: SecretsExtImpl,
67
- localizationExt: LocalizationExtImpl
68
+ localizationExt: LocalizationExtImpl,
69
+ telemetryExt: TelemetryExtImpl
68
70
  }
69
71
 
70
72
  /**
@@ -352,6 +354,9 @@ export class PluginHostRPC extends AbstractPluginHostRPC<PluginManagerExtImpl, P
352
354
  @inject(SecretsExtImpl)
353
355
  protected readonly secretsExt: SecretsExtImpl;
354
356
 
357
+ @inject(TelemetryExtImpl)
358
+ protected readonly telemetryExt: TelemetryExtImpl;
359
+
355
360
  constructor() {
356
361
  super('PLUGIN_HOST', '/scanners/backend-init-theia.js',
357
362
  {
@@ -380,18 +385,19 @@ export class PluginHostRPC extends AbstractPluginHostRPC<PluginManagerExtImpl, P
380
385
  webviewExt: this.webviewExt,
381
386
  terminalServiceExt: this.terminalServiceExt,
382
387
  secretsExt: this.secretsExt,
383
- localizationExt: this.localizationExt
388
+ localizationExt: this.localizationExt,
389
+ telemetryExt: this.telemetryExt
384
390
  };
385
391
  }
386
392
 
387
393
  protected createAPIFactory(extInterfaces: ExtInterfaces): PluginAPIFactory {
388
394
  const {
389
395
  envExt, debugExt, preferenceRegistryExt, editorsAndDocumentsExt, workspaceExt,
390
- messageRegistryExt, clipboardExt, webviewExt, localizationExt
396
+ messageRegistryExt, clipboardExt, webviewExt, localizationExt, telemetryExt
391
397
  } = extInterfaces;
392
398
  return createAPIFactory(this.rpc, this.pluginManager, envExt, debugExt, preferenceRegistryExt,
393
399
  editorsAndDocumentsExt, workspaceExt, messageRegistryExt, clipboardExt, webviewExt,
394
- localizationExt);
400
+ localizationExt, telemetryExt);
395
401
  }
396
402
 
397
403
  protected initExtApi(extApi: ExtPluginApi): void {