goscript 0.0.58 → 0.0.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +40 -33
  2. package/compiler/analysis.go +184 -43
  3. package/compiler/assignment.go +163 -217
  4. package/compiler/compiler.go +35 -31
  5. package/compiler/composite-lit.go +233 -196
  6. package/compiler/constraint.go +88 -0
  7. package/compiler/decl.go +82 -24
  8. package/compiler/expr-call-async.go +20 -34
  9. package/compiler/expr-call-builtins.go +19 -0
  10. package/compiler/expr-call-helpers.go +0 -28
  11. package/compiler/expr-call-make.go +93 -343
  12. package/compiler/expr-call-type-conversion.go +221 -249
  13. package/compiler/expr-call.go +70 -69
  14. package/compiler/expr-selector.go +21 -24
  15. package/compiler/expr.go +3 -60
  16. package/compiler/protobuf.go +180 -36
  17. package/compiler/spec-value.go +132 -24
  18. package/compiler/spec.go +14 -55
  19. package/compiler/stmt-assign.go +338 -356
  20. package/compiler/stmt-range.go +4 -24
  21. package/compiler/stmt.go +92 -203
  22. package/compiler/type-utils.go +185 -0
  23. package/compiler/type.go +26 -80
  24. package/dist/gs/builtin/slice.d.ts +1 -1
  25. package/dist/gs/builtin/slice.js +3 -0
  26. package/dist/gs/builtin/slice.js.map +1 -1
  27. package/dist/gs/builtin/type.js +8 -2
  28. package/dist/gs/builtin/type.js.map +1 -1
  29. package/dist/gs/fmt/fmt.js +113 -16
  30. package/dist/gs/fmt/fmt.js.map +1 -1
  31. package/dist/gs/runtime/runtime.d.ts +1 -1
  32. package/dist/gs/runtime/runtime.js +1 -1
  33. package/dist/gs/slices/slices.d.ts +23 -0
  34. package/dist/gs/slices/slices.js +61 -0
  35. package/dist/gs/slices/slices.js.map +1 -1
  36. package/go.mod +10 -10
  37. package/go.sum +22 -14
  38. package/gs/builtin/slice.ts +5 -2
  39. package/gs/builtin/type.ts +13 -6
  40. package/gs/fmt/fmt.test.ts +176 -0
  41. package/gs/fmt/fmt.ts +109 -18
  42. package/gs/runtime/runtime.ts +1 -1
  43. package/gs/slices/slices.ts +68 -0
  44. package/package.json +3 -3
@@ -169,7 +169,7 @@ func (c *GoToTSCompiler) writeCallArguments(exp *ast.CallExpr) error {
169
169
  func (c *GoToTSCompiler) writeArgumentWithTypeHandling(arg ast.Expr, funcSig *types.Signature, argIndex int) error {
170
170
  if funcSig != nil && argIndex < funcSig.Params().Len() {
171
171
  paramType := funcSig.Params().At(argIndex).Type()
172
- isWrapper := c.analysis.IsNamedBasicType(paramType)
172
+ isWrapper := c.isWrapperType(paramType)
173
173
 
174
174
  if isWrapper {
175
175
  // For wrapper types (now type aliases), no auto-wrapping is needed
@@ -202,7 +202,52 @@ func (c *GoToTSCompiler) writeArgumentWithTypeHandling(arg ast.Expr, funcSig *ty
202
202
  return nil
203
203
  }
204
204
 
205
+ // resolveImportAlias returns the import alias for a given package
206
+ // This is the single source of truth for import alias resolution
207
+ func (c *GoToTSCompiler) resolveImportAlias(pkg *types.Package) (alias string, found bool) {
208
+ if c.analysis == nil || pkg == nil {
209
+ return "", false
210
+ }
211
+
212
+ pkgName := pkg.Name()
213
+
214
+ // Try to match by the actual package name
215
+ if _, exists := c.analysis.Imports[pkgName]; exists {
216
+ return pkgName, true
217
+ }
218
+
219
+ return "", false
220
+ }
221
+
222
+ // getQualifiedTypeName returns the qualified type name for a Named or Alias type
223
+ // Returns empty string if the type is not Named or Alias, or has no valid object
224
+ func (c *GoToTSCompiler) getQualifiedTypeName(t types.Type) string {
225
+ var obj *types.TypeName
226
+
227
+ if namedType, ok := t.(*types.Named); ok {
228
+ obj = namedType.Obj()
229
+ } else if aliasType, ok := t.(*types.Alias); ok {
230
+ obj = aliasType.Obj()
231
+ }
232
+
233
+ if obj == nil || obj.Pkg() == nil {
234
+ return ""
235
+ }
236
+
237
+ if obj.Pkg() != c.pkg.Types {
238
+ // Imported type
239
+ if alias, found := c.resolveImportAlias(obj.Pkg()); found {
240
+ return alias + "." + obj.Name()
241
+ }
242
+ return obj.Name()
243
+ }
244
+
245
+ // Local type
246
+ return obj.Name()
247
+ }
248
+
205
249
  // getImportAlias returns the import alias for a given package path
250
+ // Deprecated: use resolveImportAlias instead for better type safety
206
251
  func (c *GoToTSCompiler) getImportAlias(pkgPath string) string {
207
252
  if c.analysis == nil {
208
253
  return ""
@@ -234,7 +279,7 @@ func (c *GoToTSCompiler) getImportAlias(pkgPath string) string {
234
279
  func (c *GoToTSCompiler) writeAutoWrappedArgument(arg ast.Expr, expectedType types.Type) error {
235
280
  // For wrapper types (now type aliases), no auto-wrapping is needed
236
281
  // Just use type casting if the types don't match exactly
237
- if c.analysis.IsNamedBasicType(expectedType) {
282
+ if c.isWrapperType(expectedType) {
238
283
  argType := c.pkg.TypesInfo.TypeOf(arg)
239
284
 
240
285
  // Only add type casting if needed
@@ -272,73 +317,12 @@ func (c *GoToTSCompiler) writeWrapperTypeMethodCall(exp *ast.CallExpr, selectorE
272
317
  }
273
318
 
274
319
  // Check if this is a wrapper type using the analysis
275
- isWrapperType := c.analysis.IsNamedBasicType(baseType)
276
-
277
- // Special handling for type aliases to basic types that might have wrapper functions
278
- // Even if IsNamedBasicType returns false, we should check for known wrapper type patterns
279
- var typeName string
280
- if !isWrapperType {
281
- // Check if this is a type alias to a basic type that might have wrapper methods
282
- if aliasType, ok := baseType.(*types.Alias); ok {
283
- if aliasType.Obj() != nil && aliasType.Obj().Pkg() != nil {
284
- // Check if the underlying type is a basic type
285
- underlying := aliasType.Underlying()
286
- if _, isBasic := underlying.(*types.Basic); isBasic {
287
- // This is a type alias to a basic type - treat it as a potential wrapper type
288
- isWrapperType = true
289
- if aliasType.Obj().Pkg() != c.pkg.Types {
290
- // Imported type alias like os.FileMode
291
- if importAlias := c.getImportAlias(aliasType.Obj().Pkg().Path()); importAlias != "" {
292
- typeName = importAlias + "." + aliasType.Obj().Name()
293
- } else {
294
- typeName = aliasType.Obj().Name()
295
- }
296
- } else {
297
- // Local type alias
298
- typeName = aliasType.Obj().Name()
299
- }
300
- }
301
- }
302
- }
303
- }
304
-
305
- if !isWrapperType {
320
+ if !c.isWrapperType(baseType) {
306
321
  return false, nil
307
322
  }
308
323
 
309
- // Get the type name for the function call if not already set
310
- if typeName == "" {
311
- if namedType, ok := baseType.(*types.Named); ok {
312
- if obj := namedType.Obj(); obj != nil {
313
- if obj.Pkg() != nil && obj.Pkg() != c.pkg.Types {
314
- // Imported type like os.FileMode
315
- if importAlias := c.getImportAlias(obj.Pkg().Path()); importAlias != "" {
316
- typeName = importAlias + "." + obj.Name()
317
- } else {
318
- typeName = obj.Name()
319
- }
320
- } else {
321
- // Local type
322
- typeName = obj.Name()
323
- }
324
- }
325
- } else if aliasType, ok := baseType.(*types.Alias); ok {
326
- if obj := aliasType.Obj(); obj != nil {
327
- if obj.Pkg() != nil && obj.Pkg() != c.pkg.Types {
328
- // Imported type alias
329
- if importAlias := c.getImportAlias(obj.Pkg().Path()); importAlias != "" {
330
- typeName = importAlias + "." + obj.Name()
331
- } else {
332
- typeName = obj.Name()
333
- }
334
- } else {
335
- // Local type alias
336
- typeName = obj.Name()
337
- }
338
- }
339
- }
340
- }
341
-
324
+ // Get the qualified type name for the function call
325
+ typeName := c.getQualifiedTypeName(baseType)
342
326
  if typeName == "" {
343
327
  return false, nil
344
328
  }
@@ -369,9 +353,26 @@ func (c *GoToTSCompiler) writeWrapperTypeMethodCall(exp *ast.CallExpr, selectorE
369
353
  }
370
354
 
371
355
  if receiverNeedsVarRef {
372
- // For pointer receivers, we need to pass the VarRef
373
- // Convert p.field to p._fields.field
374
- if selExpr, ok := selectorExpr.X.(*ast.SelectorExpr); ok {
356
+ // For pointer receivers, we need to pass the VarRef (not the value)
357
+ // Check if the receiver expression is an identifier that's already a VarRef
358
+ if ident, ok := selectorExpr.X.(*ast.Ident); ok {
359
+ if obj := c.pkg.TypesInfo.ObjectOf(ident); obj != nil {
360
+ if c.analysis != nil && c.analysis.NeedsVarRef(obj) {
361
+ // This variable is already a VarRef, pass it directly
362
+ c.tsw.WriteLiterally(ident.Name)
363
+ } else {
364
+ // Not a VarRef, write the value expression
365
+ if err := c.WriteValueExpr(selectorExpr.X); err != nil {
366
+ return true, fmt.Errorf("failed to write wrapper type method receiver: %w", err)
367
+ }
368
+ }
369
+ } else {
370
+ if err := c.WriteValueExpr(selectorExpr.X); err != nil {
371
+ return true, fmt.Errorf("failed to write wrapper type method receiver: %w", err)
372
+ }
373
+ }
374
+ } else if selExpr, ok := selectorExpr.X.(*ast.SelectorExpr); ok {
375
+ // Convert p.field to p._fields.field for struct field access
375
376
  if baseIdent, ok := selExpr.X.(*ast.Ident); ok {
376
377
  c.tsw.WriteLiterally(baseIdent.Name)
377
378
  c.tsw.WriteLiterally("._fields.")
@@ -130,30 +130,9 @@ func (c *GoToTSCompiler) WriteSelectorExpr(exp *ast.SelectorExpr) error {
130
130
  // In Go, accessing fields or calling methods on nil pointers/interfaces panics, so we should throw in TypeScript
131
131
  baseType := c.pkg.TypesInfo.TypeOf(exp.X)
132
132
 
133
- // Special case: Check if this is a method receiver alias (e.g., `b` in `const b = this`)
134
- // In this case, we should use regular field access instead of pointer access
135
- isMethodReceiverAlias := false
136
- if baseIdent, ok := exp.X.(*ast.Ident); ok {
137
- // Check if this identifier refers to a method receiver
138
- // Method receivers are typically named variables that alias `this`
139
- if obj := c.pkg.TypesInfo.ObjectOf(baseIdent); obj != nil {
140
- if varObj, ok := obj.(*types.Var); ok {
141
- // Check if this is a local variable in a method context
142
- // Method receiver aliases are local variables with pointer types that represent `this`
143
- if _, isPtr := varObj.Type().(*types.Pointer); isPtr {
144
- // Additional check: only consider it a method receiver alias if the variable name
145
- // suggests it's an alias (single letter names are common for aliases like `b`, `r`, etc.)
146
- // and it's not a common variable name like `f` which is often used for regular variables
147
- varName := baseIdent.Name
148
- if len(varName) == 1 && varName != "f" {
149
- // This is likely a method receiver alias like `const b = this`
150
- // We should treat it as a regular object access, not a pointer access
151
- isMethodReceiverAlias = true
152
- }
153
- }
154
- }
155
- }
156
- }
133
+ // Check if this is a method receiver alias (e.g., `c` in `const c = this`)
134
+ // Use proper AST/types analysis instead of heuristics
135
+ isMethodReceiverAlias := c.isReceiverAlias(exp.X)
157
136
 
158
137
  if baseType != nil && !isMethodReceiverAlias {
159
138
  // Check if the base is a pointer type
@@ -274,3 +253,21 @@ func (c *GoToTSCompiler) writeMethodValue(exp *ast.SelectorExpr, selection *type
274
253
 
275
254
  return nil
276
255
  }
256
+
257
+ // isReceiverAlias detects if a variable is likely a receiver alias (e.g., const c = this)
258
+ // These variables are guaranteed to be non-null and don't need null assertions
259
+ func (c *GoToTSCompiler) isReceiverAlias(expr ast.Expr) bool {
260
+ // Only check identifiers - receiver aliases are always identifiers
261
+ ident, ok := expr.(*ast.Ident)
262
+ if !ok {
263
+ return false
264
+ }
265
+
266
+ // If analysis marked this identifier with a mapping, treat it as a receiver alias
267
+ if mapped := c.analysis.GetIdentifierMapping(ident); mapped != "" {
268
+ return true
269
+ }
270
+
271
+ // Fallback conservative behavior
272
+ return false
273
+ }
package/compiler/expr.go CHANGED
@@ -19,7 +19,7 @@ func (c *GoToTSCompiler) WriteIndexExpr(exp *ast.IndexExpr) error {
19
19
  // Check if the index is a type expression (identifier that refers to a type)
20
20
  if indexIdent, isIdent := exp.Index.(*ast.Ident); isIdent {
21
21
  // Check if this identifier refers to a type
22
- if obj := c.pkg.TypesInfo.Uses[indexIdent]; obj != nil {
22
+ if obj := c.objectOfIdent(indexIdent); obj != nil {
23
23
  if _, isTypeName := obj.(*types.TypeName); isTypeName {
24
24
  // This is a generic function instantiation: f[T] -> f<T>
25
25
  if err := c.WriteValueExpr(exp.X); err != nil {
@@ -57,7 +57,7 @@ func (c *GoToTSCompiler) WriteIndexExpr(exp *ast.IndexExpr) error {
57
57
  }
58
58
 
59
59
  // Check if it's a string type
60
- if basicType, isBasic := underlyingType.(*types.Basic); isBasic && (basicType.Info()&types.IsString) != 0 {
60
+ if c.isStringType(tv.Type) {
61
61
  c.tsw.WriteLiterally("$.indexString(")
62
62
  if err := c.WriteValueExpr(exp.X); err != nil {
63
63
  return err
@@ -279,21 +279,6 @@ func (c *GoToTSCompiler) getFinalUnderlyingType(t types.Type) (types.Type, bool)
279
279
  }
280
280
  }
281
281
 
282
- // isNamedNumericType checks if a given type is a named type with an underlying numeric type.
283
- func (c *GoToTSCompiler) isNamedNumericType(t types.Type) bool {
284
- finalType, wasNamed := c.getFinalUnderlyingType(t)
285
- if !wasNamed {
286
- return false
287
- }
288
-
289
- if basicType, isBasic := finalType.(*types.Basic); isBasic {
290
- info := basicType.Info()
291
- return (info&types.IsInteger) != 0 || (info&types.IsFloat) != 0
292
- }
293
-
294
- return false
295
- }
296
-
297
282
  // WriteBinaryExpr translates a Go binary expression (`ast.BinaryExpr`) into its
298
283
  // TypeScript equivalent.
299
284
  // It handles several cases:
@@ -628,9 +613,7 @@ func (c *GoToTSCompiler) WriteSliceExpr(exp *ast.SliceExpr) error {
628
613
  isString := false
629
614
  isTypeParam := false
630
615
  if tv != nil {
631
- if basicType, isBasic := tv.Underlying().(*types.Basic); isBasic && (basicType.Info()&types.IsString) != 0 {
632
- isString = true
633
- }
616
+ isString = c.isStringType(tv)
634
617
  if _, isTP := tv.(*types.TypeParam); isTP {
635
618
  isTypeParam = true
636
619
  }
@@ -755,43 +738,3 @@ func (c *GoToTSCompiler) WriteKeyValueExpr(exp *ast.KeyValueExpr) error {
755
738
  }
756
739
  return nil
757
740
  }
758
-
759
- // hasMapConstraint checks if an interface constraint includes map types
760
- // For constraints like ~map[K]V, this returns true
761
- func hasMapConstraint(iface *types.Interface) bool {
762
- // Check if the interface has type terms that include map types
763
- for i := 0; i < iface.NumEmbeddeds(); i++ {
764
- embedded := iface.EmbeddedType(i)
765
- if union, ok := embedded.(*types.Union); ok {
766
- for j := 0; j < union.Len(); j++ {
767
- term := union.Term(j)
768
- if _, isMap := term.Type().Underlying().(*types.Map); isMap {
769
- return true
770
- }
771
- }
772
- } else if _, isMap := embedded.Underlying().(*types.Map); isMap {
773
- return true
774
- }
775
- }
776
- return false
777
- }
778
-
779
- // getMapValueTypeFromConstraint extracts the value type from a map constraint
780
- // For constraints like ~map[K]V, this returns V
781
- func getMapValueTypeFromConstraint(iface *types.Interface) types.Type {
782
- // Check if the interface has type terms that include map types
783
- for i := 0; i < iface.NumEmbeddeds(); i++ {
784
- embedded := iface.EmbeddedType(i)
785
- if union, ok := embedded.(*types.Union); ok {
786
- for j := 0; j < union.Len(); j++ {
787
- term := union.Term(j)
788
- if mapType, isMap := term.Type().Underlying().(*types.Map); isMap {
789
- return mapType.Elem()
790
- }
791
- }
792
- } else if mapType, isMap := embedded.Underlying().(*types.Map); isMap {
793
- return mapType.Elem()
794
- }
795
- }
796
- return nil
797
- }
@@ -12,29 +12,37 @@ import (
12
12
  "golang.org/x/tools/go/packages"
13
13
  )
14
14
 
15
- // isProtobufType checks if a given type is a protobuf type by examining its package and name
16
- func (c *GoToTSCompiler) isProtobufType(typ types.Type) bool {
17
- if namedType, ok := typ.(*types.Named); ok {
18
- obj := namedType.Obj()
19
- if obj != nil && obj.Pkg() != nil {
20
- // Check if the type is defined in the current package and has a corresponding .pb.ts file
21
- if obj.Pkg() == c.pkg.Types {
22
- // Check if there's a .pb.ts file in the package that exports this type
23
- // For now, we'll use a simple heuristic: if the type name ends with "Msg"
24
- // and there's a .pb.ts file in the package, assume it's a protobuf type
25
- typeName := obj.Name()
26
- if strings.HasSuffix(typeName, "Msg") {
27
- // Check if there are any .pb.ts files in this package
28
- for _, fileName := range c.pkg.CompiledGoFiles {
29
- if strings.HasSuffix(fileName, ".pb.go") {
30
- return true
31
- }
32
- }
33
- }
34
- }
15
+ // getProtobufMessageInterface attempts to find the protobuf-go-lite Message interface
16
+ // from the loaded packages in analysis. Returns nil if not found.
17
+ func (c *GoToTSCompiler) getProtobufMessageInterface() *types.Interface {
18
+ if c.analysis == nil || c.analysis.AllPackages == nil {
19
+ return nil
20
+ }
21
+ pkg := c.analysis.AllPackages["github.com/aperturerobotics/protobuf-go-lite"]
22
+ if pkg == nil || pkg.Types == nil {
23
+ return nil
24
+ }
25
+ obj := pkg.Types.Scope().Lookup("Message")
26
+ if obj == nil {
27
+ return nil
28
+ }
29
+ if tn, ok := obj.(*types.TypeName); ok {
30
+ if iface, ok := tn.Type().Underlying().(*types.Interface); ok {
31
+ return iface
35
32
  }
36
33
  }
37
- return false
34
+ return nil
35
+ }
36
+
37
+ // typeHasMethods returns true if the given type's method set contains all the specified names.
38
+ func (c *GoToTSCompiler) typeHasMethods(t types.Type, names ...string) bool {
39
+ mset := types.NewMethodSet(t)
40
+ for _, name := range names {
41
+ if mset.Lookup(nil, name) == nil {
42
+ return false
43
+ }
44
+ }
45
+ return true
38
46
  }
39
47
 
40
48
  // convertProtobufFieldName converts Go PascalCase field names to TypeScript camelCase
@@ -44,9 +52,11 @@ func (c *GoToTSCompiler) convertProtobufFieldName(goFieldName string) string {
44
52
  return goFieldName
45
53
  }
46
54
 
47
- // Convert first character to lowercase
55
+ // Convert first character to lowercase if ASCII uppercase
48
56
  runes := []rune(goFieldName)
49
- runes[0] = runes[0] + ('a' - 'A')
57
+ if runes[0] >= 'A' && runes[0] <= 'Z' {
58
+ runes[0] = runes[0] + ('a' - 'A')
59
+ }
50
60
  return string(runes)
51
61
  }
52
62
 
@@ -119,18 +129,104 @@ func (c *PackageCompiler) copyProtobufTSFile(sourcePath, fileName string) error
119
129
 
120
130
  // writeProtobufExports writes exports for a protobuf file to the index.ts file
121
131
  func (c *PackageCompiler) writeProtobufExports(indexFile *os.File, fileName string) error {
122
- // For protobuf files, we know they typically export message types
123
- // For now, we'll use a simple heuristic: export all types that end with "Msg"
124
- // In a full implementation, we would parse the .pb.ts file to extract actual exports
125
-
126
- // For the protobuf_lite_ts test, we know it exports ExampleMsg
127
- // This is a simplified approach - in production, we'd parse the .pb.ts file
128
- exportLine := fmt.Sprintf("export { ExampleMsg, protobufPackage } from \"./%s.js\"\n", fileName)
129
- if _, err := indexFile.WriteString(exportLine); err != nil {
132
+ // For protobuf files, try to parse the copied .pb.ts file in the output directory
133
+ // to discover exported symbols and re-export them from the index. This avoids
134
+ // hard-coding names like ExampleMsg and protobufPackage.
135
+
136
+ pbTsPath := filepath.Join(c.outputPath, fileName+".ts")
137
+ content, err := os.ReadFile(pbTsPath)
138
+ if err != nil {
130
139
  return err
131
140
  }
132
141
 
133
- return nil
142
+ // Very simple export discovery: capture names from
143
+ // - export const Name
144
+ // - export interface Name
145
+ // - export class Name
146
+ // - export function Name
147
+ // We avoid type-only exports for now.
148
+ var exports []string
149
+ lines := strings.Split(string(content), "\n")
150
+ for _, ln := range lines {
151
+ l := strings.TrimSpace(ln)
152
+ if strings.HasPrefix(l, "export const ") {
153
+ rest := strings.TrimPrefix(l, "export const ")
154
+ name := takeIdent(rest)
155
+ if name != "" {
156
+ exports = append(exports, name)
157
+ }
158
+ continue
159
+ }
160
+ if strings.HasPrefix(l, "export interface ") {
161
+ rest := strings.TrimPrefix(l, "export interface ")
162
+ name := takeIdent(rest)
163
+ if name != "" {
164
+ exports = append(exports, name)
165
+ }
166
+ continue
167
+ }
168
+ if strings.HasPrefix(l, "export class ") {
169
+ rest := strings.TrimPrefix(l, "export class ")
170
+ name := takeIdent(rest)
171
+ if name != "" {
172
+ exports = append(exports, name)
173
+ }
174
+ continue
175
+ }
176
+ if strings.HasPrefix(l, "export function ") {
177
+ rest := strings.TrimPrefix(l, "export function ")
178
+ name := takeIdent(rest)
179
+ if name != "" {
180
+ exports = append(exports, name)
181
+ }
182
+ continue
183
+ }
184
+ }
185
+
186
+ // If nothing found, fallback to default
187
+ if len(exports) == 0 {
188
+ return fmt.Errorf("no exported symbols discovered in %s.ts while generating protobuf exports", fileName)
189
+ }
190
+
191
+ // Deduplicate while preserving order
192
+ seen := map[string]bool{}
193
+ uniq := make([]string, 0, len(exports))
194
+ for _, n := range exports {
195
+ if !seen[n] {
196
+ seen[n] = true
197
+ uniq = append(uniq, n)
198
+ }
199
+ }
200
+
201
+ exportLine := fmt.Sprintf("export { %s } from \"./%s.js\"\n", strings.Join(uniq, ", "), fileName)
202
+ _, err = indexFile.WriteString(exportLine)
203
+ return err
204
+ }
205
+
206
+ // takeIdent extracts a leading identifier token from the beginning of s.
207
+ // Returns an empty string if no valid identifier is found.
208
+ func takeIdent(s string) string {
209
+ s = strings.TrimSpace(s)
210
+ if s == "" {
211
+ return ""
212
+ }
213
+ // Identifier: letter or _ followed by letters, digits, or _
214
+ var b strings.Builder
215
+ for i, r := range s {
216
+ if i == 0 {
217
+ if !(r == '_' || r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z') {
218
+ break
219
+ }
220
+ b.WriteRune(r)
221
+ continue
222
+ }
223
+ if r == '_' || r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' || r >= '0' && r <= '9' {
224
+ b.WriteRune(r)
225
+ continue
226
+ }
227
+ break
228
+ }
229
+ return b.String()
134
230
  }
135
231
 
136
232
  // addProtobufImports adds imports for protobuf types when .pb.ts files are present in the package
@@ -146,11 +242,59 @@ func (c *FileCompiler) addProtobufImports() error {
146
242
  pbTsPath := filepath.Join(packageDir, pbTsFileName)
147
243
 
148
244
  if _, err := os.Stat(pbTsPath); err == nil {
149
- // .pb.ts file exists, add imports for protobuf types
245
+ // .pb.ts file exists, parse it for exports and add imports accordingly
150
246
  pbBaseName := strings.TrimSuffix(baseFileName, ".pb.go")
151
- c.codeWriter.WriteLinef("import { ExampleMsg } from \"./%s.pb.js\";", pbBaseName)
152
- // Note: This is a simplified approach - in a full implementation,
153
- // we would parse the .pb.ts file to extract all exported types
247
+
248
+ content, rerr := os.ReadFile(pbTsPath)
249
+ if rerr != nil {
250
+ return fmt.Errorf("failed to read %s for protobuf imports: %w", pbTsPath, rerr)
251
+ }
252
+
253
+ // Discover exported identifiers (const/interface/class/function)
254
+ var exports []string
255
+ for _, ln := range strings.Split(string(content), "\n") {
256
+ l := strings.TrimSpace(ln)
257
+ if strings.HasPrefix(l, "export const ") {
258
+ if name := takeIdent(strings.TrimPrefix(l, "export const ")); name != "" {
259
+ exports = append(exports, name)
260
+ }
261
+ continue
262
+ }
263
+ if strings.HasPrefix(l, "export interface ") {
264
+ if name := takeIdent(strings.TrimPrefix(l, "export interface ")); name != "" {
265
+ exports = append(exports, name)
266
+ }
267
+ continue
268
+ }
269
+ if strings.HasPrefix(l, "export class ") {
270
+ if name := takeIdent(strings.TrimPrefix(l, "export class ")); name != "" {
271
+ exports = append(exports, name)
272
+ }
273
+ continue
274
+ }
275
+ if strings.HasPrefix(l, "export function ") {
276
+ if name := takeIdent(strings.TrimPrefix(l, "export function ")); name != "" {
277
+ exports = append(exports, name)
278
+ }
279
+ continue
280
+ }
281
+ }
282
+
283
+ if len(exports) == 0 {
284
+ return fmt.Errorf("no exported symbols discovered in %s for protobuf imports", pbTsPath)
285
+ }
286
+
287
+ // Deduplicate
288
+ seen := map[string]bool{}
289
+ uniq := make([]string, 0, len(exports))
290
+ for _, n := range exports {
291
+ if !seen[n] {
292
+ seen[n] = true
293
+ uniq = append(uniq, n)
294
+ }
295
+ }
296
+
297
+ c.codeWriter.WriteLinef("import { %s } from \"./%s.pb.js\";", strings.Join(uniq, ", "), pbBaseName)
154
298
  break
155
299
  }
156
300
  }