mrkeyoor.com_
Thu 06 Aug 15:38 UTC
npmWeb Backendupdated 06 Aug 2026

base

base is a small base class for Node applications. You extend it, and you get a namespaced key/value store with dot-path access (set, get, has, del, union, clear), events on every store operation, a define method for non-enumerable properties, an is method that stamps type flags such as isCollection onto the instance, and a use method that runs plugin functions against the instance and records them so the same plugin cannot register twice. It was the shared foundation of the Toolkit suite (assemble, verb, generate, update, templates) around 2015 to 2018. Version 3.0.0 is an ES class that extends cache-base and is the last release, published in March 2018. The important thing to understand about this package is the gap between its download count and its actual use: nearly all of those installs are version 0.11.2 from 2017 arriving as a transitive dependency of old build tooling, not people choosing it.

Verdict

Do not install this. It has not shipped since 2018, its README documents an API the current version removed, and the download count that makes it look popular is a 2017 version being dragged along by old build tooling. If you found it in your lockfile, upgrade whatever depends on micromatch 3; if you were about to add it, a plain class plus dot-prop covers it.

API stability3/5Stable in the sense that nothing has changed since March 2018 and nothing will. Not stable in the sense that matters: the 0.x to 3.0 rewrite removed the static extend method and moved stored values onto the cache namespace, and the published README was never updated, so following the documentation on the package page produces code that does not work
Docs2/5The README is generated by verb, is thorough for its era, and includes an API section with examples, but it documents the previous major. Base.extend does not exist in 3.0.0 and the usage examples assert values appear on the instance root when they appear on app.cache. The linked documentation site at base.github.io returns 404, and the support channel is a Gitter badge
Maintenance1/5Last publish 2018-03-30, last repository push 2022-10-28, 10 open issues out of 13 open issues and PRs, and the GitHub org has been renamed at least once (node-base to base-repos), which is why the repository link in older references redirects. Not archived and not marked deprecated on npm, which is the worst of both worlds: nothing warns you at install time
Ecosystem2/5The Toolkit suite it anchored (assemble, verb, generate, update, templates) and the baseplugin keyword packages around it are all on the same timeline and equally quiet. The 10M weekly downloads are real installs but they are version 0.11.2 riding along inside snapdragon, so they measure how long old build tooling survives rather than any community around this package

Use it if

  • You maintain something already built on the Toolkit suite (assemble, verb, generate, update) and need to understand or patch the base class it inherits from
  • You are reading a stack trace that goes through base and want to know what the instance in front of you actually is: a cache-base subclass with a plugin registry bolted on
  • You want a reference implementation of the idempotent-plugin idea, where use('name', fn) checks a registry and silently skips a second registration, because that specific pattern is well done and worth borrowing
  • You are auditing a dependency tree and need to establish whether the base in your lockfile is the 2017 transitive copy or something a developer installed on purpose
Skip it if

Setup reality

npm install base still works and installs cleanly: 39 packages, about 1.2 MB on disk, and npm audit currently reports zero vulnerabilities on a fresh tree, so this is a stale package rather than a dangerous one. It is CommonJS only, so require('base') from a .mjs file needs a default import and there is no named export. The engines field says Node 6 and up, which tells you when it was last considered. There are no type definitions on npm or in DefinitelyTyped, so a TypeScript project gets an implicit-any import. The behavior you have to internalize before writing a line: the default cache namespace in 3.0.0 is 'cache', so everything set through app.set lives at app.cache and never on the instance root, which contradicts both examples at the top of the README. Base.namespace('other') gives you a constructor that stores under a different property name. If instead you are here because base showed up in your lockfile, you did not install it: run npm ls base to find the parent, and it will almost always be snapdragon under micromatch 3 or braces 2. The fix is upgrading whatever pulls in micromatch 3, since micromatch 4 dropped snapdragon entirely and takes the whole subtree with it.

Patterns

Work out why base is installed at allfind-it-in-your-tree

npm ls base
# my-app@1.0.0
# └─┬ some-old-tool@2.3.0
#   └─┬ micromatch@3.1.10
#     └─┬ braces@2.3.2
#       └─┬ snapdragon@0.8.2
#         └── base@0.11.2

# who pulls the whole legacy chain
npm ls micromatch snapdragon

# yarn / pnpm equivalents
yarn why base
pnpm why base

If the tree shows 0.11.2 you did not choose this package and nothing you write against the 3.0.0 API applies. snapdragon 0.8.x is the usual parent, reached through micromatch 3, braces 2, nanomatch, or extglob. micromatch 4 replaced that entire parser with picomatch and has no snapdragon dependency, so upgrading the top-level tool that pins micromatch 3 removes base, snapdragon, cache-base, define-property, and a dozen more in one step.

Basic usage, and where the values actually gohello-world

const Base = require('base');

const app = new Base({ foo: 'bar' }, { verbose: true });

app.get('foo');      //=> 'bar'
app.foo;             //=> undefined   (NOT on the instance)
app.cache;           //=> { foo: 'bar' }
app.options;         //=> { verbose: true }

app.set('a.b.c', 1);
app.get('a.b.c');    //=> 1
app.cache;           //=> { foo: 'bar', a: { b: { c: 1 } } }

The README's first example claims app.set('foo', 'bar') makes app.foo return 'bar'. In 3.0.0 it does not: the default cache-base namespace is 'cache', so everything lands on app.cache. This is the single biggest discrepancy between the published documentation and the published code, and it is why most snippets you find for this package fail immediately.

Store values under a different propertycustom-namespace

const Base = require('base');

const Store = Base.namespace('data');
const app = new Store();

app.set('user.name', 'Ada');
app.data;            //=> { user: { name: 'Ada' } }
app.cache;           //=> undefined

// namespace() returns a fresh constructor each call,
// with its own list of global plugins
const A = Base.namespace('a');
const B = Base.namespace('b');

Base.namespace(name) builds and returns a new class rather than configuring the existing one, so two calls give you two unrelated constructors and an instanceof check against one fails for the other. The default export is namespace() called with no argument, which cache-base resolves to 'cache'. Nothing stops you naming the namespace something that collides with a method, and nothing warns you when it does.

Plugins are functions, and use() deduplicates by nameplugins

const Base = require('base');
const app = new Base();

function logger(app) {
  app.define('log', msg => console.log(`[${app.type}] ${msg}`));
}

// named registration: the second call is a no-op
app.use('logger', logger);
app.use('logger', app => { app.define('log', () => 'overwritten'); });

app.log('hi');       //=> [app] hi
app.registered;      //=> { logger: true }

// arrays and extra arguments both work
app.use([pluginA, pluginB]);
app.use(pluginC, { depth: 2 });   // options reach the plugin as arg 2

Only the string-named form deduplicates. app.use(logger) without a name runs every single time, which is the mistake that produces duplicate event listeners in long-lived processes. The plugin is called with the instance as both `this` and the first argument. The registry lives on app.registered, defined as non-enumerable, so it will not show up in JSON.stringify or Object.keys.

The guard plugin authors are supposed to writeis-registered

function myPlugin(app) {
  if (app.isRegistered('myPlugin')) return;
  // safe to mutate: this only runs once per instance
  app.define('render', str => str.toUpperCase());
}

const app = new Base();
app.on('plugin', name => console.log('registered', name));
app.use(myPlugin);
app.use(myPlugin);   // guard short-circuits the second call

isRegistered(name) has a side effect: unless you pass false as the second argument it records the name and emits a 'plugin' event, so calling it twice returns false then true. That is intentional and is what makes the one-line guard work, but it means isRegistered is not a pure query and using it to inspect an instance changes it. Pass false explicitly when you only want to look.

Subclassing in 3.0.0 (Base.extend is gone)inheritance

const Base = require('base');

class Collection extends Base {
  constructor(cache, options) {
    super(cache, options);
    this.is('collection');
    this.define('items', []);
  }

  add(item) {
    this.items.push(item);
    this.emit('add', item);
    return this;
  }
}

const c = new Collection();
c.type;            //=> 'collection'
c.isCollection;    //=> true
c.isApp;           //=> undefined  (is() clears it for any non-'app' type)

typeof Base.extend is undefined in 3.0.0. The README still shows Base.extend(MyApp) with a Base.call(this) constructor, which is the 0.x pattern built on class-utils, and copying it throws immediately. Use standard class extends and remember to call super before touching this. Note also that is() deletes isApp when you set any other type, so code branching on isApp in a subclass silently stops matching.

Attach methods without polluting serializationdefine-non-enumerable

const app = new Base();

app.define('render', (str, locals) => str.replace(/{{(\w+)}}/g, (_, k) => locals[k]));
app.define({ a: 1, b: 2 });         // object form visits each key

Object.keys(app);                    //=> [ 'cache', 'options' ]
JSON.stringify(app);                 //=> {"cache":{},"options":{}}
app.render('hi {{name}}', { name: 'Ada' });

define is Object.defineProperty with writable and configurable set, so it is for behavior and internal state that should not be serialized or iterated. Dot notation is explicitly not supported: define('a.b', 1) creates a property whose literal key is 'a.b'. Use set for data and define for methods, which is the convention every Toolkit plugin follows.

Every store operation emitsevents

const app = new Base();

app.on('set', (key, value) => console.log('set', key, value));
app.on('get', key => console.log('get', key));
app.on('del', key => console.log('del', key));

app.set('a', 1);   // set a 1
app.get('a');      // get a
app.del('a');      // del a

The emitter comes from cache-base, which uses @sellside/emitter rather than Node's EventEmitter, so there is no removeAllListeners, no setMaxListeners, and no warning when listeners accumulate. The 'get' event fires on every read, including reads from inside the library itself, so attaching a logger there is noisy and measurably slow. There is no way to unsubscribe a plugin's listeners other than tracking the functions yourself.

What you inherit from cache-basestore-methods

const app = new Base();

app.set({ x: 1, y: 2 });        // object form
app.union('tags', 'a');
app.union('tags', ['b', 'c']);  // => ['a','b','c'], deduplicated array append
app.default('retries', 3);      // only sets if unset
app.prime('list', []);          // ensure a value exists, return it
app.visit('set', { p: 1, q: 2 });

app.has('x');      //=> true
app.hasOwn('x');   //=> true
app.keys;          //=> [ 'x', 'y', 'tags', 'retries', 'list', 'p', 'q' ]
app.size;          //=> 7
app.del('x');
app.clear();       //=> cache is now {}

None of these are documented in the base README because they belong to cache-base, so the base docs undersell what the class does. keys and size are getters, not methods, so app.size() throws. union always produces an array and deduplicates, which is convenient until you actually wanted duplicates. clear empties the whole namespace with no event per key.

The base getter walks up to the first ancestorparent-chain

const first = new Base();
first.set('config.env', 'prod');

const second = new Base();
second.parent = first;

const third = new Base();
third.parent = second;

third.base.get('config.env');   //=> 'prod'
third.base === first;           //=> true

base is a getter that recurses through parent until it finds an instance with none, and it defaults to the instance itself when parent is unset. Nothing sets parent for you: you assign it by hand, and nothing checks for cycles, so a.parent = b with b.parent = a recurses until the stack overflows. It is also a plain writable property, so any plugin can reassign it.

Static use() applies to every future instanceglobal-plugins

const Base = require('base');

Base.use(app => {
  app.define('createdAt', Date.now());
});

new Base().createdAt;   //=> 1754...

// scoped to one constructor instead of the shared default export
const App = Base.namespace('data');
App.use(app => app.define('scoped', true));
new App().scoped;       //=> true
new Base().scoped;      //=> undefined

Static use on the default export mutates a module-level array, and require caches the module, so a plugin registered by any package in the process affects every consumer's instances for the rest of its life. Instances already created are unaffected, only new ones. If you are writing a library, register on your own Base.namespace(...) constructor rather than on the shared default.

What the pieces look like without basereplace-it

// dot-path store
const { getProperty, setProperty, hasProperty } = require('dot-prop');
const cache = {};
setProperty(cache, 'a.b.c', 1);
getProperty(cache, 'a.b.c');       //=> 1

// events
const { EventEmitter } = require('node:events');
class App extends EventEmitter {}

// non-enumerable method
Object.defineProperty(app, 'render', { value: fn, configurable: true });

// idempotent plugins, the one idea worth keeping
class App2 extends EventEmitter {
  #plugins = new Set();
  use(name, fn) {
    if (this.#plugins.has(name)) return this;
    this.#plugins.add(name);
    fn(this);
    return this;
  }
}

This is the honest replacement for a package that has not shipped in eight years: about fifteen lines of standard JavaScript, no transitive tree, and types that your editor understands. dot-prop is ESM only from version 7 onward, so pin 6.x if you are still on CommonJS. The named-plugin dedupe is the one genuinely nice idea in base and it is five lines with a Set.

Alternatives

PackageRegistryPick it when
dot-propnpmThe only thing you wanted was get, set, has, and delete against dot-separated paths on a plain object, with no class to extend
cache-basenpmYou genuinely want the namespaced, event-emitting store, since that is the class base extends and it is one layer less indirection
tapablenpmWhat you actually need is a plugin system, with named hooks, sync and async variants, and control over how results combine, rather than functions that mutate an instance