seitime-frappe/esbuild/esbuild.js
2021-05-01 16:04:11 +05:30

341 lines
7.8 KiB
JavaScript

let path = require("path");
let fs = require("fs");
let glob = require("fast-glob");
let esbuild = require("esbuild");
let vue = require("esbuild-vue");
let yargs = require("yargs");
let cliui = require("cliui")();
let chalk = require("chalk");
let html_plugin = require("./frappe-html");
let postCssPlugin = require("esbuild-plugin-postcss2").default;
let ignore_assets = require("./ignore-assets");
let sass_options = require("./sass_options");
let {
app_list,
assets_path,
apps_path,
sites_path,
get_app_path,
get_public_path,
log,
log_warn,
log_error,
bench_path
} = require("./utils");
let { get_redis_subscriber } = require("../node_utils");
let argv = yargs
.usage("Usage: node esbuild [options]")
.option("apps", {
type: "string",
description: "Run build for specific apps"
})
.option("watch", {
type: "boolean",
description: "Run in watch mode and rebuild on file changes"
})
.option("production", {
type: "boolean",
description: "Run build in production mode"
})
.example(
"node esbuild --apps frappe,erpnext",
"Run build only for frappe and erpnext"
)
.version(false).argv;
const APPS = !argv.apps ? app_list : argv.apps.split(",");
const WATCH_MODE = Boolean(argv.watch);
const PRODUCTION = Boolean(argv.production);
const TOTAL_BUILD_TIME = `${chalk.black.bgGreen(" DONE ")} Total Build Time`;
const NODE_PATHS = [].concat(
// node_modules of apps directly importable
app_list
.map(app => path.resolve(get_app_path(app), "../node_modules"))
.filter(fs.existsSync),
// import js file of any app if you provide the full path
app_list
.map(app => path.resolve(get_app_path(app), ".."))
.filter(fs.existsSync)
);
execute();
async function execute() {
console.time(TOTAL_BUILD_TIME);
await clean_dist_folders(APPS);
let result;
try {
result = await build_assets_for_apps(APPS);
} catch (e) {
log_error("There were some problems during build");
log();
log(chalk.dim(e.stack));
}
if (!WATCH_MODE) {
log_built_assets(result.metafile);
console.timeEnd(TOTAL_BUILD_TIME);
log();
} else {
log("Watching for changes...");
}
await write_meta_file(result.metafile);
}
function build_assets_for_apps(apps) {
let { include_patterns, ignore_patterns } = get_files_to_build(apps);
return glob(include_patterns, { ignore: ignore_patterns }).then(files => {
let output_path = assets_path;
let file_map = {};
for (let file of files) {
let relative_app_path = path.relative(apps_path, file);
let app = relative_app_path.split(path.sep)[0];
let extension = path.extname(file);
let output_name = path.basename(file, extension);
if (
[".css", ".scss", ".less", ".sass", ".styl"].includes(extension)
) {
output_name = path.join("css", output_name);
} else if ([".js", ".ts"].includes(extension)) {
output_name = path.join("js", output_name);
}
output_name = path.join(app, "dist", output_name);
if (Object.keys(file_map).includes(output_name)) {
log_warn(
`Duplicate output file ${output_name} generated from ${file}`
);
}
file_map[output_name] = file;
}
return build_files({
files: file_map,
outdir: output_path
});
});
}
function get_files_to_build(apps) {
let include_patterns = [];
let ignore_patterns = [];
for (let app of apps) {
let public_path = get_public_path(app);
include_patterns.push(
path.resolve(
public_path,
"**",
"*.bundle.{js,ts,css,sass,scss,less,styl}"
)
);
ignore_patterns.push(
path.resolve(public_path, "node_modules"),
path.resolve(public_path, "dist")
);
}
return {
include_patterns,
ignore_patterns
};
}
function build_files({ files, outdir }) {
return esbuild.build({
entryPoints: files,
entryNames: "[dir]/[name].[hash]",
outdir,
sourcemap: true,
bundle: true,
metafile: true,
minify: PRODUCTION,
nodePaths: NODE_PATHS,
define: {
"process.env.NODE_ENV": JSON.stringify(
PRODUCTION ? "production" : "development"
)
},
plugins: [
html_plugin,
ignore_assets,
vue(),
postCssPlugin({
plugins: [require("autoprefixer")],
sassOptions: sass_options
})
],
watch: WATCH_MODE
? {
onRebuild(error, result) {
if (error) {
log_error(
"There was an error during rebuilding changes."
);
log();
log(chalk.dim(error.stack));
notify_redis({ error });
} else {
console.log(
`${new Date().toLocaleTimeString()}: Compiled changes...`
);
write_meta_file(result.metafile);
notify_redis({ success: true });
}
}
}
: null
});
}
async function clean_dist_folders(apps) {
for (let app of apps) {
let public_path = get_public_path(app);
await fs.promises.rmdir(path.resolve(public_path, "dist", "js"), {
recursive: true
});
await fs.promises.rmdir(path.resolve(public_path, "dist", "css"), {
recursive: true
});
}
}
function log_built_assets(metafile) {
let column_widths = [60, 20];
cliui.div(
{
text: chalk.cyan.bold("File"),
width: column_widths[0]
},
{
text: chalk.cyan.bold("Size"),
width: column_widths[1]
}
);
cliui.div("");
let output_by_dist_path = {};
for (let outfile in metafile.outputs) {
if (outfile.endsWith(".map")) continue;
let data = metafile.outputs[outfile];
outfile = path.resolve(outfile);
outfile = path.relative(assets_path, outfile);
let filename = path.basename(outfile);
let dist_path = outfile.replace(filename, "");
output_by_dist_path[dist_path] = output_by_dist_path[dist_path] || [];
output_by_dist_path[dist_path].push({
name: filename,
size: (data.bytes / 1000).toFixed(2) + " Kb"
});
}
for (let dist_path in output_by_dist_path) {
let files = output_by_dist_path[dist_path];
cliui.div({
text: dist_path,
width: column_widths[0]
});
for (let i in files) {
let file = files[i];
let branch = "";
if (i < files.length - 1) {
branch = "├─ ";
} else {
branch = "└─ ";
}
let color = file.name.endsWith(".js") ? "green" : "blue";
cliui.div(
{
text: branch + chalk[color]("" + file.name),
width: column_widths[0]
},
{
text: file.size,
width: column_widths[1]
}
);
}
cliui.div("");
}
console.log(cliui.toString());
}
function write_meta_file(metafile) {
let out = {};
for (let output in metafile.outputs) {
let info = metafile.outputs[output];
let asset_path = "/" + path.relative(sites_path, output);
if (info.entryPoint) {
out[path.basename(info.entryPoint)] = asset_path;
}
}
let assets_json = JSON.stringify(out, null, 4);
return fs.promises
.writeFile(
path.resolve(assets_path, "frappe", "dist", "assets.json"),
assets_json
)
.then(() => {
let client = get_redis_subscriber("redis_cache");
// update assets_json cache in redis, so that it can be read directly by python
return client.set("assets_json", assets_json);
});
}
async function notify_redis({ error, success }) {
let subscriber = get_redis_subscriber("redis_socketio");
// notify redis which in turns tells socketio to publish this to browser
let payload = null;
if (error) {
let formatted = await esbuild.formatMessages(error.errors, {
kind: "error",
terminalWidth: 100
});
let stack = error.stack.replace(new RegExp(bench_path, "g"), "");
payload = {
error,
formatted,
stack
};
}
if (success) {
payload = {
success: true
};
}
subscriber.publish(
"events",
JSON.stringify({
event: "build_event",
message: payload
})
);
}
function open_in_editor() {
let subscriber = get_redis_subscriber("redis_socketio");
subscriber.on("message", (event, file) => {
if (event === "open_in_editor") {
file = JSON.parse(file);
let file_path = path.resolve(file.file);
console.log("Opening file in editor:", file_path);
let launch = require("launch-editor");
launch(`${file_path}:${file.line}:${file.column}`);
}
});
subscriber.subscribe("open_in_editor");
}
open_in_editor();