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.
 
 
 
 
 
 

276 line
6.3 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. } = require("./utils");
  24. let argv = yargs
  25. .usage("Usage: node esbuild [options]")
  26. .option("apps", {
  27. type: "string",
  28. description: "Run build for specific apps"
  29. })
  30. .option("watch", {
  31. type: "boolean",
  32. description: "Run in watch mode and rebuild on file changes"
  33. })
  34. .option("production", {
  35. type: "boolean",
  36. description: "Run build in production mode"
  37. })
  38. .example(
  39. "node esbuild --apps frappe,erpnext",
  40. "Run build only for frappe and erpnext"
  41. )
  42. .version(false).argv;
  43. const APPS = !argv.apps ? app_list : argv.apps.split(",");
  44. const WATCH_MODE = Boolean(argv.watch);
  45. const PRODUCTION = Boolean(argv.production);
  46. const TOTAL_BUILD_TIME = `${chalk.black.bgGreen(" DONE ")} Total Build Time`;
  47. const NODE_PATHS = [].concat(
  48. // node_modules of apps directly importable
  49. app_list
  50. .map(app => path.resolve(get_app_path(app), "../node_modules"))
  51. .filter(fs.existsSync),
  52. // import js file of any app if you provide the full path
  53. app_list
  54. .map(app => path.resolve(get_app_path(app), ".."))
  55. .filter(fs.existsSync)
  56. );
  57. execute();
  58. async function execute() {
  59. console.time(TOTAL_BUILD_TIME);
  60. await clean_dist_folders(APPS);
  61. let result;
  62. try {
  63. result = await build_assets_for_apps(APPS);
  64. } catch (e) {
  65. log_error("There were some problems during build");
  66. log();
  67. log(chalk.dim(e.stack));
  68. }
  69. if (!WATCH_MODE) {
  70. log_built_assets(result.metafile);
  71. console.timeEnd(TOTAL_BUILD_TIME);
  72. log();
  73. } else {
  74. log("Watching for changes...");
  75. }
  76. await write_meta_file(result.metafile);
  77. }
  78. function build_assets_for_apps(apps) {
  79. let { include_patterns, ignore_patterns } = get_files_to_build(apps);
  80. return glob(include_patterns, { ignore: ignore_patterns }).then(files => {
  81. let output_path = assets_path;
  82. let file_map = {};
  83. for (let file of files) {
  84. let relative_app_path = path.relative(apps_path, file);
  85. let app = relative_app_path.split(path.sep)[0];
  86. let extension = path.extname(file);
  87. let output_name = path.basename(file, extension);
  88. if (
  89. [".css", ".scss", ".less", ".sass", ".styl"].includes(extension)
  90. ) {
  91. output_name = path.join("css", output_name);
  92. } else if ([".js", ".ts"].includes(extension)) {
  93. output_name = path.join("js", output_name);
  94. }
  95. output_name = path.join(app, "dist", output_name);
  96. if (Object.keys(file_map).includes(output_name)) {
  97. log_warn(
  98. `Duplicate output file ${output_name} generated from ${file}`
  99. );
  100. }
  101. file_map[output_name] = file;
  102. }
  103. return build_files({
  104. files: file_map,
  105. outdir: output_path
  106. });
  107. });
  108. }
  109. function get_files_to_build(apps) {
  110. let include_patterns = [];
  111. let ignore_patterns = [];
  112. for (let app of apps) {
  113. let public_path = get_public_path(app);
  114. include_patterns.push(
  115. path.resolve(
  116. public_path,
  117. "**",
  118. "*.bundle.{js,ts,css,sass,scss,less,styl}"
  119. )
  120. );
  121. ignore_patterns.push(
  122. path.resolve(public_path, "node_modules"),
  123. path.resolve(public_path, "dist")
  124. );
  125. }
  126. return {
  127. include_patterns,
  128. ignore_patterns
  129. };
  130. }
  131. function build_files({ files, outdir }) {
  132. return esbuild.build({
  133. entryPoints: files,
  134. entryNames: "[dir]/[name].[hash]",
  135. outdir,
  136. sourcemap: true,
  137. bundle: true,
  138. metafile: true,
  139. minify: PRODUCTION,
  140. nodePaths: NODE_PATHS,
  141. define: {
  142. "process.env.NODE_ENV": JSON.stringify(
  143. PRODUCTION ? "production" : "development"
  144. )
  145. },
  146. plugins: [
  147. html_plugin,
  148. ignore_assets,
  149. vue(),
  150. postCssPlugin({
  151. plugins: [require("autoprefixer")],
  152. sassOptions: sass_options
  153. })
  154. ],
  155. watch: WATCH_MODE
  156. ? {
  157. onRebuild(error, result) {
  158. if (error) console.error("watch build failed:", error);
  159. else {
  160. console.log(
  161. `${new Date().toLocaleTimeString()}: Compiled changes...`
  162. );
  163. }
  164. }
  165. }
  166. : null
  167. });
  168. }
  169. async function clean_dist_folders(apps) {
  170. for (let app of apps) {
  171. let public_path = get_public_path(app);
  172. await fs.promises.rmdir(path.resolve(public_path, "dist", "js"), {
  173. recursive: true
  174. });
  175. await fs.promises.rmdir(path.resolve(public_path, "dist", "css"), {
  176. recursive: true
  177. });
  178. }
  179. }
  180. function log_built_assets(metafile) {
  181. let column_widths = [60, 20];
  182. cliui.div(
  183. {
  184. text: chalk.cyan.bold("File"),
  185. width: column_widths[0]
  186. },
  187. {
  188. text: chalk.cyan.bold("Size"),
  189. width: column_widths[1]
  190. }
  191. );
  192. cliui.div("");
  193. let output_by_dist_path = {};
  194. for (let outfile in metafile.outputs) {
  195. if (outfile.endsWith(".map")) continue;
  196. let data = metafile.outputs[outfile];
  197. outfile = path.resolve(outfile);
  198. outfile = path.relative(assets_path, outfile);
  199. let filename = path.basename(outfile);
  200. let dist_path = outfile.replace(filename, "");
  201. output_by_dist_path[dist_path] = output_by_dist_path[dist_path] || [];
  202. output_by_dist_path[dist_path].push({
  203. name: filename,
  204. size: (data.bytes / 1000).toFixed(2) + " Kb"
  205. });
  206. }
  207. for (let dist_path in output_by_dist_path) {
  208. let files = output_by_dist_path[dist_path];
  209. cliui.div({
  210. text: dist_path,
  211. width: column_widths[0]
  212. });
  213. for (let i in files) {
  214. let file = files[i];
  215. let branch = "";
  216. if (i < files.length - 1) {
  217. branch = "├─ ";
  218. } else {
  219. branch = "└─ ";
  220. }
  221. let color = file.name.endsWith(".js") ? "green" : "blue";
  222. cliui.div(
  223. {
  224. text: branch + chalk[color]("" + file.name),
  225. width: column_widths[0]
  226. },
  227. {
  228. text: file.size,
  229. width: column_widths[1]
  230. }
  231. );
  232. }
  233. cliui.div("");
  234. }
  235. console.log(cliui.toString());
  236. }
  237. function write_meta_file(metafile) {
  238. let out = {};
  239. for (let output in metafile.outputs) {
  240. let info = metafile.outputs[output];
  241. let asset_path = "/" + path.relative(sites_path, output);
  242. if (info.entryPoint) {
  243. out[path.basename(info.entryPoint)] = asset_path;
  244. }
  245. }
  246. return fs.promises.writeFile(
  247. path.resolve(assets_path, "frappe", "dist", "assets.json"),
  248. JSON.stringify(out, null, 4)
  249. );
  250. }