1

I'm trying to build several jsons using Modernizr at once, but it appears to break the scope of my function. It's very hard to explain so have a look at this example, give it a go if you don't believe me:

[1,2,3,4,5].forEach(function(i){
    require("modernizr").build({}, function (result) {
        console.log(i);
    });
})

outputs:

5
5
5
5
5

Instead of the expected 1, 2, 3, 4, 5, as would any similar function.

I have not come across this behaviour before in all my years of coding in ECMAScript like languages, and have built my project (and previous projects) around the idea that you cannot break a function's scope like that.

It breaks any system based on promises or even just simple callbacks. It's baffled me all day, and I can't find an appropriate fix for it. I'm have a very hard time even conceptualizing what it is that's causing this to happen. Please help.

EDIT:

OK, it appears you're all hung up on the forEach... Here's another example that will make it a little clearer:

function asd(i){
    require("modernizr").build({}, function (result) {
        console.log(i);
    });
}

asd(1);
asd(2);
asd(3);
asd(4);

outputs

4
4
4
4

What on earth is happening?

hedgehog90
  • 1,402
  • 19
  • 37
  • 2
    A guess: you're making an async call 5 times, the loop runs before any of the async calls are completed, `i` is now 5 and is called 5 times as the callbacks complete. – Jesse Kernaghan Jan 13 '16 at 17:56
  • 1
    That's why I'm using forEach. It encloses 'i' in it's own scope so it remains the same by the time the async callback fires. The issue is not that. – hedgehog90 Jan 13 '16 at 18:14
  • edited question with better example – hedgehog90 Jan 13 '16 at 18:34
  • I'm able to reproduce this with a `for` loop as well. – Evan Davis Jan 13 '16 at 18:34
  • 3
    Oh hang on, I've realised whats happening. It appears modernizr is simply not built the way I assumed. It's a single instance and it can't handle 2 or more builds at the same time! – hedgehog90 Jan 13 '16 at 18:43

2 Answers2

0

The function block is called asynchronously, so this behavior is expected because this call is much slower than the walk of your foreach, so when you reach the function (result) {} block iis already five

Quite the same problem as described in Node.JS: How to pass variables to asynchronous callbacks? here and you should be able to use the same solution

[1,2,3,4,5].forEach(function(i){
    (function(i) {
        require("modernizr").build({}, function (result) {
            console.log(i);
        });
    })(i);
})

untested but somethign like that should work

Community
  • 1
  • 1
Soulan
  • 341
  • 1
  • 6
  • 2
    As general life advice, please _never_ begin a sentence with "well, actually." People hate that. – Evan Davis Jan 13 '16 at 17:59
  • Kind of fixed if it makes most people feel more comfortable – Soulan Jan 13 '16 at 18:01
  • Puh I could think of some tricks where you attach the value to the generated modernizr object like require("modernizr").build({}, function (result) { console.log(this.i); }).i=i; but that's a little bit hacky – Soulan Jan 13 '16 at 18:17
  • This was my original thought, but it is not actually the reason why Modernizr was broken. It was a bug in Modernizr itself – Patrick Jan 14 '16 at 00:10
0

The issue specific to Modernizr had to to with a global variable being clobbered.

the build command is basically a large requirejs configuration function, all powered by a large config object. There is some basic things that are true, always, that are established at the top of the function

{
  optimize: 'none',
  generateSourceMaps: false,
  optimizeCss: 'none',
  useStrict: true,
  include: ['modernizr-init'],
  fileExclusionRegExp: /^(.git|node_modules|modulizr|media|test)$/,
  wrap: {
    start: '\n;(function(window, document, undefined){',
    end: '})(window, document);'
  }
}

Then, since Modernizr works in both the browser and in node without changes, there needs to be a way for it to know if it should be loading its dependencies via the filesystem or via http. So we add some more options like basePath inside of a environment check

if (inBrowser) {
  baseRequireConfig.baseUrl = '/i/js/modernizr-git/src';
} else {
  baseRequireConfig.baseUrl = __dirname + '/../src';  
}

At this point, the config object gets passed into requirejs.config, which wires up require and allows us to start calling build.

Finally, after all of that has been created, we have a build function that also ends up modifying the config object yet again for build specific settings (the actual detects in your build, regex to strip out some AMD crud, etc).

So here is a super simplified pseudocode version of what is ended up happening

var config = {
  name: 'modernizr'
}

if (inBrowser) {
  config.env = 'browser';
} else {
  config.env = 'node';    
}

requirejs.config(config);

module.exports = function(config, callback) {
  config.out = function (output) {
    //code to strip out AMD ceremony, add classPrefix, version, etc

    callback(output)
  }

  requirejs.optimize(config)
}

spot the problem?

Since we are touching the .out method of the config object (whose scope is the entire module, and therefore its context is saved between build() calls) right before we run the asynchronous require.optimize function, the callback you were passing was rewriting the .out method every time build is called.

This should be fixed in a couple hours in Modernizr

Patrick
  • 13,872
  • 5
  • 35
  • 53