@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.
- package/README.md +20 -16
- package/bin/commands/check.sh +387 -146
- package/bin/find-tsc.sh +30 -0
- package/bin/typescript.sh +79 -1
- 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/entry-points.js +91 -0
- package/lib/merge-knip-config.js +57 -25
- package/lib/tracked-files.js +165 -0
- package/lib/unsafe-fixers.js +25 -0
- package/lib/workspace-members.js +5 -6
- package/package.json +36 -13
- 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/oxlint/profiles/react.js +7 -0
- package/presets/prettier/astro.json +6 -0
- package/presets/tsconfig/astro.json +25 -0
- package/presets/tsconfig/expo.json +16 -6
- package/presets/tsconfig/library.json +17 -0
- package/presets/tsconfig/next.json +12 -2
- package/presets/tsconfig/node.json +18 -4
- package/presets/tsconfig/react.json +33 -0
- package/presets/tsdown/build.d.ts +13 -0
- package/presets/tsdown/bundle.d.ts +13 -0
- package/presets/tsdown/bundle.js +10 -1
- package/rules/README.md +23 -0
- package/rules/_contract.js +207 -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 +56 -0
- package/rules/bundler.js +19 -0
- package/rules/catalog.js +166 -0
- package/rules/catalog.test.ts +98 -0
- package/rules/compile.js +125 -0
- package/rules/core/eslint.js +234 -0
- package/rules/core/import.js +117 -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 +223 -0
- package/rules/core/unicorn.js +210 -0
- package/rules/next.js +53 -0
- package/rules/profiles.js +95 -0
- package/rules/react-native.js +48 -0
- package/rules/react.js +155 -0
- package/rules/sorted.js +41 -0
- package/rules/vitest.js +178 -0
- package/src/docs.d.ts +4 -4
- package/src/docs.js +75 -57
- package/src/docs.test.ts +43 -31
- package/src/index.d.ts +14 -9
- package/src/index.js +17 -8
- package/src/oxfmt.d.ts +15 -2
- package/src/oxfmt.test.ts +10 -0
- package/src/oxlint.d.ts +59 -10
- package/src/oxlint.js +36 -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,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:
|
|
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:
|
|
31
|
-
return auditDocs(tree
|
|
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:
|
|
36
|
-
return auditDocs(tree
|
|
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()
|
|
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)).
|
|
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)).
|
|
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)).
|
|
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: {
|
|
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)).
|
|
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())).
|
|
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: {
|
|
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)).
|
|
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: {
|
|
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)).
|
|
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: {
|
|
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)).
|
|
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': [
|
|
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())).
|
|
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)).
|
|
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)).
|
|
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)).
|
|
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)).
|
|
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)).
|
|
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)).
|
|
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)).
|
|
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)).
|
|
631
|
+
expect(rules(tree)).toStrictEqual([]);
|
|
620
632
|
});
|
package/src/index.d.ts
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
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;
|
|
14
|
+
react: OxlintConfig;
|
|
10
15
|
};
|
|
11
16
|
|
|
12
17
|
declare const defaultExport: {
|
|
13
18
|
oxfmt: typeof oxfmtConfig;
|
|
14
|
-
oxlint: typeof
|
|
19
|
+
oxlint: typeof oxlintProfiles;
|
|
15
20
|
};
|
|
16
21
|
|
|
17
|
-
export { oxfmtConfig as oxfmt,
|
|
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
|
|
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 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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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).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
|
-
|
|
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
|
|
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
|
+
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
|
-
*
|
|
10
|
-
*
|
|
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
|
|
47
|
+
declare function layers(definition: { id?: string; map: readonly Layer[] }): OxlintConfig;
|
|
14
48
|
|
|
15
49
|
export { defineConfig } from 'oxlint';
|
|
16
|
-
export {
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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 {
|
|
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(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,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
|
|
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
|
+
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
|
+
}
|