Skip to content

Commit aee6340

Browse files
swatkatzswatikumar
andauthored
Adds an emitter shell and sets up the tests to get options (microsoft#5033)
### Description This PR sets up the flow to use the GraphQL emitter by providing an interface for the various options that the GraphQL emitter will use eventually. It also sets up test-hosts to work with these options. The actual schema emitter doesn't really do anything other than emit "Hello World" as it did previously, but the options get pass through as confirmed by the test case. Going forward, we can change the code in schema-emitter.ts to setup it up for GraphQL using `navigateProgram`. We need to add diagnostics in the emitter lib definition, but that can be done in a separate PR. The next PR will have the outer layer of the GraphQL emitter setup to deal with multiple schemas similar to multiple services in the OAI emitter. ### Testing Run the tests and see that they pass. --------- Co-authored-by: swatikumar <swatikumar@pinterest.com>
1 parent 81ad1d5 commit aee6340

File tree

8 files changed

+187
-33
lines changed

8 files changed

+187
-33
lines changed

packages/graphql/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "graphql",
2+
"name": "@typespec/graphql",
33
"version": "0.1.0",
44
"type": "module",
55
"main": "dist/src/index.js",

packages/graphql/src/emitter.ts

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,37 @@
1-
import type { EmitContext } from "@typespec/compiler";
2-
import { emitFile, resolvePath } from "@typespec/compiler";
3-
4-
export async function $onEmit(context: EmitContext) {
5-
if (!context.program.compilerOptions.noEmit) {
6-
await emitFile(context.program, {
7-
path: resolvePath(context.emitterOutputDir, "output.txt"),
8-
content: "Hello world\n",
9-
});
10-
}
1+
import type { EmitContext, NewLine } from "@typespec/compiler";
2+
import { resolvePath } from "@typespec/compiler";
3+
import type { GraphQLEmitterOptions } from "./lib.js";
4+
import { createGraphQLEmitter } from "./schema-emitter.js";
5+
6+
const defaultOptions = {
7+
"new-line": "lf",
8+
"omit-unreachable-types": false,
9+
strict: false,
10+
} as const;
11+
12+
export async function $onEmit(context: EmitContext<GraphQLEmitterOptions>) {
13+
const options = resolveOptions(context);
14+
const emitter = createGraphQLEmitter(context, options);
15+
await emitter.emitGraphQL();
16+
}
17+
18+
export interface ResolvedGraphQLEmitterOptions {
19+
outputFile: string;
20+
newLine: NewLine;
21+
omitUnreachableTypes: boolean;
22+
strict: boolean;
23+
}
24+
25+
export function resolveOptions(
26+
context: EmitContext<GraphQLEmitterOptions>,
27+
): ResolvedGraphQLEmitterOptions {
28+
const resolvedOptions = { ...defaultOptions, ...context.options };
29+
const outputFile = resolvedOptions["output-file"] ?? "{schema-name}.graphql";
30+
31+
return {
32+
outputFile: resolvePath(context.emitterOutputDir, outputFile),
33+
newLine: resolvedOptions["new-line"],
34+
omitUnreachableTypes: resolvedOptions["omit-unreachable-types"],
35+
strict: resolvedOptions["strict"],
36+
};
1137
}

packages/graphql/src/lib.ts

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,102 @@
1-
import { createTypeSpecLibrary } from "@typespec/compiler";
1+
import { createTypeSpecLibrary, type JSONSchemaType } from "@typespec/compiler";
22

3-
export const $lib = createTypeSpecLibrary({
3+
export interface GraphQLEmitterOptions {
4+
/**
5+
* Name of the output file.
6+
* Output file will interpolate the following values:
7+
* - schema-name: Name of the schema if multiple
8+
*
9+
* @default `{schema-name}.graphql`
10+
*
11+
* @example Single schema
12+
* - `schema.graphql`
13+
*
14+
* @example Multiple schemas
15+
* - `Org1.Schema1.graphql`
16+
* - `Org1.Schema2.graphql`
17+
*/
18+
"output-file"?: string;
19+
20+
/**
21+
* Set the newline character for emitting files.
22+
* @default lf
23+
*/
24+
"new-line"?: "crlf" | "lf";
25+
26+
/**
27+
* Omit unreachable types.
28+
* 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.
29+
* @default false
30+
*/
31+
"omit-unreachable-types"?: boolean;
32+
33+
/**
34+
* 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.
35+
* By default a best effort is made to emit all types.
36+
* @default false
37+
*/
38+
strict?: boolean;
39+
}
40+
41+
const EmitterOptionsSchema: JSONSchemaType<GraphQLEmitterOptions> = {
42+
type: "object",
43+
additionalProperties: false,
44+
properties: {
45+
"output-file": {
46+
type: "string",
47+
nullable: true,
48+
description: [
49+
"Name of the output file.",
50+
" Output file will interpolate the following values:",
51+
" - schema-name: Name of the schema if multiple",
52+
"",
53+
" Default: `{schema-name}.graphql`",
54+
"",
55+
" Example Single schema",
56+
" - `schema.graphql`",
57+
"",
58+
" Example Multiple schemas",
59+
" - `Org1.Schema1.graphql`",
60+
" - `Org1.Schema2.graphql`",
61+
].join("\n"),
62+
},
63+
"new-line": {
64+
type: "string",
65+
enum: ["crlf", "lf"],
66+
default: "lf",
67+
nullable: true,
68+
description: "Set the newLine character for emitting files.",
69+
},
70+
"omit-unreachable-types": {
71+
type: "boolean",
72+
nullable: true,
73+
description: [
74+
"Omit unreachable types.",
75+
"By default all types declared under the schema namespace will be included.",
76+
"With this flag on only types references in an operation will be emitted.",
77+
].join("\n"),
78+
},
79+
strict: {
80+
type: "boolean",
81+
nullable: true,
82+
description: [
83+
"Only emit types if a correct GraphQL translation type is found.",
84+
"Don't emit Any types and operations that don't have the GraphQL decorators.",
85+
"By default a best effort is made to emit all types.",
86+
].join("\n"),
87+
},
88+
},
89+
required: [],
90+
};
91+
92+
export const libDef = {
493
name: "@typespec/graphql",
594
diagnostics: {},
6-
});
95+
emitter: {
96+
options: EmitterOptionsSchema as JSONSchemaType<GraphQLEmitterOptions>,
97+
},
98+
} as const;
99+
100+
export const $lib = createTypeSpecLibrary(libDef);
7101

8102
export const { reportDiagnostic, createDiagnostic } = $lib;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { emitFile, interpolatePath, type EmitContext } from "@typespec/compiler";
2+
import type { ResolvedGraphQLEmitterOptions } from "./emitter.js";
3+
import type { GraphQLEmitterOptions } from "./lib.js";
4+
5+
export function createGraphQLEmitter(
6+
context: EmitContext<GraphQLEmitterOptions>,
7+
options: ResolvedGraphQLEmitterOptions,
8+
) {
9+
const program = context.program;
10+
11+
return {
12+
emitGraphQL,
13+
};
14+
15+
async function emitGraphQL() {
16+
// replace this with the real emitter code
17+
if (!program.compilerOptions.noEmit) {
18+
const filePath = interpolatePath(options.outputFile, { "schema-name": "schema" });
19+
await emitFile(program, {
20+
path: filePath,
21+
content: "Hello world",
22+
newLine: options.newLine,
23+
});
24+
}
25+
}
26+
}

packages/graphql/src/testing/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ import type { TypeSpecTestLibrary } from "@typespec/compiler/testing";
22
import { createTestLibrary, findTestPackageRoot } from "@typespec/compiler/testing";
33

44
export const GraphqlTestLibrary: TypeSpecTestLibrary = createTestLibrary({
5-
name: "graphql",
5+
name: "@typespec/graphql",
66
packageRoot: await findTestPackageRoot(import.meta.url),
77
});

packages/graphql/test/hello.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { describe, it } from "vitest";
33
import { emit } from "./test-host.js";
44

55
describe("hello", () => {
6-
it("emit output.txt with content hello world", async () => {
7-
const results = await emit(`op test(): void;`);
8-
strictEqual(results["output.txt"], "Hello world\n");
6+
it("emit output file with content hello world", async () => {
7+
const emitterContent = await emit(`op test(): void;`);
8+
strictEqual(emitterContent, "Hello world");
99
});
1010
});

packages/graphql/test/test-host.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import type { Diagnostic } from "@typespec/compiler";
2-
import { resolvePath } from "@typespec/compiler";
32
import {
43
createTestHost,
54
createTestWrapper,
65
expectDiagnosticEmpty,
6+
resolveVirtualPath,
77
} from "@typespec/compiler/testing";
8+
import { ok } from "assert";
9+
import type { GraphQLEmitterOptions } from "../src/lib.js";
810
import { GraphqlTestLibrary } from "../src/testing/index.js";
911

1012
export async function createGraphqlTestHost() {
@@ -19,30 +21,33 @@ export async function createGraphqlTestRunner() {
1921
return createTestWrapper(host, {
2022
compilerOptions: {
2123
noEmit: false,
22-
emit: ["graphql"],
24+
emit: ["@typespec/graphql"],
2325
},
2426
});
2527
}
2628

2729
export async function emitWithDiagnostics(
2830
code: string,
29-
): Promise<[Record<string, string>, readonly Diagnostic[]]> {
31+
options: GraphQLEmitterOptions = {},
32+
): Promise<[string, readonly Diagnostic[]]> {
3033
const runner = await createGraphqlTestRunner();
31-
await runner.compileAndDiagnose(code, {
32-
outputDir: "tsp-output",
34+
const outputFile = resolveVirtualPath("schema.graphql");
35+
const compilerOptions = { ...options, "output-file": outputFile };
36+
const diagnostics = await runner.diagnose(code, {
37+
noEmit: false,
38+
emit: ["@typespec/graphql"],
39+
options: {
40+
"@typespec/graphql": compilerOptions,
41+
},
3342
});
34-
const emitterOutputDir = "./tsp-output/graphql";
35-
const files = await runner.program.host.readDir(emitterOutputDir);
36-
37-
const result: Record<string, string> = {};
38-
for (const file of files) {
39-
result[file] = (await runner.program.host.readFile(resolvePath(emitterOutputDir, file))).text;
40-
}
41-
return [result, runner.program.diagnostics];
43+
const content = runner.fs.get(outputFile);
44+
ok(content, "Expected to have found graphql output");
45+
// Change this to whatever makes sense for the actual GraphQL emitter, probably a GraphQLSchemaRecord
46+
return [content, diagnostics];
4247
}
4348

44-
export async function emit(code: string): Promise<Record<string, string>> {
45-
const [result, diagnostics] = await emitWithDiagnostics(code);
49+
export async function emit(code: string, options: GraphQLEmitterOptions = {}): Promise<string> {
50+
const [result, diagnostics] = await emitWithDiagnostics(code, options);
4651
expectDiagnosticEmpty(diagnostics);
4752
return result;
4853
}

packages/graphql/tspconfig.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
linter:
2+
extends:
3+
- "@typespec/graphql/strict"

0 commit comments

Comments
 (0)