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

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