@theia/plugin-ext 1.76.0-next.6 → 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,129 @@
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 } from '@theia/core/shared/inversify';
18
+ import { Disposable, Event, Emitter, URI } from '@theia/core';
19
+ import { Endpoint } from '@theia/core/lib/browser';
20
+ import {
21
+ createFileSystemProviderError, FileChange, FileDeleteOptions, FileOverwriteOptions, FilePermission, FileSystemProvider, FileSystemProviderCapabilities,
22
+ FileSystemProviderErrorCode, FileType, FileWriteOptions, Stat, WatchOptions
23
+ } from '@theia/filesystem/lib/common/files';
24
+ import { FileService, FileServiceContribution } from '@theia/filesystem/lib/browser/file-service';
25
+ import { PLUGINS_BASE_PATH, PLUGINS_SCHEME } from '@theia/plugin-utils/lib/common/constants';
26
+ import { encodePluginAssetPath } from '@theia/plugin-utils/lib/common/plugin-model';
27
+
28
+ /**
29
+ * Reads the plugin assets the build dropped under `PLUGINS_BASE_PATH`. With a backend those come
30
+ * off its disk via `file:`; browser-only has to fetch them over HTTP.
31
+ *
32
+ * Read-only, and only for what goes through the `FileService`: color themes, icon themes, icon
33
+ * fonts.
34
+ */
35
+ @injectable()
36
+ export class BrowserOnlyPluginFileSystemProvider implements FileSystemProvider {
37
+
38
+ readonly capabilities = FileSystemProviderCapabilities.Readonly | FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.PathCaseSensitive;
39
+ readonly onDidChangeCapabilities = Event.None;
40
+ readonly onFileWatchError = Event.None;
41
+
42
+ protected readonly onDidChangeFileEmitter = new Emitter<readonly FileChange[]>();
43
+ readonly onDidChangeFile = this.onDidChangeFileEmitter.event;
44
+
45
+ watch(resource: URI, opts: WatchOptions): Disposable {
46
+ // baked into the build, nothing to watch
47
+ return Disposable.NULL;
48
+ }
49
+
50
+ async stat(resource: URI): Promise<Stat> {
51
+ const response = await this.fetch(resource, 'HEAD');
52
+ const length = Number(response.headers.get('content-length'));
53
+ const modified = Date.parse(response.headers.get('last-modified') ?? '');
54
+ // Build output, so created and modified are the same. Falling back to 0 rather than
55
+ // `Date.now()` keeps it stable - callers cache on `mtime`.
56
+ const mtime = Number.isNaN(modified) ? 0 : modified;
57
+ return {
58
+ type: FileType.File,
59
+ permissions: FilePermission.Readonly,
60
+ mtime,
61
+ ctime: mtime,
62
+ size: Number.isNaN(length) ? 0 : length
63
+ };
64
+ }
65
+
66
+ async readFile(resource: URI): Promise<Uint8Array> {
67
+ const response = await this.fetch(resource, 'GET');
68
+ return new Uint8Array(await response.arrayBuffer());
69
+ }
70
+
71
+ /**
72
+ * Maps the URI back to the path the build actually wrote, built through `Endpoint` so this
73
+ * still works when the application is deployed under a sub-path. `URI.path` is decoded, so it
74
+ * has to be re-encoded - otherwise a `#` or `?` in an asset name would be read as a fragment or
75
+ * query.
76
+ */
77
+ protected toUrl(resource: URI): string {
78
+ return new Endpoint({ path: `${PLUGINS_BASE_PATH}${encodePluginAssetPath(resource.path.toString())}` }).getRestUrl().toString();
79
+ }
80
+
81
+ protected async fetch(resource: URI, method: 'GET' | 'HEAD'): Promise<Response> {
82
+ if (resource.scheme !== PLUGINS_SCHEME) {
83
+ throw createFileSystemProviderError(`Not a plugin asset: '${resource.toString()}'`, FileSystemProviderErrorCode.FileNotFound);
84
+ }
85
+ let response: Response;
86
+ try {
87
+ response = await fetch(this.toUrl(resource), { method });
88
+ } catch (error) {
89
+ throw createFileSystemProviderError(`Could not read '${resource.toString()}': ${error.message}`, FileSystemProviderErrorCode.Unavailable);
90
+ }
91
+ if (!response.ok) {
92
+ // A static server 404s for directories too, but nothing asks us for one.
93
+ const code = response.status === 404 ? FileSystemProviderErrorCode.FileNotFound : FileSystemProviderErrorCode.Unknown;
94
+ throw createFileSystemProviderError(`Could not read '${resource.toString()}': ${response.status} ${response.statusText}`, code);
95
+ }
96
+ return response;
97
+ }
98
+
99
+ readdir(resource: URI): Promise<[string, FileType][]> {
100
+ throw createFileSystemProviderError('Plugin assets cannot be listed.', FileSystemProviderErrorCode.FileNotADirectory);
101
+ }
102
+
103
+ mkdir(resource: URI): Promise<void> {
104
+ throw createFileSystemProviderError('Plugin assets are read-only.', FileSystemProviderErrorCode.NoPermissions);
105
+ }
106
+
107
+ delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
108
+ throw createFileSystemProviderError('Plugin assets are read-only.', FileSystemProviderErrorCode.NoPermissions);
109
+ }
110
+
111
+ rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
112
+ throw createFileSystemProviderError('Plugin assets are read-only.', FileSystemProviderErrorCode.NoPermissions);
113
+ }
114
+
115
+ writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
116
+ throw createFileSystemProviderError('Plugin assets are read-only.', FileSystemProviderErrorCode.NoPermissions);
117
+ }
118
+ }
119
+
120
+ @injectable()
121
+ export class BrowserOnlyPluginFileServiceContribution implements FileServiceContribution {
122
+
123
+ @inject(BrowserOnlyPluginFileSystemProvider)
124
+ protected readonly provider: BrowserOnlyPluginFileSystemProvider;
125
+
126
+ registerFileSystemProviders(service: FileService): void {
127
+ service.registerProvider(PLUGINS_SCHEME, this.provider);
128
+ }
129
+ }
@@ -0,0 +1,199 @@
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 { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
18
+ // `FileService` transitively imports browser modules that touch `document` at load time.
19
+ const disableJSDOM = enableJSDOM();
20
+
21
+ import { expect } from 'chai';
22
+ import { ILogger } from '@theia/core';
23
+ import URI from '@theia/core/lib/common/uri';
24
+ import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
25
+ import { Container } from '@theia/core/shared/inversify';
26
+ import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
27
+ import { FileService } from '@theia/filesystem/lib/browser/file-service';
28
+ import { UntitledWorkspaceService, WorkspaceFileService } from '@theia/workspace/lib/common';
29
+ import { BrowserOnlyPluginPathsService } from './browser-only-plugin-paths-service';
30
+
31
+ disableJSDOM();
32
+
33
+ /* eslint-disable @typescript-eslint/no-explicit-any */
34
+
35
+ /** The directories of the browser local file system, enough of it for the path service. */
36
+ class FakeFileService {
37
+ readonly directories = new Set<string>();
38
+ /** Lets tests assert that a cached path didn't go through {@link createFolder} again. */
39
+ createFolderCallCount = 0;
40
+
41
+ async createFolder(uri: URI): Promise<void> {
42
+ this.createFolderCallCount++;
43
+ for (let current = uri; !current.path.isRoot; current = current.parent) {
44
+ this.directories.add(current.path.toString());
45
+ }
46
+ }
47
+
48
+ async fsPath(uri: URI): Promise<string> {
49
+ return uri.path.toString();
50
+ }
51
+
52
+ async resolve(uri: URI): Promise<any> {
53
+ const parent = uri.path.toString();
54
+ const children = [...this.directories]
55
+ .filter(directory => new URI(directory).parent.path.toString() === parent)
56
+ .map(directory => ({ isDirectory: true, resource: new URI(directory) }));
57
+ return { isDirectory: true, resource: uri, children };
58
+ }
59
+
60
+ async delete(uri: URI): Promise<void> {
61
+ const prefix = `${uri.path.toString()}/`;
62
+ for (const directory of [...this.directories]) {
63
+ if (directory === uri.path.toString() || directory.startsWith(prefix)) {
64
+ this.directories.delete(directory);
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ describe('BrowserOnlyPluginPathsService', () => {
71
+
72
+ let fileService: FakeFileService;
73
+ let service: BrowserOnlyPluginPathsService;
74
+
75
+ function createService(): BrowserOnlyPluginPathsService {
76
+ const container = new Container();
77
+ container.bind(BrowserOnlyPluginPathsService).toSelf().inSingletonScope();
78
+ container.bind(ILogger).to(MockLogger);
79
+ container.bind(FileService).toConstantValue(fileService as any);
80
+ container.bind(WorkspaceFileService).toSelf().inSingletonScope();
81
+ container.bind(UntitledWorkspaceService).toSelf().inSingletonScope();
82
+ container.bind(EnvVariablesServer).toConstantValue({
83
+ getConfigDirUri: async () => 'file:///.theia'
84
+ } as EnvVariablesServer);
85
+ return container.get(BrowserOnlyPluginPathsService);
86
+ }
87
+
88
+ /** A fake session folder name for the `session`th day of December 2025, e.g. `20251203T000000`. */
89
+ function sessionFolderName(session: number): string {
90
+ const day = String(session).padStart(2, '0');
91
+ return `202512${day}T000000`;
92
+ }
93
+
94
+ function sessionFolderUri(session: number): URI {
95
+ return new URI(`file:///.theia/logs/${sessionFolderName(session)}/host`);
96
+ }
97
+
98
+ beforeEach(() => {
99
+ fileService = new FakeFileService();
100
+ service = createService();
101
+ });
102
+
103
+ describe('host log path', () => {
104
+
105
+ it('creates a session log folder', async () => {
106
+ const logPath = await service.getHostLogPath();
107
+
108
+ expect(logPath).to.match(/^\/\.theia\/logs\/\d{8}T\d{6}\/host$/);
109
+ expect(fileService.directories).to.include(logPath);
110
+ });
111
+
112
+ it('resolves the log path only once', async () => {
113
+ expect(await service.getHostLogPath()).to.equal(await service.getHostLogPath());
114
+ });
115
+
116
+ it('keeps the ten most recent session folders, the one of this session among them', async () => {
117
+ for (let session = 1; session <= 12; session++) {
118
+ await fileService.createFolder(sessionFolderUri(session));
119
+ }
120
+
121
+ const logPath = await service.getHostLogPath();
122
+
123
+ const sessions = sessionFolders();
124
+ expect(sessions).to.have.lengthOf(10);
125
+ expect(logPath).to.contain(sessions.find(session => !session.startsWith('/.theia/logs/202512')));
126
+ // the three oldest gave way to the folder of this session
127
+ expect(sessions).to.not.include(`/.theia/logs/${sessionFolderName(1)}`);
128
+ expect(sessions).to.not.include(`/.theia/logs/${sessionFolderName(3)}`);
129
+ expect(sessions).to.include(`/.theia/logs/${sessionFolderName(4)}`);
130
+ expect(sessions).to.include(`/.theia/logs/${sessionFolderName(12)}`);
131
+ });
132
+
133
+ it('never cleans up a folder that is not a session folder', async () => {
134
+ for (let session = 1; session <= 12; session++) {
135
+ await fileService.createFolder(sessionFolderUri(session));
136
+ }
137
+ await fileService.createFolder(new URI('file:///.theia/logs/not-a-session'));
138
+
139
+ await service.getHostLogPath();
140
+ // getHostLogPath() doesn't wait for the cleanup, so give it a tick to finish
141
+ await new Promise(resolve => setImmediate(resolve));
142
+
143
+ expect(fileService.directories).to.include('/.theia/logs/not-a-session');
144
+ });
145
+
146
+ function sessionFolders(): string[] {
147
+ return [...fileService.directories].filter(directory => /^\/\.theia\/logs\/[^/]+$/.test(directory)).sort();
148
+ }
149
+ });
150
+
151
+ describe('host storage path', () => {
152
+
153
+ it('has nowhere to store while no workspace is open', async () => {
154
+ expect(await service.getHostStoragePath(undefined, [])).to.be.undefined;
155
+ });
156
+
157
+ it('gives every workspace a storage folder of its own', async () => {
158
+ const one = await service.getHostStoragePath('file:///one', []);
159
+ const other = await service.getHostStoragePath('file:///other', []);
160
+
161
+ expect(one).to.match(/^\/\.theia\/workspace-storage\/.+$/);
162
+ expect(one).to.not.equal(other);
163
+ expect(fileService.directories).to.include(one!);
164
+ });
165
+
166
+ it('gives a workspace the same storage folder in every session', async () => {
167
+ expect(await service.getHostStoragePath('file:///one', [])).to.equal(await service.getHostStoragePath('file:///one', []));
168
+ });
169
+
170
+ it('keys the storage of an untitled workspace on its roots, which outlive its name', async () => {
171
+ const roots = ['file:///a', 'file:///b'];
172
+ const one = await service.getHostStoragePath('file:///.theia/workspaces/Untitled-1.theia-workspace', roots);
173
+ // the same roots under the name the next session gives the untitled workspace
174
+ const next = await service.getHostStoragePath('file:///.theia/workspaces/Untitled-2.theia-workspace', [...roots].reverse());
175
+
176
+ expect(one).to.equal(next);
177
+ });
178
+
179
+ it('resolves the storage path only once per workspace', async () => {
180
+ await service.getHostStoragePath('file:///one', []);
181
+ const callsAfterFirst = fileService.createFolderCallCount;
182
+
183
+ await service.getHostStoragePath('file:///one', []);
184
+
185
+ expect(fileService.createFolderCallCount).to.equal(callsAfterFirst);
186
+ });
187
+
188
+ it('resolves a new storage path when the roots of an untitled workspace change', async () => {
189
+ const workspaceUri = 'file:///.theia/workspaces/Untitled-1.theia-workspace';
190
+ const first = await service.getHostStoragePath(workspaceUri, ['file:///a']);
191
+ const callsAfterFirst = fileService.createFolderCallCount;
192
+
193
+ const second = await service.getHostStoragePath(workspaceUri, ['file:///a', 'file:///b']);
194
+
195
+ expect(second).to.not.equal(first);
196
+ expect(fileService.createFolderCallCount).to.be.greaterThan(callsAfterFirst);
197
+ });
198
+ });
199
+ });
@@ -0,0 +1,145 @@
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 { ILogger } from '@theia/core';
19
+ import URI from '@theia/core/lib/common/uri';
20
+ import { hashValue } from '@theia/core/lib/common/uuid';
21
+ import { compare } from '@theia/core/lib/common/strings';
22
+ import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
23
+ import { FileService } from '@theia/filesystem/lib/browser/file-service';
24
+ import { UntitledWorkspaceService } from '@theia/workspace/lib/common';
25
+ import { PluginPathsService } from '../../main/common/plugin-paths-protocol';
26
+ import { PluginPaths } from '../../main/common/paths/const';
27
+
28
+ /** Session folder name, e.g. `20181205T093828`, same format as the backend uses. */
29
+ const SESSION_TIMESTAMP_PATTERN = /^\d{8}T\d{6}$/;
30
+
31
+ /**
32
+ * Resolves plugin log and storage locations for a browser-only application, under the config
33
+ * directory, which lives on the same (browser-local) file system as the workspace.
34
+ */
35
+ @injectable()
36
+ export class BrowserOnlyPluginPathsService implements PluginPathsService {
37
+
38
+ @inject(ILogger) @named('plugin-ext:BrowserOnlyPluginPathsService')
39
+ protected readonly logger: ILogger;
40
+
41
+ @inject(EnvVariablesServer)
42
+ protected readonly envServer: EnvVariablesServer;
43
+
44
+ @inject(FileService)
45
+ protected readonly fileService: FileService;
46
+
47
+ @inject(UntitledWorkspaceService)
48
+ protected readonly untitledWorkspaceService: UntitledWorkspaceService;
49
+
50
+ protected hostLogPath: Promise<string> | undefined;
51
+
52
+ getHostLogPath(): Promise<string> {
53
+ // cached: unlike the backend, where each call creates a new session folder, here one tab
54
+ // runs one session, and the plugin host may ask again on a later load cycle
55
+ return this.hostLogPath ??= this.resolveHostLogPath();
56
+ }
57
+
58
+ protected async resolveHostLogPath(): Promise<string> {
59
+ const configDirUri = new URI(await this.envServer.getConfigDirUri());
60
+ const logsDirUri = configDirUri.resolve(PluginPaths.PLUGINS_LOGS_DIR);
61
+ const hostLogPath = await this.ensureDirectory(logsDirUri.resolve(this.generateTimeFolderName()).resolve('host'));
62
+ // as on the backend, we never wait for the cleanup
63
+ this.cleanUpOldLogs(logsDirUri).catch(error => this.logger.error('Failed to clean up old plugin log folders:', error));
64
+ return hostLogPath;
65
+ }
66
+
67
+ /**
68
+ * Resolving re-hashes the workspace id and hits {@link FileService.createFolder}, and the plugin
69
+ * server asks for this on every workspace storage read/write. The roots are part of the key
70
+ * because an untitled workspace's id is derived from them.
71
+ */
72
+ protected readonly hostStoragePaths = new Map<string, Promise<string>>();
73
+
74
+ async getHostStoragePath(workspaceUri: string | undefined, rootUris: string[]): Promise<string | undefined> {
75
+ if (!workspaceUri) {
76
+ // no workspace, no place to store workspace state - same as the backend
77
+ return undefined;
78
+ }
79
+ // '\n' cannot appear in a URI, so it is safe as a separator between the workspace URI and its roots
80
+ const cacheKey = `${workspaceUri}\n${[...rootUris].sort().join('\n')}`;
81
+ let hostStoragePath = this.hostStoragePaths.get(cacheKey);
82
+ if (!hostStoragePath) {
83
+ hostStoragePath = this.resolveHostStoragePath(workspaceUri, rootUris);
84
+ this.hostStoragePaths.set(cacheKey, hostStoragePath);
85
+ // a transient failure (e.g. a flaky file system call) shouldn't poison the cache forever
86
+ hostStoragePath.catch(() => this.hostStoragePaths.delete(cacheKey));
87
+ }
88
+ return hostStoragePath;
89
+ }
90
+
91
+ protected async resolveHostStoragePath(workspaceUri: string, rootUris: string[]): Promise<string> {
92
+ const configDirUri = new URI(await this.envServer.getConfigDirUri());
93
+ const workspaceId = await this.buildWorkspaceId(configDirUri, workspaceUri, rootUris);
94
+ return this.ensureDirectory(configDirUri.resolve(PluginPaths.PLUGINS_WORKSPACE_STORAGE_DIR).resolve(workspaceId));
95
+ }
96
+
97
+ protected async buildWorkspaceId(configDirUri: URI, workspaceUri: string, rootUris: string[]): Promise<string> {
98
+ if (this.untitledWorkspaceService.isUntitledWorkspace(new URI(workspaceUri), configDirUri)) {
99
+ // an untitled workspace is named anew in every session, so key its storage on the roots instead
100
+ return hashValue([...rootUris].sort().join(','));
101
+ }
102
+ return hashValue(workspaceUri);
103
+ }
104
+
105
+ /**
106
+ * Generate time folder name in format: YYYYMMDDTHHMMSS, for example: 20181205T093828
107
+ */
108
+ protected generateTimeFolderName(): string {
109
+ const timeStamp = new Date().toISOString().replace(/[-:]|(\..*)/g, '');
110
+ // Helps ensure our timestamp generation logic is "valid".
111
+ // Changes to the timestamp structure may break old logs deletion logic.
112
+ if (!SESSION_TIMESTAMP_PATTERN.test(timeStamp)) {
113
+ this.logger.error(`Generated log folder name: "${timeStamp}" does not match expected pattern: ${SESSION_TIMESTAMP_PATTERN}`);
114
+ }
115
+ return timeStamp;
116
+ }
117
+
118
+ /**
119
+ * Keeps only the most recent {@link PluginPaths.DEFAULT_PLUGIN_MAX_SESSION_LOGS_FOLDERS}
120
+ * session folders under `logsDirUri`, so reloading doesn't fill up browser storage.
121
+ */
122
+ protected async cleanUpOldLogs(logsDirUri: URI): Promise<void> {
123
+ const logsDir = await this.fileService.resolve(logsDirUri);
124
+ const sessionDirs = (logsDir.children ?? [])
125
+ // we never clean a folder that is not a Theia logs session folder, even if it
126
+ // does appear under `logsDirUri`
127
+ .filter(child => child.isDirectory && SESSION_TIMESTAMP_PATTERN.test(child.resource.path.base))
128
+ .map(child => child.resource)
129
+ // newest first, so the oldest ones get cut off. Ordinal comparison, not
130
+ // localeCompare - the format is fixed ASCII, and locale-aware sorting could order
131
+ // it differently depending on the user's locale
132
+ .sort((one, other) => compare(other.path.base, one.path.base));
133
+ const oldSessionDirs = sessionDirs.slice(PluginPaths.DEFAULT_PLUGIN_MAX_SESSION_LOGS_FOLDERS);
134
+ await Promise.all(oldSessionDirs.map(uri => this.fileService.delete(uri, { fromUserGesture: false, recursive: true })));
135
+ }
136
+
137
+ /**
138
+ * Creates `uri` as a directory (fine if it already exists) and returns its path. Plugins get
139
+ * the path rather than the URI, e.g. as `ExtensionContext.storagePath`.
140
+ */
141
+ protected async ensureDirectory(uri: URI): Promise<string> {
142
+ await this.fileService.createFolder(uri, { fromUserGesture: false });
143
+ return this.fileService.fsPath(uri);
144
+ }
145
+ }