@investravis.com/hexo-goose-builder
Version:
An exploratory plugin that aims to introduce a theme builder for Hexo, which supports modular development in the theme building process and supports popular ESM scripts and TailwindCSS, etc.
493 lines (418 loc) • 17 kB
JavaScript
'use strict';
const fs = require('fs');
const path = require('path');
const Utils = require('./lib/utils');
const ComponentJSBundler = require('./lib/js-bundler');
const TailwindCompiler = require('./lib/tailwind-compiler');
const ModeFactory = require('./lib/mode-factory');
const Banner = require('./lib/banner');
const chalk = require('chalk');
const yaml = require('js-yaml');
class ThemeBuilder {
constructor(hexo) {
this.hexo = hexo;
this.hasCompiled = false;
this.isCompiling = false;
this.themeConfig = null;
// 检测当前执行模式
this.currentMode = this.detectExecutionMode();
// 初始化主题配置
this.loadThemeConfig();
// 初始化构建器组件
this.jsBundler = new ComponentJSBundler(hexo);
this.tailwindCompiler = new TailwindCompiler(hexo);
this.banner = new Banner();
// 创建对应的模式处理器
this.modeHandler = ModeFactory.createHandler(this.currentMode, this);
// 绑定方法到实例
this.compileAssets = this.compileAssets.bind(this);
this.loadThemeConfig = this.loadThemeConfig.bind(this);
this.clearCompileCache = this.clearCompileCache.bind(this);
// 注册Hexo事件处理器
this.registerHexoEvents();
// 显示初始化消息
this.banner.showStatus(this.currentMode, '初始化中...', 'info');
this.logDebug(`模式特性: ${ModeFactory.getModeDescription(this.currentMode)}`);
// 在静态生成模式下确保初始状态正确
if (this.isStaticGenerationMode()) {
this.logInfo(`检测到${this.currentMode}模式,重置编译状态...`);
this.hasCompiled = false;
this.isCompiling = false;
}
}
// 检查是否启用调试模式
isDebugEnabled() {
const config = this.hexo.config;
return config && config.theme_builder && config.theme_builder.debug === true;
}
// 统一的日志输出方法
logInfo(message) {
// 总是显示重要信息
console.log(chalk.blue(`[Theme Builder] ${message}`));
}
logSuccess(message) {
// 总是显示成功信息
console.log(chalk.green(`[Theme Builder] ✓ ${message}`));
}
logError(message, error = null) {
// 总是显示错误信息
console.error(chalk.red(`[Theme Builder] ❌ ${message}`), error || '');
}
logWarning(message) {
// 总是显示警告信息
console.warn(chalk.yellow(`[Theme Builder] ⚠ ${message}`));
}
logDebug(message) {
// 只在调试模式下显示详细信息
if (this.isDebugEnabled()) {
console.log(chalk.gray(`[Theme Builder] ${message}`));
}
}
// 检测执行模式
detectExecutionMode() {
const cmd = this.hexo.env.cmd;
if (cmd === 'server' || cmd === 's') {
return 'server';
} else if (cmd === 'generate' || cmd === 'g') {
return 'generate';
} else if (cmd === 'deploy' || cmd === 'd') {
return 'deploy';
} else {
return cmd || 'unknown';
}
}
// 检查是否为服务器模式
isServerMode() {
return this.currentMode === 'server';
}
// 检查是否为静态生成模式(包括generate和deploy)
isStaticGenerationMode() {
return this.currentMode === 'generate' || this.currentMode === 'deploy';
}
// 检查是否为部署模式
isDeployMode() {
return this.currentMode === 'deploy';
}
// 加载主题配置
loadThemeConfig() {
try {
const configPath = path.join(this.hexo.theme_dir, '_config.yml');
if (fs.existsSync(configPath)) {
const configContent = fs.readFileSync(configPath, 'utf8');
this.themeConfig = yaml.load(configContent);
this.logSuccess('成功加载主题配置');
if (this.isDebugEnabled()) {
this.logDebug('主题配置: ' + JSON.stringify(this.themeConfig, null, 2));
}
} else {
this.logWarning('主题配置文件不存在: ' + configPath);
this.themeConfig = {};
}
} catch (error) {
this.logError('加载主题配置失败:', error);
this.themeConfig = {};
}
}
// 获取主题配置
getThemeConfig() {
return this.themeConfig || {};
}
// 显示欢迎banner
showWelcomeBanner() {
this.banner.show(this.currentMode);
}
// 编译资源文件
async compileAssets() {
// 防止重复编译
if (this.isCompiling) {
this.logDebug(`编译正在进行中(${this.currentMode}模式),等待完成...`);
// 等待当前编译完成
while (this.isCompiling) {
await new Promise(resolve => setTimeout(resolve, 100));
}
return this.hasCompiled;
}
this.isCompiling = true;
this.logInfo(`开始编译主题资源(${this.currentMode}模式)...`);
try {
// 检查主题目录是否存在
if (!fs.existsSync(this.hexo.theme_dir)) {
throw new Error(`主题目录不存在: ${this.hexo.theme_dir}`);
}
// 为部署模式提供额外信息
if (this.isDeployMode()) {
console.log(chalk.cyan('[Theme Builder] Deploy模式:确保所有资源都是最新编译状态...'));
}
// 先编译 CSS,因为 JS 可能依赖于生成的样式类
this.logInfo(`编译 TailwindCSS(${this.currentMode}模式)...`);
const cssOutputPath = await this.tailwindCompiler.compile();
if (cssOutputPath) {
this.logSuccess(`TailwindCSS编译完成(${this.currentMode}模式)`);
// 等待文件系统同步 - 确保文件写入完成
await new Promise(resolve => setTimeout(resolve, 200));
// 再编译 JS
this.logInfo(`编译 JS组件(${this.currentMode}模式)...`);
const bundleResult = await this.jsBundler.bundle();
if (bundleResult) {
this.logSuccess(`JS组件打包完成(${this.currentMode}模式)`);
} else {
this.logWarning(`JS组件打包未成功(${this.currentMode}模式),但继续执行`);
}
} else {
this.logWarning(`TailwindCSS编译未成功(${this.currentMode}模式)`);
}
this.hasCompiled = true;
// 为不同模式提供不同的完成信息
if (this.isDeployMode()) {
this.banner.showComplete(this.currentMode, '编译');
this.logSuccess(`Deploy模式:所有资源编译完成,准备部署`);
} else if (this.isStaticGenerationMode()) {
this.banner.showComplete(this.currentMode, '编译');
this.logSuccess(`${this.currentMode}模式:所有资源编译完成`);
} else {
this.banner.showComplete(this.currentMode, '编译');
this.logSuccess(`所有资源编译完成`);
}
return true;
} catch (error) {
this.banner.showError(this.currentMode, '资源编译失败');
this.logError(`资源编译失败(${this.currentMode}模式):`, error);
this.hasCompiled = false;
throw error; // 重新抛出错误供调用者处理
} finally {
this.isCompiling = false;
}
}
// 验证编译后的资源文件是否存在
async verifyCompiledAssets() {
this.logInfo(`验证编译后的资源文件(${this.currentMode}模式)...`);
try {
const cssDir = path.join(this.hexo.theme_dir, 'source/css');
const jsDir = path.join(this.hexo.theme_dir, 'source/js');
let cssFound = false;
let jsFound = false;
// 检查CSS文件
if (fs.existsSync(cssDir)) {
const cssFiles = fs.readdirSync(cssDir);
const componentCssFiles = cssFiles.filter(file => {
return (
file.match(/^components\.styles\.[a-f0-9]{8}\.css$/) ||
file.match(/^components\.bundle\.[a-z0-9]{6}\.css$/) ||
file.match(/^component\.bundle\.[a-z0-9]{6}\.css$/)
);
});
cssFound = componentCssFiles.length > 0;
if (cssFound) {
this.logSuccess(`找到CSS文件(${this.currentMode}模式): ${componentCssFiles.join(', ')}`);
}
}
// 检查JS文件
if (fs.existsSync(jsDir)) {
const jsFiles = fs.readdirSync(jsDir);
const componentJsFiles = jsFiles.filter(file =>
file.startsWith('components.') && file.endsWith('.js') && !file.includes('loader')
);
jsFound = componentJsFiles.length > 0;
if (jsFound) {
this.logSuccess(`找到JS文件(${this.currentMode}模式): ${componentJsFiles.join(', ')}`);
}
}
if (!cssFound && !jsFound) {
this.logWarning(`未找到编译后的资源文件(${this.currentMode}模式),可能编译未成功`);
this.hasCompiled = false;
throw new Error(`编译验证失败(${this.currentMode}模式):未找到编译后的资源文件`);
} else {
// 为不同模式提供不同的验证通过信息
if (this.isDeployMode()) {
this.logSuccess(`Deploy模式:资源文件验证通过,可以安全部署`);
} else {
this.logSuccess(`${this.currentMode}模式:资源文件验证通过`);
}
}
} catch (error) {
this.logError(`验证编译资源时出错(${this.currentMode}模式):`, error);
throw error;
}
}
// 清理编译缓存
clearCompileCache() {
this.logInfo(`正在清理编译缓存(${this.currentMode}模式)...`);
try {
const themeSourceDir = path.join(this.hexo.theme_dir, 'source');
const cssDir = path.join(themeSourceDir, 'css');
const jsDir = path.join(themeSourceDir, 'js');
let clearedCount = 0;
// 清理CSS编译文件
if (fs.existsSync(cssDir)) {
const cssFiles = fs.readdirSync(cssDir);
const compiledCssFiles = cssFiles.filter(file => {
return (
file.match(/^components\.styles\.[a-f0-9]{8}\.css$/) ||
file.match(/^components\.bundle\.[a-z0-9]{6}\.css$/) ||
file.match(/^component\.bundle\.[a-z0-9]{6}\.css$/)
);
});
compiledCssFiles.forEach(file => {
const filePath = path.join(cssDir, file);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
this.logDebug(`已删除CSS缓存文件: ${file}`);
clearedCount++;
}
});
}
// 清理JS编译文件
if (fs.existsSync(jsDir)) {
const jsFiles = fs.readdirSync(jsDir);
const compiledJsFiles = jsFiles.filter(file => {
return file.startsWith('components.') && file.endsWith('.js');
});
compiledJsFiles.forEach(file => {
const filePath = path.join(jsDir, file);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
this.logDebug(`已删除JS缓存文件: ${file}`);
clearedCount++;
}
});
}
// 清理组件manifest文件
const manifestPath = path.join(jsDir, 'components.manifest.json');
if (fs.existsSync(manifestPath)) {
fs.unlinkSync(manifestPath);
this.logDebug(`已删除组件manifest文件`);
clearedCount++;
}
// 重置编译状态
this.hasCompiled = false;
this.isCompiling = false;
// 清除编译定时器
if (this.compileDebounceTimer) {
clearTimeout(this.compileDebounceTimer);
this.compileDebounceTimer = null;
}
// 为不同模式提供不同的完成信息
if (this.isDeployMode()) {
this.banner.showComplete(this.currentMode, '缓存清理');
this.logSuccess(`Deploy模式:编译缓存清理完成,已清理 ${clearedCount} 个文件,准备全新编译`);
} else {
this.banner.showComplete(this.currentMode, '缓存清理');
this.logSuccess(`${this.currentMode}模式:编译缓存清理完成,已清理 ${clearedCount} 个文件`);
}
} catch (error) {
this.logError(`清理编译缓存失败(${this.currentMode}模式):`, error);
}
}
// 注册Hexo事件处理器
registerHexoEvents() {
// 让模式处理器先注册其特定的事件(包括early ready事件)
if (this.modeHandler && typeof this.modeHandler.registerEvents === 'function') {
this.modeHandler.registerEvents();
}
// 在Hexo初始化完成时执行(在模式处理器的ready事件之后)
this.hexo.on('ready', async () => {
this.showWelcomeBanner();
// 使用模式处理器初始化
try {
await this.modeHandler.initialize();
} catch (error) {
this.logError('模式处理器初始化失败:', error);
}
});
// 在Hexo退出时清理资源
this.hexo.on('exit', () => {
if (this.modeHandler && typeof this.modeHandler.cleanup === 'function') {
this.modeHandler.cleanup();
}
});
// 处理进程信号
const handleExit = () => {
if (this.modeHandler && typeof this.modeHandler.cleanup === 'function') {
this.modeHandler.cleanup();
}
process.exit(0);
};
// 移除可能存在的旧处理器
process.removeListener('SIGINT', handleExit);
process.removeListener('SIGTERM', handleExit);
// 添加新的处理器
process.on('SIGINT', handleExit);
process.on('SIGTERM', handleExit);
}
// 获取资源HTML标签
getAssetTags() {
const tags = [];
try {
// 添加组件样式
const cssDir = path.join(this.hexo.theme_dir, 'source/css');
if (fs.existsSync(cssDir)) {
const cssFiles = fs.readdirSync(cssDir);
// 获取所有组件相关的CSS文件
const componentCssFiles = cssFiles.filter(file => {
// 匹配所有可能的组件样式文件命名模式
return (
file.match(/^components\.styles\.[a-f0-9]{8}\.css$/) || // 匹配 components.styles.xxxxxxxx.css
file.match(/^components\.bundle\.[a-f0-9]{6}\.css$/) || // 匹配 components.bundle.xxxxxx.css
file.match(/^component\.bundle\.[a-z0-9]{6}\.css$/) // 匹配 component.bundle.xxxxxx.css
);
});
// 按照文件名排序,确保加载顺序一致
componentCssFiles.sort().forEach(file => {
this.logDebug(`${this.currentMode}模式加载样式: ${file}`);
tags.push(`<link rel="stylesheet" href="/css/${file}">`);
});
if (componentCssFiles.length === 0) {
this.logWarning(`${this.currentMode}模式:未找到编译后的CSS文件`);
}
} else {
this.logWarning(`CSS目录不存在: ${cssDir}`);
}
// 添加组件脚本
const jsDir = path.join(this.hexo.theme_dir, 'source/js');
if (fs.existsSync(jsDir)) {
const files = fs.readdirSync(jsDir);
// 获取所有 components. 开头的 JS 文件
const componentFiles = files
.filter(file => file.startsWith('components.') && file.endsWith('.js') && !file.includes('loader'))
.sort((a, b) => {
// 尝试从文件名中提取数字进行排序
const getNumber = (filename) => {
const match = filename.match(/components\.([^.]+)/);
return match ? match[1] : '';
};
const numA = getNumber(a);
const numB = getNumber(b);
return numA.localeCompare(numB);
});
// 添加所有组件脚本,使用type="module"
componentFiles.forEach(file => {
this.logDebug(`${this.currentMode}模式加载脚本: ${file}`);
tags.push(`<script type="module" src="/js/${file}"></script>`);
});
if (componentFiles.length === 0) {
this.logWarning(`${this.currentMode}模式:未找到编译后的JS文件`);
}
} else {
this.logWarning(`JS目录不存在: ${jsDir}`);
}
if (tags.length === 0) {
this.logWarning(`未找到任何编译后的资源文件(${this.currentMode}模式),请检查编译是否成功`);
// 让模式处理器处理资源缺失的情况
if (this.modeHandler && typeof this.modeHandler.handleGetAssetTags === 'function') {
this.modeHandler.handleGetAssetTags();
}
} else {
this.logSuccess(`${this.currentMode}模式:成功加载 ${tags.length} 个资源标签`);
}
} catch (error) {
this.logError(`获取资源标签时出错:`, error);
}
return tags;
}
}
// 创建主题构建器实例
const themeBuilder = new ThemeBuilder(hexo);
// 注册helper用于加载主题资源
hexo.extend.helper.register('load_theme_assets', () => {
return themeBuilder.getAssetTags().join('\n');
});