From 913a2fae98ef5849e299b2ac52cb01453fcf58cc Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:33:32 +0100 Subject: [PATCH 1/4] feat: first work --- packages/wabe/src/server/generateCodegen.ts | 114 ++++++++++++++++---- packages/wabe/src/server/index.ts | 4 +- 2 files changed, 96 insertions(+), 22 deletions(-) diff --git a/packages/wabe/src/server/generateCodegen.ts b/packages/wabe/src/server/generateCodegen.ts index 30849107..7efb03ad 100644 --- a/packages/wabe/src/server/generateCodegen.ts +++ b/packages/wabe/src/server/generateCodegen.ts @@ -321,19 +321,53 @@ const generateWabeMutationsAndQueriesTypes = (resolver: TypeResolver) => { } } -const wabeClassRecordToString = (wabeClass: Record>) => { +export type CodegenFormatOptions = { + indent?: string + comma?: boolean + semi?: boolean + quote?: 'single' | 'double' + formatCommand?: string +} + +const wabeClassRecordToString = ( + wabeClass: Record>, + options?: CodegenFormatOptions, +) => { + const indent = options?.indent ?? '\t' + const endChar = options?.semi ? ';' : options?.comma ? ',' : '' + const lastEndChar = options?.semi + ? ';' + : options?.comma === true + ? ',' + : options?.comma === false + ? '' + : '' + return Object.entries(wabeClass).reduce((acc, [className, fields]) => { + const fieldsLength = Object.keys(fields).length + if (fieldsLength === 0) return `${acc}export type ${className} = {}\n\n` + return `${acc}export type ${className} = {\n${Object.entries(fields) - .map(([fieldName, fieldType]) => `\t${fieldName.replace('undefined', '?')}: ${fieldType}`) - .join(',\n')}\n}\n\n` + .map( + ([fieldName, fieldType]) => `${indent}${fieldName.replace('undefined', '?')}: ${fieldType}`, + ) + .join(`${endChar}\n`)}${fieldsLength > 0 ? lastEndChar : ''}\n}\n\n` }, '') } -const wabeEnumRecordToString = (wabeEnum: Record>) => { +const wabeEnumRecordToString = ( + wabeEnum: Record>, + options?: CodegenFormatOptions, +) => { + const indent = options?.indent ?? '\t' + const endChar = options?.semi ? ';' : (options?.comma ?? true) ? ',' : '' + const quoteString = options?.quote === 'double' ? '"' : "'" + return Object.entries(wabeEnum).reduce((acc, [enumName, values]) => { + const valuesLength = Object.keys(values).length return `${acc}export enum ${enumName} {\n${Object.entries(values) - .map(([valueName, value]) => `\t${valueName} = "${value}"`) - .join(',\n')}\n}\n\n` + .map(([valueName, value]) => `${indent}${valueName} = ${quoteString}${value}${quoteString}`) + .join(`${endChar}\n`)}${valuesLength > 0 ? endChar : ''}\n}\n\n` }, '') } @@ -347,25 +381,31 @@ const generateWabeDevTypes = ({ scalars, enums, classes, + options, }: { enums?: EnumInterface[] scalars?: ScalarInterface[] classes: ClassInterface[] + options?: CodegenFormatOptions }) => { + const indent = options?.indent ?? '\t' + const endChar = options?.semi ? ';' : options?.comma ? ',' : '' + const quoteString = options?.quote === 'double' ? '"' : "'" + // Scalars - const listOfScalars = scalars?.map((scalar) => `"${scalar.name}"`) || [] + const listOfScalars = scalars?.map((scalar) => `${quoteString}${scalar.name}${quoteString}`) || [] const wabeScalarType = listOfScalars.length > 0 ? `export type WabeSchemaScalars = ${listOfScalars.join(' | ')}` - : 'export type WabeSchemaScalars = ""' + : `export type WabeSchemaScalars = ${quoteString}${quoteString}` // Enums const wabeEnumsGlobalTypes = enums?.map((wabeEnum) => `${wabeEnum.name}: ${wabeEnum.name}`) || [] const wabeEnumsGlobalTypesString = wabeEnumsGlobalTypes.length > 0 - ? `export type WabeSchemaEnums = {\n\t${wabeEnumsGlobalTypes.join(',\n\t')}\n}` + ? `export type WabeSchemaEnums = {\n${indent}${wabeEnumsGlobalTypes.join(`${endChar}\n${indent}`)}${endChar}\n}` : '' // Classes @@ -373,16 +413,22 @@ const generateWabeDevTypes = ({ .map((schema) => `${schema.name}: ${schema.name}`) .filter((schema) => schema) - const globalWabeTypeString = `export type WabeSchemaTypes = {\n\t${allNames.join(',\n\t')}\n}` + const globalWabeTypeString = + allNames.length > 0 + ? `export type WabeSchemaTypes = {\n${indent}${allNames.join(`${endChar}\n${indent}`)}${endChar}\n}` + : '' // Where const allWhereNames = classes .map((schema) => `${schema.name}: Where${firstLetterUpperCase(schema.name)}`) .filter((schema) => schema) - const globalWabeWhereTypeString = `export type WabeSchemaWhereTypes = {\n\t${allWhereNames.join( - ',\n\t', - )}\n}` + const globalWabeWhereTypeString = + allWhereNames.length > 0 + ? `export type WabeSchemaWhereTypes = {\n${indent}${allWhereNames.join( + `${endChar}\n${indent}`, + )}${endChar}\n}` + : '' return `${wabeScalarType}\n\n${wabeEnumsGlobalTypesString}\n\n${globalWabeTypeString}\n\n${globalWabeWhereTypeString}` } @@ -391,34 +437,50 @@ export const generateCodegen = async ({ schema, path, graphqlSchema, + options, }: { schema: SchemaInterface path: string graphqlSchema: GraphQLSchema + options?: CodegenFormatOptions }) => { - const graphqlSchemaContent = printSchema(graphqlSchema) + let graphqlSchemaContent = printSchema(graphqlSchema) + + const indentStr = options?.indent ?? '\t' + if (indentStr !== ' ') { + graphqlSchemaContent = graphqlSchemaContent.replaceAll(' ', indentStr) + } + + graphqlSchemaContent = graphqlSchemaContent.replace(/"""([^\n"]+)"""/g, '"""\n$1\n"""') const wabeClasses = generateWabeTypes(schema.classes || []) const wabeWhereTypes = generateWabeWhereTypes(schema.classes || []) const mutationsAndQueries = generateWabeMutationsAndQueriesTypes(schema.resolvers || {}) - const wabeEnumsInString = wabeEnumRecordToString(generateWabeEnumTypes(schema.enums || [])) + const wabeEnumsInString = wabeEnumRecordToString( + generateWabeEnumTypes(schema.enums || []), + options, + ) const wabeScalarsInString = wabeScalarRecordToString( generateWabeScalarTypes(schema.scalars || []), ) - const wabeObjectsInString = wabeClassRecordToString({ - ...wabeClasses, - ...wabeWhereTypes, - ...mutationsAndQueries, - }) + const wabeObjectsInString = wabeClassRecordToString( + { + ...wabeClasses, + ...wabeWhereTypes, + ...mutationsAndQueries, + }, + options, + ) const wabeDevTypes = generateWabeDevTypes({ scalars: schema.scalars, enums: schema.enums, classes: schema.classes || [], + options, }) - const wabeTsContent = `${wabeEnumsInString}${wabeScalarsInString}${wabeObjectsInString}${wabeDevTypes}` + const wabeTsContent = `${wabeEnumsInString}${wabeScalarsInString}${wabeObjectsInString}${wabeDevTypes}\n` try { const contentOfGraphqlSchema = (await readFile(`${path}/schema.graphql`)).toString() @@ -430,4 +492,14 @@ export const generateCodegen = async ({ await writeFile(`${path}/wabe.ts`, wabeTsContent) await writeFile(`${path}/schema.graphql`, graphqlSchemaContent) + + if (options?.formatCommand) { + const { exec } = await import('node:child_process') + const { promisify } = await import('node:util') + try { + await promisify(exec)(options.formatCommand) + } catch (e) { + console.error('Error running formatCommand on codegen files:', e) + } + } } diff --git a/packages/wabe/src/server/index.ts b/packages/wabe/src/server/index.ts index e6dc0f2e..d34f4764 100644 --- a/packages/wabe/src/server/index.ts +++ b/packages/wabe/src/server/index.ts @@ -6,7 +6,7 @@ import { GraphQLSchema as WabeGraphQLSchema } from '../graphql' import type { AuthenticationConfig } from '../authentication/interface' import { type WabeRoute, defaultRoutes } from './routes' import { type Hook, getDefaultHooks } from '../hooks' -import { generateCodegen } from './generateCodegen' +import { CodegenFormatOptions, generateCodegen } from './generateCodegen' import { defaultAuthenticationMethods } from '../authentication/defaultAuthentication' import { Wobe, cors, rateLimit } from 'wobe' import type { Context, CorsOptions, RateLimitOptions } from 'wobe' @@ -47,6 +47,7 @@ export interface WabeConfig { | { enabled: true path: string + formatOptions?: CodegenFormatOptions } | { enabled?: false } authentication?: AuthenticationConfig @@ -253,6 +254,7 @@ export class Wabe { path: this.config.codegen.path, schema: wabeSchema.schema, graphqlSchema: this.config.graphqlSchema, + options: this.config.codegen.formatOptions, }) // If we just want codegen we exit before server created. From 38849c26c41ed30488f9e1bf14098e58f1d7dfc0 Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:46:00 +0100 Subject: [PATCH 2/4] feat: more --- packages/wabe/src/server/generateCodegen.ts | 78 +++++++++++++++++++-- 1 file changed, 72 insertions(+), 6 deletions(-) diff --git a/packages/wabe/src/server/generateCodegen.ts b/packages/wabe/src/server/generateCodegen.ts index 7efb03ad..9b66bf08 100644 --- a/packages/wabe/src/server/generateCodegen.ts +++ b/packages/wabe/src/server/generateCodegen.ts @@ -1,4 +1,4 @@ -import { type GraphQLSchema, printSchema } from 'graphql' +import { type GraphQLSchema, parse, print, printSchema } from 'graphql' import { writeFile, readFile } from 'node:fs/promises' import type { ClassInterface, @@ -327,6 +327,51 @@ export type CodegenFormatOptions = { semi?: boolean quote?: 'single' | 'double' formatCommand?: string + graphqlPrintWidth?: number + graphqlFinalNewline?: boolean + graphqlSemanticCompare?: boolean +} + +const normalizeGraphqlSchemaForComparison = (schemaContent: string) => { + try { + return print(parse(schemaContent)) + } catch { + return schemaContent + } +} + +const wrapLongGraphqlFieldArguments = ({ + content, + indent, + printWidth, +}: { + content: string + indent: string + printWidth: number +}) => { + return content + .split('\n') + .map((line) => { + if (line.length <= printWidth) return line + + const fieldWithArgsMatch = line.match(/^(\s*)([_A-Za-z][_0-9A-Za-z]*)\((.+)\):\s*(.+)$/) + if (!fieldWithArgsMatch) return line + + const [, fieldIndent = '', fieldName = '', argsString = '', returnType = ''] = + fieldWithArgsMatch + if (!fieldName || !argsString || !returnType) return line + const args = argsString + .split(',') + .map((arg) => arg.trim()) + .filter((arg) => arg.length > 0) + + if (args.length <= 1) return line + + return `${fieldIndent}${fieldName}(\n${args + .map((arg) => `${fieldIndent}${indent}${arg}`) + .join('\n')}\n${fieldIndent}): ${returnType}` + }) + .join('\n') } const wabeClassRecordToString = ( @@ -447,12 +492,26 @@ export const generateCodegen = async ({ let graphqlSchemaContent = printSchema(graphqlSchema) const indentStr = options?.indent ?? '\t' + const graphqlPrintWidth = options?.graphqlPrintWidth ?? 100 + const shouldEnsureFinalNewline = options?.graphqlFinalNewline ?? true + const shouldUseSemanticComparison = options?.graphqlSemanticCompare ?? true + if (indentStr !== ' ') { graphqlSchemaContent = graphqlSchemaContent.replaceAll(' ', indentStr) } graphqlSchemaContent = graphqlSchemaContent.replace(/"""([^\n"]+)"""/g, '"""\n$1\n"""') + graphqlSchemaContent = wrapLongGraphqlFieldArguments({ + content: graphqlSchemaContent, + indent: indentStr, + printWidth: graphqlPrintWidth, + }) + + if (shouldEnsureFinalNewline && !graphqlSchemaContent.endsWith('\n')) { + graphqlSchemaContent += '\n' + } + const wabeClasses = generateWabeTypes(schema.classes || []) const wabeWhereTypes = generateWabeWhereTypes(schema.classes || []) const mutationsAndQueries = generateWabeMutationsAndQueriesTypes(schema.resolvers || {}) @@ -482,16 +541,23 @@ export const generateCodegen = async ({ const wabeTsContent = `${wabeEnumsInString}${wabeScalarsInString}${wabeObjectsInString}${wabeDevTypes}\n` + let shouldWriteGraphqlSchema = true try { const contentOfGraphqlSchema = (await readFile(`${path}/schema.graphql`)).toString() - - // We will need to find a better way to avoid infinite loop of loading - // Better solution will be that bun implements watch ignores) - if (!process.env.CODEGEN && contentOfGraphqlSchema === graphqlSchemaContent.toString()) return + const strictComparisonIsEqual = contentOfGraphqlSchema === graphqlSchemaContent + const semanticComparisonIsEqual = + shouldUseSemanticComparison && + normalizeGraphqlSchemaForComparison(contentOfGraphqlSchema) === + normalizeGraphqlSchemaForComparison(graphqlSchemaContent) + + // Avoid formatting-only writes and file-watch loops. + shouldWriteGraphqlSchema = !(strictComparisonIsEqual || semanticComparisonIsEqual) } catch {} await writeFile(`${path}/wabe.ts`, wabeTsContent) - await writeFile(`${path}/schema.graphql`, graphqlSchemaContent) + if (shouldWriteGraphqlSchema) { + await writeFile(`${path}/schema.graphql`, graphqlSchemaContent) + } if (options?.formatCommand) { const { exec } = await import('node:child_process') From 124e923b24cfac8f254a3a5ef7e6792764a96429 Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:52:23 +0100 Subject: [PATCH 3/4] fix: clean code --- packages/wabe/src/server/generateCodegen.ts | 92 +++++++++------------ 1 file changed, 40 insertions(+), 52 deletions(-) diff --git a/packages/wabe/src/server/generateCodegen.ts b/packages/wabe/src/server/generateCodegen.ts index 9b66bf08..e0350d55 100644 --- a/packages/wabe/src/server/generateCodegen.ts +++ b/packages/wabe/src/server/generateCodegen.ts @@ -138,12 +138,7 @@ const generateWabeTypes = (classes: ClassInterface[]) => { {} as Record, ) - const objects = objectsToLoad.reduce((acc2, object) => { - return { - ...acc2, - ...object, - } - }, {}) + const objects = mergeNestedStringRecords(objectsToLoad) return { ...acc, @@ -187,12 +182,7 @@ const generateWabeWhereTypes = (classes: ClassInterface[]) => { {} as Record, ) - const objects = objectsToLoad.reduce((acc2, object) => { - return { - ...acc2, - ...object, - } - }, {}) + const objects = mergeNestedStringRecords(objectsToLoad) return { ...acc, @@ -274,12 +264,7 @@ const generateWabeMutationOrQueryInput = ( {} as Record, ) - const objects = objectsToLoad.reduce((acc2, object) => { - return { - ...acc2, - ...object, - } - }, {}) + const objects = mergeNestedStringRecords(objectsToLoad) return { ...(isMutation @@ -327,9 +312,9 @@ export type CodegenFormatOptions = { semi?: boolean quote?: 'single' | 'double' formatCommand?: string - graphqlPrintWidth?: number - graphqlFinalNewline?: boolean - graphqlSemanticCompare?: boolean + printWidth?: number + finalNewline?: boolean + semanticCompare?: boolean } const normalizeGraphqlSchemaForComparison = (schemaContent: string) => { @@ -340,6 +325,16 @@ const normalizeGraphqlSchemaForComparison = (schemaContent: string) => { } } +const mergeNestedStringRecords = (records: Array>>) => { + return records.reduce( + (acc, currentRecord) => ({ + ...acc, + ...currentRecord, + }), + {} as Record>, + ) +} + const wrapLongGraphqlFieldArguments = ({ content, indent, @@ -380,13 +375,6 @@ const wabeClassRecordToString = ( ) => { const indent = options?.indent ?? '\t' const endChar = options?.semi ? ';' : options?.comma ? ',' : '' - const lastEndChar = options?.semi - ? ';' - : options?.comma === true - ? ',' - : options?.comma === false - ? '' - : '' return Object.entries(wabeClass).reduce((acc, [className, fields]) => { const fieldsLength = Object.keys(fields).length @@ -396,7 +384,7 @@ const wabeClassRecordToString = ( .map( ([fieldName, fieldType]) => `${indent}${fieldName.replace('undefined', '?')}: ${fieldType}`, ) - .join(`${endChar}\n`)}${fieldsLength > 0 ? lastEndChar : ''}\n}\n\n` + .join(`${endChar}\n`)}${endChar}\n}\n\n` }, '') } @@ -492,9 +480,9 @@ export const generateCodegen = async ({ let graphqlSchemaContent = printSchema(graphqlSchema) const indentStr = options?.indent ?? '\t' - const graphqlPrintWidth = options?.graphqlPrintWidth ?? 100 - const shouldEnsureFinalNewline = options?.graphqlFinalNewline ?? true - const shouldUseSemanticComparison = options?.graphqlSemanticCompare ?? true + const printWidth = options?.printWidth ?? 100 + const shouldEnsureFinalNewline = options?.finalNewline ?? true + const shouldUseSemanticComparison = options?.semanticCompare ?? true if (indentStr !== ' ') { graphqlSchemaContent = graphqlSchemaContent.replaceAll(' ', indentStr) @@ -505,24 +493,24 @@ export const generateCodegen = async ({ graphqlSchemaContent = wrapLongGraphqlFieldArguments({ content: graphqlSchemaContent, indent: indentStr, - printWidth: graphqlPrintWidth, + printWidth, }) if (shouldEnsureFinalNewline && !graphqlSchemaContent.endsWith('\n')) { graphqlSchemaContent += '\n' } - const wabeClasses = generateWabeTypes(schema.classes || []) - const wabeWhereTypes = generateWabeWhereTypes(schema.classes || []) - const mutationsAndQueries = generateWabeMutationsAndQueriesTypes(schema.resolvers || {}) + const classes = schema.classes ?? [] + const resolvers = schema.resolvers ?? {} + const enums = schema.enums ?? [] + const scalars = schema.scalars ?? [] - const wabeEnumsInString = wabeEnumRecordToString( - generateWabeEnumTypes(schema.enums || []), - options, - ) - const wabeScalarsInString = wabeScalarRecordToString( - generateWabeScalarTypes(schema.scalars || []), - ) + const wabeClasses = generateWabeTypes(classes) + const wabeWhereTypes = generateWabeWhereTypes(classes) + const mutationsAndQueries = generateWabeMutationsAndQueriesTypes(resolvers) + + const wabeEnumsInString = wabeEnumRecordToString(generateWabeEnumTypes(enums), options) + const wabeScalarsInString = wabeScalarRecordToString(generateWabeScalarTypes(scalars)) const wabeObjectsInString = wabeClassRecordToString( { ...wabeClasses, @@ -533,9 +521,9 @@ export const generateCodegen = async ({ ) const wabeDevTypes = generateWabeDevTypes({ - scalars: schema.scalars, - enums: schema.enums, - classes: schema.classes || [], + scalars, + enums, + classes, options, }) @@ -544,14 +532,14 @@ export const generateCodegen = async ({ let shouldWriteGraphqlSchema = true try { const contentOfGraphqlSchema = (await readFile(`${path}/schema.graphql`)).toString() - const strictComparisonIsEqual = contentOfGraphqlSchema === graphqlSchemaContent - const semanticComparisonIsEqual = - shouldUseSemanticComparison && - normalizeGraphqlSchemaForComparison(contentOfGraphqlSchema) === - normalizeGraphqlSchemaForComparison(graphqlSchemaContent) + const schemasAreEqual = + contentOfGraphqlSchema === graphqlSchemaContent || + (shouldUseSemanticComparison && + normalizeGraphqlSchemaForComparison(contentOfGraphqlSchema) === + normalizeGraphqlSchemaForComparison(graphqlSchemaContent)) // Avoid formatting-only writes and file-watch loops. - shouldWriteGraphqlSchema = !(strictComparisonIsEqual || semanticComparisonIsEqual) + shouldWriteGraphqlSchema = !schemasAreEqual } catch {} await writeFile(`${path}/wabe.ts`, wabeTsContent) From 7b8f1fee727af085f0c8ff51b6282c2a8cf0c760 Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:52:29 +0100 Subject: [PATCH 4/4] fix: format --- packages/wabe-buns3/package.json | 4 +++- packages/wabe-mongodb-launcher/package.json | 4 +++- packages/wabe-postgres/package.json | 2 +- packages/wabe-resend/package.json | 4 +++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/wabe-buns3/package.json b/packages/wabe-buns3/package.json index 609775e9..c6339985 100644 --- a/packages/wabe-buns3/package.json +++ b/packages/wabe-buns3/package.json @@ -18,7 +18,9 @@ "type": "git", "url": "git+https://github.com/palixir/wabe.git" }, - "files": ["dist"], + "files": [ + "dist" + ], "main": "dist/index.js", "scripts": { "build": "bun --filter wabe-build build:package $(pwd) bun", diff --git a/packages/wabe-mongodb-launcher/package.json b/packages/wabe-mongodb-launcher/package.json index 5da81ebb..c7c5fa9a 100644 --- a/packages/wabe-mongodb-launcher/package.json +++ b/packages/wabe-mongodb-launcher/package.json @@ -3,7 +3,9 @@ "version": "0.5.2", "description": "Package to launch the mongodb for test", "license": "Apache-2.0", - "files": ["dist"], + "files": [ + "dist" + ], "main": "dist/index.js", "scripts": { "ci": "bun lint", diff --git a/packages/wabe-postgres/package.json b/packages/wabe-postgres/package.json index d32059c3..7a302cea 100644 --- a/packages/wabe-postgres/package.json +++ b/packages/wabe-postgres/package.json @@ -10,10 +10,10 @@ "wabe" ], "license": "Apache-2.0", - "type": "module", "files": [ "dist" ], + "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { diff --git a/packages/wabe-resend/package.json b/packages/wabe-resend/package.json index 02089b42..17040cfb 100644 --- a/packages/wabe-resend/package.json +++ b/packages/wabe-resend/package.json @@ -18,7 +18,9 @@ "type": "git", "url": "git+https://github.com/palixir/wabe.git" }, - "files": ["dist"], + "files": [ + "dist" + ], "main": "dist/index.js", "scripts": { "build": "bun --filter wabe-build build:package $(pwd)",