diff --git a/cmd/lekko/bisync.go b/cmd/lekko/bisync.go index cf95c0a9..f2fdf244 100644 --- a/cmd/lekko/bisync.go +++ b/cmd/lekko/bisync.go @@ -36,8 +36,8 @@ func bisyncCmd() *cobra.Command { Files at the provided path that contain valid Lekko config functions will first be translated and synced to the config repository on the local filesystem, then translated back to Lekko-canonical form, performing any code generation as necessary. This may affect ordering of functions/parameters and formatting.`, RunE: func(cmd *cobra.Command, args []string) error { - _, nativeLang := try.To2(native.DetectNativeLang("")) - return bisync(context.Background(), nativeLang, lekkoPath, repoPath) + nlProject := try.To1(native.DetectNativeLang("")) + return bisync(context.Background(), nlProject, lekkoPath, repoPath) }, } cmd.Flags().StringVarP(&lekkoPath, "lekko-path", "p", "", "Path to Lekko native config files, will use autodetect if not set") @@ -47,7 +47,7 @@ This may affect ordering of functions/parameters and formatting.`, return cmd } -func bisync(ctx context.Context, nativeLang native.NativeLang, lekkoPath, repoPath string) error { +func bisync(ctx context.Context, project *native.Project, lekkoPath, repoPath string) error { if len(lekkoPath) == 0 { dot := try.To1(dotlekko.ReadDotLekko("")) lekkoPath = dot.LekkoPath @@ -55,10 +55,10 @@ func bisync(ctx context.Context, nativeLang native.NativeLang, lekkoPath, repoPa if len(repoPath) == 0 { repoPath = try.To1(repo.PrepareGithubRepo()) } - switch nativeLang { - case native.GO: + switch project.Language { + case native.LangGo: _ = try.To1(sync.BisyncGo(ctx, lekkoPath, lekkoPath, repoPath)) - case native.TS: + case native.LangTypeScript: try.To(sync.BisyncTS(lekkoPath, repoPath)) default: return errors.New("unsupported language") @@ -72,7 +72,11 @@ func bisyncGoCmd() *cobra.Command { Use: "go", Short: "Lekko bisync for Go. Should be run from project root.", RunE: func(cmd *cobra.Command, args []string) error { - return bisync(context.Background(), native.GO, lekkoPath, repoPath) + nlProject := try.To1(native.DetectNativeLang("")) + if nlProject.Language != native.LangGo { + return errors.Errorf("not a Go project, detected %v instead", nlProject.Language) + } + return bisync(context.Background(), nlProject, lekkoPath, repoPath) }, } cmd.Flags().StringVarP(&lekkoPath, "lekko-path", "p", "", "Path to Lekko native config files, will use autodetect if not set") @@ -86,7 +90,11 @@ func bisyncTSCmd() *cobra.Command { Use: "ts", Short: "Lekko bisync for Typescript. Should be run from project root.", RunE: func(cmd *cobra.Command, args []string) error { - return bisync(context.Background(), native.TS, lekkoPath, repoPath) + nlProject := try.To1(native.DetectNativeLang("")) + if nlProject.Language != native.LangTypeScript { + return errors.Errorf("not a TypeScript project, detected %v instead", nlProject.Language) + } + return bisync(context.Background(), nlProject, lekkoPath, repoPath) }, } cmd.Flags().StringVarP(&lekkoPath, "lekko-path", "p", "", "Path to Lekko native config files, will use autodetect if not set") diff --git a/cmd/lekko/gen.go b/cmd/lekko/gen.go index 5d8f3218..bdef684f 100644 --- a/cmd/lekko/gen.go +++ b/cmd/lekko/gen.go @@ -45,8 +45,8 @@ func genCmd() *cobra.Command { Use: "gen", Short: "generate Lekko config functions from a local config repository", RunE: func(cmd *cobra.Command, args []string) error { - meta, nativeLang := try.To2(native.DetectNativeLang("")) - return genNative(context.Background(), meta, nativeLang, lekkoPath, repoPath, ns, initMode) + nlProject := try.To1(native.DetectNativeLang("")) + return genNative(context.Background(), nlProject, lekkoPath, repoPath, ns, initMode) }, } cmd.Flags().StringVarP(&lekkoPath, "lekko-path", "p", "", "Path to Lekko native config files, will use autodetect if not set") @@ -59,7 +59,7 @@ func genCmd() *cobra.Command { return cmd } -func genNative(ctx context.Context, nativeMetadata native.Metadata, nativeLang native.NativeLang, lekkoPath, repoPath, ns string, initMode bool) (err error) { +func genNative(ctx context.Context, project *native.Project, lekkoPath, repoPath, ns string, initMode bool) (err error) { defer err2.Handle(&err) if len(lekkoPath) == 0 { dot := try.To1(dotlekko.ReadDotLekko("")) @@ -69,13 +69,12 @@ func genNative(ctx context.Context, nativeMetadata native.Metadata, nativeLang n repoPath = try.To1(repo.PrepareGithubRepo()) } opts := gen.GenOptions{ - InitMode: initMode, - NativeMetadata: nativeMetadata, + InitMode: initMode, } if len(ns) > 0 { opts.Namespaces = []string{ns} } - return gen.GenNative(ctx, nativeLang, lekkoPath, repoPath, opts) + return gen.GenNative(ctx, project, lekkoPath, repoPath, opts) } func genGoCmd() *cobra.Command { @@ -85,7 +84,11 @@ func genGoCmd() *cobra.Command { Use: "go", Short: "generate Go library code from configs", RunE: func(cmd *cobra.Command, args []string) error { - return genNative(cmd.Context(), nil, native.GO, lekkoPath, repoPath, ns, initMode) + nlProject := try.To1(native.DetectNativeLang("")) + if nlProject.Language != native.LangGo { + return errors.Errorf("not a Go project, detected %v instead", nlProject.Language) + } + return genNative(cmd.Context(), nlProject, lekkoPath, repoPath, ns, initMode) }, } cmd.Flags().StringVarP(&lekkoPath, "lekko-path", "p", "", "Path to Lekko native config files, will use autodetect if not set") @@ -103,7 +106,11 @@ func genTSCmd() *cobra.Command { Use: "ts", Short: "generate typescript library code from configs", RunE: func(cmd *cobra.Command, args []string) error { - return genNative(cmd.Context(), nil, native.TS, lekkoPath, repoPath, ns, false) + nlProject := try.To1(native.DetectNativeLang("")) + if nlProject.Language != native.LangTypeScript { + return errors.Errorf("not a Go project, detected %v instead", nlProject.Language) + } + return genNative(cmd.Context(), nlProject, lekkoPath, repoPath, ns, false) }, } cmd.Flags().StringVarP(&lekkoPath, "lekko-path", "p", "", "path to Lekko native config files, will use autodetect if not set") diff --git a/cmd/lekko/init.go b/cmd/lekko/init.go index 062e7074..ee239b76 100644 --- a/cmd/lekko/init.go +++ b/cmd/lekko/init.go @@ -15,11 +15,9 @@ package main import ( - "context" "fmt" "os" "os/exec" - "path/filepath" "strings" "time" @@ -37,25 +35,6 @@ import ( "github.com/spf13/cobra" ) -type projectFramework int - -const ( - pfUnknown projectFramework = iota - pfGo - pfNode - pfReact - pfVite - pfNext -) - -type packageManager string - -const ( - pmUnknown packageManager = "" - pmNPM packageManager = "npm" - pmYarn packageManager = "yarn" -) - func initCmd() *cobra.Command { var lekkoPath, repoName string cmd := &cobra.Command{ @@ -65,59 +44,42 @@ func initCmd() *cobra.Command { defer err2.Handle(&err) successCheck := logging.Green("\u2713") spin := spinner.New(spinner.CharSets[14], 100*time.Millisecond) - // TODO: - // + create .lekko file - // + generate from `default` namespace - // + install lekko deps (depending on project type) - // + setup github actions - // - install linter + + nlProject, err := native.DetectNativeLang("") + if err != nil { + return errors.Wrap(err, "detect project information") + } + // Output detected information + fmt.Println("Detected the following project information:") + fmt.Printf("- Language: %s\n", logging.Bold(nlProject.Language)) + if nlProject.PackageManager != native.PmUnknown { + fmt.Printf("- Package manager: %s\n", logging.Bold(nlProject.PackageManager)) + } + if len(nlProject.Frameworks) > 0 { + fmt.Printf("- Frameworks: ") + for i, fw := range nlProject.Frameworks { + fmt.Printf("%s", logging.Bold(fw)) + if i < len(nlProject.Frameworks)-1 { + fmt.Printf(", ") + } + } + fmt.Printf("\n") + } + fmt.Println("") + // TODO: Ask for confirmation and if no, allow manual override + _, err = dotlekko.ReadDotLekko("") if err == nil { fmt.Println("Lekko is already initialized in this project.") return nil } - // TODO: print some info - - // naive check for "known" project types - // TODO: Consolidate into DetectNativeLang - pf := pfUnknown - pm := pmUnknown - if _, err = os.Stat("go.mod"); err == nil { - pf = pfGo - } else if _, err = os.Stat("package.json"); err == nil { - pf = pfNode - pjBytes, err := os.ReadFile("package.json") - if err != nil { - return errors.Wrap(err, "failed to open package.json") - } - pjString := string(pjBytes) - if strings.Contains(pjString, "react-dom") { - pf = pfReact - } - // Vite config file could be js, cjs, mjs, etc. - if matches, err := filepath.Glob("vite.config.*"); matches != nil && err == nil { - pf = pfVite - } - // Next config file could be js, cjs, mjs, etc. - if matches, err := filepath.Glob("next.config.*"); matches != nil && err == nil { - pf = pfNext - } - - pm = pmNPM - if _, err := os.Stat("yarn.lock"); err == nil { - pm = pmYarn - } - } - if pf == pfUnknown { - return errors.New("Unknown project type, Lekko currently supports Go and NPM projects.") - } if lekkoPath == "" { lekkoPath = "lekko" if fi, err := os.Stat("src"); err == nil && fi.IsDir() { lekkoPath = "src/lekko" } - if fi, err := os.Stat("internal"); err == nil && fi.IsDir() && pf == pfGo { + if fi, err := os.Stat("internal"); err == nil && fi.IsDir() && nlProject.Language == native.LangGo { lekkoPath = "internal/lekko" } try.To(survey.AskOne(&survey.Input{ @@ -180,7 +142,7 @@ func initCmd() *cobra.Command { return errors.Wrap(err, "failed to mkdir .github/workflows") } workflowTemplate := getGitHubWorkflowTemplateBase() - if suffix, err := getGitHubWorkflowTemplateSuffix(pf, pm); err != nil { + if suffix, err := getGitHubWorkflowTemplateSuffix(nlProject); err != nil { return err } else { workflowTemplate += suffix @@ -189,15 +151,15 @@ func initCmd() *cobra.Command { return errors.Wrap(err, "failed to write Lekko workflow file") } // TODO: Consider moving instructions to end? - fmt.Printf("%s Successfully added .github/workflows/lekko.yaml, please make sure to add LEKKO_API_KEY as a secret in your GitHub repository/org settings.\n", successCheck) + fmt.Printf("%s Successfully added .github/workflows/lekko.yaml. Please make sure to add LEKKO_API_KEY as a secret in your GitHub repository/org settings.\n", successCheck) } // TODO: Install deps depending on project type // TODO: Determine package manager (npm/yarn/pnpm/etc.) for ts projects spin.Suffix = " Installing dependencies..." spin.Start() - switch pf { - case pfGo: + switch nlProject.Language { + case native.LangGo: { goGetCmd := exec.Command("go", "get", "github.com/lekkodev/go-sdk@latest") if out, err := goGetCmd.CombinedOutput(); err != nil { @@ -207,89 +169,90 @@ func initCmd() *cobra.Command { return errors.Wrap(err, "failed to run go get") } spin.Stop() - fmt.Printf("%s Successfully installed Lekko Go SDK.\n", successCheck) + fmt.Printf("%s Successfully installed the Lekko Go SDK. See https://docs.lekko.com/sdks/go-sdk on how to use the SDK.\n", successCheck) spin.Start() } - case pfVite: - // NOTE: Vite doesn't necessarily mean React but we assume for now + case native.LangTypeScript: { - var installArgs, installDevArgs []string - switch pm { - case pmNPM: - { - installArgs = []string{"install", "@lekko/react-sdk"} - installDevArgs = []string{"install", "-D", "@lekko/vite-plugin", "@lekko/eslint-plugin"} - } - case pmYarn: - { - installArgs = []string{"add", "@lekko/react-sdk"} - installDevArgs = []string{"add", "-D", "@lekko/vite-plugin", "@lekko/eslint-plugin"} + if nlProject.HasFramework(native.FwVite) { + // NOTE: Vite doesn't necessarily mean React but we assume for now + var installArgs, installDevArgs []string + switch nlProject.PackageManager { + case native.PmNPM: + { + installArgs = []string{"install", "@lekko/react-sdk"} + installDevArgs = []string{"install", "-D", "@lekko/vite-plugin", "@lekko/eslint-plugin"} + } + case native.PmYarn: + { + installArgs = []string{"add", "@lekko/react-sdk"} + installDevArgs = []string{"add", "-D", "@lekko/vite-plugin", "@lekko/eslint-plugin"} + } + default: + { + return errors.Errorf("unsupported package manager %s", nlProject.PackageManager) + } } - default: - { - return errors.Errorf("unsupported package manager %s", pm) + installCmd := exec.Command(string(nlProject.PackageManager), installArgs...) // #nosec G204 + if out, err := installCmd.CombinedOutput(); err != nil { + spin.Stop() + fmt.Println(installCmd.String()) + fmt.Println(string(out)) + return errors.Wrap(err, "failed to run install deps command") } - } - installCmd := exec.Command(string(pm), installArgs...) // #nosec G204 - if out, err := installCmd.CombinedOutput(); err != nil { spin.Stop() - fmt.Println(installCmd.String()) - fmt.Println(string(out)) - return errors.Wrap(err, "failed to run install deps command") - } - spin.Stop() - fmt.Printf("%s Successfully installed @lekko/react-sdk.\n", successCheck) - spin.Start() - installCmd = exec.Command(string(pm), installDevArgs...) // #nosec G204 - if out, err := installCmd.CombinedOutput(); err != nil { - spin.Stop() - fmt.Println(installCmd.String()) - fmt.Println(string(out)) - return errors.Wrap(err, "failed to run install dev deps command") - } - spin.Stop() - fmt.Printf("%s Successfully installed @lekko/vite-plugin and @lekko/eslint-plugin. See the docs to configure these plugins.\n", successCheck) - spin.Start() - } - case pfNext: - { - var installArgs, installDevArgs []string - switch pm { - case pmNPM: - { - installArgs = []string{"install", "@lekko/next-sdk"} - installDevArgs = []string{"install", "-D", "@lekko/eslint-plugin"} + fmt.Printf("%s Successfully installed @lekko/react-sdk. See https://docs.lekko.com/sdks/react-sdk on how to use the SDK.\n", successCheck) + spin.Start() + installCmd = exec.Command(string(nlProject.PackageManager), installDevArgs...) // #nosec G204 + if out, err := installCmd.CombinedOutput(); err != nil { + spin.Stop() + fmt.Println(installCmd.String()) + fmt.Println(string(out)) + return errors.Wrap(err, "failed to run install dev deps command") } - case pmYarn: - { - installArgs = []string{"add", "@lekko/next-sdk"} - installDevArgs = []string{"add", "-D", "@lekko/eslint-plugin"} + spin.Stop() + fmt.Printf("%s Successfully installed @lekko/vite-plugin. See https://www.npmjs.com/package/@lekko/vite-plugin on how to configure this plugin.\n", successCheck) + fmt.Printf("%s Successfully installed @lekko/eslint-plugin. See https://www.npmjs.com/package/@lekko/eslint-plugin on how to configure this plugin.\n", successCheck) + spin.Start() + } else if nlProject.HasFramework(native.FwNext) { + var installArgs, installDevArgs []string + switch nlProject.PackageManager { + case native.PmNPM: + { + installArgs = []string{"install", "@lekko/next-sdk"} + installDevArgs = []string{"install", "-D", "@lekko/eslint-plugin"} + } + case native.PmYarn: + { + installArgs = []string{"add", "@lekko/next-sdk"} + installDevArgs = []string{"add", "-D", "@lekko/eslint-plugin"} + } + default: + { + return errors.Errorf("unsupported package manager %s", nlProject.PackageManager) + } } - default: - { - return errors.Errorf("unsupported package manager %s", pm) + installCmd := exec.Command(string(nlProject.PackageManager), installArgs...) // #nosec G204 + if out, err := installCmd.CombinedOutput(); err != nil { + spin.Stop() + fmt.Println(installCmd.String()) + fmt.Println(string(out)) + return errors.Wrap(err, "failed to run install deps command") } - } - installCmd := exec.Command(string(pm), installArgs...) // #nosec G204 - if out, err := installCmd.CombinedOutput(); err != nil { spin.Stop() - fmt.Println(installCmd.String()) - fmt.Println(string(out)) - return errors.Wrap(err, "failed to run install deps command") - } - spin.Stop() - fmt.Printf("%s Successfully installed @lekko/next-sdk. See the docs to configure the SDK.\n", successCheck) - spin.Start() - installCmd = exec.Command(string(pm), installDevArgs...) // #nosec G204 - if out, err := installCmd.CombinedOutput(); err != nil { + fmt.Printf("%s Successfully installed @lekko/next-sdk. See https://docs.lekko.com/sdks/next-sdk on how to configure and use the SDK.\n", successCheck) + spin.Start() + installCmd = exec.Command(string(nlProject.PackageManager), installDevArgs...) // #nosec G204 + if out, err := installCmd.CombinedOutput(); err != nil { + spin.Stop() + fmt.Println(installCmd.String()) + fmt.Println(string(out)) + return errors.Wrap(err, "failed to run install dev deps command") + } spin.Stop() - fmt.Println(installCmd.String()) - fmt.Println(string(out)) - return errors.Wrap(err, "failed to run install dev deps command") + fmt.Printf("%s Successfully installed @lekko/eslint-plugin. See https://www.npmjs.com/package/@lekko/eslint-plugin on how to configure this plugin.\n", successCheck) + spin.Start() } - spin.Stop() - fmt.Printf("%s Successfully installed @lekko/eslint-plugin. See the docs to configure this plugin.\n", successCheck) - spin.Start() } } spin.Stop() @@ -298,14 +261,19 @@ func initCmd() *cobra.Command { spin.Suffix = " Running codegen..." spin.Start() // TODO: make sure that `default` namespace exists - try.To(runGen(cmd.Context(), lekkoPath, "default")) + repoPath := try.To1(repo.PrepareGithubRepo()) + if err := gen.GenNative(cmd.Context(), nlProject, lekkoPath, repoPath, gen.GenOptions{ + Namespaces: []string{"default"}, + }); err != nil { + return errors.Wrap(err, "codegen for default namespace") + } spin.Stop() // Post-gen steps spin.Suffix = " Running post-codegen steps..." spin.Start() - switch pf { - case pfGo: + switch nlProject.Language { + case native.LangGo: { // For Go we want to run `go mod tidy` - this handles transitive deps goTidyCmd := exec.Command("go", "mod", "tidy") @@ -319,7 +287,7 @@ func initCmd() *cobra.Command { } spin.Stop() - fmt.Printf("%s Complete! Your project is now set up to use Lekko.\n", successCheck) + fmt.Printf("\n%s Complete! Your project is now set up to use Lekko.\n", successCheck) return nil }, } @@ -328,16 +296,6 @@ func initCmd() *cobra.Command { return cmd } -func runGen(ctx context.Context, lekkoPath, ns string) (err error) { - defer err2.Handle(&err) - meta, nativeLang := try.To2(native.DetectNativeLang("")) - repoPath := try.To1(repo.PrepareGithubRepo()) - return gen.GenNative(ctx, nativeLang, lekkoPath, repoPath, gen.GenOptions{ - NativeMetadata: meta, - Namespaces: []string{ns}, - }) -} - func getGitHubWorkflowTemplateBase() string { // TODO: determine default branch name (might not be main) return `name: lekko @@ -358,36 +316,30 @@ jobs: ` } -func getGitHubWorkflowTemplateSuffix(pf projectFramework, pm packageManager) (string, error) { +func getGitHubWorkflowTemplateSuffix(nlProject *native.Project) (string, error) { // NOTE: Make sure to keep the indentation matched with base var ret string - switch pf { - case pfGo: + switch nlProject.Language { + case native.LangGo: { ret = ` - uses: actions/setup-go@v5 with: go-version-file: go.mod ` } - case pfNode: - fallthrough - case pfReact: - fallthrough - case pfVite: - fallthrough - case pfNext: + case native.LangTypeScript: { ret = ` - uses: actions/setup-node@v4 with: node-version: lts/Hydrogen ` - switch pm { - case pmNPM: + switch nlProject.PackageManager { + case native.PmNPM: { ret += ` - run: npm install ` } - case pmYarn: + case native.PmYarn: { ret += ` cache: yarn - run: yarn install diff --git a/cmd/lekko/repo.go b/cmd/lekko/repo.go index 69b65215..bb461b66 100644 --- a/cmd/lekko/repo.go +++ b/cmd/lekko/repo.go @@ -435,7 +435,7 @@ func pullCmd() *cobra.Command { ctx := cmd.Context() dot := try.To1(dotlekko.ReadDotLekko("")) - nativeMetadata, nativeLang := try.To2(native.DetectNativeLang("")) + nlProject := try.To1(native.DetectNativeLang("")) repoPath := try.To1(repo.PrepareGithubRepo()) gitRepo := try.To1(git.PlainOpen(repoPath)) @@ -476,7 +476,7 @@ func pullCmd() *cobra.Command { return fmt.Errorf("please commit or stash changes in '%s' before pulling", lekkoPath) } } - try.To(gen.GenNative(ctx, nativeLang, dot.LekkoPath, repoPath, gen.GenOptions{NativeMetadata: nativeMetadata})) + try.To(gen.GenNative(ctx, nlProject, dot.LekkoPath, repoPath, gen.GenOptions{})) dot.LockSHA = newHead.Hash().String() if err := dot.WriteBack(); err != nil { @@ -492,22 +492,22 @@ func pullCmd() *cobra.Command { } fmt.Printf("Rebasing from %s to %s\n\n", dot.LockSHA, newHead.Hash().String()) - switch nativeLang { - case native.TS: + switch nlProject.Language { + case native.LangTypeScript: tsPullCmd := exec.Command("npx", "lekko-repo-pull", "--lekko-dir", lekkoPath) output, err := tsPullCmd.CombinedOutput() fmt.Println(string(output)) if err != nil { return errors.Wrap(err, "ts pull") } - case native.GO: + case native.LangGo: files, err := sync.BisyncGo(ctx, lekkoPath, lekkoPath, repoPath) if err != nil { return errors.Wrap(err, "go bisync") } hasConflicts := false for _, f := range files { - hasConflicts = hasConflicts && try.To1(mergeFile(ctx, f, dot, nativeMetadata)) + hasConflicts = hasConflicts && try.To1(mergeFile(ctx, f, dot, nlProject)) } if !hasConflicts { if _, err := sync.BisyncGo(ctx, lekkoPath, lekkoPath, repoPath); err != nil { @@ -515,7 +515,7 @@ func pullCmd() *cobra.Command { } } default: - return fmt.Errorf("unsupported language: %s", nativeLang) + return fmt.Errorf("unsupported language: %s", nlProject.Language) } dot.LockSHA = newHead.Hash().String() @@ -530,7 +530,7 @@ func pullCmd() *cobra.Command { return cmd } -func mergeFile(ctx context.Context, filename string, dot *dotlekko.DotLekko, nativeMetadata native.Metadata) (hasConflicts bool, err error) { +func mergeFile(ctx context.Context, filename string, dot *dotlekko.DotLekko, nlProject *native.Project) (hasConflicts bool, err error) { defer err2.Handle(&err) nativeLang, err := native.NativeLangFromExt(filename) if err != nil { @@ -580,10 +580,9 @@ func mergeFile(ctx context.Context, filename string, dot *dotlekko.DotLekko, nat return false, errors.Wrap(err, "create temp dir") } defer os.RemoveAll(baseDir) - err = gen.GenNative(ctx, nativeLang, dot.LekkoPath, repoPath, gen.GenOptions{ - CodeRepoPath: baseDir, - Namespaces: []string{ns}, - NativeMetadata: nativeMetadata, + err = gen.GenNative(ctx, nlProject, dot.LekkoPath, repoPath, gen.GenOptions{ + CodeRepoPath: baseDir, + Namespaces: []string{ns}, }) if err != nil { return false, errors.Wrap(err, "gen native") @@ -620,10 +619,9 @@ func mergeFile(ctx context.Context, filename string, dot *dotlekko.DotLekko, nat return false, errors.Wrap(err, "create temp dir") } defer os.RemoveAll(remoteDir) - err = gen.GenNative(ctx, nativeLang, dot.LekkoPath, repoPath, gen.GenOptions{ - CodeRepoPath: remoteDir, - Namespaces: []string{ns}, - NativeMetadata: nativeMetadata, + err = gen.GenNative(ctx, nlProject, dot.LekkoPath, repoPath, gen.GenOptions{ + CodeRepoPath: remoteDir, + Namespaces: []string{ns}, }) if err != nil { return false, errors.Wrap(err, "gen native") diff --git a/cmd/lekko/sync.go b/cmd/lekko/sync.go index 52bfbbac..8676c615 100644 --- a/cmd/lekko/sync.go +++ b/cmd/lekko/sync.go @@ -234,8 +234,8 @@ func isSame(ctx context.Context, existing map[string]map[string]*featurev1beta1. return false, err } dot := try.To1(dotlekko.ReadDotLekko("")) - _, nativeLang := try.To2(native.DetectNativeLang("")) - files := try.To1(native.ListNativeConfigFiles(dot.LekkoPath, nativeLang)) + nlProject := try.To1(native.DetectNativeLang("")) + files := try.To1(native.ListNativeConfigFiles(dot.LekkoPath, nlProject.Language)) var notEqual bool for _, f := range files { relativePath, err := filepath.Rel(wd, f) diff --git a/pkg/gen/gen.go b/pkg/gen/gen.go index e7f2e312..0ac3df6b 100644 --- a/pkg/gen/gen.go +++ b/pkg/gen/gen.go @@ -30,20 +30,19 @@ import ( ) type GenOptions struct { - CodeRepoPath string - Namespaces []string - InitMode bool - NativeMetadata native.Metadata + CodeRepoPath string + Namespaces []string + InitMode bool } func GenAuto(ctx context.Context, configRepoPath, codeRepoPath string) (err error) { defer err2.Handle(&err) - nativeMetadata, nativeLang := try.To2(native.DetectNativeLang(codeRepoPath)) + project := try.To1(native.DetectNativeLang(codeRepoPath)) dot := try.To1(dotlekko.ReadDotLekko(codeRepoPath)) - return GenNative(ctx, nativeLang, dot.LekkoPath, configRepoPath, GenOptions{CodeRepoPath: codeRepoPath, NativeMetadata: nativeMetadata}) + return GenNative(ctx, project, dot.LekkoPath, configRepoPath, GenOptions{CodeRepoPath: codeRepoPath}) } -func GenNative(ctx context.Context, nativeLang native.NativeLang, lekkoPath, repoPath string, opts GenOptions) (err error) { +func GenNative(ctx context.Context, project *native.Project, lekkoPath, repoPath string, opts GenOptions) (err error) { defer err2.Handle(&err) absLekkoPath := filepath.Join(opts.CodeRepoPath, lekkoPath) @@ -53,30 +52,30 @@ func GenNative(ctx context.Context, nativeLang native.NativeLang, lekkoPath, rep } if len(opts.Namespaces) == 0 { - opts.Namespaces = try.To1(native.ListNamespaces(absLekkoPath, nativeLang)) + opts.Namespaces = try.To1(native.ListNamespaces(absLekkoPath, project.Language)) } - switch nativeLang { - case native.TS: + switch project.Language { + case native.LangTypeScript: if opts.InitMode { return errors.New("init mode not supported for TS") } for _, ns := range opts.Namespaces { - outFilename := filepath.Join(absLekkoPath, ns+nativeLang.Ext()) + outFilename := filepath.Join(absLekkoPath, ns+project.Language.Ext()) try.To(genFormattedTS(ctx, repoPath, ns, outFilename)) } return nil - case native.GO: - return genFormattedGo(ctx, repoPath, lekkoPath, opts) + case native.LangGo: + return genFormattedGo(ctx, project, repoPath, lekkoPath, opts) default: return errors.New("Unsupported language") } } -func genFormattedGo(ctx context.Context, repoPath, lekkoPath string, opts GenOptions) (err error) { +func genFormattedGo(ctx context.Context, project *native.Project, repoPath, lekkoPath string, opts GenOptions) (err error) { defer err2.Handle(&err) var moduleRoot string - switch m := opts.NativeMetadata.(type) { + switch m := project.Metadata.(type) { case native.GoMetadata: moduleRoot = m.ModulePath default: diff --git a/pkg/gen/golang.go b/pkg/gen/golang.go index 5a382b74..8e9c200a 100644 --- a/pkg/gen/golang.go +++ b/pkg/gen/golang.go @@ -1115,7 +1115,7 @@ func (p *noOpProvider) Close(ctx context.Context) error { } // Walk through lekko/ directory to find namespaces // We walk through dir instead of just using the namespace from above because shared client init code should include every namespace - clientTemplateData.Namespaces = try.To1(native.ListNamespaces(g.outputPath, native.GO)) + clientTemplateData.Namespaces = try.To1(native.ListNamespaces(g.outputPath, native.LangGo)) var contents bytes.Buffer templ := template.Must(template.New("client").Funcs(clientTemplateFuncs).Parse(clientTemplateBody)) if err := templ.Execute(&contents, clientTemplateData); err != nil { diff --git a/pkg/native/native.go b/pkg/native/native.go index bb8a8160..31221b75 100644 --- a/pkg/native/native.go +++ b/pkg/native/native.go @@ -15,18 +15,19 @@ package native import ( - "errors" "fmt" "io/fs" "os" "path/filepath" "regexp" + "slices" "strings" "golang.org/x/mod/modfile" "github.com/lainio/err2" "github.com/lainio/err2/try" + "github.com/pkg/errors" ) type Metadata interface { @@ -39,53 +40,117 @@ type GoMetadata struct { func (GoMetadata) isMetadata() {} -type NativeLang string +// Representation of detected information about a native lang code project +type Project struct { + Language Language + PackageManager PackageManager + // A project can have multiple "frameworks" - e.g. React and Vite + Frameworks []Framework -var ( - GO NativeLang = "go" - TS NativeLang = "ts" + Metadata Metadata +} + +type Language string + +const ( + LangUnknown Language = "" + LangGo Language = "Go" + LangTypeScript Language = "TypeScript" +) + +type PackageManager string + +const ( + PmUnknown PackageManager = "" + PmNPM PackageManager = "npm" + PmYarn PackageManager = "yarn" ) -func DetectNativeLang(codeRepoPath string) (Metadata, NativeLang, error) { - // naive check for "known" project types +type Framework string + +const ( + FwUnknown Framework = "" + FwNode Framework = "Node" + FwReact Framework = "React" + FwVite Framework = "Vite" + FwNext Framework = "Next.js" +) + +func (p *Project) HasFramework(fw Framework) bool { + return slices.Contains(p.Frameworks, fw) +} + +// Check files in a code project to detect native lang information +func DetectNativeLang(codeRepoPath string) (*Project, error) { if _, err := os.Stat(filepath.Join(codeRepoPath, "go.mod")); err == nil { + // For Go, also parse go.mod b := try.To1(os.ReadFile(filepath.Join(codeRepoPath, "go.mod"))) mf := try.To1(modfile.ParseLax("go.mod", b, nil)) - return GoMetadata{ModulePath: mf.Module.Mod.Path}, GO, nil + return &Project{ + Language: LangGo, + Metadata: GoMetadata{ModulePath: mf.Module.Mod.Path}, + }, nil } else if _, err = os.Stat(filepath.Join(codeRepoPath, "package.json")); err == nil { - return nil, TS, nil + project := &Project{ + Language: LangTypeScript, + PackageManager: PmNPM, + } + + // Read package.json to see if this is a React project + pjBytes, err := os.ReadFile(filepath.Join(codeRepoPath, "package.json")) + if err != nil { + return nil, errors.Wrap(err, "failed to open package.json") + } + pjString := string(pjBytes) + if strings.Contains(pjString, "react-dom") { + project.Frameworks = append(project.Frameworks, FwReact) + } + // Vite config file could be js, cjs, mjs, etc. + if matches, err := filepath.Glob("vite.config.*"); matches != nil && err == nil { + project.Frameworks = append(project.Frameworks, FwVite) + } + // Next config file could be js, cjs, mjs, etc. + if matches, err := filepath.Glob("next.config.*"); matches != nil && err == nil { + project.Frameworks = append(project.Frameworks, FwNext) + } + + if _, err := os.Stat("yarn.lock"); err == nil { + project.PackageManager = PmYarn + } + + return project, nil } - return nil, "", errors.New("unknown project type, Lekko currently supports Go and NPM projects") + return nil, errors.New("unknown project type, Lekko currently supports Go and NPM projects") } -func NativeLangFromExt(filename string) (NativeLang, error) { +func NativeLangFromExt(filename string) (Language, error) { ext := filepath.Ext(filename) switch ext { case ".go": - return GO, nil + return LangGo, nil case ".ts": - return TS, nil + return LangTypeScript, nil } return "", fmt.Errorf("unsupported file extension: %v", ext) } -func (l *NativeLang) Ext() string { +func (l *Language) Ext() string { switch *l { - case GO: + case LangGo: return ".go" - case TS: + case LangTypeScript: return ".ts" } return "" } -func (l *NativeLang) GetNamespace(filename string) (string, error) { +func (l *Language) GetNamespace(filename string) (string, error) { var ns string switch *l { - case GO: + case LangGo: // TODO: check that filename == ns too ns = filepath.Base(filepath.Dir(filename)) - case TS: + case LangTypeScript: base := filepath.Base(filename) ns = strings.TrimSuffix(base, l.Ext()) default: @@ -97,7 +162,7 @@ func (l *NativeLang) GetNamespace(filename string) (string, error) { return ns, nil } -func ListNativeConfigFiles(lekkoPath string, nativeLang NativeLang) ([]string, error) { +func ListNativeConfigFiles(lekkoPath string, lang Language) ([]string, error) { var files []string err := filepath.WalkDir(lekkoPath, func(path string, d fs.DirEntry, err error) error { if err != nil { @@ -107,9 +172,8 @@ func ListNativeConfigFiles(lekkoPath string, nativeLang NativeLang) ([]string, e return fs.SkipDir } if !d.IsDir() && - strings.HasSuffix(d.Name(), nativeLang.Ext()) && - !strings.HasSuffix(d.Name(), "_gen"+nativeLang.Ext()) { - // + strings.HasSuffix(d.Name(), lang.Ext()) && + !strings.HasSuffix(d.Name(), "_gen"+lang.Ext()) { files = append(files, path) } return nil @@ -120,14 +184,14 @@ func ListNativeConfigFiles(lekkoPath string, nativeLang NativeLang) ([]string, e return files, nil } -func ListNamespaces(lekkoPath string, nativeLang NativeLang) (namespaces []string, err error) { +func ListNamespaces(lekkoPath string, lang Language) (namespaces []string, err error) { defer err2.Handle(&err) - files, err := ListNativeConfigFiles(lekkoPath, nativeLang) + files, err := ListNativeConfigFiles(lekkoPath, lang) if err != nil { return nil, err } for _, file := range files { - namespaces = append(namespaces, try.To1(nativeLang.GetNamespace(file))) + namespaces = append(namespaces, try.To1(lang.GetNamespace(file))) } return namespaces, nil } diff --git a/pkg/sync/push.go b/pkg/sync/push.go index b51e033a..71b71b46 100644 --- a/pkg/sync/push.go +++ b/pkg/sync/push.go @@ -36,7 +36,7 @@ import ( func Push(ctx context.Context, commitMessage string, force bool, dot *dotlekko.DotLekko) (err error) { defer err2.Handle(&err) - nativeMetadata, nativeLang, err := native.DetectNativeLang("") + nlProject, err := native.DetectNativeLang("") if err != nil { return err } @@ -85,9 +85,9 @@ func Push(ctx context.Context, commitMessage string, force bool, dot *dotlekko.D for _, ns := range rootMD.Namespaces { nsMap[ns] = true } - nativeFiles := try.To1(native.ListNativeConfigFiles(lekkoPath, nativeLang)) + nativeFiles := try.To1(native.ListNativeConfigFiles(lekkoPath, nlProject.Language)) for _, f := range nativeFiles { - if _, ok := nsMap[try.To1(nativeLang.GetNamespace(f))]; ok { + if _, ok := nsMap[try.To1(nlProject.Language.GetNamespace(f))]; ok { updatesExistingNamespace = true } } @@ -109,25 +109,24 @@ func Push(ctx context.Context, commitMessage string, force bool, dot *dotlekko.D return errors.Wrap(err, "create temp dir") } defer os.RemoveAll(remoteDir) - namespaces := try.To1(native.ListNamespaces(lekkoPath, nativeLang)) - try.To(gen.GenNative(ctx, nativeLang, lekkoPath, repoPath, gen.GenOptions{ - CodeRepoPath: remoteDir, - Namespaces: namespaces, - NativeMetadata: nativeMetadata, + namespaces := try.To1(native.ListNamespaces(lekkoPath, nlProject.Language)) + try.To(gen.GenNative(ctx, nlProject, lekkoPath, repoPath, gen.GenOptions{ + CodeRepoPath: remoteDir, + Namespaces: namespaces, })) - switch nativeLang { - case native.TS: + switch nlProject.Language { + case native.LangTypeScript: err = BisyncTS(lekkoPath, repoPath) if err != nil { return err } - case native.GO: + case native.LangGo: _, err = BisyncGo(ctx, lekkoPath, lekkoPath, repoPath) if err != nil { return err } default: - return fmt.Errorf("unsupported language: %s", nativeLang) + return fmt.Errorf("unsupported language: %s", nlProject.Language) } // Print diff between local and remote