您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 
 
 

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