diff --git a/packages/graphql/package.json b/packages/graphql/package.json index c865f6b7c97..c233dbe9ecb 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -1,5 +1,5 @@ { - "name": "graphql", + "name": "@typespec/graphql", "version": "0.1.0", "type": "module", "main": "dist/src/index.js", diff --git a/packages/graphql/src/emitter.ts b/packages/graphql/src/emitter.ts index 4c28665e5f1..ed0ae59f739 100644 --- a/packages/graphql/src/emitter.ts +++ b/packages/graphql/src/emitter.ts @@ -1,11 +1,37 @@ -import type { EmitContext } from "@typespec/compiler"; -import { emitFile, resolvePath } from "@typespec/compiler"; - -export async function $onEmit(context: EmitContext) { - if (!context.program.compilerOptions.noEmit) { - await emitFile(context.program, { - path: resolvePath(context.emitterOutputDir, "output.txt"), - content: "Hello world\n", - }); - } +import type { EmitContext, NewLine } from "@typespec/compiler"; +import { resolvePath } from "@typespec/compiler"; +import type { GraphQLEmitterOptions } from "./lib.js"; +import { createGraphQLEmitter } from "./schema-emitter.js"; + +const defaultOptions = { + "new-line": "lf", + "omit-unreachable-types": false, + strict: false, +} as const; + +export async function $onEmit(context: EmitContext) { + const options = resolveOptions(context); + const emitter = createGraphQLEmitter(context, options); + await emitter.emitGraphQL(); +} + +export interface ResolvedGraphQLEmitterOptions { + outputFile: string; + newLine: NewLine; + omitUnreachableTypes: boolean; + strict: boolean; +} + +export function resolveOptions( + context: EmitContext, +): ResolvedGraphQLEmitterOptions { + const resolvedOptions = { ...defaultOptions, ...context.options }; + const outputFile = resolvedOptions["output-file"] ?? "{schema-name}.graphql"; + + return { + outputFile: resolvePath(context.emitterOutputDir, outputFile), + newLine: resolvedOptions["new-line"], + omitUnreachableTypes: resolvedOptions["omit-unreachable-types"], + strict: resolvedOptions["strict"], + }; } diff --git a/packages/graphql/src/lib.ts b/packages/graphql/src/lib.ts index 4a44b555d0c..f9216283c20 100644 --- a/packages/graphql/src/lib.ts +++ b/packages/graphql/src/lib.ts @@ -1,8 +1,102 @@ -import { createTypeSpecLibrary } from "@typespec/compiler"; +import { createTypeSpecLibrary, type JSONSchemaType } from "@typespec/compiler"; -export const $lib = createTypeSpecLibrary({ +export interface GraphQLEmitterOptions { + /** + * Name of the output file. + * Output file will interpolate the following values: + * - schema-name: Name of the schema if multiple + * + * @default `{schema-name}.graphql` + * + * @example Single schema + * - `schema.graphql` + * + * @example Multiple schemas + * - `Org1.Schema1.graphql` + * - `Org1.Schema2.graphql` + */ + "output-file"?: string; + + /** + * Set the newline character for emitting files. + * @default lf + */ + "new-line"?: "crlf" | "lf"; + + /** + * Omit unreachable types. + * By default all types declared under the schema namespace will be included. With this flag on only types references in an operation will be emitted. + * @default false + */ + "omit-unreachable-types"?: boolean; + + /** + * Only emit types if a correct GraphQL translation type is found. Don't emit Any types and operations that don't have the GraphQL decorators. + * By default a best effort is made to emit all types. + * @default false + */ + strict?: boolean; +} + +const EmitterOptionsSchema: JSONSchemaType = { + type: "object", + additionalProperties: false, + properties: { + "output-file": { + type: "string", + nullable: true, + description: [ + "Name of the output file.", + " Output file will interpolate the following values:", + " - schema-name: Name of the schema if multiple", + "", + " Default: `{schema-name}.graphql`", + "", + " Example Single schema", + " - `schema.graphql`", + "", + " Example Multiple schemas", + " - `Org1.Schema1.graphql`", + " - `Org1.Schema2.graphql`", + ].join("\n"), + }, + "new-line": { + type: "string", + enum: ["crlf", "lf"], + default: "lf", + nullable: true, + description: "Set the newLine character for emitting files.", + }, + "omit-unreachable-types": { + type: "boolean", + nullable: true, + description: [ + "Omit unreachable types.", + "By default all types declared under the schema namespace will be included.", + "With this flag on only types references in an operation will be emitted.", + ].join("\n"), + }, + strict: { + type: "boolean", + nullable: true, + description: [ + "Only emit types if a correct GraphQL translation type is found.", + "Don't emit Any types and operations that don't have the GraphQL decorators.", + "By default a best effort is made to emit all types.", + ].join("\n"), + }, + }, + required: [], +}; + +export const libDef = { name: "@typespec/graphql", diagnostics: {}, -}); + emitter: { + options: EmitterOptionsSchema as JSONSchemaType, + }, +} as const; + +export const $lib = createTypeSpecLibrary(libDef); export const { reportDiagnostic, createDiagnostic } = $lib; diff --git a/packages/graphql/src/schema-emitter.ts b/packages/graphql/src/schema-emitter.ts new file mode 100644 index 00000000000..8c145f53b54 --- /dev/null +++ b/packages/graphql/src/schema-emitter.ts @@ -0,0 +1,26 @@ +import { emitFile, interpolatePath, type EmitContext } from "@typespec/compiler"; +import type { ResolvedGraphQLEmitterOptions } from "./emitter.js"; +import type { GraphQLEmitterOptions } from "./lib.js"; + +export function createGraphQLEmitter( + context: EmitContext, + options: ResolvedGraphQLEmitterOptions, +) { + const program = context.program; + + return { + emitGraphQL, + }; + + async function emitGraphQL() { + // replace this with the real emitter code + if (!program.compilerOptions.noEmit) { + const filePath = interpolatePath(options.outputFile, { "schema-name": "schema" }); + await emitFile(program, { + path: filePath, + content: "Hello world", + newLine: options.newLine, + }); + } + } +} diff --git a/packages/graphql/src/testing/index.ts b/packages/graphql/src/testing/index.ts index 151fdb2c52b..db254ec8bce 100644 --- a/packages/graphql/src/testing/index.ts +++ b/packages/graphql/src/testing/index.ts @@ -2,6 +2,6 @@ import type { TypeSpecTestLibrary } from "@typespec/compiler/testing"; import { createTestLibrary, findTestPackageRoot } from "@typespec/compiler/testing"; export const GraphqlTestLibrary: TypeSpecTestLibrary = createTestLibrary({ - name: "graphql", + name: "@typespec/graphql", packageRoot: await findTestPackageRoot(import.meta.url), }); diff --git a/packages/graphql/test/hello.test.ts b/packages/graphql/test/hello.test.ts index 00d8f062a17..3a41332b322 100644 --- a/packages/graphql/test/hello.test.ts +++ b/packages/graphql/test/hello.test.ts @@ -3,8 +3,8 @@ import { describe, it } from "vitest"; import { emit } from "./test-host.js"; describe("hello", () => { - it("emit output.txt with content hello world", async () => { - const results = await emit(`op test(): void;`); - strictEqual(results["output.txt"], "Hello world\n"); + it("emit output file with content hello world", async () => { + const emitterContent = await emit(`op test(): void;`); + strictEqual(emitterContent, "Hello world"); }); }); diff --git a/packages/graphql/test/test-host.ts b/packages/graphql/test/test-host.ts index 37a8d1f940c..bfb7af8943a 100644 --- a/packages/graphql/test/test-host.ts +++ b/packages/graphql/test/test-host.ts @@ -1,10 +1,12 @@ import type { Diagnostic } from "@typespec/compiler"; -import { resolvePath } from "@typespec/compiler"; import { createTestHost, createTestWrapper, expectDiagnosticEmpty, + resolveVirtualPath, } from "@typespec/compiler/testing"; +import { ok } from "assert"; +import type { GraphQLEmitterOptions } from "../src/lib.js"; import { GraphqlTestLibrary } from "../src/testing/index.js"; export async function createGraphqlTestHost() { @@ -19,30 +21,33 @@ export async function createGraphqlTestRunner() { return createTestWrapper(host, { compilerOptions: { noEmit: false, - emit: ["graphql"], + emit: ["@typespec/graphql"], }, }); } export async function emitWithDiagnostics( code: string, -): Promise<[Record, readonly Diagnostic[]]> { + options: GraphQLEmitterOptions = {}, +): Promise<[string, readonly Diagnostic[]]> { const runner = await createGraphqlTestRunner(); - await runner.compileAndDiagnose(code, { - outputDir: "tsp-output", + const outputFile = resolveVirtualPath("schema.graphql"); + const compilerOptions = { ...options, "output-file": outputFile }; + const diagnostics = await runner.diagnose(code, { + noEmit: false, + emit: ["@typespec/graphql"], + options: { + "@typespec/graphql": compilerOptions, + }, }); - const emitterOutputDir = "./tsp-output/graphql"; - const files = await runner.program.host.readDir(emitterOutputDir); - - const result: Record = {}; - for (const file of files) { - result[file] = (await runner.program.host.readFile(resolvePath(emitterOutputDir, file))).text; - } - return [result, runner.program.diagnostics]; + const content = runner.fs.get(outputFile); + ok(content, "Expected to have found graphql output"); + // Change this to whatever makes sense for the actual GraphQL emitter, probably a GraphQLSchemaRecord + return [content, diagnostics]; } -export async function emit(code: string): Promise> { - const [result, diagnostics] = await emitWithDiagnostics(code); +export async function emit(code: string, options: GraphQLEmitterOptions = {}): Promise { + const [result, diagnostics] = await emitWithDiagnostics(code, options); expectDiagnosticEmpty(diagnostics); return result; } diff --git a/packages/graphql/tspconfig.yaml b/packages/graphql/tspconfig.yaml new file mode 100644 index 00000000000..cf30ced00a4 --- /dev/null +++ b/packages/graphql/tspconfig.yaml @@ -0,0 +1,3 @@ +linter: + extends: + - "@typespec/graphql/strict"