@thallylabs/migrate 0.2.32 → 0.2.33
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 +54 -8
- package/dist/index.d.ts +13 -5
- package/dist/index.js +399 -131
- package/dist/index.js.map +1 -1
- package/dist/{url-migration-Coy62q1L.d.ts → url-migration-_wVauqSg.d.ts} +19 -0
- package/dist/url-migration.d.ts +1 -1
- package/dist/url-migration.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2393,9 +2393,9 @@ function escapeOrphanCapitalizedTags(body) {
|
|
|
2393
2393
|
continue;
|
|
2394
2394
|
}
|
|
2395
2395
|
const start = match.index;
|
|
2396
|
-
const
|
|
2397
|
-
|
|
2398
|
-
opens.set(name,
|
|
2396
|
+
const list2 = opens.get(name) ?? [];
|
|
2397
|
+
list2.push({ start, end: start + full.length });
|
|
2398
|
+
opens.set(name, list2);
|
|
2399
2399
|
}
|
|
2400
2400
|
const edits = [];
|
|
2401
2401
|
for (const [name, ranges] of opens) {
|
|
@@ -4238,24 +4238,25 @@ import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as rea
|
|
|
4238
4238
|
import { dirname as dirname2, extname as extname3, relative as relative3 } from "path";
|
|
4239
4239
|
var MAX_MINTLIFY_CONFIG_BYTES = 2e7;
|
|
4240
4240
|
function addMintlifyDirectoryRedirects(config, pages) {
|
|
4241
|
-
const pageIds = new Set(pages.map((page) => page.id));
|
|
4241
|
+
const pageIds = new Set(pages.map((page) => page.id.toLowerCase()));
|
|
4242
4242
|
const redirects = [...config.redirects ?? []];
|
|
4243
|
-
const redirectSources = new Set(redirects.map((redirect) => redirect.source.replace(/\/$/, "") || "/"));
|
|
4243
|
+
const redirectSources = new Set(redirects.map((redirect) => (redirect.source.replace(/\/$/, "") || "/").toLowerCase()));
|
|
4244
4244
|
function hasAuthoredRedirect(source) {
|
|
4245
|
-
|
|
4245
|
+
const route = source.toLowerCase();
|
|
4246
|
+
if (redirectSources.has(route)) return true;
|
|
4246
4247
|
return (config.redirects ?? []).some((redirect) => {
|
|
4247
|
-
const match = redirect.source.match(/^(.*)\/:[
|
|
4248
|
-
return match ?
|
|
4248
|
+
const match = redirect.source.toLowerCase().match(/^(.*)\/:[a-z_][a-z0-9_]*\*\/?$/);
|
|
4249
|
+
return match ? route === match[1] || route.startsWith(`${match[1]}/`) : false;
|
|
4249
4250
|
});
|
|
4250
4251
|
}
|
|
4251
4252
|
for (const page of pages) {
|
|
4252
4253
|
if (!/\/(?:overview|introduction)$/.test(page.navigationId)) continue;
|
|
4253
4254
|
const parent = page.id.replace(/\/(?:overview|introduction)$/, "");
|
|
4254
|
-
if (!parent || pageIds.has(parent)) continue;
|
|
4255
|
+
if (!parent || pageIds.has(parent.toLowerCase())) continue;
|
|
4255
4256
|
const source = `/${parent}`;
|
|
4256
4257
|
if (hasAuthoredRedirect(source)) continue;
|
|
4257
4258
|
redirects.push({ source, destination: `/${page.id}`, permanent: false });
|
|
4258
|
-
redirectSources.add(source);
|
|
4259
|
+
redirectSources.add(source.toLowerCase());
|
|
4259
4260
|
}
|
|
4260
4261
|
const navigationOrder = [];
|
|
4261
4262
|
function collectPages(nodes) {
|
|
@@ -4285,11 +4286,11 @@ function addMintlifyDirectoryRedirects(config, pages) {
|
|
|
4285
4286
|
}
|
|
4286
4287
|
}
|
|
4287
4288
|
for (const [directory, landing] of firstDescendantByDirectory) {
|
|
4288
|
-
if (pageIds.has(directory)) continue;
|
|
4289
|
+
if (pageIds.has(directory.toLowerCase())) continue;
|
|
4289
4290
|
const source = `/${directory}`;
|
|
4290
4291
|
if (hasAuthoredRedirect(source)) continue;
|
|
4291
4292
|
redirects.push({ source, destination: `/${landing.id}`, permanent: false });
|
|
4292
|
-
redirectSources.add(source);
|
|
4293
|
+
redirectSources.add(source.toLowerCase());
|
|
4293
4294
|
}
|
|
4294
4295
|
return redirects.length > 0 ? { ...config, redirects } : config;
|
|
4295
4296
|
}
|
|
@@ -4308,11 +4309,11 @@ function addMintlifyHomepageRedirects(config, pages) {
|
|
|
4308
4309
|
const homepage = config.tabs.filter((tab) => !tab.hidden).map((tab) => firstPage([...tab.pages ?? [], ...tab.groups ?? []])).find(Boolean);
|
|
4309
4310
|
if (!homepage) return config;
|
|
4310
4311
|
const redirects = [...config.redirects ?? []];
|
|
4311
|
-
const sources = new Set(redirects.map((redirect) => redirect.source.replace(/\/$/, "") || "/"));
|
|
4312
|
+
const sources = new Set(redirects.map((redirect) => (redirect.source.replace(/\/$/, "") || "/").toLowerCase()));
|
|
4312
4313
|
const roots = ["", ...(config.i18n?.locales ?? []).filter((locale) => locale.code !== config.i18n?.defaultLocale).filter((locale) => homepage !== "introduction" || pages.some((page) => page.locale === locale.code && page.navigationId === homepage)).map((locale) => locale.code)];
|
|
4313
4314
|
for (const locale of roots) {
|
|
4314
4315
|
const source = locale ? `/${locale}` : "/";
|
|
4315
|
-
if (sources.has(source)) continue;
|
|
4316
|
+
if (sources.has(source.toLowerCase())) continue;
|
|
4316
4317
|
redirects.push({ source, destination: `${locale ? `/${locale}` : ""}/${homepage}`, permanent: true });
|
|
4317
4318
|
}
|
|
4318
4319
|
return { ...config, redirects };
|
|
@@ -4439,6 +4440,22 @@ function specSource(value) {
|
|
|
4439
4440
|
const source = typeof value === "string" ? value : objectValue2(value)?.source;
|
|
4440
4441
|
return typeof source === "string" && source.trim() ? source.trim() : void 0;
|
|
4441
4442
|
}
|
|
4443
|
+
function listedOperations(container) {
|
|
4444
|
+
const operations = [];
|
|
4445
|
+
const visit = (entry, root) => {
|
|
4446
|
+
if (typeof entry === "string") {
|
|
4447
|
+
if (OPENAPI_OPERATION_REF.test(entry)) operations.push(entry.trim());
|
|
4448
|
+
return;
|
|
4449
|
+
}
|
|
4450
|
+
const object2 = objectValue2(entry);
|
|
4451
|
+
if (!object2 || !root && (specSource(object2.openapi) || specSource(object2.asyncapi))) return;
|
|
4452
|
+
for (const key of ["groups", "pages"]) {
|
|
4453
|
+
if (Array.isArray(object2[key])) object2[key].forEach((child) => visit(child, false));
|
|
4454
|
+
}
|
|
4455
|
+
};
|
|
4456
|
+
visit(container, true);
|
|
4457
|
+
return operations;
|
|
4458
|
+
}
|
|
4442
4459
|
var hasNavigationChildren = (node) => ["groups", "pages"].some((key) => Array.isArray(node[key]) && node[key].length > 0);
|
|
4443
4460
|
function mintlifyNavigationApiReferences(config) {
|
|
4444
4461
|
const navigation = objectValue2(config.navigation) ?? config;
|
|
@@ -4450,12 +4467,14 @@ function mintlifyNavigationApiReferences(config) {
|
|
|
4450
4467
|
const value = specSource(node[kind]);
|
|
4451
4468
|
const directory = objectValue2(node[kind])?.directory;
|
|
4452
4469
|
if (value) {
|
|
4470
|
+
const operations = typeof directory === "string" && directory.trim() ? listedOperations(node) : [];
|
|
4453
4471
|
references.push({
|
|
4454
4472
|
value,
|
|
4455
4473
|
kind,
|
|
4456
4474
|
tabLabel,
|
|
4457
4475
|
...sibling,
|
|
4458
|
-
...typeof directory === "string" && directory.trim() ? { directory: directory.trim() } : {}
|
|
4476
|
+
...typeof directory === "string" && directory.trim() ? { directory: directory.trim() } : {},
|
|
4477
|
+
...operations.length > 0 ? { operations } : {}
|
|
4459
4478
|
});
|
|
4460
4479
|
}
|
|
4461
4480
|
}
|
|
@@ -7206,16 +7225,32 @@ function withheldOperationKeys(spec, withheldRefs, keptRefs) {
|
|
|
7206
7225
|
const kept = keysFor(spec, keptRefs);
|
|
7207
7226
|
return { withheld: keysFor(spec, withheldRefs).filter((key) => !kept.includes(key)), kept };
|
|
7208
7227
|
}
|
|
7228
|
+
function withoutByteOrderMark(content) {
|
|
7229
|
+
return content.length >= 3 && content[0] === 239 && content[1] === 187 && content[2] === 191 ? content.subarray(3) : content;
|
|
7230
|
+
}
|
|
7231
|
+
function parseSpecDocument(text3) {
|
|
7232
|
+
const source = text3.replace(/^\uFEFF/, "");
|
|
7233
|
+
try {
|
|
7234
|
+
return { format: "json", document: JSON.parse(source) };
|
|
7235
|
+
} catch (jsonError) {
|
|
7236
|
+
try {
|
|
7237
|
+
return { format: "yaml", document: parseYaml4(source) };
|
|
7238
|
+
} catch (yamlError) {
|
|
7239
|
+
throw source.trimStart().startsWith("{") ? jsonError : yamlError;
|
|
7240
|
+
}
|
|
7241
|
+
}
|
|
7242
|
+
}
|
|
7209
7243
|
function markExcluded(content, operationKeys, keptKeys = []) {
|
|
7210
7244
|
const untouched = { content, excluded: [] };
|
|
7211
7245
|
if (operationKeys.length === 0) return untouched;
|
|
7212
7246
|
const text3 = content.toString("utf8");
|
|
7213
|
-
let
|
|
7247
|
+
let parsed;
|
|
7214
7248
|
try {
|
|
7215
|
-
|
|
7249
|
+
parsed = parseSpecDocument(text3);
|
|
7216
7250
|
} catch {
|
|
7217
7251
|
return untouched;
|
|
7218
7252
|
}
|
|
7253
|
+
const doc = parsed.document;
|
|
7219
7254
|
if (!doc || typeof doc !== "object") return untouched;
|
|
7220
7255
|
const excluded = [];
|
|
7221
7256
|
for (const key of operationKeys) {
|
|
@@ -7238,7 +7273,7 @@ function markExcluded(content, operationKeys, keptKeys = []) {
|
|
|
7238
7273
|
}
|
|
7239
7274
|
}
|
|
7240
7275
|
if (excluded.length === 0) return untouched;
|
|
7241
|
-
const out =
|
|
7276
|
+
const out = parsed.format === "json" ? `${JSON.stringify(doc, null, 2)}
|
|
7242
7277
|
` : stringifyYaml(doc);
|
|
7243
7278
|
return { content: Buffer.from(out), excluded };
|
|
7244
7279
|
}
|
|
@@ -7469,6 +7504,150 @@ function migrationImageAttribute(value) {
|
|
|
7469
7504
|
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">").replace(/\{/g, "{").replace(/\}/g, "}").replace(/\r/g, " ").replace(/\n/g, " ");
|
|
7470
7505
|
}
|
|
7471
7506
|
|
|
7507
|
+
// src/redirects.ts
|
|
7508
|
+
var LISTED = 10;
|
|
7509
|
+
function exactRoute(route) {
|
|
7510
|
+
return route.startsWith("/") && !/[?:*#()[\]{}]/.test(route) ? route.replace(/\/+$/, "") || "/" : null;
|
|
7511
|
+
}
|
|
7512
|
+
function literalRoute(route) {
|
|
7513
|
+
return exactRoute(route)?.toLowerCase() ?? null;
|
|
7514
|
+
}
|
|
7515
|
+
function resolveDotSegments(route) {
|
|
7516
|
+
const segments = [];
|
|
7517
|
+
for (const segment of route.split("/").slice(1)) {
|
|
7518
|
+
const dots = segment.replace(/%2e/gi, ".");
|
|
7519
|
+
if (dots === "..") segments.pop();
|
|
7520
|
+
else if (dots !== ".") segments.push(segment);
|
|
7521
|
+
}
|
|
7522
|
+
return `/${segments.join("/")}`;
|
|
7523
|
+
}
|
|
7524
|
+
function destinationRoute(destination) {
|
|
7525
|
+
const route = destination.startsWith("//") ? null : exactRoute(destination.split(/[?#]/, 1)[0]);
|
|
7526
|
+
return route === null ? null : resolveDotSegments(route);
|
|
7527
|
+
}
|
|
7528
|
+
function pageRoutes(pages) {
|
|
7529
|
+
return pages.flatMap((page) => [`/${page.id}`, `/${page.id.replace(/\/index$/, "")}`]);
|
|
7530
|
+
}
|
|
7531
|
+
function servedRoutes(pages) {
|
|
7532
|
+
return new Set(pageRoutes(pages).map((route) => route.toLowerCase()));
|
|
7533
|
+
}
|
|
7534
|
+
function list(sources) {
|
|
7535
|
+
return `${sources.slice(0, LISTED).join(", ")}${sources.length > LISTED ? ", ..." : ""}`;
|
|
7536
|
+
}
|
|
7537
|
+
var ROUTE_PATTERN_OPERATORS = /[+?()[\]{}]/;
|
|
7538
|
+
function dropUnroutableRedirects(config, warnings) {
|
|
7539
|
+
if (!config.redirects?.length) return config;
|
|
7540
|
+
const unmatchable = [];
|
|
7541
|
+
const redirects = config.redirects.filter((redirect) => {
|
|
7542
|
+
const source = exactRoute(redirect.source);
|
|
7543
|
+
if (source && source === exactRoute(redirect.destination)) {
|
|
7544
|
+
warnings.push({ code: "unsupported-config", message: `Self-redirect from ${redirect.source} to ${redirect.destination} was omitted.` });
|
|
7545
|
+
return false;
|
|
7546
|
+
}
|
|
7547
|
+
if (!ROUTE_PATTERN_OPERATORS.test(redirect.source)) return true;
|
|
7548
|
+
unmatchable.push(redirect.source);
|
|
7549
|
+
return false;
|
|
7550
|
+
});
|
|
7551
|
+
if (unmatchable.length > 0) {
|
|
7552
|
+
warnings.push({
|
|
7553
|
+
code: "unsupported-config",
|
|
7554
|
+
message: `Skipped ${unmatchable.length} redirect${unmatchable.length === 1 ? "" : "s"} from ${list(unmatchable)}: ${unmatchable.length === 1 ? "that address has" : "those addresses have"} a character (+ ? ( ) [ ] { }) that Next.js reads as route syntax and cannot match literally, so visits to ${unmatchable.length === 1 ? "it" : "them"} will not redirect.`
|
|
7555
|
+
});
|
|
7556
|
+
}
|
|
7557
|
+
if (redirects.length === config.redirects.length) return config;
|
|
7558
|
+
const result = { ...config, redirects };
|
|
7559
|
+
if (redirects.length === 0) delete result.redirects;
|
|
7560
|
+
return result;
|
|
7561
|
+
}
|
|
7562
|
+
function redirectChains() {
|
|
7563
|
+
const kept = /* @__PURE__ */ new Map();
|
|
7564
|
+
const terminal = /* @__PURE__ */ new Set();
|
|
7565
|
+
const has = (source) => kept.has(source) || terminal.has(source);
|
|
7566
|
+
const add = (source, destination) => {
|
|
7567
|
+
if (destination === null) terminal.add(source);
|
|
7568
|
+
else kept.set(source, destination);
|
|
7569
|
+
};
|
|
7570
|
+
const closesLoop = (source, destination) => {
|
|
7571
|
+
let end = destination;
|
|
7572
|
+
for (let steps = 0; kept.has(end); steps++) {
|
|
7573
|
+
if (steps >= kept.size) return true;
|
|
7574
|
+
end = kept.get(end);
|
|
7575
|
+
}
|
|
7576
|
+
for (let step = destination; step !== end; ) {
|
|
7577
|
+
const next = kept.get(step);
|
|
7578
|
+
kept.set(step, end);
|
|
7579
|
+
step = next;
|
|
7580
|
+
}
|
|
7581
|
+
return end === source;
|
|
7582
|
+
};
|
|
7583
|
+
return { has, add, closesLoop };
|
|
7584
|
+
}
|
|
7585
|
+
function dropConflictingRedirects(config, pages, warnings) {
|
|
7586
|
+
if (!config.redirects?.length) return config;
|
|
7587
|
+
const served = servedRoutes(pages);
|
|
7588
|
+
const servedAsWritten = new Set(pageRoutes(pages));
|
|
7589
|
+
const hidden = [];
|
|
7590
|
+
const hiddenByCase = [];
|
|
7591
|
+
const looping = [];
|
|
7592
|
+
const loopingByCase = [];
|
|
7593
|
+
const folded = redirectChains();
|
|
7594
|
+
const asWritten = redirectChains();
|
|
7595
|
+
const redirects = config.redirects.filter((redirect) => {
|
|
7596
|
+
const source = literalRoute(redirect.source);
|
|
7597
|
+
if (!source) return true;
|
|
7598
|
+
const writtenSource = exactRoute(redirect.source);
|
|
7599
|
+
if (served.has(source)) {
|
|
7600
|
+
const hides = servedAsWritten.has(writtenSource) ? hidden : hiddenByCase;
|
|
7601
|
+
hides.push(redirect.source);
|
|
7602
|
+
return false;
|
|
7603
|
+
}
|
|
7604
|
+
const writtenDestination = destinationRoute(redirect.destination);
|
|
7605
|
+
const destination = writtenDestination?.toLowerCase() ?? null;
|
|
7606
|
+
const firesFolded = !folded.has(source);
|
|
7607
|
+
const firesAsWritten = !asWritten.has(writtenSource);
|
|
7608
|
+
const loopsFolded = firesFolded && destination !== null && folded.closesLoop(source, destination);
|
|
7609
|
+
const loopsAsWritten = firesAsWritten && writtenDestination !== null && asWritten.closesLoop(writtenSource, writtenDestination);
|
|
7610
|
+
if (loopsFolded || loopsAsWritten) {
|
|
7611
|
+
const loops = loopsAsWritten ? looping : loopingByCase;
|
|
7612
|
+
loops.push(`${redirect.source} -> ${redirect.destination}`);
|
|
7613
|
+
return false;
|
|
7614
|
+
}
|
|
7615
|
+
if (firesFolded) folded.add(source, destination);
|
|
7616
|
+
if (firesAsWritten) asWritten.add(writtenSource, writtenDestination);
|
|
7617
|
+
return true;
|
|
7618
|
+
});
|
|
7619
|
+
if (hidden.length > 0) {
|
|
7620
|
+
warnings.push({
|
|
7621
|
+
code: "unsupported-config",
|
|
7622
|
+
message: `Skipped ${hidden.length} redirect${hidden.length === 1 ? "" : "s"} from ${list(hidden)}: ${hidden.length === 1 ? "that URL is" : "those URLs are"} also ${hidden.length === 1 ? "a page" : "pages"} in this site, and a redirect would hide ${hidden.length === 1 ? "it" : "them"}. To redirect instead, delete the page and add the redirect to docs.json.`
|
|
7623
|
+
});
|
|
7624
|
+
}
|
|
7625
|
+
if (hiddenByCase.length > 0) {
|
|
7626
|
+
const one = hiddenByCase.length === 1;
|
|
7627
|
+
warnings.push({
|
|
7628
|
+
code: "unsupported-config",
|
|
7629
|
+
message: `Skipped ${hiddenByCase.length} redirect${one ? "" : "s"} from ${list(hiddenByCase)}: ${one ? "that URL differs" : "those URLs differ"} only in letter case from ${one ? "a page" : "pages"} in this site, so on a server that ignores letter case ${one ? "it" : "they"} would hide ${one ? "that page" : "those pages"}. If your host matches URLs case-sensitively, add ${one ? "it" : "them"} back to docs.json.`
|
|
7630
|
+
});
|
|
7631
|
+
}
|
|
7632
|
+
if (looping.length > 0) {
|
|
7633
|
+
warnings.push({
|
|
7634
|
+
code: "unsupported-config",
|
|
7635
|
+
message: `Skipped ${looping.length} redirect${looping.length === 1 ? "" : "s"} that would send visitors in a circle: ${list(looping)}. Check these redirects in the source docs.`
|
|
7636
|
+
});
|
|
7637
|
+
}
|
|
7638
|
+
if (loopingByCase.length > 0) {
|
|
7639
|
+
const one = loopingByCase.length === 1;
|
|
7640
|
+
warnings.push({
|
|
7641
|
+
code: "unsupported-config",
|
|
7642
|
+
message: `Skipped ${loopingByCase.length} redirect${one ? "" : "s"} that would send visitors in a circle on a server that ignores letter case, since the addresses involved differ only in letter case: ${list(loopingByCase)}. If your host matches URLs case-sensitively, add ${one ? "it" : "them"} back to docs.json.`
|
|
7643
|
+
});
|
|
7644
|
+
}
|
|
7645
|
+
if (redirects.length === config.redirects.length) return config;
|
|
7646
|
+
const result = { ...config, redirects };
|
|
7647
|
+
if (redirects.length === 0) delete result.redirects;
|
|
7648
|
+
return result;
|
|
7649
|
+
}
|
|
7650
|
+
|
|
7472
7651
|
// src/url.ts
|
|
7473
7652
|
var DEFAULT_MAX_PAGES = 1e3;
|
|
7474
7653
|
var DEFAULT_MAX_TOTAL_BYTES = 1e8;
|
|
@@ -8443,13 +8622,13 @@ function migrationHtmlArticle(body) {
|
|
|
8443
8622
|
const sourceId = select.attr("id");
|
|
8444
8623
|
if (sourceId) projection.attr("id", sourceId);
|
|
8445
8624
|
projection.append($("<p></p>").text(select.attr("aria-label")));
|
|
8446
|
-
const
|
|
8625
|
+
const list2 = $("<ul></ul>");
|
|
8447
8626
|
for (const url of urls) {
|
|
8448
8627
|
const item = $("<li></li>").append($("<a></a>").attr("href", url.href).text(url.label));
|
|
8449
8628
|
if (url.isSelected) item.append(" (selected)");
|
|
8450
|
-
|
|
8629
|
+
list2.append(item);
|
|
8451
8630
|
}
|
|
8452
|
-
projection.append(
|
|
8631
|
+
projection.append(list2);
|
|
8453
8632
|
select.replaceWith(projection);
|
|
8454
8633
|
});
|
|
8455
8634
|
return { $, root };
|
|
@@ -8691,8 +8870,8 @@ function readDocusaurusSidebar(document) {
|
|
|
8691
8870
|
const $ = load2(document.body);
|
|
8692
8871
|
const sidebar = $("ul.theme-doc-sidebar-menu").first();
|
|
8693
8872
|
if (sidebar.length === 0) return null;
|
|
8694
|
-
function readList(
|
|
8695
|
-
return $(
|
|
8873
|
+
function readList(list2) {
|
|
8874
|
+
return $(list2).children("li").toArray().flatMap((item) => {
|
|
8696
8875
|
const directLink = $(item).children("a.menu__link").first();
|
|
8697
8876
|
const collapsibleLink = $(item).children(".menu__list-item-collapsible").children("a.menu__link").first();
|
|
8698
8877
|
const link = directLink.length > 0 ? directLink : collapsibleLink;
|
|
@@ -9255,6 +9434,7 @@ async function migrateUrl(options) {
|
|
|
9255
9434
|
};
|
|
9256
9435
|
}
|
|
9257
9436
|
if (platform === "mintlify") docsConfig = addMintlifyDirectoryRedirects(docsConfig, pages);
|
|
9437
|
+
docsConfig = dropConflictingRedirects(dropUnroutableRedirects(docsConfig, warnings), pages, warnings);
|
|
9258
9438
|
return {
|
|
9259
9439
|
sourceUrl: source.toString(),
|
|
9260
9440
|
sourceKind: "url",
|
|
@@ -10402,7 +10582,7 @@ function readSpecFile(absolutePath, relativePath, warnings) {
|
|
|
10402
10582
|
});
|
|
10403
10583
|
return void 0;
|
|
10404
10584
|
}
|
|
10405
|
-
return readFileSync6(absolutePath);
|
|
10585
|
+
return withoutByteOrderMark(readFileSync6(absolutePath));
|
|
10406
10586
|
}
|
|
10407
10587
|
function specAssetPath(filename) {
|
|
10408
10588
|
return `${SPEC_DIRECTORY}/${filename}`;
|
|
@@ -10438,10 +10618,26 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, p
|
|
|
10438
10618
|
references.push({ value: key, kind: "openapi", tabLabel: `OpenAPI: ${key}`, hidden: true, pageOnly: true });
|
|
10439
10619
|
}
|
|
10440
10620
|
const seen = /* @__PURE__ */ new Set();
|
|
10621
|
+
const remoteByKey = /* @__PURE__ */ new Map();
|
|
10622
|
+
const mountDirectory = (remote, reference) => {
|
|
10623
|
+
const directory = reference.directory?.replace(/^\/+|\/+$/g, "");
|
|
10624
|
+
if (!remote || !directory) return;
|
|
10625
|
+
const mount = remote.directories?.find((existing) => existing.directory === directory);
|
|
10626
|
+
if (!mount) {
|
|
10627
|
+
remote.directories = [...remote.directories ?? [], { directory, ...reference.operations ? { operations: reference.operations } : {} }];
|
|
10628
|
+
} else if (mount.operations && reference.operations) {
|
|
10629
|
+
mount.operations = [.../* @__PURE__ */ new Set([...mount.operations, ...reference.operations])];
|
|
10630
|
+
} else {
|
|
10631
|
+
delete mount.operations;
|
|
10632
|
+
}
|
|
10633
|
+
};
|
|
10441
10634
|
const specs = [];
|
|
10442
10635
|
for (const reference of references) {
|
|
10443
10636
|
const dedupeKey = `${reference.kind}:${reference.value}:${reference.tabLabel ?? ""}`;
|
|
10444
|
-
if (seen.has(dedupeKey))
|
|
10637
|
+
if (seen.has(dedupeKey)) {
|
|
10638
|
+
mountDirectory(remoteByKey.get(dedupeKey), reference);
|
|
10639
|
+
continue;
|
|
10640
|
+
}
|
|
10445
10641
|
seen.add(dedupeKey);
|
|
10446
10642
|
const tabSuffix = reference.tabLabel ? ` (tab "${reference.tabLabel}")` : "";
|
|
10447
10643
|
if (reference.kind === "asyncapi") {
|
|
@@ -10452,7 +10648,10 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, p
|
|
|
10452
10648
|
continue;
|
|
10453
10649
|
}
|
|
10454
10650
|
if (/^https:\/\//i.test(reference.value)) {
|
|
10455
|
-
|
|
10651
|
+
const remote = { url: reference.value, ...reference.tabLabel ? { tabLabel: reference.tabLabel } : {}, ...reference.parentTab ? { parentTab: reference.parentTab } : {}, ...reference.icon ? { icon: reference.icon } : {}, ...reference.hidden ? { hidden: true } : {} };
|
|
10652
|
+
mountDirectory(remote, reference);
|
|
10653
|
+
remoteSpecs.push(remote);
|
|
10654
|
+
remoteByKey.set(dedupeKey, remote);
|
|
10456
10655
|
warnings.push({
|
|
10457
10656
|
code: "unsupported-config",
|
|
10458
10657
|
message: `The remote OpenAPI spec "${reference.value}"${tabSuffix} requires a network download before this import is complete.`,
|
|
@@ -10552,10 +10751,15 @@ function thallyOperationSlugSegments(path, method) {
|
|
|
10552
10751
|
function slugifyApiSpecId(value) {
|
|
10553
10752
|
return value.toLowerCase().replace(/[^a-z0-9/]+/g, "-").replace(/(^-|-$)+/g, "").replace(/\//g, "-") || value.toLowerCase();
|
|
10554
10753
|
}
|
|
10754
|
+
function apiTabSpecIds(tabs) {
|
|
10755
|
+
return tabs.filter((tab) => !tab.hidden && tab.api).map((tab, index) => ({ tab, specId: index === 0 ? "default" : slugifyApiSpecId(tab.tab) }));
|
|
10756
|
+
}
|
|
10757
|
+
var flagged = (value) => value === true || value === "true";
|
|
10758
|
+
var isWithheld = (node) => flagged(node["x-hidden"]) || flagged(node["x-excluded"]);
|
|
10555
10759
|
function parseOpenApiOperationsForLinking(content) {
|
|
10556
10760
|
let document;
|
|
10557
10761
|
try {
|
|
10558
|
-
document =
|
|
10762
|
+
document = parseSpecDocument(content.toString("utf8")).document;
|
|
10559
10763
|
} catch {
|
|
10560
10764
|
return [];
|
|
10561
10765
|
}
|
|
@@ -10564,10 +10768,13 @@ function parseOpenApiOperationsForLinking(content) {
|
|
|
10564
10768
|
const operations = [];
|
|
10565
10769
|
for (const [path, pathItem] of Object.entries(paths)) {
|
|
10566
10770
|
if (!pathItem || typeof pathItem !== "object") continue;
|
|
10771
|
+
const item = pathItem;
|
|
10772
|
+
if (isWithheld(item)) continue;
|
|
10567
10773
|
for (const method of OPENAPI_HTTP_METHODS) {
|
|
10568
|
-
const operation =
|
|
10774
|
+
const operation = item[method];
|
|
10569
10775
|
if (!operation || typeof operation !== "object") continue;
|
|
10570
10776
|
const op = operation;
|
|
10777
|
+
if (isWithheld(op)) continue;
|
|
10571
10778
|
const tags = Array.isArray(op.tags) ? op.tags.filter((tag) => typeof tag === "string") : [];
|
|
10572
10779
|
operations.push({
|
|
10573
10780
|
method,
|
|
@@ -10580,19 +10787,39 @@ function parseOpenApiOperationsForLinking(content) {
|
|
|
10580
10787
|
}
|
|
10581
10788
|
return operations;
|
|
10582
10789
|
}
|
|
10583
|
-
function
|
|
10790
|
+
function servedOperations(source, parsed) {
|
|
10791
|
+
let operations = parsed.get(source.filename);
|
|
10792
|
+
if (!operations) {
|
|
10793
|
+
operations = parseOpenApiOperationsForLinking(source.content);
|
|
10794
|
+
parsed.set(source.filename, operations);
|
|
10795
|
+
}
|
|
10796
|
+
if (!source.operations) return operations;
|
|
10797
|
+
const byKey = new Map(operations.map((operation) => [`${operation.method} ${operation.path}`, operation]));
|
|
10798
|
+
return source.operations.flatMap((reference) => {
|
|
10799
|
+
const [method, ...path] = reference.trim().split(/\s+/);
|
|
10800
|
+
const operation = byKey.get(`${method.toLowerCase()} ${path.join(" ")}`);
|
|
10801
|
+
return operation ? [operation] : [];
|
|
10802
|
+
});
|
|
10803
|
+
}
|
|
10804
|
+
var mountPrefix = (directory) => directory.replace(/^\/+|\/+$/g, "").toLowerCase();
|
|
10805
|
+
function apiOperationLinkMap(sources, docsConfig, otherMounts = []) {
|
|
10584
10806
|
const map = /* @__PURE__ */ new Map();
|
|
10585
10807
|
const prefixLandings = /* @__PURE__ */ new Map();
|
|
10586
10808
|
const mintlifyPages = /* @__PURE__ */ new Map();
|
|
10587
10809
|
const duplicatePages = [];
|
|
10588
|
-
const
|
|
10589
|
-
|
|
10590
|
-
|
|
10591
|
-
|
|
10592
|
-
const
|
|
10593
|
-
|
|
10810
|
+
const parsedByFile = /* @__PURE__ */ new Map();
|
|
10811
|
+
const bound = apiTabSpecIds(docsConfig.tabs).flatMap(({ tab, specId }) => sources.filter((entry) => tab.api?.source === specAssetPath(entry.filename) && (!entry.tab || entry.tab === tab.tab)).map((source) => ({ source, specId })));
|
|
10812
|
+
const mounts = new Set([...bound.map(({ source }) => source.prefix ? mountPrefix(source.prefix) : ""), ...otherMounts.map(mountPrefix)].filter(Boolean));
|
|
10813
|
+
const underDeeperMount = (route, own2) => {
|
|
10814
|
+
const lower = route.toLowerCase();
|
|
10815
|
+
return [...mounts].some((mount) => mount.length > own2.length && (lower === mount || lower.startsWith(`${mount}/`)));
|
|
10816
|
+
};
|
|
10817
|
+
bound.forEach(({ source, specId }) => {
|
|
10818
|
+
if (!source.prefix) return;
|
|
10819
|
+
const base = source.prefix.replace(/^\/+|\/+$/g, "");
|
|
10820
|
+
const prefix = mountPrefix(source.prefix);
|
|
10594
10821
|
if (prefix) prefixLandings.set(prefix, specId === "default" ? "/api" : `/api/${specId}`);
|
|
10595
|
-
for (const operation of
|
|
10822
|
+
for (const operation of servedOperations(source, parsedByFile)) {
|
|
10596
10823
|
const thallyHref = `/api/${specId}/${thallyOperationSlugSegments(operation.path, operation.method).join("/")}`;
|
|
10597
10824
|
const leafCandidates = /* @__PURE__ */ new Set();
|
|
10598
10825
|
if (operation.operationId) {
|
|
@@ -10608,31 +10835,66 @@ function apiOperationLinkMap(sources, docsConfig) {
|
|
|
10608
10835
|
const tagSegment = mintlifyOperationSlugSegment(operation.tag ?? "default");
|
|
10609
10836
|
const mintlifyTag = mintlifyUrlSlug(operation.tag ?? "default");
|
|
10610
10837
|
const mintlifyLeaf = mintlifyUrlSlug(operation.summary?.trim() || operation.operationId?.trim() || `${operation.method} ${operation.path}`);
|
|
10611
|
-
const canonical2 = `${
|
|
10612
|
-
if (mintlifyTag && mintlifyLeaf) {
|
|
10838
|
+
const canonical2 = `${base}/${mintlifyTag}/${mintlifyLeaf}`.replace(/^\/+|\/+$/g, "");
|
|
10839
|
+
if (mintlifyTag && mintlifyLeaf && !underDeeperMount(canonical2, prefix)) {
|
|
10613
10840
|
if (!mintlifyPages.has(canonical2)) mintlifyPages.set(canonical2, thallyHref);
|
|
10614
10841
|
else if (mintlifyPages.get(canonical2) !== thallyHref && !duplicatePages.includes(canonical2)) duplicatePages.push(canonical2);
|
|
10842
|
+
if (!map.has(canonical2.toLowerCase())) map.set(canonical2.toLowerCase(), thallyHref);
|
|
10843
|
+
}
|
|
10844
|
+
const tagRoot = `${base}/${mintlifyTag}`.replace(/^\/+|\/+$/g, "");
|
|
10845
|
+
if (mintlifyTag && !underDeeperMount(tagRoot, prefix)) {
|
|
10846
|
+
if (!mintlifyPages.has(tagRoot)) mintlifyPages.set(tagRoot, thallyHref);
|
|
10847
|
+
if (!map.has(tagRoot.toLowerCase())) map.set(tagRoot.toLowerCase(), thallyHref);
|
|
10615
10848
|
}
|
|
10616
|
-
if (mintlifyTag && mintlifyLeaf && !map.has(canonical2.toLowerCase())) map.set(canonical2.toLowerCase(), thallyHref);
|
|
10617
|
-
const tagRoot = `${source.prefix}/${mintlifyTag}`.replace(/^\/+|\/+$/g, "");
|
|
10618
|
-
if (mintlifyTag && !mintlifyPages.has(tagRoot)) mintlifyPages.set(tagRoot, thallyHref);
|
|
10619
|
-
if (mintlifyTag && !map.has(tagRoot.toLowerCase())) map.set(tagRoot.toLowerCase(), thallyHref);
|
|
10620
10849
|
for (const leaf of leafCandidates) {
|
|
10621
10850
|
if (!leaf) continue;
|
|
10622
|
-
const tagged = `${
|
|
10623
|
-
const flat = `${
|
|
10624
|
-
if (!map.has(tagged)) map.set(tagged, thallyHref);
|
|
10625
|
-
if (!map.has(flat)) map.set(flat, thallyHref);
|
|
10851
|
+
const tagged = `${base}/${tagSegment}/${leaf}`.replace(/^\/+|\/+$/g, "").toLowerCase();
|
|
10852
|
+
const flat = `${base}/${leaf}`.replace(/^\/+|\/+$/g, "").toLowerCase();
|
|
10853
|
+
if (!map.has(tagged) && !underDeeperMount(tagged, prefix)) map.set(tagged, thallyHref);
|
|
10854
|
+
if (!map.has(flat) && !underDeeperMount(flat, prefix)) map.set(flat, thallyHref);
|
|
10626
10855
|
}
|
|
10627
10856
|
}
|
|
10628
10857
|
});
|
|
10629
10858
|
return { operationLinks: map, prefixLandings, mintlifyPages, duplicatePages };
|
|
10630
10859
|
}
|
|
10631
|
-
function
|
|
10860
|
+
function mintlifyEndpointRedirects(maps, pages, existing, warnings) {
|
|
10861
|
+
const { mintlifyPages, duplicatePages } = maps;
|
|
10862
|
+
if (duplicatePages.length > 0) {
|
|
10863
|
+
const listed = duplicatePages.slice(0, 10).map((url) => `/${url}`).join(", ");
|
|
10864
|
+
warnings.push({
|
|
10865
|
+
code: "unsupported-config",
|
|
10866
|
+
message: `${duplicatePages.length} Mintlify endpoint URL${duplicatePages.length === 1 ? " is" : "s are"} shared by more than one operation (same tag and summary), so only the first operation is redirected from each: ${listed}${duplicatePages.length > 10 ? ", ..." : ""}.`
|
|
10867
|
+
});
|
|
10868
|
+
}
|
|
10869
|
+
const taken = servedRoutes(pages);
|
|
10870
|
+
for (const redirect of existing) taken.add(literalRoute(redirect.source) ?? redirect.source);
|
|
10871
|
+
const candidates = [...mintlifyPages].filter(([source]) => !taken.has(literalRoute(`/${source}`) ?? `/${source}`));
|
|
10872
|
+
const redirects = candidates.slice(0, MAX_ENDPOINT_REDIRECTS).map(([source, destination]) => ({ source: `/${source}`, destination }));
|
|
10873
|
+
const capped = candidates.length - redirects.length;
|
|
10874
|
+
if (capped > 0) {
|
|
10875
|
+
warnings.push({
|
|
10876
|
+
code: "unsupported-config",
|
|
10877
|
+
message: `Created ${redirects.length} of ${candidates.length} Mintlify endpoint redirects; skipped ${capped} because a site is limited to ${MAX_ENDPOINT_REDIRECTS} generated endpoint redirects. Old links to skipped endpoint URLs will not redirect.`
|
|
10878
|
+
});
|
|
10879
|
+
}
|
|
10880
|
+
return redirects;
|
|
10881
|
+
}
|
|
10882
|
+
function linkApiMounts(sources, docsConfig, pages, warnings, reservedMounts = []) {
|
|
10883
|
+
const maps = apiOperationLinkMap(sources, docsConfig, reservedMounts);
|
|
10884
|
+
rewriteApiLinksInPages(pages, maps.operationLinks, maps.prefixLandings, warnings, { mounts: reservedMounts, redirects: docsConfig.redirects });
|
|
10885
|
+
return mintlifyEndpointRedirects(maps, pages, docsConfig.redirects ?? [], warnings);
|
|
10886
|
+
}
|
|
10887
|
+
function rewriteApiLinksInPages(pages, operationLinks, prefixLandings, warnings, answered = {}) {
|
|
10632
10888
|
if (operationLinks.size === 0 && prefixLandings.size === 0) return;
|
|
10633
10889
|
const unmatched = /* @__PURE__ */ new Set();
|
|
10890
|
+
const served = servedRoutes(pages);
|
|
10891
|
+
for (const { source } of answered.redirects ?? []) {
|
|
10892
|
+
const route = literalRoute(source);
|
|
10893
|
+
if (route) served.add(route);
|
|
10894
|
+
}
|
|
10895
|
+
const others = (answered.mounts ?? []).map(mountPrefix).filter(Boolean);
|
|
10634
10896
|
for (const page of pages) {
|
|
10635
|
-
page.body = rewriteApiOperationLinks(page.body, operationLinks, prefixLandings, unmatched);
|
|
10897
|
+
page.body = rewriteApiOperationLinks(page.body, operationLinks, prefixLandings, served, unmatched, others);
|
|
10636
10898
|
}
|
|
10637
10899
|
if (unmatched.size === 0) return;
|
|
10638
10900
|
const sorted = [...unmatched].sort();
|
|
@@ -10641,7 +10903,7 @@ function rewriteApiLinksInPages(pages, operationLinks, prefixLandings, warnings)
|
|
|
10641
10903
|
message: `${sorted.length} link(s) to API endpoints matched no endpoint in the OpenAPI spec, so they now point to the API section's overview page instead: ${sorted.slice(0, 10).join(", ")}${sorted.length > 10 ? `, and ${sorted.length - 10} more` : ""}. Update those links if you want them to reach a specific endpoint.`
|
|
10642
10904
|
});
|
|
10643
10905
|
}
|
|
10644
|
-
function rewriteApiOperationLinks(body, linkMap, prefixLandings, unmatched) {
|
|
10906
|
+
function rewriteApiOperationLinks(body, linkMap, prefixLandings, served, unmatched, otherMounts) {
|
|
10645
10907
|
if (linkMap.size === 0 && prefixLandings.size === 0) return body;
|
|
10646
10908
|
const orderedPrefixes = [...prefixLandings.entries()].sort((a, b) => b[0].length - a[0].length);
|
|
10647
10909
|
let codeFence = null;
|
|
@@ -10655,8 +10917,12 @@ function rewriteApiOperationLinks(body, linkMap, prefixLandings, unmatched) {
|
|
|
10655
10917
|
if (codeFence) return line;
|
|
10656
10918
|
const rewriteTarget = (target2) => {
|
|
10657
10919
|
const suffixIndex = target2.search(/[?#]/);
|
|
10658
|
-
const
|
|
10920
|
+
const literal = suffixIndex >= 0 ? target2.slice(0, suffixIndex) : target2;
|
|
10921
|
+
if (served.has(literalRoute(literal) ?? literal)) return target2;
|
|
10922
|
+
const path = literal.replace(/^\/+|\/+$/g, "").toLowerCase();
|
|
10659
10923
|
const suffix = suffixIndex >= 0 ? target2.slice(suffixIndex) : "";
|
|
10924
|
+
const owner = [...prefixLandings.keys(), ...otherMounts].filter((mount) => path === mount || path.startsWith(`${mount}/`)).reduce((longest, mount) => longest === null || mount.length > longest.length ? mount : longest, null);
|
|
10925
|
+
if (owner !== null && !prefixLandings.has(owner)) return target2;
|
|
10660
10926
|
const mapped = linkMap.get(path);
|
|
10661
10927
|
if (mapped) return `${mapped}${suffix}`;
|
|
10662
10928
|
for (const [prefix, landing] of orderedPrefixes) {
|
|
@@ -12028,6 +12294,7 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
|
|
|
12028
12294
|
const sourceRefPages = [];
|
|
12029
12295
|
const sourceRefAssets = [];
|
|
12030
12296
|
const remoteApiSpecs = [];
|
|
12297
|
+
const localApiMounts = [];
|
|
12031
12298
|
const referencedAssetPaths = /* @__PURE__ */ new Map();
|
|
12032
12299
|
const addAssetReference = (assetPath, referencingPage) => {
|
|
12033
12300
|
const referrers = referencedAssetPaths.get(assetPath);
|
|
@@ -12994,7 +13261,7 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
|
|
|
12994
13261
|
resolvedSpecs.map((spec) => ({ filename: spec.filename, content: spec.content, prefix: spec.routeSegments.join("/") })),
|
|
12995
13262
|
docsConfig
|
|
12996
13263
|
);
|
|
12997
|
-
rewriteApiLinksInPages(pages, operationLinks, prefixLandings, warnings);
|
|
13264
|
+
rewriteApiLinksInPages(pages, operationLinks, prefixLandings, warnings, { redirects: docsConfig.redirects });
|
|
12998
13265
|
}
|
|
12999
13266
|
} else if (platform === "mintlify") {
|
|
13000
13267
|
const warnSharedSpec = (path, excluded = []) => {
|
|
@@ -13029,34 +13296,17 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
|
|
|
13029
13296
|
spec.generatedNavigation = Boolean(spec.directory) && !publishedSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, spec.filename, true));
|
|
13030
13297
|
}
|
|
13031
13298
|
docsConfig = injectOpenApiSpecs(docsConfig, resolvedSpecs, warnings);
|
|
13032
|
-
|
|
13033
|
-
resolvedSpecs.
|
|
13034
|
-
|
|
13035
|
-
|
|
13036
|
-
|
|
13037
|
-
|
|
13038
|
-
|
|
13039
|
-
|
|
13040
|
-
|
|
13041
|
-
|
|
13042
|
-
});
|
|
13043
|
-
}
|
|
13044
|
-
const taken = /* @__PURE__ */ new Set([...pages.map((page) => page.id), ...(docsConfig.redirects ?? []).map((redirect) => redirect.source.replace(/^\//, ""))]);
|
|
13045
|
-
const candidates = [...mintlifyPages].filter(([source]) => !taken.has(source));
|
|
13046
|
-
const endpointRedirects = candidates.slice(0, MAX_ENDPOINT_REDIRECTS).map(([source, destination]) => ({ source: `/${source}`, destination }));
|
|
13047
|
-
const alreadyTaken = mintlifyPages.size - candidates.length;
|
|
13048
|
-
const capped = candidates.length - endpointRedirects.length;
|
|
13049
|
-
if (alreadyTaken + capped > 0) {
|
|
13050
|
-
const reasons = [
|
|
13051
|
-
alreadyTaken > 0 ? `${alreadyTaken} because the URL already has a page or redirect` : "",
|
|
13052
|
-
capped > 0 ? `${capped} because a site is limited to ${MAX_ENDPOINT_REDIRECTS} generated endpoint redirects` : ""
|
|
13053
|
-
].filter(Boolean).join(" and ");
|
|
13054
|
-
warnings.push({
|
|
13055
|
-
code: "unsupported-config",
|
|
13056
|
-
message: `Created ${endpointRedirects.length} of ${mintlifyPages.size} Mintlify endpoint redirects; skipped ${alreadyTaken + capped}: ${reasons}. Old links to skipped endpoint URLs will not redirect.`
|
|
13057
|
-
});
|
|
13299
|
+
if (remoteApiSpecs.length > 0) {
|
|
13300
|
+
localApiMounts.push(...resolvedSpecs.flatMap((spec) => spec.directory ? [{ filename: spec.filename, directory: spec.directory, ...spec.tabLabel ? { tab: spec.tabLabel } : {} }] : []));
|
|
13301
|
+
} else {
|
|
13302
|
+
const endpointRedirects = linkApiMounts(
|
|
13303
|
+
resolvedSpecs.map((spec) => ({ filename: spec.filename, content: spec.content, prefix: spec.directory, ...spec.tabLabel ? { tab: spec.tabLabel } : {} })),
|
|
13304
|
+
docsConfig,
|
|
13305
|
+
pages,
|
|
13306
|
+
warnings
|
|
13307
|
+
);
|
|
13308
|
+
if (endpointRedirects.length > 0) docsConfig = { ...docsConfig, redirects: [...docsConfig.redirects ?? [], ...endpointRedirects] };
|
|
13058
13309
|
}
|
|
13059
|
-
if (endpointRedirects.length > 0) docsConfig = { ...docsConfig, redirects: [...docsConfig.redirects ?? [], ...endpointRedirects] };
|
|
13060
13310
|
} else {
|
|
13061
13311
|
const fallback = findOpenApi(files);
|
|
13062
13312
|
const fallbackContent = fallback ? readSpecFile(fallback.absolutePath, fallback.relativePath, warnings) : void 0;
|
|
@@ -13099,8 +13349,9 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
|
|
|
13099
13349
|
if (docsConfig.tabs.some((tab) => tab.api)) docsConfig = { ...docsConfig, api: { ...docsConfig.api, specLink: false } };
|
|
13100
13350
|
}
|
|
13101
13351
|
if (platform === "mintlify") {
|
|
13102
|
-
const sources = new Set((docsConfig.redirects ?? []).map((redirect) => redirect.source));
|
|
13103
|
-
const
|
|
13352
|
+
const sources = new Set((docsConfig.redirects ?? []).map((redirect) => redirect.source.toLowerCase()));
|
|
13353
|
+
const served = servedRoutes(pages);
|
|
13354
|
+
const aliases = routeAliases.filter((redirect) => !sources.has(redirect.source.toLowerCase()) && !served.has(literalRoute(redirect.source) ?? redirect.source));
|
|
13104
13355
|
if (aliases.length > 0) docsConfig = { ...docsConfig, redirects: [...docsConfig.redirects ?? [], ...aliases] };
|
|
13105
13356
|
docsConfig = addMintlifyHomepageRedirects(addMintlifyDirectoryRedirects(docsConfig, pages), pages);
|
|
13106
13357
|
const reachablePaths = /* @__PURE__ */ new Set([
|
|
@@ -13314,26 +13565,7 @@ ${body}`;
|
|
|
13314
13565
|
}
|
|
13315
13566
|
}
|
|
13316
13567
|
if (docsConfig.redirects) {
|
|
13317
|
-
docsConfig =
|
|
13318
|
-
...docsConfig,
|
|
13319
|
-
redirects: docsConfig.redirects.filter((redirect) => {
|
|
13320
|
-
const literalRoute = (route) => route.startsWith("/") && !/[?:*#()[\]{}]/.test(route) ? route.replace(/\/+$/, "") || "/" : null;
|
|
13321
|
-
const sourceRoute = literalRoute(redirect.source);
|
|
13322
|
-
if (sourceRoute && sourceRoute === literalRoute(redirect.destination)) {
|
|
13323
|
-
warnings.push({
|
|
13324
|
-
code: "unsupported-config",
|
|
13325
|
-
message: `Self-redirect from ${redirect.source} to ${redirect.destination} was omitted.`
|
|
13326
|
-
});
|
|
13327
|
-
return false;
|
|
13328
|
-
}
|
|
13329
|
-
if (!/[+?()[\]{}]/.test(redirect.source)) return true;
|
|
13330
|
-
warnings.push({
|
|
13331
|
-
code: "unsupported-config",
|
|
13332
|
-
message: `Redirect from ${redirect.source} contains route-pattern syntax Next.js cannot express literally and was omitted.`
|
|
13333
|
-
});
|
|
13334
|
-
return false;
|
|
13335
|
-
})
|
|
13336
|
-
};
|
|
13568
|
+
docsConfig = dropConflictingRedirects(dropUnroutableRedirects(docsConfig, warnings), [...pages, ...sourceRefPages], warnings);
|
|
13337
13569
|
}
|
|
13338
13570
|
if (pages.length === 0) {
|
|
13339
13571
|
const reasons = warnings.map((warning) => warning.message);
|
|
@@ -13546,6 +13778,7 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
|
|
|
13546
13778
|
assets,
|
|
13547
13779
|
...importedSourceRefs.length > 0 ? { sourceRefs: importedSourceRefs } : {},
|
|
13548
13780
|
...remoteApiSpecs.length > 0 ? { remoteApiSpecs } : {},
|
|
13781
|
+
...remoteApiSpecs.length > 0 && localApiMounts.length > 0 ? { localApiMounts } : {},
|
|
13549
13782
|
...componentMigrator ? { componentFiles: componentMigrator.files() } : {},
|
|
13550
13783
|
...quarantinedFiles.length > 0 ? { quarantinedFiles } : {},
|
|
13551
13784
|
...droppedGatedFiles.length > 0 ? { droppedGatedPages: droppedGatedFiles.length } : {},
|
|
@@ -15314,7 +15547,6 @@ import { createHash as createHash4 } from "crypto";
|
|
|
15314
15547
|
import { lookup } from "dns/promises";
|
|
15315
15548
|
import { request } from "https";
|
|
15316
15549
|
import { isIP } from "net";
|
|
15317
|
-
import { parse as parseYaml7 } from "yaml";
|
|
15318
15550
|
var MAX_SPEC_BYTES2 = 25e6;
|
|
15319
15551
|
var MAX_REDIRECTS = 3;
|
|
15320
15552
|
function isPublicAddress(address, family) {
|
|
@@ -15408,9 +15640,7 @@ async function downloadSpec(value, fetcher) {
|
|
|
15408
15640
|
}
|
|
15409
15641
|
throw new Error("Remote OpenAPI spec redirected too many times.");
|
|
15410
15642
|
}
|
|
15411
|
-
function parseOpenApi(
|
|
15412
|
-
const text3 = content.toString("utf8");
|
|
15413
|
-
const parsed = extension === "yaml" || !text3.trimStart().startsWith("{") ? parseYaml7(text3) : JSON.parse(text3);
|
|
15643
|
+
function parseOpenApi(parsed) {
|
|
15414
15644
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("OpenAPI spec is not an object.");
|
|
15415
15645
|
const document = parsed;
|
|
15416
15646
|
if (typeof document.openapi !== "string" || !/^3\./.test(document.openapi)) {
|
|
@@ -15423,10 +15653,11 @@ function parseOpenApi(content, extension) {
|
|
|
15423
15653
|
if (paths.length > 5e3) throw new Error("Remote OpenAPI spec has too many paths to import.");
|
|
15424
15654
|
const operations = [];
|
|
15425
15655
|
for (const [path, value] of paths) {
|
|
15426
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
15656
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || isWithheld(value)) continue;
|
|
15427
15657
|
for (const [method, raw] of Object.entries(value)) {
|
|
15428
15658
|
if (!/^(get|post|put|patch|delete|head|options|trace)$/i.test(method) || !raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
15429
15659
|
const operation = raw;
|
|
15660
|
+
if (isWithheld(operation)) continue;
|
|
15430
15661
|
const tags = Array.isArray(operation.tags) ? operation.tags : [];
|
|
15431
15662
|
operations.push({
|
|
15432
15663
|
method: method.toLowerCase(),
|
|
@@ -15441,9 +15672,8 @@ function parseOpenApi(content, extension) {
|
|
|
15441
15672
|
function slug(value) {
|
|
15442
15673
|
return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
15443
15674
|
}
|
|
15444
|
-
function operationTarget(operation) {
|
|
15445
|
-
|
|
15446
|
-
return `/api/default/${[...path, operation.method].join("/")}`;
|
|
15675
|
+
function operationTarget(specId, operation) {
|
|
15676
|
+
return `/api/${specId}/${thallyOperationSlugSegments(operation.path, operation.method).join("/")}`;
|
|
15447
15677
|
}
|
|
15448
15678
|
async function hydrateRemoteApiSpecs(bundle, fetcher) {
|
|
15449
15679
|
if (!bundle.remoteApiSpecs?.length) return bundle;
|
|
@@ -15453,28 +15683,32 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
|
|
|
15453
15683
|
const warnings = [...bundle.warnings];
|
|
15454
15684
|
const pages = bundle.pages.map((page) => ({ ...page }));
|
|
15455
15685
|
const authoredLinks = /* @__PURE__ */ new Set();
|
|
15686
|
+
const served = servedRoutes(pages);
|
|
15687
|
+
const endpointUrls = /* @__PURE__ */ new Map();
|
|
15456
15688
|
for (const page of bundle.pages) {
|
|
15457
15689
|
for (const match of page.body.matchAll(/\/[a-z0-9_/-]+/gi)) {
|
|
15458
15690
|
const segments = match[0].split("/").filter(Boolean);
|
|
15459
15691
|
if (segments.length < 3) continue;
|
|
15460
15692
|
const section = segments[0].toLowerCase();
|
|
15461
|
-
if (section.includes("api") || section.includes("reference")) authoredLinks.add(match[0]);
|
|
15693
|
+
if (section.includes("api") || section.includes("reference")) authoredLinks.add(match[0].replace(/\/+$/, ""));
|
|
15462
15694
|
}
|
|
15463
15695
|
}
|
|
15696
|
+
const bound = [];
|
|
15464
15697
|
for (const reference of bundle.remoteApiSpecs) {
|
|
15465
15698
|
for (let index = warnings.length - 1; index >= 0; index--) {
|
|
15466
15699
|
if (warnings[index].source === reference.url && warnings[index].message.includes("requires a network download")) warnings.splice(index, 1);
|
|
15467
15700
|
}
|
|
15468
15701
|
try {
|
|
15469
|
-
const
|
|
15470
|
-
const extension = /\.ya?ml$/i.test(url.pathname) ? "yaml" : "json";
|
|
15471
|
-
const downloaded = await downloadSpec(reference.url, fetcher);
|
|
15702
|
+
const downloaded = withoutByteOrderMark(await downloadSpec(reference.url, fetcher));
|
|
15472
15703
|
const pruned = markExcluded(downloaded, reference.withheldOperations ?? [], reference.keptOperations);
|
|
15473
15704
|
const body = pruned.content;
|
|
15474
|
-
if (reference.withheldOperations?.length
|
|
15475
|
-
|
|
15705
|
+
if (reference.withheldOperations?.length && !warnings.some((warning) => warning.code === "gated-page" && warning.source === reference.url)) {
|
|
15706
|
+
warnings.push({ code: "gated-page", message: sharedSpecMessage(reference.url, pruned.excluded), source: reference.url });
|
|
15707
|
+
}
|
|
15708
|
+
const { format: extension, document } = parseSpecDocument(body.toString("utf8"));
|
|
15709
|
+
const operations = parseOpenApi(document);
|
|
15476
15710
|
const filename = `openapi-${createHash4("sha256").update(reference.url).digest("hex").slice(0, 12)}.${extension}`;
|
|
15477
|
-
assets.push({ path: `openapi/${filename}`, content: body, projectRelative: true });
|
|
15711
|
+
if (!assets.some((asset) => asset.path === `openapi/${filename}`)) assets.push({ path: `openapi/${filename}`, content: body, projectRelative: true });
|
|
15478
15712
|
for (const page of pages) {
|
|
15479
15713
|
const ref = page.openapi ? splitOpenApiRef(page.openapi) : null;
|
|
15480
15714
|
if (ref?.specRef === reference.url) page.openapi = withSpecRef(ref, `openapi/${filename}`);
|
|
@@ -15482,21 +15716,7 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
|
|
|
15482
15716
|
const tab = reference.tabLabel ? tabs.find((candidate) => candidate.tab === reference.tabLabel) : tabs.find((candidate) => candidate.tab.toLowerCase().includes("api"));
|
|
15483
15717
|
if (tab) tab.api = { source: `openapi/${filename}` };
|
|
15484
15718
|
else insertApiTab(tabs, { tab: reference.tabLabel ?? "API Reference", ...reference.icon ? { icon: reference.icon } : {}, ...reference.hidden ? { hidden: true } : {}, api: { source: `openapi/${filename}` } }, reference.parentTab);
|
|
15485
|
-
|
|
15486
|
-
for (const operation of operations) {
|
|
15487
|
-
const key = `${slug(operation.tag)}/${slug(operation.summary)}`;
|
|
15488
|
-
if (routes.has(key)) routes.set(key, null);
|
|
15489
|
-
else routes.set(key, operationTarget(operation));
|
|
15490
|
-
}
|
|
15491
|
-
const existingSources = new Set(redirects.map((redirect) => redirect.source));
|
|
15492
|
-
for (const source of authoredLinks) {
|
|
15493
|
-
const parts = source.split("/").filter(Boolean);
|
|
15494
|
-
const key = parts.slice(-2).join("/");
|
|
15495
|
-
const destination = routes.get(key);
|
|
15496
|
-
if (!destination || existingSources.has(source)) continue;
|
|
15497
|
-
redirects.push({ source, destination, permanent: false });
|
|
15498
|
-
existingSources.add(source);
|
|
15499
|
-
}
|
|
15719
|
+
bound.push({ reference, filename, content: body, operations, tabName: tab?.tab ?? reference.tabLabel ?? "API Reference" });
|
|
15500
15720
|
} catch (error) {
|
|
15501
15721
|
warnings.push({
|
|
15502
15722
|
code: "fetch-failed",
|
|
@@ -15505,11 +15725,59 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
|
|
|
15505
15725
|
});
|
|
15506
15726
|
}
|
|
15507
15727
|
}
|
|
15728
|
+
const directories = [
|
|
15729
|
+
...(bundle.localApiMounts ?? []).map(({ directory }) => directory),
|
|
15730
|
+
...bundle.remoteApiSpecs.flatMap((reference) => (reference.directories ?? []).map(({ directory }) => directory))
|
|
15731
|
+
];
|
|
15732
|
+
const endpointRedirects = linkApiMounts(
|
|
15733
|
+
[
|
|
15734
|
+
...(bundle.localApiMounts ?? []).map(({ filename, directory, tab }) => ({
|
|
15735
|
+
filename,
|
|
15736
|
+
content: Buffer.from(assets.find((asset) => asset.path === `openapi/${filename}`)?.content ?? []),
|
|
15737
|
+
prefix: directory,
|
|
15738
|
+
...tab ? { tab } : {}
|
|
15739
|
+
})),
|
|
15740
|
+
...bound.flatMap(({ reference, filename, content, tabName }) => (reference.directories ?? []).map(({ directory, operations: listed }) => ({ filename, content, prefix: directory, tab: tabName, ...listed ? { operations: listed } : {} })))
|
|
15741
|
+
],
|
|
15742
|
+
{ ...bundle.docsConfig, tabs, redirects },
|
|
15743
|
+
pages,
|
|
15744
|
+
warnings,
|
|
15745
|
+
directories
|
|
15746
|
+
);
|
|
15747
|
+
for (const redirect of endpointRedirects) {
|
|
15748
|
+
redirects.push(redirect);
|
|
15749
|
+
endpointUrls.set(literalRoute(redirect.source) ?? redirect.source, redirect.destination);
|
|
15750
|
+
}
|
|
15751
|
+
const existingSources = new Set(redirects.map((redirect) => literalRoute(redirect.source) ?? redirect.source.toLowerCase()));
|
|
15752
|
+
const mounts = directories.map(mountPrefix).filter(Boolean);
|
|
15753
|
+
const underMount = (source) => mounts.some((prefix) => source.toLowerCase() === `/${prefix}` || source.toLowerCase().startsWith(`/${prefix}/`));
|
|
15754
|
+
const guesses = /* @__PURE__ */ new Map();
|
|
15755
|
+
const specIds = apiTabSpecIds(tabs);
|
|
15756
|
+
for (const { reference, filename, operations, tabName } of bound) {
|
|
15757
|
+
const specId = specIds.find(({ tab }) => tab.tab === tabName && tab.api?.source === `openapi/${filename}`)?.specId;
|
|
15758
|
+
const listed = reference.directories?.length && reference.directories.every((directory) => directory.operations) ? new Set(reference.directories.flatMap((directory) => directory.operations ?? []).map((name) => name.trim().split(/\s+/)).map(([method, ...path]) => `${method.toLowerCase()} ${path.join(" ")}`)) : null;
|
|
15759
|
+
for (const operation of operations) {
|
|
15760
|
+
if (listed && !listed.has(`${operation.method} ${operation.path}`)) continue;
|
|
15761
|
+
const key = `${slug(operation.tag)}/${slug(operation.summary)}`;
|
|
15762
|
+
guesses.set(key, guesses.has(key) || !specId ? null : operationTarget(specId, operation));
|
|
15763
|
+
}
|
|
15764
|
+
}
|
|
15765
|
+
for (const source of authoredLinks) {
|
|
15766
|
+
const destination = guesses.get(source.split("/").filter(Boolean).slice(-2).join("/"));
|
|
15767
|
+
if (!destination || existingSources.has(literalRoute(source) ?? source.toLowerCase()) || served.has(literalRoute(source) ?? source) || underMount(source)) continue;
|
|
15768
|
+
redirects.push({ source, destination, permanent: false });
|
|
15769
|
+
existingSources.add(literalRoute(source) ?? source.toLowerCase());
|
|
15770
|
+
}
|
|
15771
|
+
const direct = redirects.map((redirect) => {
|
|
15772
|
+
const endpoint = endpointUrls.get(literalRoute(redirect.destination) ?? "");
|
|
15773
|
+
return endpoint ? { ...redirect, destination: endpoint } : redirect;
|
|
15774
|
+
});
|
|
15508
15775
|
return {
|
|
15509
15776
|
...bundle,
|
|
15510
15777
|
pages,
|
|
15511
15778
|
assets,
|
|
15512
|
-
|
|
15779
|
+
// Redirects added above meet the same checks as any other: none may fail the build, hide a page that exists, or loop.
|
|
15780
|
+
docsConfig: dropConflictingRedirects(dropUnroutableRedirects({ ...bundle.docsConfig, tabs, ...direct.length > 0 ? { redirects: direct } : {} }, warnings), pages, warnings),
|
|
15513
15781
|
warnings
|
|
15514
15782
|
};
|
|
15515
15783
|
}
|