website refactor

This commit is contained in:
2026-01-14 02:02:24 +01:00
parent 8d7c709e0c
commit 4522d41aef
291 changed files with 12763 additions and 9309 deletions

View File

@@ -1,19 +1,24 @@
/**
* ESLint Rule: Mutation Contract
*
* Ensures mutations return Result type
* Enforces the basic Mutation contract:
* - Mutation classes should `implement Mutation<...>` (type-level contract)
* - `execute()` must return `Promise<Result<...>>`
*/
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Ensure mutations return Result type',
description: 'Ensure mutations implement the Mutation contract and return Result',
category: 'Mutation Contract',
recommended: true,
},
messages: {
wrongReturnType: 'Mutations must return Promise<Result<void, string>> - see apps/website/lib/contracts/Result.ts',
mustImplementMutationInterface:
'Mutation classes must implement Mutation<TInput, TOutput, TError> - see apps/website/lib/contracts/mutations/Mutation.ts',
wrongReturnType:
'Mutation execute() must return Promise<Result<TOutput, TError>> - see apps/website/lib/contracts/Result.ts',
},
schema: [],
},
@@ -21,50 +26,112 @@ module.exports = {
create(context) {
const filename = context.getFilename();
// Only apply to mutation files
if (!filename.includes('/lib/mutations/') || !filename.endsWith('.ts')) {
return {};
}
const mutationInterfaceNames = new Set(['Mutation']);
function isIdentifier(node, nameSet) {
return node && node.type === 'Identifier' && nameSet.has(node.name);
}
function isPromiseType(typeAnnotation) {
if (!typeAnnotation || typeAnnotation.type !== 'TSTypeReference') return false;
const typeName = typeAnnotation.typeName;
return typeName && typeName.type === 'Identifier' && typeName.name === 'Promise';
}
function isResultType(typeNode) {
if (!typeNode || typeNode.type !== 'TSTypeReference') return false;
const typeName = typeNode.typeName;
if (!typeName) return false;
// Common case: Result<...>
if (typeName.type === 'Identifier') {
return typeName.name === 'Result';
}
// Fallback: handle qualified names (rare)
if (typeName.type === 'TSQualifiedName') {
return typeName.right && typeName.right.type === 'Identifier' && typeName.right.name === 'Result';
}
return false;
}
return {
ImportDeclaration(node) {
const importPath = node.source && node.source.value;
if (typeof importPath !== 'string') return;
// Accept both alias and relative imports.
const isMutationContractImport =
importPath.includes('/lib/contracts/mutations/Mutation') ||
importPath.endsWith('lib/contracts/mutations/Mutation') ||
importPath.endsWith('contracts/mutations/Mutation') ||
importPath.endsWith('contracts/mutations/Mutation.ts');
if (!isMutationContractImport) return;
for (const spec of node.specifiers || []) {
// import { Mutation as X } from '...'
if (spec.type === 'ImportSpecifier' && spec.imported && spec.imported.type === 'Identifier') {
mutationInterfaceNames.add(spec.local.name);
}
}
},
ClassDeclaration(node) {
if (!node.id || node.id.type !== 'Identifier') return;
if (!node.id.name.endsWith('Mutation')) return;
const implementsNodes = node.implements || [];
const implementsMutation = implementsNodes.some((impl) => {
// `implements Mutation<...>`
return impl && isIdentifier(impl.expression, mutationInterfaceNames);
});
if (!implementsMutation) {
context.report({
node: node.id,
messageId: 'mustImplementMutationInterface',
});
}
},
MethodDefinition(node) {
if (node.key.type === 'Identifier' &&
node.key.name === 'execute' &&
node.value.type === 'FunctionExpression') {
const returnType = node.value.returnType;
// Check if it returns Promise<Result<...>>
if (!returnType ||
!returnType.typeAnnotation ||
!returnType.typeAnnotation.typeName ||
returnType.typeAnnotation.typeName.name !== 'Promise') {
context.report({
node,
messageId: 'wrongReturnType',
});
return;
}
if (node.key.type !== 'Identifier' || node.key.name !== 'execute') return;
if (!node.value) return;
// Check for Result type
const typeArgs = returnType.typeAnnotation.typeParameters;
if (!typeArgs || !typeArgs.params || typeArgs.params.length === 0) {
context.report({
node,
messageId: 'wrongReturnType',
});
return;
}
const returnType = node.value.returnType;
const typeAnnotation = returnType && returnType.typeAnnotation;
const resultType = typeArgs.params[0];
if (resultType.type !== 'TSTypeReference' ||
!resultType.typeName ||
(resultType.typeName.type === 'Identifier' && resultType.typeName.name !== 'Result')) {
context.report({
node,
messageId: 'wrongReturnType',
});
}
// Must be Promise<...>
if (!isPromiseType(typeAnnotation)) {
context.report({
node,
messageId: 'wrongReturnType',
});
return;
}
const promiseTypeArgs = typeAnnotation.typeParameters;
if (!promiseTypeArgs || !promiseTypeArgs.params || promiseTypeArgs.params.length === 0) {
context.report({
node,
messageId: 'wrongReturnType',
});
return;
}
// Must be Promise<Result<...>> (we don't constrain the generics here)
const inner = promiseTypeArgs.params[0];
if (!isResultType(inner)) {
context.report({
node,
messageId: 'wrongReturnType',
});
}
},
};