UNPKG

@msom/xbuild

Version:

@msom/xbuild

146 lines (128 loc) 4.37 kB
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, }); }, }; };