From 8cf50dae3d866c2479743e7bc9af3f27ac05c68d Mon Sep 17 00:00:00 2001 From: orta Date: Sun, 15 Sep 2024 11:54:52 +0100 Subject: [PATCH 1/9] Adds running the puzzmo codebase inside the tests in order to figure out whats slow --- src/index.ts | 15 ++++------ src/sharedSchema.ts | 8 +++-- src/tests/integration.puzzmo.test.ts | 45 ++++++++++++++++++++++++++++ src/utils.ts | 8 +++++ 4 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 src/tests/integration.puzzmo.test.ts diff --git a/src/index.ts b/src/index.ts index 4d618f4..36c3e4b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,8 @@ export * from "./types.js" import { basename, join } from "node:path" +import { makeStep } from "./utils.js" + export interface SDLCodeGenReturn { // Optional way to start up a watcher mode for the codegen createWatcher: () => { fileChanged: (path: string) => Promise } @@ -22,7 +24,10 @@ export interface SDLCodeGenReturn { } /** The API specifically for the Redwood preset */ -export async function runFullCodegen(preset: "redwood", config: { paths: RedwoodPaths; verbose?: true }): Promise +export async function runFullCodegen( + preset: "redwood", + config: { paths: RedwoodPaths; sys?: typescript.System; verbose?: true } +): Promise export async function runFullCodegen(preset: string, config: unknown): Promise @@ -149,11 +154,3 @@ const isRedwoodServiceFile = (file: string) => { if (file.endsWith("scenarios.ts") || file.endsWith("scenarios.js")) return false return file.endsWith(".ts") || file.endsWith(".tsx") || file.endsWith(".js") } - -const makeStep = (verbose: boolean) => async (msg: string, fn: () => Promise | Promise | void) => { - if (!verbose) return fn() - console.log("[sdl-codegen] " + msg) - console.time("[sdl-codegen] " + msg) - await fn() - console.timeEnd("[sdl-codegen] " + msg) -} diff --git a/src/sharedSchema.ts b/src/sharedSchema.ts index 1821a5a..4c9ef3f 100644 --- a/src/sharedSchema.ts +++ b/src/sharedSchema.ts @@ -6,10 +6,14 @@ import * as tsMorph from "ts-morph" import { AppContext } from "./context.js" import { formatDTS } from "./formatDTS.js" import { typeMapper } from "./typeMap.js" +import { makeStep } from "./utils.js" export const createSharedSchemaFiles = async (context: AppContext) => { - await createSharedExternalSchemaFile(context) - await createSharedReturnPositionSchemaFile(context) + const verbose = !!(context as { verbose?: true }).verbose + const step = makeStep(verbose) + + await step("Creating shared schema files", () => createSharedExternalSchemaFile(context)) + await step("Creating shared return position schema files", () => createSharedReturnPositionSchemaFile(context)) return [ context.join(context.pathSettings.typesFolderRoot, context.pathSettings.sharedFilename), diff --git a/src/tests/integration.puzzmo.test.ts b/src/tests/integration.puzzmo.test.ts new file mode 100644 index 0000000..0bcf91c --- /dev/null +++ b/src/tests/integration.puzzmo.test.ts @@ -0,0 +1,45 @@ +import { existsSync } from "node:fs" +import { join, resolve } from "node:path" + +import { createSystem } from "@typescript/vfs" +import { describe, expect, it } from "vitest" + +import { runFullCodegen } from "../index" + +it("Passes", () => expect(true).toBe(true)) + +const hasAccessToPuzzmo = existsSync("../app/package.json") +const desc = hasAccessToPuzzmo ? describe : describe.skip + +desc("Puzzmo", () => { + it("Runs the entire puzzmo codebase fast", async () => { + const puzzmoAPIWD = resolve(process.cwd() + "/..../../../app/apps/api.puzzmo.com") + const vfsMap = new Map() + const vfs = createSystem(vfsMap) + + // Replicates a Redwood project config object + const paths = { + base: puzzmoAPIWD, + api: { + base: puzzmoAPIWD, + config: "-", + dbSchema: join(puzzmoAPIWD, "prisma", "schema.prisma"), + directives: join(puzzmoAPIWD, "src", "directives"), + graphql: join(puzzmoAPIWD, "src", "functions", "graphql.ts"), + lib: join(puzzmoAPIWD, "src", "lib"), + models: "-", + services: join(puzzmoAPIWD, "src", "services"), + src: join(puzzmoAPIWD, "src"), + types: join(puzzmoAPIWD, "types"), + }, + generated: { + schema: join(puzzmoAPIWD, "..", "..", "api-schema.graphql"), + }, + web: {}, + scripts: "-", + } + + const results = await runFullCodegen("redwood", { paths, verbose: true, sys: vfs }) + console.log(results) + }) +}) diff --git a/src/utils.ts b/src/utils.ts index cff11aa..695d0d1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -56,3 +56,11 @@ export const createAndReferOrInlineArgsForField = ( return `${config.name}Args` } + +export const makeStep = (verbose: boolean) => async (msg: string, fn: () => Promise | Promise | void) => { + if (!verbose) return fn() + console.log("[sdl-codegen] " + msg) + console.time("[sdl-codegen] " + msg) + await fn() + console.timeEnd("[sdl-codegen] " + msg) +} From a1213c9ceee79536425f39aee7272a5b6f026d1e Mon Sep 17 00:00:00 2001 From: orta Date: Sun, 15 Sep 2024 13:37:12 +0100 Subject: [PATCH 2/9] Gets the generated shared files faster --- src/sharedSchema.ts | 47 ++++++++++++------- .../features/generatesTypesForUnions.test.ts | 6 +-- .../supportRefferingToEnumsOnlyInSDL.test.ts | 4 +- src/tests/integration.puzzmo.test.ts | 3 +- 4 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/sharedSchema.ts b/src/sharedSchema.ts index 4c9ef3f..322e19d 100644 --- a/src/sharedSchema.ts +++ b/src/sharedSchema.ts @@ -31,6 +31,9 @@ async function createSharedExternalSchemaFile(context: AppContext) { const externalTSFile = context.tsProject.createSourceFile(`/source/${context.pathSettings.sharedFilename}`, "") + const interfaces = [] as tsMorph.InterfaceDeclarationStructure[] + const typeAliases = [] as tsMorph.TypeAliasDeclarationStructure[] + Object.keys(types).forEach((name) => { if (name.startsWith("__")) { return @@ -54,7 +57,8 @@ async function createSharedExternalSchemaFile(context: AppContext) { docs.push(type.description) } - externalTSFile.addInterface({ + interfaces.push({ + kind: tsMorph.StructureKind.Interface, name: type.name, isExported: true, docs: [], @@ -91,9 +95,10 @@ async function createSharedExternalSchemaFile(context: AppContext) { } if (graphql.isEnumType(type)) { - externalTSFile.addTypeAlias({ + typeAliases.push({ name: type.name, isExported: true, + kind: tsMorph.StructureKind.TypeAlias, type: '"' + type @@ -105,9 +110,10 @@ async function createSharedExternalSchemaFile(context: AppContext) { } if (graphql.isUnionType(type)) { - externalTSFile.addTypeAlias({ + typeAliases.push({ name: type.name, isExported: true, + kind: tsMorph.StructureKind.TypeAlias, type: type .getTypes() .map((m) => m.name) @@ -116,15 +122,11 @@ async function createSharedExternalSchemaFile(context: AppContext) { } }) + externalTSFile.addInterfaces(interfaces) + externalTSFile.addTypeAliases(typeAliases) + const { scalars } = mapper.getReferencedGraphQLThingsInMapping() - if (scalars.length) { - externalTSFile.addTypeAliases( - scalars.map((s) => ({ - name: s, - type: "any", - })) - ) - } + if (scalars.length) externalTSFile.addTypeAliases(scalars.map((s) => ({ name: s, type: "any" }))) const fullPath = context.join(context.pathSettings.typesFolderRoot, context.pathSettings.sharedFilename) const formatted = await formatDTS(fullPath, externalTSFile.getText()) @@ -157,6 +159,9 @@ async function createSharedReturnPositionSchemaFile(context: AppContext) { ` ) + const interfaces = [] as tsMorph.InterfaceDeclarationStructure[] + const typeAliases = [] as tsMorph.TypeAliasDeclarationStructure[] + Object.keys(types).forEach((name) => { if (name.startsWith("__")) { return @@ -177,8 +182,9 @@ async function createSharedReturnPositionSchemaFile(context: AppContext) { return } - externalTSFile.addInterface({ + interfaces.push({ name: type.name, + kind: tsMorph.StructureKind.Interface, isExported: true, docs: [], properties: [ @@ -204,9 +210,10 @@ async function createSharedReturnPositionSchemaFile(context: AppContext) { } if (graphql.isEnumType(type)) { - externalTSFile.addTypeAlias({ + typeAliases.push({ name: type.name, isExported: true, + kind: tsMorph.StructureKind.TypeAlias, type: '"' + type @@ -218,8 +225,9 @@ async function createSharedReturnPositionSchemaFile(context: AppContext) { } if (graphql.isUnionType(type)) { - externalTSFile.addTypeAlias({ + typeAliases.push({ name: type.name, + kind: tsMorph.StructureKind.TypeAlias, isExported: true, type: type .getTypes() @@ -247,15 +255,18 @@ async function createSharedReturnPositionSchemaFile(context: AppContext) { namedImports: allPrismaModels.map((p) => `${p} as P${p}`), }) - allPrismaModels.forEach((p) => { - externalTSFile.addTypeAlias({ + externalTSFile.addTypeAliases( + allPrismaModels.map((p) => ({ isExported: true, name: p, type: `P${p}`, - }) - }) + })) + ) } + externalTSFile.addInterfaces(interfaces) + externalTSFile.addTypeAliases(typeAliases) + const fullPath = context.join(context.pathSettings.typesFolderRoot, context.pathSettings.sharedInternalFilename) const formatted = await formatDTS(fullPath, externalTSFile.getText()) diff --git a/src/tests/features/generatesTypesForUnions.test.ts b/src/tests/features/generatesTypesForUnions.test.ts index 6125ea5..07c2ef8 100644 --- a/src/tests/features/generatesTypesForUnions.test.ts +++ b/src/tests/features/generatesTypesForUnions.test.ts @@ -48,8 +48,6 @@ export const Game = { id: number; } - export type Gameish = Game | Puzzle; - export interface Query { __typename?: \\"Query\\"; gameObj?: Game | null | Puzzle | null | null; @@ -59,6 +57,8 @@ export const Game = { export interface Mutation { __typename?: \\"Mutation\\"; __?: string | null; - }" + } + + export type Gameish = Game | Puzzle;" `) }) diff --git a/src/tests/features/supportRefferingToEnumsOnlyInSDL.test.ts b/src/tests/features/supportRefferingToEnumsOnlyInSDL.test.ts index 54dcdb9..75975b3 100644 --- a/src/tests/features/supportRefferingToEnumsOnlyInSDL.test.ts +++ b/src/tests/features/supportRefferingToEnumsOnlyInSDL.test.ts @@ -76,12 +76,12 @@ export const Game: GameResolvers = {}; allGames: Game[]; } - export type GameType = \\"FOOTBALL\\" | \\"BASKETBALL\\"; - export interface Mutation { __typename?: \\"Mutation\\"; __?: string | null; } + + export type GameType = \\"FOOTBALL\\" | \\"BASKETBALL\\"; " `) }) diff --git a/src/tests/integration.puzzmo.test.ts b/src/tests/integration.puzzmo.test.ts index 0bcf91c..5a4a71a 100644 --- a/src/tests/integration.puzzmo.test.ts +++ b/src/tests/integration.puzzmo.test.ts @@ -39,7 +39,6 @@ desc("Puzzmo", () => { scripts: "-", } - const results = await runFullCodegen("redwood", { paths, verbose: true, sys: vfs }) - console.log(results) + await runFullCodegen("redwood", { paths, verbose: true, sys: vfs }) }) }) From 3fb669b19871f574c843143cb6dd09f8eacc87fd Mon Sep 17 00:00:00 2001 From: orta Date: Sun, 15 Sep 2024 15:14:23 +0100 Subject: [PATCH 3/9] Use the typescript factory ap --- src/formatDTS.ts | 3 +- src/index.ts | 6 +- src/serviceFile.ts | 3 +- src/sharedSchema.ts | 14 +- src/sharedSchemaTSC.ts | 134 ++++++++++++++++++ .../features/generatesTypesForUnions.test.ts | 26 ++-- .../supportRefferingToEnumsOnlyInSDL.test.ts | 19 ++- 7 files changed, 170 insertions(+), 35 deletions(-) create mode 100644 src/sharedSchemaTSC.ts diff --git a/src/formatDTS.ts b/src/formatDTS.ts index 3adf0ca..a8863d1 100644 --- a/src/formatDTS.ts +++ b/src/formatDTS.ts @@ -1,15 +1,16 @@ // https://prettier.io/docs/en/api.html let hasPrettierInstalled = false +let prettier = null try { hasPrettierInstalled = !!require.resolve("prettier") + prettier = require("prettier") } catch (error) {} export const formatDTS = async (path: string, content: string): Promise => { if (!hasPrettierInstalled) return content try { - const prettier = await import("prettier") if (!prettier) return content return prettier.format(content, { filepath: path }) } catch (error) { diff --git a/src/index.ts b/src/index.ts index 36c3e4b..ae08066 100644 --- a/src/index.ts +++ b/src/index.ts @@ -89,15 +89,15 @@ export async function runFullCodegen(preset: string, config: unknown): Promise { - const sharedDTSes = await createSharedSchemaFiles(appContext) + const sharedDTSes = await createSharedSchemaFiles(appContext, verbose) filepaths.push(...sharedDTSes) }) let knownServiceFiles: string[] = [] const createDTSFilesForAllServices = async () => { - // TODO: Maybe Redwood has an API for this? Its grabbing all the services const serviceFiles = appContext.sys.readDirectory(appContext.pathSettings.apiServicesPath) knownServiceFiles = serviceFiles.filter(isRedwoodServiceFile) + for (const path of knownServiceFiles) { const dts = await lookAtServiceFile(path, appContext) if (dts) filepaths.push(dts) @@ -123,7 +123,7 @@ export async function runFullCodegen(preset: string, config: unknown): Promise getGraphQLSDLFromFile(appContext.pathSettings)) - await step("Create all shared schema files", () => createSharedSchemaFiles(appContext)) + await step("Create all shared schema files", () => createSharedSchemaFiles(appContext, verbose)) await step("Create all service files", createDTSFilesForAllServices) } else if (path === appContext.pathSettings.prismaDSLPath) { await step("Prisma schema changed", () => getPrismaSchemaFromFile(appContext.pathSettings)) diff --git a/src/serviceFile.ts b/src/serviceFile.ts index f3718ae..44958c2 100644 --- a/src/serviceFile.ts +++ b/src/serviceFile.ts @@ -28,7 +28,6 @@ export const lookAtServiceFile = async (file: string, context: AppContext) => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const mutationType = gql.getMutationType()! - if (!mutationType) throw new Error("No mutation type") const externalMapper = typeMapper(context, { preferPrismaModels: true }) @@ -201,6 +200,8 @@ export const lookAtServiceFile = async (file: string, context: AppContext) => { ], returnType, }) + + interfaceDeclaration.forget() } /** Ideally, we want to be able to write the type for just the object */ diff --git a/src/sharedSchema.ts b/src/sharedSchema.ts index 322e19d..dd5559e 100644 --- a/src/sharedSchema.ts +++ b/src/sharedSchema.ts @@ -5,14 +5,15 @@ import * as tsMorph from "ts-morph" import { AppContext } from "./context.js" import { formatDTS } from "./formatDTS.js" +import { createSharedExternalSchemaFileViaTSC } from "./sharedSchemaTSC.js" import { typeMapper } from "./typeMap.js" import { makeStep } from "./utils.js" -export const createSharedSchemaFiles = async (context: AppContext) => { - const verbose = !!(context as { verbose?: true }).verbose +export const createSharedSchemaFiles = async (context: AppContext, verbose: boolean) => { const step = makeStep(verbose) await step("Creating shared schema files", () => createSharedExternalSchemaFile(context)) + await step("Creating shared schema files via tsc", () => createSharedExternalSchemaFileViaTSC(context)) await step("Creating shared return position schema files", () => createSharedReturnPositionSchemaFile(context)) return [ @@ -122,8 +123,13 @@ async function createSharedExternalSchemaFile(context: AppContext) { } }) - externalTSFile.addInterfaces(interfaces) - externalTSFile.addTypeAliases(typeAliases) + context.tsProject.forgetNodesCreatedInBlock(() => { + externalTSFile.addInterfaces(interfaces) + }) + + context.tsProject.forgetNodesCreatedInBlock(() => { + externalTSFile.addTypeAliases(typeAliases) + }) const { scalars } = mapper.getReferencedGraphQLThingsInMapping() if (scalars.length) externalTSFile.addTypeAliases(scalars.map((s) => ({ name: s, type: "any" }))) diff --git a/src/sharedSchemaTSC.ts b/src/sharedSchemaTSC.ts new file mode 100644 index 0000000..a3340d1 --- /dev/null +++ b/src/sharedSchemaTSC.ts @@ -0,0 +1,134 @@ +/// The main schema for objects and inputs + +import * as graphql from "graphql" +import ts from "typescript" + +import { AppContext } from "./context.js" +import { formatDTS } from "./formatDTS.js" +import { typeMapper } from "./typeMap.js" + +export async function createSharedExternalSchemaFileViaTSC(context: AppContext) { + const gql = context.gql + const types = gql.getTypeMap() + const knownPrimitives = ["String", "Boolean", "Int"] + + const { prisma, fieldFacts } = context + const mapper = typeMapper(context, {}) + + const statements = [] as ts.Statement[] + + console.time("") + + Object.keys(types).forEach((name) => { + if (name.startsWith("__")) { + return + } + + if (knownPrimitives.includes(name)) { + return + } + + const type = types[name] + const pType = prisma.get(name) + + if (graphql.isObjectType(type) || graphql.isInterfaceType(type) || graphql.isInputObjectType(type)) { + // This is slower than it could be, use the add many at once api + const docs = [] + if (pType?.leadingComments) { + docs.push(pType.leadingComments) + } + + if (type.description) { + docs.push(type.description) + } + + const properties = [ + ts.factory.createPropertySignature( + undefined, + "__typename", + ts.factory.createToken(ts.SyntaxKind.QuestionToken), + ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(type.name)) + ), + ] + + Object.entries(type.getFields()).forEach(([fieldName, obj]: [string, graphql.GraphQLField]) => { + const docs = [] + const prismaField = pType?.properties.get(fieldName) + const type = obj.type as graphql.GraphQLType + + if (prismaField?.leadingComments.length) { + docs.push(prismaField.leadingComments.trim()) + } + + // if (obj.description) docs.push(obj.description); + const hasResolverImplementation = fieldFacts.get(name)?.[fieldName]?.hasResolverImplementation + const isOptionalInSDL = !graphql.isNonNullType(type) + const doesNotExistInPrisma = false // !prismaField; + + const hasQuestionToken = hasResolverImplementation ?? (isOptionalInSDL || doesNotExistInPrisma) + const mappedType = mapper.map(type, { preferNullOverUndefined: true }) + if (mappedType) { + properties.push( + ts.factory.createPropertySignature( + undefined, + fieldName, + hasQuestionToken ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined, + ts.factory.createTypeReferenceNode(mappedType) + ) + ) + } + }) + + const interfaceD = ts.factory.createInterfaceDeclaration( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier(name), + undefined, + undefined, + properties + ) + + statements.push(interfaceD) + } + + if (graphql.isEnumType(type)) { + const values = type.getValues().map((m) => (m as { value: string }).value) + const typeKind = `"${values.join('" | "')}"` + + statements.push(ts.factory.createTypeAliasDeclaration(undefined, type.name, [], ts.factory.createTypeReferenceNode(typeKind))) + } + + if (graphql.isUnionType(type)) { + const types = type.getTypes().map((t) => t.name) + const typeKind = types.join(" | ") + statements.push( + ts.factory.createTypeAliasDeclaration( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + type.name, + [], + ts.factory.createTypeReferenceNode(typeKind) + ) + ) + } + }) + + const { scalars } = mapper.getReferencedGraphQLThingsInMapping() + if (scalars.length) { + statements.push( + ts.factory.createTypeAliasDeclaration( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + "Scalars", + [], + ts.factory.createTypeReferenceNode(`{ ${scalars.join(", ")} }`) + ) + ) + } + + const sourceFile = ts.factory.createSourceFile(statements, ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None) + const printer = ts.createPrinter({}) + const result = printer.printNode(ts.EmitHint.Unspecified, sourceFile, sourceFile) + + const fullPath = context.join(context.pathSettings.typesFolderRoot, context.pathSettings.sharedFilename) + + const prior = context.sys.readFile(fullPath) + if (prior !== result) context.sys.writeFile(fullPath, result) +} diff --git a/src/tests/features/generatesTypesForUnions.test.ts b/src/tests/features/generatesTypesForUnions.test.ts index 07c2ef8..a6845be 100644 --- a/src/tests/features/generatesTypesForUnions.test.ts +++ b/src/tests/features/generatesTypesForUnions.test.ts @@ -39,26 +39,22 @@ export const Game = { const dts = vfsMap.get("/types/shared-schema-types.d.ts")! expect(dts.trim()).toMatchInlineSnapshot(` "export interface Game { - __typename?: \\"Game\\"; - id?: number; + __typename?: \\"Game\\"; + id?: number; } - export interface Puzzle { - __typename?: \\"Puzzle\\"; - id: number; + __typename?: \\"Puzzle\\"; + id: number; } - + export type Gameish = Game | Puzzle; export interface Query { - __typename?: \\"Query\\"; - gameObj?: Game | null | Puzzle | null | null; - gameArr: (Game | Puzzle)[]; + __typename?: \\"Query\\"; + gameObj?: Game| null | Puzzle| null| null; + gameArr: (Game | Puzzle)[]; } - export interface Mutation { - __typename?: \\"Mutation\\"; - __?: string | null; - } - - export type Gameish = Game | Puzzle;" + __typename?: \\"Mutation\\"; + __?: string| null; + }" `) }) diff --git a/src/tests/features/supportRefferingToEnumsOnlyInSDL.test.ts b/src/tests/features/supportRefferingToEnumsOnlyInSDL.test.ts index 75975b3..2796bb9 100644 --- a/src/tests/features/supportRefferingToEnumsOnlyInSDL.test.ts +++ b/src/tests/features/supportRefferingToEnumsOnlyInSDL.test.ts @@ -66,22 +66,19 @@ export const Game: GameResolvers = {}; expect(vfsMap.get("/types/shared-schema-types.d.ts"))!.toMatchInlineSnapshot(` "export interface Game { - __typename?: \\"Game\\"; - id: number; - games: Game[]; + __typename?: \\"Game\\"; + id: number; + games: Game[]; } - export interface Query { - __typename?: \\"Query\\"; - allGames: Game[]; + __typename?: \\"Query\\"; + allGames: Game[]; } - + type GameType = \\"FOOTBALL\\" | \\"BASKETBALL\\"; export interface Mutation { - __typename?: \\"Mutation\\"; - __?: string | null; + __typename?: \\"Mutation\\"; + __?: string| null; } - - export type GameType = \\"FOOTBALL\\" | \\"BASKETBALL\\"; " `) }) From 24c50a22dd241b700a4e089ec2c913d475df4d61 Mon Sep 17 00:00:00 2001 From: Orta Therox Date: Tue, 29 Oct 2024 12:39:24 +0000 Subject: [PATCH 4/9] USe structures api --- src/sharedSchema.ts | 9 +- src/sharedSchemaStructures.ts | 154 ++++++++++++++++++++++++++++++++++ src/sharedSchemaTSC.ts | 2 +- 3 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 src/sharedSchemaStructures.ts diff --git a/src/sharedSchema.ts b/src/sharedSchema.ts index dd5559e..da4e649 100644 --- a/src/sharedSchema.ts +++ b/src/sharedSchema.ts @@ -5,6 +5,7 @@ import * as tsMorph from "ts-morph" import { AppContext } from "./context.js" import { formatDTS } from "./formatDTS.js" +import { createSharedExternalSchemaFileViaStructure } from "./sharedSchemaStructures.js" import { createSharedExternalSchemaFileViaTSC } from "./sharedSchemaTSC.js" import { typeMapper } from "./typeMap.js" import { makeStep } from "./utils.js" @@ -14,6 +15,8 @@ export const createSharedSchemaFiles = async (context: AppContext, verbose: bool await step("Creating shared schema files", () => createSharedExternalSchemaFile(context)) await step("Creating shared schema files via tsc", () => createSharedExternalSchemaFileViaTSC(context)) + await step("Creating shared schema files via structure", () => createSharedExternalSchemaFileViaStructure(context)) + await step("Creating shared return position schema files", () => createSharedReturnPositionSchemaFile(context)) return [ @@ -22,7 +25,7 @@ export const createSharedSchemaFiles = async (context: AppContext, verbose: bool ] } -async function createSharedExternalSchemaFile(context: AppContext) { +function createSharedExternalSchemaFile(context: AppContext) { const gql = context.gql const types = gql.getTypeMap() const knownPrimitives = ["String", "Boolean", "Int"] @@ -135,10 +138,10 @@ async function createSharedExternalSchemaFile(context: AppContext) { if (scalars.length) externalTSFile.addTypeAliases(scalars.map((s) => ({ name: s, type: "any" }))) const fullPath = context.join(context.pathSettings.typesFolderRoot, context.pathSettings.sharedFilename) - const formatted = await formatDTS(fullPath, externalTSFile.getText()) + const text = externalTSFile.getText() const prior = context.sys.readFile(fullPath) - if (prior !== formatted) context.sys.writeFile(fullPath, formatted) + if (prior !== text) context.sys.writeFile(fullPath, text) } async function createSharedReturnPositionSchemaFile(context: AppContext) { diff --git a/src/sharedSchemaStructures.ts b/src/sharedSchemaStructures.ts new file mode 100644 index 0000000..74fcf67 --- /dev/null +++ b/src/sharedSchemaStructures.ts @@ -0,0 +1,154 @@ +/// The main schema for objects and inputs + +import * as graphql from "graphql" +import * as tsMorph from "ts-morph" + +import { AppContext } from "./context.js" +import { typeMapper } from "./typeMap.js" + +export function createSharedExternalSchemaFileViaStructure(context: AppContext) { + const { gql, prisma, fieldFacts } = context + const types = gql.getTypeMap() + const mapper = typeMapper(context, { preferPrismaModels: true }) + + const typesToImport = [] as string[] + const knownPrimitives = ["String", "Boolean", "Int"] + + const externalTSFile = context.tsProject.createSourceFile( + `/source/a/${context.pathSettings.sharedInternalFilename}`, + ` +// You may very reasonably ask yourself, 'what is this file?' and why do I need it. + +// Roughly, this file ensures that when a resolver wants to return a type - that +// type will match a prisma model. This is useful because you can trivially extend +// the type in the SDL and not have to worry about type mis-matches because the thing +// you returned does not include those functions. + +// This gets particularly valuable when you want to return a union type, an interface, +// or a model where the prisma model is nested pretty deeply (GraphQL connections, for example.) + +` + ) + + const statements = [] as tsMorph.StatementStructures[] + + Object.keys(types).forEach((name) => { + if (name.startsWith("__")) { + return + } + + if (knownPrimitives.includes(name)) { + return + } + + const type = types[name] + const pType = prisma.get(name) + + if (graphql.isObjectType(type) || graphql.isInterfaceType(type) || graphql.isInputObjectType(type)) { + // Return straight away if we have a matching type in the prisma schema + // as we dont need it + if (pType) { + typesToImport.push(name) + return + } + + statements.push({ + name: type.name, + kind: tsMorph.StructureKind.Interface, + isExported: true, + docs: [], + properties: [ + { + name: "__typename", + type: `"${type.name}"`, + hasQuestionToken: true, + }, + ...Object.entries(type.getFields()).map(([fieldName, obj]: [string, graphql.GraphQLField]) => { + const hasResolverImplementation = fieldFacts.get(name)?.[fieldName]?.hasResolverImplementation + const isOptionalInSDL = !graphql.isNonNullType(obj.type) + const doesNotExistInPrisma = false // !prismaField; + + const field: tsMorph.OptionalKind = { + name: fieldName, + type: mapper.map(obj.type, { preferNullOverUndefined: true }), + hasQuestionToken: hasResolverImplementation ?? (isOptionalInSDL || doesNotExistInPrisma), + } + return field + }), + ], + }) + } + + if (graphql.isEnumType(type)) { + statements.push({ + name: type.name, + isExported: true, + kind: tsMorph.StructureKind.TypeAlias, + type: + '"' + + type + .getValues() + .map((m) => (m as { value: string }).value) + .join('" | "') + + '"', + }) + } + + if (graphql.isUnionType(type)) { + statements.push({ + name: type.name, + kind: tsMorph.StructureKind.TypeAlias, + isExported: true, + type: type + .getTypes() + .map((m) => m.name) + .join(" | "), + }) + } + }) + + const { scalars, prisma: prismaModels } = mapper.getReferencedGraphQLThingsInMapping() + if (scalars.length) { + statements.push( + ...scalars.map( + (s) => + ({ + kind: tsMorph.StructureKind.TypeAlias, + name: s, + type: "any", + } as const) + ) + ) + } + + const allPrismaModels = [...new Set([...prismaModels, ...typesToImport])].sort() + if (allPrismaModels.length) { + statements.push({ + kind: tsMorph.StructureKind.ImportDeclaration, + moduleSpecifier: `@prisma/client`, + namedImports: allPrismaModels.map((p) => `${p} as P${p}`), + }) + + statements.push( + ...allPrismaModels.map( + (p) => + ({ + kind: tsMorph.StructureKind.TypeAlias, + name: p, + type: `P${p}`, + } as const) + ) + ) + } + + const fullPath = context.join(context.pathSettings.typesFolderRoot, context.pathSettings.sharedInternalFilename) + externalTSFile.set({ statements }) + const text = externalTSFile.getText() + + // console.log(sourceFileStructure.statements) + // console.log(text) + // const formatted = await formatDTS(fullPath, externalTSFile) + + const prior = context.sys.readFile(fullPath) + if (prior !== text) context.sys.writeFile(fullPath, text) +} diff --git a/src/sharedSchemaTSC.ts b/src/sharedSchemaTSC.ts index a3340d1..e175b40 100644 --- a/src/sharedSchemaTSC.ts +++ b/src/sharedSchemaTSC.ts @@ -7,7 +7,7 @@ import { AppContext } from "./context.js" import { formatDTS } from "./formatDTS.js" import { typeMapper } from "./typeMap.js" -export async function createSharedExternalSchemaFileViaTSC(context: AppContext) { +export function createSharedExternalSchemaFileViaTSC(context: AppContext) { const gql = context.gql const types = gql.getTypeMap() const knownPrimitives = ["String", "Boolean", "Int"] From fa3d7d8945d7a3d17022bfa5c98a6bfc847e719c Mon Sep 17 00:00:00 2001 From: Orta Therox Date: Sat, 9 Nov 2024 20:50:47 +0000 Subject: [PATCH 5/9] WIP on using the builder pattern from burr --- package.json | 6 + pnpm-lock.yaml | 219 ++++++++++-------- src/sharedSchema.ts | 94 ++++---- src/sharedSchemaStructures.ts | 154 ------------ src/sharedSchemaTSC.ts | 134 ----------- .../features/generatesTypesForUnions.test.ts | 4 + ... supportReferringToEnumsOnlyInSDL.test.ts} | 5 +- src/tsBuilder.ts | 198 ++++++++++++++++ 8 files changed, 380 insertions(+), 434 deletions(-) delete mode 100644 src/sharedSchemaStructures.ts delete mode 100644 src/sharedSchemaTSC.ts rename src/tests/features/{supportRefferingToEnumsOnlyInSDL.test.ts => supportReferringToEnumsOnlyInSDL.test.ts} (96%) create mode 100644 src/tsBuilder.ts diff --git a/package.json b/package.json index c145395..7930978 100644 --- a/package.json +++ b/package.json @@ -39,11 +39,17 @@ "*.ts": "eslint --fix" }, "dependencies": { + "@babel/generator": "7.26.0", + "@babel/parser": "^7.26.2", + "@babel/traverse": "7.25.9", + "@babel/types": "7.26.0", "@mrleebo/prisma-ast": "^0.12.0", "ts-morph": "^22.0.0" }, "devDependencies": { "@babel/core": "^7.20.12", + "@types/babel__generator": "^7.6.0", + "@types/babel__traverse": "^7.14.0", "@types/eslint": "^8.21.1", "@types/node": "^16.16.0", "@typescript-eslint/eslint-plugin": "^5.48.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c4943a..da24773 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,21 @@ importers: .: dependencies: + '@babel/generator': + specifier: 7.26.0 + version: 7.26.0 + '@babel/parser': + specifier: ^7.26.2 + version: 7.26.2 + '@babel/traverse': + specifier: 7.25.9 + version: 7.25.9 + '@babel/types': + specifier: 7.26.0 + version: 7.26.0 '@mrleebo/prisma-ast': specifier: ^0.12.0 version: 0.12.0 - '@prettier/sync': - specifier: 0.5.2 - version: 0.5.2(prettier@2.8.3) ts-morph: specifier: ^22.0.0 version: 22.0.0 @@ -21,6 +30,12 @@ importers: '@babel/core': specifier: ^7.20.12 version: 7.20.12 + '@types/babel__generator': + specifier: ^7.6.0 + version: 7.6.8 + '@types/babel__traverse': + specifier: ^7.14.0 + version: 7.20.6 '@types/eslint': specifier: ^8.21.1 version: 8.21.2 @@ -143,6 +158,10 @@ packages: resolution: {integrity: sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.20.10': resolution: {integrity: sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg==} engines: {node: '>=6.9.0'} @@ -151,8 +170,8 @@ packages: resolution: {integrity: sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==} engines: {node: '>=6.9.0'} - '@babel/generator@7.20.7': - resolution: {integrity: sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw==} + '@babel/generator@7.26.0': + resolution: {integrity: sha512-/AIkAmInnWwgEAJGQr9vY0c66Mj6kjkE2ZPB1PurTRaRAh3U+J45sAQMjQDJdh4WbR3l0x5xkimXBKyBXXAu2w==} engines: {node: '>=6.9.0'} '@babel/helper-compilation-targets@7.20.7': @@ -165,14 +184,6 @@ packages: resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==} engines: {node: '>=6.9.0'} - '@babel/helper-function-name@7.19.0': - resolution: {integrity: sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==} - engines: {node: '>=6.9.0'} - - '@babel/helper-hoist-variables@7.18.6': - resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} - engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.18.6': resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} engines: {node: '>=6.9.0'} @@ -189,14 +200,18 @@ packages: resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.19.4': - resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==} + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} engines: {node: '>=6.9.0'} '@babel/helper-validator-identifier@7.19.1': resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.18.6': resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==} engines: {node: '>=6.9.0'} @@ -209,21 +224,21 @@ packages: resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} engines: {node: '>=6.9.0'} - '@babel/parser@7.21.4': - resolution: {integrity: sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==} + '@babel/parser@7.26.2': + resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/template@7.20.7': - resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==} + '@babel/template@7.25.9': + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.20.12': - resolution: {integrity: sha512-MsIbFN0u+raeja38qboyF8TIT7K0BFzz/Yd/77ta4MsUsmP2RAnidIlwq7d5HFQrH/OZJecGV6B71C4zAgpoSQ==} + '@babel/traverse@7.25.9': + resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==} engines: {node: '>=6.9.0'} - '@babel/types@7.21.4': - resolution: {integrity: sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==} + '@babel/types@7.26.0': + resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} engines: {node: '>=6.9.0'} '@chevrotain/cst-dts-gen@10.5.0': @@ -557,8 +572,8 @@ packages: resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} engines: {node: '>=6.0.0'} - '@jridgewell/gen-mapping@0.3.2': - resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} '@jridgewell/resolve-uri@3.1.0': @@ -569,12 +584,19 @@ packages: resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} engines: {node: '>=6.0.0'} + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.4.14': resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} '@jridgewell/trace-mapping@0.3.17': resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==} + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@mrleebo/prisma-ast@0.12.0': resolution: {integrity: sha512-qQsScED+KOMbnKP946Tiyi0Jzxq3YjvxkjD7MsR+SI815JLyoc2Ca6pi3t7ub8DJzO6kv3PC9Ly4ru7JufFnyQ==} engines: {node: '>=16'} @@ -763,11 +785,6 @@ packages: resolution: {integrity: sha512-/C0j7SsE9tGoj++f0dwePIV7zNZHcX8TcYL6pXNvZZCq4HsOMCBsIlcU9oMI/AGe+KMDfHFQSayWPO9QUuGE5w==} engines: {node: '>=14.6'} - '@prettier/sync@0.5.2': - resolution: {integrity: sha512-Yb569su456XNx5BsH/Vyem7xD6g/y9iLmLUzRKM1a/dhU/D7HqqvkAG72znulXlMXztbV0iiu9O5AL8K98TzZQ==} - peerDependencies: - prettier: '*' - '@sindresorhus/is@5.3.0': resolution: {integrity: sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==} engines: {node: '>=14.16'} @@ -788,6 +805,12 @@ packages: '@ts-morph/common@0.23.0': resolution: {integrity: sha512-m7Lllj9n/S6sOkCkRftpM7L24uvmfXQFedlW/4hENcuJH1HHm9u5EgxZb9uVjQSCGrbBWBkOGgcTxNg36r6ywA==} + '@types/babel__generator@7.6.8': + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + + '@types/babel__traverse@7.20.6': + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@types/chai-subset@1.3.3': resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} @@ -2391,9 +2414,9 @@ packages: engines: {node: '>=10'} hasBin: true - jsesc@2.5.2: - resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} - engines: {node: '>=4'} + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} hasBin: true json-buffer@3.0.1: @@ -2562,9 +2585,6 @@ packages: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} - make-synchronized@0.2.9: - resolution: {integrity: sha512-4wczOs8SLuEdpEvp3vGo83wh8rjJ78UsIk7DIX5fxdfmfMJGog4bQzxfvOwq7Q3yCHLC4jp1urPHIxRS/A93gA==} - map-age-cleaner@0.1.3: resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} engines: {node: '>=6'} @@ -3505,10 +3525,6 @@ packages: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} - to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} - to-no-case@1.0.2: resolution: {integrity: sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==} @@ -3885,20 +3901,26 @@ snapshots: dependencies: '@babel/highlight': 7.18.6 + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.0.0 + '@babel/compat-data@7.20.10': {} '@babel/core@7.20.12': dependencies: '@ampproject/remapping': 2.2.0 '@babel/code-frame': 7.21.4 - '@babel/generator': 7.20.7 + '@babel/generator': 7.26.0 '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.20.12) '@babel/helper-module-transforms': 7.20.11 '@babel/helpers': 7.20.7 - '@babel/parser': 7.21.4 - '@babel/template': 7.20.7 - '@babel/traverse': 7.20.12 - '@babel/types': 7.21.4 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 convert-source-map: 1.9.0 debug: 4.3.4 gensync: 1.0.0-beta.2 @@ -3907,11 +3929,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.20.7': + '@babel/generator@7.26.0': dependencies: - '@babel/types': 7.21.4 - '@jridgewell/gen-mapping': 0.3.2 - jsesc: 2.5.2 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.0.2 '@babel/helper-compilation-targets@7.20.7(@babel/core@7.20.12)': dependencies: @@ -3924,18 +3948,9 @@ snapshots: '@babel/helper-environment-visitor@7.18.9': {} - '@babel/helper-function-name@7.19.0': - dependencies: - '@babel/template': 7.20.7 - '@babel/types': 7.21.4 - - '@babel/helper-hoist-variables@7.18.6': - dependencies: - '@babel/types': 7.21.4 - '@babel/helper-module-imports@7.18.6': dependencies: - '@babel/types': 7.21.4 + '@babel/types': 7.26.0 '@babel/helper-module-transforms@7.20.11': dependencies: @@ -3944,31 +3959,33 @@ snapshots: '@babel/helper-simple-access': 7.20.2 '@babel/helper-split-export-declaration': 7.18.6 '@babel/helper-validator-identifier': 7.19.1 - '@babel/template': 7.20.7 - '@babel/traverse': 7.20.12 - '@babel/types': 7.21.4 + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 transitivePeerDependencies: - supports-color '@babel/helper-simple-access@7.20.2': dependencies: - '@babel/types': 7.21.4 + '@babel/types': 7.26.0 '@babel/helper-split-export-declaration@7.18.6': dependencies: - '@babel/types': 7.21.4 + '@babel/types': 7.26.0 - '@babel/helper-string-parser@7.19.4': {} + '@babel/helper-string-parser@7.25.9': {} '@babel/helper-validator-identifier@7.19.1': {} + '@babel/helper-validator-identifier@7.25.9': {} + '@babel/helper-validator-option@7.18.6': {} '@babel/helpers@7.20.7': dependencies: - '@babel/template': 7.20.7 - '@babel/traverse': 7.20.12 - '@babel/types': 7.21.4 + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 transitivePeerDependencies: - supports-color @@ -3978,36 +3995,32 @@ snapshots: chalk: 2.4.2 js-tokens: 4.0.0 - '@babel/parser@7.21.4': + '@babel/parser@7.26.2': dependencies: - '@babel/types': 7.21.4 + '@babel/types': 7.26.0 - '@babel/template@7.20.7': + '@babel/template@7.25.9': dependencies: - '@babel/code-frame': 7.21.4 - '@babel/parser': 7.21.4 - '@babel/types': 7.21.4 + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 - '@babel/traverse@7.20.12': + '@babel/traverse@7.25.9': dependencies: - '@babel/code-frame': 7.21.4 - '@babel/generator': 7.20.7 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.19.0 - '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/parser': 7.21.4 - '@babel/types': 7.21.4 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.0 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: - supports-color - '@babel/types@7.21.4': + '@babel/types@7.26.0': dependencies: - '@babel/helper-string-parser': 7.19.4 - '@babel/helper-validator-identifier': 7.19.1 - to-fast-properties: 2.0.0 + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 '@chevrotain/cst-dts-gen@10.5.0': dependencies: @@ -4276,16 +4289,18 @@ snapshots: '@jridgewell/set-array': 1.1.2 '@jridgewell/sourcemap-codec': 1.4.14 - '@jridgewell/gen-mapping@0.3.2': + '@jridgewell/gen-mapping@0.3.5': dependencies: - '@jridgewell/set-array': 1.1.2 + '@jridgewell/set-array': 1.2.1 '@jridgewell/sourcemap-codec': 1.4.14 - '@jridgewell/trace-mapping': 0.3.17 + '@jridgewell/trace-mapping': 0.3.25 '@jridgewell/resolve-uri@3.1.0': {} '@jridgewell/set-array@1.1.2': {} + '@jridgewell/set-array@1.2.1': {} + '@jridgewell/sourcemap-codec@1.4.14': {} '@jridgewell/trace-mapping@0.3.17': @@ -4293,6 +4308,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.0 '@jridgewell/sourcemap-codec': 1.4.14 + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.14 + '@mrleebo/prisma-ast@0.12.0': dependencies: chevrotain: 10.5.0 @@ -4553,11 +4573,6 @@ snapshots: write-file-atomic: 5.0.0 write-yaml-file: 4.2.0 - '@prettier/sync@0.5.2(prettier@2.8.3)': - dependencies: - make-synchronized: 0.2.9 - prettier: 2.8.3 - '@sindresorhus/is@5.3.0': {} '@snyk/github-codeowners@1.1.0': @@ -4579,6 +4594,14 @@ snapshots: mkdirp: 3.0.1 path-browserify: 1.0.1 + '@types/babel__generator@7.6.8': + dependencies: + '@babel/types': 7.26.0 + + '@types/babel__traverse@7.20.6': + dependencies: + '@babel/types': 7.26.0 + '@types/chai-subset@1.3.3': dependencies: '@types/chai': 4.3.5 @@ -6398,7 +6421,7 @@ snapshots: jsdoctypeparser@9.0.0: {} - jsesc@2.5.2: {} + jsesc@3.0.2: {} json-buffer@3.0.1: {} @@ -6586,8 +6609,6 @@ snapshots: dependencies: semver: 6.3.0 - make-synchronized@0.2.9: {} - map-age-cleaner@0.1.3: dependencies: p-defer: 1.0.0 @@ -7611,8 +7632,6 @@ snapshots: dependencies: os-tmpdir: 1.0.2 - to-fast-properties@2.0.0: {} - to-no-case@1.0.2: {} to-pascal-case@1.0.0: diff --git a/src/sharedSchema.ts b/src/sharedSchema.ts index da4e649..1c54210 100644 --- a/src/sharedSchema.ts +++ b/src/sharedSchema.ts @@ -1,22 +1,19 @@ /// The main schema for objects and inputs +import { BlockStatement } from "@babel/types" +import t from "@babel/types" import * as graphql from "graphql" import * as tsMorph from "ts-morph" import { AppContext } from "./context.js" import { formatDTS } from "./formatDTS.js" -import { createSharedExternalSchemaFileViaStructure } from "./sharedSchemaStructures.js" -import { createSharedExternalSchemaFileViaTSC } from "./sharedSchemaTSC.js" +import { builder } from "./tsBuilder.js" import { typeMapper } from "./typeMap.js" import { makeStep } from "./utils.js" export const createSharedSchemaFiles = async (context: AppContext, verbose: boolean) => { const step = makeStep(verbose) - await step("Creating shared schema files", () => createSharedExternalSchemaFile(context)) - await step("Creating shared schema files via tsc", () => createSharedExternalSchemaFileViaTSC(context)) - await step("Creating shared schema files via structure", () => createSharedExternalSchemaFileViaStructure(context)) - await step("Creating shared return position schema files", () => createSharedReturnPositionSchemaFile(context)) return [ @@ -26,6 +23,7 @@ export const createSharedSchemaFiles = async (context: AppContext, verbose: bool } function createSharedExternalSchemaFile(context: AppContext) { + console.time("createSharedExternalSchemaFile") const gql = context.gql const types = gql.getTypeMap() const knownPrimitives = ["String", "Boolean", "Int"] @@ -33,11 +31,13 @@ function createSharedExternalSchemaFile(context: AppContext) { const { prisma, fieldFacts } = context const mapper = typeMapper(context, {}) - const externalTSFile = context.tsProject.createSourceFile(`/source/${context.pathSettings.sharedFilename}`, "") + const priorFile = "" + const dts = builder(priorFile, {}) - const interfaces = [] as tsMorph.InterfaceDeclarationStructure[] - const typeAliases = [] as tsMorph.TypeAliasDeclarationStructure[] + // const externalTSFile = context.tsProject.createSourceFile(`/source/${context.pathSettings.sharedFilename}`, "") + const statements = [] as BlockStatement[] + console.time("createSharedExternalSchemaFile:types-loop") Object.keys(types).forEach((name) => { if (name.startsWith("__")) { return @@ -61,7 +61,9 @@ function createSharedExternalSchemaFile(context: AppContext) { docs.push(type.description) } - interfaces.push({ + // dts.rootScope.addTypeAlias(type.name, null, true) + + statements.push({ kind: tsMorph.StructureKind.Interface, name: type.name, isExported: true, @@ -86,7 +88,7 @@ function createSharedExternalSchemaFile(context: AppContext) { const isOptionalInSDL = !graphql.isNonNullType(type) const doesNotExistInPrisma = false // !prismaField; - const field: tsMorph.OptionalKind = { + const field = { name: fieldName, type: mapper.map(type, { preferNullOverUndefined: true }), docs, @@ -99,7 +101,7 @@ function createSharedExternalSchemaFile(context: AppContext) { } if (graphql.isEnumType(type)) { - typeAliases.push({ + statements.push({ name: type.name, isExported: true, kind: tsMorph.StructureKind.TypeAlias, @@ -114,7 +116,7 @@ function createSharedExternalSchemaFile(context: AppContext) { } if (graphql.isUnionType(type)) { - typeAliases.push({ + statements.push({ name: type.name, isExported: true, kind: tsMorph.StructureKind.TypeAlias, @@ -125,21 +127,27 @@ function createSharedExternalSchemaFile(context: AppContext) { }) } }) + console.timeEnd("createSharedExternalSchemaFile:types-loop") - context.tsProject.forgetNodesCreatedInBlock(() => { - externalTSFile.addInterfaces(interfaces) - }) + console.time("createSharedExternalSchemaFile:scalars") + const { scalars } = mapper.getReferencedGraphQLThingsInMapping() + for (const s of scalars) { + dts.rootScope.addTypeAlias(s, t.tsAnyKeyword()) + } - context.tsProject.forgetNodesCreatedInBlock(() => { - externalTSFile.addTypeAliases(typeAliases) - }) + console.timeEnd("createSharedExternalSchemaFile:scalars") - const { scalars } = mapper.getReferencedGraphQLThingsInMapping() - if (scalars.length) externalTSFile.addTypeAliases(scalars.map((s) => ({ name: s, type: "any" }))) + console.time("createSharedExternalSchemaFile:write") + // externalTSFile.set({ statements }) - const fullPath = context.join(context.pathSettings.typesFolderRoot, context.pathSettings.sharedFilename) - const text = externalTSFile.getText() + console.timeEnd("createSharedExternalSchemaFile:write") + console.time("createSharedExternalSchemaFile:read") + const text = dts.getResult() + console.timeEnd("createSharedExternalSchemaFile:read") + + console.timeEnd("createSharedExternalSchemaFile") + const fullPath = context.join(context.pathSettings.typesFolderRoot, context.pathSettings.sharedFilename) const prior = context.sys.readFile(fullPath) if (prior !== text) context.sys.writeFile(fullPath, text) } @@ -168,8 +176,7 @@ async function createSharedReturnPositionSchemaFile(context: AppContext) { ` ) - const interfaces = [] as tsMorph.InterfaceDeclarationStructure[] - const typeAliases = [] as tsMorph.TypeAliasDeclarationStructure[] + const statements = [] as tsMorph.StatementStructures[] Object.keys(types).forEach((name) => { if (name.startsWith("__")) { @@ -191,7 +198,7 @@ async function createSharedReturnPositionSchemaFile(context: AppContext) { return } - interfaces.push({ + statements.push({ name: type.name, kind: tsMorph.StructureKind.Interface, isExported: true, @@ -219,7 +226,7 @@ async function createSharedReturnPositionSchemaFile(context: AppContext) { } if (graphql.isEnumType(type)) { - typeAliases.push({ + statements.push({ name: type.name, isExported: true, kind: tsMorph.StructureKind.TypeAlias, @@ -234,7 +241,7 @@ async function createSharedReturnPositionSchemaFile(context: AppContext) { } if (graphql.isUnionType(type)) { - typeAliases.push({ + statements.push({ name: type.name, kind: tsMorph.StructureKind.TypeAlias, isExported: true, @@ -247,35 +254,32 @@ async function createSharedReturnPositionSchemaFile(context: AppContext) { }) const { scalars, prisma: prismaModels } = mapper.getReferencedGraphQLThingsInMapping() - if (scalars.length) { - externalTSFile.addTypeAliases( - scalars.map((s) => ({ - name: s, - type: "any", - })) - ) + for (const s of scalars) { + statements.push({ + kind: tsMorph.StructureKind.TypeAlias, + name: s, + type: "any", + }) } const allPrismaModels = [...new Set([...prismaModels, ...typesToImport])].sort() if (allPrismaModels.length) { - externalTSFile.addImportDeclaration({ - isTypeOnly: true, + statements.push({ + kind: tsMorph.StructureKind.ImportDeclaration, moduleSpecifier: `@prisma/client`, namedImports: allPrismaModels.map((p) => `${p} as P${p}`), }) - externalTSFile.addTypeAliases( - allPrismaModels.map((p) => ({ - isExported: true, + for (const p of allPrismaModels) { + statements.push({ + kind: tsMorph.StructureKind.TypeAlias, name: p, type: `P${p}`, - })) - ) + }) + } } - externalTSFile.addInterfaces(interfaces) - externalTSFile.addTypeAliases(typeAliases) - + externalTSFile.set({ statements }) const fullPath = context.join(context.pathSettings.typesFolderRoot, context.pathSettings.sharedInternalFilename) const formatted = await formatDTS(fullPath, externalTSFile.getText()) diff --git a/src/sharedSchemaStructures.ts b/src/sharedSchemaStructures.ts deleted file mode 100644 index 74fcf67..0000000 --- a/src/sharedSchemaStructures.ts +++ /dev/null @@ -1,154 +0,0 @@ -/// The main schema for objects and inputs - -import * as graphql from "graphql" -import * as tsMorph from "ts-morph" - -import { AppContext } from "./context.js" -import { typeMapper } from "./typeMap.js" - -export function createSharedExternalSchemaFileViaStructure(context: AppContext) { - const { gql, prisma, fieldFacts } = context - const types = gql.getTypeMap() - const mapper = typeMapper(context, { preferPrismaModels: true }) - - const typesToImport = [] as string[] - const knownPrimitives = ["String", "Boolean", "Int"] - - const externalTSFile = context.tsProject.createSourceFile( - `/source/a/${context.pathSettings.sharedInternalFilename}`, - ` -// You may very reasonably ask yourself, 'what is this file?' and why do I need it. - -// Roughly, this file ensures that when a resolver wants to return a type - that -// type will match a prisma model. This is useful because you can trivially extend -// the type in the SDL and not have to worry about type mis-matches because the thing -// you returned does not include those functions. - -// This gets particularly valuable when you want to return a union type, an interface, -// or a model where the prisma model is nested pretty deeply (GraphQL connections, for example.) - -` - ) - - const statements = [] as tsMorph.StatementStructures[] - - Object.keys(types).forEach((name) => { - if (name.startsWith("__")) { - return - } - - if (knownPrimitives.includes(name)) { - return - } - - const type = types[name] - const pType = prisma.get(name) - - if (graphql.isObjectType(type) || graphql.isInterfaceType(type) || graphql.isInputObjectType(type)) { - // Return straight away if we have a matching type in the prisma schema - // as we dont need it - if (pType) { - typesToImport.push(name) - return - } - - statements.push({ - name: type.name, - kind: tsMorph.StructureKind.Interface, - isExported: true, - docs: [], - properties: [ - { - name: "__typename", - type: `"${type.name}"`, - hasQuestionToken: true, - }, - ...Object.entries(type.getFields()).map(([fieldName, obj]: [string, graphql.GraphQLField]) => { - const hasResolverImplementation = fieldFacts.get(name)?.[fieldName]?.hasResolverImplementation - const isOptionalInSDL = !graphql.isNonNullType(obj.type) - const doesNotExistInPrisma = false // !prismaField; - - const field: tsMorph.OptionalKind = { - name: fieldName, - type: mapper.map(obj.type, { preferNullOverUndefined: true }), - hasQuestionToken: hasResolverImplementation ?? (isOptionalInSDL || doesNotExistInPrisma), - } - return field - }), - ], - }) - } - - if (graphql.isEnumType(type)) { - statements.push({ - name: type.name, - isExported: true, - kind: tsMorph.StructureKind.TypeAlias, - type: - '"' + - type - .getValues() - .map((m) => (m as { value: string }).value) - .join('" | "') + - '"', - }) - } - - if (graphql.isUnionType(type)) { - statements.push({ - name: type.name, - kind: tsMorph.StructureKind.TypeAlias, - isExported: true, - type: type - .getTypes() - .map((m) => m.name) - .join(" | "), - }) - } - }) - - const { scalars, prisma: prismaModels } = mapper.getReferencedGraphQLThingsInMapping() - if (scalars.length) { - statements.push( - ...scalars.map( - (s) => - ({ - kind: tsMorph.StructureKind.TypeAlias, - name: s, - type: "any", - } as const) - ) - ) - } - - const allPrismaModels = [...new Set([...prismaModels, ...typesToImport])].sort() - if (allPrismaModels.length) { - statements.push({ - kind: tsMorph.StructureKind.ImportDeclaration, - moduleSpecifier: `@prisma/client`, - namedImports: allPrismaModels.map((p) => `${p} as P${p}`), - }) - - statements.push( - ...allPrismaModels.map( - (p) => - ({ - kind: tsMorph.StructureKind.TypeAlias, - name: p, - type: `P${p}`, - } as const) - ) - ) - } - - const fullPath = context.join(context.pathSettings.typesFolderRoot, context.pathSettings.sharedInternalFilename) - externalTSFile.set({ statements }) - const text = externalTSFile.getText() - - // console.log(sourceFileStructure.statements) - // console.log(text) - // const formatted = await formatDTS(fullPath, externalTSFile) - - const prior = context.sys.readFile(fullPath) - if (prior !== text) context.sys.writeFile(fullPath, text) -} diff --git a/src/sharedSchemaTSC.ts b/src/sharedSchemaTSC.ts deleted file mode 100644 index e175b40..0000000 --- a/src/sharedSchemaTSC.ts +++ /dev/null @@ -1,134 +0,0 @@ -/// The main schema for objects and inputs - -import * as graphql from "graphql" -import ts from "typescript" - -import { AppContext } from "./context.js" -import { formatDTS } from "./formatDTS.js" -import { typeMapper } from "./typeMap.js" - -export function createSharedExternalSchemaFileViaTSC(context: AppContext) { - const gql = context.gql - const types = gql.getTypeMap() - const knownPrimitives = ["String", "Boolean", "Int"] - - const { prisma, fieldFacts } = context - const mapper = typeMapper(context, {}) - - const statements = [] as ts.Statement[] - - console.time("") - - Object.keys(types).forEach((name) => { - if (name.startsWith("__")) { - return - } - - if (knownPrimitives.includes(name)) { - return - } - - const type = types[name] - const pType = prisma.get(name) - - if (graphql.isObjectType(type) || graphql.isInterfaceType(type) || graphql.isInputObjectType(type)) { - // This is slower than it could be, use the add many at once api - const docs = [] - if (pType?.leadingComments) { - docs.push(pType.leadingComments) - } - - if (type.description) { - docs.push(type.description) - } - - const properties = [ - ts.factory.createPropertySignature( - undefined, - "__typename", - ts.factory.createToken(ts.SyntaxKind.QuestionToken), - ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(type.name)) - ), - ] - - Object.entries(type.getFields()).forEach(([fieldName, obj]: [string, graphql.GraphQLField]) => { - const docs = [] - const prismaField = pType?.properties.get(fieldName) - const type = obj.type as graphql.GraphQLType - - if (prismaField?.leadingComments.length) { - docs.push(prismaField.leadingComments.trim()) - } - - // if (obj.description) docs.push(obj.description); - const hasResolverImplementation = fieldFacts.get(name)?.[fieldName]?.hasResolverImplementation - const isOptionalInSDL = !graphql.isNonNullType(type) - const doesNotExistInPrisma = false // !prismaField; - - const hasQuestionToken = hasResolverImplementation ?? (isOptionalInSDL || doesNotExistInPrisma) - const mappedType = mapper.map(type, { preferNullOverUndefined: true }) - if (mappedType) { - properties.push( - ts.factory.createPropertySignature( - undefined, - fieldName, - hasQuestionToken ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined, - ts.factory.createTypeReferenceNode(mappedType) - ) - ) - } - }) - - const interfaceD = ts.factory.createInterfaceDeclaration( - [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - ts.factory.createIdentifier(name), - undefined, - undefined, - properties - ) - - statements.push(interfaceD) - } - - if (graphql.isEnumType(type)) { - const values = type.getValues().map((m) => (m as { value: string }).value) - const typeKind = `"${values.join('" | "')}"` - - statements.push(ts.factory.createTypeAliasDeclaration(undefined, type.name, [], ts.factory.createTypeReferenceNode(typeKind))) - } - - if (graphql.isUnionType(type)) { - const types = type.getTypes().map((t) => t.name) - const typeKind = types.join(" | ") - statements.push( - ts.factory.createTypeAliasDeclaration( - [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - type.name, - [], - ts.factory.createTypeReferenceNode(typeKind) - ) - ) - } - }) - - const { scalars } = mapper.getReferencedGraphQLThingsInMapping() - if (scalars.length) { - statements.push( - ts.factory.createTypeAliasDeclaration( - [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - "Scalars", - [], - ts.factory.createTypeReferenceNode(`{ ${scalars.join(", ")} }`) - ) - ) - } - - const sourceFile = ts.factory.createSourceFile(statements, ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None) - const printer = ts.createPrinter({}) - const result = printer.printNode(ts.EmitHint.Unspecified, sourceFile, sourceFile) - - const fullPath = context.join(context.pathSettings.typesFolderRoot, context.pathSettings.sharedFilename) - - const prior = context.sys.readFile(fullPath) - if (prior !== result) context.sys.writeFile(fullPath, result) -} diff --git a/src/tests/features/generatesTypesForUnions.test.ts b/src/tests/features/generatesTypesForUnions.test.ts index a6845be..f253763 100644 --- a/src/tests/features/generatesTypesForUnions.test.ts +++ b/src/tests/features/generatesTypesForUnions.test.ts @@ -42,16 +42,20 @@ export const Game = { __typename?: \\"Game\\"; id?: number; } + export interface Puzzle { __typename?: \\"Puzzle\\"; id: number; } + export type Gameish = Game | Puzzle; + export interface Query { __typename?: \\"Query\\"; gameObj?: Game| null | Puzzle| null| null; gameArr: (Game | Puzzle)[]; } + export interface Mutation { __typename?: \\"Mutation\\"; __?: string| null; diff --git a/src/tests/features/supportRefferingToEnumsOnlyInSDL.test.ts b/src/tests/features/supportReferringToEnumsOnlyInSDL.test.ts similarity index 96% rename from src/tests/features/supportRefferingToEnumsOnlyInSDL.test.ts rename to src/tests/features/supportReferringToEnumsOnlyInSDL.test.ts index 2796bb9..66388da 100644 --- a/src/tests/features/supportRefferingToEnumsOnlyInSDL.test.ts +++ b/src/tests/features/supportReferringToEnumsOnlyInSDL.test.ts @@ -70,11 +70,14 @@ export const Game: GameResolvers = {}; id: number; games: Game[]; } + export interface Query { __typename?: \\"Query\\"; allGames: Game[]; } - type GameType = \\"FOOTBALL\\" | \\"BASKETBALL\\"; + + export type GameType = \\"FOOTBALL\\" | \\"BASKETBALL\\"; + export interface Mutation { __typename?: \\"Mutation\\"; __?: string| null; diff --git a/src/tsBuilder.ts b/src/tsBuilder.ts new file mode 100644 index 0000000..f63c145 --- /dev/null +++ b/src/tsBuilder.ts @@ -0,0 +1,198 @@ +// @eslint-disable-file + +import generator from "@babel/generator" +import parser from "@babel/parser" +import traverse from "@babel/traverse" +import t, { BlockStatement, ExpressionStatement, Node, Statement, TSType, TSTypeAliasDeclaration, TypeAlias } from "@babel/types" + +export const builder = (priorSource: string, opts: {}) => { + const sourceFile = parser.parse(priorSource, { sourceType: "module", plugins: ["jsx", "typescript"] }) + + const setImport = (source: string, opts: { mainImport?: string; subImports?: string[] }) => { + const imports = sourceFile.program.body.filter((s) => s.type === "ImportDeclaration") + + const existing = imports.find((i) => i.source.value === source) + if (!existing) { + const imports = [] as (t.ImportSpecifier | t.ImportDefaultSpecifier)[] + if (opts.mainImport) { + imports.push(t.importDefaultSpecifier(t.identifier(opts.mainImport))) + } + + if (opts.subImports) { + imports.push(...opts.subImports.map((si) => t.importSpecifier(t.identifier(si), t.identifier(si)))) + } + + const importDeclaration = t.importDeclaration(imports, t.stringLiteral(source)) + sourceFile.program.body.push(importDeclaration) + return + } + + if (!existing.specifiers.find((f) => f.type === "ImportDefaultSpecifier") && opts.mainImport) { + existing.specifiers.push(t.importDefaultSpecifier(t.identifier(opts.mainImport))) + } + + if (opts.subImports) { + const existingImports = existing.specifiers.map((e) => e.local.name) + const newImports = opts.subImports.filter((si) => !existingImports.includes(si)) + + if (newImports.length) { + existing.specifiers.push(...newImports.map((si) => t.importSpecifier(t.identifier(si), t.identifier(si)))) + } + } + } + + const setTypeViaTemplate = (template: string) => { + const type = parser.parse(template, { sourceType: "module", plugins: ["jsx", "typescript"] }) + + const typeDeclaration = type.program.body.find((s) => s.type === "TSTypeAliasDeclaration") + if (!typeDeclaration) throw new Error("No type declaration found in template: " + template) + + const oldTypeDeclaration = sourceFile.program.body.find( + (s) => s.type === "TSTypeAliasDeclaration" && s.id.name === typeDeclaration.id.name + ) + if (!oldTypeDeclaration) { + sourceFile.program.body.push(typeDeclaration) + return + } + + if (!t.isTSTypeAliasDeclaration(oldTypeDeclaration)) throw new Error("Expected TSTypeAliasDeclaration") + + const newAnnotion = typeDeclaration.typeAnnotation + + // is literal primitive + if (newAnnotion.type.endsWith("LiteralTypeAnnotation")) { + oldTypeDeclaration.typeAnnotation = newAnnotion + return + } + + if (t.isTSTypeLiteral(newAnnotion) && t.isTSTypeLiteral(oldTypeDeclaration.typeAnnotation)) { + for (const field of newAnnotion.members) { + const matchingOnOld = oldTypeDeclaration.typeAnnotation.members.find((mm) => { + if (!t.isTSPropertySignature(mm) || !t.isTSPropertySignature(field)) return false + if (!t.isIdentifier(mm.key) || !t.isIdentifier(field.key)) return false + return mm.key.name === field.key.name + }) + + if (matchingOnOld) { + matchingOnOld.typeAnnotation = field.typeAnnotation + } else { + oldTypeDeclaration.typeAnnotation.members.push(field) + } + } + + return + } + + throw new Error(`Unsupported type annotation: ${newAnnotion.type} - ${generator(newAnnotion).code}`) + } + + const createScope = (name: string, statements: Statement[]) => { + const addFunction = (name: string) => { + let functionNode = statements.find( + (s) => t.isVariableDeclaration(s) && t.isIdentifier(s.declarations[0].id) && s.declarations[0].id.name === name + ) + + if (!functionNode) { + functionNode = t.variableDeclaration("const", [ + t.variableDeclarator(t.identifier(name), t.arrowFunctionExpression([], t.blockStatement([]))), + ]) + statements.push(functionNode) + } + + const arrowFn = functionNode.declarations[0].init as t.ArrowFunctionExpression + if (!t.isArrowFunctionExpression(arrowFn)) throw new Error("Expected ArrowFunctionExpression") + + return { + node: arrowFn, + addParam: (name: string, type: string) => { + const param = t.identifier(name) + + const fromParse = getTypeLevelAST(type) + param.typeAnnotation = t.tsTypeAnnotation(fromParse) + + const exists = arrowFn.params.find((p) => p.type === "Identifier" && p.name === name) + if (!exists) arrowFn.params.push(param) + else exists.typeAnnotation = param.typeAnnotation + }, + + scope: createScope(name, (arrowFn.body as BlockStatement).body), + } + } + + const addVariableDeclaration = (name: string, add: (prior: t.Expression | undefined) => t.Expression) => { + const prior = statements.find( + (b) => t.isVariableDeclaration(b) && t.isIdentifier(b.declarations[0].id) && b.declarations[0].id.name === name + ) + + if (prior && t.isVariableDeclaration(prior) && t.isVariableDeclarator(prior.declarations[0]) && prior.declarations[0].init) { + prior.declarations[0].init = add(prior.declarations[0].init) + return + } + + const declaration = t.variableDeclaration("const", [t.variableDeclarator(t.identifier(name), add(undefined))]) + statements.push(declaration) + } + + const addTypeAlias = (name: string, type: TSType, exported?: boolean) => { + const prior = statements.find( + (s) => + (t.isTSTypeAliasDeclaration(s) && s.id.name === name) || + (t.isExportNamedDeclaration(s) && t.isTSTypeAliasDeclaration(s.declaration) && s.declaration.id.name === name) + ) + if (prior) return + + if (exported) { + statements.push(t.exportNamedDeclaration(t.tsTypeAliasDeclaration(t.identifier(name), null, type))) + } else { + statements.push(t.tsTypeAliasDeclaration(t.identifier(name), null, type)) + } + } + + return { + addFunction, + addVariableDeclaration, + addTypeAlias, + } + } + + const updateGraphQLTemplateTag = (expression: t.Expression, path: string, modelFields: string[]) => { + if (path !== ".") throw new Error("Only support updating the root of the graphql tag ATM") + traverse( + expression, + { + TaggedTemplateExpression(path) { + const { tag, quasi } = path.node + if (t.isIdentifier(tag) && tag.name === "graphql") { + // This is the graphql query + const query = quasi.quasis[0].value.raw + const inner = query.match(/\{(.*)\}/)?.[1] + + path.replaceWithSourceString(`graphql\`${query.replace(inner, `${inner}, ${modelFields.join(", ")}`)}\``) + path.stop() + } + }, + }, + // Uh oh, not really sure what a Scope object does here + {} as any + ) + return expression + } + + const parseStatement = (code: string) => + parser.parse(code, { sourceType: "module", plugins: ["jsx", "typescript"] }).program.body[0] as ExpressionStatement + + const getResult = () => { + return generator(sourceFile.program, {}).code + } + + const rootScope = createScope("root", sourceFile.program.body) + return { setImport, getResult, setTypeViaTemplate, parseStatement, updateGraphQLTemplateTag, rootScope } +} + +/** Parses something as though it is in type-space and extracts the subset of the AST that the string represents */ +const getTypeLevelAST = (type: string) => { + const typeAST = parser.parse(`type A = ${type}`, { sourceType: "module", plugins: ["jsx", "typescript"] }) + const typeDeclaration = typeAST.program.body.find((s) => s.type === "TSTypeAliasDeclaration") + if (!typeDeclaration) throw new Error("No type declaration found in template: " + type) + return typeDeclaration.typeAnnotation +} From 3db7027f8b091f180b91273b373b6f23942f3e7b Mon Sep 17 00:00:00 2001 From: Orta Therox Date: Sun, 10 Nov 2024 10:51:46 +0000 Subject: [PATCH 6/9] First run with the builder --- src/sharedSchema.ts | 54 ++++++++----------- .../features/generatesTypesForUnions.test.ts | 22 ++++---- .../supportReferringToEnumsOnlyInSDL.test.ts | 20 +++---- src/tsBuilder.ts | 29 ++++++++-- 4 files changed, 63 insertions(+), 62 deletions(-) diff --git a/src/sharedSchema.ts b/src/sharedSchema.ts index 1c54210..333bf6d 100644 --- a/src/sharedSchema.ts +++ b/src/sharedSchema.ts @@ -63,16 +63,13 @@ function createSharedExternalSchemaFile(context: AppContext) { // dts.rootScope.addTypeAlias(type.name, null, true) - statements.push({ - kind: tsMorph.StructureKind.Interface, - name: type.name, - isExported: true, - docs: [], - properties: [ + dts.rootScope.addInterface( + type.name, + [ { name: "__typename", type: `"${type.name}"`, - hasQuestionToken: true, + optional: true, }, ...Object.entries(type.getFields()).map(([fieldName, obj]: [string, graphql.GraphQLField]) => { const docs = [] @@ -90,41 +87,34 @@ function createSharedExternalSchemaFile(context: AppContext) { const field = { name: fieldName, - type: mapper.map(type, { preferNullOverUndefined: true }), - docs, - hasQuestionToken: hasResolverImplementation ?? (isOptionalInSDL || doesNotExistInPrisma), + type: mapper.map(type, { preferNullOverUndefined: true })!, + // docs, + optional: hasResolverImplementation ?? (isOptionalInSDL || doesNotExistInPrisma), } return field }), ], - }) + true + ) } if (graphql.isEnumType(type)) { - statements.push({ - name: type.name, - isExported: true, - kind: tsMorph.StructureKind.TypeAlias, - type: - '"' + - type - .getValues() - .map((m) => (m as { value: string }).value) - .join('" | "') + - '"', - }) + const union = + '"' + + type + .getValues() + .map((m) => (m as { value: string }).value) + .join('" | "') + + '"' + dts.rootScope.addTypeAlias(type.name, t.tsTypeReference(t.identifier(union)), true) } if (graphql.isUnionType(type)) { - statements.push({ - name: type.name, - isExported: true, - kind: tsMorph.StructureKind.TypeAlias, - type: type - .getTypes() - .map((m) => m.name) - .join(" | "), - }) + const union = type + .getTypes() + .map((m) => m.name) + .join(" | ") + dts.rootScope.addTypeAlias(type.name, t.tsTypeReference(t.identifier(union)), true) } }) console.timeEnd("createSharedExternalSchemaFile:types-loop") diff --git a/src/tests/features/generatesTypesForUnions.test.ts b/src/tests/features/generatesTypesForUnions.test.ts index f253763..508e3da 100644 --- a/src/tests/features/generatesTypesForUnions.test.ts +++ b/src/tests/features/generatesTypesForUnions.test.ts @@ -39,26 +39,22 @@ export const Game = { const dts = vfsMap.get("/types/shared-schema-types.d.ts")! expect(dts.trim()).toMatchInlineSnapshot(` "export interface Game { - __typename?: \\"Game\\"; - id?: number; + __typename?: \\"Game\\"; + id?: number; } - export interface Puzzle { - __typename?: \\"Puzzle\\"; - id: number; + __typename?: \\"Puzzle\\"; + id: number; } - export type Gameish = Game | Puzzle; - export interface Query { - __typename?: \\"Query\\"; - gameObj?: Game| null | Puzzle| null| null; - gameArr: (Game | Puzzle)[]; + __typename?: \\"Query\\"; + gameObj?: Game| null | Puzzle| null| null; + gameArr: (Game | Puzzle)[]; } - export interface Mutation { - __typename?: \\"Mutation\\"; - __?: string| null; + __typename?: \\"Mutation\\"; + __?: string| null; }" `) }) diff --git a/src/tests/features/supportReferringToEnumsOnlyInSDL.test.ts b/src/tests/features/supportReferringToEnumsOnlyInSDL.test.ts index 66388da..644fc15 100644 --- a/src/tests/features/supportReferringToEnumsOnlyInSDL.test.ts +++ b/src/tests/features/supportReferringToEnumsOnlyInSDL.test.ts @@ -66,22 +66,18 @@ export const Game: GameResolvers = {}; expect(vfsMap.get("/types/shared-schema-types.d.ts"))!.toMatchInlineSnapshot(` "export interface Game { - __typename?: \\"Game\\"; - id: number; - games: Game[]; + __typename?: \\"Game\\"; + id: number; + games: Game[]; } - export interface Query { - __typename?: \\"Query\\"; - allGames: Game[]; + __typename?: \\"Query\\"; + allGames: Game[]; } - export type GameType = \\"FOOTBALL\\" | \\"BASKETBALL\\"; - export interface Mutation { - __typename?: \\"Mutation\\"; - __?: string| null; - } - " + __typename?: \\"Mutation\\"; + __?: string| null; + }" `) }) diff --git a/src/tsBuilder.ts b/src/tsBuilder.ts index f63c145..49afca6 100644 --- a/src/tsBuilder.ts +++ b/src/tsBuilder.ts @@ -141,17 +141,36 @@ export const builder = (priorSource: string, opts: {}) => { ) if (prior) return - if (exported) { - statements.push(t.exportNamedDeclaration(t.tsTypeAliasDeclaration(t.identifier(name), null, type))) - } else { - statements.push(t.tsTypeAliasDeclaration(t.identifier(name), null, type)) - } + const alias = t.tsTypeAliasDeclaration(t.identifier(name), null, type) + statements.push(exported ? t.exportNamedDeclaration(alias) : alias) + } + + const addInterface = (name: string, fields: { docs?: string; name: string; optional: boolean; type: string }[], exported?: boolean) => { + const prior = statements.find( + (s) => + (t.isTSInterfaceDeclaration(s) && s.id.name === name) || + (t.isExportNamedDeclaration(s) && t.isTSInterfaceDeclaration(s.declaration) && s.declaration.id.name === name) + ) + + if (prior) return + + const body = t.tsInterfaceBody( + fields.map((f) => { + const prop = t.tsPropertySignature(t.identifier(f.name), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(f.type)))) + prop.optional = f.optional + return prop + }) + ) + + const alias = t.tsInterfaceDeclaration(t.identifier(name), null, null, body) + statements.push(exported ? t.exportNamedDeclaration(alias) : alias) } return { addFunction, addVariableDeclaration, addTypeAlias, + addInterface, } } From 98a60b22f96095e7cdb248306d7753b1a4d7a6a0 Mon Sep 17 00:00:00 2001 From: Orta Therox Date: Sun, 10 Nov 2024 23:03:06 +0000 Subject: [PATCH 7/9] WIP progress on the builder --- src/serviceFile2.ts | 311 ++++++++++++++++++ src/sharedSchema.ts | 124 ++----- .../bugs/parentCanBeGraphQLObject.test.ts | 10 +- ...returnObjectCanBeGraphQLInterfaces.test.ts | 31 +- .../features/generatesTypesForUnions.test.ts | 10 +- .../features/preferPromiseFnWhenKnown.test.ts | 129 ++------ ...turnTypePositionsWhichPreferPrisma.test.ts | 43 +-- .../supportReferringToEnumsOnlyInSDL.test.ts | 38 +-- src/tests/testRunner.ts | 2 +- src/tsBuilder.ts | 125 ++++++- src/utils.ts | 21 +- 11 files changed, 532 insertions(+), 312 deletions(-) create mode 100644 src/serviceFile2.ts diff --git a/src/serviceFile2.ts b/src/serviceFile2.ts new file mode 100644 index 0000000..355abaa --- /dev/null +++ b/src/serviceFile2.ts @@ -0,0 +1,311 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ + +import t, { tsTypeAnnotation } from "@babel/types" +import * as graphql from "graphql" + +import { AppContext } from "./context.js" +import { getCodeFactsForJSTSFileAtPath } from "./serviceFile.codefacts.js" +import { builder } from "./tsBuilder.js" +import { CodeFacts, ModelResolverFacts, ResolverFuncFact } from "./typeFacts.js" +import { TypeMapper, typeMapper } from "./typeMap.js" +import { capitalizeFirstLetter, createAndReferOrInlineArgsForField, inlineArgsForField } from "./utils.js" + +export const lookAtServiceFile = async (file: string, context: AppContext) => { + const { gql, prisma, pathSettings: settings, codeFacts: serviceFacts, fieldFacts } = context + + if (!gql) throw new Error(`No schema when wanting to look at service file: ${file}`) + if (!prisma) throw new Error(`No prisma schema when wanting to look at service file: ${file}`) + + // This isn't good enough, needs to be relative to api/src/services + const fileKey = file.replace(settings.apiServicesPath, "") + + const thisFact: CodeFacts = {} + + const filename = context.basename(file) + const dts = builder("", {}) + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const queryType = gql.getQueryType()! + if (!queryType) throw new Error("No query type") + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const mutationType = gql.getMutationType()! + if (!mutationType) throw new Error("No mutation type") + + const externalMapper = typeMapper(context, { preferPrismaModels: true }) + const returnTypeMapper = typeMapper(context, {}) + + // The description of the source file + const fileFacts = getCodeFactsForJSTSFileAtPath(file, context) + if (Object.keys(fileFacts).length === 0) return + + // Tracks prospective prisma models which are used in the file + const extraPrismaReferences = new Set() + const extraSharedFileImportReferences = new Set<{ import: string; name?: string }>() + + // Basically if a top level resolver reference Query or Mutation + const knownSpecialCasesForGraphQL = new Set() + + // Add the root function declarations + const rootResolvers = fileFacts.maybe_query_mutation?.resolvers + if (rootResolvers) + rootResolvers.forEach((v) => { + const isQuery = v.name in queryType.getFields() + const isMutation = v.name in mutationType.getFields() + const parentName = isQuery ? queryType.name : isMutation ? mutationType.name : undefined + if (parentName) { + addDefinitionsForTopLevelResolvers(parentName, v) + } else { + // Add warning about unused resolver + dts.rootScope.addInterface(v.name, [], { exported: true, docs: "This resolver does not exist on Query or Mutation" }) + } + }) + + // Add the root function declarations + Object.values(fileFacts).forEach((model) => { + if (!model) return + const skip = ["maybe_query_mutation", queryType.name, mutationType.name] + if (skip.includes(model.typeName)) return + + addCustomTypeModel(model) + }) + + // Set up the module imports at the top + const sharedGraphQLObjectsReferenced = externalMapper.getReferencedGraphQLThingsInMapping() + const sharedGraphQLObjectsReferencedTypes = [...sharedGraphQLObjectsReferenced.types, ...knownSpecialCasesForGraphQL] + const sharedInternalGraphQLObjectsReferenced = returnTypeMapper.getReferencedGraphQLThingsInMapping() + + const aliases = [...new Set([...sharedGraphQLObjectsReferenced.scalars, ...sharedInternalGraphQLObjectsReferenced.scalars])] + + for (const alias of aliases) { + dts.rootScope.addTypeAlias(alias, "any") + } + + const prismases = [ + ...new Set([ + ...sharedGraphQLObjectsReferenced.prisma, + ...sharedInternalGraphQLObjectsReferenced.prisma, + ...extraPrismaReferences.values(), + ]), + ] + + const validPrismaObjs = prismases.filter((p) => prisma.has(p)) + if (validPrismaObjs.length) { + dts.setImport("@prisma/client", { subImports: validPrismaObjs.map((p) => `${p} as P${p}`) }) + } + + const initialResult = dts.getResult() + if (initialResult.includes("GraphQLResolveInfo")) { + dts.setImport("graphql", { subImports: ["GraphQLResolveInfo"] }) + } + + if (initialResult.includes("RedwoodGraphQLContext")) { + dts.setImport("@redwoodjs/graphql-server/dist/types", { subImports: ["RedwoodGraphQLContext"] }) + } + + if (sharedInternalGraphQLObjectsReferenced.types.length || extraSharedFileImportReferences.size) { + const source = `./${settings.sharedInternalFilename.replace(".d.ts", "")}` + dts.setImport(source, { + subImports: [ + ...sharedInternalGraphQLObjectsReferenced.types.map((t) => `${t} as RT${t}`), + ...[...extraSharedFileImportReferences.values()].map((t) => ("name" in t && t.name ? `${t.import} as ${t.name}` : t.import)), + ], + }) + } + + if (sharedGraphQLObjectsReferencedTypes.length) { + const source = `./${settings.sharedFilename.replace(".d.ts", "")}` + dts.setImport(source, { subImports: sharedGraphQLObjectsReferencedTypes }) + } + + serviceFacts.set(fileKey, thisFact) + + const dtsFilename = filename.endsWith(".ts") ? filename.replace(".ts", ".d.ts") : filename.replace(".js", ".d.ts") + const dtsFilepath = context.join(context.pathSettings.typesFolderRoot, dtsFilename) + + // Some manual formatting tweaks so we align with Redwood's setup more + const final = dts.getResult() + + const shouldWriteDTS = !!final.trim().length + if (!shouldWriteDTS) return + + const formatted = final // await formatDTS(dtsFilepath, dts) + + // Don't make a file write if the content is the same + const priorContent = context.sys.readFile(dtsFilename) + if (priorContent === formatted) return + + context.sys.writeFile(dtsFilepath, formatted) + return dtsFilepath + + function addDefinitionsForTopLevelResolvers(parentName: string, config: ResolverFuncFact) { + const { name } = config + let field = queryType.getFields()[name] + if (!field) { + field = mutationType.getFields()[name] + } + + const nodeDocs = field.astNode + ? ["SDL: " + graphql.print(field.astNode)] + : ["@deprecated: Could not find this field in the schema for Mutation or Query"] + const interfaceName = `${capitalizeFirstLetter(config.name)}Resolver` + + const args = createAndReferOrInlineArgsForField(field, { + name: interfaceName, + dts, + mapper: externalMapper.map, + }) + + if (parentName === queryType.name) knownSpecialCasesForGraphQL.add(queryType.name) + if (parentName === mutationType.name) knownSpecialCasesForGraphQL.add(mutationType.name) + + const argsParam = args ?? "object" + const qForInfos = config.infoParamType === "just_root_destructured" ? "?" : "" + const returnType = returnTypeForResolver(returnTypeMapper, field, config) + + dts.rootScope.addInterface( + interfaceName, + [ + { + type: "call-signature", + optional: config.funcArgCount < 1, + returnType, + params: [ + { name: "args", type: argsParam, optional: config.funcArgCount < 1 }, + { + name: "obj", + type: `{ root: ${parentName}, context${qForInfos}: RedwoodGraphQLContext, info${qForInfos}: GraphQLResolveInfo }`, + optional: config.funcArgCount < 2, + }, + ], + }, + ], + { + exported: true, + docs: nodeDocs.join(" "), + } + ) + } + + /** Ideally, we want to be able to write the type for just the object */ + function addCustomTypeModel(modelFacts: ModelResolverFacts) { + const modelName = modelFacts.typeName + extraPrismaReferences.add(modelName) + + // Make an interface, this is the version we are replacing from graphql-codegen: + // Account: MergePrismaWithSdlTypes, AllMappedModels>; + const gqlType = gql.getType(modelName) + if (!gqlType) { + // throw new Error(`Could not find a GraphQL type named ${d.getName()}`); + // fileDTS.addStatements(`\n// ${modelName} does not exist in the schema`) + return + } + + if (!graphql.isObjectType(gqlType)) { + throw new Error(`In your schema ${modelName} is not an object, which we can only make resolver types for`) + } + + const fields = gqlType.getFields() + + // See: https://github.com/redwoodjs/redwood/pull/6228#issue-1342966511 + // For more ideas + + const hasGenerics = modelFacts.hasGenericArg + + const resolverInterface = dts.rootScope.addInterface(`${modelName}TypeResolvers`, [], { + exported: true, + generics: hasGenerics ? [{ name: "Extended" }] : [], + }) + + // Handle extending classes in the runtime which only exist in SDL + const parentIsPrisma = prisma.has(modelName) + if (!parentIsPrisma) extraSharedFileImportReferences.add({ name: `S${modelName}`, import: modelName }) + const suffix = parentIsPrisma ? "P" : "S" + + const parentTypeString = `${suffix}${modelName} ${createParentAdditionallyDefinedFunctions()} ${hasGenerics ? " & Extended" : ""}` + + /** + type CurrentUserAccountAsParent = SCurrentUserAccount & { + users: () => PUser[] | Promise | (() => Promise); + registeredPublishingPartner: () => Promise; + subIsViaGift: () => boolean | Promise | (() => Promise); + } + */ + + dts.rootScope.addTypeAlias(`${modelName}AsParent`, t.tsTypeReference(t.identifier(parentTypeString)), { + generics: hasGenerics ? [{ name: "Extended" }] : [], + }) + const modelFieldFacts = fieldFacts.get(modelName) ?? {} + + // Loop through the resolvers, adding the fields which have resolvers implemented in the source file + modelFacts.resolvers.forEach((resolver) => { + const field = fields[resolver.name] + if (field) { + const fieldName = resolver.name + if (modelFieldFacts[fieldName]) modelFieldFacts[fieldName].hasResolverImplementation = true + else modelFieldFacts[fieldName] = { hasResolverImplementation: true } + + const argsType = inlineArgsForField(field, { mapper: externalMapper.map }) ?? "undefined" + const param = hasGenerics ? "" : "" + + const firstQ = resolver.funcArgCount < 1 ? "?" : "" + const secondQ = resolver.funcArgCount < 2 ? "?" : "" + const qForInfos = resolver.infoParamType === "just_root_destructured" ? "?" : "" + + const innerArgs = `args${firstQ}: ${argsType}, obj${secondQ}: { root: ${modelName}AsParent${param}, context${qForInfos}: RedwoodGraphQLContext, info${qForInfos}: GraphQLResolveInfo }` + const returnType = returnTypeForResolver(returnTypeMapper, field, resolver) + const args = resolver.isFunc || resolver.isUnknown ? `(${innerArgs}) => ${returnType ?? "any"}` : returnType + + const docs = field.astNode ? `SDL: ${graphql.print(field.astNode)}` : "" + const property = t.tsPropertySignature(t.identifier(fieldName), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(args)))) + t.addComment(property, "leading", docs) + + resolverInterface.body.body.push(property) + } else { + resolverInterface.body.body.push( + t.tsPropertySignature(t.identifier(resolver.name), t.tsTypeAnnotation(t.tsTypeReference(t.identifier("void")))) + ) + } + }) + + function createParentAdditionallyDefinedFunctions() { + const fns: string[] = [] + modelFacts.resolvers.forEach((resolver) => { + const existsInGraphQLSchema = fields[resolver.name] + if (!existsInGraphQLSchema) { + console.warn( + `The service file ${filename} has a field ${resolver.name} on ${modelName} that does not exist in the generated schema.graphql` + ) + } + + const prefix = !existsInGraphQLSchema ? "\n// This field does not exist in the generated schema.graphql\n" : "" + const returnType = returnTypeForResolver(externalMapper, existsInGraphQLSchema, resolver) + // fns.push(`${prefix}${resolver.name}: () => Promise<${externalMapper.map(type, {})}>`) + fns.push(`${prefix}${resolver.name}: () => ${returnType}`) + }) + + if (fns.length < 1) return "" + return "& {" + fns.join(", \n") + "}" + } + + fieldFacts.set(modelName, modelFieldFacts) + } +} + +function returnTypeForResolver(mapper: TypeMapper, field: graphql.GraphQLField | undefined, resolver: ResolverFuncFact) { + if (!field) return "void" + + const tType = mapper.map(field.type, { preferNullOverUndefined: true, typenamePrefix: "RT" }) ?? "void" + + let returnType = tType + const all = `${tType} | Promise<${tType}> | (() => Promise<${tType}>)` + + if (resolver.isFunc && resolver.isAsync) returnType = `Promise<${tType}>` + else if (resolver.isFunc && resolver.isObjLiteral) returnType = tType + else if (resolver.isFunc) returnType = all + else if (resolver.isObjLiteral) returnType = tType + else if (resolver.isUnknown) returnType = all + + return returnType +} +/* eslint-enable @typescript-eslint/no-unnecessary-condition */ diff --git a/src/sharedSchema.ts b/src/sharedSchema.ts index 333bf6d..28c2d59 100644 --- a/src/sharedSchema.ts +++ b/src/sharedSchema.ts @@ -23,7 +23,6 @@ export const createSharedSchemaFiles = async (context: AppContext, verbose: bool } function createSharedExternalSchemaFile(context: AppContext) { - console.time("createSharedExternalSchemaFile") const gql = context.gql const types = gql.getTypeMap() const knownPrimitives = ["String", "Boolean", "Int"] @@ -34,10 +33,6 @@ function createSharedExternalSchemaFile(context: AppContext) { const priorFile = "" const dts = builder(priorFile, {}) - // const externalTSFile = context.tsProject.createSourceFile(`/source/${context.pathSettings.sharedFilename}`, "") - const statements = [] as BlockStatement[] - - console.time("createSharedExternalSchemaFile:types-loop") Object.keys(types).forEach((name) => { if (name.startsWith("__")) { return @@ -61,8 +56,6 @@ function createSharedExternalSchemaFile(context: AppContext) { docs.push(type.description) } - // dts.rootScope.addTypeAlias(type.name, null, true) - dts.rootScope.addInterface( type.name, [ @@ -72,14 +65,9 @@ function createSharedExternalSchemaFile(context: AppContext) { optional: true, }, ...Object.entries(type.getFields()).map(([fieldName, obj]: [string, graphql.GraphQLField]) => { - const docs = [] const prismaField = pType?.properties.get(fieldName) const type = obj.type as graphql.GraphQLType - if (prismaField?.leadingComments.length) { - docs.push(prismaField.leadingComments.trim()) - } - // if (obj.description) docs.push(obj.description); const hasResolverImplementation = fieldFacts.get(name)?.[fieldName]?.hasResolverImplementation const isOptionalInSDL = !graphql.isNonNullType(type) @@ -88,13 +76,13 @@ function createSharedExternalSchemaFile(context: AppContext) { const field = { name: fieldName, type: mapper.map(type, { preferNullOverUndefined: true })!, - // docs, + docs: prismaField?.leadingComments.trim(), optional: hasResolverImplementation ?? (isOptionalInSDL || doesNotExistInPrisma), } return field }), ], - true + { exported: true, docs: docs.join(" ") } ) } @@ -106,7 +94,7 @@ function createSharedExternalSchemaFile(context: AppContext) { .map((m) => (m as { value: string }).value) .join('" | "') + '"' - dts.rootScope.addTypeAlias(type.name, t.tsTypeReference(t.identifier(union)), true) + dts.rootScope.addTypeAlias(type.name, t.tsTypeReference(t.identifier(union)), { exported: true }) } if (graphql.isUnionType(type)) { @@ -114,29 +102,16 @@ function createSharedExternalSchemaFile(context: AppContext) { .getTypes() .map((m) => m.name) .join(" | ") - dts.rootScope.addTypeAlias(type.name, t.tsTypeReference(t.identifier(union)), true) + dts.rootScope.addTypeAlias(type.name, t.tsTypeReference(t.identifier(union)), { exported: true }) } }) - console.timeEnd("createSharedExternalSchemaFile:types-loop") - console.time("createSharedExternalSchemaFile:scalars") const { scalars } = mapper.getReferencedGraphQLThingsInMapping() for (const s of scalars) { dts.rootScope.addTypeAlias(s, t.tsAnyKeyword()) } - console.timeEnd("createSharedExternalSchemaFile:scalars") - - console.time("createSharedExternalSchemaFile:write") - // externalTSFile.set({ statements }) - - console.timeEnd("createSharedExternalSchemaFile:write") - console.time("createSharedExternalSchemaFile:read") const text = dts.getResult() - console.timeEnd("createSharedExternalSchemaFile:read") - - console.timeEnd("createSharedExternalSchemaFile") - const fullPath = context.join(context.pathSettings.typesFolderRoot, context.pathSettings.sharedFilename) const prior = context.sys.readFile(fullPath) if (prior !== text) context.sys.writeFile(fullPath, text) @@ -150,10 +125,9 @@ async function createSharedReturnPositionSchemaFile(context: AppContext) { const typesToImport = [] as string[] const knownPrimitives = ["String", "Boolean", "Int"] - const externalTSFile = context.tsProject.createSourceFile( - `/source/${context.pathSettings.sharedInternalFilename}`, - ` -// You may very reasonably ask yourself, 'what is this file?' and why do I need it. + const dts = builder("", {}) + + dts.rootScope.addLeadingComment(`// You may very reasonably ask yourself, 'what is this file?' and why do I need it. // Roughly, this file ensures that when a resolver wants to return a type - that // type will match a prisma model. This is useful because you can trivially extend @@ -162,11 +136,7 @@ async function createSharedReturnPositionSchemaFile(context: AppContext) { // This gets particularly valuable when you want to return a union type, an interface, // or a model where the prisma model is nested pretty deeply (GraphQL connections, for example.) - -` - ) - - const statements = [] as tsMorph.StatementStructures[] +`) Object.keys(types).forEach((name) => { if (name.startsWith("__")) { @@ -188,91 +158,67 @@ async function createSharedReturnPositionSchemaFile(context: AppContext) { return } - statements.push({ - name: type.name, - kind: tsMorph.StructureKind.Interface, - isExported: true, - docs: [], - properties: [ + dts.rootScope.addInterface( + type.name, + [ { name: "__typename", type: `"${type.name}"`, - hasQuestionToken: true, + optional: true, }, ...Object.entries(type.getFields()).map(([fieldName, obj]: [string, graphql.GraphQLField]) => { const hasResolverImplementation = fieldFacts.get(name)?.[fieldName]?.hasResolverImplementation const isOptionalInSDL = !graphql.isNonNullType(obj.type) const doesNotExistInPrisma = false // !prismaField; - const field: tsMorph.OptionalKind = { + const field = { name: fieldName, - type: mapper.map(obj.type, { preferNullOverUndefined: true }), - hasQuestionToken: hasResolverImplementation ?? (isOptionalInSDL || doesNotExistInPrisma), + type: mapper.map(obj.type, { preferNullOverUndefined: true })!, + optional: hasResolverImplementation ?? (isOptionalInSDL || doesNotExistInPrisma), } return field }), ], - }) + { exported: true } + ) } if (graphql.isEnumType(type)) { - statements.push({ - name: type.name, - isExported: true, - kind: tsMorph.StructureKind.TypeAlias, - type: - '"' + - type - .getValues() - .map((m) => (m as { value: string }).value) - .join('" | "') + - '"', - }) + const union = + '"' + + type + .getValues() + .map((m) => (m as { value: string }).value) + .join('" | "') + + '"' + dts.rootScope.addTypeAlias(type.name, t.tsTypeReference(t.identifier(union)), { exported: true }) } if (graphql.isUnionType(type)) { - statements.push({ - name: type.name, - kind: tsMorph.StructureKind.TypeAlias, - isExported: true, - type: type - .getTypes() - .map((m) => m.name) - .join(" | "), - }) + const union = type + .getTypes() + .map((m) => m.name) + .join(" | ") + dts.rootScope.addTypeAlias(type.name, t.tsTypeReference(t.identifier(union)), { exported: true }) } }) const { scalars, prisma: prismaModels } = mapper.getReferencedGraphQLThingsInMapping() for (const s of scalars) { - statements.push({ - kind: tsMorph.StructureKind.TypeAlias, - name: s, - type: "any", - }) + dts.rootScope.addTypeAlias(s, t.tsAnyKeyword()) } const allPrismaModels = [...new Set([...prismaModels, ...typesToImport])].sort() if (allPrismaModels.length) { - statements.push({ - kind: tsMorph.StructureKind.ImportDeclaration, - moduleSpecifier: `@prisma/client`, - namedImports: allPrismaModels.map((p) => `${p} as P${p}`), - }) + dts.setImport("@prisma/client", { subImports: allPrismaModels.map((p) => `${p} as P${p}`) }) for (const p of allPrismaModels) { - statements.push({ - kind: tsMorph.StructureKind.TypeAlias, - name: p, - type: `P${p}`, - }) + dts.rootScope.addTypeAlias(p, t.tsTypeReference(t.identifier(`P${p}`))) } } - externalTSFile.set({ statements }) + const text = dts.getResult() const fullPath = context.join(context.pathSettings.typesFolderRoot, context.pathSettings.sharedInternalFilename) - const formatted = await formatDTS(fullPath, externalTSFile.getText()) - const prior = context.sys.readFile(fullPath) - if (prior !== formatted) context.sys.writeFile(fullPath, formatted) + if (prior !== text) context.sys.writeFile(fullPath, text) } diff --git a/src/tests/bugs/parentCanBeGraphQLObject.test.ts b/src/tests/bugs/parentCanBeGraphQLObject.test.ts index 111d3fd..2323445 100644 --- a/src/tests/bugs/parentCanBeGraphQLObject.test.ts +++ b/src/tests/bugs/parentCanBeGraphQLObject.test.ts @@ -30,13 +30,11 @@ export const Puzzle = { const { vfsMap } = await getDTSFilesForRun({ sdl, gamesService, prismaSchema }) const dts = vfsMap.get("/types/games.d.ts")! expect(dts.trim()).toMatchInlineSnapshot(` - "import type { Puzzle as SPuzzle } from \\"./shared-return-types\\"; - - export interface PuzzleTypeResolvers { - /** SDL: id: Int! */ + "interface PuzzleTypeResolvers { + /*SDL: id: Int!*/ id: number; } - - type PuzzleAsParent = SPuzzle & { id: () => number };" + type PuzzleAsParent = SPuzzle & {id: () => number} ; + import { Puzzle as SPuzzle } from \\"./shared-return-types\\";" `) }) diff --git a/src/tests/bugs/returnObjectCanBeGraphQLInterfaces.test.ts b/src/tests/bugs/returnObjectCanBeGraphQLInterfaces.test.ts index f706905..42fc270 100644 --- a/src/tests/bugs/returnObjectCanBeGraphQLInterfaces.test.ts +++ b/src/tests/bugs/returnObjectCanBeGraphQLInterfaces.test.ts @@ -31,28 +31,15 @@ export const Game = { const { vfsMap } = await getDTSFilesForRun({ sdl, gamesService, prismaSchema }) const dts = vfsMap.get("/types/games.d.ts")! expect(dts.trim()).toMatchInlineSnapshot(` - "import type { Game as PGame } from \\"@prisma/client\\"; - import type { GraphQLResolveInfo } from \\"graphql\\"; - - import type { RedwoodGraphQLContext } from \\"@redwoodjs/graphql-server/dist/types\\"; - - import type { Node as RTNode } from \\"./shared-return-types\\"; - import type { Node } from \\"./shared-schema-types\\"; - - export interface GameTypeResolvers { - /** SDL: puzzle: Node! */ - puzzle: ( - args?: undefined, - obj?: { - root: GameAsParent; - context: RedwoodGraphQLContext; - info: GraphQLResolveInfo; - } - ) => RTNode | Promise | (() => Promise); + "interface GameTypeResolvers { + /*SDL: puzzle: Node!*/ + puzzle: (args?: undefined, obj?: { root: GameAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => RTNode | Promise | (() => Promise); } - - type GameAsParent = PGame & { - puzzle: () => RTNode | Promise | (() => Promise); - };" + type GameAsParent = PGame & {puzzle: () => RTNode | Promise | (() => Promise)} ; + import { Game as PGame } from \\"@prisma/client\\"; + import { GraphQLResolveInfo } from \\"graphql\\"; + import { RedwoodGraphQLContext } from \\"@redwoodjs/graphql-server/dist/types\\"; + import { Node as RTNode } from \\"./shared-return-types\\"; + import { Node } from \\"./shared-schema-types\\";" `) }) diff --git a/src/tests/features/generatesTypesForUnions.test.ts b/src/tests/features/generatesTypesForUnions.test.ts index 508e3da..7762f7d 100644 --- a/src/tests/features/generatesTypesForUnions.test.ts +++ b/src/tests/features/generatesTypesForUnions.test.ts @@ -38,21 +38,21 @@ export const Game = { const { vfsMap } = await getDTSFilesForRun({ sdl, gamesService, prismaSchema, generateShared: true }) const dts = vfsMap.get("/types/shared-schema-types.d.ts")! expect(dts.trim()).toMatchInlineSnapshot(` - "export interface Game { + "interface Game { __typename?: \\"Game\\"; id?: number; } - export interface Puzzle { + interface Puzzle { __typename?: \\"Puzzle\\"; id: number; } - export type Gameish = Game | Puzzle; - export interface Query { + type Gameish = Game | Puzzle; + interface Query { __typename?: \\"Query\\"; gameObj?: Game| null | Puzzle| null| null; gameArr: (Game | Puzzle)[]; } - export interface Mutation { + interface Mutation { __typename?: \\"Mutation\\"; __?: string| null; }" diff --git a/src/tests/features/preferPromiseFnWhenKnown.test.ts b/src/tests/features/preferPromiseFnWhenKnown.test.ts index 3484f6f..18e57c2 100644 --- a/src/tests/features/preferPromiseFnWhenKnown.test.ts +++ b/src/tests/features/preferPromiseFnWhenKnown.test.ts @@ -54,114 +54,39 @@ export const Game = { const { vfsMap } = await getDTSFilesForRun({ sdl, gamesService, prismaSchema }) const dts = vfsMap.get("/types/games.d.ts")! expect(dts.trim()).toMatchInlineSnapshot(` - "import type { Game as PGame } from \\"@prisma/client\\"; - import type { GraphQLResolveInfo } from \\"graphql\\"; - - import type { RedwoodGraphQLContext } from \\"@redwoodjs/graphql-server/dist/types\\"; - - import type { Game as RTGame } from \\"./shared-return-types\\"; - import type { Query } from \\"./shared-schema-types\\"; - - /** SDL: gameSync: Game */ - export interface GameSyncResolver { - ( - args?: object, - obj?: { - root: Query; - context: RedwoodGraphQLContext; - info: GraphQLResolveInfo; - } - ): RTGame | null | Promise | (() => Promise); + "interface GameSyncResolver { + (args?: RTGame| null | Promise | (() => Promise), obj?: RTGame| null | Promise | (() => Promise)): RTGame| null | Promise | (() => Promise); } - - /** SDL: gameAsync: Game */ - export interface GameAsyncResolver { - ( - args?: object, - obj?: { - root: Query; - context: RedwoodGraphQLContext; - info: GraphQLResolveInfo; - } - ): Promise; + interface GameAsyncResolver { + (args?: Promise, obj?: Promise): Promise; } - - /** SDL: gameAsync1Arg: Game */ - export interface GameAsync1ArgResolver { - ( - args: object, - obj?: { - root: Query; - context: RedwoodGraphQLContext; - info: GraphQLResolveInfo; - } - ): RTGame | null | Promise | (() => Promise); + interface GameAsync1ArgResolver { + (args: RTGame| null | Promise | (() => Promise), obj?: RTGame| null | Promise | (() => Promise)): RTGame| null | Promise | (() => Promise); } - - /** SDL: gameAsync2Arg: Game */ - export interface GameAsync2ArgResolver { - ( - args: object, - obj: { - root: Query; - context: RedwoodGraphQLContext; - info: GraphQLResolveInfo; - } - ): RTGame | null | Promise | (() => Promise); + interface GameAsync2ArgResolver { + (args: RTGame| null | Promise | (() => Promise), obj: RTGame| null | Promise | (() => Promise)): RTGame| null | Promise | (() => Promise); } - - /** SDL: gameObj: Game */ - export interface GameObjResolver { - ( - args?: object, - obj?: { - root: Query; - context: RedwoodGraphQLContext; - info: GraphQLResolveInfo; - } - ): RTGame | null; + interface GameObjResolver { + (args?: RTGame| null, obj?: RTGame| null): RTGame| null; } - - export interface GameTypeResolvers { - /** SDL: summary: String! */ + interface GameTypeResolvers { + /*SDL: summary: String!*/ summary: string; - - /** SDL: summarySync: String! */ - summarySync: ( - args?: undefined, - obj?: { - root: GameAsParent; - context: RedwoodGraphQLContext; - info: GraphQLResolveInfo; - } - ) => string; - - /** SDL: summarySyncBlock: String! */ - summarySyncBlock: ( - args?: undefined, - obj?: { - root: GameAsParent; - context: RedwoodGraphQLContext; - info: GraphQLResolveInfo; - } - ) => string | Promise | (() => Promise); - - /** SDL: summaryAsync: String! */ - summaryAsync: ( - args?: undefined, - obj?: { - root: GameAsParent; - context: RedwoodGraphQLContext; - info: GraphQLResolveInfo; - } - ) => Promise; + /*SDL: summarySync: String!*/ + summarySync: (args?: undefined, obj?: { root: GameAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => string; + /*SDL: summarySyncBlock: String!*/ + summarySyncBlock: (args?: undefined, obj?: { root: GameAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => string | Promise | (() => Promise); + /*SDL: summaryAsync: String!*/ + summaryAsync: (args?: undefined, obj?: { root: GameAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => Promise; } - - type GameAsParent = PGame & { - summary: () => string; - summarySync: () => string; - summarySyncBlock: () => string | Promise | (() => Promise); - summaryAsync: () => Promise; - } & Extended;" + type GameAsParent = PGame & {summary: () => string, + summarySync: () => string, + summarySyncBlock: () => string | Promise | (() => Promise), + summaryAsync: () => Promise} & Extended; + import { Game as PGame } from \\"@prisma/client\\"; + import { GraphQLResolveInfo } from \\"graphql\\"; + import { RedwoodGraphQLContext } from \\"@redwoodjs/graphql-server/dist/types\\"; + import { Game as RTGame } from \\"./shared-return-types\\"; + import { Query } from \\"./shared-schema-types\\";" `) }) diff --git a/src/tests/features/returnTypePositionsWhichPreferPrisma.test.ts b/src/tests/features/returnTypePositionsWhichPreferPrisma.test.ts index 08039ab..0ffa49a 100644 --- a/src/tests/features/returnTypePositionsWhichPreferPrisma.test.ts +++ b/src/tests/features/returnTypePositionsWhichPreferPrisma.test.ts @@ -41,40 +41,19 @@ export const Game = { expect(dts.trimStart()).toMatchInlineSnapshot( ` - "import type { Game as PGame } from \\"@prisma/client\\"; - import type { GraphQLResolveInfo } from \\"graphql\\"; - - import type { RedwoodGraphQLContext } from \\"@redwoodjs/graphql-server/dist/types\\"; - - import type { Game as RTGame } from \\"./shared-return-types\\"; - import type { Query } from \\"./shared-schema-types\\"; - - /** SDL: game: Game */ - export interface GameResolver { - ( - args?: object, - obj?: { - root: Query; - context: RedwoodGraphQLContext; - info: GraphQLResolveInfo; - } - ): RTGame | null | Promise | (() => Promise); + "interface GameResolver { + (args?: RTGame| null | Promise | (() => Promise), obj?: RTGame| null | Promise | (() => Promise)): RTGame| null | Promise | (() => Promise); } - - export interface GameTypeResolvers { - /** SDL: summary: String! */ - summary: ( - args: undefined, - obj: { - root: GameAsParent; - context?: RedwoodGraphQLContext; - info?: GraphQLResolveInfo; - } - ) => string; + interface GameTypeResolvers { + /*SDL: summary: String!*/ + summary: (args: undefined, obj: { root: GameAsParent, context?: RedwoodGraphQLContext, info?: GraphQLResolveInfo }) => string; } - - type GameAsParent = PGame & { summary: () => string }; - " + type GameAsParent = PGame & {summary: () => string} ; + import { Game as PGame } from \\"@prisma/client\\"; + import { GraphQLResolveInfo } from \\"graphql\\"; + import { RedwoodGraphQLContext } from \\"@redwoodjs/graphql-server/dist/types\\"; + import { Game as RTGame } from \\"./shared-return-types\\"; + import { Query } from \\"./shared-schema-types\\";" ` ) }) diff --git a/src/tests/features/supportReferringToEnumsOnlyInSDL.test.ts b/src/tests/features/supportReferringToEnumsOnlyInSDL.test.ts index 644fc15..aa8132a 100644 --- a/src/tests/features/supportReferringToEnumsOnlyInSDL.test.ts +++ b/src/tests/features/supportReferringToEnumsOnlyInSDL.test.ts @@ -38,44 +38,28 @@ export const Game: GameResolvers = {}; // We are expecting to see import type GameType from "./shared-schema-types" expect(vfsMap.get("/types/games.d.ts")).toMatchInlineSnapshot(` - "import type { Game as PGame } from \\"@prisma/client\\"; - import type { GraphQLResolveInfo } from \\"graphql\\"; - - import type { RedwoodGraphQLContext } from \\"@redwoodjs/graphql-server/dist/types\\"; - - import type { Game as RTGame } from \\"./shared-return-types\\"; - import type { GameType, Query } from \\"./shared-schema-types\\"; - - /** SDL: allGames(type: GameType!): [Game!]! */ - export interface AllGamesResolver { - ( - args?: { type: GameType }, - obj?: { - root: Query; - context: RedwoodGraphQLContext; - info: GraphQLResolveInfo; - } - ): RTGame[] | Promise | (() => Promise); + "interface AllGamesResolver { + (args?: RTGame[] | Promise | (() => Promise), obj?: RTGame[] | Promise | (() => Promise)): RTGame[] | Promise | (() => Promise); } - - export interface GameTypeResolvers {} - - type GameAsParent = PGame; - " + interface GameTypeResolvers {} + type GameAsParent = PGame ; + import { Game as PGame } from \\"@prisma/client\\"; + import { Game as RTGame } from \\"./shared-return-types\\"; + import { GameType, Query } from \\"./shared-schema-types\\";" `) expect(vfsMap.get("/types/shared-schema-types.d.ts"))!.toMatchInlineSnapshot(` - "export interface Game { + "interface Game { __typename?: \\"Game\\"; id: number; games: Game[]; } - export interface Query { + interface Query { __typename?: \\"Query\\"; allGames: Game[]; } - export type GameType = \\"FOOTBALL\\" | \\"BASKETBALL\\"; - export interface Mutation { + type GameType = \\"FOOTBALL\\" | \\"BASKETBALL\\"; + interface Mutation { __typename?: \\"Mutation\\"; __?: string| null; }" diff --git a/src/tests/testRunner.ts b/src/tests/testRunner.ts index c969c35..0c519ac 100644 --- a/src/tests/testRunner.ts +++ b/src/tests/testRunner.ts @@ -6,7 +6,7 @@ import { Project } from "ts-morph" import { AppContext } from "../context.js" import { prismaModeller } from "../prismaModeller.js" -import { lookAtServiceFile } from "../serviceFile.js" +import { lookAtServiceFile } from "../serviceFile2.js" import { createSharedSchemaFiles } from "../sharedSchema.js" import type { CodeFacts, FieldFacts } from "../typeFacts.js" diff --git a/src/tsBuilder.ts b/src/tsBuilder.ts index 49afca6..9fb3934 100644 --- a/src/tsBuilder.ts +++ b/src/tsBuilder.ts @@ -3,11 +3,40 @@ import generator from "@babel/generator" import parser from "@babel/parser" import traverse from "@babel/traverse" -import t, { BlockStatement, ExpressionStatement, Node, Statement, TSType, TSTypeAliasDeclaration, TypeAlias } from "@babel/types" +import t, { + addComment, + BlockStatement, + Declaration, + ExpressionStatement, + Statement, + TSType, + TSTypeParameterDeclaration, +} from "@babel/types" + +interface InterfaceProperty { + docs?: string + name: string + optional: boolean + type: string +} + +interface InterfaceCallSignature { + docs?: string + params: { name: string; optional?: boolean; type: string }[] + returnType: string + type: "call-signature" +} + +interface NodeConfig { + docs?: string + exported?: boolean + generics?: { name: string }[] +} export const builder = (priorSource: string, opts: {}) => { const sourceFile = parser.parse(priorSource, { sourceType: "module", plugins: ["jsx", "typescript"] }) + /** Declares an import which should exist in the source document */ const setImport = (source: string, opts: { mainImport?: string; subImports?: string[] }) => { const imports = sourceFile.program.body.filter((s) => s.type === "ImportDeclaration") @@ -41,6 +70,7 @@ export const builder = (priorSource: string, opts: {}) => { } } + /** Allows creating a type alias via an AST parsed string */ const setTypeViaTemplate = (template: string) => { const type = parser.parse(template, { sourceType: "module", plugins: ["jsx", "typescript"] }) @@ -86,7 +116,8 @@ export const builder = (priorSource: string, opts: {}) => { throw new Error(`Unsupported type annotation: ${newAnnotion.type} - ${generator(newAnnotion).code}`) } - const createScope = (name: string, statements: Statement[]) => { + /** An internal API for describing a new area for inputting template info */ + const createScope = (scopeName: string, scopeNode: t.Node, statements: Statement[]) => { const addFunction = (name: string) => { let functionNode = statements.find( (s) => t.isVariableDeclaration(s) && t.isIdentifier(s.declarations[0].id) && s.declarations[0].id.name === name @@ -115,7 +146,7 @@ export const builder = (priorSource: string, opts: {}) => { else exists.typeAnnotation = param.typeAnnotation }, - scope: createScope(name, (arrowFn.body as BlockStatement).body), + scope: createScope(name, arrowFn, (arrowFn.body as BlockStatement).body), } } @@ -133,7 +164,7 @@ export const builder = (priorSource: string, opts: {}) => { statements.push(declaration) } - const addTypeAlias = (name: string, type: TSType, exported?: boolean) => { + const addTypeAlias = (name: string, type: "any" | "string" | TSType, nodeConfig?: NodeConfig) => { const prior = statements.find( (s) => (t.isTSTypeAliasDeclaration(s) && s.id.name === name) || @@ -141,29 +172,73 @@ export const builder = (priorSource: string, opts: {}) => { ) if (prior) return - const alias = t.tsTypeAliasDeclaration(t.identifier(name), null, type) - statements.push(exported ? t.exportNamedDeclaration(alias) : alias) + // Allow having some easy literals + let typeNode = null + if (typeof type === "string") { + if (type === "any") typeNode = t.tsAnyKeyword() + if (type === "string") typeNode = t.tsStringKeyword() + } else { + typeNode = type + } + + const alias = t.tsTypeAliasDeclaration(t.identifier(name), null, typeNode!) + const statement = nodeFromNodeConfig(alias, nodeConfig) + statements.push(statement) + + return alias } - const addInterface = (name: string, fields: { docs?: string; name: string; optional: boolean; type: string }[], exported?: boolean) => { + const addInterface = (name: string, fields: (InterfaceCallSignature | InterfaceProperty)[], nodeConfig?: NodeConfig) => { const prior = statements.find( (s) => (t.isTSInterfaceDeclaration(s) && s.id.name === name) || (t.isExportNamedDeclaration(s) && t.isTSInterfaceDeclaration(s.declaration) && s.declaration.id.name === name) ) - if (prior) return + if (prior) { + if (t.isTSInterfaceDeclaration(prior)) return prior + if (t.isExportNamedDeclaration(prior) && t.isTSInterfaceDeclaration(prior.declaration)) return prior.declaration + throw new Error("Unknown state") + } const body = t.tsInterfaceBody( fields.map((f) => { - const prop = t.tsPropertySignature(t.identifier(f.name), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(f.type)))) - prop.optional = f.optional - return prop + // Allow call signatures + if (!("name" in f) && f.type === "call-signature") { + const sig = t.tsCallSignatureDeclaration( + null, // generics + f.params.map((p) => { + const i = t.identifier(p.name) + i.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier(f.returnType))) + if (p.optional) i.optional = true + return i + }), + t.tsTypeAnnotation(t.tsTypeReference(t.identifier(f.returnType))) + ) + return sig + } else { + const prop = t.tsPropertySignature(t.identifier(f.name), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(f.type)))) + prop.optional = f.optional + if (f.docs?.length) t.addComment(prop, "leading", f.docs) + return prop + } }) ) - const alias = t.tsInterfaceDeclaration(t.identifier(name), null, null, body) - statements.push(exported ? t.exportNamedDeclaration(alias) : alias) + const interfaceDec = t.tsInterfaceDeclaration(t.identifier(name), null, null, body) + const statement = nodeFromNodeConfig(interfaceDec, nodeConfig) + statements.push(statement) + return interfaceDec + } + + const addLeadingComment = (comment: string) => { + const firstStatement = statements[0] || scopeNode + if (firstStatement) { + if (firstStatement.leadingComments?.find((c) => c.value === comment)) return + t.addComment(firstStatement, "leading", comment) + } else { + t.addComment(scopeNode, "leading", comment) + } } return { @@ -171,9 +246,11 @@ export const builder = (priorSource: string, opts: {}) => { addVariableDeclaration, addTypeAlias, addInterface, + addLeadingComment, } } + /** Experimental function for parsing out a graphql template tag, and ensuring certain fields have been called */ const updateGraphQLTemplateTag = (expression: t.Expression, path: string, modelFields: string[]) => { if (path !== ".") throw new Error("Only support updating the root of the graphql tag ATM") traverse( @@ -200,11 +277,9 @@ export const builder = (priorSource: string, opts: {}) => { const parseStatement = (code: string) => parser.parse(code, { sourceType: "module", plugins: ["jsx", "typescript"] }).program.body[0] as ExpressionStatement - const getResult = () => { - return generator(sourceFile.program, {}).code - } + const getResult = () => generator(sourceFile.program, {}).code - const rootScope = createScope("root", sourceFile.program.body) + const rootScope = createScope("root", sourceFile, sourceFile.program.body) return { setImport, getResult, setTypeViaTemplate, parseStatement, updateGraphQLTemplateTag, rootScope } } @@ -215,3 +290,19 @@ const getTypeLevelAST = (type: string) => { if (!typeDeclaration) throw new Error("No type declaration found in template: " + type) return typeDeclaration.typeAnnotation } + +export type TSBuilder = ReturnType + +/** A little helper to handle all the extras for */ +const nodeFromNodeConfig = ( + node: T, + nodeConfig?: NodeConfig +) => { + const statement = nodeConfig?.exported ? t.exportNamedDeclaration(node) : node + if (nodeConfig?.docs) addComment(statement, "leading", nodeConfig.docs) + if (nodeConfig?.generics && nodeConfig.generics.length > 0) { + node.typeParameters = t.tsTypeParameterDeclaration(nodeConfig.generics.map((g) => t.tsTypeParameter(null, null, g.name))) + } + + return node +} diff --git a/src/utils.ts b/src/utils.ts index 695d0d1..080d380 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,7 @@ import * as graphql from "graphql" import * as tsMorph from "ts-morph" +import { TSBuilder } from "./tsBuilder.js" import { TypeMapper } from "./typeMap.js" export const varStartsWithUppercase = (v: tsMorph.VariableDeclaration) => v.getName()[0].startsWith(v.getName()[0].toUpperCase()) @@ -32,7 +33,7 @@ export const inlineArgsForField = (field: graphql.GraphQLField export const createAndReferOrInlineArgsForField = ( field: graphql.GraphQLField, config: { - file: tsMorph.SourceFile + dts: TSBuilder mapper: TypeMapper["map"] name: string noSeparateType?: true @@ -42,17 +43,15 @@ export const createAndReferOrInlineArgsForField = ( if (!inlineArgs) return undefined if (inlineArgs.length < 120) return inlineArgs - const argsInterface = config.file.addInterface({ - name: `${config.name}Args`, - isExported: true, - }) - - field.args.forEach((a) => { - argsInterface.addProperty({ + const dts = config.dts + dts.rootScope.addInterface( + `${config.name}Args`, + field.args.map((a) => ({ name: a.name, - type: config.mapper(a.type, {}), - }) - }) + type: config.mapper(a.type, {})!, + optional: false, + })) + ) return `${config.name}Args` } From dc0e81f88a6f6aeceb57997cf434a005f0837736 Mon Sep 17 00:00:00 2001 From: Orta Therox Date: Mon, 11 Nov 2024 08:26:16 +0000 Subject: [PATCH 8/9] Closer --- src/index.ts | 2 +- src/serviceFile2.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index ae08066..20fd082 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ import typescript from "typescript" import { AppContext } from "./context.js" import { PrismaMap, prismaModeller } from "./prismaModeller.js" -import { lookAtServiceFile } from "./serviceFile.js" +import { lookAtServiceFile } from "./serviceFile2.js" import { createSharedSchemaFiles } from "./sharedSchema.js" import { CodeFacts, FieldFacts } from "./typeFacts.js" import { RedwoodPaths } from "./types.js" diff --git a/src/serviceFile2.ts b/src/serviceFile2.ts index 355abaa..012fbd5 100644 --- a/src/serviceFile2.ts +++ b/src/serviceFile2.ts @@ -5,7 +5,7 @@ import * as graphql from "graphql" import { AppContext } from "./context.js" import { getCodeFactsForJSTSFileAtPath } from "./serviceFile.codefacts.js" -import { builder } from "./tsBuilder.js" +import { builder, TSBuilder } from "./tsBuilder.js" import { CodeFacts, ModelResolverFacts, ResolverFuncFact } from "./typeFacts.js" import { TypeMapper, typeMapper } from "./typeMap.js" import { capitalizeFirstLetter, createAndReferOrInlineArgsForField, inlineArgsForField } from "./utils.js" @@ -54,7 +54,7 @@ export const lookAtServiceFile = async (file: string, context: AppContext) => { const isMutation = v.name in mutationType.getFields() const parentName = isQuery ? queryType.name : isMutation ? mutationType.name : undefined if (parentName) { - addDefinitionsForTopLevelResolvers(parentName, v) + addDefinitionsForTopLevelResolvers(parentName, v, dts) } else { // Add warning about unused resolver dts.rootScope.addInterface(v.name, [], { exported: true, docs: "This resolver does not exist on Query or Mutation" }) @@ -138,7 +138,7 @@ export const lookAtServiceFile = async (file: string, context: AppContext) => { context.sys.writeFile(dtsFilepath, formatted) return dtsFilepath - function addDefinitionsForTopLevelResolvers(parentName: string, config: ResolverFuncFact) { + function addDefinitionsForTopLevelResolvers(parentName: string, config: ResolverFuncFact, dts: TSBuilder) { const { name } = config let field = queryType.getFields()[name] if (!field) { From 953d0b8f883259fee2e0c3286b6c05a6dbe53d64 Mon Sep 17 00:00:00 2001 From: Orta Therox Date: Mon, 11 Nov 2024 09:38:05 +0000 Subject: [PATCH 9/9] Get green --- package.json | 2 +- pnpm-lock.yaml | 116 +++---- src/formatDTS.ts | 19 -- src/index.ts | 2 +- src/serviceFile.ts | 162 ++++----- src/serviceFile2.ts | 311 ------------------ src/sharedSchema.ts | 5 +- .../bugs/parentCanBeGraphQLObject.test.ts | 4 +- ...returnObjectCanBeGraphQLInterfaces.test.ts | 4 +- .../features/generatesTypesForUnions.test.ts | 10 +- .../features/preferPromiseFnWhenKnown.test.ts | 35 +- ...turnTypePositionsWhichPreferPrisma.test.ts | 9 +- .../features/supportGenericExtension.test.ts | 11 +- .../supportReferringToEnumsOnlyInSDL.test.ts | 17 +- src/tests/testRunner.ts | 4 +- src/tsBuilder.ts | 21 +- 16 files changed, 196 insertions(+), 536 deletions(-) delete mode 100644 src/formatDTS.ts delete mode 100644 src/serviceFile2.ts diff --git a/package.json b/package.json index 7930978..7c4e62e 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "release-it": "^15.6.0", "sentences-per-line": "^0.2.1", "should-semantic-release": "^0.1.0", - "typescript": "^5.0.0", + "typescript": "^5.6.3", "vitest": "^0.31.1", "yaml-eslint-parser": "^1.2.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da24773..3e63d59 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,10 +44,10 @@ importers: version: 16.18.24 '@typescript-eslint/eslint-plugin': specifier: ^5.48.2 - version: 5.48.2(@typescript-eslint/parser@5.48.2(eslint@8.32.0)(typescript@5.0.2))(eslint@8.32.0)(typescript@5.0.2) + version: 5.48.2(@typescript-eslint/parser@5.48.2(eslint@8.32.0)(typescript@5.6.3))(eslint@8.32.0)(typescript@5.6.3) '@typescript-eslint/parser': specifier: ^5.48.2 - version: 5.48.2(eslint@8.32.0)(typescript@5.0.2) + version: 5.48.2(eslint@8.32.0)(typescript@5.6.3) '@typescript/vfs': specifier: 1.4.0 version: 1.4.0 @@ -65,13 +65,13 @@ importers: version: 8.6.0(eslint@8.32.0) eslint-plugin-deprecation: specifier: ^1.4.1 - version: 1.4.1(eslint@8.32.0)(typescript@5.0.2) + version: 1.4.1(eslint@8.32.0)(typescript@5.6.3) eslint-plugin-eslint-comments: specifier: ^3.2.0 version: 3.2.0(eslint@8.32.0) eslint-plugin-import: specifier: ^2.27.5 - version: 2.27.5(@typescript-eslint/parser@5.48.2(eslint@8.32.0)(typescript@5.0.2))(eslint@8.32.0) + version: 2.27.5(@typescript-eslint/parser@5.48.2(eslint@8.32.0)(typescript@5.6.3))(eslint@8.32.0) eslint-plugin-jsonc: specifier: ^2.6.0 version: 2.6.0(eslint@8.32.0) @@ -89,7 +89,7 @@ importers: version: 10.0.0(eslint@8.32.0) eslint-plugin-typescript-sort-keys: specifier: ^2.3.0 - version: 2.3.0(@typescript-eslint/parser@5.48.2(eslint@8.32.0)(typescript@5.0.2))(eslint@8.32.0)(typescript@5.0.2) + version: 2.3.0(@typescript-eslint/parser@5.48.2(eslint@8.32.0)(typescript@5.6.3))(eslint@8.32.0)(typescript@5.6.3) eslint-plugin-yml: specifier: ^1.5.0 version: 1.5.0(eslint@8.32.0) @@ -139,8 +139,8 @@ importers: specifier: ^0.1.0 version: 0.1.1 typescript: - specifier: ^5.0.0 - version: 5.0.2 + specifier: ^5.6.3 + version: 5.6.3 vitest: specifier: ^0.31.1 version: 0.31.1 @@ -3620,9 +3620,9 @@ packages: typedarray-to-buffer@3.1.5: resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} - typescript@5.0.2: - resolution: {integrity: sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==} - engines: {node: '>=12.20'} + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} hasBin: true uc.micro@1.0.6: @@ -4635,41 +4635,41 @@ snapshots: '@types/unist@2.0.6': {} - '@typescript-eslint/eslint-plugin@5.48.2(@typescript-eslint/parser@5.48.2(eslint@8.32.0)(typescript@5.0.2))(eslint@8.32.0)(typescript@5.0.2)': + '@typescript-eslint/eslint-plugin@5.48.2(@typescript-eslint/parser@5.48.2(eslint@8.32.0)(typescript@5.6.3))(eslint@8.32.0)(typescript@5.6.3)': dependencies: - '@typescript-eslint/parser': 5.48.2(eslint@8.32.0)(typescript@5.0.2) + '@typescript-eslint/parser': 5.48.2(eslint@8.32.0)(typescript@5.6.3) '@typescript-eslint/scope-manager': 5.48.2 - '@typescript-eslint/type-utils': 5.48.2(eslint@8.32.0)(typescript@5.0.2) - '@typescript-eslint/utils': 5.48.2(eslint@8.32.0)(typescript@5.0.2) + '@typescript-eslint/type-utils': 5.48.2(eslint@8.32.0)(typescript@5.6.3) + '@typescript-eslint/utils': 5.48.2(eslint@8.32.0)(typescript@5.6.3) debug: 4.3.4 eslint: 8.32.0 ignore: 5.2.4 natural-compare-lite: 1.4.0 regexpp: 3.2.0 semver: 7.3.8 - tsutils: 3.21.0(typescript@5.0.2) + tsutils: 3.21.0(typescript@5.6.3) optionalDependencies: - typescript: 5.0.2 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/experimental-utils@5.48.0(eslint@8.32.0)(typescript@5.0.2)': + '@typescript-eslint/experimental-utils@5.48.0(eslint@8.32.0)(typescript@5.6.3)': dependencies: - '@typescript-eslint/utils': 5.48.0(eslint@8.32.0)(typescript@5.0.2) + '@typescript-eslint/utils': 5.48.0(eslint@8.32.0)(typescript@5.6.3) eslint: 8.32.0 transitivePeerDependencies: - supports-color - typescript - '@typescript-eslint/parser@5.48.2(eslint@8.32.0)(typescript@5.0.2)': + '@typescript-eslint/parser@5.48.2(eslint@8.32.0)(typescript@5.6.3)': dependencies: '@typescript-eslint/scope-manager': 5.48.2 '@typescript-eslint/types': 5.48.2 - '@typescript-eslint/typescript-estree': 5.48.2(typescript@5.0.2) + '@typescript-eslint/typescript-estree': 5.48.2(typescript@5.6.3) debug: 4.3.4 eslint: 8.32.0 optionalDependencies: - typescript: 5.0.2 + typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -4688,15 +4688,15 @@ snapshots: '@typescript-eslint/types': 5.59.0 '@typescript-eslint/visitor-keys': 5.59.0 - '@typescript-eslint/type-utils@5.48.2(eslint@8.32.0)(typescript@5.0.2)': + '@typescript-eslint/type-utils@5.48.2(eslint@8.32.0)(typescript@5.6.3)': dependencies: - '@typescript-eslint/typescript-estree': 5.48.2(typescript@5.0.2) - '@typescript-eslint/utils': 5.48.2(eslint@8.32.0)(typescript@5.0.2) + '@typescript-eslint/typescript-estree': 5.48.2(typescript@5.6.3) + '@typescript-eslint/utils': 5.48.2(eslint@8.32.0)(typescript@5.6.3) debug: 4.3.4 eslint: 8.32.0 - tsutils: 3.21.0(typescript@5.0.2) + tsutils: 3.21.0(typescript@5.6.3) optionalDependencies: - typescript: 5.0.2 + typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -4706,7 +4706,7 @@ snapshots: '@typescript-eslint/types@5.59.0': {} - '@typescript-eslint/typescript-estree@5.48.0(typescript@5.0.2)': + '@typescript-eslint/typescript-estree@5.48.0(typescript@5.6.3)': dependencies: '@typescript-eslint/types': 5.48.0 '@typescript-eslint/visitor-keys': 5.48.0 @@ -4714,13 +4714,13 @@ snapshots: globby: 11.1.0 is-glob: 4.0.3 semver: 7.3.8 - tsutils: 3.21.0(typescript@5.0.2) + tsutils: 3.21.0(typescript@5.6.3) optionalDependencies: - typescript: 5.0.2 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@5.48.2(typescript@5.0.2)': + '@typescript-eslint/typescript-estree@5.48.2(typescript@5.6.3)': dependencies: '@typescript-eslint/types': 5.48.2 '@typescript-eslint/visitor-keys': 5.48.2 @@ -4728,13 +4728,13 @@ snapshots: globby: 11.1.0 is-glob: 4.0.3 semver: 7.3.8 - tsutils: 3.21.0(typescript@5.0.2) + tsutils: 3.21.0(typescript@5.6.3) optionalDependencies: - typescript: 5.0.2 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@5.59.0(typescript@5.0.2)': + '@typescript-eslint/typescript-estree@5.59.0(typescript@5.6.3)': dependencies: '@typescript-eslint/types': 5.59.0 '@typescript-eslint/visitor-keys': 5.59.0 @@ -4742,19 +4742,19 @@ snapshots: globby: 11.1.0 is-glob: 4.0.3 semver: 7.3.8 - tsutils: 3.21.0(typescript@5.0.2) + tsutils: 3.21.0(typescript@5.6.3) optionalDependencies: - typescript: 5.0.2 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@5.48.0(eslint@8.32.0)(typescript@5.0.2)': + '@typescript-eslint/utils@5.48.0(eslint@8.32.0)(typescript@5.6.3)': dependencies: '@types/json-schema': 7.0.11 '@types/semver': 7.3.13 '@typescript-eslint/scope-manager': 5.48.0 '@typescript-eslint/types': 5.48.0 - '@typescript-eslint/typescript-estree': 5.48.0(typescript@5.0.2) + '@typescript-eslint/typescript-estree': 5.48.0(typescript@5.6.3) eslint: 8.32.0 eslint-scope: 5.1.1 eslint-utils: 3.0.0(eslint@8.32.0) @@ -4763,13 +4763,13 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@5.48.2(eslint@8.32.0)(typescript@5.0.2)': + '@typescript-eslint/utils@5.48.2(eslint@8.32.0)(typescript@5.6.3)': dependencies: '@types/json-schema': 7.0.11 '@types/semver': 7.3.13 '@typescript-eslint/scope-manager': 5.48.2 '@typescript-eslint/types': 5.48.2 - '@typescript-eslint/typescript-estree': 5.48.2(typescript@5.0.2) + '@typescript-eslint/typescript-estree': 5.48.2(typescript@5.6.3) eslint: 8.32.0 eslint-scope: 5.1.1 eslint-utils: 3.0.0(eslint@8.32.0) @@ -4778,14 +4778,14 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@5.59.0(eslint@8.32.0)(typescript@5.0.2)': + '@typescript-eslint/utils@5.59.0(eslint@8.32.0)(typescript@5.6.3)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.32.0) '@types/json-schema': 7.0.11 '@types/semver': 7.3.13 '@typescript-eslint/scope-manager': 5.59.0 '@typescript-eslint/types': 5.59.0 - '@typescript-eslint/typescript-estree': 5.59.0(typescript@5.0.2) + '@typescript-eslint/typescript-estree': 5.59.0(typescript@5.6.3) eslint: 8.32.0 eslint-scope: 5.1.1 semver: 7.3.8 @@ -5610,23 +5610,23 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.7.4(@typescript-eslint/parser@5.48.2(eslint@8.32.0)(typescript@5.0.2))(eslint-import-resolver-node@0.3.7)(eslint@8.32.0): + eslint-module-utils@2.7.4(@typescript-eslint/parser@5.48.2(eslint@8.32.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.7)(eslint@8.32.0): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 5.48.2(eslint@8.32.0)(typescript@5.0.2) + '@typescript-eslint/parser': 5.48.2(eslint@8.32.0)(typescript@5.6.3) eslint: 8.32.0 eslint-import-resolver-node: 0.3.7 transitivePeerDependencies: - supports-color - eslint-plugin-deprecation@1.4.1(eslint@8.32.0)(typescript@5.0.2): + eslint-plugin-deprecation@1.4.1(eslint@8.32.0)(typescript@5.6.3): dependencies: - '@typescript-eslint/utils': 5.59.0(eslint@8.32.0)(typescript@5.0.2) + '@typescript-eslint/utils': 5.59.0(eslint@8.32.0)(typescript@5.6.3) eslint: 8.32.0 tslib: 2.5.0 - tsutils: 3.21.0(typescript@5.0.2) - typescript: 5.0.2 + tsutils: 3.21.0(typescript@5.6.3) + typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -5636,7 +5636,7 @@ snapshots: eslint: 8.32.0 ignore: 5.2.4 - eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.48.2(eslint@8.32.0)(typescript@5.0.2))(eslint@8.32.0): + eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.48.2(eslint@8.32.0)(typescript@5.6.3))(eslint@8.32.0): dependencies: array-includes: 3.1.6 array.prototype.flat: 1.3.1 @@ -5645,7 +5645,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.32.0 eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.48.2(eslint@8.32.0)(typescript@5.0.2))(eslint-import-resolver-node@0.3.7)(eslint@8.32.0) + eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.48.2(eslint@8.32.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.7)(eslint@8.32.0) has: 1.0.3 is-core-module: 2.11.0 is-glob: 4.0.3 @@ -5655,7 +5655,7 @@ snapshots: semver: 6.3.0 tsconfig-paths: 3.14.1 optionalDependencies: - '@typescript-eslint/parser': 5.48.2(eslint@8.32.0)(typescript@5.0.2) + '@typescript-eslint/parser': 5.48.2(eslint@8.32.0)(typescript@5.6.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -5693,14 +5693,14 @@ snapshots: dependencies: eslint: 8.32.0 - eslint-plugin-typescript-sort-keys@2.3.0(@typescript-eslint/parser@5.48.2(eslint@8.32.0)(typescript@5.0.2))(eslint@8.32.0)(typescript@5.0.2): + eslint-plugin-typescript-sort-keys@2.3.0(@typescript-eslint/parser@5.48.2(eslint@8.32.0)(typescript@5.6.3))(eslint@8.32.0)(typescript@5.6.3): dependencies: - '@typescript-eslint/experimental-utils': 5.48.0(eslint@8.32.0)(typescript@5.0.2) - '@typescript-eslint/parser': 5.48.2(eslint@8.32.0)(typescript@5.0.2) + '@typescript-eslint/experimental-utils': 5.48.0(eslint@8.32.0)(typescript@5.6.3) + '@typescript-eslint/parser': 5.48.2(eslint@8.32.0)(typescript@5.6.3) eslint: 8.32.0 json-schema: 0.4.0 natural-compare-lite: 1.4.0 - typescript: 5.0.2 + typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -6486,7 +6486,7 @@ snapshots: pretty-ms: 8.0.0 strip-json-comments: 5.0.0 summary: 2.1.0 - typescript: 5.0.2 + typescript: 5.6.3 zod: 3.21.4 zod-validation-error: 1.2.1(zod@3.21.4) @@ -7672,10 +7672,10 @@ snapshots: tslib@2.5.0: {} - tsutils@3.21.0(typescript@5.0.2): + tsutils@3.21.0(typescript@5.6.3): dependencies: tslib: 1.14.1 - typescript: 5.0.2 + typescript: 5.6.3 type-check@0.3.2: dependencies: @@ -7713,7 +7713,7 @@ snapshots: dependencies: is-typedarray: 1.0.0 - typescript@5.0.2: {} + typescript@5.6.3: {} uc.micro@1.0.6: {} diff --git a/src/formatDTS.ts b/src/formatDTS.ts deleted file mode 100644 index a8863d1..0000000 --- a/src/formatDTS.ts +++ /dev/null @@ -1,19 +0,0 @@ -// https://prettier.io/docs/en/api.html - -let hasPrettierInstalled = false -let prettier = null -try { - hasPrettierInstalled = !!require.resolve("prettier") - prettier = require("prettier") -} catch (error) {} - -export const formatDTS = async (path: string, content: string): Promise => { - if (!hasPrettierInstalled) return content - - try { - if (!prettier) return content - return prettier.format(content, { filepath: path }) - } catch (error) { - return content - } -} diff --git a/src/index.ts b/src/index.ts index 20fd082..ae08066 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ import typescript from "typescript" import { AppContext } from "./context.js" import { PrismaMap, prismaModeller } from "./prismaModeller.js" -import { lookAtServiceFile } from "./serviceFile2.js" +import { lookAtServiceFile } from "./serviceFile.js" import { createSharedSchemaFiles } from "./sharedSchema.js" import { CodeFacts, FieldFacts } from "./typeFacts.js" import { RedwoodPaths } from "./types.js" diff --git a/src/serviceFile.ts b/src/serviceFile.ts index 44958c2..156cfa9 100644 --- a/src/serviceFile.ts +++ b/src/serviceFile.ts @@ -1,10 +1,11 @@ /* eslint-disable @typescript-eslint/no-unnecessary-condition */ +import t, { tsTypeAnnotation } from "@babel/types" import * as graphql from "graphql" import { AppContext } from "./context.js" -import { formatDTS } from "./formatDTS.js" import { getCodeFactsForJSTSFileAtPath } from "./serviceFile.codefacts.js" +import { builder, TSBuilder } from "./tsBuilder.js" import { CodeFacts, ModelResolverFacts, ResolverFuncFact } from "./typeFacts.js" import { TypeMapper, typeMapper } from "./typeMap.js" import { capitalizeFirstLetter, createAndReferOrInlineArgsForField, inlineArgsForField } from "./utils.js" @@ -21,6 +22,7 @@ export const lookAtServiceFile = async (file: string, context: AppContext) => { const thisFact: CodeFacts = {} const filename = context.basename(file) + const dts = builder("", {}) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const queryType = gql.getQueryType()! @@ -41,9 +43,6 @@ export const lookAtServiceFile = async (file: string, context: AppContext) => { const extraPrismaReferences = new Set() const extraSharedFileImportReferences = new Set<{ import: string; name?: string }>() - // The file we'll be creating in-memory throughout this fn - const fileDTS = context.tsProject.createSourceFile(`source/${fileKey}.d.ts`, "", { overwrite: true }) - // Basically if a top level resolver reference Query or Mutation const knownSpecialCasesForGraphQL = new Set() @@ -55,10 +54,10 @@ export const lookAtServiceFile = async (file: string, context: AppContext) => { const isMutation = v.name in mutationType.getFields() const parentName = isQuery ? queryType.name : isMutation ? mutationType.name : undefined if (parentName) { - addDefinitionsForTopLevelResolvers(parentName, v) + addDefinitionsForTopLevelResolvers(parentName, v, dts) } else { // Add warning about unused resolver - fileDTS.addStatements(`\n// ${v.name} does not exist on Query or Mutation`) + dts.rootScope.addInterface(v.name, [], { exported: true, docs: "This resolver does not exist on Query or Mutation" }) } }) @@ -77,13 +76,9 @@ export const lookAtServiceFile = async (file: string, context: AppContext) => { const sharedInternalGraphQLObjectsReferenced = returnTypeMapper.getReferencedGraphQLThingsInMapping() const aliases = [...new Set([...sharedGraphQLObjectsReferenced.scalars, ...sharedInternalGraphQLObjectsReferenced.scalars])] - if (aliases.length) { - fileDTS.addTypeAliases( - aliases.map((s) => ({ - name: s, - type: "any", - })) - ) + + for (const alias of aliases) { + dts.rootScope.addTypeAlias(alias, "any") } const prismases = [ @@ -96,34 +91,22 @@ export const lookAtServiceFile = async (file: string, context: AppContext) => { const validPrismaObjs = prismases.filter((p) => prisma.has(p)) if (validPrismaObjs.length) { - fileDTS.addImportDeclaration({ - isTypeOnly: true, - moduleSpecifier: "@prisma/client", - namedImports: validPrismaObjs.map((p) => `${p} as P${p}`), - }) + dts.setImport("@prisma/client", { subImports: validPrismaObjs.map((p) => `${p} as P${p}`) }) } - if (fileDTS.getText().includes("GraphQLResolveInfo")) { - fileDTS.addImportDeclaration({ - isTypeOnly: true, - moduleSpecifier: "graphql", - namedImports: ["GraphQLResolveInfo"], - }) + const initialResult = dts.getResult() + if (initialResult.includes("GraphQLResolveInfo")) { + dts.setImport("graphql", { subImports: ["GraphQLResolveInfo"] }) } - if (fileDTS.getText().includes("RedwoodGraphQLContext")) { - fileDTS.addImportDeclaration({ - isTypeOnly: true, - moduleSpecifier: "@redwoodjs/graphql-server/dist/types", - namedImports: ["RedwoodGraphQLContext"], - }) + if (initialResult.includes("RedwoodGraphQLContext")) { + dts.setImport("@redwoodjs/graphql-server/dist/types", { subImports: ["RedwoodGraphQLContext"] }) } if (sharedInternalGraphQLObjectsReferenced.types.length || extraSharedFileImportReferences.size) { - fileDTS.addImportDeclaration({ - isTypeOnly: true, - moduleSpecifier: `./${settings.sharedInternalFilename.replace(".d.ts", "")}`, - namedImports: [ + const source = `./${settings.sharedInternalFilename.replace(".d.ts", "")}` + dts.setImport(source, { + subImports: [ ...sharedInternalGraphQLObjectsReferenced.types.map((t) => `${t} as RT${t}`), ...[...extraSharedFileImportReferences.values()].map((t) => ("name" in t && t.name ? `${t.import} as ${t.name}` : t.import)), ], @@ -131,11 +114,8 @@ export const lookAtServiceFile = async (file: string, context: AppContext) => { } if (sharedGraphQLObjectsReferencedTypes.length) { - fileDTS.addImportDeclaration({ - isTypeOnly: true, - moduleSpecifier: `./${settings.sharedFilename.replace(".d.ts", "")}`, - namedImports: sharedGraphQLObjectsReferencedTypes, - }) + const source = `./${settings.sharedFilename.replace(".d.ts", "")}` + dts.setImport(source, { subImports: sharedGraphQLObjectsReferencedTypes }) } serviceFacts.set(fileKey, thisFact) @@ -144,15 +124,12 @@ export const lookAtServiceFile = async (file: string, context: AppContext) => { const dtsFilepath = context.join(context.pathSettings.typesFolderRoot, dtsFilename) // Some manual formatting tweaks so we align with Redwood's setup more - const dts = fileDTS - .getText() - .replace(`from "graphql";`, `from "graphql";\n`) - .replace(`from "@redwoodjs/graphql-server/dist/types";`, `from "@redwoodjs/graphql-server/dist/types";\n`) + const final = dts.getResult() - const shouldWriteDTS = !!dts.trim().length + const shouldWriteDTS = !!final.trim().length if (!shouldWriteDTS) return - const formatted = await formatDTS(dtsFilepath, dts) + const formatted = final // await formatDTS(dtsFilepath, dts) // Don't make a file write if the content is the same const priorContent = context.sys.readFile(dtsFilename) @@ -161,24 +138,21 @@ export const lookAtServiceFile = async (file: string, context: AppContext) => { context.sys.writeFile(dtsFilepath, formatted) return dtsFilepath - function addDefinitionsForTopLevelResolvers(parentName: string, config: ResolverFuncFact) { + function addDefinitionsForTopLevelResolvers(parentName: string, config: ResolverFuncFact, dts: TSBuilder) { const { name } = config let field = queryType.getFields()[name] if (!field) { field = mutationType.getFields()[name] } - const interfaceDeclaration = fileDTS.addInterface({ - name: `${capitalizeFirstLetter(config.name)}Resolver`, - isExported: true, - docs: field.astNode - ? ["SDL: " + graphql.print(field.astNode)] - : ["@deprecated: Could not find this field in the schema for Mutation or Query"], - }) + const nodeDocs = field.astNode + ? ["SDL: " + graphql.print(field.astNode)] + : ["@deprecated: Could not find this field in the schema for Mutation or Query"] + const interfaceName = `${capitalizeFirstLetter(config.name)}Resolver` const args = createAndReferOrInlineArgsForField(field, { - name: interfaceDeclaration.getName(), - file: fileDTS, + name: interfaceName, + dts, mapper: externalMapper.map, }) @@ -189,19 +163,28 @@ export const lookAtServiceFile = async (file: string, context: AppContext) => { const qForInfos = config.infoParamType === "just_root_destructured" ? "?" : "" const returnType = returnTypeForResolver(returnTypeMapper, field, config) - interfaceDeclaration.addCallSignature({ - parameters: [ - { name: "args", type: argsParam, hasQuestionToken: config.funcArgCount < 1 }, + dts.rootScope.addInterface( + interfaceName, + [ { - name: "obj", - type: `{ root: ${parentName}, context${qForInfos}: RedwoodGraphQLContext, info${qForInfos}: GraphQLResolveInfo }`, - hasQuestionToken: config.funcArgCount < 2, + type: "call-signature", + optional: config.funcArgCount < 1, + returnType, + params: [ + { name: "args", type: argsParam, optional: config.funcArgCount < 1 }, + { + name: "obj", + type: `{ root: ${parentName}, context${qForInfos}: RedwoodGraphQLContext, info${qForInfos}: GraphQLResolveInfo }`, + optional: config.funcArgCount < 2, + }, + ], }, ], - returnType, - }) - - interfaceDeclaration.forget() + { + exported: true, + docs: nodeDocs.join(" "), + } + ) } /** Ideally, we want to be able to write the type for just the object */ @@ -214,7 +197,7 @@ export const lookAtServiceFile = async (file: string, context: AppContext) => { const gqlType = gql.getType(modelName) if (!gqlType) { // throw new Error(`Could not find a GraphQL type named ${d.getName()}`); - fileDTS.addStatements(`\n// ${modelName} does not exist in the schema`) + // fileDTS.addStatements(`\n// ${modelName} does not exist in the schema`) return } @@ -229,11 +212,9 @@ export const lookAtServiceFile = async (file: string, context: AppContext) => { const hasGenerics = modelFacts.hasGenericArg - // This is what they would have to write - const resolverInterface = fileDTS.addInterface({ - name: `${modelName}TypeResolvers`, - typeParameters: hasGenerics ? ["Extended"] : [], - isExported: true, + const resolverInterface = dts.rootScope.addInterface(`${modelName}TypeResolvers`, [], { + exported: true, + generics: hasGenerics ? [{ name: "Extended" }] : [], }) // Handle extending classes in the runtime which only exist in SDL @@ -241,13 +222,19 @@ export const lookAtServiceFile = async (file: string, context: AppContext) => { if (!parentIsPrisma) extraSharedFileImportReferences.add({ name: `S${modelName}`, import: modelName }) const suffix = parentIsPrisma ? "P" : "S" - // The parent type for the resolvers - fileDTS.addTypeAlias({ - name: `${modelName}AsParent`, - typeParameters: hasGenerics ? ["Extended"] : [], - type: `${suffix}${modelName} ${createParentAdditionallyDefinedFunctions()} ${hasGenerics ? " & Extended" : ""}`, - }) + const parentTypeString = `${suffix}${modelName} ${createParentAdditionallyDefinedFunctions()} ${hasGenerics ? " & Extended" : ""}` + /** + type CurrentUserAccountAsParent = SCurrentUserAccount & { + users: () => PUser[] | Promise | (() => Promise); + registeredPublishingPartner: () => Promise; + subIsViaGift: () => boolean | Promise | (() => Promise); + } + */ + + dts.rootScope.addTypeAlias(`${modelName}AsParent`, t.tsTypeReference(t.identifier(parentTypeString)), { + generics: hasGenerics ? [{ name: "Extended" }] : [], + }) const modelFieldFacts = fieldFacts.get(modelName) ?? {} // Loop through the resolvers, adding the fields which have resolvers implemented in the source file @@ -266,21 +253,18 @@ export const lookAtServiceFile = async (file: string, context: AppContext) => { const qForInfos = resolver.infoParamType === "just_root_destructured" ? "?" : "" const innerArgs = `args${firstQ}: ${argsType}, obj${secondQ}: { root: ${modelName}AsParent${param}, context${qForInfos}: RedwoodGraphQLContext, info${qForInfos}: GraphQLResolveInfo }` - const returnType = returnTypeForResolver(returnTypeMapper, field, resolver) + const args = resolver.isFunc || resolver.isUnknown ? `(${innerArgs}) => ${returnType ?? "any"}` : returnType + + const docs = field.astNode ? `SDL: ${graphql.print(field.astNode)}` : "" + const property = t.tsPropertySignature(t.identifier(fieldName), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(args)))) + t.addComment(property, "leading", " " + docs) - const docs = field.astNode ? [`SDL: ${graphql.print(field.astNode)}`] : [] - // For speed we should switch this out to addProperties eventually - resolverInterface.addProperty({ - name: fieldName, - leadingTrivia: "\n", - docs, - type: resolver.isFunc || resolver.isUnknown ? `(${innerArgs}) => ${returnType ?? "any"}` : returnType, - }) + resolverInterface.body.body.push(property) } else { - resolverInterface.addCallSignature({ - docs: [` @deprecated: SDL ${modelName}.${resolver.name} does not exist in your schema`], - }) + resolverInterface.body.body.push( + t.tsPropertySignature(t.identifier(resolver.name), t.tsTypeAnnotation(t.tsTypeReference(t.identifier("void")))) + ) } }) diff --git a/src/serviceFile2.ts b/src/serviceFile2.ts deleted file mode 100644 index 012fbd5..0000000 --- a/src/serviceFile2.ts +++ /dev/null @@ -1,311 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unnecessary-condition */ - -import t, { tsTypeAnnotation } from "@babel/types" -import * as graphql from "graphql" - -import { AppContext } from "./context.js" -import { getCodeFactsForJSTSFileAtPath } from "./serviceFile.codefacts.js" -import { builder, TSBuilder } from "./tsBuilder.js" -import { CodeFacts, ModelResolverFacts, ResolverFuncFact } from "./typeFacts.js" -import { TypeMapper, typeMapper } from "./typeMap.js" -import { capitalizeFirstLetter, createAndReferOrInlineArgsForField, inlineArgsForField } from "./utils.js" - -export const lookAtServiceFile = async (file: string, context: AppContext) => { - const { gql, prisma, pathSettings: settings, codeFacts: serviceFacts, fieldFacts } = context - - if (!gql) throw new Error(`No schema when wanting to look at service file: ${file}`) - if (!prisma) throw new Error(`No prisma schema when wanting to look at service file: ${file}`) - - // This isn't good enough, needs to be relative to api/src/services - const fileKey = file.replace(settings.apiServicesPath, "") - - const thisFact: CodeFacts = {} - - const filename = context.basename(file) - const dts = builder("", {}) - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const queryType = gql.getQueryType()! - if (!queryType) throw new Error("No query type") - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const mutationType = gql.getMutationType()! - if (!mutationType) throw new Error("No mutation type") - - const externalMapper = typeMapper(context, { preferPrismaModels: true }) - const returnTypeMapper = typeMapper(context, {}) - - // The description of the source file - const fileFacts = getCodeFactsForJSTSFileAtPath(file, context) - if (Object.keys(fileFacts).length === 0) return - - // Tracks prospective prisma models which are used in the file - const extraPrismaReferences = new Set() - const extraSharedFileImportReferences = new Set<{ import: string; name?: string }>() - - // Basically if a top level resolver reference Query or Mutation - const knownSpecialCasesForGraphQL = new Set() - - // Add the root function declarations - const rootResolvers = fileFacts.maybe_query_mutation?.resolvers - if (rootResolvers) - rootResolvers.forEach((v) => { - const isQuery = v.name in queryType.getFields() - const isMutation = v.name in mutationType.getFields() - const parentName = isQuery ? queryType.name : isMutation ? mutationType.name : undefined - if (parentName) { - addDefinitionsForTopLevelResolvers(parentName, v, dts) - } else { - // Add warning about unused resolver - dts.rootScope.addInterface(v.name, [], { exported: true, docs: "This resolver does not exist on Query or Mutation" }) - } - }) - - // Add the root function declarations - Object.values(fileFacts).forEach((model) => { - if (!model) return - const skip = ["maybe_query_mutation", queryType.name, mutationType.name] - if (skip.includes(model.typeName)) return - - addCustomTypeModel(model) - }) - - // Set up the module imports at the top - const sharedGraphQLObjectsReferenced = externalMapper.getReferencedGraphQLThingsInMapping() - const sharedGraphQLObjectsReferencedTypes = [...sharedGraphQLObjectsReferenced.types, ...knownSpecialCasesForGraphQL] - const sharedInternalGraphQLObjectsReferenced = returnTypeMapper.getReferencedGraphQLThingsInMapping() - - const aliases = [...new Set([...sharedGraphQLObjectsReferenced.scalars, ...sharedInternalGraphQLObjectsReferenced.scalars])] - - for (const alias of aliases) { - dts.rootScope.addTypeAlias(alias, "any") - } - - const prismases = [ - ...new Set([ - ...sharedGraphQLObjectsReferenced.prisma, - ...sharedInternalGraphQLObjectsReferenced.prisma, - ...extraPrismaReferences.values(), - ]), - ] - - const validPrismaObjs = prismases.filter((p) => prisma.has(p)) - if (validPrismaObjs.length) { - dts.setImport("@prisma/client", { subImports: validPrismaObjs.map((p) => `${p} as P${p}`) }) - } - - const initialResult = dts.getResult() - if (initialResult.includes("GraphQLResolveInfo")) { - dts.setImport("graphql", { subImports: ["GraphQLResolveInfo"] }) - } - - if (initialResult.includes("RedwoodGraphQLContext")) { - dts.setImport("@redwoodjs/graphql-server/dist/types", { subImports: ["RedwoodGraphQLContext"] }) - } - - if (sharedInternalGraphQLObjectsReferenced.types.length || extraSharedFileImportReferences.size) { - const source = `./${settings.sharedInternalFilename.replace(".d.ts", "")}` - dts.setImport(source, { - subImports: [ - ...sharedInternalGraphQLObjectsReferenced.types.map((t) => `${t} as RT${t}`), - ...[...extraSharedFileImportReferences.values()].map((t) => ("name" in t && t.name ? `${t.import} as ${t.name}` : t.import)), - ], - }) - } - - if (sharedGraphQLObjectsReferencedTypes.length) { - const source = `./${settings.sharedFilename.replace(".d.ts", "")}` - dts.setImport(source, { subImports: sharedGraphQLObjectsReferencedTypes }) - } - - serviceFacts.set(fileKey, thisFact) - - const dtsFilename = filename.endsWith(".ts") ? filename.replace(".ts", ".d.ts") : filename.replace(".js", ".d.ts") - const dtsFilepath = context.join(context.pathSettings.typesFolderRoot, dtsFilename) - - // Some manual formatting tweaks so we align with Redwood's setup more - const final = dts.getResult() - - const shouldWriteDTS = !!final.trim().length - if (!shouldWriteDTS) return - - const formatted = final // await formatDTS(dtsFilepath, dts) - - // Don't make a file write if the content is the same - const priorContent = context.sys.readFile(dtsFilename) - if (priorContent === formatted) return - - context.sys.writeFile(dtsFilepath, formatted) - return dtsFilepath - - function addDefinitionsForTopLevelResolvers(parentName: string, config: ResolverFuncFact, dts: TSBuilder) { - const { name } = config - let field = queryType.getFields()[name] - if (!field) { - field = mutationType.getFields()[name] - } - - const nodeDocs = field.astNode - ? ["SDL: " + graphql.print(field.astNode)] - : ["@deprecated: Could not find this field in the schema for Mutation or Query"] - const interfaceName = `${capitalizeFirstLetter(config.name)}Resolver` - - const args = createAndReferOrInlineArgsForField(field, { - name: interfaceName, - dts, - mapper: externalMapper.map, - }) - - if (parentName === queryType.name) knownSpecialCasesForGraphQL.add(queryType.name) - if (parentName === mutationType.name) knownSpecialCasesForGraphQL.add(mutationType.name) - - const argsParam = args ?? "object" - const qForInfos = config.infoParamType === "just_root_destructured" ? "?" : "" - const returnType = returnTypeForResolver(returnTypeMapper, field, config) - - dts.rootScope.addInterface( - interfaceName, - [ - { - type: "call-signature", - optional: config.funcArgCount < 1, - returnType, - params: [ - { name: "args", type: argsParam, optional: config.funcArgCount < 1 }, - { - name: "obj", - type: `{ root: ${parentName}, context${qForInfos}: RedwoodGraphQLContext, info${qForInfos}: GraphQLResolveInfo }`, - optional: config.funcArgCount < 2, - }, - ], - }, - ], - { - exported: true, - docs: nodeDocs.join(" "), - } - ) - } - - /** Ideally, we want to be able to write the type for just the object */ - function addCustomTypeModel(modelFacts: ModelResolverFacts) { - const modelName = modelFacts.typeName - extraPrismaReferences.add(modelName) - - // Make an interface, this is the version we are replacing from graphql-codegen: - // Account: MergePrismaWithSdlTypes, AllMappedModels>; - const gqlType = gql.getType(modelName) - if (!gqlType) { - // throw new Error(`Could not find a GraphQL type named ${d.getName()}`); - // fileDTS.addStatements(`\n// ${modelName} does not exist in the schema`) - return - } - - if (!graphql.isObjectType(gqlType)) { - throw new Error(`In your schema ${modelName} is not an object, which we can only make resolver types for`) - } - - const fields = gqlType.getFields() - - // See: https://github.com/redwoodjs/redwood/pull/6228#issue-1342966511 - // For more ideas - - const hasGenerics = modelFacts.hasGenericArg - - const resolverInterface = dts.rootScope.addInterface(`${modelName}TypeResolvers`, [], { - exported: true, - generics: hasGenerics ? [{ name: "Extended" }] : [], - }) - - // Handle extending classes in the runtime which only exist in SDL - const parentIsPrisma = prisma.has(modelName) - if (!parentIsPrisma) extraSharedFileImportReferences.add({ name: `S${modelName}`, import: modelName }) - const suffix = parentIsPrisma ? "P" : "S" - - const parentTypeString = `${suffix}${modelName} ${createParentAdditionallyDefinedFunctions()} ${hasGenerics ? " & Extended" : ""}` - - /** - type CurrentUserAccountAsParent = SCurrentUserAccount & { - users: () => PUser[] | Promise | (() => Promise); - registeredPublishingPartner: () => Promise; - subIsViaGift: () => boolean | Promise | (() => Promise); - } - */ - - dts.rootScope.addTypeAlias(`${modelName}AsParent`, t.tsTypeReference(t.identifier(parentTypeString)), { - generics: hasGenerics ? [{ name: "Extended" }] : [], - }) - const modelFieldFacts = fieldFacts.get(modelName) ?? {} - - // Loop through the resolvers, adding the fields which have resolvers implemented in the source file - modelFacts.resolvers.forEach((resolver) => { - const field = fields[resolver.name] - if (field) { - const fieldName = resolver.name - if (modelFieldFacts[fieldName]) modelFieldFacts[fieldName].hasResolverImplementation = true - else modelFieldFacts[fieldName] = { hasResolverImplementation: true } - - const argsType = inlineArgsForField(field, { mapper: externalMapper.map }) ?? "undefined" - const param = hasGenerics ? "" : "" - - const firstQ = resolver.funcArgCount < 1 ? "?" : "" - const secondQ = resolver.funcArgCount < 2 ? "?" : "" - const qForInfos = resolver.infoParamType === "just_root_destructured" ? "?" : "" - - const innerArgs = `args${firstQ}: ${argsType}, obj${secondQ}: { root: ${modelName}AsParent${param}, context${qForInfos}: RedwoodGraphQLContext, info${qForInfos}: GraphQLResolveInfo }` - const returnType = returnTypeForResolver(returnTypeMapper, field, resolver) - const args = resolver.isFunc || resolver.isUnknown ? `(${innerArgs}) => ${returnType ?? "any"}` : returnType - - const docs = field.astNode ? `SDL: ${graphql.print(field.astNode)}` : "" - const property = t.tsPropertySignature(t.identifier(fieldName), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(args)))) - t.addComment(property, "leading", docs) - - resolverInterface.body.body.push(property) - } else { - resolverInterface.body.body.push( - t.tsPropertySignature(t.identifier(resolver.name), t.tsTypeAnnotation(t.tsTypeReference(t.identifier("void")))) - ) - } - }) - - function createParentAdditionallyDefinedFunctions() { - const fns: string[] = [] - modelFacts.resolvers.forEach((resolver) => { - const existsInGraphQLSchema = fields[resolver.name] - if (!existsInGraphQLSchema) { - console.warn( - `The service file ${filename} has a field ${resolver.name} on ${modelName} that does not exist in the generated schema.graphql` - ) - } - - const prefix = !existsInGraphQLSchema ? "\n// This field does not exist in the generated schema.graphql\n" : "" - const returnType = returnTypeForResolver(externalMapper, existsInGraphQLSchema, resolver) - // fns.push(`${prefix}${resolver.name}: () => Promise<${externalMapper.map(type, {})}>`) - fns.push(`${prefix}${resolver.name}: () => ${returnType}`) - }) - - if (fns.length < 1) return "" - return "& {" + fns.join(", \n") + "}" - } - - fieldFacts.set(modelName, modelFieldFacts) - } -} - -function returnTypeForResolver(mapper: TypeMapper, field: graphql.GraphQLField | undefined, resolver: ResolverFuncFact) { - if (!field) return "void" - - const tType = mapper.map(field.type, { preferNullOverUndefined: true, typenamePrefix: "RT" }) ?? "void" - - let returnType = tType - const all = `${tType} | Promise<${tType}> | (() => Promise<${tType}>)` - - if (resolver.isFunc && resolver.isAsync) returnType = `Promise<${tType}>` - else if (resolver.isFunc && resolver.isObjLiteral) returnType = tType - else if (resolver.isFunc) returnType = all - else if (resolver.isObjLiteral) returnType = tType - else if (resolver.isUnknown) returnType = all - - return returnType -} -/* eslint-enable @typescript-eslint/no-unnecessary-condition */ diff --git a/src/sharedSchema.ts b/src/sharedSchema.ts index 28c2d59..abb6aaf 100644 --- a/src/sharedSchema.ts +++ b/src/sharedSchema.ts @@ -1,12 +1,9 @@ /// The main schema for objects and inputs -import { BlockStatement } from "@babel/types" import t from "@babel/types" import * as graphql from "graphql" -import * as tsMorph from "ts-morph" import { AppContext } from "./context.js" -import { formatDTS } from "./formatDTS.js" import { builder } from "./tsBuilder.js" import { typeMapper } from "./typeMap.js" import { makeStep } from "./utils.js" @@ -117,7 +114,7 @@ function createSharedExternalSchemaFile(context: AppContext) { if (prior !== text) context.sys.writeFile(fullPath, text) } -async function createSharedReturnPositionSchemaFile(context: AppContext) { +function createSharedReturnPositionSchemaFile(context: AppContext) { const { gql, prisma, fieldFacts } = context const types = gql.getTypeMap() const mapper = typeMapper(context, { preferPrismaModels: true }) diff --git a/src/tests/bugs/parentCanBeGraphQLObject.test.ts b/src/tests/bugs/parentCanBeGraphQLObject.test.ts index 2323445..f4e7ed9 100644 --- a/src/tests/bugs/parentCanBeGraphQLObject.test.ts +++ b/src/tests/bugs/parentCanBeGraphQLObject.test.ts @@ -30,8 +30,8 @@ export const Puzzle = { const { vfsMap } = await getDTSFilesForRun({ sdl, gamesService, prismaSchema }) const dts = vfsMap.get("/types/games.d.ts")! expect(dts.trim()).toMatchInlineSnapshot(` - "interface PuzzleTypeResolvers { - /*SDL: id: Int!*/ + "export interface PuzzleTypeResolvers { + /* SDL: id: Int!*/ id: number; } type PuzzleAsParent = SPuzzle & {id: () => number} ; diff --git a/src/tests/bugs/returnObjectCanBeGraphQLInterfaces.test.ts b/src/tests/bugs/returnObjectCanBeGraphQLInterfaces.test.ts index 42fc270..6c391e5 100644 --- a/src/tests/bugs/returnObjectCanBeGraphQLInterfaces.test.ts +++ b/src/tests/bugs/returnObjectCanBeGraphQLInterfaces.test.ts @@ -31,8 +31,8 @@ export const Game = { const { vfsMap } = await getDTSFilesForRun({ sdl, gamesService, prismaSchema }) const dts = vfsMap.get("/types/games.d.ts")! expect(dts.trim()).toMatchInlineSnapshot(` - "interface GameTypeResolvers { - /*SDL: puzzle: Node!*/ + "export interface GameTypeResolvers { + /* SDL: puzzle: Node!*/ puzzle: (args?: undefined, obj?: { root: GameAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => RTNode | Promise | (() => Promise); } type GameAsParent = PGame & {puzzle: () => RTNode | Promise | (() => Promise)} ; diff --git a/src/tests/features/generatesTypesForUnions.test.ts b/src/tests/features/generatesTypesForUnions.test.ts index 7762f7d..508e3da 100644 --- a/src/tests/features/generatesTypesForUnions.test.ts +++ b/src/tests/features/generatesTypesForUnions.test.ts @@ -38,21 +38,21 @@ export const Game = { const { vfsMap } = await getDTSFilesForRun({ sdl, gamesService, prismaSchema, generateShared: true }) const dts = vfsMap.get("/types/shared-schema-types.d.ts")! expect(dts.trim()).toMatchInlineSnapshot(` - "interface Game { + "export interface Game { __typename?: \\"Game\\"; id?: number; } - interface Puzzle { + export interface Puzzle { __typename?: \\"Puzzle\\"; id: number; } - type Gameish = Game | Puzzle; - interface Query { + export type Gameish = Game | Puzzle; + export interface Query { __typename?: \\"Query\\"; gameObj?: Game| null | Puzzle| null| null; gameArr: (Game | Puzzle)[]; } - interface Mutation { + export interface Mutation { __typename?: \\"Mutation\\"; __?: string| null; }" diff --git a/src/tests/features/preferPromiseFnWhenKnown.test.ts b/src/tests/features/preferPromiseFnWhenKnown.test.ts index 18e57c2..388fb1e 100644 --- a/src/tests/features/preferPromiseFnWhenKnown.test.ts +++ b/src/tests/features/preferPromiseFnWhenKnown.test.ts @@ -54,29 +54,34 @@ export const Game = { const { vfsMap } = await getDTSFilesForRun({ sdl, gamesService, prismaSchema }) const dts = vfsMap.get("/types/games.d.ts")! expect(dts.trim()).toMatchInlineSnapshot(` - "interface GameSyncResolver { - (args?: RTGame| null | Promise | (() => Promise), obj?: RTGame| null | Promise | (() => Promise)): RTGame| null | Promise | (() => Promise); + "/*SDL: gameSync: Game*/ + export interface GameSyncResolver { + (args?: object, obj?: { root: Query, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTGame| null | Promise | (() => Promise); } - interface GameAsyncResolver { - (args?: Promise, obj?: Promise): Promise; + /*SDL: gameAsync: Game*/ + export interface GameAsyncResolver { + (args?: object, obj?: { root: Query, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): Promise; } - interface GameAsync1ArgResolver { - (args: RTGame| null | Promise | (() => Promise), obj?: RTGame| null | Promise | (() => Promise)): RTGame| null | Promise | (() => Promise); + /*SDL: gameAsync1Arg: Game*/ + export interface GameAsync1ArgResolver { + (args: object, obj?: { root: Query, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTGame| null | Promise | (() => Promise); } - interface GameAsync2ArgResolver { - (args: RTGame| null | Promise | (() => Promise), obj: RTGame| null | Promise | (() => Promise)): RTGame| null | Promise | (() => Promise); + /*SDL: gameAsync2Arg: Game*/ + export interface GameAsync2ArgResolver { + (args: object, obj: { root: Query, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTGame| null | Promise | (() => Promise); } - interface GameObjResolver { - (args?: RTGame| null, obj?: RTGame| null): RTGame| null; + /*SDL: gameObj: Game*/ + export interface GameObjResolver { + (args?: object, obj?: { root: Query, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTGame| null; } - interface GameTypeResolvers { - /*SDL: summary: String!*/ + export interface GameTypeResolvers { + /* SDL: summary: String!*/ summary: string; - /*SDL: summarySync: String!*/ + /* SDL: summarySync: String!*/ summarySync: (args?: undefined, obj?: { root: GameAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => string; - /*SDL: summarySyncBlock: String!*/ + /* SDL: summarySyncBlock: String!*/ summarySyncBlock: (args?: undefined, obj?: { root: GameAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => string | Promise | (() => Promise); - /*SDL: summaryAsync: String!*/ + /* SDL: summaryAsync: String!*/ summaryAsync: (args?: undefined, obj?: { root: GameAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => Promise; } type GameAsParent = PGame & {summary: () => string, diff --git a/src/tests/features/returnTypePositionsWhichPreferPrisma.test.ts b/src/tests/features/returnTypePositionsWhichPreferPrisma.test.ts index 0ffa49a..7f1a32b 100644 --- a/src/tests/features/returnTypePositionsWhichPreferPrisma.test.ts +++ b/src/tests/features/returnTypePositionsWhichPreferPrisma.test.ts @@ -41,11 +41,12 @@ export const Game = { expect(dts.trimStart()).toMatchInlineSnapshot( ` - "interface GameResolver { - (args?: RTGame| null | Promise | (() => Promise), obj?: RTGame| null | Promise | (() => Promise)): RTGame| null | Promise | (() => Promise); + "/*SDL: game: Game*/ + export interface GameResolver { + (args?: object, obj?: { root: Query, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTGame| null | Promise | (() => Promise); } - interface GameTypeResolvers { - /*SDL: summary: String!*/ + export interface GameTypeResolvers { + /* SDL: summary: String!*/ summary: (args: undefined, obj: { root: GameAsParent, context?: RedwoodGraphQLContext, info?: GraphQLResolveInfo }) => string; } type GameAsParent = PGame & {summary: () => string} ; diff --git a/src/tests/features/supportGenericExtension.test.ts b/src/tests/features/supportGenericExtension.test.ts index c8018a0..c5b375d 100644 --- a/src/tests/features/supportGenericExtension.test.ts +++ b/src/tests/features/supportGenericExtension.test.ts @@ -29,14 +29,11 @@ export const Game: GameResolvers<{ type: string }> = {}; expect(vfsMap.get("/types/games.d.ts")!).toContain("interface GameTypeResolvers") - expect(vfsMap.get("/types/games.d.ts")!).toContain("GameAsParent = PGame & Extended") + expect(vfsMap.get("/types/games.d.ts")!).toContain("GameAsParent = PGame & Extended") expect(vfsMap.get("/types/games.d.ts"))!.toMatchInlineSnapshot(` - "import type { Game as PGame } from \\"@prisma/client\\"; - - export interface GameTypeResolvers {} - - type GameAsParent = PGame & Extended; - " + "export interface GameTypeResolvers {} + type GameAsParent = PGame & Extended; + import { Game as PGame } from \\"@prisma/client\\";" `) }) diff --git a/src/tests/features/supportReferringToEnumsOnlyInSDL.test.ts b/src/tests/features/supportReferringToEnumsOnlyInSDL.test.ts index aa8132a..8f903d3 100644 --- a/src/tests/features/supportReferringToEnumsOnlyInSDL.test.ts +++ b/src/tests/features/supportReferringToEnumsOnlyInSDL.test.ts @@ -38,28 +38,31 @@ export const Game: GameResolvers = {}; // We are expecting to see import type GameType from "./shared-schema-types" expect(vfsMap.get("/types/games.d.ts")).toMatchInlineSnapshot(` - "interface AllGamesResolver { - (args?: RTGame[] | Promise | (() => Promise), obj?: RTGame[] | Promise | (() => Promise)): RTGame[] | Promise | (() => Promise); + "/*SDL: allGames(type: GameType!): [Game!]!*/ + export interface AllGamesResolver { + (args?: {type: GameType}, obj?: { root: Query, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTGame[] | Promise | (() => Promise); } - interface GameTypeResolvers {} + export interface GameTypeResolvers {} type GameAsParent = PGame ; import { Game as PGame } from \\"@prisma/client\\"; + import { GraphQLResolveInfo } from \\"graphql\\"; + import { RedwoodGraphQLContext } from \\"@redwoodjs/graphql-server/dist/types\\"; import { Game as RTGame } from \\"./shared-return-types\\"; import { GameType, Query } from \\"./shared-schema-types\\";" `) expect(vfsMap.get("/types/shared-schema-types.d.ts"))!.toMatchInlineSnapshot(` - "interface Game { + "export interface Game { __typename?: \\"Game\\"; id: number; games: Game[]; } - interface Query { + export interface Query { __typename?: \\"Query\\"; allGames: Game[]; } - type GameType = \\"FOOTBALL\\" | \\"BASKETBALL\\"; - interface Mutation { + export type GameType = \\"FOOTBALL\\" | \\"BASKETBALL\\"; + export interface Mutation { __typename?: \\"Mutation\\"; __?: string| null; }" diff --git a/src/tests/testRunner.ts b/src/tests/testRunner.ts index 0c519ac..3d4b4de 100644 --- a/src/tests/testRunner.ts +++ b/src/tests/testRunner.ts @@ -6,7 +6,7 @@ import { Project } from "ts-morph" import { AppContext } from "../context.js" import { prismaModeller } from "../prismaModeller.js" -import { lookAtServiceFile } from "../serviceFile2.js" +import { lookAtServiceFile } from "../serviceFile.js" import { createSharedSchemaFiles } from "../sharedSchema.js" import type { CodeFacts, FieldFacts } from "../typeFacts.js" @@ -56,7 +56,7 @@ export async function getDTSFilesForRun(run: Run) { } if (run.generateShared) { - await createSharedSchemaFiles(appContext) + await createSharedSchemaFiles(appContext, false) } return { diff --git a/src/tsBuilder.ts b/src/tsBuilder.ts index 9fb3934..17943df 100644 --- a/src/tsBuilder.ts +++ b/src/tsBuilder.ts @@ -1,5 +1,3 @@ -// @eslint-disable-file - import generator from "@babel/generator" import parser from "@babel/parser" import traverse from "@babel/traverse" @@ -113,7 +111,9 @@ export const builder = (priorSource: string, opts: {}) => { return } - throw new Error(`Unsupported type annotation: ${newAnnotion.type} - ${generator(newAnnotion).code}`) + // @ts-expect-error - ts/js babel interop issue + const code = generator(newAnnotion).code + throw new Error(`Unsupported type annotation: ${newAnnotion.type} - ${code}`) } /** An internal API for describing a new area for inputting template info */ @@ -121,7 +121,7 @@ export const builder = (priorSource: string, opts: {}) => { const addFunction = (name: string) => { let functionNode = statements.find( (s) => t.isVariableDeclaration(s) && t.isIdentifier(s.declarations[0].id) && s.declarations[0].id.name === name - ) + ) as t.VariableDeclaration | undefined if (!functionNode) { functionNode = t.variableDeclaration("const", [ @@ -209,7 +209,7 @@ export const builder = (priorSource: string, opts: {}) => { null, // generics f.params.map((p) => { const i = t.identifier(p.name) - i.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier(f.returnType))) + i.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier(p.type))) if (p.optional) i.optional = true return i }), @@ -219,7 +219,7 @@ export const builder = (priorSource: string, opts: {}) => { } else { const prop = t.tsPropertySignature(t.identifier(f.name), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(f.type)))) prop.optional = f.optional - if (f.docs?.length) t.addComment(prop, "leading", f.docs) + if (f.docs?.length) t.addComment(prop, "leading", " " + f.docs) return prop } }) @@ -253,15 +253,17 @@ export const builder = (priorSource: string, opts: {}) => { /** Experimental function for parsing out a graphql template tag, and ensuring certain fields have been called */ const updateGraphQLTemplateTag = (expression: t.Expression, path: string, modelFields: string[]) => { if (path !== ".") throw new Error("Only support updating the root of the graphql tag ATM") + // @ts-expect-error - ts/js babel interop issue traverse( expression, { - TaggedTemplateExpression(path) { + TaggedTemplateExpression(path: traverse.NodePath) { const { tag, quasi } = path.node if (t.isIdentifier(tag) && tag.name === "graphql") { // This is the graphql query const query = quasi.quasis[0].value.raw const inner = query.match(/\{(.*)\}/)?.[1] + if (inner === undefined) throw new Error("Could not find inner query") path.replaceWithSourceString(`graphql\`${query.replace(inner, `${inner}, ${modelFields.join(", ")}`)}\``) path.stop() @@ -277,6 +279,7 @@ export const builder = (priorSource: string, opts: {}) => { const parseStatement = (code: string) => parser.parse(code, { sourceType: "module", plugins: ["jsx", "typescript"] }).program.body[0] as ExpressionStatement + // @ts-expect-error - ts/js babel interop issue const getResult = () => generator(sourceFile.program, {}).code const rootScope = createScope("root", sourceFile, sourceFile.program.body) @@ -286,7 +289,7 @@ export const builder = (priorSource: string, opts: {}) => { /** Parses something as though it is in type-space and extracts the subset of the AST that the string represents */ const getTypeLevelAST = (type: string) => { const typeAST = parser.parse(`type A = ${type}`, { sourceType: "module", plugins: ["jsx", "typescript"] }) - const typeDeclaration = typeAST.program.body.find((s) => s.type === "TSTypeAliasDeclaration") + const typeDeclaration = typeAST.program.body.find((s) => t.isTSTypeAliasDeclaration(s)) if (!typeDeclaration) throw new Error("No type declaration found in template: " + type) return typeDeclaration.typeAnnotation } @@ -304,5 +307,5 @@ const nodeFromNodeConfig = t.tsTypeParameter(null, null, g.name))) } - return node + return statement }