9.5 KiB
Rustelo Layered Override System
🎯 Architecture Overview
The layered override system enables customization at multiple levels while maintaining framework integrity and update safety. This system follows a clear precedence hierarchy:
Precedence Order: Local > Feature > Template > Framework
🏗️ Layer Definitions
1. Framework Layer (Bottom - Never Modified)
- Location: Framework crates in dependencies
- Content: Core rustelo functionality
- Modification: ❌ Never modified directly
- Updates: Automatic via
cargo rustelo update
2. Template Layer (Foundation)
- Location: Project templates from
/rustelo/templates/ - Content: Default project structure and configurations
- Modification: ❌ Not modified directly in projects
- Updates: Synced via
cargo rustelo sync templates
3. Feature Layer (Additive)
- Location: Feature-specific additions from
/rustelo/features/*/templates/ - Content: Feature configurations, dependencies, tooling
- Modification: ✅ Can be overridden by local layer
- Updates: Managed via
cargo rustelo add/remove <feature>
4. Local Layer (Top - Full Control)
- Location: Project-specific files and customizations
- Content: Local customizations, overrides, project-specific code
- Modification: ✅ Full control by developers
- Updates: Always preserved during framework updates
🔧 Override Resolution Process
Configuration Files (Cargo.toml, justfile, package.json)
Resolution Order:
1. Check for local override: `config/local/Cargo.toml`
2. Check for feature configs: `config/features/*/Cargo.toml`
3. Check template default: `config/templates/Cargo.toml`
4. Fall back to framework defaults
Merge Strategy: Hierarchical merge with higher layers taking precedence
Component Overrides (Pages, Components)
Resolution Order:
1. Local components: `src/components/local/`
2. Feature components: `src/components/features/*/`
3. Template components: `src/components/templates/`
4. Framework components: Framework crates
Selection Strategy: First found wins (no merging for components)
Route Definitions
Resolution Order:
1. Local routes: `config/routes/local/`
2. Feature routes: `config/routes/features/*/`
3. Template routes: `config/routes/templates/`
4. Framework routes: Generated defaults
Merge Strategy: Route sets are merged with local routes overriding conflicts
Styling (CSS, UnoCSS)
Resolution Order:
1. Local styles: `styles/local/`
2. Feature styles: `styles/features/*/`
3. Template styles: `styles/templates/`
4. Framework styles: Default framework styles
Merge Strategy: CSS cascade with local styles having highest specificity
📁 Directory Structure
my-rustelo-project/
├── config/
│ ├── local/ # Local overrides (highest precedence)
│ │ ├── Cargo.toml
│ │ ├── justfile
│ │ └── app.toml
│ ├── features/ # Feature-specific configs
│ │ ├── analytics/
│ │ ├── smart-build/
│ │ └── debugging-tools/
│ └── templates/ # Template defaults (lowest precedence)
├── src/
│ ├── components/
│ │ ├── local/ # Local component overrides
│ │ ├── features/ # Feature components
│ │ └── templates/ # Template components
│ └── pages/
│ ├── local/ # Local page overrides
│ ├── features/ # Feature pages
│ └── templates/ # Template pages
├── routes/
│ ├── local/ # Local route definitions
│ ├── features/ # Feature routes
│ └── templates/ # Template routes
├── styles/
│ ├── local/ # Local styling
│ ├── features/ # Feature styles
│ └── templates/ # Template styles
└── scripts/
├── local/ # Local scripts
├── features/ # Feature scripts
└── templates/ # Template scripts
⚙️ Implementation
Configuration Resolution Engine
pub struct LayeredConfig {
layers: Vec<ConfigLayer>,
}
#[derive(Debug, Clone)]
pub enum ConfigLayer {
Local(PathBuf),
Feature(String, PathBuf),
Template(PathBuf),
Framework,
}
impl LayeredConfig {
pub fn resolve<T>(&self, config_name: &str) -> Result<T, ConfigError>
where
T: DeserializeOwned + Merge
{
let mut result = T::default();
// Apply layers from lowest to highest precedence
for layer in &self.layers {
if let Some(config) = self.load_config_from_layer(layer, config_name)? {
result.merge(config);
}
}
Ok(result)
}
}
Component Resolution Engine
pub struct LayeredComponents {
component_dirs: Vec<ComponentDir>,
}
#[derive(Debug, Clone)]
pub enum ComponentDir {
Local(PathBuf),
Feature(String, PathBuf),
Template(PathBuf),
Framework,
}
impl LayeredComponents {
pub fn find_component(&self, name: &str) -> Option<PathBuf> {
// Search from highest to lowest precedence
for dir in &self.component_dirs {
if let Some(path) = self.search_component_in_dir(dir, name) {
return Some(path);
}
}
None
}
}
🚀 CLI Integration
Automatic Layer Setup
# Create layered structure when adding features
cargo rustelo add analytics
# Creates: config/features/analytics/, src/components/features/analytics/, etc.
# Create local override
cargo rustelo override component Header
# Creates: src/components/local/Header.rs (overrides template/feature version)
# Create local config override
cargo rustelo override config Cargo.toml
# Creates: config/local/Cargo.toml (merges with other layers)
Override Management
# List all overrides
cargo rustelo list-overrides
# Show override hierarchy for specific item
cargo rustelo trace component Header
# Output:
# Header component resolution:
# ✅ src/components/local/Header.rs (USED)
# 🔸 src/components/features/analytics/Header.rs
# 🔸 src/components/templates/Header.rs
# Remove local override (fall back to next layer)
cargo rustelo remove-override component Header
🛡️ Update Safety
Framework Updates
When running cargo rustelo update:
- Framework crates are updated to latest versions
- Template defaults are refreshed (but not applied)
- Feature configs are updated if compatible
- Local overrides are never modified
- Conflict detection warns of incompatibilities
Feature Updates
When running cargo rustelo add/remove <feature>:
- Feature layer is added/removed
- Local overrides take precedence over new feature configs
- Dependency conflicts are resolved in favor of local choices
- Migration warnings provided for breaking changes
🎨 Configuration Merging Examples
Cargo.toml Merging
Template Layer:
[dependencies]
leptos = "0.6"
serde = "1.0"
Feature Layer (analytics):
[dependencies]
prometheus = "0.13"
chrono = "0.4"
Local Layer:
[dependencies]
leptos = "0.7" # Override template version
anyhow = "1.0" # Add local dependency
Final Result:
[dependencies]
leptos = "0.7" # Local override
serde = "1.0" # From template
prometheus = "0.13" # From analytics feature
chrono = "0.4" # From analytics feature
anyhow = "1.0" # Local addition
UnoCSS Configuration Merging
Template Layer:
export default {
theme: {
colors: {
primary: '#3b82f6',
}
}
}
Feature Layer (analytics):
export default {
theme: {
colors: {
analytics: '#10b981',
}
},
shortcuts: {
'metric-card': 'bg-white shadow-md rounded-lg p-4',
}
}
Local Layer:
export default {
theme: {
colors: {
primary: '#ef4444', // Override template primary
brand: '#8b5cf6', // Add local color
}
}
}
Final Result:
export default {
theme: {
colors: {
primary: '#ef4444', // Local override
analytics: '#10b981', // From analytics feature
brand: '#8b5cf6', // Local addition
}
},
shortcuts: {
'metric-card': 'bg-white shadow-md rounded-lg p-4', // From analytics
}
}
📋 Benefits
For Developers
- Full customization without losing update capability
- Clear precedence - always know which config wins
- Safe experimentation - local changes never affect framework
- Easy rollback - remove local override to revert to defaults
for Framework Maintainers
- Update safety - never break user customizations
- Feature composability - features don't conflict with each other
- Clear boundaries - framework, template, feature, and local concerns separated
- Debugging support - trace resolution path for any configuration
For Teams
- Consistent structure - all projects follow same layered approach
- Shared overrides - commit local layer for team-wide customizations
- Feature experimentation - test features without permanent changes
- Migration safety - gradual migration between framework versions
This layered override system ensures that Rustelo provides maximum flexibility while maintaining the reliability and update safety that production applications require.