create-thally-docs 0.10.62 → 0.10.64
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.
- package/README.md +6 -0
- package/dist/{chunk-NNT7JTC2.js → chunk-VDO6AJIV.js} +235 -34
- package/dist/index.js +1 -1
- package/dist/migrate/index.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -66,6 +66,12 @@ projected into Thally. A live Mintlify URL uses the structured configuration
|
|
|
66
66
|
embedded by Mintlify when it is available, with bounded same-site crawling as a
|
|
67
67
|
fallback.
|
|
68
68
|
|
|
69
|
+
Migrated OpenAPI specs are written to `openapi/`, not `public/` (which the host
|
|
70
|
+
serves as-is). `--into` never deletes files: an older copy of a spec already in
|
|
71
|
+
`public/` stays publicly downloadable (a `public/openapi.json` even answers
|
|
72
|
+
`/openapi.json` in place of the filtered spec), so the migration lists such files and
|
|
73
|
+
`thally check` warns about them until you delete them manually.
|
|
74
|
+
|
|
69
75
|
Repository-local JSX/TSX components and supported static dependencies are copied
|
|
70
76
|
into the customer-owned MDX registry. Simple interactive HTML blocks are moved
|
|
71
77
|
into client components, and locale/root routing is normalized. Unsupported
|
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
} from "./chunk-SLDENDOT.js";
|
|
9
9
|
|
|
10
10
|
// src/migrate/index.ts
|
|
11
|
-
import { existsSync as existsSync3, mkdirSync, mkdtempSync, readFileSync as
|
|
11
|
+
import { existsSync as existsSync3, mkdirSync, mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync } from "fs";
|
|
12
12
|
import { tmpdir } from "os";
|
|
13
|
-
import { dirname, isAbsolute, join as
|
|
13
|
+
import { basename, dirname, isAbsolute, join as join4, relative as relative3, resolve, sep } from "path";
|
|
14
14
|
import {
|
|
15
15
|
cloneGitHubRepository,
|
|
16
16
|
hydrateRemoteApiSpecs,
|
|
@@ -20,18 +20,98 @@ import {
|
|
|
20
20
|
renderMigrationFiles
|
|
21
21
|
} from "@thallylabs/migrate";
|
|
22
22
|
|
|
23
|
+
// src/public-specs.ts
|
|
24
|
+
import { lstatSync, readFileSync, readdirSync, statSync } from "fs";
|
|
25
|
+
import { join, relative } from "path";
|
|
26
|
+
import { parse as parseYaml } from "yaml";
|
|
27
|
+
var OPENAPI_METHODS = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
|
|
28
|
+
var SPEC_EXTENSIONS = /\.(?:json|ya?ml)$/i;
|
|
29
|
+
var MAX_SPEC_BYTES = 1e7;
|
|
30
|
+
var MAX_DEPTH = 8;
|
|
31
|
+
var MAX_ENTRIES = 5e3;
|
|
32
|
+
function shadowNote(urlPath) {
|
|
33
|
+
return urlPath === "openapi.json" || urlPath === "openapi.yaml" ? ` It is also served at /${urlPath} in place of the filtered specification, so visitors get this raw file there.` : "";
|
|
34
|
+
}
|
|
35
|
+
var isFlagged = (value) => value === true || value === "true";
|
|
36
|
+
function specHasHiddenOperations(spec, overrides) {
|
|
37
|
+
if (overrides && typeof overrides === "object" && Object.values(overrides).some((o) => o?.hidden === true)) return true;
|
|
38
|
+
const components = spec.components;
|
|
39
|
+
for (const group of [spec.paths, spec.webhooks, spec["x-webhooks"], components?.pathItems]) {
|
|
40
|
+
if (!group || typeof group !== "object") continue;
|
|
41
|
+
for (const item of Object.values(group)) {
|
|
42
|
+
if (!item || typeof item !== "object") continue;
|
|
43
|
+
const record = item;
|
|
44
|
+
if (isFlagged(record["x-excluded"]) || isFlagged(record["x-hidden"])) return true;
|
|
45
|
+
for (const method of OPENAPI_METHODS) {
|
|
46
|
+
const op = record[method];
|
|
47
|
+
if (op && typeof op === "object" && (isFlagged(op["x-excluded"]) || isFlagged(op["x-hidden"]))) return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
function findPublicSpecs(projectDir) {
|
|
54
|
+
const publicDir = join(projectDir, "public");
|
|
55
|
+
const scan = { specs: [], skipped: [] };
|
|
56
|
+
let seen = 0;
|
|
57
|
+
const walk = (dir, depth) => {
|
|
58
|
+
let entries;
|
|
59
|
+
try {
|
|
60
|
+
entries = readdirSync(dir).sort();
|
|
61
|
+
} catch {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
if (++seen > MAX_ENTRIES) return;
|
|
66
|
+
const full = join(dir, entry);
|
|
67
|
+
let stat;
|
|
68
|
+
try {
|
|
69
|
+
stat = lstatSync(full);
|
|
70
|
+
if (stat.isSymbolicLink()) {
|
|
71
|
+
const target = statSync(full);
|
|
72
|
+
if (!target.isFile()) continue;
|
|
73
|
+
stat = target;
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (stat.isDirectory()) {
|
|
79
|
+
if (depth < MAX_DEPTH && entry !== "node_modules") walk(full, depth + 1);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (!stat.isFile() || !SPEC_EXTENSIONS.test(entry)) continue;
|
|
83
|
+
const urlPath = relative(publicDir, full).split(/[\\/]/).join("/");
|
|
84
|
+
if (stat.size > MAX_SPEC_BYTES) {
|
|
85
|
+
scan.skipped.push(`public/${urlPath}`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const raw = readFileSync(full, "utf8");
|
|
90
|
+
const parsed = /\.json$/i.test(entry) ? JSON.parse(raw) : parseYaml(raw);
|
|
91
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
|
|
92
|
+
const spec = parsed;
|
|
93
|
+
if (typeof spec.openapi !== "string" && typeof spec.swagger !== "string") continue;
|
|
94
|
+
scan.specs.push({ path: `public/${urlPath}`, urlPath, hasHiddenOperations: specHasHiddenOperations(spec) });
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
walk(publicDir, 0);
|
|
100
|
+
return scan;
|
|
101
|
+
}
|
|
102
|
+
|
|
23
103
|
// src/migrate/validate.ts
|
|
24
104
|
import { spawnSync } from "child_process";
|
|
25
|
-
import { existsSync as existsSync2, readFileSync as
|
|
26
|
-
import { join as
|
|
105
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
106
|
+
import { join as join3 } from "path";
|
|
27
107
|
|
|
28
108
|
// src/check.ts
|
|
29
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
30
|
-
import { join, extname, relative } from "path";
|
|
109
|
+
import { existsSync, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
110
|
+
import { join as join2, extname, relative as relative2 } from "path";
|
|
31
111
|
import { execFileSync } from "child_process";
|
|
32
112
|
|
|
33
113
|
// src/frontmatter.ts
|
|
34
|
-
import { parse as
|
|
114
|
+
import { parse as parseYaml2 } from "yaml";
|
|
35
115
|
function parseFrontmatter(raw) {
|
|
36
116
|
const source = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
|
|
37
117
|
const opening = /^---([^\r\n]*)\r?\n/.exec(source);
|
|
@@ -46,7 +126,7 @@ function parseFrontmatter(raw) {
|
|
|
46
126
|
if (matter.trim() === "" || !["", "yaml", "yml"].includes(language)) {
|
|
47
127
|
return { content, data: {} };
|
|
48
128
|
}
|
|
49
|
-
const parsed =
|
|
129
|
+
const parsed = parseYaml2(matter);
|
|
50
130
|
return {
|
|
51
131
|
content,
|
|
52
132
|
data: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}
|
|
@@ -54,7 +134,57 @@ function parseFrontmatter(raw) {
|
|
|
54
134
|
}
|
|
55
135
|
|
|
56
136
|
// src/check.ts
|
|
57
|
-
import { parse as
|
|
137
|
+
import { parse as parseYaml3 } from "yaml";
|
|
138
|
+
|
|
139
|
+
// src/openapi-publication.ts
|
|
140
|
+
var METHODS = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
|
|
141
|
+
var isObj = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
142
|
+
var flagged = (value) => value === true || value === "true";
|
|
143
|
+
function parseDocOperation(raw) {
|
|
144
|
+
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;
|
|
149
|
+
}
|
|
150
|
+
function resolveLocalRef(document, ref) {
|
|
151
|
+
if (ref !== "#" && !ref.startsWith("#/")) return void 0;
|
|
152
|
+
let node = document;
|
|
153
|
+
for (const segment of ref.slice(2).split("/").filter(Boolean)) {
|
|
154
|
+
if (!isObj(node) && !Array.isArray(node)) return void 0;
|
|
155
|
+
let decoded = segment;
|
|
156
|
+
try {
|
|
157
|
+
decoded = decodeURIComponent(segment);
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
160
|
+
node = node[decoded.replace(/~1/g, "/").replace(/~0/g, "~")];
|
|
161
|
+
}
|
|
162
|
+
return node;
|
|
163
|
+
}
|
|
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";
|
|
169
|
+
const chain = [entry];
|
|
170
|
+
const seen = /* @__PURE__ */ new Set([entry]);
|
|
171
|
+
for (let current = entry; typeof current.$ref === "string"; ) {
|
|
172
|
+
const target = resolveLocalRef(document, current.$ref);
|
|
173
|
+
if (!isObj(target) || seen.has(target)) break;
|
|
174
|
+
seen.add(target);
|
|
175
|
+
chain.push(target);
|
|
176
|
+
current = target;
|
|
177
|
+
}
|
|
178
|
+
const item = Object.assign({}, ...[...chain].reverse());
|
|
179
|
+
const operation = item[verb];
|
|
180
|
+
if (!isObj(operation)) return "unknown";
|
|
181
|
+
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
|
+
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";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/check.ts
|
|
58
188
|
import { projectNavigationContract } from "@thallylabs/core/navigation";
|
|
59
189
|
import { slugify } from "@thallylabs/core/slugify";
|
|
60
190
|
function gitLocal(projectDir, args) {
|
|
@@ -111,14 +241,14 @@ function checkDrift(projectDir, file, data, issues) {
|
|
|
111
241
|
function scanMdx(dir, results) {
|
|
112
242
|
let entries;
|
|
113
243
|
try {
|
|
114
|
-
entries =
|
|
244
|
+
entries = readdirSync2(dir);
|
|
115
245
|
} catch {
|
|
116
246
|
return;
|
|
117
247
|
}
|
|
118
248
|
for (const entry of entries) {
|
|
119
|
-
const fullPath =
|
|
249
|
+
const fullPath = join2(dir, entry);
|
|
120
250
|
try {
|
|
121
|
-
const stat =
|
|
251
|
+
const stat = statSync2(fullPath);
|
|
122
252
|
if (stat.isDirectory()) scanMdx(fullPath, results);
|
|
123
253
|
else if (extname(entry).toLowerCase() === ".mdx") results.push(fullPath);
|
|
124
254
|
} catch {
|
|
@@ -294,21 +424,70 @@ function resolveLink(target, redirects, dynamicRedirects = []) {
|
|
|
294
424
|
current = resolveDynamicRedirectDestination(dynamicMatch.redirect, dynamicMatch.match);
|
|
295
425
|
}
|
|
296
426
|
}
|
|
297
|
-
function
|
|
298
|
-
const
|
|
427
|
+
function checkPublicSpecs(projectDir, tabs, issues) {
|
|
428
|
+
const configured = tabs.flatMap((tab) => tab.api?.source ? [tab.api.source.replace(/^\.\//, "")] : []);
|
|
429
|
+
const isConfigured = (spec) => configured.some((source) => source === `/${spec.urlPath}` || source === spec.path);
|
|
430
|
+
const hasElsewhereSource = configured.some((source) => !/^https?:/i.test(source));
|
|
431
|
+
const { specs, skipped } = findPublicSpecs(projectDir);
|
|
432
|
+
for (const spec of specs) {
|
|
433
|
+
if (isConfigured(spec)) continue;
|
|
434
|
+
if (!spec.hasHiddenOperations && !hasElsewhereSource) continue;
|
|
435
|
+
issues.push({
|
|
436
|
+
severity: "warning",
|
|
437
|
+
file: spec.path,
|
|
438
|
+
message: `${spec.path} is an OpenAPI file served publicly as /${spec.urlPath}${spec.hasHiddenOperations ? " and it contains x-excluded/x-hidden operations, which are downloadable from it" : " but is not the spec this site is configured with (an old copy?)"}.${shadowNote(spec.urlPath)} Re-running migration into an existing site does not remove old copies from public/: delete this file manually`
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
for (const path of skipped) {
|
|
442
|
+
issues.push({ severity: "warning", file: path, message: `${path} is too large to inspect; if it is an OpenAPI file with hidden operations it is served as-is from public/` });
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
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);
|
|
460
|
+
if (state === "hidden" || state === "excluded") {
|
|
461
|
+
issues.push({
|
|
462
|
+
severity: "warning",
|
|
463
|
+
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`
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function validateOpenApi(projectDir, source, issues, overrides) {
|
|
470
|
+
const specPath = source.startsWith("/") ? join2(projectDir, "public", source.slice(1)) : join2(projectDir, source);
|
|
299
471
|
if (!existsSync(specPath)) {
|
|
300
472
|
issues.push({ severity: "error", message: `API reference points at "${source}" but the file does not exist`, file: source });
|
|
301
473
|
return;
|
|
302
474
|
}
|
|
303
475
|
let spec;
|
|
304
476
|
try {
|
|
305
|
-
const raw =
|
|
306
|
-
spec = source.endsWith(".json") ? JSON.parse(raw) :
|
|
477
|
+
const raw = readFileSync2(specPath, "utf8");
|
|
478
|
+
spec = source.endsWith(".json") ? JSON.parse(raw) : parseYaml3(raw);
|
|
307
479
|
} catch (err) {
|
|
308
480
|
issues.push({ severity: "error", message: `OpenAPI spec is not valid ${source.endsWith(".json") ? "JSON" : "YAML"}: ${err.message}`, file: source });
|
|
309
481
|
return;
|
|
310
482
|
}
|
|
311
483
|
const s = spec;
|
|
484
|
+
if (s && typeof s === "object" && (source.startsWith("/") || source.replace(/^\.\//, "").startsWith("public/")) && specHasHiddenOperations(s, overrides)) {
|
|
485
|
+
issues.push({
|
|
486
|
+
severity: "warning",
|
|
487
|
+
message: `OpenAPI spec "${source}" lives under public/ and contains x-excluded/x-hidden operations; public/ is served as-is, so those operations would be downloadable.${shadowNote(source.replace(/^\.?\/+/, "").replace(/^public\//, ""))} Move it to a project-root path such as "openapi/${source.split("/").pop()}", update docs.json, and delete the copy in public/ (re-running migration into an existing site does not remove old copies from public/)`,
|
|
488
|
+
file: source
|
|
489
|
+
});
|
|
490
|
+
}
|
|
312
491
|
if (typeof s?.openapi !== "string" && typeof s?.swagger !== "string") {
|
|
313
492
|
issues.push({ severity: "error", message: 'OpenAPI spec is missing the "openapi" (or "swagger") version field', file: source });
|
|
314
493
|
}
|
|
@@ -326,7 +505,7 @@ function validateOpenApi(projectDir, source, issues) {
|
|
|
326
505
|
continue;
|
|
327
506
|
}
|
|
328
507
|
const hasOp = Object.keys(ops).some((k) => methods.has(k.toLowerCase()));
|
|
329
|
-
if (!hasOp) {
|
|
508
|
+
if (!hasOp && typeof ops.$ref !== "string") {
|
|
330
509
|
issues.push({ severity: "warning", message: `OpenAPI path "${p}" has no operations`, file: source });
|
|
331
510
|
}
|
|
332
511
|
}
|
|
@@ -334,13 +513,13 @@ function validateOpenApi(projectDir, source, issues) {
|
|
|
334
513
|
}
|
|
335
514
|
async function runCheck(projectDir, options) {
|
|
336
515
|
const { fix, ci } = options;
|
|
337
|
-
if (!existsSync(
|
|
516
|
+
if (!existsSync(join2(projectDir, "docs.json"))) {
|
|
338
517
|
console.error(`
|
|
339
518
|
\u274C Not a Thally project: docs.json not found in ${projectDir}
|
|
340
519
|
`);
|
|
341
520
|
return 1;
|
|
342
521
|
}
|
|
343
|
-
const contentDir =
|
|
522
|
+
const contentDir = join2(projectDir, "src", "content");
|
|
344
523
|
const issues = [];
|
|
345
524
|
const config = readDocsJson(projectDir);
|
|
346
525
|
const secondaryLocales = new Set(
|
|
@@ -369,7 +548,7 @@ async function runCheck(projectDir, options) {
|
|
|
369
548
|
issues.push({ severity: "warning", message: `[duplicate] "${dup}" appears more than once in docs.json` });
|
|
370
549
|
}
|
|
371
550
|
for (const pageId of navPageIds) {
|
|
372
|
-
const candidates = [
|
|
551
|
+
const candidates = [join2(contentDir, `${pageId}.mdx`), join2(contentDir, `${pageId}/index.mdx`)];
|
|
373
552
|
if (!candidates.some((c) => existsSync(c))) {
|
|
374
553
|
issues.push({ severity: "error", message: `"${pageId}" is in docs.json but has no MDX file`, file: `src/content/${pageId}.mdx` });
|
|
375
554
|
}
|
|
@@ -377,6 +556,7 @@ async function runCheck(projectDir, options) {
|
|
|
377
556
|
const allFiles = [];
|
|
378
557
|
if (existsSync(contentDir)) scanMdx(contentDir, allFiles);
|
|
379
558
|
const fixedOrphans = [];
|
|
559
|
+
const operationPages = [];
|
|
380
560
|
const validPaths = /* @__PURE__ */ new Set(["/"]);
|
|
381
561
|
const anchorsByPath = /* @__PURE__ */ new Map();
|
|
382
562
|
const linksByFile = [];
|
|
@@ -387,13 +567,13 @@ async function runCheck(projectDir, options) {
|
|
|
387
567
|
let content = "";
|
|
388
568
|
let lineOffset = 0;
|
|
389
569
|
try {
|
|
390
|
-
const raw =
|
|
570
|
+
const raw = readFileSync2(filePath, "utf8");
|
|
391
571
|
const parsed = parseFrontmatter(raw);
|
|
392
572
|
data = parsed.data;
|
|
393
573
|
content = parsed.content;
|
|
394
574
|
lineOffset = raw.slice(0, raw.indexOf(content)).split("\n").length - 1;
|
|
395
575
|
} catch {
|
|
396
|
-
issues.push({ severity: "error", message: `Could not parse frontmatter`, file:
|
|
576
|
+
issues.push({ severity: "error", message: `Could not parse frontmatter`, file: relative2(projectDir, filePath) });
|
|
397
577
|
continue;
|
|
398
578
|
}
|
|
399
579
|
const { navPageId } = localizedPage(pageId, secondaryLocales);
|
|
@@ -402,15 +582,17 @@ async function runCheck(projectDir, options) {
|
|
|
402
582
|
addOrphanToNav(projectDir, pageId);
|
|
403
583
|
fixedOrphans.push(pageId);
|
|
404
584
|
} else {
|
|
405
|
-
issues.push({ severity: "warning", message: `"${pageId}" is not in docs.json nav (orphan)`, file:
|
|
585
|
+
issues.push({ severity: "warning", message: `"${pageId}" is not in docs.json nav (orphan)`, file: relative2(projectDir, filePath) });
|
|
406
586
|
}
|
|
407
587
|
}
|
|
408
|
-
const rel2 =
|
|
588
|
+
const rel2 = relative2(projectDir, filePath);
|
|
409
589
|
if (!data.title) issues.push({ severity: "warning", message: `Missing "title" in frontmatter`, file: rel2 });
|
|
410
590
|
if (!data.description) issues.push({ severity: "warning", message: `Missing "description" in frontmatter`, file: rel2 });
|
|
411
591
|
if (typeof data.openapi !== "string" && content.trim().length < 50) {
|
|
412
592
|
issues.push({ severity: "warning", message: `Very short body (${content.trim().length} chars) \u2014 page may be empty`, file: rel2 });
|
|
413
593
|
}
|
|
594
|
+
const operation = parseDocOperation(data.openapi);
|
|
595
|
+
if (operation) operationPages.push({ file: rel2, ...operation });
|
|
414
596
|
if (options.drift) checkDrift(projectDir, rel2, data, issues);
|
|
415
597
|
const path = pageIdToPath(pageId, secondaryLocales);
|
|
416
598
|
const anchors = extractHeadingAnchors(content);
|
|
@@ -454,7 +636,7 @@ async function runCheck(projectDir, options) {
|
|
|
454
636
|
}
|
|
455
637
|
}
|
|
456
638
|
}
|
|
457
|
-
const publicDir =
|
|
639
|
+
const publicDir = join2(projectDir, "public");
|
|
458
640
|
for (const { file, images, offset } of linksByFile) {
|
|
459
641
|
for (const { target, line: contentLine } of images) {
|
|
460
642
|
const line = contentLine + offset;
|
|
@@ -467,14 +649,16 @@ async function runCheck(projectDir, options) {
|
|
|
467
649
|
} catch {
|
|
468
650
|
continue;
|
|
469
651
|
}
|
|
470
|
-
if (!existsSync(
|
|
652
|
+
if (!existsSync(join2(publicDir, decoded))) {
|
|
471
653
|
issues.push({ severity: "warning", message: `Image not found: "${target}" has no file under public/`, file, line });
|
|
472
654
|
}
|
|
473
655
|
}
|
|
474
656
|
}
|
|
475
657
|
for (const tab of config.tabs) {
|
|
476
|
-
if (tab.api?.source) validateOpenApi(projectDir, tab.api.source, issues);
|
|
658
|
+
if (tab.api?.source) validateOpenApi(projectDir, tab.api.source, issues, tab.api.overrides);
|
|
477
659
|
}
|
|
660
|
+
checkPublicSpecs(projectDir, config.tabs, issues);
|
|
661
|
+
checkUnpublishedOperationPages(projectDir, config.tabs, operationPages, issues);
|
|
478
662
|
const errors = issues.filter((i) => i.severity === "error");
|
|
479
663
|
options.onIssues?.(issues);
|
|
480
664
|
const warnings = issues.filter((i) => i.severity === "warning");
|
|
@@ -544,9 +728,9 @@ async function validateMigration(projectDir, skip = false, installationFailed =
|
|
|
544
728
|
result.messages.push("Dependency installation failed; production build was not attempted. Review the installation output before retrying.");
|
|
545
729
|
return result;
|
|
546
730
|
}
|
|
547
|
-
const packagePath =
|
|
731
|
+
const packagePath = join3(projectDir, "package.json");
|
|
548
732
|
try {
|
|
549
|
-
const manifest = existsSync2(packagePath) ? JSON.parse(
|
|
733
|
+
const manifest = existsSync2(packagePath) ? JSON.parse(readFileSync3(packagePath, "utf8")) : void 0;
|
|
550
734
|
if (!manifest?.scripts?.build) {
|
|
551
735
|
result.messages.push("No build script is available; production rendering is not verified.");
|
|
552
736
|
return result;
|
|
@@ -568,7 +752,7 @@ async function validateMigration(projectDir, skip = false, installationFailed =
|
|
|
568
752
|
// src/migrate/index.ts
|
|
569
753
|
function projectPath(projectDir, candidate) {
|
|
570
754
|
const target = resolve(projectDir, candidate);
|
|
571
|
-
const fromRoot =
|
|
755
|
+
const fromRoot = relative3(resolve(projectDir), target);
|
|
572
756
|
if (isAbsolute(fromRoot) || fromRoot === ".." || fromRoot.startsWith(`..${sep}`)) {
|
|
573
757
|
throw new Error(`Generated migration path escapes the project: ${candidate}`);
|
|
574
758
|
}
|
|
@@ -577,7 +761,7 @@ function projectPath(projectDir, candidate) {
|
|
|
577
761
|
function readExistingConfig(projectDir) {
|
|
578
762
|
const configPath = projectPath(projectDir, "docs.json");
|
|
579
763
|
if (!existsSync3(configPath)) return void 0;
|
|
580
|
-
return JSON.parse(
|
|
764
|
+
return JSON.parse(readFileSync4(configPath, "utf8"));
|
|
581
765
|
}
|
|
582
766
|
function resetFreshMigrationContent(projectDir) {
|
|
583
767
|
const contentDirectory = projectPath(projectDir, "src/content");
|
|
@@ -600,8 +784,8 @@ async function discoverMigration(options) {
|
|
|
600
784
|
}
|
|
601
785
|
const source = parseGitHubRepositoryUrl(options.sourceUrl);
|
|
602
786
|
if (options.branch) source.branch = options.branch;
|
|
603
|
-
const temporaryRoot = mkdtempSync(
|
|
604
|
-
const cloneDir =
|
|
787
|
+
const temporaryRoot = mkdtempSync(join4(tmpdir(), "thally-migrate-"));
|
|
788
|
+
const cloneDir = join4(temporaryRoot, "repository");
|
|
605
789
|
console.log(` \u{1F4E6} Cloning ${source.owner}/${source.repo}...`);
|
|
606
790
|
try {
|
|
607
791
|
const cloneWarnings = [];
|
|
@@ -618,6 +802,19 @@ async function discoverMigration(options) {
|
|
|
618
802
|
rmSync(temporaryRoot, { recursive: true, force: true });
|
|
619
803
|
}
|
|
620
804
|
}
|
|
805
|
+
function publicSpecWarning(projectDir, bundle) {
|
|
806
|
+
const { specs, skipped } = findPublicSpecs(projectDir);
|
|
807
|
+
if (specs.length === 0 && skipped.length === 0) return null;
|
|
808
|
+
const migrated = new Set(bundle.assets.filter((asset) => asset.projectRelative).map((asset) => basename(asset.path)));
|
|
809
|
+
const lines = [
|
|
810
|
+
...specs.map((spec) => `${spec.path}${migrated.has(basename(spec.path)) ? " (same file name as a migrated spec)" : ""}${spec.hasHiddenOperations ? " (contains x-excluded/x-hidden operations)" : ""}${shadowNote(spec.urlPath) ? ` (answers /${spec.urlPath} in place of the filtered specification)` : ""}`),
|
|
811
|
+
...skipped.map((path) => `${path} (too large to inspect)`)
|
|
812
|
+
];
|
|
813
|
+
return {
|
|
814
|
+
code: "unsupported-config",
|
|
815
|
+
message: `Existing OpenAPI files under public/ are served publicly as-is: ${lines.join("; ")}. Migrated specs are written to openapi/, and re-running migration does not remove old copies from public/. Delete any old copy manually (nothing was deleted), or its hidden operations stay downloadable.`
|
|
816
|
+
};
|
|
817
|
+
}
|
|
621
818
|
async function migrateDocs(options) {
|
|
622
819
|
const projectDir = resolve(options.projectDir);
|
|
623
820
|
const bundle = await discoverMigration(options);
|
|
@@ -647,9 +844,13 @@ async function migrateDocs(options) {
|
|
|
647
844
|
}
|
|
648
845
|
bundle.docsConfig.i18n ??= { defaultLocale: "en", locales: [{ code: "en", label: "English" }] };
|
|
649
846
|
}
|
|
847
|
+
if (options.into) {
|
|
848
|
+
const stale = publicSpecWarning(projectDir, bundle);
|
|
849
|
+
if (stale) bundle.warnings.unshift(stale);
|
|
850
|
+
}
|
|
650
851
|
const rendered = renderMigrationFiles(bundle, {
|
|
651
852
|
existingConfig: options.into ? readExistingConfig(projectDir) : void 0,
|
|
652
|
-
existingComponentRegistry: existsSync3(projectPath(projectDir, "src/mdx/custom-components.tsx")) ?
|
|
853
|
+
existingComponentRegistry: existsSync3(projectPath(projectDir, "src/mdx/custom-components.tsx")) ? readFileSync4(projectPath(projectDir, "src/mdx/custom-components.tsx"), "utf8") : void 0
|
|
653
854
|
});
|
|
654
855
|
for (const file of rendered) {
|
|
655
856
|
const destination = projectPath(projectDir, file.path);
|
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.64",
|
|
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.14",
|
|
49
49
|
"p-limit": "^6.1.0",
|
|
50
50
|
"tar": "^7.5.22",
|
|
51
51
|
"yaml": "^2.6.0"
|