Skip to content

Commit

Permalink
[CLI][codegen] Use yarn create @grafana/plugin to Generate Front-En…
Browse files Browse the repository at this point in the history
…d Code (#491)

Instead of using a snapshot of the output of `yarn create
@grafana/plugin` with some small tweaks when adding the `frontend`
component with `grafana-app-sdk project component add`, invoke the
`yarn` command directly, then tweak the output as needed. This keeps us
from falling behind the current plugin bootstrapping. As yarn is
required to build the frontend already, we have the component add check
if yarn is installed, and error if not, requesting that the user must
have yarn installed to add the frontend component.

Resolves #473

---------

Co-authored-by: Igor Suleymanov <[email protected]>
  • Loading branch information
IfSentient and radiohead authored Dec 2, 2024
1 parent 4184b2b commit 5562d49
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 78 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
- name: Build
run: go build -v cmd/grafana-app-sdk/*.go
integration-test:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
# git checkout
- name: Checkout code
Expand Down
171 changes: 94 additions & 77 deletions cmd/grafana-app-sdk/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ import (
//go:embed templates/*.tmpl
var templates embed.FS

//go:embed templates/frontend-static/* templates/frontend-static/.config/*
var frontEndStaticFiles embed.FS

var projectCmd = &cobra.Command{
Use: "project",
}
Expand Down Expand Up @@ -454,15 +451,15 @@ func projectAddComponent(cmd *cobra.Command, args []string) error {
os.Exit(1)
}
case "frontend":
err = addComponentFrontend(path, pluginID, !overwrite)
err = addComponentFrontend(path, pluginID)
if err != nil {
fmt.Printf("%s\n", err.Error())
os.Exit(1)
}
case "operator":
switch format {
case FormatCUE:
err = addComponentOperator(path, generator.(*codegen.Generator[codegen.Kind]), selectors, kindGrouping == kindGroupingGroup)
err = addComponentOperator(path, generator.(*codegen.Generator[codegen.Kind]), selectors, kindGrouping == kindGroupingGroup, !overwrite)
default:
return fmt.Errorf("unknown kind format '%s'", format)
}
Expand All @@ -485,12 +482,17 @@ type anyGenerator interface {
*codegen.Generator[codegen.Kind]
}

func addComponentOperator[G anyGenerator](projectRootPath string, generator G, selectors []string, groupKinds bool) error {
//nolint:revive
func addComponentOperator[G anyGenerator](projectRootPath string, generator G, selectors []string, groupKinds bool, confirmOverwrite bool) error {
// Get the repo from the go.mod file
repo, err := getGoModule(filepath.Join(projectRootPath, "go.mod"))
if err != nil {
return err
}
var writeFileFunc = writeFile
if confirmOverwrite {
writeFileFunc = writeFileWithOverwriteConfirm
}

var files codejen.Files
switch cast := any(generator).(type) {
Expand All @@ -511,7 +513,7 @@ func addComponentOperator[G anyGenerator](projectRootPath string, generator G, s
return err
}
for _, f := range files {
err = writeFile(filepath.Join(projectRootPath, f.RelativePath), f.Data)
err = writeFileFunc(filepath.Join(projectRootPath, f.RelativePath), f.Data)
if err != nil {
return err
}
Expand All @@ -521,7 +523,7 @@ func addComponentOperator[G anyGenerator](projectRootPath string, generator G, s
if err != nil {
return err
}
err = writeFile(filepath.Join(projectRootPath, "cmd", "operator", "Dockerfile"), dockerfile)
err = writeFileFunc(filepath.Join(projectRootPath, "cmd", "operator", "Dockerfile"), dockerfile)
if err != nil {
return err
}
Expand Down Expand Up @@ -609,34 +611,109 @@ func projectAddPluginAPI[G anyGenerator](generator G, repo, generatedAPIModelsPa
return nil
}

//
// Frontend plugin
//

func addComponentFrontend(projectRootPath string, pluginID string, promptForOverwrite bool) error {
//nolint:revive
func addComponentFrontend(projectRootPath string, pluginID string) error {
// Check plugin ID
if pluginID == "" {
return fmt.Errorf("plugin-id is required")
}

err := writeStaticFrontendFiles(filepath.Join(projectRootPath, "plugin"), promptForOverwrite)
if !isCommandInstalled("yarn") {
return fmt.Errorf("yarn must be installed to add the frontend component")
}

args := []string{"create", "@grafana/plugin", "--pluginType=app", "--hasBackend=true", "--pluginName=tmp", "--orgName=tmp"}
cmd := exec.Command("yarn", args...)
buf := bytes.Buffer{}
ebuf := bytes.Buffer{}
cmd.Stdout = &buf
cmd.Stderr = &ebuf
err := cmd.Start()
if err != nil {
return err
}
err = writePluginJSON(filepath.Join(projectRootPath, "plugin/src/plugin.json"),
fmt.Sprintf("%s-app", pluginID), "NAME", "AUTHOR", pluginID)
fmt.Println("Creating plugin frontend using `\033[0;32myarn create @grafana/plugin\033[0m` (this may take a moment)...")
err = cmd.Wait()
if err != nil {
// Only print command output on error
fmt.Println(buf.String())
fmt.Println(ebuf.String())
return err
}
err = writePluginConstants(filepath.Join(projectRootPath, "plugin/src/constants.ts"), pluginID)

// Remove a few directories that get created which we don't actually want
err = os.RemoveAll("./tmp-tmp-app/.github")
if err != nil {
return err
}
err = writePackageJSON(filepath.Join(projectRootPath, "plugin/package.json"), "NAME", "AUTHOR")
err = os.RemoveAll("./tmp-tmp-app/pkg")
if err != nil {
return err
}
return nil
err = os.Remove("./tmp-tmp-app/go.mod")
if err != nil {
return err
}
err = os.Remove("./tmp-tmp-app/go.sum")
if err != nil {
return err
}
// Move the remaining contents into /plugin
err = moveFiles("./tmp-tmp-app/", filepath.Join(projectRootPath, "plugin"))
if err != nil {
return err
}
err = writePluginJSON(filepath.Join(projectRootPath, "plugin/src/plugin.json"),
fmt.Sprintf("%s-app", pluginID), "NAME", "AUTHOR", pluginID)
if err != nil {
return err
}
err = writePluginConstants(filepath.Join(projectRootPath, "plugin/src/constants.ts"), pluginID)
if err != nil {
return err
}
return os.Remove("./tmp-tmp-app")
}

func moveFiles(srcDir, destDir string) error {
return filepath.WalkDir(srcDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}

// Just move directories wholesale by renaming
if d.IsDir() {
if path == srcDir {
return nil
}
dst := filepath.Join(destDir, d.Name())
if _, serr := os.Stat(dst); serr == nil {
err := moveFiles(path, dst)
if err != nil {
return err
}
if err = os.Remove(path); err != nil {
return err
}
return fs.SkipDir
}
err = os.Rename(path, filepath.Join(destDir, d.Name()))
if err != nil {
return err
}
return fs.SkipDir
}

return os.Rename(path, filepath.Join(destDir, d.Name()))
})
}

func isCommandInstalled(command string) bool {
cmd := exec.Command("which", command)
err := cmd.Run()
return err == nil
}

func writePluginJSON(fullPath, id, name, author, slug string) error {
Expand All @@ -663,26 +740,6 @@ func writePluginJSON(fullPath, id, name, author, slug string) error {
return writeFile(fullPath, b.Bytes())
}

func writePackageJSON(fullPath, name, author string) error {
tmp, err := template.ParseFS(templates, "templates/package.json.tmpl")
if err != nil {
return err
}
data := struct {
PluginName string
PluginAuthor string
}{
PluginName: name,
PluginAuthor: author,
}
b := bytes.Buffer{}
err = tmp.Execute(&b, data)
if err != nil {
return err
}
return writeFile(fullPath, b.Bytes())
}

func writePluginConstants(fullPath, pluginID string) error {
tmp, err := template.ParseFS(templates, "templates/constants.ts.tmpl")
if err != nil {
Expand All @@ -700,43 +757,3 @@ func writePluginConstants(fullPath, pluginID string) error {
}
return writeFile(fullPath, b.Bytes())
}

func writeStaticFrontendFiles(pluginPath string, promptForOverwrite bool) error {
return writeStaticFiles(frontEndStaticFiles, "templates/frontend-static", pluginPath, promptForOverwrite)
}

type mergedFS interface {
fs.ReadDirFS
fs.ReadFileFS
}

//nolint:revive
func writeStaticFiles(fs mergedFS, readDir, writeDir string, promptForOverwrite bool) error {
files, err := fs.ReadDir(readDir)
if err != nil {
return err
}
for _, f := range files {
if f.IsDir() {
err = writeStaticFiles(fs, filepath.Join(readDir, f.Name()), filepath.Join(writeDir, f.Name()),
promptForOverwrite)
if err != nil {
return err
}
continue
}
b, err := fs.ReadFile(filepath.Join(readDir, f.Name()))
if err != nil {
return err
}
if promptForOverwrite {
err = writeFileWithOverwriteConfirm(filepath.Join(writeDir, f.Name()), b)
} else {
err = writeFile(filepath.Join(writeDir, f.Name()), b)
}
if err != nil {
return err
}
}
return nil
}

0 comments on commit 5562d49

Please sign in to comment.