Resolve dist imports in runtime scripts against their TypeScript sources. Keep missing-module checks enabled and cover absent build output in the lint gate. Verified npm ci, prisma generate and full verify with dist absent at startup.
166 lines
7.2 KiB
JavaScript
166 lines
7.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'];
|
|
|
|
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.21.0' },
|
|
},
|
|
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-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/**/*.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',
|
|
},
|
|
},
|
|
);
|