diff --git a/packages/emitter-framework/package.json b/packages/emitter-framework/package.json
index fd8fdfc3368..20833afb5fb 100644
--- a/packages/emitter-framework/package.json
+++ b/packages/emitter-framework/package.json
@@ -34,6 +34,9 @@
"./python": {
"import": "./dist/src/python/index.js"
},
+ "./graphql": {
+ "import": "./dist/src/graphql/index.js"
+ },
"./testing": {
"import": "./dist/src/testing/index.js"
}
@@ -55,6 +58,10 @@
"#python/*": {
"development": "./src/python/*",
"default": "./dist/src/python/*"
+ },
+ "#graphql/*": {
+ "development": "./src/graphql/*",
+ "default": "./dist/src/graphql/*"
}
},
"keywords": [],
@@ -64,6 +71,7 @@
"peerDependencies": {
"@alloy-js/core": "^0.22.0",
"@alloy-js/csharp": "^0.22.0",
+ "@alloy-js/graphql": "^0.1.0",
"@alloy-js/python": "^0.3.0",
"@alloy-js/typescript": "^0.22.0",
"@typespec/compiler": "workspace:^"
@@ -71,6 +79,7 @@
"devDependencies": {
"@alloy-js/cli": "^0.22.0",
"@alloy-js/core": "^0.22.0",
+ "@alloy-js/graphql": "^0.1.0",
"@alloy-js/python": "^0.3.0",
"@alloy-js/rollup-plugin": "^0.1.0",
"@alloy-js/typescript": "^0.22.0",
diff --git a/packages/emitter-framework/src/graphql/components/enum-declaration.tsx b/packages/emitter-framework/src/graphql/components/enum-declaration.tsx
new file mode 100644
index 00000000000..780d91fab20
--- /dev/null
+++ b/packages/emitter-framework/src/graphql/components/enum-declaration.tsx
@@ -0,0 +1,43 @@
+import { For } from "@alloy-js/core";
+import * as gql from "@alloy-js/graphql";
+import type { Enum, Union } from "@typespec/compiler";
+import { useTsp } from "../../core/context/tsp-context.js";
+import { reportDiagnostic } from "../../lib.js";
+
+export interface EnumDeclarationProps {
+ name?: string;
+ type: Union | Enum;
+ doc?: string;
+}
+
+export function EnumDeclaration(props: EnumDeclarationProps) {
+ const { $ } = useTsp();
+ let type: Enum;
+ if ($.union.is(props.type)) {
+ if (!$.union.isValidEnum(props.type)) {
+ throw new Error("The provided union type cannot be represented as an enum");
+ }
+ type = $.enum.createFromUnion(props.type);
+ } else {
+ type = props.type;
+ }
+
+ if (!props.type.name || props.type.name === "") {
+ reportDiagnostic($.program, { code: "type-declaration-missing-name", target: props.type });
+ }
+
+ const name = props.name ?? props.type.name!;
+ const members = Array.from(type.members.entries());
+ const doc = props.doc ?? $.type.getDoc(type) ?? undefined;
+
+ return (
+
+
+ {([_key, value]) => {
+ const memberDoc = $.type.getDoc(value) ?? undefined;
+ return ;
+ }}
+
+
+ );
+}
diff --git a/packages/emitter-framework/src/graphql/components/index.ts b/packages/emitter-framework/src/graphql/components/index.ts
new file mode 100644
index 00000000000..17e3601e394
--- /dev/null
+++ b/packages/emitter-framework/src/graphql/components/index.ts
@@ -0,0 +1,5 @@
+export * from "./enum-declaration.js";
+export * from "./object-type-declaration.js";
+export * from "./type-declaration.js";
+export * from "./type-expression.js";
+export * from "./union-declaration.js";
diff --git a/packages/emitter-framework/src/graphql/components/object-type-declaration.tsx b/packages/emitter-framework/src/graphql/components/object-type-declaration.tsx
new file mode 100644
index 00000000000..7ea2bb19c05
--- /dev/null
+++ b/packages/emitter-framework/src/graphql/components/object-type-declaration.tsx
@@ -0,0 +1,40 @@
+import { For } from "@alloy-js/core";
+import * as gql from "@alloy-js/graphql";
+import type { Model } from "@typespec/compiler";
+import { isNeverType } from "@typespec/compiler";
+import { useTsp } from "../../core/context/tsp-context.js";
+import { getTypeReference } from "./type-expression.js";
+
+export interface ObjectTypeDeclarationProps {
+ name?: string;
+ type: Model;
+ doc?: string;
+}
+
+export function ObjectTypeDeclaration(props: ObjectTypeDeclarationProps) {
+ const { $ } = useTsp();
+ const type = props.type;
+ const name = props.name ?? type.name!;
+ const doc = props.doc ?? $.type.getDoc(type) ?? undefined;
+ const properties = Array.from($.model.getProperties(type).values()).filter(
+ (prop) => !isNeverType(prop.type),
+ );
+
+ return (
+
+
+ {(prop) => {
+ const propDoc = $.type.getDoc(prop) ?? undefined;
+ return (
+
+ );
+ }}
+
+
+ );
+}
diff --git a/packages/emitter-framework/src/graphql/components/type-declaration.tsx b/packages/emitter-framework/src/graphql/components/type-declaration.tsx
new file mode 100644
index 00000000000..da83f1bfd84
--- /dev/null
+++ b/packages/emitter-framework/src/graphql/components/type-declaration.tsx
@@ -0,0 +1,29 @@
+import type { Type } from "@typespec/compiler";
+import { useTsp } from "../../core/context/tsp-context.js";
+import { EnumDeclaration } from "./enum-declaration.js";
+import { ObjectTypeDeclaration } from "./object-type-declaration.js";
+import { UnionDeclaration } from "./union-declaration.js";
+
+export interface TypeDeclarationProps {
+ name?: string;
+ type: Type;
+ doc?: string;
+}
+
+export function TypeDeclaration(props: TypeDeclarationProps) {
+ const { $ } = useTsp();
+ const { type, ...restProps } = props;
+ const doc = props.doc ?? $.type.getDoc(type) ?? undefined;
+
+ switch (type.kind) {
+ case "Model":
+ return ;
+ case "Enum":
+ return ;
+ case "Union":
+ if ($.union.isValidEnum(type)) {
+ return ;
+ }
+ return ;
+ }
+}
diff --git a/packages/emitter-framework/src/graphql/components/type-expression.tsx b/packages/emitter-framework/src/graphql/components/type-expression.tsx
new file mode 100644
index 00000000000..b669c54b43c
--- /dev/null
+++ b/packages/emitter-framework/src/graphql/components/type-expression.tsx
@@ -0,0 +1,172 @@
+import type { IntrinsicType, Scalar, Type } from "@typespec/compiler";
+import type { Typekit } from "@typespec/compiler/typekit";
+import type { TypeReference } from "@alloy-js/graphql";
+import { Experimental_OverridableComponent } from "../../core/components/overrides/component-overrides.jsx";
+import { useTsp } from "../../core/context/tsp-context.js";
+import { reportGraphqlDiagnostic } from "../lib.js";
+
+export interface TypeExpressionProps {
+ type: Type;
+}
+
+export function TypeExpression(props: TypeExpressionProps) {
+ const { $ } = useTsp();
+ const type = props.type;
+
+ return (
+
+ {() => {
+ switch (type.kind) {
+ case "Scalar":
+ case "Intrinsic":
+ return <>{getScalarIntrinsicExpression($, type)}>;
+ case "Model":
+ if ($.array.is(type)) {
+ const elementType = type.indexer!.value;
+ return ;
+ }
+ if ($.record.is(type)) {
+ reportGraphqlDiagnostic($.program, {
+ code: "graphql-unsupported-type",
+ target: type,
+ });
+ return <>String>;
+ }
+ return <>{type.name}>;
+ case "Enum":
+ return <>{type.name}>;
+ case "Union":
+ return <>{type.name}>;
+ case "UnionVariant":
+ return ;
+ case "ModelProperty":
+ return ;
+ default:
+ reportGraphqlDiagnostic($.program, {
+ code: "graphql-unsupported-type",
+ target: type,
+ });
+ return <>String>;
+ }
+ }}
+
+ );
+}
+
+const intrinsicNameToGraphQLType = new Map([
+ // Core types
+ ["string", "String"],
+ ["boolean", "Boolean"],
+ ["null", null], // Not representable in GraphQL
+ ["void", null], // Not representable in GraphQL
+ ["never", null], // Not representable in GraphQL
+ ["unknown", null], // Not representable in GraphQL
+ ["bytes", "String"], // Base64 encoded
+
+ // Numeric types - GraphQL Int is 32-bit signed
+ ["numeric", "Int"], // Abstract parent type
+ ["integer", "Int"], // Abstract parent type
+ ["float", "Float"],
+ ["decimal", "Float"], // No decimal in GraphQL
+ ["decimal128", "Float"], // No decimal in GraphQL
+ ["int64", "String"], // Too large for GraphQL Int
+ ["int32", "Int"],
+ ["int16", "Int"],
+ ["int8", "Int"],
+ ["safeint", "Int"],
+ ["uint64", "String"], // Too large for GraphQL Int
+ ["uint32", "Int"], // Borderline, keep as Int
+ ["uint16", "Int"],
+ ["uint8", "Int"],
+ ["float32", "Float"],
+ ["float64", "Float"],
+
+ // Date and time types - custom scalars could override
+ ["plainDate", "String"],
+ ["plainTime", "String"],
+ ["utcDateTime", "String"],
+ ["offsetDateTime", "String"],
+ ["duration", "String"],
+
+ // String types
+ ["url", "String"],
+]);
+
+function getScalarIntrinsicExpression($: Typekit, type: Scalar | IntrinsicType): string | null {
+ let intrinsicName: string;
+ if ($.scalar.is(type)) {
+ intrinsicName = $.scalar.getStdBase(type)?.name ?? "";
+ } else {
+ intrinsicName = type.name;
+ }
+
+ const gqlType = intrinsicNameToGraphQLType.get(intrinsicName);
+
+ if (gqlType === undefined) {
+ reportGraphqlDiagnostic($.program, { code: "graphql-unsupported-scalar", target: type });
+ return "String";
+ }
+
+ if (gqlType === null) {
+ reportGraphqlDiagnostic($.program, { code: "graphql-unsupported-type", target: type });
+ return null;
+ }
+
+ return gqlType;
+}
+
+/**
+ * Returns a GraphQL TypeReference for use in Field/InputField type props.
+ */
+export function getTypeReference($: Typekit, type: Type): TypeReference {
+ switch (type.kind) {
+ case "Scalar":
+ case "Intrinsic": {
+ const gqlType = getScalarIntrinsicExpression($, type);
+ return gqlType ?? "String";
+ }
+ case "Model":
+ if ($.array.is(type)) {
+ const elementType = type.indexer!.value;
+ return { kind: "list", ofType: getTypeReference($, elementType) };
+ }
+ if ($.record.is(type)) {
+ reportGraphqlDiagnostic($.program, {
+ code: "graphql-unsupported-type",
+ target: type,
+ });
+ return "String";
+ }
+ return type.name!;
+ case "Enum":
+ return type.name!;
+ case "Union":
+ return type.name!;
+ case "UnionVariant":
+ return getTypeReference($, type.type);
+ case "ModelProperty":
+ return getTypeReference($, type.type);
+ default:
+ reportGraphqlDiagnostic($.program, {
+ code: "graphql-unsupported-type",
+ target: type,
+ });
+ return "String";
+ }
+}
+
+export function isDeclaration($: Typekit, type: Type): boolean {
+ switch (type.kind) {
+ case "Model":
+ if ($.array.is(type) || $.record.is(type)) {
+ return false;
+ }
+ return Boolean(type.name);
+ case "Enum":
+ return true;
+ case "Union":
+ return Boolean(type.name);
+ default:
+ return false;
+ }
+}
diff --git a/packages/emitter-framework/src/graphql/components/union-declaration.tsx b/packages/emitter-framework/src/graphql/components/union-declaration.tsx
new file mode 100644
index 00000000000..802537733f1
--- /dev/null
+++ b/packages/emitter-framework/src/graphql/components/union-declaration.tsx
@@ -0,0 +1,41 @@
+import * as gql from "@alloy-js/graphql";
+import type { Union } from "@typespec/compiler";
+import { useTsp } from "../../core/context/tsp-context.js";
+import { reportDiagnostic } from "../../lib.js";
+import { reportGraphqlDiagnostic } from "../lib.js";
+
+export interface UnionDeclarationProps {
+ name?: string;
+ type: Union;
+ doc?: string;
+}
+
+export function UnionDeclaration(props: UnionDeclarationProps) {
+ const { $ } = useTsp();
+ const type = props.type;
+
+ if (!type.name || type.name === "") {
+ reportDiagnostic($.program, { code: "type-declaration-missing-name", target: type });
+ }
+
+ const name = props.name ?? type.name!;
+ const doc = props.doc ?? $.type.getDoc(type) ?? undefined;
+
+ // GraphQL unions can only contain object types. Filter to named model members.
+ const validMembers: string[] = [];
+ for (const variant of type.variants.values()) {
+ const variantType = variant.type;
+ if (variantType.kind === "Model" && variantType.name && !$.array.is(variantType) && !$.record.is(variantType)) {
+ validMembers.push(variantType.name);
+ } else {
+ reportGraphqlDiagnostic($.program, {
+ code: "graphql-unsupported-type",
+ target: variant,
+ });
+ }
+ }
+
+ return (
+
+ );
+}
diff --git a/packages/emitter-framework/src/graphql/index.ts b/packages/emitter-framework/src/graphql/index.ts
new file mode 100644
index 00000000000..be95a933d22
--- /dev/null
+++ b/packages/emitter-framework/src/graphql/index.ts
@@ -0,0 +1,2 @@
+export * from "./components/index.js";
+export * from "./utils/index.js";
diff --git a/packages/emitter-framework/src/graphql/lib.ts b/packages/emitter-framework/src/graphql/lib.ts
new file mode 100644
index 00000000000..138b6e0782b
--- /dev/null
+++ b/packages/emitter-framework/src/graphql/lib.ts
@@ -0,0 +1,25 @@
+import { createTypeSpecLibrary } from "@typespec/compiler";
+
+export const $graphqlLib = createTypeSpecLibrary({
+ name: "emitter-framework",
+ diagnostics: {
+ "graphql-unsupported-scalar": {
+ severity: "warning",
+ messages: {
+ default: "Unsupported scalar type, falling back to String",
+ },
+ },
+ "graphql-unsupported-type": {
+ severity: "error",
+ messages: {
+ default: "Unsupported type, falling back to String",
+ },
+ description: "This type is not supported by the GraphQL emitter",
+ },
+ },
+});
+
+export const {
+ reportDiagnostic: reportGraphqlDiagnostic,
+ createDiagnostic: createGraphqlDiagnostic,
+} = $graphqlLib;
diff --git a/packages/emitter-framework/src/graphql/utils/index.ts b/packages/emitter-framework/src/graphql/utils/index.ts
new file mode 100644
index 00000000000..03310132c9f
--- /dev/null
+++ b/packages/emitter-framework/src/graphql/utils/index.ts
@@ -0,0 +1 @@
+export * from "./refkey.js";
diff --git a/packages/emitter-framework/src/graphql/utils/refkey.ts b/packages/emitter-framework/src/graphql/utils/refkey.ts
new file mode 100644
index 00000000000..0077071bf95
--- /dev/null
+++ b/packages/emitter-framework/src/graphql/utils/refkey.ts
@@ -0,0 +1,36 @@
+import { refkey as ayRefkey, type Refkey } from "@alloy-js/core";
+
+const refKeyPrefix = Symbol.for("emitter-framework:graphql");
+
+/**
+ * A wrapper around `refkey` that uses a custom symbol to avoid collisions with
+ * other libraries that use `refkey`.
+ *
+ * @remarks
+ *
+ * The underlying refkey function is called with the {@link refKeyPrefix} symbol as the first argument.
+ *
+ * @param args The parameters of the refkey.
+ * @returns A refkey object that can be used to identify the value.
+ */
+export function efRefkey(...args: unknown[]): Refkey {
+ if (args.length === 0) {
+ return ayRefkey(); // Generates a unique refkey
+ }
+ return ayRefkey(refKeyPrefix, ...args);
+}
+
+/**
+ * Creates a refkey for a declaration by combining the provided refkey with an internal
+ * refkey generated from the provided arguments.
+ *
+ * @param refkey The refkey provided by the user to be passed as is.
+ * @param args The parameters of the refkey.
+ * @returns An array of refkeys that can be passed to an Alloy declaration.
+ */
+export function declarationRefkeys(refkey?: Refkey | Refkey[], ...args: unknown[]): Refkey[] {
+ if (refkey) {
+ return [refkey, efRefkey(...args)].flat();
+ }
+ return [efRefkey(...args)];
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 42ac27e3349..6b7a49abc2b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -7,10 +7,15 @@ settings:
overrides:
cross-spawn@>=7.0.0 <7.0.5: ^7.0.5
rollup: 4.49.0
+ '@alloy-js/graphql': link:../../Library/pnpm/global/5/node_modules/@alloy-js/graphql
importers:
.:
+ dependencies:
+ '@alloy-js/graphql':
+ specifier: link:../../Library/pnpm/global/5/node_modules/@alloy-js/graphql
+ version: link:../../Library/pnpm/global/5/node_modules/@alloy-js/graphql
devDependencies:
'@chronus/chronus':
specifier: ^1.0.1
@@ -414,6 +419,9 @@ importers:
'@alloy-js/csharp':
specifier: ^0.22.0
version: 0.22.0
+ '@alloy-js/graphql':
+ specifier: link:../../../../Library/pnpm/global/5/node_modules/@alloy-js/graphql
+ version: link:../../../../Library/pnpm/global/5/node_modules/@alloy-js/graphql
devDependencies:
'@alloy-js/cli':
specifier: ^0.22.0
@@ -9086,11 +9094,13 @@ packages:
glob@10.5.0:
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
glob@11.1.0:
resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==}
engines: {node: 20 || >=22}
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
glob@13.0.0:
@@ -9099,12 +9109,12 @@ packages:
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
- deprecated: Glob versions prior to v9 are no longer supported
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
glob@8.1.0:
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
engines: {node: '>=12'}
- deprecated: Glob versions prior to v9 are no longer supported
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
global-directory@4.0.1:
resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==}
@@ -11243,6 +11253,7 @@ packages:
prebuild-install@7.1.3:
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
engines: {node: '>=10'}
+ deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
hasBin: true
preferred-pm@3.1.4:
@@ -12353,11 +12364,12 @@ packages:
tar@6.2.1:
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
engines: {node: '>=10'}
- deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me
+ deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
tar@7.5.4:
resolution: {integrity: sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==}
engines: {node: '>=18'}
+ deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
tau-prolog@0.2.81:
resolution: {integrity: sha512-cHSdGumv+GfRweqE3Okd81+ZH1Ux6PoJ+WPjzoAFVar0SRoUxW93vPvWTbnTtlz++IpSEQ0yUPWlLBcTMQ8uOg==}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index e161307424c..a3207f32cb7 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,16 +1,17 @@
packages:
- - "packages/*"
- - "e2e"
- - "website"
- - "!packages/http-client-csharp/**"
- - "!packages/http-client-java/**"
- - "!packages/http-client-python/**"
+ - packages/*
+ - e2e
+ - website
+ - '!packages/http-client-csharp/**'
+ - '!packages/http-client-java/**'
+ - '!packages/http-client-python/**'
-overrides:
- "cross-spawn@>=7.0.0 <7.0.5": "^7.0.5"
- rollup: 4.49.0 # Regression in 4.50.0 https://github.com/rollup/rollup/issues/6099
+minimumReleaseAge: 2880
-# Minimum age (in minutes) for a new dependency version to be able to be used.
-minimumReleaseAge: 2880 # 2 days
minimumReleaseAgeExclude:
- - "@alloy-js/*"
+ - '@alloy-js/*'
+
+overrides:
+ '@alloy-js/graphql': link:../../Library/pnpm/global/5/node_modules/@alloy-js/graphql
+ cross-spawn@>=7.0.0 <7.0.5: ^7.0.5
+ rollup: 4.49.0