create-thally-docs 0.10.68 → 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,18 +199,148 @@ 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
188
341
  import { projectNavigationContract } from "@thallylabs/core/navigation";
189
342
  import { slugify } from "@thallylabs/core/slugify";
343
+ import { validateIntegrations } from "@thallylabs/migrate";
190
344
  function gitLocal(projectDir, args) {
191
345
  try {
192
346
  const out = execFileSync("git", args, { cwd: projectDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -443,25 +597,28 @@ function checkPublicSpecs(projectDir, tabs, issues) {
443
597
  }
444
598
  }
445
599
  function checkUnpublishedOperationPages(projectDir, tabs, pages, issues) {
446
- const api = tabs.find((tab) => !tab.hidden && tab.api?.source)?.api;
447
- const source = api?.source;
448
- if (!source || pages.length === 0 || /^https?:/i.test(source)) return;
449
- const specPath = source.startsWith("/") ? join2(projectDir, "public", source.slice(1)) : join2(projectDir, source);
450
- let spec;
451
- try {
452
- const raw = readFileSync2(specPath, "utf8");
453
- spec = /\.json$/i.test(source) ? JSON.parse(raw) : parseYaml3(raw);
454
- } catch {
455
- return;
456
- }
457
- const overrides = api.overrides;
458
- for (const { file, method, path } of pages) {
459
- 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);
460
616
  if (state === "hidden" || state === "excluded") {
617
+ const operation = `${reference.specRef ? `${reference.specRef} ` : ""}${reference.method} ${reference.path}`;
461
618
  issues.push({
462
619
  severity: "warning",
463
620
  file,
464
- 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`
465
622
  });
466
623
  }
467
624
  }
@@ -539,6 +696,9 @@ async function runCheck(projectDir, options) {
539
696
  const compiled = compileDynamicRedirectSource(redirect.source, redirect.destination);
540
697
  return compiled ? [compiled] : [];
541
698
  });
699
+ for (const message of validateIntegrations(config.integrations)) {
700
+ issues.push({ severity: "warning", message, file: "docs.json" });
701
+ }
542
702
  const navigation = projectNavigationContract(config);
543
703
  const navPageIds = new Set(navigation.authoredPageIds);
544
704
  for (const tab of navigation.emptyTabs) {
@@ -591,8 +751,11 @@ async function runCheck(projectDir, options) {
591
751
  if (typeof data.openapi !== "string" && content.trim().length < 50) {
592
752
  issues.push({ severity: "warning", message: `Very short body (${content.trim().length} chars) \u2014 page may be empty`, file: rel2 });
593
753
  }
594
- const operation = parseDocOperation(data.openapi);
595
- 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
+ }
596
759
  if (options.drift) checkDrift(projectDir, rel2, data, issues);
597
760
  const path = pageIdToPath(pageId, secondaryLocales);
598
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-QNTHCVNM.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-QNTHCVNM.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.68",
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.17",
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",