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:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/eslint-plugin": patch
|
||||
---
|
||||
|
||||
An eslint plugin that ensures uniqueness on task keys
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"@trigger.dev/sdk": "workspace:*",
|
||||
"@trigger.dev/slack": "workspace:*",
|
||||
"@trigger.dev/typeform": "workspace:*",
|
||||
"@trigger.dev/eslint-plugin": "workspace:*",
|
||||
"@types/node": "18.15.13",
|
||||
"@types/react": "18.2.17",
|
||||
"@types/react-dom": "18.2.7",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
@@ -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.
|
||||
@@ -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
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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" },
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
Generated
+260
-432
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user