perf: 优化 frontmatter 生成

This commit is contained in:
pengzhanbo 2024-07-08 02:41:52 +08:00
parent 5a600048d5
commit 025a6bf680
5 changed files with 188 additions and 20 deletions

View File

@ -2,6 +2,7 @@
"name": "@vuepress-plume/plugin-auto-frontmatter",
"type": "module",
"version": "1.0.0-rc.72",
"private": true,
"description": "The Plugin for VuePress 2 - auto frontmatter",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"license": "MIT",

View File

@ -0,0 +1,122 @@
import { fs } from 'vuepress/utils'
import chokidar from 'chokidar'
import { createFilter } from 'create-filter'
import grayMatter from 'gray-matter'
import jsonToYaml from 'json2yaml'
import { isArray, isEmptyObject, promiseParallel, toArray } from '@pengzhanbo/utils'
import type { App } from 'vuepress'
import type {
AutoFrontmatter,
AutoFrontmatterArray,
AutoFrontmatterMarkdownFile,
AutoFrontmatterObject,
} from '../../shared/auto-frontmatter.js'
import type { PlumeThemeLocaleOptions } from '../../shared/index.js'
import { readMarkdown, readMarkdownList } from './readFile.js'
import { resolveOptions } from './resolveOptions.js'
export interface Generate {
globFilter: (id?: string) => boolean
global: AutoFrontmatterObject
rules: {
include: string | string[]
filter: (id?: string) => boolean
frontmatter: AutoFrontmatterObject
}[]
}
let generate: Generate | null = null
export function initAutoFrontmatter(
localeOptions: PlumeThemeLocaleOptions,
autoFrontmatter: AutoFrontmatter = {},
) {
const { include, exclude, frontmatter = {} } = resolveOptions(localeOptions, autoFrontmatter)
const globFilter = createFilter(include, exclude, { resolve: false })
const userConfig: AutoFrontmatterArray = isArray(frontmatter)
? frontmatter
: [{ include: '*', frontmatter }]
const globalConfig: AutoFrontmatterObject
= userConfig.find(({ include }) => include === '*')?.frontmatter || {}
const rules = userConfig
.filter(({ include }) => include !== '*')
.map(({ include, frontmatter }) => {
return {
include,
filter: createFilter(toArray(include), undefined, { resolve: false }),
frontmatter,
}
})
generate = {
globFilter,
global: globalConfig,
rules,
}
}
export async function generateAFrontmatter(app: App) {
if (!generate)
return
const markdownList = await readMarkdownList(app.dir.source(), generate.globFilter)
await promiseParallel(
markdownList.map(file => () => generator(file)),
64,
)
}
export async function watchAutoFrontmatter(app: App, watchers: any[]) {
if (!generate)
return
const watcher = chokidar.watch('**/*.md', {
cwd: app.dir.source(),
ignoreInitial: true,
ignored: /(node_modules|\.vuepress)\//,
})
watcher.on('add', async (relativePath) => {
if (!generate!.globFilter(relativePath))
return
const file = await readMarkdown(app.dir.source(), relativePath)
await generator(file)
})
watchers.push(watcher)
}
async function generator(file: AutoFrontmatterMarkdownFile): Promise<void> {
if (!generate)
return
const { filepath, relativePath } = file
const current = generate.rules.find(({ filter }) => filter(relativePath))
const formatter = current?.frontmatter || generate.global
const { data, content } = grayMatter(file.content)
for (const key in formatter) {
const value = await formatter[key](data[key], file, data)
data[key] = value ?? data[key]
}
try {
const yaml = isEmptyObject(data)
? ''
: jsonToYaml
.stringify(data)
.replace(/\n\s{2}/g, '\n')
.replace(/"/g, '')
.replace(/\s+\n/g, '\n')
const newContent = yaml ? `${yaml}---\n${content}` : content
fs.writeFileSync(filepath, newContent, 'utf-8')
}
catch (e) {
console.error(e)
}
}

View File

@ -0,0 +1,3 @@
export * from './generator.js'
export * from './readFile.js'
export * from './resolveOptions.js'

View File

@ -0,0 +1,38 @@
import { fs, path } from 'vuepress/utils'
import fg from 'fast-glob'
import type { AutoFrontmatterMarkdownFile } from '../../shared/auto-frontmatter.js'
export async function readMarkdownList(
sourceDir: string,
filter: (id: string) => boolean,
): Promise<AutoFrontmatterMarkdownFile[]> {
const files: string[] = await fg(['**/*.md'], {
cwd: sourceDir,
ignore: ['node_modules', '.vuepress'],
})
return await Promise.all(
files
.filter(filter)
.map(file => readMarkdown(sourceDir, file)),
)
}
export async function readMarkdown(
sourceDir: string,
relativePath: string,
): Promise<AutoFrontmatterMarkdownFile> {
const filepath = path.join(sourceDir, relativePath)
const stats = await fs.promises.stat(filepath)
return {
filepath,
relativePath,
content: await fs.promises.readFile(filepath, 'utf-8'),
createTime: getFileCreateTime(stats),
stats,
}
}
export function getFileCreateTime(stats: fs.Stats): Date {
return stats.birthtime.getFullYear() !== 1970 ? stats.birthtime : stats.atime
}

View File

@ -1,17 +1,15 @@
import { path } from 'vuepress/utils'
import { removeLeadingSlash, resolveLocalePath } from 'vuepress/shared'
import { ensureLeadingSlash } from '@vuepress/helper'
import type {
AutoFrontmatterOptions,
FrontmatterArray,
FrontmatterObject,
} from '@vuepress-plume/plugin-auto-frontmatter'
import { format } from 'date-fns'
import { uniq } from '@pengzhanbo/utils'
import type { NotesSidebar } from '@vuepress-plume/plugin-notes-data'
import type {
AutoFrontmatter,
AutoFrontmatterArray,
AutoFrontmatterObject,
PlumeThemeLocaleOptions,
PlumeThemePluginOptions,
SidebarItem,
} from '../../shared/index.js'
import {
getCurrentDirname,
@ -20,16 +18,15 @@ import {
normalizePath,
pathJoin,
withBase,
} from '../utils.js'
} from '../utils/index.js'
import { resolveNotesOptions } from '../config/index.js'
export function resolveAutoFrontmatterOptions(
pluginOptions: PlumeThemePluginOptions,
export function resolveOptions(
localeOptions: PlumeThemeLocaleOptions,
): AutoFrontmatterOptions {
frontmatter: AutoFrontmatter,
): AutoFrontmatter {
const pkg = getPackage()
const { locales = {}, article: articlePrefix = '/article/' } = localeOptions
const { frontmatter } = pluginOptions
const resolveLocale = (relativeFilepath: string) => {
const file = ensureLeadingSlash(relativeFilepath)
@ -50,7 +47,7 @@ export function resolveAutoFrontmatterOptions(
})
.filter(Boolean)
const baseFrontmatter: FrontmatterObject = {
const baseFrontmatter: AutoFrontmatterObject = {
author(author: string, { relativePath }, data: any) {
if (author)
return author
@ -197,26 +194,33 @@ export function resolveAutoFrontmatterOptions(
},
},
},
].filter(Boolean) as FrontmatterArray,
].filter(Boolean) as AutoFrontmatterArray,
}
}
function resolveLinkBySidebar(
sidebar: NotesSidebar,
prefix: string,
sidebar: 'auto' | (string | SidebarItem)[],
_prefix: string,
) {
const res: Record<string, string> = {}
if (sidebar === 'auto') {
return res
}
for (const item of sidebar) {
if (typeof item !== 'string') {
const { dir = '', link = '/', items, text = '' } = item
SidebarLink(items, link, text, pathJoin(prefix, dir), res)
const { prefix, dir = '', link = '/', items, text = '' } = item
getSidebarLink(items, link, text, pathJoin(_prefix, prefix || dir), res)
}
}
return res
}
function SidebarLink(items: NotesSidebar | undefined, link: string, text: string, dir = '', res: Record<string, string> = {}) {
function getSidebarLink(items: 'auto' | (string | SidebarItem)[] | undefined, link: string, text: string, dir = '', res: Record<string, string> = {}) {
if (items === 'auto')
return
if (!items) {
res[pathJoin(dir, `${text}.md`)] = link
return
@ -237,8 +241,8 @@ function SidebarLink(items: NotesSidebar | undefined, link: string, text: string
res[dir] = link
}
else {
const { dir: subDir = '', link: subLink = '/', items: subItems, text: subText = '' } = item
SidebarLink(subItems, pathJoin(link, subLink), subText, pathJoin(dir, subDir), res)
const { prefix, dir: subDir = '', link: subLink = '/', items: subItems, text: subText = '' } = item
getSidebarLink(subItems, pathJoin(link, subLink), subText, pathJoin(prefix || dir, subDir), res)
}
}
}