Fork of the espurna firmware for `mhsw` switches
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.

346 lines
9.6 KiB

7 years ago
7 years ago
6 years ago
7 years ago
7 years ago
7 years ago
6 years ago
6 years ago
6 years ago
6 years ago
7 years ago
5 years ago
5 years ago
  1. /*
  2. ESP8266 file system builder
  3. Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see <http://www.gnu.org/licenses/>.
  14. */
  15. /*eslint quotes: ['error', 'single']*/
  16. /*eslint-env es6*/
  17. // -----------------------------------------------------------------------------
  18. // Dependencies
  19. // -----------------------------------------------------------------------------
  20. const path = require('path');
  21. const gulp = require('gulp');
  22. const through = require('through2');
  23. const htmllint = require('gulp-htmllint');
  24. const csslint = require('gulp-csslint');
  25. const htmlmin = require('html-minifier');
  26. const gzip = require('gulp-gzip');
  27. const inline = require('gulp-inline-source-html');
  28. const rename = require('gulp-rename');
  29. const replace = require('gulp-replace');
  30. // -----------------------------------------------------------------------------
  31. // Configuration
  32. // -----------------------------------------------------------------------------
  33. const htmlFolder = 'html/';
  34. const dataFolder = 'espurna/data/';
  35. const staticFolder = 'espurna/static/';
  36. // -----------------------------------------------------------------------------
  37. // Methods
  38. // -----------------------------------------------------------------------------
  39. var toMinifiedHtml = function(options) {
  40. return through.obj(function (source, encoding, callback) {
  41. if (source.isNull()) {
  42. callback(null, source);
  43. return;
  44. }
  45. var contents = source.contents.toString();
  46. source.contents = Buffer.from(htmlmin.minify(contents, options));
  47. callback(null, source);
  48. });
  49. }
  50. var toHeader = function(name, debug) {
  51. return through.obj(function (source, encoding, callback) {
  52. var parts = source.path.split(path.sep);
  53. var filename = parts[parts.length - 1];
  54. var safename = name || filename.split('.').join('_');
  55. // Generate output
  56. var output = '';
  57. output += '#define ' + safename + '_len ' + source.contents.length + '\n';
  58. output += 'const uint8_t ' + safename + '[] PROGMEM = {';
  59. for (var i=0; i<source.contents.length; i++) {
  60. if (i > 0) { output += ','; }
  61. if (0 === (i % 20)) { output += '\n'; }
  62. output += '0x' + ('00' + source.contents[i].toString(16)).slice(-2);
  63. }
  64. output += '\n};';
  65. // clone the contents
  66. var destination = source.clone();
  67. destination.path = source.path + '.h';
  68. destination.contents = Buffer.from(output);
  69. if (debug) {
  70. console.info('Image ' + filename + ' \tsize: ' + source.contents.length + ' bytes');
  71. }
  72. callback(null, destination);
  73. });
  74. };
  75. var htmllintReporter = function(filepath, issues) {
  76. if (issues.length > 0) {
  77. issues.forEach(function (issue) {
  78. console.info(
  79. '[gulp-htmllint] ' +
  80. filepath + ' [' +
  81. issue.line + ',' +
  82. issue.column + ']: ' +
  83. '(' + issue.code + ') ' +
  84. issue.msg
  85. );
  86. });
  87. process.exitCode = 1;
  88. }
  89. };
  90. // TODO: this is a roughly equivalent port of the gulp-remove-code,
  91. // which also uses regexp rules to filter in-between specially-formatted comment blocks
  92. var jsRegexp = function(module) {
  93. return '//\\s*removeIf\\(!' + module + '\\)'
  94. + '\\s*(\n|\r|.)*?'
  95. + '//\\s*endRemoveIf\\(!' + module + '\\)';
  96. }
  97. var cssRegexp = function(module) {
  98. return '/\\*\\s*removeIf\\(!' + module + '\\)\\s*\\*/'
  99. + '\\s*(\n|\r|.)*?'
  100. + '/\\*\\s*endRemoveIf\\(!' + module + '\\)\\s*\\*/';
  101. }
  102. var htmlRegexp = function(module) {
  103. return '<!--\\s*removeIf\\(!' + module + '\\)\\s*-->'
  104. + '\\s*(\n|\r|.)*?'
  105. + '<!--\\s*endRemoveIf\\(!' + module + '\\)\\s*-->';
  106. }
  107. var generateRegexps = function(modules, func) {
  108. var regexps = new Set();
  109. for (const [module, enabled] of Object.entries(modules)) {
  110. if (enabled) {
  111. continue;
  112. }
  113. const expression = func(module);
  114. const re = new RegExp(expression, 'gm');
  115. regexps.add(re);
  116. }
  117. return regexps;
  118. }
  119. // TODO: use html parser here?
  120. // TODO: separate js files to include js, html & css and avoid 2 step regexp?
  121. var htmlRemover = function(modules) {
  122. const regexps = generateRegexps(modules, htmlRegexp);
  123. return through.obj(function (source, _, callback) {
  124. if (source.isNull()) {
  125. callback(null, source);
  126. return;
  127. }
  128. var contents = source.contents.toString();
  129. for (var regexp of regexps) {
  130. contents = contents.replace(regexp, '');
  131. }
  132. source.contents = Buffer.from(contents);
  133. callback(null, source);
  134. });
  135. }
  136. var inlineHandler = function(modules) {
  137. return function(source) {
  138. if (((source.sourcepath === 'custom.css') || (source.sourcepath === 'custom.js'))) {
  139. const filter = (source.type === 'css') ? cssRegexp : jsRegexp;
  140. const regexps = generateRegexps(modules, filter);
  141. var content = source.fileContent;
  142. for (var regexp of regexps) {
  143. content = content.replace(regexp, '');
  144. }
  145. source.fileContent = content;
  146. return;
  147. }
  148. if (source.sourcepath === "favicon.ico") {
  149. source.format = "x-icon";
  150. return;
  151. }
  152. if (source.content) {
  153. return;
  154. }
  155. // Just ignore the vendored libs, repackaging makes things worse for the size
  156. const path = source.sourcepath;
  157. if (path.endsWith('.min.js')) {
  158. source.compress = false;
  159. } else if (path.endsWith('.min.css')) {
  160. source.compress = false;
  161. }
  162. };
  163. }
  164. var buildWebUI = function(module) {
  165. // Declare some modules as optional to remove with
  166. // removeIf(!name) ...code... endRemoveIf(!name) sections
  167. // (via gulp-remove-code)
  168. var modules = {
  169. 'light': false,
  170. 'sensor': false,
  171. 'rfbridge': false,
  172. 'rfm69': false,
  173. 'garland': false,
  174. 'thermostat': false,
  175. 'lightfox': false,
  176. 'curtain': false
  177. };
  178. // Note: only build these when specified as module arg
  179. var excludeAll = [
  180. 'rfm69',
  181. 'lightfox'
  182. ];
  183. // 'all' to include all *but* excludeAll
  184. // '<module>' to include a single module
  185. // 'small' is the default state (all disabled)
  186. if ('all' === module) {
  187. Object.keys(modules).
  188. filter(function(key) {
  189. return excludeAll.indexOf(key) < 0;
  190. }).
  191. forEach(function(key) {
  192. modules[key] = true;
  193. });
  194. } else if ('small' !== module) {
  195. modules[module] = true;
  196. }
  197. return gulp.src(htmlFolder + '*.html').
  198. pipe(htmllint({
  199. 'failOnError': true,
  200. 'rules': {
  201. 'id-class-style': false,
  202. 'label-req-for': false,
  203. 'line-end-style': false,
  204. 'attr-req-value': false
  205. }
  206. }, htmllintReporter)).
  207. pipe(htmlRemover(modules)).
  208. pipe(inline({handlers: [inlineHandler(modules)]})).
  209. pipe(toMinifiedHtml({
  210. collapseWhitespace: true,
  211. removeComments: true,
  212. minifyCSS: false,
  213. minifyJS: false
  214. })).
  215. pipe(replace('pure-', 'p-')).
  216. pipe(gzip({ gzipOptions: { level: 9 } })).
  217. pipe(rename('index.' + module + '.html.gz')).
  218. pipe(gulp.dest(dataFolder)).
  219. pipe(toHeader('webui_image', true)).
  220. pipe(gulp.dest(staticFolder));
  221. };
  222. // -----------------------------------------------------------------------------
  223. // Tasks
  224. // -----------------------------------------------------------------------------
  225. gulp.task('certs', function() {
  226. gulp.src(dataFolder + 'server.*').
  227. pipe(toHeader('', false)).
  228. pipe(gulp.dest(staticFolder));
  229. });
  230. gulp.task('csslint', function() {
  231. gulp.src(htmlFolder + '*.css').
  232. pipe(csslint({ids: false})).
  233. pipe(csslint.formatter());
  234. });
  235. gulp.task('webui_small', function() {
  236. return buildWebUI('small');
  237. });
  238. gulp.task('webui_sensor', function() {
  239. return buildWebUI('sensor');
  240. });
  241. gulp.task('webui_light', function() {
  242. return buildWebUI('light');
  243. });
  244. gulp.task('webui_rfbridge', function() {
  245. return buildWebUI('rfbridge');
  246. });
  247. gulp.task('webui_rfm69', function() {
  248. return buildWebUI('rfm69');
  249. });
  250. gulp.task('webui_lightfox', function() {
  251. return buildWebUI('lightfox');
  252. });
  253. gulp.task('webui_garland', function() {
  254. return buildWebUI('garland');
  255. });
  256. gulp.task('webui_thermostat', function() {
  257. return buildWebUI('thermostat');
  258. });
  259. gulp.task('webui_curtain', function() {
  260. return buildWebUI('curtain');
  261. });
  262. gulp.task('webui_all', function() {
  263. return buildWebUI('all');
  264. });
  265. gulp.task('webui',
  266. gulp.parallel(
  267. 'webui_small',
  268. 'webui_sensor',
  269. 'webui_light',
  270. 'webui_rfbridge',
  271. 'webui_rfm69',
  272. 'webui_lightfox',
  273. 'webui_garland',
  274. 'webui_thermostat',
  275. 'webui_curtain',
  276. 'webui_all'
  277. )
  278. );
  279. gulp.task('default', gulp.series('webui'));