@tenphi/docs 0.17.0 → 0.18.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.
@@ -1,4 +1,4 @@
1
- import { i as normalizeDocsConfig } from "./config-Ci9Nv0wQ.js";
1
+ import { i as normalizeDocsConfig, m as validateTableOfContents, p as validateSeo, u as pagePublishing } from "./config-CD77r6rT.js";
2
2
  import { cloneAst, parseMarkdown, removeRenderedTitle, serializeMarkdown, stripLeadingBadgeBlock } from "./markdown/index.js";
3
3
  import { lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
4
4
  import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
@@ -150,17 +150,20 @@ function openApiPages(source, routeBase, text) {
150
150
  ];
151
151
  if (string(operation.description)) body.push(operation.description, "");
152
152
  if (Array.isArray(operation.tags) && operation.tags.length) body.push(`**Tags:** ${operation.tags.filter((tag) => typeof tag === "string").map(escapeText).join(", ")}`, "");
153
- const pathItem = resolveReference(document.paths[path], document);
154
- const parameters = [...Array.isArray(pathItem.parameters) ? pathItem.parameters : [], ...Array.isArray(operation.parameters) ? operation.parameters : []];
153
+ const parameters = mergeParameters(resolveReference(document.paths[path], document).parameters, operation.parameters, document);
154
+ renderSecurity(body, operation.security !== void 0 ? operation.security : document.security, document);
155
155
  if (parameters.length) {
156
156
  body.push("## Parameters", "", "| Name | In | Required | Description |", "| --- | --- | --- | --- |");
157
- for (const parameter of parameters) {
158
- const item = resolveReference(parameter, document);
159
- if (!record(item)) continue;
157
+ for (const item of parameters) {
160
158
  const schema = record(item.schema) ? ` (${schemaLabel(item.schema)})` : "";
161
159
  body.push(`| ${tableCell(string(item.name) ?? "—")} | ${tableCell(string(item.in) ?? "—")} | ${item.required === true ? "Yes" : "No"} | ${tableCell(`${string(item.description) ?? ""}${schema}`)} |`);
162
160
  }
163
161
  body.push("");
162
+ for (const item of parameters) if ("example" in item || "examples" in item || "content" in item) {
163
+ body.push(`### ${escapeText(String(item.name))} (${escapeText(String(item.in))})`, "");
164
+ renderExamples(body, item, document);
165
+ renderContent(body, item.content, document);
166
+ }
164
167
  }
165
168
  if (operation.requestBody !== void 0) {
166
169
  const request = resolveReference(operation.requestBody, document);
@@ -168,7 +171,7 @@ function openApiPages(source, routeBase, text) {
168
171
  body.push("## Request body", "");
169
172
  if (string(request.description)) body.push(request.description, "");
170
173
  body.push(`**Required:** ${request.required === true ? "Yes" : "No"}`, "");
171
- renderContent(body, request.content);
174
+ renderContent(body, request.content, document);
172
175
  }
173
176
  }
174
177
  if (record(operation.responses)) {
@@ -177,12 +180,12 @@ function openApiPages(source, routeBase, text) {
177
180
  const response = resolveReference(value, document);
178
181
  if (!record(response)) continue;
179
182
  body.push(`### ${escapeText(status)}${string(response.description) ? ` — ${escapeText(response.description)}` : ""}`, "");
180
- renderContent(body, response.content);
183
+ renderContent(body, response.content, document);
181
184
  }
182
185
  }
183
186
  pages.push({
184
187
  route,
185
- sourcePath: `${source}#${route.slice(routeBase.length + 1)}.md`,
188
+ sourcePath: `${source}#${route.split("/").at(-1)}.md`,
186
189
  title: operationTitle,
187
190
  ...string(operation.description) ? { description: operation.description } : {},
188
191
  body: body.join("\n")
@@ -190,15 +193,85 @@ function openApiPages(source, routeBase, text) {
190
193
  }
191
194
  return pages;
192
195
  }
193
- function renderContent(lines, content) {
196
+ function mergeParameters(path, operation, document) {
197
+ const merged = /* @__PURE__ */ new Map();
198
+ for (const list of [path, operation]) {
199
+ if (list === void 0) continue;
200
+ if (!Array.isArray(list)) throw new Error("OpenAPI parameters must be an array.");
201
+ const seen = /* @__PURE__ */ new Set();
202
+ for (const value of list) {
203
+ const item = resolveReference(value, document);
204
+ if (!record(item) || !string(item.name) || ![
205
+ "query",
206
+ "header",
207
+ "path",
208
+ "cookie"
209
+ ].includes(String(item.in))) throw new Error("OpenAPI parameters need a name and a valid in location.");
210
+ const identity = JSON.stringify([item.name, item.in]);
211
+ if (seen.has(identity)) throw new Error(`Duplicate OpenAPI parameter ${item.name} in ${item.in}.`);
212
+ seen.add(identity);
213
+ merged.set(identity, item);
214
+ }
215
+ }
216
+ return [...merged.values()];
217
+ }
218
+ function renderContent(lines, content, document) {
194
219
  if (!record(content)) return;
195
220
  for (const [mediaType, media] of Object.entries(content)) {
196
221
  lines.push(`#### ${escapeText(mediaType)}`, "");
197
222
  if (record(media) && media.schema !== void 0) lines.push(jsonBlock(media.schema), "");
198
- if (record(media) && record(media.examples)) for (const [name, example] of Object.entries(media.examples)) {
199
- const value = record(example) && "value" in example ? example.value : example;
200
- lines.push(`**Example: ${escapeText(name)}**`, "", jsonBlock(value), "");
223
+ if (record(media)) renderExamples(lines, media, document);
224
+ }
225
+ }
226
+ function renderExamples(lines, owner, document) {
227
+ if ("example" in owner) lines.push("**Example**", "", jsonBlock(owner.example), "");
228
+ if (!record(owner.examples)) return;
229
+ for (const [name, raw] of Object.entries(owner.examples)) {
230
+ const example = resolveReference(raw, document);
231
+ if (!record(example)) throw new Error(`OpenAPI example ${name} must be an Example Object.`);
232
+ lines.push(`**Example: ${escapeText(name)}**`, "");
233
+ if (string(example.summary)) lines.push(escapeText(example.summary), "");
234
+ if (string(example.description)) lines.push(example.description, "");
235
+ if ("value" in example) lines.push(jsonBlock(example.value), "");
236
+ if (string(example.externalValue)) {
237
+ const url = example.externalValue;
238
+ lines.push(/^https?:\/\/[^\s<>]+$/i.test(url) ? `[External example](<${url}>)` : `External example: ${escapeText(url)}`, "");
239
+ }
240
+ }
241
+ }
242
+ function renderSecurity(lines, security, document) {
243
+ lines.push("## Authentication", "");
244
+ if (security === void 0 || Array.isArray(security) && !security.length) {
245
+ lines.push("No authentication required.", "");
246
+ return;
247
+ }
248
+ if (!Array.isArray(security)) throw new Error("OpenAPI security must be an array.");
249
+ const schemes = record(document.components) && record(document.components.securitySchemes) ? document.components.securitySchemes : {};
250
+ if (security.length > 1) lines.push("Use any one of these alternatives:", "");
251
+ for (const [index, requirement] of security.entries()) {
252
+ if (!record(requirement)) throw new Error("OpenAPI security requirements must be objects.");
253
+ if (security.length > 1) lines.push(`### Alternative ${index + 1}`, "");
254
+ const names = Object.keys(requirement);
255
+ if (!names.length) {
256
+ lines.push("No authentication required (anonymous access).", "");
257
+ continue;
258
+ }
259
+ if (names.length > 1) lines.push("All of the following are required together:", "");
260
+ for (const name of names) {
261
+ const scheme = resolveReference(schemes[name], document);
262
+ if (!record(scheme)) throw new Error(`Unknown OpenAPI security scheme: ${name}.`);
263
+ const scopes = requirement[name];
264
+ if (!Array.isArray(scopes) || scopes.some((scope) => typeof scope !== "string")) throw new Error(`OpenAPI security scopes for ${name} must be strings.`);
265
+ let detail = string(scheme.type) ?? "security scheme";
266
+ if (scheme.type === "http") detail = `HTTP ${string(scheme.scheme) ?? "authentication"}${string(scheme.bearerFormat) ? ` (${scheme.bearerFormat})` : ""}`;
267
+ else if (scheme.type === "apiKey") detail = `API key ${string(scheme.name) ?? name} in ${string(scheme.in) ?? "request"}`;
268
+ else if (scheme.type === "oauth2") detail = "OAuth 2.0";
269
+ else if (scheme.type === "openIdConnect") detail = "OpenID Connect";
270
+ else if (scheme.type === "mutualTLS") detail = "Mutual TLS";
271
+ lines.push(`- **${escapeText(name)}:** ${escapeText(detail)}${scopes.length ? `. Required scopes: ${scopes.map((scope) => escapeText(String(scope))).join(", ")}` : ""}`);
272
+ if (string(scheme.description)) lines.push("", scheme.description, "");
201
273
  }
274
+ lines.push("");
202
275
  }
203
276
  }
204
277
  function assertLocalReferences(value, document, source, seen = /* @__PURE__ */ new Set()) {
@@ -208,22 +281,46 @@ function assertLocalReferences(value, document, source, seen = /* @__PURE__ */ n
208
281
  else for (const [key, item] of Object.entries(value)) {
209
282
  if (key === "$ref" && (typeof item !== "string" || !item.startsWith("#/"))) throw new Error(`OpenAPI source ${source} has an external or invalid $ref; bundle references into the document first.`);
210
283
  if (key === "$ref" && typeof item === "string" && lookupReference(item, document) === void 0) throw new Error(`OpenAPI source ${source} has an unresolved reference: ${item}.`);
284
+ if ([
285
+ "example",
286
+ "default",
287
+ "const",
288
+ "enum"
289
+ ].includes(key)) continue;
290
+ if (key === "examples") {
291
+ if (record(item)) {
292
+ for (const example of Object.values(item)) if (record(example)) {
293
+ const { value: _payload, ...metadata } = example;
294
+ assertLocalReferences(metadata, document, source, seen);
295
+ }
296
+ }
297
+ continue;
298
+ }
299
+ if (key === "properties" && record(item)) {
300
+ for (const schema of Object.values(item)) assertLocalReferences(schema, document, source, seen);
301
+ continue;
302
+ }
211
303
  assertLocalReferences(item, document, source, seen);
212
304
  }
213
305
  }
214
306
  function resolveReference(value, document) {
215
307
  const visited = /* @__PURE__ */ new Set();
308
+ const overrides = {};
216
309
  while (record(value) && typeof value.$ref === "string") {
310
+ for (const key of ["summary", "description"]) if (!(key in overrides) && typeof value[key] === "string") overrides[key] = value[key];
217
311
  const ref = value.$ref;
218
312
  if (visited.has(ref)) throw new Error(`Circular OpenAPI reference: ${ref}.`);
219
313
  visited.add(ref);
220
314
  value = lookupReference(ref, document);
221
315
  if (value === void 0) throw new Error(`Unresolved OpenAPI reference: ${ref}.`);
222
316
  }
223
- return value;
317
+ return record(value) ? {
318
+ ...value,
319
+ ...overrides
320
+ } : value;
224
321
  }
225
322
  function lookupReference(ref, document) {
226
- return ref.slice(2).split("/").reduce((current, part) => record(current) ? current[part.replace(/~1/g, "/").replace(/~0/g, "~")] : void 0, document);
323
+ return decodeURIComponent(ref.slice(2)).split("/").reduce((current, part) => record(current) || Array.isArray(current) ? current[part.replace(/~1/g, "/").replace(/~0/g, "~")] : void 0, document);
227
324
  }
228
325
  function record(value) {
229
326
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -247,7 +344,7 @@ function schemaLabel(schema) {
247
344
  return typeof schema.$ref === "string" ? schema.$ref.split("/").at(-1) ?? "schema" : string(schema.type) ?? "schema";
248
345
  }
249
346
  function jsonBlock(value) {
250
- const body = JSON.stringify(value, null, 2);
347
+ const body = JSON.stringify(value, null, 2) ?? "null";
251
348
  let longest = 0;
252
349
  for (const [run] of body.matchAll(/`+/g)) longest = Math.max(longest, run.length);
253
350
  const fence = "`".repeat(Math.max(3, longest + 1));
@@ -495,7 +592,8 @@ function inside$1(root, path) {
495
592
  //#region src/graph/index.ts
496
593
  const MARKDOWN_EXTENSIONS = [".md", ".mdx"];
497
594
  const execFileAsync = promisify(execFile);
498
- const FRONTMATTER_KEYS = /* @__PURE__ */ new Set([
595
+ const DOCS_FRONTMATTER_KEYS = /* @__PURE__ */ new Set([
596
+ "seo",
499
597
  "aliases",
500
598
  "title",
501
599
  "description",
@@ -576,10 +674,10 @@ async function createDocsGraph(options = {}) {
576
674
  }], routeMap, diagnostics);
577
675
  validateNavigation(tab.items ?? [], routeMap, diagnostics);
578
676
  }
579
- for (const version of config.site.versions ?? []) if (!routeMap.has(version.routeBase)) diagnostics.push({
677
+ for (const version of config.site.versions ?? []) if (![version.routeBase, ...Object.keys(config.locales ?? {}).filter((locale) => locale !== "root").map((locale) => `/${locale}${version.routeBase === "/" ? "" : version.routeBase}`)].some((route) => routeMap.has(route) && !routeMap.get(route).frontmatter.draft)) diagnostics.push({
580
678
  code: "DOCS_VERSION_ROOT_MISSING",
581
679
  severity: "error",
582
- message: `Version ${version.label} needs a page at ${version.routeBase}.`
680
+ message: `Version ${version.label} needs a non-draft page at ${version.routeBase} or a localized equivalent.`
583
681
  });
584
682
  entries.sort((left, right) => left.route.localeCompare(right.route));
585
683
  return {
@@ -592,6 +690,10 @@ async function createDocsGraph(options = {}) {
592
690
  entryId: entry.id,
593
691
  sourcePath: entry.sourcePath,
594
692
  title: entry.title,
693
+ discoverable: !entry.frontmatter.draft,
694
+ indexable: pagePublishing(entry, config).index,
695
+ sitemap: pagePublishing(entry, config).sitemap,
696
+ canonical: pagePublishing(entry, config).canonical,
595
697
  ...entry.frontmatter.sidebar !== void 0 ? { sidebar: entry.frontmatter.sidebar } : {}
596
698
  })),
597
699
  assets: Array.from(new Map(entries.flatMap((entry) => entry.assets).map((asset) => [asset.publicPath ?? `${asset.original}:${asset.line ?? ""}`, asset])).values()),
@@ -817,7 +919,7 @@ async function readEntry(source, config, diagnostics) {
817
919
  if (frontmatter.sidebar === void 0 && source.navigation !== void 0) frontmatter.sidebar = source.navigation;
818
920
  const diagnosticCount = diagnostics.length;
819
921
  const metadata = {};
820
- for (const key of Object.keys(frontmatter)) if (!FRONTMATTER_KEYS.has(key)) {
922
+ for (const key of Object.keys(frontmatter)) if (!DOCS_FRONTMATTER_KEYS.has(key)) {
821
923
  if (config.content.frontmatter !== "reject") {
822
924
  metadata[key] = frontmatter[key];
823
925
  delete frontmatter[key];
@@ -1381,6 +1483,7 @@ function validateFrontmatter(value, file, diagnostics) {
1381
1483
  message,
1382
1484
  file
1383
1485
  });
1486
+ if (value.seo !== void 0) for (const message of validateSeo(value.seo, "page")) error(message);
1384
1487
  if (value.aliases !== void 0 && (!Array.isArray(value.aliases) || value.aliases.some((alias) => typeof alias !== "string"))) error("aliases must be an array of route strings.");
1385
1488
  for (const key of [
1386
1489
  "title",
@@ -1397,10 +1500,7 @@ function validateFrontmatter(value, file, diagnostics) {
1397
1500
  validateOptionalStrings(record.sidebar, ["label", "group"], "sidebar", error);
1398
1501
  if (record.sidebar.order !== void 0 && typeof record.sidebar.order !== "number") error("sidebar.order must be a number.");
1399
1502
  }
1400
- if (isPlainRecord(record.tableOfContents)) for (const key of ["minHeadingLevel", "maxHeadingLevel"]) {
1401
- const level = record.tableOfContents[key];
1402
- if (level !== void 0 && (!Number.isInteger(level) || Number(level) < 1 || Number(level) > 6)) error(`tableOfContents.${key} must be an integer from 1 to 6.`);
1403
- }
1503
+ if (record.tableOfContents !== void 0) for (const message of validateTableOfContents(record.tableOfContents)) error(message);
1404
1504
  if (record.editUrl !== void 0 && record.editUrl !== false && typeof record.editUrl !== "string") error("editUrl must be false or a string.");
1405
1505
  if (record.lastUpdated !== void 0 && typeof record.lastUpdated !== "boolean" && !(record.lastUpdated instanceof Date)) error("lastUpdated must be a date or boolean.");
1406
1506
  if (record.hero !== void 0) {
@@ -1424,6 +1524,8 @@ function validateHero(hero, error) {
1424
1524
  if (!isPlainRecord(hero.image)) error("hero.image must be an object.");
1425
1525
  else {
1426
1526
  const image = hero.image;
1527
+ for (const key of ["width", "height"]) if (image[key] !== void 0 && (!Number.isInteger(image[key]) || Number(image[key]) <= 0)) error(`hero.image.${key} must be a positive integer.`);
1528
+ if (image.width === void 0 !== (image.height === void 0)) error("hero.image needs both width and height when dimensions are specified.");
1427
1529
  if ([
1428
1530
  "html",
1429
1531
  "file",
@@ -1549,6 +1651,6 @@ function toLoaderEntry(entry) {
1549
1651
  };
1550
1652
  }
1551
1653
  //#endregion
1552
- export { resolvePackageLock as _, validateDocs as a, routeForPath as c, discoverPackage as d, lockForSource as f, reconcileDocsLock as g, readDocsLock as h, formatDiagnostics as i, assertSafePackagePath as l, packageNameFromSpecifier as m, DocsValidationError as n, createDocsGraph as o, materializePackage as p, assertValidDocs as r, normalizeRoute as s, createDocsLoader as t, defaultLock as u, validateLock as v, writeDocsLock as y };
1654
+ export { reconcileDocsLock as _, validateDocs as a, writeDocsLock as b, normalizeRoute as c, defaultLock as d, discoverPackage as f, readDocsLock as g, packageNameFromSpecifier as h, formatDiagnostics as i, routeForPath as l, materializePackage as m, DocsValidationError as n, DOCS_FRONTMATTER_KEYS as o, lockForSource as p, assertValidDocs as r, createDocsGraph as s, createDocsLoader as t, assertSafePackagePath as u, resolvePackageLock as v, validateLock as y };
1553
1655
 
1554
- //# sourceMappingURL=content-XLQlJDnf.js.map
1656
+ //# sourceMappingURL=content-6Xp1J4Jh.js.map