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/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",
|
||||||
|
|||||||
@@ -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