diff --git a/Makefile b/Makefile index b0bde6e6..bc91c1cc 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ BIN := $(THIS_DIR)/node_modules/.bin NODE ?= $(shell which node) NPM ?= $(NODE) $(shell which npm) BROWSERIFY ?= $(NODE) $(BIN)/browserify +MOCHA ?= $(NODE) $(BIN)/_mocha all: dist/debug.js @@ -21,7 +22,7 @@ clean: dist: @mkdir -p $@ -dist/debug.js: node_modules browser.js debug.js dist +dist/debug.js: node_modules browser.js debug.js able.js | dist @$(BROWSERIFY) \ --standalone debug \ . > $@ @@ -30,4 +31,7 @@ node_modules: package.json @NODE_ENV= $(NPM) install @touch node_modules -.PHONY: all install clean +test: + @$(MOCHA) -R dot + +.PHONY: all install clean test diff --git a/able.js b/able.js new file mode 100644 index 00000000..e8a03238 --- /dev/null +++ b/able.js @@ -0,0 +1,148 @@ +/** + * in-memory cache + */ + +var enabled = []; +var disabled = []; + +/** + * Checks if the specified `ns` is enabled. + * + * @param {String} ns Wildcards are not supported here + * @returns {Boolean} + */ + +exports.enabled = function (ns) { + if (find(disabled, ns, true) > -1) return false; + if (find(enabled, ns, true) > -1) return true; + return false; +}; + +/** + * Destroys the lists of enabled/disabled. (primarilly for testing purposes) + */ + +exports.clear = function () { + disabled.length = enabled.length = 0; // truncates w/o destroying +}; + +/** + * Outputs the list of enable/disabled in a single string. + * + * @returns {String} + */ + +exports.stringify = function () { + var e = enabled.map(function (item) { + return item.string; + }); + + var d = disabled.map(function (item) { + return '-' + item.string; + }); + + return e.concat(d).join(','); +}; + +/** + * Parses a list and enables/disables accordingly. + * + * This list can either be passed from the user directly, or from .stringify() + * + * @param {String} str + */ + +exports.parse = function (str) { + if (!str) return []; + return (str || '').trim().split(/[\s,]+/).filter(function (item) { + return !!item; + }); +}; + +/** + * Enables the specified `ns`. + * + * @param {String} ns + */ + +exports.enable = function (ns) { + // special case, empty the current list and allow it to append + if ('*' == ns) exports.clear(); + + prune(disabled, ns); + if (find(enabled, ns) > -1) return; + + enabled.push({ + string: ns, + regex: regex(ns) + }); +}; + +/** + * Disables the specified `ns`. + * + * @param {String} ns + */ + +exports.disable = function (ns) { + // special case, empty the current list (since default is to allow nothing) + if ('*' == ns) return exports.clear(); + + prune(enabled, ns); + if (find(disabled, ns) > -1) return; + + disabled.push({ + string: ns, + regex: regex(ns) + }); +}; + +/** + * Searches for the given `ns` in the `arr`. + * + * By default, it only matches on the raw string, but if `regex` is set, it will + * match via the `RegExp` instead. + * + * Returns the index of the match, or -1 if not found. + * + * @param {Array} arr + * @param {String} ns + * @param {Boolean} [regex] + * @returns {Number} + */ + +function find(arr, ns, regex) { + var ret = -1; + arr.some(function (item, x) { + if (regex ? item.regex.test(ns) : ns === item.string) { + ret = x; + return true; + } + }); + return ret; +} + +/** + * Wraps around `find(...)`, but also removes the found item. + * + * @param {Array} arr + * @param {String} ns + * @param {Boolean} [regex] + */ + +function prune(arr, ns, regex) { + var x = find(arr, ns, regex); + if (x > -1) arr.splice(x, 1); +} + +/** + * Converts a raw `ns` into a `RegExp`. + * + * @param {String} ns + * @returns {RegExp} + */ + +function regex(ns) { + var pattern = ns.replace(/\*/g, '.*?'); + return new RegExp('^' + pattern + '$'); +} diff --git a/browser.js b/browser.js index 55f4cf92..44b743b9 100644 --- a/browser.js +++ b/browser.js @@ -126,8 +126,10 @@ function log() { * @api private */ -function save(namespaces) { +function save() { try { + var namespaces = able.stringify(); + if (null == namespaces) { storage.removeItem('debug'); } else { diff --git a/component.json b/component.json index 71e1abb7..6786dc03 100644 --- a/component.json +++ b/component.json @@ -11,7 +11,8 @@ "main": "browser.js", "scripts": [ "browser.js", - "debug.js" + "debug.js", + "able.js" ], "dependencies": { "guille/ms.js": "0.6.1" diff --git a/debug.js b/debug.js index 7571a860..28d89740 100644 --- a/debug.js +++ b/debug.js @@ -1,4 +1,10 @@ +/** + * Enabled/disabled status management + */ + +var able = require('./able'); + /** * This is the common logic for both the Node.js and web browser * implementations of `debug()`. @@ -8,17 +14,12 @@ exports = module.exports = debug; exports.coerce = coerce; -exports.disable = disable; -exports.enable = enable; -exports.enabled = enabled; exports.humanize = require('ms'); +exports.dynamic = dynamic; +exports.enable = enable; +exports.disable = disable; +exports.enabled = able.enabled; -/** - * The currently active debug mode names, and names to skip. - */ - -exports.names = []; -exports.skips = []; /** * Map of special "%n" handling functions, for the debug "format" argument. @@ -28,6 +29,12 @@ exports.skips = []; exports.formatters = {}; +/** + * Flag for dynamic status. + */ + +var isDynamic = false; + /** * Previously assigned color. */ @@ -117,81 +124,73 @@ function debug(namespace) { logFn.apply(self, args); } enabled.enabled = true; + enabled.namespace = namespace; - var fn = exports.enabled(namespace) ? enabled : disabled; + function dynamic() { + if (!exports.enabled(namespace)) return disabled(); + return enabled.apply(enabled, arguments); + } + dynamic.namespace = namespace; - fn.namespace = namespace; + function fn() { + if (isDynamic) return dynamic; + return exports.enabled(namespace) ? enabled : disabled; + } - return fn; + return fn(); } /** - * Enables a debug mode by namespaces. This can include modes - * separated by a colon and wildcards. + * Coerce `val`. * - * @param {String} namespaces - * @api public + * @param {Mixed} val + * @return {Mixed} + * @api private */ -function enable(namespaces) { - exports.save(namespaces); - - var split = (namespaces || '').split(/[\s,]+/); - var len = split.length; - - for (var i = 0; i < len; i++) { - if (!split[i]) continue; // ignore empty strings - namespaces = split[i].replace(/\*/g, '.*?'); - if (namespaces[0] === '-') { - exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$')); - } else { - exports.names.push(new RegExp('^' + namespaces + '$')); - } - } +function coerce(val) { + if (val instanceof Error) return val.stack || val.message; + return val; } /** - * Disable debug output. + * Get/set the dynamic flag * - * @api public + * @param {Boolean} [flag] + * @returns {Boolean} */ -function disable() { - exports.enable(''); +function dynamic(flag) { + if (0 == arguments.length) return isDynamic; + isDynamic = !!flag; } /** - * Returns true if the given mode name is enabled, false otherwise. + * Enables a string of namespaces (disables those with hyphen prefixes) * - * @param {String} name - * @return {Boolean} - * @api public + * @param {String} namespaces */ -function enabled(name) { - var i, len; - for (i = 0, len = exports.skips.length; i < len; i++) { - if (exports.skips[i].test(name)) { - return false; - } - } - for (i = 0, len = exports.names.length; i < len; i++) { - if (exports.names[i].test(name)) { - return true; - } - } - return false; +function enable(namespaces) { + able.parse(namespaces).forEach(function (ns) { + if ('-' == ns[0]) able.disable(ns.slice(1)); + else able.enable(ns); + }); + + exports.save(); } /** - * Coerce `val`. + * Disables a string of namespaces (ignores hyphen prefixs if found) * - * @param {Mixed} val - * @return {Mixed} - * @api private + * @param {String} namespaces */ -function coerce(val) { - if (val instanceof Error) return val.stack || val.message; - return val; +function disable(namespaces) { + able.parse(namespaces).forEach(function (ns) { + if ('-' == ns[0]) ns = ns.slice(1); + able.disable(ns); + }); + + exports.save(); } diff --git a/dist/debug.js b/dist/debug.js index 6045bd23..e50a8149 100644 --- a/dist/debug.js +++ b/dist/debug.js @@ -1,4 +1,160 @@ !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.debug=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o -1) return false; + if (find(enabled, ns, true) > -1) return true; + return false; +}; + +/** + * Destroys the lists of enabled/disabled. (primarilly for testing purposes) + */ + +exports.clear = function () { + disabled.length = enabled.length = 0; // truncates w/o destroying +}; + +/** + * Outputs the list of enable/disabled in a single string. + * + * @returns {String} + */ + +exports.stringify = function () { + var e = enabled.map(function (item) { + return item.string; + }); + + var d = disabled.map(function (item) { + return '-' + item.string; + }); + + return e.concat(d).join(','); +}; + +/** + * Parses a list and enables/disables accordingly. + * + * This list can either be passed from the user directly, or from .stringify() + * + * @param {String} str + */ + +exports.parse = function (str) { + if (!str) return []; + return (str || '').trim().split(/[\s,]+/).filter(function (item) { + return !!item; + }); +}; + +/** + * Enables the specified `ns`. + * + * @param {String} ns + */ + +exports.enable = function (ns) { + // special case, empty the current list and allow it to append + if ('*' == ns) exports.clear(); + + prune(disabled, ns); + if (find(enabled, ns) > -1) return; + + enabled.push({ + string: ns, + regex: regex(ns) + }); +}; + +/** + * Disables the specified `ns`. + * + * @param {String} ns + */ + +exports.disable = function (ns) { + // special case, empty the current list (since default is to allow nothing) + if ('*' == ns) return exports.clear(); + + prune(enabled, ns); + if (find(disabled, ns) > -1) return; + + disabled.push({ + string: ns, + regex: regex(ns) + }); +}; + +/** + * Searches for the given `ns` in the `arr`. + * + * By default, it only matches on the raw string, but if `regex` is set, it will + * match via the `RegExp` instead. + * + * Returns the index of the match, or -1 if not found. + * + * @param {Array} arr + * @param {String} ns + * @param {Boolean} [regex] + * @returns {Number} + */ + +function find(arr, ns, regex) { + var ret = -1; + arr.some(function (item, x) { + if (regex ? item.regex.test(ns) : ns === item.string) { + ret = x; + return true; + } + }); + return ret; +} + +/** + * Wraps around `find(...)`, but also removes the found item. + * + * @param {Array} arr + * @param {String} ns + * @param {Boolean} [regex] + */ + +function prune(arr, ns, regex) { + var x = find(arr, ns, regex); + if (x > -1) arr.splice(x, 1); +} + +/** + * Converts a raw `ns` into a `RegExp`. + * + * @param {String} ns + * @returns {RegExp} + */ + +function regex(ns) { + var pattern = ns.replace(/\*/g, '.*?'); + return new RegExp('^' + pattern + '$'); +} + +},{}],2:[function(require,module,exports){ + +/** + * Enabled/disabled status management + */ + +var able = require('./able'); /** * This is the common logic for both the Node.js and web browser @@ -9,17 +165,12 @@ exports = module.exports = debug; exports.coerce = coerce; -exports.disable = disable; -exports.enable = enable; -exports.enabled = enabled; exports.humanize = require('ms'); +exports.dynamic = dynamic; +exports.enable = enable; +exports.disable = disable; +exports.enabled = able.enabled; -/** - * The currently active debug mode names, and names to skip. - */ - -exports.names = []; -exports.skips = []; /** * Map of special "%n" handling functions, for the debug "format" argument. @@ -29,6 +180,12 @@ exports.skips = []; exports.formatters = {}; +/** + * Flag for dynamic status. + */ + +var isDynamic = false; + /** * Previously assigned color. */ @@ -118,86 +275,78 @@ function debug(namespace) { logFn.apply(self, args); } enabled.enabled = true; + enabled.namespace = namespace; - var fn = exports.enabled(namespace) ? enabled : disabled; + function dynamic() { + if (!exports.enabled(namespace)) return disabled(); + return enabled.apply(enabled, arguments); + } + dynamic.namespace = namespace; - fn.namespace = namespace; + function fn() { + if (isDynamic) return dynamic; + return exports.enabled(namespace) ? enabled : disabled; + } - return fn; + return fn(); } /** - * Enables a debug mode by namespaces. This can include modes - * separated by a colon and wildcards. + * Coerce `val`. * - * @param {String} namespaces - * @api public + * @param {Mixed} val + * @return {Mixed} + * @api private */ -function enable(namespaces) { - exports.save(namespaces); - - var split = (namespaces || '').split(/[\s,]+/); - var len = split.length; - - for (var i = 0; i < len; i++) { - if (!split[i]) continue; // ignore empty strings - namespaces = split[i].replace(/\*/g, '.*?'); - if (namespaces[0] === '-') { - exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$')); - } else { - exports.names.push(new RegExp('^' + namespaces + '$')); - } - } +function coerce(val) { + if (val instanceof Error) return val.stack || val.message; + return val; } /** - * Disable debug output. + * Get/set the dynamic flag * - * @api public + * @param {Boolean} [flag] + * @returns {Boolean} */ -function disable() { - exports.enable(''); +function dynamic(flag) { + if (1 == arguments.length) return isDynamic; + isDynamic = !!flag; } /** - * Returns true if the given mode name is enabled, false otherwise. + * Enables a string of namespaces (disables those with hyphen prefixes) * - * @param {String} name - * @return {Boolean} - * @api public + * @param {String} namespaces */ -function enabled(name) { - var i, len; - for (i = 0, len = exports.skips.length; i < len; i++) { - if (exports.skips[i].test(name)) { - return false; - } - } - for (i = 0, len = exports.names.length; i < len; i++) { - if (exports.names[i].test(name)) { - return true; - } - } - return false; +function enable(namespaces) { + able.parse(namespaces).forEach(function (ns) { + if ('-' == ns[0]) able.disable(ns.slice(1)); + else able.enable(ns); + }); + + exports.save(); } /** - * Coerce `val`. + * Disables a string of namespaces (ignores hyphen prefixs if found) * - * @param {Mixed} val - * @return {Mixed} - * @api private + * @param {String} namespaces */ -function coerce(val) { - if (val instanceof Error) return val.stack || val.message; - return val; +function disable(namespaces) { + able.parse(namespaces).forEach(function (ns) { + if ('-' == ns[0]) ns = ns.slice(1); + able.disable(ns); + }); + + exports.save(); } -},{"ms":2}],2:[function(require,module,exports){ +},{"./able":1,"ms":3}],3:[function(require,module,exports){ /** * Helpers. */ @@ -310,7 +459,7 @@ function plural(ms, n, name) { return Math.ceil(ms / n) + ' ' + name + 's'; } -},{}],3:[function(require,module,exports){ +},{}],4:[function(require,module,exports){ /** * This is the web browser implementation of `debug()`. @@ -428,8 +577,10 @@ function log() { * @api private */ -function save(namespaces) { +function save() { try { + var namespaces = able.stringify(); + if (null == namespaces) { localStorage.removeItem('debug'); } else { @@ -459,5 +610,5 @@ function load() { exports.enable(load()); -},{"./debug":1}]},{},[3])(3) +},{"./debug":2}]},{},[4])(4) }); \ No newline at end of file diff --git a/example/dynamic/app.js b/example/dynamic/app.js new file mode 100644 index 00000000..ab263525 --- /dev/null +++ b/example/dynamic/app.js @@ -0,0 +1,43 @@ +// NOTE - run using: DEBUG=example DEBUG_DYN=1 node app.js + +var debug = require('../..') + , express = require('express') + , bodyParser = require('body-parser') + , app = express() + , name = 'example'; + +var log = debug(name) +log('booting example app'); + +app.use(bodyParser.urlencoded({ extended: false })); + +app.use(function (req, res, next) { + log(req.method + ' ' + req.url); + next(); +}); + +app.get('/', function (req, res) { + log('sending form'); + res.sendFile(__dirname + '/index.html'); +}); + +app.post('/debug', function (req, res) { + if ('enable' in req.body) { + log('enabling'); + debug.enable(name); + } else { + log('disabling'); + debug.disable(name); + } + res.redirect('/'); +}); + +app.post('/disable', function (req, res) { + log('disabling'); + debug.disable(name); + res.redirect('/'); +}); + +app.listen(3000, function () { + log('listening'); +}); diff --git a/example/dynamic/index.html b/example/dynamic/index.html new file mode 100644 index 00000000..88216854 --- /dev/null +++ b/example/dynamic/index.html @@ -0,0 +1,4 @@ +
+ + +
diff --git a/node.js b/node.js index 5dc999fe..63099eb2 100644 --- a/node.js +++ b/node.js @@ -19,6 +19,12 @@ exports.save = save; exports.load = load; exports.useColors = useColors; +/** + * Enabled/disabled lists management. + */ + +var able = require('./able'); + /** * Colors. */ @@ -73,6 +79,21 @@ exports.formatters.o = function(v) { .replace(/\s*\n\s*/g, ' '); }; +var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', + 'Oct', 'Nov', 'Dec']; + +function pad(n) { + return n < 10 ? '0' + n.toString(10) : n.toString(10); +} + +function timestamp() { + var d = new Date(); + var time = [pad(d.getHours()), + pad(d.getMinutes()), + pad(d.getSeconds())].join(':'); + return [d.getDate(), months[d.getMonth()], time].join(' '); +} + /** * Adds ANSI color escape codes if enabled. * @@ -88,11 +109,11 @@ function formatArgs() { var c = this.color; args[0] = ' \u001b[9' + c + 'm' + name + ' ' - + '\u001b[0m' + + '\u001b[90m' + args[0] + '\u001b[3' + c + 'm' + ' +' + exports.humanize(this.diff) + '\u001b[0m'; } else { - args[0] = new Date().toUTCString() + args[0] = timestamp() + ' ' + name + ' ' + args[0]; } return args; @@ -109,11 +130,12 @@ function log() { /** * Save `namespaces`. * - * @param {String} namespaces * @api private */ -function save(namespaces) { +function save() { + var namespaces = able.stringify(); + if (null == namespaces) { // If you set a process.env field to null or undefined, it gets cast to the // string 'null' or 'undefined'. Just delete instead. @@ -202,6 +224,14 @@ function createWritableStdioStream (fd) { return stream; } +/** + * Enable dynamic mode when `process.env.DEBUG_DYN` is set to `1` + */ + +if (process.env.DEBUG_DYN) { + exports.dynamic(process.env.DEBUG_DYN == 1); +} + /** * Enable namespaces listed in `process.env.DEBUG` initially. */ diff --git a/package.json b/package.json index f840d0d0..3f787215 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { - "name": "debug", + "name": "@sbks/debug", "version": "2.1.1", "repository": { "type": "git", - "url": "git://github.com/visionmedia/debug.git" + "url": "git://github.com/wision/debug.git" }, "description": "small debugging utility", "keywords": [ @@ -20,15 +20,18 @@ "ms": "0.6.2" }, "devDependencies": { + "body-parser": "^1.9.2", "browserify": "6.1.0", - "mocha": "*" + "express": "^4.10.2", + "mocha": "^2.0.1" }, "main": "./node.js", "browser": "./browser.js", "component": { "scripts": { "debug/index.js": "browser.js", - "debug/debug.js": "debug.js" + "debug/debug.js": "debug.js", + "debug/able.js": "able.js" } } } diff --git a/test/able.js b/test/able.js new file mode 100644 index 00000000..41769ee9 --- /dev/null +++ b/test/able.js @@ -0,0 +1,116 @@ +var able = require('../able'); +var assert = require('assert'); + +describe('enable/disable feature', function () { + afterEach(able.clear); + + describe('.enabled(ns)', function () { + it('should return booleans', function () { + assert.strictEqual(able.enabled('foo'), false); + able.enable('*'); + assert.strictEqual(able.enabled('foo'), true); + }); + + it('should not allow anything by default', function () { + assert(!able.enabled('express')); + assert(!able.enabled('connect')); + }); + + it('should work with basic strings', function () { + able.enable('express'); + assert(able.enabled('express')); + assert(!able.enabled('connect')); + }); + + it('should work with wildcards', function () { + able.enable('express*'); + assert(able.enabled('express')); + assert(able.enabled('express:router')); + assert(!able.enabled('connect')); + }); + }); + + describe('.stringify()', function () { + it('should return a string', function () { + assert.equal(typeof able.stringify(), 'string'); + }); + + it('should return a comma-separated list of enabled namespaces', function () { + able.enable('a'); + able.enable('b'); + able.enable('c'); + assert.equal(able.stringify(), 'a,b,c'); + }); + + it('should prefix disabled namespaces with a hyphen', function () { + able.enable('*'); + able.disable('express'); + able.disable('connect'); + assert.equal(able.stringify(), '*,-express,-connect'); + }); + }); + + describe('.parse(str)', function () { + it('should respect either white-space or comma separated values', function () { + able.parse('a,b c , d e'); + assert(able.enabled('a')); + assert(able.enabled('b')); + assert(able.enabled('c')); + assert(able.enabled('d')); + assert(able.enabled('e')); + }); + + it('should treat hyphen prefixs as disabled', function () { + able.enable('*'); + able.parse('-b,-c, -d -e'); + assert(able.enabled('a')); + assert(!able.enabled('b')); + assert(!able.enabled('c')); + assert(!able.enabled('d')); + assert(!able.enabled('e')); + assert(able.enabled('f')); + }); + }); + + describe('.enable(ns)', function () { + it('should enable the specified namespace', function () { + able.enable('express'); + assert(able.enabled('express')); + }); + + it('should enable the specified wildcard namespace', function () { + able.enable('express*'); + assert(able.enabled('express:router')); + }); + + it('should work correctly even when previously disabled', function () { + able.enable('*'); + assert(able.enabled('express')); + able.disable('express'); + assert(!able.enabled('express')); + able.enable('express'); + assert(able.enabled('express')); + }); + }); + + describe('.disable(ns)', function () { + it('should disable the specified namespace', function () { + able.enable('*'); + able.disable('express'); + assert(!able.enabled('express')); + }); + + it('should disable the specified wildcard namespace', function () { + able.enable('*'); + able.disable('express*'); + assert(!able.enabled('express:router')); + }); + + it('should work correctly even when previously enabled', function () { + able.enable('express'); + assert(able.enabled('express')); + able.disable('express'); + assert(!able.enabled('express')); + }); + }); +});