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 packages/platform/src/react/chat.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,8 @@ export interface Base44ChatOptions {
/**
* Called with the new app after {@link Base44Chat.create} succeeds. The chat already follows it.
* Pass its ID back as `appId` to keep it in your own state.
*
* @param app - The app that was created.
*/
onAppCreated?: (app: Base44App) => void;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1996,7 +1996,7 @@ function applyTypeDefinitionGrouping(dir) {
* Remove links to types that are not in the exposed types list.
* Converts [TypeName](path) or [`TypeName`](path) to just TypeName or `TypeName`.
*/
function removeNonExposedTypeLinks(content, exposedTypeNames) {
function removeNonExposedTypeLinks(content, exposedTypeNames, deletedTypeNames = new Set()) {
// Match markdown links where the link text is a type name (with or without backticks)
// Pattern: [TypeName](some/path/TypeName) or [`TypeName`](some/path/TypeName)
const linkRegex = /\[(`?)([A-Z][A-Za-z0-9]*)\1\]\(([^)]+)\)/g;
Expand Down Expand Up @@ -2027,27 +2027,40 @@ function removeNonExposedTypeLinks(content, exposedTypeNames) {
}
);

return { content: updatedContent, modified };
// A type deleted after processing has no page, whatever the link's text says.
const withoutDeletedLinks = updatedContent.replace(
/\[([^\]]+)\]\(([^)\s]+)\)/g,
(match, text, linkPath) => {
if (/^https?:\/\//.test(linkPath)) return match;
const pathEnd = linkPath.split("#")[0].split("/").pop().replace(/\.mdx?$/, "");
if (!deletedTypeNames.has(pathEnd)) return match;
modified = true;
return text;
}
);

return { content: withoutDeletedLinks, modified };
}

/**
* Apply non-exposed type link removal to all files
*/
function applyNonExposedTypeLinkRemoval(dir, exposedTypeNames) {
function applyNonExposedTypeLinkRemoval(dir, exposedTypeNames, deletedTypeNames) {
if (!fs.existsSync(dir)) return;
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const entryPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
applyNonExposedTypeLinkRemoval(entryPath, exposedTypeNames);
applyNonExposedTypeLinkRemoval(entryPath, exposedTypeNames, deletedTypeNames);
} else if (
entry.isFile() &&
(entry.name.endsWith(".mdx") || entry.name.endsWith(".md"))
) {
const content = fs.readFileSync(entryPath, "utf-8");
const { content: updated, modified } = removeNonExposedTypeLinks(
content,
exposedTypeNames
exposedTypeNames,
deletedTypeNames
);
if (modified) {
fs.writeFileSync(entryPath, updated, "utf-8");
Expand Down Expand Up @@ -2182,22 +2195,23 @@ function cleanupTruncatedParamTypes(dir) {
/**
* Main function
*/
/** The types listed in types-to-delete-after-processing.json. */
function loadTypesToDelete() {
try {
const parsed = JSON.parse(fs.readFileSync(TYPES_TO_DELETE_PATH, "utf-8"));
return new Set(Array.isArray(parsed) ? parsed : []);
} catch (e) {
// No types to delete, that's fine
return new Set();
}
}

/**
* Delete types that should not appear in navigation but were needed for inline rendering.
* These types are listed in types-to-delete-after-processing.json
*/
function deleteTypesAfterProcessing(docsDir) {
let typesToDelete = new Set();
try {
const content = fs.readFileSync(TYPES_TO_DELETE_PATH, "utf-8");
const parsed = JSON.parse(content);
if (Array.isArray(parsed)) {
typesToDelete = new Set(parsed);
}
} catch (e) {
// No types to delete, that's fine
return;
}
const typesToDelete = loadTypesToDelete();

if (typesToDelete.size === 0) {
return;
Expand Down Expand Up @@ -2772,7 +2786,7 @@ function main() {
applyTypeDeclarationLinking(DOCS_DIR);

// Remove links to types that aren't exposed (would 404)
applyNonExposedTypeLinkRemoval(DOCS_DIR, exposedTypeNames);
applyNonExposedTypeLinkRemoval(DOCS_DIR, exposedTypeNames, loadTypesToDelete());

// Delete types that should not appear in navigation but were needed for inline rendering
deleteTypesAfterProcessing(DOCS_DIR);
Expand Down Expand Up @@ -2801,4 +2815,9 @@ function main() {
console.log(` Documentation directory: ${DOCS_DIR}`);
}

main();
// Run as a script; tests import the helpers without processing anything.
if (process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(__filename)) {
main();
}

export { removeNonExposedTypeLinks };
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* Content transformation functions (examples, frontmatter, etc.)
*/

import { ReflectionKind } from "typedoc";

/**
* Add headings to CodeGroups that don't have them
*/
Expand Down Expand Up @@ -131,3 +133,48 @@ title: "${escapeYaml(title)}"
return frontmatter + processedContent;
}


/**
* typedoc-plugin-markdown renders a property whose type is a function like a method: "name()" at
* the level of the other members, with its Parameters and Returns one level below. Post-processing
* then moves the other properties down a level and leaves these alone, because their heading has
* "()". Name it as the property it is, and move its own subsection down with it, so it reads and
* nests like its siblings. Which members are such properties comes from the reflection, not from
* the heading text.
*/
export function nameCallableProperties(content, model) {
const callable = new Set(
(model?.children ?? [])
.filter(
(child) =>
child.kind === ReflectionKind.Property &&
child.type?.type === "reflection" &&
(child.type.declaration?.signatures?.length ?? 0) > 0
)
.map((child) => child.name)
);
if (callable.size === 0) return content;

const lines = content.split("\n");
let inFence = false;
// The level of the callable property being rewritten, while inside its subsection.
let propertyLevel = null;
for (let i = 0; i < lines.length; i++) {
if (lines[i].trim().startsWith("```")) inFence = !inFence;
if (inFence) continue;
const heading = lines[i].match(/^(#{1,6}) (.*)$/);
if (!heading) continue;
const level = heading[1].length;
if (propertyLevel !== null && level > propertyLevel) {
if (level < 6) lines[i] = `#${lines[i]}`;
continue;
}
propertyLevel = null;
const property = heading[2].match(/^(\w+)\(\)(\??)$/);
if (property && callable.has(property[1])) {
lines[i] = `${heading[1]} ${property[1]}${property[2]}`;
propertyLevel = level;
}
}
return lines.join("\n");
}
Original file line number Diff line number Diff line change
Expand Up @@ -847,7 +847,8 @@ function renderCommentPart(part, context) {
case "text":
return part.text || "";
case "code":
return part.text ? `\`${part.text}\`` : "";
// TypeDoc keeps the backticks in a code part's text.
return part.text || "";
case "inline-tag":
if (part.tag === "@link") {
const linkText = (part.text || part.target?.name || "").trim();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
convertExamplesToCodeGroup,
addMintlifyFrontmatter,
addHeadingsToCodeGroups,
nameCallableProperties,
} from "./typedoc-mintlify-content.js";

// Shared flag with the post-processing script so Panel output can be toggled.
Expand Down Expand Up @@ -92,6 +93,10 @@ export function load(app) {
);
}

// 2a. A property whose type is a function is a property, not a method. After step 2, which
// finds Parameters and Returns at their usual levels.
content = nameCallableProperties(content, page.model);

// 2a. Document each value of a literal union; typedoc-plugin-markdown drops their comments
if (page.model?.kind === ReflectionKind.TypeAlias) {
content = appendUnionValueFields(content, page.model.type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ import {
} from "./typedoc-mintlify-linked-types.js";
import { escapeAttribute } from "./typedoc-mintlify-utils.js";

// The name of a return with no fields of its own. Such a return renders as its description alone.
const PLAIN_RETURN = "result";

// Whether a Returns section already shows this text, ignoring how its lines wrap. The @returns
// sentence can reach a section more than once: as the section's own text, from the reflection,
// and as a plain return's description.
function isShown(text, shown) {
const normalize = (value) => value.replace(/\s+/g, " ").trim();
return shown.some((part) => normalize(part) === normalize(text));
}

const PRIMITIVE_TYPES = [
"any",
"string",
Expand Down Expand Up @@ -56,7 +67,8 @@ function renderCommentParts(parts) {
case "text":
return part.text || "";
case "code":
return part.text ? "`" + part.text + "`" : "";
// TypeDoc keeps the backticks in a code part's text.
return part.text || "";
case "inline-tag":
if (part.tag === "@link") {
return (part.text || part.target?.name || "").trim();
Expand Down Expand Up @@ -500,7 +512,7 @@ function rewriteReturnSections(content, options) {
descriptionParts.push(leadingText);
}
const returnsDescription = extractReturnsDescription(page);
if (returnsDescription) {
if (returnsDescription && !isShown(returnsDescription, descriptionParts)) {
descriptionParts.push(returnsDescription);
}
if (descriptionParts.length > 0) {
Expand All @@ -514,7 +526,7 @@ function rewriteReturnSections(content, options) {
writeLinkedTypesFile,
indexSignature
);
if (fieldsBlock) {
if (fieldsBlock && !isShown(fieldsBlock, descriptionParts)) {
result.push("");
result.push(fieldsBlock);
result.push("");
Expand Down Expand Up @@ -584,11 +596,16 @@ function rewriteReturnSections(content, options) {
if (fields.length === 0 && !indexSignature) {
result.push(...sectionLines);
} else {
// The section's own leading type link is the fallback when the signature gave no type.
// The section's own leading type is the fallback when the signature gave no type, as for a
// property whose type is a function.
const plainType =
fields.length === 1 && fields[0].name === PLAIN_RETURN && fields[0].type !== "any"
? fields[0].type
: null;
const typeNameForDisplay = enrichTypeNameWithGenerics(
extractedTypeName || returnTypeName,
returnTypeFromSignature
) || sectionContent.match(/^\[`([^`]+)`\]\(/)?.[1];
) || sectionContent.match(/^\[`([^`]+)`\]\(/)?.[1] || plainType;
if (typeNameForDisplay) {
result.push("");
result.push(`\`${typeNameForDisplay}\``);
Expand All @@ -611,7 +628,7 @@ function rewriteReturnSections(content, options) {
writeLinkedTypesFile,
indexSignature
);
if (fieldsBlock) {
if (fieldsBlock && !isShown(fieldsBlock, descriptionParts)) {
result.push("");
result.push(fieldsBlock);
result.push("");
Expand Down Expand Up @@ -850,9 +867,10 @@ function parseReturnFields(
});
}

// The section's own text is the @returns description; keep it above the properties.
return {
fields: resultFields,
leadingText: "",
leadingText: description,
extractedTypeName: typeInfoToUse.typeName, // Pass the type name for display
typeDescription:
getLinkedTypeDescription(typeInfoToUse, context) || "",
Expand All @@ -861,19 +879,10 @@ function parseReturnFields(
}
}

// Use 'result' as default name, or extract from description
let name = "result";
if (description) {
// Check if description contains a type hint
const typeHint = description.match(/(\w+)\s+(?:instance|object|value)/i);
if (typeHint) {
name = typeHint[1].toLowerCase();
}
}
return {
fields: [
{
name,
name: PLAIN_RETURN,
type,
description,
optional: false,
Expand Down Expand Up @@ -1123,7 +1132,7 @@ function formatReturnFieldsOutput(

const isSingleSimpleField =
fields.length === 1 &&
fields[0].name === "result" &&
fields[0].name === PLAIN_RETURN &&
(!fields[0].nested || fields[0].nested.length === 0) &&
!indexSignature;

Expand Down
11 changes: 11 additions & 0 deletions packages/sdk/tests/unit/fixtures/typedoc-mintlify/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"skipLibCheck": true,
"noEmit": true
},
"include": ["widgets.ts"]
}
Loading
Loading