UNPKG

f2e-server3

Version:

f2e-server 3.0

290 lines (281 loc) 10.3 kB
import { BuildOptions, BuildResult } from "esbuild" import { MemoryTree } from "../../memory-tree/interface" import { F2EConfigResult } from "../../interface" import * as path from 'node:path' import * as fs from 'node:fs' import * as _ from '../../utils/misc' import { build_css_paths, build_external_file, default_config, generate_banner_code, generate_filename, generate_global_name, generate_hash, generate_inject_code, getEntryPaths } from "./utils" import { dynamicImport, logger } from "../../utils" export interface SaveParams { store: MemoryTree.Store result: BuildResult conf: F2EConfigResult } export const save = async function (params: SaveParams) { const { store, result, conf } = params const { root, esbuild } = conf if (!esbuild) return const with_metafile = esbuild.with_metafile const { outputFiles = [], metafile } = result for (const outputFile of outputFiles) { const outputPath = _.pathname_fixer(path.relative(root, outputFile.path)) const meta = metafile?.outputs?.[outputPath] const originPath = meta?.entryPoint || outputPath store.save({ data: outputFile.text, originPath, outputPath, }) /** js文件索引meta信息 */ if ( with_metafile && metafile && /\.js$/.test(outputPath)) { store.save({ originPath: outputPath + '.json', outputPath: outputPath + '.json', data: JSON.stringify(metafile, null, 2) }) } } } export interface BuildExternalOptions { conf: F2EConfigResult; store: MemoryTree.Store; option: BuildOptions; lib_hash: string; } export const external_build = async function ({ conf, store, option, lib_hash, }: BuildExternalOptions) { const { external = [], ..._option } = option if (external.length === 0 || !conf.esbuild || !conf.esbuild.build_external) { return } const { cache_root = default_config.cache_root, inject_global_name = default_config.inject_global_name, external_lib_name = default_config.external_lib_name, } = conf.esbuild const filename = generate_filename(external_lib_name, lib_hash) if (store.origin_map.has(cache_root + '/' + filename)) { // 已经编译过的不再编译 return } build_external_file({ filename, conf: conf.esbuild, modules: external, lib_hash, }) const originPath = _.pathname_fixer(cache_root + '/' + filename) const outputPath = _.pathname_fixer((option.outdir || '') + '/' + filename.replace(/\.ts$/, '.js')) const cachePath = path.join(cache_root, filename.replace(/\.ts$/, '.js')) if (fs.existsSync(cachePath)) { store.save({ data: fs.readFileSync(cachePath, 'utf-8'), originPath, outputPath, }) if (fs.existsSync(cachePath.replace(/\.js$/, '.js.map'))) { store.save({ data: fs.readFileSync(cachePath.replace(/\.js$/, '.js.map'), 'utf-8'), originPath: originPath + '.map', outputPath: outputPath + '.map', }) } return } const builder: typeof import('esbuild') = await dynamicImport('esbuild') const result = await builder.build({ ..._option, entryPoints: [cache_root + '/' + filename], bundle: true, format: 'iife', banner: { 'js': generate_inject_code(inject_global_name, lib_hash, conf.mode === 'dev'), }, }) for (const outputFile of result.outputFiles || []) { if (outputFile.path.endsWith('.js')) { fs.writeFile(cachePath, outputFile.text, (err) => { if (err) { logger.error(err) } }) store.save({ data: outputFile.text, originPath, outputPath, }) } if (outputFile.path.endsWith('.js.map')) { fs.writeFile(cachePath.replace(/\.js$/, '.js.map'), outputFile.text, (err) => { if (err) { logger.error(err) } }) store.save({ data: outputFile.text, originPath: originPath + '.map', outputPath: outputPath + '.map', }) } } } export interface OriginInfo { lib_hash: string; css_paths: string[]; hot_modules: string[]; error?: any; rebuilds: Set<{(): Promise<void>}>; } export const origin_map = new Map<string, OriginInfo>() export const build_origin_map = function ({ result, rebuild, store, lib_hash, hot_modules = [], root, }: { lib_hash: string; hot_modules: string[]; store: MemoryTree.Store; result?: BuildResult; rebuild?: {(): Promise<void>}; root: string; }) { if (!result) return const outputs = result?.metafile?.outputs const css_paths = build_css_paths(result, root) outputs && Object.entries(outputs).map(([filename, meta]) => { if (filename.endsWith('.js')) { Object.keys(meta.inputs || {}).forEach(_inputPath => { if (_inputPath.includes('node_modules')) return const inputPath = _.pathname_fixer(_inputPath) const found = origin_map.get(inputPath) || { lib_hash, css_paths, hot_modules, rebuilds: new Set(), } if (rebuild) { found.rebuilds.add(rebuild) } found.css_paths = css_paths origin_map.set(inputPath, found) store.ignores.add(inputPath) }) } }) } export interface BuildIntoStoreOptions { store: MemoryTree.Store; _option: BuildOptions; hot_modules?: string[]; conf: F2EConfigResult; } export const build_option = async ({ store, _option, conf, }: BuildIntoStoreOptions) => { if (!conf.esbuild) return const { mode, esbuild: { build_external = default_config.build_external, inject_global_name = default_config.inject_global_name, } } = conf const option = { write: false, metafile: true, sourcemap: true, ..._option, minify: mode === 'build', } const builder: typeof import('esbuild') = await dynamicImport('esbuild') const lib_hash = option.external && option.external.length > 0 ? generate_hash(option.external, conf.system_hash) : '' const with_libs = build_external && option.format === 'iife' && !!lib_hash if (with_libs) { await external_build({conf, store, option, lib_hash}) delete option.inject } const GLOBAL_NAME = generate_global_name(inject_global_name, lib_hash) const result = await builder.build({ ...option, banner: with_libs ? { ...(option.banner || {}), js: `${option.banner?.js || ''};${generate_banner_code(inject_global_name, lib_hash)}` } : option.banner, define: { 'process.env.NODE_ENV': mode === 'dev' ? '"development"' : '"production"', ...(option.define || {}), 'import.meta.hot': `${GLOBAL_NAME}.hot`, }, }) build_origin_map({ result, store, lib_hash, hot_modules: [], root: conf.root }) await save({ store, result, conf }) } export const watch_option = async ({ store, _option, hot_modules = [], conf, }: Required<BuildIntoStoreOptions>) => { if (!conf.esbuild) return const { mode, esbuild: { build_external = default_config.build_external, inject_global_name = default_config.inject_global_name, } } = conf const option = { write: false, metafile: true, sourcemap: true, ..._option, minify: _option.minify || mode === 'build', } const builder: typeof import('esbuild') = await dynamicImport('esbuild') const lib_hash = option.external && option.external.length > 0 ? generate_hash(option.external, conf.system_hash) : '' const with_libs = build_external && option.format === 'iife' && !!lib_hash if (with_libs) { await external_build({conf, store, option, lib_hash}) delete option.inject } const GLOBAL_NAME = generate_global_name(inject_global_name, lib_hash) const ctx = await builder.context({ ...option, external: hot_modules.concat(option.external || []), banner: with_libs ? { ...(option.banner || {}), js: `${option.banner?.js || ''};${generate_banner_code(inject_global_name, lib_hash)}` } : option.banner, define: { 'process.env.NODE_ENV': mode === 'dev' ? '"development"' : '"production"', ...(option.define || {}), 'import.meta.hot': `${GLOBAL_NAME}.hot`, }, }) const rebuild = async function rebuild () { try { const result = await ctx.rebuild() build_origin_map({ result, rebuild, store, lib_hash, hot_modules, root: conf.root }) logger.debug( `[esbuild] ${JSON.stringify(option.entryPoints)} rebuild`, // [...origin_map.keys()].filter(k => !k.includes('node_modules')) ) await save({ store, result, conf }) } catch (error) { getEntryPaths(option.entryPoints).forEach(({in: originPath, out: outputPath}) => { store.save({ error, originPath, data: undefined, outputPath, }) }) } } const result = await ctx.rebuild() build_origin_map({ result, rebuild, store, lib_hash, hot_modules, root: conf.root }) await save({ store, result, conf }) }