@thallylabs/migrate 0.2.32 → 0.2.34

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/dist/index.js CHANGED
@@ -230,8 +230,11 @@ var JS_GLOBALS = /* @__PURE__ */ new Set([
230
230
  "BroadcastChannel"
231
231
  ]);
232
232
  var IMPLICIT_MODULE_NAMES = /* @__PURE__ */ new Set(["React", "MintlifyComponents"]);
233
- function parse(source) {
234
- return ts.createSourceFile("inline.tsx", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
233
+ function scriptKindFor(fileName) {
234
+ return /\.[cm]?ts$/.test(fileName) ? ts.ScriptKind.TS : ts.ScriptKind.TSX;
235
+ }
236
+ function parse(source, fileName = "inline.tsx") {
237
+ return ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, scriptKindFor(fileName));
235
238
  }
236
239
  function bindingNames(name, into) {
237
240
  if (ts.isIdentifier(name)) into.add(name.text);
@@ -240,20 +243,44 @@ function bindingNames(name, into) {
240
243
  function isFunctionLike(node) {
241
244
  return ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node) || ts.isConstructorDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node);
242
245
  }
246
+ function hoistedVarNames(root, into) {
247
+ const collect = (node) => {
248
+ if (isFunctionLike(node) || ts.isClassLike(node) || ts.isModuleDeclaration(node)) return;
249
+ if (ts.isVariableDeclaration(node) && !ts.isCatchClause(node.parent) && !(ts.getCombinedNodeFlags(node) & ts.NodeFlags.BlockScoped)) bindingNames(node.name, into);
250
+ ts.forEachChild(node, collect);
251
+ };
252
+ collect(root);
253
+ }
243
254
  function functionScope(fn) {
244
255
  const scope = /* @__PURE__ */ new Set();
245
256
  if ((ts.isFunctionExpression(fn) || ts.isFunctionDeclaration(fn)) && fn.name) scope.add(fn.name.text);
246
257
  for (const parameter of fn.parameters) bindingNames(parameter.name, scope);
247
- function collect(node) {
248
- if (ts.isVariableDeclaration(node)) bindingNames(node.name, scope);
249
- if ((ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) && node.name) scope.add(node.name.text);
250
- if (isFunctionLike(node)) return;
251
- ts.forEachChild(node, collect);
252
- }
253
- if (fn.body) ts.forEachChild(fn.body, collect);
254
- if (fn.body && !ts.isBlock(fn.body)) collect(fn.body);
255
258
  return scope;
256
259
  }
260
+ function lexicalNames(statements) {
261
+ const names = /* @__PURE__ */ new Set();
262
+ for (const statement of statements) {
263
+ if (ts.isVariableStatement(statement)) {
264
+ if (statement.declarationList.flags & ts.NodeFlags.BlockScoped) for (const declaration of statement.declarationList.declarations) bindingNames(declaration.name, names);
265
+ } else if ((ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement)) && statement.name) names.add(statement.name.text);
266
+ }
267
+ return names;
268
+ }
269
+ function ownScope(node) {
270
+ if (ts.isBlock(node)) return lexicalNames(node.statements);
271
+ if (ts.isCaseBlock(node)) return lexicalNames(node.clauses.flatMap((clause) => [...clause.statements]));
272
+ if ((ts.isForStatement(node) || ts.isForInStatement(node) || ts.isForOfStatement(node)) && node.initializer && ts.isVariableDeclarationList(node.initializer)) {
273
+ const names = /* @__PURE__ */ new Set();
274
+ if (node.initializer.flags & ts.NodeFlags.BlockScoped) for (const declaration of node.initializer.declarations) bindingNames(declaration.name, names);
275
+ return names;
276
+ }
277
+ if (ts.isCatchClause(node) && node.variableDeclaration) {
278
+ const names = /* @__PURE__ */ new Set();
279
+ bindingNames(node.variableDeclaration.name, names);
280
+ return names;
281
+ }
282
+ return void 0;
283
+ }
257
284
  function collectRefs(root) {
258
285
  const refs = [];
259
286
  const assigned = [];
@@ -268,13 +295,22 @@ function collectRefs(root) {
268
295
  function walkNode(node, scopes, inAsync) {
269
296
  if (ts.isTypeNode(node)) return;
270
297
  if (isFunctionLike(node)) {
271
- const nextScopes = [...scopes, functionScope(node)];
298
+ const parameterScopes = [...scopes, functionScope(node)];
272
299
  const async = !!node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword);
273
300
  for (const parameter of node.parameters) {
274
- if (!ts.isIdentifier(parameter.name)) walkNode(parameter.name, nextScopes, async);
275
- if (parameter.initializer) walkNode(parameter.initializer, nextScopes, async);
301
+ if (!ts.isIdentifier(parameter.name)) walkNode(parameter.name, parameterScopes, async);
302
+ if (parameter.initializer) walkNode(parameter.initializer, parameterScopes, async);
303
+ }
304
+ if (node.body) {
305
+ const hoisted = /* @__PURE__ */ new Set();
306
+ hoistedVarNames(node.body, hoisted);
307
+ walkNode(node.body, [...parameterScopes, hoisted], async);
276
308
  }
277
- if (node.body) walkNode(node.body, nextScopes, async);
309
+ return;
310
+ }
311
+ const own2 = ownScope(node);
312
+ if (own2) {
313
+ ts.forEachChild(node, (child) => walkNode(child, [...scopes, own2], inAsync));
278
314
  return;
279
315
  }
280
316
  if (ts.isAwaitExpression(node) && !inAsync) awaitOutsideAsync = true;
@@ -651,6 +687,23 @@ function unboundTags(moduleSource, boundNames) {
651
687
  }
652
688
  return [...tags].sort();
653
689
  }
690
+ function unboundBuiltinTags(moduleSource, boundNames, fileName) {
691
+ const tags = /* @__PURE__ */ new Set();
692
+ const values = /* @__PURE__ */ new Set();
693
+ for (const statement of parse(moduleSource, fileName).statements) {
694
+ if (ts.isImportDeclaration(statement)) continue;
695
+ for (const { name, kind } of collectRefs(statement).refs) {
696
+ if (boundNames.has(name)) continue;
697
+ if (kind === "tag" || kind === "member") tags.add(name);
698
+ else values.add(name);
699
+ }
700
+ }
701
+ const isGlobal = (name) => JS_GLOBALS.has(name) || BROWSER_IDENTIFIERS.has(name) || SERVER_ONLY_IDENTIFIERS.has(name);
702
+ const builtinTags = [...tags].filter(isThallyBuiltinComponent);
703
+ const shadowed = builtinTags.filter((name) => JS_GLOBALS.has(name) && values.has(name)).sort();
704
+ const builtinValues = [...values].filter((name) => isThallyBuiltinComponent(name) && !isGlobal(name));
705
+ return { bind: [.../* @__PURE__ */ new Set([...builtinTags.filter((name) => !shadowed.includes(name)), ...builtinValues])].sort(), shadowed };
706
+ }
654
707
  var INLINE_SAFE_GLOBALS = /* @__PURE__ */ new Set([
655
708
  "undefined",
656
709
  "NaN",
@@ -719,12 +772,14 @@ function isSelfContainedFunction(node) {
719
772
  function pageScopeNames(esmSources) {
720
773
  const names = /* @__PURE__ */ new Set();
721
774
  for (const source of esmSources) {
722
- for (const statement of parse(source).statements) {
775
+ const sf = parse(source);
776
+ for (const statement of sf.statements) {
723
777
  for (const local of importLocals(statement.getText())) names.add(local);
724
778
  if (ts.isImportDeclaration(statement)) continue;
725
779
  const target2 = ts.isExportDeclaration(statement) || ts.isExportAssignment(statement) ? void 0 : statement;
726
780
  if (target2) for (const name of declaredNames(target2).names) names.add(name);
727
781
  }
782
+ hoistedVarNames(sf, names);
728
783
  }
729
784
  return names;
730
785
  }
@@ -2393,9 +2448,9 @@ function escapeOrphanCapitalizedTags(body) {
2393
2448
  continue;
2394
2449
  }
2395
2450
  const start = match.index;
2396
- const list = opens.get(name) ?? [];
2397
- list.push({ start, end: start + full.length });
2398
- opens.set(name, list);
2451
+ const list2 = opens.get(name) ?? [];
2452
+ list2.push({ start, end: start + full.length });
2453
+ opens.set(name, list2);
2399
2454
  }
2400
2455
  const edits = [];
2401
2456
  for (const [name, ranges] of opens) {
@@ -2934,13 +2989,7 @@ function sanitizeCssModuleSelectors(css) {
2934
2989
  return { css: root.toString(), omitted };
2935
2990
  }
2936
2991
  function sourceFile(source, filename) {
2937
- return ts2.createSourceFile(
2938
- filename,
2939
- source,
2940
- ts2.ScriptTarget.Latest,
2941
- true,
2942
- /\.[cm]?ts$/.test(filename) ? ts2.ScriptKind.TS : ts2.ScriptKind.TSX
2943
- );
2992
+ return ts2.createSourceFile(filename, source, ts2.ScriptTarget.Latest, true, scriptKindFor(filename));
2944
2993
  }
2945
2994
  function walk(node, visitor) {
2946
2995
  visitor(node);
@@ -2997,7 +3046,7 @@ function needsClientBoundary(source) {
2997
3046
  visitNode(source);
2998
3047
  return needed;
2999
3048
  }
3000
- function implicitReactImports(source) {
3049
+ function implicitReactImports(source, onShadowed) {
3001
3050
  const bindings = /* @__PURE__ */ new Set();
3002
3051
  const references = /* @__PURE__ */ new Set();
3003
3052
  function bind(name) {
@@ -3015,7 +3064,9 @@ function implicitReactImports(source) {
3015
3064
  }
3016
3065
  if (ts2.isVariableStatement(statement)) for (const declaration of statement.declarationList.declarations) bind(declaration.name);
3017
3066
  if ((ts2.isFunctionDeclaration(statement) || ts2.isClassDeclaration(statement)) && statement.name) bindings.add(statement.name.text);
3067
+ if ((ts2.isEnumDeclaration(statement) || ts2.isModuleDeclaration(statement) || ts2.isImportEqualsDeclaration(statement)) && ts2.isIdentifier(statement.name)) bindings.add(statement.name.text);
3018
3068
  }
3069
+ hoistedVarNames(source, bindings);
3019
3070
  function visit(node) {
3020
3071
  if (ts2.isIdentifier(node) && !(ts2.isPropertyAccessExpression(node.parent) && node.parent.name === node) && !(ts2.isPropertyAssignment(node.parent) && node.parent.name === node)) references.add(node.text);
3021
3072
  ts2.forEachChild(node, visit);
@@ -3023,26 +3074,50 @@ function implicitReactImports(source) {
3023
3074
  visit(source);
3024
3075
  const hooks = [...REACT_GLOBALS].filter((name) => references.has(name) && !bindings.has(name)).sort();
3025
3076
  const usesMintlifyComponents = references.has("MintlifyComponents") && !bindings.has("MintlifyComponents");
3077
+ const leafBound = /* @__PURE__ */ new Set();
3026
3078
  const implicitComponents = MINTLIFY_COMPONENT_MODULES.map(({ module, names }) => {
3027
- const needed = names.filter((name) => references.has(name) && !bindings.has(name));
3079
+ const needed = names.filter((name) => name !== "Note" && references.has(name) && !bindings.has(name));
3080
+ for (const name of needed) leafBound.add(name);
3028
3081
  return needed.length ? `import { ${needed.join(", ")} } from '${module}';` : "";
3029
3082
  }).filter(Boolean);
3083
+ const { bind: builtinTags, shadowed } = unboundBuiltinTags(source.text, bindings, source.fileName);
3084
+ const registryNames = builtinTags.filter((name) => !leafBound.has(name));
3085
+ if (shadowed.length) onShadowed?.(shadowed);
3030
3086
  return [
3031
3087
  references.has("React") && !bindings.has("React") ? "import * as React from 'react';" : "",
3032
3088
  hooks.length ? `import { ${hooks.join(", ")} } from 'react';` : "",
3033
3089
  ...implicitComponents,
3090
+ registryNames.length ? builtinRegistryBindings(registryNames, identifierNames(source)) : "",
3034
3091
  usesMintlifyComponents ? mintlifyComponentsShim() : ""
3035
3092
  ].filter(Boolean).join("\n");
3036
3093
  }
3037
3094
  function resolveInlineBuiltinReferences(source, moduleNames) {
3038
3095
  const unresolved = unboundTags(source, moduleNames);
3039
3096
  if (unresolved.length === 0) return source;
3097
+ return [builtinRegistryBindings(unresolved, identifierNames(sourceFile(source, "inline.jsx"))), source].join("\n");
3098
+ }
3099
+ function identifierNames(root) {
3100
+ const names = /* @__PURE__ */ new Set();
3101
+ const visit = (node) => {
3102
+ if (ts2.isIdentifier(node)) names.add(node.text);
3103
+ ts2.forEachChild(node, visit);
3104
+ };
3105
+ visit(root);
3106
+ return names;
3107
+ }
3108
+ function builtinRegistryBindings(names, used) {
3109
+ const registry = "builtinMdxComponents";
3110
+ let alias = registry;
3111
+ for (let suffix = 2; used.has(alias); suffix++) alias = `${registry}${suffix}`;
3040
3112
  return [
3041
- "import { builtinMdxComponents } from '@/components/mdx/builtin-components';",
3042
- `const { ${unresolved.join(", ")} } = builtinMdxComponents;`,
3043
- source
3113
+ `import { ${alias === registry ? registry : `${registry} as ${alias}`} } from '@/components/mdx/builtin-components';`,
3114
+ `const { ${names.join(", ")} } = ${alias};`
3044
3115
  ].join("\n");
3045
3116
  }
3117
+ function shadowedBuiltinMessage(file, names) {
3118
+ const tags = names.map((name) => `<${name}>`).join(", ");
3119
+ return `${file} renders ${tags} as a Thally built-in component but also uses the JavaScript global of the same name, so the tag was left unbound. Rename the component or the global use in the source.`;
3120
+ }
3046
3121
  function imports(statement) {
3047
3122
  if (!ts2.isStringLiteral(statement.moduleSpecifier) || !statement.importClause) return [];
3048
3123
  const clause = statement.importClause;
@@ -3269,6 +3344,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
3269
3344
  function copyGraph(entry) {
3270
3345
  const staged = /* @__PURE__ */ new Map();
3271
3346
  const stagedWarnings = [];
3347
+ const stagedShadowed = [];
3272
3348
  const modules = /* @__PURE__ */ new Map();
3273
3349
  let stagedBytes = 0;
3274
3350
  function stageShim(filename, content) {
@@ -3373,7 +3449,8 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
3373
3449
  const prologue = `// @ts-nocheck
3374
3450
  ${client ? "'use client';\n" : ""}
3375
3451
  `;
3376
- return shebangMatch ? shebangMatch[0] + prologue + implicitReactImports(ast) + "\n" + replaced.slice(shebangMatch[0].length) : prologue + implicitReactImports(ast) + "\n" + replaced;
3452
+ const implicit = implicitReactImports(ast, (names) => stagedShadowed.push({ path, names }));
3453
+ return shebangMatch ? shebangMatch[0] + prologue + implicit + "\n" + replaced.slice(shebangMatch[0].length) : prologue + implicit + "\n" + replaced;
3377
3454
  }
3378
3455
  });
3379
3456
  }
@@ -3391,6 +3468,7 @@ ${client ? "'use client';\n" : ""}
3391
3468
  }
3392
3469
  for (const [path, module] of modules) staged.set(path, { path, content: module.render(clientModules.has(path)) });
3393
3470
  for (const [path, file] of staged) copied.set(path, file);
3471
+ for (const { path, names } of stagedShadowed) warn(shadowedBuiltinMessage(relative2(root, path).replace(/\\/g, "/"), names), path);
3394
3472
  for (const { path, omitted } of stagedWarnings) {
3395
3473
  warn(`Site-authored CSS module ${relative2(root, path).replace(/\\/g, "/")} has ${omitted} global-only selector branch(es) that cannot be copied into Thally CSS Modules; those styles were omitted.`, path);
3396
3474
  }
@@ -3845,8 +3923,12 @@ ${inlineSource}` });
3845
3923
  }`),
3846
3924
  ""
3847
3925
  ].join("\n");
3926
+ const implicit = implicitReactImports(
3927
+ sourceFile(inlineSource, "inline.jsx"),
3928
+ (names) => warn(shadowedBuiltinMessage(relative2(root, currentFile).replace(/\\/g, "/"), names), currentFile)
3929
+ );
3848
3930
  copied.set(path, { path, content: `'use client';
3849
- ${implicitReactImports(sourceFile(inlineSource, "inline.jsx"))}
3931
+ ${implicit}
3850
3932
  ${inlineSource}` });
3851
3933
  copiedBytes += Buffer.byteLength(inlineSource);
3852
3934
  for (const { node, name } of extracted) {
@@ -4238,24 +4320,25 @@ import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as rea
4238
4320
  import { dirname as dirname2, extname as extname3, relative as relative3 } from "path";
4239
4321
  var MAX_MINTLIFY_CONFIG_BYTES = 2e7;
4240
4322
  function addMintlifyDirectoryRedirects(config, pages) {
4241
- const pageIds = new Set(pages.map((page) => page.id));
4323
+ const pageIds = new Set(pages.map((page) => page.id.toLowerCase()));
4242
4324
  const redirects = [...config.redirects ?? []];
4243
- const redirectSources = new Set(redirects.map((redirect) => redirect.source.replace(/\/$/, "") || "/"));
4325
+ const redirectSources = new Set(redirects.map((redirect) => (redirect.source.replace(/\/$/, "") || "/").toLowerCase()));
4244
4326
  function hasAuthoredRedirect(source) {
4245
- if (redirectSources.has(source)) return true;
4327
+ const route = source.toLowerCase();
4328
+ if (redirectSources.has(route)) return true;
4246
4329
  return (config.redirects ?? []).some((redirect) => {
4247
- const match = redirect.source.match(/^(.*)\/:[A-Za-z_][A-Za-z0-9_]*\*\/?$/);
4248
- return match ? source === match[1] || source.startsWith(`${match[1]}/`) : false;
4330
+ const match = redirect.source.toLowerCase().match(/^(.*)\/:[a-z_][a-z0-9_]*\*\/?$/);
4331
+ return match ? route === match[1] || route.startsWith(`${match[1]}/`) : false;
4249
4332
  });
4250
4333
  }
4251
4334
  for (const page of pages) {
4252
4335
  if (!/\/(?:overview|introduction)$/.test(page.navigationId)) continue;
4253
4336
  const parent = page.id.replace(/\/(?:overview|introduction)$/, "");
4254
- if (!parent || pageIds.has(parent)) continue;
4337
+ if (!parent || pageIds.has(parent.toLowerCase())) continue;
4255
4338
  const source = `/${parent}`;
4256
4339
  if (hasAuthoredRedirect(source)) continue;
4257
4340
  redirects.push({ source, destination: `/${page.id}`, permanent: false });
4258
- redirectSources.add(source);
4341
+ redirectSources.add(source.toLowerCase());
4259
4342
  }
4260
4343
  const navigationOrder = [];
4261
4344
  function collectPages(nodes) {
@@ -4285,11 +4368,11 @@ function addMintlifyDirectoryRedirects(config, pages) {
4285
4368
  }
4286
4369
  }
4287
4370
  for (const [directory, landing] of firstDescendantByDirectory) {
4288
- if (pageIds.has(directory)) continue;
4371
+ if (pageIds.has(directory.toLowerCase())) continue;
4289
4372
  const source = `/${directory}`;
4290
4373
  if (hasAuthoredRedirect(source)) continue;
4291
4374
  redirects.push({ source, destination: `/${landing.id}`, permanent: false });
4292
- redirectSources.add(source);
4375
+ redirectSources.add(source.toLowerCase());
4293
4376
  }
4294
4377
  return redirects.length > 0 ? { ...config, redirects } : config;
4295
4378
  }
@@ -4308,11 +4391,11 @@ function addMintlifyHomepageRedirects(config, pages) {
4308
4391
  const homepage = config.tabs.filter((tab) => !tab.hidden).map((tab) => firstPage([...tab.pages ?? [], ...tab.groups ?? []])).find(Boolean);
4309
4392
  if (!homepage) return config;
4310
4393
  const redirects = [...config.redirects ?? []];
4311
- const sources = new Set(redirects.map((redirect) => redirect.source.replace(/\/$/, "") || "/"));
4394
+ const sources = new Set(redirects.map((redirect) => (redirect.source.replace(/\/$/, "") || "/").toLowerCase()));
4312
4395
  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
4396
  for (const locale of roots) {
4314
4397
  const source = locale ? `/${locale}` : "/";
4315
- if (sources.has(source)) continue;
4398
+ if (sources.has(source.toLowerCase())) continue;
4316
4399
  redirects.push({ source, destination: `${locale ? `/${locale}` : ""}/${homepage}`, permanent: true });
4317
4400
  }
4318
4401
  return { ...config, redirects };
@@ -4439,6 +4522,22 @@ function specSource(value) {
4439
4522
  const source = typeof value === "string" ? value : objectValue2(value)?.source;
4440
4523
  return typeof source === "string" && source.trim() ? source.trim() : void 0;
4441
4524
  }
4525
+ function listedOperations(container) {
4526
+ const operations = [];
4527
+ const visit = (entry, root) => {
4528
+ if (typeof entry === "string") {
4529
+ if (OPENAPI_OPERATION_REF.test(entry)) operations.push(entry.trim());
4530
+ return;
4531
+ }
4532
+ const object2 = objectValue2(entry);
4533
+ if (!object2 || !root && (specSource(object2.openapi) || specSource(object2.asyncapi))) return;
4534
+ for (const key of ["groups", "pages"]) {
4535
+ if (Array.isArray(object2[key])) object2[key].forEach((child) => visit(child, false));
4536
+ }
4537
+ };
4538
+ visit(container, true);
4539
+ return operations;
4540
+ }
4442
4541
  var hasNavigationChildren = (node) => ["groups", "pages"].some((key) => Array.isArray(node[key]) && node[key].length > 0);
4443
4542
  function mintlifyNavigationApiReferences(config) {
4444
4543
  const navigation = objectValue2(config.navigation) ?? config;
@@ -4450,12 +4549,14 @@ function mintlifyNavigationApiReferences(config) {
4450
4549
  const value = specSource(node[kind]);
4451
4550
  const directory = objectValue2(node[kind])?.directory;
4452
4551
  if (value) {
4552
+ const operations = typeof directory === "string" && directory.trim() ? listedOperations(node) : [];
4453
4553
  references.push({
4454
4554
  value,
4455
4555
  kind,
4456
4556
  tabLabel,
4457
4557
  ...sibling,
4458
- ...typeof directory === "string" && directory.trim() ? { directory: directory.trim() } : {}
4558
+ ...typeof directory === "string" && directory.trim() ? { directory: directory.trim() } : {},
4559
+ ...operations.length > 0 ? { operations } : {}
4459
4560
  });
4460
4561
  }
4461
4562
  }
@@ -7177,8 +7278,49 @@ function splitOpenApiRef(raw) {
7177
7278
  const specRef = match[1] ? unquote(match[1]) : void 0;
7178
7279
  return { ...specRef ? { specRef } : {}, operation: match[2] };
7179
7280
  }
7281
+ function canonicalSpecRef(specRef) {
7282
+ return specRef.trim().split(/[?#]/, 1)[0].replace(/\\/g, "/").replace(/^(?:\.?\/+)+/, "");
7283
+ }
7284
+ function isRemoteSpecRef(specRef) {
7285
+ return /^[a-z][a-z0-9+.-]*:/i.test(specRef);
7286
+ }
7180
7287
  function specRefBaseName(specRef) {
7181
- return specRef.trim().replace(/\\/g, "/").split(/[?#]/, 1)[0].split("/").filter(Boolean).pop()?.toLowerCase() ?? "";
7288
+ return canonicalSpecRef(specRef).split("/").filter(Boolean).pop()?.toLowerCase() ?? "";
7289
+ }
7290
+ function withoutSpecExtension(value) {
7291
+ return value.replace(/\.(?:json|ya?ml)$/i, "");
7292
+ }
7293
+ function resolveSpecRef(specRef, candidates, anyFolder = true) {
7294
+ const key = canonicalSpecRef(specRef);
7295
+ const byId = /* @__PURE__ */ new Map();
7296
+ for (const candidate of candidates) {
7297
+ const id = candidate.sourcePath ?? candidate.filename;
7298
+ if (!byId.has(id)) byId.set(id, candidate);
7299
+ }
7300
+ const unique = [...byId.values()];
7301
+ const exact = unique.find((candidate) => candidate.sourcePath === key);
7302
+ if (exact) return { kind: "match", spec: exact };
7303
+ if (!anyFolder && key.includes("/")) return { kind: "none" };
7304
+ const name = specRefBaseName(key);
7305
+ const names = unique.map((candidate) => specRefBaseName(candidate.sourcePath ?? candidate.filename));
7306
+ for (const fits of [unique.filter((_, index) => names[index] === name), unique.filter((_, index) => withoutSpecExtension(names[index]) === name)]) {
7307
+ if (fits.length === 1) return { kind: "match", spec: fits[0] };
7308
+ if (fits.length > 1) return { kind: "ambiguous", candidates: fits };
7309
+ }
7310
+ return { kind: "none" };
7311
+ }
7312
+ function localSpecRef(openapi) {
7313
+ const specRef = openapi ? splitOpenApiRef(openapi)?.specRef : void 0;
7314
+ return specRef && !isRemoteSpecRef(specRef) ? specRef : void 0;
7315
+ }
7316
+ function specNamedByPage(openapi, specs) {
7317
+ const specRef = localSpecRef(openapi);
7318
+ const resolved = specRef ? resolveSpecRef(specRef, specs) : void 0;
7319
+ return resolved?.kind === "match" ? resolved.spec : void 0;
7320
+ }
7321
+ function namesSeveralSpecs(openapi, specs) {
7322
+ const specRef = localSpecRef(openapi);
7323
+ return Boolean(specRef) && resolveSpecRef(specRef, specs).kind === "ambiguous";
7182
7324
  }
7183
7325
  function withSpecRef(ref, specRef) {
7184
7326
  return `${specRef} ${ref.operation}`;
@@ -7186,36 +7328,71 @@ function withSpecRef(ref, specRef) {
7186
7328
 
7187
7329
  // src/spec-exclude.ts
7188
7330
  import { parse as parseYaml4, stringify as stringifyYaml } from "yaml";
7189
- function specRefMatches(ref, specPath, specFilename, isDefault) {
7190
- const prefix = /^(?:(\S+)\s+)?(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE|WEBHOOK)\s/i.exec(`${ref} `)?.[1];
7331
+ function specRefMatches(ref, specPath, isDefault) {
7332
+ const prefix = splitOpenApiRef(ref)?.specRef;
7191
7333
  if (!prefix) return isDefault;
7192
- const wanted = prefix.replace(/^\/+/, "").toLowerCase();
7334
+ const wanted = (isRemoteSpecRef(prefix) ? prefix.trim() : canonicalSpecRef(prefix)).toLowerCase();
7193
7335
  const lowerPath = specPath.toLowerCase();
7194
- return lowerPath === wanted || lowerPath.endsWith(`/${wanted}`) || specFilename.toLowerCase() === wanted;
7336
+ const stem = withoutSpecExtension(lowerPath);
7337
+ return lowerPath === wanted || lowerPath.endsWith(`/${wanted}`) || stem === wanted || stem.endsWith(`/${wanted}`);
7338
+ }
7339
+ function resolvedSources(ref, specs) {
7340
+ const specRef = splitOpenApiRef(ref)?.specRef;
7341
+ if (!specRef || isRemoteSpecRef(specRef)) return { unique: false, sources: [] };
7342
+ const resolved = resolveSpecRef(specRef, specs);
7343
+ if (resolved.kind === "match") return { unique: true, sources: [resolved.spec.sourcePath] };
7344
+ return { unique: false, sources: resolved.kind === "ambiguous" ? resolved.candidates.map((candidate) => candidate.sourcePath) : [] };
7345
+ }
7346
+ function refWithholdsFrom(ref, spec, specs) {
7347
+ const { unique, sources } = resolvedSources(ref, specs);
7348
+ return unique ? sources[0] === spec.sourcePath : specRefMatches(ref, spec.sourcePath, true) || sources.includes(spec.sourcePath);
7349
+ }
7350
+ function refKeepsIn(ref, spec, specs) {
7351
+ const specRef = splitOpenApiRef(ref)?.specRef;
7352
+ if (!specRef) return false;
7353
+ if (isRemoteSpecRef(specRef)) return specRefMatches(ref, spec.sourcePath, false);
7354
+ const { unique, sources } = resolvedSources(ref, specs);
7355
+ return unique && sources[0] === spec.sourcePath;
7195
7356
  }
7196
7357
  function operationKey(operation) {
7197
7358
  const [first, ...rest] = operation.trim().split(/\s+/);
7198
7359
  return `${first.toLowerCase()} ${rest.join(" ")}`;
7199
7360
  }
7200
- function keysFor(spec, refs) {
7361
+ function keysFor(refs, names) {
7201
7362
  return [...new Set(
7202
- refs.filter((ref) => specRefMatches(ref, spec.sourcePath, spec.filename, true)).map((ref) => splitOpenApiRef(ref)?.operation).filter((op) => !!op).map(operationKey)
7363
+ refs.filter(names).map((ref) => splitOpenApiRef(ref)?.operation).filter((op) => !!op).map(operationKey)
7203
7364
  )];
7204
7365
  }
7205
- function withheldOperationKeys(spec, withheldRefs, keptRefs) {
7206
- const kept = keysFor(spec, keptRefs);
7207
- return { withheld: keysFor(spec, withheldRefs).filter((key) => !kept.includes(key)), kept };
7366
+ function withheldOperationKeys(spec, withheldRefs, keptRefs, specs = [spec]) {
7367
+ const kept = keysFor(keptRefs, (ref) => refKeepsIn(ref, spec, specs));
7368
+ return { withheld: keysFor(withheldRefs, (ref) => refWithholdsFrom(ref, spec, specs)).filter((key) => !kept.includes(key)), kept };
7369
+ }
7370
+ function withoutByteOrderMark(content) {
7371
+ return content.length >= 3 && content[0] === 239 && content[1] === 187 && content[2] === 191 ? content.subarray(3) : content;
7372
+ }
7373
+ function parseSpecDocument(text3) {
7374
+ const source = text3.replace(/^\uFEFF/, "");
7375
+ try {
7376
+ return { format: "json", document: JSON.parse(source) };
7377
+ } catch (jsonError) {
7378
+ try {
7379
+ return { format: "yaml", document: parseYaml4(source) };
7380
+ } catch (yamlError) {
7381
+ throw source.trimStart().startsWith("{") ? jsonError : yamlError;
7382
+ }
7383
+ }
7208
7384
  }
7209
7385
  function markExcluded(content, operationKeys, keptKeys = []) {
7210
7386
  const untouched = { content, excluded: [] };
7211
7387
  if (operationKeys.length === 0) return untouched;
7212
7388
  const text3 = content.toString("utf8");
7213
- let doc;
7389
+ let parsed;
7214
7390
  try {
7215
- doc = parseYaml4(text3);
7391
+ parsed = parseSpecDocument(text3);
7216
7392
  } catch {
7217
7393
  return untouched;
7218
7394
  }
7395
+ const doc = parsed.document;
7219
7396
  if (!doc || typeof doc !== "object") return untouched;
7220
7397
  const excluded = [];
7221
7398
  for (const key of operationKeys) {
@@ -7238,7 +7415,7 @@ function markExcluded(content, operationKeys, keptKeys = []) {
7238
7415
  }
7239
7416
  }
7240
7417
  if (excluded.length === 0) return untouched;
7241
- const out = text3.trimStart().startsWith("{") ? `${JSON.stringify(doc, null, 2)}
7418
+ const out = parsed.format === "json" ? `${JSON.stringify(doc, null, 2)}
7242
7419
  ` : stringifyYaml(doc);
7243
7420
  return { content: Buffer.from(out), excluded };
7244
7421
  }
@@ -7469,6 +7646,150 @@ function migrationImageAttribute(value) {
7469
7646
  return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\{/g, "&#123;").replace(/\}/g, "&#125;").replace(/\r/g, "&#13;").replace(/\n/g, "&#10;");
7470
7647
  }
7471
7648
 
7649
+ // src/redirects.ts
7650
+ var LISTED = 10;
7651
+ function exactRoute(route) {
7652
+ return route.startsWith("/") && !/[?:*#()[\]{}]/.test(route) ? route.replace(/\/+$/, "") || "/" : null;
7653
+ }
7654
+ function literalRoute(route) {
7655
+ return exactRoute(route)?.toLowerCase() ?? null;
7656
+ }
7657
+ function resolveDotSegments(route) {
7658
+ const segments = [];
7659
+ for (const segment of route.split("/").slice(1)) {
7660
+ const dots = segment.replace(/%2e/gi, ".");
7661
+ if (dots === "..") segments.pop();
7662
+ else if (dots !== ".") segments.push(segment);
7663
+ }
7664
+ return `/${segments.join("/")}`;
7665
+ }
7666
+ function destinationRoute(destination) {
7667
+ const route = destination.startsWith("//") ? null : exactRoute(destination.split(/[?#]/, 1)[0]);
7668
+ return route === null ? null : resolveDotSegments(route);
7669
+ }
7670
+ function pageRoutes(pages) {
7671
+ return pages.flatMap((page) => [`/${page.id}`, `/${page.id.replace(/\/index$/, "")}`]);
7672
+ }
7673
+ function servedRoutes(pages) {
7674
+ return new Set(pageRoutes(pages).map((route) => route.toLowerCase()));
7675
+ }
7676
+ function list(sources) {
7677
+ return `${sources.slice(0, LISTED).join(", ")}${sources.length > LISTED ? ", ..." : ""}`;
7678
+ }
7679
+ var ROUTE_PATTERN_OPERATORS = /[+?()[\]{}]/;
7680
+ function dropUnroutableRedirects(config, warnings) {
7681
+ if (!config.redirects?.length) return config;
7682
+ const unmatchable = [];
7683
+ const redirects = config.redirects.filter((redirect) => {
7684
+ const source = exactRoute(redirect.source);
7685
+ if (source && source === exactRoute(redirect.destination)) {
7686
+ warnings.push({ code: "unsupported-config", message: `Self-redirect from ${redirect.source} to ${redirect.destination} was omitted.` });
7687
+ return false;
7688
+ }
7689
+ if (!ROUTE_PATTERN_OPERATORS.test(redirect.source)) return true;
7690
+ unmatchable.push(redirect.source);
7691
+ return false;
7692
+ });
7693
+ if (unmatchable.length > 0) {
7694
+ warnings.push({
7695
+ code: "unsupported-config",
7696
+ 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.`
7697
+ });
7698
+ }
7699
+ if (redirects.length === config.redirects.length) return config;
7700
+ const result = { ...config, redirects };
7701
+ if (redirects.length === 0) delete result.redirects;
7702
+ return result;
7703
+ }
7704
+ function redirectChains() {
7705
+ const kept = /* @__PURE__ */ new Map();
7706
+ const terminal = /* @__PURE__ */ new Set();
7707
+ const has = (source) => kept.has(source) || terminal.has(source);
7708
+ const add = (source, destination) => {
7709
+ if (destination === null) terminal.add(source);
7710
+ else kept.set(source, destination);
7711
+ };
7712
+ const closesLoop = (source, destination) => {
7713
+ let end = destination;
7714
+ for (let steps = 0; kept.has(end); steps++) {
7715
+ if (steps >= kept.size) return true;
7716
+ end = kept.get(end);
7717
+ }
7718
+ for (let step = destination; step !== end; ) {
7719
+ const next = kept.get(step);
7720
+ kept.set(step, end);
7721
+ step = next;
7722
+ }
7723
+ return end === source;
7724
+ };
7725
+ return { has, add, closesLoop };
7726
+ }
7727
+ function dropConflictingRedirects(config, pages, warnings) {
7728
+ if (!config.redirects?.length) return config;
7729
+ const served = servedRoutes(pages);
7730
+ const servedAsWritten = new Set(pageRoutes(pages));
7731
+ const hidden = [];
7732
+ const hiddenByCase = [];
7733
+ const looping = [];
7734
+ const loopingByCase = [];
7735
+ const folded = redirectChains();
7736
+ const asWritten = redirectChains();
7737
+ const redirects = config.redirects.filter((redirect) => {
7738
+ const source = literalRoute(redirect.source);
7739
+ if (!source) return true;
7740
+ const writtenSource = exactRoute(redirect.source);
7741
+ if (served.has(source)) {
7742
+ const hides = servedAsWritten.has(writtenSource) ? hidden : hiddenByCase;
7743
+ hides.push(redirect.source);
7744
+ return false;
7745
+ }
7746
+ const writtenDestination = destinationRoute(redirect.destination);
7747
+ const destination = writtenDestination?.toLowerCase() ?? null;
7748
+ const firesFolded = !folded.has(source);
7749
+ const firesAsWritten = !asWritten.has(writtenSource);
7750
+ const loopsFolded = firesFolded && destination !== null && folded.closesLoop(source, destination);
7751
+ const loopsAsWritten = firesAsWritten && writtenDestination !== null && asWritten.closesLoop(writtenSource, writtenDestination);
7752
+ if (loopsFolded || loopsAsWritten) {
7753
+ const loops = loopsAsWritten ? looping : loopingByCase;
7754
+ loops.push(`${redirect.source} -> ${redirect.destination}`);
7755
+ return false;
7756
+ }
7757
+ if (firesFolded) folded.add(source, destination);
7758
+ if (firesAsWritten) asWritten.add(writtenSource, writtenDestination);
7759
+ return true;
7760
+ });
7761
+ if (hidden.length > 0) {
7762
+ warnings.push({
7763
+ code: "unsupported-config",
7764
+ 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.`
7765
+ });
7766
+ }
7767
+ if (hiddenByCase.length > 0) {
7768
+ const one = hiddenByCase.length === 1;
7769
+ warnings.push({
7770
+ code: "unsupported-config",
7771
+ 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.`
7772
+ });
7773
+ }
7774
+ if (looping.length > 0) {
7775
+ warnings.push({
7776
+ code: "unsupported-config",
7777
+ 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.`
7778
+ });
7779
+ }
7780
+ if (loopingByCase.length > 0) {
7781
+ const one = loopingByCase.length === 1;
7782
+ warnings.push({
7783
+ code: "unsupported-config",
7784
+ 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.`
7785
+ });
7786
+ }
7787
+ if (redirects.length === config.redirects.length) return config;
7788
+ const result = { ...config, redirects };
7789
+ if (redirects.length === 0) delete result.redirects;
7790
+ return result;
7791
+ }
7792
+
7472
7793
  // src/url.ts
7473
7794
  var DEFAULT_MAX_PAGES = 1e3;
7474
7795
  var DEFAULT_MAX_TOTAL_BYTES = 1e8;
@@ -8443,13 +8764,13 @@ function migrationHtmlArticle(body) {
8443
8764
  const sourceId = select.attr("id");
8444
8765
  if (sourceId) projection.attr("id", sourceId);
8445
8766
  projection.append($("<p></p>").text(select.attr("aria-label")));
8446
- const list = $("<ul></ul>");
8767
+ const list2 = $("<ul></ul>");
8447
8768
  for (const url of urls) {
8448
8769
  const item = $("<li></li>").append($("<a></a>").attr("href", url.href).text(url.label));
8449
8770
  if (url.isSelected) item.append(" (selected)");
8450
- list.append(item);
8771
+ list2.append(item);
8451
8772
  }
8452
- projection.append(list);
8773
+ projection.append(list2);
8453
8774
  select.replaceWith(projection);
8454
8775
  });
8455
8776
  return { $, root };
@@ -8691,8 +9012,8 @@ function readDocusaurusSidebar(document) {
8691
9012
  const $ = load2(document.body);
8692
9013
  const sidebar = $("ul.theme-doc-sidebar-menu").first();
8693
9014
  if (sidebar.length === 0) return null;
8694
- function readList(list) {
8695
- return $(list).children("li").toArray().flatMap((item) => {
9015
+ function readList(list2) {
9016
+ return $(list2).children("li").toArray().flatMap((item) => {
8696
9017
  const directLink = $(item).children("a.menu__link").first();
8697
9018
  const collapsibleLink = $(item).children(".menu__list-item-collapsible").children("a.menu__link").first();
8698
9019
  const link = directLink.length > 0 ? directLink : collapsibleLink;
@@ -9255,6 +9576,7 @@ async function migrateUrl(options) {
9255
9576
  };
9256
9577
  }
9257
9578
  if (platform === "mintlify") docsConfig = addMintlifyDirectoryRedirects(docsConfig, pages);
9579
+ docsConfig = dropConflictingRedirects(dropUnroutableRedirects(docsConfig, warnings), pages, warnings);
9258
9580
  return {
9259
9581
  sourceUrl: source.toString(),
9260
9582
  sourceKind: "url",
@@ -10294,22 +10616,56 @@ function normalizedReferenceKey(value) {
10294
10616
  function exactReferenceKey(value) {
10295
10617
  return value.split(/[?#]/, 1)[0].replace(/^\/+/, "").replace(/\\/g, "/").replace(/\.(?:mdx?|rst|txt)$/i, "");
10296
10618
  }
10297
- function navigationOperationRefs(config) {
10298
- const refs = [];
10299
- const visit = (node) => {
10619
+ function containerSpec(node) {
10620
+ const value = node.openapi;
10621
+ if (value === void 0) return void 0;
10622
+ const source = value && typeof value === "object" && !Array.isArray(value) ? value.source : value;
10623
+ return typeof source === "string" && source.trim() && !splitOpenApiRef(source) ? source.trim() : null;
10624
+ }
10625
+ function navigationSpecContext(config, pageIdentity) {
10626
+ const context = { operations: [], pageOwners: /* @__PURE__ */ new Map(), pageIdentity };
10627
+ const visit = (node, owner, listed) => {
10300
10628
  if (typeof node === "string") {
10301
- if (splitOpenApiRef(node)) refs.push(node);
10302
- } else if (Array.isArray(node)) node.forEach(visit);
10629
+ if (splitOpenApiRef(node)) context.operations.push({ ref: node, owner });
10630
+ else if (listed) {
10631
+ const page = pageIdentity(node);
10632
+ if (page !== null) context.pageOwners.set(page, (context.pageOwners.get(page) ?? /* @__PURE__ */ new Set()).add(owner));
10633
+ }
10634
+ } else if (Array.isArray(node)) node.forEach((child) => visit(child, owner, true));
10303
10635
  else if (node && typeof node === "object") {
10304
10636
  if (navigationGateReason(node)) return;
10305
- Object.values(node).forEach(visit);
10306
- }
10307
- };
10308
- visit(config?.navigation);
10309
- return refs;
10310
- }
10311
- function excludeWithheldOperations(spec, withheldRefs, keptRefs) {
10312
- const { withheld, kept } = withheldOperationKeys(spec, withheldRefs, keptRefs);
10637
+ const own2 = containerSpec(node);
10638
+ Object.values(node).forEach((child) => visit(child, own2 === void 0 ? owner : own2, false));
10639
+ }
10640
+ };
10641
+ visit(config?.navigation, void 0, false);
10642
+ return context;
10643
+ }
10644
+ function keptOperationRefs(published, navigation, defaultSpec, specs) {
10645
+ const kept = [];
10646
+ const unowned = [];
10647
+ const source = (owner) => {
10648
+ if (!owner || isRemoteSpecRef(owner)) return owner;
10649
+ const resolved = resolveSpecRef(owner, specs);
10650
+ return resolved.kind === "match" ? resolved.spec.sourcePath ?? resolved.spec.filename : owner;
10651
+ };
10652
+ const add = (raw, owner) => {
10653
+ const ref = splitOpenApiRef(raw);
10654
+ if (!ref) return;
10655
+ if (ref.specRef) kept.push(raw);
10656
+ else if (owner) kept.push(withSpecRef(ref, owner));
10657
+ else unowned.push(ref.operation);
10658
+ };
10659
+ for (const { ref, file } of published) {
10660
+ const page = navigation.pageIdentity(file);
10661
+ const owners = new Set([...(page === null ? void 0 : navigation.pageOwners.get(page)) ?? [void 0]].map((owner) => source(owner === void 0 ? defaultSpec : owner)));
10662
+ add(ref, owners.size === 1 ? [...owners][0] : null);
10663
+ }
10664
+ for (const { ref, owner } of navigation.operations) add(ref, source(owner === void 0 ? defaultSpec : owner));
10665
+ return { kept, unowned };
10666
+ }
10667
+ function excludeWithheldOperations(spec, withheldRefs, keptRefs, specs) {
10668
+ const { withheld, kept } = withheldOperationKeys(spec, withheldRefs, keptRefs, specs);
10313
10669
  return markExcluded(spec.content, withheld, kept);
10314
10670
  }
10315
10671
  var TOKEN_SEPARATORS = /[\s"'()<>[\]{}=,;:|\\/`*!?#&]+/;
@@ -10402,7 +10758,7 @@ function readSpecFile(absolutePath, relativePath, warnings) {
10402
10758
  });
10403
10759
  return void 0;
10404
10760
  }
10405
- return readFileSync6(absolutePath);
10761
+ return withoutByteOrderMark(readFileSync6(absolutePath));
10406
10762
  }
10407
10763
  function specAssetPath(filename) {
10408
10764
  return `${SPEC_DIRECTORY}/${filename}`;
@@ -10410,6 +10766,23 @@ function specAssetPath(filename) {
10410
10766
  function specAsset(filename, content) {
10411
10767
  return { path: specAssetPath(filename), content, projectRelative: true };
10412
10768
  }
10769
+ function createSpecAssetNamer() {
10770
+ const nameBySource = /* @__PURE__ */ new Map();
10771
+ const sourceByName = /* @__PURE__ */ new Map();
10772
+ return ({ sourcePath, filename, tabLabel }) => {
10773
+ let name = nameBySource.get(sourcePath);
10774
+ if (name === void 0) {
10775
+ name = filename;
10776
+ if (sourceByName.has(name.toLowerCase())) {
10777
+ name = sourcePath.replace(/\//g, "-");
10778
+ while (sourceByName.has(name.toLowerCase())) name = `${(tabLabel ?? "default").replace(/\W+/g, "-")}-${name}`;
10779
+ }
10780
+ nameBySource.set(sourcePath, name);
10781
+ sourceByName.set(name.toLowerCase(), sourcePath);
10782
+ }
10783
+ return name;
10784
+ };
10785
+ }
10413
10786
  function mintlifyTopLevelApiReferences(config) {
10414
10787
  const api = config?.api && typeof config.api === "object" && !Array.isArray(config.api) ? config.api : null;
10415
10788
  if (!api) return [];
@@ -10421,27 +10794,49 @@ function mintlifyTopLevelApiReferences(config) {
10421
10794
  }
10422
10795
  return references;
10423
10796
  }
10424
- function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, pages = []) {
10797
+ function specFileCandidates(files) {
10798
+ return files.filter((file) => /\.(?:json|ya?ml)$/i.test(file.relativePath)).map((file) => ({ sourcePath: file.relativePath, filename: specRefBaseName(file.relativePath) }));
10799
+ }
10800
+ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, nameSpec, pages = []) {
10425
10801
  if (!mintlifyConfig) return [];
10426
10802
  const references = [
10427
10803
  ...mintlifyTopLevelApiReferences(mintlifyConfig),
10428
10804
  ...mintlifyNavigationApiReferences(mintlifyConfig)
10429
10805
  ];
10430
- const specKey = (value) => value.split(/[?#]/, 1)[0].replace(/\\/g, "/").replace(/^(?:\.\/)+/, "").replace(/^\/+/, "");
10431
- const known = new Set(references.map((reference) => specKey(reference.value)));
10806
+ const known = new Set(references.map((reference) => canonicalSpecRef(reference.value)));
10807
+ const specFiles = specFileCandidates(files);
10808
+ const namedFiles = /* @__PURE__ */ new Map();
10432
10809
  for (const page of pages) {
10433
10810
  const specRef = page.openapi ? splitOpenApiRef(page.openapi)?.specRef : void 0;
10434
10811
  if (!specRef || /^[a-z][a-z0-9+.-]*:/i.test(specRef)) continue;
10435
- const key = specKey(specRef);
10436
- if (known.has(key) || !files.some((file) => file.relativePath === key)) continue;
10812
+ const named = namedFiles.get(specRef) ?? resolveSpecRef(specRef, specFiles, false);
10813
+ namedFiles.set(specRef, named);
10814
+ const key = named.kind === "match" ? named.spec.sourcePath : "";
10815
+ if (!key || known.has(key)) continue;
10437
10816
  known.add(key);
10438
10817
  references.push({ value: key, kind: "openapi", tabLabel: `OpenAPI: ${key}`, hidden: true, pageOnly: true });
10439
10818
  }
10440
10819
  const seen = /* @__PURE__ */ new Set();
10820
+ const remoteByKey = /* @__PURE__ */ new Map();
10821
+ const mountDirectory = (remote, reference) => {
10822
+ const directory = reference.directory?.replace(/^\/+|\/+$/g, "");
10823
+ if (!remote || !directory) return;
10824
+ const mount = remote.directories?.find((existing) => existing.directory === directory);
10825
+ if (!mount) {
10826
+ remote.directories = [...remote.directories ?? [], { directory, ...reference.operations ? { operations: reference.operations } : {} }];
10827
+ } else if (mount.operations && reference.operations) {
10828
+ mount.operations = [.../* @__PURE__ */ new Set([...mount.operations, ...reference.operations])];
10829
+ } else {
10830
+ delete mount.operations;
10831
+ }
10832
+ };
10441
10833
  const specs = [];
10442
10834
  for (const reference of references) {
10443
10835
  const dedupeKey = `${reference.kind}:${reference.value}:${reference.tabLabel ?? ""}`;
10444
- if (seen.has(dedupeKey)) continue;
10836
+ if (seen.has(dedupeKey)) {
10837
+ mountDirectory(remoteByKey.get(dedupeKey), reference);
10838
+ continue;
10839
+ }
10445
10840
  seen.add(dedupeKey);
10446
10841
  const tabSuffix = reference.tabLabel ? ` (tab "${reference.tabLabel}")` : "";
10447
10842
  if (reference.kind === "asyncapi") {
@@ -10452,7 +10847,10 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, p
10452
10847
  continue;
10453
10848
  }
10454
10849
  if (/^https:\/\//i.test(reference.value)) {
10455
- remoteSpecs.push({ url: reference.value, ...reference.tabLabel ? { tabLabel: reference.tabLabel } : {}, ...reference.parentTab ? { parentTab: reference.parentTab } : {}, ...reference.icon ? { icon: reference.icon } : {}, ...reference.hidden ? { hidden: true } : {} });
10850
+ const remote = { url: reference.value, ...reference.tabLabel ? { tabLabel: reference.tabLabel } : {}, ...reference.parentTab ? { parentTab: reference.parentTab } : {}, ...reference.icon ? { icon: reference.icon } : {}, ...reference.hidden ? { hidden: true } : {} };
10851
+ mountDirectory(remote, reference);
10852
+ remoteSpecs.push(remote);
10853
+ remoteByKey.set(dedupeKey, remote);
10456
10854
  warnings.push({
10457
10855
  code: "unsupported-config",
10458
10856
  message: `The remote OpenAPI spec "${reference.value}"${tabSuffix} requires a network download before this import is complete.`,
@@ -10464,7 +10862,7 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, p
10464
10862
  warnings.push({ code: "unsupported-config", message: `The OpenAPI spec "${reference.value}"${tabSuffix} uses insecure HTTP and was not imported.`, source: reference.value });
10465
10863
  continue;
10466
10864
  }
10467
- const key = reference.value.split(/[?#]/, 1)[0].replace(/^\/+/, "").replace(/\\/g, "/");
10865
+ const key = canonicalSpecRef(reference.value);
10468
10866
  const match = files.find((file) => file.relativePath === key);
10469
10867
  if (!match) {
10470
10868
  warnings.push({
@@ -10493,21 +10891,7 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, p
10493
10891
  directory: reference.directory
10494
10892
  });
10495
10893
  }
10496
- const filenameOwners = /* @__PURE__ */ new Map();
10497
- const filenameSources = /* @__PURE__ */ new Map();
10498
- for (const spec of specs) {
10499
- const groupKey = spec.tabLabel ?? "\0default";
10500
- const owner = filenameOwners.get(spec.filename);
10501
- if (owner !== void 0 && owner !== groupKey && filenameSources.get(spec.filename) !== spec.sourcePath) {
10502
- let disambiguated = spec.sourcePath.replace(/\//g, "-");
10503
- while (filenameOwners.has(disambiguated) && filenameOwners.get(disambiguated) !== groupKey) {
10504
- disambiguated = `${groupKey.replace(/\W+/g, "-")}-${disambiguated}`;
10505
- }
10506
- spec.filename = disambiguated;
10507
- }
10508
- filenameOwners.set(spec.filename, groupKey);
10509
- filenameSources.set(spec.filename, spec.sourcePath);
10510
- }
10894
+ for (const spec of specs) spec.filename = nameSpec(spec);
10511
10895
  return specs;
10512
10896
  }
10513
10897
  function rewriteMintlifyPageSpecRefs(pages, specs, remoteUrls, warnings, explainMissing = () => "no such file was found") {
@@ -10520,12 +10904,13 @@ function rewriteMintlifyPageSpecRefs(pages, specs, remoteUrls, warnings, explain
10520
10904
  }
10521
10905
  continue;
10522
10906
  }
10523
- const key = ref.specRef.split(/[?#]/, 1)[0].replace(/\\/g, "/").replace(/^(?:\.\/)+/, "").replace(/^\/+/, "");
10524
- const byPath = specs.find((spec) => spec.sourcePath === key);
10525
- const byName = specs.filter((spec) => spec.filename.toLowerCase() === specRefBaseName(key));
10526
- const match = byPath ?? (byName.length === 1 ? byName[0] : void 0);
10527
- if (match) {
10528
- page.openapi = withSpecRef(ref, specAssetPath(match.filename));
10907
+ const key = canonicalSpecRef(ref.specRef);
10908
+ const resolved = resolveSpecRef(ref.specRef, specs);
10909
+ if (resolved.kind === "match") {
10910
+ page.openapi = withSpecRef(ref, specAssetPath(resolved.spec.filename));
10911
+ } else if (resolved.kind === "ambiguous") {
10912
+ const candidates = resolved.candidates.map((spec) => spec.sourcePath ?? spec.filename).join(", ");
10913
+ warnings.push({ code: "unsupported-config", message: `Page "${page.id}" references the OpenAPI spec "${ref.specRef}", which fits more than one spec (${candidates}), so it was left as written and the page will not render an endpoint. Write the full path of the spec in the page's openapi frontmatter.`, source: page.source });
10529
10914
  } else {
10530
10915
  warnings.push({ code: "unsupported-config", message: `Page "${page.id}" references the OpenAPI spec "${ref.specRef}", which was not migrated (${explainMissing(key)}); the page will not render an endpoint.`, source: page.source });
10531
10916
  }
@@ -10552,10 +10937,15 @@ function thallyOperationSlugSegments(path, method) {
10552
10937
  function slugifyApiSpecId(value) {
10553
10938
  return value.toLowerCase().replace(/[^a-z0-9/]+/g, "-").replace(/(^-|-$)+/g, "").replace(/\//g, "-") || value.toLowerCase();
10554
10939
  }
10940
+ function apiTabSpecIds(tabs) {
10941
+ return tabs.filter((tab) => !tab.hidden && tab.api).map((tab, index) => ({ tab, specId: index === 0 ? "default" : slugifyApiSpecId(tab.tab) }));
10942
+ }
10943
+ var flagged = (value) => value === true || value === "true";
10944
+ var isWithheld = (node) => flagged(node["x-hidden"]) || flagged(node["x-excluded"]);
10555
10945
  function parseOpenApiOperationsForLinking(content) {
10556
10946
  let document;
10557
10947
  try {
10558
- document = parseYaml6(content.toString("utf8"));
10948
+ document = parseSpecDocument(content.toString("utf8")).document;
10559
10949
  } catch {
10560
10950
  return [];
10561
10951
  }
@@ -10564,10 +10954,13 @@ function parseOpenApiOperationsForLinking(content) {
10564
10954
  const operations = [];
10565
10955
  for (const [path, pathItem] of Object.entries(paths)) {
10566
10956
  if (!pathItem || typeof pathItem !== "object") continue;
10957
+ const item = pathItem;
10958
+ if (isWithheld(item)) continue;
10567
10959
  for (const method of OPENAPI_HTTP_METHODS) {
10568
- const operation = pathItem[method];
10960
+ const operation = item[method];
10569
10961
  if (!operation || typeof operation !== "object") continue;
10570
10962
  const op = operation;
10963
+ if (isWithheld(op)) continue;
10571
10964
  const tags = Array.isArray(op.tags) ? op.tags.filter((tag) => typeof tag === "string") : [];
10572
10965
  operations.push({
10573
10966
  method,
@@ -10580,19 +10973,39 @@ function parseOpenApiOperationsForLinking(content) {
10580
10973
  }
10581
10974
  return operations;
10582
10975
  }
10583
- function apiOperationLinkMap(sources, docsConfig) {
10976
+ function servedOperations(source, parsed) {
10977
+ let operations = parsed.get(source.filename);
10978
+ if (!operations) {
10979
+ operations = parseOpenApiOperationsForLinking(source.content);
10980
+ parsed.set(source.filename, operations);
10981
+ }
10982
+ if (!source.operations) return operations;
10983
+ const byKey = new Map(operations.map((operation) => [`${operation.method} ${operation.path}`, operation]));
10984
+ return source.operations.flatMap((reference) => {
10985
+ const [method, ...path] = reference.trim().split(/\s+/);
10986
+ const operation = byKey.get(`${method.toLowerCase()} ${path.join(" ")}`);
10987
+ return operation ? [operation] : [];
10988
+ });
10989
+ }
10990
+ var mountPrefix = (directory) => directory.replace(/^\/+|\/+$/g, "").toLowerCase();
10991
+ function apiOperationLinkMap(sources, docsConfig, otherMounts = []) {
10584
10992
  const map = /* @__PURE__ */ new Map();
10585
10993
  const prefixLandings = /* @__PURE__ */ new Map();
10586
10994
  const mintlifyPages = /* @__PURE__ */ new Map();
10587
10995
  const duplicatePages = [];
10588
- const apiTabs = docsConfig.tabs.filter((tab) => !tab.hidden && tab.api);
10589
- apiTabs.forEach((tab, index) => {
10590
- const source = sources.find((entry) => tab.api?.source === specAssetPath(entry.filename));
10591
- if (!source || !source.prefix) return;
10592
- const specId = index === 0 ? "default" : slugifyApiSpecId(tab.tab);
10593
- const prefix = source.prefix.replace(/^\/+|\/+$/g, "").toLowerCase();
10996
+ const parsedByFile = /* @__PURE__ */ new Map();
10997
+ 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 })));
10998
+ const mounts = new Set([...bound.map(({ source }) => source.prefix ? mountPrefix(source.prefix) : ""), ...otherMounts.map(mountPrefix)].filter(Boolean));
10999
+ const underDeeperMount = (route, own2) => {
11000
+ const lower = route.toLowerCase();
11001
+ return [...mounts].some((mount) => mount.length > own2.length && (lower === mount || lower.startsWith(`${mount}/`)));
11002
+ };
11003
+ bound.forEach(({ source, specId }) => {
11004
+ if (!source.prefix) return;
11005
+ const base = source.prefix.replace(/^\/+|\/+$/g, "");
11006
+ const prefix = mountPrefix(source.prefix);
10594
11007
  if (prefix) prefixLandings.set(prefix, specId === "default" ? "/api" : `/api/${specId}`);
10595
- for (const operation of parseOpenApiOperationsForLinking(source.content)) {
11008
+ for (const operation of servedOperations(source, parsedByFile)) {
10596
11009
  const thallyHref = `/api/${specId}/${thallyOperationSlugSegments(operation.path, operation.method).join("/")}`;
10597
11010
  const leafCandidates = /* @__PURE__ */ new Set();
10598
11011
  if (operation.operationId) {
@@ -10608,31 +11021,66 @@ function apiOperationLinkMap(sources, docsConfig) {
10608
11021
  const tagSegment = mintlifyOperationSlugSegment(operation.tag ?? "default");
10609
11022
  const mintlifyTag = mintlifyUrlSlug(operation.tag ?? "default");
10610
11023
  const mintlifyLeaf = mintlifyUrlSlug(operation.summary?.trim() || operation.operationId?.trim() || `${operation.method} ${operation.path}`);
10611
- const canonical2 = `${source.prefix}/${mintlifyTag}/${mintlifyLeaf}`.replace(/^\/+|\/+$/g, "");
10612
- if (mintlifyTag && mintlifyLeaf) {
11024
+ const canonical2 = `${base}/${mintlifyTag}/${mintlifyLeaf}`.replace(/^\/+|\/+$/g, "");
11025
+ if (mintlifyTag && mintlifyLeaf && !underDeeperMount(canonical2, prefix)) {
10613
11026
  if (!mintlifyPages.has(canonical2)) mintlifyPages.set(canonical2, thallyHref);
10614
11027
  else if (mintlifyPages.get(canonical2) !== thallyHref && !duplicatePages.includes(canonical2)) duplicatePages.push(canonical2);
11028
+ if (!map.has(canonical2.toLowerCase())) map.set(canonical2.toLowerCase(), thallyHref);
11029
+ }
11030
+ const tagRoot = `${base}/${mintlifyTag}`.replace(/^\/+|\/+$/g, "");
11031
+ if (mintlifyTag && !underDeeperMount(tagRoot, prefix)) {
11032
+ if (!mintlifyPages.has(tagRoot)) mintlifyPages.set(tagRoot, thallyHref);
11033
+ if (!map.has(tagRoot.toLowerCase())) map.set(tagRoot.toLowerCase(), thallyHref);
10615
11034
  }
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
11035
  for (const leaf of leafCandidates) {
10621
11036
  if (!leaf) continue;
10622
- const tagged = `${source.prefix}/${tagSegment}/${leaf}`.replace(/^\/+|\/+$/g, "").toLowerCase();
10623
- const flat = `${source.prefix}/${leaf}`.replace(/^\/+|\/+$/g, "").toLowerCase();
10624
- if (!map.has(tagged)) map.set(tagged, thallyHref);
10625
- if (!map.has(flat)) map.set(flat, thallyHref);
11037
+ const tagged = `${base}/${tagSegment}/${leaf}`.replace(/^\/+|\/+$/g, "").toLowerCase();
11038
+ const flat = `${base}/${leaf}`.replace(/^\/+|\/+$/g, "").toLowerCase();
11039
+ if (!map.has(tagged) && !underDeeperMount(tagged, prefix)) map.set(tagged, thallyHref);
11040
+ if (!map.has(flat) && !underDeeperMount(flat, prefix)) map.set(flat, thallyHref);
10626
11041
  }
10627
11042
  }
10628
11043
  });
10629
11044
  return { operationLinks: map, prefixLandings, mintlifyPages, duplicatePages };
10630
11045
  }
10631
- function rewriteApiLinksInPages(pages, operationLinks, prefixLandings, warnings) {
11046
+ function mintlifyEndpointRedirects(maps, pages, existing, warnings) {
11047
+ const { mintlifyPages, duplicatePages } = maps;
11048
+ if (duplicatePages.length > 0) {
11049
+ const listed = duplicatePages.slice(0, 10).map((url) => `/${url}`).join(", ");
11050
+ warnings.push({
11051
+ code: "unsupported-config",
11052
+ 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 ? ", ..." : ""}.`
11053
+ });
11054
+ }
11055
+ const taken = servedRoutes(pages);
11056
+ for (const redirect of existing) taken.add(literalRoute(redirect.source) ?? redirect.source);
11057
+ const candidates = [...mintlifyPages].filter(([source]) => !taken.has(literalRoute(`/${source}`) ?? `/${source}`));
11058
+ const redirects = candidates.slice(0, MAX_ENDPOINT_REDIRECTS).map(([source, destination]) => ({ source: `/${source}`, destination }));
11059
+ const capped = candidates.length - redirects.length;
11060
+ if (capped > 0) {
11061
+ warnings.push({
11062
+ code: "unsupported-config",
11063
+ 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.`
11064
+ });
11065
+ }
11066
+ return redirects;
11067
+ }
11068
+ function linkApiMounts(sources, docsConfig, pages, warnings, reservedMounts = []) {
11069
+ const maps = apiOperationLinkMap(sources, docsConfig, reservedMounts);
11070
+ rewriteApiLinksInPages(pages, maps.operationLinks, maps.prefixLandings, warnings, { mounts: reservedMounts, redirects: docsConfig.redirects });
11071
+ return mintlifyEndpointRedirects(maps, pages, docsConfig.redirects ?? [], warnings);
11072
+ }
11073
+ function rewriteApiLinksInPages(pages, operationLinks, prefixLandings, warnings, answered = {}) {
10632
11074
  if (operationLinks.size === 0 && prefixLandings.size === 0) return;
10633
11075
  const unmatched = /* @__PURE__ */ new Set();
11076
+ const served = servedRoutes(pages);
11077
+ for (const { source } of answered.redirects ?? []) {
11078
+ const route = literalRoute(source);
11079
+ if (route) served.add(route);
11080
+ }
11081
+ const others = (answered.mounts ?? []).map(mountPrefix).filter(Boolean);
10634
11082
  for (const page of pages) {
10635
- page.body = rewriteApiOperationLinks(page.body, operationLinks, prefixLandings, unmatched);
11083
+ page.body = rewriteApiOperationLinks(page.body, operationLinks, prefixLandings, served, unmatched, others);
10636
11084
  }
10637
11085
  if (unmatched.size === 0) return;
10638
11086
  const sorted = [...unmatched].sort();
@@ -10641,7 +11089,7 @@ function rewriteApiLinksInPages(pages, operationLinks, prefixLandings, warnings)
10641
11089
  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
11090
  });
10643
11091
  }
10644
- function rewriteApiOperationLinks(body, linkMap, prefixLandings, unmatched) {
11092
+ function rewriteApiOperationLinks(body, linkMap, prefixLandings, served, unmatched, otherMounts) {
10645
11093
  if (linkMap.size === 0 && prefixLandings.size === 0) return body;
10646
11094
  const orderedPrefixes = [...prefixLandings.entries()].sort((a, b) => b[0].length - a[0].length);
10647
11095
  let codeFence = null;
@@ -10655,8 +11103,12 @@ function rewriteApiOperationLinks(body, linkMap, prefixLandings, unmatched) {
10655
11103
  if (codeFence) return line;
10656
11104
  const rewriteTarget = (target2) => {
10657
11105
  const suffixIndex = target2.search(/[?#]/);
10658
- const path = (suffixIndex >= 0 ? target2.slice(0, suffixIndex) : target2).replace(/^\/+/, "").toLowerCase();
11106
+ const literal = suffixIndex >= 0 ? target2.slice(0, suffixIndex) : target2;
11107
+ if (served.has(literalRoute(literal) ?? literal)) return target2;
11108
+ const path = literal.replace(/^\/+|\/+$/g, "").toLowerCase();
10659
11109
  const suffix = suffixIndex >= 0 ? target2.slice(suffixIndex) : "";
11110
+ const owner = [...prefixLandings.keys(), ...otherMounts].filter((mount) => path === mount || path.startsWith(`${mount}/`)).reduce((longest, mount) => longest === null || mount.length > longest.length ? mount : longest, null);
11111
+ if (owner !== null && !prefixLandings.has(owner)) return target2;
10660
11112
  const mapped = linkMap.get(path);
10661
11113
  if (mapped) return `${mapped}${suffix}`;
10662
11114
  for (const [prefix, landing] of orderedPrefixes) {
@@ -11789,9 +12241,12 @@ function injectOpenApiSpecs(config, specs, warnings) {
11789
12241
  const apiTab = spec.tabLabel ? tabs.find((tab) => tab.tab === spec.tabLabel) : tabs.find((tab) => tab.tab.toLowerCase().includes("api"));
11790
12242
  if (apiTab) {
11791
12243
  if (apiTab.api && warnings) {
12244
+ const source = specAssetPath(spec.filename);
12245
+ const pageBound = spec.pageReferenced && !tabs.some((tab) => tab.api?.source === source);
12246
+ if (pageBound) insertApiTab(tabs, { tab: `OpenAPI: ${spec.sourcePath ?? spec.filename}`, hidden: true, api: { source } });
11792
12247
  warnings.push({
11793
12248
  code: "unsupported-config",
11794
- message: `Tab "${apiTab.tab}" already uses another OpenAPI spec, and Thally supports one API spec per tab, so "${specAssetPath(spec.filename)}" was not added. Put it in its own tab to include it.`
12249
+ message: `Tab "${apiTab.tab}" already uses another OpenAPI spec, and Thally supports one API spec per tab, so "${source}" ${pageBound ? "gets no endpoint list of its own; the pages that name it still show their endpoint. Put it in its own tab to list all its endpoints." : "was not added. Put it in its own tab to include it."}`
11795
12250
  });
11796
12251
  continue;
11797
12252
  }
@@ -12028,6 +12483,7 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
12028
12483
  const sourceRefPages = [];
12029
12484
  const sourceRefAssets = [];
12030
12485
  const remoteApiSpecs = [];
12486
+ const localApiMounts = [];
12031
12487
  const referencedAssetPaths = /* @__PURE__ */ new Map();
12032
12488
  const addAssetReference = (assetPath, referencingPage) => {
12033
12489
  const referrers = referencedAssetPaths.get(assetPath);
@@ -12049,6 +12505,7 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
12049
12505
  const gateByPath = /* @__PURE__ */ new Map();
12050
12506
  const withheldSpecRefs = [];
12051
12507
  const publishedSpecRefs = [];
12508
+ const publishedSpecPages = [];
12052
12509
  const withheldPaths = /* @__PURE__ */ new Set();
12053
12510
  const isDocFile = (file) => [".md", ".mdx"].includes(extname6(file.relativePath).toLowerCase());
12054
12511
  const isGateCandidate = (file) => isDocFile(file) && !file.relativePath.split("/").some((segment) => SNIPPET_DIRECTORIES.has(segment.toLowerCase()));
@@ -12085,6 +12542,7 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
12085
12542
  const verdict = classifyPageGate(file);
12086
12543
  gateByPath.set(file.absolutePath, verdict);
12087
12544
  if (verdict.openapi) (verdict.reason ? withheldSpecRefs : publishedSpecRefs).push(verdict.openapi);
12545
+ if (verdict.openapi && !verdict.reason) publishedSpecPages.push({ ref: verdict.openapi, file: file.relativePath });
12088
12546
  if (verdict.reason) withheldPaths.add(file.absolutePath);
12089
12547
  }
12090
12548
  for (const file of mintignoreFilteredFiles) {
@@ -12188,6 +12646,23 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
12188
12646
  let discovered = files.length;
12189
12647
  const localeConfig = docsConfig.i18n;
12190
12648
  const defaultPageIds = new Set([...referenceMap.values()].filter((reference) => !reference.locale || reference.locale === localeConfig?.defaultLocale).map((reference) => reference.navigationId));
12649
+ const resolvePageIdentity = (path) => {
12650
+ const referenced = exactReferenceMap.get(exactReferenceKey(path)) ?? referenceMap.get(normalizedReferenceKey(path));
12651
+ let locale = referenced?.locale;
12652
+ let navigationId = referenced?.navigationId ?? pageIdFromReference(path, platform === "mintlify");
12653
+ if (!referenced && localeConfig) {
12654
+ for (const entry of localeConfig.locales) {
12655
+ const localized = mintlifyLocalizedReference(path, entry.code, defaultPageIds);
12656
+ if (localized === path) continue;
12657
+ locale = entry.code;
12658
+ navigationId = pageIdFromReference(localized, platform === "mintlify");
12659
+ break;
12660
+ }
12661
+ }
12662
+ if (!navigationId) return null;
12663
+ const isDefaultLocale = !locale || locale === localeConfig?.defaultLocale;
12664
+ return { locale, navigationId, id: isDefaultLocale ? navigationId : `${locale}/${navigationId}` };
12665
+ };
12191
12666
  const routeAliases = [];
12192
12667
  const pageFiles = [...files].sort((left, right) => {
12193
12668
  const leftOrder = referenceOrder.get(normalizedReferenceKey(left.relativePath)) ?? Number.MAX_SAFE_INTEGER;
@@ -12283,7 +12758,6 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
12283
12758
  }
12284
12759
  continue;
12285
12760
  }
12286
- const key = normalizedReferenceKey(file.relativePath);
12287
12761
  let pageGate;
12288
12762
  {
12289
12763
  pageGate = gateByPath.get(file.absolutePath);
@@ -12333,25 +12807,13 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
12333
12807
  }
12334
12808
  }
12335
12809
  const isRestrictedPage = Boolean(pageGate?.reason);
12336
- const referenced = exactReferenceMap.get(exactReferenceKey(file.relativePath)) ?? referenceMap.get(key);
12337
- let locale = referenced?.locale;
12338
- let navigationId = referenced?.navigationId ?? pageIdFromReference(file.relativePath, platform === "mintlify");
12339
- if (!referenced && localeConfig) {
12340
- for (const entry of localeConfig.locales) {
12341
- const localized = mintlifyLocalizedReference(file.relativePath, entry.code, defaultPageIds);
12342
- if (localized === file.relativePath) continue;
12343
- locale = entry.code;
12344
- navigationId = pageIdFromReference(localized, platform === "mintlify");
12345
- break;
12346
- }
12347
- }
12348
- if (!navigationId) {
12810
+ const identity = resolvePageIdentity(file.relativePath);
12811
+ if (!identity) {
12349
12812
  skipped++;
12350
12813
  warnings.push({ code: "invalid-page", message: "Page path could not be normalized safely.", source: file.relativePath });
12351
12814
  continue;
12352
12815
  }
12353
- const isDefaultLocale = !locale || locale === localeConfig?.defaultLocale;
12354
- const id = isDefaultLocale ? navigationId : `${locale}/${navigationId}`;
12816
+ const { locale, navigationId, id } = identity;
12355
12817
  let raw = inlineMdxSnippets(
12356
12818
  mintlifyProjectRoot ? importUndeclaredSnippetComponents(readFileSync6(file.absolutePath, "utf8"), file.absolutePath, repositoryDir, mintlifyProjectRoot, snippetExports, snippetAliases, inlineGate, warnings) : injectDocusaurusGlobalImports(readFileSync6(file.absolutePath, "utf8"), globalDocusaurusImports),
12357
12819
  file.absolutePath,
@@ -12994,14 +13456,27 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
12994
13456
  resolvedSpecs.map((spec) => ({ filename: spec.filename, content: spec.content, prefix: spec.routeSegments.join("/") })),
12995
13457
  docsConfig
12996
13458
  );
12997
- rewriteApiLinksInPages(pages, operationLinks, prefixLandings, warnings);
13459
+ rewriteApiLinksInPages(pages, operationLinks, prefixLandings, warnings, { redirects: docsConfig.redirects });
12998
13460
  }
12999
13461
  } else if (platform === "mintlify") {
13000
13462
  const warnSharedSpec = (path, excluded = []) => {
13001
13463
  warnings.push({ code: "gated-page", message: sharedSpecMessage(path, excluded), source: path });
13002
13464
  };
13003
- const keptSpecRefs = [...publishedSpecRefs, ...navigationOperationRefs(mintlifyConfig)];
13004
- const allSpecs = resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteApiSpecs, pages.filter((page) => !restrictedPages.has(page)));
13465
+ const nameSpec = createSpecAssetNamer();
13466
+ const allSpecs = resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteApiSpecs, nameSpec, pages.filter((page) => !restrictedPages.has(page)));
13467
+ const navigation = navigationSpecContext(mintlifyConfig, (path) => resolvePageIdentity(path)?.id ?? null);
13468
+ const defaultSources = /* @__PURE__ */ new Set([
13469
+ ...allSpecs.filter((spec) => !spec.pageOnly && spec.tabLabel === void 0).map((spec) => spec.sourcePath),
13470
+ ...remoteApiSpecs.filter((spec) => spec.tabLabel === void 0).map((spec) => spec.url)
13471
+ ]);
13472
+ const keptOperations = (fallbackSource) => {
13473
+ const defaultSpec = defaultSources.size === 0 ? fallbackSource : defaultSources.size === 1 ? [...defaultSources][0] : void 0;
13474
+ const candidates = fallbackSource ? [{ sourcePath: fallbackSource, filename: basename3(fallbackSource) }, ...allSpecs] : allSpecs;
13475
+ const { kept, unowned } = keptOperationRefs(publishedSpecPages, navigation, defaultSpec, candidates);
13476
+ return { kept: kept.filter((ref) => !namesSeveralSpecs(ref, allSpecs)), unowned };
13477
+ };
13478
+ let { kept: keptSpecRefs, unowned: unownedOperations } = keptOperations();
13479
+ const withheldKeys = /* @__PURE__ */ new Set();
13005
13480
  for (const remote of remoteApiSpecs) {
13006
13481
  const { withheld, kept } = withheldOperationKeys({ sourcePath: remote.url, filename: remote.url }, withheldSpecRefs, keptSpecRefs);
13007
13482
  if (withheld.length > 0) {
@@ -13011,9 +13486,10 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
13011
13486
  }
13012
13487
  const withheldOperations = /* @__PURE__ */ new Map();
13013
13488
  for (const spec of allSpecs) {
13014
- const result = excludeWithheldOperations(spec, withheldSpecRefs, keptSpecRefs);
13489
+ const result = excludeWithheldOperations(spec, withheldSpecRefs, keptSpecRefs, allSpecs);
13015
13490
  spec.content = result.content;
13016
13491
  withheldOperations.set(spec, result.excluded);
13492
+ result.excluded.forEach((operation) => withheldKeys.add(operationKey(operation)));
13017
13493
  }
13018
13494
  const resolvedSpecs = allSpecs.filter((spec) => !spec.pageOnly);
13019
13495
  const pageOnlySpecs = allSpecs.filter((spec) => spec.pageOnly);
@@ -13021,53 +13497,41 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
13021
13497
  if (!assets.some((asset) => asset.path === specAssetPath(spec.filename))) {
13022
13498
  assets.push(specAsset(spec.filename, spec.content));
13023
13499
  }
13024
- if (withheldSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, spec.filename, true))) warnSharedSpec(spec.sourcePath, withheldOperations.get(spec));
13500
+ if (withheldSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, true))) warnSharedSpec(spec.sourcePath, withheldOperations.get(spec));
13025
13501
  }
13026
13502
  let pageSpecs = resolvedSpecs;
13027
13503
  if (resolvedSpecs.length > 0) {
13504
+ const namedByPages = new Set(pages.filter((page) => !restrictedPages.has(page)).map((page) => specNamedByPage(page.openapi, resolvedSpecs)));
13028
13505
  for (const spec of resolvedSpecs) {
13029
- spec.generatedNavigation = Boolean(spec.directory) && !publishedSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, spec.filename, true));
13506
+ spec.pageReferenced = namedByPages.has(spec);
13507
+ spec.generatedNavigation = Boolean(spec.directory) && !publishedSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, true));
13030
13508
  }
13031
13509
  docsConfig = injectOpenApiSpecs(docsConfig, resolvedSpecs, warnings);
13032
- const { operationLinks, prefixLandings, mintlifyPages, duplicatePages } = apiOperationLinkMap(
13033
- resolvedSpecs.map((spec) => ({ filename: spec.filename, content: spec.content, prefix: spec.directory })),
13034
- docsConfig
13035
- );
13036
- rewriteApiLinksInPages(pages, operationLinks, prefixLandings, warnings);
13037
- if (duplicatePages.length > 0) {
13038
- const listed = duplicatePages.slice(0, 10).map((url) => `/${url}`).join(", ");
13039
- warnings.push({
13040
- code: "unsupported-config",
13041
- 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 ? ", ..." : ""}.`
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
- });
13510
+ if (remoteApiSpecs.length > 0) {
13511
+ localApiMounts.push(...resolvedSpecs.flatMap((spec) => spec.directory ? [{ filename: spec.filename, directory: spec.directory, ...spec.tabLabel ? { tab: spec.tabLabel } : {} }] : []));
13512
+ } else {
13513
+ const endpointRedirects = linkApiMounts(
13514
+ resolvedSpecs.map((spec) => ({ filename: spec.filename, content: spec.content, prefix: spec.directory, ...spec.tabLabel ? { tab: spec.tabLabel } : {} })),
13515
+ docsConfig,
13516
+ pages,
13517
+ warnings
13518
+ );
13519
+ if (endpointRedirects.length > 0) docsConfig = { ...docsConfig, redirects: [...docsConfig.redirects ?? [], ...endpointRedirects] };
13058
13520
  }
13059
- if (endpointRedirects.length > 0) docsConfig = { ...docsConfig, redirects: [...docsConfig.redirects ?? [], ...endpointRedirects] };
13060
13521
  } else {
13061
13522
  const fallback = findOpenApi(files);
13062
13523
  const fallbackContent = fallback ? readSpecFile(fallback.absolutePath, fallback.relativePath, warnings) : void 0;
13063
13524
  if (fallback && fallbackContent) {
13064
- const filename = basename3(fallback.relativePath);
13065
- const matches = (ref) => specRefMatches(ref, fallback.relativePath, filename, true);
13525
+ const matches = (ref) => specRefMatches(ref, fallback.relativePath, true);
13066
13526
  if (withheldSpecRefs.some(matches) && !publishedSpecRefs.some(matches)) {
13067
- quarantinedFiles.push({ path: `${QUARANTINE_DIRECTORY}/assets/${specAssetPath(filename)}`, content: fallbackContent });
13527
+ quarantinedFiles.push({ path: `${QUARANTINE_DIRECTORY}/assets/${specAssetPath(basename3(fallback.relativePath))}`, content: fallbackContent });
13068
13528
  withheldAssetCount++;
13069
13529
  } else {
13070
- const pruned = excludeWithheldOperations({ sourcePath: fallback.relativePath, filename, content: fallbackContent }, withheldSpecRefs, keptSpecRefs);
13530
+ const filename = nameSpec({ sourcePath: fallback.relativePath, filename: basename3(fallback.relativePath) });
13531
+ const fallbackSpec = { sourcePath: fallback.relativePath, filename, content: fallbackContent };
13532
+ ({ kept: keptSpecRefs, unowned: unownedOperations } = keptOperations(fallback.relativePath));
13533
+ const pruned = excludeWithheldOperations(fallbackSpec, withheldSpecRefs, keptSpecRefs, [fallbackSpec, ...allSpecs]);
13534
+ pruned.excluded.forEach((operation) => withheldKeys.add(operationKey(operation)));
13071
13535
  if (!assets.some((asset) => asset.path === specAssetPath(filename))) {
13072
13536
  assets.push(specAsset(filename, pruned.content));
13073
13537
  }
@@ -13079,13 +13543,22 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
13079
13543
  }
13080
13544
  for (const spec of pageOnlySpecs) {
13081
13545
  if (!assets.some((asset) => asset.path === specAssetPath(spec.filename))) assets.push(specAsset(spec.filename, spec.content));
13082
- if (withheldSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, spec.filename, true))) warnSharedSpec(spec.sourcePath, withheldOperations.get(spec));
13546
+ if (withheldSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, true))) warnSharedSpec(spec.sourcePath, withheldOperations.get(spec));
13083
13547
  }
13084
13548
  if (pageOnlySpecs.length > 0) {
13085
13549
  docsConfig = injectOpenApiSpecs(docsConfig, pageOnlySpecs, warnings);
13086
13550
  pageSpecs = [...pageSpecs, ...pageOnlySpecs];
13087
13551
  }
13552
+ const notKeptPublic = [...new Set(unownedOperations.filter((operation) => withheldKeys.has(operationKey(operation))))];
13553
+ if (notKeptPublic.length > 0) {
13554
+ warnings.push({
13555
+ code: "gated-page",
13556
+ message: `${notKeptPublic.length} operation(s) withheld from a published OpenAPI spec (${notKeptPublic.join(", ")}) are also named by a published page or navigation entry that does not say which spec it means, so that reference did not keep them public. Write the spec in its openapi value (for example "openapi.json GET /x") to publish the operation.`
13557
+ });
13558
+ }
13088
13559
  const explainMissingSpec = (key) => {
13560
+ const fits = resolveSpecRef(key, specFileCandidates(files), false);
13561
+ if (fits.kind === "ambiguous") return `several spec files fit that name (${fits.candidates.map((spec) => spec.sourcePath).join(", ")}); write the full path in the page's openapi frontmatter`;
13089
13562
  const sameName = scannedFiles.find((file) => file.relativePath.toLowerCase() === key.toLowerCase());
13090
13563
  if (sameName && sameName.relativePath !== key) return `paths are case-sensitive; the file is at "${sameName.relativePath}"`;
13091
13564
  if (sameName && mintignoreMatcher?.ignores(sameName.relativePath)) return "the file is excluded by .mintignore";
@@ -13099,8 +13572,9 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
13099
13572
  if (docsConfig.tabs.some((tab) => tab.api)) docsConfig = { ...docsConfig, api: { ...docsConfig.api, specLink: false } };
13100
13573
  }
13101
13574
  if (platform === "mintlify") {
13102
- const sources = new Set((docsConfig.redirects ?? []).map((redirect) => redirect.source));
13103
- const aliases = routeAliases.filter((redirect) => !sources.has(redirect.source));
13575
+ const sources = new Set((docsConfig.redirects ?? []).map((redirect) => redirect.source.toLowerCase()));
13576
+ const served = servedRoutes(pages);
13577
+ const aliases = routeAliases.filter((redirect) => !sources.has(redirect.source.toLowerCase()) && !served.has(literalRoute(redirect.source) ?? redirect.source));
13104
13578
  if (aliases.length > 0) docsConfig = { ...docsConfig, redirects: [...docsConfig.redirects ?? [], ...aliases] };
13105
13579
  docsConfig = addMintlifyHomepageRedirects(addMintlifyDirectoryRedirects(docsConfig, pages), pages);
13106
13580
  const reachablePaths = /* @__PURE__ */ new Set([
@@ -13314,26 +13788,7 @@ ${body}`;
13314
13788
  }
13315
13789
  }
13316
13790
  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
- };
13791
+ docsConfig = dropConflictingRedirects(dropUnroutableRedirects(docsConfig, warnings), [...pages, ...sourceRefPages], warnings);
13337
13792
  }
13338
13793
  if (pages.length === 0) {
13339
13794
  const reasons = warnings.map((warning) => warning.message);
@@ -13443,10 +13898,10 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
13443
13898
  const sourceFavicon = mintlifyConfig?.favicon ?? fernRawConfig?.favicon ?? docusaurusSettings?.favicon;
13444
13899
  const logo = brandVariants(sourceLogo);
13445
13900
  const favicon = brandVariants(sourceFavicon);
13446
- const logoLight = publicBrandPath(logo.light);
13447
13901
  const logoDark = publicBrandPath(logo.dark);
13448
- const faviconLight = publicBrandPath(favicon.light);
13902
+ const logoLight = publicBrandPath(logo.light) ?? logoDark;
13449
13903
  const faviconDark = publicBrandPath(favicon.dark);
13904
+ const faviconLight = publicBrandPath(favicon.light) ?? faviconDark;
13450
13905
  if (logoLight) docsConfig = {
13451
13906
  ...docsConfig,
13452
13907
  navbar: {
@@ -13475,15 +13930,30 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
13475
13930
  const decoration = background.decoration;
13476
13931
  const image = publicBrandPath(background.image);
13477
13932
  const imageDark = publicBrandPath(background.imageDark);
13478
- docsConfig = {
13479
- ...docsConfig,
13480
- background: {
13481
- ...docsConfig.background,
13482
- ...decoration === "none" || decoration === "grid" || decoration === "gradient" ? { decoration } : {},
13483
- ...image ? { image } : {},
13484
- ...imageDark ? { imageDark } : {}
13485
- }
13933
+ const mapped = {
13934
+ ...docsConfig.background,
13935
+ ...decoration === "none" || decoration === "grid" || decoration === "gradient" ? { decoration } : {},
13936
+ ...image ? { image } : {},
13937
+ ...imageDark ? { imageDark } : {}
13486
13938
  };
13939
+ if (Object.keys(mapped).length > 0) docsConfig = { ...docsConfig, background: mapped };
13940
+ const canvas = background.color && typeof background.color === "object" ? background.color : {};
13941
+ for (const mode of ["light", "dark"]) {
13942
+ if (canvas[mode] === void 0) continue;
13943
+ const hex = typeof canvas[mode] === "string" ? cssColorToHex(canvas[mode]) : void 0;
13944
+ if (!hex) {
13945
+ warnings.push({
13946
+ code: "unsupported-config",
13947
+ message: `background.color.${mode} ${JSON.stringify(canvas[mode])?.slice(0, 40)} is not a hex or rgb() colour (for example #121317) and was dropped, so that mode keeps Thally's default canvas. Use a hex value in docs.json to set it.`,
13948
+ source: "docs.json"
13949
+ });
13950
+ continue;
13951
+ }
13952
+ docsConfig = {
13953
+ ...docsConfig,
13954
+ colors: { ...docsConfig.colors, [mode]: { ...docsConfig.colors?.[mode], background: hex } }
13955
+ };
13956
+ }
13487
13957
  }
13488
13958
  }
13489
13959
  if (platform === "docusaurus") {
@@ -13493,13 +13963,23 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
13493
13963
  ...docsConfig,
13494
13964
  favicon: { light: faviconLight, ...faviconDark ? { dark: faviconDark } : {} }
13495
13965
  };
13496
- const missingBrand = [
13497
- ...[logo.light, logo.dark].filter((value) => typeof value === "string" && !publicBrandPath(value)),
13498
- ...[favicon.light, favicon.dark].filter((value) => typeof value === "string" && !publicBrandPath(value))
13499
- ];
13966
+ const notImported = (variants) => [variants.light, variants.dark].filter((value) => typeof value === "string" && !publicBrandPath(value));
13967
+ const missingLogo = notImported(logo);
13968
+ const missingFavicon = notImported(favicon);
13969
+ const missingBrand = [...missingLogo, ...missingFavicon];
13970
+ const substituted = [
13971
+ ...missingLogo.length > 0 && logoLight ? [`logo (${logoLight})`] : [],
13972
+ ...missingFavicon.length > 0 && faviconLight ? [`favicon (${faviconLight})`] : []
13973
+ ].map((asset) => ` The imported ${asset} is used for both light and dark mode.`).join("");
13500
13974
  if (missingBrand.length > 0) warnings.push({
13501
13975
  code: "unsupported-config",
13502
- message: `Brand asset(s) were referenced but not imported: ${[...new Set(missingBrand)].join(", ")}. Add these files under public/ or update docs.json.`
13976
+ message: `Brand asset(s) were referenced but not imported: ${[...new Set(missingBrand)].join(", ")}. Add these files under public/ or update docs.json.${substituted}`
13977
+ });
13978
+ const logoHref = sourceLogo && typeof sourceLogo === "object" ? sourceLogo.href : void 0;
13979
+ if (typeof logoHref === "string" && logoHref.trim() && logoHref.trim() !== "/") warnings.push({
13980
+ code: "unsupported-config",
13981
+ message: `logo.href ${JSON.stringify(logoHref.trim().slice(0, 120))} was dropped: the header logo always links to the docs home page. Add it as a navbar link in docs.json if readers need it.`,
13982
+ source: mintlifyConfig ? "docs.json" : "docs.yml"
13503
13983
  });
13504
13984
  if (platform === "mintlify") {
13505
13985
  docsConfig = { ...docsConfig, markdown: { enabled: true } };
@@ -13532,7 +14012,7 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
13532
14012
  message: `Fern global theme ${JSON.stringify(fernRawConfig["global-theme"])} could not be fetched during local migration; local overrides and Thally default styling tokens were applied.`,
13533
14013
  source: "global-theme"
13534
14014
  });
13535
- if (fernRawConfig?.logo && !fernRawConfig["global-theme"] && !logoLight && !logo.light) warnings.push({
14015
+ if (fernRawConfig?.logo && !fernRawConfig["global-theme"] && !logoLight && !logo.light && !logo.dark) warnings.push({
13536
14016
  code: "unsupported-config",
13537
14017
  message: "Fern supplied a logo through its global theme without a local asset; add a logo path to docs.json after import."
13538
14018
  });
@@ -13546,6 +14026,7 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
13546
14026
  assets,
13547
14027
  ...importedSourceRefs.length > 0 ? { sourceRefs: importedSourceRefs } : {},
13548
14028
  ...remoteApiSpecs.length > 0 ? { remoteApiSpecs } : {},
14029
+ ...remoteApiSpecs.length > 0 && localApiMounts.length > 0 ? { localApiMounts } : {},
13549
14030
  ...componentMigrator ? { componentFiles: componentMigrator.files() } : {},
13550
14031
  ...quarantinedFiles.length > 0 ? { quarantinedFiles } : {},
13551
14032
  ...droppedGatedFiles.length > 0 ? { droppedGatedPages: droppedGatedFiles.length } : {},
@@ -15314,7 +15795,6 @@ import { createHash as createHash4 } from "crypto";
15314
15795
  import { lookup } from "dns/promises";
15315
15796
  import { request } from "https";
15316
15797
  import { isIP } from "net";
15317
- import { parse as parseYaml7 } from "yaml";
15318
15798
  var MAX_SPEC_BYTES2 = 25e6;
15319
15799
  var MAX_REDIRECTS = 3;
15320
15800
  function isPublicAddress(address, family) {
@@ -15408,9 +15888,7 @@ async function downloadSpec(value, fetcher) {
15408
15888
  }
15409
15889
  throw new Error("Remote OpenAPI spec redirected too many times.");
15410
15890
  }
15411
- function parseOpenApi(content, extension) {
15412
- const text3 = content.toString("utf8");
15413
- const parsed = extension === "yaml" || !text3.trimStart().startsWith("{") ? parseYaml7(text3) : JSON.parse(text3);
15891
+ function parseOpenApi(parsed) {
15414
15892
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("OpenAPI spec is not an object.");
15415
15893
  const document = parsed;
15416
15894
  if (typeof document.openapi !== "string" || !/^3\./.test(document.openapi)) {
@@ -15423,10 +15901,11 @@ function parseOpenApi(content, extension) {
15423
15901
  if (paths.length > 5e3) throw new Error("Remote OpenAPI spec has too many paths to import.");
15424
15902
  const operations = [];
15425
15903
  for (const [path, value] of paths) {
15426
- if (!value || typeof value !== "object" || Array.isArray(value)) continue;
15904
+ if (!value || typeof value !== "object" || Array.isArray(value) || isWithheld(value)) continue;
15427
15905
  for (const [method, raw] of Object.entries(value)) {
15428
15906
  if (!/^(get|post|put|patch|delete|head|options|trace)$/i.test(method) || !raw || typeof raw !== "object" || Array.isArray(raw)) continue;
15429
15907
  const operation = raw;
15908
+ if (isWithheld(operation)) continue;
15430
15909
  const tags = Array.isArray(operation.tags) ? operation.tags : [];
15431
15910
  operations.push({
15432
15911
  method: method.toLowerCase(),
@@ -15441,9 +15920,8 @@ function parseOpenApi(content, extension) {
15441
15920
  function slug(value) {
15442
15921
  return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
15443
15922
  }
15444
- function operationTarget(operation) {
15445
- const path = operation.path.split("/").filter(Boolean).map((part) => part.replace(/[{}]/g, "").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").toLowerCase());
15446
- return `/api/default/${[...path, operation.method].join("/")}`;
15923
+ function operationTarget(specId, operation) {
15924
+ return `/api/${specId}/${thallyOperationSlugSegments(operation.path, operation.method).join("/")}`;
15447
15925
  }
15448
15926
  async function hydrateRemoteApiSpecs(bundle, fetcher) {
15449
15927
  if (!bundle.remoteApiSpecs?.length) return bundle;
@@ -15453,28 +15931,32 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
15453
15931
  const warnings = [...bundle.warnings];
15454
15932
  const pages = bundle.pages.map((page) => ({ ...page }));
15455
15933
  const authoredLinks = /* @__PURE__ */ new Set();
15934
+ const served = servedRoutes(pages);
15935
+ const endpointUrls = /* @__PURE__ */ new Map();
15456
15936
  for (const page of bundle.pages) {
15457
15937
  for (const match of page.body.matchAll(/\/[a-z0-9_/-]+/gi)) {
15458
15938
  const segments = match[0].split("/").filter(Boolean);
15459
15939
  if (segments.length < 3) continue;
15460
15940
  const section = segments[0].toLowerCase();
15461
- if (section.includes("api") || section.includes("reference")) authoredLinks.add(match[0]);
15941
+ if (section.includes("api") || section.includes("reference")) authoredLinks.add(match[0].replace(/\/+$/, ""));
15462
15942
  }
15463
15943
  }
15944
+ const bound = [];
15464
15945
  for (const reference of bundle.remoteApiSpecs) {
15465
15946
  for (let index = warnings.length - 1; index >= 0; index--) {
15466
15947
  if (warnings[index].source === reference.url && warnings[index].message.includes("requires a network download")) warnings.splice(index, 1);
15467
15948
  }
15468
15949
  try {
15469
- const url = validatedSpecUrl(reference.url);
15470
- const extension = /\.ya?ml$/i.test(url.pathname) ? "yaml" : "json";
15471
- const downloaded = await downloadSpec(reference.url, fetcher);
15950
+ const downloaded = withoutByteOrderMark(await downloadSpec(reference.url, fetcher));
15472
15951
  const pruned = markExcluded(downloaded, reference.withheldOperations ?? [], reference.keptOperations);
15473
15952
  const body = pruned.content;
15474
- if (reference.withheldOperations?.length) warnings.push({ code: "gated-page", message: sharedSpecMessage(reference.url, pruned.excluded), source: reference.url });
15475
- const operations = parseOpenApi(body, extension);
15953
+ if (reference.withheldOperations?.length && !warnings.some((warning) => warning.code === "gated-page" && warning.source === reference.url)) {
15954
+ warnings.push({ code: "gated-page", message: sharedSpecMessage(reference.url, pruned.excluded), source: reference.url });
15955
+ }
15956
+ const { format: extension, document } = parseSpecDocument(body.toString("utf8"));
15957
+ const operations = parseOpenApi(document);
15476
15958
  const filename = `openapi-${createHash4("sha256").update(reference.url).digest("hex").slice(0, 12)}.${extension}`;
15477
- assets.push({ path: `openapi/${filename}`, content: body, projectRelative: true });
15959
+ if (!assets.some((asset) => asset.path === `openapi/${filename}`)) assets.push({ path: `openapi/${filename}`, content: body, projectRelative: true });
15478
15960
  for (const page of pages) {
15479
15961
  const ref = page.openapi ? splitOpenApiRef(page.openapi) : null;
15480
15962
  if (ref?.specRef === reference.url) page.openapi = withSpecRef(ref, `openapi/${filename}`);
@@ -15482,21 +15964,7 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
15482
15964
  const tab = reference.tabLabel ? tabs.find((candidate) => candidate.tab === reference.tabLabel) : tabs.find((candidate) => candidate.tab.toLowerCase().includes("api"));
15483
15965
  if (tab) tab.api = { source: `openapi/${filename}` };
15484
15966
  else insertApiTab(tabs, { tab: reference.tabLabel ?? "API Reference", ...reference.icon ? { icon: reference.icon } : {}, ...reference.hidden ? { hidden: true } : {}, api: { source: `openapi/${filename}` } }, reference.parentTab);
15485
- const routes = /* @__PURE__ */ new Map();
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
- }
15967
+ bound.push({ reference, filename, content: body, operations, tabName: tab?.tab ?? reference.tabLabel ?? "API Reference" });
15500
15968
  } catch (error) {
15501
15969
  warnings.push({
15502
15970
  code: "fetch-failed",
@@ -15505,11 +15973,59 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
15505
15973
  });
15506
15974
  }
15507
15975
  }
15976
+ const directories = [
15977
+ ...(bundle.localApiMounts ?? []).map(({ directory }) => directory),
15978
+ ...bundle.remoteApiSpecs.flatMap((reference) => (reference.directories ?? []).map(({ directory }) => directory))
15979
+ ];
15980
+ const endpointRedirects = linkApiMounts(
15981
+ [
15982
+ ...(bundle.localApiMounts ?? []).map(({ filename, directory, tab }) => ({
15983
+ filename,
15984
+ content: Buffer.from(assets.find((asset) => asset.path === `openapi/${filename}`)?.content ?? []),
15985
+ prefix: directory,
15986
+ ...tab ? { tab } : {}
15987
+ })),
15988
+ ...bound.flatMap(({ reference, filename, content, tabName }) => (reference.directories ?? []).map(({ directory, operations: listed }) => ({ filename, content, prefix: directory, tab: tabName, ...listed ? { operations: listed } : {} })))
15989
+ ],
15990
+ { ...bundle.docsConfig, tabs, redirects },
15991
+ pages,
15992
+ warnings,
15993
+ directories
15994
+ );
15995
+ for (const redirect of endpointRedirects) {
15996
+ redirects.push(redirect);
15997
+ endpointUrls.set(literalRoute(redirect.source) ?? redirect.source, redirect.destination);
15998
+ }
15999
+ const existingSources = new Set(redirects.map((redirect) => literalRoute(redirect.source) ?? redirect.source.toLowerCase()));
16000
+ const mounts = directories.map(mountPrefix).filter(Boolean);
16001
+ const underMount = (source) => mounts.some((prefix) => source.toLowerCase() === `/${prefix}` || source.toLowerCase().startsWith(`/${prefix}/`));
16002
+ const guesses = /* @__PURE__ */ new Map();
16003
+ const specIds = apiTabSpecIds(tabs);
16004
+ for (const { reference, filename, operations, tabName } of bound) {
16005
+ const specId = specIds.find(({ tab }) => tab.tab === tabName && tab.api?.source === `openapi/${filename}`)?.specId;
16006
+ 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;
16007
+ for (const operation of operations) {
16008
+ if (listed && !listed.has(`${operation.method} ${operation.path}`)) continue;
16009
+ const key = `${slug(operation.tag)}/${slug(operation.summary)}`;
16010
+ guesses.set(key, guesses.has(key) || !specId ? null : operationTarget(specId, operation));
16011
+ }
16012
+ }
16013
+ for (const source of authoredLinks) {
16014
+ const destination = guesses.get(source.split("/").filter(Boolean).slice(-2).join("/"));
16015
+ if (!destination || existingSources.has(literalRoute(source) ?? source.toLowerCase()) || served.has(literalRoute(source) ?? source) || underMount(source)) continue;
16016
+ redirects.push({ source, destination, permanent: false });
16017
+ existingSources.add(literalRoute(source) ?? source.toLowerCase());
16018
+ }
16019
+ const direct = redirects.map((redirect) => {
16020
+ const endpoint = endpointUrls.get(literalRoute(redirect.destination) ?? "");
16021
+ return endpoint ? { ...redirect, destination: endpoint } : redirect;
16022
+ });
15508
16023
  return {
15509
16024
  ...bundle,
15510
16025
  pages,
15511
16026
  assets,
15512
- docsConfig: { ...bundle.docsConfig, tabs, ...redirects.length > 0 ? { redirects } : {} },
16027
+ // Redirects added above meet the same checks as any other: none may fail the build, hide a page that exists, or loop.
16028
+ docsConfig: dropConflictingRedirects(dropUnroutableRedirects({ ...bundle.docsConfig, tabs, ...direct.length > 0 ? { redirects: direct } : {} }, warnings), pages, warnings),
15513
16029
  warnings
15514
16030
  };
15515
16031
  }