236 lines
9.2 KiB
JavaScript
236 lines
9.2 KiB
JavaScript
// @ts-check
|
|
import nest from '@darraghor/eslint-plugin-nestjs-typed';
|
|
import eslint from '@eslint/js';
|
|
import stylistic from '@stylistic/eslint-plugin';
|
|
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript';
|
|
import { importX } from 'eslint-plugin-import-x';
|
|
import jest from 'eslint-plugin-jest';
|
|
import node from 'eslint-plugin-n';
|
|
import perfectionist from 'eslint-plugin-perfectionist';
|
|
import regexp from 'eslint-plugin-regexp';
|
|
import security from 'eslint-plugin-security';
|
|
import sonarjs from 'eslint-plugin-sonarjs';
|
|
import { defineConfig, globalIgnores } from 'eslint/config';
|
|
import globals from 'globals';
|
|
import tseslint from 'typescript-eslint';
|
|
|
|
const tests = ['**/*.spec.ts', 'test/**/*.ts'];
|
|
const prismaImports = [
|
|
'@prisma/*', '**/generated/prisma/**', '**/prisma.service',
|
|
];
|
|
|
|
export default defineConfig(
|
|
globalIgnores(['dist/**', 'dist-dev/**', 'build/**', 'generated/**', 'coverage/**', 'node_modules/**']),
|
|
{
|
|
files: ['**/*.{ts,js,cjs,mjs}'],
|
|
extends: [
|
|
eslint.configs.recommended,
|
|
node.configs['flat/recommended'],
|
|
importX.flatConfigs.recommended,
|
|
sonarjs.configs.recommended,
|
|
regexp.configs['flat/recommended'],
|
|
security.configs.recommended,
|
|
perfectionist.configs['recommended-natural'],
|
|
stylistic.configs.customize({ indent: 2, quotes: 'single', semi: true, jsx: false }),
|
|
],
|
|
languageOptions: { globals: globals.node },
|
|
linterOptions: { reportUnusedDisableDirectives: 'error' },
|
|
settings: {
|
|
'import-x/resolver-next': [createTypeScriptImportResolver()],
|
|
'node': { tryExtensions: ['.ts', '.js', '.json', '.node'], version: '24.22.1' },
|
|
},
|
|
rules: {
|
|
'curly': ['error', 'multi-line'],
|
|
// This private application is deployed as a whole, never published to npm.
|
|
'n/no-unpublished-import': 'off',
|
|
'n/no-unpublished-require': 'off',
|
|
// Decorator execution and object insertion order can be observable at runtime.
|
|
'perfectionist/sort-decorators': 'off',
|
|
'perfectionist/sort-objects': 'off',
|
|
'perfectionist/sort-modules': 'off',
|
|
'perfectionist/sort-classes': 'off',
|
|
'@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: false }],
|
|
'@stylistic/arrow-parens': ['error', 'always'],
|
|
'@stylistic/operator-linebreak': [
|
|
'error',
|
|
'after',
|
|
{ overrides: { '?': 'before', ':': 'before' } },
|
|
],
|
|
'import-x/no-cycle': 'error',
|
|
'import-x/no-duplicates': ['error', { 'prefer-inline': true }],
|
|
},
|
|
},
|
|
{
|
|
files: ['**/*.mjs'],
|
|
extends: [node.configs['flat/recommended-module']],
|
|
},
|
|
{
|
|
files: ['scripts/generate-openapi.cjs', 'test/fixtures/tbank-sync-crash.cjs', 'test/runtime-check.cjs'],
|
|
rules: {
|
|
// These scripts run after build; lint validates their source paths before dist exists.
|
|
'n/no-missing-require': ['error', {
|
|
resolverConfig: {
|
|
alias: { '../dist': import.meta.dirname, '../../dist': import.meta.dirname },
|
|
extensionAlias: { '.js': ['.ts', '.js'] },
|
|
},
|
|
}],
|
|
},
|
|
},
|
|
{
|
|
files: ['**/*.ts'],
|
|
extends: [tseslint.configs.recommendedTypeChecked, importX.flatConfigs.typescript],
|
|
languageOptions: {
|
|
sourceType: 'module',
|
|
parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
|
|
},
|
|
rules: {
|
|
// TypeScript resolves source imports; import-x checks them with the TS resolver.
|
|
'n/no-missing-import': 'off',
|
|
'no-magic-numbers': 'off',
|
|
'@typescript-eslint/no-magic-numbers': [
|
|
'error',
|
|
{
|
|
ignoreArrayIndexes: true,
|
|
ignoreDefaultValues: true,
|
|
ignoreEnums: true,
|
|
ignoreNumericLiteralTypes: true,
|
|
ignoreReadonlyClassProperties: true,
|
|
ignoreTypeIndexes: true,
|
|
enforceConst: true,
|
|
},
|
|
],
|
|
'@typescript-eslint/no-non-null-assertion': 'error',
|
|
'no-restricted-syntax': ['error', {
|
|
selector: 'PropertyDefinition[definite=true], VariableDeclarator[definite=true]',
|
|
message: 'Definite assignment assertions are forbidden. Initialize owned values; use declare only for externally populated DTO fields.',
|
|
}],
|
|
},
|
|
},
|
|
{
|
|
files: ['src/**/*.ts'],
|
|
ignores: tests,
|
|
extends: [nest.configs.flatRecommended],
|
|
rules: {
|
|
// Domain string unions are validated with IsIn rather than runtime TS enums.
|
|
'@darraghor/nestjs-typed/validated-non-primitive-property-needs-type-decorator': ['error', { additionalTypeDecorators: ['IsIn'] }],
|
|
},
|
|
},
|
|
{
|
|
files: tests,
|
|
extends: [jest.configs['flat/recommended']],
|
|
rules: {
|
|
'@typescript-eslint/no-magic-numbers': 'off',
|
|
'jest/expect-expect': ['error', { assertFunctionNames: ['expect', '**.expect'] }],
|
|
},
|
|
},
|
|
{
|
|
files: ['src/modules/**/domain/**/*.ts'],
|
|
ignores: tests,
|
|
rules: {
|
|
'no-restricted-imports': ['error', {
|
|
patterns: [{
|
|
group: [
|
|
'@nestjs/*', ...prismaImports, '**/*.dto', '**/*.repository',
|
|
'**/*.client', '**/application/**', '**/infrastructure/**',
|
|
],
|
|
message: 'Domain uses pure rules and value types, not transport or persistence.',
|
|
}],
|
|
}],
|
|
},
|
|
},
|
|
{
|
|
files: ['src/**/*.controller.ts'],
|
|
ignores: tests,
|
|
rules: {
|
|
'no-restricted-imports': ['error', {
|
|
patterns: [
|
|
{
|
|
group: [...prismaImports, '**/*.repository', '**/*.client'],
|
|
message: 'Controllers delegate to application services or use cases.',
|
|
},
|
|
{
|
|
group: ['**'],
|
|
importNamePattern: '(?:Client|Repository|PrismaService)$',
|
|
message: 'Use application orchestration rather than data providers in controllers.',
|
|
},
|
|
],
|
|
}],
|
|
},
|
|
},
|
|
{
|
|
files: ['src/**/*.use-case.ts'],
|
|
ignores: tests,
|
|
rules: {
|
|
'no-restricted-imports': ['error', {
|
|
patterns: [{
|
|
group: prismaImports,
|
|
message: 'Use cases obtain data through concrete repositories.',
|
|
}],
|
|
}],
|
|
},
|
|
},
|
|
{
|
|
files: [
|
|
'src/**/*.ts',
|
|
],
|
|
ignores: [
|
|
'**/*.spec.ts',
|
|
'test/**/*.ts',
|
|
],
|
|
rules: {
|
|
'no-restricted-syntax': [
|
|
'error',
|
|
{
|
|
selector: 'Literal[value=/^(?:(?:INSTRUMENT_ID_TYPE|INSTRUMENT_TYPE|OPERATION_STATE|OPERATION_TYPE|ACCOUNT_STATUS|ACCOUNT_TYPE|CANDLE_INTERVAL|COUPON_TYPE|BANK|INVALID)_[A-Z0-9]+(?:_[A-Z0-9]+)*|[A-Z][A-Z0-9_]*_(?:ERROR|UNAVAILABLE|NOT_FOUND|NOT_CONNECTED|CONFLICT|AMBIGUOUS|REGISTERED)|P[0-9]{4}|UNAUTHORIZED|FORBIDDEN|BAD_REQUEST|NOT_FOUND|UNSUPPORTED_MEDIA_TYPE)$/]:not(TSEnumMember > Literal)',
|
|
message: 'Use the owning enum member for machine codes and protocol values.',
|
|
},
|
|
{
|
|
selector: 'TemplateElement[value.cooked=/^(?:(?:INSTRUMENT_ID_TYPE|INSTRUMENT_TYPE|OPERATION_STATE|OPERATION_TYPE|ACCOUNT_STATUS|ACCOUNT_TYPE|CANDLE_INTERVAL|COUPON_TYPE|BANK|INVALID)_[A-Z0-9]+(?:_[A-Z0-9]+)*|[A-Z][A-Z0-9_]*_(?:ERROR|UNAVAILABLE|NOT_FOUND|NOT_CONNECTED|CONFLICT|AMBIGUOUS|REGISTERED)|P[0-9]{4}|UNAUTHORIZED|FORBIDDEN|BAD_REQUEST|NOT_FOUND|UNSUPPORTED_MEDIA_TYPE)$/]:not(TSEnumMember TemplateElement)',
|
|
message: 'Use the owning enum member instead of a template machine code.',
|
|
},
|
|
{
|
|
selector: 'Property:matches([key.name=/^(code|errorCode|idType|instrumentKind)$/], [key.value=/^(code|errorCode|idType|instrumentKind)$/]) > Literal.value[value=type(string)]',
|
|
message: 'Use an enum member instead of an inline code or protocol value.',
|
|
},
|
|
{
|
|
selector: 'Property:matches([key.name=/^(code|errorCode|idType|instrumentKind)$/], [key.value=/^(code|errorCode|idType|instrumentKind)$/]) > TemplateLiteral.value',
|
|
message: 'Use an enum member instead of constructing a code or protocol value.',
|
|
},
|
|
{
|
|
selector: 'Property[computed=false][key.name=/^(OPERATION_TYPE|ACCOUNT_TYPE)_/]',
|
|
message: 'Use a computed enum member as the protocol mapping key.',
|
|
},
|
|
{
|
|
selector: 'BinaryExpression:has(> :matches(MemberExpression[property.name=/^(code|errorCode)$/], Identifier[name=/^(code|errorCode)$/])) > Literal[value=type(string)]',
|
|
message: 'Compare codes with their enum members, not string literals.',
|
|
},
|
|
{
|
|
selector: 'PropertyDefinition[definite=true], VariableDeclarator[definite=true]',
|
|
message: 'Definite assignment assertions are forbidden. Initialize owned values; use declare only for externally populated DTO fields.',
|
|
},
|
|
],
|
|
'@typescript-eslint/no-unsafe-enum-comparison': 'error',
|
|
},
|
|
},
|
|
{
|
|
files: ['src/**/*.ts'],
|
|
ignores: [...tests, 'src/modules/investments/tbank-sync/**'],
|
|
rules: {
|
|
// Separate rule so it does not replace layer-specific no-restricted-imports configs.
|
|
'import-x/no-restricted-paths': ['error', {
|
|
zones: [{
|
|
target: './src',
|
|
from: './src/modules/investments/tbank-sync',
|
|
except: [
|
|
'./tbank-sync.api.ts',
|
|
'./tbank-sync.dto.ts',
|
|
'./tbank-sync.module.ts',
|
|
'./tbank-sync-repository.module.ts',
|
|
],
|
|
message: 'Use tbank-sync.api for cross-module access.',
|
|
}],
|
|
}],
|
|
},
|
|
},
|
|
);
|