UNPKG

8.73 kBJavaScriptView Raw
1/*
2 * Copyright 2018 Adobe. All rights reserved.
3 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may obtain a copy
5 * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software distributed under
8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 * OF ANY KIND, either express or implied. See the License for the specific language
10 * governing permissions and limitations under the License.
11 */
12const chalk = require('chalk');
13const glob = require('glob');
14const path = require('path');
15const fs = require('fs-extra');
16const ProgressBar = require('progress');
17const archiver = require('archiver');
18const AbstractCommand = require('./abstract.cmd.js');
19const BuildCommand = require('./build.cmd.js');
20const ActionBundler = require('./builder/ActionBundler.js');
21
22/**
23 * Uses webpack to bundle each template script and creates an OpenWhisk action for each.
24 */
25class PackageCommand extends AbstractCommand {
26 constructor(logger) {
27 super(logger);
28 this._target = null;
29 this._files = null;
30 this._modulePaths = [];
31 this._requiredModules = null;
32 this._onlyModified = false;
33 this._enableMinify = false;
34 this._customPipeline = null;
35 }
36
37 // eslint-disable-next-line class-methods-use-this
38 get requireConfigFile() {
39 return false;
40 }
41
42 withTarget(value) {
43 this._target = value;
44 return this;
45 }
46
47 withFiles(value) {
48 this._files = value;
49 return this;
50 }
51
52 withOnlyModified(value) {
53 this._onlyModified = value;
54 return this;
55 }
56
57 withMinify(value) {
58 this._enableMinify = value;
59 return this;
60 }
61
62 withModulePaths(value) {
63 this._modulePaths = value;
64 return this;
65 }
66
67 withRequiredModules(mods) {
68 this._requiredModules = mods;
69 return this;
70 }
71
72 withCustomPipeline(customPipeline) {
73 this._customPipeline = customPipeline;
74 return this;
75 }
76
77 async init() {
78 await super.init();
79 this._target = path.resolve(this.directory, this._target);
80 }
81
82 /**
83 * Creates a .zip package that contains the contents to be deployed to openwhisk.
84 * As a side effect, this method updates the {@code info.archiveSize} after completion.
85 *
86 * @param {ActionInfo} info - The action info object.
87 * @param {ProgressBar} bar - The progress bar.
88 * @returns {Promise<any>} Promise that resolves to the package file {@code path}.
89 */
90 async createPackage(info, bar) {
91 const { log } = this;
92
93 const tick = (message, name) => {
94 const shortname = name.replace(/\/package.json.*/, '').replace(/node_modules\//, '');
95 bar.tick({
96 action: name ? `packaging ${shortname}` : '',
97 });
98 if (message) {
99 this.log.infoFields(message, {
100 progress: true,
101 });
102 }
103 };
104
105 return new Promise((resolve, reject) => {
106 const archiveName = path.basename(info.zipFile);
107 let hadErrors = false;
108
109 // create zip file for package
110 const output = fs.createWriteStream(info.zipFile);
111 const archive = archiver('zip');
112
113 log.debug(`preparing package ${archiveName}`);
114 output.on('close', () => {
115 if (!hadErrors) {
116 log.debug(`${archiveName}: Created package. ${archive.pointer()} total bytes`);
117 // eslint-disable-next-line no-param-reassign
118 info.archiveSize = archive.pointer();
119 this.emit('create-package', info);
120 resolve(info);
121 }
122 });
123 archive.on('entry', (data) => {
124 log.debug(`${archiveName}: A ${data.name}`);
125 tick('', data.name);
126 });
127 archive.on('warning', (err) => {
128 log.error(`${chalk.redBright('[error] ')}Unable to create archive: ${err.message}`);
129 hadErrors = true;
130 reject(err);
131 });
132 archive.on('error', (err) => {
133 log.error(`${chalk.redBright('[error] ')}Unable to create archive: ${err.message}`);
134 hadErrors = true;
135 reject(err);
136 });
137 archive.pipe(output);
138
139 const packageJson = {
140 name: info.name,
141 version: '1.0',
142 description: `Lambda function of ${info.name}`,
143 main: path.basename(info.main),
144 license: 'Apache-2.0',
145 };
146
147 archive.append(JSON.stringify(packageJson, null, ' '), { name: 'package.json' });
148 archive.file(info.bundlePath, { name: path.basename(info.main) });
149 archive.finalize();
150 });
151 }
152
153 /**
154 * Creates the action bundles from the given scripts. It uses the {@code ActionBundler} which
155 * in turn uses webpack to create individual bundles of each {@code script}. The final bundles
156 * are wirtten to the {@code this._target} directory.
157 *
158 * @param {ActionInfo[]} scripts - the scripts information.
159 * @param {ProgressBar} bar - The progress bar.
160 */
161 async createBundles(scripts, bar) {
162 const progressHandler = (percent, msg, ...args) => {
163 /* eslint-disable no-param-reassign */
164 const action = args.length > 0 ? `${msg} ${args[0]}` : msg;
165 const rt = bar.renderThrottle;
166 if (msg !== 'building') {
167 // this is kind of a hack to force redraw for non-bundling steps.
168 bar.renderThrottle = 0;
169 }
170 bar.update(percent * 0.8, { action });
171 bar.renderThrottle = rt;
172 /* eslint-enable no-param-reassign */
173 };
174 // create the bundles
175 const bundler = new ActionBundler()
176 .withDirectory(this._target)
177 .withModulePaths(['node_modules', ...this._modulePaths, path.resolve(__dirname, '..', 'node_modules')])
178 .withLogger(this.log)
179 .withProgressHandler(progressHandler)
180 .withMinify(this._enableMinify);
181 const stats = await bundler.run(scripts);
182 if (stats.errors) {
183 stats.errors.forEach((msg) => this.log.error(msg));
184 }
185 if (stats.warnings) {
186 stats.warnings.forEach((msg) => this.log.warn(msg));
187 }
188 if (stats.errors && stats.errors.length > 0) {
189 throw new Error('Error while bundling packages.');
190 }
191 }
192
193 /**
194 * Run this command.
195 */
196 async run() {
197 await this.init();
198
199 // always run build first to make sure scripts are up to date
200 const build = new BuildCommand(this.log)
201 .withFiles(this._files)
202 .withModulePaths(this._modulePaths)
203 .withRequiredModules(this._requiredModules)
204 .withCustomPipeline(this._customPipeline)
205 .withDirectory(this.directory)
206 .withTargetDir(this._target);
207 await build.run();
208 this._modulePaths = build.modulePaths;
209
210 // get the list of scripts from the info files
211 const infos = [...glob.sync(`${this._target}/**/*.info.json`)];
212 let scripts = await Promise.all(infos.map((info) => fs.readJSON(info)));
213
214 // filter out the ones that already have the info and a valid zip file
215 if (this._onlyModified) {
216 await Promise.all(scripts.map(async (script) => {
217 // check if zip exists, and if not, clear the path entry
218 if (!script.zipFile || !(await fs.pathExists(script.zipFile))) {
219 // eslint-disable-next-line no-param-reassign
220 delete script.zipFile;
221 }
222 }));
223 scripts.filter((script) => script.zipFile).forEach((script) => {
224 this.emit('ignore-package', script);
225 });
226 scripts = scripts.filter((script) => !script.zipFile);
227 }
228
229 if (scripts.length > 0) {
230 // generate additional infos
231 scripts.forEach((script) => {
232 /* eslint-disable no-param-reassign */
233 script.name = path.basename(script.main, '.js');
234 script.bundleName = `${script.name}.bundle.js`;
235 script.bundlePath = path.resolve(script.buildDir, script.bundleName);
236 script.dirname = path.dirname(script.main);
237 script.archiveName = `${script.name}.zip`;
238 script.zipFile = path.resolve(script.buildDir, script.archiveName);
239 /* eslint-enable no-param-reassign */
240 });
241
242 // we reserve 80% for bundling the scripts and 20% for creating the zip files.
243 const bar = new ProgressBar('[:bar] :action :elapseds', {
244 total: scripts.length * 2 * 5,
245 width: 50,
246 renderThrottle: 50,
247 stream: process.stdout,
248 });
249
250 // create bundles
251 await this.createBundles(scripts, bar);
252
253 // package actions
254 await Promise.all(scripts.map((script) => this.createPackage(script, bar)));
255
256 // write back the updated infos
257 // eslint-disable-next-line max-len
258 await Promise.all(scripts.map((script) => fs.writeJson(script.infoFile, script, { spaces: 2 })));
259 }
260
261 this.log.info('✅ packaging completed');
262 return this;
263 }
264}
265module.exports = PackageCommand;