diff --git a/.changeset/fix-apidoc-broken-links.md b/.changeset/fix-apidoc-broken-links.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/fix-apidoc-broken-links.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/scripts/generate-api-docs/generate.mjs b/scripts/generate-api-docs/generate.mjs index 505f453ad6..478c397d6c 100644 --- a/scripts/generate-api-docs/generate.mjs +++ b/scripts/generate-api-docs/generate.mjs @@ -161,7 +161,93 @@ for (const file of dtsFiles) { } console.log(` Found ${defaultExports.size} default exports`); -// Second pass: rewrite the files +// Precompute namespace prefixes for all libraries (used to detect augmentations) +// e.g. "sap.f" → "sap/f/", "sap.ui.core" → "sap/ui/core/" +const libNamespacePrefixes = new Map(); +for (const lib of libraryModules.keys()) { + libNamespacePrefixes.set(lib, lib.replace(/\./g, "/") + "/"); +} + +// Remove cross-library augmentation blocks from .d.ts files. +// E.g. sap.f.d.ts augments "sap/tnt/library" — remove that block so TypeDoc +// sees "sap/tnt/library" only in sap.tnt.d.ts, generating the full content there. +let totalAugmentationsRemoved = 0; +for (const file of dtsFiles) { + const libName = file.replace(/\.d\.ts$/, ""); + const filePath = join(TYPES_DIR, file); + let content = readFileSync(filePath, "utf8"); + + const moduleRegex = /^declare module "([^"]+)"/gm; + const blocksToRemove = []; + let match; + while ((match = moduleRegex.exec(content)) !== null) { + const moduleName = match[1]; // e.g. "sap/tnt/library" + const moduleNs = moduleName + "/"; + + // Check if another library owns this module's namespace + for (const [otherLib, otherNs] of libNamespacePrefixes) { + if (otherLib !== libName && moduleNs.startsWith(otherNs)) { + // This is an augmentation of another library's module — mark for removal + const blockStart = match.index; + // Find the opening { of this declare module block + const openBrace = content.indexOf("{", blockStart); + if (openBrace < 0) break; // malformed — no opening brace, skip + // Find the closing } by counting brace depth (skip braces inside comments and strings) + let braceDepth = 0; + let blockEnd = -1; + for (let i = openBrace; i < content.length; i++) { + const ch = content[i]; + // Skip single-line comments + if (ch === "/" && content[i + 1] === "/") { + i = content.indexOf("\n", i); + if (i < 0) break; + continue; + } + // Skip block comments (including JSDoc) + if (ch === "/" && content[i + 1] === "*") { + i = content.indexOf("*/", i + 2); + if (i < 0) break; + i += 1; // will be incremented by for-loop to skip past '*/' + continue; + } + // Skip string literals (double-quoted) + if (ch === '"') { + i++; + while (i < content.length && content[i] !== '"') { + if (content[i] === "\\") i++; + i++; + } + continue; + } + if (ch === "{") braceDepth++; + else if (ch === "}") { + braceDepth--; + if (braceDepth === 0) { blockEnd = i + 1; break; } + } + } + if (blockEnd <= blockStart) break; // couldn't find matching close brace + blocksToRemove.push([blockStart, blockEnd]); + break; + } + } + } + + // Remove blocks in reverse order to preserve indices + for (const [start, end] of blocksToRemove.reverse()) { + content = content.slice(0, start) + content.slice(end); + } + + if (blocksToRemove.length > 0) { + writeFileSync(filePath, content); + totalAugmentationsRemoved += blocksToRemove.length; + console.log(` Removed ${blocksToRemove.length} cross-library augmentation(s) from ${file}`); + } +} +if (totalAugmentationsRemoved > 0) { + console.log(` Total augmentations removed: ${totalAugmentationsRemoved}`); +} + +// Rewrite pass: rename default exports to named exports for (const file of dtsFiles) { const filePath = join(TYPES_DIR, file); let content = readFileSync(filePath, "utf8"); @@ -376,12 +462,19 @@ let skippedCount = 0; // Directories/files to exclude from output (not UI5-specific) const EXCLUDE_PATTERNS = ["interfaces/JQuery", "interfaces/JQuery.", "/JQueryStatic", "JQueryPromise"]; -// Precompute namespace prefixes for all libraries (used to detect augmentations) -// e.g. "sap.f" → "sap/f/", "sap.ui.core" → "sap/ui/core/" -const libNamespacePrefixes = new Map(); -for (const lib of libraryModules.keys()) { - libNamespacePrefixes.set(lib, lib.replace(/\./g, "/") + "/"); -} +// Link patterns pointing to dead targets (jQuery, QUnit, pseudo-types) +const DEAD_LINK_PATTERNS = [ + /namespaces\/JQuery[./]/i, + /namespaces\/jQuery[./]/i, + /interfaces\/JQuery/, + /\/JQueryStatic/, + /JQueryPromise/, + /type-aliases\/jQuery\.html/, + /variables\/jQuery\.html/, + /namespaces\/QUnit[./]/, + /variables\/QUnit\.html/, + /type-aliases\/(int|float)\.html/, +]; function shouldExclude(relPath) { // Exclude jQuery-related pages @@ -493,6 +586,24 @@ function renderDir(dir) { htmlFixed = htmlFixed.replace(/([^<]*)<\/a>/g, "$1"); htmlFixed = htmlFixed.replace(/([^<]*)<\/code><\/a>/g, "$1"); + // Strip links to ClassInfo (global type alias, no useful page) + htmlFixed = htmlFixed.replace( + /]*>([^<]+)<\/code><\/a>/g, + "$1" + ); + + // Strip links whose href points to dead targets (jQuery, QUnit, pseudo-types, nested namespaces) + htmlFixed = htmlFixed.replace(/]*>([^<]+<\/code>)<\/a>/g, (match, href, content) => { + if (DEAD_LINK_PATTERNS.some((p) => p.test(href))) return content; + // Links with nested namespaces/ segments are always dead TypeDoc artifacts + if ((href.match(/\/namespaces\//g) || []).length >= 2) return content; + return match; + }); + + // Remove TypeDoc "References" section (global namespace re-export artifacts) + // This is the raw content before htmlTemplate wraps it, so strip to end of string + htmlFixed = htmlFixed.replace(/

References<\/h2>[\s\S]*$/, ""); + // Add import hint for class/interface/type-alias pages // Path pattern: sap.m/sap/m/Button/classes/Button.html → module "sap/m/Button", class "Button" const classMatch = rel.match(/^[^/]+\/(.+)\/(classes|interfaces|type-aliases)\/([^/]+)\.html$/); @@ -524,6 +635,32 @@ function renderDir(dir) { renderDir(MD_DIR); console.log(` Rendered ${fileCount} HTML files (skipped ${skippedCount} jQuery-related)`); +// Post-processing: prune README entries pointing to non-existent pages +// This covers ambient globals (QUnit, global_Element) and ungenerated web component wrappers +console.log(" Pruning README entries with dead links..."); +let prunedEntries = 0; +function pruneReadmes(dir) { + for (const entry of readdirSync(dir)) { + const full = join(dir, entry); + const stat = statSync(full); + if (stat.isDirectory()) { + pruneReadmes(full); + } else if (entry === "README.html") { + let html = readFileSync(full, "utf8"); + const original = html; + html = html.replace(/
  • [^<]+<\/a><\/li>\n?/g, (match, href) => { + const target = join(dirname(full), href.split("#")[0]); + if (existsSync(target)) return match; + prunedEntries++; + return ""; + }); + if (html !== original) writeFileSync(full, html); + } + } +} +pruneReadmes(OUT_DIR); +if (prunedEntries > 0) console.log(` Removed ${prunedEntries} dead README entries`); + // --- Step 5: Generate sitemap --- console.log("\nStep 5: Generating sitemap..."); const sitemapEntries = [];