You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

342 lines
7.8 KiB

  1. let path = require("path");
  2. let fs = require("fs");
  3. let glob = require("fast-glob");
  4. let esbuild = require("esbuild");
  5. let vue = require("esbuild-vue");
  6. let yargs = require("yargs");
  7. let cliui = require("cliui")();
  8. let chalk = require("chalk");
  9. let html_plugin = require("./frappe-html");
  10. let postCssPlugin = require("esbuild-plugin-postcss2").default;
  11. let ignore_assets = require("./ignore-assets");
  12. let sass_options = require("./sass_options");
  13. let {
  14. app_list,
  15. assets_path,
  16. apps_path,
  17. sites_path,
  18. get_app_path,
  19. get_public_path,
  20. log,
  21. log_warn,
  22. log_error,
  23. bench_path
  24. } = require("./utils");
  25. let { get_redis_subscriber } = require("../node_utils");
  26. let argv = yargs
  27. .usage("Usage: node esbuild [options]")
  28. .option("apps", {
  29. type: "string",
  30. description: "Run build for specific apps"
  31. })
  32. .option("watch", {
  33. type: "boolean",
  34. description: "Run in watch mode and rebuild on file changes"
  35. })
  36. .option("production", {
  37. type: "boolean",
  38. description: "Run build in production mode"
  39. })
  40. .example(
  41. "node esbuild --apps frappe,erpnext",
  42. "Run build only for frappe and erpnext"
  43. )
  44. .version(false).argv;
  45. const APPS = !argv.apps ? app_list : argv.apps.split(",");
  46. const WATCH_MODE = Boolean(argv.watch);
  47. const PRODUCTION = Boolean(argv.production);
  48. const TOTAL_BUILD_TIME = `${chalk.black.bgGreen(" DONE ")} Total Build Time`;
  49. const NODE_PATHS = [].concat(
  50. // node_modules of apps directly importable
  51. app_list
  52. .map(app => path.resolve(get_app_path(app), "../node_modules"))
  53. .filter(fs.existsSync),
  54. // import js file of any app if you provide the full path
  55. app_list
  56. .map(app => path.resolve(get_app_path(app), ".."))
  57. .filter(fs.existsSync)
  58. );
  59. execute();
  60. async function execute() {
  61. console.time(TOTAL_BUILD_TIME);
  62. await clean_dist_folders(APPS);
  63. let result;
  64. try {
  65. result = await build_assets_for_apps(APPS);
  66. } catch (e) {
  67. log_error("There were some problems during build");
  68. log();
  69. log(chalk.dim(e.stack));
  70. }
  71. if (!WATCH_MODE) {
  72. log_built_assets(result.metafile);
  73. console.timeEnd(TOTAL_BUILD_TIME);
  74. log();
  75. } else {
  76. log("Watching for changes...");
  77. }
  78. await write_meta_file(result.metafile);
  79. }
  80. function build_assets_for_apps(apps) {
  81. let { include_patterns, ignore_patterns } = get_files_to_build(apps);
  82. return glob(include_patterns, { ignore: ignore_patterns }).then(files => {
  83. let output_path = assets_path;
  84. let file_map = {};
  85. for (let file of files) {
  86. let relative_app_path = path.relative(apps_path, file);
  87. let app = relative_app_path.split(path.sep)[0];
  88. let extension = path.extname(file);
  89. let output_name = path.basename(file, extension);
  90. if (
  91. [".css", ".scss", ".less", ".sass", ".styl"].includes(extension)
  92. ) {
  93. output_name = path.join("css", output_name);
  94. } else if ([".js", ".ts"].includes(extension)) {
  95. output_name = path.join("js", output_name);
  96. }
  97. output_name = path.join(app, "dist", output_name);
  98. if (Object.keys(file_map).includes(output_name)) {
  99. log_warn(
  100. `Duplicate output file ${output_name} generated from ${file}`
  101. );
  102. }
  103. file_map[output_name] = file;
  104. }
  105. return build_files({
  106. files: file_map,
  107. outdir: output_path
  108. });
  109. });
  110. }
  111. function get_files_to_build(apps) {
  112. let include_patterns = [];
  113. let ignore_patterns = [];
  114. for (let app of apps) {
  115. let public_path = get_public_path(app);
  116. include_patterns.push(
  117. path.resolve(
  118. public_path,
  119. "**",
  120. "*.bundle.{js,ts,css,sass,scss,less,styl}"
  121. )
  122. );
  123. ignore_patterns.push(
  124. path.resolve(public_path, "node_modules"),
  125. path.resolve(public_path, "dist")
  126. );
  127. }
  128. return {
  129. include_patterns,
  130. ignore_patterns
  131. };
  132. }
  133. function build_files({ files, outdir }) {
  134. return esbuild.build({
  135. entryPoints: files,
  136. entryNames: "[dir]/[name].[hash]",
  137. outdir,
  138. sourcemap: true,
  139. bundle: true,
  140. metafile: true,
  141. minify: PRODUCTION,
  142. nodePaths: NODE_PATHS,
  143. define: {
  144. "process.env.NODE_ENV": JSON.stringify(
  145. PRODUCTION ? "production" : "development"
  146. )
  147. },
  148. plugins: [
  149. html_plugin,
  150. ignore_assets,
  151. vue(),
  152. postCssPlugin({
  153. plugins: [require("autoprefixer")],
  154. sassOptions: sass_options
  155. })
  156. ],
  157. watch: WATCH_MODE
  158. ? {
  159. onRebuild(error, result) {
  160. if (error) {
  161. log_error(
  162. "There was an error during rebuilding changes."
  163. );
  164. log();
  165. log(chalk.dim(error.stack));
  166. notify_redis({ error });
  167. } else {
  168. console.log(
  169. `${new Date().toLocaleTimeString()}: Compiled changes...`
  170. );
  171. write_meta_file(result.metafile);
  172. notify_redis({ success: true });
  173. }
  174. }
  175. }
  176. : null
  177. });
  178. }
  179. async function clean_dist_folders(apps) {
  180. for (let app of apps) {
  181. let public_path = get_public_path(app);
  182. await fs.promises.rmdir(path.resolve(public_path, "dist", "js"), {
  183. recursive: true
  184. });
  185. await fs.promises.rmdir(path.resolve(public_path, "dist", "css"), {
  186. recursive: true
  187. });
  188. }
  189. }
  190. function log_built_assets(metafile) {
  191. let column_widths = [60, 20];
  192. cliui.div(
  193. {
  194. text: chalk.cyan.bold("File"),
  195. width: column_widths[0]
  196. },
  197. {
  198. text: chalk.cyan.bold("Size"),
  199. width: column_widths[1]
  200. }
  201. );
  202. cliui.div("");
  203. let output_by_dist_path = {};
  204. for (let outfile in metafile.outputs) {
  205. if (outfile.endsWith(".map")) continue;
  206. let data = metafile.outputs[outfile];
  207. outfile = path.resolve(outfile);
  208. outfile = path.relative(assets_path, outfile);
  209. let filename = path.basename(outfile);
  210. let dist_path = outfile.replace(filename, "");
  211. output_by_dist_path[dist_path] = output_by_dist_path[dist_path] || [];
  212. output_by_dist_path[dist_path].push({
  213. name: filename,
  214. size: (data.bytes / 1000).toFixed(2) + " Kb"
  215. });
  216. }
  217. for (let dist_path in output_by_dist_path) {
  218. let files = output_by_dist_path[dist_path];
  219. cliui.div({
  220. text: dist_path,
  221. width: column_widths[0]
  222. });
  223. for (let i in files) {
  224. let file = files[i];
  225. let branch = "";
  226. if (i < files.length - 1) {
  227. branch = "├─ ";
  228. } else {
  229. branch = "└─ ";
  230. }
  231. let color = file.name.endsWith(".js") ? "green" : "blue";
  232. cliui.div(
  233. {
  234. text: branch + chalk[color]("" + file.name),
  235. width: column_widths[0]
  236. },
  237. {
  238. text: file.size,
  239. width: column_widths[1]
  240. }
  241. );
  242. }
  243. cliui.div("");
  244. }
  245. console.log(cliui.toString());
  246. }
  247. function write_meta_file(metafile) {
  248. let out = {};
  249. for (let output in metafile.outputs) {
  250. let info = metafile.outputs[output];
  251. let asset_path = "/" + path.relative(sites_path, output);
  252. if (info.entryPoint) {
  253. out[path.basename(info.entryPoint)] = asset_path;
  254. }
  255. }
  256. let assets_json = JSON.stringify(out, null, 4);
  257. return fs.promises
  258. .writeFile(
  259. path.resolve(assets_path, "frappe", "dist", "assets.json"),
  260. assets_json
  261. )
  262. .then(() => {
  263. let client = get_redis_subscriber("redis_cache");
  264. // update assets_json cache in redis, so that it can be read directly by python
  265. return client.set("assets_json", assets_json);
  266. });
  267. }
  268. async function notify_redis({ error, success }) {
  269. let subscriber = get_redis_subscriber("redis_socketio");
  270. // notify redis which in turns tells socketio to publish this to browser
  271. let payload = null;
  272. if (error) {
  273. let formatted = await esbuild.formatMessages(error.errors, {
  274. kind: "error",
  275. terminalWidth: 100
  276. });
  277. let stack = error.stack.replace(new RegExp(bench_path, "g"), "");
  278. payload = {
  279. error,
  280. formatted,
  281. stack
  282. };
  283. }
  284. if (success) {
  285. payload = {
  286. success: true
  287. };
  288. }
  289. subscriber.publish(
  290. "events",
  291. JSON.stringify({
  292. event: "build_event",
  293. message: payload
  294. })
  295. );
  296. }
  297. function open_in_editor() {
  298. let subscriber = get_redis_subscriber("redis_socketio");
  299. subscriber.on("message", (event, file) => {
  300. if (event === "open_in_editor") {
  301. file = JSON.parse(file);
  302. let file_path = path.resolve(file.file);
  303. console.log("Opening file in editor:", file_path);
  304. let launch = require("launch-editor");
  305. launch(`${file_path}:${file.line}:${file.column}`);
  306. }
  307. });
  308. subscriber.subscribe("open_in_editor");
  309. }
  310. open_in_editor();