blume 1.4.2 → 1.5.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 +71 -0
- package/dist/cli/index.js +2260 -1100
- package/dist/cli/index.js.map +123 -117
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/base-path.d.ts +8 -0
- package/dist/types/core/config-input.d.ts +87 -27
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/data.d.ts +16 -1
- package/dist/types/core/diagnostics.d.ts +5 -1
- package/dist/types/core/i18n-ui.d.ts +12 -0
- package/dist/types/core/schema.d.ts +116 -15
- package/dist/types/core/sources/types.d.ts +11 -1
- package/dist/types/core/standard-schema.d.ts +7 -3
- package/dist/types/core/types.d.ts +43 -2
- package/dist/types/core/ui-packs/index.d.ts +9 -1
- package/dist/types/openapi/references.d.ts +14 -7
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/configuration/ai.mdx +26 -8
- package/docs/configuration/search.mdx +2 -0
- package/docs/configuration/seo.mdx +1 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/meta.mdx +2 -1
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +35 -1
- package/docs/content/sources.mdx +1 -1
- package/docs/content/versioning.mdx +106 -0
- package/docs/reference/cli.mdx +1 -0
- package/docs/reference/frontmatter.mdx +3 -0
- package/package.json +13 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/references/docusaurus.md +1 -1
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/agent-readability.ts +40 -12
- package/src/ai/api-catalog.ts +2 -2
- package/src/ai/ask-context.ts +49 -12
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +42 -17
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +52 -16
- package/src/ai/mcp/server.ts +280 -125
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/tar.ts +29 -70
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +13 -5
- package/src/astro/generate.ts +113 -63
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +23 -12
- package/src/astro/templates.ts +185 -41
- package/src/audit/agent.ts +16 -31
- package/src/audit/checks/content.ts +26 -11
- package/src/audit/checks/dns-aid.ts +3 -0
- package/src/audit/checks/indexability.ts +24 -6
- package/src/audit/checks/llms.ts +9 -4
- package/src/audit/checks/network.ts +2 -0
- package/src/audit/checks/social.ts +18 -10
- package/src/audit/crawl.ts +78 -25
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +15 -5
- package/src/audit/snapshot.ts +29 -6
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +21 -21
- package/src/cli/commands/build.ts +30 -16
- package/src/cli/commands/dev.ts +15 -15
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eject.ts +4 -4
- package/src/cli/commands/eval.ts +24 -30
- package/src/cli/commands/init.ts +9 -9
- package/src/cli/commands/mcp-stdio.ts +3 -0
- package/src/cli/commands/translate.ts +14 -3
- package/src/cli/commands/version.ts +85 -0
- package/src/cli/dev-lock.ts +31 -10
- package/src/cli/eject-scripts.ts +17 -2
- package/src/cli/env.ts +13 -30
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +41 -13
- package/src/cli/internal-error.ts +1 -0
- package/src/cli/report-format.ts +22 -0
- package/src/components/content/AccordionItem.astro +2 -9
- package/src/components/content/ColorItem.astro +5 -13
- package/src/components/content/Component.astro +12 -8
- package/src/components/content/Frame.astro +2 -12
- package/src/components/content/Prompt.astro +12 -31
- package/src/components/content/Tab.astro +2 -9
- package/src/components/content/Tooltip.astro +1 -9
- package/src/components/content/Update.astro +2 -9
- package/src/components/content/auto-type-table.ts +3 -0
- package/src/components/content/diff.ts +9 -5
- package/src/components/content/github-info.ts +2 -0
- package/src/components/content/inline-markdown.ts +28 -0
- package/src/components/copy-feedback.ts +96 -0
- package/src/components/islands/ask-ai.tsx +111 -34
- package/src/components/islands/hooks.ts +5 -1
- package/src/components/islands/webmcp.ts +49 -12
- package/src/components/layout/Header.astro +25 -1
- package/src/components/layout/NavSelector.astro +11 -2
- package/src/components/layout/NavTree.astro +4 -2
- package/src/components/layout/PageActions.astro +20 -32
- package/src/components/layout/PageLayout.astro +8 -28
- package/src/components/layout/RootLayout.astro +24 -48
- package/src/components/layout/Search.astro +133 -22
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/drawer-inert.ts +31 -0
- package/src/components/layout/hydration-hint.ts +1 -1
- package/src/components/layout/nav-utils.ts +1 -4
- package/src/components/layout/overrides.ts +25 -12
- package/src/components/layout/search/algolia.ts +18 -5
- package/src/components/layout/search/endpoint.ts +3 -0
- package/src/components/layout/search/flexsearch.ts +23 -7
- package/src/components/layout/search/orama-cloud.ts +1 -1
- package/src/components/layout/search/orama.ts +4 -1
- package/src/components/layout/search/pagefind.ts +8 -5
- package/src/components/layout/search/types.ts +45 -1
- package/src/components/layout/search/typesense.ts +19 -3
- package/src/components/openapi/ApiOverview.astro +32 -6
- package/src/components/openapi/AsyncApiOperation.astro +237 -0
- package/src/components/openapi/Bindings.astro +89 -0
- package/src/components/openapi/MethodBadge.astro +3 -0
- package/src/components/openapi/Operation.astro +7 -2
- package/src/components/openapi/PanelTabs.astro +131 -0
- package/src/components/openapi/ParametersTable.astro +2 -0
- package/src/components/openapi/RequestPanel.astro +12 -119
- package/src/components/openapi/async-snippets.ts +174 -0
- package/src/components/openapi/async.ts +348 -0
- package/src/components/openapi/helpers.ts +52 -20
- package/src/components/openapi/panel.ts +11 -8
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/components/raf-throttle.ts +21 -0
- package/src/components/slug.ts +14 -0
- package/src/core/base-path.ts +18 -1
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +96 -27
- package/src/core/config.ts +20 -7
- package/src/core/content.ts +3 -1
- package/src/core/data.ts +16 -1
- package/src/core/define-components.ts +5 -0
- package/src/core/diagnostics.ts +46 -38
- package/src/core/frontmatter.ts +74 -4
- package/src/core/graph.ts +137 -53
- package/src/core/i18n-ui.ts +15 -0
- package/src/core/i18n.ts +16 -8
- package/src/core/load-module.ts +1 -0
- package/src/core/manifest.ts +92 -3
- package/src/core/meta.ts +44 -14
- package/src/core/nav-diagnostics.ts +3 -3
- package/src/core/navigation.ts +247 -67
- package/src/core/probe.ts +7 -19
- package/src/core/project-graph.ts +27 -4
- package/src/core/schema.ts +219 -67
- package/src/core/site-url.ts +27 -0
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +16 -8
- package/src/core/sources/github-releases.ts +39 -11
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +93 -22
- package/src/core/sources/notion.ts +76 -22
- package/src/core/sources/portable-text.ts +48 -12
- package/src/core/sources/resolve.ts +1 -0
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +17 -1
- package/src/core/sources/watch.ts +1 -1
- package/src/core/standard-schema.ts +9 -3
- package/src/core/text-width.ts +26 -0
- package/src/core/tsconfig-aliases.ts +9 -5
- package/src/core/types.ts +45 -2
- package/src/core/ui-packs/index.ts +9 -1
- package/src/core/version-cut.ts +301 -0
- package/src/core/version.ts +2 -0
- package/src/core/versions.ts +170 -0
- package/src/deploy/adapter-output.ts +5 -2
- package/src/deploy/cloudflare-negotiation.ts +40 -11
- package/src/deploy/robots.ts +2 -1
- package/src/deploy/rss.ts +2 -1
- package/src/deploy/sitemap.ts +89 -8
- package/src/deploy/vercel-negotiation.ts +11 -4
- package/src/eval/agents.ts +13 -10
- package/src/eval/report.ts +5 -18
- package/src/eval/run.ts +2 -2
- package/src/eval/schema.ts +1 -1
- package/src/markdown/base-links.ts +6 -6
- package/src/markdown/directives.ts +7 -1
- package/src/markdown/heading-anchors.ts +17 -6
- package/src/markdown/index.ts +73 -24
- package/src/markdown/inline-code.ts +14 -2
- package/src/markdown/language-icon.ts +6 -2
- package/src/markdown/mdast.ts +18 -4
- package/src/markdown/package-commands.ts +63 -58
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +50 -33
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +135 -66
- package/src/openapi/parse.ts +166 -33
- package/src/openapi/references.ts +47 -22
- package/src/openapi/render-mdx.ts +137 -59
- package/src/openapi/scalar.ts +8 -10
- package/src/openapi/source.ts +126 -29
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +103 -39
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +117 -32
- package/src/search/popular.ts +10 -5
- package/src/search/providers.ts +2 -2
- package/src/search/sync/index.ts +2 -0
- package/src/search/sync/typesense.ts +4 -2
- package/src/seo/jsonld.ts +24 -6
- package/src/seo/x-handle.ts +8 -3
- package/src/theme/chrome-icons.ts +7 -2
- package/src/theme/fonts.ts +8 -4
- package/src/theme/icons.ts +4 -2
- package/src/theme/palette.ts +27 -15
- package/src/translate/ledger.ts +4 -2
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +10 -19
- package/src/translate/run.ts +29 -38
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
- package/src/cli/coalesce.ts +0 -43
package/src/ai/tar.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { gzipSync } from "node:zlib";
|
|
2
2
|
|
|
3
|
+
import { createTar } from "nanotar";
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* entries, and Node's gzip header carries no timestamp — so a
|
|
7
|
-
* digest only changes when its content does.
|
|
6
|
+
* `.tar.gz` writer for agent-skill archives, on nanotar's ustar writer.
|
|
7
|
+
* Deterministic by construction — fixed mtime/uid/gid/owner attrs,
|
|
8
|
+
* caller-ordered entries, and Node's gzip header carries no timestamp — so a
|
|
9
|
+
* skill's archive digest only changes when its content does. That holds per
|
|
10
|
+
* machine: the tar bytes are portable, but zlib's compressed stream differs
|
|
11
|
+
* across architectures, so gzip-layer digests are not comparable across
|
|
12
|
+
* platforms.
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
15
|
/** One regular file to archive. Paths are `/`-separated, relative, no `..`. */
|
|
@@ -17,67 +22,18 @@ export interface TarEntry {
|
|
|
17
22
|
path: string;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
const BLOCK = 512;
|
|
21
25
|
/** ustar `name` field capacity; skill layouts are shallow, so no `prefix`. */
|
|
22
26
|
const NAME_MAX = 100;
|
|
23
27
|
|
|
24
28
|
const encoder = new TextEncoder();
|
|
25
29
|
|
|
26
|
-
/** Write an octal field: zero-padded digits followed by a NUL terminator. */
|
|
27
|
-
const octal = (
|
|
28
|
-
header: Uint8Array,
|
|
29
|
-
offset: number,
|
|
30
|
-
length: number,
|
|
31
|
-
value: number
|
|
32
|
-
): void => {
|
|
33
|
-
const text = value.toString(8).padStart(length - 1, "0");
|
|
34
|
-
header.set(encoder.encode(text), offset);
|
|
35
|
-
header[offset + length - 1] = 0;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const text = (header: Uint8Array, offset: number, value: string): void => {
|
|
39
|
-
header.set(encoder.encode(value), offset);
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const fileHeader = (entry: TarEntry): Uint8Array => {
|
|
43
|
-
const header = new Uint8Array(BLOCK);
|
|
44
|
-
text(header, 0, entry.path);
|
|
45
|
-
octal(header, 100, 8, entry.executable ? 0o755 : 0o644);
|
|
46
|
-
// uid and gid: root-owned, fixed for determinism.
|
|
47
|
-
octal(header, 108, 8, 0);
|
|
48
|
-
octal(header, 116, 8, 0);
|
|
49
|
-
octal(header, 124, 12, entry.content.byteLength);
|
|
50
|
-
// mtime: fixed at the epoch for determinism.
|
|
51
|
-
octal(header, 136, 12, 0);
|
|
52
|
-
// typeflag "0": regular file.
|
|
53
|
-
text(header, 156, "0");
|
|
54
|
-
text(header, 257, "ustar");
|
|
55
|
-
header[262] = 0;
|
|
56
|
-
text(header, 263, "00");
|
|
57
|
-
// devmajor and devminor.
|
|
58
|
-
octal(header, 329, 8, 0);
|
|
59
|
-
octal(header, 337, 8, 0);
|
|
60
|
-
// Checksum: computed with the checksum field treated as eight spaces, then
|
|
61
|
-
// written as six octal digits, NUL, space (the historical ustar format).
|
|
62
|
-
header.fill(0x20, 148, 156);
|
|
63
|
-
let sum = 0;
|
|
64
|
-
for (const byte of header) {
|
|
65
|
-
sum += byte;
|
|
66
|
-
}
|
|
67
|
-
text(header, 148, sum.toString(8).padStart(6, "0"));
|
|
68
|
-
header[154] = 0;
|
|
69
|
-
header[155] = 0x20;
|
|
70
|
-
return header;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
30
|
/**
|
|
74
31
|
* Build a gzipped ustar archive of the given files, in the given order. Paths
|
|
75
|
-
* longer than the ustar `name` field or escaping the archive root
|
|
76
|
-
*
|
|
77
|
-
*
|
|
32
|
+
* longer than the ustar `name` field or escaping the archive root throw to
|
|
33
|
+
* surface a programming error — nanotar would silently truncate an oversized
|
|
34
|
+
* name into a corrupt archive, so the validation stays here.
|
|
78
35
|
*/
|
|
79
36
|
export const buildTarGz = (entries: readonly TarEntry[]): Uint8Array => {
|
|
80
|
-
const blocks: Uint8Array[] = [];
|
|
81
37
|
for (const entry of entries) {
|
|
82
38
|
if (encoder.encode(entry.path).byteLength > NAME_MAX) {
|
|
83
39
|
throw new Error(`tar path exceeds ${NAME_MAX} bytes: ${entry.path}`);
|
|
@@ -85,20 +41,23 @@ export const buildTarGz = (entries: readonly TarEntry[]): Uint8Array => {
|
|
|
85
41
|
if (entry.path.startsWith("/") || entry.path.split("/").includes("..")) {
|
|
86
42
|
throw new Error(`tar path must be archive-relative: ${entry.path}`);
|
|
87
43
|
}
|
|
88
|
-
blocks.push(fileHeader(entry), entry.content);
|
|
89
|
-
const overhang = entry.content.byteLength % BLOCK;
|
|
90
|
-
if (overhang > 0) {
|
|
91
|
-
blocks.push(new Uint8Array(BLOCK - overhang));
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
// End-of-archive marker: two zero blocks.
|
|
95
|
-
blocks.push(new Uint8Array(BLOCK * 2));
|
|
96
|
-
const total = blocks.reduce((sum, block) => sum + block.byteLength, 0);
|
|
97
|
-
const tar = new Uint8Array(total);
|
|
98
|
-
let offset = 0;
|
|
99
|
-
for (const block of blocks) {
|
|
100
|
-
tar.set(block, offset);
|
|
101
|
-
offset += block.byteLength;
|
|
102
44
|
}
|
|
45
|
+
const tar = createTar(
|
|
46
|
+
entries.map((entry) => ({
|
|
47
|
+
// Root-owned, epoch-mtime, empty owner names: every field a rebuild
|
|
48
|
+
// could vary is pinned so the archive bytes are a function of content.
|
|
49
|
+
attrs: {
|
|
50
|
+
gid: 0,
|
|
51
|
+
group: "",
|
|
52
|
+
mode: entry.executable ? "755" : "644",
|
|
53
|
+
mtime: 0,
|
|
54
|
+
uid: 0,
|
|
55
|
+
user: "",
|
|
56
|
+
},
|
|
57
|
+
data: entry.content,
|
|
58
|
+
name: entry.path,
|
|
59
|
+
}))
|
|
60
|
+
);
|
|
61
|
+
// Sync gzip with a pinned level; Node writes no timestamp into the header.
|
|
103
62
|
return new Uint8Array(gzipSync(tar, { level: 9 }));
|
|
104
63
|
};
|
package/src/ai/visibility.ts
CHANGED
|
@@ -19,10 +19,10 @@ const visibilityBlock = (audience: VisibilityAudience): RegExp =>
|
|
|
19
19
|
"gu"
|
|
20
20
|
);
|
|
21
21
|
|
|
22
|
-
const BLOCKS
|
|
22
|
+
const BLOCKS = {
|
|
23
23
|
agents: visibilityBlock("agents"),
|
|
24
24
|
web: visibilityBlock("web"),
|
|
25
|
-
}
|
|
25
|
+
} satisfies Record<VisibilityAudience, RegExp>;
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Resolve `<Visibility>` blocks for one audience: blocks addressed to the
|
|
@@ -77,6 +77,8 @@ const importClause = (variable: string, name: string, path: string): string =>
|
|
|
77
77
|
|
|
78
78
|
/** A wrapper `.astro` that statically imports a component and hydrates it. */
|
|
79
79
|
const wrapperContent = (override: NormalizedOverride): string => {
|
|
80
|
+
// SAFETY: the only caller guards `if (!source)` and bails before invoking
|
|
81
|
+
// this, so the override always carries a resolved source here.
|
|
80
82
|
const { name, path } = override.source as NonNullable<
|
|
81
83
|
NormalizedOverride["source"]
|
|
82
84
|
>;
|
package/src/astro/examples.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
|
|
3
|
+
import pMap from "p-map";
|
|
3
4
|
import { join, relative } from "pathe";
|
|
4
5
|
import { glob } from "tinyglobby";
|
|
5
6
|
|
|
@@ -32,7 +33,11 @@ export interface ExampleDiscovery {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
/** Example extensions mapped to the framework that renders them. */
|
|
35
|
-
|
|
36
|
+
interface FrameworkByExtension {
|
|
37
|
+
[extension: string]: ExampleFramework;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const FRAMEWORK_BY_EXT: FrameworkByExtension = {
|
|
36
41
|
astro: "astro",
|
|
37
42
|
jsx: "react",
|
|
38
43
|
svelte: "svelte",
|
|
@@ -48,6 +53,9 @@ const EXAMPLE_FILE = /\.(?<ext>astro|jsx|svelte|tsx|vue)$/u;
|
|
|
48
53
|
// Renderable example files when `examples` names a plain directory.
|
|
49
54
|
const DEFAULT_EXAMPLE_GLOB = "**/*.{astro,jsx,svelte,tsx,vue}";
|
|
50
55
|
|
|
56
|
+
/** Ceiling on concurrent example-file reads; unbounded fan-out risks EMFILE. */
|
|
57
|
+
const READ_CONCURRENCY = 16;
|
|
58
|
+
|
|
51
59
|
// Glob magic that turns `examples` from a plain directory into a pattern. `()`,
|
|
52
60
|
// `@`, and `+` are excluded so literal path segments (npm scopes, parens) keep
|
|
53
61
|
// resolving as directories; the extglob leads `*?!` still trigger here.
|
|
@@ -58,7 +66,7 @@ const GLOB_MAGIC = /[!*?[\]{}]/u;
|
|
|
58
66
|
* discovered files can be keyed relative to that prefix (e.g.
|
|
59
67
|
* `registry/x/**\/examples/*` → `{ base: "registry/x", rest: "**\/examples/*" }`).
|
|
60
68
|
*/
|
|
61
|
-
const splitGlobBase = (pattern: string)
|
|
69
|
+
const splitGlobBase = (pattern: string) => {
|
|
62
70
|
const segments = pattern.split("/");
|
|
63
71
|
// Only called when the pattern contains glob magic (see the caller), and `/`
|
|
64
72
|
// is never magic, so the magic char always lands in a segment — `findIndex`
|
|
@@ -102,9 +110,9 @@ export const discoverExamples = async (
|
|
|
102
110
|
onlyFiles: true,
|
|
103
111
|
});
|
|
104
112
|
const files = matches.toSorted();
|
|
105
|
-
const sources = await
|
|
106
|
-
|
|
107
|
-
);
|
|
113
|
+
const sources = await pMap(files, (file) => readFile(file, "utf-8"), {
|
|
114
|
+
concurrency: READ_CONCURRENCY,
|
|
115
|
+
});
|
|
108
116
|
|
|
109
117
|
const examples: ExampleSpec[] = [];
|
|
110
118
|
const warnings: string[] = [];
|
package/src/astro/generate.ts
CHANGED
|
@@ -12,11 +12,13 @@ import {
|
|
|
12
12
|
import { createRequire } from "node:module";
|
|
13
13
|
import { pathToFileURL } from "node:url";
|
|
14
14
|
|
|
15
|
+
import { imageSize } from "image-size";
|
|
16
|
+
import pMap from "p-map";
|
|
15
17
|
import { basename, dirname, join, normalize, relative, resolve } from "pathe";
|
|
16
18
|
import { glob } from "tinyglobby";
|
|
17
19
|
|
|
18
20
|
import { buildAskData } from "../ai/ask-data.ts";
|
|
19
|
-
import { resolveAskBackend } from "../ai/ask.ts";
|
|
21
|
+
import { askBackendRuntimeDep, resolveAskBackend } from "../ai/ask.ts";
|
|
20
22
|
import { buildRawMarkdown, markdownRoutePaths } from "../ai/markdown.ts";
|
|
21
23
|
import { buildMcpData } from "../ai/mcp/data.ts";
|
|
22
24
|
import { buildMcpDiscovery, buildMcpServerCard } from "../ai/mcp/discovery.ts";
|
|
@@ -487,12 +489,14 @@ export const ensureDepsLink = async (
|
|
|
487
489
|
* (`<build.server>/.prerender/`) output — so it fires for exactly that build.
|
|
488
490
|
* Inert in dev, where there is no build and `writeBundle` never runs.
|
|
489
491
|
*/
|
|
490
|
-
export
|
|
491
|
-
pkgDir: string = packageRoot()
|
|
492
|
-
): {
|
|
492
|
+
export interface PrerenderDepsPlugin {
|
|
493
493
|
name: string;
|
|
494
494
|
writeBundle: (options: { dir?: string }) => Promise<void>;
|
|
495
|
-
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export const prerenderDepsPlugin = (
|
|
498
|
+
pkgDir: string = packageRoot()
|
|
499
|
+
): PrerenderDepsPlugin => ({
|
|
496
500
|
name: "blume:prerender-deps",
|
|
497
501
|
writeBundle: async (options) => {
|
|
498
502
|
if (!options.dir || basename(options.dir) !== ".prerender") {
|
|
@@ -526,14 +530,16 @@ interface ServerAppResolveContext {
|
|
|
526
530
|
* Stripping the spurious `.js` and delegating back to Astro's resolver lets the
|
|
527
531
|
* reload complete cleanly, so the renamed route resolves without a restart.
|
|
528
532
|
*/
|
|
529
|
-
export
|
|
533
|
+
export interface ServerAppResolvePlugin {
|
|
530
534
|
enforce: "pre";
|
|
531
535
|
name: string;
|
|
532
536
|
resolveId: (
|
|
533
537
|
this: ServerAppResolveContext,
|
|
534
538
|
id: string
|
|
535
539
|
) => Promise<string | null>;
|
|
536
|
-
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export const serverAppResolvePlugin = (): ServerAppResolvePlugin => ({
|
|
537
543
|
enforce: "pre",
|
|
538
544
|
name: "blume:server-app-resolve",
|
|
539
545
|
async resolveId(id) {
|
|
@@ -546,20 +552,20 @@ export const serverAppResolvePlugin = (): {
|
|
|
546
552
|
});
|
|
547
553
|
|
|
548
554
|
/** Astro integration package each non-React island framework needs installed. */
|
|
549
|
-
const ISLAND_FRAMEWORK_DEPS
|
|
550
|
-
svelte
|
|
551
|
-
vue
|
|
552
|
-
|
|
555
|
+
const ISLAND_FRAMEWORK_DEPS = new Map([
|
|
556
|
+
["svelte", "@astrojs/svelte"],
|
|
557
|
+
["vue", "@astrojs/vue"],
|
|
558
|
+
]);
|
|
553
559
|
|
|
554
560
|
/**
|
|
555
561
|
* Adapter package the project must install itself for each deployment
|
|
556
562
|
* platform whose adapter Blume doesn't ship. Node and Vercel ship with Blume,
|
|
557
563
|
* so they never need this.
|
|
558
564
|
*/
|
|
559
|
-
const DEPLOYMENT_ADAPTER_DEPS
|
|
560
|
-
cloudflare
|
|
561
|
-
netlify
|
|
562
|
-
|
|
565
|
+
const DEPLOYMENT_ADAPTER_DEPS = new Map([
|
|
566
|
+
["cloudflare", "@astrojs/cloudflare"],
|
|
567
|
+
["netlify", "@astrojs/netlify"],
|
|
568
|
+
]);
|
|
563
569
|
|
|
564
570
|
/**
|
|
565
571
|
* Warn when a Vue/Svelte island is present but its Astro integration isn't
|
|
@@ -572,7 +578,7 @@ const islandFrameworkWarnings = (
|
|
|
572
578
|
): string[] => {
|
|
573
579
|
const warnings: string[] = [];
|
|
574
580
|
for (const framework of frameworks) {
|
|
575
|
-
const dep = ISLAND_FRAMEWORK_DEPS
|
|
581
|
+
const dep = ISLAND_FRAMEWORK_DEPS.get(framework);
|
|
576
582
|
if (dep && !canResolveFrom(root, dep)) {
|
|
577
583
|
warnings.push(
|
|
578
584
|
`Islands use ${framework}, which needs "${dep}". Install it (e.g. \`npm install ${dep} ${framework}\`).`
|
|
@@ -597,7 +603,7 @@ const deploymentAdapterWarnings = (
|
|
|
597
603
|
): string[] => {
|
|
598
604
|
const dep =
|
|
599
605
|
deployment.output === "server" && deployment.adapter
|
|
600
|
-
? DEPLOYMENT_ADAPTER_DEPS
|
|
606
|
+
? DEPLOYMENT_ADAPTER_DEPS.get(deployment.adapter)
|
|
601
607
|
: undefined;
|
|
602
608
|
if (
|
|
603
609
|
dep &&
|
|
@@ -638,6 +644,33 @@ export const searchProviderWarnings = (
|
|
|
638
644
|
return warnings;
|
|
639
645
|
};
|
|
640
646
|
|
|
647
|
+
/**
|
|
648
|
+
* Warn when the Ask AI backend's provider SDK is missing. Like search provider
|
|
649
|
+
* SDKs, these are optional peers the project must install (only `gateway`
|
|
650
|
+
* needs nothing beyond the core `ai` package Blume ships) — warn early with
|
|
651
|
+
* the package name rather than let Vite fail to resolve the import opaquely.
|
|
652
|
+
* Same resolution rule as {@link searchProviderWarnings}: available if the
|
|
653
|
+
* project installed it or Blume can resolve it. `pkgDir` is injectable for
|
|
654
|
+
* testing.
|
|
655
|
+
*/
|
|
656
|
+
export const askProviderWarnings = (
|
|
657
|
+
ask: ResolvedConfig["ai"]["ask"],
|
|
658
|
+
root: string,
|
|
659
|
+
pkgDir: string = packageRoot()
|
|
660
|
+
): string[] => {
|
|
661
|
+
// An external `endpoint` means no generated route, so no SDK is imported.
|
|
662
|
+
if (!ask?.enabled || ask.endpoint) {
|
|
663
|
+
return [];
|
|
664
|
+
}
|
|
665
|
+
const dep = askBackendRuntimeDep(ask);
|
|
666
|
+
if (!dep || canResolveFrom(root, dep) || canResolveFrom(pkgDir, dep)) {
|
|
667
|
+
return [];
|
|
668
|
+
}
|
|
669
|
+
return [
|
|
670
|
+
`Ask AI provider "${ask.provider}" needs "${dep}", which isn't installed. Run \`npm install ${dep}\` (or your package manager's equivalent).`,
|
|
671
|
+
];
|
|
672
|
+
};
|
|
673
|
+
|
|
641
674
|
/** Absolute path to the configured `examples.css`, or null when unset. */
|
|
642
675
|
const examplesCssFile = (
|
|
643
676
|
root: string,
|
|
@@ -708,6 +741,9 @@ export const detectNeedsReact = async (root: string): Promise<boolean> => {
|
|
|
708
741
|
const containsMath = (content: string): boolean =>
|
|
709
742
|
content.includes("$$") || content.includes("<Math");
|
|
710
743
|
|
|
744
|
+
/** Ceiling on concurrent content-file reads; unbounded fan-out risks EMFILE. */
|
|
745
|
+
const READ_CONCURRENCY = 16;
|
|
746
|
+
|
|
711
747
|
/**
|
|
712
748
|
* Detect whether the project can render math: block math (`$$…$$`) or an
|
|
713
749
|
* explicit `<Math>` tag in any local `.md`/`.mdx`, or in staged (non-filesystem)
|
|
@@ -726,9 +762,9 @@ export const detectUsesMath = async (
|
|
|
726
762
|
ignore: ["**/node_modules/**", "**/.blume/**", "**/dist/**"],
|
|
727
763
|
onlyFiles: true,
|
|
728
764
|
});
|
|
729
|
-
const contents = await
|
|
730
|
-
|
|
731
|
-
);
|
|
765
|
+
const contents = await pMap(files, (file) => readOptional(join(root, file)), {
|
|
766
|
+
concurrency: READ_CONCURRENCY,
|
|
767
|
+
});
|
|
732
768
|
return [...contents, ...staged].some(containsMath);
|
|
733
769
|
};
|
|
734
770
|
|
|
@@ -838,33 +874,24 @@ interface LogoDimensions {
|
|
|
838
874
|
width: number;
|
|
839
875
|
}
|
|
840
876
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
const length = Number(value?.match(SVG_LENGTH)?.groups?.value);
|
|
850
|
-
return length > 0 ? length : undefined;
|
|
851
|
-
};
|
|
852
|
-
|
|
853
|
-
/** Read dimensions from an SVG's explicit size or its view box. */
|
|
877
|
+
/**
|
|
878
|
+
* Read dimensions from an SVG's explicit size or its view box. Measured with
|
|
879
|
+
* image-size — the same parser og/card.ts uses for the OG brand mark, so the
|
|
880
|
+
* header and the card can't disagree about one logo — which also tolerates
|
|
881
|
+
* the spellings the old regex missed (unquoted values, `em`/`pt` lengths, a
|
|
882
|
+
* `>` inside another attribute). An SVG with no usable size returns partial
|
|
883
|
+
* dimensions or throws; both collapse to undefined.
|
|
884
|
+
*/
|
|
854
885
|
const svgDimensions = (svg: string | undefined): LogoDimensions | undefined => {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
886
|
+
if (!svg) {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
try {
|
|
890
|
+
const { height, width } = imageSize(Buffer.from(svg));
|
|
891
|
+
return height && width ? { height, width } : undefined;
|
|
892
|
+
} catch {
|
|
893
|
+
return undefined;
|
|
860
894
|
}
|
|
861
|
-
|
|
862
|
-
const viewBox = attributes?.match(SVG_VIEW_BOX);
|
|
863
|
-
const viewBoxWidth = Number(viewBox?.groups?.width);
|
|
864
|
-
const viewBoxHeight = Number(viewBox?.groups?.height);
|
|
865
|
-
return viewBoxWidth > 0 && viewBoxHeight > 0
|
|
866
|
-
? { height: viewBoxHeight, width: viewBoxWidth }
|
|
867
|
-
: undefined;
|
|
868
895
|
};
|
|
869
896
|
|
|
870
897
|
/** Read a local SVG logo from the project root or public directory. */
|
|
@@ -883,6 +910,10 @@ const readLogoSvg = (
|
|
|
883
910
|
return file ? readFileSync(file, "utf-8") : undefined;
|
|
884
911
|
};
|
|
885
912
|
|
|
913
|
+
/** Narrows a config union's string shorthand from its object form. */
|
|
914
|
+
const isStringShorthand = <T>(value: T | string): value is string =>
|
|
915
|
+
typeof value === "string";
|
|
916
|
+
|
|
886
917
|
/**
|
|
887
918
|
* Resolve the configured logo. A single SVG is read and inlined so a
|
|
888
919
|
* `currentColor` logo follows the theme; other images keep their URL for an
|
|
@@ -893,12 +924,12 @@ const resolveLogo = (project: BlumeProject): BlumeLogo | null => {
|
|
|
893
924
|
if (!logo) {
|
|
894
925
|
return null;
|
|
895
926
|
}
|
|
896
|
-
const config =
|
|
927
|
+
const config = isStringShorthand(logo) ? { image: logo } : logo;
|
|
897
928
|
// `text` is passed through verbatim: `undefined` lets the brand fall back to
|
|
898
929
|
// the site title, `""` renders the mark alone (a logo with the wordmark baked
|
|
899
930
|
// in).
|
|
900
931
|
const { href, image: source, text } = config;
|
|
901
|
-
const image =
|
|
932
|
+
const image = isStringShorthand(source) ? { light: source } : source;
|
|
902
933
|
const light = image?.light ?? image?.dark;
|
|
903
934
|
const dark = image?.dark ?? image?.light;
|
|
904
935
|
const alt = image?.alt ?? "";
|
|
@@ -934,18 +965,18 @@ const FAVICON_CANDIDATES = [
|
|
|
934
965
|
];
|
|
935
966
|
|
|
936
967
|
/** `<link type>` MIME for the favicon extensions we recognize. */
|
|
937
|
-
const FAVICON_TYPES
|
|
938
|
-
ico
|
|
939
|
-
jpeg
|
|
940
|
-
jpg
|
|
941
|
-
png
|
|
942
|
-
svg
|
|
943
|
-
|
|
968
|
+
const FAVICON_TYPES = new Map([
|
|
969
|
+
["ico", "image/x-icon"],
|
|
970
|
+
["jpeg", "image/jpeg"],
|
|
971
|
+
["jpg", "image/jpeg"],
|
|
972
|
+
["png", "image/png"],
|
|
973
|
+
["svg", "image/svg+xml"],
|
|
974
|
+
]);
|
|
944
975
|
|
|
945
976
|
/** Infer the `<link type>` MIME from a filename, when we recognize the extension. */
|
|
946
977
|
const faviconType = (name: string): string | undefined => {
|
|
947
978
|
const ext = name.split(".").pop()?.toLowerCase();
|
|
948
|
-
return ext ? FAVICON_TYPES
|
|
979
|
+
return ext ? FAVICON_TYPES.get(ext) : undefined;
|
|
949
980
|
};
|
|
950
981
|
|
|
951
982
|
/** Read a file and encode it as a `data:` URI of the given MIME type. */
|
|
@@ -1018,7 +1049,7 @@ const resolveBanner = (config: ResolvedConfig): BlumeBanner | null => {
|
|
|
1018
1049
|
if (!banner) {
|
|
1019
1050
|
return null;
|
|
1020
1051
|
}
|
|
1021
|
-
if (
|
|
1052
|
+
if (isStringShorthand(banner)) {
|
|
1022
1053
|
return { content: banner, dismissible: false, key: banner };
|
|
1023
1054
|
}
|
|
1024
1055
|
return {
|
|
@@ -1211,6 +1242,7 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1211
1242
|
theme: config.theme,
|
|
1212
1243
|
title: config.title,
|
|
1213
1244
|
toc: config.toc,
|
|
1245
|
+
versions: config.versions ?? null,
|
|
1214
1246
|
webmcp: {
|
|
1215
1247
|
enabled: config.ai.webmcp,
|
|
1216
1248
|
llms: config.ai.llmsTxt.enabled,
|
|
@@ -1227,6 +1259,19 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1227
1259
|
navigation: withRepoUrl(graph.navigation),
|
|
1228
1260
|
// Per-locale navigation; the catch-all selects the active locale's tree.
|
|
1229
1261
|
navigationByLocale,
|
|
1262
|
+
// Per-archived-version navigation; the catch-all selects by the route's
|
|
1263
|
+
// version, then locale.
|
|
1264
|
+
navigationByVersion: Object.fromEntries(
|
|
1265
|
+
Object.entries(graph.navigationByVersion).map(([id, byLocale]) => [
|
|
1266
|
+
id,
|
|
1267
|
+
Object.fromEntries(
|
|
1268
|
+
Object.entries(byLocale).map(([code, nav]) => [
|
|
1269
|
+
code,
|
|
1270
|
+
withRepoUrl(nav),
|
|
1271
|
+
])
|
|
1272
|
+
),
|
|
1273
|
+
])
|
|
1274
|
+
),
|
|
1230
1275
|
routes: manifest.routes.map((route) => ({
|
|
1231
1276
|
alternates: route.alternates,
|
|
1232
1277
|
collection: route.collection,
|
|
@@ -1241,6 +1286,8 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1241
1286
|
locale: route.locale,
|
|
1242
1287
|
path: route.path,
|
|
1243
1288
|
title: route.title,
|
|
1289
|
+
version: route.version,
|
|
1290
|
+
versionAlternates: route.versionAlternates,
|
|
1244
1291
|
})),
|
|
1245
1292
|
// Default-locale chrome strings (English baseline when not under i18n).
|
|
1246
1293
|
ui: defaultUi,
|
|
@@ -1370,7 +1417,7 @@ const writeAskFiles = async (
|
|
|
1370
1417
|
}
|
|
1371
1418
|
await write(
|
|
1372
1419
|
join(srcDir, "pages", "api", "ask.ts"),
|
|
1373
|
-
askEndpointTemplate(resolveAskBackend(ask), grounded)
|
|
1420
|
+
askEndpointTemplate(resolveAskBackend(ask), grounded, ask.instructions)
|
|
1374
1421
|
);
|
|
1375
1422
|
};
|
|
1376
1423
|
|
|
@@ -1838,8 +1885,9 @@ export const generateRuntime = async (
|
|
|
1838
1885
|
]);
|
|
1839
1886
|
}
|
|
1840
1887
|
|
|
1841
|
-
// API/AsyncAPI reference pages (
|
|
1842
|
-
// mounted on its configured route and
|
|
1888
|
+
// Scalar-rendered API/AsyncAPI reference pages (`renderer: "scalar"`). One
|
|
1889
|
+
// self-contained page per source, mounted on its configured route and
|
|
1890
|
+
// regenerated each run.
|
|
1843
1891
|
const warnings: string[] = [
|
|
1844
1892
|
...(depsLinkWarning ? [depsLinkWarning] : []),
|
|
1845
1893
|
...reactCompilerWarnings(config, needsReact, reactCompilerPath),
|
|
@@ -1878,10 +1926,11 @@ export const generateRuntime = async (
|
|
|
1878
1926
|
...islandDiscovery.islands.map((island) => island.name),
|
|
1879
1927
|
...overrideTags,
|
|
1880
1928
|
]);
|
|
1881
|
-
// Missing-dependency preflights: the search provider's SDK, the
|
|
1882
|
-
// adapter's package, and — since
|
|
1883
|
-
// don't — any island framework's
|
|
1884
|
-
// let Vite fail to resolve them
|
|
1929
|
+
// Missing-dependency preflights: the search provider's SDK, the Ask AI
|
|
1930
|
+
// backend's provider SDK, the deployment adapter's package, and — since
|
|
1931
|
+
// React ships with Blume while Vue/Svelte don't — any island framework's
|
|
1932
|
+
// Astro integration. Warn early rather than let Vite fail to resolve them
|
|
1933
|
+
// opaquely.
|
|
1885
1934
|
warnings.push(
|
|
1886
1935
|
...validateUsedComponents(
|
|
1887
1936
|
project.graph.pages,
|
|
@@ -1889,6 +1938,7 @@ export const generateRuntime = async (
|
|
|
1889
1938
|
new Set(registry.map((item) => item.name))
|
|
1890
1939
|
).map(diagnosticWarning),
|
|
1891
1940
|
...searchProviderWarnings(config.search.provider, context.root),
|
|
1941
|
+
...askProviderWarnings(config.ai.ask, context.root),
|
|
1892
1942
|
...deploymentAdapterWarnings(config.deployment, context.root),
|
|
1893
1943
|
...islandFrameworkWarnings(frameworks, context.root)
|
|
1894
1944
|
);
|
package/src/astro/integration.ts
CHANGED
|
@@ -6,9 +6,20 @@ import { enrichDiagnostic } from "../core/diagnostics.ts";
|
|
|
6
6
|
import type { Diagnostic } from "../core/types.ts";
|
|
7
7
|
import { markdownVariantUrl, prefersMarkdown } from "./markdown-negotiation.ts";
|
|
8
8
|
|
|
9
|
+
/** The `{ type: "error" }` payload Vite's browser overlay renders. */
|
|
10
|
+
interface OverlayErrorPayload {
|
|
11
|
+
err: {
|
|
12
|
+
id?: string;
|
|
13
|
+
message: string;
|
|
14
|
+
plugin: string;
|
|
15
|
+
stack: string;
|
|
16
|
+
};
|
|
17
|
+
type: "error";
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
/** The dev server's HMR channel — either `.ws` (Vite ≤5) or `.hot` (Vite 6+). */
|
|
10
21
|
interface OverlayChannel {
|
|
11
|
-
send: (payload:
|
|
22
|
+
send: (payload: OverlayErrorPayload) => void;
|
|
12
23
|
}
|
|
13
24
|
interface OverlayServer {
|
|
14
25
|
hot?: OverlayChannel;
|
|
@@ -164,7 +175,7 @@ export const blumeIntegration = (
|
|
|
164
175
|
"astro:server:setup": ({ server }) => {
|
|
165
176
|
// Keep a handle on the dev server so Blume diagnostics can be pushed to
|
|
166
177
|
// its browser error overlay (see `showBlumeErrorOverlay`).
|
|
167
|
-
overlayServer = server
|
|
178
|
+
overlayServer = server;
|
|
168
179
|
// Prepend so the rewrite happens before Astro's own request handler,
|
|
169
180
|
// letting the rewritten URL resolve to the `.md` endpoint.
|
|
170
181
|
server.middlewares.stack.unshift({
|
package/src/astro/islands.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
|
|
3
|
+
import pMap from "p-map";
|
|
3
4
|
import { basename, join } from "pathe";
|
|
4
5
|
import { glob } from "tinyglobby";
|
|
5
6
|
|
|
@@ -29,6 +30,9 @@ export interface IslandDiscovery {
|
|
|
29
30
|
/** Hydration mode used when an island doesn't declare one. */
|
|
30
31
|
const DEFAULT_CLIENT: IslandClientMode = "visible";
|
|
31
32
|
|
|
33
|
+
/** Ceiling on concurrent island-file reads; unbounded fan-out risks EMFILE. */
|
|
34
|
+
const READ_CONCURRENCY = 16;
|
|
35
|
+
|
|
32
36
|
const VALID_MODES = new Set<IslandClientMode>([
|
|
33
37
|
"idle",
|
|
34
38
|
"load",
|
|
@@ -36,13 +40,20 @@ const VALID_MODES = new Set<IslandClientMode>([
|
|
|
36
40
|
"visible",
|
|
37
41
|
]);
|
|
38
42
|
|
|
43
|
+
/** Whether a declared client mode is one Astro's directives support. */
|
|
44
|
+
const isClientMode = (mode: string): mode is IslandClientMode =>
|
|
45
|
+
// SAFETY: `Set.has` only compares identity at runtime; the assertion widens
|
|
46
|
+
// the lookup key so the narrower-typed set accepts it, and the `has` result
|
|
47
|
+
// is exactly the predicate being claimed.
|
|
48
|
+
VALID_MODES.has(mode as IslandClientMode);
|
|
49
|
+
|
|
39
50
|
/** Island extensions mapped to the Astro renderer that handles them. */
|
|
40
|
-
const FRAMEWORK_BY_EXT
|
|
41
|
-
jsx
|
|
42
|
-
svelte
|
|
43
|
-
tsx
|
|
44
|
-
vue
|
|
45
|
-
|
|
51
|
+
const FRAMEWORK_BY_EXT = new Map<string, IslandFramework>([
|
|
52
|
+
["jsx", "react"],
|
|
53
|
+
["svelte", "svelte"],
|
|
54
|
+
["tsx", "react"],
|
|
55
|
+
["vue", "vue"],
|
|
56
|
+
]);
|
|
46
57
|
|
|
47
58
|
// Captures the extension so we can both strip it from the name and pick the
|
|
48
59
|
// framework. Kept in sync with the glob below.
|
|
@@ -62,13 +73,13 @@ export const readClientMode = (
|
|
|
62
73
|
if (!mode) {
|
|
63
74
|
return DEFAULT_CLIENT;
|
|
64
75
|
}
|
|
65
|
-
if (!
|
|
76
|
+
if (!isClientMode(mode)) {
|
|
66
77
|
warnings.push(
|
|
67
78
|
`Island "${file}" declares an unknown client mode "${mode}"; defaulting to "${DEFAULT_CLIENT}". Use "load", "idle", "visible", or "only".`
|
|
68
79
|
);
|
|
69
80
|
return DEFAULT_CLIENT;
|
|
70
81
|
}
|
|
71
|
-
return mode
|
|
82
|
+
return mode;
|
|
72
83
|
};
|
|
73
84
|
|
|
74
85
|
/**
|
|
@@ -92,9 +103,9 @@ export const discoverIslands = async (
|
|
|
92
103
|
onlyFiles: true,
|
|
93
104
|
});
|
|
94
105
|
const files = matches.toSorted();
|
|
95
|
-
const sources = await
|
|
96
|
-
|
|
97
|
-
);
|
|
106
|
+
const sources = await pMap(files, (file) => readFile(file, "utf-8"), {
|
|
107
|
+
concurrency: READ_CONCURRENCY,
|
|
108
|
+
});
|
|
98
109
|
|
|
99
110
|
const islands: IslandSpec[] = [];
|
|
100
111
|
const warnings: string[] = [];
|
|
@@ -105,7 +116,7 @@ export const discoverIslands = async (
|
|
|
105
116
|
const collectIsland = (file: string, source: string): void => {
|
|
106
117
|
const base = basename(file);
|
|
107
118
|
const ext = base.match(ISLAND_FILE)?.groups?.ext;
|
|
108
|
-
const framework = ext ? FRAMEWORK_BY_EXT
|
|
119
|
+
const framework = ext ? FRAMEWORK_BY_EXT.get(ext) : undefined;
|
|
109
120
|
if (!framework) {
|
|
110
121
|
return;
|
|
111
122
|
}
|