You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

487 lines
12 KiB

  1. /* eslint-disable no-console */
  2. let path = require("path");
  3. let fs = require("fs");
  4. let glob = require("fast-glob");
  5. let esbuild = require("esbuild");
  6. let vue = require("esbuild-vue");
  7. let yargs = require("yargs");
  8. let cliui = require("cliui")();
  9. let chalk = require("chalk");
  10. let html_plugin = require("./frappe-html");
  11. let postCssPlugin = require("esbuild-plugin-postcss2").default;
  12. let ignore_assets = require("./ignore-assets");
  13. let sass_options = require("./sass_options");
  14. let {
  15. app_list,
  16. assets_path,
  17. apps_path,
  18. sites_path,
  19. get_app_path,
  20. get_public_path,
  21. log,
  22. log_warn,
  23. log_error,
  24. bench_path
  25. } = require("./utils");
  26. let { get_redis_subscriber } = require("../node_utils");
  27. let argv = yargs
  28. .usage("Usage: node esbuild [options]")
  29. .option("apps", {
  30. type: "string",
  31. description: "Run build for specific apps"
  32. })
  33. .option("skip_frappe", {
  34. type: "boolean",
  35. description: "Skip building frappe assets"
  36. })
  37. .option("files", {
  38. type: "string",
  39. description: "Run build for specified bundles"
  40. })
  41. .option("watch", {
  42. type: "boolean",
  43. description: "Run in watch mode and rebuild on file changes"
  44. })
  45. .option("production", {
  46. type: "boolean",
  47. description: "Run build in production mode"
  48. })
  49. .option("run-build-command", {
  50. type: "boolean",
  51. description: "Run build command for apps"
  52. })
  53. .example(
  54. "node esbuild --apps frappe,erpnext",
  55. "Run build only for frappe and erpnext"
  56. )
  57. .example(
  58. "node esbuild --files frappe/website.bundle.js,frappe/desk.bundle.js",
  59. "Run build only for specified bundles"
  60. )
  61. .version(false).argv;
  62. const APPS = (!argv.apps ? app_list : argv.apps.split(",")).filter(
  63. app => !(argv.skip_frappe && app == "frappe")
  64. );
  65. const FILES_TO_BUILD = argv.files ? argv.files.split(",") : [];
  66. const WATCH_MODE = Boolean(argv.watch);
  67. const PRODUCTION = Boolean(argv.production);
  68. const RUN_BUILD_COMMAND = !WATCH_MODE && Boolean(argv["run-build-command"]);
  69. const TOTAL_BUILD_TIME = `${chalk.black.bgGreen(" DONE ")} Total Build Time`;
  70. const NODE_PATHS = [].concat(
  71. // node_modules of apps directly importable
  72. app_list
  73. .map(app => path.resolve(get_app_path(app), "../node_modules"))
  74. .filter(fs.existsSync),
  75. // import js file of any app if you provide the full path
  76. app_list
  77. .map(app => path.resolve(get_app_path(app), ".."))
  78. .filter(fs.existsSync)
  79. );
  80. execute()
  81. .then(() => RUN_BUILD_COMMAND && run_build_command_for_apps(APPS))
  82. .catch(e => console.error(e));
  83. if (WATCH_MODE) {
  84. // listen for open files in editor event
  85. open_in_editor();
  86. }
  87. async function execute() {
  88. console.time(TOTAL_BUILD_TIME);
  89. if (!FILES_TO_BUILD.length) {
  90. await clean_dist_folders(APPS);
  91. }
  92. let result;
  93. try {
  94. result = await build_assets_for_apps(APPS, FILES_TO_BUILD);
  95. } catch (e) {
  96. log_error("There were some problems during build");
  97. log();
  98. log(chalk.dim(e.stack));
  99. return;
  100. }
  101. if (!WATCH_MODE) {
  102. log_built_assets(result.metafile);
  103. console.timeEnd(TOTAL_BUILD_TIME);
  104. log();
  105. } else {
  106. log("Watching for changes...");
  107. }
  108. return await write_assets_json(result.metafile);
  109. }
  110. function build_assets_for_apps(apps, files) {
  111. let { include_patterns, ignore_patterns } = files.length
  112. ? get_files_to_build(files)
  113. : get_all_files_to_build(apps);
  114. return glob(include_patterns, { ignore: ignore_patterns }).then(files => {
  115. let output_path = assets_path;
  116. let file_map = {};
  117. for (let file of files) {
  118. let relative_app_path = path.relative(apps_path, file);
  119. let app = relative_app_path.split(path.sep)[0];
  120. let extension = path.extname(file);
  121. let output_name = path.basename(file, extension);
  122. if (
  123. [".css", ".scss", ".less", ".sass", ".styl"].includes(extension)
  124. ) {
  125. output_name = path.join("css", output_name);
  126. } else if ([".js", ".ts"].includes(extension)) {
  127. output_name = path.join("js", output_name);
  128. }
  129. output_name = path.join(app, "dist", output_name);
  130. if (Object.keys(file_map).includes(output_name)) {
  131. log_warn(
  132. `Duplicate output file ${output_name} generated from ${file}`
  133. );
  134. }
  135. file_map[output_name] = file;
  136. }
  137. return build_files({
  138. files: file_map,
  139. outdir: output_path
  140. });
  141. });
  142. }
  143. function get_all_files_to_build(apps) {
  144. let include_patterns = [];
  145. let ignore_patterns = [];
  146. for (let app of apps) {
  147. let public_path = get_public_path(app);
  148. include_patterns.push(
  149. path.resolve(
  150. public_path,
  151. "**",
  152. "*.bundle.{js,ts,css,sass,scss,less,styl}"
  153. )
  154. );
  155. ignore_patterns.push(
  156. path.resolve(public_path, "node_modules"),
  157. path.resolve(public_path, "dist")
  158. );
  159. }
  160. return {
  161. include_patterns,
  162. ignore_patterns
  163. };
  164. }
  165. function get_files_to_build(files) {
  166. // files: ['frappe/website.bundle.js', 'erpnext/main.bundle.js']
  167. let include_patterns = [];
  168. let ignore_patterns = [];
  169. for (let file of files) {
  170. let [app, bundle] = file.split("/");
  171. let public_path = get_public_path(app);
  172. include_patterns.push(path.resolve(public_path, "**", bundle));
  173. ignore_patterns.push(
  174. path.resolve(public_path, "node_modules"),
  175. path.resolve(public_path, "dist")
  176. );
  177. }
  178. return {
  179. include_patterns,
  180. ignore_patterns
  181. };
  182. }
  183. function build_files({ files, outdir }) {
  184. return esbuild.build({
  185. entryPoints: files,
  186. entryNames: "[dir]/[name].[hash]",
  187. outdir,
  188. sourcemap: true,
  189. bundle: true,
  190. metafile: true,
  191. minify: PRODUCTION,
  192. nodePaths: NODE_PATHS,
  193. define: {
  194. "process.env.NODE_ENV": JSON.stringify(
  195. PRODUCTION ? "production" : "development"
  196. )
  197. },
  198. plugins: [
  199. html_plugin,
  200. ignore_assets,
  201. vue(),
  202. postCssPlugin({
  203. plugins: [require("autoprefixer")],
  204. sassOptions: sass_options
  205. })
  206. ],
  207. watch: get_watch_config()
  208. });
  209. }
  210. function get_watch_config() {
  211. if (WATCH_MODE) {
  212. return {
  213. async onRebuild(error, result) {
  214. if (error) {
  215. log_error("There was an error during rebuilding changes.");
  216. log();
  217. log(chalk.dim(error.stack));
  218. notify_redis({ error });
  219. } else {
  220. let {
  221. assets_json,
  222. prev_assets_json
  223. } = await write_assets_json(result.metafile);
  224. if (prev_assets_json) {
  225. log_rebuilt_assets(prev_assets_json, assets_json);
  226. }
  227. notify_redis({ success: true });
  228. }
  229. }
  230. };
  231. }
  232. return null;
  233. }
  234. async function clean_dist_folders(apps) {
  235. for (let app of apps) {
  236. let public_path = get_public_path(app);
  237. await fs.promises.rmdir(path.resolve(public_path, "dist", "js"), {
  238. recursive: true
  239. });
  240. await fs.promises.rmdir(path.resolve(public_path, "dist", "css"), {
  241. recursive: true
  242. });
  243. }
  244. }
  245. function log_built_assets(metafile) {
  246. let column_widths = [60, 20];
  247. cliui.div(
  248. {
  249. text: chalk.cyan.bold("File"),
  250. width: column_widths[0]
  251. },
  252. {
  253. text: chalk.cyan.bold("Size"),
  254. width: column_widths[1]
  255. }
  256. );
  257. cliui.div("");
  258. let output_by_dist_path = {};
  259. for (let outfile in metafile.outputs) {
  260. if (outfile.endsWith(".map")) continue;
  261. let data = metafile.outputs[outfile];
  262. outfile = path.resolve(outfile);
  263. outfile = path.relative(assets_path, outfile);
  264. let filename = path.basename(outfile);
  265. let dist_path = outfile.replace(filename, "");
  266. output_by_dist_path[dist_path] = output_by_dist_path[dist_path] || [];
  267. output_by_dist_path[dist_path].push({
  268. name: filename,
  269. size: (data.bytes / 1000).toFixed(2) + " Kb"
  270. });
  271. }
  272. for (let dist_path in output_by_dist_path) {
  273. let files = output_by_dist_path[dist_path];
  274. cliui.div({
  275. text: dist_path,
  276. width: column_widths[0]
  277. });
  278. for (let i in files) {
  279. let file = files[i];
  280. let branch = "";
  281. if (i < files.length - 1) {
  282. branch = "├─ ";
  283. } else {
  284. branch = "└─ ";
  285. }
  286. let color = file.name.endsWith(".js") ? "green" : "blue";
  287. cliui.div(
  288. {
  289. text: branch + chalk[color]("" + file.name),
  290. width: column_widths[0]
  291. },
  292. {
  293. text: file.size,
  294. width: column_widths[1]
  295. }
  296. );
  297. }
  298. cliui.div("");
  299. }
  300. log(cliui.toString());
  301. }
  302. // to store previous build's assets.json for comparison
  303. let prev_assets_json;
  304. let curr_assets_json;
  305. async function write_assets_json(metafile) {
  306. prev_assets_json = curr_assets_json;
  307. let out = {};
  308. for (let output in metafile.outputs) {
  309. let info = metafile.outputs[output];
  310. let asset_path = "/" + path.relative(sites_path, output);
  311. if (info.entryPoint) {
  312. out[path.basename(info.entryPoint)] = asset_path;
  313. }
  314. }
  315. let assets_json_path = path.resolve(
  316. assets_path,
  317. "frappe",
  318. "dist",
  319. "assets.json"
  320. );
  321. let assets_json;
  322. try {
  323. assets_json = await fs.promises.readFile(assets_json_path, "utf-8");
  324. } catch (error) {
  325. assets_json = "{}";
  326. }
  327. assets_json = JSON.parse(assets_json);
  328. // update with new values
  329. assets_json = Object.assign({}, assets_json, out);
  330. curr_assets_json = assets_json;
  331. await fs.promises.writeFile(
  332. assets_json_path,
  333. JSON.stringify(assets_json, null, 4)
  334. );
  335. await update_assets_json_in_cache(assets_json);
  336. return {
  337. assets_json,
  338. prev_assets_json
  339. };
  340. }
  341. function update_assets_json_in_cache(assets_json) {
  342. // update assets_json cache in redis, so that it can be read directly by python
  343. return new Promise(resolve => {
  344. let client = get_redis_subscriber("redis_cache");
  345. // handle error event to avoid printing stack traces
  346. client.on("error", _ => {
  347. log_warn("Cannot connect to redis_cache to update assets_json");
  348. });
  349. client.set("assets_json", JSON.stringify(assets_json), err => {
  350. client.unref();
  351. resolve();
  352. });
  353. });
  354. }
  355. function run_build_command_for_apps(apps) {
  356. let cwd = process.cwd();
  357. let { execSync } = require("child_process");
  358. for (let app of apps) {
  359. if (app === "frappe") continue;
  360. let root_app_path = path.resolve(get_app_path(app), "..");
  361. let package_json = path.resolve(root_app_path, "package.json");
  362. if (fs.existsSync(package_json)) {
  363. let { scripts } = require(package_json);
  364. if (scripts && scripts.build) {
  365. log("\nRunning build command for", chalk.bold(app));
  366. process.chdir(root_app_path);
  367. execSync("yarn build", { encoding: "utf8", stdio: "inherit" });
  368. }
  369. }
  370. }
  371. process.chdir(cwd);
  372. }
  373. async function notify_redis({ error, success }) {
  374. // notify redis which in turns tells socketio to publish this to browser
  375. let subscriber = get_redis_subscriber("redis_socketio");
  376. subscriber.on("error", _ => {
  377. log_warn("Cannot connect to redis_socketio for browser events");
  378. });
  379. let payload = null;
  380. if (error) {
  381. let formatted = await esbuild.formatMessages(error.errors, {
  382. kind: "error",
  383. terminalWidth: 100
  384. });
  385. let stack = error.stack.replace(new RegExp(bench_path, "g"), "");
  386. payload = {
  387. error,
  388. formatted,
  389. stack
  390. };
  391. }
  392. if (success) {
  393. payload = {
  394. success: true
  395. };
  396. }
  397. subscriber.publish(
  398. "events",
  399. JSON.stringify({
  400. event: "build_event",
  401. message: payload
  402. })
  403. );
  404. }
  405. function open_in_editor() {
  406. let subscriber = get_redis_subscriber("redis_socketio");
  407. subscriber.on("error", _ => {
  408. log_warn("Cannot connect to redis_socketio for open_in_editor events");
  409. });
  410. subscriber.on("message", (event, file) => {
  411. if (event === "open_in_editor") {
  412. file = JSON.parse(file);
  413. let file_path = path.resolve(file.file);
  414. log("Opening file in editor:", file_path);
  415. let launch = require("launch-editor");
  416. launch(`${file_path}:${file.line}:${file.column}`);
  417. }
  418. });
  419. subscriber.subscribe("open_in_editor");
  420. }
  421. function log_rebuilt_assets(prev_assets, new_assets) {
  422. let added_files = [];
  423. let old_files = Object.values(prev_assets);
  424. let new_files = Object.values(new_assets);
  425. for (let filepath of new_files) {
  426. if (!old_files.includes(filepath)) {
  427. added_files.push(filepath);
  428. }
  429. }
  430. log(
  431. chalk.yellow(
  432. `${new Date().toLocaleTimeString()}: Compiled ${
  433. added_files.length
  434. } files...`
  435. )
  436. );
  437. for (let filepath of added_files) {
  438. let filename = path.basename(filepath);
  439. log(" " + filename);
  440. }
  441. log();
  442. }