├── test ├── package.json ├── htmldocs │ ├── .nojekyll │ ├── assets │ │ └── highlight.css │ ├── types │ │ ├── _TygojaAny.html │ │ ├── _TygojaDict.html │ │ └── a._subOLPog.html │ ├── functions │ │ └── _app.html │ ├── modules.html │ ├── modules │ │ ├── c.html │ │ └── b.html │ └── interfaces │ │ ├── a.Empty.html │ │ ├── c.Handler.html │ │ ├── b.Func1.html │ │ ├── b.Func7.html │ │ ├── b.Func8.html │ │ ├── b.Func2.html │ │ ├── b.Func9.html │ │ ├── b.Func4.html │ │ ├── b.Func5.html │ │ └── a.Handler.html ├── tsconfig.json ├── a │ ├── interfaces.go │ ├── structs.go │ └── vars.go ├── main.go ├── b │ └── functions.go ├── c │ └── c.go └── package-lock.json ├── go.mod ├── .gitignore ├── go.sum ├── random.go ├── iota.go ├── LICENSE ├── write_comment.go ├── config.go ├── package_generator.go ├── README.md └── tygoja.go /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "generate": "npx typedoc" 4 | }, 5 | "devDependencies": { 6 | "typedoc": "^0.24.8" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/htmldocs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "typedocOptions": { 3 | "entryPoints": ["types.d.ts"], 4 | "skipErrorChecking": true, 5 | "out": "htmldocs" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pocketbase/tygoja 2 | 3 | go 1.22.0 4 | 5 | require golang.org/x/tools v0.26.0 6 | 7 | require ( 8 | golang.org/x/mod v0.21.0 // indirect 9 | golang.org/x/sync v0.8.0 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode/ 2 | .idea 3 | 4 | .DS_Store 5 | 6 | # tests coverage 7 | coverage.out 8 | 9 | # test data artifacts 10 | test/node_modules/ 11 | test/output.json 12 | 13 | # plaintask todo files 14 | *.todo 15 | 16 | # generated markdown previews 17 | README.html 18 | CHANGELOG.html 19 | LICENSE.html 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= 2 | golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 3 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 4 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 5 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 6 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 7 | -------------------------------------------------------------------------------- /test/a/interfaces.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | import "time" 4 | 5 | type Empty interface{} 6 | 7 | // unexported interface 8 | type interfaceA[T any] interface { 9 | // some comment 10 | unexportedFunc() 11 | 12 | // some comment above the function 13 | Method0() 14 | 15 | Method1() string // inline comment 16 | 17 | // multi 18 | // line 19 | // comment 20 | Method2(argA, argB string) (T, int) 21 | 22 | Method3(argA int, argB ...string) (T, []string, error) 23 | } 24 | 25 | // multi 26 | // line 27 | // comment 28 | type InterfaceB interface { 29 | Empty 30 | interfaceA[int] 31 | 32 | // "replace" Method0 from interfaceA 33 | Method0() 34 | 35 | CustomMethod() time.Time 36 | } 37 | -------------------------------------------------------------------------------- /random.go: -------------------------------------------------------------------------------- 1 | package tygoja 2 | 3 | import ( 4 | mathRand "math/rand" 5 | "time" 6 | ) 7 | 8 | const defaultRandomAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 9 | 10 | func init() { 11 | mathRand.Seed(time.Now().UnixNano()) 12 | } 13 | 14 | // PseudorandomString generates a pseudorandom string from the default 15 | // alphabet with the specified length. 16 | func PseudorandomString(length int) string { 17 | return PseudorandomStringWithAlphabet(length, defaultRandomAlphabet) 18 | } 19 | 20 | // PseudorandomStringWithAlphabet generates a pseudorandom string 21 | // with the specified length and characters set. 22 | func PseudorandomStringWithAlphabet(length int, alphabet string) string { 23 | b := make([]byte, length) 24 | max := len(alphabet) 25 | 26 | for i := range b { 27 | b[i] = alphabet[mathRand.Intn(max)] 28 | } 29 | 30 | return string(b) 31 | } 32 | -------------------------------------------------------------------------------- /test/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/pocketbase/tygoja" 8 | ) 9 | 10 | func main() { 11 | gen := tygoja.New(tygoja.Config{ 12 | Packages: map[string][]string{ 13 | "github.com/pocketbase/tygoja/test/a": {"*"}, 14 | "github.com/pocketbase/tygoja/test/b": {"*"}, 15 | "github.com/pocketbase/tygoja/test/c": {"Example2", "Handler"}, 16 | }, 17 | Heading: `declare var $app: c.Handler;`, 18 | WithPackageFunctions: true, 19 | // enable if you want to be able to import them 20 | // StartModifier: "export", 21 | }) 22 | 23 | result, err := gen.Generate() 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | 28 | if err := os.WriteFile("./types.d.ts", []byte(result), 0644); err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | // run `npx typedoc` to generate HTML docs from the above declarations 33 | } 34 | -------------------------------------------------------------------------------- /test/a/structs.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type unexported struct { 4 | field0 string 5 | Field1 string 6 | } 7 | 8 | // structA comment 9 | type structA struct { 10 | Field1 string // after 11 | 12 | // multi 13 | // line 14 | // comment 15 | // with union type 16 | Field2 []byte 17 | } 18 | 19 | func (s structA) method0() {} 20 | 21 | // method comment 22 | func (s structA) Method1(arg1 int) {} 23 | 24 | func (s *structA) Method2(arg1 int, arg2 ...string) {} // after 25 | 26 | // structB comment 27 | type StructB[T any] struct { 28 | *unexported 29 | structA 30 | 31 | Field3 T 32 | } 33 | 34 | // StructB.Method3 comment 35 | func (s *StructB[T]) Method3(arg1 int) (a int, b string, c error) { 36 | return 37 | } 38 | 39 | // structC with multiple mixed generic types 40 | type StructC[A string, B, C any] struct { 41 | Field4 A 42 | Field5 B 43 | Field6 C 44 | } 45 | 46 | // StructC.Method4 comment 47 | func (s *StructC[A, B, C]) Method4(arg1 A) (a B, b C, c error) { 48 | return 49 | } 50 | -------------------------------------------------------------------------------- /iota.go: -------------------------------------------------------------------------------- 1 | package tygoja 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | func isProbablyIotaType(groupType string) bool { 10 | groupType = strings.Trim(groupType, "()") 11 | return groupType == "iota" || strings.HasPrefix(groupType, "iota +") || strings.HasSuffix(groupType, "+ iota") 12 | } 13 | 14 | func basicIotaOffsetValueParse(groupType string) (int, error) { 15 | if !isProbablyIotaType(groupType) { 16 | panic("can't parse non-iota type") 17 | } 18 | 19 | groupType = strings.Trim(groupType, "()") 20 | if groupType == "iota" { 21 | return 0, nil 22 | } 23 | parts := strings.Split(groupType, " + ") 24 | 25 | var numPart string 26 | if parts[0] == "iota" { 27 | numPart = parts[1] 28 | } else { 29 | numPart = parts[0] 30 | } 31 | 32 | addValue, err := strconv.ParseInt(numPart, 10, 64) 33 | if err != nil { 34 | return 0, fmt.Errorf("Failed to guesstimate initial iota value for \"%s\": %w", groupType, err) 35 | } 36 | 37 | return int(addValue), nil 38 | } 39 | -------------------------------------------------------------------------------- /test/b/functions.go: -------------------------------------------------------------------------------- 1 | // package b 2 | package b 3 | 4 | func func0() {} 5 | 6 | // single comment 7 | func Func1() {} 8 | 9 | // multi 10 | // line 11 | // comment 12 | func Func2[T any](arg1 int) (a T, b error) { 13 | return 14 | } 15 | 16 | // function with multiple generic types 17 | func Func3[A string, B, C any](arg1 A, arg2 B, arg3 int) (a A, b C) { 18 | return 19 | } 20 | 21 | // function that returns a function 22 | func Func4(arg1 int) (a func() int) { 23 | return a 24 | } 25 | 26 | // function with ommited argument types 27 | func Func5(arg0 string, arg1, arg2 int) { 28 | } 29 | 30 | // function with reserved argument name and variadic type 31 | func Func6(catch string, optional ...string) { 32 | } 33 | 34 | // function with ommited argument names 35 | func Func7(string, int, ...bool) { 36 | } 37 | 38 | // function with named return values 39 | func Func8() (b int, c string) { 40 | return 41 | } 42 | 43 | // function with shortened return values 44 | func Func9() (b, c string) { 45 | return 46 | } 47 | 48 | // function with named and shortened return values 49 | func Func10() (a int, b, c string) { 50 | return 51 | } 52 | -------------------------------------------------------------------------------- /test/c/c.go: -------------------------------------------------------------------------------- 1 | package c 2 | 3 | import "time" 4 | 5 | // func type comment 6 | type Handler func() string // after 7 | 8 | type Raw []byte 9 | 10 | type Example1 struct { 11 | Name string 12 | } 13 | 14 | // Example: 15 | // 16 | // Some 17 | // code 18 | // sample 19 | type Example2 struct { 20 | Title string 21 | Json Raw 22 | Bytes []byte // should be union 23 | } 24 | 25 | func (e *Example1) DemoEx1() string { 26 | return "" 27 | } 28 | 29 | func (e *Example2) DemoEx2() time.Time { 30 | return time.Time{} 31 | } 32 | 33 | // Pointer as argument vs return type 34 | func (e *Example2) DemoEx3(arg *Example1) (*Example1, error) { 35 | return nil, nil 36 | } 37 | 38 | // ommited types 39 | func (e *Example2) DemoEx4(n1, n2, n3 string) { 40 | } 41 | 42 | // ommited names 43 | func (e *Example2) DemoEx5(string, int) { 44 | } 45 | 46 | // named return values 47 | func (e *Example2) DemoEx6() (b int, c string) { 48 | return 49 | } 50 | 51 | // shortened return values 52 | func (e *Example2) DemoEx7() (b, c string) { 53 | return 54 | } 55 | 56 | // named and shortened return values 57 | func (e *Example2) DemoEx8() (a int, b, c string) { 58 | return 59 | } 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Guido Zuidhof 4 | Copyright (c) 2023-present Gani Georgiev 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /write_comment.go: -------------------------------------------------------------------------------- 1 | package tygoja 2 | 3 | import ( 4 | "go/ast" 5 | "strings" 6 | ) 7 | 8 | func (g *PackageGenerator) writeCommentGroup(s *strings.Builder, f *ast.CommentGroup, depth int) { 9 | if f == nil { 10 | return 11 | } 12 | 13 | docLines := strings.Split(f.Text(), "\n") 14 | 15 | g.writeIndent(s, depth) 16 | s.WriteString("/**\n") 17 | 18 | lastLineIdx := len(docLines) - 1 19 | 20 | var isCodeBlock bool 21 | 22 | emptySB := new(strings.Builder) 23 | 24 | for i, c := range docLines { 25 | isEndLine := i == lastLineIdx 26 | isEmpty := len(strings.TrimSpace(c)) == 0 27 | isIndented := strings.HasPrefix(c, "\t") || strings.HasPrefix(c, " ") 28 | 29 | // end code block 30 | if isCodeBlock && (isEndLine || (!isIndented && !isEmpty)) { 31 | g.writeIndent(s, depth) 32 | s.WriteString(" * ```\n") 33 | isCodeBlock = false 34 | } 35 | 36 | // accumulate empty comment lines 37 | // (this is done to properly enclose code blocks with new lines) 38 | if isEmpty { 39 | g.writeIndent(emptySB, depth) 40 | emptySB.WriteString(" * \n") 41 | } else { 42 | // write all empty lines 43 | s.WriteString(emptySB.String()) 44 | emptySB.Reset() 45 | } 46 | 47 | // start code block 48 | if isIndented && !isCodeBlock && !isEndLine { 49 | g.writeIndent(s, depth) 50 | s.WriteString(" * ```\n") 51 | isCodeBlock = true 52 | } 53 | 54 | // write comment line 55 | if !isEmpty { 56 | g.writeIndent(s, depth) 57 | s.WriteString(" * ") 58 | c = strings.ReplaceAll(c, "*/", "*\\/") // An edge case: a // comment can contain */ 59 | s.WriteString(c) 60 | s.WriteByte('\n') 61 | } 62 | } 63 | 64 | g.writeIndent(s, depth) 65 | s.WriteString(" */\n") 66 | } 67 | -------------------------------------------------------------------------------- /test/a/vars.go: -------------------------------------------------------------------------------- 1 | // package a docs 2 | // lorem ipsum dolor... 3 | package a 4 | 5 | import "time" 6 | 7 | // ------------------------------------------------------------------- 8 | // variables (note: currently are ignored) 9 | // ------------------------------------------------------------------- 10 | 11 | var unexportedVar int = 123 12 | 13 | // comment 14 | var VarA = 123 // after 15 | 16 | var VarB any = "test" 17 | 18 | // external package type 19 | var VarC time.Time = time.Now() 20 | 21 | // chan 22 | var VarD = make(chan int) 23 | 24 | // composite 25 | var VarE = map[string]func(){"test": func() {}} 26 | 27 | // ------------------------------------------------------------------- 28 | // constants 29 | // ------------------------------------------------------------------- 30 | 31 | const unexportedConst = "123" 32 | 33 | // comment 34 | const ConstA string = "test" // after 35 | 36 | // multi 37 | // line 38 | // comment 39 | const ConstB = 123 40 | 41 | // some generic group comment 42 | const ( 43 | ConstC0 = iota 44 | ConstC1 // after 45 | ConstC2 46 | ) 47 | 48 | // ------------------------------------------------------------------- 49 | // type alias with methods 50 | // ------------------------------------------------------------------- 51 | 52 | // type comment 53 | type SliceAlias[T any] []T // after 54 | 55 | // func comment 56 | func (s SliceAlias[T]) funcA() { 57 | } 58 | 59 | // multi 60 | // line 61 | // comment 62 | func (s SliceAlias[T]) funcB(argA, argB int) { 63 | } 64 | 65 | func (s SliceAlias[T]) funcC(argA int, argB ...string) (a T, b int, c error) { 66 | return 67 | } 68 | 69 | // ------------------------------------------------------------------- 70 | // function type 71 | // ------------------------------------------------------------------- 72 | 73 | // multi 74 | // line 75 | // comment 76 | type Handler[T comparable] func() (T, int) // after 77 | -------------------------------------------------------------------------------- /test/htmldocs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #0000FF; 3 | --dark-hl-0: #569CD6; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #A31515; 7 | --dark-hl-2: #CE9178; 8 | --light-hl-3: #795E26; 9 | --dark-hl-3: #DCDCAA; 10 | --light-hl-4: #001080; 11 | --dark-hl-4: #9CDCFE; 12 | --light-hl-5: #267F99; 13 | --dark-hl-5: #4EC9B0; 14 | --light-hl-6: #AF00DB; 15 | --dark-hl-6: #C586C0; 16 | --light-hl-7: #098658; 17 | --dark-hl-7: #B5CEA8; 18 | --light-hl-8: #000000; 19 | --dark-hl-8: #C8C8C8; 20 | --light-code-background: #FFFFFF; 21 | --dark-code-background: #1E1E1E; 22 | } 23 | 24 | @media (prefers-color-scheme: light) { :root { 25 | --hl-0: var(--light-hl-0); 26 | --hl-1: var(--light-hl-1); 27 | --hl-2: var(--light-hl-2); 28 | --hl-3: var(--light-hl-3); 29 | --hl-4: var(--light-hl-4); 30 | --hl-5: var(--light-hl-5); 31 | --hl-6: var(--light-hl-6); 32 | --hl-7: var(--light-hl-7); 33 | --hl-8: var(--light-hl-8); 34 | --code-background: var(--light-code-background); 35 | } } 36 | 37 | @media (prefers-color-scheme: dark) { :root { 38 | --hl-0: var(--dark-hl-0); 39 | --hl-1: var(--dark-hl-1); 40 | --hl-2: var(--dark-hl-2); 41 | --hl-3: var(--dark-hl-3); 42 | --hl-4: var(--dark-hl-4); 43 | --hl-5: var(--dark-hl-5); 44 | --hl-6: var(--dark-hl-6); 45 | --hl-7: var(--dark-hl-7); 46 | --hl-8: var(--dark-hl-8); 47 | --code-background: var(--dark-code-background); 48 | } } 49 | 50 | :root[data-theme='light'] { 51 | --hl-0: var(--light-hl-0); 52 | --hl-1: var(--light-hl-1); 53 | --hl-2: var(--light-hl-2); 54 | --hl-3: var(--light-hl-3); 55 | --hl-4: var(--light-hl-4); 56 | --hl-5: var(--light-hl-5); 57 | --hl-6: var(--light-hl-6); 58 | --hl-7: var(--light-hl-7); 59 | --hl-8: var(--light-hl-8); 60 | --code-background: var(--light-code-background); 61 | } 62 | 63 | :root[data-theme='dark'] { 64 | --hl-0: var(--dark-hl-0); 65 | --hl-1: var(--dark-hl-1); 66 | --hl-2: var(--dark-hl-2); 67 | --hl-3: var(--dark-hl-3); 68 | --hl-4: var(--dark-hl-4); 69 | --hl-5: var(--dark-hl-5); 70 | --hl-6: var(--dark-hl-6); 71 | --hl-7: var(--dark-hl-7); 72 | --hl-8: var(--dark-hl-8); 73 | --code-background: var(--dark-code-background); 74 | } 75 | 76 | .hl-0 { color: var(--hl-0); } 77 | .hl-1 { color: var(--hl-1); } 78 | .hl-2 { color: var(--hl-2); } 79 | .hl-3 { color: var(--hl-3); } 80 | .hl-4 { color: var(--hl-4); } 81 | .hl-5 { color: var(--hl-5); } 82 | .hl-6 { color: var(--hl-6); } 83 | .hl-7 { color: var(--hl-7); } 84 | .hl-8 { color: var(--hl-8); } 85 | pre, code { background: var(--code-background); } 86 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package tygoja 2 | 3 | const ( 4 | defaultIndent = " " 5 | 6 | // custom base types that every package has access to 7 | BaseTypeDict = "_TygojaDict" // Record type alternative as a more generic map-like type 8 | BaseTypeAny = "_TygojaAny" // any type alias to allow easier extends generation 9 | ) 10 | 11 | // FieldNameFormatterFunc defines a function for formatting a field name. 12 | type FieldNameFormatterFunc func(string) string 13 | 14 | // MethodNameFormatterFunc defines a function for formatting a method name. 15 | type MethodNameFormatterFunc func(string) string 16 | 17 | type Config struct { 18 | // Packages is a list of package paths just like you would import them in Go. 19 | // Use "*" to generate all package types. 20 | // 21 | // Example: 22 | // 23 | // Packages: map[string][]string{ 24 | // "time": {"Time"}, 25 | // "github.com/pocketbase/pocketbase/core": {"*"}, 26 | // } 27 | Packages map[string][]string 28 | 29 | // Heading specifies a content that will be put at the top of the output declaration file. 30 | // 31 | // You would generally use this to import custom types or some custom TS declarations. 32 | Heading string 33 | 34 | // TypeMappings specifies custom type translations. 35 | // 36 | // Useful for for mapping 3rd party package types, eg "unsafe.Pointer" => "CustomType". 37 | // 38 | // Be default unrecognized types will be recursively generated by 39 | // traversing their import package (when possible). 40 | TypeMappings map[string]string 41 | 42 | // WithConstants indicates whether to generate types for constants 43 | // ("false" by default). 44 | WithConstants bool 45 | 46 | // WithPackageFunctions indicates whether to generate types 47 | // for package level functions ("false" by default). 48 | WithPackageFunctions bool 49 | 50 | // FieldNameFormatter allows specifying a custom struct field name formatter. 51 | FieldNameFormatter FieldNameFormatterFunc 52 | 53 | // MethodNameFormatter allows specifying a custom method name formatter. 54 | MethodNameFormatter MethodNameFormatterFunc 55 | 56 | // StartModifier usually should be "export" or declare but as of now prevents 57 | // the LSP autocompletion so we keep it empty. 58 | // 59 | // See also: 60 | // https://github.com/microsoft/TypeScript/issues/54330 61 | // https://github.com/microsoft/TypeScript/pull/49644 62 | StartModifier string 63 | 64 | // Indent allow customizing the default indentation (use \t if you want tabs). 65 | Indent string 66 | } 67 | 68 | // Initializes the defaults (if not already) of the current config. 69 | func (c *Config) InitDefaults() { 70 | if c.Indent == "" { 71 | c.Indent = defaultIndent 72 | } 73 | 74 | if c.TypeMappings == nil { 75 | c.TypeMappings = make(map[string]string) 76 | } 77 | 78 | // special case for the unsafe package because it doesn't return its types in pkg.Syntax 79 | if _, ok := c.TypeMappings["unsafe.Pointer"]; !ok { 80 | c.TypeMappings["unsafe.Pointer"] = "number" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /package_generator.go: -------------------------------------------------------------------------------- 1 | package tygoja 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "strings" 7 | 8 | "golang.org/x/tools/go/packages" 9 | ) 10 | 11 | // PackageGenerator is responsible for generating the code for a single input package. 12 | type PackageGenerator struct { 13 | conf *Config 14 | pkg *packages.Package 15 | types []string 16 | withPkgDoc bool 17 | 18 | generatedTypes map[string]struct{} 19 | unknownTypes map[string]struct{} 20 | imports map[string][]string // path -> []names/aliases 21 | } 22 | 23 | // Generate generates the typings for a single package. 24 | func (g *PackageGenerator) Generate() (string, error) { 25 | s := new(strings.Builder) 26 | 27 | namespace := packageNameFromPath(g.pkg.ID) 28 | 29 | s.WriteString("\n") 30 | 31 | if g.withPkgDoc { 32 | for _, f := range g.pkg.Syntax { 33 | if f.Doc == nil || len(f.Doc.List) == 0 { 34 | continue 35 | } 36 | g.writeCommentGroup(s, f.Doc, 0) 37 | } 38 | } 39 | 40 | g.writeStartModifier(s, 0) 41 | s.WriteString("namespace ") 42 | s.WriteString(namespace) 43 | s.WriteString(" {\n") 44 | 45 | // register the aliased imports within the package namespace 46 | // (see https://www.typescriptlang.org/docs/handbook/namespaces.html#aliases) 47 | loadedAliases := map[string]struct{}{} 48 | for _, file := range g.pkg.Syntax { 49 | for _, imp := range file.Imports { 50 | path := strings.Trim(imp.Path.Value, `"' `) 51 | 52 | pgkName := packageNameFromPath(path) 53 | alias := pgkName 54 | 55 | if imp.Name != nil && imp.Name.Name != "" && imp.Name.Name != "_" { 56 | alias = imp.Name.Name 57 | 58 | if _, ok := loadedAliases[alias]; ok { 59 | continue // already registered 60 | } 61 | 62 | loadedAliases[alias] = struct{}{} 63 | 64 | g.writeIndent(s, 1) 65 | s.WriteString("// @ts-ignore\n") 66 | g.writeIndent(s, 1) 67 | s.WriteString("import ") 68 | s.WriteString(alias) 69 | s.WriteString(" = ") 70 | s.WriteString(pgkName) 71 | s.WriteString("\n") 72 | } 73 | 74 | // register the import to export its package later 75 | if !exists(g.imports[path], alias) { 76 | if g.imports[path] == nil { 77 | g.imports[path] = []string{} 78 | } 79 | g.imports[path] = append(g.imports[path], alias) 80 | } 81 | } 82 | 83 | ast.Inspect(file, func(n ast.Node) bool { 84 | switch x := n.(type) { 85 | case *ast.FuncDecl: // FuncDecl can be package level function or struct method 86 | g.writeFuncDecl(s, x, 1) 87 | return false 88 | case *ast.GenDecl: // GenDecl can be an import, type, var, or const expression 89 | if x.Tok == token.VAR || x.Tok == token.IMPORT { 90 | return false // ignore variables and import statements for now 91 | } 92 | 93 | g.writeGroupDecl(s, x, 1) 94 | return false 95 | } 96 | 97 | return true 98 | }) 99 | } 100 | 101 | s.WriteString("}\n") 102 | 103 | return s.String(), nil 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | (EXP) tygoja 2 | [](https://pkg.go.dev/github.com/pocketbase/tygoja) 3 | ====================================================================== 4 | 5 | **tygoja** is a small helper library for generating TypeScript declarations from Go code. 6 | 7 | The generated typings are intended to be used as import helpers to provide [ambient TypeScript declarations](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html) (aka. `.d.ts`) for [goja](https://github.com/dop251/goja) bindings. 8 | 9 | > **⚠️ Don't use it directly in production! It is not tagged and may change without notice.** 10 | > 11 | > **It was created to semi-automate the documentation of the goja integration for PocketBase.** 12 | > 13 | > **Use it only as a reference or as a non-critical step in your dev pipeline.** 14 | 15 | **tygoja** is a heavily modified fork of [tygo](https://github.com/gzuidhof/tygo) and extends its scope with: 16 | 17 | - custom field and method names formatters 18 | - types for interfaces (exported and unexported) 19 | - types for exported interface methods 20 | - types for exported struct methods 21 | - types for exported package level functions (_must enable `PackageConfig.WithPackageFunctions`_) 22 | - inheritance declarations for embeded structs (_embedded pointers are treated as values_) 23 | - autoloading all unmapped argument and return types (_when possible_) 24 | - applying the same [goja's rules](https://pkg.go.dev/github.com/dop251/goja#hdr-Nil) when resolving the return types of exported function and methods 25 | - combining multiple packages typings in a single output 26 | - generating all declarations in namespaces with the packages name (both unmapped and mapped) 27 | - preserving comment block new lines 28 | - converting Go comment code blocks to Markdown code blocks 29 | - and others... 30 | 31 | Note that by default the generated typings are not generated with `export` since the intended usage is to map them to your custom goja bindings. 32 | This mapping could be defined in the `Config.Heading` field usually with the `declare` keyword (eg. `declare let someGojaProp: app.Cache`). 33 | 34 | ## Example 35 | 36 | ```go 37 | package main 38 | 39 | import ( 40 | "log" 41 | "os" 42 | 43 | "github.com/pocketbase/tygoja" 44 | ) 45 | 46 | func main() { 47 | gen := tygoja.New(tygoja.Config{ 48 | Packages: map[string][]string{ 49 | "github.com/pocketbase/tygoja/test/a": {"*"}, 50 | "github.com/pocketbase/tygoja/test/b": {"*"}, 51 | "github.com/pocketbase/tygoja/test/c": {"Example2", "Handler"}, 52 | }, 53 | Heading: `declare var $app: c.Handler; // bind other fields `, 54 | WithPackageFunctions: true, 55 | }) 56 | 57 | result, err := gen.Generate() 58 | if err != nil { 59 | log.Fatal(err) 60 | } 61 | 62 | if err := os.WriteFile("./types.d.ts", []byte(result), 0644); err != nil { 63 | log.Fatal(err) 64 | } 65 | } 66 | ``` 67 | 68 | You can also combine it with [typedoc](https://typedoc.org/) to create HTML/JSON docs from the generated declaration(s). 69 | 70 | See the package `/test` directory for example output. 71 | 72 | For a more detailed example you can also explore the [PocketBase's jsvm plugin](https://github.com/pocketbase/pocketbase/tree/develop/plugins/jsvm/internal/docs). 73 | 74 | 75 | ## Known issues and limitations 76 | 77 | - Multiple versions of the same package may have unexpected declaration since all versions will be under the same namespace. 78 | - For easier generation, it relies on TypeScript declarations merging, meaning that the generated types may not be very compact. 79 | - Package level functions and constants, that are reserved JS identifier, are prefixed with underscore (eg. `_in()`). 80 | -------------------------------------------------------------------------------- /tygoja.go: -------------------------------------------------------------------------------- 1 | package tygoja 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "regexp" 7 | "strings" 8 | 9 | "golang.org/x/tools/go/packages" 10 | ) 11 | 12 | // Tygoja is a generator for one or more input packages, 13 | // responsible for linking them together if necessary. 14 | type Tygoja struct { 15 | conf *Config 16 | 17 | parent *Tygoja 18 | implicitPackages map[string][]string 19 | generatedTypes map[string][]string 20 | generatedPackageDocs map[string]struct{} 21 | } 22 | 23 | // New initializes a new Tygoja generator from the specified config. 24 | func New(config Config) *Tygoja { 25 | config.InitDefaults() 26 | 27 | return &Tygoja{ 28 | conf: &config, 29 | implicitPackages: map[string][]string{}, 30 | generatedTypes: map[string][]string{}, 31 | generatedPackageDocs: map[string]struct{}{}, 32 | } 33 | } 34 | 35 | // Generate executes the generator and produces the related TS files. 36 | func (g *Tygoja) Generate() (string, error) { 37 | // extract config packages 38 | configPackages := make([]string, 0, len(g.conf.Packages)) 39 | for p, types := range g.conf.Packages { 40 | if len(types) == 0 { 41 | continue // no typings 42 | } 43 | configPackages = append(configPackages, p) 44 | } 45 | 46 | // load packages info 47 | pkgs, err := packages.Load(&packages.Config{ 48 | Mode: packages.NeedSyntax | packages.NeedFiles | packages.NeedDeps | packages.NeedImports | packages.NeedTypes, 49 | }, configPackages...) 50 | if err != nil { 51 | return "", err 52 | } 53 | 54 | var s strings.Builder 55 | 56 | // Heading 57 | if g.parent == nil { 58 | s.WriteString("// GENERATED CODE - DO NOT MODIFY BY HAND\n") 59 | 60 | if g.conf.Heading != "" { 61 | s.WriteString(g.conf.Heading) 62 | } 63 | 64 | // write base types 65 | // --- 66 | s.WriteString("type ") 67 | s.WriteString(BaseTypeDict) 68 | s.WriteString(" = { [key:string | number | symbol]: any; }\n") 69 | 70 | s.WriteString("type ") 71 | s.WriteString(BaseTypeAny) 72 | s.WriteString(" = any\n") 73 | // --- 74 | } 75 | 76 | for i, pkg := range pkgs { 77 | if len(pkg.Errors) > 0 { 78 | return "", fmt.Errorf("%+v", pkg.Errors) 79 | } 80 | 81 | if len(pkg.GoFiles) == 0 { 82 | return "", fmt.Errorf("no input go files for package index %d", i) 83 | } 84 | 85 | if len(g.conf.Packages[pkg.ID]) == 0 { 86 | // ignore the package as it has no typings 87 | continue 88 | } 89 | 90 | pkgGen := &PackageGenerator{ 91 | conf: g.conf, 92 | pkg: pkg, 93 | types: g.conf.Packages[pkg.ID], 94 | withPkgDoc: !g.isPackageDocGenerated(pkg.ID), 95 | generatedTypes: map[string]struct{}{}, 96 | unknownTypes: map[string]struct{}{}, 97 | imports: map[string][]string{}, 98 | } 99 | 100 | code, err := pkgGen.Generate() 101 | if err != nil { 102 | return "", err 103 | } 104 | 105 | g.generatedPackageDocs[pkg.ID] = struct{}{} 106 | 107 | for t := range pkgGen.generatedTypes { 108 | g.generatedTypes[pkg.ID] = append(g.generatedTypes[pkg.ID], t) 109 | } 110 | 111 | for t := range pkgGen.unknownTypes { 112 | parts := strings.Split(t, ".") 113 | var tPkg string 114 | var tName string 115 | 116 | if len(parts) == 0 { 117 | continue 118 | } 119 | 120 | if len(parts) == 2 { 121 | // type from external package 122 | tPkg = parts[0] 123 | tName = parts[1] 124 | } else { 125 | // unexported type from the current package 126 | tName = parts[0] 127 | 128 | // already mapped for export 129 | if pkgGen.isTypeAllowed(tName) { 130 | continue 131 | } 132 | 133 | tPkg = packageNameFromPath(pkg.ID) 134 | 135 | // add to self import later 136 | pkgGen.imports[pkg.ID] = []string{tPkg} 137 | } 138 | 139 | for p, aliases := range pkgGen.imports { 140 | for _, alias := range aliases { 141 | if tName != "" && alias == tPkg && !g.isTypeGenerated(p, tName) && !exists(g.implicitPackages[p], tName) { 142 | if g.implicitPackages[p] == nil { 143 | g.implicitPackages[p] = []string{} 144 | } 145 | g.implicitPackages[p] = append(g.implicitPackages[p], tName) 146 | break 147 | } 148 | } 149 | } 150 | } 151 | 152 | s.WriteString(code) 153 | } 154 | 155 | // recursively try to generate the found unknown types 156 | if len(g.implicitPackages) > 0 { 157 | subConfig := *g.conf 158 | subConfig.Heading = "" 159 | if (subConfig.TypeMappings) == nil { 160 | subConfig.TypeMappings = map[string]string{} 161 | } 162 | 163 | // extract the nonempty package definitions 164 | subConfig.Packages = make(map[string][]string, len(g.implicitPackages)) 165 | for p, types := range g.implicitPackages { 166 | if len(types) == 0 { 167 | continue 168 | } 169 | subConfig.Packages[p] = types 170 | } 171 | 172 | subGenerator := New(subConfig) 173 | subGenerator.parent = g 174 | subResult, err := subGenerator.Generate() 175 | if err != nil { 176 | return "", err 177 | } 178 | 179 | s.WriteString(subResult) 180 | } 181 | 182 | return s.String(), nil 183 | } 184 | 185 | func (g *Tygoja) isPackageDocGenerated(pkgId string) bool { 186 | _, ok := g.generatedPackageDocs[pkgId] 187 | if ok { 188 | return true 189 | } 190 | 191 | if g.parent != nil { 192 | return g.parent.isPackageDocGenerated(pkgId) 193 | } 194 | 195 | return false 196 | } 197 | 198 | func (g *Tygoja) isTypeGenerated(pkg string, name string) bool { 199 | if g.parent != nil && g.parent.isTypeGenerated(pkg, name) { 200 | return true 201 | } 202 | 203 | if len(g.generatedTypes[pkg]) == 0 { 204 | return false 205 | } 206 | 207 | for _, t := range g.generatedTypes[pkg] { 208 | if t == name { 209 | return true 210 | } 211 | } 212 | 213 | return false 214 | } 215 | 216 | // isTypeAllowed checks whether the provided type name is allowed by the generator "types". 217 | func (g *PackageGenerator) isTypeAllowed(name string) bool { 218 | name = strings.TrimSpace(name) 219 | 220 | if name == "" { 221 | return false 222 | } 223 | 224 | for _, t := range g.types { 225 | if t == name || t == "*" { 226 | return true 227 | } 228 | } 229 | 230 | return false 231 | } 232 | 233 | func (g *PackageGenerator) markTypeAsGenerated(t string) { 234 | g.generatedTypes[t] = struct{}{} 235 | } 236 | 237 | var versionRegex = regexp.MustCompile(`^v\d+$`) 238 | 239 | // packageNameFromPath extracts and normalizes the imported package identifier. 240 | // 241 | // For example: 242 | // 243 | // "github.com/labstack/echo/v5" -> "echo" 244 | // "github.com/go-ozzo/ozzo-validation/v4" -> "ozzo_validation" 245 | func packageNameFromPath(path string) string { 246 | name := filepath.Base(strings.Trim(path, `"' `)) 247 | 248 | if versionRegex.MatchString(name) { 249 | name = filepath.Base(filepath.Dir(path)) 250 | } 251 | 252 | return strings.ReplaceAll(name, "-", "_") 253 | } 254 | 255 | // exists checks if search exists in list. 256 | func exists[T comparable](list []T, search T) bool { 257 | for _, v := range list { 258 | if v == search { 259 | return true 260 | } 261 | } 262 | 263 | return false 264 | } 265 | -------------------------------------------------------------------------------- /test/htmldocs/types/_TygojaAny.html: -------------------------------------------------------------------------------- 1 |
Generated using TypeDoc
Generated using TypeDoc
Generated using TypeDoc
Generated using TypeDoc
Generated using TypeDoc
Generated using TypeDoc
Generated using TypeDoc
structB comment
21 |Generated using TypeDoc
Generated using TypeDoc
Generated using TypeDoc
Generated using TypeDoc
Generated using TypeDoc
Generated using TypeDoc
Generated using TypeDoc
Generated using TypeDoc
Generated using TypeDoc
Generated using TypeDoc
func type comment
21 |