@jterrazz/typescript 9.3.0 → 10.0.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 (71) hide show
  1. package/README.md +20 -16
  2. package/bin/commands/check.sh +348 -117
  3. package/bin/typescript.sh +55 -0
  4. package/lib/check-architecture.js +89 -0
  5. package/lib/check-baseline.js +144 -0
  6. package/lib/check-docs.js +4 -3
  7. package/lib/check-drift.js +209 -0
  8. package/lib/check-gitignore.js +4 -4
  9. package/lib/check-markdown.js +279 -0
  10. package/lib/check-names.js +125 -0
  11. package/lib/check-publish.js +150 -0
  12. package/lib/check-secrets.js +115 -0
  13. package/lib/check-suppressions.js +355 -0
  14. package/lib/doctor.js +185 -0
  15. package/lib/merge-knip-config.js +57 -25
  16. package/lib/tracked-files.js +165 -0
  17. package/lib/workspace-members.js +5 -6
  18. package/package.json +19 -8
  19. package/presets/oxfmt/index.js +49 -5
  20. package/presets/oxlint/profiles/astro.js +10 -0
  21. package/presets/oxlint/profiles/bun.js +7 -0
  22. package/presets/oxlint/profiles/expo.js +7 -0
  23. package/presets/oxlint/profiles/library.js +16 -0
  24. package/presets/oxlint/profiles/next.js +7 -0
  25. package/presets/oxlint/profiles/node.js +7 -0
  26. package/presets/prettier/astro.json +6 -0
  27. package/presets/tsconfig/expo.json +16 -6
  28. package/presets/tsconfig/library.json +18 -0
  29. package/presets/tsconfig/next.json +12 -2
  30. package/presets/tsconfig/node.json +18 -4
  31. package/rules/README.md +23 -0
  32. package/rules/_contract.js +191 -0
  33. package/rules/_contract.test.ts +81 -0
  34. package/rules/a11y.js +51 -0
  35. package/rules/architecture/hexagonal.js +56 -0
  36. package/rules/architecture/layers.js +75 -0
  37. package/rules/astro.js +49 -0
  38. package/rules/catalog.js +134 -0
  39. package/rules/catalog.test.ts +84 -0
  40. package/rules/compile.js +125 -0
  41. package/rules/core/eslint.js +234 -0
  42. package/rules/core/import.js +107 -0
  43. package/rules/core/jsdoc.js +52 -0
  44. package/rules/core/node.js +36 -0
  45. package/rules/core/oxc.js +54 -0
  46. package/rules/core/promise.js +39 -0
  47. package/rules/core/typescript.js +204 -0
  48. package/rules/core/unicorn.js +200 -0
  49. package/rules/next.js +53 -0
  50. package/rules/profiles.js +89 -0
  51. package/rules/react-native.js +48 -0
  52. package/rules/react.js +148 -0
  53. package/rules/sorted.js +41 -0
  54. package/rules/vitest.js +153 -0
  55. package/src/docs.d.ts +4 -4
  56. package/src/docs.js +75 -57
  57. package/src/docs.test.ts +43 -32
  58. package/src/index.d.ts +13 -9
  59. package/src/index.js +15 -8
  60. package/src/oxfmt.d.ts +15 -2
  61. package/src/oxfmt.test.ts +10 -0
  62. package/src/oxlint.d.ts +57 -10
  63. package/src/oxlint.js +35 -50
  64. package/src/oxlint.test.ts +82 -28
  65. package/presets/oxlint/architectures/hexagonal-rules.js +0 -39
  66. package/presets/oxlint/architectures/hexagonal.js +0 -13
  67. package/presets/oxlint/base.js +0 -145
  68. package/presets/oxlint/expo.js +0 -36
  69. package/presets/oxlint/next.js +0 -43
  70. package/presets/oxlint/node.js +0 -14
  71. package/presets/oxlint/plugins/codestyle.js +0 -231
package/src/docs.test.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import { expect, test } from 'vitest';
2
2
 
3
- import { auditDocs } from './docs.js';
3
+ import { auditDocs, type DocsTree } from './docs.js';
4
4
 
5
5
  /**
6
6
  * The manual every repository carries, in its smallest compliant form: a map,
7
7
  * the three required spine chapters, and a brief that routes into it. Each test
8
8
  * breaks exactly one thing and reads the rule that names it.
9
9
  */
10
- function manual(overrides: Record<string, unknown> = {}) {
10
+ function manual(overrides: Partial<DocsTree> = {}): DocsTree {
11
11
  return {
12
12
  agents: '# Agent brief\n\nThe corpus is docs/README.md.\n',
13
13
  files: [
@@ -27,19 +27,19 @@ function manual(overrides: Record<string, unknown> = {}) {
27
27
  }
28
28
 
29
29
  /** 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);
30
+ function rules(tree: DocsTree) {
31
+ return auditDocs(tree).map((violation) => violation.rule);
32
32
  }
33
33
 
34
34
  /** 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;
35
+ function sentence(tree: DocsTree, rule: string) {
36
+ return auditDocs(tree).find((violation) => violation.rule === rule)?.message;
37
37
  }
38
38
 
39
39
  test('passes the smallest compliant manual', () => {
40
40
  // Given - a map, the three spine chapters, a brief that routes into them
41
41
  // Then - nothing to say
42
- expect(auditDocs(manual() as never)).toEqual([]);
42
+ expect(auditDocs(manual())).toStrictEqual([]);
43
43
  });
44
44
 
45
45
  test('names a repository that carries no manual at all, and says nothing else', () => {
@@ -47,7 +47,7 @@ test('names a repository that carries no manual at all, and says nothing else',
47
47
  const tree = manual({ agents: null, files: [] });
48
48
 
49
49
  // Then - one sentence, not a cascade of every rule the absence breaks
50
- expect(rules(tree)).toEqual(['docs-absent']);
50
+ expect(rules(tree)).toStrictEqual(['docs-absent']);
51
51
  expect(sentence(tree, 'docs-absent')).toBe(
52
52
  'docs/ is missing — every repository carries its own manual, starting at docs/README.md',
53
53
  );
@@ -61,7 +61,7 @@ test('names a docs/ with no map', () => {
61
61
  });
62
62
 
63
63
  // Then - the map is the one file an outside corpus points at
64
- expect(rules(tree)).toEqual(['docs-map-missing']);
64
+ expect(rules(tree)).toStrictEqual(['docs-map-missing']);
65
65
  expect(sentence(tree, 'docs-map-missing')).toBe(
66
66
  'docs/README.md is missing — the map is the one file an outside corpus points at',
67
67
  );
@@ -101,7 +101,7 @@ test('names a chapter the map forgot', () => {
101
101
  test('names a map link that leaves the manual', () => {
102
102
  // Given - a map routing to the repository's own README
103
103
  const tree = manual({
104
- links: { 'docs/README.md': [...manual().links['docs/README.md'], '../README.md'] },
104
+ links: { 'docs/README.md': [...(manual().links['docs/README.md'] ?? []), '../README.md'] },
105
105
  });
106
106
 
107
107
  // Then - the map routes to chapters, decisions/ and reference/, and nothing else
@@ -115,7 +115,7 @@ test('lets the map route to decisions/ and reference/', () => {
115
115
  const tree = manual({
116
116
  links: {
117
117
  'docs/README.md': [
118
- ...manual().links['docs/README.md'],
118
+ ...(manual().links['docs/README.md'] ?? []),
119
119
  'decisions/',
120
120
  'reference/index.md',
121
121
  ],
@@ -123,14 +123,16 @@ test('lets the map route to decisions/ and reference/', () => {
123
123
  });
124
124
 
125
125
  // Then - neither is foreign
126
- expect(rules(tree)).toEqual([]);
126
+ expect(rules(tree)).toStrictEqual([]);
127
127
  });
128
128
 
129
129
  test('names a chapter that is not NN-kebab.md', () => {
130
130
  // Given - a fourth chapter with one digit and an upper-case word
131
131
  const tree = manual({
132
132
  files: [...manual().files, 'docs/4-Lint_presets.md'],
133
- links: { 'docs/README.md': [...manual().links['docs/README.md'], '4-Lint_presets.md'] },
133
+ links: {
134
+ 'docs/README.md': [...(manual().links['docs/README.md'] ?? []), '4-Lint_presets.md'],
135
+ },
134
136
  });
135
137
 
136
138
  // Then - the mold is named, and the number it took is still judged
@@ -145,7 +147,7 @@ test('names chapter numbers that skip, and numbers claimed twice', () => {
145
147
  files: [...manual().files, 'docs/05-building.md', 'docs/05-presets.md'],
146
148
  links: {
147
149
  'docs/README.md': [
148
- ...manual().links['docs/README.md'],
150
+ ...(manual().links['docs/README.md'] ?? []),
149
151
  '05-building.md',
150
152
  '05-presets.md',
151
153
  ],
@@ -179,7 +181,7 @@ test('names each missing spine chapter', () => {
179
181
  });
180
182
 
181
183
  // Then - developing and testing are each asked for
182
- expect(rules(tree)).toEqual(['docs-spine-missing', 'docs-spine-missing']);
184
+ expect(rules(tree)).toStrictEqual(['docs-spine-missing', 'docs-spine-missing']);
183
185
  expect(sentence(tree, 'docs-spine-missing')).toBe(
184
186
  '02-developing.md is missing — the spine is architecture, developing, testing',
185
187
  );
@@ -208,41 +210,47 @@ test('asks for 04-operating.md from a published package', () => {
208
210
  test('never asks for 04-operating.md from a repository that ships nothing', () => {
209
211
  // Given - no image, no infrastructure, a private manifest
210
212
  // Then - the lint only ever requires; it never forbids a 04 either
211
- expect(rules(manual())).toEqual([]);
213
+ expect(rules(manual())).toStrictEqual([]);
212
214
  });
213
215
 
214
216
  test('lets 05 follow 03 directly when the repository ships nothing', () => {
215
217
  // Given - 01-03 then 05, no 04 chapter, nothing that would ask for one
216
218
  const tree = manual({
217
219
  files: [...manual().files, 'docs/05-building.md'],
218
- links: { 'docs/README.md': [...manual().links['docs/README.md'], '05-building.md'] },
220
+ links: {
221
+ 'docs/README.md': [...(manual().links['docs/README.md'] ?? []), '05-building.md'],
222
+ },
219
223
  });
220
224
 
221
225
  // Then - 04 is the one gap the numbering excuses
222
- expect(rules(tree)).toEqual([]);
226
+ expect(rules(tree)).toStrictEqual([]);
223
227
  });
224
228
 
225
229
  test('still asks for 04-operating.md from that same shape once it ships', () => {
226
230
  // Given - the same 01-03 + 05 shape, but an image this time
227
231
  const tree = manual({
228
232
  files: [...manual().files, 'docs/05-building.md'],
229
- links: { 'docs/README.md': [...manual().links['docs/README.md'], '05-building.md'] },
233
+ links: {
234
+ 'docs/README.md': [...(manual().links['docs/README.md'] ?? []), '05-building.md'],
235
+ },
230
236
  ships: { dockerfile: true, infrastructure: false, publishable: false },
231
237
  });
232
238
 
233
239
  // Then - the numbering stays clean; only the presence test speaks
234
- expect(rules(tree)).toEqual(['docs-operating-missing']);
240
+ expect(rules(tree)).toStrictEqual(['docs-operating-missing']);
235
241
  });
236
242
 
237
243
  test('refuses a gap at 05 even though 04 may be absent', () => {
238
244
  // Given - 01-03 then 06, skipping past the one number the spine excuses
239
245
  const tree = manual({
240
246
  files: [...manual().files, 'docs/06-quality-checks.md'],
241
- links: { 'docs/README.md': [...manual().links['docs/README.md'], '06-quality-checks.md'] },
247
+ links: {
248
+ 'docs/README.md': [...(manual().links['docs/README.md'] ?? []), '06-quality-checks.md'],
249
+ },
242
250
  });
243
251
 
244
252
  // Then - only 04 is excused; 05 still has to be there
245
- expect(rules(tree)).toEqual(['docs-chapter-numbering']);
253
+ expect(rules(tree)).toStrictEqual(['docs-chapter-numbering']);
246
254
  });
247
255
 
248
256
  test('names a chapter that is a journal, not a subject', () => {
@@ -250,7 +258,10 @@ test('names a chapter that is a journal, not a subject', () => {
250
258
  const tree = manual({
251
259
  files: [...manual().files, 'docs/04-design-exploration.md'],
252
260
  links: {
253
- 'docs/README.md': [...manual().links['docs/README.md'], '04-design-exploration.md'],
261
+ 'docs/README.md': [
262
+ ...(manual().links['docs/README.md'] ?? []),
263
+ '04-design-exploration.md',
264
+ ],
254
265
  },
255
266
  });
256
267
 
@@ -303,7 +314,7 @@ function withDecisions(overrides: Record<string, unknown> = {}) {
303
314
  test('passes a well-formed decisions folder', () => {
304
315
  // Given - one record on the mold, beside the mold itself
305
316
  // Then - nothing to say
306
- expect(rules(withDecisions())).toEqual([]);
317
+ expect(rules(withDecisions())).toStrictEqual([]);
307
318
  });
308
319
 
309
320
  test('names a decision file that is not NNN-kebab.md', () => {
@@ -349,7 +360,7 @@ test('names a decision whose heading claims another number', () => {
349
360
  });
350
361
 
351
362
  // Then - the same rule catches the disagreement
352
- expect(rules(tree)).toEqual(['docs-decision-heading']);
363
+ expect(rules(tree)).toStrictEqual(['docs-decision-heading']);
353
364
  });
354
365
 
355
366
  test('names a decision outside the status vocabulary', () => {
@@ -383,7 +394,7 @@ test('accepts a superseded status that links the record replacing it', () => {
383
394
  });
384
395
 
385
396
  // Then - nothing to say
386
- expect(rules(tree)).toEqual([]);
397
+ expect(rules(tree)).toStrictEqual([]);
387
398
  });
388
399
 
389
400
  test('refuses a superseded status with no link naming the successor', () => {
@@ -409,7 +420,7 @@ test('passes an empty decisions folder', () => {
409
420
  const tree = manual({ files: [...manual().files, 'docs/decisions/'] });
410
421
 
411
422
  // Then - a folder with nothing to number has no sequence to break
412
- expect(rules(tree)).toEqual(['docs-template-missing']);
423
+ expect(rules(tree)).toStrictEqual(['docs-template-missing']);
413
424
  });
414
425
 
415
426
  test('passes a decisions folder holding only the template', () => {
@@ -419,7 +430,7 @@ test('passes a decisions folder holding only the template', () => {
419
430
  });
420
431
 
421
432
  // Then - nothing to say
422
- expect(rules(tree)).toEqual([]);
433
+ expect(rules(tree)).toStrictEqual([]);
423
434
  });
424
435
 
425
436
  test('refuses a decision sequence that skips a number', () => {
@@ -497,7 +508,7 @@ test('lets a duplicated number pass the sequence check on its own', () => {
497
508
  });
498
509
 
499
510
  // Then - docs-decision-number still refuses it; the sequence itself is not the gap
500
- expect(rules(tree)).toEqual(['docs-decision-number']);
511
+ expect(rules(tree)).toStrictEqual(['docs-decision-number']);
501
512
  });
502
513
 
503
514
  test('names a number two records claim', () => {
@@ -565,7 +576,7 @@ test('passes a stamped reference page', () => {
565
576
  });
566
577
 
567
578
  // Then - nothing to say
568
- expect(rules(tree)).toEqual([]);
579
+ expect(rules(tree)).toStrictEqual([]);
569
580
  });
570
581
 
571
582
  test('names a repository whose brief does not route to the manual', () => {
@@ -583,7 +594,7 @@ test('names a brief that exists but routes elsewhere', () => {
583
594
  const tree = manual({ agents: '# Agent brief\n\nEverything you need is right here.\n' });
584
595
 
585
596
  // Then - the same rule
586
- expect(rules(tree)).toEqual(['docs-agents-route']);
597
+ expect(rules(tree)).toStrictEqual(['docs-agents-route']);
587
598
  });
588
599
 
589
600
  test('names a chapter reaching into another repository', () => {
@@ -616,5 +627,5 @@ test('lets a chapter cite a sibling repository by url, and reach its own root',
616
627
  });
617
628
 
618
629
  // Then - a citation is not a reach, and the repository's own tree is fair game
619
- expect(rules(tree)).toEqual([]);
630
+ expect(rules(tree)).toStrictEqual([]);
620
631
  });
package/src/index.d.ts CHANGED
@@ -1,18 +1,22 @@
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;
10
14
  };
11
15
 
12
16
  declare const defaultExport: {
13
17
  oxfmt: typeof oxfmtConfig;
14
- oxlint: typeof oxlintPresets;
18
+ oxlint: typeof oxlintProfiles;
15
19
  };
16
20
 
17
- export { oxfmtConfig as oxfmt, oxlintPresets as oxlint };
21
+ export { oxfmtConfig as oxfmt, oxlintProfiles as oxlint };
18
22
  export default defaultExport;
package/src/index.js CHANGED
@@ -1,16 +1,23 @@
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 hexagonalFragment from '../rules/architecture/hexagonal.js';
9
+ import { compile } from '../rules/compile.js';
6
10
 
7
11
  export const oxfmt = oxfmtConfig;
8
12
 
9
13
  export const oxlint = {
10
- expo: expoConfig,
11
- hexagonal: hexagonalConfig,
12
- next: nextConfig,
13
- node: nodeConfig,
14
+ astro: astroProfile,
15
+ bun: bunProfile,
16
+ expo: expoProfile,
17
+ hexagonal: compile(hexagonalFragment),
18
+ library: libraryProfile,
19
+ next: nextProfile,
20
+ node: nodeProfile,
14
21
  };
15
22
 
16
23
  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).toBeTruthy();
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,63 @@
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({ extends: [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
+
31
+ /** The six boundaries the `hexagonal` fragment enforces, as a declared map. */
32
+ declare const HEXAGONAL_MAP: readonly Layer[];
33
+
34
+ /**
35
+ * Deterministic merge of oxlint config objects, left to right:
36
+ * jsPlugins/plugins/ignorePatterns/extends concatenated and deduped,
37
+ * rules/env/globals/options/settings shallow-merged (last wins),
38
+ * overrides concatenated.
39
+ */
40
+ declare function compose(...configs: OxlintConfig[]): OxlintConfig;
7
41
 
8
42
  /**
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.
43
+ * A declared layer map as an additive fragment: one `no-restricted-imports`
44
+ * override per layer, each carrying that layer's complete pattern list.
12
45
  */
13
- declare function compose(...fragments: ConfigObject[]): ConfigObject;
46
+ declare function layers(definition: { id?: string; map: readonly Layer[] }): OxlintConfig;
14
47
 
15
48
  export { defineConfig } from 'oxlint';
16
- export { compose, expo, hexagonal, next, node };
49
+ export { type OxlintConfig, type OxlintOverride } from 'oxlint';
50
+ export {
51
+ astro,
52
+ bun,
53
+ compose,
54
+ expo,
55
+ hexagonal,
56
+ HEXAGONAL_MAP,
57
+ type Layer,
58
+ layers,
59
+ library,
60
+ next,
61
+ node,
62
+ type RuleEntry,
63
+ };
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 six
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({ extends: [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,23 @@ 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
+
37
+ export { HEXAGONAL_MAP } from '../rules/architecture/hexagonal.js';
38
+
39
+ /** The hexagonal layer map, ready to compose beside a profile. */
40
+ export const hexagonal = compile(hexagonalFragment);
41
+
42
+ /**
43
+ * A declared layer map as an additive oxlint fragment: one
44
+ * `no-restricted-imports` override per layer, each carrying that layer's
45
+ * complete pattern list (an oxlint override REPLACES, it never merges).
46
+ */
47
+ export function layers(definition) {
48
+ return compile(layersFragment(definition));
49
+ }