// ***************************************************************************** // Copyright (C) 2017 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 { EOL } from 'os'; 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)); if (this.pck.isElectron()) { await this.write(this.pck.backend('electron-main.js'), this.compileElectronMain(this.pck.targetElectronMainModules)); } } protected compileElectronMain(electronMainModules?: Map): string { return `// @ts-check require('@theia/core/shared/reflect-metadata'); ${this.emitStartupLogger('Electron main', 'electron main start', { requirePerformance: true })} ${this.emitStartupLog('loading modules...')} // Workaround for https://github.com/electron/electron/issues/9225. Chrome has an issue where // in certain locales (e.g. PL), image metrics are wrongly computed. We explicitly set the // LC_NUMERIC to prevent this from happening (selects the numeric formatting category of the // C locale, http://en.cppreference.com/w/cpp/locale/LC_categories). if (process.env.LC_ALL) { process.env.LC_ALL = 'C'; } process.env.LC_NUMERIC = 'C'; (async () => { // Useful for Electron/NW.js apps as GUI apps on macOS doesn't inherit the \`$PATH\` define // in your dotfiles (.bashrc/.bash_profile/.zshrc/etc). // https://github.com/electron/electron/issues/550#issuecomment-162037357 // https://github.com/eclipse-theia/theia/pull/3534#issuecomment-439689082 (await require('@theia/core/electron-shared/fix-path')).default(); const { resolve } = require('path'); const theiaAppProjectPath = resolve(__dirname, '..', '..'); process.env.THEIA_APP_PROJECT_PATH = theiaAppProjectPath; const { default: electronMainApplicationModule } = require('@theia/core/lib/electron-main/electron-main-application-module'); const { ElectronMainApplication, ElectronMainApplicationGlobals } = require('@theia/core/lib/electron-main/electron-main-application'); const { Container } = require('@theia/core/shared/inversify'); const { app } = require('electron'); const config = ${this.prettyStringify(this.pck.props.frontend.config)}; const isSingleInstance = ${this.pck.props.backend.config.singleInstance === true ? 'true' : 'false'}; if (isSingleInstance && !app.requestSingleInstanceLock(process.argv)) { // There is another instance running, exit now. The other instance will request focus. app.quit(); return; } const container = new Container(); container.load(electronMainApplicationModule); container.bind(ElectronMainApplicationGlobals).toConstantValue({ THEIA_APP_PROJECT_PATH: theiaAppProjectPath, THEIA_BACKEND_MAIN_PATH: resolve(__dirname, 'main.js'), THEIA_FRONTEND_HTML_PATH: resolve(__dirname, '..', '..', 'lib', 'frontend', 'index.html'), THEIA_SECONDARY_WINDOW_HTML_PATH: resolve(__dirname, '..', '..', 'lib', 'frontend', 'secondary-window.html') }); ${this.emitStartupLog('container created')} function load(raw) { return Promise.resolve(raw.default).then(module => container.load(module) ); } async function start() { ${this.emitStartupLog('resolving application')} const application = container.get(ElectronMainApplication); ${this.emitStartupLog('application resolved')} await application.start(config); } try { ${Array.from(electronMainModules?.values() ?? [], jsModulePath => `\ await load(require('${jsModulePath}'));`).join(EOL)} ${this.emitStartupLog('modules loaded')} await start(); } catch (reason) { if (typeof reason !== 'number') { console.error('Failed to start the electron application.'); if (reason) { console.error(reason); } } app.quit(); }; })(); `; } protected compileServer(backendModules: Map): string { return `// @ts-check require('reflect-metadata'); ${this.emitStartupLogger('Backend server', 'backend process start', { requirePerformance: true })} ${this.emitStartupLog('loading modules...')}${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; }`)} // Erase the ELECTRON_RUN_AS_NODE variable from the environment, else Electron apps started using Theia will pick it up. if ('ELECTRON_RUN_AS_NODE' in process.env) { delete process.env.ELECTRON_RUN_AS_NODE; } const path = require('path'); process.env.THEIA_APP_PROJECT_PATH = path.resolve(__dirname, '..', '..') const express = require('@theia/core/shared/express'); const { Container } = require('@theia/core/shared/inversify'); 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'); const container = new Container(); container.load(backendApplicationModule); container.load(messagingBackendModule); container.load(loggerBackendModule); ${this.emitStartupLog('container created')} function defaultServeStatic(app) { app.use(express.static(path.resolve(__dirname, '../../lib/frontend'))) } function load(raw) { return Promise.resolve(raw).then( module => container.load(module.default) ); } async function start(port, host, argv = process.argv) { if (!container.isBound(BackendApplicationServer)) { container.bind(BackendApplicationServer).toConstantValue({ configure: defaultServeStatic }); } let result = undefined; await container.get(CliManager).initializeCli(argv.slice(2), () => { ${this.emitStartupLog('resolving application')} const application = container.get(BackendApplication); ${this.emitStartupLog('application resolved')} return application.configured; }, async () => { result = container.get(BackendApplication).start(port, host); }); if (result) { return result; } else { return Promise.reject(0); } } module.exports = async (port, host, argv) => { try { ${Array.from(backendModules.values(), jsModulePath => `\ await load(require('${jsModulePath}'));`).join(EOL)} ${this.emitStartupLog('modules loaded')} return await start(port, host, argv); } catch (error) { if (typeof error !== 'number') { console.error('Failed to start the backend application:'); console.error(error); process.exitCode = 1; } throw error; } } `; } protected compileMain(backendModules: Map): string { return `// @ts-check ${this.emitStartupLogger('Backend main', 'backend process start', { requirePerformance: true })} ${this.emitStartupLog('entry point loaded')} 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)}); globalThis.extensionInfo = ${this.prettyStringify(this.pck.extensionPackages.map(({ name, version }) => ({ name, version })))}; const serverModule = require('./server'); const serverAddress = main.start(serverModule()); serverAddress.then((addressInfo) => { if (process && process.send && addressInfo) { process.send(addressInfo); } }); globalThis.serverAddress = serverAddress; `; } }