Merge pull request #10764 from zchsh/zs.remote-plugin-zip-approach

website: Implement RFC MKTG-033
This commit is contained in:
Kyle MacDonald 2021-03-22 09:59:38 -04:00 committed by GitHub
commit ef6093c4c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 970 additions and 641 deletions

77
.github/workflows/check-plugin-docs.js vendored Normal file
View File

@ -0,0 +1,77 @@
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 undefinedProps = ["title", "repo", "version", "path"].filter(
(key) => typeof pluginEntry[key] == "undefined"
);
if (undefinedProps.length > 0) {
throw new Error(
`Failed to validate plugin docs config. 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];
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();

29
.github/workflows/check-plugin-docs.yml vendored Normal file
View File

@ -0,0 +1,29 @@
#
# 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/**"
schedule:
- cron: "45 0 * * *"
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

View File

@ -7,7 +7,7 @@ name: Check markdown links on modified website files
jobs: jobs:
vercel-deployment-poll: vercel-deployment-poll:
runs-on: ubuntu-latest 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: outputs:
url: ${{ steps.waitForVercelPreviewDeployment.outputs.url }} url: ${{ steps.waitForVercelPreviewDeployment.outputs.url }}
steps: steps:

View File

@ -238,14 +238,22 @@ $ terraform apply
<!-- END: editing-markdown --> <!-- END: editing-markdown -->
<!-- BEGIN: editing-docs-sidebars --> <!--
<!-- Generated text, do not edit directly -->
NOTE: The "Editing Navigation Sidebars" section is forked from editing-docs-sidebars.
We plan on rolling these changes back into our "readme partials" source once all docs sites
have been transitioned to the JSON navigation format. See MKTG_032 for details:
https://docs.google.com/document/d/1kYvbyd6njHFSscoE1dtDNHQ3U8IzaMdcjOS0jg87rHg/
-->
## Editing Navigation Sidebars ## 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 ```text
. .
@ -259,36 +267,55 @@ The sidebar uses a simple recursive data structure to represent _files_ and _dir
│   └── nested-file.mdx │   └── 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 ```json
export default { [
category: 'directory', {
content: [ "title": "Directory",
'file', "routes": [
'another-file', {
{ "title": "Overview",
category: 'nested-directory', "path": "directory"
content: ['nested-file'], },
}, {
], "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/<section>` directory
- `content` values will be **file names** within their appropriately nested directory
A couple more important notes: 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. - 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 `sidebar_title` frontmatter property on each `mdx` page is responsible for displaying the human-readable page name in the navigation. - The `title` property on each node in the `nav-data` tree is the human-readable 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. - 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. Below we will discuss a couple of more unusual but still helpful patterns.
### Index-less Categories ### 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 ```text
. .
@ -297,36 +324,95 @@ Sometimes you may want to include a category but not have a need for an index pa
│   └── file.mdx │   └── file.mdx
``` ```
```js ```json
// website/data/docs-navigation.js // website/data/docs-nav-data.json
export default { [
category: 'indexless-category', {
name: 'Indexless Category', "title": "Indexless Category",
content: ['file'], "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 ### 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: 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 ```json
export default { [
category: 'directory', {
content: [ "name": "Directory",
'file', "routes": [
'another-file', {
{ title: 'Tao of HashiCorp', href: 'https://www.hashicorp.com/tao-of-hashicorp' } "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. 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.
<!-- END: editing-docs-sidebars --> ### 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, version }` properties are required for each entry.
```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).
<!-- BEGIN: releases --> <!-- BEGIN: releases -->
<!-- Generated text, do not edit directly --> <!-- Generated text, do not edit directly -->
@ -374,8 +460,18 @@ You may customize the parameters in any way you'd like. To remove a prerelease f
<!-- END: releases --> <!-- END: releases -->
<!-- BEGIN: redirects --> <!--
<!-- Generated text, do not edit directly -->
NOTE: The "Redirects" section is forked from redirects.
There are minor changes related to sidebar navigation format changes.
We plan on rolling these changes back into our "readme partials" source once all docs sites
have been transitioned to the JSON navigation format. See MKTG_032 for details:
https://docs.google.com/document/d/1kYvbyd6njHFSscoE1dtDNHQ3U8IzaMdcjOS0jg87rHg/
-->
## Link Validation ## 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. 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! /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_. 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 ```json
{ [
category: 'docs', {
content: [ "name": "Docs",
'foo' "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 ```json
{ [
category: 'docs', {
content: [ "name": "Docs",
{ title: 'Foo Title', href: 'https://learn.hashicorp.com/<product>/foo' } "routes": [
] {
} "title": "Foo",
"href": "https://learn.hashicorp.com/<product>/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. 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.

View File

@ -12,7 +12,9 @@ to match Terraform Registry tier labels.
display: inline-flex; display: inline-flex;
margin: 0; margin: 0;
padding: 1px 8px 2px 8px; padding: 1px 8px 2px 8px;
vertical-align: baseline; position: relative;
top: -1px;
vertical-align: bottom;
/* variations */ /* variations */
&[data-tier='official'] { &[data-tier='official'] {
@ -52,4 +54,3 @@ search bar present on docs pages */
} }
} }
} }

View File

@ -3,13 +3,9 @@ import path from 'path'
import { import {
getNodeFromPath, getNodeFromPath,
getPathsFromNavData, getPathsFromNavData,
validateNavData,
} from '@hashicorp/react-docs-page/server' } from '@hashicorp/react-docs-page/server'
import renderPageMdx from '@hashicorp/react-docs-page/render-page-mdx' import renderPageMdx from '@hashicorp/react-docs-page/render-page-mdx'
import fetchGithubFile from './utils/fetch-github-file' import resolveNavData from './utils/resolve-nav-data'
import mergeRemotePlugins from './utils/merge-remote-plugins'
const IS_DEV = process.env.VERCEL_ENV !== 'production'
async function generateStaticPaths(navDataFile, contentDir, options = {}) { async function generateStaticPaths(navDataFile, contentDir, options = {}) {
const navData = await resolveNavData(navDataFile, contentDir, options) const navData = await resolveNavData(navDataFile, contentDir, options)
@ -21,7 +17,8 @@ async function generateStaticProps(
navDataFile, navDataFile,
localContentDir, localContentDir,
params, params,
{ productName, remotePluginsFile, additionalComponents } = {} product,
{ remotePluginsFile, additionalComponents, mainBranch = 'main' } = {}
) { ) {
const navData = await resolveNavData(navDataFile, localContentDir, { const navData = await resolveNavData(navDataFile, localContentDir, {
remotePluginsFile, remotePluginsFile,
@ -30,12 +27,15 @@ async function generateStaticProps(
const navNode = getNodeFromPath(pathToMatch, navData, localContentDir) const navNode = getNodeFromPath(pathToMatch, navData, localContentDir)
const { filePath, remoteFile, pluginTier } = navNode const { filePath, remoteFile, pluginTier } = navNode
// Fetch the MDX file content // Fetch the MDX file content
const [err, mdxString] = filePath const mdxString = remoteFile
? // Read local content from the filesystem ? remoteFile.fileString
[null, fs.readFileSync(path.join(process.cwd(), filePath), 'utf8')] : fs.readFileSync(path.join(process.cwd(), filePath), 'utf8')
: // Fetch remote content using GitHub's API // Construct the githubFileUrl, used for "Edit this page" link
await fetchGithubFile(remoteFile) // Note: we expect remote files, such as those used to render plugin docs,
if (err) throw new Error(err) // 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}`
// For plugin pages, prefix the MDX content with a // For plugin pages, prefix the MDX content with a
// label that reflects the plugin tier // label that reflects the plugin tier
// (current options are "Official" or "Community") // (current options are "Official" or "Community")
@ -48,41 +48,22 @@ async function generateStaticProps(
} }
const { mdxSource, frontMatter } = await renderPageMdx(mdxString, { const { mdxSource, frontMatter } = await renderPageMdx(mdxString, {
additionalComponents, additionalComponents,
productName, productName: product.name,
mdxContentHook, mdxContentHook,
}) })
// Build the currentPath from page parameters // Build the currentPath from page parameters
const currentPath = !params.page ? '' : params.page.join('/') 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 { return {
currentPath, currentPath,
frontMatter, frontMatter,
isDevMissingRemotePlugins,
mdxSource, mdxSource,
mdxString, mdxString,
githubFileUrl,
navData, navData,
navNode, 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 default { generateStaticPaths, generateStaticProps }
export { generateStaticPaths, generateStaticProps } export { generateStaticPaths, generateStaticProps }

View File

@ -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)

View File

@ -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 version 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)

View File

@ -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

View File

@ -0,0 +1,50 @@
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,
//
// 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

View File

@ -0,0 +1,56 @@
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,
//
// 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

View File

@ -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 (used for "Edit this page" link)
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

View File

@ -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

View File

@ -2,6 +2,7 @@
{ {
"title": "Docker", "title": "Docker",
"path": "docker", "path": "docker",
"repo": "hashicorp/packer-plugin-docker" "repo": "hashicorp/packer-plugin-docker",
"version": "v0.0.4"
} }
] ]

View File

@ -5,118 +5,118 @@
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@algolia/cache-browser-local-storage": { "@algolia/cache-browser-local-storage": {
"version": "4.8.3", "version": "4.8.6",
"resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.8.3.tgz", "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.8.6.tgz",
"integrity": "sha512-Cwc03hikHSUI+xvgUdN+H+f6jFyoDsC9fegzXzJ2nPn1YSN9EXzDMBnbrgl0sbl9iLGXe0EIGMYqR2giCv1wMQ==", "integrity": "sha512-Bam7otzjIEgrRXWmk0Amm1+B3ROI5dQnUfJEBjIy0YPM0kMahEoJXCw6160tGKxJLl1g6icoC953nGshQKO7cA==",
"requires": { "requires": {
"@algolia/cache-common": "4.8.3" "@algolia/cache-common": "4.8.6"
} }
}, },
"@algolia/cache-common": { "@algolia/cache-common": {
"version": "4.8.3", "version": "4.8.6",
"resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.8.3.tgz", "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.8.6.tgz",
"integrity": "sha512-Cf7zZ2i6H+tLSBTkFePHhYvlgc9fnMPKsF9qTmiU38kFIGORy/TN2Fx5n1GBuRLIzaSXvcf+oHv1HvU0u1gE1g==" "integrity": "sha512-eGQlsXU5G7n4RvV/K6qe6lRAeL6EKAYPT3yZDBjCW4pAh7JWta+77a7BwUQkTqXN1MEQWZXjex3E4z/vFpzNrg=="
}, },
"@algolia/cache-in-memory": { "@algolia/cache-in-memory": {
"version": "4.8.3", "version": "4.8.6",
"resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.8.3.tgz", "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.8.6.tgz",
"integrity": "sha512-+N7tkvmijXiDy2E7u1mM73AGEgGPWFmEmPeJS96oT46I98KXAwVPNYbcAqBE79YlixdXpkYJk41cFcORzNh+Iw==", "integrity": "sha512-kbJrvCFANxL/l5Pq1NFyHLRphKDwmqcD/OJga0IbNKEulRGDPkt1+pC7/q8d2ikP12adBjLLg2CVias9RJpIaw==",
"requires": { "requires": {
"@algolia/cache-common": "4.8.3" "@algolia/cache-common": "4.8.6"
} }
}, },
"@algolia/client-account": { "@algolia/client-account": {
"version": "4.8.3", "version": "4.8.6",
"resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.8.3.tgz", "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.8.6.tgz",
"integrity": "sha512-Uku8LqnXBwfDCtsTCDYTUOz2/2oqcAQCKgaO0uGdIR8DTQENBXFQvzziambHdn9KuFuY+6Et9k1+cjpTPBDTBg==", "integrity": "sha512-FQVJE/BgCb78jtG7V0r30sMl9P5JKsrsOacGtGF2YebqI0YF25y8Z1nO39lbdjahxUS3QkDw2d0P2EVMj65g2Q==",
"requires": { "requires": {
"@algolia/client-common": "4.8.3", "@algolia/client-common": "4.8.6",
"@algolia/client-search": "4.8.3", "@algolia/client-search": "4.8.6",
"@algolia/transporter": "4.8.3" "@algolia/transporter": "4.8.6"
} }
}, },
"@algolia/client-analytics": { "@algolia/client-analytics": {
"version": "4.8.3", "version": "4.8.6",
"resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.8.3.tgz", "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.8.6.tgz",
"integrity": "sha512-9ensIWmjYJprZ+YjAVSZdWUG05xEnbytENXp508X59tf34IMIX8BR2xl0RjAQODtxBdAteGxuKt5THX6U9tQLA==", "integrity": "sha512-ZBYFUlzNaWDFtt0rYHI7xbfVX0lPWU9lcEEXI/BlnkRgEkm247H503tNatPQFA1YGkob52EU18sV1eJ+OFRBLA==",
"requires": { "requires": {
"@algolia/client-common": "4.8.3", "@algolia/client-common": "4.8.6",
"@algolia/client-search": "4.8.3", "@algolia/client-search": "4.8.6",
"@algolia/requester-common": "4.8.3", "@algolia/requester-common": "4.8.6",
"@algolia/transporter": "4.8.3" "@algolia/transporter": "4.8.6"
} }
}, },
"@algolia/client-common": { "@algolia/client-common": {
"version": "4.8.3", "version": "4.8.6",
"resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.8.3.tgz", "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.8.6.tgz",
"integrity": "sha512-TU3623AEFAWUQlDTznkgAMSYo8lfS9pNs5QYDQzkvzWdqK0GBDWthwdRfo9iIsfxiR9qdCMHqwEu+AlZMVhNSA==", "integrity": "sha512-8dI+K3Nvbes2YRZm2LY7bdCUD05e60BhacrMLxFuKxnBGuNehME1wbxq/QxcG1iNFJlxLIze5TxIcNN3+pn76g==",
"requires": { "requires": {
"@algolia/requester-common": "4.8.3", "@algolia/requester-common": "4.8.6",
"@algolia/transporter": "4.8.3" "@algolia/transporter": "4.8.6"
} }
}, },
"@algolia/client-recommendation": { "@algolia/client-recommendation": {
"version": "4.8.3", "version": "4.8.6",
"resolved": "https://registry.npmjs.org/@algolia/client-recommendation/-/client-recommendation-4.8.3.tgz", "resolved": "https://registry.npmjs.org/@algolia/client-recommendation/-/client-recommendation-4.8.6.tgz",
"integrity": "sha512-qysGbmkcc6Agt29E38KWJq9JuxjGsyEYoKuX9K+P5HyQh08yR/BlRYrA8mB7vT/OIUHRGFToGO6Vq/rcg0NIOQ==", "integrity": "sha512-Kg8DpjwvaWWujNx6sAUrSL+NTHxFe/UNaliCcSKaMhd3+FiPXN+CrSkO0KWR7I+oK2qGBTG/2Y0BhFOJ5/B/RA==",
"requires": { "requires": {
"@algolia/client-common": "4.8.3", "@algolia/client-common": "4.8.6",
"@algolia/requester-common": "4.8.3", "@algolia/requester-common": "4.8.6",
"@algolia/transporter": "4.8.3" "@algolia/transporter": "4.8.6"
} }
}, },
"@algolia/client-search": { "@algolia/client-search": {
"version": "4.8.3", "version": "4.8.6",
"resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.8.3.tgz", "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.8.6.tgz",
"integrity": "sha512-rAnvoy3GAhbzOQVniFcKVn1eM2NX77LearzYNCbtFrFYavG+hJI187bNVmajToiuGZ10FfJvK99X2OB1AzzezQ==", "integrity": "sha512-vXLS6umL/9G3bwqc6pkrS9K5/s8coq55mpfRARL+bs0NsToOf77WSTdwzlxv/KdbVF7dHjXgUpBvJ6RyR4ZdAw==",
"requires": { "requires": {
"@algolia/client-common": "4.8.3", "@algolia/client-common": "4.8.6",
"@algolia/requester-common": "4.8.3", "@algolia/requester-common": "4.8.6",
"@algolia/transporter": "4.8.3" "@algolia/transporter": "4.8.6"
} }
}, },
"@algolia/logger-common": { "@algolia/logger-common": {
"version": "4.8.3", "version": "4.8.6",
"resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.8.3.tgz", "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.8.6.tgz",
"integrity": "sha512-03wksHRbhl2DouEKnqWuUb64s1lV6kDAAabMCQ2Du1fb8X/WhDmxHC4UXMzypeOGlH5BZBsgVwSB7vsZLP3MZg==" "integrity": "sha512-FMRxZGdDxSzd0/Mv0R1021FvUt0CcbsQLYeyckvSWX8w+Uk4o0lcV6UtZdERVR5XZsGOqoXLMIYDbR2vkbGbVw=="
}, },
"@algolia/logger-console": { "@algolia/logger-console": {
"version": "4.8.3", "version": "4.8.6",
"resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.8.3.tgz", "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.8.6.tgz",
"integrity": "sha512-Npt+hI4UF8t3TLMluL5utr9Gc11BjL5kDnGZOhDOAz5jYiSO2nrHMFmnpLT4Cy/u7a5t7EB5dlypuC4/AGStkA==", "integrity": "sha512-TYw9lwUCjvApC6Z0zn36T6gkCl7hbfJmnU+Z/D8pFJ3Yp7lz06S3oWGjbdrULrYP1w1VOhjd0X7/yGNsMhzutQ==",
"requires": { "requires": {
"@algolia/logger-common": "4.8.3" "@algolia/logger-common": "4.8.6"
} }
}, },
"@algolia/requester-browser-xhr": { "@algolia/requester-browser-xhr": {
"version": "4.8.3", "version": "4.8.6",
"resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.8.3.tgz", "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.8.6.tgz",
"integrity": "sha512-/LTTIpgEmEwkyhn8yXxDdBWqXqzlgw5w2PtTpIwkSlP2/jDwdR/9w1TkFzhNbJ81ki6LAEQM5mSwoTTnbIIecg==", "integrity": "sha512-omh6uJ3CJXOmcrU9M3/KfGg8XkUuGJGIMkqEbkFvIebpBJxfs6TVs0ziNeMFAcAfhi8/CGgpLbDSgJtWdGQa6w==",
"requires": { "requires": {
"@algolia/requester-common": "4.8.3" "@algolia/requester-common": "4.8.6"
} }
}, },
"@algolia/requester-common": { "@algolia/requester-common": {
"version": "4.8.3", "version": "4.8.6",
"resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.8.3.tgz", "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.8.6.tgz",
"integrity": "sha512-+Yo9vBkofoKR1SCqqtMnmnfq9yt/BiaDewY/6bYSMNxSYCnu2Fw1JKSIaf/4zos09PMSsxGpLohZwGas3+0GDQ==" "integrity": "sha512-r5xJqq/D9KACkI5DgRbrysVL5DUUagikpciH0k0zjBbm+cXiYfpmdflo/h6JnY6kmvWgjr/4DoeTjKYb/0deAQ=="
}, },
"@algolia/requester-node-http": { "@algolia/requester-node-http": {
"version": "4.8.3", "version": "4.8.6",
"resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.8.3.tgz", "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.8.6.tgz",
"integrity": "sha512-k2fiKIeMIFqgC01FnzII6kqC2GQBAfbNaUX4k7QCPa6P8t4sp2xE6fImOUiztLnnL3C9X9ZX6Fw3L+cudi7jvQ==", "integrity": "sha512-TB36OqTVOKyHCOtdxhn/IJyI/NXi/BWy8IEbsiWwwZWlL79NWHbetj49jXWFolEYEuu8PgDjjZGpRhypSuO9XQ==",
"requires": { "requires": {
"@algolia/requester-common": "4.8.3" "@algolia/requester-common": "4.8.6"
} }
}, },
"@algolia/transporter": { "@algolia/transporter": {
"version": "4.8.3", "version": "4.8.6",
"resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.8.3.tgz", "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.8.6.tgz",
"integrity": "sha512-nU7fy2iU8snxATlsks0MjMyv97QJWQmOVwTjDc+KZ4+nue8CLcgm4LA4dsTBqvxeCQIoEtt3n72GwXcaqiJSjQ==", "integrity": "sha512-NRb31J0TP7EPoVMpXZ4yAtr61d26R8KGaf6qdULknvq5sOVHuuH4PwmF08386ERfIsgnM/OBhl+uzwACdCIjSg==",
"requires": { "requires": {
"@algolia/cache-common": "4.8.3", "@algolia/cache-common": "4.8.6",
"@algolia/logger-common": "4.8.3", "@algolia/logger-common": "4.8.6",
"@algolia/requester-common": "4.8.3" "@algolia/requester-common": "4.8.6"
} }
}, },
"@ampproject/toolbox-core": { "@ampproject/toolbox-core": {
@ -2854,12 +2854,12 @@
"integrity": "sha512-AYIe6tcOxlKPe5Sq89o/Vk0rGE6Z1dCzf+N3ynECTh5L2A1zusf9xeM659QEh/edE/Ll9EBBLmq49sQXLNDxTw==" "integrity": "sha512-AYIe6tcOxlKPe5Sq89o/Vk0rGE6Z1dCzf+N3ynECTh5L2A1zusf9xeM659QEh/edE/Ll9EBBLmq49sQXLNDxTw=="
}, },
"@hashicorp/react-docs-page": { "@hashicorp/react-docs-page": {
"version": "10.9.4-alpha.18", "version": "11.0.1",
"resolved": "https://registry.npmjs.org/@hashicorp/react-docs-page/-/react-docs-page-10.9.4-alpha.18.tgz", "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-page/-/react-docs-page-11.0.1.tgz",
"integrity": "sha512-+eRKJ2PX9s4Is0ZT2O8ZBcBWuDt7OGxwBrqKF1ulo/DcZunj7pODCQQulb+jAtQyq7YzikWdFmQ/pcvwaVHK6Q==", "integrity": "sha512-BZ746Qm97OQTyMPI7calYDb+LAQQZGTn/vZ8FaMXbVCF+X9Bvs1xYyWRDV6gV0mtgmlkCwYqIrmqneOZdd6PcA==",
"requires": { "requires": {
"@hashicorp/react-content": "^6.3.0", "@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-head": "^1.2.0",
"@hashicorp/react-search": "^4.1.0", "@hashicorp/react-search": "^4.1.0",
"fs-exists-sync": "0.1.0", "fs-exists-sync": "0.1.0",
@ -2870,121 +2870,6 @@
"readdirp": "3.5.0" "readdirp": "3.5.0"
}, },
"dependencies": { "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": { "@hashicorp/react-content": {
"version": "6.3.0", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/@hashicorp/react-content/-/react-content-6.3.0.tgz", "resolved": "https://registry.npmjs.org/@hashicorp/react-content/-/react-content-6.3.0.tgz",
@ -3016,73 +2901,23 @@
"search-insights": "^1.6.0", "search-insights": "^1.6.0",
"unist-util-visit": "^2.0.3" "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": { "@hashicorp/react-docs-sidenav": {
"version": "6.1.1-alpha.16", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/@hashicorp/react-docs-sidenav/-/react-docs-sidenav-6.1.1-alpha.16.tgz", "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-sidenav/-/react-docs-sidenav-7.0.0.tgz",
"integrity": "sha512-RpPjNwMNe5L2LA1vvgp496CauVJ8wLnKge1lPBZKL5931jR1SFEMwuWLB8R6Pe2HmkIC55nPB/c43GrmPN4FFw==", "integrity": "sha512-gzOEG4fwfdfdHvxMuRC73bZUIpUzSPrG826NIM4N0lqUPzsAkDsfEl2+Vg1ZVfgzy2+41E+lIpHW4ZmWc5OZ7A==",
"requires": { "requires": {
"@hashicorp/react-link-wrap": "^2.0.2",
"fuzzysearch": "1.0.3" "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": { "@hashicorp/react-enterprise-alert": {
@ -3183,17 +3018,17 @@
} }
}, },
"@hashicorp/react-search": { "@hashicorp/react-search": {
"version": "3.0.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/@hashicorp/react-search/-/react-search-3.0.0.tgz", "resolved": "https://registry.npmjs.org/@hashicorp/react-search/-/react-search-4.2.0.tgz",
"integrity": "sha512-62ttyCxjVFSHz1aNbdjeOcqCezpk3dLhMWTXeQb9Zsi0JYaJdBzK1M9khW1bfozTzjTXXGd/B79orlHMj/Zo9A==", "integrity": "sha512-ITj3UC06w+bZKrHv77kYdtWlEH9gbtk+pAzZ5ZRxt2GMnw8qMzWnXZKVf1yHvyKAKkHkGXA5s+uFElxRJj3AVQ==",
"requires": { "requires": {
"@hashicorp/react-inline-svg": "^1.0.2", "@hashicorp/react-inline-svg": "^1.0.2",
"@hashicorp/remark-plugins": "^3.0.0", "@hashicorp/remark-plugins": "^3.0.0",
"algoliasearch": "^4.4.0", "algoliasearch": "^4.8.4",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"glob": "^7.1.6", "glob": "^7.1.6",
"gray-matter": "^4.0.2", "gray-matter": "^4.0.2",
"react-instantsearch-dom": "^6.7.0", "react-instantsearch-dom": "^6.9.0",
"remark": "^12.0.1", "remark": "^12.0.1",
"search-insights": "^1.6.0", "search-insights": "^1.6.0",
"unist-util-visit": "^2.0.3" "unist-util-visit": "^2.0.3"
@ -3927,6 +3762,11 @@
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
"integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==" "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": { "agent-base": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@ -3973,30 +3813,30 @@
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="
}, },
"algoliasearch": { "algoliasearch": {
"version": "4.8.3", "version": "4.8.6",
"resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.8.3.tgz", "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.8.6.tgz",
"integrity": "sha512-pljX9jEE2TQ3i1JayhG8afNdE8UuJg3O9c7unW6QO67yRWCKr6b0t5aKC3hSVtjt7pA2TQXLKoAISb4SHx9ozQ==", "integrity": "sha512-G8IA3lcgaQB4r9HuQ4G+uSFjjz0Wv2OgEPiQ8emA+G2UUlroOfMl064j1bq/G+QTW0LmTQp9JwrFDRWxFM9J7w==",
"requires": { "requires": {
"@algolia/cache-browser-local-storage": "4.8.3", "@algolia/cache-browser-local-storage": "4.8.6",
"@algolia/cache-common": "4.8.3", "@algolia/cache-common": "4.8.6",
"@algolia/cache-in-memory": "4.8.3", "@algolia/cache-in-memory": "4.8.6",
"@algolia/client-account": "4.8.3", "@algolia/client-account": "4.8.6",
"@algolia/client-analytics": "4.8.3", "@algolia/client-analytics": "4.8.6",
"@algolia/client-common": "4.8.3", "@algolia/client-common": "4.8.6",
"@algolia/client-recommendation": "4.8.3", "@algolia/client-recommendation": "4.8.6",
"@algolia/client-search": "4.8.3", "@algolia/client-search": "4.8.6",
"@algolia/logger-common": "4.8.3", "@algolia/logger-common": "4.8.6",
"@algolia/logger-console": "4.8.3", "@algolia/logger-console": "4.8.6",
"@algolia/requester-browser-xhr": "4.8.3", "@algolia/requester-browser-xhr": "4.8.6",
"@algolia/requester-common": "4.8.3", "@algolia/requester-common": "4.8.6",
"@algolia/requester-node-http": "4.8.3", "@algolia/requester-node-http": "4.8.6",
"@algolia/transporter": "4.8.3" "@algolia/transporter": "4.8.6"
} }
}, },
"algoliasearch-helper": { "algoliasearch-helper": {
"version": "3.3.4", "version": "3.4.4",
"resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.3.4.tgz", "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.4.4.tgz",
"integrity": "sha512-1Ts2XcgGdjGlDrp3v6zbY8VW+X9+jJ5rBmtPBmXOQLd4b5t/LpJlaBdxoAnlMfVFjywP7KSAdmyFUNNYVHDyRQ==", "integrity": "sha512-OjyVLjykaYKCMxxRMZNiwLp8CS310E0qAeIY2NaublcmLAh8/SL19+zYHp7XCLtMem2ZXwl3ywMiA32O9jszuw==",
"requires": { "requires": {
"events": "^1.1.1" "events": "^1.1.1"
}, },
@ -11653,27 +11493,27 @@
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
}, },
"react-instantsearch-core": { "react-instantsearch-core": {
"version": "6.8.2", "version": "6.10.3",
"resolved": "https://registry.npmjs.org/react-instantsearch-core/-/react-instantsearch-core-6.8.2.tgz", "resolved": "https://registry.npmjs.org/react-instantsearch-core/-/react-instantsearch-core-6.10.3.tgz",
"integrity": "sha512-UdAjcNIXb2mSECEDS/2XuB4W6rcbnph1NjJBUpY5TLLzSCdKXNTzS2PxF5hkdeuY0L/m/hvDQX6YqxV28PqKLA==", "integrity": "sha512-7twp3OJrPGTFpyXwjJNeOTbQw7RTv+0cUyKkXR9njEyLdXKcPWfpeBirXfdQHjYIHEY2b0V2Vom1B9IHSDSUtQ==",
"requires": { "requires": {
"@babel/runtime": "^7.1.2", "@babel/runtime": "^7.1.2",
"algoliasearch-helper": "^3.1.0", "algoliasearch-helper": "^3.4.3",
"prop-types": "^15.5.10", "prop-types": "^15.6.2",
"react-fast-compare": "^3.0.0" "react-fast-compare": "^3.0.0"
} }
}, },
"react-instantsearch-dom": { "react-instantsearch-dom": {
"version": "6.8.2", "version": "6.10.3",
"resolved": "https://registry.npmjs.org/react-instantsearch-dom/-/react-instantsearch-dom-6.8.2.tgz", "resolved": "https://registry.npmjs.org/react-instantsearch-dom/-/react-instantsearch-dom-6.10.3.tgz",
"integrity": "sha512-d6YBsjW/aF3qzul7qqUV/KuzEFPVxlAZm3QhREPqMvOyrPTnG5itZZBLe7sFm9OKJ/8shR4TyNp3hb94as7COg==", "integrity": "sha512-kxc6IEruxJrc7O9lsLV5o4YK/RkGt3l7D1Y51JfmYkgeLuQHApwgcy/TAIoSN7wfR/1DONFbX8Y5VhU9Wqh87Q==",
"requires": { "requires": {
"@babel/runtime": "^7.1.2", "@babel/runtime": "^7.1.2",
"algoliasearch-helper": "^3.1.0", "algoliasearch-helper": "^3.4.3",
"classnames": "^2.2.5", "classnames": "^2.2.5",
"prop-types": "^15.5.10", "prop-types": "^15.6.2",
"react-fast-compare": "^3.0.0", "react-fast-compare": "^3.0.0",
"react-instantsearch-core": "^6.8.2" "react-instantsearch-core": "^6.10.3"
} }
}, },
"react-is": { "react-is": {

View File

@ -7,16 +7,18 @@
"@hashicorp/mktg-global-styles": "2.1.0", "@hashicorp/mktg-global-styles": "2.1.0",
"@hashicorp/nextjs-scripts": "16.2.0", "@hashicorp/nextjs-scripts": "16.2.0",
"@hashicorp/react-button": "4.0.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-hashi-stack-menu": "^1.1.0",
"@hashicorp/react-head": "1.1.6", "@hashicorp/react-head": "1.1.6",
"@hashicorp/react-inline-svg": "5.0.0", "@hashicorp/react-inline-svg": "5.0.0",
"@hashicorp/react-markdown-page": "^0.1.0", "@hashicorp/react-markdown-page": "^0.1.0",
"@hashicorp/react-product-downloader": "4.0.2", "@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-section-header": "4.0.0",
"@hashicorp/react-subnav": "7.1.0", "@hashicorp/react-subnav": "7.1.0",
"@hashicorp/react-vertical-text-block-list": "4.0.1", "@hashicorp/react-vertical-text-block-list": "4.0.1",
"adm-zip": "^0.5.4",
"gray-matter": "^4.0.2",
"next": "10.0.6", "next": "10.0.6",
"next-mdx-remote": "1.0.1", "next-mdx-remote": "1.0.1",
"next-remote-watch": "0.3.0", "next-remote-watch": "0.3.0",

View File

@ -12,12 +12,12 @@ import {
const BASE_ROUTE = 'docs' const BASE_ROUTE = 'docs'
const NAV_DATA = 'data/docs-nav-data.json' const NAV_DATA = 'data/docs-nav-data.json'
const CONTENT_DIR = 'content/docs' const CONTENT_DIR = 'content/docs'
// override default "main" value for branch for "edit on this page" const PRODUCT = { name: productName, slug: productSlug }
const MAIN_BRANCH = 'master'
// add remote plugin docs loading // add remote plugin docs loading
const OPTIONS = { const OPTIONS = {
remotePluginsFile: 'data/docs-remote-plugins.json', remotePluginsFile: 'data/docs-remote-plugins.json',
additionalComponents: { PluginTierLabel }, additionalComponents: { PluginTierLabel },
mainBranch: 'master',
} }
function DocsLayout({ isDevMissingRemotePlugins, ...props }) { function DocsLayout({ isDevMissingRemotePlugins, ...props }) {
@ -49,8 +49,7 @@ function DocsLayout({ isDevMissingRemotePlugins, ...props }) {
<DocsPage <DocsPage
additionalComponents={OPTIONS.additionalComponents} additionalComponents={OPTIONS.additionalComponents}
baseRoute={BASE_ROUTE} baseRoute={BASE_ROUTE}
mainBranch="master" // used for "edit on this page", default "main" product={PRODUCT}
product={{ name: productName, slug: productSlug }}
staticProps={props} staticProps={props}
/> />
</> </>
@ -67,6 +66,7 @@ export async function getStaticProps({ params }) {
NAV_DATA, NAV_DATA,
CONTENT_DIR, CONTENT_DIR,
params, params,
PRODUCT,
OPTIONS OPTIONS
) )
return { props } return { props }

View File

@ -10,15 +10,12 @@ import {
const BASE_ROUTE = 'guides' const BASE_ROUTE = 'guides'
const NAV_DATA = 'data/guides-nav-data.json' const NAV_DATA = 'data/guides-nav-data.json'
const CONTENT_DIR = 'content/guides' const CONTENT_DIR = 'content/guides'
const MAIN_BRANCH = 'master'
const PRODUCT = { name: productName, slug: productSlug }
export default function GuidesLayout(props) { export default function GuidesLayout(props) {
return ( return (
<DocsPage <DocsPage baseRoute={BASE_ROUTE} product={PRODUCT} staticProps={props} />
baseRoute={BASE_ROUTE}
mainBranch="master" // used for "edit on this page", default "main"
product={{ name: productName, slug: productSlug }}
staticProps={props}
/>
) )
} }
@ -28,6 +25,12 @@ export async function getStaticPaths() {
} }
export async function getStaticProps({ params }) { 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 } return { props }
} }

View File

@ -10,15 +10,12 @@ import {
const BASE_ROUTE = 'intro' const BASE_ROUTE = 'intro'
const NAV_DATA = 'data/intro-nav-data.json' const NAV_DATA = 'data/intro-nav-data.json'
const CONTENT_DIR = 'content/intro' const CONTENT_DIR = 'content/intro'
const MAIN_BRANCH = 'master'
const PRODUCT = { name: productName, slug: productSlug }
export default function IntroLayout(props) { export default function IntroLayout(props) {
return ( return (
<DocsPage <DocsPage baseRoute={BASE_ROUTE} product={PRODUCT} staticProps={props} />
baseRoute={BASE_ROUTE}
mainBranch="master" // used for "edit on this page", default "main"
product={{ name: productName, slug: productSlug }}
staticProps={props}
/>
) )
} }
@ -28,6 +25,12 @@ export async function getStaticPaths() {
} }
export async function getStaticProps({ params }) { 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 } return { props }
} }

View File

@ -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 []
}