Skip to content

Commit

Permalink
feat: support named exports in ESM/TS (#449)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
43081j authored Feb 26, 2024
1 parent 29ddd2e commit aa15471
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 50 deletions.
119 changes: 73 additions & 46 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,55 +118,82 @@ function isTypeScriptRuleHelper(node) {
* Helper for `getRuleInfo`. Handles ESM and TypeScript rules.
*/
function getRuleExportsESM(ast, scopeManager) {
return ast.body
.filter((statement) =>
[
'ExportDefaultDeclaration', // export default rule;
'TSExportAssignment', // export = rule;
].includes(statement.type)
)
.map((statement) => statement.declaration || statement.expression)

.reduce((currentExports, node) => {
if (node.type === 'ObjectExpression') {
// Check `export default { create() {}, meta: {} }`
return collectInterestingProperties(
node.properties,
INTERESTING_RULE_KEYS
);
} else if (isFunctionRule(node)) {
// Check `export default function(context) { return { ... }; }`
return { create: node, meta: null, isNewStyle: false };
} else if (isTypeScriptRuleHelper(node)) {
// Check `export default someTypeScriptHelper({ create() {}, meta: {} });
return collectInterestingProperties(
node.arguments[0].properties,
INTERESTING_RULE_KEYS
);
} else if (node.type === 'Identifier') {
// Rule could be stored in a variable before being exported.
const possibleRule = findVariableValue(node, scopeManager);
if (possibleRule) {
if (possibleRule.type === 'ObjectExpression') {
// Check `const possibleRule = { ... }; export default possibleRule;
return collectInterestingProperties(
possibleRule.properties,
INTERESTING_RULE_KEYS
);
} else if (isFunctionRule(possibleRule)) {
// Check `const possibleRule = function(context) { return { ... } }; export default possibleRule;`
return { create: possibleRule, meta: null, isNewStyle: false };
} else if (isTypeScriptRuleHelper(possibleRule)) {
// Check `const possibleRule = someTypeScriptHelper({ ... }); export default possibleRule;
return collectInterestingProperties(
possibleRule.arguments[0].properties,
INTERESTING_RULE_KEYS
);
const possibleNodes = [];

for (const statement of ast.body) {
switch (statement.type) {
// export default rule;
case 'ExportDefaultDeclaration': {
possibleNodes.push(statement.declaration);
break;
}
// export = rule;
case 'TSExportAssignment': {
possibleNodes.push(statement.expression);
break;
}
// export const rule = { ... };
// or export {rule};
case 'ExportNamedDeclaration': {
for (const specifier of statement.specifiers) {
possibleNodes.push(specifier.local);
}
if (statement.declaration) {
if (statement.declaration.type === 'VariableDeclaration') {
for (const declarator of statement.declaration.declarations) {
if (declarator.init) {
possibleNodes.push(declarator.init);
}
}
} else {
possibleNodes.push(statement.declaration);
}
}
break;
}
return currentExports;
}, {});
}
}

return possibleNodes.reduce((currentExports, node) => {
if (node.type === 'ObjectExpression') {
// Check `export default { create() {}, meta: {} }`
return collectInterestingProperties(
node.properties,
INTERESTING_RULE_KEYS
);
} else if (isFunctionRule(node)) {
// Check `export default function(context) { return { ... }; }`
return { create: node, meta: null, isNewStyle: false };
} else if (isTypeScriptRuleHelper(node)) {
// Check `export default someTypeScriptHelper({ create() {}, meta: {} });
return collectInterestingProperties(
node.arguments[0].properties,
INTERESTING_RULE_KEYS
);
} else if (node.type === 'Identifier') {
// Rule could be stored in a variable before being exported.
const possibleRule = findVariableValue(node, scopeManager);
if (possibleRule) {
if (possibleRule.type === 'ObjectExpression') {
// Check `const possibleRule = { ... }; export default possibleRule;
return collectInterestingProperties(
possibleRule.properties,
INTERESTING_RULE_KEYS
);
} else if (isFunctionRule(possibleRule)) {
// Check `const possibleRule = function(context) { return { ... } }; export default possibleRule;`
return { create: possibleRule, meta: null, isNewStyle: false };
} else if (isTypeScriptRuleHelper(possibleRule)) {
// Check `const possibleRule = someTypeScriptHelper({ ... }); export default possibleRule;
return collectInterestingProperties(
possibleRule.arguments[0].properties,
INTERESTING_RULE_KEYS
);
}
}
}
return currentExports;
}, {});
}

/**
Expand Down
53 changes: 49 additions & 4 deletions tests/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ describe('utils', () => {
'module.exports = createESLintRule({ create() {}, meta: {} });',
'module.exports = util.createRule({ create() {}, meta: {} });',
'module.exports = ESLintUtils.RuleCreator(docsUrl)({ create() {}, meta: {} });',

// Named export of a rule, only supported in ESM within this plugin
'module.exports.rule = { create: function() {} };',
'exports.rule = { create: function() {} };',
'const rule = { create: function() {} }; module.exports.rule = rule;',
'const rule = { create: function() {} }; exports.rule = rule;',
].forEach((noRuleCase) => {
it(`returns null for ${noRuleCase}`, () => {
const ast = espree.parse(noRuleCase, { ecmaVersion: 8, range: true });
Expand All @@ -65,15 +71,11 @@ describe('utils', () => {
describe('the file does not have a valid rule (ESM)', () => {
[
'',
'export const foo = { create() {} }',
'export default { foo: {} }',
'const foo = {}; export default foo',
'const foo = 123; export default foo',
'const foo = function(){}; export default foo',

// Exports function but not default export.
'export function foo (context) { return {}; }',

// Exports function but no object return inside function.
'export default function (context) { }',
'export default function (context) { return; }',
Expand Down Expand Up @@ -209,13 +211,46 @@ describe('utils', () => {
meta: { type: 'ObjectExpression' },
isNewStyle: true,
},
// No helper, exported variable.
'export const rule = { create() {}, meta: {} };': {
create: { type: 'FunctionExpression' },
meta: { type: 'ObjectExpression' },
isNewStyle: true,
},
// no helper, variable with type.
'const rule: Rule.RuleModule = { create() {}, meta: {} }; export default rule;':
{
create: { type: 'FunctionExpression' },
meta: { type: 'ObjectExpression' },
isNewStyle: true,
},
// no helper, exported variable with type.
'export const rule: Rule.RuleModule = { create() {}, meta: {} };': {
create: { type: 'FunctionExpression' },
meta: { type: 'ObjectExpression' },
isNewStyle: true,
},
// no helper, exported reference with type.
'const rule: Rule.RuleModule = { create() {}, meta: {} }; export {rule};':
{
create: { type: 'FunctionExpression' },
meta: { type: 'ObjectExpression' },
isNewStyle: true,
},
// no helper, exported aliased reference with type.
'const foo: Rule.RuleModule = { create() {}, meta: {} }; export {foo as rule};':
{
create: { type: 'FunctionExpression' },
meta: { type: 'ObjectExpression' },
isNewStyle: true,
},
// no helper, exported variable with type in multiple declarations
'export const foo = 5, rule: Rule.RuleModule = { create() {}, meta: {} };':
{
create: { type: 'FunctionExpression' },
meta: { type: 'ObjectExpression' },
isNewStyle: true,
},
// No helper, variable, `export =` syntax.
'const rule = { create() {}, meta: {} }; export = rule;': {
create: { type: 'FunctionExpression' },
Expand Down Expand Up @@ -474,6 +509,16 @@ describe('utils', () => {
meta: { type: 'ObjectExpression' },
isNewStyle: true,
},
'export const rule = { create() {}, meta: {} };': {
create: { type: 'FunctionExpression' },
meta: { type: 'ObjectExpression' },
isNewStyle: true,
},
'const rule = { create() {}, meta: {} }; export {rule};': {
create: { type: 'FunctionExpression' },
meta: { type: 'ObjectExpression' },
isNewStyle: true,
},

// ESM (function style)
'export default function (context) { return {}; }': {
Expand Down

0 comments on commit aa15471

Please sign in to comment.