Skip to content

Commit 2f71585

Browse files
authored
repo sync
2 parents f36f725 + 280ed99 commit 2f71585

4 files changed

Lines changed: 275 additions & 0 deletions

File tree

lib/create-tree.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
const fs = require('fs').promises
2+
const path = require('path')
3+
const Page = require('./page')
4+
const { sortBy } = require('lodash')
5+
const basePath = path.posix.join(__dirname, '..', 'content')
6+
7+
module.exports = async function createTree (originalPath, langObj) {
8+
// On recursive runs, this is processing page.children items in `/<link>` format.
9+
// If the path exists as is, assume this is a directory with a child index.md.
10+
// Otherwise, assume it's a child .md file and add `.md` to the path.
11+
let filepath
12+
try {
13+
await fs.access(originalPath)
14+
filepath = `${originalPath}/index.md`
15+
} catch {
16+
filepath = `${originalPath}.md`
17+
}
18+
19+
const relativePath = filepath.replace(`${basePath}/`, '')
20+
const localizedBasePath = path.posix.join(__dirname, '..', langObj.dir, 'content')
21+
22+
// Initialize the Page! This is where the file reads happen.
23+
let page = await Page.init({
24+
basePath: localizedBasePath,
25+
relativePath,
26+
languageCode: langObj.code
27+
})
28+
29+
if (!page) {
30+
// Do not throw an error if Early Access is not available.
31+
if (relativePath.startsWith('early-access')) return
32+
// If a translated path doesn't exist, fall back to the English so there is parity between
33+
// the English tree and the translated trees.
34+
if (langObj.code !== 'en') {
35+
page = await Page.init({
36+
basePath: basePath,
37+
relativePath,
38+
languageCode: langObj.code
39+
})
40+
}
41+
42+
if (!page) throw Error(`Cannot initialize page for ${filepath}`)
43+
}
44+
45+
// Create the root tree object on the first run, and create children recursively.
46+
const item = {
47+
page
48+
}
49+
50+
// Process frontmatter children recursively.
51+
if (item.page.children) {
52+
item.childPages = sortBy(
53+
(await Promise.all(item.page.children
54+
.map(async (child) => await createTree(path.posix.join(originalPath, child), langObj))))
55+
.filter(Boolean),
56+
// Sort by the ordered array of `children` in the frontmatter.
57+
item.page.children
58+
)
59+
}
60+
61+
return item
62+
}

lib/page-data.js

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
const path = require('path')
2+
const languages = require('./languages')
3+
const versions = Object.keys(require('./all-versions'))
4+
const createTree = require('./create-tree')
5+
const nonEnterpriseDefaultVersion = require('./non-enterprise-default-version')
6+
const englishPath = path.posix.join(__dirname, '..', 'content')
7+
8+
/**
9+
* We only need to initialize pages _once per language_ since pages don't change per version. So we do that
10+
* first since it's the most expensive work. This gets us a nested object with pages attached that we can use
11+
* as the basis for the siteTree after we do some versioning. We can also use it to derive the pageList.
12+
*/
13+
async function loadUnversionedTree () {
14+
const unversionedTree = {}
15+
16+
await Promise.all(Object.values(languages)
17+
.map(async (langObj) => {
18+
unversionedTree[langObj.code] = await createTree(englishPath, langObj)
19+
}))
20+
21+
return unversionedTree
22+
}
23+
24+
/**
25+
* The siteTree is a nested object with pages for every language and version, useful for nav because it
26+
* contains parent, child, and sibling relationships:
27+
*
28+
* siteTree[languageCode][version].childPages[<array of pages>].childPages[<array of pages>] (etc...)
29+
30+
* Given an unversioned tree of all pages per language, we can walk it for each version and do a couple operations:
31+
* 1. Add a versioned href to every item, where the href is the relevant permalink for the current version.
32+
* 2. Drop any child pages that are not available in the current version.
33+
*
34+
* Order of languages and versions doesn't matter, but order of child page arrays DOES matter (for navigation).
35+
*/
36+
async function loadSiteTree (unversionedTree) {
37+
const rawTree = Object.assign({}, (unversionedTree || await loadUnversionedTree()))
38+
const siteTree = {}
39+
40+
// For every language...
41+
await Promise.all(Object.keys(languages).map(async (langCode) => {
42+
const treePerVersion = {}
43+
// in every version...
44+
await Promise.all(versions.map(async (version) => {
45+
// "version" the pages.
46+
treePerVersion[version] = versionPages(Object.assign({}, rawTree[langCode]), version)
47+
}))
48+
49+
siteTree[langCode] = treePerVersion
50+
}))
51+
52+
return siteTree
53+
}
54+
55+
// This step can't be asynchronous because the order of child pages matters.
56+
function versionPages (obj, version) {
57+
// Add a versioned href as a convenience for use in layouts.
58+
obj.href = obj.page.permalinks
59+
.find(pl => pl.pageVersion === version || (pl.pageVersion === 'homepage' && version === nonEnterpriseDefaultVersion))
60+
.href
61+
62+
if (!obj.childPages) return obj
63+
64+
const versionedChildPages = obj.childPages
65+
// Drop child pages that do not apply to the current version.
66+
.filter(childPage => childPage.page.applicableVersions.includes(version))
67+
// Version the child pages recursively.
68+
.map(childPage => versionPages(Object.assign({}, childPage), version))
69+
70+
obj.childPages = [...versionedChildPages]
71+
72+
return obj
73+
}
74+
75+
// Derive a flat array of Page objects in all languages.
76+
async function loadPageList (unversionedTree) {
77+
const rawTree = unversionedTree || await loadUnversionedTree()
78+
const pageList = []
79+
80+
await Promise.all(Object.keys(languages).map(async (langCode) => {
81+
await addToCollection(rawTree[langCode], pageList)
82+
}))
83+
84+
async function addToCollection (item, collection) {
85+
if (!item.page) return
86+
collection.push(item.page)
87+
88+
if (!item.childPages) return
89+
await Promise.all(item.childPages.map(async (childPage) => await addToCollection(childPage, collection)))
90+
}
91+
92+
return pageList
93+
}
94+
95+
// Create an object from the list of all pages with permalinks as keys for fast lookup.
96+
function createMapFromArray (pageList) {
97+
const pageMap =
98+
pageList.reduce(
99+
(pageMap, page) => {
100+
for (const permalink of page.permalinks) {
101+
pageMap[permalink.href] = page
102+
}
103+
return pageMap
104+
},
105+
{}
106+
)
107+
108+
return pageMap
109+
}
110+
111+
async function loadPageMap (pageList) {
112+
const pages = pageList || await loadPageList()
113+
return createMapFromArray(pages)
114+
}
115+
116+
module.exports = {
117+
loadUnversionedTree,
118+
loadSiteTree,
119+
loadPages: loadPageList,
120+
loadPageMap
121+
}

lib/warm-server2.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
const statsd = require('./statsd')
2+
const { loadUnversionedTree, loadSiteTree, loadPages, loadPageMap } = require('./page-data')
3+
const loadRedirects = require('./redirects/precompile')
4+
const loadSiteData = require('./site-data')
5+
6+
// Instrument these functions so that
7+
// it's wrapped in a timer that reports to Datadog
8+
const dog = {
9+
loadUnversionedTree: statsd.asyncTimer(loadUnversionedTree, 'load_unversioned_tree'),
10+
loadSiteTree: statsd.asyncTimer(loadSiteTree, 'load_site_tree'),
11+
loadPages: statsd.asyncTimer(loadPages, 'load_pages'),
12+
loadPageMap: statsd.asyncTimer(loadPageMap, 'load_page_map'),
13+
loadRedirects: statsd.asyncTimer(loadRedirects, 'load_redirects'),
14+
loadSiteData: statsd.timer(loadSiteData, 'load_site_data')
15+
}
16+
17+
// For local caching
18+
let pageList, pageMap, site, redirects, unversionedTree, siteTree
19+
20+
function isFullyWarmed () {
21+
// NOTE: Yes, `pageList` is specifically excluded here as it is transient data
22+
const fullyWarmed = !!(pageMap && site && redirects && unversionedTree && siteTree)
23+
return fullyWarmed
24+
}
25+
26+
function getWarmedCache () {
27+
return {
28+
pages: pageMap,
29+
site,
30+
redirects,
31+
unversionedTree,
32+
siteTree
33+
}
34+
}
35+
36+
async function warmServer () {
37+
const startTime = Date.now()
38+
39+
if (process.env.NODE_ENV !== 'test') {
40+
console.log('Priming context information...')
41+
}
42+
43+
if (!unversionedTree) {
44+
unversionedTree = await dog.loadUnversionedTree()
45+
}
46+
47+
if (!siteTree) {
48+
siteTree = await dog.loadSiteTree(unversionedTree)
49+
}
50+
51+
if (!pageList) {
52+
pageList = await dog.loadPages(unversionedTree)
53+
}
54+
55+
if (!pageMap) {
56+
pageMap = await dog.loadPageMap(pageList)
57+
}
58+
59+
if (!site) {
60+
site = dog.loadSiteData()
61+
}
62+
63+
if (!redirects) {
64+
redirects = await dog.loadRedirects(pageList)
65+
}
66+
67+
if (process.env.NODE_ENV !== 'test') {
68+
console.log(`Context primed in ${Date.now() - startTime} ms`)
69+
}
70+
71+
return getWarmedCache()
72+
}
73+
74+
// Instrument the `warmServer` function so that
75+
// it's wrapped in a timer that reports to Datadog
76+
dog.warmServer = statsd.asyncTimer(warmServer, 'warm_server')
77+
78+
// We only want statistics if the priming needs to occur, so let's wrap the
79+
// real method and return early [without statistics] whenever possible
80+
module.exports = async function warmServerWrapper () {
81+
// Bail out early if everything is properly ready to use
82+
if (isFullyWarmed()) {
83+
return getWarmedCache()
84+
}
85+
86+
return dog.warmServer()
87+
}

middleware/breadcrumbs.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ module.exports = async function breadcrumbs (req, res, next) {
1919
pathParts.shift()
2020

2121
const productPath = path.posix.join('/', req.context.currentProduct)
22+
23+
if (!req.context.siteTree[req.language][req.context.currentVersion].products) {
24+
return next()
25+
}
26+
2227
const product = req.context.siteTree[req.language][req.context.currentVersion].products[req.context.currentProduct]
2328

2429
if (!product) {

0 commit comments

Comments
 (0)