Overview
Relevant Files
README.mdpackages/README.mdpackages.bzlpnpm-workspace.yamlpackage.json
Angular is a comprehensive development platform for building mobile and desktop web applications using TypeScript/JavaScript. This is the official Angular repository containing the framework source code, compiler, CLI tooling, and extensive documentation.
Repository Structure
The repository is organized as a monorepo using pnpm workspaces and Bazel for build management. It contains multiple interconnected packages that form the complete Angular ecosystem:
angular/
├── packages/ # Core framework packages published to npm
├── adev/ # Angular documentation site (angular.dev)
├── dev-app/ # Development application for testing
├── modules/ # Benchmarks and utilities
├── integration/ # Integration tests
├── tools/ # Build and development tools
└── devtools/ # Angular DevTools extension
Core Packages
The framework is distributed as 18 published npm packages:
Foundation Packages:
@angular/core– Runtime essentials including decorators (@Component,@Injectable), dependency injection, change detection, and lifecycle hooks@angular/common– Fundamental directives, pipes, location services, HTTP client, and localization support@angular/compiler– Template compiler for converting Angular templates to executable code@angular/compiler-cli– Command-line compiler and build tooling (powers the Angular CLI)
Platform Packages:
@angular/platform-browser– Browser-specific rendering and DOM APIs@angular/platform-browser-dynamic– JIT compilation support for browsers@angular/platform-server– Server-side rendering (SSR) capabilities
Feature Packages:
@angular/router– Client-side routing and navigation@angular/forms– Template-driven and reactive form handling@angular/animations– Animation DSL and browser rendering@angular/elements– Web Components integration@angular/service-worker– Progressive Web App (PWA) support@angular/localize– Internationalization (i18n) infrastructure@angular/language-service– IDE integration and tooling support@angular/upgrade– AngularJS to Angular migration utilities@angular/benchpress– Performance benchmarking toolszone.js– Asynchronous execution context managementangular-in-memory-web-api– Mock HTTP backend for development
Build System
The repository uses Bazel as the primary build system with TypeScript compilation. Key build artifacts:
- npm packages – Published to npm registry under
@angular/scope - API documentation – Generated from TypeScript source code
- Bundles – Optimized distribution formats (ESM, UMD, etc.)
- Tests – Unit tests, integration tests, and e2e tests
Development Workflow
Loading diagram...
Key Technologies
- TypeScript – Primary language for framework and tooling
- RxJS – Reactive programming library for observables
- Zone.js – Asynchronous context management
- Bazel – Build and test orchestration
- pnpm – Package manager (required, not npm or yarn)
Documentation & Resources
- Official Docs: angular.dev
- Contributing: See CONTRIBUTING.md
- Changelog: CHANGELOG.md
- Code of Conduct: CODE_OF_CONDUCT.md
Architecture & Compilation Pipeline
Relevant Files
packages/compiler-cli/src/ngtsc/core/src/compiler.tspackages/compiler-cli/src/ngtsc/program.tspackages/compiler-cli/src/main.tspackages/compiler/src/compiler.tspackages/core/src/render3packages/compiler/design/architecture.md
Overview
Angular's compilation pipeline transforms TypeScript source code with Angular decorators into optimized JavaScript and type definitions. The system uses ngtsc (the Ivy compiler) as the core engine, which wraps TypeScript's compiler and applies Angular-specific transformations. This architecture enables incremental compilation, template type-checking, and efficient code generation.
Compilation Architecture
The compilation process follows a layered architecture:
Loading diagram...
Key Components:
-
NgCompilerHost - Wraps the TypeScript
CompilerHostand injects synthetic Angular files (like__ng_typecheck__.tsfor template type-checking). -
NgCompiler - The heart of the Ivy compiler. It's lazy-evaluated and only performs work when output methods are called (e.g.,
getDiagnostics(),emit()). -
Trait Compiler - Analyzes decorators (
@Component,@Directive,@Injectable,@NgModule,@Pipe) and generates static definition fields (e.g.,ɵcmp,ɵdir,ɵprov). -
Template Type Checker - Validates template expressions and bindings against component metadata.
Compilation Flow
The standard compilation sequence is:
- Configuration - Read
tsconfig.jsonand merge with Angular compiler options - Host Creation - Create
NgCompilerHostwrapping the TypeScript compiler host - Program Creation - Build
ts.Programwith augmented root files - Ticket Generation - Create
CompilationTicket(fresh or incremental) - Compiler Initialization - Instantiate
NgCompilerfrom the ticket - Analysis - Analyze source files and extract decorator metadata
- Diagnostics - Gather TypeScript and Angular-specific diagnostics
- Emit - Call
prepareEmit()to get Angular transformers, then emit JavaScript
Decorator Transformation
Angular decorators are compiled to static definition fields without runtime overhead:
Input (TypeScript):
@Component({
selector: 'greet',
template: '<div>Hello {{name}}</div>'
})
export class GreetComponent {
@Input() name: string;
}
Output (JavaScript):
class GreetComponent {}
GreetComponent.ɵcmp = ɵɵdefineComponent({
type: GreetComponent,
tag: 'greet',
factory: () => new GreetComponent(),
template: function(rf, ctx) {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'div');
ɵɵtext(1);
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
ɵɵadvance();
ɵɵtextInterpolate1('Hello ', ctx.name, '!');
}
}
});
Incremental Compilation
The compiler supports two incremental strategies:
- Local Reuse - New
NgCompilerinherits local metadata from previous compilation; global information (NgModule scopes) is recomputed - Full Reuse - Previous
NgCompileris reused entirely for resource-only changes (e.g., modified CSS files)
The CompilationTicket abstraction shields consumers from managing NgCompiler lifecycle complexity.
Render3 Runtime
The packages/core/src/render3 directory contains the runtime engine that executes compiled templates. It includes:
- Instructions - Low-level rendering operations (
ɵɵelementStart,ɵɵproperty,ɵɵlistener, etc.) - Change Detection - Efficient dirty-checking and signal-based reactivity
- Dependency Injection - Service resolution and provider management
- View Management - DOM node lifecycle and view container operations
Core Runtime & Dependency Injection
Relevant Files
packages/core/src/di/r3_injector.tspackages/core/src/di/provider_collection.tspackages/core/src/application/application_ref.tspackages/core/src/application/application_init.tspackages/core/src/render3/di.tspackages/core/src/render3/component.ts
Dependency Injection System
Angular's DI system is built on the R3Injector, a hierarchical container that manages service instantiation and dependency resolution. The injector maintains a map of provider tokens to their corresponding factory functions and cached instances.
Key Components:
-
Providers – Define how services are created. Five main types exist:
- Type Provider:
MyService– Class is both token and factory - Value Provider:
{provide: Token, useValue: instance}– Pre-created value - Factory Provider:
{provide: Token, useFactory: () => instance}– Custom factory function - Class Provider:
{provide: Token, useClass: AltClass}– Alternative class implementation - Existing Provider:
{provide: Token, useExisting: OtherToken}– Alias to another token
- Type Provider:
-
Provider Processing – When an injector is created, it processes all providers:
- Converts each provider to a
Recordcontaining a factory function and cached value - Handles multi-providers (arrays of values for a single token)
- Resolves forward references and validates provider definitions
- Converts each provider to a
-
Dependency Resolution – When
inject(Token)is called:- Looks up the token in the injector's records
- If not found, checks if the token has an
@Injectable()decorator with a factory - Executes the factory function, passing resolved dependencies
- Caches the result to ensure singleton behavior
// Example: Injector resolution flow
const injector = createInjector(AppModule);
const service = injector.get(MyService); // Triggers factory execution
const cached = injector.get(MyService); // Returns cached instance
Application Runtime & Initialization
The application runtime orchestrates component bootstrapping and lifecycle management through the ApplicationRef and ApplicationInitStatus classes.
Bootstrap Sequence:
-
Platform Creation –
createPlatform()establishes the root injector with platform-level providers (NgZone, RendererFactory2, etc.) -
Application Injector –
bootstrapApplication()creates an environment injector with app-level providers, inheriting from the platform injector -
Initializers –
APP_INITIALIZERtokens are executed in sequence. They can return Promises or Observables to delay app startup:
bootstrapApplication(AppComponent, {
providers: [
{
provide: APP_INITIALIZER,
useValue: () => inject(HttpClient).get('/config'),
multi: true
}
]
});
-
Component Bootstrap – The root component is instantiated via
ComponentFactory.create(), which:- Creates an element injector for the component
- Resolves component dependencies
- Triggers change detection and renders the view
-
Change Detection –
ApplicationRef.tick()synchronizes the application state with the DOM, running change detection cycles until stability is reached.
Hierarchical Injector Architecture
Angular uses a multi-level injector hierarchy:
Loading diagram...
- Platform Injector: Singleton services shared across all applications
- Environment Injector: Application-level services and providers
- Component Injector: Component-specific providers and view providers
When resolving a dependency, the injector checks its own records first, then delegates to the parent injector if not found. This enables provider overrides at any level.
Injection Context
The inject() function requires an active injection context. This context is automatically established during:
- Component/directive construction
- Factory function execution
- Initializer execution (via
runInInjectionContext())
Attempting to call inject() outside these contexts throws an error. Use runInInjectionContext(injector, fn) to manually establish a context when needed.
Change Detection & Reactivity
Relevant Files
packages/core/primitives/signalspackages/core/src/render3/reactivitypackages/core/src/change_detectionpackages/core/src/render3/reactive_lview_consumer.ts
Angular's change detection system is built on a reactive graph of signals and effects. When a signal value changes, the framework automatically detects which components and effects depend on it and schedules updates.
The Reactive Graph
At the core is a bidirectional dependency graph connecting producers (signals that emit values) and consumers (effects, computed signals, and templates that depend on those values).
// Producers: signals that emit values
const count = signal(0);
const doubled = computed(() => count() * 2);
// Consumers: effects that react to changes
effect(() => console.log(doubled()));
Each node in the graph tracks:
- version: incremented when the value changes
- dirty: whether the node needs recomputation
- producers: signals this consumer depends on
- consumers: effects/computeds that depend on this producer
Glitch-Free Execution: Push/Pull Algorithm
Angular prevents glitches (observing inconsistent intermediate states) using a two-phase algorithm:
Phase 1 (Push): When a signal updates, the framework eagerly propagates change notifications through the graph, marking all affected consumers as dirty. No recomputation happens yet.
Phase 2 (Pull): When values are read, the framework lazily recomputes only what's needed. This ensures all dependencies are invalidated before any consumer re-executes.
const counter = signal(0);
const isEven = computed(() => counter() % 2 === 0);
effect(() => console.log(counter(), isEven())); // Logs: 0 false
counter.set(1); // Phase 1: marks isEven and effect as dirty
// Phase 2: effect re-runs, reads counter (1) and isEven (recomputes to true)
Live vs. Non-Live Consumers
Live consumers (effects, templates) receive push notifications when dependencies change. Non-live consumers (unused computed signals) poll their dependencies when accessed, preventing memory leaks.
const counter = signal(1);
let double = computed(() => counter() * 2);
console.log(double()); // 2
double = null; // Can be garbage collected—no hard reference from counter
// But if used in an effect:
effect(() => console.log(double())); // Now double is "live"
Dynamic Dependency Tracking
Dependencies are tracked implicitly during execution. If a computed signal conditionally reads different signals, the dependency set updates automatically:
const useA = signal(true);
const dataA = signal('A');
const dataB = signal('B');
const dynamic = computed(() => useA() ? dataA() : dataB());
// Dependencies: [useA, dataA] initially
// If useA changes to false, dependencies become [useA, dataB]
Change Detection Scheduling
The ChangeDetectionScheduler coordinates when change detection runs. With zoneless change detection, the framework schedules updates based on:
- Signal updates in templates
ChangeDetectorRef.markForCheck()- Input property changes
- Bound event listeners
bootstrapApplication(MyApp, {
providers: [provideZonelessChangeDetection()]
});
Template Reactivity
Templates are treated as reactive consumers. Each component view gets a ReactiveLViewConsumer that tracks which signals are read during rendering. When those signals change, the view is marked for refresh.
@Component({
template: `Count: {{count()}}`
})
export class MyComponent {
count = signal(0);
// Template automatically re-renders when count changes
}
Routing & Navigation
Relevant Files
packages/router/src/router.tspackages/router/src/directives/router_outlet.tspackages/router/src/directives/router_link.tspackages/router/src/router_state.tspackages/router/src/models.tspackages/router/src/events.tspackages/router/src/url_tree.tspackages/router/src/recognize.ts
The Angular Router manages application state transitions and URL synchronization. It enables declarative navigation between views while maintaining browser history and supporting advanced features like lazy loading, guards, and resolvers.
Core Architecture
The router operates through three main components:
- Router Service - Orchestrates navigation, manages state, and exposes APIs like
navigate()andnavigateByUrl() - RouterOutlet Directive - Placeholder that dynamically renders components based on the active route
- RouterLink Directive - Declarative navigation trigger that creates links without full page reloads
Loading diagram...
Route Configuration & Matching
Routes are defined as an array of Route objects that map URL paths to components. The router uses a hierarchical matching algorithm:
- Path matching: Segments are matched against route configurations sequentially
- Redirects: Routes can redirect to other paths using
redirectTo - Lazy loading: Child routes can be loaded on-demand via
loadChildren - Wildcards: The
**path matches any unmatched URL (typically for 404 pages)
const routes: Route[] = [
{ path: 'home', component: HomeComponent },
{ path: 'user/:id', component: UserComponent },
{ path: 'admin', loadChildren: () => import('./admin/routes') },
{ path: '**', component: NotFoundComponent }
];
Navigation Flow
When navigation is triggered, the router executes this sequence:
- URL Parsing - Converts the target URL into a
UrlTreestructure - Route Recognition - Matches URL segments against configured routes
- Guard Execution - Runs
canActivate,canDeactivate, andcanMatchguards - Data Resolution - Executes resolvers to fetch required data
- Component Activation - Creates component instances and injects them into
RouterOutlet - Event Emission - Fires navigation events (
NavigationStart,NavigationEnd, etc.)
RouterOutlet & RouterLink
RouterOutlet acts as a placeholder where routed components are rendered:
<router-outlet />
The outlet registers with ChildrenOutletContexts and receives activated routes from the router. It supports named outlets for complex layouts with multiple independent navigation branches.
RouterLink provides declarative navigation:
<a routerLink="/user/42">View User</a>
<a [routerLink]="['/team', teamId, 'user', userId]">Team Member</a>
RouterLink handles relative paths (./, ../), query parameters, and fragments while preventing default link behavior.
Router State & ActivatedRoute
The RouterState represents the current navigation tree as a hierarchy of ActivatedRoute instances. Each route node contains:
- params - Route parameters (e.g.,
:idfrom/user/42) - queryParams - Query string parameters
- data - Static route metadata
- component - The component instance for this route
- snapshot - Immutable view of the route at a specific moment
Components access route information via dependency injection:
constructor(private route: ActivatedRoute) {
this.route.params.subscribe(params => {
console.log(params['id']);
});
}
Navigation Events
The router emits events throughout the navigation lifecycle via router.events:
NavigationStart- Navigation beginsRoutesRecognized- Routes matched successfullyGuardsCheckStart/GuardsCheckEnd- Guard executionResolveStart/ResolveEnd- Data resolutionNavigationEnd- Navigation completed successfullyNavigationCancel- Navigation cancelled by guardNavigationError- Navigation failed with error
Applications can subscribe to these events for logging, analytics, or UI updates.
Forms & Validation
Relevant Files
packages/forms/src/validators.tspackages/forms/src/model/form_control.tspackages/forms/src/model/form_group.tspackages/forms/src/form_builder.tspackages/forms/src/directives/ng_model.tspackages/forms/src/directives/validators.ts
Angular Forms provides two complementary approaches for building and validating forms: Reactive Forms and Template-Driven Forms. Both leverage a unified validation system that supports synchronous validators, asynchronous validators, and custom validation logic.
Form Models
The core form model consists of three fundamental classes:
- FormControl - Tracks the value and validation status of a single form field. Accepts an initial value, optional validators, and async validators.
- FormGroup - Aggregates multiple FormControl instances into a single object. Its status is derived from its children (invalid if any child is invalid).
- FormArray - Similar to FormGroup but for dynamic lists of controls, useful for variable-length form sections.
All three extend AbstractControl, which provides common methods like setValue(), patchValue(), reset(), and updateValueAndValidity().
Validation System
Validators are functions that receive a control and return either null (valid) or a ValidationErrors object (invalid). Angular provides built-in validators through the Validators class:
required- Control must have a non-empty valuemin(n)/max(n)- Numeric bounds validationminLength(n)/maxLength(n)- String/array length validationemail- Email format validation using RFC-compliant regexpattern(regex)- Custom regex pattern matching
Validators can be composed using Validators.compose() to combine multiple validators into a single function.
Async Validators
Async validators return Promise<ValidationErrors | null> or Observable<ValidationErrors | null>. Common use cases include server-side validation (checking username availability) or expensive computations. The form enters a PENDING status while async validators run.
Register async validators via the asyncValidators option in AbstractControlOptions or through the NG_ASYNC_VALIDATORS injection token for directive-based validation.
Reactive vs Template-Driven Forms
Reactive Forms use FormBuilder or direct control instantiation:
const form = new FormGroup({
email: new FormControl('', [Validators.required, Validators.email]),
age: new FormControl(null, Validators.min(18))
});
Template-Driven Forms use directives like ngModel and ngForm:
<form #myForm="ngForm">
<input [(ngModel)]="user.email" name="email" required email>
</form>
Error Handling
Access validation errors via the errors property, which is a map of error codes to error details:
control.errors // { required: true } or { email: true } or null
control.hasError('required') // boolean check
control.getError('required') // get specific error details
Form status is tracked as VALID, INVALID, PENDING (async validation in progress), or DISABLED.
Custom Validators
Implement the Validator or AsyncValidator interface to create reusable validators:
@Directive({
selector: '[appCustomValidator]',
providers: [{provide: NG_VALIDATORS, useExisting: CustomValidatorDirective, multi: true}]
})
export class CustomValidatorDirective implements Validator {
validate(control: AbstractControl): ValidationErrors | null {
return control.value === 'forbidden' ? { forbidden: true } : null;
}
}
Register via NG_VALIDATORS (sync) or NG_ASYNC_VALIDATORS (async) injection tokens with multi: true to add to existing validators.
Platform Rendering & SSR
Relevant Files
packages/platform-browser/src/browser.tspackages/platform-server/src/server.tspackages/platform-browser/src/hydration.tspackages/core/src/hydration/annotate.tspackages/core/src/hydration/utils.tspackages/core/src/render3/instructions/render.ts
Overview
Angular supports two distinct rendering platforms: browser rendering for client-side execution and server rendering for server-side execution. The platform abstraction allows the same Angular application code to run in both environments. Hydration bridges these two worlds by enabling the client to reuse the server-rendered DOM instead of re-rendering from scratch.
Platform Architecture
The platform system is built on two main packages:
@angular/platform-browser- Provides browser-specific providers and thebootstrapApplicationfunction for client-side rendering@angular/platform-server- Provides server-specific providers andrenderApplicationfor server-side rendering
Both platforms share the same core rendering engine (Render3) but with platform-specific implementations for DOM manipulation, event handling, and HTTP requests.
Loading diagram...
Server-Side Rendering Flow
When rendering on the server, Angular executes the component tree and produces an HTML string:
- Setup -
provideServerRendering()setsngServerMode = trueglobally - Execution - Components execute their templates, creating a virtual DOM tree
- Annotation -
annotateForHydration()marks components with hydration metadata (thenghattribute) - Serialization - Hydration data is serialized into
TransferStateand embedded in the HTML - Output - The HTML string is sent to the client
Client-Side Hydration
Hydration reuses the server-rendered DOM instead of creating new elements:
- Detection - Client checks for
nghattributes andTransferStatedata - Retrieval -
retrieveHydrationInfo()extracts serialized view data fromTransferState - Reconciliation - Render instructions locate existing DOM nodes instead of creating new ones
- Attachment - Event listeners and component state are attached to existing elements
- Cleanup - Unclaimed DOM nodes are removed
The ngh attribute contains an index referencing serialized view data in TransferState under the key __nghData__.
Render3 Instructions
Both platforms use the same Render3 instruction set. Key instructions include:
- Creation mode (
rf & 1) -ɵɵelement,ɵɵtext,ɵɵtemplatecreate or locate DOM nodes - Update mode (
rf & 2) -ɵɵproperty,ɵɵattribute,ɵɵlistenerbind data and events
During hydration, creation instructions switch from creating new nodes to locating existing ones using paths stored in the serialized view data.
Hydration Features
Angular provides optional hydration features via provideClientHydration():
- DOM Reuse - Default; reuses server-rendered DOM
- Event Replay -
withEventReplay()captures and replays user events before hydration completes - Incremental Hydration -
withIncrementalHydration()hydrates components on-demand using thehydratetrigger - HTTP Transfer Cache -
withHttpTransferCacheOptions()transfers HTTP responses from server to client - i18n Support -
withI18nSupport()enables hydration for internationalized content
Mismatch Detection
In development mode, Angular validates that client and server DOM structures match:
- Node Lookup -
locateNextRNode()finds expected nodes using serialized paths - Validation -
validateMatchingNode()checks node type, tag name, and content - Error Reporting - Mismatches throw
NG0500errors with detailed expected vs. actual DOM descriptions - DevTools Integration - Mismatch info is attached to component host elements for debugging
Key Constraints
Hydration requires strict DOM consistency between server and client:
- HTML produced by server rendering must not be modified before client bootstrap
- Whitespace and comment nodes must match exactly
- Components with i18n blocks or ShadowDOM encapsulation skip hydration (marked with
ngSkipHydration) - Dynamic content must use the same logic on both server and client
Build System & Tooling
Relevant Files
BUILD.bazel- Root build configurationMODULE.bazel- Bazel module dependencies and toolchain setup.bazelrc- Bazel configuration flags and settingstools/bazel/- Custom Bazel rules and build utilitiestools/defaults.bzl- Shared build rule definitionsscripts/build/- Build scripts for distribution packagescontributing-docs/building-with-bazel.md- Detailed build documentation
Angular uses Bazel as its primary build system, providing fast, reliable incremental builds across the monorepo. The build infrastructure is complemented by pnpm for package management and custom tooling for specialized tasks.
Bazel: The Core Build System
Bazel is a polyglot build tool that enables hermetic, reproducible builds. Angular's Bazel setup includes:
-
Module System (
MODULE.bazel): Declares dependencies on Bazel rules and external toolsaspect_rules_tsfor TypeScript compilationaspect_rules_jsfor JavaScript bundlingaspect_rules_esbuildfor ES module bundlingrules_angularfor Angular-specific compilation rules- Node.js toolchain (v22.21.1) and pnpm (v10.16.1)
-
Configuration (
.bazelrc): Sets build flags for consistency- Symlink prefix:
dist/for outputs - Strict action environment for cache consistency
- Debug mode with
--config=debugfor test inspection - Release stamping with version control information
- Symlink prefix:
-
Custom Rules (
tools/bazel/): Specialized build rules for Angularng_package- Builds distributable Angular packagesng_web_test_suite- Runs browser-based tests with Karmaesbuild- Bundles and minifies codejasmine_test- Runs Node.js tests with Jasmine
Building and Testing
Common build commands:
pnpm bazel build packages/core # Build a single package
pnpm bazel build packages/... # Build all packages
pnpm bazel test packages/core/test:test # Run Node tests
pnpm bazel test packages/core/test:test_web # Run browser tests
Use ibazel for watch mode: ibazel build packages/core continuously rebuilds as files change.
Package Management
Angular uses pnpm (v10.26.0) as the package manager:
- Monorepo workspace defined in
pnpm-workspace.yaml - Lock file:
pnpm-lock.yaml(frozen mode enforced in.bazelrc) - Custom patches in
tools/pnpm-patches/for dependency fixes - All npm dependencies managed through Bazel's
npm_translate_lockextension
Distribution Build
The scripts/build/ directory contains TypeScript scripts for building distribution packages:
build-packages-dist.mts- Main entry point forpnpm buildpackage-builder.mts- Orchestrates package compilation and bundlingzone-js-builder.mts- Specialized builder for Zone.js package
Run with: pnpm build
Debugging and Profiling
Debug Node Tests:
pnpm bazel test packages/core/test:test --config=debug
Then attach debugger at chrome://inspect or VSCode port 9229.
Profile Builds:
pnpm bazel build //packages/compiler --profile profile.json
pnpm bazel analyze-profile profile.json --html --html_details
Key Configuration Highlights
- Hermetic Builds: All dependencies declared explicitly; no implicit filesystem access
- Remote Caching: Supported on CI; available to core developers with credentials
- Stamping: Release builds include git commit info via
workspace_status_command - Cross-Platform: Supports Linux, macOS (Intel & ARM), and Windows with platform-specific toolchains