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 yourdanger
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
orcommits
in your rule if you have amatch
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