Skip to content

Commit b5ac154

Browse files
committed
build/run working!
1 parent bea4592 commit b5ac154

11 files changed

Lines changed: 367 additions & 75 deletions

File tree

Taskfile.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,31 @@ tasks:
451451
cmd: npm run dev
452452
dir: tsunami/frontend
453453

454+
tsunami:frontend:build:
455+
desc: Build the tsunami frontend
456+
cmd: yarn build
457+
dir: tsunami/frontend
458+
459+
tsunami:scaffold:
460+
desc: Build scaffold for tsunami frontend development
461+
dir: tsunami/frontend
462+
deps:
463+
- tsunami:frontend:build
464+
cmds:
465+
- cmd: "{{.RMRF}} scaffold"
466+
ignore_error: true
467+
- mkdir scaffold
468+
- cd scaffold && npm --no-workspaces init -y --init-license Apache-2.0
469+
- cd scaffold && npm pkg set name=tsunami-scaffold
470+
- cd scaffold && npm pkg delete author
471+
- cd scaffold && npm pkg set author.name="Command Line Inc"
472+
- cd scaffold && npm pkg set author.email="info@commandline.dev"
473+
- cd scaffold && npm --no-workspaces install tailwindcss @tailwindcss/cli
474+
- cp -r dist scaffold/
475+
- cp ../templates/app-main.go.tmpl scaffold/app-main.go
476+
- cp ../templates/tailwind.css scaffold/
477+
- cp ../templates/gitignore.tmpl scaffold/.gitignore
478+
454479
tsunami:build:
455480
desc: Build the tsunami binary.
456481
cmds:

tsunami/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
bin/

tsunami/app/tsunamiapp.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,9 @@ func (c *Client) listenAndServe(ctx context.Context) error {
193193
return fmt.Errorf("failed to listen: %v", err)
194194
}
195195

196-
// Log the port we're listening on
196+
// Log the address we're listening on
197197
port := listener.Addr().(*net.TCPAddr).Port
198-
log.Printf("[tsunami] listening on port %d", port)
198+
log.Printf("[tsunami] listening at http://localhost:%d", port)
199199

200200
// Serve in a goroutine so we don't block
201201
go func() {

tsunami/build/build.go

Lines changed: 123 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
11
package build
22

33
import (
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

1620
type 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-
419418
func 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

Comments
 (0)