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

pertain

v0.2.1

Published

Automated pub/sub across project dependencies. Run code from any installed package based on declarative rules in package.json

Downloads

12,054

Readme

pertain 📋

npm version codecov snyk npm downloads

the easiest way to build a pluggable library

Scan all explicitly declared package dependencies in the current project for Node modules that declare a particular purpose.

Usage

You want to detect which of a project's dependencies can do a particular task. No, that's too abstract.

You're throwing a potluck dinner party with NodeJS. You're listing your guests as dependencies, and inviting them over with npm install.

potluck/package.json
{
  "name": "@my-house/potluck",
  "version": "1.0.0",
  "description": "Come over Saturday!",
  "dependencies": {
    "aunt-cathy": "^1.2.3",
    "@work/cornelius": "^4.3.1",
    "grandma": "^23.0.1",
    "philippe": "^0.5.0"
  }
}

You have cleverly selected family and friends who can cook. But you don't know what each of them wants to bring. How do you set the dang table?

potluck/prep.js
const cathy = require('aunt-cathy');
const cornelius = require('@work/cornelius');
const grandma = require('grandma');
const philippe = require('philippe');

let numSoups = 0;
numSoups += cathy.howManySoups();
numSoups += cornelius.howManySoups();
numSoups += grandma.howManySoups();
numSoups += philippe.howManySoups();


const shoppingList = [
  `${numSoups} tureens`,
  `${numSoups * 2} ladles`
];

That's a lot of manual work to build a whole shopping list. Plus, you get some updates from your guests:

  • Aunt Cathy can no longer make it!
    • npm remove aunt-cathy
    • Now the first line of prep.js will throw an exception.
  • Cornelius has joined a cult that is against soups.
    • cornelius.howManySoups() will now throw a BlasphemyError.
  • As a last minute substitute, you invite Cousin Toddwick. Maybe he cooks, right?
    • npm install cousin-todd
    • He's not mentioned in prep.js though!

You could manually edit prep.js, but it doesn't seem efficient, especially with more guests.

Like any good party planner, you ask all your guests to tell you what they're bringing.

Hey potluck pals! Could you each please add a potluck property to your package.json file? It should be the path of a module which exports an array of the dishes you'd like to make!

Some guests follow suit.

@work/cornelius/package.json
{
  "name": "@work/cornelius",
  "version": "4.4.0",
  "potluck": "./potluck-dishes.js"
}
@work/cornelius/potluck-dishes.js
const favorites = [
  'tomato soup',
  'brownies',
  'fondue'
];

// EDIT 20XX: SOUP IS EVIL
favorites = favorites.slice(1);

module.exports = favorites;

grandma/package.json
{
  "name": "grandma",
  "version": "23.0.2",
  "potluck": "./recipes"
}
grandma/recipes/index.js
module.exports = [
  'perfect enchiladas',
  'amazing pie',
  'awesome tortilla soup'
];

philippe/package.json
{
  "name": "philippe",
  "version": "0.5.1",
  "potluck": "./scrapbook/food-ideas"
}
philippe/scrapbook/food-ideas.js
module.exports = [
  'haricots verts',
  'vichysoisse soup'
];

The next time you update your dependencies, three of your guests have declared that they know how to potluck. Each of those declarations lists a Node module exporting a list.

This is going to make shopping easier.

const pertain = require('pertain');

const dishBringers = pertain('./', 'potluck');

You call pertain with the current directory to say "get the dependencies of whatever invoked this process". (In this case, that's your own prep.js script, but you always have to tell it.) To the second argument of pertain, you say 'potluck'.

This is what pertain returns:

[
  {
    "name": "@work/cornelius",
    "path": "/home/potluck/node_modules/@work/cornelius/potluck-dishes.js",
    "modulePath": "/home/potluck/node_modules/@work/cornelius",
    "subject": "potluck"
  },
  {
    "name": "grandma",
    "path": "/home/potluck/node_modules/grandma/recipes/index.js",
    "modulePath": "/home/potluck/node_modules/grandma",
    "subject": "potluck"
  },
  {
    "name": "philippe",
    "path": "/home/potluck/node_modules/philippe/scrapbook/food-ideas.js",
    "modulePath": "/home/potluck/node_modules/philippe",
    "subject": "potluck"
  }
]

Pertain has resolved each of those module paths to their actual location, so you can require() them no matter what context you're in. Let's map it into a list.

const allDishes = dishBringers.map((dishes, guest) => dishes.concat(require(guest.path)));

That code will run each named module in each package with potluck. Then it concatenates all the lists together. Now allDishes is:

[
  'brownies',
  'fondue',
  'perfect enchiladas',
  'amazing pie',
  'awesome tortilla soup',
  'haricots verts',
  'vichysoisse soup'
]

And here's our new, simpler prep:

potluck/prep.js
const pertain = require('pertain');

const dishBringers = pertain('./', 'potluck');

const allDishes = dishBringers.map((dishes, guest) => dishes.concat(require(guest.path)));

const soups = allDishes.filter(dish => dish.includes('soup'));

const shoppingList = [
  `${soups.length} tureens`,
  `${soups.length * 2} ladles`
];

That'll hold up better to changes.

This is an ultra-simple example. You can have multiple subjects in the same package, and subjects can be complex objects which you reference with dot-lookup. More TBD.

Other Examples

To make a package that pertain can automatically call when it's a listed dependency, declare a custom property in your package.json:

{
  "name": "potluck-guest-grandma",
  "description": "You're lucky she's coming",
  "potluck": {
    "desserts": "./potluck/desserts"
  }
}

When potluck-guest-grandma is installed in a project, and code in that project runs pertain("potluck.desserts"), then Pertain will load potluck-guest-grandma/potluck/desserts.js.


If potluck-guest-grandma depends on another package that pertains to the same topic, it should list that package in peerDependencies:

{
  "name": "potluck-guest-grandma",
  "description": "You're lucky she's coming",
  "potluck": {
    "desserts": "./potluck/desserts"
  },
  "peerDependencies": {
    "pie-baking-aunt": "^1.2.0"
  }
}

If this is declared, then Pertain will call potluck-guest-grandma after pie-baking-aunt by default.

To get all dependencies with potluck.desserts labeled in package.json:

const pertain = require('pertain');

const desserts = pertain(process.cwd(), 'potluck.desserts');

const dessertTable = {};

for (const dessertFile of desserts) {
  // Require and execute the module.
  const Dessert = require(dessertFile.path);
  // Expect that a dessert will be a class. Provide it with the table
  // everything else has set, so it can interact with other dependencies.
  const dessert = new Dessert(dessertTable);

  // Expect Dessert#serve() to run a side effect.
  dessert.serve();
}

Supply a custom getDependencies(found, packageJson, rootDir, subject) function to customize how pertain finds the list of dependency names. Its first argument is a union of dependencies and devDependencies, and by default it simply returns that argument. This is useful for when you are developing a pertinent package and linking it via npm link to the consuming package.

const pertain = require('pertain');

const desserts = pertain(
  process.cwd(),
  'potluck.desserts',
  deps => deps.concat(['neighbor-window-pie'])
);

const dessertTable = {};

for (const dessertFile of desserts) {
  // Require and execute the module.
  const Dessert = require(dessertFile.path);
  // Expect that a dessert will be a class. Provide it with the table
  // everything else has set, so it can interact with other dependencies.
  const dessert = new Dessert(dessertTable);

  // Expect Dessert#serve() to run a side effect.
  dessert.serve();
}

API

pertain(workingDirectory, subject, getDependencies?)

Return an array of module info, sorted in peer dependency order, for all modules declared as direct dependencies of the package root of workingDirectory. Filter those modules for only those which:

  • declare a property named subject in their package.json file
  • that property lists a JS module which can be resolved with require()

Returned module info is an array of objects with the following properties:

  • name: The name of the dependency package, e.g. left-pad.
  • path: The real filesystem path of the module file mentioned in the subject field
  • modulePath: The real filesystem path of the found module base directory
  • subject: The originally argued subject string

The subject can be a dot-lookup path, e.g. "foo.bar", which will then look for "foo": { "bar": "./path" } in the package.

pertain.clearCache()

Pertain caches expensive operations on the same package for the same subject. Use this method to clear that cache.