From 341308c58247ff932eb12333cdab4649b7162073 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 18 Mar 2021 12:21:53 -0400 Subject: [PATCH 01/19] website: add refactored remote-plugin-docs utilities --- .../utils/fetch-plugin-docs.js | 61 +++++ .../utils/parse-docs-zip.js | 44 ++++ .../utils/parse-source-zip.js | 49 ++++ .../utils/resolve-nav-data.js | 218 ++++++++++++++++++ .../utils/validate-plugin-docs-files.js | 46 ++++ 5 files changed, 418 insertions(+) create mode 100644 website/components/remote-plugin-docs/utils/fetch-plugin-docs.js create mode 100644 website/components/remote-plugin-docs/utils/parse-docs-zip.js create mode 100644 website/components/remote-plugin-docs/utils/parse-source-zip.js create mode 100644 website/components/remote-plugin-docs/utils/resolve-nav-data.js create mode 100644 website/components/remote-plugin-docs/utils/validate-plugin-docs-files.js diff --git a/website/components/remote-plugin-docs/utils/fetch-plugin-docs.js b/website/components/remote-plugin-docs/utils/fetch-plugin-docs.js new file mode 100644 index 000000000..d02930ca2 --- /dev/null +++ b/website/components/remote-plugin-docs/utils/fetch-plugin-docs.js @@ -0,0 +1,61 @@ +const fetch = require('isomorphic-unfetch') +const parseSourceZip = require('./parse-source-zip') +const parseDocsZip = require('./parse-docs-zip') + +// Given a repo and tag, +// +// return [null, docsMdxFiles] if docs files +// are successfully fetched and valid, +// where docsMdxFiles is an array of { filePath, fileString } items. +// +// otherwise, return [err, null] +// where err is an error message describing whether the +// docs files were missing or invalid, with a path to resolution +async function fetchDocsFiles({ repo, tag }) { + // If there's a docs.zip asset, we'll prefer that + const docsZipUrl = `https://github.com/${repo}/releases/download/${tag}/docs.zip` + const docsZipResponse = await fetch(docsZipUrl, { method: 'GET' }) + const hasDocsZip = docsZipResponse.status === 200 + // Note: early return! + if (hasDocsZip) return await parseDocsZip(docsZipResponse) + // Else if docs.zip is not present, and we only have the "latest" tag, + // then throw an error - we can't resolve the fallback source ZIP + // unless we resort to calling the GitHub API, which we do not want to do + if (tag === 'latest') { + const err = `Failed to fetch. Could not find "docs.zip" at ${docsZipUrl}. To fall back to parsing docs from "source", please provide a specific tag instead of "${tag}".` + return [err, null] + } + // Else if docs.zip is not present, and we have a specific tag, then + // fall back to parsing docs files from the source zip + const sourceZipUrl = `https://github.com/${repo}/archive/${tag}.zip` + const sourceZipResponse = await fetch(sourceZipUrl, { method: 'GET' }) + const missingSourceZip = sourceZipResponse.status !== 200 + if (missingSourceZip) { + const err = `Failed to fetch. Could not find "docs.zip" at ${docsZipUrl}, and could not find fallback source ZIP at ${sourceZipUrl}. Please ensure one of these assets is available.` + return [err, null] + } + // Handle parsing from plugin source zip + return await parseSourceZip(sourceZipResponse) +} + +async function fetchPluginDocs({ repo, tag }) { + const [err, docsMdxFiles] = await fetchDocsFiles({ repo, tag }) + if (err) { + const errMsg = `Invalid plugin docs ${repo}, on release ${tag}. ${err}` + throw new Error(errMsg) + } + return docsMdxFiles +} + +function memoize(method) { + let cache = {} + return async function () { + let args = JSON.stringify(arguments) + if (!cache[args]) { + cache[args] = method.apply(this, arguments) + } + return cache[args] + } +} + +module.exports = memoize(fetchPluginDocs) diff --git a/website/components/remote-plugin-docs/utils/parse-docs-zip.js b/website/components/remote-plugin-docs/utils/parse-docs-zip.js new file mode 100644 index 000000000..69563877e --- /dev/null +++ b/website/components/remote-plugin-docs/utils/parse-docs-zip.js @@ -0,0 +1,44 @@ +const path = require('path') +const AdmZip = require('adm-zip') +const validatePluginDocsFiles = require('./validate-plugin-docs-files') + +// Given a response from fetching a docs.zip file, +// which is a compressed "docs" folder, +// +// return [null, docsMdxFiles] if docs files +// are successfully fetched and valid, +// where docsMdxFiles is an array of { filePath, fileString } items. +// +// otherwise, return [err, null] +// where err is an error message describing whether the +// docs files were missing or invalid, with a path to resolution +async function parseDocsZip(response) { + // the file path from the repo root is the same as the zip entryName, + // which includes the docs directory as the first part of the path + const responseBuffer = Buffer.from(await response.arrayBuffer()) + const responseZip = new AdmZip(responseBuffer) + const docsEntries = responseZip.getEntries() + // Validate the file paths within the "docs" folder + const docsFilePaths = docsEntries.map((e) => e.entryName) + const validationError = validatePluginDocsFiles(docsFilePaths) + if (validationError) return [validationError, null] + // If valid, filter for MDX files only, and return + // a { filePath, fileString } object for each mdx file + const docsMdxFiles = docsEntries + .filter((e) => { + return path.extname(e.entryName) === '.mdx' + }) + .map((e) => { + const filePath = e.entryName + const fileString = e.getData().toString() + return { filePath, fileString } + }) + return [null, docsMdxFiles] +} + +/* + const dirs = path.dirname(e.entryName).split('/') + const pathFromDocsDir = dirs.slice(1).join('/') + */ + +module.exports = parseDocsZip diff --git a/website/components/remote-plugin-docs/utils/parse-source-zip.js b/website/components/remote-plugin-docs/utils/parse-source-zip.js new file mode 100644 index 000000000..07a26ad6a --- /dev/null +++ b/website/components/remote-plugin-docs/utils/parse-source-zip.js @@ -0,0 +1,49 @@ +const path = require('path') +const AdmZip = require('adm-zip') +const validatePluginDocsFiles = require('./validate-plugin-docs-files') + +// Given a response from fetching a source .zip file, +// which contains a "docs" folder, +// +// return [null, docsMdxFiles] if docs files +// are successfully fetched and valid, +// where docsMdxFiles is an array of { filePath, fileString } items. +// +// otherwise, return [err, null] +// where err is an error message describing whether the +// docs files were missing or invalid, with a path to resolution +async function parseSourceZip(response) { + const responseBuffer = Buffer.from(await response.arrayBuffer()) + const responseZip = new AdmZip(responseBuffer) + const sourceEntries = responseZip.getEntries() + const docsEntries = sourceEntries.filter((entry) => { + // filter for zip entries in the docs subfolder only + const dirs = path.dirname(entry.entryName).split('/') + return dirs.length > 1 && dirs[1] === 'docs' + }) + // Validate the file paths within the "docs" folder + const docsFilePaths = docsEntries.map((e) => { + // We need to remove the leading directory, + // which will be something like packer-plugin-docs-0.0.5 + const filePath = e.entryName.split('/').slice(1).join('/') + return filePath + }) + const validationError = validatePluginDocsFiles(docsFilePaths) + if (validationError) return [validationError, null] + // If valid, filter for MDX files only, and return + // a { filePath, fileString } object for each mdx file + const docsMdxFiles = docsEntries + .filter((e) => { + return path.extname(e.entryName) === '.mdx' + }) + .map((e) => { + // We need to remove the leading directory, + // which will be something like packer-plugin-docs-0.0.5 + const filePath = e.entryName.split('/').slice(1).join('/') + const fileString = e.getData().toString() + return { filePath, fileString } + }) + return [null, docsMdxFiles] +} + +module.exports = parseSourceZip diff --git a/website/components/remote-plugin-docs/utils/resolve-nav-data.js b/website/components/remote-plugin-docs/utils/resolve-nav-data.js new file mode 100644 index 000000000..837feb6b4 --- /dev/null +++ b/website/components/remote-plugin-docs/utils/resolve-nav-data.js @@ -0,0 +1,218 @@ +const fs = require('fs') +const path = require('path') +const grayMatter = require('gray-matter') +const fetchPluginDocs = require('./fetch-plugin-docs') +const validateFilePaths = require('@hashicorp/react-docs-sidenav/utils/validate-file-paths') +const validateRouteStructure = require('@hashicorp/react-docs-sidenav/utils/validate-route-structure') + +/** + * Resolves nav-data from file, including optional + * 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 {string} localContentDir path to the content root, relative to the cwd. Example: "content/docs". + * @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 {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 resolveNavData(navDataFile, localContentDir, options = {}) { + const { remotePluginsFile } = options + // Read in files + const navDataPath = path.join(process.cwd(), navDataFile) + const navData = JSON.parse(fs.readFileSync(navDataPath, 'utf8')) + // Fetch remote plugin docs, if applicable + let withPlugins = navData + if (remotePluginsFile) { + // Resolve plugins, this yields branches with NavLeafRemote nodes + withPlugins = await mergeRemotePlugins(remotePluginsFile, navData) + } + // Resolve local filePaths for NavLeaf nodes + const withFilePaths = await validateFilePaths(withPlugins, localContentDir) + validateRouteStructure(withFilePaths) + // Return the nav data with: + // 1. Plugins merged, transformed into navData structures with NavLeafRemote nodes + // 2. filePaths added to all local NavLeaf nodes + return withFilePaths +} + +// Given a remote plugins config file, and the full tree of docs navData which +// contains top-level branch routes that match plugin component types, +// fetch and parse all remote plugin docs, merge them into the +// broader tree of docs navData, and return the docs navData +// with the merged plugin docs +async function mergeRemotePlugins(remotePluginsFile, navData) { + // 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(resolvePluginEntryDocs) + ) + // group navData by component type, to prepare to merge plugin docs + // into the broader tree of navData. + const pluginDocsByComponent = pluginEntriesWithDocs.reduce( + (acc, pluginEntry) => { + const { components } = pluginEntry + Object.keys(components).forEach((type) => { + const navData = components[type] + if (!navData) return + if (!acc[type]) acc[type] = [] + acc[type].push(navData[0]) + }) + return acc + }, + {} + ) + // merge plugin docs, by plugin component type, + // into the corresponding top-level component NavBranch + const navDataWithPlugins = navData.slice().map((n) => { + // we only care about top-level NavBranch nodes + if (!n.routes) return n + // for each component type, check if this NavBranch + // is the parent route for that type + const componentTypes = Object.keys(pluginDocsByComponent) + let typeMatch = false + for (var i = 0; i < componentTypes.length; i++) { + const componentType = componentTypes[i] + const routeMatches = n.routes.filter((r) => r.path === componentType) + if (routeMatches.length > 0) { + typeMatch = componentType + break + } + } + // if this NavBranch does not match a component type slug, + // then return it unmodified + if (!typeMatch) return n + // if there are no matching remote plugin components, + // then return the navBranch unmodified + const pluginsOfType = pluginDocsByComponent[typeMatch] + if (!pluginsOfType || pluginsOfType.length == 0) return n + // if this NavBranch is the parent route for the type, + // then append all remote plugins of this type to the + // NavBranch's child routes + const routesWithPlugins = n.routes.slice().concat(pluginsOfType) + // console.log(JSON.stringify(routesWithPlugins, null, 2)) + // Also, sort the child routes so the order is alphabetical + routesWithPlugins.sort((a, b) => { + // (exception: "Overview" comes first) + if (a.title == 'Overview') return -1 + if (b.title === 'Overview') return 1 + // (exception: "Community-Supported" comes last) + if (a.title == 'Community-Supported') return 1 + if (b.title === 'Community-Supported') return -1 + // (exception: "Custom" comes second-last) + if (a.title == 'Custom') return 1 + if (b.title === 'Custom') return -1 + return a.title < b.title ? -1 : a.title > b.title ? 1 : 0 + }) + // return n + return { ...n, routes: routesWithPlugins } + }) + // return the merged navData, which now includes special NavLeaf nodes + // for plugin docs with { filePath, fileString } remoteFile properties + return navDataWithPlugins +} + +// 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) { + const { title, path: slug, repo, version } = pluginConfigEntry + const 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 a pluginTier attribute + 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) + // 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 + const sourceUrl = `https://github.com/${repo}/blob/${version}/${filePath}` + // determine pluginTier + const pluginOwner = repo.split('/')[0] + const pluginTier = pluginOwner === 'hashicorp' ? 'official' : 'community' + // Construct and return a NavLeafRemote node + return { + title, + path: urlPath, + remoteFile: { filePath, fileString, sourceUrl }, + pluginTier, + } + }) + // + 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 } + }) + // + return { type, navData: withPrefixedPaths } + }) + 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 = resolveNavData diff --git a/website/components/remote-plugin-docs/utils/validate-plugin-docs-files.js b/website/components/remote-plugin-docs/utils/validate-plugin-docs-files.js new file mode 100644 index 000000000..e846092a9 --- /dev/null +++ b/website/components/remote-plugin-docs/utils/validate-plugin-docs-files.js @@ -0,0 +1,46 @@ +const path = require('path') + +const COMPONENT_TYPES = [ + 'builders', + 'datasources', + 'post-processors', + 'provisioners', +] + +// Given an array of file paths within the "docs" folder, +// validate that no unexpected files are being included, +// and that there is at least one component subfolder +// with at least one .mdx file within it. +function validatePluginDocsFiles(filePaths) { + function isValidPath(filePath) { + const isDocsRoot = filePath === 'docs/' + const isComponentRoot = COMPONENT_TYPES.reduce((acc, type) => { + return acc || filePath === `docs/${type}/` + }, false) + const isComponentMdx = COMPONENT_TYPES.reduce((acc, type) => { + const mdxPathRegex = new RegExp(`^docs/${type}/(.*).mdx$`) + return acc || mdxPathRegex.test(filePath) + }, false) + const isValidPath = isDocsRoot || isComponentRoot || isComponentMdx + return isValidPath + } + const invalidPaths = filePaths.filter((f) => !isValidPath(f)) + if (invalidPaths.length > 0) { + return `Found invalid files or folders in the docs directory: ${JSON.stringify( + invalidPaths + )}. Please ensure the docs folder contains only component subfolders and .mdx files within those subfolders. Valid component types are: ${JSON.stringify( + COMPONENT_TYPES + )}.` + } + const validPaths = filePaths.filter(isValidPath) + const mdxFiles = validPaths.filter((fp) => path.extname(fp) === '.mdx') + const isMissingDocs = mdxFiles.length == 0 + if (isMissingDocs) { + return `Could not find valid .mdx files. Please ensure there is at least one component subfolder in the docs directory, which contains at least one .mdx file. Valid component types are: ${JSON.stringify( + COMPONENT_TYPES + )}.` + } + return null +} + +module.exports = validatePluginDocsFiles From 8b3e7e6f2ff0b94c2d58f86cbbaafd19ad6043db Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 18 Mar 2021 12:24:36 -0400 Subject: [PATCH 02/19] website: use revised remote-plugin-docs server implementation - also bumps to stable docs-page, and makes related api changes for intro and guides routes --- .../components/remote-plugin-docs/server.js | 49 +-- website/data/docs-remote-plugins.json | 3 +- website/package-lock.json | 402 ++++++------------ website/package.json | 6 +- website/pages/docs/[[...page]].jsx | 8 +- website/pages/guides/[[...page]].jsx | 17 +- website/pages/intro/[[...page]].jsx | 17 +- 7 files changed, 166 insertions(+), 336 deletions(-) diff --git a/website/components/remote-plugin-docs/server.js b/website/components/remote-plugin-docs/server.js index a7d699e57..9b00b6f92 100644 --- a/website/components/remote-plugin-docs/server.js +++ b/website/components/remote-plugin-docs/server.js @@ -3,13 +3,9 @@ import path from 'path' import { getNodeFromPath, getPathsFromNavData, - validateNavData, } from '@hashicorp/react-docs-page/server' import renderPageMdx from '@hashicorp/react-docs-page/render-page-mdx' -import fetchGithubFile from './utils/fetch-github-file' -import mergeRemotePlugins from './utils/merge-remote-plugins' - -const IS_DEV = process.env.VERCEL_ENV !== 'production' +import resolveNavData from './utils/resolve-nav-data' async function generateStaticPaths(navDataFile, contentDir, options = {}) { const navData = await resolveNavData(navDataFile, contentDir, options) @@ -21,7 +17,8 @@ async function generateStaticProps( navDataFile, localContentDir, params, - { productName, remotePluginsFile, additionalComponents } = {} + product, + { remotePluginsFile, additionalComponents, mainBranch = 'main' } = {} ) { const navData = await resolveNavData(navDataFile, localContentDir, { remotePluginsFile, @@ -30,12 +27,15 @@ async function generateStaticProps( const navNode = getNodeFromPath(pathToMatch, navData, localContentDir) const { filePath, remoteFile, pluginTier } = navNode // Fetch the MDX file content - const [err, mdxString] = filePath - ? // Read local content from the filesystem - [null, fs.readFileSync(path.join(process.cwd(), filePath), 'utf8')] - : // Fetch remote content using GitHub's API - await fetchGithubFile(remoteFile) - if (err) throw new Error(err) + const mdxString = remoteFile + ? remoteFile.fileString + : fs.readFileSync(path.join(process.cwd(), filePath), 'utf8') + // Construct the githubFileUrl, used for "Edit this page" link + // Note: for custom ".docs-artifacts" directories, the "Edit this page" + // link will lead to the artifact file rather than the "docs" source file + const githubFileUrl = remoteFile + ? remoteFile.sourceUrl + : `https://github.com/hashicorp/${product.slug}/blob/${mainBranch}/website/${filePath}` // For plugin pages, prefix the MDX content with a // label that reflects the plugin tier // (current options are "Official" or "Community") @@ -48,41 +48,22 @@ async function generateStaticProps( } const { mdxSource, frontMatter } = await renderPageMdx(mdxString, { additionalComponents, - productName, + productName: product.name, mdxContentHook, }) // Build the currentPath from page parameters const currentPath = !params.page ? '' : params.page.join('/') - // In development, set a flag if there is no GITHUB_API_TOKEN, - // as this means dev is seeing only local content, and we want to flag that - const isDevMissingRemotePlugins = IS_DEV && !process.env.GITHUB_API_TOKEN + return { currentPath, frontMatter, - isDevMissingRemotePlugins, mdxSource, mdxString, + githubFileUrl, navData, navNode, } } -async function resolveNavData(navDataFile, localContentDir, options = {}) { - const { remotePluginsFile } = options - // Read in files - const navDataPath = path.join(process.cwd(), navDataFile) - const navData = JSON.parse(fs.readFileSync(navDataPath, 'utf8')) - const remotePluginsPath = path.join(process.cwd(), remotePluginsFile) - const remotePlugins = JSON.parse(fs.readFileSync(remotePluginsPath, 'utf-8')) - // Resolve plugins, this yields branches with NavLeafRemote nodes - const withPlugins = await mergeRemotePlugins(remotePlugins, navData, IS_DEV) - // Resolve local filePaths for NavLeaf nodes - const withFilePaths = await validateNavData(withPlugins, localContentDir) - // Return the nav data with: - // 1. Plugins merged, transformed into navData structures with NavLeafRemote nodes - // 2. filePaths added to all local NavLeaf nodes - return withFilePaths -} - export default { generateStaticPaths, generateStaticProps } export { generateStaticPaths, generateStaticProps } diff --git a/website/data/docs-remote-plugins.json b/website/data/docs-remote-plugins.json index 8c247cfab..ab7aecb94 100644 --- a/website/data/docs-remote-plugins.json +++ b/website/data/docs-remote-plugins.json @@ -2,6 +2,7 @@ { "title": "Docker", "path": "docker", - "repo": "hashicorp/packer-plugin-docker" + "repo": "hashicorp/packer-plugin-docker", + "version": "v0.0.4" } ] diff --git a/website/package-lock.json b/website/package-lock.json index 1a542e882..e2585dfd9 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -5,118 +5,118 @@ "requires": true, "dependencies": { "@algolia/cache-browser-local-storage": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.8.3.tgz", - "integrity": "sha512-Cwc03hikHSUI+xvgUdN+H+f6jFyoDsC9fegzXzJ2nPn1YSN9EXzDMBnbrgl0sbl9iLGXe0EIGMYqR2giCv1wMQ==", + "version": "4.8.6", + "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.8.6.tgz", + "integrity": "sha512-Bam7otzjIEgrRXWmk0Amm1+B3ROI5dQnUfJEBjIy0YPM0kMahEoJXCw6160tGKxJLl1g6icoC953nGshQKO7cA==", "requires": { - "@algolia/cache-common": "4.8.3" + "@algolia/cache-common": "4.8.6" } }, "@algolia/cache-common": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.8.3.tgz", - "integrity": "sha512-Cf7zZ2i6H+tLSBTkFePHhYvlgc9fnMPKsF9qTmiU38kFIGORy/TN2Fx5n1GBuRLIzaSXvcf+oHv1HvU0u1gE1g==" + "version": "4.8.6", + "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.8.6.tgz", + "integrity": "sha512-eGQlsXU5G7n4RvV/K6qe6lRAeL6EKAYPT3yZDBjCW4pAh7JWta+77a7BwUQkTqXN1MEQWZXjex3E4z/vFpzNrg==" }, "@algolia/cache-in-memory": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.8.3.tgz", - "integrity": "sha512-+N7tkvmijXiDy2E7u1mM73AGEgGPWFmEmPeJS96oT46I98KXAwVPNYbcAqBE79YlixdXpkYJk41cFcORzNh+Iw==", + "version": "4.8.6", + "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.8.6.tgz", + "integrity": "sha512-kbJrvCFANxL/l5Pq1NFyHLRphKDwmqcD/OJga0IbNKEulRGDPkt1+pC7/q8d2ikP12adBjLLg2CVias9RJpIaw==", "requires": { - "@algolia/cache-common": "4.8.3" + "@algolia/cache-common": "4.8.6" } }, "@algolia/client-account": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.8.3.tgz", - "integrity": "sha512-Uku8LqnXBwfDCtsTCDYTUOz2/2oqcAQCKgaO0uGdIR8DTQENBXFQvzziambHdn9KuFuY+6Et9k1+cjpTPBDTBg==", + "version": "4.8.6", + "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.8.6.tgz", + "integrity": "sha512-FQVJE/BgCb78jtG7V0r30sMl9P5JKsrsOacGtGF2YebqI0YF25y8Z1nO39lbdjahxUS3QkDw2d0P2EVMj65g2Q==", "requires": { - "@algolia/client-common": "4.8.3", - "@algolia/client-search": "4.8.3", - "@algolia/transporter": "4.8.3" + "@algolia/client-common": "4.8.6", + "@algolia/client-search": "4.8.6", + "@algolia/transporter": "4.8.6" } }, "@algolia/client-analytics": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.8.3.tgz", - "integrity": "sha512-9ensIWmjYJprZ+YjAVSZdWUG05xEnbytENXp508X59tf34IMIX8BR2xl0RjAQODtxBdAteGxuKt5THX6U9tQLA==", + "version": "4.8.6", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.8.6.tgz", + "integrity": "sha512-ZBYFUlzNaWDFtt0rYHI7xbfVX0lPWU9lcEEXI/BlnkRgEkm247H503tNatPQFA1YGkob52EU18sV1eJ+OFRBLA==", "requires": { - "@algolia/client-common": "4.8.3", - "@algolia/client-search": "4.8.3", - "@algolia/requester-common": "4.8.3", - "@algolia/transporter": "4.8.3" + "@algolia/client-common": "4.8.6", + "@algolia/client-search": "4.8.6", + "@algolia/requester-common": "4.8.6", + "@algolia/transporter": "4.8.6" } }, "@algolia/client-common": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.8.3.tgz", - "integrity": "sha512-TU3623AEFAWUQlDTznkgAMSYo8lfS9pNs5QYDQzkvzWdqK0GBDWthwdRfo9iIsfxiR9qdCMHqwEu+AlZMVhNSA==", + "version": "4.8.6", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.8.6.tgz", + "integrity": "sha512-8dI+K3Nvbes2YRZm2LY7bdCUD05e60BhacrMLxFuKxnBGuNehME1wbxq/QxcG1iNFJlxLIze5TxIcNN3+pn76g==", "requires": { - "@algolia/requester-common": "4.8.3", - "@algolia/transporter": "4.8.3" + "@algolia/requester-common": "4.8.6", + "@algolia/transporter": "4.8.6" } }, "@algolia/client-recommendation": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/client-recommendation/-/client-recommendation-4.8.3.tgz", - "integrity": "sha512-qysGbmkcc6Agt29E38KWJq9JuxjGsyEYoKuX9K+P5HyQh08yR/BlRYrA8mB7vT/OIUHRGFToGO6Vq/rcg0NIOQ==", + "version": "4.8.6", + "resolved": "https://registry.npmjs.org/@algolia/client-recommendation/-/client-recommendation-4.8.6.tgz", + "integrity": "sha512-Kg8DpjwvaWWujNx6sAUrSL+NTHxFe/UNaliCcSKaMhd3+FiPXN+CrSkO0KWR7I+oK2qGBTG/2Y0BhFOJ5/B/RA==", "requires": { - "@algolia/client-common": "4.8.3", - "@algolia/requester-common": "4.8.3", - "@algolia/transporter": "4.8.3" + "@algolia/client-common": "4.8.6", + "@algolia/requester-common": "4.8.6", + "@algolia/transporter": "4.8.6" } }, "@algolia/client-search": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.8.3.tgz", - "integrity": "sha512-rAnvoy3GAhbzOQVniFcKVn1eM2NX77LearzYNCbtFrFYavG+hJI187bNVmajToiuGZ10FfJvK99X2OB1AzzezQ==", + "version": "4.8.6", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.8.6.tgz", + "integrity": "sha512-vXLS6umL/9G3bwqc6pkrS9K5/s8coq55mpfRARL+bs0NsToOf77WSTdwzlxv/KdbVF7dHjXgUpBvJ6RyR4ZdAw==", "requires": { - "@algolia/client-common": "4.8.3", - "@algolia/requester-common": "4.8.3", - "@algolia/transporter": "4.8.3" + "@algolia/client-common": "4.8.6", + "@algolia/requester-common": "4.8.6", + "@algolia/transporter": "4.8.6" } }, "@algolia/logger-common": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.8.3.tgz", - "integrity": "sha512-03wksHRbhl2DouEKnqWuUb64s1lV6kDAAabMCQ2Du1fb8X/WhDmxHC4UXMzypeOGlH5BZBsgVwSB7vsZLP3MZg==" + "version": "4.8.6", + "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.8.6.tgz", + "integrity": "sha512-FMRxZGdDxSzd0/Mv0R1021FvUt0CcbsQLYeyckvSWX8w+Uk4o0lcV6UtZdERVR5XZsGOqoXLMIYDbR2vkbGbVw==" }, "@algolia/logger-console": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.8.3.tgz", - "integrity": "sha512-Npt+hI4UF8t3TLMluL5utr9Gc11BjL5kDnGZOhDOAz5jYiSO2nrHMFmnpLT4Cy/u7a5t7EB5dlypuC4/AGStkA==", + "version": "4.8.6", + "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.8.6.tgz", + "integrity": "sha512-TYw9lwUCjvApC6Z0zn36T6gkCl7hbfJmnU+Z/D8pFJ3Yp7lz06S3oWGjbdrULrYP1w1VOhjd0X7/yGNsMhzutQ==", "requires": { - "@algolia/logger-common": "4.8.3" + "@algolia/logger-common": "4.8.6" } }, "@algolia/requester-browser-xhr": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.8.3.tgz", - "integrity": "sha512-/LTTIpgEmEwkyhn8yXxDdBWqXqzlgw5w2PtTpIwkSlP2/jDwdR/9w1TkFzhNbJ81ki6LAEQM5mSwoTTnbIIecg==", + "version": "4.8.6", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.8.6.tgz", + "integrity": "sha512-omh6uJ3CJXOmcrU9M3/KfGg8XkUuGJGIMkqEbkFvIebpBJxfs6TVs0ziNeMFAcAfhi8/CGgpLbDSgJtWdGQa6w==", "requires": { - "@algolia/requester-common": "4.8.3" + "@algolia/requester-common": "4.8.6" } }, "@algolia/requester-common": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.8.3.tgz", - "integrity": "sha512-+Yo9vBkofoKR1SCqqtMnmnfq9yt/BiaDewY/6bYSMNxSYCnu2Fw1JKSIaf/4zos09PMSsxGpLohZwGas3+0GDQ==" + "version": "4.8.6", + "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.8.6.tgz", + "integrity": "sha512-r5xJqq/D9KACkI5DgRbrysVL5DUUagikpciH0k0zjBbm+cXiYfpmdflo/h6JnY6kmvWgjr/4DoeTjKYb/0deAQ==" }, "@algolia/requester-node-http": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.8.3.tgz", - "integrity": "sha512-k2fiKIeMIFqgC01FnzII6kqC2GQBAfbNaUX4k7QCPa6P8t4sp2xE6fImOUiztLnnL3C9X9ZX6Fw3L+cudi7jvQ==", + "version": "4.8.6", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.8.6.tgz", + "integrity": "sha512-TB36OqTVOKyHCOtdxhn/IJyI/NXi/BWy8IEbsiWwwZWlL79NWHbetj49jXWFolEYEuu8PgDjjZGpRhypSuO9XQ==", "requires": { - "@algolia/requester-common": "4.8.3" + "@algolia/requester-common": "4.8.6" } }, "@algolia/transporter": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.8.3.tgz", - "integrity": "sha512-nU7fy2iU8snxATlsks0MjMyv97QJWQmOVwTjDc+KZ4+nue8CLcgm4LA4dsTBqvxeCQIoEtt3n72GwXcaqiJSjQ==", + "version": "4.8.6", + "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.8.6.tgz", + "integrity": "sha512-NRb31J0TP7EPoVMpXZ4yAtr61d26R8KGaf6qdULknvq5sOVHuuH4PwmF08386ERfIsgnM/OBhl+uzwACdCIjSg==", "requires": { - "@algolia/cache-common": "4.8.3", - "@algolia/logger-common": "4.8.3", - "@algolia/requester-common": "4.8.3" + "@algolia/cache-common": "4.8.6", + "@algolia/logger-common": "4.8.6", + "@algolia/requester-common": "4.8.6" } }, "@ampproject/toolbox-core": { @@ -2854,12 +2854,12 @@ "integrity": "sha512-AYIe6tcOxlKPe5Sq89o/Vk0rGE6Z1dCzf+N3ynECTh5L2A1zusf9xeM659QEh/edE/Ll9EBBLmq49sQXLNDxTw==" }, "@hashicorp/react-docs-page": { - "version": "10.9.4-alpha.18", - "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-page/-/react-docs-page-10.9.4-alpha.18.tgz", - "integrity": "sha512-+eRKJ2PX9s4Is0ZT2O8ZBcBWuDt7OGxwBrqKF1ulo/DcZunj7pODCQQulb+jAtQyq7YzikWdFmQ/pcvwaVHK6Q==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-page/-/react-docs-page-11.0.1.tgz", + "integrity": "sha512-BZ746Qm97OQTyMPI7calYDb+LAQQZGTn/vZ8FaMXbVCF+X9Bvs1xYyWRDV6gV0mtgmlkCwYqIrmqneOZdd6PcA==", "requires": { "@hashicorp/react-content": "^6.3.0", - "@hashicorp/react-docs-sidenav": "6.1.1-alpha.16", + "@hashicorp/react-docs-sidenav": "^7.0.0", "@hashicorp/react-head": "^1.2.0", "@hashicorp/react-search": "^4.1.0", "fs-exists-sync": "0.1.0", @@ -2870,121 +2870,6 @@ "readdirp": "3.5.0" }, "dependencies": { - "@algolia/cache-browser-local-storage": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.8.5.tgz", - "integrity": "sha512-9rs/Yi82ilgifweJamOy4DlJ4xPGsCN/zg+RKy4vjytNhOrkEHLRQC8vPZ3OhD8KVlw9lRQIZTlgjgFl8iMeeA==", - "requires": { - "@algolia/cache-common": "4.8.5" - } - }, - "@algolia/cache-common": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.8.5.tgz", - "integrity": "sha512-4SvRWnagKtwBFAy8Rsfmv0/Uk53fZL+6dy2idwdx6SjMGKSs0y1Qv+thb4h/k/H5MONisAoT9C2rgZ/mqwh5yw==" - }, - "@algolia/cache-in-memory": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.8.5.tgz", - "integrity": "sha512-XBBfqs28FbjwLboY3sxvuzBgYsuXdFsj2mUvkgxfb0GVEzwW4I0NM7KzSPwT+iht55WS1PgIOnynjmhPsrubCw==", - "requires": { - "@algolia/cache-common": "4.8.5" - } - }, - "@algolia/client-account": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.8.5.tgz", - "integrity": "sha512-DjXMpeCdY4J4IDBfowiG6Xl9ec/FhG1NpPQM0Uv4xXsc/TeeZ1JgbgNDhWe9jW0jBEALy+a/RmPrZ0vsxcadsg==", - "requires": { - "@algolia/client-common": "4.8.5", - "@algolia/client-search": "4.8.5", - "@algolia/transporter": "4.8.5" - } - }, - "@algolia/client-analytics": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.8.5.tgz", - "integrity": "sha512-PQEY+chbHmZnRJdaWsvUYzDpEPr60az0EPUexdouvXGZId15/SnDaXjnf89F7tYmCzkHdUtG4bSvPzAupQ4AFA==", - "requires": { - "@algolia/client-common": "4.8.5", - "@algolia/client-search": "4.8.5", - "@algolia/requester-common": "4.8.5", - "@algolia/transporter": "4.8.5" - } - }, - "@algolia/client-common": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.8.5.tgz", - "integrity": "sha512-Dn8vog2VrGsJeOcBMcSAEIjBtPyogzUBGlh1DtVd0m8GN6q+cImCESl6DY846M2PTYWsLBKBksq37eUfSe9FxQ==", - "requires": { - "@algolia/requester-common": "4.8.5", - "@algolia/transporter": "4.8.5" - } - }, - "@algolia/client-recommendation": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/client-recommendation/-/client-recommendation-4.8.5.tgz", - "integrity": "sha512-ffawCC1C25rCa8/JU2niRZgwr8aV9b2qsLVMo73GXFzi2lceXPAe9K68mt/BGHU+w7PFUwVHsV2VmB+G/HQRVw==", - "requires": { - "@algolia/client-common": "4.8.5", - "@algolia/requester-common": "4.8.5", - "@algolia/transporter": "4.8.5" - } - }, - "@algolia/client-search": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.8.5.tgz", - "integrity": "sha512-Ru2MljGZWrSQ0CVsDla11oGEPL/RinmVkLJfBtQ+/pk1868VfpAQFGKtOS/b8/xLrMA0Vm4EfC3Mgclk/p3KJA==", - "requires": { - "@algolia/client-common": "4.8.5", - "@algolia/requester-common": "4.8.5", - "@algolia/transporter": "4.8.5" - } - }, - "@algolia/logger-common": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.8.5.tgz", - "integrity": "sha512-PS6NS6bpED0rAxgCPGhjZJg9why0PnoVEE7ZoCbPq6lsAOc6FPlQLri4OiLyU7wx8RWDoVtOadyzulqAAsfPSQ==" - }, - "@algolia/logger-console": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.8.5.tgz", - "integrity": "sha512-3+4gLSbwzuGmrb5go3IZNcFIYVMSbB4c8UMtWEJ/gDBtgGZIvT6f/KlvVSOHIhthSxaM3Y13V6Qile/SpGqc6A==", - "requires": { - "@algolia/logger-common": "4.8.5" - } - }, - "@algolia/requester-browser-xhr": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.8.5.tgz", - "integrity": "sha512-M/Gf2vv/fU4+CqDW+wok7HPpEcLym3NtDzU9zaPzGYI/9X7o36581oyfnzt2pNfsXSQVj5a2pZVUWC3Z4SO27w==", - "requires": { - "@algolia/requester-common": "4.8.5" - } - }, - "@algolia/requester-common": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.8.5.tgz", - "integrity": "sha512-OIhsdwIrJVAlVlP7cwlt+RoR5AmxAoTGrFokOY9imVmgqXUUljdKO/DjhRL8vwYGFEidZ9efIjAIQ2B3XOhT9A==" - }, - "@algolia/requester-node-http": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.8.5.tgz", - "integrity": "sha512-viHAjfo53A3VSE7Bb/nzgpSMZ3prPp2qti7Wg8w7qxhntppKp3Fln6t4Vp+BoPOqruLsj139xXhheAKeRcYa0w==", - "requires": { - "@algolia/requester-common": "4.8.5" - } - }, - "@algolia/transporter": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.8.5.tgz", - "integrity": "sha512-Rb3cMlh/GoJK0+g+49GNA3IvR/EXsDEBwpyM+FOotSwxgiGt1wGBHM0K2v0GHwIEcuww02pl6KMDVlilA+qh0g==", - "requires": { - "@algolia/cache-common": "4.8.5", - "@algolia/logger-common": "4.8.5", - "@algolia/requester-common": "4.8.5" - } - }, "@hashicorp/react-content": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/@hashicorp/react-content/-/react-content-6.3.0.tgz", @@ -3016,73 +2901,23 @@ "search-insights": "^1.6.0", "unist-util-visit": "^2.0.3" } - }, - "algoliasearch": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.8.5.tgz", - "integrity": "sha512-GjKjpeevpePEJYinGokASNtIkl1t5EseNMlqDNAc+sXE8+iyyeqTyiJsN7bwlRG2BIremuslE/NlwdEfUuBLJw==", - "requires": { - "@algolia/cache-browser-local-storage": "4.8.5", - "@algolia/cache-common": "4.8.5", - "@algolia/cache-in-memory": "4.8.5", - "@algolia/client-account": "4.8.5", - "@algolia/client-analytics": "4.8.5", - "@algolia/client-common": "4.8.5", - "@algolia/client-recommendation": "4.8.5", - "@algolia/client-search": "4.8.5", - "@algolia/logger-common": "4.8.5", - "@algolia/logger-console": "4.8.5", - "@algolia/requester-browser-xhr": "4.8.5", - "@algolia/requester-common": "4.8.5", - "@algolia/requester-node-http": "4.8.5", - "@algolia/transporter": "4.8.5" - } - }, - "algoliasearch-helper": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.4.4.tgz", - "integrity": "sha512-OjyVLjykaYKCMxxRMZNiwLp8CS310E0qAeIY2NaublcmLAh8/SL19+zYHp7XCLtMem2ZXwl3ywMiA32O9jszuw==", - "requires": { - "events": "^1.1.1" - } - }, - "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" - }, - "react-instantsearch-core": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/react-instantsearch-core/-/react-instantsearch-core-6.10.0.tgz", - "integrity": "sha512-bn8rh/od4nw43caOiAsArA2Pw/JXX/7jL+nYe0n/Se66P7VR7UIA1i1ycthOrJzXCn9iNVFJFNMfyAN0HYVaWg==", - "requires": { - "@babel/runtime": "^7.1.2", - "algoliasearch-helper": "^3.4.3", - "prop-types": "^15.6.2", - "react-fast-compare": "^3.0.0" - } - }, - "react-instantsearch-dom": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/react-instantsearch-dom/-/react-instantsearch-dom-6.10.0.tgz", - "integrity": "sha512-t1IGn1i4btp9a8wNNV/OCYwfJwCx5CuCP6WNwBxYY1QeL27RKGaWPxvz6FjfRFCfrOvD2556STyvVriyGhDoeg==", - "requires": { - "@babel/runtime": "^7.1.2", - "algoliasearch-helper": "^3.4.3", - "classnames": "^2.2.5", - "prop-types": "^15.6.2", - "react-fast-compare": "^3.0.0", - "react-instantsearch-core": "^6.10.0" - } } } }, "@hashicorp/react-docs-sidenav": { - "version": "6.1.1-alpha.16", - "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-sidenav/-/react-docs-sidenav-6.1.1-alpha.16.tgz", - "integrity": "sha512-RpPjNwMNe5L2LA1vvgp496CauVJ8wLnKge1lPBZKL5931jR1SFEMwuWLB8R6Pe2HmkIC55nPB/c43GrmPN4FFw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-sidenav/-/react-docs-sidenav-7.0.0.tgz", + "integrity": "sha512-gzOEG4fwfdfdHvxMuRC73bZUIpUzSPrG826NIM4N0lqUPzsAkDsfEl2+Vg1ZVfgzy2+41E+lIpHW4ZmWc5OZ7A==", "requires": { + "@hashicorp/react-link-wrap": "^2.0.2", "fuzzysearch": "1.0.3" + }, + "dependencies": { + "@hashicorp/react-link-wrap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@hashicorp/react-link-wrap/-/react-link-wrap-2.0.2.tgz", + "integrity": "sha512-q8s2TTd9Uy3BSYyUe2TTr2Kbc0ViRc7XQga2fZI0bzlFqBTiMXtf6gh2cg3QvimHY42y4YtaO5C109V9ahMUpQ==" + } } }, "@hashicorp/react-enterprise-alert": { @@ -3183,17 +3018,17 @@ } }, "@hashicorp/react-search": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@hashicorp/react-search/-/react-search-3.0.0.tgz", - "integrity": "sha512-62ttyCxjVFSHz1aNbdjeOcqCezpk3dLhMWTXeQb9Zsi0JYaJdBzK1M9khW1bfozTzjTXXGd/B79orlHMj/Zo9A==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@hashicorp/react-search/-/react-search-4.2.0.tgz", + "integrity": "sha512-ITj3UC06w+bZKrHv77kYdtWlEH9gbtk+pAzZ5ZRxt2GMnw8qMzWnXZKVf1yHvyKAKkHkGXA5s+uFElxRJj3AVQ==", "requires": { "@hashicorp/react-inline-svg": "^1.0.2", "@hashicorp/remark-plugins": "^3.0.0", - "algoliasearch": "^4.4.0", + "algoliasearch": "^4.8.4", "dotenv": "^8.2.0", "glob": "^7.1.6", "gray-matter": "^4.0.2", - "react-instantsearch-dom": "^6.7.0", + "react-instantsearch-dom": "^6.9.0", "remark": "^12.0.1", "search-insights": "^1.6.0", "unist-util-visit": "^2.0.3" @@ -3927,6 +3762,11 @@ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==" }, + "adm-zip": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.4.tgz", + "integrity": "sha512-GMQg1a1cAegh+/EgWbz+XHZrwB467iB/IgtToldvxs7Xa5Br8mPmvCeRfY/Un2fLzrlIPt6Yu7Cej+8Ut9TGPg==" + }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -3973,30 +3813,30 @@ "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" }, "algoliasearch": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.8.3.tgz", - "integrity": "sha512-pljX9jEE2TQ3i1JayhG8afNdE8UuJg3O9c7unW6QO67yRWCKr6b0t5aKC3hSVtjt7pA2TQXLKoAISb4SHx9ozQ==", + "version": "4.8.6", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.8.6.tgz", + "integrity": "sha512-G8IA3lcgaQB4r9HuQ4G+uSFjjz0Wv2OgEPiQ8emA+G2UUlroOfMl064j1bq/G+QTW0LmTQp9JwrFDRWxFM9J7w==", "requires": { - "@algolia/cache-browser-local-storage": "4.8.3", - "@algolia/cache-common": "4.8.3", - "@algolia/cache-in-memory": "4.8.3", - "@algolia/client-account": "4.8.3", - "@algolia/client-analytics": "4.8.3", - "@algolia/client-common": "4.8.3", - "@algolia/client-recommendation": "4.8.3", - "@algolia/client-search": "4.8.3", - "@algolia/logger-common": "4.8.3", - "@algolia/logger-console": "4.8.3", - "@algolia/requester-browser-xhr": "4.8.3", - "@algolia/requester-common": "4.8.3", - "@algolia/requester-node-http": "4.8.3", - "@algolia/transporter": "4.8.3" + "@algolia/cache-browser-local-storage": "4.8.6", + "@algolia/cache-common": "4.8.6", + "@algolia/cache-in-memory": "4.8.6", + "@algolia/client-account": "4.8.6", + "@algolia/client-analytics": "4.8.6", + "@algolia/client-common": "4.8.6", + "@algolia/client-recommendation": "4.8.6", + "@algolia/client-search": "4.8.6", + "@algolia/logger-common": "4.8.6", + "@algolia/logger-console": "4.8.6", + "@algolia/requester-browser-xhr": "4.8.6", + "@algolia/requester-common": "4.8.6", + "@algolia/requester-node-http": "4.8.6", + "@algolia/transporter": "4.8.6" } }, "algoliasearch-helper": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.3.4.tgz", - "integrity": "sha512-1Ts2XcgGdjGlDrp3v6zbY8VW+X9+jJ5rBmtPBmXOQLd4b5t/LpJlaBdxoAnlMfVFjywP7KSAdmyFUNNYVHDyRQ==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.4.4.tgz", + "integrity": "sha512-OjyVLjykaYKCMxxRMZNiwLp8CS310E0qAeIY2NaublcmLAh8/SL19+zYHp7XCLtMem2ZXwl3ywMiA32O9jszuw==", "requires": { "events": "^1.1.1" }, @@ -11653,27 +11493,27 @@ "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, "react-instantsearch-core": { - "version": "6.8.2", - "resolved": "https://registry.npmjs.org/react-instantsearch-core/-/react-instantsearch-core-6.8.2.tgz", - "integrity": "sha512-UdAjcNIXb2mSECEDS/2XuB4W6rcbnph1NjJBUpY5TLLzSCdKXNTzS2PxF5hkdeuY0L/m/hvDQX6YqxV28PqKLA==", + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/react-instantsearch-core/-/react-instantsearch-core-6.10.3.tgz", + "integrity": "sha512-7twp3OJrPGTFpyXwjJNeOTbQw7RTv+0cUyKkXR9njEyLdXKcPWfpeBirXfdQHjYIHEY2b0V2Vom1B9IHSDSUtQ==", "requires": { "@babel/runtime": "^7.1.2", - "algoliasearch-helper": "^3.1.0", - "prop-types": "^15.5.10", + "algoliasearch-helper": "^3.4.3", + "prop-types": "^15.6.2", "react-fast-compare": "^3.0.0" } }, "react-instantsearch-dom": { - "version": "6.8.2", - "resolved": "https://registry.npmjs.org/react-instantsearch-dom/-/react-instantsearch-dom-6.8.2.tgz", - "integrity": "sha512-d6YBsjW/aF3qzul7qqUV/KuzEFPVxlAZm3QhREPqMvOyrPTnG5itZZBLe7sFm9OKJ/8shR4TyNp3hb94as7COg==", + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/react-instantsearch-dom/-/react-instantsearch-dom-6.10.3.tgz", + "integrity": "sha512-kxc6IEruxJrc7O9lsLV5o4YK/RkGt3l7D1Y51JfmYkgeLuQHApwgcy/TAIoSN7wfR/1DONFbX8Y5VhU9Wqh87Q==", "requires": { "@babel/runtime": "^7.1.2", - "algoliasearch-helper": "^3.1.0", + "algoliasearch-helper": "^3.4.3", "classnames": "^2.2.5", - "prop-types": "^15.5.10", + "prop-types": "^15.6.2", "react-fast-compare": "^3.0.0", - "react-instantsearch-core": "^6.8.2" + "react-instantsearch-core": "^6.10.3" } }, "react-is": { diff --git a/website/package.json b/website/package.json index 572621182..e1e2bd44d 100644 --- a/website/package.json +++ b/website/package.json @@ -7,16 +7,18 @@ "@hashicorp/mktg-global-styles": "2.1.0", "@hashicorp/nextjs-scripts": "16.2.0", "@hashicorp/react-button": "4.0.0", - "@hashicorp/react-docs-page": "10.9.4-alpha.18", + "@hashicorp/react-docs-page": "11.0.1", "@hashicorp/react-hashi-stack-menu": "^1.1.0", "@hashicorp/react-head": "1.1.6", "@hashicorp/react-inline-svg": "5.0.0", "@hashicorp/react-markdown-page": "^0.1.0", "@hashicorp/react-product-downloader": "4.0.2", - "@hashicorp/react-search": "^3.0.0", + "@hashicorp/react-search": "^4.2.0", "@hashicorp/react-section-header": "4.0.0", "@hashicorp/react-subnav": "7.1.0", "@hashicorp/react-vertical-text-block-list": "4.0.1", + "adm-zip": "^0.5.4", + "gray-matter": "^4.0.2", "next": "10.0.6", "next-mdx-remote": "1.0.1", "next-remote-watch": "0.3.0", diff --git a/website/pages/docs/[[...page]].jsx b/website/pages/docs/[[...page]].jsx index fe7d637f4..940086f9f 100644 --- a/website/pages/docs/[[...page]].jsx +++ b/website/pages/docs/[[...page]].jsx @@ -12,12 +12,12 @@ import { const BASE_ROUTE = 'docs' const NAV_DATA = 'data/docs-nav-data.json' const CONTENT_DIR = 'content/docs' -// override default "main" value for branch for "edit on this page" -const MAIN_BRANCH = 'master' +const PRODUCT = { name: productName, slug: productSlug } // add remote plugin docs loading const OPTIONS = { remotePluginsFile: 'data/docs-remote-plugins.json', additionalComponents: { PluginTierLabel }, + mainBranch: 'master', } function DocsLayout({ isDevMissingRemotePlugins, ...props }) { @@ -49,8 +49,7 @@ function DocsLayout({ isDevMissingRemotePlugins, ...props }) { @@ -67,6 +66,7 @@ export async function getStaticProps({ params }) { NAV_DATA, CONTENT_DIR, params, + PRODUCT, OPTIONS ) return { props } diff --git a/website/pages/guides/[[...page]].jsx b/website/pages/guides/[[...page]].jsx index 3eef1397e..3c90c1d45 100644 --- a/website/pages/guides/[[...page]].jsx +++ b/website/pages/guides/[[...page]].jsx @@ -10,15 +10,12 @@ import { const BASE_ROUTE = 'guides' const NAV_DATA = 'data/guides-nav-data.json' const CONTENT_DIR = 'content/guides' +const MAIN_BRANCH = 'master' +const PRODUCT = { name: productName, slug: productSlug } export default function GuidesLayout(props) { return ( - + ) } @@ -28,6 +25,12 @@ export async function getStaticPaths() { } export async function getStaticProps({ params }) { - const props = await generateStaticProps(NAV_DATA, CONTENT_DIR, params) + const props = await generateStaticProps( + NAV_DATA, + CONTENT_DIR, + params, + PRODUCT, + { mainBranch: MAIN_BRANCH } + ) return { props } } diff --git a/website/pages/intro/[[...page]].jsx b/website/pages/intro/[[...page]].jsx index 7d45b949a..7a3a65c33 100644 --- a/website/pages/intro/[[...page]].jsx +++ b/website/pages/intro/[[...page]].jsx @@ -10,15 +10,12 @@ import { const BASE_ROUTE = 'intro' const NAV_DATA = 'data/intro-nav-data.json' const CONTENT_DIR = 'content/intro' +const MAIN_BRANCH = 'master' +const PRODUCT = { name: productName, slug: productSlug } export default function IntroLayout(props) { return ( - + ) } @@ -28,6 +25,12 @@ export async function getStaticPaths() { } export async function getStaticProps({ params }) { - const props = await generateStaticProps(NAV_DATA, CONTENT_DIR, params) + const props = await generateStaticProps( + NAV_DATA, + CONTENT_DIR, + params, + PRODUCT, + { mainBranch: MAIN_BRANCH } + ) return { props } } From 26a572270d1bf36355b690f81121584cb56c6fd4 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 18 Mar 2021 12:25:36 -0400 Subject: [PATCH 03/19] website: add github action to flag plugin-docs issues --- .github/workflows/check-plugin-docs.js | 63 +++++++++++++++++++++++++ .github/workflows/check-plugin-docs.yml | 27 +++++++++++ 2 files changed, 90 insertions(+) create mode 100644 .github/workflows/check-plugin-docs.js create mode 100644 .github/workflows/check-plugin-docs.yml diff --git a/.github/workflows/check-plugin-docs.js b/.github/workflows/check-plugin-docs.js new file mode 100644 index 000000000..ef4150fec --- /dev/null +++ b/.github/workflows/check-plugin-docs.js @@ -0,0 +1,63 @@ +const fs = require("fs"); +const path = require("path"); +const fetchPluginDocs = require("../../website/components/remote-plugin-docs/utils/fetch-plugin-docs"); + +const COLOR_RESET = "\x1b[0m"; +const COLOR_GREEN = "\x1b[32m"; +const COLOR_BLUE = "\x1b[34m"; +const COLOR_RED = "\x1b[31m"; + +async function checkPluginDocs() { + const failureMessages = []; + const pluginsPath = "website/data/docs-remote-plugins.json"; + const pluginsFile = fs.readFileSync(path.join(process.cwd(), pluginsPath)); + const pluginEntries = JSON.parse(pluginsFile); + const entriesCount = pluginEntries.length; + console.log(`\nResolving plugin docs from ${entriesCount} repositories …`); + for (var i = 0; i < entriesCount; i++) { + const pluginEntry = pluginEntries[i]; + const { title, repo, version } = pluginEntry; + console.log(`\n${COLOR_BLUE}${repo}${COLOR_RESET} | ${title}`); + console.log(`Fetching docs from release "${version}" …`); + try { + const docsMdxFiles = await fetchPluginDocs({ repo, tag: version }); + const mdxFilesByComponent = docsMdxFiles.reduce((acc, mdxFile) => { + const componentType = mdxFile.filePath.split("/")[1]; + if (!acc[componentType]) acc[componentType] = []; + acc[componentType].push(mdxFile); + return acc; + }, {}); + console.log(`${COLOR_GREEN}Found valid docs:${COLOR_RESET}`); + Object.keys(mdxFilesByComponent).forEach((component) => { + const componentFiles = mdxFilesByComponent[component]; + console.log(` ${component}`); + componentFiles.forEach(({ filePath }) => { + const pathFromComponent = filePath.split("/").slice(2).join("/"); + console.log(` ├── ${pathFromComponent}`); + }); + }); + } catch (err) { + console.log(`${COLOR_RED}${err}${COLOR_RESET}`); + failureMessages.push(`\n${COLOR_RED}× ${repo}: ${COLOR_RESET}${err}`); + } + } + + if (failureMessages.length === 0) { + console.log( + `\n---\n\n${COLOR_GREEN}Summary: Successfully resolved all plugin docs.` + ); + pluginEntries.forEach((e) => + console.log(`${COLOR_GREEN}✓ ${e.repo}${COLOR_RESET}`) + ); + console.log(""); + } else { + console.log( + `\n---\n\n${COLOR_RED}Summary: Failed to fetch docs for ${failureMessages.length} plugin(s):` + ); + failureMessages.forEach((err) => console.log(err)); + console.log(""); + process.exit(1); + } +} + +checkPluginDocs(); diff --git a/.github/workflows/check-plugin-docs.yml b/.github/workflows/check-plugin-docs.yml new file mode 100644 index 000000000..7b3303c9a --- /dev/null +++ b/.github/workflows/check-plugin-docs.yml @@ -0,0 +1,27 @@ +# +# This GitHub action checks plugin repositories for valid docs. +# +# This provides a quick assessment on PRs of whether +# there might be issues with docs in plugin repositories. +# +# This is intended to help debug Vercel build issues, which +# may or may not be related to docs in plugin repositories. + +name: "website: Check plugin docs" +on: + pull_request: + paths: + - "website/**" + +jobs: + check-plugin-docs: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Node + uses: actions/setup-node@v1 + - name: Install Dependencies + run: npm i isomorphic-unfetch adm-zip gray-matter + - name: Fetch and validate plugin docs + run: node .github/workflows/check-plugin-docs.js From e68e736b6ccf6e730eb637f65ae747c58372e37e Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 18 Mar 2021 12:25:49 -0400 Subject: [PATCH 04/19] website: add indexing for plugin docs content --- website/scripts/index_search_content.js | 96 ++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/website/scripts/index_search_content.js b/website/scripts/index_search_content.js index f853b4ddf..b3f2f7cab 100644 --- a/website/scripts/index_search_content.js +++ b/website/scripts/index_search_content.js @@ -1,3 +1,95 @@ -const { indexDocsContent } = require('@hashicorp/react-search/tools') +require('dotenv').config() +const fs = require('fs') +const path = require('path') +const { + indexContent, + getDocsSearchObject, +} = require('@hashicorp/react-search/tools') +const resolveNavData = require('../components/remote-plugin-docs/utils/resolve-nav-data') -indexDocsContent() +// Run indexing +indexContent({ getSearchObjects }) + +async function getSearchObjects() { + // Resolve /docs, /guides, and /intro nav data, which + // corresponds to all the content we will actually render + // This avoids indexing non-rendered content, and partials. + // Fetch objects for `docs` content + async function fetchDocsObjects() { + const navFile = 'data/docs-nav-data.json' + const contentDir = 'content/docs' + const opts = { remotePluginsFile: 'data/docs-remote-plugins.json' } + const navData = await resolveNavData(navFile, contentDir, opts) + return await searchObjectsFromNavData(navData, 'docs') + } + // Fetch objects for `guides` content + async function fetchGuidesObjects() { + const navFile = 'data/guides-nav-data.json' + const contentDir = 'content/guides' + const navData = await resolveNavData(navFile, contentDir) + return await searchObjectsFromNavData(navData, 'guides') + } + // Fetch objects for `intro` content + async function fetchIntroObjects() { + const navFile = 'data/intro-nav-data.json' + const contentDir = 'content/intro' + const navData = await resolveNavData(navFile, contentDir) + return await searchObjectsFromNavData(navData, 'intro') + } + // Collect, flatten and return the collected search objects + const searchObjects = ( + await Promise.all([ + fetchDocsObjects(), + fetchGuidesObjects(), + fetchIntroObjects(), + ]) + ).reduce((acc, array) => acc.concat(array), []) + return searchObjects +} + +/** + * Given navData, return a flat array of search objects + * for each content file referenced in the navData nodes + * @param {Object[]} navData - an array of nav-data nodes, as detailed in [mktg-032](https://docs.google.com/document/d/1kYvbyd6njHFSscoE1dtDNHQ3U8IzaMdcjOS0jg87rHg) + * @param {string} baseRoute - the base route where the navData will be rendered. For example, "docs". + * @returns {Object[]} - an array of searchObjects to pass to Algolia. Must include an objectID property. See https://www.algolia.com/doc/api-reference/api-methods/add-objects/?client=javascript#examples. + */ +async function searchObjectsFromNavData(navData, baseRoute = '') { + const searchObjectsFromNodes = await Promise.all( + navData.map((n) => searchObjectsFromNavNode(n, baseRoute)) + ) + const flattenedSearchObjects = searchObjectsFromNodes.reduce( + (acc, searchObjects) => acc.concat(searchObjects), + [] + ) + return flattenedSearchObjects +} + +/** + * Given a single navData node, return a flat array of search objects. + * For "leaf" nodes, this will yield an array with a single object. + * For "branch" nodes, this may yield an array with zero or more search objects. + * For all other nodes, this will yield an empty array. + * @param {object} node - a nav-data nodes, as detailed in [mktg-032](https://docs.google.com/document/d/1kYvbyd6njHFSscoE1dtDNHQ3U8IzaMdcjOS0jg87rHg) + * @param {string} baseRoute - the base route where the navData will be rendered. For example, "docs". + * @returns {Object[]} - an array of searchObjects to pass to Algolia. Must include an objectID property. See https://www.algolia.com/doc/api-reference/api-methods/add-objects/?client=javascript#examples. + */ +async function searchObjectsFromNavNode(node, baseRoute) { + // If this is a node, build a search object + if (node.path) { + // Fetch the MDX file content + const fileString = node.filePath + ? fs.readFileSync(path.join(process.cwd(), node.filePath), 'utf8') + : node.remoteFile.fileString + const searchObject = await getDocsSearchObject( + path.join(baseRoute, node.path), + fileString + ) + return searchObject + } + // If this is a branch, recurse + if (node.routes) return await searchObjectsFromNavData(node.routes, baseRoute) + // Otherwise, return an empty array + // (for direct link nodes, divider nodes) + return [] +} From 9e03647ad78a2210b3baa510819c882e4c9ffa17 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 18 Mar 2021 12:26:03 -0400 Subject: [PATCH 05/19] website: update readme to reflect revised nav-data and plugin docs --- website/README.md | 232 +++++++++++++++++++++++++++++++++------------- 1 file changed, 169 insertions(+), 63 deletions(-) diff --git a/website/README.md b/website/README.md index 8117006c4..429e06819 100644 --- a/website/README.md +++ b/website/README.md @@ -238,14 +238,22 @@ $ terraform apply - - + ## Editing Navigation Sidebars -The structure of the sidebars are controlled by files in the [`/data` directory](data). For example, [this file](data/docs-navigation.js) controls the **docs** sidebar. Within the `data` folder, any file with `-navigation` after it controls the navigation for the given section. +The structure of the sidebars are controlled by files in the [`/data` directory](data). For example, [data/docs-nav-data.json](data/docs-nav-data.json) controls the **docs** sidebar. Within the `data` folder, any file with `-nav-data` after it controls the navigation for the given section. -The sidebar uses a simple recursive data structure to represent _files_ and _directories_. A file is represented by a string, and a directory is represented by an object. The sidebar is meant to reflect the structure of the docs within the filesystem while also allowing custom ordering. Let's look at an example. First, here's our example folder structure: +The sidebar uses a simple recursive data structure to represent _files_ and _directories_. The sidebar is meant to reflect the structure of the docs within the filesystem while also allowing custom ordering. Let's look at an example. First, here's our example folder structure: ```text . @@ -259,36 +267,55 @@ The sidebar uses a simple recursive data structure to represent _files_ and _dir │   └── nested-file.mdx ``` -Here's how this folder structure could be represented as a sidebar navigation, in this example it would be the file `website/data/docs-navigation.js`: +Here's how this folder structure could be represented as a sidebar navigation, in this example it would be the file `website/data/docs-nav-data.json`: -```js -export default { - category: 'directory', - content: [ - 'file', - 'another-file', - { - category: 'nested-directory', - content: ['nested-file'], - }, - ], -} +```json +[ + { + "title": "Directory", + "routes": [ + { + "title": "Overview", + "path": "directory" + }, + { + "title": "File", + "path": "directory/file" + }, + { + "title": "Another File", + "path": "directory/another-file" + }, + { + "title": "Nested Directory", + "routes": [ + { + "title": "Overview", + "path": "directory/nested-directory" + }, + { + "title": "Nested File", + "path": "directory/nested-directory/nested-file" + } + ] + } + ] + } +] ``` -- `category` values will be **directory names** within the `pages/
` directory -- `content` values will be **file names** within their appropriately nested directory - A couple more important notes: -- Within this data structure, ordering does not matter, but hierarchy does. So while you could put `file` and `another-file` in any order, or even leave one or both of them out, you could not decide to un-nest the `nested-directory` object without also un-nesting it in the filesystem. -- The `sidebar_title` frontmatter property on each `mdx` page is responsible for displaying the human-readable page name in the navigation. -- _By default_, every directory/category must have an `index.mdx` file. This file will be automatically added to the navigation as "Overview", and its `sidebar_title` property will set the human-readable name of the entire category. +- Within this data structure, ordering is flexible, but hierarchy is not. The structure of the sidebar must correspond to the structure of the content directory. So while you could put `file` and `another-file` in any order in the sidebar, or even leave one or both of them out, you could not decide to un-nest the `nested-directory` object without also un-nesting it in the filesystem. +- The `title` property on each node in the `nav-data` tree is the human-readable name in the navigation. +- The `path` property on each leaf node in the `nav-data` tree is the URL path where the `.mdx` document will be rendered, and the +- Note that "index" files must be explicitly added. These will be automatically resolved, so the `path` value should be, as above, `directory` rather than `directory/index`. A common convention is to set the `title` of an "index" node to be `"Overview"`. Below we will discuss a couple of more unusual but still helpful patterns. ### Index-less Categories -Sometimes you may want to include a category but not have a need for an index page for the category. This can be accomplished, but a human-readable category name needs to be set manually, since the category name is normally pulled from the `sidebar_title` property of the index page. Here's an example of how an index-less category might look: +Sometimes you may want to include a category but not have a need for an index page for the category. This can be accomplished, but as with other branch and leaf nodes, a human-readable `title` needs to be set manually. Here's an example of how an index-less category might look: ```text . @@ -297,36 +324,95 @@ Sometimes you may want to include a category but not have a need for an index pa │   └── file.mdx ``` -```js -// website/data/docs-navigation.js -export default { - category: 'indexless-category', - name: 'Indexless Category', - content: ['file'], -} +```json +// website/data/docs-nav-data.json +[ + { + "title": "Indexless Category", + "routes": [ + { + "title": "File", + "path": "indexless-category/file" + } + ] + } +] ``` -The addition of the `name` property to a category object is all it takes to be able to skip the index file. - ### Custom or External Links Sometimes you may have a need to include a link that is not directly to a file within the docs hierarchy. This can also be supported using a different pattern. For example: -```js -export default { - category: 'directory', - content: [ - 'file', - 'another-file', - { title: 'Tao of HashiCorp', href: 'https://www.hashicorp.com/tao-of-hashicorp' } - } - ] -} +```json +[ + { + "name": "Directory", + "routes": [ + { + "title": "File", + "path": "directory/file" + }, + { + "title": "Another File", + "path": "directory/another-file" + }, + { + "title": "Tao of HashiCorp", + "href": "https://www.hashicorp.com/tao-of-hashicorp" + } + ] + } +] ``` If the link provided in the `href` property is external, it will display a small icon indicating this. If it's internal, it will appear the same way as any other direct file link. - +### Plugin Docs + +Plugin documentation may be located within the `packer` repository, or split out into separate `packer-plugin-` repositories. For plugin docs within the `packer` repository, the process for authoring files and managing sidebar data is identical to the process for other documentation. + +For plugins in separate repositories, additional configuration is required. + +#### Setting up remote plugin docs + +Some setup is required to include docs from remote plugin repositories on the [packer.io/docs](https://www.packer.io/docs) site. + +1. The plugin repository needs to include a `docs.zip` asset in its release +2. The `packer` repository must have a corresponding entry in `website/data/docs-remote-plugins.json` which points to the plugin repository. + +The `docs.zip` release asset is expected to be generated as part of the standard release process for `packer-plugin-*` repositories. Additional details on this process can be found in [the `packer-plugin-scaffolding` `README`](https://github.com/hashicorp/packer-plugin-scaffolding#registering-documentation-on-packerio). + +The `docs-remote-plugins.json` file contains an array of entries. Each entry points to a plugin repository. The `{ title, path, repo, tag }` properties are required for each entry, all other properties are optional. + +```json5 +[ + { + // ALL FIELDS ARE REQUIRED. + // "title" sets the human-readable title shown in navigation + title: 'Scaffolding', + // "path" sets the URL subpath under the component URL (eg `docs/builders`) + path: 'scaffolding', + // "repo" points to the plugin repo, in the format "organization/repo-name" + // if the organization == hashicorp, the plugin docs will be labelled "official". + // for all other organizations or users, plugin docs will be labelled "community". + repo: 'hashicorp/packer-plugin-scaffolding', + // "version" is used to fetch "docs.zip" from the matching tagged release. + // version: "latest" is permitted, but please be aware that it + // may fetch incompatible or unintended versions of plugin docs. + // if version is NOT "latest", and if "docs.zip" is unavailable, then + // we fall back to fetching docs from the source "{version}.zip" + version: 'v0.0.5', + }, +] +``` + +#### Updating remote plugin docs + +Documentation from plugin repositories is fetched and rendered every time the Packer website builds. So, to update plugin documentation on the live site: + +1. In the plugin repository, publish a new release that includes a `docs.zip` release asset +2. In the `packer` repository, update `website/data/docs-remote-plugins.json` to ensure the corresponding entry points to the correct release `version` (which should correspond to the release's tag name). This may not be necessary if the `version` is set to `"latest"`. +3. Rebuild the website. This will happen automatically on commits to `stable-website`. In exceptional cases, the site can also be [manually re-deployed through Vercel](https://vercel.com/hashicorp/packer). @@ -374,8 +460,18 @@ You may customize the parameters in any way you'd like. To remove a prerelease f - - + ## Link Validation @@ -406,7 +502,7 @@ There are a couple important caveats with redirects. First, redirects are applie Second, redirects do not apply to client-side navigation. By default, all links in the navigation and docs sidebar will navigate purely on the client side, which makes navigation through the docs significantly faster, especially for those with low-end devices and/or weak internet connections. In the future, we plan to convert all internal links within docs pages to behave this way as well. This means that if there is a link on this website to a given piece of content that has changed locations in some way, we need to also _directly change existing links to the content_. This way, if a user clicks a link that navigates on the client side, or if they hit the url directly and the page renders from the server side, either one will work perfectly. -Let's look at an example. Say you have a page called `/docs/foo` which needs to be moved to `/docs/nested/foo`. Additionally, this is a page that has been around for a while and we know there are links into `/docs/foo.html` left over from our previous website structure. First, we move the page, then adjust the docs sidenav, in `data/docs-navigation.js`. Find the category the page is in, and move it into the appropriate subcategory. Next, we add to `_redirects` as such: +Let's look at an example. Say you have a page called `/docs/foo` which needs to be moved to `/docs/nested/foo`. Additionally, this is a page that has been around for a while and we know there are links into `/docs/foo.html` left over from our previous website structure. First, we move the page, then adjust the docs sidenav, in `data/docs-nav-data.json`. Find the category the page is in, and move it into the appropriate subcategory. Next, we add to `_redirects` as such: ``` /foo /nested/foo 301! @@ -415,26 +511,36 @@ Let's look at an example. Say you have a page called `/docs/foo` which needs to Finally, we run a global search for internal links to `/foo`, and make sure to adjust them to be `/nested/foo` - this is to ensure that client-side navigation still works correctly. _Adding a redirect alone is not enough_. -One more example - let's say that content is being moved to an external website. A common example is guides moving to `learn.hashicorp.com`. In this case, we take all the same steps, except that we need to make a different type of change to the `docs-navigation` file. If previously the structure looked like: +One more example - let's say that content is being moved to an external website. A common example is guides moving to `learn.hashicorp.com`. In this case, we take all the same steps, except that we need to make a different type of change to the `docs-nav-data` file. If previously the structure looked like: -```js -{ - category: 'docs', - content: [ - 'foo' - ] -} +```json +[ + { + "name": "Docs", + "routes": [ + { + "title": "Foo", + "path": "docs/foo" + } + ] + } +] ``` -If we no longer want the link to be in the side nav, we can simply remove it. If we do still want the link in the side nav, but pointing to an external destnation, we need to slightly change the structure as such: +If we no longer want the link to be in the side nav, we can simply remove it. If we do still want the link in the side nav, but pointing to an external destination, we need to slightly change the structure as such: -```js -{ - category: 'docs', - content: [ - { title: 'Foo Title', href: 'https://learn.hashicorp.com//foo' } - ] -} +```json +[ + { + "name": "Docs", + "routes": [ + { + "title": "Foo", + "href": "https://learn.hashicorp.com//foo" + } + ] + } +] ``` As the majority of items in the side nav are internal links, the structure makes it as easy as possible to represent these links. This alternate syntax is the most concise manner than an external link can be represented. External links can be used anywhere within the docs sidenav. From bece5c3c696607648333eee20db011a7ad13b854 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 18 Mar 2021 12:26:16 -0400 Subject: [PATCH 06/19] website: fix minor style issue in PluginTier component --- website/components/plugin-tier-label/style.module.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/website/components/plugin-tier-label/style.module.css b/website/components/plugin-tier-label/style.module.css index 4ebe4a397..d28e0f6c1 100644 --- a/website/components/plugin-tier-label/style.module.css +++ b/website/components/plugin-tier-label/style.module.css @@ -12,7 +12,9 @@ to match Terraform Registry tier labels. display: inline-flex; margin: 0; padding: 1px 8px 2px 8px; - vertical-align: baseline; + position: relative; + top: -1px; + vertical-align: bottom; /* variations */ &[data-tier='official'] { @@ -52,4 +54,3 @@ search bar present on docs pages */ } } } - From 203900e403ab99e65516d9680bb2342e299ccbe4 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 18 Mar 2021 12:26:27 -0400 Subject: [PATCH 07/19] website: delete unused plugin-docs utilities --- .../utils/fetch-github-file.js | 71 -------- .../utils/merge-remote-plugins.js | 166 ------------------ 2 files changed, 237 deletions(-) delete mode 100644 website/components/remote-plugin-docs/utils/fetch-github-file.js delete mode 100644 website/components/remote-plugin-docs/utils/merge-remote-plugins.js diff --git a/website/components/remote-plugin-docs/utils/fetch-github-file.js b/website/components/remote-plugin-docs/utils/fetch-github-file.js deleted file mode 100644 index e12a2738d..000000000 --- a/website/components/remote-plugin-docs/utils/fetch-github-file.js +++ /dev/null @@ -1,71 +0,0 @@ -const fetch = require('isomorphic-unfetch') - -const GITHUB_API_TOKEN = process.env.GITHUB_API_TOKEN - -async function githubQuery(body, token) { - const result = await fetch('https://api.github.com/graphql', { - method: 'POST', - headers: { - Authorization: `bearer ${token}`, - ContentType: 'application/json', - }, - body: JSON.stringify(body), - }) - return await result.json() -} - -// Fetch a file from GitHub using the GraphQL API -async function getGithubFile({ repo, branch, filePath }) { - const [repo_owner, repo_name] = repo.split('/') - // Set up the GraphQL query - // (usually we can keep this in a separate file, and rely on a - // plaintext loader we've set up in our NextJS config, but we need - // to fetch remote content when indexing it, which happens outside - // NextJS, so unfortunately it seems this has to be inlined) - const query = ` -query($repo_name: String!, $repo_owner: String!, $object_expression: String!) { - repository(name: $repo_name, owner: $repo_owner) { - object(expression: $object_expression) { - ... on Blob { - text - } - } - } -} -` - // Set variables - const variables = { - repo_name, - repo_owner, - object_expression: `${branch}:${filePath}`, - } - // Query the GitHub API, and parse the navigation data - const result = await githubQuery({ query, variables }, GITHUB_API_TOKEN) - try { - const fileText = result.data.repository.object.text - return [null, fileText] - } catch (e) { - const errorMsg = `Could not fetch remote file text from "${ - variables.object_expression - }" in "${repo_owner}/${repo_name}". Received instead:\n\n${JSON.stringify( - result, - null, - 2 - )}` - return [errorMsg, null] - } -} - -function memoize(method) { - let cache = {} - - return async function () { - let args = JSON.stringify(arguments[0]) - if (!cache[args]) { - cache[args] = method.apply(this, arguments) - } - return cache[args] - } -} - -module.exports = memoize(getGithubFile) diff --git a/website/components/remote-plugin-docs/utils/merge-remote-plugins.js b/website/components/remote-plugin-docs/utils/merge-remote-plugins.js deleted file mode 100644 index 41d0d7139..000000000 --- a/website/components/remote-plugin-docs/utils/merge-remote-plugins.js +++ /dev/null @@ -1,166 +0,0 @@ -const path = require('path') -const fetchGithubFile = require('./fetch-github-file') - -const COMPONENT_TYPES = [ - 'builders', - 'datasources', - 'post-processors', - 'provisioners', -] - -async function gatherRemotePlugins(pluginsData, navData, isDev = true) { - const allPluginData = await Promise.all( - pluginsData.map(async (pluginEntry) => { - const componentEntries = await Promise.all( - COMPONENT_TYPES.map(async (type) => { - const routes = await gatherPluginBranch(pluginEntry, type) - if (!routes) return false - const isSingleLeaf = - routes.length === 1 && typeof routes[0].path !== 'undefined' - const navData = isSingleLeaf - ? { ...routes[0], path: path.join(type, pluginEntry.path) } - : { title: pluginEntry.title, routes } - return { type, navData } - }) - ) - const validComponents = componentEntries.filter(Boolean) - if (validComponents.length === 0) { - const errMsg = `Could not fetch any component documentation for remote plugin from ${pluginEntry.repo}. This may be a GitHub credential issue at build time, or it may be an issue with missing docs in the source repository. Please ensure you have a valid GITHUB_API_TOKEN set in .env.local at the root of the project.` - if (isDev) { - console.warn(errMsg) - } else { - throw new Error(errMsg) - } - } - return validComponents - }) - ) - - const allPluginsByType = allPluginData.reduce((acc, pluginData) => { - pluginData.forEach((p) => { - const { type, navData } = p - if (!acc[type]) acc[type] = [] - acc[type].push(navData) - }) - return acc - }, {}) - - const navDataWithPlugins = navData.slice().map((n) => { - // we only care about top-level NavBranch nodes - if (!n.routes) return n - // for each component type, check if this NavBranch - // is the parent route for that type - for (var i = 0; i < COMPONENT_TYPES.length; i++) { - const type = COMPONENT_TYPES[i] - const isTypeRoute = n.routes.filter((nn) => nn.path === type).length > 0 - if (isTypeRoute) { - const pluginsOfType = allPluginsByType[type] - if (!pluginsOfType || pluginsOfType.length == 0) return n - // if this NavBranch is the parent route for the type, - // then append all remote plugins of this type to the - // NavBranch's child routes - const routesWithPlugins = n.routes.slice().concat(pluginsOfType) - // console.log(JSON.stringify(routesWithPlugins, null, 2)) - // Also, sort the child routes so the order is alphabetical - routesWithPlugins.sort((a, b) => { - // (exception: "Overview" comes first) - if (a.title == 'Overview') return -1 - if (b.title === 'Overview') return 1 - // (exception: "Community-Supported" comes last) - if (a.title == 'Community-Supported') return 1 - if (b.title === 'Community-Supported') return -1 - // (exception: "Custom" comes second-last) - if (a.title == 'Custom') return 1 - if (b.title === 'Custom') return -1 - return a.title < b.title ? -1 : a.title > b.title ? 1 : 0 - }) - // return n - return { ...n, routes: routesWithPlugins } - } - } - return n - }) - - return navDataWithPlugins -} - -async function gatherPluginBranch(pluginEntry, component) { - const artifactDir = pluginEntry.artifactDir || '.docs-artifacts' - const branch = pluginEntry.branch || 'main' - const navDataFilePath = `${artifactDir}/${component}/nav-data.json` - const [err, fileResult] = await fetchGithubFile({ - repo: pluginEntry.repo, - branch, - filePath: navDataFilePath, - }) - // If one component errors, that's expected - we try all components. - // We'll check one level up to see if ALL components fail. - if (err) return false - const navData = JSON.parse(fileResult) - const withPrefixedPath = await prefixNavDataPath( - navData, - { - repo: pluginEntry.repo, - branch, - componentArtifactsDir: path.join('.docs-artifacts', component), - }, - path.join(component, pluginEntry.path) - ) - // Add plugin tier - // Parse the plugin tier - const pluginOwner = pluginEntry.repo.split('/')[0] - const pluginTier = pluginOwner === 'hashicorp' ? 'official' : 'community' - const withPluginTier = addPluginTier(withPrefixedPath, pluginTier) - // Return the augmented navData - return withPluginTier -} - -function addPluginTier(navData, pluginTier) { - return navData.slice().map((navNode) => { - if (typeof navNode.path !== 'undefined') { - return { ...navNode, pluginTier } - } - if (navNode.routes) { - return { ...navNode, routes: addPluginTier(navNode.routes, pluginTier) } - } - return navNode - }) -} - -async function prefixNavDataPath( - navData, - { repo, branch, componentArtifactsDir }, - parentPath -) { - return await Promise.all( - navData.slice().map(async (navNode) => { - if (typeof navNode.path !== 'undefined') { - const prefixedPath = path.join(parentPath, navNode.path) - const remoteFile = { - repo, - branch, - filePath: path.join(componentArtifactsDir, navNode.filePath), - } - const withPrefixedRoute = { - ...navNode, - path: prefixedPath, - remoteFile: remoteFile, - } - delete withPrefixedRoute.filePath - return withPrefixedRoute - } - if (navNode.routes) { - const prefixedRoutes = await prefixNavDataPath( - navNode.routes, - { repo, branch, componentArtifactsDir }, - parentPath - ) - const withPrefixedRoutes = { ...navNode, routes: prefixedRoutes } - return withPrefixedRoutes - } - return navNode - }) - ) -} - -module.exports = gatherRemotePlugins From 618a3d42c63f78300fd8cb2e4a201d1d13a59398 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 18 Mar 2021 12:31:24 -0400 Subject: [PATCH 08/19] website: clarify purpose of sourceUrl --- website/components/remote-plugin-docs/utils/resolve-nav-data.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/components/remote-plugin-docs/utils/resolve-nav-data.js b/website/components/remote-plugin-docs/utils/resolve-nav-data.js index 837feb6b4..d5d21b7e3 100644 --- a/website/components/remote-plugin-docs/utils/resolve-nav-data.js +++ b/website/components/remote-plugin-docs/utils/resolve-nav-data.js @@ -145,7 +145,7 @@ async function resolvePluginEntryDocs(pluginConfigEntry) { const { data: frontmatter } = grayMatter(fileString) const { nav_title, sidebar_title } = frontmatter const title = nav_title || sidebar_title || basename - // construct sourceUrl + // construct sourceUrl (used for "Edit this page" link) const sourceUrl = `https://github.com/${repo}/blob/${version}/${filePath}` // determine pluginTier const pluginOwner = repo.split('/')[0] From 0f8a658a237d69b688167261f9b06d2231daf9e5 Mon Sep 17 00:00:00 2001 From: Zachary Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 18 Mar 2021 15:05:52 -0400 Subject: [PATCH 09/19] website: clarify tag vs version in error msg Co-authored-by: Wilken Rivera --- .../components/remote-plugin-docs/utils/fetch-plugin-docs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/components/remote-plugin-docs/utils/fetch-plugin-docs.js b/website/components/remote-plugin-docs/utils/fetch-plugin-docs.js index d02930ca2..ee4238b3a 100644 --- a/website/components/remote-plugin-docs/utils/fetch-plugin-docs.js +++ b/website/components/remote-plugin-docs/utils/fetch-plugin-docs.js @@ -22,7 +22,7 @@ async function fetchDocsFiles({ repo, tag }) { // then throw an error - we can't resolve the fallback source ZIP // unless we resort to calling the GitHub API, which we do not want to do if (tag === 'latest') { - const err = `Failed to fetch. Could not find "docs.zip" at ${docsZipUrl}. To fall back to parsing docs from "source", please provide a specific tag instead of "${tag}".` + const err = `Failed to fetch. Could not find "docs.zip" at ${docsZipUrl}. To fall back to parsing docs from "source", please provide a specific version tag instead of "${tag}".` return [err, null] } // Else if docs.zip is not present, and we have a specific tag, then From 597dcce2abc5698e5e3b0c87f7d3e0d2bfbd762f Mon Sep 17 00:00:00 2001 From: Zachary Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 18 Mar 2021 15:06:42 -0400 Subject: [PATCH 10/19] website: fix tag vs version reference in README Co-authored-by: Wilken Rivera --- website/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/README.md b/website/README.md index 429e06819..795f67e64 100644 --- a/website/README.md +++ b/website/README.md @@ -382,7 +382,7 @@ Some setup is required to include docs from remote plugin repositories on the [p The `docs.zip` release asset is expected to be generated as part of the standard release process for `packer-plugin-*` repositories. Additional details on this process can be found in [the `packer-plugin-scaffolding` `README`](https://github.com/hashicorp/packer-plugin-scaffolding#registering-documentation-on-packerio). -The `docs-remote-plugins.json` file contains an array of entries. Each entry points to a plugin repository. The `{ title, path, repo, tag }` properties are required for each entry, all other properties are optional. +The `docs-remote-plugins.json` file contains an array of entries. Each entry points to a plugin repository. The `{ title, path, repo, version }` properties are required for each entry, all other properties are optional. ```json5 [ From de12cd318da480a58c609b70d51bb799b6e36aed Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 18 Mar 2021 15:10:02 -0400 Subject: [PATCH 11/19] website: remove outdated comment on plugin config entries --- website/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/README.md b/website/README.md index 795f67e64..3901d3487 100644 --- a/website/README.md +++ b/website/README.md @@ -382,7 +382,7 @@ Some setup is required to include docs from remote plugin repositories on the [p The `docs.zip` release asset is expected to be generated as part of the standard release process for `packer-plugin-*` repositories. Additional details on this process can be found in [the `packer-plugin-scaffolding` `README`](https://github.com/hashicorp/packer-plugin-scaffolding#registering-documentation-on-packerio). -The `docs-remote-plugins.json` file contains an array of entries. Each entry points to a plugin repository. The `{ title, path, repo, version }` properties are required for each entry, all other properties are optional. +The `docs-remote-plugins.json` file contains an array of entries. Each entry points to a plugin repository. The `{ title, path, repo, version }` properties are required for each entry. ```json5 [ From 1d485988ea3ccb13101d930202e2d54a61733d0a Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 18 Mar 2021 15:31:04 -0400 Subject: [PATCH 12/19] website: bump timeout for vercel build polling --- .github/workflows/linkchecker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linkchecker.yml b/.github/workflows/linkchecker.yml index 0eaa2d774..5314f7325 100644 --- a/.github/workflows/linkchecker.yml +++ b/.github/workflows/linkchecker.yml @@ -7,7 +7,7 @@ name: Check markdown links on modified website files jobs: vercel-deployment-poll: runs-on: ubuntu-latest - timeout-minutes: 3 #cancel job if no deployment is found within x minutes + timeout-minutes: 5 #cancel job if no deployment is found within x minutes outputs: url: ${{ steps.waitForVercelPreviewDeployment.outputs.url }} steps: From fb0886b7245245876a76824bfe212c5cc68ee9fa Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Sat, 20 Mar 2021 21:32:13 -0400 Subject: [PATCH 13/19] website: Implement basic validation for plugin docs config --- .github/workflows/check-plugin-docs.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/check-plugin-docs.js b/.github/workflows/check-plugin-docs.js index ef4150fec..16a9495a3 100644 --- a/.github/workflows/check-plugin-docs.js +++ b/.github/workflows/check-plugin-docs.js @@ -20,6 +20,20 @@ async function checkPluginDocs() { console.log(`\n${COLOR_BLUE}${repo}${COLOR_RESET} | ${title}`); console.log(`Fetching docs from release "${version}" …`); try { + const undefinedProps = ["title", "repo", "version", "path"].filter( + (key) => typeof pluginEntry[key] == "undefined" + ); + if (undefinedProps.length > 0) { + throw new Error( + `Failed to validate plugin docs. Undefined configuration properties ${JSON.stringify( + undefinedProps + )} found for "${ + title || pluginEntry.path || repo + }". In "website/data/docs-remote-plugins.json", please ensure the missing properties ${JSON.stringify( + undefinedProps + )} are defined. Additional information on this configuration can be found in "website/README.md".` + ); + } const docsMdxFiles = await fetchPluginDocs({ repo, tag: version }); const mdxFilesByComponent = docsMdxFiles.reduce((acc, mdxFile) => { const componentType = mdxFile.filePath.split("/")[1]; From ce896351b92f7c60a41dacb5c80fc85405f5ee3e Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Sat, 20 Mar 2021 21:37:21 -0400 Subject: [PATCH 14/19] website: temporary change to double-check validation --- website/data/docs-remote-plugins.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/website/data/docs-remote-plugins.json b/website/data/docs-remote-plugins.json index ab7aecb94..8c247cfab 100644 --- a/website/data/docs-remote-plugins.json +++ b/website/data/docs-remote-plugins.json @@ -2,7 +2,6 @@ { "title": "Docker", "path": "docker", - "repo": "hashicorp/packer-plugin-docker", - "version": "v0.0.4" + "repo": "hashicorp/packer-plugin-docker" } ] From 15d467eaf1a1b840f46279c227c4529cd8a68d2d Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Sat, 20 Mar 2021 21:45:12 -0400 Subject: [PATCH 15/19] website: fix outdated comment --- website/components/remote-plugin-docs/server.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/components/remote-plugin-docs/server.js b/website/components/remote-plugin-docs/server.js index 9b00b6f92..6054b78f1 100644 --- a/website/components/remote-plugin-docs/server.js +++ b/website/components/remote-plugin-docs/server.js @@ -31,8 +31,8 @@ async function generateStaticProps( ? remoteFile.fileString : fs.readFileSync(path.join(process.cwd(), filePath), 'utf8') // Construct the githubFileUrl, used for "Edit this page" link - // Note: for custom ".docs-artifacts" directories, the "Edit this page" - // link will lead to the artifact file rather than the "docs" source file + // Note: we expect remote files, such as those used to render plugin docs, + // to have a sourceUrl defined, that points to the file we built from const githubFileUrl = remoteFile ? remoteFile.sourceUrl : `https://github.com/hashicorp/${product.slug}/blob/${mainBranch}/website/${filePath}` From 55012937a92914ad884a35d8dc44c888d5b799e9 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Sat, 20 Mar 2021 21:45:40 -0400 Subject: [PATCH 16/19] website: add comments to duo of plugin docs zip fns --- .../components/remote-plugin-docs/utils/parse-docs-zip.js | 6 ++++++ .../remote-plugin-docs/utils/parse-source-zip.js | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/website/components/remote-plugin-docs/utils/parse-docs-zip.js b/website/components/remote-plugin-docs/utils/parse-docs-zip.js index 69563877e..483065597 100644 --- a/website/components/remote-plugin-docs/utils/parse-docs-zip.js +++ b/website/components/remote-plugin-docs/utils/parse-docs-zip.js @@ -2,6 +2,12 @@ const path = require('path') const AdmZip = require('adm-zip') const validatePluginDocsFiles = require('./validate-plugin-docs-files') +/* + +NOTE: used for default `docs.zip` release assets + +*/ + // Given a response from fetching a docs.zip file, // which is a compressed "docs" folder, // diff --git a/website/components/remote-plugin-docs/utils/parse-source-zip.js b/website/components/remote-plugin-docs/utils/parse-source-zip.js index 07a26ad6a..f33208b7b 100644 --- a/website/components/remote-plugin-docs/utils/parse-source-zip.js +++ b/website/components/remote-plugin-docs/utils/parse-source-zip.js @@ -2,6 +2,13 @@ const path = require('path') const AdmZip = require('adm-zip') const validatePluginDocsFiles = require('./validate-plugin-docs-files') +/* + +NOTE: used for fallback approach, where we parse from +the full release archive + +*/ + // Given a response from fetching a source .zip file, // which contains a "docs" folder, // From c100b56d4488983d5637a876dfffd42c4500bc05 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Sat, 20 Mar 2021 21:50:20 -0400 Subject: [PATCH 17/19] website: clarify error message in plugin config check --- .github/workflows/check-plugin-docs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-plugin-docs.js b/.github/workflows/check-plugin-docs.js index 16a9495a3..96a00a83f 100644 --- a/.github/workflows/check-plugin-docs.js +++ b/.github/workflows/check-plugin-docs.js @@ -25,7 +25,7 @@ async function checkPluginDocs() { ); if (undefinedProps.length > 0) { throw new Error( - `Failed to validate plugin docs. Undefined configuration properties ${JSON.stringify( + `Failed to validate plugin docs config. Undefined configuration properties ${JSON.stringify( undefinedProps )} found for "${ title || pluginEntry.path || repo From 565ca6627cf78adc12c8fec1a01fbc09692921b2 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Sat, 20 Mar 2021 21:51:42 -0400 Subject: [PATCH 18/19] website: revert test of plugin docs config validation --- website/data/docs-remote-plugins.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/data/docs-remote-plugins.json b/website/data/docs-remote-plugins.json index 8c247cfab..ab7aecb94 100644 --- a/website/data/docs-remote-plugins.json +++ b/website/data/docs-remote-plugins.json @@ -2,6 +2,7 @@ { "title": "Docker", "path": "docker", - "repo": "hashicorp/packer-plugin-docker" + "repo": "hashicorp/packer-plugin-docker", + "version": "v0.0.4" } ] From b780da57506c8fc27d340052b23173949ba38b32 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Sat, 20 Mar 2021 21:59:34 -0400 Subject: [PATCH 19/19] website: run plugin docs check also on schedule --- .github/workflows/check-plugin-docs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/check-plugin-docs.yml b/.github/workflows/check-plugin-docs.yml index 7b3303c9a..ec6ac65d5 100644 --- a/.github/workflows/check-plugin-docs.yml +++ b/.github/workflows/check-plugin-docs.yml @@ -12,6 +12,8 @@ on: pull_request: paths: - "website/**" + schedule: + - cron: "45 0 * * *" jobs: check-plugin-docs: