25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

526 lines
14 KiB

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