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
|
|
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
|
|
146
|
-
if (
|
|
147
|
-
const
|
|
148
|
-
|
|
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
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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" : "
|
|
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
|
-
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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 ${
|
|
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
|
|
599
|
-
if (
|
|
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
package/dist/migrate/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-thally-docs",
|
|
3
|
-
"version": "0.10.
|
|
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.
|
|
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",
|