Skip to content

Commit

Permalink
feat(plugin-webpack): capture logs into web ui, handle preload scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
MarshallOfSound committed May 7, 2018
1 parent 8ffab0b commit e800049
Show file tree
Hide file tree
Showing 14 changed files with 617 additions and 85 deletions.
3 changes: 3 additions & 0 deletions package.json
Expand Up @@ -51,6 +51,7 @@
"electron-packager": "^12.0.1",
"electron-rebuild": "^1.6.0",
"express": "^4.16.2",
"express-ws": "^3.0.0",
"form-data": "^2.1.4",
"fs-extra": "^5.0.0",
"glob": "^7.1.1",
Expand Down Expand Up @@ -78,6 +79,7 @@
"webpack-dev-middleware": "^2.0.5",
"webpack-hot-middleware": "^2.21.0",
"webpack-merge": "^4.1.1",
"xterm": "^3.3.0",
"yarn-or-npm": "^2.0.2"
},
"devDependencies": {
Expand All @@ -95,6 +97,7 @@
"@types/electron-packager": "^10.1.0",
"@types/electron-winstaller": "^2.6.1",
"@types/express": "^4.11.1",
"@types/express-ws": "^3.0.0",
"@types/fetch-mock": "^6.0.1",
"@types/form-data": "^2.2.1",
"@types/fs-extra": "^5.0.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/api/core/package.json
Expand Up @@ -66,4 +66,4 @@
"engines": {
"node": ">= 6.0"
}
}
}
2 changes: 2 additions & 0 deletions packages/api/core/src/api/start.ts
Expand Up @@ -65,6 +65,7 @@ export default async ({
if (typeof spawnedPluginChild === 'string') {
electronExecPath = spawnedPluginChild;
} else if (spawnedPluginChild) {
await runHook(forgeConfig, 'postStart', spawnedPluginChild);
return spawnedPluginChild;
}

Expand Down Expand Up @@ -93,5 +94,6 @@ export default async ({
spawned = spawn(process.execPath, [electronExecPath, appPath].concat(args as string[]), spawnOpts);
});

await runHook(forgeConfig, 'postStart', spawned);
return spawned;
};
9 changes: 4 additions & 5 deletions packages/plugin/webpack/package.json
Expand Up @@ -10,17 +10,16 @@
"scripts": {
"test": "exit 0"
},
"devDependencies": {
"chai": "^4.0.0",
"mocha": "^5.0.0"
},
"devDependencies": {},
"engines": {
"node": ">= 6.0"
},
"dependencies": {
"@electron-forge/async-ora": "6.0.0-beta.10",
"@electron-forge/plugin-base": "6.0.0-beta.10",
"@electron-forge/web-multi-logger": "^6.0.0-beta.10",
"cross-spawn-promise": "^0.10.1",
"debug": "^3.0.0",
"express": "^4.16.2",
"fs-extra": "^5.0.0",
"global": "^4.3.2",
Expand All @@ -30,4 +29,4 @@
"webpack-hot-middleware": "^2.21.0",
"webpack-merge": "^4.1.1"
}
}
}
9 changes: 7 additions & 2 deletions packages/plugin/webpack/src/Config.ts
Expand Up @@ -4,13 +4,18 @@ export interface WebpackPluginEntryPoint {
html: string;
js: string;
name: string;
prefixedEntries?: string[];
preload?: WebpackPreloadEntryPoint;
}

export interface WebpackPreloadEntryPoint {
js: string;
prefixedEntries?: string[];
}

export interface WebpackPluginRendererConfig {
config: WebpackConfiguration | string;

prefixedEntries?: string[];

entryPoints: WebpackPluginEntryPoint[];
}

Expand Down
163 changes: 150 additions & 13 deletions packages/plugin/webpack/src/WebpackPlugin.ts
@@ -1,5 +1,9 @@
import { asyncOra } from '@electron-forge/async-ora';
import PluginBase from '@electron-forge/plugin-base';
import Logger from '@electron-forge/web-multi-logger';
import Tab from '@electron-forge/web-multi-logger/dist/Tab';
import { ChildProcess } from 'child_process';
import debug from 'debug';
import fs from 'fs-extra';
import merge from 'webpack-merge';
import path from 'path';
Expand All @@ -8,17 +12,23 @@ import webpack, { Configuration } from 'webpack';
import webpackHotMiddleware from 'webpack-hot-middleware';
import webpackDevMiddleware from 'webpack-dev-middleware';
import express from 'express';
import http from 'http';

import HtmlWebpackPlugin, { Config } from 'html-webpack-plugin';

import { WebpackPluginConfig, WebpackPluginEntryPoint } from './Config';
import once from './util/once';
import { WebpackPluginConfig, WebpackPluginEntryPoint, WebpackPreloadEntryPoint } from './Config';

const d = debug('electron-forge:plugin:webpack');
const BASE_PORT = 3000;

export default class WebpackPlugin extends PluginBase<WebpackPluginConfig> {
name = 'webpack';
private isProd = false;
private baseDir!: string;
private watchers: webpack.Compiler.Watching[] = [];
private servers: http.Server[] = [];
private loggers: Logger[] = [];

constructor(c: WebpackPluginConfig) {
super(c);
Expand All @@ -32,8 +42,35 @@ export default class WebpackPlugin extends PluginBase<WebpackPluginConfig> {
return config;
}

private exitHandler = (options: { cleanup?: boolean; exit?: boolean }, err?: Error) => {
d('handling process exit with:', options);
if (options.cleanup) {
for (const watcher of this.watchers) {
d('cleaning webpack watcher');
watcher.close(() => {});
}
this.watchers = [];
for (const server of this.servers) {
d('cleaning http server');
server.close();
}
this.servers = [];
for (const logger of this.loggers) {
d('stopping logger');
logger.stop();
}
this.loggers = [];
}
if (err) console.error(err.stack);
if (options.exit) process.exit();
}

init = (dir: string) => {
this.baseDir = path.resolve(dir, '.webpack');

d('hooking process events');
process.on('exit', this.exitHandler.bind(this, { cleanup: true }));
process.on('SIGINT', this.exitHandler.bind(this, { exit: true }));
}

getHook(name: string) {
Expand All @@ -44,6 +81,13 @@ export default class WebpackPlugin extends PluginBase<WebpackPluginConfig> {
await this.compileMain();
await this.compileRenderers();
};
case 'postStart':
return async (_: any, child: ChildProcess) => {
console.log(child);
console.log(Object.keys(child));
d('hooking electron process exit');
child.on('exit', () => this.exitHandler({ cleanup: true, exit: true }));
};
}
return null;
}
Expand All @@ -57,11 +101,21 @@ export default class WebpackPlugin extends PluginBase<WebpackPluginConfig> {

const defines: { [key: string]: string; } = {};
let index = 0;
if (!this.config.renderer.entryPoints || !Array.isArray(this.config.renderer.entryPoints)) {
throw new Error('Required config option "renderer.entryPoints" has not been defined');
}
for (const entryPoint of this.config.renderer.entryPoints) {
defines[`${entryPoint.name.toUpperCase().replace(/ /g, '_')}_WEBPACK_ENTRY`] =
this.isProd
? `\`file://\$\{require('path').resolve(__dirname, '../renderer', '${entryPoint.name}', 'index.html')\}\``
: `'http://localhost:${BASE_PORT + index}'`;

if (entryPoint.preload) {
defines[`${entryPoint.name.toUpperCase().replace(/ /g, '_')}_PRELOAD_WEBPACK_ENTRY`] =
this.isProd
? `\`file://\$\{require('path').resolve(__dirname, '../renderer', '${entryPoint.name}', 'preload.js')\}\``
: `'${path.resolve(this.baseDir, 'renderer', entryPoint.name, 'preload.js')}'`;
}
index += 1;
}
return merge.smart({
Expand All @@ -79,12 +133,40 @@ export default class WebpackPlugin extends PluginBase<WebpackPluginConfig> {
__dirname: false,
__filename: false,
},
resolve: {
modules: [
path.resolve(path.dirname(this.baseDir), './'),
path.resolve(path.dirname(this.baseDir), 'node_modules'),
path.resolve(__dirname, '..', 'node_modules'),
],
},
}, mainConfig || {});
}

getPreloadRendererConfig = async (parentPoint: WebpackPluginEntryPoint, entryPoint: WebpackPreloadEntryPoint) => {
const rendererConfig = this.resolveConfig(this.config.renderer.config);
const prefixedEntries = entryPoint.prefixedEntries || [];

return merge.smart({
devtool: 'inline-source-map',
target: 'electron-renderer',
entry: prefixedEntries.concat([
entryPoint.js,
]),
output: {
path: path.resolve(this.baseDir, 'renderer', parentPoint.name),
filename: 'preload.js',
},
node: {
__dirname: false,
__filename: false,
},
}, rendererConfig);
}

getRendererConfig = async (entryPoint: WebpackPluginEntryPoint) => {
const rendererConfig = this.resolveConfig(this.config.renderer.config);
const prefixedEntries = this.config.renderer.prefixedEntries || [];
const prefixedEntries = entryPoint.prefixedEntries || [];
return merge.smart({
devtool: 'inline-source-map',
target: 'electron-renderer',
Expand All @@ -108,18 +190,35 @@ export default class WebpackPlugin extends PluginBase<WebpackPluginConfig> {
}, rendererConfig);
}

compileMain = async () => {
compileMain = async (watch = false, logger?: Logger) => {
let tab: Tab;
if (logger) {
tab = logger.createTab('Main Process');
}
await asyncOra('Compiling Main Process Code', async () => {
await new Promise(async (resolve, reject) => {
webpack(await this.getMainConfig()).run((err, stats) => {
if (err) return reject(err);
resolve();
});
const compiler = webpack(await this.getMainConfig());
const [onceResolve, onceReject] = once(resolve, reject);
const cb: webpack.ICompiler.Handler = (err, stats) => {
if (tab) {
tab.log(stats.toString({
colors: true,
}));
}

if (err) return onceReject(err);
onceResolve();
};
if (watch) {
this.watchers.push(compiler.watch({}, cb));
} else {
compiler.run(cb);
}
});
});
}

compileRenderers = async () => {
compileRenderers = async (watch = false) => {
for (const entryPoint of this.config.renderer.entryPoints) {
await asyncOra(`Compiling Renderer Template: ${entryPoint.name}`, async () => {
await new Promise(async (resolve, reject) => {
Expand All @@ -128,34 +227,72 @@ export default class WebpackPlugin extends PluginBase<WebpackPluginConfig> {
resolve();
});
});
if (entryPoint.preload) {
await new Promise(async (resolve, reject) => {
webpack(await this.getPreloadRendererConfig(entryPoint, entryPoint.preload!)).run((err, stats) => {
if (err) return reject(err);
resolve();
});
});
}
});
}
}

launchDevServers = async () => {
launchDevServers = async (logger: Logger) => {
await asyncOra('Launch Dev Servers', async () => {
let index = 0;
for (const entryPoint of this.config.renderer.entryPoints) {
const tab = logger.createTab(entryPoint.name);

const config = await this.getRendererConfig(entryPoint);
const compiler = webpack(config);
const server = webpackDevMiddleware(compiler, {
logLevel: 'silent',
logger: {
log: tab.log.bind(tab),
info: tab.log.bind(tab),
error: tab.log.bind(tab),
warn: tab.log.bind(tab),
},
publicPath: '/',
hot: true,
historyApiFallback: true,
} as any);
const app = express();
app.use(server);
app.use(webpackHotMiddleware(compiler));
app.listen(BASE_PORT + index);
this.servers.push(app.listen(BASE_PORT + index));
index += 1;
}
});

await asyncOra('Compile Preload Scripts', async () => {
for (const entryPoint of this.config.renderer.entryPoints) {
if (entryPoint.preload) {
await new Promise(async (resolve, reject) => {
const tab = logger.createTab(`${entryPoint.name} - Preload`);
const [onceResolve, onceReject] = once(resolve, reject);
const cb: webpack.ICompiler.Handler = (err, stats) => {
tab.log(stats.toString({
colors: true,
}));

if (err) return onceReject(err);
onceResolve();
};
this.watchers.push(webpack(await this.getPreloadRendererConfig(entryPoint, entryPoint.preload!)).watch({}, cb));
});
}
}
});
}

async startLogic(): Promise<false> {
await this.compileMain();
await this.launchDevServers();
const logger = new Logger();
this.loggers.push(logger);
await this.compileMain(true, logger);
await this.launchDevServers(logger);
await logger.start();
return false;
}
}
17 changes: 17 additions & 0 deletions packages/plugin/webpack/src/util/once.ts
@@ -0,0 +1,17 @@
export default <A, B>(fn1: A, fn2: B): [A, B] => {
let once = true;
let val: any;
const make = <T>(fn: T): T => {
return ((...args: any[]) => {
if (once) {
val = (fn as any)(...args);
once = false;
}
return val;
}) as any as T;
};
return [
make(fn1),
make(fn2),
];
};

0 comments on commit e800049

Please sign in to comment.