@raven-js/glean
Version:
Glean documentation gold from your codebase - JSDoc parsing, validation, and beautiful doc generation
279 lines (244 loc) • 8.88 kB
JavaScript
/**
* @author Anonyfox <max@anonyfox.com>
* @license MIT
* @see {@link https://github.com/Anonyfox/ravenjs}
* @see {@link https://ravenjs.dev}
* @see {@link https://anonyfox.com}
*/
/**
* Module overview data extraction for /modules/{moduleName}/ route
*
* Surgical data transformation from Package instance to structured data
* optimized for individual module presentation. Follows WEBAPP.md specification
* for step-by-step module overview extraction with entity organization.
*/
/**
* Extract module overview data for module page rendering with entities and navigation.
*
* Locates target module, extracts entities, and builds navigation context for
* detailed module documentation pages.
*
* @param {import('../../extract/models/package.js').Package} packageInstance - Package data
* @param {string} moduleName - Module name from URL parameter
* @returns {Object} Structured data for module overview template
* @throws {Error} When module not found or invalid parameters
*
* @example
* // Extract data for utils module
* const moduleData = extractModuleOverviewData(packageInstance, 'utils');
* console.log(moduleData.organizedEntities, moduleData.stats.totalEntities);
*
* @example
* // Check for module content
* const moduleData = extractModuleOverviewData(packageInstance, 'core');
* if (moduleData.hasEntities) console.log('Entities available');
* if (moduleData.module.hasReadme) console.log('Module README available');
*/
export function extractModuleOverviewData(
packageInstance,
moduleName,
options = {},
) {
const { urlBuilder } = /** @type {any} */ (options);
if (!packageInstance || typeof packageInstance !== "object") {
throw new Error("Package instance is required");
}
if (!moduleName || typeof moduleName !== "string") {
throw new Error("Module name is required");
}
// STEP 1: Find the target module
const packageName = packageInstance.name || "";
let module = null;
// Try exact import path match first (for default modules like "package-name")
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}'`,
);
}
// STEP 2: Module metadata
const moduleData = {
importPath: module.importPath,
isDefault: module.isDefault,
name: moduleName,
fullName: module.importPath,
// Module description from @file tag
description: module.description || "",
// Documentation content
readme: module.readme || "",
hasReadme: Boolean(module.readme),
// Entity organization
publicEntityGroups: module.publicEntityGroups || {},
entityCount: module.publicEntityCount || 0,
availableTypes: module.availableEntityTypes || [],
};
// STEP 3: Entity listings organized by type (including re-exported entities)
/** @type {Object<string, Array<Object>>} */
const organizedEntities = {};
// First, add all regular entities from this module
Object.entries(moduleData.publicEntityGroups).forEach(([type, entities]) => {
organizedEntities[type] = entities
.filter((entity) => entity.entityType !== "reexport") // Exclude re-export references
.map((entity) => ({
name: entity.name,
description: entity.description || "",
location: entity.location || null,
// Quick metadata for listing
hasParams: entity.hasJSDocTag?.("param") || false,
hasReturns: entity.hasJSDocTag?.("returns") || false,
hasExamples: entity.hasJSDocTag?.("example") || false,
isDeprecated: entity.hasJSDocTag?.("deprecated") || false,
// Direct link to this module's entity
link: urlBuilder
? /** @type {any} */ (urlBuilder).entityUrl(moduleName, entity.name)
: `/modules/${moduleName}/${entity.name}/`,
isReexport: false,
}));
});
// Then, add re-exported entities from other modules
for (const reexport of module.reexports || []) {
const sourceModuleName = /** @type {{sourceModule: string}} */ (
reexport
).sourceModule
.replace("./", "")
.replace(".js", "");
const sourceModule = packageInstance.modules.find(
(m) =>
m.importPath.endsWith(sourceModuleName) ||
m.importPath.split("/").pop() === sourceModuleName,
);
if (sourceModule) {
// Find the original entity in the source module
const originalEntity = sourceModule.entities.find(
(e) =>
e.name ===
/** @type {{originalName: string}} */ (reexport).originalName,
);
if (originalEntity) {
const entityType = originalEntity.entityType || "function";
if (!organizedEntities[entityType]) {
organizedEntities[entityType] = [];
}
// Add the original entity with link to its original location
organizedEntities[entityType].push({
name: originalEntity.name,
description: originalEntity.description || "",
location: originalEntity.location || null,
// Use original entity's metadata
hasParams: originalEntity.hasJSDocTag?.("param") || false,
hasReturns: originalEntity.hasJSDocTag?.("returns") || false,
hasExamples: originalEntity.hasJSDocTag?.("example") || false,
isDeprecated: originalEntity.hasJSDocTag?.("deprecated") || false,
// Link to the original entity's location
link: urlBuilder
? /** @type {any} */ (urlBuilder).entityUrl(
sourceModuleName,
originalEntity.name,
)
: `/modules/${sourceModuleName}/${originalEntity.name}/`,
isReexport: true,
originalModule: sourceModuleName,
});
}
}
}
// STEP 4: Navigation context
const navigationContext = {
packageName: packageName,
currentModule: moduleName,
allModules: packageInstance.modules.map((m) => ({
name: m.importPath.split("/").pop() || "index",
fullImportPath: m.importPath,
isCurrent: m.importPath === module.importPath,
isDefault: m.isDefault,
link: urlBuilder
? /** @type {any} */ (urlBuilder).moduleUrl(
m.importPath.split("/").pop(),
)
: `/modules/${m.importPath.split("/").pop()}/`,
entityCount: m.publicEntityCount || 0,
})),
};
// Calculate statistics
const entityStats = {
totalEntities: moduleData.entityCount,
entitiesByType: Object.entries(organizedEntities).reduce(
(acc, [type, entities]) => {
acc[type] = entities.length;
return acc;
},
/** @type {Object<string, number>} */ ({}),
),
deprecatedCount: Object.values(organizedEntities)
.flat()
.filter(/** @param {any} e */ (e) => e.isDeprecated).length,
withExamplesCount: Object.values(organizedEntities)
.flat()
.filter(/** @param {any} e */ (e) => e.hasExamples).length,
};
return {
// Module information
module: moduleData,
// Organized entities by type
organizedEntities,
// Navigation context
navigation: navigationContext,
// Statistics
stats: entityStats,
// Package context
packageName: packageName,
packageDescription: packageInstance.description || "",
// Attribution data
moduleEntities: module.entities || [], // Raw entity instances for attribution
packageMetadata: extractPackageMetadata(packageInstance), // Package metadata
allModules: packageInstance.modules, // Pass all modules for re-export tracing
generationTimestamp: new Date().toISOString(), // Generation timestamp for footer
// Meta information for template
hasEntities: moduleData.entityCount > 0,
hasMultipleTypes: moduleData.availableTypes.length > 1,
hasDeprecatedEntities: entityStats.deprecatedCount > 0,
hasExampleEntities: entityStats.withExamplesCount > 0,
};
}
/**
* 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;
}