Skip to content

Commit 3387127

Browse files
committed
Add operation components, orchestrator, and emitter rendering
Brings the component-based GraphQL emitter online by wiring the data pipeline from the foundation skeleton into Alloy JSX rendering. New components: - components/operations/query-type.tsx, mutation-type.tsx, subscription-type.tsx: render the three GraphQL root operation types - components/operations/index.ts: barrel export - components/type-collections.tsx: orchestrator that dispatches each classified-type bucket (scalars, enums, unions, interfaces, objects, inputs) into the appropriate leaf-type component Emitter wiring: - Rename src/emitter.ts → src/emitter.tsx - Phase 5 now renders via Alloy's renderSchema, converts to SDL with graphql-js printSchema, and writes the output via emitFile - Adds output-file option handling with interpolatePath Testing: - test/e2e.test.ts: single happy-path smoke test proving the full TypeSpec → mutation → classification → rendering → SDL pipeline works - Update the existing test/emitter.test.ts data-pipeline test to now assert that SDL output is produced (Phase 5 is wired up) Broader integration coverage (nullability, input/output splitting, unions, enums, arrays, circular refs, deprecation, doc comments) lands in a follow-up PR focused on tests.
1 parent 41fbefb commit 3387127

8 files changed

Lines changed: 297 additions & 32 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { QueryType, type QueryTypeProps } from "./query-type.js";
2+
export { MutationType, type MutationTypeProps } from "./mutation-type.js";
3+
export { SubscriptionType, type SubscriptionTypeProps } from "./subscription-type.js";
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { type Operation } from "@typespec/compiler";
2+
import * as gql from "@alloy-js/graphql";
3+
import { OperationField } from "../fields/index.js";
4+
5+
export interface MutationTypeProps {
6+
/** Mutation operations to render as fields */
7+
operations: Operation[];
8+
}
9+
10+
/**
11+
* Renders the GraphQL Mutation root type using Alloy's Mutation component
12+
*
13+
* Only renders if operations exist (Mutation is optional in GraphQL)
14+
*/
15+
export function MutationType(props: MutationTypeProps) {
16+
if (props.operations.length === 0) {
17+
return null;
18+
}
19+
20+
return (
21+
<gql.Mutation>
22+
{props.operations.map((op) => (
23+
<OperationField operation={op} />
24+
))}
25+
</gql.Mutation>
26+
);
27+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { type Operation } from "@typespec/compiler";
2+
import * as gql from "@alloy-js/graphql";
3+
import { OperationField } from "../fields/index.js";
4+
5+
export interface QueryTypeProps {
6+
/** Query operations to render as fields */
7+
operations: Operation[];
8+
}
9+
10+
/**
11+
* Renders the GraphQL Query root type using Alloy's Query component.
12+
* Returns null if no query operations exist (the emitter will emit an
13+
* empty-schema diagnostic in that case).
14+
*/
15+
export function QueryType(props: QueryTypeProps) {
16+
if (props.operations.length === 0) {
17+
return null;
18+
}
19+
20+
return (
21+
<gql.Query>
22+
{props.operations.map((op) => (
23+
<OperationField operation={op} />
24+
))}
25+
</gql.Query>
26+
);
27+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { type Operation } from "@typespec/compiler";
2+
import * as gql from "@alloy-js/graphql";
3+
import { OperationField } from "../fields/index.js";
4+
5+
export interface SubscriptionTypeProps {
6+
/** Subscription operations to render as fields */
7+
operations: Operation[];
8+
}
9+
10+
/**
11+
* Renders the GraphQL Subscription root type using Alloy's Subscription component
12+
*
13+
* Only renders if operations exist (Subscription is optional in GraphQL)
14+
*/
15+
export function SubscriptionType(props: SubscriptionTypeProps) {
16+
if (props.operations.length === 0) {
17+
return null;
18+
}
19+
20+
return (
21+
<gql.Subscription>
22+
{props.operations.map((op) => (
23+
<OperationField operation={op} />
24+
))}
25+
</gql.Subscription>
26+
);
27+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { For } from "@alloy-js/core";
2+
import * as gql from "@alloy-js/graphql";
3+
import { useGraphQLSchema } from "../context/index.js";
4+
import {
5+
ScalarType,
6+
EnumType,
7+
UnionType,
8+
InterfaceType,
9+
ObjectType,
10+
InputType,
11+
} from "./types/index.js";
12+
13+
/**
14+
* Renders scalar variant types for encoded scalars (e.g., bytes + base64 -> Bytes)
15+
* AND custom user-defined scalars
16+
*/
17+
export function ScalarVariantTypes() {
18+
const { classifiedTypes } = useGraphQLSchema();
19+
20+
// Get set of variant names to avoid duplicates
21+
const variantNames = new Set(
22+
classifiedTypes.scalarVariants.map((v) => v.graphqlName),
23+
);
24+
25+
// Filter custom scalars to only include ones not already in variants
26+
const customScalars = classifiedTypes.scalars.filter(
27+
(s) => !variantNames.has(s.name),
28+
);
29+
30+
return (
31+
<>
32+
<For each={classifiedTypes.scalarVariants}>
33+
{(variant) => (
34+
<gql.ScalarType
35+
name={variant.graphqlName}
36+
specifiedByUrl={variant.specificationUrl}
37+
/>
38+
)}
39+
</For>
40+
<For each={customScalars}>
41+
{(scalar) => <ScalarType type={scalar} />}
42+
</For>
43+
</>
44+
);
45+
}
46+
47+
/**
48+
* Renders all enum types in the schema
49+
*/
50+
export function EnumTypes() {
51+
const { classifiedTypes } = useGraphQLSchema();
52+
return (
53+
<For each={classifiedTypes.enums}>
54+
{(enumType) => <EnumType type={enumType} />}
55+
</For>
56+
);
57+
}
58+
59+
/**
60+
* Renders all union types in the schema
61+
*/
62+
export function UnionTypes() {
63+
const { classifiedTypes } = useGraphQLSchema();
64+
return (
65+
<For each={classifiedTypes.unions}>
66+
{(union) => <UnionType type={union} />}
67+
</For>
68+
);
69+
}
70+
71+
/**
72+
* Renders all interface types in the schema
73+
*/
74+
export function InterfaceTypes() {
75+
const { classifiedTypes } = useGraphQLSchema();
76+
return (
77+
<For each={classifiedTypes.interfaces}>
78+
{(iface) => <InterfaceType type={iface} />}
79+
</For>
80+
);
81+
}
82+
83+
/**
84+
* Renders all object types in the schema
85+
*/
86+
export function ObjectTypes() {
87+
const { classifiedTypes } = useGraphQLSchema();
88+
return (
89+
<For each={classifiedTypes.outputModels}>
90+
{(model) => <ObjectType type={model} />}
91+
</For>
92+
);
93+
}
94+
95+
/**
96+
* Renders all input types in the schema
97+
*/
98+
export function InputTypes() {
99+
const { classifiedTypes } = useGraphQLSchema();
100+
return (
101+
<For each={classifiedTypes.inputModels}>
102+
{(model) => <InputType type={model} />}
103+
</For>
104+
);
105+
}
Lines changed: 75 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import {
2+
emitFile,
23
getEncode,
4+
interpolatePath,
35
isArrayModelType,
46
isUnknownType,
57
isVoidType,
68
navigateTypesInNamespace,
9+
resolvePath,
710
type EmitContext,
811
type Enum,
912
type Model,
@@ -15,12 +18,23 @@ import {
1518
type Type,
1619
type Union,
1720
} from "@typespec/compiler";
21+
import { renderSchema as renderAlloySchema, printSchema } from "@alloy-js/graphql";
1822
import { isInterface } from "./lib/interface.js";
1923
import { getOperationKind } from "./lib/operation-kind.js";
2024
import { type GraphQLEmitterOptions, reportDiagnostic } from "./lib.js";
2125
import { resolveTypeUsage, GraphQLTypeUsage, type TypeUsageResolver } from "./type-usage.js";
2226
import { listSchemas } from "./lib/schema.js";
2327
import { createGraphQLMutationEngine, GraphQLTypeContext } from "./mutation-engine/index.js";
28+
import { GraphQLSchema } from "./components/graphql-schema.js";
29+
import {
30+
ScalarVariantTypes,
31+
EnumTypes,
32+
UnionTypes,
33+
InterfaceTypes,
34+
ObjectTypes,
35+
InputTypes,
36+
} from "./components/type-collections.js";
37+
import { QueryType, MutationType, SubscriptionType } from "./components/operations/index.js";
2438
import type { ClassifiedTypes, ModelVariants, ScalarVariant } from "./context/index.js";
2539
import { unwrapNullableUnion } from "./lib/type-utils.js";
2640
import { getGraphQLBuiltinName, getScalarMapping } from "./lib/scalar-mappings.js";
@@ -44,8 +58,8 @@ export interface SchemaPipelineResult {
4458
* Main emitter entry point for GraphQL SDL generation.
4559
*
4660
* Runs the full data pipeline (type usage → mutation → classification →
47-
* model variants) and passes the result to `renderSchema`. Component-based
48-
* rendering is a stub in this PR and will be implemented in a follow-up.
61+
* model variants) and passes the result to `renderSchema`, which renders
62+
* via Alloy components and writes the SDL file.
4963
*/
5064
export async function $onEmit(context: EmitContext<GraphQLEmitterOptions>) {
5165
const schemas = listSchemas(context.program);
@@ -56,7 +70,7 @@ export async function $onEmit(context: EmitContext<GraphQLEmitterOptions>) {
5670
for (const schema of schemas) {
5771
const pipelineResult = await emitSchema(context, schema);
5872
if (pipelineResult) {
59-
renderSchema(pipelineResult);
73+
await renderSchema(context, schema, pipelineResult);
6074
}
6175
}
6276
}
@@ -110,22 +124,58 @@ async function emitSchema(
110124
}
111125

112126
/**
113-
* Phase 5: Render the pipeline result to GraphQL SDL.
114-
*
115-
* Stub in this PR — returns an empty string. The component-based Alloy
116-
* renderer that consumes `classifiedTypes`, `modelVariants`, and
117-
* `scalarSpecifications` is implemented in a follow-up PR.
127+
* Phase 5: Render the pipeline result to GraphQL SDL via Alloy components,
128+
* then write the SDL to the emitter output directory.
118129
*/
119-
function renderSchema(_result: SchemaPipelineResult): string {
120-
return "";
121-
}
130+
async function renderSchema(
131+
context: EmitContext<GraphQLEmitterOptions>,
132+
schema: { type: Namespace; name?: string },
133+
result: SchemaPipelineResult,
134+
): Promise<void> {
135+
const { classifiedTypes, modelVariants, scalarSpecifications } = result;
136+
const contextValue = { classifiedTypes, modelVariants, scalarSpecifications };
137+
138+
// Determine output file name
139+
const outputFilePattern = context.options["output-file"] ?? "{schema-name}.graphql";
140+
const schemaName = schema.name || "schema";
141+
const outputFileName = interpolatePath(outputFilePattern, {
142+
"schema-name": schemaName,
143+
});
144+
145+
// Render the schema using Alloy's renderSchema to get a GraphQLSchema object.
146+
// We disable name policy validation because TypeSpec has already validated names and applied mutations.
147+
const graphqlSchema = renderAlloySchema(
148+
<GraphQLSchema program={context.program} contextValue={contextValue}>
149+
<ScalarVariantTypes />
150+
<EnumTypes />
151+
<UnionTypes />
152+
<InterfaceTypes />
153+
<ObjectTypes />
154+
<InputTypes />
155+
<QueryType operations={classifiedTypes.queries} />
156+
<MutationType operations={classifiedTypes.mutations} />
157+
<SubscriptionType operations={classifiedTypes.subscriptions} />
158+
</GraphQLSchema>,
159+
{ namePolicy: null },
160+
);
122161

123-
// ---------------------------------------------------------------------------
124-
// Phase 2: Mutation
125-
// ---------------------------------------------------------------------------
162+
// Convert the GraphQLSchema to SDL string using graphql-js printSchema
163+
const rawSdl = printSchema(graphqlSchema);
164+
165+
// Ensure file ends with blank line (two newlines)
166+
const sdl = rawSdl.trimEnd() + "\n\n";
167+
168+
// Write to file
169+
const outputPath = resolvePath(context.emitterOutputDir, outputFileName);
170+
171+
await emitFile(context.program, {
172+
path: outputPath,
173+
content: sdl,
174+
});
175+
}
126176

127177
/**
128-
* Mutate all types in a schema namespace using the mutation engine.
178+
* Phase 2: Mutate all types using the mutation engine.
129179
* Unreachable enums/unions are skipped based on the type usage resolver.
130180
*/
131181
function mutateTypes(
@@ -214,6 +264,9 @@ function mutateTypes(
214264
union: (node: Union) => {
215265
// Skip nullable unions (e.g., string | null) — they're not union declarations.
216266
// Nullability for these is detected at render time in GraphQLTypeExpression.
267+
// We must NOT mutate them here because replace() would call setNullable()
268+
// on the shared inner type (e.g., the string scalar singleton), poisoning
269+
// all other uses of that type.
217270
if (unwrapNullableUnion(node) !== undefined) {
218271
return;
219272
}
@@ -223,9 +276,10 @@ function mutateTypes(
223276
wrapperModels.push(...mutation.wrapperModels);
224277
},
225278
operation: (node: Operation) => {
226-
// Operations are passed through unmutated. This is load-bearing:
227-
// typeUsage walked operation params/returns on original types to mark input/output.
228-
// classifyTypes reverse-maps mutated models via originalToMutated.
279+
// INVARIANT: Operations are passed through unmutated. This is load-bearing:
280+
// 1. typeUsage walked operation params/returns on original types to mark input/output.
281+
// 2. classifyTypes reverse-maps mutated models to originals via originalToMutated.
282+
// Operations must reference original types for this mapping to work.
229283
mutatedOperations.push(node);
230284
},
231285
});
@@ -256,6 +310,7 @@ function mutateTypes(
256310
}
257311
};
258312

313+
// Collect referenced scalars and scalar variants from model properties and operations.
259314
// Uses original (pre-mutation) models because mutated type refs won't match processedScalars.
260315
const originalModels = Array.from(originalToMutated.keys());
261316
for (const model of originalModels) {
@@ -288,12 +343,8 @@ function mutateTypes(
288343
};
289344
}
290345

291-
// ---------------------------------------------------------------------------
292-
// Phase 3: Classification
293-
// ---------------------------------------------------------------------------
294-
295346
/**
296-
* Classify types into categories (interfaces, output types, input types, operations).
347+
* Phase 3: Classify types into categories (interfaces, output types, input types, operations)
297348
*/
298349
function classifyTypes(
299350
program: Program,
@@ -375,12 +426,8 @@ function classifyTypes(
375426
};
376427
}
377428

378-
// ---------------------------------------------------------------------------
379-
// Phase 4: Model variant lookups
380-
// ---------------------------------------------------------------------------
381-
382429
/**
383-
* Build model variant lookups for checking which variants exist.
430+
* Build model variant lookups for checking which variants exist
384431
*/
385432
function buildModelVariants(classifiedTypes: ClassifiedTypes): ModelVariants {
386433
const modelVariants: ModelVariants = {

0 commit comments

Comments
 (0)