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

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