@diia-inhouse/oxc-config 5.0.2 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,6 @@ const baseConfig = {
9
9
  resolve('eslint-plugin-security'),
10
10
  resolve('eslint-plugin-regexp'),
11
11
  { name: '@stylistic/js', specifier: resolve('@stylistic/eslint-plugin') },
12
- resolve('@diia-inhouse/eslint-plugin'),
13
12
  { name: '@diia-inhouse/package', specifier: resolveFile('./plugins/packageCheck.mjs') },
14
13
  { name: '@diia-inhouse/locale', specifier: resolveFile('./plugins/noHardcodedCyrillic.mjs') },
15
14
  { name: '@diia-inhouse/test', specifier: resolveFile('./plugins/testConventions.mjs') },
@@ -69,6 +68,7 @@ const baseConfig = {
69
68
  'import/prefer-default-export': 'off',
70
69
  'import/named': 'off',
71
70
  'import/namespace': 'off',
71
+ 'import/no-cycle': ['error', { ignoreTypes: true }],
72
72
  'import/no-unassigned-import': ['warn', { allow: ['**/*.css', 'reflect-metadata', 'module-alias/register'] }],
73
73
  'typescript/consistent-type-assertions': ['error', { assertionStyle: 'as' }],
74
74
  'typescript/no-unsafe-type-assertion': 'off',
@@ -187,7 +187,6 @@ const baseConfig = {
187
187
  'security/detect-object-injection': 'off',
188
188
  'security/detect-non-literal-fs-filename': 'error',
189
189
  'security/detect-non-literal-regexp': 'error',
190
- '@diia-inhouse/logger-err-field': 'error',
191
190
  '@diia-inhouse/package/no-service-in-package-name': 'error',
192
191
  '@diia-inhouse/package/pinned-dependencies': 'error',
193
192
  '@diia-inhouse/package/unique-dependencies': 'error',
@@ -214,6 +213,7 @@ const baseConfig = {
214
213
  '@diia-inhouse/code/prefer-as-const-object': 'error',
215
214
  '@diia-inhouse/code/const-enum-naming': 'error',
216
215
  '@diia-inhouse/code/const-enum-type-name': 'error',
216
+ '@diia-inhouse/code/logger-err-field': 'error',
217
217
  '@diia-inhouse/mongoose/status-requires-history': 'error',
218
218
  // Context-specific rules — off by default, enable per service in oxlint.config.ts
219
219
  '@diia-inhouse/mongoose/schema-timestamps': 'off',
@@ -77,6 +77,83 @@ function isValidPlural(rawSingular, rawPlural) {
77
77
  return false
78
78
  }
79
79
 
80
+ const LOGGER_PROPERTY = 'logger'
81
+ const ERROR_FIELD = 'err'
82
+
83
+ function isNode(value) {
84
+ return value !== null && typeof value === 'object' && typeof value.type === 'string'
85
+ }
86
+
87
+ function collect(root, predicate) {
88
+ const found = []
89
+
90
+ const walk = (node) => {
91
+ if (predicate(node)) {
92
+ found.push(node)
93
+ }
94
+
95
+ for (const key of Object.keys(node)) {
96
+ if (key === 'parent') {
97
+ continue
98
+ }
99
+
100
+ const value = node[key]
101
+
102
+ if (Array.isArray(value)) {
103
+ for (const child of value) {
104
+ if (isNode(child)) {
105
+ walk(child)
106
+ }
107
+ }
108
+ } else if (isNode(value)) {
109
+ walk(value)
110
+ }
111
+ }
112
+ }
113
+
114
+ walk(root)
115
+
116
+ return found
117
+ }
118
+
119
+ function isThisLogger(node) {
120
+ return (
121
+ node.type === 'MemberExpression' &&
122
+ node.object.type === 'ThisExpression' &&
123
+ node.property.type === 'Identifier' &&
124
+ node.property.name === LOGGER_PROPERTY
125
+ )
126
+ }
127
+
128
+ // this.logger.error(...) and chains such as this.logger.child(...).warn(...)
129
+ function isLoggerCall(node) {
130
+ if (node.type !== 'CallExpression') {
131
+ return false
132
+ }
133
+
134
+ let callee = node.callee
135
+
136
+ while (callee.type === 'MemberExpression' || callee.type === 'CallExpression') {
137
+ if (isThisLogger(callee)) {
138
+ return true
139
+ }
140
+
141
+ callee = callee.type === 'MemberExpression' ? callee.object : callee.callee
142
+ }
143
+
144
+ return false
145
+ }
146
+
147
+ function isErrorUnderWrongField(node, errorName) {
148
+ return (
149
+ node.type === 'Property' &&
150
+ node.value.type === 'Identifier' &&
151
+ node.value.name === errorName &&
152
+ node.key.type === 'Identifier' &&
153
+ node.key.name !== ERROR_FIELD
154
+ )
155
+ }
156
+
80
157
  export default {
81
158
  meta: { name: '@diia-inhouse/oxlint-plugin-code' },
82
159
  rules: {
@@ -274,5 +351,47 @@ export default {
274
351
  }
275
352
  },
276
353
  },
354
+
355
+ 'logger-err-field': {
356
+ meta: {
357
+ type: 'problem',
358
+ messages: {
359
+ errField: 'Error must be logged only in the object with [err] field. For example: <{err: e}>',
360
+ },
361
+ },
362
+ create(context) {
363
+ return {
364
+ CatchClause(node) {
365
+ if (!node.param || node.param.type !== 'Identifier') {
366
+ return
367
+ }
368
+
369
+ const errorName = node.param.name
370
+ const report = (target) => context.report({ node: target, messageId: 'errField' })
371
+ const wrongFields = (root) => collect(root, (candidate) => isErrorUnderWrongField(candidate, errorName))
372
+
373
+ for (const call of collect(node.body, isLoggerCall)) {
374
+ for (const arg of call.arguments) {
375
+ if (arg.type === 'Identifier' && arg.name === errorName) {
376
+ report(arg)
377
+ } else if (arg.type === 'Identifier') {
378
+ const declarators = collect(
379
+ node.body,
380
+ (candidate) =>
381
+ candidate.type === 'VariableDeclarator' &&
382
+ candidate.id.type === 'Identifier' &&
383
+ candidate.id.name === arg.name,
384
+ )
385
+
386
+ declarators.flatMap(wrongFields).forEach(report)
387
+ } else if (arg.type === 'ObjectExpression') {
388
+ wrongFields(arg).forEach(report)
389
+ }
390
+ }
391
+ }
392
+ },
393
+ }
394
+ },
395
+ },
277
396
  },
278
397
  }
@@ -5,7 +5,7 @@ import { dirname, join, relative, resolve } from 'node:path'
5
5
  const FIX_FLAGS = new Set(['--fix', '--fix-suggestions', '--fix-dangerously'])
6
6
  const DEPENDENCY_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
7
7
  const PUBLISH_FIELDS = ['license', 'files', 'publishConfig', 'repository']
8
- const LEGACY_CONFIG_FIELDS = ['prettier', 'eslintConfig', 'eslintIgnore']
8
+ const LEGACY_CONFIG_FIELDS = ['prettier', 'eslintConfig', 'eslintIgnore', 'madge']
9
9
  const LEGACY_PACKAGES = [
10
10
  'eslint',
11
11
  'typescript-eslint',
@@ -13,6 +13,7 @@ const LEGACY_PACKAGES = [
13
13
  'eslint-config-prettier',
14
14
  'eslint-plugin-prettier',
15
15
  '@diia-inhouse/eslint-config',
16
+ '@diia-inhouse/eslint-plugin',
16
17
  'prettier',
17
18
  'prettier-plugin-sort-imports',
18
19
  '@ianvs/prettier-plugin-sort-imports',
@@ -20,9 +21,14 @@ const LEGACY_PACKAGES = [
20
21
  'ts-node',
21
22
  'ts-node-dev',
22
23
  'nodemon',
24
+ 'madge',
23
25
  ]
24
- const LEGACY_SCRIPT_NAMES = new Set(['eslint', 'prettier'])
25
- const LEGACY_TOOL_PATTERN = /\b(eslint|prettier)\b/
26
+ const LEGACY_SCRIPTS = new Map([
27
+ ['eslint', 'eslint'],
28
+ ['prettier', 'prettier'],
29
+ ['find-circulars', 'madge'],
30
+ ])
31
+ const LEGACY_TOOL_PATTERN = /\b(eslint|prettier|madge)\b/
26
32
  const SERVICE_NAME_PATTERN = /^service-|-service$/
27
33
  const VERSION_RANGE_PATTERN = /^[\^~]/
28
34
  const EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
@@ -50,7 +56,6 @@ const DEV_TOOL_PATTERNS = [
50
56
  /^@eslint\//,
51
57
  /^prettier(-|$)/,
52
58
  /^typescript$/,
53
- /^madge$/,
54
59
  /^lockfile-lint$/,
55
60
  /^semantic-release(-|$)/,
56
61
  /^@semantic-release\//,
@@ -64,7 +69,6 @@ const DEFAULT_NODE_MAJOR = 24
64
69
  const DEFAULT_AUTHOR = 'diia-team'
65
70
  const DEV_SCRIPT = 'diia-dev'
66
71
  const COMMITLINT_CONFIG = '@diia-inhouse/configs/dist/commitlint'
67
- const MADGE_TS_CONFIG = './tsconfig.json'
68
72
  const RUNTIME_SOURCE_PATTERN = /^src\//
69
73
  const TEST_SOURCE_PATTERN = /(^|\/)(tests?|__tests__|__mocks__)\/|\.(spec|test)\.[cm]?[jt]sx?$/
70
74
  const RUNTIME_DEPENDENCY_FIELDS = ['dependencies', 'peerDependencies', 'optionalDependencies']
@@ -408,7 +412,7 @@ export const manifestRules = {
408
412
  const violations = []
409
413
 
410
414
  for (const [name, body] of Object.entries(scripts)) {
411
- const tool = LEGACY_SCRIPT_NAMES.has(name) ? name : LEGACY_TOOL_PATTERN.exec(String(body))?.[1]
415
+ const tool = LEGACY_SCRIPTS.get(name) ?? LEGACY_TOOL_PATTERN.exec(String(body))?.[1]
412
416
 
413
417
  if (tool) {
414
418
  violations.push({ messageId: 'script', data: { name, tool } })
@@ -419,7 +423,7 @@ export const manifestRules = {
419
423
  },
420
424
  fix: (data) => {
421
425
  for (const [name, body] of Object.entries(data.scripts ?? {})) {
422
- if (LEGACY_SCRIPT_NAMES.has(name)) {
426
+ if (LEGACY_SCRIPTS.has(name)) {
423
427
  delete data.scripts[name]
424
428
  } else {
425
429
  data.scripts[name] = String(body)
@@ -558,7 +562,6 @@ export const manifestRules = {
558
562
  'shared-configs': {
559
563
  messages: {
560
564
  commitlint: 'package.json "commitlint.extends" must be "{{ expected }}" so commit messages are checked by the shared rules.',
561
- madge: 'package.json "madge.tsConfig" must be "{{ expected }}" so find-circulars resolves the same paths as the build.',
562
565
  },
563
566
  check: (data) => {
564
567
  const violations = []
@@ -567,10 +570,6 @@ export const manifestRules = {
567
570
  violations.push({ messageId: 'commitlint', data: { expected: COMMITLINT_CONFIG } })
568
571
  }
569
572
 
570
- if (dependsOn(data, 'madge') && data.madge?.tsConfig !== MADGE_TS_CONFIG) {
571
- violations.push({ messageId: 'madge', data: { expected: MADGE_TS_CONFIG } })
572
- }
573
-
574
573
  return violations
575
574
  },
576
575
  fix: (data) => {
@@ -578,10 +577,6 @@ export const manifestRules = {
578
577
  data.commitlint = { ...data.commitlint, extends: COMMITLINT_CONFIG }
579
578
  }
580
579
 
581
- if (dependsOn(data, 'madge')) {
582
- data.madge = { ...data.madge, tsConfig: MADGE_TS_CONFIG }
583
- }
584
-
585
580
  return data
586
581
  },
587
582
  },
@@ -77,6 +77,83 @@ function isValidPlural(rawSingular, rawPlural) {
77
77
  return false
78
78
  }
79
79
 
80
+ const LOGGER_PROPERTY = 'logger'
81
+ const ERROR_FIELD = 'err'
82
+
83
+ function isNode(value) {
84
+ return value !== null && typeof value === 'object' && typeof value.type === 'string'
85
+ }
86
+
87
+ function collect(root, predicate) {
88
+ const found = []
89
+
90
+ const walk = (node) => {
91
+ if (predicate(node)) {
92
+ found.push(node)
93
+ }
94
+
95
+ for (const key of Object.keys(node)) {
96
+ if (key === 'parent') {
97
+ continue
98
+ }
99
+
100
+ const value = node[key]
101
+
102
+ if (Array.isArray(value)) {
103
+ for (const child of value) {
104
+ if (isNode(child)) {
105
+ walk(child)
106
+ }
107
+ }
108
+ } else if (isNode(value)) {
109
+ walk(value)
110
+ }
111
+ }
112
+ }
113
+
114
+ walk(root)
115
+
116
+ return found
117
+ }
118
+
119
+ function isThisLogger(node) {
120
+ return (
121
+ node.type === 'MemberExpression' &&
122
+ node.object.type === 'ThisExpression' &&
123
+ node.property.type === 'Identifier' &&
124
+ node.property.name === LOGGER_PROPERTY
125
+ )
126
+ }
127
+
128
+ // this.logger.error(...) and chains such as this.logger.child(...).warn(...)
129
+ function isLoggerCall(node) {
130
+ if (node.type !== 'CallExpression') {
131
+ return false
132
+ }
133
+
134
+ let callee = node.callee
135
+
136
+ while (callee.type === 'MemberExpression' || callee.type === 'CallExpression') {
137
+ if (isThisLogger(callee)) {
138
+ return true
139
+ }
140
+
141
+ callee = callee.type === 'MemberExpression' ? callee.object : callee.callee
142
+ }
143
+
144
+ return false
145
+ }
146
+
147
+ function isErrorUnderWrongField(node, errorName) {
148
+ return (
149
+ node.type === 'Property' &&
150
+ node.value.type === 'Identifier' &&
151
+ node.value.name === errorName &&
152
+ node.key.type === 'Identifier' &&
153
+ node.key.name !== ERROR_FIELD
154
+ )
155
+ }
156
+
80
157
  export default {
81
158
  meta: { name: '@diia-inhouse/oxlint-plugin-code' },
82
159
  rules: {
@@ -274,5 +351,47 @@ export default {
274
351
  }
275
352
  },
276
353
  },
354
+
355
+ 'logger-err-field': {
356
+ meta: {
357
+ type: 'problem',
358
+ messages: {
359
+ errField: 'Error must be logged only in the object with [err] field. For example: <{err: e}>',
360
+ },
361
+ },
362
+ create(context) {
363
+ return {
364
+ CatchClause(node) {
365
+ if (!node.param || node.param.type !== 'Identifier') {
366
+ return
367
+ }
368
+
369
+ const errorName = node.param.name
370
+ const report = (target) => context.report({ node: target, messageId: 'errField' })
371
+ const wrongFields = (root) => collect(root, (candidate) => isErrorUnderWrongField(candidate, errorName))
372
+
373
+ for (const call of collect(node.body, isLoggerCall)) {
374
+ for (const arg of call.arguments) {
375
+ if (arg.type === 'Identifier' && arg.name === errorName) {
376
+ report(arg)
377
+ } else if (arg.type === 'Identifier') {
378
+ const declarators = collect(
379
+ node.body,
380
+ (candidate) =>
381
+ candidate.type === 'VariableDeclarator' &&
382
+ candidate.id.type === 'Identifier' &&
383
+ candidate.id.name === arg.name,
384
+ )
385
+
386
+ declarators.flatMap(wrongFields).forEach(report)
387
+ } else if (arg.type === 'ObjectExpression') {
388
+ wrongFields(arg).forEach(report)
389
+ }
390
+ }
391
+ }
392
+ },
393
+ }
394
+ },
395
+ },
277
396
  },
278
397
  }
@@ -5,7 +5,7 @@ import { dirname, join, relative, resolve } from 'node:path'
5
5
  const FIX_FLAGS = new Set(['--fix', '--fix-suggestions', '--fix-dangerously'])
6
6
  const DEPENDENCY_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
7
7
  const PUBLISH_FIELDS = ['license', 'files', 'publishConfig', 'repository']
8
- const LEGACY_CONFIG_FIELDS = ['prettier', 'eslintConfig', 'eslintIgnore']
8
+ const LEGACY_CONFIG_FIELDS = ['prettier', 'eslintConfig', 'eslintIgnore', 'madge']
9
9
  const LEGACY_PACKAGES = [
10
10
  'eslint',
11
11
  'typescript-eslint',
@@ -13,6 +13,7 @@ const LEGACY_PACKAGES = [
13
13
  'eslint-config-prettier',
14
14
  'eslint-plugin-prettier',
15
15
  '@diia-inhouse/eslint-config',
16
+ '@diia-inhouse/eslint-plugin',
16
17
  'prettier',
17
18
  'prettier-plugin-sort-imports',
18
19
  '@ianvs/prettier-plugin-sort-imports',
@@ -20,9 +21,14 @@ const LEGACY_PACKAGES = [
20
21
  'ts-node',
21
22
  'ts-node-dev',
22
23
  'nodemon',
24
+ 'madge',
23
25
  ]
24
- const LEGACY_SCRIPT_NAMES = new Set(['eslint', 'prettier'])
25
- const LEGACY_TOOL_PATTERN = /\b(eslint|prettier)\b/
26
+ const LEGACY_SCRIPTS = new Map([
27
+ ['eslint', 'eslint'],
28
+ ['prettier', 'prettier'],
29
+ ['find-circulars', 'madge'],
30
+ ])
31
+ const LEGACY_TOOL_PATTERN = /\b(eslint|prettier|madge)\b/
26
32
  const SERVICE_NAME_PATTERN = /^service-|-service$/
27
33
  const VERSION_RANGE_PATTERN = /^[\^~]/
28
34
  const EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
@@ -50,7 +56,6 @@ const DEV_TOOL_PATTERNS = [
50
56
  /^@eslint\//,
51
57
  /^prettier(-|$)/,
52
58
  /^typescript$/,
53
- /^madge$/,
54
59
  /^lockfile-lint$/,
55
60
  /^semantic-release(-|$)/,
56
61
  /^@semantic-release\//,
@@ -64,7 +69,6 @@ const DEFAULT_NODE_MAJOR = 24
64
69
  const DEFAULT_AUTHOR = 'diia-team'
65
70
  const DEV_SCRIPT = 'diia-dev'
66
71
  const COMMITLINT_CONFIG = '@diia-inhouse/configs/dist/commitlint'
67
- const MADGE_TS_CONFIG = './tsconfig.json'
68
72
  const RUNTIME_SOURCE_PATTERN = /^src\//
69
73
  const TEST_SOURCE_PATTERN = /(^|\/)(tests?|__tests__|__mocks__)\/|\.(spec|test)\.[cm]?[jt]sx?$/
70
74
  const RUNTIME_DEPENDENCY_FIELDS = ['dependencies', 'peerDependencies', 'optionalDependencies']
@@ -408,7 +412,7 @@ export const manifestRules = {
408
412
  const violations = []
409
413
 
410
414
  for (const [name, body] of Object.entries(scripts)) {
411
- const tool = LEGACY_SCRIPT_NAMES.has(name) ? name : LEGACY_TOOL_PATTERN.exec(String(body))?.[1]
415
+ const tool = LEGACY_SCRIPTS.get(name) ?? LEGACY_TOOL_PATTERN.exec(String(body))?.[1]
412
416
 
413
417
  if (tool) {
414
418
  violations.push({ messageId: 'script', data: { name, tool } })
@@ -419,7 +423,7 @@ export const manifestRules = {
419
423
  },
420
424
  fix: (data) => {
421
425
  for (const [name, body] of Object.entries(data.scripts ?? {})) {
422
- if (LEGACY_SCRIPT_NAMES.has(name)) {
426
+ if (LEGACY_SCRIPTS.has(name)) {
423
427
  delete data.scripts[name]
424
428
  } else {
425
429
  data.scripts[name] = String(body)
@@ -558,7 +562,6 @@ export const manifestRules = {
558
562
  'shared-configs': {
559
563
  messages: {
560
564
  commitlint: 'package.json "commitlint.extends" must be "{{ expected }}" so commit messages are checked by the shared rules.',
561
- madge: 'package.json "madge.tsConfig" must be "{{ expected }}" so find-circulars resolves the same paths as the build.',
562
565
  },
563
566
  check: (data) => {
564
567
  const violations = []
@@ -567,10 +570,6 @@ export const manifestRules = {
567
570
  violations.push({ messageId: 'commitlint', data: { expected: COMMITLINT_CONFIG } })
568
571
  }
569
572
 
570
- if (dependsOn(data, 'madge') && data.madge?.tsConfig !== MADGE_TS_CONFIG) {
571
- violations.push({ messageId: 'madge', data: { expected: MADGE_TS_CONFIG } })
572
- }
573
-
574
573
  return violations
575
574
  },
576
575
  fix: (data) => {
@@ -578,10 +577,6 @@ export const manifestRules = {
578
577
  data.commitlint = { ...data.commitlint, extends: COMMITLINT_CONFIG }
579
578
  }
580
579
 
581
- if (dependsOn(data, 'madge')) {
582
- data.madge = { ...data.madge, tsConfig: MADGE_TS_CONFIG }
583
- }
584
-
585
580
  return data
586
581
  },
587
582
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diia-inhouse/oxc-config",
3
- "version": "5.0.2",
3
+ "version": "6.1.0",
4
4
  "description": "OXC toolchain configs for Diia services: oxlint, oxfmt, oxc transformer",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "diia-team",
@@ -25,13 +25,11 @@
25
25
  "prepare": "npm run build",
26
26
  "lint": "oxlint && oxfmt --check",
27
27
  "lint-fix": "oxlint --fix && oxfmt",
28
- "find-circulars": "madge --circular --extensions ts ./",
29
28
  "lint:lockfile": "lockfile-lint --path package-lock.json --allowed-hosts registry.npmjs.org --validate-https",
30
29
  "semantic-release": "semantic-release"
31
30
  },
32
31
  "dependencies": {
33
32
  "@boundaries/eslint-plugin": "7.2.1",
34
- "@diia-inhouse/eslint-plugin": "1.9.13",
35
33
  "@stylistic/eslint-plugin": "5.10.0",
36
34
  "eslint-import-resolver-oxc": "0.15.0",
37
35
  "eslint-plugin-regexp": "3.3.1",
@@ -45,7 +43,6 @@
45
43
  "@vitest/coverage-v8": "5.0.5",
46
44
  "@vitest/ui": "5.0.5",
47
45
  "lockfile-lint": "5.0.1",
48
- "madge": "8.0.0",
49
46
  "oxfmt": "0.72.0",
50
47
  "oxlint": "1.87.0",
51
48
  "oxlint-tsgolint": "7.0.2003",
@@ -68,8 +65,5 @@
68
65
  },
69
66
  "engines": {
70
67
  "node": ">=24"
71
- },
72
- "madge": {
73
- "tsConfig": "./tsconfig.json"
74
68
  }
75
69
  }