@msom/xbuild
Version:
@msom/xbuild
146 lines (128 loc) • 4.37 kB
text/typescript
import path from "path";
import fs from "fs";
import { RolldownPlugin } from "rolldown";
// HTML 入口解析插件 (适配 Rolldown 的 pluginutils)
export const htmlEntryPlugin = (): RolldownPlugin<unknown> => {
let htmlContent = "";
const processedAssets = new Set();
return {
name: "html-entry-plugin",
async buildStart(options) {
// 1. 读取 HTML 入口文件
const input = [options.input].flat()[0];
if (typeof input === "object" || !input.endsWith(".html")) {
return;
}
const htmlPath = path.resolve(process.cwd(), input);
console.log(htmlPath);
htmlContent = fs.readFileSync(htmlPath, "utf-8");
// 2. 解析 HTML 中的脚本资源
const scriptRegex =
/<script\s+[^>]*src\s*=\s*['"]([^'"]+\.(js|ts|jsx|tsx))['"][^>]*>/gi;
const scripts: string[] = [];
let match: RegExpExecArray | null;
while ((match = scriptRegex.exec(htmlContent)) !== null) {
const relativePath = match[1];
const scriptPath = path.resolve(path.dirname(htmlPath), relativePath);
scripts.push(scriptPath);
}
// 3. 设置动态入口
if (scripts.length > 0) {
// 将第一个脚本作为主入口
this.emitFile({
type: "chunk",
id: scripts[0],
fileName: path.basename(scripts[0]),
});
// 其他脚本作为额外入口点
for (let i = 1; i < scripts.length; i++) {
this.emitFile({
type: "chunk",
id: scripts[i],
});
}
}
},
async generateBundle(outputOptions, bundle) {
if (!htmlContent) {
return;
}
// 4. 资源注入处理器
const headInjection: string[] = [];
const cssLinks: string[] = [];
const jsScripts: string[] = [];
// 创建资源类型映射
const assetTypeMap = {
".css": "style",
".woff2": "font",
".woff": "font",
".ttf": "font",
".eot": "font",
".png": "image",
".jpg": "image",
".jpeg": "image",
".gif": "image",
".webp": "image",
".svg": "image",
};
// 遍历生成的文件
for (const [fileName, assetInfo] of Object.entries(bundle)) {
// 跳过已处理资源
if (processedAssets.has(fileName)) continue;
processedAssets.add(fileName);
const ext = path.extname(fileName).toLowerCase();
// 处理 CSS 资源
if (ext === ".css") {
cssLinks.push(`<link rel="stylesheet" href="${fileName}">`);
}
// 处理 JS 资源 (非入口文件)
else if (
ext === ".js" &&
assetInfo.type === "chunk" &&
!assetInfo.isEntry
) {
jsScripts.push(`<script src="${fileName}" defer></script>`);
}
// 处理其他静态资源
else if (assetTypeMap[ext]) {
headInjection.push(
`<link rel="preload" href="${fileName}" as="${assetTypeMap[ext]}" crossorigin>`
);
}
}
// 5. 更新 HTML 内容
let updatedHtml = htmlContent;
// 更新 script 引用
updatedHtml = updatedHtml.replace(
/<script\s+[^>]*src\s*=\s*['"]([^'"]+\.(js|ts|jsx|tsx))['"][^>]*>/gi,
(match, src) => {
// 查找对应的输出文件名
const outputFile = Object.keys(bundle).find((name) =>
name.startsWith(path.basename(src, path.extname(src)))
);
return outputFile
? match
.replace(src, outputFile)
.replace(/(type\s*=\s*['"])[^'"]*['"]/i, 'type="module"')
: match;
}
);
// 注入资源到 head
updatedHtml = updatedHtml.replace(
/<\/head>/i,
`${cssLinks.join("\n")}\n${headInjection.join("\n")}\n</head>`
);
// 注入非入口 JS 文件到 body 末尾
updatedHtml = updatedHtml.replace(
/<\/body>/i,
`${jsScripts.join("\n")}\n</body>`
);
// 6. 输出最终 HTML
this.emitFile({
type: "asset",
fileName: "index.html",
source: updatedHtml,
});
},
};
};