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
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;
|