11package build
22
33import (
4+ "bufio"
45 "fmt"
6+ "io"
57 "log"
68 "os"
79 "os/exec"
810 "path/filepath"
911 "regexp"
1012 "strconv"
1113 "strings"
14+ "time"
1215
16+ "github.com/wavetermdev/waveterm/tsunami/util"
1317 "golang.org/x/mod/modfile"
1418)
1519
1620type BuildOpts struct {
1721 Dir string
1822 Verbose bool
19- DistPath string
23+ Open bool
24+ ScaffoldPath string
2025 SdkReplacePath string
2126}
2227
@@ -210,40 +215,60 @@ func verifyTsunamiDir(dir string) error {
210215 return nil
211216}
212217
213- func verifyDistPath ( distPath string ) error {
214- if distPath == "" {
215- return fmt .Errorf ("distPath cannot be empty" )
218+ func verifyScaffoldPath ( scaffoldPath string ) error {
219+ if scaffoldPath == "" {
220+ return fmt .Errorf ("scaffoldPath cannot be empty" )
216221 }
217222
218223 // Check if directory exists
219- info , err := os .Stat (distPath )
224+ info , err := os .Stat (scaffoldPath )
220225 if err != nil {
221226 if os .IsNotExist (err ) {
222- return fmt .Errorf ("distPath directory %q does not exist" , distPath )
227+ return fmt .Errorf ("scaffoldPath directory %q does not exist" , scaffoldPath )
223228 }
224- return fmt .Errorf ("error accessing distPath directory %q: %w" , distPath , err )
229+ return fmt .Errorf ("error accessing scaffoldPath directory %q: %w" , scaffoldPath , err )
225230 }
226231
227232 if ! info .IsDir () {
228- return fmt .Errorf ("distPath %q is not a directory" , distPath )
233+ return fmt .Errorf ("scaffoldPath %q is not a directory" , scaffoldPath )
229234 }
230235
231- // Check for index.html file
232- indexPath := filepath .Join (distPath , "index.html" )
233- if err := CheckFileExists (indexPath ); err != nil {
234- return fmt .Errorf ("index.html check failed in distPath %q: %w" , distPath , err )
236+ // Check for dist directory
237+ distPath := filepath .Join (scaffoldPath , "dist" )
238+ if err := IsDirOrNotFound (distPath ); err != nil {
239+ return fmt .Errorf ("dist directory check failed in scaffoldPath %q: %w" , scaffoldPath , err )
240+ }
241+ info , err = os .Stat (distPath )
242+ if err != nil || ! info .IsDir () {
243+ return fmt .Errorf ("dist directory must exist in scaffoldPath %q" , scaffoldPath )
244+ }
245+
246+ // Check for app-main.go file
247+ appMainPath := filepath .Join (scaffoldPath , "app-main.go" )
248+ if err := CheckFileExists (appMainPath ); err != nil {
249+ return fmt .Errorf ("app-main.go check failed in scaffoldPath %q: %w" , scaffoldPath , err )
235250 }
236251
237- // Check for templates/ tailwind.css file
238- tailwindPath := filepath .Join (distPath , "templates" , "tailwind.css" )
252+ // Check for tailwind.css file
253+ tailwindPath := filepath .Join (scaffoldPath , "tailwind.css" )
239254 if err := CheckFileExists (tailwindPath ); err != nil {
240- return fmt .Errorf ("templates/ tailwind.css check failed in distPath %q: %w" , distPath , err )
255+ return fmt .Errorf ("tailwind.css check failed in scaffoldPath %q: %w" , scaffoldPath , err )
241256 }
242257
243- // Check for templates/main.go.tmpl file
244- mainTmplPath := filepath .Join (distPath , "templates" , "main.go.tmpl" )
245- if err := CheckFileExists (mainTmplPath ); err != nil {
246- return fmt .Errorf ("templates/main.go.tmpl check failed in distPath %q: %w" , distPath , err )
258+ // Check for package.json file
259+ packageJsonPath := filepath .Join (scaffoldPath , "package.json" )
260+ if err := CheckFileExists (packageJsonPath ); err != nil {
261+ return fmt .Errorf ("package.json check failed in scaffoldPath %q: %w" , scaffoldPath , err )
262+ }
263+
264+ // Check for node_modules directory
265+ nodeModulesPath := filepath .Join (scaffoldPath , "node_modules" )
266+ if err := IsDirOrNotFound (nodeModulesPath ); err != nil {
267+ return fmt .Errorf ("node_modules directory check failed in scaffoldPath %q: %w" , scaffoldPath , err )
268+ }
269+ info , err = os .Stat (nodeModulesPath )
270+ if err != nil || ! info .IsDir () {
271+ return fmt .Errorf ("node_modules directory must exist in scaffoldPath %q" , scaffoldPath )
247272 }
248273
249274 return nil
@@ -259,7 +284,7 @@ func TsunamiBuild(opts BuildOpts) (*BuildEnv, error) {
259284 return nil , err
260285 }
261286
262- if err := verifyDistPath (opts .DistPath ); err != nil {
287+ if err := verifyScaffoldPath (opts .ScaffoldPath ); err != nil {
263288 return nil , err
264289 }
265290
@@ -284,32 +309,28 @@ func TsunamiBuild(opts BuildOpts) (*BuildEnv, error) {
284309 }
285310
286311 // Copy static directory
287- staticCount , err := copyStaticDir (opts .Dir , tempDir )
312+ staticSrcDir := filepath .Join (opts .Dir , "static" )
313+ staticDestDir := filepath .Join (tempDir , "static" )
314+ staticCount , err := copyDirRecursive (staticSrcDir , staticDestDir , true )
288315 if err != nil {
289316 return nil , fmt .Errorf ("failed to copy static directory: %w" , err )
290317 }
291318
292- // Create dist directory
293- distDir := filepath .Join (tempDir , "dist" )
294- if err := os .MkdirAll (distDir , 0755 ); err != nil {
295- return nil , fmt .Errorf ("failed to create dist directory: %w" , err )
296- }
297-
298- // Copy dist directory contents
299- distCount , err := copyDirRecursive (opts .DistPath , distDir )
319+ // Copy scaffold directory contents selectively
320+ scaffoldCount , err := copyScaffoldSelective (opts .ScaffoldPath , tempDir )
300321 if err != nil {
301- return nil , fmt .Errorf ("failed to copy dist directory: %w" , err )
322+ return nil , fmt .Errorf ("failed to copy scaffold directory: %w" , err )
302323 }
303324
304325 if opts .Verbose {
305- log .Printf ("Copied %d go files, %d static files, %d dist files\n " , goCount , staticCount , distCount )
326+ log .Printf ("Copied %d go files, %d static files, %d scaffold files\n " , goCount , staticCount , scaffoldCount )
306327 }
307328
308- // Copy main.go.tmpl from dist/templates to temp dir as main-app.go
309- mainTmplSrc := filepath .Join (opts . DistPath , "templates" , " main.go.tmpl " )
310- mainTmplDest := filepath .Join (tempDir , "main-app.go" )
311- if err := copyFile ( mainTmplSrc , mainTmplDest ); err != nil {
312- return nil , fmt .Errorf ("failed to copy main.go.tmpl : %w" , err )
329+ // Copy app- main.go from scaffold to main-app.go in temp dir
330+ appMainSrc := filepath .Join (tempDir , "app- main.go" )
331+ appMainDest := filepath .Join (tempDir , "main-app.go" )
332+ if err := os . Rename ( appMainSrc , appMainDest ); err != nil {
333+ return nil , fmt .Errorf ("failed to rename app- main.go to main-app.go : %w" , err )
313334 }
314335
315336 // Create go.mod file
@@ -319,7 +340,7 @@ func TsunamiBuild(opts BuildOpts) (*BuildEnv, error) {
319340 }
320341
321342 // Generate Tailwind CSS
322- if err := generateAppTailwindCss (opts . DistPath , tempDir , opts .Verbose ); err != nil {
343+ if err := generateAppTailwindCss (tempDir , opts .Verbose ); err != nil {
323344 return nil , fmt .Errorf ("failed to generate tailwind css: %w" , err )
324345 }
325346
@@ -368,14 +389,8 @@ func runGoBuild(tempDir string, verbose bool) error {
368389 return nil
369390}
370391
371- func generateAppTailwindCss (distPath , tempDir string , verbose bool ) error {
372- // Copy tailwind.css from dist/templates to temp dir
373- tailwindSrc := filepath .Join (distPath , "templates" , "tailwind.css" )
374- tailwindDest := filepath .Join (tempDir , "tailwind.css" )
375- if err := copyFile (tailwindSrc , tailwindDest ); err != nil {
376- return fmt .Errorf ("failed to copy tailwind.css: %w" , err )
377- }
378-
392+ func generateAppTailwindCss (tempDir string , verbose bool ) error {
393+ // tailwind.css is already in tempDir from scaffold copy
379394 tailwindOutput := filepath .Join (tempDir , "static" , "tw.css" )
380395
381396 tailwindCmd := exec .Command ("npx" , "@tailwindcss/cli" ,
@@ -400,22 +415,6 @@ func generateAppTailwindCss(distPath, tempDir string, verbose bool) error {
400415 return nil
401416}
402417
403- func copyStaticDir (srcDir , destDir string ) (int , error ) {
404- // Always create static directory in temp dir
405- staticDestDir := filepath .Join (destDir , "static" )
406- if err := os .MkdirAll (staticDestDir , 0755 ); err != nil {
407- return 0 , fmt .Errorf ("failed to create static directory: %w" , err )
408- }
409-
410- // Copy static/ directory contents if it exists
411- staticSrcDir := filepath .Join (srcDir , "static" )
412- if _ , err := os .Stat (staticSrcDir ); err == nil {
413- return copyDirRecursive (staticSrcDir , staticDestDir )
414- }
415-
416- return 0 , nil
417- }
418-
419418func copyGoFiles (srcDir , destDir string ) (int , error ) {
420419 entries , err := os .ReadDir (srcDir )
421420 if err != nil {
@@ -456,15 +455,72 @@ func TsunamiRun(opts BuildOpts) error {
456455 log .Printf ("Running tsunami app from %s" , opts .Dir )
457456
458457 runCmd .Stdin = os .Stdin
459- if opts .Verbose {
460- log .Printf ("Executing: %s" , appPath )
458+
459+ if opts .Open {
460+ // If --open flag is set, we need to capture stderr to parse the listening message
461+ stderr , err := runCmd .StderrPipe ()
462+ if err != nil {
463+ return fmt .Errorf ("failed to create stderr pipe: %w" , err )
464+ }
461465 runCmd .Stdout = os .Stdout
462- runCmd .Stderr = os .Stderr
463- }
464466
465- if err := runCmd .Run (); err != nil {
466- return fmt .Errorf ("failed to run application: %w" , err )
467+ if err := runCmd .Start (); err != nil {
468+ return fmt .Errorf ("failed to start application: %w" , err )
469+ }
470+
471+ // Monitor stderr for the listening message
472+ go monitorAndOpenBrowser (stderr , opts .Verbose )
473+
474+ if err := runCmd .Wait (); err != nil {
475+ return fmt .Errorf ("application exited with error: %w" , err )
476+ }
477+ } else {
478+ // Normal execution without browser opening
479+ if opts .Verbose {
480+ log .Printf ("Executing: %s" , appPath )
481+ runCmd .Stdout = os .Stdout
482+ runCmd .Stderr = os .Stderr
483+ }
484+
485+ if err := runCmd .Run (); err != nil {
486+ return fmt .Errorf ("failed to run application: %w" , err )
487+ }
467488 }
468489
469490 return nil
470491}
492+
493+ func monitorAndOpenBrowser (stdout io.ReadCloser , verbose bool ) {
494+ defer stdout .Close ()
495+
496+ scanner := bufio .NewScanner (stdout )
497+ urlRegex := regexp .MustCompile (`\[tsunami\] listening at (http://[^\s]+)` )
498+ browserOpened := false
499+ if verbose {
500+ log .Printf ("monitoring for browser open\n " )
501+ }
502+
503+ for scanner .Scan () {
504+ line := scanner .Text ()
505+ if verbose {
506+ log .Println (line )
507+ }
508+
509+ if ! browserOpened && len (urlRegex .FindStringSubmatch (line )) > 1 {
510+ matches := urlRegex .FindStringSubmatch (line )
511+ url := matches [1 ]
512+ if verbose {
513+ log .Printf ("Opening browser to %s" , url )
514+ }
515+ go util .OpenBrowser (url , 100 * time .Millisecond )
516+ browserOpened = true
517+ }
518+ }
519+
520+ // Continue reading and printing output if verbose
521+ if verbose {
522+ for scanner .Scan () {
523+ log .Println (scanner .Text ())
524+ }
525+ }
526+ }
0 commit comments