Put code next to its only consumer; promote on a second real consumer. Domain stays outside app/; React/OpenTUI stays inside app/.
| Kind of code | Where it goes | Must not |
|---|---|---|
| Domain logic (pure, no OpenTUI) | Top-level src/<domain>/ — today: connections, parser, query-patterns, indexes, live-ops, live-connection, log-tail, profiler, replication |
Import from app/ |
| Domain types | Colocated types.ts in that domain folder (e.g. src/connections/types.ts) |
Central lib/types.ts dump |
| Cross-domain pure utils | src/lib/ by concern |
Import domain modules |
| OpenTUI shell, screens, stores, shortcuts, theme, app config | src/app/ |
Own domain business rules |
| TUI-only helpers | src/app/lib/ |
Live in src/lib/ |
| React Query / UI data hooks | src/app/queries/ |
Sit at top-level src/queries/ |
| Feature screens + feature-private presentation | src/app/components/<feature>/<name>/ |
Premature extract to shared |
| Shared widgets / primitives (2+ features) | src/app/components/<name>/ or ui/ |
Feature-private helpers |
| Process entry (yargs) | src/cli/ |
App UI |
| Tests | Colocated *.test.ts next to source |
Separate tests/ tree |
Reuse ladder:
Used by one feature screen? → app/components/<feature>/
Used by 2+ features? → app/components/ or app/components/ui/
Used by domain + app, pure? → src/lib/
Domain-only helper? → that domain folder
Every non-ui/ component lives in its own kebab-case folder with an index.ts barrel (same rule as coa-erp-portal shared components):
src/app/components/<component-name>/
├── index.ts # export { ComponentName } from './component-name'
└── component-name.tsx
Feature screens follow the same pattern under a feature prefix:
src/app/components/connections/connections-dialog/
├── index.ts
└── connections-dialog.tsx
src/app/components/ui/ stays flat — import primitives by file (ui/dialog), not folder barrels.
Define a named props type (expanded, one field per line) and destructure it in the component signature. Rename when a prop collides with a local binding.
// ❌ BAD
export function ThemeProvider(props: { mode?: ThemeMode; theme?: string; children?: ReactNode }) {
return <>{props.children}</>
}
// ✅ GOOD
type ThemeProviderProps = {
mode?: ThemeMode
theme?: string
children?: ReactNode
}
export function ThemeProvider({ mode, theme: themeName, children }: ThemeProviderProps) {
return <>{children}</>
}Always pass a named function to useEffect and useLayoutEffect. The name must convey the effect’s role (what it synchronizes, subscribes to, or applies).
// ❌ BAD — anonymous arrow hides intent in stacks and reviews
useEffect(() => {
renderer.setBackgroundColor(theme.background)
}, [renderer, theme.background])
// ✅ GOOD — name states the effect’s job
useEffect(
function applyThemeBackground() {
renderer.setBackgroundColor(theme.background)
},
[renderer, theme.background],
)Do not use anonymous arrow functions or unnamed function expressions for effect callbacks.
In a component module, the exported component is the first function/component in the file (after imports and module-level constants/types for that export). Private helpers and subcomponents used by it follow below, each with their props type colocated above the component.
// imports
// module constants
type WelcomeScreenProps = {
logDir: string
}
export function WelcomeScreen({ logDir }: WelcomeScreenProps) {
// ...
}
function progressBar(percent: number): string {
// ...
}
type LogFileSectionProps = {
// ...
}
function LogFileSection({ ... }: LogFileSectionProps) {
// ...
}docs/ is a separate Next.js / Fumadocs site. It is exempt from the TUI-authored component-folder, props-destructure, and named-useEffect rules above — those target React/OpenTUI under src/app/. Docs follows framework-native conventions instead: default exports for Next-generated layout/page props, flat route-private _components/, and Tailwind (including fixed OS-brand color identifiers where those colors are not themeable UI tokens). Shared docs widgets under docs/src/components/ may still use kebab folders + barrels when that fits, but that is optional style for the docs package, not the TUI reuse ladder.
MongoScope is a single-package terminal UI (TUI) CLI for visualizing MongoDB logs. It has no HTTP server or network port. The toolchain is Bun (runtime + package manager); Bun lives at ~/.bun/bin and is on PATH via ~/.bashrc. All commands are defined in package.json scripts.
Non-obvious caveats for running/testing:
bun start(alias forbun bin/mongoscope) renders a full-screen OpenTUI interface and requires a real interactive TTY. It will not render correctly if stdout is piped or run non-interactively; test it from an actual terminal (e.g. the Desktop pane). Key bindings:Ctrl+Kcommand palette,mtoggle light/dark,tcycle themes,qquit.--uri/--host/--port(and related creds) resolve to a session-only ephemeral live connect (not saved to the OS keychain).--log-pathparses that log file on startup (static Slow Queries / Logs). Live tabs need a reachable MongoDB; static log analysis does not.bun run testruns Vitest against colocated*.test.tsfiles next to their source (see folder placement table). Preferbun run testfor the full suite.- The
lintandformatscripts mutate files (oxlint --fix,oxfmt --write). For check-only runs usebunx oxlintandbunx oxfmt --check. bun run buildcompiles a standalone binary todist/mongoscope(~110MB, gitignored).