5

We have an Ionic 2 App that is deployed natively and also to the web. When building I use npm run build --prod --release. That just wraps ionic build.

I'm trying to update our build process to be able to swap out the default main.js. that is in the index.html

So I want to be able to change this file from:

<script src="build/main.js"></script>

with (autogenerated hash)

<script src="build/main.7b297e8f7d1c2760a1bc.js"></script>

Step 1 is to generate the file. I was able to successfully generate the right file each build by using the webpack output.filename setting.

module.exports = {
   entry: [process.env.IONIC_APP_ENTRY_POINT, './web.config', './src/ai.min.js'],
   output: {
    path: '{{BUILD}}',
    filename: '[name].[chunkhash].js',

When I build I can see it's correctly generating the source file but shortly after completing the ionic build fails with a message saying it can't find build/main.js. That was the original file name so I think I need to somehow let ionic know I'm changing the name of the main.js file.

Error:

[11:00:32] build prod failed: ENOENT: no such file or directory, open '/Users/work/client/www/build/main.js' [11:00:32] ionic-app-script task: "build" [11:00:32] Error: ENOENT: no such file or directory, open '/Users/work/client/www/build/main.js'

I'm not sure how to update ionic build so that it knows to look for the dynamically generated main.js filename.

Nix
  • 57,072
  • 29
  • 149
  • 198

3 Answers3

4

EDIT

Much simpler solution that is much less likely to break when ionic has manjor updates.

https://gist.github.com/haydenbr/7df417a8678efc404c820c61b6ffdd24


So cache busting with ionic. It's a hackish solution but it works for now. The problem is ionic build system abstracts a little too much at times. Back in October, it was asked if there was a way to implement cache busting. The ionic team responded that they might consider it in the future, but there hasn't been any activity on it since. Here's the github issue.

So I'll show all the changes to webpack config and package.json and then explain everything.

The config section of your package.json should look like this.

  "config": {
    "ionic_webpack": "./webpack.config.js",
    "ionic_source_map_type": "source-map",
    "ionic_uglifyjs": "./www/uglifyjs.config.json"
  }

For your webpack config, your entry and output can remain the same. Make sure you have required the following modules and then you'll want to add the following plugins:

var path = require('path'),
    fs = require('fs'),
    ManifestPlugin = require('webpack-manifest-plugin'),
    HtmlWebpackPlugin = require('html-webpack-plugin');

...

plugins: [
  new HtmlWebpackPlugin({
    filename: './../index.html',
    inject: 'body',
    template: './src/index.html',
    title: 'My App',
  }),
  new ManifestPlugin(),
  updateFileName
]

where updateFileName is as follows

function updateFileName() {
  this.plugin("done", function() {
  var manifest = require(process.env.IONIC_BUILD_DIR + '/manifest.json'),
      fileName = process.env.IONIC_OUTPUT_JS_FILE_NAME;

    updateUglifyConfig(fileName, manifest);

    process.env.IONIC_OUTPUT_JS_FILE_NAME = manifest[fileName];
  });
}

function updateUglifyConfig(fileName, manifest) {
  var uglifyConfig = {
    sourceFile: manifest[fileName],
    destFileName: manifest[fileName],
    inSourceMap: manifest[fileName + '.map'],
    outSourceMap: manifest[fileName + '.map'],
    mangle: true,
    compress: true,
    comments: true
  };

  // we're writing this to www because it's specific to the current
  // build and we don't want to commit it
  fs.writeFileSync(
    path.join(__dirname, 'www', 'uglifyjs.config.json'),
    JSON.stringify(uglifyConfig, null, 4)
  );
}

So what's actually happening here? Fist off, in the package.json, we are going to have to generate a new uglify config for the ionic build process. You can change the file name in the middle of the build and as long as you assign the new name to process.env.IONIC_OUTPUT_JS_FILE_NAME then the rest of the build will work fine, except the uglify step will still look for the default name, main.js. We'll see how we generate that below.

Now for the three pluggins we're adding.

The first one does some magic. It's really configurable. How it's set up, it takes a default index.html, sets the title, injects a <script> tag for the javascript output, and then write's it to where you've specified in the filename property. If you're using the default index.html that comes from an ionic starter app, then all you need to do is get rid of the <script src="build/main.js"></script> and it'll automagically put the new link in their for the filename with the hash in it. Docs here.

The next plugin outputs a manifest file for us so that we can know what the filename is with the hash. By default, it outputs it in www/build/. Docs here.

The next plugin is what assigns the new file name to process.env.IONIC_OUTPUT_JS_FILE_NAME and generates the new uglify config for us. Pretty much we grab the manifest that was output, write a new uglify config to the www directory, and then assign the new file name from what we got out of the manifest.

So pretty much that's it. If you don't want the cache busting for dev, keep the html pluggin, get rid of the other two, and then change the output filename back to process.env.IONIC_OUTPUT_JS_FILE_NAME. If you do this, you wont need to reference the main js file in your src/index.html at all. It'll get put in whether your running dev or prod. For more info on using different webpack settups for different environments, check this out.

UPDATE:

For ionic 3:

  1. Make sure you have these settings in the compilerOptions of your tsconfig:

"module": "es2015", "target": "es5"

  1. npm i cheerio --save-dev
  2. in your webpack config add var cheerio = require('cheerio')
  3. Get rid of the Webpack Manifest plugin.
  4. Change updateFileName to the following:

    function updateFileName() {
      this.plugin("done", function(stats) {
        var buildOutput = stats.toJson()['assetsByChunkName']['main'],
            fileName = process.env.IONIC_OUTPUT_JS_FILE_NAME,
            manifest = {
              [fileName]: buildOutput[0],
              [fileName + '.map']: buildOutput[1]
            };
    
        updateUglifyConfig(fileName, manifest);
    
        process.env.IONIC_OUTPUT_JS_FILE_NAME = manifest[fileName];
        console.log('IONIC_OUTPUT_JS_FILE_NAME', process.env.IONIC_OUTPUT_JS_FILE_NAME);
      });
    }
    
  5. Get rid of the Html Webpack Plugin

  6. In place of the html plugin, put the following function in the plugins array in your webpack config:

    function updateIndexHTML() {
    this.plugin("done", function(stats) {
    var buildOutput = stats.toJson()['assetsByChunkName']['main'],
        outputFileName = buildOutput[0],
        currentIndexHTML = fs.readFileSync(
          path.join(__dirname, 'src', 'index.html'),
          { encoding: 'utf8' }
        ),
        $ = cheerio.load(currentIndexHTML);
    
    $('body').append(`<script src="build/${outputFileName}"></script>`);
    
    fs.writeFileSync(
      path.join(process.env.IONIC_WWW_DIR, 'index.html'),
      $.html()
    );
      });
    }
    
Hayden Braxton
  • 1,151
  • 9
  • 14
  • 1
    I already put this in our project @Nix so you can check it out there :). – Hayden Braxton Apr 15 '17 at 17:54
  • Hi, using your code Ionic v3 and it builds OK, but it does not hash the files. What do i have to change to hash the files? i have tried seting the process.env.IONIC_OUTPUT_JS_FILE_NAME in the updateFileName method but always use main.js – tito.icreativos Jun 26 '17 at 10:56
  • I'll take a look tonight and make sure I have updated everything properly. I guess it would be helpful just to post a complete webpack config for this. First thing, make sure you have `output: { path: '{{BUILD}}', filename: '[name].[chunkhash].js' }` in your config. – Hayden Braxton Jun 27 '17 at 11:30
  • if put the `output: { path: '{{BUILD}}', filename: '[name].[chunkhash].js' }`, development mode will not work. This is the error `Uncaught reflect-metadata shim is required when using class decorators` at browser console. But if only build for production mode. Then its working + lazy load as well. – ChokYeeFan Sep 28 '17 at 05:37
  • After the app loaded already, then press refresh again it will cause the issue `Uncaught reflect-metadata shim is required when using class decorators`. Any ideas? – ChokYeeFan Oct 03 '17 at 01:44
4

I found a better solution to this problem, from an ionic forum thread (https://forum.ionicframework.com/t/file-revisions/75028/2, by aszmyd) that solves the issue for hashing the main.css file name as well. I made small tweaks to the script, since I don't have oauth.html or kajam.js.

The advantage of this solution is that it doesn't try and intercept the ionic build, but just works on the results.

(Undoubtedly, someone will find fault with the way I post this, but it has been very useful to me, and I hope to others. I can't imagine having a web app without complete cache-busting for all css and js files.)

To run this, just add:

node <the-file-name.js>

in your build, after ionic script build is finished.

#!/usr/bin/env node
'use strict';

var md5File = require('md5-file'),
    fs = require('fs');

/**
 * This script renames files inside platforms/browser/www/ folder and updates their references in html files like index.html
 * The mechanism is for improve caching. So file like `main.js` will be renamed to `main.[FILE-MD5-HASH].js` and its references
 * in html files will be updated.
 */
var buildFolder = 'www/';
var assetsFolder = buildFolder + 'build/';

var jsFiles = [
    'main'
];
var cssFiles = [
    'main'
];
var htmlFilesToUpdate = [
    'index.html'
];
var replacements = [];

jsFiles.forEach(function (file) {
    var hash = md5File.sync(assetsFolder + file + '.js');
    renameFile(file + '.js', file + '.' + hash + '.js');
});

cssFiles.forEach(function (file) {
    var hash = md5File.sync(assetsFolder + file + '.css');
    renameFile(file + '.css', file + '.' + hash + '.css');
});
htmlFilesToUpdate.forEach(function (htmlFile) {
    console.log('Update "' + htmlFile + '" with new file revisions.');
    console.log('Replacements: ' + JSON.stringify(replacements));
    replacements.forEach(function (replacementObject) {
        replaceInFile(buildFolder + htmlFile, replacementObject.from, replacementObject.to);
    });
});

function renameFile(input, output) {
    console.log('Rename "' + input + '" to "' + output + '"');
    fs.rename(assetsFolder + input, assetsFolder + output);
    if (fs.existsSync(assetsFolder + input + '.map')) {
        console.log('Rename "' + input + '.map" to "' + output + '.map"');
        fs.rename(assetsFolder + input + '.map', assetsFolder + output + '.map');
    }
    replacements.push({from: input, to: output});
}

function replaceInFile(file, regex, replacement) {
    var fileContents = fs.readFileSync(file, 'utf-8');
    fs.writeFileSync(file, fileContents.replace(regex, replacement), 'utf8');
}
  • 1
    Is this solution support ionic v3 + lazy load? – ChokYeeFan Sep 28 '17 at 01:54
  • @ChokYeeFan lazy modules are for the moment 0.js, 1.js, ... So it doesn't work because angular doesn't know internally that the filenames have changed, also looking for a solution – tobika Sep 28 '17 at 13:24
0

This solution works perfectly with Ionic 2.x & 3.x

#!/usr/bin/env node

var fs = require('fs'),
    path = require('path'),
    cheerio = require('cheerio'),
    revHash = require('rev-hash');

/**
 *
 * @param string fileName
 * @returns string
 */
function hashFile(file) {

    // Get file name
    var fileName = file.replace(/\.[^/.]+$/, "");
    // Get file extension
    var re = /(?:\.([^.]+))?$/;
    var fileExtension = re.exec(file)[1];

    var filePath = path.join(buildDir, file);
    var fileHash = revHash(fs.readFileSync(filePath));
    var fileNewName = `${fileName}.${fileHash}.${fileExtension}`;
    var fileNewPath = path.join(buildDir, fileNewName);
    var fileNewRelativePath = path.join('build', fileNewName);
    //Rename file
    console.log("cache-busting.js:hashFile:Renaming " + filePath + " to " + fileNewPath);
    fs.renameSync(filePath, fileNewPath);

    return fileNewRelativePath;
}


var rootDir = path.resolve(__dirname);
var wwwRootDir = path.resolve(rootDir, 'www');
var buildDir = path.join(wwwRootDir, 'build');
var indexPath = path.join(wwwRootDir, 'index.html');
$ = cheerio.load(fs.readFileSync(indexPath, 'utf-8'));

$('head link[href="build/main.css"]').attr('href', hashFile('main.css'));
$('body script[src="build/main.js"]').attr('src', hashFile('main.js'));
$('body script[src="build/polyfills.js"]').attr('src', hashFile('polyfills.js'));
$('body script[src="build/vendor.js"]').attr('src', hashFile('vendor.js'));

fs.writeFileSync(indexPath, $.html());
anlijudavid
  • 509
  • 6
  • 12