返回 DeepSeek-Reasonix
typescript.go
根目录 / desktop / internal / hostrpc / typescript.go
1 package hostrpc
2
3 import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "maps"
8 "regexp"
9 "slices"
10 "strings"
11 )
12
13 var tsIdentifierRe = regexp.MustCompile(`^[A-Za-z_$][A-Za-z0-9_$]*$`)
14
15 // WriteTypeScript renders c as desktopContract.generated.ts: the digest and
16 // command/event tables the shell embeds plus one interface per DTO and the
17 // GeneratedDesktopCommands method table the renderer bridge implements.
18 func WriteTypeScript(w io.Writer, c Contract) error {
19 names := tsTypeNames(c.Types)
20 var b strings.Builder
21 b.WriteString("// Code generated by go run . -emit-contract; DO NOT EDIT.\n")
22 b.WriteString("/* eslint-disable */\n\n")
23 fmt.Fprintf(&b, "export const DESKTOP_PROTOCOL_VERSION = %d;\n\n", c.ProtocolVersion)
24 fmt.Fprintf(&b, "export const DESKTOP_CONTRACT_DIGEST = %s;\n\n", tsString(c.Digest()))
25
26 commandNames := make([]string, 0, len(c.Commands))
27 for _, cmd := range c.Commands {
28 commandNames = append(commandNames, cmd.Name)
29 }
30 slices.Sort(commandNames)
31 writeStringTable(&b, "DESKTOP_COMMANDS", commandNames)
32 writeStringTable(&b, "DESKTOP_EVENTS", c.Events)
33 b.WriteString("export type DesktopCommandName = (typeof DESKTOP_COMMANDS)[number];\n\n")
34 b.WriteString("export type DesktopEventName = (typeof DESKTOP_EVENTS)[number];\n\n")
35
36 for _, key := range slices.Sorted(maps.Keys(c.Types)) {
37 fmt.Fprintf(&b, "export interface %s {\n", names[key])
38 for _, f := range c.Types[key].Fields {
39 optional := ""
40 if f.Optional {
41 optional = "?"
42 }
43 fmt.Fprintf(&b, " %s%s: %s;\n", tsProperty(f.Name), optional, tsType(f.Type, names))
44 }
45 b.WriteString("}\n\n")
46 }
47
48 b.WriteString("export interface GeneratedDesktopCommands {\n")
49 for _, cmd := range c.Commands {
50 params := make([]string, 0, len(cmd.Params))
51 for i, p := range cmd.Params {
52 params = append(params, fmt.Sprintf("arg%d: %s", i, tsType(p, names)))
53 }
54 result := "void"
55 if cmd.Result != nil {
56 result = tsType(*cmd.Result, names)
57 }
58 fmt.Fprintf(&b, " %s(%s): Promise<%s>;\n", cmd.Name, strings.Join(params, ", "), result)
59 }
60 b.WriteString("}\n")
61 _, err := io.WriteString(w, b.String())
62 return err
63 }
64
65 func writeStringTable(b *strings.Builder, name string, values []string) {
66 fmt.Fprintf(b, "export const %s = [\n", name)
67 for _, v := range values {
68 fmt.Fprintf(b, " %s,\n", tsString(v))
69 }
70 b.WriteString("] as const;\n\n")
71 }
72
73 func tsType(t TypeRef, names map[string]string) string {
74 switch t.Kind {
75 case KindString:
76 return "string"
77 case KindNumber, KindInteger:
78 return "number"
79 case KindBoolean:
80 return "boolean"
81 case KindArray:
82 elem := tsType(*t.Elem, names)
83 if strings.Contains(elem, " | ") {
84 elem = "(" + elem + ")"
85 }
86 return elem + "[]"
87 case KindMap:
88 return "Record<string, " + tsType(*t.Elem, names) + ">"
89 case KindObject:
90 if name, ok := names[t.Ref]; ok {
91 return name
92 }
93 return tsIdentifier(t.Ref)
94 case KindNullable:
95 return tsType(*t.Elem, names) + " | null"
96 }
97 return "unknown"
98 }
99
100 // tsTypeNames maps each pkg.Name key to a TypeScript interface name: the
101 // bare Go name when unique, otherwise prefixed with its package.
102 func tsTypeNames(types map[string]ObjectType) map[string]string {
103 bare := map[string][]string{}
104 for key := range types {
105 _, name, _ := strings.Cut(key, ".")
106 bare[name] = append(bare[name], key)
107 }
108 out := make(map[string]string, len(types))
109 for name, ks := range bare {
110 // These names belong to the emitted module or its generic helpers.
111 // A DTO named Record must not shadow Record<string, T> elsewhere.
112 reserved := name == "Record" || name == "Promise" || name == "GeneratedDesktopCommands" ||
113 name == "DesktopCommandName" || name == "DesktopEventName"
114 for _, key := range ks {
115 if len(ks) == 1 && !reserved {
116 out[key] = tsIdentifier(name)
117 continue
118 }
119 out[key] = tsIdentifier(strings.ReplaceAll(key, ".", "_"))
120 }
121 }
122 return out
123 }
124
125 func tsIdentifier(name string) string {
126 var b strings.Builder
127 for i, r := range name {
128 switch {
129 case r == '_' || r == '$' || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z'):
130 b.WriteRune(r)
131 case r >= '0' && r <= '9':
132 if i == 0 {
133 b.WriteByte('_')
134 }
135 b.WriteRune(r)
136 default:
137 b.WriteByte('_')
138 }
139 }
140 return b.String()
141 }
142
143 func tsProperty(name string) string {
144 if tsIdentifierRe.MatchString(name) {
145 return name
146 }
147 return tsString(name)
148 }
149
150 func tsString(s string) string {
151 quoted, _ := json.Marshal(s)
152 return string(quoted)
153 }
154
154 lines GO