github.com/vugu/vugu@v0.3.6-0.20240430171613-3f6f402e014b/gen/parser-go-pkg.go (about) 1 package gen 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "go/ast" 8 "go/parser" 9 "go/token" 10 "log" 11 "os" 12 "path/filepath" 13 "regexp" 14 "strings" 15 "text/template" 16 "time" 17 18 "github.com/vugu/xxhash" 19 ) 20 21 // ParserGoPkg knows how to perform source file generation in relation to a package folder. 22 // Whereas ParserGo handles converting a single template, ParserGoPkg is a higher level interface 23 // and provides the functionality of the vugugen command line tool. It will scan a package 24 // folder for .vugu files and convert them to .go, with the appropriate defaults and logic. 25 type ParserGoPkg struct { 26 pkgPath string 27 opts ParserGoPkgOpts 28 } 29 30 // ParserGoPkgOpts is the options for ParserGoPkg. 31 type ParserGoPkgOpts struct { 32 SkipGoMod bool // do not try and create go.mod if it doesn't exist 33 SkipMainGo bool // do not try and create main_wasm.go if it doesn't exist in a main package 34 TinyGo bool // emit code intended for TinyGo compilation 35 GoFileNameAppend *string // suffix to append to file names, after base name plus .go, if nil then "_gen" is used 36 MergeSingle bool // merge all output files into a single one 37 MergeSingleName string // name of merged output file, only used if MergeSingle is true, defaults to "0_components_gen.go" 38 } 39 40 // TODO: CallVuguSetup bool // always call vuguSetup instead of trying to auto-detect it's existence 41 42 var errNoVuguFile = errors.New("no .vugu file(s) found") 43 44 // RunRecursive will create a new ParserGoPkg and call Run on it recursively for each 45 // directory under pkgPath. The opts will be modified for subfolders to disable go.mod and main.go 46 // logic. If pkgPath does not contain a .vugu file this function will return an error. 47 func RunRecursive(pkgPath string, opts *ParserGoPkgOpts) error { 48 49 if opts == nil { 50 opts = &ParserGoPkgOpts{} 51 } 52 53 dirf, err := os.Open(pkgPath) 54 if err != nil { 55 return err 56 } 57 58 fis, err := dirf.Readdir(-1) 59 if err != nil { 60 return err 61 } 62 hasVugu := false 63 var subDirList []string 64 for _, fi := range fis { 65 if fi.IsDir() && !strings.HasPrefix(fi.Name(), ".") { 66 subDirList = append(subDirList, fi.Name()) 67 continue 68 } 69 if filepath.Ext(fi.Name()) == ".vugu" { 70 hasVugu = true 71 } 72 } 73 if !hasVugu { 74 return errNoVuguFile 75 } 76 77 p := NewParserGoPkg(pkgPath, opts) 78 err = p.Run() 79 if err != nil { 80 return err 81 } 82 83 for _, subDir := range subDirList { 84 subPath := filepath.Join(pkgPath, subDir) 85 opts2 := *opts 86 // sub folders should never get these behaviors 87 opts2.SkipGoMod = true 88 opts2.SkipMainGo = true 89 err := RunRecursive(subPath, &opts2) 90 if err == errNoVuguFile { 91 continue 92 } 93 if err != nil { 94 return err 95 } 96 } 97 98 return nil 99 } 100 101 // Run will create a new ParserGoPkg and call Run on it. 102 func Run(pkgPath string, opts *ParserGoPkgOpts) error { 103 p := NewParserGoPkg(pkgPath, opts) 104 return p.Run() 105 } 106 107 // NewParserGoPkg returns a new ParserGoPkg with the specified options or default if nil. The pkgPath is required and must be an absolute path. 108 func NewParserGoPkg(pkgPath string, opts *ParserGoPkgOpts) *ParserGoPkg { 109 ret := &ParserGoPkg{ 110 pkgPath: pkgPath, 111 } 112 if opts != nil { 113 ret.opts = *opts 114 } 115 return ret 116 } 117 118 // Opts returns the options. 119 func (p *ParserGoPkg) Opts() ParserGoPkgOpts { 120 return p.opts 121 } 122 123 // Run does the work and generates the appropriate .go files from .vugu files. 124 // It will also create a go.mod file if not present and not SkipGoMod. Same for main.go and SkipMainGo (will also skip 125 // if package already has file with package name something other than main). 126 // Per-file code generation is performed by ParserGo. 127 func (p *ParserGoPkg) Run() error { 128 129 // record the times of existing files, so we can restore after if the same 130 hashTimes, err := fileHashTimes(p.pkgPath) 131 if err != nil { 132 return err 133 } 134 135 pkgF, err := os.Open(p.pkgPath) 136 if err != nil { 137 return err 138 } 139 defer pkgF.Close() 140 141 allFileNames, err := pkgF.Readdirnames(-1) 142 if err != nil { 143 return err 144 } 145 146 var vuguFileNames []string 147 for _, fn := range allFileNames { 148 if filepath.Ext(fn) == ".vugu" { 149 vuguFileNames = append(vuguFileNames, fn) 150 } 151 } 152 153 if len(vuguFileNames) == 0 { 154 return fmt.Errorf("no .vugu files found, please create one and try again") 155 } 156 157 pkgName := goGuessPkgName(p.pkgPath) 158 159 namesToCheck := []string{"main"} 160 161 goFnameAppend := "_gen" 162 if p.opts.GoFileNameAppend != nil { 163 goFnameAppend = *p.opts.GoFileNameAppend 164 } 165 166 var mergeFiles []string 167 168 mergeSingleName := "0_components_gen.go" 169 if p.opts.MergeSingleName != "" { 170 mergeSingleName = p.opts.MergeSingleName 171 } 172 173 missingFmap := make(map[string]string, len(vuguFileNames)) 174 175 // run ParserGo on each file to generate the .go files 176 for _, fn := range vuguFileNames { 177 178 baseFileName := strings.TrimSuffix(fn, ".vugu") 179 goFileName := baseFileName + goFnameAppend + ".go" 180 compTypeName := fnameToGoTypeName(baseFileName) 181 182 // keep track of which files to scan for missing structs 183 missingFmap[fn] = goFileName 184 185 mergeFiles = append(mergeFiles, goFileName) 186 187 pg := &ParserGo{} 188 189 pg.PackageName = pkgName 190 // pg.ComponentType = compTypeName 191 pg.StructType = compTypeName 192 // pg.DataType = pg.ComponentType + "Data" 193 pg.OutDir = p.pkgPath 194 pg.OutFile = goFileName 195 pg.TinyGo = p.opts.TinyGo 196 197 // add to our list of names to check after 198 namesToCheck = append(namesToCheck, pg.StructType) 199 // namesToCheck = append(namesToCheck, pg.ComponentType+".NewData") 200 // namesToCheck = append(namesToCheck, pg.DataType) 201 namesToCheck = append(namesToCheck, "vuguSetup") 202 203 // read in source 204 b, err := os.ReadFile(filepath.Join(p.pkgPath, fn)) 205 if err != nil { 206 return err 207 } 208 209 // parse it 210 err = pg.Parse(bytes.NewReader(b), fn) 211 if err != nil { 212 return fmt.Errorf("error parsing %q: %v", fn, err) 213 } 214 215 } 216 217 // after the code generation is done, check the package for the various names in question to see 218 // what we need to generate 219 namesFound, err := goPkgCheckNames(p.pkgPath, namesToCheck) 220 if err != nil { 221 return err 222 } 223 224 // if main package, generate main_wasm.go with default stuff if no main func in the package and no main_wasm.go 225 if (!p.opts.SkipMainGo) && pkgName == "main" { 226 227 mainGoPath := filepath.Join(p.pkgPath, "main_wasm.go") 228 // log.Printf("namesFound: %#v", namesFound) 229 // log.Printf("maingo found: %v", fileExists(mainGoPath)) 230 // if _, ok := namesFound["main"]; (!ok) && !fileExists(mainGoPath) { 231 232 // NOTE: For now we're disabling the "main" symbol name check, because in single-dir cases 233 // it's picking up the main_wasm.go in server.go (even though it's excluded via build tag). This 234 // needs some more thought but for now this will work for the common cases. 235 if !fileExists(mainGoPath) { 236 237 // log.Printf("WRITING TO main_wasm.go STUFF") 238 var buf bytes.Buffer 239 t, err := template.New("_main_").Parse(`// +build wasm 240 {{$opts := .Parser.Opts}} 241 package main 242 243 import ( 244 "fmt" 245 {{if not $opts.TinyGo}} 246 "flag" 247 {{end}} 248 249 "github.com/vugu/vugu" 250 "github.com/vugu/vugu/domrender" 251 ) 252 253 func main() { 254 255 {{if $opts.TinyGo}} 256 var mountPoint *string 257 { 258 mp := "#vugu_mount_point" 259 mountPoint = &mp 260 } 261 {{else}} 262 mountPoint := flag.String("mount-point", "#vugu_mount_point", "The query selector for the mount point for the root component, if it is not a full HTML component") 263 flag.Parse() 264 {{end}} 265 266 fmt.Printf("Entering main(), -mount-point=%q\n", *mountPoint) 267 {{if not $opts.TinyGo}}defer fmt.Printf("Exiting main()\n") 268 {{end}} 269 270 renderer, err := domrender.New(*mountPoint) 271 if err != nil { 272 panic(err) 273 } 274 {{if not $opts.TinyGo}}defer renderer.Release() 275 {{end}} 276 277 buildEnv, err := vugu.NewBuildEnv(renderer.EventEnv()) 278 if err != nil { 279 panic(err) 280 } 281 282 {{if (index .NamesFound "vuguSetup")}} 283 rootBuilder := vuguSetup(buildEnv, renderer.EventEnv()) 284 {{else}} 285 rootBuilder := &Root{} 286 {{end}} 287 288 289 for ok := true; ok; ok = renderer.EventWait() { 290 291 buildResults := buildEnv.RunBuild(rootBuilder) 292 293 err = renderer.Render(buildResults) 294 if err != nil { 295 panic(err) 296 } 297 } 298 299 } 300 `) 301 if err != nil { 302 return err 303 } 304 err = t.Execute(&buf, map[string]interface{}{ 305 "Parser": p, 306 "NamesFound": namesFound, 307 }) 308 if err != nil { 309 return err 310 } 311 312 bufstr := buf.String() 313 bufstr, err = gofmt(bufstr) 314 if err != nil { 315 log.Printf("WARNING: gofmt on main_wasm.go failed: %v", err) 316 } 317 318 err = os.WriteFile(mainGoPath, []byte(bufstr), 0644) 319 if err != nil { 320 return err 321 } 322 323 } 324 325 } 326 327 // write go.mod if it doesn't exist and not disabled - actually this really only makes sense for main, 328 // otherwise we really don't know what the right module name is 329 goModPath := filepath.Join(p.pkgPath, "go.mod") 330 if pkgName == "main" && !p.opts.SkipGoMod && !fileExists(goModPath) { 331 err := os.WriteFile(goModPath, []byte(`module `+pkgName+"\n"), 0644) 332 if err != nil { 333 return err 334 } 335 } 336 337 // remove the merged file so it doesn't mess with detection 338 if p.opts.MergeSingle { 339 os.Remove(filepath.Join(p.pkgPath, mergeSingleName)) 340 } 341 342 // for _, fn := range vuguFileNames { 343 344 // goFileName := strings.TrimSuffix(fn, ".vugu") + goFnameAppend + ".go" 345 // goFilePath := filepath.Join(p.pkgPath, goFileName) 346 347 // err := func() error { 348 // // get ready to append to file 349 // f, err := os.OpenFile(goFilePath, os.O_WRONLY|os.O_APPEND, 0644) 350 // if err != nil { 351 // return err 352 // } 353 // defer f.Close() 354 355 // // TODO: would be nice to clean this up and get a better grip on how we do this filename -> struct name mapping, but this works for now 356 // compTypeName := fnameToGoTypeName(strings.TrimSuffix(goFileName, goFnameAppend+".go")) 357 358 // // create CompName struct if it doesn't exist in the package 359 // if _, ok := namesFound[compTypeName]; !ok { 360 // fmt.Fprintf(f, "\ntype %s struct {}\n", compTypeName) 361 // } 362 363 // // // create CompNameData struct if it doesn't exist in the package 364 // // if _, ok := namesFound[compTypeName+"Data"]; !ok { 365 // // fmt.Fprintf(f, "\ntype %s struct {}\n", compTypeName+"Data") 366 // // } 367 368 // // create CompName.NewData with defaults if it doesn't exist in the package 369 // // if _, ok := namesFound[compTypeName+".NewData"]; !ok { 370 // // fmt.Fprintf(f, "\nfunc (ct *%s) NewData(props vugu.Props) (interface{}, error) { return &%s{}, nil }\n", 371 // // compTypeName, compTypeName+"Data") 372 // // } 373 374 // // // register component unless disabled - nope, no more component registry 375 // // if !p.opts.SkipRegisterComponentTypes && !fileHasInitFunc(goFilePath) { 376 // // fmt.Fprintf(f, "\nfunc init() { vugu.RegisterComponentType(%q, &%s{}) }\n", strings.TrimSuffix(goFileName, ".go"), compTypeName) 377 // // } 378 379 // return nil 380 // }() 381 // if err != nil { 382 // return err 383 // } 384 385 // } 386 387 // generate anything missing and process vugugen comments 388 mf := newMissingFixer(p.pkgPath, pkgName, missingFmap) 389 err = mf.run() 390 if err != nil { 391 return fmt.Errorf("missing fixer error: %w", err) 392 } 393 394 // if requested, do merge 395 if p.opts.MergeSingle { 396 397 // if a missing fix file was produced include it in the list to be merged 398 _, err := os.Stat(filepath.Join(p.pkgPath, "0_missing_gen.go")) 399 if err == nil { 400 mergeFiles = append(mergeFiles, "0_missing_gen.go") 401 } 402 403 err = mergeGoFiles(p.pkgPath, mergeSingleName, mergeFiles...) 404 if err != nil { 405 return err 406 } 407 // remove files if merge worked 408 for _, mf := range mergeFiles { 409 err := os.Remove(filepath.Join(p.pkgPath, mf)) 410 if err != nil { 411 return err 412 } 413 } 414 415 } 416 417 err = restoreFileHashTimes(p.pkgPath, hashTimes) 418 if err != nil { 419 return err 420 } 421 422 return nil 423 424 } 425 426 //nolint:golint,unused 427 func fileHasInitFunc(p string) bool { 428 b, err := os.ReadFile(p) 429 if err != nil { 430 return false 431 } 432 // hacky but workable for now 433 return regexp.MustCompile(`^func init\(`).Match(b) 434 } 435 436 func fileExists(p string) bool { 437 _, err := os.Stat(p) 438 return !os.IsNotExist(err) 439 } 440 441 func fnameToGoTypeName(s string) string { 442 s = strings.Split(s, ".")[0] // remove file extension if present 443 parts := strings.Split(s, "-") 444 for i := range parts { 445 p := parts[i] 446 if len(p) > 0 { 447 p = strings.ToUpper(p[:1]) + p[1:] 448 } 449 parts[i] = p 450 } 451 return strings.Join(parts, "") 452 } 453 454 func goGuessPkgName(pkgPath string) (ret string) { 455 456 // defer func() { log.Printf("goGuessPkgName returning %q", ret) }() 457 458 // see if the package already has a name and use it if so 459 fset := token.NewFileSet() 460 pkgs, err := parser.ParseDir(fset, pkgPath, nil, parser.PackageClauseOnly) // just get the package name 461 if err != nil { 462 goto checkMore 463 } 464 if len(pkgs) != 1 { 465 goto checkMore 466 } 467 { 468 var pkg *ast.Package 469 for _, pkg1 := range pkgs { 470 pkg = pkg1 471 } 472 return pkg.Name 473 } 474 475 checkMore: 476 477 // check for a root.vugu file, in which case we assume "main" 478 _, err = os.Stat(filepath.Join(pkgPath, "root.vugu")) 479 if err == nil { 480 return "main" 481 } 482 483 // otherwise we use the name of the folder... 484 dirBase := filepath.Base(pkgPath) 485 if regexp.MustCompile(`^[a-z0-9]+$`).MatchString(dirBase) { 486 return dirBase 487 } 488 489 // ...unless it makes no sense in which case we use "main" 490 491 return "main" 492 493 } 494 495 // goPkgCheckNames parses a package dir and looks for names, returning a map of what was 496 // found. Names like "A.B" mean a method of name "B" with receiver of type "*A" 497 func goPkgCheckNames(pkgPath string, names []string) (map[string]interface{}, error) { 498 499 ret := make(map[string]interface{}) 500 501 fset := token.NewFileSet() 502 pkgs, err := parser.ParseDir(fset, pkgPath, nil, 0) 503 if err != nil { 504 return ret, err 505 } 506 507 if len(pkgs) != 1 { 508 return ret, fmt.Errorf("unexpected package count after parsing, expected 1 and got this: %#v", pkgs) 509 } 510 511 var pkg *ast.Package 512 for _, pkg1 := range pkgs { 513 pkg = pkg1 514 } 515 516 for _, file := range pkg.Files { 517 518 if file.Scope != nil { 519 for _, n := range names { 520 if v, ok := file.Scope.Objects[n]; ok { 521 ret[n] = v 522 } 523 } 524 } 525 526 // log.Printf("file: %#v", file) 527 // log.Printf("file.Scope.Objects: %#v", file.Scope.Objects) 528 // log.Printf("next: %#v", file.Scope.Objects["Example1"]) 529 // e1 := file.Scope.Objects["Example1"] 530 // if e1.Kind == ast.Typ { 531 // e1.Decl 532 // } 533 for _, d := range file.Decls { 534 if fd, ok := d.(*ast.FuncDecl); ok { 535 536 var drecv, dmethod string 537 if fd.Recv != nil { 538 for _, f := range fd.Recv.List { 539 // log.Printf("f.Type: %#v", f.Type) 540 if tstar, ok := f.Type.(*ast.StarExpr); ok { 541 // log.Printf("tstar.X: %#v", tstar.X) 542 if tstarXi, ok := tstar.X.(*ast.Ident); ok && tstarXi != nil { 543 // log.Printf("namenamenamename: %#v", tstarXi.Name) 544 drecv = tstarXi.Name 545 } 546 } 547 // log.Printf("f.Names: %#v", f.Names) 548 // for _, fn := range f.Names { 549 // if fn != nil { 550 // log.Printf("NAMENAME: %#v", fn.Name) 551 // if fni, ok := fn.Name.(*ast.Ident); ok && fni != nil { 552 // } 553 // } 554 // } 555 556 } 557 } else { 558 continue // don't care methods with no receiver - found them already above as single (no period) names 559 } 560 561 // log.Printf("fd.Name: %#v", fd.Name) 562 if fd.Name != nil { 563 dmethod = fd.Name.Name 564 } 565 566 for _, n := range names { 567 recv, method := nameParts(n) 568 if drecv == recv && dmethod == method { 569 ret[n] = d 570 } 571 } 572 } 573 } 574 } 575 // log.Printf("Objects: %#v", pkg.Scope.Objects) 576 577 return ret, nil 578 } 579 580 func nameParts(n string) (recv, method string) { 581 582 ret := strings.SplitN(n, ".", 2) 583 if len(ret) < 2 { 584 method = n 585 return 586 } 587 recv = ret[0] 588 method = ret[1] 589 return 590 } 591 592 // fileHashTimes will scan a directory and return a map of hashes and corresponding mod times 593 func fileHashTimes(dir string) (map[uint64]time.Time, error) { 594 595 ret := make(map[uint64]time.Time) 596 597 f, err := os.Open(dir) 598 if err != nil { 599 return nil, err 600 } 601 defer f.Close() 602 603 fis, err := f.Readdir(-1) 604 if err != nil { 605 return nil, err 606 } 607 for _, fi := range fis { 608 if fi.IsDir() { 609 continue 610 } 611 h := xxhash.New() 612 fmt.Fprint(h, fi.Name()) // hash the name too so we don't confuse different files with the same contents 613 b, err := os.ReadFile(filepath.Join(dir, fi.Name())) 614 if err != nil { 615 return nil, err 616 } 617 _, err = h.Write(b) 618 if err != nil { 619 return nil, err 620 } 621 ret[h.Sum64()] = fi.ModTime() 622 } 623 624 return ret, nil 625 } 626 627 // restoreFileHashTimes takes the map returned by fileHashTimes and for any files where the hash 628 // matches we restore the mod time - this way we can clobber files during code generation but 629 // then if the resulting output is byte for byte the same we can just change the mod time back and 630 // things that look at timestamps will see the file as unchanged; somewhat hacky, but simple and 631 // workable for now - it's important for the developer experince we don't do unnecessary builds 632 // in cases where things don't change 633 func restoreFileHashTimes(dir string, hashTimes map[uint64]time.Time) error { 634 635 f, err := os.Open(dir) 636 if err != nil { 637 return err 638 } 639 defer f.Close() 640 641 fis, err := f.Readdir(-1) 642 if err != nil { 643 return err 644 } 645 for _, fi := range fis { 646 if fi.IsDir() { 647 continue 648 } 649 fiPath := filepath.Join(dir, fi.Name()) 650 h := xxhash.New() 651 fmt.Fprint(h, fi.Name()) // hash the name too so we don't confuse different files with the same contents 652 b, err := os.ReadFile(fiPath) 653 if err != nil { 654 return err 655 } 656 _, err = h.Write(b) 657 if err != nil { 658 return err 659 } 660 if t, ok := hashTimes[h.Sum64()]; ok { 661 err := os.Chtimes(fiPath, time.Now(), t) 662 if err != nil { 663 log.Printf("Error in os.Chtimes(%q, now, %q): %v", fiPath, t, err) 664 } 665 } 666 } 667 668 return nil 669 }