blume 1.1.1 → 1.1.3

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/cli/index.js +175 -26
  3. package/dist/cli/index.js.map +16 -16
  4. package/dist/types/ai/component-markdown.d.ts +10 -0
  5. package/dist/types/core/config-input.d.ts +50 -5
  6. package/dist/types/core/i18n-ui.d.ts +24 -24
  7. package/dist/types/core/schema.d.ts +301 -132
  8. package/dist/types/core/types.d.ts +21 -0
  9. package/dist/types/markdown/themes.d.ts +21 -0
  10. package/dist/types/openapi/references.d.ts +5 -0
  11. package/docs/advanced/api-reference.mdx +20 -0
  12. package/docs/configuration/index.mdx +28 -1
  13. package/docs/content/components.mdx +1 -1
  14. package/docs/content/syntax.mdx +14 -0
  15. package/package.json +1 -1
  16. package/src/ai/component-markdown.ts +28 -0
  17. package/src/ai/llms.ts +11 -2
  18. package/src/ai/markdown.ts +12 -6
  19. package/src/astro/examples.ts +13 -0
  20. package/src/astro/generate.ts +95 -14
  21. package/src/astro/templates.ts +48 -7
  22. package/src/components/content/Component.astro +99 -6
  23. package/src/components/content/diff.ts +53 -4
  24. package/src/components/layout/Logo.astro +2 -2
  25. package/src/components/layout/RootLayout.astro +23 -6
  26. package/src/components/layout/nav-utils.ts +18 -7
  27. package/src/components/openapi/ApiTagOperations.astro +17 -8
  28. package/src/core/config-input.ts +51 -5
  29. package/src/core/date-format.ts +17 -0
  30. package/src/core/navigation.ts +11 -7
  31. package/src/core/project-graph.ts +9 -0
  32. package/src/core/schema.ts +96 -2
  33. package/src/core/types.ts +23 -0
  34. package/src/markdown/index.ts +2 -0
  35. package/src/markdown/inline-code.ts +1 -1
  36. package/src/markdown/themes.ts +7 -2
  37. package/src/openapi/references.ts +6 -0
  38. package/src/openapi/scalar.ts +4 -0
  39. package/src/registry/eject.ts +6 -3
  40. package/src/theme/entry.ts +7 -0
  41. package/src/theme/twoslash.ts +10 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # blume
2
2
 
3
+ ## 1.1.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 42522fc: Downlevel `<Component>` to its example's source in agent-facing Markdown. The `/<route>.md` mirror, `llms-full.txt`, and the MCP `get_page` tool now render `<Component path="…" />` as a fenced code block of the example's source (the same code the on-page "Code" tab shows) instead of leaving the raw JSX tag, so agents reading a page get the component's code rather than an opaque element. An unknown path (or a missing `path`) is left verbatim, and a same-name `ai.markdownComponents` serializer still overrides the built-in.
8
+ - a71f70e: Add a `dateFormat` config option for the "last updated" stamp and the changelog timeline. Both surfaces previously hardcoded `dateStyle: "long"`; they now share a configurable pass-through to `Intl.DateTimeFormat` options, defaulting to `{ dateStyle: "long" }` so existing sites are unchanged. Set a preset (`dateFormat: { dateStyle: "medium" }`) or a numeric house style (`dateFormat: { year: "numeric", month: "2-digit", day: "2-digit" }`); dates still render in the site's locale and in UTC unless a `timeZone` is given.
9
+ - 68fc939: Harden the agent-facing code-fence helper against a polynomial-time regex (ReDoS). Trailing newlines are now stripped with an unambiguous pattern, so example source with many interior blank lines can't force quadratic backtracking.
10
+ - ee77cfd: Stop long OpenAPI routes from overflowing the native API reference layout. An operation's heading now wraps a long `METHOD /path` title instead of clipping it off the content column, and the overview list rows stack the summary over the route (each getting the full row width) and wrap a long route inside the card — dropping the duplicate path that overlapped the label when a spec sets no summary.
11
+ - 93a94a2: Fix two responsive/mobile layout issues. Twoslash code blocks now wrap their lines on narrow screens instead of pushing the page sideways (hover popups still escape as before), and a long site title in the header now truncates on one line instead of wrapping into the fixed-height bar.
12
+ - a27c543: Add a `scalar` passthrough object to the `openapi` and `asyncapi` config blocks (Scalar renderer). Any [Scalar configuration](https://github.com/scalar/scalar/blob/main/documentation/configuration.md) set there is forwarded verbatim to the embedded `<ScalarComponent>` — `localization` (to translate Scalar's own UI), `agent`, `hideTestRequestButton`, `orderSchemaPropertiesBy`, and the rest. Options in the `scalar` object win over Blume's derived spec/theme config, making it a full escape hatch to Scalar's API; the dedicated `theme` field remains the ergonomic shorthand.
13
+ - fa07dc4: Create the `.blume/node_modules` dependency junction when an isolated linker (Bun's `isolated` mode, pnpm) dedupes the workspace's own `astro` dependency to Blume's copy. The walk from `.blume/` found the "correct" astro through the workspace's direct-dep symlink — in a directory holding none of Blume's integrations — so the junction was skipped and the build died on `Cannot find module '@astrojs/mdx'`.
14
+
15
+ ## 1.1.2
16
+
17
+ ### Patch Changes
18
+
19
+ - 6cf2995: Allow custom Shiki theme objects in `markdown.codeBlocks.theme.light` and `markdown.codeBlocks.theme.dark`. Custom themes now flow through fenced code, inline highlighted code, `<CodeBlock>`, and `<Diff>` alongside bundled Shiki theme names.
20
+ - cb30a40: Recognize the localized/based root tab when scoping the sidebar. With i18n enabled and header tabs configured, a non-default locale's tabs arrive localized (`/` becomes `/en`), but render-time scoping compared the active tab against a bare `/` — so on any `/en/...` route the root tab was misread as a section tab, and a root-level `(group)` folder (whose path is exactly the locale prefix) collapsed the sidebar to that one group. The same bare comparison blanked the sidebar entirely under a `basePath` with a root tab. The navigation now carries its root in the tabs' own path space (`/`, `/en`, `/docs`) and the sidebar scoping compares against it, so non-default locales show the full tree minus tab-owned sections, matching the default locale.
21
+ - aa3f588: Size `<Component>` preview panes to the rendered example instead of the source line count. The generated frame page now observes the example with a ResizeObserver and reports its height to the docs page, which applies it to both the Preview and Code tabs — so short sources that render tall UIs no longer clip, and long sources that render small components no longer float in dead space. The line-count estimate remains the SSR/no-JS initial height (288px floor, 400px ceiling; the measured height is unceilinged up to the viewport), with a height transition so the settle on lazy load doesn't snap. Examples that resize after load keep the pane in sync, and viewport-tracking examples (`h-screen`) can't feed the measurement back into unbounded growth.
22
+ - 103733d: Re-point a cache-restored `.blume/node_modules` junction that resolves a superseded Blume install. A restored build cache (e.g. Vercel's) could resurrect the junction pointing into the previous release's store directory; because releases rarely bump Astro, every astro-based health check passed straight through the stale link, and the freshly generated Astro config then imported the old package — crashing on any export added since (`blumeTwoslashTransformer is not a function`). The junction is now dropped and relinked whenever the directory behind it holds a `blume` other than the one running.
23
+
3
24
  ## 1.1.1
4
25
 
5
26
  ### Patch Changes
package/dist/cli/index.js CHANGED
@@ -3106,6 +3106,7 @@ var referencesFor = (kind, block, defaultLabel, renderer, display, basePath) =>
3106
3106
  label,
3107
3107
  renderer,
3108
3108
  route,
3109
+ scalar: block.scalar,
3109
3110
  slug: routeSlug(route),
3110
3111
  spec: source.spec,
3111
3112
  theme: block.theme
@@ -4327,6 +4328,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
4327
4328
  page={{ title: seo.title ?? title, description: seo.description ?? frontmatter.description, route }}
4328
4329
  headings={headings}
4329
4330
  toc={data.config.toc}
4331
+ dateFormat={data.config.dateFormat}
4330
4332
  themeMode={data.config.theme.mode}
4331
4333
  fontCssVars={data.fontCssVars}
4332
4334
  searchEnabled={data.config.search.enabled}
@@ -4365,6 +4367,7 @@ import RootLayout from "blume/components/layout/RootLayout.astro";
4365
4367
  import Update from "blume/components/content/Update.astro";
4366
4368
  import { withBase } from "blume/components/islands/base-path.ts";
4367
4369
  import { resolveSlot } from "blume/components/layout/overrides.ts";
4370
+ import { resolveDateFormatOptions } from "blume/core/date-format.ts";
4368
4371
  import { layoutOverrides } from "../generated/components.ts";
4369
4372
  import data from "blume:data";
4370
4373
 
@@ -4392,8 +4395,9 @@ const localeMeta = i18n
4392
4395
  const dir = localeMeta?.dir ?? "ltr";
4393
4396
  const htmlLang = i18n ? i18n.defaultLocale : "en";
4394
4397
 
4395
- // Formatted in the same locale as the chrome, and in UTC, to match the
4396
- // per-page "last updated" stamp.
4398
+ // Formatted in the same locale as the chrome, and with the configured
4399
+ // \`dateFormat\` (UTC by default), to match the per-page "last updated" stamp.
4400
+ const dateFormatOptions = resolveDateFormatOptions(data.config.dateFormat);
4397
4401
  const formatDate = (value: string | null | undefined) => {
4398
4402
  if (!value) {
4399
4403
  return;
@@ -4401,10 +4405,7 @@ const formatDate = (value: string | null | undefined) => {
4401
4405
  const date = new Date(value);
4402
4406
  return Number.isNaN(date.getTime())
4403
4407
  ? undefined
4404
- : new Intl.DateTimeFormat(htmlLang, {
4405
- dateStyle: "long",
4406
- timeZone: "UTC",
4407
- }).format(date);
4408
+ : new Intl.DateTimeFormat(htmlLang, dateFormatOptions).format(date);
4408
4409
  };
4409
4410
 
4410
4411
  const slugify = (text: string) =>
@@ -4770,7 +4771,42 @@ const Example = entry.Component;
4770
4771
  <!-- Flex + margin:auto centers the example and, unlike place-items, keeps
4771
4772
  the top edge reachable when the example outgrows the frame. -->
4772
4773
  <body style="display:flex;min-height:100svh;padding:1.5rem">
4773
- <div style="margin:auto"><Example /></div>
4774
+ <div data-blume-example style="margin:auto"><Example /></div>
4775
+ <script is:inline>
4776
+ (() => {
4777
+ // Report the example's rendered height so the embedding docs page can
4778
+ // size the preview pane to the content. The wrapper is observed rather
4779
+ // than the body: the body stretches to the frame's own height, so it
4780
+ // would only echo the pane back. Direct opens have no distinct parent
4781
+ // and skip out; the frame is same-origin with the docs page (see the
4782
+ // theme sync above), so the origin is pinned on both ends.
4783
+ if (window.parent === window) {
4784
+ return;
4785
+ }
4786
+ const wrapper = document.querySelector("[data-blume-example]");
4787
+ if (!wrapper) {
4788
+ return;
4789
+ }
4790
+ // The body's padding frames the example; fold it into the report so
4791
+ // the parent can apply the number as-is. Read from the live value —
4792
+ // the user's examples.css is injected after Blume's defaults precisely
4793
+ // so their tokens win, so a root font-size override must be honored
4794
+ // rather than assuming 1.5rem is 48px.
4795
+ const bodyStyle = getComputedStyle(document.body);
4796
+ const paddingPx =
4797
+ parseFloat(bodyStyle.paddingTop) + parseFloat(bodyStyle.paddingBottom);
4798
+ new ResizeObserver(() => {
4799
+ window.parent.postMessage(
4800
+ {
4801
+ height:
4802
+ Math.ceil(wrapper.getBoundingClientRect().height) + paddingPx,
4803
+ type: "blume:example-height",
4804
+ },
4805
+ window.location.origin
4806
+ );
4807
+ }).observe(wrapper);
4808
+ })();
4809
+ </script>
4774
4810
  </body>
4775
4811
  </html>
4776
4812
  `;
@@ -7927,9 +7963,23 @@ var githubConfigSchema = z2.strictObject({
7927
7963
  owner: z2.string(),
7928
7964
  repo: z2.string()
7929
7965
  });
7966
+ var codeThemeSchema = z2.custom((value) => {
7967
+ if (typeof value === "string") {
7968
+ return true;
7969
+ }
7970
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
7971
+ return false;
7972
+ }
7973
+ const theme = value;
7974
+ const settingsValid = theme.settings === undefined || Array.isArray(theme.settings);
7975
+ const tokenColorsValid = theme.tokenColors === undefined || Array.isArray(theme.tokenColors);
7976
+ const colorsValid = theme.colors === undefined || typeof theme.colors === "object" && theme.colors !== null && !Array.isArray(theme.colors);
7977
+ const hasContent = theme.settings !== undefined || theme.tokenColors !== undefined || theme.colors !== undefined;
7978
+ return settingsValid && tokenColorsValid && colorsValid && hasContent;
7979
+ }, "Expected a Shiki theme name or custom theme object");
7930
7980
  var codeBlockThemeSchema = z2.strictObject({
7931
- dark: z2.string().default("github-dark"),
7932
- light: z2.string().default("github-light")
7981
+ dark: codeThemeSchema.default("github-dark"),
7982
+ light: codeThemeSchema.default("github-light")
7933
7983
  });
7934
7984
  var codeBlocksConfigSchema = z2.strictObject({
7935
7985
  theme: codeBlockThemeSchema.default({})
@@ -7945,6 +7995,19 @@ var lastModifiedConfigSchema = z2.union([
7945
7995
  z2.boolean(),
7946
7996
  z2.strictObject({ type: z2.enum(["git", "frontmatter"]).default("git") })
7947
7997
  ]);
7998
+ var dateFormatConfigSchema = z2.strictObject({
7999
+ calendar: z2.string().optional(),
8000
+ dateStyle: z2.enum(["full", "long", "medium", "short"]).optional(),
8001
+ day: z2.enum(["numeric", "2-digit"]).optional(),
8002
+ era: z2.enum(["long", "short", "narrow"]).optional(),
8003
+ month: z2.enum(["numeric", "2-digit", "long", "short", "narrow"]).optional(),
8004
+ numberingSystem: z2.string().optional(),
8005
+ timeZone: z2.string().optional(),
8006
+ weekday: z2.enum(["long", "short", "narrow"]).optional(),
8007
+ year: z2.enum(["numeric", "2-digit"]).optional()
8008
+ }).refine((value) => value.dateStyle === undefined || value.weekday === undefined && value.era === undefined && value.year === undefined && value.month === undefined && value.day === undefined, {
8009
+ message: "dateFormat.dateStyle can't be combined with weekday/era/year/month/day; use one or the other."
8010
+ });
7948
8011
  var codeConfigSchema = z2.strictObject({
7949
8012
  icons: z2.boolean().default(true),
7950
8013
  wrap: z2.boolean().default(false)
@@ -7963,12 +8026,14 @@ var openapiSourceSchema = z2.strictObject({
7963
8026
  route: z2.string().optional(),
7964
8027
  spec: z2.string()
7965
8028
  });
8029
+ var scalarConfigSchema = z2.record(z2.string(), z2.unknown()).optional();
7966
8030
  var openapiConfigSchema = z2.strictObject({
7967
8031
  codeSamples: z2.array(z2.string()).default(["curl", "js", "python"]),
7968
8032
  enabled: z2.boolean().default(false),
7969
8033
  expandSchemas: z2.boolean().default(false),
7970
8034
  renderer: z2.enum(["blume", "scalar"]).default("blume"),
7971
8035
  route: z2.string().default("/reference"),
8036
+ scalar: scalarConfigSchema,
7972
8037
  sources: z2.array(openapiSourceSchema).default([]),
7973
8038
  spec: z2.string().optional(),
7974
8039
  theme: z2.string().optional()
@@ -7976,6 +8041,7 @@ var openapiConfigSchema = z2.strictObject({
7976
8041
  var asyncapiConfigSchema = z2.strictObject({
7977
8042
  enabled: z2.boolean().default(false),
7978
8043
  route: z2.string().default("/events"),
8044
+ scalar: scalarConfigSchema,
7979
8045
  sources: z2.array(openapiSourceSchema).default([]),
7980
8046
  spec: z2.string().optional(),
7981
8047
  theme: z2.string().optional()
@@ -8020,6 +8086,7 @@ var blumeConfigSchema = z2.strictObject({
8020
8086
  banner: bannerConfigSchema.optional(),
8021
8087
  basePath: z2.string().optional().transform((value) => normalizeBasePath(value)),
8022
8088
  content: contentConfigSchema.default({}),
8089
+ dateFormat: dateFormatConfigSchema.default({ dateStyle: "long" }),
8023
8090
  deployment: deploymentConfigSchema.default({}),
8024
8091
  description: z2.string().optional(),
8025
8092
  examples: examplesConfigSchema.default("examples"),
@@ -8738,19 +8805,21 @@ var buildNavigation = (pages, options) => {
8738
8805
  }
8739
8806
  }
8740
8807
  }
8808
+ const rootTabPath = withBasePath(basePath, options.localizedRoot ?? "/");
8741
8809
  if (options.sidebar) {
8742
8810
  const sidebar2 = buildConfigSidebar(options.sidebar, byRoute, display, basePath);
8743
8811
  return {
8744
8812
  featured,
8813
+ root: rootTabPath,
8745
8814
  selectors,
8746
8815
  sidebar: sidebar2,
8747
8816
  tabs: withTabHrefs(tabs, sidebar2)
8748
8817
  };
8749
8818
  }
8750
- const rootTabPath = withBasePath(basePath, options.localizedRoot ?? "/");
8751
8819
  const sidebar = buildFileSystemSidebar(pages, options.folderMeta, sharedFolderMeta, metaPrefix, display, new Set(tabs.flatMap((tab) => tab.path === rootTabPath ? [] : [tab.path])), options.diagnostics);
8752
8820
  return {
8753
8821
  featured,
8822
+ root: rootTabPath,
8754
8823
  selectors,
8755
8824
  sidebar,
8756
8825
  tabs: withTabHrefs(tabs, sidebar)
@@ -11683,6 +11752,22 @@ var youtube = ({ props }) => {
11683
11752
  const title = typeof props.title === "string" && props.title !== "" ? props.title : "Watch on YouTube";
11684
11753
  return `[${title}](https://www.youtube.com/watch?v=${videoId}${start})`;
11685
11754
  };
11755
+ var fencedBlock = (lang, code) => {
11756
+ const trimmed = code.replace(/(?<!\n)\n+$/u, "");
11757
+ const runs = trimmed.match(/`+/gu);
11758
+ const longest = runs ? Math.max(...runs.map((run) => run.length)) : 0;
11759
+ const fence = "`".repeat(Math.max(3, longest + 1));
11760
+ return `${fence}${lang}
11761
+ ${trimmed}
11762
+ ${fence}`;
11763
+ };
11764
+ var exampleComponentSerializers = (examples) => ({
11765
+ Component: ({ props }) => {
11766
+ const path = typeof props.path === "string" ? props.path : undefined;
11767
+ const example = path === undefined ? undefined : examples[path];
11768
+ return example ? fencedBlock(example.lang, example.source) : null;
11769
+ }
11770
+ });
11686
11771
  var SERIALIZERS = {
11687
11772
  Callout: callout,
11688
11773
  Steps: steps,
@@ -11878,10 +11963,14 @@ var buildIndex = (project) => {
11878
11963
  var buildFull = async (project) => {
11879
11964
  const { config } = project;
11880
11965
  const pages = eligiblePages(project).toSorted((a, b) => a.route.localeCompare(b.route));
11966
+ const components = {
11967
+ ...exampleComponentSerializers(project.examples ?? {}),
11968
+ ...config.ai.markdownComponents
11969
+ };
11881
11970
  const sections = await Promise.all(pages.map(async (page) => {
11882
11971
  const raw = await readEntryText(project, page);
11883
11972
  const parsed = frontmatter_default(raw);
11884
- const body = downlevelComponents(applyAgentVisibility(parsed.content), config.ai.markdownComponents, parsed.data).trim();
11973
+ const body = downlevelComponents(applyAgentVisibility(parsed.content), components, parsed.data).trim();
11885
11974
  const url = pageUrl(page.route, config.deployment.site, normalizeBasePath(config.deployment.base));
11886
11975
  return [`# ${page.title}`, `Source: ${url}`, "", body].join(`
11887
11976
  `);
@@ -12488,6 +12577,7 @@ import {
12488
12577
  lstat,
12489
12578
  mkdir as mkdir6,
12490
12579
  readFile as readFile14,
12580
+ realpath,
12491
12581
  rename,
12492
12582
  rm as rm2,
12493
12583
  symlink,
@@ -12522,6 +12612,10 @@ import { readFile as readFile10 } from "node:fs/promises";
12522
12612
  var agentMarkdown = (entry) => entry.md ?? entry.mdx;
12523
12613
  var buildRawMarkdown = async (project) => {
12524
12614
  const pageById = new Map(project.graph.pages.map((page) => [page.id, page]));
12615
+ const components = {
12616
+ ...exampleComponentSerializers(project.examples ?? {}),
12617
+ ...project.config.ai.markdownComponents
12618
+ };
12525
12619
  const readRoute = async (route) => {
12526
12620
  const page = pageById.get(route.id);
12527
12621
  if (page) {
@@ -12531,7 +12625,7 @@ var buildRawMarkdown = async (project) => {
12531
12625
  };
12532
12626
  const entries = await Promise.all(project.manifest.routes.map(async (route) => {
12533
12627
  const source = applyAgentVisibility(await readRoute(route));
12534
- const md = downlevelComponents(source, project.config.ai.markdownComponents, frontmatter_default(source).data);
12628
+ const md = downlevelComponents(source, components, frontmatter_default(source).data);
12535
12629
  const entry = md === source ? { mdx: source } : { md, mdx: source };
12536
12630
  return [route.path, entry];
12537
12631
  }));
@@ -13299,7 +13393,8 @@ var buildReferenceFiles = async (options) => {
13299
13393
  content: scalarReferenceTemplate({
13300
13394
  configuration: {
13301
13395
  ...spec.config,
13302
- ...themeConfiguration(config, ref.theme)
13396
+ ...themeConfiguration(config, ref.theme),
13397
+ ...ref.scalar
13303
13398
  },
13304
13399
  dataImport: `${"../".repeat(depth + 1)}generated/data.json`,
13305
13400
  route: ref.route,
@@ -13530,6 +13625,13 @@ ${THEME_MAPPING}
13530
13625
  letter-spacing: 0;
13531
13626
  }
13532
13627
 
13628
+ /* A heading can carry one long unbreakable token — an OpenAPI operation's title
13629
+ is \`METHOD /very/long/{path}\` when the spec sets no summary — which would run
13630
+ off the content column. Break it across lines instead of overflowing. */
13631
+ .prose :where(h1, h2, h3, h4, h5, h6) {
13632
+ overflow-wrap: break-word;
13633
+ }
13634
+
13533
13635
  .prose :where(h1) {
13534
13636
  font-size: 3rem;
13535
13637
  line-height: 1.1;
@@ -14080,6 +14182,16 @@ var OVERRIDES = `
14080
14182
  padding-right: 1.25rem;
14081
14183
  }
14082
14184
 
14185
+ /* Twoslash blocks keep \`overflow: visible\` so popups can escape, which means
14186
+ long lines can't scroll — they'd push past the viewport on narrow screens.
14187
+ Wrap them on mobile instead; hover popups still position against the token. */
14188
+ @media (max-width: 640px) {
14189
+ .prose pre.twoslash code {
14190
+ white-space: pre-wrap;
14191
+ overflow-wrap: anywhere;
14192
+ }
14193
+ }
14194
+
14083
14195
  /* The rich renderer renders each popup's type signature as a nested Shiki
14084
14196
  pre. Strip the code-block chrome (border, radius, padding, background) so it
14085
14197
  sits flush inside the popup, which owns the frame. */
@@ -14358,6 +14470,10 @@ var discoverExamples = async (root, pattern = "examples") => {
14358
14470
  }
14359
14471
  return { examples, warnings };
14360
14472
  };
14473
+ var exampleMarkdownLookup = (examples) => Object.fromEntries(examples.map((example) => [
14474
+ example.path,
14475
+ { lang: example.lang, source: example.source }
14476
+ ]));
14361
14477
 
14362
14478
  // src/astro/generate.ts
14363
14479
  var BLUME_SRC = join26(packageRoot(), "src");
@@ -14389,12 +14505,13 @@ var resolveAstroPackageJson = (modulesDir) => {
14389
14505
  return null;
14390
14506
  }
14391
14507
  };
14392
- var resolvedAstroPath = (fromDir) => {
14508
+ var resolvedAstroHit = (fromDir) => {
14393
14509
  let dir = normalize3(fromDir);
14394
14510
  while (true) {
14395
- const resolved = resolveAstroPackageJson(join26(dir, "node_modules"));
14396
- if (resolved) {
14397
- return resolved;
14511
+ const modulesDir = join26(dir, "node_modules");
14512
+ const pkg = resolveAstroPackageJson(modulesDir);
14513
+ if (pkg) {
14514
+ return { modulesDir, pkg };
14398
14515
  }
14399
14516
  const parent = dirname9(dir);
14400
14517
  if (parent === dir) {
@@ -14403,6 +14520,13 @@ var resolvedAstroPath = (fromDir) => {
14403
14520
  dir = parent;
14404
14521
  }
14405
14522
  };
14523
+ var sameRealDir = (a, b) => {
14524
+ try {
14525
+ return realpathSync(a) === realpathSync(b);
14526
+ } catch {
14527
+ return false;
14528
+ }
14529
+ };
14406
14530
  var depsCandidates = (pkgDir) => [
14407
14531
  join26(pkgDir, "node_modules"),
14408
14532
  dirname9(pkgDir)
@@ -14441,6 +14565,28 @@ var astroConflictWarning = (blumeAstroPkg, shadowAstroPkg) => {
14441
14565
  const pin = blume ?? "<Blume's astro version>";
14442
14566
  return `Astro version conflict: another dependency hoisted ${versions} to the project root, so @astrojs/mdx binds to the wrong copy and the build fails on a missing export (e.g. "chunkToString"). A single symlink can't reconcile a split install — pin Blume's Astro by adding a package.json "overrides" (npm/bun/pnpm) or "resolutions" (yarn) entry { "astro": "${pin}" }, then reinstall. Run \`npm ls astro\` to find the dependency pulling the older copy.`;
14443
14567
  };
14568
+ var dropStaleDepsLink = async (link, pkgDir) => {
14569
+ let existing;
14570
+ try {
14571
+ existing = await lstat(link);
14572
+ } catch {
14573
+ return;
14574
+ }
14575
+ if (!existing.isSymbolicLink()) {
14576
+ return;
14577
+ }
14578
+ let linkedBlume;
14579
+ let runningBlume;
14580
+ try {
14581
+ linkedBlume = await realpath(join26(link, "blume"));
14582
+ runningBlume = await realpath(pkgDir);
14583
+ } catch {
14584
+ return;
14585
+ }
14586
+ if (linkedBlume !== runningBlume) {
14587
+ await rm2(link, { force: true });
14588
+ }
14589
+ };
14444
14590
  var ensureDepsLink = async (outDir, pkgDir = packageRoot()) => {
14445
14591
  const astroDir = candidateHolding(pkgDir, "astro");
14446
14592
  if (!astroDir) {
@@ -14448,16 +14594,18 @@ var ensureDepsLink = async (outDir, pkgDir = packageRoot()) => {
14448
14594
  }
14449
14595
  const mdxDir = candidateHolding(pkgDir, "@astrojs", "mdx");
14450
14596
  const blumeAstro = resolveAstroPackageJson(astroDir);
14451
- const outDirAstro = resolvedAstroPath(outDir);
14452
- const astroCorrect = blumeAstro !== null && outDirAstro === blumeAstro;
14453
- if (astroCorrect && mdxDir === astroDir) {
14597
+ await dropStaleDepsLink(join26(outDir, "node_modules"), pkgDir);
14598
+ const outDirHit = resolvedAstroHit(outDir);
14599
+ const astroCorrect = blumeAstro !== null && outDirHit?.pkg === blumeAstro;
14600
+ const walkLandsInDeps = astroCorrect && outDirHit !== null && sameRealDir(outDirHit.modulesDir, astroDir);
14601
+ if (walkLandsInDeps && mdxDir === astroDir) {
14454
14602
  return null;
14455
14603
  }
14456
14604
  if (mdxDir && (mdxDir === astroDir || astroCorrect)) {
14457
14605
  await linkDepsJunction(join26(outDir, "node_modules"), mdxDir);
14458
14606
  return null;
14459
14607
  }
14460
- return astroConflictWarning(blumeAstro, outDirAstro);
14608
+ return astroConflictWarning(blumeAstro, outDirHit?.pkg ?? null);
14461
14609
  };
14462
14610
  var ISLAND_FRAMEWORK_DEPS = {
14463
14611
  svelte: "@astrojs/svelte",
@@ -14943,6 +15091,7 @@ var generateRuntime = async (project) => {
14943
15091
  tags: overrideTags,
14944
15092
  warnings: overrideWarnings
14945
15093
  } = componentSlots;
15094
+ project.examples = exampleMarkdownLookup(exampleDiscovery.examples);
14946
15095
  const frameworks = new Set([
14947
15096
  ...islandDiscovery.islands.map((island) => island.framework),
14948
15097
  ...exampleDiscovery.examples.map((example) => example.framework),
@@ -16037,6 +16186,8 @@ var eject = async (root) => {
16037
16186
  const askEnabled = config.ai.ask?.enabled ?? false;
16038
16187
  const exportPdf = config.export.pdf;
16039
16188
  const exportEpub = config.export.epub;
16189
+ const examples = await discoverExamples(root, config.examples.source);
16190
+ project.examples = exampleMarkdownLookup(examples.examples);
16040
16191
  const [
16041
16192
  pages,
16042
16193
  needsReactRaw,
@@ -16044,8 +16195,7 @@ var eject = async (root) => {
16044
16195
  userTheme,
16045
16196
  userExamplesCss,
16046
16197
  rawMarkdown,
16047
- islands,
16048
- examples
16198
+ islands
16049
16199
  ] = await Promise.all([
16050
16200
  context.pagesRoot ? discoverPages(context.pagesRoot) : Promise.resolve([]),
16051
16201
  detectNeedsReact(root),
@@ -16053,8 +16203,7 @@ var eject = async (root) => {
16053
16203
  context.themeFile ? readFile15(context.themeFile, "utf-8") : Promise.resolve(""),
16054
16204
  readExamplesCss(root, config.examples.css),
16055
16205
  buildRawMarkdown(project),
16056
- discoverIslands(root),
16057
- discoverExamples(root, config.examples.source)
16206
+ discoverIslands(root)
16058
16207
  ]);
16059
16208
  const frameworks = new Set([
16060
16209
  ...islands.islands.map((island) => island.framework),
@@ -17224,5 +17373,5 @@ process.on("unhandledRejection", (error) => {
17224
17373
  });
17225
17374
  runMain(main);
17226
17375
 
17227
- //# debugId=F92BD712EE9633C264756E2164756E21
17376
+ //# debugId=74F150F28E9B859A64756E2164756E21
17228
17377
  //# sourceMappingURL=index.js.map