@thallylabs/migrate 0.2.18 → 0.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,6 +40,13 @@ instead of silently dropping that API tab's reference. AsyncAPI has no Thally
40
40
  renderer, so it always produces a clear warning naming the spec instead of
41
41
  disappearing.
42
42
 
43
+ Page `openapi:` frontmatter with a spec prefix
44
+ (`openapi: "specs/openapi-b.yaml GET /widgets/{id}"`) is rewritten to where the
45
+ spec lands in the output (`/openapi-b.yaml GET /widgets/{id}`). A prefix naming
46
+ a spec that was not migrated is kept and reported. Manual API pages keep their
47
+ `api:` and `authMethod:` frontmatter, and `api.mdx.server` / `api.mdx.auth` map
48
+ into `docs.json`; invalid values are dropped with a warning.
49
+
43
50
  In-content links to auto-generated API operation pages are rewritten to the
44
51
  matching Thally `/api/...` route. A link under a known API section that matches
45
52
  no operation points to that section's overview page instead, and one warning
package/dist/index.d.ts CHANGED
@@ -128,6 +128,16 @@ interface MigrationDocsConfig {
128
128
  analytics?: {
129
129
  enabled?: boolean;
130
130
  };
131
+ /** Manual API pages: default server(s) and auth for `api:` frontmatter pages. */
132
+ api?: {
133
+ mdx?: {
134
+ server?: string | Array<string>;
135
+ auth?: {
136
+ method?: 'bearer' | 'basic' | 'key';
137
+ name?: string;
138
+ };
139
+ };
140
+ };
131
141
  /** Third-party analytics in Mintlify's `integrations` shape; validated again by the renderer. */
132
142
  integrations?: {
133
143
  ga4?: {
@@ -179,6 +189,10 @@ interface MigrationPage {
179
189
  noindex?: boolean;
180
190
  /** OpenAPI operation key rendered by Thally instead of ordinary MDX. */
181
191
  openapi?: string;
192
+ /** Manual API page: `METHOD <url-or-path>`; rendered by Thally's playground. */
193
+ api?: string;
194
+ /** Page-level playground auth override: bearer | basic | key | none. */
195
+ authMethod?: string;
182
196
  body: string;
183
197
  source: string;
184
198
  /** Set when the page's frontmatter was invalid YAML; the page is kept with a best-effort salvage. */
@@ -463,6 +477,8 @@ declare function parseMarkdownPage(input: {
463
477
  platform?: MigrationPlatform;
464
478
  /** Resolve platform-specific routes from the already-parsed frontmatter. */
465
479
  resolveIdentity?: (frontmatter: Record<string, unknown>, fallback: MarkdownPageIdentity) => MarkdownPageIdentity;
480
+ /** Receives a message when frontmatter is dropped or altered. */
481
+ warn?: (message: string) => void;
466
482
  }): MigrationPage | null;
467
483
 
468
484
  /**
package/dist/index.js CHANGED
@@ -263,7 +263,8 @@ function unwrapDocusaurusMdxCodeBlocks(body) {
263
263
  }
264
264
  return isMdxCodeBlock ? body : output.join("\n");
265
265
  }
266
- function normalizeExplicitHeadingIds(raw) {
266
+ var HTML_ID = /^[^\s"'<>`{}&]+$/u;
267
+ function normalizeExplicitHeadingIds(raw, warn) {
267
268
  const { front, body } = splitFrontmatterBlock(raw);
268
269
  return front + replaceOutsideCode(body, (segment) => segment.split("\n").map((line) => {
269
270
  if (!/^[ \t]{0,3}#{1,6}[ \t]+/.test(line)) return line;
@@ -272,10 +273,12 @@ function normalizeExplicitHeadingIds(raw) {
272
273
  const marker = trimmed.lastIndexOf(" {#");
273
274
  if (marker >= 0) {
274
275
  const id = trimmed.slice(marker + 3, -1);
275
- if (/^[A-Za-z][A-Za-z0-9_-]*$/.test(id)) {
276
- return `<a id="${id}"></a>
277
- ${trimmed.slice(0, marker).trimEnd()}`;
278
- }
276
+ const heading = trimmed.slice(0, marker).trimEnd();
277
+ if (/[{}]/.test(id)) return line;
278
+ if (HTML_ID.test(id)) return `<a id="${id}"></a>
279
+ ${heading}`;
280
+ warn?.(`Heading anchor {#${id}} is not a valid HTML id and was removed from "${heading.replace(/^\s*#+\s*/, "")}".`);
281
+ return heading;
279
282
  }
280
283
  }
281
284
  if (trimmed.endsWith("*/}")) {
@@ -283,7 +286,7 @@ ${trimmed.slice(0, marker).trimEnd()}`;
283
286
  if (marker >= 0) {
284
287
  const comment = trimmed.slice(marker + 4, -3).trim();
285
288
  const id = comment.startsWith("#") ? comment.slice(1) : "";
286
- if (/^[A-Za-z][A-Za-z0-9_.:-]*$/.test(id)) {
289
+ if (HTML_ID.test(id)) {
287
290
  return `<a id="${id}"></a>
288
291
  ${trimmed.slice(0, marker).trimEnd()}`;
289
292
  }
@@ -671,13 +674,25 @@ function maskCode(body) {
671
674
  if (fenceBuffer.length) lines.push(stash(fenceBuffer.join("\n")));
672
675
  return {
673
676
  masked: lines.join("\n"),
674
- unmask: (text2) => text2.replace(/\u0000(\d+)\u0000/g, (_match, index) => blocks[Number(index)])
677
+ unmask: (text2) => text2.replace(/\u0000(\d+)\u0000/g, (_match, index) => blocks[Number(index)]),
678
+ openFence: codeFence
675
679
  };
676
680
  }
681
+ function closeOpenFence(body) {
682
+ const { openFence } = maskCode(body);
683
+ return openFence ? `${body}${body.endsWith("\n") ? "" : "\n"}${openFence}
684
+ ` : body;
685
+ }
677
686
  function replaceOutsideCode(body, transform) {
678
687
  const { masked, unmask } = maskCode(body);
679
688
  return unmask(transform(masked));
680
689
  }
690
+ function mdxComment(text2) {
691
+ return `{/*${text2.replace(/\*\//g, "* /")}*/}`;
692
+ }
693
+ function replaceOutsideCodeAndComments(body, transform) {
694
+ return replaceOutsideCode(body, (masked) => masked.split(/(\{\s*\/\*[\s\S]*?\*\/\s*\})/).map((part, index) => index % 2 ? part : transform(part)).join(""));
695
+ }
681
696
  function rewriteFernRelativePageLinks(body, currentSourcePath, routesBySourcePath) {
682
697
  const rewrite = (target) => {
683
698
  if (!target || target.startsWith("/") || target.startsWith("#") || target.startsWith("?") || target.includes("\\") || /[\u0000-\u001f\u007f]/.test(target) || /^[A-Za-z][A-Za-z\d+.-]*:/.test(target)) return target;
@@ -739,7 +754,7 @@ function normalizeFernFileTrees(body) {
739
754
  ));
740
755
  }
741
756
  function normalizeHtmlComments(body) {
742
- return replaceOutsideCode(body, (whole) => whole.replace(/<!--([\s\S]*?)-->/g, (_match, comment) => `{/*${comment}*/}`));
757
+ return replaceOutsideCode(body, (whole) => whole.replace(/<!--([\s\S]*?)-->/g, (_match, comment) => mdxComment(comment)));
743
758
  }
744
759
  function normalizeFernCallouts(body) {
745
760
  return replaceOutsideCode(body, (whole) => {
@@ -1230,7 +1245,7 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
1230
1245
  }
1231
1246
  if (runMintlify) rewritten = normalizeMintlifyParameterAnchors(rewritten);
1232
1247
  rewritten = replaceOutsideCode(rewritten, (segment) => {
1233
- let result = convertHtmlStyleAttributes(segment).replace(/<!--([\s\S]*?)-->/g, (_match, content) => `{/*${content}*/}`).replace(/<Danger(\s[^>]*)?>/g, "<Error$1>").replace(/<\/Danger>/g, "</Error>").replace(/<Warn(\s[^>]*)?>/g, "<Warning$1>").replace(/<\/Warn>/g, "</Warning>").replace(/<Check(\s[^>]*)?>/g, "<Note$1>").replace(/<\/Check>/g, "</Note>").replace(/<Tree\.Folder/g, "<Folder").replace(/<\/Tree\.Folder>/g, "</Folder>").replace(/<Tree\.File/g, "<File").replace(/<\/Tree\.File>/g, "</File>");
1248
+ let result = convertHtmlStyleAttributes(segment).replace(/<!--([\s\S]*?)-->/g, (_match, content) => mdxComment(content)).replace(/<Danger(\s[^>]*)?>/g, "<Error$1>").replace(/<\/Danger>/g, "</Error>").replace(/<Warn(\s[^>]*)?>/g, "<Warning$1>").replace(/<\/Warn>/g, "</Warning>").replace(/<Check(\s[^>]*)?>/g, "<Note$1>").replace(/<\/Check>/g, "</Note>").replace(/<Tree\.Folder/g, "<Folder").replace(/<\/Tree\.Folder>/g, "</Folder>").replace(/<Tree\.File/g, "<File").replace(/<\/Tree\.File>/g, "</File>");
1234
1249
  if (runDocusaurus) {
1235
1250
  result = result.replace(/<TabItem\b([^>]*)>/g, (_match, attributes) => {
1236
1251
  const title = attributes.match(/\blabel=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean) ?? attributes.match(/\bvalue=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean) ?? "Tab";
@@ -1290,11 +1305,26 @@ function parseMarkdownPage(input) {
1290
1305
  hidden: parsed.data.hidden === true ? true : void 0,
1291
1306
  noindex: parsed.data.noindex === true || parsed.data.noindex === "true" ? true : void 0,
1292
1307
  openapi: typeof parsed.data.openapi === "string" ? parsed.data.openapi.trim() : void 0,
1308
+ ...apiFrontmatter(parsed.data, input.warn),
1293
1309
  body,
1294
1310
  source: input.source,
1295
1311
  ...parsed.error ? { frontmatterError: parsed.error } : {}
1296
1312
  };
1297
1313
  }
1314
+ var AUTH_METHODS = /* @__PURE__ */ new Set(["bearer", "basic", "key", "none"]);
1315
+ function apiFrontmatter(data, warn) {
1316
+ const result = {};
1317
+ if (data.api !== void 0 && data.api !== null) {
1318
+ if (typeof data.api === "string" && data.api.trim()) result.api = data.api.trim();
1319
+ else warn?.(`The page's "api" frontmatter is not a "METHOD url-or-path" string and was dropped.`);
1320
+ }
1321
+ if (data.authMethod !== void 0 && data.authMethod !== null) {
1322
+ const method = typeof data.authMethod === "string" ? data.authMethod.trim().toLowerCase() : "";
1323
+ if (AUTH_METHODS.has(method)) result.authMethod = method;
1324
+ else warn?.(`The page's "authMethod" frontmatter ${JSON.stringify(data.authMethod)} is not one of bearer, basic, key, none and was dropped.`);
1325
+ }
1326
+ return result;
1327
+ }
1298
1328
 
1299
1329
  // src/path.ts
1300
1330
  import { isAbsolute, relative, resolve, sep } from "path";
@@ -2365,7 +2395,8 @@ function neutralizeUnresolvableImportsWithoutAst(content, currentFile, warn) {
2365
2395
  });
2366
2396
  for (const { name, specifier } of dropped) {
2367
2397
  warn(`Component import ${JSON.stringify(specifier)} could not be resolved because this page has invalid MDX (see the previous warning), so the import and its usage were removed to keep the site building.`, currentFile);
2368
- result = result.replace(new RegExp(`<${name}(?:\\s[^>]*)?/>`, "g"), `{/* Removed <${name}>: unresolved during fallback migration */}`).replace(new RegExp(`<${name}(?:\\s[^>]*)?>[\\s\\S]*?<\\/${name}>`, "g"), `{/* Removed <${name}>: unresolved during fallback migration */}`);
2398
+ const removed = mdxComment(` Removed <${name}>: unresolved during fallback migration `);
2399
+ result = replaceOutsideCodeAndComments(result, (segment) => segment.replace(new RegExp(`<${name}(?:\\s[^>]*)?/>`, "g"), removed).replace(new RegExp(`<${name}(?:\\s[^>]*)?>[\\s\\S]*?<\\/${name}>`, "g"), removed));
2369
2400
  }
2370
2401
  return result;
2371
2402
  }
@@ -3028,7 +3059,7 @@ ${inlineSource}` });
3028
3059
  const start = node.position.start.offset;
3029
3060
  const end = node.position.end.offset;
3030
3061
  const videoId = youtubeEmbedVideoId(node, specifier);
3031
- const value = videoId ? `<iframe width="560" height="315" src="https://www.youtube.com/embed/${videoId}" title="Embedded video" allowFullScreen />` : `{/* Removed <${node.name}>: unsupported import '${specifier}' */}`;
3062
+ const value = videoId ? `<iframe width="560" height="315" src="https://www.youtube.com/embed/${videoId}" title="Embedded video" allowFullScreen />` : mdxComment(` Removed <${node.name}>: unsupported import '${specifier}' `);
3032
3063
  edits.push({ start, end, value });
3033
3064
  });
3034
3065
  const rendered = applyReplacements(content, edits);
@@ -3906,6 +3937,39 @@ function projectedCompatibleConfig(config, warnings) {
3906
3937
  ...typeof feedback?.thumbsRating === "boolean" ? { feedback: { thumbsRating: feedback.thumbsRating } } : {}
3907
3938
  };
3908
3939
  }
3940
+ function projectedApiMdx(config, warnings) {
3941
+ const mdx = objectValue2(objectValue2(config.api)?.mdx);
3942
+ if (!mdx) return {};
3943
+ const warn = (message) => warnings.push({ code: "unsupported-config", message });
3944
+ const rawServers = mdx.server === void 0 ? [] : Array.isArray(mdx.server) ? mdx.server : [mdx.server];
3945
+ const servers = rawServers.filter((entry) => {
3946
+ const valid = typeof entry === "string" && /^https?:\/\/[^\s/@?#]+(?:\/[^\s?#]*)?$/i.test(entry.trim());
3947
+ if (!valid) warn(`api.mdx.server entry ${JSON.stringify(entry)} is not an absolute http(s) URL and was dropped.`);
3948
+ return valid;
3949
+ }).map((entry) => entry.trim());
3950
+ const auth = objectValue2(mdx.auth);
3951
+ const method = typeof auth?.method === "string" ? auth.method.toLowerCase() : void 0;
3952
+ const name = typeof auth?.name === "string" && auth.name.trim() ? auth.name.trim() : void 0;
3953
+ let projectedAuth;
3954
+ if (mdx.auth !== void 0 && auth && method !== void 0) {
3955
+ if (method === "bearer" || method === "basic" || method === "key" && name) {
3956
+ projectedAuth = { method, ...name ? { name } : {} };
3957
+ } else {
3958
+ warn(`api.mdx.auth method ${JSON.stringify(auth.method)} is not supported (bearer, basic, or key with a name) and was dropped.`);
3959
+ }
3960
+ } else if (mdx.auth !== void 0 && !auth) {
3961
+ warn("api.mdx.auth is not an object and was dropped.");
3962
+ }
3963
+ if (servers.length === 0 && !projectedAuth) return {};
3964
+ return {
3965
+ api: {
3966
+ mdx: {
3967
+ ...servers.length > 0 ? { server: servers.length === 1 && typeof mdx.server === "string" ? servers[0] : servers } : {},
3968
+ ...projectedAuth ? { auth: projectedAuth } : {}
3969
+ }
3970
+ }
3971
+ };
3972
+ }
3909
3973
  var NEXT_REDIRECT_PARAM_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
3910
3974
  function translateRedirectWildcards(source, destination) {
3911
3975
  const sourceSegments = source.split("/");
@@ -4093,6 +4157,7 @@ function projectMintlifyNavigation(config, options = {}) {
4093
4157
  tabs,
4094
4158
  ...projectionTrace.rootContainerKind === "dropdowns" ? { navigation: { display: "dropdown" } } : {},
4095
4159
  ...projectedCompatibleConfig(config, warnings),
4160
+ ...projectedApiMdx(config, warnings),
4096
4161
  ...i18n ? { i18n } : {},
4097
4162
  ...redirects.length > 0 ? { redirects } : {}
4098
4163
  },
@@ -5661,6 +5726,26 @@ function projectFernNavigation(input) {
5661
5726
  };
5662
5727
  }
5663
5728
 
5729
+ // src/openapi-ref.ts
5730
+ function unquote(value) {
5731
+ const trimmed = value.trim();
5732
+ const quote = trimmed[0];
5733
+ return (quote === '"' || quote === "'") && trimmed.length >= 2 && trimmed.endsWith(quote) ? trimmed.slice(1, -1).trim() : trimmed;
5734
+ }
5735
+ function splitOpenApiRef(raw) {
5736
+ const value = unquote(raw);
5737
+ const match = /^(?:(.+?)\s+)?((?:webhook\s+[^\s/]\S*)|(?:[A-Za-z]+\s+\/.*))$/i.exec(value);
5738
+ if (!match) return null;
5739
+ const specRef = match[1] ? unquote(match[1]) : void 0;
5740
+ return { ...specRef ? { specRef } : {}, operation: match[2] };
5741
+ }
5742
+ function specRefBaseName(specRef) {
5743
+ return specRef.trim().replace(/\\/g, "/").split(/[?#]/, 1)[0].split("/").filter(Boolean).pop()?.toLowerCase() ?? "";
5744
+ }
5745
+ function withSpecRef(ref, specRef) {
5746
+ return `${specRef} ${ref.operation}`;
5747
+ }
5748
+
5664
5749
  // src/analytics.ts
5665
5750
  var GA4_ID = /^G-[A-Z0-9]{4,20}$/;
5666
5751
  var GTM_ID = /^GTM-[A-Z0-9]{4,12}$/;
@@ -6572,7 +6657,7 @@ function selectGroupWithinBudget(scanned, rank, warnings, allVersionPrefixes, la
6572
6657
  const dropped = ranked.slice(budget).map(({ file }) => file);
6573
6658
  if (warnings) {
6574
6659
  const droppedPages = label === "file" ? dropped.filter((file) => isDocumentationExtension(file.relativePath)) : dropped;
6575
- const noun = label === "file" ? "page" : "image or media file";
6660
+ const noun = label === "file" ? "page" : label === "snippet" ? "snippet file" : "image or media file";
6576
6661
  const droppedVersions = /* @__PURE__ */ new Set();
6577
6662
  for (const file of dropped) {
6578
6663
  const firstSegment = file.relativePath.split("/", 1)[0];
@@ -6582,16 +6667,19 @@ function selectGroupWithinBudget(scanned, rank, warnings, allVersionPrefixes, la
6582
6667
  const rest = droppedPages.length - examples.length;
6583
6668
  warnings.push({
6584
6669
  code: "limit-reached",
6585
- message: `This repository has more than ${budget} ${label === "file" ? "files" : "assets"}, so only the first ${budget} (in navigation order, default version first) were migrated. ${droppedPages.length} ${noun}(s) were left out` + (examples.length > 0 ? `: ${examples.join(", ")}${rest > 0 ? `, and ${rest} more` : ""}` : "") + (droppedVersions.size > 0 ? ` (versions: ${[...droppedVersions].slice(0, 5).join(", ")}${droppedVersions.size > 5 ? `, and ${droppedVersions.size - 5} more` : ""})` : "") + (label === "file" ? ". To include them, run the migration on a smaller part of the repository with --docs-dir." : ". Copy them into public/ manually if your pages use them.")
6670
+ message: `This repository has more than ${budget} ${label === "file" ? "files" : label === "snippet" ? "snippets" : "assets"}, so only the first ${budget} (in navigation order, default version first) were migrated. ${droppedPages.length} ${noun}(s) were left out` + (examples.length > 0 ? `: ${examples.join(", ")}${rest > 0 ? `, and ${rest} more` : ""}` : "") + (droppedVersions.size > 0 ? ` (versions: ${[...droppedVersions].slice(0, 5).join(", ")}${droppedVersions.size > 5 ? `, and ${droppedVersions.size - 5} more` : ""})` : "") + (label === "asset" ? ". Copy them into public/ manually if your pages use them." : ". To include them, run the migration on a smaller part of the repository with --docs-dir.")
6586
6671
  });
6587
6672
  }
6588
6673
  return ranked.slice(0, budget).map(({ file }) => file);
6589
6674
  }
6590
6675
  function selectFilesWithinBudget(scanned, rank, warnings, allVersionPrefixes, budget = MAX_SOURCE_FILES) {
6591
6676
  const isAsset = (file) => ASSET_EXTENSIONS.has(extname6(file.relativePath).toLowerCase());
6677
+ const isSnippet = (file) => file.relativePath.split("/").some((segment) => SNIPPET_DIRECTORIES.has(segment.toLowerCase()));
6678
+ const snippetFiles = scanned.filter((file) => !isAsset(file) && isSnippet(file));
6592
6679
  const assetFiles = scanned.filter(isAsset);
6593
- const otherFiles = scanned.filter((file) => !isAsset(file));
6680
+ const otherFiles = scanned.filter((file) => !isAsset(file) && !isSnippet(file));
6594
6681
  return [
6682
+ ...selectGroupWithinBudget(snippetFiles, rank, warnings, allVersionPrefixes, "snippet", budget),
6595
6683
  ...selectGroupWithinBudget(otherFiles, rank, warnings, allVersionPrefixes, "file", budget),
6596
6684
  ...selectGroupWithinBudget(assetFiles, rank, warnings, allVersionPrefixes, "asset", budget)
6597
6685
  ];
@@ -6792,6 +6880,27 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs) {
6792
6880
  }
6793
6881
  return specs;
6794
6882
  }
6883
+ function rewriteMintlifyPageSpecRefs(pages, specs, remoteUrls, warnings) {
6884
+ for (const page of pages) {
6885
+ const ref = page.openapi ? splitOpenApiRef(page.openapi) : null;
6886
+ if (!ref?.specRef) continue;
6887
+ if (/^https?:\/\//i.test(ref.specRef)) {
6888
+ if (!remoteUrls.has(ref.specRef)) {
6889
+ warnings.push({ code: "unsupported-config", message: `Page "${page.id}" references the remote OpenAPI spec "${ref.specRef}", which is not referenced from docs.json and was not migrated; the page will not render an endpoint.`, source: page.source });
6890
+ }
6891
+ continue;
6892
+ }
6893
+ const key = ref.specRef.split(/[?#]/, 1)[0].replace(/\\/g, "/").replace(/^(?:\.\/)+/, "").replace(/^\/+/, "");
6894
+ const byPath = specs.find((spec) => spec.sourcePath === key);
6895
+ const byName = specs.filter((spec) => spec.filename.toLowerCase() === specRefBaseName(key));
6896
+ const match = byPath ?? (byName.length === 1 ? byName[0] : void 0);
6897
+ if (match) {
6898
+ page.openapi = withSpecRef(ref, specAssetPath(match.filename));
6899
+ } else {
6900
+ 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 });
6901
+ }
6902
+ }
6903
+ }
6795
6904
  var OPENAPI_HTTP_METHODS = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
6796
6905
  var FERN_CRUD_METHOD_NAMES = {
6797
6906
  findall: "list",
@@ -7069,6 +7178,13 @@ function interpolateSnippet(snippet, attributes, children) {
7069
7178
  });
7070
7179
  }).join("\n");
7071
7180
  }
7181
+ function isolateFences(snippet, source, start, end) {
7182
+ const fence = /^\s*(?:`{3,}|~{3,})/;
7183
+ const lines = snippet.split("\n");
7184
+ const before = source.slice(source.lastIndexOf("\n", start - 1) + 1, start);
7185
+ const after = source.slice(end, source.indexOf("\n", end) < 0 ? void 0 : source.indexOf("\n", end));
7186
+ return `${fence.test(lines[0]) && /\S/.test(before) ? "\n" : ""}${snippet}${fence.test(lines[lines.length - 1]) && /\S/.test(after) ? "\n" : ""}`;
7187
+ }
7072
7188
  function snippetComponentBody(source, componentName) {
7073
7189
  const parsed = ts3.createSourceFile("snippet.tsx", source, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TSX);
7074
7190
  for (const statement of parsed.statements) {
@@ -7517,7 +7633,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7517
7633
  message: `Imported snippet ${sourcePath} could not be resolved and was left as a comment.`,
7518
7634
  source: relative5(repositoryRoot, currentFile).replace(/\\/g, "/")
7519
7635
  });
7520
- snippets.set(componentName, `{/* Missing imported snippet: ${sourcePath} */}`);
7636
+ snippets.set(componentName, mdxComment(` Missing imported snippet: ${sourcePath} `));
7521
7637
  return "";
7522
7638
  }
7523
7639
  }
@@ -7586,48 +7702,49 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7586
7702
  if (declaration) preservedDeclarations.set(componentName, declaration);
7587
7703
  else snippets.set(componentName, snippetComponentBody(nested, componentName));
7588
7704
  }
7589
- let result = withoutImports;
7590
- for (const [componentName, snippet] of snippets) {
7591
- result = result.replace(new RegExp(`<${componentName}((?:\\s[^>]*)?)\\s*/>`, "g"), (_tag, attributes) => {
7592
- return interpolateSnippet(snippet, attributes);
7593
- }).replace(new RegExp(`<${componentName}((?:\\s[^>]*)?)>([\\s\\S]*?)<\\/${componentName}>`, "g"), (_tag, attributes, children) => {
7594
- return interpolateSnippet(snippet, attributes, children);
7595
- });
7596
- }
7597
- result = result.replace(SNIPPET_TAG_PATTERN, (_tag, doubleQuoted, singleQuoted) => {
7598
- const filePath = doubleQuoted ?? singleQuoted;
7599
- try {
7600
- const candidate = [
7601
- () => resolveWithin(siteRoot, `snippets/${filePath}`),
7602
- () => resolveSnippetPath(filePath, currentFile, repositoryRoot, siteRoot)
7603
- ].map((resolveCandidate) => {
7604
- try {
7605
- return resolveCandidate();
7606
- } catch {
7607
- return void 0;
7608
- }
7609
- }).find((path) => path !== void 0 && existsSync5(path) && lstatSync5(path).isFile());
7610
- if (!candidate) throw new Error("file not found");
7611
- const blocked = blockInline(candidate, filePath, `Snippet file="${filePath}"`);
7612
- if (blocked) return blocked;
7613
- return inlineMdxSnippets(
7614
- withoutFrontmatter(readFileSync5(candidate, "utf8")),
7615
- candidate,
7616
- repositoryRoot,
7617
- warnings,
7618
- depth + 1,
7619
- siteRoot,
7620
- globalAliases,
7621
- gate
7622
- );
7623
- } catch {
7624
- warnings.push({
7625
- code: "missing-page",
7626
- message: `Snippet file="${filePath}" could not be resolved and was left as a comment.`,
7627
- source: relative5(repositoryRoot, currentFile).replace(/\\/g, "/")
7705
+ let result = replaceOutsideCodeAndComments(withoutImports, (segment) => {
7706
+ for (const [componentName, snippet] of snippets) {
7707
+ segment = segment.replace(new RegExp(`<${componentName}((?:\\s[^>]*)?)\\s*/>`, "g"), (tag, attributes, offset, source2) => {
7708
+ return isolateFences(interpolateSnippet(snippet, attributes), source2, offset, offset + tag.length);
7709
+ }).replace(new RegExp(`<${componentName}((?:\\s[^>]*)?)>([\\s\\S]*?)<\\/${componentName}>`, "g"), (tag, attributes, children, offset, source2) => {
7710
+ return isolateFences(interpolateSnippet(snippet, attributes, children), source2, offset, offset + tag.length);
7628
7711
  });
7629
- return `{/* Missing snippet: ${filePath} */}`;
7630
7712
  }
7713
+ return segment.replace(SNIPPET_TAG_PATTERN, (tag, doubleQuoted, singleQuoted, offset, source2) => {
7714
+ const filePath = doubleQuoted ?? singleQuoted;
7715
+ try {
7716
+ const candidate = [
7717
+ () => resolveWithin(siteRoot, `snippets/${filePath}`),
7718
+ () => resolveSnippetPath(filePath, currentFile, repositoryRoot, siteRoot)
7719
+ ].map((resolveCandidate) => {
7720
+ try {
7721
+ return resolveCandidate();
7722
+ } catch {
7723
+ return void 0;
7724
+ }
7725
+ }).find((path) => path !== void 0 && existsSync5(path) && lstatSync5(path).isFile());
7726
+ if (!candidate) throw new Error("file not found");
7727
+ const blocked = blockInline(candidate, filePath, `Snippet file="${filePath}"`);
7728
+ if (blocked) return blocked;
7729
+ return isolateFences(inlineMdxSnippets(
7730
+ withoutFrontmatter(readFileSync5(candidate, "utf8")),
7731
+ candidate,
7732
+ repositoryRoot,
7733
+ warnings,
7734
+ depth + 1,
7735
+ siteRoot,
7736
+ globalAliases,
7737
+ gate
7738
+ ), source2, offset, offset + tag.length);
7739
+ } catch {
7740
+ warnings.push({
7741
+ code: "missing-page",
7742
+ message: `Snippet file="${filePath}" could not be resolved and was left as a comment.`,
7743
+ source: relative5(repositoryRoot, currentFile).replace(/\\/g, "/")
7744
+ });
7745
+ return mdxComment(` Missing snippet: ${filePath} `);
7746
+ }
7747
+ });
7631
7748
  });
7632
7749
  if (preservedDeclarations.size > 0) {
7633
7750
  const frontmatter = result.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
@@ -7636,7 +7753,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7636
7753
 
7637
7754
  ${result.slice(prefix.length)}`;
7638
7755
  }
7639
- return depth === 0 ? hoistMdxImports(result) : result;
7756
+ return depth === 0 ? hoistMdxImports(result) : closeOpenFence(result);
7640
7757
  }
7641
7758
  function injectOpenApiSpecs(config, specs, warnings) {
7642
7759
  const tabs = config.tabs.map((tab) => ({ ...tab }));
@@ -8124,7 +8241,7 @@ function migrateRepository(options) {
8124
8241
  const publishedText = trackPublishedRefs ? raw : void 0;
8125
8242
  const pageAssetReferences = [];
8126
8243
  if (platform === "fern" || platform === "mintlify" || platform === "docusaurus") {
8127
- raw = normalizeExplicitHeadingIds(raw);
8244
+ raw = normalizeExplicitHeadingIds(raw, (message) => warnings.push({ code: "invalid-page", message, source: file.relativePath }));
8128
8245
  }
8129
8246
  if (componentMigrator) {
8130
8247
  const warningsBeforeTransform = warnings.length;
@@ -8159,6 +8276,7 @@ function migrateRepository(options) {
8159
8276
  const page = parseMarkdownPage({
8160
8277
  id,
8161
8278
  navigationId,
8279
+ warn: (message) => warnings.push({ code: "unsupported-config", message, source: relative5(repositoryDir, file.absolutePath).replace(/\\/g, "/") }),
8162
8280
  ...locale ? { locale } : {},
8163
8281
  raw,
8164
8282
  platform,
@@ -8619,6 +8737,7 @@ function migrateRepository(options) {
8619
8737
  }
8620
8738
  if (withheldSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, spec.filename, true))) warnSharedSpec(spec.sourcePath);
8621
8739
  }
8740
+ let pageSpecs = resolvedSpecs;
8622
8741
  if (resolvedSpecs.length > 0) {
8623
8742
  docsConfig = injectOpenApiSpecs(docsConfig, resolvedSpecs, warnings);
8624
8743
  const { operationLinks, prefixLandings } = apiOperationLinkMap(
@@ -8639,10 +8758,12 @@ function migrateRepository(options) {
8639
8758
  assets.push(specAsset(filename, readFileSync5(fallback.absolutePath)));
8640
8759
  }
8641
8760
  docsConfig = injectOpenApiSpecs(docsConfig, [{ filename }]);
8761
+ pageSpecs = [{ filename, sourcePath: fallback.relativePath }];
8642
8762
  if (withheldSpecRefs.some(matches)) warnSharedSpec(fallback.relativePath);
8643
8763
  }
8644
8764
  }
8645
8765
  }
8766
+ rewriteMintlifyPageSpecRefs(pages, pageSpecs, new Set(remoteApiSpecs.map((spec) => spec.url)), warnings);
8646
8767
  }
8647
8768
  if (platform === "mintlify") {
8648
8769
  const sources = new Set((docsConfig.redirects ?? []).map((redirect) => redirect.source));
@@ -9065,6 +9186,8 @@ function renderPage(bundle, page) {
9065
9186
  page.hidden ? "hidden: true" : null,
9066
9187
  page.noindex ? "noindex: true" : null,
9067
9188
  page.openapi ? `openapi: ${yamlString(page.openapi)}` : null,
9189
+ page.api ? `api: ${yamlString(page.api)}` : null,
9190
+ page.authMethod ? `authMethod: ${yamlString(page.authMethod)}` : null,
9068
9191
  bundle.sourceKind === "url" ? `source: ${yamlString(page.source)}` : null,
9069
9192
  "---",
9070
9193
  "",
@@ -10892,6 +11015,7 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
10892
11015
  const tabs = bundle.docsConfig.tabs.map((tab) => ({ ...tab }));
10893
11016
  const redirects = [...bundle.docsConfig.redirects ?? []];
10894
11017
  const warnings = [...bundle.warnings];
11018
+ const pages = bundle.pages.map((page) => ({ ...page }));
10895
11019
  const authoredLinks = /* @__PURE__ */ new Set();
10896
11020
  for (const page of bundle.pages) {
10897
11021
  for (const match of page.body.matchAll(/\/[a-z0-9_/-]+/gi)) {
@@ -10912,6 +11036,10 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
10912
11036
  const operations = parseOpenApi(body, extension);
10913
11037
  const filename = `openapi-${createHash2("sha256").update(reference.url).digest("hex").slice(0, 12)}.${extension}`;
10914
11038
  assets.push({ path: `openapi/${filename}`, content: body, projectRelative: true });
11039
+ for (const page of pages) {
11040
+ const ref = page.openapi ? splitOpenApiRef(page.openapi) : null;
11041
+ if (ref?.specRef === reference.url) page.openapi = withSpecRef(ref, `openapi/${filename}`);
11042
+ }
10915
11043
  const tab = reference.tabLabel ? tabs.find((candidate) => candidate.tab === reference.tabLabel) : tabs.find((candidate) => candidate.tab.toLowerCase().includes("api"));
10916
11044
  if (tab) tab.api = { source: `openapi/${filename}` };
10917
11045
  else insertApiTab(tabs, { tab: reference.tabLabel ?? "API Reference", ...reference.icon ? { icon: reference.icon } : {}, ...reference.hidden ? { hidden: true } : {}, api: { source: `openapi/${filename}` } }, reference.parentTab);
@@ -10940,6 +11068,7 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
10940
11068
  }
10941
11069
  return {
10942
11070
  ...bundle,
11071
+ pages,
10943
11072
  assets,
10944
11073
  docsConfig: { ...bundle.docsConfig, tabs, ...redirects.length > 0 ? { redirects } : {} },
10945
11074
  warnings