var gulp = require('gulp'); var gutil = require('gulp-util'); var taskListing = require('gulp-task-listing'); var path = require('canonical-path'); var del = require('del'); var _ = require('lodash'); var argv = require('yargs').argv; var Q = require("q"); // delPromise is a 'promise' version of del var delPromise = Q.denodeify(del); var Minimatch = require("minimatch").Minimatch; var Dgeni = require('dgeni'); var Package = require('dgeni').Package; var fsExtra = require('fs-extra'); var fs = fsExtra; var exec = require('child_process').exec; var execPromise = Q.denodeify(exec); // cross platform version of spawn that also works on windows. var xSpawn = require('cross-spawn'); var prompt = require('prompt'); var globby = require("globby"); // Ugh... replacement needed to kill processes on any OS // - because childProcess.kill does not work properly on windows var treeKill = require("tree-kill"); // TODO: // 1. Think about using runSequence // 2. Think about using spawn instead of exec in case of long error messages. var TOOLS_PATH = './tools'; var ANGULAR_PROJECT_PATH = '../angular'; var PUBLIC_PATH = './public'; var DOCS_PATH = path.join(PUBLIC_PATH, 'docs'); var EXAMPLES_PATH = path.join(DOCS_PATH, '_examples'); var NOT_API_DOCS_GLOB = path.join(PUBLIC_PATH, './{docs/*/latest/!(api),!(docs)}/**/*'); var RESOURCES_PATH = path.join(PUBLIC_PATH, 'resources'); var LIVE_EXAMPLES_PATH = path.join(RESOURCES_PATH, 'live-examples'); var docShredder = require(path.resolve(TOOLS_PATH, 'doc-shredder/doc-shredder')); var exampleZipper = require(path.resolve(TOOLS_PATH, '_example-zipper/exampleZipper')); var plunkerBuilder = require(path.resolve(TOOLS_PATH, 'plunker-builder/plunkerBuilder')); var fsUtils = require(path.resolve(TOOLS_PATH, 'fs-utils/fsUtils')); var _devguideShredOptions = { examplesDir: path.join(DOCS_PATH, '_examples'), fragmentsDir: path.join(DOCS_PATH, '_fragments'), zipDir: path.join(RESOURCES_PATH, 'zips') }; var _apiShredOptions = { examplesDir: path.join(ANGULAR_PROJECT_PATH, 'modules/angular2/examples'), fragmentsDir: path.join(DOCS_PATH, '_fragments/_api'), zipDir: path.join(RESOURCES_PATH, 'zips/api') }; var _excludePatterns = ['**/node_modules/**', '**/typings/**', '**/packages/**']; var _excludeMatchers = _excludePatterns.map(function(excludePattern){ return new Minimatch(excludePattern) }); var _exampleBoilerplateFiles = ['package.json', 'tsconfig.json', 'karma.conf.js', 'karma-test-shim.js' ]; // --filter may be passed in to filter/select _example app subdir names // i.e. gulp run-e2e-tests --filter=foo ; would select all example apps with // 'foo' in their folder names. gulp.task('run-e2e-tests', function() { var spawnInfo = spawnExt('npm', ['install'], { cwd: EXAMPLES_PATH}); return spawnInfo.promise.then(function() { copyExampleBoilerplate(); var exePath = path.join(process.cwd(), "./node_modules/.bin/"); spawnInfo = spawnExt('webdriver-manager', ['update'], {cwd: exePath}); return spawnInfo.promise; }).then(function() { return findAndRunE2eTests(argv.filter); }).then(function(status) { reportStatus(status); }).fail(function(e) { return e; }); }); // finds all of the *e2e-spec.tests under the _examples folder along // with the corresponding apps that they should run under. Then run // each app/spec collection sequentially. function findAndRunE2eTests(filter) { var startTime = new Date().getTime(); // create an output file with header. var outputFile = path.join(process.cwd(), 'protractor-results.txt'); var header = "Protractor example results for: " + (new Date()).toLocaleString() + "\n\n"; if (filter) { header += ' Filter: ' + filter.toString() + '\n\n'; } fs.writeFileSync(outputFile, header); // create an array of combos where each // combo consists of { examplePath: ... , protractorConfigFilename: ... } var exeConfigs = []; var e2eSpecPaths = getE2eSpecPaths(EXAMPLES_PATH); var srcConfig = path.join(EXAMPLES_PATH, 'protractor.config.js'); e2eSpecPaths.forEach(function(specPath) { var destConfig = path.join(specPath, 'protractor.config.js'); fsExtra.copySync(srcConfig, destConfig); // get all of the examples under each dir where a pcFilename is found examplePaths = getExamplePaths(specPath, true); if (filter) { examplePaths = examplePaths.filter(function (fn) { return fn.match(filter) != null; }) } examplePaths.forEach(function(exPath) { exeConfigs.push( { examplePath: exPath, protractorConfigFilename: destConfig }); }) }); // run the tests sequentially var status = { passed: [], failed: [] }; return exeConfigs.reduce(function (promise, combo) { return promise.then(function () { return runE2eTests(combo.examplePath, combo.protractorConfigFilename, outputFile).then(function(ok) { var arr = ok ? status.passed : status.failed; arr.push(combo.examplePath); }) }); }, Q.resolve()).then(function() { var stopTime = new Date().getTime(); status.elapsedTime = (stopTime - startTime)/1000; fs.appendFileSync(outputFile, '\nElaped Time: ' + status.elapsedTime + ' seconds'); return status; }); } // start the example in appDir; then run protractor with the specified // fileName; then shut down the example. All protractor output is appended // to the outputFile. function runE2eTests(appDir, protractorConfigFilename, outputFile ) { // start the app var appRunSpawnInfo = spawnExt('npm',['run','http-server', '--', '-s' ], { cwd: appDir }); // start protractor var pcFilename = path.resolve(protractorConfigFilename); // need to resolve because we are going to be running from a different dir var exePath = path.join(process.cwd(), "./node_modules/.bin/"); var spawnInfo = spawnExt('protractor', [ pcFilename, '--params.appDir=' + appDir, '--params.outputFile=' + outputFile], { cwd: exePath }); return spawnInfo.promise.then(function(data) { // kill the app now that protractor has completed. // Ugh... proc.kill does not work properly on windows with child processes. // appRun.proc.kill(); treeKill(appRunSpawnInfo.proc.pid); return true; }).fail(function(err) { // Ugh... proc.kill does not work properly on windows with child processes. // appRun.proc.kill(); treeKill(appRunSpawnInfo.proc.pid); return false; }); } function reportStatus(status) { gutil.log('Suites passed:'); status.passed.forEach(function(val) { gutil.log(' ' + val); }); gutil.log('Suites failed:'); status.failed.forEach(function(val) { gutil.log(' ' + val); }); if (status.failed.length == 0) { gutil.log('All tests passed'); } gutil.log('Elapsed time: ' + status.elapsedTime + ' seconds'); } // returns both a promise and the spawned process so that it can be killed if needed. function spawnExt(command, args, options) { var deferred = Q.defer(); var descr = command + " " + args.join(' '); var proc; gutil.log('running: ' + descr); try { proc = xSpawn.spawn(command, args, options); } catch(e) { gutil.log(e); deferred.reject(e); return { proc: null, promise: deferred.promise }; } proc.stdout.on('data', function (data) { gutil.log(data.toString()); }); proc.stderr.on('data', function (data) { gutil.log(data.toString()); }); proc.on('close', function (data) { gutil.log('completed: ' + descr); deferred.resolve(data); }); proc.on('error', function (data) { gutil.log('completed with error:' + descr); gutil.log(data.toString()); deferred.reject(data); }); return { proc: proc, promise: deferred.promise }; } // Public tasks gulp.task('default', ['help']); gulp.task('help', taskListing.withFilters(function(taskName) { var isSubTask = taskName.substr(0,1) == "_"; return isSubTask; }, function(taskName) { var shouldRemove = taskName === 'default'; return shouldRemove; })); // requires admin access gulp.task('add-example-boilerplate', function() { var realPath = path.join(EXAMPLES_PATH, '/node_modules'); var nodeModulesPaths = getNodeModulesPaths(EXAMPLES_PATH); nodeModulesPaths.forEach(function(linkPath) { gutil.log("symlinking " + linkPath + ' -> ' + realPath) fsUtils.addSymlink(realPath, linkPath); }); copyExampleBoilerplate(); }); // copies boilerplate files to locations // where an example app is found // also copies protractor.config.js file function copyExampleBoilerplate() { var sourceFiles = _exampleBoilerplateFiles.map(function(fn) { return path.join(EXAMPLES_PATH, fn); }); var examplePaths = getExamplePaths(EXAMPLES_PATH); // copies protractor.config.js from _examples dir to each subdir that // contains a e2e-spec file. return copyFiles(sourceFiles, examplePaths).then(function() { var sourceFiles = [ path.join(EXAMPLES_PATH, 'protractor.config.js') ]; var e2eSpecPaths = getE2eSpecPaths(EXAMPLES_PATH); return copyFiles(sourceFiles, e2eSpecPaths); }); } gulp.task('remove-example-boilerplate', function() { var nodeModulesPaths = getNodeModulesPaths(EXAMPLES_PATH); nodeModulesPaths.forEach(function(linkPath) { fsUtils.removeSymlink(linkPath); }); var examplePaths = getExamplePaths(EXAMPLES_PATH); return deleteFiles(_exampleBoilerplateFiles, examplePaths).then(function() { var e2eSpecPaths = getE2eSpecPaths(EXAMPLES_PATH); return deleteFiles(['protractor.config.js'], e2eSpecPaths); }) }); gulp.task('serve-and-sync', ['build-docs'], function (cb) { watchAndSync({devGuide: true, apiDocs: true, apiExamples: true, localFiles: true}, cb); }); gulp.task('serve-and-sync-api', ['build-docs'], function (cb) { watchAndSync({apiDocs: true, apiExamples: true}, cb); }); gulp.task('serve-and-sync-devguide', ['build-devguide-docs', 'build-plunkers', '_zip-examples'], function (cb) { watchAndSync({devGuide: true, localFiles: true}, cb); }); gulp.task('build-and-serve', ['build-docs'], function (cb) { watchAndSync({localFiles: true}, cb); }); gulp.task('build-docs', ['build-devguide-docs', 'build-api-docs', 'build-plunkers', '_zip-examples']); gulp.task('build-api-docs', ['build-js-api-docs', 'build-ts-api-docs', 'build-dart-cheatsheet']); gulp.task('build-devguide-docs', ['_shred-devguide-examples'], function() { return buildShredMaps(true); }); gulp.task('build-ts-api-docs', ['_shred-api-examples'], function() { return buildApiDocs('ts'); }); gulp.task('build-js-api-docs', ['_shred-api-examples'], function() { return buildApiDocs('js'); }); gulp.task('build-plunkers', function() { return plunkerBuilder.buildPlunkers(EXAMPLES_PATH, LIVE_EXAMPLES_PATH, { errFn: gutil.log }); }); gulp.task('build-dart-cheatsheet', [], function() { return buildApiDocs('dart'); }); gulp.task('git-changed-examples', ['_shred-devguide-examples'], function(){ var after, sha, messageSuffix; if (argv.after) { try { after = new Date(argv.after); messageSuffix = ' after: ' + argv.after; } catch (e) { throw argv.after + " is not a valid date."; } } else if (argv.sha) { sha = argv.sha; messageSuffix = ' on commit: ' + (argv.sha.length ? argv.sha : '[last commit]'); } else { gutil.log('git-changed-examples may be called with either an "--sha" argument like this:'); gutil.log(' gulp git-changed-examples --sha=4d2ac96fa247306ddd2d4c4e0c8dee2223502eb2'); gutil.log('or with an "--after" argument like this') gutil.log(' gulp git-changed-examples --after="August 1, 2015"'); return; } var jadeShredMap; return buildShredMaps(false).then(function(docs) { jadeShredMap = docs[0]; if (after) { return getChangedExamplesAfter(after); } else if (sha) { return getChangedExamples(sha); } else { gutil.log('git-changed-examples may be called with either an "--sha" argument like this:'); gutil.log(' gulp git-changed-examples --sha=4d2ac96fa247306ddd2d4c4e0c8dee2223502eb2'); gutil.log('or with an "--after" argument like this') gutil.log(' gulp git-changed-examples --after="August 1, 2015"'); } }).then(function(examplePaths) { examplePaths = filterOutExcludedPatterns(examplePaths, _excludeMatchers); gutil.log('\nExamples changed ' + messageSuffix); gutil.log(examplePaths) gutil.log("\nJade files affected by changed example files " + messageSuffix); var jadeExampleMap = jadeShredMapToJadeExampleMap(jadeShredMap, examplePaths); gutil.log(JSON.stringify(jadeExampleMap, null, " ")); gutil.log("-----"); }).catch(function(err) { gutil.log(err); throw err; }); }); gulp.task('check-deploy', ['build-docs'], function() { gutil.log('running harp compile...'); return execPromise('npm run harp -- compile . ./www', {}).then(function() { gutil.log('compile ok - running live server ...'); execPromise('npm run live-server ./www'); return askDeploy(); }).then(function(shouldDeploy) { if (shouldDeploy) { gutil.log('deploying...'); return execPromise('firebase deploy'); } else { return ['Not deploying']; } }).then(function(s) { gutil.log(s.join('')); }).catch(function(e) { gutil.log(e); }); }); gulp.task('test-api-builder', function (cb) { execCommands(['npm run test-api-builder'], {}, cb); }); // Internal tasks gulp.task('_shred-devguide-examples', ['_shred-clean-devguide'], function() { return docShredder.shred( _devguideShredOptions); }); gulp.task('_shred-clean-devguide', function(cb) { var cleanPath = path.join(_devguideShredOptions.fragmentsDir, '**/*.*') return delPromise([ cleanPath, '!**/*.ovr.*', '!**/_api/**']); }); gulp.task('_shred-api-examples', ['_shred-clean-api'], function() { checkAngularProjectPath(); return docShredder.shred( _apiShredOptions); }); gulp.task('_shred-clean-api', function(cb) { var cleanPath = path.join(_apiShredOptions.fragmentsDir, '**/*.*') return delPromise([ cleanPath, '!**/*.ovr.*' ]); }); gulp.task('_zip-examples', function() { exampleZipper.zipExamples(_devguideShredOptions.examplesDir, _devguideShredOptions.zipDir); exampleZipper.zipExamples(_apiShredOptions.examplesDir, _apiShredOptions.zipDir); }); // Helper functions // returns a promise function copyFiles(fileNames, destPaths) { var copy = Q.denodeify(fsExtra.copy); var copyPromises = []; destPaths.forEach(function(destPath) { fileNames.forEach(function(fileName) { var baseName = path.basename(fileName); var destName = path.join(destPath, baseName); var p = copy(fileName, destName, { clobber: true}); copyPromises.push(p); }); }); return Q.all(copyPromises); } function deleteFiles(baseFileNames, destPaths) { var remove = Q.denodeify(fsExtra.remove); var delPromises = []; destPaths.forEach(function(destPath) { baseFileNames.forEach(function(baseFileName) { var destFileName = path.join(destPath, baseFileName); var p = remove(destFileName); delPromises.push(p); }); }); return Q.all(delPromises); } // TODO: filter out all paths that are subdirs of another // path in the result. function getE2eSpecPaths(basePath) { var paths = getPaths(basePath, '*e2e-spec.js', true); return _.uniq(paths); } function getNodeModulesPaths(basePath) { var paths = getExamplePaths(basePath).map(function(examplePath) { return path.join(examplePath, "/node_modules"); }); return paths; } function getExamplePaths(basePath, includeBase) { // includeBase defaults to false return getPaths(basePath, "example-config.json", includeBase) } function getPaths(basePath, filename, includeBase) { var filenames = getFilenames(basePath, filename, includeBase); var paths = filenames.map(function(fileName) { return path.dirname(fileName); }); return paths; } function getFilenames(basePath, filename, includeBase) { // includeBase defaults to false var includePatterns = [path.join(basePath, "**/" + filename)]; if (!includeBase) { // ignore (skip) the top level version. includePatterns.push("!" + path.join(basePath, "/" + filename)); } var nmPattern = path.join(basePath, "**/node_modules/**"); var filenames = globby.sync(includePatterns, {ignore: [nmPattern]}); return filenames; } function watchAndSync(options, cb) { execCommands(['npm run harp -- server .'], {}, cb); var browserSync = require('browser-sync').create(); browserSync.init({proxy: 'localhost:9000'}); if (options.devGuide) { devGuideExamplesWatch(_devguideShredOptions, browserSync.reload); } if (options.apiDocs) { apiSourceWatch(browserSync.reload); } if (options.apiExamples) { apiExamplesWatch(browserSync.reload); } if (options.localFiles) { gulp.watch(NOT_API_DOCS_GLOB, browserSync.reload); } } // returns a promise; function askDeploy() { prompt.start(); var schema = { name: 'shouldDeploy', description: 'Deploy to Firebase? (y/n): ', type: 'string', pattern: /Y|N|y|n/, message: "Respond with either a 'y' or 'n'", required: true } var getPromise = Q.denodeify(prompt.get); return getPromise([schema]).then(function(result) { return result.shouldDeploy.toLowerCase() === 'y'; }); } function filterOutExcludedPatterns(fileNames, excludeMatchers) { return fileNames.filter(function(fileName) { return !excludeMatchers.some(function(excludeMatcher) { return excludeMatcher.match(fileName); }); }); } function apiSourceWatch(postBuildAction) { var srcPattern = [path.join(ANGULAR_PROJECT_PATH, 'modules/angular2/src/**/*.*')]; gulp.watch(srcPattern, {readDelay: 500}, function (event, done) { gutil.log('API source changed'); gutil.log('Event type: ' + event.event); // added, changed, or deleted gutil.log('Event path: ' + event.path); // The path of the modified file return Q.all([buildApiDocs('ts'), buildApiDocs('js')]).then(postBuildAction); }); } function apiExamplesWatch(postShredAction) { var examplesPath = path.join(ANGULAR_PROJECT_PATH, 'modules/angular2/examples/**'); var includePattern = path.join(examplesPath, '**/*.*'); var excludePattern = '!' + path.join(examplesPath, '**/node_modules/**/*.*'); var cleanPath = [path.join(_apiShredOptions.fragmentsDir, '**/*.*'), '!**/*.ovr.*']; gulp.watch([includePattern, excludePattern], {readDelay: 500}, function (event, done) { gutil.log('API example changed'); gutil.log('Event type: ' + event.type); // added, changed, or deleted gutil.log('Event path: ' + event.path); // The path of the modified file return delPromise(cleanPath).then(function() { return docShredder.shred(_apiShredOptions); }).then(postShredAction); }); } function devGuideExamplesWatch(shredOptions, postShredAction) { var includePattern = path.join(shredOptions.examplesDir, '**/*.*'); var excludePattern = '!' + path.join(shredOptions.examplesDir, '**/node_modules/**/*.*'); // removed this version because gulp.watch has the same glob issue that dgeni has. // gulp.watch([includePattern, excludePattern], {readDelay: 500}, function (event, done) { var files = globby.sync( [includePattern], { ignore: [ '**/node_modules/**']}); gulp.watch([files], {readDelay: 500}, function (event, done) { gutil.log('Dev Guide example changed') gutil.log('Event type: ' + event.type); // added, changed, or deleted gutil.log('Event path: ' + event.path); // The path of the modified file return docShredder.shredSingleDir(shredOptions, event.path).then(postShredAction); }); } // Generate the API docs for the specified language, if not specified then it defaults to ts function buildApiDocs(targetLanguage) { var ALLOWED_LANGUAGES = ['ts', 'js', 'dart']; var GENERATE_API_LANGUAGES = ['ts', 'js']; checkAngularProjectPath(); try { // Build a specialized package to generate different versions of the API docs var package = new Package('apiDocs', [require(path.resolve(TOOLS_PATH, 'api-builder/angular.io-package'))]); package.config(function(targetEnvironments, writeFilesProcessor, readTypeScriptModules) { ALLOWED_LANGUAGES.forEach(function(target) { targetEnvironments.addAllowed(target); }); if (targetLanguage) { targetEnvironments.activate(targetLanguage); if (GENERATE_API_LANGUAGES.indexOf(targetLanguage) === -1) { // Don't read TypeScript modules if we are not generating API docs - Dart I am looking at you! readTypeScriptModules.$enabled = false; } writeFilesProcessor.outputFolder = targetLanguage + '/latest/api'; } }); var dgeni = new Dgeni([package]); return dgeni.generate(); } catch(err) { gutil.log(err); gutil.log(err.stack); throw err; } function copyApiDocsToJsFolder() { // Make a copy of the JS API docs to the TS folder return gulp.src([path.join(DOCS_PATH, 'ts/latest/api/**/*.*'), '!' + path.join(DOCS_PATH, 'ts/latest/api/index.jade')]) .pipe(gulp.dest('./public/docs/js/latest/api')); } } function buildShredMaps(shouldWrite) { var options = { devguideExamplesDir: _devguideShredOptions.examplesDir, apiExamplesDir: _apiShredOptions.examplesDir, fragmentsDir: _devguideShredOptions.fragmentsDir, jadeDir: './public/docs', outputDir: './public/docs', writeFilesEnabled: shouldWrite }; return docShredder.buildShredMap(options).then(function(docs) { return docs; }); } // returns a promise containing filePaths with any changed or added examples; function getChangedExamples(sha) { var Git = require("nodegit"); var examplesPath = _devguideShredOptions.examplesDir; var relativePath = path.relative(process.cwd(), examplesPath); return Git.Repository.open(".").then(function(repo) { if (sha.length) { return repo.getCommit(sha); } else { return repo.getHeadCommit(); } }).then(function(commit) { return getChangedExamplesForCommit(commit, relativePath); }).catch(function(err) { }); } function getChangedExamplesAfter(date, relativePath) { var Git = require("nodegit"); var examplesPath = _devguideShredOptions.examplesDir; var relativePath = path.relative(process.cwd(), examplesPath); return Git.Repository.open(".").then(function(repo) { return repo.getHeadCommit(); }).then(function(commit) { var repo = commit.owner(); var revWalker = repo.createRevWalk(); revWalker.sorting(Git.Revwalk.SORT.TIME); revWalker.push(commit.id()); return revWalker.getCommitsUntil(function (commit) { return commit.date().getTime() > date.getTime(); }); }).then(function(commits) { return Q.all(commits.map(function(commit) { return getChangedExamplesForCommit(commit, relativePath); })); }).then(function(arrayOfPaths) { var pathMap = {}; arrayOfPaths.forEach(function(paths) { paths.forEach(function(path) { pathMap[path] = true; }); }); var uniqPaths = _.keys(pathMap); return uniqPaths; }).catch(function(err) { var x = err; }); } function getChangedExamplesForCommit(commit, relativePath) { return commit.getDiff().then(function(diffList) { var filePaths = []; diffList.forEach(function (diff) { diff.patches().forEach(function (patch) { if (patch.isAdded() || patch.isModified) { var filePath = path.normalize(patch.newFile().path()); var isExample = filePath.indexOf(relativePath) >= 0; // gutil.log(filePath + " isExample: " + isExample); if (isExample) { filePaths.push(filePath); } } }); }); return filePaths; }); } function jadeShredMapToJadeExampleMap(jadeShredMap, examplePaths) { // remove dups in examplePaths var exampleSet = {}; examplePaths.forEach(function(examplePath) { exampleSet[examplePath] = examplePath; }); var basePath = path.resolve("."); var jadeToFragMap = jadeShredMap.jadeToFragMap; var jadeExampleMap = {}; for (var jadePath in jadeToFragMap) { var relativeJadePath = path.relative(basePath, jadePath); var vals = jadeToFragMap[jadePath]; vals.forEach(function(val) { var relativeExamplePath = path.relative(basePath, val.examplePath); if (exampleSet[relativeExamplePath] != null) { addKeyValue(jadeExampleMap, relativeJadePath, relativeExamplePath); } }); } return jadeExampleMap; } function jadeShredMapToExampleJadeMap(jadeShredMap) { var jadeToFragMap = jadeShredMap.jadeToFragMap; var exampleJadeMap = {}; for (var jadePath in jadeToFragMap) { var vals = jadeToFragMap[jadePath]; vals.forEach(function(val) { var examplePath = val.examplePath; addKeyValue(exampleJadeMap, examplePath, jadePath); }); } return exampleJadeMap; } function addKeyValue(map, key, value) { var vals = map[key]; if (vals) { if (vals.indexOf(value) == -1) { vals.push(value); } } else { map[key] = [value]; } } // Synchronously execute a chain of commands. // cmds: an array of commands // options: { shouldLog: true, shouldThrow: true } // cb: function(err, stdout, stderr) function execCommands(cmds, options, cb) { options = options || {}; options.shouldThrow = options.shouldThrow == null ? true : options.shouldThrow; options.shouldLog = options.shouldLog == null ? true : options.shouldLog; if (!cmds || cmds.length == 0) cb(null, null, null); var exec = require('child_process').exec; // just to make it more portable. exec(cmds[0], options, function(err, stdout, stderr) { if (err == null) { if (options.shouldLog) { gutil.log('cmd: ' + cmds[0]); gutil.log('stdout: ' + stdout); } if (cmds.length == 1) { cb(err, stdout, stderr); } else { execCommands(cmds.slice(1), options, cb); } } else { if (options.shouldLog) { gutil.log('exec error on cmd: ' + cmds[0]); gutil.log('exec error: ' + err); if (stdout) gutil.log('stdout: ' + stdout); if (stderr) gutil.log('stderr: ' + stderr); } if (err && options.shouldThrow) throw err; cb(err, stdout, stderr); } }); } function checkAngularProjectPath() { if (!fs.existsSync(ANGULAR_PROJECT_PATH)) { throw new Error('API related tasks require the angular2 repo to be at ' + path.resolve(ANGULAR_PROJECT_PATH)); } }