/ lib / modules / pipeline / index.js
index.js
  1  const fs = require('../../core/fs.js');
  2  const path = require('path');
  3  const async = require('async');
  4  const utils = require('../../utils/utils.js');
  5  const ProcessLauncher = require('../../core/processes/processLauncher');
  6  const constants = require('../../constants');
  7  const WebpackConfigReader = require('../pipeline/webpackConfigReader');
  8  
  9  class Pipeline {
 10    constructor(embark, options) {
 11      this.embark = embark;
 12      this.env = embark.config.env;
 13      this.buildDir  = embark.config.buildDir;
 14      this.contractsFiles = embark.config.contractsFiles;
 15      this.assetFiles = embark.config.assetFiles;
 16      this.events = embark.events;
 17      this.logger = embark.config.logger;
 18      this.plugins = embark.config.plugins;
 19      this.webpackConfigName = options.webpackConfigName;
 20      this.pipelinePlugins = this.plugins.getPluginsFor('pipeline');
 21      this.pipelineConfig = embark.config.pipelineConfig;
 22      this.isFirstBuild = true;
 23  
 24      this.events.setCommandHandler('pipeline:build', (options, callback) => this.build(options, callback));
 25      fs.removeSync(this.buildDir);
 26  
 27      let plugin = this.plugins.createPlugin('deployment', {});
 28      plugin.registerAPICall(
 29        'get',
 30        '/embark-api/file',
 31        (req, res) => {
 32          if (!fs.existsSync(req.query.path) || !req.query.path.startsWith(fs.dappPath())) {
 33            return res.send({error: 'Path is invalid'});
 34          }
 35          const name = path.basename(req.query.path);
 36          const content = fs.readFileSync(req.query.path, 'utf8');
 37          res.send({name, content, path: req.query.path});
 38  
 39        }
 40      );
 41  
 42      plugin.registerAPICall(
 43        'post',
 44        '/embark-api/files',
 45        (req, res) => {
 46          try {
 47            this.apiGuardBadFile(req.body.path);
 48          } catch (error) {
 49            return res.send({error: error.message});
 50          }
 51  
 52          fs.writeFileSync(req.body.path, req.body.content, { encoding: 'utf8'});
 53          const name = path.basename(req.body.path);
 54          res.send({name, path: req.body.path, content: req.body.content});
 55        }
 56      );
 57  
 58      plugin.registerAPICall(
 59        'delete',
 60        '/embark-api/file',
 61        (req, res) => {
 62          try {
 63            this.apiGuardBadFile(req.query.path);
 64          } catch (error) {
 65            return res.send({error: error.message});
 66          }
 67          fs.removeSync(req.query.path);
 68          res.send();
 69        }
 70      );
 71  
 72      plugin.registerAPICall(
 73        'get',
 74        '/embark-api/files',
 75        (req, res) => {
 76          const rootPath = fs.dappPath();
 77  
 78          const walk = (dir, filelist = []) => fs.readdirSync(dir).map(name => {
 79            let isRoot = rootPath === dir;
 80            if (fs.statSync(path.join(dir, name)).isDirectory()) {
 81              return { 
 82                isRoot,
 83                name,
 84                dirname: dir,
 85                path: path.join(dir, name),
 86                isHidden: name.indexOf('.') === 0,
 87                children: utils.fileTreeSort(walk(path.join(dir, name), filelist))};
 88            }
 89            return {
 90              name,
 91              isRoot,
 92              path: path.join(dir, name),
 93              dirname: dir,
 94              isHidden: name.indexOf('.') === 0
 95            };
 96          });
 97          const files = utils.fileTreeSort(walk(fs.dappPath()));
 98          res.send(files);
 99        }
100      );
101    }
102  
103    apiGuardBadFile(pathToCheck) {
104      const dir = path.dirname(pathToCheck);
105      if (!fs.existsSync(pathToCheck) || !dir.startsWith(fs.dappPath())) {
106        throw new Error('Path is invalid');
107      }
108    }
109  
110    build({modifiedAssets}, callback) {
111      let self = this;
112      const importsList = {};
113      let placeholderPage;
114  
115      if (!self.assetFiles || !Object.keys(self.assetFiles).length) {
116        return self.buildContracts(callback);
117      }
118  
119      async.waterfall([
120        function createPlaceholderPage(next) {
121          if (self.isFirstBuild) {
122            self.isFirstBuild = false;
123            return next();
124          }
125          self.events.request('build-placeholder', next);
126        },
127        (next) => self.buildContracts(next),
128        (next) => self.buildWeb3JS(next),
129        function createImportList(next) {
130          importsList["Embark/EmbarkJS"] = fs.dappPath(".embark", 'embark.js');
131          importsList["Embark/web3"] = fs.dappPath(".embark", 'web3_instance.js');
132          importsList["Embark/contracts"] = fs.dappPath(".embark/contracts", '');
133  
134          self.plugins.getPluginsProperty('imports', 'imports').forEach(importObject => {
135            let [importName, importLocation] = importObject;
136            importsList[importName] = importLocation;
137          });
138          next();
139        },
140        function writeContracts(next) {
141          self.events.request('contracts:list', (_err, contracts) => {
142            // ensure the .embark/contracts directory exists (create if not exists)
143            fs.mkdirp(fs.dappPath(".embark/contracts", ''), err => {
144              if(err) return next(err);
145  
146              // Create a file .embark/contracts/index.js that requires all contract files
147              // Used to enable alternate import syntax:
148              // e.g. import {Token} from 'Embark/contracts'
149              // e.g. import * as Contracts from 'Embark/contracts'
150              let importsHelperFile = fs.createWriteStream(fs.dappPath(".embark/contracts", 'index.js'));
151              importsHelperFile.write('module.exports = {\n');
152  
153              async.eachOf(contracts, (contract, idx, eachCb) => {
154                self.events.request('code-generator:contract', contract.className, contractCode => {
155                  let filePath = fs.dappPath(".embark/contracts", contract.className + '.js');
156                  importsList["Embark/contracts/" + contract.className] = filePath;
157                  fs.writeFile(filePath, contractCode, eachCb);
158  
159                  // add the contract to the exports list to support alternate import syntax
160                  importsHelperFile.write(`"${contract.className}": require('./${contract.className}').default`);
161                  if(idx < contracts.length - 1) importsHelperFile.write(',\n'); // add a comma if we have more contracts to add
162                });
163              }, () => {
164                importsHelperFile.write('\n}'); // close the module.exports = {}
165                importsHelperFile.close(next); // close the write stream
166              });
167            });
168          });
169        },
170        function shouldRunWebpack(next){
171          // assuming we got here because an asset was changed, let's check our webpack config
172          // to see if the changed asset requires webpack to run
173          if(!(modifiedAssets && modifiedAssets.length)) return next(null, false);
174          const configReader = new WebpackConfigReader({webpackConfigName: self.webpackConfigName});
175          return configReader.readConfig((err, config) => {
176            if(err) return next(err);
177  
178            if (typeof config !== 'object' || config === null) {
179              return next(__('bad webpack config, the resolved config was null or not an object'));
180            }
181  
182            const shouldRun = modifiedAssets.some(modifiedAsset => config.module.rules.some(rule => rule.test.test(modifiedAsset)));
183            return next(null, !shouldRun);
184          });
185        },
186        function runWebpack(shouldNotRun, next) {
187          if(shouldNotRun) return next();
188          self.logger.info(__(`running webpack with '${self.webpackConfigName}' config...`));
189          const assets = Object.keys(self.assetFiles).filter(key => key.match(/\.js$/));
190          if (!assets || !assets.length) {
191            return next();
192          }
193          assets.forEach(key => {
194            self.logger.info(__("writing file") + " " + (utils.joinPath(self.buildDir, key)).bold.dim);
195          });
196          let built = false;
197          const webpackProcess = new ProcessLauncher({
198            embark: self.embark,
199            plugins: self.plugins,
200            modulePath: utils.joinPath(__dirname, 'webpackProcess.js'),
201            logger: self.logger,
202            events: self.events,
203            exitCallback: code => {
204              if (!built) {
205                return next(`Webpack build exited with code ${code} before the process finished`);
206              }
207              if (code) {
208                self.logger.error(__('Webpack build process exited with code ', code));
209              }
210            }
211          });
212          webpackProcess.send({
213            action: constants.pipeline.init,
214            options: {
215              webpackConfigName: self.webpackConfigName,
216              pipelineConfig: self.pipelineConfig
217            }
218          });
219          webpackProcess.send({action: constants.pipeline.build, assets: self.assetFiles, importsList});
220  
221          webpackProcess.once('result', constants.pipeline.built, (msg) => {
222            built = true;
223            webpackProcess.kill();
224            return next(msg.error);
225          });
226        },
227        function assetFileWrite(next) {
228          async.eachOf(
229            // assetFileWrite should not process .js files
230            Object.keys(self.assetFiles)
231              .filter(key => !key.match(/\.js$/))
232              .reduce((obj, key) => {
233                obj[key] = self.assetFiles[key];
234                return obj;
235              }, {}),
236            function (files, targetFile, cb) {
237              const isDir = targetFile.slice(-1) === '/' || targetFile.slice(-1) === '\\' || targetFile.indexOf('.') === -1;
238              // if it's not a directory
239              if (!isDir) {
240                self.logger.info(__("writing file") + " " + (utils.joinPath(self.buildDir, targetFile)).bold.dim);
241              }
242              async.map(
243                files,
244                function (file, fileCb) {
245                  self.logger.trace("reading " + file.filename);
246                  return file.content(fileContent => {
247                    self.runPlugins(file, fileContent, fileCb);
248                  });
249                },
250                function (err, contentFiles) {
251                  if (err) {
252                    self.logger.error(__('errors found while generating') + ' ' + targetFile);
253                  }
254                  let dir = targetFile.split('/').slice(0, -1).join('/');
255                  self.logger.trace("creating dir " + utils.joinPath(self.buildDir, dir));
256                  fs.mkdirpSync(utils.joinPath(self.buildDir, dir));
257  
258                  // if it's a directory
259                  if (isDir) {
260                    let targetDir = targetFile;
261  
262                    if (targetDir.slice(-1) !== '/') {
263                      targetDir = targetDir + '/';
264                    }
265  
266                    async.each(contentFiles, function (file, eachCb) {
267                      let filename = file.filename.replace(file.basedir + '/', '');
268                      self.logger.info("writing file " + (utils.joinPath(self.buildDir, targetDir, filename)).bold.dim);
269  
270                      fs.copy(file.path, utils.joinPath(self.buildDir, targetDir, filename), {overwrite: true}, eachCb);
271                    }, cb);
272                    return;
273                  }
274  
275                  let content = contentFiles.map(file => {
276                    if (file === undefined) {
277                      return "";
278                    }
279                    return file.content;
280                  }).join("\n");
281  
282                  if (new RegExp(/^index.html?/i).test(targetFile)) {
283                    targetFile = targetFile.replace('index', 'index-temp');
284                    placeholderPage = targetFile;
285                  }
286                  fs.writeFile(utils.joinPath(self.buildDir, targetFile), content, cb);
287                }
288              );
289            },
290            next
291          );
292        },
293        function removePlaceholderPage(next){
294          let placeholderFile = utils.joinPath(self.buildDir, placeholderPage);
295          fs.access(utils.joinPath(self.buildDir, placeholderPage), (err) => {
296            if (err) return next(); // index-temp doesn't exist, do nothing
297  
298            // rename index-temp.htm/l to index.htm/l, effectively replacing our placeholder page
299            // with the contents of the built index.html page
300            fs.move(placeholderFile, placeholderFile.replace('index-temp', 'index'), {overwrite: true}, next);
301          });
302        }
303      ], callback);
304    }
305  
306    buildContracts(cb) {
307      const self = this;
308      async.waterfall([
309        function makeDirectory(next) {
310          fs.mkdirp(fs.dappPath(self.buildDir, 'contracts'), err => next(err));
311        },
312        function getContracts(next) {
313          self.events.request('contracts:list', next);
314        },
315        function writeContractsJSON(contracts, next) {
316          async.each(contracts,(contract, eachCb) => {
317            fs.writeJson(fs.dappPath(
318              self.buildDir,
319              'contracts', contract.className + '.json'
320            ), contract, {spaces: 2}, eachCb);
321          }, () => next());
322        }
323      ], cb);
324    }
325  
326    buildWeb3JS(cb) {
327      const self = this;
328      async.waterfall([
329        function makeDirectory(next) {
330          fs.mkdirp(fs.dappPath(".embark"), err => next(err));
331        },
332        function getWeb3Code(next) {
333          self.events.request('code-generator:web3js', next);
334        },
335        function writeFile(code, next) {
336          fs.writeFile(fs.dappPath(".embark", 'web3_instance.js'), code, next);
337        }
338      ], cb);
339    }
340  
341    runPlugins(file, fileContent, fileCb) {
342      const self = this;
343      if (self.pipelinePlugins.length <= 0) {
344        return fileCb(null, {content: fileContent, filename: file.filename, path: file.path, basedir: file.basedir, modified: true});
345      }
346      async.eachSeries(self.pipelinePlugins, (plugin, pluginCB) => {
347        if (file.options && file.options.skipPipeline) {
348          return pluginCB();
349        }
350  
351        fileContent = plugin.runPipeline({targetFile: file.filename, source: fileContent});
352        file.modified = true;
353        pluginCB();
354      }, err => {
355        if (err) {
356          self.logger.error(err.message);
357        }
358        return fileCb(null, {content: fileContent, filename: file.filename, path: file.path, basedir: file.basedir, modified: true});
359      });
360    }
361  
362  }
363  
364  module.exports = Pipeline;