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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@prun/prun

v0.1.3

Published

`prun` is a least opinionated modular ecosystem of modules for hexagonal architecture. This package is the shell entrypoint for prun prorgrams. It acts as the thin minimal basis for the core services

Readme

prun

Pronounced prun, or P-Run, or Pro-run - however you like, all are correct. P stands for Project, Program, Process, Professional, Power - whatever you like.

In the best spirit of JavaScript - it is opinionated about being unopinionated. It aspires to let you do whatever you want, but still funnel you into healthy practices by trying to make them the path of least resistence for you.

However, we aspire to let you take ownership and responsibility and go the hard way whenever you feel you must.

Architecture & Philosophy

The current trend in frameworks is to obfuscate the lower levels from the programmer, focusing her on the high-level business logic.

But: How do I expect programmers to guard and protect architectural boundaries if I do everything I can to hide it from them?

prun takes the opposite approach. prun wants the developers to be exposed to the layers the program is made of, and acknowledge the borders between them. prun wants to make the discourse about tidy code, SOLID principals and architecture to be an elementary part of working on a prun project.

Principals

IoC. Not DI.

prun choses to keep SOLID's Invertion of Control principal (IoC), but stop one step before the complex voodoo of a full DI.

The thing about DI is that users of advanced DI systems become completely oblivious to the initiation process. This could be both a boon and a bane.

Common code smells observed with projects that use DI systems are:

  • lacking understanding of architectural boundaries
  • circular dependencies
  • unpredictable order of operations in initiation
  • complacency or helplessness about the startup sequence

prun wants more than making the initiation logic obvious and transparent, it wants its users to have a mental map of what belongs where, and why. It wants users to be able to own this structure and be a part of the architectural considerations it embodies.

Using Layers to Separate Execution Context and Business Logic

When the Business Logic gets a message to process - it should not be aware if it's a request that came through a web-interface, a remote storage input for a step-function, a message that came from a queue, or through arguments of a batch-job CLI.

In the same manner, the business logic should be concerned with business entities and logical operations applied to them, while the underlying implications of these logical operations and their implementation details are kept abstract.

For this, a prun application is built of layers.

You can have as many layers as you like. These layers are meant to support and encourage what won the name of hexagonal architecture (TBD: link). If you flatten and simplify a hexagonal architecture to a list of layers, you end up with:

[downstream ports (DP)]
       |
       |------------- [DTOs]
      \|/
[business-Logic (BL)] -> [Domain Entities]
       |
       |--------------[DTOs]
      \|/
[upstream ports (UP, aka DAL)] 
  • Downstream Ports - are all the interfaces through which the application can accept a message: HTTP, Queue, TCP, CLI.
  • Business Logic - the layer that encapsulataes and facilitates the Domain entities.
  • Upstream Ports - are all the backend interfaces through which applicaiton can ask to retrieve or modify information.

Over this basis, you can add as many layers as you like.

An example: Caching Logic

Ports that manage cache are basically another form of Upstream ports. However, given a cache DAL component next to your DB compopnent - it's useful to place a cache controller in a caching layer between BL and UP, separating the concern of caching logic from the BL. So:

  • Downstream Ports
  • Business Logic - the layer that encapsulataes and facilitates the Domain entities.
  • Caching Logic - the layer that encapsulates caching logic and faciliates it to the BL
  • Upstream Ports - are all the backend interfaces through which applicaiton can ask to retrieve or modify information (i.e - from DB or from cache).

Another example: Context-depednent Validation Logic

While BL should be devoid of execution context and focus on business rules and domain entities, sometimes there are validations that are applicable to specific ports. However, loading these validation logic on the layer of the ports would mix concerns of protocol implementation details with control logic.

  • Downstream Ports - are all the interfaces through which the application can accept a message: HTTP, Queue, TCP, CLI.
  • Context-Validations - proxies the BL to the Ports layer, but adds logic of filtering invalid runtime requests
  • Business Logic - the layer that encapsulataes and facilitates the Domain entities.
  • Upstream Ports

Another example: View Logic

In this example, unlike the Context-Validation, this layer will not hide the BL from the interfaces, but will provide the interfaces mapping and formatting logic.

Mapping and formatting logic tend to be synchronous, and manage only logic of what piece of information goes where in the view, rathern than where it is obtained from.

If you need to enrich the information - consider consolidating enrichment logic in it's own components, which will probably be placed in a deeper layer of it's own.

Another example: Business Entities

Business entities are expected to be pure entities, not only devode of context, but also ignorant to the use-cases. If you use facilitators for Business entities - it is advised to place them in the deepest layer. This is true also for auxilliary, like custom error classes that are meant to be reused accross layers. e.g:

  • Downstream Ports
  • Business Logic
  • Upstream Ports
  • Business Entities

Generic Components Architecture

For this, each component module is expected to export a factory method. This factory is called with the configuration for this component, and the IoC bag of all components that were initiated in previous layers.

Both bags are meant to be destructured, but you don't have to.

MJS:

//load time
export default async (options, ioc) => {
  //initiation time
  return {
    //... runtime API
  };
};

CJS:

//load time
module.exports = async (options, ioc) => {
  //initiation code
  return {
    //... runtime API
  };
};

Note the factory does not have to be async, but prun awaits for it.

prun encourages you to avoid load-time code. Its core is designed to handle the load/init/run sequence for you with maximum predictability and transparency. For this, you are encouraged to relay on dependencies passed in the ioc rather than modules you import directly.

This separation will also help you with test.

Fail Fast

In our context, the Fail Fast principal sais that configuration, network and topography errors must be discovered before the application starts to process workload. Components should do everything they can do in validation/initiation time to assert their ability to handle workload.

More over - ideally, prun wishes to know that the configured layers should work before committing any resources like TCP connections or local cache building, rather than successfully initiate the lower layers and obtain DB connection for them, just to find out that the configuration is broken for the upper layers.

For this, prun encourages the use of early validation steps through which each component can assert that it has all it needs to be able to function. If any of these load-time validation fails - the process will fail and refuse to join the workload.

The first validations are core-level validations. They validate that the generic structure of the configuration, and that all the modules named are found. prun will throw very detailed errors for any rejections from these stages. Then it proceeds to load all your components.

Once the component modules are loaded and before invoking any of their factories - the core will run any configuration validation hooks you provide.

The API for configuration validation handler is: validateConfig(options, IoC) throws PrunConfigError.

The validation is synchronous. Mind that in this stage, none of the components are initiated, so the IoC contains initiated instances only for the core components, and the rest hold just metadata. (The errors component is a core component, the rest of the core is explained later).

MJS

export const validateConfig = ({ positive }, { errors: { configError } }) => {
  if (positive < 0) {
    throw configError('options.positive is expected to be a positive number');
  }
};

CJS

module.exports = (options, ioc) => { /* ... */ };
module.exports.validateConfig = ({ positive }, { errors: { configError } }) => {
  if (positive < 0) {
    throw configError('options.positive is expected to be a positive number');
  }
};

Note that this handler is provided with a factory faclitator: configError(..). Using it is equivalent to throw new PrunConfigError(..)

Upon running all config validation handlers - the core proceeds to initiation, calling your factories.

The contract is: If the factory returns/resolves successfully - your component should have everything it needs to do it's work.

If you have validations to make after components are loaded - you need to validate data obtained through async operations - you may fall back to doing validations in initiation time.

There are also load-time events you can use - more on that later.

Distinct, Clear and Descriptive Errors

prun looks at errors like a fully-fledged interface messages. True, in many cases these errors are not meant for end-users, however, prun counts developers are users (albeit more privileged ones).

For this, prun does few things:

  1. It utilizes an errors registry that helps developers invest in their errors.
  2. The facilitation of errors puts emphasize on how they look - i.e - it treats errors as views, even when they are thrown to the CLI, or represented as JSON for log visualizations.
  3. All the errors prun throws are very descriptive, and bring with them every piece of context that is safe to float up. These errors serve as an example for users to follow.

A component may register custom errors to errors registry by exporting a custom-errors factory as follows:

MJS:

export const errors = (options, { PrunError }, ioc) => {
  //initiation code
  return [
    class PrunFutureError extends PrunError {
      constructor(meta) {
        super('This error came from the future', meta);
      }
    },
  ];
};
module.exports = (options, { PrunError }, ioc) => {
  //initiation code
  return [
    class PrunFutureError extends PrunError {
      constructor(meta) {
        super('This error came from the future', meta);
      }
    },
  ];
};

The factory should return an array of subclasses of PrunError.

All custom errors should extend PrunError. This helps distinct violent unhandled errors from deliberated errors. Note that while the name for the error type is an InitCapitalErrorType name, the registry facilitates for each error type an error factory that just saves you the new operator as errorTypeFactory.

So, assuming MyFutureError is declared, both these forms are equivalents:

export default (options, { errors: { MyFutureError, myFutureError } }) => {
  return {
    run() {
      //...
      throw new MyFutureError({ oupsy: 'dazy' });
      //...
      throw myFutureError({ oupsy: 'dazy' });
    },
  };
};

No voodoo (Maximize User Control)

prun aspires to make as much heavy lifting as lightweight as possible for the least amount of envelope code. This refers to the heavy lifting of: configuration, initiation, and process management. It aspires to do that without sacrificing user control.

To keep the control on the users side, it does few things:

Minimalist but robust host process

prun offers you an implementation of a host process. Just like a test-runner consumes your test suites and runs them for you - prun host process consumes your application modules and runs them for you.

It does so within a modular minimalist core. The minimalist core that comes with prun is made of two rings:

Core components:

The core components:

  • bus - local in-memory event bus, commonly for the shutduwn event, but can be used for custom events as well.
  • config - config data-structure, validated by extensible load-time config validations
  • errors - extensible error types registry, which facilitates factories for all error types it learns.
  • logger - there are two built-in minimalist loggers. The user can provide a custom logger if they wish.

The purpose of the core is to be able to fire the first log entry. Being able to emit log entries means it can be consisten and transparent about the loading sequence of your modules.

To be able to emit log entries, it has to load your configuation and obey any log-verbosity directives you provide. For this, the config is loaded before the log is initiated.

It loads all configurations from all sources (file, env, CLI switches) and consolidate them into a consistent configuration model.

User-Application life-cycle manager: The life-cycle of a single component is: load -> validate -> initialize -> runtime.

Components are grouped in layers that are processed together. The overal lifecycle is:

  1. loads all layers from bottom to top, with their components in them. This includes:
  • import the component module
  • process directives pull values from custom env-vars to the config model
  • register any custom errors
  1. validate all layers, from bottom to top, with their components, calling any exported configValidation handlers.
  2. initialize all layers, from bottom to top, with their components - by calling your factories, and accumulating the results on the IoC bag of the layer.
  3. activate the run layers - the interface layers.

you control the structure

Users determine the structure of the application and the initiation order-of-operations it implies.

The structure is described in a yaml file that by default is called .prunrc. The main config keys of this file is the root keys layers, and run.

  • layers is a list of objects.
    • Each object represents a layer, where the keys are the iocNames of components in this layer, and the values specify and configure these components.
    • All components in the same layer are initiated in parallel and should not consume each other.
    • All components of all layers are accumulated on the same IoC context under the iocName they are defined.
    • Each layer is provided the initiated context as the previous layer had left it. The value returned by the factories is accumulated on the IoC bag that will be passed to the next layer.
    • The lowest layer is initiated first, and is exposed only to the minimalist core components. Thus, in a classical structure, interfaces can access BL and BL can access DAL. This also means a mid layer can shadow components of lower layers from upper layers if you so wish.
  • run names the components that also act as interfaces.
    • it specifies what method to call on them (if no method is specified - it defaults to a default method run()).
    • the IoC bag that is passed to the runner is the IoC bag as the highest layer had left it. This means that a component that is an interface must not be shadowed by upper layers.

The structure is demonstrated with examples, test-fixtures, documented, and validated with very descriptive errors. (TBD: - add links to these different communicators).

Ecosystem of Modularity over the core

Any component can be a module that is local to the project, or a module that is shipped independently as a package.

prun aspires to facilitate the growth of a community ecosystem of SOLID modular components that can be shared and reused just by installing them from npm and naming them in your .prunrc.

Modularity of the core

prun aspires to let the user replace any core part with a part of their own.

Currently, the only core component that can be replaced using only configuration is the logger.

There is intent to support users placing their own replacement for any component (under the premise that you take responsibility on concerns of any components you replace).

Components you cannot replace now are transparent and verbose on their actions. It's done in two channels:

  1. using log entries with verbosity you can control
  2. by firing events you can hook on (see next section)

Last - it exports all its internals for your use if you ever need to rewrite any of it's parts, including the host process itself.

expose events of the load, validation and initiation lifecycle

These events are emitted on the local event-bus exposed on the IoC as bus. Each event accepts a context payload. The lifecycle fires the following events:

  • module-loaded
  • module-loaded:${iocName}
  • layer-loaded
  • layer-loaded:${layerName}
  • layer-validated
  • layer-validated:${layerName}
  • module-initiated
  • module-initiated:${iocName}
  • layer-initiated
  • layer-initiated:${layerName}

Mind the order of operations: It does not make sense to wait for an initiation event of a component in a deeper layer - all components of deeper layers have been initiated before all the components in your layer.

These events were born from an use-case that requires a circular dependency and a two way communication to be established. The initiated events let components of lower layer hook on initiation events of upper layers, and obtain a reference to components from higher levels once they are ready to serve.

Modern logger with High Log Verbosity control

The logger is a core component. The logger facilitates using different emission channels, each channel can be configured with its own warn level. The logger is configured using the root key logger:

logger:
  levels:
    main: info
    default: warn

Logger instances follows the pino interface (i.e meta-first). (TBD: link) Logger instances are augmented with an additional logger-broker method: of(name, meta): Logger, used like so:

export default (cfg, { logger }) => {
  const log = logger.of('my-logging-channel');
  // ...
};

It is common to name logging channels after components, however it does not have to be so. You can use any naming scheme for your log channels and control the verbosity of each channel.

The .of(..) api should be used instead of .child(meta) whenever you pull a logger instance. This assures that the level of the logger is set according to level defined in the logger.levels[channelName] (TBD: link to code). Currently, this method is a simple factory that returns a new instance whenever it is called. However, it may be refactored in the future, so that there will be a single logger instance per channel.

Configuration is a Model

An application may consume configuration from channels: files, env-variables, CLI switches. Components shoud not be aware of what channel the configuration was obtained. Any logic of precedence between the different channels should be kept predictable and consistent.

The configuration model is what consumes all these channels and consolidates then into a single model that is consistent and available synchronously.

The config logic is:

  1. it uses under the hood rc (TBD: link) to locate and load your .prunrc file as an overlay over the baked-in base defaults (TBD: link). As such, it supports the --config <filePath> switch.
  2. if cwd is defined (in the file or in --cwd) - it changes current dir to that path.
  3. loads name and version from the package.json in the cwd.
  4. loads env vars from .env using dotenv (TBD: link)
  5. it applies user directives to project env vars onto the config tree (TBD: explain, link)
  6. last - it exposes config.isTTY and config.env The later a step is in the logic - the more it may cascade previous steps.