UNPKG

@raven-js/glean

Version:

Glean documentation gold from your codebase - JSDoc parsing, validation, and beautiful doc generation

755 lines (682 loc) 22.5 kB
/** * @author Anonyfox <max@anonyfox.com> * @license MIT * @see {@link https://github.com/Anonyfox/ravenjs} * @see {@link https://ravenjs.dev} * @see {@link https://anonyfox.com} */ /** * Entity page data extractor for /modules/{moduleName}/{entityName}/ route * * Implements comprehensive entity documentation extraction including JSDoc processing, * cross-references, related entities, and navigation context following WEBAPP.md * specification for detailed API documentation pages. */ /** * Extract entity page data for documentation rendering with comprehensive JSDoc processing. * * Implements full entity documentation extraction including parameters, examples, * cross-references, and navigation context for detailed API pages. * * @param {import('../../extract/models/package.js').Package} packageInstance - Package data * @param {string} moduleName - Module name from URL parameter * @param {string} entityName - Entity name from URL parameter * @returns {Object} Complete entity page data structure * * @example * // Extract data for a function entity * const pageData = extractEntityPageData(package, 'utils', 'myFunction'); * console.log(pageData.documentation.parameters); * * @example * // Extract data for a class entity * const pageData = extractEntityPageData(package, 'core', 'MyClass'); * console.log(pageData.documentation.methods); */ export function extractEntityPageData( packageInstance, moduleName, entityName, options = {}, ) { const { urlBuilder } = /** @type {any} */ (options); /** @type {any} */ const pkg = packageInstance; // STEP 1: Find the target entity const packageName = packageInstance.name || ""; // Find target module using same logic as module overview let module = null; if (packageInstance.findModuleByImportPath) { module = packageInstance.findModuleByImportPath(moduleName); } else { module = packageInstance.modules.find((m) => m.importPath === moduleName); } // If not found, try constructing full import path if (!module && packageName) { const fullImportPath = `${packageName}/${moduleName}`; if (packageInstance.findModuleByImportPath) { module = packageInstance.findModuleByImportPath(fullImportPath); } else { module = packageInstance.modules.find( (m) => m.importPath === fullImportPath, ); } } // If still not found, try finding by module name (last part of import path) if (!module) { module = packageInstance.modules.find( (m) => m.importPath.split("/").pop() === moduleName, ); } if (!module) { throw new Error( `Module '${moduleName}' not found in package '${packageName}'`, ); } // Find target entity within the module /** @type {any} */ let entity = null; /** @type {any} */ const mod = module; if (mod.findEntityByName) { entity = mod.findEntityByName(entityName); } else if (mod.entities) { entity = mod.entities.find( /** @param {any} e */ (e) => e.name === entityName, ); } if (!entity) { throw new Error( `Entity '${entityName}' not found in module '${mod.importPath}'`, ); } // STEP 2: Core entity information const entityData = { name: entity.name, type: entity.entityType || "unknown", description: entity.description || "", moduleId: entity.moduleId || mod.importPath, // Import statement generation importPath: mod.importPath, importStatement: `import { ${entity.name} } from '${mod.importPath}';`, isDefault: mod.isDefault || false, }; // STEP 3: JSDoc documentation extraction const documentation = { // Parameters (for functions) parameters: extractParameters(entity), // Properties (for typedefs and classes) properties: extractProperties(entity), // Methods (for classes) methods: extractMethods(entity), // Return information returns: extractReturns(entity), // Examples examples: extractExamples(entity), // Other JSDoc tags since: extractSinceVersion(entity), deprecated: extractDeprecationInfo(entity), author: extractAuthorInfo(entity), throws: extractThrowsInfo(entity), // Type information for TypeScript users typeInfo: extractTypeInfo(entity), }; // STEP 4: Related entities and cross-references const relatedEntities = { // Entities in the same module sameModule: getSameModuleEntities(mod, entity, urlBuilder), // Entities that reference this entity referencedBy: getReferencingEntities(pkg, entity), // Entities that this entity references references: getReferencedEntities(pkg, entity), // Similar entities (same type across modules) similar: getSimilarEntities(pkg, entity, urlBuilder), }; // STEP 5: Navigation context const navigationContext = { packageName: packageName, currentModule: { name: moduleName, fullImportPath: mod.importPath, link: urlBuilder ? /** @type {any} */ (urlBuilder).moduleUrl(moduleName) : `/modules/${moduleName}/`, }, currentEntity: { name: entityName, type: entity.entityType || "unknown", }, allModules: pkg.modules.map( /** @param {any} m */ (m) => ({ name: m.importPath.split("/").pop(), fullImportPath: m.importPath, isCurrent: m.importPath === mod.importPath, isDefault: m.isDefault || false, link: urlBuilder ? /** @type {any} */ (urlBuilder).moduleUrl( m.importPath.split("/").pop(), ) : `/modules/${m.importPath.split("/").pop()}/`, entityCount: m.publicEntityCount || 0, }), ), moduleEntities: getModuleEntityNavigation(mod, entity, urlBuilder), }; // STEP 6: Build complete data structure /** @type {any} */ const docs = documentation; return { entity: entityData, entityInstance: entity, // Pass the real entity instance for attribution documentation, relatedEntities, navigation: navigationContext, packageName: packageName, moduleName: moduleName, packageMetadata: extractPackageMetadata(packageInstance), // Add package metadata allModules: packageInstance.modules, // Pass all modules for re-export tracing generationTimestamp: new Date().toISOString(), // Generation timestamp for footer // Computed flags for conditional rendering hasParameters: docs.parameters.length > 0, hasProperties: docs.properties.length > 0, hasMethods: docs.methods.length > 0, hasReturns: Boolean(docs.returns.description), hasExamples: docs.examples.length > 0, hasRelatedEntities: relatedEntities.sameModule.length > 0 || relatedEntities.referencedBy.length > 0 || relatedEntities.references.length > 0, hasTypeInfo: Boolean(docs.typeInfo.signature), isDeprecated: Boolean(docs.deprecated.isDeprecated), }; } /** * Extract package metadata for attribution * @param {import('../../extract/models/package.js').Package} packageInstance - Package instance * @returns {Object|null} Package metadata for attribution */ function extractPackageMetadata(packageInstance) { /** @type {any} */ const pkg = packageInstance; // Get package.json data from the package instance if (pkg.packageJsonData) { return { author: pkg.packageJsonData.author, homepage: pkg.packageJsonData.homepage, repository: pkg.packageJsonData.repository, bugs: pkg.packageJsonData.bugs, funding: pkg.packageJsonData.funding, }; } // Fallback to null if no package data available return null; } /** * Extract parameter information from JSDoc tags * @param {Object} entity - Entity instance * @returns {Array<Object>} Parameter documentation */ function extractParameters(entity) { /** @type {any} */ const ent = entity; if (!ent.getJSDocTagsByType) return []; return ent.getJSDocTagsByType("param").map( /** @param {any} paramTag */ (paramTag) => ({ name: paramTag.name || "", type: paramTag.type || "", description: paramTag.description || "", isOptional: paramTag.optional || false, defaultValue: paramTag.defaultValue || null, }), ); } /** * Extract property information from JSDoc @property tags (for typedefs) and class properties * @param {Object} entity - Entity instance * @returns {Array<Object>} Property documentation */ function extractProperties(entity) { /** @type {any} */ const ent = entity; const properties = []; // Extract JSDoc @property tags (for typedefs) if (ent.getJSDocTagsByType) { properties.push( ...ent.getJSDocTagsByType("property").map( /** @param {any} propertyTag */ (propertyTag) => ({ name: propertyTag.name || "", type: propertyTag.type || "", description: propertyTag.description || "", isOptional: propertyTag.optional || false, defaultValue: propertyTag.defaultValue || null, }), ), ); } // Extract class properties (for class entities) if ( ent.entityType === "class" && ent.properties && Array.isArray(ent.properties) ) { properties.push( ...ent.properties.map( /** @param {any} classProp */ (classProp) => ({ name: classProp.name || "", type: inferPropertyType(classProp), // Try to infer better types description: classProp.description || (classProp.isInstance ? "Instance property" : "Class property"), isOptional: false, // Class properties are not optional by default defaultValue: classProp.hasInitializer ? "initialized" : null, isStatic: classProp.isStatic || false, isPrivate: classProp.isPrivate || false, isInstance: classProp.isInstance || false, }), ), ); } return properties; } /** * Extract method information from class entities * @param {Object} entity - Entity instance * @returns {Array<Object>} Method documentation */ function extractMethods(entity) { /** @type {any} */ const ent = entity; // Only extract methods for class entities if ( ent.entityType !== "class" || !ent.methods || !Array.isArray(ent.methods) ) { return []; } return ent.methods.map( /** @param {any} classMethod */ (classMethod) => ({ name: classMethod.name || "", type: classMethod.methodType || "method", description: getMethodDescription(classMethod, ent.name), returnType: getMethodReturnType(classMethod), parameters: getMethodParameters(classMethod), isStatic: classMethod.isStatic || false, isPrivate: classMethod.isPrivate || false, isAsync: classMethod.isAsync || false, isGenerator: classMethod.isGenerator || false, isDeprecated: classMethod.documentation?.deprecated || false, signature: classMethod.signature || "", line: classMethod.line || 0, }), ); } /** * Get method description from JSDoc or generate fallback * @param {{documentation?: {description?: string}, methodType?: string, name?: string, isStatic?: boolean}} classMethod - Class method object * @param {string} className - Name of the containing class * @returns {string} Method description */ function getMethodDescription(classMethod, className) { // Use JSDoc description if available if (classMethod.documentation?.description) { return classMethod.documentation.description; } // Generate fallback description const methodType = classMethod.methodType || "method"; if (methodType === "constructor") { return `Creates a new instance of ${className}`; } else if (methodType === "getter") { return `Gets the value of ${classMethod.name}`; } else if (methodType === "setter") { return `Sets the value of ${classMethod.name}`; } else { return `${classMethod.isStatic ? "Static method" : "Method"} of ${className}`; } } /** * Get method return type from JSDoc * @param {{documentation?: {returns?: {type?: string}}, methodType?: string}} classMethod - Class method object * @returns {string} Return type */ function getMethodReturnType(classMethod) { if (classMethod.documentation?.returns?.type) { return classMethod.documentation.returns.type; } // Infer return type from method type const methodType = classMethod.methodType || "method"; if (methodType === "constructor") { return "void"; } else if (methodType === "setter") { return "void"; } else if (methodType === "getter") { return "any"; } return "any"; } /** * Get method parameters from JSDoc * @param {{documentation?: {parameters?: any[]}, signature?: string}} classMethod - Class method object * @returns {Array<Object>} Method parameters */ function getMethodParameters(classMethod) { if (classMethod.documentation?.parameters) { return classMethod.documentation.parameters.map( /** @param {any} param */ (param) => ({ name: param.name, type: param.type, description: param.description, isOptional: param.optional || false, }), ); } // Try to extract parameters from signature const signature = classMethod.signature || ""; const paramMatch = signature.match(/\(([^)]*)\)/); if (paramMatch?.[1].trim()) { const paramNames = paramMatch[1] .split(",") .map(/** @param {string} p */ (p) => p.trim().split(/\s+/)[0]); return paramNames.map( /** @param {string} name */ (name) => ({ name: name.replace(/[=\s].*$/, ""), // Remove default values type: "any", description: "", isOptional: name.includes("=") || name.includes("null"), }), ); } return []; } /** * Try to infer property type from its characteristics * @param {{signature?: string, name?: string}} classProp - Class property object * @returns {string} Inferred type */ function inferPropertyType(classProp) { // If signature contains type hints, try to extract if (classProp.signature) { const signature = classProp.signature; // Look for common patterns if (signature.includes("new Float32Array")) return "Float32Array"; if (signature.includes("new Array")) return "Array"; if (signature.includes("[]")) return "Array"; if (signature.includes('"') || signature.includes("'")) return "string"; if (signature.includes("true") || signature.includes("false")) return "boolean"; if (/= \d+/.test(signature)) return "number"; if (signature.includes("null")) return "null"; if (signature.includes("{}")) return "Object"; } // Try to infer from property name patterns const name = classProp.name; if ( name.includes("count") || name.includes("length") || name.includes("size") || name === "rows" || name === "cols" ) { return "number"; } if (name.includes("is") || name.includes("has") || name.includes("enabled")) { return "boolean"; } if (name.includes("data") || name.includes("buffer")) { return "TypedArray"; } return "any"; // fallback } /** * Extract return information from JSDoc tags * @param {Object} entity - Entity instance * @returns {Object} Return documentation */ function extractReturns(entity) { /** @type {any} */ const ent = entity; if (!ent.getJSDocTag) return { type: "", description: "" }; const returnsTag = ent.getJSDocTag("returns") || ent.getJSDocTag("return"); return { type: returnsTag?.type || "", description: returnsTag?.description || "", }; } /** * Extract code examples from JSDoc tags * @param {Object} entity - Entity instance * @returns {Array<Object>} Example code blocks */ function extractExamples(entity) { /** @type {any} */ const ent = entity; if (!ent.getJSDocTagsByType) return []; return ent .getJSDocTagsByType("example") .map((/** @type {any} */ exampleTag, /** @type {any} */ index) => ({ code: exampleTag.description || exampleTag.code || "", title: exampleTag.title || `Example ${index + 1}`, language: exampleTag.language || "javascript", })); } /** * Extract version information from JSDoc tags * @param {Object} entity - Entity instance * @returns {string} Version information */ function extractSinceVersion(entity) { /** @type {any} */ const ent = entity; if (!ent.getJSDocTag) return ""; const sinceTag = ent.getJSDocTag("since"); return sinceTag?.description || ""; } /** * Extract deprecation information from JSDoc tags * @param {Object} entity - Entity instance * @returns {Object} Deprecation details */ function extractDeprecationInfo(entity) { /** @type {any} */ const ent = entity; if (!ent.hasJSDocTag || !ent.getJSDocTag) { return { isDeprecated: false, reason: "", since: "" }; } const isDeprecated = ent.hasJSDocTag("deprecated"); if (!isDeprecated) { return { isDeprecated: false, reason: "", since: "" }; } const deprecatedTag = ent.getJSDocTag("deprecated"); return { isDeprecated: true, reason: deprecatedTag?.description || "This API is deprecated", since: deprecatedTag?.since || "", }; } /** * Extract author information from JSDoc tags * @param {Object} entity - Entity instance * @returns {Array<string>} Author information */ function extractAuthorInfo(entity) { /** @type {any} */ const ent = entity; if (!ent.getJSDocTagsByType) return []; return ent .getJSDocTagsByType("author") .map(/** @param {any} tag */ (tag) => tag.description || ""); } /** * Extract exception/error information from JSDoc tags * @param {Object} entity - Entity instance * @returns {Array<Object>} Exception documentation */ function extractThrowsInfo(entity) { /** @type {any} */ const ent = entity; if (!ent.getJSDocTagsByType) return []; return ent .getJSDocTagsByType("throws") .concat(ent.getJSDocTagsByType("exception") || []) .map( /** @param {any} throwsTag */ (throwsTag) => ({ type: throwsTag.type || "Error", description: throwsTag.description || "", }), ); } /** * Extract TypeScript type information * @param {Object} entity - Entity instance * @returns {Object} Type information for TS users */ function extractTypeInfo(entity) { /** @type {any} */ const ent = entity; return { signature: ent.signature || "", typeParameters: ent.typeParameters || [], namespace: ent.namespace || "", }; } /** * Get entities in the same module (excluding current entity) * @param {Object} module - Module instance * @param {Object} currentEntity - Current entity * @param {Object} [urlBuilder] - URL builder for base path support * @returns {Array<Object>} Same module entities */ function getSameModuleEntities(module, currentEntity, urlBuilder) { /** @type {any} */ const mod = module; /** @type {any} */ const curr = currentEntity; if (!mod.entities) return []; return mod.entities .filter(/** @param {any} e */ (e) => e.name !== curr.name) .filter( /** @param {any} e */ (e) => !e.hasJSDocTag?.("private") && !e.name?.startsWith("_"), ) .slice(0, 10) // Limit for performance .map( /** @param {any} entity */ (entity) => ({ name: entity.name, type: entity.entityType || "unknown", description: (entity.description || "").slice(0, 100), link: urlBuilder ? /** @type {any} */ (urlBuilder).entityUrl( mod.importPath.split("/").pop(), entity.name, ) : `/modules/${mod.importPath.split("/").pop()}/${entity.name}/`, }), ); } /** * Get entities that reference this entity * @param {Object} packageInstance - Package instance * @param {Object} _entity - Current entity (unused in current implementation) * @returns {Array<Object>} Referencing entities */ function getReferencingEntities(packageInstance, /** @type {any} */ _entity) { /** @type {any} */ const pkg = packageInstance; if (!pkg.allEntities) return []; // This would require more sophisticated analysis in a real implementation // For now, return empty array as this needs dependency graph analysis return []; } /** * Get entities that this entity references * @param {Object} packageInstance - Package instance * @param {Object} entity - Current entity * @returns {Array<Object>} Referenced entities */ function getReferencedEntities(packageInstance, entity) { /** @type {any} */ const pkg = packageInstance; /** @type {any} */ const ent = entity; if (!pkg.allEntities || !ent.source) return []; // This would require parsing the source code for references // For now, return empty array as this needs AST analysis return []; } /** * Get similar entities (same type across other modules) * @param {Object} packageInstance - Package instance * @param {Object} entity - Current entity * @param {Object} [urlBuilder] - URL builder for base path support * @returns {Array<Object>} Similar entities */ function getSimilarEntities(packageInstance, entity, urlBuilder) { /** @type {any} */ const pkg = packageInstance; /** @type {any} */ const ent = entity; if (!pkg.allEntities) return []; const currentType = ent.entityType || "unknown"; const currentModulePath = ent.moduleId; return pkg.allEntities .filter(/** @param {any} e */ (e) => e.entityType === currentType) .filter(/** @param {any} e */ (e) => e.moduleId !== currentModulePath) .filter( /** @param {any} e */ (e) => !e.hasJSDocTag?.("private") && !e.name?.startsWith("_"), ) .slice(0, 5) // Limit for performance .map( /** @param {any} similarEntity */ (similarEntity) => { // Find the module for this entity const parentModule = pkg.modules.find( /** @param {any} m */ (m) => m.entities?.includes(similarEntity), ); return { name: similarEntity.name, type: similarEntity.entityType || "unknown", description: (similarEntity.description || "").slice(0, 100), moduleName: parentModule?.importPath?.split("/").pop() || "unknown", link: /** @type {any} */ (urlBuilder) && parentModule?.importPath ? /** @type {any} */ (urlBuilder).entityUrl( parentModule.importPath.split("/").pop(), similarEntity.name, ) : `/modules/${parentModule?.importPath?.split("/").pop()}/${similarEntity.name}/`, }; }, ); } /** * Get navigation data for entities within the current module * @param {Object} module - Module instance * @param {Object} currentEntity - Current entity * @param {Object} [urlBuilder] - URL builder for base path support * @returns {Array<Object>} Module entity navigation */ function getModuleEntityNavigation(module, currentEntity, urlBuilder) { /** @type {any} */ const mod = module; /** @type {any} */ const curr = currentEntity; if (!mod.entities) return []; return mod.entities .filter( /** @param {any} e */ (e) => !e.hasJSDocTag?.("private") && !e.name?.startsWith("_"), ) .map( /** @param {any} entity */ (entity) => ({ name: entity.name, type: entity.entityType || "unknown", isCurrent: entity.name === curr.name, link: urlBuilder ? /** @type {any} */ (urlBuilder).entityUrl( mod.importPath.split("/").pop(), entity.name, ) : `/modules/${mod.importPath.split("/").pop()}/${entity.name}/`, }), ); }