Skip to content

Commit 60e98cd

Browse files
committed
initial commit
0 parents  commit 60e98cd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+8672
-0
lines changed

.gitignore

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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

LICENSE

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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.

README.md

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
(EXP) tygoja
2+
[![GoDoc](https://godoc.org/github.com/pocketbase/tygoja?status.svg)](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 versioned 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 learning 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+
32+
## Known issues and limitations
33+
34+
- Multiple versions of the same package may have unexpected declaration as all versions will be under the same namespace
35+
- For easier generation, it relies on TypeScript declarations merging, meaning that the generated types may not be very compact
36+
37+
## Example
38+
39+
```go
40+
package main
41+
42+
import (
43+
"log"
44+
"os"
45+
46+
"github.com/pocketbase/tygoja"
47+
)
48+
49+
func main() {
50+
gen := tygoja.New(tygoja.Config{
51+
Packages: map[string][]string{
52+
"github.com/pocketbase/tygoja/test/a": {"*"},
53+
"github.com/pocketbase/tygoja/test/b": {"*"},
54+
"github.com/pocketbase/tygoja/test/c": {"Example2", "Handler"},
55+
},
56+
Heading: `declare var $app: c.Handler;`,
57+
WithPackageFunctions: true,
58+
})
59+
60+
result, err := gen.Generate()
61+
if err != nil {
62+
log.Fatal(err)
63+
}
64+
65+
if err := os.WriteFile("./types.d.ts", []byte(result), 0644); err != nil {
66+
log.Fatal(err)
67+
}
68+
}
69+
```
70+
71+
You can also combine it with [typedoc](https://typedoc.org/) to create HTML/JSON docs from the generated declaration(s).
72+
73+
See the package `/test` directory for example output.

config.go

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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+
}

go.mod

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module github.com/pocketbase/tygoja
2+
3+
go 1.18
4+
5+
require golang.org/x/tools v0.10.0
6+
7+
require (
8+
golang.org/x/mod v0.11.0 // indirect
9+
golang.org/x/sys v0.9.0 // indirect
10+
)

go.sum

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
2+
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
3+
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
4+
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
5+
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
6+
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
7+
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=

iota.go

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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+
}

package_generator.go

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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+
17+
generatedTypes map[string]struct{}
18+
unknownTypes map[string]struct{}
19+
imports map[string][]string // path -> []names/aliases
20+
}
21+
22+
// Generate generates the typings for a single package.
23+
func (g *PackageGenerator) Generate() (string, error) {
24+
s := new(strings.Builder)
25+
26+
namespace := packageNameFromPath(g.pkg.ID)
27+
28+
s.WriteString("\n")
29+
for _, f := range g.pkg.Syntax {
30+
if f.Doc == nil || len(f.Doc.List) == 0 {
31+
continue
32+
}
33+
g.writeCommentGroup(s, f.Doc, 0)
34+
}
35+
g.writeStartModifier(s, 0)
36+
s.WriteString("namespace ")
37+
s.WriteString(namespace)
38+
s.WriteString(" {\n")
39+
40+
// register the aliased imports within the package namespace
41+
// (see https://www.typescriptlang.org/docs/handbook/namespaces.html#aliases)
42+
loadedAliases := map[string]struct{}{}
43+
for _, file := range g.pkg.Syntax {
44+
for _, imp := range file.Imports {
45+
path := strings.Trim(imp.Path.Value, `"' `)
46+
47+
pgkName := packageNameFromPath(path)
48+
alias := pgkName
49+
50+
if imp.Name != nil && imp.Name.Name != "" && imp.Name.Name != "_" {
51+
alias = imp.Name.Name
52+
53+
if _, ok := loadedAliases[alias]; ok {
54+
continue // already registered
55+
}
56+
57+
loadedAliases[alias] = struct{}{}
58+
59+
g.writeIndent(s, 1)
60+
s.WriteString("// @ts-ignore\n")
61+
g.writeIndent(s, 1)
62+
s.WriteString("import ")
63+
s.WriteString(alias)
64+
s.WriteString(" = ")
65+
s.WriteString(pgkName)
66+
s.WriteString("\n")
67+
}
68+
69+
// register the import to export its package later
70+
if !exists(g.imports[path], alias) {
71+
if g.imports[path] == nil {
72+
g.imports[path] = []string{}
73+
}
74+
g.imports[path] = append(g.imports[path], alias)
75+
}
76+
}
77+
78+
ast.Inspect(file, func(n ast.Node) bool {
79+
switch x := n.(type) {
80+
case *ast.FuncDecl: // FuncDecl can be package level function or struct method
81+
g.writeFuncDecl(s, x, 1)
82+
return false
83+
case *ast.GenDecl: // GenDecl can be an import, type, var, or const expression
84+
if x.Tok == token.VAR || x.Tok == token.IMPORT {
85+
return false // ignore variables and import statements for now
86+
}
87+
88+
g.writeGroupDecl(s, x, 1)
89+
return false
90+
}
91+
92+
return true
93+
})
94+
}
95+
96+
s.WriteString("}\n")
97+
98+
return s.String(), nil
99+
}

random.go

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
}

0 commit comments

Comments
 (0)