From 2880525ee542e6d895872f194256cecae21aeff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Mar=C3=A9chal?= Date: Mon, 10 May 2021 16:31:18 -0400 Subject: [PATCH] core: add BackendApplicationServer The BackendApplicationServer is an optional backend contribution that serves frontend files. When not bound, the generators may bind one. This component is useful when your application is packaged in such a way that you need to customize how the frontend is served. --- .../src/generator/backend-generator.ts | 50 ++++++++++--------- examples/api-samples/package.json | 3 +- .../src/node/api-samples-backend-module.ts | 25 ++++++++++ .../node/sample-backend-application-server.ts | 29 +++++++++++ .../src/node/backend-application-module.ts | 12 ++++- packages/core/src/node/backend-application.ts | 35 +++++++------ 6 files changed, 114 insertions(+), 40 deletions(-) create mode 100644 examples/api-samples/src/node/api-samples-backend-module.ts create mode 100644 examples/api-samples/src/node/sample-backend-application-server.ts diff --git a/dev-packages/application-manager/src/generator/backend-generator.ts b/dev-packages/application-manager/src/generator/backend-generator.ts index 62304ebaa5637..7020a4eb90d5d 100644 --- a/dev-packages/application-manager/src/generator/backend-generator.ts +++ b/dev-packages/application-manager/src/generator/backend-generator.ts @@ -26,17 +26,17 @@ export class BackendGenerator extends AbstractGenerator { protected compileServer(backendModules: Map): string { return `// @ts-check -require('reflect-metadata'); +require('reflect-metadata');${this.ifElectron(` // Patch electron version if missing, see https://github.com/eclipse-theia/theia/pull/7361#pullrequestreview-377065146 if (typeof process.versions.electron === 'undefined' && typeof process.env.THEIA_ELECTRON_VERSION === 'string') { process.versions.electron = process.env.THEIA_ELECTRON_VERSION; -} +}`)} const path = require('path'); const express = require('express'); const { Container } = require('inversify'); -const { BackendApplication, CliManager } = require('@theia/core/lib/node'); +const { BackendApplication, BackendApplicationServer, CliManager } = require('@theia/core/lib/node'); const { backendApplicationModule } = require('@theia/core/lib/node/backend-application-module'); const { messagingBackendModule } = require('@theia/core/lib/node/messaging/messaging-backend-module'); const { loggerBackendModule } = require('@theia/core/lib/node/logger-backend-module'); @@ -46,49 +46,51 @@ container.load(backendApplicationModule); container.load(messagingBackendModule); container.load(loggerBackendModule); +function defaultServeStatic(app) { + app.use(express.static(path.resolve(__dirname, '../../lib'))) +} + function load(raw) { - return Promise.resolve(raw.default).then(module => - container.load(module) - ) + return Promise.resolve(raw.default).then( + module => container.load(module) + ); } -function start(port, host, argv) { - if (argv === undefined) { - argv = process.argv; +function start(port, host, argv = process.argv) { + if (!container.isBound(BackendApplicationServer)) { + container.bind(BackendApplicationServer).toConstantValue({ configure: defaultServeStatic }); } - - const cliManager = container.get(CliManager); - return cliManager.initializeCli(argv).then(function () { - const application = container.get(BackendApplication); - application.use(express.static(path.join(__dirname, '../../lib'))); - application.use(express.static(path.join(__dirname, '../../lib/index.html'))); - return application.start(port, host); + return container.get(CliManager).initializeCli(argv).then(() => { + return container.get(BackendApplication).start(port, host); }); } module.exports = (port, host, argv) => Promise.resolve()${this.compileBackendModuleImports(backendModules)} - .then(() => start(port, host, argv)).catch(reason => { - console.error('Failed to start the backend application.'); - if (reason) { - console.error(reason); - } - throw reason; - });`; + .then(() => start(port, host, argv)).catch(error => { + console.error('Failed to start the backend application:'); + console.error(error); + process.exitCode = 1; + throw error; + }); +`; } protected compileMain(backendModules: Map): string { return `// @ts-check const { BackendApplicationConfigProvider } = require('@theia/core/lib/node/backend-application-config-provider'); const main = require('@theia/core/lib/node/main'); + BackendApplicationConfigProvider.set(${this.prettyStringify(this.pck.props.backend.config)}); const serverModule = require('./server'); const serverAddress = main.start(serverModule()); -serverAddress.then(function ({ port, address }) { + +serverAddress.then(({ port, address }) => { if (process && process.send) { process.send({ port, address }); } }); + module.exports = serverAddress; `; } diff --git a/examples/api-samples/package.json b/examples/api-samples/package.json index 1cf4acfabbf76..17319bb270093 100644 --- a/examples/api-samples/package.json +++ b/examples/api-samples/package.json @@ -12,7 +12,8 @@ }, "theiaExtensions": [ { - "frontend": "lib/browser/api-samples-frontend-module" + "frontend": "lib/browser/api-samples-frontend-module", + "backend": "lib/node/api-samples-backend-module" }, { "frontend": "lib/browser/menu/sample-browser-menu-module", diff --git a/examples/api-samples/src/node/api-samples-backend-module.ts b/examples/api-samples/src/node/api-samples-backend-module.ts new file mode 100644 index 0000000000000..4d32e810cd110 --- /dev/null +++ b/examples/api-samples/src/node/api-samples-backend-module.ts @@ -0,0 +1,25 @@ +/******************************************************************************** + * Copyright (C) 2021 Ericsson 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { ContainerModule } from '@theia/core/shared/inversify'; +import { BackendApplicationServer } from '@theia/core/lib/node'; +import { SampleBackendApplicationServer } from './sample-backend-application-server'; + +export default new ContainerModule(bind => { + if (process.env.SAMPLE_BACKEND_APPLICATION_SERVER) { + bind(BackendApplicationServer).to(SampleBackendApplicationServer).inSingletonScope(); + } +}); diff --git a/examples/api-samples/src/node/sample-backend-application-server.ts b/examples/api-samples/src/node/sample-backend-application-server.ts new file mode 100644 index 0000000000000..560ad50bb737b --- /dev/null +++ b/examples/api-samples/src/node/sample-backend-application-server.ts @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (C) 2021 Ericsson 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable } from '@theia/core/shared/inversify'; +import { BackendApplicationServer } from '@theia/core/lib/node'; +import express = require('@theia/core/shared/express'); + +@injectable() +export class SampleBackendApplicationServer implements BackendApplicationServer { + + configure(app: express.Application): void { + app.get('*', (req, res) => { + res.status(200).send('SampleBackendApplicationServer OK'); + }); + } +} diff --git a/packages/core/src/node/backend-application-module.ts b/packages/core/src/node/backend-application-module.ts index 41d43532205d6..5443c6a08afce 100644 --- a/packages/core/src/node/backend-application-module.ts +++ b/packages/core/src/node/backend-application-module.ts @@ -20,7 +20,7 @@ import { bindContributionProvider, MessageService, MessageClient, ConnectionHandler, JsonRpcConnectionHandler, CommandService, commandServicePath, messageServicePath } from '../common'; -import { BackendApplication, BackendApplicationContribution, BackendApplicationCliContribution } from './backend-application'; +import { BackendApplication, BackendApplicationContribution, BackendApplicationCliContribution, BackendApplicationServer } from './backend-application'; import { CliManager, CliContribution } from './cli'; import { IPCConnectionProvider } from './messaging'; import { ApplicationServerImpl } from './application-server'; @@ -59,6 +59,16 @@ export const backendApplicationModule = new ContainerModule(bind => { bind(BackendApplication).toSelf().inSingletonScope(); bindContributionProvider(bind, BackendApplicationContribution); + // Bind the BackendApplicationServer as a BackendApplicationContribution + // and fallback to an empty contribution if never bound. + bind(BackendApplicationContribution).toDynamicValue(ctx => { + if (ctx.container.isBound(BackendApplicationServer)) { + return ctx.container.get(BackendApplicationServer); + } else { + console.warn('no BackendApplicationServer is set, frontend might not be available'); + return {}; + } + }).inSingletonScope(); bind(IPCConnectionProvider).toSelf().inSingletonScope(); diff --git a/packages/core/src/node/backend-application.ts b/packages/core/src/node/backend-application.ts index 07c73209aa8ee..f6da69ee205aa 100644 --- a/packages/core/src/node/backend-application.ts +++ b/packages/core/src/node/backend-application.ts @@ -29,8 +29,23 @@ import { environment } from '../common/index'; import { AddressInfo } from 'net'; import { ApplicationPackage } from '@theia/application-package'; -export const BackendApplicationContribution = Symbol('BackendApplicationContribution'); +const APP_PROJECT_PATH = 'app-project-path'; + +const TIMER_WARNING_THRESHOLD = 50; + +const DEFAULT_PORT = environment.electron.is() ? 0 : 3000; +const DEFAULT_HOST = 'localhost'; +const DEFAULT_SSL = false; +export const BackendApplicationServer = Symbol('BackendApplicationServer'); +/** + * This service is responsible for serving the frontend files. + * + * When not bound, `@theia/cli` generators will bind it on the fly to serve files according to its own layout. + */ +export interface BackendApplicationServer extends BackendApplicationContribution { } + +export const BackendApplicationContribution = Symbol('BackendApplicationContribution'); /** * Contribution for hooking into the backend lifecycle. */ @@ -82,14 +97,6 @@ export interface BackendApplicationContribution { onStop?(app?: express.Application): void; } -const defaultPort = environment.electron.is() ? 0 : 3000; -const defaultHost = 'localhost'; -const defaultSSL = false; - -const appProjectPath = 'app-project-path'; - -const TIMER_WARNING_THRESHOLD = 50; - @injectable() export class BackendApplicationCliContribution implements CliContribution { @@ -101,12 +108,12 @@ export class BackendApplicationCliContribution implements CliContribution { projectPath: string; configure(conf: yargs.Argv): void { - conf.option('port', { alias: 'p', description: 'The port the backend server listens on.', type: 'number', default: defaultPort }); - conf.option('hostname', { alias: 'h', description: 'The allowed hostname for connections.', type: 'string', default: defaultHost }); - conf.option('ssl', { description: 'Use SSL (HTTPS), cert and certkey must also be set', type: 'boolean', default: defaultSSL }); + conf.option('port', { alias: 'p', description: 'The port the backend server listens on.', type: 'number', default: DEFAULT_PORT }); + conf.option('hostname', { alias: 'h', description: 'The allowed hostname for connections.', type: 'string', default: DEFAULT_HOST }); + conf.option('ssl', { description: 'Use SSL (HTTPS), cert and certkey must also be set', type: 'boolean', default: DEFAULT_SSL }); conf.option('cert', { description: 'Path to SSL certificate.', type: 'string' }); conf.option('certkey', { description: 'Path to SSL certificate key.', type: 'string' }); - conf.option(appProjectPath, { description: 'Sets the application project directory', default: this.appProjectPath() }); + conf.option(APP_PROJECT_PATH, { description: 'Sets the application project directory', default: this.appProjectPath() }); } setArguments(args: yargs.Arguments): void { @@ -115,7 +122,7 @@ export class BackendApplicationCliContribution implements CliContribution { this.ssl = args.ssl as boolean; this.cert = args.cert as string; this.certkey = args.certkey as string; - this.projectPath = args[appProjectPath] as string; + this.projectPath = args[APP_PROJECT_PATH] as string; } protected appProjectPath(): string {