@thallylabs/migrate 0.2.18 → 0.2.19
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 +102 -0
- 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
|
@@ -1290,11 +1290,26 @@ function parseMarkdownPage(input) {
|
|
|
1290
1290
|
hidden: parsed.data.hidden === true ? true : void 0,
|
|
1291
1291
|
noindex: parsed.data.noindex === true || parsed.data.noindex === "true" ? true : void 0,
|
|
1292
1292
|
openapi: typeof parsed.data.openapi === "string" ? parsed.data.openapi.trim() : void 0,
|
|
1293
|
+
...apiFrontmatter(parsed.data, input.warn),
|
|
1293
1294
|
body,
|
|
1294
1295
|
source: input.source,
|
|
1295
1296
|
...parsed.error ? { frontmatterError: parsed.error } : {}
|
|
1296
1297
|
};
|
|
1297
1298
|
}
|
|
1299
|
+
var AUTH_METHODS = /* @__PURE__ */ new Set(["bearer", "basic", "key", "none"]);
|
|
1300
|
+
function apiFrontmatter(data, warn) {
|
|
1301
|
+
const result = {};
|
|
1302
|
+
if (data.api !== void 0 && data.api !== null) {
|
|
1303
|
+
if (typeof data.api === "string" && data.api.trim()) result.api = data.api.trim();
|
|
1304
|
+
else warn?.(`The page's "api" frontmatter is not a "METHOD url-or-path" string and was dropped.`);
|
|
1305
|
+
}
|
|
1306
|
+
if (data.authMethod !== void 0 && data.authMethod !== null) {
|
|
1307
|
+
const method = typeof data.authMethod === "string" ? data.authMethod.trim().toLowerCase() : "";
|
|
1308
|
+
if (AUTH_METHODS.has(method)) result.authMethod = method;
|
|
1309
|
+
else warn?.(`The page's "authMethod" frontmatter ${JSON.stringify(data.authMethod)} is not one of bearer, basic, key, none and was dropped.`);
|
|
1310
|
+
}
|
|
1311
|
+
return result;
|
|
1312
|
+
}
|
|
1298
1313
|
|
|
1299
1314
|
// src/path.ts
|
|
1300
1315
|
import { isAbsolute, relative, resolve, sep } from "path";
|
|
@@ -3906,6 +3921,39 @@ function projectedCompatibleConfig(config, warnings) {
|
|
|
3906
3921
|
...typeof feedback?.thumbsRating === "boolean" ? { feedback: { thumbsRating: feedback.thumbsRating } } : {}
|
|
3907
3922
|
};
|
|
3908
3923
|
}
|
|
3924
|
+
function projectedApiMdx(config, warnings) {
|
|
3925
|
+
const mdx = objectValue2(objectValue2(config.api)?.mdx);
|
|
3926
|
+
if (!mdx) return {};
|
|
3927
|
+
const warn = (message) => warnings.push({ code: "unsupported-config", message });
|
|
3928
|
+
const rawServers = mdx.server === void 0 ? [] : Array.isArray(mdx.server) ? mdx.server : [mdx.server];
|
|
3929
|
+
const servers = rawServers.filter((entry) => {
|
|
3930
|
+
const valid = typeof entry === "string" && /^https?:\/\/[^\s/@?#]+(?:\/[^\s?#]*)?$/i.test(entry.trim());
|
|
3931
|
+
if (!valid) warn(`api.mdx.server entry ${JSON.stringify(entry)} is not an absolute http(s) URL and was dropped.`);
|
|
3932
|
+
return valid;
|
|
3933
|
+
}).map((entry) => entry.trim());
|
|
3934
|
+
const auth = objectValue2(mdx.auth);
|
|
3935
|
+
const method = typeof auth?.method === "string" ? auth.method.toLowerCase() : void 0;
|
|
3936
|
+
const name = typeof auth?.name === "string" && auth.name.trim() ? auth.name.trim() : void 0;
|
|
3937
|
+
let projectedAuth;
|
|
3938
|
+
if (mdx.auth !== void 0 && auth && method !== void 0) {
|
|
3939
|
+
if (method === "bearer" || method === "basic" || method === "key" && name) {
|
|
3940
|
+
projectedAuth = { method, ...name ? { name } : {} };
|
|
3941
|
+
} else {
|
|
3942
|
+
warn(`api.mdx.auth method ${JSON.stringify(auth.method)} is not supported (bearer, basic, or key with a name) and was dropped.`);
|
|
3943
|
+
}
|
|
3944
|
+
} else if (mdx.auth !== void 0 && !auth) {
|
|
3945
|
+
warn("api.mdx.auth is not an object and was dropped.");
|
|
3946
|
+
}
|
|
3947
|
+
if (servers.length === 0 && !projectedAuth) return {};
|
|
3948
|
+
return {
|
|
3949
|
+
api: {
|
|
3950
|
+
mdx: {
|
|
3951
|
+
...servers.length > 0 ? { server: servers.length === 1 && typeof mdx.server === "string" ? servers[0] : servers } : {},
|
|
3952
|
+
...projectedAuth ? { auth: projectedAuth } : {}
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
};
|
|
3956
|
+
}
|
|
3909
3957
|
var NEXT_REDIRECT_PARAM_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3910
3958
|
function translateRedirectWildcards(source, destination) {
|
|
3911
3959
|
const sourceSegments = source.split("/");
|
|
@@ -4093,6 +4141,7 @@ function projectMintlifyNavigation(config, options = {}) {
|
|
|
4093
4141
|
tabs,
|
|
4094
4142
|
...projectionTrace.rootContainerKind === "dropdowns" ? { navigation: { display: "dropdown" } } : {},
|
|
4095
4143
|
...projectedCompatibleConfig(config, warnings),
|
|
4144
|
+
...projectedApiMdx(config, warnings),
|
|
4096
4145
|
...i18n ? { i18n } : {},
|
|
4097
4146
|
...redirects.length > 0 ? { redirects } : {}
|
|
4098
4147
|
},
|
|
@@ -5661,6 +5710,26 @@ function projectFernNavigation(input) {
|
|
|
5661
5710
|
};
|
|
5662
5711
|
}
|
|
5663
5712
|
|
|
5713
|
+
// src/openapi-ref.ts
|
|
5714
|
+
function unquote(value) {
|
|
5715
|
+
const trimmed = value.trim();
|
|
5716
|
+
const quote = trimmed[0];
|
|
5717
|
+
return (quote === '"' || quote === "'") && trimmed.length >= 2 && trimmed.endsWith(quote) ? trimmed.slice(1, -1).trim() : trimmed;
|
|
5718
|
+
}
|
|
5719
|
+
function splitOpenApiRef(raw) {
|
|
5720
|
+
const value = unquote(raw);
|
|
5721
|
+
const match = /^(?:(.+?)\s+)?((?:webhook\s+[^\s/]\S*)|(?:[A-Za-z]+\s+\/.*))$/i.exec(value);
|
|
5722
|
+
if (!match) return null;
|
|
5723
|
+
const specRef = match[1] ? unquote(match[1]) : void 0;
|
|
5724
|
+
return { ...specRef ? { specRef } : {}, operation: match[2] };
|
|
5725
|
+
}
|
|
5726
|
+
function specRefBaseName(specRef) {
|
|
5727
|
+
return specRef.trim().replace(/\\/g, "/").split(/[?#]/, 1)[0].split("/").filter(Boolean).pop()?.toLowerCase() ?? "";
|
|
5728
|
+
}
|
|
5729
|
+
function withSpecRef(ref, specRef) {
|
|
5730
|
+
return `${specRef} ${ref.operation}`;
|
|
5731
|
+
}
|
|
5732
|
+
|
|
5664
5733
|
// src/analytics.ts
|
|
5665
5734
|
var GA4_ID = /^G-[A-Z0-9]{4,20}$/;
|
|
5666
5735
|
var GTM_ID = /^GTM-[A-Z0-9]{4,12}$/;
|
|
@@ -6792,6 +6861,27 @@ function resolveMintlifyApiSpecs(mintlifyConfig, files, warnings, remoteSpecs) {
|
|
|
6792
6861
|
}
|
|
6793
6862
|
return specs;
|
|
6794
6863
|
}
|
|
6864
|
+
function rewriteMintlifyPageSpecRefs(pages, specs, remoteUrls, warnings) {
|
|
6865
|
+
for (const page of pages) {
|
|
6866
|
+
const ref = page.openapi ? splitOpenApiRef(page.openapi) : null;
|
|
6867
|
+
if (!ref?.specRef) continue;
|
|
6868
|
+
if (/^https?:\/\//i.test(ref.specRef)) {
|
|
6869
|
+
if (!remoteUrls.has(ref.specRef)) {
|
|
6870
|
+
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 });
|
|
6871
|
+
}
|
|
6872
|
+
continue;
|
|
6873
|
+
}
|
|
6874
|
+
const key = ref.specRef.split(/[?#]/, 1)[0].replace(/\\/g, "/").replace(/^(?:\.\/)+/, "").replace(/^\/+/, "");
|
|
6875
|
+
const byPath = specs.find((spec) => spec.sourcePath === key);
|
|
6876
|
+
const byName = specs.filter((spec) => spec.filename.toLowerCase() === specRefBaseName(key));
|
|
6877
|
+
const match = byPath ?? (byName.length === 1 ? byName[0] : void 0);
|
|
6878
|
+
if (match) {
|
|
6879
|
+
page.openapi = withSpecRef(ref, specAssetPath(match.filename));
|
|
6880
|
+
} else {
|
|
6881
|
+
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 });
|
|
6882
|
+
}
|
|
6883
|
+
}
|
|
6884
|
+
}
|
|
6795
6885
|
var OPENAPI_HTTP_METHODS = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
|
|
6796
6886
|
var FERN_CRUD_METHOD_NAMES = {
|
|
6797
6887
|
findall: "list",
|
|
@@ -8159,6 +8249,7 @@ function migrateRepository(options) {
|
|
|
8159
8249
|
const page = parseMarkdownPage({
|
|
8160
8250
|
id,
|
|
8161
8251
|
navigationId,
|
|
8252
|
+
warn: (message) => warnings.push({ code: "unsupported-config", message, source: relative5(repositoryDir, file.absolutePath).replace(/\\/g, "/") }),
|
|
8162
8253
|
...locale ? { locale } : {},
|
|
8163
8254
|
raw,
|
|
8164
8255
|
platform,
|
|
@@ -8619,6 +8710,7 @@ function migrateRepository(options) {
|
|
|
8619
8710
|
}
|
|
8620
8711
|
if (withheldSpecRefs.some((ref) => specRefMatches(ref, spec.sourcePath, spec.filename, true))) warnSharedSpec(spec.sourcePath);
|
|
8621
8712
|
}
|
|
8713
|
+
let pageSpecs = resolvedSpecs;
|
|
8622
8714
|
if (resolvedSpecs.length > 0) {
|
|
8623
8715
|
docsConfig = injectOpenApiSpecs(docsConfig, resolvedSpecs, warnings);
|
|
8624
8716
|
const { operationLinks, prefixLandings } = apiOperationLinkMap(
|
|
@@ -8639,10 +8731,12 @@ function migrateRepository(options) {
|
|
|
8639
8731
|
assets.push(specAsset(filename, readFileSync5(fallback.absolutePath)));
|
|
8640
8732
|
}
|
|
8641
8733
|
docsConfig = injectOpenApiSpecs(docsConfig, [{ filename }]);
|
|
8734
|
+
pageSpecs = [{ filename, sourcePath: fallback.relativePath }];
|
|
8642
8735
|
if (withheldSpecRefs.some(matches)) warnSharedSpec(fallback.relativePath);
|
|
8643
8736
|
}
|
|
8644
8737
|
}
|
|
8645
8738
|
}
|
|
8739
|
+
rewriteMintlifyPageSpecRefs(pages, pageSpecs, new Set(remoteApiSpecs.map((spec) => spec.url)), warnings);
|
|
8646
8740
|
}
|
|
8647
8741
|
if (platform === "mintlify") {
|
|
8648
8742
|
const sources = new Set((docsConfig.redirects ?? []).map((redirect) => redirect.source));
|
|
@@ -9065,6 +9159,8 @@ function renderPage(bundle, page) {
|
|
|
9065
9159
|
page.hidden ? "hidden: true" : null,
|
|
9066
9160
|
page.noindex ? "noindex: true" : null,
|
|
9067
9161
|
page.openapi ? `openapi: ${yamlString(page.openapi)}` : null,
|
|
9162
|
+
page.api ? `api: ${yamlString(page.api)}` : null,
|
|
9163
|
+
page.authMethod ? `authMethod: ${yamlString(page.authMethod)}` : null,
|
|
9068
9164
|
bundle.sourceKind === "url" ? `source: ${yamlString(page.source)}` : null,
|
|
9069
9165
|
"---",
|
|
9070
9166
|
"",
|
|
@@ -10892,6 +10988,7 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
|
|
|
10892
10988
|
const tabs = bundle.docsConfig.tabs.map((tab) => ({ ...tab }));
|
|
10893
10989
|
const redirects = [...bundle.docsConfig.redirects ?? []];
|
|
10894
10990
|
const warnings = [...bundle.warnings];
|
|
10991
|
+
const pages = bundle.pages.map((page) => ({ ...page }));
|
|
10895
10992
|
const authoredLinks = /* @__PURE__ */ new Set();
|
|
10896
10993
|
for (const page of bundle.pages) {
|
|
10897
10994
|
for (const match of page.body.matchAll(/\/[a-z0-9_/-]+/gi)) {
|
|
@@ -10912,6 +11009,10 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
|
|
|
10912
11009
|
const operations = parseOpenApi(body, extension);
|
|
10913
11010
|
const filename = `openapi-${createHash2("sha256").update(reference.url).digest("hex").slice(0, 12)}.${extension}`;
|
|
10914
11011
|
assets.push({ path: `openapi/${filename}`, content: body, projectRelative: true });
|
|
11012
|
+
for (const page of pages) {
|
|
11013
|
+
const ref = page.openapi ? splitOpenApiRef(page.openapi) : null;
|
|
11014
|
+
if (ref?.specRef === reference.url) page.openapi = withSpecRef(ref, `openapi/${filename}`);
|
|
11015
|
+
}
|
|
10915
11016
|
const tab = reference.tabLabel ? tabs.find((candidate) => candidate.tab === reference.tabLabel) : tabs.find((candidate) => candidate.tab.toLowerCase().includes("api"));
|
|
10916
11017
|
if (tab) tab.api = { source: `openapi/${filename}` };
|
|
10917
11018
|
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 +11041,7 @@ async function hydrateRemoteApiSpecs(bundle, fetcher) {
|
|
|
10940
11041
|
}
|
|
10941
11042
|
return {
|
|
10942
11043
|
...bundle,
|
|
11044
|
+
pages,
|
|
10943
11045
|
assets,
|
|
10944
11046
|
docsConfig: { ...bundle.docsConfig, tabs, ...redirects.length > 0 ? { redirects } : {} },
|
|
10945
11047
|
warnings
|