blume 1.5.3 → 1.6.1
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 +94 -0
- package/dist/cli/index.js +3949 -1403
- package/dist/cli/index.js.map +111 -96
- 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 +33 -2
- package/dist/types/core/github.d.ts +35 -0
- package/dist/types/core/i18n-ui.d.ts +10 -0
- package/dist/types/core/navigation.d.ts +69 -0
- package/dist/types/core/schema.d.ts +122 -1
- package/dist/types/core/sources/types.d.ts +31 -6
- package/dist/types/core/types.d.ts +29 -2
- package/dist/types/markdown/features.d.ts +21 -0
- package/dist/types/openapi/references.d.ts +26 -1
- package/dist/types/seo/jsonld.d.ts +105 -0
- package/dist/types/theme/fonts.d.ts +34 -4
- package/docs/07-faq.mdx +9 -9
- package/docs/_snippets/include-demo.mdx +7 -0
- package/docs/advanced/api-reference.mdx +13 -4
- package/docs/advanced/custom-pages.mdx +4 -2
- package/docs/advanced/graphql.mdx +84 -0
- package/docs/advanced/meta.ts +8 -1
- package/docs/configuration/ai.mdx +25 -3
- package/docs/configuration/index.mdx +24 -0
- package/docs/configuration/search.mdx +13 -1
- package/docs/configuration/seo.mdx +30 -3
- package/docs/configuration/theming.mdx +23 -0
- package/docs/content/components.mdx +15 -1
- 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 +66 -57
- 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/examples.ts +29 -2
- package/src/astro/generate.ts +282 -177
- package/src/astro/include-hmr.ts +81 -0
- package/src/astro/include-refresh.ts +0 -0
- package/src/astro/index.ts +10 -5
- package/src/astro/markdown-negotiation.ts +1 -1
- package/src/astro/runtime-modules.ts +196 -0
- package/src/astro/templates.ts +365 -113
- package/src/cli/commands/build.ts +91 -16
- package/src/cli/commands/dev.ts +6 -3
- package/src/cli/host-args.ts +18 -0
- package/src/cli/index.ts +2 -1
- 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/copy-feedback.ts +93 -9
- package/src/components/dropdown-dismiss.ts +122 -0
- package/src/components/islands/ask-ai.tsx +4 -1
- package/src/components/islands/hooks.ts +3 -1
- 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 +54 -22
- package/src/components/layout/PageLayout.astro +2 -0
- package/src/components/layout/ReferenceLayout.astro +6 -1
- package/src/components/layout/RootLayout.astro +42 -15
- package/src/components/layout/Search.astro +36 -4
- package/src/components/layout/TableOfContents.astro +8 -2
- package/src/components/layout/head-scripts.ts +30 -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 +28 -3
- package/src/core/define-components.ts +2 -0
- 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 +12 -0
- package/src/core/includes.ts +633 -0
- package/src/core/last-modified.ts +36 -11
- package/src/core/links.ts +79 -13
- package/src/core/manifest.ts +10 -0
- 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 +235 -36
- 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 +31 -2
- package/src/core/ui-packs/ar.ts +2 -0
- package/src/core/ui-packs/bg.ts +3 -0
- package/src/core/ui-packs/bn.ts +2 -0
- package/src/core/ui-packs/ca.ts +3 -0
- package/src/core/ui-packs/cs.ts +2 -0
- package/src/core/ui-packs/da.ts +2 -0
- package/src/core/ui-packs/de.ts +3 -0
- package/src/core/ui-packs/el.ts +3 -0
- package/src/core/ui-packs/es.ts +3 -0
- package/src/core/ui-packs/fa.ts +2 -0
- package/src/core/ui-packs/fi.ts +2 -0
- package/src/core/ui-packs/fr.ts +3 -0
- package/src/core/ui-packs/he.ts +2 -0
- package/src/core/ui-packs/hi.ts +2 -0
- package/src/core/ui-packs/hr.ts +3 -0
- package/src/core/ui-packs/hu.ts +3 -0
- package/src/core/ui-packs/id.ts +3 -0
- package/src/core/ui-packs/it.ts +2 -0
- package/src/core/ui-packs/ja.ts +3 -0
- package/src/core/ui-packs/ko.ts +3 -0
- package/src/core/ui-packs/nl.ts +3 -0
- package/src/core/ui-packs/no.ts +3 -0
- package/src/core/ui-packs/pl.ts +3 -0
- package/src/core/ui-packs/pt-br.ts +3 -0
- package/src/core/ui-packs/pt.ts +3 -0
- package/src/core/ui-packs/ro.ts +3 -0
- package/src/core/ui-packs/ru.ts +3 -0
- package/src/core/ui-packs/sk.ts +2 -0
- package/src/core/ui-packs/sr.ts +2 -0
- package/src/core/ui-packs/sv.ts +3 -0
- package/src/core/ui-packs/th.ts +2 -0
- package/src/core/ui-packs/tr.ts +3 -0
- package/src/core/ui-packs/uk.ts +3 -0
- package/src/core/ui-packs/vi.ts +2 -0
- package/src/core/ui-packs/zh-tw.ts +2 -0
- package/src/core/ui-packs/zh.ts +2 -0
- package/src/core/version-cut.ts +26 -6
- package/src/core/yaml.ts +26 -0
- package/src/deploy/function-bundle.ts +251 -0
- package/src/deploy/vercel-negotiation.ts +49 -6
- 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/og/card.ts +1 -1
- 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 +97 -13
- package/src/openapi/render-mdx.ts +66 -12
- package/src/openapi/scalar.ts +5 -16
- package/src/openapi/source.ts +91 -23
- package/src/registry/eject.ts +47 -17
- 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 +67 -13
- package/src/theme/fonts.ts +189 -16
- package/src/theme/sources.ts +49 -0
- package/src/translate/prompts.ts +2 -0
- package/src/translate/run.ts +7 -0
- package/src/translate/work-list.ts +0 -0
|
@@ -3,6 +3,7 @@ import data from "blume:data";
|
|
|
3
3
|
|
|
4
4
|
import { highlightCode } from "../../markdown/index.ts";
|
|
5
5
|
import { exampleValue, type SchemaLike, toJson } from "./helpers.ts";
|
|
6
|
+
import { languageSamplePanels } from "./sample-panels.ts";
|
|
6
7
|
import PanelTabs from "./PanelTabs.astro";
|
|
7
8
|
import type { RequestSample, SampleLanguage } from "./snippets.ts";
|
|
8
9
|
|
|
@@ -25,16 +26,10 @@ interface Props {
|
|
|
25
26
|
|
|
26
27
|
const { sample, languages, responses, schemas } = Astro.props;
|
|
27
28
|
|
|
28
|
-
const requestPanels = await
|
|
29
|
-
languages
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
themes: data.config.codeThemes,
|
|
33
|
-
}),
|
|
34
|
-
key: language.id,
|
|
35
|
-
label: language.label,
|
|
36
|
-
lang: language.id,
|
|
37
|
-
}))
|
|
29
|
+
const requestPanels = await languageSamplePanels(
|
|
30
|
+
languages,
|
|
31
|
+
sample,
|
|
32
|
+
data.config.codeThemes
|
|
38
33
|
);
|
|
39
34
|
|
|
40
35
|
const responsePanels = await Promise.all(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { statusColor } from "../colors.ts";
|
|
2
3
|
import type { SchemaLike } from "./helpers.ts";
|
|
3
4
|
import SchemaTable from "./SchemaTable.astro";
|
|
4
5
|
|
|
@@ -19,22 +20,6 @@ interface Props {
|
|
|
19
20
|
|
|
20
21
|
const { responses, schemas, expandAll = false } = Astro.props;
|
|
21
22
|
|
|
22
|
-
const statusColor = (status: string): string => {
|
|
23
|
-
if (status.startsWith("2")) {
|
|
24
|
-
return "bg-green-500/15 text-green-700 dark:text-green-300";
|
|
25
|
-
}
|
|
26
|
-
if (status.startsWith("3")) {
|
|
27
|
-
return "bg-blue-500/15 text-blue-700 dark:text-blue-300";
|
|
28
|
-
}
|
|
29
|
-
if (status.startsWith("4")) {
|
|
30
|
-
return "bg-orange-500/15 text-orange-700 dark:text-orange-300";
|
|
31
|
-
}
|
|
32
|
-
if (status.startsWith("5")) {
|
|
33
|
-
return "bg-red-500/15 text-red-700 dark:text-red-300";
|
|
34
|
-
}
|
|
35
|
-
return "bg-muted text-muted-foreground";
|
|
36
|
-
};
|
|
37
|
-
|
|
38
23
|
const items = Object.entries(responses);
|
|
39
24
|
---
|
|
40
25
|
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GraphqlDocument,
|
|
3
|
+
GraphqlField,
|
|
4
|
+
GraphqlInputValue,
|
|
5
|
+
GraphqlOperationKind,
|
|
6
|
+
GraphqlTypeRef,
|
|
7
|
+
} from "../../openapi/graphql.ts";
|
|
8
|
+
import {
|
|
9
|
+
GRAPHQL_OPERATION_KINDS,
|
|
10
|
+
isGraphqlOperationKind,
|
|
11
|
+
} from "../../openapi/graphql.ts";
|
|
12
|
+
import type { ApiSpecData } from "../../openapi/model.ts";
|
|
13
|
+
import type { SpecValue } from "./helpers.ts";
|
|
14
|
+
import type { PlaygroundModel } from "./request.ts";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build-time logic behind the GraphQL reference pages: the generated example
|
|
18
|
+
* operation (a valid, bounded-depth query with a variable per argument), its
|
|
19
|
+
* example variables and response, the shared playground/request model, and the
|
|
20
|
+
* usage backlinks. Everything here works off the serializable
|
|
21
|
+
* `GraphqlDocument` — no `graphql`-js — so none of it can drag the parser
|
|
22
|
+
* into the runtime.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Stands in for the live endpoint when the config sets none, so the samples
|
|
27
|
+
* and playground render a complete request the reader edits instead of a
|
|
28
|
+
* URL-less fragment.
|
|
29
|
+
*/
|
|
30
|
+
export const GRAPHQL_ENDPOINT_PLACEHOLDER =
|
|
31
|
+
"https://your-api.example.com/graphql";
|
|
32
|
+
|
|
33
|
+
/** How deep a generated selection set descends into nested object fields. */
|
|
34
|
+
const SELECTION_DEPTH = 2;
|
|
35
|
+
|
|
36
|
+
/** How deep example variables descend into nested input objects. */
|
|
37
|
+
const VARIABLE_DEPTH = 3;
|
|
38
|
+
|
|
39
|
+
/** Example values for the spec-defined scalars. */
|
|
40
|
+
const SCALAR_SAMPLES = new Map<string, SpecValue>([
|
|
41
|
+
["Boolean", true],
|
|
42
|
+
["Float", 0],
|
|
43
|
+
["ID", "id"],
|
|
44
|
+
["Int", 0],
|
|
45
|
+
["String", "string"],
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
/** A generated example object (variables, response data). */
|
|
49
|
+
export interface GraphqlSampleObject {
|
|
50
|
+
[key: string]: SpecValue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** One field of a generated selection set. */
|
|
54
|
+
export interface GraphqlSelection {
|
|
55
|
+
name: string;
|
|
56
|
+
/** The field's declared type (absent on the `__typename` meta field). */
|
|
57
|
+
type?: GraphqlTypeRef;
|
|
58
|
+
/** Sub-selection for composite fields; absent on leaves. */
|
|
59
|
+
children?: GraphqlSelection[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Whether selecting this named type requires a sub-selection. */
|
|
63
|
+
const isComposite = (document: GraphqlDocument, name: string): boolean => {
|
|
64
|
+
const kind = document.types[name]?.kind;
|
|
65
|
+
return kind === "object" || kind === "interface" || kind === "union";
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A valid selection set for a composite type, or undefined for leaves: every
|
|
70
|
+
* scalar/enum field, plus nested composites while `depth` allows. A composite
|
|
71
|
+
* with nothing selectable at this depth falls back to `__typename`, which is
|
|
72
|
+
* legal on any composite — the set must never come out empty.
|
|
73
|
+
*/
|
|
74
|
+
export const selectionSet = (
|
|
75
|
+
document: GraphqlDocument,
|
|
76
|
+
typeName: string,
|
|
77
|
+
depth: number = SELECTION_DEPTH
|
|
78
|
+
): GraphqlSelection[] | undefined => {
|
|
79
|
+
const type = document.types[typeName];
|
|
80
|
+
if (!(type && isComposite(document, typeName))) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
if (type.kind === "union") {
|
|
84
|
+
return [{ name: "__typename" }];
|
|
85
|
+
}
|
|
86
|
+
const selections: GraphqlSelection[] = [];
|
|
87
|
+
for (const field of type.fields ?? []) {
|
|
88
|
+
// A field with an argument that must be supplied — non-null and no
|
|
89
|
+
// default — can't ride an example selection; there is nothing valid to
|
|
90
|
+
// fill it with inline. A defaulted non-null argument is omittable.
|
|
91
|
+
if (
|
|
92
|
+
field.args.some(
|
|
93
|
+
(arg) => arg.type.display.endsWith("!") && arg.default === undefined
|
|
94
|
+
)
|
|
95
|
+
) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (isComposite(document, field.type.name)) {
|
|
99
|
+
if (depth > 1) {
|
|
100
|
+
selections.push({
|
|
101
|
+
children: selectionSet(document, field.type.name, depth - 1),
|
|
102
|
+
name: field.name,
|
|
103
|
+
type: field.type,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
selections.push({ name: field.name, type: field.type });
|
|
109
|
+
}
|
|
110
|
+
return selections.length > 0 ? selections : [{ name: "__typename" }];
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const INDENT = " ";
|
|
114
|
+
|
|
115
|
+
const printSelections = (
|
|
116
|
+
selections: GraphqlSelection[],
|
|
117
|
+
level: number
|
|
118
|
+
): string =>
|
|
119
|
+
selections
|
|
120
|
+
.map((selection) => {
|
|
121
|
+
const pad = INDENT.repeat(level);
|
|
122
|
+
return selection.children
|
|
123
|
+
? `${pad}${selection.name} {\n${printSelections(selection.children, level + 1)}\n${pad}}`
|
|
124
|
+
: `${pad}${selection.name}`;
|
|
125
|
+
})
|
|
126
|
+
.join("\n");
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A complete, valid example operation for one root field: one variable per
|
|
130
|
+
* argument (typed off the schema), and a bounded-depth selection set over the
|
|
131
|
+
* return type.
|
|
132
|
+
*/
|
|
133
|
+
export const exampleQuery = (
|
|
134
|
+
document: GraphqlDocument,
|
|
135
|
+
field: GraphqlField,
|
|
136
|
+
kind: GraphqlOperationKind
|
|
137
|
+
): string => {
|
|
138
|
+
const name = field.name.charAt(0).toUpperCase() + field.name.slice(1);
|
|
139
|
+
const variables = field.args
|
|
140
|
+
.map((arg) => `$${arg.name}: ${arg.type.display}`)
|
|
141
|
+
.join(", ");
|
|
142
|
+
const args = field.args.map((arg) => `${arg.name}: $${arg.name}`).join(", ");
|
|
143
|
+
const head = `${kind} ${name}${variables ? `(${variables})` : ""}`;
|
|
144
|
+
const call = `${field.name}${args ? `(${args})` : ""}`;
|
|
145
|
+
const selections = selectionSet(document, field.type.name);
|
|
146
|
+
const body = selections
|
|
147
|
+
? `${INDENT}${call} {\n${printSelections(selections, 2)}\n${INDENT}}`
|
|
148
|
+
: `${INDENT}${call}`;
|
|
149
|
+
return `${head} {\n${body}\n}`;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Wrap a sample in one array per list layer of the type expression, so a
|
|
154
|
+
* nested-list field (`[[Cell]]`) samples with the same nesting a real server
|
|
155
|
+
* returns. `[` can only come from list wrappers — it never appears in a type
|
|
156
|
+
* name — so counting occurrences is exact.
|
|
157
|
+
*/
|
|
158
|
+
const wrapLists = (display: string, value: SpecValue): SpecValue => {
|
|
159
|
+
let wrapped = value;
|
|
160
|
+
for (let lists = display.split("[").length - 1; lists > 0; lists -= 1) {
|
|
161
|
+
wrapped = [wrapped];
|
|
162
|
+
}
|
|
163
|
+
return wrapped;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Whether the named type sits in a non-null position of the expression —
|
|
168
|
+
* `Date!` or the element of `[Date!]`, but not `[Date]!`, whose outer
|
|
169
|
+
* non-null wraps the list, not the name.
|
|
170
|
+
*/
|
|
171
|
+
const namedNonNull = (ref: GraphqlTypeRef): boolean =>
|
|
172
|
+
ref.display.includes(`${ref.name}!`);
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* An example value for a type expression: scalar/enum samples at the leaves,
|
|
176
|
+
* nested objects for input types while `depth` allows, and one array wrapper
|
|
177
|
+
* per list layer. A custom scalar or a depth-exhausted input samples as null
|
|
178
|
+
* only where null is legal — in a non-null position they fall back to a
|
|
179
|
+
* placeholder (a string / an empty object) so the example variables stay
|
|
180
|
+
* valid against the schema.
|
|
181
|
+
*/
|
|
182
|
+
const sampleForRef = (
|
|
183
|
+
document: GraphqlDocument,
|
|
184
|
+
ref: GraphqlTypeRef,
|
|
185
|
+
depth: number
|
|
186
|
+
): SpecValue => {
|
|
187
|
+
let named: SpecValue = SCALAR_SAMPLES.get(ref.name) ?? null;
|
|
188
|
+
const type = document.types[ref.name];
|
|
189
|
+
if (type?.kind === "enum") {
|
|
190
|
+
named = type.enumValues?.[0]?.name ?? null;
|
|
191
|
+
} else if (type?.kind === "input" && depth > 0) {
|
|
192
|
+
named = Object.fromEntries(
|
|
193
|
+
(type.inputFields ?? []).map((field) => [
|
|
194
|
+
field.name,
|
|
195
|
+
sampleForRef(document, field.type, depth - 1),
|
|
196
|
+
])
|
|
197
|
+
);
|
|
198
|
+
} else if (type?.kind === "input") {
|
|
199
|
+
named = namedNonNull(ref) ? {} : null;
|
|
200
|
+
} else if (named === null && namedNonNull(ref)) {
|
|
201
|
+
named = "string";
|
|
202
|
+
}
|
|
203
|
+
return wrapLists(ref.display, named);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/** Example `variables` for a root field's arguments; undefined when it has none. */
|
|
207
|
+
export const exampleVariables = (
|
|
208
|
+
document: GraphqlDocument,
|
|
209
|
+
field: GraphqlField
|
|
210
|
+
): GraphqlSampleObject | undefined =>
|
|
211
|
+
field.args.length > 0
|
|
212
|
+
? Object.fromEntries(
|
|
213
|
+
field.args.map((arg) => [
|
|
214
|
+
arg.name,
|
|
215
|
+
sampleForRef(document, arg.type, VARIABLE_DEPTH),
|
|
216
|
+
])
|
|
217
|
+
)
|
|
218
|
+
: undefined;
|
|
219
|
+
|
|
220
|
+
/** The example value one selection produces, mirroring the generated query. */
|
|
221
|
+
const responseForSelections = (
|
|
222
|
+
document: GraphqlDocument,
|
|
223
|
+
typeName: string,
|
|
224
|
+
selections: GraphqlSelection[]
|
|
225
|
+
): GraphqlSampleObject => {
|
|
226
|
+
const out: GraphqlSampleObject = {};
|
|
227
|
+
for (const selection of selections) {
|
|
228
|
+
if (selection.name === "__typename") {
|
|
229
|
+
// A union's example names its first member; other composites name
|
|
230
|
+
// themselves.
|
|
231
|
+
out.__typename = document.types[typeName]?.possibleTypes?.[0] ?? typeName;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
// SAFETY: every non-`__typename` selection is built with its field type.
|
|
235
|
+
const ref = selection.type as GraphqlTypeRef;
|
|
236
|
+
if (selection.children) {
|
|
237
|
+
const nested = responseForSelections(
|
|
238
|
+
document,
|
|
239
|
+
ref.name,
|
|
240
|
+
selection.children
|
|
241
|
+
);
|
|
242
|
+
out[selection.name] = wrapLists(ref.display, nested);
|
|
243
|
+
} else {
|
|
244
|
+
out[selection.name] = sampleForRef(document, ref, 0);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/** The example response envelope: `data` keyed by the root field. */
|
|
251
|
+
export interface GraphqlExampleResponse {
|
|
252
|
+
data: GraphqlSampleObject;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* An example response envelope for the generated query — the same selection
|
|
257
|
+
* set, so the response shows exactly the fields the query asks for.
|
|
258
|
+
*/
|
|
259
|
+
export const exampleResponse = (
|
|
260
|
+
document: GraphqlDocument,
|
|
261
|
+
field: GraphqlField
|
|
262
|
+
): GraphqlExampleResponse => {
|
|
263
|
+
const selections = selectionSet(document, field.type.name);
|
|
264
|
+
const data: GraphqlSampleObject = {};
|
|
265
|
+
if (selections) {
|
|
266
|
+
const value = responseForSelections(document, field.type.name, selections);
|
|
267
|
+
data[field.name] = wrapLists(field.type.display, value);
|
|
268
|
+
} else {
|
|
269
|
+
data[field.name] = sampleForRef(document, field.type, 0);
|
|
270
|
+
}
|
|
271
|
+
return { data };
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The playground/request model for one GraphQL operation: a plain POST whose
|
|
276
|
+
* JSON body carries the query and variables. Reusing `PlaygroundModel` means
|
|
277
|
+
* the OpenAPI playground UI, its client module, and `buildRequest` all work
|
|
278
|
+
* unchanged — the samples show byte-for-byte what the Send button transmits.
|
|
279
|
+
*/
|
|
280
|
+
export const graphqlPlaygroundModel = (
|
|
281
|
+
spec: ApiSpecData,
|
|
282
|
+
query: string,
|
|
283
|
+
variables?: GraphqlSampleObject
|
|
284
|
+
): PlaygroundModel => ({
|
|
285
|
+
auth: [],
|
|
286
|
+
authOptional: true,
|
|
287
|
+
body: {
|
|
288
|
+
contentType: "application/json",
|
|
289
|
+
example: JSON.stringify(
|
|
290
|
+
variables === undefined ? { query } : { query, variables },
|
|
291
|
+
null,
|
|
292
|
+
2
|
|
293
|
+
),
|
|
294
|
+
schema: {
|
|
295
|
+
properties: { query: { type: "string" }, variables: { type: "object" } },
|
|
296
|
+
required: ["query"],
|
|
297
|
+
type: "object",
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
method: "POST",
|
|
301
|
+
params: [],
|
|
302
|
+
path: "",
|
|
303
|
+
// `||`, not `??`: an empty `endpoint: ""` would otherwise survive into the
|
|
304
|
+
// playground and every code sample as a request to an empty URL.
|
|
305
|
+
servers: [spec.endpoint || GRAPHQL_ENDPOINT_PLACEHOLDER],
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Route lookups derived from a spec's full operation list. Each page render
|
|
310
|
+
* would otherwise rebuild them from scratch — O(pages × operations) across a
|
|
311
|
+
* build — so they memoize on the spec object, which the `blume:openapi` data
|
|
312
|
+
* module instantiates once per process.
|
|
313
|
+
*/
|
|
314
|
+
interface GraphqlRouteMaps {
|
|
315
|
+
/** Operation-page routes keyed `kind:fieldName`. */
|
|
316
|
+
operations: Map<string, string>;
|
|
317
|
+
/** Type-page routes keyed by schema name. */
|
|
318
|
+
types: Map<string, string>;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const routeMapsCache = new WeakMap<ApiSpecData, GraphqlRouteMaps>();
|
|
322
|
+
|
|
323
|
+
const routeMaps = (spec: ApiSpecData): GraphqlRouteMaps => {
|
|
324
|
+
let maps = routeMapsCache.get(spec);
|
|
325
|
+
if (!maps) {
|
|
326
|
+
maps = { operations: new Map(), types: new Map() };
|
|
327
|
+
for (const ref of Object.values(spec.operations)) {
|
|
328
|
+
if (ref.operationId === undefined) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (isGraphqlOperationKind(ref.method)) {
|
|
332
|
+
maps.operations.set(`${ref.method}:${ref.operationId}`, ref.route);
|
|
333
|
+
} else {
|
|
334
|
+
// Type pages keyed by bare name: a root field named like a type must
|
|
335
|
+
// not shadow it.
|
|
336
|
+
maps.types.set(ref.operationId, ref.route);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
routeMapsCache.set(spec, maps);
|
|
340
|
+
}
|
|
341
|
+
return maps;
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Routes for every type page of a spec, keyed by its schema name — the link
|
|
346
|
+
* table type references resolve against (a name with no page, like a built-in
|
|
347
|
+
* scalar, renders as plain text).
|
|
348
|
+
*/
|
|
349
|
+
export const graphqlRoutes = (spec: ApiSpecData): Map<string, string> =>
|
|
350
|
+
routeMaps(spec).types;
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Routes for the operation pages, keyed `kind:fieldName` — the link table the
|
|
354
|
+
* "Used by" backlinks resolve against.
|
|
355
|
+
*/
|
|
356
|
+
export const graphqlOperationRoutes = (
|
|
357
|
+
spec: ApiSpecData
|
|
358
|
+
): Map<string, string> => routeMaps(spec).operations;
|
|
359
|
+
|
|
360
|
+
/** Where one named type is used across the schema. */
|
|
361
|
+
export interface GraphqlUsage {
|
|
362
|
+
/** Root fields whose return type or arguments mention the type. */
|
|
363
|
+
operations: { kind: GraphqlOperationKind; name: string }[];
|
|
364
|
+
/** Named types whose fields, arguments, or members mention the type. */
|
|
365
|
+
types: string[];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** The names a field mentions: its own type plus each argument's type. */
|
|
369
|
+
const fieldMentionNames = (field: GraphqlField): Set<string> => {
|
|
370
|
+
const names = new Set([field.type.name]);
|
|
371
|
+
for (const arg of field.args) {
|
|
372
|
+
names.add(arg.type.name);
|
|
373
|
+
}
|
|
374
|
+
return names;
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const usageFor = (
|
|
378
|
+
index: Map<string, GraphqlUsage>,
|
|
379
|
+
name: string
|
|
380
|
+
): GraphqlUsage => {
|
|
381
|
+
let usage = index.get(name);
|
|
382
|
+
if (!usage) {
|
|
383
|
+
usage = { operations: [], types: [] };
|
|
384
|
+
index.set(name, usage);
|
|
385
|
+
}
|
|
386
|
+
return usage;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// Computing one type's backlinks means walking every type's every field and
|
|
390
|
+
// argument, so per-page computation would be quadratic across the build; the
|
|
391
|
+
// whole reverse index costs the same single walk and memoizes on the document
|
|
392
|
+
// object the `blume:openapi` data module instantiates once per process.
|
|
393
|
+
const usageCache = new WeakMap<GraphqlDocument, Map<string, GraphqlUsage>>();
|
|
394
|
+
|
|
395
|
+
const usageIndex = (document: GraphqlDocument): Map<string, GraphqlUsage> => {
|
|
396
|
+
const cached = usageCache.get(document);
|
|
397
|
+
if (cached) {
|
|
398
|
+
return cached;
|
|
399
|
+
}
|
|
400
|
+
const index = new Map<string, GraphqlUsage>();
|
|
401
|
+
const rootNames = new Map<string, GraphqlOperationKind>();
|
|
402
|
+
for (const kind of GRAPHQL_OPERATION_KINDS) {
|
|
403
|
+
const name = document.roots[kind];
|
|
404
|
+
if (name !== undefined) {
|
|
405
|
+
rootNames.set(name, kind);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
for (const type of Object.values(document.types)) {
|
|
409
|
+
const rootKind = rootNames.get(type.name);
|
|
410
|
+
if (rootKind !== undefined) {
|
|
411
|
+
for (const field of type.fields ?? []) {
|
|
412
|
+
for (const name of fieldMentionNames(field)) {
|
|
413
|
+
// Self-references never backlink (`name !== type.name`, here and
|
|
414
|
+
// below) — a type page must not list itself as its own user.
|
|
415
|
+
if (name !== type.name) {
|
|
416
|
+
usageFor(index, name).operations.push({
|
|
417
|
+
kind: rootKind,
|
|
418
|
+
name: field.name,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
const mentioned = new Set<string>();
|
|
426
|
+
for (const field of type.fields ?? []) {
|
|
427
|
+
for (const name of fieldMentionNames(field)) {
|
|
428
|
+
mentioned.add(name);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
for (const input of type.inputFields ?? []) {
|
|
432
|
+
mentioned.add(input.type.name);
|
|
433
|
+
}
|
|
434
|
+
if (type.kind === "union") {
|
|
435
|
+
for (const member of type.possibleTypes ?? []) {
|
|
436
|
+
mentioned.add(member);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
for (const name of mentioned) {
|
|
440
|
+
if (name !== type.name) {
|
|
441
|
+
usageFor(index, name).types.push(type.name);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
usageCache.set(document, index);
|
|
446
|
+
return index;
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Usage backlinks for one type page: the operations that return or accept it,
|
|
451
|
+
* and the other types that reference it (fields, input fields, union
|
|
452
|
+
* membership). Root types are folded into `operations` — their fields are the
|
|
453
|
+
* operation pages.
|
|
454
|
+
*/
|
|
455
|
+
export const graphqlUsage = (
|
|
456
|
+
document: GraphqlDocument,
|
|
457
|
+
target: string
|
|
458
|
+
): GraphqlUsage =>
|
|
459
|
+
usageIndex(document).get(target) ?? { operations: [], types: [] };
|
|
460
|
+
|
|
461
|
+
/** The row shapes the fields table renders: output fields or input values. */
|
|
462
|
+
export type GraphqlFieldRow = GraphqlField | GraphqlInputValue;
|
|
463
|
+
|
|
464
|
+
/** Narrow a table row to an output field (with arguments). */
|
|
465
|
+
export const isOutputField = (row: GraphqlFieldRow): row is GraphqlField =>
|
|
466
|
+
"args" in row;
|
|
@@ -337,6 +337,21 @@ export const initPlayground = (root: HTMLElement): void => {
|
|
|
337
337
|
code.textContent = prettyBody(text);
|
|
338
338
|
pre.append(code);
|
|
339
339
|
region.append(pre);
|
|
340
|
+
// At xl the panel is its own scroll region capped to the viewport, so a
|
|
341
|
+
// response appended under a tall form can land below the panel's fold
|
|
342
|
+
// where nothing brings it into view. Scroll the panel alone — "nearest",
|
|
343
|
+
// by hand — and never the document: below xl the panel does not scroll,
|
|
344
|
+
// and a document scroll would carry the form (Send, the body editor, its
|
|
345
|
+
// errors) off the top on a phone.
|
|
346
|
+
const panel = region.closest<HTMLElement>("[data-operation-panel]");
|
|
347
|
+
if (panel && panel.scrollHeight > panel.clientHeight) {
|
|
348
|
+
const box = panel.getBoundingClientRect();
|
|
349
|
+
const target = region.getBoundingClientRect();
|
|
350
|
+
const delta = Math.min(target.bottom - box.bottom, target.top - box.top);
|
|
351
|
+
if (delta > 0) {
|
|
352
|
+
panel.scrollBy({ top: delta });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
340
355
|
};
|
|
341
356
|
|
|
342
357
|
/**
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { CodeThemes } from "../../markdown/index.ts";
|
|
2
|
+
import { highlightCode } from "../../markdown/index.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The Request-tab rendering shared by the operation renderers (OpenAPI's
|
|
6
|
+
* `RequestPanel`, `AsyncApiOperation`, `GraphqlOperation`): one highlighted
|
|
7
|
+
* panel per code-sample language, so the panel shape and highlight options
|
|
8
|
+
* can't drift between the three. Kept apart from `snippets.ts`, which must
|
|
9
|
+
* stay dependency-free for the browser playground client — this module pulls
|
|
10
|
+
* in Shiki.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** One rendered tab: highlighted HTML plus the `PanelTabs` metadata. */
|
|
14
|
+
export interface SamplePanel {
|
|
15
|
+
html: string;
|
|
16
|
+
key: string;
|
|
17
|
+
label: string;
|
|
18
|
+
lang: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The shape `SampleLanguage` and `AsyncSampleLanguage` share. */
|
|
22
|
+
interface PanelLanguage<Sample> {
|
|
23
|
+
id: string;
|
|
24
|
+
label: string;
|
|
25
|
+
lang: string;
|
|
26
|
+
build: (sample: Sample) => string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Render one highlighted panel per language for a built request sample. */
|
|
30
|
+
export const languageSamplePanels = <Sample>(
|
|
31
|
+
languages: PanelLanguage<Sample>[],
|
|
32
|
+
sample: Sample,
|
|
33
|
+
themes: CodeThemes
|
|
34
|
+
): Promise<SamplePanel[]> =>
|
|
35
|
+
Promise.all(
|
|
36
|
+
languages.map(async (language) => ({
|
|
37
|
+
html: await highlightCode(language.build(sample), language.lang, {
|
|
38
|
+
icons: false,
|
|
39
|
+
themes,
|
|
40
|
+
}),
|
|
41
|
+
key: language.id,
|
|
42
|
+
label: language.label,
|
|
43
|
+
lang: language.id,
|
|
44
|
+
}))
|
|
45
|
+
);
|
|
@@ -46,39 +46,20 @@ const fetchSnippet = (sample: RequestSample): string => {
|
|
|
46
46
|
options.push(` headers: {\n${headers}\n }`);
|
|
47
47
|
}
|
|
48
48
|
if (sample.body) {
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
);
|
|
49
|
+
// Always the raw editor text as a string literal, never re-read as a JS
|
|
50
|
+
// expression: the sample must send byte-for-byte what the live request
|
|
51
|
+
// sends, and an object literal doesn't round-trip every valid JSON
|
|
52
|
+
// document — `{"__proto__":{"x":1}}` sets a prototype instead of a key,
|
|
53
|
+
// and an id past 2^53 loses digits through a JS number. The string
|
|
54
|
+
// literal also stays syntactically valid while the editor holds mid-edit
|
|
55
|
+
// text that isn't JSON yet.
|
|
56
|
+
options.push(` body: ${JSON.stringify(sample.body)}`);
|
|
58
57
|
}
|
|
59
58
|
return `const response = await fetch("${sample.url}", {\n${options.join(
|
|
60
59
|
",\n"
|
|
61
60
|
)}\n});`;
|
|
62
61
|
};
|
|
63
62
|
|
|
64
|
-
// Split-with-capture: odd segments are JSON string literals, kept verbatim so
|
|
65
|
-
// a string *value* containing the words true/false/null isn't rewritten.
|
|
66
|
-
const JSON_STRING = /(?<literal>"(?:\\.|[^"\\])*")/gu;
|
|
67
|
-
|
|
68
|
-
/** Turn a JSON literal into an equivalent Python literal (`true` -> `True`). */
|
|
69
|
-
const toPython = (json: string): string =>
|
|
70
|
-
json
|
|
71
|
-
.split(JSON_STRING)
|
|
72
|
-
.map((part, index) =>
|
|
73
|
-
index % 2 === 1
|
|
74
|
-
? part
|
|
75
|
-
: part
|
|
76
|
-
.replaceAll(/\btrue\b/gu, "True")
|
|
77
|
-
.replaceAll(/\bfalse\b/gu, "False")
|
|
78
|
-
.replaceAll(/\bnull\b/gu, "None")
|
|
79
|
-
)
|
|
80
|
-
.join("");
|
|
81
|
-
|
|
82
63
|
const pythonSnippet = (sample: RequestSample): string => {
|
|
83
64
|
const args = [` "${sample.url}"`];
|
|
84
65
|
if (Object.keys(sample.headers).length > 0) {
|
|
@@ -89,14 +70,11 @@ const pythonSnippet = (sample: RequestSample): string => {
|
|
|
89
70
|
args.push(` headers={\n${headers}\n }`);
|
|
90
71
|
}
|
|
91
72
|
if (sample.body) {
|
|
92
|
-
// Same rule as the fetch snippet:
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
? ` data=${JSON.stringify(sample.body)}`
|
|
98
|
-
: ` json=${toPython(sample.body)}`
|
|
99
|
-
);
|
|
73
|
+
// Same rule as the fetch snippet: the raw text travels as a string via
|
|
74
|
+
// `data=` (JSON string escapes are a subset of Python's, so the literal is
|
|
75
|
+
// valid) rather than a `json=` dict — a Python literal re-serializes
|
|
76
|
+
// `1e400` as `Infinity` and would otherwise diverge from the live send.
|
|
77
|
+
args.push(` data=${JSON.stringify(sample.body)}`);
|
|
100
78
|
}
|
|
101
79
|
return `import requests\n\nresponse = requests.${sample.method.toLowerCase()}(\n${args.join(
|
|
102
80
|
",\n"
|
package/src/core/base-path.ts
CHANGED
|
@@ -62,6 +62,17 @@ export const normalizeRoute = (input: string): string => {
|
|
|
62
62
|
export const isInternalPath = (target: string): boolean =>
|
|
63
63
|
target.startsWith("/") && !target.startsWith("//");
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Whether a rendered link should open in a new tab: an absolute http(s) URL or
|
|
67
|
+
* a protocol-relative one (`//host/path`). Other schemes (`mailto:`, `tel:`)
|
|
68
|
+
* also leave the site but hand off to another application, where a `_blank`
|
|
69
|
+
* target only opens an empty tab beside it. The one predicate behind every
|
|
70
|
+
* chrome link, so a header action and a sidebar featured link treat the same
|
|
71
|
+
* href alike.
|
|
72
|
+
*/
|
|
73
|
+
export const isExternalUrl = (target: string): boolean =>
|
|
74
|
+
/^https?:\/\//iu.test(target) || target.startsWith("//");
|
|
75
|
+
|
|
65
76
|
/**
|
|
66
77
|
* Idempotently prepend `basePath` to a root-relative route. A route already
|
|
67
78
|
* equal to or nested under the base is returned unchanged, so authors who write
|