Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/graphql/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "graphql",
"name": "@typespec/graphql",
"version": "0.1.0",
"type": "module",
"main": "dist/src/index.js",
Expand Down
46 changes: 36 additions & 10 deletions packages/graphql/src/emitter.ts
Original file line number Diff line number Diff line change
@@ -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<GraphQLEmitterOptions>) {
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<GraphQLEmitterOptions>,
): 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"],
};
}
100 changes: 97 additions & 3 deletions packages/graphql/src/lib.ts
Original file line number Diff line number Diff line change
@@ -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<GraphQLEmitterOptions> = {
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<GraphQLEmitterOptions>,
},
} as const;

export const $lib = createTypeSpecLibrary(libDef);

export const { reportDiagnostic, createDiagnostic } = $lib;
26 changes: 26 additions & 0 deletions packages/graphql/src/schema-emitter.ts
Original file line number Diff line number Diff line change
@@ -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<GraphQLEmitterOptions>,
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,
});
}
}
}
2 changes: 1 addition & 1 deletion packages/graphql/src/testing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});
6 changes: 3 additions & 3 deletions packages/graphql/test/hello.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
35 changes: 20 additions & 15 deletions packages/graphql/test/test-host.ts
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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<string, string>, 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<string, string> = {};
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<Record<string, string>> {
const [result, diagnostics] = await emitWithDiagnostics(code);
export async function emit(code: string, options: GraphQLEmitterOptions = {}): Promise<string> {
const [result, diagnostics] = await emitWithDiagnostics(code, options);
expectDiagnosticEmpty(diagnostics);
return result;
}
3 changes: 3 additions & 0 deletions packages/graphql/tspconfig.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
linter:
extends:
- "@typespec/graphql/strict"