You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

285 lines
8.8 KiB
JavaScript

var SourceMapConsumer = require('source-map').SourceMapConsumer;
var fs = require('fs');
var path = require('path');
var http = require('http');
var https = require('https');
var url = require('url');
var override = require('../utils/object.js').override;
var MAP_MARKER = /\/\*# sourceMappingURL=(\S+) \*\//;
var REMOTE_RESOURCE = /^(https?:)?\/\//;
var DATA_URI = /^data:(\S*?)?(;charset=[^;]+)?(;[^,]+?)?,(.+)/;
var unescape = global.unescape;
function InputSourceMapStore(outerContext) {
this.options = outerContext.options;
this.errors = outerContext.errors;
this.warnings = outerContext.warnings;
this.sourceTracker = outerContext.sourceTracker;
this.timeout = this.options.inliner.timeout;
this.requestOptions = this.options.inliner.request;
this.localOnly = outerContext.localOnly;
this.relativeTo = outerContext.options.target || process.cwd();
this.maps = {};
this.sourcesContent = {};
}
function fromString(self, _, whenDone) {
self.trackLoaded(undefined, undefined, self.options.sourceMap);
return whenDone();
}
function fromSource(self, data, whenDone, context) {
var nextAt = 0;
function proceedToNext() {
context.cursor += nextAt + 1;
fromSource(self, data, whenDone, context);
}
while (context.cursor < data.length) {
var fragment = data.substring(context.cursor);
var markerStartMatch = self.sourceTracker.nextStart(fragment) || { index: -1 };
var markerEndMatch = self.sourceTracker.nextEnd(fragment) || { index: -1 };
var mapMatch = MAP_MARKER.exec(fragment) || { index: -1 };
var sourceMapFile = mapMatch[1];
nextAt = data.length;
if (markerStartMatch.index > -1)
nextAt = markerStartMatch.index;
if (markerEndMatch.index > -1 && markerEndMatch.index < nextAt)
nextAt = markerEndMatch.index;
if (mapMatch.index > -1 && mapMatch.index < nextAt)
nextAt = mapMatch.index;
if (nextAt == data.length)
break;
if (nextAt == markerStartMatch.index) {
context.files.push(markerStartMatch.filename);
} else if (nextAt == markerEndMatch.index) {
context.files.pop();
} else if (nextAt == mapMatch.index) {
var isRemote = /^https?:\/\//.test(sourceMapFile) || /^\/\//.test(sourceMapFile);
var isDataUri = DATA_URI.test(sourceMapFile);
if (isRemote) {
return fetchMapFile(self, sourceMapFile, context, proceedToNext);
} else {
var sourceFile = context.files[context.files.length - 1];
var sourceMapPath, sourceMapData;
var sourceDir = sourceFile ? path.dirname(sourceFile) : self.options.relativeTo;
if (isDataUri) {
// source map's path is the same as the source file it comes from
sourceMapPath = path.resolve(self.options.root, sourceFile || '');
sourceMapData = fromDataUri(sourceMapFile);
} else {
sourceMapPath = path.resolve(self.options.root, path.join(sourceDir || '', sourceMapFile));
sourceMapData = fs.readFileSync(sourceMapPath, 'utf-8');
}
self.trackLoaded(sourceFile || undefined, sourceMapPath, sourceMapData);
}
}
context.cursor += nextAt + 1;
}
return whenDone();
}
function fromDataUri(uriString) {
var match = DATA_URI.exec(uriString);
var charset = match[2] ? match[2].split(/[=;]/)[2] : 'us-ascii';
var encoding = match[3] ? match[3].split(';')[1] : 'utf8';
var data = encoding == 'utf8' ? unescape(match[4]) : match[4];
var buffer = new Buffer(data, encoding);
buffer.charset = charset;
return buffer.toString();
}
function fetchMapFile(self, sourceUrl, context, done) {
fetch(self, sourceUrl, function (data) {
self.trackLoaded(context.files[context.files.length - 1] || undefined, sourceUrl, data);
done();
}, function (message) {
context.errors.push('Broken source map at "' + sourceUrl + '" - ' + message);
return done();
});
}
function fetch(self, path, onSuccess, onFailure) {
var protocol = path.indexOf('https') === 0 ? https : http;
var requestOptions = override(url.parse(path), self.requestOptions);
var errorHandled = false;
protocol
.get(requestOptions, function (res) {
if (res.statusCode < 200 || res.statusCode > 299)
return onFailure(res.statusCode);
var chunks = [];
res.on('data', function (chunk) {
chunks.push(chunk.toString());
});
res.on('end', function () {
onSuccess(chunks.join(''));
});
})
.on('error', function (res) {
if (errorHandled)
return;
onFailure(res.message);
errorHandled = true;
})
.on('timeout', function () {
if (errorHandled)
return;
onFailure('timeout');
errorHandled = true;
})
.setTimeout(self.timeout);
}
function originalPositionIn(trackedSource, line, column, token, allowNFallbacks) {
var originalPosition;
var maxRange = token.length;
var position = {
line: line,
column: column + maxRange
};
while (maxRange-- > 0) {
position.column--;
originalPosition = trackedSource.data.originalPositionFor(position);
if (originalPosition)
break;
}
if (originalPosition.line === null && line > 1 && allowNFallbacks > 0)
return originalPositionIn(trackedSource, line - 1, column, token, allowNFallbacks - 1);
if (trackedSource.path && originalPosition.source) {
originalPosition.source = REMOTE_RESOURCE.test(trackedSource.path) ?
url.resolve(trackedSource.path, originalPosition.source) :
path.join(trackedSource.path, originalPosition.source);
originalPosition.sourceResolved = true;
}
return originalPosition;
}
function trackContentSources(self, sourceFile) {
var consumer = self.maps[sourceFile].data;
var isRemote = REMOTE_RESOURCE.test(sourceFile);
var sourcesMapping = {};
consumer.sources.forEach(function (file, index) {
var uniquePath = isRemote ?
url.resolve(path.dirname(sourceFile), file) :
path.relative(self.relativeTo, path.resolve(path.dirname(sourceFile), file));
sourcesMapping[uniquePath] = consumer.sourcesContent && consumer.sourcesContent[index];
});
self.sourcesContent[sourceFile] = sourcesMapping;
}
function _resolveSources(self, remaining, whenDone) {
function processNext() {
return _resolveSources(self, remaining, whenDone);
}
if (remaining.length === 0)
return whenDone();
var current = remaining.shift();
var sourceFile = current[0];
var originalFile = current[1];
var isRemote = REMOTE_RESOURCE.test(sourceFile);
if (isRemote && self.localOnly) {
self.warnings.push('No callback given to `#minify` method, cannot fetch a remote file from "' + originalFile + '"');
return processNext();
}
if (isRemote) {
fetch(self, originalFile, function (data) {
self.sourcesContent[sourceFile][originalFile] = data;
processNext();
}, function (message) {
self.warnings.push('Broken original source file at "' + originalFile + '" - ' + message);
processNext();
});
} else {
var fullPath = path.join(self.options.root, originalFile);
if (fs.existsSync(fullPath))
self.sourcesContent[sourceFile][originalFile] = fs.readFileSync(fullPath, 'utf-8');
else
self.warnings.push('Missing original source file at "' + fullPath + '".');
return processNext();
}
}
InputSourceMapStore.prototype.track = function (data, whenDone) {
return typeof this.options.sourceMap == 'string' ?
fromString(this, data, whenDone) :
fromSource(this, data, whenDone, { files: [], cursor: 0, errors: this.errors });
};
InputSourceMapStore.prototype.trackLoaded = function (sourcePath, mapPath, mapData) {
var relativeTo = this.options.explicitTarget ? this.options.target : this.options.root;
var isRemote = REMOTE_RESOURCE.test(sourcePath);
if (mapPath) {
mapPath = isRemote ?
path.dirname(mapPath) :
path.dirname(path.relative(relativeTo, mapPath));
}
this.maps[sourcePath] = {
path: mapPath,
data: new SourceMapConsumer(mapData)
};
trackContentSources(this, sourcePath);
};
InputSourceMapStore.prototype.isTracking = function (source) {
return !!this.maps[source];
};
InputSourceMapStore.prototype.originalPositionFor = function (sourceInfo, token, allowNFallbacks) {
return originalPositionIn(this.maps[sourceInfo.source], sourceInfo.line, sourceInfo.column, token, allowNFallbacks);
};
InputSourceMapStore.prototype.sourcesContentFor = function (contextSource) {
return this.sourcesContent[contextSource];
};
InputSourceMapStore.prototype.resolveSources = function (whenDone) {
var toResolve = [];
for (var sourceFile in this.sourcesContent) {
var contents = this.sourcesContent[sourceFile];
for (var originalFile in contents) {
if (!contents[originalFile])
toResolve.push([sourceFile, originalFile]);
}
}
return _resolveSources(this, toResolve, whenDone);
};
module.exports = InputSourceMapStore;