Skip to content
Source
Sign inSign up

vite.config.ts

971 lines43.4 KB

1// @effect-diagnostics nodeBuiltinImport:off - Config load checks the debt-ceiling files exist before any Effect runtime exists.
2import "vite-plus/test/config";
3import { defineConfig } from "vite-plus";
4import * as NodeFS from "node:fs";
5import * as NodeURL from "node:url";
6import { remoteCacheUrl } from "./infra/task-cache/src/remoteCache.ts";
7import { cachedTestTasks } from "./scripts/lib/cached-tests.ts";
8​
9const TEST_EXCLUDE = [
10 "**/.repos/**",
11 "**/.t3/**",
12 "**/node_modules/**",
13 "**/dist/**",
14 "**/dist-electron/**",
15 "**/.{idea,git,cache,output,temp}/**",
16];
17​
18// Directories whose tests belong to a package's own test config. desktop also
19// owns the kde-snap-shot tests through its `include`.
20const TEST_CONFIG_PACKAGES = [
21 "apps/desktop",
22 "apps/mobile",
23 "apps/server",
24 "apps/web",
25 "packages/client-runtime",
26];
27​
28// `vp test run <file>` from the repo root hands each file to its package's
29// config. Packages that borrow this config (`--config ../../vite.config.ts`) run
30// from their own directory, as CI does, so they keep one plain project.
31const isRepoRootTestRun = process.cwd() === import.meta.dirname;
32​
33/** Debt ceilings for files that still exist, so a branch deleting a file can't break the config. */
34const existingCeilings = (ceilings: Record<string, number>) =>
35 Object.entries(ceilings).filter(([file]) =>
36 NodeFS.existsSync(NodeURL.fileURLToPath(new URL(file, import.meta.url))),
37 );
38​
39const RESTRICTED_IMPORT_PATHS = [
40 {
41 name: "react-native",
42 importNames: ["FlatList"],
43 message:
44 "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.",
45 },
46 {
47 name: "@t3tools/client-runtime",
48 message:
49 "Import from an explicit @t3tools/client-runtime/* subpath. The package has no root export.",
50 },
51 {
52 name: "@pierre/diffs/react",
53 importNames: ["CodeView"],
54 message: "Use StyledDiffCodeView so web diff surfaces share styling and virtualized geometry.",
55 },
56 {
57 name: "react-native-screens",
58 importNames: ["ScrollViewMarker"],
59 message:
60 "Use NativeScrollViewMarker or NativeScrollView (src/native/NativeScrollViewMarker.tsx): they carry the header's scroll-edge blur under the Duo's vertical bar.",
61 },
62];
63​
64const RESTRICTED_UI_VARIANT_PATTERNS = [
65 {
66 group: ["**/components/ui/*", "**/ui/*", "./ui/*"],
67 importNames: ["buttonVariants", "toggleVariants", "badgeVariants", "selectTriggerVariants"],
68 message:
69 "Render the components/ui export instead of borrowing its class recipe (render={<Button …/>}, SelectButton, ToggleGroup).",
70 },
71];
72​
73const RESTRICTED_PULL_REQUEST_GLYPH_IMPORTS = {
74 name: "lucide-react",
75 importNames: [
76 "GitMerge",
77 "GitMergeIcon",
78 "GitPullRequest",
79 "GitPullRequestIcon",
80 "GitPullRequestArrow",
81 "GitPullRequestArrowIcon",
82 "GitPullRequestClosed",
83 "GitPullRequestClosedIcon",
84 "GitPullRequestDraft",
85 "GitPullRequestDraftIcon",
86 "GitPullRequestCreate",
87 "GitPullRequestCreateIcon",
88 "GitPullRequestCreateArrow",
89 "GitPullRequestCreateArrowIcon",
90 ],
91 message:
92 "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.",
93};
94​
95// Vite+ supports remote configuration before its published types include it.
96const cacheUrl = remoteCacheUrl(import.meta.dirname);
97const taskCacheConfig = {
98 scripts: false,
99 ...(cacheUrl ? { remote: { url: cacheUrl } } : {}),
100};
101​
102export default defineConfig({
103 run: {
104 cache: taskCacheConfig,
105 tasks: {
106 ...cachedTestTasks(process.env.T3_CACHED_TESTS),
107 // The two knip passes read the same files and write nothing, so they run side by side.
108 // The `knip:check` script runs this task, so `vp run knip:check` and `pnpm run knip:check` both use it.
109 "knip:check:all": {
110 command: 'node -e ""',
111 dependsOn: ["knip:check:files", "knip:check:exports"],
112 cache: false,
113 },
114 },
115 },
116 resolve: {
117 alias: {
118 "~": NodeURL.fileURLToPath(new URL("./apps/web/src", import.meta.url)),
119 },
120 },
121 test: {
122 environment: "node",
123 // Fixture subprocesses must not inherit the outer task runner’s cache policy.
124 env: { VP_REMOTE_CACHE: "off", VP_REMOTE_CACHE_URL: "" },
125 exclude: TEST_EXCLUDE,
126 hookTimeout: 60_000,
127 testTimeout: 60_000,
128 allowOnly: !process.env.CI,
129 // Flaky tests are fixed, not retried.
130 retry: 0,
131 fakeTimers: {
132 toFake: [
133 "setTimeout",
134 "clearTimeout",
135 "setInterval",
136 "clearInterval",
137 "setImmediate",
138 "clearImmediate",
139 "Date",
140 "performance",
141 "requestAnimationFrame",
142 "cancelAnimationFrame",
143 "requestIdleCallback",
144 "cancelIdleCallback",
145 ],
146 },
147 setupFiles: [
148 NodeURL.fileURLToPath(
149 new URL("./packages/shared/src/testing/longTempDir.ts", import.meta.url),
150 ),
151 NodeURL.fileURLToPath(
152 new URL("./packages/shared/src/testing/providerEnv.ts", import.meta.url),
153 ),
154 ],
155 ...(isRepoRootTestRun
156 ? {
157 projects: [
158 ...TEST_CONFIG_PACKAGES,
159 {
160 extends: true,
161 test: {
162 name: "root",
163 exclude: [
164 ...TEST_EXCLUDE,
165 ...TEST_CONFIG_PACKAGES.map((dir) => `${dir}/**`),
166 "native/kde-snap-shot/**",
167 ],
168 },
169 },
170 ],
171 }
172 : {}),
173 },
174 staged: {
175 // In order, so lint reads the formatted file. CI lint failures cost a full round trip.
176 "*": ["vp fmt --no-error-on-unmatched-pattern", "vp lint --no-error-on-unmatched-pattern"],
177 // knip checks the whole repo, so it takes no file list. Any code or manifest change can leave
178 // a file, export, or dependency unused.
179 "{*.{js,jsx,ts,tsx,mjs,cjs,mts,cts},package.json,knip.jsonc}": () => "vp run knip:check",
180 },
181 fmt: {
182 ignorePatterns: [
183 ".repos/**",
184 "infra/task-cache/vendor/**",
185 // Macroscope requires one glob per line; Markdown formatting corrupts it.
186 ".macroscope/ignore.md",
187 ".alchemy",
188 "dist",
189 "dist-electron",
190 "node_modules",
191 "pnpm-lock.yaml",
192 "*.tsbuildinfo",
193 "**/routeTree.gen.ts",
194 "**/*.graphql.ts",
195 "packages/source-control-github/schema/**",
196 "apps/server/schema/**",
197 "apps/mobile/android/**",
198 "apps/mobile/ios/**",
199 "apps/mobile/uniwind-types.d.ts",
200 // Vendored and generated C.
201 "apps/mobile/modules/t3-highlights/Vendor/**",
202 "apps/mobile/modules/t3-highlights/cpp/generated/**",
203 "apps/mobile/modules/t3-highlights/nitrogen/generated/**",
204 "*.icon/**",
205 ],
206 sortPackageJson: {},
207 overrides: [
208 {
209 files: [".devcontainer/devcontainer.json"],
210 options: {
211 trailingComma: "none",
212 },
213 },
214 ],
215 },
216 lint: {
217 ignorePatterns: [
218 ".repos",
219 ".repos/**",
220 "infra/task-cache/vendor/**",
221 "dist",
222 "dist-electron",
223 "node_modules",
224 "pnpm-lock.yaml",
225 "*.tsbuildinfo",
226 "**/routeTree.gen.ts",
227 "**/*.graphql.ts",
228 "packages/source-control-github/schema/**",
229 "apps/server/schema/**",
230 "apps/mobile/android/**",
231 "apps/mobile/ios/**",
232 "apps/mobile/uniwind-types.d.ts",
233 ],
234 plugins: ["eslint", "oxc", "react", "unicorn", "typescript"],
235 jsPlugins: ["./oxlint-plugin-t3code/index.ts", "@shadcn/lint"],
236 settings: {
237 shadcn: { ui: "~/components/ui" },
238 },
239 categories: {
240 correctness: "warn",
241 suspicious: "warn",
242 perf: "warn",
243 },
244 rules: {
245 "oxc/no-accumulating-spread": "error",
246 "eslint/no-control-regex": "off",
247 "typescript/no-this-alias": "off",
248 "unicorn/prefer-add-event-listener": "off",
249 "unicorn/prefer-string-starts-ends-with": "off",
250 "unicorn/prefer-set-has": "off",
251 "unicorn/prefer-array-find": "off",
252 "unicorn/no-new-array": "off",
253 "unicorn/no-array-reverse": "off",
254 "react/no-object-type-as-default-prop": "off",
255 "react/jsx-no-constructed-context-values": "off",
256 "react/memo-dependencies": "error",
257 "react/purity": "error",
258 "react/refs": "error",
259 "react/exhaustive-effect-dependencies": "error",
260 "react/set-state-in-effect": "error",
261 "react/immutability": "error",
262 "react/no-array-index-key": "error",
263 "react/no-unstable-nested-components": "error",
264 "react/static-components": "error",
265 "react/capitalized-calls": "error",
266 "typescript/no-extraneous-class": "error",
267 "eslint/no-empty-pattern": "error",
268 "eslint/no-new": "error",
269 "unicorn/no-useless-spread": "error",
270 "eslint/no-unused-vars": [
271 "error",
272 { argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" },
273 ],
274 "eslint/no-unsafe-optional-chaining": "error",
275 "eslint/no-unneeded-ternary": "error",
276 "unicorn/no-useless-fallback-in-spread": "error",
277 "unicorn/require-module-specifiers": "error",
278 "unicorn/no-array-sort": "off",
279 "unicorn/consistent-function-scoping": "off",
280 "oxc/no-map-spread": "off",
281 "react-in-jsx-scope": "off",
282 "react-hooks/exhaustive-deps": "off",
283 "eslint/no-shadow": "off",
284 "eslint/no-await-in-loop": "off",
285 "eslint/no-underscore-dangle": "off",
286 "typescript/consistent-return": "off",
287 "typescript/no-base-to-string": "off",
288 "typescript/no-duplicate-type-constituents": "off",
289 "typescript/no-floating-promises": "off",
290 "typescript/no-implied-eval": "off",
291 "typescript/no-meaningless-void-operator": "off",
292 "typescript/no-redundant-type-constituents": "off",
293 "typescript/no-unnecessary-boolean-literal-compare": "off",
294 "typescript/no-unnecessary-type-conversion": "off",
295 "typescript/no-unnecessary-type-arguments": "off",
296 "typescript/no-unnecessary-type-assertion": "off",
297 "typescript/no-unnecessary-type-parameters": "off",
298 "typescript/no-unsafe-type-assertion": "off",
299 "typescript/await-thenable": "off",
300 "typescript/require-array-sort-compare": "off",
301 "typescript/restrict-template-expressions": "off",
302 "typescript/unbound-method": "off",
303 "eslint/no-restricted-imports": [
304 "error",
305 { paths: [...RESTRICTED_IMPORT_PATHS, RESTRICTED_PULL_REQUEST_GLYPH_IMPORTS] },
306 ],
307 "t3code/no-global-process-runtime": "error",
308 "t3code/no-inline-schema-compile": "error",
309 "t3code/no-low-value-comments": "error",
310 "t3code/no-manual-effect-runtime-in-tests": "error",
311 "t3code/no-native-title-tooltip": "error",
312 // Test files only (the rule checks the filename).
313 "t3code/no-polling-loop-in-tests": "error",
314 "t3code/no-raw-mcp-registration": "error",
315 "t3code/no-real-time-wait-in-tests": "error",
316 "t3code/no-test-in-loop": "error",
317 "t3code/no-test-timeout-or-unreasoned-skip": "error",
318 "t3code/no-rpc-permission-bypass": ["error", { allowRawClientAccess: true }],
319 "t3code/no-unscoped-has": "error",
320 "t3code/no-wall-clock-in-tests": "error",
321 "t3code/namespace-node-imports": "error",
322 "t3code/prefer-catch-tags": "error",
323 "t3code/prefer-effect-fn": "error",
324 "t3code/require-centered-scroll-gutter": "error",
325 "t3code/require-suppression-reason": "error",
326 },
327 overrides: [
328 {
329 files: [
330 "**/*.{test,spec,testkit}.{ts,tsx}",
331 "**/{test,tests,testkit,testing,fixtures,integration}/**",
332 "packages/provider-testing/**",
333 ],
334 rules: { "t3code/prefer-effect-fn": "off" },
335 },
336 {
337 files: ["apps/web/src/**", "apps/mobile/src/**", "apps/desktop/src/**"],
338 // Users still pick their own project icon from the full Lucide set.
339 excludeFiles: ["apps/web/src/projectIconOptions.ts"],
340 rules: { "t3code/no-sparkles-icon": "error" },
341 },
342 {
343 files: ["apps/web/**/*.test.{ts,tsx}"],
344 rules: { "t3code/require-disposable-render": "error" },
345 },
346 {
347 files: ["apps/web/src/**/*.browser.test.{ts,tsx}"],
348 rules: { "t3code/no-frame-waits-in-browser-tests": "error" },
349 },
350 {
351 files: ["apps/**/src/**", "packages/**/src/**"],
352 rules: { "t3code/no-dom-test-environment": "error" },
353 },
354 {
355 files: ["apps/web/src/**", "apps/mobile/src/**"],
356 rules: { "t3code/no-spaced-caps": "error" },
357 },
358 {
359 files: ["apps/web/src/**"],
360 rules: { "t3code/no-dot-separated-text": "error" },
361 },
362 {
363 // A <button>, <a> or <input> with appearance classes is a hand-built control that
364 // skips the components/ui variants; shadcn/no-restyle cannot see it because it only
365 // checks ui components. components/ui builds the primitives from raw elements.
366 files: ["apps/web/src/**"],
367 excludeFiles: ["apps/web/src/components/ui/**"],
368 rules: { "t3code/no-styled-raw-controls": "error" },
369 },
370 {
371 // Type comes from the seven roles (text-title … text-micro, or <Text>), not
372 // Tailwind's size and weight scale. components/ui defines the roles' users,
373 // tests assert on markup, and the catalog demos raw classes on purpose.
374 files: ["apps/web/src/**"],
375 excludeFiles: [
376 "apps/web/src/components/ui/**",
377 "apps/web/src/components/design-system/**",
378 "apps/web/src/**/*.test.{ts,tsx}",
379 ],
380 rules: { "t3code/no-raw-type-classes": "error" },
381 },
382 {
383 files: ["packages/client-runtime/src/state/**", "apps/{web,mobile,desktop}/src/**"],
384 rules: { "t3code/no-rpc-permission-bypass": ["error", { allowRawClientAccess: false }] },
385 },
386 {
387 files: [
388 "packages/client-runtime/src/state/runtime.ts",
389 "packages/client-runtime/src/state/vcsAction.ts",
390 ],
391 rules: {
392 "t3code/no-rpc-permission-bypass": [
393 "error",
394 { allowGuardInstallation: true, allowRawClientAccess: false },
395 ],
396 },
397 },
398 {
399 files: [
400 "apps/web/src/components/settings/AuthorizedClientsSettings.tsx",
401 "apps/mobile/src/features/updates/app-updates.ts",
402 "apps/web/src/components/device/DevicePhoneViewport.tsx",
403 "apps/web/src/components/device/DeviceDuoViewport.tsx",
404 ],
405 rules: { "t3code/no-rpc-permission-bypass": ["error", { allowRawClientAccess: true }] },
406 },
407 {
408 files: ["packages/client-runtime/src/connection/outdatedHostUpdate.ts"],
409 rules: { "t3code/no-rpc-permission-bypass": "off" },
410 },
411 {
412 files: [
413 "packages/client-runtime/src/rpc/**",
414 "**/*.{test,spec}.{ts,tsx,js,jsx,mts,cts,mjs,cjs}",
415 ],
416 rules: { "t3code/no-rpc-permission-bypass": "off" },
417 },
418 {
419 files: ["packages/shared/src/HostProcess.ts"],
420 rules: { "t3code/no-global-process-runtime": "off" },
421 },
422 {
423 // The `t3` npm launcher is published as plain, dependency-free Node.
424 files: ["scripts/npm-launcher/**"],
425 rules: {
426 "t3code/no-global-process-runtime": "off",
427 "t3code/namespace-node-imports": "off",
428 },
429 },
430 {
431 files: ["apps/server/src/mcp/McpHttpServer.ts"],
432 rules: { "t3code/no-raw-mcp-registration": "off" },
433 },
434 {
435 files: ["apps/web/src/**"],
436 excludeFiles: ["apps/web/src/components/ui/**"],
437 rules: {
438 "eslint/no-restricted-imports": [
439 "error",
440 {
441 paths: [...RESTRICTED_IMPORT_PATHS, RESTRICTED_PULL_REQUEST_GLYPH_IMPORTS],
442 patterns: RESTRICTED_UI_VARIANT_PATTERNS,
443 },
444 ],
445 },
446 },
447 {
448 files: ["apps/web/src/components/pullRequest/pullRequestIcons.tsx"],
449 rules: { "eslint/no-restricted-imports": ["error", { paths: RESTRICTED_IMPORT_PATHS }] },
450 },
451 {
452 // The server and every client load a forge's shared definition, so a UI runtime
453 // imported there would break the server; client-only forge code goes in `src/client/`.
454 files: ["packages/source-control-*/src/shared/**"],
455 rules: {
456 "eslint/no-restricted-imports": [
457 "error",
458 {
459 paths: [...RESTRICTED_IMPORT_PATHS, RESTRICTED_PULL_REQUEST_GLYPH_IMPORTS],
460 patterns: [
461 {
462 group: [
463 "react",
464 "react/*",
465 "react-*",
466 "expo",
467 "expo-*",
468 "lucide-react",
469 "@t3tools/client-runtime",
470 "@t3tools/client-runtime/*",
471 "../client/*",
472 "../server/*",
473 "../../client/*",
474 "../../server/*",
475 "../../../client/*",
476 "../../../server/*",
477 "@t3tools/source-control-*/client/*",
478 "@t3tools/source-control-*/server/*",
479 ],
480 message:
481 "Shared forge definitions load on the server and every client; keep UI and server code out.",
482 },
483 ],
484 },
485 ],
486 },
487 },
488 {
489 files: ["apps/server/src/**", "packages/source-control-*/src/server/**"],
490 rules: {
491 "eslint/no-restricted-imports": [
492 "error",
493 {
494 paths: [...RESTRICTED_IMPORT_PATHS, RESTRICTED_PULL_REQUEST_GLYPH_IMPORTS],
495 patterns: [
496 {
497 group: [
498 "@t3tools/source-control-*/client/*",
499 "../client/*",
500 "../../client/*",
501 "../../../client/*",
502 ],
503 message:
504 "A forge's client entry is UI-only; the server reads its `shared/definition`.",
505 },
506 ],
507 },
508 ],
509 },
510 },
511 {
512 files: ["apps/mobile/src/**"],
513 rules: {
514 "t3code/no-mobile-uniwind-theme-escape-hatches": "error",
515 "t3code/require-registered-list-scroll": "error",
516 },
517 },
518 {
519 files: ["apps/mobile/src/**"],
520 excludeFiles: [
521 "apps/mobile/src/components/ui/AppSymbol.tsx",
522 "apps/mobile/src/components/ui/AppSymbol.android.tsx",
523 "apps/mobile/src/features/pull-requests/pullRequestGlyphs.ts",
524 ],
525 rules: { "t3code/no-mobile-pull-request-glyph-literals": "error" },
526 },
527 {
528 files: ["apps/mobile/src/**"],
529 excludeFiles: ["**/*.test.{ts,tsx}"],
530 rules: { "t3code/no-ambiguous-detail-navigation": "error" },
531 },
532 {
533 // Raw ResizeObserver state commits miss the paint; observeResize flushes derived layout together.
534 files: ["apps/web/src/**"],
535 excludeFiles: [
536 "apps/web/src/lib/observeResize.ts",
537 "**/*.{test,spec}.{ts,tsx,js,jsx,mts,cts,mjs,cjs}",
538 ],
539 rules: { "t3code/no-raw-resize-observer": "error" },
540 },
541 {
542 files: ["apps/web/src/**"],
543 rules: { "shadcn/no-unknown-classes": "error" },
544 },
545 {
546 files: ["apps/web/src/**"],
547 rules: { "t3code/no-outset-state-indicators": "error" },
548 },
549 {
550 files: ["apps/web/src/**"],
551 rules: { "t3code/max-settings-description-length": "error" },
552 },
553 {
554 files: ["apps/*/src/**", "packages/*/src/**"],
555 excludeFiles: [
556 "**/*.{test,spec}.{ts,tsx,js,jsx,mts,cts,mjs,cjs}",
557 "**/testkit/**",
558 "**/_generated/**",
559 // Real user quotes stay verbatim.
560 "apps/marketing/src/lib/tweets.ts",
561 ],
562 rules: { "t3code/no-em-dash-copy": "error" },
563 },
564 {
565 files: ["apps/web/src/**"],
566 rules: { "shadcn/no-raw-colors": "error" },
567 },
568 {
569 // Third-party marks must retain their exact colors.
570 files: ["apps/web/src/components/Icons.tsx", "apps/web/src/components/JetBrainsIcons.tsx"],
571 rules: { "shadcn/no-raw-colors": "off" },
572 },
573 {
574 files: ["apps/web/src/**"],
575 excludeFiles: ["apps/web/src/components/ui/**"],
576 rules: {
577 "shadcn/require-static-classes": "error",
578 "shadcn/no-arbitrary-values": [
579 "error",
580 {
581 allow: [
582 "layout",
583 "transition",
584 "rounded-[inherit]",
585 "gap-[0.33em]",
586 "gap-[0.2em]",
587 "leading-[normal]",
588 "underline-offset-[0.2em]",
589 "rounded-[25%]",
590 "text-[length:80cqh]",
591 "bg-[Highlight]",
592 "fill-[#26251E]",
593 "fill-[#EDECEC]",
594 "fill-[#0F0F0F]",
595 "fill-[#F5F5F5]",
596 "fill-[#d97757]",
597 "text-[#d97757]",
598 ],
599 },
600 ],
601 "shadcn/no-restyle": [
602 "error",
603 {
604 allow: ["layout"],
605 contracts: [
606 {
607 // CollapsibleTrigger has no styled counterpart; other triggers use render={<Button …/>}.
608 pattern: "^CollapsibleTrigger$",
609 allow: ["layout", "color", "typography", "spacing", "shape", "effects", "motion"],
610 },
611 ],
612 },
613 ],
614 },
615 },
616 {
617 // React Compiler owns memoization; upstream rules do not ban redundant useMemo/useCallback.
618 files: ["apps/web/src/**/*.{ts,tsx}"],
619 rules: {
620 "react/exhaustive-effect-dependencies": "error",
621 "react/globals": "error",
622 "react/hooks": "error",
623 "react/immutability": "error",
624 // The compiler memoizes context values; this rule would ask for useMemo.
625 "react/jsx-no-constructed-context-values": "off",
626 "react/memo-dependencies": "error",
627 "react/preserve-manual-memoization": "error",
628 // Render-time clock reads use @t3tools/client-runtime/clock or state seeded with the
629 // time; any other read carries a suppression that says why. oxlint also flags time reads
630 // inside event handlers (oxc#27309); those suppressions cite it.
631 "react/purity": "error",
632 "react/refs": "error",
633 "react/set-state-in-effect": "error",
634 "react/set-state-in-render": "error",
635 "react/static-components": "error",
636 "react/unsupported-syntax": "error",
637 "t3code/no-manual-memo": "error",
638 },
639 },
640 {
641 // ESNext types allow APIs absent from Hermes; restrict shared client code, exempting Node tests.
642 files: [
643 "apps/mobile/src/**",
644 "packages/client-runtime/src/**",
645 "packages/contracts/src/**",
646 "packages/shared/src/**",
647 ],
648 excludeFiles: ["**/*.test.ts", "**/*.test.tsx"],
649 rules: { "t3code/no-hermes-unsupported-apis": "error" },
650 },
651 {
652 // Directives hide bailouts from mobile's compiler-coverage check; retain hooks until its allowlist is empty.
653 files: ["apps/mobile/src/**/*.{ts,tsx}"],
654 rules: { "t3code/no-manual-memo": ["error", { allowMemoHooks: true }] },
655 },
656 {
657 files: ["apps/web/src/**", "apps/mobile/src/**", "packages/*/src/**"],
658 rules: { "t3code/no-unreasoned-state-sync": "error" },
659 },
660 {
661 // React Compiler caches use* functions only if they call a hook.
662 files: ["apps/web/src/**/*.{ts,tsx}", "apps/mobile/src/**/*.{ts,tsx}"],
663 excludeFiles: ["**/*.test.ts", "**/*.test.tsx"],
664 rules: { "t3code/no-hookless-use-function": "error" },
665 },
666 {
667 files: [
668 "apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx",
669 "apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx",
670 "apps/mobile/src/features/files/FileMarkdownPreview.tsx",
671 "apps/mobile/src/features/files/SourceFileSurface.tsx",
672 "apps/mobile/src/features/files/AttachmentFileScreen.tsx",
673 "apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx",
674 "apps/mobile/src/features/files/thread-file-navigator-pane.tsx",
675 "apps/mobile/src/features/layout/nested-stacks.tsx",
676 "apps/mobile/src/features/review/ReviewSheet.tsx",
677 "apps/mobile/src/features/review/useNativeReviewDiffBridge.ts",
678 "apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx",
679 "apps/mobile/src/features/threads/ThreadComposer.tsx",
680 "apps/mobile/src/features/threads/ThreadFeed.tsx",
681 "apps/mobile/src/features/threads/ThreadQueueControl.tsx",
682 "apps/mobile/src/features/review/ReviewCommentCard.tsx",
683 "apps/mobile/src/features/threads/thread-list-v2-items.tsx",
684 "apps/mobile/src/components/ui/SecretTextInput.android.tsx",
685 "apps/mobile/src/lib/useMobileNavigationTheme.ts",
686 "apps/mobile/src/native/T3ComposerEditor.ios.tsx",
687 "apps/mobile/src/native/T3ComposerEditor.native.tsx",
688 "apps/mobile/src/native/SelectableMarkdownText.tsx",
689 "apps/mobile/src/native/useEnrichedLinkVariants.ts",
690 ],
691 rules: {
692 "t3code/no-mobile-uniwind-theme-escape-hatches": ["error", { allowUniwindTheme: true }],
693 },
694 },
695 // Hand-styled raw controls tracked as debt: no net-new occurrences. Lower a
696 // ceiling when you replace one with a components/ui control; delete it at zero.
697 // Never raise a ceiling or add a file to pass lint: render the ui control.
698 ...existingCeilings({
699 "apps/web/src/components/pullRequest/guide/GuideRail.tsx": 1,
700 "apps/web/src/components/Sidebar.tsx": 1,
701 "apps/web/src/components/sidebar/SidebarDraftBlock.tsx": 1,
702 "apps/web/src/components/sidebar/SidebarThreadRow.tsx": 10,
703 "apps/web/src/components/chat/MessagesTimeline.tsx": 3,
704 "apps/web/src/components/chat/TimelineMinimap.tsx": 1,
705 "apps/web/src/components/chat/TimelineUserMessageRow.tsx": 1,
706 "apps/web/src/components/chat/TimelineV2EventRow.tsx": 1,
707 "apps/web/src/components/chat/TimelineWorkEntryRow.tsx": 2,
708 "apps/web/src/components/chat/TimelineWorkGroupRows.tsx": 1,
709 "apps/web/src/components/chat/UserMessageContextChips.tsx": 1,
710 "apps/web/src/components/chat/ChatComposer.tsx": 2,
711 "apps/web/src/components/chat/ComposerAttachmentStrip.tsx": 3,
712 "apps/web/src/components/chat/ComposerPrimaryActions.tsx": 3,
713 "apps/web/src/components/settings/ThemeSettings.tsx": 5,
714 "apps/web/src/components/settings/AddEnvironmentDialog.tsx": 1,
715 "apps/web/src/components/settings/AuthorizedClientsSettings.tsx": 3,
716 "apps/web/src/components/settings/ThemeColorPicker.tsx": 3,
717 "apps/web/src/components/chat/ChatHeader.tsx": 2,
718 "apps/web/src/components/pullRequest/PullRequestReactions.tsx": 3,
719 "apps/web/src/components/RightPanelTabs.tsx": 3,
720 "apps/web/src/components/settings/ProjectIconPickerDialog.tsx": 3,
721 "apps/web/src/components/chat/ChangedFilesTree.tsx": 2,
722 "apps/web/src/components/preview/PreviewRecentUrlCard.tsx": 2,
723 "apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx": 1,
724 "apps/web/src/components/settings/AddProviderInstanceDialog.tsx": 2,
725 "apps/web/src/components/sidebar/SidebarProviderUpdatePill.tsx": 2,
726 "apps/web/src/components/ThreadTerminalDrawer.tsx": 1,
727 "apps/web/src/components/chat/AssistantCitationCommentEditor.tsx": 1,
728 "apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx": 1,
729 "apps/web/src/components/chat/ComposerStashMenu.tsx": 1,
730 "apps/web/src/components/chat/DraftHeroHeadline.tsx": 1,
731 "apps/web/src/components/chat/TimelineSystemDivider.tsx": 1,
732 "apps/web/src/components/chat/V2ItemInspector.tsx": 1,
733 "apps/web/src/components/chat/V2LifecycleRow.tsx": 1,
734 "apps/web/src/components/ChatMarkdown.tsx": 1,
735 "apps/web/src/browser/BrowserViewportResizeHandles.tsx": 1,
736 "apps/web/src/components/desktopUpdate.toast.tsx": 1,
737 "apps/web/src/components/files/FileBreadcrumbs.tsx": 1,
738 "apps/web/src/components/files/FileBrowserPanel.tsx": 1,
739 "apps/web/src/components/media/MediaVideoPlayer.tsx": 1,
740 "apps/web/src/components/projectScriptEditor.tsx": 1,
741 "apps/web/src/components/pullRequest/PullRequestChecksPopover.tsx": 1,
742 "apps/web/src/components/pullRequest/PullRequestCopyableCode.tsx": 1,
743 "apps/web/src/components/pullRequest/PullRequestMarkdown.tsx": 1,
744 "apps/web/src/components/pullRequest/PullRequestReviewAnnotation.tsx": 1,
745 "apps/web/src/components/pullRequest/PullRequestRow.tsx": 1,
746 "apps/web/src/components/pullRequest/PullRequestTimelineTab.tsx": 1,
747 "apps/web/src/components/search/ProjectContentSearchDialog.tsx": 1,
748 "apps/web/src/components/settings/BrowserImportWizard.tsx": 1,
749 "apps/web/src/components/settings/CodexSetupSection.tsx": 1,
750 "apps/web/src/components/settings/DeviceHostEditor.tsx": 1,
751 "apps/web/src/components/settings/DiagnosticsSettings.tsx": 1,
752 "apps/web/src/components/settings/KeybindingsSettings.tsx": 1,
753 "apps/web/src/components/settings/PanelAnimationsPreview.tsx": 1,
754 "apps/web/src/components/settings/ProviderInstanceCard.tsx": 1,
755 "apps/web/src/components/settings/RedactedSensitiveText.tsx": 1,
756 "apps/web/src/components/settings/ThemeImportDialog.tsx": 1,
757 "apps/web/src/components/settings/ThemePreviewCircles.tsx": 1,
758 "apps/web/src/components/sidebar/SidebarUpdatePill.tsx": 1,
759 "apps/web/src/components/sidebar/SidebarUpdateReleaseNotes.tsx": 1,
760 "apps/web/src/components/ThreadStatusIndicators.tsx": 1,
761 "apps/web/src/components/usage/UsageLimitsPooled.tsx": 1,
762 }).map(([file, maxOccurrences]) => {
763 const rule: ["error", { maxOccurrences: number }] = ["error", { maxOccurrences }];
764 return { files: [file], rules: { "t3code/no-styled-raw-controls": rule } };
765 }),
766 // Raw type classes tracked as debt: no net-new occurrences. Lower a ceiling
767 // when you move a file onto the type roles; delete its entry at zero.
768 // Never raise a ceiling or add a file to pass lint: use <Text> or a role class.
769 ...existingCeilings({
770 "apps/web/src/components/settings/AboutVersionTitle.tsx": 1,
771 "apps/web/src/components/chat/SideChatPane.tsx": 2,
772 "apps/web/src/components/pullRequest/guide/GuideNotes.tsx": 7,
773 "apps/web/src/components/pullRequest/guide/GuideRail.tsx": 4,
774 "apps/web/src/components/settings/ResourceTelemetryDiagnostics.tsx": 58,
775 "apps/web/src/components/chat/MessagesTimeline.tsx": 4,
776 "apps/web/src/components/chat/TimelineAssistantRows.tsx": 3,
777 "apps/web/src/components/chat/TimelineMinimap.tsx": 2,
778 "apps/web/src/components/chat/TimelineRowParts.tsx": 1,
779 "apps/web/src/components/chat/TimelineUserMessageRow.tsx": 2,
780 "apps/web/src/components/chat/TimelineV2EventRow.tsx": 14,
781 "apps/web/src/components/chat/TimelineWorkEntryRow.tsx": 7,
782 "apps/web/src/components/chat/TimelineWorkGroupRows.tsx": 1,
783 "apps/web/src/components/chat/UserMessageContextChips.tsx": 14,
784 "apps/web/src/components/settings/DiagnosticsSettings.tsx": 15,
785 "apps/web/src/components/settings/ConnectionsSettings.tsx": 2,
786 "apps/web/src/components/settings/AuthorizedClientsSettings.tsx": 1,
787 "apps/web/src/components/usage/UsagePage.tsx": 34,
788 "apps/web/src/components/GitActionsControl.tsx": 1,
789 "apps/web/src/components/Sidebar.tsx": 3,
790 "apps/web/src/components/sidebar/SidebarDraftBlock.tsx": 2,
791 "apps/web/src/components/sidebar/SidebarThreadRow.tsx": 20,
792 "apps/web/src/components/sidebar/SidebarListMarkers.tsx": 2,
793 "apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx": 3,
794 "apps/web/src/components/usage/UsageLimitsPooled.tsx": 25,
795 "apps/web/src/components/settings/SettingsPanels.tsx": 3,
796 "apps/web/src/components/settings/AppearanceSettingsPanel.tsx": 1,
797 "apps/web/src/components/settings/BackgroundActivitySettings.tsx": 15,
798 "apps/web/src/components/settings/TypographySettings.tsx": 1,
799 "apps/web/src/components/pullRequest/PullRequestTimelineTab.tsx": 18,
800 "apps/web/src/components/chat/ChatComposer.tsx": 1,
801 "apps/web/src/components/chat/ComposerControlsBar.tsx": 1,
802 "apps/web/src/components/chat/ComposerAttachmentStrip.tsx": 4,
803 "apps/web/src/components/chat/ComposerFooterControls.tsx": 2,
804 "apps/web/src/components/device/DeviceSetup.tsx": 14,
805 "apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx": 1,
806 "apps/web/src/components/DiffPanel.tsx": 1,
807 "apps/web/src/components/pullRequest/PullRequestGhosts.tsx": 4,
808 "apps/web/src/components/settings/ProviderInstanceCard.tsx": 11,
809 "apps/web/src/components/settings/AddProviderInstanceDialog.tsx": 11,
810 "apps/web/src/components/settings/CodexSetupSection.tsx": 4,
811 "apps/web/src/components/chat/V2LifecycleRow.tsx": 9,
812 "apps/web/src/components/device/DeviceToolsPanel.tsx": 10,
813 "apps/web/src/components/settings/KeybindingsSettings.tsx": 1,
814 "apps/web/src/components/settings/OpenSourceLicenses.tsx": 1,
815 "apps/web/src/components/settings/ThemeEditorPanel.tsx": 6,
816 "apps/web/src/components/settings/CaptureShortcutConfig.tsx": 9,
817 "apps/web/src/components/chat/WorktreeSetupCard.tsx": 1,
818 "apps/web/src/components/ChatMarkdown.tsx": 4,
819 "apps/web/src/components/chat/ChatMarkdownMedia.tsx": 2,
820 "apps/web/src/components/chat/MarkdownCodeBlock.tsx": 1,
821 "apps/web/src/components/chat/MarkdownDetails.tsx": 1,
822 "apps/web/src/components/device/DevicePanel.tsx": 4,
823 "apps/web/src/components/settings/BrowserImportWizard.tsx": 8,
824 "apps/web/src/components/settings/ProviderModelsSection.tsx": 5,
825 "apps/web/src/components/settings/ScheduledTasksSettings.tsx": 6,
826 "apps/web/src/components/settings/SourceControlSettings.tsx": 2,
827 "apps/web/src/components/projectScriptEditor.tsx": 2,
828 "apps/web/src/components/pullRequest/PullRequestChecksPopover.tsx": 7,
829 "apps/web/src/components/search/ProjectContentSearchDialog.tsx": 1,
830 "apps/web/src/components/settings/CustomModelEditor.tsx": 7,
831 "apps/web/src/components/settings/DeviceHostEditor.tsx": 7,
832 "apps/web/src/components/settings/ThemeColorPicker.tsx": 6,
833 "apps/web/src/components/settings/ThemeSettings.tsx": 7,
834 "apps/web/src/components/usage/UsagePriceOverrides.tsx": 7,
835 "apps/web/src/components/chat/ChangedFilesTree.tsx": 6,
836 "apps/web/src/components/chat/SnapShotAttachmentDetails.tsx": 6,
837 "apps/web/src/components/chat/TraitsPicker.tsx": 6,
838 "apps/web/src/components/cloud/CloudEnvironmentConnectList.tsx": 6,
839 "apps/web/src/components/CommandPalette.tsx": 1,
840 "apps/web/src/components/CommandPaletteAddProject.tsx": 4,
841 "apps/web/src/components/CommandPaletteThemeItems.tsx": 2,
842 "apps/web/src/components/CommandPaletteResults.tsx": 6,
843 "apps/web/src/components/pullRequest/PullRequestListFilters.tsx": 4,
844 "apps/web/src/components/pullRequest/PullRequestListRow.tsx": 5,
845 "apps/web/src/components/PullRequestThreadDialog.tsx": 6,
846 "apps/web/src/components/settings/AcpSessionManagementSection.tsx": 6,
847 "apps/web/src/components/settings/ProjectIconPickerDialog.tsx": 1,
848 "apps/web/src/components/settings/SnapShotSetupDialog.tsx": 6,
849 "apps/web/src/components/sidebar/SidebarUpdateReleaseNotes.tsx": 6,
850 "apps/web/src/components/chat/V2ItemInspector.tsx": 2,
851 "apps/web/src/components/device/DeviceToolVersions.tsx": 5,
852 "apps/web/src/components/preview/PreviewEmptyState.tsx": 2,
853 "apps/web/src/components/pullRequest/PullRequestReviewAnnotation.tsx": 1,
854 "apps/web/src/components/RightPanelTabs.tsx": 2,
855 "apps/web/src/components/settings/AcpRegistrySearchStep.tsx": 5,
856 "apps/web/src/components/settings/ProviderAuthenticationSection.tsx": 5,
857 "apps/web/src/components/settings/SettingInheritance.tsx": 1,
858 "apps/web/src/components/chat/ComposerUsageLimits.tsx": 4,
859 "apps/web/src/components/chat/TimelineOverlays.tsx": 1,
860 "apps/web/src/components/composerContextPresentation.tsx": 4,
861 "apps/web/src/components/contextChipParts.tsx": 4,
862 "apps/web/src/components/files/AttachmentFilePreview.tsx": 4,
863 "apps/web/src/components/settings/ChatGptAccountPicker.tsx": 4,
864 "apps/web/src/components/pullRequest/PullRequestDetailLayout.tsx": 4,
865 "apps/web/src/components/settings/ProjectActionsList.tsx": 1,
866 "apps/web/src/components/settings/ProviderSettingsPanel.tsx": 2,
867 "apps/web/src/components/settings/SettingsSidebarNav.tsx": 3,
868 "apps/web/src/components/ThreadStatusIndicators.tsx": 4,
869 "apps/web/src/components/ThreadTerminalDrawer.tsx": 1,
870 "apps/web/src/components/usage/UsageLimits.tsx": 4,
871 "apps/web/src/routes/__root.tsx": 4,
872 "apps/web/src/components/pullRequest/PullRequestsPage.tsx": 2,
873 "apps/web/src/components/pullRequest/PullRequestListGroups.tsx": 2,
874 "apps/web/src/browser/BrowserDeviceToolbar.tsx": 3,
875 "apps/web/src/components/BranchToolbarEnvModeSelector.tsx": 1,
876 "apps/web/src/components/chat/ComposerCommandMenu.tsx": 2,
877 "apps/web/src/components/chat/ComposerControl.tsx": 3,
878 "apps/web/src/components/chat/ExpandedImageDialog.tsx": 3,
879 "apps/web/src/components/chat/ModelListRow.tsx": 3,
880 "apps/web/src/components/chat/ModelPickerContent.tsx": 3,
881 "apps/web/src/components/chat/TerminalContextInlineChip.tsx": 3,
882 "apps/web/src/components/chat/ThreadDetailsPrRow.tsx": 3,
883 "apps/web/src/components/device/DeviceLoadingView.tsx": 3,
884 "apps/web/src/components/device/DeviceStreamView.tsx": 3,
885 "apps/web/src/components/files/FileBrowserPanel.tsx": 3,
886 "apps/web/src/components/files/fileSurfaceChrome.tsx": 1,
887 "apps/web/src/components/permissions/PermissionChecklist.tsx": 3,
888 "apps/web/src/components/pullRequest/PullRequestLinkPreview.tsx": 3,
889 "apps/web/src/components/pullRequest/PullRequestThreadLinks.tsx": 3,
890 "apps/web/src/components/pullRequest/ThreadPullRequestsPanel.tsx": 1,
891 "apps/web/src/components/ServerUpdateAction.tsx": 3,
892 "apps/web/src/components/settings/FoldedSettingsSection.tsx": 3,
893 "apps/web/src/components/settings/ProjectDefaultsSettings.tsx": 1,
894 "apps/web/src/components/settings/ProviderSettingsForm.tsx": 3,
895 "apps/web/src/components/SlowRpcRequestToastCoordinator.tsx": 1,
896 "apps/web/src/components/usage/UsageProviderChart.tsx": 3,
897 "apps/web/src/components/BranchToolbarBranchSelector.tsx": 2,
898 "apps/web/src/components/chat/AssistantCitationCommentEditor.tsx": 2,
899 "apps/web/src/components/chat/CompactComposerControlsMenu.tsx": 2,
900 "apps/web/src/components/chat/ComposerBanner.tsx": 2,
901 "apps/web/src/components/chat/ComposerBannerStack.tsx": 1,
902 "apps/web/src/components/chat/DiffStatLabel.tsx": 2,
903 "apps/web/src/components/chat/DraftHeroHeadline.tsx": 2,
904 "apps/web/src/components/chat/ProposedPlanCard.tsx": 2,
905 "apps/web/src/components/chat/ProviderInstanceIcon.tsx": 3,
906 "apps/web/src/components/chat/ProviderSubagentBar.tsx": 2,
907 "apps/web/src/components/chat/ThreadContextDragGhost.tsx": 1,
908 "apps/web/src/components/chat/TimelineSystemDivider.tsx": 2,
909 "apps/web/src/components/chat/useAutoBalanceUpdateBanner.tsx": 2,
910 "apps/web/src/components/ComposerPromptEditorTiptap.tsx": 2,
911 "apps/web/src/components/device/DeviceHostUpdates.tsx": 2,
912 "apps/web/src/components/diffs/DiffFileTree.tsx": 2,
913 "apps/web/src/components/files/DelimitedTablePreview.tsx": 2,
914 "apps/web/src/components/files/ProjectFilePicker.tsx": 2,
915 "apps/web/src/components/NoActiveThreadState.tsx": 2,
916 "apps/web/src/components/preview/PreviewRecentUrlCard.tsx": 1,
917 "apps/web/src/components/pullRequest/PullRequestActivityUnavailableState.tsx": 2,
918 "apps/web/src/components/pullRequest/pullRequestPresentation.tsx": 2,
919 "apps/web/src/components/pullRequest/PullRequestReactions.tsx": 2,
920 "apps/web/src/components/PullRequestContextDetails.tsx": 2,
921 "apps/web/src/components/settings/EnvironmentRow.tsx": 2,
922 "apps/web/src/components/settings/ProjectActionsSettings.tsx": 1,
923 "apps/web/src/components/settings/ProviderAccentColorPicker.tsx": 2,
924 "apps/web/src/components/ThreadHoverCard.tsx": 2,
925 "apps/web/src/components/ThreadSearchMatch.tsx": 2,
926 "apps/web/src/components/WorkspaceBreadcrumb.tsx": 2,
927 "apps/web/src/contextMenuFallback.ts": 1,
928 "apps/web/src/browser/HostedBrowserWebview.tsx": 1,
929 "apps/web/src/components/BranchToolbarEnvironmentSelector.tsx": 1,
930 "apps/web/src/components/chat/ChatGptSharingControl.tsx": 1,
931 "apps/web/src/components/chat/ComposerPrimaryActions.tsx": 1,
932 "apps/web/src/components/chat/ProviderModelPicker.tsx": 1,
933 "apps/web/src/components/chat/SubagentTooltipContent.tsx": 1,
934 "apps/web/src/components/chat/WorkLog.tsx": 1,
935 "apps/web/src/components/ContextChip.tsx": 1,
936 "apps/web/src/components/device/DeviceAndroidFoldControls.tsx": 1,
937 "apps/web/src/components/device/DeviceDuoControls.tsx": 1,
938 "apps/web/src/components/device/DeviceHostAvailability.tsx": 1,
939 "apps/web/src/components/device/DeviceWorkspace.tsx": 1,
940 "apps/web/src/components/files/FileBreadcrumbs.tsx": 1,
941 "apps/web/src/components/media/MediaVideoPlayer.tsx": 1,
942 "apps/web/src/components/preview/PreviewMoreMenu.tsx": 1,
943 "apps/web/src/components/preview/PreviewPanel.tsx": 1,
944 "apps/web/src/components/preview/ThreadPreviewMiniPlayer.tsx": 1,
945 "apps/web/src/components/preview/ZoomIndicator.tsx": 1,
946 "apps/web/src/components/PreviousWorktreeItemContent.tsx": 1,
947 "apps/web/src/components/pullRequest/PullRequestMarkdown.tsx": 1,
948 "apps/web/src/components/pullRequest/PullRequestMarkdownEditor.tsx": 1,
949 "apps/web/src/components/pullRequest/PullRequestRow.tsx": 1,
950 "apps/web/src/components/QuitHoldOverlay.tsx": 1,
951 "apps/web/src/components/settings/EnvironmentIconPicker.tsx": 1,
952 "apps/web/src/components/settings/FontFamilyPicker.tsx": 1,
953 "apps/web/src/components/settings/LocalEnvironmentSetting.tsx": 1,
954 "apps/web/src/components/settings/ProviderSetupSection.tsx": 1,
955 "apps/web/src/components/settings/RedactedSensitiveText.tsx": 1,
956 "apps/web/src/components/sidebar/SidebarProviderUpdatePill.tsx": 1,
957 "apps/web/src/components/ThreadCommandSubtitle.tsx": 1,
958 "apps/web/src/components/settings/SecretsSettings.tsx": 1,
959 "apps/web/src/components/settings/CloudVoiceConfiguration.tsx": 8,
960 }).map(([file, maxOccurrences]) => {
961 const rule: ["error", { maxOccurrences: number }] = ["error", { maxOccurrences }];
962 return { files: [file], rules: { "t3code/no-raw-type-classes": rule } };
963 }),
964 ],
965 options: {
966 reportUnusedDisableDirectives: "error",
967 typeAware: false,
968 typeCheck: false,
969 },
970 },
971});