Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/fix-apidoc-broken-links.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
151 changes: 144 additions & 7 deletions scripts/generate-api-docs/generate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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,
Comment thread
codeworrior marked this conversation as resolved.
/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
Expand Down Expand Up @@ -493,6 +586,24 @@ function renderDir(dir) {
htmlFixed = htmlFixed.replace(/<a href="[^"]*default\.html[^"]*">([^<]*)<\/a>/g, "$1");
htmlFixed = htmlFixed.replace(/<a href="[^"]*default\.html[^"]*"><code>([^<]*)<\/code><\/a>/g, "<code>$1</code>");

// Strip links to ClassInfo (global type alias, no useful page)
htmlFixed = htmlFixed.replace(
/<a href="[^"]*\/type-aliases\/ClassInfo\.html(?:#[^"]*)?"[^>]*><code>([^<]+)<\/code><\/a>/g,
"<code>$1</code>"
);

// Strip links whose href points to dead targets (jQuery, QUnit, pseudo-types, nested namespaces)
htmlFixed = htmlFixed.replace(/<a href="([^"]*)"[^>]*>(<code>[^<]+<\/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(/<h2>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$/);
Expand Down Expand Up @@ -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(/<li><a href="([^"]+)">[^<]+<\/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 = [];
Expand Down
Loading