Skip to content

Commit

Permalink
chore: add comments and update readme
Browse files Browse the repository at this point in the history
  • Loading branch information
Emyrk committed Dec 10, 2024
1 parent d08c147 commit 644e14f
Show file tree
Hide file tree
Showing 16 changed files with 98 additions and 67 deletions.
23 changes: 19 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![Go Reference](https://pkg.go.dev/badge/github.com/coder/guts.svg)](https://pkg.go.dev/github.com/coder/guts)

`guts` is a tool to convert golang types to typescript for enabling a consistent type definition across the frontend and backend. It is intended to be called and customized as a library, rather than as a command line tool.
`guts` is a tool to convert golang types to typescript for enabling a consistent type definition across the frontend and backend. It is intended to be called and customized as a library, rather than as a command line executable.

See the [simple example](./example/simple) for a basic usage of the library.
```go
Expand Down Expand Up @@ -32,17 +32,32 @@ interface SimpleType<T extends Comparable> {

`guts` is a library, not a command line utility. This is to allow configuration with code, and also helps with package resolution.

See the [simple example](./example/simple) for a basic usage of the library.
See the [simple example](./example/simple) for a basic usage of the library. A larger example can be found in the [Coder repository](https://github.com/coder/coder/blob/a632a841d4f5666c2c1690801f88cd1a1fcffc00/scripts/apitypings/main.go).

```go
// Step 1: Create a new Golang parser
golang, _ := guts.NewGolangParser()
// Step 2: Configure the parser
_ = golang.IncludeGenerate("github.com/coder/guts/example/simple")
// Step 3: Convert the Golang to the typescript AST
ts, _ := golang.ToTypescript()
// Step 4: Mutate the typescript AST
ts.ApplyMutations(
config.ExportTypes, // add 'export' to all top level declarations
)
// Step 5: Serialize the typescript AST to a string
output, _ := ts.Serialize()
fmt.Println(output)
```


# How it works

`guts` first parses a set of golang packages. The Go AST is traversed to find all the types defined in the packages.

These types are placed into a simple AST that directly maps to the typescript AST.

Using [goja](https://github.com/dop251/goja), these types are then converted to typescript using the typescript compiler API.
Using [goja](https://github.com/dop251/goja), these types are then serialized to typescript using the typescript compiler API.


# Generator Opinions
Expand All @@ -64,4 +79,4 @@ output, _ := ts.Serialize()

# Helpful notes

An incredible website to visualize the AST of typescript: https://ts-ast-viewer.com/
An incredible website to visualize the AST of typescript: https://ts-ast-viewer.com/
4 changes: 0 additions & 4 deletions TODO.md

This file was deleted.

13 changes: 12 additions & 1 deletion bindings/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package bindings

import (
"fmt"
"go/token"
"go/types"

"github.com/dop251/goja"
Expand All @@ -11,12 +12,16 @@ type Node interface {
isNode()
}

// Identifier is a name given to a variable, function, class, etc.
// Identifiers should be unique within a package. Package information is
// included to help with disambiguation in the case of name collisions.
type Identifier struct {
Name string
Package *types.Package
Prefix string
}

// GoName should be a unique name for the identifier across all Go packages.
func (i Identifier) GoName() string {
if i.PkgName() != "" {
return fmt.Sprintf("%s.%s", i.PkgName(), i.Name)
Expand All @@ -36,12 +41,16 @@ func (i Identifier) String() string {
}

// Ref returns the identifier reference to be used in the generated code.
// This is the identifier to be used in typescript, since all generated code
// lands in the same namespace.
func (i Identifier) Ref() string {
return i.Prefix + i.Name
}

// Source is the golang file that an entity is sourced from.
type Source struct {
File string
File string
Position token.Position
}

type HeritageType string
Expand All @@ -51,6 +60,8 @@ const (
HeritageTypeImplements HeritageType = "implements"
)

// HeritageClause
// interface Foo extends Bar, Baz {}
type HeritageClause struct {
Token HeritageType
Args []ExpressionType
Expand Down
4 changes: 4 additions & 0 deletions bindings/declarations.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Interface struct {
func (*Interface) isNode() {}
func (*Interface) isDeclarationType() {}

// PropertySignature is a field in an interface
type PropertySignature struct {
// Name is the field name
Name string
Expand Down Expand Up @@ -85,6 +86,9 @@ func Simplify(p []*TypeParameter) ([]*TypeParameter, error) {
return params, nil
}

// VariableStatement is a top level declaration of a variable
// var foo: string = "bar"
// const foo: string = "bar"
type VariableStatement struct {
Modifiers []Modifier
Declarations *VariableDeclarationList
Expand Down
14 changes: 13 additions & 1 deletion bindings/expressions.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package bindings

import "golang.org/x/xerrors"
import (
"fmt"

"golang.org/x/xerrors"
)

// ExpressionType
type ExpressionType interface {
Expand Down Expand Up @@ -134,7 +138,15 @@ type OperatorNodeType struct {
Type ExpressionType
}

// OperatorNode allows adding a keyword to a type
// Keyword must be "KeyOfKeyword" | "UniqueKeyword" | "ReadonlyKeyword"
func OperatorNode(keyword LiteralKeyword, node ExpressionType) *OperatorNodeType {
switch keyword {
case KeywordReadonly, KeywordUnique, KeywordKeyOf:
default:
// TODO: Would be better to raise some error here.
panic(fmt.Sprint("unsupported operator keyword: ", keyword))
}
return &OperatorNodeType{
Keyword: keyword,
Type: node,
Expand Down
44 changes: 0 additions & 44 deletions bindings/parse.go

This file was deleted.

20 changes: 13 additions & 7 deletions builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@ package guts

import "github.com/coder/guts/bindings"

// Some references are built into either Golang or Typescript.
var (
// builtInComparable is a reference to the 'comparable' type in Golang.
builtInComparable = bindings.Identifier{Name: "Comparable"}
builtInString = bindings.Identifier{Name: "string"}
builtInNumber = bindings.Identifier{Name: "number"}
builtInBoolean = bindings.Identifier{Name: "boolean"}
builtInRecord = bindings.Identifier{Name: "Record"}
// builtInRecord is a reference to the 'Record' type in Typescript.
builtInRecord = bindings.Identifier{Name: "Record"}
)

// RecordReference creates a reference to the 'Record' type in Typescript.
// The Record type takes in 2 type parameters, key and value.
func RecordReference(key, value bindings.ExpressionType) *bindings.ReferenceType {
return bindings.Reference(builtInRecord, key, value)
}

func (ts *Typescript) includeComparable() {
// The zzz just pushes it to the end of the sorting.
// Kinda strange, but it works.
Expand All @@ -18,9 +24,9 @@ func (ts *Typescript) includeComparable() {
Name: builtInComparable,
Modifiers: []bindings.Modifier{},
Type: bindings.Union(
bindings.Reference(builtInString),
bindings.Reference(builtInNumber),
bindings.Reference(builtInBoolean),
ptr(bindings.KeywordString),
ptr(bindings.KeywordNumber),
ptr(bindings.KeywordBoolean),
),
Parameters: []*bindings.TypeParameter{},
Source: bindings.Source{},
Expand Down
1 change: 1 addition & 0 deletions cmd/gots/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/coder/guts/config"
)

// TODO: Build out a decent cli for this, just for easy experimentation.
func main() {
//ctx := context.Background()
gen, err := guts.NewGolangParser()
Expand Down
3 changes: 3 additions & 0 deletions config/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package config provides standard configurations for the guts package.
// These configurations are useful for common use cases.
package config
2 changes: 1 addition & 1 deletion convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,7 @@ func (ts *Typescript) typescriptType(ty types.Type) (parsedType, error) {
return parsedType{}, xerrors.Errorf("simplify generics in map: %w", err)
}
parsed := parsedType{
Value: bindings.Reference(builtInRecord, keyType.Value, valueType.Value),
Value: RecordReference(keyType.Value, valueType.Value),
TypeParameters: tp,
RaisedComments: append(keyType.RaisedComments, valueType.RaisedComments...),
}
Expand Down
14 changes: 14 additions & 0 deletions example/simple/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"time"

"github.com/coder/guts"
"github.com/coder/guts/config"
)

// SimpleType is a simple struct with a generic type
Expand All @@ -24,8 +25,21 @@ func main() {
"time.Time": "string",
})

// Convert the golang types to typescript AST
ts, _ := golang.ToTypescript()

// ApplyMutations allows adding in generation opinions to the typescript output.
// The basic generator has no opinions, so mutations are required to make the output
// more usable and idiomatic.
ts.ApplyMutations(
// Export all top level types
config.ExportTypes,
// Readonly changes all fields and types to being immutable.
// Useful if the types are only used for api responses, which should
// not be modified.
//config.ReadOnly,
)

// to see the AST tree
//ts.ForEach(func(key string, node *convert.typescriptNode) {
// walk.Walk(walk.PrintingVisitor(0), node.Node)
Expand Down
4 changes: 3 additions & 1 deletion lookup.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import (

func (ts *Typescript) location(obj types.Object) bindings.Source {
file := ts.parsed.fileSet.File(obj.Pos())
position := file.Position(obj.Pos())
return bindings.Source{
// Do not use filepath, as that changes behavior based on OS
File: path.Join(obj.Pkg().Name(), filepath.Base(file.Name())),
File: path.Join(obj.Pkg().Name(), filepath.Base(file.Name())),
Position: position,
}
}
6 changes: 6 additions & 0 deletions references.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import (
"go/types"
)

// referencedTypes is a helper struct to keep track of which types have been referenced
// from the generated code. This is required to generate externally referenced types.
// Since those types can also reference types, we have to continue to loop over
// the referenced types until we don't generate anything new.
type referencedTypes struct {
// ReferencedTypes is a map of package paths to a map of type strings to a boolean
// The bool is true if it was generated, false if it was only referenced
Expand All @@ -22,6 +26,8 @@ func newReferencedTypes() *referencedTypes {
}
}

// Remaining will call the next function for each type that has not been generated
// but should be.
func (r *referencedTypes) Remaining(next func(object types.Object) error) error {
// Keep looping over the referenced types until we don't generate anything new
// TODO: This could be optimized with a queue vs a full loop every time.
Expand Down
8 changes: 6 additions & 2 deletions single.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ import (
"github.com/coder/guts/bindings"
)

// parseExpression is kinda janky, but it allows you to send in Golang types
// to be parsed into Typescript types. Helps for type overrides.
// parseExpression feels a bit janky, however it enables the caller to send in
// a golang expression, eg `map[string]string`, and get back a Typescript type.
func parseExpression(expr string) (bindings.ExpressionType, error) {
fs := token.NewFileSet()
// This line means the expression must be an expression type, not a statement.
// This removes the ability for things like generics. If there is a reason
// to allow a larger subset of golang expressions and statements, this
// can be changed.
src := fmt.Sprintf(`package main; type check = %s;`, expr)

asFile, err := parser.ParseFile(fs, "main.go", []byte(src), 0)
Expand Down
3 changes: 3 additions & 0 deletions typescript-engine/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
Serves as a wrapper for the typescript compiler to generate typescript.
The wrapper exists to make the compiler easier to use with primitive types (strings).

# To recompile the javascript bindings

```
Expand Down
2 changes: 0 additions & 2 deletions typescript-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,6 @@ const resultFile = ts.createSourceFile(
ts.ScriptKind.TS
);

const savedNodes: ts.Node[] = [];

// printer is used to convert AST to string
const printer = ts.createPrinter({
newLine: ts.NewLineKind.LineFeed,
Expand Down

0 comments on commit 644e14f

Please sign in to comment.