├── .npmignore ├── tests ├── taskFiles │ ├── brokenModule.js │ ├── missingOnEvent.js │ ├── report.js │ └── report-2.js ├── clis │ ├── report.js │ └── report-2.js ├── utils │ ├── testPath.js │ ├── tmpPath.js │ ├── printMismatches.js │ ├── unlinkFilesSync.js │ ├── normalizeWatchGroupPath.js │ ├── writeFilesSync.js │ ├── getTestFiles.js │ ├── testEach.js │ └── onDone.js ├── configs-errors │ ├── noEventsArray.js │ ├── brokenModule.js │ ├── noTaskFilesOrCommands.js │ ├── missingOnEvent.js │ ├── nonExistentFile.js │ ├── badCommand.js │ └── nonExistentFileInArray.js ├── test-add.js ├── test-change.js ├── test-unlink.js ├── test-watch.js ├── test-errors.js ├── configs-watch │ ├── hideAll.js │ ├── singleFileAddChange.js │ ├── hideBoth.js │ ├── globAddChange.js │ ├── singleFileAddChangeWatchInitial.js │ ├── hideChildFiles.js │ ├── multiWatchGroup.js │ ├── ignoreDotFiles.js │ ├── hideChildDirs.js │ ├── ignoredGlob.js │ ├── ignored.js │ ├── nestedFolders.js │ ├── globAddChangeWatchInitial.js │ ├── watchDotFiles.js │ └── multiPaths.js ├── configs-add │ ├── singleFile.js │ ├── glob.js │ ├── dir.js │ └── multiWatchGroup.js ├── configs-change │ ├── commandTask.js │ ├── singleFile.js │ ├── commandTaskEnv.js │ ├── taskFileOptions.js │ ├── multipleCommandTasks.js │ ├── commandTaskWithTaskFile.js │ ├── multipleTaskFiles.js │ ├── multipleCommandTasksWithEnv.js │ ├── glob.js │ ├── multipleTaskFilesWithOptions.js │ ├── commandTasksWithTaskFiles.js │ └── commandTasksWithTaskFilesWithEnvOptions.js └── configs-unlink │ ├── singleFile.js │ ├── glob.js │ ├── dir.js │ ├── targetFiles.js │ ├── targetFilesArray.js │ ├── targetFilesFnModifyFilePath.js │ ├── targetFilesMultiple.js │ └── targetFilesMultipleFn.js ├── .notifyrc ├── .gitignore ├── screenshots ├── dev.png ├── build.png ├── compile.png ├── wsk-layout.ai ├── wsk-layout.png ├── wsk-watcher.png ├── example-project.png ├── npm-scripts-v1.png ├── npm-scripts-v2.png ├── npm-scripts-v1-output.png └── npm-scripts-v2-output.png ├── .travis.yml ├── .eslintrc ├── .editorconfig ├── src ├── notify.js ├── index.js ├── utils │ ├── exists.js │ └── existsSync.js ├── lib │ ├── filterForPaths.js │ ├── shortenWatchTree.js │ ├── validatePath.js │ ├── runTask.js │ ├── processWatchGroup.js │ ├── fullPathsToPrjPath.js │ ├── eventNotices.js │ ├── spawnCommand.js │ ├── handleEvent.js │ ├── printWatchTree.js │ ├── validateGroup.js │ └── initWatch.js └── watcher.js ├── CHANGELOG.md ├── DCO.MD ├── package.json ├── CONTRIBUTING.MD ├── HOW-DID-WE-GET-HERE.md ├── LICENSE └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | screenshots 2 | tests 3 | .notifyrc 4 | -------------------------------------------------------------------------------- /tests/taskFiles/brokenModule.js: -------------------------------------------------------------------------------- 1 | // Bad module 2 | var 5 3 | -------------------------------------------------------------------------------- /.notifyrc: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'time': '00:00:00.00' 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | tests/tmp 3 | coverage 4 | .nyc_output 5 | -------------------------------------------------------------------------------- /screenshots/dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloomberg/wsk/HEAD/screenshots/dev.png -------------------------------------------------------------------------------- /screenshots/build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloomberg/wsk/HEAD/screenshots/build.png -------------------------------------------------------------------------------- /screenshots/compile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloomberg/wsk/HEAD/screenshots/compile.png -------------------------------------------------------------------------------- /screenshots/wsk-layout.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloomberg/wsk/HEAD/screenshots/wsk-layout.ai -------------------------------------------------------------------------------- /screenshots/wsk-layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloomberg/wsk/HEAD/screenshots/wsk-layout.png -------------------------------------------------------------------------------- /screenshots/wsk-watcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloomberg/wsk/HEAD/screenshots/wsk-watcher.png -------------------------------------------------------------------------------- /tests/taskFiles/missingOnEvent.js: -------------------------------------------------------------------------------- 1 | module.exports = {foo: foo}; 2 | 3 | function foo () { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /screenshots/example-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloomberg/wsk/HEAD/screenshots/example-project.png -------------------------------------------------------------------------------- /screenshots/npm-scripts-v1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloomberg/wsk/HEAD/screenshots/npm-scripts-v1.png -------------------------------------------------------------------------------- /screenshots/npm-scripts-v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloomberg/wsk/HEAD/screenshots/npm-scripts-v2.png -------------------------------------------------------------------------------- /screenshots/npm-scripts-v1-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloomberg/wsk/HEAD/screenshots/npm-scripts-v1-output.png -------------------------------------------------------------------------------- /screenshots/npm-scripts-v2-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloomberg/wsk/HEAD/screenshots/npm-scripts-v2-output.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "7" 5 | - "6" 6 | - "5" 7 | os: osx 8 | after_success: npm run coverage 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "rules": { 4 | "semi": ["error", "always"], 5 | "max-len": ["error", 120] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /tests/clis/report.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* -------------------------------------------- 4 | * Set our exit code to something custom so we can verify this script ran 5 | * 6 | */ 7 | process.exit(100); 8 | -------------------------------------------------------------------------------- /src/notify.js: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------- 2 | * 3 | * Load the wsk-notify module 4 | * 5 | * -------------------------------------------- 6 | */ 7 | 8 | module.exports = require('wsk-notify'); 9 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------- 2 | * 3 | * Export our modules to the main library 4 | * 5 | * -------------------------------------------- 6 | */ 7 | 8 | exports.notify = require('./notify.js'); 9 | exports.watcher = require('./watcher.js'); 10 | -------------------------------------------------------------------------------- /tests/taskFiles/report.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | module.exports = {onEvent}; 3 | 4 | function onEvent (eventType, filePath, opts, cb) { 5 | var shortPath = __filename.replace(path.resolve('./') + '/', ''); 6 | cb(null, {eventType: eventType, filePath: filePath, options: opts, taskFilePath: shortPath}); 7 | } 8 | -------------------------------------------------------------------------------- /tests/taskFiles/report-2.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | module.exports = {onEvent}; 3 | 4 | function onEvent (eventType, filePath, opts, cb) { 5 | setTimeout(function () { 6 | var shortPath = __filename.replace(path.resolve('./') + '/', ''); 7 | cb(null, {eventType: eventType, filePath: filePath, options: opts, taskFilePath: shortPath}); 8 | }, 500); 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/exists.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | 5 | module.exports = function exists (filename, cb) { 6 | fs.access(filename, function (err) { 7 | var exists; 8 | if (err && err.code === 'ENOENT') { 9 | exists = false; 10 | err = null; 11 | } else if (!err) { 12 | exists = true; 13 | } 14 | cb(err, exists); 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /src/lib/filterForPaths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var isGlob = require('is-glob'); 4 | 5 | module.exports = function filterForPaths (warnIfMissingPath) { 6 | return function (pathCandidate) { 7 | if (isGlob(pathCandidate)) { 8 | return false; 9 | } else if (warnIfMissingPath === false) { 10 | return false; 11 | } else { 12 | return true; 13 | } 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/existsSync.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | 5 | // Defer to fs.existsSync if it exists 6 | module.exports = function existsSync (filename) { 7 | if (fs.existsSync) { 8 | return fs.existsSync(filename); 9 | } else { 10 | try { 11 | fs.accessSync(filename); 12 | return true; 13 | } catch (ex) { 14 | return false; 15 | } 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /tests/clis/report-2.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* -------------------------------------------- 4 | * Set our exit code to something custom so we can verify this script ran. 5 | * Set this in a timeout to ensure it finishes second to make our tests better. 6 | * Not great but it helps verify we're running the file we want. 7 | * 8 | */ 9 | 10 | setTimeout(function () { 11 | process.exit(100); 12 | }, 500); 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | === 3 | 4 | ## 1.0.1 5 | 6 | > 2018-01-04 7 | 8 | Package.json changes. 9 | 10 | * Change the `engines` field to be `>5.0.0` from `^4.4.3` to solve [#5](https://github.com/bloomberg/wsk/issues/5). Update the tests timeout 11 | * [180a1c26eb29f884034dbdb9410bcb8478de2f1d](https://github.com/bloomberg/wsk/commit/180a1c26eb29f884034dbdb9410bcb8478de2f1d) 12 | 13 | ## 1.0.0 14 | 15 | > 2017-11-17 16 | 17 | Initial release. 18 | -------------------------------------------------------------------------------- /tests/utils/testPath.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* -------------------------------------------- 4 | * 5 | * Simple file path abbrebiation to the `tests/` dir 6 | * 7 | * -------------------------------------------- 8 | */ 9 | 10 | var path = require('path'); 11 | 12 | module.exports = function testPath () { 13 | var args = Array.prototype.slice.call(arguments); 14 | return path.join.apply(null, ['tests'].concat(args)); 15 | // return path.join('tests', ...arguments); 16 | }; 17 | -------------------------------------------------------------------------------- /tests/utils/tmpPath.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* -------------------------------------------- 4 | * 5 | * Simple file path abbrebiation to the `tests/fixtures/` dir 6 | * 7 | * -------------------------------------------- 8 | */ 9 | 10 | var path = require('path'); 11 | var testPath = require('./testPath'); 12 | 13 | module.exports = function fixturesPath () { 14 | var args = Array.prototype.slice.call(arguments); 15 | return path.join.apply(null, [testPath(), 'tmp'].concat(args)); 16 | }; 17 | -------------------------------------------------------------------------------- /tests/utils/printMismatches.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* -------------------------------------------- 4 | * 5 | * Helper function to print out the actual result when `toTest` does not equal `expected`. 6 | * Useful for colored strings, which we have a lot of. 7 | * 8 | * -------------------------------------------- 9 | */ 10 | var io = require('indian-ocean'); 11 | 12 | module.exports = function printMismatches (toTest, info) { 13 | var str = JSON.stringify(toTest); 14 | io.writeDataSync('tests/tmp/snippets/' + [info.name, info.which, info.counter].join('_'), str, {makeDirs: true}); 15 | }; 16 | -------------------------------------------------------------------------------- /src/lib/shortenWatchTree.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* -------------------------------------------- 4 | * 5 | * Convert the long file paths to project relative 6 | * 7 | * -------------------------------------------- 8 | */ 9 | var fullPathsToPrjPath = require('./fullPathsToPrjPath'); 10 | 11 | module.exports = function shortenWatchTree (watchTree) { 12 | var shortWatchTree = {}; 13 | Object.keys(watchTree).forEach(watchedDir => { 14 | var shortPathRoot = fullPathsToPrjPath(watchedDir); 15 | shortWatchTree[shortPathRoot] = watchTree[watchedDir]; 16 | }); 17 | return shortWatchTree; 18 | }; 19 | -------------------------------------------------------------------------------- /tests/configs-errors/noEventsArray.js: -------------------------------------------------------------------------------- 1 | var tmpPath = require('../utils/tmpPath'); 2 | 3 | var filePath = tmpPath('errors', 'noEventsArray', 'file.test'); 4 | 5 | module.exports = { 6 | watchGroups: [ 7 | { 8 | serviceName: 'basic', 9 | path: filePath, 10 | warnIfMissingPath: false 11 | } 12 | ], 13 | expected: { 14 | onPublicInitDone: [{ 15 | error: 'Error: No events specified for `basic` service.', 16 | notification: null 17 | }], 18 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[31mError: No events specified for `basic` service.\u001b[39m'] 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /tests/utils/unlinkFilesSync.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* -------------------------------------------- 4 | * 5 | * Delete files or dirs so we can test the watcher reports these events. 6 | * 7 | * -------------------------------------------- 8 | */ 9 | 10 | var fse = require('fs-extra'); 11 | var normalizeWatchGroupPath = require('./normalizeWatchGroupPath'); 12 | 13 | module.exports = function unlinkFilesSync (watchGroups, cb) { 14 | watchGroups.forEach(normalizeWatchGroupPath(processPath)); 15 | }; 16 | 17 | function processPath (err, filePaths) { 18 | if (err) { 19 | console.error(err); 20 | return; 21 | } 22 | filePaths.forEach(function (filePath) { 23 | fse.remove(filePath); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /tests/utils/normalizeWatchGroupPath.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* -------------------------------------------- 4 | * 5 | * Normalize the `path` in the watch group into an array of paths depending on whether it's a simple string, glob or we're manually setting the path via `testPath`. 6 | * 7 | * -------------------------------------------- 8 | */ 9 | 10 | var glob = require('glob'); 11 | var isGlob = require('is-glob'); 12 | 13 | module.exports = function (cb) { 14 | return function (watchGroup) { 15 | if (isGlob(watchGroup.path) === false) { 16 | cb(null, [watchGroup.path]); 17 | } else if (watchGroup.testPath) { 18 | cb(null, [watchGroup.testPath]); 19 | } else { 20 | glob(watchGroup.path, cb); 21 | } 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/lib/validatePath.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var exists = require('../utils/exists'); 4 | var _ = require('underscore'); 5 | 6 | module.exports = function validatePath (messageValue) { 7 | return function (pathToTest, cb) { 8 | exists(pathToTest, function (err, exists) { 9 | /* istanbul ignore if */ 10 | if (err) { 11 | let nS = { 12 | message: 'Error testing whether path exists...', 13 | value: pathToTest, 14 | display: 'error', 15 | error: err 16 | }; 17 | cb(nS); 18 | } else if (exists === false) { 19 | var nS = _.extend({ 20 | display: 'warn' 21 | }, messageValue); 22 | cb(null, nS); 23 | } else { 24 | cb(); 25 | } 26 | }); 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /tests/configs-errors/brokenModule.js: -------------------------------------------------------------------------------- 1 | var testPath = require('../utils/testPath'); 2 | var tmpPath = require('../utils/tmpPath'); 3 | 4 | var filePath = tmpPath('errors', 'brokenModule', 'file.test'); 5 | 6 | var errMsg = 'Error: Could not require taskFile in `basic` watchGroup... tests/taskFiles/brokenModule.js' 7 | 8 | module.exports = { 9 | watchGroups: [ 10 | { 11 | serviceName: 'basic', 12 | path: filePath, 13 | warnIfMissingPath: false, 14 | events: [ 15 | { 16 | type: 'add', 17 | taskFiles: testPath('taskFiles', 'brokenModule.js') 18 | } 19 | ] 20 | } 21 | ], 22 | expected: { 23 | onPublicInitDone: [{ 24 | error: errMsg, 25 | notification: null 26 | }], 27 | onInit: [errMsg] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /tests/configs-errors/noTaskFilesOrCommands.js: -------------------------------------------------------------------------------- 1 | var tmpPath = require('../utils/tmpPath'); 2 | 3 | var filePath = tmpPath('errors', 'noTaskFilesOrCommands', 'file.test'); 4 | 5 | module.exports = { 6 | watchGroups: [ 7 | { 8 | serviceName: 'basic', 9 | path: filePath, 10 | events: [ 11 | { 12 | type: 'add' 13 | } 14 | ] 15 | } 16 | ], 17 | expected: { 18 | onPublicInitDone: [{ 19 | error: 'Error: No `taskFiles` or `commands` set for `basic` service\'s event... add', 20 | notification: null 21 | }], 22 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[31mError: No `taskFiles` or `commands` set for `basic` service\'s event...\u001b[39m \u001b[1madd\u001b[22m'] 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /tests/test-add.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | var getTestFiles = require('./utils/getTestFiles'); 3 | var writeFilesSync = require('./utils/writeFilesSync'); 4 | var testEach = require('./utils/testEach'); 5 | 6 | /* -------------------------------------------- 7 | * Load all modules in corresponding config directory for this topic 8 | */ 9 | var topic = 'add'; 10 | var testFiles = getTestFiles(topic); 11 | 12 | function on (which, counters, watchGroups, cb) { 13 | counters[which] = 0; 14 | return function (err, notification) { 15 | if (err) { 16 | cb(err); 17 | } else { 18 | cb(null, notification, which, counters[which]++); 19 | if (which === 'onInit') { 20 | writeFilesSync(watchGroups); 21 | } 22 | } 23 | }; 24 | } 25 | 26 | testFiles.forEach(function (testFile) { 27 | test.cb(testFile.name, testEach(on, testFile)); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/test-change.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | var getTestFiles = require('./utils/getTestFiles'); 3 | var writeFilesSync = require('./utils/writeFilesSync'); 4 | var testEach = require('./utils/testEach'); 5 | 6 | /* -------------------------------------------- 7 | * Load all modules in corresponding config directory for this topic 8 | */ 9 | var topic = 'change'; 10 | var testFiles = getTestFiles(topic); 11 | 12 | function on (which, counters, watchGroups, cb) { 13 | counters[which] = 0; 14 | return function (err, notification) { 15 | if (err) { 16 | cb(err); 17 | } else { 18 | cb(null, notification, which, counters[which]++); 19 | if (which === 'onInit') { 20 | writeFilesSync(watchGroups); 21 | } 22 | } 23 | }; 24 | } 25 | 26 | testFiles.forEach(function (testFile) { 27 | test.cb(testFile.name, testEach(on, testFile)); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/test-unlink.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | var getTestFiles = require('./utils/getTestFiles'); 3 | var unlinkFilesSync = require('./utils/unlinkFilesSync'); 4 | var testEach = require('./utils/testEach'); 5 | 6 | /* -------------------------------------------- 7 | * Load all modules in corresponding config directory for this topic 8 | */ 9 | var topic = 'unlink'; 10 | var testFiles = getTestFiles(topic); 11 | 12 | function on (which, counters, watchGroups, cb) { 13 | counters[which] = 0; 14 | return function (err, notification) { 15 | if (err) { 16 | cb(err); 17 | } else { 18 | cb(null, notification, which, counters[which]++); 19 | if (which === 'onInit') { 20 | unlinkFilesSync(watchGroups); 21 | } 22 | } 23 | }; 24 | } 25 | 26 | testFiles.forEach(function (testFile) { 27 | test.cb(testFile.name, testEach(on, testFile)); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/utils/writeFilesSync.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* -------------------------------------------- 4 | * 5 | * Create (or modify existing) files or dirs so we can test the watcher reports these events. 6 | * 7 | * -------------------------------------------- 8 | */ 9 | 10 | var io = require('indian-ocean'); 11 | var normalizeWatchGroupPath = require('./normalizeWatchGroupPath'); 12 | 13 | module.exports = function writeFilesSync (watchGroups, cb) { 14 | watchGroups.forEach(normalizeWatchGroupPath(processPath)); 15 | }; 16 | 17 | function processPath (err, filePaths) { 18 | if (err) { 19 | console.error(err); 20 | return; 21 | } 22 | filePaths.forEach(function (filePath) { 23 | if (/\/$/.test(filePath) === true) { 24 | io.makeDirectoriesSync(filePath); 25 | } else { 26 | io.writeDataSync(filePath, 'modified', {makeDirs: true}); 27 | } 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /tests/test-watch.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | // import sinon from 'sinon'; 3 | 4 | var getTestFiles = require('./utils/getTestFiles'); 5 | var testEach = require('./utils/testEach'); 6 | 7 | /* -------------------------------------------- 8 | * Load all modules in corresponding config directory for this topic 9 | */ 10 | var topic = 'watch'; 11 | var testFiles = getTestFiles(topic); 12 | 13 | function on (which, counters, watchGroups, cb) { 14 | counters[which] = 0; 15 | return function (err, notification) { 16 | if (err) { 17 | cb(err); 18 | } else { 19 | cb(null, notification, which, counters[which]++); 20 | } 21 | }; 22 | } 23 | 24 | // test.beforeEach(t => { 25 | // sinon.spy(console, 'log'); 26 | // }); 27 | 28 | // test.afterEach(t => { 29 | // console.log.restore(); 30 | // }); 31 | 32 | testFiles.forEach(function (testFile) { 33 | test.serial.cb(testFile.name, testEach(on, testFile)); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/test-errors.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | // import sinon from 'sinon'; 3 | var getTestFiles = require('./utils/getTestFiles'); 4 | var testEach = require('./utils/testEach'); 5 | 6 | /* -------------------------------------------- 7 | * Load all modules in corresponding config directory for this topic 8 | */ 9 | var topic = 'errors'; 10 | var testFiles = getTestFiles(topic); 11 | 12 | function on (which, counters, watchGroups, cb) { 13 | counters[which] = 0; 14 | return function (err, notification) { 15 | if (err) { 16 | cb(err, null, which, counters[which]++); 17 | } else { 18 | cb(null, notification, which, counters[which]++); 19 | } 20 | }; 21 | } 22 | 23 | // test.beforeEach(t => { 24 | // sinon.spy(console, 'log'); 25 | // }); 26 | 27 | // test.afterEach(t => { 28 | // console.log.restore(); 29 | // }); 30 | 31 | testFiles.forEach(function (testFile) { 32 | test.serial.cb(testFile.name, testEach(on, testFile)); 33 | }); 34 | -------------------------------------------------------------------------------- /tests/configs-errors/missingOnEvent.js: -------------------------------------------------------------------------------- 1 | var testPath = require('../utils/testPath'); 2 | var tmpPath = require('../utils/tmpPath'); 3 | 4 | var filePath = tmpPath('errors', 'missingOnEvent', 'file.test'); 5 | 6 | module.exports = { 7 | watchGroups: [ 8 | { 9 | serviceName: 'basic', 10 | path: filePath, 11 | warnIfMissingPath: false, 12 | events: [ 13 | { 14 | type: 'add', 15 | taskFiles: testPath('taskFiles', 'missingOnEvent.js') 16 | } 17 | ] 18 | } 19 | ], 20 | expected: { 21 | onPublicInitDone: [{ 22 | error: 'Error: Task file for `basic` does not export an `onEvent` function... tests/taskFiles/missingOnEvent.js', 23 | notification: null 24 | }], 25 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[31mError: Task file for `basic` does not export an `onEvent` function...\u001b[39m \u001b[1mtests/taskFiles/missingOnEvent.js\u001b[22m'] 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/lib/runTask.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('underscore'); 4 | 5 | module.exports = function runTask (filePath, evt, cb) { 6 | return function (taskInfo) { 7 | // We pass it the event type and the file path in case that task file has different directives accordingly 8 | // Per convention, task files can have an `onEvent` function 9 | // Change the filePath to any user-specified filePath on this event configuration 10 | if (evt.targetFiles) { 11 | if (_.isString(evt.targetFiles) || _.isArray(evt.targetFiles)) { 12 | filePath = evt.targetFiles; 13 | } else if (_.isFunction(evt.targetFiles)) { 14 | filePath = evt.targetFiles(filePath); 15 | } 16 | } 17 | // Also allow for a list of things 18 | if (!_.isArray(filePath)) { 19 | filePath = [filePath]; 20 | } 21 | // Call the `onEvent` of this task file 22 | filePath.forEach(function (singleFilePath, i) { 23 | taskInfo.module.onEvent(evt.type, singleFilePath, evt.options, cb.onTaskFileEvent); 24 | }); 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /src/watcher.js: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------- 2 | * 3 | * Watch files and run task files when they change 4 | * 5 | * -------------------------------------------- 6 | */ 7 | var _ = require('underscore'); 8 | var queue = require('d3-queue').queue; 9 | var processWatchGroup = require('./lib/processWatchGroup'); 10 | var identity = function () {}; 11 | 12 | function add (watchGroups, onDone, testsCb) { 13 | if (!_.isArray(watchGroups)) { 14 | watchGroups = [watchGroups]; 15 | } 16 | onDone = onDone || identity; 17 | // Add this hook for testing 18 | testsCb = _.defaults({}, testsCb || {}, { 19 | onInit: identity, 20 | onEvent: identity, 21 | onCommandEvent: identity, 22 | onTaskFileEvent: identity 23 | }); 24 | 25 | var q = queue(); 26 | 27 | // Loop through each file directive 28 | watchGroups.forEach(function (watchGroup) { 29 | q.defer(processWatchGroup(testsCb), watchGroup); 30 | }); 31 | q.awaitAll(function (err, onDones) { 32 | onDone(err, onDones); 33 | }); 34 | } 35 | 36 | module.exports = { add }; 37 | -------------------------------------------------------------------------------- /src/lib/processWatchGroup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('underscore'); 4 | var notify = require('../notify'); 5 | var initWatch = require('./initWatch'); 6 | var validateGroup = require('./validateGroup'); 7 | 8 | module.exports = function processWatchGroup (cb) { 9 | return function (watchGroup, onDone) { 10 | // Set up some defaults 11 | var withDefaults = _.defaults({}, watchGroup, {warnIfMissingPath: true}); 12 | if (!_.isArray(withDefaults.path)) { 13 | withDefaults.path = [withDefaults.path]; 14 | } 15 | validateGroup(withDefaults, { 16 | fail: function (notifySettings) { 17 | var n = notify(notifySettings); 18 | cb.onInit(null, n); 19 | onDone(new Error(notifySettings.message + (notifySettings.value ? ' ' + notifySettings.value : ''))); 20 | }, 21 | warn: function (notifySettings) { 22 | var n = notify(notifySettings); 23 | cb.onInit(null, n); 24 | }, 25 | proceed: function () { 26 | initWatch(withDefaults, onDone, cb); 27 | } 28 | }); 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /tests/utils/getTestFiles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* -------------------------------------------- 4 | * 5 | * Read in our individual test files from our `*-configs` folders, 6 | * skipping files that have `skip` in their name, which is useful when writing tests to just focus on the new ones 7 | * 8 | * -------------------------------------------- 9 | */ 10 | var path = require('path'); 11 | var io = require('indian-ocean'); 12 | var chalk = require('chalk'); 13 | 14 | module.exports = function getTestFiles (topic) { 15 | var skipping = io.readdirFilterSync(path.join('tests', 'configs-' + topic), {include: /skip\.js/}) 16 | .map(d => chalk.gray('Skipping... ') + d); 17 | console.log('Skipping', skipping.length); 18 | console.log(skipping.join('\n')); 19 | return io.readdirFilterSync(path.join('tests', 'configs-' + topic), {include: 'js', exclude: /skip/}) 20 | .map(filePath => { 21 | return { 22 | name: path.basename(filePath).replace('.js', ''), 23 | file: require('../' + path.join('configs-' + topic, filePath)) 24 | }; 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /src/lib/fullPathsToPrjPath.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* -------------------------------------------- 4 | * 5 | * Take a string or array of strings that looks like `/Users/my-name/code/my-project/src/js/main.js`, or a list of them 6 | * And return the path relative to the my-project base directory such as `src/js/main.js` 7 | * 8 | * -------------------------------------------- 9 | */ 10 | var path = require('path'); 11 | var _ = require('underscore'); 12 | 13 | var baseDir = path.resolve('./'); 14 | 15 | module.exports = function fullPathsToPrjPath (filePaths) { 16 | // We were given an array of file paths, return an array 17 | // Otherwise return the converted string 18 | if (_.isArray(filePaths)) { 19 | return filePaths.filter(d => d).map(convert); 20 | } else { 21 | return convert(filePaths); 22 | } 23 | 24 | // Do a string replacement on our `baseDir`, which is `/Users/my-name/code/project`, that replaces it with an empty string 25 | // Optionally remove a trailing slash if it exists 26 | function convert (filePath) { 27 | return filePath.replace(new RegExp(baseDir + '(\\' + path.sep + '?)'), ''); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /tests/configs-watch/hideAll.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | io.writeDataSync(tmpPath('watch', 'hideAll', 'file.test'), 'file', {makeDirs: true}); 6 | io.writeDataSync(tmpPath('watch', 'hideAll', 'subdir', 'file-2.test'), 'file', {makeDirs: true}); 7 | 8 | var glob = tmpPath('watch', 'hideAll', '**', '*'); 9 | 10 | var taskFilePath = testPath('taskFiles', 'report.js'); 11 | 12 | module.exports = { 13 | watchGroups: [ 14 | { 15 | serviceName: 'basic', 16 | path: glob, 17 | displayOptions: { 18 | hideAll: true 19 | }, 20 | events: [ 21 | { 22 | type: 'add', 23 | taskFiles: taskFilePath 24 | }, 25 | { 26 | type: 'change', 27 | taskFiles: taskFilePath 28 | } 29 | ] 30 | } 31 | ], 32 | expected: { 33 | onPublicInitDone: [{ 34 | error: null, 35 | notification: [{'tests/tmp/watch/hideAll': ['file.test', 'subdir'], 'tests/tmp/watch/hideAll/subdir': ['file-2.test']}] 36 | }], 37 | onInit: [''] 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /tests/configs-errors/nonExistentFile.js: -------------------------------------------------------------------------------- 1 | var testPath = require('../utils/testPath'); 2 | var tmpPath = require('../utils/tmpPath'); 3 | 4 | var filePath = tmpPath('errors', 'nonExistentFile', 'file.test'); 5 | 6 | module.exports = { 7 | watchGroups: [ 8 | { 9 | serviceName: 'basic', 10 | path: filePath, 11 | events: [ 12 | { 13 | type: 'add', 14 | taskFiles: testPath('taskFiles', 'report.js') 15 | } 16 | ] 17 | } 18 | ], 19 | expected: { 20 | onPublicInitDone: [{ 21 | error: null, 22 | notification: [{}] 23 | }], 24 | onInit: [ 25 | '[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[1m\u001b[33mWarning: the file you asked to watch does not exist...\u001b[39m\u001b[22m \u001b[1mtests/tmp/errors/nonExistentFile/file.test\u001b[0m\nTo disable this warning, set `warnIfMissingPath: false` in your watchGroup config.\n\u001b[0m\u001b[22m', 26 | '[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/errors/nonExistentFile/file.test\u001b[22m' 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /tests/configs-watch/singleFileAddChange.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var filePath = tmpPath('watch', 'singleFileAddChange', 'file.test'); 6 | io.writeDataSync(filePath, 'file', {makeDirs: true}); 7 | 8 | var taskFilePath = testPath('taskFiles', 'report.js'); 9 | 10 | module.exports = { 11 | watchGroups: [ 12 | { 13 | serviceName: 'basic', 14 | path: filePath, 15 | events: [ 16 | { 17 | type: 'add', 18 | taskFiles: taskFilePath 19 | }, 20 | { 21 | type: 'change', 22 | taskFiles: taskFilePath 23 | } 24 | ] 25 | } 26 | ], 27 | expected: { 28 | onPublicInitDone: [{ 29 | error: null, 30 | notification: [{'tests/tmp/watch/singleFileAddChange': ['file.test']}] 31 | }], 32 | onInit: ['[00:00:00.00 | wsk] Watching basic bundle... tests/tmp/watch/singleFileAddChange/file.test\n[00:00:00.00 | wsk] --- tests/tmp/watch/singleFileAddChange/file.test'] 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /DCO.MD: -------------------------------------------------------------------------------- 1 | Developer's Certificate of Origin 1.1 2 | 3 | By making a contribution to this project, I certify that: 4 | 5 | 1. The contribution was created in whole or in part by me and I 6 | have the right to submit it under the open source license 7 | indicated in the file; or 8 | 9 | 2. The contribution is based upon previous work that, to the best 10 | of my knowledge, is covered under an appropriate open source 11 | license and I have the right under that license to submit that 12 | work with modifications, whether created in whole or in part 13 | by me, under the same open source license (unless I am 14 | permitted to submit under a different license), as indicated 15 | in the file; or 16 | 17 | 3. The contribution was provided directly to me by some other 18 | person who certified (1), (2) or (3) and I have not modified 19 | it. 20 | 21 | 4. I understand and agree that this project and the contribution 22 | are public and that a record of the contribution (including all 23 | personal information I submit with it, including my sign-off) is 24 | maintained indefinitely and may be redistributed consistent with 25 | this project or the open source license(s) involved. 26 | -------------------------------------------------------------------------------- /tests/utils/testEach.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* -------------------------------------------- 4 | * 5 | * The shared testing function that runs each test config file. 6 | * Examines our expectations as described in the config file and sets up a test plan. 7 | * 8 | * -------------------------------------------- 9 | */ 10 | var _ = require('underscore'); 11 | var watcher = require('../../src/index').watcher; 12 | var onDone = require('./onDone'); 13 | 14 | module.exports = (on, testFile) => { 15 | return t => { 16 | var file = testFile.file; 17 | var expectedCallbacks = _.chain(file.expected).values().reduce(function (memo, val) { 18 | return val.length + memo; 19 | }, 0).value(); 20 | // We run two asserts per test, one for the err being null and the second for string equality 21 | t.plan(expectedCallbacks * 2); 22 | var thisDone = onDone(t, file, testFile.name, expectedCallbacks); 23 | var cbCounters = {}; 24 | var expectedFns = _.mapObject(file.expected, function (val, key) { 25 | return on(key, cbCounters, file.watchGroups, thisDone); 26 | }); 27 | watcher.add(file.watchGroups, expectedFns.onPublicInitDone, _.omit(expectedFns, 'onPublicInitDone')); 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /tests/configs-add/singleFile.js: -------------------------------------------------------------------------------- 1 | var testPath = require('../utils/testPath'); 2 | var tmpPath = require('../utils/tmpPath'); 3 | 4 | var filePath = tmpPath('add', 'singleFile', 'file.test'); 5 | var taskFilePath = testPath('taskFiles', 'report.js'); 6 | 7 | module.exports = { 8 | watchGroups: [ 9 | { 10 | serviceName: 'basic', 11 | path: filePath, 12 | warnIfMissingPath: false, 13 | events: [ 14 | { 15 | type: 'add', 16 | taskFiles: taskFilePath 17 | } 18 | ] 19 | } 20 | ], 21 | expected: { 22 | onPublicInitDone: [{ 23 | error: null, 24 | notification: [{}] 25 | }], 26 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/add/singleFile/file.test\u001b[22m'], 27 | onEvent: ['\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mNew basic file detected. \u001b[90mWatching file...\u001b[36m\u001b[39m \u001b[1mtests/tmp/add/singleFile/file.test\u001b[22m'], 28 | onTaskFileEvent: [{ 29 | eventType: 'add', 30 | filePath: filePath, 31 | taskFilePath: taskFilePath, 32 | options: undefined 33 | }] 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /tests/configs-watch/hideBoth.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | io.writeDataSync(tmpPath('watch', 'hideBoth', 'file.test'), 'file', {makeDirs: true}); 6 | io.writeDataSync(tmpPath('watch', 'hideBoth', 'subdir', 'file.test'), 'file', {makeDirs: true}); 7 | 8 | var filePath = tmpPath('watch', 'hideBoth', '**', '*'); 9 | 10 | var taskFilePath = testPath('taskFiles', 'report.js'); 11 | 12 | module.exports = { 13 | watchGroups: [ 14 | { 15 | serviceName: 'basic', 16 | path: filePath, 17 | displayOptions: { 18 | hideChildFiles: true, 19 | hideChildDirs: true 20 | }, 21 | events: [ 22 | { 23 | type: 'add', 24 | taskFiles: taskFilePath 25 | }, 26 | { 27 | type: 'change', 28 | taskFiles: taskFilePath 29 | } 30 | ] 31 | } 32 | ], 33 | expected: { 34 | onPublicInitDone: [{ 35 | error: null, 36 | notification: [{'tests/tmp/watch/hideBoth': ['file.test', 'subdir'], 'tests/tmp/watch/hideBoth/subdir': ['file.test']}] 37 | }], 38 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/watch/hideBoth/**/*\u001b[22m'] 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /tests/configs-add/glob.js: -------------------------------------------------------------------------------- 1 | var testPath = require('../utils/testPath'); 2 | var tmpPath = require('../utils/tmpPath'); 3 | 4 | var glob = tmpPath('add', 'glob', '*.test'); 5 | var testFilePath = tmpPath('add', 'glob', 'file.test'); 6 | 7 | var taskFilePath = testPath('taskFiles', 'report.js'); 8 | 9 | module.exports = { 10 | watchGroups: [ 11 | { 12 | serviceName: 'basic', 13 | path: glob, 14 | testPath: testFilePath, // Add some extra data here just for testing. This file will created to test our add event. 15 | events: [ 16 | { 17 | type: 'add', 18 | taskFiles: taskFilePath 19 | } 20 | ] 21 | } 22 | ], 23 | expected: { 24 | onPublicInitDone: [{ 25 | error: null, 26 | notification: [{}] 27 | }], 28 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/add/glob/*.test\u001b[22m'], 29 | onEvent: ['\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mNew basic file detected. \u001b[90mWatching file...\u001b[36m\u001b[39m \u001b[1mtests/tmp/add/glob/file.test\u001b[22m'], 30 | onTaskFileEvent: [{ 31 | eventType: 'add', 32 | filePath: testFilePath, 33 | taskFilePath: taskFilePath, 34 | options: undefined 35 | }] 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /tests/configs-add/dir.js: -------------------------------------------------------------------------------- 1 | var testPath = require('../utils/testPath'); 2 | var tmpPath = require('../utils/tmpPath'); 3 | 4 | var glob = tmpPath('add', 'dir', '*'); 5 | var testFilePath = tmpPath('add', 'dir', 'subdir/'); 6 | 7 | var taskFilePath = testPath('taskFiles', 'report.js'); 8 | 9 | module.exports = { 10 | watchGroups: [ 11 | { 12 | serviceName: 'basic', 13 | path: glob, 14 | testPath: testFilePath, // Add some extra data here just for testing. This file will created to test our add event. 15 | events: [ 16 | { 17 | type: 'addDir', 18 | taskFiles: taskFilePath 19 | } 20 | ] 21 | } 22 | ], 23 | expected: { 24 | onPublicInitDone: [{ 25 | error: null, 26 | notification: [{}] 27 | }], 28 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/add/dir/*\u001b[22m'], 29 | onEvent: ['\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mNew basic dir detected. \u001b[90mWatching dir...\u001b[36m\u001b[39m \u001b[1mtests/tmp/add/dir/subdir\u001b[22m'], 30 | onTaskFileEvent: [{ 31 | eventType: 'addDir', 32 | filePath: testPath('tmp', 'add', 'dir', 'subdir'), 33 | taskFilePath: taskFilePath, 34 | options: undefined 35 | }] 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/lib/eventNotices.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* -------------------------------------------- 4 | * 5 | * Specify what language to display based on the event type 6 | * 7 | * These are arrays so we can have the option of more than one notice per event 8 | * Their color will be determined by the `notifyType` that gets passed to our notify module 9 | * But you can also do your own coloring for some of these events where we want to tell the user more about what is going on in the background 10 | * If you would like to also include the originating service, you can include it in `{{service}}` and that will get replaced 11 | * For example, `New {{service}} file detected` will be replaced with `New html file detected` for the `html` service and `New static file detected` when run from the static file watcher. 12 | * 13 | * -------------------------------------------- 14 | */ 15 | var chalk = require('chalk'); 16 | 17 | module.exports = { 18 | add: [ 19 | {notifyType: 'change', msg: 'New {{service}} file detected. ' + chalk.gray('Watching file...')} 20 | ], 21 | addDir: [ 22 | {notifyType: 'change', msg: 'New {{service}} dir detected. ' + chalk.gray('Watching dir...')} 23 | ], 24 | change: [ 25 | {notifyType: 'change', msg: 'File changed...'} 26 | ], 27 | unlink: [ 28 | {notifyType: 'remove', msg: 'File removal detected...'} 29 | ], 30 | unlinkDir: [ 31 | {notifyType: 'remove', msg: 'Dir removal detected...'} 32 | ] 33 | }; 34 | -------------------------------------------------------------------------------- /tests/configs-change/commandTask.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var tmpFile = tmpPath('change', 'commandTask', 'file.test'); 6 | io.writeDataSync(tmpFile, 'file', {makeDirs: true}); 7 | 8 | var commandPath = testPath('clis', 'report.js'); 9 | 10 | module.exports = { 11 | watchGroups: [ 12 | { 13 | serviceName: 'basic', 14 | path: tmpFile, 15 | events: [ 16 | { 17 | type: 'change', 18 | commands: 'node ' + commandPath 19 | } 20 | ] 21 | } 22 | ], 23 | expected: { 24 | onPublicInitDone: [{ 25 | error: null, 26 | notification: [{ 'tests/tmp/change/commandTask': [ 'file.test' ] }] 27 | }], 28 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/change/commandTask/file.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/change/commandTask/file.test\u001b[22m'], 29 | onEvent: ['\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mFile changed...\u001b[39m \u001b[1mtests/tmp/change/commandTask/file.test\u001b[22m'], 30 | onCommandEvent: [{ 31 | code: 100, 32 | command: commandPath, 33 | env: undefined 34 | }] 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /tests/configs-watch/globAddChange.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var filePath = tmpPath('watch', 'globAddChange', 'file.test'); 6 | io.writeDataSync(filePath, 'file', {makeDirs: true}); 7 | var filePath2 = tmpPath('watch', 'globAddChange', 'file-2.test'); 8 | io.writeDataSync(filePath2, 'file', {makeDirs: true}); 9 | 10 | var glob = tmpPath('watch', 'globAddChange', '*.test'); 11 | 12 | var taskFilePath = testPath('taskFiles', 'report.js'); 13 | 14 | module.exports = { 15 | watchGroups: [ 16 | { 17 | serviceName: 'basic', 18 | path: glob, 19 | events: [ 20 | { 21 | type: 'add', 22 | taskFiles: taskFilePath 23 | }, 24 | { 25 | type: 'change', 26 | taskFiles: taskFilePath 27 | } 28 | ] 29 | } 30 | ], 31 | expected: { 32 | onPublicInitDone: [{ 33 | error: null, 34 | notification: [{ 'tests/tmp/watch/globAddChange': [ 'file-2.test', 'file.test' ] }] 35 | }], 36 | onInit: ['[00:00:00.00 | wsk] Watching basic bundle... tests/tmp/watch/globAddChange/*.test\n[00:00:00.00 | wsk] --- tests/tmp/watch/globAddChange/file-2.test\n[00:00:00.00 | wsk] --- tests/tmp/watch/globAddChange/file.test'] 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/lib/spawnCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var child = require('child_process'); 4 | var notify = require('../notify.js'); 5 | 6 | module.exports = function spawnCommand (subProcesses, evt, cb) { 7 | return function (command, i) { 8 | subProcesses[i] = child.spawn(command, {shell: true, stdio: 'inherit', env: evt.env}); 9 | /* istanbul ignore next */ 10 | subProcesses[i].on('error', function (err) { 11 | var msg = 'Error running command...'; 12 | notify({ 13 | message: msg, 14 | value: command, 15 | display: 'error', 16 | error: err 17 | }); 18 | cb.onCommandEvent(null, msg); 19 | }); 20 | subProcesses[i].on('close', function (code) { 21 | if (code === 127) { 22 | let en = notify({ 23 | message: 'Error running command...', 24 | value: command, 25 | display: 'error' 26 | }); 27 | cb.onCommandEvent(null, en); 28 | } 29 | var spawnArg; 30 | // Take just the filename which is handled differently in different version of node 31 | /* istanbul ignore if */ 32 | if (subProcesses[i].spawnargs.length === 1) { 33 | spawnArg = subProcesses[i].spawnargs[0]; 34 | } else { 35 | spawnArg = subProcesses[i].spawnargs[2]; 36 | } 37 | var fileParts = spawnArg.split(' '); 38 | cb.onCommandEvent(null, { command: fileParts[fileParts.length - 1], env: evt.env, code: code }); 39 | }); 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /tests/configs-change/singleFile.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var tmpFile = tmpPath('change', 'singleFile', 'file.test'); 6 | io.writeDataSync(tmpFile, 'file', {makeDirs: true}); 7 | 8 | var taskFilePath = testPath('taskFiles', 'report.js'); 9 | 10 | module.exports = { 11 | watchGroups: [ 12 | { 13 | serviceName: 'basic', 14 | path: tmpFile, 15 | events: [ 16 | { 17 | type: 'change', 18 | taskFiles: taskFilePath 19 | } 20 | ] 21 | } 22 | ], 23 | expected: { 24 | onPublicInitDone: [{ 25 | error: null, 26 | notification: [{ 'tests/tmp/change/singleFile': [ 'file.test' ] }] 27 | }], 28 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/change/singleFile/file.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/change/singleFile/file.test\u001b[22m'], 29 | onEvent: ['\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mFile changed...\u001b[39m \u001b[1mtests/tmp/change/singleFile/file.test\u001b[22m'], 30 | onTaskFileEvent: [{ 31 | eventType: 'change', 32 | filePath: tmpFile, 33 | taskFilePath: taskFilePath, 34 | options: undefined 35 | }] 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /tests/configs-errors/badCommand.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var filePath = tmpPath('errors', 'badCommand', 'file.test'); 6 | io.writeDataSync(filePath, 'file', {makeDirs: true}); 7 | 8 | module.exports = { 9 | watchGroups: [ 10 | { 11 | serviceName: 'basic', 12 | path: filePath, 13 | chokidarOptions: { 14 | ignoreInitial: false 15 | }, 16 | events: [ 17 | { 18 | type: 'add', 19 | commands: 'ndoe ' + testPath('clis', 'report.js') 20 | } 21 | ] 22 | } 23 | ], 24 | expected: { 25 | onPublicInitDone: [{ 26 | error: null, 27 | notification: [{ 'tests/tmp/errors/badCommand': [ 'file.test' ] }] 28 | }], 29 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/errors/badCommand/file.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/errors/badCommand/file.test\u001b[22m'], 30 | onCommandEvent: [ 31 | '[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[31mError running command...\u001b[39m \u001b[1mndoe tests/clis/report.js\u001b[22m', 32 | { 33 | code: 127, 34 | command: 'tests/clis/report.js', 35 | env: undefined 36 | } 37 | ] 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /tests/configs-unlink/singleFile.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var filePath = tmpPath('unlink', 'singleFile', 'file.test'); 6 | // Write the file we are going to be deleting 7 | io.writeDataSync(filePath, 'test', {makeDirs: true}); 8 | 9 | var taskFilePath = testPath('taskFiles', 'report.js'); 10 | 11 | module.exports = { 12 | watchGroups: [ 13 | { 14 | serviceName: 'basic', 15 | path: filePath, 16 | events: [ 17 | { 18 | type: 'unlink', 19 | taskFiles: taskFilePath 20 | } 21 | ] 22 | } 23 | ], 24 | expected: { 25 | onPublicInitDone: [{ 26 | error: null, 27 | notification: [{ 'tests/tmp/unlink/singleFile': [ 'file.test' ] }] 28 | }], 29 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/unlink/singleFile/file.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/unlink/singleFile/file.test\u001b[22m'], 30 | onEvent: ['\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mFile removal detected...\u001b[39m \u001b[1mtests/tmp/unlink/singleFile/file.test\u001b[22m'], 31 | onTaskFileEvent: [{ 32 | eventType: 'unlink', 33 | filePath: filePath, 34 | taskFilePath: taskFilePath, 35 | options: undefined 36 | }] 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /tests/configs-unlink/glob.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var filePath = tmpPath('unlink', 'glob', 'file.test'); 6 | // Write the file we are going to be deleting 7 | io.writeDataSync(filePath, 'test', {makeDirs: true}); 8 | var glob = tmpPath('unlink', 'glob', '*.test'); 9 | 10 | var taskFilePath = testPath('taskFiles', 'report.js'); 11 | 12 | module.exports = { 13 | watchGroups: [ 14 | { 15 | serviceName: 'basic', 16 | path: glob, 17 | events: [ 18 | { 19 | type: 'unlink', 20 | taskFiles: taskFilePath 21 | } 22 | ] 23 | } 24 | ], 25 | expected: { 26 | onPublicInitDone: [{ 27 | error: null, 28 | notification: [{ 'tests/tmp/unlink/glob': [ 'file.test' ] }] 29 | }], 30 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/unlink/glob/*.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/unlink/glob/file.test\u001b[22m'], 31 | onEvent: ['\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mFile removal detected...\u001b[39m \u001b[1mtests/tmp/unlink/glob/file.test\u001b[22m'], 32 | onTaskFileEvent: [{ 33 | eventType: 'unlink', 34 | filePath: filePath, 35 | taskFilePath: taskFilePath, 36 | options: undefined 37 | }] 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /tests/configs-change/commandTaskEnv.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var tmpFile = tmpPath('change', 'commandTaskEnv', 'file.test'); 6 | io.writeDataSync(tmpFile, 'file', {makeDirs: true}); 7 | 8 | var taskFilePath = testPath('clis', 'report.js'); 9 | 10 | module.exports = { 11 | watchGroups: [ 12 | { 13 | serviceName: 'basic', 14 | path: tmpFile, 15 | events: [ 16 | { 17 | type: 'change', 18 | commands: 'node ' + taskFilePath, 19 | env: { 20 | report: true 21 | } 22 | } 23 | ] 24 | } 25 | ], 26 | expected: { 27 | onPublicInitDone: [{ 28 | error: null, 29 | notification: [{ 'tests/tmp/change/commandTaskEnv': [ 'file.test' ] }] 30 | }], 31 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/change/commandTaskEnv/file.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/change/commandTaskEnv/file.test\u001b[22m'], 32 | onEvent: ['\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mFile changed...\u001b[39m \u001b[1mtests/tmp/change/commandTaskEnv/file.test\u001b[22m'], 33 | onCommandEvent: [{ 34 | code: 100, 35 | command: taskFilePath, 36 | env: { 37 | report: true 38 | } 39 | }] 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wsk", 3 | "version": "1.0.1", 4 | "description": "A minimal task runner system to complement your npm scripts.", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "clear-tmp": "rm -rf tests/tmp/*", 8 | "test": "nyc ava tests/test-*.js --timeout=300s -s -v", 9 | "coverage": "nyc report --reporter=text-lcov | coveralls", 10 | "pretest": "npm run clear-tmp", 11 | "precoverage": "npm run clear-tmp" 12 | }, 13 | "author": "Michael Keller", 14 | "license": "Apache-2.0", 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/bloomberg/wsk.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/bloomberg/wsk/issues" 21 | }, 22 | "homepage": "https://github.com/bloomberg/wsk#readme", 23 | "dependencies": { 24 | "chalk": "^1.1.1", 25 | "chokidar": "^1.4.3", 26 | "d3-queue": "^3.0.7", 27 | "underscore": "^1.8.3", 28 | "wsk-notify": "^1.0.0" 29 | }, 30 | "engines": { 31 | "node": ">=5.0.0" 32 | }, 33 | "nyc": { 34 | "exclude": [ 35 | "**/*tests/**/*", 36 | "**/src/utils/**/*" 37 | ] 38 | }, 39 | "devDependencies": { 40 | "ava": "^0.22.0", 41 | "coveralls": "^3.0.0", 42 | "eslint": "^4.8.0", 43 | "eslint-config-standard": "^10.2.1", 44 | "eslint-plugin-import": "^2.7.0", 45 | "eslint-plugin-node": "^5.2.0", 46 | "eslint-plugin-promise": "^3.6.0", 47 | "eslint-plugin-standard": "^3.0.1", 48 | "fs-extra": "^4.0.2", 49 | "indian-ocean": "^3.0.2", 50 | "is-glob": "^4.0.0", 51 | "nyc": "^11.2.1", 52 | "semistandard": "^11.0.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/configs-change/taskFileOptions.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var tmpFile = tmpPath('change', 'taskFileOptions', 'file.test'); 6 | io.writeDataSync(tmpFile, 'file', {makeDirs: true}); 7 | 8 | var taskFilePath = testPath('taskFiles', 'report.js'); 9 | 10 | module.exports = { 11 | watchGroups: [ 12 | { 13 | serviceName: 'basic', 14 | path: tmpFile, 15 | events: [ 16 | { 17 | type: 'change', 18 | taskFiles: taskFilePath, 19 | options: { 20 | test: true 21 | } 22 | } 23 | ] 24 | } 25 | ], 26 | expected: { 27 | onPublicInitDone: [{ 28 | error: null, 29 | notification: [{ 'tests/tmp/change/taskFileOptions': [ 'file.test' ] }] 30 | }], 31 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/change/taskFileOptions/file.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/change/taskFileOptions/file.test\u001b[22m'], 32 | onEvent: ['\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mFile changed...\u001b[39m \u001b[1mtests/tmp/change/taskFileOptions/file.test\u001b[22m'], 33 | onTaskFileEvent: [{ 34 | eventType: 'change', 35 | filePath: tmpFile, 36 | taskFilePath: taskFilePath, 37 | options: { 38 | test: true 39 | } 40 | }] 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /tests/configs-unlink/dir.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var filePath = tmpPath('unlink', 'dir', 'subdir/'); 6 | io.makeDirectoriesSync(filePath); 7 | var dirToRemove = tmpPath('unlink', 'dir', 'subdir'); 8 | 9 | var glob = tmpPath('unlink', 'dir', '**', '*'); 10 | 11 | var taskFilePath = testPath('taskFiles', 'report.js'); 12 | 13 | module.exports = { 14 | watchGroups: [ 15 | { 16 | serviceName: 'basic', 17 | path: glob, 18 | testPath: dirToRemove, 19 | events: [ 20 | { 21 | type: 'unlinkDir', 22 | taskFiles: taskFilePath 23 | } 24 | ] 25 | } 26 | ], 27 | expected: { 28 | onPublicInitDone: [{ 29 | error: null, 30 | notification: [{ 31 | 'tests/tmp/unlink/dir': [ 'subdir' ], 32 | 'tests/tmp/unlink/dir/subdir': [] 33 | }] 34 | }], 35 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/unlink/dir/**/*\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/unlink/dir/subdir/\u001b[22m'], 36 | onEvent: ['\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mDir removal detected...\u001b[39m \u001b[1mtests/tmp/unlink/dir/subdir\u001b[22m'], 37 | onTaskFileEvent: [{ 38 | eventType: 'unlinkDir', 39 | filePath: dirToRemove, 40 | taskFilePath: taskFilePath, 41 | options: undefined 42 | }] 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /tests/configs-errors/nonExistentFileInArray.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var filePath = tmpPath('errors', 'nonExistentFileInArray', 'file.test'); 6 | var filePath1 = tmpPath('errors', 'nonExistentFileInArray', 'file-2.test'); 7 | io.writeDataSync(filePath1, 'file', {makeDirs: true}); 8 | 9 | module.exports = { 10 | watchGroups: [ 11 | { 12 | serviceName: 'basic', 13 | path: [filePath, filePath1], 14 | events: [ 15 | { 16 | type: 'add', 17 | taskFiles: testPath('taskFiles', 'report.js') 18 | } 19 | ] 20 | } 21 | ], 22 | expected: { 23 | onPublicInitDone: [{ 24 | error: null, 25 | notification: [{ 'tests/tmp/errors/nonExistentFileInArray': [ 'file-2.test' ] }] 26 | }], 27 | onInit: [ 28 | '[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[1m\u001b[33mWarning: the file you asked to watch does not exist...\u001b[39m\u001b[22m \u001b[1mtests/tmp/errors/nonExistentFileInArray/file.test\u001b[0m\nTo disable this warning, set `warnIfMissingPath: false` in your watchGroup config.\n\u001b[0m\u001b[22m', 29 | '[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/errors/nonExistentFileInArray/file.test, tests/tmp/errors/nonExistentFileInArray/file-2.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/errors/nonExistentFileInArray/file-2.test\u001b[22m' 30 | ] 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /tests/configs-watch/singleFileAddChangeWatchInitial.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var filePath = tmpPath('watch', 'singleFileAddChangeWatchInitial', 'file.test'); 6 | io.writeDataSync(filePath, 'file', {makeDirs: true}); 7 | 8 | var taskFilePath = testPath('taskFiles', 'report.js'); 9 | 10 | module.exports = { 11 | watchGroups: [ 12 | { 13 | serviceName: 'basic', 14 | path: filePath, 15 | chokidarOptions: { 16 | ignoreInitial: false 17 | }, 18 | events: [ 19 | { 20 | type: 'add', 21 | taskFiles: taskFilePath 22 | }, 23 | { 24 | type: 'change', 25 | taskFiles: taskFilePath 26 | } 27 | ] 28 | } 29 | ], 30 | expected: { 31 | onPublicInitDone: [{ 32 | error: null, 33 | notification: [{'tests/tmp/watch/singleFileAddChangeWatchInitial': ['file.test']}] 34 | }], 35 | onInit: ['[00:00:00.00 | wsk] Watching basic bundle... tests/tmp/watch/singleFileAddChangeWatchInitial/file.test\n[00:00:00.00 | wsk] --- tests/tmp/watch/singleFileAddChangeWatchInitial/file.test'], 36 | onEvent: ['\n[00:00:00.00 | wsk] New basic file detected. Watching file... tests/tmp/watch/singleFileAddChangeWatchInitial/file.test'], 37 | onTaskFileEvent: [{ 38 | eventType: 'add', 39 | filePath: filePath, 40 | taskFilePath: taskFilePath, 41 | options: undefined 42 | }] 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /tests/configs-change/multipleCommandTasks.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var tmpFile = tmpPath('change', 'multipleCommandTasks', 'file.test'); 6 | io.writeDataSync(tmpFile, 'file', {makeDirs: true}); 7 | 8 | var commandPath = testPath('clis', 'report.js'); 9 | var commandPath2 = testPath('clis', 'report-2.js'); 10 | 11 | module.exports = { 12 | watchGroups: [ 13 | { 14 | serviceName: 'basic', 15 | path: tmpFile, 16 | events: [ 17 | { 18 | type: 'change', 19 | commands: [ 20 | 'node ' + commandPath, 21 | 'node ' + commandPath2 22 | ] 23 | } 24 | ] 25 | } 26 | ], 27 | expected: { 28 | onPublicInitDone: [{ 29 | error: null, 30 | notification: [{ 'tests/tmp/change/multipleCommandTasks': [ 'file.test' ] }] 31 | }], 32 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/change/multipleCommandTasks/file.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/change/multipleCommandTasks/file.test\u001b[22m'], 33 | onEvent: ['\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mFile changed...\u001b[39m \u001b[1mtests/tmp/change/multipleCommandTasks/file.test\u001b[22m'], 34 | onCommandEvent: [{ 35 | code: 100, 36 | command: commandPath, 37 | env: undefined 38 | }, { 39 | code: 100, 40 | command: commandPath2, 41 | env: undefined 42 | }] 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /tests/configs-unlink/targetFiles.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var filePath = tmpPath('unlink', 'targetFiles', 'file.test'); 6 | // Write the file we are going to be deleting 7 | io.writeDataSync(filePath, 'test', {makeDirs: true}); 8 | var glob = tmpPath('unlink', 'targetFiles', '*.test'); 9 | var targetPath = tmpPath('unlink', 'targetFiles', 'file-2.test'); 10 | 11 | var taskFilePath = testPath('taskFiles', 'report.js'); 12 | 13 | module.exports = { 14 | watchGroups: [ 15 | { 16 | serviceName: 'basic', 17 | path: glob, 18 | testPath: filePath, 19 | events: [ 20 | { 21 | type: 'unlink', 22 | taskFiles: taskFilePath, 23 | targetFiles: targetPath 24 | } 25 | ] 26 | } 27 | ], 28 | expected: { 29 | onPublicInitDone: [{ 30 | error: null, 31 | notification: [{ 'tests/tmp/unlink/targetFiles': [ 'file.test' ] }] 32 | }], 33 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/unlink/targetFiles/*.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/unlink/targetFiles/file.test\u001b[22m'], 34 | onEvent: ['\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mFile removal detected...\u001b[39m \u001b[1mtests/tmp/unlink/targetFiles/file.test\u001b[22m'], 35 | onTaskFileEvent: [{ 36 | eventType: 'unlink', 37 | filePath: targetPath, 38 | taskFilePath: taskFilePath, 39 | options: undefined 40 | }] 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /tests/configs-change/commandTaskWithTaskFile.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var tmpFile = tmpPath('change', 'commandTaskWithTaskFile', 'file.test'); 6 | io.writeDataSync(tmpFile, 'file', {makeDirs: true}); 7 | 8 | var commandPath = testPath('clis', 'report.js'); 9 | 10 | var taskFilePath = testPath('taskFiles', 'report.js'); 11 | 12 | module.exports = { 13 | watchGroups: [ 14 | { 15 | serviceName: 'basic', 16 | path: tmpFile, 17 | events: [ 18 | { 19 | type: 'change', 20 | commands: 'node ' + commandPath, 21 | taskFiles: taskFilePath 22 | } 23 | ] 24 | } 25 | ], 26 | expected: { 27 | onPublicInitDone: [{ 28 | error: null, 29 | notification: [{ 'tests/tmp/change/commandTaskWithTaskFile': [ 'file.test' ] }] 30 | }], 31 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/change/commandTaskWithTaskFile/file.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/change/commandTaskWithTaskFile/file.test\u001b[22m'], 32 | onEvent: ['\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mFile changed...\u001b[39m \u001b[1mtests/tmp/change/commandTaskWithTaskFile/file.test\u001b[22m'], 33 | onCommandEvent: [{ 34 | code: 100, 35 | command: commandPath, 36 | env: undefined 37 | }], 38 | onTaskFileEvent: [{ 39 | eventType: 'change', 40 | filePath: tmpFile, 41 | taskFilePath: taskFilePath, 42 | options: undefined 43 | }] 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /tests/configs-unlink/targetFilesArray.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var filePath = tmpPath('unlink', 'targetFilesArray', 'file.test'); 6 | // Write the file we are going to be deleting 7 | io.writeDataSync(filePath, 'test', {makeDirs: true}); 8 | var glob = tmpPath('unlink', 'targetFilesArray', '*.test'); 9 | var targetPath = tmpPath('unlink', 'targetFilesArray', 'file-2.test'); 10 | 11 | var taskFilePath = testPath('taskFiles', 'report.js'); 12 | 13 | module.exports = { 14 | watchGroups: [ 15 | { 16 | serviceName: 'basic', 17 | path: glob, 18 | testPath: filePath, 19 | events: [ 20 | { 21 | type: 'unlink', 22 | taskFiles: taskFilePath, 23 | targetFiles: [targetPath] 24 | } 25 | ] 26 | } 27 | ], 28 | expected: { 29 | onPublicInitDone: [{ 30 | error: null, 31 | notification: [{ 'tests/tmp/unlink/targetFilesArray': [ 'file.test' ] }] 32 | }], 33 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/unlink/targetFilesArray/*.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/unlink/targetFilesArray/file.test\u001b[22m'], 34 | onEvent: ['\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mFile removal detected...\u001b[39m \u001b[1mtests/tmp/unlink/targetFilesArray/file.test\u001b[22m'], 35 | onTaskFileEvent: [{ 36 | eventType: 'unlink', 37 | filePath: targetPath, 38 | taskFilePath: taskFilePath, 39 | options: undefined 40 | }] 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /tests/configs-change/multipleTaskFiles.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var tmpFile = tmpPath('change', 'multipleTaskFiles', 'file.test'); 6 | io.writeDataSync(tmpFile, 'file', {makeDirs: true}); 7 | 8 | var taskFilePath = testPath('taskFiles', 'report.js'); 9 | var taskFilePath2 = testPath('taskFiles', 'report-2.js'); 10 | 11 | module.exports = { 12 | watchGroups: [ 13 | { 14 | serviceName: 'basic', 15 | path: tmpFile, 16 | events: [ 17 | { 18 | type: 'change', 19 | taskFiles: [ 20 | taskFilePath, 21 | taskFilePath2 22 | ] 23 | } 24 | ] 25 | } 26 | ], 27 | expected: { 28 | onPublicInitDone: [{ 29 | error: null, 30 | notification: [{ 'tests/tmp/change/multipleTaskFiles': [ 'file.test' ] }] 31 | }], 32 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/change/multipleTaskFiles/file.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/change/multipleTaskFiles/file.test\u001b[22m'], 33 | onEvent: ['\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mFile changed...\u001b[39m \u001b[1mtests/tmp/change/multipleTaskFiles/file.test\u001b[22m'], 34 | onTaskFileEvent: [{ 35 | eventType: 'change', 36 | filePath: tmpFile, 37 | taskFilePath: taskFilePath, 38 | options: undefined 39 | }, { 40 | eventType: 'change', 41 | filePath: tmpFile, 42 | taskFilePath: taskFilePath2, 43 | options: undefined 44 | }] 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.MD: -------------------------------------------------------------------------------- 1 | # Contributing to `wsk` 2 | 3 | If you'd like to help us improve and extend this project, then we welcome your contributions! 4 | 5 | Below you will find some basic steps required to be able to contribute to the project. If you have any questions about this process or any other aspect of contributing to a Bloomberg open source project, feel free to send an email to opensource@bloomberg.net and we'll get your questions answered as quickly as we can. 6 | 7 | This library currently has *100%* test coverage. Pull Requests are always welcome, however they will only be accepted if the test coverage remains at 100%. 8 | 9 | ## Contribution Licensing 10 | 11 | Since `wsk` is distributed under the terms of the [Apache Version 2 license](LICENSE), contributions that you make are licensed under the same terms. In order for us to be able to accept your contributions, we will need explicit confirmation from you that you are able and willing to provide them under these terms, and the mechanism we use to do this is called a Developer's Certificate of Origin [DCO](DCO.md). This is very similar to the process used by the Linux(R) kernel, Samba, and many other major open source projects. 12 | 13 | To participate under these terms, all that you must do is include a line like the following as the last line of the commit message for each commit in your contribution: 14 | 15 | Signed-Off-By: Random J. Developer 16 | 17 | The simplest way to accomplish this is to add `-s` or `--signoff` to your `git commit` command. 18 | 19 | You must use your real name (sorry, no pseudonyms, and no anonymous contributions). 20 | 21 | ## Help / Documentation 22 | 23 | The [README](README.md) contains the entire API documentation. If your need more or different information, please create an [Issue](../../issues). 24 | -------------------------------------------------------------------------------- /src/lib/handleEvent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* -------------------------------------------- 4 | * 5 | * This function is run whenever a change event occurs 6 | * It will notify the user that the event happened 7 | * if a `taskFile` is specified, it will run that file 8 | * 9 | * -------------------------------------------- 10 | */ 11 | var notify = require('../notify.js'); 12 | var eventNotices = require('./eventNotices'); 13 | var fullPathsToPrjPath = require('./fullPathsToPrjPath'); 14 | 15 | var spawnCommand = require('./spawnCommand'); 16 | var runTask = require('./runTask'); 17 | 18 | module.exports = function handleEvent (evt, filePath, serviceName, cb) { 19 | // Report the action we're telling it to do according to the event type 20 | // Notification strings are defined above 21 | // For example, if we fire a `change` event, the language reported to the user is `File changed...` 22 | // See the comments for the `eventNotices` above for more info. 23 | eventNotices[evt.type].forEach(function (eventNoticeInfo) { 24 | var notification = notify({ 25 | message: eventNoticeInfo.msg.replace('{{service}}', serviceName), 26 | value: fullPathsToPrjPath(filePath), 27 | display: eventNoticeInfo.notifyType 28 | }); 29 | cb.onEvent(null, notification); 30 | }); 31 | 32 | // If supplied one or more file in `evt.taskFiles` and this file exists, 33 | // require it as a module and run its `render` function 34 | // Our compile modules are built to be run with defaults sensible 35 | // for running on change if nothing passed 36 | // In the future we could allow for functions to be defined as `taskFiles` 37 | // directly but that's not as organized as the current setup 38 | var subProcesses = {}; 39 | evt.commands.forEach(spawnCommand(subProcesses, evt, cb)); 40 | 41 | evt.taskFiles.forEach(runTask(filePath, evt, cb)); 42 | }; 43 | -------------------------------------------------------------------------------- /tests/configs-change/multipleCommandTasksWithEnv.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var tmpFile = tmpPath('change', 'multipleCommandTasksWithEnv', 'file.test'); 6 | io.writeDataSync(tmpFile, 'file', {makeDirs: true}); 7 | 8 | var commandPath = testPath('clis', 'report.js'); 9 | var commandPath2 = testPath('clis', 'report-2.js'); 10 | 11 | module.exports = { 12 | watchGroups: [ 13 | { 14 | serviceName: 'basic', 15 | path: tmpFile, 16 | events: [ 17 | { 18 | type: 'change', 19 | commands: [ 20 | 'node ' + commandPath, 21 | 'node ' + commandPath2 22 | ], 23 | env: { 24 | report: true 25 | } 26 | } 27 | ] 28 | } 29 | ], 30 | expected: { 31 | onPublicInitDone: [{ 32 | error: null, 33 | notification: [{ 'tests/tmp/change/multipleCommandTasksWithEnv': [ 'file.test' ] }] 34 | }], 35 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/change/multipleCommandTasksWithEnv/file.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/change/multipleCommandTasksWithEnv/file.test\u001b[22m'], 36 | onEvent: ['\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mFile changed...\u001b[39m \u001b[1mtests/tmp/change/multipleCommandTasksWithEnv/file.test\u001b[22m'], 37 | onCommandEvent: [{ 38 | code: 100, 39 | command: commandPath, 40 | env: { 41 | report: true 42 | } 43 | }, { 44 | code: 100, 45 | command: commandPath2, 46 | env: { 47 | report: true 48 | } 49 | }] 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /tests/configs-change/glob.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var tmpFile = tmpPath('change', 'glob', 'file.test'); 6 | io.writeDataSync(tmpFile, 'file', {makeDirs: true}); 7 | var tmpFile2 = tmpPath('change', 'glob', 'file-2.test'); 8 | io.writeDataSync(tmpFile2, 'file', {makeDirs: true}); 9 | var tmpFileGlob = tmpPath('change', 'glob', '*.test'); 10 | 11 | var taskFilePath = testPath('taskFiles', 'report.js'); 12 | 13 | module.exports = { 14 | watchGroups: [ 15 | { 16 | serviceName: 'basic', 17 | path: tmpFileGlob, 18 | testPath: tmpFile, // Add some extra data here just for testing. This file will created to test our add event. 19 | events: [ 20 | { 21 | type: 'change', 22 | taskFiles: taskFilePath 23 | } 24 | ] 25 | } 26 | ], 27 | expected: { 28 | onPublicInitDone: [{ 29 | error: null, 30 | notification: [{ 'tests/tmp/change/glob': ['file-2.test', 'file.test'] }] 31 | }], 32 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/change/glob/*.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/change/glob/file-2.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/change/glob/file.test\u001b[22m'], 33 | onEvent: ['\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mFile changed...\u001b[39m \u001b[1mtests/tmp/change/glob/file.test\u001b[22m'], 34 | onTaskFileEvent: [{ 35 | eventType: 'change', 36 | filePath: tmpFile, 37 | taskFilePath: taskFilePath, 38 | options: undefined 39 | }] 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /tests/configs-watch/hideChildFiles.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | io.writeDataSync(tmpPath('watch', 'hideChildFiles', 'file.test'), 'file', {makeDirs: true}); 6 | io.writeDataSync(tmpPath('watch', 'hideChildFiles', 'subdir', 'file.test'), 'file', {makeDirs: true}); 7 | io.writeDataSync(tmpPath('watch', 'hideChildFiles', 'subdir', 'subsubdir', 'file.test'), 'file', {makeDirs: true}); 8 | 9 | var filePath = tmpPath('watch', 'hideChildFiles', '**', '*'); 10 | 11 | var taskFilePath = testPath('taskFiles', 'report.js'); 12 | 13 | module.exports = { 14 | watchGroups: [ 15 | { 16 | serviceName: 'basic', 17 | path: filePath, 18 | displayOptions: { 19 | hideChildFiles: true 20 | }, 21 | events: [ 22 | { 23 | type: 'add', 24 | taskFiles: taskFilePath 25 | }, 26 | { 27 | type: 'change', 28 | taskFiles: taskFilePath 29 | } 30 | ] 31 | } 32 | ], 33 | expected: { 34 | onPublicInitDone: [{ 35 | error: null, 36 | notification: [{ 37 | 'tests/tmp/watch/hideChildFiles': ['file.test', 'subdir'], 38 | 'tests/tmp/watch/hideChildFiles/subdir': ['file.test', 'subsubdir'], 39 | 'tests/tmp/watch/hideChildFiles/subdir/subsubdir': ['file.test'] 40 | }] 41 | }], 42 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/watch/hideChildFiles/**/*\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/hideChildFiles/subdir/\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/hideChildFiles/subdir/subsubdir/\u001b[22m'] 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /tests/configs-unlink/targetFilesFnModifyFilePath.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var filePath = tmpPath('unlink', 'targetFilesFnModifyFilePath', 'file.test'); 6 | // Write the file we are going to be deleting 7 | io.writeDataSync(filePath, 'test', {makeDirs: true}); 8 | 9 | var glob = tmpPath('unlink', 'targetFilesFnModifyFilePath', '*.test'); 10 | var taskFilePath = testPath('taskFiles', 'report.js'); 11 | var targetPath = tmpPath('unlink', 'targetFilesFnModifyFilePath', 'file-2.test'); 12 | 13 | module.exports = { 14 | watchGroups: [ 15 | { 16 | serviceName: 'basic', 17 | path: glob, 18 | testPath: filePath, 19 | events: [ 20 | { 21 | type: 'unlink', 22 | taskFiles: taskFilePath, 23 | targetFiles: function (filePath) { 24 | return filePath.replace('file.test', 'file-2.test'); 25 | } 26 | } 27 | ] 28 | } 29 | ], 30 | onPublicInitDone: [{ 31 | error: null, 32 | notification: [{ 'tests/tmp/unlink/targetFilesFnModifyFilePath': [ 'file.test' ] }] 33 | }], 34 | expected: { 35 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/unlink/targetFilesFnModifyFilePath/*.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/unlink/targetFilesFnModifyFilePath/file.test\u001b[22m'], 36 | onEvent: ['\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mFile removal detected...\u001b[39m \u001b[1mtests/tmp/unlink/targetFilesFnModifyFilePath/file.test\u001b[22m'], 37 | onTaskFileEvent: [{ 38 | eventType: 'unlink', 39 | filePath: targetPath, 40 | taskFilePath: taskFilePath, 41 | options: undefined 42 | }] 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /tests/configs-change/multipleTaskFilesWithOptions.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var tmpFile = tmpPath('change', 'multipleTaskFilesWithOptions', 'file.test'); 6 | io.writeDataSync(tmpFile, 'file', {makeDirs: true}); 7 | 8 | var taskFilePath = testPath('taskFiles', 'report.js'); 9 | var taskFilePath2 = testPath('taskFiles', 'report-2.js'); 10 | 11 | module.exports = { 12 | watchGroups: [ 13 | { 14 | serviceName: 'basic', 15 | path: tmpFile, 16 | events: [ 17 | { 18 | type: 'change', 19 | taskFiles: [ 20 | taskFilePath, 21 | taskFilePath2 22 | ], 23 | options: { 24 | test: true 25 | } 26 | } 27 | ] 28 | } 29 | ], 30 | expected: { 31 | onPublicInitDone: [{ 32 | error: null, 33 | notification: [{ 'tests/tmp/change/multipleTaskFilesWithOptions': [ 'file.test' ] }] 34 | }], 35 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/change/multipleTaskFilesWithOptions/file.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/change/multipleTaskFilesWithOptions/file.test\u001b[22m'], 36 | onEvent: ['\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mFile changed...\u001b[39m \u001b[1mtests/tmp/change/multipleTaskFilesWithOptions/file.test\u001b[22m'], 37 | onTaskFileEvent: [{ 38 | eventType: 'change', 39 | filePath: tmpFile, 40 | taskFilePath: taskFilePath, 41 | options: { 42 | test: true 43 | } 44 | }, { 45 | eventType: 'change', 46 | filePath: tmpFile, 47 | taskFilePath: taskFilePath2, 48 | options: { 49 | test: true 50 | } 51 | }] 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /tests/utils/onDone.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* -------------------------------------------- 4 | * 5 | * Callback to test equality whenever `watcher.js` sends us back some even information 6 | * 7 | * -------------------------------------------- 8 | */ 9 | // var path = require('path'); 10 | var printMismatches = require('./printMismatches'); 11 | var _ = require('underscore'); 12 | 13 | module.exports = function onDone (t, file, name, expectedCallbacks) { 14 | var counter = 0; 15 | return function (err, notification, which, i) { 16 | counter++; 17 | if (which !== 'onPublicInitDone') { 18 | t.is(err, null, 'Error is not null'); 19 | let expectedList = file.expected[which]; 20 | let foundIndex = expectedList.findIndex(d => _.isEqual(d, notification)); 21 | let expected; 22 | if (foundIndex > -1) { 23 | expected = file.expected[which][foundIndex]; 24 | file.expected[which][foundIndex] = null; 25 | file.expected[which] = file.expected[which].filter(d => d); 26 | t.deepEqual(notification, expected, 'Log message doesn\'t match expected'); 27 | } else { 28 | printMismatches(notification, {name, which, counter}); 29 | t.deepEqual(notification, expectedList.join('\n'), 'Log message doesn\'t match expected'); 30 | } 31 | } else { 32 | if (err) { 33 | if (!_.isEqual(notification, file.expected[which][0].notification)) { 34 | printMismatches(notification, {name, which, counter: 'n'}); 35 | } 36 | t.is(err.message, file.expected[which][0].error, 'Error does not match'); 37 | } else { 38 | t.is(err, null, 'Error is not null'); 39 | } 40 | if (!_.isEqual(notification, file.expected[which][0].notification)) { 41 | printMismatches(notification, {name, which, counter: 'n'}); 42 | } 43 | t.deepEqual(notification, file.expected[which][0].notification, 'Message matches'); 44 | } 45 | if (counter === expectedCallbacks) { 46 | t.end(); 47 | } 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /tests/configs-watch/multiWatchGroup.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var filePath = tmpPath('watch', 'multiWatchGroup', 'file.test'); 6 | io.writeDataSync(filePath, 'file', {makeDirs: true}); 7 | 8 | var filePath2 = tmpPath('watch', 'multiWatchGroup', 'file-2.test'); 9 | io.writeDataSync(filePath2, 'file', {makeDirs: true}); 10 | 11 | var taskFilePath = testPath('taskFiles', 'report.js'); 12 | 13 | module.exports = { 14 | watchGroups: [ 15 | { 16 | serviceName: 'basic', 17 | path: filePath, 18 | warnIfMissingPath: false, 19 | events: [ 20 | { 21 | type: 'add', 22 | taskFiles: taskFilePath 23 | } 24 | ] 25 | }, 26 | { 27 | serviceName: 'basic', 28 | path: filePath2, 29 | warnIfMissingPath: false, 30 | events: [ 31 | { 32 | type: 'add', 33 | taskFiles: taskFilePath 34 | } 35 | ] 36 | } 37 | ], 38 | expected: { 39 | onPublicInitDone: [{ 40 | error: null, 41 | notification: [{ 42 | 'tests/tmp/watch/multiWatchGroup': ['file.test'] 43 | }, { 44 | 'tests/tmp/watch/multiWatchGroup': ['file-2.test'] 45 | }] 46 | }], 47 | onInit: [ 48 | '[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/watch/multiWatchGroup/file.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/multiWatchGroup/file.test\u001b[22m', 49 | '[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/watch/multiWatchGroup/file-2.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/multiWatchGroup/file-2.test\u001b[22m' 50 | ] 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /tests/configs-unlink/targetFilesMultiple.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var filePath = tmpPath('unlink', 'targetFilesMultiple', 'file.test'); 6 | // Write the file we are going to be deleting 7 | io.writeDataSync(filePath, 'test', {makeDirs: true}); 8 | 9 | var glob = tmpPath('unlink', 'targetFilesMultiple', '*.test'); 10 | 11 | var taskFilePath = testPath('taskFiles', 'report.js'); 12 | var targetFile2 = tmpPath('unlink', 'targetFilesMultiple', 'file-2.test'); 13 | var targetFile3 = tmpPath('unlink', 'targetFilesMultiple', 'file-3.test'); 14 | 15 | module.exports = { 16 | watchGroups: [ 17 | { 18 | serviceName: 'basic', 19 | path: glob, 20 | testPath: filePath, 21 | events: [ 22 | { 23 | type: 'unlink', 24 | taskFiles: taskFilePath, 25 | targetFiles: [ 26 | targetFile2, 27 | targetFile3 28 | ] 29 | } 30 | ] 31 | } 32 | ], 33 | expected: { 34 | onPublicInitDone: [{ 35 | error: null, 36 | notification: [{ 'tests/tmp/unlink/targetFilesMultiple': [ 'file.test' ] }] 37 | }], 38 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/unlink/targetFilesMultiple/*.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/unlink/targetFilesMultiple/file.test\u001b[22m'], 39 | onEvent: ['\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mFile removal detected...\u001b[39m \u001b[1mtests/tmp/unlink/targetFilesMultiple/file.test\u001b[22m'], 40 | onTaskFileEvent: [{ 41 | eventType: 'unlink', 42 | filePath: targetFile2, 43 | taskFilePath: taskFilePath, 44 | options: undefined 45 | }, { 46 | eventType: 'unlink', 47 | filePath: targetFile3, 48 | taskFilePath: taskFilePath, 49 | options: undefined 50 | }] 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /tests/configs-watch/ignoreDotFiles.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var filePath = tmpPath('watch', 'ignoreDotFiles', 'file.test'); 6 | var filePath2 = tmpPath('watch', 'ignoreDotFiles', '.file-2.test'); 7 | var filePath3 = tmpPath('watch', 'ignoreDotFiles', 'subdir', 'file.test'); 8 | var filePath4 = tmpPath('watch', 'ignoreDotFiles', 'subdir', '.file-2.test'); 9 | 10 | io.writeDataSync(filePath, 'file', {makeDirs: true}); 11 | io.writeDataSync(filePath2, 'file', {makeDirs: true}); 12 | io.writeDataSync(filePath3, 'file', {makeDirs: true}); 13 | io.writeDataSync(filePath4, 'file', {makeDirs: true}); 14 | 15 | var glob = tmpPath('watch', 'ignoreDotFiles', '**', '*'); 16 | 17 | var taskFilePath = testPath('taskFiles', 'report.js'); 18 | 19 | module.exports = { 20 | watchGroups: [ 21 | { 22 | serviceName: 'basic', 23 | path: glob, 24 | events: [ 25 | { 26 | type: 'add', 27 | taskFiles: taskFilePath 28 | }, 29 | { 30 | type: 'change', 31 | taskFiles: taskFilePath 32 | } 33 | ] 34 | } 35 | ], 36 | expected: { 37 | onPublicInitDone: [{ 38 | error: null, 39 | notification: [{ 40 | 'tests/tmp/watch/ignoreDotFiles': ['file.test', 'subdir'], 41 | 'tests/tmp/watch/ignoreDotFiles/subdir': ['file.test'] 42 | }] 43 | }], 44 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/watch/ignoreDotFiles/**/*\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/ignoreDotFiles/file.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/ignoreDotFiles/subdir/\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/ignoreDotFiles/subdir/file.test\u001b[22m'] 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /tests/configs-unlink/targetFilesMultipleFn.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var filePath = tmpPath('unlink', 'targetFilesMultipleFn', 'file.test'); 6 | // Write the file we are going to be deleting 7 | io.writeDataSync(filePath, 'test', {makeDirs: true}); 8 | 9 | var glob = tmpPath('unlink', 'targetFilesMultipleFn', '*.test'); 10 | var taskFilePath = testPath('taskFiles', 'report.js'); 11 | 12 | var targetFile2 = tmpPath('unlink', 'targetFilesMultipleFn', 'file-2.test'); 13 | var targetFile3 = tmpPath('unlink', 'targetFilesMultipleFn', 'file-3.test'); 14 | 15 | module.exports = { 16 | watchGroups: [ 17 | { 18 | serviceName: 'basic', 19 | path: glob, 20 | testPath: filePath, 21 | events: [ 22 | { 23 | type: 'unlink', 24 | taskFiles: taskFilePath, 25 | targetFiles: function (filePath) { 26 | return [ 27 | targetFile2, 28 | targetFile3 29 | ]; 30 | } 31 | } 32 | ] 33 | } 34 | ], 35 | expected: { 36 | onPublicInitDone: [{ 37 | error: null, 38 | notification: [{ 'tests/tmp/unlink/targetFilesMultipleFn': [ 'file.test' ] }] 39 | }], 40 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/unlink/targetFilesMultipleFn/*.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/unlink/targetFilesMultipleFn/file.test\u001b[22m'], 41 | onEvent: ['\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mFile removal detected...\u001b[39m \u001b[1mtests/tmp/unlink/targetFilesMultipleFn/file.test\u001b[22m'], 42 | onTaskFileEvent: [{ 43 | eventType: 'unlink', 44 | filePath: targetFile2, 45 | taskFilePath: taskFilePath, 46 | options: undefined 47 | }, { 48 | eventType: 'unlink', 49 | filePath: targetFile3, 50 | taskFilePath: taskFilePath, 51 | options: undefined 52 | }] 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /tests/configs-add/multiWatchGroup.js: -------------------------------------------------------------------------------- 1 | var testPath = require('../utils/testPath'); 2 | var tmpPath = require('../utils/tmpPath'); 3 | 4 | var filePath = tmpPath('add', 'multiWatchGroup', 'file.test'); 5 | var filePath2 = tmpPath('add', 'multiWatchGroup', 'file-2.test'); 6 | var taskFilePath = testPath('taskFiles', 'report.js'); 7 | 8 | module.exports = { 9 | watchGroups: [ 10 | { 11 | serviceName: 'basic', 12 | path: filePath, 13 | warnIfMissingPath: false, 14 | events: [ 15 | { 16 | type: 'add', 17 | taskFiles: taskFilePath 18 | } 19 | ] 20 | }, 21 | { 22 | serviceName: 'basic', 23 | path: filePath2, 24 | warnIfMissingPath: false, 25 | events: [ 26 | { 27 | type: 'add', 28 | taskFiles: taskFilePath 29 | } 30 | ] 31 | } 32 | ], 33 | expected: { 34 | onPublicInitDone: [{ 35 | error: null, 36 | notification: [{}, {}] 37 | }], 38 | onInit: [ 39 | '[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/add/multiWatchGroup/file.test\u001b[22m', 40 | '[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/add/multiWatchGroup/file-2.test\u001b[22m' 41 | ], 42 | onEvent: [ 43 | '\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mNew basic file detected. \u001b[90mWatching file...\u001b[36m\u001b[39m \u001b[1mtests/tmp/add/multiWatchGroup/file.test\u001b[22m', 44 | '\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mNew basic file detected. \u001b[90mWatching file...\u001b[36m\u001b[39m \u001b[1mtests/tmp/add/multiWatchGroup/file-2.test\u001b[22m' 45 | ], 46 | onTaskFileEvent: [{ 47 | eventType: 'add', 48 | filePath: filePath, 49 | taskFilePath: taskFilePath, 50 | options: undefined 51 | }, { 52 | eventType: 'add', 53 | filePath: filePath2, 54 | taskFilePath: taskFilePath, 55 | options: undefined 56 | }] 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /tests/configs-watch/hideChildDirs.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | io.writeDataSync(tmpPath('watch', 'hideChildDirs', 'file.test'), 'file', {makeDirs: true}); 6 | io.writeDataSync(tmpPath('watch', 'hideChildDirs', 'file-2.test'), 'file', {makeDirs: true}); 7 | io.writeDataSync(tmpPath('watch', 'hideChildDirs', 'subdir', 'file.test'), 'file', {makeDirs: true}); 8 | io.writeDataSync(tmpPath('watch', 'hideChildDirs', 'subdir', 'file-2.test'), 'file', {makeDirs: true}); 9 | 10 | var filePath = tmpPath('watch', 'hideChildDirs', '**', '*'); 11 | 12 | var taskFilePath = testPath('taskFiles', 'report.js'); 13 | 14 | module.exports = { 15 | watchGroups: [ 16 | { 17 | serviceName: 'basic', 18 | path: filePath, 19 | displayOptions: { 20 | hideChildDirs: true 21 | }, 22 | events: [ 23 | { 24 | type: 'add', 25 | taskFiles: taskFilePath 26 | }, 27 | { 28 | type: 'change', 29 | taskFiles: taskFilePath 30 | } 31 | ] 32 | } 33 | ], 34 | expected: { 35 | onPublicInitDone: [{ 36 | error: null, 37 | notification: [{'tests/tmp/watch/hideChildDirs': ['file-2.test', 'file.test', 'subdir'], 'tests/tmp/watch/hideChildDirs/subdir': ['file-2.test', 'file.test']}] 38 | }], 39 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/watch/hideChildDirs/**/*\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/hideChildDirs/file-2.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/hideChildDirs/file.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/hideChildDirs/subdir/file-2.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/hideChildDirs/subdir/file.test\u001b[22m'] 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /tests/configs-watch/ignoredGlob.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var filePath = tmpPath('watch', 'ignoredGlob', 'file.test'); 6 | var filePath2 = tmpPath('watch', 'ignoredGlob', 'file-2.ignore'); 7 | var filePath3 = tmpPath('watch', 'ignoredGlob', 'subdir', 'file.test'); 8 | var filePath4 = tmpPath('watch', 'ignoredGlob', 'subdir', 'file-2.ignore'); 9 | 10 | io.writeDataSync(filePath, 'file', {makeDirs: true}); 11 | io.writeDataSync(filePath2, 'file', {makeDirs: true}); 12 | io.writeDataSync(filePath3, 'file', {makeDirs: true}); 13 | io.writeDataSync(filePath4, 'file', {makeDirs: true}); 14 | 15 | var glob = tmpPath('watch', 'ignoredGlob', '**', '*'); 16 | var ignoreGlob = tmpPath('watch', 'ignoredGlob', '**', '*.ignore'); 17 | 18 | var taskFilePath = testPath('taskFiles', 'report.js'); 19 | 20 | module.exports = { 21 | watchGroups: [ 22 | { 23 | serviceName: 'basic', 24 | path: glob, 25 | chokidarOptions: { 26 | ignored: [ignoreGlob] 27 | }, 28 | events: [ 29 | { 30 | type: 'add', 31 | taskFiles: taskFilePath 32 | }, 33 | { 34 | type: 'change', 35 | taskFiles: taskFilePath 36 | } 37 | ] 38 | } 39 | ], 40 | expected: { 41 | onPublicInitDone: [{ 42 | error: null, 43 | notification: [{ 44 | 'tests/tmp/watch/ignoredGlob': ['file.test', 'subdir'], 45 | 'tests/tmp/watch/ignoredGlob/subdir': ['file.test'] 46 | }] 47 | }], 48 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/watch/ignoredGlob/**/*\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/ignoredGlob/file.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/ignoredGlob/subdir/\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/ignoredGlob/subdir/file.test\u001b[22m'] 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /tests/configs-change/commandTasksWithTaskFiles.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var tmpFile = tmpPath('change', 'commandTasksWithTaskFiles', 'file.test'); 6 | io.writeDataSync(tmpFile, 'file', {makeDirs: true}); 7 | 8 | var taskFilePath = testPath('taskFiles', 'report.js'); 9 | var taskFilePath2 = testPath('taskFiles', 'report-2.js'); 10 | 11 | var commandPath = testPath('clis', 'report.js'); 12 | var commandPath2 = testPath('clis', 'report-2.js'); 13 | 14 | module.exports = { 15 | watchGroups: [ 16 | { 17 | serviceName: 'basic', 18 | path: tmpFile, 19 | events: [ 20 | { 21 | type: 'change', 22 | commands: [ 23 | 'node ' + commandPath, 24 | 'node ' + commandPath2 25 | ], 26 | taskFiles: [ 27 | taskFilePath, 28 | taskFilePath2 29 | ] 30 | } 31 | ] 32 | } 33 | ], 34 | expected: { 35 | onPublicInitDone: [{ 36 | error: null, 37 | notification: [{ 'tests/tmp/change/commandTasksWithTaskFiles': [ 'file.test' ] }] 38 | }], 39 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/change/commandTasksWithTaskFiles/file.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/change/commandTasksWithTaskFiles/file.test\u001b[22m'], 40 | onEvent: ['\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mFile changed...\u001b[39m \u001b[1mtests/tmp/change/commandTasksWithTaskFiles/file.test\u001b[22m'], 41 | onCommandEvent: [{ 42 | code: 100, 43 | command: commandPath, 44 | env: undefined 45 | }, { 46 | code: 100, 47 | command: commandPath2, 48 | env: undefined 49 | }], 50 | onTaskFileEvent: [{ 51 | eventType: 'change', 52 | filePath: tmpFile, 53 | taskFilePath: taskFilePath, 54 | options: undefined 55 | }, { 56 | eventType: 'change', 57 | filePath: tmpFile, 58 | taskFilePath: taskFilePath2, 59 | options: undefined 60 | }] 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /tests/configs-watch/ignored.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var filePath = tmpPath('watch', 'ignored', 'file.test'); 6 | var filePath2 = tmpPath('watch', 'ignored', 'file-2.test'); 7 | var filePath3 = tmpPath('watch', 'ignored', 'subdir', 'file.test'); 8 | var filePath4 = tmpPath('watch', 'ignored', 'subdir', 'file-2.test'); 9 | 10 | io.writeDataSync(filePath, 'file', {makeDirs: true}); 11 | io.writeDataSync(filePath2, 'file', {makeDirs: true}); 12 | io.writeDataSync(filePath3, 'file', {makeDirs: true}); 13 | io.writeDataSync(filePath4, 'file', {makeDirs: true}); 14 | 15 | var glob = tmpPath('watch', 'ignored', '**', '*'); 16 | 17 | var taskFilePath = testPath('taskFiles', 'report.js'); 18 | 19 | module.exports = { 20 | watchGroups: [ 21 | { 22 | serviceName: 'basic', 23 | path: glob, 24 | chokidarOptions: { 25 | ignored: [filePath2] 26 | }, 27 | events: [ 28 | { 29 | type: 'add', 30 | taskFiles: taskFilePath 31 | }, 32 | { 33 | type: 'change', 34 | taskFiles: taskFilePath 35 | } 36 | ] 37 | } 38 | ], 39 | expected: { 40 | onPublicInitDone: [{ 41 | error: null, 42 | notification: [{ 43 | 'tests/tmp/watch/ignored': ['file.test', 'subdir'], 44 | 'tests/tmp/watch/ignored/subdir': ['file-2.test', 'file.test'] 45 | }] 46 | }], 47 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/watch/ignored/**/*\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/ignored/file.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/ignored/subdir/\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/ignored/subdir/file-2.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/ignored/subdir/file.test\u001b[22m'] 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /tests/configs-watch/nestedFolders.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | io.writeDataSync(tmpPath('watch', 'nestedFolders', 'file.test'), 'file', {makeDirs: true}); 6 | io.writeDataSync(tmpPath('watch', 'nestedFolders', 'subdir', 'file.test'), 'file', {makeDirs: true}); 7 | io.writeDataSync(tmpPath('watch', 'nestedFolders', 'subdir', 'subsubdir', 'file.test'), 'file', {makeDirs: true}); 8 | 9 | var glob = tmpPath('watch', 'nestedFolders', '**', '*'); 10 | 11 | var taskFilePath = testPath('taskFiles', 'report.js'); 12 | 13 | module.exports = { 14 | watchGroups: [ 15 | { 16 | serviceName: 'basic', 17 | path: glob, 18 | events: [ 19 | { 20 | type: 'add', 21 | taskFiles: taskFilePath 22 | }, 23 | { 24 | type: 'change', 25 | taskFiles: taskFilePath 26 | } 27 | ] 28 | } 29 | ], 30 | expected: { 31 | onPublicInitDone: [{ 32 | error: null, 33 | notification: [{ 34 | 'tests/tmp/watch/nestedFolders': ['file.test', 'subdir'], 35 | 'tests/tmp/watch/nestedFolders/subdir': ['file.test', 'subsubdir'], 36 | 'tests/tmp/watch/nestedFolders/subdir/subsubdir': ['file.test'] 37 | }] 38 | }], 39 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/watch/nestedFolders/**/*\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/nestedFolders/file.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/nestedFolders/subdir/\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/nestedFolders/subdir/file.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/nestedFolders/subdir/subsubdir/\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/nestedFolders/subdir/subsubdir/file.test\u001b[22m'] 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /tests/configs-watch/globAddChangeWatchInitial.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var filePath = tmpPath('watch', 'globAddChangeWatchInitial', 'file.test'); 6 | io.writeDataSync(filePath, 'file', {makeDirs: true}); 7 | var filePath2 = tmpPath('watch', 'globAddChangeWatchInitial', 'file-2.test'); 8 | io.writeDataSync(filePath2, 'file', {makeDirs: true}); 9 | 10 | var glob = tmpPath('watch', 'globAddChangeWatchInitial', '*.test'); 11 | 12 | var taskFilePath = testPath('taskFiles', 'report.js'); 13 | 14 | module.exports = { 15 | // Test this as an object, not an array 16 | watchGroups: { 17 | serviceName: 'basic', 18 | path: glob, 19 | chokidarOptions: { 20 | ignoreInitial: false 21 | }, 22 | events: [ 23 | { 24 | type: 'add', 25 | taskFiles: taskFilePath 26 | }, 27 | { 28 | type: 'change', 29 | taskFiles: taskFilePath 30 | } 31 | ] 32 | }, 33 | expected: { 34 | onPublicInitDone: [{ 35 | error: null, 36 | notification: [{ 'tests/tmp/watch/globAddChangeWatchInitial': [ 'file-2.test', 'file.test' ] }] 37 | }], 38 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/watch/globAddChangeWatchInitial/*.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/globAddChangeWatchInitial/file-2.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/globAddChangeWatchInitial/file.test\u001b[22m'], 39 | onEvent: [ 40 | '\n[00:00:00.00 | wsk] New basic file detected. Watching file... tests/tmp/watch/globAddChangeWatchInitial/file-2.test', 41 | '\n[00:00:00.00 | wsk] New basic file detected. Watching file... tests/tmp/watch/globAddChangeWatchInitial/file.test' 42 | ], 43 | onTaskFileEvent: [{ 44 | eventType: 'add', 45 | filePath: filePath2, 46 | taskFilePath: taskFilePath, 47 | options: undefined 48 | }, { 49 | eventType: 'add', 50 | filePath: filePath, 51 | taskFilePath: taskFilePath, 52 | options: undefined 53 | }] 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /tests/configs-watch/watchDotFiles.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var filePath = tmpPath('watch', 'watchDotFiles', 'file.test'); 6 | var filePath2 = tmpPath('watch', 'watchDotFiles', '.file-2.test'); 7 | var filePath3 = tmpPath('watch', 'watchDotFiles', 'subdir', 'file.test'); 8 | var filePath4 = tmpPath('watch', 'watchDotFiles', 'subdir', '.file-2.test'); 9 | 10 | io.writeDataSync(filePath, 'file', {makeDirs: true}); 11 | io.writeDataSync(filePath2, 'file', {makeDirs: true}); 12 | io.writeDataSync(filePath3, 'file', {makeDirs: true}); 13 | io.writeDataSync(filePath4, 'file', {makeDirs: true}); 14 | 15 | var glob = tmpPath('watch', 'watchDotFiles', '**', '*'); 16 | 17 | var taskFilePath = testPath('taskFiles', 'report.js'); 18 | 19 | module.exports = { 20 | watchGroups: [ 21 | { 22 | serviceName: 'basic', 23 | path: glob, 24 | ignoreDotFiles: false, 25 | chokidarOptions: { 26 | ignoreInitial: true 27 | }, 28 | events: [ 29 | { 30 | type: 'add', 31 | taskFiles: taskFilePath 32 | }, 33 | { 34 | type: 'change', 35 | taskFiles: taskFilePath 36 | } 37 | ] 38 | } 39 | ], 40 | expected: { 41 | onPublicInitDone: [{ 42 | error: null, 43 | notification: [{ 44 | 'tests/tmp/watch/watchDotFiles': ['.file-2.test', 'file.test', 'subdir'], 45 | 'tests/tmp/watch/watchDotFiles/subdir': ['.file-2.test', 'file.test'] 46 | }] 47 | }], 48 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/watch/watchDotFiles/**/*\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/watchDotFiles/.file-2.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/watchDotFiles/file.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/watchDotFiles/subdir/\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/watchDotFiles/subdir/.file-2.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/watchDotFiles/subdir/file.test\u001b[22m'] 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /tests/configs-change/commandTasksWithTaskFilesWithEnvOptions.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var tmpFile = tmpPath('change', 'commandTasksWithTaskFilesWithEnvOptions', 'file.test'); 6 | io.writeDataSync(tmpFile, 'file', {makeDirs: true}); 7 | 8 | var commandPath = testPath('clis', 'report.js'); 9 | var commandPath2 = testPath('clis', 'report-2.js'); 10 | 11 | var taskFilePath = testPath('taskFiles', 'report.js'); 12 | var taskFilePath2 = testPath('taskFiles', 'report-2.js'); 13 | 14 | module.exports = { 15 | watchGroups: [ 16 | { 17 | serviceName: 'basic', 18 | path: tmpFile, 19 | events: [ 20 | { 21 | type: 'change', 22 | commands: [ 23 | 'node ' + commandPath, 24 | 'node ' + commandPath2 25 | ], 26 | taskFiles: [ 27 | taskFilePath, 28 | taskFilePath2 29 | ], 30 | env: { 31 | report: true 32 | }, 33 | options: { 34 | report: true 35 | } 36 | } 37 | ] 38 | } 39 | ], 40 | expected: { 41 | onPublicInitDone: [{ 42 | error: null, 43 | notification: [{ 'tests/tmp/change/commandTasksWithTaskFilesWithEnvOptions': [ 'file.test' ] }] 44 | }], 45 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/change/commandTasksWithTaskFilesWithEnvOptions/file.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/change/commandTasksWithTaskFilesWithEnvOptions/file.test\u001b[22m'], 46 | onEvent: ['\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mFile changed...\u001b[39m \u001b[1mtests/tmp/change/commandTasksWithTaskFilesWithEnvOptions/file.test\u001b[22m'], 47 | onCommandEvent: [{ 48 | code: 100, 49 | command: commandPath, 50 | env: { 51 | report: true 52 | } 53 | }, { 54 | code: 100, 55 | command: commandPath2, 56 | env: { 57 | report: true 58 | } 59 | }], 60 | onTaskFileEvent: [{ 61 | eventType: 'change', 62 | filePath: tmpFile, 63 | taskFilePath: taskFilePath, 64 | options: { 65 | report: true 66 | } 67 | }, { 68 | eventType: 'change', 69 | filePath: tmpFile, 70 | taskFilePath: taskFilePath2, 71 | options: { 72 | report: true 73 | } 74 | }] 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/lib/printWatchTree.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var fullPathsToPrjPath = require('./fullPathsToPrjPath'); 6 | 7 | var notify = require('../notify.js'); 8 | 9 | module.exports = function printWatchTree (watchTree, notification, displayOptions, onDone, cb) { 10 | // Per the chokidar docs: The object's keys are all the directories 11 | // (using absolute paths unless the cwd option was used) 12 | // The values are arrays of the names of the items contained in each directory. 13 | // https://github.com/paulmillr/chokidar#methods--events 14 | var shortWatchTree = {}; 15 | Object.keys(watchTree).forEach(watchedDir => { 16 | // The `fullPath` starts at the computer root, `fullPathsToPrjPath` will shorten that to 17 | // begin the path at the whisk root directory 18 | // This is nicer for displaying to users 19 | // var shortPathRoot = fullPathsToPrjPath(watchedDir); 20 | var shortPathRoot = fullPathsToPrjPath(watchedDir); 21 | shortWatchTree[shortPathRoot] = watchTree[watchedDir]; 22 | // Each value is an array, so loop through them to construct full file paths 23 | watchTree[watchedDir].forEach(child => { 24 | // Join the child path with the key to get the full path 25 | var fullPath = path.join(watchedDir, child); 26 | // See if the thing we're watching is a folder or a file 27 | // For simplicity, we're only going to show users files that are being watched 28 | var statInfo; 29 | try { 30 | statInfo = fs.statSync(fullPath); 31 | } catch (err) { 32 | /* istanbul ignore next */ 33 | let en = notify({ 34 | message: 'Error determining if path is file or directory...', 35 | value: fullPath, 36 | display: 'error', 37 | error: err 38 | }); 39 | /* istanbul ignore next */ 40 | cb.onInit(en); 41 | /* istanbul ignore next */ 42 | onDone(en); 43 | /* istanbul ignore next */ 44 | return; 45 | } 46 | // If we're logging directories, then log those out 47 | // By using the `---`, this will nicely indent underneath our bundle notification above 48 | if (statInfo && statInfo.isDirectory()) { 49 | if (displayOptions.hideChildDirs === false) { 50 | notification += '\n' + notify({ 51 | message: '---', 52 | value: path.join(shortPathRoot, child) + '/', 53 | display: 'watch', 54 | silent: true 55 | }); 56 | } 57 | } else if (displayOptions.hideChildFiles === false) { 58 | // If we're not watching directories notify the user that we're watching this file 59 | notification += '\n' + notify({ 60 | message: '---', 61 | value: path.join(shortPathRoot, child), 62 | display: 'watch', 63 | silent: true 64 | }); 65 | } 66 | }); 67 | }); 68 | console.log(notification); 69 | onDone(null, shortWatchTree); 70 | cb.onInit(null, notification); 71 | }; 72 | -------------------------------------------------------------------------------- /tests/configs-watch/multiPaths.js: -------------------------------------------------------------------------------- 1 | var io = require('indian-ocean'); 2 | var testPath = require('../utils/testPath'); 3 | var tmpPath = require('../utils/tmpPath'); 4 | 5 | var filePath1 = tmpPath('watch', 'multiPaths', 'file.test'); 6 | var filePath2 = tmpPath('watch', 'multiPaths', 'file-2.test'); 7 | var filePath3 = tmpPath('watch', 'multiPaths', 'file.test3'); 8 | 9 | io.writeDataSync(filePath1, 'file', {makeDirs: true}); 10 | io.writeDataSync(filePath2, 'file', {makeDirs: true}); 11 | io.writeDataSync(filePath3, 'file', {makeDirs: true}); 12 | 13 | var taskFilePath = testPath('taskFiles', 'report.js'); 14 | 15 | module.exports = { 16 | watchGroups: [ 17 | { 18 | serviceName: 'basic', 19 | path: [filePath1, filePath2, filePath3], 20 | chokidarOptions: { 21 | ignoreInitial: false 22 | }, 23 | events: [ 24 | { 25 | type: 'add', 26 | taskFiles: taskFilePath 27 | }, 28 | { 29 | type: 'change', 30 | taskFiles: taskFilePath 31 | } 32 | ] 33 | } 34 | ], 35 | expected: { 36 | onPublicInitDone: [{ 37 | error: null, 38 | notification: [{ 39 | 'tests/tmp/watch/multiPaths': ['file-2.test', 'file.test', 'file.test3'] 40 | }] 41 | }], 42 | onInit: ['[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90mWatching basic bundle...\u001b[39m \u001b[1mtests/tmp/watch/multiPaths/file.test, tests/tmp/watch/multiPaths/file-2.test, tests/tmp/watch/multiPaths/file.test3\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/multiPaths/file-2.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/multiPaths/file.test\u001b[22m\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[90m---\u001b[39m \u001b[1mtests/tmp/watch/multiPaths/file.test3\u001b[22m'], 43 | onEvent: [ 44 | '\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mNew basic file detected. \u001b[90mWatching file...\u001b[36m\u001b[39m \u001b[1mtests/tmp/watch/multiPaths/file.test\u001b[22m', 45 | '\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mNew basic file detected. \u001b[90mWatching file...\u001b[36m\u001b[39m \u001b[1mtests/tmp/watch/multiPaths/file-2.test\u001b[22m', 46 | '\n[\u001b[90m00:00:00.00\u001b[39m | \u001b[1m\u001b[34mwsk\u001b[39m\u001b[22m] \u001b[36mNew basic file detected. \u001b[90mWatching file...\u001b[36m\u001b[39m \u001b[1mtests/tmp/watch/multiPaths/file.test3\u001b[22m' 47 | ], 48 | onTaskFileEvent: [{ 49 | eventType: 'add', 50 | filePath: filePath1, 51 | taskFilePath: taskFilePath, 52 | options: undefined 53 | }, { 54 | eventType: 'add', 55 | filePath: filePath2, 56 | taskFilePath: taskFilePath, 57 | options: undefined 58 | }, { 59 | eventType: 'add', 60 | filePath: filePath3, 61 | taskFilePath: taskFilePath, 62 | options: undefined 63 | }] 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /src/lib/validateGroup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* -------------------------------------------- 4 | * 5 | * Validation tests for watchGroups 6 | * Each validation test should call back to cb.fail if there is fail sending back a 7 | * wsk-notify object to be instantiated 8 | * If testing async, use a q and send failing errors to the first argument, wsk-object warnings to the second 9 | * and nothing for both if test passes 10 | * 11 | * -------------------------------------------- 12 | */ 13 | var _ = require('underscore'); 14 | var chalk = require('chalk'); 15 | 16 | var validatePath = require('./validatePath'); 17 | var filterForPaths = require('./filterForPaths'); 18 | var queue = require('d3-queue').queue; 19 | 20 | module.exports = function validateGroup (watchGroup, cb) { 21 | var q = queue(); 22 | 23 | /* -------------------------------------------- 24 | * Check events is an array 25 | */ 26 | if (!_.isArray(watchGroup.events)) { 27 | let nS = { 28 | message: `Error: No events specified for \`${watchGroup.serviceName}\` service.`, 29 | display: 'error', 30 | silent: true 31 | }; 32 | cb.fail(nS); 33 | return false; 34 | } 35 | 36 | /* -------------------------------------------- 37 | * Check we have either commands or tasksFiles 38 | */ 39 | watchGroup.events.forEach(evt => { 40 | if (!evt.taskFiles && !evt.commands) { 41 | let msg = `Error: No \`taskFiles\` or \`commands\` set for \`${watchGroup.serviceName}\` service's event...`; 42 | let nS = { 43 | message: msg, 44 | value: evt.type, 45 | display: 'error' 46 | }; 47 | cb.fail(nS); 48 | return false; 49 | } 50 | }); 51 | 52 | /* -------------------------------------------- 53 | * Check our paths are either globs or if file paths, that they exist, if we care about that 54 | */ 55 | var pathsToTest = watchGroup.path.filter(filterForPaths(watchGroup.warnIfMissingPath)); 56 | var hintStr = chalk.reset('\nTo disable this warning, set `warnIfMissingPath: false` in your watchGroup config.\n'); 57 | pathsToTest.forEach(pathToTest => { 58 | var messageValue = { 59 | message: 'Warning: the file you asked to watch does not exist...', 60 | value: pathToTest + hintStr 61 | }; 62 | 63 | q.defer(validatePath(messageValue), pathToTest); 64 | }); 65 | 66 | /* -------------------------------------------- 67 | * Check that our taskFiles exist 68 | */ 69 | watchGroup.events.forEach(evt => { 70 | if (evt.taskFiles) { 71 | let taskFiles = _.isArray(evt.taskFiles) ? evt.taskFiles : [evt.taskFiles]; 72 | taskFiles.forEach(pathToTest => { 73 | var messageValue = { 74 | message: `Error: Task file for \`${watchGroup.serviceName}\` service's \`${evt.type}\` event does not exist.`, 75 | value: pathToTest 76 | }; 77 | q.defer(validatePath(messageValue), pathToTest); 78 | }); 79 | } 80 | }); 81 | 82 | /* -------------------------------------------- 83 | * General purpose error reporter 84 | * The first argument are failing errors 85 | * Second argument are warnings 86 | */ 87 | q.awaitAll(function (nS, warnings) { 88 | /* istanbul ignore if */ 89 | if (nS) { 90 | cb.fail(nS); 91 | return false; 92 | } 93 | // Send any warnings back out to report 94 | warnings.filter(nS => nS).forEach(nS => cb.warn(nS)); 95 | // And then proceed 96 | cb.proceed(); 97 | }); 98 | }; 99 | -------------------------------------------------------------------------------- /src/lib/initWatch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* -------------------------------------------- 4 | * 5 | * Send our file paths and watch options to chokidar to do the watching 6 | * 7 | * -------------------------------------------- 8 | */ 9 | var path = require('path'); 10 | var chokidar = require('chokidar'); 11 | var _ = require('underscore'); 12 | 13 | var notify = require('../notify.js'); 14 | 15 | var fullPathsToPrjPath = require('./fullPathsToPrjPath'); 16 | var handleEvent = require('./handleEvent'); 17 | var printWatchTree = require('./printWatchTree'); 18 | var shortenWatchTree = require('./shortenWatchTree'); 19 | 20 | module.exports = function initWatch (watchGroup, onDone, cb) { 21 | // Normalize the options we're passing to chokidar 22 | var chokidarOptions = _.defaults(watchGroup.chokidarOptions || {}, { 23 | ignored: [], 24 | ignoreInitial: true 25 | }); 26 | 27 | // Normalize the display options 28 | var displayOptions = _.defaults(watchGroup.displayOptions || {}, { 29 | hideChildFiles: false, 30 | hideChildDirs: false, 31 | hideAll: false 32 | }); 33 | 34 | // Add our ignored dot files default 35 | if (_.isUndefined(watchGroup.ignoreDotFiles) || watchGroup.ignoreDotFiles === true) { 36 | chokidarOptions.ignored = [/[/\\]\./].concat(chokidarOptions.ignored); 37 | } 38 | 39 | // Hydrate our taskFiles and do some error checking 40 | var hasErr = false; 41 | watchGroup.events.forEach(evt => { 42 | _.defaults(evt, {commands: [], taskFiles: []}); 43 | if (evt.commands) { 44 | evt.commands = _.isArray(evt.commands) ? evt.commands : [evt.commands]; 45 | } 46 | if (evt.taskFiles) { 47 | let taskFiles = evt.taskFiles || []; 48 | taskFiles = _.isArray(evt.taskFiles) ? evt.taskFiles : [evt.taskFiles]; 49 | evt.taskFiles = taskFiles.map(d => { 50 | var module; 51 | var modulePath = path.join(path.resolve('./'), d); 52 | var moduleShortPath = fullPathsToPrjPath(modulePath); 53 | try { 54 | module = require(modulePath); 55 | } catch (err) { 56 | let msg = `Error: Could not require taskFile in \`${watchGroup.serviceName}\` watchGroup...`; 57 | notify({ 58 | message: msg, 59 | value: moduleShortPath, 60 | display: 'error', 61 | error: err 62 | }); 63 | let errMessage = msg + ' ' + moduleShortPath; 64 | onDone(new Error(errMessage)); 65 | cb.onInit(null, errMessage); 66 | hasErr = true; 67 | return false; 68 | } 69 | if (!module.onEvent || !_.isFunction(module.onEvent)) { 70 | let msg = `Error: Task file for \`${watchGroup.serviceName}\` does not export an \`onEvent\` function...`; 71 | let n = notify({ 72 | message: msg, 73 | value: d, 74 | display: 'error' 75 | }); 76 | onDone(new Error(msg + ' ' + d)); 77 | cb.onInit(null, n); 78 | hasErr = true; 79 | return false; 80 | } 81 | return { 82 | path: d, 83 | module: module 84 | }; 85 | }); 86 | } 87 | }); 88 | if (hasErr) { 89 | return false; 90 | } 91 | 92 | // Initialize watcher 93 | var watcher = chokidar.watch(watchGroup.path, chokidarOptions); 94 | 95 | // Add an error event handler 96 | /* istanbul ignore next */ 97 | watcher.on('error', function (err) { 98 | var msg = `Error: Problem watching group...`; 99 | var n = notify({ 100 | message: msg, 101 | value: watchGroup.serviceName, 102 | display: 'error', 103 | error: err 104 | }); 105 | onDone(msg + ' ' + watchGroup.serviceName); 106 | cb.onInit(null, n); 107 | }); 108 | 109 | // Add a ready event handler, which we will use to notify the user that we are watching this file or group of files 110 | watcher.on('ready', function () { 111 | if (displayOptions.hideAll === true) { 112 | cb.onInit(null, ''); 113 | onDone(null, shortenWatchTree(watcher.getWatched())); 114 | return false; 115 | } 116 | // Notify the user that we are watching the file paths or blobs they specified 117 | var fullPaths = fullPathsToPrjPath(watchGroup.path); 118 | // If we passed in an array of paths, print them here separated by commas 119 | if (_.isArray(fullPaths)) { 120 | fullPaths = fullPaths.join(', '); 121 | } 122 | var notification = notify({ 123 | message: 'Watching ' + watchGroup.serviceName + ' bundle...', 124 | value: fullPaths, 125 | display: 'watch', 126 | silent: true 127 | }); 128 | 129 | // Also notify the resulting files that chokidar found in the system 130 | // The difference between the above notifcation and this one is two-fold: 131 | // 1. This is handy for verification. We told chokidar to watch `config.json` but now chokidar is telling us, 132 | // "Hey config.json is one of the things I'm watching" 133 | // 2. If we passed chokidar a glob such as `src/html/**/*`, this will report out the individual files it 134 | // found such as `src/html/body.jst` and others 135 | if (displayOptions.hideChildDirs === true && displayOptions.hideChildFiles === true) { 136 | onDone(null, shortenWatchTree(watcher.getWatched())); 137 | cb.onInit(null, notification); 138 | return false; 139 | } 140 | // Get the files that chokidar is watching 141 | var watchedFiles = watcher.getWatched(); 142 | printWatchTree(watchedFiles, notification, displayOptions, onDone, cb); 143 | }); 144 | 145 | // Loop through our events and add those listeners 146 | // When that event trigger, run the `handleEvt` function, 147 | // which will notify and optionally run a `taskFile` if one is specified 148 | watchGroup.events.forEach(function (evt) { 149 | watcher.on(evt.type, function (filePath) { 150 | handleEvent(evt, filePath, watchGroup.serviceName, cb); 151 | }); 152 | }); 153 | }; 154 | -------------------------------------------------------------------------------- /HOW-DID-WE-GET-HERE.md: -------------------------------------------------------------------------------- 1 | How did we get here? 2 | === 3 | 4 | > A brief history of why this project got made 5 | 6 | ## Our goals 7 | 8 | We wanted to make a standardized build system for our projects, which are often one-off graphics that we build as a part of [news stories](https://bloomberg.com/graphics). It had the following requirements (in no particular order): 9 | 10 | 1. Be flexible and extensible for people on our team unfamiliar with the latest build system frameworks. 11 | 2. Provide rich feedback so that if the user did something wrong, it would try to help them correct it. Also make clear what was a problem with **the project** versus a problem with **the build system**. This was critical during the adoption phase. 12 | 3. Don't close off possibilities and don't lock us in to one way of doing things. Each of our projects is a custom design. We didn't want a system that constrained creativity or had an architecture that slowed down implementing new features (such as waiting for dependencies to exist or be updated). 13 | 14 | ## What was out there 15 | 16 | The most common choices, at the time, were Gulp and Webpack. We ended up not going with these because they had elements that went against at least one of our three goals. 17 | 18 | 1. Writing a Gulp plugin, for example, requires knowledge of streams, which is a barrier to entry. 19 | 2. Console feedback tells you that a task happened and how long it took but not what it did, specifically. 20 | 3. Gulp requires an eco-system of plugins. If you want to use Sass, you rely on `gulp-sass`, which [might not be up to date](https://github.com/dlmanning/gulp-sass/issues/621) or it might introduce its own bugs. 21 | 4. Webpack tends to take full ownership over the whole process; breaking off pieces of it or adding to it can be difficult. This might close off possibilities or require us to find "the Webpack way" of doing a given transformation if a project creator wanted it done. 22 | 23 | ## Where we started 24 | 25 | Our first approach in service of these goals was to use vanilla npm scripts and the command-line interfaces for the libraries we wanted to use. We used [chokidar-cli](https://github.com/kimmobrunfeldt/chokidar-cli) to trigger actions when files changed. Our package.json looked like this: 26 | 27 | ![](screenshots/npm-scripts-v1.png) 28 | 29 | The functionality was okay but we saw very inconsistent console output. 30 | 31 | ![](screenshots/npm-scripts-v1-output.png) 32 | 33 | This concern goes beyond aesthetics. If the console is sending differently styled output on change, the user has to interpret multiple signals. This is distracting and makes it harder to recognize actionable feedback. 34 | 35 | It would be better if normal compile messages were in a consistent format. Errors should appear in a different color and with a stack trace, if possible, to break up the formatting. The user can detect this change of the corner of his or her eye and attend to it. 36 | 37 | Cognitively, the eye is better at noticing movement in the periphery of vision than it is at catching detail. If the build console is in the corner of screen monitor, it's easier to notice a disruption if there's a break from consistency than if you have a number of variable length messages printing all the time. 38 | 39 | Since we were implementing a build system where one previously did not exist, clear notifications were important to separate system errors from project errors. It's very easy to blame the new, unknown thing and we wanted to avoid the build system being faulted unnecessarily. In fact, where possible, build notifications should help the user diagnose the problem, more quickly. 40 | 41 | ## Improving console outputs 42 | 43 | The first step in trying to make console outputs more consistent was by piping output to a script that would standardize output from the libraries we were using. Here's the new output: 44 | 45 | ![](screenshots/npm-scripts-v2-output.png) 46 | 47 | The problems: 48 | 49 | 1. This was a lot of work that was likely to change if a library modified its output style 50 | 2. It would require more work if we changed libraries, in opposition to goal number one. 51 | 3. It made our `package.json` very difficult to read, also in opposition to goal number one for anyone who wanted to edit their npm scripts. Here's what that looked like: 52 | 53 | ![](screenshots/npm-scripts-v2.png) 54 | 55 | Because each of these scripts handles its output differently, we had to pipe console messages all over the place. Some went to `/dev/null`; others used `2>&1` to redirect stderr to stdout. It was a mess. 56 | 57 | ## A better setup 58 | 59 | These experiments led us to create [wsk](https://github.com/bloomberg/wsk) and [wsk-notify](https://github.com/bloomberg/wsk-notify), which is our current setup. wsk is a watcher specification around [chokidar](https://github.com/paulmillr/chokidar) that makes it easier to declare a glob of files to watch and Vanilla JavaScript modules to run when those files change. wsk-notify is a module for standardized console and desktop notifications. 60 | 61 | This setup work with our three goals because: 62 | 63 | 1. No special knowledge is required to write a task file (a "plugin" using Gulp vocabulary) 64 | 2. By using chokidar to watch files, we get access to a diverse set of events. Our notifications can report exactly what events are happening and what tasks it's dispatching as a result. 65 | 3. By using libraries directly, we avoid intermediate plugins as much as we can. 66 | 67 | Although we passed over using command-line APIs for other reasons, we found that using libraries directly via their JavaScript API also increased the number of options we had when using these target libraries. That makes intuitive sense as you can only squeeze so much functionality into command-line flags. 68 | 69 | ## Conclusion 70 | 71 | We built wsk to address our mix of skills and constraints. It might not be the right solution for everyone but if you've run into issues with stale plugins, being constrained by adding extra concepts into your pipeline, such as streams, or been frustrated when you want to use a new library and have to look for a "How to use with Webpack/Gulp/Grunt" section of the readme, wsk might be helpful for you. 72 | 73 | Over the last 18 months of usage, it's proven to be a flexible and maintainable system. Its powered projects as simple as [one-off graphics](https://www.bloomberg.com/graphics/2016-takata-recall/) to [multi-day](https://www.bloomberg.com/politics/graphics/2016-bus-to-november/) or [multi-page](https://www.bloomberg.com/graphics/2016-asia-space-race/) [series](https://www.bloomberg.com/graphics/2017-arctic/) that function more like static-site generators. 74 | 75 | You can find usage examples and more documentation on the [wsk](https://bloomberg.github.io/wsk) and [wsk-notify](https://bloomberg.github.io/wsk-notify) sites. 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2017 Bloomberg Finance L.P. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | wsk [wsk](https://bloomberg.github.io/wsk) 2 | === 3 | 4 | [![Build status](https://img.shields.io/travis/bloomberg/wsk/master.svg?style=flat-square)](https://travis-ci.org/bloomberg/wsk) 5 | [![Coverage](https://img.shields.io/coveralls/github/bloomberg/wsk/master.svg?style=flat-square)](https://coveralls.io/github/bloomberg/wsk?branch=master) 6 | [![npm version](https://img.shields.io/npm/v/wsk.svg?style=flat-square)](https://www.npmjs.com/package/wsk) 7 | [![js-semistandard-style](https://img.shields.io/badge/code%20style-semistandard-00b9e7.svg?style=flat-square)](https://github.com/Flet/semistandard) 8 | 9 | > A straightforward and maintainable build system from the [Bloomberg Graphics team](https://www.bloomberg.com/graphics). 10 | 11 | * [Installation](#installation) 12 | * [About](#about) 13 | - [Built-in Functionality](#built-in-functionality) 14 | - [Architecture](#architecture) 15 | * [FAQ](#faq) 16 | - [How is it different?](#how-is-it-different) 17 | - [What are the advantages?](#what-are-the-advantages) 18 | - [Why not Gulp?](#why-not-gulp) 19 | - [Aren't streams useful, though?](#arent-streams-useful-though) 20 | - [But isn't it faster to load a file with a stream?](#but-isnt-it-faster-to-load-a-file-with-a-stream) 21 | - [How are task files different from plugins?](#how-are-task-files-different-from-plugins) 22 | - [Why not Webpack?](#why-not-webpack) 23 | - [Why not command-line libraries in npm scripts?](#why-not-command-line-libraries-in-npm-scripts) 24 | - [How did you design wsk?](#how-did-you-design-wsk) 25 | * [Usage](#usage) 26 | - [Creating A Watch File](#creating-a-watch-file) 27 | - [Specifying A `targetFile` For An Event](#specifying-a-targetfile-for-an-event) 28 | - [Creating A Task File](#creating-a-task-file) 29 | - [Package.json Setup](#packagejson-setup) 30 | - [Notifying Users](#notifying-users) 31 | * [Roadmap](#roadmap) 32 | * [Contributors](#contributors) 33 | * [License](#license) 34 | 35 | ## Installation 36 | 37 | ``` 38 | npm install --save wsk 39 | ``` 40 | 41 | ## About 42 | 43 | wsk is a small JavaScript library combined with a recommended architecture for creating a reliable build system. It was built by the Bloomberg Graphics and Data Journalism team as an open-ended and maintainable solution to building projects. It has allowed us to democratize our build system—allowing the greatest number of people the ability to modify it if need be—and delivers the right level of notification in order to quickly diagnose any JavaScript, CSS or templating errors that may arise. 44 | 45 | **[See an example project](https://bloomberg.github.io/wsk.example)** 46 | 47 | ### Built-in Functionality 48 | 49 | ![wsk layout](screenshots/wsk-watcher.png) 50 | 51 | At its core, wsk provides a specification around the [chokidar](https://github.com/paulmillr/chokidar) for watching one or more files and declaring scripts to run when those files are created, modified or deleted. The scripts that run are designed to be as arbitrary as possible so that wsk does not lock you into a plugin ecosystem that might get stale or you might want to work outside for a given task. 52 | 53 | This design optimizes for maintainability and clarity, without depending on intermediate wrapper plugins. 54 | 55 | ### Architecture 56 | 57 | Since wsk as few built-in opinions, you can configure it multiple ways. Here is one way based on some common build patterns. 58 | 59 | ![wsk layout](screenshots/wsk-layout.png) 60 | 61 | Generally, a build system has two modes of operation: **development** and **production**. 62 | 63 | In **development** mode, you want to be watching a set of files and when one of them changes, that file should rebuild. 64 | 65 | In **production** mode, you're acting across a group of files and building them all. 66 | 67 | In this setup, `npm run` commands in `package.json` will initialize watchers when in development mode and build files directly in production mode. 68 | 69 | It leverages npm scripts to orchestrate these commands and bash to handle parallelism to avoid reinventing the wheel. 70 | 71 | See how this system looks in production by looking at [the example project's source](https://www.github.com/bloomberg/wsk.example). 72 | 73 | ## FAQ 74 | 75 | ### How is it different? 76 | 77 | Its main differences are that it: 78 | 79 | 1. recommends project creators use libraries directly through the wsk task file pattern instead of relying on intermediate plugins such as you would in Gulp (e.g. `gulp-sass`, `gulp-live-server`). 80 | 2. recommends npm scripts as a consistent and powerful way to orchestrate these task files as opposed to building a task runner into your build system. 81 | 3. notifies the user of granular events with pertinent information. With wsk, it's clear how your task processes are interacting with each other and is verbose about any errors. 82 | 83 | ### What are the advantages? 84 | 85 | Through a combination of the above factors, wsk removes itself as much as possible from the list of things that can go wrong during your build. If your build step has an error, you'll have fewer layers to debug and its notifications will help you pinpoint the error more directly. 86 | 87 | The code inside of wsk concerns itself with the boring plumbing of watching files and then firing off task events when they've changed. By keeping the core library small and using existing JavaScript and npm patterns, it keeps you focused on what you want your build to do instead of learning wsk-specific concepts. 88 | 89 | ### Why not Gulp? 90 | 91 | Gulp can be great in getting you up to speed creating a project by leveraging the community of plugin developers. This can also be problematic, however, since sometimes plugins aren't well maintained or only work with a certain version of the target library. If plugin design switches between major versions, you might also have to redo a large portion of your system. 92 | 93 | This situation can be tricky to debug unless you're fairly familiar with Gulp infrastructure. Having these plugins in your node_modules folder, where they might be written in a different coding styles, also makes it harder for anyone to jump in, diagnose and fix the problem. In some cases, you might have to fork the plugin if the maintainer isn't quick to respond. 94 | 95 | Gulp also requires the use of advanced concepts such as streams and vinyl objects. Getting up to speed on the ins and outs of these technologies is non-trivial and introduces cognitive load that could be better spent elsewhere. 96 | 97 | Using libraries directly in a wsk task file pattern means you aren't depending on a wrapper plugin of dubious provenance. wsk tasks files are vanilla JavaScript that do not depend on any more advanced programming concepts than event names, file paths and functions. 98 | 99 | ### Aren't streams useful, though? 100 | 101 | Nothing in wsk precludes you from writing your tasks to use streams if you feel it is a better way of reading and writing files. Why Gulp uses streams is worth discussing, though, if you want to decide whether to use them in your project. 102 | 103 | Gulp uses streams to avoid the sin of its immediate predecessor, Grunt, of reading and writing intermediate files to disk, which caused very slow builds. 104 | 105 | As a solution, Gulp streams are a contract between itself and plugins for handing off files from transformation to transformation. By having a way to pass a file in memory, Gulp is much faster. 106 | 107 | The key here is that streams are a means to an end, not the end in itself. As the next section discusses, for most of the file I/O you're doing in a build system, you're not getting a big advantage with streams. 108 | 109 | ### But isn't it faster to load a file with a stream? 110 | 111 | In some cases. It seems that for files [over 10mb this is the case](https://medium.com/@dalaidunc/fs-readfile-vs-streams-to-read-text-files-in-node-js-5dd0710c80ea). But files in your build will be much smaller than that so you're not often getting the efficiencies streams would bring. 112 | 113 | if you're loading json files or a similar format that must be read in completely before the transformation can begin, you can't take advantage of a stream's ability to start acting on the file before its fully loaded. In addition, if you're using a Gulp plugin, there's no guarantee that the plugin has actually implemented streams in a way that you're getting the performance benefit unless you do a full code review. 114 | 115 | HTML templating is a good example of how you'd have to optimize far down the rabbit hole to get the full benefit (again, your files would still have to be very large). Let's say you are loading in an HTML file that pulls in other HTML partials. Unless those partials are also coming in as streams, the bulk of your markup compilation is most likely using `fs.readFile`. 116 | 117 | This is also the case with Rollup, which uses `fs.readFileSync` in [parts of its code](https://github.com/rollup/rollup/blob/9e481d890d333210de3364379b0b1004ee2169f7/src/watch/fileWatchers.js#L69) for example. 118 | 119 | gulp-sass, a widely used plugin with over a million monthly downloads [explicitly doesn't support streaming](https://github.com/dlmanning/gulp-sass/blob/035b759f51713e44f7c280f43cd1176e84b0f124/index.js#L26) since its underlying node-sass library requires either [a file path or a string](https://github.com/sass/node-sass/blob/e934a55d5a0433e8e1d483a485c4717c9a416b6c/README.md#file). 120 | 121 | Wrapping these libraries in a Gulp plugin doesn't get you around their own internal functionality. 122 | 123 | ### How are task files different from plugins? 124 | 125 | One of the design ideas we've implemented in these example projects is that task files—what would be plugins in a Gulp setup—are committed into each project instead of hosted on npm and required as modules. While this is not mandated by wsk, it has been a helpful practice for our team because it helps avoid some of the issues we've seen with the plugin ecosystems. It might not make sense for your setup and there are some drawbacks, so it's worth discussing. 126 | 127 | A convenience of plugins is that it's easier when someone has done the work for you, assuming the work they've done is what you're looking for. 128 | 129 | However, canonical plugins can get bloated with complex features you don't need. Or, they become too advanced and incompatible with your version of Node. Also, you might have to wait for a plugin update to support the latest version. 130 | 131 | Looking at the library and writing something as close to the "Hello, World" example as possible has been a sensible guideline for us. 132 | 133 | There's no reason why you can't write your own Gulp plugins this way, of course. In that scenario, however, your team needs to feel comfortable with Gulp conventions such as Node streams and vinyl objects, so there's larger investment in training. wsk is vanilla JavaScript/Node with no extra learning necessary. 134 | 135 | As background, our stories are independent projects that are built from a common scaffold. Committing each task file into the project gives the creator flexibility to make changes without having to 1) make a fork or create a pull request on a registered plugin 2) verify the change won't break previous projects that published without a problem 3) bump the version 4) change the package.json and reinstall. If a change is useful for future projects, such as a bug fix or an API update, we update the project scaffold. Those are rarer events and can happen after the story's deadline. 136 | 137 | wsk itself has very few baked in opinions about how plugins/task files must work, leaving possibilities open. 138 | 139 | So what *should* a wsk ecosystem look like? 140 | 141 | For now, let's say it works more like a template or cookbook where you look at an existing example to get going and implement that pattern in your project with any necessary changes. The [wsk example project](https://github.com/bloomberg/wsk.example/tree/master/build/tasks) has templates for [BrowserSync](https://browsersync.io/), HTML building with [Underscore.js templates](https://underscorejs.org#template), [Rollup](https://rollupjs.org/), [Stylus](https://stylus-lang.com/), [node-sass](https://github.com/sass/node-sass) and static file copying. 142 | 143 | ### Why not Webpack? 144 | 145 | Architecturally, it's much higher level than wsk. 146 | 147 | Webpack does a lot of really neat stuff but it's also monolithic; it's hard to break off just a piece of it or swap in something else. Whereas in Webpack, you give your project over to its way of doing things from the JavaScript to the CSS, wsk is designed to have each task file be easily interchangeable. 148 | 149 | ### Why not command-line libraries in npm scripts? 150 | 151 | JavaScript APIs expose more functionality than their command-line interfaces. Thus making a task file gives you more control than stringing command line arguments into an npm script. Command-line interfaces can make your `package.json` difficult to read and are tricky to comment and scale. Also, your notifications will all be in different styles at different levels of log specificity, which will confuse the user and make it more difficult to spot errors. 152 | 153 | ### How did you design wsk? 154 | 155 | Check out our [HOW-DID-WE-GET-HERE.md](https://github.com/bloomberg/wsk/blob/master/HOW-DID-WE-GET-HERE.md) document to read the illustrated journey. 156 | 157 | ## Usage 158 | 159 | ### Creating A Watch File 160 | 161 | In a wsk project, the idea is you have **files to watch** and **tasks that are run** when certain events happen to those files. You configure this relationship with a watcher file. Here's an example schema with defaults shown. Required fields are `serviceName`, `path` and an `events` array. 162 | 163 | ```js 164 | // Watch file 165 | 166 | var watcher = require('wsk').watcher; 167 | 168 | // This can be an object or an array of objects 169 | var watchGroups = [ 170 | { 171 | serviceName: 'stylus', // Give this watcher a name so we can use it to notify project creators what is happening 172 | path: 'src/css/**/*.styl', // A glob or an array of globs 173 | ignoreDotFiles: true, // Defaults to ignoring files that start with a `.`. 174 | chokidarOptions: { // Any options that can be passed to chokidar https://github.com/paulmillr/chokidar#api 175 | ignoreInitial: true 176 | }, 177 | warnIfMissingPath: true, // By default, wsk will warn you (but proceed normally) if you have entered a non-glob path (or an array that contains a non-glob path) for a file that does not exist. This is because it's easy to get file paths wrong and it's nice to be told so. Sometimes, you will be watching a non-existent file by design. So, you can disable this warning by setting this to `false`. 178 | displayOptions: { 179 | hideAll: false, // Toggle the notification of what is being watched. 180 | hideChildFiles: false, // If path is a glob, toggle whether to show files in that glob 181 | hideChildDirs: false // If path is a glob, toggle whether to show directories in that glob 182 | // `hideAll` is different from setting both `hideChildFiles` and `hideChildDirs` are `true` because in the latter case, the bundle notification will still display. 183 | }, 184 | // Specify an array of events that will be listened to and the files that will be run when those events occur. This array is empty by default. 185 | // Event types can be anything that chokidar supports: https://github.com/paulmillr/chokidar#methods--events 186 | events: [ 187 | { 188 | type: 'change', 189 | taskFiles: 'path/to/stylus-build.js', 190 | options: { 191 | myOption: true // You can pass an options object which will be the third argument to the task file's `onEvent` function. 192 | } 193 | }, 194 | { 195 | type: 'add', 196 | taskFiles: ['path/to/stylus-build.js', 'path/to/other-task.js'], 197 | commands: ['python path/to/python.py'], 198 | env: { 199 | myEnvVariable: true // Same thing to `options` for task files except these get sent to `process.env` object. 200 | } 201 | }, 202 | { 203 | type: 'unlink', 204 | taskFiles: 'path/to/stylus-build.js', 205 | commands: 'npm run other-task' 206 | } 207 | ] 208 | } 209 | ]; 210 | 211 | // Add this watch group, will return an error or at list of the chokidar .getWatched() objects on done 212 | watcher.add(watchGroups, function (err, watchTrees) { 213 | if (err) { 214 | console.error(err); 215 | return; 216 | } 217 | // An array of directory tree objects for every watchGroup. 218 | // If you've passed an array of watchGroups, the callback happens after they are all done 219 | console.log(watchTree); 220 | /* 221 | [{ 222 | 'src/css': [ 'styles.styl', 'embed.styl' ], 223 | 'src/css/modules': [ 'colors.styl' ] 224 | }] 225 | */ 226 | }); 227 | ``` 228 | 229 | **Note:** wsk uses [chokidar](https://npmjs.org/package/chokidar) for its event watching, which [supports](https://github.com/paulmillr/chokidar#methods--events) the following events: 230 | 231 | > `add`, `addDir`, `change`, `unlink`, `unlinkDir`, `ready`, `raw`, `error`. Additionally `all` is available which gets emitted with the underlying event name and path for every event other than `ready`, `raw`, and `error`. 232 | 233 | #### Specifying A `targetFile` For An Event 234 | 235 | Your event configurations can also specify replacement or additional files to be acted on. This can be useful if you want to delete a corresponding sourcemap if a CSS file is deleted, for example. 236 | 237 | ```js 238 | // within the events array... 239 | { 240 | type: 'unlink', 241 | taskFiles: 'path/to/unlink.js', 242 | // this can also be a single string 243 | targetFiles: [ 244 | 'public/css/styles.css', 245 | 'public/css/styles.css.map' 246 | ] 247 | } 248 | 249 | // or give it a function that gets passed the file that triggered the event 250 | 251 | { 252 | type: 'unlink', 253 | taskFiles: 'path/to/unlink.js', 254 | targetFiles: function (filePath) { 255 | // filePath is the deleted file `src/css/filename.scss 256 | var outPath = filePath.replace('src', 'public').replace('.scss', '.css') 257 | return [ 258 | outPath, 259 | outPath + '.map' 260 | ] // This can return a string or an array 261 | } 262 | } 263 | ``` 264 | 265 | ### Creating A Task File 266 | 267 | Here is a basic task file setup. Its `onEvent` function runs whenever the files specified in the watch file `path` glob has the specified action. 268 | 269 | ```js 270 | // Task file 271 | 272 | // Load our notification module 273 | var fs = require('fs'); 274 | var notify = require('wsk').notify; 275 | 276 | // To run on an event specified in your watcher, it must export an `onEvent` function. 277 | // This function gets passed three arguments: 278 | // `eventType`, name of the event specified in your watch file 279 | // `changedPath` is the path to the changed file or directory 280 | // An optional `options` hash that comes from the watcher event specification. 281 | function onEvent (eventType, changedPath, options) { 282 | // Here write JavaScript API code for your target library 283 | // to transform the changed file... 284 | myRenderLib(changedPath, (err, code) => { 285 | if (err) { 286 | // Log out what we're doing to the user 287 | notify({ 288 | message: 'Error converting file to foo format...', 289 | value: changedPath, 290 | display: 'error', 291 | error: err 292 | }); 293 | } else { 294 | notify({ 295 | message: 'We did something to this file:', 296 | value: changedPath, 297 | display: 'compile' 298 | }); 299 | // Write out the file 300 | var outPath = changedPath.replace('src', 'public'); 301 | fs.writeFileSync(outPath, code); 302 | } 303 | }); 304 | } 305 | 306 | // Export a public function called `onEvent`, which is what watcher expects 307 | module.exports = { 308 | onEvent: onEvent 309 | }; 310 | ``` 311 | 312 | See the [wsk example project](https://github.com/bloomberg/wsk.example/tree/master/build/tasks) for production examples. 313 | 314 | ### Package.json Setup 315 | 316 | In this pattern, you run your watch files with an npm command such as `npm run dev` and build your project with `npm run build`. You can configure your scripts however you like but these docs will discuss the conventions in our example. 317 | 318 | Each dev command starts with `dev:` and points to watcher files. Build commands start with `build:` and point to build task files. We add a `predev` tasks that builds your files every time you start up the dev process. 319 | 320 | ```json 321 | { 322 | "scripts": { 323 | "dev:css": "node build/tasks/watch-css.js", 324 | "dev:js": "node build/tasks/watch-js.js", 325 | "dev:html": "node build/tasks/watch-html.js", 326 | "build:css": "node build/tasks/build-css.js", 327 | "build:js": "node build/tasks/build-js.js", 328 | "build:html": "node build/tasks/build-html.js", 329 | "dev": "npm run dev:css --silent && npm run dev:js --silent && npm run dev:html --silent", 330 | "build": "npm run build:css --silent && npm run build:js --silent && npm run build:html --silent", 331 | "predev": "npm run build --silent" 332 | } 333 | } 334 | ``` 335 | 336 | **Note:** The `--silent` flag is optional but including it will make your console output more readable and easier to follow along with. It supresses boilerplate npm output. 337 | 338 | ### Notifying Users 339 | 340 | wsk uses the [wsk-notify](https://github.com/bloomberg/wsk-notify) library to handle styling. One cool thing to point out is that errors appear as desktop notifications, which is very helpful. 341 | 342 | You can take a look at [the wsk-notify documentation](https://bloomberg.github.io/wsk-notify) for full options and styles. The basic usage is as follows: 343 | 344 | ```js 345 | var notify = require('wsk').notify; 346 | 347 | notify({ 348 | message: 'My message...', 349 | value: 'value', 350 | display: 'compile' // Can be any of the following: 'add', 'change', 'compile', 'error', 'reload', 'remove', 'serve', 'success', 'watch' or 'warn' 351 | }); 352 | ``` 353 | 354 | The basic compile display style looks like this: 355 | 356 | ![compile display](screenshots/compile.png) 357 | 358 | Here what `npm run build` gives you: 359 | 360 | ![wsk build example](screenshots/build.png) 361 | 362 | And here is what `npm run dev` gives you: 363 | 364 | ![wsk watch example](screenshots/dev.png) 365 | 366 | ## Roadmap 367 | 368 | Here are features that we would like to integrate into future versions of wsk. 369 | 370 | 1. Dependency tree creation. It would be nice if the user could supply a function that defines a way to find dependency in a given file. This could be either by getting passed an AST or through simple string matching. For example, if you always read templates through a `h.readTemplate` function, you could extract those files recursively. 371 | 2. Pass in a clock object or timestamp to a taskfile for easy timing. 372 | 3. Set up a "Resolved" notification style so you can know that your error was fixed via desktop notification. 373 | 374 | ## Contributors 375 | 376 | * [Michael Keller](https://twitter.com/mhkeller) 377 | * [Jeremy Diamond](https://twitter.com/_jsdiamond) 378 | * [Blacki Migliozzi](https://twitter.com/blackili) 379 | 380 | ## License 381 | 382 | Apache-2.0 383 | --------------------------------------------------------------------------------