@naturalcycles/dev-lib 20.69.0 → 20.71.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/oxlint-plugin-nc.d.ts +10 -0
- package/cfg/oxlint-plugin-nc.js +148 -0
- package/cfg/oxlint.config.js +10 -9
- package/dist/check.util.js +18 -17
- package/dist/release/notes.util.js +2 -1
- package/dist/testing/mockAllKindsOfThings.js +1 -0
- package/dist/vendor/mitm.d.ts +1 -1
- package/dist/vendor/mitm.js +11 -11
- package/package.json +1 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const ASSERTABLE_ERRORS = new Set(['Error', 'AppError', 'AssertionError'])
|
|
2
|
+
const EQUALITY_OPERATORS = new Set(['===', '!==', '==', '!='])
|
|
3
|
+
// A condition that stays readable once negated; `!(a instanceof B && c)` does not
|
|
4
|
+
const UNASSERTABLE_TESTS = new Set([
|
|
5
|
+
'LogicalExpression',
|
|
6
|
+
'ConditionalExpression',
|
|
7
|
+
'AssignmentExpression',
|
|
8
|
+
'SequenceExpression',
|
|
9
|
+
])
|
|
10
|
+
// An inline callback in the condition (`items.some(item => ...)`) does not read as an assertion once negated
|
|
11
|
+
const FUNCTION_NODES = new Set(['ArrowFunctionExpression', 'FunctionExpression'])
|
|
12
|
+
// `_assert` builds its message and data before checking, so a call, await or interpolated message there
|
|
13
|
+
// would run on the happy path too (keep ids in the data bag), and a value the condition narrows
|
|
14
|
+
// (`if (!res.ok) throw new Error(res.error)`) no longer typechecks
|
|
15
|
+
const EVALUATED_ARGUMENTS = new Set([
|
|
16
|
+
'CallExpression',
|
|
17
|
+
'NewExpression',
|
|
18
|
+
'AwaitExpression',
|
|
19
|
+
'TaggedTemplateExpression',
|
|
20
|
+
'YieldExpression',
|
|
21
|
+
'UpdateExpression',
|
|
22
|
+
'AssignmentExpression',
|
|
23
|
+
'SpreadElement',
|
|
24
|
+
])
|
|
25
|
+
// Test files and test helpers throw plain errors on purpose (mocks, fakes). Checked here rather than in
|
|
26
|
+
// a shared `overrides` entry, because `defineOxlintConfig` lets a consumer's `overrides` replace it
|
|
27
|
+
const TEST_FILE_PATTERN = /\.(?:test|spec)\.[cm]?[jt]sx?$|\/(?:test|testing)\//
|
|
28
|
+
|
|
29
|
+
// No `fix`/`suggest`: dev-lib runs oxlint with --fix-dangerously, which would apply them and leave
|
|
30
|
+
// `_assert` unimported
|
|
31
|
+
const preferAssertRule = {
|
|
32
|
+
meta: {
|
|
33
|
+
type: 'suggestion',
|
|
34
|
+
docs: {
|
|
35
|
+
description:
|
|
36
|
+
'Prefer _assert over an if whose only statement throws an Error or AppError with a static message',
|
|
37
|
+
},
|
|
38
|
+
messages: {
|
|
39
|
+
preferAssert:
|
|
40
|
+
'Use _assert(condition, message, data) from @naturalcycles/js-lib/error instead of throwing in an if',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
create(context) {
|
|
44
|
+
if (TEST_FILE_PATTERN.test(context.filename)) return {}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
IfStatement(node) {
|
|
48
|
+
if (node.alternate || node.parent.type === 'IfStatement') return
|
|
49
|
+
if (!isAssertableTest(node.test)) return
|
|
50
|
+
|
|
51
|
+
const { consequent } = node
|
|
52
|
+
const statement =
|
|
53
|
+
consequent.type === 'BlockStatement' && consequent.body.length === 1
|
|
54
|
+
? consequent.body[0]
|
|
55
|
+
: consequent
|
|
56
|
+
if (statement?.type !== 'ThrowStatement') return
|
|
57
|
+
|
|
58
|
+
const error = statement.argument
|
|
59
|
+
if (
|
|
60
|
+
error.type !== 'NewExpression' ||
|
|
61
|
+
error.callee.type !== 'Identifier' ||
|
|
62
|
+
!ASSERTABLE_ERRORS.has(error.callee.name) ||
|
|
63
|
+
error.arguments.length > maxArguments(error.callee.name) ||
|
|
64
|
+
error.arguments.some(argument => containsEvaluation(argument))
|
|
65
|
+
) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
const narrowed = referencedNames(node.test)
|
|
69
|
+
if (error.arguments.some(argument => referencesAny(argument, narrowed))) return
|
|
70
|
+
|
|
71
|
+
context.report({ node, messageId: 'preferAssert' })
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// A second `Error` argument is `{ cause }`, which `_assert` would store as data and lose as a cause
|
|
78
|
+
function maxArguments(errorName) {
|
|
79
|
+
return errorName === 'Error' ? 1 : 2
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isAssertableTest(test) {
|
|
83
|
+
if (UNASSERTABLE_TESTS.has(test.type)) return false
|
|
84
|
+
if (test.type === 'BinaryExpression' && !EQUALITY_OPERATORS.has(test.operator)) return false
|
|
85
|
+
return !containsFunction(test)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function containsFunction(node) {
|
|
89
|
+
return visit(node, child => FUNCTION_NODES.has(child.type))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function containsEvaluation(node) {
|
|
93
|
+
return visit(
|
|
94
|
+
node,
|
|
95
|
+
child =>
|
|
96
|
+
EVALUATED_ARGUMENTS.has(child.type) ||
|
|
97
|
+
(child.type === 'TemplateLiteral' && child.expressions.length > 0),
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function referencedNames(node) {
|
|
102
|
+
const names = new Set()
|
|
103
|
+
visit(node, child => {
|
|
104
|
+
if (child.type === 'Identifier') names.add(child.name)
|
|
105
|
+
if (child.type === 'ThisExpression') names.add('this')
|
|
106
|
+
return false
|
|
107
|
+
})
|
|
108
|
+
return names
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function referencesAny(node, names) {
|
|
112
|
+
return visit(node, child => {
|
|
113
|
+
if (child.type === 'Identifier') return names.has(child.name)
|
|
114
|
+
if (child.type === 'ThisExpression') return names.has('this')
|
|
115
|
+
return false
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Depth-first walk over the AST below `node`, skipping `parent` links and the names that are not
|
|
121
|
+
* references: non-computed member properties and object keys. Stops at the first node `fn` returns
|
|
122
|
+
* true for, and returns whether there was one.
|
|
123
|
+
*/
|
|
124
|
+
function visit(node, fn) {
|
|
125
|
+
if (!node || typeof node !== 'object') return false
|
|
126
|
+
if (Array.isArray(node)) return node.some(item => visit(item, fn))
|
|
127
|
+
const { type } = node
|
|
128
|
+
if (!type) return false
|
|
129
|
+
if (fn(node)) return true
|
|
130
|
+
for (const [key, child] of Object.entries(node)) {
|
|
131
|
+
if (key === 'parent' || key === 'loc' || key === 'range') continue
|
|
132
|
+
if (key === 'property' && type === 'MemberExpression' && !node.computed) continue
|
|
133
|
+
if (key === 'key' && type === 'Property' && !node.computed) continue
|
|
134
|
+
if (visit(child, fn)) return true
|
|
135
|
+
}
|
|
136
|
+
return false
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const ncPlugin = {
|
|
140
|
+
meta: {
|
|
141
|
+
name: 'nc',
|
|
142
|
+
},
|
|
143
|
+
rules: {
|
|
144
|
+
'prefer-assert': preferAssertRule,
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export default ncPlugin
|
package/cfg/oxlint.config.js
CHANGED
|
@@ -39,7 +39,10 @@ export const sharedConfig = {
|
|
|
39
39
|
// "vue",
|
|
40
40
|
'vitest',
|
|
41
41
|
],
|
|
42
|
-
jsPlugins: [
|
|
42
|
+
jsPlugins: [
|
|
43
|
+
`${import.meta.dirname}/oxlint-plugin-stylistic.js`,
|
|
44
|
+
`${import.meta.dirname}/oxlint-plugin-nc.js`,
|
|
45
|
+
],
|
|
43
46
|
options: {
|
|
44
47
|
reportUnusedDisableDirectives: 'deny',
|
|
45
48
|
respectEslintDisableDirectives: false,
|
|
@@ -69,6 +72,7 @@ export const sharedConfig = {
|
|
|
69
72
|
{ blankLine: 'always', prev: '*', next: 'class' },
|
|
70
73
|
],
|
|
71
74
|
'@stylistic/lines-between-class-members': [2, 'always', { exceptAfterSingleLine: true }],
|
|
75
|
+
'nc/prefer-assert': 2,
|
|
72
76
|
complexity: [2, { max: 40 }],
|
|
73
77
|
'logical-assignment-operators': [
|
|
74
78
|
2,
|
|
@@ -92,8 +96,10 @@ export const sharedConfig = {
|
|
|
92
96
|
'object-shorthand': 2,
|
|
93
97
|
'valid-expect': 0,
|
|
94
98
|
'valid-title': 0,
|
|
95
|
-
'vitest/valid-title':
|
|
99
|
+
'vitest/valid-title': [2, { mustMatch: { it: '^should ' } }],
|
|
96
100
|
'valid-describe-callback': 0,
|
|
101
|
+
// Options arguments and variables are `opt`, never `opts`
|
|
102
|
+
'id-denylist': [2, 'opts'],
|
|
97
103
|
// 'id-denylist': [
|
|
98
104
|
// 0, // actually not useful
|
|
99
105
|
// 'any',
|
|
@@ -142,6 +148,7 @@ export const sharedConfig = {
|
|
|
142
148
|
name: 'Intl',
|
|
143
149
|
message: 'Use Intl2 instead.',
|
|
144
150
|
},
|
|
151
|
+
{ name: 'Date', message: 'Use localTime / localDate from @naturalcycles/js-lib/datetime.' },
|
|
145
152
|
],
|
|
146
153
|
'no-unused-expressions': 2,
|
|
147
154
|
'no-var': 2,
|
|
@@ -458,13 +465,7 @@ export const sharedConfig = {
|
|
|
458
465
|
'vitest/prefer-called-exactly-once-with': 2,
|
|
459
466
|
'vitest/consistent-test-filename': 2,
|
|
460
467
|
'vitest/warn-todo': 0,
|
|
461
|
-
'vitest/consistent-test-it': [
|
|
462
|
-
2,
|
|
463
|
-
{
|
|
464
|
-
fn: 'test',
|
|
465
|
-
withinDescribe: 'test',
|
|
466
|
-
},
|
|
467
|
-
],
|
|
468
|
+
'vitest/consistent-test-it': [2, { fn: 'it', withinDescribe: 'it' }],
|
|
468
469
|
'vitest/max-nested-describe': [0, { max: 4 }], // not good
|
|
469
470
|
'vitest/no-alias-methods': 2,
|
|
470
471
|
'vitest/no-restricted-vi-methods': [
|
package/dist/check.util.js
CHANGED
|
@@ -4,6 +4,7 @@ import { findPackageJSON } from 'node:module';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { _isTruthy } from '@naturalcycles/js-lib';
|
|
6
6
|
import { _uniq } from '@naturalcycles/js-lib/array';
|
|
7
|
+
import { nowUnixMillis } from '@naturalcycles/js-lib/datetime/localTime.js';
|
|
7
8
|
import { _since } from '@naturalcycles/js-lib/datetime/time.util.js';
|
|
8
9
|
import { _assert } from '@naturalcycles/js-lib/error/assert.js';
|
|
9
10
|
import { _filterFalsyValues } from '@naturalcycles/js-lib/object/object.util.js';
|
|
@@ -24,7 +25,7 @@ const { CI } = process.env;
|
|
|
24
25
|
*/
|
|
25
26
|
export async function runCheck(opt = {}) {
|
|
26
27
|
const { fastLinters = true, eslint = true, stylelint = true, oxfmt = true, ktlint = true, typecheck = false, test = true, } = opt;
|
|
27
|
-
const started =
|
|
28
|
+
const started = nowUnixMillis();
|
|
28
29
|
let s;
|
|
29
30
|
const timings = {};
|
|
30
31
|
// const { commitOnChanges, failOnChanges } = _yargs().options({
|
|
@@ -49,52 +50,52 @@ export async function runCheck(opt = {}) {
|
|
|
49
50
|
const fix = !CI;
|
|
50
51
|
if (fastLinters) {
|
|
51
52
|
// Fast linters (that run in <1 second) go first
|
|
52
|
-
s =
|
|
53
|
+
s = nowUnixMillis();
|
|
53
54
|
if (runActionLint()) {
|
|
54
|
-
timings['actionlint'] =
|
|
55
|
+
timings['actionlint'] = nowUnixMillis() - s;
|
|
55
56
|
}
|
|
56
|
-
s =
|
|
57
|
+
s = nowUnixMillis();
|
|
57
58
|
if (runOxlint(fix)) {
|
|
58
|
-
timings['oxlint'] =
|
|
59
|
+
timings['oxlint'] = nowUnixMillis() - s;
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
// From this point we start the "slow" linters, with ESLint leading the way
|
|
62
63
|
if (eslint) {
|
|
63
64
|
// We run eslint BEFORE Prettier, because eslint can delete e.g unused imports.
|
|
64
|
-
s =
|
|
65
|
+
s = nowUnixMillis();
|
|
65
66
|
if (eslintAll({ fix })) {
|
|
66
|
-
timings['eslint'] =
|
|
67
|
+
timings['eslint'] = nowUnixMillis() - s;
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
if (stylelint &&
|
|
70
71
|
hasDependencyInNodeModules('stylelint') &&
|
|
71
72
|
hasDependencyInNodeModules('stylelint-config-standard-scss')) {
|
|
72
|
-
s =
|
|
73
|
+
s = nowUnixMillis();
|
|
73
74
|
if (stylelintAll(fix)) {
|
|
74
|
-
timings['stylelint'] =
|
|
75
|
+
timings['stylelint'] = nowUnixMillis() - s;
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
if (oxfmt) {
|
|
78
|
-
s =
|
|
79
|
+
s = nowUnixMillis();
|
|
79
80
|
if (runOxfmt(fix)) {
|
|
80
|
-
timings['oxfmt'] =
|
|
81
|
+
timings['oxfmt'] = nowUnixMillis() - s;
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
if (ktlint) {
|
|
84
|
-
s =
|
|
85
|
+
s = nowUnixMillis();
|
|
85
86
|
if (await runKTLint(fix)) {
|
|
86
|
-
timings['ktlint'] =
|
|
87
|
+
timings['ktlint'] = nowUnixMillis() - s;
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
if (typecheck) {
|
|
90
|
-
s =
|
|
91
|
+
s = nowUnixMillis();
|
|
91
92
|
await runTypecheck();
|
|
92
|
-
timings['tsc'] =
|
|
93
|
+
timings['tsc'] = nowUnixMillis() - s;
|
|
93
94
|
}
|
|
94
95
|
if (test) {
|
|
95
|
-
s =
|
|
96
|
+
s = nowUnixMillis();
|
|
96
97
|
if (runTest()) {
|
|
97
|
-
timings['test'] =
|
|
98
|
+
timings['test'] = nowUnixMillis() - s;
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
101
|
console.log(`${check(true)}${white(`check`)} ${dimGrey(`took ` + _since(started))}`);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { localDate } from '@naturalcycles/js-lib/datetime/localDate.js';
|
|
1
2
|
/**
|
|
2
3
|
* Generate release notes markdown for a GitHub Release,
|
|
3
4
|
* following the format of `@semantic-release/release-notes-generator` (angular preset):
|
|
@@ -5,7 +6,7 @@
|
|
|
5
6
|
*/
|
|
6
7
|
export function generateReleaseNotes(input) {
|
|
7
8
|
const { repo, commits, lastTag, nextTag } = input;
|
|
8
|
-
const date = input.date ||
|
|
9
|
+
const date = input.date || localDate.todayInUTC().toISODate();
|
|
9
10
|
const header = lastTag
|
|
10
11
|
? `## [${nextTag}](${repo.url}/compare/${lastTag}...${nextTag}) (${date})`
|
|
11
12
|
: `## ${nextTag} (${date})`;
|
package/dist/vendor/mitm.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface BypassableSocket extends NetType.Socket {
|
|
|
11
11
|
bypass: () => void;
|
|
12
12
|
bypassed?: boolean;
|
|
13
13
|
}
|
|
14
|
-
export type ConnectCallback = (socket: BypassableSocket,
|
|
14
|
+
export type ConnectCallback = (socket: BypassableSocket, opt: SocketOptions) => void;
|
|
15
15
|
export interface Mitm {
|
|
16
16
|
on: (event: 'connect', callback: ConnectCallback) => void;
|
|
17
17
|
disable: () => void;
|
package/dist/vendor/mitm.js
CHANGED
|
@@ -18,7 +18,7 @@ export function createMitm() {
|
|
|
18
18
|
s[0][s[1]] = s[2];
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
-
function connect(orig,
|
|
21
|
+
function connect(orig, opt, done) {
|
|
22
22
|
// Create a bypassable socket that we'll pass to listeners
|
|
23
23
|
const socket = new Net.Socket();
|
|
24
24
|
socket.bypassed = false;
|
|
@@ -27,11 +27,11 @@ export function createMitm() {
|
|
|
27
27
|
};
|
|
28
28
|
// Emit connect event to all listeners
|
|
29
29
|
for (const listener of listeners) {
|
|
30
|
-
listener(socket,
|
|
30
|
+
listener(socket, opt);
|
|
31
31
|
}
|
|
32
32
|
// If bypassed, call the original connect function
|
|
33
33
|
if (socket.bypassed) {
|
|
34
|
-
return orig.call(Net,
|
|
34
|
+
return orig.call(Net, opt, done);
|
|
35
35
|
}
|
|
36
36
|
// Not bypassed - return the socket that will never actually connect.
|
|
37
37
|
// testOffline throws an error before we get here, so this is just a fallback.
|
|
@@ -43,12 +43,12 @@ export function createMitm() {
|
|
|
43
43
|
// connect(options, callback?)
|
|
44
44
|
// connect(path, callback?) - Unix socket
|
|
45
45
|
if (typeof args[0] === 'number') {
|
|
46
|
-
const
|
|
46
|
+
const opt = { port: args[0] };
|
|
47
47
|
if (typeof args[1] === 'string') {
|
|
48
|
-
|
|
49
|
-
return [
|
|
48
|
+
opt.host = args[1];
|
|
49
|
+
return [opt, args[2]];
|
|
50
50
|
}
|
|
51
|
-
return [
|
|
51
|
+
return [opt, args[1]];
|
|
52
52
|
}
|
|
53
53
|
if (typeof args[0] === 'object' && args[0] !== null) {
|
|
54
54
|
return [args[0], args[1]];
|
|
@@ -58,14 +58,14 @@ export function createMitm() {
|
|
|
58
58
|
}
|
|
59
59
|
function createNetConnect(orig) {
|
|
60
60
|
return function (...args) {
|
|
61
|
-
const [
|
|
62
|
-
return connect(orig,
|
|
61
|
+
const [opt, done] = normalizeArgs(args);
|
|
62
|
+
return connect(orig, opt, done);
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
65
|
function createTlsConnect(orig) {
|
|
66
66
|
return function (...args) {
|
|
67
|
-
const [
|
|
68
|
-
return connect(orig,
|
|
67
|
+
const [opt, done] = normalizeArgs(args);
|
|
68
|
+
return connect(orig, opt, done);
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
// Stub network functions
|