Skip to content

Commit

Permalink
1
Browse files Browse the repository at this point in the history
  • Loading branch information
fisker committed Mar 17, 2021
1 parent 6179cbc commit cb5d4e8
Show file tree
Hide file tree
Showing 5 changed files with 535 additions and 140 deletions.
84 changes: 79 additions & 5 deletions docs/rules/prefer-module.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,91 @@
# Prefer ES modules over CommonJS.
# Prefer ES modules over CommonJS

<!-- More detailed description. Remove this comment. -->
Prefer use ESM over legacy CommonJS module.

1. Forbid `use strict` directive.

ESM scripts use Strict Mode by default.

1. Forbid `Global Return`

This is a CommonJS-only feature.

1. Forbid global variables `__dirname` and `__filename`.

It's not available in ESM.

1. Forbid `require(…)`

`import …` is preferred in ESM.

1. Forbid `exports` and `module.exports`

`export …` is preferred in ESM.

This rule is fixable.

## Fail

```js
const foo = 'unicorn';
'use strict';

//
```

```js
if (foo) {
return;
}

//
```

```js
const file = path.join(__dirname, 'foo.js');
```

```js
const {fromPairs} = require('lodash');
```

```js
module.exports = foo;
```

```js
module.exports.foo = foo;
```

## Pass


```js
function run() {
if (foo) {
return;
}

//
}

run();
```

```js
const foo = '🦄';
const file = fileURLToPath(new URL('foo.js', import.meta.url));
```
```js
import {fromPairs} from 'lodash-es';
```
```js
export default foo;
```
```js
export {foo};
```
## Resources
- [Get Ready For ESM](https://blog.sindresorhus.com/get-ready-for-esm-aa53530b3f77) by @sindresorhus
142 changes: 96 additions & 46 deletions rules/prefer-module.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use strict';
const {isParenthesized, isOpeningParenToken, ReferenceTracker, READ} = require('eslint-utils');
const {isOpeningParenToken} = require('eslint-utils');
const getDocumentationUrl = require('./utils/get-documentation-url');
const isShadowed = require('./utils/is-shadowed');
const removeSpacesAfter = require('./utils/remove-spaces-after');
Expand All @@ -11,26 +11,90 @@ const assertToken = require('./utils/assert-token');
const ERROR_USE_STRICT_DIRECTIVE = 'error/use-strict-directive';
const ERROR_GLOBAL_RETURN = 'error/global-return';
const ERROR_REQUIRE = 'error/require';
const ERROR_EXPORTS = 'error/exports';
const ERROR_IDENTIFIER = 'error/identifier';
const SUGGESTION_DIRNAME = 'suggestion/dirname';
const SUGGESTION_FILENAME = 'suggestion/filename';
const SUGGESTION_REQUIRE = 'suggestion/require';
const messages = {
[ERROR_USE_STRICT_DIRECTIVE]: 'Do not use "use strict" directive.',
[ERROR_GLOBAL_RETURN]: '"return" should used inside a function.',
[ERROR_GLOBAL_RETURN]: '"return" should be used inside a function.',
[ERROR_IDENTIFIER]: 'Do not use "{{name}}".',
[ERROR_REQUIRE]: 'Use `import` instead of `require`.',
[ERROR_EXPORTS]: 'Do not use `module.exports` or `exports`.',
[SUGGESTION_DIRNAME]: 'Replace "__dirname" with `"…(import.meta.url)"`.',
[SUGGESTION_FILENAME]: 'Replace "__filename" with `"…(import.meta.url)"`.',
[SUGGESTION_REQUIRE]: 'Switch to `import`.'
};

// TODO: DRY this and `propertiesSelector` in `./prefer-number-properties.js`
const identifierSelector = [
'Identifier',
`:matches(${
[
'__dirname',
'__filename'
].map(name => `[name="${name}"]`).join(', ')
})`,
`:not(${
[
'MemberExpression[computed=false] > .property',
'FunctionDeclaration > .id',
'ImportSpecifier > .imported',
'ExportSpecifier > .exported',
'ClassDeclaration > .id',
'ClassProperty[computed=false] > .key',
'MethodDefinition[computed=false] > .key',
'VariableDeclarator > .id',
'Property[shorthand=false][computed=false] > .key',
'TSDeclareFunction > .id',
'TSEnumMember > .id',
'TSPropertySignature > .key'
].join(', ')
})`
].join('');

const requireCallSelector = [
'CallExpression',
'[optional=false]',
'[callee.type="Identifier"]',
'[callee.name="require"]',
'[arguments.length=1]'
];
].join('');

const exportsSelector = [
'AssignmentExpression',
'[operator="="]',
' > ',
`:matches(${[
// `module.exports = foo`
[
'MemberExpression',
'[optional=false]',
'[computed=false]',
'[object.type="Identifier"]',
'[object.name="module"]',
'[property.type="Identifier"]',
'[property.name="exports"]'
].join(''),
// `module.exports.foo = foo`
[
'MemberExpression',
'[optional=false]',
'[object.type="MemberExpression"]',
'[object.optional=false]',
'[object.computed=false]',
'[object.object.type="Identifier"]',
'[object.object.name="module"]',
'[object.property.type="Identifier"]',
'[object.property.name="exports"]'
].join(''),
// `exports = foo`
'Identifier[name="exports"]',
// `exports.foo = foo`
'MemberExpression[optional=false][object.type="Identifier"][object.name="exports"]'
].join(', ')}).left`
].join('');

function fixImport(requireCall, sourceCode) {
if (!isStaticRequire(requireCall)) {
Expand Down Expand Up @@ -69,7 +133,7 @@ function fixImport(requireCall, sourceCode) {
parent.init === requireCall &&
parent.parent.type === 'VariableDeclaration' &&
parent.parent.kind === 'const' &&
parent.parent.declarations.length == 1 &&
parent.parent.declarations.length === 1 &&
parent.parent.declarations[0] === parent
) {
const declarator = parent;
Expand Down Expand Up @@ -128,7 +192,7 @@ function fixImport(requireCall, sourceCode) {
const {properties} = id;

for (const property of properties) {
const {key, value, shorthand} = property;
const {key, shorthand} = property;
if (!shorthand) {
const commaToken = sourceCode.getTokenAfter(key);
assertToken(commaToken, {
Expand All @@ -145,14 +209,6 @@ function fixImport(requireCall, sourceCode) {
}
}

function getExportSuggestions() {
// module.exports = {} -> export default
// module.exports.x = {} -> export const x = {}
// module.exports.x = b -> export {b as x} // Maybe not, TODO
// exports = {} -> export default
// exports.x = {} -> export const x = {}
}

function create(context) {
const sourceCode = context.getSourceCode();

Expand Down Expand Up @@ -195,42 +251,37 @@ function create(context) {

context.report(problem);
},
Program() {
const tracker = new ReferenceTracker(context.getScope());
const trackMap = {
__dirname: { [READ]: true },
__filename: { [READ]: true }
};

for (const {node} of tracker.iterateGlobalReferences(trackMap)) {
const replacement = node.name === '__dirname' ?
'path.dirname(url.fileURLToPath(import.meta.url))' :
'url.fileURLToPath(import.meta.url)';

context.report({
node,
messageId: ERROR_IDENTIFIER,
data: {name},
suggest: [
{
messageId: SUGGESTION_FILENAME,
fix: fixer =>
renameIdentifier(
node,
replacement,
fixer,
sourceCode
)
}
]
});
}
[exportsSelector](node) {
context.report({
node,
messageId: ERROR_EXPORTS
});
},
[identifierSelector](node) {
if (isShadowed(context.getScope(), node)) {
return;
}

const replacement = node.name === '__dirname' ?
'path.dirname(url.fileURLToPath(import.meta.url))' :
'url.fileURLToPath(import.meta.url)';
const messageId = node.name === '__dirname' ? SUGGESTION_DIRNAME : SUGGESTION_FILENAME;

context.report({
node,
messageId: ERROR_IDENTIFIER,
data: {name: node.name},
suggest: [
{
messageId,
fix: fixer => renameIdentifier(node, replacement, fixer, sourceCode)
}
]
});
}
};
}

const schema = [];

module.exports = {
create,
meta: {
Expand All @@ -239,7 +290,6 @@ module.exports = {
url: getDocumentationUrl(__filename)
},
fixable: 'code',
schema,
messages
}
};

0 comments on commit cb5d4e8

Please sign in to comment.