Cordova-app-loader

Hi everybody,

does anyone has some experience with

  • Are there any issues with IOS / Android ( Violation of App Store rules ?)
  • Integration with Ionic / AngularJS ?

I know the Ionic Team is working on a solution to update apps on the fly too, which i will use when its ready.

Regards

3 Likes
  1. So there shouldn’t be an issue with the iOS app store.

3.3.2 An Application may not download or install executable code. Interpreted code may only be used in an Application if all scripts, code and interpreters are packaged in the Application and not downloaded. The only exception to the foregoing is scripts and code downloaded and run by Apple’s built-in WebKit framework, provided that such scripts and code do not change the primary purpose of the Application by providing features or functionality that are inconsistent with the intended and advertised purpose of the Application as submitted to the App Store.

  1. Havne’t used that plugin, but there are other options if that one doesn’t work for you.

THX for this anwser i will report my results. The APP cache way looks really interesting

Yes, app cache is pretty good, but its got some edges to it.

I implemented Cordova-app-loader in my Ionic app yesterday. It was a long day :smile: , but most of the time was spent messing with Grunt (which I’m not good at) to automate everything. It seems to work well so far, the only issue that I’ve seen since integrating it was this one: https://github.com/driftyco/ionic/issues/2977 which didn’t used to occur. I’m guessing the dynamic loading of all the files is messing up some timing thing with Ionic somewhere.

1 Like

Hi ,

is it possible to see some code, Im interested in your grunt and and updateService

Regards

The service is pretty straightforward. Call Bootstrap.check() and if there’s an update it will prompt the user (once per app load) to download the update. If they choose OK, it does the download and then applies the update and reloads.

.factory('Bootstrap', function($ionicPopup, $rootScope, Toast, LoadingService) {
    var checked = false;

    return {
        reset: function() {
            checked = false;
        },
        hasBeenChecked: function() {
            return checked;
        },
        check: function() {
            // stuff for hot code push. 

            // only do one check. 
            if (checked) {
                return;
            }
            checked = true;

            // Initialize filesystem and loader
            var fs = new CordovaPromiseFS({
                persistent: $rootScope.vars.isCordovaApp, // Chrome should use temporary storage.
                Promise: Promise
            });

            var loader = new CordovaAppLoader({
                fs: fs,
                localRoot: 'app',
                serverRoot: $rootScope.vars.baseURL + 'webapp/',
                mode: 'mirror',
                cacheBuster: true,
                checkTimeout: 10000
            });

            loader.check().then(
                function (updateAvailable) {
                    if (updateAvailable) {
                        var confirmPopup = $ionicPopup.confirm({
                            title: 'Update Available!',
                            template: "<div align='center'>There's a new mini-update available! Get it now?</div>"
                        });
                        confirmPopup.then(function(res) {
                            if (res) {
                                LoadingService.show ("Downloading the update!");
                                loader.download().then(
                                    function(manifest) {
                                        loader.update().then(
                                            function() {
                                                LoadingService.hide();
                                                Toast.show("App updated!");
                                            },
                                            function() {
                                                LoadingService.hide();
                                                Toast.show ("Couldn't apply the update, try again later!");
                                            }
                                        );
                                    },
                                    function(failedDownloadUrlArray) {
                                        LoadingService.hide();
                                        Toast.show ("Couldn't download the update, try again later!");
                                    }
                                );

                            }
                            else {
                                // user has said not to update. do nothing. 
                                Toast.show ("OK! We'll try again later...");                                    
                            }
                        });
                    }
                    else {
                        // no update available, do nothing. 
                        console.log ("NO UPDATE AVAILABLE");
                    }
                },
                function () {
                    // some problem, fail silently. 
                }
            );
        }
    };
}) 

For Grunt:
I just took the Grunt code from here: https://gist.github.com/lylepratt/d8bf84b3b7d6932e3549 and integrated it into my existing Grunt script (which was from an old version of the Ionic Yeoman Generator: https://github.com/diegonetto/generator-ionic) to automatically create the manifest.json in my www folder. Then in my index.html file, where usemin was being used to concat/uglify/minify the js and css into a single file, I needed it to not write the result into index.html, since all those files are now loaded by the bootstrap.js loader.

So index.html now has sections that look like:

    <!-- build:myjs scripts/scripts.js -->
    <script src="scripts/config.js"></script>
    <script src="scripts/app.js"></script>
    <script src="scripts/controllers.js"></script>
    <script src="scripts/services.js"></script>
  <!-- endbuild -->

That use the “myjs” block replacement instead of the standard js one.

It also loads the bootstrap.js file:

<!-- bootstrap script (this will fail to load manifest.json during development) -->
<script type="text/javascript" timeout="10000" manifest="manifest.json" src="bootstrap/bootstrap.js"></script>

All the Cordova-app-loader files (bootstrap.js, cordova-app-loader-complete.min.js) get loaded from the new “bootstrap” directory.

My entire gruntfile is below. Note that I have no idea how most of it works :smile:

// Generated on 2014-09-05 using generator-ionic 0.5.3
'use strict';

var _ = require('lodash');
var path = require('path');
var cordova = require('cordova');
var spawn = require('child_process').spawn;

module.exports = function (grunt) {

  // Load grunt tasks automatically
  require('load-grunt-tasks')(grunt);

  // Time how long tasks take. Can help when optimizing build times
  require('time-grunt')(grunt);

  // Define the configuration for all the tasks
  grunt.initConfig({

    // Project settings
    yeoman: {
      // configurable paths
      app: 'app',
      scripts: 'scripts',
      styles: 'styles',
      images: 'images'
    },

    // Environment Variables for Angular App
    // This creates an Angular Module that can be injected via ENV
    // Add any desired constants to the ENV objects below.
    // https://github.com/diegonetto/generator-ionic#environment-specific-configuration
    ngconstant: {
      options: {
        space: '  ',
        wrap: '"use strict";\n\n {%= __ngModule %}',
        name: 'config',
        dest: '<%= yeoman.app %>/scripts/config.js'
      },
      development: {
        constants: {
          ENV: {
            name: 'development',
            apiEndpoint: 'http://dev.yoursite.com:10000/'
          }
        }
      },
      production: {
        constants: {
          ENV: {
            name: 'production',
            apiEndpoint: 'http://api.yoursite.com/'
          }
        }
      }
    },

    // Watches files for changes and runs tasks based on the changed files
    watch: {
      bower: {
        files: ['bower.json'],
        tasks: ['wiredep']
      },
      js: {
        files: ['<%= yeoman.app %>/<%= yeoman.scripts %>/**/*.js'],
        tasks: ['newer:jshint:all'],
        options: {
          livereload: true
        }
      },
      styles: {
        files: ['<%= yeoman.app %>/<%= yeoman.styles %>/**/*.css'],
        tasks: ['newer:copy:styles', 'autoprefixer']
      },
      livereload: {
        options: {
          livereload: '<%= connect.options.livereload %>'
        },
        files: [
          '<%= yeoman.app %>/*.html',
          '<%= yeoman.app %>/templates/**/*.html',
          '.tmp/<%= yeoman.styles %>/**/*.css',
          '<%= yeoman.app %>/<%= yeoman.images %>/**/*.{png,jpg,jpeg,gif,webp,svg,caf}'
        ]
      }
    },

    // The actual grunt server settings
    connect: {
      options: {
        port: 9000,
        // Change this to '0.0.0.0' to access the server from outside.
        hostname: 'localhost',
        livereload: 35729
      },
      livereload: {
        options: {
          open: true,
          base: [
            '.tmp',
            '<%= yeoman.app %>'
          ]
        }
      },
      dist: {
        options: {
          base: 'www'
        }
      },
      coverage: {
        options: {
          port: 9002,
          open: true,
          base: ['coverage']
        }
      }
    },

    // Make sure code styles are up to par and there are no obvious mistakes
    jshint: {
      options: {
        jshintrc: '.jshintrc',
        reporter: require('jshint-stylish')
      },
      all: [
        'Gruntfile.js',
        '<%= yeoman.app %>/<%= yeoman.scripts %>/**/*.js'
      ],
      test: {
        options: {
          jshintrc: 'test/.jshintrc'
        },
        src: ['test/unit/**/*.js']
      }
    },

    // Empties folders to start fresh
    clean: {
      dist: {
        files: [{
          dot: true,
          src: [
            '.tmp',
            'www/*',
            '!www/.git*'
          ]
        }]
      },
      server: '.tmp'
    },

    autoprefixer: {
      options: {
        browsers: ['last 1 version']
      },
      dist: {
        files: [{
          expand: true,
          cwd: '.tmp/<%= yeoman.styles %>/',
          src: '{,*/}*.css',
          dest: '.tmp/<%= yeoman.styles %>/'
        }]
      }
    },

    // Automatically inject Bower components into the app
    wiredep: {
      options: {
        cwd: '<%= yeoman.app %>'
      },
      app: {
        src: ['<%= yeoman.app %>/index.html'],
        ignorePath:  /\.\.\//
      }
    },

    

    // Reads HTML for usemin blocks to enable smart builds that automatically
    // concat, minify and revision files. Creates configurations in memory so
    // additional tasks can operate on them
    useminPrepare: {
      html: '<%= yeoman.app %>/index.html',
      options: {
        dest: 'www',
        flow: {
          html: {
            steps: {
              js: ['concat', 'uglifyjs'],
              css: ['cssmin'],
              myjs: ['concat', 'uglifyjs'],
              mycss: ['cssmin']
            },
            post: {}
          }
        }
      }
    },

    // Performs rewrites based on the useminPrepare configuration
    usemin: {
      html: ['www/**/*.html'],
      css: ['www/<%= yeoman.styles %>/**/*.css'],
      options: {
        assetsDirs: ['www'],
        blockReplacements: {
            myjs: function(block) {
                return '';
            },            
            mycss: function(block) {
                return '';
            }
        }
      }
    },

    // The following *-min tasks produce minified files in the dist folder
    cssmin: {
      options: {
        root: '<%= yeoman.app %>',
        noRebase: true
      }
    },
    htmlmin: {
      dist: {
        options: {
          collapseWhitespace: true,
          collapseBooleanAttributes: true,
          removeCommentsFromCDATA: true,
          removeOptionalTags: true
        },
        files: [{
          expand: true,
          cwd: 'www',
          src: ['*.html', 'templates/**/*.html'],
          dest: 'www'
        }]
      }
    },

    // Copies remaining files to places other tasks can use
    copy: {
      dist: {
        files: [{
          expand: true,
          dot: true,
          cwd: '<%= yeoman.app %>',
          dest: 'www',
          src: [
            'images/**/*.{png,jpg,jpeg,gif,webp,svg,caf}',
            '*.html',
            'templates/**/*.html',
            'fonts/*',
            'bootstrap/*'
          ]
        }, {
          expand: true,
          cwd: '.tmp/<%= yeoman.images %>',
          dest: 'www/<%= yeoman.images %>',
          src: ['generated/*']
        }]
      },
      styles: {
        expand: true,
        cwd: '<%= yeoman.app %>/<%= yeoman.styles %>',
        dest: '.tmp/<%= yeoman.styles %>/',
        src: '{,*/}*.css'
      },
      fonts: {
        expand: true,
        cwd: 'app/bower_components/ionic/release/fonts/',
        dest: '<%= yeoman.app %>/fonts/',
        src: '*'
      },
      vendor: {
        expand: true,
        cwd: '<%= yeoman.app %>/vendor',
        dest: '.tmp/<%= yeoman.styles %>/',
        src: '{,*/}*.css'
      },
      all: {
        expand: true,
        cwd: '<%= yeoman.app %>/',
        src: '**',
        dest: 'www/'
      }
    },

    concurrent: {
      server: [
        'copy:styles',
        'copy:vendor',
        'copy:fonts'
      ],
      test: [
        'copy:styles',
        'copy:vendor',
        'copy:fonts'
      ],
      dist: [
        'copy:styles',
        'copy:vendor',
        'copy:fonts'
      ]
    },

    // By default, your `index.html`'s <!-- Usemin block --> will take care of
    // minification. These next options are pre-configured if you do not wish
    // to use the Usemin blocks.
    // cssmin: {
    //   dist: {
    //     files: {
    //       'www/<%= yeoman.styles %>/main.css': [
    //         '.tmp/<%= yeoman.styles %>/**/*.css',
    //         '<%= yeoman.app %>/<%= yeoman.styles %>/**/*.css'
    //       ]
    //     }
    //   }
    // },
    // uglify: {
    //   dist: {
    //     files: {
    //       'www/<%= yeoman.scripts %>/scripts.js': [
    //         'www/<%= yeoman.scripts %>/scripts.js'
    //       ]
    //     }
    //   }
    // },
    // concat: {
    //   dist: {}
    // },

    // Test settings
    // These will override any config options in karma.conf.js if you create it.
    karma: {
      options: {
        basePath: '',
        frameworks: ['mocha', 'chai'],
        files: [
          '<%= yeoman.app %>/bower_components/angular/angular.js',
          '<%= yeoman.app %>/bower_components/angular-animate/angular-animate.js',
          '<%= yeoman.app %>/bower_components/angular-sanitize/angular-sanitize.js',
          '<%= yeoman.app %>/bower_components/angular-ui-router/release/angular-ui-router.js',
          '<%= yeoman.app %>/bower_components/ionic/release/js/ionic.js',
          '<%= yeoman.app %>/bower_components/ionic/release/js/ionic-angular.js',
          '<%= yeoman.app %>/bower_components/angular-mocks/angular-mocks.js',
          '<%= yeoman.app %>/<%= yeoman.scripts %>/**/*.js',
          'test/mock/**/*.js',
          'test/spec/**/*.js'
        ],
        autoWatch: false,
        reporters: ['dots', 'coverage'],
        port: 8080,
        singleRun: false,
        preprocessors: {
          // Update this if you change the yeoman config path
          'app/scripts/**/*.js': ['coverage']
        },
        coverageReporter: {
          reporters: [
            { type: 'html', dir: 'coverage/' },
            { type: 'text-summary' }
          ]
        }
      },
      unit: {
        // Change this to 'Chrome', 'Firefox', etc. Note that you will need
        // to install a karma launcher plugin for browsers other than Chrome.
        browsers: ['PhantomJS'],
        background: true
      },
      continuous: {
        browsers: ['PhantomJS'],
        singleRun: true,
      }
    },

    // ngAnnotate tries to make the code safe for minification automatically by
    // using the Angular long form for dependency injection.
    ngAnnotate: {
      dist: {
        files: [{
          expand: true,
          cwd: '.tmp/concat/<%= yeoman.scripts %>',
          src: '*.js',
          dest: '.tmp/concat/<%= yeoman.scripts %>'
        }]
      }
    },

    //jsonmanifest settings
    jsonmanifest: {
      generate: {
        options: {
          basePath: 'www',
          exclude: [],
          //load all found assets
          loadall: false,
          //manually add files to the manifest
          files: {},
          //manually define the files that should be injected into the page
          load: ["bootstrap/cordova-app-loader-complete.min.js", "styles/vendor.css", "styles/main.css", "scripts/vendor.js", "scripts/scripts.js"],
          // root location of files to be loaded in the load array.
          root: "./"
        },
        src: [
            'bootstrap/*.js',
            'scripts/*.js',
            'styles/*.css',
            'templates/*.html',
            'fonts/*.*',
            'images/*.*'
        ],
        dest: ['www/manifest.json']
      }
    }    

  });

  // Register tasks for all Cordova commands, but namespace
  // the cordova:build since we already have a build task.
  _.functions(cordova).forEach(function (name) {
    name = (name === 'build') ? 'cordova:build' : name;
    grunt.registerTask(name, function () {
      this.args.unshift(name.replace('cordova:', ''));
      // Handle URL's being split up by Grunt because of `:` characters
      if (_.contains(this.args, 'http') || _.contains(this.args, 'https')) {
        this.args = this.args.slice(0, -2).concat(_.last(this.args, 2).join(':'));
      }
      var done = this.async();
      var exec = process.platform === 'win32' ? 'cordova.cmd' : 'cordova';
      var cmd = path.resolve('./node_modules/cordova/bin', exec);
      var child = spawn(cmd, this.args);
      child.stdout.on('data', function (data) {
        grunt.log.writeln(data);
      });
      child.stderr.on('data', function (data) {
        grunt.log.error(data);
      });
      child.on('close', function (code) {
        code = (name === 'cordova:build') ? true : code ? false : true;
        done(code);
      });
    });
  });

  //GRUNT TASK TO BUILD A JSON MANIFEST FILE FOR HOT CODE UPDATES
  grunt.registerMultiTask('jsonmanifest', 'Generate JSON Manifest for Hot Updates', function () {
 
    var options = this.options({loadall:true, root: "./", files: {}, load: []});
    var done = this.async();
 
    var path = require('path');
 
    this.files.forEach(function (file) {
      var files;
 
      //manifest format
      var json = {
        "files": options.files,
        "load": options.load,
        "root": options.root
      };
 
      //clear load array if loading all found assets
      if(options.loadall) {
        json.load = [];
      }
 
      // check to see if src has been set
      if (typeof file.src === "undefined") {
        grunt.fatal('Need to specify which files to include in the json manifest.', 2);
      }
 
      // if a basePath is set, expand using the original file pattern
      if (options.basePath) {
        files = grunt.file.expand({cwd: options.basePath}, file.orig.src);
      } else {
        files = file.src;
      }
 
      // Exclude files
      if (options.exclude) {
        files = files.filter(function (item) {
          return options.exclude.indexOf(item) === -1;
        });
      }

      // Set default destination file
      if (!file.dest) {
        file.dest = ['manifest.json'];
      }


 
      // add files
      if (files) {
        files.forEach(function (item) {
            var hasher = require('crypto').createHash('sha256');
            var filename = encodeURI(item);
            
            //var key = filename.split("-").slice(1).join('-');
            var key = filename.split("/");
            key = key[key.length -1];
            
            json.files[key] = {}
            json.files[key]['filename'] = filename;
            json.files[key]['version'] = hasher.update(grunt.file.read(path.join(options.basePath, item))).digest("hex")
 
            if(options.loadall) {
              json.load.push(filename);  
            }
        });
      }
      //write out the JSON to the manifest files
      
      file.dest.forEach(function(f) {
        grunt.file.write(f, JSON.stringify(json, null, 2));
      });

      done();
    });
 
  });  


  // Since Apache Ripple serves assets directly out of their respective platform
  // directories, we watch all registered files and then copy all un-built assets
  // over to www/. Last step is running cordova prepare so we can refresh the ripple
  // browser tab to see the changes. Technically ripple runs `cordova prepare` on browser
  // refreshes, but at this time you would need to re-run the emulator to see changes.
  grunt.registerTask('ripple', ['wiredep', 'copy:all', 'ripple-emulator']);
  grunt.registerTask('ripple-emulator', function () {
    grunt.config.set('watch', {
      all: {
        files: _.flatten(_.pluck(grunt.config.get('watch'), 'files')),
        tasks: ['copy:all', 'prepare']
      }
    });

    var cmd = path.resolve('./node_modules/ripple-emulator/bin', 'ripple');
    var child = spawn(cmd, ['emulate']);
    child.stdout.on('data', function (data) {
      grunt.log.writeln(data);
    });
    child.stderr.on('data', function (data) {
      grunt.log.error(data);
    });
    process.on('exit', function (code) {
      child.kill('SIGINT');
      process.exit(code);
    });

    return grunt.task.run(['watch']);
  });

  // Dynamically configure `karma` target of `watch` task so that
  // we don't have to run the karma test server as part of `grunt serve`
  grunt.registerTask('watch:karma', function () {
    var karma = {
      files: ['<%= yeoman.app %>/<%= yeoman.scripts %>/**/*.js', 'test/spec/**/*.js'],
      tasks: ['newer:jshint:test', 'karma:unit:run']
    };
    grunt.config.set('watch', karma);
    return grunt.task.run(['watch']);
  });

  grunt.registerTask('serve', function (target) {
    if (target === 'dist') {
      return grunt.task.run(['build', 'connect:dist:keepalive']);
    }

    grunt.task.run([
      'clean:server',
      'wiredep',
      'concurrent:server',
      'autoprefixer',
      'connect:livereload',
      'watch'
    ]);
  });

  grunt.registerTask('test', [
    'clean:server',
    'concurrent:test',
    'autoprefixer',
    'karma:unit:start',
    'watch:karma'
  ]);

  grunt.registerTask('build', [
    'clean:dist',
    'wiredep',
    'useminPrepare',
    'concurrent:dist',
    'autoprefixer',
    'concat',
    'ngAnnotate',
    'copy:dist',
    'cssmin',
    'uglify',
    'usemin',
    'htmlmin',
    'cordova:build'
  ]);

	grunt.registerTask('buildweb', [
		'clean:dist',
		'wiredep',
		'useminPrepare',        
		'concurrent:dist',
		'autoprefixer',
		'concat',        
		'ngAnnotate',
		'copy:dist',        
		'cssmin',
		'uglify',        
		'usemin',        
		'htmlmin',
        'jsonmanifest',
        'prepare'
	]);


  grunt.registerTask('cordova', ['copy:all', 'cordova:build']);

  grunt.registerTask('web', ['copy:all']);

  grunt.registerTask('android', ['copy:all', 'cordova:run:android']);

  grunt.registerTask('coverage', ['karma:continuous', 'connect:coverage:keepalive']);

  grunt.registerTask('default', [
    'buildweb'
  ]);


/*
  grunt.registerTask('default', [
    'newer:jshint',
    'karma:continuous',
    'build'
  ]);
*/  

};
2 Likes

Thank you very much for the detailed answer.

I will post my solution too, asap

Regards

Just submitted my app for review today, and actually had to remove Cordova-app-loader for the time being due to this problem: https://github.com/markmarijnissen/cordova-app-loader/issues/17

1 Like

Rajatrocks, Thanks for sharing your experience here. I’ve spent the morning ramping up on cordova-app-loader and was about to burn another half-day implementing it but I guess I’ll hold off for now.

Just curious, did Apple reject your app or did you voluntarily remove it because you (or they?) discovered this issue?

They didn’t reject it, I pulled it out when I discovered the issue.

Thanks. Would love to hear if you manage to solve this issue.

Think I found a solution: https://github.com/markmarijnissen/cordova-app-loader/issues/17

Hey @rajatrocks. I tried unsuccessfully as I’m just starting out with ionic / angular, but it would be cool if we could create a sample ionic app that has cordova-app-loader working. You seem to have a good understanding of some of the hiccups that one could encounter when trying to get it working. I’d be happy to contribute if you post a link to the repo but I’m not sure I have the chops to lead the endeavor.

Well, I have the initial loading working, but now I’m hitting problems elsewhere (content not updating properly). If I ever do get it all working, I’ll see if I can put something together.

Hi
i ve created a small example app with ionic and cordova app loader

It works well in chrome and android

i added the update function to a controller

$scope.checkUpdate = function () {
                var fs = new CordovaPromiseFS({
                    Promise: $q
                });
                // Initialize a CordovaAppLoader
                var loader = new CordovaAppLoader({
                    fs: fs,
                    serverRoot: 'http://localhost:8080/',
                    localRoot: 'app',
                    cacheBuster: true, // make sure we're not downloading cached files.
                    checkTimeout: 10000 // timeout for the "check" function - when you loose internet connection
                });
                loader.check().then(function (updateAvailable) {
                    console.log(updateAvailable);
                    if (updateAvailable)
                    {
                        loader.download()
                                .then(
                                        function (manifest)
                                        {
                                            console.log(manifest);
                                            loader.update();  // we can update the app
                                        },
                                        function (failedDownloadUrlArray)
                                        {
                                            console.log(failedDownloadUrlArray);
                                        }
                                )
                    }
                });
            }

Code

2 Likes

hi… i’m also having trouble with getting the changed templates to load properly…js and css is fine, but html template content not

Yeah, I’m not getting CSS applied properly on Android. Going to experiment with application Cache tomorrow and see if that’s a better solution.

css seems to work fine for me on all platforms including android…

Would be great to if share your know knowledge about current issues.
Which Android Version have you tested ?