blume 1.1.2 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
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
+
3
15
  ## 1.1.2
4
16
 
5
17
  ### 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) =>
@@ -7994,6 +7995,19 @@ var lastModifiedConfigSchema = z2.union([
7994
7995
  z2.boolean(),
7995
7996
  z2.strictObject({ type: z2.enum(["git", "frontmatter"]).default("git") })
7996
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
+ });
7997
8011
  var codeConfigSchema = z2.strictObject({
7998
8012
  icons: z2.boolean().default(true),
7999
8013
  wrap: z2.boolean().default(false)
@@ -8012,12 +8026,14 @@ var openapiSourceSchema = z2.strictObject({
8012
8026
  route: z2.string().optional(),
8013
8027
  spec: z2.string()
8014
8028
  });
8029
+ var scalarConfigSchema = z2.record(z2.string(), z2.unknown()).optional();
8015
8030
  var openapiConfigSchema = z2.strictObject({
8016
8031
  codeSamples: z2.array(z2.string()).default(["curl", "js", "python"]),
8017
8032
  enabled: z2.boolean().default(false),
8018
8033
  expandSchemas: z2.boolean().default(false),
8019
8034
  renderer: z2.enum(["blume", "scalar"]).default("blume"),
8020
8035
  route: z2.string().default("/reference"),
8036
+ scalar: scalarConfigSchema,
8021
8037
  sources: z2.array(openapiSourceSchema).default([]),
8022
8038
  spec: z2.string().optional(),
8023
8039
  theme: z2.string().optional()
@@ -8025,6 +8041,7 @@ var openapiConfigSchema = z2.strictObject({
8025
8041
  var asyncapiConfigSchema = z2.strictObject({
8026
8042
  enabled: z2.boolean().default(false),
8027
8043
  route: z2.string().default("/events"),
8044
+ scalar: scalarConfigSchema,
8028
8045
  sources: z2.array(openapiSourceSchema).default([]),
8029
8046
  spec: z2.string().optional(),
8030
8047
  theme: z2.string().optional()
@@ -8069,6 +8086,7 @@ var blumeConfigSchema = z2.strictObject({
8069
8086
  banner: bannerConfigSchema.optional(),
8070
8087
  basePath: z2.string().optional().transform((value) => normalizeBasePath(value)),
8071
8088
  content: contentConfigSchema.default({}),
8089
+ dateFormat: dateFormatConfigSchema.default({ dateStyle: "long" }),
8072
8090
  deployment: deploymentConfigSchema.default({}),
8073
8091
  description: z2.string().optional(),
8074
8092
  examples: examplesConfigSchema.default("examples"),
@@ -11734,6 +11752,22 @@ var youtube = ({ props }) => {
11734
11752
  const title = typeof props.title === "string" && props.title !== "" ? props.title : "Watch on YouTube";
11735
11753
  return `[${title}](https://www.youtube.com/watch?v=${videoId}${start})`;
11736
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
+ });
11737
11771
  var SERIALIZERS = {
11738
11772
  Callout: callout,
11739
11773
  Steps: steps,
@@ -11929,10 +11963,14 @@ var buildIndex = (project) => {
11929
11963
  var buildFull = async (project) => {
11930
11964
  const { config } = project;
11931
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
+ };
11932
11970
  const sections = await Promise.all(pages.map(async (page) => {
11933
11971
  const raw = await readEntryText(project, page);
11934
11972
  const parsed = frontmatter_default(raw);
11935
- const body = downlevelComponents(applyAgentVisibility(parsed.content), config.ai.markdownComponents, parsed.data).trim();
11973
+ const body = downlevelComponents(applyAgentVisibility(parsed.content), components, parsed.data).trim();
11936
11974
  const url = pageUrl(page.route, config.deployment.site, normalizeBasePath(config.deployment.base));
11937
11975
  return [`# ${page.title}`, `Source: ${url}`, "", body].join(`
11938
11976
  `);
@@ -12574,6 +12612,10 @@ import { readFile as readFile10 } from "node:fs/promises";
12574
12612
  var agentMarkdown = (entry) => entry.md ?? entry.mdx;
12575
12613
  var buildRawMarkdown = async (project) => {
12576
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
+ };
12577
12619
  const readRoute = async (route) => {
12578
12620
  const page = pageById.get(route.id);
12579
12621
  if (page) {
@@ -12583,7 +12625,7 @@ var buildRawMarkdown = async (project) => {
12583
12625
  };
12584
12626
  const entries = await Promise.all(project.manifest.routes.map(async (route) => {
12585
12627
  const source = applyAgentVisibility(await readRoute(route));
12586
- const md = downlevelComponents(source, project.config.ai.markdownComponents, frontmatter_default(source).data);
12628
+ const md = downlevelComponents(source, components, frontmatter_default(source).data);
12587
12629
  const entry = md === source ? { mdx: source } : { md, mdx: source };
12588
12630
  return [route.path, entry];
12589
12631
  }));
@@ -13351,7 +13393,8 @@ var buildReferenceFiles = async (options) => {
13351
13393
  content: scalarReferenceTemplate({
13352
13394
  configuration: {
13353
13395
  ...spec.config,
13354
- ...themeConfiguration(config, ref.theme)
13396
+ ...themeConfiguration(config, ref.theme),
13397
+ ...ref.scalar
13355
13398
  },
13356
13399
  dataImport: `${"../".repeat(depth + 1)}generated/data.json`,
13357
13400
  route: ref.route,
@@ -13582,6 +13625,13 @@ ${THEME_MAPPING}
13582
13625
  letter-spacing: 0;
13583
13626
  }
13584
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
+
13585
13635
  .prose :where(h1) {
13586
13636
  font-size: 3rem;
13587
13637
  line-height: 1.1;
@@ -14132,6 +14182,16 @@ var OVERRIDES = `
14132
14182
  padding-right: 1.25rem;
14133
14183
  }
14134
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
+
14135
14195
  /* The rich renderer renders each popup's type signature as a nested Shiki
14136
14196
  pre. Strip the code-block chrome (border, radius, padding, background) so it
14137
14197
  sits flush inside the popup, which owns the frame. */
@@ -14410,6 +14470,10 @@ var discoverExamples = async (root, pattern = "examples") => {
14410
14470
  }
14411
14471
  return { examples, warnings };
14412
14472
  };
14473
+ var exampleMarkdownLookup = (examples) => Object.fromEntries(examples.map((example) => [
14474
+ example.path,
14475
+ { lang: example.lang, source: example.source }
14476
+ ]));
14413
14477
 
14414
14478
  // src/astro/generate.ts
14415
14479
  var BLUME_SRC = join26(packageRoot(), "src");
@@ -14441,12 +14505,13 @@ var resolveAstroPackageJson = (modulesDir) => {
14441
14505
  return null;
14442
14506
  }
14443
14507
  };
14444
- var resolvedAstroPath = (fromDir) => {
14508
+ var resolvedAstroHit = (fromDir) => {
14445
14509
  let dir = normalize3(fromDir);
14446
14510
  while (true) {
14447
- const resolved = resolveAstroPackageJson(join26(dir, "node_modules"));
14448
- if (resolved) {
14449
- return resolved;
14511
+ const modulesDir = join26(dir, "node_modules");
14512
+ const pkg = resolveAstroPackageJson(modulesDir);
14513
+ if (pkg) {
14514
+ return { modulesDir, pkg };
14450
14515
  }
14451
14516
  const parent = dirname9(dir);
14452
14517
  if (parent === dir) {
@@ -14455,6 +14520,13 @@ var resolvedAstroPath = (fromDir) => {
14455
14520
  dir = parent;
14456
14521
  }
14457
14522
  };
14523
+ var sameRealDir = (a, b) => {
14524
+ try {
14525
+ return realpathSync(a) === realpathSync(b);
14526
+ } catch {
14527
+ return false;
14528
+ }
14529
+ };
14458
14530
  var depsCandidates = (pkgDir) => [
14459
14531
  join26(pkgDir, "node_modules"),
14460
14532
  dirname9(pkgDir)
@@ -14523,16 +14595,17 @@ var ensureDepsLink = async (outDir, pkgDir = packageRoot()) => {
14523
14595
  const mdxDir = candidateHolding(pkgDir, "@astrojs", "mdx");
14524
14596
  const blumeAstro = resolveAstroPackageJson(astroDir);
14525
14597
  await dropStaleDepsLink(join26(outDir, "node_modules"), pkgDir);
14526
- const outDirAstro = resolvedAstroPath(outDir);
14527
- const astroCorrect = blumeAstro !== null && outDirAstro === blumeAstro;
14528
- if (astroCorrect && mdxDir === astroDir) {
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) {
14529
14602
  return null;
14530
14603
  }
14531
14604
  if (mdxDir && (mdxDir === astroDir || astroCorrect)) {
14532
14605
  await linkDepsJunction(join26(outDir, "node_modules"), mdxDir);
14533
14606
  return null;
14534
14607
  }
14535
- return astroConflictWarning(blumeAstro, outDirAstro);
14608
+ return astroConflictWarning(blumeAstro, outDirHit?.pkg ?? null);
14536
14609
  };
14537
14610
  var ISLAND_FRAMEWORK_DEPS = {
14538
14611
  svelte: "@astrojs/svelte",
@@ -15018,6 +15091,7 @@ var generateRuntime = async (project) => {
15018
15091
  tags: overrideTags,
15019
15092
  warnings: overrideWarnings
15020
15093
  } = componentSlots;
15094
+ project.examples = exampleMarkdownLookup(exampleDiscovery.examples);
15021
15095
  const frameworks = new Set([
15022
15096
  ...islandDiscovery.islands.map((island) => island.framework),
15023
15097
  ...exampleDiscovery.examples.map((example) => example.framework),
@@ -16112,6 +16186,8 @@ var eject = async (root) => {
16112
16186
  const askEnabled = config.ai.ask?.enabled ?? false;
16113
16187
  const exportPdf = config.export.pdf;
16114
16188
  const exportEpub = config.export.epub;
16189
+ const examples = await discoverExamples(root, config.examples.source);
16190
+ project.examples = exampleMarkdownLookup(examples.examples);
16115
16191
  const [
16116
16192
  pages,
16117
16193
  needsReactRaw,
@@ -16119,8 +16195,7 @@ var eject = async (root) => {
16119
16195
  userTheme,
16120
16196
  userExamplesCss,
16121
16197
  rawMarkdown,
16122
- islands,
16123
- examples
16198
+ islands
16124
16199
  ] = await Promise.all([
16125
16200
  context.pagesRoot ? discoverPages(context.pagesRoot) : Promise.resolve([]),
16126
16201
  detectNeedsReact(root),
@@ -16128,8 +16203,7 @@ var eject = async (root) => {
16128
16203
  context.themeFile ? readFile15(context.themeFile, "utf-8") : Promise.resolve(""),
16129
16204
  readExamplesCss(root, config.examples.css),
16130
16205
  buildRawMarkdown(project),
16131
- discoverIslands(root),
16132
- discoverExamples(root, config.examples.source)
16206
+ discoverIslands(root)
16133
16207
  ]);
16134
16208
  const frameworks = new Set([
16135
16209
  ...islands.islands.map((island) => island.framework),
@@ -17299,5 +17373,5 @@ process.on("unhandledRejection", (error) => {
17299
17373
  });
17300
17374
  runMain(main);
17301
17375
 
17302
- //# debugId=7D02660AA9B8797064756E2164756E21
17376
+ //# debugId=74F150F28E9B859A64756E2164756E21
17303
17377
  //# sourceMappingURL=index.js.map