source: Dev/trunk/node_modules/grunt-amd-check/tasks/amd-check.js @ 484

Last change on this file since 484 was 484, checked in by hendrikvanantwerpen, 11 years ago

Commit node_modules, to make checkouts and builds more deterministic.

File size: 9.4 KB
Line 
1module.exports = function(grunt) {
2
3        'use strict';
4
5        var path = require('path');
6        var amd = require('grunt-lib-amd');
7        var _ = require('underscore');
8
9
10        var _getPlugin = function(pluginName, rjsconfig) {
11                rjsconfig.nodeRequire = require;
12                amd.requirejs.config(rjsconfig);
13
14                var plugin = amd.requirejs(pluginName);
15
16                if (!plugin || !plugin.load) {
17                        throw '"' + pluginName + '" plugin could not be resolved';
18                }
19
20                return plugin;
21        };
22
23
24        var _pluginCanLoad = function(plugin, loadArgs) {
25                var didLoad = false;
26                var load = function() {
27                        didLoad = true;
28                };
29                load.fromText = function() {
30                        didLoad = true;
31                };
32
33                try {
34                        plugin.load(loadArgs, amd.requirejs, load, {});
35                }
36                catch(e) {
37                        return false;
38                }
39
40                return didLoad;
41        };
42
43
44        grunt.registerMultiTask('amd-check', 'Checks AMD modules for unresolvable dependencies and circular dependencies', function() {
45                var files = this.filesSrc.map(function(file) {
46                        return path.resolve(process.cwd() + '/' + file);
47                });
48                var rjsconfig = amd.loadConfig(grunt.config.get('requirejs'));
49
50                grunt.verbose.writeln('Loaded RequireJS config:');
51                grunt.verbose.writeln(JSON.stringify(rjsconfig, false, 4));
52
53                var found = false;
54
55                grunt.log.writeln('Scanning ' + files.length + ' files for unresolved dependencies...');
56
57                var depCache = {};
58
59                files.forEach(function(file) {
60                        depCache[file] = amd.getDeps(file);
61                });
62
63                files.forEach(function(file) {
64                        var deps = depCache[file]
65                                .filter(function(depPath) {
66                                        //ignore special 'require' dependency
67                                        return depPath !== 'require';
68                                })
69                                .map(function(depPath) {
70                                        if (depPath.search(/!/) !== -1) {
71                                                var rParts = /^(.*)!(.*)$/;
72                                                var parts = depPath.match(rParts);
73                                                var pluginName = parts[1];
74                                                var pluginArgs = parts[2];
75
76                                                var plugin;
77                                                try {
78                                                        plugin = _getPlugin(pluginName, rjsconfig);
79                                                }
80                                                catch(e) {
81                                                        return {
82                                                                declared: depPath,
83                                                                resolved: false,
84                                                                message: 'Error on \u001b[4m\u001b[31m' + pluginName + '\u001b[0m!' + pluginArgs + ': "' + pluginName + '" plugin could not be resolved'
85                                                        };
86                                                }
87
88                                                if (!_pluginCanLoad(plugin, pluginArgs)) {
89                                                        return {
90                                                                declared: depPath,
91                                                                resolved: false,
92                                                                message: 'Error on ' + pluginName + '!\u001b[4m\u001b[31m' + pluginArgs + '\u001b[0m: "' + pluginName + '" plugin could not load with given arguments'
93                                                        };
94                                                }
95
96                                                return {
97                                                        declared: depPath,
98                                                        resolved: true
99                                                };
100                                        }
101
102                                        return {
103                                                declared: depPath,
104                                                resolved: amd.moduleToFileName(depPath, path.dirname(file), rjsconfig)
105                                        };
106                                })
107                                .filter(function(dep) {
108                                        return !dep.resolved;
109                                });
110
111                        if (deps.length) {
112                                found = true;
113                                grunt.log.writeln('');
114                                grunt.log.writeln('\u001b[31mWarning:\u001b[0m Unresolved dependencies in ' + file + ':');
115                                deps.forEach(function(dep) {
116                                        grunt.log.writeln('\t' + dep.declared);
117                                        if (dep.message) {
118                                                grunt.log.writeln('\t\t' + dep.message);
119                                        }
120                                });
121                        }
122                });
123
124                if (!found) {
125                        grunt.log.writeln('All dependencies resolved properly!');
126                }
127
128                grunt.log.write('\n\n');
129                grunt.log.write('Checking for circular dependencies...');
130
131                found = [];
132                _.each(depCache, function(deps, file) {
133                        depCache[file] = deps.map(function(dep) {
134                                return amd.moduleToFileName(dep, path.dirname(file), rjsconfig);
135                        });
136                });
137
138                var checkCircular = function(file, graphPath) {
139                        var i = graphPath.indexOf(file);
140                        if (i !== -1) {
141                                var loop = graphPath.slice(i);
142                                found.push(loop);
143                                return;
144                        }
145                        graphPath.push(file);
146
147                        if (depCache[file] === undefined) {
148                                depCache[file] = amd.getDeps(file).map(function(dep) {
149                                        return amd.moduleToFileName(dep, path.dirname(file), rjsconfig);
150                                });
151                        }
152
153                        depCache[file].forEach(function(dep) {
154                                checkCircular(dep, graphPath.slice());
155                        });
156                };
157
158                files.forEach(function(file) {
159                        checkCircular(file, []);
160                });
161
162                //eliminate duplicate circular dependency loops
163                var _rotated = function(arr, i) {
164                        var ret = arr.slice(0);
165                        for (var j = 0; j < i; j++) {
166                                ret = [ret[ret.length - 1]].concat(ret.slice(0, ret.length - 1));
167                        }
168                        return ret;
169                };
170
171                var _equal = function(first, second) {
172                        return JSON.stringify(first) === JSON.stringify(second);
173                };
174
175                //loops are "equal" if their elements are equal when "shifted" to the right by some amount
176                var _loopEqual = function(first, second) {
177                        if (_equal(first, second)) {
178                                return true;
179                        }
180                        for (var i = 1; i <= first.length; i++) {
181                                if (_equal(first, _rotated(second, i))) {
182                                        return true;
183                                }
184                        }
185                        return false;
186                };
187
188                var dupes = [];
189                found = found
190                        .filter(function(loop, i) {
191                                if (i === 0) {
192                                        return true;
193                                }
194
195                                var isDuplicate = false;
196                                var before = found.slice(0, i - 1);
197                                before.every(function(candidateLoop, j) {
198                                        if (dupes.indexOf(j) !== -1) {
199                                                return true; //continue
200                                        }
201                                        if (_loopEqual(loop, candidateLoop)) {
202                                                isDuplicate = true;
203                                                return false;
204                                        }
205                                        return true;
206                                });
207                                if (isDuplicate) {
208                                        dupes.push(i);
209                                }
210                                return !isDuplicate;
211                        })
212                        .map(function(loop) {
213                                return loop.map(function(file) {
214                                        return amd.fileToModuleName(file, rjsconfig);
215                                });
216                        });
217
218                if (!found.length) {
219                        grunt.log.writeln(' none found');
220                        return;
221                }
222
223                grunt.log.writeln('\u001b[31m ' + found.length + ' found:\u001b[0m\n');
224
225                /*
226                 *found.forEach(function(loop) {
227                 *    grunt.log.writeln(
228                 *        loop
229                 *            .concat([loop[0]])
230                 *            .join(' -> ')
231                 *    );
232                 *});
233                 */
234
235
236                var _longestCommonSublist = function(first, second) {
237                        var start = 0;
238                        var max = 0;
239
240                        for (var i = 0; i < first.length; i++) {
241                                for (var j = 0; j < second.length; j++) {
242                                        var x = 0;
243                                        while (first[i + x] === second[j + x]) {
244                                                x++;
245                                                if ((i + x >= first.length) || (j + x >= second.length)) {
246                                                        break;
247                                                }
248                                        }
249                                        if (x > max) {
250                                                max = x;
251                                                start = i;
252                                        }
253                                }
254                        }
255
256                        return first.slice(start, start + max);
257                };
258
259                var _longestCommonSublistInLoops = function(first, second) {
260                        var bestCase = Math.min(first.length, second.length);
261                        var max = [];
262
263                        for (var i = 0; i < first.length; i++) {
264                                var firstList = _rotated(first, i);
265                                for (var j = 0; j < second.length; j++) {
266                                        var secondList = _rotated(second, j);
267                                        var commonSublist = _longestCommonSublist(firstList, secondList);
268                                        if (commonSublist.length === bestCase) {
269                                                return commonSublist;
270                                        }
271                                        if (commonSublist.length > max.length) {
272                                                max = commonSublist;
273                                        }
274                                }
275                        }
276
277                        return max;
278                };
279
280                var _grouped = function(loops) {
281                        var worstLength = 2;
282                        var groups = {};
283
284                        for (var i = 0; i < loops.length; i++) {
285                                var first = loops[i];
286                                for (var j = 0; j < loops.length; j++) {
287                                        if (i === j) {
288                                                continue;
289                                        }
290
291                                        var second = loops[j];
292                                        var commonSublist = _longestCommonSublistInLoops(first, second);
293                                        if (commonSublist.length >= worstLength) {
294                                                var key = commonSublist.join(',');
295                                                groups[key] = groups[key] || [];
296                                                groups[key].push(first);
297                                                groups[key].push(second);
298                                        }
299                                }
300                        }
301
302                        _.each(groups, function(loops, key) {
303                                groups[key] = _.uniq(loops);
304                        });
305
306                        return groups;
307                };
308
309                var grouped = _grouped(found);
310                var ungrouped = _.difference(
311                        found,
312                        _.reduce(grouped, function(memo, loops) {
313                                return memo.concat(loops);
314                        }, [])
315                );
316
317                _.each(grouped, function(loops, key) {
318                        grunt.log.writeln('Grouped by longest common subpath:');
319                        var group = key.split(',');
320                        grunt.log.writeln('{ ' + group.join(' -> ') + ' }');
321
322                        loops
323                                .sort(function(a, b) {
324                                        return a.length - b.length;
325                                })
326                                .map(function(loop) {
327                                        for (var i = 0; i < loop.length; i++) {
328                                                var rotated = _rotated(loop, i);
329                                                if (rotated[0] === group[0]) {
330                                                        return rotated;
331                                                }
332                                        }
333                                })
334                                .forEach(function(loop) {
335                                        grunt.log.write('\t' + loop.join(' -> '));
336                                        grunt.log.write(' ( -> ' + loop[0] + ' -> ...)\n');
337                                });
338                });
339
340                ungrouped.forEach(function(loop) {
341                        grunt.log.write(loop.join(' -> '));
342                        grunt.log.write(' ( -> ' + loop[0] + ' -> ...)\n');
343                });
344
345
346        });
347
348
349        grunt.registerTask('whatrequires', 'Traces which files depend on given js file', function(searchFile) {
350                searchFile = path.resolve(searchFile);
351                var rjsconfig = amd.loadConfig(grunt.config.get('requirejs'));
352
353                if (!grunt.file.exists(searchFile)) {
354                        grunt.log.write(searchFile + ' does not exist! ').error();
355                        return;
356                }
357
358                var config = grunt.config.get('amd-check');
359                var pool = grunt.task.normalizeMultiTaskFiles(config.files)[0].src;
360
361                grunt.verbose.writeln('Loaded RequireJS config:');
362                grunt.verbose.writeln(JSON.stringify(rjsconfig, false, 4));
363
364                var matches = pool.filter(function(file) {
365                        file = path.resolve(file);
366
367                        var deps = _.map(amd.getDeps(file), function(depPath) {
368                                return amd.moduleToFileName(depPath, path.dirname(file), rjsconfig);
369                        });
370
371                        return deps.some(function(dep) {
372                                //take case-insensitive filesystems like HFS+ into account
373                                return dep && dep.toLowerCase() === searchFile.toLowerCase();
374                        });
375                });
376
377                switch (matches.length) {
378                case 0:
379                        grunt.log.write('No files depend');
380                        break;
381                case 1:
382                        grunt.log.write('1 file depends');
383                        break;
384                default:
385                        grunt.log.write(matches.length + ' files depend');
386                        break;
387                }
388
389                grunt.log.write(' on ' + searchFile);
390
391                if (!matches.length) {
392                        grunt.log.write('.\n');
393                }
394                else {
395                        grunt.log.write(':\n');
396                        grunt.log.writeln('-----------------------------------------------------------');
397                        matches.forEach(function(file) {
398                                grunt.log.writeln(file);
399                        });
400                }
401
402        });
403
404};
Note: See TracBrowser for help on using the repository browser.