@jterrazz/typescript 9.3.0 → 10.0.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/README.md +20 -16
- package/bin/commands/check.sh +348 -117
- package/bin/typescript.sh +55 -0
- package/lib/check-architecture.js +89 -0
- package/lib/check-baseline.js +144 -0
- package/lib/check-docs.js +4 -3
- package/lib/check-drift.js +209 -0
- package/lib/check-gitignore.js +4 -4
- package/lib/check-markdown.js +279 -0
- package/lib/check-names.js +125 -0
- package/lib/check-publish.js +150 -0
- package/lib/check-secrets.js +115 -0
- package/lib/check-suppressions.js +355 -0
- package/lib/doctor.js +185 -0
- package/lib/merge-knip-config.js +57 -25
- package/lib/tracked-files.js +165 -0
- package/lib/workspace-members.js +5 -6
- package/package.json +19 -8
- package/presets/oxfmt/index.js +49 -5
- package/presets/oxlint/profiles/astro.js +10 -0
- package/presets/oxlint/profiles/bun.js +7 -0
- package/presets/oxlint/profiles/expo.js +7 -0
- package/presets/oxlint/profiles/library.js +16 -0
- package/presets/oxlint/profiles/next.js +7 -0
- package/presets/oxlint/profiles/node.js +7 -0
- package/presets/prettier/astro.json +6 -0
- package/presets/tsconfig/expo.json +16 -6
- package/presets/tsconfig/library.json +18 -0
- package/presets/tsconfig/next.json +12 -2
- package/presets/tsconfig/node.json +18 -4
- package/rules/README.md +23 -0
- package/rules/_contract.js +191 -0
- package/rules/_contract.test.ts +81 -0
- package/rules/a11y.js +51 -0
- package/rules/architecture/hexagonal.js +56 -0
- package/rules/architecture/layers.js +75 -0
- package/rules/astro.js +49 -0
- package/rules/catalog.js +134 -0
- package/rules/catalog.test.ts +84 -0
- package/rules/compile.js +125 -0
- package/rules/core/eslint.js +234 -0
- package/rules/core/import.js +107 -0
- package/rules/core/jsdoc.js +52 -0
- package/rules/core/node.js +36 -0
- package/rules/core/oxc.js +54 -0
- package/rules/core/promise.js +39 -0
- package/rules/core/typescript.js +204 -0
- package/rules/core/unicorn.js +200 -0
- package/rules/next.js +53 -0
- package/rules/profiles.js +89 -0
- package/rules/react-native.js +48 -0
- package/rules/react.js +148 -0
- package/rules/sorted.js +41 -0
- package/rules/vitest.js +153 -0
- package/src/docs.d.ts +4 -4
- package/src/docs.js +75 -57
- package/src/docs.test.ts +43 -32
- package/src/index.d.ts +13 -9
- package/src/index.js +15 -8
- package/src/oxfmt.d.ts +15 -2
- package/src/oxfmt.test.ts +10 -0
- package/src/oxlint.d.ts +57 -10
- package/src/oxlint.js +35 -50
- package/src/oxlint.test.ts +82 -28
- package/presets/oxlint/architectures/hexagonal-rules.js +0 -39
- package/presets/oxlint/architectures/hexagonal.js +0 -13
- package/presets/oxlint/base.js +0 -145
- package/presets/oxlint/expo.js +0 -36
- package/presets/oxlint/next.js +0 -43
- package/presets/oxlint/node.js +0 -14
- package/presets/oxlint/plugins/codestyle.js +0 -231
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { expect, test } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { catalog, MARKERS, render, renderReference } from './catalog.js';
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* The catalogue has two readers and one source. `docs/07-lint-presets.md` is
|
|
9
|
+
* the human's, with the reason and the version behind every decision; the
|
|
10
|
+
* skill reference is an agent's, trimmed to what it needs to answer "may I
|
|
11
|
+
* write this". Both carry a table between two markers, both are PROJECTIONS of
|
|
12
|
+
* `rules/`, and this suite fails either one that has drifted. Regenerate with
|
|
13
|
+
* `TEST_UPDATE=1 npm test`, the gesture every other golden here takes.
|
|
14
|
+
*
|
|
15
|
+
* It is the @jterrazz/test pattern — one manifest, every roster derived from
|
|
16
|
+
* it, freshness meta-tested — so a decision is never recorded in two places
|
|
17
|
+
* that can disagree.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const PROJECTIONS = {
|
|
21
|
+
chapter: {
|
|
22
|
+
page: resolve(import.meta.dirname, '../docs/07-lint-presets.md'),
|
|
23
|
+
render,
|
|
24
|
+
},
|
|
25
|
+
'skill reference': {
|
|
26
|
+
page: resolve(import.meta.dirname, '../skills/jterrazz-typescript/references/rules.md'),
|
|
27
|
+
render: renderReference,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** A markdown table as its cells, with the alignment padding taken out. */
|
|
32
|
+
function cells(table: string): string[] {
|
|
33
|
+
return table
|
|
34
|
+
.trim()
|
|
35
|
+
.split('\n')
|
|
36
|
+
.map((row) =>
|
|
37
|
+
row
|
|
38
|
+
.split('|')
|
|
39
|
+
.map((cell) => cell.trim().replaceAll(/^-+$/gu, '---'))
|
|
40
|
+
.join(' | '),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test('every decision carries the version it was taken in', () => {
|
|
45
|
+
// Given - the whole catalogue
|
|
46
|
+
const entries = catalog();
|
|
47
|
+
expect(entries.length).toBeGreaterThan(500);
|
|
48
|
+
|
|
49
|
+
// Then - each one names a semver, which is what replaces a changelog here
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
expect.soft(entry.since, `${entry.rule} has no since`).toMatch(/^\d+\.\d+\.\d+$/u);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test.each(Object.entries(PROJECTIONS))(
|
|
56
|
+
'the %s carries the catalogue the manifest renders',
|
|
57
|
+
(_name, { page, render: project }) => {
|
|
58
|
+
// Given - the generated section of the projection
|
|
59
|
+
const before = readFileSync(page, 'utf8');
|
|
60
|
+
const start = before.indexOf(MARKERS.start);
|
|
61
|
+
const end = before.indexOf(MARKERS.end);
|
|
62
|
+
expect(start, 'the page has lost its GENERATED marker').toBeGreaterThan(-1);
|
|
63
|
+
expect(end, 'the page has lost its /GENERATED marker').toBeGreaterThan(start);
|
|
64
|
+
|
|
65
|
+
// Then - it is exactly what the manifest renders today
|
|
66
|
+
const table = project();
|
|
67
|
+
if (process.env.TEST_UPDATE === '1') {
|
|
68
|
+
const head = before.slice(0, start + MARKERS.start.length);
|
|
69
|
+
writeFileSync(page, `${head}\n\n${table}\n\n${before.slice(end)}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/*
|
|
73
|
+
* The comparison is column-insensitive on purpose: oxfmt owns markdown
|
|
74
|
+
* and it aligns a table's pipes. What a projection must carry is the
|
|
75
|
+
* DECISIONS.
|
|
76
|
+
*/
|
|
77
|
+
const fresh = readFileSync(page, 'utf8');
|
|
78
|
+
const carried = fresh.slice(
|
|
79
|
+
fresh.indexOf(MARKERS.start) + MARKERS.start.length,
|
|
80
|
+
fresh.indexOf(MARKERS.end),
|
|
81
|
+
);
|
|
82
|
+
expect(cells(carried)).toStrictEqual(cells(table));
|
|
83
|
+
},
|
|
84
|
+
);
|
package/rules/compile.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Fragment -> oxlint config. The compiler is the only place a rule level is
|
|
3
|
+
* spelled, and it never emits `categories`: a category switch arms rules of
|
|
4
|
+
* plugins the config never names, inert until a framework config activates the
|
|
5
|
+
* plugin and then firing unannounced. Every rule of this package is decided by
|
|
6
|
+
* name ([Lint presets](../docs/07-lint-presets.md)).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** The linter options every profile ships: type information on, dead directives refused. */
|
|
10
|
+
export const OPTIONS = Object.freeze({
|
|
11
|
+
reportUnusedDisableDirectives: 'error',
|
|
12
|
+
typeAware: true,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/** Config keys concatenated across configs, duplicates dropped (===). */
|
|
16
|
+
const CONCAT_DEDUPE = new Set(['extends', 'ignorePatterns', 'jsPlugins', 'plugins']);
|
|
17
|
+
/** Config keys concatenated verbatim — order matters, no dedupe. */
|
|
18
|
+
const CONCAT = new Set(['overrides']);
|
|
19
|
+
/** Config keys shallow-merged as objects — the LAST config wins per key. */
|
|
20
|
+
const SHALLOW_MERGE = new Set(['env', 'globals', 'options', 'rules', 'settings']);
|
|
21
|
+
|
|
22
|
+
/** One fragment as the plain oxlint config object oxlint itself reads. */
|
|
23
|
+
export function compile(fragment) {
|
|
24
|
+
const config = { rules: rulesOf(fragment.decisions) };
|
|
25
|
+
|
|
26
|
+
if (fragment.plugins.length > 0) {
|
|
27
|
+
config.plugins = [...fragment.plugins];
|
|
28
|
+
}
|
|
29
|
+
if (fragment.jsPlugins.length > 0) {
|
|
30
|
+
config.jsPlugins = [...fragment.jsPlugins];
|
|
31
|
+
}
|
|
32
|
+
if (fragment.ignorePatterns.length > 0) {
|
|
33
|
+
config.ignorePatterns = [...fragment.ignorePatterns];
|
|
34
|
+
}
|
|
35
|
+
if (fragment.overrides.length > 0) {
|
|
36
|
+
config.overrides = fragment.overrides.map((override) => ({
|
|
37
|
+
files: [...override.files],
|
|
38
|
+
rules: rulesOf(override.decisions),
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
if (fragment.settings) {
|
|
42
|
+
config.settings = fragment.settings;
|
|
43
|
+
}
|
|
44
|
+
if (fragment.options) {
|
|
45
|
+
config.options = fragment.options;
|
|
46
|
+
}
|
|
47
|
+
if (fragment.env) {
|
|
48
|
+
config.env = fragment.env;
|
|
49
|
+
}
|
|
50
|
+
if (fragment.globals) {
|
|
51
|
+
config.globals = fragment.globals;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return config;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Deterministic merge of oxlint config objects, left to right:
|
|
59
|
+
* `jsPlugins` / `plugins` / `ignorePatterns` / `extends` concatenated and
|
|
60
|
+
* deduped, `rules` / `env` / `globals` / `options` / `settings` shallow-merged
|
|
61
|
+
* with last-wins per key, `overrides` concatenated, any other key taken from
|
|
62
|
+
* the last config that sets it.
|
|
63
|
+
*/
|
|
64
|
+
export function merge(...configs) {
|
|
65
|
+
const merged = {};
|
|
66
|
+
for (const config of configs) {
|
|
67
|
+
if (!config || typeof config !== 'object') {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
for (const [key, value] of Object.entries(config)) {
|
|
71
|
+
if (value !== undefined) {
|
|
72
|
+
merged[key] = mergeKey(key, merged[key], value);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return merged;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** How one key merges: the three tables above, then last-wins. */
|
|
80
|
+
function mergeKey(key, previous, value) {
|
|
81
|
+
if (CONCAT_DEDUPE.has(key)) {
|
|
82
|
+
const combined = [...asArray(previous), ...asArray(value)];
|
|
83
|
+
return combined.filter((entry, index) => combined.indexOf(entry) === index);
|
|
84
|
+
}
|
|
85
|
+
if (CONCAT.has(key)) {
|
|
86
|
+
return [...asArray(previous), ...asArray(value)];
|
|
87
|
+
}
|
|
88
|
+
if (SHALLOW_MERGE.has(key)) {
|
|
89
|
+
return { ...previous, ...value };
|
|
90
|
+
}
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function asArray(value) {
|
|
95
|
+
if (value === undefined) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
return Array.isArray(value) ? value : [value];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A profile: fragments compiled, merged, and given the linter options. */
|
|
102
|
+
export function profile({ fragments, env, globals, ignorePatterns = [] }) {
|
|
103
|
+
return merge(...fragments.map((one) => compile(one)), {
|
|
104
|
+
env,
|
|
105
|
+
globals,
|
|
106
|
+
ignorePatterns,
|
|
107
|
+
options: { ...OPTIONS },
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function rulesOf(decisions) {
|
|
112
|
+
const rules = {};
|
|
113
|
+
for (const [rule, decision] of Object.entries(decisions)) {
|
|
114
|
+
if (decision.level === 'off') {
|
|
115
|
+
rules[rule] = 'off';
|
|
116
|
+
} else if (decision.options === undefined) {
|
|
117
|
+
rules[rule] = 'error';
|
|
118
|
+
} else if (Array.isArray(decision.options)) {
|
|
119
|
+
rules[rule] = ['error', ...decision.options];
|
|
120
|
+
} else {
|
|
121
|
+
rules[rule] = ['error', decision.options];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return rules;
|
|
125
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { allOn, fragment, off, on } from '../_contract.js';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* The `eslint` plugin: oxlint's port of the ESLint core rules, all 183
|
|
5
|
+
* non-nursery ones decided by name. The offs below are the whole list, and
|
|
6
|
+
* each carries the one reason that justifies it.
|
|
7
|
+
*/
|
|
8
|
+
export default fragment({
|
|
9
|
+
id: 'core/eslint',
|
|
10
|
+
plugins: ['eslint'],
|
|
11
|
+
rules: {
|
|
12
|
+
...allOn([
|
|
13
|
+
'accessor-pairs',
|
|
14
|
+
'array-callback-return',
|
|
15
|
+
'block-scoped-var',
|
|
16
|
+
'class-methods-use-this',
|
|
17
|
+
'constructor-super',
|
|
18
|
+
'curly',
|
|
19
|
+
'default-case',
|
|
20
|
+
'default-case-last',
|
|
21
|
+
'default-param-last',
|
|
22
|
+
'eqeqeq',
|
|
23
|
+
'for-direction',
|
|
24
|
+
'func-name-matching',
|
|
25
|
+
'func-names',
|
|
26
|
+
'getter-return',
|
|
27
|
+
'grouped-accessor-pairs',
|
|
28
|
+
'guard-for-in',
|
|
29
|
+
'id-denylist',
|
|
30
|
+
'id-match',
|
|
31
|
+
'logical-assignment-operators',
|
|
32
|
+
'max-classes-per-file',
|
|
33
|
+
'max-nested-callbacks',
|
|
34
|
+
'new-cap',
|
|
35
|
+
'no-alert',
|
|
36
|
+
'no-array-constructor',
|
|
37
|
+
'no-async-promise-executor',
|
|
38
|
+
'no-await-in-loop',
|
|
39
|
+
'no-bitwise',
|
|
40
|
+
'no-caller',
|
|
41
|
+
'no-case-declarations',
|
|
42
|
+
'no-class-assign',
|
|
43
|
+
'no-compare-neg-zero',
|
|
44
|
+
'no-cond-assign',
|
|
45
|
+
'no-console',
|
|
46
|
+
'no-const-assign',
|
|
47
|
+
'no-constant-binary-expression',
|
|
48
|
+
'no-constant-condition',
|
|
49
|
+
'no-constructor-return',
|
|
50
|
+
'no-control-regex',
|
|
51
|
+
'no-debugger',
|
|
52
|
+
'no-delete-var',
|
|
53
|
+
'no-div-regex',
|
|
54
|
+
'no-dupe-class-members',
|
|
55
|
+
'no-dupe-else-if',
|
|
56
|
+
'no-dupe-keys',
|
|
57
|
+
'no-duplicate-case',
|
|
58
|
+
'no-else-return',
|
|
59
|
+
'no-empty',
|
|
60
|
+
'no-empty-character-class',
|
|
61
|
+
'no-empty-function',
|
|
62
|
+
'no-empty-pattern',
|
|
63
|
+
'no-empty-static-block',
|
|
64
|
+
'no-eq-null',
|
|
65
|
+
'no-eval',
|
|
66
|
+
'no-ex-assign',
|
|
67
|
+
'no-extend-native',
|
|
68
|
+
'no-extra-bind',
|
|
69
|
+
'no-extra-boolean-cast',
|
|
70
|
+
'no-extra-label',
|
|
71
|
+
'no-fallthrough',
|
|
72
|
+
'no-func-assign',
|
|
73
|
+
'no-global-assign',
|
|
74
|
+
'no-implicit-coercion',
|
|
75
|
+
'no-implicit-globals',
|
|
76
|
+
'no-import-assign',
|
|
77
|
+
'no-inline-comments',
|
|
78
|
+
'no-inner-declarations',
|
|
79
|
+
'no-invalid-regexp',
|
|
80
|
+
'no-irregular-whitespace',
|
|
81
|
+
'no-iterator',
|
|
82
|
+
'no-label-var',
|
|
83
|
+
'no-labels',
|
|
84
|
+
'no-lone-blocks',
|
|
85
|
+
'no-loop-func',
|
|
86
|
+
'no-loss-of-precision',
|
|
87
|
+
'no-misleading-character-class',
|
|
88
|
+
'no-multi-assign',
|
|
89
|
+
'no-multi-str',
|
|
90
|
+
'no-new',
|
|
91
|
+
'no-new-func',
|
|
92
|
+
'no-new-native-nonconstructor',
|
|
93
|
+
'no-new-wrappers',
|
|
94
|
+
'no-nonoctal-decimal-escape',
|
|
95
|
+
'no-obj-calls',
|
|
96
|
+
'no-object-constructor',
|
|
97
|
+
'no-param-reassign',
|
|
98
|
+
'no-plusplus',
|
|
99
|
+
'no-promise-executor-return',
|
|
100
|
+
'no-proto',
|
|
101
|
+
'no-prototype-builtins',
|
|
102
|
+
'no-redeclare',
|
|
103
|
+
'no-regex-spaces',
|
|
104
|
+
'no-restricted-globals',
|
|
105
|
+
'no-restricted-imports',
|
|
106
|
+
'no-restricted-properties',
|
|
107
|
+
'no-return-assign',
|
|
108
|
+
'no-script-url',
|
|
109
|
+
'no-self-assign',
|
|
110
|
+
'no-self-compare',
|
|
111
|
+
'no-sequences',
|
|
112
|
+
'no-setter-return',
|
|
113
|
+
'no-shadow',
|
|
114
|
+
'no-shadow-restricted-names',
|
|
115
|
+
'no-sparse-arrays',
|
|
116
|
+
'no-template-curly-in-string',
|
|
117
|
+
'no-this-before-super',
|
|
118
|
+
'no-unassigned-vars',
|
|
119
|
+
'no-underscore-dangle',
|
|
120
|
+
'no-unexpected-multiline',
|
|
121
|
+
'no-unneeded-ternary',
|
|
122
|
+
'no-unreachable',
|
|
123
|
+
'no-unsafe-finally',
|
|
124
|
+
'no-unsafe-negation',
|
|
125
|
+
'no-unsafe-optional-chaining',
|
|
126
|
+
'no-unused-labels',
|
|
127
|
+
'no-unused-private-class-members',
|
|
128
|
+
'no-unused-vars',
|
|
129
|
+
'no-useless-backreference',
|
|
130
|
+
'no-useless-call',
|
|
131
|
+
'no-useless-catch',
|
|
132
|
+
'no-useless-computed-key',
|
|
133
|
+
'no-useless-concat',
|
|
134
|
+
'no-useless-constructor',
|
|
135
|
+
'no-useless-escape',
|
|
136
|
+
'no-useless-rename',
|
|
137
|
+
'no-useless-return',
|
|
138
|
+
'no-var',
|
|
139
|
+
'no-warning-comments',
|
|
140
|
+
'no-with',
|
|
141
|
+
'object-shorthand',
|
|
142
|
+
'operator-assignment',
|
|
143
|
+
'prefer-arrow-callback',
|
|
144
|
+
'prefer-const',
|
|
145
|
+
'prefer-exponentiation-operator',
|
|
146
|
+
'prefer-named-capture-group',
|
|
147
|
+
'prefer-numeric-literals',
|
|
148
|
+
'prefer-object-has-own',
|
|
149
|
+
'prefer-object-spread',
|
|
150
|
+
'prefer-regex-literals',
|
|
151
|
+
'prefer-rest-params',
|
|
152
|
+
'prefer-spread',
|
|
153
|
+
'prefer-template',
|
|
154
|
+
'preserve-caught-error',
|
|
155
|
+
'radix',
|
|
156
|
+
'require-unicode-regexp',
|
|
157
|
+
'require-yield',
|
|
158
|
+
'symbol-description',
|
|
159
|
+
'unicode-bom',
|
|
160
|
+
'use-isnan',
|
|
161
|
+
'valid-typeof',
|
|
162
|
+
'vars-on-top',
|
|
163
|
+
'yoda',
|
|
164
|
+
]),
|
|
165
|
+
|
|
166
|
+
// -- On, at the strictest value the option carries --------------------
|
|
167
|
+
'arrow-body-style': on(['as-needed']),
|
|
168
|
+
complexity: on([12]),
|
|
169
|
+
'func-style': on(['declaration', { allowArrowFunctions: true }]),
|
|
170
|
+
'max-depth': on([4]),
|
|
171
|
+
'max-params': on([4]),
|
|
172
|
+
'no-duplicate-imports': on([{ allowSeparateTypeImports: true }]),
|
|
173
|
+
'no-unmodified-loop-condition': on([{ checkConditionalExpressions: true }]),
|
|
174
|
+
/* A hoisted function declaration has no temporal dead zone, and the
|
|
175
|
+
* estate writes the public function first and its helpers below it.
|
|
176
|
+
* The defect this rule names — reading a binding before it exists — is
|
|
177
|
+
* a `let`, a `const` or a `class`, and all three stay refused. */
|
|
178
|
+
'no-use-before-define': on([{ classes: true, functions: false, variables: true }]),
|
|
179
|
+
'no-unused-expressions': on([
|
|
180
|
+
{ allowShortCircuit: true, allowTaggedTemplates: true, allowTernary: true },
|
|
181
|
+
]),
|
|
182
|
+
'no-void': on([{ allowAsStatement: true }]),
|
|
183
|
+
/* The object form names what it takes; the array form past index 0
|
|
184
|
+
* counts commas, and `const [, , , operating] = SPINE` is not clearer
|
|
185
|
+
* than `SPINE[3]`. */
|
|
186
|
+
'prefer-destructuring': on([{ array: false, object: true }]),
|
|
187
|
+
/* `never`, not `always`. Both exist and both fix, but the `always` fixer
|
|
188
|
+
* FUSES adjacent declarations, and a comment standing between two of
|
|
189
|
+
* them is pulled inside the chain — which silently relocates the
|
|
190
|
+
* `// Given -` / `// Then -` narration @jterrazz/test requires. `never`
|
|
191
|
+
* splits chains and leaves every comment where it stands. */
|
|
192
|
+
'one-var': on(['never']),
|
|
193
|
+
|
|
194
|
+
// -- Off, each with its one reason ------------------------------------
|
|
195
|
+
'capitalized-comments': off({
|
|
196
|
+
by: 'jterrazz-design — 198 of 198 reports were false positives (quality study, 2026-09-15)',
|
|
197
|
+
kind: 'evidence',
|
|
198
|
+
}),
|
|
199
|
+
'id-length': off({
|
|
200
|
+
by: 'measured on this package — every report was a conventional short binding (k, v, x)',
|
|
201
|
+
kind: 'evidence',
|
|
202
|
+
}),
|
|
203
|
+
'init-declarations': off({ by: 'no-unassigned-vars, prefer-const', kind: 'covered' }),
|
|
204
|
+
'max-lines': off({ by: 'complexity (12)', kind: 'covered' }),
|
|
205
|
+
'max-lines-per-function': off({ by: 'complexity (12)', kind: 'covered' }),
|
|
206
|
+
'max-statements': off({ by: 'complexity (12)', kind: 'covered' }),
|
|
207
|
+
'no-continue': off({ by: 'max-depth (4)', kind: 'exclusive' }),
|
|
208
|
+
'no-implied-eval': off({ by: 'typescript/no-implied-eval', kind: 'covered' }),
|
|
209
|
+
'no-lonely-if': off({ by: 'unicorn/no-lonely-if', kind: 'covered' }),
|
|
210
|
+
'no-magic-numbers': off({
|
|
211
|
+
by: 'measured on this package — 125 reports, not one of them a defect',
|
|
212
|
+
kind: 'evidence',
|
|
213
|
+
}),
|
|
214
|
+
'no-negated-condition': off({ by: 'unicorn/no-negated-condition', kind: 'covered' }),
|
|
215
|
+
'no-nested-ternary': off({ by: 'unicorn/prefer-ternary', kind: 'exclusive' }),
|
|
216
|
+
'no-ternary': off({ by: 'unicorn/prefer-ternary', kind: 'exclusive' }),
|
|
217
|
+
'no-throw-literal': off({ by: 'typescript/only-throw-error', kind: 'covered' }),
|
|
218
|
+
'no-undefined': off({
|
|
219
|
+
by: 'no-shadow-restricted-names, no-global-assign — `undefined` cannot be rebound, which is the hazard this ES3-era rule was written for',
|
|
220
|
+
kind: 'covered',
|
|
221
|
+
}),
|
|
222
|
+
'prefer-promise-reject-errors': off({
|
|
223
|
+
by: 'typescript/prefer-promise-reject-errors',
|
|
224
|
+
kind: 'covered',
|
|
225
|
+
}),
|
|
226
|
+
'require-await': off({ by: 'typescript/require-await', kind: 'covered' }),
|
|
227
|
+
'sort-imports': off({ by: 'oxfmt sortImports', kind: 'covered' }),
|
|
228
|
+
'sort-keys': off({
|
|
229
|
+
by: 'docs/07-lint-presets.md — an object orders by meaning, and generic inference is order-sensitive',
|
|
230
|
+
kind: 'convention',
|
|
231
|
+
}),
|
|
232
|
+
'sort-vars': off({ by: 'one-var (never)', kind: 'covered' }),
|
|
233
|
+
},
|
|
234
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { allOn, fragment, off, on } from '../_contract.js';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* The `import` plugin, all 31 non-nursery rules decided by name.
|
|
5
|
+
*
|
|
6
|
+
* `import/extensions` is the one rule two platforms answer differently: Node
|
|
7
|
+
* ESM resolves a specifier literally and needs the `.js`, a bundler resolves it
|
|
8
|
+
* and refuses one. Core states the Node answer; `rules/next.js`,
|
|
9
|
+
* `rules/astro.js` and `rules/react-native.js` re-decide it for their platform,
|
|
10
|
+
* at `error` either way — a profile changes the convention, never the level.
|
|
11
|
+
*
|
|
12
|
+
* `import/no-cycle` rides here for +0.05s. Mind what it does NOT see: oxlint's
|
|
13
|
+
* implementation ignores type-only imports, so a value import one way and an
|
|
14
|
+
* `import type` back is invisible to it (oxc#20551).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Asset specifiers keep their extension on every platform — a bundler resolves them by it. */
|
|
18
|
+
const ASSETS = {
|
|
19
|
+
avif: 'always',
|
|
20
|
+
css: 'always',
|
|
21
|
+
gif: 'always',
|
|
22
|
+
jpeg: 'always',
|
|
23
|
+
jpg: 'always',
|
|
24
|
+
json: 'always',
|
|
25
|
+
less: 'always',
|
|
26
|
+
png: 'always',
|
|
27
|
+
sass: 'always',
|
|
28
|
+
scss: 'always',
|
|
29
|
+
svg: 'always',
|
|
30
|
+
webp: 'always',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const EXTENSIONS_ALWAYS = on(['always', { ignorePackages: true, ...ASSETS }]);
|
|
34
|
+
export const EXTENSIONS_NEVER = on(['never', ASSETS]);
|
|
35
|
+
|
|
36
|
+
export default fragment({
|
|
37
|
+
id: 'core/import',
|
|
38
|
+
plugins: ['import'],
|
|
39
|
+
rules: {
|
|
40
|
+
...allOn(
|
|
41
|
+
[
|
|
42
|
+
'default',
|
|
43
|
+
'first',
|
|
44
|
+
'namespace',
|
|
45
|
+
'newline-after-import',
|
|
46
|
+
'no-absolute-path',
|
|
47
|
+
'no-amd',
|
|
48
|
+
'no-commonjs',
|
|
49
|
+
'no-cycle',
|
|
50
|
+
'no-duplicates',
|
|
51
|
+
'no-dynamic-require',
|
|
52
|
+
'no-empty-named-blocks',
|
|
53
|
+
'no-mutable-exports',
|
|
54
|
+
'no-named-as-default',
|
|
55
|
+
'no-named-as-default-member',
|
|
56
|
+
'no-named-default',
|
|
57
|
+
'no-namespace',
|
|
58
|
+
'no-self-import',
|
|
59
|
+
'no-unassigned-import',
|
|
60
|
+
'no-webpack-loader-syntax',
|
|
61
|
+
'unambiguous',
|
|
62
|
+
].map((rule) => `import/${rule}`),
|
|
63
|
+
),
|
|
64
|
+
|
|
65
|
+
// -- On, at the strictest value the option carries ---------------------
|
|
66
|
+
'import/consistent-type-specifier-style': on(['prefer-inline']),
|
|
67
|
+
'import/extensions': EXTENSIONS_ALWAYS,
|
|
68
|
+
|
|
69
|
+
// -- Off, each with its one reason -------------------------------------
|
|
70
|
+
'import/exports-last': off({
|
|
71
|
+
by: 'docs/07-lint-presets.md — an export sits with the declaration it exports; three repositories had refused this rule locally before the decision moved here',
|
|
72
|
+
kind: 'convention',
|
|
73
|
+
}),
|
|
74
|
+
'import/group-exports': off({
|
|
75
|
+
by: 'docs/07-lint-presets.md — an export sits with the declaration it exports',
|
|
76
|
+
kind: 'convention',
|
|
77
|
+
}),
|
|
78
|
+
'import/max-dependencies': off({
|
|
79
|
+
by: 'oxc/no-barrel-file — the defect a dependency count stands in for is the barrel, and that rule names it',
|
|
80
|
+
kind: 'covered',
|
|
81
|
+
}),
|
|
82
|
+
'import/no-anonymous-default-export': off({
|
|
83
|
+
by: 'unicorn/no-anonymous-default-export',
|
|
84
|
+
kind: 'covered',
|
|
85
|
+
}),
|
|
86
|
+
'import/no-default-export': off({
|
|
87
|
+
by: 'docs/07-lint-presets.md — every framework config file and every preset of this package is a default export',
|
|
88
|
+
kind: 'convention',
|
|
89
|
+
}),
|
|
90
|
+
'import/no-named-export': off({
|
|
91
|
+
by: 'docs/07-lint-presets.md — a module exports what it owns by name',
|
|
92
|
+
kind: 'convention',
|
|
93
|
+
}),
|
|
94
|
+
'import/no-nodejs-modules': off({
|
|
95
|
+
by: 'docs/07-lint-presets.md — the estate ships Node services, and a browser boundary is the profile that states it',
|
|
96
|
+
kind: 'convention',
|
|
97
|
+
}),
|
|
98
|
+
'import/no-relative-parent-imports': off({
|
|
99
|
+
by: 'docs/07-lint-presets.md — a relative parent import is how a package reaches its own sibling module; layer boundaries are the layer map, not the path shape',
|
|
100
|
+
kind: 'convention',
|
|
101
|
+
}),
|
|
102
|
+
'import/prefer-default-export': off({
|
|
103
|
+
by: 'docs/07-lint-presets.md — a module exports what it owns by name',
|
|
104
|
+
kind: 'convention',
|
|
105
|
+
}),
|
|
106
|
+
},
|
|
107
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { allOn, fragment, off } from '../_contract.js';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* The `jsdoc` plugin, all 23 rules decided by name. JSDoc is never REQUIRED —
|
|
5
|
+
* TypeScript carries the types and `typescript docs` derives the reference from
|
|
6
|
+
* them — but a block that is written must be well formed.
|
|
7
|
+
*/
|
|
8
|
+
const BY_TYPESCRIPT = {
|
|
9
|
+
by: 'TypeScript — the signature carries the type, and `typescript docs` reads it from there',
|
|
10
|
+
kind: 'covered',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default fragment({
|
|
14
|
+
id: 'core/jsdoc',
|
|
15
|
+
plugins: ['jsdoc'],
|
|
16
|
+
rules: {
|
|
17
|
+
...allOn(
|
|
18
|
+
[
|
|
19
|
+
'check-access',
|
|
20
|
+
'check-property-names',
|
|
21
|
+
'check-tag-names',
|
|
22
|
+
'empty-tags',
|
|
23
|
+
'implements-on-classes',
|
|
24
|
+
'no-blank-blocks',
|
|
25
|
+
'no-defaults',
|
|
26
|
+
'require-param-description',
|
|
27
|
+
'require-param-name',
|
|
28
|
+
'require-property',
|
|
29
|
+
'require-property-description',
|
|
30
|
+
'require-property-name',
|
|
31
|
+
'require-returns-description',
|
|
32
|
+
'require-throws-description',
|
|
33
|
+
'require-yields',
|
|
34
|
+
'require-yields-description',
|
|
35
|
+
].map((rule) => `jsdoc/${rule}`),
|
|
36
|
+
),
|
|
37
|
+
|
|
38
|
+
'jsdoc/require-param': off({
|
|
39
|
+
by: 'TypeScript — a parameter is documented by its type, and a description is optional prose',
|
|
40
|
+
kind: 'covered',
|
|
41
|
+
}),
|
|
42
|
+
'jsdoc/require-param-type': off(BY_TYPESCRIPT),
|
|
43
|
+
'jsdoc/require-property-type': off(BY_TYPESCRIPT),
|
|
44
|
+
'jsdoc/require-returns': off({
|
|
45
|
+
by: 'TypeScript — a return is documented by its type, and a description is optional prose',
|
|
46
|
+
kind: 'covered',
|
|
47
|
+
}),
|
|
48
|
+
'jsdoc/require-returns-type': off(BY_TYPESCRIPT),
|
|
49
|
+
'jsdoc/require-throws-type': off(BY_TYPESCRIPT),
|
|
50
|
+
'jsdoc/require-yields-type': off(BY_TYPESCRIPT),
|
|
51
|
+
},
|
|
52
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { allOn, fragment, off } from '../_contract.js';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* The `node` plugin, all 11 rules decided by name.
|
|
5
|
+
*/
|
|
6
|
+
export default fragment({
|
|
7
|
+
id: 'core/node',
|
|
8
|
+
plugins: ['node'],
|
|
9
|
+
rules: {
|
|
10
|
+
...allOn(
|
|
11
|
+
[
|
|
12
|
+
'callback-return',
|
|
13
|
+
'exports-style',
|
|
14
|
+
'global-require',
|
|
15
|
+
'handle-callback-err',
|
|
16
|
+
'no-exports-assign',
|
|
17
|
+
'no-mixed-requires',
|
|
18
|
+
'no-new-require',
|
|
19
|
+
'no-path-concat',
|
|
20
|
+
].map((rule) => `node/${rule}`),
|
|
21
|
+
),
|
|
22
|
+
|
|
23
|
+
'node/no-process-env': off({
|
|
24
|
+
by: 'docs/07-lint-presets.md — the estate reads configuration from the environment at the edge and has no config-module to funnel it through',
|
|
25
|
+
kind: 'convention',
|
|
26
|
+
}),
|
|
27
|
+
'node/no-sync': off({
|
|
28
|
+
by: 'docs/07-lint-presets.md — a one-shot CLI script has no event loop to protect, and the toolchain gates are exactly that',
|
|
29
|
+
kind: 'convention',
|
|
30
|
+
}),
|
|
31
|
+
'node/no-top-level-await': off({
|
|
32
|
+
by: 'unicorn/prefer-top-level-await — one rule demands it, the other forbids it',
|
|
33
|
+
kind: 'exclusive',
|
|
34
|
+
}),
|
|
35
|
+
},
|
|
36
|
+
});
|