Skip to content

Commit 4339c09

Browse files
committed
feat: enhance internalImportsRelative rule with depth control and improved alias recommendations
1 parent 4453dba commit 4339c09

File tree

4 files changed

+108
-23
lines changed

4 files changed

+108
-23
lines changed

README.md

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -105,24 +105,24 @@ The plugin will now enforce modular architecture patterns in your Vue.js project
105105

106106
This plugin provides rules to enforce modular architecture boundaries in Vue.js applications. Here is a summary of the available rules:
107107

108-
| Rule | Description |
109-
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
110-
| [app-imports](./docs/rules/app-imports.md) | App folder can import from shared and features with specific exceptions |
111-
| [components-index-required](./docs/rules/components-index-required.md) | All components folders must contain an index.ts file for component exports |
112-
| [cross-imports-alias](./docs/rules/cross-imports-alias.md) | Cross-layer imports must use the project root alias instead of absolute paths |
113-
| [feature-imports](./docs/rules/feature-imports.md) | Features should only import from the shared layer or their own internal files |
114-
| [feature-index-required](./docs/rules/feature-index-required.md) | Each feature folder must contain an index.ts file as its public API |
115-
| [file-component-naming](./docs/rules/file-component-naming.md) | All Vue components must use PascalCase naming |
116-
| [file-ts-naming](./docs/rules/file-ts-naming.md) | All TypeScript files must use camelCase naming |
117-
| [folder-kebab-case](./docs/rules/folder-kebab-case.md) | All folders must use kebab-case naming |
118-
| [internal-imports-relative](./docs/rules/internal-imports-relative.md) | Internal feature/shared/app imports should use relative paths instead of alias |
119-
| [service-filename-no-suffix](./docs/rules/service-filename-no-suffix.md) | Service files must not have Service suffix |
120-
| [sfc-order](./docs/rules/sfc-order.md) | Enforce SFC block order: script, template, style |
121-
| [sfc-required](./docs/rules/sfc-required.md) | All Vue components should be written as Single File Components |
122-
| [shared-imports](./docs/rules/shared-imports.md) | Shared folder cannot import from features or views |
123-
| [shared-ui-index-required](./docs/rules/shared-ui-index-required.md) | The shared/ui folder must contain an index.ts file for UI component exports |
124-
| [store-filename-no-suffix](./docs/rules/store-filename-no-suffix.md) | Store files must not have Store suffix |
125-
| [views-suffix](./docs/rules/views-suffix.md) | View files must end with View.vue suffix |
108+
| Rule | Description |
109+
| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
110+
| [app-imports](./docs/rules/app-imports.md) | App folder can import from shared and features with specific exceptions |
111+
| [components-index-required](./docs/rules/components-index-required.md) | All components folders must contain an index.ts file for component exports |
112+
| [cross-imports-alias](./docs/rules/cross-imports-alias.md) | Cross-layer imports must use the project root alias instead of absolute paths |
113+
| [feature-imports](./docs/rules/feature-imports.md) | Features should only import from the shared layer or their own internal files |
114+
| [feature-index-required](./docs/rules/feature-index-required.md) | Each feature folder must contain an index.ts file as its public API |
115+
| [file-component-naming](./docs/rules/file-component-naming.md) | All Vue components must use PascalCase naming |
116+
| [file-ts-naming](./docs/rules/file-ts-naming.md) | All TypeScript files must use camelCase naming |
117+
| [folder-kebab-case](./docs/rules/folder-kebab-case.md) | All folders must use kebab-case naming |
118+
| [internal-imports-relative](./docs/rules/internal-imports-relative.md) | Prefer relative imports for local feature/shared/app files but suggest alias for deep relative traversals. |
119+
| [service-filename-no-suffix](./docs/rules/service-filename-no-suffix.md) | Service files must not have Service suffix |
120+
| [sfc-order](./docs/rules/sfc-order.md) | Enforce SFC block order: script, template, style |
121+
| [sfc-required](./docs/rules/sfc-required.md) | All Vue components should be written as Single File Components |
122+
| [shared-imports](./docs/rules/shared-imports.md) | Shared folder cannot import from features or views |
123+
| [shared-ui-index-required](./docs/rules/shared-ui-index-required.md) | The shared/ui folder must contain an index.ts file for UI component exports |
124+
| [store-filename-no-suffix](./docs/rules/store-filename-no-suffix.md) | Store files must not have Store suffix |
125+
| [views-suffix](./docs/rules/views-suffix.md) | View files must end with View.vue suffix |
126126

127127
## Contributing
128128

docs/rules/internal-imports-relative.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,30 @@ How it works
1717
- Imports within shared or app when both sides are inside the same `shared` or `app` folders.
1818
- If both source and target are within the project areas of interest but the import uses the project root alias (e.g. `@/features/...` or `@/shared/...`), the rule reports and recommends using a relative import instead.
1919

20+
Behavior details (new/important):
21+
22+
- depth control: the rule accepts a `depth` option (default: `2`) which defines how many `/` segments are allowed for relative imports before the rule suggests switching to the alias. In other words:
23+
- Short relative imports (up to the configured `depth`) are preferred for clearly-local dependencies.
24+
- Long/deep relative imports that exceed `depth` are considered "distant" and the rule will recommend using the project root alias (message id `useAliasImport`).
25+
26+
- Two message types are emitted by the rule:
27+
- `useRelativeImport` — the import uses the project root alias but the target is local (same feature/shared/app); recommend changing to a relative import.
28+
- `useAliasImport` — the import is a relative path that exceeds the configured `depth`; recommend changing to the project root alias for clarity.
29+
30+
- Alias exceptions: certain alias imports remain allowed (no report)
31+
- Alias imports between different features (for example `@/features/featureA/...` importing `@/features/featureB/...`) are permitted.
32+
- Alias imports from `app` files to targets outside `app` (and similarly from `shared` to outside `shared`) are permitted to avoid spurious reports when crossing architectural areas.
33+
34+
- Path normalization for monorepo / nested-app layouts: filenames like `apps/web/src/...` are normalized to the configured `rootPath` (for example `src/...`) before rule logic is applied. That means files under `apps/web/src` are treated the same as `src` files for the purposes of this rule.
35+
2036
## Options
2137

2238
The rule accepts an options object with the following properties:
2339

2440
- `ignores` (string[], default: `[]`) — array of minimatch glob patterns tested against the resolved project-relative filename. If any pattern matches, the rule will skip that file. Use this to permit exceptions during migration or for generated files.
2541

42+
- `depth` (number, default: `2`) — maximum number of `/` segments allowed for a relative import before the rule recommends switching to the project root alias. Increase this value if your feature layout commonly requires deeper relative traversals and you want to prefer relative imports more often.
43+
2644
Note: project-level settings control `rootPath`, `rootAlias`, `appPath`, `featuresPath`, and `sharedPath` used by the rule; those defaults are provided by the plugin and can be overridden via settings.
2745

2846
### Example configuration
@@ -62,6 +80,24 @@ import { theme } from '@/shared/utils/theme'
6280
```
6381

6482
Correct:
83+
Incorrect (relative import is too deep; prefer alias):
84+
85+
```ts
86+
// File: src/features/auth/components/very/deep/path/File.vue
87+
import helper from '../../../utils/helper' // many ../ segments
88+
```
89+
90+
Correct (use alias when relative traversal is deep):
91+
92+
```ts
93+
// File: src/features/auth/components/very/deep/path/File.vue
94+
import helper from '@/features/auth/utils/helper'
95+
```
96+
97+
Notes:
98+
99+
- The rule will still allow alias imports when crossing feature boundaries or when the import target is outside the same architectural area (see "Alias exceptions" above).
100+
- Files that cannot be resolved to the project root (for example external scripts) are ignored; use the `ignores` option for explicit exceptions.
65101

66102
```ts
67103
// File: src/shared/components/Button.vue

src/rules/internal-imports-relative.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { createRule, parseRuleOptions, parseProjectOptions, resolvePath, isIgnored } from '../utils'
1+
import { createRule, parseRuleOptions, parseProjectOptions, resolvePath, isIgnored, normalizePath } from '../utils'
22
import type { VueModularRuleModule, VueModularRuleContext } from '../types'
33
import { resolveImportPath } from '../utils'
44

55
const defaultOptions = {
6+
depth: 2,
67
ignores: [] as string[],
78
}
89

@@ -26,25 +27,53 @@ export const internalImportsRelative = createRule<VueModularRuleModule>({
2627
const alias = projectOptions.rootAlias
2728
const isAliasImport = importPath === alias || importPath.startsWith(`${alias}/`)
2829

30+
let depthSegments = 0
31+
if (!isAliasImport && importPath.startsWith('.')) {
32+
let path = normalizePath(importPath)
33+
depthSegments = path.split('/').length - 1
34+
}
35+
2936
// Only care about imports within the same feature or same shared folder
3037
const fromFeature = filename.startsWith(projectOptions.featuresPath)
3138
const toFeature = resolvedPath.startsWith(projectOptions.featuresPath)
3239
if (fromFeature && toFeature) {
3340
const fromFeatureSegment = filename.slice(projectOptions.featuresPath.length).split('/')[1]
3441
const toFeatureSegment = resolvedPath.slice(projectOptions.featuresPath.length).split('/')[1]
35-
if (fromFeatureSegment && toFeatureSegment && fromFeatureSegment === toFeatureSegment && !isAliasImport) return
42+
43+
if (fromFeatureSegment && toFeatureSegment && fromFeatureSegment === toFeatureSegment && !isAliasImport) {
44+
if (depthSegments <= options.depth) return
45+
context.report({
46+
node,
47+
messageId: 'useAliasImport',
48+
data: { file: filename, target: importPath, distance: String(depthSegments) },
49+
})
50+
}
3651
if (fromFeatureSegment && toFeatureSegment && fromFeatureSegment !== toFeatureSegment && isAliasImport) return
3752
}
3853

3954
// Care about app and shared folders as well
4055
const fromApp = filename.startsWith(projectOptions.appPath)
4156
const toApp = resolvedPath.startsWith(projectOptions.appPath)
42-
if (fromApp && toApp && !isAliasImport) return
57+
if (fromApp && toApp && !isAliasImport) {
58+
if (depthSegments <= options.depth) return
59+
context.report({
60+
node,
61+
messageId: 'useAliasImport',
62+
data: { file: filename, target: importPath, distance: String(depthSegments) },
63+
})
64+
}
4365
if (fromApp && !toApp && isAliasImport) return
4466

4567
const fromShared = filename.startsWith(projectOptions.sharedPath)
4668
const toShared = resolvedPath.startsWith(projectOptions.sharedPath)
47-
if (fromShared && toShared && !isAliasImport) return
69+
if (fromShared && toShared && !isAliasImport) {
70+
if (depthSegments <= options.depth) return
71+
context.report({
72+
node,
73+
messageId: 'useAliasImport',
74+
data: { file: filename, target: importPath, distance: String(depthSegments) },
75+
})
76+
}
4877
if (fromShared && !toShared && isAliasImport) return
4978

5079
// If import is outside of app, features, or shared, ignore
@@ -71,13 +100,15 @@ export const internalImportsRelative = createRule<VueModularRuleModule>({
71100
{
72101
type: 'object',
73102
properties: {
103+
depth: { type: 'number' },
74104
ignores: { type: 'array', items: { type: 'string' } },
75105
},
76106
additionalProperties: false,
77107
},
78108
],
79109
messages: {
80110
useRelativeImport: 'Use relative import for same-feature or nearby file imports ({{ file }} -> {{ target }}).',
111+
useAliasImport: 'Use alias import for distant ({{ distance }} levels) file imports ({{ file }} -> {{ target }}).',
81112
},
82113
},
83114
})

tests/rules/internal-imports-relative.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ describe('internal-imports-relative', () => {
6767
// Alias import within the same feature
6868
{
6969
filename: '/project/src/features/featureA/fileA.ts',
70-
code: "<script>import x from '@/features/featureA/fileB'",
70+
code: "<script>import x from '@/features/featureA/fileB'</script>",
7171
errors: [{ messageId: 'useRelativeImport' }],
7272
},
7373
// Alias import within the same feature (subfolder)
@@ -82,6 +82,24 @@ describe('internal-imports-relative', () => {
8282
code: "<script setup>import { cn } from '@/shared/utils/cn'</script>",
8383
errors: [{ messageId: 'useRelativeImport' }],
8484
},
85+
// Relative import within the same feature but too deep -> should report useAliasImport
86+
{
87+
filename: '/project/src/features/featureA/sub/sub2/fileA.ts',
88+
code: "<script>import x from '../aaa/bbb/fileB'</script>",
89+
errors: [{ messageId: 'useAliasImport' }],
90+
},
91+
// Relative import within the app but too deep -> should report useAliasImport
92+
{
93+
filename: 'src/app/sub1/sub2/fileA.ts',
94+
code: "<script>import x from '../aaa/bbb/fileB'</script>",
95+
errors: [{ messageId: 'useAliasImport' }],
96+
},
97+
// Relative import within shared but too deep -> should report useAliasImport
98+
{
99+
filename: '/project/src/shared/ui/button/iButton.vue',
100+
code: "<script setup>import { cn } from '../../utils/cn'</script>",
101+
errors: [{ messageId: 'useAliasImport' }],
102+
},
85103
],
86104
})
87105
})

0 commit comments

Comments
 (0)