Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 44 additions & 38 deletions crates/cli/src/spacetime_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,46 +164,53 @@ pub struct LoadedConfig {

impl SpacetimeConfig {
/// Collect all database targets with parent→child inheritance.
/// Children inherit unset `additional_fields` and `generate` from their parent.
/// `dev` and `children` are NOT propagated to child targets.
/// Children inherit unset `additional_fields` from their parent.
/// `dev`, `generate`, and `children` are NOT propagated to child targets.
/// Returns `Vec<FlatTarget>` with fully resolved fields.
pub fn collect_all_targets_with_inheritance(&self) -> Vec<FlatTarget> {
self.collect_targets_inner(None, None)
self.collect_targets_inner(None)
}

fn collect_targets_inner(
&self,
parent_fields: Option<&HashMap<String, Value>>,
parent_generate: Option<&Vec<HashMap<String, Value>>>,
) -> Vec<FlatTarget> {
fn collect_targets_inner(&self, parent_fields: Option<&HashMap<String, Value>>) -> Vec<FlatTarget> {
// module-path, bin-path, and js-path are mutually exclusive module sources.
// If a child specifies any one, the other two are not inherited from the parent.
const MODULE_SOURCE_KEYS: &[&str] = &["module-path", "bin-path", "js-path"];
let child_specifies_source = MODULE_SOURCE_KEYS
.iter()
.any(|k| self.additional_fields.contains_key(*k));

// Build this node's fields by inheriting from parent
let mut fields = self.additional_fields.clone();
if let Some(parent) = parent_fields {
for (key, value) in parent {
if !fields.contains_key(key) {
fields.insert(key.clone(), value.clone());
if fields.contains_key(key) {
continue;
}
// If the child specifies any module source, skip inheriting the others
if child_specifies_source && MODULE_SOURCE_KEYS.contains(&key.as_str()) {
continue;
}
fields.insert(key.clone(), value.clone());
}
}

// Generate: child's generate replaces parent's; if absent, inherit parent's
let effective_generate = if self.generate.is_some() {
self.generate.clone()
} else {
parent_generate.cloned()
};
// Generate is never inherited. It is tied to a specific module and output location:
// inheriting is redundant when the child shares the parent's module (deduplication
// handles it) and dangerous when the child uses a different module (two modules
// would write bindings to the same output directory).
let effective_generate = self.generate.clone();

let target = FlatTarget {
fields: fields.clone(),
source_config: self.source_config.clone(),
generate: effective_generate.clone(),
generate: effective_generate,
};

let mut result = vec![target];

if let Some(children) = &self.children {
for child in children {
let child_targets = child.collect_targets_inner(Some(&fields), effective_generate.as_ref());
let child_targets = child.collect_targets_inner(Some(&fields));
result.extend(child_targets);
}
}
Expand Down Expand Up @@ -2595,8 +2602,8 @@ mod tests {
}

#[test]
fn test_generate_inheritance_from_parent() {
// Children inherit generate from parent if they don't define their own
fn test_generate_not_inherited_from_parent() {
// Generate is never inherited. Children must define their own.
let json = r#"{
"database": "parent-db",
"server": "local",
Expand Down Expand Up @@ -2627,15 +2634,10 @@ mod tests {
Some("typescript")
);

// Child 1 inherits parent's generate
let child1_gen = targets[1].generate.as_ref().unwrap();
assert_eq!(child1_gen.len(), 1);
assert_eq!(
child1_gen[0].get("language").and_then(|v| v.as_str()),
Some("typescript")
);
// Child 1 does not inherit parent's generate
assert!(targets[1].generate.is_none());

// Child 2 overrides with its own generate
// Child 2 has its own generate
let child2_gen = targets[2].generate.as_ref().unwrap();
assert_eq!(child2_gen.len(), 1);
assert_eq!(child2_gen[0].get("language").and_then(|v| v.as_str()), Some("csharp"));
Expand Down Expand Up @@ -3079,9 +3081,11 @@ mod tests {
}

#[test]
fn test_generate_dedup_with_inherited_generate() {
// Two sibling databases sharing parent's generate + same module path
// should deduplicate to a single generate entry
/// Even when children share the parent's module-path, generate is not inherited.
///
/// Deduplication in generate.rs handles the common case; inheritance would be
/// dangerous when a child overrides module-path.
fn test_generate_not_inherited_for_children_sharing_module() {
let json = r#"{
"module-path": "./server",
"generate": [
Expand All @@ -3096,20 +3100,22 @@ mod tests {
let config: SpacetimeConfig = json5::from_str(json).unwrap();
let targets = config.collect_all_targets_with_inheritance();

// All 3 targets (parent + 2 children) share the same module-path and generate
assert_eq!(targets.len(), 3);

// Parent has generate
assert!(targets[0].generate.is_some());

// Children do not inherit generate
assert!(targets[1].generate.is_none());
assert!(targets[2].generate.is_none());

// All share the same module-path via field inheritance
for target in &targets {
assert_eq!(
target.fields.get("module-path").and_then(|v| v.as_str()),
Some("./server")
);
let r#gen = target.generate.as_ref().unwrap();
assert_eq!(r#gen.len(), 1);
assert_eq!(r#gen[0].get("language").and_then(|v| v.as_str()), Some("typescript"));
}

// All have the same (module-path, generate) so dedup should reduce to 1
// (this is verified in generate.rs tests, but we confirm the data here)
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions crates/cli/src/subcommands/dev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
}

// Safety prompt: warn if any selected database target is defined in spacetime.json.
// spacetime.local.json is gitignored and personal, so it's fine for dev use.
if let Some(ref lc) = loaded_config {
let database_sources = resolve_database_sources(&lc.config);
let databases_from_main_config: Vec<String> = db_names_for_logging
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/src/subcommands/publish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub fn build_publish_schema(command: &clap::Command) -> Result<CommandSchema, an
.key(Key::new("build_options").module_specific())
.key(Key::new("wasm_file").module_specific())
.key(Key::new("js_file").module_specific())
.key(Key::new("num_replicas"))
.key(Key::new("num_replicas").module_specific())
.key(Key::new("break_clients"))
.key(Key::new("anon_identity"))
.key(Key::new("parent"))
Expand Down
Loading
Loading