website refactor

This commit is contained in:
2026-01-16 11:51:12 +01:00
parent 63dfa58bcb
commit 0208334c59
49 changed files with 525 additions and 89 deletions

13
apps/api/.eslintrc.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extends": [
"../../.eslintrc.json",
"plugin:gridpilot-api-rules/recommended"
],
"plugins": [
"gridpilot-api-rules"
],
"rules": {
"gridpilot-api-rules/no-index-files": "error",
"gridpilot-api-rules/controller-location": "error"
}
}

View File

@@ -0,0 +1,52 @@
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce that API controllers only call Use Cases or Application Services',
category: 'Architecture',
recommended: true,
},
fixable: null,
schema: [],
messages: {
forbiddenCall: 'Controllers should only call Use Cases or Application Services. Forbidden call to "{{name}}".',
},
},
create(context) {
return {
ClassDeclaration(node) {
const isController = node.decorators && node.decorators.some(d =>
d.expression.type === 'CallExpression' &&
d.expression.callee.name === 'Controller'
);
if (!isController) return;
// Check constructor for injected dependencies
const constructor = node.body.body.find(m => m.kind === 'constructor');
if (constructor) {
constructor.value.params.forEach(param => {
if (param.type === 'TSParameterProperty') {
const typeAnnotation = param.parameter.typeAnnotation;
if (typeAnnotation && typeAnnotation.typeAnnotation.type === 'TSTypeReference') {
const typeName = typeAnnotation.typeAnnotation.typeName.name;
const isValidDependency = /UseCase$/.test(typeName) || /Service$/.test(typeName) || /Logger$/.test(typeName) || /Module$/.test(typeName);
if (!isValidDependency) {
context.report({
node: param,
messageId: 'forbiddenCall',
data: {
name: typeName,
},
});
}
}
}
});
}
},
};
},
};

View File

@@ -0,0 +1,46 @@
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce that API services do not use repositories directly',
category: 'Architecture',
recommended: true,
},
fixable: null,
schema: [],
messages: {
forbiddenRepository: 'API Services should not use repositories directly. Use Cases should handle data access. Forbidden use of "{{name}}".',
},
},
create(context) {
return {
ClassDeclaration(node) {
const isService = node.id.name.endsWith('Service');
if (!isService) return;
// Check constructor for injected repositories
const constructor = node.body.body.find(m => m.kind === 'constructor');
if (constructor) {
constructor.value.params.forEach(param => {
if (param.type === 'TSParameterProperty') {
const typeAnnotation = param.parameter.typeAnnotation;
if (typeAnnotation && typeAnnotation.typeAnnotation.type === 'TSTypeReference') {
const typeName = typeAnnotation.typeAnnotation.typeName.name;
if (typeName.endsWith('Repository')) {
context.report({
node: param,
messageId: 'forbiddenRepository',
data: {
name: typeName,
},
});
}
}
}
});
}
},
};
},
};

View File

@@ -0,0 +1,41 @@
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Enforce controller location in apps/api',
category: 'Architecture',
recommended: true,
},
fixable: null,
schema: [],
messages: {
invalidLocation: 'Controllers must be located in "src/domain/<feature>/" or "src/features/". Found: {{location}}',
},
},
create(context) {
return {
ClassDeclaration(node) {
const isController = node.decorators && node.decorators.some(d =>
d.expression.type === 'CallExpression' &&
d.expression.callee.name === 'Controller'
);
if (isController) {
const filename = context.getFilename();
const isValidLocation = /apps\/api\/src\/(domain\/[^/]+\/|features\/)/.test(filename);
if (!isValidLocation) {
context.report({
node,
messageId: 'invalidLocation',
data: {
location: filename,
},
});
}
}
},
};
},
};

View File

@@ -0,0 +1,24 @@
const noIndexFiles = require('./no-index-files');
const controllerLocation = require('./controller-location');
const apiControllerRules = require('./api-controller-rules');
const apiServiceRules = require('./api-service-rules');
module.exports = {
rules: {
'no-index-files': noIndexFiles,
'controller-location': controllerLocation,
'api-controller-rules': apiControllerRules,
'api-service-rules': apiServiceRules,
},
configs: {
recommended: {
plugins: ['gridpilot-api-rules'],
rules: {
'gridpilot-api-rules/no-index-files': 'error',
'gridpilot-api-rules/controller-location': 'error',
'gridpilot-api-rules/api-controller-rules': 'error',
'gridpilot-api-rules/api-service-rules': 'error',
},
},
},
};

View File

@@ -0,0 +1,36 @@
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Ban index.ts files in API - use explicit imports instead',
category: 'Best Practices',
recommended: true,
},
fixable: null,
schema: [],
messages: {
indexFile: 'Index files are banned in apps/api. Use explicit imports. Example: Instead of "import { foo } from "./", use "import { foo } from "./foo".',
},
},
create(context) {
const filename = context.getFilename();
const isIndexFile = /(^|\/|\\)index\.ts$/.test(filename);
// Allow root index.ts
const allowedPaths = [
'apps/api/index.ts',
'apps/api/src/index.ts',
];
if (isIndexFile && !allowedPaths.some(path => filename.endsWith(path))) {
context.report({
node: null,
loc: { line: 1, column: 0 },
messageId: 'indexFile',
});
}
return {};
},
};

View File

@@ -0,0 +1,8 @@
{
"name": "eslint-plugin-gridpilot-api-rules",
"version": "0.1.0",
"main": "index.js",
"peerDependencies": {
"eslint": ">=8.0.0"
}
}

View File

@@ -17,6 +17,7 @@
"license": "ISC",
"devDependencies": {
"@nestjs/testing": "^10.4.20",
"eslint-plugin-gridpilot-api-rules": "file:eslint-rules",
"ts-node-dev": "^2.0.0",
"tsconfig-paths": "^3.15.0"
},