@next-vibe/checker 1.0.11

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.
@@ -0,0 +1,989 @@
1
+ /* eslint-disable i18next/no-literal-string */
2
+ /**
3
+ * Unified Check Configuration
4
+ *
5
+ * Single source of truth for all code quality tools:
6
+ * - Oxlint (fast Rust linter)
7
+ * - ESLint (import sorting, react hooks)
8
+ * - Prettier (code formatting)
9
+ * - TypeScript (type checking)
10
+ * - Testing (vitest)
11
+ * - VSCode integration
12
+ */
13
+
14
+ import type {
15
+ CheckConfig,
16
+ EslintFlatConfigItem,
17
+ } from "next-vibe/system/check/config/types";
18
+
19
+ // --------------------------------------------------------
20
+ // Vibe Check Defaults
21
+ // --------------------------------------------------------
22
+ const vibeCheck: CheckConfig["vibeCheck"] = {
23
+ fix: false,
24
+ skipLint: false,
25
+ skipEslint: false,
26
+ skipOxlint: false,
27
+ skipTypecheck: false,
28
+ timeout: 3600,
29
+ limit: 200,
30
+ maxFilesInSummary: 50,
31
+ };
32
+
33
+ // ============================================================
34
+ // Feature Flags (local to this file, not part of CheckConfig)
35
+ // ============================================================
36
+
37
+ const features = {
38
+ // Linting features
39
+ react: true, // React-specific rules
40
+ reactCompiler: true, // React compiler rules (requires react)
41
+ accessibility: true, // jsx-a11y rules
42
+ promise: true, // Promise best practices
43
+ node: true, // Node.js rules
44
+ unicorn: true, // Unicorn rules (modern JS)
45
+ nextjs: true, // Next.js specific rules
46
+ pedantic: false, // Stricter/pedantic rules
47
+ // Custom plugins
48
+ i18n: true, // Check for untranslated strings
49
+ jsxCapitalization: true, // Enforce capitalized JSX components
50
+ restrictedSyntax: true, // No throw, unknown, object types
51
+ // TypeScript
52
+ tsgo: true, // Use tsgo instead of tsc for type checking
53
+ strictTypes: true, // Strict type checking rules
54
+ } as const;
55
+
56
+ // ============================================================
57
+ // Shared Ignores (files, folders, globs - mixed)
58
+ // ============================================================
59
+
60
+ const { oxlintIgnores, eslintIgnores } = formatIgnorePatterns([
61
+ // Directories
62
+ "dist",
63
+ ".dist",
64
+ ".next",
65
+ ".tmp",
66
+ "node_modules",
67
+ ".git",
68
+ "coverage",
69
+ "public",
70
+ "drizzle",
71
+ ".vscode",
72
+ ".vibe-guard-instance",
73
+ ".github",
74
+ ".claude",
75
+ "to_migrate",
76
+ "postgres_data",
77
+ ".nyc_output",
78
+ "build",
79
+ "test-files",
80
+ "test-project",
81
+ // Files
82
+ ".DS_Store",
83
+ "thumbs.db",
84
+ ".gitignore",
85
+ ".env",
86
+ ".env.local",
87
+ ".env.development",
88
+ ".env.production",
89
+ "next-env.d.ts",
90
+ "nativewind-env.d.ts",
91
+ // Glob patterns
92
+ "**/test-files/**",
93
+ ]);
94
+
95
+ // --------------------------------------------------------
96
+ // Typecheck Configuration
97
+ // --------------------------------------------------------
98
+ const typecheck = {
99
+ enabled: true as const,
100
+ cachePath: ".tmp/typecheck-cache",
101
+ useTsgo: features.tsgo,
102
+ };
103
+
104
+ // --------------------------------------------------------
105
+ // Oxlint Configuration
106
+ // --------------------------------------------------------
107
+ const oxlint: CheckConfig["oxlint"] = {
108
+ enabled: true,
109
+ configPath: ".tmp/.oxlintrc.json",
110
+ cachePath: ".tmp/oxlint-cache",
111
+ lintableExtensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
112
+ $schema: "./node_modules/oxlint/configuration_schema.json",
113
+ ignorePatterns: oxlintIgnores,
114
+ plugins: [
115
+ "typescript",
116
+ "oxc",
117
+ ...(features.unicorn ? ["unicorn"] : []),
118
+ ...(features.react ? ["react"] : []),
119
+ ...(features.accessibility ? ["jsx-a11y"] : []),
120
+ ...(features.promise ? ["promise"] : []),
121
+ ...(features.node ? ["node"] : []),
122
+ ...(features.nextjs ? ["nextjs"] : []),
123
+ ],
124
+ jsPlugins: [
125
+ ...(features.restrictedSyntax
126
+ ? [
127
+ "next-vibe/src/app/api/[locale]/system/check/oxlint/plugins/restricted-syntax/src/index.ts",
128
+ ]
129
+ : []),
130
+ ...(features.jsxCapitalization
131
+ ? [
132
+ "next-vibe/src/app/api/[locale]/system/check/oxlint/plugins/jsx-capitalization/src/index.ts",
133
+ ]
134
+ : []),
135
+ ...(features.i18n
136
+ ? [
137
+ "next-vibe/src/app/api/[locale]/system/check/oxlint/plugins/i18n/src/index.ts",
138
+ ]
139
+ : []),
140
+ ],
141
+ categories: {
142
+ correctness: "error",
143
+ suspicious: "error",
144
+ pedantic: features.pedantic ? "warn" : "off",
145
+ style: "off",
146
+ },
147
+ rules: {
148
+ // ── Core ESLint (always enabled) ──────────────────────────
149
+ "no-debugger": "error",
150
+ "no-console": "error",
151
+ curly: "error",
152
+ eqeqeq: "error",
153
+ "no-undef": "off",
154
+ camelcase: "error",
155
+ "no-template-curly-in-string": "error",
156
+ "no-unsafe-optional-chaining": "error",
157
+ "array-callback-return": "error",
158
+ "no-constructor-return": "error",
159
+ "no-self-compare": "error",
160
+ "no-unreachable-loop": "error",
161
+ "no-unused-private-class-members": "error",
162
+ "prefer-template": "error",
163
+ "require-atomic-updates": "warn",
164
+ "no-promise-executor-return": "error",
165
+
166
+ // ── TypeScript (always enabled) ───────────────────────────
167
+ "typescript/no-explicit-any": "error",
168
+ "typescript/no-unused-vars": [
169
+ "error",
170
+ {
171
+ vars: "all",
172
+ args: "all",
173
+ caughtErrors: "none",
174
+ ignoreRestSiblings: false,
175
+ argsIgnorePattern: "^$",
176
+ varsIgnorePattern: "^$",
177
+ },
178
+ ],
179
+ "typescript/no-inferrable-types": "error",
180
+ "typescript/consistent-type-imports": "error",
181
+ "typescript/no-empty-function": "error",
182
+ "typescript/prefer-includes": "error",
183
+ "typescript/prefer-string-starts-ends-with": "error",
184
+ "typescript/ban-ts-comment": "error",
185
+ "typescript/consistent-type-definitions": "error",
186
+ "typescript/no-empty-object-type": "error",
187
+ "typescript/no-unsafe-function-type": "error",
188
+ "typescript/no-wrapper-object-types": "error",
189
+ "typescript/no-duplicate-enum-values": "error",
190
+ "typescript/no-extra-non-null-assertion": "error",
191
+ "no-extraneous-class": "off",
192
+ // Strict type rules (enabled via strictTypes flag)
193
+ ...(features.strictTypes
194
+ ? {
195
+ "typescript/await-thenable": "error",
196
+ "typescript/no-floating-promises": "error",
197
+ "typescript/no-for-in-array": "error",
198
+ "typescript/no-misused-promises": "error",
199
+ "typescript/no-unsafe-assignment": "error",
200
+ "typescript/no-unnecessary-type-assertion": "error",
201
+ "typescript/explicit-function-return-type": "error",
202
+ "typescript/restrict-template-expressions": "error",
203
+ }
204
+ : {}),
205
+
206
+ // ── React (enabled via react flag) ────────────────────────
207
+ ...(features.react
208
+ ? {
209
+ "react/jsx-key": "error",
210
+ "react/jsx-no-duplicate-props": "error",
211
+ "react/jsx-no-undef": "error",
212
+ "react/jsx-uses-react": "off",
213
+ "react/jsx-uses-vars": "error",
214
+ "react/no-children-prop": "error",
215
+ "react/no-deprecated": "error",
216
+ "react/no-direct-mutation-state": "error",
217
+ "react/no-unknown-property": "error",
218
+ "react/self-closing-comp": "error",
219
+ "react/react-in-jsx-scope": "off",
220
+ }
221
+ : {}),
222
+
223
+ // ── Accessibility (enabled via accessibility flag) ────────
224
+ ...(features.accessibility
225
+ ? {
226
+ "jsx-a11y/alt-text": "error",
227
+ "jsx-a11y/aria-props": "error",
228
+ "jsx-a11y/aria-role": "error",
229
+ "jsx-a11y/anchor-has-content": "error",
230
+ "jsx-a11y/aria-proptypes": "error",
231
+ "jsx-a11y/aria-unsupported-elements": "error",
232
+ "jsx-a11y/click-events-have-key-events": "error",
233
+ "jsx-a11y/heading-has-content": "error",
234
+ "jsx-a11y/html-has-lang": "error",
235
+ "jsx-a11y/iframe-has-title": "error",
236
+ "jsx-a11y/img-redundant-alt": "error",
237
+ "jsx-a11y/no-access-key": "error",
238
+ "jsx-a11y/no-autofocus": "error",
239
+ "jsx-a11y/no-distracting-elements": "error",
240
+ "jsx-a11y/no-redundant-roles": "error",
241
+ "jsx-a11y/role-has-required-aria-props": "error",
242
+ "jsx-a11y/role-supports-aria-props": "error",
243
+ "jsx-a11y/scope": "error",
244
+ "jsx-a11y/tabindex-no-positive": "error",
245
+ }
246
+ : {}),
247
+
248
+ // ── Promise (enabled via promise flag) ────────────────────
249
+ ...(features.promise
250
+ ? {
251
+ "promise/param-names": "error",
252
+ "promise/always-return": "error",
253
+ "promise/catch-or-return": "error",
254
+ }
255
+ : {}),
256
+
257
+ // ── Node (enabled via node flag) ──────────────────────────
258
+ ...(features.node
259
+ ? {
260
+ "node/no-missing-import": "off",
261
+ "node/no-unsupported-features/es-syntax": "off",
262
+ }
263
+ : {}),
264
+
265
+ // ── Unicorn (enabled via unicorn flag) ────────────────────
266
+ ...(features.unicorn
267
+ ? {
268
+ "unicorn/prefer-string-starts-ends-with": "error",
269
+ "unicorn/no-empty-file": "error",
270
+ "unicorn/no-unnecessary-await": "error",
271
+ "unicorn/no-useless-spread": "error",
272
+ "unicorn/prefer-set-size": "error",
273
+ "unicorn/no-await-in-promise-methods": "error",
274
+ "unicorn/no-invalid-fetch-options": "error",
275
+ "unicorn/no-invalid-remove-event-listener": "error",
276
+ "unicorn/no-new-array": "error",
277
+ "unicorn/no-single-promise-in-promise-methods": "error",
278
+ "unicorn/no-thenable": "error",
279
+ "unicorn/no-useless-fallback-in-spread": "off",
280
+ "unicorn/no-useless-length-check": "error",
281
+ "unicorn/prefer-array-flat": "error",
282
+ "unicorn/prefer-array-flat-map": "error",
283
+ "unicorn/prefer-includes": "error",
284
+ "unicorn/prefer-modern-dom-apis": "error",
285
+ "unicorn/prefer-node-protocol": "error",
286
+ "unicorn/prefer-spread": "error",
287
+ }
288
+ : {}),
289
+
290
+ // ── OXC (always enabled) ──────────────────────────────────
291
+ "oxc/no-optional-chaining": "off",
292
+ "oxc/missing-throw": "error",
293
+ "oxc/number-arg-out-of-range": "error",
294
+ "oxc/only-used-in-recursion": "error",
295
+ "oxc/bad-array-method-on-arguments": "error",
296
+ "oxc/bad-comparison-sequence": "error",
297
+ "oxc/const-comparisons": "error",
298
+ "oxc/double-comparisons": "error",
299
+ "oxc/erasing-op": "error",
300
+ "oxc/bad-char-at-comparison": "error",
301
+ "oxc/bad-min-max-func": "error",
302
+ "oxc/bad-object-literal-comparison": "error",
303
+ "oxc/bad-replace-all-arg": "error",
304
+ "oxc/uninvoked-array-callback": "error",
305
+
306
+ // ── Pedantic rule overrides ─────────────────────────────────
307
+ "max-lines": "off",
308
+ ...(features.pedantic
309
+ ? { "max-lines-per-function": ["warn", { max: 150 }] }
310
+ : {}),
311
+ // Disable rules that conflict with TypeScript strict null checks
312
+ // (.at() and .codePointAt() return T | undefined)
313
+ "unicorn/prefer-at": "off",
314
+ "unicorn/prefer-code-point": "off",
315
+ "unicorn/prefer-negative-index": "off",
316
+ // Conflicts with promise/always-return which requires returning something
317
+ "unicorn/no-useless-undefined": "off",
318
+ "unicorn/no-useless-switch-case": "off",
319
+ "unicorn/no-array-callback-reference": "off",
320
+ "unicorn/prefer-query-selector": "off",
321
+
322
+ // ── Custom JS Plugins (enabled via feature flags) ────────
323
+ // Options defined inline, plugins read from config.oxlint.rules
324
+ ...(features.restrictedSyntax
325
+ ? {
326
+ "oxlint-plugin-restricted/restricted-syntax": [
327
+ "error",
328
+ {
329
+ jsxAllowedProperties: [
330
+ "icon",
331
+ "content",
332
+ "title",
333
+ "description",
334
+ "children",
335
+ "header",
336
+ "footer",
337
+ "element",
338
+ "component",
339
+ "label",
340
+ "placeholder",
341
+ "tooltip",
342
+ "badge",
343
+ "prefix",
344
+ "suffix",
345
+ "startAdornment",
346
+ "endAdornment",
347
+ "emptyState",
348
+ "fallback",
349
+ ],
350
+ },
351
+ ],
352
+ }
353
+ : {}),
354
+ ...(features.jsxCapitalization
355
+ ? {
356
+ "oxlint-plugin-jsx-capitalization/jsx-capitalization": [
357
+ "error",
358
+ {
359
+ excludedPaths: ["/src/packages/next-vibe-ui/web/"],
360
+ excludedFilePatterns: [
361
+ "/email.tsx",
362
+ ".email.tsx",
363
+ "/test.tsx",
364
+ ".test.tsx",
365
+ ".spec.tsx",
366
+ "/__tests__/",
367
+ ],
368
+ typographyElements: [
369
+ "h1",
370
+ "h2",
371
+ "h3",
372
+ "h4",
373
+ "p",
374
+ "blockquote",
375
+ "code",
376
+ ],
377
+ standaloneElements: ["span", "pre"],
378
+ svgElements: [
379
+ "svg",
380
+ "path",
381
+ "circle",
382
+ "rect",
383
+ "line",
384
+ "polyline",
385
+ "polygon",
386
+ "ellipse",
387
+ "g",
388
+ "text",
389
+ "tspan",
390
+ "defs",
391
+ "linearGradient",
392
+ "radialGradient",
393
+ "stop",
394
+ "clipPath",
395
+ "mask",
396
+ "pattern",
397
+ "use",
398
+ "symbol",
399
+ "marker",
400
+ "foreignObject",
401
+ ],
402
+ imageElements: ["img", "picture"],
403
+ commonUiElements: [
404
+ "div",
405
+ "section",
406
+ "article",
407
+ "aside",
408
+ "header",
409
+ "footer",
410
+ "main",
411
+ "nav",
412
+ "button",
413
+ "input",
414
+ "textarea",
415
+ "select",
416
+ "option",
417
+ "label",
418
+ "form",
419
+ "fieldset",
420
+ "legend",
421
+ "ul",
422
+ "ol",
423
+ "li",
424
+ "dl",
425
+ "dt",
426
+ "dd",
427
+ "table",
428
+ "thead",
429
+ "tbody",
430
+ "tfoot",
431
+ "tr",
432
+ "th",
433
+ "td",
434
+ "caption",
435
+ "video",
436
+ "audio",
437
+ "source",
438
+ "track",
439
+ "canvas",
440
+ "hr",
441
+ "br",
442
+ "iframe",
443
+ "embed",
444
+ "object",
445
+ "details",
446
+ "summary",
447
+ "dialog",
448
+ "menu",
449
+ "figure",
450
+ "figcaption",
451
+ "time",
452
+ "progress",
453
+ "meter",
454
+ "output",
455
+ "strong",
456
+ "em",
457
+ "b",
458
+ "i",
459
+ "u",
460
+ "s",
461
+ ],
462
+ },
463
+ ],
464
+ }
465
+ : {}),
466
+ ...(features.i18n
467
+ ? {
468
+ "oxlint-plugin-i18n/no-literal-string": [
469
+ "error",
470
+ {
471
+ words: {
472
+ exclude: [
473
+ String.raw`^[\[\]{}—<>•+%#@.:_*;,/()\-]+$`,
474
+ String.raw`^\s+$`,
475
+ String.raw`^\d+$`,
476
+ String.raw`^[^\s]+\.[^\s]+$`,
477
+ String.raw`\.(?:jpe?g|png|svg|webp|gif|csv|json|xml|pdf)$`,
478
+ String.raw`^(?:https?://|/)[^\s]*$`,
479
+ String.raw`^[#@]\w+$`,
480
+ String.raw`^[a-z]+$`,
481
+ String.raw`^[a-z]+(?:[A-Z][a-zA-Z0-9]*)*$`,
482
+ String.raw`^[^\s]+(?:-[^\s]+)+$`,
483
+ String.raw`^[^\s]+\/(?:[^\s]*)$`,
484
+ String.raw`^[A-Z]+(?:_[A-Z]+)*$`,
485
+ String.raw`^use (?:client|server|custom)$`,
486
+ String.raw`^&[a-z]+;$`,
487
+ String.raw`^[MmLlHhVvCcSsQqTtAaZz0-9\s,.-]+$`,
488
+ String.raw`^[▶◀▲▼►◄▴▾►◄✅✕✔✓🔧]+$`,
489
+ String.raw`^[\d\s]+(?:px|em|rem|%|vh|vw|deg|rad)?(?:\s+[\d]+)*$`,
490
+ String.raw`^url\([^)]+\)$`,
491
+ String.raw`^(?:translate|rotate|scale|matrix|skew)\([^)]+\)$`,
492
+ String.raw`^(?:Esc|Enter|Tab|Shift|Ctrl|Alt|Cmd|Space|Backspace|Delete|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|F\d+)$`,
493
+ String.raw`^[A-Z0-9]{1,2}$`,
494
+ ],
495
+ },
496
+ "jsx-attributes": {
497
+ exclude: [
498
+ "className",
499
+ "*ClassName",
500
+ "id",
501
+ "data-testid",
502
+ "to",
503
+ "href",
504
+ "style",
505
+ "target",
506
+ "rel",
507
+ "type",
508
+ "src",
509
+ "viewBox",
510
+ "d",
511
+ "fill",
512
+ "stroke",
513
+ "transform",
514
+ "gradientTransform",
515
+ "gradientUnits",
516
+ "cn",
517
+ "cx",
518
+ "cy",
519
+ "r",
520
+ "fx",
521
+ "fy",
522
+ "offset",
523
+ "stopColor",
524
+ "stopOpacity",
525
+ "width",
526
+ "height",
527
+ "x",
528
+ "y",
529
+ "x1",
530
+ "x2",
531
+ "y1",
532
+ "y2",
533
+ "strokeWidth",
534
+ "strokeLinecap",
535
+ "strokeLinejoin",
536
+ "fillRule",
537
+ "clipRule",
538
+ "opacity",
539
+ "xmlns",
540
+ "xmlnsXlink",
541
+ "aria-label",
542
+ "aria-labelledby",
543
+ "aria-describedby",
544
+ "title",
545
+ "placeholder",
546
+ ],
547
+ },
548
+ "object-properties": {
549
+ exclude: [
550
+ "id",
551
+ "key",
552
+ "type",
553
+ "className",
554
+ "*ClassName",
555
+ "imageUrl",
556
+ "style",
557
+ "path",
558
+ "href",
559
+ "to",
560
+ "data",
561
+ "alg",
562
+ "backgroundColor",
563
+ "borderRadius",
564
+ "color",
565
+ "fontSize",
566
+ "lineHeight",
567
+ "padding",
568
+ "textDecoration",
569
+ "marginTop",
570
+ "marginBottom",
571
+ "margin",
572
+ "value",
573
+ "email",
574
+ "displayName",
575
+ ],
576
+ },
577
+ },
578
+ ],
579
+ }
580
+ : {}),
581
+
582
+ // ── Next.js (enabled via nextjs flag) ─────────────────────
583
+ ...(features.nextjs
584
+ ? {
585
+ "nextjs/google-font-display": "error",
586
+ "nextjs/google-font-preconnect": "error",
587
+ "nextjs/inline-script-id": "error",
588
+ "nextjs/next-script-for-ga": "error",
589
+ "nextjs/no-assign-module-variable": "error",
590
+ "nextjs/no-before-interactive-script-outside-document": "error",
591
+ "nextjs/no-css-tags": "error",
592
+ "nextjs/no-document-import-in-page": "error",
593
+ "nextjs/no-duplicate-head": "error",
594
+ "nextjs/no-head-element": "error",
595
+ "nextjs/no-head-import-in-document": "error",
596
+ "nextjs/no-html-link-for-pages": "error",
597
+ "nextjs/no-img-element": "warn",
598
+ "nextjs/no-page-custom-font": "error",
599
+ "nextjs/no-script-component-in-head": "error",
600
+ "nextjs/no-styled-jsx-in-document": "error",
601
+ "nextjs/no-sync-scripts": "error",
602
+ "nextjs/no-title-in-document-head": "error",
603
+ "nextjs/no-typos": "error",
604
+ "nextjs/no-unwanted-polyfillio": "error",
605
+ }
606
+ : {}),
607
+ },
608
+ settings: {
609
+ "jsx-a11y": {
610
+ polymorphicPropName: null,
611
+ components: {},
612
+ attributes: {},
613
+ },
614
+ next: { rootDir: ["."] },
615
+ react: { formComponents: [], linkComponents: [] },
616
+ jsdoc: {
617
+ ignorePrivate: false,
618
+ ignoreInternal: false,
619
+ ignoreReplacesDocs: true,
620
+ overrideReplacesDocs: true,
621
+ augmentsExtendsReplacesDocs: false,
622
+ implementsReplacesDocs: false,
623
+ exemptDestructuredRootsFromChecks: false,
624
+ tagNamePreference: {},
625
+ },
626
+ },
627
+ env: { builtin: true },
628
+ globals: {
629
+ React: "readonly",
630
+ JSX: "readonly",
631
+ NodeJS: "readonly",
632
+ __dirname: "readonly",
633
+ __filename: "readonly",
634
+ process: "readonly",
635
+ global: "readonly",
636
+ },
637
+ };
638
+
639
+ // --------------------------------------------------------
640
+ // Prettier Configuration
641
+ // --------------------------------------------------------
642
+ const prettier: CheckConfig["prettier"] = {
643
+ enabled: true,
644
+ configPath: ".tmp/.oxfmtrc.json",
645
+ semi: true,
646
+ singleQuote: false,
647
+ trailingComma: "all",
648
+ tabWidth: 2,
649
+ useTabs: false,
650
+ printWidth: 80,
651
+ arrowParens: "always",
652
+ endOfLine: "lf",
653
+ bracketSpacing: true,
654
+ jsxSingleQuote: false,
655
+ jsxBracketSameLine: false,
656
+ proseWrap: "preserve",
657
+ };
658
+
659
+ // --------------------------------------------------------
660
+ // Testing Configuration
661
+ // --------------------------------------------------------
662
+ const testing: CheckConfig["testing"] = {
663
+ enabled: true,
664
+ command: "bun test",
665
+ timeout: 300_000,
666
+ include: ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"],
667
+ exclude: [...oxlintIgnores],
668
+ coverage: {
669
+ enabled: false,
670
+ provider: "v8",
671
+ thresholds: { lines: 80, branches: 80, functions: 80, statements: 80 },
672
+ },
673
+ };
674
+
675
+ // --------------------------------------------------------
676
+ // VSCode Integration
677
+ // --------------------------------------------------------
678
+ const vscode: CheckConfig["vscode"] = {
679
+ enabled: true,
680
+ autoGenerateSettings: true,
681
+ settingsPath: "./.vscode/settings.json",
682
+ settings: {
683
+ oxc: {
684
+ enable: true,
685
+ lintRun: "onSave",
686
+ configPath: ".tmp/.oxlintrc.json",
687
+ fmtConfigPath: ".tmp/.oxfmtrc.json",
688
+ fmtExperimental: true,
689
+ typeAware: true,
690
+ traceServer: "verbose",
691
+ },
692
+ editor: {
693
+ formatOnSave: true,
694
+ defaultFormatter: "oxc.oxc-vscode",
695
+ codeActionsOnSave: {
696
+ "source.fixAll.eslint": "explicit",
697
+ "source.organizeImports": "explicit",
698
+ },
699
+ },
700
+ typescript: {
701
+ validateEnable: true,
702
+ suggestAutoImports: true,
703
+ preferTypeOnlyAutoImports: true,
704
+ experimentalUseTsgo: typecheck.enabled && typecheck.useTsgo,
705
+ },
706
+ files: { eol: "\n" },
707
+ search: {
708
+ exclude: {
709
+ "**/node_modules": true,
710
+ "**/tsconfig.tsbuildinfo": true,
711
+ "**/*.tmp": true,
712
+ "**/*.next": true,
713
+ },
714
+ },
715
+ },
716
+ };
717
+
718
+ // ============================================================
719
+ // ESLint FlatConfig Cache (lazy-loaded on first access)
720
+ // ============================================================
721
+ let cachedEslintFlatConfig: EslintFlatConfigItem[] | null = null;
722
+
723
+ const config = (): CheckConfig => {
724
+ // --------------------------------------------------------
725
+ // ESLint Configuration (for rules oxlint doesn't support)
726
+ // --------------------------------------------------------
727
+ const eslint: CheckConfig["eslint"] = {
728
+ enabled: true,
729
+ configPath: ".tmp/eslint.config.mjs",
730
+ cachePath: ".tmp/eslint-cache",
731
+ lintableExtensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
732
+ ignores: oxlintIgnores,
733
+ // Build flatConfig with plugins (called from eslint.config.mjs which loads plugins)
734
+ buildFlatConfig(
735
+ reactCompilerPlugin: unknown,
736
+ reactHooksPlugin: unknown,
737
+ simpleImportSortPlugin: unknown,
738
+ tseslint: unknown,
739
+ ): EslintFlatConfigItem[] {
740
+ if (cachedEslintFlatConfig) {
741
+ return cachedEslintFlatConfig;
742
+ }
743
+
744
+ cachedEslintFlatConfig = [
745
+ { ignores: eslintIgnores },
746
+ {
747
+ files: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
748
+ languageOptions: {
749
+ parser: tseslint.parser,
750
+ parserOptions: {
751
+ ecmaVersion: "latest",
752
+ sourceType: "module",
753
+ ecmaFeatures: { jsx: true },
754
+ },
755
+ },
756
+ linterOptions: { reportUnusedDisableDirectives: "off" },
757
+ plugins: {
758
+ "simple-import-sort": simpleImportSortPlugin,
759
+ ...(features.react ? { "react-hooks": reactHooksPlugin } : {}),
760
+ ...(features.reactCompiler
761
+ ? { "react-compiler": reactCompilerPlugin }
762
+ : {}),
763
+ // Stubs for oxlint rules
764
+ i18next: createEslintStub(["no-literal-string"]),
765
+ "oxlint-plugin-restricted": createEslintStub(["restricted-syntax"]),
766
+ "oxlint-plugin-jsx-capitalization": createEslintStub([
767
+ "jsx-capitalization",
768
+ ]),
769
+ "oxlint-plugin-i18n": createEslintStub(["no-literal-string"]),
770
+ "@typescript-eslint": createEslintStub([
771
+ "no-explicit-any",
772
+ "no-unused-vars",
773
+ "no-empty-function",
774
+ "no-empty-object-type",
775
+ "no-floating-promises",
776
+ "require-await",
777
+ "only-throw-error",
778
+ "no-unsafe-assignment",
779
+ "triple-slash-reference",
780
+ "consistent-type-definitions",
781
+ "no-require-imports",
782
+ "no-unsafe-function-type",
783
+ "explicit-function-return-type",
784
+ "no-namespace",
785
+ "no-unsafe-enum-comparison",
786
+ "consistent-type-imports",
787
+ "no-unnecessary-condition",
788
+ "no-base-to-string",
789
+ "no-unsafe-member-access",
790
+ "no-implied-eval",
791
+ ]),
792
+ "typescript-eslint": createEslintStub([
793
+ "no-explicit-any",
794
+ "no-unused-vars",
795
+ ]),
796
+ "eslint-plugin-unicorn": createEslintStub([
797
+ "require-module-specifiers",
798
+ ]),
799
+ prettier: createEslintStub(["prettier"]),
800
+ "eslint-plugin-promise": createEslintStub(["no-multiple-resolved"]),
801
+ "eslint-plugin-next": createEslintStub([
802
+ "no-html-link-for-pages",
803
+ "no-img-element",
804
+ "no-assign-module-variable",
805
+ ]),
806
+ "@next/next": createEslintStub(["no-img-element"]),
807
+ eslint: createEslintStub(["no-template-curly-in-string"]),
808
+ "jsx-a11y": createEslintStub(["prefer-tag-over-role"]),
809
+ oxc: createEslintStub(["only-used-in-recursion"]),
810
+ "eslint-plugin-import": createEslintStub(["no-named-as-default"]),
811
+ i18n: createEslintStub(["no-literal-string"]),
812
+ },
813
+ rules: {
814
+ "simple-import-sort/imports": "error",
815
+ "simple-import-sort/exports": "error",
816
+ ...(features.react
817
+ ? {
818
+ "react-hooks/rules-of-hooks": "error",
819
+ "react-hooks/exhaustive-deps": "error",
820
+ }
821
+ : {}),
822
+ ...(features.reactCompiler
823
+ ? { "react-compiler/react-compiler": "error" }
824
+ : {}),
825
+ "no-unused-vars": "off",
826
+ "no-console": "off",
827
+ "no-template-curly-in-string": "off",
828
+ "no-control-regex": "off",
829
+ "prefer-template": "off",
830
+ },
831
+ },
832
+ {
833
+ files: ["**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs"],
834
+ linterOptions: { reportUnusedDisableDirectives: "off" },
835
+ plugins: {
836
+ "simple-import-sort": simpleImportSortPlugin,
837
+ ...(features.react ? { "react-hooks": reactHooksPlugin } : {}),
838
+ i18next: createEslintStub(["no-literal-string"]),
839
+ "oxlint-plugin-restricted": createEslintStub(["restricted-syntax"]),
840
+ "@typescript-eslint": createEslintStub([
841
+ "no-explicit-any",
842
+ "no-unused-vars",
843
+ ]),
844
+ "eslint-plugin-unicorn": createEslintStub([
845
+ "require-module-specifiers",
846
+ ]),
847
+ prettier: createEslintStub(["prettier"]),
848
+ "eslint-plugin-promise": createEslintStub(["no-multiple-resolved"]),
849
+ "eslint-plugin-next": createEslintStub([
850
+ "no-html-link-for-pages",
851
+ "no-img-element",
852
+ "no-assign-module-variable",
853
+ ]),
854
+ "@next/next": createEslintStub(["no-img-element"]),
855
+ eslint: createEslintStub(["no-template-curly-in-string"]),
856
+ "jsx-a11y": createEslintStub(["prefer-tag-over-role"]),
857
+ oxc: createEslintStub(["only-used-in-recursion"]),
858
+ "eslint-plugin-import": createEslintStub(["no-named-as-default"]),
859
+ i18n: createEslintStub(["no-literal-string"]),
860
+ },
861
+ rules: {
862
+ "simple-import-sort/imports": "error",
863
+ "simple-import-sort/exports": "error",
864
+ ...(features.react
865
+ ? {
866
+ "react-hooks/rules-of-hooks": "error",
867
+ "react-hooks/exhaustive-deps": "error",
868
+ }
869
+ : {}),
870
+ "no-unused-vars": "off",
871
+ "no-console": "off",
872
+ "no-template-curly-in-string": "off",
873
+ "no-control-regex": "off",
874
+ "prefer-template": "off",
875
+ },
876
+ },
877
+ ];
878
+
879
+ return cachedEslintFlatConfig;
880
+ },
881
+ };
882
+
883
+ return {
884
+ vibeCheck,
885
+ oxlint,
886
+ prettier,
887
+ eslint,
888
+ typecheck,
889
+ testing,
890
+ vscode,
891
+ };
892
+ };
893
+
894
+ /** ESLint stub plugin for rules handled by oxlint */
895
+ interface EslintStubPlugin {
896
+ rules: Record<
897
+ string,
898
+ {
899
+ create: () => Record<string, never>;
900
+ meta: { docs: { description: string } };
901
+ }
902
+ >;
903
+ }
904
+
905
+ /** Format result for ignore patterns */
906
+ interface IgnoreFormats {
907
+ oxlintIgnores: string[];
908
+ eslintIgnores: string[];
909
+ }
910
+
911
+ /**
912
+ * Creates stub ESLint plugin for rules handled by oxlint.
913
+ *
914
+ * This allows ESLint disable comments (e.g., `// eslint-disable-next-line rule-name`)
915
+ * to work without errors, even though the actual linting is done by oxlint.
916
+ *
917
+ * @param rules - Array of rule names to stub
918
+ * @returns ESLint plugin with no-op rules
919
+ *
920
+ * @example
921
+ * ```typescript
922
+ * const tsStub = createEslintStub(["no-explicit-any", "no-unused-vars"]);
923
+ * // Use in ESLint config:
924
+ * plugins: { "@typescript-eslint": tsStub }
925
+ * ```
926
+ */
927
+ export function createEslintStub(rules: string[]): EslintStubPlugin {
928
+ return {
929
+ rules: Object.fromEntries(
930
+ rules.map((rule) => [
931
+ rule,
932
+ {
933
+ create: (): Record<string, never> => ({}),
934
+ meta: { docs: { description: "Stub - handled by oxlint" } },
935
+ },
936
+ ]),
937
+ ),
938
+ };
939
+ }
940
+
941
+ /**
942
+ * Converts an array of ignore patterns to both oxlint and ESLint formats.
943
+ *
944
+ * Oxlint accepts mixed patterns (directories, files, globs) directly.
945
+ * ESLint flat config requires glob patterns with proper prefixes/suffixes.
946
+ *
947
+ * Pattern conversion rules for ESLint:
948
+ * - Glob patterns (contain `*`): kept as-is
949
+ * - Files with extensions (contain `.` but don't start with `.`): wrapped as `** /filename`
950
+ * - Directories and dotfiles/dotdirs: wrapped as `** /name/**`
951
+ *
952
+ * @param patterns - Array of ignore patterns (directories, files, or globs)
953
+ * @returns Object with patterns formatted for oxlint and eslint
954
+ *
955
+ * @example
956
+ * ```typescript
957
+ * const { oxlintIgnores, eslintIgnores } = formatIgnorePatterns([
958
+ * "dist", // Directory
959
+ * ".next", // Dotdir
960
+ * ".env", // Dotfile
961
+ * "file.min.js", // File
962
+ * "** /test/**", // Glob (already formatted)
963
+ * ]);
964
+ *
965
+ * // oxlintIgnores = ["dist", ".next", ".env", "file.min.js", "** /test/**"]
966
+ * // eslintIgnores = ["** /dist/**", "** /.next/**", "** /.env/**", "** /file.min.js", "** /test/**"]
967
+ * ```
968
+ */
969
+ export function formatIgnorePatterns(patterns: string[]): IgnoreFormats {
970
+ const eslintPatterns = patterns.map((pattern) => {
971
+ // Already a glob pattern - keep as-is
972
+ if (pattern.includes("*")) {
973
+ return pattern;
974
+ }
975
+ // File with extension (but not dotfile) - wrap with **/ prefix only
976
+ if (pattern.includes(".") && !pattern.startsWith(".")) {
977
+ return `**/${pattern}`;
978
+ }
979
+ // Directory or dotfile/dotdir - wrap with **/ prefix and /** suffix
980
+ return `**/${pattern}/**`;
981
+ });
982
+
983
+ return {
984
+ oxlintIgnores: patterns,
985
+ eslintIgnores: eslintPatterns,
986
+ };
987
+ }
988
+
989
+ export default config;