emitter.go raw
1 package jsbackend
2
3 import (
4 "fmt"
5 "strings"
6 )
7
8 // Emitter builds JavaScript source code with proper indentation.
9 type Emitter struct {
10 buf strings.Builder
11 indent int
12 // Source file being emitted (for source map generation later).
13 sourceFile string
14 }
15
16 // NewEmitter creates a new JS code emitter.
17 func NewEmitter() *Emitter {
18 return &Emitter{}
19 }
20
21 // Indent increases the indentation level.
22 func (e *Emitter) Indent() {
23 e.indent++
24 }
25
26 // Dedent decreases the indentation level.
27 func (e *Emitter) Dedent() {
28 if e.indent > 0 {
29 e.indent--
30 }
31 }
32
33 // Line emits a line of code with proper indentation.
34 func (e *Emitter) Line(format string, args ...interface{}) {
35 for i := 0; i < e.indent; i++ {
36 e.buf.WriteString(" ")
37 }
38 fmt.Fprintf(&e.buf, format, args...)
39 e.buf.WriteByte('\n')
40 }
41
42 // Raw emits raw text without indentation or newline.
43 func (e *Emitter) Raw(s string) {
44 e.buf.WriteString(s)
45 }
46
47 // Newline emits an empty line.
48 func (e *Emitter) Newline() {
49 e.buf.WriteByte('\n')
50 }
51
52 // Comment emits a JS comment.
53 func (e *Emitter) Comment(format string, args ...interface{}) {
54 for i := 0; i < e.indent; i++ {
55 e.buf.WriteString(" ")
56 }
57 e.buf.WriteString("// ")
58 fmt.Fprintf(&e.buf, format, args...)
59 e.buf.WriteByte('\n')
60 }
61
62 // Block opens a braced block: { with optional prefix.
63 func (e *Emitter) Block(format string, args ...interface{}) {
64 for i := 0; i < e.indent; i++ {
65 e.buf.WriteString(" ")
66 }
67 if format != "" {
68 fmt.Fprintf(&e.buf, format, args...)
69 e.buf.WriteString(" {\n")
70 } else {
71 e.buf.WriteString("{\n")
72 }
73 e.indent++
74 }
75
76 // EndBlock closes a braced block: }
77 func (e *Emitter) EndBlock() {
78 e.indent--
79 for i := 0; i < e.indent; i++ {
80 e.buf.WriteString(" ")
81 }
82 e.buf.WriteString("}\n")
83 }
84
85 // EndBlockSuffix closes a braced block with a suffix (e.g., "} else {").
86 func (e *Emitter) EndBlockSuffix(suffix string) {
87 e.indent--
88 for i := 0; i < e.indent; i++ {
89 e.buf.WriteString(" ")
90 }
91 fmt.Fprintf(&e.buf, "} %s\n", suffix)
92 }
93
94 // String returns the generated JavaScript source code.
95 func (e *Emitter) String() string {
96 return e.buf.String()
97 }
98
99 // Reset clears the emitter buffer.
100 func (e *Emitter) Reset() {
101 e.buf.Reset()
102 e.indent = 0
103 }
104
105 // Len returns the current buffer length.
106 func (e *Emitter) Len() int {
107 return e.buf.Len()
108 }
109
110 // ImportStatement emits an ES module import.
111 func (e *Emitter) ImportStatement(names string, from string) {
112 e.Line("import %s from '%s';", names, from)
113 }
114
115 // ImportAll emits import * as X from '...'.
116 func (e *Emitter) ImportAll(alias string, from string) {
117 e.Line("import * as %s from '%s';", alias, from)
118 }
119
120 // ExportFunction emits an exported async or sync function declaration.
121 func (e *Emitter) ExportFunction(name string, params string, async bool) {
122 prefix := ""
123 if async {
124 prefix = "async "
125 }
126 e.Block("export %sfunction %s(%s)", prefix, name, params)
127 }
128
129 // Function emits a function declaration.
130 func (e *Emitter) Function(name string, params string, async bool) {
131 prefix := ""
132 if async {
133 prefix = "async "
134 }
135 e.Block("%sfunction %s(%s)", prefix, name, params)
136 }
137
138 // JsString returns a properly escaped JavaScript string literal.
139 func JsString(s string) string {
140 var b strings.Builder
141 b.WriteByte('\'')
142 for _, r := range s {
143 switch r {
144 case '\'':
145 b.WriteString("\\'")
146 case '\\':
147 b.WriteString("\\\\")
148 case '\n':
149 b.WriteString("\\n")
150 case '\r':
151 b.WriteString("\\r")
152 case '\t':
153 b.WriteString("\\t")
154 case '\x00':
155 b.WriteString("\\0")
156 default:
157 if r < 0x20 {
158 fmt.Fprintf(&b, "\\x%02x", r)
159 } else {
160 b.WriteRune(r)
161 }
162 }
163 }
164 b.WriteByte('\'')
165 return b.String()
166 }
167
168 // JsIdentifier converts a Go identifier to a valid JS identifier.
169 // Go allows unicode identifiers that JS also allows, but we need to handle
170 // cases like package paths becoming module names.
171 func JsIdentifier(name string) string {
172 name = strings.ReplaceAll(name, "#", "$")
173 name = strings.ReplaceAll(name, "/", "$")
174 name = strings.ReplaceAll(name, ".", "$")
175 name = strings.ReplaceAll(name, "-", "_")
176 if isJsReserved(name) {
177 return "$" + name
178 }
179 return name
180 }
181
182 // isJsReserved checks if a name is a JS reserved word.
183 func isJsReserved(name string) bool {
184 switch name {
185 case "break", "case", "catch", "continue", "debugger", "default",
186 "delete", "do", "else", "finally", "for", "function", "if",
187 "in", "instanceof", "new", "return", "switch", "this",
188 "throw", "try", "typeof", "var", "void", "while", "with",
189 "class", "const", "enum", "export", "extends", "import",
190 "super", "implements", "interface", "let", "package",
191 "private", "protected", "public", "static", "yield",
192 "await", "async":
193 return true
194 }
195 return false
196 }
197
198 // PackageModuleName converts a Go package import path to a JS module filename.
199 func PackageModuleName(pkgPath string) string {
200 name := strings.ReplaceAll(pkgPath, "/", "_")
201 name = strings.ReplaceAll(name, ".", "_")
202 name = strings.ReplaceAll(name, "-", "_")
203 return name + ".mjs"
204 }
205