├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── debug.ts ├── debug_test.ts ├── demo.png ├── demo.ts ├── format.ts ├── format_test.ts ├── test.ts └── utils.ts /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | install: 4 | - curl -L https://deno.land/x/install/install.sh | sh 5 | - export PATH="$HOME/.deno/bin:$PATH" 6 | 7 | script: 8 | - deno --version 9 | - make test 10 | 11 | cache: 12 | directories: 13 | - "$HOME/.deno" 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Nikola Ristic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: fmt test 2 | 3 | demo: 4 | DEBUG=* deno run --allow-env ./demo.ts 5 | 6 | test: 7 | DEBUG=* deno test --allow-env 8 | 9 | fmt: 10 | deno fmt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deno Debug [![Build Status](https://travis-ci.org/rista404/deno-debug.svg?branch=master)](https://travis-ci.org/rista404/deno-debug) 2 | 3 | 4 | 5 | Debug utility for deno. 6 | 7 | ## Usage 8 | 9 | ```javascript 10 | import debug from "https://deno.land/x/debuglog/debug.ts"; 11 | 12 | // create debugger 13 | const service = debug("service"); 14 | 15 | const serviceName = "app"; 16 | 17 | // log 18 | service("booting %s", serviceName); 19 | ``` 20 | 21 | Then run your app. 22 | 23 | ```sh 24 | > DEBUG=* deno run --allow-env app.ts 25 | ``` 26 | 27 | ## Todo 28 | 29 | - [x] extending debuggers 30 | - [x] custom log functions 31 | - [x] custom formatters 32 | - [x] `log` override in all namespaces 33 | - [x] inspect opts 34 | - [x] detecting color support 35 | - [ ] non-tty env 36 | - [x] add `debug` to registry 37 | 38 | ## Notes 39 | 40 | - Currently debug assumes it is TTY and shows colors by default. 41 | - Deno's `inspect` differs from node's `util.inspect` so the output may not be 42 | the same. 43 | - We're using a custom `format` function ported from `util`. Might be cool to 44 | extract it when `util` is ported entirely. 45 | - We should cover more functionality with tests. 46 | -------------------------------------------------------------------------------- /debug.ts: -------------------------------------------------------------------------------- 1 | const { noColor } = Deno; 2 | import { ms } from "https://deno.land/x/ms/ms.ts"; 3 | import format from "./format.ts"; 4 | import { coerce, regexpToNamespace, selectColor } from "./utils.ts"; 5 | 6 | const DEBUG = { 7 | debug: "", 8 | }; 9 | 10 | interface DebugInstance { 11 | (log: string | Error, ...args: any[]): void; 12 | namespace: string; 13 | enabled: boolean; 14 | color: number; 15 | destroy: () => boolean; 16 | extend: (namespace: string, delimiter?: string) => DebugInstance; 17 | log?: Function; 18 | } 19 | 20 | interface DebugModule { 21 | (namespace: string): DebugInstance; 22 | enable: (namespaces: any) => void; 23 | disable: () => string; 24 | enabled: (namespace: string) => boolean; 25 | names: RegExp[]; 26 | skips: RegExp[]; 27 | formatters: Formatters; 28 | log: Function; 29 | } 30 | 31 | interface Formatters { 32 | [key: string]: (value: any) => string; 33 | } 34 | 35 | /** 36 | * Active `debug` instances. 37 | */ 38 | let instances: DebugInstance[] = []; 39 | 40 | /** 41 | * The currently active debug mode names, and names to skip. 42 | */ 43 | let names: RegExp[] = []; 44 | let skips: RegExp[] = []; 45 | 46 | /** 47 | * Map of special "%n" handling functions, for the debug "format" argument. 48 | * 49 | * Valid key names are a single, lower or upper-case letter, i.e. "n" and "N". 50 | */ 51 | let formatters: Formatters = {}; 52 | 53 | /** 54 | * Create a debugger with the given `namespace`. 55 | */ 56 | function createDebug(namespace: string): DebugInstance { 57 | let prevTime: number; 58 | 59 | let debug: DebugInstance; 60 | 61 | // @ts-ignore-next-line 62 | debug = function (log: string | Error, ...args: any[]) { 63 | // Skip if debugger is disabled 64 | if (!debug.enabled) { 65 | return; 66 | } 67 | 68 | const self = debug; 69 | 70 | log = coerce(log); 71 | 72 | if (typeof log !== "string") { 73 | // Anything else let's inspect with %O 74 | args.unshift(log); 75 | log = "%O"; 76 | } 77 | 78 | // Set `diff` timestamp 79 | const currTime = Number(Date.now()); 80 | // Difference in miliseconds 81 | const diff = currTime - (prevTime || currTime); 82 | prevTime = currTime; 83 | 84 | // Apply all custom formatters to our arguments 85 | const customFormattedArgs = applyFormatters.call(self, log, ...args); 86 | const { namespace, color } = self; 87 | 88 | // Format the string before logging 89 | const formattedArgs = formatArgs( 90 | { namespace, color, diff }, 91 | customFormattedArgs, 92 | ); 93 | 94 | // Use a custom logger if defined 95 | // If not, we use the default logger 96 | const logFn = self.log || debugModule.log; 97 | 98 | // Finally, log 99 | logFn.apply(self, formattedArgs); 100 | return; 101 | }; 102 | 103 | debug.namespace = namespace; 104 | debug.color = selectColor(namespace); 105 | debug.enabled = enabled(namespace); 106 | debug.destroy = destroy; 107 | debug.extend = extend; 108 | 109 | instances.push(debug); 110 | 111 | return debug; 112 | } 113 | 114 | function destroy(this: DebugInstance) { 115 | if (instances.includes(this)) { 116 | this.enabled = false; 117 | instances = instances.filter((instance) => instance !== this); 118 | return true; 119 | } 120 | return false; 121 | } 122 | 123 | /** 124 | * const server = debug('server'); 125 | * const serverHttp = server.extend('http') // server:http 126 | * const serverHttpReq = serverHttp.extend('req', '-') // server:http-req 127 | */ 128 | function extend( 129 | this: DebugInstance, 130 | subNamespace: string, 131 | delimiter: string = ":", 132 | ) { 133 | const newNamespace = `${this.namespace}${delimiter}${subNamespace}`; 134 | const newDebug = createDebug(newNamespace); 135 | // Pass down the custom logger 136 | newDebug.log = this.log; 137 | return newDebug; 138 | } 139 | 140 | function applyFormatters(this: DebugInstance, fmt: string, ...args: any[]) { 141 | let index = 0; 142 | const newFmt = fmt.replace(/%([a-zA-Z%])/g, (match, format) => { 143 | // If we encounter an escaped % then don't increase the array index 144 | if (match === "%%") { 145 | return match; 146 | } 147 | 148 | const formatter = formatters[format]; 149 | 150 | if (typeof formatter === "function") { 151 | const value = args[index]; 152 | // Remove the argument we used in the custom formatter 153 | args = [...args.slice(0, index), ...args.slice(index + 1)]; 154 | return formatter.call(this, value); 155 | } 156 | 157 | index++; 158 | return match; 159 | }); 160 | 161 | // Return the update fmt string and updated args 162 | return [newFmt, ...args]; 163 | } 164 | 165 | /** 166 | * Returns true if the given mode name is enabled, false otherwise. 167 | */ 168 | export function enabled(namespace: string): boolean { 169 | if (namespace[namespace.length - 1] === "*") { 170 | return true; 171 | } 172 | 173 | for (const skip of skips) { 174 | if (skip.test(namespace)) { 175 | return false; 176 | } 177 | } 178 | for (const name of names) { 179 | if (name.test(namespace)) { 180 | return true; 181 | } 182 | } 183 | 184 | return false; 185 | } 186 | 187 | /** 188 | * Enables a debug mode by namespaces. This can include modes 189 | * separated by a colon and wildcards. 190 | */ 191 | export function enable(namespaces: any) { 192 | updateNamespacesEnv(namespaces); 193 | 194 | // Resets enabled and disable namespaces 195 | names = []; 196 | skips = []; 197 | // Splits on comma 198 | // Loops through the passed namespaces 199 | // And groups them in enabled and disabled lists 200 | (typeof namespaces === "string" ? namespaces : "") 201 | .split(/[\s,]+/) 202 | // Ignore empty strings 203 | .filter(Boolean) 204 | .map((namespace) => namespace.replace(/\*/g, ".*?")) 205 | .forEach((ns) => { 206 | // If a namespace starts with `-`, we should disable that namespace 207 | if (ns[0] === "-") { 208 | skips.push(new RegExp("^" + ns.slice(1) + "$")); 209 | } else { 210 | names.push(new RegExp("^" + ns + "$")); 211 | } 212 | }); 213 | 214 | instances.forEach((instance) => { 215 | instance.enabled = enabled(instance.namespace); 216 | }); 217 | } 218 | 219 | interface FormatArgsOptions { 220 | namespace: string; 221 | color: number; 222 | diff: number; 223 | } 224 | 225 | function formatArgs( 226 | { namespace, color, diff }: FormatArgsOptions, 227 | args: any[], 228 | ): any[] { 229 | const colorCode = "\u001B[3" + (color < 8 ? color : "8;5;" + color); 230 | const prefix = noColor 231 | ? ` ${namespace} ` 232 | : ` ${colorCode};1m${namespace} \u001B[0m`; 233 | // Add a prefix on every line 234 | args[0] = args[0] 235 | .split("\n") 236 | .map((line: string) => `${prefix}${line}`) 237 | .join("\n"); 238 | 239 | const lastArg = noColor 240 | ? `+${ms(diff)}` 241 | : `${colorCode}m+${ms(diff)}${"\u001B[0m"}`; 242 | 243 | return [...args, lastArg]; 244 | } 245 | 246 | /** 247 | * Disable debug output. 248 | */ 249 | export function disable(): string { 250 | const namespaces = [ 251 | ...names.map(regexpToNamespace), 252 | ...skips.map(regexpToNamespace).map((namespace) => `-${namespace}`), 253 | ].join(","); 254 | enable(""); 255 | return namespaces; 256 | } 257 | 258 | /** 259 | * Save `namespaces` to env. 260 | */ 261 | function updateNamespacesEnv(namespaces: string): void { 262 | if (namespaces) { 263 | DEBUG.debug = namespaces; 264 | } else { 265 | DEBUG.debug = ""; 266 | } 267 | } 268 | 269 | // Default logger 270 | function log(...args: any[]): void { 271 | const result = format(...args); 272 | console.log(result); 273 | } 274 | 275 | // Exports 276 | 277 | const debugModule: DebugModule = Object.assign(createDebug, { 278 | enable, 279 | disable, 280 | enabled, 281 | names, 282 | skips, 283 | formatters, 284 | log, 285 | }); 286 | 287 | // Enable namespaces passed from env 288 | try { 289 | const setting = Deno.env.get("DEBUG"); 290 | if (setting) { 291 | DEBUG.debug = setting; 292 | } 293 | } catch (err) {} 294 | 295 | enable(DEBUG.debug); 296 | 297 | export default debugModule; 298 | -------------------------------------------------------------------------------- /debug_test.ts: -------------------------------------------------------------------------------- 1 | // Copied from https://github.com/defunctzombie/node-util/blob/master/test/node/format.js 2 | import { 3 | assert, 4 | assertEquals, 5 | assertStrictEquals, 6 | } from "https://deno.land/std/testing/asserts.ts"; 7 | import debug from "./debug.ts"; 8 | 9 | Deno.test("passes a basic sanity check", function () { 10 | const log = debug("test"); 11 | log.enabled = true; 12 | log.log = () => {}; 13 | 14 | log("hello world"); 15 | }); 16 | 17 | Deno.test("allows namespaces to be a non-string value", function () { 18 | const log = debug("test"); 19 | log.enabled = true; 20 | log.log = () => {}; 21 | 22 | debug.enable(true); 23 | }); 24 | 25 | Deno.test("logger should handle error as the first param", function () { 26 | const log = debug("test"); 27 | log.enabled = true; 28 | const messages: any[][] = []; 29 | log.log = (...args: any[]) => messages.push(args); 30 | 31 | log(new Error()); 32 | 33 | assertEquals(typeof messages[0][0], "string"); 34 | assertEquals(typeof messages[0][1], "string"); 35 | }); 36 | 37 | Deno.test("honors global debug namespace enable calls", function () { 38 | assertEquals(debug("test:12345").enabled, false); 39 | assertEquals(debug("test:67890").enabled, false); 40 | 41 | debug.enable("test:12345"); 42 | assertEquals(debug("test:12345").enabled, true); 43 | assertEquals(debug("test:67890").enabled, false); 44 | }); 45 | 46 | Deno.test("uses custom log function", function () { 47 | const log = debug("test"); 48 | log.enabled = true; 49 | 50 | const messages = []; 51 | log.log = (...args: any[]) => messages.push(args); 52 | 53 | log("using custom log function"); 54 | log("using custom log function again"); 55 | log("%O", 12345); 56 | 57 | assertEquals(messages.length, 3); 58 | }); 59 | 60 | // Extending 61 | 62 | Deno.test("extend should extend namespace", function () { 63 | const log = debug("foo"); 64 | log.enabled = true; 65 | log.log = () => {}; 66 | 67 | const logBar = log.extend("bar"); 68 | assertEquals(logBar.namespace, "foo:bar"); 69 | }); 70 | 71 | Deno.test("extend should extend namespace with custom delimiter", function () { 72 | const log = debug("foo"); 73 | log.enabled = true; 74 | log.log = () => {}; 75 | 76 | const logBar = log.extend("bar", "--"); 77 | assertEquals(logBar.namespace, "foo--bar"); 78 | }); 79 | 80 | Deno.test("extend should extend namespace with empty delimiter", function () { 81 | const log = debug("foo"); 82 | log.enabled = true; 83 | log.log = () => {}; 84 | 85 | const logBar = log.extend("bar", ""); 86 | assertStrictEquals(logBar.namespace, "foobar"); 87 | }); 88 | 89 | Deno.test( 90 | "extend should keep the log function between extensions", 91 | function () { 92 | const log = debug("foo"); 93 | log.log = () => {}; 94 | 95 | const logBar = log.extend("bar"); 96 | assertStrictEquals(log.log, logBar.log); 97 | }, 98 | ); 99 | 100 | // log.destroy() 101 | 102 | Deno.test("destroy works", function () { 103 | const log = debug("test"); 104 | log.enabled = true; 105 | 106 | const messages = []; 107 | log.log = (...args: any[]) => messages.push(args); 108 | 109 | log("using custom log function"); 110 | log("using custom log function again"); 111 | 112 | log.destroy(); 113 | 114 | log("using custom log function"); 115 | 116 | assertEquals(messages.length, 2); 117 | }); 118 | 119 | // debug.enable 120 | 121 | Deno.test("enable handles empty", function () { 122 | debug.enable(""); 123 | assertEquals(debug.names, []); 124 | assertEquals(debug.skips, []); 125 | }); 126 | 127 | Deno.test("enable works", function () { 128 | assertEquals(debug.enabled("test"), false); 129 | 130 | debug.enable("test"); 131 | assertEquals(debug.enabled("test"), true); 132 | 133 | debug.disable(); 134 | assertEquals(debug.enabled("test"), false); 135 | }); 136 | 137 | // debug.disable 138 | 139 | Deno.test( 140 | "disable should keep the log function between extensions", 141 | function () { 142 | debug.enable("test,abc*,-abc"); 143 | const namespaces = debug.disable(); 144 | assertEquals(namespaces, "test,abc*,-abc"); 145 | }, 146 | ); 147 | 148 | Deno.test("disable handles empty", function () { 149 | debug.enable(""); 150 | const namespaces = debug.disable(); 151 | assertEquals(namespaces, ""); 152 | assertEquals(debug.names, []); 153 | assertEquals(debug.skips, []); 154 | }); 155 | 156 | Deno.test("disable handles all", function () { 157 | debug.enable("*"); 158 | const namespaces = debug.disable(); 159 | assertEquals(namespaces, "*"); 160 | }); 161 | 162 | Deno.test("disable handles skip all", function () { 163 | debug.enable("-*"); 164 | const namespaces = debug.disable(); 165 | assertEquals(namespaces, "-*"); 166 | }); 167 | 168 | Deno.test("properly skips logging if all is disabled", function () { 169 | debug.enable("-*"); 170 | const log = debug("test"); 171 | 172 | const messages = []; 173 | log.log = (...args: any[]) => messages.push(args); 174 | 175 | log("using custom log function"); 176 | log("using custom log function again"); 177 | log("%O", 12345); 178 | 179 | assertEquals(messages.length, 0); 180 | 181 | debug.enable("test"); 182 | debug.disable(); 183 | 184 | log("using custom log function"); 185 | log("using custom log function again"); 186 | log("%O", 12345); 187 | 188 | assertEquals(messages.length, 0); 189 | }); 190 | 191 | Deno.test("names+skips same with new string", function () { 192 | debug.enable("test,abc*,-abc"); 193 | const oldNames = [...debug.names]; 194 | const oldSkips = [...debug.skips]; 195 | const namespaces = debug.disable(); 196 | assertEquals(namespaces, "test,abc*,-abc"); 197 | debug.enable(namespaces); 198 | assertEquals(oldNames.map(String), debug.names.map(String)); 199 | assertEquals(oldSkips.map(String), debug.skips.map(String)); 200 | }); 201 | 202 | // custom formatters 203 | 204 | Deno.test("adds a custom formatter", function () { 205 | const log = debug("test"); 206 | log.enabled = true; 207 | const messages: any[][] = []; 208 | log.log = (...args: any[]) => messages.push(args); 209 | 210 | debug.formatters.t = function (v: any) { 211 | return `test`; 212 | }; 213 | debug.formatters.w = (v) => { 214 | return v + 5; 215 | }; 216 | log("this is: %t", "this will be ignored"); 217 | log("this is: %w", 5); 218 | 219 | assert(messages[0][0].includes("this is: test")); 220 | assert(messages[1][0].includes("this is: 10")); 221 | }); 222 | 223 | Deno.test("formatters can access logger on this", function () { 224 | const log = debug("test"); 225 | log.enabled = true; 226 | log.log = () => {}; 227 | 228 | debug.formatters.t = function (v: any) { 229 | assertStrictEquals(this as any, log as any); 230 | return `test`; 231 | }; 232 | log("this is: %t", "this will be ignored"); 233 | }); 234 | 235 | // Custom global logger 236 | 237 | Deno.test("overrides all per-namespace log settings", function () { 238 | const loger1 = debug("test"); 239 | loger1.enabled = true; 240 | const loger2 = debug("test2"); 241 | loger2.enabled = true; 242 | 243 | const messages = []; 244 | 245 | debug.log = (...args: any[]) => messages.push(args); 246 | 247 | loger1("using custom log function"); 248 | loger2("using custom log function again"); 249 | loger1("%O", 12345); 250 | 251 | assertEquals(messages.length, 3); 252 | }); 253 | -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rista404/deno-debug/2feca67fe85af6b7c6a2473cd8ad94061daf7382/demo.png -------------------------------------------------------------------------------- /demo.ts: -------------------------------------------------------------------------------- 1 | import debug from "./debug.ts"; 2 | 3 | function sleep(ms: number) { 4 | return new Promise((r) => { 5 | setTimeout(() => r(), ms); 6 | }); 7 | } 8 | 9 | // Not actually a test, more of a demo 10 | // Should improve it 11 | 12 | const msg1 = "Doing lots of work"; 13 | const msg2 = "Doing lots of other work"; 14 | 15 | async function demo() { 16 | const http = debug("http"); 17 | const dotenv = debug("dotenv"); 18 | const worker = debug("worker"); 19 | const workerApi = worker.extend("api"); 20 | const workerGen = worker.extend("gen"); 21 | 22 | dotenv("env variables loaded"); 23 | http("booting %s", `'My App'`); 24 | 25 | await sleep(200); 26 | workerApi(msg1); 27 | 28 | await sleep(44); 29 | workerGen(msg1); 30 | 31 | await sleep(99); 32 | 33 | workerApi(msg1); 34 | await sleep(1200); 35 | workerApi(msg1); 36 | await sleep(55); 37 | workerApi(msg1); 38 | await sleep(12); 39 | workerApi(msg1); 40 | await sleep(92); 41 | workerApi(msg1); 42 | 43 | await sleep(58); 44 | 45 | workerGen(msg1); 46 | 47 | workerApi(msg1); 48 | 49 | await sleep(233); 50 | 51 | workerGen(msg2); 52 | 53 | const nestedObj: any = { a: { b: { c: { d: { e: { f: 42 } } } } } }; 54 | workerGen("event: %O", nestedObj); 55 | http("received event: %O", { 56 | bar: true, 57 | justify: "column", 58 | align: { center: true }, 59 | }); 60 | await sleep(100); 61 | http("received event: %O", { 62 | bar: true, 63 | justify: "column", 64 | align: { center: true }, 65 | }); 66 | 67 | // TODO 68 | // const obj = { a: 1 }; 69 | // const obj2 = { b: 2 }; 70 | // const weakSet = new WeakSet([obj, obj2]); 71 | // workerB("Object with hidden fields: %O", weakSet); 72 | 73 | workerApi(`${msg1} 74 | request: /multi-line-comment 75 | response: /working`); 76 | 77 | workerGen("wooooorking"); 78 | 79 | await sleep(500); 80 | 81 | await sleep(220); 82 | 83 | workerGen(msg2); 84 | 85 | await sleep(88); 86 | 87 | workerGen(msg2); 88 | workerApi(msg2); 89 | workerGen(msg2); 90 | workerGen(msg2); 91 | 92 | sleep(55555); 93 | return; 94 | } 95 | 96 | demo(); 97 | -------------------------------------------------------------------------------- /format.ts: -------------------------------------------------------------------------------- 1 | // Copied from https://github.com/defunctzombie/node-util/blob/master/util.js 2 | // Modified to format %o and %O as deno objects 3 | const { inspect } = Deno; 4 | import { getInspectOpts } from "./utils.ts"; 5 | 6 | const inspectOpts = getInspectOpts(); 7 | const formatRegExp = /%[sdjoO%]/g; 8 | 9 | export default function format(...args: any[]) { 10 | if (typeof args[0] !== "string") { 11 | let objects = []; 12 | for (let i = 0; i < arguments.length; i++) { 13 | objects.push(inspect(arguments[i], inspectOpts)); 14 | } 15 | return objects.join(" "); 16 | } 17 | 18 | let i = 1; 19 | const f = args[0]; 20 | const len = args.length; 21 | let str = String(f).replace(formatRegExp, function (x) { 22 | if (x === "%%") return "%"; 23 | if (i >= len) return x; 24 | switch (x) { 25 | case "%s": 26 | return String(args[i++]); 27 | case "%d": 28 | return String(Number(args[i++])); 29 | case "%j": 30 | try { 31 | return JSON.stringify(args[i++]); 32 | } catch (_) { 33 | return "[Circular]"; 34 | } 35 | case "%o": 36 | case "%O": 37 | return inspect(args[i++], inspectOpts); 38 | default: 39 | return x; 40 | } 41 | }); 42 | for (let x = args[i]; i < len; x = args[++i]) { 43 | if (x == null || typeof x !== "object") { 44 | str += " " + x; 45 | } else { 46 | str += " " + inspect(x); 47 | } 48 | } 49 | return str; 50 | } 51 | -------------------------------------------------------------------------------- /format_test.ts: -------------------------------------------------------------------------------- 1 | // Copied from https://github.com/defunctzombie/node-util/blob/master/test/node/format.js 2 | import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; 3 | import format from "./format.ts"; 4 | 5 | Deno.test("testFormat", function () { 6 | assertEquals(format(), ""); 7 | assertEquals(format(""), ""); 8 | assertEquals(format([]), "[]"); 9 | assertEquals(format({}), "{}"); 10 | assertEquals(format(null), "null"); 11 | assertEquals(format(true), "true"); 12 | assertEquals(format(false), "false"); 13 | assertEquals(format("test"), "test"); 14 | 15 | // CHECKME this is for console.log() compatibility - but is it *right*? 16 | assertEquals(format("foo", "bar", "baz"), "foo bar baz"); 17 | 18 | assertEquals(format("%d", 42.0), "42"); 19 | assertEquals(format("%d", 42), "42"); 20 | assertEquals(format("%s", 42), "42"); 21 | assertEquals(format("%j", 42), "42"); 22 | 23 | assertEquals(format("%d", "42.0"), "42"); 24 | assertEquals(format("%d", "42"), "42"); 25 | assertEquals(format("%s", "42"), "42"); 26 | assertEquals(format("%j", "42"), '"42"'); 27 | 28 | assertEquals(format("%%s%s", "foo"), "%sfoo"); 29 | 30 | assertEquals(format("%s"), "%s"); 31 | assertEquals(format("%s", undefined), "undefined"); 32 | assertEquals(format("%s", "foo"), "foo"); 33 | assertEquals(format("%s:%s"), "%s:%s"); 34 | assertEquals(format("%s:%s", undefined), "undefined:%s"); 35 | assertEquals(format("%s:%s", "foo"), "foo:%s"); 36 | assertEquals(format("%s:%s", "foo", "bar"), "foo:bar"); 37 | assertEquals(format("%s:%s", "foo", "bar", "baz"), "foo:bar baz"); 38 | assertEquals(format("%%%s%%", "hi"), "%hi%"); 39 | assertEquals(format("%%%s%%%%", "hi"), "%hi%%"); 40 | 41 | (function () { 42 | let o: { o?: any } = {}; 43 | o.o = o; 44 | assertEquals(format("%j", o), "[Circular]"); 45 | })(); 46 | 47 | // Ignore erros until we see should util package be ported 48 | // or a replacement found 49 | 50 | // Errors 51 | // assertEquals(format(new Error('foo')), 'Error: foo'); 52 | // class CustomError extends Error { 53 | // constructor(msg) { 54 | // super(msg); 55 | // Object.defineProperty(this, 'message', { value: msg, enumerable: false }); 56 | // Object.defineProperty(this, 'name', { value: 'CustomError', enumerable: false }); 57 | // } 58 | // } 59 | // assertEquals(format(new CustomError('bar')), 'CustomError: bar'); 60 | }); 61 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | import "./debug_test.ts"; 2 | import "./format_test.ts"; 3 | -------------------------------------------------------------------------------- /utils.ts: -------------------------------------------------------------------------------- 1 | const { env } = Deno; 2 | import { camelCase } from "https://raw.githubusercontent.com/denolib/camelcase/master/mod.ts"; 3 | 4 | // We assume the terminal supports colors 5 | export const colors = [ 6 | 20, 7 | 21, 8 | 26, 9 | 27, 10 | 32, 11 | 33, 12 | 38, 13 | 39, 14 | 40, 15 | 41, 16 | 42, 17 | 43, 18 | 44, 19 | 45, 20 | 56, 21 | 57, 22 | 62, 23 | 63, 24 | 68, 25 | 69, 26 | 74, 27 | 75, 28 | 76, 29 | 77, 30 | 78, 31 | 79, 32 | 80, 33 | 81, 34 | 92, 35 | 93, 36 | 98, 37 | 99, 38 | 112, 39 | 113, 40 | 128, 41 | 129, 42 | 134, 43 | 135, 44 | 148, 45 | 149, 46 | 160, 47 | 161, 48 | 162, 49 | 163, 50 | 164, 51 | 165, 52 | 166, 53 | 167, 54 | 168, 55 | 169, 56 | 170, 57 | 171, 58 | 172, 59 | 173, 60 | 178, 61 | 179, 62 | 184, 63 | 185, 64 | 196, 65 | 197, 66 | 198, 67 | 199, 68 | 200, 69 | 201, 70 | 202, 71 | 203, 72 | 204, 73 | 205, 74 | 206, 75 | 207, 76 | 208, 77 | 209, 78 | 214, 79 | 215, 80 | 220, 81 | 221, 82 | ]; 83 | 84 | /** 85 | * Selects a color for a debug namespace 86 | */ 87 | export function selectColor(namespace: string): number { 88 | let hash = 0; 89 | 90 | for (let i = 0; i < namespace.length; i++) { 91 | hash = (hash << 5) - hash + namespace.charCodeAt(i); 92 | hash |= 0; // Convert to 32bit integer 93 | } 94 | 95 | return colors[Math.abs(hash) % colors.length]; 96 | } 97 | 98 | export function getInspectOpts(): Deno.InspectOptions { 99 | const currentEnv = env.toObject(); 100 | const inspectOpts: Deno.InspectOptions = Object.keys(currentEnv) 101 | .filter((key) => /^debug_/i.test(key)) 102 | .reduce((obj: { [key: string]: number | boolean | null }, key) => { 103 | const prop = camelCase(key.slice(6)); 104 | 105 | let envVar: string = currentEnv[key]; 106 | let val: boolean | number | null; 107 | if (/^(yes|on|true|enabled)$/i.test(envVar)) { 108 | val = true; 109 | } else if (/^(no|off|false|disabled)$/i.test(envVar)) { 110 | val = false; 111 | } else if (envVar === "null") { 112 | val = null; 113 | } else { 114 | val = Number(envVar); 115 | } 116 | 117 | obj[prop] = val; 118 | return obj; 119 | }, {}); 120 | return inspectOpts; 121 | } 122 | 123 | /** 124 | * Coerce `val`. 125 | */ 126 | export function coerce(val: any): any { 127 | if (val instanceof Error) { 128 | return val.stack || val.message; 129 | } 130 | return val; 131 | } 132 | 133 | /** 134 | * Convert regexp to namespace 135 | */ 136 | export function regexpToNamespace(regexp: RegExp): string { 137 | return regexp 138 | .toString() 139 | .substring(2, regexp.toString().length - 2) 140 | .replace(/\.\*\?$/, "*"); 141 | } 142 | --------------------------------------------------------------------------------