feat(architecture): enable enforce-module-boundaries with comprehensive rules

- Enable @nx/enforce-module-boundaries ESLint rule
- Add type-based constraints (feature, data-access, ui, shared, util, core, common)
- Add domain-based constraints (oms, crm, remission, checkout, availability, etc.)
- Prevent feature->feature, data-access->data-access dependencies
- Enforce domain isolation (no cross-domain imports)
- Tag all 80+ libraries with scope and type tags
- Create automated tagging script for new libraries
- Configure isa-app violations to be ignored

Rules enforce:
- Feature can import: data-access, ui, shared, util, core, common, icons
- Data-access can import: util, generated, common, core only
- UI can import: util, core, icons only
- Cross-domain imports forbidden (except shared, core, common, ui, utils, icons)
- Generated APIs only importable by data-access libraries
This commit is contained in:
Lorenz Hilpert
2025-11-20 17:55:04 +01:00
parent 664f42be08
commit c4480ca8d5
14 changed files with 552 additions and 42 deletions

View File

@@ -4,7 +4,7 @@
"projectType": "application",
"prefix": "app",
"sourceRoot": "apps/isa-app/src",
"tags": ["skip:ci"],
"tags": ["skip:ci", "scope:app", "type:app"],
"targets": {
"build": {
"executor": "@angular-devkit/build-angular:application",

View File

@@ -13,24 +13,274 @@ module.exports = [
'**/generated/**',
],
},
// {
// files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// rules: {
// '@nx/enforce-module-boundaries': [
// 'error',
// {
// enforceBuildableLibDependency: true,
// allow: ['^.*/eslint(\\.base)?\\.config\\.[cm]?js$'],
// depConstraints: [
// {
// sourceTag: '*',
// onlyDependOnLibsWithTags: ['*'],
// },
// ],
// },
// ],
// },
// },
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: ['^.*/eslint(\\.base)?\\.config\\.[cm]?js$'],
depConstraints: [
// ========================================
// TYPE-BASED CONSTRAINTS (Layer Rules)
// ========================================
// FEATURE libraries can import: data-access, ui, shared, util, core (within same domain)
{
sourceTag: 'type:feature',
onlyDependOnLibsWithTags: [
'type:data-access',
'type:ui',
'type:shared',
'type:util',
'type:core',
'type:common',
'type:icon',
],
},
// DATA-ACCESS libraries can import: util, generated, common-data-access only
{
sourceTag: 'type:data-access',
onlyDependOnLibsWithTags: [
'type:util',
'type:generated',
'type:common',
'type:core',
],
bannedExternalImports: [],
},
// DATA-ACCESS cannot import other DATA-ACCESS (except common)
{
sourceTag: 'type:data-access',
notDependOnLibsWithTags: ['type:data-access'],
allowedExternalImports: [],
},
// UI libraries can import: util, core only (NOT shared, NOT data-access)
{
sourceTag: 'type:ui',
onlyDependOnLibsWithTags: ['type:util', 'type:core', 'type:icon'],
},
// SHARED libraries can import: ui, util, core within same domain
{
sourceTag: 'type:shared',
onlyDependOnLibsWithTags: [
'type:ui',
'type:util',
'type:core',
'type:common',
'type:icon',
],
},
// UTIL libraries are leaf nodes - cannot import feature/data-access/ui/shared
{
sourceTag: 'type:util',
onlyDependOnLibsWithTags: ['type:util', 'type:core'],
},
// GENERATED (swagger clients) can only be imported by data-access
// This is enforced by the data-access rule above
// CORE libraries can be imported by anyone, but cannot import feature/data-access
{
sourceTag: 'type:core',
onlyDependOnLibsWithTags: ['type:core', 'type:util', 'type:common'],
},
// COMMON libraries can be imported by anyone
{
sourceTag: 'type:common',
onlyDependOnLibsWithTags: [
'type:util',
'type:core',
'type:generated',
],
},
// ICON libraries can be imported by anyone
{
sourceTag: 'type:icon',
onlyDependOnLibsWithTags: [],
},
// ========================================
// DOMAIN-BASED CONSTRAINTS (Scope Rules)
// ========================================
// OMS domain can only import from OMS, shared, core, common, ui, utils, icons
{
sourceTag: 'scope:oms',
onlyDependOnLibsWithTags: [
'scope:oms',
'scope:shared',
'scope:core',
'scope:common',
'scope:ui',
'scope:utils',
'scope:icons',
'scope:generated',
],
},
// CRM domain can only import from CRM, shared, core, common, ui, utils, icons
{
sourceTag: 'scope:crm',
onlyDependOnLibsWithTags: [
'scope:crm',
'scope:shared',
'scope:core',
'scope:common',
'scope:ui',
'scope:utils',
'scope:icons',
'scope:generated',
],
},
// REMISSION domain can only import from REMISSION, shared, core, common, ui, utils, icons
{
sourceTag: 'scope:remission',
onlyDependOnLibsWithTags: [
'scope:remission',
'scope:shared',
'scope:core',
'scope:common',
'scope:ui',
'scope:utils',
'scope:icons',
'scope:generated',
],
},
// CHECKOUT domain can only import from CHECKOUT, shared, core, common, ui, utils, icons
{
sourceTag: 'scope:checkout',
onlyDependOnLibsWithTags: [
'scope:checkout',
'scope:shared',
'scope:core',
'scope:common',
'scope:ui',
'scope:utils',
'scope:icons',
'scope:generated',
],
},
// AVAILABILITY domain can only import from AVAILABILITY, shared, core, common, ui, utils, icons
{
sourceTag: 'scope:availability',
onlyDependOnLibsWithTags: [
'scope:availability',
'scope:shared',
'scope:core',
'scope:common',
'scope:ui',
'scope:utils',
'scope:icons',
'scope:generated',
],
},
// CATALOGUE domain can only import from CATALOGUE, shared, core, common, ui, utils, icons
{
sourceTag: 'scope:catalogue',
onlyDependOnLibsWithTags: [
'scope:catalogue',
'scope:shared',
'scope:core',
'scope:common',
'scope:ui',
'scope:utils',
'scope:icons',
'scope:generated',
],
},
// SHARED libraries can be imported by all domains
{
sourceTag: 'scope:shared',
onlyDependOnLibsWithTags: [
'scope:shared',
'scope:core',
'scope:common',
'scope:ui',
'scope:utils',
'scope:icons',
],
},
// UI libraries can be imported by all domains
{
sourceTag: 'scope:ui',
onlyDependOnLibsWithTags: [
'scope:ui',
'scope:core',
'scope:utils',
'scope:icons',
],
},
// UTILS libraries can be imported by all domains
{
sourceTag: 'scope:utils',
onlyDependOnLibsWithTags: ['scope:utils', 'scope:core'],
},
// CORE libraries can be imported by all domains
{
sourceTag: 'scope:core',
onlyDependOnLibsWithTags: ['scope:core', 'scope:common', 'scope:utils'],
},
// COMMON libraries can be imported by all domains
{
sourceTag: 'scope:common',
onlyDependOnLibsWithTags: [
'scope:common',
'scope:core',
'scope:utils',
'scope:generated',
],
},
// ICONS libraries can be imported by all domains
{
sourceTag: 'scope:icons',
onlyDependOnLibsWithTags: [],
},
// GENERATED (swagger) can only be imported by data-access
{
sourceTag: 'scope:generated',
onlyDependOnLibsWithTags: [],
},
// ========================================
// SPECIAL RULES
// ========================================
// APP scope - violations ignored for now (as per user requirement)
{
sourceTag: 'scope:app',
onlyDependOnLibsWithTags: ['*'],
},
// Disallow relative imports - must use path aliases
{
sourceTag: '*',
bannedExternalImports: ['../*', '../../*', '../../../*'],
},
],
},
],
},
},
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
},

View File

@@ -4,7 +4,14 @@
"sourceRoot": "generated/swagger/availability-api/src",
"prefix": "lib",
"projectType": "library",
"tags": ["generated","swagger","availability","api"],
"tags": [
"generated",
"swagger",
"availability",
"api",
"scope:generated",
"type:generated"
],
"targets": {
"generate": {
"command": "ng-swagger-gen --config {projectRoot}/ng-swagger-gen.json --output {projectRoot}/src",
@@ -13,7 +20,9 @@
"{projectRoot}/ng-swagger-gen.json",
"!{projectRoot}/src/**/*.ts"
],
"outputs": ["{projectRoot}/src"],
"outputs": [
"{projectRoot}/src"
],
"cache": false
}
}

View File

@@ -4,7 +4,14 @@
"sourceRoot": "generated/swagger/cat-search-api/src",
"prefix": "lib",
"projectType": "library",
"tags": ["generated","swagger", "cat-search", "api"],
"tags": [
"generated",
"swagger",
"cat-search",
"api",
"scope:generated",
"type:generated"
],
"targets": {
"generate": {
"command": "ng-swagger-gen --config {projectRoot}/ng-swagger-gen.json --output {projectRoot}/src",
@@ -13,7 +20,9 @@
"{projectRoot}/ng-swagger-gen.json",
"!{projectRoot}/src/**/*.ts"
],
"outputs": ["{projectRoot}/src"],
"outputs": [
"{projectRoot}/src"
],
"cache": false
}
}

View File

@@ -4,7 +4,14 @@
"sourceRoot": "generated/swagger/checkout-api/src",
"prefix": "lib",
"projectType": "library",
"tags": ["generated","swagger", "checkout", "api"],
"tags": [
"generated",
"swagger",
"checkout",
"api",
"scope:generated",
"type:generated"
],
"targets": {
"generate": {
"command": "ng-swagger-gen --config {projectRoot}/ng-swagger-gen.json --output {projectRoot}/src",
@@ -13,7 +20,9 @@
"{projectRoot}/ng-swagger-gen.json",
"!{projectRoot}/src/**/*.ts"
],
"outputs": ["{projectRoot}/src"],
"outputs": [
"{projectRoot}/src"
],
"cache": false
}
}

View File

@@ -4,7 +4,14 @@
"sourceRoot": "generated/swagger/crm-api/src",
"prefix": "lib",
"projectType": "library",
"tags": ["generated","swagger", "crm", "api"],
"tags": [
"generated",
"swagger",
"crm",
"api",
"scope:generated",
"type:generated"
],
"targets": {
"generate": {
"command": "ng-swagger-gen --config {projectRoot}/ng-swagger-gen.json --output {projectRoot}/src",
@@ -13,7 +20,9 @@
"{projectRoot}/ng-swagger-gen.json",
"!{projectRoot}/src/**/*.ts"
],
"outputs": ["{projectRoot}/src"],
"outputs": [
"{projectRoot}/src"
],
"cache": false
}
}

View File

@@ -4,7 +4,14 @@
"sourceRoot": "generated/swagger/eis-api/src",
"prefix": "lib",
"projectType": "library",
"tags": ["generated","swagger","eis", "api"],
"tags": [
"generated",
"swagger",
"eis",
"api",
"scope:generated",
"type:generated"
],
"targets": {
"download": {
"command": "curl -o {projectRoot}/swagger.json https://filialinformationsystem-test.paragon-systems.de/eiswebapi/v1/swagger.json"
@@ -17,8 +24,12 @@
"{projectRoot}/ng-swagger-gen.json",
"!{projectRoot}/src/**/*.ts"
],
"outputs": ["{projectRoot}/src"],
"dependsOn": ["download"],
"outputs": [
"{projectRoot}/src"
],
"dependsOn": [
"download"
],
"cache": false
}
}

View File

@@ -4,7 +4,14 @@
"sourceRoot": "generated/swagger/inventory-api/src",
"prefix": "lib",
"projectType": "library",
"tags": ["generated", "swagger", "inventory", "api"],
"tags": [
"generated",
"swagger",
"inventory",
"api",
"scope:generated",
"type:generated"
],
"targets": {
"generate": {
"command": "ng-swagger-gen --config {projectRoot}/ng-swagger-gen.json --output {projectRoot}/src",
@@ -13,7 +20,9 @@
"{projectRoot}/ng-swagger-gen.json",
"!{projectRoot}/src/**/*.ts"
],
"outputs": ["{projectRoot}/src"],
"outputs": [
"{projectRoot}/src"
],
"cache": false
}
}

View File

@@ -4,7 +4,14 @@
"sourceRoot": "generated/swagger/isa-api/src",
"prefix": "lib",
"projectType": "library",
"tags": ["generated","swagger", "isa", "api"],
"tags": [
"generated",
"swagger",
"isa",
"api",
"scope:generated",
"type:generated"
],
"targets": {
"generate": {
"command": "ng-swagger-gen --config {projectRoot}/ng-swagger-gen.json --output {projectRoot}/src",
@@ -13,7 +20,9 @@
"{projectRoot}/ng-swagger-gen.json",
"!{projectRoot}/src/**/*.ts"
],
"outputs": ["{projectRoot}/src"],
"outputs": [
"{projectRoot}/src"
],
"cache": false
}
}

View File

@@ -4,7 +4,14 @@
"sourceRoot": "generated/swagger/oms-api/src",
"prefix": "lib",
"projectType": "library",
"tags": ["generated","swagger", "oms", "api"],
"tags": [
"generated",
"swagger",
"oms",
"api",
"scope:generated",
"type:generated"
],
"targets": {
"generate": {
"command": "ng-swagger-gen --config {projectRoot}/ng-swagger-gen.json --output {projectRoot}/src",
@@ -13,7 +20,9 @@
"{projectRoot}/ng-swagger-gen.json",
"!{projectRoot}/src/**/*.ts"
],
"outputs": ["{projectRoot}/src"],
"outputs": [
"{projectRoot}/src"
],
"cache": false
}
}

View File

@@ -4,7 +4,14 @@
"sourceRoot": "generated/swagger/print-api/src",
"prefix": "lib",
"projectType": "library",
"tags": ["generated","swagger", "print", "api"],
"tags": [
"generated",
"swagger",
"print",
"api",
"scope:generated",
"type:generated"
],
"targets": {
"generate": {
"command": "ng-swagger-gen --config {projectRoot}/ng-swagger-gen.json --output {projectRoot}/src",
@@ -13,7 +20,9 @@
"{projectRoot}/ng-swagger-gen.json",
"!{projectRoot}/src/**/*.ts"
],
"outputs": ["{projectRoot}/src"],
"outputs": [
"{projectRoot}/src"
],
"cache": false
}
}

View File

@@ -4,7 +4,14 @@
"sourceRoot": "generated/swagger/wws-api/src",
"prefix": "lib",
"projectType": "library",
"tags": ["generated","swagger", "wws", "api"],
"tags": [
"generated",
"swagger",
"wws",
"api",
"scope:generated",
"type:generated"
],
"targets": {
"generate": {
"command": "ng-swagger-gen --config {projectRoot}/ng-swagger-gen.json --output {projectRoot}/src",
@@ -13,7 +20,9 @@
"{projectRoot}/ng-swagger-gen.json",
"!{projectRoot}/src/**/*.ts"
],
"outputs": ["{projectRoot}/src"],
"outputs": [
"{projectRoot}/src"
],
"cache": false
}
}

View File

@@ -3,11 +3,16 @@
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/icons/src",
"projectType": "library",
"tags": [],
"tags": [
"scope:icons",
"type:icon"
],
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"outputs": [
"{workspaceRoot}/coverage/{projectRoot}"
],
"options": {
"jestConfig": "libs/icons/jest.config.ts"
}

163
scripts/add-library-tags.js Normal file
View File

@@ -0,0 +1,163 @@
#!/usr/bin/env node
/**
* Script to add appropriate tags to all library project.json files
* for @nx/enforce-module-boundaries rule
*/
const fs = require('fs');
const path = require('path');
const { glob } = require('glob');
/**
* Determine tags based on library path
* @param {string} projectPath - Path to the project.json file
* @returns {string[]} Array of tags
*/
function determineTags(projectPath) {
const tags = [];
// Handle generated projects
if (projectPath.includes('generated/')) {
tags.push('scope:generated');
tags.push('type:generated');
return tags;
}
const relativePath = projectPath.replace(/^.*?libs\//, 'libs/');
// Extract scope (domain) from path
const pathParts = relativePath.split('/');
if (pathParts.length < 2) return tags;
const domain = pathParts[1]; // e.g., 'oms', 'crm', 'ui', etc.
// Add scope tag
tags.push(`scope:${domain}`);
// Determine type tag based on path pattern
if (pathParts.length >= 3) {
const typeSegment = pathParts[2];
if (typeSegment === 'feature') {
tags.push('type:feature');
} else if (typeSegment === 'data-access') {
tags.push('type:data-access');
} else if (typeSegment === 'shared') {
tags.push('type:shared');
} else if (typeSegment === 'utils' || typeSegment === 'util') {
tags.push('type:util');
} else if (domain === 'ui') {
tags.push('type:ui');
} else if (domain === 'core') {
tags.push('type:core');
} else if (domain === 'common') {
tags.push('type:common');
} else if (domain === 'icons') {
tags.push('type:icon');
} else if (domain === 'utils') {
tags.push('type:util');
} else if (domain === 'shared') {
// Libraries directly under libs/shared/* are shared components
tags.push('type:shared');
}
} else {
// Top-level libraries (no subdirectory)
if (domain === 'ui') {
tags.push('type:ui');
} else if (domain === 'core') {
tags.push('type:core');
} else if (domain === 'common') {
tags.push('type:common');
} else if (domain === 'icons') {
tags.push('type:icon');
} else if (domain === 'utils') {
tags.push('type:util');
} else if (domain === 'shared') {
tags.push('type:shared');
}
}
return tags;
}
/**
* Update a project.json file with appropriate tags
* @param {string} projectPath - Path to the project.json file
*/
function updateProjectTags(projectPath) {
try {
const content = fs.readFileSync(projectPath, 'utf8');
const project = JSON.parse(content);
// Determine new tags
const newTags = determineTags(projectPath);
if (newTags.length === 0) {
console.warn(`⚠️ No tags determined for ${projectPath}`);
return;
}
// Preserve existing non-architectural tags (like skip:ci)
const existingTags = project.tags || [];
const preservedTags = existingTags.filter(
tag => !tag.startsWith('scope:') && !tag.startsWith('type:')
);
// Combine tags (preserved + new)
project.tags = [...new Set([...preservedTags, ...newTags])];
// Write back to file
fs.writeFileSync(
projectPath,
JSON.stringify(project, null, 2) + '\n',
'utf8'
);
console.log(`${projectPath}`);
console.log(` Tags: ${project.tags.join(', ')}`);
} catch (error) {
console.error(`❌ Error processing ${projectPath}:`, error.message);
}
}
/**
* Main execution
*/
async function main() {
console.log('🔍 Finding all library project.json files...\n');
// Find all project.json files in libs directory
const libProjectFiles = await glob('libs/**/project.json', {
ignore: ['**/node_modules/**'],
cwd: path.resolve(__dirname, '..'),
absolute: true,
});
// Find all project.json files in generated directory
const generatedProjectFiles = await glob('generated/**/project.json', {
ignore: ['**/node_modules/**'],
cwd: path.resolve(__dirname, '..'),
absolute: true,
});
const projectFiles = [...libProjectFiles, ...generatedProjectFiles];
console.log(`Found ${libProjectFiles.length} library projects`);
console.log(`Found ${generatedProjectFiles.length} generated API projects`);
console.log(`Total: ${projectFiles.length} projects\n`);
// Process each project.json
for (const projectFile of projectFiles) {
updateProjectTags(projectFile);
}
console.log('\n✨ Done! All library tags have been updated.');
console.log('\n📝 Next steps:');
console.log(' 1. Review the changes: git diff');
console.log(' 2. Run lint to check for violations: npx nx run-many --target=lint --all');
console.log(' 3. Commit the changes if everything looks good');
}
// Run the script
main().catch(console.error);