Skip to content

Commit

Permalink
[example-hybrid] Connect Electron to a server
Browse files Browse the repository at this point in the history
Add a new example package serving both web browsers and electron clients.
Add new commands for electron to connect to remote servers.

`electron.remote.connect` command to open a QuickOpenMenu and input an URL.
`electron.remote.history.clear` command to clear the local URL history.
`electron.remote.disconnect` to close the window in case you are connected to a server.

Adds an endpoint to tell if it is a Theia application or not.

Signed-off-by: Paul Maréchal <paul.marechal@ericsson.com>
  • Loading branch information
paul-marechal committed Sep 20, 2018
1 parent 6ea5e2f commit a9506fc
Show file tree
Hide file tree
Showing 24 changed files with 910 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,12 @@ export class ApplicationPackageManager {

async copy(): Promise<void> {
await fs.ensureDir(this.pck.lib());
await fs.copy(this.pck.frontend('index.html'), this.pck.lib('index.html'));
if (this.pck.isHybrid()) {
await fs.copy(this.pck.frontend('electron', 'index.html'), this.pck.lib('electron', 'index.html'));
await fs.copy(this.pck.frontend('browser', 'index.html'), this.pck.lib('browser', 'index.html'));
} else {
await fs.copy(this.pck.frontend('index.html'), this.pck.lib('index.html'));
}
}

async build(args: string[] = []): Promise<void> {
Expand All @@ -73,8 +78,14 @@ export class ApplicationPackageManager {
async start(args: string[] = []): Promise<void> {
if (this.pck.isElectron()) {
return this.startElectron(args);

} else if (this.pck.isBrowser()) {
return this.startBrowser(args);

} else if (this.pck.isHybrid()) {
return this.startHybrid(args);
}
return this.startBrowser(args);
throw new Error(`Unknown target: '${this.pck.target}'`);
}

async startElectron(args: string[]): Promise<void> {
Expand All @@ -99,4 +110,8 @@ export class ApplicationPackageManager {
this.__process.fork(this.pck.backend('main.js'), mainArgs, options);
}

async startHybrid(args: string[]): Promise<void> {
return this.startBrowser(args);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ export abstract class AbstractGenerator {
return os.EOL + lines.join(os.EOL);
}

get package(): ApplicationPackage {
return this.pck;
}

normalized(string: string): string {
return string.replace(/\W/, '');
}

protected ifBrowser(value: string, defaultValue: string = '') {
return this.pck.ifBrowser(value, defaultValue);
}
Expand All @@ -67,6 +75,10 @@ export abstract class AbstractGenerator {
return this.pck.ifElectron(value, defaultValue);
}

protected ifHybrid(value: string, defaultValue: string = '') {
return this.pck.ifHybrid(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 @@ -24,8 +24,32 @@ export class BackendGenerator extends AbstractGenerator {
await this.write(this.pck.backend('main.js'), this.compileMain(backendModules));
}

protected compileExpressStatic(segment: string = ''): string {
return `express.static(path.join(__dirname, '../../lib${segment}'), {
index: 'index.html'
})`;
}

protected compileMiddleware(backendModules: Map<string, string>): string {
return this.pck.isHybrid() ? `
const electron = ${this.compileExpressStatic('/electron')};
const browser = ${this.compileExpressStatic('/browser')};
application.use('*', (request, ...args) => {
const userAgent = request.headers['user-agent'] || 'unknown';
const isElectron = /electron/ig.test(userAgent);
request.url = request.baseUrl || request.url;
return (isElectron ?
electron : browser)(request, ...args);
});
` : `
application.use(${this.compileExpressStatic()});
`;
}

protected compileServer(backendModules: Map<string, string>): string {
return `// @ts-check
return `\
// @ts-check
require('reflect-metadata');
const path = require('path');
const express = require('express');
Expand Down Expand Up @@ -54,9 +78,7 @@ function start(port, host) {
const cliManager = container.get(CliManager);
return cliManager.initializeCli().then(function () {
const application = container.get(BackendApplication);
application.use(express.static(path.join(__dirname, '../../lib'), {
index: 'index.html'
}));
${this.compileMiddleware(backendModules)}
return application.start(port, host);
});
}
Expand All @@ -72,7 +94,8 @@ module.exports = (port, host) => Promise.resolve()${this.compileBackendModuleImp
}

protected compileMain(backendModules: Map<string, string>): string {
return `// @ts-check
return `\
// @ts-check
const serverPath = require('path').resolve(__dirname, 'server');
const address = require('@theia/core/lib/node/cluster/main').default(serverPath);
address.then(function (address) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,25 @@ export class FrontendGenerator extends AbstractGenerator {

async generate(): Promise<void> {
const frontendModules = this.pck.targetFrontendModules;
await this.write(this.pck.frontend('index.html'), this.compileIndexHtml(frontendModules));
await this.write(this.pck.frontend('index.js'), this.compileIndexJs(frontendModules));

if (this.package.isHybrid()) {
await this.write(this.pck.frontend('browser', 'index.html'), this.compileIndexHtml(this.package.frontendModules));
await this.write(this.pck.frontend('electron', 'index.html'), this.compileIndexHtml(this.package.frontendElectronModules));
await this.write(this.pck.frontend('browser', 'index.js'), this.compileIndexJs(this.package.frontendModules));
await this.write(this.pck.frontend('electron', 'index.js'), this.compileIndexJs(this.package.frontendElectronModules));
} else {
await this.write(this.pck.frontend('index.html'), this.compileIndexHtml(frontendModules));
await this.write(this.pck.frontend('index.js'), this.compileIndexJs(frontendModules));
}

if (this.pck.isElectron()) {
await this.write(this.pck.frontend('electron-main.js'), this.compileElectronMain());
}
}

protected compileIndexHtml(frontendModules: Map<string, string>): string {
return `<!DOCTYPE html>
return `\
<!DOCTYPE html>
<html>
<head>${this.compileIndexHead(frontendModules)}
Expand All @@ -48,8 +58,10 @@ export class FrontendGenerator extends AbstractGenerator {
}

protected compileIndexJs(frontendModules: Map<string, string>): string {
return `// @ts-check
${this.ifBrowser("require('es6-promise/auto');")}
return `\
// @ts-check
${this.ifBrowser(`require('es6-promise/auto');
`)}\
require('reflect-metadata');
const { Container } = require('inversify');
const { FrontendApplication } = require('@theia/core/lib/browser');
Expand Down Expand Up @@ -90,8 +102,8 @@ module.exports = Promise.resolve()${this.compileFrontendModuleImports(frontendMo
}

protected compileElectronMain(): string {
return `// @ts-check
return `\
// @ts-check
// 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
Expand All @@ -105,18 +117,45 @@ const { join } = require('path');
const { isMaster } = require('cluster');
const { fork } = require('child_process');
const { app, BrowserWindow, ipcMain } = require('electron');
const EventEmitter = require('events');
const fileSchemeTester = /^file:/;
const localUriEvent = new EventEmitter();
let localUri = undefined;
const windows = [];
function setLocalUri(uri) {
localUriEvent.emit('update', localUri = uri);
}
function resolveLocalUriFromPort(port) {
setLocalUri('file://' + join(__dirname, '../../lib/index.html') + '?port=' + port);
}
function createNewWindow(theUrl) {
const newWindow = new BrowserWindow({ width: 1024, height: 728, show: !!theUrl });
const config = {
width: 1024,
height: 728,
show: !!theUrl
};
// Converts 'localhost' to the running local backend endpoint
if (localUri && theUrl === 'localhost') {
theUrl = localUri;
}
if (!!theUrl && !fileSchemeTester.test(theUrl)) {
config.webPreferences = {
// nodeIntegration: false,
// contextIsolation: true,
};
};
const newWindow = new BrowserWindow(config);
if (windows.length === 0) {
newWindow.webContents.on('new-window', (event, url, frameName, disposition, options) => {
// If the first electron window isn't visible, then all other new windows will remain invisible.
// https://github.com/electron/electron/issues/3751
options.show = true;
options.width = 1024;
options.height = 728;
Object.assign(options, config);
});
}
windows.push(newWindow);
Expand Down Expand Up @@ -148,25 +187,29 @@ if (isMaster) {
});
app.on('ready', () => {
// Check whether we are in bundled application or development mode.
const devMode = process.defaultApp || /node_modules[\/]electron[\/]/.test(process.execPath);
const devMode = process.defaultApp || /node_modules[\\/]electron[\\/]/.test(process.execPath);
const mainWindow = createNewWindow();
const loadMainWindow = (port) => {
mainWindow.loadURL('file://' + join(__dirname, '../../lib/index.html') + '?port=' + port);
const loadMainWindow = (uri) => {
// mainWindow.loadURL(\`http://localhost:\${port}\`);
mainWindow.loadURL(uri);
};
localUriEvent.once('update', loadMainWindow);
const mainPath = join(__dirname, '..', 'backend', 'main');
// We need to distinguish between bundled application and development mode when starting the clusters.
// See: https://github.com/electron/electron/issues/6337#issuecomment-230183287
if (devMode) {
require(mainPath).then(address => {
loadMainWindow(address.port);
resolveLocalUriFromPort(address.port)
}).catch((error) => {
console.error(error);
app.exit(1);
});
} else {
const cp = fork(mainPath);
cp.on('message', (message) => {
loadMainWindow(message);
resolveLocalUriFromPort(message);
});
cp.on('error', (error) => {
console.error(error);
Expand Down
51 changes: 37 additions & 14 deletions dev-packages/application-manager/src/generator/webpack-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@

import * as paths from 'path';
import { AbstractGenerator } from './abstract-generator';
import { ApplicationProps } from '@theia/application-package';

export class WebpackGenerator extends AbstractGenerator {

async generate(): Promise<void> {
await this.write(this.configPath, this.compileWebpackConfig());
await this.write(this.configPath, this.compileWebpackConfigFile());
}

get configPath(): string {
Expand All @@ -31,7 +32,18 @@ export class WebpackGenerator extends AbstractGenerator {
return this.pck.resolveModulePath(moduleName, path).split(paths.sep).join('/');
}

protected compileWebpackConfig(): string {
protected compileExports(): string {
const exports = [];
if (this.package.isHybrid() || this.package.isBrowser()) {
exports.push(this.normalized('browser'));
}
if (this.package.isHybrid() || this.package.isElectron()) {
exports.push(this.normalized('electron'));
}
return `[${exports.join(', ')}]`;
}

protected compileWebpackConfigFile(): string {
return `// @ts-check
const path = require('path');
const webpack = require('webpack');
Expand All @@ -45,27 +57,38 @@ const { mode } = yargs.option('mode', {
choices: ["development", "production"],
default: "production"
}).argv;
const development = mode === 'development';${this.ifMonaco(() => `
const development = mode === 'development';
${this.ifMonaco(() => `
const monacoEditorCorePath = development ? '${this.resolve('@typefox/monaco-editor-core', 'dev/vs')}' : '${this.resolve('@typefox/monaco-editor-core', 'min/vs')}';
const monacoCssLanguagePath = '${this.resolve('monaco-css', 'release/min')}';
const monacoHtmlLanguagePath = '${this.resolve('monaco-html', 'release/min')}';`)}
const monacoHtmlLanguagePath = '${this.resolve('monaco-html', 'release/min')}';
`)}
${this.package.isHybrid() || this.package.isBrowser() ? this.compileWebpackConfigObjectFor('browser') : ''}\
${this.package.isHybrid() || this.package.isElectron() ? this.compileWebpackConfigObjectFor('electron') : ''}\
module.exports = {
entry: path.resolve(__dirname, 'src-gen/frontend/index.js'),
module.exports = ${this.compileExports()};
`;
}

protected compileWebpackConfigObjectFor(target: ApplicationProps.Target): string {
const normalizedTarget = this.normalized(target);
return `\
const ${normalizedTarget} = {
entry: path.resolve(__dirname, 'src-gen/frontend/${this.ifHybrid(normalizedTarget + '/')}index.js'),
output: {
filename: 'bundle.js',
path: outputPath
path: path.resolve(outputPath, '${this.ifHybrid(normalizedTarget)}')
},
target: '${this.ifBrowser('web', 'electron-renderer')}',
target: '${target === 'browser' ? 'web' : 'electron-renderer'}',
mode,
node: {${this.ifElectron(`
node: {${target === 'electron' ? `
__dirname: false,
__filename: false`, `
__filename: false`
: /* else */ `
fs: 'empty',
child_process: 'empty',
net: 'empty',
crypto: 'empty'`)}
crypto: 'empty'` }
},
module: {
rules: [
Expand Down Expand Up @@ -153,7 +176,7 @@ module.exports = {
stats: {
warnings: true
}
};`;
};
`;
}

}
7 changes: 5 additions & 2 deletions dev-packages/application-manager/src/rebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { ApplicationProps } from '@theia/application-package/lib/';
import fs = require('fs-extra');
import path = require('path');
import cp = require('child_process');

export function rebuild(target: 'electron' | 'browser', modules: string[]) {
export function rebuild(target: ApplicationProps.Target, modules: string[]) {
const nodeModulesPath = path.join(process.cwd(), 'node_modules');
const browserModulesPath = path.join(process.cwd(), '.browser_modules');
const modulesToProcess = modules || ['node-pty', 'vscode-nsfw', 'find-git-repositories'];
Expand Down Expand Up @@ -54,7 +55,8 @@ export function rebuild(target: 'electron' | 'browser', modules: string[]) {
fs.writeFile(packFile, packageText);
}, 100);
}
} else if (target === 'browser' && fs.existsSync(browserModulesPath)) {

} else if (/^(browser|hybrid)$/.test(target) && fs.existsSync(browserModulesPath)) {
for (const moduleName of fs.readdirSync(browserModulesPath)) {
console.log('Reverting ' + moduleName);
const src = path.join(browserModulesPath, moduleName);
Expand All @@ -63,6 +65,7 @@ export function rebuild(target: 'electron' | 'browser', modules: string[]) {
fs.copySync(src, dest);
}
fs.removeSync(browserModulesPath);

} else {
console.log('native node modules are already rebuilt for ' + target);
}
Expand Down
Loading

0 comments on commit a9506fc

Please sign in to comment.