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)