UNPKG

serverless-webpack

Version:

Serverless plugin to bundle your javascript with Webpack

331 lines (288 loc) 12.5 kB
'use strict'; const BbPromise = require('bluebird'); const path = require('path'); const fse = require('fs-extra'); const glob = require('glob'); const lib = require('./index'); const _ = require('lodash'); const Configuration = require('./Configuration'); const { getAllNodeFunctions, isNodeRuntime, isProviderGoogle } = require('./utils'); /** * For automatic entry detection we sort the found files to solve ambiguities. * This should cover most of the cases. For complex setups the user should * build his own entries with help of the other exports. */ const preferredExtensions = ['.js', '.ts', '.jsx', '.tsx']; module.exports = { validate() { const getHandlerFileAndFunctionName = functionDefinition => { const { handler: handlerProp, image: imageProp, entrypoint: entryPoint } = functionDefinition; // CASE: The lambda handler is inside lambda layer. Use functionDefinition `entrypoint` // to define the original handler. if (entryPoint) return entryPoint; if (handlerProp) { return handlerProp; } if (imageProp && typeof imageProp == 'string') return imageProp; if (!imageProp || !imageProp.command || imageProp.command.length < 1) { const docsLink = 'https://www.serverless.com/blog/container-support-for-lambda'; throw new this.serverless.classes.Error( `Either function.handler or function.image must be defined. Pass the handler name (i.e. 'index.handler') as the value for function.image.command[0]. For help see: ${docsLink}` ); } return imageProp.command[0]; }; const getHandlerFile = handler => { // Check if handler is a well-formed path based handler. const handlerEntry = /(.*)\..*?$/.exec(handler); if (handlerEntry) { return handlerEntry[1]; } }; const getEntryExtension = fileName => { const files = glob.sync(`${fileName}.*`, { cwd: this.serverless.config.servicePath, nodir: true, ignore: this.configuration.excludeFiles ? this.configuration.excludeFiles : undefined }); if (_.isEmpty(files)) { // If we cannot find any handler we should terminate with an error throw new this.serverless.classes.Error( `No matching handler found for '${fileName}' in '${this.serverless.config.servicePath}'. Check your service definition.` ); } // Move preferred file extensions to the beginning const sortedFiles = _.uniq( _.concat( _.sortBy( _.filter(files, file => _.includes(preferredExtensions, path.extname(file))), a => _.size(a) ), files ) ); if (_.size(sortedFiles) > 1) { if (this.log) { this.log.warning(`More than one matching handlers found for "${fileName}". Using "${_.first(sortedFiles)}"`); } else { this.serverless.cli.log( `WARNING: More than one matching handlers found for '${fileName}'. Using '${_.first(sortedFiles)}'.` ); } } return path.extname(_.first(sortedFiles)); }; const getEntryForFunction = (name, serverlessFunction) => { const handler = getHandlerFileAndFunctionName(serverlessFunction); const handlerFile = getHandlerFile(handler); if (!handlerFile) { if (!isProviderGoogle(this.serverless)) { if (this.log) { this.log.warning(); this.log.warning( `Entry for ${name}@${handler} could not be retrieved.\nPlease check your service config if you want to use lib.entries.` ); } else { this.serverless.cli.log( `\nWARNING: Entry for ${name}@${handler} could not be retrieved.\nPlease check your service config if you want to use lib.entries.` ); } } return {}; } const ext = getEntryExtension(handlerFile); // Create a valid entry key return { [handlerFile]: `./${handlerFile}${ext}` }; }; // Initialize plugin configuration this.configuration = new Configuration(this.serverless.service.custom); if (this.log) { this.log.verbose(`Using configuration:\n${JSON.stringify(this.configuration, null, 2)}`); } else { this.options.verbose && this.serverless.cli.log(`Using configuration:\n${JSON.stringify(this.configuration, null, 2)}`); } if (this.configuration.hasLegacyConfig) { if (this.log) { this.log.warning('Legacy configuration detected. Consider to use "custom.webpack" as object (see README).'); } else { this.serverless.cli.log( 'Legacy configuration detected. Consider to use "custom.webpack" as object (see README).' ); } } this.webpackConfig = this.configuration.config || this.configuration.webpackConfig; if (this.webpackConfig.includeModules && this.webpackConfig.packagerOptions.noInstall) { throw new this.serverless.classes.Error( '"includeModules" requires an installation, and cannot be used with "packagerOptions.noInstall".' ); } // Expose entries - must be done before requiring the webpack configuration const entries = {}; const functions = getAllNodeFunctions.call(this); if (this.options.function) { const serverlessFunction = this.serverless.service.getFunction(this.options.function); const entry = getEntryForFunction.call(this, this.options.function, serverlessFunction); _.merge(entries, entry); } else { _.forEach(functions, (func, index) => { const loadedFunc = this.serverless.service.getFunction(func); const runtime = loadedFunc.runtime || this.serverless.service.provider.runtime || 'nodejs'; if (isNodeRuntime(runtime)) { // runtimes can be 'nodejsX.Y' (AWS, Azure) or 'google-nodejs' (Google Cloud) const entry = getEntryForFunction.call(this, functions[index], loadedFunc); _.merge(entries, entry); } if (runtime === 'provided' && loadedFunc.allowCustomRuntime) { // allow custom runtime if the user has specified it const entry = getEntryForFunction.call(this, functions[index], loadedFunc); _.merge(entries, entry); } }); } // Expose service file and options lib.serverless = this.serverless; lib.options = this.options; lib.entries = entries; if (_.isString(this.webpackConfig)) { const webpackConfigFilePath = path.join(this.serverless.config.servicePath, this.webpackConfig); if (!this.serverless.utils.fileExistsSync(webpackConfigFilePath)) { return BbPromise.reject( new this.serverless.classes.Error( 'The webpack plugin could not find the configuration file at: ' + webpackConfigFilePath ) ); } try { const webpackConfig = require(webpackConfigFilePath); this.webpackConfig = webpackConfig.default || webpackConfig; } catch (err) { if (this.log) { this.log.error(`Could not load webpack config "${webpackConfigFilePath}"`); } else { this.serverless.cli.log(`Could not load webpack config '${webpackConfigFilePath}'`); } return BbPromise.reject(err); } } // Intermediate function to handle async webpack config const processConfig = _config => { this.webpackConfig = _config; // Default context if (!this.webpackConfig.context) { this.webpackConfig.context = this.serverless.config.servicePath; } // Default target if (!this.webpackConfig.target) { this.webpackConfig.target = 'node'; } // Default output if (!this.webpackConfig.output || _.isEmpty(this.webpackConfig.output)) { const outputPath = path.join(this.serverless.config.servicePath, '.webpack'); this.webpackConfig.output = { libraryTarget: 'commonjs', path: outputPath, filename: '[name].js' }; } // Default node if (!this.webpackConfig.node || _.isEmpty(this.webpackConfig.node)) { this.webpackConfig.node = false; } // Custom output path if (this.options.out) { this.webpackConfig.output.path = path.join(this.serverless.config.servicePath, this.options.out); } this.skipCompile = _.get(this.serverless, 'service.custom.webpack.noBuild') === true || _.get(this.options, 'skip-build') === true; // Skip compilation with --skip-build or noBuild if (this.skipCompile) { if (this.log) { this.log('Skipping build and using existing compiled output'); } else { this.serverless.cli.log('Skipping build and using existing compiled output'); } if (!fse.pathExistsSync(this.webpackConfig.output.path)) { return BbPromise.reject(new this.serverless.classes.Error('No compiled output found')); } this.keepOutputDirectory = true; } if (!this.keepOutputDirectory) { if (this.log) { this.log.verbose(`Removing ${this.webpackConfig.output.path}`); } else { this.options.verbose && this.serverless.cli.log(`Removing ${this.webpackConfig.output.path}`); } fse.removeSync(this.webpackConfig.output.path); } this.webpackOutputPath = this.webpackConfig.output.path; // In case of individual packaging we have to create a separate config for each function if (_.has(this.serverless, 'service.package') && this.serverless.service.package.individually) { if (this.log) { this.log.verbose( `Individually packaging with concurrency at ${this.configuration.concurrency} entries a time.` ); } else { this.options.verbose && this.serverless.cli.log( `Individually packaging with concurrency at ${this.configuration.concurrency} entries a time.` ); } if (this.webpackConfig.entry && !_.isEqual(this.webpackConfig.entry, entries)) { return BbPromise.reject( new this.serverless.classes.Error( 'Webpack entry must be automatically resolved when package.individually is set to true. ' + 'In webpack.config.js, remove the entry declaration or set entry to slsw.lib.entries.' ) ); } // Lookup associated Serverless functions const allEntryFunctions = _.map(getAllNodeFunctions.call(this), funcName => { const func = this.serverless.service.getFunction(funcName); const handler = getHandlerFileAndFunctionName(func); const handlerFile = path.relative('.', getHandlerFile(handler)); return { handlerFile, funcName, func }; }); this.entryFunctions = _.flatMap(entries, (value, key) => { const entry = path.relative('.', value); const entryFile = _.replace(entry, new RegExp(`${path.extname(entry)}$`), ''); const entryFuncs = _.filter(allEntryFunctions, ['handlerFile', entryFile]); if (_.isEmpty(entryFuncs)) { // We have to make sure that for each entry there is an entry function item. entryFuncs.push({}); } _.forEach(entryFuncs, entryFunc => { entryFunc.entry = { key, value }; }); return entryFuncs; }); this.webpackConfig = _.map(this.entryFunctions, entryFunc => { const config = _.cloneDeep(this.webpackConfig); config.entry = { [entryFunc.entry.key]: entryFunc.entry.value }; const compileName = entryFunc.funcName || _.camelCase(entryFunc.entry.key); config.output.path = path.join(config.output.path, compileName); return config; }); } else { this.webpackConfig.output.path = path.join(this.webpackConfig.output.path, 'service'); } return BbPromise.resolve(); }; // Webpack config can be a Promise, If it's a Promise wait for resolved config object. if (this.webpackConfig && _.isFunction(this.webpackConfig.then)) { return BbPromise.resolve(this.webpackConfig.then(config => processConfig(config))); } return processConfig(this.webpackConfig); } };