diff --git a/index.js b/index.js index 6d06b03..73f104a 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) { @@ -49,7 +46,7 @@ function findPropertyWithName(name, propertyArray) { function componentContainsPropertyCheck(context, node, propertyName, message) { let component; if (isDefaultExport(node)) { - component = node?.body[0]?.declaration; + component = node.declaration; } if (node.expression) { @@ -85,9 +82,8 @@ function getProps(moduleProperties) { function componentPropsContainsPropertyCheck(context, node, propertyName) { let component; if (isDefaultExport(node)) { - component = node?.body[0]?.declaration; + component = node.declaration; } - if (node.expression) { const { left, @@ -125,7 +121,7 @@ function componentPropsContainsPropertyCheck(context, node, propertyName) { function optionalComponentPropsHaveDefaultProperty(context, node) { let component; if (isDefaultExport(node)) { - component = node?.body[0]?.declaration; + component = node.declaration; } if (node.expression) { @@ -172,7 +168,7 @@ function optionalComponentPropsHaveDefaultProperty(context, node) { function checkComponentIsSourceAndReturnTargetProp(node, propertyName) { let component; if (isDefaultExport(node)) { - component = node?.body[0]?.declaration; + component = node.declaration; } if (node.expression) { @@ -221,7 +217,7 @@ function componentSourceDescriptionCheck(context, node) { function componentVersionTsMacroCheck(context, node) { let component; if (isDefaultExport(node)) { - component = node?.body[0]?.declaration; + component = node.declaration; } if (node.expression) { @@ -247,7 +243,8 @@ function componentVersionTsMacroCheck(context, node) { } } -// Rules run on two different AST node types: ExpressionStatement (CJS) and Program (ESM) +// Rules run on two different AST node types: ExpressionStatement (CJS) and +// ExportDefaultDeclaration (ESM) module.exports = { rules: { "required-properties-key": { @@ -256,7 +253,7 @@ module.exports = { ExpressionStatement(node) { componentContainsPropertyCheck(context, node, "key"); }, - Program(node) { + ExportDefaultDeclaration(node) { componentContainsPropertyCheck(context, node, "key"); }, }; @@ -268,7 +265,7 @@ module.exports = { ExpressionStatement(node) { componentContainsPropertyCheck(context, node, "name"); }, - Program(node) { + ExportDefaultDeclaration(node) { componentContainsPropertyCheck(context, node, "name"); }, }; @@ -280,7 +277,7 @@ module.exports = { ExpressionStatement(node) { componentContainsPropertyCheck(context, node, "version"); }, - Program(node) { + ExportDefaultDeclaration(node) { componentContainsPropertyCheck(context, node, "version"); }, }; @@ -292,7 +289,7 @@ module.exports = { ExpressionStatement(node) { componentContainsPropertyCheck(context, node, "description"); }, - Program(node) { + ExportDefaultDeclaration(node) { componentContainsPropertyCheck(context, node, "description"); }, }; @@ -304,7 +301,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 +313,7 @@ module.exports = { ExpressionStatement(node) { componentPropsContainsPropertyCheck(context, node, "label"); }, - Program(node) { + ExportDefaultDeclaration(node) { componentPropsContainsPropertyCheck(context, node, "label"); }, }; @@ -328,7 +325,7 @@ module.exports = { ExpressionStatement(node) { componentPropsContainsPropertyCheck(context, node, "description"); }, - Program(node) { + ExportDefaultDeclaration(node) { componentPropsContainsPropertyCheck(context, node, "description"); }, }; @@ -340,7 +337,7 @@ module.exports = { ExpressionStatement(node) { optionalComponentPropsHaveDefaultProperty(context, node); }, - Program(node) { + ExportDefaultDeclaration(node) { optionalComponentPropsHaveDefaultProperty(context, node); }, }; @@ -352,7 +349,7 @@ module.exports = { ExpressionStatement(node) { componentSourceNameCheck(context, node); }, - Program(node) { + ExportDefaultDeclaration(node) { componentSourceNameCheck(context, node); }, }; @@ -364,7 +361,7 @@ module.exports = { ExpressionStatement(node) { componentSourceDescriptionCheck(context, node); }, - Program(node) { + ExportDefaultDeclaration(node) { componentSourceDescriptionCheck(context, node); }, }; @@ -376,7 +373,7 @@ module.exports = { ExpressionStatement(node) { componentVersionTsMacroCheck(context, node); }, - Program(node) { + ExportDefaultDeclaration(node) { componentVersionTsMacroCheck(context, node); }, }; diff --git a/package-lock.json b/package-lock.json index 7919cf7..e2fa358 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-pipedream", - "version": "0.1.0", + "version": "0.2.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 04d3cb4..20224c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-pipedream", - "version": "0.2.1", + "version": "0.2.2", "description": "ESLint plugin for Pipedream components: https://pipedream.com/docs/components/api/", "main": "index.js", "scripts": { diff --git a/tests/rules.test.js b/tests/rules.test.js index 230e06e..008b68c 100644 --- a/tests/rules.test.js +++ b/tests/rules.test.js @@ -31,321 +31,157 @@ 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\")", - }, - ], - }, - ], -}); +function withPrecedingStatement(code) { + return ` + import foo from "bar"; + ${code} + `; +} -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 makeComponentTestCase ({ + ruleName, + name = `${ruleName}-test`, + validComponent = valid, + invalidComponent, + errorMessage, +}) { + return { + name, + ruleName, + validComponent, + invalidComponent, + errorMessage, + }; +} -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", - }, - ], - }, - ], -}); +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", + invalidComponent: missingPropsLabel, + errorMessage: "Component prop test must have a label. See https://pipedream.com/docs/components/guidelines/#props", + }, + { + ruleName: "props-description", + 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", + }, +]; -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 componentTestCases = componentTestConfigs.map(makeComponentTestCase); -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", - }, - ], - }, - ], +// Run `ruleTester.run` on each test case +componentTestCases.forEach((testCase) => { + const { + name, + ruleName, + validComponent, + invalidComponent, + errorMessage, + } = testCase; + ruleTester.run(name, rules[ruleName], { + valid: [ + { + code: convertObjectToCJSExportString(validComponent), + }, + { + code: convertObjectToESMExportString(validComponent), + }, + ], + invalid: [ + { + code: convertObjectToCJSExportString(invalidComponent), + errors: [ + { + message: errorMessage, + }, + ], + }, + { + code: convertObjectToESMExportString(invalidComponent), + errors: [ + { + message: errorMessage, + }, + ], + }, + ], + }); }); -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", - }, - ], - }, - ], +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, + }, + ], + }, + ], + }); + }); }); -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", - }, - ], - }, - ], -});