import { MiddlewareCreater } from "../interface"; import * as path from 'node:path' import * as fs from 'node:fs' import logger from "../../utils/logger"; import { exit } from "node:process"; import * as _ from '../../utils/misc' import { MemoryTree } from "../../memory-tree/interface"; import type { BuildOptions, BuildResult } from 'esbuild' import { build_option, origin_map, watch_option } from './store' import { build_external_file, default_config, generate_filename, generate_hash, getEntryPaths } from "./utils"; import { createResponseHelper, dynamicImport } from "../../utils"; import { page_esbuild_analyze } from "../../utils/templates"; const middleware_esbuild: MiddlewareCreater = { name: 'esbuild', mode: ['dev', 'build'], execute: async (conf) => { const { root, esbuild: esbuildConfig, mode } = conf // prod 模式下不加载esbuild if (!esbuildConfig) { return } const { analyze_page_path = default_config.analyze_page_path, esbuildrc = default_config.esbuildrc, external_lib_name = default_config.external_lib_name, reg_inject = default_config.reg_inject, reg_replacer = default_config.reg_replacer, cache_root = default_config.cache_root, } = esbuildConfig const conf_path = path.join(root, esbuildrc ) // 使用默认配置,检查配置文件是否存在,不存在时,中间件失效 if (!fs.existsSync(conf_path)) { // logger.debug(`[esbuild] esbuildrc: ${conf_path} not found, esbuild middleware disabled`) return } try { dynamicImport(conf_path) } catch (e) { logger.error(`[esbuild] esbuildrc: ${conf_path} is not valid:`, e) exit(1) } // 检查esbuild依赖 dynamicImport('esbuild').catch(e => { logger.error(`[esbuild] esbuild not found, esbuild middleware disabled`) exit(1) }) const esbuildConfigs = await dynamicImport(conf_path) const esbuildOptions = ([] as (BuildOptions & { hot_modules?: string[] }[])).concat(esbuildConfigs).map(option => ({ ...option, ...(esbuildConfig.esbuildOptions || default_config.esbuildOptions), })) const metafile_map = new Map() const entry_set = new Set() const build = async function (store: MemoryTree.Store) { await Promise.all(esbuildOptions.map(async (option) => { delete option.hot_modules await build_option({ metafile_map, store, conf, _option: option, }) })) } const watch = async function (store: MemoryTree.Store) { await Promise.all(esbuildOptions.map(async (option) => { const lib_hash = generate_hash(option.external || [], conf.system_hash) const hot_modules = option.hot_modules || [] delete option.hot_modules const entryPaths = getEntryPaths(option.entryPoints) const options = entryPaths.map((entry) => { entry_set.add(entry.in) return { ...option, entryPoints: { [entry.out]: entry.in, }, } }) return Promise.all(hot_modules.map(async (moduleId) => { const filename = generate_filename(external_lib_name, moduleId.replace(/[\\\/]/g, '_')); build_external_file({ filename, conf: esbuildConfig, modules: [moduleId], lib_hash, }) await watch_option({ metafile_map, store, hot_modules: [], conf, _option: { ...option, entryPoints: [ cache_root + '/' + filename], }, }) }).concat( options.map((option) => { return watch_option({ metafile_map, store, hot_modules, conf, _option: option, }) }) )) })) } const { handleSuccess } = createResponseHelper(conf) return { async onMemoryInit (store) { return mode === 'dev' ? await watch(store) : await build(store) }, async onSet(pathname, data, store) { const result = { originPath: pathname, outputPath: pathname, data, } if (reg_inject.test(pathname) && data) { result.data = data.toString() // 原来正则在某些情况下会导致v8(node和chrome)卡死,JavaScriptCore(bun和safari)不会,所以这里用简单正则替换 .replace(reg_replacer || /\s*<\/script\>/g, function (___, src) { const key = _.pathname_fixer('/' === src.charAt(0) ? src : path.join(path.dirname(pathname), src)) const item = origin_map.get(key) let scripts: string[] = [] if (item) { item.css_paths?.forEach(cssPath => { scripts.push(`\n`) }) if (item.lib_hash) { const sourcefile = generate_filename(external_lib_name, item.lib_hash) scripts.push(`\n`) } item.hot_modules?.forEach(moduleId => { const sourcefile = generate_filename(external_lib_name, moduleId.replace(/[\\\/]/g, '_')); scripts.push(`\n`) }) } return scripts.join('') + ___ }) } return result }, async onRoute(pathname, ctx) { if (pathname.startsWith(analyze_page_path)) { const entry = decodeURIComponent(pathname.replace(analyze_page_path, '').slice(1)); const item = entry_set.has(entry) ? metafile_map.get(entry) : undefined if (item) { handleSuccess(ctx, '.json', JSON.stringify(item)) } else { handleSuccess(ctx, '.htm', _.template(page_esbuild_analyze, { meta_list: Array.from(entry_set).sort().map((name) => ({ name, path: `/${analyze_page_path}/${encodeURIComponent(name)}`, })), })) } return false; } }, async buildWatcher(pathname, eventType, build, store) { const item = origin_map.get(pathname) if (item && item.rebuilds.size > 0) { for (const rebuild of item.rebuilds) { await rebuild() } } }, } } } export default middleware_esbuild