@@ -75,9 +70,7 @@ export function DocsPageWrapper({
{/* if desired, show an "edit this page" link on the bottom right, linking to github */}
{showEditPage && (
-
+ Edit this page
@@ -89,12 +82,10 @@ export function DocsPageWrapper({
export default function DocsPage({
product,
- subpath,
- order,
- mainBranch = 'main',
+ baseRoute,
showEditPage = true,
additionalComponents,
- staticProps: { mdxSource, data, frontMatter, pagePath, filePath },
+ staticProps: { mdxSource, frontMatter, currentPath, navData, githubFileUrl },
}) {
// This component is written to work with next-mdx-remote -- here it hydrates the content
const content = hydrate(mdxSource, {
@@ -103,17 +94,15 @@ export default function DocsPage({
return (
{content}
diff --git a/packages/docs-page/index.test.js b/packages/docs-page/index.test.js
index 1336a0c3e..b59aaaac0 100644
--- a/packages/docs-page/index.test.js
+++ b/packages/docs-page/index.test.js
@@ -1,16 +1,130 @@
-test.todo(
- 'passes `title`, `description`, and `siteName` correctly to '
-)
-test.todo(
- 'passes `product`, `category`, `currentPage`, `data`, and `order` correctly to '
-)
-test.todo('passes `product` and `content` correctly to ')
-test.todo('displays `showEditPage` as true by default')
-test.todo('renders the `mainBranch` correctly within the edit page link')
-test.todo('if `showEditPage` is set to false, does not display')
-test.todo(
- 'passes `additionalComponents` to mdx remote for rendering if present'
-)
-test.todo(
- 'initializes jump to section UI if there are more than one `h2`s in the content'
-)
+require('dotenv').config()
+// import 'regenerator-runtime/runtime'
+import { render, screen } from '@testing-library/react'
+// import expectThrow from '../../__test-helpers/expect-throw'
+import DocsPage from './'
+import props from './props'
+import { getTestValues } from 'swingset/testing'
+import renderPageMdx from './render-page-mdx'
+
+const defaultProps = getTestValues(props)
+
+// Mocking next/head makes it easier to confirm
+// that we're passing stuff to
+jest.mock('next/head', () => {
+ return {
+ __esModule: true,
+ default: function HeadMock({ children }) {
+ return <>{children}>
+ },
+ }
+})
+
+describe('', () => {
+ it('passes `title`, `description`, and `siteName` correctly to ', () => {
+ render()
+ // title renders correctly
+ expect(document.title).toBe(`Test Page | Terraform by HashiCorp`)
+ // description renders correctly
+ const description = Array.prototype.slice
+ .call(document.getElementsByTagName('meta'))
+ .filter((tag) => tag.getAttribute('name') === 'description')[0]
+ .getAttribute('content')
+ expect(description).toBe(`Test description`)
+ // siteName renders correctly
+ const site_name = Array.prototype.slice
+ .call(document.getElementsByTagName('meta'))
+ .filter((tag) => tag.getAttribute('property') === 'og:site_name')[0]
+ .getAttribute('content')
+ expect(site_name).toBe(`Terraform by HashiCorp`)
+ })
+
+ it('passes props correctly to ', () => {
+ render()
+ // Confirm `product` is passed via document title
+ expect(document.title).toBe(`Test Page | Terraform by HashiCorp`)
+ // Confirm `baseRoute` and `navData` by checking for a rendered link
+ const activeLeaf = screen.getByText('AWS').closest('a')
+ expect(activeLeaf.getAttribute('href')).toBe(
+ '/docs/agent/autoauth/methods/aws'
+ )
+ // Confirm `currentPath` by ensuring a link is marked as active
+ expect(activeLeaf.getAttribute('data-is-active')).toBe('true')
+ })
+
+ it('passes `product` and `content` correctly to ', () => {
+ render()
+ // Confirm `content` is being rendered
+ const contentParagraph = screen.getByText('This is a cool docs page!')
+ expect(contentParagraph.tagName).toBe('P')
+ // Confirm `product` is passed via class
+ const contentContainer = contentParagraph.closest('article')
+ expect(contentContainer.className).toContain('terraform')
+ })
+
+ it('displays `showEditPage` as true by default, and renders `mainBranch` in the link', () => {
+ render()
+ const expectedHref =
+ 'https://github.com/hashicorp/vault/blob/master/website/content/docs/agent/autoauth/methods/aws.mdx'
+ const editPageLink = screen.getByText('Edit this page').closest('a')
+ expect(editPageLink.getAttribute('href')).toBe(expectedHref)
+ })
+
+ it('if `showEditPage` is set to false, does not display', () => {
+ render()
+ const editPageLink = screen.queryByText('Edit this page')
+ expect(editPageLink).toBeNull()
+ })
+
+ it('passes `additionalComponents` to mdx remote for rendering if present', async () => {
+ function CustomComponent() {
+ return Text in custom component
+ }
+ const additionalComponents = { CustomComponent }
+ const {
+ mdxSource,
+ frontMatter,
+ } = await renderPageMdx(
+ "## Heading Two\n\nHere's a paragraph of content.\n\n",
+ { productName: 'Terraform', additionalComponents }
+ )
+ render(
+
+ )
+ // Find the text rendered by the custom component
+ const customComponentElem = screen.getByText('Text in custom component')
+ expect(customComponentElem.tagName).toBe('STRONG')
+ })
+
+ it('initializes jump to section UI if there is an h1 and two or more h2s', async () => {
+ const {
+ mdxSource,
+ frontMatter,
+ } = await renderPageMdx(
+ "---\n\npage_title: Test Title\ndescription: Test description\n---\n\n# Heading One\n\nAn intro paragraph.\n\n## Heading Two\n\nHere's a paragraph of content.\n\n## Here a second heading\n\nAnd another paragraph.",
+ { productName: 'Terraform' }
+ )
+ render(
+
+ )
+ const jumpToSectionElem = screen.getByText('Jump to Section')
+ expect(jumpToSectionElem.tagName).toBe('SPAN')
+ })
+})
diff --git a/packages/docs-page/package-lock.json b/packages/docs-page/package-lock.json
index 289a0cb4a..68c38a358 100644
--- a/packages/docs-page/package-lock.json
+++ b/packages/docs-page/package-lock.json
@@ -1,126 +1,150 @@
{
- "name": "@hashicorp/react-docs-page",
- "version": "10.9.3",
- "lockfileVersion": 1,
- "requires": true,
- "dependencies": {
- "argparse": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
- "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
- "requires": {
- "sprintf-js": "~1.0.2"
- }
- },
- "esprima": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
- "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
- },
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "requires": {
- "is-extendable": "^0.1.0"
- }
- },
- "fast-equals": {
- "version": "1.6.3",
- "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-1.6.3.tgz",
- "integrity": "sha512-4WKW0AL5+WEqO0zWavAfYGY1qwLsBgE//DN4TTcVEN2UlINgkv9b3vm2iHicoenWKSX9mKWmGOsU/iI5IST7pQ=="
- },
- "fast-stringify": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/fast-stringify/-/fast-stringify-1.1.2.tgz",
- "integrity": "sha512-SfslXjiH8km0WnRiuPfpUKwlZjW5I878qsOm+2x8x3TgqmElOOLh1rgJFb+PolNdNRK3r8urEefqx0wt7vx1dA=="
- },
- "fs-exists-sync": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz",
- "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0="
- },
- "gray-matter": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.2.tgz",
- "integrity": "sha512-7hB/+LxrOjq/dd8APlK0r24uL/67w7SkYnfwhNFwg/VDIGWGmduTDYf3WNstLW2fbbmRwrDGCVSJ2isuf2+4Hw==",
- "requires": {
- "js-yaml": "^3.11.0",
- "kind-of": "^6.0.2",
- "section-matter": "^1.0.0",
- "strip-bom-string": "^1.0.0"
- }
- },
- "is-extendable": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
- "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik="
- },
- "js-yaml": {
- "version": "3.14.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
- "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
- "requires": {
- "argparse": "^1.0.7",
- "esprima": "^4.0.0"
- }
- },
- "kind-of": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
- "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
- },
- "line-reader": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/line-reader/-/line-reader-0.4.0.tgz",
- "integrity": "sha1-F+RIGNoKwzVnW6MAlU+U72cOZv0="
- },
- "micro-memoize": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/micro-memoize/-/micro-memoize-2.1.2.tgz",
- "integrity": "sha512-COjNutiFgnDHXZEIM/jYuZPwq2h8zMUeScf6Sh6so98a+REqdlpaNS7Cb2ffGfK5I+xfgoA3Rx49NGuNJTJq3w=="
- },
- "moize": {
- "version": "5.4.7",
- "resolved": "https://registry.npmjs.org/moize/-/moize-5.4.7.tgz",
- "integrity": "sha512-7PZH8QFJ51cIVtDv7wfUREBd3gL59JB0v/ARA3RI9zkSRa9LyGjS1Bdldii2J1/NQXRQ/3OOVOSdnZrCcVaZlw==",
- "requires": {
- "fast-equals": "^1.6.0",
- "fast-stringify": "^1.1.0",
- "micro-memoize": "^2.1.1"
- }
- },
- "picomatch": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
- "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg=="
- },
- "readdirp": {
- "version": "3.5.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
- "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
- "requires": {
- "picomatch": "^2.2.1"
- }
- },
- "section-matter": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
- "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
- "requires": {
- "extend-shallow": "^2.0.1",
- "kind-of": "^6.0.0"
- }
- },
- "sprintf-js": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
- "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
- },
- "strip-bom-string": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
- "integrity": "sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI="
- }
- }
+ "name": "@hashicorp/react-docs-page",
+ "version": "10.9.3",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@hashicorp/react-content": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/@hashicorp/react-content/-/react-content-6.3.0.tgz",
+ "integrity": "sha512-B+QMlkMGryeNx3dGON4ExbzNvvll2ZXN3x+TkX80tUGClMI80MKjfSXiXIoVixlp22DMNG6wrnL42LC4WzZOxg=="
+ },
+ "@hashicorp/react-docs-sidenav": {
+ "version": "6.1.1-alpha.60",
+ "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-sidenav/-/react-docs-sidenav-6.1.1-alpha.60.tgz",
+ "integrity": "sha512-mfR6Wl4R4k5KD7lJpl0l4pn4NecmoqV6HxQ+nvMW/d6nIkb6/xWxynmDkEeiyEVpphLKqJFh6boWuFYaar7PbA==",
+ "requires": {
+ "@hashicorp/react-link-wrap": "^2.0.2",
+ "fuzzysearch": "1.0.3"
+ }
+ },
+ "@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=="
+ },
+ "argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "requires": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
+ },
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ },
+ "fast-equals": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-1.6.3.tgz",
+ "integrity": "sha512-4WKW0AL5+WEqO0zWavAfYGY1qwLsBgE//DN4TTcVEN2UlINgkv9b3vm2iHicoenWKSX9mKWmGOsU/iI5IST7pQ=="
+ },
+ "fast-stringify": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/fast-stringify/-/fast-stringify-1.1.2.tgz",
+ "integrity": "sha512-SfslXjiH8km0WnRiuPfpUKwlZjW5I878qsOm+2x8x3TgqmElOOLh1rgJFb+PolNdNRK3r8urEefqx0wt7vx1dA=="
+ },
+ "fs-exists-sync": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz",
+ "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0="
+ },
+ "fuzzysearch": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/fuzzysearch/-/fuzzysearch-1.0.3.tgz",
+ "integrity": "sha1-3/yA9tawQiPyImqnndGUIxCW0Ag="
+ },
+ "gray-matter": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.2.tgz",
+ "integrity": "sha512-7hB/+LxrOjq/dd8APlK0r24uL/67w7SkYnfwhNFwg/VDIGWGmduTDYf3WNstLW2fbbmRwrDGCVSJ2isuf2+4Hw==",
+ "requires": {
+ "js-yaml": "^3.11.0",
+ "kind-of": "^6.0.2",
+ "section-matter": "^1.0.0",
+ "strip-bom-string": "^1.0.0"
+ }
+ },
+ "is-extendable": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+ "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik="
+ },
+ "js-yaml": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
+ "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ }
+ },
+ "kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
+ },
+ "line-reader": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/line-reader/-/line-reader-0.4.0.tgz",
+ "integrity": "sha1-F+RIGNoKwzVnW6MAlU+U72cOZv0="
+ },
+ "micro-memoize": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/micro-memoize/-/micro-memoize-2.1.2.tgz",
+ "integrity": "sha512-COjNutiFgnDHXZEIM/jYuZPwq2h8zMUeScf6Sh6so98a+REqdlpaNS7Cb2ffGfK5I+xfgoA3Rx49NGuNJTJq3w=="
+ },
+ "moize": {
+ "version": "5.4.7",
+ "resolved": "https://registry.npmjs.org/moize/-/moize-5.4.7.tgz",
+ "integrity": "sha512-7PZH8QFJ51cIVtDv7wfUREBd3gL59JB0v/ARA3RI9zkSRa9LyGjS1Bdldii2J1/NQXRQ/3OOVOSdnZrCcVaZlw==",
+ "requires": {
+ "fast-equals": "^1.6.0",
+ "fast-stringify": "^1.1.0",
+ "micro-memoize": "^2.1.1"
+ }
+ },
+ "picomatch": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
+ "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg=="
+ },
+ "readdirp": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
+ "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
+ "requires": {
+ "picomatch": "^2.2.1"
+ }
+ },
+ "section-matter": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
+ "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
+ "requires": {
+ "extend-shallow": "^2.0.1",
+ "kind-of": "^6.0.0"
+ }
+ },
+ "sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
+ },
+ "strip-bom-string": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
+ "integrity": "sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI="
+ }
+ }
}
diff --git a/packages/docs-page/package.json b/packages/docs-page/package.json
index a81522de0..fed1af4a7 100644
--- a/packages/docs-page/package.json
+++ b/packages/docs-page/package.json
@@ -8,7 +8,7 @@
],
"dependencies": {
"@hashicorp/react-content": "^6.3.0",
- "@hashicorp/react-docs-sidenav": "^6.1.0",
+ "@hashicorp/react-docs-sidenav": "6.1.1-alpha.60",
"@hashicorp/react-head": "^1.2.0",
"@hashicorp/react-search": "^4.1.0",
"fs-exists-sync": "0.1.0",
diff --git a/packages/docs-page/props.js b/packages/docs-page/props.js
index a64e6a295..5598aa272 100644
--- a/packages/docs-page/props.js
+++ b/packages/docs-page/props.js
@@ -1,60 +1,121 @@
+const docsSidenavProps = require('../docs-sidenav/props')
+const sharedProps = require('../../props')
+
module.exports = {
product: {
type: 'string',
- description: 'Name and slug of the product this page is being rendered for',
+ required: true,
+ description:
+ 'The `name` and `slug` of the product this page is being rendered for. The `slug` is used for the `Edit this page` link.',
properties: {
name: {
type: 'string',
- description: 'Human-readable proper case product name',
- },
- slug: {
- type: 'string',
- description: 'HashiCorp product slug',
- control: { type: 'select' },
- options: [
- 'hashicorp',
- 'boundary',
- 'consul',
- 'nomad',
- 'packer',
- 'terraform',
- 'vault',
- 'vagrant',
- 'waypoint',
- ],
+ required: true,
+ description:
+ 'Human-readable proper case product name. Used for the page `` and `og:site_name`.',
},
+ slug: sharedProps.product,
},
+ testValue: { name: 'Terraform', slug: sharedProps.product.testValue },
},
- subpath: {
+ baseRoute: {
type: 'string',
+ required: true,
description:
- 'The path this page is rendering under, for example "docs" or "api-docs". Passed directly to the `category` prop of `@hashicorp/react-docs-sidenav`',
- },
- order: {
- type: 'object',
- description:
- 'Pass in the export of a `data/xxx-navigation.js` file, this is the user-defined navigation order and structure. Passed directly to the `order` prop to `@hashicorp/react-docs-sidenav` - see that component for details on object structure.',
- },
- additionalComponents: {
- type: 'object',
- description:
- 'Object containing additional components to be made available within mdx pages. Uses the format { [key]: Component }, for example, `{ TestComponent: () =>
hello world
}`',
+ 'The path this page is rendering under, for example `"docs"` or `"api-docs"`. Passed directly to the `baseRoute` prop of `@hashicorp/react-docs-sidenav`.',
+ testValue: 'docs',
},
showEditPage: {
type: 'boolean',
description:
- 'if true, an "edit this page" link will appear on the bottom right',
+ 'If `true`, an `Edit this page` link will appear on the bottom right of each page.',
default: true,
},
- mainBranch: {
- type: 'string',
+ additionalComponents: {
+ type: 'object',
description:
- 'The default branch of the project being documented, typically either "master" or "main". Used for the `showEditPage` prop',
- default: 'main',
+ 'Object containing additional components to be made available within mdx pages. Uses the format `{ [key]: Component }`, for example, `{ TestComponent: () =>
hello world
}`',
},
staticProps: {
type: 'object',
+ required: true,
description:
'Directly pass the return value of `server/generateStaticProps` in here.',
+ properties: {
+ githubFileUrl: {
+ type: 'string',
+ description:
+ "A link to the page's associated `.mdx` file on GitHub. Used for the `Edit this page` link.",
+ testValue: `https://github.com/hashicorp/vault/blob/master/website/content/docs/agent/autoauth/methods/aws.mdx`,
+ },
+ mdxSource: {
+ type: 'object',
+ description:
+ "Data returned from running `next-mdx-remote/render-to-string` on the page's `.mdx` file contents.",
+ required: true,
+ testValue: {
+ compiledSource:
+ '"use strict";\n' +
+ '\n' +
+ 'function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n' +
+ '\n' +
+ 'function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }\n' +
+ '\n' +
+ 'function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }\n' +
+ '\n' +
+ '/* @jsxRuntime classic */\n' +
+ '\n' +
+ '/* @jsx mdx */\n' +
+ 'var layoutProps = {};\n' +
+ 'var MDXLayout = "wrapper";\n' +
+ '\n' +
+ 'function MDXContent(_ref) {\n' +
+ ' var components = _ref.components,\n' +
+ ' props = _objectWithoutProperties(_ref, ["components"]);\n' +
+ '\n' +
+ ' return mdx(MDXLayout, _extends({}, layoutProps, props, {\n' +
+ ' components: components,\n' +
+ ' mdxType: "MDXLayout"\n' +
+ ' }), mdx("h1", { className: "g-type-display-2" }, "Example Page"), mdx("p", null, "This is a cool docs page!"));\n' +
+ '}\n' +
+ '\n' +
+ ';\n' +
+ 'MDXContent.isMDXComponent = true;',
+ renderedOutput:
+ '
Example Page
This is a cool docs page!
',
+ scope: {},
+ },
+ },
+ frontMatter: {
+ type: 'object',
+ required: true,
+ description: "Frontmatter object parsed from the page's `.mdx` file.",
+ properties: {
+ canonical_url: {
+ type: 'string',
+ description:
+ 'Optional canonical URL. Passed directly to [@hashicorp/react-head](/?component=Head).',
+ },
+ description: {
+ type: 'string',
+ description:
+ 'Used for the ``. Passed directly to [@hashicorp/react-head](/?component=Head).',
+ required: true,
+ testValue: 'Test description',
+ },
+ page_title: {
+ type: 'string',
+ description:
+ 'Used to construct the meta `` tag, then passed to [@hashicorp/react-head](/?component=Head).',
+ required: true,
+ testValue: 'Test Page',
+ },
+ },
+ testValue: {},
+ },
+ currentPath: docsSidenavProps.currentPath,
+ navData: docsSidenavProps.navData,
+ },
+ testValue: {},
},
}
diff --git a/packages/docs-page/render-page-mdx.js b/packages/docs-page/render-page-mdx.js
new file mode 100644
index 000000000..03c960dc0
--- /dev/null
+++ b/packages/docs-page/render-page-mdx.js
@@ -0,0 +1,31 @@
+import path from 'path'
+import renderToString from 'next-mdx-remote/render-to-string'
+import markdownDefaults from '@hashicorp/nextjs-scripts/markdown'
+import generateComponents from './components'
+import grayMatter from 'gray-matter'
+
+async function renderPageMdx(
+ mdxFileString,
+ {
+ productName,
+ mdxContentHook = (c) => c,
+ additionalComponents = {},
+ remarkPlugins = [],
+ scope,
+ } = {}
+) {
+ const components = generateComponents(productName, additionalComponents)
+ const { data: frontMatter, content: rawContent } = grayMatter(mdxFileString)
+ const content = mdxContentHook(rawContent)
+ const mdxSource = await renderToString(content, {
+ mdxOptions: markdownDefaults({
+ resolveIncludes: path.join(process.cwd(), 'content/partials'),
+ addRemarkPlugins: remarkPlugins,
+ }),
+ components,
+ scope,
+ })
+ return { mdxSource, frontMatter }
+}
+
+export default renderPageMdx
diff --git a/packages/docs-page/server.js b/packages/docs-page/server.js
index b3edfa949..1922eeab5 100644
--- a/packages/docs-page/server.js
+++ b/packages/docs-page/server.js
@@ -1,161 +1,158 @@
import fs from 'fs'
import path from 'path'
-import existsSync from 'fs-exists-sync'
-import readdirp from 'readdirp'
-import lineReader from 'line-reader'
-import moize from 'moize'
-import matter from 'gray-matter'
-import { safeLoad } from 'js-yaml'
-import renderToString from 'next-mdx-remote/render-to-string'
-import markdownDefaults from '@hashicorp/nextjs-scripts/markdown'
-import generateComponents from './components'
+import validateFilePaths from '@hashicorp/react-docs-sidenav/utils/validate-file-paths'
+import validateRouteStructure from '@hashicorp/react-docs-sidenav/utils/validate-route-structure'
+import renderPageMdx from './render-page-mdx'
-export async function generateStaticPaths(subpath) {
- const paths = await getStaticMdxPaths(
- path.join(process.cwd(), 'content', subpath)
- )
+// So far, we have a pattern of using a common value for
+// docs catchall route parameters: route/[[...page]].jsx.
+// This default parameter ID captures that pattern.
+// It can be overridden via options.
+const DEFAULT_PARAM_ID = 'page'
- return { paths, fallback: false }
+async function generateStaticPaths(
+ navDataFile,
+ localContentDir,
+ { paramId = DEFAULT_PARAM_ID } = {}
+) {
+ // Fetch and parse navigation data
+ const navData = await resolveNavData(navDataFile, localContentDir)
+ const paths = getPathsFromNavData(navData, paramId)
+ return paths
}
-export async function generateStaticProps({
- subpath,
- productName,
- params,
- additionalComponents,
- scope,
- remarkPlugins,
-}) {
- const docsPath = path.join(process.cwd(), 'content', subpath)
- const pagePath = params.page ? params.page.join('/') : '/'
+async function resolveNavData(filePath, localContentDir) {
+ const navDataFile = path.join(process.cwd(), filePath)
+ const navDataRaw = JSON.parse(fs.readFileSync(navDataFile, 'utf8'))
+ const withFilePaths = await validateFilePaths(navDataRaw, localContentDir)
+ return withFilePaths
+}
- // get frontmatter from all other pages in the category, for the sidebar
- const allFrontMatter = await fastReadFrontMatter(docsPath)
+async function getPathsFromNavData(
+ navDataResolved,
+ paramId = DEFAULT_PARAM_ID
+) {
+ // Transform navigation data into path arrays
+ const pagePathArrays = getPathArraysFromNodes(navDataResolved)
+ // Include an empty array for the "/" index page path
+ const allPathArrays = [[]].concat(pagePathArrays)
+ const paths = allPathArrays.map((p) => ({ params: { [paramId]: p } }))
+ return paths
+}
- // render the current page path markdown
- const { mdxSource, frontMatter, filePath } = await renderPageMdx(
- docsPath,
- pagePath,
- generateComponents(productName, additionalComponents),
+async function generateStaticProps(
+ navDataFile,
+ localContentDir,
+ params,
+ product,
+ {
+ additionalComponents = {},
+ mainBranch = 'main',
+ remarkPlugins = [],
+ scope, // optional, I think?
+ paramId = DEFAULT_PARAM_ID,
+ } = {}
+) {
+ // Read in the nav data, and resolve local filePaths
+ const navData = await resolveNavData(navDataFile, localContentDir)
+ // Build the currentPath from page parameters
+ const currentPath = params[paramId] ? params[paramId].join('/') : ''
+ // Get the navNode that matches this path
+ const navNode = getNodeFromPath(currentPath, navData, localContentDir)
+ // Read in and process MDX content from the navNode's filePath
+ const mdxFile = path.join(process.cwd(), navNode.filePath)
+ const mdxString = fs.readFileSync(mdxFile, 'utf8')
+ const { mdxSource, frontMatter } = await renderPageMdx(mdxString, {
+ productName: product.name,
+ additionalComponents,
+ remarkPlugins,
scope,
- remarkPlugins
- )
-
- return {
- props: {
- data: allFrontMatter.map((p) => ({
- ...p,
- __resourcePath: `${subpath}/${p.__resourcePath}`,
- })),
- mdxSource,
- frontMatter,
- filePath: `${subpath}/${filePath}`,
- pagePath: `/${subpath}/${pagePath}`,
- },
- }
+ })
+ // Construct the githubFileUrl, used for "Edit this page" link
+ const githubFileUrl = `https://github.com/hashicorp/${product.slug}/blob/${mainBranch}/website/${navNode.filePath}`
+ // Return all the props
+ return { currentPath, frontMatter, githubFileUrl, mdxSource, navData }
}
-async function getStaticMdxPaths(root) {
- const files = await readdirp.promise(root, { fileFilter: ['*.mdx'] })
+async function validateNavData(navData, localContentDir) {
+ const withFilePaths = await validateFilePaths(navData, localContentDir)
+ // Note: validateRouteStructure returns navData with additional __stack properties,
+ // which detail the path we've inferred for each branch and node
+ // (branches do not have paths defined explicitly, so we need to infer them)
+ // We don't actually need the __stack properties for rendering, they're just
+ // used in validation, so we don't use the output of this function.
+ validateRouteStructure(withFilePaths)
+ // Return the resolved, validated navData
+ return withFilePaths
+}
- return files.map(({ path: p }) => {
+function getNodeFromPath(pathToMatch, navData, localContentDir) {
+ // If there is no path array, we return a
+ // constructed "home page" node. This is just to
+ // provide authoring convenience to not have to define
+ // this node. However, we could ask for this node to
+ // be explicitly defined in `navData` (and if it isn't,
+ // then we'd render a 404 for the root path)
+ const isLandingPage = pathToMatch === ''
+ if (isLandingPage) {
return {
- params: {
- page: p
- .replace(/\.mdx$/, '')
- .split('/')
- .filter((p) => p !== 'index'),
- },
+ filePath: path.join(localContentDir, 'index.mdx'),
}
- })
-}
-
-async function renderPageMdx(
- root,
- pagePath,
- components,
- scope,
- remarkPlugins = []
-) {
- // get the page being requested - figure out if its index page or leaf
- // prefer leaf if both are present
- const leafPath = path.join(root, `${pagePath}.mdx`)
- const indexPath = path.join(root, `${pagePath}/index.mdx`)
- let page, filePath
-
- if (existsSync(leafPath)) {
- page = fs.readFileSync(leafPath, 'utf8')
- filePath = leafPath
- } else if (existsSync(indexPath)) {
- page = fs.readFileSync(indexPath, 'utf8')
- filePath = indexPath
- } else {
- // NOTE: if we decide to let docs pages render dynamically, we should replace this
- // error with a straight 404, at least in production.
+ }
+ // If it's not a landing page, then we search
+ // through our navData to find the node with a path
+ // that matches the pathArray we're looking for.
+ function flattenRoutes(nodes) {
+ return nodes.reduce((acc, n) => {
+ if (!n.routes) return acc.concat(n)
+ return acc.concat(flattenRoutes(n.routes))
+ }, [])
+ }
+ const allNodes = flattenRoutes(navData)
+ const matches = allNodes.filter((n) => n.path === pathToMatch)
+ // Throw an error for missing files - if this happens,
+ // we might have an issue with `getStaticPaths` or something
+ if (matches.length === 0) {
+ throw new Error(`Missing resource to match "${pathToMatch}"`)
+ }
+ // Throw an error if there are multiple matches
+ // If this happens, there's likely an issue in the
+ // content source repo
+ if (matches.length > 1) {
throw new Error(
- `We went looking for "${leafPath}" and "${indexPath}" but neither one was found.`
+ `Ambiguous path matches for "${pathToMatch}". Found:\n\n${JSON.stringify(
+ matches
+ )}`
)
}
-
- const { data: frontMatter, content } = matter(page)
- const mdxSource = await renderToString(content, {
- mdxOptions: markdownDefaults({
- resolveIncludes: path.join(process.cwd(), 'content/partials'),
- addRemarkPlugins: remarkPlugins,
- }),
- components,
- scope,
- })
-
- return {
- mdxSource,
- frontMatter,
- filePath: filePath.replace(`${root}/`, ''),
- }
+ // Otherwise, we have exactly one match,
+ // and we can return the filePath off of it
+ return matches[0]
}
-// We are memoizing this function as it does a non-trivial amount of I/O to read frontmatter for all mdx files in a directory
-export const fastReadFrontMatter =
- process.env.NODE_ENV === 'production'
- ? moize(fastReadFrontMatterFn)
- : fastReadFrontMatterFn
+function getPathArraysFromNodes(navNodes) {
+ const slugs = navNodes.reduce((acc, navNode) => {
+ // Individual items have a path, these should be added
+ if (navNode.path) return acc.concat([navNode.path.split('/')])
+ // Category items have child routes, these should all be added
+ if (navNode.routes)
+ return acc.concat(getPathArraysFromNodes(navNode.routes))
+ // All other node types (dividers, external links) can be ignored
+ return acc
+ }, [])
+ return slugs
+}
-async function fastReadFrontMatterFn(p) {
- const fm = []
- for await (const entry of readdirp(p, { fileFilter: '*.mdx' })) {
- let lineNum = 0
- const content = []
- fm.push(
- new Promise((resolve, reject) => {
- lineReader.eachLine(
- entry.fullPath,
- (line) => {
- // if it has any content other than `---`, the file doesn't have front matter, so we close
- if (lineNum === 0 && !line.match(/^---$/)) {
- console.warn(
- `WARNING: The file "${entry.path}" is missing front matter. Please add front matter to ensure the file's metadata is properly populated.`
- )
- content.push('---')
- content.push('page_title: "ERROR: Missing Frontmatter"')
- return false
- }
- // if it's not the first line and we have a bottom delimiter, exit
- if (lineNum !== 0 && line.match(/^---$/)) return false
- // now we read lines until we match the bottom delimiters
- content.push(line)
- // increment line number
- lineNum++
- },
- (err) => {
- if (err) return reject(err)
- content.push(`__resourcePath: "${entry.path}"`)
- resolve(safeLoad(content.slice(1).join('\n')), {
- filename: entry.fullPath,
- })
- }
- )
- })
- )
- }
- return Promise.all(fm)
+// We currently export most utilities individually,
+// since we have cases such as Packer remote plugin docs
+// where we want to re-use these utilities to build
+// getStaticPaths and getStaticProps functions that
+// fall outside the use case of local-only content
+export {
+ generateStaticPaths,
+ generateStaticProps,
+ getNodeFromPath,
+ getPathsFromNavData,
+ validateNavData,
+ validateFilePaths,
}
diff --git a/packages/docs-page/temporary_jump-to-section.js b/packages/docs-page/temporary_jump-to-section.js
index 40fc0cb7c..b52310a29 100644
--- a/packages/docs-page/temporary_jump-to-section.js
+++ b/packages/docs-page/temporary_jump-to-section.js
@@ -14,7 +14,7 @@ export default function temporary_injectJumpToSection(node) {
// slice removes the anchor link character
return {
id: h2.querySelector('.__target-h').id,
- text: h2.innerText.slice(1),
+ text: h2.textContent.slice(1), // slice removes permalink ยป character
}
})
diff --git a/packages/docs-sidenav/README.md b/packages/docs-sidenav/README.md
index 31420e860..99b3f5097 100644
--- a/packages/docs-sidenav/README.md
+++ b/packages/docs-sidenav/README.md
@@ -2,6 +2,6 @@
Side navigation for HashiCorp's product documentation. Fairly tightly tied to our specific documentation sites and the format that middleman uses to catalog pages and frontmatter at the moment.
-### Props
+## Props
See [the props file](props.js) for more details.
diff --git a/packages/docs-sidenav/chevron-icon.js b/packages/docs-sidenav/chevron-icon.js
deleted file mode 100644
index cebb2a374..000000000
--- a/packages/docs-sidenav/chevron-icon.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from 'react'
-
-export default function ChevronIcon() {
- return (
-
- )
-}
diff --git a/packages/docs-sidenav/docs.mdx b/packages/docs-sidenav/docs.mdx
index aeb0f35fb..f3c0d6696 100644
--- a/packages/docs-sidenav/docs.mdx
+++ b/packages/docs-sidenav/docs.mdx
@@ -2,14 +2,13 @@
componentName: 'DocsSidenav'
---
-This is a cool component for documentation
+DocsSidenav renders a tree of links, with the option to include nested sections. Horizontal dividers can also be included between items.
{``}
diff --git a/packages/docs-sidenav/fixtures/content/agent/index.mdx b/packages/docs-sidenav/fixtures/content/agent/index.mdx
new file mode 100644
index 000000000..131609398
--- /dev/null
+++ b/packages/docs-sidenav/fixtures/content/agent/index.mdx
@@ -0,0 +1,173 @@
+---
+page_title: Vault Agent
+description: |-
+ Vault Agent is a client-side daemon that can be used to perform some Vault
+ functionality automatically.
+---
+
+# Vault Agent
+
+Vault Agent is a client daemon that provides the following features:
+
+- [Auto-Auth][autoauth] - Automatically authenticate to Vault and manage the token renewal process for locally-retrieved dynamic secrets.
+- [Caching][caching] - Allows client-side caching of responses containing newly created tokens and responses containing leased secrets generated off of these newly created tokens.
+- [Windows Service][winsvc] - Allows running the Vault Agent as a Windows service.
+- [Templating][template] - Allows rendering of user supplied templates by Vault Agent, using the token generated by the Auto-Auth step.
+ To get help, run:
+
+```shell-session
+$ vault agent -h
+```
+
+## Auto-Auth
+
+Vault Agent allows for easy authentication to Vault in a wide variety of
+environments. Please see the [Auto-Auth docs][autoauth]
+for information.
+
+Auto-Auth functionality takes place within an `auto_auth` configuration stanza.
+
+## Caching
+
+Vault Agent allows client-side caching of responses containing newly created tokens
+and responses containing leased secrets generated off of these newly created tokens.
+Please see the [Caching docs][caching] for information.
+
+## Configuration
+
+These are the currently-available general configuration option:
+
+- `vault` ([vault][vault]: ) - Specifies the remote Vault server the Agent connects to.
+
+- `auto_auth` ([auto_auth][autoauth]: ) - Specifies the method and other options used for Auto-Auth functionality.
+
+- `cache` ([cache][caching]: ) - Specifies options used for Caching functionality.
+
+- `listener` ([listener][listener]: ) - Specifies the addresses and ports on which the Agent will respond to requests.
+
+- `pid_file` `(string: "")` - Path to the file in which the agent's Process ID
+ (PID) should be stored
+
+- `exit_after_auth` `(bool: false)` - If set to `true`, the agent will exit
+ with code `0` after a single successful auth, where success means that a
+ token was retrieved and all sinks successfully wrote it
+
+- `template` ([template][template]: ) - Specifies options used for templating Vault secrets to files.
+
+### vault Stanza
+
+There can at most be one top level `vault` block and it has the following
+configuration entries:
+
+- `address` `(string: )` - The address of the Vault server. This should
+ be a complete URL such as `https://127.0.0.1:8200`. This value can be
+ overridden by setting the `VAULT_ADDR` environment variable.
+
+- `ca_cert` `(string: )` - Path on the local disk to a single PEM-encoded
+ CA certificate to verify the Vault server's SSL certificate. This value can
+ be overridden by setting the `VAULT_CACERT` environment variable.
+
+- `ca_path` `(string: )` - Path on the local disk to a directory of
+ PEM-encoded CA certificates to verify the Vault server's SSL certificate.
+ This value can be overridden by setting the `VAULT_CAPATH` environment
+ variable.
+
+- `client_cert` `(string: )` - Path on the local disk to a single
+ PEM-encoded CA certificate to use for TLS authentication to the Vault server.
+ This value can be overridden by setting the `VAULT_CLIENT_CERT` environment
+ variable.
+
+- `client_key` `(string: )` - Path on the local disk to a single
+ PEM-encoded private key matching the client certificate from `client_cert`.
+ This value can be overridden by setting the `VAULT_CLIENT_KEY` environment
+ variable.
+
+- `tls_skip_verify` `(string: )` - Disable verification of TLS
+ certificates. Using this option is highly discouraged as it decreases the
+ security of data transmissions to and from the Vault server. This value can
+ be overridden by setting the `VAULT_SKIP_VERIFY` environment variable.
+
+- `tls_server_name` `(string: )` - Name to use as the SNI host when
+ connecting via TLS. This value can be overridden by setting the
+ `VAULT_TLS_SERVER_NAME` environment variable.
+
+### listener Stanza
+
+Agent supports one or more [listener][listener_main] stanzas. In addition to
+the standard listener configuration, an Agent's listener configuration also
+supports an additional optional entry:
+
+- `require_request_header` `(bool: false)` - Require that all incoming HTTP
+ requests on this listener must have an `X-Vault-Request: true` header entry.
+ Using this option offers an additional layer of protection from Server Side
+ Request Forgery attacks. Requests on the listener that do not have the proper
+ `X-Vault-Request` header will fail, with a HTTP response status code of `412: Precondition Failed`.
+
+## Example Configuration
+
+An example configuration, with very contrived values, follows:
+
+```python
+pid_file = "./pidfile"
+
+vault {
+ address = "https://127.0.0.1:8200"
+}
+
+auto_auth {
+ method "aws" {
+ mount_path = "auth/aws-subaccount"
+ config = {
+ type = "iam"
+ role = "foobar"
+ }
+ }
+
+ sink "file" {
+ config = {
+ path = "/tmp/file-foo"
+ }
+ }
+
+ sink "file" {
+ wrap_ttl = "5m"
+ aad_env_var = "TEST_AAD_ENV"
+ dh_type = "curve25519"
+ dh_path = "/tmp/file-foo-dhpath2"
+ config = {
+ path = "/tmp/file-bar"
+ }
+ }
+}
+
+cache {
+ use_auto_auth_token = true
+}
+
+listener "unix" {
+ address = "/path/to/socket"
+ tls_disable = true
+}
+
+listener "tcp" {
+ address = "127.0.0.1:8100"
+ tls_disable = true
+}
+
+template {
+ source = "/etc/vault/server.key.ctmpl"
+ destination = "/etc/vault/server.key"
+}
+
+template {
+ source = "/etc/vault/server.crt.ctmpl"
+ destination = "/etc/vault/server.crt"
+}
+```
+
+[vault]: /docs/agent#vault-stanza
+[autoauth]: /docs/agent/autoauth
+[caching]: /docs/agent/caching
+[template]: /docs/agent/template
+[listener]: /docs/agent#listener-stanza
+[listener_main]: /docs/configuration/listener/tcp
diff --git a/packages/docs-sidenav/fixtures/content/ambiguous.mdx b/packages/docs-sidenav/fixtures/content/ambiguous.mdx
new file mode 100644
index 000000000..f86aab5d3
--- /dev/null
+++ b/packages/docs-sidenav/fixtures/content/ambiguous.mdx
@@ -0,0 +1,17 @@
+---
+page_title: Ambiguous
+description: >-
+ This file is located at both ambiguous.mdx and ambiguous/index.mdx, so trying to
+ include it should throw an error.
+---
+
+# Introduction to Vault
+
+Welcome to the introduction guide to HashiCorp Vault! This guide is the best
+place to get started with Vault. This guide covers what Vault is, what problems
+it can solve, how it compares to existing software, and contains a quick start
+for using Vault.
+
+If you are already familiar with the basics of Vault, the
+[documentation](/docs) provides a better reference guide for all
+available features as well as internals.
diff --git a/packages/docs-sidenav/fixtures/content/ambiguous/index.mdx b/packages/docs-sidenav/fixtures/content/ambiguous/index.mdx
new file mode 100644
index 000000000..f86aab5d3
--- /dev/null
+++ b/packages/docs-sidenav/fixtures/content/ambiguous/index.mdx
@@ -0,0 +1,17 @@
+---
+page_title: Ambiguous
+description: >-
+ This file is located at both ambiguous.mdx and ambiguous/index.mdx, so trying to
+ include it should throw an error.
+---
+
+# Introduction to Vault
+
+Welcome to the introduction guide to HashiCorp Vault! This guide is the best
+place to get started with Vault. This guide covers what Vault is, what problems
+it can solve, how it compares to existing software, and contains a quick start
+for using Vault.
+
+If you are already familiar with the basics of Vault, the
+[documentation](/docs) provides a better reference guide for all
+available features as well as internals.
diff --git a/packages/docs-sidenav/fixtures/content/what-is-vault.mdx b/packages/docs-sidenav/fixtures/content/what-is-vault.mdx
new file mode 100644
index 000000000..a220e6cb1
--- /dev/null
+++ b/packages/docs-sidenav/fixtures/content/what-is-vault.mdx
@@ -0,0 +1,72 @@
+---
+page_title: Introduction
+description: >-
+ Welcome to the intro guide to Vault! This guide is the best place to start
+ with Vault. We cover what Vault is, what problems it can solve, how it
+ compares to existing software, and contains a quick start for using Vault.
+---
+
+# Introduction to Vault
+
+Welcome to the introduction guide to HashiCorp Vault! This guide is the best
+place to get started with Vault. This guide covers what Vault is, what problems
+it can solve, how it compares to existing software, and contains a quick start
+for using Vault.
+
+If you are already familiar with the basics of Vault, the
+[documentation](/docs) provides a better reference guide for all
+available features as well as internals.
+
+## What is Vault?
+
+Vault is a tool for securely accessing _secrets_. A secret is anything that you
+want to tightly control access to, such as API keys, passwords, or certificates.
+Vault provides a unified interface to any secret, while providing tight access
+control and recording a detailed audit log.
+
+A modern system requires access to a multitude of secrets: database credentials,
+API keys for external services, credentials for service-oriented architecture
+communication, etc. Understanding who is accessing what secrets is already very
+difficult and platform-specific. Adding on key rolling, secure storage, and
+detailed audit logs is almost impossible without a custom solution. This is
+where Vault steps in.
+
+Examples work best to showcase Vault. Please see the
+[use cases](/docs/use-cases).
+
+The key features of Vault are:
+
+- **Secure Secret Storage**: Arbitrary key/value secrets can be stored
+ in Vault. Vault encrypts these secrets prior to writing them to persistent
+ storage, so gaining access to the raw storage isn't enough to access
+ your secrets. Vault can write to disk, [Consul](https://www.consul.io),
+ and more.
+
+- **Dynamic Secrets**: Vault can generate secrets on-demand for some
+ systems, such as AWS or SQL databases. For example, when an application
+ needs to access an S3 bucket, it asks Vault for credentials, and Vault
+ will generate an AWS keypair with valid permissions on demand. After
+ creating these dynamic secrets, Vault will also automatically revoke them
+ after the lease is up.
+
+- **Data Encryption**: Vault can encrypt and decrypt data without storing
+ it. This allows security teams to define encryption parameters and
+ developers to store encrypted data in a location such as SQL without
+ having to design their own encryption methods.
+
+- **Leasing and Renewal**: All secrets in Vault have a _lease_ associated
+ with them. At the end of the lease, Vault will automatically revoke that
+ secret. Clients are able to renew leases via built-in renew APIs.
+
+- **Revocation**: Vault has built-in support for secret revocation. Vault
+ can revoke not only single secrets, but a tree of secrets, for example
+ all secrets read by a specific user, or all secrets of a particular type.
+ Revocation assists in key rolling as well as locking down systems in the
+ case of an intrusion.
+
+## Next Steps
+
+See the page on [Vault use cases](/docs/use-cases) to see the multiple ways
+Vault can be used. Then, continue onwards with the [getting started
+guide](https://learn.hashicorp.com/vault/getting-started/install) to use Vault
+to read, write, and create real secrets and see how it works in practice.
diff --git a/packages/docs-sidenav/fixtures/nav-data.json b/packages/docs-sidenav/fixtures/nav-data.json
new file mode 100644
index 000000000..3c8e2926e
--- /dev/null
+++ b/packages/docs-sidenav/fixtures/nav-data.json
@@ -0,0 +1,77 @@
+[
+ {
+ "title": "What is Vault?",
+ "path": "what-is-vault"
+ },
+ {
+ "title": "Vault Agent",
+ "routes": [
+ {
+ "title": "Overview",
+ "path": "agent"
+ },
+ {
+ "title": "Auto-Auth",
+ "routes": [
+ {
+ "title": "Overview",
+ "path": "agent/autoauth"
+ },
+ {
+ "title": "AWS Agent",
+ "path": "agent/autoauth/aws"
+ },
+ {
+ "title": "Methods",
+ "routes": [
+ {
+ "title": "Overview",
+ "path": "agent/autoauth/methods"
+ },
+ {
+ "title": "AliCloud",
+ "path": "agent/autoauth/methods/alicloud"
+ },
+ {
+ "title": "AWS",
+ "path": "agent/autoauth/methods/aws"
+ },
+ { "divider": true },
+ {
+ "title": "GCP",
+ "path": "agent/autoauth/methods/gcp"
+ },
+ { "divider": true },
+ {
+ "title": "External Link",
+ "href": "https://google.com"
+ },
+ {
+ "title": "Internal Direct Link",
+ "href": "/some-non-docs-path"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "title": "No Index Category",
+ "routes": [
+ {
+ "title": "Foo Item",
+ "path": "agent/no-index-test/foo"
+ }
+ ]
+ },
+ {
+ "title": "Only Index Test ENT",
+ "routes": [
+ {
+ "title": "Overview",
+ "path": "agent/only-index-test"
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/packages/docs-sidenav/icons/bullet.svg b/packages/docs-sidenav/icons/bullet.svg
new file mode 100644
index 000000000..6e68f3963
--- /dev/null
+++ b/packages/docs-sidenav/icons/bullet.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/docs-sidenav/icons/chevron.svg b/packages/docs-sidenav/icons/chevron.svg
new file mode 100644
index 000000000..7b0cfaaee
--- /dev/null
+++ b/packages/docs-sidenav/icons/chevron.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/docs-sidenav/img/external.svg b/packages/docs-sidenav/icons/external-link.svg
similarity index 100%
rename from packages/docs-sidenav/img/external.svg
rename to packages/docs-sidenav/icons/external-link.svg
diff --git a/packages/docs-sidenav/menu-icon.js b/packages/docs-sidenav/icons/menu.svg
similarity index 94%
rename from packages/docs-sidenav/menu-icon.js
rename to packages/docs-sidenav/icons/menu.svg
index a3b50f53b..c586219cc 100644
--- a/packages/docs-sidenav/menu-icon.js
+++ b/packages/docs-sidenav/icons/menu.svg
@@ -1,14 +1,8 @@
-import React from 'react'
-
-export default function MenuIcon() {
- return (
-
\ No newline at end of file
diff --git a/packages/docs-sidenav/index.js b/packages/docs-sidenav/index.js
index 997179d4b..194b3c309 100644
--- a/packages/docs-sidenav/index.js
+++ b/packages/docs-sidenav/index.js
@@ -1,478 +1,237 @@
-import React, { useState, useMemo } from 'react'
+import React, { useCallback, useEffect, useRef, useState } from 'react'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+// @hashicorp imports
import useProductMeta from '@hashicorp/nextjs-scripts/lib/providers/product-meta'
-import LinkWrap from '@hashicorp/react-link-wrap'
-import MenuIcon from './menu-icon'
-import ChevronIcon from './chevron-icon'
-import fuzzysearch from 'fuzzysearch'
+import LinkWrap, { isAbsoluteURL } from '@hashicorp/react-link-wrap'
+import InlineSvg from '@hashicorp/react-inline-svg'
+// local utilities
+import flagActiveNodes from './utils/flag-active-nodes'
+import filterContent from './utils/filter-content'
+import useEventListener from './utils/use-event-listener'
+// svg
+import svgMenuIcon from './icons/menu.svg?include'
+import svgChevron from './icons/chevron.svg?include'
+import svgBullet from './icons/bullet.svg?include'
+import svgExternalLink from './icons/external-link.svg?include'
+// styles
+import s from './style.module.css'
export default function DocsSidenav({
- data,
- order,
- currentPage,
- category,
- Link,
+ currentPath,
+ baseRoute,
product,
+ navData,
disableFilter = false,
}) {
- const [open, setOpen] = useState(false)
- const [filterInput, setFilterInput] = useState('')
- const { themeClass } = useProductMeta(product)
+ const router = useRouter()
+ const pathname = router ? router.pathname : null
- // we memoize here as the page matching is a pure, expensive calculation that
- // does not need to re-run every render
- const allContent = useMemo(
- () => matchOrderToData(currentPage, order, calculatePath(data, category)),
- [order, data, category, currentPage]
- )
- const [content, setContent] = useState(allContent)
+ // Get theme class
+ // ( note: we could consider getting the product prop here,
+ // rather than requiring it to be passed in )
+ const { themeClass } = useProductMeta(product)
- // remove leading slash and base level "docs"/"api"/etc
- const currentPath = currentPage
- .split('/')
- .slice(1 + category.split('/').length)
+ // Set up filtering state
+ const [filterInput, setFilterInput] = useState('')
+ const [content, setContent] = useState(navData)
+ const [filteredContent, setFilteredContent] = useState(navData)
+
+ // isMobileOpen controls menu open / close state
+ const [isMobileOpen, setIsMobileOpen] = useState(false)
+ // isMobileFullyHidden reflects if the menu is fully transitioned to a hidden state
+ const [isMenuFullyHidden, setIsMenuFullyHidden] = useState(true)
+ // We want to avoid exposing links to keyboard navigation
+ // when the menu is hidden on mobile. But we don't want our
+ // menu to flash when hide and shown. To meet both needs,
+ // we listen for transition end on the menu element, and when
+ // a transition ends and the menu is not open, we set isMenuFullyHidden
+ // which translates into a visibility: hidden CSS property
+ const menuRef = useRef(null)
+ const handleMenuTransitionEnd = useCallback(() => {
+ setIsMenuFullyHidden(!isMobileOpen)
+ }, [isMobileOpen, setIsMenuFullyHidden])
+ useEventListener('transitionend', handleMenuTransitionEnd, menuRef.current)
+
+ // When client-side navigation occurs,
+ // we want to close the mobile rather than keep it open
+ useEffect(() => {
+ setIsMobileOpen(false)
+ }, [pathname])
+
+ // When path-related data changes, update content to ensure
+ // `__isActive` props on each content item are up-to-date
+ // Note: we could also reset filter input here, if we don't
+ // want to filter input to persist across client-side nav, ie:
+ // setFilterInput("")
+ useEffect(() => {
+ if (!navData) return
+ setContent(flagActiveNodes(navData, currentPath, pathname))
+ }, [currentPath, navData, pathname])
+
+ // When filter input changes, update content
+ // to filter out items that don't match
+ useEffect(() => {
+ setFilteredContent(filterContent(content, filterInput))
+ }, [filterInput, content])
return (
-
)
}
-// Filter nav items
-function filterInputChange(setFilterInput, allContent, setContent, e) {
- setFilterInput(e.target.value)
- setContent(findContent(allContent, e.target.value.toLowerCase()))
-}
-
-function findContent(content, value) {
- // if there's no search value we short-circuit and return everything
- if (!value) return content
-
- return content.reduce((acc, item) => {
- // recurse on content, depth-first
- if (item.content) item.content = findContent(item.content, value)
-
- // here we check for conditions on a branch node
- // first, if a branch has children, that means at least one of the leaf nodes has
- // matched, so we push the branch so that the leaf is displayed
- const hasContent = item.content && item.content.length
- // second, we check to see if a branch's title matches against the query, if so we
- // push the branch because it matched directly
- const categoryTitleMatches =
- item.indexData &&
- fuzzysearch(value, item.indexData.page_title.toLowerCase())
- if (hasContent || categoryTitleMatches) {
- if (categoryTitleMatches) {
- item.matchedFilter = true
- }
- acc.push(item)
- }
-
- // now we check against content on leaf nodes
- if (item.page_title && fuzzysearch(value, item.page_title.toLowerCase())) {
- item.matchedFilter = true
- acc.push(item)
- }
-
- // and that's all!
- return acc
- }, [])
-}
-
-// Given a set of front matter data, adds a `path` variable formatted for correct links
-function calculatePath(pageData, category) {
- return pageData.map((p) => ({
- ...p,
- path: p.__resourcePath
- .split('/')
- .slice(category.split('/').length)
- .join('/')
- .replace(/\.mdx$/, ''),
- }))
-}
-
-// Matches up user-defined navigation hierarcy with front matter from the correct pages.
-//
-// For context, the user-defined nav hierarchy is called "content" in this function, and
-// the shape of its data is as such:
-// [{
-// category: 'foo',
-// content: ['bar', 'baz']
-// },
-// '--------------',
-// {
-// category: 'quux'
-// }]
-//
-// The front matter data is structured as such:
-// {
-// __resourcePath: '/docs/foo/bar.mdx',
-// path: 'foo/bar',
-// ...frontMatterObject
-// }
-function matchOrderToData(currentPage, order, pageData, stack = []) {
- // go through each item in the user-established order
- return order.map((item) => {
- if (typeof item === 'string') {
- // if a string like '-----' is given, we render a divider
- if (item.match(/^-+$/)) return item
-
- // if we have a string, that's a terminal page. we match it with
- // the provided page data and return the enhanced object
- const itemData = pageData.filter((page) => {
- // break down the full path and strip the html extension
- const pageDataPath = page.path.split('/')
- // copy the stack and push the item as the file path
- const contentPath = [...stack, item]
- // match them up!
- return pageDataPath.join('/') === contentPath.join('/')
- })[0]
-
- // If we don't have a match here, the user has defined a page that doesn't exist, so let's give them
- // a very clear error message on how to resolve this situation.
- if (!itemData) {
- const pageCategory = currentPage.split('/')[1]
- const missingPath = `${pageCategory}/${stack.join('/')}/${item}.mdx`
- const cat = `${stack.join('/')}`
- throw new Error(
- `The page "${item}" was not found within the category "${cat}". Please double-check to ensure that "${missingPath}" exists. If this page was never intended to exist, remove the key "${item}" from the category "${cat}" in "data/${pageCategory}-navigation.js"`
- )
- }
-
- return itemData
- } else if (item.title && item.href) {
- // this is the syntax for direct links, we can return them directly
- return item
- } else {
- // catch errors where direct links are formatted incorrectly
- if (item.title || item.href) {
- throw new Error(
- `Malformed direct sidebar link:\n\n ${JSON.stringify(
- item
- )}\n\nDirect links must have a "href" and a "title" property.`
- )
- }
-
- // this method mutates the object, which causes an error on subsequent renders,
- // so we clone it first.
- const _item = Object.assign({}, item)
-
- // keep track of all parent categories
- _item.stack = stack.concat(_item.category)
-
- // using a category without content is not allowed
- if (_item.category && !_item.content) {
- const topLevelCategory = currentPage.split('/')[1]
- throw new Error(
- `The item "${_item.stack.join(
- '/'
- )}" within "data/${topLevelCategory}-navigation.js" has a category but no content, indicating that there is a folder that contains only an "index.mdx" file, which is not allowed. To fix this, move and rename "pages/${topLevelCategory}/${
- _item.stack.join('/') + '/index.mdx'
- }" to "pages/${topLevelCategory}/${
- _item.stack.join('/') + '.mdx'
- }", then change the value from "{ category: '${
- _item.category
- }' }" to just "${item.category}"`
- )
- }
-
- // grab the index page, as it can contain data about the top level link
- pageData.some((page) => {
- const pageDataPath = page.path.split('/')
-
- const depthLevelsMatch = _item.stack.length === pageDataPath.length - 1
- const pathItemsMatch = _item.stack.every(
- (s, i) => s === pageDataPath[i]
- )
- const isIndexFile = pageDataPath[pageDataPath.length - 1] === 'index'
-
- if (depthLevelsMatch && pathItemsMatch && isIndexFile) {
- _item.indexData = page
- // now that we know its an index page, we can remove it from the path, as
- // its not necessary for links
- _item.indexData.path = _item.indexData.path.replace(/\/index$/, '')
- return true
- }
- })
-
- // error handling for nav nesting mistakes
- if (!_item.indexData && !_item.name) {
- throw new Error(
- `An index page or "name" property is required for all categories.\nIf you would like an index page for this category, please add an index file at the path "${_item.stack.join(
- '/'
- )}/index.mdx".\nIf you do not want an index page for this category, please add a "name" property to the category object to specify the category's human-readable title.\n\nItem:\n${JSON.stringify(
- _item,
- null,
- 2
- )}`
- )
- }
-
- // using "name" and manually adding an "overview" page is silly. let's prevent that.
- if (_item.name && _item.content.includes('overview')) {
- throw new Error(`The category "${_item.stack.join(
- '/'
- )}" is using a "name" property to indicate that it has no index, but also has a manually added "overview" page. This can be fixed with the following steps:
-
-- Change the "overview.mdx" page to be "index.mdx"
-- Remove the "name" property from the "${
- _item.category
- }" data, instead indicate the category's name using the frontmatter on the new "index.mdx" page`)
- }
-
- // otherwise, it's a nested category. if the category has content, we
- // recurse, passing in that category's content, and the matching
- // subsection of page data from middleman
- if (_item.content) {
- _item.content = matchOrderToData(
- currentPage,
- _item.content,
- filterData(pageData, _item.category),
- _item.stack
- )
- }
-
- return _item
- }
- })
-}
-
-// Recursively renders the markup for the nested navigation
-function renderNavTree({
- category,
- content,
- currentPath,
- currentPage,
- filterInput,
- Link,
-}) {
+function NavTree({ baseRoute, content }) {
return content.map((item, idx) => {
- // dividers are the only items left as strings
- // This array is stable, so we can use index as key
- // eslint-disable-next-line react/no-array-index-key
- if (typeof item === 'string') return
-
- // if the link property has been set to true, we're rendering a direct link
- // rather than a link to a docs page
+ // Dividers
+ if (item.divider) {
+ // eslint-disable-next-line react/no-array-index-key
+ return
+ }
+ // Direct links
if (item.title && item.href) {
- let className = item.href.match(/^http[s]*:\/\//) ? 'external ' : ''
- // allow direct links to be highlighted if they happen to live in the docs hierarchy
- if (item.href === currentPage) className += 'active'
-
return (
-
-
-
+
)
}
-
+ // Individual pages (leaf nodes)
if (item.path) {
- // if the item has a path, it's a leaf node so we render a link to the page
- let className = ''
- if (
- fileMatch(
- item.path.split('/').filter((x) => x),
- currentPath.filter((x) => x)
- )
- )
- className += 'active '
- if (item.matchedFilter) className += 'matched'
-
- return (
-
-
-
- )
- } else {
- // if not, its an index page in a folder, so we render it as an expandable category
- // alternately its a folder with no index page, in which case we skip the "overview" link
-
- // here we search for the sidebar title. if the category has an index page, we look on this
- // first, preferring sidebar_title and falling back to page_title. next we look for name, which
- // is the standard for categories without index files, and all else failing, we use the raw
- // folder name itself
- const title = item.indexData
- ? item.indexData.sidebar_title || item.indexData.page_title
- : item.name || item.category
-
- // we need to know the path of the category/folder itself, which we can get from the stack
- const folderPath = item.stack.join('/')
-
- // now we test whether the current url is a match for the category and the page
- const categoryMatches = categoryMatch(folderPath.split('/'), currentPath)
- const fileMatches = fileMatch(
- folderPath.split('/').filter((x) => x),
- currentPath.filter((x) => x)
- )
- ? 'active'
- : ''
- const containsFilterMatches = findFilterMatches(item)
-
- let className = ''
- if (item.content) className += 'dir '
- if (categoryMatches) className += 'open active '
- if (containsFilterMatches) className += 'open '
- if (item.matchedFilter && !item.content) className += 'matched'
-
- // and finally, we can render the folder
return (
-
-
- {/* Note: this is rendered as a link, but with no href. We should test to see if */}
- {/* a button element would be more semantically appropriate for a11y. (https://app.asana.com/0/1100423001970639/1199667739287943/f) */}
- {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
-
- {item.content ? (
- <>
- {' '}
- {
-
- }
- >
- ) : (
-
- )}
-
-
-
- {/* if the item has content, we need to recurse */}
- {item.content && (
-
+
)
}
+ // Otherwise, render a nav branch
+ // (this will recurse and render a nav tree)
+ return (
+
+ )
})
}
-// Given a single item, returns whether it or any of its children have the
-// `matchedFilter` property.
-function findFilterMatches(item) {
- if (item.matchedFilter) return true
+function NavBranch({ title, routes, baseRoute, isActive, isFiltered }) {
+ const [isOpen, setIsOpen] = useState(false)
+
+ // Ensure categories appear open if they're active
+ // or match the current filter
+ useEffect(() => setIsOpen(isActive || isFiltered), [isActive, isFiltered])
+
return (
- item.content &&
- item.content.map((child) => findFilterMatches(child)).some((x) => x)
+
+
+
+
+
+
+
)
}
-// Given an array of pages, returns only pages whose paths contain the given category.
-function filterData(data, category) {
- return data.filter((d) => d.path.split('/').indexOf(category) > -1)
-}
-
-// If the nav item category is entirely contained by the current page's path,
-// this means we're inside that category and should mark it as open.
-function categoryMatch(navItemPath, currentPath) {
- return navItemPath.every((item, i) => item === currentPath[i])
+function NavLeaf({ title, url, isActive }) {
+ // if the item has a path, it's a leaf node so we render a link to the page
+ return (
+
+ )
}
-// If the current page's path exactly matches the passed in nav item's path,
-// we have a match and can highlight the currently active page.
-function fileMatch(navItemPath, currentPath) {
- if (currentPath.length !== navItemPath.length) return false
- return currentPath.every((item, i) => item === navItemPath[i])
+function DirectLink({ title, href, isActive }) {
+ return (
+
+ )
}
-// Opens and closes a given nav category, the easy way
-function toggleNav(e) {
- e.preventDefault()
- e.currentTarget.parentElement.parentElement.classList.toggle('open')
+function Divider() {
+ return
}
diff --git a/packages/docs-sidenav/index.test.js b/packages/docs-sidenav/index.test.js
index 27d196a42..8dc044c7a 100644
--- a/packages/docs-sidenav/index.test.js
+++ b/packages/docs-sidenav/index.test.js
@@ -1,145 +1,100 @@
-import 'regenerator-runtime/runtime'
import { render, fireEvent, screen } from '@testing-library/react'
import DocsSidenav from './'
-import expectThrow from '../../__test-helpers/expect-throw'
import props from './props'
import { getTestValues } from 'swingset/testing'
const defaultProps = getTestValues(props)
describe('', () => {
- it('should render and display nesting levels correctly', () => {
- render()
- expect(screen.getByTestId('root').className).toContain('g-docs-sidenav')
+ it('renders a root element with a g-docs-sidenav className', () => {
+ const { container } = render()
+ expect(container.firstChild.className).toContain('g-docs-sidenav')
+ })
+ it('renders and displays nesting levels correctly', () => {
+ render()
// For this test, we step through the expected nesting levels based on
// the fixture data, ensuring that each level is nested properly and has
// the classes to reflect whether it's shown as active
- const levelOne = screen.getByTestId('/docs/agent')
- expect(levelOne.className).toMatch(/dir/)
- expect(levelOne.className).toMatch(/open active/)
+ const branchOne = screen.getByText('Vault Agent').closest('button')
+ expect(branchOne.getAttribute('data-is-active')).toBe('true')
+ expect(branchOne.getAttribute('data-is-open')).toBe('true')
- const levelTwo = screen.getByTestId('/docs/agent/autoauth')
- expect(levelTwo.className).toMatch(/dir/)
- expect(levelTwo.className).toMatch(/open active/)
+ const branchTwo = screen.getByText('Auto-Auth').closest('button')
+ expect(branchTwo.getAttribute('data-is-active')).toBe('true')
+ expect(branchTwo.getAttribute('data-is-open')).toBe('true')
- const levelThree = screen.getByTestId('/docs/agent/autoauth/methods')
- expect(levelThree.className).toMatch(/dir/)
- expect(levelThree.className).toMatch(/open active/)
+ const branchThree = screen.getByText('Methods').closest('button')
+ expect(branchThree.getAttribute('data-is-active')).toBe('true')
+ expect(branchThree.getAttribute('data-is-open')).toBe('true')
- const levelFour = screen.getByTestId('/docs/agent/autoauth/methods/aws')
- expect(levelFour.className).toMatch(/active/)
+ const activeLeaf = screen.getByText('AWS').closest('a')
+ expect(activeLeaf.getAttribute('data-is-active')).toBe('true')
// Let's also make sure that other pages are not also displaying as active
- // First we check an identically named page at a different level
- const dupe1 = screen.getByTestId('/docs/agent/autoauth/aws')
- expect(dupe1.className).not.toMatch(/active/)
+ // First we check an similarly named page at a different level
+ const inactiveLeafOne = screen.getByText('AWS Agent').closest('a')
+ expect(inactiveLeafOne.getAttribute('data-is-active')).toBe('false')
// Next we check a page at the same level but with a different name
- const dupe2 = screen.getByTestId('/docs/agent/autoauth/methods/gcp')
- expect(dupe2.className).not.toMatch(/active/)
+ const inactiveLeafTwo = screen.getByText('GCP').closest('a')
+ expect(inactiveLeafTwo.getAttribute('data-is-active')).toBe('false')
// Finally we check the overview page at the same level
- const dupe3 = screen.getByTestId('/docs/agent/autoauth/methods/index')
- expect(dupe3.className).not.toMatch(/active/)
+ const inactiveLeafThree = screen
+ .getAllByText('Overview')
+ .map((node) => node.closest('a'))
+ .filter((linkElem) => {
+ return linkElem.getAttribute('href') === '/docs/agent/autoauth/methods'
+ })[0]
+ expect(inactiveLeafThree.getAttribute('data-is-active')).toBe('false')
})
- it.todo('should render accurately when the current page is an "overview"')
-
- it('should expand/collapse directory-level menu items when clicked', () => {
- render()
-
- const levelTwoLink = screen.getByTestId('/docs/agent/autoauth - link')
- fireEvent.click(levelTwoLink)
- const levelTwo = screen.getByTestId('/docs/agent/autoauth')
- expect(levelTwo.className).not.toMatch(/open/)
- fireEvent.click(levelTwoLink)
- expect(levelTwo.className).toMatch(/open/)
+ it('renders accurately when the current page is an "overview"', () => {
+ const currentPath = 'agent/autoauth/methods'
+ const expectedHref = `/docs/${currentPath}`
+ render()
+ // Check the "overview" index node we've set as active using currentPath
+ const activeIndexLeaf = screen
+ .getAllByText('Overview')
+ .map((node) => node.closest('a'))
+ .filter((linkElem) => {
+ return linkElem.getAttribute('href') === expectedHref
+ })[0]
+ expect(activeIndexLeaf.getAttribute('data-is-active')).toBe('true')
})
- it('should show/hide the menu when the "menu" button is clicked on mobile', async () => {
+ it('expands and collapses nav branch items when clicked', () => {
render()
-
- const mobileMenu = screen.getByTestId('mobile-menu')
- const sidebarNav = screen.getByTestId('root')
-
- expect(sidebarNav).not.toHaveClass('open')
- fireEvent.click(mobileMenu)
- expect(sidebarNav).toHaveClass('open')
- fireEvent.click(mobileMenu)
- expect(sidebarNav).not.toHaveClass('open')
+ // Ensure the element exists, and is currently open
+ const branchTwo = screen.getByText('Auto-Auth').closest('button')
+ expect(branchTwo.getAttribute('data-is-active')).toBe('true')
+ expect(branchTwo.getAttribute('data-is-open')).toBe('true')
+ // Click the item, then ensure it's closed, but still active
+ fireEvent.click(branchTwo)
+ expect(branchTwo.getAttribute('data-is-active')).toBe('true')
+ expect(branchTwo.getAttribute('data-is-open')).toBe('false')
+ // Click it again, and it should be open again, still active
+ fireEvent.click(branchTwo)
+ expect(branchTwo.getAttribute('data-is-active')).toBe('true')
+ expect(branchTwo.getAttribute('data-is-open')).toBe('true')
})
- it('should error when a category is used with no content', () => {
- expectThrow(() => {
- render()
- }, 'The item "test" within "data/docs-navigation.js" has a category but no content, indicating that there is a folder that contains only an "index.mdx" file, which is not allowed. To fix this, move and rename "pages/docs/test/index.mdx" to "pages/docs/test.mdx", then change the value from "{ category: \'test\' }" to just "test"')
- })
-
- it('should error when a page is not found within a category', () => {
- expectThrow(() => {
- render(
-
- )
- }, 'The page "foo" was not found within the category "agent/test". Please double-check to ensure that "docs/agent/test/foo.mdx" exists. If this page was never intended to exist, remove the key "foo" from the category "agent/test" in "data/docs-navigation.js"')
- })
-
- it('should error when a direct link does not use both "title" and "href"', () => {
- expectThrow(() => {
- render()
- }, 'Malformed direct sidebar link:\n\n {"title":"foo"}\n\nDirect links must have a "href" and a "title" property.')
-
- expectThrow(() => {
- render()
- }, 'Malformed direct sidebar link:\n\n {"href":"bar"}\n\nDirect links must have a "href" and a "title" property.')
- })
-
- it('should error when a category contains no index file and no name', () => {
- expectThrow(() => {
- render(
-
- )
- }, 'An index page or "name" property is required for all categories.\nIf you would like an index page for this category, please add an index file at the path "agent/no-index-test/index.mdx".\nIf you do not want an index page for this category, please add a "name" property to the category object to specify the category\'s human-readable title.\n\nItem:\n{\n "category": "no-index-test",\n "content": [\n "foo"\n ],\n "stack": [\n "agent",\n "no-index-test"\n ]\n}')
- })
+ it('shows and hides the mobile menu when the "menu" button is clicked', () => {
+ render()
- it('should error when a category uses a name property and specifies an overview page', () => {
- expectThrow(() => {
- render(
-
- )
- }, 'The category "agent/no-index-test" is using a "name" property to indicate that it has no index, but also has a manually added "overview" page. This can be fixed with the following steps:\n\n- Change the "overview.mdx" page to be "index.mdx"\n- Remove the "name" property from the "no-index-test" data, instead indicate the category\'s name using the frontmatter on the new "index.mdx" page')
+ // Get the sidebar nav list
+ // (it's the only role=list element with data-is-mobile-open defined)
+ const sidebarNavList = screen.getAllByRole('list')[0]
+ expect(sidebarNavList.nodeName).toBe('UL')
+ expect(sidebarNavList.getAttribute('data-is-mobile-open')).toBe('false')
+ // Get the menu button
+ const mobileMenuToggle = screen
+ .getByText('Documentation Menu')
+ .closest('button')
+ // Click the menu button, and check the sidebar opens
+ fireEvent.click(mobileMenuToggle)
+ expect(sidebarNavList.getAttribute('data-is-mobile-open')).toBe('true')
+ // Click the menu button again, and check the sidebar closes
+ fireEvent.click(mobileMenuToggle)
+ expect(sidebarNavList.getAttribute('data-is-mobile-open')).toBe('false')
})
})
diff --git a/packages/docs-sidenav/package-lock.json b/packages/docs-sidenav/package-lock.json
index 0679c0b20..c1ad4a81b 100644
--- a/packages/docs-sidenav/package-lock.json
+++ b/packages/docs-sidenav/package-lock.json
@@ -4,23 +4,10 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
- "@hashicorp/react-link-wrap": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/@hashicorp/react-link-wrap/-/react-link-wrap-0.0.3.tgz",
- "integrity": "sha512-BLH7SLMJZee8md+YU7seiOh0zpa7y8sEhEATYpK2ieFanXAQEgU58lNSC/V3ZdkSXrfyplftYuOj0E3APtE1pg==",
- "requires": {
- "is-absolute-url": "^3.0.3"
- }
- },
"fuzzysearch": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/fuzzysearch/-/fuzzysearch-1.0.3.tgz",
"integrity": "sha1-3/yA9tawQiPyImqnndGUIxCW0Ag="
- },
- "is-absolute-url": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz",
- "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q=="
}
}
}
diff --git a/packages/docs-sidenav/package.json b/packages/docs-sidenav/package.json
index 0ec99fd0b..06eaba39e 100644
--- a/packages/docs-sidenav/package.json
+++ b/packages/docs-sidenav/package.json
@@ -7,12 +7,12 @@
"Jeff Escalante"
],
"dependencies": {
- "@hashicorp/react-link-wrap": "^0.0.3",
+ "@hashicorp/react-link-wrap": "^2.0.2",
"fuzzysearch": "1.0.3"
},
"license": "MPL-2.0",
"peerDependencies": {
- "@hashicorp/nextjs-scripts": ">16.x",
+ "@hashicorp/nextjs-scripts": ">16.2",
"react": "^16.9.0"
},
"publishConfig": {
diff --git a/packages/docs-sidenav/props.js b/packages/docs-sidenav/props.js
index affb62107..d0c47729f 100644
--- a/packages/docs-sidenav/props.js
+++ b/packages/docs-sidenav/props.js
@@ -1,172 +1,31 @@
+const sampleNavData = require('./fixtures/nav-data.json')
+const sharedProps = require('../../props')
+
module.exports = {
- product: {
- type: 'string',
- description: 'Name of the current product for color theming.',
- testValue: 'nomad',
- options: [
- 'hashicorp', // default
- 'boundary',
- 'consul',
- 'nomad',
- 'packer',
- 'terraform',
- 'vault',
- 'vagrant',
- 'waypoint',
- ],
- },
- currentPage: {
+ product: sharedProps.product,
+ currentPath: {
type: 'string',
description:
- 'Path to the current page, used to select the currently active page.',
- testValue: '/docs/agent/autoauth/methods/aws',
+ 'Path to the current page, relative to the `baseRoute`. Used to highlight the current page.',
+ testValue: 'agent/autoauth/methods/aws',
},
- category: {
+ baseRoute: {
type: 'string',
- description: 'Top level navigation category, for example docs, api, etc.',
+ required: true,
+ description:
+ 'Top level navigation route, for example `docs`, `api-docs`, etc.',
testValue: 'docs',
},
disableFilter: {
type: 'boolean',
- description: 'If true, disable the sidebar filter input',
+ description: 'If `true`, disable the sidebar filter input.',
testValue: false,
},
- order: {
+ navData: {
type: 'object',
- description: 'user-defined navigation configuration object',
- testValue: [
- {
- category: 'agent',
- content: [
- {
- category: 'autoauth',
- content: [
- {
- category: 'methods',
- content: [
- { title: 'External Link', href: 'https://google.com' },
- 'alicloud',
- 'aws',
- 'azure',
- '----------',
- 'gcp',
- 'jwt',
- 'kubernetes',
- ],
- },
- {
- category: 'sinks',
- content: ['file'],
- },
- 'aws',
- ],
- },
- // { category: 'test' },
- {
- category: 'no-index-test',
- name: 'No Index Category',
- content: ['foo'],
- },
- {
- category: 'only-index-test',
- content: [],
- },
- // { category: 'only-index-no-content' }
- ],
- },
- ],
- },
- data: {
- type: 'array',
+ required: true,
description:
- 'array of frontmatter data objects from all pages that need to be displayed within the nav',
- properties: [
- {
- type: 'object',
- properties: {
- __resourcePath: { type: 'string' },
- page_title: { type: 'string' },
- sidebar_title: { type: 'string' },
- },
- },
- ],
- testValue: [
- {
- __resourcePath: 'docs/agent/autoauth/methods/gcp.mdx',
- page_title: 'Vault Agent Auto-Auth GCP Method',
- sidebar_title: 'GCP',
- },
- {
- __resourcePath: 'docs/agent/autoauth/methods/index.mdx',
- page_title: 'Vault Agent Auto-Auth Methods',
- sidebar_title: 'Methods',
- },
- {
- __resourcePath: 'docs/agent/autoauth/methods/aws.mdx',
- page_title: 'Vault Agent Auto-Auth AWS Method',
- sidebar_title: 'AWS',
- sidebar_current: 'docs-agent-autoauth-methods-aws',
- },
- {
- __resourcePath: 'docs/agent/autoauth/methods/kubernetes.mdx',
- page_title: 'Vault Agent Auto-Auth Kubernetes Method',
- sidebar_title: 'Kubernetes',
- },
- {
- __resourcePath: 'docs/agent/autoauth/methods/azure.mdx',
- page_title: 'Vault Agent Auto-Auth Azure Method',
- sidebar_title: 'Azure',
- },
- {
- __resourcePath: 'docs/agent/autoauth/methods/alicloud.mdx',
- page_title: 'Vault Agent Auto-Auth AliCloud Method',
- sidebar_title: 'AliCloud',
- },
- {
- __resourcePath: 'docs/agent/autoauth/methods/jwt.mdx',
- page_title: 'Vault Agent Auto-Auth JWT Method',
- sidebar_title: 'JWT',
- },
- {
- __resourcePath: 'docs/agent/autoauth/index.mdx',
- page_title: 'Vault Agent Auto-Auth',
- sidebar_title: 'Auto-Auth',
- },
- {
- __resourcePath: 'docs/agent/autoauth/sinks/file.mdx',
- page_title: 'Vault Agent Auto-Auth File Sink',
- sidebar_title: 'File',
- },
- {
- __resourcePath: 'docs/agent/autoauth/sinks/index.mdx',
- page_title: 'Vault Agent Auto-Auth Sinks',
- sidebar_title: 'Sinks',
- },
- {
- __resourcePath: 'docs/agent/index.mdx',
- page_title: 'Vault Agent',
- sidebar_title: 'Vault Agent',
- },
- {
- __resourcePath: 'docs/agent/test/index.mdx',
- page_title: 'Test Item',
- },
- {
- __resourcePath: 'docs/agent/no-index-test/foo.mdx',
- page_title: 'Foo Item',
- },
- {
- __resourcePath: 'docs/agent/autoauth/aws.mdx',
- page_title: 'AWS',
- },
- {
- __resourcePath: 'docs/agent/only-index-test/index.mdx',
- page_title: 'Only Index Test ENT',
- },
- {
- __resourcePath: 'docs/agent/only-index-no-content/index.mdx',
- page_title: 'Only Index No Content ENT',
- },
- ],
+ 'Tree of navigation data to render. See `docs-sidenav/types.js` for details.',
+ testValue: sampleNavData,
},
}
diff --git a/packages/docs-sidenav/style.css b/packages/docs-sidenav/style.css
deleted file mode 100644
index 0854ca2ec..000000000
--- a/packages/docs-sidenav/style.css
+++ /dev/null
@@ -1,276 +0,0 @@
-.g-docs-sidenav {
- --highlight-color: var(
- --brand
- ); /* color overridden per product by themeClass */
-
- & > .nav {
- width: 275px;
- }
-
- & .nav {
- padding-left: 5px;
- z-index: 900;
- }
-
- & .filter {
- color: var(--gray-4);
- border: 1px solid var(--gray-4);
- background: var(--white);
- border-radius: 2px;
- padding: 8px;
- width: 100%;
- margin-bottom: 10px;
- max-width: 90%;
-
- &[data-has-error='true'] {
- border-color: var(--danger);
- }
-
- &::placeholder {
- opacity: 0.8;
- }
- }
-
- & .mobile-close {
- position: absolute;
- top: 18px;
- right: 13px;
- font-size: 1.7em;
- padding: 0 14px;
- cursor: pointer;
- color: #333;
- transition: opacity 0.3s ease;
- display: none;
- z-index: 100;
-
- &:hover {
- opacity: 0.7;
- }
-
- @media (max-width: 939px) {
- display: block;
- }
- }
-
- & ul {
- list-style: none;
- margin: 0;
- padding: 0;
-
- & a {
- color: var(--DEPRECATED-gray-4);
- padding: 7px 0 7px 12px;
- display: block;
- transition: color 0.2s ease;
- cursor: pointer;
-
- &:hover {
- color: var(--DEPRECATED-gray-2);
- }
- }
-
- & hr {
- background: none;
- padding: 8px 0;
- margin: 0;
-
- &::after {
- content: '';
- border-bottom: 1px solid var(--DEPRECATED-gray-9);
- display: block;
- width: 90%;
-
- @media (max-width: 939px) {
- width: 100%;
- }
- }
- }
-
- & > li {
- position: relative;
-
- &.active,
- &.dir {
- &::after,
- &::before {
- content: '';
- display: block;
- position: absolute;
- border-radius: 50%;
- }
-
- &::before {
- background: var(--white);
- }
- }
-
- &.active {
- & > a,
- & > span > a {
- color: var(--highlight-color);
- position: relative;
- }
-
- &:not(.dir) {
- &::after {
- width: 4px;
- height: 4px;
- background: var(--highlight-color);
- left: -2px;
- top: 18px;
- border-radius: 50%;
- }
-
- &::before {
- width: 14px;
- height: 14px;
- left: -7px;
- top: 13px;
- border-radius: 50%;
- }
- }
- }
-
- &.dir {
- & .chevron {
- border-radius: 50%;
- background: var(--white);
- position: absolute;
- top: 13px;
- left: -6px;
- width: 12px;
- height: 16px;
- text-align: center;
- padding-top: 4px;
- padding-left: 4px;
- transition: transform 0.15s ease;
- }
-
- &.active > span > a > .chevron path {
- fill: var(--highlight-color);
- }
-
- &.open > span > a > .chevron {
- transform: rotate(90deg);
- }
- }
-
- &.external {
- & > a {
- display: inline-block;
- position: relative;
-
- &::after {
- content: '';
- display: block;
- width: 12px;
- height: 12px;
- background: url('./img/external.svg');
- position: absolute;
- right: -20px;
- top: 15px;
- opacity: 0.25;
- transition: opacity 0.25s ease;
- }
-
- &:hover::after {
- opacity: 0.5;
- }
- }
- }
-
- & > ul > li,
- & > ul > hr {
- display: none;
- }
-
- &.open > ul > li,
- &.open > ul > hr {
- display: block;
- }
-
- & > ul {
- & > li {
- margin-left: 21.5px;
- border-left: 1px solid var(--DEPRECATED-gray-9);
- }
-
- & > hr {
- border-left: 1px solid var(--DEPRECATED-gray-9);
- margin-left: 21.5px;
- }
- }
- }
- }
-
- &.open {
- & > ul {
- @media (max-width: 939px) {
- box-shadow: 2px 2px 20px rgba(37, 38, 45, 0.2);
- transform: translateX(100%);
- padding-left: 25px;
- }
- }
-
- & .toggle {
- transition-delay: 0s;
- z-index: 102;
- }
- }
-
- & > ul {
- @media (max-width: 939px) {
- background: var(--white);
- bottom: 0;
- right: 100%;
- min-width: 375px;
- padding: 25px 32px 120px;
- position: fixed;
- overflow: auto;
- transition: 0.3s ease-in-out;
- transition-property: box-shadow, transform;
- top: 0;
- max-width: 100%;
- z-index: 101;
- }
-
- @media (max-width: 375px) {
- min-width: 100%;
- }
- }
-
- & .toggle {
- align-items: center;
- background: var(--white);
- bottom: 0;
- border-top: 1px solid var(--gray-6);
- border-bottom: 1px solid var(--gray-6);
- cursor: pointer;
- display: none;
- justify-content: center;
- left: 0;
- padding: 12px;
- transition-delay: 0.3s; /* waits for menu to close before adjusting z-index */
- width: 100%;
- z-index: 74; /* less than product-subnav */
-
- @media (max-width: 939px) {
- display: flex;
- }
-
- & span {
- align-items: center;
- display: flex;
- justify-content: center;
- }
-
- & svg {
- margin-right: 12px;
- }
- }
-
- & code {
- font-size: 1em;
- line-height: unset;
- }
-}
diff --git a/packages/docs-sidenav/style.module.css b/packages/docs-sidenav/style.module.css
new file mode 100644
index 000000000..737c58879
--- /dev/null
+++ b/packages/docs-sidenav/style.module.css
@@ -0,0 +1,241 @@
+@custom-media --mobile-viewports (max-width: 939px);
+
+.root {
+ position: relative;
+
+ @media (--mobile-viewports) {
+ z-index: 901; /* higher than g-subnav */
+ }
+}
+
+.rootList {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ width: 275px;
+ padding-left: 5px;
+
+ @media (--mobile-viewports) {
+ background: var(--white);
+ bottom: 0;
+ right: 100%;
+ width: min(100%, 375px);
+ padding: 25px 32px 120px;
+ position: fixed;
+ overflow: auto;
+ transition: 0.3s ease-in-out;
+ transition-property: box-shadow, transform;
+ top: 0;
+
+ &[data-is-mobile-open='true'] {
+ box-shadow: 2px 2px 20px rgba(37, 38, 45, 0.2);
+ transform: translateX(100%);
+ padding-left: 25px;
+ }
+
+ &[data-is-mobile-hidden='true'] {
+ visibility: hidden;
+ }
+ }
+}
+
+.mobileClose {
+ display: none;
+
+ @media (--mobile-viewports) {
+ background: none;
+ border: none;
+ color: #333;
+ cursor: pointer;
+ display: block;
+ font-size: 1.7em;
+ line-height: inherit;
+ padding: 0 14px;
+ position: absolute;
+ right: 13px;
+ top: 18px;
+ transition: opacity 0.3s ease;
+ z-index: 100;
+
+ &:hover {
+ opacity: 0.7;
+ }
+ }
+}
+
+.mobileMenuToggle {
+ align-items: center;
+ background: var(--white);
+ bottom: 0;
+ border: none;
+ border-top: 1px solid var(--gray-6);
+ border-bottom: 1px solid var(--gray-6);
+ cursor: pointer;
+ display: none;
+ font-size: inherit;
+ justify-content: center;
+ line-height: inherit;
+ left: 0;
+ padding: 12px;
+ transition-delay: 0.3s; /* waits for menu to close before adjusting z-index */
+ width: 100%;
+ z-index: 74; /* less than product-subnav */
+
+ @media (--mobile-viewports) {
+ display: flex;
+ }
+
+ & span {
+ align-items: center;
+ display: flex;
+ justify-content: center;
+ }
+
+ & svg {
+ display: block;
+ margin-right: 12px;
+ }
+}
+
+.filterInput {
+ color: var(--gray-2);
+ border: 1px solid var(--gray-6);
+ background: var(--white);
+ border-radius: 2px;
+ padding: 8px;
+ width: 100%;
+ margin-bottom: 10px;
+ max-width: 90%;
+ font-family: inherit;
+ font-size: inherit;
+
+ &[data-has-error='true'] {
+ border-color: var(--danger);
+ }
+}
+
+.navItem {
+ color: var(--DEPRECATED-gray-4);
+ cursor: pointer;
+ display: flex;
+ padding: 7px 0;
+ transition: color 0.2s ease;
+ align-items: center;
+ background: none;
+ border: none;
+ font-size: inherit;
+ font-family: inherit;
+ line-height: inherit;
+
+ &:hover {
+ color: var(--DEPRECATED-gray-2);
+ }
+
+ &[data-is-active='true'] {
+ color: var(--brand);
+
+ &:hover {
+ color: var(--brand);
+ }
+ }
+
+ & code {
+ margin-left: 2px;
+ font-size: 0.875rem;
+ }
+}
+
+.navItemIcon {
+ position: relative;
+ left: -8.5px;
+ top: 1px;
+ width: 16px;
+ height: 16px;
+ border-radius: 8px;
+ background: white;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ & svg {
+ display: block;
+ & [stroke] {
+ stroke: var(--gray-4);
+ }
+ & [fill] {
+ fill: var(--gray-4);
+ }
+ }
+
+ &[data-is-active='true'] {
+ & svg {
+ & [stroke] {
+ stroke: var(--brand);
+ }
+ & [fill] {
+ fill: var(--brand);
+ }
+ }
+ }
+}
+
+.navLeafIcon {
+ composes: navItemIcon;
+ opacity: 0;
+
+ &[data-is-active='true'] {
+ opacity: 1;
+ }
+}
+
+.navBranchIcon {
+ composes: navItemIcon;
+ transition: transform 0.15s ease;
+
+ &[data-is-open='true'] {
+ transform: rotate(90deg);
+ }
+}
+
+.navBranchSubnav {
+ display: none;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+
+ &[data-is-open='true'] {
+ display: block;
+ }
+
+ & > li {
+ margin-left: 21.5px;
+ border-left: 1px solid var(--DEPRECATED-gray-9);
+ }
+
+ & > hr {
+ border-left: 1px solid var(--DEPRECATED-gray-9);
+ margin-left: 21.5px;
+ }
+}
+
+.externalLinkIcon {
+ display: block;
+ margin-left: 8px;
+}
+
+.divider {
+ background: none;
+ padding: 8px 0;
+ margin: 0;
+
+ &::after {
+ content: '';
+ border-bottom: 1px solid var(--DEPRECATED-gray-9);
+ display: block;
+ width: 90%;
+
+ @media (--mobile-viewports) {
+ width: 100%;
+ }
+ }
+}
diff --git a/packages/docs-sidenav/types.ts b/packages/docs-sidenav/types.ts
new file mode 100644
index 000000000..57cdad876
--- /dev/null
+++ b/packages/docs-sidenav/types.ts
@@ -0,0 +1,45 @@
+// NavData is an array of NavNodes
+export type NavData = NavNode[]
+
+// A NavNode can be any of these types
+export type NavNode = NavLeaf | NavDirectLink | NavDivider | NavBranch
+
+// A NavLeaf represents a page with content.
+//
+// The "path" refers to the URL route from the content subpath.
+// For all current docs sites, this "path" also
+// corresponds to the content location in the filesystem.
+//
+// Note that "path" can refer to either "named" or "index" files.
+// For example, we will automatically resolve the path
+// "commands" to either "commands.mdx" or "commands/index.mdx".
+// If both exist, we will throw an error to alert authors
+// to the ambiguity.
+interface NavLeaf {
+ title: string
+ path: string
+}
+
+// A NavDirectLink allows linking outside the content subpath.
+//
+// This includes links on the same domain,
+// for example, where the content subpath is `/docs`,
+// one can create a direct link with href `/use-cases`.
+//
+// This also allows for linking to external URLs,
+// for example, one could link to `https://hashiconf.com/`.
+interface NavDirectLink {
+ title: string
+ href: string
+}
+
+// A NavDivider represents a divider line
+interface NavDivider {
+ divider: true
+}
+
+// A NavBranch represents nested navigation data.
+interface NavBranch {
+ title: string
+ routes: NavNode[]
+}
diff --git a/packages/docs-sidenav/utils/filter-content.js b/packages/docs-sidenav/utils/filter-content.js
new file mode 100644
index 000000000..9d5d0d8b4
--- /dev/null
+++ b/packages/docs-sidenav/utils/filter-content.js
@@ -0,0 +1,25 @@
+import fuzzysearch from 'fuzzysearch'
+
+function filterContent(content, searchValue) {
+ // if there's no search searchValue we short-circuit and return everything
+ if (!searchValue) return content
+ // Otherwise we reduce the content array to only matching content
+ return content.reduce((acc, item) => {
+ // if this is a divider node, don't show it in filtered results
+ if (item.divider) return acc
+ // all other nodes have a title, use it to check if the item is a direct match
+ const isTitleMatch = fuzzysearch(searchValue, item.title.toLowerCase())
+ // For nodes with no children, return early, only add the item if the title matches
+ if (!item.routes) return isTitleMatch ? acc.concat(item) : acc
+ // for branch nodes with matching children, return a clone of the
+ // node with filtered content children
+ const filteredRoutes = filterContent(item.routes, searchValue)
+ const filteredItem = isTitleMatch
+ ? { ...item, __isFiltered: true }
+ : { ...item, routes: filteredRoutes, __isFiltered: true }
+ const isCategoryMatch = isTitleMatch || filteredRoutes.length > 0
+ return isCategoryMatch ? acc.concat(filteredItem) : acc
+ }, [])
+}
+
+export default filterContent
diff --git a/packages/docs-sidenav/utils/flag-active-nodes.js b/packages/docs-sidenav/utils/flag-active-nodes.js
new file mode 100644
index 000000000..9e7b720d0
--- /dev/null
+++ b/packages/docs-sidenav/utils/flag-active-nodes.js
@@ -0,0 +1,35 @@
+function addIsActiveToNodes(navNodes, currentPath, pathname) {
+ return navNodes
+ .slice()
+ .map((node) => addIsActiveToNode(node, currentPath, pathname))
+}
+
+function addIsActiveToNode(navNode, currentPath, pathname) {
+ // If it's a node with child routes, return true
+ // if any of the child routes are active
+ if (navNode.routes) {
+ const routesWithActive = addIsActiveToNodes(
+ navNode.routes,
+ currentPath,
+ pathname
+ )
+ const isActive = routesWithActive.filter((r) => r.__isActive).length > 0
+ return { ...navNode, routes: routesWithActive, __isActive: isActive }
+ }
+ // If it's a node with a path value,
+ // return true if the path is a match
+ if (navNode.path) {
+ const isActive = navNode.path === currentPath
+ return { ...navNode, __isActive: isActive }
+ }
+ // If it's a direct link,
+ // return true if the path matches the router.pathname
+ if (navNode.href) {
+ const isActive = navNode.href === pathname
+ return { ...navNode, __isActive: isActive }
+ }
+ // Otherwise, it's a divider, so return unmodified
+ return navNode
+}
+
+export default addIsActiveToNodes
diff --git a/packages/docs-sidenav/utils/use-event-listener.js b/packages/docs-sidenav/utils/use-event-listener.js
new file mode 100644
index 000000000..20f0c15ad
--- /dev/null
+++ b/packages/docs-sidenav/utils/use-event-listener.js
@@ -0,0 +1,26 @@
+import { useRef, useEffect } from 'react'
+
+const useEventListener = (
+ eventName,
+ handler,
+ element = global,
+ options = {}
+) => {
+ const savedHandler = useRef()
+ const { capture, passive, once } = options
+
+ useEffect(() => {
+ savedHandler.current = handler
+ }, [handler])
+
+ useEffect(() => {
+ const isSupported = element && element.addEventListener
+ if (!isSupported) return
+ const eventListener = (event) => savedHandler.current(event)
+ const opts = { capture, passive, once }
+ element.addEventListener(eventName, eventListener, opts)
+ return () => element.removeEventListener(eventName, eventListener, opts)
+ }, [eventName, element, capture, passive, once])
+}
+
+export default useEventListener
diff --git a/packages/docs-sidenav/utils/validate-file-paths/index.js b/packages/docs-sidenav/utils/validate-file-paths/index.js
new file mode 100644
index 000000000..05253290f
--- /dev/null
+++ b/packages/docs-sidenav/utils/validate-file-paths/index.js
@@ -0,0 +1,57 @@
+const fs = require('fs')
+const path = require('path')
+
+async function validateFilePaths(navNodes, localDir) {
+ // Clone the nodes, and validate each one
+ return await Promise.all(
+ navNodes.slice(0).map(async (navNode) => {
+ return await validateNode(navNode, localDir)
+ })
+ )
+}
+
+async function validateNode(navNode, localDir) {
+ // Ignore remote leaf nodes, these already
+ // have their content file explicitly defined
+ // (note: remote leaf nodes are currently only used
+ // for Packer plugin documentation)
+ if (navNode.remoteFile) return navNode
+ // Handle local leaf nodes
+ if (navNode.path) {
+ const indexFilePath = path.join(navNode.path, 'index.mdx')
+ const namedFilePath = `${navNode.path}.mdx`
+ const hasIndexFile = fs.existsSync(
+ path.join(process.cwd(), localDir, indexFilePath)
+ )
+ const hasNamedFile = fs.existsSync(
+ path.join(process.cwd(), localDir, namedFilePath)
+ )
+ if (!hasIndexFile && !hasNamedFile) {
+ throw new Error(
+ `Could not find file to match path "${navNode.path}". Neither "${namedFilePath}" or "${indexFilePath}" could be found.`
+ )
+ }
+ if (hasIndexFile && hasNamedFile) {
+ throw new Error(
+ `Ambiguous path "${navNode.path}". Both "${namedFilePath}" and "${indexFilePath}" exist. Please delete one of these files.`
+ )
+ }
+ const filePath = path.join(
+ localDir,
+ hasIndexFile ? indexFilePath : namedFilePath
+ )
+ return { ...navNode, filePath }
+ }
+ // Handle local branch nodes
+ if (navNode.routes) {
+ const routesWithFilePaths = await validateFilePaths(
+ navNode.routes,
+ localDir
+ )
+ return { ...navNode, routes: routesWithFilePaths }
+ }
+ // Return all other node types unmodified
+ return navNode
+}
+
+module.exports = validateFilePaths
diff --git a/packages/docs-sidenav/utils/validate-file-paths/index.test.js b/packages/docs-sidenav/utils/validate-file-paths/index.test.js
new file mode 100644
index 000000000..bee8fa0b5
--- /dev/null
+++ b/packages/docs-sidenav/utils/validate-file-paths/index.test.js
@@ -0,0 +1,61 @@
+import path from 'path'
+import validateFilePaths from './'
+
+// We have a content folder fixture set up so that
+// we can properly test this function
+const CONTENT_DIR = 'packages/docs-sidenav/fixtures/content'
+
+describe(' - validate-file-paths', () => {
+ it('resolves the path for a named .mdx file', async () => {
+ const navData = [
+ {
+ title: 'What is Vault?',
+ path: 'what-is-vault',
+ },
+ ]
+ const withFilePaths = await validateFilePaths(navData, CONTENT_DIR)
+ const resolvedPath = withFilePaths[0].filePath
+ expect(resolvedPath).toBe(path.join(CONTENT_DIR, 'what-is-vault.mdx'))
+ })
+
+ it('resolves the path for an index.mdx file', async () => {
+ const navData = [
+ {
+ title: 'Vault Agent',
+ routes: [
+ {
+ title: 'Overview',
+ path: 'agent',
+ },
+ ],
+ },
+ ]
+ const withFilePaths = await validateFilePaths(navData, CONTENT_DIR)
+ const resolvedPath = withFilePaths[0].routes[0].filePath
+ expect(resolvedPath).toBe(path.join(CONTENT_DIR, 'agent', 'index.mdx'))
+ })
+
+ it('throws an error if there is a NavLeaf with a missing file', async () => {
+ const navData = [
+ {
+ title: 'Missing File Example',
+ path: 'this-file-should-not-exist',
+ },
+ ]
+ await expect(validateFilePaths(navData, CONTENT_DIR)).rejects.toThrow(
+ 'Could not find file to match path "this-file-should-not-exist". Neither "this-file-should-not-exist.mdx" or "this-file-should-not-exist/index.mdx" could be found.'
+ )
+ })
+
+ it('throws an error if there is a NavLeaf with an ambiguous file', async () => {
+ const navData = [
+ {
+ title: 'Ambiguous File Example',
+ path: 'ambiguous',
+ },
+ ]
+ await expect(validateFilePaths(navData, CONTENT_DIR)).rejects.toThrow(
+ `Ambiguous path "ambiguous". Both "ambiguous.mdx" and "ambiguous/index.mdx" exist. Please delete one of these files.`
+ )
+ })
+})
diff --git a/packages/docs-sidenav/utils/validate-route-structure/index.js b/packages/docs-sidenav/utils/validate-route-structure/index.js
new file mode 100644
index 000000000..c19298069
--- /dev/null
+++ b/packages/docs-sidenav/utils/validate-route-structure/index.js
@@ -0,0 +1,154 @@
+function validateRouteStructure(navData) {
+ return validateBranchRoutes(navData)[1]
+}
+
+function validateBranchRoutes(navNodes, depth = 0) {
+ // In order to be a valid branch, there needs to be at least one navNode.
+ if (navNodes.length === 0) {
+ throw new Error(
+ `Found empty array of navNodes at depth ${depth}. There must be more than one route.`
+ )
+ }
+ // Augment each navNode with its path __stack
+ const navNodesWithStacks = navNodes.map((navNode) => {
+ // Handle leaf nodes - split their paths into a __stack
+ if (typeof navNode.path !== 'undefined') {
+ if (navNode.path == '') {
+ throw new Error(
+ `Empty path value on NavLeaf. Path values must be non-empty strings. Node: ${JSON.stringify(
+ navNode
+ )}.`
+ )
+ }
+ if (!navNode.title) {
+ throw new Error(
+ `Missing nav-data title. Please add a non-empty title to the node with the path "${navNode.path}".`
+ )
+ }
+ return { ...navNode, __stack: navNode.path.split('/') }
+ }
+ // Handle branch nodes - we recurse depth-first here
+ if (navNode.routes) {
+ const nodeWithStacks = handleBranchNode(navNode, depth)
+ if (!navNode.title) {
+ const branchPath = nodeWithStacks.__stack.join('/')
+ throw new Error(
+ `Missing nav-data title on NavBranch. Please add a title to the node with the inferred path "${branchPath}".`
+ )
+ }
+ return nodeWithStacks
+ }
+ // Handle direct link nodes, identifiable
+ // by the presence of an href, to ensure they have a title
+ if (typeof navNode.href !== 'undefined') {
+ if (navNode.href == '') {
+ throw new Error(
+ `Empty href value on NavDirectLink. href values must be non-empty strings. Node: ${JSON.stringify(
+ navNode
+ )}.`
+ )
+ }
+ if (!navNode.title) {
+ throw new Error(
+ `Missing nav-data title on NavDirectLink. Please add a title to the node with href "${navNode.href}".`
+ )
+ }
+ // Otherwise, we have a valid direct link node, we return it
+ return navNode
+ }
+ // Ensure the only other node type is
+ // a divider node, if not, throw an error
+ if (!navNode.divider) {
+ throw new Error(
+ `Unrecognized nav-data node. Please ensure all nav-data nodes are either NavLeaf, NavBranch, NavDirectLink, or NavDivider types. Invalid node: ${JSON.stringify(
+ navNode
+ )}.`
+ )
+ }
+ // Other nodes, really just divider nodes,
+ // aren't relevant, so we don't touch them
+ return navNode
+ })
+ // Gather all the path stacks at this level
+ const routeStacks = navNodesWithStacks.reduce((acc, navNode) => {
+ // Ignore nodes that don't have a path stack
+ if (!navNode.__stack) return acc
+ // For other nodes, add their stacks
+ return acc.concat([navNode.__stack])
+ }, [])
+ // Ensure that there are no duplicate routes
+ // (for example, a nested route with a particular path,
+ // and a named page at the same level with the same path)
+ const routePaths = routeStacks.map((s) => s.join('/'))
+ const duplicateRoutes = routePaths.filter((value, index, self) => {
+ return self.indexOf(value) !== index
+ })
+ if (duplicateRoutes.length > 0) {
+ throw new Error(
+ `Duplicate routes found for "${duplicateRoutes[0]}". Please resolve duplicates.`
+ )
+ }
+ // Gather an array of all resolved paths at this level
+ const parentRoutes = routeStacks.map((stack) => {
+ // Index leaf nodes will have the same
+ // number of path parts as the current nesting depth.
+ const isIndexNode = stack.length === depth
+ if (isIndexNode) {
+ // The "dirPath" for index nodes is
+ // just the original path
+ return stack.join('/')
+ }
+ // Named leaf nodes, and nested routes,
+ // will have one more path part than the current nesting depth.
+ const isNamedNode = stack.length === depth + 1
+ if (isNamedNode) {
+ // The "dirPath" for named nodes is
+ // the original path with the last part dropped.
+ return stack.slice(0, stack.length - 1).join('/')
+ }
+ // If we have any other number of parts in the
+ // leaf node's path, then it is invalid.
+ throw new Error(
+ `Invalid path depth. At depth ${depth}, found path "${stack.join(
+ '/'
+ )}". Please move this path to the correct depth of ${stack.length - 1}.`
+ )
+ })
+ // We expect all routes at any level to share the same parent directory.
+ // In other words, we expect there to be exactly one unique "dirPath"
+ // shared across all the routes at this level.
+ const uniqueParents = parentRoutes.filter((value, index, self) => {
+ return self.indexOf(value) === index
+ })
+ // We throw an error if we find mismatched paths
+ // that don't share the same parent path.
+ if (uniqueParents.length > 1) {
+ throw new Error(
+ `Found mismatched paths at depth ${depth}: ${JSON.stringify(
+ uniqueParents
+ )}.`
+ )
+ }
+ // Note: some branches may not have any children with paths,
+ // for example branches with only direct links. So, path may be undefined.
+ const path = uniqueParents[0]
+ // Finally, we return
+ return [path, navNodesWithStacks]
+}
+
+function handleBranchNode(navNode, depth) {
+ // We recurse depth-first here, and we'll throw an error
+ // if any nested routes have structural issues
+ const [path, routesWithStacks] = validateBranchRoutes(
+ navNode.routes,
+ depth + 1
+ )
+ // Path will be undefined if the child routes are
+ // only non-path nodes (such as direct links).
+ // In this case, we set __stack to false so this route
+ // is left out of tree structure validation
+ const __stack = !path ? false : path.split('/')
+ return { ...navNode, __stack, routes: routesWithStacks }
+}
+
+module.exports = validateRouteStructure
diff --git a/packages/docs-sidenav/utils/validate-route-structure/index.test.js b/packages/docs-sidenav/utils/validate-route-structure/index.test.js
new file mode 100644
index 000000000..292c7c89d
--- /dev/null
+++ b/packages/docs-sidenav/utils/validate-route-structure/index.test.js
@@ -0,0 +1,196 @@
+import validateRouteStructure from './'
+
+describe(' - validate-file-paths', () => {
+ it("throws an error if a NavLeaf's path is an empty string", () => {
+ const navData = [
+ {
+ title: 'Whoops I Left The Path Empty',
+ path: '',
+ },
+ ]
+ const emptyPathError = `Empty path value on NavLeaf. Path values must be non-empty strings. Node: ${JSON.stringify(
+ navData[0]
+ )}.`
+ expect(() => validateRouteStructure(navData)).toThrow(emptyPathError)
+ })
+
+ it("throws an error if a NavLeaf's path is nested at the wrong depth", () => {
+ const navData = [
+ {
+ title: 'Directory',
+ routes: [
+ {
+ title: 'Overview',
+ path: 'directory',
+ },
+ {
+ title: 'Valid Depth',
+ path: 'directory/some-file',
+ },
+ {
+ title: 'Invalid Depth',
+ path: 'directory/some-nested-dir/some-file',
+ },
+ ],
+ },
+ ]
+ const depthError = `Invalid path depth. At depth 1, found path "directory/some-nested-dir/some-file". Please move this path to the correct depth of 2.`
+ expect(() => validateRouteStructure(navData)).toThrow(depthError)
+ })
+
+ it('throws an error if an empty array is passed', () => {
+ const emptyRoutesError = `Found empty array of navNodes at depth 0. There must be more than one route.`
+ expect(() => validateRouteStructure([])).toThrow(emptyRoutesError)
+ })
+
+ it('throws an error if a NavBranch has has an empty array of routes', () => {
+ const navData = [
+ {
+ title: 'Directory',
+ routes: [],
+ },
+ ]
+ const emptyRoutesError = `Found empty array of navNodes at depth 1. There must be more than one route.`
+ expect(() => validateRouteStructure(navData)).toThrow(emptyRoutesError)
+ })
+
+ it('throws an error if sibling routes have different parent routes', () => {
+ const navData = [
+ {
+ title: 'Directory',
+ routes: [
+ {
+ title: 'Overview',
+ path: 'directory',
+ },
+ {
+ title: 'Valid Parent',
+ path: 'directory/some-file',
+ },
+ {
+ title: 'Invalid Parent',
+ path: 'another-directory/another-file',
+ },
+ ],
+ },
+ ]
+ const siblingError = `Found mismatched paths at depth 1: ["directory","another-directory"].`
+ expect(() => validateRouteStructure(navData)).toThrow(siblingError)
+ })
+
+ it('throws an error if there are duplicate routes', () => {
+ const navData = [
+ {
+ title: 'Directory Dupe',
+ path: 'directory',
+ },
+ {
+ title: 'Directory',
+ routes: [
+ {
+ title: 'Overview',
+ path: 'directory',
+ },
+ {
+ title: 'Some File',
+ path: 'directory/some-file',
+ },
+ ],
+ },
+ ]
+ const duplicateError = `Duplicate routes found for "directory". Please resolve duplicates.`
+ expect(() => validateRouteStructure(navData)).toThrow(duplicateError)
+ })
+
+ it('throws an error if a NavLeaf has a missing title', () => {
+ const noTitleNode = { path: 'no-title' }
+ const noTitleError = `Missing nav-data title. Please add a non-empty title to the node with the path "no-title".`
+ expect(() => validateRouteStructure([noTitleNode])).toThrow(noTitleError)
+ const emptyTitleNode = { title: '', path: 'empty-title' }
+ const emptyTitleError = `Missing nav-data title. Please add a non-empty title to the node with the path "empty-title".`
+ expect(() => validateRouteStructure([emptyTitleNode])).toThrow(
+ emptyTitleError
+ )
+ })
+
+ it('throws an error if a NavBranch has a missing title', () => {
+ const navData = [
+ {
+ routes: [
+ {
+ title: 'Overview',
+ path: 'some-directory',
+ },
+ {
+ title: 'Some File',
+ path: 'some-directory/some-file',
+ },
+ ],
+ },
+ ]
+ const noTitleError = `Missing nav-data title on NavBranch. Please add a title to the node with the inferred path "some-directory".`
+ expect(() => validateRouteStructure(navData)).toThrow(noTitleError)
+ })
+
+ it('throws an error if a NavDirectLink has a missing title', () => {
+ const navData = [
+ {
+ href: '/some-direct-link',
+ },
+ ]
+ const noTitleError = `Missing nav-data title on NavDirectLink. Please add a title to the node with href "/some-direct-link".`
+ expect(() => validateRouteStructure(navData)).toThrow(noTitleError)
+ })
+
+ it('throws an error if a NavDirectLink has an empty href', () => {
+ const navData = [
+ {
+ title: 'Empty Href Link',
+ href: '',
+ },
+ ]
+ const emptyHrefError = `Empty href value on NavDirectLink. href values must be non-empty strings. Node: ${JSON.stringify(
+ navData[0]
+ )}.`
+ expect(() => validateRouteStructure(navData)).toThrow(emptyHrefError)
+ })
+
+ it('throws an error for unrecognized nodes', () => {
+ const navData = [
+ {
+ foo: 'bar',
+ },
+ ]
+ const emptyHrefError = `Unrecognized nav-data node. Please ensure all nav-data nodes are either NavLeaf, NavBranch, NavDirectLink, or NavDivider types. Invalid node: ${JSON.stringify(
+ navData[0]
+ )}.`
+ expect(() => validateRouteStructure(navData)).toThrow(emptyHrefError)
+ })
+
+ it('does not throw an error for a valid nav-data tree with a direct-links-only branch', () => {
+ const navData = [
+ {
+ title: 'Why Use Packer?',
+ path: 'why',
+ },
+ {
+ title: 'Direct Link',
+ href: 'https://www.hashicorp.com',
+ },
+ {
+ title: 'Direct Links Only Branch',
+ routes: [
+ {
+ title: 'Install',
+ href: '/intro/getting-started/install',
+ },
+ {
+ title: 'Build An Image',
+ href: '/intro/getting-started/build-image',
+ },
+ ],
+ },
+ ]
+ expect(() => validateRouteStructure(navData)).not.toThrow()
+ })
+})
diff --git a/packages/glossary-page/LICENSE b/packages/glossary-page/LICENSE
new file mode 100644
index 000000000..a612ad981
--- /dev/null
+++ b/packages/glossary-page/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
diff --git a/packages/glossary-page/docs.mdx b/packages/glossary-page/docs.mdx
index 9d7f5596a..ddfe6c39a 100644
--- a/packages/glossary-page/docs.mdx
+++ b/packages/glossary-page/docs.mdx
@@ -7,7 +7,7 @@ This is a specialized view built on top of `DocsPage` to render a glossary page
- {``}
+ {``}
-
+
### Props
diff --git a/packages/glossary-page/index.js b/packages/glossary-page/index.js
index 0f8e9e246..208153544 100644
--- a/packages/glossary-page/index.js
+++ b/packages/glossary-page/index.js
@@ -17,23 +17,20 @@ function GlossaryTableOfContents({ terms }) {
export default function GlossaryPage({
additionalComponents,
- mainBranch,
- order,
product,
showEditPage,
- staticProps: { content, terms, docsPageData },
+ staticProps: { mdxSource, terms, navData, githubFileUrl },
}) {
return (
<>
@@ -45,7 +42,7 @@ export default function GlossaryPage({
community.
- {hydrate(content, {
+ {hydrate(mdxSource, {
components: generateComponents(product.name, additionalComponents),
})}
>
diff --git a/packages/glossary-page/package-lock.json b/packages/glossary-page/package-lock.json
index fc340318a..44a2fb35f 100644
--- a/packages/glossary-page/package-lock.json
+++ b/packages/glossary-page/package-lock.json
@@ -4,6 +4,192 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+ "@algolia/cache-browser-local-storage": {
+ "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.6"
+ }
+ },
+ "@algolia/cache-common": {
+ "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.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.6"
+ }
+ },
+ "@algolia/client-account": {
+ "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.6",
+ "@algolia/client-search": "4.8.6",
+ "@algolia/transporter": "4.8.6"
+ }
+ },
+ "@algolia/client-analytics": {
+ "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.6",
+ "@algolia/client-search": "4.8.6",
+ "@algolia/requester-common": "4.8.6",
+ "@algolia/transporter": "4.8.6"
+ }
+ },
+ "@algolia/client-common": {
+ "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.6",
+ "@algolia/transporter": "4.8.6"
+ }
+ },
+ "@algolia/client-recommendation": {
+ "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.6",
+ "@algolia/requester-common": "4.8.6",
+ "@algolia/transporter": "4.8.6"
+ }
+ },
+ "@algolia/client-search": {
+ "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.6",
+ "@algolia/requester-common": "4.8.6",
+ "@algolia/transporter": "4.8.6"
+ }
+ },
+ "@algolia/logger-common": {
+ "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.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.6"
+ }
+ },
+ "@algolia/requester-browser-xhr": {
+ "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.6"
+ }
+ },
+ "@algolia/requester-common": {
+ "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.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.6"
+ }
+ },
+ "@algolia/transporter": {
+ "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.6",
+ "@algolia/logger-common": "4.8.6",
+ "@algolia/requester-common": "4.8.6"
+ }
+ },
+ "@babel/runtime": {
+ "version": "7.13.9",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.9.tgz",
+ "integrity": "sha512-aY2kU+xgJ3dJ1eU6FMB9EH8dIe8dmusF1xEku52joLvw6eAFN0AI+WxCLDnpev2LEejWBAy2sBvBOBAjI3zmvA==",
+ "requires": {
+ "regenerator-runtime": "^0.13.4"
+ }
+ },
+ "@hashicorp/react-content": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/@hashicorp/react-content/-/react-content-6.3.0.tgz",
+ "integrity": "sha512-B+QMlkMGryeNx3dGON4ExbzNvvll2ZXN3x+TkX80tUGClMI80MKjfSXiXIoVixlp22DMNG6wrnL42LC4WzZOxg=="
+ },
+ "@hashicorp/react-docs-page": {
+ "version": "10.9.4-alpha.57",
+ "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-page/-/react-docs-page-10.9.4-alpha.57.tgz",
+ "integrity": "sha512-gIUnCnVa5Zdied5LMmdZpQuvd4fZI8NijUqqWqF8zCC7kX1mYYeBcmeWjnUM2wgYo/7JBiXnHqwCkeAlfwdoeQ==",
+ "requires": {
+ "@hashicorp/react-content": "^6.3.0",
+ "@hashicorp/react-docs-sidenav": "6.1.1-alpha.60",
+ "@hashicorp/react-head": "^1.2.0",
+ "@hashicorp/react-search": "^4.1.0",
+ "fs-exists-sync": "0.1.0",
+ "gray-matter": "4.0.2",
+ "js-yaml": "3.14.0",
+ "line-reader": "0.4.0",
+ "moize": "^5.4.7",
+ "readdirp": "3.5.0"
+ }
+ },
+ "@hashicorp/react-docs-sidenav": {
+ "version": "6.1.1-alpha.60",
+ "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-sidenav/-/react-docs-sidenav-6.1.1-alpha.60.tgz",
+ "integrity": "sha512-mfR6Wl4R4k5KD7lJpl0l4pn4NecmoqV6HxQ+nvMW/d6nIkb6/xWxynmDkEeiyEVpphLKqJFh6boWuFYaar7PbA==",
+ "requires": {
+ "@hashicorp/react-link-wrap": "^2.0.2",
+ "fuzzysearch": "1.0.3"
+ }
+ },
+ "@hashicorp/react-head": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@hashicorp/react-head/-/react-head-1.2.0.tgz",
+ "integrity": "sha512-6BNmhsrzVwJFOAcT3WhSeDlCdtlD3d7vzhXOGfkpPYVnYRaIpLLC6seemAr/wqZhYB87W+KvFilz8vZcpDAZzQ=="
+ },
+ "@hashicorp/react-inline-svg": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@hashicorp/react-inline-svg/-/react-inline-svg-1.0.2.tgz",
+ "integrity": "sha512-AAFnBslSTgnEr++dTbMn3sybAqvn7myIj88ijGigF6u11eSRiV64zqEcyYLQKWTV6dF4AvYoxiYC6GSOgiM0Yw=="
+ },
+ "@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-search": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@hashicorp/react-search/-/react-search-4.1.0.tgz",
+ "integrity": "sha512-TZChez9q/4bn/flQXRo0h/9B0kDMvin759hd8+vRrt1M3Qhz2C1TKpfZRKrX6dFZI8w4obGm1EzUzR130gdFfQ==",
+ "requires": {
+ "@hashicorp/react-inline-svg": "^1.0.2",
+ "@hashicorp/remark-plugins": "^3.0.0",
+ "algoliasearch": "^4.8.4",
+ "dotenv": "^8.2.0",
+ "glob": "^7.1.6",
+ "gray-matter": "^4.0.2",
+ "react-instantsearch-dom": "^6.9.0",
+ "remark": "^12.0.1",
+ "search-insights": "^1.6.0",
+ "unist-util-visit": "^2.0.3"
+ }
+ },
"@hashicorp/remark-plugins": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@hashicorp/remark-plugins/-/remark-plugins-3.1.1.tgz",
@@ -31,11 +217,62 @@
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz",
"integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ=="
},
+ "algoliasearch": {
+ "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.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.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"
+ }
+ },
+ "argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "requires": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
"bail": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz",
"integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ=="
},
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
"ccount": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/ccount/-/ccount-1.1.0.tgz",
@@ -61,21 +298,79 @@
"resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz",
"integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg=="
},
+ "classnames": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
+ "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
+ },
"collapse-white-space": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz",
"integrity": "sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ=="
},
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+ },
+ "dotenv": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
+ "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
+ },
"emoji-regex": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.1.1.tgz",
"integrity": "sha1-xs0OwbBkLio8Z6ETfvxeeW2k+I4="
},
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
+ },
+ "events": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
+ "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ="
+ },
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ },
+ "fast-equals": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-1.6.3.tgz",
+ "integrity": "sha512-4WKW0AL5+WEqO0zWavAfYGY1qwLsBgE//DN4TTcVEN2UlINgkv9b3vm2iHicoenWKSX9mKWmGOsU/iI5IST7pQ=="
+ },
+ "fast-stringify": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/fast-stringify/-/fast-stringify-1.1.2.tgz",
+ "integrity": "sha512-SfslXjiH8km0WnRiuPfpUKwlZjW5I878qsOm+2x8x3TgqmElOOLh1rgJFb+PolNdNRK3r8urEefqx0wt7vx1dA=="
+ },
+ "fs-exists-sync": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz",
+ "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0="
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
+ },
+ "fuzzysearch": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/fuzzysearch/-/fuzzysearch-1.0.3.tgz",
+ "integrity": "sha1-3/yA9tawQiPyImqnndGUIxCW0Ag="
+ },
"github-slugger": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.3.0.tgz",
@@ -84,6 +379,39 @@
"emoji-regex": ">=6.0.0 <=6.1.1"
}
},
+ "glob": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "gray-matter": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.2.tgz",
+ "integrity": "sha512-7hB/+LxrOjq/dd8APlK0r24uL/67w7SkYnfwhNFwg/VDIGWGmduTDYf3WNstLW2fbbmRwrDGCVSJ2isuf2+4Hw==",
+ "requires": {
+ "js-yaml": "^3.11.0",
+ "kind-of": "^6.0.2",
+ "section-matter": "^1.0.0",
+ "strip-bom-string": "^1.0.0"
+ }
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -118,6 +446,11 @@
"resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz",
"integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw=="
},
+ "is-extendable": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+ "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik="
+ },
"is-hexadecimal": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
@@ -138,11 +471,43 @@
"resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.4.tgz",
"integrity": "sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA=="
},
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
+ "js-yaml": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
+ "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ }
+ },
+ "kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
+ },
+ "line-reader": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/line-reader/-/line-reader-0.4.0.tgz",
+ "integrity": "sha1-F+RIGNoKwzVnW6MAlU+U72cOZv0="
+ },
"longest-streak": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz",
"integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg=="
},
+ "loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "requires": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ }
+ },
"markdown-escapes": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz",
@@ -164,11 +529,42 @@
"unist-util-visit": "^2.0.0"
}
},
+ "micro-memoize": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/micro-memoize/-/micro-memoize-2.1.2.tgz",
+ "integrity": "sha512-COjNutiFgnDHXZEIM/jYuZPwq2h8zMUeScf6Sh6so98a+REqdlpaNS7Cb2ffGfK5I+xfgoA3Rx49NGuNJTJq3w=="
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "moize": {
+ "version": "5.4.7",
+ "resolved": "https://registry.npmjs.org/moize/-/moize-5.4.7.tgz",
+ "integrity": "sha512-7PZH8QFJ51cIVtDv7wfUREBd3gL59JB0v/ARA3RI9zkSRa9LyGjS1Bdldii2J1/NQXRQ/3OOVOSdnZrCcVaZlw==",
+ "requires": {
+ "fast-equals": "^1.6.0",
+ "fast-stringify": "^1.1.0",
+ "micro-memoize": "^2.1.1"
+ }
+ },
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "requires": {
+ "wrappy": "1"
+ }
+ },
"parse-entities": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz",
@@ -182,6 +578,73 @@
"is-hexadecimal": "^1.0.0"
}
},
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
+ },
+ "picomatch": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
+ "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg=="
+ },
+ "prop-types": {
+ "version": "15.7.2",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
+ "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
+ "requires": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.8.1"
+ }
+ },
+ "react-fast-compare": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
+ "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
+ },
+ "react-instantsearch-core": {
+ "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.4.3",
+ "prop-types": "^15.6.2",
+ "react-fast-compare": "^3.0.0"
+ }
+ },
+ "react-instantsearch-dom": {
+ "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.4.3",
+ "classnames": "^2.2.5",
+ "prop-types": "^15.6.2",
+ "react-fast-compare": "^3.0.0",
+ "react-instantsearch-core": "^6.10.3"
+ }
+ },
+ "react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ },
+ "readdirp": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
+ "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
+ "requires": {
+ "picomatch": "^2.2.1"
+ }
+ },
+ "regenerator-runtime": {
+ "version": "0.13.7",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
+ "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
+ },
"remark": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/remark/-/remark-12.0.1.tgz",
@@ -241,6 +704,25 @@
"resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
"integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc="
},
+ "search-insights": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-1.7.1.tgz",
+ "integrity": "sha512-CSuSKIJp+WcSwYrD9GgIt1e3xmI85uyAefC4/KYGgtvNEm6rt4kBGilhVRmTJXxRE2W1JknvP598Q7SMhm7qKA=="
+ },
+ "section-matter": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
+ "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
+ "requires": {
+ "extend-shallow": "^2.0.1",
+ "kind-of": "^6.0.0"
+ }
+ },
+ "sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
+ },
"state-toggle": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz",
@@ -256,6 +738,11 @@
"xtend": "^4.0.0"
}
},
+ "strip-bom-string": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
+ "integrity": "sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI="
+ },
"to-vfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/to-vfile/-/to-vfile-6.1.0.tgz",
@@ -381,6 +868,11 @@
"unist-util-stringify-position": "^2.0.0"
}
},
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+ },
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
diff --git a/packages/glossary-page/package.json b/packages/glossary-page/package.json
index 1af005677..0e94cf7d8 100644
--- a/packages/glossary-page/package.json
+++ b/packages/glossary-page/package.json
@@ -7,17 +7,14 @@
"Bryce Kalow"
],
"dependencies": {
+ "@hashicorp/react-docs-page": "10.9.4-alpha.57",
"@hashicorp/remark-plugins": "^3.1.1"
},
"license": "MPL-2.0",
"peerDependencies": {
"@hashicorp/nextjs-scripts": "13.0.0",
- "@hashicorp/react-docs-page": "^10.4.0",
"react": "^16.9.0"
},
- "devDependencies": {
- "@hashicorp/react-docs-page": "^10.9.3"
- },
"publishConfig": {
"access": "public"
}
diff --git a/packages/glossary-page/props.js b/packages/glossary-page/props.js
index 18e0712bf..f1dbeaf70 100644
--- a/packages/glossary-page/props.js
+++ b/packages/glossary-page/props.js
@@ -1,3 +1,5 @@
+const docsPageProps = require('../docs-page/props')
+
module.exports = {
product: {
type: 'object',
@@ -25,11 +27,6 @@ module.exports = {
},
},
},
- order: {
- type: 'object',
- description:
- 'Pass in the export of a `data/xxx-navigation.js` file, this is the user-defined navigation order and structure. Passed directly to the `order` prop to `@hashicorp/react-docs-sidenav` - see that component for details on object structure.',
- },
additionalComponents: {
type: 'object',
description:
@@ -41,15 +38,37 @@ module.exports = {
'if true, an "edit this page" link will appear on the bottom right',
default: true,
},
- mainBranch: {
- type: 'string',
- description:
- 'The default branch of the project being documented, typically either "master" or "main". Used for the `showEditPage` prop',
- default: 'main',
- },
staticProps: {
type: 'object',
description:
'Directly pass the return value of `server/generateStaticProps` in here.',
+ properties: {
+ terms: {
+ type: 'array',
+ description:
+ 'A list of glossary terms, passed to ``',
+ properties: [
+ {
+ type: 'object',
+ description: 'A glossary term item',
+ properties: {
+ slug: {
+ type: 'string',
+ description:
+ "The term's slug, used to construct an anchor link",
+ },
+ title: {
+ type: 'string',
+ description:
+ "The term's title, used to label an anchor link to the term",
+ },
+ },
+ },
+ ],
+ },
+ githubFileUrl: docsPageProps.staticProps.properties.githubFileUrl,
+ mdxSource: docsPageProps.staticProps.properties.mdxSource,
+ navData: docsPageProps.staticProps.properties.navData,
+ },
},
}
diff --git a/packages/glossary-page/server.js b/packages/glossary-page/server.js
index c1fc17f7f..698ccc5c1 100644
--- a/packages/glossary-page/server.js
+++ b/packages/glossary-page/server.js
@@ -2,33 +2,42 @@ import renderToString from 'next-mdx-remote/render-to-string'
import matter from 'gray-matter'
import fs from 'fs'
import path from 'path'
-import { fastReadFrontMatter } from '@hashicorp/react-docs-page/server'
+import { validateFilePaths } from '@hashicorp/react-docs-page/server'
import generateComponents from '@hashicorp/react-docs-page/components'
import markdownDefaults from '@hashicorp/nextjs-scripts/markdown'
import generateSlug from '@hashicorp/remark-plugins/generate_slug'
export default async function generateStaticProps({
+ navDataFile,
additionalComponents,
- productName,
+ product,
+ mainBranch = 'main',
}) {
const docsPath = path.join(process.cwd(), 'content', 'docs')
- const docsPageData = (await fastReadFrontMatter(docsPath)).map((p) => {
- p.__resourcePath = `docs/${p.__resourcePath}`
- return p
- })
+ // Read in the nav-data.json file
+ const navDataFilePath = path.join(process.cwd(), navDataFile)
+ const navDataRaw = JSON.parse(fs.readFileSync(navDataFilePath, 'utf8'))
+ const navData = await validateFilePaths(navDataRaw, docsPath)
+ // Construct the mdxSource from the provided terms
const { terms, mdxBlob } = await getGlossaryTerms()
+ const mdxSource = await renderToString(mdxBlob, {
+ mdxOptions: markdownDefaults({
+ resolveIncludes: path.join(process.cwd(), 'content/partials'),
+ }),
+ components: generateComponents(product.name, additionalComponents),
+ })
+
+ // Construct the githubFileUrl, used for "Edit this page" link
+ const githubFileUrl = `https://github.com/hashicorp/${product.slug}/blob/${mainBranch}/website/content/glossary`
+
return {
props: {
terms,
- content: await renderToString(mdxBlob, {
- mdxOptions: markdownDefaults({
- resolveIncludes: path.join(process.cwd(), 'content/partials'),
- }),
- components: generateComponents(productName, additionalComponents),
- }),
- docsPageData,
+ mdxSource,
+ navData,
+ githubFileUrl,
},
}
}
diff --git a/pages/global.css b/pages/global.css
index 9ee5aa1d7..73d718068 100644
--- a/pages/global.css
+++ b/pages/global.css
@@ -11,7 +11,6 @@
@import '../packages/button/styles/index.css';
@import '../packages/callouts/styles/index.css';
@import '../packages/checkbox-input/style.css';
-@import '../packages/docs-sidenav/style.css';
@import '../packages/accordion/style.css';
@import '../packages/subnav/style.css';
@import '../packages/call-to-action/style.css';