Skip to content

Commit 444f442

Browse files
committed
Pushing 0.1.0.
Supporting ES modules (export default syntax). Adding associated tests.
1 parent 4c02274 commit 444f442

File tree

7 files changed

+306
-69
lines changed

7 files changed

+306
-69
lines changed

.eslintrc

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,14 @@
1515
"*.json"
1616
],
1717
"parser": "jsonc-eslint-parser"
18-
},
19-
],
18+
}
19+
],
2020
"parserOptions": {
21-
"ecmaVersion": 12,
2221
"sourceType": "module"
2322
},
2423
"plugins": [
2524
"jsonc",
26-
"putout",
25+
"putout"
2726
],
2827
"root": true,
2928
"rules": {

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,24 @@ Then add the Pipedream plugin to the `plugins` section of your [ESLint config fi
2626
],
2727
```
2828

29+
## Developing the plugin
30+
31+
When you're contributing to this plugin, first install dependencies:
32+
33+
```bash
34+
npm i
35+
```
36+
37+
You can run tests with:
38+
39+
```bash
40+
npm run test
41+
```
42+
43+
All rules are exported from `index.js`. Tests can be found in the `tests` directory. Each new rule should have associated tests.
44+
45+
If this is your first time creating ESLint rules, see these helpful resouces:
46+
47+
* [ESLint Developer Guide](https://eslint.org/docs/developer-guide/)
48+
* [AST Explorer](https://astexplorer.net/). Useful for examining the AST of any code snippet.
49+
* [How To Write Your First ESLint Plugin](https://dev.to/spukas/how-to-write-your-first-eslint-plugin-145)

index.js

Lines changed: 113 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ function isModuleExports(node) {
55
return true;
66
}
77

8+
function isDefaultExport(node) {
9+
if (!node) return false;
10+
if (node.type !== "Program" || !node.body || !node.body.length) return false;
11+
if (node?.body[0]?.type !== "ExportDefaultDeclaration") return false;
12+
return true;
13+
}
14+
815
function isObjectWithProperties(node) {
916
if (!node) return false;
1017
const {
@@ -20,8 +27,11 @@ function isObjectWithProperties(node) {
2027
// propertyArray is the array of Property nodes in the component object
2128
function astIncludesProperty(name, propertyArray) {
2229
// value for Literals (quotes), name for Identifiers (no quotes)
23-
const propertyNames = propertyArray.map((p) => p?.key?.value ?? p?.key?.name);
24-
return propertyNames.includes(name) || propertyNames.includes(`"${name}"`);
30+
const propertyNames = propertyArray.map((p) => {
31+
return p?.key?.value ?? p?.key?.name;
32+
});
33+
const val = propertyNames.includes(name) || propertyNames.includes(`"${name}"`);
34+
return val;
2535
}
2636

2737
// Returns the Property node matching the given name
@@ -37,14 +47,23 @@ function findPropertyWithName(name, propertyArray) {
3747

3848
// Does a component contain the right property? e.g. key, version
3949
function componentContainsPropertyCheck(context, node, propertyName, message) {
40-
const {
41-
left,
42-
right,
43-
} = node.expression;
44-
if (!isModuleExports(left)) return;
45-
if (!isObjectWithProperties(right)) return;
50+
let component;
51+
if (isDefaultExport(node)) {
52+
component = node?.body[0]?.declaration;
53+
}
4654

47-
if (!astIncludesProperty(propertyName, right.properties)) {
55+
if (node.expression) {
56+
const {
57+
left,
58+
right,
59+
} = node.expression;
60+
if (isModuleExports(left) && isObjectWithProperties(right)) {
61+
component = right;
62+
}
63+
}
64+
65+
if (!component) return;
66+
if (!astIncludesProperty(propertyName, component.properties)) {
4867
context.report({
4968
node: node,
5069
message: message ?? `Components must export a ${propertyName} property. See https://pipedream.com/docs/components/guidelines/#required-metadata`,
@@ -64,14 +83,24 @@ function getProps(moduleProperties) {
6483

6584
// Do component props contain the right properties? e.g. label, description
6685
function componentPropsContainsPropertyCheck(context, node, propertyName) {
67-
const {
68-
left,
69-
right,
70-
} = node.expression;
71-
if (!isModuleExports(left)) return;
72-
if (!isObjectWithProperties(right)) return;
86+
let component;
87+
if (isDefaultExport(node)) {
88+
component = node?.body[0]?.declaration;
89+
}
7390

74-
const { properties } = right;
91+
if (node.expression) {
92+
const {
93+
left,
94+
right,
95+
} = node.expression;
96+
if (isModuleExports(left) && isObjectWithProperties(right)) {
97+
component = right;
98+
}
99+
}
100+
101+
if (!component) return;
102+
103+
const { properties } = component;
75104
if (!(astIncludesProperty("props", properties) || astIncludesProperty("propDefinitions", properties))) return;
76105
const props = getProps(properties);
77106
if (!isObjectWithProperties(props?.value)) return;
@@ -94,14 +123,23 @@ function componentPropsContainsPropertyCheck(context, node, propertyName) {
94123
}
95124

96125
function optionalComponentPropsHaveDefaultProperty(context, node) {
97-
const {
98-
left,
99-
right,
100-
} = node.expression;
101-
if (!isModuleExports(left)) return;
102-
if (!isObjectWithProperties(right)) return;
126+
let component;
127+
if (isDefaultExport(node)) {
128+
component = node?.body[0]?.declaration;
129+
}
130+
131+
if (node.expression) {
132+
const {
133+
left,
134+
right,
135+
} = node.expression;
136+
if (isModuleExports(left) && isObjectWithProperties(right)) {
137+
component = right;
138+
}
139+
}
103140

104-
const { properties } = right;
141+
if (!component) return;
142+
const { properties } = component;
105143
if (!(astIncludesProperty("props", properties) || astIncludesProperty("propDefinitions", properties))) return;
106144
const props = getProps(properties);
107145
if (!isObjectWithProperties(props?.value)) return;
@@ -113,8 +151,8 @@ function optionalComponentPropsHaveDefaultProperty(context, node) {
113151

114152
// We don't want to lint app props or props that are defined in propDefinitions
115153
if (!isObjectWithProperties(propDef)) continue;
116-
if (!isObjectWithProperties(right)) continue;
117-
if (astIncludesProperty("propDefinition", right.properties)) continue;
154+
if (!isObjectWithProperties(component)) continue;
155+
if (astIncludesProperty("propDefinition", component.properties)) continue;
118156

119157
// value for Literals (quotes), name for Identifiers (no quotes)
120158
const optionalProp = findPropertyWithName("optional", propDef.properties);
@@ -132,20 +170,30 @@ function optionalComponentPropsHaveDefaultProperty(context, node) {
132170
// Checks to confirm the component is a source, and returns
133171
// the node with the name specified by the user
134172
function checkComponentIsSourceAndReturnTargetProp(node, propertyName) {
135-
const {
136-
left,
137-
right,
138-
} = node.expression;
173+
let component;
174+
if (isDefaultExport(node)) {
175+
component = node?.body[0]?.declaration;
176+
}
139177

140-
if (!isModuleExports(left)) return;
141-
if (!isObjectWithProperties(right)) return;
178+
if (node.expression) {
179+
const {
180+
left,
181+
right,
182+
} = node.expression;
183+
if (isModuleExports(left) && isObjectWithProperties(right)) {
184+
component = right;
185+
}
186+
}
142187

143-
const typeProp = findPropertyWithName("type", right.properties);
188+
if (!component) return;
189+
const { properties } = component;
190+
191+
const typeProp = findPropertyWithName("type", properties);
144192
// A separate rule checks the presence of the type property
145193
if (!typeProp) return;
146194
if (typeProp?.value?.value !== "source") return;
147195

148-
return findPropertyWithName(propertyName, right.properties);
196+
return findPropertyWithName(propertyName, properties);
149197
}
150198

151199
function componentSourceNameCheck(context, node) {
@@ -161,7 +209,7 @@ function componentSourceNameCheck(context, node) {
161209

162210
function componentSourceDescriptionCheck(context, node) {
163211
const nameProp = checkComponentIsSourceAndReturnTargetProp(node, "description");
164-
if (!nameProp || typeof nameProp?.value?.value !== 'string') return;
212+
if (!nameProp || typeof nameProp?.value?.value !== "string") return;
165213
if (!nameProp.value.value.startsWith("Emit new ")) {
166214
context.report({
167215
node: nameProp,
@@ -170,6 +218,7 @@ function componentSourceDescriptionCheck(context, node) {
170218
}
171219
}
172220

221+
// Rules run on two different AST node types: ExpressionStatement (CJS) and Program (ESM)
173222
module.exports = {
174223
rules: {
175224
"required-properties-key": {
@@ -178,6 +227,9 @@ module.exports = {
178227
ExpressionStatement(node) {
179228
componentContainsPropertyCheck(context, node, "key");
180229
},
230+
Program(node) {
231+
componentContainsPropertyCheck(context, node, "key");
232+
},
181233
};
182234
},
183235
},
@@ -187,6 +239,9 @@ module.exports = {
187239
ExpressionStatement(node) {
188240
componentContainsPropertyCheck(context, node, "name");
189241
},
242+
Program(node) {
243+
componentContainsPropertyCheck(context, node, "name");
244+
},
190245
};
191246
},
192247
},
@@ -196,6 +251,9 @@ module.exports = {
196251
ExpressionStatement(node) {
197252
componentContainsPropertyCheck(context, node, "version");
198253
},
254+
Program(node) {
255+
componentContainsPropertyCheck(context, node, "version");
256+
},
199257
};
200258
},
201259
},
@@ -205,6 +263,9 @@ module.exports = {
205263
ExpressionStatement(node) {
206264
componentContainsPropertyCheck(context, node, "description");
207265
},
266+
Program(node) {
267+
componentContainsPropertyCheck(context, node, "description");
268+
},
208269
};
209270
},
210271
},
@@ -214,6 +275,9 @@ module.exports = {
214275
ExpressionStatement(node) {
215276
componentContainsPropertyCheck(context, node, "type", "Components must export a type property (\"source\" or \"action\")");
216277
},
278+
Program(node) {
279+
componentContainsPropertyCheck(context, node, "type", "Components must export a type property (\"source\" or \"action\")");
280+
},
217281
};
218282
},
219283
},
@@ -223,6 +287,9 @@ module.exports = {
223287
ExpressionStatement(node) {
224288
componentPropsContainsPropertyCheck(context, node, "label");
225289
},
290+
Program(node) {
291+
componentPropsContainsPropertyCheck(context, node, "label");
292+
},
226293
};
227294
},
228295
},
@@ -232,6 +299,9 @@ module.exports = {
232299
ExpressionStatement(node) {
233300
componentPropsContainsPropertyCheck(context, node, "description");
234301
},
302+
Program(node) {
303+
componentPropsContainsPropertyCheck(context, node, "description");
304+
},
235305
};
236306
},
237307
},
@@ -241,6 +311,9 @@ module.exports = {
241311
ExpressionStatement(node) {
242312
optionalComponentPropsHaveDefaultProperty(context, node);
243313
},
314+
Program(node) {
315+
optionalComponentPropsHaveDefaultProperty(context, node);
316+
},
244317
};
245318
},
246319
},
@@ -250,6 +323,9 @@ module.exports = {
250323
ExpressionStatement(node) {
251324
componentSourceNameCheck(context, node);
252325
},
326+
Program(node) {
327+
componentSourceNameCheck(context, node);
328+
},
253329
};
254330
},
255331
},
@@ -259,6 +335,9 @@ module.exports = {
259335
ExpressionStatement(node) {
260336
componentSourceDescriptionCheck(context, node);
261337
},
338+
Program(node) {
339+
componentSourceDescriptionCheck(context, node);
340+
},
262341
};
263342
},
264343
},

package-lock.json

Lines changed: 25 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"name": "eslint-plugin-pipedream",
3-
"version": "0.0.7",
3+
"version": "0.1.0",
44
"description": "ESLint plugin for Pipedream components: https://pipedream.com/docs/components/api/",
55
"main": "index.js",
66
"scripts": {
7-
"test": "echo \"Error: no test specified\" && exit 1"
7+
"test": "jest"
88
},
99
"keywords": [
1010
"pipedream",

tests/esm-components.js

Whitespace-only changes.

0 commit comments

Comments
 (0)