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
This commit is contained in:
Jason Etcovitch 2021-01-14 10:46:59 -05:00 коммит произвёл GitHub
Родитель 48549b8d57
Коммит 0ec47e0246
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 101 добавлений и 46 удалений

Просмотреть файл

@ -21,16 +21,18 @@ const frontmatter = require('./frontmatter')
const products = require('./all-products')
const slash = require('slash')
const statsd = require('./statsd')
const fmfromf = require('./read-frontmatter')
const getLinkData = require('./get-link-data')
class Page {
static init (opts) {
opts = Page.read(opts)
static async init (opts) {
opts = await Page.read(opts)
if (!opts) return
return new Page(opts)
}
static read (opts) {
static async read (opts) {
assert(opts.languageCode, 'languageCode is required')
assert(opts.relativePath, 'relativePath is required')
assert(opts.basePath, 'basePath is required')
@ -40,8 +42,16 @@ class Page {
// Per https://nodejs.org/api/fs.html#fs_fs_exists_path_callback
// its better to read and handle errors than to check access/stats first
try {
const raw = fs.readFileSync(fullPath, 'utf8')
return { ...opts, relativePath, fullPath, raw }
const { data, content, errors: frontmatterErrors } = await fmfromf(fullPath, opts.languageCode)
return {
...opts,
relativePath,
fullPath,
...data,
markdown: content,
frontmatterErrors
}
} catch (err) {
if (err.code === 'ENOENT') return false
console.error(err)
@ -49,31 +59,12 @@ class Page {
}
constructor (opts) {
assert(opts.languageCode, 'languageCode is required')
Object.assign(this, { ...opts })
// TODO remove this when crowdin-support issue 66 has been resolved
if (this.languageCode !== 'en' && this.raw.includes(': verdadero')) {
this.raw = this.raw.replace(': verdadero', ': true')
}
// parse frontmatter and save any errors for validation in the test suite
const { content, data, errors: frontmatterErrors } = frontmatter(this.raw, { filepath: this.fullPath })
this.frontmatterErrors = frontmatterErrors
if (this.frontmatterErrors.length) {
throw new Error(JSON.stringify(this.frontmatterErrors, null, 2))
}
// preserve the frontmatter-free markdown content,
this.markdown = content
// prevent `[foo] (bar)` strings with a space between from being interpreted as markdown links
this.markdown = encodeBracketedParentheses(this.markdown)
Object.assign(this, data)
// Store raw data so we can cache parsed versions
this.rawIntro = this.intro
this.rawTitle = this.title
@ -94,8 +85,10 @@ class Page {
// derive array of Permalink objects
this.permalinks = Permalink.derive(this.languageCode, this.relativePath, this.title, this.versions)
// get an array of linked items in product and category TOCs
this.tocItems = getTocItems(this)
if (this.relativePath.endsWith('index.md')) {
// get an array of linked items in product and category TOCs
this.tocItems = getTocItems(this)
}
// if this is an article and it doesn't have showMiniToc = false, set mini TOC to true
if (!this.relativePath.endsWith('index.md') && !this.mapTopic) {
@ -146,7 +139,20 @@ class Page {
: this.renderProp('title', context, opts)
}
async getMarkdown () {
const raw = fs.readFileSync(this.fullPath, 'utf8')
const { content } = frontmatter(raw, { filepath: this.fullPath })
// prevent `[foo] (bar)` strings with a space between from being interpreted as markdown links
const encodedMarkdown = encodeBracketedParentheses(content)
return encodedMarkdown
}
async _render (context) {
// Get the raw markdown if we need to
if (!this.markdown) {
this.markdown = await this.getMarkdown()
}
this.intro = await renderContent(this.rawIntro, context)
// rewrite local links in the intro to include current language code and GHE version if needed

Просмотреть файл

@ -3,19 +3,19 @@ const walk = require('walk-sync').entries
const Page = require('./page')
const languages = require('./languages')
function loadPageList () {
async function loadPageList () {
// load english pages
const englishPath = path.join(__dirname, '..', languages.en.dir, 'content')
const englishPaths = walk(englishPath, {
globs: ['**/*.md'],
ignore: ['**/README.md']
})
const englishPages = englishPaths.map(
opts => Page.read({
const englishPages = await Promise.all(englishPaths.map(
async opts => Page.init({
...opts,
languageCode: languages.en.code
})
)
))
// load matching pages in other languages
const localizedPaths = Object.values(languages)
@ -30,16 +30,18 @@ function loadPageList () {
})
.flat()
const localizedPages = localizedPaths.map(
({ basePath, relativePath, languageCode }) =>
Page.read({ basePath, relativePath, languageCode })
)
const localizedPages = await Promise.all(localizedPaths.map(
async ({ basePath, relativePath, languageCode }) => Page.init({
basePath,
relativePath,
languageCode
})
))
// Build out the list of prepared pages
return englishPages
.concat(localizedPages)
.filter(Boolean)
.map(opts => new Page(opts))
}
function createMapFromArray (pageList) {
@ -58,8 +60,8 @@ function createMapFromArray (pageList) {
return pageMap
}
function loadPageMap (pageList) {
const pages = pageList || loadPageList()
async function loadPageMap (pageList) {
const pages = pageList || await loadPageList()
return createMapFromArray(pages)
}

47
lib/read-frontmatter.js Normal file
Просмотреть файл

@ -0,0 +1,47 @@
const fs = require('fs')
const fm = require('./frontmatter')
const endLine = '\n---\n'
/**
* Reads the given filepath, but only up until `endLine`, using streams to
* read each chunk and close the stream early.
*/
async function readFrontmatter (filepath) {
const readStream = fs.createReadStream(filepath, { encoding: 'utf8', emitClose: true })
return new Promise((resolve, reject) => {
let frontmatter = ''
readStream
.on('data', function (chunk) {
const endOfFrontmatterIndex = chunk.indexOf(endLine)
if (endOfFrontmatterIndex !== -1) {
frontmatter += chunk.slice(0, endOfFrontmatterIndex + endLine.length)
// Stop early!
readStream.destroy()
} else {
frontmatter += chunk
}
})
.on('error', (error) => reject(error))
// Stream has been destroyed and file has been closed
.on('close', () => resolve(frontmatter))
})
}
/**
* Read only the frontmatter from a file
*/
module.exports = async function fmfromf (filepath, languageCode) {
let fileContent = filepath.endsWith('index.md')
// For index files, we need to read the whole file because they contain ToC info
? await fs.promises.readFile(filepath, 'utf8')
// For everything else, only read the frontmatter
: await readFrontmatter(filepath)
// TODO remove this when crowdin-support issue 66 has been resolved
if (languageCode !== 'en' && fileContent.includes(': verdadero')) {
fileContent = fileContent.replace(': verdadero', ': true')
}
return fm(fileContent, { filepath })
}

Просмотреть файл

@ -40,7 +40,7 @@ async function warmServer () {
}
if (!pageList) {
pageList = dog.loadPages()
pageList = await dog.loadPages()
}
if (!site) {
@ -48,7 +48,7 @@ async function warmServer () {
}
if (!pageMap) {
pageMap = dog.loadPageMap(pageList)
pageMap = await dog.loadPageMap(pageList)
}
if (!redirects) {

Просмотреть файл

@ -430,7 +430,7 @@ describe('Page class', () => {
})
describe('catches errors thrown in Page class', () => {
test('frontmatter parsing error', () => {
test('frontmatter parsing error', async () => {
async function getPage () {
return await Page.init({
relativePath: 'page-with-frontmatter-error.md',
@ -439,10 +439,10 @@ describe('catches errors thrown in Page class', () => {
})
}
expect(getPage).rejects.toThrowError('invalid frontmatter entry')
await expect(getPage).rejects.toThrowError('invalid frontmatter entry')
})
test('missing versions frontmatter', () => {
test('missing versions frontmatter', async () => {
async function getPage () {
return await Page.init({
relativePath: 'page-with-missing-product-versions.md',
@ -451,10 +451,10 @@ describe('catches errors thrown in Page class', () => {
})
}
expect(getPage).rejects.toThrowError('versions')
await expect(getPage).rejects.toThrowError('versions')
})
test('English page with a version in frontmatter that its parent product is not available in', () => {
test('English page with a version in frontmatter that its parent product is not available in', async () => {
async function getPage () {
return await Page.init({
relativePath: 'admin/some-category/some-article-with-mismatched-versions-frontmatter.md',
@ -466,7 +466,7 @@ describe('catches errors thrown in Page class', () => {
expect(getPage).rejects.toThrowError(/`versions` frontmatter.*? product is not available in/)
})
test('non-English page with a version in frontmatter that its parent product is not available in', () => {
test('non-English page with a version in frontmatter that its parent product is not available in', async () => {
async function getPage () {
return await Page.init({
relativePath: 'admin/some-category/some-article-with-mismatched-versions-frontmatter.md',
@ -475,6 +475,6 @@ describe('catches errors thrown in Page class', () => {
})
}
expect(getPage).rejects.toThrowError(/`versions` frontmatter.*? product is not available in/)
await expect(getPage).rejects.toThrowError(/`versions` frontmatter.*? product is not available in/)
})
})