@jterrazz/typescript 10.1.1 → 10.1.3
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 +12 -8
- package/lib/check-markdown.js +42 -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 &
|
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,34 @@ 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
|
+
// An absent path has no kind: `dist/` in .gitignore matches the directory
|
|
227
|
+
// form only, so both spellings are asked, one probe each (`-q` takes one).
|
|
228
|
+
return [target, `${target}/`].some(
|
|
229
|
+
(spelling) =>
|
|
230
|
+
spawnSync('git', ['check-ignore', '-q', '--no-index', spelling], {
|
|
231
|
+
cwd: root,
|
|
232
|
+
stdio: 'ignore',
|
|
233
|
+
}).status === 0,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
223
236
|
|
|
237
|
+
/** The links of one page that resolve to nothing, ignored coordinates forgiven. */
|
|
238
|
+
function missingLinks(root, path, cited) {
|
|
239
|
+
const found = [];
|
|
224
240
|
for (const match of cited.matchAll(LINK)) {
|
|
225
241
|
const { target } = match.groups;
|
|
226
242
|
const clean = target.split('#')[0] ?? '';
|
|
227
243
|
if (!isRelative(target) || clean === '') {
|
|
228
244
|
continue;
|
|
229
245
|
}
|
|
230
|
-
|
|
246
|
+
const linked = resolve(root, dirname(path), clean);
|
|
247
|
+
if (!existsSync(linked) && !isIgnored(root, relative(root, linked))) {
|
|
231
248
|
found.push({
|
|
232
249
|
message: `${target} resolves to nothing on disk`,
|
|
233
250
|
rule: 'markdown-link-missing',
|
|
@@ -235,9 +252,19 @@ function auditPage(root, path, markdown) {
|
|
|
235
252
|
}
|
|
236
253
|
}
|
|
237
254
|
|
|
255
|
+
return found;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** The backticked paths of one page that name nothing, ignored coordinates forgiven. */
|
|
259
|
+
function missingPaths(root, path, cited) {
|
|
260
|
+
const found = [];
|
|
238
261
|
for (const match of cited.matchAll(BACKTICK_PATH)) {
|
|
239
262
|
const target = match.groups.path.replace(/\/$/u, '');
|
|
240
|
-
if (
|
|
263
|
+
if (
|
|
264
|
+
!existsSync(join(root, target)) &&
|
|
265
|
+
!existsSync(resolve(root, dirname(path), target)) &&
|
|
266
|
+
!isIgnored(root, target)
|
|
267
|
+
) {
|
|
241
268
|
found.push({
|
|
242
269
|
message: `\`${target}\` names nothing on disk`,
|
|
243
270
|
rule: 'markdown-path-missing',
|
|
@@ -245,6 +272,14 @@ function auditPage(root, path, markdown) {
|
|
|
245
272
|
}
|
|
246
273
|
}
|
|
247
274
|
|
|
275
|
+
return found;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Every violation of one page, in the order the rules are declared. */
|
|
279
|
+
function auditPage(root, path, markdown) {
|
|
280
|
+
const cited = withoutFences(markdown);
|
|
281
|
+
const found = [...missingLinks(root, path, cited), ...missingPaths(root, path, cited)];
|
|
282
|
+
|
|
248
283
|
for (const reason of longProseBlocks(markdown)) {
|
|
249
284
|
found.push({ message: reason, rule: 'markdown-block-long' });
|
|
250
285
|
}
|
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
|
+
});
|