var inherits = require('util').inherits;
var path = require('path');
var zlib = require('zlib');
var docblock = require('../parse/docblock');
var extract = require('../parse/extract');
var extractJavelinSymbols = require('../parse/extractJavelinSymbols');
var JS = require('../resource/JS');
var MessageList = require('../MessageList');
var PathResolver = require('../PathResolver');
var ResourceLoader = require('./ResourceLoader');
function JSLoader(options) {
ResourceLoader.call(this, options);
if (this.options.networkSize) {
this.extractExtra = this.extractNetworkSize;
} else {
this.extractExtra = function(js, sourceCode, messages, callback) {
process.nextTick(function() {
callback(messages, js);
});
};
}
}
inherits(JSLoader, ResourceLoader);
JSLoader.prototype.path = __filename;
JSLoader.prototype.getResourceTypes = function() {
return [JS];
};
JSLoader.prototype.getExtensions = function() {
return this.options.extensions || ['.js', '.jsx'];
};
JSLoader.prototype.extractNetworkSize =
function(js, sourceCode, messages,callback) {
zlib.gzip(sourceCode, function(err, buffer) {
js.networkSize = buffer.length;
callback(messages, js);
});
};
var spaceRe = /\s+/;
JSLoader.prototype.parseDocblockOptions =
function(js, sourceCode, messages) {
var props = docblock.parse(docblock.extract(sourceCode));
props.forEach(function(pair) {
var name = pair[0];
var value = pair[1];
switch (name) {
case 'provides':
js.id = value.split(spaceRe)[0];
break;
case 'providesModule':
js.isModule = true;
js.id = value.split(spaceRe)[0];
break;
case 'providesLegacy':
js.isRunWhenReady = true;
js.isLegacy = true;
js.isModule = true;
js.id = 'legacy:' + value.split(spaceRe)[0];
break;
case 'css':
value.split(spaceRe).forEach(js.addRequiredCSS, js);
break;
case 'requires':
value.split(spaceRe).forEach(js.addRequiredLegacyComponent, js);
break;
case 'javelin':
if (js.path.indexOf('/js/javelin/docs/') !== -1) {
break;
}
js.isModule = true;
js.isJavelin = true;
js.isRunWhenReady = true;
break;
case 'polyfill':
js.isPolyfill = true;
if (value.match(/\S/)) {
js.polyfillUAs = value.split(spaceRe);
} else {
js.polyfillUAs = ['all'];
}
break;
case 'runWhenReady_DEPRECATED':
js.isRunWhenReady = true;
break;
case 'jsx':
var match = value && value.match(/^([^\.]+)/);
js.isJSXEnabled = true;
js.jsxDOMImplementor = value;
if (match[0]) {
js.addRequiredModule(match[0]);
}
break;
case 'permanent':
js.isPermanent = true;
break;
case 'nopackage':
js.isNopackage = true;
break;
case 'option':
case 'options':
value.split(spaceRe).forEach(function(key) {
js.options[key] = true;
});
break;
case 'suggests':
messages.addClowntownError(js.path, 'docblock',
'@suggests is deprecated. Simply use the Bootloader APIs.');
break;
case 'author':
case 'deprecated':
break;
case 'bolt':
break;
case 'javelin-installs':
break;
case 'param':
case 'params':
case 'task':
case 'return':
case 'returns':
case 'access':
messages.addWarning(js.path, 'docblock',
"File has a header docblock, but the docblock is class or " +
"function documentation, not file documentation. Header blocks " +
"should not have @param, @task, @returns, @access, etc.");
break;
case 'nolint':
case 'generated':
case 'preserve-header':
case 'emails':
break;
case 'layer':
break;
default:
messages.addClowntownError(js.path, 'docblock',
'Unknown directive ' + name);
}
});
};
JSLoader.prototype.loadFromSource =
function(path, configuration, sourceCode, messages, callback) {
var js = new JS(path);
if (configuration) {
js.isModule = true;
}
this.parseDocblockOptions(js, sourceCode, messages);
if (js.isJavelin) {
var data = extractJavelinSymbols(sourceCode);
js.definedJavelinSymbols = data.defines;
js.requiredJavelinSymbols = data.requires;
if (data.id) {
js.id = data.id;
}
if (js.id != 'javelin-magical-init') {
js.addRequiredModule('javelin-magical-init');
}
}
if (js.isModule || js.path.indexOf('__browsertests__') !== -1) {
if (configuration) {
if (!js.id) {
js.id = configuration.resolveID(js.path);
}
}
extract.requireCalls(sourceCode).forEach(js.addRequiredModule, js);
if (this.options.extractSpecialRequires) {
js.requiredLazyModules =
extract.requireLazyCalls(sourceCode);
js.suggests = extract.loadModules(sourceCode);
}
} else {
if (this.options.extractSpecialRequires) {
js.requiredLazyModules =
extract.requireLazyCalls(sourceCode);
js.suggests = extract.loadComponents(sourceCode);
}
}
extract.cxModules(sourceCode).forEach(js.addRequiredCSS, js);
this.extractExtra(js, sourceCode, messages, function(m, js) {
if (js) {
js.finalize();
}
callback(m, js);
});
};
JSLoader.prototype.matchPath = function(filePath) {
return this.getExtensions().some(function (ext) {
return filePath.lastIndexOf(ext) === filePath.length - ext.length;
});
};
function findAbsolutePathForRequired(requiredText, callersPath, resourceMap) {
var callerData = {
id: callersPath,
paths: resourceMap.getAllInferredProjectPaths(),
fileName: callersPath
};
return PathResolver._resolveFileName(requiredText, callerData, resourceMap);
}
JSLoader.prototype.postProcess = function(map, resources, callback) {
var messages = MessageList.create();
var isJavelin = false;
var nonRelativePathCache = {};
resources.forEach(function(r) {
var required = r.requiredModules;
if (r.isJavelin) {
isJavelin = true;
}
for (var i = 0; i < required.length; i++) {
var requiredText = required[i];
var resourceByID = map.getResource('JS', requiredText);
if (resourceByID) {
continue;
}
var beginsWithDot = requiredText.charAt(0) !== '.';
var textInCache = requiredText in nonRelativePathCache;
var commonJSResolvedPath = beginsWithDot && textInCache ?
nonRelativePathCache[requiredText] :
findAbsolutePathForRequired(requiredText, r.path, map);
if (beginsWithDot && !textInCache) {
nonRelativePathCache[requiredText] = commonJSResolvedPath;
}
var resolvedResource =
commonJSResolvedPath &&
map.getResourceByPath(commonJSResolvedPath);
if (resolvedResource && resolvedResource.id) {
if (resolvedResource.id !== required[i]) {
if (r.recordRequiredModuleOrigin) {
r.recordRequiredModuleOrigin(required[i], resolvedResource.id);
required[i] = resolvedResource.id;
}
}
}
}
});
resources.forEach(function(r) {
var resource, i, required;
required = r.requiredCSS;
if (required) {
for (i = 0; i < required.length; i++) {
resource = map.getResource('CSS', 'css:' + required[i]);
if (resource && resource.isModule) {
required[i] = 'css:' + required[i];
}
}
}
if (r.isModule) {
return;
}
required = r.requiredLegacyComponents;
if (required) {
for (i = 0; i < required.length; i++) {
resource = map.getResource('JS', 'legacy:' + required[i]);
if (resource && resource.isLegacy) {
required[i] = 'legacy:' + required[i];
}
}
}
required = r.suggests;
if (required) {
for (i = 0; i < required.length; i++) {
resource = map.getResource('JS', 'legacy:' + required[i]);
if (resource && resource.isLegacy) {
required[i] = 'legacy:' + required[i];
}
}
}
});
if (isJavelin) {
var providesMap = {};
map.getAllResourcesByType('JS').forEach(function(r) {
if (r.isJavelin) {
r.definedJavelinSymbols.forEach(function(s) {
if (providesMap[s]) {
messages.addClowntownError(r.path, 'javelin',
'Javlin symbol ' + s + ' is already defined in ' +
providesMap[s].path);
return;
}
providesMap[s] = r;
});
}
});
map.getAllResourcesByType('JS').forEach(function(r) {
if (r.isJavelin) {
r.requiredJavelinSymbols.forEach(function(s) {
var resolved = providesMap[s];
if (!resolved) {
messages.addClowntownError(r.path, 'javelin',
'Javlin symbol ' + s + ' is required but never defined');
return;
}
if (r.requiredModules.indexOf(resolved.id) === -1) {
r.requiredModules.push(resolved.id);
}
if (r.requiredLegacyComponents.indexOf(resolved.id) !== -1) {
r.requiredLegacyComponents = r.requiredLegacyComponents
.filter(function(id) { return id !== resolved.id; });
}
});
}
});
}
process.nextTick(function() {
callback(messages);
});
};
module.exports = JSLoader;