@jterrazz/typescript 9.2.1 → 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.
- package/README.md +20 -16
- package/bin/commands/check.sh +348 -117
- package/bin/typescript.sh +55 -0
- package/lib/check-architecture.js +89 -0
- package/lib/check-baseline.js +144 -0
- package/lib/check-docs.js +4 -3
- package/lib/check-drift.js +209 -0
- package/lib/check-gitignore.js +4 -4
- package/lib/check-markdown.js +279 -0
- package/lib/check-names.js +125 -0
- package/lib/check-publish.js +150 -0
- package/lib/check-secrets.js +115 -0
- package/lib/check-suppressions.js +355 -0
- package/lib/doctor.js +185 -0
- package/lib/merge-knip-config.js +57 -25
- package/lib/tracked-files.js +165 -0
- package/lib/workspace-members.js +5 -6
- package/package.json +19 -8
- package/presets/oxfmt/index.js +49 -5
- package/presets/oxlint/profiles/astro.js +10 -0
- package/presets/oxlint/profiles/bun.js +7 -0
- package/presets/oxlint/profiles/expo.js +7 -0
- package/presets/oxlint/profiles/library.js +16 -0
- package/presets/oxlint/profiles/next.js +7 -0
- package/presets/oxlint/profiles/node.js +7 -0
- package/presets/prettier/astro.json +6 -0
- package/presets/tsconfig/expo.json +16 -6
- package/presets/tsconfig/library.json +18 -0
- package/presets/tsconfig/next.json +12 -2
- package/presets/tsconfig/node.json +18 -4
- package/rules/README.md +23 -0
- package/rules/_contract.js +191 -0
- package/rules/_contract.test.ts +81 -0
- package/rules/a11y.js +51 -0
- package/rules/architecture/hexagonal.js +56 -0
- package/rules/architecture/layers.js +75 -0
- package/rules/astro.js +49 -0
- package/rules/catalog.js +134 -0
- package/rules/catalog.test.ts +84 -0
- package/rules/compile.js +125 -0
- package/rules/core/eslint.js +234 -0
- package/rules/core/import.js +107 -0
- package/rules/core/jsdoc.js +52 -0
- package/rules/core/node.js +36 -0
- package/rules/core/oxc.js +54 -0
- package/rules/core/promise.js +39 -0
- package/rules/core/typescript.js +204 -0
- package/rules/core/unicorn.js +200 -0
- package/rules/next.js +53 -0
- package/rules/profiles.js +89 -0
- package/rules/react-native.js +48 -0
- package/rules/react.js +148 -0
- package/rules/sorted.js +41 -0
- package/rules/vitest.js +153 -0
- package/src/docs.d.ts +4 -4
- package/src/docs.js +75 -47
- package/src/docs.test.ts +136 -29
- package/src/index.d.ts +13 -9
- package/src/index.js +15 -8
- package/src/oxfmt.d.ts +15 -2
- package/src/oxfmt.test.ts +10 -0
- package/src/oxlint.d.ts +57 -10
- package/src/oxlint.js +35 -50
- package/src/oxlint.test.ts +82 -28
- package/presets/oxlint/architectures/hexagonal-rules.js +0 -39
- package/presets/oxlint/architectures/hexagonal.js +0 -13
- package/presets/oxlint/base.js +0 -145
- package/presets/oxlint/expo.js +0 -36
- package/presets/oxlint/next.js +0 -43
- package/presets/oxlint/node.js +0 -14
- 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:
|
|
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:
|
|
31
|
-
return auditDocs(tree
|
|
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:
|
|
36
|
-
return auditDocs(tree
|
|
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()
|
|
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)).
|
|
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)).
|
|
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)).
|
|
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: {
|
|
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)).
|
|
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())).
|
|
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: {
|
|
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)).
|
|
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: {
|
|
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)).
|
|
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: {
|
|
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)).
|
|
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': [
|
|
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())).
|
|
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)).
|
|
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)).
|
|
397
|
+
expect(rules(tree)).toStrictEqual([]);
|
|
387
398
|
});
|
|
388
399
|
|
|
389
400
|
test('refuses a superseded status with no link naming the successor', () => {
|
|
@@ -404,6 +415,102 @@ test('refuses a superseded status with no link naming the successor', () => {
|
|
|
404
415
|
);
|
|
405
416
|
});
|
|
406
417
|
|
|
418
|
+
test('passes an empty decisions folder', () => {
|
|
419
|
+
// Given - the folder exists but holds no record yet
|
|
420
|
+
const tree = manual({ files: [...manual().files, 'docs/decisions/'] });
|
|
421
|
+
|
|
422
|
+
// Then - a folder with nothing to number has no sequence to break
|
|
423
|
+
expect(rules(tree)).toStrictEqual(['docs-template-missing']);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test('passes a decisions folder holding only the template', () => {
|
|
427
|
+
// Given - the mold, and not a single record beside it
|
|
428
|
+
const tree = manual({
|
|
429
|
+
files: [...manual().files, 'docs/decisions/', 'docs/decisions/_template.md'],
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Then - nothing to say
|
|
433
|
+
expect(rules(tree)).toStrictEqual([]);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test('refuses a decision sequence that skips a number', () => {
|
|
437
|
+
// Given - 001 and 003, with no record ever numbered 002
|
|
438
|
+
const tree = withDecisions({
|
|
439
|
+
files: [
|
|
440
|
+
...manual().files,
|
|
441
|
+
'docs/decisions/',
|
|
442
|
+
'docs/decisions/001-the-first-call.md',
|
|
443
|
+
'docs/decisions/003-the-third-call.md',
|
|
444
|
+
'docs/decisions/_template.md',
|
|
445
|
+
],
|
|
446
|
+
heads: {
|
|
447
|
+
'docs/decisions/001-the-first-call.md': [
|
|
448
|
+
'# ADR-001: The first call',
|
|
449
|
+
'',
|
|
450
|
+
'**Status:** Accepted',
|
|
451
|
+
],
|
|
452
|
+
'docs/decisions/003-the-third-call.md': [
|
|
453
|
+
'# ADR-003: The third call',
|
|
454
|
+
'',
|
|
455
|
+
'**Status:** Accepted',
|
|
456
|
+
],
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Then - the gap is named, from 001
|
|
461
|
+
expect(sentence(tree, 'docs-decision-sequence')).toBe(
|
|
462
|
+
'docs/decisions/ numbers run 001, 003: they run from 001 with no gap — a decision that moved folders takes the next number where it lands',
|
|
463
|
+
);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test('refuses a decision sequence that starts above 001', () => {
|
|
467
|
+
// Given - 006 and 008, with nothing numbered before them
|
|
468
|
+
const tree = withDecisions({
|
|
469
|
+
files: [
|
|
470
|
+
...manual().files,
|
|
471
|
+
'docs/decisions/',
|
|
472
|
+
'docs/decisions/006-the-sixth-call.md',
|
|
473
|
+
'docs/decisions/008-the-eighth-call.md',
|
|
474
|
+
'docs/decisions/_template.md',
|
|
475
|
+
],
|
|
476
|
+
heads: {
|
|
477
|
+
'docs/decisions/006-the-sixth-call.md': [
|
|
478
|
+
'# ADR-006: The sixth call',
|
|
479
|
+
'',
|
|
480
|
+
'**Status:** Accepted',
|
|
481
|
+
],
|
|
482
|
+
'docs/decisions/008-the-eighth-call.md': [
|
|
483
|
+
'# ADR-008: The eighth call',
|
|
484
|
+
'',
|
|
485
|
+
'**Status:** Accepted',
|
|
486
|
+
],
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// Then - the same rule, from an offset start
|
|
491
|
+
expect(sentence(tree, 'docs-decision-sequence')).toBe(
|
|
492
|
+
'docs/decisions/ numbers run 006, 008: they run from 001 with no gap — a decision that moved folders takes the next number where it lands',
|
|
493
|
+
);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test('lets a duplicated number pass the sequence check on its own', () => {
|
|
497
|
+
// Given - two records claiming 001, and none other — the set is just {001}
|
|
498
|
+
const tree = withDecisions({
|
|
499
|
+
files: [...withDecisions().files, 'docs/decisions/001-the-same-call.md'],
|
|
500
|
+
heads: {
|
|
501
|
+
...withDecisions().heads,
|
|
502
|
+
'docs/decisions/001-the-same-call.md': [
|
|
503
|
+
'# ADR-001: The same call',
|
|
504
|
+
'',
|
|
505
|
+
'**Status:** Proposed',
|
|
506
|
+
],
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// Then - docs-decision-number still refuses it; the sequence itself is not the gap
|
|
511
|
+
expect(rules(tree)).toStrictEqual(['docs-decision-number']);
|
|
512
|
+
});
|
|
513
|
+
|
|
407
514
|
test('names a number two records claim', () => {
|
|
408
515
|
// Given - two records numbered 001
|
|
409
516
|
const tree = withDecisions({
|
|
@@ -469,7 +576,7 @@ test('passes a stamped reference page', () => {
|
|
|
469
576
|
});
|
|
470
577
|
|
|
471
578
|
// Then - nothing to say
|
|
472
|
-
expect(rules(tree)).
|
|
579
|
+
expect(rules(tree)).toStrictEqual([]);
|
|
473
580
|
});
|
|
474
581
|
|
|
475
582
|
test('names a repository whose brief does not route to the manual', () => {
|
|
@@ -487,7 +594,7 @@ test('names a brief that exists but routes elsewhere', () => {
|
|
|
487
594
|
const tree = manual({ agents: '# Agent brief\n\nEverything you need is right here.\n' });
|
|
488
595
|
|
|
489
596
|
// Then - the same rule
|
|
490
|
-
expect(rules(tree)).
|
|
597
|
+
expect(rules(tree)).toStrictEqual(['docs-agents-route']);
|
|
491
598
|
});
|
|
492
599
|
|
|
493
600
|
test('names a chapter reaching into another repository', () => {
|
|
@@ -520,5 +627,5 @@ test('lets a chapter cite a sibling repository by url, and reach its own root',
|
|
|
520
627
|
});
|
|
521
628
|
|
|
522
629
|
// Then - a citation is not a reach, and the repository's own tree is fair game
|
|
523
|
-
expect(rules(tree)).
|
|
630
|
+
expect(rules(tree)).toStrictEqual([]);
|
|
524
631
|
});
|
package/src/index.d.ts
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
type
|
|
1
|
+
import { type OxfmtConfig } from './oxfmt.js';
|
|
2
|
+
import { type OxlintConfig } from './oxlint.js';
|
|
2
3
|
|
|
3
|
-
declare const oxfmtConfig:
|
|
4
|
+
declare const oxfmtConfig: OxfmtConfig;
|
|
4
5
|
|
|
5
|
-
declare const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
18
|
+
oxlint: typeof oxlintProfiles;
|
|
15
19
|
};
|
|
16
20
|
|
|
17
|
-
export { oxfmtConfig as oxfmt,
|
|
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
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4
|
-
declare const
|
|
5
|
-
declare const
|
|
6
|
-
declare const
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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
|
|
46
|
+
declare function layers(definition: { id?: string; map: readonly Layer[] }): OxlintConfig;
|
|
14
47
|
|
|
15
48
|
export { defineConfig } from 'oxlint';
|
|
16
|
-
export {
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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 {
|
|
8
|
-
* import { compose, node } from '@jterrazz/typescript/oxlint';
|
|
6
|
+
* import { defineConfig, node } from '@jterrazz/typescript/oxlint';
|
|
9
7
|
*
|
|
10
|
-
* export default
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
62
|
-
export { default as
|
|
63
|
-
export { default as
|
|
64
|
-
export { default as
|
|
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
|
+
}
|