Skip to content

Commit 806adc8

Browse files
committed
Delete legacy emitter files replaced by component architecture
Removes the old procedural emitter implementation now fully replaced by the Alloy JSX component-based emitter: - graphql-emitter.ts (old emitter implementation) - schema-emitter.ts (old schema-specific emitter) - registry.ts (old type registry) - type-maps.ts (old type mapping logic) - emitter.test.ts (old emitter tests) Replaces emitter.ts with a no-op stub so the package still conforms to the TypeSpec emitter interface. The real component-based emitter is added in the next PR in the chain.
1 parent 8b51ca4 commit 806adc8

6 files changed

Lines changed: 340 additions & 422 deletions

File tree

packages/graphql/src/emitter.ts

Lines changed: 340 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,352 @@
1-
import type { EmitContext, NewLine } from "@typespec/compiler";
2-
import { resolvePath } from "@typespec/compiler";
3-
import { createGraphQLEmitter } from "./graphql-emitter.js";
1+
import {
2+
getEncode,
3+
isArrayModelType,
4+
isUnknownType,
5+
navigateTypesInNamespace,
6+
type EmitContext,
7+
type Enum,
8+
type Model,
9+
type ModelProperty,
10+
type Namespace,
11+
type Operation,
12+
type Program,
13+
type Scalar,
14+
type Type,
15+
type Union,
16+
} from "@typespec/compiler";
17+
import { isInterface } from "./lib/interface.js";
18+
import { getOperationKind } from "./lib/operation-kind.js";
419
import type { GraphQLEmitterOptions } from "./lib.js";
20+
import { resolveTypeUsage, GraphQLTypeUsage, type TypeUsageResolver } from "./type-usage.js";
21+
import { listSchemas } from "./lib/schema.js";
22+
import { createGraphQLMutationEngine, GraphQLTypeContext } from "./mutation-engine/index.js";
23+
import type { ClassifiedTypes, ModelVariants, ScalarVariant } from "./context/index.js";
24+
import { unwrapNullableUnion } from "./lib/type-utils.js";
25+
import { getGraphQLBuiltinName, getScalarMapping } from "./lib/scalar-mappings.js";
26+
import { getSpecifiedBy } from "./lib/specified-by.js";
527

6-
const defaultOptions = {
7-
"new-line": "lf",
8-
"omit-unreachable-types": false,
9-
strict: false,
10-
} as const;
11-
28+
/**
29+
* Main emitter entry point for GraphQL SDL generation.
30+
*
31+
* Runs the full data pipeline (type usage → mutation → classification) but
32+
* does not yet render output. Component-based SDL rendering will be added
33+
* in follow-up PRs.
34+
*/
1235
export async function $onEmit(context: EmitContext<GraphQLEmitterOptions>) {
13-
const options = resolveOptions(context);
14-
const emitter = createGraphQLEmitter(context, options);
15-
await emitter.emitGraphQL();
36+
const schemas = listSchemas(context.program);
37+
if (schemas.length === 0) {
38+
schemas.push({ type: context.program.getGlobalNamespaceType() });
39+
}
40+
41+
for (const schema of schemas) {
42+
await emitSchema(context, schema);
43+
}
1644
}
1745

18-
export interface ResolvedGraphQLEmitterOptions {
19-
outputFile: string;
20-
newLine: NewLine;
21-
omitUnreachableTypes: boolean;
22-
strict: boolean;
46+
/**
47+
* Process a single GraphQL schema through the data pipeline.
48+
*/
49+
async function emitSchema(
50+
context: EmitContext<GraphQLEmitterOptions>,
51+
schema: { type: Namespace; name?: string },
52+
) {
53+
// Phase 1: Type usage tracking — determine which types are reachable from operations.
54+
// Must run before mutation so we can filter on original (pre-clone) type objects.
55+
const omitUnreachable = context.options["omit-unreachable-types"] ?? false;
56+
const typeUsage = resolveTypeUsage(schema.type, omitUnreachable);
57+
58+
// Phase 2: Mutation — transform TypeSpec types with GraphQL naming conventions.
59+
// Unreachable enums/unions are skipped during mutation (avoids identity mismatch with cloned objects).
60+
const { mutatedTypes, scalarSpecifications, originalToMutated } = mutateTypes(
61+
context,
62+
schema,
63+
typeUsage,
64+
);
65+
66+
// Phase 3: Classification — separate types by category.
67+
const classifiedTypes = classifyTypes(
68+
context.program,
69+
mutatedTypes,
70+
originalToMutated,
71+
typeUsage,
72+
);
73+
74+
// Phase 4: Build model variant lookups.
75+
const _modelVariants = buildModelVariants(classifiedTypes);
76+
77+
// Phase 5: Component-based SDL rendering.
78+
// TODO: Render using Alloy JSX components (added in follow-up PRs).
79+
void _modelVariants;
80+
void scalarSpecifications;
2381
}
2482

25-
export function resolveOptions(
83+
// ---------------------------------------------------------------------------
84+
// Phase 2: Mutation
85+
// ---------------------------------------------------------------------------
86+
87+
/**
88+
* Mutate all types in a schema namespace using the mutation engine.
89+
* Unreachable enums/unions are skipped based on the type usage resolver.
90+
*/
91+
function mutateTypes(
2692
context: EmitContext<GraphQLEmitterOptions>,
27-
): ResolvedGraphQLEmitterOptions {
28-
const resolvedOptions = { ...defaultOptions, ...context.options };
29-
const outputFile = resolvedOptions["output-file"] ?? "{schema-name}.graphql";
93+
schema: { type: Namespace },
94+
typeUsage: TypeUsageResolver,
95+
) {
96+
const engine = createGraphQLMutationEngine(context.program);
97+
const mutatedModels: Model[] = [];
98+
const mutatedEnums: Enum[] = [];
99+
const mutatedScalars: Scalar[] = [];
100+
const mutatedUnions: Union[] = [];
101+
const mutatedOperations: Operation[] = [];
102+
const wrapperModels: Model[] = [];
103+
const scalarSpecifications = new Map<string, string>();
104+
const originalToMutated = new Map<Model, Model>();
105+
const processedScalars = new Set<string>();
106+
const scalarVariantsMap = new Map<string, ScalarVariant>();
107+
108+
const processScalar = (node: Scalar): void => {
109+
const isDirectGraphQLBuiltin =
110+
context.program.checker.isStdType(node, "string") ||
111+
context.program.checker.isStdType(node, "int32") ||
112+
context.program.checker.isStdType(node, "float32") ||
113+
context.program.checker.isStdType(node, "float64") ||
114+
context.program.checker.isStdType(node, "boolean");
115+
116+
if (isDirectGraphQLBuiltin) return;
117+
118+
const mutation = engine.mutateScalar(node);
119+
const graphqlName = mutation.mutatedType.name;
120+
121+
if (!processedScalars.has(graphqlName)) {
122+
processedScalars.add(graphqlName);
123+
mutatedScalars.push(mutation.mutatedType);
124+
125+
const specUrl = getSpecifiedBy(context.program, mutation.mutatedType);
126+
if (specUrl) {
127+
scalarSpecifications.set(graphqlName, specUrl);
128+
}
129+
}
130+
};
131+
132+
const processScalarVariant = (target: ModelProperty): void => {
133+
if (isUnknownType(target.type)) {
134+
if (!scalarVariantsMap.has("Unknown")) {
135+
scalarVariantsMap.set("Unknown", {
136+
sourceScalar: target.type,
137+
encoding: "default",
138+
graphqlName: "Unknown",
139+
specificationUrl: undefined,
140+
});
141+
}
142+
return;
143+
}
144+
if (
145+
target.type.kind === "Scalar" &&
146+
context.program.checker.isStdType(target.type) &&
147+
!getGraphQLBuiltinName(context.program, target.type)
148+
) {
149+
const encodeData = getEncode(context.program, target);
150+
const encoding = encodeData?.encoding;
151+
const mapping = getScalarMapping(context.program, target.type, encoding);
152+
if (mapping && !scalarVariantsMap.has(mapping.graphqlName)) {
153+
scalarVariantsMap.set(mapping.graphqlName, {
154+
sourceScalar: target.type,
155+
encoding: encoding || "default",
156+
graphqlName: mapping.graphqlName,
157+
specificationUrl: mapping.specificationUrl,
158+
});
159+
}
160+
}
161+
};
162+
163+
navigateTypesInNamespace(schema.type, {
164+
model: (node: Model) => {
165+
if (isArrayModelType(context.program, node)) return;
166+
const mutation = engine.mutateModel(node, GraphQLTypeContext.Output);
167+
mutatedModels.push(mutation.mutatedType);
168+
originalToMutated.set(node, mutation.mutatedType);
169+
},
170+
enum: (node: Enum) => {
171+
if (typeUsage.isUnreachable(node)) return;
172+
const mutation = engine.mutateEnum(node);
173+
mutatedEnums.push(mutation.mutatedType);
174+
},
175+
scalar: (node: Scalar) => {
176+
processScalar(node);
177+
},
178+
union: (node: Union) => {
179+
// Skip nullable unions (e.g., string | null) — they're not union declarations.
180+
// Nullability for these is detected at render time in GraphQLTypeExpression.
181+
if (unwrapNullableUnion(node) !== undefined) {
182+
return;
183+
}
184+
if (typeUsage.isUnreachable(node)) return;
185+
const mutation = engine.mutateUnion(node, GraphQLTypeContext.Output);
186+
mutatedUnions.push(mutation.mutatedType as Union);
187+
wrapperModels.push(...mutation.wrapperModels);
188+
},
189+
operation: (node: Operation) => {
190+
// Operations are passed through unmutated. This is load-bearing:
191+
// typeUsage walked operation params/returns on original types to mark input/output.
192+
// classifyTypes reverse-maps mutated models via originalToMutated.
193+
mutatedOperations.push(node);
194+
},
195+
});
196+
197+
// Collect referenced scalars from model properties and operations.
198+
// Standard library scalars like int64, utcDateTime are not declared in the schema,
199+
// but are referenced in model properties — we need to collect and mutate them.
200+
const visitedTypes = new Set<Type>();
201+
202+
const collectReferencedScalars = (type: Type): void => {
203+
if (visitedTypes.has(type)) return;
204+
visitedTypes.add(type);
205+
206+
if (type.kind === "Scalar") {
207+
processScalar(type);
208+
} else if (type.kind === "Model" && isArrayModelType(context.program, type)) {
209+
if (type.indexer?.value) {
210+
collectReferencedScalars(type.indexer.value);
211+
}
212+
} else if (type.kind === "Model") {
213+
for (const prop of type.properties.values()) {
214+
collectReferencedScalars(prop.type);
215+
}
216+
} else if (type.kind === "Union") {
217+
for (const variant of type.variants.values()) {
218+
collectReferencedScalars(variant.type);
219+
}
220+
}
221+
};
222+
223+
// Uses original (pre-mutation) models because mutated type refs won't match processedScalars.
224+
const originalModels = Array.from(originalToMutated.keys());
225+
for (const model of originalModels) {
226+
for (const prop of model.properties.values()) {
227+
collectReferencedScalars(prop.type);
228+
processScalarVariant(prop);
229+
}
230+
}
231+
232+
for (const op of mutatedOperations) {
233+
for (const param of op.parameters.properties.values()) {
234+
collectReferencedScalars(param.type);
235+
processScalarVariant(param);
236+
}
237+
collectReferencedScalars(op.returnType);
238+
}
239+
240+
return {
241+
mutatedTypes: {
242+
models: mutatedModels,
243+
enums: mutatedEnums,
244+
scalars: mutatedScalars,
245+
unions: mutatedUnions,
246+
operations: mutatedOperations,
247+
wrapperModels,
248+
scalarVariants: Array.from(scalarVariantsMap.values()),
249+
},
250+
scalarSpecifications,
251+
originalToMutated,
252+
};
253+
}
254+
255+
// ---------------------------------------------------------------------------
256+
// Phase 3: Classification
257+
// ---------------------------------------------------------------------------
258+
259+
/**
260+
* Classify types into categories (interfaces, output types, input types, operations).
261+
*/
262+
function classifyTypes(
263+
program: Program,
264+
mutatedTypes: ReturnType<typeof mutateTypes>["mutatedTypes"],
265+
originalToMutated: Map<Model, Model>,
266+
typeUsage: TypeUsageResolver,
267+
): ClassifiedTypes {
268+
const interfaces: Model[] = [];
269+
const outputModels: Model[] = [];
270+
const inputModels: Model[] = [];
271+
const queries: Operation[] = [];
272+
const mutations: Operation[] = [];
273+
const subscriptions: Operation[] = [];
274+
275+
// Create reverse mapping
276+
const mutatedToOriginal = new Map<Model, Model>();
277+
for (const [orig, mut] of originalToMutated) {
278+
mutatedToOriginal.set(mut, orig);
279+
}
280+
281+
for (const model of mutatedTypes.models) {
282+
const originalModel = mutatedToOriginal.get(model) || model;
283+
if (typeUsage.isUnreachable(originalModel)) {
284+
continue;
285+
}
286+
287+
// Check @Interface on the original (pre-clone) model, since decorator state
288+
// is stored against original type identity, not mutated clones.
289+
if (isInterface(program, originalModel)) {
290+
interfaces.push(model);
291+
} else {
292+
const usage = typeUsage.getUsage(originalModel);
293+
const usedAsInput = usage?.has(GraphQLTypeUsage.Input) ?? false;
294+
const usedAsOutput = usage?.has(GraphQLTypeUsage.Output) ?? false;
295+
296+
if (!usedAsInput && !usedAsOutput) {
297+
// Reachable but not referenced by any operation — default to Output
298+
outputModels.push(model);
299+
} else {
300+
if (usedAsOutput) outputModels.push(model);
301+
if (usedAsInput) inputModels.push(model);
302+
}
303+
}
304+
}
305+
306+
// Add wrapper models created by union mutations (always used as output)
307+
outputModels.push(...mutatedTypes.wrapperModels);
308+
309+
// Classify operations by kind
310+
for (const op of mutatedTypes.operations) {
311+
const kind = getOperationKind(program, op);
312+
if (kind === "Query") queries.push(op);
313+
else if (kind === "Mutation") mutations.push(op);
314+
else if (kind === "Subscription") subscriptions.push(op);
315+
}
30316

31317
return {
32-
outputFile: resolvePath(context.emitterOutputDir, outputFile),
33-
newLine: resolvedOptions["new-line"],
34-
omitUnreachableTypes: resolvedOptions["omit-unreachable-types"],
35-
strict: resolvedOptions["strict"],
318+
interfaces,
319+
outputModels,
320+
inputModels,
321+
enums: mutatedTypes.enums,
322+
scalars: mutatedTypes.scalars,
323+
scalarVariants: mutatedTypes.scalarVariants,
324+
unions: mutatedTypes.unions,
325+
queries,
326+
mutations,
327+
subscriptions,
328+
};
329+
}
330+
331+
// ---------------------------------------------------------------------------
332+
// Phase 4: Model variant lookups
333+
// ---------------------------------------------------------------------------
334+
335+
/**
336+
* Build model variant lookups for checking which variants exist.
337+
*/
338+
function buildModelVariants(classifiedTypes: ClassifiedTypes): ModelVariants {
339+
const modelVariants: ModelVariants = {
340+
outputModels: new Map(),
341+
inputModels: new Map(),
36342
};
343+
344+
classifiedTypes.outputModels.forEach((m) =>
345+
modelVariants.outputModels.set(m.name, m),
346+
);
347+
classifiedTypes.inputModels.forEach((m) =>
348+
modelVariants.inputModels.set(m.name, m),
349+
);
350+
351+
return modelVariants;
37352
}

0 commit comments

Comments
 (0)