263 lines
8.6 KiB
JavaScript
263 lines
8.6 KiB
JavaScript
/*
|
|
* grunt-contrib-concat
|
|
* http://gruntjs.com/
|
|
*
|
|
* Copyright (c) 2016 "Cowboy" Ben Alman, contributors
|
|
* Licensed under the MIT license.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
exports.init = function(grunt) {
|
|
var exports = {};
|
|
|
|
// Node first party libs
|
|
var path = require('path');
|
|
|
|
// Third party libs
|
|
var chalk = require('chalk');
|
|
var SourceMap = require('source-map');
|
|
var SourceMapConsumer = SourceMap.SourceMapConsumer;
|
|
var SourceMapGenerator = SourceMap.SourceMapGenerator;
|
|
|
|
var NO_OP = function(){};
|
|
|
|
function SourceMapConcatHelper(options) {
|
|
this.files = options.files;
|
|
this.dest = options.dest;
|
|
this.options = options.options;
|
|
this.line = 1;
|
|
this.column = 0;
|
|
|
|
// ensure we're using forward slashes, because these are URLs
|
|
var file = path.relative(path.dirname(this.dest), this.files.dest).replace(/\\/g, '/');
|
|
var generator = new SourceMapGenerator({
|
|
file: file
|
|
});
|
|
this.file = file;
|
|
this.generator = generator;
|
|
this.addMapping = function(genLine, genCol, orgLine, orgCol, source, name) {
|
|
if (!source) {
|
|
generator.addMapping({
|
|
generated: {line: genLine, column: genCol}
|
|
});
|
|
} else {
|
|
if (!name) {
|
|
generator.addMapping({
|
|
generated: {line: genLine, column: genCol},
|
|
original: {line: orgLine, column: orgCol},
|
|
source: source
|
|
});
|
|
} else {
|
|
generator.addMapping({
|
|
generated: {line: genLine, column: genCol},
|
|
original: {line: orgLine, column: orgCol},
|
|
source: source,
|
|
name: name
|
|
});
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// Return an object that is used to track sourcemap data between calls.
|
|
exports.helper = function(files, options) {
|
|
// Figure out the source map destination.
|
|
var dest = files.dest;
|
|
if (options.sourceMapStyle === 'inline') {
|
|
// Leave dest as is. It will be used to compute relative sources.
|
|
} else if (typeof options.sourceMapName === 'string') {
|
|
dest = options.sourceMapName;
|
|
} else if (typeof options.sourceMapName === 'function') {
|
|
dest = options.sourceMapName(dest);
|
|
} else {
|
|
dest += '.map';
|
|
}
|
|
|
|
// Inline style and sourceMapName together doesn't work
|
|
if (options.sourceMapStyle === 'inline' && options.sourceMapName) {
|
|
grunt.log.warn(
|
|
'Source map will be inlined, sourceMapName option ignored.'
|
|
);
|
|
}
|
|
|
|
return new SourceMapConcatHelper({
|
|
files: files,
|
|
dest: dest,
|
|
options: options
|
|
});
|
|
};
|
|
|
|
// Parse only to increment the generated file's column and line count
|
|
SourceMapConcatHelper.prototype.add = function(src) {
|
|
this._forEachTokenPosition(src);
|
|
};
|
|
|
|
/**
|
|
* Parse the source file into tokens and apply the provided callback
|
|
* with the position of the token boundaries in the original file, and
|
|
* in the generated file.
|
|
*
|
|
* @param src The sources to tokenize. Required
|
|
* @param filename The name of the source file. Optional
|
|
* @param callback What to do with the token position indices. Optional
|
|
*/
|
|
SourceMapConcatHelper.prototype._forEachTokenPosition = function(src, filename, callback) {
|
|
var genLine = this.line;
|
|
var genCol = this.column;
|
|
var orgLine = 1;
|
|
var orgCol = 0;
|
|
// Tokenize on words, new lines, and white space.
|
|
var tokens = src.split(/(\n|[^\S\n]+|\b)/g);
|
|
if (!callback) {
|
|
callback = NO_OP;
|
|
}
|
|
for (var i = 0, len = tokens.length; i < len; i++) {
|
|
var token = tokens[i];
|
|
if (token) {
|
|
// The if statement filters out empty strings.
|
|
callback(genLine, genCol, orgLine, orgCol, filename);
|
|
if (token === '\n') {
|
|
++orgLine;
|
|
++genLine;
|
|
orgCol = 0;
|
|
genCol = 0;
|
|
} else {
|
|
orgCol += token.length;
|
|
genCol += token.length;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.line = genLine;
|
|
this.column = genCol;
|
|
};
|
|
|
|
// Add the lines of a given file to the sourcemap. If in the file, store a
|
|
// prior sourcemap and return src with sourceMappingURL removed.
|
|
SourceMapConcatHelper.prototype.addlines = function(src, filename) {
|
|
var sourceMapRegEx = /\n\/[*/][@#]\s+sourceMappingURL=((?:(?!\s+\*\/).)*).*/;
|
|
var relativeFilename = path.relative(path.dirname(this.dest), filename);
|
|
// sourceMap path references are URLs, so ensure forward slashes are used for paths passed to sourcemap library
|
|
relativeFilename = relativeFilename.replace(/\\/g, '/');
|
|
if (sourceMapRegEx.test(src)) {
|
|
var sourceMapFile = RegExp.$1;
|
|
var sourceMapPath;
|
|
|
|
var sourceContent;
|
|
// Browserify, as an example, stores a datauri at sourceMappingURL.
|
|
if (/data:application\/json;(charset:utf-8;)?base64,([^\s]+)/.test(sourceMapFile)) {
|
|
// Set sourceMapPath to the file that the map is inlined.
|
|
sourceMapPath = filename;
|
|
sourceContent = new Buffer(RegExp.$2, 'base64').toString();
|
|
} else {
|
|
// If sourceMapPath is relative, expand relative to the file
|
|
// referring to it.
|
|
sourceMapPath = path.resolve(path.dirname(filename), sourceMapFile);
|
|
sourceContent = grunt.file.read(sourceMapPath);
|
|
}
|
|
var sourceMapDir = path.dirname(sourceMapPath);
|
|
var sourceMap = JSON.parse(sourceContent);
|
|
var sourceMapConsumer = new SourceMapConsumer(sourceMap);
|
|
// Consider the relative path from source files to new sourcemap.
|
|
var sourcePathToSourceMapPath = path.relative(path.dirname(this.dest), sourceMapDir);
|
|
// Transfer the existing mappings into this mapping
|
|
var initLine = this.line;
|
|
var initCol = this.column;
|
|
sourceMapConsumer.eachMapping(function(args){
|
|
var source;
|
|
if (args.source) {
|
|
source = path.join(sourcePathToSourceMapPath, args.source).replace(/\\/g, '/');
|
|
} else {
|
|
source = null;
|
|
}
|
|
this.line = initLine + args.generatedLine - 1;
|
|
if (this.line === initLine) {
|
|
this.column = initCol + args.generatedColumn;
|
|
} else {
|
|
this.column = args.generatedColumn;
|
|
}
|
|
this.addMapping(
|
|
this.line,
|
|
this.column,
|
|
args.originalLine,
|
|
args.originalColumn,
|
|
source,
|
|
args.name
|
|
);
|
|
}, this);
|
|
if (sourceMap.sources && sourceMap.sources.length && sourceMap.sourcesContent) {
|
|
for (var i = 0; i < sourceMap.sources.length; ++i) {
|
|
this.generator.setSourceContent(
|
|
path.join(sourcePathToSourceMapPath, sourceMap.sources[i]).replace(/\\/g, '/'),
|
|
sourceMap.sourcesContent[i]
|
|
);
|
|
}
|
|
}
|
|
// Remove the old sourceMappingURL.
|
|
src = src.replace(sourceMapRegEx, '');
|
|
} else {
|
|
// Otherwise perform a rudimentary tokenization of the source.
|
|
this._forEachTokenPosition(src, relativeFilename, this.addMapping);
|
|
}
|
|
|
|
if (this.options.sourceMapStyle !== 'link') {
|
|
this.generator.setSourceContent(relativeFilename, src);
|
|
}
|
|
|
|
return src;
|
|
};
|
|
|
|
// Return the comment sourceMappingURL that must be appended to the
|
|
// concatenated file.
|
|
SourceMapConcatHelper.prototype.url = function() {
|
|
// Create the map filepath. Either datauri or destination path.
|
|
var mapfilepath;
|
|
if (this.options.sourceMapStyle === 'inline') {
|
|
var inlineMap = new Buffer(this._write()).toString('base64');
|
|
mapfilepath = 'data:application/json;base64,' + inlineMap;
|
|
} else {
|
|
// Compute relative path to source map destination.
|
|
mapfilepath = path.relative(path.dirname(this.files.dest), this.dest);
|
|
}
|
|
// Create the sourceMappingURL.
|
|
var url;
|
|
if (/\.css$/.test(this.files.dest)) {
|
|
url = '\n/*# sourceMappingURL=' + mapfilepath + ' */';
|
|
} else {
|
|
url = '\n//# sourceMappingURL=' + mapfilepath;
|
|
}
|
|
|
|
return url;
|
|
};
|
|
|
|
// Return a string for inline use or write the source map to disk.
|
|
SourceMapConcatHelper.prototype._write = function() {
|
|
// New sourcemap.
|
|
var newSourceMap = this.generator.toJSON();
|
|
// Return a string for inline use or write the map.
|
|
if (this.options.sourceMapStyle === 'inline') {
|
|
grunt.verbose.writeln(
|
|
'Source map for ' + chalk.cyan(this.files.dest) + ' inlined.'
|
|
);
|
|
return JSON.stringify(newSourceMap, null, '');
|
|
}
|
|
grunt.file.write(
|
|
this.dest,
|
|
JSON.stringify(newSourceMap, null, '')
|
|
);
|
|
grunt.verbose.writeln('Source map ' + chalk.cyan(this.dest) + ' created.');
|
|
|
|
};
|
|
|
|
// Non-private function to write the sourcemap. Shortcuts if writing a inline
|
|
// style map.
|
|
SourceMapConcatHelper.prototype.write = function() {
|
|
if (this.options.sourceMapStyle !== 'inline') {
|
|
this._write();
|
|
}
|
|
};
|
|
|
|
return exports;
|
|
};
|