@theia/core 1.77.0-next.1 → 1.77.0-next.14

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 (32) hide show
  1. package/lib/browser/catalog.json +130 -9
  2. package/lib/browser/tree/tree-widget.d.ts +28 -0
  3. package/lib/browser/tree/tree-widget.d.ts.map +1 -1
  4. package/lib/browser/tree/tree-widget.js +59 -3
  5. package/lib/browser/tree/tree-widget.js.map +1 -1
  6. package/lib/browser/tree/tree-widget.spec.js +53 -3
  7. package/lib/browser/tree/tree-widget.spec.js.map +1 -1
  8. package/lib/common/core-preferences.js +5 -5
  9. package/lib/common/core-preferences.js.map +1 -1
  10. package/lib/node/backend-application-module.d.ts.map +1 -1
  11. package/lib/node/backend-application-module.js +3 -0
  12. package/lib/node/backend-application-module.js.map +1 -1
  13. package/lib/node/bundled-resource-provider.d.ts +86 -0
  14. package/lib/node/bundled-resource-provider.d.ts.map +1 -0
  15. package/lib/node/bundled-resource-provider.js +241 -0
  16. package/lib/node/bundled-resource-provider.js.map +1 -0
  17. package/lib/node/bundled-resource-provider.spec.d.ts +2 -0
  18. package/lib/node/bundled-resource-provider.spec.d.ts.map +1 -0
  19. package/lib/node/bundled-resource-provider.spec.js +160 -0
  20. package/lib/node/bundled-resource-provider.spec.js.map +1 -0
  21. package/lib/node/index.d.ts +1 -0
  22. package/lib/node/index.d.ts.map +1 -1
  23. package/lib/node/index.js +3 -1
  24. package/lib/node/index.js.map +1 -1
  25. package/package.json +4 -4
  26. package/src/browser/tree/tree-widget.spec.tsx +59 -3
  27. package/src/browser/tree/tree-widget.tsx +64 -2
  28. package/src/common/core-preferences.ts +5 -5
  29. package/src/node/backend-application-module.ts +4 -0
  30. package/src/node/bundled-resource-provider.spec.ts +196 -0
  31. package/src/node/bundled-resource-provider.ts +266 -0
  32. package/src/node/index.ts +1 -0
@@ -0,0 +1,196 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource GmbH and others.
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 'reflect-metadata';
18
+ import { expect } from 'chai';
19
+ import * as fs from 'fs';
20
+ import * as os from 'os';
21
+ import * as path from 'path';
22
+ import * as temp from 'temp';
23
+ import { Container } from 'inversify';
24
+ import { EnvVariablesServer } from '../common/env-variables';
25
+ import { ILogger } from '../common/logger';
26
+ import { MockLogger } from '../common/test/mock-logger';
27
+ import { FileUri } from '../common/file-uri';
28
+ import { BundledResourceProvider, BundledResourceProviderImpl } from './bundled-resource-provider';
29
+ import { FileSystemLocking, FileSystemLockingImpl } from './filesystem-locking';
30
+
31
+ /**
32
+ * The provider recognizes archives by their `.asar` path segment and reads them through `fs`, which Electron
33
+ * patches to transparently read from the archive. Plain directories named `*.asar` therefore behave like
34
+ * archives from the point of view of the provider and let us test it outside of Electron.
35
+ */
36
+ describe('BundledResourceProvider', () => {
37
+
38
+ const track = temp.track();
39
+
40
+ let configDir: string;
41
+ let appDir: string;
42
+ let provider: BundledResourceProvider;
43
+
44
+ beforeEach(() => {
45
+ const root = track.mkdirSync();
46
+ configDir = path.join(root, 'config');
47
+ appDir = path.join(root, 'app');
48
+ fs.mkdirSync(configDir);
49
+ fs.mkdirSync(appDir);
50
+ provider = createProvider();
51
+ });
52
+
53
+ /** Each provider instance stands for one run of the application, as resolved paths are cached. */
54
+ function createProvider(): BundledResourceProvider {
55
+ const container = new Container();
56
+ container.bind(ILogger).toConstantValue(new MockLogger());
57
+ container.bind(FileSystemLocking).to(FileSystemLockingImpl).inSingletonScope();
58
+ container.bind(EnvVariablesServer).toConstantValue({
59
+ getConfigDirUri: async () => FileUri.create(configDir).toString()
60
+ } as unknown as EnvVariablesServer);
61
+ container.bind(BundledResourceProviderImpl).toSelf().inSingletonScope();
62
+ container.bind(BundledResourceProvider).toService(BundledResourceProviderImpl);
63
+ return container.get(BundledResourceProvider);
64
+ }
65
+
66
+ afterEach(() => track.cleanupSync());
67
+
68
+ /**
69
+ * Assert that the given path is nested in the given directory. The provider derives the configuration
70
+ * directory from a URI, which on Windows yields a lower-case drive letter, so the paths cannot simply be
71
+ * compared by prefix. `path.relative` ignores the case of a path on Windows.
72
+ */
73
+ function expectNestedIn(directoryPath: string, nestedPath: string): void {
74
+ const relativePath = path.relative(directoryPath, nestedPath);
75
+ expect(relativePath).to.not.be.empty;
76
+ expect(relativePath.split(path.sep)).to.not.contain('..');
77
+ }
78
+
79
+ function writeResources(rootPath: string, files: Record<string, string>): string {
80
+ for (const [relativePath, content] of Object.entries(files)) {
81
+ const filePath = path.join(rootPath, relativePath);
82
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
83
+ fs.writeFileSync(filePath, content);
84
+ }
85
+ return rootPath;
86
+ }
87
+
88
+ it('returns the path of a resource that is not packaged into an archive', async () => {
89
+ const resourcePath = path.join(writeResources(appDir, { 'scripts/greet.sh': 'echo hello' }), 'scripts');
90
+ expect(await provider.resolveExternalPath(resourcePath)).to.equal(resourcePath);
91
+ });
92
+
93
+ it('rejects for a resource that does not exist', async () => {
94
+ const resourcePath = path.join(appDir, 'scripts');
95
+ await provider.resolveExternalPath(resourcePath).then(
96
+ () => expect.fail('should have been rejected'),
97
+ error => expect(String(error)).to.contain(resourcePath)
98
+ );
99
+ });
100
+
101
+ it('extracts a packaged resource into the configuration directory', async () => {
102
+ const archivePath = path.join(appDir, 'app.asar');
103
+ writeResources(archivePath, { 'scripts/greet.sh': 'echo hello', 'scripts/nested/.hidden': 'secret' });
104
+
105
+ const resolvedPath = await provider.resolveExternalPath(path.join(archivePath, 'scripts'));
106
+
107
+ expect(resolvedPath).to.not.contain('.asar');
108
+ expectNestedIn(configDir, resolvedPath);
109
+ expect(fs.readFileSync(path.join(resolvedPath, 'greet.sh'), 'utf8')).to.equal('echo hello');
110
+ expect(fs.readFileSync(path.join(resolvedPath, 'nested', '.hidden'), 'utf8')).to.equal('secret');
111
+ });
112
+
113
+ it('extracts a packaged resource that is a plain file', async () => {
114
+ const archivePath = path.join(appDir, 'app.asar');
115
+ writeResources(archivePath, { 'scripts/greet.sh': 'echo hello' });
116
+
117
+ const resolvedPath = await provider.resolveExternalPath(path.join(archivePath, 'scripts', 'greet.sh'));
118
+
119
+ expectNestedIn(configDir, resolvedPath);
120
+ expect(fs.readFileSync(resolvedPath, 'utf8')).to.equal('echo hello');
121
+ });
122
+
123
+ it('extracts a packaged file as executable', async function (): Promise<void> {
124
+ if (process.platform === 'win32') {
125
+ this.skip();
126
+ }
127
+ // Electron reports the files of an asar archive with this mode, even executables.
128
+ const archivePath = path.join(appDir, 'app.asar');
129
+ writeResources(archivePath, { 'native/tool': 'binary' });
130
+ fs.chmodSync(path.join(archivePath, 'native', 'tool'), 0o644);
131
+
132
+ const resolvedPath = await provider.resolveExternalPath(path.join(archivePath, 'native', 'tool'));
133
+
134
+ expect(fs.statSync(resolvedPath).mode & 0o700).to.equal(0o700);
135
+ });
136
+
137
+ it('prefers the copy that the packaging step has left outside of the archive', async () => {
138
+ const archivePath = path.join(appDir, 'app.asar');
139
+ writeResources(archivePath, { 'scripts/greet.sh': 'echo hello' });
140
+ const unpackedPath = writeResources(`${archivePath}.unpacked`, { 'scripts/greet.sh': 'echo hello' });
141
+
142
+ const resolvedPath = await provider.resolveExternalPath(path.join(archivePath, 'scripts'));
143
+
144
+ expect(resolvedPath).to.equal(path.join(unpackedPath, 'scripts'));
145
+ });
146
+
147
+ it('extracts a packaged resource that was only unpacked partially', async () => {
148
+ const archivePath = path.join(appDir, 'app.asar');
149
+ writeResources(archivePath, { 'scripts/greet.sh': 'echo hello', 'scripts/farewell.sh': 'echo bye' });
150
+ writeResources(`${archivePath}.unpacked`, { 'scripts/greet.sh': 'echo hello' });
151
+
152
+ const resolvedPath = await provider.resolveExternalPath(path.join(archivePath, 'scripts'));
153
+
154
+ expectNestedIn(configDir, resolvedPath);
155
+ expect(fs.readFileSync(path.join(resolvedPath, 'farewell.sh'), 'utf8')).to.equal('echo bye');
156
+ });
157
+
158
+ it('re-extracts a resource that was removed from the configuration directory', async () => {
159
+ const archivePath = path.join(appDir, 'app.asar');
160
+ writeResources(archivePath, { 'scripts/greet.sh': 'echo hello' });
161
+ const resourcePath = path.join(archivePath, 'scripts');
162
+
163
+ const resolvedPath = await provider.resolveExternalPath(resourcePath);
164
+ fs.rmSync(resolvedPath, { recursive: true });
165
+
166
+ expect(await provider.resolveExternalPath(resourcePath)).to.equal(resolvedPath);
167
+ expect(fs.readFileSync(path.join(resolvedPath, 'greet.sh'), 'utf8')).to.equal('echo hello');
168
+ });
169
+
170
+ it('extracts into a temporary directory if the configuration directory cannot be written to', async () => {
171
+ const archivePath = path.join(appDir, 'app.asar');
172
+ writeResources(archivePath, { 'scripts/greet.sh': 'echo hello' });
173
+ // a configuration directory that is a regular file cannot hold the extracted resources
174
+ configDir = path.join(appDir, 'not-a-directory');
175
+ fs.writeFileSync(configDir, '');
176
+
177
+ const resolvedPath = await createProvider().resolveExternalPath(path.join(archivePath, 'scripts'));
178
+
179
+ expectNestedIn(os.tmpdir(), resolvedPath);
180
+ expect(fs.readFileSync(path.join(resolvedPath, 'greet.sh'), 'utf8')).to.equal('echo hello');
181
+ fs.rmSync(resolvedPath, { recursive: true, force: true });
182
+ });
183
+
184
+ it('updates an extracted resource when the application has been updated', async () => {
185
+ const archivePath = path.join(appDir, 'app.asar');
186
+ writeResources(archivePath, { 'scripts/greet.sh': 'echo hello' });
187
+ const resourcePath = path.join(archivePath, 'scripts');
188
+
189
+ await provider.resolveExternalPath(resourcePath);
190
+ writeResources(archivePath, { 'scripts/greet.sh': 'echo hello again' });
191
+ const resolvedPath = await createProvider().resolveExternalPath(resourcePath);
192
+
193
+ expect(fs.readFileSync(path.join(resolvedPath, 'greet.sh'), 'utf8')).to.equal('echo hello again');
194
+ });
195
+
196
+ });
@@ -0,0 +1,266 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource GmbH and others.
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 * as crypto from 'crypto';
18
+ import * as fs from 'fs';
19
+ import * as os from 'os';
20
+ import * as path from 'path';
21
+ import { inject, injectable, interfaces, named } from 'inversify';
22
+ import { EnvVariablesServer } from '../common/env-variables';
23
+ import { FileUri } from '../common/file-uri';
24
+ import { ILogger } from '../common/logger';
25
+ import { FileSystemLocking } from './filesystem-locking';
26
+
27
+ export const BundledResourceProvider = Symbol('BundledResourceProvider') as symbol & interfaces.Abstract<BundledResourceProvider>;
28
+ /**
29
+ * Provides paths to resources that are bundled with the application, in a form that can also be used by
30
+ * processes which do not run inside the application, such as shells, debug adapters or other external tools.
31
+ *
32
+ * When an Electron application is packaged with `asar: true`, the resources of the application are stored in
33
+ * an `app.asar` archive. Paths pointing into such an archive can only be read through Electron's patched `fs`
34
+ * module, so handing them to an external process fails. Therefore, always resolve a bundled resource through
35
+ * this service before passing its path to a process outside of the application.
36
+ */
37
+ export interface BundledResourceProvider {
38
+ /**
39
+ * Resolve the given path of a bundled file or directory to a path that external processes can read.
40
+ *
41
+ * If the resource is not packaged into an archive, its path is returned unchanged. Otherwise the copy that
42
+ * the packaging step has left outside of the archive is used, if it is complete, and the resource is
43
+ * extracted from the archive into the configuration directory as a last resort.
44
+ *
45
+ * Results are cached, so that a resource is extracted at most once per application version.
46
+ *
47
+ * @param resourcePath the path of the bundled resource, typically resolved against `__dirname`.
48
+ * @returns the path to use outside of the application.
49
+ * @throws if the resource does not exist or cannot be made available.
50
+ */
51
+ resolveExternalPath(resourcePath: string): Promise<string>;
52
+ }
53
+
54
+ @injectable()
55
+ export class BundledResourceProviderImpl implements BundledResourceProvider {
56
+
57
+ @inject(ILogger) @named('core:BundledResourceProviderImpl')
58
+ protected readonly logger: ILogger;
59
+
60
+ @inject(EnvVariablesServer)
61
+ protected readonly envVariablesServer: EnvVariablesServer;
62
+
63
+ @inject(FileSystemLocking)
64
+ protected readonly fileSystemLocking: FileSystemLocking;
65
+
66
+ /** The folder within the configuration directory into which resources are extracted. */
67
+ protected readonly extractionFolder = 'bundled-resources';
68
+
69
+ /** Resources extracted for another application version are removed when they are older than this. */
70
+ protected readonly staleExtractionAge = 7 * 24 * 60 * 60 * 1000;
71
+
72
+ protected readonly resolved = new Map<string, Promise<string>>();
73
+ protected temporaryExtractionPath: Promise<string> | undefined;
74
+
75
+ async resolveExternalPath(resourcePath: string): Promise<string> {
76
+ const normalizedPath = path.resolve(resourcePath);
77
+ const pending = this.resolved.get(normalizedPath);
78
+ if (pending) {
79
+ try {
80
+ const resolvedPath = await pending;
81
+ if (await this.exists(resolvedPath)) {
82
+ return resolvedPath;
83
+ }
84
+ } catch {
85
+ // the previous attempt failed, so try again below
86
+ }
87
+ this.resolved.delete(normalizedPath);
88
+ }
89
+ const result = this.doResolveExternalPath(normalizedPath);
90
+ this.resolved.set(normalizedPath, result);
91
+ return result;
92
+ }
93
+
94
+ protected async doResolveExternalPath(resourcePath: string): Promise<string> {
95
+ if (!await this.exists(resourcePath)) {
96
+ throw new Error(`Bundled resource does not exist: ${resourcePath}`);
97
+ }
98
+ const archivePath = this.findArchivePath(resourcePath);
99
+ if (!archivePath) {
100
+ // The resource is not packaged into an archive, so external processes can read it directly.
101
+ return resourcePath;
102
+ }
103
+ const relativePath = path.relative(archivePath, resourcePath);
104
+ const unpackedPath = path.join(`${archivePath}.unpacked`, relativePath);
105
+ if (await this.isExtracted(resourcePath, unpackedPath)) {
106
+ return unpackedPath;
107
+ }
108
+ const extractionPath = path.join(await this.getExtractionPath(archivePath), relativePath);
109
+ await this.fileSystemLocking.lockPath(archivePath, () => this.extract(resourcePath, extractionPath));
110
+ this.logger.info(`Extracted bundled resource '${resourcePath}' to '${extractionPath}'.`);
111
+ return extractionPath;
112
+ }
113
+
114
+ /**
115
+ * Find the archive that the given resource is packaged into, if any.
116
+ *
117
+ * @returns the path of the archive, i.e. the shortest prefix of the given path ending in an `.asar` segment,
118
+ * which is also how Electron itself determines the archive that a path refers to.
119
+ */
120
+ protected findArchivePath(resourcePath: string): string | undefined {
121
+ const segments = resourcePath.split(/[\\/]/);
122
+ const archiveIndex = segments.findIndex(segment => segment.toLowerCase().endsWith('.asar'));
123
+ return archiveIndex < 0 ? undefined : segments.slice(0, archiveIndex + 1).join(path.sep);
124
+ }
125
+
126
+ /**
127
+ * Whether every file of the given resource is already available at the given target path. Packaging tools
128
+ * only unpack the files that the application configured them to unpack, so a partial copy must be ignored.
129
+ */
130
+ protected async isExtracted(resourcePath: string, targetPath: string): Promise<boolean> {
131
+ for (const file of await this.collectFiles(resourcePath)) {
132
+ if (!await this.exists(path.join(targetPath, file))) {
133
+ return false;
134
+ }
135
+ }
136
+ return true;
137
+ }
138
+
139
+ protected async extract(resourcePath: string, targetPath: string): Promise<void> {
140
+ for (const file of await this.collectFiles(resourcePath)) {
141
+ await this.copyFile(path.join(resourcePath, file), path.join(targetPath, file));
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Collect the files of the given resource as paths relative to it. The relative path of a resource that is
147
+ * a plain file is the empty string.
148
+ */
149
+ protected async collectFiles(resourcePath: string, relativePath: string = ''): Promise<string[]> {
150
+ const currentPath = path.join(resourcePath, relativePath);
151
+ if (!(await fs.promises.stat(currentPath)).isDirectory()) {
152
+ return [relativePath];
153
+ }
154
+ const files: string[] = [];
155
+ for (const entry of await fs.promises.readdir(currentPath)) {
156
+ files.push(...await this.collectFiles(resourcePath, path.join(relativePath, entry)));
157
+ }
158
+ return files;
159
+ }
160
+
161
+ protected async copyFile(sourcePath: string, targetPath: string): Promise<void> {
162
+ const sourceStat = await fs.promises.stat(sourcePath);
163
+ if (await this.isUpToDate(targetPath, sourceStat)) {
164
+ return;
165
+ }
166
+ await fs.promises.mkdir(path.dirname(targetPath), { recursive: true, mode: 0o700 });
167
+ // Write to a temporary file and move it into place, so that a concurrently starting application
168
+ // instance never reads partially written content.
169
+ const temporaryPath = `${targetPath}.${process.pid}.tmp`;
170
+ try {
171
+ // Electron reports the files of an asar archive as not executable, even those that the archive marks as
172
+ // executable, so every extracted file is made executable for the owner to keep bundled executables usable.
173
+ await fs.promises.writeFile(temporaryPath, await fs.promises.readFile(sourcePath), { mode: sourceStat.mode & 0o777 | 0o700 });
174
+ try {
175
+ await fs.promises.rename(temporaryPath, targetPath);
176
+ } catch {
177
+ // renaming onto an existing file is not supported on all platforms
178
+ await fs.promises.rm(targetPath, { force: true });
179
+ await fs.promises.rename(temporaryPath, targetPath);
180
+ }
181
+ } finally {
182
+ await fs.promises.rm(temporaryPath, { force: true });
183
+ }
184
+ }
185
+
186
+ protected async isUpToDate(targetPath: string, sourceStat: fs.Stats): Promise<boolean> {
187
+ try {
188
+ return (await fs.promises.stat(targetPath)).size === sourceStat.size;
189
+ } catch {
190
+ return false;
191
+ }
192
+ }
193
+
194
+ /**
195
+ * The directory into which resources of the given archive are extracted. It is specific to the content of
196
+ * the archive, so that updating the application invalidates previously extracted copies.
197
+ */
198
+ protected async getExtractionPath(archivePath: string): Promise<string> {
199
+ const archiveName = path.basename(archivePath, path.extname(archivePath));
200
+ try {
201
+ const extractionRoot = path.join(FileUri.fsPath(await this.envVariablesServer.getConfigDirUri()), this.extractionFolder);
202
+ const extractionPath = path.join(extractionRoot, `${archiveName}-${this.getArchiveStamp(archivePath)}`);
203
+ await fs.promises.mkdir(extractionPath, { recursive: true, mode: 0o700 });
204
+ this.removeStaleExtractions(extractionRoot, `${archiveName}-`, extractionPath);
205
+ return extractionPath;
206
+ } catch (error) {
207
+ this.logger.warn('Could not extract bundled resources into the configuration directory, using a temporary directory instead.', error);
208
+ return this.getTemporaryExtractionPath();
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Fallback for applications whose configuration directory cannot be written to. The directory is unique to
214
+ * this run of the application, so that resources are extracted again whenever the application is started.
215
+ */
216
+ protected getTemporaryExtractionPath(): Promise<string> {
217
+ return this.temporaryExtractionPath ??= fs.promises.mkdtemp(path.join(os.tmpdir(), 'theia-bundled-resources-'));
218
+ }
219
+
220
+ /**
221
+ * Identify the content of the given archive. `process.noAsar` is toggled around the synchronous `stat` call
222
+ * to get the stats of the archive file itself rather than of the directory that it appears as.
223
+ */
224
+ protected getArchiveStamp(archivePath: string): string {
225
+ const asarProcess = process as NodeJS.Process & { noAsar?: boolean };
226
+ const noAsar = asarProcess.noAsar;
227
+ try {
228
+ asarProcess.noAsar = true;
229
+ const stat = fs.statSync(archivePath);
230
+ return crypto.createHash('sha256').update(`${archivePath}:${stat.size}:${stat.mtimeMs}`).digest('hex').substring(0, 12);
231
+ } finally {
232
+ asarProcess.noAsar = noAsar;
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Remove resources that were extracted from an earlier version of the archive, unless they were used
238
+ * recently, because another application instance may still be running on that version.
239
+ */
240
+ protected removeStaleExtractions(extractionRoot: string, prefix: string, currentPath: string): void {
241
+ const removeStale = async () => {
242
+ for (const entry of await fs.promises.readdir(extractionRoot)) {
243
+ const entryPath = path.join(extractionRoot, entry);
244
+ if (!entry.startsWith(prefix) || entryPath === currentPath) {
245
+ continue;
246
+ }
247
+ const stat = await fs.promises.stat(entryPath);
248
+ if (Date.now() - stat.mtimeMs > this.staleExtractionAge) {
249
+ await fs.promises.rm(entryPath, { recursive: true, force: true });
250
+ this.logger.info(`Removed stale extracted resources: ${entryPath}`);
251
+ }
252
+ }
253
+ };
254
+ removeStale().catch(error => this.logger.warn(`Could not remove stale extracted resources in '${extractionRoot}'.`, error));
255
+ }
256
+
257
+ protected async exists(targetPath: string): Promise<boolean> {
258
+ try {
259
+ await fs.promises.stat(targetPath);
260
+ return true;
261
+ } catch {
262
+ return false;
263
+ }
264
+ }
265
+
266
+ }
package/src/node/index.ts CHANGED
@@ -22,5 +22,6 @@ export * from './messaging';
22
22
  export * from './cli';
23
23
  export * from './setting-service';
24
24
  export { FileSystemLocking } from './filesystem-locking';
25
+ export { BundledResourceProvider } from './bundled-resource-provider';
25
26
  export { BackendRequestAllowedContribution } from './request/backend-request-facade';
26
27
  export { HttpConnectionValidator } from './hosting/browser-connection-token';