Skip to content

Commit

Permalink
New rule: no-relative-packages
Browse files Browse the repository at this point in the history
Use this rule to prevent importing packages through relative paths.

It's useful in Yarn/Lerna workspaces, were it's possible to import a sibling
package using `../package` relative path, while direct `package` is the correct one.
  • Loading branch information
panrafal committed Nov 2, 2017
1 parent c9dd91d commit 0a8d571
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 0 deletions.
66 changes: 66 additions & 0 deletions docs/rules/no-relative-packages.md
@@ -0,0 +1,66 @@
# no-relative-packages

Use this rule to prevent importing packages through relative paths.

It's useful in Yarn/Lerna workspaces, were it's possible to import a sibling
package using `../package` relative path, while direct `package` is the correct one.


### Examples

Given the following folder structure:

```
my-project
├── packages
│ ├── foo
│ │ ├── index.js
│ │ └── package.json
│ └── bar
│ ├── index.js
│ └── package.json
└── entry.js
```

And the .eslintrc file:
```
{
...
"rules": {
"import/no-relative-packages": "error"
}
}
```

The following patterns are considered problems:

```js
/**
* in my-project/packages/foo.js
*/

import bar from '../bar'; // Import sibling package using relative path
import entry from '../../entry.js'; // Import from parent package using relative path

/**
* in my-project/entry.js
*/

import bar from './packages/bar'; // Import child package using relative path
```

The following patterns are NOT considered problems:

```js
/**
* in my-project/packages/foo.js
*/

import bar from 'bar'; // Import sibling package using package name

/**
* in my-project/entry.js
*/

import bar from 'bar'; // Import sibling package using package name
```
1 change: 1 addition & 0 deletions src/index.js
Expand Up @@ -9,6 +9,7 @@ export const rules = {
'extensions': require('./rules/extensions'),
'no-restricted-paths': require('./rules/no-restricted-paths'),
'no-internal-modules': require('./rules/no-internal-modules'),
'no-relative-packages': require('./rules/no-relative-packages'),

'no-named-default': require('./rules/no-named-default'),
'no-named-as-default': require('./rules/no-named-as-default'),
Expand Down
69 changes: 69 additions & 0 deletions src/rules/no-relative-packages.js
@@ -0,0 +1,69 @@
import path from 'path'
import readPkgUp from 'read-pkg-up'

import resolve from 'eslint-module-utils/resolve'
import importType from '../core/importType'
import isStaticRequire from '../core/staticRequire'

module.exports = {
meta: {
docs: {},
},

create: function noRelativePackages(context) {

function findNamedPackage(filePath) {
const found = readPkgUp.sync({cwd: filePath, normalize: false})
// console.log(found)
if (found.pkg && !found.pkg.name) {
return findNamedPackage(path.join(found.path, '../..'))
}
return found
}

function checkImportForRelativePackage(importPath, node) {
const potentialViolationTypes = ['parent', 'index', 'sibling']
if (potentialViolationTypes.indexOf(importType(importPath, context)) === -1) {
return
}

const resolvedImport = resolve(importPath, context)
const resolvedContext = context.getFilename()

if (!resolvedImport || !resolvedContext) {
return
}

const importPkg = findNamedPackage(resolvedImport)
const contextPkg = findNamedPackage(resolvedContext)

if (importPkg.pkg && contextPkg.pkg && importPkg.pkg.name !== contextPkg.pkg.name) {
const importBaseName = path.basename(importPath)
const importRoot = path.dirname(importPkg.path)
const properPath = path.relative(importRoot, resolvedImport)
const properImport = path.join(
importPkg.pkg.name,
path.dirname(properPath),
importBaseName === path.basename(importRoot) ? '' : importBaseName
)
context.report({
node,
message: 'Relative import from another package is not allowed. ' +
`Use "${properImport}" instead of "${importPath}"`,
})
}
}

return {
ImportDeclaration(node) {
checkImportForRelativePackage(node.source.value, node.source)
},
CallExpression(node) {
if (isStaticRequire(node)) {
const [ firstArgument ] = node.arguments
checkImportForRelativePackage(firstArgument.value, firstArgument)
}
},
}
},
}
1 change: 1 addition & 0 deletions tests/files/package-named/index.js
@@ -0,0 +1 @@
export default function () {}
5 changes: 5 additions & 0 deletions tests/files/package-named/package.json
@@ -0,0 +1,5 @@
{
"name": "package-named",
"description": "Standard, named package",
"main": "index.js"
}
1 change: 1 addition & 0 deletions tests/files/package/index.js
@@ -0,0 +1 @@
export default function () {}
4 changes: 4 additions & 0 deletions tests/files/package/package.json
@@ -0,0 +1,4 @@
{
"description": "Unnamed package for reaching through main field - rxjs style",
"main": "index.js"
}
66 changes: 66 additions & 0 deletions tests/src/rules/no-relative-packages.js
@@ -0,0 +1,66 @@
import { RuleTester } from 'eslint'
import rule from 'rules/no-relative-packages'

import { test, testFilePath } from '../utils'

const ruleTester = new RuleTester()

ruleTester.run('no-relative-packages', rule, {
valid: [
test({
code: 'import foo from "./index.js"',
filename: testFilePath('./package/index.js'),
}),
test({
code: 'import bar from "../bar"',
filename: testFilePath('./package/index.js'),
}),
test({
code: 'import {foo} from "a"',
filename: testFilePath('./package-named/index.js'),
}),
test({
code: 'const bar = require("../bar.js")',
filename: testFilePath('./package/index.js'),
}),
],

invalid: [
test({
code: 'import foo from "./package-named"',
filename: testFilePath('./bar.js'),
errors: [ {
message: 'Relative import from another package is not allowed. Use "package-named" instead of "./package-named"',
line: 1,
column: 17,
} ],
}),
test({
code: 'import foo from "../package-named"',
filename: testFilePath('./package/index.js'),
errors: [ {
message: 'Relative import from another package is not allowed. Use "package-named" instead of "../package-named"',
line: 1,
column: 17,
} ],
}),
test({
code: 'import foo from "../package-scoped"',
filename: testFilePath('./package/index.js'),
errors: [ {
message: 'Relative import from another package is not allowed. Use "@scope/package-named" instead of "../package-scoped"',
line: 1,
column: 17,
} ],
}),
test({
code: 'import bar from "../bar"',
filename: testFilePath('./package-named/index.js'),
errors: [ {
message: 'Relative import from another package is not allowed. Use "eslint-plugin-import/tests/files/bar" instead of "../bar"',
line: 1,
column: 17,
} ],
}),
],
})

0 comments on commit 0a8d571

Please sign in to comment.