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;