@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.
@@ -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 &
@@ -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
- /** 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
+ // 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
- if (!existsSync(resolve(root, dirname(path), clean))) {
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 (!existsSync(join(root, target)) && !existsSync(resolve(root, dirname(path), target))) {
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
  }
@@ -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.1",
3
+ "version": "10.1.3",
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
+ });