@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.
194 lines (167 loc) • 7.13 kB
JavaScript
;
const path = require('path');
const rollup = require('rollup');
const chalk = require('chalk');
const Utils = require('./utils');
const JsEncryption = require('./js-encryption');
class ESMProcessor {
constructor(config) {
this.config = config;
this.jsEncryption = new JsEncryption(config.hexo);
}
async bundleESM(files, componentsDir) {
try {
Utils.logInfo(this.config.hexo, '开始处理 ESM 文件...', 'ESM Processor');
// 创建入口点映射
const entryPoints = {};
files.forEach(file => {
const relativePath = path.relative(componentsDir, file);
const componentName = relativePath.split(path.sep)[0];
// 使用相对路径作为入口点名称,这样可以保持目录结构
const entryName = relativePath.replace(/\.js$/, '');
entryPoints[entryName] = file;
});
Utils.logDebug(this.config.hexo, '入口文件:', 'ESM Processor');
Object.entries(entryPoints).forEach(([name, file]) => {
Utils.logDebug(this.config.hexo, ` └─ ${name}`, 'ESM Processor');
});
const jsDir = this.config.getJsDir();
const cssDir = this.config.getCssDir();
Utils.ensureDirectoryExists(jsDir);
Utils.ensureDirectoryExists(cssDir);
// Rollup 配置
const bundle = await rollup.rollup({
input: entryPoints,
plugins: this.config.rollupConfig.plugins,
external: (id) => {
// 将 CDN 链接标记为外部依赖
if (id.startsWith('https://') || id.startsWith('http://')) {
return true;
}
// 其他外部依赖处理
return false;
},
onwarn: (warning, warn) => {
// 忽略循环依赖警告
if (warning.code === 'CIRCULAR_DEPENDENCY') return;
// 忽略空包警告
if (warning.code === 'EMPTY_BUNDLE') return;
// 忽略无法解析的 CDN 导入警告
if (warning.code === 'UNRESOLVED_IMPORT' &&
warning.source &&
(warning.source.startsWith('https://') || warning.source.startsWith('http://'))) {
Utils.logDebug(this.config.hexo, `外部 CDN 依赖: ${warning.source}`, 'ESM Processor');
return;
}
warn(warning);
}
});
Utils.logDebug(this.config.hexo, '生成输出...', 'ESM Processor');
// 输出配置
Utils.logDebug(this.config.hexo, '正在写入文件...', 'ESM Processor');
const { output } = await bundle.write({
dir: jsDir,
format: 'es',
entryFileNames: 'components.[hash].bundle.js',
chunkFileNames: (chunkInfo) => {
// 根据分块名称生成文件名
if (chunkInfo.name === 'vendor-framework') {
return 'components.vendor.framework.[hash].js';
}
if (chunkInfo.name === 'vendor-markdown') {
return 'components.vendor.markdown.[hash].js';
}
if (chunkInfo.name === 'vendor') {
return 'components.vendor.[hash].js';
}
if (chunkInfo.name === 'shared') {
return 'components.shared.[hash].js';
}
// 其他模块代码
return 'components.[hash].chunk.js';
},
manualChunks(id) {
// 如果是 node_modules 中的模块,统一放入 vendor
if (id.includes('node_modules')) {
// 根据依赖类型进行分组
if (id.includes('vue')) {
return 'vendor-framework';
}
if (id.includes('katex') || id.includes('marked')) {
return 'vendor-markdown';
}
return 'vendor';
}
// 工具函数和共享代码
if (id.includes('/utils/') || id.includes('/shared/') || id.includes('/helpers/')) {
return 'shared';
}
// 其他模块保持独立
return null;
}
});
// 处理JS文件加密和压缩
const processedOutput = [];
for (const chunk of output) {
if (chunk.type === 'chunk' && chunk.code) {
// 对JS代码进行加密处理
const processedCode = await this.jsEncryption.process(chunk.code, chunk.fileName);
// 更新chunk中的代码
chunk.code = processedCode;
// 写入处理后的文件
const filePath = path.join(jsDir, chunk.fileName);
require('fs').writeFileSync(filePath, processedCode);
}
processedOutput.push(chunk);
}
// 创建manifest
const manifest = {};
processedOutput.forEach(chunk => {
if (chunk.isEntry) {
const componentName = path.basename(chunk.facadeModuleId).split(path.sep)[0];
manifest[componentName] = {
file: chunk.fileName,
imports: chunk.imports,
dynamicImports: chunk.dynamicImports
};
}
});
// 保存manifest
const manifestPath = path.join(jsDir, 'components.manifest.json');
require('fs').writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
// 处理CSS输出
processedOutput.forEach(chunk => {
if (chunk.type === 'asset' && chunk.fileName.endsWith('.css')) {
// CSS文件已经由postcss插件写入到正确的位置
const relativePath = path.relative(this.config.hexo.theme_dir, this.config.cssFullPath)
.split(path.sep)
.join('/');
Utils.logSuccess(this.config.hexo, `CSS文件已生成: ${relativePath} (${Utils.formatSize(chunk.source.length)})`, 'ESM Processor');
}
});
await bundle.close();
// 输出文件信息
const jsFiles = processedOutput.filter(chunk => chunk.type === 'chunk');
const cssFiles = processedOutput.filter(chunk => chunk.type === 'asset' && chunk.fileName.endsWith('.css'));
Utils.logDebug(this.config.hexo, '文件生成统计:', 'ESM Processor');
jsFiles.forEach(chunk => {
const size = chunk.code ? Utils.formatSize(chunk.code.length) : '0B';
if (chunk.isEntry) {
Utils.logDebug(this.config.hexo, ` ├─ 入口文件: ${chunk.fileName} (${size})`, 'ESM Processor');
} else {
Utils.logDebug(this.config.hexo, ` ├─ 分块文件: ${chunk.fileName} (${size})`, 'ESM Processor');
}
});
cssFiles.forEach(chunk => {
const size = chunk.source ? Utils.formatSize(chunk.source.length) : '0B';
Utils.logDebug(this.config.hexo, ` ├─ CSS文件: ${chunk.fileName} (${size})`, 'ESM Processor');
});
Utils.logSuccess(this.config.hexo, `ESM模块处理完成,输出 ${jsFiles.length} 个JS文件,${cssFiles.length} 个CSS文件`, 'ESM Processor');
return processedOutput;
} catch (error) {
Utils.logError(this.config.hexo, 'Rollup 打包错误:', error, 'ESM Processor');
return [];
}
}
}
module.exports = ESMProcessor;