Skip to content

Commit e42e170

Browse files
committed
chore: support external types in typescript codegen
1 parent fd8a868 commit e42e170

File tree

2 files changed

+103
-21
lines changed

2 files changed

+103
-21
lines changed

codersdk/deployment.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -405,9 +405,6 @@ func DefaultCacheDir() string {
405405
}
406406

407407
// DeploymentConfig contains both the deployment values and how they're set.
408-
//
409-
// @typescript-ignore DeploymentConfig
410-
// apitypings doesn't know how to generate the OptionSet... yet.
411408
type DeploymentConfig struct {
412409
Values *DeploymentValues `json:"config,omitempty"`
413410
Options clibase.OptionSet `json:"options,omitempty"`

scripts/apitypings/main.go

Lines changed: 103 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,36 +26,64 @@ import (
2626
)
2727

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

3340
func main() {
3441
ctx := context.Background()
3542
log := slog.Make(sloghuman.Sink(os.Stderr))
43+
44+
external := []*Generator{}
45+
for _, dir := range externalTypeDirs {
46+
extGen, err := ParseDirectory(ctx, log, dir)
47+
if err != nil {
48+
log.Fatal(ctx, fmt.Sprintf("parse external directory %s: %s", dir, err.Error()))
49+
}
50+
extGen.onlyOptIn = true
51+
external = append(external, extGen)
52+
}
53+
3654
_, _ = fmt.Print("// Code generated by 'make site/src/api/typesGenerated.ts'. DO NOT EDIT.\n\n")
3755
for _, baseDir := range baseDirs {
3856
_, _ = fmt.Printf("// The code below is generated from %s.\n\n", strings.TrimPrefix(baseDir, "./"))
39-
output, err := Generate(baseDir)
57+
output, err := Generate(baseDir, external...)
4058
if err != nil {
4159
log.Fatal(ctx, err.Error())
4260
}
4361

4462
// Just cat the output to a file to capture it
4563
_, _ = fmt.Print(output, "\n\n")
4664
}
65+
66+
for i, ext := range external {
67+
ts, err := ext.generateAll()
68+
if err != nil {
69+
log.Fatal(ctx, fmt.Sprintf("generate external: %s", err.Error()))
70+
}
71+
dir := externalTypeDirs[i]
72+
_, _ = fmt.Printf("// The code below is generated from %s.\n\n", strings.TrimPrefix(dir, "./"))
73+
_, _ = fmt.Print(ts.String(), "\n\n")
74+
}
4775
}
4876

49-
func Generate(directory string) (string, error) {
77+
func Generate(directory string, externals ...*Generator) (string, error) {
5078
ctx := context.Background()
5179
log := slog.Make(sloghuman.Sink(os.Stderr))
52-
codeBlocks, err := GenerateFromDirectory(ctx, log, directory)
80+
gen, err := GenerateFromDirectory(ctx, log, directory, externals...)
5381
if err != nil {
5482
return "", err
5583
}
5684

5785
// Just cat the output to a file to capture it
58-
return codeBlocks.String(), nil
86+
return gen.cachedResult.String(), nil
5987
}
6088

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

112-
// GenerateFromDirectory will return all the typescript code blocks for a directory
113-
func GenerateFromDirectory(ctx context.Context, log slog.Logger, directory string) (*TypescriptTypes, error) {
114-
g := Generator{
115-
log: log,
116-
builtins: make(map[string]string),
140+
func ParseDirectory(ctx context.Context, log slog.Logger, directory string, externals ...*Generator) (*Generator, error) {
141+
g := &Generator{
142+
log: log,
143+
builtins: make(map[string]string),
144+
externals: externals,
117145
}
118146
err := g.parsePackage(ctx, directory)
119147
if err != nil {
120148
return nil, xerrors.Errorf("parse package %q: %w", directory, err)
121149
}
122150

151+
return g, nil
152+
}
153+
154+
// GenerateFromDirectory will return all the typescript code blocks for a directory
155+
func GenerateFromDirectory(ctx context.Context, log slog.Logger, directory string, externals ...*Generator) (*Generator, error) {
156+
g, err := ParseDirectory(ctx, log, directory, externals...)
157+
if err != nil {
158+
return nil, err
159+
}
160+
123161
codeBlocks, err := g.generateAll()
124162
if err != nil {
125-
return nil, xerrors.Errorf("parse package %q: %w", directory, err)
163+
return nil, xerrors.Errorf("generate package %q: %w", directory, err)
126164
}
165+
g.cachedResult = codeBlocks
127166

128-
return codeBlocks, nil
167+
return g, nil
129168
}
130169

131170
type Generator struct {
132171
// Package we are scanning.
133172
pkg *packages.Package
134173
log slog.Logger
135174

175+
// allowList if set only generates types in the allow list.
176+
// This is kinda a hack to get around the fact that external types
177+
// only should generate referenced types, and multiple packages can
178+
// reference the same external types.
179+
onlyOptIn bool
180+
allowList []string
181+
182+
// externals are other packages referenced. Optional
183+
externals []*Generator
184+
136185
// builtins is kinda a hack to get around the fact that using builtin
137186
// generic constraints is common. We want to support them even though
138187
// they are external to our package.
@@ -141,6 +190,8 @@ type Generator struct {
141190
// cannot be implemented in go. So they are a first class thing that we just
142191
// have to make a static string for ¯\_(ツ)_/¯
143192
builtins map[string]string
193+
194+
cachedResult *TypescriptTypes
144195
}
145196

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

234+
for _, a := range g.allowList {
235+
m.AllowedTypes[strings.TrimSpace(a)] = struct{}{}
236+
}
237+
183238
// Look for comments that indicate to ignore a type for typescript generation.
184239
ignoreRegex := regexp.MustCompile("@typescript-ignore[:]?(?P<ignored_types>.*)")
185240
for _, file := range g.pkg.Syntax {
@@ -303,7 +358,7 @@ func (g *Generator) generateOne(m *Maps, obj types.Object) error {
303358
}
304359

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

@@ -789,8 +844,16 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) {
789844
objName := objName(n.Obj())
790845
genericName := ""
791846
genericTypes := make(map[string]string)
792-
pkgName := n.Obj().Pkg().Name()
793-
if obj := g.pkg.Types.Scope().Lookup(n.Obj().Name()); g.pkg.Name == pkgName && obj != nil {
847+
848+
obj, objGen, local := g.lookupNamedReference(n)
849+
if obj != nil {
850+
if !local {
851+
objGen.allowList = append(objGen.allowList, objName)
852+
g.log.Debug(context.Background(), "found external type",
853+
"name", objName,
854+
"ext_pkg", objGen.pkg.String(),
855+
)
856+
}
794857
// Sweet! Using other typescript types as fields. This could be an
795858
// enum or another struct
796859
if args := n.TypeArgs(); args != nil && args.Len() > 0 {
@@ -817,10 +880,16 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) {
817880
genericName = objName + fmt.Sprintf("<%s>", strings.Join(genericNames, ", "))
818881
objName += fmt.Sprintf("<%s>", strings.Join(genericConstraints, ", "))
819882
}
883+
884+
cmt := ""
885+
if !local {
886+
indentedComment("external reference")
887+
}
820888
return TypescriptType{
821-
GenericTypes: genericTypes,
822-
GenericValue: genericName,
823-
ValueType: objName,
889+
GenericTypes: genericTypes,
890+
GenericValue: genericName,
891+
ValueType: objName,
892+
AboveTypeLine: cmt,
824893
}, nil
825894
}
826895

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

1000+
func (g *Generator) lookupNamedReference(n *types.Named) (obj types.Object, generator *Generator, local bool) {
1001+
pkgName := n.Obj().Pkg().Name()
1002+
1003+
if obj := g.pkg.Types.Scope().Lookup(n.Obj().Name()); g.pkg.Name == pkgName && obj != nil {
1004+
return obj, g, true
1005+
}
1006+
1007+
for _, ext := range g.externals {
1008+
if obj := ext.pkg.Types.Scope().Lookup(n.Obj().Name()); ext.pkg.Name == pkgName && obj != nil {
1009+
return obj, ext, false
1010+
}
1011+
}
1012+
1013+
return nil, nil, false
1014+
}
1015+
9311016
// isBuiltIn returns the string for a builtin type that we want to support
9321017
// if the name is a reserved builtin type. This is for types like 'comparable'.
9331018
// These types are not implemented in golang, so we just have to hardcode it.

0 commit comments

Comments
 (0)