2025-09-05 22:40:22 +08:00

202 lines
6.8 KiB
TypeScript

import type { IPluginAPI } from '@fesjs/shared';
import type webpack from 'webpack';
import type { WebpackBuildConfig } from '../../shared';
import { existsSync, readFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import zlib from 'node:zlib';
import { chalk, rimraf } from '@fesjs/utils';
import UI from 'cliui';
import getConfig from './webpackConfig';
interface GetBundleAndConfigsOptions {
api: IPluginAPI<WebpackBuildConfig>;
}
interface GetBundleAndConfigsResult {
bundleConfig: webpack.Configuration;
}
export async function getBundleAndConfigs({ api }: GetBundleAndConfigsOptions): Promise<GetBundleAndConfigsResult> {
// get config
const env = api.env === 'production' ? 'production' : 'development';
const getConfigOpts: any = await api.applyPlugins({
type: api.ApplyPluginsType.modify,
key: 'modifyBundleConfigOpts',
initialValue: {
cwd: api.paths.cwd,
config: api.config,
env,
entry: {
index: join(api.paths.absTmpPath, 'fes.js'),
},
async modifyBabelOpts(opts: any) {
return api.applyPlugins({
type: api.ApplyPluginsType.modify,
key: 'modifyBabelOpts',
initialValue: opts,
});
},
async modifyBabelPresetOpts(opts: any) {
return api.applyPlugins({
type: api.ApplyPluginsType.modify,
key: 'modifyBabelPresetOpts',
initialValue: opts,
});
},
async chainWebpack(webpackConfig: any, opts: any) {
return api.applyPlugins({
type: api.ApplyPluginsType.modify,
key: 'chainWebpack',
initialValue: webpackConfig,
args: {
...opts,
},
});
},
async headScripts() {
return api.applyPlugins({
key: 'addHTMLHeadScripts',
type: api.ApplyPluginsType.add,
initialValue: [],
});
},
publicPath: await api.applyPlugins({
key: 'modifyPublicPathStr',
type: api.ApplyPluginsType.modify,
initialValue: api.config.publicPath || '',
args: {},
}),
},
args: {},
});
const bundleConfig: webpack.Configuration = await api.applyPlugins({
type: api.ApplyPluginsType.modify,
key: 'modifyBundleConfig',
initialValue: await getConfig({ api, ...getConfigOpts }),
args: {},
});
return { bundleConfig };
}
interface CleanTmpPathExceptCacheOptions {
absTmpPath: string;
}
export function cleanTmpPathExceptCache({ absTmpPath }: CleanTmpPathExceptCacheOptions) {
rimraf.sync(absTmpPath);
}
// These sizes are pretty large. We'll warn for bundles exceeding them.
const WARN_AFTER_BUNDLE_GZIP_SIZE = 1.8 * 1024 * 1024;
const WARN_AFTER_CHUNK_GZIP_SIZE = 1 * 1024 * 1024;
interface PrintFileSizesOptions {
stats: webpack.Stats;
dir: string;
}
export function printFileSizes({ stats, dir }: PrintFileSizesOptions) {
const ui = UI({ width: 80 });
const json: any = stats.toJson({
hash: false,
modules: false,
chunks: false,
});
const filesize = (bytes: number) => {
bytes = Math.abs(bytes);
const radix = 1024;
const unit = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
let loop = 0;
// calculate
while (bytes >= radix) {
bytes /= radix;
++loop;
}
return `${bytes.toFixed(1)} ${unit[loop]}`;
};
const assets: any[] = json.assets ? json.assets : json?.children?.reduce((acc: any, child: any) => acc.concat(child?.assets), []);
const seenNames = new Map();
const isJS = (val: string) => /\.js$/.test(val);
const isCSS = (val: string) => /\.css$/.test(val);
const orderedAssets: any[] = assets
.map((a: any) => {
a.name = a.name.split('?')[0];
// These sizes are pretty large
const isMainBundle = a.name.indexOf('fes.') === 0;
const maxRecommendedSize = isMainBundle ? WARN_AFTER_BUNDLE_GZIP_SIZE : WARN_AFTER_CHUNK_GZIP_SIZE;
const isLarge = maxRecommendedSize && a.size > maxRecommendedSize;
return {
...a,
suggested: isLarge && isJS(a.name),
};
})
.filter((a: any) => {
if (seenNames.has(a.name)) {
return false;
}
seenNames.set(a.name, true);
return isJS(a.name) || isCSS(a.name);
})
.sort((a: any, b: any) => {
if (isJS(a.name) && isCSS(b.name)) {
return -1;
}
if (isCSS(a.name) && isJS(b.name)) {
return 1;
}
return b.size - a.size;
});
function getGzippedSize(asset: any) {
const filepath = resolve(join(dir, asset.name));
if (existsSync(filepath)) {
const buffer = readFileSync(filepath);
return filesize(zlib.gzipSync(buffer).length);
}
return filesize(0);
}
function makeRow(a: string, b: string, c: string) {
return ` ${a}\t ${b}\t ${c}`;
}
ui.div(
`${makeRow(chalk.cyan.bold('File'), chalk.cyan.bold('Size'), chalk.cyan.bold('Gzipped'))}\n\n${orderedAssets
.map((asset: any) =>
makeRow(
asset.name.endsWith('js')
? asset.suggested
? chalk.yellow(join(dir, asset.name))
: chalk.green(join(dir, asset.name))
: chalk.blue(join(dir, asset.name)),
filesize(asset.size),
getGzippedSize(asset),
),
)
.join('\n')}`,
);
// eslint-disable-next-line no-console
console.log(`${ui.toString()}\n\n ${chalk.gray('Images and other types of assets omitted.')}\n`);
if (orderedAssets?.some((asset: any) => asset.suggested)) {
// eslint-disable-next-line no-console
console.log();
// eslint-disable-next-line no-console
console.log(chalk.yellow('The bundle size is significantly larger than recommended.'));
// eslint-disable-next-line no-console
console.log(chalk.yellow('Consider reducing it with code splitting'));
// eslint-disable-next-line no-console
console.log(chalk.yellow('You can also analyze the project dependencies using ANALYZE=1'));
// eslint-disable-next-line no-console
console.log();
}
}