Plugin Architecture
The Vite plugin is intentionally split across a few focused modules so it is easier to follow and to extend.
File layout
packages/alloy/src/plugins/
├── vite-plugin/
│ └── index.ts # Main Vite plugin entry point (discovery, manifest ingestion, provider imports)
├── core/
│ ├── codegen.ts # Generates virtual container module (imports, stubs, registrations, provider application)
│ ├── scanner.ts # Decorator + Lazy discovery (AST-like text scanning)
│ ├── types.ts # Shared metadata types used by plugins/core
│ └── utils.ts # General helpers (paths, hashing, alias generation)
├── rollup-plugin/
│ └── index.ts # Rolldown/Rollup manifest plugin (emits alloy.manifest.mjs)Responsibilities
vite-plugin/index.ts
- Hosts the
alloy()factory. - Keeps build-time state (
discoveredClasses, file indexes, lazy reference indexes). - Drives the AST walk, deferring to helpers for decorator parsing and lazy tracking.
- Generates the virtual module by delegating to
codegen.ts. - Ingests internal library manifests, merging discovered services with manifest-described ones.
- Imports provider modules (from config and manifests) and applies them in the generated container.
- Throws a helpful error when duplicate service registrations are detected (same class name discovered locally and provided via manifest).
core/codegen.ts
- Receives the discovered metadata and lazy-only class keys.
- Builds import statements, resolves name collisions via aliases, and emits the registration array + container boilerplate.
core/scanner.ts
- Parses source text to collect decorated classes and extracts raw options text.
- Detects
Lazy(...)references and records unique class keys for codegen decisions.
core/utils.ts
- Shared helpers for hashing, alias creation, and POSIX path normalization used across plugins.
vite-plugin/utils.ts
- Shared helpers for hashing, alias creation, and POSIX path normalization used by both the discoverer and the code generator.
Flow overview
alloy()registers Vite hooks using the state holders inplugin/index.ts.- During
transform, the AST walker records decorated classes and forwards every call expression toprocessLazyCallfromlazy.ts. - When Vite requests the
virtual:alloy-containermodule, the plugin passes the collected metadata + lazy-only set intogenerateContainerModule(). - The generated container imports only eagerly referenced services; lazy-only and factory-lazy (
lazyServices) entries receive stubs plusfactory: Lazy(...)metadata. - Provider modules are imported and
applyProviders(container, ...)is invoked after decorator-based registrations, enabling external libraries to register values, services, and lazy services.
This separation keeps each concern small and makes future additions (e.g., factory-lazy strategies, incremental scanning, new analysers) straightforward.
lazyServices Option
Add ServiceIdentifier symbols to lazyServices in plugin config to lazily import entire service modules:
ts
import { serviceIdentifiers } from "./src/virtual-container";
alloy({
lazyServices: [serviceIdentifiers.ReportingService],
});Codegen behavior:
- Omits static import for those classes.
- Synthesizes empty stub class with the same name (DI key).
- Injects
factory: Lazy(() => import(<path>).then(m => m.<Name>))into registration metadata. - First resolution triggers dynamic chunk load via container.
Import { serviceIdentifiers } from virtual:alloy-container and call container.get(serviceIdentifiers.ReportingService) to resolve without importing the class symbol when type info is not required at runtime.