import { minify } from "../deps/terser.ts";
import { merge } from "../core/utils/object.ts";
import { Page } from "../core/file.ts";
import { prepareAsset, saveAsset } from "./source_maps.ts";
import { log, warnUntil } from "../core/utils/log.ts";
import { concurrent } from "../core/utils/concurrent.ts";
import { bytes, percentage } from "../core/utils/format.ts";
import type Site from "../core/site.ts";
import type { MinifyOptions } from "../deps/terser.ts";
import type { Item } from "../deps/debugbar.ts";
export interface Options {
/** File extensions to minify */
extensions?: string[];
/**
* Options passed to `terser`
* @see https://terser.org/docs/api-reference/#minify-options
*/
options?: MinifyOptions;
}
// Default options
export const defaults: Options = {
extensions: [".js"],
options: {
module: true,
compress: true,
mangle: true,
},
};
/**
* A plugin to minify them using Terser
* @see https://lume.land/plugins/terser/
*/
export function terser(userOptions?: Options) {
const options = merge(defaults, userOptions);
return (site: Site) => {
site.filter("terser", filter, true);
site.process(options.extensions, function processTerser(files: Page[]) {
const hasPages = warnUntil(
"[terser plugin] No files found. Make sure to add the JS files with site.add()",
files.length,
);
if (!hasPages) {
return;
}
const item = site.debugBar?.buildItem(
"[terser plugin] minification completed",
);
return concurrent(files, (file) => terser(file, item));
});
async function terser(page: Page, item?: Item) {
const { content, filename, sourceMap, enableSourceMap } = prepareAsset(
site,
page,
);
const terserOptions = {
...options.options,
sourceMap: enableSourceMap
? {
content: JSON.stringify(sourceMap),
filename: filename,
}
: undefined,
};
try {
const output = await minify({ [filename]: content }, terserOptions);
if (item && output.code) {
item.items ??= [];
const old = content.length;
const minified = output.code.length;
item.items.push({
title: `[${percentage(old, minified)}] ${page.data.url}`,
details: `${bytes(page.bytes.length)}`,
});
}
saveAsset(
site,
page,
output.code!,
// @ts-expect-error: terser uses @jridgewell/gen-mapping, which incorrectly has typed some types as nullable: https://github.com/jridgewell/gen-mapping/pull/9
output.map,
);
} catch (cause) {
log.error(
`[terser plugin] Error processing the file: ${filename} by the Terser plugin. (${cause})`,
);
}
}
async function filter(code: string): Promise {
const output = await minify(code, options.options);
return output.code;
}
};
}
export default terser;
/** Extends Helpers interface */
declare global {
namespace Lume {
export interface Helpers {
/** @see https://lume.land/plugins/terser/#the-terser-filter */
terser: (code: string) => Promise;
}
}
}