Skip to content

Commit a4d29da

Browse files
cloutiertylerdrogusjdetterclockwork-labs-bot
authored
Fix spacetime dev template issues and clean up CLI (#4396)
## Summary Multiple fixes and improvements to `spacetime dev`, `spacetime publish`, `spacetime init`, and templates: ### CLI fixes - **Fix `spacetime dev` watch path and .env.local handling** for templates with a `spacetimedb/` subdirectory - **Validate package manager availability** before installing dependencies — re-prompts if the selected PM isn't on PATH - **Remove JS/TS beta warning** from `spacetime publish` - **Remove unstable warning** from `spacetime init` - **Remove unused `spacetime energy` command** (can be re-added later when properly implemented) - **Fix `tsc` detection on Windows** — `set_extension(".cmd")` produced `tsc..cmd` (double dot); fixed to `set_extension("cmd")` - **Add `typescript` as devDependency** to all server template `spacetimedb/package.json` files so `tsc` is available during build - **Strip `\?\` extended-length path prefix** from user-facing output in both `dev` and `publish` commands (produced by `fs::canonicalize()` on Windows) - **Fix publish reporting success when user declines** non-local server prompt — changed `return Ok(())` to `bail!()` consistent with other abort cases - **Forward stdin to client process** in `spacetime dev` so interactive CLI templates work ### Template fixes - **Fix Angular template environment variables** — Angular's esbuild builder doesn't support `import.meta.env`, so we use a dev script to generate `environment.local.ts` with Angular's native `fileReplacements` pattern - **Fix TanStack Start template** — rename `createRouter` to `getRouter` (v1.121+ breaking change), upgrade to React 19, and add `@vitejs/plugin-react` for automatic JSX runtime support - **Fix `moduleResolution` in remix-ts and nextjs-ts** server tsconfigs — change from `"node"` to `"bundler"` to support subpath exports like `spacetimedb/server` - **Fix browser-ts template** — convert from static HTML + IIFE bundle to standard Vite dev server app so it works with `spacetime dev` and reads env vars from `.env.local` - **Add `deno` as devDependency** to deno-ts template (matches how bun-ts includes bun) - **Convert basic-ts from empty web app to CLI app** matching basic-rs — connect, subscribe to person table, print on insert ## Test plan - [x] `cargo test -p spacetimedb-cli` — all 133 tests pass - [ ] `spacetime dev --template angular-ts` — verify Angular app connects with correct database name - [ ] `spacetime dev --template react-ts` — verify React template still works - [ ] `spacetime dev --template tanstack-ts` — verify TanStack Start template loads without errors - [ ] `spacetime dev --template remix-ts` — verify Remix template builds and runs - [ ] `spacetime dev --template nextjs-ts` — verify Next.js template still works - [ ] `spacetime dev --template browser-ts` — verify Vite dev server starts and app connects - [ ] `spacetime dev --template bun-ts` — verify Bun CLI template runs interactively - [ ] `spacetime dev --template basic-ts` — verify CLI connects and prints on insert - [ ] Verify no `\?\` prefix in any displayed paths on Windows - [ ] Verify no `tsc not found` warning when building server modules - [ ] Select unavailable package manager at init prompt — verify re-prompt - [ ] Decline non-local server prompt — verify clean error instead of "Published successfully!" --------- Co-authored-by: Piotr Sarnacki <drogus@gmail.com> Co-authored-by: John Detter <4099508+jdetter@users.noreply.github.com> Co-authored-by: clockwork-labs-bot <bot@clockworklabs.com>
1 parent e9ecfa9 commit a4d29da

79 files changed

Lines changed: 1191 additions & 1043 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

crates/cli/src/common_args.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,6 @@ pub fn server() -> Arg {
1515
.help("The nickname, host name or URL of the server")
1616
}
1717

18-
pub fn identity() -> Arg {
19-
Arg::new("identity")
20-
.long("identity")
21-
.short('i')
22-
.help("The identity to use")
23-
}
24-
2518
pub fn anonymous() -> Arg {
2619
Arg::new("anon_identity")
2720
.long("anonymous")

crates/cli/src/detect.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,21 @@ pub(crate) fn has_wasm32_target() -> bool {
5757
})
5858
}
5959

60+
/// Check if a given `PackageManager` executable is available on `PATH`.
61+
///
62+
/// On Windows, npm/pnpm/yarn are `.cmd` shims while bun is a `.exe`,
63+
/// so we check the platform-appropriate extension.
64+
pub(crate) fn has_package_manager(pm: crate::spacetime_config::PackageManager) -> bool {
65+
let name = pm.to_string();
66+
if cfg!(windows) {
67+
// bun ships as bun.exe; npm, pnpm, yarn are .cmd shims
68+
let ext = if name == "bun" { "exe" } else { "cmd" };
69+
find_executable(format!("{name}.{ext}")).is_some()
70+
} else {
71+
find_executable(&name).is_some()
72+
}
73+
}
74+
6075
#[cfg(test)]
6176
mod tests {
6277
use super::*;

crates/cli/src/lib.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ pub fn get_subcommands() -> Vec<Command> {
2727
call::cli(),
2828
describe::cli(),
2929
dev::cli(),
30-
energy::cli(),
3130
sql::cli(),
3231
dns::cli(),
3332
generate::cli(),
@@ -54,7 +53,6 @@ pub async fn exec_subcommand(
5453
"call" => call::exec(config, args).await,
5554
"describe" => describe::exec(config, args).await,
5655
"dev" => dev::exec(config, args).await,
57-
"energy" => energy::exec(config, args).await,
5856
"publish" => publish::exec(config, args).await,
5957
"delete" => delete::exec(config, args).await,
6058
"logs" => logs::exec(config, args).await,

crates/cli/src/subcommands/dev.rs

Lines changed: 61 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::spacetime_config::{
77
use crate::subcommands::init;
88
use crate::util::{
99
add_auth_header_opt, database_identity, find_module_path, get_auth_header, get_login_token_or_log_in,
10-
spacetime_reverse_dns, ResponseExt,
10+
spacetime_reverse_dns, strip_verbatim_prefix, ResponseExt,
1111
};
1212
use crate::{common_args, generate};
1313
use crate::{publish, tasks};
@@ -228,7 +228,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
228228
.map(|lc| {
229229
lc.loaded_files
230230
.iter()
231-
.map(|f| f.display().to_string())
231+
.map(|f| strip_verbatim_prefix(f).display().to_string())
232232
.collect::<Vec<_>>()
233233
.join(", ")
234234
})
@@ -279,7 +279,11 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
279279

280280
// Save to root `spacetime.json` (not env/local overlays), then reload merged config.
281281
let saved_path = save_root_module_path_to_spacetime_json(&config_dir, &provided_module_path)?;
282-
println!("{} Updated {}", "✓".green(), saved_path.display());
282+
println!(
283+
"{} Updated {}",
284+
"✓".green(),
285+
strip_verbatim_prefix(&saved_path).display()
286+
);
283287

284288
loaded_config = find_and_load_with_env_from(Some(env), project_dir.clone())
285289
.with_context(|| "Failed to reload spacetime.json after updating module-path")?;
@@ -402,10 +406,9 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
402406
// If the project was created in a subdirectory, hint the user to cd into it
403407
// and show useful CLI commands they can run from there.
404408
let current_dir = std::env::current_dir().context("Failed to get current directory")?;
405-
if canonical_created_path != current_dir {
406-
let rel_path = canonical_created_path
407-
.strip_prefix(&current_dir)
408-
.unwrap_or(&canonical_created_path);
409+
let display_path = strip_verbatim_prefix(&canonical_created_path);
410+
if display_path != current_dir {
411+
let rel_path = display_path.strip_prefix(&current_dir).unwrap_or(display_path);
409412
println!(
410413
"\n{} To interact with your database, open a new terminal and run:",
411414
"Tip:".yellow().bold(),
@@ -474,7 +477,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
474477

475478
if !no_config {
476479
if let Some(path) = create_default_spacetime_config_if_missing(&project_dir)? {
477-
println!("{} Created {}", "✓".green(), path.display());
480+
println!("{} Created {}", "✓".green(), strip_verbatim_prefix(&path).display());
478481
}
479482
}
480483

@@ -527,7 +530,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
527530
});
528531
if let Some(db_name) = db_to_persist {
529532
if let Some(path) = create_local_spacetime_config_if_missing(&project_dir, db_name)? {
530-
println!("{} Created {}", "✓".green(), path.display());
533+
println!("{} Created {}", "✓".green(), strip_verbatim_prefix(&path).display());
531534
}
532535
}
533536
}
@@ -601,7 +604,11 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
601604
} else if let Some(sc) = spacetime_config {
602605
// Reuse already-loaded config instead of loading again
603606
if let Some(ref lc) = loaded_config {
604-
let files: Vec<_> = lc.loaded_files.iter().map(|f| f.display().to_string()).collect();
607+
let files: Vec<_> = lc
608+
.loaded_files
609+
.iter()
610+
.map(|f| strip_verbatim_prefix(f).display().to_string())
611+
.collect();
605612
println!("{} Using configuration from {}", "✓".green(), files.join(", "));
606613
}
607614

@@ -631,7 +638,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
631638
let db_name_for_client = &db_names_for_logging[0];
632639

633640
// Extract watch directories from publish configs
634-
let watch_dirs = extract_watch_dirs(&publish_configs, &spacetimedb_dir);
641+
let watch_dirs = extract_watch_dirs(&publish_configs, &spacetimedb_dir, &project_dir);
635642

636643
println!("\n{}", "Starting development mode...".green().bold());
637644
if db_names_for_logging.len() == 1 {
@@ -644,13 +651,16 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
644651
if watch_dirs.len() == 1 {
645652
println!(
646653
"Watching for changes in: {}",
647-
watch_dirs.iter().next().unwrap().display().to_string().cyan()
654+
strip_verbatim_prefix(watch_dirs.iter().next().unwrap())
655+
.display()
656+
.to_string()
657+
.cyan()
648658
);
649659
} else {
650660
let watch_dirs_vec: Vec<_> = watch_dirs.iter().collect();
651661
println!("Watching for changes in {} directories:", watch_dirs.len());
652662
for dir in &watch_dirs_vec {
653-
println!(" - {}", dir.display().to_string().cyan());
663+
println!(" - {}", strip_verbatim_prefix(dir).display().to_string().cyan());
654664
}
655665
}
656666

@@ -936,6 +946,31 @@ async fn generate_build_and_publish(
936946
tasks::build(spacetimedb_dir, Some(Path::new("src")), false, None).context("Failed to build project")?;
937947
println!("{}", "Build complete!".green());
938948

949+
// For TypeScript client, always update .env.local with the database name
950+
// from config so the client connects to the correct database.
951+
if let Some(first_config) = publish_configs.first() {
952+
let is_ts_client = client_language == Some(&Language::TypeScript)
953+
|| generate::resolve_language(spacetimedb_dir, client_language.copied())
954+
.map(|l| l == Language::TypeScript)
955+
.unwrap_or(false);
956+
957+
if is_ts_client {
958+
if let Some(first_db_name) = first_config.get_config_value("database").and_then(|v| v.as_str()) {
959+
let server_for_env =
960+
server.or_else(|| first_config.get_config_value("server").and_then(|v| v.as_str()));
961+
962+
println!(
963+
"{} {}...",
964+
"Updating .env.local with database name".cyan(),
965+
first_db_name
966+
);
967+
let env_path = project_dir.join(".env.local");
968+
let server_host_url = config.get_host_url(server_for_env)?;
969+
upsert_env_db_names_and_hosts(&env_path, &server_host_url, first_db_name)?;
970+
}
971+
}
972+
}
973+
939974
if skip_generate {
940975
println!("{}", "Skipping generate step (--skip-generate).".dimmed());
941976
} else if using_spacetime_config {
@@ -957,27 +992,6 @@ async fn generate_build_and_publish(
957992
} else {
958993
let resolved_client_language = generate::resolve_language(spacetimedb_dir, client_language.copied())?;
959994

960-
// For TypeScript client, update .env.local with first database name
961-
if resolved_client_language == Language::TypeScript {
962-
let first_config = publish_configs.first().expect("publish_configs cannot be empty");
963-
let first_db_name = first_config
964-
.get_config_value("database")
965-
.and_then(|v| v.as_str())
966-
.expect("database is a required field");
967-
968-
// CLI server takes precedence, otherwise use server from config
969-
let server_for_env = server.or_else(|| first_config.get_config_value("server").and_then(|v| v.as_str()));
970-
971-
println!(
972-
"{} {}...",
973-
"Updating .env.local with database name".cyan(),
974-
first_db_name
975-
);
976-
let env_path = project_dir.join(".env.local");
977-
let server_host_url = config.get_host_url(server_for_env)?;
978-
upsert_env_db_names_and_hosts(&env_path, &server_host_url, first_db_name)?;
979-
}
980-
981995
println!("{}", "Generating module bindings...".cyan());
982996
let generate_entry = generate::build_generate_entry(
983997
Some(spacetimedb_dir),
@@ -1454,6 +1468,7 @@ fn resolve_database_sources(config: &SpacetimeConfig) -> HashMap<String, Option<
14541468
fn extract_watch_dirs(
14551469
publish_configs: &[CommandConfig<'_>],
14561470
default_spacetimedb_dir: &Path,
1471+
project_dir: &Path,
14571472
) -> std::collections::HashSet<PathBuf> {
14581473
use std::collections::HashSet;
14591474
let mut watch_dirs = HashSet::new();
@@ -1462,10 +1477,17 @@ fn extract_watch_dirs(
14621477
let module_path = config_entry
14631478
.get_config_value("module_path")
14641479
.and_then(|v| v.as_str())
1465-
.map(PathBuf::from)
1480+
.map(|s| {
1481+
let p = PathBuf::from(s);
1482+
if p.is_absolute() {
1483+
p
1484+
} else {
1485+
project_dir.join(p)
1486+
}
1487+
})
14661488
.unwrap_or_else(|| default_spacetimedb_dir.to_path_buf());
14671489

1468-
// Canonicalize to handle relative paths
1490+
// Canonicalize to normalize the path
14691491
let canonical_path = module_path.canonicalize().unwrap_or(module_path);
14701492

14711493
watch_dirs.insert(canonical_path);
@@ -1501,7 +1523,7 @@ fn detect_and_save_client_command(project_dir: &Path, existing_config: Option<Sp
15011523
println!(
15021524
"{} Detected client command and saved to {}",
15031525
"✓".green(),
1504-
path.display()
1526+
strip_verbatim_prefix(&path).display()
15051527
);
15061528
}
15071529
Some(detected_cmd)
@@ -1603,7 +1625,7 @@ fn start_client_process(
16031625
.env("SPACETIMEDB_HOST", host_url)
16041626
.stdout(std::process::Stdio::inherit())
16051627
.stderr(std::process::Stdio::inherit())
1606-
.stdin(std::process::Stdio::null())
1628+
.stdin(std::process::Stdio::inherit())
16071629
.kill_on_drop(true)
16081630
.spawn()
16091631
.with_context(|| format!("Failed to start client command: {}", command))?;
@@ -1616,7 +1638,7 @@ fn start_client_process(
16161638
.env("SPACETIMEDB_HOST", host_url)
16171639
.stdout(std::process::Stdio::inherit())
16181640
.stderr(std::process::Stdio::inherit())
1619-
.stdin(std::process::Stdio::null())
1641+
.stdin(std::process::Stdio::inherit())
16201642
.kill_on_drop(true)
16211643
.spawn()
16221644
.with_context(|| format!("Failed to start client command: {}", command))?;

crates/cli/src/subcommands/energy.rs

Lines changed: 0 additions & 72 deletions
This file was deleted.

0 commit comments

Comments
 (0)