Skip to content

Commit aa15471

Browse files
authored
feat: support named exports in ESM/TS (#449)
* feat: support named exports in ESM/TS This adds support for named exports in ESM (whether it be TS or not). For example: ```ts export const rule: RuleModule = { create: () => { ... }; }; ``` Also, exported symbols: ```ts const rule = { ... }; export {rule}; ``` While ESLint plugins are still usually CJS at time of writing this, many are written as ESM sources, and an increasing number are using named exports. * chore: document node types & add cjs tests
1 parent 29ddd2e commit aa15471

File tree

2 files changed

+122
-50
lines changed

2 files changed

+122
-50
lines changed

lib/utils.js

+73-46
Original file line numberDiff line numberDiff line change
@@ -118,55 +118,82 @@ function isTypeScriptRuleHelper(node) {
118118
* Helper for `getRuleInfo`. Handles ESM and TypeScript rules.
119119
*/
120120
function getRuleExportsESM(ast, scopeManager) {
121-
return ast.body
122-
.filter((statement) =>
123-
[
124-
'ExportDefaultDeclaration', // export default rule;
125-
'TSExportAssignment', // export = rule;
126-
].includes(statement.type)
127-
)
128-
.map((statement) => statement.declaration || statement.expression)
129-
130-
.reduce((currentExports, node) => {
131-
if (node.type === 'ObjectExpression') {
132-
// Check `export default { create() {}, meta: {} }`
133-
return collectInterestingProperties(
134-
node.properties,
135-
INTERESTING_RULE_KEYS
136-
);
137-
} else if (isFunctionRule(node)) {
138-
// Check `export default function(context) { return { ... }; }`
139-
return { create: node, meta: null, isNewStyle: false };
140-
} else if (isTypeScriptRuleHelper(node)) {
141-
// Check `export default someTypeScriptHelper({ create() {}, meta: {} });
142-
return collectInterestingProperties(
143-
node.arguments[0].properties,
144-
INTERESTING_RULE_KEYS
145-
);
146-
} else if (node.type === 'Identifier') {
147-
// Rule could be stored in a variable before being exported.
148-
const possibleRule = findVariableValue(node, scopeManager);
149-
if (possibleRule) {
150-
if (possibleRule.type === 'ObjectExpression') {
151-
// Check `const possibleRule = { ... }; export default possibleRule;
152-
return collectInterestingProperties(
153-
possibleRule.properties,
154-
INTERESTING_RULE_KEYS
155-
);
156-
} else if (isFunctionRule(possibleRule)) {
157-
// Check `const possibleRule = function(context) { return { ... } }; export default possibleRule;`
158-
return { create: possibleRule, meta: null, isNewStyle: false };
159-
} else if (isTypeScriptRuleHelper(possibleRule)) {
160-
// Check `const possibleRule = someTypeScriptHelper({ ... }); export default possibleRule;
161-
return collectInterestingProperties(
162-
possibleRule.arguments[0].properties,
163-
INTERESTING_RULE_KEYS
164-
);
121+
const possibleNodes = [];
122+
123+
for (const statement of ast.body) {
124+
switch (statement.type) {
125+
// export default rule;
126+
case 'ExportDefaultDeclaration': {
127+
possibleNodes.push(statement.declaration);
128+
break;
129+
}
130+
// export = rule;
131+
case 'TSExportAssignment': {
132+
possibleNodes.push(statement.expression);
133+
break;
134+
}
135+
// export const rule = { ... };
136+
// or export {rule};
137+
case 'ExportNamedDeclaration': {
138+
for (const specifier of statement.specifiers) {
139+
possibleNodes.push(specifier.local);
140+
}
141+
if (statement.declaration) {
142+
if (statement.declaration.type === 'VariableDeclaration') {
143+
for (const declarator of statement.declaration.declarations) {
144+
if (declarator.init) {
145+
possibleNodes.push(declarator.init);
146+
}
147+
}
148+
} else {
149+
possibleNodes.push(statement.declaration);
165150
}
166151
}
152+
break;
167153
}
168-
return currentExports;
169-
}, {});
154+
}
155+
}
156+
157+
return possibleNodes.reduce((currentExports, node) => {
158+
if (node.type === 'ObjectExpression') {
159+
// Check `export default { create() {}, meta: {} }`
160+
return collectInterestingProperties(
161+
node.properties,
162+
INTERESTING_RULE_KEYS
163+
);
164+
} else if (isFunctionRule(node)) {
165+
// Check `export default function(context) { return { ... }; }`
166+
return { create: node, meta: null, isNewStyle: false };
167+
} else if (isTypeScriptRuleHelper(node)) {
168+
// Check `export default someTypeScriptHelper({ create() {}, meta: {} });
169+
return collectInterestingProperties(
170+
node.arguments[0].properties,
171+
INTERESTING_RULE_KEYS
172+
);
173+
} else if (node.type === 'Identifier') {
174+
// Rule could be stored in a variable before being exported.
175+
const possibleRule = findVariableValue(node, scopeManager);
176+
if (possibleRule) {
177+
if (possibleRule.type === 'ObjectExpression') {
178+
// Check `const possibleRule = { ... }; export default possibleRule;
179+
return collectInterestingProperties(
180+
possibleRule.properties,
181+
INTERESTING_RULE_KEYS
182+
);
183+
} else if (isFunctionRule(possibleRule)) {
184+
// Check `const possibleRule = function(context) { return { ... } }; export default possibleRule;`
185+
return { create: possibleRule, meta: null, isNewStyle: false };
186+
} else if (isTypeScriptRuleHelper(possibleRule)) {
187+
// Check `const possibleRule = someTypeScriptHelper({ ... }); export default possibleRule;
188+
return collectInterestingProperties(
189+
possibleRule.arguments[0].properties,
190+
INTERESTING_RULE_KEYS
191+
);
192+
}
193+
}
194+
}
195+
return currentExports;
196+
}, {});
170197
}
171198

172199
/**

tests/lib/utils.js

+49-4
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ describe('utils', () => {
5050
'module.exports = createESLintRule({ create() {}, meta: {} });',
5151
'module.exports = util.createRule({ create() {}, meta: {} });',
5252
'module.exports = ESLintUtils.RuleCreator(docsUrl)({ create() {}, meta: {} });',
53+
54+
// Named export of a rule, only supported in ESM within this plugin
55+
'module.exports.rule = { create: function() {} };',
56+
'exports.rule = { create: function() {} };',
57+
'const rule = { create: function() {} }; module.exports.rule = rule;',
58+
'const rule = { create: function() {} }; exports.rule = rule;',
5359
].forEach((noRuleCase) => {
5460
it(`returns null for ${noRuleCase}`, () => {
5561
const ast = espree.parse(noRuleCase, { ecmaVersion: 8, range: true });
@@ -65,15 +71,11 @@ describe('utils', () => {
6571
describe('the file does not have a valid rule (ESM)', () => {
6672
[
6773
'',
68-
'export const foo = { create() {} }',
6974
'export default { foo: {} }',
7075
'const foo = {}; export default foo',
7176
'const foo = 123; export default foo',
7277
'const foo = function(){}; export default foo',
7378

74-
// Exports function but not default export.
75-
'export function foo (context) { return {}; }',
76-
7779
// Exports function but no object return inside function.
7880
'export default function (context) { }',
7981
'export default function (context) { return; }',
@@ -209,13 +211,46 @@ describe('utils', () => {
209211
meta: { type: 'ObjectExpression' },
210212
isNewStyle: true,
211213
},
214+
// No helper, exported variable.
215+
'export const rule = { create() {}, meta: {} };': {
216+
create: { type: 'FunctionExpression' },
217+
meta: { type: 'ObjectExpression' },
218+
isNewStyle: true,
219+
},
212220
// no helper, variable with type.
213221
'const rule: Rule.RuleModule = { create() {}, meta: {} }; export default rule;':
214222
{
215223
create: { type: 'FunctionExpression' },
216224
meta: { type: 'ObjectExpression' },
217225
isNewStyle: true,
218226
},
227+
// no helper, exported variable with type.
228+
'export const rule: Rule.RuleModule = { create() {}, meta: {} };': {
229+
create: { type: 'FunctionExpression' },
230+
meta: { type: 'ObjectExpression' },
231+
isNewStyle: true,
232+
},
233+
// no helper, exported reference with type.
234+
'const rule: Rule.RuleModule = { create() {}, meta: {} }; export {rule};':
235+
{
236+
create: { type: 'FunctionExpression' },
237+
meta: { type: 'ObjectExpression' },
238+
isNewStyle: true,
239+
},
240+
// no helper, exported aliased reference with type.
241+
'const foo: Rule.RuleModule = { create() {}, meta: {} }; export {foo as rule};':
242+
{
243+
create: { type: 'FunctionExpression' },
244+
meta: { type: 'ObjectExpression' },
245+
isNewStyle: true,
246+
},
247+
// no helper, exported variable with type in multiple declarations
248+
'export const foo = 5, rule: Rule.RuleModule = { create() {}, meta: {} };':
249+
{
250+
create: { type: 'FunctionExpression' },
251+
meta: { type: 'ObjectExpression' },
252+
isNewStyle: true,
253+
},
219254
// No helper, variable, `export =` syntax.
220255
'const rule = { create() {}, meta: {} }; export = rule;': {
221256
create: { type: 'FunctionExpression' },
@@ -474,6 +509,16 @@ describe('utils', () => {
474509
meta: { type: 'ObjectExpression' },
475510
isNewStyle: true,
476511
},
512+
'export const rule = { create() {}, meta: {} };': {
513+
create: { type: 'FunctionExpression' },
514+
meta: { type: 'ObjectExpression' },
515+
isNewStyle: true,
516+
},
517+
'const rule = { create() {}, meta: {} }; export {rule};': {
518+
create: { type: 'FunctionExpression' },
519+
meta: { type: 'ObjectExpression' },
520+
isNewStyle: true,
521+
},
477522

478523
// ESM (function style)
479524
'export default function (context) { return {}; }': {

0 commit comments

Comments
 (0)