@clarify-labs/cli 0.8.0 → 0.9.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.
@@ -9,12 +9,12 @@ import shiki from "shiki";
9
9
  import { visit } from "unist-util-visit";
10
10
  import { markdownRemarkPlugins, themeBootstrapScript, themePresets } from "@clarify-labs/renderer";
11
11
  import { stringify } from "yaml";
12
- import { remark } from "remark";
13
12
  import matter from "gray-matter";
13
+ import { remark } from "remark";
14
+ import { fileURLToPath, pathToFileURL } from "node:url";
14
15
  import SwaggerParser from "@apidevtools/swagger-parser";
15
16
  import { tmpdir } from "node:os";
16
17
  import { z } from "zod";
17
- import { pathToFileURL } from "node:url";
18
18
  import { build, loadConfigFromFile } from "vite";
19
19
  import { createRequire } from "node:module";
20
20
  //#region source/core/utils.ts
@@ -140,23 +140,76 @@ function readRouteArtifactContent(route) {
140
140
  const content = readRouteContent(route);
141
141
  return shouldUseUtf8Signature(route) ? withUtf8Signature(content) : content;
142
142
  }
143
+ function isLlmsTxtRoute(route) {
144
+ return !route.path.split("/").includes("404");
145
+ }
146
+ function llmsTxtDescription(route) {
147
+ if (route.description) return route.description;
148
+ const sections = route.sections?.filter((section) => section.level === 2).slice(0, 3).map((section) => section.title);
149
+ if (sections?.length) return `Covers ${sections.join(", ")}.`;
150
+ if (route.keywords?.length) return `Related topics: ${route.keywords.join(", ")}.`;
151
+ if (route.kind === "openapi") return "OpenAPI artifact for machine-readable API reference data.";
152
+ }
153
+ function llmsTxtListItem(route, basePath) {
154
+ if (!route.contentArtifactUrl) return void 0;
155
+ const description = llmsTxtDescription(route);
156
+ return description ? `- [${route.title}](${basePath}${route.contentArtifactUrl}): ${description}` : `- [${route.title}](${basePath}${route.contentArtifactUrl})`;
157
+ }
158
+ function llmsTxtLocaleLabel(locale, projectConfig) {
159
+ return projectConfig.i18n?.locales.find((item) => item.code === locale)?.label ?? locale;
160
+ }
161
+ function groupRoutesByLocale(routes) {
162
+ const groups = /* @__PURE__ */ new Map();
163
+ for (const route of routes) {
164
+ const key = route.locale ?? "default";
165
+ groups.set(key, [...groups.get(key) ?? [], route]);
166
+ }
167
+ return groups;
168
+ }
169
+ function groupLlmsTxtRoutesByLocale(routes, projectConfig) {
170
+ const routesBySource = /* @__PURE__ */ new Map();
171
+ const defaultLocale = projectConfig.i18n?.defaultLocale;
172
+ for (const route of routes) {
173
+ const sourceKey = `${route.locale ?? defaultLocale ?? "default"}:${route.basePath ?? route.contentArtifactUrl ?? route.path}`;
174
+ const previousRoute = routesBySource.get(sourceKey);
175
+ if (!previousRoute || previousRoute.locale && !route.locale) routesBySource.set(sourceKey, route);
176
+ }
177
+ return groupRoutesByLocale([...routesBySource.values()].map((route) => ({
178
+ ...route,
179
+ locale: route.locale ?? defaultLocale
180
+ })));
181
+ }
182
+ function llmsTxtDocsSectionTitle(locale, groupCount, projectConfig) {
183
+ if (groupCount <= 1) return "Docs";
184
+ return `Docs - ${locale === "default" ? "Default" : llmsTxtLocaleLabel(locale, projectConfig)}`;
185
+ }
143
186
  function createLlmsTxt(routes, projectConfig) {
144
187
  const basePath = normalizeBasePath(projectConfig.routePrefix);
145
188
  const lines = [`# ${projectConfig.title}`, ""];
146
189
  if (projectConfig.description) lines.push(`> ${projectConfig.description}`, "");
147
- lines.push("## Docs");
148
- for (const route of routes.filter((route) => route.kind === "mdx")) {
149
- if (!route.contentArtifactUrl) continue;
150
- lines.push(`- [${route.title}](${basePath}${route.contentArtifactUrl})`);
190
+ lines.push("This file lists the source-ready Markdown and OpenAPI artifacts for this documentation site.", "");
191
+ const mdxRoutes = routes.filter((route) => route.kind === "mdx" && isLlmsTxtRoute(route));
192
+ if (mdxRoutes.length > 0) {
193
+ const localizedGroups = groupLlmsTxtRoutesByLocale(mdxRoutes, projectConfig);
194
+ for (const [locale, localeRoutes] of localizedGroups) {
195
+ lines.push(`## ${llmsTxtDocsSectionTitle(locale, localizedGroups.size, projectConfig)}`);
196
+ for (const route of localeRoutes) {
197
+ const item = llmsTxtListItem(route, basePath);
198
+ if (item) lines.push(item);
199
+ }
200
+ lines.push("");
201
+ }
151
202
  }
152
- const openApiRoutes = routes.filter((route) => route.kind === "openapi");
203
+ const openApiRoutes = routes.filter((route) => route.kind === "openapi" && isLlmsTxtRoute(route));
153
204
  if (openApiRoutes.length > 0) {
154
- lines.push("", "## OpenAPI");
205
+ if (lines.at(-1) !== "") lines.push("");
206
+ lines.push("## OpenAPI");
155
207
  for (const route of openApiRoutes) {
156
- if (!route.contentArtifactUrl) continue;
157
- lines.push(`- [${route.title}](${basePath}${route.contentArtifactUrl})`);
208
+ const item = llmsTxtListItem(route, basePath);
209
+ if (item) lines.push(item);
158
210
  }
159
211
  }
212
+ while (lines.at(-1) === "") lines.pop();
160
213
  return `${lines.join("\n")}\n`;
161
214
  }
162
215
  function createLlmsTxtArtifact(routes, projectConfig) {
@@ -316,6 +369,101 @@ function parseFrontmatter(content) {
316
369
  };
317
370
  }
318
371
  //#endregion
372
+ //#region source/parsers/content.ts
373
+ async function transformContent(transform, input) {
374
+ return transform ? await transform(input) : input;
375
+ }
376
+ function createContentProcessor(transform) {
377
+ return {
378
+ async processMdx(source, filePath) {
379
+ const { frontmatter, content } = parseFrontmatter(source);
380
+ const transformed = await transformContent(transform, {
381
+ kind: "mdx",
382
+ source,
383
+ filePath,
384
+ frontmatter,
385
+ content
386
+ });
387
+ return {
388
+ frontmatter: transformed.frontmatter,
389
+ content: transformed.content
390
+ };
391
+ },
392
+ async processText(source, kind, filePath) {
393
+ return (await transformContent(transform, {
394
+ kind,
395
+ source,
396
+ filePath,
397
+ frontmatter: {},
398
+ content: source
399
+ })).content;
400
+ }
401
+ };
402
+ }
403
+ //#endregion
404
+ //#region source/core/hooks.ts
405
+ async function runHooks(plugins, hookName, input, ctx) {
406
+ let result = input;
407
+ for (const plugin of plugins) {
408
+ const hook = plugin.hooks?.[hookName];
409
+ if (!hook) continue;
410
+ try {
411
+ result = await hook(result, ctx);
412
+ } catch (err) {
413
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "${hookName}" failed: ${err}`, { cause: err });
414
+ }
415
+ }
416
+ return result;
417
+ }
418
+ async function runDevConfigureServerHooks(plugins, server, ctx) {
419
+ for (const plugin of plugins) {
420
+ const hook = plugin.hooks?.["dev:configureServer"];
421
+ if (!hook) continue;
422
+ try {
423
+ await hook(server, ctx);
424
+ } catch (err) {
425
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "dev:configureServer" failed: ${err}`, { cause: err });
426
+ }
427
+ }
428
+ }
429
+ async function runBuildAssetsHooks(plugins, ctx) {
430
+ const assets = [];
431
+ for (const plugin of plugins) {
432
+ const hook = plugin.hooks?.["build:assets"];
433
+ if (!hook) continue;
434
+ try {
435
+ const pluginAssets = await hook(ctx);
436
+ assets.push(...pluginAssets);
437
+ } catch (err) {
438
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "build:assets" failed: ${err}`, { cause: err });
439
+ }
440
+ }
441
+ return assets;
442
+ }
443
+ async function runBuildDoneHooks(plugins, ctx) {
444
+ for (const plugin of plugins) {
445
+ const hook = plugin.hooks?.["build:done"];
446
+ if (!hook) continue;
447
+ try {
448
+ await hook(ctx);
449
+ } catch (err) {
450
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "build:done" failed: ${err}`, { cause: err });
451
+ }
452
+ }
453
+ }
454
+ //#endregion
455
+ //#region source/core/content.ts
456
+ var contentProcessors = /* @__PURE__ */ new WeakMap();
457
+ function createProjectContentProcessor(plugins, ctx) {
458
+ return createContentProcessor((input) => runHooks(plugins, "content:transform", input, ctx));
459
+ }
460
+ function setProjectContentProcessor(ctx, processor) {
461
+ contentProcessors.set(ctx, processor);
462
+ }
463
+ function getProjectContentProcessor(ctx) {
464
+ return contentProcessors.get(ctx);
465
+ }
466
+ //#endregion
319
467
  //#region source/parsers/routes.ts
320
468
  function kebabToTitle(str) {
321
469
  return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
@@ -392,18 +540,19 @@ function resolveLocalizedText(text, locale, fallbackLocale) {
392
540
  if (text === void 0) return void 0;
393
541
  return text[locale] ?? (fallbackLocale ? text[fallbackLocale] : void 0) ?? Object.values(text)[0];
394
542
  }
395
- function findContentRoutes(dir, base = dir) {
543
+ async function findContentRoutes(dir, base = dir, options = {}) {
396
544
  const routes = [];
397
545
  if (!existsSync(dir)) return routes;
398
546
  const entries = readdirSync(dir, { withFileTypes: true });
399
547
  for (const entry of entries) {
400
548
  const fullPath = join(dir, entry.name);
401
- if (entry.isDirectory()) routes.push(...findContentRoutes(fullPath, base));
549
+ if (entry.isDirectory()) routes.push(...await findContentRoutes(fullPath, base, options));
402
550
  else if (entry.isFile() && /\.mdx?$/.test(entry.name)) {
403
551
  const relativePath = relative(base, fullPath);
404
552
  const pathParts = relativePath.replace(/\.mdx?$/, "").split("/");
405
553
  const cleanPath = ("/" + pathParts.map((p) => p === "index" ? "" : p).filter(Boolean).join("/")).replace(/\/+/g, "/").replace(/\/$/, "") || "/";
406
- const { frontmatter, content } = parseFrontmatter(readFileSync(fullPath, "utf-8"));
554
+ const source = readFileSync(fullPath, "utf-8");
555
+ const { frontmatter, content } = await (options.contentProcessor ?? createContentProcessor()).processMdx(source, fullPath);
407
556
  let title = typeof frontmatter.title === "string" ? frontmatter.title : "";
408
557
  if (!title) {
409
558
  const lastPart = pathParts[pathParts.length - 1] ?? "";
@@ -660,6 +809,13 @@ var OPENAPI_HTTP_METHODS = [
660
809
  function errorMessage(error) {
661
810
  return error instanceof Error ? error.message : String(error);
662
811
  }
812
+ function isSameOpenAPIFile(url, filePath) {
813
+ try {
814
+ return resolve(fileURLToPath(url)) === resolve(filePath);
815
+ } catch {
816
+ return resolve(url) === resolve(filePath);
817
+ }
818
+ }
663
819
  function findOpenAPIRoutes(dir, base = dir) {
664
820
  const routes = [];
665
821
  if (!existsSync(dir)) return routes;
@@ -723,12 +879,26 @@ function extractOpenAPISections(spec, filterTags) {
723
879
  }
724
880
  return sections;
725
881
  }
726
- async function readOpenAPISpec(filePath) {
882
+ async function readOpenAPISpec(filePath, contentProcessor) {
727
883
  try {
728
- return {
884
+ if (!contentProcessor) return {
729
885
  ok: true,
730
886
  spec: await SwaggerParser.dereference(filePath)
731
887
  };
888
+ const source = readFileSync(filePath, "utf-8");
889
+ const transformedSource = await contentProcessor.processText(source, "openapi", filePath);
890
+ if (transformedSource === source) return {
891
+ ok: true,
892
+ spec: await SwaggerParser.dereference(filePath)
893
+ };
894
+ return {
895
+ ok: true,
896
+ spec: await SwaggerParser.dereference(filePath, { resolve: { transformedOpenAPISource: {
897
+ order: 1,
898
+ canRead: (file) => isSameOpenAPIFile(file.url, filePath),
899
+ read: () => transformedSource
900
+ } } })
901
+ };
732
902
  } catch (error) {
733
903
  return {
734
904
  ok: false,
@@ -989,7 +1159,7 @@ function createOpenAPIPlugin() {
989
1159
  const result = specFromCache ? {
990
1160
  ok: true,
991
1161
  spec: specFromCache
992
- } : await readOpenAPISpec(route.filePath);
1162
+ } : await readOpenAPISpec(route.filePath, getProjectContentProcessor(ctx));
993
1163
  if (!result.ok) {
994
1164
  route.diagnostic = result.diagnostic;
995
1165
  route.title = route.title || "OpenAPI parse error";
@@ -1306,9 +1476,51 @@ function createSourceLinksPlugin() {
1306
1476
  };
1307
1477
  }
1308
1478
  //#endregion
1479
+ //#region source/parsers/variables.ts
1480
+ var variablePattern = /{{\s*([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*)\s*}}/g;
1481
+ function getVariableValue(variables, path) {
1482
+ let current = variables;
1483
+ for (const segment of path.split(".")) {
1484
+ if (!current || typeof current !== "object" || Array.isArray(current)) return void 0;
1485
+ current = current[segment];
1486
+ }
1487
+ return current;
1488
+ }
1489
+ function stringifyVariableValue(variables, path, stack) {
1490
+ if (stack.includes(path)) throw new Error(`[clarify] variable "${path}" contains a circular reference: ${[...stack, path].join(" -> ")}`);
1491
+ const value = getVariableValue(variables, path);
1492
+ if (value === void 0 || typeof value === "object") return void 0;
1493
+ if (typeof value !== "string") return String(value);
1494
+ return value.replace(variablePattern, (match, nestedPath) => stringifyVariableValue(variables, nestedPath, [...stack, path]) ?? match);
1495
+ }
1496
+ function applyVariables(content, variables = {}) {
1497
+ return content.replace(variablePattern, (match, path) => stringifyVariableValue(variables, path, []) ?? match);
1498
+ }
1499
+ //#endregion
1500
+ //#region source/plugins/variables/index.ts
1501
+ function applyVariablesToFrontmatter(frontmatter, variables) {
1502
+ const nextFrontmatter = { ...frontmatter };
1503
+ for (const key of ["title", "description"]) {
1504
+ if (typeof nextFrontmatter[key] !== "string") continue;
1505
+ nextFrontmatter[key] = applyVariables(nextFrontmatter[key], variables);
1506
+ }
1507
+ return nextFrontmatter;
1508
+ }
1509
+ function createVariablesPlugin() {
1510
+ return {
1511
+ name: "clarify:variables",
1512
+ hooks: { "content:transform": (input, ctx) => ({
1513
+ ...input,
1514
+ frontmatter: applyVariablesToFrontmatter(input.frontmatter, ctx.projectConfig.variables),
1515
+ content: applyVariables(input.content, ctx.projectConfig.variables)
1516
+ }) }
1517
+ };
1518
+ }
1519
+ //#endregion
1309
1520
  //#region source/core/builtin.ts
1310
1521
  function createBuiltinPlugins(options = {}) {
1311
1522
  const plugins = [
1523
+ createVariablesPlugin(),
1312
1524
  createOpenAPIPlugin(),
1313
1525
  createSourceLinksPlugin(),
1314
1526
  createContentArtifactsPlugin(),
@@ -1377,6 +1589,13 @@ var clarifyFooterConfigSchema = z.object({
1377
1589
  socials: z.record(z.string(), z.string()).optional(),
1378
1590
  copyright: clarifyLocalizedTextSchema.optional()
1379
1591
  }).strict();
1592
+ var clarifyVariableValueSchema = z.lazy(() => z.union([
1593
+ z.string(),
1594
+ z.number(),
1595
+ z.boolean(),
1596
+ z.record(z.string(), clarifyVariableValueSchema)
1597
+ ]));
1598
+ var clarifyVariablesConfigSchema = z.record(z.string(), clarifyVariableValueSchema);
1380
1599
  var clarifySourceConfigSchema = z.object({
1381
1600
  repository: z.string(),
1382
1601
  branch: z.string().optional(),
@@ -1454,9 +1673,11 @@ var clarifyProjectConfigSchema = z.object({
1454
1673
  favicon: clarifyFaviconConfigSchema.optional(),
1455
1674
  theme: clarifyThemeConfigSchema.optional(),
1456
1675
  routePrefix: z.string().optional(),
1676
+ assetPrefix: z.string().optional(),
1457
1677
  navbar: z.object({ links: z.array(clarifyNavbarLinkSchema).optional() }).optional(),
1458
1678
  banner: clarifyBannerConfigSchema.optional(),
1459
1679
  footer: clarifyFooterConfigSchema.optional(),
1680
+ variables: clarifyVariablesConfigSchema.optional(),
1460
1681
  i18n: clarifyI18nConfigSchema.optional(),
1461
1682
  tabs: clarifyTabsConfigSchema.optional()
1462
1683
  });
@@ -1516,7 +1737,22 @@ function resolveI18nConfig(i18n) {
1516
1737
  locales: i18n.locales
1517
1738
  };
1518
1739
  }
1740
+ function resolveRoutePrefix(routePrefix) {
1741
+ const trimmed = routePrefix?.trim();
1742
+ if (!trimmed || trimmed === "/") return "/";
1743
+ return `/${trimmed.replace(/^\/+|\/+$/g, "")}/`;
1744
+ }
1745
+ function resolveAssetPrefix(assetPrefix, routePrefix) {
1746
+ const trimmed = assetPrefix?.trim();
1747
+ if (trimmed === void 0) return routePrefix;
1748
+ if (trimmed === "./" || trimmed === "") return trimmed || "/";
1749
+ if (!trimmed || trimmed === "/") return "/";
1750
+ if (/^[a-z][a-z\d+.-]*:\/\//i.test(trimmed)) return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
1751
+ if (trimmed.startsWith("./") || trimmed.startsWith("../")) return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
1752
+ return `/${trimmed.replace(/^\/+|\/+$/g, "")}/`;
1753
+ }
1519
1754
  function resolveProjectConfig(config = {}) {
1755
+ const routePrefix = resolveRoutePrefix(config.routePrefix);
1520
1756
  return {
1521
1757
  title: config.title ?? "Clarify Docs",
1522
1758
  description: config.description ?? "",
@@ -1525,15 +1761,137 @@ function resolveProjectConfig(config = {}) {
1525
1761
  logo: config.logo,
1526
1762
  homeUrl: config.homeUrl,
1527
1763
  favicon: config.favicon,
1528
- routePrefix: config.routePrefix ?? "/",
1764
+ routePrefix,
1765
+ assetPrefix: resolveAssetPrefix(config.assetPrefix, routePrefix),
1529
1766
  theme: resolveThemeConfig(config.theme),
1530
1767
  navbar: config.navbar,
1531
1768
  banner: config.banner,
1532
1769
  footer: config.footer,
1770
+ variables: config.variables ?? {},
1533
1771
  i18n: resolveI18nConfig(config.i18n),
1534
1772
  tabs: config.tabs
1535
1773
  };
1536
1774
  }
1775
+ /** Serialize a ContentRoute into the minimal shape exposed to tooling. */
1776
+ function toDevRouteEntry(route) {
1777
+ return {
1778
+ path: route.path,
1779
+ filePath: route.filePath,
1780
+ locale: route.locale,
1781
+ basePath: route.basePath,
1782
+ kind: route.kind,
1783
+ title: route.title
1784
+ };
1785
+ }
1786
+ /**
1787
+ * Resolve a single file path to its preview route.
1788
+ *
1789
+ * Selection order:
1790
+ * 1. Routes whose `filePath` exactly matches.
1791
+ * 2. Among matches, prefer the one whose `locale` matches the file's locale
1792
+ * directory (e.g. `source/zh-CN/x.mdx` → locale `zh-CN`).
1793
+ * 3. If no locale match, fall back to the default locale (if i18n is enabled)
1794
+ * or the first match.
1795
+ */
1796
+ function resolveRouteForFile(filePath, routes, defaultLocale) {
1797
+ const matches = routes.filter((r) => r.filePath === filePath);
1798
+ if (matches.length === 0) return void 0;
1799
+ if (matches.length === 1) return matches[0];
1800
+ const fileLocale = inferLocaleFromPath(filePath);
1801
+ if (fileLocale) {
1802
+ const localeMatch = matches.find((r) => r.locale === fileLocale);
1803
+ if (localeMatch) return localeMatch;
1804
+ }
1805
+ if (defaultLocale) {
1806
+ const defaultMatch = matches.find((r) => r.locale === defaultLocale);
1807
+ if (defaultMatch) return defaultMatch;
1808
+ }
1809
+ return matches[0];
1810
+ }
1811
+ /**
1812
+ * Derive a best-effort preview route for a file that is not in the route
1813
+ * manifest (e.g. a draft or newly created file). The path is computed from the
1814
+ * file's location relative to the content root, following the same convention
1815
+ * as route discovery (`source/{locale}/path/index.mdx` → `/{locale}/path`).
1816
+ *
1817
+ * Returns null when the file is outside the content root or is not a
1818
+ * recognized content file type.
1819
+ */
1820
+ function inferRouteForFile(filePath, contentRoot, defaultLocale) {
1821
+ const ref = relative(contentRoot, filePath);
1822
+ if (!ref || ref.startsWith("..")) return null;
1823
+ const isMdx = /\.mdx?$/.test(filePath);
1824
+ const isOpenApi = /\.openapi\.(json|ya?ml)$/.test(filePath);
1825
+ if (!isMdx && !isOpenApi) return null;
1826
+ const segments = ref.split("/");
1827
+ const fileLocale = inferLocaleFromPath(filePath);
1828
+ const localeSegmentIndex = fileLocale ? segments.indexOf(fileLocale) : -1;
1829
+ const basePath = basePathFromRef(localeSegmentIndex >= 0 ? segments.slice(localeSegmentIndex + 1).join("/") : ref);
1830
+ const path = fileLocale && defaultLocale ? localizedRoutePath(basePath, fileLocale, {
1831
+ defaultLocale,
1832
+ missing: "fallback",
1833
+ locales: []
1834
+ }) : basePath;
1835
+ const title = segments[segments.length - 1]?.replace(/\.(mdx?|openapi\.(json|ya?ml))$/, "") || "Untitled";
1836
+ return {
1837
+ path: normalizePath(path),
1838
+ filePath,
1839
+ locale: fileLocale,
1840
+ basePath: normalizePath(basePath),
1841
+ kind: isMdx ? "mdx" : "openapi",
1842
+ title,
1843
+ inferred: true
1844
+ };
1845
+ }
1846
+ /** Best-effort locale inference from a file path segment like `.../zh-CN/x.mdx`. */
1847
+ function inferLocaleFromPath(filePath) {
1848
+ const segments = filePath.split("/");
1849
+ for (const segment of segments) if (/^[a-z]{2,3}(-[A-Z][a-z]{3})?(-[A-Z]{2})?$/.test(segment)) return segment;
1850
+ }
1851
+ /** Read and parse the JSON body of an IncomingMessage. */
1852
+ function readJsonBody(req) {
1853
+ return new Promise((resolveBody, reject) => {
1854
+ const chunks = [];
1855
+ req.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
1856
+ req.on("error", reject);
1857
+ req.on("end", () => {
1858
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
1859
+ if (!raw) return resolveBody({});
1860
+ try {
1861
+ resolveBody(JSON.parse(raw));
1862
+ } catch (err) {
1863
+ reject(err);
1864
+ }
1865
+ });
1866
+ });
1867
+ }
1868
+ /**
1869
+ * Handle a request to the dev route endpoint. Pure function — does not touch
1870
+ * the network directly, only reads the request body and writes to `res`.
1871
+ */
1872
+ async function handleDevRouteRequest(req, res, routes, projectConfig, contentRoot) {
1873
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
1874
+ let body;
1875
+ try {
1876
+ body = await readJsonBody(req);
1877
+ } catch {
1878
+ res.statusCode = 400;
1879
+ res.end(JSON.stringify({ error: "Invalid JSON body" }));
1880
+ return;
1881
+ }
1882
+ const file = typeof body === "object" && body !== null ? body.file : void 0;
1883
+ if (typeof file === "string" && file.length > 0) {
1884
+ const route = resolveRouteForFile(file, routes, projectConfig.i18n?.defaultLocale);
1885
+ if (route) {
1886
+ res.end(JSON.stringify(toDevRouteEntry(route)));
1887
+ return;
1888
+ }
1889
+ const inferred = inferRouteForFile(file, contentRoot, projectConfig.i18n?.defaultLocale);
1890
+ res.end(JSON.stringify(inferred));
1891
+ return;
1892
+ }
1893
+ res.end(JSON.stringify(routes.map(toDevRouteEntry)));
1894
+ }
1537
1895
  //#endregion
1538
1896
  //#region source/core/env-types.ts
1539
1897
  var GENERATED_FILE_NAME = "clarify-env.d.ts";
@@ -1583,57 +1941,6 @@ function writeClarifyEnvDts(projectRoot, plugins) {
1583
1941
  writeFileSync(filePath, next, "utf-8");
1584
1942
  }
1585
1943
  //#endregion
1586
- //#region source/core/hooks.ts
1587
- async function runHooks(plugins, hookName, input, ctx) {
1588
- let result = input;
1589
- for (const plugin of plugins) {
1590
- const hook = plugin.hooks?.[hookName];
1591
- if (!hook) continue;
1592
- try {
1593
- result = await hook(result, ctx);
1594
- } catch (err) {
1595
- throw new Error(`[clarify] plugin "${plugin.name}" hook "${hookName}" failed: ${err}`, { cause: err });
1596
- }
1597
- }
1598
- return result;
1599
- }
1600
- async function runDevConfigureServerHooks(plugins, server, ctx) {
1601
- for (const plugin of plugins) {
1602
- const hook = plugin.hooks?.["dev:configureServer"];
1603
- if (!hook) continue;
1604
- try {
1605
- await hook(server, ctx);
1606
- } catch (err) {
1607
- throw new Error(`[clarify] plugin "${plugin.name}" hook "dev:configureServer" failed: ${err}`, { cause: err });
1608
- }
1609
- }
1610
- }
1611
- async function runBuildAssetsHooks(plugins, ctx) {
1612
- const assets = [];
1613
- for (const plugin of plugins) {
1614
- const hook = plugin.hooks?.["build:assets"];
1615
- if (!hook) continue;
1616
- try {
1617
- const pluginAssets = await hook(ctx);
1618
- assets.push(...pluginAssets);
1619
- } catch (err) {
1620
- throw new Error(`[clarify] plugin "${plugin.name}" hook "build:assets" failed: ${err}`, { cause: err });
1621
- }
1622
- }
1623
- return assets;
1624
- }
1625
- async function runBuildDoneHooks(plugins, ctx) {
1626
- for (const plugin of plugins) {
1627
- const hook = plugin.hooks?.["build:done"];
1628
- if (!hook) continue;
1629
- try {
1630
- await hook(ctx);
1631
- } catch (err) {
1632
- throw new Error(`[clarify] plugin "${plugin.name}" hook "build:done" failed: ${err}`, { cause: err });
1633
- }
1634
- }
1635
- }
1636
- //#endregion
1637
1944
  //#region source/core/options.ts
1638
1945
  function resolveBuildOptions(options = {}) {
1639
1946
  return {
@@ -1644,12 +1951,90 @@ function resolveBuildOptions(options = {}) {
1644
1951
  };
1645
1952
  }
1646
1953
  //#endregion
1954
+ //#region source/core/user-config.ts
1955
+ function defineConfig(config) {
1956
+ validateProjectConfig(config);
1957
+ return config;
1958
+ }
1959
+ /**
1960
+ * Config filenames Clarify CLI looks for at the project root, in resolution
1961
+ * order. Exported so other modules (e.g. the dev `project-info` endpoint) can
1962
+ * surface the same list to tooling without duplicating it.
1963
+ */
1964
+ var CONFIG_FILENAMES = [
1965
+ "clarify.ts",
1966
+ "clarify.js",
1967
+ "clarify.json"
1968
+ ];
1969
+ function findClarifyConfigFile(root) {
1970
+ for (const filename of CONFIG_FILENAMES) {
1971
+ const filePath = resolve(root, filename);
1972
+ if (existsSync(filePath)) return filePath;
1973
+ }
1974
+ }
1975
+ function assertConfigObject(config, configFile) {
1976
+ if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error(`[clarify] ${configFile} must export a configuration object`);
1977
+ return config;
1978
+ }
1979
+ function loadJsonConfig(configFile) {
1980
+ const config = assertConfigObject(JSON.parse(readFileSync(configFile, "utf-8")), configFile);
1981
+ const projectConfig = validateProjectConfig(config);
1982
+ const ssg = config.ssg && typeof config.ssg === "object" && !Array.isArray(config.ssg) ? config.ssg : void 0;
1983
+ return {
1984
+ ...projectConfig,
1985
+ ...ssg ? { ssg } : {}
1986
+ };
1987
+ }
1988
+ async function loadClarifyConfig(root, env) {
1989
+ const configFile = findClarifyConfigFile(root);
1990
+ if (!configFile) return {};
1991
+ if (configFile.endsWith(".json")) return loadJsonConfig(configFile);
1992
+ return assertConfigObject((await loadConfigFromFile(env, configFile, root, "silent"))?.config, configFile);
1993
+ }
1994
+ /**
1995
+ * File extensions Clarify treats as content files. Kept in sync with the
1996
+ * patterns used by route discovery in `parsers/routes.ts` and the OpenAPI
1997
+ * page generator. Exposed to tooling so the extension's "is this a content
1998
+ * file?" check stays accurate without duplicating the regex.
1999
+ */
2000
+ var CONTENT_FILE_EXTENSIONS = [
2001
+ ".md",
2002
+ ".mdx",
2003
+ ".openapi.json",
2004
+ ".openapi.yaml",
2005
+ ".openapi.yml"
2006
+ ];
2007
+ /**
2008
+ * Build the project-info response payload from the resolved plugin state.
2009
+ */
2010
+ function buildProjectInfo(projectRoot, contentRoot, projectConfig) {
2011
+ const response = {
2012
+ configFilenames: CONFIG_FILENAMES,
2013
+ contentFileExtensions: CONTENT_FILE_EXTENSIONS,
2014
+ contentRoot: (contentRoot.startsWith(projectRoot) ? contentRoot.slice(projectRoot.length).replace(/^[/\\]+/, "") : contentRoot) || "source",
2015
+ projectRoot
2016
+ };
2017
+ if (projectConfig.i18n) response.i18n = {
2018
+ defaultLocale: projectConfig.i18n.defaultLocale,
2019
+ locales: projectConfig.i18n.locales.map((l) => l.code)
2020
+ };
2021
+ return response;
2022
+ }
2023
+ /**
2024
+ * Handle a request to the project-info endpoint. Responds to any method
2025
+ * (GET is typical) with the JSON payload — there is no request body to parse.
2026
+ */
2027
+ function handleProjectInfoRequest(_req, res, projectRoot, contentRoot, projectConfig) {
2028
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
2029
+ res.end(JSON.stringify(buildProjectInfo(projectRoot, contentRoot, projectConfig)));
2030
+ }
2031
+ //#endregion
1647
2032
  //#region source/core/site.ts
1648
2033
  async function discoverRoutesForRoot(routeRoot, locale, plugins, ctx) {
1649
2034
  return (await runHooks(plugins, "routes:discover", {
1650
2035
  contentRoot: routeRoot,
1651
2036
  locale,
1652
- routes: findContentRoutes(routeRoot)
2037
+ routes: await findContentRoutes(routeRoot, routeRoot, { contentProcessor: getProjectContentProcessor(ctx) })
1653
2038
  }, ctx)).routes;
1654
2039
  }
1655
2040
  async function discoverRoutes(root, contentRoot, plugins, ctx) {
@@ -1714,6 +2099,7 @@ async function resolveClarifySite(options = {}, resolveOptions = {}) {
1714
2099
  routes: [],
1715
2100
  navigation: []
1716
2101
  };
2102
+ setProjectContentProcessor(ctx, createProjectContentProcessor(plugins, ctx));
1717
2103
  let routes = await discoverRoutes(root, contentRoot, plugins, ctx);
1718
2104
  routes = await runHooks(plugins, "routes:discovered", routes, ctx);
1719
2105
  routes = applyConfiguredPageRoutePaths(routes, projectConfig.tabs, projectConfig.i18n);
@@ -1916,42 +2302,6 @@ async function renderSSGRoutes(routes, projectConfig, outputDirectory, ssrBundle
1916
2302
  }
1917
2303
  }
1918
2304
  //#endregion
1919
- //#region source/core/user-config.ts
1920
- function defineConfig(config) {
1921
- validateProjectConfig(config);
1922
- return config;
1923
- }
1924
- var CONFIG_FILENAMES = [
1925
- "clarify.ts",
1926
- "clarify.js",
1927
- "clarify.json"
1928
- ];
1929
- function findClarifyConfigFile(root) {
1930
- for (const filename of CONFIG_FILENAMES) {
1931
- const filePath = resolve(root, filename);
1932
- if (existsSync(filePath)) return filePath;
1933
- }
1934
- }
1935
- function assertConfigObject(config, configFile) {
1936
- if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error(`[clarify] ${configFile} must export a configuration object`);
1937
- return config;
1938
- }
1939
- function loadJsonConfig(configFile) {
1940
- const config = assertConfigObject(JSON.parse(readFileSync(configFile, "utf-8")), configFile);
1941
- const projectConfig = validateProjectConfig(config);
1942
- const ssg = config.ssg && typeof config.ssg === "object" && !Array.isArray(config.ssg) ? config.ssg : void 0;
1943
- return {
1944
- ...projectConfig,
1945
- ...ssg ? { ssg } : {}
1946
- };
1947
- }
1948
- async function loadClarifyConfig(root, env) {
1949
- const configFile = findClarifyConfigFile(root);
1950
- if (!configFile) return {};
1951
- if (configFile.endsWith(".json")) return loadJsonConfig(configFile);
1952
- return assertConfigObject((await loadConfigFromFile(env, configFile, root, "silent"))?.config, configFile);
1953
- }
1954
- //#endregion
1955
2305
  //#region source/core/plugin.ts
1956
2306
  function loadVirtualModule(id, modules) {
1957
2307
  const bareId = stripVirtualPrefix(id);
@@ -2042,7 +2392,7 @@ function clarifyPlugin(options = {}) {
2042
2392
  async config() {
2043
2393
  await resolveRoutesAndSpecs();
2044
2394
  return {
2045
- base: projectConfig.routePrefix,
2395
+ base: projectConfig.assetPrefix,
2046
2396
  build: {
2047
2397
  ...generateOptions.outputDirectory ? { outDir: generateOptions.outputDirectory } : {},
2048
2398
  manifest: true
@@ -2099,6 +2449,11 @@ function clarifyPlugin(options = {}) {
2099
2449
  async configureServer(server) {
2100
2450
  server.watcher.add(contentRoot);
2101
2451
  if (configFilePath) server.watcher.add(configFilePath);
2452
+ server.middlewares.use((req, res, next) => {
2453
+ if (req.url === "/dev/query-preview-route" && req.method === "POST") return handleDevRouteRequest(req, res, routes, projectConfig, contentRoot);
2454
+ if (req.url === "/dev/project-info" && (req.method === "GET" || req.method === "HEAD")) return handleProjectInfoRequest(req, res, root, contentRoot, projectConfig);
2455
+ next();
2456
+ });
2102
2457
  const handleContentTreeChange = async (filePath) => {
2103
2458
  const relativeContentFile = relative(contentRoot, isAbsolute(filePath) ? filePath : join(root, filePath));
2104
2459
  if (!(relativeContentFile && !relativeContentFile.startsWith("..") && !isAbsolute(relativeContentFile))) return;
@@ -2180,6 +2535,6 @@ function clarifyPlugin(options = {}) {
2180
2535
  ].flat().filter(Boolean);
2181
2536
  }
2182
2537
  //#endregion
2183
- export { resolveClarifySite as a, createClarifyRuntimeAliases as i, defineConfig as n, createClarifyTempDir as o, loadClarifyConfig as r, clarifyPlugin as t };
2538
+ export { loadClarifyConfig as a, defineConfig as i, createClarifyRuntimeAliases as n, createClarifyTempDir as o, resolveClarifySite as r, clarifyPlugin as t };
2184
2539
 
2185
- //# sourceMappingURL=plugin-CcD5HeTN.js.map
2540
+ //# sourceMappingURL=plugin-_mqQSBqE.js.map