@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.
@@ -36,14 +36,14 @@ shiki = __toESM(shiki, 1);
36
36
  let unist_util_visit = require("unist-util-visit");
37
37
  let _clarify_labs_renderer = require("@clarify-labs/renderer");
38
38
  let yaml = require("yaml");
39
- let remark = require("remark");
40
39
  let gray_matter = require("gray-matter");
41
40
  gray_matter = __toESM(gray_matter, 1);
41
+ let remark = require("remark");
42
+ let node_url = require("node:url");
42
43
  let _apidevtools_swagger_parser = require("@apidevtools/swagger-parser");
43
44
  _apidevtools_swagger_parser = __toESM(_apidevtools_swagger_parser, 1);
44
45
  let node_os = require("node:os");
45
46
  let zod = require("zod");
46
- let node_url = require("node:url");
47
47
  let vite = require("vite");
48
48
  let node_module = require("node:module");
49
49
  //#region source/core/utils.ts
@@ -169,23 +169,76 @@ function readRouteArtifactContent(route) {
169
169
  const content = readRouteContent(route);
170
170
  return shouldUseUtf8Signature(route) ? withUtf8Signature(content) : content;
171
171
  }
172
+ function isLlmsTxtRoute(route) {
173
+ return !route.path.split("/").includes("404");
174
+ }
175
+ function llmsTxtDescription(route) {
176
+ if (route.description) return route.description;
177
+ const sections = route.sections?.filter((section) => section.level === 2).slice(0, 3).map((section) => section.title);
178
+ if (sections?.length) return `Covers ${sections.join(", ")}.`;
179
+ if (route.keywords?.length) return `Related topics: ${route.keywords.join(", ")}.`;
180
+ if (route.kind === "openapi") return "OpenAPI artifact for machine-readable API reference data.";
181
+ }
182
+ function llmsTxtListItem(route, basePath) {
183
+ if (!route.contentArtifactUrl) return void 0;
184
+ const description = llmsTxtDescription(route);
185
+ return description ? `- [${route.title}](${basePath}${route.contentArtifactUrl}): ${description}` : `- [${route.title}](${basePath}${route.contentArtifactUrl})`;
186
+ }
187
+ function llmsTxtLocaleLabel(locale, projectConfig) {
188
+ return projectConfig.i18n?.locales.find((item) => item.code === locale)?.label ?? locale;
189
+ }
190
+ function groupRoutesByLocale(routes) {
191
+ const groups = /* @__PURE__ */ new Map();
192
+ for (const route of routes) {
193
+ const key = route.locale ?? "default";
194
+ groups.set(key, [...groups.get(key) ?? [], route]);
195
+ }
196
+ return groups;
197
+ }
198
+ function groupLlmsTxtRoutesByLocale(routes, projectConfig) {
199
+ const routesBySource = /* @__PURE__ */ new Map();
200
+ const defaultLocale = projectConfig.i18n?.defaultLocale;
201
+ for (const route of routes) {
202
+ const sourceKey = `${route.locale ?? defaultLocale ?? "default"}:${route.basePath ?? route.contentArtifactUrl ?? route.path}`;
203
+ const previousRoute = routesBySource.get(sourceKey);
204
+ if (!previousRoute || previousRoute.locale && !route.locale) routesBySource.set(sourceKey, route);
205
+ }
206
+ return groupRoutesByLocale([...routesBySource.values()].map((route) => ({
207
+ ...route,
208
+ locale: route.locale ?? defaultLocale
209
+ })));
210
+ }
211
+ function llmsTxtDocsSectionTitle(locale, groupCount, projectConfig) {
212
+ if (groupCount <= 1) return "Docs";
213
+ return `Docs - ${locale === "default" ? "Default" : llmsTxtLocaleLabel(locale, projectConfig)}`;
214
+ }
172
215
  function createLlmsTxt(routes, projectConfig) {
173
216
  const basePath = normalizeBasePath(projectConfig.routePrefix);
174
217
  const lines = [`# ${projectConfig.title}`, ""];
175
218
  if (projectConfig.description) lines.push(`> ${projectConfig.description}`, "");
176
- lines.push("## Docs");
177
- for (const route of routes.filter((route) => route.kind === "mdx")) {
178
- if (!route.contentArtifactUrl) continue;
179
- lines.push(`- [${route.title}](${basePath}${route.contentArtifactUrl})`);
219
+ lines.push("This file lists the source-ready Markdown and OpenAPI artifacts for this documentation site.", "");
220
+ const mdxRoutes = routes.filter((route) => route.kind === "mdx" && isLlmsTxtRoute(route));
221
+ if (mdxRoutes.length > 0) {
222
+ const localizedGroups = groupLlmsTxtRoutesByLocale(mdxRoutes, projectConfig);
223
+ for (const [locale, localeRoutes] of localizedGroups) {
224
+ lines.push(`## ${llmsTxtDocsSectionTitle(locale, localizedGroups.size, projectConfig)}`);
225
+ for (const route of localeRoutes) {
226
+ const item = llmsTxtListItem(route, basePath);
227
+ if (item) lines.push(item);
228
+ }
229
+ lines.push("");
230
+ }
180
231
  }
181
- const openApiRoutes = routes.filter((route) => route.kind === "openapi");
232
+ const openApiRoutes = routes.filter((route) => route.kind === "openapi" && isLlmsTxtRoute(route));
182
233
  if (openApiRoutes.length > 0) {
183
- lines.push("", "## OpenAPI");
234
+ if (lines.at(-1) !== "") lines.push("");
235
+ lines.push("## OpenAPI");
184
236
  for (const route of openApiRoutes) {
185
- if (!route.contentArtifactUrl) continue;
186
- lines.push(`- [${route.title}](${basePath}${route.contentArtifactUrl})`);
237
+ const item = llmsTxtListItem(route, basePath);
238
+ if (item) lines.push(item);
187
239
  }
188
240
  }
241
+ while (lines.at(-1) === "") lines.pop();
189
242
  return `${lines.join("\n")}\n`;
190
243
  }
191
244
  function createLlmsTxtArtifact(routes, projectConfig) {
@@ -345,6 +398,101 @@ function parseFrontmatter(content) {
345
398
  };
346
399
  }
347
400
  //#endregion
401
+ //#region source/parsers/content.ts
402
+ async function transformContent(transform, input) {
403
+ return transform ? await transform(input) : input;
404
+ }
405
+ function createContentProcessor(transform) {
406
+ return {
407
+ async processMdx(source, filePath) {
408
+ const { frontmatter, content } = parseFrontmatter(source);
409
+ const transformed = await transformContent(transform, {
410
+ kind: "mdx",
411
+ source,
412
+ filePath,
413
+ frontmatter,
414
+ content
415
+ });
416
+ return {
417
+ frontmatter: transformed.frontmatter,
418
+ content: transformed.content
419
+ };
420
+ },
421
+ async processText(source, kind, filePath) {
422
+ return (await transformContent(transform, {
423
+ kind,
424
+ source,
425
+ filePath,
426
+ frontmatter: {},
427
+ content: source
428
+ })).content;
429
+ }
430
+ };
431
+ }
432
+ //#endregion
433
+ //#region source/core/hooks.ts
434
+ async function runHooks(plugins, hookName, input, ctx) {
435
+ let result = input;
436
+ for (const plugin of plugins) {
437
+ const hook = plugin.hooks?.[hookName];
438
+ if (!hook) continue;
439
+ try {
440
+ result = await hook(result, ctx);
441
+ } catch (err) {
442
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "${hookName}" failed: ${err}`, { cause: err });
443
+ }
444
+ }
445
+ return result;
446
+ }
447
+ async function runDevConfigureServerHooks(plugins, server, ctx) {
448
+ for (const plugin of plugins) {
449
+ const hook = plugin.hooks?.["dev:configureServer"];
450
+ if (!hook) continue;
451
+ try {
452
+ await hook(server, ctx);
453
+ } catch (err) {
454
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "dev:configureServer" failed: ${err}`, { cause: err });
455
+ }
456
+ }
457
+ }
458
+ async function runBuildAssetsHooks(plugins, ctx) {
459
+ const assets = [];
460
+ for (const plugin of plugins) {
461
+ const hook = plugin.hooks?.["build:assets"];
462
+ if (!hook) continue;
463
+ try {
464
+ const pluginAssets = await hook(ctx);
465
+ assets.push(...pluginAssets);
466
+ } catch (err) {
467
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "build:assets" failed: ${err}`, { cause: err });
468
+ }
469
+ }
470
+ return assets;
471
+ }
472
+ async function runBuildDoneHooks(plugins, ctx) {
473
+ for (const plugin of plugins) {
474
+ const hook = plugin.hooks?.["build:done"];
475
+ if (!hook) continue;
476
+ try {
477
+ await hook(ctx);
478
+ } catch (err) {
479
+ throw new Error(`[clarify] plugin "${plugin.name}" hook "build:done" failed: ${err}`, { cause: err });
480
+ }
481
+ }
482
+ }
483
+ //#endregion
484
+ //#region source/core/content.ts
485
+ var contentProcessors = /* @__PURE__ */ new WeakMap();
486
+ function createProjectContentProcessor(plugins, ctx) {
487
+ return createContentProcessor((input) => runHooks(plugins, "content:transform", input, ctx));
488
+ }
489
+ function setProjectContentProcessor(ctx, processor) {
490
+ contentProcessors.set(ctx, processor);
491
+ }
492
+ function getProjectContentProcessor(ctx) {
493
+ return contentProcessors.get(ctx);
494
+ }
495
+ //#endregion
348
496
  //#region source/parsers/routes.ts
349
497
  function kebabToTitle(str) {
350
498
  return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
@@ -421,18 +569,19 @@ function resolveLocalizedText(text, locale, fallbackLocale) {
421
569
  if (text === void 0) return void 0;
422
570
  return text[locale] ?? (fallbackLocale ? text[fallbackLocale] : void 0) ?? Object.values(text)[0];
423
571
  }
424
- function findContentRoutes(dir, base = dir) {
572
+ async function findContentRoutes(dir, base = dir, options = {}) {
425
573
  const routes = [];
426
574
  if (!(0, node_fs.existsSync)(dir)) return routes;
427
575
  const entries = (0, node_fs.readdirSync)(dir, { withFileTypes: true });
428
576
  for (const entry of entries) {
429
577
  const fullPath = (0, node_path.join)(dir, entry.name);
430
- if (entry.isDirectory()) routes.push(...findContentRoutes(fullPath, base));
578
+ if (entry.isDirectory()) routes.push(...await findContentRoutes(fullPath, base, options));
431
579
  else if (entry.isFile() && /\.mdx?$/.test(entry.name)) {
432
580
  const relativePath = (0, node_path.relative)(base, fullPath);
433
581
  const pathParts = relativePath.replace(/\.mdx?$/, "").split("/");
434
582
  const cleanPath = ("/" + pathParts.map((p) => p === "index" ? "" : p).filter(Boolean).join("/")).replace(/\/+/g, "/").replace(/\/$/, "") || "/";
435
- const { frontmatter, content } = parseFrontmatter((0, node_fs.readFileSync)(fullPath, "utf-8"));
583
+ const source = (0, node_fs.readFileSync)(fullPath, "utf-8");
584
+ const { frontmatter, content } = await (options.contentProcessor ?? createContentProcessor()).processMdx(source, fullPath);
436
585
  let title = typeof frontmatter.title === "string" ? frontmatter.title : "";
437
586
  if (!title) {
438
587
  const lastPart = pathParts[pathParts.length - 1] ?? "";
@@ -689,6 +838,13 @@ var OPENAPI_HTTP_METHODS = [
689
838
  function errorMessage(error) {
690
839
  return error instanceof Error ? error.message : String(error);
691
840
  }
841
+ function isSameOpenAPIFile(url, filePath) {
842
+ try {
843
+ return (0, node_path.resolve)((0, node_url.fileURLToPath)(url)) === (0, node_path.resolve)(filePath);
844
+ } catch {
845
+ return (0, node_path.resolve)(url) === (0, node_path.resolve)(filePath);
846
+ }
847
+ }
692
848
  function findOpenAPIRoutes(dir, base = dir) {
693
849
  const routes = [];
694
850
  if (!(0, node_fs.existsSync)(dir)) return routes;
@@ -752,12 +908,26 @@ function extractOpenAPISections(spec, filterTags) {
752
908
  }
753
909
  return sections;
754
910
  }
755
- async function readOpenAPISpec(filePath) {
911
+ async function readOpenAPISpec(filePath, contentProcessor) {
756
912
  try {
757
- return {
913
+ if (!contentProcessor) return {
758
914
  ok: true,
759
915
  spec: await _apidevtools_swagger_parser.default.dereference(filePath)
760
916
  };
917
+ const source = (0, node_fs.readFileSync)(filePath, "utf-8");
918
+ const transformedSource = await contentProcessor.processText(source, "openapi", filePath);
919
+ if (transformedSource === source) return {
920
+ ok: true,
921
+ spec: await _apidevtools_swagger_parser.default.dereference(filePath)
922
+ };
923
+ return {
924
+ ok: true,
925
+ spec: await _apidevtools_swagger_parser.default.dereference(filePath, { resolve: { transformedOpenAPISource: {
926
+ order: 1,
927
+ canRead: (file) => isSameOpenAPIFile(file.url, filePath),
928
+ read: () => transformedSource
929
+ } } })
930
+ };
761
931
  } catch (error) {
762
932
  return {
763
933
  ok: false,
@@ -1018,7 +1188,7 @@ function createOpenAPIPlugin() {
1018
1188
  const result = specFromCache ? {
1019
1189
  ok: true,
1020
1190
  spec: specFromCache
1021
- } : await readOpenAPISpec(route.filePath);
1191
+ } : await readOpenAPISpec(route.filePath, getProjectContentProcessor(ctx));
1022
1192
  if (!result.ok) {
1023
1193
  route.diagnostic = result.diagnostic;
1024
1194
  route.title = route.title || "OpenAPI parse error";
@@ -1335,9 +1505,51 @@ function createSourceLinksPlugin() {
1335
1505
  };
1336
1506
  }
1337
1507
  //#endregion
1508
+ //#region source/parsers/variables.ts
1509
+ var variablePattern = /{{\s*([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*)\s*}}/g;
1510
+ function getVariableValue(variables, path) {
1511
+ let current = variables;
1512
+ for (const segment of path.split(".")) {
1513
+ if (!current || typeof current !== "object" || Array.isArray(current)) return void 0;
1514
+ current = current[segment];
1515
+ }
1516
+ return current;
1517
+ }
1518
+ function stringifyVariableValue(variables, path, stack) {
1519
+ if (stack.includes(path)) throw new Error(`[clarify] variable "${path}" contains a circular reference: ${[...stack, path].join(" -> ")}`);
1520
+ const value = getVariableValue(variables, path);
1521
+ if (value === void 0 || typeof value === "object") return void 0;
1522
+ if (typeof value !== "string") return String(value);
1523
+ return value.replace(variablePattern, (match, nestedPath) => stringifyVariableValue(variables, nestedPath, [...stack, path]) ?? match);
1524
+ }
1525
+ function applyVariables(content, variables = {}) {
1526
+ return content.replace(variablePattern, (match, path) => stringifyVariableValue(variables, path, []) ?? match);
1527
+ }
1528
+ //#endregion
1529
+ //#region source/plugins/variables/index.ts
1530
+ function applyVariablesToFrontmatter(frontmatter, variables) {
1531
+ const nextFrontmatter = { ...frontmatter };
1532
+ for (const key of ["title", "description"]) {
1533
+ if (typeof nextFrontmatter[key] !== "string") continue;
1534
+ nextFrontmatter[key] = applyVariables(nextFrontmatter[key], variables);
1535
+ }
1536
+ return nextFrontmatter;
1537
+ }
1538
+ function createVariablesPlugin() {
1539
+ return {
1540
+ name: "clarify:variables",
1541
+ hooks: { "content:transform": (input, ctx) => ({
1542
+ ...input,
1543
+ frontmatter: applyVariablesToFrontmatter(input.frontmatter, ctx.projectConfig.variables),
1544
+ content: applyVariables(input.content, ctx.projectConfig.variables)
1545
+ }) }
1546
+ };
1547
+ }
1548
+ //#endregion
1338
1549
  //#region source/core/builtin.ts
1339
1550
  function createBuiltinPlugins(options = {}) {
1340
1551
  const plugins = [
1552
+ createVariablesPlugin(),
1341
1553
  createOpenAPIPlugin(),
1342
1554
  createSourceLinksPlugin(),
1343
1555
  createContentArtifactsPlugin(),
@@ -1406,6 +1618,13 @@ var clarifyFooterConfigSchema = zod.z.object({
1406
1618
  socials: zod.z.record(zod.z.string(), zod.z.string()).optional(),
1407
1619
  copyright: clarifyLocalizedTextSchema.optional()
1408
1620
  }).strict();
1621
+ var clarifyVariableValueSchema = zod.z.lazy(() => zod.z.union([
1622
+ zod.z.string(),
1623
+ zod.z.number(),
1624
+ zod.z.boolean(),
1625
+ zod.z.record(zod.z.string(), clarifyVariableValueSchema)
1626
+ ]));
1627
+ var clarifyVariablesConfigSchema = zod.z.record(zod.z.string(), clarifyVariableValueSchema);
1409
1628
  var clarifySourceConfigSchema = zod.z.object({
1410
1629
  repository: zod.z.string(),
1411
1630
  branch: zod.z.string().optional(),
@@ -1483,9 +1702,11 @@ var clarifyProjectConfigSchema = zod.z.object({
1483
1702
  favicon: clarifyFaviconConfigSchema.optional(),
1484
1703
  theme: clarifyThemeConfigSchema.optional(),
1485
1704
  routePrefix: zod.z.string().optional(),
1705
+ assetPrefix: zod.z.string().optional(),
1486
1706
  navbar: zod.z.object({ links: zod.z.array(clarifyNavbarLinkSchema).optional() }).optional(),
1487
1707
  banner: clarifyBannerConfigSchema.optional(),
1488
1708
  footer: clarifyFooterConfigSchema.optional(),
1709
+ variables: clarifyVariablesConfigSchema.optional(),
1489
1710
  i18n: clarifyI18nConfigSchema.optional(),
1490
1711
  tabs: clarifyTabsConfigSchema.optional()
1491
1712
  });
@@ -1545,7 +1766,22 @@ function resolveI18nConfig(i18n) {
1545
1766
  locales: i18n.locales
1546
1767
  };
1547
1768
  }
1769
+ function resolveRoutePrefix(routePrefix) {
1770
+ const trimmed = routePrefix?.trim();
1771
+ if (!trimmed || trimmed === "/") return "/";
1772
+ return `/${trimmed.replace(/^\/+|\/+$/g, "")}/`;
1773
+ }
1774
+ function resolveAssetPrefix(assetPrefix, routePrefix) {
1775
+ const trimmed = assetPrefix?.trim();
1776
+ if (trimmed === void 0) return routePrefix;
1777
+ if (trimmed === "./" || trimmed === "") return trimmed || "/";
1778
+ if (!trimmed || trimmed === "/") return "/";
1779
+ if (/^[a-z][a-z\d+.-]*:\/\//i.test(trimmed)) return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
1780
+ if (trimmed.startsWith("./") || trimmed.startsWith("../")) return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
1781
+ return `/${trimmed.replace(/^\/+|\/+$/g, "")}/`;
1782
+ }
1548
1783
  function resolveProjectConfig(config = {}) {
1784
+ const routePrefix = resolveRoutePrefix(config.routePrefix);
1549
1785
  return {
1550
1786
  title: config.title ?? "Clarify Docs",
1551
1787
  description: config.description ?? "",
@@ -1554,15 +1790,137 @@ function resolveProjectConfig(config = {}) {
1554
1790
  logo: config.logo,
1555
1791
  homeUrl: config.homeUrl,
1556
1792
  favicon: config.favicon,
1557
- routePrefix: config.routePrefix ?? "/",
1793
+ routePrefix,
1794
+ assetPrefix: resolveAssetPrefix(config.assetPrefix, routePrefix),
1558
1795
  theme: resolveThemeConfig(config.theme),
1559
1796
  navbar: config.navbar,
1560
1797
  banner: config.banner,
1561
1798
  footer: config.footer,
1799
+ variables: config.variables ?? {},
1562
1800
  i18n: resolveI18nConfig(config.i18n),
1563
1801
  tabs: config.tabs
1564
1802
  };
1565
1803
  }
1804
+ /** Serialize a ContentRoute into the minimal shape exposed to tooling. */
1805
+ function toDevRouteEntry(route) {
1806
+ return {
1807
+ path: route.path,
1808
+ filePath: route.filePath,
1809
+ locale: route.locale,
1810
+ basePath: route.basePath,
1811
+ kind: route.kind,
1812
+ title: route.title
1813
+ };
1814
+ }
1815
+ /**
1816
+ * Resolve a single file path to its preview route.
1817
+ *
1818
+ * Selection order:
1819
+ * 1. Routes whose `filePath` exactly matches.
1820
+ * 2. Among matches, prefer the one whose `locale` matches the file's locale
1821
+ * directory (e.g. `source/zh-CN/x.mdx` → locale `zh-CN`).
1822
+ * 3. If no locale match, fall back to the default locale (if i18n is enabled)
1823
+ * or the first match.
1824
+ */
1825
+ function resolveRouteForFile(filePath, routes, defaultLocale) {
1826
+ const matches = routes.filter((r) => r.filePath === filePath);
1827
+ if (matches.length === 0) return void 0;
1828
+ if (matches.length === 1) return matches[0];
1829
+ const fileLocale = inferLocaleFromPath(filePath);
1830
+ if (fileLocale) {
1831
+ const localeMatch = matches.find((r) => r.locale === fileLocale);
1832
+ if (localeMatch) return localeMatch;
1833
+ }
1834
+ if (defaultLocale) {
1835
+ const defaultMatch = matches.find((r) => r.locale === defaultLocale);
1836
+ if (defaultMatch) return defaultMatch;
1837
+ }
1838
+ return matches[0];
1839
+ }
1840
+ /**
1841
+ * Derive a best-effort preview route for a file that is not in the route
1842
+ * manifest (e.g. a draft or newly created file). The path is computed from the
1843
+ * file's location relative to the content root, following the same convention
1844
+ * as route discovery (`source/{locale}/path/index.mdx` → `/{locale}/path`).
1845
+ *
1846
+ * Returns null when the file is outside the content root or is not a
1847
+ * recognized content file type.
1848
+ */
1849
+ function inferRouteForFile(filePath, contentRoot, defaultLocale) {
1850
+ const ref = (0, node_path.relative)(contentRoot, filePath);
1851
+ if (!ref || ref.startsWith("..")) return null;
1852
+ const isMdx = /\.mdx?$/.test(filePath);
1853
+ const isOpenApi = /\.openapi\.(json|ya?ml)$/.test(filePath);
1854
+ if (!isMdx && !isOpenApi) return null;
1855
+ const segments = ref.split("/");
1856
+ const fileLocale = inferLocaleFromPath(filePath);
1857
+ const localeSegmentIndex = fileLocale ? segments.indexOf(fileLocale) : -1;
1858
+ const basePath = basePathFromRef(localeSegmentIndex >= 0 ? segments.slice(localeSegmentIndex + 1).join("/") : ref);
1859
+ const path = fileLocale && defaultLocale ? localizedRoutePath(basePath, fileLocale, {
1860
+ defaultLocale,
1861
+ missing: "fallback",
1862
+ locales: []
1863
+ }) : basePath;
1864
+ const title = segments[segments.length - 1]?.replace(/\.(mdx?|openapi\.(json|ya?ml))$/, "") || "Untitled";
1865
+ return {
1866
+ path: normalizePath(path),
1867
+ filePath,
1868
+ locale: fileLocale,
1869
+ basePath: normalizePath(basePath),
1870
+ kind: isMdx ? "mdx" : "openapi",
1871
+ title,
1872
+ inferred: true
1873
+ };
1874
+ }
1875
+ /** Best-effort locale inference from a file path segment like `.../zh-CN/x.mdx`. */
1876
+ function inferLocaleFromPath(filePath) {
1877
+ const segments = filePath.split("/");
1878
+ for (const segment of segments) if (/^[a-z]{2,3}(-[A-Z][a-z]{3})?(-[A-Z]{2})?$/.test(segment)) return segment;
1879
+ }
1880
+ /** Read and parse the JSON body of an IncomingMessage. */
1881
+ function readJsonBody(req) {
1882
+ return new Promise((resolveBody, reject) => {
1883
+ const chunks = [];
1884
+ req.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
1885
+ req.on("error", reject);
1886
+ req.on("end", () => {
1887
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
1888
+ if (!raw) return resolveBody({});
1889
+ try {
1890
+ resolveBody(JSON.parse(raw));
1891
+ } catch (err) {
1892
+ reject(err);
1893
+ }
1894
+ });
1895
+ });
1896
+ }
1897
+ /**
1898
+ * Handle a request to the dev route endpoint. Pure function — does not touch
1899
+ * the network directly, only reads the request body and writes to `res`.
1900
+ */
1901
+ async function handleDevRouteRequest(req, res, routes, projectConfig, contentRoot) {
1902
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
1903
+ let body;
1904
+ try {
1905
+ body = await readJsonBody(req);
1906
+ } catch {
1907
+ res.statusCode = 400;
1908
+ res.end(JSON.stringify({ error: "Invalid JSON body" }));
1909
+ return;
1910
+ }
1911
+ const file = typeof body === "object" && body !== null ? body.file : void 0;
1912
+ if (typeof file === "string" && file.length > 0) {
1913
+ const route = resolveRouteForFile(file, routes, projectConfig.i18n?.defaultLocale);
1914
+ if (route) {
1915
+ res.end(JSON.stringify(toDevRouteEntry(route)));
1916
+ return;
1917
+ }
1918
+ const inferred = inferRouteForFile(file, contentRoot, projectConfig.i18n?.defaultLocale);
1919
+ res.end(JSON.stringify(inferred));
1920
+ return;
1921
+ }
1922
+ res.end(JSON.stringify(routes.map(toDevRouteEntry)));
1923
+ }
1566
1924
  //#endregion
1567
1925
  //#region source/core/env-types.ts
1568
1926
  var GENERATED_FILE_NAME = "clarify-env.d.ts";
@@ -1612,57 +1970,6 @@ function writeClarifyEnvDts(projectRoot, plugins) {
1612
1970
  (0, node_fs.writeFileSync)(filePath, next, "utf-8");
1613
1971
  }
1614
1972
  //#endregion
1615
- //#region source/core/hooks.ts
1616
- async function runHooks(plugins, hookName, input, ctx) {
1617
- let result = input;
1618
- for (const plugin of plugins) {
1619
- const hook = plugin.hooks?.[hookName];
1620
- if (!hook) continue;
1621
- try {
1622
- result = await hook(result, ctx);
1623
- } catch (err) {
1624
- throw new Error(`[clarify] plugin "${plugin.name}" hook "${hookName}" failed: ${err}`, { cause: err });
1625
- }
1626
- }
1627
- return result;
1628
- }
1629
- async function runDevConfigureServerHooks(plugins, server, ctx) {
1630
- for (const plugin of plugins) {
1631
- const hook = plugin.hooks?.["dev:configureServer"];
1632
- if (!hook) continue;
1633
- try {
1634
- await hook(server, ctx);
1635
- } catch (err) {
1636
- throw new Error(`[clarify] plugin "${plugin.name}" hook "dev:configureServer" failed: ${err}`, { cause: err });
1637
- }
1638
- }
1639
- }
1640
- async function runBuildAssetsHooks(plugins, ctx) {
1641
- const assets = [];
1642
- for (const plugin of plugins) {
1643
- const hook = plugin.hooks?.["build:assets"];
1644
- if (!hook) continue;
1645
- try {
1646
- const pluginAssets = await hook(ctx);
1647
- assets.push(...pluginAssets);
1648
- } catch (err) {
1649
- throw new Error(`[clarify] plugin "${plugin.name}" hook "build:assets" failed: ${err}`, { cause: err });
1650
- }
1651
- }
1652
- return assets;
1653
- }
1654
- async function runBuildDoneHooks(plugins, ctx) {
1655
- for (const plugin of plugins) {
1656
- const hook = plugin.hooks?.["build:done"];
1657
- if (!hook) continue;
1658
- try {
1659
- await hook(ctx);
1660
- } catch (err) {
1661
- throw new Error(`[clarify] plugin "${plugin.name}" hook "build:done" failed: ${err}`, { cause: err });
1662
- }
1663
- }
1664
- }
1665
- //#endregion
1666
1973
  //#region source/core/options.ts
1667
1974
  function resolveBuildOptions(options = {}) {
1668
1975
  return {
@@ -1673,12 +1980,90 @@ function resolveBuildOptions(options = {}) {
1673
1980
  };
1674
1981
  }
1675
1982
  //#endregion
1983
+ //#region source/core/user-config.ts
1984
+ function defineConfig(config) {
1985
+ validateProjectConfig(config);
1986
+ return config;
1987
+ }
1988
+ /**
1989
+ * Config filenames Clarify CLI looks for at the project root, in resolution
1990
+ * order. Exported so other modules (e.g. the dev `project-info` endpoint) can
1991
+ * surface the same list to tooling without duplicating it.
1992
+ */
1993
+ var CONFIG_FILENAMES = [
1994
+ "clarify.ts",
1995
+ "clarify.js",
1996
+ "clarify.json"
1997
+ ];
1998
+ function findClarifyConfigFile(root) {
1999
+ for (const filename of CONFIG_FILENAMES) {
2000
+ const filePath = (0, node_path.resolve)(root, filename);
2001
+ if ((0, node_fs.existsSync)(filePath)) return filePath;
2002
+ }
2003
+ }
2004
+ function assertConfigObject(config, configFile) {
2005
+ if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error(`[clarify] ${configFile} must export a configuration object`);
2006
+ return config;
2007
+ }
2008
+ function loadJsonConfig(configFile) {
2009
+ const config = assertConfigObject(JSON.parse((0, node_fs.readFileSync)(configFile, "utf-8")), configFile);
2010
+ const projectConfig = validateProjectConfig(config);
2011
+ const ssg = config.ssg && typeof config.ssg === "object" && !Array.isArray(config.ssg) ? config.ssg : void 0;
2012
+ return {
2013
+ ...projectConfig,
2014
+ ...ssg ? { ssg } : {}
2015
+ };
2016
+ }
2017
+ async function loadClarifyConfig(root, env) {
2018
+ const configFile = findClarifyConfigFile(root);
2019
+ if (!configFile) return {};
2020
+ if (configFile.endsWith(".json")) return loadJsonConfig(configFile);
2021
+ return assertConfigObject((await (0, vite.loadConfigFromFile)(env, configFile, root, "silent"))?.config, configFile);
2022
+ }
2023
+ /**
2024
+ * File extensions Clarify treats as content files. Kept in sync with the
2025
+ * patterns used by route discovery in `parsers/routes.ts` and the OpenAPI
2026
+ * page generator. Exposed to tooling so the extension's "is this a content
2027
+ * file?" check stays accurate without duplicating the regex.
2028
+ */
2029
+ var CONTENT_FILE_EXTENSIONS = [
2030
+ ".md",
2031
+ ".mdx",
2032
+ ".openapi.json",
2033
+ ".openapi.yaml",
2034
+ ".openapi.yml"
2035
+ ];
2036
+ /**
2037
+ * Build the project-info response payload from the resolved plugin state.
2038
+ */
2039
+ function buildProjectInfo(projectRoot, contentRoot, projectConfig) {
2040
+ const response = {
2041
+ configFilenames: CONFIG_FILENAMES,
2042
+ contentFileExtensions: CONTENT_FILE_EXTENSIONS,
2043
+ contentRoot: (contentRoot.startsWith(projectRoot) ? contentRoot.slice(projectRoot.length).replace(/^[/\\]+/, "") : contentRoot) || "source",
2044
+ projectRoot
2045
+ };
2046
+ if (projectConfig.i18n) response.i18n = {
2047
+ defaultLocale: projectConfig.i18n.defaultLocale,
2048
+ locales: projectConfig.i18n.locales.map((l) => l.code)
2049
+ };
2050
+ return response;
2051
+ }
2052
+ /**
2053
+ * Handle a request to the project-info endpoint. Responds to any method
2054
+ * (GET is typical) with the JSON payload — there is no request body to parse.
2055
+ */
2056
+ function handleProjectInfoRequest(_req, res, projectRoot, contentRoot, projectConfig) {
2057
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
2058
+ res.end(JSON.stringify(buildProjectInfo(projectRoot, contentRoot, projectConfig)));
2059
+ }
2060
+ //#endregion
1676
2061
  //#region source/core/site.ts
1677
2062
  async function discoverRoutesForRoot(routeRoot, locale, plugins, ctx) {
1678
2063
  return (await runHooks(plugins, "routes:discover", {
1679
2064
  contentRoot: routeRoot,
1680
2065
  locale,
1681
- routes: findContentRoutes(routeRoot)
2066
+ routes: await findContentRoutes(routeRoot, routeRoot, { contentProcessor: getProjectContentProcessor(ctx) })
1682
2067
  }, ctx)).routes;
1683
2068
  }
1684
2069
  async function discoverRoutes(root, contentRoot, plugins, ctx) {
@@ -1743,6 +2128,7 @@ async function resolveClarifySite(options = {}, resolveOptions = {}) {
1743
2128
  routes: [],
1744
2129
  navigation: []
1745
2130
  };
2131
+ setProjectContentProcessor(ctx, createProjectContentProcessor(plugins, ctx));
1746
2132
  let routes = await discoverRoutes(root, contentRoot, plugins, ctx);
1747
2133
  routes = await runHooks(plugins, "routes:discovered", routes, ctx);
1748
2134
  routes = applyConfiguredPageRoutePaths(routes, projectConfig.tabs, projectConfig.i18n);
@@ -1945,42 +2331,6 @@ async function renderSSGRoutes(routes, projectConfig, outputDirectory, ssrBundle
1945
2331
  }
1946
2332
  }
1947
2333
  //#endregion
1948
- //#region source/core/user-config.ts
1949
- function defineConfig(config) {
1950
- validateProjectConfig(config);
1951
- return config;
1952
- }
1953
- var CONFIG_FILENAMES = [
1954
- "clarify.ts",
1955
- "clarify.js",
1956
- "clarify.json"
1957
- ];
1958
- function findClarifyConfigFile(root) {
1959
- for (const filename of CONFIG_FILENAMES) {
1960
- const filePath = (0, node_path.resolve)(root, filename);
1961
- if ((0, node_fs.existsSync)(filePath)) return filePath;
1962
- }
1963
- }
1964
- function assertConfigObject(config, configFile) {
1965
- if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error(`[clarify] ${configFile} must export a configuration object`);
1966
- return config;
1967
- }
1968
- function loadJsonConfig(configFile) {
1969
- const config = assertConfigObject(JSON.parse((0, node_fs.readFileSync)(configFile, "utf-8")), configFile);
1970
- const projectConfig = validateProjectConfig(config);
1971
- const ssg = config.ssg && typeof config.ssg === "object" && !Array.isArray(config.ssg) ? config.ssg : void 0;
1972
- return {
1973
- ...projectConfig,
1974
- ...ssg ? { ssg } : {}
1975
- };
1976
- }
1977
- async function loadClarifyConfig(root, env) {
1978
- const configFile = findClarifyConfigFile(root);
1979
- if (!configFile) return {};
1980
- if (configFile.endsWith(".json")) return loadJsonConfig(configFile);
1981
- return assertConfigObject((await (0, vite.loadConfigFromFile)(env, configFile, root, "silent"))?.config, configFile);
1982
- }
1983
- //#endregion
1984
2334
  //#region source/core/plugin.ts
1985
2335
  function loadVirtualModule(id, modules) {
1986
2336
  const bareId = stripVirtualPrefix(id);
@@ -2071,7 +2421,7 @@ function clarifyPlugin(options = {}) {
2071
2421
  async config() {
2072
2422
  await resolveRoutesAndSpecs();
2073
2423
  return {
2074
- base: projectConfig.routePrefix,
2424
+ base: projectConfig.assetPrefix,
2075
2425
  build: {
2076
2426
  ...generateOptions.outputDirectory ? { outDir: generateOptions.outputDirectory } : {},
2077
2427
  manifest: true
@@ -2128,6 +2478,11 @@ function clarifyPlugin(options = {}) {
2128
2478
  async configureServer(server) {
2129
2479
  server.watcher.add(contentRoot);
2130
2480
  if (configFilePath) server.watcher.add(configFilePath);
2481
+ server.middlewares.use((req, res, next) => {
2482
+ if (req.url === "/dev/query-preview-route" && req.method === "POST") return handleDevRouteRequest(req, res, routes, projectConfig, contentRoot);
2483
+ if (req.url === "/dev/project-info" && (req.method === "GET" || req.method === "HEAD")) return handleProjectInfoRequest(req, res, root, contentRoot, projectConfig);
2484
+ next();
2485
+ });
2131
2486
  const handleContentTreeChange = async (filePath) => {
2132
2487
  const relativeContentFile = (0, node_path.relative)(contentRoot, (0, node_path.isAbsolute)(filePath) ? filePath : (0, node_path.join)(root, filePath));
2133
2488
  if (!(relativeContentFile && !relativeContentFile.startsWith("..") && !(0, node_path.isAbsolute)(relativeContentFile))) return;
@@ -2246,4 +2601,4 @@ Object.defineProperty(exports, "resolveClarifySite", {
2246
2601
  }
2247
2602
  });
2248
2603
 
2249
- //# sourceMappingURL=plugin-B7c0iHMy.cjs.map
2604
+ //# sourceMappingURL=plugin-JvxU6ZjU.cjs.map