@thallylabs/migrate 0.2.33 → 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
  }
@@ -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) {
@@ -7196,8 +7278,49 @@ function splitOpenApiRef(raw) {
7196
7278
  const specRef = match[1] ? unquote(match[1]) : void 0;
7197
7279
  return { ...specRef ? { specRef } : {}, operation: match[2] };
7198
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
+ }
7199
7287
  function specRefBaseName(specRef) {
7200
- 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";
7201
7324
  }
7202
7325
  function withSpecRef(ref, specRef) {
7203
7326
  return `${specRef} ${ref.operation}`;
@@ -7205,25 +7328,44 @@ function withSpecRef(ref, specRef) {
7205
7328
 
7206
7329
  // src/spec-exclude.ts
7207
7330
  import { parse as parseYaml4, stringify as stringifyYaml } from "yaml";
7208
- function specRefMatches(ref, specPath, specFilename, isDefault) {
7209
- 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;
7210
7333
  if (!prefix) return isDefault;
7211
- const wanted = prefix.replace(/^\/+/, "").toLowerCase();
7334
+ const wanted = (isRemoteSpecRef(prefix) ? prefix.trim() : canonicalSpecRef(prefix)).toLowerCase();
7212
7335
  const lowerPath = specPath.toLowerCase();
7213
- 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;
7214
7356
  }
7215
7357
  function operationKey(operation) {
7216
7358
  const [first, ...rest] = operation.trim().split(/\s+/);
7217
7359
  return `${first.toLowerCase()} ${rest.join(" ")}`;
7218
7360
  }
7219
- function keysFor(spec, refs) {
7361
+ function keysFor(refs, names) {
7220
7362
  return [...new Set(
7221
- 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)
7222
7364
  )];
7223
7365
  }
7224
- function withheldOperationKeys(spec, withheldRefs, keptRefs) {
7225
- const kept = keysFor(spec, keptRefs);
7226
- 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 };
7227
7369
  }
7228
7370
  function withoutByteOrderMark(content) {
7229
7371
  return content.length >= 3 && content[0] === 239 && content[1] === 187 && content[2] === 191 ? content.subarray(3) : content;
@@ -10474,22 +10616,56 @@ function normalizedReferenceKey(value) {
10474
10616
  function exactReferenceKey(value) {
10475
10617
  return value.split(/[?#]/, 1)[0].replace(/^\/+/, "").replace(/\\/g, "/").replace(/\.(?:mdx?|rst|txt)$/i, "");
10476
10618
  }
10477
- function navigationOperationRefs(config) {
10478
- const refs = [];
10479
- 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) => {
10480
10628
  if (typeof node === "string") {
10481
- if (splitOpenApiRef(node)) refs.push(node);
10482
- } 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));
10483
10635
  else if (node && typeof node === "object") {
10484
10636
  if (navigationGateReason(node)) return;
10485
- Object.values(node).forEach(visit);
10486
- }
10487
- };
10488
- visit(config?.navigation);
10489
- return refs;
10490
- }
10491
- function excludeWithheldOperations(spec, withheldRefs, keptRefs) {
10492
- 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);
10493
10669
  return markExcluded(spec.content, withheld, kept);
10494
10670
  }
10495
10671
  var TOKEN_SEPARATORS = /[\s"'()<>[\]{}=,;:|\\/`*!?#&]+/;
@@ -10590,6 +10766,23 @@ function specAssetPath(filename) {
10590
10766
  function specAsset(filename, content) {
10591
10767
  return { path: specAssetPath(filename), content, projectRelative: true };
10592
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
+ }
10593
10786
  function mintlifyTopLevelApiReferences(config) {
10594
10787
  const api = config?.api && typeof config.api === "object" && !Array.isArray(config.api) ? config.api : null;
10595
10788
  if (!api) return [];
@@ -10601,19 +10794,25 @@ function mintlifyTopLevelApiReferences(config) {
10601
10794
  }
10602
10795
  return references;
10603
10796
  }
10604
- 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 = []) {
10605
10801
  if (!mintlifyConfig) return [];
10606
10802
  const references = [
10607
10803
  ...mintlifyTopLevelApiReferences(mintlifyConfig),
10608
10804
  ...mintlifyNavigationApiReferences(mintlifyConfig)
10609
10805
  ];
10610
- const specKey = (value) => value.split(/[?#]/, 1)[0].replace(/\\/g, "/").replace(/^(?:\.\/)+/, "").replace(/^\/+/, "");
10611
- 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();
10612
10809
  for (const page of pages) {
10613
10810
  const specRef = page.openapi ? splitOpenApiRef(page.openapi)?.specRef : void 0;
10614
10811
  if (!specRef || /^[a-z][a-z0-9+.-]*:/i.test(specRef)) continue;
10615
- const key = specKey(specRef);
10616
- 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;
10617
10816
  known.add(key);
10618
10817
  references.push({ value: key, kind: "openapi", tabLabel: `OpenAPI: ${key}`, hidden: true, pageOnly: true });
10619
10818
  }
@@ -10663,7 +10862,7 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, p
10663
10862
  warnings.push({ code: "unsupported-config", message: `The OpenAPI spec "${reference.value}"${tabSuffix} uses insecure HTTP and was not imported.`, source: reference.value });
10664
10863
  continue;
10665
10864
  }
10666
- const key = reference.value.split(/[?#]/, 1)[0].replace(/^\/+/, "").replace(/\\/g, "/");
10865
+ const key = canonicalSpecRef(reference.value);
10667
10866
  const match = files.find((file) => file.relativePath === key);
10668
10867
  if (!match) {
10669
10868
  warnings.push({
@@ -10692,21 +10891,7 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, p
10692
10891
  directory: reference.directory
10693
10892
  });
10694
10893
  }
10695
- const filenameOwners = /* @__PURE__ */ new Map();
10696
- const filenameSources = /* @__PURE__ */ new Map();
10697
- for (const spec of specs) {
10698
- const groupKey = spec.tabLabel ?? "\0default";
10699
- const owner = filenameOwners.get(spec.filename);
10700
- if (owner !== void 0 && owner !== groupKey && filenameSources.get(spec.filename) !== spec.sourcePath) {
10701
- let disambiguated = spec.sourcePath.replace(/\//g, "-");
10702
- while (filenameOwners.has(disambiguated) && filenameOwners.get(disambiguated) !== groupKey) {
10703
- disambiguated = `${groupKey.replace(/\W+/g, "-")}-${disambiguated}`;
10704
- }
10705
- spec.filename = disambiguated;
10706
- }
10707
- filenameOwners.set(spec.filename, groupKey);
10708
- filenameSources.set(spec.filename, spec.sourcePath);
10709
- }
10894
+ for (const spec of specs) spec.filename = nameSpec(spec);
10710
10895
  return specs;
10711
10896
  }
10712
10897
  function rewriteMintlifyPageSpecRefs(pages, specs, remoteUrls, warnings, explainMissing = () => "no such file was found") {
@@ -10719,12 +10904,13 @@ function rewriteMintlifyPageSpecRefs(pages, specs, remoteUrls, warnings, explain
10719
10904
  }
10720
10905
  continue;
10721
10906
  }
10722
- const key = ref.specRef.split(/[?#]/, 1)[0].replace(/\\/g, "/").replace(/^(?:\.\/)+/, "").replace(/^\/+/, "");
10723
- const byPath = specs.find((spec) => spec.sourcePath === key);
10724
- const byName = specs.filter((spec) => spec.filename.toLowerCase() === specRefBaseName(key));
10725
- const match = byPath ?? (byName.length === 1 ? byName[0] : void 0);
10726
- if (match) {
10727
- 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 });
10728
10914
  } else {
10729
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 });
10730
10916
  }
@@ -12055,9 +12241,12 @@ function injectOpenApiSpecs(config, specs, warnings) {
12055
12241
  const apiTab = spec.tabLabel ? tabs.find((tab) => tab.tab === spec.tabLabel) : tabs.find((tab) => tab.tab.toLowerCase().includes("api"));
12056
12242
  if (apiTab) {
12057
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 } });
12058
12247
  warnings.push({
12059
12248
  code: "unsupported-config",
12060
- 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."}`
12061
12250
  });
12062
12251
  continue;
12063
12252
  }
@@ -12316,6 +12505,7 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
12316
12505
  const gateByPath = /* @__PURE__ */ new Map();
12317
12506
  const withheldSpecRefs = [];
12318
12507
  const publishedSpecRefs = [];
12508
+ const publishedSpecPages = [];
12319
12509
  const withheldPaths = /* @__PURE__ */ new Set();
12320
12510
  const isDocFile = (file) => [".md", ".mdx"].includes(extname6(file.relativePath).toLowerCase());
12321
12511
  const isGateCandidate = (file) => isDocFile(file) && !file.relativePath.split("/").some((segment) => SNIPPET_DIRECTORIES.has(segment.toLowerCase()));
@@ -12352,6 +12542,7 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
12352
12542
  const verdict = classifyPageGate(file);
12353
12543
  gateByPath.set(file.absolutePath, verdict);
12354
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 });
12355
12546
  if (verdict.reason) withheldPaths.add(file.absolutePath);
12356
12547
  }
12357
12548
  for (const file of mintignoreFilteredFiles) {
@@ -12455,6 +12646,23 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
12455
12646
  let discovered = files.length;
12456
12647
  const localeConfig = docsConfig.i18n;
12457
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
+ };
12458
12666
  const routeAliases = [];
12459
12667
  const pageFiles = [...files].sort((left, right) => {
12460
12668
  const leftOrder = referenceOrder.get(normalizedReferenceKey(left.relativePath)) ?? Number.MAX_SAFE_INTEGER;
@@ -12550,7 +12758,6 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
12550
12758
  }
12551
12759
  continue;
12552
12760
  }
12553
- const key = normalizedReferenceKey(file.relativePath);
12554
12761
  let pageGate;
12555
12762
  {
12556
12763
  pageGate = gateByPath.get(file.absolutePath);
@@ -12600,25 +12807,13 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
12600
12807
  }
12601
12808
  }
12602
12809
  const isRestrictedPage = Boolean(pageGate?.reason);
12603
- const referenced = exactReferenceMap.get(exactReferenceKey(file.relativePath)) ?? referenceMap.get(key);
12604
- let locale = referenced?.locale;
12605
- let navigationId = referenced?.navigationId ?? pageIdFromReference(file.relativePath, platform === "mintlify");
12606
- if (!referenced && localeConfig) {
12607
- for (const entry of localeConfig.locales) {
12608
- const localized = mintlifyLocalizedReference(file.relativePath, entry.code, defaultPageIds);
12609
- if (localized === file.relativePath) continue;
12610
- locale = entry.code;
12611
- navigationId = pageIdFromReference(localized, platform === "mintlify");
12612
- break;
12613
- }
12614
- }
12615
- if (!navigationId) {
12810
+ const identity = resolvePageIdentity(file.relativePath);
12811
+ if (!identity) {
12616
12812
  skipped++;
12617
12813
  warnings.push({ code: "invalid-page", message: "Page path could not be normalized safely.", source: file.relativePath });
12618
12814
  continue;
12619
12815
  }
12620
- const isDefaultLocale = !locale || locale === localeConfig?.defaultLocale;
12621
- const id = isDefaultLocale ? navigationId : `${locale}/${navigationId}`;
12816
+ const { locale, navigationId, id } = identity;
12622
12817
  let raw = inlineMdxSnippets(
12623
12818
  mintlifyProjectRoot ? importUndeclaredSnippetComponents(readFileSync6(file.absolutePath, "utf8"), file.absolutePath, repositoryDir, mintlifyProjectRoot, snippetExports, snippetAliases, inlineGate, warnings) : injectDocusaurusGlobalImports(readFileSync6(file.absolutePath, "utf8"), globalDocusaurusImports),
12624
12819
  file.absolutePath,
@@ -13267,8 +13462,21 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
13267
13462
  const warnSharedSpec = (path, excluded = []) => {
13268
13463
  warnings.push({ code: "gated-page", message: sharedSpecMessage(path, excluded), source: path });
13269
13464
  };
13270
- const keptSpecRefs = [...publishedSpecRefs, ...navigationOperationRefs(mintlifyConfig)];
13271
- 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();
13272
13480
  for (const remote of remoteApiSpecs) {
13273
13481
  const { withheld, kept } = withheldOperationKeys({ sourcePath: remote.url, filename: remote.url }, withheldSpecRefs, keptSpecRefs);
13274
13482
  if (withheld.length > 0) {
@@ -13278,9 +13486,10 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
13278
13486
  }
13279
13487
  const withheldOperations = /* @__PURE__ */ new Map();
13280
13488
  for (const spec of allSpecs) {
13281
- const result = excludeWithheldOperations(spec, withheldSpecRefs, keptSpecRefs);
13489
+ const result = excludeWithheldOperations(spec, withheldSpecRefs, keptSpecRefs, allSpecs);
13282
13490
  spec.content = result.content;
13283
13491
  withheldOperations.set(spec, result.excluded);
13492
+ result.excluded.forEach((operation) => withheldKeys.add(operationKey(operation)));
13284
13493
  }
13285
13494
  const resolvedSpecs = allSpecs.filter((spec) => !spec.pageOnly);
13286
13495
  const pageOnlySpecs = allSpecs.filter((spec) => spec.pageOnly);
@@ -13288,12 +13497,14 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
13288
13497
  if (!assets.some((asset) => asset.path === specAssetPath(spec.filename))) {
13289
13498
  assets.push(specAsset(spec.filename, spec.content));
13290
13499
  }
13291
- 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));
13292
13501
  }
13293
13502
  let pageSpecs = resolvedSpecs;
13294
13503
  if (resolvedSpecs.length > 0) {
13504
+ const namedByPages = new Set(pages.filter((page) => !restrictedPages.has(page)).map((page) => specNamedByPage(page.openapi, resolvedSpecs)));
13295
13505
  for (const spec of resolvedSpecs) {
13296
- 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));
13297
13508
  }
13298
13509
  docsConfig = injectOpenApiSpecs(docsConfig, resolvedSpecs, warnings);
13299
13510
  if (remoteApiSpecs.length > 0) {
@@ -13311,13 +13522,16 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
13311
13522
  const fallback = findOpenApi(files);
13312
13523
  const fallbackContent = fallback ? readSpecFile(fallback.absolutePath, fallback.relativePath, warnings) : void 0;
13313
13524
  if (fallback && fallbackContent) {
13314
- const filename = basename3(fallback.relativePath);
13315
- const matches = (ref) => specRefMatches(ref, fallback.relativePath, filename, true);
13525
+ const matches = (ref) => specRefMatches(ref, fallback.relativePath, true);
13316
13526
  if (withheldSpecRefs.some(matches) && !publishedSpecRefs.some(matches)) {
13317
- quarantinedFiles.push({ path: `${QUARANTINE_DIRECTORY}/assets/${specAssetPath(filename)}`, content: fallbackContent });
13527
+ quarantinedFiles.push({ path: `${QUARANTINE_DIRECTORY}/assets/${specAssetPath(basename3(fallback.relativePath))}`, content: fallbackContent });
13318
13528
  withheldAssetCount++;
13319
13529
  } else {
13320
- 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)));
13321
13535
  if (!assets.some((asset) => asset.path === specAssetPath(filename))) {
13322
13536
  assets.push(specAsset(filename, pruned.content));
13323
13537
  }
@@ -13329,13 +13543,22 @@ ${warnings.map((warning) => `- ${warning.message}`).join("\n")}`);
13329
13543
  }
13330
13544
  for (const spec of pageOnlySpecs) {
13331
13545
  if (!assets.some((asset) => asset.path === specAssetPath(spec.filename))) assets.push(specAsset(spec.filename, spec.content));
13332
- 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));
13333
13547
  }
13334
13548
  if (pageOnlySpecs.length > 0) {
13335
13549
  docsConfig = injectOpenApiSpecs(docsConfig, pageOnlySpecs, warnings);
13336
13550
  pageSpecs = [...pageSpecs, ...pageOnlySpecs];
13337
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
+ }
13338
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`;
13339
13562
  const sameName = scannedFiles.find((file) => file.relativePath.toLowerCase() === key.toLowerCase());
13340
13563
  if (sameName && sameName.relativePath !== key) return `paths are case-sensitive; the file is at "${sameName.relativePath}"`;
13341
13564
  if (sameName && mintignoreMatcher?.ignores(sameName.relativePath)) return "the file is excluded by .mintignore";
@@ -13675,10 +13898,10 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
13675
13898
  const sourceFavicon = mintlifyConfig?.favicon ?? fernRawConfig?.favicon ?? docusaurusSettings?.favicon;
13676
13899
  const logo = brandVariants(sourceLogo);
13677
13900
  const favicon = brandVariants(sourceFavicon);
13678
- const logoLight = publicBrandPath(logo.light);
13679
13901
  const logoDark = publicBrandPath(logo.dark);
13680
- const faviconLight = publicBrandPath(favicon.light);
13902
+ const logoLight = publicBrandPath(logo.light) ?? logoDark;
13681
13903
  const faviconDark = publicBrandPath(favicon.dark);
13904
+ const faviconLight = publicBrandPath(favicon.light) ?? faviconDark;
13682
13905
  if (logoLight) docsConfig = {
13683
13906
  ...docsConfig,
13684
13907
  navbar: {
@@ -13707,15 +13930,30 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
13707
13930
  const decoration = background.decoration;
13708
13931
  const image = publicBrandPath(background.image);
13709
13932
  const imageDark = publicBrandPath(background.imageDark);
13710
- docsConfig = {
13711
- ...docsConfig,
13712
- background: {
13713
- ...docsConfig.background,
13714
- ...decoration === "none" || decoration === "grid" || decoration === "gradient" ? { decoration } : {},
13715
- ...image ? { image } : {},
13716
- ...imageDark ? { imageDark } : {}
13717
- }
13933
+ const mapped = {
13934
+ ...docsConfig.background,
13935
+ ...decoration === "none" || decoration === "grid" || decoration === "gradient" ? { decoration } : {},
13936
+ ...image ? { image } : {},
13937
+ ...imageDark ? { imageDark } : {}
13718
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
+ }
13719
13957
  }
13720
13958
  }
13721
13959
  if (platform === "docusaurus") {
@@ -13725,13 +13963,23 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
13725
13963
  ...docsConfig,
13726
13964
  favicon: { light: faviconLight, ...faviconDark ? { dark: faviconDark } : {} }
13727
13965
  };
13728
- const missingBrand = [
13729
- ...[logo.light, logo.dark].filter((value) => typeof value === "string" && !publicBrandPath(value)),
13730
- ...[favicon.light, favicon.dark].filter((value) => typeof value === "string" && !publicBrandPath(value))
13731
- ];
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("");
13732
13974
  if (missingBrand.length > 0) warnings.push({
13733
13975
  code: "unsupported-config",
13734
- 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"
13735
13983
  });
13736
13984
  if (platform === "mintlify") {
13737
13985
  docsConfig = { ...docsConfig, markdown: { enabled: true } };
@@ -13764,7 +14012,7 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
13764
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.`,
13765
14013
  source: "global-theme"
13766
14014
  });
13767
- 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({
13768
14016
  code: "unsupported-config",
13769
14017
  message: "Fern supplied a logo through its global theme without a local asset; add a logo path to docs.json after import."
13770
14018
  });