Skip to content

Commit 39b066b

Browse files
committed
🤖 feat: add ESLint rule to enforce folder boundaries
Add 'no-cross-boundary-imports' rule to prevent architectural violations: - browser/ cannot import from node/ - node/ cannot import from desktop/ or cli/ - cli/ cannot import from browser/ - desktop/ cannot import from browser/ - All folders can import from common/ - Type-only imports allowed for DI patterns Fixes all 17 violations by moving shared code to common/: - Move telemetry module (browser-only PostHog client) - Move git utilities (pure parsers, no Node dependencies) - Move error formatting utilities - Move compaction utilities (uses localStorage/window.api) - Move mode utilities (pure logic) - Create common model defaults for CLI use _Generated with `mux`_
1 parent a55417a commit 39b066b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+136
-30
lines changed

eslint.config.mjs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,93 @@ const localPlugin = {
106106
};
107107
},
108108
},
109+
"no-cross-boundary-imports": {
110+
meta: {
111+
type: "problem",
112+
docs: {
113+
description: "Enforce folder boundaries to prevent architectural violations",
114+
},
115+
messages: {
116+
browserToNode:
117+
"browser/ cannot import from node/. Move shared code to common/ or use IPC.",
118+
nodeToDesktop:
119+
"node/ cannot import from desktop/. Move shared code to common/ or use dependency injection.",
120+
nodeToCli:
121+
"node/ cannot import from cli/. Move shared code to common/.",
122+
cliToBrowser:
123+
"cli/ cannot import from browser/. Move shared code to common/.",
124+
desktopToBrowser:
125+
"desktop/ cannot import from browser/. Move shared code to common/.",
126+
},
127+
},
128+
create(context) {
129+
return {
130+
ImportDeclaration(node) {
131+
// Allow type-only imports (for DI patterns)
132+
if (node.importKind === "type") {
133+
return;
134+
}
135+
136+
const sourceFile = context.filename;
137+
const importPath = node.source.value;
138+
139+
// Extract folder from source file (browser, node, desktop, cli, common)
140+
const sourceFolderMatch = sourceFile.match(/\/src\/(browser|node|desktop|cli|common)\//);
141+
if (!sourceFolderMatch) return;
142+
const sourceFolder = sourceFolderMatch[1];
143+
144+
// Extract folder from import target
145+
// Handle relative imports (e.g., '../node/...')
146+
let targetFolder = null;
147+
if (importPath.startsWith("../")) {
148+
const targetMatch = importPath.match(/\.\.\/(browser|node|desktop|cli|common)\//);
149+
if (targetMatch) {
150+
targetFolder = targetMatch[1];
151+
}
152+
} else if (importPath.startsWith("@/")) {
153+
// Handle alias imports (e.g., '@/node/...')
154+
const targetMatch = importPath.match(/@\/(browser|node|desktop|cli|common)\//);
155+
if (targetMatch) {
156+
targetFolder = targetMatch[1];
157+
}
158+
}
159+
160+
if (!targetFolder) return;
161+
162+
// Allow imports from common
163+
if (targetFolder === "common") return;
164+
165+
// Check for violations
166+
if (sourceFolder === "browser" && targetFolder === "node") {
167+
context.report({
168+
node,
169+
messageId: "browserToNode",
170+
});
171+
} else if (sourceFolder === "node" && targetFolder === "desktop") {
172+
context.report({
173+
node,
174+
messageId: "nodeToDesktop",
175+
});
176+
} else if (sourceFolder === "node" && targetFolder === "cli") {
177+
context.report({
178+
node,
179+
messageId: "nodeToCli",
180+
});
181+
} else if (sourceFolder === "cli" && targetFolder === "browser") {
182+
context.report({
183+
node,
184+
messageId: "cliToBrowser",
185+
});
186+
} else if (sourceFolder === "desktop" && targetFolder === "browser") {
187+
context.report({
188+
node,
189+
messageId: "desktopToBrowser",
190+
});
191+
}
192+
},
193+
};
194+
},
195+
},
109196
},
110197
};
111198

@@ -265,6 +352,7 @@ export default defineConfig([
265352
// Safe Node.js patterns
266353
"local/no-unsafe-child-process": "error",
267354
"local/no-sync-fs-methods": "error",
355+
"local/no-cross-boundary-imports": "error",
268356

269357
// Allow console for this app (it's a dev tool)
270358
"no-console": "off",
@@ -410,6 +498,8 @@ export default defineConfig([
410498
// This file is only used by Node.js code (cli/debug) but lives in common/
411499
// TODO: Consider moving to node/utils/
412500
"src/common/utils/providers/ensureProvidersConfig.ts",
501+
// Telemetry uses defensive process checks for test environments
502+
"src/common/telemetry/**",
413503
],
414504
rules: {
415505
"no-restricted-globals": [

src/browser/components/ChatInput/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ import {
5858
import type { ThinkingLevel } from "@/common/types/thinking";
5959
import type { MuxFrontendMetadata } from "@/common/types/message";
6060
import { useTelemetry } from "@/browser/hooks/useTelemetry";
61-
import { setTelemetryEnabled } from "@/node/telemetry";
61+
import { setTelemetryEnabled } from "@/common/telemetry";
6262
import { getTokenCountPromise } from "@/browser/utils/tokenizer/rendererClient";
6363
import { CreationCenterContent } from "./CreationCenterContent";
6464
import { CreationControls } from "./CreationControls";

src/browser/components/ChatInputToasts.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Toast } from "./ChatInputToast";
33
import { SolutionLabel } from "./ChatInputToast";
44
import type { ParsedCommand } from "@/browser/utils/slashCommands/types";
55
import type { SendMessageError as SendMessageErrorType } from "@/common/types/errors";
6-
import { formatSendMessageError } from "@/node/utils/errors/formatSendError";
6+
import { formatSendMessageError } from "@/common/utils/errors/formatSendError";
77

88
/**
99
* Creates a toast message for command-related errors and help messages

src/browser/components/GitStatusIndicatorView.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Meta, StoryObj } from "@storybook/react-vite";
22
import { expect, userEvent, waitFor } from "storybook/test";
33
import { GitStatusIndicatorView } from "./GitStatusIndicatorView";
4-
import type { GitCommit, GitBranchHeader } from "@/node/utils/git/parseGitLog";
4+
import type { GitCommit, GitBranchHeader } from "@/common/utils/git/parseGitLog";
55
import { useState } from "react";
66

77
// Type for the wrapped component props (without interaction handlers)

src/browser/components/GitStatusIndicatorView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from "react";
22
import { createPortal } from "react-dom";
33
import type { GitStatus } from "@/common/types/workspace";
4-
import type { GitCommit, GitBranchHeader } from "@/node/utils/git/parseGitLog";
4+
import type { GitCommit, GitBranchHeader } from "@/common/utils/git/parseGitLog";
55
import { cn } from "@/common/lib/utils";
66

77
// Helper for indicator colors

src/browser/components/Messages/ChatBarrier/RetryBarrier.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
isEligibleForAutoRetry,
1010
isNonRetryableSendError,
1111
} from "@/browser/utils/messages/retryEligibility";
12-
import { formatSendMessageError } from "@/node/utils/errors/formatSendError";
12+
import { formatSendMessageError } from "@/common/utils/errors/formatSendError";
1313
import { createManualRetryState, calculateBackoffDelay } from "@/browser/utils/messages/retryState";
1414

1515
interface RetryBarrierProps {

src/browser/components/RightSidebar/CodeReview/FileTree.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44

55
import React from "react";
6-
import type { FileTreeNode } from "@/node/utils/git/numstatParser";
6+
import type { FileTreeNode } from "@/common/utils/git/numstatParser";
77
import { usePersistedState } from "@/browser/hooks/usePersistedState";
88
import { getFileTreeExpandStateKey } from "@/common/constants/storage";
99
import { cn } from "@/common/lib/utils";

src/browser/components/RightSidebar/CodeReview/ReviewPanel.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ import { ReviewControls } from "./ReviewControls";
2828
import { FileTree } from "./FileTree";
2929
import { usePersistedState } from "@/browser/hooks/usePersistedState";
3030
import { useReviewState } from "@/browser/hooks/useReviewState";
31-
import { parseDiff, extractAllHunks } from "@/node/utils/git/diffParser";
31+
import { parseDiff, extractAllHunks } from "@/common/utils/git/diffParser";
3232
import { getReviewSearchStateKey } from "@/common/constants/storage";
3333
import { Tooltip, TooltipWrapper } from "@/browser/components/Tooltip";
34-
import { parseNumstat, buildFileTree, extractNewPath } from "@/node/utils/git/numstatParser";
34+
import { parseNumstat, buildFileTree, extractNewPath } from "@/common/utils/git/numstatParser";
3535
import type { DiffHunk, ReviewFilters as ReviewFiltersType } from "@/common/types/review";
36-
import type { FileTreeNode } from "@/node/utils/git/numstatParser";
36+
import type { FileTreeNode } from "@/common/utils/git/numstatParser";
3737
import { matchesKeybind, KEYBINDS, formatKeybind } from "@/browser/utils/ui/keybinds";
3838
import { applyFrontendFilters } from "@/browser/utils/review/filterHunks";
3939
import { cn } from "@/common/lib/utils";

src/browser/components/TitleBar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { cn } from "@/common/lib/utils";
33
import { VERSION } from "@/version";
44
import { TooltipWrapper, Tooltip } from "./Tooltip";
55
import type { UpdateStatus } from "@/common/types/ipc";
6-
import { isTelemetryEnabled } from "@/node/telemetry";
6+
import { isTelemetryEnabled } from "@/common/telemetry";
77

88
// Update check intervals
99
const UPDATE_CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000; // 4 hours

src/browser/components/hooks/useGitBranchDetails.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
parseGitShowBranch,
66
type GitCommit,
77
type GitBranchHeader,
8-
} from "@/node/utils/git/parseGitLog";
8+
} from "@/common/utils/git/parseGitLog";
99

1010
const GitBranchDataSchema = z.object({
1111
showBranch: z.string(),

0 commit comments

Comments
 (0)