@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
@@ -1,28 +1,28 @@
1
1
  import { expect, test } from 'vitest';
2
2
 
3
- import { compose, defineConfig, expo, hexagonal, next, node } from './oxlint.js';
3
+ import { compose, defineConfig, hexagonal, layers } from './oxlint.js';
4
4
 
5
5
  test('concatenates and dedupes plugin lists', () => {
6
- // Given - two fragments sharing one jsPlugin
6
+ // Given - two configs sharing one jsPlugin
7
7
  const merged = compose(
8
8
  { jsPlugins: ['a', 'b'], plugins: ['typescript'] },
9
9
  { jsPlugins: ['b', 'c'], plugins: ['typescript', 'import'] },
10
10
  );
11
11
 
12
12
  // Then - order preserved, duplicates dropped
13
- expect(merged.jsPlugins).toEqual(['a', 'b', 'c']);
14
- expect(merged.plugins).toEqual(['typescript', 'import']);
13
+ expect(merged.jsPlugins).toStrictEqual(['a', 'b', 'c']);
14
+ expect(merged.plugins).toStrictEqual(['typescript', 'import']);
15
15
  });
16
16
 
17
- test('shallow-merges rules with last fragment winning', () => {
18
- // Given - two fragments disagreeing on one rule
17
+ test('shallow-merges rules with the last config winning', () => {
18
+ // Given - two configs disagreeing on one rule
19
19
  const merged = compose(
20
20
  { rules: { curly: 'error', 'no-ternary': 'off' } },
21
21
  { rules: { curly: 'off', 'jterrazz/b4-given-then': 'error' } },
22
22
  );
23
23
 
24
- // Then - the later fragment wins per key, others survive
25
- expect(merged.rules).toEqual({
24
+ // Then - the later config wins per key, others survive
25
+ expect(merged.rules).toStrictEqual({
26
26
  curly: 'off',
27
27
  'jterrazz/b4-given-then': 'error',
28
28
  'no-ternary': 'off',
@@ -30,13 +30,13 @@ test('shallow-merges rules with last fragment winning', () => {
30
30
  });
31
31
 
32
32
  test('concatenates overrides in order without deduping', () => {
33
- // Given - two fragments each shipping an override
33
+ // Given - two configs each shipping an override
34
34
  const first = { files: ['**/*.specification.ts'], rules: {} };
35
35
  const second = { files: ['src/**'], rules: {} };
36
36
  const merged = compose({ overrides: [first] }, { overrides: [second] });
37
37
 
38
38
  // Then - both overrides survive, in composition order
39
- expect(merged.overrides).toEqual([first, second]);
39
+ expect(merged.overrides).toStrictEqual([first, second]);
40
40
  });
41
41
 
42
42
  test('concatenates and dedupes ignorePatterns', () => {
@@ -47,40 +47,94 @@ test('concatenates and dedupes ignorePatterns', () => {
47
47
  );
48
48
 
49
49
  // Then - one entry each
50
- expect(merged.ignorePatterns).toEqual(['dist/**', 'node_modules/**', '**/_fixtures/**']);
50
+ expect(merged.ignorePatterns).toStrictEqual(['dist/**', 'node_modules/**', '**/_fixtures/**']);
51
51
  });
52
52
 
53
- test('takes unknown scalar keys from the last fragment', () => {
54
- // Given - fragments disagreeing on a scalar key
55
- const merged = compose({ somethingElse: 1 }, { somethingElse: 2 });
53
+ test('takes a key the merge tables do not name from the last config that sets it', () => {
54
+ // Given - configs disagreeing on `categories`, a key the merge tables leave out
55
+ const merged = compose(
56
+ { categories: { correctness: 'off' } },
57
+ { categories: { correctness: 'error' } },
58
+ );
59
+
60
+ /*
61
+ * No config this package SHIPS carries a `categories` — every rule is
62
+ * decided by name — and that is exactly why it is the key available here.
63
+ */
56
64
 
57
- // Then - last wins
58
- expect(merged.somethingElse).toBe(2);
65
+ // Then - last wins, which is the rule for every key the three tables leave out
66
+ expect(merged.categories).toStrictEqual({ correctness: 'error' });
59
67
  });
60
68
 
61
- test('ignores null and undefined fragments', () => {
62
- // Given - a composition with holes (conditional fragments)
69
+ test('ignores null and undefined configs', () => {
70
+ // Given - a composition with holes (a conditional fragment)
63
71
  const merged = compose(
64
- undefined as unknown as Record<string, unknown>,
72
+ // @ts-expect-error - a conditional fragment is undefined at runtime, which the merge must survive
73
+ undefined,
65
74
  { rules: { curly: 'error' } },
66
- null as unknown as Record<string, unknown>,
75
+ null,
67
76
  );
68
77
 
69
78
  // Then - the holes contribute nothing
70
- expect(merged.rules).toEqual({ curly: 'error' });
79
+ expect(merged.rules).toStrictEqual({ curly: 'error' });
80
+ });
81
+
82
+ test('gives a layer map one override per layer, complete', () => {
83
+ // Given - a two-layer map
84
+ const fragment = layers({
85
+ map: [
86
+ {
87
+ deny: ['**/adapters/**'],
88
+ files: ['**/core/**'],
89
+ message: 'the core is pure',
90
+ name: 'core',
91
+ },
92
+ ],
93
+ });
94
+
95
+ // Then - the layer's whole pattern list sits inside its own override,
96
+ // Because an oxlint override REPLACES the base entry and never merges it
97
+ expect(fragment.overrides).toStrictEqual([
98
+ {
99
+ files: ['**/core/**'],
100
+ rules: {
101
+ 'no-restricted-imports': [
102
+ 'error',
103
+ {
104
+ patterns: [{ group: ['**/adapters/**'], message: 'the core is pure' }],
105
+ },
106
+ ],
107
+ },
108
+ },
109
+ ]);
110
+ });
111
+
112
+ /** A map whose two layers claim the same files — the shape `layers()` refuses. */
113
+ function buildOverlappingMap() {
114
+ return layers({
115
+ map: [
116
+ { deny: ['**/a/**'], files: ['**/same/**'], message: 'a', name: 'first' },
117
+ { deny: ['**/b/**'], files: ['**/same/**'], message: 'b', name: 'second' },
118
+ ],
119
+ });
120
+ }
121
+
122
+ test('refuses a layer map where one glob belongs to two layers', () => {
123
+ // Given - two layers claiming the same files
124
+ // Then - the map is refused rather than silently losing one of the two
125
+ expect(buildOverlappingMap).toThrow(/belongs to both/u);
71
126
  });
72
127
 
73
- test('exports the named presets', () => {
74
- // Given - the tool-facing entry
75
- // Then - every preset is a config object
76
- for (const preset of [node, expo, next, hexagonal]) {
77
- expect(typeof preset).toBe('object');
78
- }
128
+ test('ships the hexagonal map as six complete overrides', () => {
129
+ // Given - the default map
130
+ // Then - one override per layer, and the features carve-out is a negated glob
131
+ expect(hexagonal.overrides).toHaveLength(6);
132
+ expect(JSON.stringify(hexagonal.overrides)).toContain('!**/presentation/features/common/**');
79
133
  });
80
134
 
81
135
  test("re-exports oxlint's own defineConfig", () => {
82
136
  // Given - a config a consumer would write, with oxlint declared nowhere in its project
83
- const config = { rules: { curly: 'error' } };
137
+ const config = { rules: { curly: 'error' } } as const;
84
138
 
85
139
  // Then - the entry carries the tool's helper, which returns the config unchanged
86
140
  expect(defineConfig(config)).toBe(config);
@@ -1,39 +0,0 @@
1
- export default [
2
- {
3
- from: '/domain/',
4
- disallow: [
5
- '/application/',
6
- '/infrastructure/',
7
- '/presentation/',
8
- '/di/',
9
- '/config/',
10
- '/generated/',
11
- ],
12
- message: 'Domain layer must be pure - cannot import from other layers',
13
- },
14
- {
15
- from: '/application/use-cases/',
16
- disallow: ['/infrastructure/', '/presentation/', '/di/'],
17
- message: 'Use cases can only depend on domain and ports',
18
- },
19
- {
20
- from: '/application/ports/',
21
- disallow: ['/infrastructure/', '/presentation/', '/di/'],
22
- message: 'Ports are interfaces - cannot depend on implementations',
23
- },
24
- {
25
- from: '/infrastructure/inbound/',
26
- disallow: ['/infrastructure/outbound/'],
27
- message: 'Inbound adapters should not import outbound adapters - use DI',
28
- },
29
- {
30
- from: '/presentation/ui/(atoms|molecules)/',
31
- disallow: ['/navigation/'],
32
- message: 'Atoms and molecules must be pure - no navigation imports',
33
- },
34
- {
35
- from: '/presentation/features/',
36
- disallow: ['/presentation/features/(?!common)'],
37
- message: 'Features should be independent - use shared code in features/common',
38
- },
39
- ];
@@ -1,13 +0,0 @@
1
- import { resolve } from 'node:path';
2
- import { defineConfig } from 'oxlint';
3
-
4
- import defaultRules from './hexagonal-rules.js';
5
-
6
- const pluginPath = resolve(import.meta.dirname, '../plugins/codestyle.js');
7
-
8
- export default defineConfig({
9
- jsPlugins: [pluginPath],
10
- rules: {
11
- 'codestyle/arch-hexagonal': ['error', { rules: defaultRules }],
12
- },
13
- });
@@ -1,145 +0,0 @@
1
- import { createRequire } from 'node:module';
2
- import { defineConfig } from 'oxlint';
3
-
4
- const require = createRequire(import.meta.url);
5
- const perfectionistPath = require.resolve('eslint-plugin-perfectionist');
6
-
7
- /*
8
- * Base lint preset — tooling rules only. Wiring is EXPLICIT: this preset never
9
- * auto-detects other jterrazz packages. A project using @jterrazz/test composes
10
- * its `testing` fragment itself:
11
- *
12
- * import { testing } from '@jterrazz/test/oxlint';
13
- * import { compose, node } from '@jterrazz/typescript/oxlint';
14
- *
15
- * export default compose(node, testing);
16
- *
17
- * (The @jterrazz/test plugin is ESM-only: a CommonJS oxlint config silently
18
- * drops it — `typescript check` still warns loudly about that pitfall.)
19
- */
20
- export default defineConfig({
21
- plugins: ['typescript', 'import', 'oxc', 'unicorn'],
22
- jsPlugins: [perfectionistPath],
23
- categories: {
24
- correctness: 'error',
25
- suspicious: 'error',
26
- perf: 'error',
27
- pedantic: 'off',
28
- style: 'error',
29
- nursery: 'off',
30
- },
31
- rules: {
32
- // ============================================
33
- // ENABLED RULES
34
- // ============================================
35
-
36
- 'no-unused-vars': 'error',
37
-
38
- // -- TypeScript --
39
- 'typescript/consistent-type-imports': [
40
- 'error',
41
- {
42
- disallowTypeAnnotations: true,
43
- fixStyle: 'inline-type-imports',
44
- prefer: 'type-imports',
45
- },
46
- ],
47
- 'typescript/no-unused-expressions': [
48
- 'error',
49
- {
50
- allowShortCircuit: true,
51
- allowTernary: true,
52
- allowTaggedTemplates: true,
53
- },
54
- ],
55
-
56
- // -- Perfectionist (sorting/ordering) --
57
- 'perfectionist/sort-heritage-clauses': ['error', { type: 'natural' }],
58
- 'perfectionist/sort-intersection-types': ['error', { type: 'natural' }],
59
- 'perfectionist/sort-jsx-props': ['error', { type: 'natural' }],
60
- 'perfectionist/sort-named-exports': ['error', { type: 'natural' }],
61
- 'perfectionist/sort-named-imports': ['error', { type: 'natural' }],
62
- 'perfectionist/sort-union-types': ['error', { type: 'natural' }],
63
- 'perfectionist/sort-imports': [
64
- 'error',
65
- {
66
- type: 'alphabetical',
67
- order: 'asc',
68
- ignoreCase: true,
69
- newlinesBetween: 1,
70
- groups: [
71
- ['builtin', 'external'],
72
- 'internal',
73
- ['parent', 'sibling', 'index'],
74
- 'style',
75
- 'unknown',
76
- ],
77
- },
78
- ],
79
-
80
- // ============================================
81
- // DISABLED - Conflicts with perfectionist
82
- // ============================================
83
-
84
- 'sort-keys': 'off',
85
- 'sort-imports': 'off',
86
-
87
- // ============================================
88
- // DISABLED - Too strict / opinionated
89
- // ============================================
90
-
91
- 'func-style': 'off',
92
- 'no-magic-numbers': 'off',
93
- 'no-ternary': 'off',
94
- 'init-declarations': 'off',
95
- 'id-length': 'off',
96
- 'max-statements': 'off',
97
- 'max-params': 'off',
98
- 'no-continue': 'off',
99
- 'arrow-body-style': 'off',
100
- 'prefer-destructuring': 'off',
101
- 'new-cap': 'off',
102
- 'no-await-in-loop': 'off',
103
- 'typescript/consistent-type-definitions': 'off',
104
- 'typescript/array-type': 'off',
105
- 'typescript/no-inferrable-types': 'off',
106
- 'unicorn/no-null': 'off',
107
- 'unicorn/no-array-sort': 'off',
108
- 'import/no-unassigned-import': 'off',
109
- 'import/no-named-default': 'off',
110
- 'import/no-nodejs-modules': 'off',
111
- // Exports-at-end fights how these files actually read everywhere in
112
- // The ecosystem — three repos had disabled it locally (jterrazz-web,
113
- // Package-test, package-attestation) before it moved here.
114
- 'import/exports-last': 'off',
115
-
116
- // ============================================
117
- // ENABLED - Code style enforcement
118
- // ============================================
119
-
120
- 'capitalized-comments': 'error',
121
- curly: 'error',
122
- 'no-nested-ternary': 'error',
123
- 'unicorn/catch-error-name': 'error',
124
- 'unicorn/numeric-separators-style': 'error',
125
-
126
- // ============================================
127
- // DISABLED - Conflicts with other rules
128
- // ============================================
129
-
130
- 'import/no-named-export': 'off',
131
- 'import/consistent-type-specifier-style': 'off',
132
- 'import/prefer-default-export': 'off',
133
- 'import/no-default-export': 'off',
134
- 'import/group-exports': 'off',
135
- 'import/no-anonymous-default-export': 'off',
136
-
137
- // ============================================
138
- // ENABLED - Clean imports
139
- // ============================================
140
-
141
- 'import/first': 'error',
142
- 'import/no-namespace': 'error',
143
- },
144
- ignorePatterns: ['dist/**', 'node_modules/**'],
145
- });
@@ -1,36 +0,0 @@
1
- import { resolve } from 'node:path';
2
- import { defineConfig } from 'oxlint';
3
-
4
- import base from './base.js';
5
-
6
- const pluginPath = resolve(import.meta.dirname, 'plugins/codestyle.js');
7
-
8
- export default defineConfig({
9
- extends: [base],
10
- plugins: ['typescript', 'import', 'react'],
11
- jsPlugins: [pluginPath],
12
- // `.expo/` is the toolchain cache and `assets/` is scaffolded by Expo
13
- // Itself (icons, splash, fonts) — never source.
14
- ignorePatterns: ['dist/**', 'node_modules/**', '.expo/**', 'assets/**'],
15
- rules: {
16
- 'typescript/no-require-imports': [
17
- 'error',
18
- {
19
- allow: [
20
- String.raw`\.png$`,
21
- String.raw`\.jpg$`,
22
- String.raw`\.jpeg$`,
23
- String.raw`\.gif$`,
24
- String.raw`\.webp$`,
25
- ],
26
- },
27
- ],
28
- 'codestyle/imports-without-ext': 'error',
29
- 'react/react-in-jsx-scope': 'off',
30
- 'react/jsx-props-no-spreading': 'off',
31
- 'react/jsx-boolean-value': 'off',
32
- 'react/jsx-handler-names': 'off',
33
- 'react/jsx-curly-brace-presence': 'off',
34
- 'unicorn/no-nested-ternary': 'off',
35
- },
36
- });
@@ -1,43 +0,0 @@
1
- import { resolve } from 'node:path';
2
- import { defineConfig } from 'oxlint';
3
-
4
- import base from './base.js';
5
-
6
- const pluginPath = resolve(import.meta.dirname, 'plugins/codestyle.js');
7
-
8
- export default defineConfig({
9
- extends: [base],
10
- plugins: ['typescript', 'import', 'react', 'nextjs'],
11
- jsPlugins: [pluginPath],
12
- // `public/` is Next's static dir and `assets/` is the stack's convention
13
- // For content trees (jterrazz-web: signed attestation bytes live there —
14
- // A formatter pass would invalidate every proof). Neither is source.
15
- ignorePatterns: [
16
- 'dist/**',
17
- 'node_modules/**',
18
- '.next/**',
19
- 'next-env.d.ts',
20
- 'assets/**',
21
- 'public/**',
22
- ],
23
- rules: {
24
- 'codestyle/imports-without-ext': 'error',
25
- 'react/react-in-jsx-scope': 'off',
26
- 'react/jsx-props-no-spreading': 'off',
27
- 'react/jsx-boolean-value': 'off',
28
- 'react/jsx-handler-names': 'off',
29
- 'react/jsx-curly-brace-presence': 'off',
30
- 'react/jsx-max-depth': 'off',
31
- 'unicorn/no-nested-ternary': 'off',
32
- // -- Rules that fight the Next idiom itself, not bad code --
33
- // (import/exports-last is off in base: it fights the whole ecosystem.)
34
- // Immutable serialization maps (`.map((x) => ({ ...x, url }))`) are
35
- // The dominant React idiom; the Object.assign form is strictly worse
36
- // For the build-time collection sizes a site actually maps over.
37
- 'oxc/no-map-spread': 'off',
38
- // Client components are browser-only code that means `window`;
39
- // Asking them to say `globalThis` trades clarity for portability
40
- // They will never need.
41
- 'unicorn/prefer-global-this': 'off',
42
- },
43
- });
@@ -1,14 +0,0 @@
1
- import { resolve } from 'node:path';
2
- import { defineConfig } from 'oxlint';
3
-
4
- import base from './base.js';
5
-
6
- const pluginPath = resolve(import.meta.dirname, 'plugins/codestyle.js');
7
-
8
- export default defineConfig({
9
- extends: [base],
10
- jsPlugins: [pluginPath],
11
- rules: {
12
- 'codestyle/imports-with-ext': 'error',
13
- },
14
- });