Skip to content

Commit 0ec47e0

Browse files
authored
Only read the frontmatter from files in warm-server (#17222)
* Add read-frontmatter.js * Use it * Page static read/init are async now * Fix some blockers * I'm confused * Fix some more bugs * Use frontmatter schema, ensure end fence exists * Fix a bug * Still read full contents for index.md files * Remove comment * Only get ToC items for index pages * Readd frontmatter error and verdadero handling * Fix some borked tests * Simplify the code * Add a comment * Remove redundant variable * Re-simplify the Page construction * End chunk _after_ endline * Just use Page.init
1 parent 48549b8 commit 0ec47e0

5 files changed

Lines changed: 101 additions & 46 deletions

File tree

lib/page.js

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,18 @@ const frontmatter = require('./frontmatter')
2121
const products = require('./all-products')
2222
const slash = require('slash')
2323
const statsd = require('./statsd')
24+
const fmfromf = require('./read-frontmatter')
2425
const getLinkData = require('./get-link-data')
2526

2627
class Page {
27-
static init (opts) {
28-
opts = Page.read(opts)
28+
static async init (opts) {
29+
opts = await Page.read(opts)
2930
if (!opts) return
3031
return new Page(opts)
3132
}
3233

33-
static read (opts) {
34+
static async read (opts) {
35+
assert(opts.languageCode, 'languageCode is required')
3436
assert(opts.relativePath, 'relativePath is required')
3537
assert(opts.basePath, 'basePath is required')
3638

@@ -40,40 +42,29 @@ class Page {
4042
// Per https://nodejs.org/api/fs.html#fs_fs_exists_path_callback
4143
// its better to read and handle errors than to check access/stats first
4244
try {
43-
const raw = fs.readFileSync(fullPath, 'utf8')
44-
return { ...opts, relativePath, fullPath, raw }
45+
const { data, content, errors: frontmatterErrors } = await fmfromf(fullPath, opts.languageCode)
46+
47+
return {
48+
...opts,
49+
relativePath,
50+
fullPath,
51+
...data,
52+
markdown: content,
53+
frontmatterErrors
54+
}
4555
} catch (err) {
4656
if (err.code === 'ENOENT') return false
4757
console.error(err)
4858
}
4959
}
5060

5161
constructor (opts) {
52-
assert(opts.languageCode, 'languageCode is required')
53-
5462
Object.assign(this, { ...opts })
5563

56-
// TODO remove this when crowdin-support issue 66 has been resolved
57-
if (this.languageCode !== 'en' && this.raw.includes(': verdadero')) {
58-
this.raw = this.raw.replace(': verdadero', ': true')
59-
}
60-
61-
// parse frontmatter and save any errors for validation in the test suite
62-
const { content, data, errors: frontmatterErrors } = frontmatter(this.raw, { filepath: this.fullPath })
63-
this.frontmatterErrors = frontmatterErrors
64-
6564
if (this.frontmatterErrors.length) {
6665
throw new Error(JSON.stringify(this.frontmatterErrors, null, 2))
6766
}
6867

69-
// preserve the frontmatter-free markdown content,
70-
this.markdown = content
71-
72-
// prevent `[foo] (bar)` strings with a space between from being interpreted as markdown links
73-
this.markdown = encodeBracketedParentheses(this.markdown)
74-
75-
Object.assign(this, data)
76-
7768
// Store raw data so we can cache parsed versions
7869
this.rawIntro = this.intro
7970
this.rawTitle = this.title
@@ -94,8 +85,10 @@ class Page {
9485
// derive array of Permalink objects
9586
this.permalinks = Permalink.derive(this.languageCode, this.relativePath, this.title, this.versions)
9687

97-
// get an array of linked items in product and category TOCs
98-
this.tocItems = getTocItems(this)
88+
if (this.relativePath.endsWith('index.md')) {
89+
// get an array of linked items in product and category TOCs
90+
this.tocItems = getTocItems(this)
91+
}
9992

10093
// if this is an article and it doesn't have showMiniToc = false, set mini TOC to true
10194
if (!this.relativePath.endsWith('index.md') && !this.mapTopic) {
@@ -146,7 +139,20 @@ class Page {
146139
: this.renderProp('title', context, opts)
147140
}
148141

142+
async getMarkdown () {
143+
const raw = fs.readFileSync(this.fullPath, 'utf8')
144+
const { content } = frontmatter(raw, { filepath: this.fullPath })
145+
// prevent `[foo] (bar)` strings with a space between from being interpreted as markdown links
146+
const encodedMarkdown = encodeBracketedParentheses(content)
147+
return encodedMarkdown
148+
}
149+
149150
async _render (context) {
151+
// Get the raw markdown if we need to
152+
if (!this.markdown) {
153+
this.markdown = await this.getMarkdown()
154+
}
155+
150156
this.intro = await renderContent(this.rawIntro, context)
151157

152158
// rewrite local links in the intro to include current language code and GHE version if needed

lib/pages.js

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@ const walk = require('walk-sync').entries
33
const Page = require('./page')
44
const languages = require('./languages')
55

6-
function loadPageList () {
6+
async function loadPageList () {
77
// load english pages
88
const englishPath = path.join(__dirname, '..', languages.en.dir, 'content')
99
const englishPaths = walk(englishPath, {
1010
globs: ['**/*.md'],
1111
ignore: ['**/README.md']
1212
})
13-
const englishPages = englishPaths.map(
14-
opts => Page.read({
13+
const englishPages = await Promise.all(englishPaths.map(
14+
async opts => Page.init({
1515
...opts,
1616
languageCode: languages.en.code
1717
})
18-
)
18+
))
1919

2020
// load matching pages in other languages
2121
const localizedPaths = Object.values(languages)
@@ -30,16 +30,18 @@ function loadPageList () {
3030
})
3131
.flat()
3232

33-
const localizedPages = localizedPaths.map(
34-
({ basePath, relativePath, languageCode }) =>
35-
Page.read({ basePath, relativePath, languageCode })
36-
)
33+
const localizedPages = await Promise.all(localizedPaths.map(
34+
async ({ basePath, relativePath, languageCode }) => Page.init({
35+
basePath,
36+
relativePath,
37+
languageCode
38+
})
39+
))
3740

3841
// Build out the list of prepared pages
3942
return englishPages
4043
.concat(localizedPages)
4144
.filter(Boolean)
42-
.map(opts => new Page(opts))
4345
}
4446

4547
function createMapFromArray (pageList) {
@@ -58,8 +60,8 @@ function createMapFromArray (pageList) {
5860
return pageMap
5961
}
6062

61-
function loadPageMap (pageList) {
62-
const pages = pageList || loadPageList()
63+
async function loadPageMap (pageList) {
64+
const pages = pageList || await loadPageList()
6365
return createMapFromArray(pages)
6466
}
6567

lib/read-frontmatter.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
const fs = require('fs')
2+
const fm = require('./frontmatter')
3+
4+
const endLine = '\n---\n'
5+
6+
/**
7+
* Reads the given filepath, but only up until `endLine`, using streams to
8+
* read each chunk and close the stream early.
9+
*/
10+
async function readFrontmatter (filepath) {
11+
const readStream = fs.createReadStream(filepath, { encoding: 'utf8', emitClose: true })
12+
return new Promise((resolve, reject) => {
13+
let frontmatter = ''
14+
readStream
15+
.on('data', function (chunk) {
16+
const endOfFrontmatterIndex = chunk.indexOf(endLine)
17+
if (endOfFrontmatterIndex !== -1) {
18+
frontmatter += chunk.slice(0, endOfFrontmatterIndex + endLine.length)
19+
// Stop early!
20+
readStream.destroy()
21+
} else {
22+
frontmatter += chunk
23+
}
24+
})
25+
.on('error', (error) => reject(error))
26+
// Stream has been destroyed and file has been closed
27+
.on('close', () => resolve(frontmatter))
28+
})
29+
}
30+
31+
/**
32+
* Read only the frontmatter from a file
33+
*/
34+
module.exports = async function fmfromf (filepath, languageCode) {
35+
let fileContent = filepath.endsWith('index.md')
36+
// For index files, we need to read the whole file because they contain ToC info
37+
? await fs.promises.readFile(filepath, 'utf8')
38+
// For everything else, only read the frontmatter
39+
: await readFrontmatter(filepath)
40+
41+
// TODO remove this when crowdin-support issue 66 has been resolved
42+
if (languageCode !== 'en' && fileContent.includes(': verdadero')) {
43+
fileContent = fileContent.replace(': verdadero', ': true')
44+
}
45+
46+
return fm(fileContent, { filepath })
47+
}

lib/warm-server.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,15 @@ async function warmServer () {
4040
}
4141

4242
if (!pageList) {
43-
pageList = dog.loadPages()
43+
pageList = await dog.loadPages()
4444
}
4545

4646
if (!site) {
4747
site = dog.loadSiteData()
4848
}
4949

5050
if (!pageMap) {
51-
pageMap = dog.loadPageMap(pageList)
51+
pageMap = await dog.loadPageMap(pageList)
5252
}
5353

5454
if (!redirects) {

tests/unit/page.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ describe('Page class', () => {
430430
})
431431

432432
describe('catches errors thrown in Page class', () => {
433-
test('frontmatter parsing error', () => {
433+
test('frontmatter parsing error', async () => {
434434
async function getPage () {
435435
return await Page.init({
436436
relativePath: 'page-with-frontmatter-error.md',
@@ -439,10 +439,10 @@ describe('catches errors thrown in Page class', () => {
439439
})
440440
}
441441

442-
expect(getPage).rejects.toThrowError('invalid frontmatter entry')
442+
await expect(getPage).rejects.toThrowError('invalid frontmatter entry')
443443
})
444444

445-
test('missing versions frontmatter', () => {
445+
test('missing versions frontmatter', async () => {
446446
async function getPage () {
447447
return await Page.init({
448448
relativePath: 'page-with-missing-product-versions.md',
@@ -451,10 +451,10 @@ describe('catches errors thrown in Page class', () => {
451451
})
452452
}
453453

454-
expect(getPage).rejects.toThrowError('versions')
454+
await expect(getPage).rejects.toThrowError('versions')
455455
})
456456

457-
test('English page with a version in frontmatter that its parent product is not available in', () => {
457+
test('English page with a version in frontmatter that its parent product is not available in', async () => {
458458
async function getPage () {
459459
return await Page.init({
460460
relativePath: 'admin/some-category/some-article-with-mismatched-versions-frontmatter.md',
@@ -466,7 +466,7 @@ describe('catches errors thrown in Page class', () => {
466466
expect(getPage).rejects.toThrowError(/`versions` frontmatter.*? product is not available in/)
467467
})
468468

469-
test('non-English page with a version in frontmatter that its parent product is not available in', () => {
469+
test('non-English page with a version in frontmatter that its parent product is not available in', async () => {
470470
async function getPage () {
471471
return await Page.init({
472472
relativePath: 'admin/some-category/some-article-with-mismatched-versions-frontmatter.md',
@@ -475,6 +475,6 @@ describe('catches errors thrown in Page class', () => {
475475
})
476476
}
477477

478-
expect(getPage).rejects.toThrowError(/`versions` frontmatter.*? product is not available in/)
478+
await expect(getPage).rejects.toThrowError(/`versions` frontmatter.*? product is not available in/)
479479
})
480480
})

0 commit comments

Comments
 (0)