Files
CherryHQ-cherry-studio/docs/references/main-process-architecture.md
fullex 87b2b64ba5 refactor(process-entry): rename main/preload entries to named files per naming §6.4
src/main/index.ts → main.ts and src/preload/index.ts → preload.ts, so the process entries stop occupying the barrel-reserved index filename (§6.4) without needing an exemption list. The rename is wired through electron.vite (main build.lib.entry — rollupOptions.input would bypass electron-vite's output-format detection — and the preload input key), package.json main (out/main/main.js), WindowManager's default preload filename, MainWindowService's webview preload path, and DbService's slow-query stack filter, which matches the bundled artifact name. preload.d.ts becomes globals.d.ts: sharing preload.ts's basename makes TypeScript drop the .d.ts as that file's presumed output declaration, and tsgo only applies its global Window augmentation when the declaration sorts before its importer, so the file keeps a distinct, alphabetically-early name. Entry-path mentions in code comments and reference docs follow.
2026-07-04 20:18:14 -07:00

14 KiB

Main Process Architecture (src/main)

This is the canonical reference for how src/main/ is organized: what each top-level directory is for, the rules that keep them from sprawling, and how they depend on each other. It is the main-process peer of Renderer Architecture and Shared Layer Architecture; the cross-process picture (process model, monorepo tree) lives in Architecture Overview.

The top level is a closed, locked set of principled categories, not an open list of modules. Each top-level directory holds one kind of thing and earns its place for a distinct reason. The set is locked — a new capability is always routed into an existing category by its nature, never given a new top-level directory (§4).

1. The Closed Top-Level Set

Exactly these, each with a single charter:

Dir Category Why it earns a top-level home
core App runtime Business-agnostic infrastructure concerned only with running the app. The test: lift core/ onto a different Electron app, add other business code, and you have a different application. One kind of thing — the app substrate: lifecycle / DI container, path registry, logger, window manager, scheduler & jobs, preboot, diagnostics.
ipc Cross-process boundary Electron's defining inter-process mechanism — special and important enough to stand alone. Unified as IpcApi (schema + router + handler): the single typed boundary between main and renderer.
data Data layer The general business-data store — a first-class data layer, hence independent. Holds DbService / CacheService / PreferenceService / DataApiService / BootConfig, DB schemas, and the v1→v2 migrators (which by design read domain data — throwaway migration code). Detailed in Data System Reference.
ai Core domain Cherry Studio is an AI client, so AI earns its own top-level home: everything tied to the AI essence lives here (providers, middleware, MCP, agents, stream manager). Mirrors @shared/ai.
features Domain modules Business domains, one directory each. A complex domain bundles its own related services / utils / etc. under features/<domain>/.
services Business services Business feature services. A simple service is a single file; a larger one is organized into its own subdirectory.
utils Stateless helpers Cross-domain stateless, domain-agnostic functions with no single owner. "Stateless" is the bar, not "pure": a helper may reach infra through the ambient @application / @logger (§3); it just owns no state and performs no outward side effects (§2).
i18n Main-process localization Main's own locale catalog (locales/ human + translate/ machine) and its t() / getI18n() resolver. A deliberate, governed expansion of the closed set (§4), mirroring src/renderer/i18n/ so each process owns an independent catalog; the utils/i18n/ alternative was rejected for that cross-process symmetry.

Entry files: main.ts (process entry — runs preboot, then application.bootstrap(); a named file, since index is reserved for barrels per Naming §6.4) and ipc.ts (legacy IPC registration, being retired into ipc/).

Naming follows Naming Conventions §4.9: core / data / ai / ipc / i18n are singular namespaces; features / services / utils are plural buckets.

src/main/
├── main.ts      # process entry: preboot → application.bootstrap()
├── ipc.ts       # legacy IPC registration (being retired into ipc/)
├── core/        # business-agnostic app runtime (lifecycle/DI, paths, logger, window, scheduler/job, preboot)
├── ipc/         # IpcApi — the typed main↔renderer boundary
├── data/        # the data layer (DB/Cache/Preference/DataApi/BootConfig, schemas, migration)
├── ai/          # the AI subsystem — the product's core domain
├── features/    # business domains, one dir each (each bundles its own services/utils)
├── services/    # business feature services (single file, or a subdirectory)
└── utils/       # cross-domain stateless helpers

2. features vs services (Placement)

services/ and features/ are the same kind of thing — business logic — at two sizes. The split follows the cross-process rule in Naming Conventions §4.10:

  • Promotion, not default — and in steps. A small, self-contained service starts as a single file at the bucket root — services/<Topic>Service.ts (a stateful Service/Manager class is PascalCase matching its class name, Naming §5.2; a generic helper is utils/<topic>.ts — a topic-specific one stays inline until the subdirectory step below). When one file can no longer hold it, grow it in place into a camelCase topic subdirectory first — services/<topic>/ holding <Topic>Service.ts plus its helpers — not straight into a feature. Mind the shape: the directory is the topic name and carries no Service suffix (Naming §4.5); only the class file keeps the suffix (e.g. services/webSearch/WebSearchService.ts). It earns a features/<domain>/ home only once it grows into a large, multi-file domain bundling its own services, utils, and helpers (knowledge, apiGateway, fileProcessing). Do not pre-create a subdirectory or a feature for an anticipated module.
  • ai/ is not an ordinary feature. It is the product's core domain and has its own top-level home (§1); it is foundational, not one domain among many.
  • Route by role (Naming Conventions §5.2): a stateful class owning long-lived resources or persistent side effects → a lifecycle Service (Lifecycle Reference); a stateless module → utils/ by default, promoted to services/ only by outward side effects or a forced upward dependency (§5.2 routing table); a large domain → features/<domain>/. Reads never promote — a helper touching infra for queries is still a helper.

2.1 Subdirectories and Barrels

A single .ts file is the default; promote a topic to a subdirectory only when it actually owns multiple files. Barrels follow Naming §6.4 (the cross-process authority), applied here to services/ and utils/:

  • The bucket roots services/ and utils/ have no index.ts. A bucket is a category, not a module — import the specific file or topic, never the whole bucket.
  • A services/<topic>/ subdirectory has exactly one index.ts as its public API (explicit named exports, no export *); its other files stay private behind it.
  • A complex utils/<topic>/ subdirectory likewise has one index.ts.
  • Why: each topic is then imported through a single public entry — exactly like a one-file module — so its internal files stay private and consumers never deep-import. For utils/, where file and directory share the topic name, the specifier (@main/utils/<topic>) is even unchanged when a file grows into a folder.

(A features/<domain>/ is the same single-entry idea one tier up: consumers import the domain through its one public entry, not its internal files.)

3. Dependency Direction

The charters imply a direction; dependencies flow toward the business-agnostic foundation:

  • Foundationcore/ and utils/ carry no business knowledge; nothing business sits below them.
  • Data layerdata/ is the storage layer above the foundation.
  • Businessai/, features/, and services/ are the business tier; they depend down on data/, core/, and utils/.
  • ai/ is foundational within the business tier: features/ and services/ may depend on it; it must not import a feature.
  • Feature domains are mutually isolated: a features/<domain>/ must not import a sibling feature — share through services/, ai/, data/, or @shared.
  • ipc/ is the boundary adapter: it resolves services through the DI container (application.get) rather than importing domain modules directly.
  • No renderer imports: src/main and src/preload must not import renderer code. Cross-process types live in @shared, main-only types stay in src/main — see Shared Layer Architecture for placement. Enforced by an ESLint no-restricted-imports rule banning @renderer in src/main + src/preload; §7 tracks the one remaining exception.

Two dependencies cut across every directory and are not layering edges — they are ambient infrastructure access: @logger (logging) and @application (the DI container / service locator). A raw import scan shows almost everything "depending on core" only because of these two; the rules above concern direct module imports between domains.

There is no automated enforcement of the internal direction edges above yet (unlike the import/no-restricted-paths zones proposed for the renderer in Renderer Architecture §5); direction is held by convention and review. The external main↔renderer boundary (no renderer imports) is enforced — see the rule above.

4. Closed Top-Level Governance

The top-level set is closed and locked — treat adding a new directory under src/main/ as off the table. This is Naming Conventions §4.8 (top-level closed by default) at its strict end: §4.8 admits a new top-level directory only on proven necessity (no existing category can host the files) and completeness, and main's categories already span the space — so a new capability is routed into an existing category, never given its own directory. The one deliberate expansion is i18n/ (§1), added so the main process owns a locale catalog symmetric with src/renderer/i18n/; it is a governed exception with a recorded rationale, not a loosening of the rule. The renderer (§6) and @shared (§2) top levels are held to the same governance.

A new capability never earns a new top-level directory; route it by nature:

The capability is… Home
tied to the AI essence ai/
business data / storage data/
an IPC route ipc/ (IpcApi)
business-agnostic app-runtime infra core/
a business service services/ — or features/<domain>/ if it is a large, multi-file domain
pure, domain-agnostic logic utils/

5. Anti-Patterns

  • Business code (anything specific to what Cherry Studio does) placed in core/core/ must stay app-runtime-only.
  • A features/<domain>/ importing a sibling feature (cross-domain coupling).
  • ai/ importing features/ (the core domain depending up on a feature).
  • Opening a new top-level directory for a single capability (§4).
  • Scattering business data through ad-hoc storage instead of the data/ subsystems, or imperative commands through ad-hoc channels instead of ipc/ (IpcApi).

6. Subsystem References

Per-subsystem depth lives in dedicated docs; this page owns only the directory layout. Do not duplicate subsystem detail here.

Subsystem Location Reference
Service lifecycle (IoC, phased bootstrap) core/lifecycle/, core/application/ Lifecycle Reference
Startup phases (preboot / bootstrap / running) core/preboot/, core/application/ core/README
Window manager core/window/ Window Manager Reference
Scheduler & jobs core/scheduler/, core/job/ Job & Scheduler Reference
Path registry core/paths/ paths/README
Data systems (DB/Cache/Preference/DataApi/BootConfig) data/ Data System Reference
IPC (IpcApi) ipc/ IPC Reference
AI subsystem ai/ AI Reference

7. Current Deviations (target vs current)

This page describes the target. Where current code does not yet match it, the gap is tracked below — this pass changes no code. Only structural deviations are listed (the closed top-level set §4, bucket barrels §2.1, placement §2); a per-file naming-suffix audit (§5.2) is out of scope here.

Area Current Target
utils/index.ts a bucket-root index.ts holds loose helpers (debounce, makeSureDirExists, toAsarUnpackedPath, …) — the junk-drawer root barrel §2.1 forbids split into named topic files (utils/<topic>.ts); @shared has already done exactly this — see Shared Layer Architecture §6
legacy ipc.ts v1 IPC registration at the process root, coexisting with IpcApi domains migrate into ipc/ (IpcApi) incrementally until ipc.ts is retired (§1)

By-design edges are not deviations and are deliberately omitted: the data/migration/v2/ migrators reading domain data (§1) and @logger / @application ambient access from any tier (§3).