1import "vite-plus/test/config";
2import { defineConfig } from "vite-plus";
3import * as NodeURL from "node:url";
4
5/** Import restrictions every file keeps, including the one module exempt from the glyph rule. */
6const RESTRICTED_IMPORT_PATHS = [
7 {
8 name: "@t3tools/client-runtime",
9 message:
10 "Import from an explicit @t3tools/client-runtime/* subpath. The package has no root export.",
11 },
12 {
13 name: "@pierre/diffs/react",
14 importNames: ["CodeView"],
15 message: "Use StyledDiffCodeView so web diff surfaces share styling and virtualized geometry.",
16 },
17];
18
19/**
20 * The cva functions behind components/ui exports. They style a foreign element to look
21 * like a Button or Toggle, which bypasses the component's variants; render the component
22 * instead (`render={<Button …/>}`, or `SelectButton` for a picker trigger).
23 */
24const RESTRICTED_UI_VARIANT_PATTERNS = [
25 {
26 group: ["**/components/ui/*", "**/ui/*", "./ui/*"],
27 importNames: ["buttonVariants", "toggleVariants", "badgeVariants", "selectTriggerVariants"],
28 message:
29 "Render the components/ui export instead of borrowing its class recipe (render={<Button …/>}, SelectButton, ToggleGroup).",
30 },
31];
32
33/** Lucide's pull-request glyphs, which only `pullRequestIcons.tsx` may name. */
34const RESTRICTED_PULL_REQUEST_GLYPH_IMPORTS = {
35 name: "lucide-react",
36 importNames: [
37 "GitMerge",
38 "GitMergeIcon",
39 "GitPullRequest",
40 "GitPullRequestIcon",
41 "GitPullRequestArrow",
42 "GitPullRequestArrowIcon",
43 "GitPullRequestClosed",
44 "GitPullRequestClosedIcon",
45 "GitPullRequestDraft",
46 "GitPullRequestDraftIcon",
47 "GitPullRequestCreate",
48 "GitPullRequestCreateIcon",
49 "GitPullRequestCreateArrow",
50 "GitPullRequestCreateArrowIcon",
51 ],
52 message:
53 "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.",
54};
55
56export default defineConfig({
57 resolve: {
58 alias: {
59 "~": NodeURL.fileURLToPath(new URL("./apps/web/src", import.meta.url)),
60 },
61 },
62 test: {
63 environment: "node",
64 exclude: [
65 "**/.repos/**",
66 "**/.t3/**",
67 "**/node_modules/**",
68 "**/dist/**",
69 "**/dist-electron/**",
70 "**/.{idea,git,cache,output,temp}/**",
71 ],
72 hookTimeout: 60_000,
73 testTimeout: 60_000,
74 setupFiles: [
75 NodeURL.fileURLToPath(
76 new URL("./packages/shared/src/testing/longTempDir.ts", import.meta.url),
77 ),
78 ],
79 },
80 staged: {
81 // Formatter only for now — no lint or typecheck on commit.
82 "*": "vp fmt --no-error-on-unmatched-pattern",
83 },
84 fmt: {
85 ignorePatterns: [
86 ".repos/**",
87 // Macroscope's glob-per-line ignore grammar, not Markdown: formatting
88 // it rewrites `*` as `_` and joins lines.
89 ".macroscope/ignore.md",
90 ".alchemy",
91 "dist",
92 "dist-electron",
93 "node_modules",
94 "pnpm-lock.yaml",
95 "*.tsbuildinfo",
96 "**/routeTree.gen.ts",
97 "apps/mobile/android/**",
98 "apps/mobile/ios/**",
99 "apps/mobile/uniwind-types.d.ts",
100 "*.icon/**",
101 ],
102 sortPackageJson: {},
103 overrides: [
104 {
105 files: [".devcontainer/devcontainer.json"],
106 options: {
107 trailingComma: "none",
108 },
109 },
110 ],
111 },
112 lint: {
113 ignorePatterns: [
114 ".repos",
115 ".repos/**",
116 "dist",
117 "dist-electron",
118 "node_modules",
119 "pnpm-lock.yaml",
120 "*.tsbuildinfo",
121 "**/routeTree.gen.ts",
122 "apps/mobile/android/**",
123 "apps/mobile/ios/**",
124 "apps/mobile/uniwind-types.d.ts",
125 ],
126 plugins: ["eslint", "oxc", "react", "unicorn", "typescript"],
127 jsPlugins: ["./oxlint-plugin-t3code/index.ts", "@shadcn/lint"],
128 settings: {
129 shadcn: { ui: "~/components/ui" },
130 },
131 categories: {
132 correctness: "warn",
133 suspicious: "warn",
134 perf: "warn",
135 },
136 rules: {
137 "unicorn/no-array-sort": "off",
138 "unicorn/consistent-function-scoping": "off",
139 "oxc/no-map-spread": "off",
140 "react-in-jsx-scope": "off",
141 "react-hooks/exhaustive-deps": "off",
142 "eslint/no-shadow": "off",
143 "eslint/no-await-in-loop": "off",
144 "eslint/no-underscore-dangle": "off",
145 "typescript/consistent-return": "off",
146 "typescript/no-base-to-string": "off",
147 "typescript/no-duplicate-type-constituents": "off",
148 "typescript/no-floating-promises": "off",
149 "typescript/no-implied-eval": "off",
150 "typescript/no-meaningless-void-operator": "off",
151 "typescript/no-redundant-type-constituents": "off",
152 "typescript/no-unnecessary-boolean-literal-compare": "off",
153 "typescript/no-unnecessary-type-conversion": "off",
154 "typescript/no-unnecessary-type-arguments": "off",
155 "typescript/no-unnecessary-type-assertion": "off",
156 "typescript/no-unnecessary-type-parameters": "off",
157 "typescript/no-unsafe-type-assertion": "off",
158 "typescript/await-thenable": "off",
159 "typescript/require-array-sort-compare": "off",
160 "typescript/restrict-template-expressions": "off",
161 "typescript/unbound-method": "off",
162 "eslint/no-restricted-imports": [
163 "error",
164 { paths: [...RESTRICTED_IMPORT_PATHS, RESTRICTED_PULL_REQUEST_GLYPH_IMPORTS] },
165 ],
166 "t3code/no-global-process-runtime": "error",
167 "t3code/no-inline-schema-compile": "warn",
168 "t3code/no-manual-effect-runtime-in-tests": "error",
169 "t3code/no-native-title-tooltip": "error",
170 "t3code/no-raw-mcp-registration": "error",
171 "t3code/no-test-in-loop": "error",
172 "t3code/no-rpc-permission-bypass": ["error", { allowRawClientAccess: true }],
173 "t3code/no-unscoped-has": "error",
174 "t3code/namespace-node-imports": "error",
175 "t3code/prefer-catch-tags": "error",
176 "t3code/require-centered-scroll-gutter": "error",
177 "t3code/require-suppression-reason": "error",
178 },
179 overrides: [
180 {
181 files: ["packages/client-runtime/src/state/**", "apps/{web,mobile,desktop}/src/**"],
182 rules: { "t3code/no-rpc-permission-bypass": ["error", { allowRawClientAccess: false }] },
183 },
184 {
185 // Only shared command boundaries install the session-backed permission guard.
186 files: [
187 "packages/client-runtime/src/state/runtime.ts",
188 "packages/client-runtime/src/state/vcsAction.ts",
189 ],
190 rules: {
191 "t3code/no-rpc-permission-bypass": [
192 "error",
193 { allowGuardInstallation: true, allowRawClientAccess: false },
194 ],
195 },
196 },
197 {
198 // These clients are session metadata, device streams, and an Expo update adapter.
199 files: [
200 "apps/web/src/components/settings/ConnectionsSettings.tsx",
201 "apps/mobile/src/features/updates/app-updates.ts",
202 "apps/web/src/components/device/DevicePhoneViewport.tsx",
203 "apps/web/src/components/device/DeviceDuoViewport.tsx",
204 ],
205 rules: { "t3code/no-rpc-permission-bypass": ["error", { allowRawClientAccess: true }] },
206 },
207 {
208 // Incompatible hosts cannot open a normal session; their updater uses a dedicated socket.
209 files: ["packages/client-runtime/src/connection/outdatedHostUpdate.ts"],
210 rules: { "t3code/no-rpc-permission-bypass": "off" },
211 },
212 {
213 // RPC implementation and transport test fixtures need the raw client.
214 files: [
215 "packages/client-runtime/src/rpc/**",
216 "**/*.{test,spec}.{ts,tsx,js,jsx,mts,cts,mjs,cjs}",
217 ],
218 rules: { "t3code/no-rpc-permission-bypass": "off" },
219 },
220 {
221 // The one place that reads the host platform to seed the injected references.
222 files: ["packages/shared/src/HostProcess.ts"],
223 rules: { "t3code/no-global-process-runtime": "off" },
224 },
225 {
226 // The registration helpers that only accept handlers built by McpToolAccess.
227 files: ["apps/server/src/mcp/McpHttpServer.ts"],
228 rules: { "t3code/no-raw-mcp-registration": "off" },
229 },
230 {
231 files: ["apps/web/src/**"],
232 excludeFiles: ["apps/web/src/components/ui/**"],
233 rules: {
234 "eslint/no-restricted-imports": [
235 "error",
236 {
237 paths: [...RESTRICTED_IMPORT_PATHS, RESTRICTED_PULL_REQUEST_GLYPH_IMPORTS],
238 patterns: RESTRICTED_UI_VARIANT_PATTERNS,
239 },
240 ],
241 },
242 },
243 {
244 // The one module allowed to name lucide's pull-request glyphs; everything else picks
245 // from its vocabulary. The other import restrictions still apply here.
246 files: ["apps/web/src/components/pullRequest/pullRequestIcons.tsx"],
247 rules: { "eslint/no-restricted-imports": ["error", { paths: RESTRICTED_IMPORT_PATHS }] },
248 },
249 {
250 files: ["apps/mobile/src/**"],
251 rules: { "t3code/no-mobile-uniwind-theme-escape-hatches": "error" },
252 },
253 {
254 // React commits state set in a raw ResizeObserver callback after the paint, so derived
255 // layout lands a frame late. observeResize flushes every observed resize in one render.
256 files: ["apps/web/src/**"],
257 excludeFiles: [
258 "apps/web/src/lib/observeResize.ts",
259 "**/*.{test,spec}.{ts,tsx,js,jsx,mts,cts,mjs,cjs}",
260 ],
261 rules: { "t3code/no-raw-resize-observer": "error" },
262 },
263 {
264 // Every class in web code must be one Tailwind generates: a typo or a class nothing
265 // declares ships silently unstyled. JS hooks use data attributes, not class names.
266 files: ["apps/web/src/**"],
267 rules: { "shadcn/no-unknown-classes": "error" },
268 },
269 {
270 // State indicators paint inward; decorative rings keep their own geometry.
271 files: ["apps/web/src/**"],
272 rules: { "t3code/no-outset-state-indicators": "error" },
273 },
274 {
275 // Colors come from theme tokens so status tones follow custom themes. components/ui
276 // has no findings and stays covered too.
277 files: ["apps/web/src/**"],
278 rules: { "shadcn/no-raw-colors": "error" },
279 },
280 {
281 // Third-party marks (brand logos, the macOS permission panes, Codex's Computer Use
282 // mark) must keep their exact colors, so the files that hold them are exempt.
283 files: ["apps/web/src/components/Icons.tsx", "apps/web/src/components/JetBrainsIcons.tsx"],
284 rules: { "shadcn/no-raw-colors": "off" },
285 },
286 {
287 // components/ui exports own their look. App code picks a variant or size instead
288 // of restyling with className; layout classes (width, flex, margin, position) stay
289 // allowed because placement belongs to the parent. components/ui is for generic
290 // primitives: a look that belongs to one feature stays in that feature's component.
291 files: ["apps/web/src/**"],
292 excludeFiles: ["apps/web/src/components/ui/**"],
293 rules: {
294 // A className built at runtime on a ui component is one no-restyle cannot read.
295 "shadcn/require-static-classes": "error",
296 // Appearance values come from the theme and Tailwind's scales. Layout stays free
297 // (placement belongs to the parent); the other entries are values no scale can hold.
298 "shadcn/no-arbitrary-values": [
299 "error",
300 {
301 allow: [
302 "layout",
303 // Which properties an element animates is per-element behaviour, like layout,
304 // not a design value; timing curves and durations still come from the theme.
305 "transition",
306 // Overlays that follow their frame's corner, which is set at runtime
307 // (floating preview) or by the element they decorate (composer outline).
308 "rounded-[inherit]",
309 // Inline chips size in em so they scale with the text they sit in
310 // (the composer honours the prompt font-size preference).
311 "gap-[0.33em]",
312 "px-[0.5em]",
313 "rounded-[0.5em]",
314 "text-[0.86em]",
315 // Project icons render from 14px to 48px and keep one proportional corner.
316 "rounded-[25%]",
317 // An emoji project icon fills its container, whatever size the parent gives it.
318 "text-[length:80cqh]",
319 // The platform's own selection colour on a selected composer chip.
320 "bg-[Highlight]",
321 // Brand marks keep their brand colours (Cursor, Grok, Claude).
322 "fill-[#26251E]",
323 "fill-[#EDECEC]",
324 "fill-[#0F0F0F]",
325 "fill-[#F5F5F5]",
326 "fill-[#d97757]",
327 "text-[#d97757]",
328 ],
329 },
330 ],
331 "shadcn/no-restyle": [
332 "error",
333 {
334 allow: ["layout"],
335 contracts: [
336 {
337 // CollapsibleTrigger is a bare button with no styled counterpart
338 // (a disclosure row is not a Button), so its className is the API.
339 // Every other trigger has one: style them with render={<Button …/>}.
340 pattern: "^CollapsibleTrigger$",
341 allow: ["layout", "color", "typography", "spacing", "shape", "effects", "motion"],
342 },
343 ],
344 },
345 ],
346 },
347 },
348 {
349 // The sign-in masthead is T3 brand artwork: fixed gradients, not theme surfaces.
350 files: ["apps/web/src/components/auth/AuthSurfaceShell.tsx"],
351 rules: { "shadcn/no-arbitrary-values": "off" },
352 },
353 {
354 // Shared client code must not call APIs missing from Hermes. Our ESNext
355 // TypeScript target accepts them even when they would crash mobile at launch.
356 // Tests run on Node and are exempt.
357 files: [
358 "apps/mobile/src/**",
359 "packages/client-runtime/src/**",
360 "packages/contracts/src/**",
361 "packages/shared/src/**",
362 ],
363 excludeFiles: ["**/*.test.ts", "**/*.test.tsx"],
364 rules: { "t3code/no-hermes-unsupported-apis": "error" },
365 },
366 {
367 // Reviewed native and third-party interop boundaries that cannot consume a className.
368 files: [
369 "apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx",
370 "apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx",
371 "apps/mobile/src/features/files/FileMarkdownPreview.tsx",
372 "apps/mobile/src/features/files/SourceFileSurface.tsx",
373 "apps/mobile/src/features/files/AttachmentFileScreen.tsx",
374 "apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx",
375 "apps/mobile/src/features/files/thread-file-navigator-pane.tsx",
376 "apps/mobile/src/features/home/HomeHeader.tsx",
377 "apps/mobile/src/features/review/ReviewSheet.tsx",
378 "apps/mobile/src/features/review/useNativeReviewDiffBridge.ts",
379 "apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx",
380 "apps/mobile/src/features/threads/GitActionProgressOverlay.tsx",
381 "apps/mobile/src/features/threads/NewTaskDraftScreen.tsx",
382 "apps/mobile/src/features/threads/ThreadComposer.tsx",
383 "apps/mobile/src/features/threads/ThreadFeed.tsx",
384 "apps/mobile/src/features/settings/appearance/components/FontSizeSliderRow.tsx",
385 "apps/mobile/src/features/threads/NewTaskContextPickerScreens.tsx",
386 "apps/mobile/src/features/threads/ThreadQueueControl.tsx",
387 "apps/mobile/src/features/threads/ThreadAgentsSheet.tsx",
388 "apps/mobile/src/features/review/ReviewCommentCard.tsx",
389 "apps/mobile/src/features/threads/ThreadSettingsSheet.tsx",
390 "apps/mobile/src/features/threads/git/GitOverviewSheet.tsx",
391 "apps/mobile/src/features/threads/thread-list-items.tsx",
392 "apps/mobile/src/features/threads/thread-list-v2-items.tsx",
393 "apps/mobile/src/lib/useMobileNavigationTheme.ts",
394 "apps/mobile/src/native/T3ComposerEditor.ios.tsx",
395 "apps/mobile/src/native/T3ComposerEditor.native.tsx",
396 "apps/mobile/src/native/SelectableMarkdownText.tsx",
397 ],
398 rules: {
399 "t3code/no-mobile-uniwind-theme-escape-hatches": ["error", { allowUniwindTheme: true }],
400 },
401 },
402 ],
403 options: {
404 reportUnusedDisableDirectives: "error",
405 // Revisit once Oxlint's tsgolint path can integrate with @effect/tsgo diagnostics.
406 typeAware: false,
407 typeCheck: false,
408 },
409 },
410});