Skip to content

Commit 5325644

Browse files
cloutiertylerclockwork-labs-botgefjonjdetter
authored
spacetime.json updates (#4504)
# Description of Changes Adds a new reference documentation page for `spacetime.json` and fixes several bugs where the CLI behavior diverged from the proposal. **Docs:** - New page at `/cli-reference/spacetime-json` covering config structure, field reference, generate configuration, children/inheritance, `spacetime dev` config, database selection, flag overrides, `--no-config`, `--env`/environments, config file discovery, and editor support **Bug fixes:** - `generate` was incorrectly inherited by child databases. A child with a different `module-path` would silently inherit the parent's generate entries, causing bindings to be written to the wrong output directory. Generate is now never inherited, matching the proposal. - The source conflict rule for `module-path`/`bin-path`/`js-path` was not implemented during inheritance. A child specifying `module-path` could still inherit `bin-path` from the parent. Now, if a child specifies any module source, the others are not inherited. - `--num-replicas` was not marked as a per-database override, so it could be used with multiple databases selected without error. # API and ABI breaking changes None. These are bug fixes aligning the implementation with the intended behavior from the proposal: - `generate` inheritance was never documented or intended - The source conflict rule was specified in the proposal but not implemented - `--num-replicas` as per-database is consistent with the other module-source flags # Expected complexity level and risk 2 - The changes are small and well-scoped. The generate inheritance fix simplifies the code (removes a parameter). The source conflict rule adds a straightforward check during field inheritance. Tests have been updated to match. # Testing - [x] All 136 existing CLI tests pass - [x] Updated tests for generate non-inheritance behavior - [x] Docs site builds successfully, page renders in sidebar - [ ] Manual test: verify a child with a different `module-path` no longer inherits parent's `generate` - [ ] Manual test: verify `--num-replicas` errors when multiple databases are selected --------- Signed-off-by: Tyler Cloutier <cloutiertyler@users.noreply.github.com> Co-authored-by: clockwork-labs-bot <clockwork-labs-bot@users.noreply.github.com> Co-authored-by: Phoebe Goldman <phoebe@clockworklabs.io> Co-authored-by: John Detter <4099508+jdetter@users.noreply.github.com>
1 parent c659dc9 commit 5325644

4 files changed

Lines changed: 385 additions & 39 deletions

File tree

crates/cli/src/spacetime_config.rs

Lines changed: 44 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -164,46 +164,53 @@ pub struct LoadedConfig {
164164

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

174-
fn collect_targets_inner(
175-
&self,
176-
parent_fields: Option<&HashMap<String, Value>>,
177-
parent_generate: Option<&Vec<HashMap<String, Value>>>,
178-
) -> Vec<FlatTarget> {
174+
fn collect_targets_inner(&self, parent_fields: Option<&HashMap<String, Value>>) -> Vec<FlatTarget> {
175+
// module-path, bin-path, and js-path are mutually exclusive module sources.
176+
// If a child specifies any one, the other two are not inherited from the parent.
177+
const MODULE_SOURCE_KEYS: &[&str] = &["module-path", "bin-path", "js-path"];
178+
let child_specifies_source = MODULE_SOURCE_KEYS
179+
.iter()
180+
.any(|k| self.additional_fields.contains_key(*k));
181+
179182
// Build this node's fields by inheriting from parent
180183
let mut fields = self.additional_fields.clone();
181184
if let Some(parent) = parent_fields {
182185
for (key, value) in parent {
183-
if !fields.contains_key(key) {
184-
fields.insert(key.clone(), value.clone());
186+
if fields.contains_key(key) {
187+
continue;
188+
}
189+
// If the child specifies any module source, skip inheriting the others
190+
if child_specifies_source && MODULE_SOURCE_KEYS.contains(&key.as_str()) {
191+
continue;
185192
}
193+
fields.insert(key.clone(), value.clone());
186194
}
187195
}
188196

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

196203
let target = FlatTarget {
197204
fields: fields.clone(),
198205
source_config: self.source_config.clone(),
199-
generate: effective_generate.clone(),
206+
generate: effective_generate,
200207
};
201208

202209
let mut result = vec![target];
203210

204211
if let Some(children) = &self.children {
205212
for child in children {
206-
let child_targets = child.collect_targets_inner(Some(&fields), effective_generate.as_ref());
213+
let child_targets = child.collect_targets_inner(Some(&fields));
207214
result.extend(child_targets);
208215
}
209216
}
@@ -2595,8 +2602,8 @@ mod tests {
25952602
}
25962603

25972604
#[test]
2598-
fn test_generate_inheritance_from_parent() {
2599-
// Children inherit generate from parent if they don't define their own
2605+
fn test_generate_not_inherited_from_parent() {
2606+
// Generate is never inherited. Children must define their own.
26002607
let json = r#"{
26012608
"database": "parent-db",
26022609
"server": "local",
@@ -2627,15 +2634,10 @@ mod tests {
26272634
Some("typescript")
26282635
);
26292636

2630-
// Child 1 inherits parent's generate
2631-
let child1_gen = targets[1].generate.as_ref().unwrap();
2632-
assert_eq!(child1_gen.len(), 1);
2633-
assert_eq!(
2634-
child1_gen[0].get("language").and_then(|v| v.as_str()),
2635-
Some("typescript")
2636-
);
2637+
// Child 1 does not inherit parent's generate
2638+
assert!(targets[1].generate.is_none());
26372639

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

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

3099-
// All 3 targets (parent + 2 children) share the same module-path and generate
31003103
assert_eq!(targets.len(), 3);
3104+
3105+
// Parent has generate
3106+
assert!(targets[0].generate.is_some());
3107+
3108+
// Children do not inherit generate
3109+
assert!(targets[1].generate.is_none());
3110+
assert!(targets[2].generate.is_none());
3111+
3112+
// All share the same module-path via field inheritance
31013113
for target in &targets {
31023114
assert_eq!(
31033115
target.fields.get("module-path").and_then(|v| v.as_str()),
31043116
Some("./server")
31053117
);
3106-
let r#gen = target.generate.as_ref().unwrap();
3107-
assert_eq!(r#gen.len(), 1);
3108-
assert_eq!(r#gen[0].get("language").and_then(|v| v.as_str()), Some("typescript"));
31093118
}
3110-
3111-
// All have the same (module-path, generate) so dedup should reduce to 1
3112-
// (this is verified in generate.rs tests, but we confirm the data here)
31133119
}
31143120

31153121
#[test]

crates/cli/src/subcommands/dev.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
676676
}
677677

678678
// Safety prompt: warn if any selected database target is defined in spacetime.json.
679+
// spacetime.local.json is gitignored and personal, so it's fine for dev use.
679680
if let Some(ref lc) = loaded_config {
680681
let database_sources = resolve_database_sources(&lc.config);
681682
let databases_from_main_config: Vec<String> = db_names_for_logging

crates/cli/src/subcommands/publish.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ pub fn build_publish_schema(command: &clap::Command) -> Result<CommandSchema, an
2828
.key(Key::new("build_options").module_specific())
2929
.key(Key::new("wasm_file").module_specific())
3030
.key(Key::new("js_file").module_specific())
31-
.key(Key::new("num_replicas"))
31+
.key(Key::new("num_replicas").module_specific())
3232
.key(Key::new("break_clients"))
3333
.key(Key::new("anon_identity"))
3434
.key(Key::new("parent"))

0 commit comments

Comments
 (0)