import "vite-plus/test/config";
import { defineConfig } from "vite-plus";
import * as NodeURL from "node:url";
/** Import restrictions every file keeps, including the one module exempt from the glyph rule. */
const RESTRICTED_IMPORT_PATHS = [
{
name: "@t3tools/client-runtime",
message:
"Import from an explicit @t3tools/client-runtime/* subpath. The package has no root export.",
},
{
name: "@pierre/diffs/react",
importNames: ["CodeView"],
message: "Use StyledDiffCodeView so web diff surfaces share styling and virtualized geometry.",
},
];
/** Lucide's pull-request glyphs, which only `pullRequestIcons.tsx` may name. */
const RESTRICTED_PULL_REQUEST_GLYPH_IMPORTS = {
name: "lucide-react",
importNames: [
"GitMerge",
"GitMergeIcon",
"GitPullRequest",
"GitPullRequestIcon",
"GitPullRequestArrow",
"GitPullRequestArrowIcon",
"GitPullRequestClosed",
"GitPullRequestClosedIcon",
"GitPullRequestDraft",
"GitPullRequestDraftIcon",
"GitPullRequestCreate",
"GitPullRequestCreateIcon",
"GitPullRequestCreateArrow",
"GitPullRequestCreateArrowIcon",
],
message:
"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.",
};
export default defineConfig({
resolve: {
alias: {
"~": NodeURL.fileURLToPath(new URL("./apps/web/src", import.meta.url)),
},
},
test: {
environment: "node",
exclude: [
"**/.repos/**",
"**/node_modules/**",
"**/dist/**",
"**/dist-electron/**",
"**/.{idea,git,cache,output,temp}/**",
],
hookTimeout: 60_000,
testTimeout: 60_000,
setupFiles: [
NodeURL.fileURLToPath(
new URL("./packages/shared/src/testing/longTempDir.ts", import.meta.url),
),
],
},
staged: {
// Formatter only for now — no lint or typecheck on commit.
"*": "vp fmt --no-error-on-unmatched-pattern",
},
fmt: {
ignorePatterns: [
".repos/**",
// Macroscope's glob-per-line ignore grammar, not Markdown: formatting
// it rewrites `*` as `_` and joins lines.
".macroscope/ignore.md",
".alchemy",
"dist",
"dist-electron",
"node_modules",
"pnpm-lock.yaml",
"*.tsbuildinfo",
"**/routeTree.gen.ts",
"apps/mobile/android/**",
"apps/mobile/ios/**",
"apps/mobile/uniwind-types.d.ts",
"*.icon/**",
],
sortPackageJson: {},
overrides: [
{
files: [".devcontainer/devcontainer.json"],
options: {
trailingComma: "none",
},
},
],
},
lint: {
ignorePatterns: [
".repos",
".repos/**",
"dist",
"dist-electron",
"node_modules",
"pnpm-lock.yaml",
"*.tsbuildinfo",
"**/routeTree.gen.ts",
"apps/mobile/android/**",
"apps/mobile/ios/**",
"apps/mobile/uniwind-types.d.ts",
],
plugins: ["eslint", "oxc", "react", "unicorn", "typescript"],
jsPlugins: ["./oxlint-plugin-t3code/index.ts", "@shadcn/lint"],
settings: {
shadcn: { ui: "~/components/ui" },
},
categories: {
correctness: "warn",
suspicious: "warn",
perf: "warn",
},
rules: {
"unicorn/no-array-sort": "off",
"unicorn/consistent-function-scoping": "off",
"oxc/no-map-spread": "off",
"react-in-jsx-scope": "off",
"react-hooks/exhaustive-deps": "off",
"eslint/no-shadow": "off",
"eslint/no-await-in-loop": "off",
"eslint/no-underscore-dangle": "off",
"typescript/consistent-return": "off",
"typescript/no-base-to-string": "off",
"typescript/no-duplicate-type-constituents": "off",
"typescript/no-floating-promises": "off",
"typescript/no-implied-eval": "off",
"typescript/no-meaningless-void-operator": "off",
"typescript/no-redundant-type-constituents": "off",
"typescript/no-unnecessary-boolean-literal-compare": "off",
"typescript/no-unnecessary-type-conversion": "off",
"typescript/no-unnecessary-type-arguments": "off",
"typescript/no-unnecessary-type-assertion": "off",
"typescript/no-unnecessary-type-parameters": "off",
"typescript/no-unsafe-type-assertion": "off",
"typescript/await-thenable": "off",
"typescript/require-array-sort-compare": "off",
"typescript/restrict-template-expressions": "off",
"typescript/unbound-method": "off",
"eslint/no-restricted-imports": [
"error",
{ paths: [...RESTRICTED_IMPORT_PATHS, RESTRICTED_PULL_REQUEST_GLYPH_IMPORTS] },
],
"t3code/no-global-process-runtime": "error",
"t3code/no-inline-schema-compile": "warn",
"t3code/no-manual-effect-runtime-in-tests": "error",
"t3code/no-native-title-tooltip": "error",
"t3code/namespace-node-imports": "error",
},
overrides: [
{
// The one place that reads the host platform to seed the injected references.
files: ["packages/shared/src/hostProcess.ts"],
rules: { "t3code/no-global-process-runtime": "off" },
},
{
// The one module allowed to name lucide's pull-request glyphs; everything else picks
// from its vocabulary. The other import restrictions still apply here.
files: ["apps/web/src/components/pullRequest/pullRequestIcons.tsx"],
rules: { "eslint/no-restricted-imports": ["error", { paths: RESTRICTED_IMPORT_PATHS }] },
},
{
files: ["apps/mobile/src/**"],
rules: { "t3code/no-mobile-uniwind-theme-escape-hatches": "error" },
},
{
// components/ui exports own their look. App code picks a variant or size instead
// of restyling with className; layout classes (width, flex, margin, position) stay
// allowed because placement belongs to the parent. Warn-only until the existing
// overrides are migrated to variants; the ceiling below stops the count growing.
files: ["apps/web/src/**"],
excludeFiles: ["apps/web/src/components/ui/**"],
rules: {
"shadcn/no-restyle": [
"warn",
{
allow: ["layout"],
contracts: [
{
// CollapsibleTrigger is a bare button with no styled counterpart
// (a disclosure row is not a Button), so its className is the API.
// Every other trigger has one: style them with render={<Button …/>}.
pattern: "^CollapsibleTrigger$",
allow: ["layout", "color", "typography", "spacing", "shape", "effects", "motion"],
},
],
},
],
},
},
{
// Shared client code must not call APIs missing from Hermes. Our ESNext
// TypeScript target accepts them even when they would crash mobile at launch.
// Tests run on Node and are exempt.
files: [
"apps/mobile/src/**",
"packages/client-runtime/src/**",
"packages/contracts/src/**",
"packages/shared/src/**",
],
excludeFiles: ["**/*.test.ts", "**/*.test.tsx"],
rules: { "t3code/no-hermes-unsupported-apis": "error" },
},
{
// Reviewed native and third-party interop boundaries that cannot consume a className.
files: [
"apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx",
"apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx",
"apps/mobile/src/features/files/FileMarkdownPreview.tsx",
"apps/mobile/src/features/files/SourceFileSurface.tsx",
"apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx",
"apps/mobile/src/features/files/AttachmentFileScreen.tsx",
"apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx",
"apps/mobile/src/features/files/thread-file-navigator-pane.tsx",
"apps/mobile/src/features/home/HomeHeader.tsx",
"apps/mobile/src/features/review/ReviewSheet.tsx",
"apps/mobile/src/features/review/useNativeReviewDiffBridge.ts",
"apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx",
"apps/mobile/src/features/settings/appearance/components/AppearancePreviews.tsx",
"apps/mobile/src/features/threads/GitActionProgressOverlay.tsx",
"apps/mobile/src/features/threads/NewTaskDraftScreen.tsx",
"apps/mobile/src/features/threads/ThreadComposer.tsx",
"apps/mobile/src/features/threads/ThreadFeed.tsx",
"apps/mobile/src/features/review/ReviewCommentCard.tsx",
"apps/mobile/src/features/threads/ThreadSettingsSheet.tsx",
"apps/mobile/src/features/threads/git/GitOverviewSheet.tsx",
"apps/mobile/src/features/threads/thread-list-items.tsx",
"apps/mobile/src/features/threads/thread-list-v2-items.tsx",
"apps/mobile/src/lib/useMobileNavigationTheme.ts",
"apps/mobile/src/native/T3ComposerEditor.ios.tsx",
"apps/mobile/src/native/T3ComposerEditor.native.tsx",
"apps/mobile/src/native/SelectableMarkdownText.android.tsx",
],
rules: {
"t3code/no-mobile-uniwind-theme-escape-hatches": ["error", { allowUniwindTheme: true }],
},
},
// Legacy manual Effect runners tracked as debt: no net-new occurrences.
// Lower a ceiling when you migrate a file, and delete its entry at zero.
...Object.entries({
"apps/server/src/orchestration/Layers/CheckpointReactor.test.ts": 42,
"apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts": 5,
"apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts": 4,
"apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts": 66,
"apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts": 29,
"apps/server/src/orchestration/Layers/ThreadDeletionReactor.test.ts": 2,
"apps/server/src/orchestration/commandInvariants.test.ts": 5,
"apps/server/src/orchestration/projector.test.ts": 20,
"apps/server/src/provider/Layers/CodexAdapter.test.ts": 1,
"apps/server/src/provider/Layers/CodexSessionRuntime.test.ts": 5,
"apps/server/src/provider/Layers/CursorAdapter.test.ts": 1,
"apps/server/src/provider/Layers/CursorProvider.test.ts": 1,
"apps/server/src/provider/Layers/ProviderService.test.ts": 2,
"apps/server/src/provider/Layers/ProviderSessionReaper.test.ts": 12,
"apps/server/src/provider/acp/CursorAcpSupport.test.ts": 1,
}).map(([file, maxOccurrences]) => {
const rule: ["error", { maxOccurrences: number }] = ["error", { maxOccurrences }];
return { files: [file], rules: { "t3code/no-manual-effect-runtime-in-tests": rule } };
}),
],
options: {
reportUnusedDisableDirectives: "error",
// Revisit once Oxlint's tsgolint path can integrate with @effect/tsgo diagnostics.
typeAware: false,
typeCheck: false,
},
},
});