blume 1.5.2 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/dist/cli/index.js +3639 -1377
  3. package/dist/cli/index.js.map +103 -91
  4. package/dist/types/ai/component-markdown.d.ts +79 -0
  5. package/dist/types/components/layout/nav-utils.d.ts +60 -0
  6. package/dist/types/core/base-path.d.ts +9 -0
  7. package/dist/types/core/config-input.d.ts +206 -4
  8. package/dist/types/core/config.d.ts +6 -4
  9. package/dist/types/core/data.d.ts +23 -1
  10. package/dist/types/core/github.d.ts +35 -0
  11. package/dist/types/core/i18n-ui.d.ts +8 -0
  12. package/dist/types/core/navigation.d.ts +69 -0
  13. package/dist/types/core/schema.d.ts +117 -1
  14. package/dist/types/core/sources/types.d.ts +31 -6
  15. package/dist/types/core/types.d.ts +23 -2
  16. package/dist/types/markdown/features.d.ts +21 -0
  17. package/dist/types/openapi/references.d.ts +21 -1
  18. package/dist/types/seo/jsonld.d.ts +105 -0
  19. package/dist/types/theme/fonts.d.ts +34 -4
  20. package/docs/_snippets/include-demo.mdx +7 -0
  21. package/docs/advanced/api-reference.mdx +3 -3
  22. package/docs/advanced/custom-pages.mdx +1 -1
  23. package/docs/advanced/graphql.mdx +84 -0
  24. package/docs/advanced/meta.ts +8 -1
  25. package/docs/configuration/ai.mdx +21 -3
  26. package/docs/configuration/index.mdx +24 -0
  27. package/docs/configuration/search.mdx +13 -1
  28. package/docs/configuration/seo.mdx +27 -0
  29. package/docs/configuration/theming.mdx +17 -0
  30. package/docs/content/components.mdx +7 -0
  31. package/docs/content/includes.mdx +68 -0
  32. package/docs/content/meta.ts +1 -0
  33. package/docs/content/navigation.mdx +25 -0
  34. package/docs/content/sources.mdx +42 -1
  35. package/docs/content/syntax.mdx +69 -1
  36. package/docs/content/versioning.mdx +15 -9
  37. package/docs/reference/cli.mdx +2 -1
  38. package/package.json +23 -14
  39. package/skills/blume-migrate/SKILL.md +16 -7
  40. package/skills/blume-migrate/references/docusaurus.md +5 -3
  41. package/skills/blume-migrate/references/fumadocs.md +10 -2
  42. package/skills/blume-migrate/references/mintlify.md +3 -2
  43. package/skills/blume-migrate/references/nextra.md +2 -2
  44. package/skills/blume-migrate/references/starlight.md +1 -1
  45. package/src/ai/agent-readability.ts +2 -1
  46. package/src/ai/ask-data.ts +2 -1
  47. package/src/ai/component-markdown.ts +199 -36
  48. package/src/ai/llms.ts +93 -6
  49. package/src/ai/markdown.ts +2 -2
  50. package/src/ai/mcp/discovery.ts +10 -2
  51. package/src/ai/mcp/server.ts +74 -2
  52. package/src/astro/generate.ts +183 -116
  53. package/src/astro/include-hmr.ts +81 -0
  54. package/src/astro/include-refresh.ts +0 -0
  55. package/src/astro/index.ts +3 -5
  56. package/src/astro/templates.ts +125 -76
  57. package/src/cli/commands/build.ts +84 -15
  58. package/src/cli/init/questions.ts +1 -0
  59. package/src/cli/init/scaffold.ts +27 -4
  60. package/src/components/colors.ts +142 -0
  61. package/src/components/content/Badge.astro +5 -12
  62. package/src/components/content/Callout.astro +19 -36
  63. package/src/components/content/Card.astro +15 -21
  64. package/src/components/content/Component.astro +10 -1
  65. package/src/components/content/GithubInfo.astro +28 -9
  66. package/src/components/content/Tabs.astro +27 -5
  67. package/src/components/content/github-info.ts +20 -5
  68. package/src/components/dropdown-dismiss.ts +122 -0
  69. package/src/components/layout/Fonts.astro +15 -8
  70. package/src/components/layout/Header.astro +44 -0
  71. package/src/components/layout/LanguageSwitcher.astro +9 -1
  72. package/src/components/layout/NavSelector.astro +12 -3
  73. package/src/components/layout/NavTree.astro +6 -18
  74. package/src/components/layout/PageActions.astro +29 -8
  75. package/src/components/layout/PageLayout.astro +10 -1
  76. package/src/components/layout/ReferenceLayout.astro +6 -1
  77. package/src/components/layout/RootLayout.astro +46 -15
  78. package/src/components/layout/Search.astro +36 -4
  79. package/src/components/layout/TableOfContents.astro +8 -2
  80. package/src/components/layout/head-scripts.ts +53 -1
  81. package/src/components/openapi/ApiOverview.astro +13 -3
  82. package/src/components/openapi/AsyncApiOperation.astro +7 -14
  83. package/src/components/openapi/GraphqlChip.astro +33 -0
  84. package/src/components/openapi/GraphqlFieldsTable.astro +111 -0
  85. package/src/components/openapi/GraphqlOperation.astro +186 -0
  86. package/src/components/openapi/GraphqlType.astro +154 -0
  87. package/src/components/openapi/MethodBadge.astro +3 -14
  88. package/src/components/openapi/Operation.astro +12 -5
  89. package/src/components/openapi/OperationPanel.astro +43 -0
  90. package/src/components/openapi/RequestPanel.astro +5 -10
  91. package/src/components/openapi/Responses.astro +1 -16
  92. package/src/components/openapi/graphql-helpers.ts +466 -0
  93. package/src/components/openapi/playground-client.ts +15 -0
  94. package/src/components/openapi/sample-panels.ts +45 -0
  95. package/src/components/openapi/snippets.ts +13 -35
  96. package/src/core/base-path.ts +11 -0
  97. package/src/core/config-input.ts +209 -2
  98. package/src/core/config.ts +6 -4
  99. package/src/core/content-assets.ts +15 -4
  100. package/src/core/data.ts +18 -2
  101. package/src/core/diagnostics.ts +8 -0
  102. package/src/core/frontmatter.ts +20 -8
  103. package/src/core/github.ts +71 -0
  104. package/src/core/graph.ts +22 -8
  105. package/src/core/heading-markers.ts +96 -0
  106. package/src/core/i18n-ui.ts +11 -0
  107. package/src/core/includes.ts +632 -0
  108. package/src/core/last-modified.ts +36 -11
  109. package/src/core/links.ts +79 -13
  110. package/src/core/meta.ts +2 -1
  111. package/src/core/nav-diagnostics.ts +11 -2
  112. package/src/core/navigation.ts +27 -6
  113. package/src/core/project-graph.ts +61 -9
  114. package/src/core/schema.ts +226 -35
  115. package/src/core/server-features.ts +5 -9
  116. package/src/core/sources/github-releases.ts +2 -2
  117. package/src/core/sources/normalize.ts +502 -115
  118. package/src/core/sources/notion.ts +43 -8
  119. package/src/core/sources/obsidian.ts +1038 -0
  120. package/src/core/sources/read.ts +36 -1
  121. package/src/core/sources/resolve.ts +34 -1
  122. package/src/core/sources/types.ts +28 -6
  123. package/src/core/sources/watch.ts +12 -8
  124. package/src/core/tsconfig-aliases.ts +48 -35
  125. package/src/core/types.ts +25 -2
  126. package/src/core/ui-packs/ar.ts +1 -0
  127. package/src/core/ui-packs/bg.ts +2 -0
  128. package/src/core/ui-packs/bn.ts +1 -0
  129. package/src/core/ui-packs/ca.ts +2 -0
  130. package/src/core/ui-packs/cs.ts +1 -0
  131. package/src/core/ui-packs/da.ts +1 -0
  132. package/src/core/ui-packs/de.ts +2 -0
  133. package/src/core/ui-packs/el.ts +2 -0
  134. package/src/core/ui-packs/es.ts +2 -0
  135. package/src/core/ui-packs/fa.ts +1 -0
  136. package/src/core/ui-packs/fi.ts +1 -0
  137. package/src/core/ui-packs/fr.ts +2 -0
  138. package/src/core/ui-packs/he.ts +1 -0
  139. package/src/core/ui-packs/hi.ts +1 -0
  140. package/src/core/ui-packs/hr.ts +2 -0
  141. package/src/core/ui-packs/hu.ts +2 -0
  142. package/src/core/ui-packs/id.ts +2 -0
  143. package/src/core/ui-packs/it.ts +1 -0
  144. package/src/core/ui-packs/ja.ts +2 -0
  145. package/src/core/ui-packs/ko.ts +2 -0
  146. package/src/core/ui-packs/nl.ts +2 -0
  147. package/src/core/ui-packs/no.ts +2 -0
  148. package/src/core/ui-packs/pl.ts +2 -0
  149. package/src/core/ui-packs/pt-br.ts +2 -0
  150. package/src/core/ui-packs/pt.ts +2 -0
  151. package/src/core/ui-packs/ro.ts +2 -0
  152. package/src/core/ui-packs/ru.ts +2 -0
  153. package/src/core/ui-packs/sk.ts +1 -0
  154. package/src/core/ui-packs/sr.ts +1 -0
  155. package/src/core/ui-packs/sv.ts +2 -0
  156. package/src/core/ui-packs/th.ts +1 -0
  157. package/src/core/ui-packs/tr.ts +2 -0
  158. package/src/core/ui-packs/uk.ts +2 -0
  159. package/src/core/ui-packs/vi.ts +1 -0
  160. package/src/core/ui-packs/zh-tw.ts +1 -0
  161. package/src/core/ui-packs/zh.ts +1 -0
  162. package/src/core/version-cut.ts +21 -3
  163. package/src/core/yaml.ts +26 -0
  164. package/src/deploy/function-bundle.ts +251 -0
  165. package/src/eval/schema.ts +3 -1
  166. package/src/markdown/code-title.ts +22 -16
  167. package/src/markdown/features.ts +21 -0
  168. package/src/markdown/fence-meta.ts +50 -0
  169. package/src/markdown/heading-anchors.ts +198 -37
  170. package/src/markdown/include.ts +247 -0
  171. package/src/markdown/index.ts +43 -34
  172. package/src/markdown/language-icon.ts +2 -2
  173. package/src/markdown/mdast.ts +7 -3
  174. package/src/markdown/ts2js.ts +264 -0
  175. package/src/openapi/asyncapi.ts +4 -1
  176. package/src/openapi/graphql-build.ts +293 -0
  177. package/src/openapi/graphql.ts +212 -0
  178. package/src/openapi/model.ts +38 -5
  179. package/src/openapi/parse.ts +34 -0
  180. package/src/openapi/proxy.ts +30 -5
  181. package/src/openapi/references.ts +89 -13
  182. package/src/openapi/render-mdx.ts +48 -8
  183. package/src/openapi/scalar.ts +5 -12
  184. package/src/openapi/source.ts +91 -23
  185. package/src/registry/eject.ts +11 -0
  186. package/src/search/documents.ts +229 -37
  187. package/src/search/orama-index.ts +9 -5
  188. package/src/seo/jsonld.ts +293 -51
  189. package/src/theme/code-block-padding.ts +16 -0
  190. package/src/theme/entry.ts +65 -11
  191. package/src/theme/fonts.ts +189 -16
  192. package/src/translate/prompts.ts +2 -0
  193. package/src/translate/run.ts +7 -0
  194. package/src/translate/work-list.ts +0 -0
@@ -0,0 +1,293 @@
1
+ import type {
2
+ GraphQLArgument,
3
+ GraphQLEnumType,
4
+ GraphQLField,
5
+ GraphQLInputField,
6
+ GraphQLInputObjectType,
7
+ GraphQLInterfaceType,
8
+ GraphQLNamedType,
9
+ GraphQLObjectType,
10
+ GraphQLScalarType,
11
+ GraphQLSchema,
12
+ GraphQLUnionType,
13
+ } from "graphql";
14
+ import {
15
+ buildClientSchema,
16
+ buildSchema,
17
+ getNamedType,
18
+ isEnumType,
19
+ isInputObjectType,
20
+ isInterfaceType,
21
+ isObjectType,
22
+ isSpecifiedScalarType,
23
+ isUnionType,
24
+ print,
25
+ } from "graphql";
26
+
27
+ import type {
28
+ GraphqlDocument,
29
+ GraphqlEnumValue,
30
+ GraphqlField as ModelField,
31
+ GraphqlInputValue,
32
+ GraphqlTypeDef,
33
+ GraphqlTypeRef,
34
+ } from "./graphql.ts";
35
+
36
+ /**
37
+ * Lower schema text — SDL or an introspection result — into the serializable
38
+ * {@link GraphqlDocument} the reference renders from. This is the only module
39
+ * that imports `graphql`-js, and it runs Node-side at parse time only (see
40
+ * `parseGraphqlSpec` in `parse.ts`), so the parser never reaches the generated
41
+ * runtime or the browser. Throws plain Errors for unreadable schemas; the
42
+ * caller wraps them as content problems.
43
+ */
44
+
45
+ /** A type expression flattened to its display form and core named type. */
46
+ const typeRef = (
47
+ type: GraphQLField<unknown, unknown>["type"] | GraphQLInputField["type"]
48
+ ): GraphqlTypeRef => ({
49
+ display: String(type),
50
+ name: getNamedType(type).name,
51
+ });
52
+
53
+ /**
54
+ * An argument's or input field's default, printed as a GraphQL literal. Both
55
+ * SDL- and introspection-built schemas carry the default as a const AST node
56
+ * (`default.literal`); a schema constructed programmatically with a coerced
57
+ * `value` instead never reaches this module (it only builds from text).
58
+ */
59
+ const defaultLiteral = (
60
+ input: GraphQLArgument | GraphQLInputField
61
+ ): string | undefined => {
62
+ const literal = input.default?.literal;
63
+ return literal ? print(literal) : undefined;
64
+ };
65
+
66
+ const inputValue = (
67
+ input: GraphQLArgument | GraphQLInputField
68
+ ): GraphqlInputValue => {
69
+ const value: GraphqlInputValue = {
70
+ description: input.description ?? "",
71
+ name: input.name,
72
+ type: typeRef(input.type),
73
+ };
74
+ const printed = defaultLiteral(input);
75
+ if (printed !== undefined) {
76
+ value.default = printed;
77
+ }
78
+ // `deprecationReason` is null on non-deprecated members in some builds;
79
+ // absent means "not deprecated" downstream, so both collapse to absent.
80
+ if (
81
+ input.deprecationReason !== null &&
82
+ input.deprecationReason !== undefined
83
+ ) {
84
+ value.deprecationReason = input.deprecationReason;
85
+ }
86
+ return value;
87
+ };
88
+
89
+ const outputField = (field: GraphQLField<unknown, unknown>): ModelField => {
90
+ const value: ModelField = {
91
+ args: field.args.map(inputValue),
92
+ description: field.description ?? "",
93
+ name: field.name,
94
+ type: typeRef(field.type),
95
+ };
96
+ if (
97
+ field.deprecationReason !== null &&
98
+ field.deprecationReason !== undefined
99
+ ) {
100
+ value.deprecationReason = field.deprecationReason;
101
+ }
102
+ return value;
103
+ };
104
+
105
+ const enumValues = (type: GraphQLEnumType): GraphqlEnumValue[] =>
106
+ type.getValues().map((value) => {
107
+ const member: GraphqlEnumValue = {
108
+ description: value.description ?? "",
109
+ name: value.name,
110
+ };
111
+ if (
112
+ value.deprecationReason !== null &&
113
+ value.deprecationReason !== undefined
114
+ ) {
115
+ member.deprecationReason = value.deprecationReason;
116
+ }
117
+ return member;
118
+ });
119
+
120
+ const objectDef = (
121
+ type: GraphQLObjectType | GraphQLInterfaceType,
122
+ kind: "object" | "interface"
123
+ ): GraphqlTypeDef => {
124
+ const interfaces = type.getInterfaces().map((iface) => iface.name);
125
+ const def: GraphqlTypeDef = {
126
+ description: type.description ?? "",
127
+ fields: Object.values(type.getFields()).map(outputField),
128
+ kind,
129
+ name: type.name,
130
+ };
131
+ if (interfaces.length > 0) {
132
+ def.interfaces = interfaces;
133
+ }
134
+ return def;
135
+ };
136
+
137
+ const inputDef = (type: GraphQLInputObjectType): GraphqlTypeDef => ({
138
+ description: type.description ?? "",
139
+ inputFields: Object.values(type.getFields()).map(inputValue),
140
+ kind: "input",
141
+ name: type.name,
142
+ });
143
+
144
+ const unionDef = (type: GraphQLUnionType): GraphqlTypeDef => ({
145
+ description: type.description ?? "",
146
+ kind: "union",
147
+ name: type.name,
148
+ possibleTypes: type.getTypes().map((member) => member.name),
149
+ });
150
+
151
+ const scalarDef = (type: GraphQLScalarType): GraphqlTypeDef => {
152
+ const def: GraphqlTypeDef = {
153
+ description: type.description ?? "",
154
+ kind: "scalar",
155
+ name: type.name,
156
+ };
157
+ if (type.specifiedByURL !== null && type.specifiedByURL !== undefined) {
158
+ def.specifiedByUrl = type.specifiedByURL;
159
+ }
160
+ if (isSpecifiedScalarType(type)) {
161
+ def.builtIn = true;
162
+ }
163
+ return def;
164
+ };
165
+
166
+ const typeDef = (
167
+ type: GraphQLNamedType,
168
+ schema: GraphQLSchema
169
+ ): GraphqlTypeDef => {
170
+ if (isObjectType(type)) {
171
+ return objectDef(type, "object");
172
+ }
173
+ if (isInterfaceType(type)) {
174
+ const def = objectDef(type, "interface");
175
+ // Implementations are indexed on the schema, not the type — captured here
176
+ // so the type page can list "implemented by" without a schema in hand.
177
+ const implementers = schema
178
+ .getImplementations(type)
179
+ .objects.map((object) => object.name);
180
+ if (implementers.length > 0) {
181
+ def.possibleTypes = implementers;
182
+ }
183
+ return def;
184
+ }
185
+ if (isInputObjectType(type)) {
186
+ return inputDef(type);
187
+ }
188
+ if (isEnumType(type)) {
189
+ return {
190
+ description: type.description ?? "",
191
+ enumValues: enumValues(type),
192
+ kind: "enum",
193
+ name: type.name,
194
+ };
195
+ }
196
+ if (isUnionType(type)) {
197
+ return unionDef(type);
198
+ }
199
+ // SAFETY: the named-type union is exhaustive — after the object, interface,
200
+ // input, enum, and union guards, only scalars remain.
201
+ return scalarDef(type as GraphQLScalarType);
202
+ };
203
+
204
+ /** Lower a built schema to the serializable document. */
205
+ const documentOf = (schema: GraphQLSchema): GraphqlDocument => {
206
+ const types: Record<string, GraphqlTypeDef> = {};
207
+ for (const [name, type] of Object.entries(schema.getTypeMap())) {
208
+ // Introspection meta-types (`__Schema`, `__Type`, …) are machinery, not API.
209
+ if (name.startsWith("__")) {
210
+ continue;
211
+ }
212
+ types[name] = typeDef(type, schema);
213
+ }
214
+ const roots: GraphqlDocument["roots"] = {};
215
+ const query = schema.getQueryType();
216
+ const mutation = schema.getMutationType();
217
+ const subscription = schema.getSubscriptionType();
218
+ if (query) {
219
+ roots.query = query.name;
220
+ }
221
+ if (mutation) {
222
+ roots.mutation = mutation.name;
223
+ }
224
+ if (subscription) {
225
+ roots.subscription = subscription.name;
226
+ }
227
+ return {
228
+ info: { description: schema.description ?? "" },
229
+ roots,
230
+ types,
231
+ };
232
+ };
233
+
234
+ /**
235
+ * A parsed JSON value carrying an introspection result's `__schema` key —
236
+ * possibly under the standard `{ data: … }` GraphQL response envelope. The
237
+ * generic-guard shape mirrors the document checks in `parse.ts`; the real
238
+ * validation is `buildClientSchema`'s, which throws a descriptive error on
239
+ * anything malformed inside `__schema`.
240
+ */
241
+ const hasSchemaKey = <Value>(
242
+ value: Value
243
+ ): value is Value & { __schema: object } =>
244
+ typeof value === "object" && value !== null && "__schema" in value;
245
+
246
+ /** A parsed JSON value shaped like a GraphQL response envelope. */
247
+ const hasDataEnvelope = <Value>(
248
+ value: Value
249
+ ): value is Value & { data: unknown } =>
250
+ typeof value === "object" && value !== null && "data" in value;
251
+
252
+ /** The introspection result inside a parsed JSON value, if it carries one. */
253
+ const introspectionOf = <Value>(parsed: Value): object | undefined => {
254
+ if (hasSchemaKey(parsed)) {
255
+ return parsed;
256
+ }
257
+ return hasDataEnvelope(parsed) && hasSchemaKey(parsed.data)
258
+ ? parsed.data
259
+ : undefined;
260
+ };
261
+
262
+ /**
263
+ * Build the document from schema text: an introspection JSON result (the raw
264
+ * `{ __schema }` shape or a `{ data: { __schema } }` response envelope) or
265
+ * SDL. Anything else throws — JSON that isn't an introspection result names
266
+ * that problem directly instead of earning SDL syntax errors for a `{`.
267
+ */
268
+ export const buildGraphqlDocument = (text: string): GraphqlDocument => {
269
+ let parsed: unknown;
270
+ let isJson = true;
271
+ try {
272
+ parsed = JSON.parse(text);
273
+ } catch {
274
+ isJson = false;
275
+ }
276
+ if (isJson) {
277
+ const introspection = introspectionOf(parsed);
278
+ if (!introspection) {
279
+ throw new Error(
280
+ "the JSON document is not a GraphQL introspection result (no `__schema` key)."
281
+ );
282
+ }
283
+ // SAFETY: the guard above only establishes the `__schema` key;
284
+ // `buildClientSchema` validates the full introspection shape itself and
285
+ // throws a descriptive error on anything malformed.
286
+ return documentOf(
287
+ buildClientSchema(
288
+ introspection as Parameters<typeof buildClientSchema>[0]
289
+ )
290
+ );
291
+ }
292
+ return documentOf(buildSchema(text));
293
+ };
@@ -0,0 +1,212 @@
1
+ import type { ApiOperationRef, ExtractedOperations } from "./model.ts";
2
+ import { operationCollector, operationKey } from "./model.ts";
3
+
4
+ /**
5
+ * Blume's own GraphQL model — the third front-end of the API reference
6
+ * pipeline. Schemas are lowered to this JSON-serializable document at parse
7
+ * time (see `buildGraphqlDocument` in `graphql-build.ts`, kept separate so
8
+ * `graphql`-js stays out of this module's import graph), so the components
9
+ * render entirely from plain data, mirroring how `model.ts` keeps OpenAPI
10
+ * documents serializable. Browser-safe: no Node imports (the components
11
+ * import from here).
12
+ */
13
+
14
+ /** The three GraphQL operation kinds, each a sidebar group of root fields. */
15
+ export const GRAPHQL_OPERATION_KINDS = [
16
+ "query",
17
+ "mutation",
18
+ "subscription",
19
+ ] as const;
20
+
21
+ export type GraphqlOperationKind = (typeof GRAPHQL_OPERATION_KINDS)[number];
22
+
23
+ /** The named-type kinds that get reference pages, in sidebar-group order. */
24
+ export const GRAPHQL_TYPE_KINDS = [
25
+ "object",
26
+ "input",
27
+ "enum",
28
+ "interface",
29
+ "union",
30
+ "scalar",
31
+ ] as const;
32
+
33
+ export type GraphqlTypeKind = (typeof GRAPHQL_TYPE_KINDS)[number];
34
+
35
+ /** Everything a GraphQL operation ref's `method` can carry. */
36
+ export type GraphqlMember = GraphqlOperationKind | GraphqlTypeKind;
37
+
38
+ /** Sidebar/overview group label for each member kind. */
39
+ export const GRAPHQL_GROUP_LABELS = {
40
+ enum: "Enums",
41
+ input: "Input Objects",
42
+ interface: "Interfaces",
43
+ mutation: "Mutations",
44
+ object: "Objects",
45
+ query: "Queries",
46
+ scalar: "Scalars",
47
+ subscription: "Subscriptions",
48
+ union: "Unions",
49
+ } satisfies Record<GraphqlMember, string>;
50
+
51
+ /**
52
+ * A type expression as it appears in the schema: the rendered display form
53
+ * (`[String!]!`) plus the named type at its core (`String`), so components can
54
+ * print the exact wrapper shape and still link to the named type's page.
55
+ */
56
+ export interface GraphqlTypeRef {
57
+ display: string;
58
+ name: string;
59
+ }
60
+
61
+ /** An argument or input-object field. */
62
+ export interface GraphqlInputValue {
63
+ name: string;
64
+ description: string;
65
+ type: GraphqlTypeRef;
66
+ /** Default value printed as a GraphQL literal (`10`, `"asc"`, `[1, 2]`). */
67
+ default?: string;
68
+ deprecationReason?: string;
69
+ }
70
+
71
+ /** An output field of an object or interface type (root fields included). */
72
+ export interface GraphqlField {
73
+ name: string;
74
+ description: string;
75
+ type: GraphqlTypeRef;
76
+ args: GraphqlInputValue[];
77
+ deprecationReason?: string;
78
+ }
79
+
80
+ /** One member of an enum type. */
81
+ export interface GraphqlEnumValue {
82
+ name: string;
83
+ description: string;
84
+ deprecationReason?: string;
85
+ }
86
+
87
+ /** One named type of the schema, flattened to the fields the reference renders. */
88
+ export interface GraphqlTypeDef {
89
+ name: string;
90
+ kind: GraphqlTypeKind;
91
+ description: string;
92
+ /** Output fields (object and interface types). */
93
+ fields?: GraphqlField[];
94
+ /** Input fields (input object types). */
95
+ inputFields?: GraphqlInputValue[];
96
+ /** Members (enum types). */
97
+ enumValues?: GraphqlEnumValue[];
98
+ /** Member type names (unions) or implementing object types (interfaces). */
99
+ possibleTypes?: string[];
100
+ /** Interfaces this object/interface type implements. */
101
+ interfaces?: string[];
102
+ /** `specifiedBy` URL (custom scalars). */
103
+ specifiedByUrl?: string;
104
+ /**
105
+ * A spec-defined scalar (`String`, `Int`, …): kept for type refs to resolve
106
+ * against, but never given a reference page of its own.
107
+ */
108
+ builtIn?: boolean;
109
+ }
110
+
111
+ /** A normalized GraphQL schema, lowered to serializable data at parse time. */
112
+ export interface GraphqlDocument {
113
+ /** Mirrors the OpenAPI `info` block so `source.ts` reads one shape. */
114
+ info?: { title?: string; version?: string; description?: string };
115
+ /** Root operation type names (`Query`, `Mutation`, `Subscription`). */
116
+ roots: Partial<Record<GraphqlOperationKind, string>>;
117
+ /** Named types by name, spec-defined scalars included, introspection types excluded. */
118
+ types: Record<string, GraphqlTypeDef>;
119
+ }
120
+
121
+ /** Whether an operation ref's method is a root-field kind (vs a type page). */
122
+ export const isGraphqlOperationKind = (
123
+ method: string
124
+ ): method is GraphqlOperationKind =>
125
+ GRAPHQL_OPERATION_KINDS.some((kind) => kind === method);
126
+
127
+ // Deterministic name order for type pages, independent of schema declaration
128
+ // order and of the platform's collation (localeCompare varies across ICU
129
+ // builds; codepoint order does not).
130
+ const byName = (a: GraphqlTypeDef, b: GraphqlTypeDef): number =>
131
+ a.name < b.name ? -1 : 1;
132
+
133
+ /**
134
+ * Flatten a GraphQL document into a route-mapped page list and its ordered
135
+ * groups — the GraphQL counterpart of `extractOperations` in `model.ts`, built
136
+ * on the same `operationCollector`. Root fields become operation pages grouped
137
+ * as Queries/Mutations/Subscriptions; every other named type becomes a type
138
+ * page grouped by kind (Objects, Input Objects, …). Spec-defined scalars and
139
+ * the root types themselves get no page — the roots' fields ARE the operation
140
+ * pages, and `String`/`Int`/… document nothing.
141
+ */
142
+ export const extractGraphqlOperations = (
143
+ document: GraphqlDocument,
144
+ baseRoute: string
145
+ ): ExtractedOperations => {
146
+ const collector = operationCollector(baseRoute, new Map());
147
+ const rootNames = new Set(Object.values(document.roots));
148
+
149
+ for (const kind of GRAPHQL_OPERATION_KINDS) {
150
+ const rootName = document.roots[kind];
151
+ const root = rootName === undefined ? undefined : document.types[rootName];
152
+ for (const field of root?.fields ?? []) {
153
+ collector.add({
154
+ deprecated: field.deprecationReason !== undefined,
155
+ description: field.description,
156
+ // `operationKey` slug rules keep GraphQL URLs consistent with the
157
+ // other spec kinds; its fallback covers a name that slugifies empty.
158
+ key: operationKey(kind, field.name, field.name),
159
+ method: kind,
160
+ operationId: field.name,
161
+ path: field.name,
162
+ summary: "",
163
+ tag: GRAPHQL_GROUP_LABELS[kind],
164
+ });
165
+ }
166
+ }
167
+
168
+ for (const kind of GRAPHQL_TYPE_KINDS) {
169
+ const members = Object.values(document.types)
170
+ .filter(
171
+ (type) =>
172
+ type.kind === kind && !(type.builtIn || rootNames.has(type.name))
173
+ )
174
+ .toSorted(byName);
175
+ for (const type of members) {
176
+ collector.add({
177
+ deprecated: false,
178
+ description: type.description,
179
+ key: operationKey(kind, type.name, type.name),
180
+ method: kind,
181
+ operationId: type.name,
182
+ path: type.name,
183
+ summary: "",
184
+ tag: GRAPHQL_GROUP_LABELS[kind],
185
+ });
186
+ }
187
+ }
188
+
189
+ return { ...collector.finish(), warnings: [] };
190
+ };
191
+
192
+ /** Resolve the root field behind an operation-kind ref, if it still exists. */
193
+ export const graphqlRootField = (
194
+ document: GraphqlDocument,
195
+ ref: ApiOperationRef
196
+ ): GraphqlField | undefined => {
197
+ if (!isGraphqlOperationKind(ref.method)) {
198
+ return undefined;
199
+ }
200
+ const rootName = document.roots[ref.method];
201
+ const root = rootName === undefined ? undefined : document.types[rootName];
202
+ return root?.fields?.find((field) => field.name === ref.operationId);
203
+ };
204
+
205
+ /** Resolve the named type behind a type-page ref, if it still exists. */
206
+ export const graphqlTypeDef = (
207
+ document: GraphqlDocument,
208
+ ref: ApiOperationRef
209
+ ): GraphqlTypeDef | undefined => {
210
+ const type = document.types[ref.operationId ?? ""];
211
+ return type && type.kind === ref.method ? type : undefined;
212
+ };
@@ -1,6 +1,9 @@
1
1
  import type { Document, OperationObject } from "@scalar/openapi-types/3.1";
2
2
 
3
3
  import type { AsyncApiAction, AsyncApiDocument } from "./asyncapi.ts";
4
+ // Type-only, so the import can't cycle at runtime (graphql.ts imports the
5
+ // collector from here).
6
+ import type { GraphqlDocument, GraphqlMember } from "./graphql.ts";
4
7
  import type { ReferenceKind } from "./references.ts";
5
8
  import { slugify } from "./references.ts";
6
9
 
@@ -39,13 +42,32 @@ export type HttpMethod = (typeof HTTP_METHODS)[number];
39
42
  /** Group used for operations that declare no tag. */
40
43
  const UNTAGGED = "Operations";
41
44
 
45
+ /** A lower-case letter or digit followed by a capital: `addPet`, `v2List`. */
46
+ const CASE_BOUNDARY = /(?<before>[\p{Ll}\p{N}])(?<capital>\p{Lu})/gu;
47
+ /** The last capital of an acronym before a capitalized word: `HTTPResponse`. */
48
+ const ACRONYM_BOUNDARY = /(?<acronym>\p{Lu})(?<word>\p{Lu}\p{Ll})/gu;
49
+
50
+ /**
51
+ * Hyphenate an identifier's word boundaries before slugifying, so a camelCase
52
+ * `operationId` keeps its words in the URL (`getHTTPResponse` ->
53
+ * `get-http-response`) instead of collapsing once `slugify` lowercases it.
54
+ * OpenAPI generators reuse operation ids as SDK method names, so camelCase is
55
+ * the norm there; GraphQL field and type names and AsyncAPI operation ids go
56
+ * through the same rule so every spec kind derives routes alike. An id that
57
+ * is already kebab-case has no boundaries to split and passes through as is.
58
+ */
59
+ const splitIdentifier = (identifier: string): string =>
60
+ identifier
61
+ .replace(CASE_BOUNDARY, "$<before>-$<capital>")
62
+ .replace(ACRONYM_BOUNDARY, "$<acronym>-$<word>");
63
+
42
64
  /** A stable, URL-safe key for an operation: its `operationId`, else method+path. */
43
65
  export const operationKey = (
44
66
  method: string,
45
67
  path: string,
46
68
  operationId?: string
47
69
  ): string => {
48
- const fromId = operationId ? slugify(operationId) : "";
70
+ const fromId = operationId ? slugify(splitIdentifier(operationId)) : "";
49
71
  return fromId || slugify(`${method}-${path}`);
50
72
  };
51
73
 
@@ -53,9 +75,15 @@ export const operationKey = (
53
75
  export interface ApiOperationRef {
54
76
  /** Stable key, unique within a spec; matches the MDX `<Operation id>`. */
55
77
  key: string;
56
- /** HTTP method (OpenAPI) or `send`/`receive` action (AsyncAPI). */
57
- method: HttpMethod | AsyncApiAction;
58
- /** Templated path, e.g. `/pets/{id}` or the channel address (AsyncAPI). */
78
+ /**
79
+ * HTTP method (OpenAPI), `send`/`receive` action (AsyncAPI), or the member
80
+ * kind a root-field operation kind or a named-type kind (GraphQL).
81
+ */
82
+ method: HttpMethod | AsyncApiAction | GraphqlMember;
83
+ /**
84
+ * Templated path, e.g. `/pets/{id}` — the channel address (AsyncAPI), or
85
+ * the root field / type name (GraphQL).
86
+ */
59
87
  path: string;
60
88
  /** Full site route for this operation's page, e.g. `/reference/pet/add-pet`. */
61
89
  route: string;
@@ -90,7 +118,12 @@ export interface ApiSpecData {
90
118
  title: string;
91
119
  version: string;
92
120
  description: string;
93
- document: ApiDocument | AsyncApiDocument;
121
+ document: ApiDocument | AsyncApiDocument | GraphqlDocument;
122
+ /**
123
+ * URL of the live GraphQL endpoint the playground and code samples target
124
+ * (GraphQL only; OpenAPI documents carry their servers in the document).
125
+ */
126
+ endpoint?: string;
94
127
  /** Operations keyed by {@link ApiOperationRef.key}. */
95
128
  operations: Record<string, ApiOperationRef>;
96
129
  tags: ApiTagRef[];
@@ -10,6 +10,8 @@ import { isAbsolute, join } from "pathe";
10
10
  import { hashText } from "../core/sources/cache.ts";
11
11
  import type { AsyncApiDocument } from "./asyncapi.ts";
12
12
  import { normalizeAsyncApiDocument } from "./asyncapi.ts";
13
+ import { buildGraphqlDocument } from "./graphql-build.ts";
14
+ import type { GraphqlDocument } from "./graphql.ts";
13
15
  import type { ApiDocument } from "./model.ts";
14
16
 
15
17
  /**
@@ -393,3 +395,35 @@ export const parseAsyncApiSpec = async (
393
395
  }
394
396
  return { document: normalizeAsyncApiDocument(document), warnings };
395
397
  };
398
+
399
+ export interface ParsedGraphqlSpec {
400
+ document: GraphqlDocument;
401
+ warnings: string[];
402
+ }
403
+
404
+ /**
405
+ * Read and lower a GraphQL schema — SDL text or an introspection JSON result —
406
+ * to the serializable document the reference renders from; the GraphQL mirror
407
+ * of {@link parseSpec}. Error semantics match the other kinds: an unreadable
408
+ * spec throws the read/fetch error, while readable-but-invalid schema text
409
+ * (SDL syntax errors, validation failures, JSON that isn't an introspection
410
+ * result) throws {@link InvalidSpecError} so callers suggest fixing the file
411
+ * rather than checking reachability.
412
+ */
413
+ export const parseGraphqlSpec = async (
414
+ spec: string,
415
+ root: string,
416
+ options: SpecFetchOptions = {}
417
+ ): Promise<ParsedGraphqlSpec> => {
418
+ const { text, warnings } = await readSpecText(spec, root, options);
419
+ try {
420
+ return { document: buildGraphqlDocument(text), warnings };
421
+ } catch (error) {
422
+ // SAFETY: `graphql`-js throws GraphQLError (an Error) for syntax and
423
+ // validation failures, and `buildGraphqlDocument` throws plain Errors for
424
+ // non-introspection JSON; only the message is surfaced.
425
+ throw new InvalidSpecError(
426
+ `${spec} is not a valid GraphQL schema (${(error as Error).message.trim()})`
427
+ );
428
+ }
429
+ };
@@ -51,6 +51,15 @@ const REDIRECT_STATUS = new Set([301, 302, 303, 307, 308]);
51
51
  /** Hops followed before giving up, matching fetch's own redirect limit. */
52
52
  const MAX_REDIRECTS = 20;
53
53
 
54
+ /**
55
+ * Deadline for the whole upstream exchange, redirects included. The
56
+ * playground client aborts its own request after 30 s, but that never
57
+ * reaches the server-side fetch: without a deadline here, a documented API
58
+ * that accepts the connection and never answers would hold a server request
59
+ * slot until the platform killed it. Matches the client's limit.
60
+ */
61
+ const UPSTREAM_TIMEOUT_MS = 30_000;
62
+
54
63
  /** A 400 the playground client can render verbatim. */
55
64
  const badRequest = (error: string): Response =>
56
65
  Response.json({ error }, { status: 400 });
@@ -96,6 +105,8 @@ const followUpstream = async (args: {
96
105
  /** Hops already followed; the chain is bounded by {@link MAX_REDIRECTS}. */
97
106
  hop: number;
98
107
  method: string;
108
+ /** One deadline shared by every hop, so a redirect chain can't extend it. */
109
+ signal: AbortSignal;
99
110
  url: URL;
100
111
  }): Promise<Response> => {
101
112
  const response = await args.fetchImpl(args.url, {
@@ -103,6 +114,7 @@ const followUpstream = async (args: {
103
114
  headers: args.headers,
104
115
  method: args.method,
105
116
  redirect: "manual",
117
+ signal: args.signal,
106
118
  });
107
119
  const location = REDIRECT_STATUS.has(response.status)
108
120
  ? response.headers.get("location")
@@ -151,10 +163,15 @@ const followUpstream = async (args: {
151
163
  * Loopback and private addresses need no separate rule: they are reachable only
152
164
  * when a spec documents them, which is exactly the local-API case that must
153
165
  * keep working.
166
+ *
167
+ * `timeoutMs` bounds the upstream exchange (see {@link UPSTREAM_TIMEOUT_MS});
168
+ * an upstream that doesn't answer in time is the same 502 as an unreachable
169
+ * one. Injectable so tests don't wait out the real deadline.
154
170
  */
155
171
  export const createPlaygroundProxyHandler = (
156
172
  origins: readonly string[],
157
- fetchImpl: typeof fetch = fetch
173
+ fetchImpl: typeof fetch = fetch,
174
+ timeoutMs = UPSTREAM_TIMEOUT_MS
158
175
  ) => {
159
176
  const allowed = new Set(origins);
160
177
  return async (request: Request): Promise<Response> => {
@@ -192,14 +209,22 @@ export const createPlaygroundProxyHandler = (
192
209
  headers: filterHeaders(request.headers, REQUEST_DROP),
193
210
  hop: 0,
194
211
  method: request.method,
212
+ signal: AbortSignal.timeout(timeoutMs),
195
213
  url: parsed,
196
214
  });
197
215
  } catch (error) {
198
- // SAFETY: followUpstream itself only throws `new Error(...)`, and a
199
- // failed fetch rejects with a TypeError per spec both are Errors
200
- // carrying `.message`.
216
+ // SAFETY: followUpstream itself only throws `new Error(...)`, a failed
217
+ // fetch rejects with a TypeError per spec, and a timed-out one with a
218
+ // `TimeoutError` DOMException — all Errors carrying `.name` and
219
+ // `.message`.
220
+ const { name, message } = error as Error;
201
221
  return Response.json(
202
- { error: (error as Error).message },
222
+ {
223
+ error:
224
+ name === "TimeoutError"
225
+ ? `Upstream ${parsed.href} did not respond within ${timeoutMs}ms.`
226
+ : message,
227
+ },
203
228
  { status: 502 }
204
229
  );
205
230
  }