From 487448d6112d124d4ab8405426190b9a6c45a91b Mon Sep 17 00:00:00 2001 From: pengzhanbo Date: Mon, 7 Apr 2025 07:38:52 +0800 Subject: [PATCH] feat(theme): add theme deps version detection (#551) * feat(theme): add theme deps version detection * chore: tweak --- theme/src/node/detector/index.ts | 24 +---- theme/src/node/detector/options.ts | 21 +++++ theme/src/node/detector/versions.ts | 134 ++++++++++++++++++++++++++++ theme/src/node/plugins/code.ts | 2 +- theme/src/node/theme.ts | 4 +- 5 files changed, 161 insertions(+), 24 deletions(-) create mode 100644 theme/src/node/detector/options.ts create mode 100644 theme/src/node/detector/versions.ts diff --git a/theme/src/node/detector/index.ts b/theme/src/node/detector/index.ts index 9404a1e2..eff7375d 100644 --- a/theme/src/node/detector/index.ts +++ b/theme/src/node/detector/index.ts @@ -1,23 +1,3 @@ -import type { ThemeOptions } from '../../shared/index.js' -import { detectDependencies } from './dependency.js' -import { detectMarkdown } from './markdown.js' -import { detectPlugins } from './plugins.js' - export * from './fields.js' - -/** - * 检测主题选项 - */ -export function detectThemeOptions({ - plugins = {}, - configFile, - ...themeOptions -}: ThemeOptions) { - detectDependencies(themeOptions, plugins) - - // detect options - detectMarkdown(themeOptions) - detectPlugins(plugins) - - return { configFile, plugins, themeOptions } -} +export * from './options.js' +export * from './versions.js' diff --git a/theme/src/node/detector/options.ts b/theme/src/node/detector/options.ts new file mode 100644 index 00000000..c40acb5f --- /dev/null +++ b/theme/src/node/detector/options.ts @@ -0,0 +1,21 @@ +import type { ThemeOptions } from '../../shared/index.js' +import { detectDependencies } from './dependency.js' +import { detectMarkdown } from './markdown.js' +import { detectPlugins } from './plugins.js' + +/** + * 检测主题选项 + */ +export function detectThemeOptions({ + plugins = {}, + configFile, + ...themeOptions +}: ThemeOptions) { + detectDependencies(themeOptions, plugins) + + // detect options + detectMarkdown(themeOptions) + detectPlugins(plugins) + + return { configFile, plugins, themeOptions } +} diff --git a/theme/src/node/detector/versions.ts b/theme/src/node/detector/versions.ts new file mode 100644 index 00000000..ca8e120a --- /dev/null +++ b/theme/src/node/detector/versions.ts @@ -0,0 +1,134 @@ +import type { App } from 'vuepress' +import fs from 'node:fs' +import path from 'node:path' +import { colors } from 'vuepress/utils' +import { createTranslate, getPackage, getThemePackage, logger } from '../utils/index.js' + +const t = createTranslate({ + en: { + title: 'The following dependencies have version mismatches:', + footer: 'Please update the dependencies to the correct versions.', + }, + zh: { + title: '以下依赖版本不匹配:', + footer: '请更新依赖至正确的版本。', + }, +}) + +export function detectVersions(app: App) { + detectVuepressVersion() + detectThemeVersion(app) +} + +interface DepVersion { + name: string + expected: string + current: string +} + +/** + * 检查 vuepress 相关依赖, + * 当依赖不匹配时,可能会导致 vuepress 无法正常运行 + * 比如 某些插件依赖了不同版本的 `@vuepress/helper` ,会导致在浏览器中无法正常运行 + */ +function detectVuepressVersion() { + const themePackage = getThemePackage() + const userPackage = getPackage() + + const vuepressDeps = Object.entries({ + 'vuepress-theme-plume': themePackage.version, + '@vuepress/bundler-vite': themePackage.peerDependencies?.vuepress, + '@vuepress/bundler-webpack': themePackage.peerDependencies?.vuepress, + ...themePackage.dependencies, + ...themePackage.peerDependencies, + }).reduce((deps, [name, version]) => { + if (name.includes('vuepress') && version && version !== 'workspace:*') + deps[name] = version as string + return deps + }, {} as Record) + + /** + * 检查依赖是否匹配 + * TODO: 检查 pnpm catalog + */ + const detect = (deps: Record) => { + const results: DepVersion[] = [] + for (const [name, version] of Object.entries(deps)) { + const resolved = resolveVersion(version) + if (resolved && vuepressDeps[name] && vuepressDeps[name] !== resolved) + results.push({ name, expected: vuepressDeps[name], current: version as string }) + } + return results + } + + const devResults = detect(userPackage.devDependencies) + const prodResults = detect(userPackage.dependencies) + + if (devResults.length || prodResults.length) { + const output = (deps: DepVersion[]) => deps + .map(dep => + ` ${colors.green(dep.name)}: ${colors.gray(dep.current)} -> ${colors.cyan(dep.expected)}`) + .join(' \n') + + logger.warn(`${t('title')} +${ + devResults.length ? `\ndevDependencies:\n${output(devResults)}\n` : '' +}${ + prodResults.length ? `\ndependencies:\n${output(prodResults)}\n` : '' +} +${t('footer')} +`) + } +} + +/** + * 检查用户是否升级主题版本, + * 如果升级了主题版本,则清空缓存 + */ +function detectThemeVersion(app: App) { + if (app.env.isBuild) + return + + try { + const versionCache = app.dir.cache('.theme-plume-version') + const themePackage = getThemePackage() + const current = themePackage.version + + const updateCache = () => { + fs.mkdirSync(path.dirname(versionCache), { recursive: true }) + fs.writeFileSync(versionCache, current, 'utf-8') + } + + // 缓存文件不存在时,不做检查 + if (!fs.existsSync(versionCache)) { + updateCache() + return + } + + const cached = fs.readFileSync(versionCache, 'utf-8') || '' + + if (cached === current) + return + + /** + * 当主题版本有更新时,清空缓存, + * 避免由于缓存问题,导致主题的更新内容无法生效 + */ + fs.rmSync(app.dir.cache(), { recursive: true }) + fs.rmSync(app.dir.temp(), { recursive: true }) + + updateCache() + } + catch {} +} + +const RE_FLAG = /^[\^~<>=]+/ + +function resolveVersion(version: string) { + if (RE_FLAG.test(version)) + return version.replace(RE_FLAG, '') + if (/^\d/.test(version)) + return version + + return '' +} diff --git a/theme/src/node/plugins/code.ts b/theme/src/node/plugins/code.ts index 8a81a34c..c1bc2e74 100644 --- a/theme/src/node/plugins/code.ts +++ b/theme/src/node/plugins/code.ts @@ -40,7 +40,7 @@ export function codePlugins(pluginOptions: ThemeBuiltinPlugins): PluginConfig { notationWordHighlight: true, highlightLines: true, collapsedLines: false, - langs: uniq([...twoslash ? ['ts', 'js', 'vue', 'json'] : [], ...langs]), + langs: uniq([...twoslash ? ['ts', 'js', 'vue', 'json', 'bash', 'sh'] : [], ...langs]), codeBlockTitle: (title, code) => { const icon = getIcon(title) return `
${icon ? `` : ''}${title}
${code}
` diff --git a/theme/src/node/theme.ts b/theme/src/node/theme.ts index ffc45e01..436dd532 100644 --- a/theme/src/node/theme.ts +++ b/theme/src/node/theme.ts @@ -12,7 +12,7 @@ import { setupProvideData, templateBuildRenderer, } from './config/index.js' -import { detectThemeOptions } from './detector/index.js' +import { detectThemeOptions, detectVersions } from './detector/index.js' import { initConfigLoader, waitForConfigLoaded, watchConfigFile } from './loadConfig/index.js' import { createPages, extendsPageData } from './pages/index.js' import { setupPlugins } from './plugins/index.js' @@ -25,6 +25,8 @@ export function plumeTheme(options: ThemeOptions = {}): Theme { setTranslateLang(app.options.lang) perf.init(app.env.isDebug) + detectVersions(app) + const { configFile, plugins, themeOptions } = detectThemeOptions(options) initConfigLoader(app, {