@jterrazz/typescript 10.1.0 → 10.1.2
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/bin/commands/check.sh +17 -10
- package/lib/check-baseline.js +12 -1
- package/lib/check-markdown.js +39 -7
- package/lib/unsafe-fixers.js +33 -11
- package/package.json +1 -1
- package/src/index.d.ts +1 -0
- package/src/index.js +2 -0
- package/src/oxlint.d.ts +7 -0
- package/src/oxlint.js +11 -0
- package/src/oxlint.test.ts +12 -1
package/bin/commands/check.sh
CHANGED
|
@@ -359,16 +359,20 @@ run_checks() {
|
|
|
359
359
|
local format_status=0
|
|
360
360
|
if [ "$FIX_MODE" = true ]; then
|
|
361
361
|
# A fixer that changes MEANING is never applied unattended: the rules
|
|
362
|
-
# marked `unsafe` in the manifest are
|
|
363
|
-
#
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
362
|
+
# marked `unsafe` in the manifest are turned off for THIS run by a
|
|
363
|
+
# wrapper config written beside the consumer's own, so `fix` leaves
|
|
364
|
+
# them alone and `check` still reports them for a human.
|
|
365
|
+
local fix_config="oxlint.fix.config.mjs"
|
|
366
|
+
local fix_args=()
|
|
367
|
+
if [ -n "$OXLINT_CONFIG" ]; then
|
|
368
|
+
node "$PACKAGE_ROOT/lib/unsafe-fixers.js" "$PWD/$OXLINT_CONFIG" "$fix_config"
|
|
369
|
+
fix_args=(-c "$fix_config")
|
|
370
|
+
fi
|
|
371
|
+
|
|
372
|
+
"$OXLINT" --type-aware --fix "${fix_args[@]}" "${LINT_ARGS[@]}" \
|
|
370
373
|
> "$tmp_dir/lint.log" 2>&1 ||
|
|
371
374
|
lint_status=$?
|
|
375
|
+
rm -f "$fix_config"
|
|
372
376
|
"$OXFMT" > "$tmp_dir/format.log" 2>&1 || format_status=$?
|
|
373
377
|
else
|
|
374
378
|
"$OXLINT" --type-aware "${LINT_ARGS[@]}" > "$tmp_dir/lint.log" 2>&1 &
|
|
@@ -614,8 +618,11 @@ run_checks() {
|
|
|
614
618
|
# Bash decides whether the file is there; the script decides what it says.
|
|
615
619
|
#
|
|
616
620
|
# What fix mode judges is what SURVIVED the rewrite, so its machine-readable
|
|
617
|
-
# run is made here, after the fixer, and in the foreground
|
|
618
|
-
|
|
621
|
+
# run is made here, after the fixer, and in the foreground — with the unsafe
|
|
622
|
+
# fixers armed again, so a directive that names one of them is used, not
|
|
623
|
+
# reported unused. The fixer's own exit code is never the verdict: it ran
|
|
624
|
+
# with those rules allowed. Without a baseline the judge wants zero.
|
|
625
|
+
if [ -f "$BASELINE_FILE" ] || [ "$FIX_MODE" = true ]; then
|
|
619
626
|
if [ -n "$lint_json_pid" ]; then
|
|
620
627
|
wait $lint_json_pid
|
|
621
628
|
else
|
package/lib/check-baseline.js
CHANGED
|
@@ -128,7 +128,18 @@ if (import.meta.main) {
|
|
|
128
128
|
const baseline = readBaseline(root);
|
|
129
129
|
|
|
130
130
|
if (baseline === null) {
|
|
131
|
-
|
|
131
|
+
const rules = Object.keys(counts).toSorted();
|
|
132
|
+
for (const rule of rules) {
|
|
133
|
+
stdout.write(
|
|
134
|
+
`${rule} ${counts[rule]} diagnostic(s), and the project keeps no baseline\n`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (rules.length > 0) {
|
|
138
|
+
stdout.write(
|
|
139
|
+
"Fix them, or run 'typescript baseline' to record where the project stands.\n",
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
exit(rules.length > 0 ? 1 : 0);
|
|
132
143
|
}
|
|
133
144
|
|
|
134
145
|
const broken = judge(counts, baseline);
|
package/lib/check-markdown.js
CHANGED
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
* paragraph into the two ideas it was carrying.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
import { spawnSync } from 'node:child_process';
|
|
23
24
|
import { existsSync } from 'node:fs';
|
|
24
|
-
import { dirname, join, resolve } from 'node:path';
|
|
25
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
25
26
|
import { argv, exit, stdout } from 'node:process';
|
|
26
27
|
|
|
27
28
|
import { ignorePatternsOf, readText, trackedFiles } from './tracked-files.js';
|
|
@@ -216,18 +217,31 @@ function isRelative(target) {
|
|
|
216
217
|
);
|
|
217
218
|
}
|
|
218
219
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
220
|
+
/*
|
|
221
|
+
* A coordinate git ignores is not stale: a build product, a workbench clone
|
|
222
|
+
* or a generated tree is absent from a fresh checkout by design, and the page
|
|
223
|
+
* that names it is still true. Outside a repository nothing is ignored.
|
|
224
|
+
*/
|
|
225
|
+
function isIgnored(root, target) {
|
|
226
|
+
const probe = spawnSync('git', ['check-ignore', '-q', '--no-index', target], {
|
|
227
|
+
cwd: root,
|
|
228
|
+
stdio: 'ignore',
|
|
229
|
+
});
|
|
223
230
|
|
|
231
|
+
return probe.status === 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** The links of one page that resolve to nothing, ignored coordinates forgiven. */
|
|
235
|
+
function missingLinks(root, path, cited) {
|
|
236
|
+
const found = [];
|
|
224
237
|
for (const match of cited.matchAll(LINK)) {
|
|
225
238
|
const { target } = match.groups;
|
|
226
239
|
const clean = target.split('#')[0] ?? '';
|
|
227
240
|
if (!isRelative(target) || clean === '') {
|
|
228
241
|
continue;
|
|
229
242
|
}
|
|
230
|
-
|
|
243
|
+
const linked = resolve(root, dirname(path), clean);
|
|
244
|
+
if (!existsSync(linked) && !isIgnored(root, relative(root, linked))) {
|
|
231
245
|
found.push({
|
|
232
246
|
message: `${target} resolves to nothing on disk`,
|
|
233
247
|
rule: 'markdown-link-missing',
|
|
@@ -235,9 +249,19 @@ function auditPage(root, path, markdown) {
|
|
|
235
249
|
}
|
|
236
250
|
}
|
|
237
251
|
|
|
252
|
+
return found;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** The backticked paths of one page that name nothing, ignored coordinates forgiven. */
|
|
256
|
+
function missingPaths(root, path, cited) {
|
|
257
|
+
const found = [];
|
|
238
258
|
for (const match of cited.matchAll(BACKTICK_PATH)) {
|
|
239
259
|
const target = match.groups.path.replace(/\/$/u, '');
|
|
240
|
-
if (
|
|
260
|
+
if (
|
|
261
|
+
!existsSync(join(root, target)) &&
|
|
262
|
+
!existsSync(resolve(root, dirname(path), target)) &&
|
|
263
|
+
!isIgnored(root, target)
|
|
264
|
+
) {
|
|
241
265
|
found.push({
|
|
242
266
|
message: `\`${target}\` names nothing on disk`,
|
|
243
267
|
rule: 'markdown-path-missing',
|
|
@@ -245,6 +269,14 @@ function auditPage(root, path, markdown) {
|
|
|
245
269
|
}
|
|
246
270
|
}
|
|
247
271
|
|
|
272
|
+
return found;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Every violation of one page, in the order the rules are declared. */
|
|
276
|
+
function auditPage(root, path, markdown) {
|
|
277
|
+
const cited = withoutFences(markdown);
|
|
278
|
+
const found = [...missingLinks(root, path, cited), ...missingPaths(root, path, cited)];
|
|
279
|
+
|
|
248
280
|
for (const reason of longProseBlocks(markdown)) {
|
|
249
281
|
found.push({ message: reason, rule: 'markdown-block-long' });
|
|
250
282
|
}
|
package/lib/unsafe-fixers.js
CHANGED
|
@@ -1,25 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
4
|
+
* The config the oxlint pass of `fix` runs with: the consumer's own, plus one
|
|
5
|
+
* trailing override turning every rule marked `unsafe` in the manifest off.
|
|
6
6
|
*
|
|
7
7
|
* A fixer that changes MEANING cannot be applied unattended — it turns working
|
|
8
8
|
* code into code that does not compile, or into code that claims something
|
|
9
|
-
* else.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* else. An override is the one form that reaches a rule wherever it was armed:
|
|
10
|
+
* a CLI `--allow` stops at the base config, and a rule the consumer's own
|
|
11
|
+
* overrides arm again (the vitest block, on the test globs) would still
|
|
12
|
+
* rewrite. Check mode keeps every one of them armed: the diagnostic is still
|
|
13
|
+
* owed an answer, from a human ([Quality checks](../docs/06-quality-checks.md)).
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
+
* The file is written beside the consumer's config, never elsewhere: oxlint
|
|
16
|
+
* resolves `ignorePatterns` against the directory its config sits in.
|
|
17
|
+
*
|
|
18
|
+
* Usage: node unsafe-fixers.js <consumer config, absolute> <wrapper to write>
|
|
15
19
|
*/
|
|
16
20
|
|
|
17
|
-
import {
|
|
21
|
+
import { writeFileSync } from 'node:fs';
|
|
22
|
+
import { argv } from 'node:process';
|
|
18
23
|
|
|
19
24
|
import { unsafeFixers } from '../rules/catalog.js';
|
|
20
25
|
|
|
26
|
+
/** The wrapper module's source, for a JSON or an ES module config. */
|
|
27
|
+
export function wrapperOf(consumerConfig) {
|
|
28
|
+
const off = Object.fromEntries(unsafeFixers().map(({ rule }) => [rule, 'off']));
|
|
29
|
+
const attributes = consumerConfig.endsWith('.json') ? " with { type: 'json' }" : '';
|
|
30
|
+
|
|
31
|
+
return [
|
|
32
|
+
`import base from ${JSON.stringify(consumerConfig)}${attributes};`,
|
|
33
|
+
'',
|
|
34
|
+
`const OFF = ${JSON.stringify(off)};`,
|
|
35
|
+
'',
|
|
36
|
+
'export default {',
|
|
37
|
+
' ...base,',
|
|
38
|
+
" overrides: [...(base.overrides ?? []), { files: ['**/*'], rules: OFF }],",
|
|
39
|
+
'};',
|
|
40
|
+
'',
|
|
41
|
+
].join('\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
21
44
|
if (import.meta.main) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
45
|
+
const [consumerConfig, wrapper] = argv.slice(2);
|
|
46
|
+
writeFileSync(wrapper, wrapperOf(consumerConfig));
|
|
25
47
|
}
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
package/src/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import nodeProfile from '../presets/oxlint/profiles/node.js';
|
|
|
8
8
|
import reactProfile from '../presets/oxlint/profiles/react.js';
|
|
9
9
|
import hexagonalFragment from '../rules/architecture/hexagonal.js';
|
|
10
10
|
import { compile } from '../rules/compile.js';
|
|
11
|
+
import { nested } from './oxlint.js';
|
|
11
12
|
|
|
12
13
|
export const oxfmt = oxfmtConfig;
|
|
13
14
|
|
|
@@ -17,6 +18,7 @@ export const oxlint = {
|
|
|
17
18
|
expo: expoProfile,
|
|
18
19
|
hexagonal: compile(hexagonalFragment),
|
|
19
20
|
library: libraryProfile,
|
|
21
|
+
nested,
|
|
20
22
|
next: nextProfile,
|
|
21
23
|
node: nodeProfile,
|
|
22
24
|
react: reactProfile,
|
package/src/oxlint.d.ts
CHANGED
|
@@ -46,6 +46,12 @@ declare function compose(...configs: OxlintConfig[]): OxlintConfig;
|
|
|
46
46
|
*/
|
|
47
47
|
declare function layers(definition: { id?: string; map: readonly Layer[] }): OxlintConfig;
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* A config for a nested oxlint.config: the profile without the linter
|
|
51
|
+
* `options` oxlint accepts in the root config only.
|
|
52
|
+
*/
|
|
53
|
+
declare function nested(config: OxlintConfig): OxlintConfig;
|
|
54
|
+
|
|
49
55
|
export { defineConfig } from 'oxlint';
|
|
50
56
|
export { type OxlintConfig, type OxlintOverride } from 'oxlint';
|
|
51
57
|
export {
|
|
@@ -58,6 +64,7 @@ export {
|
|
|
58
64
|
type Layer,
|
|
59
65
|
layers,
|
|
60
66
|
library,
|
|
67
|
+
nested,
|
|
61
68
|
next,
|
|
62
69
|
node,
|
|
63
70
|
react,
|
package/src/oxlint.js
CHANGED
|
@@ -48,3 +48,14 @@ export const hexagonal = compile(hexagonalFragment);
|
|
|
48
48
|
export function layers(definition) {
|
|
49
49
|
return compile(layersFragment(definition));
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A config for a NESTED oxlint.config: oxlint accepts linter `options` in the
|
|
54
|
+
* root config only, and the root's already turn type information on for the
|
|
55
|
+
* whole tree, so a subtree's profile ships without its own.
|
|
56
|
+
*/
|
|
57
|
+
export function nested(config) {
|
|
58
|
+
const { options: _rootOnly, ...subtree } = config;
|
|
59
|
+
|
|
60
|
+
return subtree;
|
|
61
|
+
}
|
package/src/oxlint.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { expect, test } from 'vitest';
|
|
2
2
|
|
|
3
|
-
import { compose, defineConfig, hexagonal, layers } from './oxlint.js';
|
|
3
|
+
import { compose, defineConfig, hexagonal, layers, nested, react } from './oxlint.js';
|
|
4
4
|
|
|
5
5
|
test('concatenates and dedupes plugin lists', () => {
|
|
6
6
|
// Given - two configs sharing one jsPlugin
|
|
@@ -139,3 +139,14 @@ test("re-exports oxlint's own defineConfig", () => {
|
|
|
139
139
|
// Then - the entry carries the tool's helper, which returns the config unchanged
|
|
140
140
|
expect(defineConfig(config)).toBe(config);
|
|
141
141
|
});
|
|
142
|
+
|
|
143
|
+
test('a nested config carries no linter options, which oxlint reads in the root only', () => {
|
|
144
|
+
// Given - a profile, which ships the options every root config needs
|
|
145
|
+
expect(react.options).toBeDefined();
|
|
146
|
+
|
|
147
|
+
// Then - its nested form keeps everything but them
|
|
148
|
+
const subtree = nested(compose(react, { rules: { curly: 'off' } }));
|
|
149
|
+
expect(subtree.options).toBeUndefined();
|
|
150
|
+
expect(subtree.plugins).toStrictEqual(react.plugins);
|
|
151
|
+
expect(subtree.rules?.curly).toBe('off');
|
|
152
|
+
});
|