blume 0.6.7 → 0.8.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 +618 -0
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/cli/index.js +2609 -1041
- package/dist/cli/index.js.map +110 -103
- package/dist/types/ai/component-markdown.d.ts +34 -0
- package/dist/types/components/content/youtube.d.ts +18 -0
- package/dist/types/core/base-path.d.ts +47 -0
- package/dist/types/core/config-input.d.ts +110 -12
- package/dist/types/core/config.d.ts +6 -4
- package/dist/types/core/data.d.ts +4 -0
- package/dist/types/core/i18n-ui.d.ts +477 -135
- package/dist/types/core/schema.d.ts +309 -195
- package/dist/types/core/sources/types.d.ts +2 -0
- package/dist/types/core/types.d.ts +6 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/openapi/references.d.ts +60 -0
- package/docs/01-quickstart.mdx +5 -2
- package/docs/02-deployment.mdx +24 -9
- package/docs/03-faq.mdx +46 -16
- package/docs/advanced/custom-pages.mdx +1 -1
- package/docs/advanced/skills.mdx +1 -1
- package/docs/configuration/ai.mdx +49 -10
- package/docs/configuration/customization.mdx +11 -0
- package/docs/configuration/index.mdx +33 -3
- package/docs/configuration/seo.mdx +2 -2
- package/docs/content/components.mdx +30 -3
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/islands.mdx +8 -0
- package/docs/content/navigation.mdx +3 -3
- package/docs/content/sources.mdx +1 -1
- package/docs/content/syntax.mdx +17 -2
- package/docs/index.mdx +2 -2
- package/docs/reference/cli.mdx +8 -6
- package/package.json +15 -4
- package/skills/blume/SKILL.md +5 -3
- package/skills/blume-update-docs/SKILL.md +3 -2
- package/src/ai/agent-readability.ts +11 -5
- package/src/ai/ask-context.ts +7 -2
- package/src/ai/ask-data.ts +3 -0
- package/src/ai/ask.ts +12 -7
- package/src/ai/component-markdown.ts +461 -0
- package/src/ai/llms.ts +143 -23
- package/src/ai/markdown.ts +35 -6
- package/src/ai/mcp/data.ts +33 -8
- package/src/ai/mcp/discovery.ts +10 -3
- package/src/ai/mcp/server.ts +24 -7
- package/src/ai/visibility.ts +74 -0
- package/src/astro/component-slots.ts +16 -4
- package/src/astro/examples.ts +12 -7
- package/src/astro/generate.ts +393 -189
- package/src/astro/index.ts +5 -1
- package/src/astro/integration.ts +9 -5
- package/src/astro/islands.ts +11 -5
- package/src/astro/markdown-negotiation.ts +2 -2
- package/src/astro/pages.ts +89 -22
- package/src/astro/templates.ts +259 -25
- package/src/blume-modules.d.ts +8 -0
- package/src/cli/commands/build.ts +131 -38
- package/src/cli/commands/check.ts +1 -1
- package/src/cli/commands/dev.ts +71 -17
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/eject.ts +47 -19
- package/src/cli/commands/init.ts +120 -180
- package/src/cli/commands/preview.ts +4 -1
- package/src/cli/commands/validate.ts +44 -2
- package/src/cli/dev-lock.ts +34 -19
- package/src/cli/eject-scripts.ts +72 -0
- package/src/cli/env.ts +15 -5
- package/src/cli/init/questions.ts +158 -0
- package/src/cli/init/scaffold.ts +380 -0
- package/src/cli/required-secrets.ts +2 -1
- package/src/components/content/AccordionItem.astro +23 -4
- package/src/components/content/Badge.astro +3 -1
- package/src/components/content/Card.astro +4 -2
- 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/Step.astro +10 -1
- package/src/components/content/Tabs.astro +15 -3
- package/src/components/content/Tile.astro +2 -1
- package/src/components/content/Tooltip.astro +3 -1
- package/src/components/content/Update.astro +9 -2
- package/src/components/content/auto-type-table.ts +25 -9
- package/src/components/content/base-href.ts +33 -0
- package/src/components/content/changelog-element.ts +9 -2
- package/src/components/content/diff.ts +12 -6
- package/src/components/content/mermaid-element.ts +10 -2
- package/src/components/index.ts +23 -1
- package/src/components/islands/AskAI.astro +5 -2
- package/src/components/islands/ask-ai.tsx +68 -12
- package/src/components/islands/base-path.ts +28 -0
- package/src/components/islands/hooks.ts +44 -9
- package/src/components/layout/Banner.astro +12 -3
- package/src/components/layout/Breadcrumbs.astro +2 -1
- package/src/components/layout/Favicon.astro +3 -2
- package/src/components/layout/Header.astro +15 -5
- package/src/components/layout/LanguageSwitcher.astro +2 -1
- package/src/components/layout/Logo.astro +13 -4
- package/src/components/layout/NavSelector.astro +2 -1
- package/src/components/layout/NavTree.astro +22 -7
- package/src/components/layout/PageActions.astro +25 -10
- package/src/components/layout/PageFeedback.astro +4 -1
- package/src/components/layout/PageLayout.astro +51 -9
- package/src/components/layout/Pagination.astro +3 -2
- package/src/components/layout/ReferenceLayout.astro +8 -1
- package/src/components/layout/RootLayout.astro +74 -13
- package/src/components/layout/Search.astro +107 -27
- package/src/components/layout/nav-utils.ts +18 -10
- package/src/components/layout/search/algolia.ts +11 -2
- package/src/components/layout/search/endpoint.ts +11 -5
- package/src/components/layout/search/orama-cloud.ts +8 -2
- package/src/components/layout/search/pagefind.ts +3 -0
- package/src/components/layout/search/types.ts +5 -1
- package/src/components/layout/search/typesense.ts +4 -1
- package/src/components/layout/toc-element.ts +8 -2
- package/src/components/openapi/ApiTagOperations.astro +2 -1
- package/src/components/openapi/Operation.astro +47 -40
- package/src/components/openapi/RequestPanel.astro +8 -2
- package/src/components/openapi/helpers.ts +71 -3
- package/src/components/openapi/panel.ts +1 -1
- package/src/components/openapi/snippets.ts +25 -11
- package/src/core/base-path.ts +94 -0
- package/src/core/builtin-tags.ts +2 -0
- package/src/core/component-overrides.ts +103 -74
- package/src/core/config-input.ts +118 -17
- package/src/core/config.ts +8 -5
- package/src/core/content.ts +2 -0
- package/src/core/data.ts +4 -0
- package/src/core/diagnostics.ts +54 -34
- package/src/core/gitignore.ts +4 -1
- package/src/core/graph.ts +166 -88
- package/src/core/i18n-ui.ts +63 -3
- package/src/core/last-modified.ts +15 -6
- package/src/core/links.ts +69 -25
- package/src/core/manifest.ts +62 -45
- package/src/core/nav-diagnostics.ts +1 -1
- package/src/core/navigation.ts +144 -58
- package/src/core/package-json.ts +17 -2
- package/src/core/project-graph.ts +25 -15
- package/src/core/schema.ts +605 -620
- package/src/core/sources/assets.ts +6 -1
- package/src/core/sources/filesystem.ts +4 -0
- package/src/core/sources/github-releases.ts +2 -1
- package/src/core/sources/mdx-remote.ts +76 -63
- package/src/core/sources/normalize.ts +236 -91
- package/src/core/sources/notion.ts +27 -18
- 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/core/ui-packs/ar.ts +1 -0
- package/src/core/ui-packs/bg.ts +1 -0
- package/src/core/ui-packs/bn.ts +1 -0
- package/src/core/ui-packs/ca.ts +1 -0
- package/src/core/ui-packs/cs.ts +1 -0
- package/src/core/ui-packs/da.ts +1 -0
- package/src/core/ui-packs/de.ts +1 -0
- package/src/core/ui-packs/el.ts +1 -0
- package/src/core/ui-packs/es.ts +1 -0
- package/src/core/ui-packs/fa.ts +1 -0
- package/src/core/ui-packs/fi.ts +1 -0
- package/src/core/ui-packs/fr.ts +2 -1
- package/src/core/ui-packs/he.ts +1 -0
- package/src/core/ui-packs/hi.ts +1 -0
- package/src/core/ui-packs/hr.ts +1 -0
- package/src/core/ui-packs/hu.ts +1 -0
- package/src/core/ui-packs/id.ts +1 -0
- package/src/core/ui-packs/it.ts +1 -0
- package/src/core/ui-packs/ja.ts +1 -0
- package/src/core/ui-packs/ko.ts +1 -0
- package/src/core/ui-packs/nl.ts +1 -0
- package/src/core/ui-packs/no.ts +1 -0
- package/src/core/ui-packs/pl.ts +1 -0
- package/src/core/ui-packs/pt-br.ts +1 -0
- package/src/core/ui-packs/pt.ts +1 -0
- package/src/core/ui-packs/ro.ts +1 -0
- package/src/core/ui-packs/ru.ts +1 -0
- package/src/core/ui-packs/sk.ts +1 -0
- package/src/core/ui-packs/sr.ts +1 -0
- package/src/core/ui-packs/sv.ts +1 -0
- package/src/core/ui-packs/th.ts +1 -0
- package/src/core/ui-packs/tr.ts +1 -0
- package/src/core/ui-packs/uk.ts +1 -0
- package/src/core/ui-packs/vi.ts +1 -0
- package/src/core/ui-packs/zh-tw.ts +1 -0
- package/src/core/ui-packs/zh.ts +1 -0
- package/src/deploy/adapter-output.ts +18 -8
- package/src/deploy/redirects.ts +25 -2
- package/src/deploy/robots.ts +6 -1
- package/src/deploy/rss.ts +10 -3
- package/src/deploy/sitemap.ts +59 -13
- package/src/index.ts +5 -0
- package/src/markdown/base-links.ts +60 -0
- package/src/markdown/code-title.ts +11 -14
- package/src/markdown/index.ts +46 -9
- package/src/markdown/inline-code.ts +14 -4
- package/src/markdown/package-commands.ts +10 -4
- package/src/markdown/themes.ts +24 -0
- package/src/openapi/model.ts +15 -5
- package/src/openapi/parse.ts +21 -0
- package/src/openapi/references.ts +75 -21
- package/src/openapi/render-mdx.ts +11 -6
- package/src/openapi/scalar.ts +32 -16
- package/src/openapi/source.ts +59 -10
- package/src/registry/eject.ts +247 -19
- package/src/registry/registry.ts +0 -3
- package/src/search/build.ts +3 -0
- package/src/search/documents.ts +36 -4
- package/src/search/sync/typesense.ts +6 -4
- package/src/seo/jsonld.ts +28 -17
- package/src/theme/entry.ts +85 -20
|
@@ -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";
|
|
@@ -22,7 +24,7 @@ interface Props {
|
|
|
22
24
|
|
|
23
25
|
const { sample, languages, responses, schemas } = Astro.props;
|
|
24
26
|
|
|
25
|
-
// A `.prose` wrapper gives Shiki its scoped token
|
|
27
|
+
// A `.prose` wrapper gives Shiki its scoped token colors; the global style at
|
|
26
28
|
// the foot of this file strips the standalone code block's own box (border,
|
|
27
29
|
// injected copy button, language label) so the code sits flush inside the one
|
|
28
30
|
// panel border.
|
|
@@ -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
|
);
|
|
@@ -39,8 +39,71 @@ export interface SchemaLike {
|
|
|
39
39
|
[key: string]: unknown;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** A permissive view of an operation parameter — only the fields we render. */
|
|
43
|
+
export interface ParameterLike {
|
|
44
|
+
$ref?: string;
|
|
45
|
+
name?: string;
|
|
46
|
+
in?: string;
|
|
47
|
+
description?: string;
|
|
48
|
+
required?: boolean;
|
|
49
|
+
deprecated?: boolean;
|
|
50
|
+
schema?: SchemaLike;
|
|
51
|
+
example?: unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
const REF_PATTERN = /#\/components\/schemas\/(?<name>[^/]+)$/u;
|
|
43
55
|
|
|
56
|
+
const COMPONENT_REF = /#\/components\/(?<section>[^/]+)\/(?<name>[^/]+)$/u;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve one level of `$ref` against a named `components` section
|
|
60
|
+
* (`parameters`, `requestBodies`, `responses`). Mirrors {@link resolveSchema}:
|
|
61
|
+
* an unknown ref — or one pointing into a different section — is returned
|
|
62
|
+
* as-is.
|
|
63
|
+
*/
|
|
64
|
+
export const resolveComponentRef = <T extends { $ref?: string }>(
|
|
65
|
+
node: T,
|
|
66
|
+
components: Record<string, unknown> | undefined,
|
|
67
|
+
section: string
|
|
68
|
+
): T => {
|
|
69
|
+
if (typeof node.$ref !== "string") {
|
|
70
|
+
return node;
|
|
71
|
+
}
|
|
72
|
+
const groups = COMPONENT_REF.exec(node.$ref)?.groups;
|
|
73
|
+
if (groups?.section !== section) {
|
|
74
|
+
return node;
|
|
75
|
+
}
|
|
76
|
+
const table = components?.[section] as Record<string, T> | undefined;
|
|
77
|
+
return table?.[groups.name ?? ""] ?? node;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Path-level and operation-level parameters merged into one render list.
|
|
82
|
+
* `$ref`s resolve against `components.parameters` first; then an operation
|
|
83
|
+
* parameter overrides a path-level one with the same `name` + `in` (the
|
|
84
|
+
* OpenAPI override rule), so a re-declared parameter appears once.
|
|
85
|
+
*/
|
|
86
|
+
export const mergeParameters = (
|
|
87
|
+
pathParameters: ParameterLike[] | undefined,
|
|
88
|
+
operationParameters: ParameterLike[] | undefined,
|
|
89
|
+
components?: Record<string, unknown>
|
|
90
|
+
): ParameterLike[] => {
|
|
91
|
+
const merged = new Map<string, ParameterLike>();
|
|
92
|
+
let position = 0;
|
|
93
|
+
for (const raw of [
|
|
94
|
+
...(pathParameters ?? []),
|
|
95
|
+
...(operationParameters ?? []),
|
|
96
|
+
]) {
|
|
97
|
+
const param = resolveComponentRef(raw, components, "parameters");
|
|
98
|
+
// A nameless parameter is invalid per spec, but key it uniquely so it is
|
|
99
|
+
// still rendered rather than collapsing with other invalid entries.
|
|
100
|
+
const key = param.name ? `${param.in ?? ""}:${param.name}` : `#${position}`;
|
|
101
|
+
merged.set(key, param);
|
|
102
|
+
position += 1;
|
|
103
|
+
}
|
|
104
|
+
return [...merged.values()];
|
|
105
|
+
};
|
|
106
|
+
|
|
44
107
|
/** The display name of a `$ref`, e.g. `#/components/schemas/Pet` -> `Pet`. */
|
|
45
108
|
export const refName = (ref: string): string =>
|
|
46
109
|
REF_PATTERN.exec(ref)?.groups?.name ?? ref.split("/").at(-1) ?? ref;
|
|
@@ -166,7 +229,7 @@ export const objectProperties = (
|
|
|
166
229
|
/** Sentinel: no explicit example is declared on a schema. */
|
|
167
230
|
const NO_VALUE = Symbol("no-value");
|
|
168
231
|
|
|
169
|
-
/** The declared example/default/enum for a schema, or {@link NO_VALUE}. */
|
|
232
|
+
/** The declared example/const/default/enum for a schema, or {@link NO_VALUE}. */
|
|
170
233
|
const explicitExample = (schema: SchemaLike): unknown => {
|
|
171
234
|
if (schema.example !== undefined) {
|
|
172
235
|
return schema.example;
|
|
@@ -174,6 +237,11 @@ const explicitExample = (schema: SchemaLike): unknown => {
|
|
|
174
237
|
if (Array.isArray(schema.examples) && schema.examples.length > 0) {
|
|
175
238
|
return schema.examples[0];
|
|
176
239
|
}
|
|
240
|
+
// `const` is the schema's only valid value (the 3.1 discriminator idiom), so
|
|
241
|
+
// it outranks `default`/`enum` — either of those differing would be invalid.
|
|
242
|
+
if (schema.const !== undefined) {
|
|
243
|
+
return schema.const;
|
|
244
|
+
}
|
|
177
245
|
if (schema.default !== undefined) {
|
|
178
246
|
return schema.default;
|
|
179
247
|
}
|
|
@@ -202,8 +270,8 @@ const primitiveExample = (
|
|
|
202
270
|
|
|
203
271
|
/**
|
|
204
272
|
* Build a representative example value for a schema (honoring `example` /
|
|
205
|
-
* `default` / `enum` first). A `seen` set of `$ref`s guards against
|
|
206
|
-
* schemas that keeping refs intact allows.
|
|
273
|
+
* `const` / `default` / `enum` first). A `seen` set of `$ref`s guards against
|
|
274
|
+
* the circular schemas that keeping refs intact allows.
|
|
207
275
|
*/
|
|
208
276
|
export const exampleValue = (
|
|
209
277
|
schema: SchemaLike | undefined,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Client
|
|
2
|
+
* Client behavior for the OpenAPI request/response panels. `<blume-panel-tabs>`
|
|
3
3
|
* switches the visible `[data-panel="key"]` region when a `[data-panel-tab="key"]`
|
|
4
4
|
* button is clicked, and an optional `[data-panel-copy]` button copies the active
|
|
5
5
|
* panel's text. Vanilla custom element — no framework, in keeping with the core
|
|
@@ -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,94 @@
|
|
|
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
|
+
* {@link withBasePath} for the composed `deployment.base` + `basePath` stack
|
|
56
|
+
* (`/base` + `/docs` serves pages at `/base/docs/x`). The hand-written-base
|
|
57
|
+
* promise applies per layer: authors write `basePath` by hand (see
|
|
58
|
+
* `markdown/base-links.ts`), so a `/docs/x` link gains only the deployment base
|
|
59
|
+
* (`/base/docs/x`) rather than being double-prefixed to `/base/docs/docs/x`,
|
|
60
|
+
* and a route already under the full composite is returned unchanged.
|
|
61
|
+
*/
|
|
62
|
+
export const withComposedBasePath = (
|
|
63
|
+
deployBase: string,
|
|
64
|
+
basePath: string,
|
|
65
|
+
route: string
|
|
66
|
+
): string => {
|
|
67
|
+
const composed = `${deployBase}${basePath}`;
|
|
68
|
+
if (
|
|
69
|
+
composed &&
|
|
70
|
+
isInternalPath(route) &&
|
|
71
|
+
(route === composed || route.startsWith(`${composed}/`))
|
|
72
|
+
) {
|
|
73
|
+
return route;
|
|
74
|
+
}
|
|
75
|
+
return withBasePath(deployBase, withBasePath(basePath, route));
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Remove `basePath` from the front of a route (`/docs/guide` -> `/guide`,
|
|
80
|
+
* `/docs` -> `/`). A route not under the base is returned unchanged. Inverse of
|
|
81
|
+
* {@link withBasePath}; used to resolve public assets, which live at the site
|
|
82
|
+
* root regardless of the base.
|
|
83
|
+
*/
|
|
84
|
+
export const stripBasePath = (basePath: string, route: string): string => {
|
|
85
|
+
if (!basePath) {
|
|
86
|
+
return route;
|
|
87
|
+
}
|
|
88
|
+
if (route === basePath) {
|
|
89
|
+
return "/";
|
|
90
|
+
}
|
|
91
|
+
return route.startsWith(`${basePath}/`)
|
|
92
|
+
? route.slice(basePath.length)
|
|
93
|
+
: route;
|
|
94
|
+
};
|
package/src/core/builtin-tags.ts
CHANGED
|
@@ -26,6 +26,8 @@ export const BUILTIN_MDX_TAGS = new Set<string>([
|
|
|
26
26
|
"Frame",
|
|
27
27
|
"GithubInfo",
|
|
28
28
|
"Icon",
|
|
29
|
+
// Conditionally imported by the catch-all, but `detectUsesMath` matches any
|
|
30
|
+
// authored `<Math`, so it is always wired wherever it can appear.
|
|
29
31
|
"Math",
|
|
30
32
|
"Operation",
|
|
31
33
|
"Panel",
|
|
@@ -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;
|