Files
serverless-webpack/lib/packExternalModules.js
T
Frank Schmid b379de95f8 Utils unit tests
Enhance error message

Removed deprecated option from README

Convert all outstanding changes (yarn and package)

Adjusted NPM unit tests

Use spawn in any npm commands

Use spawn instead of exec

Fix unit test description

Do not swallow errors in case npm ls fails without a proper stdout
2018-04-25 18:05:37 +02:00

337 lines
14 KiB
JavaScript

'use strict';
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');
function rebaseFileReferences(pathToPackageRoot, moduleVersion) {
if (/^file:[^/]{2}/.test(moduleVersion)) {
const filePath = _.replace(moduleVersion, /^file:/, '');
return _.replace(`file:${pathToPackageRoot}/${filePath}`, /\\/g, '/');
}
return moduleVersion;
}
/**
* Add the given modules to a package json's dependencies.
*/
function addModulesToPackageJson(externalModules, packageJson, pathToPackageRoot) {
_.forEach(externalModules, externalModule => {
const splitModule = _.split(externalModule, '@');
// If we have a scoped module we have to re-add the @
if (_.startsWith(externalModule, '@')) {
splitModule.splice(0, 1);
splitModule[0] = '@' + splitModule[0];
}
let moduleVersion = _.join(_.tail(splitModule), '@');
// We have to rebase file references to the target package.json
moduleVersion = rebaseFileReferences(pathToPackageRoot, moduleVersion);
packageJson.dependencies = packageJson.dependencies || {};
packageJson.dependencies[_.first(splitModule)] = moduleVersion;
});
}
/**
* Remove a given list of excluded modules from a module list
* @this - The active plugin instance
*/
function removeExcludedModules(modules, packageForceExcludes, log) {
const excludedModules = _.remove(modules, externalModule => { // eslint-disable-line lodash/prefer-immutable-method
const splitModule = _.split(externalModule, '@');
// If we have a scoped module we have to re-add the @
if (_.startsWith(externalModule, '@')) {
splitModule.splice(0, 1);
splitModule[0] = '@' + splitModule[0];
}
const moduleName = _.first(splitModule);
return _.includes(packageForceExcludes, moduleName);
});
if (log && !_.isEmpty(excludedModules)) {
this.serverless.cli.log(`Excluding external modules: ${_.join(excludedModules, ', ')}`);
}
}
/**
* Resolve the needed versions of production depenencies for external modules.
* @this - The active plugin instance
*/
function getProdModules(externalModules, packagePath, dependencyGraph) {
const packageJsonPath = path.join(process.cwd(), packagePath);
const packageJson = require(packageJsonPath);
const prodModules = [];
// only process the module stated in dependencies section
if (!packageJson.dependencies) {
return [];
}
// Get versions of all transient modules
_.forEach(externalModules, module => {
let moduleVersion = packageJson.dependencies[module.external];
if (moduleVersion) {
prodModules.push(`${module.external}@${moduleVersion}`);
// Check if the module has any peer dependencies and include them too
try {
const modulePackagePath = path.join(
path.dirname(path.join(process.cwd(), packagePath)),
'node_modules',
module.external,
'package.json'
);
const peerDependencies = require(modulePackagePath).peerDependencies;
if (!_.isEmpty(peerDependencies)) {
this.options.verbose && this.serverless.cli.log(`Adding explicit peers for dependency ${module.external}`);
const peerModules = getProdModules.call(this, _.map(peerDependencies, (value, key) => ({ external: key })), packagePath, dependencyGraph);
Array.prototype.push.apply(prodModules, peerModules);
}
} catch (e) {
this.serverless.cli.log(`WARNING: Could not check for peer dependencies of ${module.external}`);
}
} else if (!packageJson.devDependencies || !packageJson.devDependencies[module.external]) {
// Add transient dependencies if they appear not in the service's dev dependencies
const originInfo = _.get(dependencyGraph, 'dependencies', {})[module.origin] || {};
moduleVersion = _.get(_.get(originInfo, 'dependencies', {})[module.external], 'version');
if (!moduleVersion) {
this.serverless.cli.log(`WARNING: Could not determine version of module ${module.external}`);
}
prodModules.push(moduleVersion ? `${module.external}@${moduleVersion}` : module.external);
}
});
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) {
const externals = new Set();
_.forEach(stats.compilation.chunks, chunk => {
// Explore each module within the chunk (built inputs):
chunk.forEachModule(module => {
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
* function (in case we package individually).
* (1) We fetch ALL packages needed by ALL functions in a first step
* and use this as a base npm checkout. The checkout will be done to a
* separate temporary directory with a package.json that contains everything.
* (2) For each single compile we copy the whole node_modules to the compile
* directory and create a (function) compile specific package.json and store
* it in the compile directory. Now we start npm again there, and npm will just
* remove the superfluous packages and optimize the remaining dependencies.
* This will utilize the npm cache at its best and give us the needed results
* and performance.
*/
packExternalModules() {
const stats = this.compileStats;
const includes = this.configuration.includeModules;
if (!includes) {
return BbPromise.resolve();
}
// Read plugin configuration
const packageForceIncludes = _.get(includes, 'forceInclude', []);
const packageForceExcludes = _.get(includes, 'forceExclude', []);
const packagePath = includes.packagePath || './package.json';
const packageJsonPath = path.join(process.cwd(), packagePath);
const packageScripts = _.reduce(this.configuration.packagerOptions.scripts || [], (__, script, index) => {
__[`script${index}`] = script;
return __;
}, {});
// Determine and create packager
return BbPromise.try(() => Packagers.get.call(this, this.configuration.packager))
.then(packager => {
// Get first level dependency graph
this.options.verbose && this.serverless.cli.log(`Fetch dependency graph from ${packageJsonPath}`);
return packager.getProdDependencies(path.dirname(packageJsonPath), 1)
.then(dependencyGraph => {
const problems = _.get(dependencyGraph, 'problems', []);
if (this.options.verbose && !_.isEmpty(problems)) {
this.serverless.cli.log(`Ignoring ${_.size(problems)} NPM errors:`);
_.forEach(problems, problem => {
this.serverless.cli.log(`=> ${problem}`);
});
}
// (1) Generate dependency composition
const compositeModules = _.uniq(_.flatMap(stats.stats, compileStats => {
const externalModules = _.concat(
getExternalModules.call(this, compileStats),
_.map(packageForceIncludes, whitelistedPackage => ({ external: whitelistedPackage }))
);
return getProdModules.call(this, externalModules, packagePath, dependencyGraph);
}));
removeExcludedModules.call(this, compositeModules, packageForceExcludes, true);
if (_.isEmpty(compositeModules)) {
// The compiled code does not reference any external modules at all
this.serverless.cli.log('No external modules needed');
return BbPromise.resolve();
}
// (1.a) Install all needed modules
const compositeModulePath = path.join(this.webpackOutputPath, 'dependencies');
const compositePackageJson = path.join(compositeModulePath, 'package.json');
// (1.a.1) Create a package.json
const compositePackage = {
name: this.serverless.service.service,
version: '1.0.0',
description: `Packaged externals for ${this.serverless.service.service}`,
private: true,
scripts: packageScripts
};
const relPath = path.relative(compositeModulePath, path.dirname(packageJsonPath));
addModulesToPackageJson(compositeModules, compositePackage, relPath);
this.serverless.utils.writeFileSync(compositePackageJson, JSON.stringify(compositePackage, null, 2));
// (1.a.2) Copy package-lock.json if it exists, to prevent unwanted upgrades
const packageLockPath = path.join(path.dirname(packageJsonPath), packager.lockfileName);
let hasPackageLock = false;
return BbPromise.fromCallback(cb => fse.pathExists(packageLockPath, cb))
.then(exists => {
if (exists) {
this.serverless.cli.log('Package lock found - Using locked versions');
try {
let packageLockJson = this.serverless.utils.readFileSync(packageLockPath);
/**
* We should not be modifying 'package-lock.json'
* because this file should be treat as internal to npm.
*
* Rebase package-lock is a temporary workaround and must be
* removed as soon as https://github.com/npm/npm/issues/19183 gets fixed.
*/
packager.rebaseLockfile(relPath, packageLockJson);
if (_.isObject(packageLockJson)) {
packageLockJson = JSON.stringify(packageLockJson, null, 2);
}
this.serverless.utils.writeFileSync(path.join(compositeModulePath, packager.lockfileName), packageLockJson);
hasPackageLock = true;
} catch(err) {
this.serverless.cli.log(`Warning: Could not read lock file: ${err.message}`);
}
}
return BbPromise.resolve();
})
.then(() => {
const start = _.now();
this.serverless.cli.log('Packing external modules: ' + compositeModules.join(', '));
return packager.install(compositeModulePath, this.configuration.packagerOptions)
.then(() => this.options.verbose && this.serverless.cli.log(`Package took [${_.now() - start} ms]`))
.return(stats.stats);
})
.mapSeries(compileStats => {
const modulePath = compileStats.compilation.compiler.outputPath;
// Create package.json
const modulePackageJson = path.join(modulePath, 'package.json');
const modulePackage = {
name: this.serverless.service.service,
version: '1.0.0',
description: `Packaged externals for ${this.serverless.service.service}`,
private: true,
scripts: packageScripts,
dependencies: {}
};
const prodModules = getProdModules.call(this,
_.concat(
getExternalModules.call(this, compileStats),
_.map(packageForceIncludes, whitelistedPackage => ({ external: whitelistedPackage }))
), packagePath, dependencyGraph);
removeExcludedModules.call(this, prodModules, packageForceExcludes);
const relPath = path.relative(modulePath, path.dirname(packageJsonPath));
addModulesToPackageJson(prodModules, modulePackage, relPath);
this.serverless.utils.writeFileSync(modulePackageJson, JSON.stringify(modulePackage, null, 2));
// GOOGLE: Copy modules only if not google-cloud-functions
// GCF Auto installs the package json
if (_.get(this.serverless, 'service.provider.name') === 'google') {
return BbPromise.resolve();
}
const startCopy = _.now();
return BbPromise.try(() => {
// Only copy dependency modules if demanded by packager
if (packager.mustCopyModules) {
return BbPromise.fromCallback(callback => fse.copy(path.join(compositeModulePath, 'node_modules'), path.join(modulePath, 'node_modules'), callback));
}
return BbPromise.resolve();
})
.then(() => hasPackageLock ?
BbPromise.fromCallback(callback => fse.copy(path.join(compositeModulePath, packager.lockfileName), path.join(modulePath, packager.lockfileName), callback)) :
BbPromise.resolve()
)
.tap(() => this.options.verbose && this.serverless.cli.log(`Copy modules: ${modulePath} [${_.now() - startCopy} ms]`))
.then(() => {
// Prune extraneous packages - removes not needed ones
const startPrune = _.now();
return packager.prune(modulePath, this.configuration.packagerOptions)
.tap(() => this.options.verbose && this.serverless.cli.log(`Prune: ${modulePath} [${_.now() - startPrune} ms]`));
})
.then(() => {
// Prune extraneous packages - removes not needed ones
const startRunScripts = _.now();
return packager.runScripts(modulePath, _.keys(packageScripts))
.tap(() => this.options.verbose && this.serverless.cli.log(`Run scripts: ${modulePath} [${_.now() - startRunScripts} ms]`));
});
})
.return();
});
});
}
};