1import "vite-plus/test/config";
2import { defineConfig } from "vite-plus";
3import * as NodeURL from "node:url";
4import { remoteCacheUrl } from "./infra/task-cache/src/remoteCache.ts";
5import { cachedTestTasks } from "./scripts/lib/cached-tests.ts";
6
7const TEST_EXCLUDE = [
8 "**/.repos/**",
9 "**/.t3/**",
10 "**/node_modules/**",
11 "**/dist/**",
12 "**/dist-electron/**",
13 "**/.{idea,git,cache,output,temp}/**",
14];
15
16// Directories whose tests belong to a package's own test config. desktop also
17// owns the kde-snap-shot tests through its `include`.
18const TEST_CONFIG_PACKAGES = [
19 "apps/desktop",
20 "apps/mobile",
21 "apps/server",
22 "apps/web",
23 "packages/client-runtime",
24];
25
26// `vp test run <file>` from the repo root hands each file to its package's
27// config. Packages that borrow this config (`--config ../../vite.config.ts`) run
28// from their own directory, as CI does, so they keep one plain project.
29const isRepoRootTestRun = process.cwd() === import.meta.dirname;
30
31const RESTRICTED_IMPORT_PATHS = [
32 {
33 name: "react-native",
34 importNames: ["FlatList"],
35 message:
36 "Use LegendList from @legendapp/list/react-native, with renderScrollComponent={NativeScrollView} in a screen. Pass whatever renderItem reads through extraData: rows only re-render when their item or extraData changes.",
37 },
38 {
39 name: "@t3tools/client-runtime",
40 message:
41 "Import from an explicit @t3tools/client-runtime/* subpath. The package has no root export.",
42 },
43 {
44 name: "@pierre/diffs/react",
45 importNames: ["CodeView"],
46 message: "Use StyledDiffCodeView so web diff surfaces share styling and virtualized geometry.",
47 },
48 {
49 name: "react-native-screens",
50 importNames: ["ScrollViewMarker"],
51 message:
52 "Use NativeScrollViewMarker or NativeScrollView (src/native/NativeScrollViewMarker.tsx): they carry the header's scroll-edge blur under the Duo's vertical bar.",
53 },
54 {
55 name: "react-native",
56 importNames: ["RefreshControl"],
57 message:
58 "Use RefreshControl from src/components/ui/RefreshControl: Android bottom sheets own the pull at the top, so it steps aside there.",
59 },
60];
61
62const RESTRICTED_UI_VARIANT_PATTERNS = [
63 {
64 group: ["**/components/ui/*", "**/ui/*", "./ui/*"],
65 importNames: ["buttonVariants", "toggleVariants", "badgeVariants", "selectTriggerVariants"],
66 message:
67 "Render the components/ui export instead of borrowing its class recipe (render={<Button …/>}, SelectButton, ToggleGroup).",
68 },
69];
70
71const RESTRICTED_PULL_REQUEST_GLYPH_IMPORTS = {
72 name: "lucide-react",
73 importNames: [
74 "GitMerge",
75 "GitMergeIcon",
76 "GitPullRequest",
77 "GitPullRequestIcon",
78 "GitPullRequestArrow",
79 "GitPullRequestArrowIcon",
80 "GitPullRequestClosed",
81 "GitPullRequestClosedIcon",
82 "GitPullRequestDraft",
83 "GitPullRequestDraftIcon",
84 "GitPullRequestCreate",
85 "GitPullRequestCreateIcon",
86 "GitPullRequestCreateArrow",
87 "GitPullRequestCreateArrowIcon",
88 ],
89 message:
90 "Pick a glyph by meaning from PullRequestGlyph in apps/web/src/components/pullRequest/pullRequestIcons.tsx so every surface draws the same pull request the same way.",
91};
92
93// Vite+ supports remote configuration before its published types include it.
94const cacheUrl = remoteCacheUrl(import.meta.dirname);
95const taskCacheConfig = {
96 scripts: false,
97 ...(cacheUrl ? { remote: { url: cacheUrl } } : {}),
98};
99
100export default defineConfig({
101 run: {
102 cache: taskCacheConfig,
103 tasks: {
104 ...cachedTestTasks(process.env.T3_CACHED_TESTS),
105 // The two knip passes read the same files and write nothing, so they run side by side.
106 // The `knip:check` script runs this task, so `vp run knip:check` and `pnpm run knip:check` both use it.
107 "knip:check:all": {
108 command: 'node -e ""',
109 dependsOn: ["knip:check:files", "knip:check:exports"],
110 cache: false,
111 },
112 },
113 },
114 resolve: {
115 alias: {
116 "~": NodeURL.fileURLToPath(new URL("./apps/web/src", import.meta.url)),
117 },
118 },
119 test: {
120 environment: "node",
121 // Fixture subprocesses must not inherit the outer task runner’s cache policy.
122 env: { VP_REMOTE_CACHE: "off", VP_REMOTE_CACHE_URL: "" },
123 exclude: TEST_EXCLUDE,
124 hookTimeout: 60_000,
125 testTimeout: 60_000,
126 allowOnly: !process.env.CI,
127 // Flaky tests are fixed, not retried.
128 retry: 0,
129 fakeTimers: {
130 toFake: [
131 "setTimeout",
132 "clearTimeout",
133 "setInterval",
134 "clearInterval",
135 "setImmediate",
136 "clearImmediate",
137 "Date",
138 "performance",
139 "requestAnimationFrame",
140 "cancelAnimationFrame",
141 "requestIdleCallback",
142 "cancelIdleCallback",
143 ],
144 },
145 setupFiles: [
146 NodeURL.fileURLToPath(
147 new URL("./packages/shared/src/testing/longTempDir.ts", import.meta.url),
148 ),
149 NodeURL.fileURLToPath(
150 new URL("./packages/shared/src/testing/providerEnv.ts", import.meta.url),
151 ),
152 ],
153 ...(isRepoRootTestRun
154 ? {
155 projects: [
156 ...TEST_CONFIG_PACKAGES,
157 {
158 extends: true,
159 test: {
160 name: "root",
161 exclude: [
162 ...TEST_EXCLUDE,
163 ...TEST_CONFIG_PACKAGES.map((dir) => `${dir}/**`),
164 "native/kde-snap-shot/**",
165 ],
166 },
167 },
168 ],
169 }
170 : {}),
171 },
172 staged: {
173 // In order, so lint reads the formatted file. CI lint failures cost a full round trip.
174 "*": ["vp fmt --no-error-on-unmatched-pattern", "vp lint --no-error-on-unmatched-pattern"],
175 // knip checks the whole repo, so it takes no file list. Any code or manifest change can leave
176 // a file, export, or dependency unused.
177 "{*.{js,jsx,ts,tsx,mjs,cjs,mts,cts},package.json,knip.jsonc}": () => "vp run knip:check",
178 },
179 fmt: {
180 ignorePatterns: [
181 ".repos/**",
182 "infra/task-cache/vendor/**",
183 // Macroscope requires one glob per line; Markdown formatting corrupts it.
184 ".macroscope/ignore.md",
185 ".alchemy",
186 "dist",
187 "dist-electron",
188 "node_modules",
189 "pnpm-lock.yaml",
190 "*.tsbuildinfo",
191 "**/routeTree.gen.ts",
192 "**/*.graphql.ts",
193 "packages/source-control-github/schema/**",
194 "apps/server/schema/**",
195 "apps/mobile/android/**",
196 "apps/mobile/ios/**",
197 "apps/mobile/uniwind-types.d.ts",
198 // Vendored and generated C.
199 "apps/mobile/modules/t3-highlights/Vendor/**",
200 "apps/mobile/modules/t3-highlights/cpp/generated/**",
201 "apps/mobile/modules/t3-highlights/nitrogen/generated/**",
202 "*.icon/**",
203 ],
204 sortPackageJson: {},
205 overrides: [
206 {
207 files: [".devcontainer/devcontainer.json"],
208 options: {
209 trailingComma: "none",
210 },
211 },
212 ],
213 },
214 lint: {
215 ignorePatterns: [
216 ".repos",
217 ".repos/**",
218 "infra/task-cache/vendor/**",
219 "dist",
220 "dist-electron",
221 "node_modules",
222 "pnpm-lock.yaml",
223 "*.tsbuildinfo",
224 "**/routeTree.gen.ts",
225 "**/*.graphql.ts",
226 "packages/source-control-github/schema/**",
227 "apps/server/schema/**",
228 "apps/mobile/android/**",
229 "apps/mobile/ios/**",
230 "apps/mobile/uniwind-types.d.ts",
231 ],
232 plugins: ["eslint", "oxc", "react", "unicorn", "typescript"],
233 jsPlugins: ["./oxlint-plugin-t3code/index.ts", "@shadcn/lint"],
234 settings: {
235 shadcn: { ui: "~/components/ui" },
236 },
237 categories: {
238 correctness: "warn",
239 suspicious: "warn",
240 perf: "warn",
241 },
242 rules: {
243 "oxc/no-accumulating-spread": "error",
244 "eslint/no-control-regex": "off",
245 "typescript/no-this-alias": "off",
246 "unicorn/prefer-add-event-listener": "off",
247 "unicorn/prefer-string-starts-ends-with": "off",
248 "unicorn/prefer-set-has": "off",
249 "unicorn/prefer-array-find": "off",
250 "unicorn/no-new-array": "off",
251 "unicorn/no-array-reverse": "off",
252 "react/no-object-type-as-default-prop": "off",
253 "react/jsx-no-constructed-context-values": "off",
254 "react/memo-dependencies": "error",
255 "react/purity": "error",
256 "react/refs": "error",
257 "react/exhaustive-effect-dependencies": "error",
258 "react/set-state-in-effect": "error",
259 "react/immutability": "error",
260 "react/no-array-index-key": "error",
261 "react/no-unstable-nested-components": "error",
262 "react/static-components": "error",
263 "react/capitalized-calls": "error",
264 "typescript/no-extraneous-class": "error",
265 "eslint/no-empty-pattern": "error",
266 "eslint/no-new": "error",
267 "unicorn/no-useless-spread": "error",
268 "eslint/no-unused-vars": [
269 "error",
270 { argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" },
271 ],
272 "eslint/no-unsafe-optional-chaining": "error",
273 "eslint/no-unneeded-ternary": "error",
274 "unicorn/no-useless-fallback-in-spread": "error",
275 "unicorn/require-module-specifiers": "error",
276 "unicorn/no-array-sort": "off",
277 "unicorn/consistent-function-scoping": "off",
278 "oxc/no-map-spread": "off",
279 "react-in-jsx-scope": "off",
280 "react-hooks/exhaustive-deps": "off",
281 "eslint/no-shadow": "off",
282 "eslint/no-await-in-loop": "off",
283 "eslint/no-underscore-dangle": "off",
284 "typescript/consistent-return": "off",
285 "typescript/no-base-to-string": "off",
286 "typescript/no-duplicate-type-constituents": "off",
287 "typescript/no-floating-promises": "off",
288 "typescript/no-implied-eval": "off",
289 "typescript/no-meaningless-void-operator": "off",
290 "typescript/no-redundant-type-constituents": "off",
291 "typescript/no-unnecessary-boolean-literal-compare": "off",
292 "typescript/no-unnecessary-type-conversion": "off",
293 "typescript/no-unnecessary-type-arguments": "off",
294 "typescript/no-unnecessary-type-assertion": "off",
295 "typescript/no-unnecessary-type-parameters": "off",
296 "typescript/no-unsafe-type-assertion": "off",
297 "typescript/await-thenable": "off",
298 "typescript/require-array-sort-compare": "off",
299 "typescript/restrict-template-expressions": "off",
300 "typescript/unbound-method": "off",
301 "eslint/no-restricted-imports": [
302 "error",
303 { paths: [...RESTRICTED_IMPORT_PATHS, RESTRICTED_PULL_REQUEST_GLYPH_IMPORTS] },
304 ],
305 "t3code/no-global-process-runtime": "error",
306 "t3code/no-inline-schema-compile": "error",
307 "t3code/no-low-value-comments": "error",
308 "t3code/no-manual-effect-runtime-in-tests": "error",
309 "t3code/no-native-title-tooltip": "error",
310 // Test files only (the rule checks the filename).
311 "t3code/no-polling-loop-in-tests": "error",
312 "t3code/no-raw-mcp-registration": "error",
313 "t3code/no-real-time-wait-in-tests": "error",
314 "t3code/no-test-in-loop": "error",
315 "t3code/no-test-timeout-or-unreasoned-skip": "error",
316 "t3code/no-rpc-permission-bypass": ["error", { allowRawClientAccess: true }],
317 "t3code/no-unscoped-has": "error",
318 "t3code/no-wall-clock-in-tests": "error",
319 "t3code/namespace-node-imports": "error",
320 "t3code/prefer-catch-tags": "error",
321 "t3code/prefer-effect-fn": "error",
322 "t3code/require-centered-scroll-gutter": "error",
323 "t3code/require-suppression-reason": "error",
324 },
325 overrides: [
326 {
327 files: [
328 "**/*.{test,spec,testkit}.{ts,tsx}",
329 "**/{test,tests,testkit,testing,fixtures,integration}/**",
330 "packages/provider-testing/**",
331 ],
332 rules: { "t3code/prefer-effect-fn": "off" },
333 },
334 {
335 files: ["apps/web/src/**", "apps/mobile/src/**", "apps/desktop/src/**"],
336 // Users still pick their own project icon from the full Lucide set.
337 excludeFiles: ["apps/web/src/projectIconOptions.ts"],
338 rules: { "t3code/no-sparkles-icon": "error" },
339 },
340 {
341 files: ["apps/web/**/*.test.{ts,tsx}"],
342 rules: { "t3code/require-disposable-render": "error" },
343 },
344 {
345 files: ["apps/web/src/**/*.browser.test.{ts,tsx}"],
346 rules: { "t3code/no-frame-waits-in-browser-tests": "error" },
347 },
348 {
349 files: ["apps/**/src/**", "packages/**/src/**"],
350 rules: { "t3code/no-dom-test-environment": "error" },
351 },
352 {
353 files: ["packages/client-runtime/src/state/**", "apps/{web,mobile,desktop}/src/**"],
354 rules: { "t3code/no-rpc-permission-bypass": ["error", { allowRawClientAccess: false }] },
355 },
356 {
357 files: [
358 "packages/client-runtime/src/state/runtime.ts",
359 "packages/client-runtime/src/state/vcsAction.ts",
360 ],
361 rules: {
362 "t3code/no-rpc-permission-bypass": [
363 "error",
364 { allowGuardInstallation: true, allowRawClientAccess: false },
365 ],
366 },
367 },
368 {
369 files: [
370 "apps/web/src/components/settings/AuthorizedClientsSettings.tsx",
371 "apps/mobile/src/features/updates/app-updates.ts",
372 "apps/web/src/components/device/DevicePhoneViewport.tsx",
373 "apps/web/src/components/device/DeviceDuoViewport.tsx",
374 ],
375 rules: { "t3code/no-rpc-permission-bypass": ["error", { allowRawClientAccess: true }] },
376 },
377 {
378 files: ["packages/client-runtime/src/connection/outdatedHostUpdate.ts"],
379 rules: { "t3code/no-rpc-permission-bypass": "off" },
380 },
381 {
382 files: [
383 "packages/client-runtime/src/rpc/**",
384 "**/*.{test,spec}.{ts,tsx,js,jsx,mts,cts,mjs,cjs}",
385 ],
386 rules: { "t3code/no-rpc-permission-bypass": "off" },
387 },
388 {
389 files: ["packages/shared/src/HostProcess.ts"],
390 rules: { "t3code/no-global-process-runtime": "off" },
391 },
392 {
393 // The `t3` npm launcher is published as plain, dependency-free Node.
394 files: ["scripts/npm-launcher/**"],
395 rules: {
396 "t3code/no-global-process-runtime": "off",
397 "t3code/namespace-node-imports": "off",
398 },
399 },
400 {
401 files: ["apps/server/src/mcp/McpHttpServer.ts"],
402 rules: { "t3code/no-raw-mcp-registration": "off" },
403 },
404 {
405 files: ["apps/web/src/**"],
406 excludeFiles: ["apps/web/src/components/ui/**"],
407 rules: {
408 "eslint/no-restricted-imports": [
409 "error",
410 {
411 paths: [...RESTRICTED_IMPORT_PATHS, RESTRICTED_PULL_REQUEST_GLYPH_IMPORTS],
412 patterns: RESTRICTED_UI_VARIANT_PATTERNS,
413 },
414 ],
415 },
416 },
417 {
418 files: ["apps/web/src/components/pullRequest/pullRequestIcons.tsx"],
419 rules: { "eslint/no-restricted-imports": ["error", { paths: RESTRICTED_IMPORT_PATHS }] },
420 },
421 {
422 // The server and every client load a forge's shared definition, so a UI runtime
423 // imported there would break the server; client-only forge code goes in `src/client/`.
424 files: ["packages/source-control-*/src/shared/**"],
425 rules: {
426 "eslint/no-restricted-imports": [
427 "error",
428 {
429 paths: [...RESTRICTED_IMPORT_PATHS, RESTRICTED_PULL_REQUEST_GLYPH_IMPORTS],
430 patterns: [
431 {
432 group: [
433 "react",
434 "react/*",
435 "react-*",
436 "expo",
437 "expo-*",
438 "lucide-react",
439 "@t3tools/client-runtime",
440 "@t3tools/client-runtime/*",
441 "../client/*",
442 "../server/*",
443 "../../client/*",
444 "../../server/*",
445 "../../../client/*",
446 "../../../server/*",
447 "@t3tools/source-control-*/client/*",
448 "@t3tools/source-control-*/server/*",
449 ],
450 message:
451 "Shared forge definitions load on the server and every client; keep UI and server code out.",
452 },
453 ],
454 },
455 ],
456 },
457 },
458 {
459 files: ["apps/server/src/**", "packages/source-control-*/src/server/**"],
460 rules: {
461 "eslint/no-restricted-imports": [
462 "error",
463 {
464 paths: [...RESTRICTED_IMPORT_PATHS, RESTRICTED_PULL_REQUEST_GLYPH_IMPORTS],
465 patterns: [
466 {
467 group: [
468 "@t3tools/source-control-*/client/*",
469 "../client/*",
470 "../../client/*",
471 "../../../client/*",
472 ],
473 message:
474 "A forge's client entry is UI-only; the server reads its `shared/definition`.",
475 },
476 ],
477 },
478 ],
479 },
480 },
481 {
482 files: ["apps/mobile/src/**"],
483 rules: {
484 "t3code/no-mobile-uniwind-theme-escape-hatches": "error",
485 "t3code/require-registered-list-scroll": "error",
486 },
487 },
488 {
489 files: ["apps/mobile/src/**"],
490 excludeFiles: [
491 "apps/mobile/src/components/ui/AppSymbol.tsx",
492 "apps/mobile/src/components/ui/AppSymbol.android.tsx",
493 "apps/mobile/src/features/pull-requests/pullRequestGlyphs.ts",
494 ],
495 rules: { "t3code/no-mobile-pull-request-glyph-literals": "error" },
496 },
497 {
498 files: ["apps/mobile/src/**"],
499 excludeFiles: ["**/*.test.{ts,tsx}"],
500 rules: { "t3code/no-ambiguous-detail-navigation": "error" },
501 },
502 {
503 // Raw ResizeObserver state commits miss the paint; observeResize flushes derived layout together.
504 files: ["apps/web/src/**"],
505 excludeFiles: [
506 "apps/web/src/lib/observeResize.ts",
507 "**/*.{test,spec}.{ts,tsx,js,jsx,mts,cts,mjs,cjs}",
508 ],
509 rules: { "t3code/no-raw-resize-observer": "error" },
510 },
511 {
512 files: ["apps/web/src/**"],
513 rules: { "shadcn/no-unknown-classes": "error" },
514 },
515 {
516 files: ["apps/web/src/**"],
517 rules: { "t3code/no-outset-state-indicators": "error" },
518 },
519 {
520 files: ["apps/web/src/**"],
521 rules: { "t3code/max-settings-description-length": "error" },
522 },
523 {
524 files: ["apps/*/src/**", "packages/*/src/**"],
525 excludeFiles: [
526 "**/*.{test,spec}.{ts,tsx,js,jsx,mts,cts,mjs,cjs}",
527 "**/testkit/**",
528 "**/_generated/**",
529 // Real user quotes stay verbatim.
530 "apps/marketing/src/lib/tweets.ts",
531 ],
532 rules: { "t3code/no-em-dash-copy": "error" },
533 },
534 {
535 files: ["apps/web/src/**"],
536 rules: { "shadcn/no-raw-colors": "error" },
537 },
538 {
539 // Third-party marks must retain their exact colors.
540 files: ["apps/web/src/components/Icons.tsx", "apps/web/src/components/JetBrainsIcons.tsx"],
541 rules: { "shadcn/no-raw-colors": "off" },
542 },
543 {
544 files: ["apps/web/src/**"],
545 excludeFiles: ["apps/web/src/components/ui/**"],
546 rules: {
547 "shadcn/require-static-classes": "error",
548 "shadcn/no-arbitrary-values": [
549 "error",
550 {
551 allow: [
552 "layout",
553 "transition",
554 "rounded-[inherit]",
555 "gap-[0.33em]",
556 "gap-[0.2em]",
557 "leading-[normal]",
558 "underline-offset-[0.2em]",
559 "rounded-[25%]",
560 "text-[length:80cqh]",
561 "bg-[Highlight]",
562 "fill-[#26251E]",
563 "fill-[#EDECEC]",
564 "fill-[#0F0F0F]",
565 "fill-[#F5F5F5]",
566 "fill-[#d97757]",
567 "text-[#d97757]",
568 ],
569 },
570 ],
571 "shadcn/no-restyle": [
572 "error",
573 {
574 allow: ["layout"],
575 contracts: [
576 {
577 // CollapsibleTrigger has no styled counterpart; other triggers use render={<Button …/>}.
578 pattern: "^CollapsibleTrigger$",
579 allow: ["layout", "color", "typography", "spacing", "shape", "effects", "motion"],
580 },
581 ],
582 },
583 ],
584 },
585 },
586 {
587 // React Compiler owns memoization; upstream rules do not ban redundant useMemo/useCallback.
588 files: ["apps/web/src/**/*.{ts,tsx}"],
589 rules: {
590 "react/exhaustive-effect-dependencies": "error",
591 "react/globals": "error",
592 "react/hooks": "error",
593 "react/immutability": "error",
594 // The compiler memoizes context values; this rule would ask for useMemo.
595 "react/jsx-no-constructed-context-values": "off",
596 "react/memo-dependencies": "error",
597 "react/preserve-manual-memoization": "error",
598 // Render-time clock reads use @t3tools/client-runtime/clock or state seeded with the
599 // time; any other read carries a suppression that says why. oxlint also flags time reads
600 // inside event handlers (oxc#27309); those suppressions cite it.
601 "react/purity": "error",
602 "react/refs": "error",
603 "react/set-state-in-effect": "error",
604 "react/set-state-in-render": "error",
605 "react/static-components": "error",
606 "react/unsupported-syntax": "error",
607 "t3code/no-manual-memo": "error",
608 },
609 },
610 {
611 // ESNext types allow APIs absent from Hermes; restrict shared client code, exempting Node tests.
612 files: [
613 "apps/mobile/src/**",
614 "packages/client-runtime/src/**",
615 "packages/contracts/src/**",
616 "packages/shared/src/**",
617 ],
618 excludeFiles: ["**/*.test.ts", "**/*.test.tsx"],
619 rules: { "t3code/no-hermes-unsupported-apis": "error" },
620 },
621 {
622 // Directives hide bailouts from mobile's compiler-coverage check; retain hooks until its allowlist is empty.
623 files: ["apps/mobile/src/**/*.{ts,tsx}"],
624 rules: { "t3code/no-manual-memo": ["error", { allowMemoHooks: true }] },
625 },
626 {
627 files: ["apps/web/src/**", "apps/mobile/src/**", "packages/*/src/**"],
628 rules: { "t3code/no-unreasoned-state-sync": "error" },
629 },
630 {
631 // React Compiler caches use* functions only if they call a hook.
632 files: ["apps/web/src/**/*.{ts,tsx}", "apps/mobile/src/**/*.{ts,tsx}"],
633 excludeFiles: ["**/*.test.ts", "**/*.test.tsx"],
634 rules: { "t3code/no-hookless-use-function": "error" },
635 },
636 {
637 files: [
638 "apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx",
639 "apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx",
640 "apps/mobile/src/features/files/FileMarkdownPreview.tsx",
641 "apps/mobile/src/features/files/SourceFileSurface.tsx",
642 "apps/mobile/src/features/files/AttachmentFileScreen.tsx",
643 "apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx",
644 "apps/mobile/src/features/files/thread-file-navigator-pane.tsx",
645 "apps/mobile/src/features/layout/nested-stacks.tsx",
646 "apps/mobile/src/features/review/ReviewSheet.tsx",
647 "apps/mobile/src/features/review/useNativeReviewDiffBridge.ts",
648 "apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx",
649 "apps/mobile/src/features/threads/ThreadComposer.tsx",
650 "apps/mobile/src/features/threads/ThreadFeed.tsx",
651 "apps/mobile/src/features/threads/ThreadQueueControl.tsx",
652 "apps/mobile/src/features/review/ReviewCommentCard.tsx",
653 "apps/mobile/src/features/threads/thread-list-v2-items.tsx",
654 "apps/mobile/src/components/ui/SecretTextInput.android.tsx",
655 "apps/mobile/src/lib/useMobileNavigationTheme.ts",
656 "apps/mobile/src/native/T3ComposerEditor.ios.tsx",
657 "apps/mobile/src/native/T3ComposerEditor.native.tsx",
658 "apps/mobile/src/native/SelectableMarkdownText.tsx",
659 "apps/mobile/src/native/useEnrichedLinkVariants.ts",
660 ],
661 rules: {
662 "t3code/no-mobile-uniwind-theme-escape-hatches": ["error", { allowUniwindTheme: true }],
663 },
664 },
665 ],
666 options: {
667 reportUnusedDisableDirectives: "error",
668 typeAware: false,
669 typeCheck: false,
670 },
671 },
672});