Skip to content

chore: support external types in typescript codegen #9633

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
chore: support external types in typescript codegen
  • Loading branch information
Emyrk committed Sep 11, 2023
commit e42e170d9fc2c9667c1066487047205fae786ea9
3 changes: 0 additions & 3 deletions codersdk/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,9 +405,6 @@ func DefaultCacheDir() string {
}

// DeploymentConfig contains both the deployment values and how they're set.
//
// @typescript-ignore DeploymentConfig
// apitypings doesn't know how to generate the OptionSet... yet.
type DeploymentConfig struct {
Values *DeploymentValues `json:"config,omitempty"`
Options clibase.OptionSet `json:"options,omitempty"`
Expand Down
121 changes: 103 additions & 18 deletions scripts/apitypings/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,36 +26,64 @@ import (
)

var (
// baseDirs are the directories to introspect for types to generate.
baseDirs = [...]string{"./codersdk", "./coderd/healthcheck", "./coderd/healthcheck/derphealth"}
indent = " "
// externalTypes are types that are not in the baseDirs, but we want to
// support. These are usually types that are used in the baseDirs.
// Do not include things like "Database", as that would break the idea
// of splitting db and api types.
// Only include dirs that are client facing packages.
externalTypeDirs = [...]string{"./cli/clibase"}
indent = " "
)

func main() {
ctx := context.Background()
log := slog.Make(sloghuman.Sink(os.Stderr))

external := []*Generator{}
for _, dir := range externalTypeDirs {
extGen, err := ParseDirectory(ctx, log, dir)
if err != nil {
log.Fatal(ctx, fmt.Sprintf("parse external directory %s: %s", dir, err.Error()))
}
extGen.onlyOptIn = true
external = append(external, extGen)
}

_, _ = fmt.Print("// Code generated by 'make site/src/api/typesGenerated.ts'. DO NOT EDIT.\n\n")
for _, baseDir := range baseDirs {
_, _ = fmt.Printf("// The code below is generated from %s.\n\n", strings.TrimPrefix(baseDir, "./"))
output, err := Generate(baseDir)
output, err := Generate(baseDir, external...)
if err != nil {
log.Fatal(ctx, err.Error())
}

// Just cat the output to a file to capture it
_, _ = fmt.Print(output, "\n\n")
}

for i, ext := range external {
ts, err := ext.generateAll()
if err != nil {
log.Fatal(ctx, fmt.Sprintf("generate external: %s", err.Error()))
}
dir := externalTypeDirs[i]
_, _ = fmt.Printf("// The code below is generated from %s.\n\n", strings.TrimPrefix(dir, "./"))
_, _ = fmt.Print(ts.String(), "\n\n")
}
}

func Generate(directory string) (string, error) {
func Generate(directory string, externals ...*Generator) (string, error) {
ctx := context.Background()
log := slog.Make(sloghuman.Sink(os.Stderr))
codeBlocks, err := GenerateFromDirectory(ctx, log, directory)
gen, err := GenerateFromDirectory(ctx, log, directory, externals...)
if err != nil {
return "", err
}

// Just cat the output to a file to capture it
return codeBlocks.String(), nil
return gen.cachedResult.String(), nil
}

// TypescriptTypes holds all the code blocks created.
Expand Down Expand Up @@ -109,30 +137,51 @@ func (t TypescriptTypes) String() string {
return strings.TrimRight(s.String(), "\n")
}

// GenerateFromDirectory will return all the typescript code blocks for a directory
func GenerateFromDirectory(ctx context.Context, log slog.Logger, directory string) (*TypescriptTypes, error) {
g := Generator{
log: log,
builtins: make(map[string]string),
func ParseDirectory(ctx context.Context, log slog.Logger, directory string, externals ...*Generator) (*Generator, error) {
g := &Generator{
log: log,
builtins: make(map[string]string),
externals: externals,
}
err := g.parsePackage(ctx, directory)
if err != nil {
return nil, xerrors.Errorf("parse package %q: %w", directory, err)
}

return g, nil
}

// GenerateFromDirectory will return all the typescript code blocks for a directory
func GenerateFromDirectory(ctx context.Context, log slog.Logger, directory string, externals ...*Generator) (*Generator, error) {
g, err := ParseDirectory(ctx, log, directory, externals...)
if err != nil {
return nil, err
}

codeBlocks, err := g.generateAll()
if err != nil {
return nil, xerrors.Errorf("parse package %q: %w", directory, err)
return nil, xerrors.Errorf("generate package %q: %w", directory, err)
}
g.cachedResult = codeBlocks

return codeBlocks, nil
return g, nil
}

type Generator struct {
// Package we are scanning.
pkg *packages.Package
log slog.Logger

// allowList if set only generates types in the allow list.
// This is kinda a hack to get around the fact that external types
// only should generate referenced types, and multiple packages can
// reference the same external types.
onlyOptIn bool
allowList []string

// externals are other packages referenced. Optional
externals []*Generator

// builtins is kinda a hack to get around the fact that using builtin
// generic constraints is common. We want to support them even though
// they are external to our package.
Expand All @@ -141,6 +190,8 @@ type Generator struct {
// cannot be implemented in go. So they are a first class thing that we just
// have to make a static string for ¯\_(ツ)_/¯
builtins map[string]string

cachedResult *TypescriptTypes
}

// parsePackage takes a list of patterns such as a directory, and parses them.
Expand Down Expand Up @@ -180,6 +231,10 @@ func (g *Generator) generateAll() (*TypescriptTypes, error) {
AllowedTypes: make(map[string]struct{}),
}

for _, a := range g.allowList {
m.AllowedTypes[strings.TrimSpace(a)] = struct{}{}
}

// Look for comments that indicate to ignore a type for typescript generation.
ignoreRegex := regexp.MustCompile("@typescript-ignore[:]?(?P<ignored_types>.*)")
for _, file := range g.pkg.Syntax {
Expand Down Expand Up @@ -303,7 +358,7 @@ func (g *Generator) generateOne(m *Maps, obj types.Object) error {
}

// If we have allowed types, only allow those to be generated.
if _, ok := m.AllowedTypes[obj.Name()]; len(m.AllowedTypes) > 0 && !ok {
if _, ok := m.AllowedTypes[obj.Name()]; (len(m.AllowedTypes) > 0 || g.onlyOptIn) && !ok {
return nil
}

Expand Down Expand Up @@ -789,8 +844,16 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) {
objName := objName(n.Obj())
genericName := ""
genericTypes := make(map[string]string)
pkgName := n.Obj().Pkg().Name()
if obj := g.pkg.Types.Scope().Lookup(n.Obj().Name()); g.pkg.Name == pkgName && obj != nil {

obj, objGen, local := g.lookupNamedReference(n)
if obj != nil {
if !local {
objGen.allowList = append(objGen.allowList, objName)
g.log.Debug(context.Background(), "found external type",
"name", objName,
"ext_pkg", objGen.pkg.String(),
)
}
// Sweet! Using other typescript types as fields. This could be an
// enum or another struct
if args := n.TypeArgs(); args != nil && args.Len() > 0 {
Expand All @@ -817,10 +880,16 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) {
genericName = objName + fmt.Sprintf("<%s>", strings.Join(genericNames, ", "))
objName += fmt.Sprintf("<%s>", strings.Join(genericConstraints, ", "))
}

cmt := ""
if !local {
indentedComment("external reference")
}
return TypescriptType{
GenericTypes: genericTypes,
GenericValue: genericName,
ValueType: objName,
GenericTypes: genericTypes,
GenericValue: genericName,
ValueType: objName,
AboveTypeLine: cmt,
}, nil
}

Expand Down Expand Up @@ -928,6 +997,22 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) {
return TypescriptType{}, xerrors.Errorf("unknown type: %s", ty.String())
}

func (g *Generator) lookupNamedReference(n *types.Named) (obj types.Object, generator *Generator, local bool) {
pkgName := n.Obj().Pkg().Name()

if obj := g.pkg.Types.Scope().Lookup(n.Obj().Name()); g.pkg.Name == pkgName && obj != nil {
return obj, g, true
}

for _, ext := range g.externals {
if obj := ext.pkg.Types.Scope().Lookup(n.Obj().Name()); ext.pkg.Name == pkgName && obj != nil {
return obj, ext, false
}
}

return nil, nil, false
}

// isBuiltIn returns the string for a builtin type that we want to support
// if the name is a reserved builtin type. This is for types like 'comparable'.
// These types are not implemented in golang, so we just have to hardcode it.
Expand Down