@gebruederheitz/debuggable
v3.0.4
Published
Flexible debug output for browser applications.
Downloads
468
Readme
Debuggable
Flexible debug output control for browser oder NodeJS applications.
Installation
> npm i @gebruederheitz/debuggable
Usage
The library exports a singleton object, which can be used directly, or to spawn
further child debuggers. While it's enabled (~~which is the default state~~) it will
simply proxy its arguments through to console
. Currently implemented are the
methods log()
, warn()
and error()
.
import { debug } from '@gebruederheitz/debuggable';
const object = 'world';
debug.log('Hello %s.', object); // Will print "Hello world." to the browser console.
debug.disable(); // You could alternatively use `debug.toggle(false);`
debug.log('Goodbye!') // Will now not output anything.
Spawing children and using namespaces
Namespaces make it easier to identify where a particular log message came from. They also allow filtering the console for messages from a particular component quite simply.
const childLogger = debug.spawn('childID', 'ChildNamespace');
childLogger.log('happy');
/*
-->> [ChildNamespace] happy
*/
Child debuggers are disabled and enabled with their parent, while maintaining their internal state. This sounds more complicated than it is:
const child = debug.spawn('child');
child.log('something') // logs something
child.disable();
child.log('something') // doesn't log anything, because "locally disabled"
debug.disable();
child.enable();
child.log('something') // doesn't log anything, because globally disabled
debug.enable();
child.log('something') // logs something
Instances can recursively spawn children of their own:
const child = debug.spawn('child', 'Child');
const grandChild = child.spawn('grandchild', 'Grandchild');
const greatGrandChild = grandChild.spawn('greatgrandchild', 'Great-Grandchild');
greatGrandChild.log('Hi Grandpa!');
/*
-->> [Child] [Grandchild] [Great-Grandchild] Hi Grandpa!
*/
grancChild.disable();
greatGrandChild.log('**crickets**'); // no output, because disabled state is inherited
Here's some example usage with ES classes, each using their own child instance:
import { debug } from '@gebruederheitz/debuggable';
class MyModule {
constructor() {
this.debug = debug.spawn('MyModule');
}
someMethod(arg) {
this.debug.log('User has called someMethod with', arg);
}
quiet() {
this.debug.disable();
}
}
class OtherModule {
constructor() {
this.debug = debug.spawn('OtherModule');
}
otherMethod() {
this.debug.log('otherMethod has been called!');
}
}
const x = new MyModule();
const y = new OtherModule();
x.someMethod('someArg');
y.otherMethod();
x.quiet();
x.someMethod('otherArg'); // no output
y.otherMethod();
debug.disable();
x.someMethod(42); // no output
y.otherMethod(); // no output
Resulting output:
>> [MyModule] User has called someMethod with someArg
>> [OtherModule] otherMethod has been called!
>> [OtherModule] otherMethod has been called!
IDs, Namespaces & Tagging
interface DebugHelper {
spawn(
id?: string, // A unique identifier. Pass null to have
// a UUIDv4 generated for you.
namespace?: string, // The prefix for this instance, which will
// be prepended [in square brackets] along
// the prefixes of all ancestors.
startsEnabled?: boolean, // Default true, pass false to initially
// silence this instance.
...tags: string[] // A list of tags to be associated with this
// instance. Can be used with configure()
// as shown below.
): DebugHelper;
// ...
}
Another way to add tags is using the dedicated, chainable method:
const child = debug.spawn('some-id').addTags('firstTag', 'secondTag');
Runtime Configuration
You can toggle instances by their ID or assigned tags using the configure()
function on the global instance.
debug.configure({
tag: false, // Will be disabled
othertag: true, // Will be enabled
childId: true,
});
Typescript Class Decorator
If you're using Typescript, you can use the decorator to automagically inject a child instance as a class property. It will automatically inherit from the debug instance on the parent class if it exists.
import { debug, DecoratedWithDebug } from '@gebruederheitz/debuggable';
// A little hack to communicate the additional property to Typescript
export interface MyDecoratedClass extends DecoratedWithDebug {}
@debug.decorate('id', 'tag', 'more-tags', '...')
class MyDecoratedClass {
squawk() {
this.debug.log('Quack!');
}
}
new MyDecoratedClass().squawk();
// Automatic inheritance
@debug.decorate('child')
class MyChildClass extends MyDecoratedClass {}
const child = new MyChildClass();
child.squawk();
// -->> [id] [child] Quack!
MyDecoratedClass.prototype.debug.disable();
child.squawk();
// > silence
Event Interface
The event interface is an instance of mitt that is exposed as a public property on the global instance.
import type { Events } from 'gebruederheitz/debuggable';
import type { Emitter } from 'mitt';
import { debug } from 'gebruederheitz/debuggable';
const eventInterface: Emitter<Events> = debug.events;
// Listen to events
eventInterface.on('register', ({instance, parent}: Events['register']) => {
console.log('New instance has been spawned.', {
newInstance: instance,
parentInstance: parent,
});
});
// Trigger events manually
eventInterface.emit('message', {
type: 'log',
instance: someDebugHelperInstance,
message: ['Hello'],
});
eventInterface.emit('toggle_some-id', { enabled: false });
On-Page Console (browser only)
This feature is particularly useful for debugging web applications on mobile
devices, particularly ones you might not have direct access to – i.e. whenever
using remote or USB debugging is impractical or impossible. Somewhere on the
page running your scripts you'll insert an empty div
element with the ID
attribute debug-visualize
, and enable "visualisation" on the debug object.
Now the debugger will replicate any debug output into this "fake console" element, allowing you to read your app's debug messages without access to the browser console. Obviously this is not suitable for production use.
<div id="debug-visualize"></div>
debug.toggleVisualization(true);
// Use toggleVisualization(false) to turn it back off again
debug.spawn('Test').log('Hello!');
<div id="debug-visualize" style="...">
<div class="console" style="...">
<code class="debug-visualize__entry debug-visualize__entry--log" style="...">
[Test] Hello!
</code>
</div>
</div>
Helpers & Utilities
timeout()
I sometimes use this to simulate any asynchronous action, like a network request
that might take a while. It simply wraps the native setTimeout()
in a Promise
and returns that, so it's a convenient one-liner in an async flow:
async function getDataFromApi() {
// @TODO: write actual API request once backend team is ready
await debug.timeout(1200) // Wait for 1.2 seconds to simulate a request
return {
success: true,
data: {
items: [],
},
};
}
devnull()
Slightly silly, but I found it useful during development to just shut the linters up (and especially TS) while I'm working with method stubs. Maybe there's more or even better uses for this:
class MyClass {
methodStub(knownArg) {
// It simply returns the args passed to it. Nothing else.
this.debug.devnull(knownArg);
}
}
Development
Dependencies
- nodeJS LTS (18.x)
- nice to have:
- GNU make or drop-in alternative
- NVM or asdf
Quickstart
You can use the watch task:
$> nvm use
$> npm run watch
# or
$> make
# or, more explicitly
$> make dev
After making your changes, run
$> npm run build
# or
$> make build
to create the ES-module build at dist/index.mjs
and make
certain everything runs smoothly. You should also run make test
at least once
to avoid simple linting issues and run the test suite.
When you're finished, you can use make release
on the main branch to publish
your changes.