From 0ce1f204950a5e4c14ed20b541ed32fc4dda15d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Bourgier?= Date: Tue, 4 Jun 2024 19:25:18 +0200 Subject: [PATCH 1/8] feat: add `forSubpaths` and `forSiblings` options --- src/rules/prefer-alias.js | 44 ++++++++++++++++++++---- src/rules/prefer-alias.spec.js | 62 ++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 6 deletions(-) diff --git a/src/rules/prefer-alias.js b/src/rules/prefer-alias.js index 5397f15..f7cb503 100644 --- a/src/rules/prefer-alias.js +++ b/src/rules/prefer-alias.js @@ -6,6 +6,10 @@ import P from 'path' const isParentImport = path => /^(\.\/)?\.\.\//.test(path) +const isSiblingImport = path => /^\.\/[^/]+$/.test(path) + +const isSubpathImport = path => /^\.\/.+\//.test(path) + const findMatchingAlias = (sourcePath, currentFile, options) => { const resolvePath = options.resolvePath || defaultResolvePath @@ -61,8 +65,22 @@ export default { options.alias |> keys |> some(alias => sourcePath |> startsWith(`${alias}/`)) - // relative parent - if (sourcePath |> isParentImport) { + + const importWithoutAlias = resolvePath(sourcePath, currentFile, options) + + const [shouldAlias, shouldUnalias] = [ + !hasAlias && + ((importWithoutAlias |> isParentImport) || + ((importWithoutAlias |> isSiblingImport) && + options.forSiblings) || + ((importWithoutAlias |> isSubpathImport) && options.forSubpaths)), + hasAlias && + (((importWithoutAlias |> isSiblingImport) && + !options.forSiblings) || + ((importWithoutAlias |> isSubpathImport) && + !options.forSubpaths)), + ] + if (shouldAlias) { const matchingAlias = findMatchingAlias( sourcePath, currentFile, @@ -78,6 +96,14 @@ export default { P.relative(matchingAlias.path, absoluteImportPath) |> replace(/\\/g, '/') }` + let importType + if (importWithoutAlias |> isSiblingImport) { + importType = 'sibling' + } else if (importWithoutAlias |> isSubpathImport) { + importType = 'subpath' + } else { + importType = 'parent' + } return context.report({ fix: fixer => @@ -85,13 +111,11 @@ export default { [node.source.range[0] + 1, node.source.range[1] - 1], rewrittenImport, ), - message: `Unexpected parent import '${sourcePath}'. Use '${rewrittenImport}' instead`, + message: `Unexpected ${importType} import '${sourcePath}'. Use '${rewrittenImport}' instead`, node, }) } - - const importWithoutAlias = resolvePath(sourcePath, currentFile, options) - if (!(importWithoutAlias |> isParentImport) && hasAlias) { + if (shouldUnalias) { return context.report({ fix: fixer => fixer.replaceTextRange( @@ -116,6 +140,14 @@ export default { alias: { type: 'object', }, + forSiblings: { + default: false, + type: 'boolean', + }, + forSubpaths: { + default: false, + type: 'boolean', + }, }, type: 'object', }, diff --git a/src/rules/prefer-alias.spec.js b/src/rules/prefer-alias.spec.js index 5bb45fd..71eaa5d 100644 --- a/src/rules/prefer-alias.spec.js +++ b/src/rules/prefer-alias.spec.js @@ -43,6 +43,68 @@ const lint = (code, options = {}) => { export default tester( { + 'alias for siblings': async () => { + await outputFiles({ + '.babelrc.json': JSON.stringify({ + plugins: [ + [ + packageName`babel-plugin-module-resolver`, + { alias: { '@': '.' } }, + ], + ], + }), + 'foo.js': '', + }) + expect( + lint("import foo from './foo'", { + eslintConfig: { + rules: { + 'self/self': [ + 'error', + { + forSiblings: true, + }, + ], + }, + }, + }), + ).toEqual({ + messages: ["Unexpected sibling import './foo'. Use '@/foo' instead"], + output: "import foo from '@/foo'", + }) + }, + 'alias for subpaths': async () => { + await outputFiles({ + '.babelrc.json': JSON.stringify({ + plugins: [ + [ + packageName`babel-plugin-module-resolver`, + { alias: { '@': '.' } }, + ], + ], + }), + 'sub/foo.js': '', + }) + expect( + lint("import foo from './sub/foo'", { + eslintConfig: { + rules: { + 'self/self': [ + 'error', + { + forSubpaths: true, + }, + ], + }, + }, + }), + ).toEqual({ + messages: [ + "Unexpected subpath import './sub/foo'. Use '@/sub/foo' instead", + ], + output: "import foo from '@/sub/foo'", + }) + }, 'alias parent': async () => { await outputFiles({ '.babelrc.json': JSON.stringify({ From c73732d550bbdd45fd30cdad6e960dc901da9124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Bourgier?= Date: Wed, 7 Aug 2024 09:02:37 +0200 Subject: [PATCH 2/8] refactor: use 2 separate calls instead of an array destructuring --- src/rules/prefer-alias.js | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/rules/prefer-alias.js b/src/rules/prefer-alias.js index f7cb503..fbd5ab1 100644 --- a/src/rules/prefer-alias.js +++ b/src/rules/prefer-alias.js @@ -68,18 +68,11 @@ export default { const importWithoutAlias = resolvePath(sourcePath, currentFile, options) - const [shouldAlias, shouldUnalias] = [ + const shouldAlias = !hasAlias && - ((importWithoutAlias |> isParentImport) || - ((importWithoutAlias |> isSiblingImport) && - options.forSiblings) || - ((importWithoutAlias |> isSubpathImport) && options.forSubpaths)), - hasAlias && - (((importWithoutAlias |> isSiblingImport) && - !options.forSiblings) || - ((importWithoutAlias |> isSubpathImport) && - !options.forSubpaths)), - ] + ((importWithoutAlias |> isParentImport) || + ((importWithoutAlias |> isSiblingImport) && options.forSiblings) || + ((importWithoutAlias |> isSubpathImport) && options.forSubpaths)) if (shouldAlias) { const matchingAlias = findMatchingAlias( sourcePath, @@ -115,6 +108,11 @@ export default { node, }) } + + const shouldUnalias = + hasAlias && + (((importWithoutAlias |> isSiblingImport) && !options.forSiblings) || + ((importWithoutAlias |> isSubpathImport) && !options.forSubpaths)) if (shouldUnalias) { return context.report({ fix: fixer => From 6f05f08f6ba28018bea44d7dd5279310b380b0bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Bourgier?= Date: Wed, 7 Aug 2024 09:12:17 +0200 Subject: [PATCH 3/8] refactor: create a function to remove a `let` declaration --- src/rules/prefer-alias.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/rules/prefer-alias.js b/src/rules/prefer-alias.js index fbd5ab1..3c6cf8e 100644 --- a/src/rules/prefer-alias.js +++ b/src/rules/prefer-alias.js @@ -27,6 +27,17 @@ const findMatchingAlias = (sourcePath, currentFile, options) => { return undefined } +const getImportType = importWithoutAlias => { + if (importWithoutAlias |> isSiblingImport) { + return 'sibling' + } + if (importWithoutAlias |> isSubpathImport) { + return 'subpath' + } + + return 'parent' +} + export default { create: context => { const currentFile = context.getFilename() @@ -89,14 +100,8 @@ export default { P.relative(matchingAlias.path, absoluteImportPath) |> replace(/\\/g, '/') }` - let importType - if (importWithoutAlias |> isSiblingImport) { - importType = 'sibling' - } else if (importWithoutAlias |> isSubpathImport) { - importType = 'subpath' - } else { - importType = 'parent' - } + + const importType = getImportType(importWithoutAlias) return context.report({ fix: fixer => From f0ba5fa0c43ce7652582453386ff1b583e4f5a35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Bourgier?= Date: Wed, 7 Aug 2024 17:57:17 +0200 Subject: [PATCH 4/8] fix: handle when an alias is directly imported from another one --- src/rules/prefer-alias.js | 14 +++++++++----- src/rules/prefer-alias.spec.js | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/rules/prefer-alias.js b/src/rules/prefer-alias.js index 3c6cf8e..5fc019f 100644 --- a/src/rules/prefer-alias.js +++ b/src/rules/prefer-alias.js @@ -75,7 +75,10 @@ export default { const hasAlias = options.alias |> keys - |> some(alias => sourcePath |> startsWith(`${alias}/`)) + |> some( + alias => + (sourcePath |> startsWith(`${alias}/`)) || sourcePath === alias, + ) const importWithoutAlias = resolvePath(sourcePath, currentFile, options) @@ -96,10 +99,11 @@ export default { const absoluteImportPath = P.resolve(folder, sourcePath) - const rewrittenImport = `${matchingAlias.name}/${ - P.relative(matchingAlias.path, absoluteImportPath) - |> replace(/\\/g, '/') - }` + const rewrittenImport = + `${matchingAlias.name}/${ + P.relative(matchingAlias.path, absoluteImportPath) + |> replace(/\\/g, '/') + }` |> replace(/\/$/, '') const importType = getImportType(importWithoutAlias) diff --git a/src/rules/prefer-alias.spec.js b/src/rules/prefer-alias.spec.js index 71eaa5d..64ccf56 100644 --- a/src/rules/prefer-alias.spec.js +++ b/src/rules/prefer-alias.spec.js @@ -245,6 +245,26 @@ export default tester( output: "import foo from '@/foo'", }) }, + 'direct import of an alias from another one': async () => { + await outputFiles({ + '.babelrc.json': JSON.stringify({ + plugins: [ + [ + packageName`babel-plugin-module-resolver`, + { alias: { '@components': './components', '@hooks': './hooks' } }, + ], + ], + }), + }) + expect( + lint("import { foo } from '../hooks'", { + filename: 'components/bar.js', + }), + ).toEqual({ + messages: ["Unexpected parent import '../hooks'. Use '@hooks' instead"], + output: "import { foo } from '@hooks'", + }) + }, external: async () => { await fs.outputFile( '.babelrc.json', From 006bdc9008f4b565b40873dd32e57669266d8271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Bourgier?= Date: Wed, 7 Aug 2024 18:41:16 +0200 Subject: [PATCH 5/8] fix: regression when an alias is directly imported from a sibling or a parent --- src/rules/prefer-alias.js | 4 ++++ src/rules/prefer-alias.spec.js | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/rules/prefer-alias.js b/src/rules/prefer-alias.js index 5fc019f..230e9b5 100644 --- a/src/rules/prefer-alias.js +++ b/src/rules/prefer-alias.js @@ -118,8 +118,12 @@ export default { }) } + const isDirectAlias = + options.alias |> keys |> some(alias => sourcePath === alias) + const shouldUnalias = hasAlias && + !isDirectAlias && (((importWithoutAlias |> isSiblingImport) && !options.forSiblings) || ((importWithoutAlias |> isSubpathImport) && !options.forSubpaths)) if (shouldUnalias) { diff --git a/src/rules/prefer-alias.spec.js b/src/rules/prefer-alias.spec.js index 64ccf56..07a32e2 100644 --- a/src/rules/prefer-alias.spec.js +++ b/src/rules/prefer-alias.spec.js @@ -245,6 +245,32 @@ export default tester( output: "import foo from '@/foo'", }) }, + 'direct import of an alias from a parent': async () => { + await outputFiles({ + '.babelrc.json': JSON.stringify({ + plugins: [ + [ + packageName`babel-plugin-module-resolver`, + { alias: { '@components': './sub/components' } }, + ], + ], + }), + }) + expect(lint("import { foo } from '@components'").messages).toEqual([]) + }, + 'direct import of an alias from a sibling': async () => { + await outputFiles({ + '.babelrc.json': JSON.stringify({ + plugins: [ + [ + packageName`babel-plugin-module-resolver`, + { alias: { '@components': './components' } }, + ], + ], + }), + }) + expect(lint("import { foo } from '@components'").messages).toEqual([]) + }, 'direct import of an alias from another one': async () => { await outputFiles({ '.babelrc.json': JSON.stringify({ From cd90dcff5006a44316f62ef054b4a29ec7703bb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Bourgier?= Date: Wed, 7 Aug 2024 19:26:44 +0200 Subject: [PATCH 6/8] feat: improve `forSiblings` option (add `forMaxNestingLevel` sub-option) --- src/rules/prefer-alias.js | 58 +++++++++--- src/rules/prefer-alias.spec.js | 162 ++++++++++++++++++++++++++++++++- 2 files changed, 207 insertions(+), 13 deletions(-) diff --git a/src/rules/prefer-alias.js b/src/rules/prefer-alias.js index 230e9b5..1f6bce9 100644 --- a/src/rules/prefer-alias.js +++ b/src/rules/prefer-alias.js @@ -38,6 +38,17 @@ const getImportType = importWithoutAlias => { return 'parent' } +const getSiblingsMaxNestingLevel = options => { + if (options.forSiblings === true) { + return Infinity + } + if (options.forSiblings) { + return options.forSiblings.ofMaxNestingLevel + } + + return -1 +} + export default { create: context => { const currentFile = context.getFilename() @@ -66,6 +77,8 @@ export default { ) } + const siblingsMaxNestingLevel = getSiblingsMaxNestingLevel(options) + const resolvePath = options.resolvePath || defaultResolvePath return { @@ -82,17 +95,25 @@ export default { const importWithoutAlias = resolvePath(sourcePath, currentFile, options) + const importType = getImportType(importWithoutAlias) + + const matchingAlias = findMatchingAlias( + sourcePath, + currentFile, + options, + ) + + const currentFileNestingLevel = + matchingAlias && + P.relative(matchingAlias.path, currentFile).split(P.sep).length - 1 + const shouldAlias = !hasAlias && ((importWithoutAlias |> isParentImport) || - ((importWithoutAlias |> isSiblingImport) && options.forSiblings) || + ((importWithoutAlias |> isSiblingImport) && + currentFileNestingLevel <= siblingsMaxNestingLevel) || ((importWithoutAlias |> isSubpathImport) && options.forSubpaths)) if (shouldAlias) { - const matchingAlias = findMatchingAlias( - sourcePath, - currentFile, - options, - ) if (!matchingAlias) { return undefined } @@ -105,8 +126,6 @@ export default { |> replace(/\\/g, '/') }` |> replace(/\/$/, '') - const importType = getImportType(importWithoutAlias) - return context.report({ fix: fixer => fixer.replaceTextRange( @@ -124,7 +143,8 @@ export default { const shouldUnalias = hasAlias && !isDirectAlias && - (((importWithoutAlias |> isSiblingImport) && !options.forSiblings) || + (((importWithoutAlias |> isSiblingImport) && + currentFileNestingLevel > siblingsMaxNestingLevel) || ((importWithoutAlias |> isSubpathImport) && !options.forSubpaths)) if (shouldUnalias) { return context.report({ @@ -133,7 +153,7 @@ export default { [node.source.range[0] + 1, node.source.range[1] - 1], importWithoutAlias, ), - message: `Unexpected subpath import via alias '${sourcePath}'. Use '${importWithoutAlias}' instead`, + message: `Unexpected ${importType} import via alias '${sourcePath}'. Use '${importWithoutAlias}' instead`, node, }) } @@ -152,8 +172,22 @@ export default { type: 'object', }, forSiblings: { - default: false, - type: 'boolean', + anyOf: [ + { + default: false, + type: 'boolean', + }, + { + additionalProperties: false, + properties: { + ofMaxNestingLevel: { + minimum: 0, + type: 'number', + }, + }, + type: 'object', + }, + ], }, forSubpaths: { default: false, diff --git a/src/rules/prefer-alias.spec.js b/src/rules/prefer-alias.spec.js index 07a32e2..5f5dad1 100644 --- a/src/rules/prefer-alias.spec.js +++ b/src/rules/prefer-alias.spec.js @@ -73,6 +73,166 @@ export default tester( output: "import foo from '@/foo'", }) }, + 'alias for siblings with max nested level': async () => { + await outputFiles({ + '.babelrc.json': JSON.stringify({ + plugins: [ + [ + packageName`babel-plugin-module-resolver`, + { alias: { '@': '.' } }, + ], + ], + }), + }) + expect( + lint("import foo from './foo'", { + eslintConfig: { + rules: { + 'self/self': [ + 'error', + { + forSiblings: { + ofMaxNestingLevel: 0, + }, + }, + ], + }, + }, + filename: 'bar.js', + }), + ).toEqual({ + messages: ["Unexpected sibling import './foo'. Use '@/foo' instead"], + output: "import foo from '@/foo'", + }) + expect( + lint("import foo from './foo'", { + eslintConfig: { + rules: { + 'self/self': [ + 'error', + { + forSiblings: { + ofMaxNestingLevel: 0, + }, + }, + ], + }, + }, + filename: 'sub/bar.js', + }).messages, + ).toEqual([]) + expect( + lint("import foo from './foo'", { + eslintConfig: { + rules: { + 'self/self': [ + 'error', + { + forSiblings: { + ofMaxNestingLevel: 1, + }, + }, + ], + }, + }, + filename: 'sub/bar.js', + }), + ).toEqual({ + messages: [ + "Unexpected sibling import './foo'. Use '@/sub/foo' instead", + ], + output: "import foo from '@/sub/foo'", + }) + expect( + lint("import foo from './foo'", { + eslintConfig: { + rules: { + 'self/self': [ + 'error', + { + forSiblings: { + ofMaxNestingLevel: 1, + }, + }, + ], + }, + }, + filename: 'sub/sub/bar.js', + }).messages, + ).toEqual([]) + expect( + lint("import foo from './foo'", { + eslintConfig: { + rules: { + 'self/self': [ + 'error', + { + forSiblings: { + ofMaxNestingLevel: 2, + }, + }, + ], + }, + }, + filename: 'sub/sub/bar.js', + }), + ).toEqual({ + messages: [ + "Unexpected sibling import './foo'. Use '@/sub/sub/foo' instead", + ], + output: "import foo from '@/sub/sub/foo'", + }) + expect( + lint("import foo from './foo'", { + eslintConfig: { + rules: { + 'self/self': [ + 'error', + { + forSiblings: { + ofMaxNestingLevel: 2, + }, + }, + ], + }, + }, + filename: 'sub/sub/sub/bar.js', + }).messages, + ).toEqual([]) + }, + 'alias for siblings, nested': async () => { + await outputFiles({ + '.babelrc.json': JSON.stringify({ + plugins: [ + [ + packageName`babel-plugin-module-resolver`, + { alias: { '@': '.' } }, + ], + ], + }), + 'sub/foo.js': '', + }) + expect( + lint("import foo from './foo'", { + eslintConfig: { + rules: { + 'self/self': [ + 'error', + { + forSiblings: true, + }, + ], + }, + }, + filename: 'sub/bar.js', + }), + ).toEqual({ + messages: [ + "Unexpected sibling import './foo'. Use '@/sub/foo' instead", + ], + output: "import foo from '@/sub/foo'", + }) + }, 'alias for subpaths': async () => { await outputFiles({ '.babelrc.json': JSON.stringify({ @@ -135,7 +295,7 @@ export default tester( }) expect(lint("import foo from '@/foo'")).toEqual({ messages: [ - "Unexpected subpath import via alias '@/foo'. Use './foo' instead", + "Unexpected sibling import via alias '@/foo'. Use './foo' instead", ], output: "import foo from './foo'", }) From 49584c9bf99b3d31d83c329c70a44ba39f31ee87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Bourgier?= Date: Fri, 9 Aug 2024 12:53:41 +0200 Subject: [PATCH 7/8] feat: improve `forSubpaths` option (add `fromInside` and `fromOuside` sub-options) --- src/rules/prefer-alias.js | 48 +++++++++++-- src/rules/prefer-alias.spec.js | 120 +++++++++++++++++++++++++++++---- 2 files changed, 151 insertions(+), 17 deletions(-) diff --git a/src/rules/prefer-alias.js b/src/rules/prefer-alias.js index 1f6bce9..81d1f1e 100644 --- a/src/rules/prefer-alias.js +++ b/src/rules/prefer-alias.js @@ -49,6 +49,22 @@ const getSiblingsMaxNestingLevel = options => { return -1 } +const optionForSubpathsMatches = (options, currentFileIsInsideAlias) => { + if (options.forSubpaths === true) { + return true + } + if (options.forSubpaths) { + if (currentFileIsInsideAlias && options.forSubpaths.fromInside) { + return true + } + if (!currentFileIsInsideAlias && options.forSubpaths.fromOutside) { + return true + } + } + + return false +} + export default { create: context => { const currentFile = context.getFilename() @@ -107,12 +123,17 @@ export default { matchingAlias && P.relative(matchingAlias.path, currentFile).split(P.sep).length - 1 + const currentFileIsInsideAlias = + matchingAlias && + !(P.relative(matchingAlias.path, currentFile) |> startsWith('..')) + const shouldAlias = !hasAlias && ((importWithoutAlias |> isParentImport) || ((importWithoutAlias |> isSiblingImport) && currentFileNestingLevel <= siblingsMaxNestingLevel) || - ((importWithoutAlias |> isSubpathImport) && options.forSubpaths)) + ((importWithoutAlias |> isSubpathImport) && + optionForSubpathsMatches(options, currentFileIsInsideAlias))) if (shouldAlias) { if (!matchingAlias) { return undefined @@ -145,7 +166,8 @@ export default { !isDirectAlias && (((importWithoutAlias |> isSiblingImport) && currentFileNestingLevel > siblingsMaxNestingLevel) || - ((importWithoutAlias |> isSubpathImport) && !options.forSubpaths)) + ((importWithoutAlias |> isSubpathImport) && + !optionForSubpathsMatches(options, currentFileIsInsideAlias))) if (shouldUnalias) { return context.report({ fix: fixer => @@ -190,8 +212,26 @@ export default { ], }, forSubpaths: { - default: false, - type: 'boolean', + anyOf: [ + { + default: false, + type: 'boolean', + }, + { + additionalProperties: false, + properties: { + fromInside: { + default: false, + type: 'boolean', + }, + fromOutside: { + default: false, + type: 'boolean', + }, + }, + type: 'object', + }, + ], }, }, type: 'object', diff --git a/src/rules/prefer-alias.spec.js b/src/rules/prefer-alias.spec.js index 5f5dad1..223dcaf 100644 --- a/src/rules/prefer-alias.spec.js +++ b/src/rules/prefer-alias.spec.js @@ -239,31 +239,125 @@ export default tester( plugins: [ [ packageName`babel-plugin-module-resolver`, - { alias: { '@': '.' } }, + { alias: { '@components': './components' } }, ], ], }), - 'sub/foo.js': '', + }) + + const eslintConfig = { + rules: { + 'self/self': [ + 'error', + { + forSubpaths: true, + }, + ], + }, + } + expect( + lint("import foo from './components/foo'", { + eslintConfig, + }), + ).toEqual({ + messages: [ + "Unexpected subpath import './components/foo'. Use '@components/foo' instead", + ], + output: "import foo from '@components/foo'", }) expect( lint("import foo from './sub/foo'", { - eslintConfig: { - rules: { - 'self/self': [ - 'error', - { - forSubpaths: true, - }, - ], + eslintConfig, + filename: './components/bar.js', + }), + ).toEqual({ + messages: [ + "Unexpected subpath import './sub/foo'. Use '@components/sub/foo' instead", + ], + output: "import foo from '@components/sub/foo'", + }) + }, + 'alias for subpaths from inside': async () => { + await outputFiles({ + '.babelrc.json': JSON.stringify({ + plugins: [ + [ + packageName`babel-plugin-module-resolver`, + { alias: { '@components': './components' } }, + ], + ], + }), + }) + + const eslintConfig = { + rules: { + 'self/self': [ + 'error', + { + forSubpaths: { + fromInside: true, + }, }, - }, + ], + }, + } + expect( + lint("import foo from './sub/foo'", { + eslintConfig, + filename: './components/bar.js', }), ).toEqual({ messages: [ - "Unexpected subpath import './sub/foo'. Use '@/sub/foo' instead", + "Unexpected subpath import './sub/foo'. Use '@components/sub/foo' instead", ], - output: "import foo from '@/sub/foo'", + output: "import foo from '@components/sub/foo'", }) + expect( + lint("import foo from './components/foo'", { + eslintConfig, + }).messages, + ).toEqual([]) + }, + 'alias for subpaths from outside': async () => { + await outputFiles({ + '.babelrc.json': JSON.stringify({ + plugins: [ + [ + packageName`babel-plugin-module-resolver`, + { alias: { '@components': './components' } }, + ], + ], + }), + }) + + const eslintConfig = { + rules: { + 'self/self': [ + 'error', + { + forSubpaths: { + fromOutside: true, + }, + }, + ], + }, + } + expect( + lint("import foo from './components/foo'", { + eslintConfig, + }), + ).toEqual({ + messages: [ + "Unexpected subpath import './components/foo'. Use '@components/foo' instead", + ], + output: "import foo from '@components/foo'", + }) + expect( + lint("import foo from './sub/foo'", { + eslintConfig, + filename: './components/bar.js', + }).messages, + ).toEqual([]) }, 'alias parent': async () => { await outputFiles({ From d23289d5d2e86da1be96d7604f710c679fbb5479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Bourgier?= Date: Sat, 10 Aug 2024 10:19:42 +0200 Subject: [PATCH 8/8] docs: add section on new options in README --- README.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/README.md b/README.md index 421be91..eb9cbfd 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,57 @@ If you have a special project setup that does not have a babel config in the pro } ``` +### Sibling and subpath aliases + +By default, this plugin enforce relative paths when importing sibling and subpath files (e.g. `import from./sibling` and `import from ./subpath/file`). +You can change this behaviour with the `forSiblings` and `forSubpaths` options: + +```json +"rules": { + "@dword-design/import-alias/prefer-alias": [ + "error", + { + "alias": { + "@": "./src", + "@components: "./components" + }, + "forSiblings": ..., + "forSubpaths": ... + } + ] +} +``` + +#### `forSiblings` + +The `forSiblings` option can take a boolean or an object with a property `forMaxNestingLevel` of type number. + +When setting the option to `true`, all sibling imports will be enforced to use aliases. +When setting the option to an object, you can specify a maximum nesting level for which sibling imports would be enforced. +For example, setting the option to `{ forMaxNestingLevel: 0 }` will enforce aliases for all sibling imports that are at the root level of the project, and enforce relative paths for all other sibling imports. + +Here are some examples, considering `@` as an alias for `.`: + +| | `false` (default) | `true` | `{ forMaxNestingLevel: 0 }` | `{ forMaxNestingLevel: 1 }` | +|------------------------------------------|-------------------|------------------------|-----------------------------|-----------------------------| +| `./foo.js` that `import './bar'` | `import ./bar` | `import @/bar` | `import @/bar` | `import @/bar` | +| `./sub/foo.js` that `import './bar'` | `import ./bar` | `import @/sub/bar` | `import ./bar` | `import @/sub/bar` | +| `./sub/sub/foo.js` that `import './bar'` | `import ./bar` | `import @/sub/sub/bar` | `import ./bar` | `import ./bar` | + +#### `forSubpaths` + +The `forSubpaths` option can take a boolean or an object with the properties `fromInside` and `fromOutside` of type boolean. + +When setting the option to `true`, all subpath imports will be enforced to use aliases. +When setting the option to an object, you can specify whether subpath should be enforced to use aliases or relative paths if the calling file is located inside or outside the alias that match the imported file. + +Here are some examples, considering `@components` as an alias for `./components`: + +| | `false` (default) | `true` | `{ fromInside: true }` | `{ fromOutside: true }` | +|-----------------------------------------------|---------------------------|------------------------------|------------------------------|--------------------------| +| `./foo.js` that `import ./components/bar` | `import ./components/bar` | `import @components/bar` | `import ./components/bar` | `import @components/bar` | +| `./components/foo.js` that `import ./sub/bar` | `import ./sub/bar` | `import @components/sub/bar` | `import @components/sub/bar` | `import ./sub/bar` | + ## Contribute