From 416c68b80d574cd278f6f7f730a033523bfc8dad Mon Sep 17 00:00:00 2001 From: simse Date: Fri, 13 Sep 2024 15:10:53 +0100 Subject: [PATCH 1/2] feat: Add rudimentary plugin support --- .../packages/feature-account/codegen.yml | 4 +- .../feature-account/introspection.json | 1 + internal/config.go | 13 +- internal/converter.go | 342 ----------------- internal/{ => plugins}/casing.go | 2 +- internal/plugins/introspection.go | 5 + internal/plugins/plugins.go | 24 ++ internal/plugins/typescript.go | 350 ++++++++++++++++++ internal/project.go | 47 ++- 9 files changed, 433 insertions(+), 355 deletions(-) create mode 100644 examples/projects/monorepo/packages/feature-account/introspection.json rename internal/{ => plugins}/casing.go (92%) create mode 100644 internal/plugins/introspection.go create mode 100644 internal/plugins/plugins.go create mode 100644 internal/plugins/typescript.go diff --git a/examples/projects/monorepo/packages/feature-account/codegen.yml b/examples/projects/monorepo/packages/feature-account/codegen.yml index 937e268..a00c956 100644 --- a/examples/projects/monorepo/packages/feature-account/codegen.yml +++ b/examples/projects/monorepo/packages/feature-account/codegen.yml @@ -3,4 +3,6 @@ documents: [] overwrite: true generates: '__generated__/baseTypes.ts': - plugins: [typescript] \ No newline at end of file + plugins: [typescript] + 'introspection.json': + plugins: [introspection] \ No newline at end of file diff --git a/examples/projects/monorepo/packages/feature-account/introspection.json b/examples/projects/monorepo/packages/feature-account/introspection.json new file mode 100644 index 0000000..5d526e2 --- /dev/null +++ b/examples/projects/monorepo/packages/feature-account/introspection.json @@ -0,0 +1 @@ +{"message":"introspection plugin is not implemented"} diff --git a/internal/config.go b/internal/config.go index ab5bf60..1d558fa 100644 --- a/internal/config.go +++ b/internal/config.go @@ -8,17 +8,18 @@ import ( ) type Config struct { - Schema []string `yaml:"schema"` + Schemas []string `yaml:"schema"` Documents []string `yaml:"documents"` Overwrite bool `yaml:"overwrite"` - Generates map[string]struct{ - Plugins []string `yaml:"plugins"` - Preset string `yaml:"preset"` - } `yaml:"generates"` + Generates map[string]Generates `yaml:"generates"` } +type Generates struct{ + Plugins []string `yaml:"plugins"` + Preset string `yaml:"preset"` +} func (p *Project) GetConfig() Config { - if !reflect.ValueOf(p.config.Schema).IsZero() { + if !reflect.ValueOf(p.config.Schemas).IsZero() { return p.config } diff --git a/internal/converter.go b/internal/converter.go index 86a259b..917bc89 100644 --- a/internal/converter.go +++ b/internal/converter.go @@ -1,346 +1,4 @@ package internal -import ( - "errors" - "github.com/vektah/gqlparser/v2/ast" - "log/slog" - "strings" - "time" -) -/* -ConvertSchema converts a graphql schema to Typescript output -*/ -func ConvertSchema(schema *ast.Schema, output *strings.Builder) { - output.WriteString("/* Generated by faster-graphql-codegen on " + time.Now().Format(time.DateTime) + " */\n") - AddBaseTypes(output) - knownScalars := AddScalars(schema, output) - - for _, definition := range schema.Types { - if definition.BuiltIn { - continue - } - - err := ConvertDefinition(definition, output, knownScalars) - if err != nil { - slog.Error(err.Error(), "kind", definition.Kind, "name", definition.Name) - } else { - output.WriteString("\n") - } - } -} - -var builtInScalars = map[string]string{ - "String": "string", -} - -/* -AddScalars parses a schema and outputs a Scalars type, it also returns a list of scalars it found -*/ -func AddScalars(schema *ast.Schema, output *strings.Builder) []*ast.Definition { - output.WriteString("/** All built-in and custom scalars, mapped to their actual values */\n") - output.WriteString("export type Scalars = {\n") - - var scalars []*ast.Definition - - for _, definition := range schema.Types { - if definition.Kind == ast.Scalar { - scalars = append(scalars, definition) - - output.WriteString("\t" + definition.Name + ": ") - - if knownScalarType, ok := builtInScalars[definition.Name]; ok { - output.WriteString(knownScalarType) - } else { - output.WriteString("any") - } - - output.WriteString(";\n") - } - } - - output.WriteString("};\n") - - return scalars -} - -func AddBaseTypes(output *strings.Builder) { - output.WriteString("export type Maybe = T | null;\nexport type InputMaybe = Maybe;\nexport type Exact = { [K in keyof T]: T[K] };\nexport type MakeOptional = Omit & { [SubKey in K]?: Maybe };\nexport type MakeMaybe = Omit & { [SubKey in K]: Maybe };\nexport type MakeEmpty = { [_ in K]?: never };\nexport type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };") - output.WriteString("\n") -} - -func ConvertDefinition(definition *ast.Definition, output *strings.Builder, knownScalars []*ast.Definition) error { - switch definition.Kind { - case ast.Enum: - ConvertEnum(definition, output) - case ast.Union: - ConvertUnion(definition, output) - case ast.Interface: - ConvertInterface(definition, output, knownScalars) - case ast.Object: - ConvertObject(definition, output, knownScalars) - case ast.InputObject: - ConvertInputObject(definition, output, knownScalars) - case ast.Scalar: - default: - return errors.New("unknown definition kind") - } - - return nil -} - -func ConvertEnum(definition *ast.Definition, output *strings.Builder) { - enumName := ToCamel(definition.Name) - - WriteComment(definition, output) - - output.WriteString("export enum " + enumName + " {\n") - - for i, enumValue := range definition.EnumValues { - enumName := enumValue.Name - enumKey := ToUpper(enumValue.Name) - output.WriteString("\t" + enumKey + " = '" + enumName + "'") - - if i != len(definition.EnumValues)-1 { - output.WriteString(",") - } - output.WriteString("\n") - } - output.WriteString("}\n") -} - -func WriteComment(definition *ast.Definition, output *strings.Builder) { - comment := definition.Description - - if comment != "" { - output.WriteString("/* " + comment + " */\n") - } -} - -func WriteFieldComment(definition *ast.FieldDefinition, output *strings.Builder) { - comment := definition.Description - - if comment != "" { - output.WriteString("\t/* " + comment + " */\n") - } -} - -func WriteArgumentComment(definition *ast.ArgumentDefinition, output *strings.Builder) { - comment := definition.Description - - if comment != "" { - output.WriteString("\t/* " + comment + " */\n") - } -} - -func ConvertUnion(definition *ast.Definition, output *strings.Builder) { - unionName := ToCamel(definition.Name) - - WriteComment(definition, output) - - output.WriteString("export type " + unionName + " = ") - for i, alias := range definition.Types { - output.WriteString(alias) - - if i != len(definition.Types)-1 { - output.WriteString(" | ") - } else { - output.WriteString(";") - } - } - output.WriteString("\n") -} - -func ConvertInterface(definition *ast.Definition, output *strings.Builder, knownScalars []*ast.Definition) { - interfaceName := ToCamel(definition.Name) - - WriteComment(definition, output) - - output.WriteString("export type " + interfaceName + " = {\n") - for _, field := range definition.Fields { - WriteFieldComment(field, output) - - fieldName := field.Name - - output.WriteString("\t" + fieldName) - AddFieldType(field, output, "Maybe", knownScalars) - } - - output.WriteString("}\n") - - for _, field := range definition.Fields { - WriteFieldArguments(field, output, knownScalars, interfaceName) - } -} - -func ConvertObject(definition *ast.Definition, output *strings.Builder, knownScalars []*ast.Definition) { - interfaceName := ToCamel(definition.Name) - - WriteComment(definition, output) - - output.WriteString("export type " + interfaceName + " = ") - - // check implements - for _, implementsInterface := range definition.Interfaces { - output.WriteString(implementsInterface + " & ") - } - output.WriteString("{\n") - - output.WriteString("\t__typename: '" + interfaceName + "';\n") - for _, field := range definition.Fields { - if field.Name == "__type" || field.Name == "__schema" { - continue - } - - WriteFieldComment(field, output) - - fieldName := field.Name - - output.WriteString("\t" + fieldName) - AddFieldType(field, output, "Maybe", knownScalars) - } - - output.WriteString("}\n") - - for _, field := range definition.Fields { - WriteFieldArguments(field, output, knownScalars, interfaceName) - } -} - -func WriteFieldArguments(definition *ast.FieldDefinition, output *strings.Builder, knownScalars []*ast.Definition, rootName string) { - if len(definition.Arguments) == 0 { - return - } - - fieldName := ToCamel(definition.Name) - - WriteFieldComment(definition, output) - output.WriteString("export type " + rootName + fieldName + "Args = {\n") - for _, argument := range definition.Arguments { - WriteArgumentComment(argument, output) - output.WriteString("\t" + argument.Name) - AddArgumentType(argument, output, "Maybe", knownScalars) - } - output.WriteString("}\n") -} - -func ConvertInputObject(definition *ast.Definition, output *strings.Builder, knownScalars []*ast.Definition) { - interfaceName := ToCamel(definition.Name) - - WriteComment(definition, output) - - output.WriteString("export type " + interfaceName + " = {\n") - for _, field := range definition.Fields { - WriteFieldComment(field, output) - - fieldName := field.Name - - output.WriteString("\t" + fieldName) - AddFieldType(field, output, "InputMaybe", knownScalars) - } - - output.WriteString("}\n") -} - -func AddFieldType(definition *ast.FieldDefinition, output *strings.Builder, maybeType string, knownScalars []*ast.Definition) { - isNullable := !definition.Type.NonNull - - if isNullable { - output.WriteString("?: " + maybeType + "<") - } else { - output.WriteString(": ") - } - - isArray := definition.Type.Elem != nil - isElemNullable := true - - if isArray { - output.WriteString("Array<") - - isElemNullable = !definition.Type.Elem.NonNull - } - - if isArray && isElemNullable { - output.WriteString(maybeType + "<") - } - - // check if scalar is known - isScalarKnown := false - for _, scalar := range knownScalars { - if scalar.Name == definition.Type.Name() { - isScalarKnown = true - } - } - - if isScalarKnown { - output.WriteString("Scalars['" + definition.Type.Name() + "']") - } else { - output.WriteString(ToCamel(definition.Type.Name())) - } - - if isNullable { - output.WriteString(">") - } - - if isArray { - output.WriteString(">") - } - - if isArray && isElemNullable { - output.WriteString(">") - } - - output.WriteString(";\n") -} - -func AddArgumentType(definition *ast.ArgumentDefinition, output *strings.Builder, maybeType string, knownScalars []*ast.Definition) { - isNullable := !definition.Type.NonNull - - if isNullable { - output.WriteString("?: " + maybeType + "<") - } else { - output.WriteString(": ") - } - - isArray := definition.Type.Elem != nil - isElemNullable := true - - if isArray { - output.WriteString("Array<") - - isElemNullable = !definition.Type.Elem.NonNull - } - - if isArray && isElemNullable { - output.WriteString(maybeType + "<") - } - - // check if scalar is known - isScalarKnown := false - for _, scalar := range knownScalars { - if scalar.Name == definition.Type.Name() { - isScalarKnown = true - } - } - - if isScalarKnown { - output.WriteString("Scalars['" + definition.Type.Name() + "']") - } else { - output.WriteString(ToCamel(definition.Type.Name())) - } - - if isNullable { - output.WriteString(">") - } - - if isArray { - output.WriteString(">") - } - - if isArray && isElemNullable { - output.WriteString(">") - } - - output.WriteString(";\n") -} \ No newline at end of file diff --git a/internal/casing.go b/internal/plugins/casing.go similarity index 92% rename from internal/casing.go rename to internal/plugins/casing.go index 7facfe3..0b9a899 100644 --- a/internal/casing.go +++ b/internal/plugins/casing.go @@ -1,4 +1,4 @@ -package internal +package plugins import ( "github.com/iancoleman/strcase" diff --git a/internal/plugins/introspection.go b/internal/plugins/introspection.go new file mode 100644 index 0000000..3ba6d6e --- /dev/null +++ b/internal/plugins/introspection.go @@ -0,0 +1,5 @@ +package plugins + +func (p* PluginTask) Introspect() { + p.Output.WriteString("{\"message\":\"introspection plugin is not implemented\"}\n") +} \ No newline at end of file diff --git a/internal/plugins/plugins.go b/internal/plugins/plugins.go new file mode 100644 index 0000000..f1c2480 --- /dev/null +++ b/internal/plugins/plugins.go @@ -0,0 +1,24 @@ +package plugins + +import ( + "errors" + "github.com/vektah/gqlparser/v2/ast" + "strings" +) + +type PluginTask struct { + Schema *ast.Schema + Output *strings.Builder + Config interface{} +} + +/* +VerifyPlugin checks if a plugin is executable by faster-graphql-codegen +*/ +func VerifyPlugin(pluginName string, config interface{}) error { + if pluginName != "typescript" && pluginName != "introspection" { + return errors.New("unknown plugin") + } + + return nil +} \ No newline at end of file diff --git a/internal/plugins/typescript.go b/internal/plugins/typescript.go new file mode 100644 index 0000000..e7695da --- /dev/null +++ b/internal/plugins/typescript.go @@ -0,0 +1,350 @@ +package plugins + +import ( + "errors" + "github.com/vektah/gqlparser/v2/ast" + "log/slog" + "strings" + "time" +) + +func (p* PluginTask) Typescript() { + ConvertSchema(p.Schema, p.Output) +} + +/* +ConvertSchema converts a graphql schema to Typescript output +*/ +func ConvertSchema(schema *ast.Schema, output *strings.Builder) { + output.WriteString("/* Generated by faster-graphql-codegen on " + time.Now().Format(time.DateTime) + " */\n") + + AddBaseTypes(output) + knownScalars := AddScalars(schema, output) + + for _, definition := range schema.Types { + if definition.BuiltIn { + continue + } + + err := ConvertDefinition(definition, output, knownScalars) + if err != nil { + slog.Error(err.Error(), "kind", definition.Kind, "name", definition.Name) + } else { + output.WriteString("\n") + } + } +} + +var builtInScalars = map[string]string{ + "String": "string", +} + +/* +AddScalars parses a schema and outputs a Scalars type, it also returns a list of scalars it found +*/ +func AddScalars(schema *ast.Schema, output *strings.Builder) []*ast.Definition { + output.WriteString("/** All built-in and custom scalars, mapped to their actual values */\n") + output.WriteString("export type Scalars = {\n") + + var scalars []*ast.Definition + + for _, definition := range schema.Types { + if definition.Kind == ast.Scalar { + scalars = append(scalars, definition) + + output.WriteString("\t" + definition.Name + ": ") + + if knownScalarType, ok := builtInScalars[definition.Name]; ok { + output.WriteString(knownScalarType) + } else { + output.WriteString("any") + } + + output.WriteString(";\n") + } + } + + output.WriteString("};\n") + + return scalars +} + +func AddBaseTypes(output *strings.Builder) { + output.WriteString("export type Maybe = T | null;\nexport type InputMaybe = Maybe;\nexport type Exact = { [K in keyof T]: T[K] };\nexport type MakeOptional = Omit & { [SubKey in K]?: Maybe };\nexport type MakeMaybe = Omit & { [SubKey in K]: Maybe };\nexport type MakeEmpty = { [_ in K]?: never };\nexport type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };") + output.WriteString("\n") +} + +func ConvertDefinition(definition *ast.Definition, output *strings.Builder, knownScalars []*ast.Definition) error { + switch definition.Kind { + case ast.Enum: + ConvertEnum(definition, output) + case ast.Union: + ConvertUnion(definition, output) + case ast.Interface: + ConvertInterface(definition, output, knownScalars) + case ast.Object: + ConvertObject(definition, output, knownScalars) + case ast.InputObject: + ConvertInputObject(definition, output, knownScalars) + case ast.Scalar: + default: + return errors.New("unknown definition kind") + } + + return nil +} + +func ConvertEnum(definition *ast.Definition, output *strings.Builder) { + enumName := ToCamel(definition.Name) + + WriteComment(definition, output) + + output.WriteString("export enum " + enumName + " {\n") + + for i, enumValue := range definition.EnumValues { + enumName := enumValue.Name + enumKey := ToUpper(enumValue.Name) + output.WriteString("\t" + enumKey + " = '" + enumName + "'") + + if i != len(definition.EnumValues)-1 { + output.WriteString(",") + } + output.WriteString("\n") + } + output.WriteString("}\n") +} + +func WriteComment(definition *ast.Definition, output *strings.Builder) { + comment := definition.Description + + if comment != "" { + output.WriteString("/* " + comment + " */\n") + } +} + +func WriteFieldComment(definition *ast.FieldDefinition, output *strings.Builder) { + comment := definition.Description + + if comment != "" { + output.WriteString("\t/* " + comment + " */\n") + } +} + +func WriteArgumentComment(definition *ast.ArgumentDefinition, output *strings.Builder) { + comment := definition.Description + + if comment != "" { + output.WriteString("\t/* " + comment + " */\n") + } +} + +func ConvertUnion(definition *ast.Definition, output *strings.Builder) { + unionName := ToCamel(definition.Name) + + WriteComment(definition, output) + + output.WriteString("export type " + unionName + " = ") + for i, alias := range definition.Types { + output.WriteString(alias) + + if i != len(definition.Types)-1 { + output.WriteString(" | ") + } else { + output.WriteString(";") + } + } + output.WriteString("\n") +} + +func ConvertInterface(definition *ast.Definition, output *strings.Builder, knownScalars []*ast.Definition) { + interfaceName := ToCamel(definition.Name) + + WriteComment(definition, output) + + output.WriteString("export type " + interfaceName + " = {\n") + for _, field := range definition.Fields { + WriteFieldComment(field, output) + + fieldName := field.Name + + output.WriteString("\t" + fieldName) + AddFieldType(field, output, "Maybe", knownScalars) + } + + output.WriteString("}\n") + + for _, field := range definition.Fields { + WriteFieldArguments(field, output, knownScalars, interfaceName) + } +} + +func ConvertObject(definition *ast.Definition, output *strings.Builder, knownScalars []*ast.Definition) { + interfaceName := ToCamel(definition.Name) + + WriteComment(definition, output) + + output.WriteString("export type " + interfaceName + " = ") + + // check implements + for _, implementsInterface := range definition.Interfaces { + output.WriteString(implementsInterface + " & ") + } + output.WriteString("{\n") + + output.WriteString("\t__typename: '" + interfaceName + "';\n") + for _, field := range definition.Fields { + if field.Name == "__type" || field.Name == "__schema" { + continue + } + + WriteFieldComment(field, output) + + fieldName := field.Name + + output.WriteString("\t" + fieldName) + AddFieldType(field, output, "Maybe", knownScalars) + } + + output.WriteString("}\n") + + for _, field := range definition.Fields { + WriteFieldArguments(field, output, knownScalars, interfaceName) + } +} + +func WriteFieldArguments(definition *ast.FieldDefinition, output *strings.Builder, knownScalars []*ast.Definition, rootName string) { + if len(definition.Arguments) == 0 { + return + } + + fieldName := ToCamel(definition.Name) + + WriteFieldComment(definition, output) + output.WriteString("export type " + rootName + fieldName + "Args = {\n") + for _, argument := range definition.Arguments { + WriteArgumentComment(argument, output) + output.WriteString("\t" + argument.Name) + AddArgumentType(argument, output, "Maybe", knownScalars) + } + output.WriteString("}\n") +} + +func ConvertInputObject(definition *ast.Definition, output *strings.Builder, knownScalars []*ast.Definition) { + interfaceName := ToCamel(definition.Name) + + WriteComment(definition, output) + + output.WriteString("export type " + interfaceName + " = {\n") + for _, field := range definition.Fields { + WriteFieldComment(field, output) + + fieldName := field.Name + + output.WriteString("\t" + fieldName) + AddFieldType(field, output, "InputMaybe", knownScalars) + } + + output.WriteString("}\n") +} + +func AddFieldType(definition *ast.FieldDefinition, output *strings.Builder, maybeType string, knownScalars []*ast.Definition) { + isNullable := !definition.Type.NonNull + + if isNullable { + output.WriteString("?: " + maybeType + "<") + } else { + output.WriteString(": ") + } + + isArray := definition.Type.Elem != nil + isElemNullable := true + + if isArray { + output.WriteString("Array<") + + isElemNullable = !definition.Type.Elem.NonNull + } + + if isArray && isElemNullable { + output.WriteString(maybeType + "<") + } + + // check if scalar is known + isScalarKnown := false + for _, scalar := range knownScalars { + if scalar.Name == definition.Type.Name() { + isScalarKnown = true + } + } + + if isScalarKnown { + output.WriteString("Scalars['" + definition.Type.Name() + "']") + } else { + output.WriteString(ToCamel(definition.Type.Name())) + } + + if isNullable { + output.WriteString(">") + } + + if isArray { + output.WriteString(">") + } + + if isArray && isElemNullable { + output.WriteString(">") + } + + output.WriteString(";\n") +} + +func AddArgumentType(definition *ast.ArgumentDefinition, output *strings.Builder, maybeType string, knownScalars []*ast.Definition) { + isNullable := !definition.Type.NonNull + + if isNullable { + output.WriteString("?: " + maybeType + "<") + } else { + output.WriteString(": ") + } + + isArray := definition.Type.Elem != nil + isElemNullable := true + + if isArray { + output.WriteString("Array<") + + isElemNullable = !definition.Type.Elem.NonNull + } + + if isArray && isElemNullable { + output.WriteString(maybeType + "<") + } + + // check if scalar is known + isScalarKnown := false + for _, scalar := range knownScalars { + if scalar.Name == definition.Type.Name() { + isScalarKnown = true + } + } + + if isScalarKnown { + output.WriteString("Scalars['" + definition.Type.Name() + "']") + } else { + output.WriteString(ToCamel(definition.Type.Name())) + } + + if isNullable { + output.WriteString(">") + } + + if isArray { + output.WriteString(">") + } + + if isArray && isElemNullable { + output.WriteString(">") + } + + output.WriteString(";\n") +} \ No newline at end of file diff --git a/internal/project.go b/internal/project.go index 6f7704e..0d2ce0a 100644 --- a/internal/project.go +++ b/internal/project.go @@ -1,8 +1,10 @@ package internal import ( + "github.com/simse/faster-graphql-codegen/internal/plugins" "github.com/vektah/gqlparser/v2/ast" "io/fs" + "log/slog" "os" "path" "path/filepath" @@ -30,7 +32,7 @@ func FindProjects(rootDir string, walkDir func(string, fs.WalkDirFunc) error) [] // prime project config := project.GetConfig() - project.Schemas = config.Schema + project.Schemas = config.Schemas projects = append(projects, project) } @@ -50,7 +52,7 @@ SchemaKey generates a string get is unique to a combination of schema documents func (p *Project) SchemaKey() string { projectConfig := p.GetConfig() - sortedSchemas := slices.Clone(projectConfig.Schema) + sortedSchemas := slices.Clone(projectConfig.Schemas) slices.Sort(sortedSchemas) return strings.Join(sortedSchemas, ",") @@ -127,32 +129,37 @@ func (e *ExecutionContext) Execute() { // execute all generation tasks config := project.GetConfig() - for destination, _ := range config.Generates { + for destination, destinationConfig := range config.Generates { wg.Add(1) go func() { defer wg.Done() + // ensure output dir exists destinationFile := path.Join(project.RootDir, destination) - dirCreationErr := EnsureDir(destinationFile) if dirCreationErr != nil { panic(dirCreationErr) } + // create output string in memory output := strings.Builder{} - ConvertSchema(schema, &output) + e.ExecuteDestinationTasks(destinationConfig, &output, schema, project) + + // create output file outputFile, openErr := os.Create(destinationFile) if openErr != nil { panic(openErr) } + // write output file _, writeErr := outputFile.WriteString(output.String()) if writeErr != nil { panic(writeErr) } + // close output files err := outputFile.Close() if err != nil { panic(err) @@ -164,6 +171,36 @@ func (e *ExecutionContext) Execute() { wg.Wait() } +func (e *ExecutionContext) ExecuteDestinationTasks( + destinationConfig Generates, + output *strings.Builder, + schema *ast.Schema, + project Project, +) { + task := plugins.PluginTask{ + Schema: schema, + Output: output, + Config: project.GetConfig(), + } + + // execute plugins + for _, plugin := range destinationConfig.Plugins { + if pluginErr := plugins.VerifyPlugin(plugin, destinationConfig); pluginErr != nil { + slog.Error("unknown plugin", "plugin", plugin) + } + + // slog.Info(plugin) + + if plugin == "typescript" { + task.Typescript() + } + + if plugin == "introspection" { + task.Introspect() + } + } +} + func EnsureDir(filePath string) error { dir := filepath.Dir(filePath) From 656dcf50e2335abd6f46e3d9757e0066571a8511 Mon Sep 17 00:00:00 2001 From: simse Date: Fri, 13 Sep 2024 18:04:15 +0100 Subject: [PATCH 2/2] feat: Add better CLI --- examples/projects/monorepo/.gitignore | 3 +- go.mod | 11 +++ go.sum | 24 ++++++ internal/project.go | 19 ++++- main.go | 110 +++++++++++++++----------- 5 files changed, 115 insertions(+), 52 deletions(-) diff --git a/examples/projects/monorepo/.gitignore b/examples/projects/monorepo/.gitignore index 64436b9..7e74e7e 100644 --- a/examples/projects/monorepo/.gitignore +++ b/examples/projects/monorepo/.gitignore @@ -1 +1,2 @@ -baseTypes.ts \ No newline at end of file +baseTypes.ts +introspection.json \ No newline at end of file diff --git a/go.mod b/go.mod index 1587979..069f11e 100644 --- a/go.mod +++ b/go.mod @@ -3,17 +3,28 @@ module github.com/simse/faster-graphql-codegen go 1.23.1 require ( + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/agnivade/levenshtein v1.1.1 // indirect + github.com/briandowns/spinner v1.23.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dop251/goja v0.0.0-20240828124009-016eb7256539 // indirect + github.com/fatih/color v1.7.0 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect github.com/iancoleman/strcase v0.3.0 // indirect github.com/lmittmann/tint v1.0.5 // indirect + github.com/mattn/go-colorable v0.1.2 // indirect + github.com/mattn/go-isatty v0.0.8 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/stretchr/testify v1.9.0 // indirect + github.com/vbauerster/mpb/v8 v8.8.3 // indirect github.com/vektah/gqlparser/v2 v2.5.16 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/term v0.1.0 // indirect golang.org/x/text v0.18.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 721bde1..bbae584 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,12 @@ +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/briandowns/spinner v1.23.1 h1:t5fDPmScwUjozhDj4FA46p5acZWIPXYE30qW2Ptu650= +github.com/briandowns/spinner v1.23.1/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= @@ -8,6 +14,8 @@ github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yA github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dop251/goja v0.0.0-20240828124009-016eb7256539 h1:YIxvsQAoCLGScK2c9ag+4sFCgiQFpMzywJG6dQZFu9k= github.com/dop251/goja v0.0.0-20240828124009-016eb7256539/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= @@ -16,12 +24,28 @@ github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSAS github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/lmittmann/tint v1.0.5 h1:NQclAutOfYsqs2F1Lenue6OoWCajs5wJcP3DfWVpePw= github.com/lmittmann/tint v1.0.5/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/vbauerster/mpb/v8 v8.8.3 h1:dTOByGoqwaTJYPubhVz3lO5O6MK553XVgUo33LdnNsQ= +github.com/vbauerster/mpb/v8 v8.8.3/go.mod h1:JfCCrtcMsJwP6ZwMn9e5LMnNyp3TVNpUWWkN+nd4EWk= github.com/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0vy5p8= github.com/vektah/gqlparser/v2 v2.5.16/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/project.go b/internal/project.go index 0d2ce0a..e8294d7 100644 --- a/internal/project.go +++ b/internal/project.go @@ -20,10 +20,19 @@ type Project struct { config Config } -func FindProjects(rootDir string, walkDir func(string, fs.WalkDirFunc) error) []Project { +func FindProjects(rootDir string, walkDir func(string, fs.WalkDirFunc) error) ([]Project, error) { + // check if path exists + if _, err := os.Stat(rootDir); err != nil { + return nil, err + } + var projects []Project err := walkDir(rootDir, func(path string, d fs.DirEntry, err error) error { + if d.IsDir() && d.Name() == "node_modules" { + return fs.SkipDir + } + if strings.HasSuffix(path, "codegen.ts") || strings.HasSuffix(path, "codegen.yml") { project := Project{ RootDir: filepath.Dir(path), @@ -40,10 +49,10 @@ func FindProjects(rootDir string, walkDir func(string, fs.WalkDirFunc) error) [] return nil }) if err != nil { - panic(err) + return nil, err } - return projects + return projects, nil } /* @@ -82,7 +91,7 @@ func (e *ExecutionContext) GetSchema(key string) *ast.Schema { /* LoadSchemas will find every project with a unique list of schemas and load those to cache. */ -func (e *ExecutionContext) LoadSchemas() { +func (e *ExecutionContext) LoadSchemas() int { // find unique schemas var uniqueSchemas []string var projectsToLoad []Project @@ -118,6 +127,8 @@ func (e *ExecutionContext) LoadSchemas() { } wg.Wait() + + return len(uniqueSchemas) } func (e *ExecutionContext) Execute() { diff --git a/main.go b/main.go index 5f182b3..b6d99d7 100644 --- a/main.go +++ b/main.go @@ -1,62 +1,78 @@ package main import ( - "github.com/lmittmann/tint" + "errors" + "fmt" + "github.com/briandowns/spinner" "github.com/simse/faster-graphql-codegen/internal" - "log/slog" "os" "path/filepath" "time" ) func main() { - // set up coloured logging - w := os.Stderr - - // set global logger with custom options - slog.SetDefault(slog.New( - tint.NewHandler(w, &tint.Options{ - Level: slog.LevelDebug, - TimeFormat: time.TimeOnly, - }), - )) - - /*timeStart := time.Now() - schema, err := internal.LoadSchema("examples/github/github.graphql") - if err != nil { - panic(err) - } - loadSchemaTime := time.Since(timeStart) - - convertTimeStart := time.Now() - output := strings.Builder{} - internal.ConvertSchema(schema, &output) - convertSchemaTime := time.Since(convertTimeStart) - - writeTypesTimeStart := time.Now() - outputFile, openErr := os.Create("output/github/baseTypes.ts") - if openErr != nil { - panic(err) - } - - _, writeErr := outputFile.WriteString(output.String()) - if writeErr != nil { - panic(err) - } - writeTypesTime := time.Since(writeTypesTimeStart) - - outputFile.Close() - slog.Info("finished codegen", "duration", time.Since(timeStart), "load_duration", loadSchemaTime, "convert_duration", convertSchemaTime, "write_duration", writeTypesTime)*/ timeStart := time.Now() - projects := internal.FindProjects("./examples/projects/monorepo", filepath.WalkDir) - slog.Info("search for packages using codegen complete", "found_projects", len(projects)) - /*for _, project := range projects { - internal.ExecuteProject(project) - }*/ + projects := findProjects() + executionContext := internal.ExecutionContext{} executionContext.SetProjects(projects) - executionContext.LoadSchemas() - executionContext.Execute() - slog.Info("finished all codegen tasks", "duration", time.Since(timeStart)) + + loadSchemas(&executionContext) + execute(&executionContext, timeStart) +} + +func findProjects() []internal.Project { + // get input folder + searchFolder := "." + argsWithoutProg := os.Args[1:] + + if len(argsWithoutProg) > 0 { + searchFolder = argsWithoutProg[0] + } + + s := spinner.New(spinner.CharSets[14], 100*time.Millisecond, spinner.WithWriter(os.Stderr)) + s.Suffix = " Finding projects using codegen" + + s.Start() + projects, err := internal.FindProjects(searchFolder, filepath.WalkDir) + + if err != nil { + if errors.Is(err, os.ErrNotExist) { + s.FinalMSG = "✗ Input folder does not exist\n" + s.Stop() + } else { + s.FinalMSG = "✗ Unknown error while searching for projects: " + err.Error() + "\n" + s.Stop() + } + + os.Exit(1) + } else { + s.FinalMSG = fmt.Sprintf("✓ Found %d projects\n", len(projects)) + s.Stop() + } + + return projects +} + +func loadSchemas(e *internal.ExecutionContext) { + s := spinner.New(spinner.CharSets[14], 100*time.Millisecond, spinner.WithWriter(os.Stderr)) + s.Suffix = " Loading graphql schemas" + + s.Start() + schemasLoaded := e.LoadSchemas() + + s.FinalMSG = fmt.Sprintf("✓ Loaded %d unique schemas\n", schemasLoaded) + s.Stop() +} + +func execute(e *internal.ExecutionContext, timeStart time.Time) { + s := spinner.New(spinner.CharSets[14], 100*time.Millisecond, spinner.WithWriter(os.Stderr)) + s.Suffix = " Executing codegen tasks" + + s.Start() + e.Execute() + + s.FinalMSG = fmt.Sprintf("✓ Codegen completed in %s\n", time.Since(timeStart).String()) + s.Stop() } \ No newline at end of file