Gruntfile.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. "use strict";
  2. const webpack = require("webpack");
  3. const HtmlWebpackPlugin = require("html-webpack-plugin");
  4. const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
  5. const glob = require("glob");
  6. const path = require("path");
  7. const nodeFlags = "--experimental-modules --experimental-json-modules --experimental-specifier-resolution=node --no-warnings --no-deprecation";
  8. /**
  9. * Grunt configuration for building the app in various formats.
  10. *
  11. * @author n1474335 [n1474335@gmail.com]
  12. * @copyright Crown Copyright 2017
  13. * @license Apache-2.0
  14. */
  15. module.exports = function (grunt) {
  16. grunt.file.defaultEncoding = "utf8";
  17. grunt.file.preserveBOM = false;
  18. // Tasks
  19. grunt.registerTask("dev",
  20. "A persistent task which creates a development build whenever source files are modified.",
  21. ["clean:dev", "clean:config", "exec:generateConfig", "concurrent:dev"]);
  22. grunt.registerTask("prod",
  23. "Creates a production-ready build. Use the --msg flag to add a compile message.",
  24. [
  25. "eslint", "clean:prod", "clean:config", "exec:generateConfig", "findModules", "webpack:web",
  26. "copy:standalone", "zip:standalone", "clean:standalone", "exec:calcDownloadHash", "chmod"
  27. ]);
  28. grunt.registerTask("node",
  29. "Compiles CyberChef into a single NodeJS module.",
  30. [
  31. "clean:node", "clean:config", "clean:nodeConfig", "exec:generateConfig", "exec:generateNodeIndex"
  32. ]);
  33. grunt.registerTask("configTests",
  34. "A task which configures config files in preparation for tests to be run. Use `npm test` to run tests.",
  35. [
  36. "clean:config", "clean:nodeConfig", "exec:generateConfig", "exec:generateNodeIndex"
  37. ]);
  38. grunt.registerTask("testui",
  39. "A task which runs all the UI tests in the tests directory. The prod task must already have been run.",
  40. ["connect:prod", "exec:browserTests"]);
  41. grunt.registerTask("testnodeconsumer",
  42. "A task which checks whether consuming CJS and ESM apps work with the CyberChef build",
  43. ["exec:setupNodeConsumers", "exec:testCJSNodeConsumer", "exec:testESMNodeConsumer", "exec:teardownNodeConsumers"]);
  44. grunt.registerTask("default",
  45. "Lints the code base",
  46. ["eslint", "exec:repoSize"]);
  47. grunt.registerTask("lint", "eslint");
  48. grunt.registerTask("findModules",
  49. "Finds all generated modules and updates the entry point list for Webpack",
  50. function(arg1, arg2) {
  51. const moduleEntryPoints = listEntryModules();
  52. grunt.log.writeln(`Found ${Object.keys(moduleEntryPoints).length} modules.`);
  53. grunt.config.set("webpack.web.entry",
  54. Object.assign({
  55. main: "./src/web/index.js"
  56. }, moduleEntryPoints));
  57. });
  58. // Load tasks provided by each plugin
  59. grunt.loadNpmTasks("grunt-eslint");
  60. grunt.loadNpmTasks("grunt-webpack");
  61. grunt.loadNpmTasks("grunt-contrib-clean");
  62. grunt.loadNpmTasks("grunt-contrib-copy");
  63. grunt.loadNpmTasks("grunt-contrib-watch");
  64. grunt.loadNpmTasks("grunt-chmod");
  65. grunt.loadNpmTasks("grunt-exec");
  66. grunt.loadNpmTasks("grunt-concurrent");
  67. grunt.loadNpmTasks("grunt-contrib-connect");
  68. grunt.loadNpmTasks("grunt-zip");
  69. // Project configuration
  70. const compileTime = grunt.template.today("UTC:dd/mm/yyyy HH:MM:ss") + " UTC",
  71. pkg = grunt.file.readJSON("package.json"),
  72. webpackConfig = require("./webpack.config.js"),
  73. BUILD_CONSTANTS = {
  74. COMPILE_TIME: JSON.stringify(compileTime),
  75. COMPILE_MSG: JSON.stringify(grunt.option("compile-msg") || grunt.option("msg") || ""),
  76. PKG_VERSION: JSON.stringify(pkg.version),
  77. },
  78. moduleEntryPoints = listEntryModules(),
  79. nodeConsumerTestPath = "~/tmp-cyberchef",
  80. /**
  81. * Configuration for Webpack production build. Defined as a function so that it
  82. * can be recalculated when new modules are generated.
  83. */
  84. webpackProdConf = () => {
  85. return {
  86. mode: "production",
  87. target: "web",
  88. entry: Object.assign({
  89. main: "./src/web/index.js"
  90. }, moduleEntryPoints),
  91. output: {
  92. path: __dirname + "/build/prod",
  93. filename: chunkData => {
  94. return chunkData.chunk.name === "main" ? "assets/[name].js": "[name].js";
  95. },
  96. globalObject: "this"
  97. },
  98. resolve: {
  99. alias: {
  100. "./config/modules/OpModules.mjs": "./config/modules/Default.mjs"
  101. }
  102. },
  103. plugins: [
  104. new webpack.DefinePlugin(BUILD_CONSTANTS),
  105. new HtmlWebpackPlugin({
  106. filename: "index.html",
  107. template: "./src/web/html/index.html",
  108. chunks: ["main"],
  109. compileTime: compileTime,
  110. version: pkg.version,
  111. minify: {
  112. removeComments: true,
  113. collapseWhitespace: true,
  114. minifyJS: true,
  115. minifyCSS: true
  116. }
  117. }),
  118. new BundleAnalyzerPlugin({
  119. analyzerMode: "static",
  120. reportFilename: "BundleAnalyzerReport.html",
  121. openAnalyzer: false
  122. }),
  123. ]
  124. };
  125. };
  126. /**
  127. * Generates an entry list for all the modules.
  128. */
  129. function listEntryModules() {
  130. const entryModules = {};
  131. glob.sync("./src/core/config/modules/*.mjs").forEach(file => {
  132. const basename = path.basename(file);
  133. if (basename !== "Default.mjs" && basename !== "OpModules.mjs")
  134. entryModules["modules/" + basename.split(".mjs")[0]] = path.resolve(file);
  135. });
  136. return entryModules;
  137. }
  138. /**
  139. * Detects the correct delimiter to use to chain shell commands together
  140. * based on the current OS.
  141. *
  142. * @param {string[]} cmds
  143. * @returns {string}
  144. */
  145. function chainCommands(cmds) {
  146. const win = process.platform === "win32";
  147. if (!win) {
  148. return cmds.join(";");
  149. }
  150. return cmds
  151. // && means that subsequent commands will not be executed if the
  152. // previous one fails. & would coninue on a fail
  153. .join("&&")
  154. // Windows does not support \n properly
  155. .replace(/\n/g, "\\n");
  156. }
  157. grunt.initConfig({
  158. clean: {
  159. dev: ["build/dev/*"],
  160. prod: ["build/prod/*"],
  161. node: ["build/node/*"],
  162. config: ["src/core/config/OperationConfig.json", "src/core/config/modules/*", "src/code/operations/index.mjs"],
  163. nodeConfig: ["src/node/index.mjs", "src/node/config/OperationConfig.json"],
  164. standalone: ["build/prod/CyberChef*.html"]
  165. },
  166. eslint: {
  167. configs: ["*.{js,mjs}"],
  168. core: ["src/core/**/*.{js,mjs}", "!src/core/vendor/**/*", "!src/core/operations/legacy/**/*"],
  169. web: ["src/web/**/*.{js,mjs}", "!src/web/static/**/*"],
  170. node: ["src/node/**/*.{js,mjs}"],
  171. tests: ["tests/**/*.{js,mjs}"],
  172. },
  173. webpack: {
  174. options: webpackConfig,
  175. myConfig: webpackConfig,
  176. web: webpackProdConf(),
  177. },
  178. "webpack-dev-server": {
  179. options: webpackConfig,
  180. start: {
  181. mode: "development",
  182. target: "web",
  183. entry: Object.assign({
  184. main: "./src/web/index.js"
  185. }, moduleEntryPoints),
  186. resolve: {
  187. alias: {
  188. "./config/modules/OpModules.mjs": "./config/modules/Default.mjs"
  189. }
  190. },
  191. devServer: {
  192. port: grunt.option("port") || 8080,
  193. client: {
  194. logging: "error",
  195. overlay: true
  196. },
  197. hot: "only"
  198. },
  199. plugins: [
  200. new webpack.DefinePlugin(BUILD_CONSTANTS),
  201. new HtmlWebpackPlugin({
  202. filename: "index.html",
  203. template: "./src/web/html/index.html",
  204. chunks: ["main"],
  205. compileTime: compileTime,
  206. version: pkg.version,
  207. })
  208. ]
  209. }
  210. },
  211. zip: {
  212. standalone: {
  213. cwd: "build/prod/",
  214. src: [
  215. "build/prod/**/*",
  216. "!build/prod/index.html",
  217. "!build/prod/BundleAnalyzerReport.html",
  218. ],
  219. dest: `build/prod/CyberChef_v${pkg.version}.zip`
  220. }
  221. },
  222. connect: {
  223. prod: {
  224. options: {
  225. port: grunt.option("port") || 8000,
  226. base: "build/prod/"
  227. }
  228. }
  229. },
  230. copy: {
  231. ghPages: {
  232. options: {
  233. process: function (content, srcpath) {
  234. if (srcpath.indexOf("index.html") >= 0) {
  235. // Add Google Analytics code to index.html
  236. content = content.replace("</body></html>",
  237. grunt.file.read("src/web/static/ga.html") + "</body></html>");
  238. // Add Structured Data for SEO
  239. content = content.replace("</head>",
  240. "<script type='application/ld+json'>" +
  241. JSON.stringify(JSON.parse(grunt.file.read("src/web/static/structuredData.json"))) +
  242. "</script></head>");
  243. return grunt.template.process(content, srcpath);
  244. } else {
  245. return content;
  246. }
  247. },
  248. noProcess: ["**", "!**/*.html"]
  249. },
  250. files: [
  251. {
  252. src: ["build/prod/index.html"],
  253. dest: "build/prod/index.html"
  254. }
  255. ]
  256. },
  257. standalone: {
  258. options: {
  259. process: function (content, srcpath) {
  260. if (srcpath.indexOf("index.html") >= 0) {
  261. // Replace download link with version number
  262. content = content.replace(/<a [^>]+>Download CyberChef.+?<\/a>/,
  263. `<span>Version ${pkg.version}</span>`);
  264. return grunt.template.process(content, srcpath);
  265. } else {
  266. return content;
  267. }
  268. },
  269. noProcess: ["**", "!**/*.html"]
  270. },
  271. files: [
  272. {
  273. src: ["build/prod/index.html"],
  274. dest: `build/prod/CyberChef_v${pkg.version}.html`
  275. }
  276. ]
  277. }
  278. },
  279. chmod: {
  280. build: {
  281. options: {
  282. mode: "755",
  283. },
  284. src: ["build/**/*", "build/"]
  285. }
  286. },
  287. watch: {
  288. config: {
  289. files: ["src/core/operations/**/*", "!src/core/operations/index.mjs"],
  290. tasks: ["exec:generateNodeIndex", "exec:generateConfig"]
  291. }
  292. },
  293. concurrent: {
  294. dev: ["watch:config", "webpack-dev-server:start"],
  295. options: {
  296. logConcurrentOutput: true
  297. }
  298. },
  299. exec: {
  300. calcDownloadHash: {
  301. command: function () {
  302. switch (process.platform) {
  303. case "darwin":
  304. return chainCommands([
  305. `shasum -a 256 build/prod/CyberChef_v${pkg.version}.zip | awk '{print $1;}' > build/prod/sha256digest.txt`,
  306. `sed -i '' -e "s/DOWNLOAD_HASH_PLACEHOLDER/$(cat build/prod/sha256digest.txt)/" build/prod/index.html`
  307. ]);
  308. default:
  309. return chainCommands([
  310. `sha256sum build/prod/CyberChef_v${pkg.version}.zip | awk '{print $1;}' > build/prod/sha256digest.txt`,
  311. `sed -i -e "s/DOWNLOAD_HASH_PLACEHOLDER/$(cat build/prod/sha256digest.txt)/" build/prod/index.html`
  312. ]);
  313. }
  314. },
  315. },
  316. repoSize: {
  317. command: chainCommands([
  318. "git ls-files | wc -l | xargs printf '\n%b\ttracked files\n'",
  319. "du -hs | egrep -o '^[^\t]*' | xargs printf '%b\trepository size\n'"
  320. ]),
  321. stderr: false
  322. },
  323. cleanGit: {
  324. command: "git gc --prune=now --aggressive"
  325. },
  326. sitemap: {
  327. command: `node ${nodeFlags} src/web/static/sitemap.mjs > build/prod/sitemap.xml`,
  328. sync: true
  329. },
  330. generateConfig: {
  331. command: chainCommands([
  332. "echo '\n--- Regenerating config files. ---'",
  333. "echo [] > src/core/config/OperationConfig.json",
  334. `node ${nodeFlags} src/core/config/scripts/generateOpsIndex.mjs`,
  335. `node ${nodeFlags} src/core/config/scripts/generateConfig.mjs`,
  336. "echo '--- Config scripts finished. ---\n'"
  337. ]),
  338. sync: true
  339. },
  340. generateNodeIndex: {
  341. command: chainCommands([
  342. "echo '\n--- Regenerating node index ---'",
  343. `node ${nodeFlags} src/node/config/scripts/generateNodeIndex.mjs`,
  344. "echo '--- Node index generated. ---\n'"
  345. ]),
  346. sync: true
  347. },
  348. browserTests: {
  349. command: "./node_modules/.bin/nightwatch --env prod"
  350. },
  351. setupNodeConsumers: {
  352. command: chainCommands([
  353. "echo '\n--- Testing node consumers ---'",
  354. "npm link",
  355. `mkdir ${nodeConsumerTestPath}`,
  356. `cp tests/node/consumers/* ${nodeConsumerTestPath}`,
  357. `cd ${nodeConsumerTestPath}`,
  358. "npm link cyberchef"
  359. ]),
  360. sync: true
  361. },
  362. teardownNodeConsumers: {
  363. command: chainCommands([
  364. `rm -rf ${nodeConsumerTestPath}`,
  365. "echo '\n--- Node consumer tests complete ---'"
  366. ]),
  367. },
  368. testCJSNodeConsumer: {
  369. command: chainCommands([
  370. `cd ${nodeConsumerTestPath}`,
  371. `node ${nodeFlags} cjs-consumer.js`,
  372. ]),
  373. stdout: false,
  374. },
  375. testESMNodeConsumer: {
  376. command: chainCommands([
  377. `cd ${nodeConsumerTestPath}`,
  378. `node ${nodeFlags} esm-consumer.mjs`,
  379. ]),
  380. stdout: false,
  381. },
  382. fixCryptoApiImports: {
  383. command: function () {
  384. switch (process.platform) {
  385. case "darwin":
  386. return `find ./node_modules/crypto-api/src/ \\( -type d -name .git -prune \\) -o -type f -print0 | xargs -0 sed -i '' -e '/\\.mjs/!s/\\(from "\\.[^"]*\\)";/\\1.mjs";/g'`;
  387. default:
  388. return `find ./node_modules/crypto-api/src/ \\( -type d -name .git -prune \\) -o -type f -print0 | xargs -0 sed -i -e '/\\.mjs/!s/\\(from "\\.[^"]*\\)";/\\1.mjs";/g'`;
  389. }
  390. },
  391. stdout: false
  392. },
  393. fixSnackbarMarkup: {
  394. command: function () {
  395. switch (process.platform) {
  396. case "darwin":
  397. return `sed -i '' 's/<div id=snackbar-container\\/>/<div id=snackbar-container>/g' ./node_modules/snackbarjs/src/snackbar.js`;
  398. default:
  399. return `sed -i 's/<div id=snackbar-container\\/>/<div id=snackbar-container>/g' ./node_modules/snackbarjs/src/snackbar.js`;
  400. }
  401. },
  402. stdout: false
  403. }
  404. },
  405. });
  406. };