Skip to content

Commit

Permalink
Support 'browser-only' Theia (#12853)
Browse files Browse the repository at this point in the history
Adds tooling, adapts the current code base and provides an example application for the
new 'browser-only' Theia application target. This target generates a Theia frontend
application which can run without a backend, transforming the Theia application to a
static site.

Adapts tooling to:
- support new 'browser-only' application target
- support new 'frontendOnly' and 'frontendOnlyPreload' module declarations
- use http-server when starting 'browser-only' applications

Replaces backend services with browser-only variants
- implementation of BrowserFS-based filesystem
- implementation of 'ServiceConnectionProvider' which timeouts for all requests,
  enabling loading all Theia packages

Adds a browser-only example application and api-samples showcasing the customization of
the new BrowserFS-based filesystem.

Co-authored-by: Remi Schnekenburger <[email protected]>
Co-authored-by: Alexandra Buzila <[email protected]>
Co-authored-by: Tobias Ortmayr <[email protected]>
Co-authored-by: Eugen Neufeld <[email protected]>
  • Loading branch information
5 people authored Jan 16, 2024
1 parent a6d7e9b commit 6f01be5
Show file tree
Hide file tree
Showing 71 changed files with 1,602 additions and 63 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<a name="breaking_changes_not_yet_released">[Breaking Changes:](#breaking_changes_not_yet_released)</a>

- [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)
Expand Down
1 change: 1 addition & 0 deletions dev-packages/application-manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
await fs.ensureFile(path);
await fs.writeFile(path, content);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import { AbstractGenerator } from './abstract-generator';
export class BackendGenerator extends AbstractGenerator {

async generate(): Promise<void> {
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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class FrontendGenerator extends AbstractGenerator {

async generate(options?: GeneratorOptions): Promise<void> {
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()) {
Expand Down Expand Up @@ -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 => `\
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ export class WebpackGenerator extends AbstractGenerator {

async generate(): Promise<void> {
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());
}
Expand Down Expand Up @@ -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.
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion dev-packages/application-manager/src/rebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];

Expand Down
39 changes: 38 additions & 1 deletion dev-packages/application-package/src/application-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -140,10 +140,24 @@ export class ApplicationPackage {
return this._frontendPreloadModules ??= this.computeModules('frontendPreload');
}

get frontendOnlyPreloadModules(): Map<string, string> {
if (!this._frontendPreloadModules) {
this._frontendPreloadModules = this.computeModules('frontendOnlyPreload', 'frontendPreload');
}
return this._frontendPreloadModules;
}

get frontendModules(): Map<string, string> {
return this._frontendModules ??= this.computeModules('frontend');
}

get frontendOnlyModules(): Map<string, string> {
if (!this._frontendModules) {
this._frontendModules = this.computeModules('frontendOnly', 'frontend');
}
return this._frontendModules;
}

get frontendElectronModules(): Map<string, string> {
return this._frontendElectronModules ??= this.computeModules('frontendElectron', 'frontend');
}
Expand Down Expand Up @@ -227,6 +241,10 @@ export class ApplicationPackage {
return this.target === ApplicationProps.ApplicationTarget.electron;
}

isBrowserOnly(): boolean {
return this.target === ApplicationProps.ApplicationTarget.browserOnly;
}

ifBrowser<T>(value: T): T | undefined;
ifBrowser<T>(value: T, defaultValue: T): T;
ifBrowser<T>(value: T, defaultValue?: T): T | undefined {
Expand All @@ -239,14 +257,33 @@ export class ApplicationPackage {
return this.isElectron() ? value : defaultValue;
}

ifBrowserOnly<T>(value: T): T | undefined;
ifBrowserOnly<T>(value: T, defaultValue: T): T;
ifBrowserOnly<T>(value: T, defaultValue?: T): T | undefined {
return this.isBrowserOnly() ? value : defaultValue;
}

get targetBackendModules(): Map<string, string> {
if (this.isBrowserOnly()) {
return new Map();
}
return this.ifBrowser(this.backendModules, this.backendElectronModules);
}

get targetFrontendModules(): Map<string, string> {
if (this.isBrowserOnly()) {
return this.frontendOnlyModules;
}
return this.ifBrowser(this.frontendModules, this.frontendElectronModules);
}

get targetFrontendPreloadModules(): Map<string, string> {
if (this.isBrowserOnly()) {
return this.frontendOnlyPreloadModules;
}
return this.frontendPreloadModules;
}

get targetElectronMainModules(): Map<string, string> {
return this.ifElectron(this.electronMainModules, new Map());
}
Expand Down
5 changes: 3 additions & 2 deletions dev-packages/application-package/src/application-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions dev-packages/application-package/src/extension-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions dev-packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions dev-packages/cli/src/theia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,12 @@ function rebuildCommand(command: string, target: ApplicationProps.Target): yargs
}

function defineCommonOptions<T>(cli: yargs.Argv<T>): yargs.Argv<T & {
appTarget?: 'browser' | 'electron'
appTarget?: 'browser' | 'electron' | 'browser-only'
}> {
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,
});
}

Expand Down
3 changes: 3 additions & 0 deletions examples/api-samples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
Original file line number Diff line number Diff line change
@@ -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<void> {
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);
};
Loading

0 comments on commit 6f01be5

Please sign in to comment.