@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 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