diff --git a/crates/cli/src/spacetime_config.rs b/crates/cli/src/spacetime_config.rs index 2f263e70f9d..b3316f2c318 100644 --- a/crates/cli/src/spacetime_config.rs +++ b/crates/cli/src/spacetime_config.rs @@ -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` with fully resolved fields. pub fn collect_all_targets_with_inheritance(&self) -> Vec { - self.collect_targets_inner(None, None) + self.collect_targets_inner(None) } - fn collect_targets_inner( - &self, - parent_fields: Option<&HashMap>, - parent_generate: Option<&Vec>>, - ) -> Vec { + fn collect_targets_inner(&self, parent_fields: Option<&HashMap>) -> Vec { + // 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); } } @@ -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", @@ -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")); @@ -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": [ @@ -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] diff --git a/crates/cli/src/subcommands/dev.rs b/crates/cli/src/subcommands/dev.rs index ce831d5e0ef..a75fc8e523c 100644 --- a/crates/cli/src/subcommands/dev.rs +++ b/crates/cli/src/subcommands/dev.rs @@ -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 = db_names_for_logging diff --git a/crates/cli/src/subcommands/publish.rs b/crates/cli/src/subcommands/publish.rs index 532cfacf794..30faf5e6052 100644 --- a/crates/cli/src/subcommands/publish.rs +++ b/crates/cli/src/subcommands/publish.rs @@ -28,7 +28,7 @@ pub fn build_publish_schema(command: &clap::Command) -> Result