diff --git a/.github/workflows/publish-package.yaml b/.github/workflows/publish-package.yaml new file mode 100644 index 0000000..5586d92 --- /dev/null +++ b/.github/workflows/publish-package.yaml @@ -0,0 +1,24 @@ +name: Publish Package to npm +on: + push: + branches: + - master +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '14.x' + registry-url: 'https://registry.npmjs.org/' + cache: 'npm' + - run: npm ci + - run: npm test + - name: npm publish + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/pull-request-checks.yaml b/.github/workflows/pull-request-checks.yaml new file mode 100644 index 0000000..e48fbb2 --- /dev/null +++ b/.github/workflows/pull-request-checks.yaml @@ -0,0 +1,22 @@ +name: Pull Request Checks + +on: + pull_request: + branches: [ master ] + +jobs: + test: + name: Run Tests + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js 14.x + uses: actions/setup-node@v4 + with: + node-version: 14.x + cache: 'npm' + - run: npm ci + - run: npm run build --if-present + - run: npm test diff --git a/README.md b/README.md index 77255ce..2229f6e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ We will gladly accept issues and PRs in this repo. In the repo where you manage Pipedream components, run: ```bash -npm install eslint-plugin-pipedream --save-dev +npm install @pipedream/eslint-plugin-pipedream --save-dev ``` Then add the Pipedream plugin to the `plugins` section of your [ESLint config file](https://eslint.org/docs/user-guide/configuring/): diff --git a/index.js b/index.js index 6d06b03..86c685b 100644 --- a/index.js +++ b/index.js @@ -6,10 +6,7 @@ function isModuleExports(node) { } function isDefaultExport(node) { - if (!node) return false; - if (node.type !== "Program" || !node.body || !node.body.length) return false; - if (node?.body[0]?.type !== "ExportDefaultDeclaration") return false; - return true; + return node?.type === "ExportDefaultDeclaration"; } function isObjectWithProperties(node) { @@ -23,6 +20,37 @@ function isObjectWithProperties(node) { return true; } +// Check if a prop type is exempt from label/description requirements +function isExemptFromLabelDescriptionRequirement(propertyName, properties) { + if (propertyName !== "label" && propertyName !== "description") return false; + const typePropValue = findPropertyWithName("type", properties)?.value; + if (!typePropValue?.value) return false; + // Default interface props don't need labels and descriptions + const isDefaultInterface = typePropValue.value === "$.interface.timer" || + typePropValue.value === "$.interface.http"; + // Dir props don't need labels and descriptions + const isDirProp = typePropValue.value === "dir"; + return isDefaultInterface || isDirProp; +} + +function getComponentFromNode(node) { + if (isDefaultExport(node) && isObjectWithProperties(node.declaration)) { + return node.declaration; + } + + if (node.expression) { + const { + left, + right, + } = node.expression; + if (isModuleExports(left) && isObjectWithProperties(right)) { + return right; + } + } + + return null; +} + // Objects can contain key names surrounded by quotes, or not // propertyArray is the array of Property nodes in the component object function astIncludesProperty(name, propertyArray) { @@ -47,20 +75,7 @@ function findPropertyWithName(name, propertyArray) { // Does a component contain the right property? e.g. key, version function componentContainsPropertyCheck(context, node, propertyName, message) { - let component; - if (isDefaultExport(node)) { - component = node?.body[0]?.declaration; - } - - if (node.expression) { - const { - left, - right, - } = node.expression; - if (isModuleExports(left) && isObjectWithProperties(right)) { - component = right; - } - } + const component = getComponentFromNode(node); if (!component) return; if (!astIncludesProperty(propertyName, component.properties)) { @@ -83,20 +98,7 @@ function getProps(moduleProperties) { // Do component props contain the right properties? e.g. label, description function componentPropsContainsPropertyCheck(context, node, propertyName) { - let component; - if (isDefaultExport(node)) { - component = node?.body[0]?.declaration; - } - - if (node.expression) { - const { - left, - right, - } = node.expression; - if (isModuleExports(left) && isObjectWithProperties(right)) { - component = right; - } - } + const component = getComponentFromNode(node); if (!component) return; @@ -113,6 +115,7 @@ function componentPropsContainsPropertyCheck(context, node, propertyName) { // We don't want to lint app props or props that are defined in propDefinitions if (!isObjectWithProperties(propDef)) continue; if (astIncludesProperty("propDefinition", propDef.properties)) continue; + if (isExemptFromLabelDescriptionRequirement(propertyName, propDef.properties)) continue; if (!astIncludesProperty(propertyName, propDef.properties)) { context.report({ node: prop, @@ -123,20 +126,7 @@ function componentPropsContainsPropertyCheck(context, node, propertyName) { } function optionalComponentPropsHaveDefaultProperty(context, node) { - let component; - if (isDefaultExport(node)) { - component = node?.body[0]?.declaration; - } - - if (node.expression) { - const { - left, - right, - } = node.expression; - if (isModuleExports(left) && isObjectWithProperties(right)) { - component = right; - } - } + const component = getComponentFromNode(node); if (!component) return; const { properties } = component; @@ -170,20 +160,7 @@ function optionalComponentPropsHaveDefaultProperty(context, node) { // Checks to confirm the component is a source, and returns // the node with the name specified by the user function checkComponentIsSourceAndReturnTargetProp(node, propertyName) { - let component; - if (isDefaultExport(node)) { - component = node?.body[0]?.declaration; - } - - if (node.expression) { - const { - left, - right, - } = node.expression; - if (isModuleExports(left) && isObjectWithProperties(right)) { - component = right; - } - } + const component = getComponentFromNode(node); if (!component) return; const { properties } = component; @@ -219,20 +196,7 @@ function componentSourceDescriptionCheck(context, node) { } function componentVersionTsMacroCheck(context, node) { - let component; - if (isDefaultExport(node)) { - component = node?.body[0]?.declaration; - } - - if (node.expression) { - const { - left, - right, - } = node.expression; - if (isModuleExports(left) && isObjectWithProperties(right)) { - component = right; - } - } + const component = getComponentFromNode(node); if (!component) return; const { properties } = component; @@ -247,7 +211,54 @@ function componentVersionTsMacroCheck(context, node) { } } -// Rules run on two different AST node types: ExpressionStatement (CJS) and Program (ESM) +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", + ]; + + for (const requiredKey of requiredKeys) { + if (!astIncludesProperty(requiredKey, annotationsProp.value.properties)) { + 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 = { rules: { "required-properties-key": { @@ -256,7 +267,7 @@ module.exports = { ExpressionStatement(node) { componentContainsPropertyCheck(context, node, "key"); }, - Program(node) { + ExportDefaultDeclaration(node) { componentContainsPropertyCheck(context, node, "key"); }, }; @@ -268,7 +279,7 @@ module.exports = { ExpressionStatement(node) { componentContainsPropertyCheck(context, node, "name"); }, - Program(node) { + ExportDefaultDeclaration(node) { componentContainsPropertyCheck(context, node, "name"); }, }; @@ -280,7 +291,7 @@ module.exports = { ExpressionStatement(node) { componentContainsPropertyCheck(context, node, "version"); }, - Program(node) { + ExportDefaultDeclaration(node) { componentContainsPropertyCheck(context, node, "version"); }, }; @@ -292,7 +303,7 @@ module.exports = { ExpressionStatement(node) { componentContainsPropertyCheck(context, node, "description"); }, - Program(node) { + ExportDefaultDeclaration(node) { componentContainsPropertyCheck(context, node, "description"); }, }; @@ -304,7 +315,7 @@ module.exports = { ExpressionStatement(node) { componentContainsPropertyCheck(context, node, "type", "Components must export a type property (\"source\" or \"action\")"); }, - Program(node) { + ExportDefaultDeclaration(node) { componentContainsPropertyCheck(context, node, "type", "Components must export a type property (\"source\" or \"action\")"); }, }; @@ -316,7 +327,7 @@ module.exports = { ExpressionStatement(node) { componentPropsContainsPropertyCheck(context, node, "label"); }, - Program(node) { + ExportDefaultDeclaration(node) { componentPropsContainsPropertyCheck(context, node, "label"); }, }; @@ -328,7 +339,7 @@ module.exports = { ExpressionStatement(node) { componentPropsContainsPropertyCheck(context, node, "description"); }, - Program(node) { + ExportDefaultDeclaration(node) { componentPropsContainsPropertyCheck(context, node, "description"); }, }; @@ -340,7 +351,7 @@ module.exports = { ExpressionStatement(node) { optionalComponentPropsHaveDefaultProperty(context, node); }, - Program(node) { + ExportDefaultDeclaration(node) { optionalComponentPropsHaveDefaultProperty(context, node); }, }; @@ -352,7 +363,7 @@ module.exports = { ExpressionStatement(node) { componentSourceNameCheck(context, node); }, - Program(node) { + ExportDefaultDeclaration(node) { componentSourceNameCheck(context, node); }, }; @@ -364,7 +375,7 @@ module.exports = { ExpressionStatement(node) { componentSourceDescriptionCheck(context, node); }, - Program(node) { + ExportDefaultDeclaration(node) { componentSourceDescriptionCheck(context, node); }, }; @@ -376,11 +387,23 @@ module.exports = { ExpressionStatement(node) { componentVersionTsMacroCheck(context, node); }, - Program(node) { + ExportDefaultDeclaration(node) { componentVersionTsMacroCheck(context, node); }, }; }, }, + "action-annotations": { + create: function (context) { + return { + ExpressionStatement(node) { + componentActionAnnotationsCheck(context, node); + }, + ExportDefaultDeclaration(node) { + componentActionAnnotationsCheck(context, node); + }, + }; + }, + }, }, }; diff --git a/package-lock.json b/package-lock.json index 7919cf7..43f005d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { - "name": "eslint-plugin-pipedream", - "version": "0.1.0", + "name": "@pipedream/eslint-plugin-pipedream", + "version": "0.2.5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 04d3cb4..094063d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "eslint-plugin-pipedream", - "version": "0.2.1", + "name": "@pipedream/eslint-plugin-pipedream", + "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 36ff57e..b9b572f 100644 --- a/tests/components.js +++ b/tests/components.js @@ -71,6 +71,42 @@ module.exports = { }, }, }, + missingPropsLabelTimer: { + key: "test", + name: "Test", + description: "hello", + version: "0.0.1", + props: { + test: { + type: "$.interface.timer", + description: "test", + }, + }, + }, + missingPropsLabelHttp: { + key: "test", + name: "Test", + description: "hello", + version: "0.0.1", + props: { + test: { + type: "$.interface.http", + description: "test", + }, + }, + }, + missingPropsLabelDir: { + key: "test", + name: "Test", + description: "hello", + version: "0.0.1", + props: { + test: { + type: "dir", + description: "test", + }, + }, + }, missingPropsDescription: { key: "test", name: "Test", @@ -83,6 +119,42 @@ module.exports = { }, }, }, + missingPropsDescriptionTimer: { + key: "test", + name: "Test", + description: "hello", + version: "0.0.1", + props: { + test: { + type: "$.interface.timer", + label: "Test", + }, + }, + }, + missingPropsDescriptionHttp: { + key: "test", + name: "Test", + description: "hello", + version: "0.0.1", + props: { + test: { + type: "$.interface.http", + label: "Test", + }, + }, + }, + missingPropsDescriptionDir: { + key: "test", + name: "Test", + description: "hello", + version: "0.0.1", + props: { + test: { + type: "dir", + label: "Test", + }, + }, + }, badSourceName: { key: "test", name: "Test", @@ -102,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 230e06e..ab7d139 100644 --- a/tests/rules.test.js +++ b/tests/rules.test.js @@ -9,10 +9,20 @@ const { requiredPropertyTypeMissing, optionalPropWithoutDefaultValue, missingPropsLabel, + missingPropsLabelTimer, + missingPropsLabelHttp, + missingPropsLabelDir, missingPropsDescription, + missingPropsDescriptionTimer, + missingPropsDescriptionHttp, + missingPropsDescriptionDir, badSourceName, badSourceDescription, tsVersion, + validActionWithAnnotations, + actionMissingAnnotations, + actionMissingAnnotationKey, + actionInvalidAnnotations, } = require("./components"); const ruleTester = new RuleTester({ @@ -31,321 +41,195 @@ function convertObjectToESMExportString(obj) { return `export default ${JSON.stringify(obj)}`; } -ruleTester.run("required-properties-key-test", rules["required-properties-key"], { - valid: [ - { - code: convertObjectToCJSExportString(valid), - }, - { - code: convertObjectToESMExportString(valid), - }, - ], - invalid: [ - { - code: convertObjectToCJSExportString(requiredPropertyKeyMissing), - errors: [ - { - message: "Components must export a key property. See https://pipedream.com/docs/components/guidelines/#required-metadata", - }, - ], - }, - { - code: convertObjectToESMExportString(requiredPropertyKeyMissing), - errors: [ - { - message: "Components must export a key property. See https://pipedream.com/docs/components/guidelines/#required-metadata", - }, - ], - }, - ], -}); - -ruleTester.run("required-properties-name-test", rules["required-properties-name"], { - valid: [ - { - code: convertObjectToCJSExportString(valid), - }, - { - code: convertObjectToESMExportString(valid), - }, - ], - invalid: [ - { - code: convertObjectToCJSExportString(requiredPropertyNameMissing), - errors: [ - { - message: "Components must export a name property. See https://pipedream.com/docs/components/guidelines/#required-metadata", - }, - ], - }, - { - code: convertObjectToESMExportString(requiredPropertyNameMissing), - errors: [ - { - message: "Components must export a name property. See https://pipedream.com/docs/components/guidelines/#required-metadata", - }, - ], - }, - ], -}); - -ruleTester.run("required-properties-description-test", rules["required-properties-description"], { - valid: [ - { - code: convertObjectToCJSExportString(valid), - }, - { - code: convertObjectToESMExportString(valid), - }, - ], - invalid: [ - { - code: convertObjectToCJSExportString(requiredPropertyDescriptionMissing), - errors: [ - { - message: "Components must export a description property. See https://pipedream.com/docs/components/guidelines/#required-metadata", - }, - ], - }, - { - code: convertObjectToESMExportString(requiredPropertyDescriptionMissing), - errors: [ - { - message: "Components must export a description property. See https://pipedream.com/docs/components/guidelines/#required-metadata", - }, - ], - }, - ], -}); - -ruleTester.run("required-properties-version-test", rules["required-properties-version"], { - valid: [ - { - code: convertObjectToCJSExportString(valid), - }, - { - code: convertObjectToESMExportString(valid), - }, - ], - invalid: [ - { - code: convertObjectToCJSExportString(requiredPropertyVersionMissing), - errors: [ - { - message: "Components must export a version property. See https://pipedream.com/docs/components/guidelines/#required-metadata", - }, - ], - }, - { - code: convertObjectToESMExportString(requiredPropertyVersionMissing), - errors: [ - { - message: "Components must export a version property. See https://pipedream.com/docs/components/guidelines/#required-metadata", - }, - ], - }, - ], -}); - -ruleTester.run("required-properties-type-test", rules["required-properties-type"], { - valid: [ - { - code: convertObjectToCJSExportString(valid), - }, - { - code: convertObjectToESMExportString(valid), - }, - ], - invalid: [ - { - code: convertObjectToCJSExportString(requiredPropertyTypeMissing), - errors: [ - { - message: "Components must export a type property (\"source\" or \"action\")", - }, - ], - }, - { - code: convertObjectToESMExportString(requiredPropertyTypeMissing), - errors: [ - { - message: "Components must export a type property (\"source\" or \"action\")", - }, - ], - }, - ], -}); - -ruleTester.run("default-value-required-for-optional-props-test", rules["default-value-required-for-optional-props"], { - valid: [ - { - code: convertObjectToCJSExportString(valid), - }, - { - code: convertObjectToESMExportString(valid), - }, - ], - invalid: [ - { - code: convertObjectToCJSExportString(optionalPropWithoutDefaultValue), - errors: [ - { - message: "Component prop test is marked \"optional\", so it may need a \"default\" property. See https://pipedream.com/docs/components/guidelines/#default-values", - }, - ], - }, - { - code: convertObjectToESMExportString(optionalPropWithoutDefaultValue), - errors: [ - { - message: "Component prop test is marked \"optional\", so it may need a \"default\" property. See https://pipedream.com/docs/components/guidelines/#default-values", - }, - ], - }, - ], -}); +function withPrecedingStatement(code) { + return ` + import foo from "bar"; + ${code} + `; +} -ruleTester.run("props-label-test", rules["props-label"], { - valid: [ - { - code: convertObjectToCJSExportString(valid), - }, - { - code: convertObjectToESMExportString(valid), - }, - ], - invalid: [ - { - code: convertObjectToCJSExportString(missingPropsLabel), - errors: [ - { - message: "Component prop test must have a label. See https://pipedream.com/docs/components/guidelines/#props", - }, - ], - }, - { - code: convertObjectToESMExportString(missingPropsLabel), - errors: [ - { - message: "Component prop test must have a label. See https://pipedream.com/docs/components/guidelines/#props", - }, - ], - }, - ], -}); +function makeComponentTestCase ({ + ruleName, + name = `${ruleName}-test`, + validComponents = [ + valid, + ], + invalidComponent, + errorMessage, +}) { + return { + name, + ruleName, + validComponents, + invalidComponent, + errorMessage, + }; +} -ruleTester.run("props-description-test", rules["props-description"], { - valid: [ - { - code: convertObjectToCJSExportString(valid), - }, - { - code: convertObjectToESMExportString(valid), - }, - ], - invalid: [ - { - code: convertObjectToCJSExportString(missingPropsDescription), - errors: [ - { - message: "Component prop test must have a description. See https://pipedream.com/docs/components/guidelines/#props", - }, - ], - }, - { - code: convertObjectToESMExportString(missingPropsDescription), - errors: [ - { - message: "Component prop test must have a description. See https://pipedream.com/docs/components/guidelines/#props", - }, - ], - }, - ], -}); +const componentTestConfigs = [ + { + ruleName: "required-properties-key", + invalidComponent: requiredPropertyKeyMissing, + errorMessage: "Components must export a key property. See https://pipedream.com/docs/components/guidelines/#required-metadata", + }, + { + ruleName: "required-properties-name", + invalidComponent: requiredPropertyNameMissing, + errorMessage: "Components must export a name property. See https://pipedream.com/docs/components/guidelines/#required-metadata", + }, + { + ruleName: "required-properties-description", + invalidComponent: requiredPropertyDescriptionMissing, + errorMessage: "Components must export a description property. See https://pipedream.com/docs/components/guidelines/#required-metadata", + }, + { + ruleName: "required-properties-version", + invalidComponent: requiredPropertyVersionMissing, + errorMessage: "Components must export a version property. See https://pipedream.com/docs/components/guidelines/#required-metadata", + }, + { + ruleName: "required-properties-type", + invalidComponent: requiredPropertyTypeMissing, + errorMessage: "Components must export a type property (\"source\" or \"action\")", + }, + { + ruleName: "default-value-required-for-optional-props", + invalidComponent: optionalPropWithoutDefaultValue, + errorMessage: "Component prop test is marked \"optional\", so it may need a \"default\" property. See https://pipedream.com/docs/components/guidelines/#default-values", + }, + { + ruleName: "props-label", + validComponents: [ + missingPropsLabelTimer, + missingPropsLabelHttp, + missingPropsLabelDir, + ], + invalidComponent: missingPropsLabel, + errorMessage: "Component prop test must have a label. See https://pipedream.com/docs/components/guidelines/#props", + }, + { + ruleName: "props-description", + validComponents: [ + missingPropsDescriptionTimer, + missingPropsDescriptionHttp, + missingPropsDescriptionDir, + ], + invalidComponent: missingPropsDescription, + errorMessage: "Component prop test must have a description. See https://pipedream.com/docs/components/guidelines/#props", + }, + { + ruleName: "source-name", + invalidComponent: badSourceName, + errorMessage: "Source names should start with \"New\". See https://pipedream.com/docs/components/guidelines/#source-name", + }, + { + ruleName: "source-description", + invalidComponent: badSourceDescription, + errorMessage: "Source descriptions should start with \"Emit new\". See https://pipedream.com/docs/components/guidelines/#source-description", + }, + { + name: "ts-version-test", + ruleName: "no-ts-version", + 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", + }, +]; -ruleTester.run("source-name-test", rules["source-name"], { - valid: [ - { - code: convertObjectToCJSExportString(valid), - }, - { - code: convertObjectToESMExportString(valid), - }, - ], - invalid: [ - { - code: convertObjectToCJSExportString(badSourceName), - errors: [ - { - message: "Source names should start with \"New\". See https://pipedream.com/docs/components/guidelines/#source-name", - }, - ], - }, - { - code: convertObjectToESMExportString(badSourceName), - errors: [ - { - message: "Source names should start with \"New\". See https://pipedream.com/docs/components/guidelines/#source-name", - }, - ], - }, - ], -}); +const componentTestCases = componentTestConfigs.map(makeComponentTestCase); -ruleTester.run("source-description-test", rules["source-description"], { - valid: [ - { - code: convertObjectToCJSExportString(valid), - }, - { - code: convertObjectToESMExportString(valid), - }, - ], - invalid: [ - { - code: convertObjectToCJSExportString(badSourceDescription), - errors: [ - { - message: "Source descriptions should start with \"Emit new\". See https://pipedream.com/docs/components/guidelines/#source-description", - }, - ], - }, - { - code: convertObjectToESMExportString(badSourceDescription), - errors: [ - { - message: "Source descriptions should start with \"Emit new\". See https://pipedream.com/docs/components/guidelines/#source-description", - }, - ], - }, - ], +// Run `ruleTester.run` on each test case +componentTestCases.forEach((testCase) => { + const { + name, + ruleName, + validComponents, + invalidComponent, + errorMessage, + } = testCase; + ruleTester.run(name, rules[ruleName], { + valid: validComponents.map((component) => ([ + { + code: convertObjectToCJSExportString(component), + }, + { + code: convertObjectToESMExportString(component), + }, + ])).flat(), + invalid: [ + { + code: convertObjectToCJSExportString(invalidComponent), + errors: [ + { + message: errorMessage, + }, + ], + }, + { + code: convertObjectToESMExportString(invalidComponent), + errors: [ + { + message: errorMessage, + }, + ], + }, + ], + }); }); -ruleTester.run("ts-version-test", rules["no-ts-version"], { - valid: [ - { - code: convertObjectToCJSExportString(valid), - }, - { - code: convertObjectToESMExportString(valid), - }, - ], - invalid: [ - { - code: convertObjectToCJSExportString(tsVersion), - errors: [ - { - message: "{{ts}} macro should be removed before committing", - }, - ], - }, - { - code: convertObjectToESMExportString(tsVersion), - errors: [ - { - message: "{{ts}} macro should be removed before committing", - }, - ], - }, - ], +RuleTester.describe("On ESM export default with preceding statements", () => { + // Run each test case on ESM default export components with preceding statements + // (lines above the `export default` declaration) + componentTestCases.forEach((testCase) => { + const { + name, + ruleName, + validComponent, + invalidComponent, + errorMessage, + } = testCase; + ruleTester.run(name, rules[ruleName], { + valid: [ + { + code: withPrecedingStatement(convertObjectToESMExportString(validComponent)), + }, + ], + invalid: [ + { + code: withPrecedingStatement(convertObjectToESMExportString(invalidComponent)), + errors: [ + { + message: errorMessage, + }, + ], + }, + ], + }); + }); });