From dfd39a3ae06938600d928a68a97b3f54c28bf2b2 Mon Sep 17 00:00:00 2001 From: GTFalcao Date: Tue, 7 Oct 2025 19:11:56 -0300 Subject: [PATCH 1/3] Adding ESLint rule for action component MCP annotations --- index.js | 62 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- tests/components.js | 38 +++++++++++++++++++++++++++ tests/rules.test.js | 31 +++++++++++++++++++++++ 4 files changed, 132 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index ab8428a..cf7849d 100644 --- a/index.js +++ b/index.js @@ -211,6 +211,56 @@ function componentVersionTsMacroCheck(context, node) { } } +function componentActionAnnotationsCheck(context, node) { + const component = getComponentFromNode(node); + + if (!component) return; + const { properties } = component; + + const typeProp = findPropertyWithName("type", properties); + if (typeProp?.value?.value !== "action") return; + + const annotationsProp = findPropertyWithName("annotations", properties); + + // Error 1 - annotations missing entirely + if (!annotationsProp) { + context.report({ + node: component, + message: "Action component is missing required 'annotations' object", + }); + return; + } + + // Error 2 - annotations is not an object expression + if (annotationsProp.value.type !== "ObjectExpression") { + context.report({ + node: annotationsProp.value, + message: "Property 'annotations' must be an object expression", + }); + return; + } + + // Error 3 - required keys missing + const requiredKeys = [ + "destructiveHint", + "openWorldHint", + "readOnlyHint", + ]; + + const annotationKeys = annotationsProp.value.properties.map( + (prop) => prop.key && (prop.key.name || prop.key.value) + ); + + for (const requiredKey of requiredKeys) { + if (!annotationKeys.includes(requiredKey)) { + context.report({ + node: annotationsProp.value, + message: `Property 'annotations' is missing required key: '${requiredKey}'`, + }); + } + } +} + // Rules run on two different AST node types: ExpressionStatement (CJS) and // ExportDefaultDeclaration (ESM) module.exports = { @@ -347,5 +397,17 @@ module.exports = { }; }, }, + "action-annotations": { + create: function (context) { + return { + ExpressionStatement(node) { + componentActionAnnotationsCheck(context, node); + }, + ExportDefaultDeclaration(node) { + componentActionAnnotationsCheck(context, node); + }, + }; + }, + }, }, }; diff --git a/package.json b/package.json index b1eef55..094063d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/eslint-plugin-pipedream", - "version": "0.2.5", + "version": "0.3.0", "description": "ESLint plugin for Pipedream components: https://pipedream.com/docs/components/api/", "main": "index.js", "scripts": { diff --git a/tests/components.js b/tests/components.js index 85870d5..b9b572f 100644 --- a/tests/components.js +++ b/tests/components.js @@ -174,4 +174,42 @@ module.exports = { type: "action", version: "0.0.{{ts}}", }, + validActionWithAnnotations: { + key: "test", + name: "Test", + description: "foo", + type: "action", + version: "0.0.1", + annotations: { + destructiveHint: false, + openWorldHint: false, + readOnlyHint: true, + }, + }, + actionMissingAnnotations: { + key: "test", + name: "Test", + description: "foo", + type: "action", + version: "0.0.1", + }, + actionMissingAnnotationKey: { + key: "test", + name: "Test", + description: "foo", + type: "action", + version: "0.0.1", + annotations: { + destructiveHint: false, + openWorldHint: false, + }, + }, + actionInvalidAnnotations: { + key: "test", + name: "Test", + description: "foo", + type: "action", + version: "0.0.1", + annotations: "invalid", + }, }; diff --git a/tests/rules.test.js b/tests/rules.test.js index fd3ed82..ab7d139 100644 --- a/tests/rules.test.js +++ b/tests/rules.test.js @@ -19,6 +19,10 @@ const { badSourceName, badSourceDescription, tsVersion, + validActionWithAnnotations, + actionMissingAnnotations, + actionMissingAnnotationKey, + actionInvalidAnnotations, } = require("./components"); const ruleTester = new RuleTester({ @@ -129,6 +133,33 @@ const componentTestConfigs = [ invalidComponent: tsVersion, errorMessage: "{{ts}} macro should be removed before committing", }, + { + name: "action-annotations-missing", + ruleName: "action-annotations", + validComponents: [ + validActionWithAnnotations, + ], + invalidComponent: actionMissingAnnotations, + errorMessage: "Action component is missing required 'annotations' object", + }, + { + name: "action-annotations-missing-key", + ruleName: "action-annotations", + validComponents: [ + validActionWithAnnotations, + ], + invalidComponent: actionMissingAnnotationKey, + errorMessage: "Property 'annotations' is missing required key: 'readOnlyHint'", + }, + { + name: "action-annotations-invalid", + ruleName: "action-annotations", + validComponents: [ + validActionWithAnnotations, + ], + invalidComponent: actionInvalidAnnotations, + errorMessage: "Property 'annotations' must be an object expression", + }, ]; const componentTestCases = componentTestConfigs.map(makeComponentTestCase); From a4368ea358dba11ff4d2ea82b4236c683a098f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Falc=C3=A3o?= <48412907+GTFalcao@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:13:01 -0300 Subject: [PATCH 2/3] Update index.js Co-authored-by: js07 <19861096+js07@users.noreply.github.com> --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index cf7849d..b552dd8 100644 --- a/index.js +++ b/index.js @@ -248,7 +248,7 @@ function componentActionAnnotationsCheck(context, node) { ]; const annotationKeys = annotationsProp.value.properties.map( - (prop) => prop.key && (prop.key.name || prop.key.value) + (prop) => prop.key && (prop.key.name || prop.key.value), ); for (const requiredKey of requiredKeys) { From e2d732310f2bc8ea1efc8c9d6dac8db889a2e145 Mon Sep 17 00:00:00 2001 From: GTFalcao Date: Tue, 7 Oct 2025 21:15:34 -0300 Subject: [PATCH 3/3] Optimizing code with astIncludesProperty function --- index.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/index.js b/index.js index cf7849d..86c685b 100644 --- a/index.js +++ b/index.js @@ -247,12 +247,8 @@ function componentActionAnnotationsCheck(context, node) { "readOnlyHint", ]; - const annotationKeys = annotationsProp.value.properties.map( - (prop) => prop.key && (prop.key.name || prop.key.value) - ); - for (const requiredKey of requiredKeys) { - if (!annotationKeys.includes(requiredKey)) { + if (!astIncludesProperty(requiredKey, annotationsProp.value.properties)) { context.report({ node: annotationsProp.value, message: `Property 'annotations' is missing required key: '${requiredKey}'`,