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

enj

v0.0.8

Published

A powerful CLI framework wrapped in an intuitive file based router.

Downloads

94

Readme

Enj

Enj - noun (/ɛndʒ/)

A powerful CLI framework wrapped in an intuitive file based router.

Why

  • Simple to start, simple to use
  • Easy to iterate, easy to configure
  • Clean, readable file structure
  • Extraordinarily powerful command definitions

Why not

  • Doesn't support Windows yet

Quickstart

  1. Create a directory in your project root called enj

  2. Add a file to that directory called hello-world.js containing the following

    ESM
    // enj/hello-world.js
    export default (cmd) =>
      cmd.action(() => {
        console.log(`Hello World!`)
      })
    CommonJS
    // enj/hello-world.js
    module.exports = (cmd) =>
      cmd.action(() => {
        console.log(`Hello World!`)
      })
    Typescript

    Enj also supports typescript with one simple installation.

    Make sure your file extension is .ts.

    // enj/hello-world.ts
    import type { Cmd } from "enj"
    
    export default (cmd: Cmd) =>
      cmd.action(() => {
        console.log(`Hello World!`)
      })
  3. Run the command with Enj

    npx enj hello-world
    # Hello World!

Install

Enj can be installed globally or locally.

Install locally in team projects to ensure versions stay in sync.

Local

npm install --save-dev enj

Global

npm install --global enj

Adding Commands

This file structure

enj
├── cowsay
│   ├── dragon.js
│   └── index.js
├── hello-world.js
├── _utils.js
└── index.js

Gives the following command structure

enj
enj cowsay
enj cowsay dragon
enj hello-world

Enj uses a file-based router to define the command structure.

To add the command enj hello-world, we add a file to the enj root directory called hello-world.js, or hello-world.ts if you're using typescript.

Directories are used to create subcommands. For instance adding the directory cowsay/ and the file cowsay/dragon.js, we create the command enj cowsay dragon.

Enj respects index.[js|ts] files. You can add configure the enj cowsay command by adding the command file cowsay/index.js.

Ignoring files

Files and directories starting with _ will be ignored by Enj.

Where to add command files

Unless it has been reconfigured, the Enj root directory is <DIR>/enj, where DIR is the nearest project root or the Current Working Directory.

Writing command files

CommonJS
module.exports = (cmd) =>
  cmd
    .argument("<name>", "Your name")
    .option("-e, --excite", "Be excited")
    .action((name, { excite }) => {
      const punctuation = excite ? "!" : "."
      console.log(`Hello ${name}${punctuation}`)
    })
ESM
export default (cmd) =>
  cmd
    .argument("<name>", "Your name")
    .option("-e, --excite", "Be excited")
    .action((name, { excite }) => {
      const punctuation = excite ? "!" : "."
      console.log(`Hello ${name}${punctuation}`)
    })
Typescript

Make sure you are using typescript.

import type { Cmd } from "enj"

export default (cmd: Cmd) =>
  cmd
    .argument("<name>", "Your name")
    .option("-e, --excite", "Be excited")
    .action((name, { excite }) => {
      const punctuation = excite ? "!" : "."
      console.log(`Hello ${name}${punctuation}`)
    })

As of v0.0.x, Enj uses commander.js for command definitions. This may change in future versions.

This means the cmd argument that is passed into Enj command files is a commander Command object.

NOTE: Because of this, you have control over more than just the command you are configuring in your command file. You could rename your command, add subcommands, return a different command, alias your command to something else, and much more. However this is undefined behaviour and really possibly could break things. Only get freaky with it if you know what you're doing.

cmd Examples

Full documentation of the cmd child methods can be found here.

Some useful examples:

Options
cmd
    .option("--long", "Long option")
    .option("-s", "Short option")
    .option("-b, --both", "Both long and short options")
    .option("-n, --no-description")
    .action(({ long, s, both, noDescription }) => { ... })
Option arguments
cmd
    .option("-n, --no-arg", "No option-argument")
    .option("-r, --required-arg <VALUE>", "Required option-argument")
    .option("-o, --optional-arg [VALUE]", "Optional option-argument")
    .action(({ arg, requiredArg, optionalArg }) => { ... })

Note that no-arg is inverted and becomes arg rather than noArg. Read more here.

Option argument defaults
cmd
    .option("-r, --required-arg <VALUE>", "Required") // required args don't take defaults
    .option("-o, --optional-arg [VALUE]", "Optional with default", "default value")
    .action(({ optionalArg }) => { ... })

Note that this only applies if the option is left out entirely. If it is used but empty, it will be true.

Option argument parsing
cmd
    .option("-r, --required-arg <VALUE>", "Required", parseFloat)
    .option("-o, --optional-arg [VALUE]", "Optional with default", parseInt, 7)
    .action(({ requiredArg, optionalArg }) => { ... })
Option argument choices
const { Option } = require("enj");
...

cmd
    .addOption(
        new Option("-c, --cheese [CHEESE]", "add extra cheese")
            .default("cheddar")
            .choices(["cheddar", "brie", "gouda", "parmesan"])
    )
    .action(({ cheese }) => { ... })
Arguments
  cmd
    .argument("<required>", "Required argument")
    .argument("[optional]", "Optional argument")
    .argument("[no-description]")
    .action((required, optional, noDescription) => { ... })

Note that unlike options, arguments are given to the action handler as separate arguments that precede the options object. Read more here.

Argument defaults
  cmd
    .argument("<required>", "Required argument") // required args don't take defaults
    .argument("[optional]", "Optional argument with default", "optional default")
    .action((required, optional) => { ... })
Argument parsing
  cmd
    .argument("<required>", "Required argument", parseFloat)
    .argument("[optional]", "Optional argument", parseInt, 7)
    .action((required, optional) => { ... })
Argument choices
const { Argument } = require("enj");
...

cmd
    .addArgument(
        new Argument("<size>", "Choose a size")
            .default("small")
            .choices([
                "small",
                "medium",
                "large",
            ]),
    )
    .action((size) => { ... })

This is only scratching the surface. Read the commander.js examples and docs for more.

Config

Enj can be configured in all of three ways

Values

| Config | Default | Environment varable | Run script arg | Config file value | | ----------------------- | -------------------- | ------------------- | ----------------------------- | ----------------- | | Config file path | Found dynamically | ENJ_CONFIG_FILE | configFile | N/A | | Commands root directory | <PROJECT_ROOT>/enj | ENJ_ROOT_DIR | rootDir | rootDir |

Relative paths

Paths can be absolute. If they are relative, Enj does it's best to resolve them to something sensible. This depends on the configuration method.

| | Environment varable | Run script arg | Config file value | | ------------------------- | ------------------------- | ----------------------------- | ----------------- | | Relative to the directory | The command was called in | Of the run script | The config is in |

Config files

Package.json

Add an enj object to your package.json:

"enj": {
    "rootDir": "./commands"
},
Other config files

Supported config files are

.enjrc
.enjrc.[format]
enj.config.[format]
.config/enjrc
.config/enjrc.[format]

Supported formats are json|yaml|yml|js|ts|mjs|cjs

If no config file is defined explicitly, Enj will search for a config file all the way down to your home directory.

If you are using a custom run script to configure Enj, config search will start from the directory your script is in.

Otherwise it will fall back through the following locations:

  1. Environment variable ENJ_ROOT_DIR
  2. Run script config rootDir
  3. The nearest nodejs project root
  4. The Current Working Directory

After arriving at your home directory, if no config files are found, the global configuration directory is also checked. The search location is defined by env-paths (without suffix.)

This directory is searched for the following files:

config
config.[format]

Invoking

Commandline

If installed locally

npx enj -h

If installed globally

enj -h

Run script

A custom CLI can be created with custom configuration.

Create a cli file and make it executable (use any name you like, cli is an example):

touch ./cli && chmod +x ./cli
CommonJS
// ./cli
#! /usr/bin/env node

const { run } = require('enj')
run()
ESM
// ./cli
#! /usr/bin/env node

import { run } from 'enj'
run()
Typescript

Make sure you are using typescript.

IMPORTANT: Note the use of npx tsx instead of node in the shebang.</sub>

// ./cli
#! /usr/bin/env npx tsx

import { run } from 'enj'
run()
Configuring

The run() function takes an optional config object:

run({
    rootDir: 'commands',
})

Usage

Run the script from the Commandline (if you called it something else, use that name.)

./cli -h

Why

Why use a run script? You may want to publish your own CLI, rename Enj locally, or avoid using configuration files. These are all valid reasons to use a run script.

Package.json

You can of course use a package.json script to call enj.

"scripts": {
    ...
    "hello": "enj hello-world",
    ...
},

Typescript

Using typescript with Enj is as easy as installing tsx either globally or locally.

Installation


npm install --save-dev tsx

Usage

// enj/hello-world.ts
import type { Cmd } from "enj"

export default (cmd: Cmd) =>
  cmd.action(() => {
    console.log("Hello World!")
  })

Enj still supports js files if you install tsx, so you can incrementally adopt typescript if necessary.

Enj also supports both ESM and CommonJS modules in JS, meaning you don't need to choose.