Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 
 
 
 
 

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