From 813dfae35ee21756f3d956876f49c39c5222463d Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 25 Feb 2021 10:09:27 -0500 Subject: [PATCH 01/62] WIP initial refactor of DocsSidenav --- packages/docs-page/docs.mdx | 16 +- packages/docs-page/index.js | 23 +- packages/docs-page/package-lock.json | 261 ++++---- packages/docs-page/package.json | 6 +- packages/docs-page/props.js | 11 +- packages/docs-page/server.js | 14 +- packages/docs-sidenav/README.md | 2 +- packages/docs-sidenav/chevron-icon.js | 18 - packages/docs-sidenav/docs.mdx | 9 +- .../fixtures/navdata-example.json | 60 ++ packages/docs-sidenav/icons/bullet.svg | 3 + packages/docs-sidenav/icons/chevron.svg | 3 + .../external.svg => icons/external-link.svg} | 0 .../{menu-icon.js => icons/menu.svg} | 14 +- packages/docs-sidenav/index.js | 590 +++++------------- packages/docs-sidenav/index.test.js | 159 +---- packages/docs-sidenav/package-lock.json | 15 +- packages/docs-sidenav/package.json | 3 +- packages/docs-sidenav/props.js | 176 +----- packages/docs-sidenav/style.module.css | 237 +++++++ packages/docs-sidenav/types.ts | 45 ++ .../utils/_wip-migrate-to-nav-json.js | 186 ++++++ .../utils/validate-file-paths/index.js | 58 ++ .../utils/validate-route-structure/index.js | 110 ++++ pages/global.css | 1 - 25 files changed, 1085 insertions(+), 935 deletions(-) delete mode 100644 packages/docs-sidenav/chevron-icon.js create mode 100644 packages/docs-sidenav/fixtures/navdata-example.json create mode 100644 packages/docs-sidenav/icons/bullet.svg create mode 100644 packages/docs-sidenav/icons/chevron.svg rename packages/docs-sidenav/{img/external.svg => icons/external-link.svg} (100%) rename packages/docs-sidenav/{menu-icon.js => icons/menu.svg} (94%) create mode 100644 packages/docs-sidenav/style.module.css create mode 100644 packages/docs-sidenav/types.ts create mode 100644 packages/docs-sidenav/utils/_wip-migrate-to-nav-json.js create mode 100644 packages/docs-sidenav/utils/validate-file-paths/index.js create mode 100644 packages/docs-sidenav/utils/validate-route-structure/index.js diff --git a/packages/docs-page/docs.mdx b/packages/docs-page/docs.mdx index 1ff5e3924..5ed0f23e5 100644 --- a/packages/docs-page/docs.mdx +++ b/packages/docs-page/docs.mdx @@ -39,27 +39,15 @@ We have a lot of docs sites, all of which render content in exactly the same way '

Example Page

This is a cool docs page!

', scope: {}, }, - data: [ - { - __resourcePath: 'docs/test.mdx', - page_title: 'Testing Page', - sidebar_title: 'Testing Page', - }, - { - __resourcePath: 'docs/test2.mdx', - page_title: 'Other Testing Page', - sidebar_title: 'Other Testing Page', - }, - ], frontMatter: { page_title: 'Test Page', description: 'test description' }, - pagePath: '/docs/test', filePath: 'test.mdx', + currentPath: componentProps.currentPath.testValue, }, }} >{` diff --git a/packages/docs-page/index.js b/packages/docs-page/index.js index 47d2b3081..986100c51 100644 --- a/packages/docs-page/index.js +++ b/packages/docs-page/index.js @@ -3,7 +3,6 @@ import Content from '@hashicorp/react-content' import DocsSidenav from '@hashicorp/react-docs-sidenav' import HashiHead from '@hashicorp/react-head' import Head from 'next/head' -import Link from 'next/link' import hydrate from 'next-mdx-remote/hydrate' import { SearchProvider } from '@hashicorp/react-search' import SearchBar from './search-bar' @@ -11,18 +10,17 @@ import generateComponents from './components' import temporary_injectJumpToSection from './temporary_jump-to-section' export function DocsPageWrapper({ - allPageData, canonicalUrl, children, description, filePath, mainBranch = 'main', - order, - pagePath, + currentPath, pageTitle, product: { name, slug }, showEditPage = true, subpath, + navData, }) { // TEMPORARY (https://app.asana.com/0/1100423001970639/1160656182754009) // activates the "jump to section" feature @@ -49,11 +47,9 @@ export function DocsPageWrapper({
@@ -90,11 +86,11 @@ export function DocsPageWrapper({ export default function DocsPage({ product, subpath, - order, + navData, mainBranch = 'main', showEditPage = true, additionalComponents, - staticProps: { mdxSource, data, frontMatter, pagePath, filePath }, + staticProps: { mdxSource, frontMatter, currentPath, filePath }, }) { // This component is written to work with next-mdx-remote -- here it hydrates the content const content = hydrate(mdxSource, { @@ -103,13 +99,12 @@ export default function DocsPage({ return ( - - - ) -} diff --git a/packages/docs-sidenav/docs.mdx b/packages/docs-sidenav/docs.mdx index aeb0f35fb..3edf74178 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/navdata-example.json b/packages/docs-sidenav/fixtures/navdata-example.json new file mode 100644 index 000000000..e41024357 --- /dev/null +++ b/packages/docs-sidenav/fixtures/navdata-example.json @@ -0,0 +1,60 @@ +[ + { + "title": "Vault Agent", + "routes": [ + { + "title": "Overview", + "path": "agent" + }, + { + "title": "Auto-Auth", + "routes": [ + { + "title": "Overview", + "path": "agent/autoauth" + }, + { + "title": "Methods", + "routes": [ + { + "title": "Overview", + "path": "agent/autoauth/methods" + }, + { + "title": "External Link", + "href": "https://google.com" + }, + { + "title": "AliCloud", + "path": "agent/autoauth/methods/alicloud" + }, + { "divider": true }, + { + "title": "GCP", + "path": "agent/autoauth/methods/gcp" + } + ] + } + ] + }, + { + "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..4cc6a96d6 100644 --- a/packages/docs-sidenav/index.js +++ b/packages/docs-sidenav/index.js @@ -1,478 +1,218 @@ -import React, { useState, useMemo } from 'react' -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 React, { useEffect, useState } from 'react' +import Link from 'next/link' +import InlineSvg from '@hashicorp/react-inline-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' import fuzzysearch from 'fuzzysearch' +import s from './style.module.css' export default function DocsSidenav({ - data, - order, - currentPage, - category, - Link, + currentPath, + rootPath, product, disableFilter = false, + navData, }) { const [open, setOpen] = useState(false) const [filterInput, setFilterInput] = useState('') - const { themeClass } = useProductMeta(product) - - // 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) - - // remove leading slash and base level "docs"/"api"/etc - const currentPath = currentPage - .split('/') - .slice(1 + category.split('/').length) + const [content, setContent] = useState(navData) + const [filteredContent, setFilteredContent] = useState(navData) + + // When currentPath changes, update content + // to ensure `__isActive` props on each content item + // are accurate and up-to-date + // (Note: we could also reset filter input here, + // if we don't want to filter input to persist + // across client-side navigation, with something like: + // setFilterInput("") + useEffect(() => { + if (!navData) return + setContent(addIsActiveToNodes(navData, currentPath)) + }, [currentPath, navData]) + + // When filter input changes, update content + // to filter out items that don't match + useEffect(() => { + setFilteredContent(filterContent(content, filterInput)) + }, [filterInput, content]) return ( -
-
setOpen(!open)} - data-testid="mobile-menu" - > +
+
setOpen(!open)}> - Documentation Menu + Documentation Menu
-
    -
    setOpen(!open)}> +
      +
      setOpen(!open)}> ×
      {!disableFilter && ( setFilterInput(e.target.value)} value={filterInput} /> )} - {renderNavTree({ - category, - content, - currentPath, - currentPage, - filterInput, - Link, - })} +
    ) } -// Filter nav items -function filterInputChange(setFilterInput, allContent, setContent, e) { - setFilterInput(e.target.value) - setContent(findContent(allContent, e.target.value.toLowerCase())) +function addIsActiveToNodes(navNodes, currentPath) { + return navNodes.slice().map((node) => addIsActiveToNode(node, currentPath)) } -function findContent(content, value) { - // if there's no search value we short-circuit and return everything - if (!value) return content +function addIsActiveToNode(navNode, currentPath) { + // 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) + 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 } + } + // Otherwise, return false + // (for dividers, external links, etc) + // TODO - do we need to worry about highlighting external links? + return navNode +} +function filterContent(content, searchValue) { + // if there's no search searchValue we short-circuit and return everything + if (!searchValue) 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 + // 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 }, []) } -// 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({ content, category, Link }) { 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 ( -
  • - -
  • - ) + 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 && ( -
      - {!item.name && (!filterInput || item.matchedFilter) && ( -
    • - {/* hide "overview" links if there's no overview (aka there is a name), or while searching */} - - - Overview - -
    • - )} - {renderNavTree({ - category, - content: item.content, - currentPath, - filterInput, - Link, - })} -
    - )} -
  • + ) } + // Otherwise, render a nav category + // (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 NavLeaf({ title, url, Link, isActive }) { + // if the item has a path, it's a leaf node so we render a link to the page 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) -} +function NavCategory({ title, routes, category, isActive, isFiltered, Link }) { + const [isOpen, setIsOpen] = useState(false) -// 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]) + // Ensure categories appear open if they're active + // or match the current filter + useEffect(() => setIsOpen(isActive || isFiltered), [isActive, isFiltered]) + + 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 Divider() { + return
    } -// Opens and closes a given nav category, the easy way -function toggleNav(e) { - e.preventDefault() - e.currentTarget.parentElement.parentElement.classList.toggle('open') +function DirectLink({ title, href }) { + return ( +
  • + + + + + +
  • + ) } diff --git a/packages/docs-sidenav/index.test.js b/packages/docs-sidenav/index.test.js index 27d196a42..e3d8930ea 100644 --- a/packages/docs-sidenav/index.test.js +++ b/packages/docs-sidenav/index.test.js @@ -1,145 +1,22 @@ -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) +// import 'regenerator-runtime/runtime' +// import { render, fireEvent, screen } from '@testing-library/react' +// import DocsSidenav from './' +// import expectThrow from '../../__test-helpers/expect-throw' describe('', () => { - it('should render and display nesting levels correctly', () => { - render() - expect(screen.getByTestId('root').className).toContain('g-docs-sidenav') - - // 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 levelTwo = screen.getByTestId('/docs/agent/autoauth') - expect(levelTwo.className).toMatch(/dir/) - expect(levelTwo.className).toMatch(/open active/) - - const levelThree = screen.getByTestId('/docs/agent/autoauth/methods') - expect(levelThree.className).toMatch(/dir/) - expect(levelThree.className).toMatch(/open active/) - - const levelFour = screen.getByTestId('/docs/agent/autoauth/methods/aws') - expect(levelFour.className).toMatch(/active/) - - // 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/) - // 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/) - // 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/) - }) - + it.todo('should render and display nesting levels correctly') 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('should show/hide the menu when the "menu" button is clicked on mobile', async () => { - 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') - }) - - 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('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') - }) + it.todo('should expand/collapse directory-level menu items when clicked') + it.todo( + 'should show/hide the menu when the "menu" button is clicked on mobile' + ) + it.todo('should error when a category is used with no content') + it.todo('should error when a page is not found within a category') + it.todo( + 'should error when a direct link does not use both "title" and "href"' + ) + it.todo('should error when a category contains no index file and no name') + it.todo( + 'should error when a category uses a name property and specifies an overview page' + ) }) diff --git a/packages/docs-sidenav/package-lock.json b/packages/docs-sidenav/package-lock.json index 0679c0b20..935a9a04b 100644 --- a/packages/docs-sidenav/package-lock.json +++ b/packages/docs-sidenav/package-lock.json @@ -1,26 +1,13 @@ { "name": "@hashicorp/react-docs-sidenav", - "version": "6.1.0", + "version": "7.0.0", "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..eefcda6f9 100644 --- a/packages/docs-sidenav/package.json +++ b/packages/docs-sidenav/package.json @@ -1,13 +1,12 @@ { "name": "@hashicorp/react-docs-sidenav", "description": "Sidebar used for navigation HashiCorp docs", - "version": "6.1.0", + "version": "7.0.0", "author": "HashiCorp", "contributors": [ "Jeff Escalante" ], "dependencies": { - "@hashicorp/react-link-wrap": "^0.0.3", "fuzzysearch": "1.0.3" }, "license": "MPL-2.0", diff --git a/packages/docs-sidenav/props.js b/packages/docs-sidenav/props.js index affb62107..1a9882951 100644 --- a/packages/docs-sidenav/props.js +++ b/packages/docs-sidenav/props.js @@ -1,29 +1,32 @@ +const sampleNavData = require('./fixtures/navdata-example.json') + +const productSlugs = [ + 'nomad', + 'consul', + 'terraform', + 'packer', + 'vagrant', + 'boundary', + 'waypoint', +] + 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', - ], + description: 'Slug of the current product for color theming', + testValue: 'terraform', + options: productSlugs, }, - currentPage: { + currentPath: { type: 'string', description: 'Path to the current page, used to select the currently active page.', - testValue: '/docs/agent/autoauth/methods/aws', + testValue: 'agent/autoauth', }, - category: { + rootPath: { type: 'string', - description: 'Top level navigation category, for example docs, api, etc.', + description: + 'Top level navigation category, for example `docs`, `api`, etc.', testValue: 'docs', }, disableFilter: { @@ -31,142 +34,9 @@ module.exports = { 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', - 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', - }, - ], + description: 'Tree of navigation data to render', + testValue: sampleNavData, }, } diff --git a/packages/docs-sidenav/style.module.css b/packages/docs-sidenav/style.module.css new file mode 100644 index 000000000..081c6b8e1 --- /dev/null +++ b/packages/docs-sidenav/style.module.css @@ -0,0 +1,237 @@ +@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-open='true'] { + box-shadow: 2px 2px 20px rgba(37, 38, 45, 0.2); + transform: translateX(100%); + padding-left: 25px; + } + } +} + +.mobileClose { + display: none; + + @media (--mobile-viewports) { + display: block; + position: absolute; + top: 18px; + right: 13px; + font-size: 1.7em; + padding: 0 14px; + cursor: pointer; + color: #333; + transition: opacity 0.3s ease; + z-index: 100; + + &:hover { + opacity: 0.7; + } + } +} + +.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 (--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; + + /* Alt design option: with background, as inline + color: var(--code-dark); + background: var(--code-light-transparent); + padding: 0.3em 0.625em; + border-radius: 3px; */ + } +} + +.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; + } +} + +.navCategoryIcon { + composes: navItemIcon; + transition: transform 0.15s ease; + + &[data-is-open='true'] { + transform: rotate(90deg); + } +} + +.navCategorySubnav { + 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..9628c167a --- /dev/null +++ b/packages/docs-sidenav/types.ts @@ -0,0 +1,45 @@ +// navData is an array of NavNodes +declare const navData: NavNode[] + +// A NavNode can be any of these types +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/_wip-migrate-to-nav-json.js b/packages/docs-sidenav/utils/_wip-migrate-to-nav-json.js new file mode 100644 index 000000000..d0773ce67 --- /dev/null +++ b/packages/docs-sidenav/utils/_wip-migrate-to-nav-json.js @@ -0,0 +1,186 @@ +const fs = require('fs') +const path = require('path') +const grayMatter = require('gray-matter') +const klawSync = require('klaw-sync') +const navigationJs = require('../../website/data/docs-navigation') +// Temp - setting this up as part of a GitHub action, just to make +// things easier during dev, really the JSON format would be +// updated itself and the old format would be dropped +const util = require('util') +const exec = util.promisify(require('child_process').exec) + +const INPUT_DIR = 'website/content' +const OUTPUT_DIR = '.generated' +const OUTPUT_FILE = 'docs-navigation.json' + +convertAndWrite().then(() => { + // Stage the changes so they're ready to commit + // (kind of relies on the outputDir, which is why it happens here) + const outputFile = path.join(process.cwd(), OUTPUT_DIR, OUTPUT_FILE) + exec(`git add ${outputFile}`).then(() => { + console.log('✅ Done') + }) +}) + +async function convertAndWrite() { + const fileFilter = (f) => path.extname(f.path) === '.mdx' + const collectedFrontmatter = await collectFrontmatter(INPUT_DIR, fileFilter) + const navJson = convertNavTree(navigationJs, collectedFrontmatter, [], 'docs') + writeJsonFile(navJson, OUTPUT_DIR, OUTPUT_FILE) +} + +function convertNavTree(navTree, collectedFrontmatter, pathStack, subfolder) { + const convertedTree = navTree.map((navNode) => { + // if the node is a string like '-----', + // we want to render a divider + const isString = typeof navNode === 'string' + const isDivider = isString && navNode.match(/^-+$/) + if (isDivider) return { divider: true } + // if the node is a string, but not a divider, + // we want to render a leaf node + if (isString) + return convertNavLeaf(navNode, collectedFrontmatter, pathStack, subfolder) + // if the node has an `href` or `title`, it's a direct link + if (navNode.href || navNode.title) { + // if a direct link doesn't have both `href` and `title`, we throw an error + throw new Error( + `Direct sidebar links must have both a "href" and "title". Found a direct link with only one of the two:\n\n ${JSON.stringify( + navNode + )}` + ) + } + // Otherwise, we expect the node to be a nested category + return convertNavCategory( + navNode, + collectedFrontmatter, + pathStack, + subfolder + ) + }) + return convertedTree +} + +function convertNavCategory( + navNode, + collectedFrontmatter, + pathStack, + subfolder +) { + // Throw an error if the category is invalid + if (!navNode.category || !navNode.content) { + throw new Error( + `Nav category nodes must have either a .name or .category property.` + ) + } + // Process the navNode's nested content into the new format + const nestedPathStack = pathStack.concat(navNode.category) + const nestedRoutes = convertNavTree( + navNode.content, + collectedFrontmatter, + nestedPathStack, + subfolder + ) + // Then, handle index data, which is a bit more of an undertaking... + // First, we try to gather index data for the entry + // The path we want for our new format does NOT contain + // the content subfolder or the file extension (always `.mdx`) + const USE_EXPLICIT_INDEX = false + const pathParts = USE_EXPLICIT_INDEX + ? [navNode.category, 'index'] + : [navNode.category] + const pathNewFormat = pathStack.concat(pathParts).join('/') + // The path we need to match from the older format + // includes the content subfolder as well as the file extension + const pathFromSubfolder = `${pathNewFormat}${ + USE_EXPLICIT_INDEX ? '' : '/index' + }.mdx` + const pathToMatch = path.join(subfolder, pathFromSubfolder) + // Try to find the corresponding index page resource + const matchedFrontmatter = collectedFrontmatter.filter((resource) => { + return resource.__resourcePath === pathToMatch + })[0] + const fmTitle = matchedFrontmatter + ? matchedFrontmatter.sidebar_title || matchedFrontmatter.page_title + : false + if (!fmTitle && !navNode.name) { + throw new Error( + `Nav category nodes must have either an index file with a sidebar_title or page_title in the frontmatter, or a .name property.` + ) + } + const title = fmTitle || navNode.name + // Set up an index page entry, if applicable + const indexRoute = matchedFrontmatter + ? { + title: 'Overview', + path: pathNewFormat, + } + : false + + // Finally, construct and return the category node + const routes = indexRoute ? [indexRoute, ...nestedRoutes] : nestedRoutes + return { + title: formatTitle(title), + routes, + } +} + +function convertNavLeaf(navNode, collectedFrontmatter, pathStack, subfolder) { + // The path we want for our new format does NOT contain + // the content subfolder or the file extension (always `.mdx`) + const pathNewFormat = pathStack.concat(navNode).join('/') + // The path we need to match from the older format + // includes the content subfolder as well as the file extension + const pathToMatch = path.join(subfolder, `${pathNewFormat}.mdx`) + // We filter for matching frontmatter to get the "title" for the nav leaf. + // We throw an error if there is no matching resource - there should be! + const matchedFrontmatter = collectedFrontmatter.filter((resource) => { + return resource.__resourcePath === pathToMatch + })[0] + if (!matchedFrontmatter) { + throw new Error( + `Could not find frontmatter for resource path ${pathToMatch}.` + ) + } + // We pull the title from frontmatter. + // We throw an error if there is no title in frontmatter - there should be! + const { sidebar_title, page_title } = matchedFrontmatter + const title = sidebar_title || page_title + if (!title) { + throw new Error(`Could not find title in frontmatter of ${pathToMatch}.`) + } + // Return the new format for the nav leaf + return { title: formatTitle(title), path: pathNewFormat } +} + +async function collectFrontmatter(inputDir, fileFilter) { + // Traverse directories and parse frontmatter + const targetFilepaths = klawSync(inputDir, { + traverseAll: true, + filter: fileFilter, + }).map((f) => f.path) + const inputDirPath = path.join(process.cwd(), inputDir) + const collectedFrontmatter = await Promise.all( + targetFilepaths.map(async (filePath) => { + const rawFile = fs.readFileSync(filePath, 'utf-8') + const { data: frontmatter } = grayMatter(rawFile) + const __resourcePath = path.relative(inputDirPath, filePath) + return { __resourcePath, ...frontmatter } + }) + ) + return collectedFrontmatter +} + +function formatTitle(title) { + return title.replace(//g, '').replace(/<\/tt>/g, '') +} + +function writeJsonFile(data, dir, file) { + // Set up a directory for output + const outputDir = path.join(process.cwd(), dir) + const outputFile = path.join(outputDir, file) + fs.mkdirSync(outputDir, { recursive: true }) + // Stringify the collected frontmatter, and write the file + const fileString = JSON.stringify(data, null, 2) + fs.writeFileSync(outputFile, fileString) + return true +} 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..059e38ad9 --- /dev/null +++ b/packages/docs-sidenav/utils/validate-file-paths/index.js @@ -0,0 +1,58 @@ +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) { + // Handle leaf nodes + if (navNode.path) { + // Ignore remote leaf nodes, these already + // have their content file explicitly defined + // TODO is there a way we can avoid remoteFile nodes, without making this implementation have to know about them? + if (navNode.remoteFile) return navNode + // Handle local leaf nodes + 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) { + console.log(JSON.stringify(navNode, null, 2)) + throw new Error( + `Could not find file to match path "${navNode.path}". Neither "${namedFilePath}" or "${indexFilePath}" could be found.` + ) + } + if (hasIndexFile && hasNamedFile) { + console.warn( + `Ambiguous path "${navNode.path}". Both "${namedFilePath}" and "${indexFilePath}" exist.` + ) + } + 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-route-structure/index.js b/packages/docs-sidenav/utils/validate-route-structure/index.js new file mode 100644 index 000000000..af23f6bb9 --- /dev/null +++ b/packages/docs-sidenav/utils/validate-route-structure/index.js @@ -0,0 +1,110 @@ +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. Depth: ${depth}`) + } + // Augment each navNode with its path __stack + const navNodesWithStacks = navNodes.map((navNode) => { + // Handle leaf nodes - split their paths into a __stack + if (navNode.path) return { ...navNode, __stack: navNode.path.split('/') } + // Handle branch nodes - we recurse depth-first here + if (navNode.routes) return handleBranchNode(navNode, depth) + // Other nodes aren't relevant, 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:\n\n${JSON.stringify(duplicateRoutes)}\n` + ) + } + // 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('/')}"` + ) + }) + // 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: ${JSON.stringify(uniqueParents)}`) + } + 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 the nested routes had structural issues + const [path, routesWithStacks] = validateBranchRoutes( + navNode.routes, + depth + 1 + ) + const __stack = path.split('/') + return { ...navNode, __stack, routes: routesWithStacks } +} + +module.exports = validateRouteStructure + +// +// +// +// +// +// +// +// + +// TODO - throw error if any non-divider node does not have a title +// function collectEmptyTitleErrors(navData) { +// const errors = []; +// return errors; +// } + +// TODO - throw error if direct link nodes don't have both { title, href } +// function collectDirectLinkErrors(navData) { +// const errors = []; +// return errors; +// } 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'; From 8bc32139fc5e60ab6af6756f64abe363eea74bae Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 25 Feb 2021 10:11:44 -0500 Subject: [PATCH 02/62] Reorder props for clearer diff --- packages/docs-page/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/docs-page/index.js b/packages/docs-page/index.js index 986100c51..67392199f 100644 --- a/packages/docs-page/index.js +++ b/packages/docs-page/index.js @@ -15,12 +15,12 @@ export function DocsPageWrapper({ description, filePath, mainBranch = 'main', + navData, currentPath, pageTitle, product: { name, slug }, showEditPage = true, subpath, - navData, }) { // TEMPORARY (https://app.asana.com/0/1100423001970639/1160656182754009) // activates the "jump to section" feature @@ -99,11 +99,11 @@ export default function DocsPage({ return ( Date: Thu, 25 Feb 2021 10:20:09 -0500 Subject: [PATCH 03/62] Add back useProductMeta --- packages/docs-page/server.js | 2 +- packages/docs-sidenav/index.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/docs-page/server.js b/packages/docs-page/server.js index ec6756c66..68c0083ce 100644 --- a/packages/docs-page/server.js +++ b/packages/docs-page/server.js @@ -75,7 +75,7 @@ async function renderPageMdx( currentPath, components, scope, - remarkPlugins + remarkPlugins = [] ) { // get the page being requested - figure out if its index page or leaf // prefer leaf if both are present diff --git a/packages/docs-sidenav/index.js b/packages/docs-sidenav/index.js index 4cc6a96d6..dd169c8df 100644 --- a/packages/docs-sidenav/index.js +++ b/packages/docs-sidenav/index.js @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react' +import useProductMeta from '@hashicorp/nextjs-scripts/lib/providers/product-meta' import Link from 'next/link' import InlineSvg from '@hashicorp/react-inline-svg' import svgMenuIcon from './icons/menu.svg?include' @@ -17,6 +18,7 @@ export default function DocsSidenav({ }) { const [open, setOpen] = useState(false) const [filterInput, setFilterInput] = useState('') + const { themeClass } = useProductMeta(product) const [content, setContent] = useState(navData) const [filteredContent, setFilteredContent] = useState(navData) @@ -39,7 +41,7 @@ export default function DocsSidenav({ }, [filterInput, content]) return ( -
    +
    setOpen(!open)}> Documentation Menu From 455b8ccbd85ea6f2b304b9eff063f0bea03faf12 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 25 Feb 2021 13:17:49 -0500 Subject: [PATCH 04/62] Test out npx-based approach to migration script --- .../migrate-nav-data.js} | 50 ++++------ packages/docs-sidenav/bin/npx-hello-world.js | 3 + packages/docs-sidenav/package-lock.json | 95 +++++++++++++++++++ packages/docs-sidenav/package.json | 10 +- 4 files changed, 123 insertions(+), 35 deletions(-) rename packages/docs-sidenav/{utils/_wip-migrate-to-nav-json.js => bin/migrate-nav-data.js} (78%) create mode 100644 packages/docs-sidenav/bin/npx-hello-world.js diff --git a/packages/docs-sidenav/utils/_wip-migrate-to-nav-json.js b/packages/docs-sidenav/bin/migrate-nav-data.js similarity index 78% rename from packages/docs-sidenav/utils/_wip-migrate-to-nav-json.js rename to packages/docs-sidenav/bin/migrate-nav-data.js index d0773ce67..dad42acb2 100644 --- a/packages/docs-sidenav/utils/_wip-migrate-to-nav-json.js +++ b/packages/docs-sidenav/bin/migrate-nav-data.js @@ -1,32 +1,26 @@ +#! /usr/bin/env node const fs = require('fs') const path = require('path') const grayMatter = require('gray-matter') const klawSync = require('klaw-sync') -const navigationJs = require('../../website/data/docs-navigation') -// Temp - setting this up as part of a GitHub action, just to make -// things easier during dev, really the JSON format would be -// updated itself and the old format would be dropped -const util = require('util') -const exec = util.promisify(require('child_process').exec) -const INPUT_DIR = 'website/content' -const OUTPUT_DIR = '.generated' -const OUTPUT_FILE = 'docs-navigation.json' +const navigationJsData = require(path.resolve(process.argv[2])) +const CONTENT_DIR = path.join(process.cwd(), process.argv[3]) +const OUT_FILE = path.join(process.cwd(), process.argv[4]) -convertAndWrite().then(() => { - // Stage the changes so they're ready to commit - // (kind of relies on the outputDir, which is why it happens here) - const outputFile = path.join(process.cwd(), OUTPUT_DIR, OUTPUT_FILE) - exec(`git add ${outputFile}`).then(() => { - console.log('✅ Done') - }) -}) +runMigration(navigationJsData, CONTENT_DIR, OUT_FILE) + +async function runMigration(navigationJsData, contentDir, outFile) { + const { navData } = await getMigratedNavData(navigationJsData, contentDir) + fs.writeFileSync(outFile, JSON.stringify(navData, null, 2)) + // fs.writeFileSync(outFile, JSON.stringify(collectedFrontmatter, null, 2)) +} -async function convertAndWrite() { +async function getMigratedNavData(navigationJsData, contentDir) { const fileFilter = (f) => path.extname(f.path) === '.mdx' - const collectedFrontmatter = await collectFrontmatter(INPUT_DIR, fileFilter) - const navJson = convertNavTree(navigationJs, collectedFrontmatter, [], 'docs') - writeJsonFile(navJson, OUTPUT_DIR, OUTPUT_FILE) + const collectedFrontmatter = await collectFrontmatter(contentDir, fileFilter) + const navData = convertNavTree(navigationJsData, collectedFrontmatter, [], '') + return { navData, collectedFrontmatter } } function convertNavTree(navTree, collectedFrontmatter, pathStack, subfolder) { @@ -158,12 +152,11 @@ async function collectFrontmatter(inputDir, fileFilter) { traverseAll: true, filter: fileFilter, }).map((f) => f.path) - const inputDirPath = path.join(process.cwd(), inputDir) const collectedFrontmatter = await Promise.all( targetFilepaths.map(async (filePath) => { const rawFile = fs.readFileSync(filePath, 'utf-8') const { data: frontmatter } = grayMatter(rawFile) - const __resourcePath = path.relative(inputDirPath, filePath) + const __resourcePath = path.relative(inputDir, filePath) return { __resourcePath, ...frontmatter } }) ) @@ -173,14 +166,3 @@ async function collectFrontmatter(inputDir, fileFilter) { function formatTitle(title) { return title.replace(//g, '').replace(/<\/tt>/g, '') } - -function writeJsonFile(data, dir, file) { - // Set up a directory for output - const outputDir = path.join(process.cwd(), dir) - const outputFile = path.join(outputDir, file) - fs.mkdirSync(outputDir, { recursive: true }) - // Stringify the collected frontmatter, and write the file - const fileString = JSON.stringify(data, null, 2) - fs.writeFileSync(outputFile, fileString) - return true -} diff --git a/packages/docs-sidenav/bin/npx-hello-world.js b/packages/docs-sidenav/bin/npx-hello-world.js new file mode 100644 index 000000000..593097b40 --- /dev/null +++ b/packages/docs-sidenav/bin/npx-hello-world.js @@ -0,0 +1,3 @@ +#! /usr/bin/env node +console.log(process.argv) +console.log('Hello world, executing from react-docs-subnav!') diff --git a/packages/docs-sidenav/package-lock.json b/packages/docs-sidenav/package-lock.json index 935a9a04b..8cff3fe71 100644 --- a/packages/docs-sidenav/package-lock.json +++ b/packages/docs-sidenav/package-lock.json @@ -4,10 +4,105 @@ "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==", + "dev": true, + "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==", + "dev": true + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, "fuzzysearch": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/fuzzysearch/-/fuzzysearch-1.0.3.tgz", "integrity": "sha1-3/yA9tawQiPyImqnndGUIxCW0Ag=" + }, + "graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", + "dev": true + }, + "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==", + "dev": true, + "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=", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "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==", + "dev": true + }, + "klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11" + } + }, + "section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "dev": true, + "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=", + "dev": true + }, + "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=", + "dev": true } } } diff --git a/packages/docs-sidenav/package.json b/packages/docs-sidenav/package.json index eefcda6f9..ed98d6384 100644 --- a/packages/docs-sidenav/package.json +++ b/packages/docs-sidenav/package.json @@ -3,6 +3,10 @@ "description": "Sidebar used for navigation HashiCorp docs", "version": "7.0.0", "author": "HashiCorp", + "bin": { + "npx-hello-world": "bin/npx-hello-world.js", + "migrate-nav-data": "bin/migrate-nav-data.js" + }, "contributors": [ "Jeff Escalante" ], @@ -17,5 +21,9 @@ "publishConfig": { "access": "public" }, - "gitHead": "c47c50a8419f6278073505ebbe25c9fbe4faab07" + "gitHead": "c47c50a8419f6278073505ebbe25c9fbe4faab07", + "devDependencies": { + "gray-matter": "^4.0.2", + "klaw-sync": "^6.0.0" + } } From 644398e8274b37e73abb4dc956a7743df1a9bdbf Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 25 Feb 2021 13:26:18 -0500 Subject: [PATCH 05/62] Remove devdeps, not useful --- packages/docs-sidenav/bin/migrate-nav-data.js | 11 +++ packages/docs-sidenav/package-lock.json | 95 ------------------- packages/docs-sidenav/package.json | 6 +- 3 files changed, 12 insertions(+), 100 deletions(-) diff --git a/packages/docs-sidenav/bin/migrate-nav-data.js b/packages/docs-sidenav/bin/migrate-nav-data.js index dad42acb2..dbf377c18 100644 --- a/packages/docs-sidenav/bin/migrate-nav-data.js +++ b/packages/docs-sidenav/bin/migrate-nav-data.js @@ -4,6 +4,17 @@ const path = require('path') const grayMatter = require('gray-matter') const klawSync = require('klaw-sync') +/* +USAGE + +Eg: +- old nav data is in ./data/docs-navigation.js +- content is in ./content/docs +- output file is ./data/docs-nav-data.json + +npx --package gray-matter --package klaw-sync --package @hashicorp/react-docs-sidenav@6.1.1-alpha.7 migrate-nav-data ./data/docs-navigation.js ./content/docs ./data/docs-nav-data.json +*/ + const navigationJsData = require(path.resolve(process.argv[2])) const CONTENT_DIR = path.join(process.cwd(), process.argv[3]) const OUT_FILE = path.join(process.cwd(), process.argv[4]) diff --git a/packages/docs-sidenav/package-lock.json b/packages/docs-sidenav/package-lock.json index 8cff3fe71..935a9a04b 100644 --- a/packages/docs-sidenav/package-lock.json +++ b/packages/docs-sidenav/package-lock.json @@ -4,105 +4,10 @@ "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==", - "dev": true, - "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==", - "dev": true - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, "fuzzysearch": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/fuzzysearch/-/fuzzysearch-1.0.3.tgz", "integrity": "sha1-3/yA9tawQiPyImqnndGUIxCW0Ag=" - }, - "graceful-fs": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", - "dev": true - }, - "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==", - "dev": true, - "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=", - "dev": true - }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "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==", - "dev": true - }, - "klaw-sync": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", - "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11" - } - }, - "section-matter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", - "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", - "dev": true, - "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=", - "dev": true - }, - "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=", - "dev": true } } } diff --git a/packages/docs-sidenav/package.json b/packages/docs-sidenav/package.json index ed98d6384..135e11a16 100644 --- a/packages/docs-sidenav/package.json +++ b/packages/docs-sidenav/package.json @@ -21,9 +21,5 @@ "publishConfig": { "access": "public" }, - "gitHead": "c47c50a8419f6278073505ebbe25c9fbe4faab07", - "devDependencies": { - "gray-matter": "^4.0.2", - "klaw-sync": "^6.0.0" - } + "gitHead": "c47c50a8419f6278073505ebbe25c9fbe4faab07" } From 8620fb583f1908c4eaf881948e9059b1648c6010 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 25 Feb 2021 13:27:34 -0500 Subject: [PATCH 06/62] Update comment --- packages/docs-sidenav/bin/migrate-nav-data.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docs-sidenav/bin/migrate-nav-data.js b/packages/docs-sidenav/bin/migrate-nav-data.js index dbf377c18..9e5ddeb3f 100644 --- a/packages/docs-sidenav/bin/migrate-nav-data.js +++ b/packages/docs-sidenav/bin/migrate-nav-data.js @@ -12,7 +12,7 @@ Eg: - content is in ./content/docs - output file is ./data/docs-nav-data.json -npx --package gray-matter --package klaw-sync --package @hashicorp/react-docs-sidenav@6.1.1-alpha.7 migrate-nav-data ./data/docs-navigation.js ./content/docs ./data/docs-nav-data.json +npx --package gray-matter --package klaw-sync --package @hashicorp/react-docs-sidenav@6.1.1-alpha.8 migrate-nav-data ./data/docs-navigation.js ./content/docs ./data/docs-nav-data.json */ const navigationJsData = require(path.resolve(process.argv[2])) From 87bc56cf9aa3042fe7b7477f60a75d8209a1243e Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 25 Feb 2021 15:47:49 -0500 Subject: [PATCH 07/62] Fix issue with direct links --- packages/docs-sidenav/bin/migrate-nav-data.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/docs-sidenav/bin/migrate-nav-data.js b/packages/docs-sidenav/bin/migrate-nav-data.js index 9e5ddeb3f..0255bebe9 100644 --- a/packages/docs-sidenav/bin/migrate-nav-data.js +++ b/packages/docs-sidenav/bin/migrate-nav-data.js @@ -47,12 +47,15 @@ function convertNavTree(navTree, collectedFrontmatter, pathStack, subfolder) { return convertNavLeaf(navNode, collectedFrontmatter, pathStack, subfolder) // if the node has an `href` or `title`, it's a direct link if (navNode.href || navNode.title) { - // if a direct link doesn't have both `href` and `title`, we throw an error - throw new Error( - `Direct sidebar links must have both a "href" and "title". Found a direct link with only one of the two:\n\n ${JSON.stringify( - navNode - )}` - ) + if (!navNode.href || !navNode.title) { + // if a direct link doesn't have both `href` and `title`, we throw an error + throw new Error( + `Direct sidebar links must have both a "href" and "title". Found a direct link with only one of the two:\n\n ${JSON.stringify( + navNode + )}` + ) + } + return { title: navNode.title, href: navNode.href } } // Otherwise, we expect the node to be a nested category return convertNavCategory( From a6ca716937356007bfc839720959ee7be91532de Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 25 Feb 2021 16:31:59 -0500 Subject: [PATCH 08/62] Refine DocsPage props API --- packages/docs-page/index.js | 33 ++++++++++++++----- packages/docs-page/package-lock.json | 6 ++-- packages/docs-page/package.json | 2 +- packages/docs-sidenav/bin/migrate-nav-data.js | 2 +- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/docs-page/index.js b/packages/docs-page/index.js index 67392199f..9fa2ff3cb 100644 --- a/packages/docs-page/index.js +++ b/packages/docs-page/index.js @@ -13,8 +13,7 @@ export function DocsPageWrapper({ canonicalUrl, children, description, - filePath, - mainBranch = 'main', + editLink, navData, currentPath, pageTitle, @@ -71,9 +70,7 @@ export function DocsPageWrapper({ {/* if desired, show an "edit this page" link on the bottom right, linking to github */} {showEditPage && (
    - + github logo Edit this page @@ -83,14 +80,33 @@ export function DocsPageWrapper({ ) } +/* + + const editLink = `https://github.com/hashicorp/${product.slug}/blob/${mainBranch}/website/content/${filePath}` + + Date: Thu, 25 Feb 2021 16:47:15 -0500 Subject: [PATCH 09/62] Update comment --- packages/docs-page/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/docs-page/index.js b/packages/docs-page/index.js index 9fa2ff3cb..72e4f0604 100644 --- a/packages/docs-page/index.js +++ b/packages/docs-page/index.js @@ -96,6 +96,7 @@ export function DocsPageWrapper({ frontMatter, currentPath }} +/> */ From c53eb2c8bfe3136f4d2abde4d675e3cb690594be Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 25 Feb 2021 17:12:21 -0500 Subject: [PATCH 10/62] Add todo --- packages/docs-page/index.js | 28 ++++------------------------ packages/docs-sidenav/index.js | 2 +- packages/docs-sidenav/package.json | 2 +- 3 files changed, 6 insertions(+), 26 deletions(-) diff --git a/packages/docs-page/index.js b/packages/docs-page/index.js index 72e4f0604..86c335a1e 100644 --- a/packages/docs-page/index.js +++ b/packages/docs-page/index.js @@ -19,7 +19,7 @@ export function DocsPageWrapper({ pageTitle, product: { name, slug }, showEditPage = true, - subpath, + subpath, // TODO rename to baseRoute, more accurate }) { // TEMPORARY (https://app.asana.com/0/1100423001970639/1160656182754009) // activates the "jump to section" feature @@ -46,7 +46,7 @@ export function DocsPageWrapper({
    @@ -80,29 +80,9 @@ export function DocsPageWrapper({ ) } -/* - - const editLink = `https://github.com/hashicorp/${product.slug}/blob/${mainBranch}/website/content/${filePath}` - - - -*/ - export default function DocsPage({ product, - subpath, + subpath, // TODO rename to baseRoute, more accurate navData, editLink, showEditPage = true, @@ -124,7 +104,7 @@ export default function DocsPage({ pageTitle={frontMatter.page_title} product={product} showEditPage={showEditPage} - subpath={subpath} + subpath={subpath} // TODO rename to baseRoute, more accurate > {content} diff --git a/packages/docs-sidenav/index.js b/packages/docs-sidenav/index.js index dd169c8df..bfb121fb3 100644 --- a/packages/docs-sidenav/index.js +++ b/packages/docs-sidenav/index.js @@ -11,7 +11,7 @@ import s from './style.module.css' export default function DocsSidenav({ currentPath, - rootPath, + rootPath, // TODO rename to baseRoute, more accurate product, disableFilter = false, navData, diff --git a/packages/docs-sidenav/package.json b/packages/docs-sidenav/package.json index 135e11a16..d647ee07b 100644 --- a/packages/docs-sidenav/package.json +++ b/packages/docs-sidenav/package.json @@ -15,7 +15,7 @@ }, "license": "MPL-2.0", "peerDependencies": { - "@hashicorp/nextjs-scripts": ">16.x", + "@hashicorp/nextjs-scripts": ">16.2", "react": "^16.9.0" }, "publishConfig": { From 231ed1d4448d84d82410670777e900e04c5660d4 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 26 Feb 2021 08:19:54 -0500 Subject: [PATCH 11/62] Begin cleanup --- packages/docs-page/index.js | 19 +- packages/docs-sidenav/index.js | 22 +- packages/docs-sidenav/style.css | 276 ------------------------- packages/docs-sidenav/style.module.css | 4 +- 4 files changed, 24 insertions(+), 297 deletions(-) delete mode 100644 packages/docs-sidenav/style.css diff --git a/packages/docs-page/index.js b/packages/docs-page/index.js index 86c335a1e..9c7b7834f 100644 --- a/packages/docs-page/index.js +++ b/packages/docs-page/index.js @@ -13,13 +13,13 @@ export function DocsPageWrapper({ canonicalUrl, children, description, - editLink, navData, currentPath, pageTitle, + baseRoute, + mainBranch = 'main', product: { name, slug }, showEditPage = true, - subpath, // TODO rename to baseRoute, more accurate }) { // TEMPORARY (https://app.asana.com/0/1100423001970639/1160656182754009) // activates the "jump to section" feature @@ -29,6 +29,9 @@ export function DocsPageWrapper({ return temporary_injectJumpToSection(node) }, [children]) + // mainBranch is only used to generate the editPageUrl + const editPageUrl = `https://github.com/hashicorp/${slug}/blob/${mainBranch}/website/content/${baseRoute}/${currentPath}` + return (
    {/* render the page's data to the document head */} @@ -46,7 +49,7 @@ export function DocsPageWrapper({
    @@ -70,7 +73,7 @@ export function DocsPageWrapper({ {/* if desired, show an "edit this page" link on the bottom right, linking to github */} {showEditPage && (
    - + github logo Edit this page @@ -82,9 +85,9 @@ export function DocsPageWrapper({ export default function DocsPage({ product, - subpath, // TODO rename to baseRoute, more accurate + baseRoute, navData, - editLink, + mainBranch, showEditPage = true, additionalComponents, staticProps: { mdxSource, frontMatter, currentPath }, @@ -98,13 +101,13 @@ export default function DocsPage({ {content} diff --git a/packages/docs-sidenav/index.js b/packages/docs-sidenav/index.js index bfb121fb3..6f4460c9c 100644 --- a/packages/docs-sidenav/index.js +++ b/packages/docs-sidenav/index.js @@ -11,7 +11,7 @@ import s from './style.module.css' export default function DocsSidenav({ currentPath, - rootPath, // TODO rename to baseRoute, more accurate + baseRoute, product, disableFilter = false, navData, @@ -60,7 +60,7 @@ export default function DocsSidenav({ /> )} { // Dividers if (item.divider) { @@ -133,21 +133,21 @@ function NavTree({ content, category, Link }) { key={item.path} title={item.title} isActive={item.__isActive} - url={`/${category}/${item.path}`} + url={`/${baseRoute}/${item.path}`} Link={Link} /> ) } - // Otherwise, render a nav category + // Otherwise, render a nav branch // (this will recurse and render a nav tree) return ( - ) @@ -172,7 +172,7 @@ function NavLeaf({ title, url, Link, isActive }) { ) } -function NavCategory({ title, routes, category, isActive, isFiltered, Link }) { +function NavBranch({ title, routes, baseRoute, isActive, isFiltered, Link }) { const [isOpen, setIsOpen] = useState(false) // Ensure categories appear open if they're active @@ -189,15 +189,15 @@ function NavCategory({ title, routes, category, isActive, isFiltered, Link }) { > -
      - +
        +
      ) 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 index 081c6b8e1..b1679872b 100644 --- a/packages/docs-sidenav/style.module.css +++ b/packages/docs-sidenav/style.module.css @@ -184,7 +184,7 @@ } } -.navCategoryIcon { +.navBranchIcon { composes: navItemIcon; transition: transform 0.15s ease; @@ -193,7 +193,7 @@ } } -.navCategorySubnav { +.navBranchSubnav { display: none; list-style: none; margin: 0; From 4cd6b44e50eb14ef4230dbec0539132819d3e4de Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 26 Feb 2021 08:22:24 -0500 Subject: [PATCH 12/62] Fix up docs --- packages/docs-page/docs.mdx | 12 ++++++------ packages/docs-page/index.js | 3 +-- packages/docs-sidenav/docs.mdx | 2 +- packages/docs-sidenav/props.js | 5 ++--- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/docs-page/docs.mdx b/packages/docs-page/docs.mdx index 5ed0f23e5..7bd3b5209 100644 --- a/packages/docs-page/docs.mdx +++ b/packages/docs-page/docs.mdx @@ -7,6 +7,8 @@ We have a lot of docs sites, all of which render content in exactly the same way Example Page

      This is a cool docs page!

      ', scope: {}, }, - frontMatter: { page_title: 'Test Page', description: 'test description' }, - filePath: 'test.mdx', - currentPath: componentProps.currentPath.testValue, + navData: componentProps.navData.testValue, }, }} >{`

      Example Page

      diff --git a/packages/docs-page/index.js b/packages/docs-page/index.js index 9c7b7834f..30face2f9 100644 --- a/packages/docs-page/index.js +++ b/packages/docs-page/index.js @@ -86,11 +86,10 @@ export function DocsPageWrapper({ export default function DocsPage({ product, baseRoute, - navData, mainBranch, showEditPage = true, additionalComponents, - staticProps: { mdxSource, frontMatter, currentPath }, + staticProps: { mdxSource, frontMatter, currentPath, navData }, }) { // This component is written to work with next-mdx-remote -- here it hydrates the content const content = hydrate(mdxSource, { diff --git a/packages/docs-sidenav/docs.mdx b/packages/docs-sidenav/docs.mdx index 3edf74178..f3c0d6696 100644 --- a/packages/docs-sidenav/docs.mdx +++ b/packages/docs-sidenav/docs.mdx @@ -7,7 +7,7 @@ DocsSidenav renders a tree of links, with the option to include nested sections. {``} diff --git a/packages/docs-sidenav/props.js b/packages/docs-sidenav/props.js index 1a9882951..8acb10a0a 100644 --- a/packages/docs-sidenav/props.js +++ b/packages/docs-sidenav/props.js @@ -23,10 +23,9 @@ module.exports = { 'Path to the current page, used to select the currently active page.', testValue: 'agent/autoauth', }, - rootPath: { + baseRoute: { type: 'string', - description: - 'Top level navigation category, for example `docs`, `api`, etc.', + description: 'Top level navigation route, for example `docs`, `api`, etc.', testValue: 'docs', }, disableFilter: { From dd7231e513477549aa24d5811ba86abf15291a5f Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 26 Feb 2021 08:27:18 -0500 Subject: [PATCH 13/62] Lerna please recognize that I've changed DocsSidenav --- packages/docs-sidenav/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docs-sidenav/index.js b/packages/docs-sidenav/index.js index 6f4460c9c..73db68dfb 100644 --- a/packages/docs-sidenav/index.js +++ b/packages/docs-sidenav/index.js @@ -13,8 +13,8 @@ export default function DocsSidenav({ currentPath, baseRoute, product, - disableFilter = false, navData, + disableFilter = false, }) { const [open, setOpen] = useState(false) const [filterInput, setFilterInput] = useState('') From e0d67214f944bd059a091040a7c205ca0cc49320 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 26 Feb 2021 08:28:56 -0500 Subject: [PATCH 14/62] Bump docs-sidenav prerelease in DocsPage --- packages/docs-page/package-lock.json | 6 +++--- packages/docs-page/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/docs-page/package-lock.json b/packages/docs-page/package-lock.json index dbd4ddd5c..aef250741 100644 --- a/packages/docs-page/package-lock.json +++ b/packages/docs-page/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "@hashicorp/react-docs-sidenav": { - "version": "6.1.1-alpha.10", - "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-sidenav/-/react-docs-sidenav-6.1.1-alpha.10.tgz", - "integrity": "sha512-5+Rn6nB+Uq8OOhSNOwb+Ecf8dVi4MjUn2cAPeP4Q450lAnSspGyEW38XFXNGO7a1oHsQrLEhcPk7TiqtAeRL0A==", + "version": "6.1.1-alpha.16", + "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-sidenav/-/react-docs-sidenav-6.1.1-alpha.16.tgz", + "integrity": "sha512-RpPjNwMNe5L2LA1vvgp496CauVJ8wLnKge1lPBZKL5931jR1SFEMwuWLB8R6Pe2HmkIC55nPB/c43GrmPN4FFw==", "requires": { "fuzzysearch": "1.0.3" } diff --git a/packages/docs-page/package.json b/packages/docs-page/package.json index 3b5d96a39..5cbbce2b7 100644 --- a/packages/docs-page/package.json +++ b/packages/docs-page/package.json @@ -8,7 +8,7 @@ ], "dependencies": { "@hashicorp/react-content": "^6.2.2", - "@hashicorp/react-docs-sidenav": "6.1.1-alpha.10", + "@hashicorp/react-docs-sidenav": "6.1.1-alpha.16", "@hashicorp/react-head": "^1.2.0", "@hashicorp/react-search": "^4.1.0", "fs-exists-sync": "0.1.0", From 00e0902ddc24d51e81dc84409c9cda98c082269c Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 26 Feb 2021 09:11:30 -0500 Subject: [PATCH 15/62] Pull in DocsPage server changes from Packer work --- packages/docs-page/render-page-mdx.js | 41 +++ packages/docs-page/server.js | 261 +++++++++----------- packages/docs-page/server.temp-reference.js | 161 ++++++++++++ 3 files changed, 322 insertions(+), 141 deletions(-) create mode 100644 packages/docs-page/render-page-mdx.js create mode 100644 packages/docs-page/server.temp-reference.js diff --git a/packages/docs-page/render-page-mdx.js b/packages/docs-page/render-page-mdx.js new file mode 100644 index 000000000..22ceaa364 --- /dev/null +++ b/packages/docs-page/render-page-mdx.js @@ -0,0 +1,41 @@ +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' + +/* + + // example basic use + const { mdxSource, frontMatter } = await renderPageMdx( + mdxString, + productName + ) + +*/ + +async function renderPageMdx( + mdxFileString, + productName, + scope, // optional, i think? + additionalComponents = {}, + remarkPlugins = [] +) { + const components = generateComponents(productName, additionalComponents) + const { data: frontMatter, content } = grayMatter(mdxFileString) + 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 68c0083ce..4fffb09f6 100644 --- a/packages/docs-page/server.js +++ b/packages/docs-page/server.js @@ -1,161 +1,140 @@ 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) - ) +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 currentPath = params.page ? params.page.join('/') : '/' - - // get frontmatter from all other pages in the category, for the sidebar - const allFrontMatter = await fastReadFrontMatter(docsPath) +async function resolveNavData(filePath, localContentDir) { + // TODO - memo-ize? Will that affect things? Rationale is that NextJS + // must be calling this function twice for every page render... + // and input arguments and therefore return value will always be the same. + // So may be worth memo-izing. + const navDataFile = path.join(process.cwd(), filePath) + const navDataRaw = JSON.parse(fs.readFileSync(navDataFile, 'utf8')) + const withFilePaths = await validateFilePaths(navDataRaw, localContentDir) + return withFilePaths +} - // render the current page path markdown - const { mdxSource, frontMatter, filePath } = await renderPageMdx( - docsPath, - currentPath, - generateComponents(productName, additionalComponents), - scope, - remarkPlugins - ) +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 +} - return { - props: { - data: allFrontMatter.map((p) => ({ - ...p, - __resourcePath: `${subpath}/${p.__resourcePath}`, - })), - mdxSource, - frontMatter, - filePath: `${subpath}/${filePath}`, - currentPath, - }, - } +async function generateStaticProps( + navDataFile, + localContentDir, + params, + productName, + 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) + // Set up the MDX content to re-hydrate client-side + const { filePath } = navNode + const mdxString = fs.readFileSync(path.join(process.cwd(), filePath), 'utf8') + const { mdxSource, frontMatter } = await renderPageMdx(mdxString, productName) + // Return all the props + return { currentPath, frontMatter, 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, - currentPath, - 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, `${currentPath}.mdx`) - const indexPath = path.join(root, `${currentPath}/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) +export { + generateStaticPaths, + generateStaticProps, + getNodeFromPath, + getPathsFromNavData, + validateNavData, + validateFilePaths, } diff --git a/packages/docs-page/server.temp-reference.js b/packages/docs-page/server.temp-reference.js new file mode 100644 index 000000000..68c0083ce --- /dev/null +++ b/packages/docs-page/server.temp-reference.js @@ -0,0 +1,161 @@ +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' + +export async function generateStaticPaths(subpath) { + const paths = await getStaticMdxPaths( + path.join(process.cwd(), 'content', subpath) + ) + + return { paths, fallback: false } +} + +export async function generateStaticProps({ + subpath, + productName, + params, + additionalComponents, + scope, + remarkPlugins, +}) { + const docsPath = path.join(process.cwd(), 'content', subpath) + const currentPath = params.page ? params.page.join('/') : '/' + + // get frontmatter from all other pages in the category, for the sidebar + const allFrontMatter = await fastReadFrontMatter(docsPath) + + // render the current page path markdown + const { mdxSource, frontMatter, filePath } = await renderPageMdx( + docsPath, + currentPath, + generateComponents(productName, additionalComponents), + scope, + remarkPlugins + ) + + return { + props: { + data: allFrontMatter.map((p) => ({ + ...p, + __resourcePath: `${subpath}/${p.__resourcePath}`, + })), + mdxSource, + frontMatter, + filePath: `${subpath}/${filePath}`, + currentPath, + }, + } +} + +async function getStaticMdxPaths(root) { + const files = await readdirp.promise(root, { fileFilter: ['*.mdx'] }) + + return files.map(({ path: p }) => { + return { + params: { + page: p + .replace(/\.mdx$/, '') + .split('/') + .filter((p) => p !== 'index'), + }, + } + }) +} + +async function renderPageMdx( + root, + currentPath, + 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, `${currentPath}.mdx`) + const indexPath = path.join(root, `${currentPath}/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. + throw new Error( + `We went looking for "${leafPath}" and "${indexPath}" but neither one was found.` + ) + } + + 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}/`, ''), + } +} + +// 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 + +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) +} From 9f7a9e75c42f3861727f21f19114ddb3eb3f1eb3 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 26 Feb 2021 09:22:20 -0500 Subject: [PATCH 16/62] Pull in server work from Packer --- packages/docs-page/render-page-mdx.js | 20 ++++++-------------- packages/docs-page/server.js | 16 +++++++++++++--- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/docs-page/render-page-mdx.js b/packages/docs-page/render-page-mdx.js index 22ceaa364..0a0c38767 100644 --- a/packages/docs-page/render-page-mdx.js +++ b/packages/docs-page/render-page-mdx.js @@ -4,22 +4,14 @@ import markdownDefaults from '@hashicorp/nextjs-scripts/markdown' import generateComponents from './components' import grayMatter from 'gray-matter' -/* - - // example basic use - const { mdxSource, frontMatter } = await renderPageMdx( - mdxString, - productName - ) - -*/ - async function renderPageMdx( mdxFileString, - productName, - scope, // optional, i think? - additionalComponents = {}, - remarkPlugins = [] + { + productName, + additionalComponents = {}, + remarkPlugins = [], + scope, // optional, i think? + } ) { const components = generateComponents(productName, additionalComponents) const { data: frontMatter, content } = grayMatter(mdxFileString) diff --git a/packages/docs-page/server.js b/packages/docs-page/server.js index 4fffb09f6..c0aecdadf 100644 --- a/packages/docs-page/server.js +++ b/packages/docs-page/server.js @@ -44,8 +44,13 @@ async function generateStaticProps( navDataFile, localContentDir, params, - productName, - paramId = DEFAULT_PARAM_ID + { + productName, // optional, used to configure EnterpriseAlert + additionalComponents = {}, + remarkPlugins = [], + scope, // optional, I think? + paramId = DEFAULT_PARAM_ID, + } ) { // Read in the nav data, and resolve local filePaths const navData = await resolveNavData(navDataFile, localContentDir) @@ -56,7 +61,12 @@ async function generateStaticProps( // Set up the MDX content to re-hydrate client-side const { filePath } = navNode const mdxString = fs.readFileSync(path.join(process.cwd(), filePath), 'utf8') - const { mdxSource, frontMatter } = await renderPageMdx(mdxString, productName) + const { mdxSource, frontMatter } = await renderPageMdx(mdxString, { + productName, + additionalComponents, + remarkPlugins, + scope, + }) // Return all the props return { currentPath, frontMatter, mdxSource, navData } } From 60f6395185650b297af8482d70434fb2117b4dba Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 26 Feb 2021 10:12:42 -0500 Subject: [PATCH 17/62] Fix issue with undefined default options object --- packages/docs-page/render-page-mdx.js | 2 +- packages/docs-page/server.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/docs-page/render-page-mdx.js b/packages/docs-page/render-page-mdx.js index 0a0c38767..e62f38d19 100644 --- a/packages/docs-page/render-page-mdx.js +++ b/packages/docs-page/render-page-mdx.js @@ -11,7 +11,7 @@ async function renderPageMdx( additionalComponents = {}, remarkPlugins = [], scope, // optional, i think? - } + } = {} ) { const components = generateComponents(productName, additionalComponents) const { data: frontMatter, content } = grayMatter(mdxFileString) diff --git a/packages/docs-page/server.js b/packages/docs-page/server.js index c0aecdadf..898ee62b1 100644 --- a/packages/docs-page/server.js +++ b/packages/docs-page/server.js @@ -50,7 +50,7 @@ async function generateStaticProps( remarkPlugins = [], scope, // optional, I think? paramId = DEFAULT_PARAM_ID, - } + } = {} ) { // Read in the nav data, and resolve local filePaths const navData = await resolveNavData(navDataFile, localContentDir) From 1d7ee19021b2f46981ab8bcf828510ccfe15c0a2 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 26 Feb 2021 10:56:25 -0500 Subject: [PATCH 18/62] Change generateStaticPaths args for consistency --- packages/docs-page/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docs-page/server.js b/packages/docs-page/server.js index 898ee62b1..7d3e65b9e 100644 --- a/packages/docs-page/server.js +++ b/packages/docs-page/server.js @@ -9,7 +9,7 @@ const DEFAULT_PARAM_ID = 'page' async function generateStaticPaths( navDataFile, localContentDir, - paramId = DEFAULT_PARAM_ID + { paramId = DEFAULT_PARAM_ID } = {} ) { // Fetch and parse navigation data const navData = await resolveNavData(navDataFile, localContentDir) From 6d70cd31bd5970084bc91aae7049a643f09e329b Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 26 Feb 2021 15:47:06 -0500 Subject: [PATCH 19/62] Add content hook to allow modification of mdxString --- packages/docs-page/render-page-mdx.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/docs-page/render-page-mdx.js b/packages/docs-page/render-page-mdx.js index e62f38d19..1f1bc6f5f 100644 --- a/packages/docs-page/render-page-mdx.js +++ b/packages/docs-page/render-page-mdx.js @@ -8,13 +8,15 @@ async function renderPageMdx( mdxFileString, { productName, + mdxContentHook = (c) => c, additionalComponents = {}, remarkPlugins = [], scope, // optional, i think? } = {} ) { const components = generateComponents(productName, additionalComponents) - const { data: frontMatter, content } = grayMatter(mdxFileString) + 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'), From db0e6f4a78031e95910f098dca298e2023838fc5 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 4 Mar 2021 10:31:52 -0500 Subject: [PATCH 20/62] Delete reference file --- packages/docs-page/server.temp-reference.js | 161 -------------------- 1 file changed, 161 deletions(-) delete mode 100644 packages/docs-page/server.temp-reference.js diff --git a/packages/docs-page/server.temp-reference.js b/packages/docs-page/server.temp-reference.js deleted file mode 100644 index 68c0083ce..000000000 --- a/packages/docs-page/server.temp-reference.js +++ /dev/null @@ -1,161 +0,0 @@ -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' - -export async function generateStaticPaths(subpath) { - const paths = await getStaticMdxPaths( - path.join(process.cwd(), 'content', subpath) - ) - - return { paths, fallback: false } -} - -export async function generateStaticProps({ - subpath, - productName, - params, - additionalComponents, - scope, - remarkPlugins, -}) { - const docsPath = path.join(process.cwd(), 'content', subpath) - const currentPath = params.page ? params.page.join('/') : '/' - - // get frontmatter from all other pages in the category, for the sidebar - const allFrontMatter = await fastReadFrontMatter(docsPath) - - // render the current page path markdown - const { mdxSource, frontMatter, filePath } = await renderPageMdx( - docsPath, - currentPath, - generateComponents(productName, additionalComponents), - scope, - remarkPlugins - ) - - return { - props: { - data: allFrontMatter.map((p) => ({ - ...p, - __resourcePath: `${subpath}/${p.__resourcePath}`, - })), - mdxSource, - frontMatter, - filePath: `${subpath}/${filePath}`, - currentPath, - }, - } -} - -async function getStaticMdxPaths(root) { - const files = await readdirp.promise(root, { fileFilter: ['*.mdx'] }) - - return files.map(({ path: p }) => { - return { - params: { - page: p - .replace(/\.mdx$/, '') - .split('/') - .filter((p) => p !== 'index'), - }, - } - }) -} - -async function renderPageMdx( - root, - currentPath, - 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, `${currentPath}.mdx`) - const indexPath = path.join(root, `${currentPath}/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. - throw new Error( - `We went looking for "${leafPath}" and "${indexPath}" but neither one was found.` - ) - } - - 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}/`, ''), - } -} - -// 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 - -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) -} From 5eff59670d8f6e7fb516187bccfa1ed273de18fb Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 4 Mar 2021 10:38:34 -0500 Subject: [PATCH 21/62] Add comment on productName arg --- packages/docs-page/render-page-mdx.js | 2 +- packages/docs-page/server.js | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/docs-page/render-page-mdx.js b/packages/docs-page/render-page-mdx.js index 1f1bc6f5f..f57f77c0a 100644 --- a/packages/docs-page/render-page-mdx.js +++ b/packages/docs-page/render-page-mdx.js @@ -11,7 +11,7 @@ async function renderPageMdx( mdxContentHook = (c) => c, additionalComponents = {}, remarkPlugins = [], - scope, // optional, i think? + scope, } = {} ) { const components = generateComponents(productName, additionalComponents) diff --git a/packages/docs-page/server.js b/packages/docs-page/server.js index 7d3e65b9e..2113afd90 100644 --- a/packages/docs-page/server.js +++ b/packages/docs-page/server.js @@ -45,7 +45,12 @@ async function generateStaticProps( localContentDir, params, { - productName, // optional, used to configure EnterpriseAlert + // Note: productName is ultimately only passed to createEnterpriseAlert. + // we may be able to remove the need for this additional arg / option by + // leveraging recent work on our product-meta provider: + // where arg is used: https://github.com/hashicorp/nextjs-scripts/blob/462eb2efa0c95ab5d81ad1b5d7896427a0263011/lib/providers/docs/index.jsx#L35-L48 + // product-meta: https://github.com/hashicorp/nextjs-scripts/tree/main/lib/providers/product-meta + productName, additionalComponents = {}, remarkPlugins = [], scope, // optional, I think? From a794e0d7d6005cde3f79fae9cb73f8428ea94084 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 4 Mar 2021 12:39:33 -0500 Subject: [PATCH 22/62] Use shared product slug prop --- packages/docs-page/props.js | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/packages/docs-page/props.js b/packages/docs-page/props.js index 6016a7f1f..500817a36 100644 --- a/packages/docs-page/props.js +++ b/packages/docs-page/props.js @@ -1,4 +1,5 @@ const navDataExample = require('../docs-sidenav/fixtures/navdata-example.json') +const sharedProps = require('../../props') module.exports = { product: { @@ -9,22 +10,7 @@ module.exports = { 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', - ], - }, + slug: sharedProps.product, }, }, subpath: { From 9785ada8c23a427078fd970f3195176821faae52 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 4 Mar 2021 13:13:31 -0500 Subject: [PATCH 23/62] Continue cleanup, update props specs --- packages/docs-page/_temp-pr-notes.md | 26 +++++++++++ packages/docs-page/docs.mdx | 6 +-- packages/docs-page/props.js | 44 +++++++++++-------- .../{navdata-example.json => nav-data.json} | 0 packages/docs-sidenav/props.js | 26 +++-------- packages/docs-sidenav/style.module.css | 6 --- packages/docs-sidenav/types.ts | 6 +-- 7 files changed, 64 insertions(+), 50 deletions(-) create mode 100644 packages/docs-page/_temp-pr-notes.md rename packages/docs-sidenav/fixtures/{navdata-example.json => nav-data.json} (100%) diff --git a/packages/docs-page/_temp-pr-notes.md b/packages/docs-page/_temp-pr-notes.md new file mode 100644 index 000000000..eff271788 --- /dev/null +++ b/packages/docs-page/_temp-pr-notes.md @@ -0,0 +1,26 @@ +# Changes to implement MKTG-032 + +To implement MKTG-032, changes have been made to both the `docs-page` and `doc-sidenav` components. + +## Common changes + +### Renamed props + +- `subpath` prop renamed to `baseRoute` +- `pagePath` prop renamed to `currentPath` + +### Refactored props + +- `order` and `data` props replaced by `staticProps.navData` + +## Changes to DocsPageWrapper + +- `order` and `allPageData` props replaced by `staticProps.navData` + +## Changes to DocsPage + +--- + +## Changes to DocsSidenav + +--- diff --git a/packages/docs-page/docs.mdx b/packages/docs-page/docs.mdx index 7bd3b5209..d4d9c0c52 100644 --- a/packages/docs-page/docs.mdx +++ b/packages/docs-page/docs.mdx @@ -7,8 +7,6 @@ We have a lot of docs sites, all of which render content in exactly the same way Example Page

      This is a cool docs page!

      ', scope: {}, }, - navData: componentProps.navData.testValue, + navData: componentProps.staticProps.properties.navData.testValue, + currentPath: componentProps.staticProps.properties.currentPath.testValue, + frontMatter: { page_title: 'Test Page', description: 'test description' }, }, }} >{``, and the `Edit this page` link.', properties: { name: { type: 'string', @@ -13,39 +14,44 @@ module.exports = { slug: sharedProps.product, }, }, - subpath: { + baseRoute: { type: 'string', 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`', - }, - navData: { - type: 'object', - testValue: navDataExample, + 'The path this page is rendering under, for example `"docs"` or `"api-docs"`. Passed directly to the `baseRoute` prop of `@hashicorp/react-docs-sidenav`. Also used for the `Edit this page` link.', }, - currentPath: { + mainBranch: { type: 'string', - testValue: 'agent/autoauth', - }, - 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 default branch of the project being documented, typically either "master" or "main". Used for the `Edit this page` link.', + default: 'main', }, 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', description: 'Directly pass the return value of `server/generateStaticProps` in here.', + properties: { + mdxSource: { + type: 'object', + description: + "Data returned from running `next-mdx-remote/render-to-string` on the page's isolated `.mdx` file contents.", + }, + frontmatter: { + type: 'object', + description: "Frontmatter object parsed from the page's `.mdx` file.", + }, + currentPath: docsSidenavProps.currentPath, + navData: docsSidenavProps.navData, + }, }, } diff --git a/packages/docs-sidenav/fixtures/navdata-example.json b/packages/docs-sidenav/fixtures/nav-data.json similarity index 100% rename from packages/docs-sidenav/fixtures/navdata-example.json rename to packages/docs-sidenav/fixtures/nav-data.json diff --git a/packages/docs-sidenav/props.js b/packages/docs-sidenav/props.js index 8acb10a0a..4866a9e48 100644 --- a/packages/docs-sidenav/props.js +++ b/packages/docs-sidenav/props.js @@ -1,22 +1,8 @@ -const sampleNavData = require('./fixtures/navdata-example.json') - -const productSlugs = [ - 'nomad', - 'consul', - 'terraform', - 'packer', - 'vagrant', - 'boundary', - 'waypoint', -] +const sampleNavData = require('./fixtures/nav-data.json') +const sharedProps = require('../../props') module.exports = { - product: { - type: 'string', - description: 'Slug of the current product for color theming', - testValue: 'terraform', - options: productSlugs, - }, + product: sharedProps.product, currentPath: { type: 'string', description: @@ -25,7 +11,8 @@ module.exports = { }, baseRoute: { type: 'string', - description: 'Top level navigation route, for example `docs`, `api`, etc.', + description: + 'Top level navigation route, for example `docs`, `api-docs`, etc.', testValue: 'docs', }, disableFilter: { @@ -35,7 +22,8 @@ module.exports = { }, navData: { type: 'object', - description: 'Tree of navigation data to render', + description: + 'Tree of navigation data to render. See `docs-sidenav/types.js` for details.', testValue: sampleNavData, }, } diff --git a/packages/docs-sidenav/style.module.css b/packages/docs-sidenav/style.module.css index b1679872b..ae79220fe 100644 --- a/packages/docs-sidenav/style.module.css +++ b/packages/docs-sidenav/style.module.css @@ -132,12 +132,6 @@ & code { margin-left: 2px; font-size: 0.875rem; - - /* Alt design option: with background, as inline - color: var(--code-dark); - background: var(--code-light-transparent); - padding: 0.3em 0.625em; - border-radius: 3px; */ } } diff --git a/packages/docs-sidenav/types.ts b/packages/docs-sidenav/types.ts index 9628c167a..57cdad876 100644 --- a/packages/docs-sidenav/types.ts +++ b/packages/docs-sidenav/types.ts @@ -1,8 +1,8 @@ -// navData is an array of NavNodes -declare const navData: NavNode[] +// NavData is an array of NavNodes +export type NavData = NavNode[] // A NavNode can be any of these types -type NavNode = NavLeaf | NavDirectLink | NavDivider | NavBranch +export type NavNode = NavLeaf | NavDirectLink | NavDivider | NavBranch // A NavLeaf represents a page with content. // From 8b3f2f36075fdf93be85648db80746b2495eca9d Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 4 Mar 2021 13:16:45 -0500 Subject: [PATCH 24/62] Nit to cleanup diff --- packages/docs-page/docs.mdx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/docs-page/docs.mdx b/packages/docs-page/docs.mdx index d4d9c0c52..4839a25ae 100644 --- a/packages/docs-page/docs.mdx +++ b/packages/docs-page/docs.mdx @@ -47,8 +47,7 @@ We have a lot of docs sites, all of which render content in exactly the same way >{`

      Example Page

      From e41cef700fce955aaabe537caa69dde88c9f4341 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 4 Mar 2021 13:33:00 -0500 Subject: [PATCH 25/62] Continue refining docs --- packages/docs-page/docs.mdx | 2 +- packages/docs-page/index.js | 7 +++---- packages/docs-page/props.js | 28 +++++++++++++++++++++++++++- packages/docs-sidenav/index.js | 3 ++- packages/docs-sidenav/props.js | 4 +++- 5 files changed, 36 insertions(+), 8 deletions(-) diff --git a/packages/docs-page/docs.mdx b/packages/docs-page/docs.mdx index 4839a25ae..e8e788004 100644 --- a/packages/docs-page/docs.mdx +++ b/packages/docs-page/docs.mdx @@ -40,8 +40,8 @@ We have a lot of docs sites, all of which render content in exactly the same way scope: {}, }, navData: componentProps.staticProps.properties.navData.testValue, - currentPath: componentProps.staticProps.properties.currentPath.testValue, frontMatter: { page_title: 'Test Page', description: 'test description' }, + currentPath: componentProps.staticProps.properties.currentPath.testValue, }, }} >{` {/* render the page's data to the document head */} @@ -73,7 +70,9 @@ export function DocsPageWrapper({ {/* if desired, show an "edit this page" link on the bottom right, linking to github */} {showEditPage && (
      - + github logo Edit this page diff --git a/packages/docs-page/props.js b/packages/docs-page/props.js index 0f7d391fd..646aee14a 100644 --- a/packages/docs-page/props.js +++ b/packages/docs-page/props.js @@ -4,18 +4,22 @@ const sharedProps = require('../../props') module.exports = { product: { type: 'string', + required: true, description: 'The `name` and `slug` of the product this page is being rendered for. Used for theming, page ``, and the `Edit this page` link.', properties: { name: { type: 'string', - description: 'Human-readable proper case product name', + required: true, + description: + 'Human-readable proper case product name. Used for the page `<title />` and `og:site_name`.', }, slug: sharedProps.product, }, }, baseRoute: { type: 'string', + required: true, description: 'The path this page is rendering under, for example `"docs"` or `"api-docs"`. Passed directly to the `baseRoute` prop of `@hashicorp/react-docs-sidenav`. Also used for the `Edit this page` link.', }, @@ -38,17 +42,39 @@ module.exports = { }, staticProps: { type: 'object', + required: true, description: 'Directly pass the return value of `server/generateStaticProps` in here.', properties: { mdxSource: { type: 'object', + required: true, description: "Data returned from running `next-mdx-remote/render-to-string` on the page's isolated `.mdx` file contents.", }, frontmatter: { type: 'object', + required: true, description: "Frontmatter object parsed from the page's `.mdx` file.", + properties: { + canonicalUrl: { + type: 'string', + description: + 'Optional canonical URL. Passed directly to [@hashicorp/react-head](/?component=Head).', + }, + description: { + type: 'string', + description: + 'Used for the `<meta name="description" />`. Passed directly to [@hashicorp/react-head](/?component=Head).', + required: true, + }, + pageTitle: { + type: 'string', + description: + 'Used to construct the meta `<title />` tag, then passed to [@hashicorp/react-head](/?component=Head).', + required: true, + }, + }, }, currentPath: docsSidenavProps.currentPath, navData: docsSidenavProps.navData, diff --git a/packages/docs-sidenav/index.js b/packages/docs-sidenav/index.js index 73db68dfb..325d389b3 100644 --- a/packages/docs-sidenav/index.js +++ b/packages/docs-sidenav/index.js @@ -90,7 +90,8 @@ function addIsActiveToNode(navNode, currentPath) { } // Otherwise, return false // (for dividers, external links, etc) - // TODO - do we need to worry about highlighting external links? + // TODO - do we need to worry about highlighting external links? yes probably, + // sometimes these are used not as "external" but to internal links outside the baseRoute return navNode } diff --git a/packages/docs-sidenav/props.js b/packages/docs-sidenav/props.js index 4866a9e48..1f77f32fd 100644 --- a/packages/docs-sidenav/props.js +++ b/packages/docs-sidenav/props.js @@ -6,11 +6,12 @@ module.exports = { currentPath: { type: 'string', description: - 'Path to the current page, used to select the currently active page.', + 'Path to the current page, relative to the `baseRoute`. Used to highlight the current page.', testValue: 'agent/autoauth', }, baseRoute: { type: 'string', + required: true, description: 'Top level navigation route, for example `docs`, `api-docs`, etc.', testValue: 'docs', @@ -22,6 +23,7 @@ module.exports = { }, navData: { type: 'object', + required: true, description: 'Tree of navigation data to render. See `docs-sidenav/types.js` for details.', testValue: sampleNavData, From 7896cb46c5f5b52601f9e3b72b330eba657fc8b1 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 4 Mar 2021 13:40:00 -0500 Subject: [PATCH 26/62] Update readme --- packages/docs-page/README.md | 36 +++++++++++++--------------- packages/docs-page/_temp-pr-notes.md | 15 ++++-------- 2 files changed, 22 insertions(+), 29 deletions(-) diff --git a/packages/docs-page/README.md b/packages/docs-page/README.md index 508dabf29..c2886644a 100644 --- a/packages/docs-page/README.md +++ b/packages/docs-page/README.md @@ -1,44 +1,42 @@ # DocsPage -The **DocsPage** component lets you create a Hashicorp branded docs page in NextJS projects using `next-mdx-remote`. This is a very highly abstracted component with slightly more involved usage since it renders an entire page. +The **DocsPage** component lets you create a Hashicorp branded docs page in NextJS projects using `next-mdx-remote`. This is a very highly abstracted component with slightly more involved usage since it renders an entire collection of pages. ## Example Usage -This component is intended to be used on an [optional catch-all route](https://nextjs.org/docs/routing/dynamic-routes#optional-catch-all-routes) page, like `pages/docs/[[slug]].mdx` - example source shown below: +This component is intended to be used on an [optional catch-all route](https://nextjs.org/docs/routing/dynamic-routes#optional-catch-all-routes) page, like `pages/docs/[[...page]].mdx` - example source shown below: -```js -import order from 'data/docs-navigation.js' +```jsx import DocsPage from '@hashicorp/react-docs-page' +// Imports below are only used server-side import { generateStaticPaths, generateStaticProps, } from '@hashicorp/react-docs-page/server' -const productName = 'Vault' -const productSlug = 'vault' -// this example is at `pages/docs/[[slug]].mdx` - if the path is different -// this 'subpath' prop should be adjusted to match -const subpath = 'docs' +// Set up DocsPage settings +const BASE_ROUTE = 'docs' +const NAV_DATA = 'data/docs-nav-data.json' +const CONTENT_DIR = 'content/docs' +const PRODUCT = { + name: 'Packer', + slug: 'packer', +} function DocsLayout(props) { return ( - <DocsPage - productName={productName} - productSlug={productSlug} - subpath={subpath} - order={order} - showEditPage={true} - staticProps={props} - /> + <DocsPage baseRoute={BASE_ROUTE} product={PRODUCT} staticProps={props} /> ) } export async function getStaticPaths() { - return generateStaticPaths(subpath) + const paths = await generateStaticPaths(NAV_DATA, CONTENT_DIR) + return { paths, fallback: false } } export async function getStaticProps({ params }) { - return generateStaticProps({ subpath, productName, params }) + const props = await generateStaticProps(NAV_DATA, CONTENT_DIR, params) + return { props } } export default DocsLayout diff --git a/packages/docs-page/_temp-pr-notes.md b/packages/docs-page/_temp-pr-notes.md index eff271788..afd17cce7 100644 --- a/packages/docs-page/_temp-pr-notes.md +++ b/packages/docs-page/_temp-pr-notes.md @@ -4,23 +4,18 @@ To implement MKTG-032, changes have been made to both the `docs-page` and `doc-s ## Common changes -### Renamed props - - `subpath` prop renamed to `baseRoute` -- `pagePath` prop renamed to `currentPath` - -### Refactored props - -- `order` and `data` props replaced by `staticProps.navData` +- `pagePath` prop replaced by `staticProps.currentPath` +- `order` and `data` (aka `allPageData`) props replaced by `staticProps.navData` ## Changes to DocsPageWrapper -- `order` and `allPageData` props replaced by `staticProps.navData` +... ## Changes to DocsPage ---- +... ## Changes to DocsSidenav ---- +... From 1ad98f6104c0c8a3a3aa1065c08151fb46f7ff48 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 4 Mar 2021 13:58:20 -0500 Subject: [PATCH 27/62] Add comments, esp on migration script --- packages/docs-page/render-page-mdx.js | 6 +--- packages/docs-page/server.js | 19 +++++++---- ...ate-nav-data.js => migrate-to-mktg-032.js} | 34 ++++++++++++++++--- packages/docs-sidenav/bin/npx-hello-world.js | 3 -- packages/docs-sidenav/package.json | 3 +- 5 files changed, 43 insertions(+), 22 deletions(-) rename packages/docs-sidenav/bin/{migrate-nav-data.js => migrate-to-mktg-032.js} (78%) delete mode 100644 packages/docs-sidenav/bin/npx-hello-world.js diff --git a/packages/docs-page/render-page-mdx.js b/packages/docs-page/render-page-mdx.js index f57f77c0a..03c960dc0 100644 --- a/packages/docs-page/render-page-mdx.js +++ b/packages/docs-page/render-page-mdx.js @@ -25,11 +25,7 @@ async function renderPageMdx( components, scope, }) - - return { - mdxSource, - frontMatter, - } + return { mdxSource, frontMatter } } export default renderPageMdx diff --git a/packages/docs-page/server.js b/packages/docs-page/server.js index 2113afd90..6bd5c171b 100644 --- a/packages/docs-page/server.js +++ b/packages/docs-page/server.js @@ -4,6 +4,10 @@ import validateFilePaths from '@hashicorp/react-docs-sidenav/utils/validate-file import validateRouteStructure from '@hashicorp/react-docs-sidenav/utils/validate-route-structure' import renderPageMdx from './render-page-mdx' +// 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' async function generateStaticPaths( @@ -18,10 +22,6 @@ async function generateStaticPaths( } async function resolveNavData(filePath, localContentDir) { - // TODO - memo-ize? Will that affect things? Rationale is that NextJS - // must be calling this function twice for every page render... - // and input arguments and therefore return value will always be the same. - // So may be worth memo-izing. const navDataFile = path.join(process.cwd(), filePath) const navDataRaw = JSON.parse(fs.readFileSync(navDataFile, 'utf8')) const withFilePaths = await validateFilePaths(navDataRaw, localContentDir) @@ -63,9 +63,9 @@ async function generateStaticProps( const currentPath = params[paramId] ? params[paramId].join('/') : '' // Get the navNode that matches this path const navNode = getNodeFromPath(currentPath, navData, localContentDir) - // Set up the MDX content to re-hydrate client-side - const { filePath } = navNode - const mdxString = fs.readFileSync(path.join(process.cwd(), filePath), 'utf8') + // 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, additionalComponents, @@ -145,6 +145,11 @@ function getPathArraysFromNodes(navNodes) { return slugs } +// 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, diff --git a/packages/docs-sidenav/bin/migrate-nav-data.js b/packages/docs-sidenav/bin/migrate-to-mktg-032.js similarity index 78% rename from packages/docs-sidenav/bin/migrate-nav-data.js rename to packages/docs-sidenav/bin/migrate-to-mktg-032.js index 73660f454..71ea7a422 100644 --- a/packages/docs-sidenav/bin/migrate-nav-data.js +++ b/packages/docs-sidenav/bin/migrate-to-mktg-032.js @@ -1,18 +1,41 @@ #! /usr/bin/env node + const fs = require('fs') const path = require('path') const grayMatter = require('gray-matter') const klawSync = require('klaw-sync') /* + +MIGRATE TO MKTG-032 + +This utility is intended to automate migration from the `.js` navigation data format, +to the `.json` format proposed and approved in the MKTG-032 RFC. + +ref: https://docs.google.com/document/d/1kYvbyd6njHFSscoE1dtDNHQ3U8IzaMdcjOS0jg87rHg + USAGE -Eg: -- old nav data is in ./data/docs-navigation.js -- content is in ./content/docs -- output file is ./data/docs-nav-data.json +Using this command isn't *quite* as straightforward as we'd like, +we've had to make some compromises to the terseness of the command in order +to avoid installing migration-related packages into `@hashicorp/react-docs-sidenav`. + +Here's a full example command: + +``` +npx --package gray-matter --package klaw-sync --package @hashicorp/react-docs-sidenav@6.1.1-alpha.10 migrate-to-mktg-032 ./data/docs-navigation.js ./content/docs ./data/docs-nav-data.json +``` + +This a bit wordy - we can break this command down argument by argument: + +- `npx` - We're running the command with `npx`. The `bin` config in `@hashicorp/react-docs-sidenav` points to this file. +- `--package gray-matter` - we need `gray-matter` to run the command, but we don't need it in `docs-sidenav`. Using this argument lets us install `gray-matter` as we run the migration script, rather than having to make it a dependency of `docs-sidenav`. +- `--package klaw-sync` - as above, we need this dep, but don't want to install it in `docs-sidenav` +- `--package @hashicorp/react-docs-sidenav@6.1.1-alpha.10 migrate-to-mktg-032` - this final `--package` argument both installs the package, and acts as the package where `npx` expects to find the `migrate-to-mktg-032` script (this file!) +- `./data/docs-navigation.js` - the data source file, our previous `.js` format +- `./content/docs` - the content source file, we need to extract `sidebar_title` values from frontmatter. +- `./data/docs-nav-data.json` - the destination file for the converted format -npx --package gray-matter --package klaw-sync --package @hashicorp/react-docs-sidenav@6.1.1-alpha.10 migrate-nav-data ./data/docs-navigation.js ./content/docs ./data/docs-nav-data.json */ const navigationJsData = require(path.resolve(process.argv[2])) @@ -156,6 +179,7 @@ function convertNavLeaf(navNode, collectedFrontmatter, pathStack, subfolder) { if (!title) { throw new Error(`Could not find title in frontmatter of ${pathToMatch}.`) } + // TODO should remove all `sidebar_title` values from each `.mdx` file as part of this process // Return the new format for the nav leaf return { title: formatTitle(title), path: pathNewFormat } } diff --git a/packages/docs-sidenav/bin/npx-hello-world.js b/packages/docs-sidenav/bin/npx-hello-world.js deleted file mode 100644 index 593097b40..000000000 --- a/packages/docs-sidenav/bin/npx-hello-world.js +++ /dev/null @@ -1,3 +0,0 @@ -#! /usr/bin/env node -console.log(process.argv) -console.log('Hello world, executing from react-docs-subnav!') diff --git a/packages/docs-sidenav/package.json b/packages/docs-sidenav/package.json index d647ee07b..5000e8393 100644 --- a/packages/docs-sidenav/package.json +++ b/packages/docs-sidenav/package.json @@ -4,8 +4,7 @@ "version": "7.0.0", "author": "HashiCorp", "bin": { - "npx-hello-world": "bin/npx-hello-world.js", - "migrate-nav-data": "bin/migrate-nav-data.js" + "migrate-to-mktg-032": "bin/migrate-to-mktg-032.js" }, "contributors": [ "Jeff Escalante" From 62f36be936bbbc81b7264769fe16c68542c0ba72 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 4 Mar 2021 14:35:00 -0500 Subject: [PATCH 28/62] Implement highlighting of internal direct links --- packages/docs-page/props.js | 2 +- packages/docs-sidenav/fixtures/nav-data.json | 13 ++- packages/docs-sidenav/index.js | 86 ++++++++++++------- packages/docs-sidenav/package-lock.json | 5 ++ packages/docs-sidenav/package.json | 1 + packages/docs-sidenav/props.js | 2 +- .../utils/validate-file-paths/index.js | 10 +-- .../utils/validate-route-structure/index.js | 2 +- 8 files changed, 79 insertions(+), 42 deletions(-) diff --git a/packages/docs-page/props.js b/packages/docs-page/props.js index 646aee14a..aafe36c4e 100644 --- a/packages/docs-page/props.js +++ b/packages/docs-page/props.js @@ -6,7 +6,7 @@ module.exports = { type: 'string', required: true, description: - 'The `name` and `slug` of the product this page is being rendered for. Used for theming, page `<title />`, and the `Edit this page` link.', + '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', diff --git a/packages/docs-sidenav/fixtures/nav-data.json b/packages/docs-sidenav/fixtures/nav-data.json index e41024357..3aa224819 100644 --- a/packages/docs-sidenav/fixtures/nav-data.json +++ b/packages/docs-sidenav/fixtures/nav-data.json @@ -20,10 +20,6 @@ "title": "Overview", "path": "agent/autoauth/methods" }, - { - "title": "External Link", - "href": "https://google.com" - }, { "title": "AliCloud", "path": "agent/autoauth/methods/alicloud" @@ -32,6 +28,15 @@ { "title": "<code>GCP</code>", "path": "agent/autoauth/methods/gcp" + }, + { "divider": true }, + { + "title": "External Link", + "href": "https://google.com" + }, + { + "title": "Internal Direct Link", + "href": "/some-non-docs-path" } ] } diff --git a/packages/docs-sidenav/index.js b/packages/docs-sidenav/index.js index 325d389b3..1d484fcac 100644 --- a/packages/docs-sidenav/index.js +++ b/packages/docs-sidenav/index.js @@ -1,6 +1,8 @@ import React, { useEffect, useState } from 'react' import useProductMeta from '@hashicorp/nextjs-scripts/lib/providers/product-meta' import Link from 'next/link' +import { useRouter } from 'next/router' +import LinkWrap, { isAbsoluteURL } from '@hashicorp/react-link-wrap' import InlineSvg from '@hashicorp/react-inline-svg' import svgMenuIcon from './icons/menu.svg?include' import svgChevron from './icons/chevron.svg?include' @@ -16,23 +18,23 @@ export default function DocsSidenav({ navData, disableFilter = false, }) { + const router = useRouter() + const pathname = router ? router.pathname : null const [open, setOpen] = useState(false) const [filterInput, setFilterInput] = useState('') - const { themeClass } = useProductMeta(product) const [content, setContent] = useState(navData) const [filteredContent, setFilteredContent] = useState(navData) + const { themeClass } = useProductMeta(product) - // When currentPath changes, update content - // to ensure `__isActive` props on each content item - // are accurate and up-to-date - // (Note: we could also reset filter input here, - // if we don't want to filter input to persist - // across client-side navigation, with something like: + // When currentPath 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(addIsActiveToNodes(navData, currentPath)) - }, [currentPath, navData]) + setContent(addIsActiveToNodes(navData, currentPath, pathname)) + }, [currentPath, navData, pathname]) // When filter input changes, update content // to filter out items that don't match @@ -70,15 +72,21 @@ export default function DocsSidenav({ ) } -function addIsActiveToNodes(navNodes, currentPath) { - return navNodes.slice().map((node) => addIsActiveToNode(node, currentPath)) +function addIsActiveToNodes(navNodes, currentPath, pathname) { + return navNodes + .slice() + .map((node) => addIsActiveToNode(node, currentPath, pathname)) } -function addIsActiveToNode(navNode, currentPath) { +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) + const routesWithActive = addIsActiveToNodes( + navNode.routes, + currentPath, + pathname + ) const isActive = routesWithActive.filter((r) => r.__isActive).length > 0 return { ...navNode, routes: routesWithActive, __isActive: isActive } } @@ -88,10 +96,14 @@ function addIsActiveToNode(navNode, currentPath) { const isActive = navNode.path === currentPath return { ...navNode, __isActive: isActive } } - // Otherwise, return false - // (for dividers, external links, etc) - // TODO - do we need to worry about highlighting external links? yes probably, - // sometimes these are used not as "external" but to internal links outside the baseRoute + // If it's a direct link, + // return true if the path matches the router.pathname + if (navNode.href) { + const isActive = navNode.href === pathname + console.log({ href: navNode.href, pathname, isActive }) + return { ...navNode, __isActive: isActive } + } + // Otherwise, it's a divider, so return unmodified return navNode } @@ -116,7 +128,7 @@ function filterContent(content, searchValue) { }, []) } -function NavTree({ content, baseRoute, Link }) { +function NavTree({ baseRoute, content }) { return content.map((item, idx) => { // Dividers if (item.divider) { @@ -125,7 +137,14 @@ function NavTree({ content, baseRoute, Link }) { } // Direct links if (item.title && item.href) { - return <DirectLink key={item.title} title={item.title} href={item.href} /> + return ( + <DirectLink + key={item.title + item.href} + title={item.title} + href={item.href} + isActive={item.__isActive} + /> + ) } // Individual pages (leaf nodes) if (item.path) { @@ -135,7 +154,6 @@ function NavTree({ content, baseRoute, Link }) { title={item.title} isActive={item.__isActive} url={`/${baseRoute}/${item.path}`} - Link={Link} /> ) } @@ -149,13 +167,12 @@ function NavTree({ content, baseRoute, Link }) { isActive={item.__isActive} isFiltered={item.__isFiltered} baseRoute={baseRoute} - Link={Link} /> ) }) } -function NavLeaf({ title, url, Link, isActive }) { +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 ( <li> @@ -173,7 +190,7 @@ function NavLeaf({ title, url, Link, isActive }) { ) } -function NavBranch({ title, routes, baseRoute, isActive, isFiltered, Link }) { +function NavBranch({ title, routes, baseRoute, isActive, isFiltered }) { const [isOpen, setIsOpen] = useState(false) // Ensure categories appear open if they're active @@ -198,7 +215,7 @@ function NavBranch({ title, routes, baseRoute, isActive, isFiltered, Link }) { </button> <ul className={s.navBranchSubnav} data-is-open={isOpen}> - <NavTree baseRoute={baseRoute} content={routes} Link={Link} /> + <NavTree baseRoute={baseRoute} content={routes} /> </ul> </li> ) @@ -208,14 +225,25 @@ function Divider() { return <hr className={s.divider} /> } -function DirectLink({ title, href }) { +function DirectLink({ title, href, isActive }) { return ( <li> - <a className={s.navItem} href={href}> - <InlineSvg src={svgBullet} className={s.navLeafIcon} /> + <LinkWrap + className={s.navItem} + href={href} + Link={Link} + data-is-active={isActive} + > + <InlineSvg + src={svgBullet} + className={s.navLeafIcon} + data-is-active={isActive} + /> <span dangerouslySetInnerHTML={{ __html: title }} /> - <InlineSvg src={svgExternalLink} className={s.externalLinkIcon} /> - </a> + {isAbsoluteURL(href) ? ( + <InlineSvg src={svgExternalLink} className={s.externalLinkIcon} /> + ) : null} + </LinkWrap> </li> ) } diff --git a/packages/docs-sidenav/package-lock.json b/packages/docs-sidenav/package-lock.json index 935a9a04b..aee622add 100644 --- a/packages/docs-sidenav/package-lock.json +++ b/packages/docs-sidenav/package-lock.json @@ -4,6 +4,11 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@hashicorp/react-link-wrap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@hashicorp/react-link-wrap/-/react-link-wrap-2.0.2.tgz", + "integrity": "sha512-q8s2TTd9Uy3BSYyUe2TTr2Kbc0ViRc7XQga2fZI0bzlFqBTiMXtf6gh2cg3QvimHY42y4YtaO5C109V9ahMUpQ==" + }, "fuzzysearch": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/fuzzysearch/-/fuzzysearch-1.0.3.tgz", diff --git a/packages/docs-sidenav/package.json b/packages/docs-sidenav/package.json index 5000e8393..80a528917 100644 --- a/packages/docs-sidenav/package.json +++ b/packages/docs-sidenav/package.json @@ -10,6 +10,7 @@ "Jeff Escalante" ], "dependencies": { + "@hashicorp/react-link-wrap": "^2.0.2", "fuzzysearch": "1.0.3" }, "license": "MPL-2.0", diff --git a/packages/docs-sidenav/props.js b/packages/docs-sidenav/props.js index 1f77f32fd..6a2b72073 100644 --- a/packages/docs-sidenav/props.js +++ b/packages/docs-sidenav/props.js @@ -18,7 +18,7 @@ module.exports = { }, disableFilter: { type: 'boolean', - description: 'If true, disable the sidebar filter input', + description: 'If `true`, disable the sidebar filter input.', testValue: false, }, navData: { diff --git a/packages/docs-sidenav/utils/validate-file-paths/index.js b/packages/docs-sidenav/utils/validate-file-paths/index.js index 059e38ad9..51821d3b8 100644 --- a/packages/docs-sidenav/utils/validate-file-paths/index.js +++ b/packages/docs-sidenav/utils/validate-file-paths/index.js @@ -11,13 +11,11 @@ async function validateFilePaths(navNodes, localDir) { } async function validateNode(navNode, localDir) { - // Handle leaf nodes + // Ignore remote leaf nodes, these already + // have their content file explicitly defined + if (navNode.remoteFile) return navNode + // Handle local leaf nodes if (navNode.path) { - // Ignore remote leaf nodes, these already - // have their content file explicitly defined - // TODO is there a way we can avoid remoteFile nodes, without making this implementation have to know about them? - if (navNode.remoteFile) return navNode - // Handle local leaf nodes const indexFilePath = path.join(navNode.path, 'index.mdx') const namedFilePath = `${navNode.path}.mdx` const hasIndexFile = fs.existsSync( diff --git a/packages/docs-sidenav/utils/validate-route-structure/index.js b/packages/docs-sidenav/utils/validate-route-structure/index.js index af23f6bb9..bced5fb50 100644 --- a/packages/docs-sidenav/utils/validate-route-structure/index.js +++ b/packages/docs-sidenav/utils/validate-route-structure/index.js @@ -77,7 +77,7 @@ function validateBranchRoutes(navNodes, depth = 0) { function handleBranchNode(navNode, depth) { // We recurse depth-first here, and we'll throw an error - // if the nested routes had structural issues + // if any nested routes have structural issues const [path, routesWithStacks] = validateBranchRoutes( navNode.routes, depth + 1 From caed62f81503dc2703c588583bd4bc72ec20e5d4 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 4 Mar 2021 14:53:37 -0500 Subject: [PATCH 29/62] Add further static validation of nav-data --- .../utils/validate-route-structure/index.js | 68 ++++++++++++------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/packages/docs-sidenav/utils/validate-route-structure/index.js b/packages/docs-sidenav/utils/validate-route-structure/index.js index bced5fb50..429766fbd 100644 --- a/packages/docs-sidenav/utils/validate-route-structure/index.js +++ b/packages/docs-sidenav/utils/validate-route-structure/index.js @@ -10,10 +10,51 @@ function validateBranchRoutes(navNodes, depth = 0) { // Augment each navNode with its path __stack const navNodesWithStacks = navNodes.map((navNode) => { // Handle leaf nodes - split their paths into a __stack - if (navNode.path) return { ...navNode, __stack: navNode.path.split('/') } + if (navNode.path) { + if (!navNode.title) { + throw new Error( + `Missing nav-data title on NavLeaf. Please add a title to the node with the path value ${navNode.path}.` + ) + } + return { ...navNode, __stack: navNode.path.split('/') } + } // Handle branch nodes - we recurse depth-first here - if (navNode.routes) return handleBranchNode(navNode, depth) - // Other nodes aren't relevant, we don't touch them + 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 (navNode.href) { + if (!navNode.title) { + throw new Error( + `Missing nav-data title on NavDirectLink. Please add a title to the node with href ${navNode.href}.` + ) + } + } + // Handle unrecognized navNodes that have a title value, but nothing else + if (navNode.title) { + throw new Error( + `Missing nav-data title on unrecognized node. Please add an href, path, or routes to the node with title ${navNode.title}.` + ) + } + // 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 @@ -87,24 +128,3 @@ function handleBranchNode(navNode, depth) { } module.exports = validateRouteStructure - -// -// -// -// -// -// -// -// - -// TODO - throw error if any non-divider node does not have a title -// function collectEmptyTitleErrors(navData) { -// const errors = []; -// return errors; -// } - -// TODO - throw error if direct link nodes don't have both { title, href } -// function collectDirectLinkErrors(navData) { -// const errors = []; -// return errors; -// } From 45a6b73bfd4f3813dfb92582f2d2f8992a4369c1 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 4 Mar 2021 14:56:24 -0500 Subject: [PATCH 30/62] Add clarifying comment --- packages/docs-sidenav/utils/validate-file-paths/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/docs-sidenav/utils/validate-file-paths/index.js b/packages/docs-sidenav/utils/validate-file-paths/index.js index 51821d3b8..0c50d8fbf 100644 --- a/packages/docs-sidenav/utils/validate-file-paths/index.js +++ b/packages/docs-sidenav/utils/validate-file-paths/index.js @@ -13,6 +13,8 @@ async function validateFilePaths(navNodes, 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) { From 8d9b087d8c80bce418901cf9bfc2c4ae2d469aa2 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 4 Mar 2021 15:24:40 -0500 Subject: [PATCH 31/62] Remove console.log --- packages/docs-sidenav/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/docs-sidenav/index.js b/packages/docs-sidenav/index.js index 1d484fcac..bc2730373 100644 --- a/packages/docs-sidenav/index.js +++ b/packages/docs-sidenav/index.js @@ -100,7 +100,6 @@ function addIsActiveToNode(navNode, currentPath, pathname) { // return true if the path matches the router.pathname if (navNode.href) { const isActive = navNode.href === pathname - console.log({ href: navNode.href, pathname, isActive }) return { ...navNode, __isActive: isActive } } // Otherwise, it's a divider, so return unmodified From fb2d4b89e7cdb37df2caf0248d173c091a8b0637 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 4 Mar 2021 15:29:32 -0500 Subject: [PATCH 32/62] Bump docs-sidenav in docs-page --- packages/docs-page/package-lock.json | 12 +++++++++--- packages/docs-page/package.json | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/docs-page/package-lock.json b/packages/docs-page/package-lock.json index aef250741..da0dc2524 100644 --- a/packages/docs-page/package-lock.json +++ b/packages/docs-page/package-lock.json @@ -5,13 +5,19 @@ "requires": true, "dependencies": { "@hashicorp/react-docs-sidenav": { - "version": "6.1.1-alpha.16", - "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-sidenav/-/react-docs-sidenav-6.1.1-alpha.16.tgz", - "integrity": "sha512-RpPjNwMNe5L2LA1vvgp496CauVJ8wLnKge1lPBZKL5931jR1SFEMwuWLB8R6Pe2HmkIC55nPB/c43GrmPN4FFw==", + "version": "6.1.1-alpha.34", + "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-sidenav/-/react-docs-sidenav-6.1.1-alpha.34.tgz", + "integrity": "sha512-2f3cN70ri0LMgGKzivekeG9c8t4uwEplbT2NDKgQNAlDlsT51twtXoDxld3csp0Trt9/usKrSWipDIBmW2kpxg==", "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", diff --git a/packages/docs-page/package.json b/packages/docs-page/package.json index 5cbbce2b7..aaf90b37e 100644 --- a/packages/docs-page/package.json +++ b/packages/docs-page/package.json @@ -8,7 +8,7 @@ ], "dependencies": { "@hashicorp/react-content": "^6.2.2", - "@hashicorp/react-docs-sidenav": "6.1.1-alpha.16", + "@hashicorp/react-docs-sidenav": "6.1.1-alpha.34", "@hashicorp/react-head": "^1.2.0", "@hashicorp/react-search": "^4.1.0", "fs-exists-sync": "0.1.0", From 84cf7ef0cb170a020eeea0ed6c79e96a1c766652 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Thu, 4 Mar 2021 16:51:57 -0500 Subject: [PATCH 33/62] Plan out tests --- packages/docs-page/index.test.js | 2 +- packages/docs-sidenav/index.test.js | 19 ++++--------------- .../utils/validate-file-paths/index.test.js | 9 +++++++++ .../utils/validate-route-structure/index.js | 18 ++++++++++++++++-- .../validate-route-structure/index.test.js | 16 ++++++++++++++++ 5 files changed, 46 insertions(+), 18 deletions(-) create mode 100644 packages/docs-sidenav/utils/validate-file-paths/index.test.js create mode 100644 packages/docs-sidenav/utils/validate-route-structure/index.test.js diff --git a/packages/docs-page/index.test.js b/packages/docs-page/index.test.js index 1336a0c3e..c713bf8a5 100644 --- a/packages/docs-page/index.test.js +++ b/packages/docs-page/index.test.js @@ -2,7 +2,7 @@ test.todo( 'passes `title`, `description`, and `siteName` correctly to <HashiHead>' ) test.todo( - 'passes `product`, `category`, `currentPage`, `data`, and `order` correctly to <DocsSidenav>' + 'passes `product`, `baseRoute`, `currentPath`, and `navData` correctly to <DocsSidenav>' ) test.todo('passes `product` and `content` correctly to <Content>') test.todo('displays `showEditPage` as true by default') diff --git a/packages/docs-sidenav/index.test.js b/packages/docs-sidenav/index.test.js index e3d8930ea..bb9458c87 100644 --- a/packages/docs-sidenav/index.test.js +++ b/packages/docs-sidenav/index.test.js @@ -4,19 +4,8 @@ // import expectThrow from '../../__test-helpers/expect-throw' describe('<DocsSidenav />', () => { - it.todo('should render and display nesting levels correctly') - it.todo('should render accurately when the current page is an "overview"') - it.todo('should expand/collapse directory-level menu items when clicked') - it.todo( - 'should show/hide the menu when the "menu" button is clicked on mobile' - ) - it.todo('should error when a category is used with no content') - it.todo('should error when a page is not found within a category') - it.todo( - 'should error when a direct link does not use both "title" and "href"' - ) - it.todo('should error when a category contains no index file and no name') - it.todo( - 'should error when a category uses a name property and specifies an overview page' - ) + it.todo('renders and displays nesting levels correctly') + it.todo('renders accurately when the current page is an "overview"') + it.todo('expands and collapses nav branch items when clicked') + it.todo('shows and hides the mobile menu when the "menu" button is clicked') }) 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..29b5f492a --- /dev/null +++ b/packages/docs-sidenav/utils/validate-file-paths/index.test.js @@ -0,0 +1,9 @@ +// import validateFilePaths from './index.test.js' +// import expectThrow from '../../__test-helpers/expect-throw' + +describe('<DocsSidenav /> - validate-file-paths', () => { + it.todo('resolves the path for a named .mdx file') + it.todo('resolves the path for an index.mdx file') + it.todo('throws an error if there is a NavLeaf with a missing file') + it.todo('throws an error if there is a NavLeaf with an ambiguous file') +}) diff --git a/packages/docs-sidenav/utils/validate-route-structure/index.js b/packages/docs-sidenav/utils/validate-route-structure/index.js index 429766fbd..a51a7dabb 100644 --- a/packages/docs-sidenav/utils/validate-route-structure/index.js +++ b/packages/docs-sidenav/utils/validate-route-structure/index.js @@ -10,7 +10,14 @@ function validateBranchRoutes(navNodes, depth = 0) { // Augment each navNode with its path __stack const navNodesWithStacks = navNodes.map((navNode) => { // Handle leaf nodes - split their paths into a __stack - if (navNode.path) { + 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 on NavLeaf. Please add a title to the node with the path value ${navNode.path}.` @@ -31,7 +38,14 @@ function validateBranchRoutes(navNodes, depth = 0) { } // Handle direct link nodes, identifiable // by the presence of an href, to ensure they have a title - if (navNode.href) { + 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}.` 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..088df9371 --- /dev/null +++ b/packages/docs-sidenav/utils/validate-route-structure/index.test.js @@ -0,0 +1,16 @@ +// import validateRouteStructure from './index.test.js' +// import expectThrow from '../../__test-helpers/expect-throw' + +describe('<DocsSidenav /> - validate-file-paths', () => { + it.todo("throws an error if a NavLeaf's path is an empty string") + it.todo("throws an error if a NavLeaf's path is nested at the wrong depth") + it.todo('throws an error if a NavBranch has has an empty array of routes') + it.todo('throws an error if sibling routes have different parent routes') + it.todo("throws an error if a NavLeaf's path is nested at the wrong depth") + it.todo('throws an error if there are duplicate routes') + it.todo('throws an error if a NavLeaf has a missing title') + it.todo('throws an error if a NavBranch has a missing title') + it.todo('throws an error if a NavDirectLink has a missing title') + it.todo('throws an error if a NavDirectLink has an empty href') + it.todo('throws an error for unrecognized nodes') +}) From 42132c199d2a92c93516fe39e71bd366cdaaf380 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 5 Mar 2021 10:18:13 -0500 Subject: [PATCH 34/62] Update package-lock after fresh npm i & bootstrap --- packages/docs-sidenav/package-lock.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/docs-sidenav/package-lock.json b/packages/docs-sidenav/package-lock.json index aee622add..935a9a04b 100644 --- a/packages/docs-sidenav/package-lock.json +++ b/packages/docs-sidenav/package-lock.json @@ -4,11 +4,6 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "@hashicorp/react-link-wrap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@hashicorp/react-link-wrap/-/react-link-wrap-2.0.2.tgz", - "integrity": "sha512-q8s2TTd9Uy3BSYyUe2TTr2Kbc0ViRc7XQga2fZI0bzlFqBTiMXtf6gh2cg3QvimHY42y4YtaO5C109V9ahMUpQ==" - }, "fuzzysearch": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/fuzzysearch/-/fuzzysearch-1.0.3.tgz", From 30bb2f233cee26fa5c4b4b58ef06e205aaed7d45 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 5 Mar 2021 10:43:26 -0500 Subject: [PATCH 35/62] Add tests for validate-file-paths --- .../fixtures/content/agent/index.mdx | 173 ++++++++++++++++++ .../fixtures/content/ambiguous.mdx | 17 ++ .../fixtures/content/ambiguous/index.mdx | 17 ++ .../fixtures/content/what-is-vault.mdx | 72 ++++++++ packages/docs-sidenav/fixtures/nav-data.json | 4 + .../utils/validate-file-paths/index.js | 5 +- .../utils/validate-file-paths/index.test.js | 64 ++++++- 7 files changed, 343 insertions(+), 9 deletions(-) create mode 100644 packages/docs-sidenav/fixtures/content/agent/index.mdx create mode 100644 packages/docs-sidenav/fixtures/content/ambiguous.mdx create mode 100644 packages/docs-sidenav/fixtures/content/ambiguous/index.mdx create mode 100644 packages/docs-sidenav/fixtures/content/what-is-vault.mdx 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` <code>([vault][vault]: <optional\>)</code> - Specifies the remote Vault server the Agent connects to. + +- `auto_auth` <code>([auto_auth][autoauth]: <optional\>)</code> - Specifies the method and other options used for Auto-Auth functionality. + +- `cache` <code>([cache][caching]: <optional\>)</code> - Specifies options used for Caching functionality. + +- `listener` <code>([listener][listener]: <optional\>)</code> - 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` <code>([template][template]: <optional\>)</code> - 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: <optional>)` - 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: <optional>)` - 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: <optional>)` - 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: <optional>)` - 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: <optional>)` - 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: <optional>)` - 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: <optional>)` - 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 index 3aa224819..5b96be23e 100644 --- a/packages/docs-sidenav/fixtures/nav-data.json +++ b/packages/docs-sidenav/fixtures/nav-data.json @@ -1,4 +1,8 @@ [ + { + "title": "What is Vault?", + "path": "what-is-vault" + }, { "title": "Vault Agent", "routes": [ diff --git a/packages/docs-sidenav/utils/validate-file-paths/index.js b/packages/docs-sidenav/utils/validate-file-paths/index.js index 0c50d8fbf..05253290f 100644 --- a/packages/docs-sidenav/utils/validate-file-paths/index.js +++ b/packages/docs-sidenav/utils/validate-file-paths/index.js @@ -27,14 +27,13 @@ async function validateNode(navNode, localDir) { path.join(process.cwd(), localDir, namedFilePath) ) if (!hasIndexFile && !hasNamedFile) { - console.log(JSON.stringify(navNode, null, 2)) throw new Error( `Could not find file to match path "${navNode.path}". Neither "${namedFilePath}" or "${indexFilePath}" could be found.` ) } if (hasIndexFile && hasNamedFile) { - console.warn( - `Ambiguous path "${navNode.path}". Both "${namedFilePath}" and "${indexFilePath}" exist.` + throw new Error( + `Ambiguous path "${navNode.path}". Both "${namedFilePath}" and "${indexFilePath}" exist. Please delete one of these files.` ) } const filePath = path.join( diff --git a/packages/docs-sidenav/utils/validate-file-paths/index.test.js b/packages/docs-sidenav/utils/validate-file-paths/index.test.js index 29b5f492a..bee8fa0b5 100644 --- a/packages/docs-sidenav/utils/validate-file-paths/index.test.js +++ b/packages/docs-sidenav/utils/validate-file-paths/index.test.js @@ -1,9 +1,61 @@ -// import validateFilePaths from './index.test.js' -// import expectThrow from '../../__test-helpers/expect-throw' +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('<DocsSidenav /> - validate-file-paths', () => { - it.todo('resolves the path for a named .mdx file') - it.todo('resolves the path for an index.mdx file') - it.todo('throws an error if there is a NavLeaf with a missing file') - it.todo('throws an error if there is a NavLeaf with an ambiguous file') + 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.` + ) + }) }) From aacb8ff0eb4345794ca412d854ad17607d57b83f Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 5 Mar 2021 11:32:48 -0500 Subject: [PATCH 36/62] Add tests for validate-route-structure --- .../utils/validate-route-structure/index.js | 30 +-- .../validate-route-structure/index.test.js | 179 ++++++++++++++++-- 2 files changed, 182 insertions(+), 27 deletions(-) diff --git a/packages/docs-sidenav/utils/validate-route-structure/index.js b/packages/docs-sidenav/utils/validate-route-structure/index.js index a51a7dabb..785cf2108 100644 --- a/packages/docs-sidenav/utils/validate-route-structure/index.js +++ b/packages/docs-sidenav/utils/validate-route-structure/index.js @@ -5,7 +5,9 @@ function validateRouteStructure(navData) { 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. Depth: ${depth}`) + 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) => { @@ -20,7 +22,7 @@ function validateBranchRoutes(navNodes, depth = 0) { } if (!navNode.title) { throw new Error( - `Missing nav-data title on NavLeaf. Please add a title to the node with the path value ${navNode.path}.` + `Missing nav-data title. Please add a non-empty title to the node with the path "${navNode.path}".` ) } return { ...navNode, __stack: navNode.path.split('/') } @@ -31,7 +33,7 @@ function validateBranchRoutes(navNodes, depth = 0) { 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}.` + `Missing nav-data title on NavBranch. Please add a title to the node with the inferred path "${branchPath}".` ) } return nodeWithStacks @@ -48,23 +50,17 @@ function validateBranchRoutes(navNodes, depth = 0) { } if (!navNode.title) { throw new Error( - `Missing nav-data title on NavDirectLink. Please add a title to the node with href ${navNode.href}.` + `Missing nav-data title on NavDirectLink. Please add a title to the node with href "${navNode.href}".` ) } } - // Handle unrecognized navNodes that have a title value, but nothing else - if (navNode.title) { - throw new Error( - `Missing nav-data title on unrecognized node. Please add an href, path, or routes to the node with title ${navNode.title}.` - ) - } // 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, @@ -87,7 +83,7 @@ function validateBranchRoutes(navNodes, depth = 0) { }) if (duplicateRoutes.length > 0) { throw new Error( - `Duplicate routes found:\n\n${JSON.stringify(duplicateRoutes)}\n` + `Duplicate routes found for "${duplicateRoutes[0]}". Please resolve duplicates.` ) } // Gather an array of all resolved paths at this level @@ -111,7 +107,9 @@ function validateBranchRoutes(navNodes, depth = 0) { // 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('/')}"` + `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. @@ -123,7 +121,11 @@ function validateBranchRoutes(navNodes, depth = 0) { // 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: ${JSON.stringify(uniqueParents)}`) + throw new Error( + `Found mismatched paths at depth ${depth}: ${JSON.stringify( + uniqueParents + )}.` + ) } const path = uniqueParents[0] // Finally, we return diff --git a/packages/docs-sidenav/utils/validate-route-structure/index.test.js b/packages/docs-sidenav/utils/validate-route-structure/index.test.js index 088df9371..e99ed5263 100644 --- a/packages/docs-sidenav/utils/validate-route-structure/index.test.js +++ b/packages/docs-sidenav/utils/validate-route-structure/index.test.js @@ -1,16 +1,169 @@ -// import validateRouteStructure from './index.test.js' -// import expectThrow from '../../__test-helpers/expect-throw' +import validateRouteStructure from './' describe('<DocsSidenav /> - validate-file-paths', () => { - it.todo("throws an error if a NavLeaf's path is an empty string") - it.todo("throws an error if a NavLeaf's path is nested at the wrong depth") - it.todo('throws an error if a NavBranch has has an empty array of routes') - it.todo('throws an error if sibling routes have different parent routes') - it.todo("throws an error if a NavLeaf's path is nested at the wrong depth") - it.todo('throws an error if there are duplicate routes') - it.todo('throws an error if a NavLeaf has a missing title') - it.todo('throws an error if a NavBranch has a missing title') - it.todo('throws an error if a NavDirectLink has a missing title') - it.todo('throws an error if a NavDirectLink has an empty href') - it.todo('throws an error for unrecognized nodes') + 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) + }) }) From def68bc82324dcc9fefc5689fd034431031c0388 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 5 Mar 2021 15:32:35 -0500 Subject: [PATCH 37/62] Add tests for docs-sidenav --- packages/docs-sidenav/fixtures/nav-data.json | 8 ++ packages/docs-sidenav/index.js | 18 ++-- packages/docs-sidenav/index.test.js | 104 +++++++++++++++++-- packages/docs-sidenav/props.js | 2 +- packages/docs-sidenav/style.module.css | 6 +- 5 files changed, 123 insertions(+), 15 deletions(-) diff --git a/packages/docs-sidenav/fixtures/nav-data.json b/packages/docs-sidenav/fixtures/nav-data.json index 5b96be23e..3c8e2926e 100644 --- a/packages/docs-sidenav/fixtures/nav-data.json +++ b/packages/docs-sidenav/fixtures/nav-data.json @@ -17,6 +17,10 @@ "title": "Overview", "path": "agent/autoauth" }, + { + "title": "AWS Agent", + "path": "agent/autoauth/aws" + }, { "title": "Methods", "routes": [ @@ -28,6 +32,10 @@ "title": "AliCloud", "path": "agent/autoauth/methods/alicloud" }, + { + "title": "AWS", + "path": "agent/autoauth/methods/aws" + }, { "divider": true }, { "title": "<code>GCP</code>", diff --git a/packages/docs-sidenav/index.js b/packages/docs-sidenav/index.js index bc2730373..c44af36bd 100644 --- a/packages/docs-sidenav/index.js +++ b/packages/docs-sidenav/index.js @@ -20,7 +20,7 @@ export default function DocsSidenav({ }) { const router = useRouter() const pathname = router ? router.pathname : null - const [open, setOpen] = useState(false) + const [isMobileOpen, setIsMobileOpen] = useState(false) const [filterInput, setFilterInput] = useState('') const [content, setContent] = useState(navData) const [filteredContent, setFilteredContent] = useState(navData) @@ -43,14 +43,20 @@ export default function DocsSidenav({ }, [filterInput, content]) return ( - <div className={`${s.root} ${themeClass || ''}`}> - <div className={s.toggle} onClick={() => setOpen(!open)}> + <div className={`g-docs-sidenav ${s.root} ${themeClass || ''}`}> + <button + className={s.mobileMenuToggle} + onClick={() => setIsMobileOpen(!isMobileOpen)} + > <span> <InlineSvg src={svgMenuIcon} /> Documentation Menu </span> - </div> - <ul className={s.rootList} data-open={open}> - <div className={s.mobileClose} onClick={() => setOpen(!open)}> + </button> + <ul className={s.rootList} data-is-mobile-open={isMobileOpen}> + <div + className={s.mobileClose} + onClick={() => setIsMobileOpen(!isMobileOpen)} + > × </div> {!disableFilter && ( diff --git a/packages/docs-sidenav/index.test.js b/packages/docs-sidenav/index.test.js index bb9458c87..158d2853a 100644 --- a/packages/docs-sidenav/index.test.js +++ b/packages/docs-sidenav/index.test.js @@ -1,11 +1,103 @@ // import 'regenerator-runtime/runtime' -// import { render, fireEvent, screen } from '@testing-library/react' -// import DocsSidenav from './' +import { render, fireEvent, screen } from '@testing-library/react' // import expectThrow from '../../__test-helpers/expect-throw' +import DocsSidenav from './' +import props from './props' +import { getTestValues } from 'swingset/testing' + +const defaultProps = getTestValues(props) describe('<DocsSidenav />', () => { - it.todo('renders and displays nesting levels correctly') - it.todo('renders accurately when the current page is an "overview"') - it.todo('expands and collapses nav branch items when clicked') - it.todo('shows and hides the mobile menu when the "menu" button is clicked') + it('renders a root element with a g-docs-sidenav className', () => { + const { container } = render(<DocsSidenav {...defaultProps} />) + expect(container.firstChild.className).toContain('g-docs-sidenav') + }) + + it('renders and displays nesting levels correctly', () => { + render(<DocsSidenav {...defaultProps} />) + // 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 branchOne = screen.getByText('Vault Agent').parentNode + expect(branchOne.nodeName).toBe('BUTTON') + expect(branchOne.getAttribute('data-is-active')).toBe('true') + expect(branchOne.getAttribute('data-is-open')).toBe('true') + + const branchTwo = screen.getByText('Auto-Auth').parentNode + expect(branchTwo.nodeName).toBe('BUTTON') + expect(branchTwo.getAttribute('data-is-active')).toBe('true') + expect(branchTwo.getAttribute('data-is-open')).toBe('true') + + const branchThree = screen.getByText('Methods').parentNode + expect(branchThree.nodeName).toBe('BUTTON') + expect(branchThree.getAttribute('data-is-active')).toBe('true') + expect(branchThree.getAttribute('data-is-open')).toBe('true') + + const activeLeaf = screen.getByText('AWS').parentNode + expect(activeLeaf.nodeName).toBe('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 similarly named page at a different level + const inactiveLeafOne = screen.getByText('AWS Agent').parentNode + expect(inactiveLeafOne.nodeName).toBe('A') + expect(inactiveLeafOne.getAttribute('data-is-active')).toBe('false') + // Next we check a page at the same level but with a different name + const inactiveLeafTwo = screen.getByText('GCP').parentNode.parentNode + expect(inactiveLeafTwo.nodeName).toBe('A') + expect(inactiveLeafTwo.getAttribute('data-is-active')).toBe('false') + // Finally we check the overview page at the same level + const inactiveLeafThree = screen.getAllByText('Overview').filter((node) => { + const href = node.parentNode.getAttribute('href') + return href === '/docs/agent/autoauth/methods' + })[0].parentNode + expect(inactiveLeafThree.getAttribute('data-is-active')).toBe('false') + }) + + it('renders accurately when the current page is an "overview"', () => { + const currentPath = 'agent/autoauth/methods' + const expectedHref = `/docs/${currentPath}` + render(<DocsSidenav {...defaultProps} currentPath={currentPath} />) + // Check the "overview" index node we've set as active using currentPath + const activeIndexLeaf = screen.getAllByText('Overview').filter((node) => { + return node.parentNode.getAttribute('href') === expectedHref + })[0].parentNode + expect(activeIndexLeaf.getAttribute('data-is-active')).toBe('true') + }) + + it('expands and collapses nav branch items when clicked', () => { + render(<DocsSidenav {...defaultProps} />) + // Ensure the element exists, and is currently open + const branchTwo = screen.getByText('Auto-Auth').parentNode + expect(branchTwo.nodeName).toBe('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('shows and hides the mobile menu when the "menu" button is clicked', () => { + render(<DocsSidenav {...defaultProps} />) + + // 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').parentNode + expect(mobileMenuToggle.nodeName).toBe('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/props.js b/packages/docs-sidenav/props.js index 6a2b72073..d0c47729f 100644 --- a/packages/docs-sidenav/props.js +++ b/packages/docs-sidenav/props.js @@ -7,7 +7,7 @@ module.exports = { type: 'string', description: 'Path to the current page, relative to the `baseRoute`. Used to highlight the current page.', - testValue: 'agent/autoauth', + testValue: 'agent/autoauth/methods/aws', }, baseRoute: { type: 'string', diff --git a/packages/docs-sidenav/style.module.css b/packages/docs-sidenav/style.module.css index ae79220fe..f4acac48e 100644 --- a/packages/docs-sidenav/style.module.css +++ b/packages/docs-sidenav/style.module.css @@ -27,7 +27,7 @@ transition-property: box-shadow, transform; top: 0; - &[data-open='true'] { + &[data-is-mobile-open='true'] { box-shadow: 2px 2px 20px rgba(37, 38, 45, 0.2); transform: translateX(100%); padding-left: 25px; @@ -56,15 +56,17 @@ } } -.toggle { +.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; justify-content: center; + line-height: inherit; left: 0; padding: 12px; transition-delay: 0.3s; /* waits for menu to close before adjusting z-index */ From e9ffac99e3584865c0b026b8617c3c59bcd51ab6 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 5 Mar 2021 15:32:48 -0500 Subject: [PATCH 38/62] Add reference file for docs-page --- packages/docs-page/_temp-reference-index-test.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 packages/docs-page/_temp-reference-index-test.js diff --git a/packages/docs-page/_temp-reference-index-test.js b/packages/docs-page/_temp-reference-index-test.js new file mode 100644 index 000000000..1336a0c3e --- /dev/null +++ b/packages/docs-page/_temp-reference-index-test.js @@ -0,0 +1,16 @@ +test.todo( + 'passes `title`, `description`, and `siteName` correctly to <HashiHead>' +) +test.todo( + 'passes `product`, `category`, `currentPage`, `data`, and `order` correctly to <DocsSidenav>' +) +test.todo('passes `product` and `content` correctly to <Content>') +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' +) From e9e8a665e080144c0b146b16089eaab4da10b71f Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 5 Mar 2021 16:22:22 -0500 Subject: [PATCH 39/62] A11y improvements, and split out utilities for clarity - change mobile-menu-related divs to buttons so they're focusable - properly hide mobile-menu so links are NOT focusable when hidden --- packages/docs-sidenav/index.js | 153 ++++++++---------- packages/docs-sidenav/style.module.css | 18 ++- packages/docs-sidenav/utils/filter-content.js | 25 +++ .../docs-sidenav/utils/flag-active-nodes.js | 35 ++++ .../docs-sidenav/utils/use-event-listener.js | 30 ++++ 5 files changed, 171 insertions(+), 90 deletions(-) create mode 100644 packages/docs-sidenav/utils/filter-content.js create mode 100644 packages/docs-sidenav/utils/flag-active-nodes.js create mode 100644 packages/docs-sidenav/utils/use-event-listener.js diff --git a/packages/docs-sidenav/index.js b/packages/docs-sidenav/index.js index c44af36bd..194b3c309 100644 --- a/packages/docs-sidenav/index.js +++ b/packages/docs-sidenav/index.js @@ -1,14 +1,20 @@ -import React, { useEffect, useState } from 'react' -import useProductMeta from '@hashicorp/nextjs-scripts/lib/providers/product-meta' +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, { 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' -import fuzzysearch from 'fuzzysearch' +// styles import s from './style.module.css' export default function DocsSidenav({ @@ -20,20 +26,47 @@ export default function DocsSidenav({ }) { const router = useRouter() const pathname = router ? router.pathname : null - const [isMobileOpen, setIsMobileOpen] = useState(false) + + // Get theme class + // ( note: we could consider getting the product prop here, + // rather than requiring it to be passed in ) + const { themeClass } = useProductMeta(product) + + // Set up filtering state const [filterInput, setFilterInput] = useState('') const [content, setContent] = useState(navData) const [filteredContent, setFilteredContent] = useState(navData) - const { themeClass } = useProductMeta(product) - // When currentPath changes, update content to ensure + // 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(addIsActiveToNodes(navData, currentPath, pathname)) + setContent(flagActiveNodes(navData, currentPath, pathname)) }, [currentPath, navData, pathname]) // When filter input changes, update content @@ -52,13 +85,18 @@ export default function DocsSidenav({ <InlineSvg src={svgMenuIcon} /> Documentation Menu </span> </button> - <ul className={s.rootList} data-is-mobile-open={isMobileOpen}> - <div + <ul + className={s.rootList} + ref={menuRef} + data-is-mobile-hidden={!isMobileOpen && isMenuFullyHidden} + data-is-mobile-open={isMobileOpen} + > + <button className={s.mobileClose} onClick={() => setIsMobileOpen(!isMobileOpen)} > × - </div> + </button> {!disableFilter && ( <input className={s.filterInput} @@ -78,61 +116,6 @@ export default function DocsSidenav({ ) } -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 -} - -function filterContent(content, searchValue) { - // if there's no search searchValue we short-circuit and return everything - if (!searchValue) return 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 - }, []) -} - function NavTree({ baseRoute, content }) { return content.map((item, idx) => { // Dividers @@ -177,24 +160,6 @@ function NavTree({ baseRoute, content }) { }) } -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 ( - <li> - <Link href={url}> - <a className={s.navItem} data-is-active={isActive}> - <InlineSvg - src={svgBullet} - className={s.navLeafIcon} - data-is-active={isActive} - /> - <span dangerouslySetInnerHTML={{ __html: title }} /> - </a> - </Link> - </li> - ) -} - function NavBranch({ title, routes, baseRoute, isActive, isFiltered }) { const [isOpen, setIsOpen] = useState(false) @@ -226,8 +191,22 @@ function NavBranch({ title, routes, baseRoute, isActive, isFiltered }) { ) } -function Divider() { - return <hr className={s.divider} /> +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 ( + <li> + <Link href={url}> + <a className={s.navItem} data-is-active={isActive}> + <InlineSvg + src={svgBullet} + className={s.navLeafIcon} + data-is-active={isActive} + /> + <span dangerouslySetInnerHTML={{ __html: title }} /> + </a> + </Link> + </li> + ) } function DirectLink({ title, href, isActive }) { @@ -252,3 +231,7 @@ function DirectLink({ title, href, isActive }) { </li> ) } + +function Divider() { + return <hr className={s.divider} /> +} diff --git a/packages/docs-sidenav/style.module.css b/packages/docs-sidenav/style.module.css index f4acac48e..737c58879 100644 --- a/packages/docs-sidenav/style.module.css +++ b/packages/docs-sidenav/style.module.css @@ -32,6 +32,10 @@ transform: translateX(100%); padding-left: 25px; } + + &[data-is-mobile-hidden='true'] { + visibility: hidden; + } } } @@ -39,14 +43,17 @@ display: none; @media (--mobile-viewports) { + background: none; + border: none; + color: #333; + cursor: pointer; display: block; - position: absolute; - top: 18px; - right: 13px; font-size: 1.7em; + line-height: inherit; padding: 0 14px; - cursor: pointer; - color: #333; + position: absolute; + right: 13px; + top: 18px; transition: opacity 0.3s ease; z-index: 100; @@ -65,6 +72,7 @@ border-bottom: 1px solid var(--gray-6); cursor: pointer; display: none; + font-size: inherit; justify-content: center; line-height: inherit; left: 0; 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..292f6b564 --- /dev/null +++ b/packages/docs-sidenav/utils/use-event-listener.js @@ -0,0 +1,30 @@ +/* eslint-disable max-params */ +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 From 9014586f426821c5a0cffa33b7ecb16e135bcc1e Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 5 Mar 2021 18:08:27 -0500 Subject: [PATCH 40/62] Clean up comment --- packages/docs-sidenav/utils/use-event-listener.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/docs-sidenav/utils/use-event-listener.js b/packages/docs-sidenav/utils/use-event-listener.js index 292f6b564..20f0c15ad 100644 --- a/packages/docs-sidenav/utils/use-event-listener.js +++ b/packages/docs-sidenav/utils/use-event-listener.js @@ -1,4 +1,3 @@ -/* eslint-disable max-params */ import { useRef, useEffect } from 'react' const useEventListener = ( @@ -17,13 +16,10 @@ const useEventListener = ( 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) - } + return () => element.removeEventListener(eventName, eventListener, opts) }, [eventName, element, capture, passive, once]) } From 88965e38d5ca35bfc80c34fae135903193fa4029 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 5 Mar 2021 20:53:13 -0500 Subject: [PATCH 41/62] Add tests for docs-page --- package-lock.json | 6 + package.json | 1 + packages/docs-page/_temp-pr-notes.md | 6 + packages/docs-page/docs.mdx | 46 +----- packages/docs-page/index.test.js | 144 ++++++++++++++++-- packages/docs-page/props.js | 46 +++++- .../docs-page/temporary_jump-to-section.js | 2 +- 7 files changed, 189 insertions(+), 62 deletions(-) diff --git a/package-lock.json b/package-lock.json index 77b921667..962f4411d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11023,6 +11023,12 @@ "is-obj": "^1.0.0" } }, + "dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", + "dev": true + }, "download": { "version": "6.2.5", "resolved": "https://registry.npmjs.org/download/-/download-6.2.5.tgz", diff --git a/package.json b/package.json index f036265a4..6883749bd 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "devDependencies": { "@testing-library/jest-dom": "^5.11.6", "@testing-library/react": "^11.2.2", + "dotenv": "^8.2.0", "eslint": "^7.18.0", "husky": "^4.3.8", "identity-obj-proxy": "^3.0.0", diff --git a/packages/docs-page/_temp-pr-notes.md b/packages/docs-page/_temp-pr-notes.md index afd17cce7..4b9464893 100644 --- a/packages/docs-page/_temp-pr-notes.md +++ b/packages/docs-page/_temp-pr-notes.md @@ -2,6 +2,12 @@ To implement MKTG-032, changes have been made to both the `docs-page` and `doc-sidenav` components. +## Notes on tests + +- ran into difficulty testing jump-to-section +- Jest doesn't know what `innerText` is (line 13 of temporary_jump-to-section) +- Replace with `textContent` (?) We did a similar thing for `code-block`: https://app.asana.com/0/1199883977708219/1199877101801285/f ... but in this case we actually don't want to see the "permalink" text 🤔 EDIT: nvm it's fine, we were already using slice() to omit the >> char from the permalink text. + ## Common changes - `subpath` prop renamed to `baseRoute` diff --git a/packages/docs-page/docs.mdx b/packages/docs-page/docs.mdx index e8e788004..9d39ef7de 100644 --- a/packages/docs-page/docs.mdx +++ b/packages/docs-page/docs.mdx @@ -5,46 +5,9 @@ componentName: 'DocsPage' We have a lot of docs sites, all of which render content in exactly the same way. This component is a minimal abstraction of that rendering, and is intended to be used entirely as a template for all documentation content. <LiveComponent - components={{ - staticPropsResult: { - mdxSource: { - 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: - '<h1 className="g-type-display-2">Example Page</h1><p>This is a cool docs page!</p>', - scope: {}, - }, - navData: componentProps.staticProps.properties.navData.testValue, - frontMatter: { page_title: 'Test Page', description: 'test description' }, - currentPath: componentProps.staticProps.properties.currentPath.testValue, - }, - }} ->{`<DocsPage + components={{ staticPropsResult: componentProps.staticProps.testValue }} +> + {`<DocsPage product={{ name: 'Nomad', slug: 'nomad' }} baseRoute='docs' // pass in the results of executing the 'generateStaticProps' function from '@hashicorp/react-docs-page/server' @@ -52,7 +15,8 @@ We have a lot of docs sites, all of which render content in exactly the same way > <h1 className='g-type-display-2'>Example Page</h1> <p>This is a cool docs page!</p> -</DocsPage>`}</LiveComponent> +</DocsPage>`} +</LiveComponent> <UsageDetails packageJson={packageJson} /> diff --git a/packages/docs-page/index.test.js b/packages/docs-page/index.test.js index c713bf8a5..38460288e 100644 --- a/packages/docs-page/index.test.js +++ b/packages/docs-page/index.test.js @@ -1,16 +1,128 @@ -test.todo( - 'passes `title`, `description`, and `siteName` correctly to <HashiHead>' -) -test.todo( - 'passes `product`, `baseRoute`, `currentPath`, and `navData` correctly to <DocsSidenav>' -) -test.todo('passes `product` and `content` correctly to <Content>') -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 <HashiHead /> +jest.mock('next/head', () => { + return { + __esModule: true, + default: function HeadMock({ children }) { + return <>{children}</> + }, + } +}) + +describe('<DocsPage />', () => { + it('passes `title`, `description`, and `siteName` correctly to <HashiHead>', () => { + render(<DocsPage {...defaultProps} />) + // 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 <DocsSidenav>', () => { + render(<DocsPage {...defaultProps} />) + // 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').parentNode + expect(activeLeaf.nodeName).toBe('A') + // 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 <Content>', () => { + render(<DocsPage {...defaultProps} />) + // 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.parentNode.parentNode + expect(contentContainer.className).toContain('terraform') + }) + + it('displays `showEditPage` as true by default, and renders `mainBranch` in the link', () => { + render(<DocsPage {...defaultProps} mainBranch="master" />) + const expectedHref = + 'https://github.com/hashicorp/terraform/blob/master/website/content/docs/agent/autoauth/methods/aws' + const editPageLink = screen.getByText('Edit this page').parentNode + expect(editPageLink.getAttribute('href')).toBe(expectedHref) + }) + + it('if `showEditPage` is set to false, does not display', () => { + render(<DocsPage {...defaultProps} showEditPage={false} />) + const editPageLink = screen.queryByText('Edit this page') + expect(editPageLink).toBeNull() + }) + + it('passes `additionalComponents` to mdx remote for rendering if present', async () => { + function CustomComponent() { + return <strong>Text in custom component</strong> + } + const additionalComponents = { CustomComponent } + const { + mdxSource, + frontMatter, + } = await renderPageMdx( + "## Heading Two\n\nHere's a paragraph of content.\n\n<CustomComponent />", + { productName: 'Terraform', additionalComponents } + ) + render( + <DocsPage + {...defaultProps} + additionalComponents={additionalComponents} + staticProps={{ + currentPath: defaultProps.staticProps.currentPath, + mdxSource, + frontMatter, + navData: defaultProps.staticProps.navData, + }} + /> + ) + // 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( + <DocsPage + {...defaultProps} + staticProps={{ + currentPath: defaultProps.staticProps.currentPath, + mdxSource, + frontMatter, + navData: defaultProps.staticProps.navData, + }} + /> + ) + const jumpToSectionElem = screen.getByText('Jump to Section') + expect(jumpToSectionElem.tagName).toBe('SPAN') + }) +}) diff --git a/packages/docs-page/props.js b/packages/docs-page/props.js index aafe36c4e..3ca027881 100644 --- a/packages/docs-page/props.js +++ b/packages/docs-page/props.js @@ -16,12 +16,14 @@ module.exports = { }, slug: sharedProps.product, }, + testValue: { name: 'Terraform', slug: sharedProps.product.testValue }, }, baseRoute: { type: 'string', required: true, description: 'The path this page is rendering under, for example `"docs"` or `"api-docs"`. Passed directly to the `baseRoute` prop of `@hashicorp/react-docs-sidenav`. Also used for the `Edit this page` link.', + testValue: 'docs', }, mainBranch: { type: 'string', @@ -48,16 +50,48 @@ module.exports = { properties: { mdxSource: { type: 'object', - required: true, description: "Data returned from running `next-mdx-remote/render-to-string` on the page's isolated `.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: + '<h1 className="g-type-display-2">Example Page</h1><p>This is a cool docs page!</p>', + scope: {}, + }, }, - frontmatter: { + frontMatter: { type: 'object', required: true, description: "Frontmatter object parsed from the page's `.mdx` file.", properties: { - canonicalUrl: { + canonical_url: { type: 'string', description: 'Optional canonical URL. Passed directly to [@hashicorp/react-head](/?component=Head).', @@ -67,17 +101,21 @@ module.exports = { description: 'Used for the `<meta name="description" />`. Passed directly to [@hashicorp/react-head](/?component=Head).', required: true, + testValue: 'Test description', }, - pageTitle: { + page_title: { type: 'string', description: 'Used to construct the meta `<title />` 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/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 } }) From 30a45bfa9a35be4e752823f8adc245dffbf1a2d8 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 5 Mar 2021 22:12:32 -0500 Subject: [PATCH 42/62] Update DocsPage dep in GlossaryPage --- packages/docs-page/docs.mdx | 22 +- packages/docs-page/props.js | 2 +- packages/glossary-page/docs.mdx | 19 +- packages/glossary-page/index.js | 12 +- packages/glossary-page/package-lock.json | 492 +++++++++++++++++++++++ packages/glossary-page/package.json | 5 +- packages/glossary-page/props.js | 34 +- 7 files changed, 549 insertions(+), 37 deletions(-) diff --git a/packages/docs-page/docs.mdx b/packages/docs-page/docs.mdx index 9d39ef7de..7ca86e715 100644 --- a/packages/docs-page/docs.mdx +++ b/packages/docs-page/docs.mdx @@ -5,9 +5,22 @@ componentName: 'DocsPage' We have a lot of docs sites, all of which render content in exactly the same way. This component is a minimal abstraction of that rendering, and is intended to be used entirely as a template for all documentation content. <LiveComponent - components={{ staticPropsResult: componentProps.staticProps.testValue }} -> - {`<DocsPage + components={{ + staticPropsResult: { + mdxSource: componentProps.staticProps.properties.mdxSource.testValue, + frontMatter: { + page_title: + componentProps.staticProps.properties.frontMatter.properties + .page_title.testValue, + description: + componentProps.staticProps.properties.frontMatter.properties + .description.testValue, + }, + currentPath: componentProps.staticProps.properties.currentPath.testValue, + navData: componentProps.staticProps.properties.navData.testValue, + }, + }} +>{`<DocsPage product={{ name: 'Nomad', slug: 'nomad' }} baseRoute='docs' // pass in the results of executing the 'generateStaticProps' function from '@hashicorp/react-docs-page/server' @@ -15,8 +28,7 @@ We have a lot of docs sites, all of which render content in exactly the same way > <h1 className='g-type-display-2'>Example Page</h1> <p>This is a cool docs page!</p> -</DocsPage>`} -</LiveComponent> +</DocsPage>`}</LiveComponent> <UsageDetails packageJson={packageJson} /> diff --git a/packages/docs-page/props.js b/packages/docs-page/props.js index 3ca027881..b7935ab06 100644 --- a/packages/docs-page/props.js +++ b/packages/docs-page/props.js @@ -51,7 +51,7 @@ module.exports = { mdxSource: { type: 'object', description: - "Data returned from running `next-mdx-remote/render-to-string` on the page's isolated `.mdx` file contents.", + "Data returned from running `next-mdx-remote/render-to-string` on the page's `.mdx` file contents.", required: true, testValue: { compiledSource: 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 <LiveComponent components={{ staticPropsResult: { - content: { + mdxSource: { compiledSource: '"use strict";\n' + '\n' + @@ -150,25 +150,14 @@ bandwidth. `, }, ], - docsPageData: [ - { - __resourcePath: 'docs/test.mdx', - page_title: 'Testing Page', - sidebar_title: 'Testing Page', - }, - { - __resourcePath: 'docs/test2.mdx', - page_title: 'Other Testing Page', - sidebar_title: 'Other Testing Page', - }, - ], + navData: componentProps.staticProps.properties.navData.testValue, }, }} > - {`<GlossaryPage product={{ name: 'Consul', slug: 'consul' }} order={['test', 'test2']} staticProps={staticPropsResult} />`} + {`<GlossaryPage product={{ name: 'Consul', slug: 'consul' }} staticProps={staticPropsResult} />`} </LiveComponent> -<UsageDetails packageName="@hashicorp/react-glossary-page" /> +<UsageDetails packageJson={packageJson} /> ### Props diff --git a/packages/glossary-page/index.js b/packages/glossary-page/index.js index 0f8e9e246..4024be6bd 100644 --- a/packages/glossary-page/index.js +++ b/packages/glossary-page/index.js @@ -18,22 +18,20 @@ function GlossaryTableOfContents({ terms }) { export default function GlossaryPage({ additionalComponents, mainBranch, - order, product, showEditPage, - staticProps: { content, terms, docsPageData }, + staticProps: { mdxSource, terms, navData }, }) { return ( <DocsPageWrapper - allPageData={docsPageData} + navData={navData} description="Glossary" filePath="glossary" mainBranch={mainBranch} - order={order} - pagePath="/docs/glossary" + currentPath="glossary" pageTitle="Glossary" product={{ name: product.name, slug: product.slug }} - subpath="docs" + baseRoute="docs" showEditPage={showEditPage} > <> @@ -45,7 +43,7 @@ export default function GlossaryPage({ community. </p> <GlossaryTableOfContents terms={terms} /> - {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..a76fedcf1 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.5", + "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.8.5.tgz", + "integrity": "sha512-9rs/Yi82ilgifweJamOy4DlJ4xPGsCN/zg+RKy4vjytNhOrkEHLRQC8vPZ3OhD8KVlw9lRQIZTlgjgFl8iMeeA==", + "requires": { + "@algolia/cache-common": "4.8.5" + } + }, + "@algolia/cache-common": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.8.5.tgz", + "integrity": "sha512-4SvRWnagKtwBFAy8Rsfmv0/Uk53fZL+6dy2idwdx6SjMGKSs0y1Qv+thb4h/k/H5MONisAoT9C2rgZ/mqwh5yw==" + }, + "@algolia/cache-in-memory": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.8.5.tgz", + "integrity": "sha512-XBBfqs28FbjwLboY3sxvuzBgYsuXdFsj2mUvkgxfb0GVEzwW4I0NM7KzSPwT+iht55WS1PgIOnynjmhPsrubCw==", + "requires": { + "@algolia/cache-common": "4.8.5" + } + }, + "@algolia/client-account": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.8.5.tgz", + "integrity": "sha512-DjXMpeCdY4J4IDBfowiG6Xl9ec/FhG1NpPQM0Uv4xXsc/TeeZ1JgbgNDhWe9jW0jBEALy+a/RmPrZ0vsxcadsg==", + "requires": { + "@algolia/client-common": "4.8.5", + "@algolia/client-search": "4.8.5", + "@algolia/transporter": "4.8.5" + } + }, + "@algolia/client-analytics": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.8.5.tgz", + "integrity": "sha512-PQEY+chbHmZnRJdaWsvUYzDpEPr60az0EPUexdouvXGZId15/SnDaXjnf89F7tYmCzkHdUtG4bSvPzAupQ4AFA==", + "requires": { + "@algolia/client-common": "4.8.5", + "@algolia/client-search": "4.8.5", + "@algolia/requester-common": "4.8.5", + "@algolia/transporter": "4.8.5" + } + }, + "@algolia/client-common": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.8.5.tgz", + "integrity": "sha512-Dn8vog2VrGsJeOcBMcSAEIjBtPyogzUBGlh1DtVd0m8GN6q+cImCESl6DY846M2PTYWsLBKBksq37eUfSe9FxQ==", + "requires": { + "@algolia/requester-common": "4.8.5", + "@algolia/transporter": "4.8.5" + } + }, + "@algolia/client-recommendation": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/@algolia/client-recommendation/-/client-recommendation-4.8.5.tgz", + "integrity": "sha512-ffawCC1C25rCa8/JU2niRZgwr8aV9b2qsLVMo73GXFzi2lceXPAe9K68mt/BGHU+w7PFUwVHsV2VmB+G/HQRVw==", + "requires": { + "@algolia/client-common": "4.8.5", + "@algolia/requester-common": "4.8.5", + "@algolia/transporter": "4.8.5" + } + }, + "@algolia/client-search": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.8.5.tgz", + "integrity": "sha512-Ru2MljGZWrSQ0CVsDla11oGEPL/RinmVkLJfBtQ+/pk1868VfpAQFGKtOS/b8/xLrMA0Vm4EfC3Mgclk/p3KJA==", + "requires": { + "@algolia/client-common": "4.8.5", + "@algolia/requester-common": "4.8.5", + "@algolia/transporter": "4.8.5" + } + }, + "@algolia/logger-common": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.8.5.tgz", + "integrity": "sha512-PS6NS6bpED0rAxgCPGhjZJg9why0PnoVEE7ZoCbPq6lsAOc6FPlQLri4OiLyU7wx8RWDoVtOadyzulqAAsfPSQ==" + }, + "@algolia/logger-console": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.8.5.tgz", + "integrity": "sha512-3+4gLSbwzuGmrb5go3IZNcFIYVMSbB4c8UMtWEJ/gDBtgGZIvT6f/KlvVSOHIhthSxaM3Y13V6Qile/SpGqc6A==", + "requires": { + "@algolia/logger-common": "4.8.5" + } + }, + "@algolia/requester-browser-xhr": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.8.5.tgz", + "integrity": "sha512-M/Gf2vv/fU4+CqDW+wok7HPpEcLym3NtDzU9zaPzGYI/9X7o36581oyfnzt2pNfsXSQVj5a2pZVUWC3Z4SO27w==", + "requires": { + "@algolia/requester-common": "4.8.5" + } + }, + "@algolia/requester-common": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.8.5.tgz", + "integrity": "sha512-OIhsdwIrJVAlVlP7cwlt+RoR5AmxAoTGrFokOY9imVmgqXUUljdKO/DjhRL8vwYGFEidZ9efIjAIQ2B3XOhT9A==" + }, + "@algolia/requester-node-http": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.8.5.tgz", + "integrity": "sha512-viHAjfo53A3VSE7Bb/nzgpSMZ3prPp2qti7Wg8w7qxhntppKp3Fln6t4Vp+BoPOqruLsj139xXhheAKeRcYa0w==", + "requires": { + "@algolia/requester-common": "4.8.5" + } + }, + "@algolia/transporter": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.8.5.tgz", + "integrity": "sha512-Rb3cMlh/GoJK0+g+49GNA3IvR/EXsDEBwpyM+FOotSwxgiGt1wGBHM0K2v0GHwIEcuww02pl6KMDVlilA+qh0g==", + "requires": { + "@algolia/cache-common": "4.8.5", + "@algolia/logger-common": "4.8.5", + "@algolia/requester-common": "4.8.5" + } + }, + "@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.31", + "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-page/-/react-docs-page-10.9.4-alpha.31.tgz", + "integrity": "sha512-Zm2NPlJ0B41afIabOza8Etkokh1gEYXwb9etv8NgxXvY+2uD+cfU51+/N+gVXREEHKnU3MgX/Hj6Qx/tHcSTtw==", + "requires": { + "@hashicorp/react-content": "^6.3.0", + "@hashicorp/react-docs-sidenav": "6.1.1-alpha.34", + "@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.34", + "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-sidenav/-/react-docs-sidenav-6.1.1-alpha.34.tgz", + "integrity": "sha512-2f3cN70ri0LMgGKzivekeG9c8t4uwEplbT2NDKgQNAlDlsT51twtXoDxld3csp0Trt9/usKrSWipDIBmW2kpxg==", + "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.5", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.8.5.tgz", + "integrity": "sha512-GjKjpeevpePEJYinGokASNtIkl1t5EseNMlqDNAc+sXE8+iyyeqTyiJsN7bwlRG2BIremuslE/NlwdEfUuBLJw==", + "requires": { + "@algolia/cache-browser-local-storage": "4.8.5", + "@algolia/cache-common": "4.8.5", + "@algolia/cache-in-memory": "4.8.5", + "@algolia/client-account": "4.8.5", + "@algolia/client-analytics": "4.8.5", + "@algolia/client-common": "4.8.5", + "@algolia/client-recommendation": "4.8.5", + "@algolia/client-search": "4.8.5", + "@algolia/logger-common": "4.8.5", + "@algolia/logger-console": "4.8.5", + "@algolia/requester-browser-xhr": "4.8.5", + "@algolia/requester-common": "4.8.5", + "@algolia/requester-node-http": "4.8.5", + "@algolia/transporter": "4.8.5" + } + }, + "algoliasearch-helper": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.4.4.tgz", + "integrity": "sha512-OjyVLjykaYKCMxxRMZNiwLp8CS310E0qAeIY2NaublcmLAh8/SL19+zYHp7XCLtMem2ZXwl3ywMiA32O9jszuw==", + "requires": { + "events": "^1.1.1" + } + }, + "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..3f0ed6106 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.31", "@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..620f2cec8 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: @@ -51,5 +48,32 @@ module.exports = { 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 `<GlossaryTableOfContents />`', + 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", + }, + }, + }, + ], + }, + mdxSource: docsPageProps.staticProps.properties.mdxSource, + navData: docsPageProps.staticProps.properties.navData, + }, }, } From 2955bb9f4cdc81ffda745f2f010954de18b0f06e Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 5 Mar 2021 22:16:59 -0500 Subject: [PATCH 43/62] Delete stale dev files --- packages/docs-page/_temp-pr-notes.md | 27 ------------------- .../docs-page/_temp-reference-index-test.js | 16 ----------- 2 files changed, 43 deletions(-) delete mode 100644 packages/docs-page/_temp-pr-notes.md delete mode 100644 packages/docs-page/_temp-reference-index-test.js diff --git a/packages/docs-page/_temp-pr-notes.md b/packages/docs-page/_temp-pr-notes.md deleted file mode 100644 index 4b9464893..000000000 --- a/packages/docs-page/_temp-pr-notes.md +++ /dev/null @@ -1,27 +0,0 @@ -# Changes to implement MKTG-032 - -To implement MKTG-032, changes have been made to both the `docs-page` and `doc-sidenav` components. - -## Notes on tests - -- ran into difficulty testing jump-to-section -- Jest doesn't know what `innerText` is (line 13 of temporary_jump-to-section) -- Replace with `textContent` (?) We did a similar thing for `code-block`: https://app.asana.com/0/1199883977708219/1199877101801285/f ... but in this case we actually don't want to see the "permalink" text 🤔 EDIT: nvm it's fine, we were already using slice() to omit the >> char from the permalink text. - -## Common changes - -- `subpath` prop renamed to `baseRoute` -- `pagePath` prop replaced by `staticProps.currentPath` -- `order` and `data` (aka `allPageData`) props replaced by `staticProps.navData` - -## Changes to DocsPageWrapper - -... - -## Changes to DocsPage - -... - -## Changes to DocsSidenav - -... diff --git a/packages/docs-page/_temp-reference-index-test.js b/packages/docs-page/_temp-reference-index-test.js deleted file mode 100644 index 1336a0c3e..000000000 --- a/packages/docs-page/_temp-reference-index-test.js +++ /dev/null @@ -1,16 +0,0 @@ -test.todo( - 'passes `title`, `description`, and `siteName` correctly to <HashiHead>' -) -test.todo( - 'passes `product`, `category`, `currentPage`, `data`, and `order` correctly to <DocsSidenav>' -) -test.todo('passes `product` and `content` correctly to <Content>') -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' -) From 9f50a4d061eb060f07a3953982f5c80cd5045479 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 5 Mar 2021 22:43:51 -0500 Subject: [PATCH 44/62] Replace mainBranch prop with githubFileUrl --- packages/docs-page/README.md | 7 ++++++- packages/docs-page/docs.mdx | 2 ++ packages/docs-page/index.js | 11 ++++------ packages/docs-page/index.test.js | 4 ++-- packages/docs-page/props.js | 14 ++++++------- packages/docs-page/server.js | 14 ++++++------- packages/glossary-page/index.js | 5 ++--- packages/glossary-page/props.js | 7 +------ packages/glossary-page/server.js | 35 ++++++++++++++++++++------------ 9 files changed, 52 insertions(+), 47 deletions(-) diff --git a/packages/docs-page/README.md b/packages/docs-page/README.md index c2886644a..c00c65375 100644 --- a/packages/docs-page/README.md +++ b/packages/docs-page/README.md @@ -35,7 +35,12 @@ export async function getStaticPaths() { } export async function getStaticProps({ params }) { - const props = await generateStaticProps(NAV_DATA, CONTENT_DIR, params) + const props = await generateStaticProps( + NAV_DATA, + CONTENT_DIR, + params, + PRODUCT + ) return { props } } diff --git a/packages/docs-page/docs.mdx b/packages/docs-page/docs.mdx index 7ca86e715..6503110df 100644 --- a/packages/docs-page/docs.mdx +++ b/packages/docs-page/docs.mdx @@ -16,6 +16,8 @@ We have a lot of docs sites, all of which render content in exactly the same way componentProps.staticProps.properties.frontMatter.properties .description.testValue, }, + githubFileUrl: + componentProps.staticProps.properties.githubFileUrl.testValue, currentPath: componentProps.staticProps.properties.currentPath.testValue, navData: componentProps.staticProps.properties.navData.testValue, }, diff --git a/packages/docs-page/index.js b/packages/docs-page/index.js index ba88932ea..eb4acb21c 100644 --- a/packages/docs-page/index.js +++ b/packages/docs-page/index.js @@ -17,7 +17,7 @@ export function DocsPageWrapper({ currentPath, pageTitle, baseRoute, - mainBranch = 'main', + githubFileUrl, product: { name, slug }, showEditPage = true, }) { @@ -70,9 +70,7 @@ export function DocsPageWrapper({ {/* if desired, show an "edit this page" link on the bottom right, linking to github */} {showEditPage && ( <div id="edit-this-page" className="g-container"> - <a - href={`https://github.com/hashicorp/${slug}/blob/${mainBranch}/website/content/${baseRoute}/${currentPath}`} - > + <a href={githubFileUrl}> <img src={require('./img/github-logo.svg')} alt="github logo" /> <span>Edit this page</span> </a> @@ -85,10 +83,9 @@ export function DocsPageWrapper({ export default function DocsPage({ product, baseRoute, - mainBranch, showEditPage = true, additionalComponents, - staticProps: { mdxSource, frontMatter, currentPath, navData }, + 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, { @@ -99,7 +96,7 @@ export default function DocsPage({ <DocsPageWrapper canonicalUrl={frontMatter.canonical_url} description={frontMatter.description} - mainBranch={mainBranch} + githubFileUrl={githubFileUrl} navData={navData} currentPath={currentPath} pageTitle={frontMatter.page_title} diff --git a/packages/docs-page/index.test.js b/packages/docs-page/index.test.js index 38460288e..de82c35d8 100644 --- a/packages/docs-page/index.test.js +++ b/packages/docs-page/index.test.js @@ -61,9 +61,9 @@ describe('<DocsPage />', () => { }) it('displays `showEditPage` as true by default, and renders `mainBranch` in the link', () => { - render(<DocsPage {...defaultProps} mainBranch="master" />) + render(<DocsPage {...defaultProps} />) const expectedHref = - 'https://github.com/hashicorp/terraform/blob/master/website/content/docs/agent/autoauth/methods/aws' + 'https://github.com/hashicorp/terraform/blob/main/website/content/docs/agent/autoauth/methods/aws.mdx' const editPageLink = screen.getByText('Edit this page').parentNode expect(editPageLink.getAttribute('href')).toBe(expectedHref) }) diff --git a/packages/docs-page/props.js b/packages/docs-page/props.js index b7935ab06..5598aa272 100644 --- a/packages/docs-page/props.js +++ b/packages/docs-page/props.js @@ -22,15 +22,9 @@ module.exports = { type: 'string', required: true, description: - 'The path this page is rendering under, for example `"docs"` or `"api-docs"`. Passed directly to the `baseRoute` prop of `@hashicorp/react-docs-sidenav`. Also used for the `Edit this page` link.', + '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', }, - mainBranch: { - type: 'string', - description: - 'The default branch of the project being documented, typically either "master" or "main". Used for the `Edit this page` link.', - default: 'main', - }, showEditPage: { type: 'boolean', description: @@ -48,6 +42,12 @@ module.exports = { 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: diff --git a/packages/docs-page/server.js b/packages/docs-page/server.js index 6bd5c171b..65d417af3 100644 --- a/packages/docs-page/server.js +++ b/packages/docs-page/server.js @@ -44,14 +44,10 @@ async function generateStaticProps( navDataFile, localContentDir, params, + product, { - // Note: productName is ultimately only passed to createEnterpriseAlert. - // we may be able to remove the need for this additional arg / option by - // leveraging recent work on our product-meta provider: - // where arg is used: https://github.com/hashicorp/nextjs-scripts/blob/462eb2efa0c95ab5d81ad1b5d7896427a0263011/lib/providers/docs/index.jsx#L35-L48 - // product-meta: https://github.com/hashicorp/nextjs-scripts/tree/main/lib/providers/product-meta - productName, additionalComponents = {}, + mainBranch = 'main', remarkPlugins = [], scope, // optional, I think? paramId = DEFAULT_PARAM_ID, @@ -67,13 +63,15 @@ async function generateStaticProps( const mdxFile = path.join(process.cwd(), navNode.filePath) const mdxString = fs.readFileSync(mdxFile, 'utf8') const { mdxSource, frontMatter } = await renderPageMdx(mdxString, { - productName, + productName: product.name, additionalComponents, remarkPlugins, scope, }) + // Construct the githubFileUrl, used for "Edit this page" link + const githubFileUrl = `https://github.com/hashicorp/${product.slug}/blob/${mainBranch}/website/content/${navNode.filePath}` // Return all the props - return { currentPath, frontMatter, mdxSource, navData } + return { currentPath, frontMatter, githubFileUrl, mdxSource, navData } } async function validateNavData(navData, localContentDir) { diff --git a/packages/glossary-page/index.js b/packages/glossary-page/index.js index 4024be6bd..208153544 100644 --- a/packages/glossary-page/index.js +++ b/packages/glossary-page/index.js @@ -17,17 +17,16 @@ function GlossaryTableOfContents({ terms }) { export default function GlossaryPage({ additionalComponents, - mainBranch, product, showEditPage, - staticProps: { mdxSource, terms, navData }, + staticProps: { mdxSource, terms, navData, githubFileUrl }, }) { return ( <DocsPageWrapper navData={navData} description="Glossary" filePath="glossary" - mainBranch={mainBranch} + githubFileUrl={githubFileUrl} currentPath="glossary" pageTitle="Glossary" product={{ name: product.name, slug: product.slug }} diff --git a/packages/glossary-page/props.js b/packages/glossary-page/props.js index 620f2cec8..f1dbeaf70 100644 --- a/packages/glossary-page/props.js +++ b/packages/glossary-page/props.js @@ -38,12 +38,6 @@ 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: @@ -72,6 +66,7 @@ module.exports = { }, ], }, + 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..31b536702 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.mdx` + return { props: { terms, - content: await renderToString(mdxBlob, { - mdxOptions: markdownDefaults({ - resolveIncludes: path.join(process.cwd(), 'content/partials'), - }), - components: generateComponents(productName, additionalComponents), - }), - docsPageData, + mdxSource, + navData, + githubFileUrl, }, } } From 1a1d5d076b410713e003a5f5daaaaa0d2a33f38e Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 5 Mar 2021 23:00:25 -0500 Subject: [PATCH 45/62] Fix issue with githubFileUrl --- packages/docs-page/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docs-page/server.js b/packages/docs-page/server.js index 65d417af3..1922eeab5 100644 --- a/packages/docs-page/server.js +++ b/packages/docs-page/server.js @@ -69,7 +69,7 @@ async function generateStaticProps( scope, }) // Construct the githubFileUrl, used for "Edit this page" link - const githubFileUrl = `https://github.com/hashicorp/${product.slug}/blob/${mainBranch}/website/content/${navNode.filePath}` + const githubFileUrl = `https://github.com/hashicorp/${product.slug}/blob/${mainBranch}/website/${navNode.filePath}` // Return all the props return { currentPath, frontMatter, githubFileUrl, mdxSource, navData } } From 9c781fd6ba25f415eb9b8cee05433f2ea7dcc9d5 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 5 Mar 2021 23:06:27 -0500 Subject: [PATCH 46/62] Bump content dep, glossary-page dep --- packages/docs-page/package-lock.json | 5 +++++ packages/docs-page/package.json | 2 +- packages/glossary-page/package-lock.json | 6 +++--- packages/glossary-page/package.json | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/docs-page/package-lock.json b/packages/docs-page/package-lock.json index da0dc2524..22b506710 100644 --- a/packages/docs-page/package-lock.json +++ b/packages/docs-page/package-lock.json @@ -4,6 +4,11 @@ "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.34", "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-sidenav/-/react-docs-sidenav-6.1.1-alpha.34.tgz", diff --git a/packages/docs-page/package.json b/packages/docs-page/package.json index aaf90b37e..40e97b10b 100644 --- a/packages/docs-page/package.json +++ b/packages/docs-page/package.json @@ -7,7 +7,7 @@ "Jeff Escalante" ], "dependencies": { - "@hashicorp/react-content": "^6.2.2", + "@hashicorp/react-content": "^6.3.0", "@hashicorp/react-docs-sidenav": "6.1.1-alpha.34", "@hashicorp/react-head": "^1.2.0", "@hashicorp/react-search": "^4.1.0", diff --git a/packages/glossary-page/package-lock.json b/packages/glossary-page/package-lock.json index a76fedcf1..b43c402f4 100644 --- a/packages/glossary-page/package-lock.json +++ b/packages/glossary-page/package-lock.json @@ -133,9 +133,9 @@ "integrity": "sha512-B+QMlkMGryeNx3dGON4ExbzNvvll2ZXN3x+TkX80tUGClMI80MKjfSXiXIoVixlp22DMNG6wrnL42LC4WzZOxg==" }, "@hashicorp/react-docs-page": { - "version": "10.9.4-alpha.31", - "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-page/-/react-docs-page-10.9.4-alpha.31.tgz", - "integrity": "sha512-Zm2NPlJ0B41afIabOza8Etkokh1gEYXwb9etv8NgxXvY+2uD+cfU51+/N+gVXREEHKnU3MgX/Hj6Qx/tHcSTtw==", + "version": "10.9.4-alpha.44", + "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-page/-/react-docs-page-10.9.4-alpha.44.tgz", + "integrity": "sha512-RYDdWfnA6DrfSPN9kU7w0yFFL3Gk32dtoAyQFbKEC7CwczQJk7aSd5gEgcnRFUfl/QBoZynXoQVxLQIwUhaGYw==", "requires": { "@hashicorp/react-content": "^6.3.0", "@hashicorp/react-docs-sidenav": "6.1.1-alpha.34", diff --git a/packages/glossary-page/package.json b/packages/glossary-page/package.json index 3f0ed6106..8cd239b5d 100644 --- a/packages/glossary-page/package.json +++ b/packages/glossary-page/package.json @@ -7,7 +7,7 @@ "Bryce Kalow" ], "dependencies": { - "@hashicorp/react-docs-page": "^10.9.4-alpha.31", + "@hashicorp/react-docs-page": "^10.9.4-alpha.44", "@hashicorp/remark-plugins": "^3.1.1" }, "license": "MPL-2.0", From dcc0280a54593763522e7b156fa71d36fa34453e Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 5 Mar 2021 23:08:49 -0500 Subject: [PATCH 47/62] Revert useless changes to package.jsons --- packages/docs-page/package-lock.json | 2 +- packages/docs-page/package.json | 2 +- packages/docs-sidenav/package-lock.json | 2 +- packages/docs-sidenav/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/docs-page/package-lock.json b/packages/docs-page/package-lock.json index 22b506710..e99a051a8 100644 --- a/packages/docs-page/package-lock.json +++ b/packages/docs-page/package-lock.json @@ -1,6 +1,6 @@ { "name": "@hashicorp/react-docs-page", - "version": "11.0.0", + "version": "10.9.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/docs-page/package.json b/packages/docs-page/package.json index 40e97b10b..84eae2649 100644 --- a/packages/docs-page/package.json +++ b/packages/docs-page/package.json @@ -1,7 +1,7 @@ { "name": "@hashicorp/react-docs-page", "description": "Create a Hashicorp branded docs page in NextJS projects.", - "version": "11.0.0", + "version": "10.9.3", "author": "HashiCorp", "contributors": [ "Jeff Escalante" diff --git a/packages/docs-sidenav/package-lock.json b/packages/docs-sidenav/package-lock.json index 935a9a04b..c1ad4a81b 100644 --- a/packages/docs-sidenav/package-lock.json +++ b/packages/docs-sidenav/package-lock.json @@ -1,6 +1,6 @@ { "name": "@hashicorp/react-docs-sidenav", - "version": "7.0.0", + "version": "6.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/docs-sidenav/package.json b/packages/docs-sidenav/package.json index 80a528917..422063a30 100644 --- a/packages/docs-sidenav/package.json +++ b/packages/docs-sidenav/package.json @@ -1,7 +1,7 @@ { "name": "@hashicorp/react-docs-sidenav", "description": "Sidebar used for navigation HashiCorp docs", - "version": "7.0.0", + "version": "6.1.0", "author": "HashiCorp", "bin": { "migrate-to-mktg-032": "bin/migrate-to-mktg-032.js" From 38eb643ffca5f026d9f2a086ec10e58fe22995e7 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 5 Mar 2021 23:14:28 -0500 Subject: [PATCH 48/62] Remove unused imports --- packages/docs-sidenav/index.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/docs-sidenav/index.test.js b/packages/docs-sidenav/index.test.js index 158d2853a..f00e2f539 100644 --- a/packages/docs-sidenav/index.test.js +++ b/packages/docs-sidenav/index.test.js @@ -1,6 +1,4 @@ -// import 'regenerator-runtime/runtime' import { render, fireEvent, screen } from '@testing-library/react' -// import expectThrow from '../../__test-helpers/expect-throw' import DocsSidenav from './' import props from './props' import { getTestValues } from 'swingset/testing' From 1944c79ccd095a241d25bb6cca7bd730a0c47fbd Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 5 Mar 2021 23:36:56 -0500 Subject: [PATCH 49/62] Add delete fm script to docs-sidenav --- .../bin/delete-sidebar-title-frontmatter.js | 46 +++++++++++++++++++ packages/docs-sidenav/package.json | 1 + 2 files changed, 47 insertions(+) create mode 100644 packages/docs-sidenav/bin/delete-sidebar-title-frontmatter.js diff --git a/packages/docs-sidenav/bin/delete-sidebar-title-frontmatter.js b/packages/docs-sidenav/bin/delete-sidebar-title-frontmatter.js new file mode 100644 index 000000000..745ebb411 --- /dev/null +++ b/packages/docs-sidenav/bin/delete-sidebar-title-frontmatter.js @@ -0,0 +1,46 @@ +#! /usr/bin/env node + +const fs = require('fs') +const path = require('path') +const grayMatter = require('gray-matter') +const klawSync = require('klaw-sync') + +const CONTENT_DIR = path.join(process.cwd(), process.argv[3]) + +function fileFilter(f) { + return path.extname(f.path) === '.mdx' +} + +function processFrontmatter(frontmatter) { + const processed = { ...frontmatter } + delete processed.sidebar_title + return processed +} + +modifyFrontmatter(CONTENT_DIR, fileFilter, processFrontmatter).then( + (processedEntries) => { + console.log(processedEntries) + } +) + +async function modifyFrontmatter(inputDir, fileFilter, processFrontmatter) { + // Traverse directories and parse frontmatter + const targetFilepaths = klawSync(inputDir, { + traverseAll: true, + filter: fileFilter, + }).map((f) => f.path) + const withProcessedFrontmatter = await Promise.all( + targetFilepaths.map(async (filePath) => { + const rawFile = fs.readFileSync(filePath, 'utf-8') + const { data: frontmatter, content } = grayMatter(rawFile) + const processedFrontmatter = processFrontmatter(frontmatter) + const processedFile = grayMatter.stringify(content, processedFrontmatter) + fs.writeFileSync(filePath, processedFile) + return { + filePath, + after: processedFrontmatter, + } + }) + ) + return withProcessedFrontmatter +} diff --git a/packages/docs-sidenav/package.json b/packages/docs-sidenav/package.json index 422063a30..054e68120 100644 --- a/packages/docs-sidenav/package.json +++ b/packages/docs-sidenav/package.json @@ -4,6 +4,7 @@ "version": "6.1.0", "author": "HashiCorp", "bin": { + "delete-sidebar-title-frontmatter": "bin/delete-sidebar-title-frontmatter.js", "migrate-to-mktg-032": "bin/migrate-to-mktg-032.js" }, "contributors": [ From 6c09a1c23b1297b7e793fe18e71a1b16abad4095 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 5 Mar 2021 23:38:59 -0500 Subject: [PATCH 50/62] Fix issue with extra arg --- .../bin/delete-sidebar-title-frontmatter.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/docs-sidenav/bin/delete-sidebar-title-frontmatter.js b/packages/docs-sidenav/bin/delete-sidebar-title-frontmatter.js index 745ebb411..6f511947d 100644 --- a/packages/docs-sidenav/bin/delete-sidebar-title-frontmatter.js +++ b/packages/docs-sidenav/bin/delete-sidebar-title-frontmatter.js @@ -5,7 +5,19 @@ const path = require('path') const grayMatter = require('gray-matter') const klawSync = require('klaw-sync') -const CONTENT_DIR = path.join(process.cwd(), process.argv[3]) +/* + +DELETE sidebar_title FROM ALL .mdx FRONTMATTER + +USAGE + +``` +npx --package gray-matter --package klaw-sync --package @hashicorp/react-docs-sidenav@6.1.1-alpha.52 delete-sidebar-title-frontmatter ./content +``` + +*/ + +const CONTENT_DIR = path.join(process.cwd(), process.argv[2]) function fileFilter(f) { return path.extname(f.path) === '.mdx' From 4688cc96467dce0363da303a6226325acb66ce8f Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 5 Mar 2021 23:39:28 -0500 Subject: [PATCH 51/62] Update comment --- packages/docs-sidenav/bin/delete-sidebar-title-frontmatter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docs-sidenav/bin/delete-sidebar-title-frontmatter.js b/packages/docs-sidenav/bin/delete-sidebar-title-frontmatter.js index 6f511947d..8258a8113 100644 --- a/packages/docs-sidenav/bin/delete-sidebar-title-frontmatter.js +++ b/packages/docs-sidenav/bin/delete-sidebar-title-frontmatter.js @@ -12,7 +12,7 @@ DELETE sidebar_title FROM ALL .mdx FRONTMATTER USAGE ``` -npx --package gray-matter --package klaw-sync --package @hashicorp/react-docs-sidenav@6.1.1-alpha.52 delete-sidebar-title-frontmatter ./content +npx --package gray-matter --package klaw-sync --package @hashicorp/react-docs-sidenav@6.1.1-alpha.53 delete-sidebar-title-frontmatter ./content ``` */ From 9c9cbc6aba4d93aa2348dec392195b41698bba71 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 5 Mar 2021 23:41:01 -0500 Subject: [PATCH 52/62] Remote outdated comment --- packages/docs-sidenav/bin/migrate-to-mktg-032.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/docs-sidenav/bin/migrate-to-mktg-032.js b/packages/docs-sidenav/bin/migrate-to-mktg-032.js index 71ea7a422..de8375083 100644 --- a/packages/docs-sidenav/bin/migrate-to-mktg-032.js +++ b/packages/docs-sidenav/bin/migrate-to-mktg-032.js @@ -23,7 +23,7 @@ to avoid installing migration-related packages into `@hashicorp/react-docs-siden Here's a full example command: ``` -npx --package gray-matter --package klaw-sync --package @hashicorp/react-docs-sidenav@6.1.1-alpha.10 migrate-to-mktg-032 ./data/docs-navigation.js ./content/docs ./data/docs-nav-data.json +npx --package gray-matter --package klaw-sync --package @hashicorp/react-docs-sidenav@6.1.1-alpha.52 migrate-to-mktg-032 ./data/docs-navigation.js ./content/docs ./data/docs-nav-data.json ``` This a bit wordy - we can break this command down argument by argument: @@ -179,7 +179,6 @@ function convertNavLeaf(navNode, collectedFrontmatter, pathStack, subfolder) { if (!title) { throw new Error(`Could not find title in frontmatter of ${pathToMatch}.`) } - // TODO should remove all `sidebar_title` values from each `.mdx` file as part of this process // Return the new format for the nav leaf return { title: formatTitle(title), path: pathNewFormat } } From 132e9c92357f998fea415042a434c832daaf6c03 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 5 Mar 2021 23:43:57 -0500 Subject: [PATCH 53/62] Fix updated test value --- packages/docs-page/index.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docs-page/index.test.js b/packages/docs-page/index.test.js index de82c35d8..670002ddf 100644 --- a/packages/docs-page/index.test.js +++ b/packages/docs-page/index.test.js @@ -63,7 +63,7 @@ describe('<DocsPage />', () => { it('displays `showEditPage` as true by default, and renders `mainBranch` in the link', () => { render(<DocsPage {...defaultProps} />) const expectedHref = - 'https://github.com/hashicorp/terraform/blob/main/website/content/docs/agent/autoauth/methods/aws.mdx' + 'https://github.com/hashicorp/vault/blob/master/website/content/docs/agent/autoauth/methods/aws.mdx' const editPageLink = screen.getByText('Edit this page').parentNode expect(editPageLink.getAttribute('href')).toBe(expectedHref) }) From 0cd39d4f7bb6b991872592d360ea2f010d4ce1be Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 5 Mar 2021 23:50:44 -0500 Subject: [PATCH 54/62] Bump docs-sidenav in docs-page --- packages/docs-page/package-lock.json | 6 +++--- packages/docs-page/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/docs-page/package-lock.json b/packages/docs-page/package-lock.json index e99a051a8..173cbbbb2 100644 --- a/packages/docs-page/package-lock.json +++ b/packages/docs-page/package-lock.json @@ -10,9 +10,9 @@ "integrity": "sha512-B+QMlkMGryeNx3dGON4ExbzNvvll2ZXN3x+TkX80tUGClMI80MKjfSXiXIoVixlp22DMNG6wrnL42LC4WzZOxg==" }, "@hashicorp/react-docs-sidenav": { - "version": "6.1.1-alpha.34", - "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-sidenav/-/react-docs-sidenav-6.1.1-alpha.34.tgz", - "integrity": "sha512-2f3cN70ri0LMgGKzivekeG9c8t4uwEplbT2NDKgQNAlDlsT51twtXoDxld3csp0Trt9/usKrSWipDIBmW2kpxg==", + "version": "6.1.1-alpha.53", + "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-sidenav/-/react-docs-sidenav-6.1.1-alpha.53.tgz", + "integrity": "sha512-rM0WM8E+3fC2G6kXGWsEAarObzVTAVZDWh/aBPNqi2phK3PBPGF6Av0wvkaemt1C67HlEdqqBOiE5ZmkY1SwRA==", "requires": { "@hashicorp/react-link-wrap": "^2.0.2", "fuzzysearch": "1.0.3" diff --git a/packages/docs-page/package.json b/packages/docs-page/package.json index 84eae2649..26d55e80e 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.1-alpha.34", + "@hashicorp/react-docs-sidenav": "6.1.1-alpha.53", "@hashicorp/react-head": "^1.2.0", "@hashicorp/react-search": "^4.1.0", "fs-exists-sync": "0.1.0", From 96bd152b4db03f697c83ed07e795e96b0260e74f Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Fri, 5 Mar 2021 23:53:02 -0500 Subject: [PATCH 55/62] Bump docs-page in glossary-page --- packages/glossary-page/package-lock.json | 14 +++++++------- packages/glossary-page/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/glossary-page/package-lock.json b/packages/glossary-page/package-lock.json index b43c402f4..28c11e50a 100644 --- a/packages/glossary-page/package-lock.json +++ b/packages/glossary-page/package-lock.json @@ -133,12 +133,12 @@ "integrity": "sha512-B+QMlkMGryeNx3dGON4ExbzNvvll2ZXN3x+TkX80tUGClMI80MKjfSXiXIoVixlp22DMNG6wrnL42LC4WzZOxg==" }, "@hashicorp/react-docs-page": { - "version": "10.9.4-alpha.44", - "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-page/-/react-docs-page-10.9.4-alpha.44.tgz", - "integrity": "sha512-RYDdWfnA6DrfSPN9kU7w0yFFL3Gk32dtoAyQFbKEC7CwczQJk7aSd5gEgcnRFUfl/QBoZynXoQVxLQIwUhaGYw==", + "version": "10.9.4-alpha.53", + "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-page/-/react-docs-page-10.9.4-alpha.53.tgz", + "integrity": "sha512-a2P/zXeVRMN/l8Tw7Y5/YoHvmNSxijQsS6hTZLMf2tULGTsM0PQ24RAhnIoKiyXqZDfLuAEg4GZMH6rVKVetwA==", "requires": { "@hashicorp/react-content": "^6.3.0", - "@hashicorp/react-docs-sidenav": "6.1.1-alpha.34", + "@hashicorp/react-docs-sidenav": "6.1.1-alpha.53", "@hashicorp/react-head": "^1.2.0", "@hashicorp/react-search": "^4.1.0", "fs-exists-sync": "0.1.0", @@ -150,9 +150,9 @@ } }, "@hashicorp/react-docs-sidenav": { - "version": "6.1.1-alpha.34", - "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-sidenav/-/react-docs-sidenav-6.1.1-alpha.34.tgz", - "integrity": "sha512-2f3cN70ri0LMgGKzivekeG9c8t4uwEplbT2NDKgQNAlDlsT51twtXoDxld3csp0Trt9/usKrSWipDIBmW2kpxg==", + "version": "6.1.1-alpha.53", + "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-sidenav/-/react-docs-sidenav-6.1.1-alpha.53.tgz", + "integrity": "sha512-rM0WM8E+3fC2G6kXGWsEAarObzVTAVZDWh/aBPNqi2phK3PBPGF6Av0wvkaemt1C67HlEdqqBOiE5ZmkY1SwRA==", "requires": { "@hashicorp/react-link-wrap": "^2.0.2", "fuzzysearch": "1.0.3" diff --git a/packages/glossary-page/package.json b/packages/glossary-page/package.json index 8cd239b5d..899b52296 100644 --- a/packages/glossary-page/package.json +++ b/packages/glossary-page/package.json @@ -7,7 +7,7 @@ "Bryce Kalow" ], "dependencies": { - "@hashicorp/react-docs-page": "^10.9.4-alpha.44", + "@hashicorp/react-docs-page": "10.9.4-alpha.53", "@hashicorp/remark-plugins": "^3.1.1" }, "license": "MPL-2.0", From 661bbb370a22a3bd21401eb1101126ce627dc2dc Mon Sep 17 00:00:00 2001 From: Zachary Shilton <4624598+zchsh@users.noreply.github.com> Date: Sat, 6 Mar 2021 19:57:52 -0500 Subject: [PATCH 56/62] =?UTF-8?q?Fix=20glossary-page=20=E2=80=9CEdit=20thi?= =?UTF-8?q?s=20page=E2=80=9D=20link?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bryce Kalow <bkalow@hashicorp.com> --- packages/glossary-page/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/glossary-page/server.js b/packages/glossary-page/server.js index 31b536702..698ccc5c1 100644 --- a/packages/glossary-page/server.js +++ b/packages/glossary-page/server.js @@ -30,7 +30,7 @@ export default async function generateStaticProps({ }) // Construct the githubFileUrl, used for "Edit this page" link - const githubFileUrl = `https://github.com/hashicorp/${product.slug}/blob/${mainBranch}/website/content/glossary.mdx` + const githubFileUrl = `https://github.com/hashicorp/${product.slug}/blob/${mainBranch}/website/content/glossary` return { props: { From 8d8092c17109dbcb042085c8b6eac998700b7cd4 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Mon, 8 Mar 2021 14:39:10 -0500 Subject: [PATCH 57/62] Fix issue with navData direct link edge case, add related test --- .../utils/validate-route-structure/index.js | 10 ++++++- .../validate-route-structure/index.test.js | 27 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/docs-sidenav/utils/validate-route-structure/index.js b/packages/docs-sidenav/utils/validate-route-structure/index.js index 785cf2108..c19298069 100644 --- a/packages/docs-sidenav/utils/validate-route-structure/index.js +++ b/packages/docs-sidenav/utils/validate-route-structure/index.js @@ -53,6 +53,8 @@ function validateBranchRoutes(navNodes, depth = 0) { `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 @@ -127,6 +129,8 @@ function validateBranchRoutes(navNodes, depth = 0) { )}.` ) } + // 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] @@ -139,7 +143,11 @@ function handleBranchNode(navNode, depth) { navNode.routes, depth + 1 ) - const __stack = path.split('/') + // 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 } } diff --git a/packages/docs-sidenav/utils/validate-route-structure/index.test.js b/packages/docs-sidenav/utils/validate-route-structure/index.test.js index e99ed5263..292c7c89d 100644 --- a/packages/docs-sidenav/utils/validate-route-structure/index.test.js +++ b/packages/docs-sidenav/utils/validate-route-structure/index.test.js @@ -166,4 +166,31 @@ describe('<DocsSidenav /> - validate-file-paths', () => { )}.` 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() + }) }) From 36b794fa0778e1d4786608bad4f483fcf3644d30 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Mon, 8 Mar 2021 14:40:05 -0500 Subject: [PATCH 58/62] Bump docs-sidenav in docs-page --- packages/docs-page/package-lock.json | 6 +++--- packages/docs-page/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/docs-page/package-lock.json b/packages/docs-page/package-lock.json index 173cbbbb2..68c38a358 100644 --- a/packages/docs-page/package-lock.json +++ b/packages/docs-page/package-lock.json @@ -10,9 +10,9 @@ "integrity": "sha512-B+QMlkMGryeNx3dGON4ExbzNvvll2ZXN3x+TkX80tUGClMI80MKjfSXiXIoVixlp22DMNG6wrnL42LC4WzZOxg==" }, "@hashicorp/react-docs-sidenav": { - "version": "6.1.1-alpha.53", - "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-sidenav/-/react-docs-sidenav-6.1.1-alpha.53.tgz", - "integrity": "sha512-rM0WM8E+3fC2G6kXGWsEAarObzVTAVZDWh/aBPNqi2phK3PBPGF6Av0wvkaemt1C67HlEdqqBOiE5ZmkY1SwRA==", + "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" diff --git a/packages/docs-page/package.json b/packages/docs-page/package.json index 26d55e80e..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.1-alpha.53", + "@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", From d860d4d4fd28c78d9ec155d3fab6069deae5f978 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Mon, 8 Mar 2021 14:40:59 -0500 Subject: [PATCH 59/62] Bump docs-page in glossary-page --- packages/glossary-page/package-lock.json | 180 +++++++++++------------ packages/glossary-page/package.json | 2 +- 2 files changed, 91 insertions(+), 91 deletions(-) diff --git a/packages/glossary-page/package-lock.json b/packages/glossary-page/package-lock.json index 28c11e50a..44a2fb35f 100644 --- a/packages/glossary-page/package-lock.json +++ b/packages/glossary-page/package-lock.json @@ -5,118 +5,118 @@ "requires": true, "dependencies": { "@algolia/cache-browser-local-storage": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.8.5.tgz", - "integrity": "sha512-9rs/Yi82ilgifweJamOy4DlJ4xPGsCN/zg+RKy4vjytNhOrkEHLRQC8vPZ3OhD8KVlw9lRQIZTlgjgFl8iMeeA==", + "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.5" + "@algolia/cache-common": "4.8.6" } }, "@algolia/cache-common": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.8.5.tgz", - "integrity": "sha512-4SvRWnagKtwBFAy8Rsfmv0/Uk53fZL+6dy2idwdx6SjMGKSs0y1Qv+thb4h/k/H5MONisAoT9C2rgZ/mqwh5yw==" + "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.5", - "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.8.5.tgz", - "integrity": "sha512-XBBfqs28FbjwLboY3sxvuzBgYsuXdFsj2mUvkgxfb0GVEzwW4I0NM7KzSPwT+iht55WS1PgIOnynjmhPsrubCw==", + "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.5" + "@algolia/cache-common": "4.8.6" } }, "@algolia/client-account": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.8.5.tgz", - "integrity": "sha512-DjXMpeCdY4J4IDBfowiG6Xl9ec/FhG1NpPQM0Uv4xXsc/TeeZ1JgbgNDhWe9jW0jBEALy+a/RmPrZ0vsxcadsg==", + "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.5", - "@algolia/client-search": "4.8.5", - "@algolia/transporter": "4.8.5" + "@algolia/client-common": "4.8.6", + "@algolia/client-search": "4.8.6", + "@algolia/transporter": "4.8.6" } }, "@algolia/client-analytics": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.8.5.tgz", - "integrity": "sha512-PQEY+chbHmZnRJdaWsvUYzDpEPr60az0EPUexdouvXGZId15/SnDaXjnf89F7tYmCzkHdUtG4bSvPzAupQ4AFA==", + "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.5", - "@algolia/client-search": "4.8.5", - "@algolia/requester-common": "4.8.5", - "@algolia/transporter": "4.8.5" + "@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.5", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.8.5.tgz", - "integrity": "sha512-Dn8vog2VrGsJeOcBMcSAEIjBtPyogzUBGlh1DtVd0m8GN6q+cImCESl6DY846M2PTYWsLBKBksq37eUfSe9FxQ==", + "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.5", - "@algolia/transporter": "4.8.5" + "@algolia/requester-common": "4.8.6", + "@algolia/transporter": "4.8.6" } }, "@algolia/client-recommendation": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/client-recommendation/-/client-recommendation-4.8.5.tgz", - "integrity": "sha512-ffawCC1C25rCa8/JU2niRZgwr8aV9b2qsLVMo73GXFzi2lceXPAe9K68mt/BGHU+w7PFUwVHsV2VmB+G/HQRVw==", + "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.5", - "@algolia/requester-common": "4.8.5", - "@algolia/transporter": "4.8.5" + "@algolia/client-common": "4.8.6", + "@algolia/requester-common": "4.8.6", + "@algolia/transporter": "4.8.6" } }, "@algolia/client-search": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.8.5.tgz", - "integrity": "sha512-Ru2MljGZWrSQ0CVsDla11oGEPL/RinmVkLJfBtQ+/pk1868VfpAQFGKtOS/b8/xLrMA0Vm4EfC3Mgclk/p3KJA==", + "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.5", - "@algolia/requester-common": "4.8.5", - "@algolia/transporter": "4.8.5" + "@algolia/client-common": "4.8.6", + "@algolia/requester-common": "4.8.6", + "@algolia/transporter": "4.8.6" } }, "@algolia/logger-common": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.8.5.tgz", - "integrity": "sha512-PS6NS6bpED0rAxgCPGhjZJg9why0PnoVEE7ZoCbPq6lsAOc6FPlQLri4OiLyU7wx8RWDoVtOadyzulqAAsfPSQ==" + "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.5", - "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.8.5.tgz", - "integrity": "sha512-3+4gLSbwzuGmrb5go3IZNcFIYVMSbB4c8UMtWEJ/gDBtgGZIvT6f/KlvVSOHIhthSxaM3Y13V6Qile/SpGqc6A==", + "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.5" + "@algolia/logger-common": "4.8.6" } }, "@algolia/requester-browser-xhr": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.8.5.tgz", - "integrity": "sha512-M/Gf2vv/fU4+CqDW+wok7HPpEcLym3NtDzU9zaPzGYI/9X7o36581oyfnzt2pNfsXSQVj5a2pZVUWC3Z4SO27w==", + "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.5" + "@algolia/requester-common": "4.8.6" } }, "@algolia/requester-common": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.8.5.tgz", - "integrity": "sha512-OIhsdwIrJVAlVlP7cwlt+RoR5AmxAoTGrFokOY9imVmgqXUUljdKO/DjhRL8vwYGFEidZ9efIjAIQ2B3XOhT9A==" + "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.5", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.8.5.tgz", - "integrity": "sha512-viHAjfo53A3VSE7Bb/nzgpSMZ3prPp2qti7Wg8w7qxhntppKp3Fln6t4Vp+BoPOqruLsj139xXhheAKeRcYa0w==", + "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.5" + "@algolia/requester-common": "4.8.6" } }, "@algolia/transporter": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.8.5.tgz", - "integrity": "sha512-Rb3cMlh/GoJK0+g+49GNA3IvR/EXsDEBwpyM+FOotSwxgiGt1wGBHM0K2v0GHwIEcuww02pl6KMDVlilA+qh0g==", + "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.5", - "@algolia/logger-common": "4.8.5", - "@algolia/requester-common": "4.8.5" + "@algolia/cache-common": "4.8.6", + "@algolia/logger-common": "4.8.6", + "@algolia/requester-common": "4.8.6" } }, "@babel/runtime": { @@ -133,12 +133,12 @@ "integrity": "sha512-B+QMlkMGryeNx3dGON4ExbzNvvll2ZXN3x+TkX80tUGClMI80MKjfSXiXIoVixlp22DMNG6wrnL42LC4WzZOxg==" }, "@hashicorp/react-docs-page": { - "version": "10.9.4-alpha.53", - "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-page/-/react-docs-page-10.9.4-alpha.53.tgz", - "integrity": "sha512-a2P/zXeVRMN/l8Tw7Y5/YoHvmNSxijQsS6hTZLMf2tULGTsM0PQ24RAhnIoKiyXqZDfLuAEg4GZMH6rVKVetwA==", + "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.53", + "@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", @@ -150,9 +150,9 @@ } }, "@hashicorp/react-docs-sidenav": { - "version": "6.1.1-alpha.53", - "resolved": "https://registry.npmjs.org/@hashicorp/react-docs-sidenav/-/react-docs-sidenav-6.1.1-alpha.53.tgz", - "integrity": "sha512-rM0WM8E+3fC2G6kXGWsEAarObzVTAVZDWh/aBPNqi2phK3PBPGF6Av0wvkaemt1C67HlEdqqBOiE5ZmkY1SwRA==", + "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" @@ -218,24 +218,24 @@ "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==" }, "algoliasearch": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.8.5.tgz", - "integrity": "sha512-GjKjpeevpePEJYinGokASNtIkl1t5EseNMlqDNAc+sXE8+iyyeqTyiJsN7bwlRG2BIremuslE/NlwdEfUuBLJw==", - "requires": { - "@algolia/cache-browser-local-storage": "4.8.5", - "@algolia/cache-common": "4.8.5", - "@algolia/cache-in-memory": "4.8.5", - "@algolia/client-account": "4.8.5", - "@algolia/client-analytics": "4.8.5", - "@algolia/client-common": "4.8.5", - "@algolia/client-recommendation": "4.8.5", - "@algolia/client-search": "4.8.5", - "@algolia/logger-common": "4.8.5", - "@algolia/logger-console": "4.8.5", - "@algolia/requester-browser-xhr": "4.8.5", - "@algolia/requester-common": "4.8.5", - "@algolia/requester-node-http": "4.8.5", - "@algolia/transporter": "4.8.5" + "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": { diff --git a/packages/glossary-page/package.json b/packages/glossary-page/package.json index 899b52296..0e94cf7d8 100644 --- a/packages/glossary-page/package.json +++ b/packages/glossary-page/package.json @@ -7,7 +7,7 @@ "Bryce Kalow" ], "dependencies": { - "@hashicorp/react-docs-page": "10.9.4-alpha.53", + "@hashicorp/react-docs-page": "10.9.4-alpha.57", "@hashicorp/remark-plugins": "^3.1.1" }, "license": "MPL-2.0", From 01020344f99aeac143839220cacc6bb43551173b Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Mon, 8 Mar 2021 14:41:56 -0500 Subject: [PATCH 60/62] Add LICENSE to glossary-page --- packages/glossary-page/LICENSE | 373 +++++++++++++++++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 packages/glossary-page/LICENSE 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. From 9ab4d6207f187f8f35254e843ce42ce67fffa458 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Mon, 8 Mar 2021 15:47:32 -0500 Subject: [PATCH 61/62] Rm migration scripts from docs-sidenav, moving to mktg-codemods repo --- .../bin/delete-sidebar-title-frontmatter.js | 58 ----- .../docs-sidenav/bin/migrate-to-mktg-032.js | 205 ------------------ packages/docs-sidenav/package.json | 4 - 3 files changed, 267 deletions(-) delete mode 100644 packages/docs-sidenav/bin/delete-sidebar-title-frontmatter.js delete mode 100644 packages/docs-sidenav/bin/migrate-to-mktg-032.js diff --git a/packages/docs-sidenav/bin/delete-sidebar-title-frontmatter.js b/packages/docs-sidenav/bin/delete-sidebar-title-frontmatter.js deleted file mode 100644 index 8258a8113..000000000 --- a/packages/docs-sidenav/bin/delete-sidebar-title-frontmatter.js +++ /dev/null @@ -1,58 +0,0 @@ -#! /usr/bin/env node - -const fs = require('fs') -const path = require('path') -const grayMatter = require('gray-matter') -const klawSync = require('klaw-sync') - -/* - -DELETE sidebar_title FROM ALL .mdx FRONTMATTER - -USAGE - -``` -npx --package gray-matter --package klaw-sync --package @hashicorp/react-docs-sidenav@6.1.1-alpha.53 delete-sidebar-title-frontmatter ./content -``` - -*/ - -const CONTENT_DIR = path.join(process.cwd(), process.argv[2]) - -function fileFilter(f) { - return path.extname(f.path) === '.mdx' -} - -function processFrontmatter(frontmatter) { - const processed = { ...frontmatter } - delete processed.sidebar_title - return processed -} - -modifyFrontmatter(CONTENT_DIR, fileFilter, processFrontmatter).then( - (processedEntries) => { - console.log(processedEntries) - } -) - -async function modifyFrontmatter(inputDir, fileFilter, processFrontmatter) { - // Traverse directories and parse frontmatter - const targetFilepaths = klawSync(inputDir, { - traverseAll: true, - filter: fileFilter, - }).map((f) => f.path) - const withProcessedFrontmatter = await Promise.all( - targetFilepaths.map(async (filePath) => { - const rawFile = fs.readFileSync(filePath, 'utf-8') - const { data: frontmatter, content } = grayMatter(rawFile) - const processedFrontmatter = processFrontmatter(frontmatter) - const processedFile = grayMatter.stringify(content, processedFrontmatter) - fs.writeFileSync(filePath, processedFile) - return { - filePath, - after: processedFrontmatter, - } - }) - ) - return withProcessedFrontmatter -} diff --git a/packages/docs-sidenav/bin/migrate-to-mktg-032.js b/packages/docs-sidenav/bin/migrate-to-mktg-032.js deleted file mode 100644 index de8375083..000000000 --- a/packages/docs-sidenav/bin/migrate-to-mktg-032.js +++ /dev/null @@ -1,205 +0,0 @@ -#! /usr/bin/env node - -const fs = require('fs') -const path = require('path') -const grayMatter = require('gray-matter') -const klawSync = require('klaw-sync') - -/* - -MIGRATE TO MKTG-032 - -This utility is intended to automate migration from the `.js` navigation data format, -to the `.json` format proposed and approved in the MKTG-032 RFC. - -ref: https://docs.google.com/document/d/1kYvbyd6njHFSscoE1dtDNHQ3U8IzaMdcjOS0jg87rHg - -USAGE - -Using this command isn't *quite* as straightforward as we'd like, -we've had to make some compromises to the terseness of the command in order -to avoid installing migration-related packages into `@hashicorp/react-docs-sidenav`. - -Here's a full example command: - -``` -npx --package gray-matter --package klaw-sync --package @hashicorp/react-docs-sidenav@6.1.1-alpha.52 migrate-to-mktg-032 ./data/docs-navigation.js ./content/docs ./data/docs-nav-data.json -``` - -This a bit wordy - we can break this command down argument by argument: - -- `npx` - We're running the command with `npx`. The `bin` config in `@hashicorp/react-docs-sidenav` points to this file. -- `--package gray-matter` - we need `gray-matter` to run the command, but we don't need it in `docs-sidenav`. Using this argument lets us install `gray-matter` as we run the migration script, rather than having to make it a dependency of `docs-sidenav`. -- `--package klaw-sync` - as above, we need this dep, but don't want to install it in `docs-sidenav` -- `--package @hashicorp/react-docs-sidenav@6.1.1-alpha.10 migrate-to-mktg-032` - this final `--package` argument both installs the package, and acts as the package where `npx` expects to find the `migrate-to-mktg-032` script (this file!) -- `./data/docs-navigation.js` - the data source file, our previous `.js` format -- `./content/docs` - the content source file, we need to extract `sidebar_title` values from frontmatter. -- `./data/docs-nav-data.json` - the destination file for the converted format - -*/ - -const navigationJsData = require(path.resolve(process.argv[2])) -const CONTENT_DIR = path.join(process.cwd(), process.argv[3]) -const OUT_FILE = path.join(process.cwd(), process.argv[4]) - -runMigration(navigationJsData, CONTENT_DIR, OUT_FILE) - -async function runMigration(navigationJsData, contentDir, outFile) { - const { navData } = await getMigratedNavData(navigationJsData, contentDir) - fs.writeFileSync(outFile, JSON.stringify(navData, null, 2)) - // fs.writeFileSync(outFile, JSON.stringify(collectedFrontmatter, null, 2)) -} - -async function getMigratedNavData(navigationJsData, contentDir) { - const fileFilter = (f) => path.extname(f.path) === '.mdx' - const collectedFrontmatter = await collectFrontmatter(contentDir, fileFilter) - const navData = convertNavTree(navigationJsData, collectedFrontmatter, [], '') - return { navData, collectedFrontmatter } -} - -function convertNavTree(navTree, collectedFrontmatter, pathStack, subfolder) { - const convertedTree = navTree.map((navNode) => { - // if the node is a string like '-----', - // we want to render a divider - const isString = typeof navNode === 'string' - const isDivider = isString && navNode.match(/^-+$/) - if (isDivider) return { divider: true } - // if the node is a string, but not a divider, - // we want to render a leaf node - if (isString) - return convertNavLeaf(navNode, collectedFrontmatter, pathStack, subfolder) - // if the node has an `href` or `title`, it's a direct link - if (navNode.href || navNode.title) { - if (!navNode.href || !navNode.title) { - // if a direct link doesn't have both `href` and `title`, we throw an error - throw new Error( - `Direct sidebar links must have both a "href" and "title". Found a direct link with only one of the two:\n\n ${JSON.stringify( - navNode - )}` - ) - } - return { title: navNode.title, href: navNode.href } - } - // Otherwise, we expect the node to be a nested category - return convertNavCategory( - navNode, - collectedFrontmatter, - pathStack, - subfolder - ) - }) - return convertedTree -} - -function convertNavCategory( - navNode, - collectedFrontmatter, - pathStack, - subfolder -) { - // Throw an error if the category is invalid - if (!navNode.category || !navNode.content) { - throw new Error( - `Nav category nodes must have either a .name or .category property.` - ) - } - // Process the navNode's nested content into the new format - const nestedPathStack = pathStack.concat(navNode.category) - const nestedRoutes = convertNavTree( - navNode.content, - collectedFrontmatter, - nestedPathStack, - subfolder - ) - // Then, handle index data, which is a bit more of an undertaking... - // First, we try to gather index data for the entry - // The path we want for our new format does NOT contain - // the content subfolder or the file extension (always `.mdx`) - const USE_EXPLICIT_INDEX = false - const pathParts = USE_EXPLICIT_INDEX - ? [navNode.category, 'index'] - : [navNode.category] - const pathNewFormat = pathStack.concat(pathParts).join('/') - // The path we need to match from the older format - // includes the content subfolder as well as the file extension - const pathFromSubfolder = `${pathNewFormat}${ - USE_EXPLICIT_INDEX ? '' : '/index' - }.mdx` - const pathToMatch = path.join(subfolder, pathFromSubfolder) - // Try to find the corresponding index page resource - const matchedFrontmatter = collectedFrontmatter.filter((resource) => { - return resource.__resourcePath === pathToMatch - })[0] - const fmTitle = matchedFrontmatter - ? matchedFrontmatter.sidebar_title || matchedFrontmatter.page_title - : false - if (!fmTitle && !navNode.name) { - throw new Error( - `Nav category nodes must have either an index file with a sidebar_title or page_title in the frontmatter, or a .name property.` - ) - } - const title = fmTitle || navNode.name - // Set up an index page entry, if applicable - const indexRoute = matchedFrontmatter - ? { - title: 'Overview', - path: pathNewFormat, - } - : false - - // Finally, construct and return the category node - const routes = indexRoute ? [indexRoute, ...nestedRoutes] : nestedRoutes - return { - title: formatTitle(title), - routes, - } -} - -function convertNavLeaf(navNode, collectedFrontmatter, pathStack, subfolder) { - // The path we want for our new format does NOT contain - // the content subfolder or the file extension (always `.mdx`) - const pathNewFormat = pathStack.concat(navNode).join('/') - // The path we need to match from the older format - // includes the content subfolder as well as the file extension - const pathToMatch = path.join(subfolder, `${pathNewFormat}.mdx`) - // We filter for matching frontmatter to get the "title" for the nav leaf. - // We throw an error if there is no matching resource - there should be! - const matchedFrontmatter = collectedFrontmatter.filter((resource) => { - return resource.__resourcePath === pathToMatch - })[0] - if (!matchedFrontmatter) { - throw new Error( - `Could not find frontmatter for resource path ${pathToMatch}.` - ) - } - // We pull the title from frontmatter. - // We throw an error if there is no title in frontmatter - there should be! - const { sidebar_title, page_title } = matchedFrontmatter - const title = sidebar_title || page_title - if (!title) { - throw new Error(`Could not find title in frontmatter of ${pathToMatch}.`) - } - // Return the new format for the nav leaf - return { title: formatTitle(title), path: pathNewFormat } -} - -async function collectFrontmatter(inputDir, fileFilter) { - // Traverse directories and parse frontmatter - const targetFilepaths = klawSync(inputDir, { - traverseAll: true, - filter: fileFilter, - }).map((f) => f.path) - const collectedFrontmatter = await Promise.all( - targetFilepaths.map(async (filePath) => { - const rawFile = fs.readFileSync(filePath, 'utf-8') - const { data: frontmatter } = grayMatter(rawFile) - const __resourcePath = path.relative(inputDir, filePath) - return { __resourcePath, ...frontmatter } - }) - ) - return collectedFrontmatter -} - -function formatTitle(title) { - return title.replace(/<tt>/g, '<code>').replace(/<\/tt>/g, '</code>') -} diff --git a/packages/docs-sidenav/package.json b/packages/docs-sidenav/package.json index 054e68120..06eaba39e 100644 --- a/packages/docs-sidenav/package.json +++ b/packages/docs-sidenav/package.json @@ -3,10 +3,6 @@ "description": "Sidebar used for navigation HashiCorp docs", "version": "6.1.0", "author": "HashiCorp", - "bin": { - "delete-sidebar-title-frontmatter": "bin/delete-sidebar-title-frontmatter.js", - "migrate-to-mktg-032": "bin/migrate-to-mktg-032.js" - }, "contributors": [ "Jeff Escalante" ], From 89f85aea1f832525cb0caeb6cc4df1172a5ecfe8 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Mon, 8 Mar 2021 18:34:02 -0500 Subject: [PATCH 62/62] Update tests to avoid unstable parentNode selection --- packages/docs-page/index.test.js | 10 ++++--- packages/docs-sidenav/index.test.js | 45 ++++++++++++++--------------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/packages/docs-page/index.test.js b/packages/docs-page/index.test.js index 670002ddf..b59aaaac0 100644 --- a/packages/docs-page/index.test.js +++ b/packages/docs-page/index.test.js @@ -44,8 +44,10 @@ describe('<DocsPage />', () => { // 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').parentNode - expect(activeLeaf.nodeName).toBe('A') + 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') }) @@ -56,7 +58,7 @@ describe('<DocsPage />', () => { const contentParagraph = screen.getByText('This is a cool docs page!') expect(contentParagraph.tagName).toBe('P') // Confirm `product` is passed via class - const contentContainer = contentParagraph.parentNode.parentNode + const contentContainer = contentParagraph.closest('article') expect(contentContainer.className).toContain('terraform') }) @@ -64,7 +66,7 @@ describe('<DocsPage />', () => { render(<DocsPage {...defaultProps} />) const expectedHref = 'https://github.com/hashicorp/vault/blob/master/website/content/docs/agent/autoauth/methods/aws.mdx' - const editPageLink = screen.getByText('Edit this page').parentNode + const editPageLink = screen.getByText('Edit this page').closest('a') expect(editPageLink.getAttribute('href')).toBe(expectedHref) }) diff --git a/packages/docs-sidenav/index.test.js b/packages/docs-sidenav/index.test.js index f00e2f539..8dc044c7a 100644 --- a/packages/docs-sidenav/index.test.js +++ b/packages/docs-sidenav/index.test.js @@ -16,39 +16,35 @@ describe('<DocsSidenav />', () => { // 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 branchOne = screen.getByText('Vault Agent').parentNode - expect(branchOne.nodeName).toBe('BUTTON') + 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 branchTwo = screen.getByText('Auto-Auth').parentNode - expect(branchTwo.nodeName).toBe('BUTTON') + 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 branchThree = screen.getByText('Methods').parentNode - expect(branchThree.nodeName).toBe('BUTTON') + const branchThree = screen.getByText('Methods').closest('button') expect(branchThree.getAttribute('data-is-active')).toBe('true') expect(branchThree.getAttribute('data-is-open')).toBe('true') - const activeLeaf = screen.getByText('AWS').parentNode - expect(activeLeaf.nodeName).toBe('A') + 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 similarly named page at a different level - const inactiveLeafOne = screen.getByText('AWS Agent').parentNode - expect(inactiveLeafOne.nodeName).toBe('A') + 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 inactiveLeafTwo = screen.getByText('GCP').parentNode.parentNode - expect(inactiveLeafTwo.nodeName).toBe('A') + 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 inactiveLeafThree = screen.getAllByText('Overview').filter((node) => { - const href = node.parentNode.getAttribute('href') - return href === '/docs/agent/autoauth/methods' - })[0].parentNode + 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') }) @@ -57,17 +53,19 @@ describe('<DocsSidenav />', () => { const expectedHref = `/docs/${currentPath}` render(<DocsSidenav {...defaultProps} currentPath={currentPath} />) // Check the "overview" index node we've set as active using currentPath - const activeIndexLeaf = screen.getAllByText('Overview').filter((node) => { - return node.parentNode.getAttribute('href') === expectedHref - })[0].parentNode + 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('expands and collapses nav branch items when clicked', () => { render(<DocsSidenav {...defaultProps} />) // Ensure the element exists, and is currently open - const branchTwo = screen.getByText('Auto-Auth').parentNode - expect(branchTwo.nodeName).toBe('BUTTON') + 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 @@ -89,8 +87,9 @@ describe('<DocsSidenav />', () => { expect(sidebarNavList.nodeName).toBe('UL') expect(sidebarNavList.getAttribute('data-is-mobile-open')).toBe('false') // Get the menu button - const mobileMenuToggle = screen.getByText('Documentation Menu').parentNode - expect(mobileMenuToggle.nodeName).toBe('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')