1- use crate :: common_args;
21use crate :: config:: Config ;
2+ use crate :: generate:: Language ;
33use crate :: subcommands:: init;
44use crate :: util:: {
55 add_auth_header_opt, database_identity, detect_module_language, get_auth_header, get_login_token_or_log_in,
66 spacetime_reverse_dns, ResponseExt ,
77} ;
8+ use crate :: { common_args, generate} ;
89use crate :: { publish, tasks} ;
910use anyhow:: Context ;
1011use clap:: { Arg , ArgAction , ArgMatches , Command } ;
@@ -29,7 +30,7 @@ use tokio::task::JoinHandle;
2930
3031pub fn cli ( ) -> Command {
3132 Command :: new ( "dev" )
32- . about ( "Start development mode with auto-rebuild and publish" )
33+ . about ( "Start development mode with auto-regenerate client module bindings, auto- rebuild, and auto- publish on file changes. " )
3334 . arg (
3435 Arg :: new ( "database" )
3536 . long ( "database" )
@@ -42,6 +43,29 @@ pub fn cli() -> Command {
4243 . default_value ( "." )
4344 . help ( "The path to the project directory" ) ,
4445 )
46+ . arg (
47+ Arg :: new ( "module-bindings-path" )
48+ . long ( "module-bindings-path" )
49+ . value_parser ( clap:: value_parser!( PathBuf ) )
50+ . default_value ( "src/module_bindings" )
51+ . help ( "The path to the module bindings directory relative to the project directory, defaults to `<project-path>/src/module_bindings`" ) ,
52+ )
53+ // NOTE: All server templates must have their server code in `spacetimedb/` directory
54+ // This is not a requirement in general, but is a requirement for all templates
55+ // i.e. `spacetime dev` is valid on non-templates.
56+ . arg (
57+ Arg :: new ( "module-project-path" )
58+ . long ( "module-project-path" )
59+ . value_parser ( clap:: value_parser!( PathBuf ) )
60+ . default_value ( "spacetimedb" )
61+ . help ( "The path to the SpacetimeDB server module project relative to the project directory, defaults to `<project-path>/spacetimedb`" ) ,
62+ )
63+ . arg (
64+ Arg :: new ( "client-lang" )
65+ . long ( "client-lang" )
66+ . value_parser ( clap:: value_parser!( Language ) )
67+ . help ( "The programming language for the generated client module bindings (e.g., typescript, csharp, python). If not specified, it will be detected from the project." ) ,
68+ )
4569 . arg (
4670 Arg :: new ( "local" )
4771 . long ( "local" )
@@ -65,7 +89,10 @@ struct DatabaseRow {
6589
6690pub async fn exec ( mut config : Config , args : & ArgMatches ) -> Result < ( ) , anyhow:: Error > {
6791 let project_path = args. get_one :: < PathBuf > ( "project-path" ) . unwrap ( ) ;
92+ let spacetimedb_project_path = args. get_one :: < PathBuf > ( "module-project-path" ) . unwrap ( ) ;
93+ let module_bindings_path = args. get_one :: < PathBuf > ( "module-bindings-path" ) . unwrap ( ) ;
6894 let use_local = args. get_flag ( "local" ) ;
95+ let client_language = args. get_one :: < Language > ( "client-lang" ) ;
6996 let force = args. get_flag ( "force" ) ;
7097
7198 let server = if let Some ( s) = args. get_one :: < String > ( "server" ) {
@@ -76,7 +103,18 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
76103 Some ( "maincloud" )
77104 } ;
78105
79- let mut spacetimedb_dir = project_path. join ( "spacetimedb" ) ;
106+ if module_bindings_path. is_absolute ( ) {
107+ anyhow:: bail!( "Module bindings path must be a relative path" ) ;
108+ }
109+ let mut module_bindings_dir = project_path. join ( module_bindings_path) ;
110+
111+ if spacetimedb_project_path. is_absolute ( ) {
112+ anyhow:: bail!( "SpacetimeDB project path must be a relative path" ) ;
113+ }
114+ let mut spacetimedb_dir = project_path. join ( spacetimedb_project_path) ;
115+
116+ // Check if we are in a SpacetimeDB project directory
117+
80118 if !spacetimedb_dir. exists ( ) || !spacetimedb_dir. is_dir ( ) {
81119 println ! ( "{}" , "No SpacetimeDB project found in current directory." . yellow( ) ) ;
82120 let should_init = Confirm :: new ( )
@@ -95,7 +133,8 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
95133 let canonical_created_path = created_project_path
96134 . canonicalize ( )
97135 . context ( "Failed to canonicalize created project path" ) ?;
98- spacetimedb_dir = canonical_created_path. join ( "spacetimedb" ) ;
136+ spacetimedb_dir = canonical_created_path. join ( spacetimedb_project_path) ;
137+ module_bindings_dir = canonical_created_path. join ( module_bindings_path) ;
99138
100139 if !spacetimedb_dir. exists ( ) {
101140 anyhow:: bail!( "Project initialization did not create spacetimedb directory" ) ;
@@ -105,6 +144,21 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
105144 }
106145 }
107146
147+ if !module_bindings_dir. exists ( ) {
148+ // Create the module bindings directory if it doesn't exist
149+ std:: fs:: create_dir_all ( & module_bindings_dir) . with_context ( || {
150+ format ! (
151+ "Failed to create module bindings path {}" ,
152+ module_bindings_dir. display( )
153+ )
154+ } ) ?;
155+ } else if !module_bindings_dir. is_dir ( ) {
156+ anyhow:: bail!(
157+ "Module bindings path {} exists but is not a directory." ,
158+ module_bindings_path. display( )
159+ ) ;
160+ }
161+
108162 let use_local = if use_local {
109163 true
110164 } else if config. spacetimedb_token ( ) . is_some ( ) {
@@ -160,7 +214,16 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
160214 println ! ( "{}" , "Press Ctrl+C to stop" . dimmed( ) ) ;
161215 println ! ( ) ;
162216
163- build_and_publish ( & config, & spacetimedb_dir, & database_name, server, use_local) . await ?;
217+ generate_build_and_publish (
218+ & config,
219+ & spacetimedb_dir,
220+ & module_bindings_dir,
221+ & database_name,
222+ client_language,
223+ server,
224+ use_local,
225+ )
226+ . await ?;
164227
165228 let db_identity = database_identity ( & config, & database_name, server) . await ?;
166229 let _log_handle = start_log_stream ( config. clone ( ) , db_identity. to_hex ( ) . to_string ( ) , server) . await ?;
@@ -196,7 +259,17 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
196259 }
197260
198261 println ! ( "\n {}" , "File change detected, rebuilding..." . yellow( ) ) ;
199- match build_and_publish ( & config, & spacetimedb_dir, & database_name, server, use_local) . await {
262+ match generate_build_and_publish (
263+ & config,
264+ & spacetimedb_dir,
265+ & module_bindings_dir,
266+ & database_name,
267+ client_language,
268+ server,
269+ use_local,
270+ )
271+ . await
272+ {
200273 Ok ( _) => { }
201274 Err ( e) => {
202275 eprintln ! ( "{} {}" , "Error:" . red( ) . bold( ) , e) ;
@@ -207,20 +280,45 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
207280 }
208281}
209282
210- async fn build_and_publish (
283+ async fn generate_build_and_publish (
211284 config : & Config ,
212285 project_path : & Path ,
286+ module_bindings_path : & Path ,
213287 database_name : & str ,
288+ client_language : Option < & Language > ,
214289 server : Option < & str > ,
215290 _use_local : bool ,
216291) -> Result < ( ) , anyhow:: Error > {
217- detect_module_language ( project_path) ?;
292+ let module_language = detect_module_language ( project_path) ?;
293+ let client_language = client_language. unwrap_or_else ( || match module_language {
294+ crate :: util:: ModuleLanguage :: Rust => & Language :: Rust ,
295+ crate :: util:: ModuleLanguage :: Csharp => & Language :: Csharp ,
296+ crate :: util:: ModuleLanguage :: Javascript => & Language :: TypeScript ,
297+ } ) ;
298+ let client_language_str = match client_language {
299+ Language :: Rust => "rust" ,
300+ Language :: Csharp => "csharp" ,
301+ Language :: TypeScript => "typescript" ,
302+ Language :: UnrealCpp => "unrealcpp" ,
303+ } ;
218304
219305 println ! ( "{}" , "Building..." . cyan( ) ) ;
220306 let ( _path_to_program, _host_type) =
221307 tasks:: build ( project_path, Some ( Path :: new ( "src" ) ) , false ) . context ( "Failed to build project" ) ?;
222308 println ! ( "{}" , "Build complete!" . green( ) ) ;
223309
310+ println ! ( "{}" , "Generating module bindings..." . cyan( ) ) ;
311+ let generate_args = generate:: cli ( ) . get_matches_from ( vec ! [
312+ "generate" ,
313+ "--lang" ,
314+ client_language_str,
315+ "--project-path" ,
316+ project_path. to_str( ) . unwrap( ) ,
317+ "--out-dir" ,
318+ module_bindings_path. to_str( ) . unwrap( ) ,
319+ ] ) ;
320+ generate:: exec ( config. clone ( ) , & generate_args) . await ?;
321+
224322 println ! ( "{}" , "Publishing..." . cyan( ) ) ;
225323
226324 let project_path_str = project_path. to_str ( ) . unwrap ( ) ;
0 commit comments