@thallylabs/migrate 0.2.17 → 0.2.18

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
@@ -11,7 +11,13 @@ authored nesting or order. The adapter supports tabs, dropdowns, products,
11
11
  versions, languages, and reusable `$ref` navigation files; it also preserves
12
12
  dropdown presentation, collection descriptions and icons, explicit default
13
13
  versions, page route casing, redirects, local snippets, static assets,
14
- compatible page frontmatter, site identity, and OpenAPI configuration. For public Mintlify
14
+ compatible page frontmatter, site identity, and OpenAPI configuration.
15
+ Analytics come across too: docs.json `integrations` and legacy mint.json
16
+ `analytics` for GA4, Google Tag Manager, PostHog, and Plausible are validated
17
+ and written to `docs.json` `integrations` (`integrations` wins per provider when
18
+ both exist). Invalid values are skipped with a warning, and every other
19
+ Mintlify provider (Amplitude, Mixpanel, Segment, and so on) is listed once in a
20
+ warning instead of being dropped silently. For public Mintlify
15
21
  sites, the URL adapter reads the structured configuration embedded in the
16
22
  rendered application before falling back to DOM link inference.
17
23
 
package/dist/index.d.ts CHANGED
@@ -128,6 +128,24 @@ interface MigrationDocsConfig {
128
128
  analytics?: {
129
129
  enabled?: boolean;
130
130
  };
131
+ /** Third-party analytics in Mintlify's `integrations` shape; validated again by the renderer. */
132
+ integrations?: {
133
+ ga4?: {
134
+ measurementId: string;
135
+ };
136
+ gtm?: {
137
+ tagId: string;
138
+ };
139
+ posthog?: {
140
+ apiKey: string;
141
+ apiHost?: string;
142
+ sessionRecording?: boolean;
143
+ };
144
+ plausible?: {
145
+ domain: string;
146
+ server?: string;
147
+ };
148
+ };
131
149
  redirects?: Array<{
132
150
  source: string;
133
151
  destination: string;
@@ -447,6 +465,26 @@ declare function parseMarkdownPage(input: {
447
465
  resolveIdentity?: (frontmatter: Record<string, unknown>, fallback: MarkdownPageIdentity) => MarkdownPageIdentity;
448
466
  }): MigrationPage | null;
449
467
 
468
+ /**
469
+ * Mintlify analytics -> Thally `docs.json` `integrations`.
470
+ *
471
+ * Reads docs.json `integrations` and the legacy mint.json `analytics` block.
472
+ * Values land in an inline script on the rendered site, so each is validated
473
+ * here with the same patterns the renderer applies (src/lib/analytics-config.ts;
474
+ * a test keeps the two in agreement). Invalid values are skipped with a warning,
475
+ * never coerced, and no warning echoes a full value.
476
+ */
477
+ type MigrationIntegrations = NonNullable<MigrationDocsConfig['integrations']>;
478
+ declare function projectMintlifyIntegrations(config: Record<string, unknown>): {
479
+ integrations?: MigrationIntegrations;
480
+ warnings: Array<MigrationWarning>;
481
+ };
482
+ /**
483
+ * Validate an authored Thally `docs.json` `integrations` value with the same
484
+ * rules as the import and the renderer; one value-free message per problem.
485
+ */
486
+ declare function validateIntegrations(integrations: unknown): Array<string>;
487
+
450
488
  /** Render canonical migration bundles into a portable Thally repository tree. */
451
489
 
452
490
  /** Merge imported tabs into an existing site without duplicating changelog tabs. */
@@ -486,4 +524,4 @@ declare function migrateUrl(options: UrlMigrationOptions): Promise<MigrationBund
486
524
  */
487
525
  declare function hydrateRemoteApiSpecs(bundle: MigrationBundle, fetcher?: MigrationFetcher): Promise<MigrationBundle>;
488
526
 
489
- export { type GitHubRepositorySource, type MigrationAsset, type MigrationBundle, type MigrationDocsConfig, type MigrationFetchRequest, type MigrationFetchResponse, type MigrationFetcher, type MigrationNavigationGroup, type MigrationNavigationTab, type MigrationPage, type MigrationPlatform, type MigrationWarning, type RenderedMigrationFile, type RepositoryMigrationOptions, type UrlMigrationOptions, addMintlifyDirectoryRedirects, buildNavigationFromPages, cloneGitHubRepository, defaultMigrationFetcher, detectRepositoryDocsDir, detectRepositoryPlatform, gitmodulePaths, hydrateRemoteApiSpecs, mergeMigrationConfig, migrateRepository, migrateUrl, normalizeMdx, parseGitHubRepositoryUrl, parseMarkdownPage, projectFernNavigation, projectMintlifyNavigation, pruneMissingNavigationPages, readFernConfig, readMintlifyConfig, renderMigrationFiles, validateMigrationUrl };
527
+ export { type GitHubRepositorySource, type MigrationAsset, type MigrationBundle, type MigrationDocsConfig, type MigrationFetchRequest, type MigrationFetchResponse, type MigrationFetcher, type MigrationNavigationGroup, type MigrationNavigationTab, type MigrationPage, type MigrationPlatform, type MigrationWarning, type RenderedMigrationFile, type RepositoryMigrationOptions, type UrlMigrationOptions, addMintlifyDirectoryRedirects, buildNavigationFromPages, cloneGitHubRepository, defaultMigrationFetcher, detectRepositoryDocsDir, detectRepositoryPlatform, gitmodulePaths, hydrateRemoteApiSpecs, mergeMigrationConfig, migrateRepository, migrateUrl, normalizeMdx, parseGitHubRepositoryUrl, parseMarkdownPage, projectFernNavigation, projectMintlifyIntegrations, projectMintlifyNavigation, pruneMissingNavigationPages, readFernConfig, readMintlifyConfig, renderMigrationFiles, validateIntegrations, validateMigrationUrl };
package/dist/index.js CHANGED
@@ -326,8 +326,8 @@ function leadingPageHeading(body) {
326
326
  const root = descriptionParser.parse(body);
327
327
  const first = root.children?.find((node) => node.type !== "mdxjsEsm" && node.type !== "html");
328
328
  if (first?.type !== "heading" || first.depth !== 1) return void 0;
329
- const text = (node) => node.type === "mdxTextExpression" || node.type === "mdxFlowExpression" ? "" : node.value ?? (node.children ?? []).map(text).join("");
330
- const title = text(first).replace(/\s+/g, " ").trim();
329
+ const text2 = (node) => node.type === "mdxTextExpression" || node.type === "mdxFlowExpression" ? "" : node.value ?? (node.children ?? []).map(text2).join("");
330
+ const title = text2(first).replace(/\s+/g, " ").trim();
331
331
  const start = first.position?.start.offset;
332
332
  const end = first.position?.end.offset;
333
333
  return title && start !== void 0 && end !== void 0 ? { title, start, end } : void 0;
@@ -400,16 +400,16 @@ function collectEsmBindingsByPattern(source, declared) {
400
400
  var FENCE_OPEN = /^(`{3,}|~{3,})/;
401
401
  function maskInlineSpansForMath(line) {
402
402
  const blocks = [];
403
- const stash = (text) => {
404
- blocks.push(text);
403
+ const stash = (text2) => {
404
+ blocks.push(text2);
405
405
  return `\0${blocks.length - 1}\0`;
406
406
  };
407
407
  const codeMasked = line.split(/(`[^`]*`)/).map((segment, index) => index % 2 === 1 ? stash(segment) : segment).join("");
408
408
  const tagMasked = codeMasked.replace(/<\/?[a-zA-Z][^<>]*>/g, stash);
409
- const masked = [tagMasked, tagMasked].reduce((text) => text.replace(/(?<!\$)\{[^{}]*\}/g, stash), tagMasked);
409
+ const masked = [tagMasked, tagMasked].reduce((text2) => text2.replace(/(?<!\$)\{[^{}]*\}/g, stash), tagMasked);
410
410
  return {
411
411
  masked,
412
- unmask: (text) => text.replace(/\u0000(\d+)\u0000/g, (_match, index) => blocks[Number(index)])
412
+ unmask: (text2) => text2.replace(/\u0000(\d+)\u0000/g, (_match, index) => blocks[Number(index)])
413
413
  };
414
414
  }
415
415
  var TEX_SIGNAL = /\\[a-zA-Z]|[\^_{]/;
@@ -562,11 +562,11 @@ function splitFrontmatterBlock(raw) {
562
562
  return { front: bom + source.slice(0, end), body: source.slice(end) };
563
563
  }
564
564
  var MIGRATED_COMPONENT_TAG = /^Migrated[0-9a-f]{12}$/;
565
- function isSelfClosingJsx(text) {
566
- return /\/>\s*$/.test(text);
565
+ function isSelfClosingJsx(text2) {
566
+ return /\/>\s*$/.test(text2);
567
567
  }
568
- function renameTagOccurrences(text, name, newName) {
569
- let result = text;
568
+ function renameTagOccurrences(text2, name, newName) {
569
+ let result = text2;
570
570
  const openIndex = result.indexOf(`<${name}`);
571
571
  if (openIndex >= 0) result = `${result.slice(0, openIndex + 1)}${newName}${result.slice(openIndex + 1 + name.length)}`;
572
572
  const closeIndex = result.lastIndexOf(`</${name}`);
@@ -612,9 +612,9 @@ function replaceUnknownComponents(raw, warn, options = {}) {
612
612
  if (start === void 0 || end === void 0) return "";
613
613
  const isJsx = node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement";
614
614
  const tagRoot = node.name ? node.name.split(".")[0] : void 0;
615
- const text = body.slice(start, end);
615
+ const text2 = body.slice(start, end);
616
616
  if (isJsx && tagRoot === "Link" && !declared.has("Link")) {
617
- if (isSelfClosingJsx(text) || !node.children?.length) return renameTagOccurrences(text, node.name, "a");
617
+ if (isSelfClosingJsx(text2) || !node.children?.length) return renameTagOccurrences(text2, node.name, "a");
618
618
  const openEnd = node.children[0].position?.start.offset ?? start;
619
619
  const closeStart = node.children.at(-1).position?.end.offset ?? end;
620
620
  return renameTagOccurrences(body.slice(start, openEnd), node.name, "a") + renderChildrenSpan(node.children, openEnd, closeStart, body, renderNode) + renameTagOccurrences(body.slice(closeStart, end), node.name, "a");
@@ -624,7 +624,7 @@ function replaceUnknownComponents(raw, warn, options = {}) {
624
624
  warned.add(node.name);
625
625
  warn(node.name);
626
626
  }
627
- if (isSelfClosingJsx(text)) return "";
627
+ if (isSelfClosingJsx(text2)) return "";
628
628
  if (!node.children?.length) return "<div></div>";
629
629
  const openEnd = node.children[0].position?.start.offset ?? start;
630
630
  const closeStart = node.children.at(-1).position?.end.offset ?? end;
@@ -635,7 +635,7 @@ ${children}
635
635
 
636
636
  </div>` : `<div>${children}</div>`;
637
637
  }
638
- if (!node.children?.length) return text;
638
+ if (!node.children?.length) return text2;
639
639
  return renderChildrenSpan(node.children, start, end, body, renderNode);
640
640
  };
641
641
  return front + renderNode(root);
@@ -643,8 +643,8 @@ ${children}
643
643
  function maskCode(body) {
644
644
  const source = body.replace(/\u0000/g, "");
645
645
  const blocks = [];
646
- const stash = (text) => {
647
- blocks.push(text);
646
+ const stash = (text2) => {
647
+ blocks.push(text2);
648
648
  return `\0${blocks.length - 1}\0`;
649
649
  };
650
650
  const lines = [];
@@ -671,7 +671,7 @@ function maskCode(body) {
671
671
  if (fenceBuffer.length) lines.push(stash(fenceBuffer.join("\n")));
672
672
  return {
673
673
  masked: lines.join("\n"),
674
- unmask: (text) => text.replace(/\u0000(\d+)\u0000/g, (_match, index) => blocks[Number(index)])
674
+ unmask: (text2) => text2.replace(/\u0000(\d+)\u0000/g, (_match, index) => blocks[Number(index)])
675
675
  };
676
676
  }
677
677
  function replaceOutsideCode(body, transform) {
@@ -690,7 +690,7 @@ function rewriteFernRelativePageLinks(body, currentSourcePath, routesBySourcePat
690
690
  const route = candidates.map((candidate) => routesBySourcePath.get(candidate)).find(Boolean);
691
691
  return route ? `/${route.replace(/^\/+/, "")}${suffix}` : target;
692
692
  };
693
- return replaceOutsideCode(body, (text) => text.replace(
693
+ return replaceOutsideCode(body, (text2) => text2.replace(
694
694
  /(\]\()(<[^>\n]+>|[^\s)]+)(?=\s|\))/g,
695
695
  (_match, opening, target) => `${opening}${target.startsWith("<") && target.endsWith(">") ? `<${rewrite(target.slice(1, -1))}>` : rewrite(target)}`
696
696
  ).replace(
@@ -2165,7 +2165,7 @@ const MintlifyComponents = {
2165
2165
  }
2166
2166
  function applyReplacements(source, replacements) {
2167
2167
  return replacements.sort((a, b) => b.start - a.start).reduce(
2168
- (text, edit) => text.slice(0, edit.start) + edit.value + text.slice(edit.end),
2168
+ (text2, edit) => text2.slice(0, edit.start) + edit.value + text2.slice(edit.end),
2169
2169
  source
2170
2170
  );
2171
2171
  }
@@ -2454,14 +2454,14 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2454
2454
  }
2455
2455
  return;
2456
2456
  }
2457
- const text = content.toString("utf8");
2458
- const syntax = ts2.transpileModule(text, {
2457
+ const text2 = content.toString("utf8");
2458
+ const syntax = ts2.transpileModule(text2, {
2459
2459
  fileName: path,
2460
2460
  reportDiagnostics: true,
2461
2461
  compilerOptions: { jsx: ts2.JsxEmit.Preserve, target: ts2.ScriptTarget.ESNext, module: ts2.ModuleKind.ESNext }
2462
2462
  });
2463
2463
  if (syntax.diagnostics?.some((diagnostic) => diagnostic.category === ts2.DiagnosticCategory.Error)) throw new Error("component source contains unsupported JavaScript or TypeScript syntax");
2464
- const ast = sourceFile(text, path);
2464
+ const ast = sourceFile(text2, path);
2465
2465
  if (ast.statements.some((statement) => ts2.isExpressionStatement(statement) && ts2.isStringLiteral(statement.expression) && statement.expression.text === "use server")) throw new Error("server-only component modules require manual migration");
2466
2466
  const edits = [];
2467
2467
  function dependency(literal, importDeclaration) {
@@ -2509,7 +2509,7 @@ function createComponentMigrator(siteRoot, confinementRoot, warnings, sourceIden
2509
2509
  ts2.forEachChild(node, inspect);
2510
2510
  }
2511
2511
  inspect(ast);
2512
- const replaced = applyReplacements(text, edits);
2512
+ const replaced = applyReplacements(text2, edits);
2513
2513
  const shebangMatch = /^#!.*\r?\n/.exec(replaced);
2514
2514
  const prologue = "// @ts-nocheck\n'use client';\n\n";
2515
2515
  staged.set(destination, {
@@ -3016,10 +3016,10 @@ ${inlineSource}` });
3016
3016
  const start = node.position.start.offset;
3017
3017
  const end = node.position.end.offset;
3018
3018
  if (extracted.some((item) => start >= item.node.position.start.offset && end <= item.node.position.end.offset)) return;
3019
- const text = content.slice(start, end);
3020
- const opening = text.indexOf(`<${node.name}`);
3019
+ const text2 = content.slice(start, end);
3020
+ const opening = text2.indexOf(`<${node.name}`);
3021
3021
  if (opening >= 0) edits.push({ start: start + opening + 1, end: start + opening + 1 + node.name.length, value: replacement });
3022
- const closing = text.lastIndexOf(`</${node.name}`);
3022
+ const closing = text2.lastIndexOf(`</${node.name}`);
3023
3023
  if (closing >= 0) edits.push({ start: start + closing + 2, end: start + closing + 2 + node.name.length, value: replacement });
3024
3024
  });
3025
3025
  walk(tree, (node) => {
@@ -5141,9 +5141,9 @@ function announcementMarkdown(message) {
5141
5141
  links.set(marker, destination && label ? `[${escapeText(label)}](${destination})` : escapeText(label));
5142
5142
  document(element).replaceWith(marker);
5143
5143
  });
5144
- let text = escapeText(document("body").text());
5145
- for (const [marker, link] of links) text = text.replaceAll(marker, link);
5146
- return text.trim();
5144
+ let text2 = escapeText(document("body").text());
5145
+ for (const [marker, link] of links) text2 = text2.replaceAll(marker, link);
5146
+ return text2.trim();
5147
5147
  }
5148
5148
  function objectValue4(value) {
5149
5149
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -5661,6 +5661,179 @@ function projectFernNavigation(input) {
5661
5661
  };
5662
5662
  }
5663
5663
 
5664
+ // src/analytics.ts
5665
+ var GA4_ID = /^G-[A-Z0-9]{4,20}$/;
5666
+ var GTM_ID = /^GTM-[A-Z0-9]{4,12}$/;
5667
+ var POSTHOG_KEY = /^phc_[A-Za-z0-9]{20,64}$/;
5668
+ var HOSTNAME_LABEL = "[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?";
5669
+ var HOSTNAME = new RegExp(`^${HOSTNAME_LABEL}(?:\\.${HOSTNAME_LABEL})*$`);
5670
+ var SAFE_PATH = /^\/[A-Za-z0-9._~\-/]*$/;
5671
+ var MAX_PLAUSIBLE_DOMAINS = 10;
5672
+ var SUPPORTED = ["ga4", "gtm", "posthog", "plausible"];
5673
+ var LEGACY_GA4_ALIAS = "googleAnalytics";
5674
+ function own(record, key) {
5675
+ return Object.prototype.hasOwnProperty.call(record, key) ? record[key] : void 0;
5676
+ }
5677
+ function isRecord(value) {
5678
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
5679
+ }
5680
+ function redact(value) {
5681
+ if (typeof value !== "string") return Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
5682
+ return value.length === 0 ? "empty string" : `"${value.trim().slice(0, 4).replace(/[^\x20-\x7e]/g, "?")}..." (${value.length} chars)`;
5683
+ }
5684
+ function isPublicHostname(hostname) {
5685
+ return !/(^|\.)localhost$/.test(hostname) && !/(^|\.)\d+$/.test(hostname) && !hostname.includes(":") && !hostname.startsWith("[");
5686
+ }
5687
+ function normalizeHttpsUrl(value, allowPath) {
5688
+ if (!/^https:\/\//i.test(value) || /[?#\\]/.test(value)) return null;
5689
+ let url;
5690
+ try {
5691
+ url = new URL(value);
5692
+ } catch {
5693
+ return null;
5694
+ }
5695
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) return null;
5696
+ if (!HOSTNAME.test(url.hostname) || !isPublicHostname(url.hostname) || url.port === "0") return null;
5697
+ const fullPath = /^https:\/\/[^/]*(\/.*)?$/i.exec(value)?.[1] ?? "";
5698
+ if (fullPath.includes("//")) return null;
5699
+ const path = fullPath.replace(/\/+$/, "");
5700
+ if (path && (!allowPath || !SAFE_PATH.test(path) || path.split("/").some((s) => s === ".." || s === "."))) return null;
5701
+ return `${url.origin}${path}`;
5702
+ }
5703
+ function normalizeDomains(value) {
5704
+ const parts = value.split(",").map((part) => part.trim().toLowerCase());
5705
+ if (parts.length > MAX_PLAUSIBLE_DOMAINS) return null;
5706
+ return parts.every((part) => part.length <= 253 && HOSTNAME.test(part)) ? parts.join(",") : null;
5707
+ }
5708
+ function normalizeServer(value) {
5709
+ if (/^https:\/\//i.test(value)) {
5710
+ const origin2 = normalizeHttpsUrl(value, false);
5711
+ return origin2 ? origin2.replace(/^https:\/\//, "") : null;
5712
+ }
5713
+ const bare = /^([a-z0-9.-]+)(?::(\d{1,5}))?$/i.exec(value);
5714
+ if (!bare || bare[2] !== void 0 && (Number(bare[2]) < 1 || Number(bare[2]) > 65535)) return null;
5715
+ const origin = normalizeHttpsUrl(`https://${bare[1].toLowerCase()}${bare[2] === void 0 ? "" : `:${Number(bare[2])}`}`, false);
5716
+ return origin ? origin.replace(/^https:\/\//, "") : null;
5717
+ }
5718
+ function text(value) {
5719
+ return typeof value === "string" && value.trim() && value.length <= 2048 ? value.trim() : null;
5720
+ }
5721
+ function projectMintlifyIntegrations(config) {
5722
+ if (!isRecord(config)) return { warnings: [] };
5723
+ try {
5724
+ return project(config);
5725
+ } catch {
5726
+ return { warnings: [{ code: "unsupported-config", message: "Mintlify analytics configuration could not be read; analytics were not imported." }] };
5727
+ }
5728
+ }
5729
+ function project(config) {
5730
+ const warnings = [];
5731
+ const warn = (message) => warnings.push({ code: "unsupported-config", message });
5732
+ const sources = [];
5733
+ for (const [label, key] of [["integrations", "integrations"], ["analytics", "analytics"]]) {
5734
+ const value = own(config, key);
5735
+ if (value === void 0) continue;
5736
+ if (isRecord(value)) sources.push({ label, block: value });
5737
+ else warn(`Mintlify ${label} must be an object (found ${redact(value)}); analytics from it were not imported.`);
5738
+ }
5739
+ const integrations = {};
5740
+ const pick = (name) => {
5741
+ const found = [];
5742
+ for (const source of sources) {
5743
+ let value = own(source.block, name);
5744
+ if (value === void 0 && name === "ga4" && source.label === "analytics") value = own(source.block, LEGACY_GA4_ALIAS);
5745
+ if (value !== void 0) found.push({ label: `${source.label}.${name}`, value });
5746
+ }
5747
+ if (found.length > 1) {
5748
+ warn(`Mintlify ${found[0].label} and ${found[1].label} are both set; using ${found[0].label} for ${name}.`);
5749
+ }
5750
+ return found;
5751
+ };
5752
+ const field = (label, block, key, required) => {
5753
+ const raw = own(block, key);
5754
+ if (raw === void 0) {
5755
+ if (required) warn(`Mintlify ${label}.${key} is missing; ${label} was not imported.`);
5756
+ return required ? null : void 0;
5757
+ }
5758
+ const value = text(raw);
5759
+ if (value === null) warn(`Mintlify ${label}.${key} is not a usable string (found ${redact(raw)}); ${label} was not imported.`);
5760
+ return value;
5761
+ };
5762
+ const objectFor = (entry) => {
5763
+ if (isRecord(entry.value)) return entry.value;
5764
+ warn(`Mintlify ${entry.label} must be an object (found ${redact(entry.value)}); provider was not imported.`);
5765
+ return null;
5766
+ };
5767
+ const ga4 = pick("ga4")[0];
5768
+ const ga4Block = ga4 ? objectFor(ga4) : null;
5769
+ if (ga4 && ga4Block) {
5770
+ const id = field(ga4.label, ga4Block, "measurementId", true);
5771
+ if (id && GA4_ID.test(id)) integrations.ga4 = { measurementId: id };
5772
+ else if (id) warn(`Mintlify ${ga4.label}.measurementId ${redact(id)} is not a GA4 ID (G-XXXXXXXXXX); Universal Analytics and lowercase IDs are not imported.`);
5773
+ }
5774
+ const gtm = pick("gtm")[0];
5775
+ const gtmBlock = gtm ? objectFor(gtm) : null;
5776
+ if (gtm && gtmBlock) {
5777
+ const id = field(gtm.label, gtmBlock, "tagId", true);
5778
+ if (id && GTM_ID.test(id)) integrations.gtm = { tagId: id };
5779
+ else if (id) warn(`Mintlify ${gtm.label}.tagId ${redact(id)} is not a GTM container ID (GTM-XXXXXXX); GTM was not imported.`);
5780
+ }
5781
+ const plausible = pick("plausible")[0];
5782
+ const plausibleBlock = plausible ? objectFor(plausible) : null;
5783
+ if (plausible && plausibleBlock) {
5784
+ const domainRaw = field(plausible.label, plausibleBlock, "domain", true);
5785
+ const serverRaw = field(plausible.label, plausibleBlock, "server", false);
5786
+ const domain = domainRaw ? normalizeDomains(domainRaw) : null;
5787
+ const server = serverRaw ? normalizeServer(serverRaw) : serverRaw;
5788
+ if (domainRaw && !domain) warn(`Mintlify ${plausible.label}.domain ${redact(domainRaw)} must be hostname(s) without a protocol or path; Plausible was not imported.`);
5789
+ if (serverRaw && !server) warn(`Mintlify ${plausible.label}.server ${redact(serverRaw)} must be a hostname; Plausible was not imported.`);
5790
+ if (domain && server !== null) integrations.plausible = { domain, ...server ? { server } : {} };
5791
+ }
5792
+ const posthog = pick("posthog")[0];
5793
+ const posthogBlock = posthog ? objectFor(posthog) : null;
5794
+ if (posthog && posthogBlock) {
5795
+ const key = field(posthog.label, posthogBlock, "apiKey", true);
5796
+ const hostRaw = field(posthog.label, posthogBlock, "apiHost", false);
5797
+ const host = hostRaw ? normalizeHttpsUrl(hostRaw, true) : hostRaw;
5798
+ const recording = own(posthogBlock, "sessionRecording");
5799
+ if (recording !== void 0 && typeof recording !== "boolean") warn(`Mintlify ${posthog.label}.sessionRecording must be a boolean (found ${redact(recording)}); session recording disabled.`);
5800
+ if (key && !POSTHOG_KEY.test(key)) warn(`Mintlify ${posthog.label}.apiKey ${redact(key)} is not a PostHog project key (phc_...); PostHog was not imported.`);
5801
+ if (hostRaw && !host) warn(`Mintlify ${posthog.label}.apiHost ${redact(hostRaw)} must be an https URL without credentials, query, or fragment; PostHog was not imported.`);
5802
+ if (key && POSTHOG_KEY.test(key) && host !== null) {
5803
+ integrations.posthog = {
5804
+ apiKey: key,
5805
+ ...host ? { apiHost: host } : {},
5806
+ // Fail closed: a present non-boolean value turns recording off.
5807
+ ...recording !== void 0 ? { sessionRecording: recording === true } : {}
5808
+ };
5809
+ }
5810
+ }
5811
+ const unsupported = /* @__PURE__ */ new Set();
5812
+ for (const source of sources) {
5813
+ for (const key of Object.keys(source.block)) {
5814
+ if (SUPPORTED.includes(key)) continue;
5815
+ if (key === LEGACY_GA4_ALIAS && source.label === "analytics") continue;
5816
+ unsupported.add(/^[A-Za-z0-9_-]{1,40}$/.test(key) ? key : "(invalid name)");
5817
+ }
5818
+ }
5819
+ if (unsupported.size > 0) {
5820
+ const names = [...unsupported].slice(0, 30);
5821
+ warn(`Mintlify analytics/integration providers Thally cannot render were not imported: ${names.join(", ")}${unsupported.size > names.length ? ", ..." : ""}. Supported: ga4, gtm, posthog, plausible.`);
5822
+ }
5823
+ return { ...Object.keys(integrations).length > 0 ? { integrations } : {}, warnings };
5824
+ }
5825
+ function validateIntegrations(integrations) {
5826
+ if (integrations === void 0) return [];
5827
+ const result = projectMintlifyIntegrations({ integrations });
5828
+ const messages = result.warnings.map(
5829
+ (warning) => warning.message.replace(/^Mintlify analytics\/integration providers Thally cannot render were not imported: /, "docs.json integrations: Thally does not support these providers, so they will not be rendered: ").replace(/^Mintlify /, "docs.json ").replace(/\b(?:was|were|are) not imported\b/g, "will not be rendered")
5830
+ );
5831
+ if (result.integrations?.ga4 && result.integrations.gtm) {
5832
+ messages.push("docs.json integrations enable both ga4 and gtm; a GA4 tag inside the GTM container would double-count page views.");
5833
+ }
5834
+ return messages;
5835
+ }
5836
+
5664
5837
  // src/repository.ts
5665
5838
  var MAX_SOURCE_FILES = 5e3;
5666
5839
  var MAX_PAGE_BYTES = 2e6;
@@ -6477,8 +6650,8 @@ var PATH_SEPARATORS = /[\s"'()<>[\]{}=,;:|\\`*!?#&]+/;
6477
6650
  function publicAssetKey(repoRelative) {
6478
6651
  return repoRelative.split("/", 1)[0].toLowerCase() === "public" ? repoRelative.slice("public/".length) : repoRelative;
6479
6652
  }
6480
- function addPathReferences(text, baseDir, exact, loose) {
6481
- for (const word of text.split(PATH_SEPARATORS)) {
6653
+ function addPathReferences(text2, baseDir, exact, loose) {
6654
+ for (const word of text2.split(PATH_SEPARATORS)) {
6482
6655
  let token = word.replace(/\.+$/, "");
6483
6656
  if (!/\.[a-z0-9]{2,5}$/i.test(token) || token.startsWith("//")) continue;
6484
6657
  if (token.includes("%")) {
@@ -6520,8 +6693,8 @@ function classifyApiSpec(path) {
6520
6693
  } catch {
6521
6694
  return "unknown";
6522
6695
  }
6523
- const text = buffer.toString("utf8", 0, bytesRead);
6524
- const topLevel = text.match(/^(?:openapi|swagger|asyncapi)\s*:/m)?.[0] ?? text.match(/^\s*\{\s*"(?:openapi|swagger|asyncapi)"\s*:/m)?.[0];
6696
+ const text2 = buffer.toString("utf8", 0, bytesRead);
6697
+ const topLevel = text2.match(/^(?:openapi|swagger|asyncapi)\s*:/m)?.[0] ?? text2.match(/^\s*\{\s*"(?:openapi|swagger|asyncapi)"\s*:/m)?.[0];
6525
6698
  if (!topLevel) return "unknown";
6526
6699
  return /asyncapi/i.test(topLevel) ? "asyncapi" : "openapi";
6527
6700
  }
@@ -7009,12 +7182,12 @@ function externalizeMissingFernLinks(body, pageIds, sourceSiteUrl, externalized)
7009
7182
  return target;
7010
7183
  }
7011
7184
  };
7012
- return replaceOutsideCode(body, (text) => text.replace(/(\]\()\/(?!\/)([^\s)]+)(\))/g, (_match, before, target, after) => `${before}${rewrite(`/${target}`)}${after}`).replace(/(\b(?:href|to)=(['"]))\/(?!\/)([^'"\n]+)\2/g, (_match, before, quote, target) => `${before}${rewrite(`/${target}`)}${quote}`));
7185
+ return replaceOutsideCode(body, (text2) => text2.replace(/(\]\()\/(?!\/)([^\s)]+)(\))/g, (_match, before, target, after) => `${before}${rewrite(`/${target}`)}${after}`).replace(/(\b(?:href|to)=(['"]))\/(?!\/)([^'"\n]+)\2/g, (_match, before, quote, target) => `${before}${rewrite(`/${target}`)}${quote}`));
7013
7186
  }
7014
7187
  function stripFernBasePathFromLinks(body, basePath) {
7015
7188
  if (!basePath) return body;
7016
7189
  const escaped = basePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7017
- return replaceOutsideCode(body, (text) => text.replace(new RegExp(`(\\]\\()${escaped}(?=\\/|[)#?])`, "g"), (match, prefix, offset, source) => prefix + (source[offset + match.length] === "/" ? "" : "/")).replace(new RegExp(`(\\b(?:href|to)=["'])${escaped}(?=\\/|["'#?])`, "g"), (match, prefix, offset, source) => prefix + (source[offset + match.length] === "/" ? "" : "/")));
7190
+ return replaceOutsideCode(body, (text2) => text2.replace(new RegExp(`(\\]\\()${escaped}(?=\\/|[)#?])`, "g"), (match, prefix, offset, source) => prefix + (source[offset + match.length] === "/" ? "" : "/")).replace(new RegExp(`(\\b(?:href|to)=["'])${escaped}(?=\\/|["'#?])`, "g"), (match, prefix, offset, source) => prefix + (source[offset + match.length] === "/" ? "" : "/")));
7018
7191
  }
7019
7192
  function rewriteMintlifyMountedLinks(body, pageIds) {
7020
7193
  const rewrite = (target) => {
@@ -7031,7 +7204,7 @@ function rewriteMintlifyMountedLinks(body, pageIds) {
7031
7204
  }
7032
7205
  return target;
7033
7206
  };
7034
- return replaceOutsideCode(body, (text) => text.replace(/(\]\()(\/[^\s)]+)(\))/g, (_match, before, target, after) => `${before}${rewrite(target)}${after}`).replace(/(\b(?:href|to)=(['"]))(\/[^'"\n]+)\2/g, (_match, before, quote, target) => `${before}${rewrite(target)}${quote}`));
7207
+ return replaceOutsideCode(body, (text2) => text2.replace(/(\]\()(\/[^\s)]+)(\))/g, (_match, before, target, after) => `${before}${rewrite(target)}${after}`).replace(/(\b(?:href|to)=(['"]))(\/[^'"\n]+)\2/g, (_match, before, quote, target) => `${before}${rewrite(target)}${quote}`));
7035
7208
  }
7036
7209
  function preserveLinkedAnchors(pages) {
7037
7210
  const pageById = new Map(pages.map((page) => [page.id, page]));
@@ -7055,10 +7228,10 @@ function preserveLinkedAnchors(pages) {
7055
7228
  if (id !== currentId) crossPageRequests.add(`${id}#${fragment}`);
7056
7229
  };
7057
7230
  for (const page of pages) {
7058
- replaceOutsideCode(page.body, (text) => {
7059
- for (const match of text.matchAll(/\]\(([^)\s]+)\)/g)) addTarget(match[1], page.id);
7060
- for (const match of text.matchAll(/\b(?:href|to)=(['"])([^'"\n]+)\1/g)) addTarget(match[2], page.id);
7061
- return text;
7231
+ replaceOutsideCode(page.body, (text2) => {
7232
+ for (const match of text2.matchAll(/\]\(([^)\s]+)\)/g)) addTarget(match[1], page.id);
7233
+ for (const match of text2.matchAll(/\b(?:href|to)=(['"])([^'"\n]+)\1/g)) addTarget(match[2], page.id);
7234
+ return text2;
7062
7235
  });
7063
7236
  }
7064
7237
  const comparable = (value) => value.replace(/&(?:lt|gt|amp|quot);/g, " ").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "");
@@ -7082,9 +7255,9 @@ function preserveLinkedAnchors(pages) {
7082
7255
  return match ? { index, text: match[1] } : null;
7083
7256
  }).filter((entry) => entry !== null);
7084
7257
  const existing = /* @__PURE__ */ new Set();
7085
- replaceOutsideCode(page.body, (text) => {
7086
- for (const match of text.matchAll(/\bid=(?:"([^"]+)"|'([^']+)')/g)) existing.add(match[1] ?? match[2]);
7087
- return text;
7258
+ replaceOutsideCode(page.body, (text2) => {
7259
+ for (const match of text2.matchAll(/\bid=(?:"([^"]+)"|'([^']+)')/g)) existing.add(match[1] ?? match[2]);
7260
+ return text2;
7088
7261
  });
7089
7262
  const candidateIndexes = /* @__PURE__ */ new Map();
7090
7263
  for (const heading of headings) {
@@ -7246,7 +7419,7 @@ function repositoryAssetHref(value, currentFile, siteRoot, onReferenced) {
7246
7419
  }
7247
7420
  }
7248
7421
  function rewriteFernIdRenameLinks(body, renames) {
7249
- return replaceOutsideCode(body, (text) => {
7422
+ return replaceOutsideCode(body, (text2) => {
7250
7423
  const rewriteTarget = (target) => {
7251
7424
  const suffixIndex = target.search(/[?#]/);
7252
7425
  const path = (suffixIndex >= 0 ? target.slice(0, suffixIndex) : target).replace(/^\/+/, "");
@@ -7254,7 +7427,7 @@ function rewriteFernIdRenameLinks(body, renames) {
7254
7427
  const renamed = renames.get(path);
7255
7428
  return renamed ? `/${renamed}${suffix}` : target;
7256
7429
  };
7257
- return text.replace(/(\]\()\/([^\s)]+)(?=[\s)]|$)/g, (_match, prefix, target) => `${prefix}${rewriteTarget(`/${target}`)}`).replace(/(\bhref=")\/([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${rewriteTarget(`/${target}`)}${suffix}`);
7430
+ return text2.replace(/(\]\()\/([^\s)]+)(?=[\s)]|$)/g, (_match, prefix, target) => `${prefix}${rewriteTarget(`/${target}`)}`).replace(/(\bhref=")\/([^"]+)(")/g, (_match, prefix, target, suffix) => `${prefix}${rewriteTarget(`/${target}`)}${suffix}`);
7258
7431
  });
7259
7432
  }
7260
7433
  function rewriteRepositoryAssetLinks(body, currentFile, siteRoot, onReferenced) {
@@ -8778,6 +8951,9 @@ ${reasons.map((reason) => `- ${reason}`).join("\n")}` : "No importable Markdown
8778
8951
  docsConfig = { ...docsConfig, navbar: { ...docsConfig.navbar, logo: null } };
8779
8952
  }
8780
8953
  if (mintlifyConfig) {
8954
+ const analytics = projectMintlifyIntegrations(mintlifyConfig);
8955
+ warnings.push(...analytics.warnings);
8956
+ if (analytics.integrations) docsConfig = { ...docsConfig, integrations: analytics.integrations };
8781
8957
  const appearance = mintlifyAppearance(mintlifyConfig, warnings);
8782
8958
  if (Object.keys(appearance).length > 0) {
8783
8959
  docsConfig = { ...docsConfig, appearance: { ...docsConfig.appearance, ...appearance } };
@@ -8896,6 +9072,9 @@ function renderPage(bundle, page) {
8896
9072
  ""
8897
9073
  ].filter((line) => line !== null).join("\n");
8898
9074
  }
9075
+ function isIntegrationsObject(value) {
9076
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
9077
+ }
8899
9078
  function mergeMigrationConfig(existing, incoming) {
8900
9079
  const tabs = existing.tabs.filter((tab) => tab.tab.toLowerCase() !== "changelog");
8901
9080
  const names = new Set(tabs.map((tab) => tab.tab.toLowerCase()));
@@ -8966,7 +9145,15 @@ function mergeMigrationConfig(existing, incoming) {
8966
9145
  // Collection presentation belongs to the imported information
8967
9146
  // architecture; starter defaults must not coerce source dropdowns to tabs.
8968
9147
  navigation: incoming.navigation ?? existing.navigation,
8969
- ...i18n ? { i18n } : {}
9148
+ ...i18n ? { i18n } : {},
9149
+ // Existing providers win per provider; imported ones fill the gaps.
9150
+ // A malformed (string/array) existing value must not spread into index keys.
9151
+ ...isIntegrationsObject(existing.integrations) || isIntegrationsObject(incoming.integrations) ? {
9152
+ integrations: {
9153
+ ...isIntegrationsObject(incoming.integrations) ? incoming.integrations : {},
9154
+ ...isIntegrationsObject(existing.integrations) ? existing.integrations : {}
9155
+ }
9156
+ } : {}
8970
9157
  };
8971
9158
  }
8972
9159
  function renderMigrationFiles(bundle, options = {}) {
@@ -9957,7 +10144,7 @@ ${fence}
9957
10144
  }
9958
10145
  });
9959
10146
  const escape = turndown.escape.bind(turndown);
9960
- turndown.escape = (text) => escape(text).replace(/[<{]/g, (match) => `\\${match}`);
10147
+ turndown.escape = (text2) => escape(text2).replace(/[<{]/g, (match) => `\\${match}`);
9961
10148
  const body = turndown.turndown(root.html() ?? "").trim();
9962
10149
  if (!title || body.length < 40 && $("article").length === 0) return { page: null, links };
9963
10150
  return {
@@ -10510,6 +10697,11 @@ async function migrateUrl(options) {
10510
10697
  topLevelNavigation
10511
10698
  });
10512
10699
  if (sourceMintlifyProjection) warnings.push(...sourceMintlifyProjection.warnings);
10700
+ if (sourceMintlifyConfig) {
10701
+ const analytics = projectMintlifyIntegrations(sourceMintlifyConfig);
10702
+ warnings.push(...analytics.warnings);
10703
+ if (analytics.integrations) docsConfig.integrations = analytics.integrations;
10704
+ }
10513
10705
  if (docusaurusRedirects.length > 0) {
10514
10706
  docsConfig.redirects = [...new Map(docusaurusRedirects.map((redirect) => [
10515
10707
  `${redirect.source}:${redirect.destination}`,
@@ -10658,8 +10850,8 @@ async function downloadSpec(value, fetcher) {
10658
10850
  throw new Error("Remote OpenAPI spec redirected too many times.");
10659
10851
  }
10660
10852
  function parseOpenApi(content, extension) {
10661
- const text = content.toString("utf8");
10662
- const parsed = extension === "yaml" || !text.trimStart().startsWith("{") ? parseYaml6(text) : JSON.parse(text);
10853
+ const text2 = content.toString("utf8");
10854
+ const parsed = extension === "yaml" || !text2.trimStart().startsWith("{") ? parseYaml6(text2) : JSON.parse(text2);
10663
10855
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("OpenAPI spec is not an object.");
10664
10856
  const document = parsed;
10665
10857
  if (typeof document.openapi !== "string" || !/^3\./.test(document.openapi)) {
@@ -10769,11 +10961,13 @@ export {
10769
10961
  parseGitHubRepositoryUrl,
10770
10962
  parseMarkdownPage,
10771
10963
  projectFernNavigation,
10964
+ projectMintlifyIntegrations,
10772
10965
  projectMintlifyNavigation,
10773
10966
  pruneMissingNavigationPages,
10774
10967
  readFernConfig,
10775
10968
  readMintlifyConfig,
10776
10969
  renderMigrationFiles,
10970
+ validateIntegrations,
10777
10971
  validateMigrationUrl
10778
10972
  };
10779
10973
  //# sourceMappingURL=index.js.map