Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New: Use woker-farm to handle webpack multi compile #570

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
51 changes: 34 additions & 17 deletions lib/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,58 @@

const _ = require('lodash');
const BbPromise = require('bluebird');
const webpack = require('webpack');
const tty = require('tty');

const { compiler } = require('./compiler');
const { multiCompiler } = require('./multiCompiler/compiler');

module.exports = {
compile() {
this.serverless.cli.log('Bundling with Webpack...');
const consoleStats = this.webpackConfig.stats ||
_.get(this, 'webpackConfig[0].stats') || {
colors: tty.isatty(process.stdout.fd),
hash: false,
version: false,
chunks: false,
children: false
};

const configOptions = {
servicePath: this.serverless.config.servicePath,
out: this.options.out
};

const compileOptions = {
webpackConfigFilePath: this.webpackConfigFilePath,
webpackConfig: this.webpackConfig,

entryFunctions: this.entryFunctions,

configOptions,
consoleStats
};

const compiler = webpack(this.webpackConfig);
if (this.multiCompile) {
this.options.verbose && this.serverless.cli.log('Using multi-thread function compiler');
}

return BbPromise.fromCallback(cb => compiler.run(cb)).then(stats => {
if (!this.multiCompile) {
stats = { stats: [stats] };
}
const webpackCompiler = this.multiCompile ? multiCompiler(compileOptions) : compiler(compileOptions);

return webpackCompiler.then(stats => {
const compileOutputPaths = [];
const consoleStats = this.webpackConfig.stats ||
_.get(this, 'webpackConfig[0].stats') || {
colors: tty.isatty(process.stdout.fd),
hash: false,
version: false,
chunks: false,
children: false
};

_.forEach(stats.stats, compileStats => {
const statsOutput = compileStats.toString(consoleStats);
const statsOutput = compileStats.cliOutput;
if (statsOutput) {
this.serverless.cli.consoleLog(statsOutput);
}

if (compileStats.compilation.errors.length) {
if (compileStats.errors.length) {
throw new Error('Webpack compilation error, see above');
}

compileOutputPaths.push(compileStats.compilation.compiler.outputPath);
compileOutputPaths.push(compileStats.outputPath);
});

this.compileOutputPaths = compileOutputPaths;
Expand Down
27 changes: 27 additions & 0 deletions lib/compiler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use strict';

const BbPromise = require('bluebird');
const webpack = require('webpack');

const { processWebpackStats } = require('./processWebpackStats');
const { setOptionsOnConfig } = require('./processConfig');

module.exports = {
compiler(options) {
const webpackConfig = options.webpackConfig;
const configOptions = options.configOptions;
const consoleStats = options.consoleStats;

const config = setOptionsOnConfig(webpackConfig, configOptions);

const compiler = webpack(config);

return BbPromise.fromCallback(cb => compiler.run(cb)).then(stats => {
const result = processWebpackStats(stats, consoleStats);

return {
stats: [result]
};
});
}
};
45 changes: 45 additions & 0 deletions lib/multiCompiler/compiler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use strict';

const _ = require('lodash');
const BbPromise = require('bluebird');
const workerFarm = require('worker-farm');

module.exports = {
multiCompiler(options) {
const workerOptions = {
maxCallsPerWorker: 1
};
const workerThreadSourcePath = require.resolve('./workerThread');
const methods = ['runWebpack'];

const workers = workerFarm(workerOptions, workerThreadSourcePath, methods);

const configOptions = options.configOptions;

const threads = _.map(options.entryFunctions, entryFunc => {
const webpackConfigFilePath = options.webpackConfigFilePath;
const workerOptions = {
webpackConfigFilePath,
configOptions: {
...configOptions,
entryFunc
},
consoleStats: options.consoleStats
};

if (!webpackConfigFilePath) {
workerOptions.webpackConfig = options.webpackConfig;
}

return BbPromise.fromCallback(cb => {
workers.runWebpack(workerOptions, cb);
});
});

return Promise.all(threads).then(stats => {
return {
stats
};
});
}
};
43 changes: 43 additions & 0 deletions lib/multiCompiler/workerThread.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict';

const webpack = require('webpack');
const { setOptionsOnConfig } = require('../processConfig');
const { processWebpackStats } = require('../processWebpackStats');

module.exports = {
runWebpack(options, callback) {
const webpackConfigFilePath = options.webpackConfigFilePath;
const configOptions = options.configOptions;
const consoleStats = options.consoleStats;

let webpackConfig = null;
try {
if (webpackConfigFilePath) {
webpackConfig = require(webpackConfigFilePath);
} else if (options.webpackConfig) {
webpackConfig = options.webpackConfig;
} else {
throw new Error('Missing config');
}
} catch (error) {
callback(new Error('Failed to load config'));
return;
}

const entryConfig = setOptionsOnConfig(webpackConfig, configOptions);

const compiler = webpack(entryConfig);

compiler.run((error, stats) => {
if (error) {
callback(new Error('Failed to compile'));

return;
}

const result = processWebpackStats(stats, consoleStats);

callback(null, result);
});
}
};
59 changes: 3 additions & 56 deletions lib/packExternalModules.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const BbPromise = require('bluebird');
const _ = require('lodash');
const path = require('path');
const fse = require('fs-extra');
const isBuiltinModule = require('is-builtin-module');

const Packagers = require('./packagers');

Expand Down Expand Up @@ -143,58 +142,6 @@ function getProdModules(externalModules, packagePath, dependencyGraph, forceExcl
return prodModules;
}

function getExternalModuleName(module) {
const path = /^external "(.*)"$/.exec(module.identifier())[1];
const pathComponents = path.split('/');
const main = pathComponents[0];

// this is a package within a namespace
if (main.charAt(0) == '@') {
return `${main}/${pathComponents[1]}`;
}

return main;
}

function isExternalModule(module) {
return _.startsWith(module.identifier(), 'external ') && !isBuiltinModule(getExternalModuleName(module));
}

/**
* Find the original module that required the transient dependency. Returns
* undefined if the module is a first level dependency.
* @param {Object} issuer - Module issuer
*/
function findExternalOrigin(issuer) {
if (!_.isNil(issuer) && _.startsWith(issuer.rawRequest, './')) {
return findExternalOrigin(issuer.issuer);
}
return issuer;
}

function getExternalModules(stats) {
if (!stats.compilation.chunks) {
return [];
}
const externals = new Set();
for (const chunk of stats.compilation.chunks) {
if (!chunk.modulesIterable) {
continue;
}

// Explore each module within the chunk (built inputs):
for (const module of chunk.modulesIterable) {
if (isExternalModule(module)) {
externals.add({
origin: _.get(findExternalOrigin(module.issuer), 'rawRequest'),
external: getExternalModuleName(module)
});
}
}
}
return Array.from(externals);
}

module.exports = {
/**
* We need a performant algorithm to install the packages for each single
Expand Down Expand Up @@ -259,7 +206,7 @@ module.exports = {
const compositeModules = _.uniq(
_.flatMap(stats.stats, compileStats => {
const externalModules = _.concat(
getExternalModules.call(this, compileStats),
compileStats.externalModules,
_.map(packageForceIncludes, whitelistedPackage => ({
external: whitelistedPackage
}))
Expand Down Expand Up @@ -328,7 +275,7 @@ module.exports = {
.return(stats.stats);
})
.mapSeries(compileStats => {
const modulePath = compileStats.compilation.compiler.outputPath;
const modulePath = compileStats.outputPath;

// Create package.json
const modulePackageJson = path.join(modulePath, 'package.json');
Expand All @@ -346,7 +293,7 @@ module.exports = {
const prodModules = getProdModules.call(
this,
_.concat(
getExternalModules.call(this, compileStats),
compileStats.externalModules,
_.map(packageForceIncludes, whitelistedPackage => ({
external: whitelistedPackage
}))
Expand Down
2 changes: 1 addition & 1 deletion lib/packageModules.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ module.exports = {
return BbPromise.mapSeries(stats.stats, (compileStats, index) => {
const entryFunction = _.get(this.entryFunctions, index, {});
const filename = `${entryFunction.funcName || this.serverless.service.getServiceObject().name}.zip`;
const modulePath = compileStats.compilation.compiler.outputPath;
const modulePath = compileStats.outputPath;

const startZip = _.now();
return zip
Expand Down
47 changes: 47 additions & 0 deletions lib/processConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use strict';

const _ = require('lodash');
const path = require('path');

module.exports = {
setOptionsOnConfig(webpackConfig, options) {
// Default context
if (!webpackConfig.context) {
webpackConfig.context = options.servicePath;
}

// Default target
if (!webpackConfig.target) {
webpackConfig.target = 'node';
}

// Default output
if (!webpackConfig.output || _.isEmpty(webpackConfig.output)) {
const outputPath = path.join(options.servicePath, '.webpack');
webpackConfig.output = {
libraryTarget: 'commonjs',
path: outputPath,
filename: '[name].js'
};
}

// Custom output path
if (options.out) {
webpackConfig.output.path = path.join(options.servicePath, options.out);
}

// In case of individual packaging we have to create a separate config for each function
if (options.entryFunc) {
const entryFunc = options.entryFunc;
webpackConfig.entry = {
[entryFunc.entry.key]: entryFunc.entry.value
};
const compileName = entryFunc.funcName || _.camelCase(entryFunc.entry.key);
webpackConfig.output.path = path.join(webpackConfig.output.path, compileName);
} else {
webpackConfig.output.path = path.join(webpackConfig.output.path, 'service');
}

return webpackConfig;
}
};