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
78 changes: 49 additions & 29 deletions packages/graphql/src/emitter.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,57 @@
import type { EmitContext, NewLine } from "@typespec/compiler";
import { resolvePath } from "@typespec/compiler";
import { createGraphQLEmitter } from "./graphql-emitter.js";
import type { GraphQLEmitterOptions } from "./lib.js";

const defaultOptions = {
"new-line": "lf",
"omit-unreachable-types": false,
strict: false,
} as const;
import { type EmitContext, type Namespace } from "@typespec/compiler";
import { type GraphQLEmitterOptions, reportDiagnostic } from "./lib.js";
import { listSchemas } from "./lib/schema.js";
import { createGraphQLMutationEngine } from "./mutation-engine/index.js";
import { mutateSchema } from "./mutation-engine/schema-mutator.js";
import type { TypeGraph } from "./mutation-engine/type-graph.js";
import { resolveTypeUsage } from "./type-usage.js";

/**
* Main emitter entry point for GraphQL SDL generation.
*
* Pipeline: type-usage → mutation → buildTypeGraph → render.
* Rendering is a stub in this PR — will be implemented when the Schema
* orchestrator component is added.
*/
export async function $onEmit(context: EmitContext<GraphQLEmitterOptions>) {
const options = resolveOptions(context);
const emitter = createGraphQLEmitter(context, options);
await emitter.emitGraphQL();
}
const schemas = listSchemas(context.program);
if (schemas.length === 0) {
schemas.push({ type: context.program.getGlobalNamespaceType() });
}

export interface ResolvedGraphQLEmitterOptions {
outputFile: string;
newLine: NewLine;
omitUnreachableTypes: boolean;
strict: boolean;
for (const schema of schemas) {
const typeGraph = emitSchema(context, schema.type);
if (typeGraph) {
renderSchema(typeGraph, schema.name);
}
}
}

export function resolveOptions(
function emitSchema(
context: EmitContext<GraphQLEmitterOptions>,
): ResolvedGraphQLEmitterOptions {
const resolvedOptions = { ...defaultOptions, ...context.options };
const outputFile = resolvedOptions["output-file"] ?? "{schema-name}.graphql";
schema: Namespace,
): TypeGraph | undefined {
const program = context.program;
const omitUnreachable = context.options["omit-unreachable-types"] ?? false;

const typeUsage = resolveTypeUsage(schema, omitUnreachable);
const engine = createGraphQLMutationEngine(program);
const typeGraph = mutateSchema(program, engine, schema, typeUsage);

if (typeGraph.globalNamespace.operations.size === 0) {
reportDiagnostic(program, {
code: "empty-schema",
target: schema,
});
return undefined;
}

return typeGraph;
}

return {
outputFile: resolvePath(context.emitterOutputDir, outputFile),
newLine: resolvedOptions["new-line"],
omitUnreachableTypes: resolvedOptions["omit-unreachable-types"],
strict: resolvedOptions["strict"],
};
function renderSchema(_typeGraph: TypeGraph, _schemaName?: string): void {
// Stub — will be replaced with Alloy component rendering:
// <GraphQLSchemaContext.Provider value={{ typeGraph }}>
// <Schema />
// </GraphQLSchemaContext.Provider>
}
63 changes: 0 additions & 63 deletions packages/graphql/src/graphql-emitter.ts

This file was deleted.

7 changes: 7 additions & 0 deletions packages/graphql/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,13 @@ export const libDef = {
default: paramMessage`Scalar "${"name"}" collides with GraphQL built-in type "${"builtinName"}". This may cause unexpected behavior. Consider renaming the scalar.`,
},
},
"empty-schema": {
severity: "warning",
messages: {
default:
"GraphQL schema has no operations. At minimum a Query root type is required.",
},
},
},
emitter: {
options: EmitterOptionsSchema as JSONSchemaType<GraphQLEmitterOptions>,
Expand Down
4 changes: 3 additions & 1 deletion packages/graphql/src/lib/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import {

import { useStateMap, useStateSet } from "@typespec/compiler/utils";
import { GraphQLKeys, NAMESPACE, reportDiagnostic } from "../lib.js";
import type { Tagged } from "../types.d.ts";
import { propertiesEqual } from "./utils.js";

declare const tags: unique symbol;
type Tagged<BaseType, Tag extends PropertyKey> = BaseType & { [tags]: { [K in Tag]: void } };

// This will set the namespace for decorators implemented in this file
export const namespace = NAMESPACE;

Expand Down
2 changes: 2 additions & 0 deletions packages/graphql/src/mutation-engine/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export {
GraphQLUnionMutation,
} from "./mutations/index.js";
export { GraphQLMutationOptions, GraphQLTypeContext } from "./options.js";
export { mutateSchema } from "./schema-mutator.js";
export { buildTypeGraph, type TypeGraph } from "./type-graph.js";
70 changes: 70 additions & 0 deletions packages/graphql/src/mutation-engine/schema-mutator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {
isArrayModelType,
navigateTypesInNamespace,
type Enum,
type Model,
type Namespace,
type Operation,
type Program,
type Scalar,
type Type,
type Union,
} from "@typespec/compiler";
import { $ } from "@typespec/compiler/typekit";
import type { TypeUsageResolver } from "../type-usage.js";
import type { GraphQLMutationEngine } from "./engine.js";
import { GraphQLTypeContext } from "./options.js";
import { buildTypeGraph, type TypeGraph } from "./type-graph.js";

/**
* Walk every type in the schema namespace, mutate it through the GraphQL
* mutation engine, and package the results into a TypeGraph.
*
* Filtering (unreachable types, array models, nullable unions) happens here
* so the engine only processes types that belong in the schema.
*/
export function mutateSchema(
program: Program,
engine: GraphQLMutationEngine,
schema: Namespace,
typeUsage: TypeUsageResolver,
): TypeGraph {
const tk = $(program);
const mutatedTypes: Type[] = [];

navigateTypesInNamespace(schema, {
model: (node: Model) => {
if (isArrayModelType(node)) return;
if (typeUsage.isUnreachable(node)) return;

const mutation = engine.mutateModel(node, GraphQLTypeContext.Output);
mutatedTypes.push(mutation.mutatedType);
},
enum: (node: Enum) => {
if (typeUsage.isUnreachable(node)) return;

const mutation = engine.mutateEnum(node);
mutatedTypes.push(mutation.mutatedType);
},
scalar: (node: Scalar) => {
if (typeUsage.isUnreachable(node)) return;
const mutation = engine.mutateScalar(node);
mutatedTypes.push(mutation.mutatedType);
},
union: (node: Union) => {
if (typeUsage.isUnreachable(node)) return;

const mutation = engine.mutateUnion(node, GraphQLTypeContext.Output);
mutatedTypes.push(mutation.mutatedType);
for (const wrapper of mutation.wrapperModels) {
mutatedTypes.push(wrapper);
}
},
operation: (node: Operation) => {
const mutation = engine.mutateOperation(node);
mutatedTypes.push(mutation.mutatedType);
},
});

return buildTypeGraph(program, tk, mutatedTypes);
}
115 changes: 0 additions & 115 deletions packages/graphql/src/registry.ts

This file was deleted.

Loading