Skip to content
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

feat(ts-client): optional custom scalars #768

Merged
merged 2 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
19 changes: 14 additions & 5 deletions src/generator/code/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export interface Input {
importPaths?: {
customScalarCodecs?: string
}
/**
* The GraphQL SDL source code.
*/
schemaSource: string
options?: {
/**
Expand All @@ -32,16 +35,19 @@ export interface Input {

export interface Config {
schema: GraphQLSchema
typeMapByKind: TypeMapByKind
libraryPaths: {
schema: string
scalars: string
}
importPaths: {
customScalarCodecs: string
}
typeMapByKind: TypeMapByKind
TSDoc: {
noDocPolicy: 'message' | 'ignore'
options: {
customScalars: boolean
TSDoc: {
noDocPolicy: 'message' | 'ignore'
}
}
}

Expand All @@ -57,8 +63,11 @@ export const resolveOptions = (input: Input): Config => {
schema: input.libraryPaths?.schema ?? `graphql-request/alpha/schema`,
},
typeMapByKind: getTypeMapByKind(schema),
TSDoc: {
noDocPolicy: input.options?.TSDoc?.noDocPolicy ?? `ignore`,
options: {
customScalars: input.options?.customScalars ?? false,
TSDoc: {
noDocPolicy: input.options?.TSDoc?.noDocPolicy ?? `ignore`,
},
},
}
}
Expand Down
26 changes: 23 additions & 3 deletions src/generator/code/scalar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,42 @@ import type { Config } from './code.js'
export const generateScalar = (config: Config) => {
let code = ``

// todo test case for when this is true
const needsDefaultCustomScalarImplementation = config.typeMapByKind.GraphQLScalarTypeCustom.length > 0
&& !config.options.customScalars

code += `
import type * as CustomScalar from '${config.importPaths.customScalarCodecs}'

${config.options.customScalars ? `import type * as CustomScalar from '${config.importPaths.customScalarCodecs}'` : ``}

declare global {
interface SchemaCustomScalars {
${
config.typeMapByKind.GraphQLScalarTypeCustom
.map((_) => {
return `${_.name}: CustomScalar.${_.name}`
return `${_.name}: ${needsDefaultCustomScalarImplementation ? `String` : `CustomScalar.${_.name}`}`
}).join(`\n`)
}
}
}

export * from '${config.libraryPaths.scalars}'
export * from '${config.importPaths.customScalarCodecs}'
${config.options.customScalars ? `export * from '${config.importPaths.customScalarCodecs}'` : ``}
`

if (needsDefaultCustomScalarImplementation) {
console.log(
`WARNING: Custom scalars detected in the schema, but you have not created a custom scalars module to import implementations from.`,
)
code += `
${
config.typeMapByKind.GraphQLScalarTypeCustom
.flatMap((_) => {
return [`export const ${_.name} = String`, `export type ${_.name} = String`]
}).join(`\n`)
}
`
}

return code
}
4 changes: 2 additions & 2 deletions src/generator/code/schemaBuildtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ const concreteRenderers = defineConcreteRenderers({

const getDocumentation = (config: Config, node: Describable) => {
const generalDescription = node.description
?? (config.TSDoc.noDocPolicy === `message` ? defaultDescription(node) : null)
?? (config.options.TSDoc.noDocPolicy === `message` ? defaultDescription(node) : null)

const deprecationDescription = isDeprecatableNode(node) && node.deprecationReason
? `@deprecated ${node.deprecationReason}`
Expand All @@ -191,7 +191,7 @@ const getDocumentation = (config: Config, node: Describable) => {
: null
const generalDescription = _.description
? _.description
: config.TSDoc.noDocPolicy === `message`
: config.options.TSDoc.noDocPolicy === `message`
? `Missing description.`
: null
if (!generalDescription && !deprecationDescription) return null
Expand Down
35 changes: 13 additions & 22 deletions src/generator/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { getPath } from '@dprint/typescript'
import _ from 'json-bigint'
import fs from 'node:fs/promises'
import * as Path from 'node:path'
import { errorFromMaybeError } from '../lib/prelude.js'
import { fileExists } from '../lib/prelude.js'
import { generateCode, type Input as GenerateInput } from './code/code.js'

export interface Input {
outputDirPath: string
code?: Omit<GenerateInput, 'schemaSource' | 'sourceDirPath'>
code?: Omit<GenerateInput, 'schemaSource' | 'sourceDirPath' | 'options'>
sourceDirPath?: string
schemaPath?: string
format?: boolean
Expand All @@ -19,38 +19,29 @@ export const generateFiles = async (input: Input) => {
const schemaPath = input.schemaPath ?? Path.join(sourceDirPath, `schema.graphql`)
const schemaSource = await fs.readFile(schemaPath, `utf8`)

const customScalarCodecsPath = Path.relative(input.outputDirPath, Path.join(sourceDirPath, `customScalarCodecs.js`))
// todo support other extensions: .tsx,.js,.mjs,.cjs
const customScalarCodecsPathExists = await fileExists(customScalarCodecsPath.replace(`.js`, `.ts`))
const customScalarCodecsFilePath = Path.join(sourceDirPath, `customScalarCodecs.ts`)
const customScalarCodecsImportPath = Path.relative(
input.outputDirPath,
customScalarCodecsFilePath.replace(/\.ts$/, `.js`),
)
const customScalarCodecsPathExists = await fileExists(customScalarCodecsFilePath)
const formatter = (input.format ?? true) ? createFromBuffer(await fs.readFile(getPath())) : undefined

const options: GenerateInput['options'] = {
formatter,
customScalars: customScalarCodecsPathExists,
}

const code = generateCode({
schemaSource,
importPaths: {
customScalarCodecs: customScalarCodecsPath,
customScalarCodecs: customScalarCodecsImportPath,
},
...input.code,
options,
options: {
formatter,
customScalars: customScalarCodecsPathExists,
},
})
await fs.mkdir(input.outputDirPath, { recursive: true })
await fs.writeFile(`${input.outputDirPath}/Index.ts`, code.index, { encoding: `utf8` })
await fs.writeFile(`${input.outputDirPath}/SchemaBuildtime.ts`, code.schemaBuildtime, { encoding: `utf8` })
await fs.writeFile(`${input.outputDirPath}/Scalar.ts`, code.scalars, { encoding: `utf8` })
await fs.writeFile(`${input.outputDirPath}/SchemaRuntime.ts`, code.schemaRuntime, { encoding: `utf8` })
}

const fileExists = async (path: string) => {
return Boolean(
await fs.stat(path).catch((_: unknown) => {
const error = errorFromMaybeError(_)
return `code` in error && typeof error.code === `string` && error.code === `ENOENT`
? null
: Promise.reject(error)
}),
)
}
13 changes: 13 additions & 0 deletions src/lib/prelude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,16 @@ export type GetKeyOr<T, Key, Or> = Key extends keyof T ? T[Key] : Or
import type { ConditionalSimplifyDeep } from 'type-fest/source/conditional-simplify.js'

export type SimplifyDeep<T> = ConditionalSimplifyDeep<T, Function | Iterable<unknown> | Date, object>

import fs from 'node:fs/promises'

export const fileExists = async (path: string) => {
return Boolean(
await fs.stat(path).catch((_: unknown) => {
const error = errorFromMaybeError(_)
return `code` in error && typeof error.code === `string` && error.code === `ENOENT`
? null
: Promise.reject(error)
}),
)
}