@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 +7 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +186 -57
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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 (
|
|
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) =>
|
|
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) =>
|
|
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
|
-
|
|
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 />` : `
|
|
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 === "
|
|
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, `
|
|
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
|
-
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
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
|