github.com/vugu/vugu@v0.3.5/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 "io/ioutil" 11 "log" 12 "os" 13 "path/filepath" 14 "regexp" 15 "strings" 16 "text/template" 17 "time" 18 19 "github.com/vugu/xxhash" 20 ) 21 22 // ParserGoPkg knows how to perform source file generation in relation to a package folder. 23 // Whereas ParserGo handles converting a single template, ParserGoPkg is a higher level interface 24 // and provides the functionality of the vugugen command line tool. It will scan a package 25 // folder for .vugu files and convert them to .go, with the appropriate defaults and logic. 26 type ParserGoPkg struct { 27 pkgPath string 28 opts ParserGoPkgOpts 29 } 30 31 // ParserGoPkgOpts is the options for ParserGoPkg. 32 type ParserGoPkgOpts struct { 33 SkipGoMod bool // do not try and create go.mod if it doesn't exist 34 SkipMainGo bool // do not try and create main_wasm.go if it doesn't exist in a main package 35 TinyGo bool // emit code intended for TinyGo compilation 36 GoFileNameAppend *string // suffix to append to file names, after base name plus .go, if nil then "_vgen" is used 37 MergeSingle bool // merge all output files into a single one 38 MergeSingleName string // name of merged output file, only used if MergeSingle is true, defaults to "0_components_vgen.go" 39 } 40 41 // TODO: CallVuguSetup bool // always call vuguSetup instead of trying to auto-detect it's existence 42 43 var errNoVuguFile = errors.New("no .vugu file(s) found") 44 45 // RunRecursive will create a new ParserGoPkg and call Run on it recursively for each 46 // directory under pkgPath. The opts will be modified for subfolders to disable go.mod and main.go 47 // logic. If pkgPath does not contain a .vugu file this function will return an error. 48 func RunRecursive(pkgPath string, opts *ParserGoPkgOpts) error { 49 50 if opts == nil { 51 opts = &ParserGoPkgOpts{} 52 } 53 54 dirf, err := os.Open(pkgPath) 55 if err != nil { 56 return err 57 } 58 59 fis, err := dirf.Readdir(-1) 60 if err != nil { 61 return err 62 } 63 hasVugu := false 64 var subDirList []string 65 for _, fi := range fis { 66 if fi.IsDir() && !strings.HasPrefix(fi.Name(), ".") { 67 subDirList = append(subDirList, fi.Name()) 68 continue 69 } 70 if filepath.Ext(fi.Name()) == ".vugu" { 71 hasVugu = true 72 } 73 } 74 if !hasVugu { 75 return errNoVuguFile 76 } 77 78 p := NewParserGoPkg(pkgPath, opts) 79 err = p.Run() 80 if err != nil { 81 return err 82 } 83 84 for _, subDir := range subDirList { 85 subPath := filepath.Join(pkgPath, subDir) 86 opts2 := *opts 87 // sub folders should never get these behaviors 88 opts2.SkipGoMod = true 89 opts2.SkipMainGo = true 90 err := RunRecursive(subPath, &opts2) 91 if err == errNoVuguFile { 92 continue 93 } 94 if err != nil { 95 return err 96 } 97 } 98 99 return nil 100 } 101 102 // Run will create a new ParserGoPkg and call Run on it. 103 func Run(pkgPath string, opts *ParserGoPkgOpts) error { 104 p := NewParserGoPkg(pkgPath, opts) 105 return p.Run() 106 } 107 108 // NewParserGoPkg returns a new ParserGoPkg with the specified options or default if nil. The pkgPath is required and must be an absolute path. 109 func NewParserGoPkg(pkgPath string, opts *ParserGoPkgOpts) *ParserGoPkg { 110 ret := &ParserGoPkg{ 111 pkgPath: pkgPath, 112 } 113 if opts != nil { 114 ret.opts = *opts 115 } 116 return ret 117 } 118 119 // Opts returns the options. 120 func (p *ParserGoPkg) Opts() ParserGoPkgOpts { 121 return p.opts 122 } 123 124 // Run does the work and generates the appropriate .go files from .vugu files. 125 // It will also create a go.mod file if not present and not SkipGoMod. Same for main.go and SkipMainGo (will also skip 126 // if package already has file with package name something other than main). 127 // Per-file code generation is performed by ParserGo. 128 func (p *ParserGoPkg) Run() error { 129 130 // record the times of existing files, so we can restore after if the same 131 hashTimes, err := fileHashTimes(p.pkgPath) 132 if err != nil { 133 return err 134 } 135 136 pkgF, err := os.Open(p.pkgPath) 137 if err != nil { 138 return err 139 } 140 defer pkgF.Close() 141 142 allFileNames, err := pkgF.Readdirnames(-1) 143 if err != nil { 144 return err 145 } 146 147 var vuguFileNames []string 148 for _, fn := range allFileNames { 149 if filepath.Ext(fn) == ".vugu" { 150 vuguFileNames = append(vuguFileNames, fn) 151 } 152 } 153 154 if len(vuguFileNames) == 0 { 155 return fmt.Errorf("no .vugu files found, please create one and try again") 156 } 157 158 pkgName := goGuessPkgName(p.pkgPath) 159 160 namesToCheck := []string{"main"} 161 162 goFnameAppend := "_vgen" 163 if p.opts.GoFileNameAppend != nil { 164 goFnameAppend = *p.opts.GoFileNameAppend 165 } 166 167 var mergeFiles []string 168 169 mergeSingleName := "0_components_vgen.go" 170 if p.opts.MergeSingleName != "" { 171 mergeSingleName = p.opts.MergeSingleName 172 } 173 174 missingFmap := make(map[string]string, len(vuguFileNames)) 175 176 // run ParserGo on each file to generate the .go files 177 for _, fn := range vuguFileNames { 178 179 baseFileName := strings.TrimSuffix(fn, ".vugu") 180 goFileName := baseFileName + goFnameAppend + ".go" 181 compTypeName := fnameToGoTypeName(baseFileName) 182 183 // keep track of which files to scan for missing structs 184 missingFmap[fn] = goFileName 185 186 mergeFiles = append(mergeFiles, goFileName) 187 188 pg := &ParserGo{} 189 190 pg.PackageName = pkgName 191 // pg.ComponentType = compTypeName 192 pg.StructType = compTypeName 193 // pg.DataType = pg.ComponentType + "Data" 194 pg.OutDir = p.pkgPath 195 pg.OutFile = goFileName 196 pg.TinyGo = p.opts.TinyGo 197 198 // add to our list of names to check after 199 namesToCheck = append(namesToCheck, pg.StructType) 200 // namesToCheck = append(namesToCheck, pg.ComponentType+".NewData") 201 // namesToCheck = append(namesToCheck, pg.DataType) 202 namesToCheck = append(namesToCheck, "vuguSetup") 203 204 // read in source 205 b, err := ioutil.ReadFile(filepath.Join(p.pkgPath, fn)) 206 if err != nil { 207 return err 208 } 209 210 // parse it 211 err = pg.Parse(bytes.NewReader(b), fn) 212 if err != nil { 213 return fmt.Errorf("error parsing %q: %v", fn, err) 214 } 215 216 } 217 218 // after the code generation is done, check the package for the various names in question to see 219 // what we need to generate 220 namesFound, err := goPkgCheckNames(p.pkgPath, namesToCheck) 221 if err != nil { 222 return err 223 } 224 225 // if main package, generate main_wasm.go with default stuff if no main func in the package and no main_wasm.go 226 if (!p.opts.SkipMainGo) && pkgName == "main" { 227 228 mainGoPath := filepath.Join(p.pkgPath, "main_wasm.go") 229 // log.Printf("namesFound: %#v", namesFound) 230 // log.Printf("maingo found: %v", fileExists(mainGoPath)) 231 // if _, ok := namesFound["main"]; (!ok) && !fileExists(mainGoPath) { 232 233 // NOTE: For now we're disabling the "main" symbol name check, because in single-dir cases 234 // it's picking up the main_wasm.go in server.go (even though it's excluded via build tag). This 235 // needs some more thought but for now this will work for the common cases. 236 if !fileExists(mainGoPath) { 237 238 // log.Printf("WRITING TO main_wasm.go STUFF") 239 var buf bytes.Buffer 240 t, err := template.New("_main_").Parse(`// +build wasm 241 {{$opts := .Parser.Opts}} 242 package main 243 244 import ( 245 "fmt" 246 {{if not $opts.TinyGo}} 247 "flag" 248 {{end}} 249 250 "github.com/vugu/vugu" 251 "github.com/vugu/vugu/domrender" 252 ) 253 254 func main() { 255 256 {{if $opts.TinyGo}} 257 var mountPoint *string 258 { 259 mp := "#vugu_mount_point" 260 mountPoint = &mp 261 } 262 {{else}} 263 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") 264 flag.Parse() 265 {{end}} 266 267 fmt.Printf("Entering main(), -mount-point=%q\n", *mountPoint) 268 {{if not $opts.TinyGo}}defer fmt.Printf("Exiting main()\n") 269 {{end}} 270 271 renderer, err := domrender.New(*mountPoint) 272 if err != nil { 273 panic(err) 274 } 275 {{if not $opts.TinyGo}}defer renderer.Release() 276 {{end}} 277 278 buildEnv, err := vugu.NewBuildEnv(renderer.EventEnv()) 279 if err != nil { 280 panic(err) 281 } 282 283 {{if (index .NamesFound "vuguSetup")}} 284 rootBuilder := vuguSetup(buildEnv, renderer.EventEnv()) 285 {{else}} 286 rootBuilder := &Root{} 287 {{end}} 288 289 290 for ok := true; ok; ok = renderer.EventWait() { 291 292 buildResults := buildEnv.RunBuild(rootBuilder) 293 294 err = renderer.Render(buildResults) 295 if err != nil { 296 panic(err) 297 } 298 } 299 300 } 301 `) 302 if err != nil { 303 return err 304 } 305 err = t.Execute(&buf, map[string]interface{}{ 306 "Parser": p, 307 "NamesFound": namesFound, 308 }) 309 if err != nil { 310 return err 311 } 312 313 bufstr := buf.String() 314 bufstr, err = gofmt(bufstr) 315 if err != nil { 316 log.Printf("WARNING: gofmt on main_wasm.go failed: %v", err) 317 } 318 319 err = ioutil.WriteFile(mainGoPath, []byte(bufstr), 0644) 320 if err != nil { 321 return err 322 } 323 324 } 325 326 } 327 328 // write go.mod if it doesn't exist and not disabled - actually this really only makes sense for main, 329 // otherwise we really don't know what the right module name is 330 goModPath := filepath.Join(p.pkgPath, "go.mod") 331 if pkgName == "main" && !p.opts.SkipGoMod && !fileExists(goModPath) { 332 err := ioutil.WriteFile(goModPath, []byte(`module `+pkgName+"\n"), 0644) 333 if err != nil { 334 return err 335 } 336 } 337 338 // remove the merged file so it doesn't mess with detection 339 if p.opts.MergeSingle { 340 os.Remove(filepath.Join(p.pkgPath, mergeSingleName)) 341 } 342 343 // for _, fn := range vuguFileNames { 344 345 // goFileName := strings.TrimSuffix(fn, ".vugu") + goFnameAppend + ".go" 346 // goFilePath := filepath.Join(p.pkgPath, goFileName) 347 348 // err := func() error { 349 // // get ready to append to file 350 // f, err := os.OpenFile(goFilePath, os.O_WRONLY|os.O_APPEND, 0644) 351 // if err != nil { 352 // return err 353 // } 354 // defer f.Close() 355 356 // // 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 357 // compTypeName := fnameToGoTypeName(strings.TrimSuffix(goFileName, goFnameAppend+".go")) 358 359 // // create CompName struct if it doesn't exist in the package 360 // if _, ok := namesFound[compTypeName]; !ok { 361 // fmt.Fprintf(f, "\ntype %s struct {}\n", compTypeName) 362 // } 363 364 // // // create CompNameData struct if it doesn't exist in the package 365 // // if _, ok := namesFound[compTypeName+"Data"]; !ok { 366 // // fmt.Fprintf(f, "\ntype %s struct {}\n", compTypeName+"Data") 367 // // } 368 369 // // create CompName.NewData with defaults if it doesn't exist in the package 370 // // if _, ok := namesFound[compTypeName+".NewData"]; !ok { 371 // // fmt.Fprintf(f, "\nfunc (ct *%s) NewData(props vugu.Props) (interface{}, error) { return &%s{}, nil }\n", 372 // // compTypeName, compTypeName+"Data") 373 // // } 374 375 // // // register component unless disabled - nope, no more component registry 376 // // if !p.opts.SkipRegisterComponentTypes && !fileHasInitFunc(goFilePath) { 377 // // fmt.Fprintf(f, "\nfunc init() { vugu.RegisterComponentType(%q, &%s{}) }\n", strings.TrimSuffix(goFileName, ".go"), compTypeName) 378 // // } 379 380 // return nil 381 // }() 382 // if err != nil { 383 // return err 384 // } 385 386 // } 387 388 // generate anything missing and process vugugen comments 389 mf := newMissingFixer(p.pkgPath, pkgName, missingFmap) 390 err = mf.run() 391 if err != nil { 392 return fmt.Errorf("missing fixer error: %w", err) 393 } 394 395 // if requested, do merge 396 if p.opts.MergeSingle { 397 398 // if a missing fix file was produced include it in the list to be merged 399 _, err := os.Stat(filepath.Join(p.pkgPath, "0_missing_vgen.go")) 400 if err == nil { 401 mergeFiles = append(mergeFiles, "0_missing_vgen.go") 402 } 403 404 err = mergeGoFiles(p.pkgPath, mergeSingleName, mergeFiles...) 405 if err != nil { 406 return err 407 } 408 // remove files if merge worked 409 for _, mf := range mergeFiles { 410 err := os.Remove(filepath.Join(p.pkgPath, mf)) 411 if err != nil { 412 return err 413 } 414 } 415 416 } 417 418 err = restoreFileHashTimes(p.pkgPath, hashTimes) 419 if err != nil { 420 return err 421 } 422 423 return nil 424 425 } 426 427 func fileHasInitFunc(p string) bool { 428 b, err := ioutil.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 := ioutil.ReadFile(filepath.Join(dir, fi.Name())) 614 if err != nil { 615 return nil, err 616 } 617 h.Write(b) 618 ret[h.Sum64()] = fi.ModTime() 619 } 620 621 return ret, nil 622 } 623 624 // restoreFileHashTimes takes the map returned by fileHashTimes and for any files where the hash 625 // matches we restore the mod time - this way we can clobber files during code generation but 626 // then if the resulting output is byte for byte the same we can just change the mod time back and 627 // things that look at timestamps will see the file as unchanged; somewhat hacky, but simple and 628 // workable for now - it's important for the developer experince we don't do unnecessary builds 629 // in cases where things don't change 630 func restoreFileHashTimes(dir string, hashTimes map[uint64]time.Time) error { 631 632 f, err := os.Open(dir) 633 if err != nil { 634 return err 635 } 636 defer f.Close() 637 638 fis, err := f.Readdir(-1) 639 if err != nil { 640 return err 641 } 642 for _, fi := range fis { 643 if fi.IsDir() { 644 continue 645 } 646 fiPath := filepath.Join(dir, fi.Name()) 647 h := xxhash.New() 648 fmt.Fprint(h, fi.Name()) // hash the name too so we don't confuse different files with the same contents 649 b, err := ioutil.ReadFile(fiPath) 650 if err != nil { 651 return err 652 } 653 h.Write(b) 654 if t, ok := hashTimes[h.Sum64()]; ok { 655 err := os.Chtimes(fiPath, time.Now(), t) 656 if err != nil { 657 log.Printf("Error in os.Chtimes(%q, now, %q): %v", fiPath, t, err) 658 } 659 } 660 } 661 662 return nil 663 }