diff --git a/CHANGELOG.md b/CHANGELOG.md index fcf2f5a2f8c03..505c6060e9f59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ - [plugin] stub multiDocumentHighlightProvider proposed API [#13248](https://github.com/eclipse-theia/theia/pull/13248) - contributed on behalf of STMicroelectronics - [terminal] update terminalQuickFixProvider proposed API according to vscode 1.85 version [#13240](https://github.com/eclipse-theia/theia/pull/13240) - contributed on behalf of STMicroelectronics +[Breaking Changes:](#breaking_changes_not_yet_released) + +- [core] moved `FileUri` from `node` package to `common` + ## v1.45.0 - 12/21/2023 - [application-manager] updated logic to allow rebinding messaging services in preload [#13199](https://github.com/eclipse-theia/theia/pull/13199) diff --git a/dev-packages/application-manager/package.json b/dev-packages/application-manager/package.json index 1d7cfbdf2478e..7ad70e5831baf 100644 --- a/dev-packages/application-manager/package.json +++ b/dev-packages/application-manager/package.json @@ -45,6 +45,7 @@ "css-loader": "^6.2.0", "electron-rebuild": "^3.2.7", "fs-extra": "^4.0.2", + "http-server": "^14.1.1", "ignore-loader": "^0.1.2", "less": "^3.0.3", "mini-css-extract-plugin": "^2.6.1", diff --git a/dev-packages/application-manager/src/application-package-manager.ts b/dev-packages/application-manager/src/application-package-manager.ts index 86b5126c8eb6b..09d3d26956789 100644 --- a/dev-packages/application-manager/src/application-package-manager.ts +++ b/dev-packages/application-manager/src/application-package-manager.ts @@ -119,10 +119,32 @@ export class ApplicationPackageManager { start(args: string[] = []): cp.ChildProcess { if (this.pck.isElectron()) { return this.startElectron(args); + } else if (this.pck.isBrowserOnly()) { + return this.startBrowserOnly(args); } return this.startBrowser(args); } + startBrowserOnly(args: string[]): cp.ChildProcess { + const { command, mainArgs, options } = this.adjustBrowserOnlyArgs(args); + return this.__process.spawnBin(command, mainArgs, options); + } + + adjustBrowserOnlyArgs(args: string[]): Readonly<{ command: string, mainArgs: string[]; options: cp.SpawnOptions }> { + let { mainArgs, options } = this.adjustArgs(args); + + // first parameter: path to generated frontend + // second parameter: disable cache to support watching + mainArgs = ['lib/frontend', '-c-1', ...mainArgs]; + + const portIndex = mainArgs.findIndex(v => v.startsWith('--port')); + if (portIndex === -1) { + mainArgs.push('--port=3000'); + } + + return { command: 'http-server', mainArgs, options }; + } + startElectron(args: string[]): cp.ChildProcess { // If possible, pass the project root directory to electron rather than the script file so that Electron // can determine the app name. This requires that the package.json has a main field. diff --git a/dev-packages/application-manager/src/generator/abstract-generator.ts b/dev-packages/application-manager/src/generator/abstract-generator.ts index 2ff52527ef3ec..da35c5d588849 100644 --- a/dev-packages/application-manager/src/generator/abstract-generator.ts +++ b/dev-packages/application-manager/src/generator/abstract-generator.ts @@ -37,6 +37,10 @@ export abstract class AbstractGenerator { return this.pck.ifElectron(value, defaultValue); } + protected ifBrowserOnly(value: string, defaultValue: string = ''): string { + return this.pck.ifBrowserOnly(value, defaultValue); + } + protected async write(path: string, content: string): Promise { await fs.ensureFile(path); await fs.writeFile(path, content); diff --git a/dev-packages/application-manager/src/generator/backend-generator.ts b/dev-packages/application-manager/src/generator/backend-generator.ts index 5f457d0d9dba5..a5df1bdb80b46 100644 --- a/dev-packages/application-manager/src/generator/backend-generator.ts +++ b/dev-packages/application-manager/src/generator/backend-generator.ts @@ -20,6 +20,10 @@ import { AbstractGenerator } from './abstract-generator'; export class BackendGenerator extends AbstractGenerator { async generate(): Promise { + if (this.pck.isBrowserOnly()) { + // no backend generation in case of browser-only target + return; + } const backendModules = this.pck.targetBackendModules; await this.write(this.pck.backend('server.js'), this.compileServer(backendModules)); await this.write(this.pck.backend('main.js'), this.compileMain(backendModules)); diff --git a/dev-packages/application-manager/src/generator/frontend-generator.ts b/dev-packages/application-manager/src/generator/frontend-generator.ts index d4218f3b174b6..672b11cbcb43e 100644 --- a/dev-packages/application-manager/src/generator/frontend-generator.ts +++ b/dev-packages/application-manager/src/generator/frontend-generator.ts @@ -24,7 +24,7 @@ export class FrontendGenerator extends AbstractGenerator { async generate(options?: GeneratorOptions): Promise { await this.write(this.pck.frontend('index.html'), this.compileIndexHtml(this.pck.targetFrontendModules)); - await this.write(this.pck.frontend('index.js'), this.compileIndexJs(this.pck.targetFrontendModules, this.pck.frontendPreloadModules)); + await this.write(this.pck.frontend('index.js'), this.compileIndexJs(this.pck.targetFrontendModules, this.pck.targetFrontendPreloadModules)); await this.write(this.pck.frontend('secondary-window.html'), this.compileSecondaryWindowHtml()); await this.write(this.pck.frontend('secondary-index.js'), this.compileSecondaryIndexJs(this.pck.secondaryWindowModules)); if (this.pck.isElectron()) { @@ -108,18 +108,26 @@ ${Array.from(frontendPreloadModules.values(), jsModulePath => `\ } module.exports = (async () => { - const { messagingFrontendModule } = require('@theia/core/lib/${this.pck.isBrowser() - ? 'browser/messaging/messaging-frontend-module' - : 'electron-browser/messaging/electron-messaging-frontend-module'}'); + const { messagingFrontendModule } = require('@theia/core/lib/${!this.pck.isElectron() + ? 'browser/messaging/messaging-frontend-module' + : 'electron-browser/messaging/electron-messaging-frontend-module'}'); const container = new Container(); container.load(messagingFrontendModule); + ${this.ifBrowserOnly(`const { messagingFrontendOnlyModule } = require('@theia/core/lib/browser-only/messaging/messaging-frontend-only-module'); + container.load(messagingFrontendOnlyModule);`)} + await preload(container); const { FrontendApplication } = require('@theia/core/lib/browser'); const { frontendApplicationModule } = require('@theia/core/lib/browser/frontend-application-module'); const { loggerFrontendModule } = require('@theia/core/lib/browser/logger-frontend-module'); container.load(frontendApplicationModule); + ${this.pck.ifBrowserOnly(`const { frontendOnlyApplicationModule } = require('@theia/core/lib/browser-only/frontend-only-application-module'); + container.load(frontendOnlyApplicationModule);`)} + container.load(loggerFrontendModule); + ${this.ifBrowserOnly(`const { loggerFrontendOnlyModule } = require('@theia/core/lib/browser-only/logger-frontend-only-module'); + container.load(loggerFrontendOnlyModule);`)} try { ${Array.from(frontendModules.values(), jsModulePath => `\ diff --git a/dev-packages/application-manager/src/generator/webpack-generator.ts b/dev-packages/application-manager/src/generator/webpack-generator.ts index e650c7a7dbd09..fa942815843b9 100644 --- a/dev-packages/application-manager/src/generator/webpack-generator.ts +++ b/dev-packages/application-manager/src/generator/webpack-generator.ts @@ -22,7 +22,9 @@ export class WebpackGenerator extends AbstractGenerator { async generate(): Promise { await this.write(this.genConfigPath, this.compileWebpackConfig()); - await this.write(this.genNodeConfigPath, this.compileNodeWebpackConfig()); + if (!this.pck.isBrowserOnly()) { + await this.write(this.genNodeConfigPath, this.compileNodeWebpackConfig()); + } if (await this.shouldGenerateUserWebpackConfig()) { await this.write(this.configPath, this.compileUserWebpackConfig()); } @@ -332,7 +334,7 @@ module.exports = [{ */ // @ts-check const configs = require('./${paths.basename(this.genConfigPath)}'); -const nodeConfig = require('./${paths.basename(this.genNodeConfigPath)}'); +${this.ifBrowserOnly('', `const nodeConfig = require('./${paths.basename(this.genNodeConfigPath)}');`)} /** * Expose bundled modules on window.theia.moduleName namespace, e.g. @@ -343,10 +345,11 @@ configs[0].module.rules.push({ loader: require.resolve('@theia/application-manager/lib/expose-loader') }); */ -module.exports = [ +${this.ifBrowserOnly('module.exports = configs;', `module.exports = [ ...configs, nodeConfig.config -];`; +];`)} +`; } protected compileNodeWebpackConfig(): string { diff --git a/dev-packages/application-manager/src/rebuild.ts b/dev-packages/application-manager/src/rebuild.ts index 0784c315558ba..82e454be7c802 100644 --- a/dev-packages/application-manager/src/rebuild.ts +++ b/dev-packages/application-manager/src/rebuild.ts @@ -19,7 +19,7 @@ import fs = require('fs-extra'); import path = require('path'); import os = require('os'); -export type RebuildTarget = 'electron' | 'browser'; +export type RebuildTarget = 'electron' | 'browser' | 'browser-only'; const EXIT_SIGNALS: NodeJS.Signals[] = ['SIGINT', 'SIGTERM']; diff --git a/dev-packages/application-package/src/application-package.ts b/dev-packages/application-package/src/application-package.ts index 68b5cb896c6ec..700dec43bb2d8 100644 --- a/dev-packages/application-package/src/application-package.ts +++ b/dev-packages/application-package/src/application-package.ts @@ -73,7 +73,7 @@ export class ApplicationPackage { theia.target = this.options.appTarget; } - if (theia.target && !(theia.target in ApplicationProps.ApplicationTarget)) { + if (theia.target && !(Object.values(ApplicationProps.ApplicationTarget).includes(theia.target))) { const defaultTarget = ApplicationProps.ApplicationTarget.browser; console.warn(`Unknown application target '${theia.target}', '${defaultTarget}' to be used instead`); theia.target = defaultTarget; @@ -140,10 +140,24 @@ export class ApplicationPackage { return this._frontendPreloadModules ??= this.computeModules('frontendPreload'); } + get frontendOnlyPreloadModules(): Map { + if (!this._frontendPreloadModules) { + this._frontendPreloadModules = this.computeModules('frontendOnlyPreload', 'frontendPreload'); + } + return this._frontendPreloadModules; + } + get frontendModules(): Map { return this._frontendModules ??= this.computeModules('frontend'); } + get frontendOnlyModules(): Map { + if (!this._frontendModules) { + this._frontendModules = this.computeModules('frontendOnly', 'frontend'); + } + return this._frontendModules; + } + get frontendElectronModules(): Map { return this._frontendElectronModules ??= this.computeModules('frontendElectron', 'frontend'); } @@ -227,6 +241,10 @@ export class ApplicationPackage { return this.target === ApplicationProps.ApplicationTarget.electron; } + isBrowserOnly(): boolean { + return this.target === ApplicationProps.ApplicationTarget.browserOnly; + } + ifBrowser(value: T): T | undefined; ifBrowser(value: T, defaultValue: T): T; ifBrowser(value: T, defaultValue?: T): T | undefined { @@ -239,14 +257,33 @@ export class ApplicationPackage { return this.isElectron() ? value : defaultValue; } + ifBrowserOnly(value: T): T | undefined; + ifBrowserOnly(value: T, defaultValue: T): T; + ifBrowserOnly(value: T, defaultValue?: T): T | undefined { + return this.isBrowserOnly() ? value : defaultValue; + } + get targetBackendModules(): Map { + if (this.isBrowserOnly()) { + return new Map(); + } return this.ifBrowser(this.backendModules, this.backendElectronModules); } get targetFrontendModules(): Map { + if (this.isBrowserOnly()) { + return this.frontendOnlyModules; + } return this.ifBrowser(this.frontendModules, this.frontendElectronModules); } + get targetFrontendPreloadModules(): Map { + if (this.isBrowserOnly()) { + return this.frontendOnlyPreloadModules; + } + return this.frontendPreloadModules; + } + get targetElectronMainModules(): Map { return this.ifElectron(this.electronMainModules, new Map()); } diff --git a/dev-packages/application-package/src/application-props.ts b/dev-packages/application-package/src/application-props.ts index 78bcece1c011e..a443a60df679d 100644 --- a/dev-packages/application-package/src/application-props.ts +++ b/dev-packages/application-package/src/application-props.ts @@ -241,10 +241,11 @@ export interface ApplicationProps extends NpmRegistryProps { }; } export namespace ApplicationProps { - export type Target = keyof typeof ApplicationTarget; + export type Target = `${ApplicationTarget}`; export enum ApplicationTarget { browser = 'browser', - electron = 'electron' + electron = 'electron', + browserOnly = 'browser-only' }; export const DEFAULT: ApplicationProps = { ...NpmRegistryProps.DEFAULT, diff --git a/dev-packages/application-package/src/extension-package.ts b/dev-packages/application-package/src/extension-package.ts index 8ccd8d661513a..dced483c99674 100644 --- a/dev-packages/application-package/src/extension-package.ts +++ b/dev-packages/application-package/src/extension-package.ts @@ -21,7 +21,9 @@ import { NpmRegistry, PublishedNodePackage, NodePackage } from './npm-registry'; export interface Extension { frontendPreload?: string; + frontendOnlyPreload?: string; frontend?: string; + frontendOnly?: string; frontendElectron?: string; secondaryWindow?: string; backend?: string; diff --git a/dev-packages/cli/package.json b/dev-packages/cli/package.json index ee50fc60bfde2..7b70f54e5d527 100644 --- a/dev-packages/cli/package.json +++ b/dev-packages/cli/package.json @@ -44,6 +44,7 @@ "decompress": "^4.2.1", "escape-string-regexp": "4.0.0", "glob": "^8.0.3", + "http-server": "^14.1.1", "limiter": "^2.1.0", "log-update": "^4.0.0", "mocha": "^10.1.0", diff --git a/dev-packages/cli/src/theia.ts b/dev-packages/cli/src/theia.ts index 41acf9d6e09e4..d215c578de288 100644 --- a/dev-packages/cli/src/theia.ts +++ b/dev-packages/cli/src/theia.ts @@ -94,12 +94,12 @@ function rebuildCommand(command: string, target: ApplicationProps.Target): yargs } function defineCommonOptions(cli: yargs.Argv): yargs.Argv { return cli .option('app-target', { description: 'The target application type. Overrides `theia.target` in the application\'s package.json', - choices: ['browser', 'electron'] as const, + choices: ['browser', 'electron', 'browser-only'] as const, }); } diff --git a/examples/api-samples/package.json b/examples/api-samples/package.json index d300bb90c332b..28f584a2356e9 100644 --- a/examples/api-samples/package.json +++ b/examples/api-samples/package.json @@ -29,6 +29,9 @@ { "electronMain": "lib/electron-main/update/sample-updater-main-module", "frontendElectron": "lib/electron-browser/updater/sample-updater-frontend-module" + }, + { + "frontendOnly": "lib/browser-only/api-samples-frontend-only-module" } ], "keywords": [ diff --git a/examples/api-samples/src/browser-only/api-samples-frontend-only-module.ts b/examples/api-samples/src/browser-only/api-samples-frontend-only-module.ts new file mode 100644 index 0000000000000..2fe1703ffa8b1 --- /dev/null +++ b/examples/api-samples/src/browser-only/api-samples-frontend-only-module.ts @@ -0,0 +1,27 @@ +// ***************************************************************************** +// Copyright (C) 2023 EclipseSource and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { ContainerModule, interfaces } from '@theia/core/shared/inversify'; +import { bindBrowserFSInitialization } from './filesystem/example-filesystem-initialization'; + +export default new ContainerModule(( + bind: interfaces.Bind, + _unbind: interfaces.Unbind, + _isBound: interfaces.IsBound, + rebind: interfaces.Rebind, +) => { + bindBrowserFSInitialization(bind, rebind); +}); diff --git a/examples/api-samples/src/browser-only/filesystem/example-filesystem-initialization.ts b/examples/api-samples/src/browser-only/filesystem/example-filesystem-initialization.ts new file mode 100644 index 0000000000000..f048231f977a5 --- /dev/null +++ b/examples/api-samples/src/browser-only/filesystem/example-filesystem-initialization.ts @@ -0,0 +1,51 @@ +// ***************************************************************************** +// Copyright (C) 2023 EclipseSource and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { URI } from '@theia/core'; +import { inject, injectable, interfaces } from '@theia/core/shared/inversify'; +import { EncodingService } from '@theia/core/lib/common/encoding-service'; +import { BrowserFSInitialization, DefaultBrowserFSInitialization } from '@theia/filesystem/lib/browser-only/browserfs-filesystem-initialization'; +import { BrowserFSFileSystemProvider } from '@theia/filesystem/lib/browser-only/browserfs-filesystem-provider'; +import type { FSModule } from 'browserfs/dist/node/core/FS'; + +@injectable() +export class ExampleBrowserFSInitialization extends DefaultBrowserFSInitialization { + + @inject(EncodingService) + protected encodingService: EncodingService; + + override async initializeFS(fs: FSModule, provider: BrowserFSFileSystemProvider): Promise { + try { + if (!fs.existsSync('/home/workspace')) { + await provider.mkdir(new URI('/home/workspace')); + await provider.writeFile(new URI('/home/workspace/my-file.txt'), this.encodingService.encode('foo').buffer, { create: true, overwrite: false }); + await provider.writeFile(new URI('/home/workspace/my-file2.txt'), this.encodingService.encode('bar').buffer, { create: true, overwrite: false }); + } + if (!fs.existsSync('/home/workspace2')) { + await provider.mkdir(new URI('/home/workspace2')); + await provider.writeFile(new URI('/home/workspace2/my-file.json'), this.encodingService.encode('{ foo: true }').buffer, { create: true, overwrite: false }); + await provider.writeFile(new URI('/home/workspace2/my-file2.json'), this.encodingService.encode('{ bar: false }').buffer, { create: true, overwrite: false }); + } + } catch (e) { + console.error('An error occurred while initializing the demo workspaces', e); + } + } +} + +export const bindBrowserFSInitialization = (bind: interfaces.Bind, rebind: interfaces.Rebind): void => { + bind(ExampleBrowserFSInitialization).toSelf(); + rebind(BrowserFSInitialization).toService(ExampleBrowserFSInitialization); +}; diff --git a/examples/browser-only/package.json b/examples/browser-only/package.json new file mode 100644 index 0000000000000..453fb7dae24a4 --- /dev/null +++ b/examples/browser-only/package.json @@ -0,0 +1,78 @@ +{ + "private": true, + "name": "@theia/example-browser-only", + "version": "1.45.0", + "license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0", + "theia": { + "target": "browser-only", + "frontend": { + "config": { + "applicationName": "Theia Browser-Only Example", + "preferences": { + "files.enableTrash": false + } + } + } + }, + "dependencies": { + "@theia/api-samples": "1.45.0", + "@theia/bulk-edit": "1.45.0", + "@theia/callhierarchy": "1.45.0", + "@theia/console": "1.45.0", + "@theia/core": "1.45.0", + "@theia/debug": "1.45.0", + "@theia/editor": "1.45.0", + "@theia/editor-preview": "1.45.0", + "@theia/file-search": "1.45.0", + "@theia/filesystem": "1.45.0", + "@theia/getting-started": "1.45.0", + "@theia/git": "1.45.0", + "@theia/keymaps": "1.45.0", + "@theia/markers": "1.45.0", + "@theia/memory-inspector": "1.45.0", + "@theia/messages": "1.45.0", + "@theia/metrics": "1.45.0", + "@theia/mini-browser": "1.45.0", + "@theia/monaco": "1.45.0", + "@theia/navigator": "1.45.0", + "@theia/outline-view": "1.45.0", + "@theia/output": "1.45.0", + "@theia/plugin-dev": "1.45.0", + "@theia/plugin-ext": "1.45.0", + "@theia/plugin-ext-vscode": "1.45.0", + "@theia/plugin-metrics": "1.45.0", + "@theia/preferences": "1.45.0", + "@theia/preview": "1.45.0", + "@theia/process": "1.45.0", + "@theia/property-view": "1.45.0", + "@theia/scm": "1.45.0", + "@theia/scm-extra": "1.45.0", + "@theia/search-in-workspace": "1.45.0", + "@theia/secondary-window": "1.45.0", + "@theia/task": "1.45.0", + "@theia/terminal": "1.45.0", + "@theia/timeline": "1.45.0", + "@theia/toolbar": "1.45.0", + "@theia/typehierarchy": "1.45.0", + "@theia/userstorage": "1.45.0", + "@theia/variable-resolver": "1.45.0", + "@theia/vsx-registry": "1.45.0", + "@theia/workspace": "1.45.0" + }, + "scripts": { + "prepare:no-native": "lerna run prepare --scope=\"@theia/re-exports\" && lerna run generate-theia-re-exports --scope=\"@theia/core\"", + "clean": "theia clean", + "build": "yarn -s compile && yarn -s bundle", + "bundle": "theia build --mode development", + "compile": "tsc -b", + "start": "theia start", + "start:debug": "yarn -s start --log-level=debug", + "start:watch": "concurrently --kill-others -n tsc,bundle,run -c red,yellow,green \"tsc -b -w --preserveWatchOutput\" \"yarn -s watch:bundle\" \"yarn -s start\"", + "watch": "concurrently --kill-others -n tsc,bundle -c red,yellow \"tsc -b -w --preserveWatchOutput\" \"yarn -s watch:bundle\"", + "watch:bundle": "theia build --watch --mode development", + "watch:compile": "tsc -b -w" + }, + "devDependencies": { + "@theia/cli": "1.45.0" + } +} diff --git a/examples/browser-only/tsconfig.json b/examples/browser-only/tsconfig.json new file mode 100644 index 0000000000000..d4bcfc14426b9 --- /dev/null +++ b/examples/browser-only/tsconfig.json @@ -0,0 +1,141 @@ +{ + "extends": "../../configs/base.tsconfig", + "include": [], + "compilerOptions": { + "composite": true + }, + "references": [ + { + "path": "../../dev-packages/cli" + }, + { + "path": "../../packages/bulk-edit" + }, + { + "path": "../../packages/callhierarchy" + }, + { + "path": "../../packages/console" + }, + { + "path": "../../packages/core" + }, + { + "path": "../../packages/debug" + }, + { + "path": "../../packages/editor" + }, + { + "path": "../../packages/editor-preview" + }, + { + "path": "../../packages/file-search" + }, + { + "path": "../../packages/filesystem" + }, + { + "path": "../../packages/getting-started" + }, + { + "path": "../../packages/git" + }, + { + "path": "../../packages/keymaps" + }, + { + "path": "../../packages/markers" + }, + { + "path": "../../packages/memory-inspector" + }, + { + "path": "../../packages/messages" + }, + { + "path": "../../packages/metrics" + }, + { + "path": "../../packages/mini-browser" + }, + { + "path": "../../packages/monaco" + }, + { + "path": "../../packages/navigator" + }, + { + "path": "../../packages/outline-view" + }, + { + "path": "../../packages/output" + }, + { + "path": "../../packages/plugin-dev" + }, + { + "path": "../../packages/plugin-ext" + }, + { + "path": "../../packages/plugin-ext-vscode" + }, + { + "path": "../../packages/plugin-metrics" + }, + { + "path": "../../packages/preferences" + }, + { + "path": "../../packages/preview" + }, + { + "path": "../../packages/process" + }, + { + "path": "../../packages/property-view" + }, + { + "path": "../../packages/scm" + }, + { + "path": "../../packages/scm-extra" + }, + { + "path": "../../packages/search-in-workspace" + }, + { + "path": "../../packages/secondary-window" + }, + { + "path": "../../packages/task" + }, + { + "path": "../../packages/terminal" + }, + { + "path": "../../packages/timeline" + }, + { + "path": "../../packages/toolbar" + }, + { + "path": "../../packages/typehierarchy" + }, + { + "path": "../../packages/userstorage" + }, + { + "path": "../../packages/variable-resolver" + }, + { + "path": "../../packages/vsx-registry" + }, + { + "path": "../../packages/workspace" + }, + { + "path": "../api-samples" + } + ] +} diff --git a/examples/browser-only/webpack.config.js b/examples/browser-only/webpack.config.js new file mode 100644 index 0000000000000..40e4ee963ba9f --- /dev/null +++ b/examples/browser-only/webpack.config.js @@ -0,0 +1,18 @@ +/** + * This file can be edited to customize webpack configuration. + * To reset delete this file and rerun theia build again. + */ +// @ts-check +const configs = require('./gen-webpack.config.js'); + + +/** + * Expose bundled modules on window.theia.moduleName namespace, e.g. + * window['theia']['@theia/core/lib/common/uri']. + * Such syntax can be used by external code, for instance, for testing. +configs[0].module.rules.push({ + test: /\.js$/, + loader: require.resolve('@theia/application-manager/lib/expose-loader') +}); */ + +module.exports = configs; diff --git a/package.json b/package.json index eedb0306c48a5..250eb85206fa3 100644 --- a/package.json +++ b/package.json @@ -62,8 +62,9 @@ "scripts": { "all": "yarn -s install && yarn -s lint && yarn -s build", "browser": "yarn -s --cwd examples/browser", + "browser-only": "yarn -s --cwd examples/browser-only", "build": "yarn -s compile && yarn -s build:examples", - "build:examples": "yarn browser build && yarn electron build", + "build:examples": "yarn browser build && yarn electron build && yarn browser-only build", "clean": "yarn -s rebuild:clean && yarn -s lint:clean && node scripts/run-reverse-topo.js yarn -s clean", "compile": "echo Compiling TypeScript sources... && yarn -s compile:clean && yarn -s compile:tsc", "compile:clean": "ts-clean dev-packages/* packages/*", diff --git a/packages/core/package.json b/packages/core/package.json index 85c21e0f86dce..babe4599f4599 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -134,8 +134,12 @@ "frontendPreload": "lib/browser/preload/preload-module", "preload": "lib/electron-browser/preload" }, + { + "frontendOnlyPreload": "lib/browser-only/preload/frontend-only-preload-module" + }, { "frontend": "lib/browser/i18n/i18n-frontend-module", + "frontendOnly": "lib/browser-only/i18n/i18n-frontend-only-module", "backend": "lib/node/i18n/i18n-backend-module" }, { diff --git a/packages/core/src/browser-only/frontend-only-application-module.ts b/packages/core/src/browser-only/frontend-only-application-module.ts new file mode 100644 index 0000000000000..680e7d227c83a --- /dev/null +++ b/packages/core/src/browser-only/frontend-only-application-module.ts @@ -0,0 +1,114 @@ +// ***************************************************************************** +// Copyright (C) 2023 EclipseSource and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { ContainerModule } from 'inversify'; +import { BackendStopwatch, CommandRegistry, Emitter, MeasurementOptions, OS } from '../common'; +import { ApplicationInfo, ApplicationServer, ExtensionInfo } from '../common/application-protocol'; +import { EnvVariable, EnvVariablesServer } from './../common/env-variables'; +import { bindMessageService } from '../browser/frontend-application-bindings'; +import { KeyStoreService } from '../common/key-store'; +import { QuickPickService } from '../common/quick-pick-service'; +import { QuickPickServiceImpl } from '../browser/quick-input'; +import { BackendRequestService, RequestService } from '@theia/request'; +import { ConnectionStatus, ConnectionStatusService } from '../browser/connection-status-service'; + +export { bindMessageService }; + +// is loaded directly after the regular frontend module +export const frontendOnlyApplicationModule = new ContainerModule((bind, unbind, isBound, rebind) => { + + if (isBound(CommandRegistry)) { + rebind(CommandRegistry).toSelf().inSingletonScope(); + } else { + bind(CommandRegistry).toSelf().inSingletonScope(); + } + + const stopwatch: BackendStopwatch = { + start: async (_name: string, _options?: MeasurementOptions | undefined): Promise => -1, + stop: async (_measurement: number, _message: string, _messageArgs: unknown[]): Promise => { } + }; + if (isBound(BackendStopwatch)) { + rebind(BackendStopwatch).toConstantValue(stopwatch); + } else { + bind(BackendStopwatch).toConstantValue(stopwatch); + } + + if (isBound(CommandRegistry)) { + rebind(QuickPickService).to(QuickPickServiceImpl).inSingletonScope(); + } else { + bind(QuickPickService).to(QuickPickServiceImpl).inSingletonScope(); + } + + const mockedApplicationServer: ApplicationServer = { + getExtensionsInfos: async (): Promise => [], + getApplicationInfo: async (): Promise => undefined, + getBackendOS: async (): Promise => OS.Type.Linux + }; + if (isBound(ApplicationServer)) { + rebind(ApplicationServer).toConstantValue(mockedApplicationServer); + } else { + bind(ApplicationServer).toConstantValue(mockedApplicationServer); + } + + const varServer: EnvVariablesServer = { + getExecPath: async (): Promise => '', + getVariables: async (): Promise => [], + getValue: async (_key: string): Promise => undefined, + getConfigDirUri: async (): Promise => '', + getHomeDirUri: async (): Promise => '', + getDrives: async (): Promise => [] + }; + if (isBound(EnvVariablesServer)) { + rebind(EnvVariablesServer).toConstantValue(varServer); + } else { + bind(EnvVariablesServer).toConstantValue(varServer); + } + + const keyStoreService: KeyStoreService = { + deletePassword: () => Promise.resolve(false), + findCredentials: () => Promise.resolve([]), + findPassword: () => Promise.resolve(undefined), + setPassword: () => Promise.resolve(), + getPassword: () => Promise.resolve(undefined) + }; + if (isBound(KeyStoreService)) { + rebind(KeyStoreService).toConstantValue(keyStoreService); + } else { + bind(KeyStoreService).toConstantValue(keyStoreService); + } + + const requestService: RequestService = { + configure: () => Promise.resolve(), + request: () => Promise.reject(), + resolveProxy: () => Promise.resolve(undefined) + }; + if (isBound(BackendRequestService)) { + rebind(BackendRequestService).toConstantValue(requestService); + } else { + bind(BackendRequestService).toConstantValue(requestService); + } + + const connectionStatusService: ConnectionStatusService = { + currentStatus: ConnectionStatus.ONLINE, + onStatusChange: new Emitter().event + }; + if (isBound(ConnectionStatusService)) { + rebind(ConnectionStatusService).toConstantValue(connectionStatusService); + } else { + bind(ConnectionStatusService).toConstantValue(connectionStatusService); + } + +}); diff --git a/packages/core/src/browser-only/i18n/i18n-frontend-only-module.ts b/packages/core/src/browser-only/i18n/i18n-frontend-only-module.ts new file mode 100644 index 0000000000000..e88337fcb65b7 --- /dev/null +++ b/packages/core/src/browser-only/i18n/i18n-frontend-only-module.ts @@ -0,0 +1,37 @@ +// ***************************************************************************** +// Copyright (C) 2023 EclipseSource and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { ContainerModule } from 'inversify'; +import { AsyncLocalizationProvider, LanguageInfo, Localization } from '../../common/i18n/localization'; +import { LanguageQuickPickService } from '../../browser/i18n/language-quick-pick-service'; + +export default new ContainerModule(bind => { + const i18nMock: AsyncLocalizationProvider = { + getCurrentLanguage: async (): Promise => 'en', + setCurrentLanguage: async (_languageId: string): Promise => { + + }, + getAvailableLanguages: async (): Promise => + [] + , + loadLocalization: async (_languageId: string): Promise => ({ + translations: {}, + languageId: 'en' + }) + }; + bind(AsyncLocalizationProvider).toConstantValue(i18nMock); + bind(LanguageQuickPickService).toSelf().inSingletonScope(); +}); diff --git a/packages/core/src/browser-only/logger-frontend-only-module.ts b/packages/core/src/browser-only/logger-frontend-only-module.ts new file mode 100644 index 0000000000000..e5fc84021476d --- /dev/null +++ b/packages/core/src/browser-only/logger-frontend-only-module.ts @@ -0,0 +1,63 @@ +// ***************************************************************************** +// Copyright (C) 2023 EclipseSource and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { ContainerModule, Container } from 'inversify'; +import { ILoggerServer, ILoggerClient, LogLevel, ConsoleLogger } from '../common/logger-protocol'; +import { ILogger, Logger, LoggerFactory, LoggerName } from '../common/logger'; + +// is loaded directly after the regular logger frontend module +export const loggerFrontendOnlyModule = new ContainerModule((bind, unbind, isBound, rebind) => { + const logger: ILoggerServer = { + setLogLevel: async (_name: string, _logLevel: number): Promise => { }, + getLogLevel: async (_name: string): Promise => LogLevel.INFO, + log: async (name: string, logLevel: number, message: string, params: unknown[]): Promise => { + ConsoleLogger.log(name, logLevel, message, params); + + }, + child: async (_name: string): Promise => { }, + dispose: (): void => { + }, + setClient: (_client: ILoggerClient | undefined): void => { + } + }; + if (isBound(ILoggerServer)) { + rebind(ILoggerServer).toConstantValue(logger); + } else { + bind(ILoggerServer).toConstantValue(logger); + } + + if (isBound(ILoggerServer)) { + rebind(LoggerFactory).toFactory(ctx => + (name: string) => { + const child = new Container({ defaultScope: 'Singleton' }); + child.parent = ctx.container; + child.bind(ILogger).to(Logger).inTransientScope(); + child.bind(LoggerName).toConstantValue(name); + return child.get(ILogger); + } + ); + } else { + bind(LoggerFactory).toFactory(ctx => + (name: string) => { + const child = new Container({ defaultScope: 'Singleton' }); + child.parent = ctx.container; + child.bind(ILogger).to(Logger).inTransientScope(); + child.bind(LoggerName).toConstantValue(name); + return child.get(ILogger); + } + ); + } +}); diff --git a/packages/core/src/browser-only/messaging/frontend-only-service-connection-provider.ts b/packages/core/src/browser-only/messaging/frontend-only-service-connection-provider.ts new file mode 100644 index 0000000000000..e2208565be4eb --- /dev/null +++ b/packages/core/src/browser-only/messaging/frontend-only-service-connection-provider.ts @@ -0,0 +1,39 @@ +// ***************************************************************************** +// Copyright (C) 2023 EclipseSource and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** +import { Event, RpcProxy, Channel, RpcProxyFactory, Emitter } from '../../common'; +import { injectable } from 'inversify'; +import { ServiceConnectionProvider } from '../../browser/messaging/service-connection-provider'; +import { ConnectionSource } from '../../browser/messaging/connection-source'; + +@injectable() +export class FrontendOnlyConnectionSource implements ConnectionSource { + onConnectionDidOpen = new Emitter().event; +} + +@injectable() +export class FrontendOnlyServiceConnectionProvider extends ServiceConnectionProvider { + onSocketDidOpen = Event.None; + onSocketDidClose = Event.None; + onIncomingMessageActivity = Event.None; + override createProxy(path: unknown, target?: unknown): RpcProxy { + console.debug(`[Frontend-Only Fallback] Created proxy connection for ${path}`); + const factory = target instanceof RpcProxyFactory ? target : new RpcProxyFactory(target); + return factory.createProxy(); + } + override listen(path: string, handler: ServiceConnectionProvider.ConnectionHandler, reconnect: boolean): void { + console.debug('[Frontend-Only Fallback] Listen to websocket connection requested'); + } +} diff --git a/packages/core/src/browser-only/messaging/messaging-frontend-only-module.ts b/packages/core/src/browser-only/messaging/messaging-frontend-only-module.ts new file mode 100644 index 0000000000000..55029105c81d8 --- /dev/null +++ b/packages/core/src/browser-only/messaging/messaging-frontend-only-module.ts @@ -0,0 +1,42 @@ +// ***************************************************************************** +// Copyright (C) 2023 EclipseSource and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** +import { ContainerModule } from 'inversify'; +import { WebSocketConnectionSource } from '../../browser/messaging/ws-connection-source'; +import { FrontendOnlyConnectionSource, FrontendOnlyServiceConnectionProvider } from './frontend-only-service-connection-provider'; +import { ConnectionSource } from '../../browser/messaging/connection-source'; +import { LocalConnectionProvider, RemoteConnectionProvider } from '../../browser/messaging/service-connection-provider'; + +// is loaded directly after the regular message frontend module +export const messagingFrontendOnlyModule = new ContainerModule((bind, unbind, isBound, rebind) => { + unbind(WebSocketConnectionSource); + bind(FrontendOnlyConnectionSource).toSelf().inSingletonScope(); + if (isBound(ConnectionSource)) { + rebind(ConnectionSource).toService(FrontendOnlyConnectionSource); + } else { + bind(ConnectionSource).toService(FrontendOnlyConnectionSource); + } + bind(FrontendOnlyServiceConnectionProvider).toSelf().inSingletonScope(); + if (isBound(LocalConnectionProvider)) { + rebind(LocalConnectionProvider).toService(FrontendOnlyServiceConnectionProvider); + } else { + bind(LocalConnectionProvider).toService(FrontendOnlyServiceConnectionProvider); + } + if (isBound(RemoteConnectionProvider)) { + rebind(RemoteConnectionProvider).toService(FrontendOnlyServiceConnectionProvider); + } else { + bind(RemoteConnectionProvider).toService(FrontendOnlyServiceConnectionProvider); + } +}); diff --git a/packages/core/src/browser-only/preload/frontend-only-preload-module.ts b/packages/core/src/browser-only/preload/frontend-only-preload-module.ts new file mode 100644 index 0000000000000..ea0ed3fcb7e2a --- /dev/null +++ b/packages/core/src/browser-only/preload/frontend-only-preload-module.ts @@ -0,0 +1,49 @@ +// ***************************************************************************** +// Copyright (C) 2023 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { ContainerModule } from 'inversify'; +import { LocalizationServer } from '../../common/i18n/localization-server'; +import { OS, OSBackendProvider } from '../../common/os'; +import { Localization } from '../../common/i18n/localization'; + +// loaded after regular preload module +export default new ContainerModule((bind, unbind, isBound, rebind) => { + const frontendOnlyLocalizationServer: LocalizationServer = { + loadLocalization: async (languageId: string): Promise => ({ translations: {}, languageId }) + }; + if (isBound(LocalizationServer)) { + rebind(LocalizationServer).toConstantValue(frontendOnlyLocalizationServer); + } else { + bind(LocalizationServer).toConstantValue(frontendOnlyLocalizationServer); + } + + const frontendOnlyOSBackendProvider: OSBackendProvider = { + getBackendOS: async (): Promise => { + if (window.navigator.platform.startsWith('Win')) { + return OS.Type.Windows; + } else if (window.navigator.platform.startsWith('Mac')) { + return OS.Type.OSX; + } else { + return OS.Type.Linux; + } + } + }; + if (isBound(OSBackendProvider)) { + rebind(OSBackendProvider).toConstantValue(frontendOnlyOSBackendProvider); + } else { + bind(OSBackendProvider).toConstantValue(frontendOnlyOSBackendProvider); + } +}); diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index ca1bb7f3ab971..34e857decdf2b 100644 --- a/packages/core/src/browser/frontend-application-module.ts +++ b/packages/core/src/browser/frontend-application-module.ts @@ -346,7 +346,6 @@ export const frontendApplicationModule = new ContainerModule((bind, _unbind, _is }); bind(FrontendConnectionStatusService).toSelf().inSingletonScope(); bind(ConnectionStatusService).toService(FrontendConnectionStatusService); - bind(FrontendApplicationContribution).toService(FrontendConnectionStatusService); bind(ApplicationConnectionStatusContribution).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).toService(ApplicationConnectionStatusContribution); diff --git a/packages/core/src/node/file-uri.ts b/packages/core/src/common/file-uri.ts similarity index 97% rename from packages/core/src/node/file-uri.ts rename to packages/core/src/common/file-uri.ts index 99a86fbb7b5ca..f92af9fc0fdaf 100644 --- a/packages/core/src/node/file-uri.ts +++ b/packages/core/src/common/file-uri.ts @@ -15,8 +15,8 @@ // ***************************************************************************** import { URI as Uri } from 'vscode-uri'; -import URI from '../common/uri'; -import { isWindows } from '../common/os'; +import URI from './uri'; +import { isWindows } from './os'; export namespace FileUri { diff --git a/packages/core/src/electron-main/electron-main-application.ts b/packages/core/src/electron-main/electron-main-application.ts index cc9b03039b15c..2efa9fce419e2 100644 --- a/packages/core/src/electron-main/electron-main-application.ts +++ b/packages/core/src/electron-main/electron-main-application.ts @@ -24,7 +24,7 @@ import { existsSync, mkdirSync } from 'fs-extra'; import { fork, ForkOptions } from 'child_process'; import { DefaultTheme, FrontendApplicationConfig } from '@theia/application-package/lib/application-props'; import URI from '../common/uri'; -import { FileUri } from '../node/file-uri'; +import { FileUri } from '../common/file-uri'; import { Deferred } from '../common/promise-util'; import { MaybePromise } from '../common/types'; import { ContributionProvider } from '../common/contribution-provider'; diff --git a/packages/core/src/electron-main/theia-electron-window.ts b/packages/core/src/electron-main/theia-electron-window.ts index 855939e1ca9dc..17c82fa9a4511 100644 --- a/packages/core/src/electron-main/theia-electron-window.ts +++ b/packages/core/src/electron-main/theia-electron-window.ts @@ -22,7 +22,7 @@ import { ElectronMainApplicationGlobals } from './electron-main-constants'; import { DisposableCollection, Emitter, Event } from '../common'; import { createDisposableListener } from './event-utils'; import { URI } from '../common/uri'; -import { FileUri } from '../node/file-uri'; +import { FileUri } from '../common/file-uri'; import { TheiaRendererAPI } from './electron-api-main'; /** diff --git a/packages/core/src/node/env-variables/env-variables-server.ts b/packages/core/src/node/env-variables/env-variables-server.ts index 3d795a9063dde..7c337992ac90b 100644 --- a/packages/core/src/node/env-variables/env-variables-server.ts +++ b/packages/core/src/node/env-variables/env-variables-server.ts @@ -21,7 +21,7 @@ import * as drivelist from 'drivelist'; import { pathExists, mkdir } from 'fs-extra'; import { EnvVariable, EnvVariablesServer } from '../../common/env-variables'; import { isWindows } from '../../common/os'; -import { FileUri } from '../file-uri'; +import { FileUri } from '../../common/file-uri'; @injectable() export class EnvVariablesServerImpl implements EnvVariablesServer { diff --git a/packages/core/src/node/file-uri.spec.ts b/packages/core/src/node/file-uri.spec.ts index 378fe71edfc99..f4689d6a58672 100644 --- a/packages/core/src/node/file-uri.spec.ts +++ b/packages/core/src/node/file-uri.spec.ts @@ -17,7 +17,7 @@ import * as os from 'os'; import * as path from 'path'; import * as chai from 'chai'; -import { FileUri } from './file-uri'; +import { FileUri } from '../common/file-uri'; import { isWindows } from '../common/os'; const expect = chai.expect; diff --git a/packages/core/src/node/index.ts b/packages/core/src/node/index.ts index 192f8089d99de..c7d735f4f03d6 100644 --- a/packages/core/src/node/index.ts +++ b/packages/core/src/node/index.ts @@ -16,7 +16,7 @@ export * from './backend-application'; export * from './debug'; -export * from './file-uri'; +export * from '../common/file-uri'; export * from './messaging'; export * from './cli'; export { FileSystemLocking } from './filesystem-locking'; diff --git a/packages/external-terminal/src/electron-node/linux-external-terminal-service.ts b/packages/external-terminal/src/electron-node/linux-external-terminal-service.ts index 44180a1fa1743..2c0b34b4d1035 100644 --- a/packages/external-terminal/src/electron-node/linux-external-terminal-service.ts +++ b/packages/external-terminal/src/electron-node/linux-external-terminal-service.ts @@ -18,7 +18,7 @@ import * as cp from 'child_process'; import * as fs from '@theia/core/shared/fs-extra'; import { injectable } from '@theia/core/shared/inversify'; import { OS } from '@theia/core/lib/common/os'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { ExternalTerminalService, ExternalTerminalConfiguration } from '../common/external-terminal'; /*--------------------------------------------------------------------------------------------- diff --git a/packages/external-terminal/src/electron-node/mac-external-terminal-service.ts b/packages/external-terminal/src/electron-node/mac-external-terminal-service.ts index 9dd06d706fb89..7c5e8bd43acc4 100644 --- a/packages/external-terminal/src/electron-node/mac-external-terminal-service.ts +++ b/packages/external-terminal/src/electron-node/mac-external-terminal-service.ts @@ -16,7 +16,7 @@ import * as cp from 'child_process'; import { injectable } from '@theia/core/shared/inversify'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { ExternalTerminalService, ExternalTerminalConfiguration } from '../common/external-terminal'; /*--------------------------------------------------------------------------------------------- diff --git a/packages/external-terminal/src/electron-node/windows-external-terminal-service.ts b/packages/external-terminal/src/electron-node/windows-external-terminal-service.ts index d169339c38e95..7e8c54a1a900e 100644 --- a/packages/external-terminal/src/electron-node/windows-external-terminal-service.ts +++ b/packages/external-terminal/src/electron-node/windows-external-terminal-service.ts @@ -17,7 +17,7 @@ import * as cp from 'child_process'; import * as path from 'path'; import { injectable } from '@theia/core/shared/inversify'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { ExternalTerminalService, ExternalTerminalConfiguration } from '../common/external-terminal'; /*--------------------------------------------------------------------------------------------- diff --git a/packages/file-search/src/node/file-search-service-impl.ts b/packages/file-search/src/node/file-search-service-impl.ts index 8169a5077b788..609be17abef81 100644 --- a/packages/file-search/src/node/file-search-service-impl.ts +++ b/packages/file-search/src/node/file-search-service-impl.ts @@ -20,7 +20,7 @@ import * as readline from 'readline'; import { rgPath } from '@vscode/ripgrep'; import { injectable, inject } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { CancellationTokenSource, CancellationToken, ILogger, isWindows } from '@theia/core'; import { RawProcessFactory } from '@theia/process/lib/node'; import { FileSearchService, WHITESPACE_QUERY_SEPARATOR } from '../common/file-search-service'; diff --git a/packages/filesystem/package.json b/packages/filesystem/package.json index a1517d8fe786f..997fca2738f1f 100644 --- a/packages/filesystem/package.json +++ b/packages/filesystem/package.json @@ -11,6 +11,7 @@ "@types/uuid": "^7.0.3", "async-mutex": "^0.3.1", "body-parser": "^1.18.3", + "browserfs": "^1.4.3", "http-status-codes": "^1.3.0", "minimatch": "^5.1.0", "multer": "1.4.4-lts.1", @@ -33,6 +34,9 @@ "frontend": "lib/browser/filesystem-frontend-module", "backend": "lib/node/filesystem-backend-module" }, + { + "frontendOnly": "lib/browser-only/browser-only-filesystem-frontend-module" + }, { "frontend": "lib/browser/download/file-download-frontend-module", "backend": "lib/node/download/file-download-backend-module" diff --git a/packages/filesystem/src/browser-only/browser-only-filesystem-frontend-module.ts b/packages/filesystem/src/browser-only/browser-only-filesystem-frontend-module.ts new file mode 100644 index 0000000000000..eae27435b6d90 --- /dev/null +++ b/packages/filesystem/src/browser-only/browser-only-filesystem-frontend-module.ts @@ -0,0 +1,38 @@ +// ***************************************************************************** +// Copyright (C) 2023 EclipseSource and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { ContainerModule } from '@theia/core/shared/inversify'; +import { FileSystemProvider } from '../common/files'; +import { BrowserFSFileSystemProvider } from './browserfs-filesystem-provider'; +import { RemoteFileSystemProvider, RemoteFileSystemServer } from '../common/remote-file-system-provider'; +import { BrowserFSInitialization, DefaultBrowserFSInitialization } from './browserfs-filesystem-initialization'; +import { BrowserOnlyFileSystemProviderServer } from './browser-only-filesystem-provider-server'; + +export default new ContainerModule((bind, _unbind, isBound, rebind) => { + bind(DefaultBrowserFSInitialization).toSelf(); + bind(BrowserFSFileSystemProvider).toSelf(); + bind(BrowserFSInitialization).toService(DefaultBrowserFSInitialization); + if (isBound(FileSystemProvider)) { + rebind(FileSystemProvider).to(BrowserFSFileSystemProvider).inSingletonScope(); + } else { + bind(FileSystemProvider).to(BrowserFSFileSystemProvider).inSingletonScope(); + } + if (isBound(RemoteFileSystemProvider)) { + rebind(RemoteFileSystemServer).to(BrowserOnlyFileSystemProviderServer).inSingletonScope(); + } else { + bind(RemoteFileSystemServer).to(BrowserOnlyFileSystemProviderServer).inSingletonScope(); + } +}); diff --git a/packages/filesystem/src/browser-only/browser-only-filesystem-provider-server.ts b/packages/filesystem/src/browser-only/browser-only-filesystem-provider-server.ts new file mode 100644 index 0000000000000..5b3c2b4a3a555 --- /dev/null +++ b/packages/filesystem/src/browser-only/browser-only-filesystem-provider-server.ts @@ -0,0 +1,32 @@ +// ***************************************************************************** +// Copyright (C) 2023 EclipseSource and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { injectable } from '@theia/core/shared/inversify'; +import { FileSystemProviderServer } from '../common/remote-file-system-provider'; +import { Event } from '@theia/core'; + +/** + * Backend component. + * + * JSON-RPC server exposing a wrapped file system provider remotely. + */ +@injectable() +export class BrowserOnlyFileSystemProviderServer extends FileSystemProviderServer { + + // needed because users expect implicitly the RemoteFileSystemServer to be a RemoteFileSystemProxyFactory + onDidOpenConnection = Event.None; + onDidCloseConnection = Event.None; +} diff --git a/packages/filesystem/src/browser-only/browserfs-filesystem-initialization.ts b/packages/filesystem/src/browser-only/browserfs-filesystem-initialization.ts new file mode 100644 index 0000000000000..0b17d3076a468 --- /dev/null +++ b/packages/filesystem/src/browser-only/browserfs-filesystem-initialization.ts @@ -0,0 +1,61 @@ +// ***************************************************************************** +// Copyright (C) 2023 EclipseSource and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import type { FSModule } from 'browserfs/dist/node/core/FS'; +import type { BrowserFSFileSystemProvider } from './browserfs-filesystem-provider'; +import { injectable } from '@theia/core/shared/inversify'; +import { FileSystem, initialize } from 'browserfs'; +import MountableFileSystem from 'browserfs/dist/node/backend/MountableFileSystem'; + +export const BrowserFSInitialization = Symbol('BrowserFSInitialization'); +export interface BrowserFSInitialization { + createMountableFileSystem(): Promise + initializeFS: (fs: FSModule, provider: BrowserFSFileSystemProvider) => Promise; +} + +@injectable() +export class DefaultBrowserFSInitialization implements BrowserFSInitialization { + + createMountableFileSystem(): Promise { + return new Promise(resolve => { + FileSystem.IndexedDB.Create({}, (e, persistedFS) => { + if (e) { + throw e; + } + if (!persistedFS) { + throw Error('Could not create filesystem'); + } + FileSystem.MountableFileSystem.Create({ + '/home': persistedFS + + }, (error, mountableFS) => { + if (error) { + throw error; + } + if (!mountableFS) { + throw Error('Could not create filesystem'); + } + initialize(mountableFS); + resolve(mountableFS); + }); + }); + }); + } + + async initializeFS(fs: FSModule, provider: BrowserFSFileSystemProvider): Promise { + + } +} diff --git a/packages/filesystem/src/browser-only/browserfs-filesystem-provider.ts b/packages/filesystem/src/browser-only/browserfs-filesystem-provider.ts new file mode 100644 index 0000000000000..ca833d9536313 --- /dev/null +++ b/packages/filesystem/src/browser-only/browserfs-filesystem-provider.ts @@ -0,0 +1,462 @@ +// ***************************************************************************** +// Copyright (C) 2023 EclipseSource and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/platform/files/node/diskFileSystemProvider.ts + +/* eslint-disable no-null/no-null */ + +import { inject, injectable } from '@theia/core/shared/inversify'; +import { + FileChange, FileDeleteOptions, FileOpenOptions, + FileOverwriteOptions, FileReadStreamOptions, FileSystemProviderCapabilities, + FileSystemProviderError, + FileSystemProviderErrorCode, + FileSystemProviderWithFileReadWriteCapability, + FileType, FileUpdateOptions, FileUpdateResult, FileWriteOptions, Stat, WatchOptions, createFileSystemProviderError +} from '../common/files'; +import { Event, URI, Disposable, CancellationToken } from '@theia/core'; +import { TextDocumentContentChangeEvent } from '@theia/core/shared/vscode-languageserver-protocol'; +import { ReadableStreamEvents } from '@theia/core/lib/common/stream'; +import { BFSRequire } from 'browserfs'; +import type { FSModule } from 'browserfs/dist/node/core/FS'; +import type { FileSystem } from 'browserfs/dist/node/core/file_system'; +import MountableFileSystem from 'browserfs/dist/node/backend/MountableFileSystem'; +import { basename, dirname, normalize } from 'path'; +import Stats from 'browserfs/dist/node/core/node_fs_stats'; +import { retry } from '@theia/core/lib/common/promise-util'; +import { BrowserFSInitialization } from './browserfs-filesystem-initialization'; + +// adapted from DiskFileSystemProvider +@injectable() +export class BrowserFSFileSystemProvider implements FileSystemProviderWithFileReadWriteCapability { + capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.FileReadWrite; + onDidChangeCapabilities: Event = Event.None; + onDidChangeFile: Event = Event.None; + onFileWatchError: Event = Event.None; + private mapHandleToPos: Map = new Map(); + private writeHandles: Set = new Set(); + private canFlush: boolean = true; + + private fs: FSModule; + private mountableFS: MountableFileSystem; + private initialized: Promise; + + constructor(@inject(BrowserFSInitialization) readonly initialization: BrowserFSInitialization) { + const init = async (): Promise => { + this.mountableFS = await initialization.createMountableFileSystem(); + this.fs = BFSRequire('fs'); + await initialization.initializeFS(this.fs, new Proxy(this, { + get(target, prop, receiver): unknown { + if (prop === 'initialized') { + return Promise.resolve(true); + } + return Reflect.get(target, prop, receiver); + } + })); + return true; + }; + this.initialized = init(); + } + + async mount(mountPoint: string, fs: FileSystem): Promise { + await this.initialized; + this.mountableFS.mount(mountPoint, fs); + }; + + watch(_resource: URI, _opts: WatchOptions): Disposable { + return Disposable.NULL; + } + async stat(resource: URI): Promise { + await this.initialized; + const path = this.toFilePath(resource); + + let stats: Stats; + try { + stats = await this.promisify(this.fs.stat)(path) as Stats; + } catch (error) { + throw this.toFileSystemProviderError(error); + } + if (stats === undefined) { + throw new Error(`Could not read file stat for resource '${path}'`); + } + return { + type: this.toType(stats, /* symbolicLink */undefined), // FIXME: missing symbolicLink + ctime: stats.birthtime.getTime(), // intentionally not using ctime here, we want the creation time + mtime: stats.mtime.getTime(), + size: stats.size, + // FIXME: missing mode, permissions + }; + + } + async mkdir(resource: URI): Promise { + await this.initialized; + try { + await this.promisify(this.fs.mkdir)(this.toFilePath(resource)); + } catch (error) { + throw this.toFileSystemProviderError(error); + } + } + async readdir(resource: URI): Promise<[string, FileType][]> { + await this.initialized; + try { + + const children = await this.promisify(this.fs.readdir)(this.toFilePath(resource)) as string[]; + const result: [string, FileType][] = []; + await Promise.all(children.map(async child => { + try { + const stat = await this.stat(resource.resolve(child)); + result.push([child, stat.type]); + } catch (error) { + console.trace(error); // ignore errors for individual entries that can arise from permission denied + } + })); + + return result; + } catch (error) { + throw this.toFileSystemProviderError(error); + } + } + async delete(resource: URI, _opts: FileDeleteOptions): Promise { + await this.initialized; + // FIXME use options + try { + await this.promisify(this.fs.unlink)(this.toFilePath(resource)); + } catch (error) { + throw this.toFileSystemProviderError(error); + } + } + async rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise { + await this.initialized; + const fromFilePath = this.toFilePath(from); + const toFilePath = this.toFilePath(to); + if (fromFilePath === toFilePath) { + return; // simulate node.js behaviour here and do a no-op if paths match + } + try { + // assume FS is path case sensitive - correct? + const targetExists = await this.promisify(this.fs.exists)(toFilePath); + if (targetExists) { + throw Error(`File '${toFilePath}' already exists.`); + } + if (fromFilePath === toFilePath) { + return Promise.resolve(); + } + + await this.promisify(this.fs.rename)(fromFilePath, toFilePath); + + const stat = await this.promisify(this.fs.lstat)(toFilePath) as Stats; + if (stat.isDirectory() || stat.isSymbolicLink()) { + return Promise.resolve(); // only for files + } + const fd = await this.promisify(open)(toFilePath, 'a'); + try { + await this.promisify(this.fs.futimes)(fd, stat.atime, new Date()); + } catch (error) { + // ignore + } + + this.promisify(this.fs.close)(fd); + } catch (error) { + // rewrite some typical errors that can happen especially around symlinks + // to something the user can better understand + if (error.code === 'EINVAL' || error.code === 'EBUSY' || error.code === 'ENAMETOOLONG') { + error = new Error(`Unable to move '${basename(fromFilePath)}' into '${basename(dirname(toFilePath))}' (${error.toString()}).`); + } + + throw this.toFileSystemProviderError(error); + } + } + async copy?(from: URI, to: URI, opts: FileOverwriteOptions): Promise { + await this.initialized; + throw new Error('Method not implemented.'); + } + async readFile(resource: URI): Promise { + await this.initialized; + try { + const filePath = this.toFilePath(resource); + return await this.promisify(this.fs.readFile)(filePath) as Uint8Array; + } catch (error) { + throw this.toFileSystemProviderError(error); + } + } + async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { + await this.initialized; + let handle: number | undefined = undefined; + try { + const filePath = this.toFilePath(resource); + + // Validate target unless { create: true, overwrite: true } + if (!opts.create || !opts.overwrite) { + const fileExists = await this.promisify(this.fs.exists)(filePath); + if (fileExists) { + if (!opts.overwrite) { + throw createFileSystemProviderError('File already exists', FileSystemProviderErrorCode.FileExists); + } + } else { + if (!opts.create) { + throw createFileSystemProviderError('File does not exist', FileSystemProviderErrorCode.FileNotFound); + } + } + } + + // Open + handle = await this.open(resource, { create: true }); + + // Write content at once + await this.write(handle, 0, content, 0, content.byteLength); + } catch (error) { + throw this.toFileSystemProviderError(error); + } finally { + if (typeof handle === 'number') { + await this.close(handle); + } + } + } + readFileStream?(resource: URI, opts: FileReadStreamOptions, token: CancellationToken): ReadableStreamEvents { + throw new Error('Method not implemented.'); + } + async open(resource: URI, opts: FileOpenOptions): Promise { + await this.initialized; + try { + const filePath = this.toFilePath(resource); + + let flags: string | undefined = undefined; + if (opts.create) { + // we take opts.create as a hint that the file is opened for writing + // as such we use 'w' to truncate an existing or create the + // file otherwise. we do not allow reading. + if (!flags) { + flags = 'w'; + } + } else { + // otherwise we assume the file is opened for reading + // as such we use 'r' to neither truncate, nor create + // the file. + flags = 'r'; + } + + const handle = await this.promisify(this.fs.open)(filePath, flags) as number; + + // remember this handle to track file position of the handle + // we init the position to 0 since the file descriptor was + // just created and the position was not moved so far (see + // also http://man7.org/linux/man-pages/man2/open.2.html - + // "The file offset is set to the beginning of the file.") + this.mapHandleToPos.set(handle, 0); + + // remember that this handle was used for writing + if (opts.create) { + this.writeHandles.add(handle); + } + + return handle; + } catch (error) { + throw this.toFileSystemProviderError(error); + } + } + async close(fd: number): Promise { + await this.initialized; + // remove this handle from map of positions + this.mapHandleToPos.delete(fd); + + // if a handle is closed that was used for writing, ensure + // to flush the contents to disk if possible. + if (this.writeHandles.delete(fd) && this.canFlush) { + try { + await this.promisify(this.fs.fdatasync)(fd); + } catch (error) { + // In some exotic setups it is well possible that node fails to sync + // In that case we disable flushing and log the error to our logger + this.canFlush = false; + console.error(error); + } + } + + await this.promisify(this.fs.close)(fd); + } + async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + await this.initialized; + const normalizedPos = this.normalizePos(fd, pos); + + let bytesRead: number | null = null; + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result: { bytesRead: number, buffer: Uint8Array } | number = (await this.promisify(this.fs.read)(fd, data, offset, length, normalizedPos)) as any; + + if (typeof result === 'number') { + bytesRead = result; // node.d.ts fail + } else { + bytesRead = result.bytesRead; + } + + return bytesRead; + } catch (error) { + throw this.toFileSystemProviderError(error); + } finally { + this.updatePos(fd, normalizedPos, bytesRead); + } + } + async write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + await this.initialized; + // we know at this point that the file to write to is truncated and thus empty + // if the write now fails, the file remains empty. as such we really try hard + // to ensure the write succeeds by retrying up to three times. + return retry(() => this.doWrite(fd, pos, data, offset, length), 100 /* ms delay */, 3 /* retries */); + + } + private async doWrite(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + await this.initialized; + const normalizedPos = this.normalizePos(fd, pos); + + let bytesWritten: number | null = null; + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result: { bytesWritten: number, buffer: Uint8Array } | number = (await this.promisify(this.fs.write)(fd, data, offset, length, normalizedPos)) as any; + + if (typeof result === 'number') { + bytesWritten = result; // node.d.ts fail + } else { + bytesWritten = result.bytesWritten; + } + + return bytesWritten; + } catch (error) { + throw this.toFileSystemProviderError(error); + } finally { + this.updatePos(fd, normalizedPos, bytesWritten); + } + } + private normalizePos(fd: number, pos: number): number | null { + + // when calling fs.read/write we try to avoid passing in the "pos" argument and + // rather prefer to pass in "null" because this avoids an extra seek(pos) + // call that in some cases can even fail (e.g. when opening a file over FTP - + // see https://github.com/microsoft/vscode/issues/73884). + // + // as such, we compare the passed in position argument with our last known + // position for the file descriptor and use "null" if they match. + if (pos === this.mapHandleToPos.get(fd)) { + return null; + } + + return pos; + } + private updatePos(fd: number, pos: number | null, bytesLength: number | null): void { + const lastKnownPos = this.mapHandleToPos.get(fd); + if (typeof lastKnownPos === 'number') { + + // pos !== null signals that previously a position was used that is + // not null. node.js documentation explains, that in this case + // the internal file pointer is not moving and as such we do not move + // our position pointer. + // + // Docs: "If position is null, data will be read from the current file position, + // and the file position will be updated. If position is an integer, the file position + // will remain unchanged." + if (typeof pos === 'number') { + // do not modify the position + } else if (typeof bytesLength === 'number') { + this.mapHandleToPos.set(fd, lastKnownPos + bytesLength); + } else { + this.mapHandleToPos.delete(fd); + } + } + } + async access?(resource: URI, mode?: number | undefined): Promise { + await this.initialized; + throw new Error('Method not implemented.'); + } + async fsPath?(resource: URI): Promise { + await this.initialized; + throw new Error('Method not implemented.'); + } + async updateFile?(resource: URI, changes: TextDocumentContentChangeEvent[], opts: FileUpdateOptions): Promise { + await this.initialized; + throw new Error('Method not implemented.'); + } + + private toFilePath(resource: URI): string { + return normalize(resource.path.toString()); + } + + private toType(entry: Stats, symbolicLink?: { dangling: boolean }): FileType { + // Signal file type by checking for file / directory, except: + // - symbolic links pointing to non-existing files are FileType.Unknown + // - files that are neither file nor directory are FileType.Unknown + let type: FileType; + if (symbolicLink?.dangling) { + type = FileType.Unknown; + } else if (entry.isFile()) { + type = FileType.File; + } else if (entry.isDirectory()) { + type = FileType.Directory; + } else { + type = FileType.Unknown; + } + + // Always signal symbolic link as file type additionally + if (symbolicLink) { + type |= FileType.SymbolicLink; + } + + return type; + } + + // FIXME typing + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private promisify(f: Function): (...args: any[]) => Promise { + // eslint-disable-next-line @typescript-eslint/tslint/config, @typescript-eslint/no-explicit-any + return function (...args: any[]) { + return new Promise((resolve, reject) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + f(...args, (err: Error, result: T) => err ? reject(err) : resolve(result)); + }); + }; + } + + private toFileSystemProviderError(error: NodeJS.ErrnoException): FileSystemProviderError { + if (error instanceof FileSystemProviderError) { + return error; // avoid double conversion + } + + let code: FileSystemProviderErrorCode; + switch (error.code) { + case 'ENOENT': + code = FileSystemProviderErrorCode.FileNotFound; + break; + case 'EISDIR': + code = FileSystemProviderErrorCode.FileIsADirectory; + break; + case 'ENOTDIR': + code = FileSystemProviderErrorCode.FileNotADirectory; + break; + case 'EEXIST': + code = FileSystemProviderErrorCode.FileExists; + break; + case 'EPERM': + case 'EACCES': + code = FileSystemProviderErrorCode.NoPermissions; + break; + default: + code = FileSystemProviderErrorCode.Unknown; + } + + return createFileSystemProviderError(error, code); + } +} diff --git a/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts b/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts index c3edec5afd967..a5cd9a7a4ad63 100644 --- a/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts +++ b/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts @@ -28,7 +28,7 @@ import { DefaultFileDialogService, OpenFileDialogProps, SaveFileDialogProps } fr // solution. // // eslint-disable-next-line @theia/runtime-import-check -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { OpenDialogOptions, SaveDialogOptions } from '../../electron-common/electron-api'; import '@theia/core/lib/electron-common/electron-api'; diff --git a/packages/filesystem/src/node/disk-file-system-provider.spec.ts b/packages/filesystem/src/node/disk-file-system-provider.spec.ts index ed0011db27e92..f5f434fb7b0fd 100644 --- a/packages/filesystem/src/node/disk-file-system-provider.spec.ts +++ b/packages/filesystem/src/node/disk-file-system-provider.spec.ts @@ -18,7 +18,7 @@ import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposa import { EncodingService } from '@theia/core/lib/common/encoding-service'; import { ILogger } from '@theia/core/lib/common/logger'; import { MockLogger } from '@theia/core/lib/common/test/mock-logger'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { IPCConnectionProvider } from '@theia/core/lib/node/messaging/ipc-connection-provider'; import { Container, ContainerModule } from '@theia/core/shared/inversify'; import { equal, fail } from 'assert'; diff --git a/packages/filesystem/src/node/disk-file-system-provider.ts b/packages/filesystem/src/node/disk-file-system-provider.ts index b5bdf2234934c..e1e1e64e532a3 100644 --- a/packages/filesystem/src/node/disk-file-system-provider.ts +++ b/packages/filesystem/src/node/disk-file-system-provider.ts @@ -35,7 +35,7 @@ import { import { promisify } from 'util'; import URI from '@theia/core/lib/common/uri'; import { Path } from '@theia/core/lib/common/path'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { Event, Emitter } from '@theia/core/lib/common/event'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { OS, isWindows } from '@theia/core/lib/common/os'; diff --git a/packages/filesystem/src/node/download/directory-archiver.spec.ts b/packages/filesystem/src/node/download/directory-archiver.spec.ts index a1c1e2c410c21..12a2752f25a8c 100644 --- a/packages/filesystem/src/node/download/directory-archiver.spec.ts +++ b/packages/filesystem/src/node/download/directory-archiver.spec.ts @@ -21,7 +21,7 @@ import { extract } from 'tar-fs'; import { expect } from 'chai'; import URI from '@theia/core/lib/common/uri'; import { MockDirectoryArchiver } from './test/mock-directory-archiver'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; const track = temp.track(); diff --git a/packages/filesystem/src/node/download/directory-archiver.ts b/packages/filesystem/src/node/download/directory-archiver.ts index c8c99526e8af8..40513d74f720e 100644 --- a/packages/filesystem/src/node/download/directory-archiver.ts +++ b/packages/filesystem/src/node/download/directory-archiver.ts @@ -18,7 +18,7 @@ import { injectable } from '@theia/core/shared/inversify'; import * as fs from '@theia/core/shared/fs-extra'; import { pack } from 'tar-fs'; import URI from '@theia/core/lib/common/uri'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; @injectable() export class DirectoryArchiver { diff --git a/packages/filesystem/src/node/download/file-download-endpoint.ts b/packages/filesystem/src/node/download/file-download-endpoint.ts index 6237c1e7ef050..a33523ef82bd0 100644 --- a/packages/filesystem/src/node/download/file-download-endpoint.ts +++ b/packages/filesystem/src/node/download/file-download-endpoint.ts @@ -21,7 +21,7 @@ import { injectable, inject, named } from '@theia/core/shared/inversify'; import { json } from 'body-parser'; import { Application, Router } from '@theia/core/shared/express'; import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { FileDownloadHandler } from './file-download-handler'; @injectable() diff --git a/packages/filesystem/src/node/download/file-download-handler.ts b/packages/filesystem/src/node/download/file-download-handler.ts index f1ae5e478c366..7a54513e6bcae 100644 --- a/packages/filesystem/src/node/download/file-download-handler.ts +++ b/packages/filesystem/src/node/download/file-download-handler.ts @@ -24,7 +24,7 @@ import { OK, BAD_REQUEST, METHOD_NOT_ALLOWED, NOT_FOUND, INTERNAL_SERVER_ERROR, import URI from '@theia/core/lib/common/uri'; import { isEmpty } from '@theia/core/lib/common/objects'; import { ILogger } from '@theia/core/lib/common/logger'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { DirectoryArchiver } from './directory-archiver'; import { FileDownloadData } from '../../common/download/file-download-data'; import { FileDownloadCache, DownloadStorageItem } from './file-download-cache'; diff --git a/packages/filesystem/src/node/file-change-collection.spec.ts b/packages/filesystem/src/node/file-change-collection.spec.ts index 30f218a5e0eeb..a745e20899084 100644 --- a/packages/filesystem/src/node/file-change-collection.spec.ts +++ b/packages/filesystem/src/node/file-change-collection.spec.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import * as assert from 'assert'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { FileChangeCollection } from './file-change-collection'; import { FileChangeType } from '../common/files'; diff --git a/packages/filesystem/src/node/nsfw-watcher/nsfw-filesystem-service.ts b/packages/filesystem/src/node/nsfw-watcher/nsfw-filesystem-service.ts index 6e4723076fea3..ebc947846e804 100644 --- a/packages/filesystem/src/node/nsfw-watcher/nsfw-filesystem-service.ts +++ b/packages/filesystem/src/node/nsfw-watcher/nsfw-filesystem-service.ts @@ -18,7 +18,7 @@ import nsfw = require('@theia/core/shared/nsfw'); import path = require('path'); import { promises as fsp } from 'fs'; import { IMinimatch, Minimatch } from 'minimatch'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { FileChangeType, FileSystemWatcherService, FileSystemWatcherServiceClient, WatchOptions } from '../../common/filesystem-watcher-protocol'; diff --git a/packages/git/src/node/dugite-git-watcher.slow-spec.ts b/packages/git/src/node/dugite-git-watcher.slow-spec.ts index 5a4f0e95bdd4f..843e092029471 100644 --- a/packages/git/src/node/dugite-git-watcher.slow-spec.ts +++ b/packages/git/src/node/dugite-git-watcher.slow-spec.ts @@ -18,7 +18,7 @@ import * as fs from '@theia/core/shared/fs-extra'; import * as temp from 'temp'; import * as path from 'path'; import { expect } from 'chai'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { Git } from '../common/git'; import { DugiteGit } from './dugite-git'; import { Repository } from '../common'; diff --git a/packages/git/src/node/dugite-git.slow-spec.ts b/packages/git/src/node/dugite-git.slow-spec.ts index 888507b401d54..b4954dd321188 100644 --- a/packages/git/src/node/dugite-git.slow-spec.ts +++ b/packages/git/src/node/dugite-git.slow-spec.ts @@ -16,7 +16,7 @@ import * as temp from 'temp'; import { expect } from 'chai'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { GitFileStatus } from '../common'; import { createGit } from './test/binding-helper'; diff --git a/packages/git/src/node/dugite-git.spec.ts b/packages/git/src/node/dugite-git.spec.ts index db3592f321c7c..340fafaf6e1b2 100644 --- a/packages/git/src/node/dugite-git.spec.ts +++ b/packages/git/src/node/dugite-git.spec.ts @@ -22,7 +22,7 @@ import * as fs from '@theia/core/shared/fs-extra'; import { expect } from 'chai'; import { Git } from '../common/git'; import { git as gitExec } from 'dugite-extra/lib/core/git'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { WorkingDirectoryStatus, Repository, GitUtils, GitFileStatus, GitFileChange } from '../common'; import { initRepository, createTestRepository } from 'dugite-extra/lib/command/test-helper'; import { createGit } from './test/binding-helper'; diff --git a/packages/git/src/node/dugite-git.ts b/packages/git/src/node/dugite-git.ts index e72ad9dee1724..857c757b12ba6 100644 --- a/packages/git/src/node/dugite-git.ts +++ b/packages/git/src/node/dugite-git.ts @@ -24,7 +24,7 @@ import { clone } from 'dugite-extra/lib/command/clone'; import { fetch } from 'dugite-extra/lib/command/fetch'; import { stash } from 'dugite-extra/lib/command/stash'; import { merge } from 'dugite-extra/lib/command/merge'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { getStatus } from 'dugite-extra/lib/command/status'; import { createCommit } from 'dugite-extra/lib/command/commit'; import { stage, unstage } from 'dugite-extra/lib/command/stage'; diff --git a/packages/mini-browser/src/node/mini-browser-endpoint.ts b/packages/mini-browser/src/node/mini-browser-endpoint.ts index f8400b862bfff..1c3420bf5eba7 100644 --- a/packages/mini-browser/src/node/mini-browser-endpoint.ts +++ b/packages/mini-browser/src/node/mini-browser-endpoint.ts @@ -20,7 +20,7 @@ import * as fs from '@theia/core/shared/fs-extra'; import { lookup } from 'mime-types'; import { injectable, inject, named } from '@theia/core/shared/inversify'; import { Application, Request, Response } from '@theia/core/shared/express'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { ILogger } from '@theia/core/lib/common/logger'; import { MaybePromise } from '@theia/core/lib/common/types'; import { ContributionProvider } from '@theia/core/lib/common/contribution-provider'; diff --git a/packages/navigator/src/browser/navigator-diff.spec.ts b/packages/navigator/src/browser/navigator-diff.spec.ts index ced5d792c6678..2f70af9ace45f 100644 --- a/packages/navigator/src/browser/navigator-diff.spec.ts +++ b/packages/navigator/src/browser/navigator-diff.spec.ts @@ -31,7 +31,7 @@ import { OpenerService } from '@theia/core/lib/browser'; import { MockOpenerService } from '@theia/core/lib/browser/test/mock-opener-service'; import { MessageService } from '@theia/core/lib/common/message-service'; import { MessageClient } from '@theia/core/lib/common/message-service-protocol'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { DiskFileSystemProvider } from '@theia/filesystem/lib/node/disk-file-system-provider'; diff --git a/packages/plugin-dev/src/node/hosted-instance-manager.ts b/packages/plugin-dev/src/node/hosted-instance-manager.ts index dc11d18603478..e56ad1d1932f7 100644 --- a/packages/plugin-dev/src/node/hosted-instance-manager.ts +++ b/packages/plugin-dev/src/node/hosted-instance-manager.ts @@ -24,7 +24,7 @@ import URI from '@theia/core/lib/common/uri'; import { ContributionProvider } from '@theia/core/lib/common/contribution-provider'; import { HostedPluginUriPostProcessor, HostedPluginUriPostProcessorSymbolName } from './hosted-plugin-uri-postprocessor'; import { environment, isWindows } from '@theia/core'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { LogType } from '@theia/plugin-ext/lib/common/types'; import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/node/hosted-plugin'; import { MetadataScanner } from '@theia/plugin-ext/lib/hosted/node/metadata-scanner'; diff --git a/packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts b/packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts index f3f6459aa95cc..599d61fae2158 100644 --- a/packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts +++ b/packages/plugin-ext-vscode/src/node/plugin-vscode-file-handler.ts @@ -18,8 +18,8 @@ import { PluginDeployerFileHandler, PluginDeployerEntry, PluginDeployerFileHandl import * as filenamify from 'filenamify'; import { inject, injectable } from '@theia/core/shared/inversify'; import * as fs from '@theia/core/shared/fs-extra'; -import { FileUri } from '@theia/core/lib/node'; import { PluginVSCodeEnvironment } from '../common/plugin-vscode-environment'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { unpackToDeploymentDir } from './plugin-vscode-utils'; export const isVSCodePluginFile = (pluginPath?: string) => Boolean(pluginPath && (pluginPath.endsWith('.vsix') || pluginPath.endsWith('.tgz'))); diff --git a/packages/plugin-ext/src/hosted/node/scanners/file-plugin-uri-factory.ts b/packages/plugin-ext/src/hosted/node/scanners/file-plugin-uri-factory.ts index 3ad460efd6c78..74518f5097d66 100644 --- a/packages/plugin-ext/src/hosted/node/scanners/file-plugin-uri-factory.ts +++ b/packages/plugin-ext/src/hosted/node/scanners/file-plugin-uri-factory.ts @@ -17,7 +17,7 @@ import { injectable } from '@theia/core/shared/inversify'; import * as path from 'path'; import URI from '@theia/core/lib/common/uri'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { PluginPackage } from '../../../common'; import { PluginUriFactory } from './plugin-uri-factory'; /** diff --git a/packages/plugin-ext/src/main/node/handlers/plugin-theia-file-handler.ts b/packages/plugin-ext/src/main/node/handlers/plugin-theia-file-handler.ts index 28983e18b15af..367bdf5aa4d01 100644 --- a/packages/plugin-ext/src/main/node/handlers/plugin-theia-file-handler.ts +++ b/packages/plugin-ext/src/main/node/handlers/plugin-theia-file-handler.ts @@ -21,7 +21,7 @@ import { Deferred } from '@theia/core/lib/common/promise-util'; import { getTempDirPathAsync } from '../temp-dir-util'; import * as fs from '@theia/core/shared/fs-extra'; import * as filenamify from 'filenamify'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { PluginTheiaEnvironment } from '../../common/plugin-theia-environment'; @injectable() diff --git a/packages/plugin-ext/src/main/node/plugins-key-value-storage.ts b/packages/plugin-ext/src/main/node/plugins-key-value-storage.ts index 833654c727e98..48aa75ebc3fa1 100644 --- a/packages/plugin-ext/src/main/node/plugins-key-value-storage.ts +++ b/packages/plugin-ext/src/main/node/plugins-key-value-storage.ts @@ -18,7 +18,7 @@ import { injectable, inject, postConstruct } from '@theia/core/shared/inversify' import { FileSystemLocking } from '@theia/core/lib/node'; import * as fs from '@theia/core/shared/fs-extra'; import * as path from 'path'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { PluginPaths } from './paths/const'; diff --git a/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.slow-spec.ts b/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.slow-spec.ts index 70f659b355bd0..d33d0e8c1a736 100644 --- a/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.slow-spec.ts +++ b/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.slow-spec.ts @@ -16,7 +16,7 @@ import { Container } from '@theia/core/shared/inversify'; import { ILogger, isWindows } from '@theia/core'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { MockLogger } from '@theia/core/lib/common/test/mock-logger'; import { RawProcessFactory, RawProcessOptions, RawProcess, ProcessManager } from '@theia/process/lib/node'; import { RipgrepSearchInWorkspaceServer, RgPath } from './ripgrep-search-in-workspace-server'; diff --git a/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.ts b/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.ts index a10fc6588a15c..dc55caa616149 100644 --- a/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.ts +++ b/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.ts @@ -18,7 +18,7 @@ import * as fs from '@theia/core/shared/fs-extra'; import * as path from 'path'; import { ILogger } from '@theia/core'; import { RawProcess, RawProcessFactory, RawProcessOptions } from '@theia/process/lib/node'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import URI from '@theia/core/lib/common/uri'; import { inject, injectable } from '@theia/core/shared/inversify'; import { SearchInWorkspaceServer, SearchInWorkspaceOptions, SearchInWorkspaceResult, SearchInWorkspaceClient, LinePreview } from '../common/search-in-workspace-interface'; diff --git a/packages/terminal/src/node/shell-process.ts b/packages/terminal/src/node/shell-process.ts index 524a697082c7e..4bf64080756c5 100644 --- a/packages/terminal/src/node/shell-process.ts +++ b/packages/terminal/src/node/shell-process.ts @@ -20,7 +20,7 @@ import { ILogger } from '@theia/core/lib/common/logger'; import { TerminalProcess, TerminalProcessOptions, ProcessManager, MultiRingBuffer } from '@theia/process/lib/node'; import { isWindows, isOSX } from '@theia/core/lib/common'; import URI from '@theia/core/lib/common/uri'; -import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileUri } from '@theia/core/lib/common/file-uri'; import { EnvironmentUtils } from '@theia/core/lib/node/environment-utils'; import { parseArgs } from '@theia/process/lib/node/utils'; diff --git a/packages/workspace/package.json b/packages/workspace/package.json index 797c43fedbdb2..542c9a1b56205 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -16,6 +16,9 @@ { "frontend": "lib/browser/workspace-frontend-module", "backend": "lib/node/workspace-backend-module" + }, + { + "frontendOnly": "lib/browser-only/workspace-frontend-only-module" } ], "keywords": [ diff --git a/packages/workspace/src/browser-only/browser-only-workspace-server.ts b/packages/workspace/src/browser-only/browser-only-workspace-server.ts new file mode 100644 index 0000000000000..2c94b3b997791 --- /dev/null +++ b/packages/workspace/src/browser-only/browser-only-workspace-server.ts @@ -0,0 +1,69 @@ +// ***************************************************************************** +// Copyright (C) 2023 EclipseSource and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** +import { inject, injectable } from '@theia/core/shared/inversify'; +import { WorkspaceServer } from '../common/workspace-protocol'; +import { ILogger, isStringArray } from '@theia/core'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; + +export const RECENT_WORKSPACES_LOCAL_STORAGE_KEY = 'workspaces'; + +@injectable() +export class BrowserOnlyWorkspaceServer implements WorkspaceServer { + + @inject(ILogger) + protected logger: ILogger; + + @inject(FileService) + protected readonly fileService: FileService; + + async getRecentWorkspaces(): Promise { + const storedWorkspaces = localStorage.getItem(RECENT_WORKSPACES_LOCAL_STORAGE_KEY); + if (!storedWorkspaces) { + return []; + } + try { + const parsedWorkspaces = JSON.parse(storedWorkspaces); + if (isStringArray(parsedWorkspaces)) { + return parsedWorkspaces; + } + } catch (e) { + this.logger.error(e); + return []; + } + return []; + } + + async getMostRecentlyUsedWorkspace(): Promise { + const workspaces = await this.getRecentWorkspaces(); + return workspaces[0]; + } + + async setMostRecentlyUsedWorkspace(uri: string): Promise { + const workspaces = await this.getRecentWorkspaces(); + if (workspaces.includes(uri)) { + workspaces.splice(workspaces.indexOf(uri), 1); + } + localStorage.setItem(RECENT_WORKSPACES_LOCAL_STORAGE_KEY, JSON.stringify([uri, ...workspaces])); + } + + async removeRecentWorkspace(uri: string): Promise { + const workspaces = await this.getRecentWorkspaces(); + if (workspaces.includes(uri)) { + workspaces.splice(workspaces.indexOf(uri), 1); + } + localStorage.setItem(RECENT_WORKSPACES_LOCAL_STORAGE_KEY, JSON.stringify(workspaces)); + } +} diff --git a/packages/workspace/src/browser-only/workspace-frontend-only-module.ts b/packages/workspace/src/browser-only/workspace-frontend-only-module.ts new file mode 100644 index 0000000000000..2953f66fdfd2d --- /dev/null +++ b/packages/workspace/src/browser-only/workspace-frontend-only-module.ts @@ -0,0 +1,28 @@ +// ***************************************************************************** +// Copyright (C) 2023 EclipseSource and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { ContainerModule, interfaces } from '@theia/core/shared/inversify'; +import { BrowserOnlyWorkspaceServer } from './browser-only-workspace-server'; +import { WorkspaceServer } from '../common'; + +export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => { + bind(BrowserOnlyWorkspaceServer).toSelf().inSingletonScope(); + if (isBound(WorkspaceServer)) { + rebind(WorkspaceServer).toService(BrowserOnlyWorkspaceServer); + } else { + bind(WorkspaceServer).toService(BrowserOnlyWorkspaceServer); + } +}); diff --git a/tsconfig.json b/tsconfig.json index 732d0029c6500..ac07be3ca619e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -42,6 +42,9 @@ { "path": "examples/browser" }, + { + "path": "examples/browser-only" + }, { "path": "examples/electron" }, diff --git a/yarn.lock b/yarn.lock index 4a9da1bdbf274..011b396fd23e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3160,6 +3160,13 @@ async-mutex@^0.4.0: dependencies: tslib "^2.4.0" +async@^2.1.4, async@^2.6.4: + version "2.6.4" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" + integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== + dependencies: + lodash "^4.17.14" + async@^3.2.3, async@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" @@ -3295,6 +3302,13 @@ base64id@2.0.0, base64id@~2.0.0: resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== +basic-auth@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + bcrypt-pbkdf@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" @@ -3446,6 +3460,14 @@ browser-stdout@1.3.1: resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== +browserfs@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/browserfs/-/browserfs-1.4.3.tgz#92ffc6063967612daccdb8566d3fc03f521205fb" + integrity sha512-tz8HClVrzTJshcyIu8frE15cjqjcBIu15Bezxsvl/i+6f59iNCN3kznlWjz0FEb3DlnDx3gW5szxeT6D1x0s0w== + dependencies: + async "^2.1.4" + pako "^1.0.4" + browserslist@^4.14.5, browserslist@^4.21.9, browserslist@^4.22.1: version "4.22.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.1.tgz#ba91958d1a59b87dab6fed8dfbcb3da5e2e9c619" @@ -3702,7 +3724,7 @@ chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1: +chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -4200,6 +4222,11 @@ cors@~2.8.5: object-assign "^4" vary "^1" +corser@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87" + integrity sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ== + cosmiconfig@8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.0.0.tgz#e9feae014eab580f858f8a0288f38997a7bebe97" @@ -5315,7 +5342,7 @@ event-stream@=3.3.4: stream-combiner "~0.0.4" through "~2.3.1" -eventemitter3@^4.0.4: +eventemitter3@^4.0.0, eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== @@ -5670,7 +5697,7 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== -follow-redirects@^1.14.0, follow-redirects@^1.15.0: +follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.15.0: version "1.15.3" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== @@ -6258,7 +6285,7 @@ hasha@^5.0.0: is-stream "^2.0.0" type-fest "^0.8.0" -he@1.2.0: +he@1.2.0, he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== @@ -6355,6 +6382,34 @@ http-proxy-agent@^5.0.0: agent-base "6" debug "4" +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +http-server@^14.1.1: + version "14.1.1" + resolved "https://registry.yarnpkg.com/http-server/-/http-server-14.1.1.tgz#d60fbb37d7c2fdff0f0fbff0d0ee6670bd285e2e" + integrity sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A== + dependencies: + basic-auth "^2.0.1" + chalk "^4.1.2" + corser "^2.0.1" + he "^1.2.0" + html-encoding-sniffer "^3.0.0" + http-proxy "^1.18.1" + mime "^1.6.0" + minimist "^1.2.6" + opener "^1.5.1" + portfinder "^1.0.28" + secure-compare "3.0.1" + union "~0.5.0" + url-join "^4.0.1" + http-status-codes@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-1.4.0.tgz#6e4c15d16ff3a9e2df03b89f3a55e1aae05fb477" @@ -7558,7 +7613,7 @@ lodash.union@^4.6.0: resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" integrity sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw== -lodash@^4.17.15, lodash@^4.17.21, lodash@^4.5.1: +lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.21, lodash@^4.5.1: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -7821,7 +7876,7 @@ mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.27, mime-types@~2.1.24, dependencies: mime-db "1.52.0" -mime@1.6.0, mime@^1.3.4, mime@^1.4.1: +mime@1.6.0, mime@^1.3.4, mime@^1.4.1, mime@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== @@ -8018,7 +8073,7 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.4: +"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.4, mkdirp@^0.5.6: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== @@ -8786,6 +8841,11 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +opener@^1.5.1: + version "1.5.2" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" + integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== + optionator@^0.9.1: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" @@ -8992,6 +9052,11 @@ pacote@^15.2.0: ssri "^10.0.0" tar "^6.1.11" +pako@^1.0.4: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -9234,6 +9299,15 @@ playwright@1.38.1: optionalDependencies: fsevents "2.3.2" +portfinder@^1.0.28: + version "1.0.32" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.32.tgz#2fe1b9e58389712429dc2bea5beb2146146c7f81" + integrity sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg== + dependencies: + async "^2.6.4" + debug "^3.2.7" + mkdirp "^0.5.6" + postcss-modules-extract-imports@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" @@ -9530,7 +9604,7 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" -qs@^6.9.1, qs@^6.9.4: +qs@^6.4.0, qs@^6.9.1, qs@^6.9.4: version "6.11.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== @@ -10058,16 +10132,16 @@ safe-array-concat@^1.0.1: has-symbols "^1.0.3" isarray "^2.0.5" +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - safe-regex-test@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" @@ -10141,6 +10215,11 @@ schema-utils@^4.0.0: ajv-formats "^2.1.1" ajv-keywords "^5.1.0" +secure-compare@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/secure-compare/-/secure-compare-3.0.1.tgz#f1a0329b308b221fae37b9974f3d578d0ca999e3" + integrity sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw== + seek-bzip@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.6.tgz#35c4171f55a680916b52a07859ecf3b5857f21c4" @@ -11429,6 +11508,13 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== +union@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/union/-/union-0.5.0.tgz#b2c11be84f60538537b846edb9ba266ba0090075" + integrity sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA== + dependencies: + qs "^6.4.0" + unique-filename@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-3.0.0.tgz#48ba7a5a16849f5080d26c760c86cf5cf05770ea"