mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			356 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			356 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			Go
		
	
	
	
| package codegen
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	gerrors "errors"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"io/fs"
 | |
| 	"path/filepath"
 | |
| 	"strings"
 | |
| 	"text/template"
 | |
| 
 | |
| 	"cuelang.org/go/cue"
 | |
| 	"cuelang.org/go/cue/ast"
 | |
| 	"cuelang.org/go/cue/build"
 | |
| 	"cuelang.org/go/cue/cuecontext"
 | |
| 	"cuelang.org/go/cue/errors"
 | |
| 	"cuelang.org/go/cue/load"
 | |
| 	"cuelang.org/go/cue/parser"
 | |
| 	"github.com/grafana/cuetsy"
 | |
| 	"github.com/grafana/grafana"
 | |
| 	"github.com/grafana/thema"
 | |
| 	tload "github.com/grafana/thema/load"
 | |
| )
 | |
| 
 | |
| // The only import statement we currently allow in any models.cue file
 | |
| const schemasPath = "github.com/grafana/grafana/packages/grafana-schema/src/schema"
 | |
| 
 | |
| // CUE import paths, mapped to corresponding TS import paths. An empty value
 | |
| // indicates the import path should be dropped in the conversion to TS. Imports
 | |
| // not present in the list are not not allowed, and code generation will fail.
 | |
| var importMap = map[string]string{
 | |
| 	"github.com/grafana/thema": "",
 | |
| 	schemasPath:                "@grafana/schema",
 | |
| }
 | |
| 
 | |
| // Hard-coded list of paths to skip. Remove a particular file as we're ready
 | |
| // to rely on the TypeScript auto-generated by cuetsy for that particular file.
 | |
| var skipPaths = []string{
 | |
| 	"public/app/plugins/panel/canvas/models.cue",
 | |
| 	"public/app/plugins/panel/heatmap/models.cue",
 | |
| 	"public/app/plugins/panel/heatmap-old/models.cue",
 | |
| 	"public/app/plugins/panel/candlestick/models.cue",
 | |
| 	"public/app/plugins/panel/state-timeline/models.cue",
 | |
| 	"public/app/plugins/panel/status-history/models.cue",
 | |
| 	"public/app/plugins/panel/table/models.cue",
 | |
| 	"public/app/plugins/panel/timeseries/models.cue",
 | |
| }
 | |
| 
 | |
| const prefix = "/"
 | |
| 
 | |
| // CuetsifyPlugins runs cuetsy against plugins' models.cue files.
 | |
| func CuetsifyPlugins(ctx *cue.Context, root string) (WriteDiffer, error) {
 | |
| 	lib := thema.NewLibrary(ctx)
 | |
| 	// TODO this whole func has a lot of old, crufty behavior from the scuemata era; needs TLC
 | |
| 	overlay := make(map[string]load.Source)
 | |
| 	err := toOverlay(prefix, grafana.CueSchemaFS, overlay)
 | |
| 	// err := tload.ToOverlay(prefix, grafana.CueSchemaFS, overlay)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	exclude := func(path string) bool {
 | |
| 		for _, p := range skipPaths {
 | |
| 			if path == p {
 | |
| 				return true
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		return filepath.Dir(path) == "cue.mod"
 | |
| 	}
 | |
| 
 | |
| 	// Prep the cue load config
 | |
| 	clcfg := &load.Config{
 | |
| 		Overlay: overlay,
 | |
| 		// FIXME these module paths won't work for things not under our cue.mod - AKA third-party plugins
 | |
| 		ModuleRoot: prefix,
 | |
| 		Module:     "github.com/grafana/grafana",
 | |
| 	}
 | |
| 
 | |
| 	outfiles := NewWriteDiffer()
 | |
| 
 | |
| 	cuetsify := func(in fs.FS) error {
 | |
| 		seen := make(map[string]bool)
 | |
| 		return fs.WalkDir(in, ".", func(path string, d fs.DirEntry, err error) error {
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 			dir := filepath.Dir(path)
 | |
| 
 | |
| 			if d.IsDir() || filepath.Ext(d.Name()) != ".cue" || seen[dir] || exclude(path) {
 | |
| 				return nil
 | |
| 			}
 | |
| 			seen[dir] = true
 | |
| 			clcfg.Dir = filepath.Join(root, dir)
 | |
| 
 | |
| 			var b []byte
 | |
| 			f := &tsFile{}
 | |
| 
 | |
| 			switch {
 | |
| 			default:
 | |
| 				insts := load.Instances(nil, clcfg)
 | |
| 				if len(insts) > 1 {
 | |
| 					return fmt.Errorf("%s: resulted in more than one instance", path)
 | |
| 				}
 | |
| 				v := ctx.BuildInstance(insts[0])
 | |
| 
 | |
| 				b, err = cuetsy.Generate(v, cuetsy.Config{})
 | |
| 				if err != nil {
 | |
| 					return err
 | |
| 				}
 | |
| 
 | |
| 			case strings.Contains(path, "public/app/plugins"): // panel plugin models.cue files
 | |
| 				// The simple - and preferable - thing would be to have plugins use the same
 | |
| 				// package name for their models.cue as their containing dir. That's not
 | |
| 				// possible, though, because we allow dashes in plugin names, but CUE does not
 | |
| 				// allow them in package names. Yuck.
 | |
| 				inst, err := loadInstancesWithThema(in, dir, "grafanaschema")
 | |
| 				if err != nil {
 | |
| 					return fmt.Errorf("could not load CUE instance for %s: %w", dir, err)
 | |
| 				}
 | |
| 
 | |
| 				// Also parse file directly to extract imports.
 | |
| 				// NOTE this will need refactoring to support working with more than one file at a time
 | |
| 				of, _ := in.Open(path)
 | |
| 				pf, _ := parser.ParseFile(filepath.Base(path), of, parser.ParseComments)
 | |
| 
 | |
| 				iseen := make(map[string]bool)
 | |
| 				for _, im := range pf.Imports {
 | |
| 					ip := strings.Trim(im.Path.Value, "\"")
 | |
| 					mappath, has := importMap[ip]
 | |
| 					if !has {
 | |
| 						// TODO make a specific error type for this
 | |
| 						var all []string
 | |
| 						for im := range importMap {
 | |
| 							all = append(all, fmt.Sprintf("\t%s", im))
 | |
| 						}
 | |
| 						return errors.Newf(im.Pos(), "%s: import %q not allowed, panel plugins may only import from:\n%s\n", path, ip, strings.Join(all, "\n"))
 | |
| 					}
 | |
| 					// TODO this approach will silently swallow the unfixable
 | |
| 					// error case where multiple files in the same dir import
 | |
| 					// the same package to a different ident
 | |
| 					if mappath != "" && !iseen[ip] {
 | |
| 						iseen[ip] = true
 | |
| 						f.Imports = append(f.Imports, convertImport(im))
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				v := ctx.BuildInstance(inst)
 | |
| 
 | |
| 				lin, err := thema.BindLineage(v.LookupPath(cue.ParsePath("Panel")), lib)
 | |
| 				if err != nil {
 | |
| 					return fmt.Errorf("%s: failed to bind lineage: %w", path, err)
 | |
| 				}
 | |
| 				f.V = thema.LatestVersion(lin)
 | |
| 				f.WriteModelVersion = true
 | |
| 
 | |
| 				b, err = cuetsy.Generate(thema.SchemaP(lin, f.V).UnwrapCUE(), cuetsy.Config{})
 | |
| 				if err != nil {
 | |
| 					return err
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			f.Body = string(b)
 | |
| 
 | |
| 			var buf bytes.Buffer
 | |
| 			err = tsTemplate.Execute(&buf, f)
 | |
| 			outfiles[filepath.Join(root, strings.Replace(path, ".cue", ".gen.ts", -1))] = buf.Bytes()
 | |
| 			return err
 | |
| 		})
 | |
| 	}
 | |
| 
 | |
| 	err = cuetsify(grafana.CueSchemaFS)
 | |
| 	if err != nil {
 | |
| 		return nil, gerrors.New(errors.Details(err, nil))
 | |
| 	}
 | |
| 
 | |
| 	return outfiles, nil
 | |
| }
 | |
| 
 | |
| func convertImport(im *ast.ImportSpec) *tsImport {
 | |
| 	tsim := &tsImport{
 | |
| 		Pkg: importMap[schemasPath],
 | |
| 	}
 | |
| 	if im.Name != nil && im.Name.String() != "" {
 | |
| 		tsim.Ident = im.Name.String()
 | |
| 	} else {
 | |
| 		sl := strings.Split(im.Path.Value, "/")
 | |
| 		final := sl[len(sl)-1]
 | |
| 		if idx := strings.Index(final, ":"); idx != -1 {
 | |
| 			tsim.Pkg = final[idx:]
 | |
| 		} else {
 | |
| 			tsim.Pkg = final
 | |
| 		}
 | |
| 	}
 | |
| 	return tsim
 | |
| }
 | |
| 
 | |
| var themamodpath string = filepath.Join("cue.mod", "pkg", "github.com", "grafana", "thema")
 | |
| 
 | |
| // all copied and hacked up from Thema's LoadInstancesWithThema, simply to allow setting the
 | |
| // package name
 | |
| func loadInstancesWithThema(modFS fs.FS, dir string, pkgname string) (*build.Instance, error) {
 | |
| 	var modname string
 | |
| 	err := fs.WalkDir(modFS, "cue.mod", func(path string, d fs.DirEntry, err error) error {
 | |
| 		// fs.FS implementations tend to not use path separators as expected. Use a
 | |
| 		// normalized one for comparisons, but retain the original for calls back into modFS.
 | |
| 		normpath := filepath.FromSlash(path)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		if d.IsDir() {
 | |
| 			switch normpath {
 | |
| 			case filepath.Join("cue.mod", "gen"), filepath.Join("cue.mod", "usr"):
 | |
| 				return fs.SkipDir
 | |
| 			case themamodpath:
 | |
| 				return fmt.Errorf("path %q already exists in modFS passed to InstancesWithThema, must be absent for dynamic dependency injection", themamodpath)
 | |
| 			}
 | |
| 			return nil
 | |
| 		} else if normpath == filepath.Join("cue.mod", "module.cue") {
 | |
| 			modf, err := modFS.Open(path)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 			defer modf.Close() // nolint: errcheck
 | |
| 
 | |
| 			b, err := io.ReadAll(modf)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 
 | |
| 			modname, err = cuecontext.New().CompileBytes(b).LookupPath(cue.MakePath(cue.Str("module"))).String()
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 			if modname == "" {
 | |
| 				return fmt.Errorf("InstancesWithThema requires non-empty module name in modFS' cue.mod/module.cue")
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		return nil
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	if modname == "" {
 | |
| 		return nil, errors.New("cue.mod/module.cue did not exist")
 | |
| 	}
 | |
| 
 | |
| 	modroot := filepath.FromSlash(filepath.Join("/", modname))
 | |
| 	overlay := make(map[string]load.Source)
 | |
| 	if err := tload.ToOverlay(modroot, modFS, overlay); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// Special case for when we're calling this loader with paths inside the thema module
 | |
| 	if modname == "github.com/grafana/thema" {
 | |
| 		if err := tload.ToOverlay(modroot, thema.CueJointFS, overlay); err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	} else {
 | |
| 		if err := tload.ToOverlay(filepath.Join(modroot, themamodpath), thema.CueFS, overlay); err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if dir == "" {
 | |
| 		dir = "."
 | |
| 	}
 | |
| 
 | |
| 	cfg := &load.Config{
 | |
| 		Overlay:    overlay,
 | |
| 		ModuleRoot: modroot,
 | |
| 		Module:     modname,
 | |
| 		Dir:        filepath.Join(modroot, dir),
 | |
| 		Package:    pkgname,
 | |
| 	}
 | |
| 	if dir == "." {
 | |
| 		cfg.Package = filepath.Base(modroot)
 | |
| 		cfg.Dir = modroot
 | |
| 	}
 | |
| 
 | |
| 	inst := load.Instances(nil, cfg)[0]
 | |
| 	if inst.Err != nil {
 | |
| 		return nil, inst.Err
 | |
| 	}
 | |
| 
 | |
| 	return inst, nil
 | |
| }
 | |
| 
 | |
| func toOverlay(prefix string, vfs fs.FS, overlay map[string]load.Source) error {
 | |
| 	if !filepath.IsAbs(prefix) {
 | |
| 		return fmt.Errorf("must provide absolute path prefix when generating cue overlay, got %q", prefix)
 | |
| 	}
 | |
| 	err := fs.WalkDir(vfs, ".", func(path string, d fs.DirEntry, err error) error {
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		if d.IsDir() {
 | |
| 			return nil
 | |
| 		}
 | |
| 
 | |
| 		f, err := vfs.Open(path)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		defer func(f fs.File) {
 | |
| 			err := f.Close()
 | |
| 			if err != nil {
 | |
| 				return
 | |
| 			}
 | |
| 		}(f)
 | |
| 
 | |
| 		b, err := io.ReadAll(f)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		overlay[filepath.Join(prefix, path)] = load.FromBytes(b)
 | |
| 		return nil
 | |
| 	})
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| type tsFile struct {
 | |
| 	V                 thema.SyntacticVersion
 | |
| 	WriteModelVersion bool
 | |
| 	Imports           []*tsImport
 | |
| 	Body              string
 | |
| }
 | |
| 
 | |
| type tsImport struct {
 | |
| 	Ident string
 | |
| 	Pkg   string
 | |
| }
 | |
| 
 | |
| var tsTemplate = template.Must(template.New("cuetsygen").Parse(`//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | |
| // This file is autogenerated. DO NOT EDIT.
 | |
| //
 | |
| // To regenerate, run "make gen-cue" from the repository root.
 | |
| //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | |
| {{range .Imports}}
 | |
| import * as {{.Ident}} from '{{.Pkg}}';{{end}}
 | |
| {{if .WriteModelVersion }}
 | |
| export const modelVersion = Object.freeze([{{index .V 0}}, {{index .V 1}}]);
 | |
| {{end}}
 | |
| {{.Body}}`))
 |