@jterrazz/typescript 9.3.0 → 10.1.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.
Files changed (81) hide show
  1. package/README.md +20 -16
  2. package/bin/commands/check.sh +387 -146
  3. package/bin/find-tsc.sh +30 -0
  4. package/bin/typescript.sh +79 -1
  5. package/lib/check-architecture.js +89 -0
  6. package/lib/check-baseline.js +144 -0
  7. package/lib/check-docs.js +4 -3
  8. package/lib/check-drift.js +209 -0
  9. package/lib/check-gitignore.js +4 -4
  10. package/lib/check-markdown.js +279 -0
  11. package/lib/check-names.js +125 -0
  12. package/lib/check-publish.js +150 -0
  13. package/lib/check-secrets.js +115 -0
  14. package/lib/check-suppressions.js +355 -0
  15. package/lib/doctor.js +185 -0
  16. package/lib/entry-points.js +91 -0
  17. package/lib/merge-knip-config.js +57 -25
  18. package/lib/tracked-files.js +165 -0
  19. package/lib/unsafe-fixers.js +25 -0
  20. package/lib/workspace-members.js +5 -6
  21. package/package.json +36 -13
  22. package/presets/oxfmt/index.js +49 -5
  23. package/presets/oxlint/profiles/astro.js +10 -0
  24. package/presets/oxlint/profiles/bun.js +7 -0
  25. package/presets/oxlint/profiles/expo.js +7 -0
  26. package/presets/oxlint/profiles/library.js +16 -0
  27. package/presets/oxlint/profiles/next.js +7 -0
  28. package/presets/oxlint/profiles/node.js +7 -0
  29. package/presets/oxlint/profiles/react.js +7 -0
  30. package/presets/prettier/astro.json +6 -0
  31. package/presets/tsconfig/astro.json +25 -0
  32. package/presets/tsconfig/expo.json +16 -6
  33. package/presets/tsconfig/library.json +17 -0
  34. package/presets/tsconfig/next.json +12 -2
  35. package/presets/tsconfig/node.json +18 -4
  36. package/presets/tsconfig/react.json +33 -0
  37. package/presets/tsdown/build.d.ts +13 -0
  38. package/presets/tsdown/bundle.d.ts +13 -0
  39. package/presets/tsdown/bundle.js +10 -1
  40. package/rules/README.md +23 -0
  41. package/rules/_contract.js +207 -0
  42. package/rules/_contract.test.ts +81 -0
  43. package/rules/a11y.js +51 -0
  44. package/rules/architecture/hexagonal.js +56 -0
  45. package/rules/architecture/layers.js +75 -0
  46. package/rules/astro.js +56 -0
  47. package/rules/bundler.js +19 -0
  48. package/rules/catalog.js +166 -0
  49. package/rules/catalog.test.ts +98 -0
  50. package/rules/compile.js +125 -0
  51. package/rules/core/eslint.js +234 -0
  52. package/rules/core/import.js +117 -0
  53. package/rules/core/jsdoc.js +52 -0
  54. package/rules/core/node.js +36 -0
  55. package/rules/core/oxc.js +54 -0
  56. package/rules/core/promise.js +39 -0
  57. package/rules/core/typescript.js +223 -0
  58. package/rules/core/unicorn.js +210 -0
  59. package/rules/next.js +53 -0
  60. package/rules/profiles.js +95 -0
  61. package/rules/react-native.js +48 -0
  62. package/rules/react.js +155 -0
  63. package/rules/sorted.js +41 -0
  64. package/rules/vitest.js +178 -0
  65. package/src/docs.d.ts +4 -4
  66. package/src/docs.js +75 -57
  67. package/src/docs.test.ts +43 -31
  68. package/src/index.d.ts +14 -9
  69. package/src/index.js +17 -8
  70. package/src/oxfmt.d.ts +15 -2
  71. package/src/oxfmt.test.ts +10 -0
  72. package/src/oxlint.d.ts +59 -10
  73. package/src/oxlint.js +36 -50
  74. package/src/oxlint.test.ts +82 -28
  75. package/presets/oxlint/architectures/hexagonal-rules.js +0 -39
  76. package/presets/oxlint/architectures/hexagonal.js +0 -13
  77. package/presets/oxlint/base.js +0 -145
  78. package/presets/oxlint/expo.js +0 -36
  79. package/presets/oxlint/next.js +0 -43
  80. package/presets/oxlint/node.js +0 -14
  81. package/presets/oxlint/plugins/codestyle.js +0 -231
package/src/docs.test.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import { expect, test } from 'vitest';
2
2
 
3
3
  import { auditDocs } from './docs.js';
4
+ import type { DocsTree } from './docs.js';
4
5
 
5
6
  /**
6
7
  * The manual every repository carries, in its smallest compliant form: a map,
7
8
  * the three required spine chapters, and a brief that routes into it. Each test
8
9
  * breaks exactly one thing and reads the rule that names it.
9
10
  */
10
- function manual(overrides: Record<string, unknown> = {}) {
11
+ function manual(overrides: Partial<DocsTree> = {}): DocsTree {
11
12
  return {
12
13
  agents: '# Agent brief\n\nThe corpus is docs/README.md.\n',
13
14
  files: [
@@ -27,19 +28,19 @@ function manual(overrides: Record<string, unknown> = {}) {
27
28
  }
28
29
 
29
30
  /** The rule ids a tree breaks, in the order the engine reports them. */
30
- function rules(tree: ReturnType<typeof manual>) {
31
- return auditDocs(tree as never).map((violation) => violation.rule);
31
+ function rules(tree: DocsTree) {
32
+ return auditDocs(tree).map((violation) => violation.rule);
32
33
  }
33
34
 
34
35
  /** The sentence one rule printed about a tree. */
35
- function sentence(tree: ReturnType<typeof manual>, rule: string) {
36
- return auditDocs(tree as never).find((violation) => violation.rule === rule)?.message;
36
+ function sentence(tree: DocsTree, rule: string) {
37
+ return auditDocs(tree).find((violation) => violation.rule === rule)?.message;
37
38
  }
38
39
 
39
40
  test('passes the smallest compliant manual', () => {
40
41
  // Given - a map, the three spine chapters, a brief that routes into them
41
42
  // Then - nothing to say
42
- expect(auditDocs(manual() as never)).toEqual([]);
43
+ expect(auditDocs(manual())).toStrictEqual([]);
43
44
  });
44
45
 
45
46
  test('names a repository that carries no manual at all, and says nothing else', () => {
@@ -47,7 +48,7 @@ test('names a repository that carries no manual at all, and says nothing else',
47
48
  const tree = manual({ agents: null, files: [] });
48
49
 
49
50
  // Then - one sentence, not a cascade of every rule the absence breaks
50
- expect(rules(tree)).toEqual(['docs-absent']);
51
+ expect(rules(tree)).toStrictEqual(['docs-absent']);
51
52
  expect(sentence(tree, 'docs-absent')).toBe(
52
53
  'docs/ is missing — every repository carries its own manual, starting at docs/README.md',
53
54
  );
@@ -61,7 +62,7 @@ test('names a docs/ with no map', () => {
61
62
  });
62
63
 
63
64
  // Then - the map is the one file an outside corpus points at
64
- expect(rules(tree)).toEqual(['docs-map-missing']);
65
+ expect(rules(tree)).toStrictEqual(['docs-map-missing']);
65
66
  expect(sentence(tree, 'docs-map-missing')).toBe(
66
67
  'docs/README.md is missing — the map is the one file an outside corpus points at',
67
68
  );
@@ -101,7 +102,7 @@ test('names a chapter the map forgot', () => {
101
102
  test('names a map link that leaves the manual', () => {
102
103
  // Given - a map routing to the repository's own README
103
104
  const tree = manual({
104
- links: { 'docs/README.md': [...manual().links['docs/README.md'], '../README.md'] },
105
+ links: { 'docs/README.md': [...(manual().links['docs/README.md'] ?? []), '../README.md'] },
105
106
  });
106
107
 
107
108
  // Then - the map routes to chapters, decisions/ and reference/, and nothing else
@@ -115,7 +116,7 @@ test('lets the map route to decisions/ and reference/', () => {
115
116
  const tree = manual({
116
117
  links: {
117
118
  'docs/README.md': [
118
- ...manual().links['docs/README.md'],
119
+ ...(manual().links['docs/README.md'] ?? []),
119
120
  'decisions/',
120
121
  'reference/index.md',
121
122
  ],
@@ -123,14 +124,16 @@ test('lets the map route to decisions/ and reference/', () => {
123
124
  });
124
125
 
125
126
  // Then - neither is foreign
126
- expect(rules(tree)).toEqual([]);
127
+ expect(rules(tree)).toStrictEqual([]);
127
128
  });
128
129
 
129
130
  test('names a chapter that is not NN-kebab.md', () => {
130
131
  // Given - a fourth chapter with one digit and an upper-case word
131
132
  const tree = manual({
132
133
  files: [...manual().files, 'docs/4-Lint_presets.md'],
133
- links: { 'docs/README.md': [...manual().links['docs/README.md'], '4-Lint_presets.md'] },
134
+ links: {
135
+ 'docs/README.md': [...(manual().links['docs/README.md'] ?? []), '4-Lint_presets.md'],
136
+ },
134
137
  });
135
138
 
136
139
  // Then - the mold is named, and the number it took is still judged
@@ -145,7 +148,7 @@ test('names chapter numbers that skip, and numbers claimed twice', () => {
145
148
  files: [...manual().files, 'docs/05-building.md', 'docs/05-presets.md'],
146
149
  links: {
147
150
  'docs/README.md': [
148
- ...manual().links['docs/README.md'],
151
+ ...(manual().links['docs/README.md'] ?? []),
149
152
  '05-building.md',
150
153
  '05-presets.md',
151
154
  ],
@@ -179,7 +182,7 @@ test('names each missing spine chapter', () => {
179
182
  });
180
183
 
181
184
  // Then - developing and testing are each asked for
182
- expect(rules(tree)).toEqual(['docs-spine-missing', 'docs-spine-missing']);
185
+ expect(rules(tree)).toStrictEqual(['docs-spine-missing', 'docs-spine-missing']);
183
186
  expect(sentence(tree, 'docs-spine-missing')).toBe(
184
187
  '02-developing.md is missing — the spine is architecture, developing, testing',
185
188
  );
@@ -208,41 +211,47 @@ test('asks for 04-operating.md from a published package', () => {
208
211
  test('never asks for 04-operating.md from a repository that ships nothing', () => {
209
212
  // Given - no image, no infrastructure, a private manifest
210
213
  // Then - the lint only ever requires; it never forbids a 04 either
211
- expect(rules(manual())).toEqual([]);
214
+ expect(rules(manual())).toStrictEqual([]);
212
215
  });
213
216
 
214
217
  test('lets 05 follow 03 directly when the repository ships nothing', () => {
215
218
  // Given - 01-03 then 05, no 04 chapter, nothing that would ask for one
216
219
  const tree = manual({
217
220
  files: [...manual().files, 'docs/05-building.md'],
218
- links: { 'docs/README.md': [...manual().links['docs/README.md'], '05-building.md'] },
221
+ links: {
222
+ 'docs/README.md': [...(manual().links['docs/README.md'] ?? []), '05-building.md'],
223
+ },
219
224
  });
220
225
 
221
226
  // Then - 04 is the one gap the numbering excuses
222
- expect(rules(tree)).toEqual([]);
227
+ expect(rules(tree)).toStrictEqual([]);
223
228
  });
224
229
 
225
230
  test('still asks for 04-operating.md from that same shape once it ships', () => {
226
231
  // Given - the same 01-03 + 05 shape, but an image this time
227
232
  const tree = manual({
228
233
  files: [...manual().files, 'docs/05-building.md'],
229
- links: { 'docs/README.md': [...manual().links['docs/README.md'], '05-building.md'] },
234
+ links: {
235
+ 'docs/README.md': [...(manual().links['docs/README.md'] ?? []), '05-building.md'],
236
+ },
230
237
  ships: { dockerfile: true, infrastructure: false, publishable: false },
231
238
  });
232
239
 
233
240
  // Then - the numbering stays clean; only the presence test speaks
234
- expect(rules(tree)).toEqual(['docs-operating-missing']);
241
+ expect(rules(tree)).toStrictEqual(['docs-operating-missing']);
235
242
  });
236
243
 
237
244
  test('refuses a gap at 05 even though 04 may be absent', () => {
238
245
  // Given - 01-03 then 06, skipping past the one number the spine excuses
239
246
  const tree = manual({
240
247
  files: [...manual().files, 'docs/06-quality-checks.md'],
241
- links: { 'docs/README.md': [...manual().links['docs/README.md'], '06-quality-checks.md'] },
248
+ links: {
249
+ 'docs/README.md': [...(manual().links['docs/README.md'] ?? []), '06-quality-checks.md'],
250
+ },
242
251
  });
243
252
 
244
253
  // Then - only 04 is excused; 05 still has to be there
245
- expect(rules(tree)).toEqual(['docs-chapter-numbering']);
254
+ expect(rules(tree)).toStrictEqual(['docs-chapter-numbering']);
246
255
  });
247
256
 
248
257
  test('names a chapter that is a journal, not a subject', () => {
@@ -250,7 +259,10 @@ test('names a chapter that is a journal, not a subject', () => {
250
259
  const tree = manual({
251
260
  files: [...manual().files, 'docs/04-design-exploration.md'],
252
261
  links: {
253
- 'docs/README.md': [...manual().links['docs/README.md'], '04-design-exploration.md'],
262
+ 'docs/README.md': [
263
+ ...(manual().links['docs/README.md'] ?? []),
264
+ '04-design-exploration.md',
265
+ ],
254
266
  },
255
267
  });
256
268
 
@@ -303,7 +315,7 @@ function withDecisions(overrides: Record<string, unknown> = {}) {
303
315
  test('passes a well-formed decisions folder', () => {
304
316
  // Given - one record on the mold, beside the mold itself
305
317
  // Then - nothing to say
306
- expect(rules(withDecisions())).toEqual([]);
318
+ expect(rules(withDecisions())).toStrictEqual([]);
307
319
  });
308
320
 
309
321
  test('names a decision file that is not NNN-kebab.md', () => {
@@ -349,7 +361,7 @@ test('names a decision whose heading claims another number', () => {
349
361
  });
350
362
 
351
363
  // Then - the same rule catches the disagreement
352
- expect(rules(tree)).toEqual(['docs-decision-heading']);
364
+ expect(rules(tree)).toStrictEqual(['docs-decision-heading']);
353
365
  });
354
366
 
355
367
  test('names a decision outside the status vocabulary', () => {
@@ -383,7 +395,7 @@ test('accepts a superseded status that links the record replacing it', () => {
383
395
  });
384
396
 
385
397
  // Then - nothing to say
386
- expect(rules(tree)).toEqual([]);
398
+ expect(rules(tree)).toStrictEqual([]);
387
399
  });
388
400
 
389
401
  test('refuses a superseded status with no link naming the successor', () => {
@@ -409,7 +421,7 @@ test('passes an empty decisions folder', () => {
409
421
  const tree = manual({ files: [...manual().files, 'docs/decisions/'] });
410
422
 
411
423
  // Then - a folder with nothing to number has no sequence to break
412
- expect(rules(tree)).toEqual(['docs-template-missing']);
424
+ expect(rules(tree)).toStrictEqual(['docs-template-missing']);
413
425
  });
414
426
 
415
427
  test('passes a decisions folder holding only the template', () => {
@@ -419,7 +431,7 @@ test('passes a decisions folder holding only the template', () => {
419
431
  });
420
432
 
421
433
  // Then - nothing to say
422
- expect(rules(tree)).toEqual([]);
434
+ expect(rules(tree)).toStrictEqual([]);
423
435
  });
424
436
 
425
437
  test('refuses a decision sequence that skips a number', () => {
@@ -497,7 +509,7 @@ test('lets a duplicated number pass the sequence check on its own', () => {
497
509
  });
498
510
 
499
511
  // Then - docs-decision-number still refuses it; the sequence itself is not the gap
500
- expect(rules(tree)).toEqual(['docs-decision-number']);
512
+ expect(rules(tree)).toStrictEqual(['docs-decision-number']);
501
513
  });
502
514
 
503
515
  test('names a number two records claim', () => {
@@ -565,7 +577,7 @@ test('passes a stamped reference page', () => {
565
577
  });
566
578
 
567
579
  // Then - nothing to say
568
- expect(rules(tree)).toEqual([]);
580
+ expect(rules(tree)).toStrictEqual([]);
569
581
  });
570
582
 
571
583
  test('names a repository whose brief does not route to the manual', () => {
@@ -583,7 +595,7 @@ test('names a brief that exists but routes elsewhere', () => {
583
595
  const tree = manual({ agents: '# Agent brief\n\nEverything you need is right here.\n' });
584
596
 
585
597
  // Then - the same rule
586
- expect(rules(tree)).toEqual(['docs-agents-route']);
598
+ expect(rules(tree)).toStrictEqual(['docs-agents-route']);
587
599
  });
588
600
 
589
601
  test('names a chapter reaching into another repository', () => {
@@ -616,5 +628,5 @@ test('lets a chapter cite a sibling repository by url, and reach its own root',
616
628
  });
617
629
 
618
630
  // Then - a citation is not a reach, and the repository's own tree is fair game
619
- expect(rules(tree)).toEqual([]);
631
+ expect(rules(tree)).toStrictEqual([]);
620
632
  });
package/src/index.d.ts CHANGED
@@ -1,18 +1,23 @@
1
- type ConfigObject = Record<string, unknown>;
1
+ import type { OxfmtConfig } from './oxfmt.js';
2
+ import type { OxlintConfig } from './oxlint.js';
2
3
 
3
- declare const oxfmtConfig: ConfigObject;
4
+ declare const oxfmtConfig: OxfmtConfig;
4
5
 
5
- declare const oxlintPresets: {
6
- expo: ConfigObject;
7
- hexagonal: ConfigObject;
8
- next: ConfigObject;
9
- node: ConfigObject;
6
+ declare const oxlintProfiles: {
7
+ astro: OxlintConfig;
8
+ bun: OxlintConfig;
9
+ expo: OxlintConfig;
10
+ hexagonal: OxlintConfig;
11
+ library: OxlintConfig;
12
+ next: OxlintConfig;
13
+ node: OxlintConfig;
14
+ react: OxlintConfig;
10
15
  };
11
16
 
12
17
  declare const defaultExport: {
13
18
  oxfmt: typeof oxfmtConfig;
14
- oxlint: typeof oxlintPresets;
19
+ oxlint: typeof oxlintProfiles;
15
20
  };
16
21
 
17
- export { oxfmtConfig as oxfmt, oxlintPresets as oxlint };
22
+ export { oxfmtConfig as oxfmt, oxlintProfiles as oxlint };
18
23
  export default defaultExport;
package/src/index.js CHANGED
@@ -1,16 +1,25 @@
1
1
  import oxfmtConfig from '../presets/oxfmt/index.js';
2
- import hexagonalConfig from '../presets/oxlint/architectures/hexagonal.js';
3
- import expoConfig from '../presets/oxlint/expo.js';
4
- import nextConfig from '../presets/oxlint/next.js';
5
- import nodeConfig from '../presets/oxlint/node.js';
2
+ import astroProfile from '../presets/oxlint/profiles/astro.js';
3
+ import bunProfile from '../presets/oxlint/profiles/bun.js';
4
+ import expoProfile from '../presets/oxlint/profiles/expo.js';
5
+ import libraryProfile from '../presets/oxlint/profiles/library.js';
6
+ import nextProfile from '../presets/oxlint/profiles/next.js';
7
+ import nodeProfile from '../presets/oxlint/profiles/node.js';
8
+ import reactProfile from '../presets/oxlint/profiles/react.js';
9
+ import hexagonalFragment from '../rules/architecture/hexagonal.js';
10
+ import { compile } from '../rules/compile.js';
6
11
 
7
12
  export const oxfmt = oxfmtConfig;
8
13
 
9
14
  export const oxlint = {
10
- expo: expoConfig,
11
- hexagonal: hexagonalConfig,
12
- next: nextConfig,
13
- node: nodeConfig,
15
+ astro: astroProfile,
16
+ bun: bunProfile,
17
+ expo: expoProfile,
18
+ hexagonal: compile(hexagonalFragment),
19
+ library: libraryProfile,
20
+ next: nextProfile,
21
+ node: nodeProfile,
22
+ react: reactProfile,
14
23
  };
15
24
 
16
25
  export default { oxfmt, oxlint };
package/src/oxfmt.d.ts CHANGED
@@ -1,6 +1,19 @@
1
- type ConfigObject = Record<string, unknown>;
1
+ /*
2
+ * The shape of a formatting config is oxfmt's own fact, so this entry does not
3
+ * restate it: `OxfmtConfig` and the two sorter shapes are re-exported from the
4
+ * tool, under the names a consumer already reads here. A hand copy drifts —
5
+ * this one had grown an `endOfLine: 'auto'` oxfmt does not accept, and a
6
+ * consumer's `defineConfig(base)` stopped type-checking because of it.
7
+ */
2
8
 
3
- declare const base: ConfigObject;
9
+ import type { OxfmtConfig } from 'oxfmt';
10
+
11
+ declare const base: OxfmtConfig;
4
12
 
5
13
  export { defineConfig } from 'oxfmt';
14
+ export {
15
+ type OxfmtConfig,
16
+ type SortImportsUserConfig as SortImports,
17
+ type SortTailwindcssUserConfig as SortTailwindcss,
18
+ } from 'oxfmt';
6
19
  export { base };
package/src/oxfmt.test.ts CHANGED
@@ -8,6 +8,16 @@ test('exports the shared formatting preset', () => {
8
8
  expect(base).toMatchObject({ printWidth: 100, singleQuote: true, tabWidth: 4 });
9
9
  });
10
10
 
11
+ test('gives the formatter the three sorters, so no lint rule owns an order', () => {
12
+ // Given - the shared preset
13
+ // Then - import order, package.json key order and Tailwind class order are oxfmt's
14
+ expect(base.sortImports).toMatchObject({ ignoreCase: true, order: 'asc' });
15
+ expect(base.sortPackageJson).toBe(true);
16
+ expect(base.sortTailwindcss).toStrictEqual({
17
+ functions: ['clsx', 'cn', 'cva', 'tv', 'twMerge', 'twJoin', 'tw'],
18
+ });
19
+ });
20
+
11
21
  test("re-exports oxfmt's own defineConfig", () => {
12
22
  // Given - a config a consumer would write, with oxfmt declared nowhere in its project
13
23
  const config = { printWidth: 80 };
package/src/oxlint.d.ts CHANGED
@@ -1,16 +1,65 @@
1
- type ConfigObject = Record<string, unknown>;
1
+ /*
2
+ * The shape of a lint config is oxlint's own fact, so this entry does not
3
+ * restate it: `OxlintConfig` and `OxlintOverride` are re-exported from the
4
+ * tool, under the names a consumer already reads here. A hand copy drifts —
5
+ * this one had `plugins?: string[]` where oxlint takes a closed union, and a
6
+ * consumer's `defineConfig(node)` stopped type-checking.
7
+ */
8
+
9
+ import type { OxlintConfig } from 'oxlint';
10
+
11
+ /** A rule entry as oxlint reads it: a level, or a level and its options. */
12
+ type RuleEntry = NonNullable<OxlintConfig['rules']>[string];
13
+
14
+ /** One layer of a map: the files that belong to it, and what they may not import. */
15
+ type Layer = {
16
+ allow?: string[];
17
+ deny: string[];
18
+ files: string[];
19
+ message: string;
20
+ name: string;
21
+ };
2
22
 
3
- declare const expo: ConfigObject;
4
- declare const hexagonal: ConfigObject;
5
- declare const next: ConfigObject;
6
- declare const node: ConfigObject;
23
+ declare const astro: OxlintConfig;
24
+ declare const bun: OxlintConfig;
25
+ declare const expo: OxlintConfig;
26
+ declare const hexagonal: OxlintConfig;
27
+ declare const library: OxlintConfig;
28
+ declare const next: OxlintConfig;
29
+ declare const node: OxlintConfig;
30
+ declare const react: OxlintConfig;
31
+
32
+ /** The six boundaries the `hexagonal` fragment enforces, as a declared map. */
33
+ declare const HEXAGONAL_MAP: readonly Layer[];
34
+
35
+ /**
36
+ * Deterministic merge of oxlint config objects, left to right:
37
+ * jsPlugins/plugins/ignorePatterns/extends concatenated and deduped,
38
+ * rules/env/globals/options/settings shallow-merged (last wins),
39
+ * overrides concatenated.
40
+ */
41
+ declare function compose(...configs: OxlintConfig[]): OxlintConfig;
7
42
 
8
43
  /**
9
- * Deterministic merge of oxlint config fragments, left to right:
10
- * jsPlugins/plugins/ignorePatterns/extends concatenated + deduped,
11
- * rules/categories shallow-merged (last wins), overrides concatenated.
44
+ * A declared layer map as an additive fragment: one `no-restricted-imports`
45
+ * override per layer, each carrying that layer's complete pattern list.
12
46
  */
13
- declare function compose(...fragments: ConfigObject[]): ConfigObject;
47
+ declare function layers(definition: { id?: string; map: readonly Layer[] }): OxlintConfig;
14
48
 
15
49
  export { defineConfig } from 'oxlint';
16
- export { compose, expo, hexagonal, next, node };
50
+ export { type OxlintConfig, type OxlintOverride } from 'oxlint';
51
+ export {
52
+ astro,
53
+ bun,
54
+ compose,
55
+ expo,
56
+ hexagonal,
57
+ HEXAGONAL_MAP,
58
+ type Layer,
59
+ layers,
60
+ library,
61
+ next,
62
+ node,
63
+ react,
64
+ type RuleEntry,
65
+ };
package/src/oxlint.js CHANGED
@@ -1,55 +1,24 @@
1
1
  /*
2
- * The tool-facing oxlint entry (`@jterrazz/typescript/oxlint`): the named
3
- * presets, the `compose()` helper, and oxlint's own `defineConfig`. Wiring is
4
- * EXPLICIT a consumer composes exactly the fragments it wants, nothing is
5
- * auto-detected:
2
+ * The tool-facing oxlint entry (`@jterrazz/typescript/oxlint`): the seven
3
+ * profiles, the `compose()` merger, the layer-map builder, and oxlint's own
4
+ * `defineConfig`. A consumer names a PROFILE, not a set of fragments:
6
5
  *
7
- * import { testing } from '@jterrazz/test/oxlint';
8
- * import { compose, node } from '@jterrazz/typescript/oxlint';
6
+ * import { defineConfig, node } from '@jterrazz/typescript/oxlint';
9
7
  *
10
- * export default compose(node, testing);
8
+ * export default defineConfig(node);
9
+ *
10
+ * What each profile carries, and why every rule of every loaded plugin is
11
+ * decided by name, is [Lint presets](../docs/07-lint-presets.md).
12
+ *
13
+ * `rules/` holds the manifest — decisions, with a reason behind every `off`.
14
+ * What crosses this file is always a plain oxlint config, compiled from it.
11
15
  */
12
16
 
13
- /** Config keys concatenated across fragments, duplicates dropped (===). */
14
- const CONCAT_DEDUPE = new Set(['extends', 'ignorePatterns', 'jsPlugins', 'plugins']);
15
- /** Config keys concatenated verbatim (order matters, no dedupe). */
16
- const CONCAT = new Set(['overrides']);
17
- /** Config keys shallow-merged as objects — the LAST fragment wins per key. */
18
- const SHALLOW_MERGE = new Set(['categories', 'env', 'globals', 'rules', 'settings']);
17
+ import hexagonalFragment from '../rules/architecture/hexagonal.js';
18
+ import { layers as layersFragment } from '../rules/architecture/layers.js';
19
+ import { compile } from '../rules/compile.js';
19
20
 
20
- /**
21
- * Deterministic merge of oxlint config fragments, left to right:
22
- * `jsPlugins` / `plugins` / `ignorePatterns` / `extends` are concatenated and
23
- * deduped, `rules` / `categories` (and env/globals/settings) are shallow-merged
24
- * with last-wins per key, `overrides` are concatenated, and any other key is
25
- * taken from the last fragment that sets it.
26
- */
27
- export function compose(...fragments) {
28
- const merged = {};
29
- for (const fragment of fragments) {
30
- if (!fragment || typeof fragment !== 'object') {
31
- continue;
32
- }
33
- for (const [key, value] of Object.entries(fragment)) {
34
- if (value === undefined) {
35
- continue;
36
- }
37
- if (CONCAT_DEDUPE.has(key)) {
38
- const previous = Array.isArray(merged[key]) ? merged[key] : [];
39
- const combined = [...previous, ...(Array.isArray(value) ? value : [value])];
40
- merged[key] = combined.filter((entry, index) => combined.indexOf(entry) === index);
41
- } else if (CONCAT.has(key)) {
42
- const previous = Array.isArray(merged[key]) ? merged[key] : [];
43
- merged[key] = [...previous, ...(Array.isArray(value) ? value : [value])];
44
- } else if (SHALLOW_MERGE.has(key)) {
45
- merged[key] = { ...merged[key], ...value };
46
- } else {
47
- merged[key] = value;
48
- }
49
- }
50
- }
51
- return merged;
52
- }
21
+ export { merge as compose } from '../rules/compile.js';
53
22
 
54
23
  /* Oxlint's own `defineConfig`, re-exported from here: a bare
55
24
  * `import { defineConfig } from 'oxlint'` in a consumer's config resolves only
@@ -58,7 +27,24 @@ export function compose(...fragments) {
58
27
  * dependency — the one-devDependency shape holds on every package manager. */
59
28
  export { defineConfig } from 'oxlint';
60
29
 
61
- export { default as hexagonal } from '../presets/oxlint/architectures/hexagonal.js';
62
- export { default as expo } from '../presets/oxlint/expo.js';
63
- export { default as next } from '../presets/oxlint/next.js';
64
- export { default as node } from '../presets/oxlint/node.js';
30
+ export { default as astro } from '../presets/oxlint/profiles/astro.js';
31
+ export { default as bun } from '../presets/oxlint/profiles/bun.js';
32
+ export { default as expo } from '../presets/oxlint/profiles/expo.js';
33
+ export { default as library } from '../presets/oxlint/profiles/library.js';
34
+ export { default as next } from '../presets/oxlint/profiles/next.js';
35
+ export { default as node } from '../presets/oxlint/profiles/node.js';
36
+ export { default as react } from '../presets/oxlint/profiles/react.js';
37
+
38
+ export { HEXAGONAL_MAP } from '../rules/architecture/hexagonal.js';
39
+
40
+ /** The hexagonal layer map, ready to compose beside a profile. */
41
+ export const hexagonal = compile(hexagonalFragment);
42
+
43
+ /**
44
+ * A declared layer map as an additive oxlint fragment: one
45
+ * `no-restricted-imports` override per layer, each carrying that layer's
46
+ * complete pattern list (an oxlint override REPLACES, it never merges).
47
+ */
48
+ export function layers(definition) {
49
+ return compile(layersFragment(definition));
50
+ }