@thallylabs/migrate 0.2.19 → 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/dist/index.js CHANGED
@@ -263,7 +263,8 @@ function unwrapDocusaurusMdxCodeBlocks(body) {
263
263
  }
264
264
  return isMdxCodeBlock ? body : output.join("\n");
265
265
  }
266
- function normalizeExplicitHeadingIds(raw) {
266
+ var HTML_ID = /^[^\s"'<>`{}&]+$/u;
267
+ function normalizeExplicitHeadingIds(raw, warn) {
267
268
  const { front, body } = splitFrontmatterBlock(raw);
268
269
  return front + replaceOutsideCode(body, (segment) => segment.split("\n").map((line) => {
269
270
  if (!/^[ \t]{0,3}#{1,6}[ \t]+/.test(line)) return line;
@@ -272,10 +273,12 @@ function normalizeExplicitHeadingIds(raw) {
272
273
  const marker = trimmed.lastIndexOf(" {#");
273
274
  if (marker >= 0) {
274
275
  const id = trimmed.slice(marker + 3, -1);
275
- if (/^[A-Za-z][A-Za-z0-9_-]*$/.test(id)) {
276
- return `<a id="${id}"></a>
277
- ${trimmed.slice(0, marker).trimEnd()}`;
278
- }
276
+ const heading = trimmed.slice(0, marker).trimEnd();
277
+ if (/[{}]/.test(id)) return line;
278
+ if (HTML_ID.test(id)) return `<a id="${id}"></a>
279
+ ${heading}`;
280
+ warn?.(`Heading anchor {#${id}} is not a valid HTML id and was removed from "${heading.replace(/^\s*#+\s*/, "")}".`);
281
+ return heading;
279
282
  }
280
283
  }
281
284
  if (trimmed.endsWith("*/}")) {
@@ -283,7 +286,7 @@ ${trimmed.slice(0, marker).trimEnd()}`;
283
286
  if (marker >= 0) {
284
287
  const comment = trimmed.slice(marker + 4, -3).trim();
285
288
  const id = comment.startsWith("#") ? comment.slice(1) : "";
286
- if (/^[A-Za-z][A-Za-z0-9_.:-]*$/.test(id)) {
289
+ if (HTML_ID.test(id)) {
287
290
  return `<a id="${id}"></a>
288
291
  ${trimmed.slice(0, marker).trimEnd()}`;
289
292
  }
@@ -671,13 +674,25 @@ function maskCode(body) {
671
674
  if (fenceBuffer.length) lines.push(stash(fenceBuffer.join("\n")));
672
675
  return {
673
676
  masked: lines.join("\n"),
674
- unmask: (text2) => text2.replace(/\u0000(\d+)\u0000/g, (_match, index) => blocks[Number(index)])
677
+ unmask: (text2) => text2.replace(/\u0000(\d+)\u0000/g, (_match, index) => blocks[Number(index)]),
678
+ openFence: codeFence
675
679
  };
676
680
  }
681
+ function closeOpenFence(body) {
682
+ const { openFence } = maskCode(body);
683
+ return openFence ? `${body}${body.endsWith("\n") ? "" : "\n"}${openFence}
684
+ ` : body;
685
+ }
677
686
  function replaceOutsideCode(body, transform) {
678
687
  const { masked, unmask } = maskCode(body);
679
688
  return unmask(transform(masked));
680
689
  }
690
+ function mdxComment(text2) {
691
+ return `{/*${text2.replace(/\*\//g, "* /")}*/}`;
692
+ }
693
+ function replaceOutsideCodeAndComments(body, transform) {
694
+ return replaceOutsideCode(body, (masked) => masked.split(/(\{\s*\/\*[\s\S]*?\*\/\s*\})/).map((part, index) => index % 2 ? part : transform(part)).join(""));
695
+ }
681
696
  function rewriteFernRelativePageLinks(body, currentSourcePath, routesBySourcePath) {
682
697
  const rewrite = (target) => {
683
698
  if (!target || target.startsWith("/") || target.startsWith("#") || target.startsWith("?") || target.includes("\\") || /[\u0000-\u001f\u007f]/.test(target) || /^[A-Za-z][A-Za-z\d+.-]*:/.test(target)) return target;
@@ -739,7 +754,7 @@ function normalizeFernFileTrees(body) {
739
754
  ));
740
755
  }
741
756
  function normalizeHtmlComments(body) {
742
- return replaceOutsideCode(body, (whole) => whole.replace(/<!--([\s\S]*?)-->/g, (_match, comment) => `{/*${comment}*/}`));
757
+ return replaceOutsideCode(body, (whole) => whole.replace(/<!--([\s\S]*?)-->/g, (_match, comment) => mdxComment(comment)));
743
758
  }
744
759
  function normalizeFernCallouts(body) {
745
760
  return replaceOutsideCode(body, (whole) => {
@@ -1230,7 +1245,7 @@ function normalizeMdx(body, platform, unwrapMdxCodeBlocks = true) {
1230
1245
  }
1231
1246
  if (runMintlify) rewritten = normalizeMintlifyParameterAnchors(rewritten);
1232
1247
  rewritten = replaceOutsideCode(rewritten, (segment) => {
1233
- let result = convertHtmlStyleAttributes(segment).replace(/<!--([\s\S]*?)-->/g, (_match, content) => `{/*${content}*/}`).replace(/<Danger(\s[^>]*)?>/g, "<Error$1>").replace(/<\/Danger>/g, "</Error>").replace(/<Warn(\s[^>]*)?>/g, "<Warning$1>").replace(/<\/Warn>/g, "</Warning>").replace(/<Check(\s[^>]*)?>/g, "<Note$1>").replace(/<\/Check>/g, "</Note>").replace(/<Tree\.Folder/g, "<Folder").replace(/<\/Tree\.Folder>/g, "</Folder>").replace(/<Tree\.File/g, "<File").replace(/<\/Tree\.File>/g, "</File>");
1248
+ let result = convertHtmlStyleAttributes(segment).replace(/<!--([\s\S]*?)-->/g, (_match, content) => mdxComment(content)).replace(/<Danger(\s[^>]*)?>/g, "<Error$1>").replace(/<\/Danger>/g, "</Error>").replace(/<Warn(\s[^>]*)?>/g, "<Warning$1>").replace(/<\/Warn>/g, "</Warning>").replace(/<Check(\s[^>]*)?>/g, "<Note$1>").replace(/<\/Check>/g, "</Note>").replace(/<Tree\.Folder/g, "<Folder").replace(/<\/Tree\.Folder>/g, "</Folder>").replace(/<Tree\.File/g, "<File").replace(/<\/Tree\.File>/g, "</File>");
1234
1249
  if (runDocusaurus) {
1235
1250
  result = result.replace(/<TabItem\b([^>]*)>/g, (_match, attributes) => {
1236
1251
  const title = attributes.match(/\blabel=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean) ?? attributes.match(/\bvalue=(?:"([^"]*)"|'([^']*)')/)?.slice(1).find(Boolean) ?? "Tab";
@@ -2380,7 +2395,8 @@ function neutralizeUnresolvableImportsWithoutAst(content, currentFile, warn) {
2380
2395
  });
2381
2396
  for (const { name, specifier } of dropped) {
2382
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);
2383
- result = result.replace(new RegExp(`<${name}(?:\\s[^>]*)?/>`, "g"), `{/* Removed <${name}>: unresolved during fallback migration */}`).replace(new RegExp(`<${name}(?:\\s[^>]*)?>[\\s\\S]*?<\\/${name}>`, "g"), `{/* Removed <${name}>: unresolved during fallback migration */}`);
2398
+ const removed = mdxComment(` Removed <${name}>: unresolved during fallback migration `);
2399
+ result = replaceOutsideCodeAndComments(result, (segment) => segment.replace(new RegExp(`<${name}(?:\\s[^>]*)?/>`, "g"), removed).replace(new RegExp(`<${name}(?:\\s[^>]*)?>[\\s\\S]*?<\\/${name}>`, "g"), removed));
2384
2400
  }
2385
2401
  return result;
2386
2402
  }
@@ -3043,7 +3059,7 @@ ${inlineSource}` });
3043
3059
  const start = node.position.start.offset;
3044
3060
  const end = node.position.end.offset;
3045
3061
  const videoId = youtubeEmbedVideoId(node, specifier);
3046
- const value = videoId ? `<iframe width="560" height="315" src="https://www.youtube.com/embed/${videoId}" title="Embedded video" allowFullScreen />` : `{/* Removed <${node.name}>: unsupported import '${specifier}' */}`;
3062
+ const value = videoId ? `<iframe width="560" height="315" src="https://www.youtube.com/embed/${videoId}" title="Embedded video" allowFullScreen />` : mdxComment(` Removed <${node.name}>: unsupported import '${specifier}' `);
3047
3063
  edits.push({ start, end, value });
3048
3064
  });
3049
3065
  const rendered = applyReplacements(content, edits);
@@ -6641,7 +6657,7 @@ function selectGroupWithinBudget(scanned, rank, warnings, allVersionPrefixes, la
6641
6657
  const dropped = ranked.slice(budget).map(({ file }) => file);
6642
6658
  if (warnings) {
6643
6659
  const droppedPages = label === "file" ? dropped.filter((file) => isDocumentationExtension(file.relativePath)) : dropped;
6644
- const noun = label === "file" ? "page" : "image or media file";
6660
+ const noun = label === "file" ? "page" : label === "snippet" ? "snippet file" : "image or media file";
6645
6661
  const droppedVersions = /* @__PURE__ */ new Set();
6646
6662
  for (const file of dropped) {
6647
6663
  const firstSegment = file.relativePath.split("/", 1)[0];
@@ -6651,16 +6667,19 @@ function selectGroupWithinBudget(scanned, rank, warnings, allVersionPrefixes, la
6651
6667
  const rest = droppedPages.length - examples.length;
6652
6668
  warnings.push({
6653
6669
  code: "limit-reached",
6654
- message: `This repository has more than ${budget} ${label === "file" ? "files" : "assets"}, so only the first ${budget} (in navigation order, default version first) were migrated. ${droppedPages.length} ${noun}(s) were left out` + (examples.length > 0 ? `: ${examples.join(", ")}${rest > 0 ? `, and ${rest} more` : ""}` : "") + (droppedVersions.size > 0 ? ` (versions: ${[...droppedVersions].slice(0, 5).join(", ")}${droppedVersions.size > 5 ? `, and ${droppedVersions.size - 5} more` : ""})` : "") + (label === "file" ? ". To include them, run the migration on a smaller part of the repository with --docs-dir." : ". Copy them into public/ manually if your pages use them.")
6670
+ message: `This repository has more than ${budget} ${label === "file" ? "files" : label === "snippet" ? "snippets" : "assets"}, so only the first ${budget} (in navigation order, default version first) were migrated. ${droppedPages.length} ${noun}(s) were left out` + (examples.length > 0 ? `: ${examples.join(", ")}${rest > 0 ? `, and ${rest} more` : ""}` : "") + (droppedVersions.size > 0 ? ` (versions: ${[...droppedVersions].slice(0, 5).join(", ")}${droppedVersions.size > 5 ? `, and ${droppedVersions.size - 5} more` : ""})` : "") + (label === "asset" ? ". Copy them into public/ manually if your pages use them." : ". To include them, run the migration on a smaller part of the repository with --docs-dir.")
6655
6671
  });
6656
6672
  }
6657
6673
  return ranked.slice(0, budget).map(({ file }) => file);
6658
6674
  }
6659
6675
  function selectFilesWithinBudget(scanned, rank, warnings, allVersionPrefixes, budget = MAX_SOURCE_FILES) {
6660
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));
6661
6679
  const assetFiles = scanned.filter(isAsset);
6662
- const otherFiles = scanned.filter((file) => !isAsset(file));
6680
+ const otherFiles = scanned.filter((file) => !isAsset(file) && !isSnippet(file));
6663
6681
  return [
6682
+ ...selectGroupWithinBudget(snippetFiles, rank, warnings, allVersionPrefixes, "snippet", budget),
6664
6683
  ...selectGroupWithinBudget(otherFiles, rank, warnings, allVersionPrefixes, "file", budget),
6665
6684
  ...selectGroupWithinBudget(assetFiles, rank, warnings, allVersionPrefixes, "asset", budget)
6666
6685
  ];
@@ -7159,6 +7178,13 @@ function interpolateSnippet(snippet, attributes, children) {
7159
7178
  });
7160
7179
  }).join("\n");
7161
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
+ }
7162
7188
  function snippetComponentBody(source, componentName) {
7163
7189
  const parsed = ts3.createSourceFile("snippet.tsx", source, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TSX);
7164
7190
  for (const statement of parsed.statements) {
@@ -7607,7 +7633,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7607
7633
  message: `Imported snippet ${sourcePath} could not be resolved and was left as a comment.`,
7608
7634
  source: relative5(repositoryRoot, currentFile).replace(/\\/g, "/")
7609
7635
  });
7610
- snippets.set(componentName, `{/* Missing imported snippet: ${sourcePath} */}`);
7636
+ snippets.set(componentName, mdxComment(` Missing imported snippet: ${sourcePath} `));
7611
7637
  return "";
7612
7638
  }
7613
7639
  }
@@ -7676,48 +7702,49 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7676
7702
  if (declaration) preservedDeclarations.set(componentName, declaration);
7677
7703
  else snippets.set(componentName, snippetComponentBody(nested, componentName));
7678
7704
  }
7679
- let result = withoutImports;
7680
- for (const [componentName, snippet] of snippets) {
7681
- result = result.replace(new RegExp(`<${componentName}((?:\\s[^>]*)?)\\s*/>`, "g"), (_tag, attributes) => {
7682
- return interpolateSnippet(snippet, attributes);
7683
- }).replace(new RegExp(`<${componentName}((?:\\s[^>]*)?)>([\\s\\S]*?)<\\/${componentName}>`, "g"), (_tag, attributes, children) => {
7684
- return interpolateSnippet(snippet, attributes, children);
7685
- });
7686
- }
7687
- result = result.replace(SNIPPET_TAG_PATTERN, (_tag, doubleQuoted, singleQuoted) => {
7688
- const filePath = doubleQuoted ?? singleQuoted;
7689
- try {
7690
- const candidate = [
7691
- () => resolveWithin(siteRoot, `snippets/${filePath}`),
7692
- () => resolveSnippetPath(filePath, currentFile, repositoryRoot, siteRoot)
7693
- ].map((resolveCandidate) => {
7694
- try {
7695
- return resolveCandidate();
7696
- } catch {
7697
- return void 0;
7698
- }
7699
- }).find((path) => path !== void 0 && existsSync5(path) && lstatSync5(path).isFile());
7700
- if (!candidate) throw new Error("file not found");
7701
- const blocked = blockInline(candidate, filePath, `Snippet file="${filePath}"`);
7702
- if (blocked) return blocked;
7703
- return inlineMdxSnippets(
7704
- withoutFrontmatter(readFileSync5(candidate, "utf8")),
7705
- candidate,
7706
- repositoryRoot,
7707
- warnings,
7708
- depth + 1,
7709
- siteRoot,
7710
- globalAliases,
7711
- gate
7712
- );
7713
- } catch {
7714
- warnings.push({
7715
- code: "missing-page",
7716
- message: `Snippet file="${filePath}" could not be resolved and was left as a comment.`,
7717
- 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);
7718
7711
  });
7719
- return `{/* Missing snippet: ${filePath} */}`;
7720
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
+ });
7721
7748
  });
7722
7749
  if (preservedDeclarations.size > 0) {
7723
7750
  const frontmatter = result.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
@@ -7726,7 +7753,7 @@ function inlineMdxSnippets(raw, currentFile, repositoryRoot, warnings, depth = 0
7726
7753
 
7727
7754
  ${result.slice(prefix.length)}`;
7728
7755
  }
7729
- return depth === 0 ? hoistMdxImports(result) : result;
7756
+ return depth === 0 ? hoistMdxImports(result) : closeOpenFence(result);
7730
7757
  }
7731
7758
  function injectOpenApiSpecs(config, specs, warnings) {
7732
7759
  const tabs = config.tabs.map((tab) => ({ ...tab }));
@@ -8214,7 +8241,7 @@ function migrateRepository(options) {
8214
8241
  const publishedText = trackPublishedRefs ? raw : void 0;
8215
8242
  const pageAssetReferences = [];
8216
8243
  if (platform === "fern" || platform === "mintlify" || platform === "docusaurus") {
8217
- raw = normalizeExplicitHeadingIds(raw);
8244
+ raw = normalizeExplicitHeadingIds(raw, (message) => warnings.push({ code: "invalid-page", message, source: file.relativePath }));
8218
8245
  }
8219
8246
  if (componentMigrator) {
8220
8247
  const warningsBeforeTransform = warnings.length;