7.0.4 • Published 2 years ago

endanger v7.0.4

Weekly downloads
1,962
License
MIT
Repository
-
Last release
2 years ago

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 danger@10.5.3 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
7.0.4

2 years ago

7.0.3

2 years ago

7.0.2

3 years ago

7.0.0

3 years ago

7.0.1

3 years ago

6.0.0

3 years ago

5.0.1

3 years ago

5.0.0

3 years ago

4.0.4

3 years ago

4.0.3

3 years ago

4.0.2

3 years ago

3.1.0

3 years ago

4.0.1

3 years ago

4.0.0

3 years ago

3.0.4

3 years ago

3.0.3

3 years ago

3.0.2

3 years ago

3.0.1

3 years ago

2.0.2

3 years ago

3.0.0

3 years ago

2.0.1

3 years ago

1.0.2

3 years ago

1.0.1

3 years ago

2.0.0

3 years ago

1.0.0

3 years ago