Skip to content
Merged

Next #37

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ If you have suggestions for new rules or improvements, please open an issue or w
| ------------------------------------------------------------------------ | ----------------------------------------------------------------------------- |
| [app-imports](./docs/rules/app-imports.md) | App folder can import from shared and features with specific exceptions |
| [components-index-required](./docs/rules/components-index-required.md) | All components folders must contain an index.ts file for component exports |
| [cross-imports-absolute](./docs/rules/cross-imports-absolute.md) | Cross-layer imports must use the project root alias instead of absolute paths |
| [feature-imports](./docs/rules/feature-imports.md) | Features should only import from the shared layer or their own internal files |
| [feature-index-required](./docs/rules/feature-index-required.md) | Each feature folder must contain an index.ts file as its public API |
| [file-component-naming](./docs/rules/file-component-naming.md) | All Vue components must use PascalCase naming |
Expand Down
90 changes: 90 additions & 0 deletions docs/rules/cross-imports-absolute.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
````markdown
# vue-modular/cross-imports-absolute

Enforce that cross-layer imports (between features or from features to shared) must use the project root alias (e.g. `@/`) instead of non-aliased absolute paths or relative paths that cross architectural boundaries.

## Rule Details

This rule flags import declarations that reference other parts of the project using non-aliased absolute paths (like `src/features/...` or `/features/...`) or relative paths that traverse outside the current architectural layer. Cross-layer imports should use the configured root alias to maintain consistency and prevent coupling to internal directory structures.

The rule allows relative imports within the same architectural layer (same feature, within shared, or within app) but requires alias imports when crossing these boundaries.

How it works

- Resolve the current filename to a project-relative path using the plugin project options.
- For each import, resolve the import specifier to a normalized project-relative path. If resolution fails (external package, unresolved alias) skip the import.
- Determine the architectural layers of both the source file and target import (app, feature, shared).
- Allow imports within the same layer:
- Same feature: files within the same feature subdirectory can use relative imports
- Within shared: files in shared can import other shared files
- Within app: files in app can import other app files
- For cross-layer imports, require that the import uses the configured root alias (e.g. `@/shared/utils` instead of `src/shared/utils` or `../../shared/utils`).

## Options

The rule accepts an optional options object with the following properties:

- `ignores` (string[], default: `[]`) — minimatch patterns that skip files from rule checks when they match the resolved project-relative filename. Use this to exempt specific files or directories from the alias requirement.

Note: project-level settings control `rootPath`, `rootAlias`, `appPath`, `featuresPath`, and `sharedPath` used by the rule. Configure these via `settings['vue-modular']`.

### Example configuration

```js
{
"vue-modular/cross-imports-absolute": ["error"]
}

// ignore specific legacy files
{
"vue-modular/cross-imports-absolute": ["error", { "ignores": ["src/legacy/**"] }]
}
```

## Examples

Incorrect (non-aliased cross-layer imports):

```ts
// File: src/features/auth/components/LoginForm.vue
import { util } from 'src/shared/utils' // should use alias
import { charge } from '/features/payments/utils/api' // should use alias
import svc from '../../../shared/services/api' // should use alias
```

```ts
// File: src/app/main.ts
import authRoutes from '../features/auth/routes' // should use alias
```

Correct (use alias for cross-layer, relative within same layer):

```ts
// File: src/features/auth/components/LoginForm.vue
import { util } from '@/shared/utils' // correct alias usage
import helper from '../utils/helper' // correct relative within same feature
import { config } from '@/features/auth/config' // correct alias for same feature
```

```ts
// File: src/app/main.ts
import authRoutes from '@/features/auth/routes' // correct alias usage
import layout from './layouts/MainLayout' // correct relative within app
```

```ts
// File: src/shared/components/Button.vue
import { theme } from '../utils/theme' // correct relative within shared
import { config } from '@/shared/config' // correct alias within shared
```

## When Not To Use

- Disable this rule if your project intentionally allows non-aliased absolute imports or doesn't enforce consistent import styles across architectural layers.
- Consider disabling during large refactoring efforts where maintaining import consistency is less important than completing the migration.

## Implementation notes

- The rule reports with `messageId: 'useAlias'` and includes `file` (project-relative filename), `target` (import specifier), and `alias` (configured root alias) in the message data.
- Empty import strings are handled gracefully as a defensive measure against malformed AST nodes.
````
145 changes: 2 additions & 143 deletions docs/vue-modular-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,150 +211,9 @@ src/
└── utilities.css # Utility classes
```

## Rules
## ESLint Plugin Integration

### File Organization Rules

- All Vue components must use PascalCase naming (e.g., `UserForm.vue`, `ProductList.vue`)
- All TypeScript files must use camelCase naming (e.g., `useAuth.ts`, `userApi.ts`)
- All folders must use kebab-case naming (e.g., `user-management/`, `auth/`)
- Each feature folder must contain an `index.ts` file as its public API
- All `components/` folders must contain an `index.ts` file for component exports
- The `shared/ui/` folder must contain an `index.ts` file for UI component exports

### Dependency Rules

- Features cannot import from other features directly
- Features can only import from `shared/` folder
- `shared/` folder cannot import from `features/` or `views/`
- `app/` folder can import from `shared/` and `features/` (exception: `app/router.ts` may import feature route files to compose the global router)
- All cross-feature communication must go through the `shared/` layer

### Component Rules

- All Vue components should be written as Single File Components (SFC) with `.vue` extension
- SFC blocks must be ordered: `<script>`, `<template>`, `<style>` (at least one of script or template must exist)
- Layout-specific components must be in `app/components/`
- Reusable UI components (design system) must be in `shared/ui/`
- Business components used across features must be in `shared/components/`
- Feature-specific components must be in `features/{feature}/components/`
- Component props must be typed with TypeScript interfaces

### Service Rules

- Cross-cutting services must be in `shared/services/`
- Feature-specific services must be in `features/{feature}/services/`
- Service files must not have "Service" suffix (e.g., `auth.ts`, not `authService.ts`)
- Services must export named classes or named functions (avoid default exports)
- API services must use the shared `apiClient.ts`

### Store Rules

- Global state must be in `shared/stores/`
- Feature-specific state must be in `features/{feature}/stores/`
- Store files must use Pinia composition API syntax
- Store files must not have "Store" suffix (e.g., `auth.ts`, not `authStore.ts`)
- Cross-cutting concerns (auth, notifications) must be in `shared/stores/`
- Feature stores cannot import other feature stores directly

### Type Rules

- Global types must be in `shared/types/`
- Feature-specific types must be in `features/{feature}/types/`
- Type files must export interfaces and types, not classes
- Common utility types must be in `shared/types/common.ts`
- API response types must be in `shared/types/api.ts`

### Routing Rules

- Global routes must be in `app/router.ts`
- Feature routes must be in `features/{feature}/routes.ts`
- Feature routes must be imported and merged in `app/router.ts`
- Route components must be lazy-loaded using dynamic imports
- Layout selection must be defined in route `meta.layout` property

### View Rules

- Feature views must be in `features/{feature}/views/`
- Global views (not feature-specific) must be in `views/` folder
- View files must end with `View.vue` suffix
- Views cannot contain business logic (delegate to composables/services)
- Layout selection must be defined in route metadata, not imported directly in views

### Composable Rules

- Global composables must be in `shared/composables/`
- Feature composables must be in `features/{feature}/composables/`
- Composable files must start with `use` prefix
- Composables must return reactive refs and computed properties
- Composables cannot directly manipulate DOM elements

### Asset Rules

- Global styles must be in `assets/styles/`
- Images must be in `assets/images/`
- Icons must be in `assets/icons/`
- Fonts must be in `assets/fonts/`
- Component-specific styles must be scoped within component files

### Utility Rules

- Global utilities must be in `shared/utils/`
- Feature-specific utilities must be in `features/{feature}/utils/`
- Utility files must export pure functions without side effects
- Utility functions must be stateless and deterministic

### Middleware Rules

- Global route middleware must be in `app/middleware/`
- Feature-specific middleware must be in `features/{feature}/middleware/`
- Middleware files must use descriptive names (e.g., `authGuard.ts`, not `guard.ts`)
- Route guards must be registered in feature route configurations
- Middleware must be composable and chainable

### Plugin Rules

- Global plugin registration must be in `app/plugins.ts`
- Plugin configurations must be environment-aware
- Third-party plugins must be initialized before app mounting
- Custom plugins must follow Vue.js plugin API conventions
- Plugin dependencies must be clearly documented

### Environment/Config Rules

- Environment configurations must be type-safe
- Configuration files must use `.env` files for environment variables
- Sensitive data must not be committed to version control
- Different environments (dev/staging/prod) must have separate config handling
- Runtime configuration must be validated at application startup
- App configurations must be in `app/config/` folder
- Config files must export typed configuration objects

### Import Rules

- Use absolute imports with `@/` alias for cross-layer imports and shared resources
- Use relative imports for same-feature or nearby file imports (within 2 levels)
- Avoid relative imports with more than 2 levels (`../../../`) - use absolute instead
- Import from `index.ts` files when available
- Group imports: Vue imports, third-party imports, internal imports
- Type imports must use `import type` syntax
- Avoid deep imports into feature internals

### Export Rules

- Use named exports instead of default exports for better tree-shaking
- Internal feature helpers should not be exported from their modules
- Types must be exported with `export type` syntax
- Components must use default exports and be re-exported as named exports in `index.ts`

### Naming Conventions

- Use camelCase for variables, function names, and named exports (e.g., `fetchUsers`, `getUserById`).
- Use PascalCase for exported types, interfaces, classes, and component identifiers (e.g., `User`, `AuthState`, `UserCard`). Avoid `I` prefixes on interfaces
- Prefix composable functions with `use` and keep them camelCase (e.g., `useAuth`, `useProductForm`).
- Pinia store factory exports should follow Pinia's recommendation: start with `use` and include the `Store` suffix (e.g., `useAuthStore`, `useNotificationsStore`)
- Use UPPER_SNAKE_CASE for compile-time constants and environment keys (e.g., `API_TIMEOUT_MS`) and camelCase for runtime constants.
- Component-emitted custom event names should use kebab-case (e.g., `item-selected`, `save:success`).
To enforce modular architecture patterns, use the `eslint-plugin-vue-modular` plugin. This plugin provides rules to ensure that your Vue.js project adheres to the modular architecture principles outlined above.

## Conclusion

Expand Down
2 changes: 2 additions & 0 deletions src/rules.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { componentsIndexRequired } from './rules/components-index-required'
import { featureImports } from './rules/feature-imports'
import { crossImportsAbsolute } from './rules/cross-imports-absolute'
import { featureIndexRequired } from './rules/feature-index-required'
import { fileComponentNaming } from './rules/file-component-naming'
import { fileTsNaming } from './rules/file-ts-naming'
Expand All @@ -25,6 +26,7 @@ export const rules: Record<string, VueModularRuleModule> = {
'app-imports': appImports,
'service-filename-no-suffix': serviceFilenameNoSuffix,
'store-filename-no-suffix': storeFilenameNoSuffix,
'cross-imports-absolute': crossImportsAbsolute,
'feature-imports': featureImports,
'views-suffix': viewsSuffix,
}
82 changes: 82 additions & 0 deletions src/rules/cross-imports-absolute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { createRule, parseRuleOptions, parseProjectOptions, resolvePath, isIgnored } from '../utils'
import type { VueModularRuleModule, VueModularRuleContext } from '../types'
import { resolveImportPath } from '../utils/resolveImportPath'

const defaultOptions = {
ignores: [] as string[],
}

// Enforce that when importing across layers (e.g., feature -> feature or feature -> shared)
// the import must use the project root alias (e.g. '@' by default) or be relative when inside
// the same module. This rule reports imports that reference other parts of the project using
// non-aliased absolute or other non-allowed forms.
export const crossImportsAbsolute = createRule<VueModularRuleModule>({
create(context: VueModularRuleContext) {
const options = parseRuleOptions(context, defaultOptions as unknown as Record<string, unknown>) as typeof defaultOptions
const projectOptions = parseProjectOptions(context)
const filename = resolvePath(context.filename, projectOptions.rootPath, projectOptions.rootAlias)
if (!filename) return {}
if (isIgnored(filename, options.ignores)) return {}

return {
ImportDeclaration(node) {
const importPath = node.source.value as string
// Defensive: AST nodes without a string source are extremely rare in our tests
if (!importPath || typeof importPath !== 'string') return

let resolvedPath = resolveImportPath(filename, importPath, projectOptions.rootPath, projectOptions.rootAlias)
if (!resolvedPath) return

// If import is within same logical area (same feature or both in shared), allow
const fromApp = filename.startsWith(projectOptions.appPath)
const toApp = resolvedPath.startsWith(projectOptions.appPath)
if (fromApp && toApp) return

const fromFeature = filename.startsWith(projectOptions.featuresPath)
const toFeature = resolvedPath.startsWith(projectOptions.featuresPath)
// same feature -> allow (could be relative or alias)
if (fromFeature && toFeature) {
const fromFeatureSegment = filename.slice(projectOptions.featuresPath.length).split('/')[1]
const toFeatureSegment = resolvedPath.slice(projectOptions.featuresPath.length).split('/')[1]
if (fromFeatureSegment && toFeatureSegment && fromFeatureSegment === toFeatureSegment) return
}

const fromShared = filename.startsWith(projectOptions.sharedPath)
const toShared = resolvedPath.startsWith(projectOptions.sharedPath)
if (fromShared && toShared) return

// Determine if this import is a non-aliased absolute import (e.g. '/features/..' or 'src/features/...')
const alias = projectOptions.rootAlias
const isAliasImport = importPath === alias || importPath.startsWith(`${alias}/`)

if (!isAliasImport) {
context.report({ node, messageId: 'useAlias', data: { file: filename, target: importPath, alias } })
}
},
}
},
name: 'cross-imports-absolute',
recommended: false,
level: 'error',
meta: {
type: 'problem',
docs: {
description: 'Cross-layer imports must use the project root alias (e.g. @/) instead of non-aliased absolute paths',
category: 'Dependency',
recommended: false,
},
defaultOptions: [defaultOptions],
schema: [
{
type: 'object',
properties: {
ignores: { type: 'array', items: { type: 'string' } },
},
additionalProperties: false,
},
],
messages: {
useAlias: "File '{{file}}' must import cross-layer resources using the alias '{{alias}}' (found '{{target}}').",
},
},
})
Loading