@naturalcycles/dev-lib 20.8.0 → 20.10.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.
package/cfg/eslint-rules.js
CHANGED
|
@@ -503,14 +503,14 @@ export default {
|
|
|
503
503
|
'@typescript-eslint/dot-notation': 0, // not always desireable
|
|
504
504
|
'@typescript-eslint/consistent-indexed-object-style': 0, // Record looses the name of the key
|
|
505
505
|
'@typescript-eslint/no-unsafe-enum-comparison': 0, // not practically helpful
|
|
506
|
-
// stylistic
|
|
507
|
-
'@stylistic/padding-line-between-statements': [
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
],
|
|
514
|
-
'@stylistic/lines-between-class-members': [2, 'always', { exceptAfterSingleLine: true }],
|
|
506
|
+
// stylistic - replaced by custom oxlint rules
|
|
507
|
+
// '@stylistic/padding-line-between-statements': [
|
|
508
|
+
// 2,
|
|
509
|
+
// { blankLine: 'always', prev: 'function', next: '*' },
|
|
510
|
+
// { blankLine: 'always', prev: '*', next: 'function' },
|
|
511
|
+
// { blankLine: 'always', prev: 'class', next: '*' },
|
|
512
|
+
// { blankLine: 'always', prev: '*', next: 'class' },
|
|
513
|
+
// ],
|
|
514
|
+
// '@stylistic/lines-between-class-members': [2, 'always', { exceptAfterSingleLine: true }],
|
|
515
515
|
},
|
|
516
516
|
}
|
package/cfg/eslint.config.js
CHANGED
|
@@ -13,7 +13,6 @@ import eslintPluginOxlint from 'eslint-plugin-oxlint'
|
|
|
13
13
|
import eslintPluginVitest from '@vitest/eslint-plugin'
|
|
14
14
|
import eslintPluginImportX from 'eslint-plugin-import-x'
|
|
15
15
|
import eslintPluginSimpleImportSort from 'eslint-plugin-simple-import-sort'
|
|
16
|
-
import eslintPluginStylistic from '@stylistic/eslint-plugin'
|
|
17
16
|
import eslintRules from './eslint-rules.js'
|
|
18
17
|
import eslintVueRules from './eslint-vue-rules.js'
|
|
19
18
|
import eslintVitestRules from './eslint-vitest-rules.js'
|
|
@@ -106,7 +105,7 @@ function getConfig(tsconfigPath) {
|
|
|
106
105
|
// 'unused-imports': require('eslint-plugin-unused-imports'), // disabled in favor of biome rules
|
|
107
106
|
'simple-import-sort': eslintPluginSimpleImportSort,
|
|
108
107
|
// jsdoc: eslintPluginJsdoc, // oxlint
|
|
109
|
-
'@stylistic': eslintPluginStylistic,
|
|
108
|
+
// '@stylistic': eslintPluginStylistic, // oxlint custom plugin
|
|
110
109
|
},
|
|
111
110
|
languageOptions: {
|
|
112
111
|
ecmaVersion: 'latest',
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
const BLANK_LINE_PATTERN = /(?:\r?\n)[ \t]*(?:\r?\n)/
|
|
2
|
+
const FALLBACK_PADDING_OPTIONS = [
|
|
3
|
+
{ blankLine: 'always', prev: 'function', next: '*' },
|
|
4
|
+
{ blankLine: 'always', prev: '*', next: 'function' },
|
|
5
|
+
{ blankLine: 'always', prev: 'class', next: '*' },
|
|
6
|
+
{ blankLine: 'always', prev: '*', next: 'class' },
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Utility that unwraps export declarations so the underlying declaration
|
|
11
|
+
* (function/class) can be inspected.
|
|
12
|
+
*/
|
|
13
|
+
function unwrapExported(node) {
|
|
14
|
+
if (!node) return null
|
|
15
|
+
if (node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') {
|
|
16
|
+
return node.declaration || null
|
|
17
|
+
}
|
|
18
|
+
return node
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isFunctionLikeStatement(node) {
|
|
22
|
+
const inner = unwrapExported(node)
|
|
23
|
+
if (!inner) return false
|
|
24
|
+
|
|
25
|
+
return inner.type === 'FunctionDeclaration'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isFunctionOverloadStatement(node) {
|
|
29
|
+
const inner = unwrapExported(node)
|
|
30
|
+
if (!inner) return false
|
|
31
|
+
|
|
32
|
+
return inner.type === 'TSDeclareFunction'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isClassLikeStatement(node) {
|
|
36
|
+
const inner = unwrapExported(node)
|
|
37
|
+
if (!inner) return false
|
|
38
|
+
|
|
39
|
+
return inner.type === 'ClassDeclaration'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function describeStatement(node) {
|
|
43
|
+
if (!node) return 'statement'
|
|
44
|
+
if (isFunctionLikeStatement(node)) return 'function declaration'
|
|
45
|
+
if (isClassLikeStatement(node)) return 'class declaration'
|
|
46
|
+
return 'statement'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isMethodOverload(node) {
|
|
50
|
+
if (!node) return false
|
|
51
|
+
if (node.type !== 'MethodDefinition' && node.type !== 'TSAbstractMethodDefinition') return false
|
|
52
|
+
return node.value && node.value.type === 'TSEmptyBodyFunctionExpression'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function hasBlankLineBetween(sourceCode, prevNode, nextNode) {
|
|
56
|
+
if (!prevNode || !nextNode) return false
|
|
57
|
+
if (!prevNode.range || !nextNode.range) return false
|
|
58
|
+
if (prevNode.range[1] >= nextNode.range[0]) return false
|
|
59
|
+
|
|
60
|
+
const between = sourceCode.text.slice(prevNode.range[1], nextNode.range[0])
|
|
61
|
+
return BLANK_LINE_PATTERN.test(between)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function insertBlankLineBeforeNext(fixer, sourceCode, prevNode, nextNode) {
|
|
65
|
+
if (!prevNode || !nextNode) return null
|
|
66
|
+
if (!prevNode.range || !nextNode.range) return null
|
|
67
|
+
if (prevNode.range[1] >= nextNode.range[0]) return null
|
|
68
|
+
|
|
69
|
+
const between = sourceCode.text.slice(prevNode.range[1], nextNode.range[0])
|
|
70
|
+
const hasLinebreak = /\r?\n/.test(between)
|
|
71
|
+
const linebreak = sourceCode.text.includes('\r\n') ? '\r\n' : '\n'
|
|
72
|
+
const text = hasLinebreak ? linebreak : linebreak + linebreak
|
|
73
|
+
|
|
74
|
+
return fixer.insertTextBeforeRange([nextNode.range[0], nextNode.range[0]], text)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function matchesSelector(selector, candidate) {
|
|
78
|
+
if (selector == null) return false
|
|
79
|
+
if (selector === '*') return true
|
|
80
|
+
if (Array.isArray(selector)) return selector.some(item => matchesSelector(item, candidate))
|
|
81
|
+
return selector === candidate
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const paddingLineBetweenStatementsRule = {
|
|
85
|
+
meta: {
|
|
86
|
+
type: 'layout',
|
|
87
|
+
docs: {
|
|
88
|
+
description: 'Require a blank line between functions and classes and surrounding statements',
|
|
89
|
+
},
|
|
90
|
+
fixable: 'whitespace',
|
|
91
|
+
schema: {
|
|
92
|
+
type: 'array',
|
|
93
|
+
items: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
properties: {
|
|
96
|
+
blankLine: { enum: ['always'] },
|
|
97
|
+
prev: { anyOf: [{ enum: ['*', 'function', 'class'] }, { type: 'array' }] },
|
|
98
|
+
next: { anyOf: [{ enum: ['*', 'function', 'class'] }, { type: 'array' }] },
|
|
99
|
+
},
|
|
100
|
+
required: ['blankLine', 'prev', 'next'],
|
|
101
|
+
additionalProperties: false,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
defaultOptions: [],
|
|
105
|
+
messages: {
|
|
106
|
+
expectedBlankLine: 'Expected blank line between {prev} and {next}.',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
create(context) {
|
|
110
|
+
const sourceCode = context.sourceCode
|
|
111
|
+
const options =
|
|
112
|
+
Array.isArray(context.options) && context.options.length > 0
|
|
113
|
+
? context.options
|
|
114
|
+
: FALLBACK_PADDING_OPTIONS
|
|
115
|
+
|
|
116
|
+
const requiresBlankLine = {
|
|
117
|
+
beforeFunction: false,
|
|
118
|
+
afterFunction: false,
|
|
119
|
+
beforeClass: false,
|
|
120
|
+
afterClass: false,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const option of options) {
|
|
124
|
+
if (!option || option.blankLine !== 'always') continue
|
|
125
|
+
|
|
126
|
+
if (matchesSelector(option.prev, 'function') && matchesSelector(option.next, '*')) {
|
|
127
|
+
requiresBlankLine.afterFunction = true
|
|
128
|
+
}
|
|
129
|
+
if (matchesSelector(option.prev, '*') && matchesSelector(option.next, 'function')) {
|
|
130
|
+
requiresBlankLine.beforeFunction = true
|
|
131
|
+
}
|
|
132
|
+
if (matchesSelector(option.prev, 'class') && matchesSelector(option.next, '*')) {
|
|
133
|
+
requiresBlankLine.afterClass = true
|
|
134
|
+
}
|
|
135
|
+
if (matchesSelector(option.prev, '*') && matchesSelector(option.next, 'class')) {
|
|
136
|
+
requiresBlankLine.beforeClass = true
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const shouldCheckFunctions = requiresBlankLine.beforeFunction || requiresBlankLine.afterFunction
|
|
141
|
+
const shouldCheckClasses = requiresBlankLine.beforeClass || requiresBlankLine.afterClass
|
|
142
|
+
|
|
143
|
+
if (!shouldCheckFunctions && !shouldCheckClasses) {
|
|
144
|
+
// Nothing to enforce, bail early.
|
|
145
|
+
return {}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function needsBlankLine(prevNode, nextNode) {
|
|
149
|
+
if (shouldCheckFunctions) {
|
|
150
|
+
if (
|
|
151
|
+
requiresBlankLine.afterFunction &&
|
|
152
|
+
isFunctionLikeStatement(prevNode) &&
|
|
153
|
+
!isFunctionOverloadStatement(nextNode)
|
|
154
|
+
) {
|
|
155
|
+
return true
|
|
156
|
+
}
|
|
157
|
+
if (
|
|
158
|
+
requiresBlankLine.beforeFunction &&
|
|
159
|
+
isFunctionLikeStatement(nextNode) &&
|
|
160
|
+
!isFunctionOverloadStatement(prevNode)
|
|
161
|
+
) {
|
|
162
|
+
return true
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (shouldCheckClasses) {
|
|
166
|
+
if (requiresBlankLine.afterClass && isClassLikeStatement(prevNode)) return true
|
|
167
|
+
if (requiresBlankLine.beforeClass && isClassLikeStatement(nextNode)) return true
|
|
168
|
+
}
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function checkStatements(statements) {
|
|
173
|
+
if (!Array.isArray(statements) || statements.length < 2) return
|
|
174
|
+
|
|
175
|
+
for (let i = 1; i < statements.length; i += 1) {
|
|
176
|
+
const prevNode = statements[i - 1]
|
|
177
|
+
const nextNode = statements[i]
|
|
178
|
+
|
|
179
|
+
if (!prevNode || !nextNode) continue
|
|
180
|
+
if (!needsBlankLine(prevNode, nextNode)) continue
|
|
181
|
+
if (hasBlankLineBetween(sourceCode, prevNode, nextNode)) continue
|
|
182
|
+
|
|
183
|
+
context.report({
|
|
184
|
+
node: nextNode,
|
|
185
|
+
messageId: 'expectedBlankLine',
|
|
186
|
+
data: {
|
|
187
|
+
prev: describeStatement(prevNode),
|
|
188
|
+
next: describeStatement(nextNode),
|
|
189
|
+
},
|
|
190
|
+
fix(fixer) {
|
|
191
|
+
return insertBlankLineBeforeNext(fixer, sourceCode, prevNode, nextNode)
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
Program(node) {
|
|
199
|
+
checkStatements(node.body)
|
|
200
|
+
},
|
|
201
|
+
BlockStatement(node) {
|
|
202
|
+
checkStatements(node.body)
|
|
203
|
+
},
|
|
204
|
+
StaticBlock(node) {
|
|
205
|
+
checkStatements(node.body)
|
|
206
|
+
},
|
|
207
|
+
SwitchCase(node) {
|
|
208
|
+
checkStatements(node.consequent)
|
|
209
|
+
},
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isSingleLine(node, sourceCode) {
|
|
215
|
+
if (!node || !node.range) return false
|
|
216
|
+
const start = sourceCode.getLocFromIndex(node.range[0])
|
|
217
|
+
const endIndex = node.range[1] > node.range[0] ? node.range[1] - 1 : node.range[1]
|
|
218
|
+
const end = sourceCode.getLocFromIndex(endIndex)
|
|
219
|
+
return start.line === end.line
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const linesBetweenClassMembersRule = {
|
|
223
|
+
meta: {
|
|
224
|
+
type: 'layout',
|
|
225
|
+
docs: {
|
|
226
|
+
description: 'Require blank lines between class members',
|
|
227
|
+
},
|
|
228
|
+
fixable: 'whitespace',
|
|
229
|
+
schema: [
|
|
230
|
+
{ enum: ['always'] },
|
|
231
|
+
{
|
|
232
|
+
type: 'object',
|
|
233
|
+
properties: {
|
|
234
|
+
exceptAfterSingleLine: { type: 'boolean' },
|
|
235
|
+
},
|
|
236
|
+
additionalProperties: false,
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
defaultOptions: ['always', { exceptAfterSingleLine: false }],
|
|
240
|
+
messages: {
|
|
241
|
+
expectedBlankLine: 'Expected blank line between class members.',
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
create(context) {
|
|
245
|
+
const sourceCode = context.sourceCode
|
|
246
|
+
const mode = context.options?.[0] || 'always'
|
|
247
|
+
const exceptAfterSingleLine =
|
|
248
|
+
context.options?.[1]?.exceptAfterSingleLine !== undefined
|
|
249
|
+
? Boolean(context.options[1].exceptAfterSingleLine)
|
|
250
|
+
: true
|
|
251
|
+
const exceptAfterOverload = true
|
|
252
|
+
|
|
253
|
+
if (mode !== 'always') {
|
|
254
|
+
return {}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function checkClassBody(body) {
|
|
258
|
+
if (!Array.isArray(body) || body.length < 2) return
|
|
259
|
+
|
|
260
|
+
const members = exceptAfterOverload ? body.filter(member => !isMethodOverload(member)) : body
|
|
261
|
+
if (members.length < 2) return
|
|
262
|
+
|
|
263
|
+
for (let i = 1; i < members.length; i += 1) {
|
|
264
|
+
const prevElement = members[i - 1]
|
|
265
|
+
const nextElement = members[i]
|
|
266
|
+
if (!prevElement || !nextElement) continue
|
|
267
|
+
|
|
268
|
+
if (exceptAfterSingleLine && isSingleLine(prevElement, sourceCode)) continue
|
|
269
|
+
if (hasBlankLineBetween(sourceCode, prevElement, nextElement)) continue
|
|
270
|
+
|
|
271
|
+
context.report({
|
|
272
|
+
node: nextElement,
|
|
273
|
+
messageId: 'expectedBlankLine',
|
|
274
|
+
fix(fixer) {
|
|
275
|
+
return insertBlankLineBeforeNext(fixer, sourceCode, prevElement, nextElement)
|
|
276
|
+
},
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
ClassBody(node) {
|
|
283
|
+
checkClassBody(node.body)
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const stylisticPlugin = {
|
|
290
|
+
meta: {
|
|
291
|
+
name: '@stylistic',
|
|
292
|
+
},
|
|
293
|
+
rules: {
|
|
294
|
+
'padding-line-between-statements': paddingLineBetweenStatementsRule,
|
|
295
|
+
'lines-between-class-members': linesBetweenClassMembersRule,
|
|
296
|
+
},
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export default stylisticPlugin
|
package/cfg/oxlint.config.json
CHANGED
|
@@ -11,7 +11,16 @@
|
|
|
11
11
|
"promise",
|
|
12
12
|
"vitest"
|
|
13
13
|
],
|
|
14
|
+
"jsPlugins": ["./oxlint-plugin-stylistic.mjs"],
|
|
14
15
|
"rules": {
|
|
16
|
+
"@stylistic/padding-line-between-statements": [
|
|
17
|
+
2,
|
|
18
|
+
{ "blankLine": "always", "prev": "function", "next": "*" },
|
|
19
|
+
{ "blankLine": "always", "prev": "*", "next": "function" },
|
|
20
|
+
{ "blankLine": "always", "prev": "class", "next": "*" },
|
|
21
|
+
{ "blankLine": "always", "prev": "*", "next": "class" }
|
|
22
|
+
],
|
|
23
|
+
"@stylistic/lines-between-class-members": [2, "always", { "exceptAfterSingleLine": true }],
|
|
15
24
|
"no-this-alias": 0,
|
|
16
25
|
"no-async-promise-executor": 0,
|
|
17
26
|
"no-standalone-expect": 0,
|
package/cfg/tsconfig.src.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"baseUrl": "${configDir}/src",
|
|
10
10
|
"outDir": "${configDir}/dist",
|
|
11
11
|
// Target/module
|
|
12
|
-
"target": "
|
|
12
|
+
"target": "es2024",
|
|
13
13
|
"lib": ["esnext"], // add "dom" if needed
|
|
14
14
|
// module `nodenext` is a modern mode that auto-detects cjs/esm
|
|
15
15
|
// it also defaults `esModuleInterop` and `allowSyntheticDefaultImports` to true
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturalcycles/dev-lib",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "20.
|
|
4
|
+
"version": "20.10.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@biomejs/biome": "^2",
|
|
7
7
|
"@commitlint/cli": "^20",
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
"@inquirer/prompts": "^7",
|
|
11
11
|
"@naturalcycles/js-lib": "^15",
|
|
12
12
|
"@naturalcycles/nodejs-lib": "^15",
|
|
13
|
-
"@stylistic/eslint-plugin": "^5",
|
|
14
13
|
"@vitest/coverage-v8": "^4",
|
|
15
14
|
"@vitest/eslint-plugin": "^1",
|
|
16
15
|
"eslint": "^9",
|
|
@@ -78,7 +77,7 @@
|
|
|
78
77
|
"dev-lib": "dist/bin/dev-lib.js"
|
|
79
78
|
},
|
|
80
79
|
"engines": {
|
|
81
|
-
"node": ">=
|
|
80
|
+
"node": ">=24.10.0"
|
|
82
81
|
},
|
|
83
82
|
"publishConfig": {
|
|
84
83
|
"provenance": true,
|