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 <matt@mattaitken.com>
This commit is contained in:
Wesley
2023-08-18 08:57:12 -03:00
committed by GitHub
parent 8f3e550d03
commit 74686c00cb
11 changed files with 798 additions and 433 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/eslint-plugin": patch
---
An eslint plugin that ensures uniqueness on task keys
+5 -1
View File
@@ -1,3 +1,7 @@
{ {
"extends": "next/core-web-vitals" "extends": ["next/core-web-vitals"],
"plugins": ["@trigger.dev"],
"rules": {
"@trigger.dev/no-duplicated-task-keys": 2
}
} }
+1
View File
@@ -19,6 +19,7 @@
"@trigger.dev/sdk": "workspace:*", "@trigger.dev/sdk": "workspace:*",
"@trigger.dev/slack": "workspace:*", "@trigger.dev/slack": "workspace:*",
"@trigger.dev/typeform": "workspace:*", "@trigger.dev/typeform": "workspace:*",
"@trigger.dev/eslint-plugin": "workspace:*",
"@types/node": "18.15.13", "@types/node": "18.15.13",
"@types/react": "18.2.17", "@types/react": "18.2.17",
"@types/react-dom": "18.2.7", "@types/react-dom": "18.2.7",
+23
View File
@@ -0,0 +1,23 @@
"use strict";
module.exports = {
root: true,
extends: [
"eslint:recommended",
"plugin:eslint-plugin/recommended",
"plugin:node/recommended",
],
env: {
node: true,
},
overrides: [
{
files: ["tests/**/*.js"],
env: { mocha: true },
},
],
parserOptions: {
sourceType: "module",
ecmaVersion: 2020,
},
};
+52
View File
@@ -0,0 +1,52 @@
# @trigger.dev/eslint-plugin
ESLint plugin with trigger.dev best practices
## Installation
You'll first need to install [ESLint](https://eslint.org/):
```sh
npm i eslint --save-dev
```
Next, install `@trigger.dev/eslint-plugin`:
```sh
npm install @trigger.dev/eslint-plugin --save-dev
```
## Usage
Add `trigger-dev` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix:
```json
{
"plugins": [
"trigger-dev"
]
}
```
Then configure the rules you want to use under the rules section.
```json
{
"rules": {
"trigger-dev/rule-name": 2
}
}
```
## Rules
<!-- begin auto-generated rules list -->
| Name | Description |
| :--------------------------------------------------------------- | :----------------------------------------------- |
| [no-duplicated-task-keys](docs/rules/no-duplicated-task-keys.md) | Prevent duplicated task keys on trigger.dev jobs |
<!-- end auto-generated rules list -->
@@ -0,0 +1,37 @@
# Prevent duplicated task keys on trigger.dev jobs (`trigger-dev/no-duplicated-task-keys`)
<!-- end auto-generated rule header -->
Please describe the origin of the rule here.
## Rule Details
This rule aims to...
Examples of **incorrect** code for this rule:
```js
// fill me in
```
Examples of **correct** code for this rule:
```js
// fill me in
```
### Options
If there are any options, describe them here. Otherwise, delete this section.
## When Not To Use It
Give a short description of when it would be appropriate to turn off this rule.
## Further Reading
If there are other links that describe the issue this rule addresses, please include them here in a bulleted list.
+22
View File
@@ -0,0 +1,22 @@
/**
* @fileoverview ESLint plugin with trigger.dev best practices
* @author
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const requireIndex = require("requireindex");
//------------------------------------------------------------------------------
// Plugin Definition
//------------------------------------------------------------------------------
// import all rules in lib/rules
module.exports.rules = requireIndex(__dirname + "/rules");
@@ -0,0 +1,110 @@
/**
* @fileoverview Prevent duplicated task keys on trigger.dev jobs
* @author
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem', // `problem`, `suggestion`, or `layout`
docs: {
description: "Prevent duplicated task keys on trigger.dev jobs",
recommended: true,
url: null, // URL to the documentation page for this rule
},
fixable: null, // Or `code` or `whitespace`
schema: [], // Add a schema if the rule has options
messages: {
duplicatedTaskKey: "Task key '{{taskKey}}' is duplicated"
}
},
create(context) {
const getArguments = (node) => node.arguments || node.argument.arguments;
const getKey = (node) => {
const args = getArguments(node);
const key = args.find((arg) => arg.type === 'Literal');
if (!key) return;
return key.value;
}
const getTaskName = (expression) => {
const callee = expression.callee || expression.argument.callee;
const property = callee.property;
// We need property to be an Identifier, otherwise it's not a task
if (property.type !== 'Identifier') return;
// for io.slack.postMessage, postMessage
return property.name;
}
const groupExpressionsByTask = (ExpressionStatements, map = new Map()) => ExpressionStatements.reduce((acc, { expression }) => {
const taskName = getTaskName(expression);
const taskKey = getKey(expression);
if (acc.has(taskName)) {
acc.get(taskName).push(taskKey);
} else {
acc.set(taskName, [taskKey]);
}
return acc;
}, map);
const groupVariableDeclarationsByTask = VariableDeclarations => VariableDeclarations.reduce((acc, { declarations }) => {
declarations.forEach((declaration) => {
if (!['AwaitExpression', 'CallExpression'].includes(declaration.init.type)) return;
const taskName = getTaskName(declaration.init);
const taskKey = getKey(declaration.init);
if (acc.has(taskName)) {
acc.get(taskName).push(taskKey);
} else {
acc.set(taskName, [taskKey]);
}
});
return acc;
}, new Map());
return {
"CallExpression[callee.property.name='defineJob'] ObjectExpression BlockStatement": (node) => {
const VariableDeclarations = node.body.filter((arg) => arg.type === 'VariableDeclaration');
const grouped = groupVariableDeclarationsByTask(VariableDeclarations);
const ExpressionStatements = node.body.filter((arg) => arg.type === 'ExpressionStatement');
// it'll be a map of taskName => [key1, key2, ...]
const groupedByTask = groupExpressionsByTask(ExpressionStatements, grouped);
groupedByTask.forEach((keys) => {
const duplicated = keys.find((key, index) => keys.indexOf(key) !== index);
if (duplicated) {
context.report({
node,
messageId: 'duplicatedTaskKey',
data: {
taskKey: duplicated
},
});
}
})
}
}
}
};
+38
View File
@@ -0,0 +1,38 @@
{
"name": "@trigger.dev/eslint-plugin",
"version": "2.0.9",
"description": "ESLint plugin with trigger.dev best practices",
"keywords": [
"eslint",
"eslintplugin",
"eslint-plugin"
],
"author": "",
"main": "./lib/index.js",
"exports": "./lib/index.js",
"scripts": {
"lint": "npm-run-all \"lint:*\"",
"lint:eslint-docs": "npm-run-all \"update:eslint-docs -- --check\"",
"lint:js": "eslint .",
"test": "mocha tests --recursive",
"update:eslint-docs": "eslint-doc-generator"
},
"dependencies": {
"requireindex": "^1.2.0"
},
"devDependencies": {
"eslint": "^8.19.0",
"eslint-doc-generator": "^1.0.0",
"eslint-plugin-eslint-plugin": "^5.0.0",
"eslint-plugin-node": "^11.1.0",
"mocha": "^10.0.0",
"npm-run-all": "^4.1.5"
},
"engines": {
"node": "^14.17.0 || ^16.0.0 || >= 18.0.0"
},
"peerDependencies": {
"eslint": ">=7"
},
"license": "ISC"
}
@@ -0,0 +1,245 @@
/**
* @fileoverview Prevent duplicated task keys on trigger.dev jobs
* @author
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const rule = require("../../../lib/rules/no-duplicated-task-keys"),
RuleTester = require("eslint").RuleTester;
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
const ruleTester = new RuleTester({
parserOptions: {
sourceType: "module",
ecmaVersion: 2020,
}
});
ruleTester.run("no-duplicated-task-keys", rule, {
valid: [
{
code: `client.defineJob({
run: async (payload, io, ctx) => {
await io.runTask("task", { name: "My Task" }, async () => {});
}
})`
},
{
code: `client.defineJob({
run: async (payload, io, ctx) => {
await io.stripe.createCharge("charge", {})
}
})`
},
{
code: `client.defineJob({
run: async (payload, io, ctx) => {
await io.supabase.createProject("create-project", {})
}
})`
},
{
code: `client.defineJob({
run: async (payload, io, ctx) => {
await io.typeform.listForms("list-forms");
}
})`
},
],
invalid: [
{
code: `client.defineJob({
run: async (payload, io, ctx) => {
await io.runTask("duplicated-task", { name: "My Task" }, async () => {
return await longRunningCode(payload.userId);
});
await io.runTask("duplicated-task", { name: "My Task" }, async () => {
return await longRunningCode(payload.userId);
});
}
})`,
errors: [{ message: "Task key 'duplicated-task' is duplicated" }]
},
{
code: `client.defineJob({
run: async (payload, io, ctx) => {
await io.stripe.createCharge("duplicated-charge", {
amount: 100,
currency: "usd",
source: payload.source,
customer: payload.customerId,
});
await io.stripe.createCharge("duplicated-charge", {
amount: 100,
currency: "usd",
source: payload.source,
customer: payload.customerId,
});
}
})`,
errors: [{ message: "Task key 'duplicated-charge' is duplicated" }]
},
{
code: `client.defineJob({
run: async (payload, io, ctx) => {
await io.supabase.createProject("create-project", {
name: payload.name,
organization_id: payload.organization_id,
plan: payload.plan,
region: payload.region,
kps_enabled: true,
db_pass: payload.password,
})
await io.supabase.createProject("create-project", {
name: payload.name,
organization_id: payload.organization_id,
plan: payload.plan,
region: payload.region,
kps_enabled: true,
db_pass: payload.password,
})
}
});`,
errors: [{ message: "Task key 'create-project' is duplicated" }]
},
{
code: `client.defineJob({
run: async (payload, io, ctx) => {
await io.typeform.listForms("list-forms");
await io.typeform.listForms("list-forms");
}
})`,
errors: [{ message: "Task key 'list-forms' is duplicated" }]
},
{
code: `client.defineJob({
id: "github-integration-on-issue-opened",
name: "GitHub Integration - On Issue Opened",
version: "0.1.0",
integrations: { github: githubApiKey },
trigger: githubApiKey.triggers.repo({
event: events.onIssueOpened,
owner: "triggerdotdev",
repo: "empty",
}),
run: async (payload, io, ctx) => {
await io.github.addIssueAssignees("add assignee", {
owner: payload.repository.owner.login,
repo: payload.repository.name,
issueNumber: payload.issue.number,
assignees: ["matt-aitken"],
});
await io.github.addIssueAssignees("add assignee", {
owner: payload.repository.owner.login,
repo: payload.repository.name,
issueNumber: payload.issue.number,
assignees: ["matt-aitken"],
});
await io.github.addIssueLabels("add label", {
owner: payload.repository.owner.login,
repo: payload.repository.name,
issueNumber: payload.issue.number,
labels: ["bug"],
});
return { payload, ctx };
},
})`,
errors: [
{ message: "Task key 'add assignee' is duplicated" },
]
},
{
code: `client.defineJob({
id: "react-hook",
name: "React Hook test",
version: "0.0.1",
trigger: eventTrigger({
name: "react-hook",
}),
integrations: {
openai,
},
run: async (_payload, io) => {
await io.wait("Wait 2 seconds", 2);
await io.wait("Wait 1 second", 1);
await io.wait("Wait 1 second", 1);
await io.openai.backgroundCreateChatCompletion("Tell me a joke", {
model: "gpt-3.5-turbo-16k",
messages: [
{
role: "user",
content: 'Tell me a joke please',
},
],
});
const result = await io.openai.backgroundCreateChatCompletion("Tell me a joke", {
model: "gpt-3.5-turbo-16k",
messages: [
{
role: "user",
content: 'Tell me a joke please',
},
],
});
return {
summary: result?.choices[0]?.message?.content,
};
},
});`,
errors: [
{ message: "Task key 'Tell me a joke' is duplicated" },
{ message: "Task key 'Wait 1 second' is duplicated" },
]
},
{
code: `client.defineJob({
id: "github-integration-get-tag",
name: "GitHub Integration - Get Tag",
version: "0.1.0",
integrations: { github },
trigger: githubApiKey.triggers.repo({
event: events.onNewBranchOrTag,
owner: "triggerdotdev",
repo: "empty",
}),
run: async (payload, io, ctx) => {
await io.logger.info("This is a simple log info message");
if (payload.ref_type === "tag") {
const tag = io.github.getTag("Get Tag", {
owner: payload.repository.owner.login,
repo: payload.repository.name,
tagSHA: payload.ref,
});
io.github.getTag("Get Tag", {
owner: payload.repository.owner.login,
repo: payload.repository.name,
tagSHA: payload.ref,
});
await io.logger.info("Tag ", tag);
await io.logger.info("Tag ", tag);
}
return { payload, ctx };
},
});`,
errors: [
{ message: "Task key 'Get Tag' is duplicated" },
{ message: "Task key 'Tag ' is duplicated" },
]
}
]
});
+260 -432
View File
File diff suppressed because it is too large Load Diff