Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular "state already defined" error after running concat/uglify

Tags:

angularjs

I am developing an AngularJS application. To ship the code in production, I'm using this Grunt configuration/task:

  grunt.registerTask( 'compile', [
    'sass:compile', 'copy:compile_assets', 'ngAnnotate', 'concat:compile_js', 'uglify', 'index:compile'
  ]);

It's really hard to debug, and it's kind of a question to people who already ran into such problems and can point to some direction.

My main module is including those submodules:

angular
  .module('controlcenter', [
    'ui.router',
    'ui.bootstrap',
    'templates-app',
    'templates-common',
    'authentication',
    'api',
    'reports',
    'interceptors',
    'controlcenter.websites',
    'controlcenter.users',
    'controlcenter.campaigns',
    'controlcenter.reports',
    'controlcenter.login'
  ])
  .run(run);

The error I get is following:

Uncaught Error: [$injector:modulerr] Failed to instantiate module controlcenter due to:
Error: [$injector:modulerr] Failed to instantiate module controlcenter.websites due to:
Error: State 'websites'' is already defined

If I remove the websites module, I get the same error for controlcenter.users.

I am using the ui-router to handle routing inside the app.

After my build process (for integration testing), everything works just fine:

  grunt.registerTask( 'build', [
    'clean', 'html2js', 'jshint', 'sass:build',
    'concat:build_css', 'copy:build_app_assets', 'copy:build_vendor_assets',
    'copy:build_appjs', 'copy:build_vendorjs', 'copy:build_vendorcss', 'index:build', 'karmaconfig',
    'karma:continuous'
  ]);

So maybe ngAnnotate or or concat/uglify are doing weird things here?


UPDATE 1: It has something to do with my configuration of the modules. Here is the code:

angular
  .module('controlcenter.websites',
    [
      'ui.router'
    ]
  )
  .config(config);

config.$inject = ['$stateProvider'];

function config($stateProvider) {
  $stateProvider.state( 'websites', {
    url: '/websites',
    views: {
      "main": {
        controller: 'WebsitesController',
        templateUrl: 'websites/websites.tpl.html'
      }
    }
  });
}
  1. When I change the name of the state to websites_2, I get an error with 'websites_2 is already defined'.
  2. When I remove the module completely, the next one hast the same problem inside the config file. So is the structure wrong?

Update 2:

The problem seems concat related.

It takes every JS file and adds it one after another to one, bigger file. All of my modules are at the end. The last module always has the problem with 'state already defined'. So it's not just the order of the modules appending to each other, it's something elsse...


Update 3: I placed my code (I've excluded every Controller-Code and functions, just the scaffold) in a gist. This is the outcome after my compile process, without uglifying it.

like image 766
ohboy21 Avatar asked Jun 17 '15 10:06

ohboy21


2 Answers

Issue:

You have multiple files that contains a config function to configure your module, like this:

angular
    .module('controlcenter.websites', [])
    .config(config);

function config() {
    // ...
}

The problem is that after you concatenate all files you end up with a big file with multiple declarations of config. Because of JavaScript's variable hoisting, all declarations are moved to the top and only the very last of them is evaluated, and this one is:

function config($stateProvider) {
  $stateProvider.state( 'websites', {
    url: '/websites',
    views: {
      "main": {
        controller: 'WebsitesController',
        templateUrl: 'websites/overview/websites.tpl.html'
      }
    },
    data : {requiresLogin : true }
  });
}

Hence, each time you .config(config) a module, you are telling Angular to configure your module with that particular configuration function, which means that it executes multiple times and tries to define the state websites more than once.

Solution:

Wrap each JavaScript file code with a closure. This way you will avoid declaring a variable/function more than once:

(function (angular) {

    'use strict';

    angular
      .module('controlcenter.website.details', ['ui.router'])
      .config(config);

    config.$inject = ['$stateProvider'];

    function config($stateProvider) {
      $stateProvider
        .state( 'websiteDetails', {
          url: '/websiteDetails/:id',
          views: {
            "main": {
              controller: 'WebsiteDetailsController',
              templateUrl: 'websites/details/website.details.tpl.html'
            }
          },
          data : {requiresLogin : true }
        })

        .state( 'websiteDetails.categories', {
          url: '/categories',
          views: {
            "detailsContent": {
              templateUrl: 'websites/details/website.details.categories.tpl.html'
            }
          },
          data : {requiresLogin : true }
        })
        ;
    }
})(window.angular);

Edit:

  • I strongly recommend you wrap your files into closures. However, if you still don't want to do that, you can name your functions according to their respective modules. This way your configuration function for controlcenter.website.details would become controlcenterWebsiteDetailsConfig. Another option is to wrap your code during build phase with grunt-wrap.

  • window.angular and closures: This is a technique I like to use on my code when I'm going to uglify it. By wrapping your code into a closure and giving it a parameter called angular with the actual value of window.angular you are actually creating a variable that can be uglified. This code, for instance:

    (function (angular) {
        // You could also declare a variable, instead of a closure parameter:
        // var angular = window.angular;
    
        angular.module('app', ['controllers']);
        angular.module('controllers', []);
        // ...
    })(window.angular);
    

    Could be easily uglified to this (notice that every reference to angular is replaced by a):

    !function(a){a.module("app",["controllers"]),a.module("controllers",[])}(window.angular);
    

    On the other side, an unwrapped code snippet like this:

    angular.module('app', ['controllers']);
    angular.module('controllers', []);
    

    Would become:

    angular.module("app",["controllers"]),angular.module("controllers",[]);
    

    For more on closures, check this post and this post.

like image 121
Danilo Valente Avatar answered Dec 04 '22 06:12

Danilo Valente


If you check it in the concatenated file, do you have the states defined twice? Can it be that you are copying the files twice? Check the temporary folders from where you are taking the files (also in grunt config, what you are copying and what you are deleting...).

like image 41
eesdil Avatar answered Dec 04 '22 05:12

eesdil