blume 1.1.3 → 1.2.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 +54 -0
- package/README.md +1 -1
- package/dist/cli/index.js +1473 -149
- package/dist/cli/index.js.map +47 -36
- package/dist/types/core/config-input.d.ts +18 -0
- package/dist/types/core/config.d.ts +4 -0
- package/dist/types/core/data.d.ts +3 -0
- package/dist/types/core/schema.d.ts +132 -17
- package/dist/types/core/types.d.ts +5 -3
- package/dist/types/openapi/references.d.ts +6 -0
- package/docs/advanced/api-reference.mdx +27 -0
- package/docs/advanced/changelog.mdx +10 -0
- package/docs/configuration/ai.mdx +38 -2
- package/docs/configuration/customization.mdx +27 -0
- package/docs/configuration/index.mdx +5 -0
- package/docs/content/navigation.mdx +12 -0
- package/docs/reference/cli.mdx +17 -13
- package/docs/reference/eval.mdx +106 -0
- package/docs/reference/meta.ts +1 -1
- package/package.json +1 -1
- package/src/ai/agent-readability.ts +19 -1
- package/src/ai/llms.ts +9 -4
- package/src/ai/mcp/server.ts +48 -14
- package/src/ai/mcp/stdio.ts +35 -0
- package/src/astro/generate.ts +119 -48
- package/src/astro/templates.ts +173 -37
- package/src/audit/checks/duplicates.ts +15 -6
- package/src/audit/checks/indexability.ts +11 -2
- package/src/audit/checks/network.ts +22 -8
- package/src/audit/checks/sitemap.ts +42 -16
- package/src/audit/redirects.ts +12 -1
- package/src/audit/run.ts +13 -3
- package/src/audit/url.ts +21 -2
- package/src/cli/commands/audit.ts +21 -6
- package/src/cli/commands/dev.ts +19 -2
- package/src/cli/commands/eval.ts +291 -0
- package/src/cli/commands/init.ts +9 -4
- package/src/cli/commands/mcp-stdio.ts +36 -0
- package/src/cli/index.ts +4 -0
- package/src/cli/required-secrets.ts +1 -1
- package/src/components/content/AccordionItem.astro +2 -2
- package/src/components/content/Frame.astro +4 -1
- package/src/components/content/Prompt.astro +4 -1
- package/src/components/content/Tooltip.astro +4 -1
- package/src/components/content/TreeFolder.astro +1 -2
- package/src/components/content/Update.astro +45 -0
- package/src/components/islands/AskAI.astro +9 -2
- package/src/components/islands/ask-ai.tsx +23 -4
- package/src/components/islands/hooks.ts +48 -15
- package/src/components/layout/NavTree.astro +37 -19
- package/src/components/layout/ReferenceLayout.astro +4 -0
- package/src/components/layout/RootLayout.astro +14 -3
- package/src/components/layout/Search.astro +5 -1
- package/src/components/layout/head-scripts.ts +22 -5
- package/src/components/openapi/SchemaProperty.astro +3 -3
- package/src/core/config-input.ts +18 -0
- package/src/core/config.ts +4 -0
- package/src/core/data.ts +3 -0
- package/src/core/deployment-env.ts +7 -2
- package/src/core/graph.ts +8 -1
- package/src/core/i18n.ts +10 -2
- package/src/core/navigation.ts +16 -5
- package/src/core/schema.ts +51 -4
- package/src/core/server-features.ts +1 -1
- package/src/core/sources/normalize.ts +69 -8
- package/src/core/sources/notion.ts +4 -2
- package/src/core/sources/sanity.ts +5 -3
- package/src/core/types.ts +5 -3
- package/src/eval/agents.ts +340 -0
- package/src/eval/findings.ts +103 -0
- package/src/eval/prompts.ts +78 -0
- package/src/eval/report.ts +214 -0
- package/src/eval/run.ts +290 -0
- package/src/eval/schema.ts +124 -0
- package/src/markdown/code-title.ts +7 -1
- package/src/openapi/model.ts +31 -2
- package/src/openapi/references.ts +23 -2
- package/src/openapi/render-mdx.ts +39 -11
- package/src/openapi/scalar.ts +1 -0
- package/src/openapi/source.ts +11 -4
- package/src/registry/eject.ts +23 -1
- package/src/search/build.ts +4 -3
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import { load } from "js-yaml";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
/** Question ids are kebab-case slugs so they read well in reports and CI logs. */
|
|
7
|
+
const ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/u;
|
|
8
|
+
|
|
9
|
+
const questionSchema = z.strictObject({
|
|
10
|
+
expected: z
|
|
11
|
+
.array(z.string().min(1))
|
|
12
|
+
.min(1, "expected must list at least one fact"),
|
|
13
|
+
id: z
|
|
14
|
+
.string()
|
|
15
|
+
.regex(ID_PATTERN, "id must be a kebab-case slug (a-z, 0-9, dashes)"),
|
|
16
|
+
question: z.string().min(1),
|
|
17
|
+
routes: z
|
|
18
|
+
.union([z.string(), z.array(z.string())])
|
|
19
|
+
.default([])
|
|
20
|
+
.transform((value) => (typeof value === "string" ? [value] : value)),
|
|
21
|
+
severity: z.enum(["error", "warning"]).default("error"),
|
|
22
|
+
skip: z.boolean().default(false),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/** One author-written eval: a question plus the facts a passing answer states. */
|
|
26
|
+
export type EvalQuestion = z.infer<typeof questionSchema>;
|
|
27
|
+
|
|
28
|
+
const fullSchema = z.strictObject({
|
|
29
|
+
questions: z.array(questionSchema).min(1),
|
|
30
|
+
version: z.literal(1).default(1),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The evals file schema. A bare top-level list of questions is accepted as
|
|
35
|
+
* shorthand — `loadEvalsFile` wraps it before validating, rather than a
|
|
36
|
+
* `z.union`, so schema errors name the offending field instead of collapsing
|
|
37
|
+
* into an opaque "invalid union" issue.
|
|
38
|
+
*/
|
|
39
|
+
export const evalsFileSchema = fullSchema.superRefine((value, context) => {
|
|
40
|
+
const seen = new Set<string>();
|
|
41
|
+
for (const question of value.questions) {
|
|
42
|
+
if (seen.has(question.id)) {
|
|
43
|
+
context.addIssue({
|
|
44
|
+
code: z.ZodIssueCode.custom,
|
|
45
|
+
message: `duplicate question id "${question.id}"`,
|
|
46
|
+
path: ["questions"],
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
seen.add(question.id);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export type EvalsFile = z.infer<typeof evalsFileSchema>;
|
|
54
|
+
|
|
55
|
+
/** A problem loading or validating the evals file, with the path it names. */
|
|
56
|
+
export class EvalsFileError extends Error {
|
|
57
|
+
readonly path: string;
|
|
58
|
+
|
|
59
|
+
constructor(path: string, message: string) {
|
|
60
|
+
super(message);
|
|
61
|
+
this.name = "EvalsFileError";
|
|
62
|
+
this.path = path;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const describeIssues = (error: z.ZodError): string =>
|
|
67
|
+
error.issues
|
|
68
|
+
.map((issue) => {
|
|
69
|
+
const at = issue.path.length > 0 ? ` at ${issue.path.join(".")}` : "";
|
|
70
|
+
return `${issue.message}${at}`;
|
|
71
|
+
})
|
|
72
|
+
.join("; ");
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Read and validate an evals file. Returns the parsed questions plus the raw
|
|
76
|
+
* text, kept so findings can anchor to the line a question is defined on.
|
|
77
|
+
*/
|
|
78
|
+
export const loadEvalsFile = async (
|
|
79
|
+
path: string
|
|
80
|
+
): Promise<{ evals: EvalsFile; raw: string }> => {
|
|
81
|
+
let raw: string;
|
|
82
|
+
try {
|
|
83
|
+
raw = await readFile(path, "utf-8");
|
|
84
|
+
} catch {
|
|
85
|
+
throw new EvalsFileError(
|
|
86
|
+
path,
|
|
87
|
+
`No evals file found at ${path}. Run \`blume eval init\` to draft one.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let parsed: unknown;
|
|
92
|
+
try {
|
|
93
|
+
parsed = load(raw);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
96
|
+
throw new EvalsFileError(path, `Invalid YAML in ${path}: ${detail}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// The bare-list shorthand: a top-level sequence of questions.
|
|
100
|
+
const candidate = Array.isArray(parsed) ? { questions: parsed } : parsed;
|
|
101
|
+
const result = evalsFileSchema.safeParse(candidate);
|
|
102
|
+
if (!result.success) {
|
|
103
|
+
throw new EvalsFileError(
|
|
104
|
+
path,
|
|
105
|
+
`Invalid evals file at ${path}: ${describeIssues(result.error)}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return { evals: result.data, raw };
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The 1-based line where a question's `id:` entry appears in the raw evals
|
|
113
|
+
* file, so a finding with no route hint can still point somewhere editable.
|
|
114
|
+
*/
|
|
115
|
+
export const locateQuestion = (raw: string, id: string): number | undefined => {
|
|
116
|
+
const pattern = new RegExp(`^\\s*-?\\s*id:\\s*["']?${id}["']?\\s*$`, "u");
|
|
117
|
+
const lines = raw.split("\n");
|
|
118
|
+
for (const [index, line] of lines.entries()) {
|
|
119
|
+
if (pattern.test(line)) {
|
|
120
|
+
return index + 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
};
|
|
@@ -51,7 +51,13 @@ const parseTitle = (raw: string | undefined): string | undefined => {
|
|
|
51
51
|
if (!raw) {
|
|
52
52
|
return undefined;
|
|
53
53
|
}
|
|
54
|
-
|
|
54
|
+
// Blank every *other* quoted attr first, so a `title="…"` embedded in
|
|
55
|
+
// another attribute's value (`caption='set title="X" here'`) can't be
|
|
56
|
+
// promoted to the block title.
|
|
57
|
+
const scrubbed = raw.replace(QUOTED_ATTR, (attr) =>
|
|
58
|
+
attr.startsWith("title=") ? attr : " "
|
|
59
|
+
);
|
|
60
|
+
const explicit = scrubbed.match(TITLE_ATTR);
|
|
55
61
|
const attrTitle = explicit?.groups?.dq ?? explicit?.groups?.sq;
|
|
56
62
|
if (attrTitle) {
|
|
57
63
|
return attrTitle;
|
package/src/openapi/model.ts
CHANGED
|
@@ -101,6 +101,32 @@ export type OpenApiData = Record<string, ApiSpecData>;
|
|
|
101
101
|
const isOperation = (value: unknown): value is OperationObject =>
|
|
102
102
|
typeof value === "object" && value !== null;
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Assign each distinct tag name a unique slug. `slugify` can collapse
|
|
106
|
+
* different names onto one value — any two all-non-ASCII tags (`ペット`,
|
|
107
|
+
* `注文`) both fall through to the `operations` fallback — and a shared slug
|
|
108
|
+
* silently merges the tags' routes, sidebar groups, and overview sections.
|
|
109
|
+
* Collisions gain `-2`, `-3`, … in first-seen order.
|
|
110
|
+
*/
|
|
111
|
+
const tagSlugger = (): ((name: string) => string) => {
|
|
112
|
+
const assigned = new Map<string, string>();
|
|
113
|
+
const taken = new Set<string>();
|
|
114
|
+
return (name) => {
|
|
115
|
+
const existing = assigned.get(name);
|
|
116
|
+
if (existing) {
|
|
117
|
+
return existing;
|
|
118
|
+
}
|
|
119
|
+
const base = slugify(name) || "operations";
|
|
120
|
+
let slug = base;
|
|
121
|
+
for (let suffix = 2; taken.has(slug); suffix += 1) {
|
|
122
|
+
slug = `${base}-${suffix}`;
|
|
123
|
+
}
|
|
124
|
+
taken.add(slug);
|
|
125
|
+
assigned.set(name, slug);
|
|
126
|
+
return slug;
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
104
130
|
/**
|
|
105
131
|
* Flatten a 3.1 document into a route-mapped operation list and its ordered
|
|
106
132
|
* tags. Operations inherit the first tag they declare; keys are de-duplicated so
|
|
@@ -119,6 +145,7 @@ export const extractOperations = (
|
|
|
119
145
|
);
|
|
120
146
|
const seen = new Set<string>();
|
|
121
147
|
const warnings: string[] = [];
|
|
148
|
+
const slugForTag = tagSlugger();
|
|
122
149
|
|
|
123
150
|
for (const [path, rawItem] of Object.entries(document.paths ?? {})) {
|
|
124
151
|
const item = rawItem as PathItemObject | undefined;
|
|
@@ -137,7 +164,7 @@ export const extractOperations = (
|
|
|
137
164
|
continue;
|
|
138
165
|
}
|
|
139
166
|
const tag = operation.tags?.[0] ?? UNTAGGED;
|
|
140
|
-
const tagSlug =
|
|
167
|
+
const tagSlug = slugForTag(tag);
|
|
141
168
|
if (!tagsSeen.has(tag)) {
|
|
142
169
|
tagsSeen.add(tag);
|
|
143
170
|
tagOrder.push(tag);
|
|
@@ -166,7 +193,9 @@ export const extractOperations = (
|
|
|
166
193
|
const tags: ApiTagRef[] = tagOrder.map((name) => ({
|
|
167
194
|
description: tagMeta.get(name) ?? "",
|
|
168
195
|
name,
|
|
169
|
-
|
|
196
|
+
// The same slugger instance, so every tag resolves to the slug its
|
|
197
|
+
// operations were routed under.
|
|
198
|
+
slug: slugForTag(name),
|
|
170
199
|
}));
|
|
171
200
|
|
|
172
201
|
return { operations, tags, warnings };
|
|
@@ -38,6 +38,12 @@ export interface ReferenceSource {
|
|
|
38
38
|
*/
|
|
39
39
|
basePath: string;
|
|
40
40
|
label: string;
|
|
41
|
+
/** Whether generated pages are included in llms.txt/llms-full.txt. */
|
|
42
|
+
includeInLlms: boolean;
|
|
43
|
+
/** Whether generated pages are included in site search. */
|
|
44
|
+
includeInSearch: boolean;
|
|
45
|
+
/** Whether generated pages emit noindex metadata and stay out of the sitemap. */
|
|
46
|
+
noindex: boolean;
|
|
41
47
|
/** Local path or `http(s)` URL, verbatim from config. */
|
|
42
48
|
spec: string;
|
|
43
49
|
/** Per-block Scalar theme name override, if any (Scalar renderer only). */
|
|
@@ -78,10 +84,22 @@ type Block = ResolvedConfig["openapi"] | ResolvedConfig["asyncapi"];
|
|
|
78
84
|
/** A spec is a single source (`spec` shorthand prepended to any `sources`). */
|
|
79
85
|
const sourcesOf = (
|
|
80
86
|
block: Block
|
|
81
|
-
): {
|
|
87
|
+
): {
|
|
88
|
+
includeInLlms: boolean;
|
|
89
|
+
includeInSearch: boolean;
|
|
90
|
+
label?: string;
|
|
91
|
+
noindex: boolean;
|
|
92
|
+
route?: string;
|
|
93
|
+
spec: string;
|
|
94
|
+
}[] => {
|
|
82
95
|
const sources = [...block.sources];
|
|
83
96
|
if (block.spec) {
|
|
84
|
-
sources.unshift({
|
|
97
|
+
sources.unshift({
|
|
98
|
+
includeInLlms: true,
|
|
99
|
+
includeInSearch: true,
|
|
100
|
+
noindex: false,
|
|
101
|
+
spec: block.spec,
|
|
102
|
+
});
|
|
85
103
|
}
|
|
86
104
|
return sources;
|
|
87
105
|
};
|
|
@@ -118,8 +136,11 @@ const referencesFor = (
|
|
|
118
136
|
return {
|
|
119
137
|
basePath,
|
|
120
138
|
display,
|
|
139
|
+
includeInLlms: source.includeInLlms,
|
|
140
|
+
includeInSearch: source.includeInSearch,
|
|
121
141
|
kind,
|
|
122
142
|
label,
|
|
143
|
+
noindex: source.noindex,
|
|
123
144
|
renderer,
|
|
124
145
|
route,
|
|
125
146
|
scalar: block.scalar,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ApiOperationRef, ApiSpecData } from "./model.ts";
|
|
2
|
+
import type { ReferenceSource } from "./references.ts";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Lower a parsed spec into MDX for the staged content source. Each operation and
|
|
@@ -11,13 +12,14 @@ import type { ApiOperationRef, ApiSpecData } from "./model.ts";
|
|
|
11
12
|
* omit their own top heading.
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
|
-
// Neutralize the few characters MDX treats specially (`{` expressions, `<`
|
|
15
|
-
// so an arbitrary spec description can be embedded in the body verbatim
|
|
16
|
-
// breaking compilation. They render as their literal selves.
|
|
17
|
-
|
|
15
|
+
// Neutralize the few characters MDX treats specially (`{` expressions, `<`
|
|
16
|
+
// JSX) so an arbitrary spec description can be embedded in the body verbatim
|
|
17
|
+
// without breaking compilation. They render as their literal selves. `>` is
|
|
18
|
+
// deliberately not escaped: it isn't MDX-special on its own, and escaping it
|
|
19
|
+
// turns a `> Note:` blockquote into literal "> Note:" text.
|
|
20
|
+
const MDX_UNSAFE = /[<{}]/gu;
|
|
18
21
|
const ENTITIES: Record<string, string> = {
|
|
19
22
|
"<": "<",
|
|
20
|
-
">": ">",
|
|
21
23
|
"{": "{",
|
|
22
24
|
"}": "}",
|
|
23
25
|
};
|
|
@@ -28,8 +30,12 @@ const MDX_ESM_KEYWORD = /^(?<keyword>import|export)\b/gmu;
|
|
|
28
30
|
// Backtick code — inline spans and fences alike — is already literal in MDX,
|
|
29
31
|
// and entities are NOT decoded inside it, so escaping there would render the
|
|
30
32
|
// entity text verbatim (`/pets/{petId}`). Matching any balanced
|
|
31
|
-
// backtick run covers `code`, ``code``, and ```fences``` in one shot.
|
|
32
|
-
|
|
33
|
+
// backtick run covers `code`, ``code``, and ```fences``` in one shot. Both
|
|
34
|
+
// runs are pinned by the backtick lookarounds: CommonMark pairs a span only
|
|
35
|
+
// with an *equal-length* run, so without them a lone backtick would "close" on
|
|
36
|
+
// the first backtick of a longer fence run — leaving `{` in the real prose
|
|
37
|
+
// unescaped (a compile error) and escaping entities into the fence body.
|
|
38
|
+
const BACKTICK_CODE = /(?<!`)(?<bt>`+)(?!`)[\s\S]*?(?<!`)\k<bt>(?!`)/gu;
|
|
33
39
|
|
|
34
40
|
const escapeProse = (text: string): string =>
|
|
35
41
|
text
|
|
@@ -121,7 +127,11 @@ const withDescription = (description: string, component: string): string =>
|
|
|
121
127
|
|
|
122
128
|
export const operationMdx = (
|
|
123
129
|
spec: ApiSpecData,
|
|
124
|
-
operation: ApiOperationRef
|
|
130
|
+
operation: ApiOperationRef,
|
|
131
|
+
reference?: Pick<
|
|
132
|
+
ReferenceSource,
|
|
133
|
+
"includeInLlms" | "includeInSearch" | "noindex"
|
|
134
|
+
>
|
|
125
135
|
): RenderedPage => {
|
|
126
136
|
const method = operation.method.toUpperCase();
|
|
127
137
|
const title = operation.summary || `${method} ${operation.path}`;
|
|
@@ -137,9 +147,16 @@ export const operationMdx = (
|
|
|
137
147
|
`<Operation source="${spec.slug}" id="${operation.key}" />`
|
|
138
148
|
),
|
|
139
149
|
data: {
|
|
150
|
+
...(reference?.includeInLlms === false ? { ai: { exclude: true } } : {}),
|
|
140
151
|
...(operation.deprecated ? { deprecated: true } : {}),
|
|
141
|
-
search: {
|
|
142
|
-
|
|
152
|
+
search: {
|
|
153
|
+
...(reference?.includeInSearch === false ? { exclude: true } : {}),
|
|
154
|
+
tags: [operation.tag, method],
|
|
155
|
+
},
|
|
156
|
+
seo: {
|
|
157
|
+
description: operationDescription(spec, operation),
|
|
158
|
+
...(reference?.noindex ? { noindex: true } : {}),
|
|
159
|
+
},
|
|
143
160
|
sidebar: { badge: method, label: operation.summary || operation.path },
|
|
144
161
|
title,
|
|
145
162
|
// Signals the two-column API layout (request panel instead of the TOC).
|
|
@@ -148,7 +165,13 @@ export const operationMdx = (
|
|
|
148
165
|
};
|
|
149
166
|
};
|
|
150
167
|
|
|
151
|
-
export const overviewMdx = (
|
|
168
|
+
export const overviewMdx = (
|
|
169
|
+
spec: ApiSpecData,
|
|
170
|
+
reference?: Pick<
|
|
171
|
+
ReferenceSource,
|
|
172
|
+
"includeInLlms" | "includeInSearch" | "noindex"
|
|
173
|
+
>
|
|
174
|
+
): RenderedPage => {
|
|
152
175
|
// Tag sections: declared tags in spec order, then any tag an operation
|
|
153
176
|
// references that isn't declared under `tags`. The section headings are
|
|
154
177
|
// emitted as real markdown `##` (not markup inside a component) so the
|
|
@@ -200,10 +223,15 @@ export const overviewMdx = (spec: ApiSpecData): RenderedPage => {
|
|
|
200
223
|
...tagSections,
|
|
201
224
|
].join("\n\n"),
|
|
202
225
|
data: {
|
|
226
|
+
...(reference?.includeInLlms === false ? { ai: { exclude: true } } : {}),
|
|
227
|
+
...(reference?.includeInSearch === false
|
|
228
|
+
? { search: { exclude: true } }
|
|
229
|
+
: {}),
|
|
203
230
|
seo: {
|
|
204
231
|
description:
|
|
205
232
|
clip(plainProse(spec.description), META_DESCRIPTION_MAX) ||
|
|
206
233
|
`${apiName(spec)} API reference.`,
|
|
234
|
+
...(reference?.noindex ? { noindex: true } : {}),
|
|
207
235
|
},
|
|
208
236
|
sidebar: { label: "Overview" },
|
|
209
237
|
title: apiName(spec),
|
package/src/openapi/scalar.ts
CHANGED
package/src/openapi/source.ts
CHANGED
|
@@ -53,17 +53,24 @@ const toEntry = (rendered: RenderedPage, ref: string): SourceEntry => {
|
|
|
53
53
|
/** All staged entries for one spec: operations first, overview last. */
|
|
54
54
|
const specEntries = (
|
|
55
55
|
spec: ApiSpecData,
|
|
56
|
-
operations: ApiOperationRef[]
|
|
56
|
+
operations: ApiOperationRef[],
|
|
57
|
+
reference: ReferenceSource
|
|
57
58
|
): SourceEntry[] => {
|
|
58
59
|
const entries = operations.map((operation) =>
|
|
59
|
-
toEntry(
|
|
60
|
+
toEntry(
|
|
61
|
+
operationMdx(spec, operation, reference),
|
|
62
|
+
`${routeToRef(operation.route)}.mdx`
|
|
63
|
+
)
|
|
60
64
|
);
|
|
61
65
|
// Overview last so an operation sets the section's routePath before the index
|
|
62
66
|
// page is inserted (the group's routePath is derived from its first child).
|
|
63
67
|
// A root-mounted reference refs `index.mdx`, not `/index.mdx`.
|
|
64
68
|
const base = routeToRef(spec.route);
|
|
65
69
|
entries.push(
|
|
66
|
-
toEntry(
|
|
70
|
+
toEntry(
|
|
71
|
+
overviewMdx(spec, reference),
|
|
72
|
+
base ? `${base}/index.mdx` : "index.mdx"
|
|
73
|
+
)
|
|
67
74
|
);
|
|
68
75
|
return entries;
|
|
69
76
|
};
|
|
@@ -149,7 +156,7 @@ export const openApiSource = (
|
|
|
149
156
|
]
|
|
150
157
|
: []),
|
|
151
158
|
],
|
|
152
|
-
entries: specEntries(spec, operations),
|
|
159
|
+
entries: specEntries(spec, operations, reference),
|
|
153
160
|
slug: reference.slug,
|
|
154
161
|
spec,
|
|
155
162
|
};
|
package/src/registry/eject.ts
CHANGED
|
@@ -112,7 +112,15 @@ const askFiles = async (
|
|
|
112
112
|
genDir: string
|
|
113
113
|
): Promise<{ content: string; path: string }[]> => {
|
|
114
114
|
const { ask } = project.config.ai;
|
|
115
|
-
if (!ask?.enabled) {
|
|
115
|
+
if (!(ask?.enabled && !ask.endpoint)) {
|
|
116
|
+
const endpointPath = join(srcDir, "pages", "api", "ask.ts");
|
|
117
|
+
if (existsSync(endpointPath)) {
|
|
118
|
+
const content = await readFile(endpointPath, "utf-8");
|
|
119
|
+
if (content.startsWith("// Generated by Blume. Do not edit.")) {
|
|
120
|
+
await rm(endpointPath, { force: true });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
await rm(join(genDir, "ask-data.json"), { force: true });
|
|
116
124
|
return [];
|
|
117
125
|
}
|
|
118
126
|
const grounded = ask.provider !== "inkeep";
|
|
@@ -271,6 +279,15 @@ const examplesPreviewFiles = (
|
|
|
271
279
|
]
|
|
272
280
|
: [];
|
|
273
281
|
|
|
282
|
+
const ejectIntegrationBridge = (
|
|
283
|
+
config: BlumeProject["config"],
|
|
284
|
+
root: string,
|
|
285
|
+
configFile: string | null
|
|
286
|
+
): Parameters<typeof astroConfigTemplate>[0]["integrationBridge"] =>
|
|
287
|
+
config.integrations.length > 0 && configFile
|
|
288
|
+
? { configFile: toPosix(relative(root, configFile)) }
|
|
289
|
+
: undefined;
|
|
290
|
+
|
|
274
291
|
/**
|
|
275
292
|
* Promote the generated runtime into the project as an owned Astro app. After
|
|
276
293
|
* eject the project has a normal `astro.config.mjs` and `src/`, the `blume` CLI
|
|
@@ -367,6 +384,11 @@ export const eject = async (
|
|
|
367
384
|
dataPath: "./src/generated/data.json",
|
|
368
385
|
examplesPath: "./src/generated/examples.ts",
|
|
369
386
|
examplesThemePath: "./src/generated/examples.css",
|
|
387
|
+
integrationBridge: ejectIntegrationBridge(
|
|
388
|
+
config,
|
|
389
|
+
root,
|
|
390
|
+
context.configFile
|
|
391
|
+
),
|
|
370
392
|
needsReact,
|
|
371
393
|
needsSvelte,
|
|
372
394
|
needsVue,
|
package/src/search/build.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { join } from "pathe";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Build a local Pagefind search index over the built site. Pagefind
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Build a local Pagefind search index over the built site. Pagefind indexes
|
|
5
|
+
* every rendered page except those whose `<html>` carries
|
|
6
|
+
* `data-pagefind-ignore`, which Blume stamps on non-indexable pages
|
|
7
|
+
* (search-excluded, or hidden without the opt-in), so those stay out.
|
|
7
8
|
*
|
|
8
9
|
* Returns the number of pages indexed.
|
|
9
10
|
*/
|