diff --git a/src/resolvers/md2md.ts b/src/resolvers/md2md.ts index ebde286a..368c12f5 100644 --- a/src/resolvers/md2md.ts +++ b/src/resolvers/md2md.ts @@ -1,4 +1,4 @@ -import {existsSync, readFileSync, writeFileSync} from 'fs'; +import {readFileSync, writeFileSync} from 'fs'; import {basename, dirname, extname, join, resolve} from 'path'; import shell from 'shelljs'; import log from '@diplodoc/transform/lib/log'; @@ -57,11 +57,7 @@ export async function resolveMd2Md(options: ResolveMd2MdOptions): Promise )}`; } - const changesPath = join(outputDir, `changes-${changesName}.json`); - - if (existsSync(changesPath)) { - throw new Error(`Changelog ${changesPath} already exists!`); - } + const changesPath = join(outputDir, `__changes-${changesName}.json`); writeFileSync( changesPath, diff --git a/src/services/tocs.ts b/src/services/tocs.ts index e7769f94..f66764d5 100644 --- a/src/services/tocs.ts +++ b/src/services/tocs.ts @@ -17,11 +17,13 @@ import {IncludersError, applyIncluders} from './includers'; export interface TocServiceData { storage: Map; + tocs: Map; navigationPaths: string[]; includedTocPaths: Set; } const storage: TocServiceData['storage'] = new Map(); +const tocs: TocServiceData['tocs'] = new Map(); let navigationPaths: TocServiceData['navigationPaths'] = []; const includedTocPaths: TocServiceData['includedTocPaths'] = new Set(); const tocFileCopyMap = new Map(); @@ -76,6 +78,9 @@ async function add(path: string) { /* Store parsed toc for .md output format */ storage.set(path, parsedToc); + /* save toc in distinct set, storage includes .md files too */ + tocs.set(path, parsedToc); + /* Store path to toc file to handle relative paths in navigation */ parsedToc.base = pathToDir; @@ -411,6 +416,10 @@ function getCopyFileMap() { return tocFileCopyMap; } +function getAllTocs() { + return [...tocs.keys()]; +} + export default { add, getForPath, @@ -419,4 +428,5 @@ export default { getIncludedTocPaths, setNavigationPaths, getCopyFileMap, + getAllTocs, }; diff --git a/src/steps/processChangelogs.ts b/src/steps/processChangelogs.ts index 13eaaf53..dbfcce0d 100644 --- a/src/steps/processChangelogs.ts +++ b/src/steps/processChangelogs.ts @@ -1,31 +1,99 @@ import {glob} from '../utils/glob'; import {join} from 'node:path'; -import {ArgvService} from '../services'; +import {ArgvService, TocService} from '../services'; import {readFile, unlink, writeFile} from 'node:fs/promises'; import {Lang} from '../constants'; -type Language = string; type MergedChangelogs = { - [language: Language]: Record>; + [pathToProjectToc: string]: { + [language: string]: { + [pathToFile: string]: unknown; + }; + }; +}; + +type ParsedPath = { + language: string; + path: string; + level: number; +}; + +const hasSingleLanguage = () => { + const {langs} = ArgvService.getConfig(); + return typeof langs === 'undefined' || langs.length === 1; +}; + +const project = (path: string): ParsedPath => { + const parts = path.split('/').slice(0, -1); + const language = hasSingleLanguage() ? Lang.EN : parts.shift()!; + const level = parts.length; + + return { + path: '/' + parts.join('/'), + language, + level, + }; +}; + +const file = (path: string): ParsedPath => { + const parts = path.split('/'); + const language = hasSingleLanguage() ? Lang.EN : parts.shift()!; + + return { + path: '/' + parts.join('/'), + language, + level: -1, + }; +}; + +type Project = { + path: string; + languages: string[]; + level: number; }; /* - { - "ru": { - "/": { - "12314": - }, - "/plugins": { - "213312": - } - } - } + This function collects all the project's subprojects and sorts them by depth level. + This is done to make it easier to find which toc.yaml file is responsible + for the necessary changes file: the earlier the project is in the list, the deeper it is. + The first project whose path to toc.yaml shares a common prefix with the path to changes + will be considered responsible for it. + */ +const uniqueProjects = (tocs: string[]): [string, Project][] => { + const projects = tocs.map(project); + const composed = projects.reduce( + (acc, curr) => { + if (acc[curr.path]) { + acc[curr.path].languages.push(curr.language); + + return acc; + } + + acc[curr.path] = { + languages: [curr.language], + path: curr.path, + level: curr.level, + }; + + return acc; + }, + {} as Record, + ); + + const entries = Object.entries(composed).sort((a, b) => { + return b[1].level - a[1].level; + }); + + return entries; +}; export async function processChangelogs() { - const {output: outputFolderPath, langs} = ArgvService.getConfig(); + const {output: outputFolderPath} = ArgvService.getConfig(); + const tocs = TocService.getAllTocs(); + const projects = uniqueProjects(tocs); - const result = await glob('**/**/changes-*', { + const result = await glob('**/**/__changes-*.json', { cwd: outputFolderPath, }); @@ -48,25 +116,23 @@ export async function processChangelogs() { ); changes.forEach(([path, value]) => { - if (!langs) { - path = `${Lang.EN}/${path}`; - } - - const [lang, ...rest] = path.split('/'); - const [, hash] = rest.pop()!.split(/[-.]/); + const {language, path: pathToFile} = file(path); - const fullPath = '/' + rest.join('/'); + const project = projects.find(([pathToProject, project]) => { + return pathToFile.startsWith(pathToProject) && project.languages.includes(language); + }); - if (!merged[lang]) { - merged[lang] = {}; + if (!project) { + return; } - if (!merged[lang][fullPath]) { - merged[lang][fullPath] = {}; - } + const [projectPath] = project; + + merged[projectPath] ??= {}; + merged[projectPath][language] ??= {}; - Object.assign(merged[lang][fullPath], { - [hash]: value, + Object.assign(merged[projectPath][language], { + [path]: value, }); });