Skip to content

Instantly share code, notes, and snippets.

@jfoclpf
Last active September 2, 2022 19:17
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jfoclpf/5be2694f47327ce7969c7b4dc942b528 to your computer and use it in GitHub Desktop.
Save jfoclpf/5be2694f47327ce7969c7b4dc942b528 to your computer and use it in GitHub Desktop.
How to minify html, css and js files in Apache Cordova

How to minify html, css and js files in Apache Cordova

Problem

Minifying assets in Apache Cordova helps you to reduce the size of your apk or aab file, and also allows you to uglify any sensible information you may have therein. The minification should be done to all assets, specifically html, css and js files after cordova prepare, that is, before cordova compile.

What to minify

The files to be minified should obey the following pattern, inside your www dir:

./**/*.html        # all .html files
+-- css/**/*.css   # all .css files inside css/ dir, except .min.css files
+-- js/**/*.js     # all .js files inside js/ dir, except .min.js files

Implementation

Install dependencies

npm i -D async walk uglify-js uglifycss html-minifier

Add a hook to your config.xml, let's name it minifyFiles.js. In your config.xml add this line

<hook src="hooks/minifyFiles.js" type="after_prepare"/>

This works on every platform, thus do not include this line inside any specific platform. Pay attention that hooks are run in order by which they appear in config.xml.

Create a file hooks/minifyFiles.js at the root directory (or whatever path you defined above in config.xml) with this content:

/* NodeJS script that minifies files stored in pltaforms/ after cordova prepare, and for every added platform
   It minifies all js, css and html files that will be compiled to build apk or aab file.
   This script is asynchronous for every file in any platform, to improve performance.
*/

// node/npm includes
const fs = require('fs')
const path = require('path')
const async = require('async')
const walk = require('walk')

// minification tools
const UglifyJS = require('uglify-js')
const uglifycss = require('uglifycss')
const minifyHTML = require('html-minifier').minify

const twoSpaces = '  '

module.exports = function (context) {
  console.log(`${context.hook} : ${path.relative(context.opts.projectRoot, context.scriptLocation)}`)
  var projectRoot = context.opts.projectRoot

  var platforms = []
  for (var i = 0; i < context.opts.platforms.length; i++) {
    const platform = { name: context.opts.platforms[i], path: context.opts.paths[i] }
    platforms.push(platform)
  }

  return new Promise((resolve, reject) => {
    async.each(platforms, function (platform, callback) {
      console.log(`${twoSpaces}Minifying html/css/js files for ${platform.name} at ${path.relative(projectRoot, platform.path)}`)
      processAllFilesForOnePlatform(platform, callback)
    }, function (err) {
      if (err) {
        console.error(Error(err))
        reject(Error(err))
      } else {
        console.log(`${twoSpaces}All files for all platforms have been minified successfully`)
        resolve()
      }
    })
  })
}

function processAllFilesForOnePlatform (platform, mainCallback) {
  async.parallel([
    (localCallback) => {
      processJSfiles(platform, localCallback)
    },
    (localCallback) => {
      processCSSFiles(platform, localCallback)
    },
    (localCallback) => {
      processHTMLfiles(platform, localCallback)
    }],
  function (err, results) {
    if (err) {
      console.error(Error(('\nError minifying file.\n' + err.message)), err)
      mainCallback(new Error(err))
    } else {
      console.log(`${twoSpaces + twoSpaces}All files minified successfully for ${platform.name}`)
      mainCallback()
    }
  }
  )
}

function processJSfiles (platform, callback) {
  const wwwDistDir = platform.path
  var walker = walk.walk(path.join(wwwDistDir, 'js'))

  walker.on('file', function (root, fileStats, next) {
    var filename = path.join(root, fileStats.name)

    // gets file extension
    if (getFileExtension(filename) === 'js' && !filename.includes('.min.js')) {
      var code = fs.readFileSync(filename, 'utf-8')

      var result = UglifyJS.minify(code)

      if (result.error) {
        callback(Error('Error minifying file: ' + path.relative(wwwDistDir, filename) + '.\n' + result.error))
        console.error(result)
        return
      } else {
        console.log(`${twoSpaces + twoSpaces}${platform.name}:${path.relative(wwwDistDir, filename)}`)
        fs.writeFileSync(filename, result.code, 'utf8')
      }
    }
    next()
  })

  walker.on('errors', function (root, nodeStatsArray, next) {
    callback(Error('There was an error with' + nodeStatsArray.name))
  })

  walker.on('end', function () {
    callback()
  })
}

// minifies all css files on the client side, namely on the build/css/ directory,
// i.e., these are CSS files that will be sent from the server to the client
function processCSSFiles (platform, callback) {
  const wwwDistDir = platform.path
  var walker = walk.walk(path.join(wwwDistDir, 'css')) // dir to walk into

  walker.on('file', function (root, fileStats, next) {
    var filename = path.join(root, fileStats.name)

    if (filename.includes('.css') && !filename.includes('.min.css')) {
      var code = fs.readFileSync(filename, 'utf-8')
      var result = uglifycss.processString(code)

      if (!result) {
        callback(Error('Error minifying file: ' + filename + '.\n'))
        return
      } else {
        console.log(`${twoSpaces + twoSpaces}${platform.name}:${path.relative(wwwDistDir, filename)}`)
        fs.writeFileSync(filename, result, 'utf8')
      }
    }
    next()
  })

  walker.on('errors', function (root, nodeStatsArray, next) {
    callback(Error('There was an error with' + nodeStatsArray.name))
  })

  walker.on('end', function () {
    callback()
  })
}

// minifies all html files
function processHTMLfiles (platform, callback) {
  const wwwDistDir = platform.path
  var walker = walk.walk(wwwDistDir) // dir to walk into
  walker.on('file', function (root, fileStats, next) {
    var filename = path.join(root, fileStats.name)

    if (getFileExtension(filename) === 'html') {
      var code = fs.readFileSync(filename, 'utf-8')

      var result = minifyHTML(code, {
        ignoreCustomFragments: [
          /<%[\s\S]*?%>/, // ignore default fragments
          /<\?[\s\S]*?\?>/
        ],
        collapseWhitespace: true, // collapse white space that contributes to text nodes in a document tree
        removeComments: true, // strip HTML comments
        removeOptionalTags: true, // remove optional tags http://perfectionkills.com/experimenting-with-html-minifier/#remove_optional_tags
        caseSensitive: true // treat attributes in case sensitive manner (useful for custom HTML tags)
      })

      if (!result) {
        callback(Error('Error minifying file: ' + filename + '.\n'))
        return
      } else {
        console.log(`${twoSpaces + twoSpaces}${platform.name}:${path.relative(wwwDistDir, filename)}`)
        fs.writeFileSync(filename, result, 'utf8')
      }
    }
    next()
  })

  walker.on('errors', function (root, nodeStatsArray, next) {
    callback(Error('There was an error with' + nodeStatsArray.name))
  })

  walker.on('end', function () {
    callback()
  })
}

function getFileExtension (fileName) {
  return fileName.split('.').pop()
}

Simply test it by doing cordova prepare and then check the content of dist directory (for example in Android platforms/android/app/src/main/assets/www) to be sure the files are minified.

@peace-for-God
Copy link

Thank you for this gist.

  1. When you say, create a file hooks/convertHbsToHtml.js did you mean hooks/minifyFiles.js

  2. Since https://www.npmjs.com/package/uglify-es is deprecated, suggest to switch to: https://www.npmjs.com/package/uglify-js

@jfoclpf
Copy link
Author

jfoclpf commented Jul 18, 2022

@peace-for-God thank you for the remarks. Updated accordingly.

I guess the API for uglify-js and uglify-es are exactly the same, right?

@jfoclpf
Copy link
Author

jfoclpf commented Jul 18, 2022

I guess the API for uglify-js and uglify-es are exactly the same, right?

They are indeed the same

@peace-for-God
Copy link

@peace-for-God thank you for the remarks. Updated accordingly.

I guess the API for uglify-js and uglify-es are exactly the same, right?

I think they are compatible, per: https://www.npmjs.com/package/uglify-es which says: ... uglify-es is API/CLI compatible with uglify-js@3....

When i substituted and used uglify-js in my Cordova app, it worked without a problem. But I have not tested it beyond that.

@jfoclpf
Copy link
Author

jfoclpf commented Jul 18, 2022 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment