Gruntfile.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  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. /**
  8. * Grunt configuration for building the app in various formats.
  9. *
  10. * @author n1474335 [n1474335@gmail.com]
  11. * @copyright Crown Copyright 2017
  12. * @license Apache-2.0
  13. */
  14. module.exports = function (grunt) {
  15. grunt.file.defaultEncoding = "utf8";
  16. grunt.file.preserveBOM = false;
  17. // Tasks
  18. grunt.registerTask("dev",
  19. "A persistent task which creates a development build whenever source files are modified.",
  20. ["clean:dev", "clean:config", "exec:generateConfig", "concurrent:dev"]);
  21. grunt.registerTask("prod",
  22. "Creates a production-ready build. Use the --msg flag to add a compile message.",
  23. [
  24. "eslint", "clean:prod", "clean:config", "exec:generateConfig", "findModules", "webpack:web",
  25. "copy:standalone", "zip:standalone", "clean:standalone", "chmod"
  26. ]);
  27. grunt.registerTask("node",
  28. "Compiles CyberChef into a single NodeJS module.",
  29. [
  30. "clean:node", "clean:config", "clean:nodeConfig", "exec:generateConfig", "exec:generateNodeIndex"
  31. ]);
  32. grunt.registerTask("configTests",
  33. "A task which configures config files in preparation for tests to be run. Use `npm test` to run tests.",
  34. [
  35. "clean:config", "clean:nodeConfig", "exec:generateConfig", "exec:generateNodeIndex"
  36. ]);
  37. grunt.registerTask("testui",
  38. "A task which runs all the UI tests in the tests directory. The prod task must already have been run.",
  39. ["connect:prod", "exec:browserTests"]);
  40. grunt.registerTask("testnodeconsumer",
  41. "A task which checks whether consuming CJS and ESM apps work with the CyberChef build",
  42. ["exec:setupNodeConsumers", "exec:testCJSNodeConsumer", "exec:testESMNodeConsumer", "exec:testESMDeepImportNodeConsumer", "exec:teardownNodeConsumers"]);
  43. grunt.registerTask("default",
  44. "Lints the code base",
  45. ["eslint", "exec:repoSize"]);
  46. grunt.registerTask("lint", "eslint");
  47. grunt.registerTask("findModules",
  48. "Finds all generated modules and updates the entry point list for Webpack",
  49. function(arg1, arg2) {
  50. const moduleEntryPoints = listEntryModules();
  51. grunt.log.writeln(`Found ${Object.keys(moduleEntryPoints).length} modules.`);
  52. grunt.config.set("webpack.web.entry",
  53. Object.assign({
  54. main: "./src/web/index.js"
  55. }, moduleEntryPoints));
  56. });
  57. // Load tasks provided by each plugin
  58. grunt.loadNpmTasks("grunt-eslint");
  59. grunt.loadNpmTasks("grunt-webpack");
  60. grunt.loadNpmTasks("grunt-contrib-clean");
  61. grunt.loadNpmTasks("grunt-contrib-copy");
  62. grunt.loadNpmTasks("grunt-contrib-watch");
  63. grunt.loadNpmTasks("grunt-chmod");
  64. grunt.loadNpmTasks("grunt-exec");
  65. grunt.loadNpmTasks("grunt-accessibility");
  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", "\\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. options: {
  168. configFile: "./.eslintrc.json"
  169. },
  170. configs: ["*.{js,mjs}"],
  171. core: ["src/core/**/*.{js,mjs}", "!src/core/vendor/**/*", "!src/core/operations/legacy/**/*"],
  172. web: ["src/web/**/*.{js,mjs}", "!src/web/static/**/*"],
  173. node: ["src/node/**/*.{js,mjs}"],
  174. tests: ["tests/**/*.{js,mjs}"],
  175. },
  176. accessibility: {
  177. options: {
  178. accessibilityLevel: "WCAG2A",
  179. verbose: false,
  180. ignore: [
  181. "WCAG2A.Principle1.Guideline1_3.1_3_1.H42.2"
  182. ]
  183. },
  184. test: {
  185. src: ["build/**/*.html"]
  186. }
  187. },
  188. webpack: {
  189. options: webpackConfig,
  190. web: webpackProdConf(),
  191. },
  192. "webpack-dev-server": {
  193. options: {
  194. webpack: webpackConfig,
  195. host: "0.0.0.0",
  196. port: grunt.option("port") || 8080,
  197. disableHostCheck: true,
  198. overlay: true,
  199. inline: false,
  200. clientLogLevel: "error",
  201. stats: {
  202. children: false,
  203. chunks: false,
  204. modules: false,
  205. entrypoints: false,
  206. warningsFilter: [
  207. /source-map/,
  208. /dependency is an expression/,
  209. /export 'default'/,
  210. /Can't resolve 'sodium'/
  211. ],
  212. }
  213. },
  214. start: {
  215. webpack: {
  216. mode: "development",
  217. target: "web",
  218. entry: Object.assign({
  219. main: "./src/web/index.js"
  220. }, moduleEntryPoints),
  221. resolve: {
  222. alias: {
  223. "./config/modules/OpModules.mjs": "./config/modules/Default.mjs"
  224. }
  225. },
  226. plugins: [
  227. new webpack.DefinePlugin(BUILD_CONSTANTS),
  228. new HtmlWebpackPlugin({
  229. filename: "index.html",
  230. template: "./src/web/html/index.html",
  231. chunks: ["main"],
  232. compileTime: compileTime,
  233. version: pkg.version,
  234. })
  235. ]
  236. }
  237. }
  238. },
  239. zip: {
  240. standalone: {
  241. cwd: "build/prod/",
  242. src: [
  243. "build/prod/**/*",
  244. "!build/prod/index.html",
  245. "!build/prod/BundleAnalyzerReport.html",
  246. ],
  247. dest: `build/prod/CyberChef_v${pkg.version}.zip`
  248. }
  249. },
  250. connect: {
  251. prod: {
  252. options: {
  253. port: grunt.option("port") || 8000,
  254. base: "build/prod/"
  255. }
  256. }
  257. },
  258. copy: {
  259. ghPages: {
  260. options: {
  261. process: function (content, srcpath) {
  262. if (srcpath.indexOf("index.html") >= 0) {
  263. // Add Google Analytics code to index.html
  264. content = content.replace("</body></html>",
  265. grunt.file.read("src/web/static/ga.html") + "</body></html>");
  266. // Add Structured Data for SEO
  267. content = content.replace("</head>",
  268. "<script type='application/ld+json'>" +
  269. JSON.stringify(JSON.parse(grunt.file.read("src/web/static/structuredData.json"))) +
  270. "</script></head>");
  271. return grunt.template.process(content, srcpath);
  272. } else {
  273. return content;
  274. }
  275. },
  276. noProcess: ["**", "!**/*.html"]
  277. },
  278. files: [
  279. {
  280. src: "build/prod/index.html",
  281. dest: "build/prod/index.html"
  282. }
  283. ]
  284. },
  285. standalone: {
  286. options: {
  287. process: function (content, srcpath) {
  288. if (srcpath.indexOf("index.html") >= 0) {
  289. // Replace download link with version number
  290. content = content.replace(/<a [^>]+>Download CyberChef.+?<\/a>/,
  291. `<span>Version ${pkg.version}</span>`);
  292. return grunt.template.process(content, srcpath);
  293. } else {
  294. return content;
  295. }
  296. },
  297. noProcess: ["**", "!**/*.html"]
  298. },
  299. files: [
  300. {
  301. src: "build/prod/index.html",
  302. dest: `build/prod/CyberChef_v${pkg.version}.html`
  303. }
  304. ]
  305. }
  306. },
  307. chmod: {
  308. build: {
  309. options: {
  310. mode: "755",
  311. },
  312. src: ["build/**/*", "build/"]
  313. }
  314. },
  315. watch: {
  316. config: {
  317. files: ["src/core/operations/**/*", "!src/core/operations/index.mjs"],
  318. tasks: ["exec:generateNodeIndex", "exec:generateConfig"]
  319. }
  320. },
  321. concurrent: {
  322. dev: ["watch:config", "webpack-dev-server:start"],
  323. options: {
  324. logConcurrentOutput: true
  325. }
  326. },
  327. exec: {
  328. repoSize: {
  329. command: chainCommands([
  330. "git ls-files | wc -l | xargs printf '\n%b\ttracked files\n'",
  331. "du -hs | egrep -o '^[^\t]*' | xargs printf '%b\trepository size\n'"
  332. ]),
  333. stderr: false
  334. },
  335. cleanGit: {
  336. command: "git gc --prune=now --aggressive"
  337. },
  338. sitemap: {
  339. command: "node --experimental-modules --no-warnings --no-deprecation src/web/static/sitemap.mjs > build/prod/sitemap.xml",
  340. sync: true
  341. },
  342. generateConfig: {
  343. command: chainCommands([
  344. "echo '\n--- Regenerating config files. ---'",
  345. "echo [] > src/core/config/OperationConfig.json",
  346. "node --experimental-modules --no-warnings --no-deprecation src/core/config/scripts/generateOpsIndex.mjs",
  347. "node --experimental-modules --no-warnings --no-deprecation src/core/config/scripts/generateConfig.mjs",
  348. "echo '--- Config scripts finished. ---\n'"
  349. ]),
  350. sync: true
  351. },
  352. generateNodeIndex: {
  353. command: chainCommands([
  354. "echo '\n--- Regenerating node index ---'",
  355. "node --experimental-modules --no-warnings --no-deprecation src/node/config/scripts/generateNodeIndex.mjs",
  356. "echo '--- Node index generated. ---\n'"
  357. ]),
  358. sync: true
  359. },
  360. browserTests: {
  361. command: "./node_modules/.bin/nightwatch --env prod"
  362. },
  363. setupNodeConsumers: {
  364. command: chainCommands([
  365. "echo '\n--- Testing node consumers ---'",
  366. "npm link",
  367. `mkdir ${nodeConsumerTestPath}`,
  368. `cp tests/node/consumers/* ${nodeConsumerTestPath}`,
  369. `cd ${nodeConsumerTestPath}`,
  370. "npm link cyberchef"
  371. ]),
  372. sync: true
  373. },
  374. teardownNodeConsumers: {
  375. command: chainCommands([
  376. `rm -rf ${nodeConsumerTestPath}`,
  377. "echo '\n--- Node consumer tests complete ---'"
  378. ]),
  379. },
  380. testCJSNodeConsumer: {
  381. command: chainCommands([
  382. `cd ${nodeConsumerTestPath}`,
  383. "node --no-warnings cjs-consumer.js",
  384. ]),
  385. stdout: false,
  386. },
  387. testESMNodeConsumer: {
  388. command: chainCommands([
  389. `cd ${nodeConsumerTestPath}`,
  390. "node --no-warnings --experimental-modules esm-consumer.mjs",
  391. ]),
  392. stdout: false,
  393. },
  394. testESMDeepImportNodeConsumer: {
  395. command: chainCommands([
  396. `cd ${nodeConsumerTestPath}`,
  397. "node --no-warnings --experimental-modules esm-deep-import-consumer.mjs",
  398. ]),
  399. stdout: false,
  400. },
  401. },
  402. });
  403. };