perf: 优化主题配置解析

This commit is contained in:
pengzhanbo 2024-07-08 02:44:01 +08:00
parent 025a6bf680
commit 9a8df18286
14 changed files with 100 additions and 218 deletions

View File

@ -0,0 +1,20 @@
import {
blogPostData as blogPostDataRaw,
} from '@internal/blogData'
import { ref } from 'vue'
import type { Ref } from 'vue'
import type { PlumeThemeBlogPostData } from '../../shared/index.js'
export type BlogDataRef = Ref<PlumeThemeBlogPostData>
export const blogPostData: BlogDataRef = ref(blogPostDataRaw)
export function useBlogPostData(): BlogDataRef {
return blogPostData as BlogDataRef
}
if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) {
__VUE_HMR_RUNTIME__.updateBlogPostData = (data: PlumeThemeBlogPostData) => {
blogPostData.value = data
}
}

View File

@ -0,0 +1,55 @@
import {
encrypt as rawEncrypt,
} from '@internal/encrypt'
import { ref } from 'vue'
import type { Ref } from 'vue'
export type EncryptConfig = readonly [
boolean, // global
string, // separator
string, // admin
string[], // keys
Record<string, string>, // rules
]
export interface EncryptData {
global: boolean
separator: string
admins: string[]
matches: string[]
ruleList: {
key: string
match: string
rules: string[]
}[]
}
export type EncryptRef = Ref<EncryptData>
export const encrypt: EncryptRef = ref(resolveEncryptData(rawEncrypt))
export function useEncryptData(): EncryptRef {
return encrypt as EncryptRef
}
function resolveEncryptData(
[global, separator, admin, matches, rules]: EncryptConfig,
): EncryptData {
return {
global,
separator,
matches,
admins: admin.split(separator),
ruleList: Object.keys(rules).map(key => ({
key,
match: matches[key] as string,
rules: rules[key].split(separator),
})),
}
}
if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) {
__VUE_HMR_RUNTIME__.updateEncrypt = (data: EncryptConfig) => {
encrypt.value = resolveEncryptData(data)
}
}

View File

@ -9,5 +9,4 @@ export * from './templateBuildRenderer.js'
export * from './resolveSearchOptions.js'
export * from './resolvePageHead.js'
export * from './resolveEncrypt.js'
export * from './resolveNotesOptions.js'

View File

@ -1,5 +1,5 @@
import { fs, path } from 'vuepress/utils'
import { resolve } from '../utils.js'
import { resolve } from '../utils/index.js'
export function resolveAlias() {
return {

View File

@ -1,59 +0,0 @@
import { genSaltSync, hashSync } from 'bcrypt-ts'
import { isNumber, isString, random, toArray } from '@pengzhanbo/utils'
import type { Page } from 'vuepress/core'
import type { PlumeThemeEncrypt, PlumeThemePageData } from '../../shared/index.js'
const isStringLike = (value: unknown): boolean => isString(value) || isNumber(value)
const separator = ':'
export function resolveEncrypt(encrypt?: PlumeThemeEncrypt) {
const salt = () => genSaltSync(random(8, 16))
const admin = encrypt?.admin
? toArray(encrypt.admin)
.filter(isStringLike)
.map(item => hashSync(String(item), salt()))
.join(separator)
: ''
const rules: Record<string, string> = {}
const keys = Object.keys(encrypt?.rules ?? {})
if (encrypt?.rules) {
Object.keys(encrypt.rules).forEach((key) => {
const index = keys.indexOf(key)
rules[String(index)] = toArray(encrypt.rules![key])
.filter(isStringLike)
.map(item => hashSync(String(item), salt()))
.join(separator)
})
}
return {
__PLUME_ENCRYPT_GLOBAL__: encrypt?.global ?? false,
__PLUME_ENCRYPT_SEPARATOR__: separator,
__PLUME_ENCRYPT_ADMIN__: admin,
__PLUME_ENCRYPT_KEYS__: keys,
__PLUME_ENCRYPT_RULES__: rules,
}
}
export function isEncryptPage(page: Page<PlumeThemePageData>, encrypt?: PlumeThemeEncrypt) {
if (!encrypt)
return false
const rules = encrypt.rules ?? {}
return Object.keys(rules).some((match) => {
const relativePath = page.data.filePathRelative || ''
if (match[0] === '^') {
const regex = new RegExp(match)
return regex.test(page.path) || (relativePath && regex.test(relativePath))
}
if (match.endsWith('.md'))
return relativePath && relativePath.endsWith(match)
return page.path.startsWith(match) || relativePath.startsWith(match)
})
}

View File

@ -2,7 +2,7 @@ import { entries, fromEntries, getLocaleConfig } from '@vuepress/helper'
import type { App } from 'vuepress'
import { LOCALE_OPTIONS } from '../locales/index.js'
import type { PlumeThemeLocaleData, PlumeThemeLocaleOptions } from '../../shared/index.js'
import { THEME_NAME } from '../utils.js'
import { THEME_NAME } from '../utils/index.js'
const FALLBACK_OPTIONS: PlumeThemeLocaleData = {
appearance: true,
@ -18,6 +18,10 @@ const FALLBACK_OPTIONS: PlumeThemeLocaleData = {
editLink: true,
contributors: true,
footer: {
message:
'Power by <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://theme-plume.vuejs.press">vuepress-theme-plume</a>',
},
}
export function resolveLocaleOptions(app: App, { locales, ...options }: PlumeThemeLocaleOptions): PlumeThemeLocaleOptions {

View File

@ -1,8 +1,7 @@
import type { NotesDataOptions } from '@vuepress-plume/plugin-notes-data'
import { entries } from '@vuepress/helper'
import { uniq } from '@pengzhanbo/utils'
import type { PlumeThemeLocaleOptions } from '../..//shared/index.js'
import { withBase } from '../utils.js'
import type { NotesOptions, PlumeThemeLocaleOptions } from '../../shared/index.js'
import { withBase } from '../utils/index.js'
export function resolveNotesLinkList(localeOptions: PlumeThemeLocaleOptions) {
const locales = localeOptions.locales || {}
@ -22,9 +21,9 @@ export function resolveNotesLinkList(localeOptions: PlumeThemeLocaleOptions) {
return uniq(notesLinks)
}
export function resolveNotesOptions(localeOptions: PlumeThemeLocaleOptions): NotesDataOptions[] {
export function resolveNotesOptions(localeOptions: PlumeThemeLocaleOptions): NotesOptions[] {
const locales = localeOptions.locales || {}
const notesOptionsList: NotesDataOptions[] = []
const notesOptionsList: NotesOptions[] = []
for (const [locale, opt] of entries(locales)) {
const options = locale === '/' ? (opt.notes || localeOptions.notes) : opt.notes
if (options) {

View File

@ -1,20 +1,15 @@
import type { App } from 'vuepress'
import { entries, fromEntries, getRootLangPath, isPlainObject } from '@vuepress/helper'
import type { PlumeThemeEncrypt, PlumeThemePluginOptions } from '../../shared/index.js'
import type { PlumeThemePluginOptions } from '../../shared/index.js'
import { PRESET_LOCALES } from '../locales/index.js'
import { resolveEncrypt } from './resolveEncrypt.js'
export function resolveProvideData(
app: App,
plugins: PlumeThemePluginOptions,
encrypt?: PlumeThemeEncrypt,
): Record<string, any> {
const root = getRootLangPath(app)
return {
// 注入 加密配置
...resolveEncrypt(encrypt),
// 注入水印配置
__PLUME_WM_FP__: isPlainObject(plugins.watermark)
? plugins.watermark.fullPage !== false

View File

@ -2,9 +2,9 @@ import { entries, getRootLangPath } from '@vuepress/helper'
import type { App } from 'vuepress'
import type { NavItem, PlumeThemeLocaleOptions } from '../../shared/index.js'
import { PRESET_LOCALES } from '../locales/index.js'
import { withBase } from '../utils.js'
import { withBase } from '../utils/index.js'
const EXCLUDE_LIST = ['locales', 'sidebar', 'navbar', 'notes', 'article', 'avatar']
const EXCLUDE_LIST = ['locales', 'sidebar', 'navbar', 'notes', 'sidebar', 'article', 'avatar']
// 过滤不需要出现在多语言配置中的字段
const EXCLUDE_LOCALE_LIST = [...EXCLUDE_LIST, 'blog', 'appearance']

View File

@ -1,7 +1,13 @@
import type { PlumeThemeOptions } from '../../shared/index.js'
import { logger } from '../utils.js'
import { logger } from '../utils/index.js'
export function resolveThemeOptions({ themePlugins, plugins, encrypt, hostname, ...localeOptions }: PlumeThemeOptions) {
export function resolveThemeOptions({
themePlugins,
plugins,
hostname,
configFile,
...localeOptions
}: PlumeThemeOptions) {
const pluginOptions = plugins ?? themePlugins ?? {}
if (themePlugins) {
@ -11,8 +17,8 @@ export function resolveThemeOptions({ themePlugins, plugins, encrypt, hostname,
}
return {
configFile,
pluginOptions,
encrypt,
hostname,
localeOptions,
}

View File

@ -1,5 +1,5 @@
import { type TemplateRendererContext, templateRenderer } from 'vuepress/utils'
import { getThemePackage } from '../utils.js'
import { getThemePackage } from '../utils/index.js'
export function templateBuildRenderer(template: string, context: TemplateRendererContext) {
const pkg = getThemePackage()

View File

@ -4,12 +4,8 @@ import { docsearchPlugin } from '@vuepress/plugin-docsearch'
import { gitPlugin } from '@vuepress/plugin-git'
import { photoSwipePlugin } from '@vuepress/plugin-photo-swipe'
import { nprogressPlugin } from '@vuepress/plugin-nprogress'
import { themeDataPlugin } from '@vuepress/plugin-theme-data'
import { autoFrontmatterPlugin } from '@vuepress-plume/plugin-auto-frontmatter'
import { baiduTongjiPlugin } from '@vuepress-plume/plugin-baidu-tongji'
import { blogDataPlugin } from '@vuepress-plume/plugin-blog-data'
import { iconifyPlugin } from '@vuepress-plume/plugin-iconify'
import { notesDataPlugin } from '@vuepress-plume/plugin-notes-data'
import { shikiPlugin } from '@vuepress-plume/plugin-shikiji'
import { commentPlugin } from '@vuepress/plugin-comment'
import { type MarkdownEnhancePluginOptions, mdEnhancePlugin } from 'vuepress-plugin-md-enhance'
@ -21,54 +17,31 @@ import { searchPlugin } from '@vuepress-plume/plugin-search'
import { markdownPowerPlugin } from 'vuepress-plugin-md-power'
import { watermarkPlugin } from '@vuepress/plugin-watermark'
import { fontsPlugin } from '@vuepress-plume/plugin-fonts'
import type {
PlumeThemeEncrypt,
PlumeThemeLocaleOptions,
PlumeThemePluginOptions,
} from '../../shared/index.js'
import type { PlumeThemeLocaleOptions, PlumeThemePluginOptions } from '../../shared/index.js'
import {
resolveDocsearchOptions,
resolveNotesOptions,
resolveSearchOptions,
resolveThemeData,
} from '../config/index.js'
import { resolveAutoFrontmatterOptions } from './resolveAutoFrontmatterOptions.js'
import { resolveBlogDataOptions } from './resolveBlogDataOptions.js'
import { customContainerPlugins } from './containerPlugins.js'
export interface SetupPluginOptions {
app: App
pluginOptions: PlumeThemePluginOptions
localeOptions: PlumeThemeLocaleOptions
encrypt?: PlumeThemeEncrypt
hostname?: string
}
export function getPlugins({
app,
pluginOptions,
localeOptions,
encrypt,
hostname,
}: SetupPluginOptions): PluginConfig {
const isProd = !app.env.isDev
const plugins: PluginConfig = [
themeDataPlugin({ themeData: resolveThemeData(app, localeOptions) }),
autoFrontmatterPlugin(resolveAutoFrontmatterOptions(pluginOptions, localeOptions)),
blogDataPlugin(resolveBlogDataOptions(localeOptions, encrypt)),
notesDataPlugin(resolveNotesOptions(localeOptions)),
iconifyPlugin(),
fontsPlugin(),
contentUpdatePlugin(),
activeHeaderLinksPlugin({
headerLinkSelector: 'a.outline-link',
headerAnchorSelector: '.header-anchor',
@ -185,10 +158,7 @@ export function getPlugins({
}
if (pluginOptions.seo !== false && hostname && isProd) {
plugins.push(seoPlugin({
hostname,
author: localeOptions.locales?.['/'].profile?.name || localeOptions.profile?.name || localeOptions.locales?.['/'].avatar?.name || localeOptions.avatar?.name,
}))
plugins.push(seoPlugin({ hostname }))
}
return plugins

View File

@ -1,45 +0,0 @@
import type { BlogDataPluginOptions } from '@vuepress-plume/plugin-blog-data'
import { removeLeadingSlash } from '@vuepress/helper'
import {
isEncryptPage,
resolveNotesOptions,
} from '../config/index.js'
import { normalizePath } from '../utils.js'
import type { PlumeThemeEncrypt, PlumeThemeLocaleOptions } from '../..//shared/index.js'
export function resolveBlogDataOptions(
localeOptions: PlumeThemeLocaleOptions,
encrypt?: PlumeThemeEncrypt,
): BlogDataPluginOptions {
const blog = localeOptions.blog || {}
const notesList = resolveNotesOptions(localeOptions)
const notesDirList = notesList
.map(notes => removeLeadingSlash(normalizePath(`${notes.dir}/**`)))
.filter(Boolean)
return {
include: blog?.include ?? ['**/*.md'],
exclude: [
'**/{README,readme,index}.md',
'.vuepress/',
'node_modules/',
...(blog.exclude ?? []),
...notesDirList,
].filter(Boolean),
sortBy: 'createTime',
excerpt: true,
pageFilter: (page: any) => page.frontmatter.draft !== true,
extendBlogData: (page: any) => {
const tags = page.frontmatter.tags
const data: Record<string, any> = {
categoryList: page.data.categoryList,
tags,
sticky: page.frontmatter.sticky,
createTime: page.data.frontmatter.createTime,
lang: page.lang,
}
isEncryptPage(page, encrypt) && (data.encrypt = true)
return data
},
}
}

View File

@ -1,62 +0,0 @@
import process from 'node:process'
import { createHash } from 'node:crypto'
import { customAlphabet } from 'nanoid'
import { fs, getDirname, path } from 'vuepress/utils'
import { Logger, ensureEndingSlash, ensureLeadingSlash } from '@vuepress/helper'
export const THEME_NAME = 'vuepress-theme-plume'
const __dirname = getDirname(import.meta.url)
export const resolve = (...args: string[]) => path.resolve(__dirname, '../', ...args)
export const templates = (url: string) => resolve('../templates', url)
export const nanoid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 8)
export const hash = (content: string) => createHash('md5').update(content).digest('hex')
export const logger = new Logger(THEME_NAME)
export function getPackage() {
let pkg = {} as any
try {
const content = fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf-8')
pkg = JSON.parse(content)
}
catch { }
return pkg
}
export function getThemePackage() {
let pkg = {} as any
try {
const content = fs.readFileSync(resolve('../package.json'), 'utf-8')
pkg = JSON.parse(content)
}
catch {}
return pkg
}
const RE_SLASH = /(\\|\/)+/g
export function normalizePath(path: string) {
return path.replace(RE_SLASH, '/')
}
export function pathJoin(...args: string[]) {
return normalizePath(path.join(...args))
}
const RE_START_END_SLASH = /^\/|\/$/g
export function getCurrentDirname(basePath: string | undefined, filepath: string) {
const dirList = normalizePath(basePath || path.dirname(filepath))
.replace(RE_START_END_SLASH, '')
.split('/')
return dirList.length > 0 ? dirList[dirList.length - 1] : ''
}
export function withBase(path = '', base = '/'): string {
path = ensureEndingSlash(ensureLeadingSlash(path))
if (path.startsWith(base))
return normalizePath(path)
return normalizePath(`${base}${path}`)
}