import * as chalk from 'chalk'; import { readJsonSync } from 'fs-extra'; import { resolve } from 'path'; import { convertToDocumentMetadata, createDocumentMetadata, DocumentMetadata, } from '@nx/nx-dev/models-document'; import { MenuItem } from '@nx/nx-dev/models-menu'; import { PackageMetadata, ProcessedPackageMetadata, } from '@nx/nx-dev/models-package'; import { generateIndexMarkdownFile, generateJsonFile } from '../utils'; import { convertToDictionary } from './utils-generator/convert-to-dictionary'; interface DocumentSection { name: string; content: Partial[]; prefix: string; } interface DocumentManifest { id: string; records: Record; } interface PackageManifest { id: string; records: Record; } type Manifest = DocumentManifest | PackageManifest; const isDocument = ( e: DocumentMetadata | ProcessedPackageMetadata ): e is DocumentMetadata => !('packageName' in e); const isPackage = ( e: DocumentMetadata | ProcessedPackageMetadata ): e is ProcessedPackageMetadata => 'packageName' in e; export function generateManifests(workspace: string): Promise { console.log(`${chalk.blue('i')} Generating Manifests`); const documentationPath = resolve(workspace, 'docs'); const generatedDocumentationPath = resolve(documentationPath, 'generated'); const targetFolder: string = resolve(generatedDocumentationPath, 'manifests'); const documents: Partial[] = readJsonSync( `${documentationPath}/map.json`, { encoding: 'utf8', } ).content; const packages: PackageMetadata[] = readJsonSync( `${generatedDocumentationPath}/packages-metadata.json`, { encoding: 'utf8', } ); /** * We are starting by selecting what section of the map.json we want to work with. * @type {DocumentSection[]} */ const documentSections = createDocumentSections(documents); /** * Once we have the DocumentSections we can start creating our DocumentManifests. * @type {Manifest[]} */ const manifests = getDocumentManifests(documentSections); /** * Packages are not Documents and need to be handled in a custom way. * @type {{id: string, records: Record}} */ const packagesManifest = createPackagesManifest(packages); /** * Add the packages manifest to the manifest collection for simplicity. */ manifests.push(packagesManifest); /** * We can easily infer all Documents menus but need a custom way to handle them * for the packages. * @type {{id: string, menu: MenuItem[]}[]} */ const menus: { id: string; menu: MenuItem[]; }[] = getDocumentMenus( manifests.filter((m): m is DocumentManifest => isDocument(m.records[Object.keys(m.records)[0]]) ) ); /** * Creating packages menu with custom package logic. * @type {{id: string, menu: MenuItem[]}} */ const packagesMenu: { id: string; menu: MenuItem[]; } = createPackagesMenu(packagesManifest); /** * Add the packages menu to the main menu collection for simplicity. */ menus.push(packagesMenu); /** * We can easily get all associated existing tags from each manifest. * @type {Record} */ const tags: Record< string, { description: string; file: string; id: string; name: string; path: string; }[] > = generateTags(manifests); /** * We can now create manifest files. */ const fileGenerationPromises: Promise[] = []; manifests.forEach((manifest) => fileGenerationPromises.push( generateJsonFile( resolve(targetFolder + `/${manifest.id}.json`), manifest.records ) ) ); fileGenerationPromises.push( generateJsonFile(resolve(targetFolder, `tags.json`), tags) ); fileGenerationPromises.push( generateJsonFile(resolve(targetFolder, `menus.json`), menus) ); fileGenerationPromises.push( generateIndexMarkdownFile( resolve(documentationPath, `shared`, `reference`, `sitemap.md`), menus ) ); return Promise.all(fileGenerationPromises); } function generateTags(manifests: Manifest[]) { const tags: Record< string, { description: string; file: string; id: string; name: string; path: string; }[] > = {}; manifests.map((manifest) => { for (let key in manifest.records) { const item: DocumentMetadata | ProcessedPackageMetadata = manifest.records[key]; if (isDocument(item)) item.tags.forEach((t) => { const tagData = { description: item.description, file: item.file, id: item.id, name: item.name, path: item.path, }; !tags[t] ? (tags[t] = [tagData]) : tags[t].push(tagData); }); if (isPackage(item)) Object.values(item.documents).forEach( (documentMetadata: DocumentMetadata) => { documentMetadata.tags.forEach((t: string) => { const filePath = documentMetadata.file.startsWith( 'generated/packages' ) ? documentMetadata.file : ['generated', 'packages', documentMetadata.file].join('/'); const tagData = { description: documentMetadata.description, file: filePath, id: documentMetadata.id, name: documentMetadata.name, path: documentMetadata.path, }; !tags[t] ? (tags[t] = [tagData]) : tags[t].push(tagData); }); } ); } }); return tags; } function createPackagesMenu(packages: PackageManifest): { id: string; menu: MenuItem[]; } { const packagesMenu: MenuItem[] = Object.values(packages.records).map((p) => { const item: MenuItem = { id: p.name, path: '/nx-api/' + p.name, name: p.name, children: [], isExternal: false, disableCollapsible: false, }; if (!!Object.values(p.documents).length) { // Might need to remove the path set in the "additional api resources" items item.children.push({ id: 'documents', path: '/' + ['nx-api', p.name, 'documents'].join('/'), name: 'documents', children: Object.values(p.documents).map((d) => menuItemRecurseOperations(d) ), isExternal: false, disableCollapsible: false, }); } if (!!Object.values(p.executors).length) { item.children.push({ id: 'executors', path: '/' + ['nx-api', p.name, 'executors'].join('/'), name: 'executors', children: Object.values(p.executors).map((e) => ({ id: e.name, path: '/' + ['nx-api', p.name, 'executors', e.name].join('/'), name: e.name, children: [], isExternal: false, disableCollapsible: false, })), isExternal: false, disableCollapsible: false, }); } if (!!Object.values(p.generators).length) { item.children.push({ id: 'generators', path: '/' + ['nx-api', p.name, 'generators'].join('/'), name: 'generators', children: Object.values(p.generators).map((g) => ({ id: g.name, path: '/' + ['nx-api', p.name, 'generators', g.name].join('/'), name: g.name, children: [], isExternal: false, disableCollapsible: false, })), isExternal: false, disableCollapsible: false, }); } return item; }); return { id: 'nx-api', menu: packagesMenu }; } function getDocumentMenus(manifests: DocumentManifest[]): { id: string; menu: MenuItem[]; }[] { return manifests.map((record) => ({ id: record.id, menu: Object.values(record.records) .map((item: any) => convertToDocumentMetadata(item)) .map((item: DocumentMetadata) => menuItemRecurseOperations(item)), })); } function createPackagesManifest(packages: PackageMetadata[]): { id: string; records: Record; } { const packagesManifest: { id: string; records: Record; } = { id: 'nx-api', records: {} }; packages.forEach((p) => { packagesManifest.records[p.name] = { githubRoot: p.githubRoot, name: p.name, packageName: p.packageName, description: p.description, documents: convertToDictionary( p.documents.map((d) => documentRecurseOperations( d, createDocumentMetadata({ id: p.name, path: 'nx-api/' }) ) ), 'path' ), root: p.root, source: p.source, executors: convertToDictionary( p.executors.map((e) => ({ ...e, path: generatePath({ id: e.name, path: e.path }, 'nx-api'), })), 'path' ), generators: convertToDictionary( p.generators.map((g) => ({ ...g, path: generatePath({ id: g.name, path: g.path }, 'nx-api'), })), 'path' ), path: generatePath({ id: p.name, path: '' }, 'nx-api'), }; }); return packagesManifest; } function getDocumentManifests(sections: DocumentSection[]): Manifest[] { return sections.map((section) => { const records: Record = {}; section.content .map((item: any) => convertToDocumentMetadata(item)) .map((item: DocumentMetadata) => documentRecurseOperations( item, createDocumentMetadata({ id: section.name, path: section.prefix }) ) ) .forEach((item: DocumentMetadata) => { populateDictionary(item, records); }); return { id: section.name, records, }; }); } function createDocumentSections( documents: Partial[] ): DocumentSection[] { return [ { name: 'nx', content: documents.find((x) => x.id === 'nx-documentation')! .itemList as Partial[], prefix: '', }, { name: 'extending-nx', content: documents.find((x) => x.id === 'extending-nx')! .itemList as Partial[], prefix: 'extending-nx', }, { name: 'ci', content: documents.find((x) => x.id === 'ci')! .itemList as Partial[], prefix: 'ci', }, ]; } function generatePath( item: { path: string; id: string }, prefix: string = '' ): string { const isLinkExternal: (p: string) => boolean = (p: string) => p.startsWith('http'); const isLinkAbsolute: (p: string) => boolean = (p: string) => p.startsWith('/'); if (!item.path) return '/' + [...prefix.split('/'), item.id].filter(Boolean).join('/'); if (isLinkAbsolute(item.path) || isLinkExternal(item.path)) return item.path; return ( '/' + [...prefix.split('/'), ...item.path.split('/')].filter(Boolean).join('/') ); } /** * Handle data interpolation for all items and their children. * @param target * @param parent */ function documentRecurseOperations( target: DocumentMetadata, parent: DocumentMetadata ): DocumentMetadata { const document: DocumentMetadata = structuredClone(target); /** * Calculate `path` */ if (!!parent) document.path = generatePath(target, parent.path); else document.path = generatePath(document); /** * Calculate `isExternal` */ if (document['isExternal'] === undefined) { document.isExternal = document.path.startsWith('http'); } if (!!target.itemList.length) { document.itemList = target.itemList.map((i) => documentRecurseOperations(i, document) ); } return document; } function populateDictionary( document: DocumentMetadata, dictionary: Record ) { if (document.path.startsWith('http')) return; dictionary[document.path] = document; document.itemList.forEach((item: DocumentMetadata) => populateDictionary(item, dictionary) ); } // Creates menus dictionary mapping function menuItemRecurseOperations(target: DocumentMetadata): MenuItem { const menuItem: MenuItem = { name: target.name, path: target.path, id: target.id, isExternal: target.isExternal, children: [], disableCollapsible: false, }; /** * Calculate `isExternal` */ if (menuItem['isExternal'] === undefined) { menuItem.isExternal = menuItem.path.startsWith('http'); } if (!!target.itemList.length) { menuItem.children = target.itemList.map((i) => menuItemRecurseOperations(i) ); } return menuItem; }