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.
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 statefulService/Managerclass isPascalCasematching its class name, Naming §5.2; a generic helper isutils/<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 acamelCasetopic subdirectory first —services/<topic>/holding<Topic>Service.tsplus its helpers — not straight into a feature. Mind the shape: the directory is the topic name and carries noServicesuffix (Naming §4.5); only the class file keeps the suffix (e.g.services/webSearch/WebSearchService.ts). It earns afeatures/<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 toservices/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/andutils/have noindex.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 oneindex.tsas its public API (explicit named exports, noexport *); its other files stay private behind it. - A complex
utils/<topic>/subdirectory likewise has oneindex.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:
- Foundation —
core/andutils/carry no business knowledge; nothing business sits below them. - Data layer —
data/is the storage layer above the foundation. - Business —
ai/,features/, andservices/are the business tier; they depend down ondata/,core/, andutils/. ai/is foundational within the business tier:features/andservices/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 throughservices/,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/mainandsrc/preloadmust not import renderer code. Cross-process types live in@shared, main-only types stay insrc/main— see Shared Layer Architecture for placement. Enforced by an ESLintno-restricted-importsrule banning@rendererinsrc/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 isi18n/(§1), added so the main process owns a locale catalog symmetric withsrc/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/importingfeatures/(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 ofipc/(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).
8. Related
- Architecture Overview — process model, data flow, monorepo tree (the cross-process parent of this doc).
- Renderer Architecture / Shared Layer Architecture — the peer per-process directory references.
- Naming Conventions — §4.8 closed top-level, §4.9 singular/plural, §4.10 feature vs type-bucket, §5.2 route-by-shape.