UNPKG

serverless-webpack

Version:

Serverless plugin to bundle your javascript with Webpack

268 lines (225 loc) 9.23 kB
'use strict'; const BbPromise = require('bluebird'); const _ = require('lodash'); const path = require('path'); const glob = require('glob'); const archiver = require('archiver'); const semver = require('semver'); const fs = require('fs'); const { getAllNodeFunctions, isProviderGoogle } = require('./utils'); const readFileAsync = BbPromise.promisify(fs.readFile); const statAsync = BbPromise.promisify(fs.stat); function setArtifactPath(funcName, func, artifactPath) { const version = this.serverless.getVersion(); if (this.log) { this.log.verbose(`Setting artifact for function '${funcName}' to '${artifactPath}'`); } else { this.options.verbose && this.serverless.cli.log(`Setting artifact for function '${funcName}' to '${artifactPath}'`); } // Serverless changed the artifact path location in version 1.18 if (semver.lt(version, '1.18.0')) { func.artifact = artifactPath; func.package = _.assign({}, func.package, { disable: true }); if (!this.log) { this.serverless.cli.log(`${funcName} is packaged by the webpack plugin. Ignore messages from SLS.`); } } else { func.package = { artifact: artifactPath }; } } /** * Copy pasted from Serverless * * @see https://github.com/serverless/serverless/blob/63d54e1537e10ae63c171892edd886f6b81e83f6/lib/plugins/package/lib/zipService.js#L65 */ function serverlessZip(args) { const { artifactFilePath, directory, files } = args; const zip = archiver.create('zip'); const output = fs.createWriteStream(artifactFilePath); return new BbPromise((resolve, reject) => { output.on('close', () => resolve(artifactFilePath)); output.on('error', err => reject(err)); zip.on('error', err => reject(err)); output.on('open', () => { zip.pipe(output); // normalize both maps to avoid problems with e.g. Path Separators in different shells const normalizedFiles = _.uniq(_.map(files, file => path.normalize(file))); BbPromise.all(_.map(normalizedFiles, file => getFileContentAndStat.call(this, directory, file))) .then(contents => { _.forEach( contents.sort((content1, content2) => content1.filePath.localeCompare(content2.filePath)), file => { const name = file.filePath; // Ensure file is executable if it is locally executable or // we force it to be executable if platform is windows const mode = file.stat.mode & 0o100 || process.platform === 'win32' ? 0o755 : 0o644; zip.append(file.data, { name, mode, // necessary to get the same hash when zipping the same content // as well as `contents.sort` few lines above date: new Date(0) }); } ); return zip.finalize(); }) .catch(reject); }); }); } /** * Copy pasted from Serverless * * @see https://github.com/serverless/serverless/blob/63d54e1537e10ae63c171892edd886f6b81e83f6/lib/plugins/package/lib/zipService.js#L112 */ function getFileContentAndStat(directory, filePath) { const fullPath = `${directory}/${filePath}`; return BbPromise.all([ // Get file contents and stat in parallel readFileAsync(fullPath), statAsync(fullPath) ]).then( result => ({ data: result[0], stat: result[1], filePath }), error => { throw new this.serverless.classes.Error( `Cannot read file ${filePath} due to: ${error.message}`, 'CANNOT_READ_FILE' ); } ); } function zip(directory, zipFileName) { // Check that files exist to be zipped let files = glob.sync('**', { cwd: directory, dot: true, silent: true, follow: true, nodir: true }); // if excludeRegex option is defined, we'll have to list all files to be zipped // and then force the node way to zip to avoid hitting the arguments limit (ie: E2BIG) // when using the native way (ie: the zip command) if (this.configuration.excludeRegex) { const existingFilesLength = files.length; files = _.filter(files, f => f.match(this.configuration.excludeRegex) === null); if (this.log) { this.log.verbose(`Excluded ${existingFilesLength - files.length} file(s) based on excludeRegex`); } else { this.options.verbose && this.serverless.cli.log(`Excluded ${existingFilesLength - files.length} file(s) based on excludeRegex`); } } if (_.isEmpty(files)) { const error = new this.serverless.classes.Error('Packaging: No files found'); return BbPromise.reject(error); } // Create artifact in temp path and move it to the package path (if any) later // This allows us to persist the webpackOutputPath and re-use the compiled output const artifactFilePath = path.join(this.webpackOutputPath, zipFileName); this.serverless.utils.writeFileDir(artifactFilePath); return serverlessZip.call(this, { directory, artifactFilePath, files }); } function getArtifactLocations(name) { const archiveName = `${name}.zip`; const webpackArtifact = path.join(this.webpackOutputPath, archiveName); const serverlessArtifact = path.join('.serverless', archiveName); return { webpackArtifact, serverlessArtifact }; } function copyArtifactByName(artifactName) { const { webpackArtifact, serverlessArtifact } = getArtifactLocations.call(this, artifactName); // Make sure the destination dir exists this.serverless.utils.writeFileDir(serverlessArtifact); fs.copyFileSync(webpackArtifact, serverlessArtifact); } function setServiceArtifactPath(artifactPath) { _.set(this.serverless, 'service.package.artifact', artifactPath); } function isIndividualPackaging() { return _.get(this.serverless, 'service.package.individually'); } function getArtifactName(entryFunction) { return `${entryFunction.funcName || this.serverless.service.getServiceObject().name}.zip`; } module.exports = { packageModules() { if (this.skipCompile) { return BbPromise.resolve(); } if (this.log) { this.log.verbose('[Webpack] Packaging modules'); this.progress.get('webpack').notice('[Webpack] Packaging modules'); } const stats = this.compileStats; return BbPromise.mapSeries(stats.stats, (compileStats, index) => { const entryFunction = _.get(this.entryFunctions, index, {}); const filename = getArtifactName.call(this, entryFunction); const modulePath = compileStats.outputPath; const startZip = _.now(); return zip.call(this, modulePath, filename).tap(() => { if (this.log) { this.log.verbose( `Zip ${_.isEmpty(entryFunction) ? 'service' : 'function'}: ${modulePath} [${_.now() - startZip} ms]` ); } else { this.options.verbose && this.serverless.cli.log( `Zip ${_.isEmpty(entryFunction) ? 'service' : 'function'}: ${modulePath} [${_.now() - startZip} ms]` ); } }); }); }, copyExistingArtifacts() { if (this.log) { this.log.verbose('[Webpack] Copying existing artifacts'); this.progress.get('webpack').notice('[Webpack] Copying existing artifacts'); } else { this.serverless.cli.log('Copying existing artifacts...'); } // When invoked as a part of `deploy function`, // only function passed with `-f` flag should be processed. const functionNames = this.options.function ? [this.options.function] : getAllNodeFunctions.call(this); const serviceName = this.serverless.service.getServiceObject().name; const individualPackagingEnabled = isIndividualPackaging.call(this); const providerIsGoogle = isProviderGoogle(this.serverless); // Copy artifacts to package location if (individualPackagingEnabled) { _.forEach(functionNames, funcName => copyArtifactByName.call(this, funcName)); } else { // Copy service packaged artifact copyArtifactByName.call(this, serviceName); } // Loop through every function and make sure that the correct artifact is assigned // (the one built by webpack) _.forEach(functionNames, funcName => { const func = this.serverless.service.getFunction(funcName); // When individual packaging is enabled, each functions gets it's own // artifact, otherwise every function gets set to the same artifact const archiveName = individualPackagingEnabled ? funcName : serviceName; const { serverlessArtifact } = getArtifactLocations.call(this, archiveName); setArtifactPath.call(this, funcName, func, serverlessArtifact); }); // If we are deploying to 'google' we need to set an artifact for the whole service, // rather than for each function, so there is special case here if (!individualPackagingEnabled && providerIsGoogle) { const archiveName = serviceName; // This may look similar to the loop above, but note that this calls // setServiceArtifactPath rather than setArtifactPath const { serverlessArtifact } = getArtifactLocations.call(this, archiveName); setServiceArtifactPath.call(this, serverlessArtifact); } return BbPromise.resolve(); } };