3131// -------
3232//
3333// ### Standard library
34- use std:: { ffi:: OsStr , fs, io, path:: Path , process:: Command } ;
34+ use std:: {
35+ ffi:: OsStr ,
36+ fs, io,
37+ path:: { Path , PathBuf } ,
38+ process:: Command ,
39+ } ;
3540
3641// ### Third-party
3742use clap:: { Parser , Subcommand } ;
3843use cmd_lib:: run_cmd;
3944use current_platform:: CURRENT_PLATFORM ;
45+ use path_slash:: PathBufExt ;
4046use regex:: Regex ;
4147
4248// ### Local
@@ -68,6 +74,12 @@ enum Commands {
6874 Test ,
6975 /// Build everything.
7076 Build ,
77+ /// Build the Client.
78+ ClientBuild {
79+ /// True to build for distribution, instead of development.
80+ #[ arg( short, long, default_value_t = false ) ]
81+ dist : bool ,
82+ } ,
7183 /// Change the version for the client, server, and extensions.
7284 ChangeVersion {
7385 /// The new version number, such as "0.1.1".
@@ -98,17 +110,18 @@ enum Commands {
98110// These functions are called by the build support functions.
99111/// On Windows, scripts must be run from a shell; on Linux and OS X, scripts are
100112/// directly executable. This function runs a script regardless of OS.
101- fn run_script < T : AsRef < OsStr > , P : AsRef < Path > + std:: fmt:: Display > (
113+ fn run_script < T : AsRef < Path > , A : AsRef < OsStr > , P : AsRef < Path > + std:: fmt:: Display > (
102114 // The script to run.
103115 script : T ,
104116 // Arguments to pass.
105- args : & [ T ] ,
117+ args : & [ A ] ,
106118 // The directory to run the script in.
107119 dir : P ,
108120 // True to report errors based on the process' exit code; false to ignore
109121 // the code.
110122 check_exit_code : bool ,
111123) -> io:: Result < ( ) > {
124+ let script = OsStr :: new ( script. as_ref ( ) ) ;
112125 let mut process;
113126 if cfg ! ( windows) {
114127 process = Command :: new ( "cmd" ) ;
@@ -136,9 +149,11 @@ fn run_script<T: AsRef<OsStr>, P: AsRef<Path> + std::fmt::Display>(
136149/// programs (`robocopy`/`rsync`) to accomplish this. Very important: the `src`
137150/// **must** end with a `/`, otherwise the Windows and Linux copies aren't
138151/// identical.
139- fn quick_copy_dir < P : AsRef < OsStr > > ( src : P , dest : P , files : Option < P > ) -> io:: Result < ( ) > {
152+ fn quick_copy_dir < P : AsRef < Path > > ( src : P , dest : P , files : Option < P > ) -> io:: Result < ( ) > {
140153 assert ! ( src. as_ref( ) . to_string_lossy( ) . ends_with( '/' ) ) ;
141154 let mut copy_process;
155+ let src = OsStr :: new ( src. as_ref ( ) ) ;
156+ let dest = OsStr :: new ( dest. as_ref ( ) ) ;
142157 #[ cfg( windows) ]
143158 {
144159 // From `robocopy /?`:
@@ -165,31 +180,26 @@ fn quick_copy_dir<P: AsRef<OsStr>>(src: P, dest: P, files: Option<P>) -> io::Res
165180 . args ( [
166181 "/MIR" , "/MT" , "/NFL" , "/NDL" , "/NJH" , "/NJS" , "/NP" , "/NS" , "/NC" ,
167182 ] )
168- . arg ( & src)
169- . arg ( & dest) ;
183+ . arg ( src)
184+ . arg ( dest) ;
170185 // Robocopy expects the files to copy after the dest.
171186 if let Some ( files_) = & files {
172- copy_process. arg ( files_) ;
187+ copy_process. arg ( OsStr :: new ( files_. as_ref ( ) ) ) ;
173188 }
174189 }
175190 #[ cfg( not( windows) ) ]
176191 {
177192 // Create the dest directory, since old CI OSes don't support `rsync
178193 // --mkpath`.
179- run_script (
180- "mkdir" ,
181- & [ "-p" , dest. as_ref ( ) . to_str ( ) . unwrap ( ) ] ,
182- "./" ,
183- true ,
184- ) ?;
194+ run_script ( "mkdir" , & [ "-p" , dest. to_str ( ) . unwrap ( ) ] , "./" , true ) ?;
185195 let mut tmp;
186196 let src_combined = match files. as_ref ( ) {
187197 Some ( files_) => {
188- tmp = src. as_ref ( ) . to_os_string ( ) ;
189- tmp. push ( files_) ;
198+ tmp = src. to_os_string ( ) ;
199+ tmp. push ( OsStr :: new ( files_. as_ref ( ) ) ) ;
190200 tmp. as_os_str ( )
191201 }
192- None => src. as_ref ( ) ,
202+ None => src,
193203 } ;
194204
195205 // Use bash to perform globbing, since rsync doesn't do this.
@@ -199,7 +209,7 @@ fn quick_copy_dir<P: AsRef<OsStr>>(src: P, dest: P, files: Option<P>) -> io::Res
199209 format ! (
200210 "rsync --archive --delete {} {}" ,
201211 & src_combined. to_str( ) . unwrap( ) ,
202- & dest. as_ref ( ) . to_str( ) . unwrap( )
212+ & dest. to_str( ) . unwrap( )
203213 )
204214 . as_str ( ) ,
205215 ] ) ;
@@ -410,7 +420,7 @@ fn run_test() -> io::Result<()> {
410420fn run_build ( ) -> io:: Result < ( ) > {
411421 // Clean out all bundled files before the rebuild.
412422 remove_dir_all_if_exists ( "../client/static/bundled" ) ?;
413- run_script ( "npm" , & [ "run" , "build" ] , "../client" , true ) ?;
423+ run_client_build ( false ) ?;
414424 run_script ( "npm" , & [ "run" , "compile" ] , "../extensions/VSCode" , true ) ?;
415425 run_cmd ! (
416426 cargo build --manifest-path=../builder/Cargo . toml;
@@ -419,6 +429,82 @@ fn run_build() -> io::Result<()> {
419429 Ok ( ( ) )
420430}
421431
432+ // Build the NPM Client.
433+ fn run_client_build (
434+ // True to build for distribution, not development.
435+ dist : bool ,
436+ ) -> io:: Result < ( ) > {
437+ let esbuild = PathBuf :: from_slash ( "node_modules/.bin/esbuild" ) ;
438+ let distflag = if dist { "--minify" } else { "--sourcemap" } ;
439+ // This makes the program work from either the `server/` or `client/` directories.
440+ let rel_path = "../client" ;
441+
442+ // The main build for the Client.
443+ run_script (
444+ & esbuild,
445+ & [
446+ "src/CodeChatEditorFramework.mts" ,
447+ "src/CodeChatEditor.mts" ,
448+ "src/CodeChatEditor-test.mts" ,
449+ "src/css/CodeChatEditorProject.css" ,
450+ "src/css/CodeChatEditor.css" ,
451+ "--bundle" ,
452+ "--outdir=./static/bundled" ,
453+ distflag,
454+ "--format=esm" ,
455+ "--splitting" ,
456+ "--metafile=meta.json" ,
457+ "--entry-names=[dir]/[name]-[hash]" ,
458+ ] ,
459+ rel_path,
460+ true ,
461+ ) ?;
462+ // <a id="#pdf.js></a>The PDF viewer for use with VSCode. Built it separately, since it's loaded apart from the rest of the Client.
463+ run_script (
464+ & esbuild,
465+ & [
466+ "src/pdf.js/viewer.mjs" ,
467+ "node_modules/pdfjs-dist/build/pdf.worker.mjs" ,
468+ "--bundle" ,
469+ "--outdir=./static/bundled" ,
470+ distflag,
471+ "--format=esm" ,
472+ "--loader:.png=dataurl" ,
473+ "--loader:.svg=dataurl" ,
474+ "--loader:.gif=dataurl" ,
475+ ] ,
476+ rel_path,
477+ true ,
478+ ) ?;
479+ // Copy over the cmap (color map?) files, which the bundler doesn't handle.
480+ quick_copy_dir (
481+ format ! ( "{rel_path}/node_modules/pdfjs-dist/cmaps/" ) ,
482+ format ! ( "{rel_path}/static/bundled/node_modules/pdfjs-dist/cmaps/" ) ,
483+ None ,
484+ ) ?;
485+ // The HashReader isn't bundled; instead, it's used to translate the JSON metafile produced by the main esbuild run to the simpler format used by the CodeChat Editor. TODO: rewrite this in Rust.
486+ run_script (
487+ & esbuild,
488+ & [
489+ "src/HashReader.mts" ,
490+ "--outdir=." ,
491+ "--platform=node" ,
492+ "--format=esm" ,
493+ ] ,
494+ rel_path,
495+ true ,
496+ ) ?;
497+ run_script ( "node" , & [ "HashReader.js" ] , rel_path, true ) ?;
498+ // Finally, check the TypeScript with the (slow) TypeScript compiler.
499+ run_script (
500+ PathBuf :: from_slash ( "node_modules/.bin/tsc" ) ,
501+ & [ "-noEmit" ] ,
502+ rel_path,
503+ true ,
504+ ) ?;
505+ Ok ( ( ) )
506+ }
507+
422508fn run_change_version ( new_version : & String ) -> io:: Result < ( ) > {
423509 let replacement_string = format ! ( "${{1}}{new_version}${{2}}" ) ;
424510 search_and_replace_file (
@@ -490,6 +576,7 @@ impl Cli {
490576 Commands :: Update => run_update ( ) ,
491577 Commands :: Test => run_test ( ) ,
492578 Commands :: Build => run_build ( ) ,
579+ Commands :: ClientBuild { dist } => run_client_build ( * dist) ,
493580 Commands :: ChangeVersion { new_version } => run_change_version ( new_version) ,
494581 Commands :: Prerelease => run_prerelease ( ) ,
495582 Commands :: Postrelease { target, .. } => run_postrelease ( target) ,
0 commit comments