diff --git a/cmd/lokstra/main.go b/cmd/lokstra/main.go index 26fdf45..969e98d 100644 --- a/cmd/lokstra/main.go +++ b/cmd/lokstra/main.go @@ -56,7 +56,7 @@ func printUsage() { fmt.Println() fmt.Println("Usage:") fmt.Println(" lokstra new [flags]") - fmt.Println(" lokstra autogen|generate [folder]") + fmt.Println(" lokstra autogen|generate [folder] [flags]") fmt.Println(" lokstra migration|migrate [flags]") fmt.Println(" lokstra version") fmt.Println(" lokstra help") @@ -65,6 +65,9 @@ func printUsage() { fmt.Println(" -template Template to use (optional, interactive if not specified)") fmt.Println(" -branch Git branch to download from (default: main)") fmt.Println() + fmt.Println("Flags for 'generate' command:") + fmt.Println(" -force Force rebuild by deleting all cache files") + fmt.Println() fmt.Println("Migration commands:") fmt.Println(" lokstra migration create Create new migration files") fmt.Println(" lokstra migration up [flags] Run pending migrations") @@ -140,20 +143,25 @@ func executeNew(projectName, templatePath, branch string) error { } func autogenCmd() { + // Parse flags + fs := flag.NewFlagSet("generate", flag.ExitOnError) + force := fs.Bool("force", false, "Force rebuild by deleting all cache files") + fs.Parse(os.Args[2:]) + // Get target folder (optional, defaults to current directory) targetFolder := "." - if len(os.Args) >= 3 { - targetFolder = os.Args[2] + if fs.NArg() > 0 { + targetFolder = fs.Arg(0) } // Execute autogen - if err := executeAutogen(targetFolder); err != nil { + if err := executeAutogen(targetFolder, *force); err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) } } -func executeAutogen(targetFolder string) error { +func executeAutogen(targetFolder string, force bool) error { fmt.Printf("🔧 Running code generation in: %s\n\n", targetFolder) // Convert to absolute path first @@ -169,17 +177,22 @@ func executeAutogen(targetFolder string) error { // Import annotation processor // Instead of running "go run . --generate-only", call annotation processor directly - return generateCodeForFolder(absPath) + return generateCodeForFolder(absPath, force) } // generateCodeForFolder calls the annotation processor to generate code -func generateCodeForFolder(absPath string) error { - // Delete all cache files first to force rebuild - if err := deleteAllCacheFilesInFolder(absPath); err != nil { - return fmt.Errorf("failed to delete cache files: %w", err) +func generateCodeForFolder(absPath string, force bool) error { + // Delete all cache files if --force flag is set + if force { + fmt.Println("🗑️ Force rebuild: deleting all cache files") + fmt.Println() + if err := deleteAllCacheFilesInFolder(absPath); err != nil { + return fmt.Errorf("failed to delete cache files: %w", err) + } } // Process the folder recursively using annotation processor + // Cache will be used automatically unless files changed or generated code was manually modified _, err := annotation.ProcessComplexAnnotations( []string{absPath}, 0, // Use default worker count (CPU * 2) diff --git a/core/annotation/codegen.go b/core/annotation/codegen.go index 5a8f8d5..7686d80 100644 --- a/core/annotation/codegen.go +++ b/core/annotation/codegen.go @@ -789,6 +789,28 @@ func writeGenFile(path string, ctx *RouterServiceContext, existingImports map[st // Sort struct names for deterministic order in init() sort.Strings(allStructNames) + // Determine which hardcoded imports are actually needed + needsDeploy := false + needsProxy := false + + for _, service := range ctx.GeneratedCode.Services { + if !service.IsService { + // @RouterService needs deploy and proxy + needsDeploy = true + needsProxy = true + } + } + + // Also check preserved sections for RouterService usage + for _, code := range ctx.GeneratedCode.PreservedSections { + if strings.Contains(code, "deploy.ServiceTypeConfig") || strings.Contains(code, "deploy.RouteConfig") { + needsDeploy = true + } + if strings.Contains(code, "proxy.Service") || strings.Contains(code, "proxy.Call") { + needsProxy = true + } + } + // Collect used packages from method signatures, dependencies, and struct name usedPackages := make(map[string]bool) for _, service := range ctx.GeneratedCode.Services { @@ -817,18 +839,18 @@ func writeGenFile(path string, ctx *RouterServiceContext, existingImports map[st // Filter imports to only used packages allImports := make(map[string]string) // path -> alias - // Hardcoded imports that are always included in template + // Hardcoded imports - conditionally included based on usage hardcodedImports := map[string]bool{ - "github.com/primadi/lokstra/core/deploy": true, - "github.com/primadi/lokstra/core/proxy": true, - "github.com/primadi/lokstra/lokstra_registry": true, + "github.com/primadi/lokstra/core/deploy": needsDeploy, + "github.com/primadi/lokstra/core/proxy": needsProxy, + "github.com/primadi/lokstra/lokstra_registry": true, // Always needed } // First, add imports from updated services for _, service := range ctx.GeneratedCode.Services { for alias, importPath := range service.Imports { - // Skip hardcoded imports - if hardcodedImports[importPath] { + // Skip hardcoded imports that are conditionally included + if _, isHardcoded := hardcodedImports[importPath]; isHardcoded { continue } // Only include if package is actually used in generated code @@ -843,8 +865,8 @@ func writeGenFile(path string, ctx *RouterServiceContext, existingImports map[st // Second, add imports from existing generated file if package is still used for alias, importPath := range existingImports { - // Skip hardcoded imports - if hardcodedImports[importPath] { + // Skip hardcoded imports that are conditionally included + if _, isHardcoded := hardcodedImports[importPath]; isHardcoded { continue } if usedPackages[alias] { @@ -868,6 +890,8 @@ func writeGenFile(path string, ctx *RouterServiceContext, existingImports map[st "PreservedSections": ctx.GeneratedCode.PreservedSections, "AllImports": allImports, "AllStructNames": allStructNames, + "NeedsDeploy": needsDeploy, + "NeedsProxy": needsProxy, }); err != nil { return err } @@ -1120,8 +1144,12 @@ var genTemplate = template.Must(template.New("gen").Funcs(template.FuncMap{ package {{.Package}} import ( +{{- if .NeedsDeploy }} "github.com/primadi/lokstra/core/deploy" +{{- end }} +{{- if .NeedsProxy }} "github.com/primadi/lokstra/core/proxy" +{{- end }} "github.com/primadi/lokstra/lokstra_registry" {{- range $path, $alias := .AllImports }} {{$alias}} "{{$path}}" diff --git a/core/annotation/complex_processor.go b/core/annotation/complex_processor.go index 171a474..f918ae8 100644 --- a/core/annotation/complex_processor.go +++ b/core/annotation/complex_processor.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "runtime" + "sort" "strings" "sync" "time" @@ -57,6 +58,10 @@ func ProcessComplexAnnotations(rootPath []string, maxWorkers int, allFolders = append(allFolders, folder) } + // Track packages that have generated code (for import file generation) + packagesWithServices := make([]string, 0) + var packageMutex sync.Mutex + if maxWorkers == 0 { maxWorkers = runtime.NumCPU() * 2 } @@ -76,6 +81,17 @@ func ProcessComplexAnnotations(rootPath []string, maxWorkers int, errChan <- fmt.Errorf("folder %s: %w", folder, err) } else if codeChanged { changedChan <- true + + // Check if this folder has generated code + genPath := filepath.Join(folder, internal.GeneratedFileName) + if _, err := os.Stat(genPath); err == nil { + // Get package import path for this folder + if pkgPath := getPackageImportPath(folder); pkgPath != "" { + packageMutex.Lock() + packagesWithServices = append(packagesWithServices, pkgPath) + packageMutex.Unlock() + } + } } } }) @@ -105,6 +121,14 @@ func ProcessComplexAnnotations(rootPath []string, maxWorkers int, return anyCodeChanged, fmt.Errorf("processing failed with %d errors: %v", len(errors), errors[0]) } + // Generate import file if we have packages with services + if len(packagesWithServices) > 0 && len(rootPath) > 0 { + if err := generateImportFile(rootPath[0], packagesWithServices); err != nil { + // Log warning but don't fail the whole operation + fmt.Printf("⚠️ Warning: Failed to generate import file: %v\n", err) + } + } + return anyCodeChanged, nil } @@ -596,3 +620,110 @@ func cleanupFolder(folderPath string) { internal.GeneratedFileName, folderPath) } } + +// getPackageImportPath determines the Go import path for a folder +// It reads the go.mod and calculates relative path from module root +func getPackageImportPath(folderPath string) string { + // Find go.mod by walking up + moduleRoot, moduleName := findGoModule(folderPath) + if moduleRoot == "" { + return "" + } + + // Calculate relative path from module root + relPath, err := filepath.Rel(moduleRoot, folderPath) + if err != nil { + return "" + } + + // Convert to import path (use forward slashes) + relPath = filepath.ToSlash(relPath) + + if relPath == "." { + return moduleName + } + + return moduleName + "/" + relPath +} + +// findGoModule finds go.mod by walking up from startPath +// Returns (moduleRoot, moduleName) +func findGoModule(startPath string) (string, string) { + dir := startPath + for { + goModPath := filepath.Join(dir, "go.mod") + if data, err := os.ReadFile(goModPath); err == nil { + // Parse module name from go.mod + lines := strings.Split(string(data), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "module ") { + moduleName := strings.TrimSpace(strings.TrimPrefix(line, "module")) + return dir, moduleName + } + } + } + + parent := filepath.Dir(dir) + if parent == dir { + // Reached root + return "", "" + } + dir = parent + } +} + +// findMainGoFolder finds the folder containing main.go by walking up from startPath +func findMainGoFolder(startPath string) string { + dir := startPath + for { + mainGoPath := filepath.Join(dir, "main.go") + if _, err := os.Stat(mainGoPath); err == nil { + return dir + } + + parent := filepath.Dir(dir) + if parent == dir { + // Reached root + return "" + } + dir = parent + } +} + +// generateImportFile creates zz_lokstra_imports.go in the same folder as main.go +func generateImportFile(startPath string, packages []string) error { + // Find main.go folder + mainFolder := findMainGoFolder(startPath) + if mainFolder == "" { + fmt.Fprintf(os.Stderr, "⚠️ Warning: main.go not found - skipping import file generation\n") + return nil // Not an error, just skip + } + + importFilePath := filepath.Join(mainFolder, "zz_lokstra_imports.go") + + // Sort packages for deterministic output + sortedPackages := make([]string, len(packages)) + copy(sortedPackages, packages) + sort.Strings(sortedPackages) + + // Generate import file content + var buf bytes.Buffer + buf.WriteString("// AUTO-GENERATED CODE - DO NOT EDIT\n") + buf.WriteString("// Generated by lokstra-annotation to auto-register services via init()\n") + buf.WriteString("// This file imports all packages containing @Service or @RouterService annotations\n\n") + buf.WriteString("package main\n\n") + buf.WriteString("import (\n") + for _, pkg := range sortedPackages { + buf.WriteString(fmt.Sprintf("\t_ %q\n", pkg)) + } + buf.WriteString(")\n") + + // Write file + if err := os.WriteFile(importFilePath, buf.Bytes(), 0644); err != nil { + return fmt.Errorf("failed to write %s: %w", importFilePath, err) + } + + fmt.Printf("✅ Generated: %s\n", importFilePath) + return nil +} diff --git a/docs/00-introduction/examples/annotations/zz_cache.lokstra.json b/docs/00-introduction/examples/annotations/zz_cache.lokstra.json index c3b30e1..608dbd7 100644 --- a/docs/00-introduction/examples/annotations/zz_cache.lokstra.json +++ b/docs/00-introduction/examples/annotations/zz_cache.lokstra.json @@ -5,53 +5,53 @@ "filename": "init_example.go", "checksum": "696a7b4b95565d9f3716cd685310671eb138cf19f86772a58f06e40fe794b61e", "annotations": 3, - "last_scan": "2025-12-07T02:26:09.4257958+07:00", + "last_scan": "2025-12-07T03:46:41.7400124+07:00", "generated": [ "zz_generated.lokstra.go" ], - "generated_mod_time": "2025-12-07T02:26:09.4257958+07:00" + "generated_mod_time": "2025-12-07T03:46:41.7400124+07:00" }, "mixed_services_example.go": { "filename": "mixed_services_example.go", "checksum": "7e27d4c1dd03132ad25c2d48a3d9aee10848963897d8e436b950ebcc184f902a", "annotations": 18, - "last_scan": "2025-12-07T02:26:09.4257958+07:00", + "last_scan": "2025-12-07T03:46:41.7400124+07:00", "generated": [ "zz_generated.lokstra.go" ], - "generated_mod_time": "2025-12-07T02:26:09.4257958+07:00" + "generated_mod_time": "2025-12-07T03:46:41.7400124+07:00" }, "router_service_with_config_example.go": { "filename": "router_service_with_config_example.go", "checksum": "8615e7b99db57225d9c442e2bcfcdddaceae1d38be692dec2daaf7be3cdfcbe0", "annotations": 16, - "last_scan": "2025-12-07T02:26:09.4257958+07:00", + "last_scan": "2025-12-07T03:46:41.7400124+07:00", "generated": [ "zz_generated.lokstra.go" ], - "generated_mod_time": "2025-12-07T02:26:09.4257958+07:00" + "generated_mod_time": "2025-12-07T03:46:41.7400124+07:00" }, "router_with_init_example.go": { "filename": "router_with_init_example.go", "checksum": "0618ae863b04e0d7f1759085749465572e9e41fff1f8c489afaa6de43e69564f", "annotations": 5, - "last_scan": "2025-12-07T02:26:09.4257958+07:00", + "last_scan": "2025-12-07T03:46:41.7400124+07:00", "generated": [ "zz_generated.lokstra.go" ], - "generated_mod_time": "2025-12-07T02:26:09.4257958+07:00" + "generated_mod_time": "2025-12-07T03:46:41.7400124+07:00" }, "service_example.go": { "filename": "service_example.go", "checksum": "e1830383b1cab4f69bca409897d06039121f08185bbcbbdb1c869f7e20943e68", "annotations": 12, - "last_scan": "2025-12-07T02:26:09.4257958+07:00", + "last_scan": "2025-12-07T03:46:41.7400124+07:00", "generated": [ "zz_generated.lokstra.go" ], - "generated_mod_time": "2025-12-07T02:26:09.4257958+07:00" + "generated_mod_time": "2025-12-07T03:46:41.7400124+07:00" } }, - "updated_at": "2025-12-07T02:26:09.4257958+07:00", + "updated_at": "2025-12-07T03:46:41.7400124+07:00", "generated_checksum": "f900aa878c75bf0f7595877fe58eec0a989cd7a7450fd66ebfcd2cec3a15f507" } \ No newline at end of file