@thallylabs/migrate 0.2.23 → 0.2.24

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.d.ts CHANGED
@@ -255,6 +255,18 @@ interface MigrationWarning {
255
255
  message: string;
256
256
  source?: string;
257
257
  }
258
+ /** A repository-configured remote OpenAPI spec, downloaded after the bundle is built. */
259
+ interface RemoteApiSpec {
260
+ url: string;
261
+ tabLabel?: string;
262
+ parentTab?: string;
263
+ icon?: string;
264
+ hidden?: boolean;
265
+ /** Operation keys (`get /x`) only access-restricted pages document; marked `x-excluded` after download. */
266
+ withheldOperations?: Array<string>;
267
+ /** Operation keys published pages or docs.json also name; keeps a shared `$ref` path item public. */
268
+ keptOperations?: Array<string>;
269
+ }
258
270
  interface MigrationBundle {
259
271
  sourceUrl: string;
260
272
  sourceKind: 'repository' | 'url';
@@ -262,13 +274,7 @@ interface MigrationBundle {
262
274
  pages: Array<MigrationPage>;
263
275
  assets: Array<MigrationAsset>;
264
276
  /** Repository-configured remote specs awaiting a bounded network fetch by the host. */
265
- remoteApiSpecs?: Array<{
266
- url: string;
267
- tabLabel?: string;
268
- parentTab?: string;
269
- icon?: string;
270
- hidden?: boolean;
271
- }>;
277
+ remoteApiSpecs?: Array<RemoteApiSpec>;
272
278
  /** Customer-owned component source and registry; paths are repository-relative. */
273
279
  componentFiles?: Array<RenderedMigrationFile>;
274
280
  /** Access-restricted source pages, written outside every published path. */
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  } from "fs";
17
17
  import { createRequire } from "module";
18
18
  import { basename as basename3, dirname as dirname4, extname as extname6, isAbsolute as isAbsolute2, posix as posix3, relative as relative5, resolve as resolvePath, sep as sep2 } from "path";
19
- import { parse as parseYaml4 } from "yaml";
19
+ import { parse as parseYaml5 } from "yaml";
20
20
  import * as ts4 from "typescript";
21
21
 
22
22
  // src/components.ts
@@ -321,7 +321,7 @@ function playgroundDisplay(value, where, warn) {
321
321
  const mode = typeof value === "string" ? value.trim().toLowerCase() : "";
322
322
  if (mode === "interactive" || mode === "simple" || mode === "none") return mode;
323
323
  if (mode === "auth") {
324
- warn?.(`${where} "auth" shows the playground only to signed-in readers, which Thally cannot tell; migrated as "simple" (no playground).`);
324
+ warn?.(`${where} "auth" requires reader sign-in, which Thally does not support. Migrated as "simple" (no Try it). Set it to "interactive" to allow everyone.`);
325
325
  return "simple";
326
326
  }
327
327
  warn?.(`${where} ${JSON.stringify(value)} is not one of interactive, simple, none, auth and was dropped.`);
@@ -451,7 +451,7 @@ function normalizeExplicitHeadingIds(raw, warn, options = {}) {
451
451
  if (usable && options.headingMarkers) return `${heading} ${headingIdMarker(id)}`;
452
452
  if (usable) return `<a id="${id}"></a>
453
453
  ${heading}`;
454
- warn?.(`Heading anchor {#${id}} is not a valid HTML id and was removed from "${heading.replace(/^\s*#+\s*/, "")}".`);
454
+ warn?.(`Heading anchor {#${id}} ${options.headingMarkers ? 'contains whitespace or "*/"' : "is not a valid HTML id"} and was removed from "${heading.replace(/^\s*#+\s*/, "")}".`);
455
455
  return heading;
456
456
  }
457
457
  }
@@ -485,7 +485,7 @@ function typesetQuotes(text2) {
485
485
  }).join("");
486
486
  }
487
487
  function plainHeadingText(source) {
488
- return source.replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/<\/?[A-Za-z][^>]*>/g, "").replace(/`([^`]*)`/g, "$1").replace(/(\*\*|__|~~|\*)/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/\\([\\`*_{}[\]()#+.!&<>|~-])/g, "$1");
488
+ return source.replace(/\{\/\*[\s\S]*?\*\/\}/g, " ").replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/<\/?[A-Za-z][^>]*>/g, "").replace(/`([^`]*)`/g, "$1").replace(/(\*\*|__|~~|\*)/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/\\([\\`*_{}[\]()#+.!&<>|~-])/g, "$1");
489
489
  }
490
490
  function markMintlifyHeadings(body) {
491
491
  let fence;
@@ -4040,19 +4040,28 @@ function projectedTheme(value) {
4040
4040
  if (["almond", "palm"].includes(String(value))) return "minimal";
4041
4041
  return typeof value === "string" ? "default" : void 0;
4042
4042
  }
4043
- function projectedColors(value) {
4043
+ function projectedColors(value, warnings) {
4044
4044
  const colors = objectValue2(value);
4045
- if (!colors) return void 0;
4046
- const hex = (entry) => {
4045
+ if (!colors) {
4046
+ if (value !== void 0) warnings.push({ code: "unsupported-config", message: "colors is not an object with a primary hex colour and was dropped.", source: "docs.json" });
4047
+ return void 0;
4048
+ }
4049
+ const hex = (entry, key) => {
4047
4050
  const match = typeof entry === "string" ? /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(entry.trim()) : null;
4048
- if (!match) return void 0;
4051
+ if (!match) {
4052
+ if (entry !== void 0) warnings.push({ code: "unsupported-config", message: `colors.${key} ${JSON.stringify(entry)} is not a 3- or 6-digit hex colour (for example #16a34a) and was dropped.`, source: "docs.json" });
4053
+ return void 0;
4054
+ }
4049
4055
  const digits = match[1].length === 3 ? [...match[1]].map((digit) => digit + digit).join("") : match[1];
4050
4056
  return `#${digits.toLowerCase()}`;
4051
4057
  };
4052
- const primary = hex(colors.primary);
4053
- const light = hex(colors.light) ?? primary;
4054
- const dark = hex(colors.dark) ?? primary;
4055
- if (!primary) return void 0;
4058
+ const primary = hex(colors.primary, "primary");
4059
+ const light = hex(colors.light, "light") ?? primary;
4060
+ const dark = hex(colors.dark, "dark") ?? primary;
4061
+ if (!primary) {
4062
+ if (colors.primary === void 0) warnings.push({ code: "unsupported-config", message: "colors has no primary hex colour, so none of its colours were migrated.", source: "docs.json" });
4063
+ return void 0;
4064
+ }
4056
4065
  return { light: { accent: primary, primary: dark ?? primary }, dark: { accent: light ?? primary, primary: light ?? primary } };
4057
4066
  }
4058
4067
  function projectedNavbar(value) {
@@ -4157,8 +4166,16 @@ function projectedCompatibleConfig(config, warnings) {
4157
4166
  if (droppedContextual.length > 0) {
4158
4167
  warnings.push({ code: "unsupported-config", message: `contextual.options entries not supported by the page menu were dropped: ${droppedContextual.map((option) => typeof option === "string" ? option : "custom entry").join(", ")}.`, source: "docs.json" });
4159
4168
  }
4160
- const metatagEntries = Object.entries(objectValue2(seo?.metatags) ?? {}).filter((entry) => /^[\w:.-]+$/.test(entry[0]) && typeof entry[1] === "string" && entry[1].length <= 1e3);
4169
+ const rawMetatags = Object.entries(objectValue2(seo?.metatags) ?? {});
4170
+ const httpEquivNames = /* @__PURE__ */ new Set(["refresh", "set-cookie", "location", "content-security-policy", "content-security-policy-report-only", "content-type", "default-style", "x-ua-compatible"]);
4171
+ const validMetatag = (entry) => /^[\w:.-]+$/.test(entry[0]) && !entry[0].toLowerCase().startsWith("http-equiv") && !httpEquivNames.has(entry[0].toLowerCase()) && typeof entry[1] === "string" && entry[1].length <= 1e3 && ![...entry[1]].some((character) => character.charCodeAt(0) < 32);
4172
+ const metatagEntries = rawMetatags.filter(validMetatag);
4173
+ const droppedMetatags = rawMetatags.filter((entry) => !validMetatag(entry)).map(([key]) => key);
4174
+ if (droppedMetatags.length > 0) {
4175
+ warnings.push({ code: "unsupported-config", message: `seo.metatags entries were dropped because the name is not a plain meta name, is an http-equiv directive, or the value is not a string of at most 1000 characters: ${droppedMetatags.join(", ")}.`, source: "docs.json" });
4176
+ }
4161
4177
  const projectedMetatags = metatagEntries.length > 0 ? Object.fromEntries(metatagEntries) : void 0;
4178
+ const projectedColorsValue = projectedColors(config.colors, warnings);
4162
4179
  const iconLibrary = objectValue2(config.icons)?.library;
4163
4180
  const projectedIconLibrary = iconLibrary === void 0 ? "fontawesome" : ["lucide", "fontawesome", "tabler"].includes(String(iconLibrary)) ? String(iconLibrary) : void 0;
4164
4181
  const docsNavbar = projectedNavbar(config.navbar);
@@ -4189,7 +4206,7 @@ function projectedCompatibleConfig(config, warnings) {
4189
4206
  return {
4190
4207
  ...projectedTheme(config.theme) ? { theme: projectedTheme(config.theme) } : {},
4191
4208
  ...projectedContextual.length > 0 ? { contextual: { options: projectedContextual } } : {},
4192
- ...projectedColors(config.colors) ? { colors: projectedColors(config.colors) } : {},
4209
+ ...projectedColorsValue ? { colors: projectedColorsValue } : {},
4193
4210
  ...projectedIconLibrary ? { icons: { library: projectedIconLibrary } } : {},
4194
4211
  ...bannerContent ? {
4195
4212
  banner: {
@@ -6075,6 +6092,68 @@ function withSpecRef(ref, specRef) {
6075
6092
  return `${specRef} ${ref.operation}`;
6076
6093
  }
6077
6094
 
6095
+ // src/spec-exclude.ts
6096
+ import { parse as parseYaml4, stringify as stringifyYaml } from "yaml";
6097
+ function specRefMatches(ref, specPath, specFilename, isDefault) {
6098
+ const prefix = /^(?:(\S+)\s+)?(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE|WEBHOOK)\s/i.exec(`${ref} `)?.[1];
6099
+ if (!prefix) return isDefault;
6100
+ const wanted = prefix.replace(/^\/+/, "").toLowerCase();
6101
+ const lowerPath = specPath.toLowerCase();
6102
+ return lowerPath === wanted || lowerPath.endsWith(`/${wanted}`) || specFilename.toLowerCase() === wanted;
6103
+ }
6104
+ function operationKey(operation) {
6105
+ const [first, ...rest] = operation.trim().split(/\s+/);
6106
+ return `${first.toLowerCase()} ${rest.join(" ")}`;
6107
+ }
6108
+ function keysFor(spec, refs) {
6109
+ return [...new Set(
6110
+ refs.filter((ref) => specRefMatches(ref, spec.sourcePath, spec.filename, true)).map((ref) => splitOpenApiRef(ref)?.operation).filter((op) => !!op).map(operationKey)
6111
+ )];
6112
+ }
6113
+ function withheldOperationKeys(spec, withheldRefs, keptRefs) {
6114
+ const kept = keysFor(spec, keptRefs);
6115
+ return { withheld: keysFor(spec, withheldRefs).filter((key) => !kept.includes(key)), kept };
6116
+ }
6117
+ function markExcluded(content, operationKeys, keptKeys = []) {
6118
+ const untouched = { content, excluded: [] };
6119
+ if (operationKeys.length === 0) return untouched;
6120
+ const text2 = content.toString("utf8");
6121
+ let doc;
6122
+ try {
6123
+ doc = parseYaml4(text2);
6124
+ } catch {
6125
+ return untouched;
6126
+ }
6127
+ if (!doc || typeof doc !== "object") return untouched;
6128
+ const excluded = [];
6129
+ for (const key of operationKeys) {
6130
+ const [method, ...rest] = key.split(" ");
6131
+ const name = rest.join(" ");
6132
+ if (method === "webhook") {
6133
+ const entry2 = doc.webhooks?.[name] ?? doc["x-webhooks"]?.[name];
6134
+ if (entry2 && typeof entry2 === "object") {
6135
+ entry2["x-excluded"] = true;
6136
+ excluded.push(`WEBHOOK ${name}`);
6137
+ }
6138
+ continue;
6139
+ }
6140
+ const entry = doc.paths?.[name];
6141
+ if (!entry || typeof entry !== "object") continue;
6142
+ const target = typeof entry.$ref === "string" ? keptKeys.some((k) => k.slice(k.indexOf(" ") + 1) === name) ? void 0 : entry : entry[method];
6143
+ if (target && typeof target === "object") {
6144
+ target["x-excluded"] = true;
6145
+ excluded.push(`${method.toUpperCase()} ${name}`);
6146
+ }
6147
+ }
6148
+ if (excluded.length === 0) return untouched;
6149
+ const out = text2.trimStart().startsWith("{") ? `${JSON.stringify(doc, null, 2)}
6150
+ ` : stringifyYaml(doc);
6151
+ return { content: Buffer.from(out), excluded };
6152
+ }
6153
+ function sharedSpecMessage(path, excluded = []) {
6154
+ return excluded.length ? `OpenAPI spec ${path} is shared with access-restricted pages and is published, but ${excluded.length} operation(s) documented only on those pages were withheld from it (${excluded.join(", ")}). Review the rest of the spec before publishing.` : `OpenAPI spec ${path} is shared with access-restricted pages and is published; it may describe restricted endpoints. Review it before publishing.`;
6155
+ }
6156
+
6078
6157
  // src/analytics.ts
6079
6158
  var GA4_ID = /^G-[A-Z0-9]{4,20}$/;
6080
6159
  var GTM_ID = /^GTM-[A-Z0-9]{4,12}$/;
@@ -7056,12 +7135,23 @@ function normalizedReferenceKey(value) {
7056
7135
  function exactReferenceKey(value) {
7057
7136
  return value.split(/[?#]/, 1)[0].replace(/^\/+/, "").replace(/\\/g, "/").replace(/\.(?:mdx?|rst|txt)$/i, "");
7058
7137
  }
7059
- function specRefMatches(ref, specPath, specFilename, isDefault) {
7060
- const prefix = /^(?:(\S+)\s+)?(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE|WEBHOOK)\s/i.exec(`${ref} `)?.[1];
7061
- if (!prefix) return isDefault;
7062
- const wanted = prefix.replace(/^\/+/, "").toLowerCase();
7063
- const lowerPath = specPath.toLowerCase();
7064
- return lowerPath === wanted || lowerPath.endsWith(`/${wanted}`) || specFilename.toLowerCase() === wanted;
7138
+ function navigationOperationRefs(config) {
7139
+ const refs = [];
7140
+ const visit = (node) => {
7141
+ if (typeof node === "string") {
7142
+ if (splitOpenApiRef(node)) refs.push(node);
7143
+ } else if (Array.isArray(node)) node.forEach(visit);
7144
+ else if (node && typeof node === "object") {
7145
+ if (navigationGateReason(node)) return;
7146
+ Object.values(node).forEach(visit);
7147
+ }
7148
+ };
7149
+ visit(config?.navigation);
7150
+ return refs;
7151
+ }
7152
+ function excludeWithheldOperations(spec, withheldRefs, keptRefs) {
7153
+ const { withheld, kept } = withheldOperationKeys(spec, withheldRefs, keptRefs);
7154
+ return markExcluded(spec.content, withheld, kept);
7065
7155
  }
7066
7156
  var TOKEN_SEPARATORS = /[\s"'()<>[\]{}=,;:|\\/`*!?#&]+/;
7067
7157
  var PATH_SEPARATORS = /[\s"'()<>[\]{}=,;:|\\`*!?#&]+/;
@@ -7221,7 +7311,7 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs, p
7221
7311
  }
7222
7312
  return specs;
7223
7313
  }
7224
- function rewriteMintlifyPageSpecRefs(pages, specs, remoteUrls, warnings) {
7314
+ function rewriteMintlifyPageSpecRefs(pages, specs, remoteUrls, warnings, explainMissing = () => "no such file was found") {
7225
7315
  for (const page of pages) {
7226
7316
  const ref = page.openapi ? splitOpenApiRef(page.openapi) : null;
7227
7317
  if (!ref?.specRef) continue;
@@ -7238,7 +7328,7 @@ function rewriteMintlifyPageSpecRefs(pages, specs, remoteUrls, warnings) {
7238
7328
  if (match) {
7239
7329
  page.openapi = withSpecRef(ref, specAssetPath(match.filename));
7240
7330
  } else {
7241
- warnings.push({ code: "unsupported-config", message: `Page "${page.id}" references the OpenAPI spec "${ref.specRef}", which is not referenced from docs.json and was not migrated; the page will not render an endpoint. Add the spec to a tab's api setting.`, source: page.source });
7331
+ 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 });
7242
7332
  }
7243
7333
  }
7244
7334
  }
@@ -7263,7 +7353,7 @@ function slugifyApiSpecId(value) {
7263
7353
  function parseOpenApiOperationsForLinking(content) {
7264
7354
  let document;
7265
7355
  try {
7266
- document = parseYaml4(content.toString("utf8"));
7356
+ document = parseYaml5(content.toString("utf8"));
7267
7357
  } catch {
7268
7358
  return [];
7269
7359
  }
@@ -7395,7 +7485,7 @@ function fernGeneratorsUnsupportedSpecPaths(config) {
7395
7485
  }
7396
7486
  function readFernGeneratorsConfig(path) {
7397
7487
  if (!existsSync5(path) || !lstatSync5(path).isFile() || lstatSync5(path).size > MAX_FERN_GENERATORS_BYTES) return null;
7398
- const parsed = parseYaml4(readFileSync5(path, "utf8"));
7488
+ const parsed = parseYaml5(readFileSync5(path, "utf8"));
7399
7489
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
7400
7490
  }
7401
7491
  function fernOpenApiCandidateDirs(fernRoot, apiName, apiNameExplicit) {
@@ -7842,7 +7932,7 @@ function globalSnippetAliases(files, repositoryRoot, siteRoot) {
7842
7932
  }
7843
7933
  return aliases;
7844
7934
  }
7845
- function repositoryAssetHref(value, currentFile, siteRoot, onReferenced) {
7935
+ function repositoryAssetHref(value, currentFile, siteRoot, onReferenced, onRootFallback) {
7846
7936
  const isBracketed = value.startsWith("<") && value.endsWith(">");
7847
7937
  const raw = isBracketed ? value.slice(1, -1) : value;
7848
7938
  if (!raw || raw.startsWith("#") || /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(raw)) return null;
@@ -7857,10 +7947,14 @@ function repositoryAssetHref(value, currentFile, siteRoot, onReferenced) {
7857
7947
  }
7858
7948
  if (!ASSET_EXTENSIONS.has(extname6(decodedPath).toLowerCase())) return null;
7859
7949
  try {
7950
+ let usedRootFallback = false;
7860
7951
  let candidate = decodedPath.startsWith("/") ? resolveWithin(siteRoot, decodedPath.replace(/^\/+/, "")) : resolveWithinRoot(dirname4(currentFile), decodedPath, siteRoot);
7861
7952
  if (!decodedPath.startsWith("/") && !existsSync5(candidate)) {
7862
7953
  const fromRoot = resolveWithin(siteRoot, decodedPath.replace(/^(?:\.\/)+/, ""));
7863
- if (existsSync5(fromRoot)) candidate = fromRoot;
7954
+ if (existsSync5(fromRoot)) {
7955
+ candidate = fromRoot;
7956
+ usedRootFallback = true;
7957
+ }
7864
7958
  }
7865
7959
  const realRoot = realpathSync2(siteRoot);
7866
7960
  const realCandidate = realpathSync2(candidate);
@@ -7874,6 +7968,7 @@ function repositoryAssetHref(value, currentFile, siteRoot, onReferenced) {
7874
7968
  const onDiskRelative = relative5(realRoot, realpathSync2.native(candidate)).replace(/\\/g, "/");
7875
7969
  const onDisk = onDiskRelative.toLowerCase() === siteRelative.toLowerCase() ? normalizeAssetPath(onDiskRelative) : null;
7876
7970
  onReferenced?.(normalized, onDisk ?? void 0);
7971
+ if (usedRootFallback) onRootFallback?.(decodedPath, normalized);
7877
7972
  const rewritten = `/${normalized}${suffix}`;
7878
7973
  return isBracketed ? `<${rewritten}>` : rewritten;
7879
7974
  } catch {
@@ -7892,12 +7987,12 @@ function rewriteFernIdRenameLinks(body, renames) {
7892
7987
  return text2.replace(/(\]\()\/([^\s)]+)(?=[\s)]|$)/g, (_match, prefix, target) => `${prefix}${rewriteTarget(`/${target}`)}`).replace(/(\bhref=")\/([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${rewriteTarget(`/${target}`)}${suffix}`);
7893
7988
  });
7894
7989
  }
7895
- function rewriteRepositoryAssetLinks(body, currentFile, siteRoot, onReferenced) {
7990
+ function rewriteRepositoryAssetLinks(body, currentFile, siteRoot, onReferenced, onRootFallback) {
7896
7991
  return body.replace(/(!?\[[^\]]*\]\()(<[^>]+>|[^)\s]+)([^)]*\))/g, (original, opening, destination, closing) => {
7897
- const rewritten = repositoryAssetHref(destination, currentFile, siteRoot, onReferenced);
7992
+ const rewritten = repositoryAssetHref(destination, currentFile, siteRoot, onReferenced, onRootFallback);
7898
7993
  return rewritten ? `${opening}${rewritten}${closing}` : original;
7899
7994
  }).replace(/\b(src|img|image|href)=(['"])([^'"]+)\2/g, (original, property, quote, destination) => {
7900
- const rewritten = repositoryAssetHref(destination, currentFile, siteRoot, onReferenced);
7995
+ const rewritten = repositoryAssetHref(destination, currentFile, siteRoot, onReferenced, onRootFallback);
7901
7996
  return rewritten ? `${property}=${quote}${rewritten}${quote}` : original;
7902
7997
  });
7903
7998
  }
@@ -8706,6 +8801,8 @@ function migrateRepository(options) {
8706
8801
  if (platform === "mintlify" && mintlifyProjectRoot) {
8707
8802
  page.body = rewriteRepositoryAssetLinks(page.body, file.absolutePath, mintlifyProjectRoot, (assetPath, onDiskSpelling) => {
8708
8803
  pageAssetReferences.push(publicAssetKey(onDiskSpelling ?? assetPath));
8804
+ }, (spelled, used) => {
8805
+ warnings.push({ code: "unsupported-config", message: `"${spelled}" was not found beside this page; the file "${used}" in the site root was used instead and will be published.`, source: file.relativePath });
8709
8806
  });
8710
8807
  }
8711
8808
  if (platform === "fern" && fernProjectRoot) {
@@ -9158,21 +9255,31 @@ function migrateRepository(options) {
9158
9255
  rewriteApiLinksInPages(pages, operationLinks, prefixLandings, warnings);
9159
9256
  }
9160
9257
  } else if (platform === "mintlify") {
9161
- const warnSharedSpec = (path) => {
9162
- warnings.push({
9163
- code: "gated-page",
9164
- message: `OpenAPI spec ${path} is shared with access-restricted pages and is published; it may describe restricted endpoints. Review it before publishing.`,
9165
- source: path
9166
- });
9258
+ const warnSharedSpec = (path, excluded = []) => {
9259
+ warnings.push({ code: "gated-page", message: sharedSpecMessage(path, excluded), source: path });
9167
9260
  };
9261
+ const keptSpecRefs = [...publishedSpecRefs, ...navigationOperationRefs(mintlifyConfig)];
9168
9262
  const allSpecs = resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteApiSpecs, pages);
9263
+ for (const remote of remoteApiSpecs) {
9264
+ const { withheld, kept } = withheldOperationKeys({ sourcePath: remote.url, filename: remote.url }, withheldSpecRefs, keptSpecRefs);
9265
+ if (withheld.length > 0) {
9266
+ remote.withheldOperations = withheld;
9267
+ remote.keptOperations = kept;
9268
+ }
9269
+ }
9270
+ const withheldOperations = /* @__PURE__ */ new Map();
9271
+ for (const spec of allSpecs) {
9272
+ const result = excludeWithheldOperations(spec, withheldSpecRefs, keptSpecRefs);
9273
+ spec.content = result.content;
9274
+ withheldOperations.set(spec, result.excluded);
9275
+ }
9169
9276
  const resolvedSpecs = allSpecs.filter((spec) => !spec.pageOnly);
9170
9277
  const pageOnlySpecs = allSpecs.filter((spec) => spec.pageOnly);
9171
9278
  for (const spec of resolvedSpecs) {
9172
9279
  if (!assets.some((asset) => asset.path === specAssetPath(spec.filename))) {
9173
9280
  assets.push(specAsset(spec.filename, spec.content));
9174
9281
  }
9175
- if (withheldSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, spec.filename, true))) warnSharedSpec(spec.sourcePath);
9282
+ if (withheldSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, spec.filename, true))) warnSharedSpec(spec.sourcePath, withheldOperations.get(spec));
9176
9283
  }
9177
9284
  let pageSpecs = resolvedSpecs;
9178
9285
  if (resolvedSpecs.length > 0) {
@@ -9191,24 +9298,35 @@ function migrateRepository(options) {
9191
9298
  quarantinedFiles.push({ path: `${QUARANTINE_DIRECTORY}/assets/${specAssetPath(filename)}`, content: readFileSync5(fallback.absolutePath) });
9192
9299
  withheldAssetCount++;
9193
9300
  } else {
9301
+ const pruned = excludeWithheldOperations({ sourcePath: fallback.relativePath, filename, content: readFileSync5(fallback.absolutePath) }, withheldSpecRefs, keptSpecRefs);
9194
9302
  if (!assets.some((asset) => asset.path === specAssetPath(filename))) {
9195
- assets.push(specAsset(filename, readFileSync5(fallback.absolutePath)));
9303
+ assets.push(specAsset(filename, pruned.content));
9196
9304
  }
9197
9305
  docsConfig = injectOpenApiSpecs(docsConfig, [{ filename }]);
9198
9306
  pageSpecs = [{ filename, sourcePath: fallback.relativePath }];
9199
- if (withheldSpecRefs.some(matches)) warnSharedSpec(fallback.relativePath);
9307
+ if (withheldSpecRefs.some(matches)) warnSharedSpec(fallback.relativePath, pruned.excluded);
9200
9308
  }
9201
9309
  }
9202
9310
  }
9203
9311
  for (const spec of pageOnlySpecs) {
9204
9312
  if (!assets.some((asset) => asset.path === specAssetPath(spec.filename))) assets.push(specAsset(spec.filename, spec.content));
9205
- if (withheldSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, spec.filename, true))) warnSharedSpec(spec.sourcePath);
9313
+ if (withheldSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, spec.filename, true))) warnSharedSpec(spec.sourcePath, withheldOperations.get(spec));
9206
9314
  }
9207
9315
  if (pageOnlySpecs.length > 0) {
9208
9316
  docsConfig = injectOpenApiSpecs(docsConfig, pageOnlySpecs, warnings);
9209
9317
  pageSpecs = [...pageSpecs, ...pageOnlySpecs];
9210
9318
  }
9211
- rewriteMintlifyPageSpecRefs(pages, pageSpecs, new Set(remoteApiSpecs.map((spec) => spec.url)), warnings);
9319
+ const explainMissingSpec = (key) => {
9320
+ const sameName = scannedFiles.find((file) => file.relativePath.toLowerCase() === key.toLowerCase());
9321
+ if (sameName && sameName.relativePath !== key) return `paths are case-sensitive; the file is at "${sameName.relativePath}"`;
9322
+ if (sameName && mintignoreMatcher?.ignores(sameName.relativePath)) return "the file is excluded by .mintignore";
9323
+ try {
9324
+ if (lstatSync5(resolvePath(repositoryDir, key)).isSymbolicLink()) return "the file is a symbolic link, which is never followed";
9325
+ } catch {
9326
+ }
9327
+ return sameName ? "the file could not be read as an OpenAPI document" : `no file exists at "${key}"`;
9328
+ };
9329
+ rewriteMintlifyPageSpecRefs(pages, pageSpecs, new Set(remoteApiSpecs.map((spec) => spec.url)), warnings, explainMissingSpec);
9212
9330
  }
9213
9331
  if (platform === "mintlify") {
9214
9332
  const sources = new Set((docsConfig.redirects ?? []).map((redirect) => redirect.source));
@@ -9750,7 +9868,7 @@ import remarkMdx3 from "remark-mdx";
9750
9868
  import remarkParse3 from "remark-parse";
9751
9869
  import TurndownService from "turndown";
9752
9870
  import { unified as unified3 } from "unified";
9753
- import { parse as parseYaml5, stringify as stringifyYaml } from "yaml";
9871
+ import { parse as parseYaml6, stringify as stringifyYaml2 } from "yaml";
9754
9872
  var DEFAULT_MAX_PAGES = 1e3;
9755
9873
  var DEFAULT_MAX_TOTAL_BYTES = 1e8;
9756
9874
  var MAX_DISCOVERED_URLS = 5e3;
@@ -10475,7 +10593,7 @@ function extractEmbeddedOpenApi(content) {
10475
10593
  const match = fencePattern.exec(content);
10476
10594
  if (!match) return { content };
10477
10595
  try {
10478
- const parsed = parseYaml5(match[5], { maxAliasCount: 50 });
10596
+ const parsed = parseYaml6(match[5], { maxAliasCount: 50 });
10479
10597
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { content };
10480
10598
  const document = parsed;
10481
10599
  if (typeof document.openapi !== "string" && typeof document.swagger !== "string") return { content };
@@ -10589,7 +10707,7 @@ function mergeEmbeddedOpenApi(fragments) {
10589
10707
  if (Object.keys(combinedPaths).length === 0) return null;
10590
10708
  if (Object.keys(combinedComponents).length === 0) delete combined.components;
10591
10709
  if (tags.size > 0) combined.tags = [...tags.values()];
10592
- return new TextEncoder().encode(stringifyYaml(combined));
10710
+ return new TextEncoder().encode(stringifyYaml2(combined));
10593
10711
  }
10594
10712
  function htmlPage(document, id) {
10595
10713
  const $ = load2(document.body);
@@ -11328,7 +11446,7 @@ import { createHash as createHash3 } from "crypto";
11328
11446
  import { lookup } from "dns/promises";
11329
11447
  import { request } from "https";
11330
11448
  import { isIP } from "net";
11331
- import { parse as parseYaml6 } from "yaml";
11449
+ import { parse as parseYaml7 } from "yaml";
11332
11450
  var MAX_SPEC_BYTES = 25e6;
11333
11451
  var MAX_REDIRECTS = 3;
11334
11452
  function isPublicAddress(address, family) {
@@ -11424,7 +11542,7 @@ async function downloadSpec(value, fetcher) {
11424
11542
  }
11425
11543
  function parseOpenApi(content, extension) {
11426
11544
  const text2 = content.toString("utf8");
11427
- const parsed = extension === "yaml" || !text2.trimStart().startsWith("{") ? parseYaml6(text2) : JSON.parse(text2);
11545
+ const parsed = extension === "yaml" || !text2.trimStart().startsWith("{") ? parseYaml7(text2) : JSON.parse(text2);
11428
11546
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("OpenAPI spec is not an object.");
11429
11547
  const document = parsed;
11430
11548
  if (typeof document.openapi !== "string" || !/^3\./.test(document.openapi)) {
@@ -11482,7 +11600,10 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
11482
11600
  try {
11483
11601
  const url = validatedSpecUrl(reference.url);
11484
11602
  const extension = /\.ya?ml$/i.test(url.pathname) ? "yaml" : "json";
11485
- const body = await downloadSpec(reference.url, fetcher);
11603
+ const downloaded = await downloadSpec(reference.url, fetcher);
11604
+ const pruned = markExcluded(downloaded, reference.withheldOperations ?? [], reference.keptOperations);
11605
+ const body = pruned.content;
11606
+ if (reference.withheldOperations?.length) warnings.push({ code: "gated-page", message: sharedSpecMessage(reference.url, pruned.excluded), source: reference.url });
11486
11607
  const operations = parseOpenApi(body, extension);
11487
11608
  const filename = `openapi-${createHash3("sha256").update(reference.url).digest("hex").slice(0, 12)}.${extension}`;
11488
11609
  assets.push({ path: `openapi/${filename}`, content: body, projectRelative: true });