@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.
@@ -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 allowed for THIS run only, so
363
- # `fix` leaves them alone and `check` still reports them for a human.
364
- local unsafe_fixers=()
365
- while IFS= read -r flag; do
366
- [ -n "$flag" ] && unsafe_fixers+=("$flag")
367
- done < <(node "$PACKAGE_ROOT/lib/unsafe-fixers.js")
368
-
369
- "$OXLINT" --type-aware --fix "${unsafe_fixers[@]}" "${LINT_ARGS[@]}" \
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
- if [ -f "$BASELINE_FILE" ]; then
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
@@ -128,7 +128,18 @@ if (import.meta.main) {
128
128
  const baseline = readBaseline(root);
129
129
 
130
130
  if (baseline === null) {
131
- exit(Object.keys(counts).length > 0 ? 1 : 0);
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);
@@ -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
- /** Every violation of one page, in the order the rules are declared. */
220
- function auditPage(root, path, markdown) {
221
- const found = [];
222
- const cited = withoutFences(markdown);
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
- if (!existsSync(resolve(root, dirname(path), clean))) {
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 (!existsSync(join(root, target)) && !existsSync(resolve(root, dirname(path), target))) {
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
  }
@@ -1,25 +1,47 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * The rules `fix` must not let oxlint rewrite, as the flags that switch them
5
- * off for one run: `--allow <rule>` per rule, one per line.
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. The rules are marked in the manifest (`unsafeFix()`), this prints them
10
- * for the shell, and the oxlint pass of `fix` passes them through. Check mode
11
- * keeps every one of them armed: the diagnostic is still owed an answer, from
12
- * a human ([Quality checks](../docs/06-quality-checks.md)).
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
- * Usage: node unsafe-fixers.js
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 { stdout } from 'node:process';
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
- for (const { rule } of unsafeFixers()) {
23
- stdout.write(`--allow\n${rule}\n`);
24
- }
45
+ const [consumerConfig, wrapper] = argv.slice(2);
46
+ writeFileSync(wrapper, wrapperOf(consumerConfig));
25
47
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jterrazz/typescript",
3
- "version": "10.1.0",
3
+ "version": "10.1.2",
4
4
  "license": "MIT",
5
5
  "author": "Jean-Baptiste Terrazzoni <contact@jterrazz.com>",
6
6
  "repository": {
package/src/index.d.ts CHANGED
@@ -9,6 +9,7 @@ declare const oxlintProfiles: {
9
9
  expo: OxlintConfig;
10
10
  hexagonal: OxlintConfig;
11
11
  library: OxlintConfig;
12
+ nested: (config: OxlintConfig) => OxlintConfig;
12
13
  next: OxlintConfig;
13
14
  node: OxlintConfig;
14
15
  react: OxlintConfig;
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
+ }
@@ -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
+ });