Skip to content

[RFC]: Bringing the debug Dependency In-House #11081

@therealharshit

Description

@therealharshit

Bringing the debug Dependency In-House

Replacing the external debug npm package (v2.6.9) with stdlib-native equivalents to eliminate a supply-chain risk and align all production code with stdlib's quality standards (docs, tests, examples, benchmarks, backward compatibility).

A key improvement: the in-house replacement will leverage @stdlib/string/format for string interpolation, providing significantly richer formatting capabilities than the external debug package's limited %s/%d placeholders

Background

What debug v2.6.9 Does

A tiny JavaScript debugging utility modelled after Node.js core's debugging technique. Works in Node.js and web browsers.
More here: https://github.com/debug-js/debug

Its sole dependency

  • ms v2.0.0: Converts milliseconds to/from human-readable strings (e.g., 1500"1.5s"). Used only for the +1.5s diff suffix in log output.

How stdlib uses it

  • 680+ files across: @stdlib/random/streams/*, @stdlib/plot/*, @stdlib/streams/*, @stdlib/math/base/special/*, @stdlib/fs/*, @stdlib/repl/*, @stdlib/_tools/*, and more.
  • Only the factory function is used: var logger = require('debug'); var debug = logger('namespace'); then debug('message: %s.', val);.
  • No advanced API usage: none of the 680+ files call enable(), disable(), enabled(), or register custom formatters.

Proposed Changes

The plan creates 2 new packages mirroring stdlib's decomposable architecture, with each package being independently consumable. The implementation leverages the existing @stdlib/string/format package for string interpolation for richer debug messages — e.g., debug( 'Value: %.6f, Index: %03d', 3.14, 7 )'Value: 3.140000, Index: 007'


Component 1: @stdlib/time/ms

Replaces the ms npm dependency (transitive via debug).

A utility to convert between millisecond values and human-readable time strings.

Scope: Only the ms → string direction is needed by the debug logger (e.g., 1500"1.5s"). Optionally support string → ms for completeness.

@stdlib/time/ms/
├── lib/
│   ├── index.js          # re-export main
│   └── main.js           # ms(val) → string or number
├── test/
│   └── test.js
├── benchmark/
│   └── benchmark.js
├── docs/
│   ├── repl.txt
│   └── types/
│       ├── index.d.ts
│       └── test.ts
├── examples/
│   └── index.js
├── README.md
└── package.json

API:

var ms = require( '@stdlib/time/ms' );

ms( 1500 );     // => '1.5s'
ms( 60000 );    // => '1m'
ms( 3600000 );  // => '1h'
ms( 86400000 ); // => '1d'
ms( 500 );      // => '500ms'

Component 2: @stdlib/console/debug

The core package — the direct replacement for require('debug').

A namespace-based diagnostic logging utility. This is the main factory function that all 680+ files will switch to.

@stdlib/console/debug/
├── lib/
│   ├── index.js          # re-export main
│   ├── main.js           # createDebug(namespace) factory (core logic)
│   ├── enable.js         # enable(namespaces) — parse glob patterns
│   ├── disable.js        # disable()
│   ├── enabled.js        # enabled(name) → boolean
│   ├── format_args.js    # environment-specific argument formatting
│   ├── colors.js         # ANSI color selection (Node.js)
│   └── defaults.js       # default options
├── test/
│   ├── test.js           # main export tests
│   ├── test.enable.js    # enable/disable/enabled tests
│   ├── test.format.js    # format string placeholder tests
│   └── test.colors.js    # color selection tests
├── benchmark/
│   ├── benchmark.js           # benchmark: logger creation
│   └── benchmark.disabled.js  # benchmark: no-op when disabled (perf)
├── docs/
│   ├── repl.txt
│   └── types/
│       ├── index.d.ts
│       └── test.ts
├── examples/
│   └── index.js
├── README.md
└── package.json

API (drop-in compatible with current usage):

var logger = require( '@stdlib/console/debug' );

// Create a namespaced logger:
var debug = logger( 'sparkline:unicode:main' );

// Log messages (no-op when disabled):
debug( 'Creating an instance with config: %s.', JSON.stringify( opts ) );
debug( 'Current value: %s.', this._yMin );


// Now with richer formatting via @stdlib/string/format:
debug( 'Computed value: %.6f at index %03d.', 3.14159, 7 );
debug( 'Hex: %x, Binary: %b, Octal: %o.', 255, 255, 255 );
debug( 'Padded: %10s | %-10s.', 'right', 'left' );

// Programmatic control (used internally, not by consumers):
logger.enable( 'sparkline:*' );
logger.disable();
logger.enabled( 'sparkline:unicode:main' ); // => true/false

Key implementation details:

  • Reads DEBUG env var (Node.js) or localStorage.debug (browser) on load
  • Supports glob patterns: * matches any characters, - prefix excludes
  • When disabled (the default), the function is a no-op with zero cost (early return)
  • Format placeholders: %s (string), %d (number), %o (inspect compact), %O (inspect multiline), %j (JSON), %% (escaped %)
  • ANSI colors in Node.js via deterministic namespace hashing
  • Appends +Xms diff timestamp between successive calls
  • Output target: stderr in Node.js, console.log in browsers
  • Internal dependencies only: @stdlib/string/format, @stdlib/time/ms

Component 3: Migration — Updating All Consumers

This is the mechanical bulk change, done after the new packages are created and verified.

[MODIFY] All 680+ files using require( 'debug' )

There are two primary usage patterns across the codebase:

  1. Pattern 1: Dedicated Wrapper Files (53 Packages) Files like @stdlib/random/streams/arcsine/lib/debug.js whose sole purpose is to initialize and export a namespaced logger.

  2. Pattern 2: Direct Imports (645 Files) Implementation files like @stdlib/fs/read-file-list/lib/async.js that import the utility directly alongside other modules.

In both patterns, only a single line changes:

-var logger = require( 'debug' );
+var logger = require( '@stdlib/console/debug' );

Migration strategy ((phased to account for both patterns)::

Phase Scope Files
Phase 1 @stdlib/random/streams/* ~200 files (all have identical debug.js pattern)
Phase 2 @stdlib/plot/* and @stdlib/streams/* ~150 files
Phase 3 @stdlib/math/base/special/* ~100 files
Phase 4 @stdlib/fs/*, @stdlib/repl/*, @stdlib/_tools/*, and remaining ~200+ files

Each phase follows the same process:

  1. Run find + sed to replace require( 'debug' )require( '@stdlib/console/debug' )
  2. Run existing tests for the affected module area
  3. Verify with DEBUG='*' node <example> that output matches current behavior

Uninstall the dependancy

After all phases are complete:

npm uninstall debug

Verification Plan

Automated Tests

1. Unit tests for @stdlib/time/ms

make TESTS_FILTER=".*/time/ms/.*" test

Tests should cover:

  • Millisecond → string conversion for all ranges (ms, seconds, minutes, hours, days)
  • Edge cases: 0, negative values, Infinity, NaN
  • Type validation: non-numeric input throws

2. Unit tests for @stdlib/console/debug

make TESTS_FILTER=".*/console/debug/.*" test

Tests should cover:

  • Factory returns a function
  • Logger is a no-op when DEBUG is unset
  • Logger outputs when DEBUG matches namespace
  • enable()/disable()/enabled() work correctly
  • Glob matching: *, -prefix exclusion, multiple comma-separated patterns
  • Format strings are processed via @stdlib/string/format (all specifiers: %s, %d, %f, %e, %x, %b, etc.)
  • Width/precision/flags are correctly passed through (e.g., %.2f, %05d, %-10s)
  • Color assignment is deterministic per namespace
  • Diff timestamp is appended

3. Regression tests for migrated modules

Run the full test suite for each phase:

# Phase 1:
make TESTS_FILTER=".*/random/streams/.*" test

# Phase 2:
make TESTS_FILTER=".*/plot/.*" test
make TESTS_FILTER=".*/streams/.*" test

# Phase 3:
make TESTS_FILTER=".*/math/base/special/.*" test

# Phase 4: Full suite
make test

Manual Verification

After migration, verify that debug output works end-to-end:

  1. Create a test script that uses @stdlib/random/streams/t (or any migrated module)

  2. Run without DEBUG:

    node test_script.js

    → Verify: no debug output appears (silent by default)

  3. Run with DEBUG:

    DEBUG='random:streams:*' node test_script.js

    → Verify: colored namespace-prefixed debug messages appear on stderr, with +Xms diff timestamps

  4. Run with exclusion:

    DEBUG='*,-random:*' node test_script.js

    → Verify: excluded namespaces produce no output

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions