serverless-webpack
Version:
Serverless plugin to bundle your javascript with Webpack
521 lines (481 loc) • 21.7 kB
JavaScript
;
const BbPromise = require('bluebird');
const _ = require('lodash');
const path = require('path');
const fse = require('fs-extra');
const findWorkspaceRoot = require('find-yarn-workspace-root');
const Packagers = require('./packagers');
const { isProviderGoogle } = require('./utils');
function rebaseFileReferences(pathToPackageRoot, moduleVersion) {
if (/^(?:file:[^/]{2}|\.\/|\.\.\/)/.test(moduleVersion)) {
const filePath = _.replace(moduleVersion, /^file:/, '');
return _.replace(
`${_.startsWith(moduleVersion, 'file:') ? 'file:' : ''}${pathToPackageRoot}/${filePath}`,
/\\/g,
'/'
);
}
return moduleVersion;
}
/**
* Add the given modules to a package json's dependencies.
*/
function addModulesToPackageJson(externalModules, packageJson, pathToPackageRoot) {
_.forEach(externalModules.sort(), 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) {
// eslint-disable-next-line lodash/prefer-immutable-method
const excludedModules = _.remove(modules, 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];
}
const moduleName = _.first(splitModule);
return _.includes(packageForceExcludes, moduleName);
});
if (log && !_.isEmpty(excludedModules)) {
if (this.log) {
this.log(`Excluding external modules: ${_.join(excludedModules, ', ')}`);
} else {
this.serverless.cli.log(`Excluding external modules: ${_.join(excludedModules, ', ')}`);
}
}
}
/**
* Resolve the needed versions of production dependencies for external modules.
* @this - The active plugin instance
*/
function getProdModules(externalModules, packagePath, nodeModulesRelativeDir, dependencyGraph, forceExcludes) {
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}`);
let nodeModulesBase = path.join(path.dirname(path.join(process.cwd(), packagePath)), 'node_modules');
if (nodeModulesRelativeDir) {
const customNodeModulesDir = path.join(process.cwd(), nodeModulesRelativeDir, 'node_modules');
if (fse.pathExistsSync(customNodeModulesDir)) {
nodeModulesBase = customNodeModulesDir;
} else {
if (this.log) {
this.log.warning(`${customNodeModulesDir} dose not exist. Please check nodeModulesRelativeDir setting`);
} else {
this.serverless.cli.log(
`WARNING: ${customNodeModulesDir} dose not exist. Please check nodeModulesRelativeDir setting`
);
}
}
}
// Check if the module has any peer dependencies and include them too
try {
const modulePackagePath = path.join(nodeModulesBase, module.external, 'package.json');
const peerDependencies = require(modulePackagePath).peerDependencies;
if (!_.isEmpty(peerDependencies)) {
if (this.log) {
this.log.verbose(`Adding explicit peers for dependency ${module.external}`);
} else {
this.options.verbose && this.serverless.cli.log(`Adding explicit peers for dependency ${module.external}`);
}
const peerDependenciesMeta = require(modulePackagePath).peerDependenciesMeta;
if (!_.isEmpty(peerDependenciesMeta)) {
_.forEach(peerDependencies, (value, key) => {
if (peerDependenciesMeta[key] && peerDependenciesMeta[key].optional === true) {
if (this.log) {
this.log.verbose(
`Skipping peers dependency ${key} for dependency ${module.external} because it's optional`
);
} else {
this.options.verbose &&
this.serverless.cli.log(
`Skipping peers dependency ${key} for dependency ${module.external} because it's optional`
);
}
_.unset(peerDependencies, key);
}
});
}
if (!_.isEmpty(peerDependencies)) {
const peerModules = getProdModules.call(
this,
_.map(peerDependencies, (value, key) => ({ external: key })),
packagePath,
nodeModulesRelativeDir,
dependencyGraph,
forceExcludes
);
Array.prototype.push.apply(prodModules, peerModules);
}
}
} catch (e) {
if (this.log) {
this.log.warning(
`Could not check for peer dependencies of ${module.external}. Set nodeModulesRelativeDir if node_modules is in different directory.`
);
} else {
this.serverless.cli.log(
`WARNING: Could not check for peer dependencies of ${module.external}. Set nodeModulesRelativeDir if node_modules is in different directory.`
);
}
}
} 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) {
// eslint-disable-next-line lodash/path-style
moduleVersion = _.get(dependencyGraph, ['dependencies', module.external, 'version']);
}
if (!moduleVersion) {
if (this.log) {
this.log.warning(`Could not determine version of module ${module.external}`);
} else {
this.serverless.cli.log(`WARNING: Could not determine version of module ${module.external}`);
}
}
prodModules.push(moduleVersion ? `${module.external}@${moduleVersion}` : module.external);
} else if (
packageJson.devDependencies &&
packageJson.devDependencies[module.external] &&
!_.includes(forceExcludes, module.external)
) {
// To minimize the chance of breaking setups we whitelist packages available on AWS here. These are due to the previously missing check
// most likely set in devDependencies and should not lead to an error now.
const ignoredDevDependencies = ['aws-sdk'];
if (!_.includes(ignoredDevDependencies, module.external)) {
// Runtime dependency found in devDependencies but not forcefully excluded
if (this.log) {
this.log.error(
`Runtime dependency '${module.external}' found in devDependencies. Move it to dependencies or use forceExclude to explicitly exclude it.`
);
} else {
this.serverless.cli.log(
`ERROR: Runtime dependency '${module.external}' found in devDependencies. Move it to dependencies or use forceExclude to explicitly exclude it.`
);
}
throw new this.serverless.classes.Error(`Serverless-webpack dependency error: ${module.external}.`);
}
if (this.log) {
this.log.verbose(
`Runtime dependency '${module.external}' found in devDependencies. It has been excluded automatically.`
);
} else {
this.options.verbose &&
this.serverless.cli.log(
`INFO: Runtime dependency '${module.external}' found in devDependencies. It has been excluded automatically.`
);
}
}
}
});
return prodModules;
}
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() {
if (this.skipCompile) {
return BbPromise.resolve();
}
const stats = this.compileStats;
const includes = this.configuration.includeModules;
if (!includes) {
return BbPromise.resolve();
}
if (this.log) {
this.log.verbose('Packing external modules');
this.progress.get('webpack').notice('[Webpack] Packing external modules');
}
// Read plugin configuration
const packageForceIncludes = _.get(includes, 'forceInclude', []);
const packageForceExcludes = _.get(includes, 'forceExclude', []);
const packagePath = includes.packagePath || './package.json';
const nodeModulesRelativeDir = includes.nodeModulesRelativeDir;
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 => {
// Fetch needed original package.json sections
const sectionNames = packager.copyPackageSectionNames(this.configuration.packagerOptions);
const packageJson = this.serverless.utils.readFileSync(packageJsonPath);
const packageSections = _.pick(packageJson, sectionNames);
if (!_.isEmpty(packageSections)) {
if (this.log) {
this.log.verbose(`Using package.json sections ${_.join(_.keys(packageSections), ', ')}`);
} else {
this.options.verbose &&
this.serverless.cli.log(`Using package.json sections ${_.join(_.keys(packageSections), ', ')}`);
}
}
// Get first level dependency graph
if (this.log) {
this.log.verbose(`Fetch dependency graph from ${packageJsonPath}`);
} else {
this.options.verbose && this.serverless.cli.log(`Fetch dependency graph from ${packageJsonPath}`);
}
return packager
.getProdDependencies(path.dirname(packageJsonPath), 1, this.configuration.packagerOptions)
.then(dependencyGraph => {
const problems = _.get(dependencyGraph, 'problems', []);
if (this.options.verbose && !_.isEmpty(problems)) {
if (this.log) {
this.log.verbose(`Ignoring ${_.size(problems)} NPM errors:`);
} else {
this.serverless.cli.log(`Ignoring ${_.size(problems)} NPM errors:`);
}
_.forEach(problems, problem => {
if (this.log) {
this.log.verbose(`=> ${problem}`);
} else {
this.serverless.cli.log(`=> ${problem}`);
}
});
}
// (1) Generate dependency composition
const compositeModules = _.uniq(
_.flatMap(stats.stats, compileStats => {
const externalModules = _.concat(
compileStats.externalModules,
_.map(packageForceIncludes, whitelistedPackage => ({
external: whitelistedPackage
}))
);
return getProdModules.call(
this,
externalModules,
packagePath,
nodeModulesRelativeDir,
dependencyGraph,
packageForceExcludes
);
})
);
removeExcludedModules.call(this, compositeModules, packageForceExcludes, true);
if (_.isEmpty(compositeModules)) {
// The compiled code does not reference any external modules at all
if (this.log) {
this.log('No external modules needed');
} else {
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 = _.defaults(
{
name: this.serverless.service.service,
version: '1.0.0',
description: `Packaged externals for ${this.serverless.service.service}`,
private: true,
scripts: packageScripts
},
packageSections
);
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 packagerOptions = this.configuration.packagerOptions || {};
const packageLockPath = path.join(
findWorkspaceRoot(path.dirname(packageJsonPath)) || path.dirname(packageJsonPath),
packagerOptions.lockFile || packager.lockfileName
);
let hasPackageLock = false;
return BbPromise.fromCallback(cb => fse.pathExists(packageLockPath, cb))
.then(exists => {
if (exists) {
if (this.log) {
this.log('Package lock found - Using locked versions');
} else {
this.serverless.cli.log('Package lock found - Using locked versions');
}
try {
let packageLockFile = this.serverless.utils.readFileSync(packageLockPath);
packageLockFile = packager.rebaseLockfile(relPath, packageLockFile);
if (_.isObject(packageLockFile)) {
packageLockFile = JSON.stringify(packageLockFile, null, 2);
}
this.serverless.utils.writeFileSync(
path.join(compositeModulePath, packager.lockfileName),
packageLockFile
);
hasPackageLock = true;
} catch (err) {
if (this.log) {
this.log.warning(`Could not read lock file: ${err.message}`);
} else {
this.serverless.cli.log(`Warning: Could not read lock file: ${err.message}`);
}
}
}
return BbPromise.resolve();
})
.then(() => {
const start = _.now();
if (this.log) {
this.log('Packing external modules: ' + compositeModules.join(', '));
} else {
this.serverless.cli.log('Packing external modules: ' + compositeModules.join(', '));
}
return packager.getPackagerVersion(compositeModulePath).then(version => {
return packager
.install(compositeModulePath, this.configuration.packagerOptions, version)
.then(() => {
if (this.log) {
this.log.verbose(`Package took [${_.now() - start} ms]`);
} else {
this.options.verbose && this.serverless.cli.log(`Package took [${_.now() - start} ms]`);
}
return null;
})
.return(stats.stats);
});
})
.mapSeries(compileStats => {
const modulePath = compileStats.outputPath;
// Create package.json
const modulePackageJson = path.join(modulePath, 'package.json');
const modulePackage = _.defaults(
{
name: this.serverless.service.service,
version: '1.0.0',
description: `Packaged externals for ${this.serverless.service.service}`,
private: true,
scripts: packageScripts,
dependencies: {}
},
packageSections
);
const prodModules = getProdModules.call(
this,
_.concat(
compileStats.externalModules,
_.map(packageForceIncludes, whitelistedPackage => ({
external: whitelistedPackage
}))
),
packagePath,
nodeModulesRelativeDir,
dependencyGraph,
packageForceExcludes
);
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 (isProviderGoogle(this.serverless)) {
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(() => {
if (this.log) {
this.log.verbose(`Copy modules: ${modulePath} [${_.now() - startCopy} ms]`);
} else {
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.getPackagerVersion(modulePath).then(version => {
return packager.prune(modulePath, this.configuration.packagerOptions, version).tap(() => {
if (this.log) {
this.log.verbose(`Prune: ${modulePath} [${_.now() - startPrune} ms]`);
} else {
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(() => {
if (this.log) {
this.log.verbose(`Run scripts: ${modulePath} [${_.now() - startRunScripts} ms]`);
} else {
this.options.verbose &&
this.serverless.cli.log(`Run scripts: ${modulePath} [${_.now() - startRunScripts} ms]`);
}
});
});
})
.return();
});
});
}
};