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.
- package/CHANGELOG.md +85 -0
- package/dist/cli/index.js +3639 -1377
- package/dist/cli/index.js.map +103 -91
- package/dist/types/ai/component-markdown.d.ts +79 -0
- package/dist/types/components/layout/nav-utils.d.ts +60 -0
- package/dist/types/core/base-path.d.ts +9 -0
- package/dist/types/core/config-input.d.ts +206 -4
- package/dist/types/core/config.d.ts +6 -4
- package/dist/types/core/data.d.ts +23 -1
- package/dist/types/core/github.d.ts +35 -0
- package/dist/types/core/i18n-ui.d.ts +8 -0
- package/dist/types/core/navigation.d.ts +69 -0
- package/dist/types/core/schema.d.ts +117 -1
- package/dist/types/core/sources/types.d.ts +31 -6
- package/dist/types/core/types.d.ts +23 -2
- package/dist/types/markdown/features.d.ts +21 -0
- package/dist/types/openapi/references.d.ts +21 -1
- package/dist/types/seo/jsonld.d.ts +105 -0
- package/dist/types/theme/fonts.d.ts +34 -4
- package/docs/_snippets/include-demo.mdx +7 -0
- package/docs/advanced/api-reference.mdx +3 -3
- package/docs/advanced/custom-pages.mdx +1 -1
- package/docs/advanced/graphql.mdx +84 -0
- package/docs/advanced/meta.ts +8 -1
- package/docs/configuration/ai.mdx +21 -3
- package/docs/configuration/index.mdx +24 -0
- package/docs/configuration/search.mdx +13 -1
- package/docs/configuration/seo.mdx +27 -0
- package/docs/configuration/theming.mdx +17 -0
- package/docs/content/components.mdx +7 -0
- package/docs/content/includes.mdx +68 -0
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +25 -0
- package/docs/content/sources.mdx +42 -1
- package/docs/content/syntax.mdx +69 -1
- package/docs/content/versioning.mdx +15 -9
- package/docs/reference/cli.mdx +2 -1
- package/package.json +23 -14
- package/skills/blume-migrate/SKILL.md +16 -7
- package/skills/blume-migrate/references/docusaurus.md +5 -3
- package/skills/blume-migrate/references/fumadocs.md +10 -2
- package/skills/blume-migrate/references/mintlify.md +3 -2
- package/skills/blume-migrate/references/nextra.md +2 -2
- package/skills/blume-migrate/references/starlight.md +1 -1
- package/src/ai/agent-readability.ts +2 -1
- package/src/ai/ask-data.ts +2 -1
- package/src/ai/component-markdown.ts +199 -36
- package/src/ai/llms.ts +93 -6
- package/src/ai/markdown.ts +2 -2
- package/src/ai/mcp/discovery.ts +10 -2
- package/src/ai/mcp/server.ts +74 -2
- package/src/astro/generate.ts +183 -116
- package/src/astro/include-hmr.ts +81 -0
- package/src/astro/include-refresh.ts +0 -0
- package/src/astro/index.ts +3 -5
- package/src/astro/templates.ts +125 -76
- package/src/cli/commands/build.ts +84 -15
- package/src/cli/init/questions.ts +1 -0
- package/src/cli/init/scaffold.ts +27 -4
- package/src/components/colors.ts +142 -0
- package/src/components/content/Badge.astro +5 -12
- package/src/components/content/Callout.astro +19 -36
- package/src/components/content/Card.astro +15 -21
- package/src/components/content/Component.astro +10 -1
- package/src/components/content/GithubInfo.astro +28 -9
- package/src/components/content/Tabs.astro +27 -5
- package/src/components/content/github-info.ts +20 -5
- package/src/components/dropdown-dismiss.ts +122 -0
- package/src/components/layout/Fonts.astro +15 -8
- package/src/components/layout/Header.astro +44 -0
- package/src/components/layout/LanguageSwitcher.astro +9 -1
- package/src/components/layout/NavSelector.astro +12 -3
- package/src/components/layout/NavTree.astro +6 -18
- package/src/components/layout/PageActions.astro +29 -8
- package/src/components/layout/PageLayout.astro +10 -1
- package/src/components/layout/ReferenceLayout.astro +6 -1
- package/src/components/layout/RootLayout.astro +46 -15
- package/src/components/layout/Search.astro +36 -4
- package/src/components/layout/TableOfContents.astro +8 -2
- package/src/components/layout/head-scripts.ts +53 -1
- package/src/components/openapi/ApiOverview.astro +13 -3
- package/src/components/openapi/AsyncApiOperation.astro +7 -14
- package/src/components/openapi/GraphqlChip.astro +33 -0
- package/src/components/openapi/GraphqlFieldsTable.astro +111 -0
- package/src/components/openapi/GraphqlOperation.astro +186 -0
- package/src/components/openapi/GraphqlType.astro +154 -0
- package/src/components/openapi/MethodBadge.astro +3 -14
- package/src/components/openapi/Operation.astro +12 -5
- package/src/components/openapi/OperationPanel.astro +43 -0
- package/src/components/openapi/RequestPanel.astro +5 -10
- package/src/components/openapi/Responses.astro +1 -16
- package/src/components/openapi/graphql-helpers.ts +466 -0
- package/src/components/openapi/playground-client.ts +15 -0
- package/src/components/openapi/sample-panels.ts +45 -0
- package/src/components/openapi/snippets.ts +13 -35
- package/src/core/base-path.ts +11 -0
- package/src/core/config-input.ts +209 -2
- package/src/core/config.ts +6 -4
- package/src/core/content-assets.ts +15 -4
- package/src/core/data.ts +18 -2
- package/src/core/diagnostics.ts +8 -0
- package/src/core/frontmatter.ts +20 -8
- package/src/core/github.ts +71 -0
- package/src/core/graph.ts +22 -8
- package/src/core/heading-markers.ts +96 -0
- package/src/core/i18n-ui.ts +11 -0
- package/src/core/includes.ts +632 -0
- package/src/core/last-modified.ts +36 -11
- package/src/core/links.ts +79 -13
- package/src/core/meta.ts +2 -1
- package/src/core/nav-diagnostics.ts +11 -2
- package/src/core/navigation.ts +27 -6
- package/src/core/project-graph.ts +61 -9
- package/src/core/schema.ts +226 -35
- package/src/core/server-features.ts +5 -9
- package/src/core/sources/github-releases.ts +2 -2
- package/src/core/sources/normalize.ts +502 -115
- package/src/core/sources/notion.ts +43 -8
- package/src/core/sources/obsidian.ts +1038 -0
- package/src/core/sources/read.ts +36 -1
- package/src/core/sources/resolve.ts +34 -1
- package/src/core/sources/types.ts +28 -6
- package/src/core/sources/watch.ts +12 -8
- package/src/core/tsconfig-aliases.ts +48 -35
- package/src/core/types.ts +25 -2
- package/src/core/ui-packs/ar.ts +1 -0
- package/src/core/ui-packs/bg.ts +2 -0
- package/src/core/ui-packs/bn.ts +1 -0
- package/src/core/ui-packs/ca.ts +2 -0
- package/src/core/ui-packs/cs.ts +1 -0
- package/src/core/ui-packs/da.ts +1 -0
- package/src/core/ui-packs/de.ts +2 -0
- package/src/core/ui-packs/el.ts +2 -0
- package/src/core/ui-packs/es.ts +2 -0
- package/src/core/ui-packs/fa.ts +1 -0
- package/src/core/ui-packs/fi.ts +1 -0
- package/src/core/ui-packs/fr.ts +2 -0
- package/src/core/ui-packs/he.ts +1 -0
- package/src/core/ui-packs/hi.ts +1 -0
- package/src/core/ui-packs/hr.ts +2 -0
- package/src/core/ui-packs/hu.ts +2 -0
- package/src/core/ui-packs/id.ts +2 -0
- package/src/core/ui-packs/it.ts +1 -0
- package/src/core/ui-packs/ja.ts +2 -0
- package/src/core/ui-packs/ko.ts +2 -0
- package/src/core/ui-packs/nl.ts +2 -0
- package/src/core/ui-packs/no.ts +2 -0
- package/src/core/ui-packs/pl.ts +2 -0
- package/src/core/ui-packs/pt-br.ts +2 -0
- package/src/core/ui-packs/pt.ts +2 -0
- package/src/core/ui-packs/ro.ts +2 -0
- package/src/core/ui-packs/ru.ts +2 -0
- package/src/core/ui-packs/sk.ts +1 -0
- package/src/core/ui-packs/sr.ts +1 -0
- package/src/core/ui-packs/sv.ts +2 -0
- package/src/core/ui-packs/th.ts +1 -0
- package/src/core/ui-packs/tr.ts +2 -0
- package/src/core/ui-packs/uk.ts +2 -0
- package/src/core/ui-packs/vi.ts +1 -0
- package/src/core/ui-packs/zh-tw.ts +1 -0
- package/src/core/ui-packs/zh.ts +1 -0
- package/src/core/version-cut.ts +21 -3
- package/src/core/yaml.ts +26 -0
- package/src/deploy/function-bundle.ts +251 -0
- package/src/eval/schema.ts +3 -1
- package/src/markdown/code-title.ts +22 -16
- package/src/markdown/features.ts +21 -0
- package/src/markdown/fence-meta.ts +50 -0
- package/src/markdown/heading-anchors.ts +198 -37
- package/src/markdown/include.ts +247 -0
- package/src/markdown/index.ts +43 -34
- package/src/markdown/language-icon.ts +2 -2
- package/src/markdown/mdast.ts +7 -3
- package/src/markdown/ts2js.ts +264 -0
- package/src/openapi/asyncapi.ts +4 -1
- package/src/openapi/graphql-build.ts +293 -0
- package/src/openapi/graphql.ts +212 -0
- package/src/openapi/model.ts +38 -5
- package/src/openapi/parse.ts +34 -0
- package/src/openapi/proxy.ts +30 -5
- package/src/openapi/references.ts +89 -13
- package/src/openapi/render-mdx.ts +48 -8
- package/src/openapi/scalar.ts +5 -12
- package/src/openapi/source.ts +91 -23
- package/src/registry/eject.ts +11 -0
- package/src/search/documents.ts +229 -37
- package/src/search/orama-index.ts +9 -5
- package/src/seo/jsonld.ts +293 -51
- package/src/theme/code-block-padding.ts +16 -0
- package/src/theme/entry.ts +65 -11
- package/src/theme/fonts.ts +189 -16
- package/src/translate/prompts.ts +2 -0
- package/src/translate/run.ts +7 -0
- 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
|
+
};
|
package/src/openapi/model.ts
CHANGED
|
@@ -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
|
-
/**
|
|
57
|
-
|
|
58
|
-
|
|
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[];
|
package/src/openapi/parse.ts
CHANGED
|
@@ -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
|
+
};
|
package/src/openapi/proxy.ts
CHANGED
|
@@ -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(...)`,
|
|
199
|
-
//
|
|
200
|
-
// carrying `.
|
|
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
|
-
{
|
|
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
|
}
|