blume 0.6.7 → 0.7.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/dist/cli/index.js +1179 -738
- package/dist/cli/index.js.map +52 -51
- package/dist/types/core/base-path.d.ts +38 -0
- package/dist/types/core/config-input.d.ts +74 -10
- package/dist/types/core/config.d.ts +3 -2
- package/dist/types/core/data.d.ts +2 -0
- package/dist/types/core/i18n-ui.d.ts +1 -3
- package/dist/types/core/schema.d.ts +95 -52
- package/dist/types/core/sources/types.d.ts +2 -0
- package/dist/types/core/types.d.ts +6 -1
- package/docs/02-deployment.mdx +16 -1
- package/docs/03-faq.mdx +8 -8
- package/docs/configuration/index.mdx +6 -0
- package/docs/content/components.mdx +29 -2
- package/docs/content/islands.mdx +8 -0
- package/docs/content/syntax.mdx +13 -0
- package/package.json +2 -1
- package/src/ai/agent-readability.ts +7 -2
- package/src/ai/ask.ts +12 -7
- package/src/ai/llms.ts +15 -4
- package/src/ai/mcp/data.ts +8 -4
- package/src/ai/mcp/server.ts +3 -0
- package/src/astro/component-slots.ts +5 -3
- package/src/astro/examples.ts +12 -7
- package/src/astro/generate.ts +317 -144
- package/src/astro/index.ts +5 -1
- package/src/astro/integration.ts +8 -4
- package/src/astro/islands.ts +11 -5
- package/src/astro/markdown-negotiation.ts +1 -1
- package/src/astro/pages.ts +8 -3
- package/src/astro/templates.ts +166 -19
- package/src/cli/commands/build.ts +32 -19
- package/src/cli/commands/dev.ts +48 -15
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/validate.ts +1 -0
- package/src/cli/dev-lock.ts +26 -15
- package/src/cli/required-secrets.ts +2 -1
- package/src/components/content/CodeBlock.astro +3 -0
- package/src/components/content/Component.astro +30 -16
- package/src/components/content/Diff.astro +3 -1
- package/src/components/content/auto-type-table.ts +18 -8
- package/src/components/content/diff.ts +12 -6
- package/src/components/content/mermaid-element.ts +3 -0
- package/src/components/index.ts +23 -1
- package/src/components/islands/ask-ai.tsx +12 -6
- package/src/components/islands/base-path.ts +28 -0
- package/src/components/islands/hooks.ts +16 -1
- package/src/components/layout/Banner.astro +2 -1
- package/src/components/layout/Breadcrumbs.astro +2 -1
- package/src/components/layout/Favicon.astro +3 -2
- package/src/components/layout/Header.astro +2 -1
- package/src/components/layout/LanguageSwitcher.astro +2 -1
- package/src/components/layout/Logo.astro +2 -1
- package/src/components/layout/NavSelector.astro +2 -1
- package/src/components/layout/NavTree.astro +5 -4
- package/src/components/layout/PageFeedback.astro +4 -1
- package/src/components/layout/PageLayout.astro +9 -4
- package/src/components/layout/Pagination.astro +3 -2
- package/src/components/layout/RootLayout.astro +7 -4
- package/src/components/layout/Search.astro +13 -5
- package/src/components/layout/nav-utils.ts +18 -10
- package/src/components/layout/search/pagefind.ts +3 -0
- package/src/components/layout/toc-element.ts +7 -1
- package/src/components/openapi/RequestPanel.astro +7 -1
- package/src/components/openapi/snippets.ts +25 -11
- package/src/core/base-path.ts +70 -0
- package/src/core/component-overrides.ts +103 -74
- package/src/core/config-input.ts +81 -15
- package/src/core/config.ts +5 -3
- package/src/core/content.ts +2 -0
- package/src/core/data.ts +2 -0
- package/src/core/diagnostics.ts +54 -34
- package/src/core/gitignore.ts +4 -1
- package/src/core/graph.ts +156 -88
- package/src/core/i18n-ui.ts +18 -3
- package/src/core/last-modified.ts +2 -0
- package/src/core/links.ts +38 -18
- package/src/core/manifest.ts +62 -45
- package/src/core/nav-diagnostics.ts +1 -1
- package/src/core/navigation.ts +116 -55
- package/src/core/project-graph.ts +10 -9
- package/src/core/schema.ts +572 -621
- package/src/core/sources/github-releases.ts +2 -1
- package/src/core/sources/mdx-remote.ts +58 -54
- package/src/core/sources/normalize.ts +116 -73
- package/src/core/sources/notion.ts +19 -10
- package/src/core/sources/types.ts +2 -0
- package/src/core/tsconfig-aliases.ts +59 -30
- package/src/core/types.ts +6 -1
- package/src/deploy/redirects.ts +18 -0
- package/src/deploy/robots.ts +6 -1
- package/src/deploy/rss.ts +10 -3
- package/src/deploy/sitemap.ts +14 -10
- package/src/markdown/base-links.ts +58 -0
- package/src/markdown/code-title.ts +11 -14
- package/src/markdown/index.ts +34 -9
- package/src/markdown/inline-code.ts +7 -2
- package/src/markdown/themes.ts +24 -0
- package/src/openapi/model.ts +3 -1
- package/src/openapi/references.ts +41 -17
- package/src/openapi/render-mdx.ts +11 -6
- package/src/openapi/scalar.ts +32 -16
- package/src/registry/eject.ts +64 -8
- package/src/search/build.ts +3 -0
- package/src/search/documents.ts +2 -2
- package/src/search/sync/typesense.ts +6 -4
- package/src/seo/jsonld.ts +16 -6
- package/src/theme/entry.ts +85 -20
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
import { EN_UI } from "../../core/i18n-ui.ts";
|
|
26
26
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
27
27
|
import type { LocaleSwitchOption, Navigation } from "../../core/types.ts";
|
|
28
|
+
import { withBase } from "../islands/base-path.ts";
|
|
28
29
|
import "blume:theme";
|
|
29
30
|
import Analytics from "./Analytics.astro";
|
|
30
31
|
import Banner from "./Banner.astro";
|
|
@@ -121,18 +122,22 @@ const route = page?.route ?? "/";
|
|
|
121
122
|
// (`/og/<slug>.png`, `index` for the home) is generated for static custom pages
|
|
122
123
|
// when `og.enabled`. Explicit props still win.
|
|
123
124
|
const ogSlug = route === "/" ? "index" : route.slice(1);
|
|
125
|
+
// Absolute URLs carry the deployment base too (`site + base + route`), since the
|
|
126
|
+
// page is served under it. `withBase` handles a root base as a no-op.
|
|
127
|
+
const basedRoute = withBase(route);
|
|
124
128
|
const resolvedCanonical =
|
|
125
|
-
canonical ??
|
|
129
|
+
canonical ??
|
|
130
|
+
(siteUrl ? `${siteUrl}${basedRoute === "/" ? "" : basedRoute}` : null);
|
|
126
131
|
// An explicit `ogImage` wins. A root-relative path (e.g. an image dropped in
|
|
127
132
|
// `public/`) is resolved against the site URL so crawlers get an absolute
|
|
128
133
|
// `og:image`; an already-absolute URL passes through untouched. Otherwise fall
|
|
129
134
|
// back to the generated OG card for this route.
|
|
130
135
|
const absolutizeOgImage = (value: string): string =>
|
|
131
|
-
value.startsWith("/") && siteUrl ? `${siteUrl}${value}` : value;
|
|
136
|
+
value.startsWith("/") && siteUrl ? `${siteUrl}${withBase(value)}` : value;
|
|
132
137
|
const resolvedOgImage = ogImage
|
|
133
138
|
? absolutizeOgImage(ogImage)
|
|
134
139
|
: ogEnabled && siteUrl
|
|
135
|
-
? `${siteUrl}
|
|
140
|
+
? `${siteUrl}${withBase(`/og/${ogSlug}.png`)}`
|
|
136
141
|
: null;
|
|
137
142
|
|
|
138
143
|
const initialThemeScript = themeInitScript(themeMode);
|
|
@@ -215,7 +220,7 @@ const bannerScript = banner?.dismissible
|
|
|
215
220
|
: undefined
|
|
216
221
|
}
|
|
217
222
|
class="block rounded-[0.65rem] px-2.5 py-1.5 font-medium text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:bg-muted aria-[current=page]:text-foreground"
|
|
218
|
-
href={tab.path}
|
|
223
|
+
href={withBase(tab.path)}
|
|
219
224
|
>
|
|
220
225
|
{tab.label}
|
|
221
226
|
</a>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { withBase } from "../islands/base-path.ts";
|
|
2
3
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
3
4
|
import type { FlatPage } from "./nav-utils.ts";
|
|
4
5
|
import Icon from "../Icon.astro";
|
|
@@ -24,7 +25,7 @@ const { prev, next, strings } = Astro.props;
|
|
|
24
25
|
{prev ? (
|
|
25
26
|
<a
|
|
26
27
|
class="flex max-w-[48%] flex-1 items-center gap-2 rounded-full border border-border px-4 py-3 text-foreground transition-colors hover:border-foreground max-md:max-w-full"
|
|
27
|
-
href={prev.route}
|
|
28
|
+
href={withBase(prev.route)}
|
|
28
29
|
>
|
|
29
30
|
<Icon class="rtl:-scale-x-100" name="arrow-left" size={16} />
|
|
30
31
|
<span>
|
|
@@ -40,7 +41,7 @@ const { prev, next, strings } = Astro.props;
|
|
|
40
41
|
{next && (
|
|
41
42
|
<a
|
|
42
43
|
class="ms-auto flex max-w-[48%] flex-1 items-center justify-end gap-2 rounded-full border border-border px-4 py-3 text-end text-foreground transition-colors hover:border-foreground max-md:max-w-full"
|
|
43
|
-
href={next.route}
|
|
44
|
+
href={withBase(next.route)}
|
|
44
45
|
>
|
|
45
46
|
<span>
|
|
46
47
|
<span class="block text-muted-foreground text-xs max-md:hidden">
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
import "blume:theme";
|
|
11
11
|
import type { ComponentOverride } from "../../core/define-components.ts";
|
|
12
12
|
import { buildStructuredData } from "../../seo/jsonld.ts";
|
|
13
|
+
import { withBase } from "../islands/base-path.ts";
|
|
13
14
|
import Analytics from "./Analytics.astro";
|
|
14
15
|
import Banner from "./Banner.astro";
|
|
15
16
|
import Breadcrumbs from "./Breadcrumbs.astro";
|
|
@@ -250,7 +251,8 @@ const formattedLastModified =
|
|
|
250
251
|
|
|
251
252
|
// The hosted MCP server's absolute URL, used by the page-actions install menu.
|
|
252
253
|
// Needs a configured site to be useful, so the menu is hidden without one.
|
|
253
|
-
const mcpUrl =
|
|
254
|
+
const mcpUrl =
|
|
255
|
+
mcp && siteUrl ? new URL(withBase(mcp.route), siteUrl).href : null;
|
|
254
256
|
|
|
255
257
|
// Scope the sidebar (and the breadcrumbs/pagination derived from it) to the
|
|
256
258
|
// active tab's section, so a multi-section site drills each tab into its own
|
|
@@ -266,6 +268,7 @@ const structuredData =
|
|
|
266
268
|
structuredDataEnabled === false || noindex
|
|
267
269
|
? null
|
|
268
270
|
: buildStructuredData({
|
|
271
|
+
base: import.meta.env.BASE_URL,
|
|
269
272
|
breadcrumbs: crumbs,
|
|
270
273
|
description,
|
|
271
274
|
locale,
|
|
@@ -318,7 +321,7 @@ const bannerScript = banner?.dismissible
|
|
|
318
321
|
{
|
|
319
322
|
feeds?.map((feed) => (
|
|
320
323
|
<link
|
|
321
|
-
href={feed.href}
|
|
324
|
+
href={withBase(feed.href)}
|
|
322
325
|
rel="alternate"
|
|
323
326
|
title={feed.title}
|
|
324
327
|
type="application/rss+xml"
|
|
@@ -390,7 +393,7 @@ const bannerScript = banner?.dismissible
|
|
|
390
393
|
<li>
|
|
391
394
|
<a
|
|
392
395
|
class="block rounded-[0.65rem] px-2.5 py-1.5 text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground"
|
|
393
|
-
href={link.href}
|
|
396
|
+
href={withBase(link.href)}
|
|
394
397
|
rel={external ? "noreferrer" : undefined}
|
|
395
398
|
target={external ? "_blank" : undefined}
|
|
396
399
|
>
|
|
@@ -445,7 +448,7 @@ const bannerScript = banner?.dismissible
|
|
|
445
448
|
: undefined
|
|
446
449
|
}
|
|
447
450
|
class="block rounded-[0.65rem] px-2.5 py-1.5 font-medium text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:bg-muted aria-[current=page]:text-foreground"
|
|
448
|
-
href={tab.path}
|
|
451
|
+
href={withBase(tab.path)}
|
|
449
452
|
>
|
|
450
453
|
{tab.label}
|
|
451
454
|
</a>
|
|
@@ -19,7 +19,10 @@ interface Props {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const { askEnabled = false, navigation, strings, locale } = Astro.props;
|
|
22
|
-
|
|
22
|
+
// Merge over the English baseline per key (rather than `strings ?? …`) so a
|
|
23
|
+
// partial — or empty `{}` — strings object still resolves every label to a
|
|
24
|
+
// default, matching the pattern PageActions uses for its own dictionary.
|
|
25
|
+
const s = { ...EN_UI.search, ...strings };
|
|
23
26
|
|
|
24
27
|
// Pages shown in the empty state, before the user has typed anything.
|
|
25
28
|
const popular = navigation ? flattenPages(navigation.sidebar).slice(0, 6) : [];
|
|
@@ -139,6 +142,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
139
142
|
|
|
140
143
|
<script>
|
|
141
144
|
import { chromeIcons as icons } from "../../theme/chrome-icons.ts";
|
|
145
|
+
import { prefixBase } from "../islands/base-path.ts";
|
|
142
146
|
import {
|
|
143
147
|
escapeHtml,
|
|
144
148
|
highlight,
|
|
@@ -475,7 +479,10 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
475
479
|
|
|
476
480
|
createHitRow(hit: SearchHit, query: string): Selectable {
|
|
477
481
|
const el = document.createElement("a");
|
|
478
|
-
|
|
482
|
+
// Index URLs are base-less logical routes; prefix the deployment base so
|
|
483
|
+
// clicking a result lands on the page's real served URL.
|
|
484
|
+
const href = prefixBase(import.meta.env.BASE_URL, hit.url);
|
|
485
|
+
el.href = href;
|
|
479
486
|
el.className = `${ROW_CLASS} ${MARK}`;
|
|
480
487
|
// `line-clamp-2` already sets `display`, so no `block` here (it would
|
|
481
488
|
// override the clamp and let the excerpt run to full height).
|
|
@@ -488,21 +495,22 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
488
495
|
<span class="block truncate font-normal text-foreground text-sm">${hit.title}</span>
|
|
489
496
|
${excerpt}
|
|
490
497
|
</span>`;
|
|
491
|
-
const item: Selectable = { el, hit, kind: "link", url:
|
|
498
|
+
const item: Selectable = { el, hit, kind: "link", url: href };
|
|
492
499
|
this.bindRow(item);
|
|
493
500
|
return item;
|
|
494
501
|
}
|
|
495
502
|
|
|
496
503
|
createLinkRow(url: string, label: string): Selectable {
|
|
497
504
|
const el = document.createElement("a");
|
|
498
|
-
|
|
505
|
+
const href = prefixBase(import.meta.env.BASE_URL, url);
|
|
506
|
+
el.href = href;
|
|
499
507
|
el.className = ROW_CLASS;
|
|
500
508
|
el.innerHTML = `
|
|
501
509
|
<span class="mt-0.5 shrink-0 text-muted-foreground">${svg("file")}</span>
|
|
502
510
|
<span class="flex-1">
|
|
503
511
|
<span class="block truncate font-normal text-foreground text-sm">${escapeHtml(label)}</span>
|
|
504
512
|
</span>`;
|
|
505
|
-
const item: Selectable = { el, kind: "link", url };
|
|
513
|
+
const item: Selectable = { el, kind: "link", url: href };
|
|
506
514
|
this.bindRow(item);
|
|
507
515
|
return item;
|
|
508
516
|
}
|
|
@@ -119,10 +119,14 @@ const sectionChildren = (nodes: NavNode[], base: string): NavNode[] | null => {
|
|
|
119
119
|
};
|
|
120
120
|
|
|
121
121
|
/** Whether a group maps to a header tab (matched on its path or link route). */
|
|
122
|
-
const isTabSection = (node: NavNode, tabPaths: Set<string>): boolean =>
|
|
123
|
-
node.kind
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
const isTabSection = (node: NavNode, tabPaths: Set<string>): boolean => {
|
|
123
|
+
if (node.kind !== "group") {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const byPath = node.path !== undefined && tabPaths.has(node.path);
|
|
127
|
+
const byRoute = node.route !== undefined && tabPaths.has(node.route);
|
|
128
|
+
return byPath || byRoute;
|
|
129
|
+
};
|
|
126
130
|
|
|
127
131
|
/**
|
|
128
132
|
* Drop the groups that already own a header tab from the tree, at any depth —
|
|
@@ -132,9 +136,12 @@ const isTabSection = (node: NavNode, tabPaths: Set<string>): boolean =>
|
|
|
132
136
|
* (`/`) spans everything, so it never removes anything.
|
|
133
137
|
*/
|
|
134
138
|
const withoutTabSections = (nodes: NavNode[], tabs: NavTab[]): NavNode[] => {
|
|
135
|
-
const tabPaths = new Set(
|
|
136
|
-
|
|
137
|
-
|
|
139
|
+
const tabPaths = new Set<string>();
|
|
140
|
+
for (const tab of tabs) {
|
|
141
|
+
if (tab.path !== "/") {
|
|
142
|
+
tabPaths.add(tab.path);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
138
145
|
if (tabPaths.size === 0) {
|
|
139
146
|
return nodes;
|
|
140
147
|
}
|
|
@@ -145,11 +152,12 @@ const withoutTabSections = (nodes: NavNode[], tabs: NavTab[]): NavNode[] => {
|
|
|
145
152
|
continue;
|
|
146
153
|
}
|
|
147
154
|
if (item.kind === "group") {
|
|
155
|
+
// A container left empty by pruning is dropped, so no bare heading is
|
|
156
|
+
// stranded.
|
|
148
157
|
const children = prune(item.children);
|
|
149
|
-
if (children.length
|
|
150
|
-
|
|
158
|
+
if (children.length > 0) {
|
|
159
|
+
kept.push({ ...item, children });
|
|
151
160
|
}
|
|
152
|
-
kept.push({ ...item, children });
|
|
153
161
|
} else {
|
|
154
162
|
kept.push(item);
|
|
155
163
|
}
|
|
@@ -21,6 +21,9 @@ interface PagefindModule {
|
|
|
21
21
|
export const createSearch = async (opts: {
|
|
22
22
|
url: string;
|
|
23
23
|
}): Promise<SearchFn> => {
|
|
24
|
+
// The pagefind bundle lives in the built site (not node_modules) and is
|
|
25
|
+
// resolved at runtime by URL — it can't be a static, code-splittable path.
|
|
26
|
+
// oxlint-disable-next-line react-doctor/no-dynamic-import-path
|
|
24
27
|
const pagefind = (await import(
|
|
25
28
|
/* @vite-ignore */
|
|
26
29
|
opts.url
|
|
@@ -52,7 +52,13 @@ class BlumeToc extends HTMLElement {
|
|
|
52
52
|
for (const { heading } of this.#entries) {
|
|
53
53
|
this.#observer.observe(heading);
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
// The scroll listener is the intentional fallback for the one case the
|
|
56
|
+
// IntersectionObserver can't cover (a final section too short to push its
|
|
57
|
+
// heading past the trigger line), so replacing it with an observer would
|
|
58
|
+
// change behavior. `scroll` isn't cancelable, so it never preventDefaults
|
|
59
|
+
// and `{ passive: true }` would be a no-op here.
|
|
60
|
+
// oxlint-disable-next-line github/prefer-observers, react-doctor/client-passive-event-listeners
|
|
61
|
+
window.addEventListener("scroll", this.#onScroll);
|
|
56
62
|
this.#update();
|
|
57
63
|
}
|
|
58
64
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
+
import data from "blume:data";
|
|
3
|
+
|
|
2
4
|
import { highlightCode } from "../../markdown/index.ts";
|
|
3
5
|
import { exampleValue, type SchemaLike, toJson } from "./helpers.ts";
|
|
4
6
|
import type { RequestSample, SampleLanguage } from "./snippets.ts";
|
|
@@ -35,6 +37,7 @@ const requestSamples = await Promise.all(
|
|
|
35
37
|
languages.map(async (language) => ({
|
|
36
38
|
html: await highlightCode(language.build(sample), language.lang, {
|
|
37
39
|
icons: false,
|
|
40
|
+
themes: data.config.codeThemes,
|
|
38
41
|
}),
|
|
39
42
|
id: language.id,
|
|
40
43
|
label: language.label,
|
|
@@ -53,7 +56,10 @@ const responseEntries = await Promise.all(
|
|
|
53
56
|
const html =
|
|
54
57
|
example === undefined || example === null
|
|
55
58
|
? null
|
|
56
|
-
: await highlightCode(toJson(example), "json", {
|
|
59
|
+
: await highlightCode(toJson(example), "json", {
|
|
60
|
+
icons: false,
|
|
61
|
+
themes: data.config.codeThemes,
|
|
62
|
+
});
|
|
57
63
|
return { description: response.description ?? "", html, status };
|
|
58
64
|
})
|
|
59
65
|
);
|
|
@@ -43,6 +43,26 @@ const jsonContentType = (
|
|
|
43
43
|
return entries.find(([type]) => type.includes("json")) ?? entries[0];
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
/** The `?a=1&b=2` query string from an operation's required query params. */
|
|
47
|
+
const queryString = (
|
|
48
|
+
params: ParamLike[],
|
|
49
|
+
schemas: Record<string, SchemaLike>
|
|
50
|
+
): string => {
|
|
51
|
+
const query: string[] = [];
|
|
52
|
+
for (const param of params) {
|
|
53
|
+
if (!(param.in === "query" && param.required && param.name)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const value = param.example ?? exampleValue(param.schema, schemas);
|
|
57
|
+
query.push(
|
|
58
|
+
`${encodeURIComponent(param.name)}=${encodeURIComponent(
|
|
59
|
+
String(value ?? "")
|
|
60
|
+
)}`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return query.length > 0 ? `?${query.join("&")}` : "";
|
|
64
|
+
};
|
|
65
|
+
|
|
46
66
|
/** Assemble a representative request from an operation and the spec servers. */
|
|
47
67
|
export const buildRequestSample = (
|
|
48
68
|
operation: OperationLike,
|
|
@@ -65,15 +85,7 @@ export const buildRequestSample = (
|
|
|
65
85
|
}
|
|
66
86
|
}
|
|
67
87
|
|
|
68
|
-
const
|
|
69
|
-
.filter((param) => param.in === "query" && param.required && param.name)
|
|
70
|
-
.map((param) => {
|
|
71
|
-
const value = param.example ?? exampleValue(param.schema, schemas);
|
|
72
|
-
return `${encodeURIComponent(param.name ?? "")}=${encodeURIComponent(
|
|
73
|
-
String(value ?? "")
|
|
74
|
-
)}`;
|
|
75
|
-
});
|
|
76
|
-
const search = query.length > 0 ? `?${query.join("&")}` : "";
|
|
88
|
+
const search = queryString(params, schemas);
|
|
77
89
|
|
|
78
90
|
const headers: Record<string, string> = {};
|
|
79
91
|
for (const param of params) {
|
|
@@ -117,7 +129,8 @@ const curlSnippet = (sample: RequestSample): string => {
|
|
|
117
129
|
if (sample.body) {
|
|
118
130
|
// Close-quote/escaped-quote/reopen: the POSIX way to put a literal ' in a
|
|
119
131
|
// single-quoted string, so an example like "it's" doesn't break the shell.
|
|
120
|
-
|
|
132
|
+
const escapedBody = sample.body.replaceAll("'", String.raw`'\''`);
|
|
133
|
+
lines.push(` -d '${escapedBody}'`);
|
|
121
134
|
}
|
|
122
135
|
return lines.join(" \\\n");
|
|
123
136
|
};
|
|
@@ -200,11 +213,12 @@ const ALIASES: Record<string, string> = {
|
|
|
200
213
|
/** The sample languages to render, resolved from config ids (unknown ids dropped). */
|
|
201
214
|
export const sampleLanguages = (ids: string[]): SampleLanguage[] => {
|
|
202
215
|
const wanted = ids.length > 0 ? ids : ["curl", "js", "python"];
|
|
216
|
+
const byId = new Map(LANGUAGES.map((entry) => [entry.id, entry]));
|
|
203
217
|
const out: SampleLanguage[] = [];
|
|
204
218
|
const seen = new Set<string>();
|
|
205
219
|
for (const raw of wanted) {
|
|
206
220
|
const id = ALIASES[raw.toLowerCase()] ?? raw.toLowerCase();
|
|
207
|
-
const language =
|
|
221
|
+
const language = byId.get(id);
|
|
208
222
|
if (language && !seen.has(id)) {
|
|
209
223
|
seen.add(id);
|
|
210
224
|
out.push(language);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The site-wide `basePath` — a mount point that prepends a segment to every
|
|
3
|
+
* generated route (`/docs/getting-started`) while staying invisible to the
|
|
4
|
+
* navigation tree (see `core/navigation.ts`, which builds groups from a page's
|
|
5
|
+
* base-less `navPath` and takes URLs from its based `route`). This is distinct
|
|
6
|
+
* from a per-source `prefix` (a namespace that *does* create a sidebar group)
|
|
7
|
+
* and from `deployment.base` (Astro's host-subdirectory base); the two compose,
|
|
8
|
+
* stacking as `{deployment.base}/{basePath}/page`.
|
|
9
|
+
*
|
|
10
|
+
* These helpers run server-side (config, route construction, the link checker,
|
|
11
|
+
* redirects, the markdown link plugin). The client-side counterpart lives in
|
|
12
|
+
* `components/islands/base-path.ts` and serves `deployment.base` via `BASE_URL`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Canonicalize a configured base path to either `""` (none) or `/seg[/seg…]`
|
|
17
|
+
* (leading slash, no trailing slash, collapsed inner slashes). A blank value or
|
|
18
|
+
* bare `/` normalizes to `""`, so an unset/`"/"` base is a clean no-op.
|
|
19
|
+
*/
|
|
20
|
+
export const normalizeBasePath = (input?: string): string => {
|
|
21
|
+
if (!input) {
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
const trimmed = input
|
|
25
|
+
.trim()
|
|
26
|
+
.replaceAll(/^\/+|\/+$/gu, "")
|
|
27
|
+
.replaceAll(/\/{2,}/gu, "/");
|
|
28
|
+
return trimmed === "" ? "" : `/${trimmed}`;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether a link target is a root-relative internal path (`/x`) — the only
|
|
33
|
+
* shape a base path applies to. Protocol-relative (`//host`), absolute URLs,
|
|
34
|
+
* other schemes (`mailto:`), fragments (`#x`), and relative paths are excluded.
|
|
35
|
+
*/
|
|
36
|
+
export const isInternalPath = (target: string): boolean =>
|
|
37
|
+
target.startsWith("/") && !target.startsWith("//");
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Idempotently prepend `basePath` to a root-relative route. A route already
|
|
41
|
+
* equal to or nested under the base is returned unchanged, so authors who write
|
|
42
|
+
* the base by hand (`/docs/x`) aren't double-prefixed to `/docs/docs/x`.
|
|
43
|
+
*/
|
|
44
|
+
export const withBasePath = (basePath: string, route: string): string => {
|
|
45
|
+
if (!basePath || !isInternalPath(route)) {
|
|
46
|
+
return route;
|
|
47
|
+
}
|
|
48
|
+
if (route === basePath || route.startsWith(`${basePath}/`)) {
|
|
49
|
+
return route;
|
|
50
|
+
}
|
|
51
|
+
return route === "/" ? basePath : `${basePath}${route}`;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Remove `basePath` from the front of a route (`/docs/guide` -> `/guide`,
|
|
56
|
+
* `/docs` -> `/`). A route not under the base is returned unchanged. Inverse of
|
|
57
|
+
* {@link withBasePath}; used to resolve public assets, which live at the site
|
|
58
|
+
* root regardless of the base.
|
|
59
|
+
*/
|
|
60
|
+
export const stripBasePath = (basePath: string, route: string): string => {
|
|
61
|
+
if (!basePath) {
|
|
62
|
+
return route;
|
|
63
|
+
}
|
|
64
|
+
if (route === basePath) {
|
|
65
|
+
return "/";
|
|
66
|
+
}
|
|
67
|
+
return route.startsWith(`${basePath}/`)
|
|
68
|
+
? route.slice(basePath.length)
|
|
69
|
+
: route;
|
|
70
|
+
};
|
|
@@ -62,6 +62,7 @@ export interface ComponentOverrideAnalysis {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
const GROUPS = ["mdx", "layout", "islands"] as const;
|
|
65
|
+
const GROUP_SET = new Set<string>(GROUPS);
|
|
65
66
|
type Group = (typeof GROUPS)[number];
|
|
66
67
|
|
|
67
68
|
const FRAMEWORK_BY_EXT: Record<string, OverrideFramework> = {
|
|
@@ -121,35 +122,43 @@ const emptyAnalysis = (): ComponentOverrideAnalysis => ({
|
|
|
121
122
|
const propName = (name: ts.PropertyName): string | undefined =>
|
|
122
123
|
ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined;
|
|
123
124
|
|
|
125
|
+
/** Record the bindings declared by one import statement into `map`. */
|
|
126
|
+
const addImportBindings = (
|
|
127
|
+
map: Map<string, ImportBinding>,
|
|
128
|
+
statement: ts.Statement
|
|
129
|
+
): void => {
|
|
130
|
+
if (
|
|
131
|
+
!ts.isImportDeclaration(statement) ||
|
|
132
|
+
!ts.isStringLiteral(statement.moduleSpecifier)
|
|
133
|
+
) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const specifier = statement.moduleSpecifier.text;
|
|
137
|
+
const clause = statement.importClause;
|
|
138
|
+
if (!clause) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (clause.name) {
|
|
142
|
+
map.set(clause.name.text, { imported: "default", specifier });
|
|
143
|
+
}
|
|
144
|
+
const named = clause.namedBindings;
|
|
145
|
+
if (named && ts.isNamedImports(named)) {
|
|
146
|
+
for (const element of named.elements) {
|
|
147
|
+
map.set(element.name.text, {
|
|
148
|
+
imported: (element.propertyName ?? element.name).text,
|
|
149
|
+
specifier,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
124
155
|
/** Map each local binding name to the module + exported name it came from. */
|
|
125
156
|
const collectImports = (
|
|
126
157
|
sourceFile: ts.SourceFile
|
|
127
158
|
): Map<string, ImportBinding> => {
|
|
128
159
|
const map = new Map<string, ImportBinding>();
|
|
129
160
|
for (const statement of sourceFile.statements) {
|
|
130
|
-
|
|
131
|
-
!ts.isImportDeclaration(statement) ||
|
|
132
|
-
!ts.isStringLiteral(statement.moduleSpecifier)
|
|
133
|
-
) {
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
const specifier = statement.moduleSpecifier.text;
|
|
137
|
-
const clause = statement.importClause;
|
|
138
|
-
if (!clause) {
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
if (clause.name) {
|
|
142
|
-
map.set(clause.name.text, { imported: "default", specifier });
|
|
143
|
-
}
|
|
144
|
-
const named = clause.namedBindings;
|
|
145
|
-
if (named && ts.isNamedImports(named)) {
|
|
146
|
-
for (const element of named.elements) {
|
|
147
|
-
map.set(element.name.text, {
|
|
148
|
-
imported: (element.propertyName ?? element.name).text,
|
|
149
|
-
specifier,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
}
|
|
161
|
+
addImportBindings(map, statement);
|
|
153
162
|
}
|
|
154
163
|
return map;
|
|
155
164
|
};
|
|
@@ -232,6 +241,43 @@ const resolveIdentifier = (
|
|
|
232
241
|
return binding ? toImport(binding.specifier, binding.imported, dir) : null;
|
|
233
242
|
};
|
|
234
243
|
|
|
244
|
+
/** Fold one descriptor-object property into the accumulating descriptor. */
|
|
245
|
+
const applyDescriptorProperty = (
|
|
246
|
+
descriptor: RawDescriptor,
|
|
247
|
+
property: ts.ObjectLiteralElementLike,
|
|
248
|
+
imports: Map<string, ImportBinding>,
|
|
249
|
+
dir: string
|
|
250
|
+
): void => {
|
|
251
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
252
|
+
if (property.name.text === "component") {
|
|
253
|
+
descriptor.hadComponent = true;
|
|
254
|
+
descriptor.source = resolveIdentifier(property.name.text, imports, dir);
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const name = propName(property.name);
|
|
262
|
+
const init = property.initializer;
|
|
263
|
+
if (name === "component") {
|
|
264
|
+
descriptor.hadComponent = true;
|
|
265
|
+
if (ts.isStringLiteral(init)) {
|
|
266
|
+
descriptor.source = toImport(init.text, "default", dir);
|
|
267
|
+
} else if (ts.isIdentifier(init)) {
|
|
268
|
+
descriptor.source = resolveIdentifier(init.text, imports, dir);
|
|
269
|
+
}
|
|
270
|
+
} else if (
|
|
271
|
+
name === "client" &&
|
|
272
|
+
ts.isStringLiteral(init) &&
|
|
273
|
+
HYDRATION_MODES.has(init.text as HydrationMode)
|
|
274
|
+
) {
|
|
275
|
+
descriptor.client = init.text as HydrationMode;
|
|
276
|
+
} else if (name === "media" && ts.isStringLiteral(init)) {
|
|
277
|
+
descriptor.media = init.text;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
235
281
|
const readDescriptor = (
|
|
236
282
|
object: ts.ObjectLiteralExpression,
|
|
237
283
|
imports: Map<string, ImportBinding>,
|
|
@@ -239,34 +285,7 @@ const readDescriptor = (
|
|
|
239
285
|
): RawDescriptor => {
|
|
240
286
|
const descriptor: RawDescriptor = { hadComponent: false, source: null };
|
|
241
287
|
for (const property of object.properties) {
|
|
242
|
-
|
|
243
|
-
if (property.name.text === "component") {
|
|
244
|
-
descriptor.hadComponent = true;
|
|
245
|
-
descriptor.source = resolveIdentifier(property.name.text, imports, dir);
|
|
246
|
-
}
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
if (!ts.isPropertyAssignment(property)) {
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
const name = propName(property.name);
|
|
253
|
-
const init = property.initializer;
|
|
254
|
-
if (name === "component") {
|
|
255
|
-
descriptor.hadComponent = true;
|
|
256
|
-
if (ts.isStringLiteral(init)) {
|
|
257
|
-
descriptor.source = toImport(init.text, "default", dir);
|
|
258
|
-
} else if (ts.isIdentifier(init)) {
|
|
259
|
-
descriptor.source = resolveIdentifier(init.text, imports, dir);
|
|
260
|
-
}
|
|
261
|
-
} else if (
|
|
262
|
-
name === "client" &&
|
|
263
|
-
ts.isStringLiteral(init) &&
|
|
264
|
-
HYDRATION_MODES.has(init.text as HydrationMode)
|
|
265
|
-
) {
|
|
266
|
-
descriptor.client = init.text as HydrationMode;
|
|
267
|
-
} else if (name === "media" && ts.isStringLiteral(init)) {
|
|
268
|
-
descriptor.media = init.text;
|
|
269
|
-
}
|
|
288
|
+
applyDescriptorProperty(descriptor, property, imports, dir);
|
|
270
289
|
}
|
|
271
290
|
return descriptor;
|
|
272
291
|
};
|
|
@@ -422,6 +441,38 @@ const normalizeEntry = (
|
|
|
422
441
|
return { identifier: false, key, source: null };
|
|
423
442
|
};
|
|
424
443
|
|
|
444
|
+
/** Normalize one top-level `{ mdx | layout | islands }` group into `result`. */
|
|
445
|
+
const collectGroupOverrides = (
|
|
446
|
+
property: ts.ObjectLiteralElementLike,
|
|
447
|
+
imports: Map<string, ImportBinding>,
|
|
448
|
+
dir: string,
|
|
449
|
+
result: ComponentOverrideAnalysis
|
|
450
|
+
): void => {
|
|
451
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const name = propName(property.name);
|
|
455
|
+
if (
|
|
456
|
+
!(name && GROUP_SET.has(name)) ||
|
|
457
|
+
!ts.isObjectLiteralExpression(property.initializer)
|
|
458
|
+
) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const group = name as Group;
|
|
462
|
+
for (const entry of property.initializer.properties) {
|
|
463
|
+
const normalized = normalizeEntry(
|
|
464
|
+
entry,
|
|
465
|
+
group,
|
|
466
|
+
imports,
|
|
467
|
+
dir,
|
|
468
|
+
result.warnings
|
|
469
|
+
);
|
|
470
|
+
if (normalized) {
|
|
471
|
+
result[group].push(normalized);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
425
476
|
/**
|
|
426
477
|
* Parse a user `components.ts`/`.tsx` and return its normalized overrides. Never
|
|
427
478
|
* executes the file. On a parse failure or unrecognized shape, returns empty
|
|
@@ -449,29 +500,7 @@ export const analyzeComponentOverrides = (
|
|
|
449
500
|
const dir = dirname(filePath);
|
|
450
501
|
|
|
451
502
|
for (const property of object.properties) {
|
|
452
|
-
|
|
453
|
-
continue;
|
|
454
|
-
}
|
|
455
|
-
const name = propName(property.name);
|
|
456
|
-
if (
|
|
457
|
-
!(name && (GROUPS as readonly string[]).includes(name)) ||
|
|
458
|
-
!ts.isObjectLiteralExpression(property.initializer)
|
|
459
|
-
) {
|
|
460
|
-
continue;
|
|
461
|
-
}
|
|
462
|
-
const group = name as Group;
|
|
463
|
-
for (const entry of property.initializer.properties) {
|
|
464
|
-
const normalized = normalizeEntry(
|
|
465
|
-
entry,
|
|
466
|
-
group,
|
|
467
|
-
imports,
|
|
468
|
-
dir,
|
|
469
|
-
result.warnings
|
|
470
|
-
);
|
|
471
|
-
if (normalized) {
|
|
472
|
-
result[group].push(normalized);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
503
|
+
collectGroupOverrides(property, imports, dir, result);
|
|
475
504
|
}
|
|
476
505
|
|
|
477
506
|
return result;
|