Skip to content

Commit 9fac546

Browse files
skozinljharb
authored andcommitted
[Fix] make 'import/order' work in a monorepo setup with scoped modules
Fixes #1597
1 parent 6af5772 commit 9fac546

File tree

8 files changed

+176
-7
lines changed

8 files changed

+176
-7
lines changed

Diff for: CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).
55
This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com).
66

77
## [Unreleased]
8+
### Fixed
9+
- [`import/external-module-folders` setting] now correctly works with directories containing modules symlinked from `node_modules` ([#1605], thanks [@skozin])
10+
11+
### Changed
12+
- [`import/external-module-folders` setting] behavior is more strict now: it will only match complete path segments ([#1605], thanks [@skozin])
813

914
## [2.20.0] - 2020-01-10
1015
### Added
@@ -636,6 +641,7 @@ for info on changes for earlier releases.
636641

637642
[`memo-parser`]: ./memo-parser/README.md
638643

644+
[#1605]: https://github.com/benmosher/eslint-plugin-import/pull/1605
639645
[#1589]: https://github.com/benmosher/eslint-plugin-import/issues/1589
640646
[#1586]: https://github.com/benmosher/eslint-plugin-import/pull/1586
641647
[#1572]: https://github.com/benmosher/eslint-plugin-import/pull/1572
@@ -1069,3 +1075,4 @@ for info on changes for earlier releases.
10691075
[@rsolomon]: https://github.com/rsolomon
10701076
[@joaovieira]: https://github.com/joaovieira
10711077
[@ivo-stefchev]: https://github.com/ivo-stefchev
1078+
[@skozin]: https://github.com/skozin

Diff for: README.md

+13-1
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,19 @@ Contribution of more such shared configs for other platforms are welcome!
339339

340340
#### `import/external-module-folders`
341341

342-
An array of folders. Resolved modules only from those folders will be considered as "external". By default - `["node_modules"]`. Makes sense if you have configured your path or webpack to handle your internal paths differently and want to considered modules from some folders, for example `bower_components` or `jspm_modules`, as "external".
342+
An array of folders. Resolved modules only from those folders will be considered as "external". By default - `["node_modules"]`. Makes sense if you have configured your path or webpack to handle your internal paths differently and want to consider modules from some folders, for example `bower_components` or `jspm_modules`, as "external".
343+
344+
This option is also useful in a monorepo setup: list here all directories that contain monorepo's packages and they will be treated as external ones no matter which resolver is used.
345+
346+
Each item in this array is either a folder's name, its subpath, or its absolute prefix path:
347+
348+
- `jspm_modules` will match any file or folder named `jspm_modules` or which has a direct or non-direct parent named `jspm_modules`, e.g. `/home/me/project/jspm_modules` or `/home/me/project/jspm_modules/some-pkg/index.js`.
349+
350+
- `packages/core` will match any path that contains these two segments, for example `/home/me/project/packages/core/src/utils.js`.
351+
352+
- `/home/me/project/packages` will only match files and directories inside this directory, and the directory itself.
353+
354+
Please note that incomplete names are not allowed here so `components` won't match `bower_components` and `packages/ui` won't match `packages/ui-utils` (but will match `packages/ui/utils`).
343355

344356
#### `import/parsers`
345357

Diff for: package.json

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"homepage": "https://github.com/benmosher/eslint-plugin-import",
5151
"devDependencies": {
5252
"@eslint/import-test-order-redirect-scoped": "file:./tests/files/order-redirect-scoped",
53+
"@test-scope/some-module": "file:./tests/files/symlinked-module",
5354
"@typescript-eslint/parser": "1.10.3-alpha.13",
5455
"babel-cli": "^6.26.0",
5556
"babel-core": "^6.26.3",

Diff for: src/core/importType.js

+12-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import coreModules from 'resolve/lib/core'
2-
import { join } from 'path'
32

43
import resolve from 'eslint-module-utils/resolve'
54

@@ -26,11 +25,19 @@ export function isBuiltIn(name, settings, path) {
2625

2726
function isExternalPath(path, name, settings) {
2827
const folders = (settings && settings['import/external-module-folders']) || ['node_modules']
28+
return !path || folders.some(folder => isSubpath(folder, path))
29+
}
2930

30-
// extract the part before the first / (redux-saga/effects => redux-saga)
31-
const packageName = name.match(/([^/]+)/)[0]
32-
33-
return !path || folders.some(folder => -1 < path.indexOf(join(folder, packageName)))
31+
function isSubpath(subpath, path) {
32+
const normSubpath = subpath.replace(/[/]$/, '')
33+
if (normSubpath.length === 0) {
34+
return false
35+
}
36+
const left = path.indexOf(normSubpath)
37+
const right = left + normSubpath.length
38+
return left !== -1 &&
39+
(left === 0 || normSubpath[0] !== '/' && path[left - 1] === '/') &&
40+
(right >= path.length || path[right] === '/')
3441
}
3542

3643
const externalModuleRegExp = /^\w/

Diff for: tests/files/symlinked-module/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default {}

Diff for: tests/files/symlinked-module/package.json

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "@test-scope/some-module",
3+
"version": "1.0.0",
4+
"private": true
5+
}

Diff for: tests/src/core/importType.js

+89-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as path from 'path'
33

44
import importType from 'core/importType'
55

6-
import { testContext } from '../utils'
6+
import { testContext, testFilePath } from '../utils'
77

88
describe('importType(name)', function () {
99
const context = testContext()
@@ -145,4 +145,92 @@ describe('importType(name)', function () {
145145
const foldersContext = testContext({ 'import/external-module-folders': ['node_modules'] })
146146
expect(importType('resolve', foldersContext)).to.equal('external')
147147
})
148+
149+
it("should return 'external' for a scoped symlinked module", function() {
150+
const foldersContext = testContext({
151+
'import/resolver': 'node',
152+
'import/external-module-folders': ['node_modules'],
153+
})
154+
expect(importType('@test-scope/some-module', foldersContext)).to.equal('external')
155+
})
156+
157+
// We're using Webpack resolver here since it resolves all symlinks, which means that
158+
// directory path will not contain node_modules/<package-name> but will point to the
159+
// actual directory inside 'files' instead
160+
it("should return 'external' for a scoped module from a symlinked directory which name " +
161+
"is contained in 'external-module-folders' (webpack resolver)", function() {
162+
const foldersContext = testContext({
163+
'import/resolver': 'webpack',
164+
'import/external-module-folders': ['symlinked-module'],
165+
})
166+
expect(importType('@test-scope/some-module', foldersContext)).to.equal('external')
167+
})
168+
169+
it("should return 'internal' for a scoped module from a symlinked directory which incomplete " +
170+
"name is contained in 'external-module-folders' (webpack resolver)", function() {
171+
const foldersContext_1 = testContext({
172+
'import/resolver': 'webpack',
173+
'import/external-module-folders': ['symlinked-mod'],
174+
})
175+
expect(importType('@test-scope/some-module', foldersContext_1)).to.equal('internal')
176+
177+
const foldersContext_2 = testContext({
178+
'import/resolver': 'webpack',
179+
'import/external-module-folders': ['linked-module'],
180+
})
181+
expect(importType('@test-scope/some-module', foldersContext_1)).to.equal('internal')
182+
})
183+
184+
it("should return 'external' for a scoped module from a symlinked directory which partial path " +
185+
"is contained in 'external-module-folders' (webpack resolver)", function() {
186+
const foldersContext = testContext({
187+
'import/resolver': 'webpack',
188+
'import/external-module-folders': ['files/symlinked-module'],
189+
})
190+
expect(importType('@test-scope/some-module', foldersContext)).to.equal('external')
191+
})
192+
193+
it("should return 'internal' for a scoped module from a symlinked directory which partial path " +
194+
"w/ incomplete segment is contained in 'external-module-folders' " +
195+
"(webpack resolver)", function() {
196+
const foldersContext_1 = testContext({
197+
'import/resolver': 'webpack',
198+
'import/external-module-folders': ['files/symlinked-mod'],
199+
})
200+
expect(importType('@test-scope/some-module', foldersContext_1)).to.equal('internal')
201+
202+
const foldersContext_2 = testContext({
203+
'import/resolver': 'webpack',
204+
'import/external-module-folders': ['les/symlinked-module'],
205+
})
206+
expect(importType('@test-scope/some-module', foldersContext_2)).to.equal('internal')
207+
})
208+
209+
it("should return 'external' for a scoped module from a symlinked directory which partial path " +
210+
"ending w/ slash is contained in 'external-module-folders' (webpack resolver)", function() {
211+
const foldersContext = testContext({
212+
'import/resolver': 'webpack',
213+
'import/external-module-folders': ['files/symlinked-module/'],
214+
})
215+
expect(importType('@test-scope/some-module', foldersContext)).to.equal('external')
216+
})
217+
218+
it("should return 'internal' for a scoped module from a symlinked directory when " +
219+
"'external-module-folders' contains an absolute path resembling directory's relative " +
220+
"path (webpack resolver)", function() {
221+
const foldersContext = testContext({
222+
'import/resolver': 'webpack',
223+
'import/external-module-folders': ['/files/symlinked-module'],
224+
})
225+
expect(importType('@test-scope/some-module', foldersContext)).to.equal('internal')
226+
})
227+
228+
it("should return 'external' for a scoped module from a symlinked directory which absolute " +
229+
"path is contained in 'external-module-folders' (webpack resolver)", function() {
230+
const foldersContext = testContext({
231+
'import/resolver': 'webpack',
232+
'import/external-module-folders': [testFilePath('symlinked-module')],
233+
})
234+
expect(importType('@test-scope/some-module', foldersContext)).to.equal('external')
235+
})
148236
})

Diff for: tests/src/rules/order.js

+48
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,55 @@ ruleTester.run('order', rule, {
298298
],
299299
}],
300300
}),
301+
// Monorepo setup, using Webpack resolver, workspace folder name in external-module-folders
302+
test({
303+
code: `
304+
import _ from 'lodash';
305+
import m from '@test-scope/some-module';
306+
307+
import bar from './bar';
308+
`,
309+
options: [{
310+
'newlines-between': 'always',
311+
}],
312+
settings: {
313+
'import/resolver': 'webpack',
314+
'import/external-module-folders': ['node_modules', 'symlinked-module'],
315+
},
316+
}),
317+
// Monorepo setup, using Webpack resolver, partial workspace folder path
318+
// in external-module-folders
319+
test({
320+
code: `
321+
import _ from 'lodash';
322+
import m from '@test-scope/some-module';
323+
324+
import bar from './bar';
325+
`,
326+
options: [{
327+
'newlines-between': 'always',
328+
}],
329+
settings: {
330+
'import/resolver': 'webpack',
331+
'import/external-module-folders': ['node_modules', 'files/symlinked-module'],
332+
},
333+
}),
334+
// Monorepo setup, using Node resolver (doesn't resolve symlinks)
335+
test({
336+
code: `
337+
import _ from 'lodash';
338+
import m from '@test-scope/some-module';
301339
340+
import bar from './bar';
341+
`,
342+
options: [{
343+
'newlines-between': 'always',
344+
}],
345+
settings: {
346+
'import/resolver': 'node',
347+
'import/external-module-folders': ['node_modules', 'files/symlinked-module'],
348+
},
349+
}),
302350
// Option: newlines-between: 'always'
303351
test({
304352
code: `

0 commit comments

Comments
 (0)