11

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.

ohboy21
  • 4,259
  • 8
  • 38
  • 66
  • Do all these modules ( controlcenter.websites , controlcenter.users , ...) have states with same name or states are named differently in each module ? – Prayag Verma Jun 21 '15 at 08:50
  • Different names. States are always websites, users etc. Weird thing is that even if I rename websites to websites_2, I get an error with 'websites_2' is already defined. – ohboy21 Jun 21 '15 at 12:28
  • 2
    If you're using `whatever.$inject = ...`, then you don't need to use `ngAnnotate`. The annotation will be transforming `.config(config)` into `.config(['$stateProvider', config])` – Phil Jun 22 '15 at 00:30

3 Answers3

22

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.

Community
  • 1
  • 1
Danilo Valente
  • 11,270
  • 8
  • 53
  • 67
  • It's not that it can't *detect* them, its entire purpose is to take away the need to write `.$inject` or `['$providerName', function($providerName) {...}]`. OP shouldn't be mixing *ngAnnotate* with `.$inject` – Phil Jun 22 '15 at 00:32
  • @Phil I know, but when it comes to be a declaration using array notation(`['dep', function (dep) { }]`) ngAnnotate is smart enough to understand that the dependencies have already been declared, so there's nothing to be done. – Danilo Valente Jun 22 '15 at 01:05
  • Thanks for your help! But even if I remove ngAnnotate from my compile task, I'm getting the same error. – ohboy21 Jun 22 '15 at 07:36
  • @gruberb Can you post your compiled (ngAnnotated & uglified) script? – Danilo Valente Jun 22 '15 at 17:56
  • @gruberb Thanks. Do you have the compiled version too? Otherwise, can you also post your gulpfile? – Danilo Valente Jun 24 '15 at 14:47
  • This is compiled. The step which is missing is the uglify. Which would be stupid to post, wouldn't it? So here it is... it has to do sth with the order of the files. Grunt file is already in my questions. It just copies every file in one single file (see link), and then uglifies it. – ohboy21 Jun 24 '15 at 15:03
  • -.- sometimes the solution hits you right into your face...thanks! But I have one concern: the (window.angular) at the end. What does it do? – ohboy21 Jun 25 '15 at 07:51
  • I fixed it with changing the name of each config-function to a specific/different name. Awesome, thanks a bunch! Can you please provide an edit/include my suggestion to a different name to each function? I think that's much nicer than wrapping each config-block. – ohboy21 Jun 25 '15 at 08:01
  • @gruberb Done! Included in my answer. – Danilo Valente Jun 25 '15 at 12:38
1

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...).

eesdil
  • 1,931
  • 3
  • 16
  • 25
0

So I had the same problem but with the following setup:

  • yeoman angular-fullstack (using typescript)
  • Webstorm

With the angular-fullstack configuration, the closures were already implemented (as Danilo Valente suggests) so I struggled quite a bit until I found out that in Webstorm, I had the typescript compiler enabled which compiled all of my *.ts files to *.js. But since Webstorm is so 'smart', it does not show these compiled files in the working tree. Grunt however concatenated of course all files regardless if it is typescript of JS. That's why - in the end- all of my states were defined twice.

So the obvious fix: Disabled typescript compiler of webstorm and deleted all the generated *.js files and it works.

Brueni92
  • 137
  • 1
  • 9