Skip to content

Commit 74686c0

Browse files
feature/TRI 712/create eslint plugin to ensure uniqueness on task keys (#324)
* test: add test to no-duplicated-task-keys rule * feat: implement no-duplicated-task-keys * chore: create testing scripts * refactor: fill in rule meta * feat: export no-duplicated-task-keys rule * chore: bump package version to 0.0.1 * chore: create eslint plugin * feat: create no-duplicated-task-keys rule * refactor: delete no-duplicated-task-keys from config-custom * refactor: rename eslint-plugin folder * feat: cover case from nextjs-example * feat: cover additional cases from examples/package-tester * chore: add eslint-plugin on nextjs-example * feat: add more cases from `nextjs-example` * revert: rollback changes on eslint-config-custom * Set version to 2.0.9, inline with other packages * Create chilly-pianos-try.md --------- Co-authored-by: Matt Aitken <[email protected]>
1 parent 8f3e550 commit 74686c0

File tree

11 files changed

+798
-433
lines changed

11 files changed

+798
-433
lines changed

.changeset/chilly-pianos-try.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@trigger.dev/eslint-plugin": patch
3+
---
4+
5+
An eslint plugin that ensures uniqueness on task keys
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
{
2-
"extends": "next/core-web-vitals"
2+
"extends": ["next/core-web-vitals"],
3+
"plugins": ["@trigger.dev"],
4+
"rules": {
5+
"@trigger.dev/no-duplicated-task-keys": 2
6+
}
37
}

examples/nextjs-example/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"@trigger.dev/sdk": "workspace:*",
2020
"@trigger.dev/slack": "workspace:*",
2121
"@trigger.dev/typeform": "workspace:*",
22+
"@trigger.dev/eslint-plugin": "workspace:*",
2223
"@types/node": "18.15.13",
2324
"@types/react": "18.2.17",
2425
"@types/react-dom": "18.2.7",

packages/eslint-plugin/.eslintrc.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"use strict";
2+
3+
module.exports = {
4+
root: true,
5+
extends: [
6+
"eslint:recommended",
7+
"plugin:eslint-plugin/recommended",
8+
"plugin:node/recommended",
9+
],
10+
env: {
11+
node: true,
12+
},
13+
overrides: [
14+
{
15+
files: ["tests/**/*.js"],
16+
env: { mocha: true },
17+
},
18+
],
19+
parserOptions: {
20+
sourceType: "module",
21+
ecmaVersion: 2020,
22+
},
23+
};

packages/eslint-plugin/README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# @trigger.dev/eslint-plugin
2+
3+
ESLint plugin with trigger.dev best practices
4+
5+
## Installation
6+
7+
You'll first need to install [ESLint](https://eslint.org/):
8+
9+
```sh
10+
npm i eslint --save-dev
11+
```
12+
13+
Next, install `@trigger.dev/eslint-plugin`:
14+
15+
```sh
16+
npm install @trigger.dev/eslint-plugin --save-dev
17+
```
18+
19+
## Usage
20+
21+
Add `trigger-dev` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix:
22+
23+
```json
24+
{
25+
"plugins": [
26+
"trigger-dev"
27+
]
28+
}
29+
```
30+
31+
32+
Then configure the rules you want to use under the rules section.
33+
34+
```json
35+
{
36+
"rules": {
37+
"trigger-dev/rule-name": 2
38+
}
39+
}
40+
```
41+
42+
## Rules
43+
44+
<!-- begin auto-generated rules list -->
45+
46+
| Name | Description |
47+
| :--------------------------------------------------------------- | :----------------------------------------------- |
48+
| [no-duplicated-task-keys](docs/rules/no-duplicated-task-keys.md) | Prevent duplicated task keys on trigger.dev jobs |
49+
50+
<!-- end auto-generated rules list -->
51+
52+
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Prevent duplicated task keys on trigger.dev jobs (`trigger-dev/no-duplicated-task-keys`)
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Please describe the origin of the rule here.
6+
7+
## Rule Details
8+
9+
This rule aims to...
10+
11+
Examples of **incorrect** code for this rule:
12+
13+
```js
14+
15+
// fill me in
16+
17+
```
18+
19+
Examples of **correct** code for this rule:
20+
21+
```js
22+
23+
// fill me in
24+
25+
```
26+
27+
### Options
28+
29+
If there are any options, describe them here. Otherwise, delete this section.
30+
31+
## When Not To Use It
32+
33+
Give a short description of when it would be appropriate to turn off this rule.
34+
35+
## Further Reading
36+
37+
If there are other links that describe the issue this rule addresses, please include them here in a bulleted list.

packages/eslint-plugin/lib/index.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* @fileoverview ESLint plugin with trigger.dev best practices
3+
* @author
4+
*/
5+
"use strict";
6+
7+
//------------------------------------------------------------------------------
8+
// Requirements
9+
//------------------------------------------------------------------------------
10+
11+
const requireIndex = require("requireindex");
12+
13+
//------------------------------------------------------------------------------
14+
// Plugin Definition
15+
//------------------------------------------------------------------------------
16+
17+
18+
// import all rules in lib/rules
19+
module.exports.rules = requireIndex(__dirname + "/rules");
20+
21+
22+
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* @fileoverview Prevent duplicated task keys on trigger.dev jobs
3+
* @author
4+
*/
5+
"use strict";
6+
7+
//------------------------------------------------------------------------------
8+
// Rule Definition
9+
//------------------------------------------------------------------------------
10+
11+
/** @type {import('eslint').Rule.RuleModule} */
12+
module.exports = {
13+
meta: {
14+
type: 'problem', // `problem`, `suggestion`, or `layout`
15+
docs: {
16+
description: "Prevent duplicated task keys on trigger.dev jobs",
17+
recommended: true,
18+
url: null, // URL to the documentation page for this rule
19+
},
20+
fixable: null, // Or `code` or `whitespace`
21+
schema: [], // Add a schema if the rule has options
22+
messages: {
23+
duplicatedTaskKey: "Task key '{{taskKey}}' is duplicated"
24+
}
25+
},
26+
27+
create(context) {
28+
const getArguments = (node) => node.arguments || node.argument.arguments;
29+
30+
const getKey = (node) => {
31+
const args = getArguments(node);
32+
33+
const key = args.find((arg) => arg.type === 'Literal');
34+
35+
if (!key) return;
36+
37+
return key.value;
38+
}
39+
40+
const getTaskName = (expression) => {
41+
const callee = expression.callee || expression.argument.callee;
42+
43+
const property = callee.property;
44+
45+
// We need property to be an Identifier, otherwise it's not a task
46+
if (property.type !== 'Identifier') return;
47+
48+
// for io.slack.postMessage, postMessage
49+
return property.name;
50+
}
51+
52+
const groupExpressionsByTask = (ExpressionStatements, map = new Map()) => ExpressionStatements.reduce((acc, { expression }) => {
53+
const taskName = getTaskName(expression);
54+
const taskKey = getKey(expression);
55+
56+
if (acc.has(taskName)) {
57+
acc.get(taskName).push(taskKey);
58+
} else {
59+
acc.set(taskName, [taskKey]);
60+
}
61+
62+
return acc;
63+
}, map);
64+
65+
const groupVariableDeclarationsByTask = VariableDeclarations => VariableDeclarations.reduce((acc, { declarations }) => {
66+
declarations.forEach((declaration) => {
67+
if (!['AwaitExpression', 'CallExpression'].includes(declaration.init.type)) return;
68+
69+
const taskName = getTaskName(declaration.init);
70+
71+
const taskKey = getKey(declaration.init);
72+
73+
if (acc.has(taskName)) {
74+
acc.get(taskName).push(taskKey);
75+
} else {
76+
acc.set(taskName, [taskKey]);
77+
}
78+
});
79+
80+
return acc;
81+
}, new Map());
82+
83+
return {
84+
"CallExpression[callee.property.name='defineJob'] ObjectExpression BlockStatement": (node) => {
85+
const VariableDeclarations = node.body.filter((arg) => arg.type === 'VariableDeclaration');
86+
87+
const grouped = groupVariableDeclarationsByTask(VariableDeclarations);
88+
89+
const ExpressionStatements = node.body.filter((arg) => arg.type === 'ExpressionStatement');
90+
91+
// it'll be a map of taskName => [key1, key2, ...]
92+
const groupedByTask = groupExpressionsByTask(ExpressionStatements, grouped);
93+
94+
groupedByTask.forEach((keys) => {
95+
const duplicated = keys.find((key, index) => keys.indexOf(key) !== index);
96+
97+
if (duplicated) {
98+
context.report({
99+
node,
100+
messageId: 'duplicatedTaskKey',
101+
data: {
102+
taskKey: duplicated
103+
},
104+
});
105+
}
106+
})
107+
}
108+
}
109+
}
110+
};

packages/eslint-plugin/package.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "@trigger.dev/eslint-plugin",
3+
"version": "2.0.9",
4+
"description": "ESLint plugin with trigger.dev best practices",
5+
"keywords": [
6+
"eslint",
7+
"eslintplugin",
8+
"eslint-plugin"
9+
],
10+
"author": "",
11+
"main": "./lib/index.js",
12+
"exports": "./lib/index.js",
13+
"scripts": {
14+
"lint": "npm-run-all \"lint:*\"",
15+
"lint:eslint-docs": "npm-run-all \"update:eslint-docs -- --check\"",
16+
"lint:js": "eslint .",
17+
"test": "mocha tests --recursive",
18+
"update:eslint-docs": "eslint-doc-generator"
19+
},
20+
"dependencies": {
21+
"requireindex": "^1.2.0"
22+
},
23+
"devDependencies": {
24+
"eslint": "^8.19.0",
25+
"eslint-doc-generator": "^1.0.0",
26+
"eslint-plugin-eslint-plugin": "^5.0.0",
27+
"eslint-plugin-node": "^11.1.0",
28+
"mocha": "^10.0.0",
29+
"npm-run-all": "^4.1.5"
30+
},
31+
"engines": {
32+
"node": "^14.17.0 || ^16.0.0 || >= 18.0.0"
33+
},
34+
"peerDependencies": {
35+
"eslint": ">=7"
36+
},
37+
"license": "ISC"
38+
}

0 commit comments

Comments
 (0)