f2e-server3
Version:
f2e-server 3.0
290 lines (281 loc) • 10.3 kB
text/typescript
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 })
}