Module Boundary Enforcement

This document explains how module boundaries are technically enforced in the admin panel, not just by convention.

Why Technical Enforcement?

“At scale, discipline fails.”
Your architecture relies on clear boundaries:
  • packages/ui - Pure UI components
  • packages/core - Business logic and API calls
  • apps/panel - Application that uses both
Without enforcement, developers can accidentally:
  • ❌ Import @monorepo/solid-pkg-ui from packages/core
  • ❌ Import @monorepo/solid-pkg-core from packages/ui
  • ❌ Use deep imports like @monorepo/solid-pkg-ui/src/components/Button

ESLint Rules

We use ESLint’s no-restricted-imports rule to enforce boundaries automatically.

Rule 1: UI Cannot Import Core

// ❌ WRONG - This will fail ESLint
// In packages/ui/src/components/UserCard.tsx
import { useUser } from '@monorepo/solid-pkg-core';

export function UserCard() {
  const user = useUser(); // ❌ UI should not know about business logic
  return <div>{user.name}</div>;
}
// ✅ CORRECT - Pass data as props
// In packages/ui/src/components/UserCard.tsx
interface UserCardProps {
  userName: string;
}

export function UserCard(props: UserCardProps) {
  return <div>{props.userName}</div>;
}

// In apps/panel/src/routes/users.tsx
import { UserCard } from '@monorepo/solid-pkg-ui';
import { useUser } from '@monorepo/solid-pkg-core';

export default function UsersPage() {
  const user = useUser();
  return <UserCard userName={user.data?.name} />;
}
Error Message:
❌ UI package cannot import from core package.
Keep UI components pure and framework-agnostic!
If you need business logic, pass it as props or use dependency injection.

Rule 2: Core Cannot Import UI

// ❌ WRONG - This will fail ESLint
// In packages/core/src/services/userService.ts
import { Button } from '@monorepo/solid-pkg-ui';

export function createUser() {
  // ❌ Business logic should not depend on UI
  return <Button>Create</Button>;
}
// ✅ CORRECT - Keep business logic UI-agnostic
// In packages/core/src/services/userService.ts
export async function createUser(userData: UserData) {
  return apiClient.post('/users', userData);
}

// In apps/panel/src/routes/users.tsx
import { Button } from '@monorepo/solid-pkg-ui';
import { createUser } from '@monorepo/solid-pkg-core';

export default function CreateUserPage() {
  const handleCreate = () => createUser(formData);
  return <Button onClick={handleCreate}>Create</Button>;
}
Error Message:
❌ Core package cannot import from UI package.
Business logic should be UI-agnostic!
If you need UI components, move the logic to the app layer.

Rule 3: No Deep Imports

// ❌ WRONG - Deep imports bypass the public API
import { Button } from "@monorepo/solid-pkg-ui/src/components/Button";
import { apiClient } from "@monorepo/solid-pkg-core/src/api/client";
// ✅ CORRECT - Use the public API
import { Button } from "@monorepo/solid-pkg-ui";
import { apiClient } from "@monorepo/solid-pkg-core";
Error Message:
❌ Deep imports are not allowed!
Use the public API instead:
  import { Component } from "@monorepo/solid-pkg-ui"
  import { hook } from "@monorepo/solid-pkg-core"

Running the Linter

Check for Violations

cd admin
pnpm lint

Auto-Fix (where possible)

cd admin
pnpm lint:fix

In CI/CD

Add to your CI pipeline:
# .github/workflows/ci.yml
- name: Lint Admin Panel
  run: |
    cd admin
    pnpm lint

Architecture Diagram

┌─────────────────────────────────────┐
│         apps/panel                  │
│  (Can import from both)             │
│  ✅ import from @monorepo/solid-pkg-ui  │
│  ✅ import from @monorepo/solid-pkg-core│
└─────────────────────────────────────┘
           │                │
           │                │
           ▼                ▼
┌──────────────────┐  ┌──────────────────┐
│  packages/ui     │  │  packages/core   │
│  (UI only)       │  │  (Logic only)    │
│  ❌ Cannot import│  │  ❌ Cannot import│
│     from core    │  │     from ui      │
└──────────────────┘  └──────────────────┘

Benefits

1. Prevents Circular Dependencies ✅

Without enforcement:
ui → core → ui → core → ... (infinite loop)
With enforcement:
app → ui
app → core
(No cycles possible)

2. Enforces Clean Architecture ✅

  • UI Layer: Presentation only
  • Core Layer: Business logic only
  • App Layer: Orchestration

3. Catches Mistakes Early ✅

  • ✅ Fails in development (IDE shows errors)
  • ✅ Fails in CI (prevents bad PRs)
  • ✅ Clear error messages (explains why it’s wrong)

4. Self-Documenting ✅

The ESLint config serves as documentation:
// This rule documents the architecture
'no-restricted-imports': [
  'error',
  {
    patterns: ['@monorepo/solid-pkg-core'],
    message: 'UI cannot import from core',
  },
],

Common Scenarios

Scenario 1: UI Component Needs Data

❌ Wrong:
// packages/ui/src/components/UserList.tsx
import { useUsers } from '@monorepo/solid-pkg-core'; // ❌ Violates boundary

export function UserList() {
  const users = useUsers();
  return <div>{users.data?.map(...)}</div>;
}
✅ Correct:
// packages/ui/src/components/UserList.tsx
interface UserListProps {
  users: User[];
  isLoading: boolean;
}

export function UserList(props: UserListProps) {
  if (props.isLoading) return <div>Loading...</div>;
  return <div>{props.users.map(...)}</div>;
}

// apps/panel/src/routes/users.tsx
import { UserList } from '@monorepo/solid-pkg-ui';
import { useUsers } from '@monorepo/solid-pkg-core';

export default function UsersPage() {
  const users = useUsers();
  return <UserList users={users.data ?? []} isLoading={users.isLoading} />;
}

Scenario 2: Core Needs to Show UI

❌ Wrong:
// packages/core/src/services/notificationService.ts
import { Toast } from '@monorepo/solid-pkg-ui'; // ❌ Violates boundary

export function showNotification(message: string) {
  return <Toast>{message}</Toast>;
}
✅ Correct:
// packages/core/src/services/notificationService.ts
export interface Notification {
  id: string;
  message: string;
  type: 'success' | 'error';
}

export function createNotification(message: string): Notification {
  return { id: nanoid(), message, type: 'success' };
}

// apps/panel/src/components/NotificationProvider.tsx
import { Toast } from '@monorepo/solid-pkg-ui';
import { createNotification } from '@monorepo/solid-pkg-core';

export function NotificationProvider() {
  const notification = createNotification('Success!');
  return <Toast>{notification.message}</Toast>;
}

Configuration

The ESLint config is located at:
admin/eslint.config.js
To modify the rules, edit this file and run:
cd admin
pnpm lint