Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 
 
 

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