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

endanger

v7.0.4

Published

Build Dangerfiles with ease.

Downloads

9,284

Readme

endanger

Build Dangerfiles with ease.

  • Break your Danger code into "rules".
  • Only run rules when a relevant file changes.
  • Make adding new rules more accessible to non-JS developers.

Setup

npm install --save-dev endanger

Note: Endanger requires [email protected] and above. Please update your danger dependency.

Create a file system like this:

package.json
dangerfile.ts
/danger/
  myFirstRule.ts
  mySecondRule.ts
  myThirdRule.ts

Then use the run(...rules) function from endanger in your dangerfile:

// dangerfile.ts
import { run } from "endanger"

import myFirstRule from "./danger/myFirstRule"
import mySecondRule from "./danger/mySecondRule"
import myThirdRule from "./danger/myThirdRule"

run(
  myFirstRule(),
  mySecondRule(),
  myThirdRule({
    someOption: "foo",
  }),
  myThirdRule({
    someOption: "bar",
  }),
)

Now let's write your first endanger rule.

import { Rule } from "endanger"

export default function myFirstRule() {
  return new Rule({
    match: {
      // "Glob" patterns of files you want to look at in this rule.
      files: ["scary-directory/**"],
    },
    // A map of strings for different warnings/failures/etc so you don't have to
    // clutter your rule code.
    messages: {
      // Pro-tip: The indentation will automatically be stripped away :P
      myFirstWarning: `
        Hey you added a new file to "scary-directory/"!
      `,
    },
    // And here goes your code for the rule...
    async run({ files, context }) {
      // You can explore the state of the files you matched with your glob patterns.
      for (let file of files.created) {
        // Then you can report a warning/failure/etc by referencing your message
        // from the map of strings above. You can also optionally include a file
        // and even a line number.
        context.warn("myFirstWarning", { file })
      }
    },
  })
}

This rule warns you whenever you create a new file in the scary-directory/. But endanger makes it easy to write lots of other types of rules.

import { Rule } from "endanger"

export default function mySecondRule() {
  return new Rule({
    match: {
      files: ["api/routes/*.py"],
    },
    messages: {
      foundNewRouteWithoutRateLimit: `...`,
      foundRemovedRateLimit: `...`,
      foundAddedRateLimit: `...`,
    },
    // And here goes your code for the rule...
    async run({ files, context }) {
      // files.modifiedOrCreated will give you a list of all files created or modified
      for (let file of files.modifiedOrCreated) {
        // file.created will tell you if the current file was created in this diff
        if (file.created) {
          // file.contains() will tell you if the file contains a string or regex
          if (!(await file.contains("@ratelimit("))) {
            context.warn("foundNewRouteWithoutRateLimit", { file })
          }
        }

        // file.modifiedOnly will tell you if the current file was created in this diff
        if (file.modifiedOnly) {
          // file.before() returns the state of the file before the changes (if it existed)
          let before = await file.before()?.contains("@ratelimit(")
          let after = await file.contains("@ratelimit(")

          if (before && !after) {
            context.fail("foundAddedRateLimit", { file })
          } else if (!before && after) {
            context.message("foundAddedRateLimit", { file })
          }
        }
      }
    },
  })
}

You can have rules that fire on things other than files, you could also match commits like so:

import { Rule } from "endanger"

let TICKET_REGEX = /\b(JIRA-\d+)\b/

export default function mySecondRule() {
  return new Rule({
    match: {
      commit: [TICKET_REGEX],
    },
    messages: {
      jiraLink: `
        [View linked ticket {ticket} on JIRA](https://jira.intranet.corp/{ticket})
      `,
    },
    async run({ commits, context }) {
      for (let commit of commits) {
        let match = commit.message.match(TICKET_REGEX)
        if (match) {
          context.message("jiraLink", {}, { ticket: match[1] })
        }
      }
    },
  })
}

Important! You can only access files or commits in your rule if you have a match filter defined for them. And you can only access files or commits which match your defined filter.

API

run

This should be in your Dangerfile, pass Rule's run them.

import { run } from "endanger"

import rule1 from "./danger/rule1"
import rule2 from "./danger/rule2"
import rule3 from "./danger/rule3"

run(
  rule1,
  rule2,
  rule3,
)

Rule

import { Rule } from "endanger"

export default function myRule() {
  return new Rule({
    match: {
      files: ["path/to/**", "{glob,patterns}"],
      commits: ["messages that contain this string", /or match this regex/],
    },
    messages: {
      myFirstWarning: `...`,
      mySecondWarning: `...`,
    },
    async run({ files, commits, context }) {
      // ...
    },
  })
}

Note: It's recommended you wrap your rules with a function so you could add options to them later. For example, you could run the same rule twice on different directories provided as options.

Context

context.warn("myMessage", location?, values?)
context.fail("myMessage", location?, values?)
context.message("myMessage", location?, values?)

// examples:
context.warn("myMessage")
context.warn("myMessage", { file })
context.warn("myMessage", { file, line })
context.warn("myMessage", { file, line }, { ...values })

Note: Your Rule's messages can have also have special {values} in them:

new Rule({
  messages: {
    myMessage: `
      Hello {value}!
    `,
  },
  async run(files, context) {
    context.warn("myMessage", {}, { value: "World" }) // "Hello World!"
  },
})

Bytes

This represents some readable data whether it be a File, FileState, or Diff.

// Read the contents of this file/diff/etc.
await bytes.contents() // "line1/nline2"

// Does this file/diff/etc contain a string or match a regex?
await bytes.contains("string") // true/false
await bytes.contains(/regex/) // true/false

Line

(extends Bytes)

line.lineNumber // 42

FileState

(extends Bytes)

// Get the file path (relative to repo root)
file.path // "path/to/file.ext"

// Get the file's name
file.name // "file.ext"

// Get the file dirname (relative to repo root)
file.dirname // "path/to"

// Get the file basename
file.basename // "file"

// Get the file extension
file.extension // ".ext"

// Does the file path match a set of glob patterns?
file.matches("path/to/**", "{glob,patterns}") // true/false

// Parse the file as JSON
await file.json() // { ... }

// Parse the file as YAML
await file.yaml() // { ... }

// Read this file line by line
await file.lines() // [Line (1), Line (2), Line (3)]
await file.lines({ after: line1 }) // [Line (2), Line (3)]
await file.lines({ before: line3 }) // [Line (1), Line (2)]
await file.lines({ after: line1, before: line3 }) // [Line (2)]

DiffLine

(extends Bytes)

// Has this diff line's content been addedd?
diffLine.added // true | false

// Has this diff line's content been removed?
diffLine.removed // true | false

// Has this diff line's content been changed (added or removed)?
diffLine.changed // true | false

// Is this diff line's content unchanged?
diffLine.unchanged // true | false

// What is the line number before the change?
diffLine.lineNumberBefore // number | null

// What is the line number after the change?
diffLine.lineNumberAfter // number | null

Diff

// Only the added lines
await diff.added() // [DiffLine, DiffLine]

// Only the removed lines
await diff.removed() // [DiffLine, DiffLine]

// All of the changed lines
await diff.changed() // [DiffLine, DiffLine, DiffLine, DiffLine]

// All of the changed lines with several lines of surrounding context
await diff.unified() // [DiffLine, DiffLine, DiffLine, DiffLine, DiffLine, ...]

// Returns a JSONDiff of the file (assuming the file is JSON)
await diff.jsonDiff() // JSONDiff { ... }

// Returns a JSONPatch of the file (assuming the file is JSON)
await diff.jsonPatch() // JSONPatch { ... }

// Get stats on the diff (number of changed/added/removed/etc lines)
await diff.stats() // { changed: 5, added: 3, removed: 2, before: 2, after: 3 }

// Test if the diff contains changes greater than one of these thresholds
// (Thresholds are 0-1 as percentages)
await diff.changedBy({ total: 0.5 }) // true/false
await diff.changedBy({ added: 0.3 }) // true/false
await diff.changedBy({ removed: 0.2 }) // true/false
await diff.changedBy({ added: 0.3, removed: 0.2 }) // true/false

File

(extends FileState)

// Has the file been created?
file.created // true/false

// Has the file been deleted?
file.deleted // true/false

// Has the file been modified? (This doesn't include created files)
file.modifiedOnly // true/false

// Has the file been modified or created?
file.modifiedOrCreated // true/false

// Has the file been touched (created, modified, or deleted)?
file.touched // true/false

// Has the file been moved from another location?
await file.moved() // true/false

// Get the state of the file before all the changes made.
file.before() // File | null

// Get information about the diff of the file
file.diff() // Diff

Files

(extends Bytes)

// Get all of the created files.
files.created // [File, File, ...]

// Get all of the deleted files.
files.deleted // [File, File, ...]

// Get all of the modified (not including created) files.
files.modifiedOnly // [File, File, ...]

// Get all of the modified and created files.
files.modifiedOrCreated // [File, File, ...]

// Get all of the touched (created, modified, or deleted) files.
files.touched // [File, File, ...]

// Get all of the untouched files.
files.untouched // [File, File, ...]

// Get all files regardless of if they have been touched or not.
files.all // [File, File, ...]

// Get a specific file. (throws if it doesn't exist)
files.get("path/to/file.ext") // File

// Filter files by a set of glob patterns
files.matches("path/to/**", "{glob,patterns}") // Files