Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 
 

507 linhas
13 KiB

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