Skip to content

Commit a45661b

Browse files
ttmarekljharb
authored andcommitted
[New] add no-import-module-exports rule: report import declarations with CommonJS exports
Fixes #760
1 parent fe51583 commit a45661b

File tree

7 files changed

+250
-1
lines changed

7 files changed

+250
-1
lines changed

CHANGELOG.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
1010
- [`no-commonjs`]: Also detect require calls with expressionless template literals: ``` require(`x`) ``` ([#1958], thanks [@FloEdelmann])
1111
- [`no-internal-modules`]: Add `forbid` option ([#1846], thanks [@guillaumewuip])
1212
- add [`no-relative-packages`] ([#1860], [#966], thanks [@tapayne88] [@panrafal])
13+
- add [`no-import-module-exports`] rule: report import declarations with CommonJS exports ([#804], thanks [@kentcdodds] and [@ttmarek])
1314

1415
### Fixed
1516
- [`export`]/TypeScript: properly detect export specifiers as children of a TS module block ([#1889], thanks [@andreubotella])
@@ -732,6 +733,7 @@ for info on changes for earlier releases.
732733
[`no-duplicates`]: ./docs/rules/no-duplicates.md
733734
[`no-dynamic-require`]: ./docs/rules/no-dynamic-require.md
734735
[`no-extraneous-dependencies`]: ./docs/rules/no-extraneous-dependencies.md
736+
[`no-import-module-exports`]: ./docs/rules/no-import-module-exports.md
735737
[`no-internal-modules`]: ./docs/rules/no-internal-modules.md
736738
[`no-mutable-exports`]: ./docs/rules/no-mutable-exports.md
737739
[`no-named-as-default-member`]: ./docs/rules/no-named-as-default-member.md
@@ -908,6 +910,7 @@ for info on changes for earlier releases.
908910
[#871]: https://github.com/benmosher/eslint-plugin-import/pull/871
909911
[#858]: https://github.com/benmosher/eslint-plugin-import/pull/858
910912
[#843]: https://github.com/benmosher/eslint-plugin-import/pull/843
913+
[#804]: https://github.com/benmosher/eslint-plugin-import/pull/804
911914
[#797]: https://github.com/benmosher/eslint-plugin-import/pull/797
912915
[#794]: https://github.com/benmosher/eslint-plugin-import/pull/794
913916
[#744]: https://github.com/benmosher/eslint-plugin-import/pull/744
@@ -1330,4 +1333,5 @@ for info on changes for earlier releases.
13301333
[@bicstone]: https://github.com/bicstone
13311334
[@guillaumewuip]: https://github.com/guillaumewuip
13321335
[@tapayne88]: https://github.com/tapayne88
1333-
[@panrafal]: https://github.com/panrafal
1336+
[@panrafal]: https://github.com/panrafal
1337+
[@ttmarek]: https://github.com/ttmarek

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,13 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
6767
* Report CommonJS `require` calls and `module.exports` or `exports.*`. ([`no-commonjs`])
6868
* Report AMD `require` and `define` calls. ([`no-amd`])
6969
* No Node.js builtin modules. ([`no-nodejs-modules`])
70+
* Forbid imports with CommonJS exports ([`no-import-module-exports`])
7071

7172
[`unambiguous`]: ./docs/rules/unambiguous.md
7273
[`no-commonjs`]: ./docs/rules/no-commonjs.md
7374
[`no-amd`]: ./docs/rules/no-amd.md
7475
[`no-nodejs-modules`]: ./docs/rules/no-nodejs-modules.md
76+
[`no-import-module-exports`]: ./docs/rules/no-import-module-exports.md
7577

7678

7779
### Style guide
+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# no-import-module-exports
2+
3+
Reports the use of import declarations with CommonJS exports in any module
4+
except for the [main module](https://docs.npmjs.com/files/package.json#main).
5+
6+
If you have multiple entry points or are using `js:next` this rule includes an
7+
`exceptions` option which you can use to exclude those files from the rule.
8+
9+
## Options
10+
11+
#### `exceptions`
12+
- An array of globs. The rule will be omitted from any file that matches a glob
13+
in the options array. For example, the following setting will omit the rule
14+
in the `some-file.js` file.
15+
16+
```json
17+
"import/no-import-module-exports": ["error", {
18+
"exceptions": ["**/*/some-file.js"]
19+
}]
20+
```
21+
22+
## Rule Details
23+
24+
### Fail
25+
26+
```js
27+
import { stuff } from 'starwars'
28+
module.exports = thing
29+
30+
import * as allThings from 'starwars'
31+
exports.bar = thing
32+
33+
import thing from 'other-thing'
34+
exports.foo = bar
35+
36+
import thing from 'starwars'
37+
const baz = module.exports = thing
38+
console.log(baz)
39+
```
40+
41+
### Pass
42+
Given the following package.json:
43+
44+
```json
45+
{
46+
"main": "lib/index.js",
47+
}
48+
```
49+
50+
```js
51+
import thing from 'other-thing'
52+
export default thing
53+
54+
const thing = require('thing')
55+
module.exports = thing
56+
57+
const thing = require('thing')
58+
exports.foo = bar
59+
60+
import thing from 'otherthing'
61+
console.log(thing.module.exports)
62+
63+
// in lib/index.js
64+
import foo from 'path';
65+
module.exports = foo;
66+
67+
// in some-file.js
68+
// eslint import/no-import-module-exports: ["error", {"exceptions": ["**/*/some-file.js"]}]
69+
import foo from 'path';
70+
module.exports = foo;
71+
```
72+
73+
### Further Reading
74+
- [webpack issue #4039](https://github.com/webpack/webpack/issues/4039)

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
"is-core-module": "^1.0.2",
110110
"minimatch": "^3.0.4",
111111
"object.values": "^1.1.1",
112+
"pkg-up": "^1.0.0",
112113
"read-pkg-up": "^2.0.0",
113114
"resolve": "^1.17.0",
114115
"tsconfig-paths": "^3.9.0"

src/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const rules = {
4040
'no-unassigned-import': require('./rules/no-unassigned-import'),
4141
'no-useless-path-segments': require('./rules/no-useless-path-segments'),
4242
'dynamic-import-chunkname': require('./rules/dynamic-import-chunkname'),
43+
'no-import-module-exports': require('./rules/no-import-module-exports'),
4344

4445
// export
4546
'exports-last': require('./rules/exports-last'),

src/rules/no-import-module-exports.js

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import minimatch from 'minimatch';
2+
import path from 'path';
3+
import pkgUp from 'pkg-up';
4+
5+
function getEntryPoint(context) {
6+
const pkgPath = pkgUp.sync(context.getFilename());
7+
return require.resolve(path.dirname(pkgPath));
8+
}
9+
10+
module.exports = {
11+
meta: {
12+
type: 'problem',
13+
docs: {
14+
description: 'Disallow import statements with module.exports',
15+
category: 'Best Practices',
16+
recommended: true,
17+
},
18+
fixable: 'code',
19+
schema: [
20+
{
21+
'type': 'object',
22+
'properties': {
23+
'exceptions': { 'type': 'array' },
24+
},
25+
'additionalProperties': false,
26+
},
27+
],
28+
},
29+
create(context) {
30+
const importDeclarations = [];
31+
const entryPoint = getEntryPoint(context);
32+
const options = context.options[0] || {};
33+
let alreadyReported = false;
34+
35+
function report(node) {
36+
const fileName = context.getFilename();
37+
const isEntryPoint = entryPoint === fileName;
38+
const isIdentifier = node.object.type === 'Identifier';
39+
const hasKeywords = (/^(module|exports)$/).test(node.object.name);
40+
const isException = options.exceptions &&
41+
options.exceptions.some(glob => minimatch(fileName, glob));
42+
43+
if (isIdentifier && hasKeywords && !isEntryPoint && !isException) {
44+
importDeclarations.forEach(importDeclaration => {
45+
context.report({
46+
node: importDeclaration,
47+
message: `Cannot use import declarations in modules that export using ` +
48+
`CommonJS (module.exports = 'foo' or exports.bar = 'hi')`,
49+
});
50+
});
51+
alreadyReported = true;
52+
}
53+
}
54+
55+
return {
56+
ImportDeclaration(node) {
57+
importDeclarations.push(node);
58+
},
59+
MemberExpression(node) {
60+
if (!alreadyReported) {
61+
report(node);
62+
}
63+
},
64+
};
65+
},
66+
};
+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import path from 'path';
2+
import { RuleTester } from 'eslint';
3+
4+
import { test } from '../utils';
5+
6+
const ruleTester = new RuleTester({
7+
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
8+
});
9+
const rule = require('rules/no-import-module-exports');
10+
11+
const error = {
12+
message: `Cannot use import declarations in modules that export using CommonJS ` +
13+
`(module.exports = 'foo' or exports.bar = 'hi')`,
14+
type: 'ImportDeclaration',
15+
};
16+
17+
ruleTester.run('no-import-module-exports', rule, {
18+
valid: [
19+
test({
20+
code: `
21+
const thing = require('thing')
22+
module.exports = thing
23+
`,
24+
}),
25+
test({
26+
code: `
27+
import thing from 'otherthing'
28+
console.log(thing.module.exports)
29+
`,
30+
}),
31+
test({
32+
code: `
33+
import thing from 'other-thing'
34+
export default thing
35+
`,
36+
}),
37+
test({
38+
code: `
39+
const thing = require('thing')
40+
exports.foo = bar
41+
`,
42+
}),
43+
test({
44+
code: `
45+
import foo from 'path';
46+
module.exports = foo;
47+
`,
48+
// When the file matches the entry point defined in package.json
49+
// See tests/files/package.json
50+
filename: path.join(process.cwd(), 'tests/files/index.js'),
51+
}),
52+
test({
53+
code: `
54+
import foo from 'path';
55+
module.exports = foo;
56+
`,
57+
filename: path.join(process.cwd(), 'tests/files/some/other/entry-point.js'),
58+
options: [{ exceptions: ['**/*/other/entry-point.js'] }],
59+
}),
60+
],
61+
invalid: [
62+
test({
63+
code: `
64+
import { stuff } from 'starwars'
65+
module.exports = thing
66+
`,
67+
errors: [error],
68+
}),
69+
test({
70+
code: `
71+
import thing from 'starwars'
72+
const baz = module.exports = thing
73+
console.log(baz)
74+
`,
75+
errors: [error],
76+
}),
77+
test({
78+
code: `
79+
import * as allThings from 'starwars'
80+
exports.bar = thing
81+
`,
82+
errors: [error],
83+
}),
84+
test({
85+
code: `
86+
import thing from 'other-thing'
87+
exports.foo = bar
88+
`,
89+
errors: [error],
90+
}),
91+
test({
92+
code: `
93+
import foo from 'path';
94+
module.exports = foo;
95+
`,
96+
filename: path.join(process.cwd(), 'tests/files/some/other/entry-point.js'),
97+
options: [{ exceptions: ['**/*/other/file.js'] }],
98+
errors: [error],
99+
}),
100+
],
101+
});

0 commit comments

Comments
 (0)