create-thally-docs 0.10.69 → 0.10.70

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.
@@ -140,12 +140,40 @@ import { parse as parseYaml3 } from "yaml";
140
140
  var METHODS = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
141
141
  var isObj = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
142
142
  var flagged = (value) => value === true || value === "true";
143
- function parseDocOperation(raw) {
143
+ function unquote(value) {
144
+ const trimmed = value.trim();
145
+ const quote = trimmed[0];
146
+ return (quote === '"' || quote === "'") && trimmed.length >= 2 && trimmed.endsWith(quote) ? trimmed.slice(1, -1).trim() : trimmed;
147
+ }
148
+ function parseDocReference(raw) {
144
149
  if (typeof raw !== "string") return null;
145
- const parts = raw.trim().split(/\s+/);
146
- if (parts.length < 2) return null;
147
- const path = parts.slice(1).join(" ");
148
- return path.startsWith("/") ? { method: parts[0].toUpperCase(), path } : null;
150
+ const trimmed = unquote(raw);
151
+ if (!trimmed) return null;
152
+ const webhook = /^(?:(.*?\S)\s+)?webhook\s+([^\s/]\S*)$/i.exec(trimmed);
153
+ if (webhook) {
154
+ const specRef2 = webhook[1] ? unquote(webhook[1]) : void 0;
155
+ return { ...specRef2 ? { specRef: specRef2 } : {}, method: "WEBHOOK", path: webhook[2], webhook: true };
156
+ }
157
+ const operation = /^(?:(.*?\S)\s+)?([A-Za-z]+)\s+(\/.*)$/.exec(trimmed);
158
+ if (!operation) return null;
159
+ const specRef = operation[1] ? unquote(operation[1]) : void 0;
160
+ return { ...specRef ? { specRef } : {}, method: operation[2].toUpperCase(), path: operation[3].trim().split(/\s+/).join(" ") };
161
+ }
162
+ var canonicalRef = (value) => value.trim().replace(/\\/g, "/").replace(/^(?:\.\/)+/, "").replace(/^\/+/, "").replace(/^[a-z][a-z0-9+.-]*:\/\/[^/?#]*/i, (origin) => origin.toLowerCase());
163
+ var baseName = (value) => canonicalRef(value).split(/[?#]/, 1)[0].split("/").filter(Boolean).pop() ?? "";
164
+ function pageState(ref, specs) {
165
+ const judge = (spec) => spec.document === void 0 ? "unknown" : ref.webhook ? webhookState(spec.document, ref.path, spec.overrides) : operationState(spec.document, ref.method, ref.path, spec.overrides);
166
+ if (ref.specRef) {
167
+ const wanted = canonicalRef(ref.specRef);
168
+ if (!wanted) return "unknown";
169
+ const wantedBase = baseName(ref.specRef);
170
+ const spec = specs.find((entry) => canonicalRef(entry.source) === wanted) ?? specs.find((entry) => canonicalRef(entry.source).toLowerCase() === wanted.toLowerCase()) ?? (wantedBase ? specs.find((entry) => baseName(entry.source) === wantedBase) : void 0) ?? (wantedBase ? specs.find((entry) => baseName(entry.source).toLowerCase() === wantedBase.toLowerCase()) : void 0);
171
+ return spec ? judge(spec) : "unknown";
172
+ }
173
+ const states = specs.map(judge);
174
+ if (states.includes("published")) return "published";
175
+ if (specs.some((spec) => spec.document === void 0)) return "unknown";
176
+ return states.find((state) => state === "hidden" || state === "excluded") ?? "unknown";
149
177
  }
150
178
  function resolveLocalRef(document, ref) {
151
179
  if (ref !== "#" && !ref.startsWith("#/")) return void 0;
@@ -161,11 +189,7 @@ function resolveLocalRef(document, ref) {
161
189
  }
162
190
  return node;
163
191
  }
164
- function operationState(document, method, path, overrides) {
165
- const verb = method.toLowerCase();
166
- if (!METHODS.includes(verb) || !isObj(document) || !isObj(document.paths)) return "unknown";
167
- const entry = document.paths[path];
168
- if (!isObj(entry)) return "unknown";
192
+ function viewEntry(document, entry) {
169
193
  const chain = [entry];
170
194
  const seen = /* @__PURE__ */ new Set([entry]);
171
195
  for (let current = entry; typeof current.$ref === "string"; ) {
@@ -175,13 +199,142 @@ function operationState(document, method, path, overrides) {
175
199
  chain.push(target);
176
200
  current = target;
177
201
  }
178
- const item = Object.assign({}, ...[...chain].reverse());
179
- const operation = item[verb];
180
- if (!isObj(operation)) return "unknown";
202
+ return { chain, item: Object.assign({}, ...[...chain].reverse()) };
203
+ }
204
+ function visibility(chain, operation, override) {
181
205
  if (chain.some((level) => flagged(level["x-excluded"])) || flagged(operation["x-excluded"])) return "excluded";
182
- const override = isObj(overrides) ? overrides[`${verb.toUpperCase()} ${path}`] : void 0;
183
206
  const hidden = isObj(override) && typeof override.hidden === "boolean" ? override.hidden : chain.some((level) => flagged(level["x-hidden"])) || flagged(operation["x-hidden"]);
184
- return hidden ? "hidden" : "published";
207
+ return hidden ? "hidden" : "visible";
208
+ }
209
+ function operationState(document, method, path, overrides) {
210
+ const verb = method.toLowerCase();
211
+ if (!METHODS.includes(verb) || !isObj(document) || !isObj(document.paths)) return "unknown";
212
+ const entry = document.paths[path];
213
+ if (!isObj(entry)) return "unknown";
214
+ const { chain, item } = viewEntry(document, entry);
215
+ const operation = item[verb];
216
+ if (!isObj(operation)) return "unknown";
217
+ const result = visibility(chain, operation, isObj(overrides) ? overrides[`${verb.toUpperCase()} ${path}`] : void 0);
218
+ return result === "visible" ? "published" : result;
219
+ }
220
+ function webhookState(document, name, overrides) {
221
+ if (!isObj(document) || !isObj(document.webhooks) || !Object.hasOwn(document.webhooks, name)) return "unknown";
222
+ const entry = document.webhooks[name];
223
+ if (!isObj(entry)) return "unknown";
224
+ const { chain, item } = viewEntry(document, entry);
225
+ const states = METHODS.filter((verb) => isObj(item[verb])).map((verb) => visibility(chain, item[verb], isObj(overrides) ? overrides[`WEBHOOK ${verb.toUpperCase()} ${name}`] : void 0));
226
+ if (states.length === 0) return "unknown";
227
+ return states.includes("visible") ? "published" : states[0];
228
+ }
229
+
230
+ // src/manual-api-check.ts
231
+ import { unified } from "unified";
232
+ import remarkParse from "remark-parse";
233
+ import remarkMdx from "remark-mdx";
234
+ var METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]);
235
+ function normalizeServerUrl(value) {
236
+ if (typeof value !== "string") return null;
237
+ const trimmed = value.trim();
238
+ if (!/^https?:\/\/[^\s]+$/i.test(trimmed)) return null;
239
+ let url;
240
+ try {
241
+ url = new URL(trimmed);
242
+ } catch {
243
+ return null;
244
+ }
245
+ if (url.username || url.password || url.search || url.hash || !url.hostname) return null;
246
+ return trimmed.replace(/\/+$/, "");
247
+ }
248
+ function parseManualApi(raw) {
249
+ if (typeof raw !== "string") return null;
250
+ const value = raw.trim().replace(/^(["'])([\s\S]*)\1$/, "$2").trim();
251
+ const match = /^([A-Za-z]+)\s+(\S+)$/.exec(value);
252
+ if (!match) return null;
253
+ const method = match[1].toUpperCase();
254
+ if (!METHODS2.has(method)) return null;
255
+ const split = /^(https?:\/\/[^/?#]*)?([^?#]*)(?:\?([^#]*))?(?:#.*)?$/i.exec(match[2]);
256
+ if (!split || !split[1] && !split[2].startsWith("/")) return null;
257
+ let server;
258
+ if (split[1]) {
259
+ const origin = normalizeServerUrl(split[1]);
260
+ if (!origin || split[1].includes("@")) return null;
261
+ server = origin;
262
+ }
263
+ const query = {};
264
+ if (split[3]) {
265
+ for (const [key, entry] of new URLSearchParams(split[3])) if (key) query[key] = entry;
266
+ }
267
+ return { method, ...server ? { server } : {}, path: split[2] || "/", query };
268
+ }
269
+ function attributeValue(attribute) {
270
+ if (!attribute) return void 0;
271
+ const value = attribute.value;
272
+ if (value === null || value === void 0) return true;
273
+ if (typeof value === "string") return value;
274
+ const expression = (value.value ?? "").trim();
275
+ try {
276
+ const parsed = JSON.parse(expression);
277
+ if (typeof parsed === "string" || typeof parsed === "boolean") return parsed;
278
+ if (typeof parsed === "number") return String(parsed);
279
+ return expression;
280
+ } catch {
281
+ return expression;
282
+ }
283
+ }
284
+ function paramFieldPathNames(mdx) {
285
+ let tree;
286
+ try {
287
+ tree = unified().use(remarkParse).use(remarkMdx).parse(mdx);
288
+ } catch {
289
+ return [];
290
+ }
291
+ const names = [];
292
+ const seen = /* @__PURE__ */ new Set();
293
+ const visit = (node) => {
294
+ if ((node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") && node.name === "ParamField") {
295
+ const attrs = /* @__PURE__ */ new Map();
296
+ for (const attribute of node.attributes ?? []) {
297
+ if (attribute.type === "mdxJsxAttribute" && attribute.name) attrs.set(attribute.name, attribute);
298
+ }
299
+ const located = ["path", "query", "header", "body"].find((key) => {
300
+ const value = attributeValue(attrs.get(key));
301
+ return value !== void 0 && value !== false && value !== "false";
302
+ });
303
+ const locatedName = located ? attributeValue(attrs.get(located)) : void 0;
304
+ const explicit = attributeValue(attrs.get("name"));
305
+ const name = typeof locatedName === "string" && locatedName ? locatedName : typeof explicit === "string" && explicit ? explicit : "";
306
+ const location = located ?? "body";
307
+ if (name && !seen.has(`${location}:${name}`)) {
308
+ seen.add(`${location}:${name}`);
309
+ if (location === "path") names.push(name);
310
+ }
311
+ }
312
+ for (const child of node.children ?? []) visit(child);
313
+ };
314
+ visit(tree);
315
+ return names;
316
+ }
317
+ function mdxServers(docsApi) {
318
+ const mdx = docsApi?.mdx;
319
+ const raw = mdx?.server === void 0 ? [] : Array.isArray(mdx.server) ? mdx.server : [mdx.server];
320
+ return [...new Set(raw.flatMap((entry) => normalizeServerUrl(entry) ?? []))];
321
+ }
322
+ function manualApiProblems(file, data, hasOpenApiReference, mdx, docsApi) {
323
+ if (data.api === void 0 || data.api === null) return [];
324
+ if (hasOpenApiReference) return [`page ${file}: both "openapi" and "api" are set; "api" is ignored`];
325
+ const target = parseManualApi(data.api);
326
+ if (!target) {
327
+ return [`page ${file}: "api" frontmatter is not "METHOD https://host/path" or "METHOD /path"; the page renders without an API playground`];
328
+ }
329
+ const problems = [];
330
+ if (!target.server && mdxServers(docsApi).length === 0) {
331
+ problems.push(`page ${file}: "api" is a path and docs.json has no server: set docs.json "api.mdx.server" or use a full URL, or the playground is disabled`);
332
+ }
333
+ const template = new Set([...target.path.matchAll(/{([^{}/]+)}/g)].map((match) => match[1]));
334
+ for (const name of paramFieldPathNames(mdx)) {
335
+ if (!template.has(name)) problems.push(`page ${file}: <ParamField path="${name}"> has no matching {${name}} in the "api" path; it is ignored in the playground`);
336
+ }
337
+ return problems;
185
338
  }
186
339
 
187
340
  // src/check.ts
@@ -444,25 +597,28 @@ function checkPublicSpecs(projectDir, tabs, issues) {
444
597
  }
445
598
  }
446
599
  function checkUnpublishedOperationPages(projectDir, tabs, pages, issues) {
447
- const api = tabs.find((tab) => !tab.hidden && tab.api?.source)?.api;
448
- const source = api?.source;
449
- if (!source || pages.length === 0 || /^https?:/i.test(source)) return;
450
- const specPath = source.startsWith("/") ? join2(projectDir, "public", source.slice(1)) : join2(projectDir, source);
451
- let spec;
452
- try {
453
- const raw = readFileSync2(specPath, "utf8");
454
- spec = /\.json$/i.test(source) ? JSON.parse(raw) : parseYaml3(raw);
455
- } catch {
456
- return;
457
- }
458
- const overrides = api.overrides;
459
- for (const { file, method, path } of pages) {
460
- const state = operationState(spec, method, path, overrides);
600
+ if (pages.length === 0) return;
601
+ const specs = tabs.filter((tab) => !tab.hidden && typeof tab.api?.source === "string").map((tab) => {
602
+ const source = tab.api.source;
603
+ const overrides = tab.api.overrides;
604
+ if (/^https?:/i.test(source)) return { source, overrides };
605
+ const specPath = source.startsWith("/") ? join2(projectDir, "public", source.slice(1)) : join2(projectDir, source);
606
+ try {
607
+ const raw = readFileSync2(specPath, "utf8");
608
+ return { source, overrides, document: /\.json$/i.test(source) ? JSON.parse(raw) : parseYaml3(raw) };
609
+ } catch {
610
+ return { source, overrides };
611
+ }
612
+ });
613
+ if (specs.length === 0) return;
614
+ for (const { file, reference } of pages) {
615
+ const state = pageState(reference, specs);
461
616
  if (state === "hidden" || state === "excluded") {
617
+ const operation = `${reference.specRef ? `${reference.specRef} ` : ""}${reference.method} ${reference.path}`;
462
618
  issues.push({
463
619
  severity: "warning",
464
620
  file,
465
- message: `page ${file} points at ${state} operation ${method} ${path} and is not published: it returns 404 and is left out of navigation, search, sitemap and llms.txt. Publish the operation or point the page at another one`
621
+ message: `page ${file} points at ${state} operation ${operation} and is not published: it returns 404 and is left out of navigation, search, sitemap and llms.txt. Publish the operation or point the page at another one`
466
622
  });
467
623
  }
468
624
  }
@@ -595,8 +751,11 @@ async function runCheck(projectDir, options) {
595
751
  if (typeof data.openapi !== "string" && content.trim().length < 50) {
596
752
  issues.push({ severity: "warning", message: `Very short body (${content.trim().length} chars) \u2014 page may be empty`, file: rel2 });
597
753
  }
598
- const operation = parseDocOperation(data.openapi);
599
- if (operation) operationPages.push({ file: rel2, ...operation });
754
+ const reference = parseDocReference(data.openapi);
755
+ if (reference) operationPages.push({ file: rel2, reference });
756
+ for (const message of manualApiProblems(rel2, data, Boolean(reference), content, config.api)) {
757
+ issues.push({ severity: "warning", file: rel2, message });
758
+ }
600
759
  if (options.drift) checkDrift(projectDir, rel2, data, issues);
601
760
  const path = pageIdToPath(pageId, secondaryLocales);
602
761
  const anchors = extractHeadingAnchors(content);
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  migrateDocs,
4
4
  parseFrontmatter,
5
5
  runCheck
6
- } from "./chunk-4JW7VBXN.js";
6
+ } from "./chunk-UL4XFPLQ.js";
7
7
  import {
8
8
  logo,
9
9
  scaffold,
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  migrateDocs
4
- } from "../chunk-4JW7VBXN.js";
4
+ } from "../chunk-UL4XFPLQ.js";
5
5
  import "../chunk-KCBUOBEF.js";
6
6
  import "../chunk-XYRM4GAT.js";
7
7
  import "../chunk-UBIX5ISG.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-thally-docs",
3
- "version": "0.10.69",
3
+ "version": "0.10.70",
4
4
  "description": "Scaffold an open-source Thally docs site for people, search engines, and AI tools.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -45,7 +45,7 @@
45
45
  "@anthropic-ai/sdk": "^0.36.0",
46
46
  "@inquirer/prompts": "^7.0.0",
47
47
  "@thallylabs/core": "0.2.9",
48
- "@thallylabs/migrate": "0.2.18",
48
+ "@thallylabs/migrate": "0.2.19",
49
49
  "p-limit": "^6.1.0",
50
50
  "remark-mdx": "^3.1.1",
51
51
  "remark-parse": "^11.0.0",