You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
packer/website/components/remote-plugin-docs/utils/resolve-nav-data.js

228 lines
8.4 KiB

const fs = require('fs')
const path = require('path')
const grayMatter = require('gray-matter')
const fetchPluginDocs = require('./fetch-plugin-docs')
const fetchDevPluginDocs = require('./fetch-dev-plugin-docs')
/**
* Resolves nav-data from file with
* resolution of remote plugin docs entries
*
* @param {string} navDataFile path to the nav-data.json file, relative to the cwd. Example: "data/docs-nav-data.json".
* @param {object} options optional configuration object
* @param {string} options.remotePluginsFile path to a remote-plugins.json file, relative to the cwd. Example: "data/docs-remote-plugins.json".
* @returns {Promise<array>} the resolved navData. This includes NavBranch nodes pulled from remote plugin repositories, as well as filePath properties on all local NavLeaf nodes, and remoteFile properties on all NavLeafRemote nodes.
*/
async function resolveNavDataWithRemotePlugins(navDataFile, options = {}) {
const { remotePluginsFile, currentPath } = options
const navDataPath = path.join(process.cwd(), navDataFile)
let navData = JSON.parse(fs.readFileSync(navDataPath, 'utf8'))
return await appendRemotePluginsNavData(
remotePluginsFile,
navData,
currentPath
)
}
async function appendRemotePluginsNavData(
remotePluginsFile,
navData,
currentPath
) {
// Read in and parse the plugin configuration JSON
const remotePluginsPath = path.join(process.cwd(), remotePluginsFile)
const pluginEntries = JSON.parse(fs.readFileSync(remotePluginsPath, 'utf-8'))
// Add navData for each plugin's component.
// Note that leaf nodes include a remoteFile property object with the full MDX fileString
const pluginEntriesWithDocs = await Promise.all(
pluginEntries.map(
async (entry) => await resolvePluginEntryDocs(entry, currentPath)
)
)
const titleMap = {
builders: 'Builders',
provisioners: 'Provisioners',
'post-processors': 'Post-Processors',
datasources: 'Data Sources',
}
return navData.concat(
pluginEntriesWithDocs.map((entry) => {
return {
title: entry.title,
routes: Object.entries(entry.components).map(
([type, componentList]) => {
return {
title: titleMap[type],
// Flat map to avoid ┐
// > Proxmox │
// > Builders │
// > Proxmox <---┘
// > Overview
// > Clone
// > ISO
routes: componentList.flatMap((c) => {
if ('path' in c) {
return c
} else if ('routes' in c) {
return c.routes
}
}),
}
}
),
}
})
)
}
// Fetch remote plugin docs .mdx files, and
// transform each plugin's array of .mdx files into navData.
// Organize this navData by component, add it to the plugin config entry,
// and return the modified entry.
//
// Note that navData leaf nodes have a special remoteFile property,
// which contains { filePath, fileString } data for the remote
// plugin doc .mdx file
async function resolvePluginEntryDocs(pluginConfigEntry, currentPath) {
const {
title,
path: slug,
repo,
version,
pluginTier,
isHcpPackerReady = false,
sourceBranch = 'main',
zipFile = '',
} = pluginConfigEntry
// Determine the pluginTier, which can be set manually,
// or will be automatically set based on repo ownership
const pluginOwner = repo.split('/')[0]
const parsedPluginTier =
pluginTier || (pluginOwner === 'hashicorp' ? 'official' : 'community')
// Fetch the MDX files for the plugin entry
var docsMdxFiles
if (zipFile !== '') {
docsMdxFiles = await fetchDevPluginDocs(zipFile)
} else {
docsMdxFiles = await fetchPluginDocs({ repo, tag: version })
}
// We construct a special kind of "NavLeaf" node, with a remoteFile property,
// consisting of a { filePath, fileString, sourceUrl }, where:
// - filePath is the path to the source file in the source repo
// - fileString is a string representing the file source
// - sourceUrl is a link to the original file in the source repo
// We also add pluginData, which is used to add badges
// such as the plugin's tier when rendering the page.
const navNodes = docsMdxFiles.map((mdxFile) => {
const { filePath, fileString } = mdxFile
// Process into a NavLeaf, with a remoteFile attribute
const dirs = path.dirname(filePath).split('/')
const dirUrl = dirs.slice(2).join('/')
const basename = path.basename(filePath, path.extname(filePath))
// build urlPath
// note that this will be prefixed to get to our final path
const isIndexFile = basename === 'index'
const urlPath = isIndexFile ? dirUrl : path.join(dirUrl, basename)
// parse title, either from frontmatter or file name
const { data: frontmatter } = grayMatter(fileString)
const { nav_title, sidebar_title } = frontmatter
const title = nav_title || sidebar_title || basename
// construct sourceUrl (used for "Edit this page" link)
const sourceUrl = `https://github.com/${repo}/blob/${sourceBranch}/${filePath}`
// Construct and return a NavLeafRemote node
return {
title,
path: urlPath,
remoteFile: { filePath, fileString, sourceUrl },
pluginData: {
repo,
tier: parsedPluginTier,
isHcpPackerReady,
version,
},
}
})
//
navNodes.sort((a, b) => {
// ensure casing does not affect ordering
const aTitle = a.title.toLowerCase()
const bTitle = b.title.toLowerCase()
// (exception: "Overview" comes first)
if (aTitle === 'overview') return -1
if (bTitle === 'overview') return 1
return aTitle < bTitle ? -1 : aTitle > bTitle ? 1 : 0
})
//
const navNodesByComponent = navNodes.reduce((acc, navLeaf) => {
const componentType = navLeaf.remoteFile.filePath.split('/')[1]
if (!acc[componentType]) acc[componentType] = []
acc[componentType].push(navLeaf)
return acc
}, {})
//
const components = Object.keys(navNodesByComponent).map((type) => {
// Plugins many not contain every component type,
// we return null if this is the case
const rawNavNodes = navNodesByComponent[type]
if (!rawNavNodes) return null
// Avoid unnecessary nesting if there's only a single doc file
const navData = normalizeNavNodes(title, rawNavNodes)
// Prefix paths to fit into broader docs nav-data
const pathPrefix = path.join(type, slug)
const withPrefixedPaths = visitNavLeaves(navData, (n) => {
const prefixedPath = path.join(pathPrefix, n.path)
return { ...n, path: prefixedPath }
})
// If currentPath is provided, then remove the remoteFile
// from all nodes except the currentPath. This ensures we deliver
// only a single fileString in our getStaticProps JSON.
// Without this optimization, we would send all fileStrings
// for all NavLeafRemote nodes
const withOptimizedFileStrings = visitNavLeaves(withPrefixedPaths, (n) => {
if (!n.remoteFile) return n
const noCurrentPath = typeof currentPath === 'undefined'
const isPathMatch = currentPath === n.path
if (noCurrentPath || isPathMatch) return n
const { filePath } = n.remoteFile
return { ...n, remoteFile: { filePath } }
})
// Return the component, with processed navData
return { type, navData: withOptimizedFileStrings }
})
const componentsObj = components.reduce((acc, component) => {
if (!component) return acc
acc[component.type] = component.navData
return acc
}, {})
return { ...pluginConfigEntry, components: componentsObj }
}
// For components with a single doc file, transform so that
// a single leaf node renders, rather than a nav branch
function normalizeNavNodes(pluginName, routes) {
const isSingleLeaf =
routes.length === 1 && typeof routes[0].path !== 'undefined'
const navData = isSingleLeaf
? [{ ...routes[0], path: '' }]
: [{ title: pluginName, routes }]
return navData
}
// Traverse a clone of the given navData,
// modifying any NavLeaf nodes with the provided visitFn
function visitNavLeaves(navData, visitFn) {
return navData.slice().map((navNode) => {
if (typeof navNode.path !== 'undefined') {
return visitFn(navNode)
}
if (navNode.routes) {
return { ...navNode, routes: visitNavLeaves(navNode.routes, visitFn) }
}
return navNode
})
}
module.exports = resolveNavDataWithRemotePlugins