1
0

generate.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. //go:generate go run .
  2. package main
  3. import (
  4. "fmt"
  5. "go/ast"
  6. "go/parser"
  7. "go/token"
  8. "os"
  9. "path/filepath"
  10. "runtime"
  11. "strings"
  12. "github.com/uozi-tech/cosy/logger"
  13. )
  14. // Structure for notification function calls
  15. type NotificationCall struct {
  16. Type string
  17. Title string
  18. Content string
  19. Path string
  20. }
  21. // Directories to exclude
  22. var excludeDirs = []string{
  23. ".devcontainer", ".github", ".idea", ".pnpm-store",
  24. ".vscode", "app", "query", "tmp", "cmd",
  25. }
  26. // Main function
  27. func main() {
  28. logger.Init("release")
  29. // Start scanning from the project root
  30. _, file, _, ok := runtime.Caller(0)
  31. if !ok {
  32. logger.Error("Unable to get the current file")
  33. return
  34. }
  35. root := filepath.Join(filepath.Dir(file), "../../")
  36. calls := []NotificationCall{}
  37. // Scan all Go files
  38. err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
  39. if err != nil {
  40. return err
  41. }
  42. // Skip excluded directories
  43. for _, dir := range excludeDirs {
  44. if strings.Contains(path, dir) {
  45. if info.IsDir() {
  46. return filepath.SkipDir
  47. }
  48. return nil
  49. }
  50. }
  51. // Only process Go files
  52. if !info.IsDir() && strings.HasSuffix(path, ".go") {
  53. findNotificationCalls(path, &calls)
  54. }
  55. return nil
  56. })
  57. if err != nil {
  58. logger.Errorf("Error walking the path: %v\n", err)
  59. return
  60. }
  61. // Generate a single TS file
  62. generateSingleTSFile(root, calls)
  63. logger.Infof("Found %d notification calls\n", len(calls))
  64. }
  65. // Find notification function calls in Go files
  66. func findNotificationCalls(filePath string, calls *[]NotificationCall) {
  67. // Parse Go code
  68. fset := token.NewFileSet()
  69. node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
  70. if err != nil {
  71. logger.Errorf("Error parsing %s: %v\n", filePath, err)
  72. return
  73. }
  74. // Traverse the AST to find function calls
  75. ast.Inspect(node, func(n ast.Node) bool {
  76. callExpr, ok := n.(*ast.CallExpr)
  77. if !ok {
  78. return true
  79. }
  80. // Check if it's a call to the notification package
  81. selExpr, ok := callExpr.Fun.(*ast.SelectorExpr)
  82. if !ok {
  83. return true
  84. }
  85. xident, ok := selExpr.X.(*ast.Ident)
  86. if !ok {
  87. return true
  88. }
  89. // Check if it's one of the functions we're interested in: notification.Info/Error/Warning/Success
  90. if xident.Name == "notification" {
  91. funcName := selExpr.Sel.Name
  92. if funcName == "Info" || funcName == "Error" || funcName == "Warning" || funcName == "Success" {
  93. // Function must have at least two parameters (title, content)
  94. if len(callExpr.Args) >= 2 {
  95. titleArg := callExpr.Args[0]
  96. contentArg := callExpr.Args[1]
  97. // Get parameter values
  98. title := getStringValue(titleArg)
  99. content := getStringValue(contentArg)
  100. // Ignore cases where content is a variable name or function call
  101. if content != "" && !isVariableOrFunctionCall(content) {
  102. *calls = append(*calls, NotificationCall{
  103. Type: funcName,
  104. Title: title,
  105. Content: content,
  106. Path: filePath,
  107. })
  108. }
  109. }
  110. }
  111. }
  112. return true
  113. })
  114. }
  115. // Check if the string is a variable name or function call
  116. func isVariableOrFunctionCall(s string) bool {
  117. // Simple check: if the string doesn't contain spaces or quotes, it might be a variable name
  118. if !strings.Contains(s, " ") && !strings.Contains(s, "\"") && !strings.Contains(s, "'") {
  119. return true
  120. }
  121. // If it looks like a function call, e.g., err.Error()
  122. if strings.Contains(s, "(") && strings.Contains(s, ")") {
  123. return true
  124. }
  125. return false
  126. }
  127. // Get string value from AST node
  128. func getStringValue(expr ast.Expr) string {
  129. // Direct string
  130. if lit, ok := expr.(*ast.BasicLit); ok && lit.Kind == token.STRING {
  131. // Return string without quotes
  132. return strings.Trim(lit.Value, "\"")
  133. }
  134. // Recover string value from source code expression
  135. var str strings.Builder
  136. if bin, ok := expr.(*ast.BinaryExpr); ok {
  137. // Handle string concatenation expression
  138. leftStr := getStringValue(bin.X)
  139. rightStr := getStringValue(bin.Y)
  140. str.WriteString(leftStr)
  141. str.WriteString(rightStr)
  142. }
  143. if str.Len() > 0 {
  144. return str.String()
  145. }
  146. // Return empty string if unable to parse as string
  147. return ""
  148. }
  149. // Generate a single TypeScript file
  150. func generateSingleTSFile(root string, calls []NotificationCall) {
  151. // Create target directory
  152. targetDir := filepath.Join(root, "app/src/components/Notification")
  153. err := os.MkdirAll(targetDir, 0755)
  154. if err != nil {
  155. logger.Errorf("Error creating directory %s: %v\n", targetDir, err)
  156. return
  157. }
  158. // Create file name
  159. tsFilePath := filepath.Join(targetDir, "notifications.ts")
  160. // Prepare file content
  161. var content strings.Builder
  162. content.WriteString("// Auto-generated notification texts\n")
  163. content.WriteString("// Extracted from Go source code notification function calls\n")
  164. content.WriteString("/* eslint-disable ts/no-explicit-any */\n\n")
  165. content.WriteString("const notifications: Record<string, { title: () => string, content: (args: any) => string }> = {\n")
  166. // Track used keys to avoid duplicates
  167. usedKeys := make(map[string]bool)
  168. // Organize notifications by directory
  169. messagesByDir := make(map[string][]NotificationCall)
  170. for _, call := range calls {
  171. dir := filepath.Dir(call.Path)
  172. // Extract module name from directory path
  173. dirParts := strings.Split(dir, "/")
  174. moduleName := dirParts[len(dirParts)-1]
  175. if strings.HasPrefix(dir, "internal/") || strings.HasPrefix(dir, "api/") {
  176. messagesByDir[moduleName] = append(messagesByDir[moduleName], call)
  177. } else {
  178. messagesByDir["general"] = append(messagesByDir["general"], call)
  179. }
  180. }
  181. // Add comments for each module and write notifications
  182. for module, moduleCalls := range messagesByDir {
  183. content.WriteString(fmt.Sprintf("\n // %s module notifications\n", module))
  184. for _, call := range moduleCalls {
  185. // Escape quotes in title and content
  186. escapedTitle := strings.ReplaceAll(call.Title, "'", "\\'")
  187. escapedContent := strings.ReplaceAll(call.Content, "'", "\\'")
  188. // Use just the title as the key
  189. key := call.Title
  190. // Check if key is already used, generate unique key if necessary
  191. uniqueKey := key
  192. counter := 1
  193. for usedKeys[uniqueKey] {
  194. uniqueKey = fmt.Sprintf("%s_%d", key, counter)
  195. counter++
  196. }
  197. usedKeys[uniqueKey] = true
  198. // Write record with both title and content as functions
  199. content.WriteString(fmt.Sprintf(" '%s': {\n", uniqueKey))
  200. content.WriteString(fmt.Sprintf(" title: () => $gettext('%s'),\n", escapedTitle))
  201. content.WriteString(fmt.Sprintf(" content: (args: any) => $gettext('%s', args),\n", escapedContent))
  202. content.WriteString(" },\n")
  203. }
  204. }
  205. content.WriteString("}\n\n")
  206. content.WriteString("export default notifications\n")
  207. // Write file
  208. err = os.WriteFile(tsFilePath, []byte(content.String()), 0644)
  209. if err != nil {
  210. logger.Errorf("Error writing TS file %s: %v\n", tsFilePath, err)
  211. return
  212. }
  213. logger.Infof("Generated single TS file: %s with %d notifications\n", tsFilePath, len(calls))
  214. }