npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

cerise

v0.1.2

Published

Intuitive and lightweight Dependency Injection library for Node.js

Downloads

11

Readme

Cerise

Intuitive Dependency Injection (DI) library for Node.js, written in JavaScript and weighing less than 10 KB. Ironically, Cerise does not depend on any package.

API documentation -- Examples -- FAQ

Installation

Install with npm or yarn

$ npm install cerise
$ yarn add cerise

Both CommonJS and ES modules builds are included; the latter will be automatically selected if your build system supports it.

Usage

Using Cerise is dead simple. There are two concepts you'll need to understand first: containers and factories.

createContainer

A container (also known as an injector) is a master object that knows how to create services, thanks to factories.

const { createContainer, constant, factory, service } = require('cerise');

// Create a container and immediately register a factory
// for the `name` service.
const container = createContainer({
  package_name: constant('cerise'),
});

// You can also register a service for an existing container.
container.register('package_name', constant('cerise'));

// You can retrieve services using either container as a
// function, or its `proxy` property.
assert('cerise' === container('package_name'));
assert('cerise' === container.proxy.package_name);

There are multiple ways to declare a service: using the constant, factory and service helpers.

constant

When using constant you cannot depend on an other service. You can register any value: a number, string, function, etc.

container.register('base_url', constant('https://npmjs.com'));
container.register('concat', constant((...args) => args.join('')));

assert('string' === typeof container('base_url'));
assert('function' === typeof container('concat'));

factory

If you need to depend on an other service, use a factory. factory takes a function that will be passed container.proxy (which can be destructured to access other services) and returns a service.

container.register(
  'package_url',
  factory(proxy => {
    return proxy.concat(proxy.base_url, '/', proxy.package_name);
  }),
);

// Using destructuring
container.register(
  'package_url',
  factory(({ concat, base_url: baseUrl, package_name: packageName }) => {
    return concat(baseUrl, '/', packageName);
  }),
);

// Alternatively, call the proxy as a function
container.register(
  'package_url',
  factory(inject => {
    return inject('concat')(inject('base_url'), '/', inject('package_name'));
  }),
);

assert('https://npmjs.com/cerise' === container('package_url'));

You'll notice that constant(x) is equivalent to factory(() => x): it's just sugar.

service

Lastly, service is passed a class and will return an instance on retrieval. Use it if you're more familiar with OOP.

class PackageUrl {
  constructor({ concat, base_url: baseUrl }) {
    this._concat = concat;
    this._baseUrl = baseUrl;
  }

  get(packageName) {
    return this._concat(this._baseUrl, '/', packageName);
  }
}

container.register('package_url', service(PackageUrl));

assert('https://npmjs.com/cerise' === container('package_url').get('cerise'));

Once again, it's just sugar: service(T) is equivalent to factory(proxy => new T(proxy)).

Scopes

Oftentimes you'll want to create a scope from a container. Scopes inherit their parent and their registered service, but can also have their own service. For instance, if you're using Express, you might want to have a master container to store your database connexion, and another container for request-specific data.

const express = require('express');
const { Database } = require('sqlite3');
const { createContainer, constant } = require('cerise');

const app = express();
const container = createContainer({
  db: constant(new Database(':memory:')),
});

// For each request, create a scope and fetch session data.
app.use((req, res, next) => {
  const id = req.get('x-session-id');
  const db = container('db');

  req.scope = container.scope();

  db.get('select * from sessions where id = ?', [id], (err, session) => {
    // Only alter the request scope, not the parent container
    req.scope.register('session', constant(session));
    next();
  });
});

// Session data is available on child scope.
app.get('/session', (req, res) => {
  res.json(req.scope('session'));
});

// Parent container services are also available on child scope.
app.get('/time', (req, res) => {
  const db = req.scope('db');

  db.get('select current_timestamp as time', (err, { time }) => {
    res.json({ time });
  });
});

Lifetimes

default lifetime

By default (except for constant) the factory will be called each time you wish to retrieve a value from a factory.

// Each resolution will result in a new Thing instance being created.
container.register('thing', service(class Thing {}));

const foo = container('thing');
const bar = container('thing');

assert(foo !== bar);

singletons

You may however wish to specify a lifetime to your factory in order to cache its result.

// Now the first instance will be cached and returned each time.
container.register('thing', service(class Thing {}).singleton());

const foo = container('thing');
const bar = container('thing');

assert(foo === bar);

scoped

Singletons only make sense on the root container; if you wish to cache a service for scopes you will want to use the scoped lifetime qualifier:

const winston = require('winston');

container.register(
  'logger',
  constant(
    winston.createLogger({
      transports: winston.transports.Console(),
    }),
  ),
);

// Create a scope on every request
app.use((req, res, next) => {
  req.scope = container.scope();
  next();
});

// Register a *scoped* logger (with request id metadata)
app.use((req, res, next) => {
  req.scope.register(
    'reqlog',
    factory(({ logger }) =>
      logger.child({ requestId: req.get('x-request-id') }),
    ).scoped(),
  );
  next();
});

// Logging middleware
app.use((req, res, next) => {
  const start = Date.now();
  const logger = req.scope('reqlog');

  req.on('finish', () => {
    const elapsed = Date.now() - start;

    logger.info('[%s] %s %s', elapsed, req.method, req.path);
  });

  next();
});

Saving and restoring state

Root containers' state can be saved and restored which can be useful for testing. For instance, in Mocha's beforeEach and afterEach hooks:

describe('My API', () => {
  beforeEach(() => container.save());
  afterEach(() => container.restore());

  it('...', () => {
    // package_url service will be 'nope' but only for this particular test
    container.register('package_url', constant('nope'));
  });
});

Utils

Middlewares: Cerise provides middlewares for Express and Koa.
See the API documentation.

Controllers: since calling req.scope gets old really fast, Cerise also provides a controller helper -- with an async error handler for convenience. Pass it a callback, and it will get called with req.scope.proxy, req, res and next.
See the API documentation.

FAQ

How to overwrite a parent service in a scope?

You can register a service with the same name:

const parent = createContainer();
parent.register('scopeName', constant('parent'));

const child = parent.scope();
child.register('scopeName', constant('child'));

assert('parent' === parent('scopeName'));
assert('child' === child('scopeName'));

Can a child scope service depend on a parent scope service of the same name?

Yes, but you cannot depend directly on the parent service.

parent.register('breadcrumb', constant('/'));

// This will break: `breadcrumb` on the child cannot depend on `breadcrumb`.
child.register(
  'breadcrumb',
  factory(({ breadcrumb }) => breadcrumb + 'child/'),
);

// Workaround #1: access parent scope directly
child.register('breadcrumb', factory(() => parent('breadcrumb') + 'child/'));

// Workaround #2 (preferred): register the parent as a child service
child.register('$parent', constant(parent.scope));
child.register(
  'breadcrumb',
  factory(({ $parent: { breadcrumb } }) => breadcrumb + 'child/'),
);

Examples

Head over to the examples directory for in-depth examples.

Contributing

Constructive feedback is always welcome! Feel free to create issues if you have any question, suggestion or bug reports. A pull request is also always appreciated.

Clone this repository, run npm install or yarn to install the development dependencies, launch npm test -- -w or yarn test -w and start hacking!

Before you submit your pull request, please make sur you've run Prettier (npm run lint or yarn lint) and that your test coverage is at 100% (npm run coverage or yarn coverage).