Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 23 additions & 10 deletions cmd/lokstra/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func printUsage() {
fmt.Println()
fmt.Println("Usage:")
fmt.Println(" lokstra new <project-name> [flags]")
fmt.Println(" lokstra autogen|generate [folder]")
fmt.Println(" lokstra autogen|generate [folder] [flags]")
fmt.Println(" lokstra migration|migrate <command> [flags]")
fmt.Println(" lokstra version")
fmt.Println(" lokstra help")
Expand All @@ -65,6 +65,9 @@ func printUsage() {
fmt.Println(" -template <name> Template to use (optional, interactive if not specified)")
fmt.Println(" -branch <name> 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 <name> Create new migration files")
fmt.Println(" lokstra migration up [flags] Run pending migrations")
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
44 changes: 36 additions & 8 deletions core/annotation/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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] {
Expand All @@ -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
}
Expand Down Expand Up @@ -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}}"
Expand Down
131 changes: 131 additions & 0 deletions core/annotation/complex_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -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
}
Expand All @@ -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()
}
}
}
}
})
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
22 changes: 11 additions & 11 deletions docs/00-introduction/examples/annotations/zz_cache.lokstra.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}