All files / src/pipeline/references process-xtrefs-data.js

0% Statements 0/74
0% Branches 0/57
0% Functions 0/10
0% Lines 0/72

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175                                                                                                                                                                                                                                                                                                                                                             
const fs = require('node:fs');
const path = require('node:path');
const { fetchAllTermsFromIndex } = require('./fetch-terms-from-index');
const { getPath } = require('../../../config/paths');
const Logger = require('../../utils/logger');
 
const CACHE_DIR = getPath('githubcache');
 
async function processXTrefsData(allXTrefs, GITHUB_API_TOKEN, outputPathJSON, outputPathJS, outputPathJSTimeStamped) {
    try {
        if (!fs.existsSync(CACHE_DIR)) {
            fs.mkdirSync(CACHE_DIR, { recursive: true });
        }
 
        allXTrefs.xtrefs = allXTrefs.xtrefs.filter(xtref => {
            if (!xtref.owner || !xtref.repo || !xtref.repoUrl) {
                Logger.error(`Removing incomplete reference: ${xtref.externalSpec}, ${xtref.term}`, {
                    context: 'External reference is missing required repository information',
                    hint: 'Verify external_specs configuration in specs.json includes github_repo_url for each entry',
                    details: `Missing: ${!xtref.owner ? 'owner' : ''} ${!xtref.repo ? 'repo' : ''} ${!xtref.repoUrl ? 'repoUrl' : ''}`
                });
                return false;
            }
            return true;
        });
 
        const xrefsByRepo = allXTrefs.xtrefs.reduce((groups, xtref) => {
            const repoKey = `${xtref.owner}/${xtref.repo}`;
            if (!groups[repoKey]) {
                groups[repoKey] = {
                    owner: xtref.owner,
                    repo: xtref.repo,
                    xtrefs: []
                };
            }
            groups[repoKey].xtrefs.push(xtref);
            return groups;
        }, {});
 
        Logger.highlight(`Grouped ${allXTrefs.xtrefs.length} terms into ${Object.keys(xrefsByRepo).length} repositories`);
 
        for (const repoKey of Object.keys(xrefsByRepo)) {
            const repoGroup = xrefsByRepo[repoKey];
            // Build a repository URL for logging. Prefer an explicit repoUrl from
            // an xtref, otherwise fall back to the canonical GitHub URL.
            const repoUrl = repoGroup.xtrefs[0]?.repoUrl || `https://github.com/${repoKey}`;
            Logger.process(`Processing repository: ${repoKey} (${repoGroup.xtrefs.length} terms) - ${repoUrl}`);
 
            const ghPageUrl = repoGroup.xtrefs[0]?.ghPageUrl;
            const allTermsData = await fetchAllTermsFromIndex(
                GITHUB_API_TOKEN,
                repoGroup.owner,
                repoGroup.repo,
                { ghPageUrl }
            );
 
            if (!allTermsData) {
                Logger.error(`Could not fetch terms from repository ${repoKey}`, {
                    context: `Failed to retrieve terminology from ${repoUrl}`,
                    hint: 'Ensure the repository exists, is accessible, and has published its spec. If it\'s private, set GITHUB_PAT environment variable',
                    details: `Repository: ${repoUrl}. Check if GitHub Pages is enabled or if the repo has a valid specs.json`
                });
                repoGroup.xtrefs.forEach(xtref => {
                    xtref.commitHash = 'not found';
                    xtref.content = 'This term was not found in the external repository.';
                    xtref.avatarUrl = null;
                });
                continue;
            }
 
            for (const xtref of repoGroup.xtrefs) {
                const foundTerm = allTermsData.terms.find(
                    t => t.term.toLowerCase() === xtref.term.toLowerCase()
                );
 
                if (foundTerm) {
                    xtref.commitHash = allTermsData.sha;
                    xtref.content = foundTerm.definition;
                    xtref.avatarUrl = allTermsData.avatarUrl;
                    // Copy the classes array from the foundTerm to identify if this is a local or external term.
                    // This helps determine if a tref to an external resource is itself a tref (term-external).
                    xtref.classes = foundTerm.classes || [];
 
                    // Check if this is a tref to an external tref (nested tref)
                    // A term with 'term-external' class means it's transcluded from another spec
                    const isExternalTref = foundTerm.classes && foundTerm.classes.includes('term-external');
                    const isTref = xtref.sourceFiles && xtref.sourceFiles.some(sf => sf.type === 'tref');
                    const isXref = xtref.sourceFiles && xtref.sourceFiles.some(sf => sf.type === 'xref');
 
                    if (isExternalTref && isTref) {
                        // Build a readable list of source files for the error message
                        const sourceFilesList = xtref.sourceFile
                            ? xtref.sourceFile
                            : (xtref.sourceFiles || []).map(sf => sf.file).join(', ');
 
                        // Construct the external repository URL
                        const externalRepoUrl = xtref.ghPageUrl || xtref.repoUrl || `https://github.com/${xtref.owner}/${xtref.repo}`;
 
                        Logger.error(`NESTED TREF DETECTED: Term "${xtref.term}" in ${xtref.externalSpec}`, {
                            context: `Origin: ${sourceFilesList} - This term is itself a tref transcluded from another spec`,
                            hint: 'Avoid chaining trefs (tref → tref). Either reference the original source spec directly, or define the term locally',
                            details: `Repository: ${externalRepoUrl}. Nested trefs create complex dependency chains`
                        });
                    }
 
                    if (isExternalTref && isXref) {
                        // Build a readable list of source files for the warning message
                        const sourceFilesList = xtref.sourceFile
                            ? xtref.sourceFile
                            : (xtref.sourceFiles || []).map(sf => sf.file).join(', ');
 
                        // Construct the external repository URL
                        const externalRepoUrl = xtref.ghPageUrl || xtref.repoUrl || `https://github.com/${xtref.owner}/${xtref.repo}`;
 
                        Logger.error(`NESTED XREF DETECTED: Term "${xtref.term}" in ${xtref.externalSpec}`, {
                            context: `Origin: ${sourceFilesList} - This xref points to a term that is already transcluded elsewhere`,
                            hint: 'Use [[xref]] only for terms directly defined in the external spec. For nested refs, reference the original source',
                            details: `Repository: ${externalRepoUrl}. This creates a chain of external references`
                        });
                    }
 
                    Logger.success(`Match found for term: ${xtref.term} in ${xtref.externalSpec}`);
                } else {
                    xtref.commitHash = 'not found';
                    xtref.content = 'This term was not found in the external repository.';
                    xtref.avatarUrl = null;
 
                    // Build a readable list of source files for the error message.
                    // Two possible data structures exist:
                    // 1. xtref.sourceFile is a STRING like "primitive.md"
                    // 2. xtref.sourceFiles is an ARRAY OF OBJECTS like [{file: "primitive.md", type: "xref"}]
                    //
                    // The ternary operator works as follows:
                    // - If xtref.sourceFile exists (legacy case) → use it directly (it's already a string)
                    // - Otherwise → extract file names from the sourceFiles array:
                    //   - .map(sf => sf.file) extracts just the filename from each object
                    //   - .join(', ') combines them into a comma-separated string
                    const sourceFilesList = xtref.sourceFile
                        ? xtref.sourceFile
                        : (xtref.sourceFiles || []).map(sf => sf.file).join(', ');
 
                    // Prefer an explicit repo URL if provided on the xtref, otherwise
                    // build a standard GitHub URL from the owner/repo.
                    const githubUrl = xtref.repoUrl || `https://github.com/${repoKey}`;
 
                    Logger.error(`No match found for term: ${xtref.term} in ${xtref.externalSpec}`, {
                        context: `Origin: ${sourceFilesList} - Term not found in external repository`,
                        hint: 'Check if the term exists in the external spec. Verify spelling, ensure the external spec has published, and confirm the term is in their terminology section',
                        details: `Repository: ${githubUrl}. The term may have been renamed or removed`
                    });
                }
            }
 
            Logger.success(`Finished processing repository: ${repoKey} (${repoUrl})`);
            Logger.separator();
        }
 
        const allXTrefsStr = JSON.stringify(allXTrefs, null, 2);
        fs.writeFileSync(outputPathJSON, allXTrefsStr, 'utf8');
        const jsPayload = `const allXTrefs = ${allXTrefsStr};`;
        fs.writeFileSync(outputPathJS, jsPayload, 'utf8');
        fs.writeFileSync(outputPathJSTimeStamped, jsPayload, 'utf8');
    } catch (error) {
        Logger.error('An error occurred during xtrefs processing', {
            context: 'Failed while processing external references and fetching terms',
            hint: 'Check your internet connection, verify GITHUB_PAT is set if needed, and ensure specs.json external_specs configuration is correct',
            details: error.message
        });
    }
}
 
module.exports = {
    processXTrefsData
};