diff --git a/toolkit/devtools/sourcemap/SourceMap.jsm b/toolkit/devtools/sourcemap/SourceMap.jsm index 3b829efa11cb..62dce36e2c6f 100644 --- a/toolkit/devtools/sourcemap/SourceMap.jsm +++ b/toolkit/devtools/sourcemap/SourceMap.jsm @@ -1,4 +1,4 @@ -/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* -*- Mode: js; js-indent-level: 2; -*- */ /* * Copyright 2011 Mozilla Foundation and contributors * Licensed under the New BSD license. See LICENSE or: @@ -46,7 +46,7 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou * - sourceRoot: Optional. The URL root from which all sources are relative. * - sourcesContent: Optional. An array of contents of the original source files. * - mappings: A string of base64 VLQs which contain the actual mappings. - * - file: The generated file this source map is associated with. + * - file: Optional. The generated file this source map is associated with. * * Here is an example source map, taken from the source map spec[0]: * @@ -133,7 +133,7 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou Object.defineProperty(SourceMapConsumer.prototype, 'sources', { get: function () { return this._sources.toArray().map(function (s) { - return this.sourceRoot ? util.join(this.sourceRoot, s) : s; + return this.sourceRoot != null ? util.join(this.sourceRoot, s) : s; }, this); } }); @@ -194,6 +194,12 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou } }); + SourceMapConsumer.prototype._nextCharIsMappingSeparator = + function SourceMapConsumer_nextCharIsMappingSeparator(aStr) { + var c = aStr.charAt(0); + return c === ";" || c === ","; + }; + /** * Parse the mappings in a string in to a data structure which we can easily * query (the ordered arrays in the `this.__generatedMappings` and @@ -207,10 +213,9 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou var previousOriginalColumn = 0; var previousSource = 0; var previousName = 0; - var mappingSeparator = /^[,;]/; var str = aStr; + var temp = {}; var mapping; - var temp; while (str.length > 0) { if (str.charAt(0) === ';') { @@ -226,41 +231,41 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou mapping.generatedLine = generatedLine; // Generated column. - temp = base64VLQ.decode(str); + base64VLQ.decode(str, temp); mapping.generatedColumn = previousGeneratedColumn + temp.value; previousGeneratedColumn = mapping.generatedColumn; str = temp.rest; - if (str.length > 0 && !mappingSeparator.test(str.charAt(0))) { + if (str.length > 0 && !this._nextCharIsMappingSeparator(str)) { // Original source. - temp = base64VLQ.decode(str); + base64VLQ.decode(str, temp); mapping.source = this._sources.at(previousSource + temp.value); previousSource += temp.value; str = temp.rest; - if (str.length === 0 || mappingSeparator.test(str.charAt(0))) { + if (str.length === 0 || this._nextCharIsMappingSeparator(str)) { throw new Error('Found a source, but no line and column'); } // Original line. - temp = base64VLQ.decode(str); + base64VLQ.decode(str, temp); mapping.originalLine = previousOriginalLine + temp.value; previousOriginalLine = mapping.originalLine; // Lines are stored 0-based mapping.originalLine += 1; str = temp.rest; - if (str.length === 0 || mappingSeparator.test(str.charAt(0))) { + if (str.length === 0 || this._nextCharIsMappingSeparator(str)) { throw new Error('Found a source and line, but no column'); } // Original column. - temp = base64VLQ.decode(str); + base64VLQ.decode(str, temp); mapping.originalColumn = previousOriginalColumn + temp.value; previousOriginalColumn = mapping.originalColumn; str = temp.rest; - if (str.length > 0 && !mappingSeparator.test(str.charAt(0))) { + if (str.length > 0 && !this._nextCharIsMappingSeparator(str)) { // Original name. - temp = base64VLQ.decode(str); + base64VLQ.decode(str, temp); mapping.name = this._names.at(previousName + temp.value); previousName += temp.value; str = temp.rest; @@ -274,6 +279,7 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou } } + this.__generatedMappings.sort(util.compareByGeneratedPositions); this.__originalMappings.sort(util.compareByOriginalPositions); }; @@ -329,9 +335,9 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou "generatedColumn", util.compareByGeneratedPositions); - if (mapping) { + if (mapping && mapping.generatedLine === needle.generatedLine) { var source = util.getArg(mapping, 'source', null); - if (source && this.sourceRoot) { + if (source != null && this.sourceRoot != null) { source = util.join(this.sourceRoot, source); } return { @@ -361,7 +367,7 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou return null; } - if (this.sourceRoot) { + if (this.sourceRoot != null) { aSource = util.relative(this.sourceRoot, aSource); } @@ -370,7 +376,7 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou } var url; - if (this.sourceRoot + if (this.sourceRoot != null && (url = util.urlParse(this.sourceRoot))) { // XXX: file:// URIs and absolute paths lead to unexpected behavior for // many users. We can help them out when they expect file:// URIs to @@ -413,7 +419,7 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou originalColumn: util.getArg(aArgs, 'column') }; - if (this.sourceRoot) { + if (this.sourceRoot != null) { needle.source = util.relative(this.sourceRoot, needle.source); } @@ -475,7 +481,7 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou var sourceRoot = this.sourceRoot; mappings.map(function (mapping) { var source = mapping.source; - if (source && sourceRoot) { + if (source != null && sourceRoot != null) { source = util.join(sourceRoot, source); } return { @@ -521,8 +527,8 @@ define('source-map/util', ['require', 'exports', 'module' , ], function(require, } exports.getArg = getArg; - var urlRegexp = /([\w+\-.]+):\/\/((\w+:\w+)@)?([\w.]+)?(:(\d+))?(\S+)?/; - var dataUrlRegexp = /^data:.+\,.+/; + var urlRegexp = /^(?:([\w+\-.]+):)?\/\/(?:(\w+:\w+)@)?([\w.]*)(?::(\d+))?(\S*)$/; + var dataUrlRegexp = /^data:.+\,.+$/; function urlParse(aUrl) { var match = aUrl.match(urlRegexp); @@ -531,18 +537,22 @@ define('source-map/util', ['require', 'exports', 'module' , ], function(require, } return { scheme: match[1], - auth: match[3], - host: match[4], - port: match[6], - path: match[7] + auth: match[2], + host: match[3], + port: match[4], + path: match[5] }; } exports.urlParse = urlParse; function urlGenerate(aParsedUrl) { - var url = aParsedUrl.scheme + "://"; + var url = ''; + if (aParsedUrl.scheme) { + url += aParsedUrl.scheme + ':'; + } + url += '//'; if (aParsedUrl.auth) { - url += aParsedUrl.auth + "@" + url += aParsedUrl.auth + '@'; } if (aParsedUrl.host) { url += aParsedUrl.host; @@ -557,22 +567,146 @@ define('source-map/util', ['require', 'exports', 'module' , ], function(require, } exports.urlGenerate = urlGenerate; - function join(aRoot, aPath) { - var url; + /** + * Normalizes a path, or the path portion of a URL: + * + * - Replaces consequtive slashes with one slash. + * - Removes unnecessary '.' parts. + * - Removes unnecessary '/..' parts. + * + * Based on code in the Node.js 'path' core module. + * + * @param aPath The path or url to normalize. + */ + function normalize(aPath) { + var path = aPath; + var url = urlParse(aPath); + if (url) { + if (!url.path) { + return aPath; + } + path = url.path; + } + var isAbsolute = (path.charAt(0) === '/'); - if (aPath.match(urlRegexp) || aPath.match(dataUrlRegexp)) { + var parts = path.split(/\/+/); + for (var part, up = 0, i = parts.length - 1; i >= 0; i--) { + part = parts[i]; + if (part === '.') { + parts.splice(i, 1); + } else if (part === '..') { + up++; + } else if (up > 0) { + if (part === '') { + // The first part is blank if the path is absolute. Trying to go + // above the root is a no-op. Therefore we can remove all '..' parts + // directly after the root. + parts.splice(i + 1, up); + up = 0; + } else { + parts.splice(i, 2); + up--; + } + } + } + path = parts.join('/'); + + if (path === '') { + path = isAbsolute ? '/' : '.'; + } + + if (url) { + url.path = path; + return urlGenerate(url); + } + return path; + } + exports.normalize = normalize; + + /** + * Joins two paths/URLs. + * + * @param aRoot The root path or URL. + * @param aPath The path or URL to be joined with the root. + * + * - If aPath is a URL or a data URI, aPath is returned, unless aPath is a + * scheme-relative URL: Then the scheme of aRoot, if any, is prepended + * first. + * - Otherwise aPath is a path. If aRoot is a URL, then its path portion + * is updated with the result and aRoot is returned. Otherwise the result + * is returned. + * - If aPath is absolute, the result is aPath. + * - Otherwise the two paths are joined with a slash. + * - Joining for example 'http://' and 'www.example.com' is also supported. + */ + function join(aRoot, aPath) { + if (aRoot === "") { + aRoot = "."; + } + if (aPath === "") { + aPath = "."; + } + var aPathUrl = urlParse(aPath); + var aRootUrl = urlParse(aRoot); + if (aRootUrl) { + aRoot = aRootUrl.path || '/'; + } + + // `join(foo, '//www.example.org')` + if (aPathUrl && !aPathUrl.scheme) { + if (aRootUrl) { + aPathUrl.scheme = aRootUrl.scheme; + } + return urlGenerate(aPathUrl); + } + + if (aPathUrl || aPath.match(dataUrlRegexp)) { return aPath; } - if (aPath.charAt(0) === '/' && (url = urlParse(aRoot))) { - url.path = aPath; - return urlGenerate(url); + // `join('http://', 'www.example.com')` + if (aRootUrl && !aRootUrl.host && !aRootUrl.path) { + aRootUrl.host = aPath; + return urlGenerate(aRootUrl); } - return aRoot.replace(/\/$/, '') + '/' + aPath; + var joined = aPath.charAt(0) === '/' + ? aPath + : normalize(aRoot.replace(/\/+$/, '') + '/' + aPath); + + if (aRootUrl) { + aRootUrl.path = joined; + return urlGenerate(aRootUrl); + } + return joined; } exports.join = join; + /** + * Make a path relative to a URL or another path. + * + * @param aRoot The root path or URL. + * @param aPath The path or URL to be made relative to aRoot. + */ + function relative(aRoot, aPath) { + if (aRoot === "") { + aRoot = "."; + } + + aRoot = aRoot.replace(/\/$/, ''); + + // XXX: It is possible to remove this block, and the tests still pass! + var url = urlParse(aRoot); + if (aPath.charAt(0) == "/" && url && url.path == "/") { + return aPath.slice(1); + } + + return aPath.indexOf(aRoot + '/') === 0 + ? aPath.substr(aRoot.length + 1) + : aPath; + } + exports.relative = relative; + /** * Because behavior goes wacky when you set `__proto__` on objects, we * have to prefix all the strings in our set with an arbitrary character. @@ -592,20 +726,6 @@ define('source-map/util', ['require', 'exports', 'module' , ], function(require, } exports.fromSetString = fromSetString; - function relative(aRoot, aPath) { - aRoot = aRoot.replace(/\/$/, ''); - - var url = urlParse(aRoot); - if (aPath.charAt(0) == "/" && url && url.path == "/") { - return aPath.slice(1); - } - - return aPath.indexOf(aRoot + '/') === 0 - ? aPath.substr(aRoot.length + 1) - : aPath; - } - exports.relative = relative; - function strcmp(aStr1, aStr2) { var s1 = aStr1 || ""; var s2 = aStr2 || ""; @@ -980,9 +1100,9 @@ define('source-map/base64-vlq', ['require', 'exports', 'module' , 'source-map/b /** * Decodes the next base 64 VLQ value from the given string and returns the - * value and the rest of the string. + * value and the rest of the string via the out parameter. */ - exports.decode = function base64VLQ_decode(aStr) { + exports.decode = function base64VLQ_decode(aStr, aOutParam) { var i = 0; var strLen = aStr.length; var result = 0; @@ -1000,10 +1120,8 @@ define('source-map/base64-vlq', ['require', 'exports', 'module' , 'source-map/b shift += VLQ_BASE_SHIFT; } while (continuation); - return { - value: fromVLQSigned(result), - rest: aStr.slice(i) - }; + aOutParam.value = fromVLQSigned(result); + aOutParam.rest = aStr.slice(i); }; }); @@ -1060,14 +1178,17 @@ define('source-map/source-map-generator', ['require', 'exports', 'module' , 'so /** * An instance of the SourceMapGenerator represents a source map which is - * being built incrementally. To create a new one, you must pass an object - * with the following properties: + * being built incrementally. You may pass an object with the following + * properties: * * - file: The filename of the generated source. - * - sourceRoot: An optional root for all URLs in this source map. + * - sourceRoot: A root for all relative URLs in this source map. */ function SourceMapGenerator(aArgs) { - this._file = util.getArg(aArgs, 'file'); + if (!aArgs) { + aArgs = {}; + } + this._file = util.getArg(aArgs, 'file', null); this._sourceRoot = util.getArg(aArgs, 'sourceRoot', null); this._sources = new ArraySet(); this._names = new ArraySet(); @@ -1097,9 +1218,9 @@ define('source-map/source-map-generator', ['require', 'exports', 'module' , 'so } }; - if (mapping.source) { + if (mapping.source != null) { newMapping.source = mapping.source; - if (sourceRoot) { + if (sourceRoot != null) { newMapping.source = util.relative(sourceRoot, newMapping.source); } @@ -1108,7 +1229,7 @@ define('source-map/source-map-generator', ['require', 'exports', 'module' , 'so column: mapping.originalColumn }; - if (mapping.name) { + if (mapping.name != null) { newMapping.name = mapping.name; } } @@ -1117,7 +1238,7 @@ define('source-map/source-map-generator', ['require', 'exports', 'module' , 'so }); aSourceMapConsumer.sources.forEach(function (sourceFile) { var content = aSourceMapConsumer.sourceContentFor(sourceFile); - if (content) { + if (content != null) { generator.setSourceContent(sourceFile, content); } }); @@ -1143,11 +1264,11 @@ define('source-map/source-map-generator', ['require', 'exports', 'module' , 'so this._validateMapping(generated, original, source, name); - if (source && !this._sources.has(source)) { + if (source != null && !this._sources.has(source)) { this._sources.add(source); } - if (name && !this._names.has(name)) { + if (name != null && !this._names.has(name)) { this._names.add(name); } @@ -1167,18 +1288,18 @@ define('source-map/source-map-generator', ['require', 'exports', 'module' , 'so SourceMapGenerator.prototype.setSourceContent = function SourceMapGenerator_setSourceContent(aSourceFile, aSourceContent) { var source = aSourceFile; - if (this._sourceRoot) { + if (this._sourceRoot != null) { source = util.relative(this._sourceRoot, source); } - if (aSourceContent !== null) { + if (aSourceContent != null) { // Add the source content to the _sourcesContents map. // Create a new _sourcesContents map if the property is null. if (!this._sourcesContents) { this._sourcesContents = {}; } this._sourcesContents[util.toSetString(source)] = aSourceContent; - } else { + } else if (this._sourcesContents) { // Remove the source file from the _sourcesContents map. // If the _sourcesContents map is empty, set the property to null. delete this._sourcesContents[util.toSetString(source)]; @@ -1197,55 +1318,68 @@ define('source-map/source-map-generator', ['require', 'exports', 'module' , 'so * @param aSourceMapConsumer The source map to be applied. * @param aSourceFile Optional. The filename of the source file. * If omitted, SourceMapConsumer's file property will be used. + * @param aSourceMapPath Optional. The dirname of the path to the source map + * to be applied. If relative, it is relative to the SourceMapConsumer. + * This parameter is needed when the two source maps aren't in the same + * directory, and the source map to be applied contains relative source + * paths. If so, those relative source paths need to be rewritten + * relative to the SourceMapGenerator. */ SourceMapGenerator.prototype.applySourceMap = - function SourceMapGenerator_applySourceMap(aSourceMapConsumer, aSourceFile) { + function SourceMapGenerator_applySourceMap(aSourceMapConsumer, aSourceFile, aSourceMapPath) { + var sourceFile = aSourceFile; // If aSourceFile is omitted, we will use the file property of the SourceMap - if (!aSourceFile) { - aSourceFile = aSourceMapConsumer.file; + if (aSourceFile == null) { + if (aSourceMapConsumer.file == null) { + throw new Error( + 'SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, ' + + 'or the source map\'s "file" property. Both were omitted.' + ); + } + sourceFile = aSourceMapConsumer.file; } var sourceRoot = this._sourceRoot; - // Make "aSourceFile" relative if an absolute Url is passed. - if (sourceRoot) { - aSourceFile = util.relative(sourceRoot, aSourceFile); + // Make "sourceFile" relative if an absolute Url is passed. + if (sourceRoot != null) { + sourceFile = util.relative(sourceRoot, sourceFile); } // Applying the SourceMap can add and remove items from the sources and // the names array. var newSources = new ArraySet(); var newNames = new ArraySet(); - // Find mappings for the "aSourceFile" + // Find mappings for the "sourceFile" this._mappings.forEach(function (mapping) { - if (mapping.source === aSourceFile && mapping.originalLine) { + if (mapping.source === sourceFile && mapping.originalLine != null) { // Check if it can be mapped by the source map, then update the mapping. var original = aSourceMapConsumer.originalPositionFor({ line: mapping.originalLine, column: mapping.originalColumn }); - if (original.source !== null) { + if (original.source != null) { // Copy mapping - if (sourceRoot) { - mapping.source = util.relative(sourceRoot, original.source); - } else { - mapping.source = original.source; + mapping.source = original.source; + if (aSourceMapPath != null) { + mapping.source = util.join(aSourceMapPath, mapping.source) + } + if (sourceRoot != null) { + mapping.source = util.relative(sourceRoot, mapping.source); } mapping.originalLine = original.line; mapping.originalColumn = original.column; - if (original.name !== null && mapping.name !== null) { - // Only use the identifier name if it's an identifier - // in both SourceMaps + if (original.name != null) { mapping.name = original.name; } } } var source = mapping.source; - if (source && !newSources.has(source)) { + if (source != null && !newSources.has(source)) { newSources.add(source); } var name = mapping.name; - if (name && !newNames.has(name)) { + if (name != null && !newNames.has(name)) { newNames.add(name); } @@ -1256,8 +1390,11 @@ define('source-map/source-map-generator', ['require', 'exports', 'module' , 'so // Copy sourcesContents of applied map. aSourceMapConsumer.sources.forEach(function (sourceFile) { var content = aSourceMapConsumer.sourceContentFor(sourceFile); - if (content) { - if (sourceRoot) { + if (content != null) { + if (aSourceMapPath != null) { + sourceFile = util.join(aSourceMapPath, sourceFile); + } + if (sourceRoot != null) { sourceFile = util.relative(sourceRoot, sourceFile); } this.setSourceContent(sourceFile, content); @@ -1348,7 +1485,7 @@ define('source-map/source-map-generator', ['require', 'exports', 'module' , 'so - previousGeneratedColumn); previousGeneratedColumn = mapping.generatedColumn; - if (mapping.source) { + if (mapping.source != null) { result += base64VLQ.encode(this._sources.indexOf(mapping.source) - previousSource); previousSource = this._sources.indexOf(mapping.source); @@ -1362,7 +1499,7 @@ define('source-map/source-map-generator', ['require', 'exports', 'module' , 'so - previousOriginalColumn); previousOriginalColumn = mapping.originalColumn; - if (mapping.name) { + if (mapping.name != null) { result += base64VLQ.encode(this._names.indexOf(mapping.name) - previousName); previousName = this._names.indexOf(mapping.name); @@ -1379,7 +1516,7 @@ define('source-map/source-map-generator', ['require', 'exports', 'module' , 'so if (!this._sourcesContents) { return null; } - if (aSourceRoot) { + if (aSourceRoot != null) { source = util.relative(aSourceRoot, source); } var key = util.toSetString(source); @@ -1397,17 +1534,18 @@ define('source-map/source-map-generator', ['require', 'exports', 'module' , 'so function SourceMapGenerator_toJSON() { var map = { version: this._version, - file: this._file, sources: this._sources.toArray(), names: this._names.toArray(), mappings: this._serializeMappings() }; - if (this._sourceRoot) { + if (this._file != null) { + map.file = this._file; + } + if (this._sourceRoot != null) { map.sourceRoot = this._sourceRoot; } if (this._sourcesContents) { - map.sourcesContent = this._generateSourcesContent(map.sources, - map.sourceRoot || undefined); + map.sourcesContent = this._generateSourcesContent(map.sources, map.sourceRoot); } return map; @@ -1435,6 +1573,13 @@ define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/ var SourceMapGenerator = require('source-map/source-map-generator').SourceMapGenerator; var util = require('source-map/util'); + // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other + // operating systems these days (capturing the result). + var REGEX_NEWLINE = /(\r?\n)/; + + // Matches a Windows-style newline, or any character. + var REGEX_CHARACTER = /\r\n|[\s\S]/g; + /** * SourceNodes provide a way to abstract over interpolating/concatenating * snippets of generated JavaScript source code while maintaining the line and @@ -1450,10 +1595,10 @@ define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/ function SourceNode(aLine, aColumn, aSource, aChunks, aName) { this.children = []; this.sourceContents = {}; - this.line = aLine === undefined ? null : aLine; - this.column = aColumn === undefined ? null : aColumn; - this.source = aSource === undefined ? null : aSource; - this.name = aName === undefined ? null : aName; + this.line = aLine == null ? null : aLine; + this.column = aColumn == null ? null : aColumn; + this.source = aSource == null ? null : aSource; + this.name = aName == null ? null : aName; if (aChunks != null) this.add(aChunks); } @@ -1462,16 +1607,26 @@ define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/ * * @param aGeneratedCode The generated code * @param aSourceMapConsumer The SourceMap for the generated code + * @param aRelativePath Optional. The path that relative sources in the + * SourceMapConsumer should be relative to. */ SourceNode.fromStringWithSourceMap = - function SourceNode_fromStringWithSourceMap(aGeneratedCode, aSourceMapConsumer) { + function SourceNode_fromStringWithSourceMap(aGeneratedCode, aSourceMapConsumer, aRelativePath) { // The SourceNode we want to fill with the generated code // and the SourceMap var node = new SourceNode(); - // The generated code - // Processed fragments are removed from this array. - var remainingLines = aGeneratedCode.split('\n'); + // All even indices of this array are one line of the generated code, + // while all odd indices are the newlines between two adjacent lines + // (since `REGEX_NEWLINE` captures its match). + // Processed fragments are removed from this array, by calling `shiftNextLine`. + var remainingLines = aGeneratedCode.split(REGEX_NEWLINE); + var shiftNextLine = function() { + var lineContents = remainingLines.shift(); + // The last line of a file might not have a newline. + var newLine = remainingLines.shift() || ""; + return lineContents + newLine; + }; // We need to remember the position of "remainingLines" var lastGeneratedLine = 1, lastGeneratedColumn = 0; @@ -1482,41 +1637,16 @@ define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/ var lastMapping = null; aSourceMapConsumer.eachMapping(function (mapping) { - if (lastMapping === null) { - // We add the generated code until the first mapping - // to the SourceNode without any mapping. - // Each line is added as separate string. - while (lastGeneratedLine < mapping.generatedLine) { - node.add(remainingLines.shift() + "\n"); - lastGeneratedLine++; - } - if (lastGeneratedColumn < mapping.generatedColumn) { - var nextLine = remainingLines[0]; - node.add(nextLine.substr(0, mapping.generatedColumn)); - remainingLines[0] = nextLine.substr(mapping.generatedColumn); - lastGeneratedColumn = mapping.generatedColumn; - } - } else { + if (lastMapping !== null) { // We add the code from "lastMapping" to "mapping": // First check if there is a new line in between. if (lastGeneratedLine < mapping.generatedLine) { var code = ""; - // Associate full lines with "lastMapping" - do { - code += remainingLines.shift() + "\n"; - lastGeneratedLine++; - lastGeneratedColumn = 0; - } while (lastGeneratedLine < mapping.generatedLine); - // When we reached the correct line, we add code until we - // reach the correct column too. - if (lastGeneratedColumn < mapping.generatedColumn) { - var nextLine = remainingLines[0]; - code += nextLine.substr(0, mapping.generatedColumn); - remainingLines[0] = nextLine.substr(mapping.generatedColumn); - lastGeneratedColumn = mapping.generatedColumn; - } - // Create the SourceNode. - addMappingWithCode(lastMapping, code); + // Associate first line with "lastMapping" + addMappingWithCode(lastMapping, shiftNextLine()); + lastGeneratedLine++; + lastGeneratedColumn = 0; + // The remaining code is added without mapping } else { // There is no new line in between. // Associate the code between "lastGeneratedColumn" and @@ -1528,19 +1658,43 @@ define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/ lastGeneratedColumn); lastGeneratedColumn = mapping.generatedColumn; addMappingWithCode(lastMapping, code); + // No more remaining code, continue + lastMapping = mapping; + return; } } + // We add the generated code until the first mapping + // to the SourceNode without any mapping. + // Each line is added as separate string. + while (lastGeneratedLine < mapping.generatedLine) { + node.add(shiftNextLine()); + lastGeneratedLine++; + } + if (lastGeneratedColumn < mapping.generatedColumn) { + var nextLine = remainingLines[0]; + node.add(nextLine.substr(0, mapping.generatedColumn)); + remainingLines[0] = nextLine.substr(mapping.generatedColumn); + lastGeneratedColumn = mapping.generatedColumn; + } lastMapping = mapping; }, this); // We have processed all mappings. - // Associate the remaining code in the current line with "lastMapping" - // and add the remaining lines without any mapping - addMappingWithCode(lastMapping, remainingLines.join("\n")); + if (remainingLines.length > 0) { + if (lastMapping) { + // Associate the remaining code in the current line with "lastMapping" + addMappingWithCode(lastMapping, shiftNextLine()); + } + // and add the remaining lines without any mapping + node.add(remainingLines.join("")); + } // Copy sourcesContent into SourceNode aSourceMapConsumer.sources.forEach(function (sourceFile) { var content = aSourceMapConsumer.sourceContentFor(sourceFile); - if (content) { + if (content != null) { + if (aRelativePath != null) { + sourceFile = util.join(aRelativePath, sourceFile); + } node.setSourceContent(sourceFile, content); } }); @@ -1551,9 +1705,12 @@ define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/ if (mapping === null || mapping.source === undefined) { node.add(code); } else { + var source = aRelativePath + ? util.join(aRelativePath, mapping.source) + : mapping.source; node.add(new SourceNode(mapping.originalLine, mapping.originalColumn, - mapping.source, + source, code, mapping.name)); } @@ -1773,12 +1930,30 @@ define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/ lastOriginalSource = null; sourceMappingActive = false; } - chunk.split('').forEach(function (ch) { - if (ch === '\n') { + chunk.match(REGEX_CHARACTER).forEach(function (ch, idx, array) { + if (REGEX_NEWLINE.test(ch)) { generated.line++; generated.column = 0; + // Mappings end at eol + if (idx + 1 === array.length) { + lastOriginalSource = null; + sourceMappingActive = false; + } else if (sourceMappingActive) { + map.addMapping({ + source: original.source, + original: { + line: original.line, + column: original.column + }, + generated: { + line: generated.line, + column: generated.column + }, + name: original.name + }); + } } else { - generated.column++; + generated.column += ch.length; } }); }); diff --git a/toolkit/devtools/sourcemap/tests/unit/Utils.jsm b/toolkit/devtools/sourcemap/tests/unit/Utils.jsm index fa1f0ae89160..5142e5e68a58 100644 --- a/toolkit/devtools/sourcemap/tests/unit/Utils.jsm +++ b/toolkit/devtools/sourcemap/tests/unit/Utils.jsm @@ -1,4 +1,4 @@ -/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* -*- Mode: js; js-indent-level: 2; -*- */ /* * Copyright 2011 Mozilla Foundation and contributors * Licensed under the New BSD license. See LICENSE or: @@ -111,6 +111,21 @@ define('test/source-map/util', ['require', 'exports', 'module' , 'lib/source-ma sourceRoot: '/the/root', mappings: 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA' }; + exports.testMapNoSourceRoot = { + version: 3, + file: 'min.js', + names: ['bar', 'baz', 'n'], + sources: ['one.js', 'two.js'], + mappings: 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA' + }; + exports.testMapEmptySourceRoot = { + version: 3, + file: 'min.js', + names: ['bar', 'baz', 'n'], + sources: ['one.js', 'two.js'], + sourceRoot: '', + mappings: 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA' + }; exports.testMapWithSourcesContent = { version: 3, file: 'min.js', @@ -259,8 +274,8 @@ define('lib/source-map/util', ['require', 'exports', 'module' , ], function(requ } exports.getArg = getArg; - var urlRegexp = /([\w+\-.]+):\/\/((\w+:\w+)@)?([\w.]+)?(:(\d+))?(\S+)?/; - var dataUrlRegexp = /^data:.+\,.+/; + var urlRegexp = /^(?:([\w+\-.]+):)?\/\/(?:(\w+:\w+)@)?([\w.]*)(?::(\d+))?(\S*)$/; + var dataUrlRegexp = /^data:.+\,.+$/; function urlParse(aUrl) { var match = aUrl.match(urlRegexp); @@ -269,18 +284,22 @@ define('lib/source-map/util', ['require', 'exports', 'module' , ], function(requ } return { scheme: match[1], - auth: match[3], - host: match[4], - port: match[6], - path: match[7] + auth: match[2], + host: match[3], + port: match[4], + path: match[5] }; } exports.urlParse = urlParse; function urlGenerate(aParsedUrl) { - var url = aParsedUrl.scheme + "://"; + var url = ''; + if (aParsedUrl.scheme) { + url += aParsedUrl.scheme + ':'; + } + url += '//'; if (aParsedUrl.auth) { - url += aParsedUrl.auth + "@" + url += aParsedUrl.auth + '@'; } if (aParsedUrl.host) { url += aParsedUrl.host; @@ -295,22 +314,146 @@ define('lib/source-map/util', ['require', 'exports', 'module' , ], function(requ } exports.urlGenerate = urlGenerate; - function join(aRoot, aPath) { - var url; + /** + * Normalizes a path, or the path portion of a URL: + * + * - Replaces consequtive slashes with one slash. + * - Removes unnecessary '.' parts. + * - Removes unnecessary '/..' parts. + * + * Based on code in the Node.js 'path' core module. + * + * @param aPath The path or url to normalize. + */ + function normalize(aPath) { + var path = aPath; + var url = urlParse(aPath); + if (url) { + if (!url.path) { + return aPath; + } + path = url.path; + } + var isAbsolute = (path.charAt(0) === '/'); - if (aPath.match(urlRegexp) || aPath.match(dataUrlRegexp)) { + var parts = path.split(/\/+/); + for (var part, up = 0, i = parts.length - 1; i >= 0; i--) { + part = parts[i]; + if (part === '.') { + parts.splice(i, 1); + } else if (part === '..') { + up++; + } else if (up > 0) { + if (part === '') { + // The first part is blank if the path is absolute. Trying to go + // above the root is a no-op. Therefore we can remove all '..' parts + // directly after the root. + parts.splice(i + 1, up); + up = 0; + } else { + parts.splice(i, 2); + up--; + } + } + } + path = parts.join('/'); + + if (path === '') { + path = isAbsolute ? '/' : '.'; + } + + if (url) { + url.path = path; + return urlGenerate(url); + } + return path; + } + exports.normalize = normalize; + + /** + * Joins two paths/URLs. + * + * @param aRoot The root path or URL. + * @param aPath The path or URL to be joined with the root. + * + * - If aPath is a URL or a data URI, aPath is returned, unless aPath is a + * scheme-relative URL: Then the scheme of aRoot, if any, is prepended + * first. + * - Otherwise aPath is a path. If aRoot is a URL, then its path portion + * is updated with the result and aRoot is returned. Otherwise the result + * is returned. + * - If aPath is absolute, the result is aPath. + * - Otherwise the two paths are joined with a slash. + * - Joining for example 'http://' and 'www.example.com' is also supported. + */ + function join(aRoot, aPath) { + if (aRoot === "") { + aRoot = "."; + } + if (aPath === "") { + aPath = "."; + } + var aPathUrl = urlParse(aPath); + var aRootUrl = urlParse(aRoot); + if (aRootUrl) { + aRoot = aRootUrl.path || '/'; + } + + // `join(foo, '//www.example.org')` + if (aPathUrl && !aPathUrl.scheme) { + if (aRootUrl) { + aPathUrl.scheme = aRootUrl.scheme; + } + return urlGenerate(aPathUrl); + } + + if (aPathUrl || aPath.match(dataUrlRegexp)) { return aPath; } - if (aPath.charAt(0) === '/' && (url = urlParse(aRoot))) { - url.path = aPath; - return urlGenerate(url); + // `join('http://', 'www.example.com')` + if (aRootUrl && !aRootUrl.host && !aRootUrl.path) { + aRootUrl.host = aPath; + return urlGenerate(aRootUrl); } - return aRoot.replace(/\/$/, '') + '/' + aPath; + var joined = aPath.charAt(0) === '/' + ? aPath + : normalize(aRoot.replace(/\/+$/, '') + '/' + aPath); + + if (aRootUrl) { + aRootUrl.path = joined; + return urlGenerate(aRootUrl); + } + return joined; } exports.join = join; + /** + * Make a path relative to a URL or another path. + * + * @param aRoot The root path or URL. + * @param aPath The path or URL to be made relative to aRoot. + */ + function relative(aRoot, aPath) { + if (aRoot === "") { + aRoot = "."; + } + + aRoot = aRoot.replace(/\/$/, ''); + + // XXX: It is possible to remove this block, and the tests still pass! + var url = urlParse(aRoot); + if (aPath.charAt(0) == "/" && url && url.path == "/") { + return aPath.slice(1); + } + + return aPath.indexOf(aRoot + '/') === 0 + ? aPath.substr(aRoot.length + 1) + : aPath; + } + exports.relative = relative; + /** * Because behavior goes wacky when you set `__proto__` on objects, we * have to prefix all the strings in our set with an arbitrary character. @@ -330,20 +473,6 @@ define('lib/source-map/util', ['require', 'exports', 'module' , ], function(requ } exports.fromSetString = fromSetString; - function relative(aRoot, aPath) { - aRoot = aRoot.replace(/\/$/, ''); - - var url = urlParse(aRoot); - if (aPath.charAt(0) == "/" && url && url.path == "/") { - return aPath.slice(1); - } - - return aPath.indexOf(aRoot + '/') === 0 - ? aPath.substr(aRoot.length + 1) - : aPath; - } - exports.relative = relative; - function strcmp(aStr1, aStr2) { var s1 = aStr1 || ""; var s2 = aStr2 || ""; diff --git a/toolkit/devtools/sourcemap/tests/unit/test_base64_vlq.js b/toolkit/devtools/sourcemap/tests/unit/test_base64_vlq.js index 4ca3d2d75477..4b8c52efafe6 100644 --- a/toolkit/devtools/sourcemap/tests/unit/test_base64_vlq.js +++ b/toolkit/devtools/sourcemap/tests/unit/test_base64_vlq.js @@ -17,10 +17,9 @@ define("test/source-map/test-base64-vlq", ["require", "exports", "module"], func var base64VLQ = require('source-map/base64-vlq'); exports['test normal encoding and decoding'] = function (assert, util) { - var result; + var result = {}; for (var i = -255; i < 256; i++) { - result = base64VLQ.decode(base64VLQ.encode(i)); - assert.ok(result); + base64VLQ.decode(base64VLQ.encode(i), result); assert.equal(result.value, i); assert.equal(result.rest, ""); } diff --git a/toolkit/devtools/sourcemap/tests/unit/test_dog_fooding.js b/toolkit/devtools/sourcemap/tests/unit/test_dog_fooding.js index 8fb24f4c587b..77017063b402 100644 --- a/toolkit/devtools/sourcemap/tests/unit/test_dog_fooding.js +++ b/toolkit/devtools/sourcemap/tests/unit/test_dog_fooding.js @@ -47,6 +47,12 @@ define("test/source-map/test-dog-fooding", ["require", "exports", "module"], fun generated: { line: 5, column: 2 } }); + smg.addMapping({ + source: 'gza.coffee', + original: { line: 5, column: 10 }, + generated: { line: 6, column: 12 } + }); + var smc = new SourceMapConsumer(smg.toString()); // Exact @@ -54,24 +60,30 @@ define("test/source-map/test-dog-fooding", ["require", "exports", "module"], fun util.assertMapping(3, 2, '/wu/tang/gza.coffee', 2, 0, null, smc, assert); util.assertMapping(4, 2, '/wu/tang/gza.coffee', 3, 0, null, smc, assert); util.assertMapping(5, 2, '/wu/tang/gza.coffee', 4, 0, null, smc, assert); + util.assertMapping(6, 12, '/wu/tang/gza.coffee', 5, 10, null, smc, assert); // Fuzzy - // Original to generated + // Generated to original util.assertMapping(2, 0, null, null, null, null, smc, assert, true); util.assertMapping(2, 9, '/wu/tang/gza.coffee', 1, 0, null, smc, assert, true); - util.assertMapping(3, 0, '/wu/tang/gza.coffee', 1, 0, null, smc, assert, true); + util.assertMapping(3, 0, null, null, null, null, smc, assert, true); util.assertMapping(3, 9, '/wu/tang/gza.coffee', 2, 0, null, smc, assert, true); - util.assertMapping(4, 0, '/wu/tang/gza.coffee', 2, 0, null, smc, assert, true); + util.assertMapping(4, 0, null, null, null, null, smc, assert, true); util.assertMapping(4, 9, '/wu/tang/gza.coffee', 3, 0, null, smc, assert, true); - util.assertMapping(5, 0, '/wu/tang/gza.coffee', 3, 0, null, smc, assert, true); + util.assertMapping(5, 0, null, null, null, null, smc, assert, true); util.assertMapping(5, 9, '/wu/tang/gza.coffee', 4, 0, null, smc, assert, true); + util.assertMapping(6, 0, null, null, null, null, smc, assert, true); + util.assertMapping(6, 9, null, null, null, null, smc, assert, true); + util.assertMapping(6, 13, '/wu/tang/gza.coffee', 5, 10, null, smc, assert, true); - // Generated to original + // Original to generated util.assertMapping(2, 2, '/wu/tang/gza.coffee', 1, 1, null, smc, assert, null, true); util.assertMapping(3, 2, '/wu/tang/gza.coffee', 2, 3, null, smc, assert, null, true); util.assertMapping(4, 2, '/wu/tang/gza.coffee', 3, 6, null, smc, assert, null, true); util.assertMapping(5, 2, '/wu/tang/gza.coffee', 4, 9, null, smc, assert, null, true); + util.assertMapping(5, 2, '/wu/tang/gza.coffee', 5, 9, null, smc, assert, null, true); + util.assertMapping(6, 12, '/wu/tang/gza.coffee', 6, 19, null, smc, assert, null, true); }; }); diff --git a/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js b/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js index d2533ea6663c..42fb2e102601 100644 --- a/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js +++ b/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js @@ -17,7 +17,7 @@ define("test/source-map/test-source-map-consumer", ["require", "exports", "modul var SourceMapConsumer = require('source-map/source-map-consumer').SourceMapConsumer; var SourceMapGenerator = require('source-map/source-map-generator').SourceMapGenerator; - exports['test that we can instantiate with a string or an objects'] = function (assert, util) { + exports['test that we can instantiate with a string or an object'] = function (assert, util) { assert.doesNotThrow(function () { var map = new SourceMapConsumer(util.testMap); }); @@ -27,18 +27,34 @@ define("test/source-map/test-source-map-consumer", ["require", "exports", "modul }; exports['test that the `sources` field has the original sources'] = function (assert, util) { - var map = new SourceMapConsumer(util.testMap); - var sources = map.sources; + var map; + var sources; + map = new SourceMapConsumer(util.testMap); + sources = map.sources; assert.equal(sources[0], '/the/root/one.js'); assert.equal(sources[1], '/the/root/two.js'); assert.equal(sources.length, 2); + + map = new SourceMapConsumer(util.testMapNoSourceRoot); + sources = map.sources; + assert.equal(sources[0], 'one.js'); + assert.equal(sources[1], 'two.js'); + assert.equal(sources.length, 2); + + map = new SourceMapConsumer(util.testMapEmptySourceRoot); + sources = map.sources; + assert.equal(sources[0], 'one.js'); + assert.equal(sources[1], 'two.js'); + assert.equal(sources.length, 2); }; exports['test that the source root is reflected in a mapping\'s source field'] = function (assert, util) { - var map = new SourceMapConsumer(util.testMap); + var map; var mapping; + map = new SourceMapConsumer(util.testMap); + mapping = map.originalPositionFor({ line: 2, column: 1 @@ -50,6 +66,36 @@ define("test/source-map/test-source-map-consumer", ["require", "exports", "modul column: 1 }); assert.equal(mapping.source, '/the/root/one.js'); + + + map = new SourceMapConsumer(util.testMapNoSourceRoot); + + mapping = map.originalPositionFor({ + line: 2, + column: 1 + }); + assert.equal(mapping.source, 'two.js'); + + mapping = map.originalPositionFor({ + line: 1, + column: 1 + }); + assert.equal(mapping.source, 'one.js'); + + + map = new SourceMapConsumer(util.testMapEmptySourceRoot); + + mapping = map.originalPositionFor({ + line: 2, + column: 1 + }); + assert.equal(mapping.source, 'two.js'); + + mapping = map.originalPositionFor({ + line: 1, + column: 1 + }); + assert.equal(mapping.source, 'one.js'); }; exports['test mapping tokens back exactly'] = function (assert, util) { @@ -85,6 +131,30 @@ define("test/source-map/test-source-map-consumer", ["require", "exports", "modul util.assertMapping(2, 9, '/the/root/two.js', 1, 16, null, map, assert, null, true); }; + exports['test mappings and end of lines'] = function (assert, util) { + var smg = new SourceMapGenerator({ + file: 'foo.js' + }); + smg.addMapping({ + original: { line: 1, column: 1 }, + generated: { line: 1, column: 1 }, + source: 'bar.js' + }); + smg.addMapping({ + original: { line: 2, column: 2 }, + generated: { line: 2, column: 2 }, + source: 'bar.js' + }); + + var map = SourceMapConsumer.fromSourceMap(smg); + + // When finding original positions, mappings end at the end of the line. + util.assertMapping(2, 1, null, null, null, null, map, assert, true) + + // When finding generated positions, mappings do not end at the end of the line. + util.assertMapping(1, 1, 'bar.js', 2, 1, null, map, assert, null, true); + }; + exports['test creating source map consumers with )]}\' prefix'] = function (assert, util) { assert.doesNotThrow(function () { var map = new SourceMapConsumer(")]}'" + JSON.stringify(util.testMap)); @@ -92,15 +162,15 @@ define("test/source-map/test-source-map-consumer", ["require", "exports", "modul }; exports['test eachMapping'] = function (assert, util) { - var map = new SourceMapConsumer(util.testMap); + var map; + + map = new SourceMapConsumer(util.testMap); var previousLine = -Infinity; var previousColumn = -Infinity; map.eachMapping(function (mapping) { assert.ok(mapping.generatedLine >= previousLine); - if (mapping.source) { - assert.equal(mapping.source.indexOf(util.testMap.sourceRoot), 0); - } + assert.ok(mapping.source === '/the/root/one.js' || mapping.source === '/the/root/two.js'); if (mapping.generatedLine === previousLine) { assert.ok(mapping.generatedColumn >= previousColumn); @@ -111,6 +181,16 @@ define("test/source-map/test-source-map-consumer", ["require", "exports", "modul previousColumn = -Infinity; } }); + + map = new SourceMapConsumer(util.testMapNoSourceRoot); + map.eachMapping(function (mapping) { + assert.ok(mapping.source === 'one.js' || mapping.source === 'two.js'); + }); + + map = new SourceMapConsumer(util.testMapEmptySourceRoot); + map.eachMapping(function (mapping) { + assert.ok(mapping.source === 'one.js' || mapping.source === 'two.js'); + }); }; exports['test iterating over mappings in a different order'] = function (assert, util) { diff --git a/toolkit/devtools/sourcemap/tests/unit/test_source_map_generator.js b/toolkit/devtools/sourcemap/tests/unit/test_source_map_generator.js index b49682b0d907..58582d9966e0 100644 --- a/toolkit/devtools/sourcemap/tests/unit/test_source_map_generator.js +++ b/toolkit/devtools/sourcemap/tests/unit/test_source_map_generator.js @@ -25,6 +25,10 @@ define("test/source-map/test-source-map-generator", ["require", "exports", "modu sourceRoot: '.' }); assert.ok(true); + + var map = new SourceMapGenerator().toJSON(); + assert.ok(!('file' in map)); + assert.ok(!('sourceRoot' in map)); }; exports['test JSON serialization'] = function (assert, util) { @@ -182,6 +186,24 @@ define("test/source-map/test-source-map-generator", ["require", "exports", "modu util.assertEqualMaps(assert, map, util.testMap); }; + exports['test that adding a mapping with an empty string name does not break generation'] = function (assert, util) { + var map = new SourceMapGenerator({ + file: 'generated-foo.js', + sourceRoot: '.' + }); + + map.addMapping({ + generated: { line: 1, column: 1 }, + source: 'bar.js', + original: { line: 1, column: 1 }, + name: '' + }); + + assert.doesNotThrow(function () { + JSON.parse(map.toString()); + }); + }; + exports['test that source content can be set'] = function (assert, util) { var map = new SourceMapGenerator({ file: 'min.js', @@ -274,6 +296,218 @@ define("test/source-map/test-source-map-generator", ["require", "exports", "modu util.assertEqualMaps(assert, actualMap, expectedMap); }; + exports['test applySourceMap throws when file is missing'] = function (assert, util) { + var map = new SourceMapGenerator({ + file: 'test.js' + }); + var map2 = new SourceMapGenerator(); + assert.throws(function() { + map.applySourceMap(new SourceMapConsumer(map2.toJSON())); + }); + }; + + exports['test the two additional parameters of applySourceMap'] = function (assert, util) { + // Assume the following directory structure: + // + // http://foo.org/ + // bar.coffee + // app/ + // coffee/ + // foo.coffee + // temp/ + // bundle.js + // temp_maps/ + // bundle.js.map + // public/ + // bundle.min.js + // bundle.min.js.map + // + // http://www.example.com/ + // baz.coffee + + var bundleMap = new SourceMapGenerator({ + file: 'bundle.js' + }); + bundleMap.addMapping({ + generated: { line: 3, column: 3 }, + original: { line: 2, column: 2 }, + source: '../../coffee/foo.coffee' + }); + bundleMap.setSourceContent('../../coffee/foo.coffee', 'foo coffee'); + bundleMap.addMapping({ + generated: { line: 13, column: 13 }, + original: { line: 12, column: 12 }, + source: '/bar.coffee' + }); + bundleMap.setSourceContent('/bar.coffee', 'bar coffee'); + bundleMap.addMapping({ + generated: { line: 23, column: 23 }, + original: { line: 22, column: 22 }, + source: 'http://www.example.com/baz.coffee' + }); + bundleMap.setSourceContent( + 'http://www.example.com/baz.coffee', + 'baz coffee' + ); + bundleMap = new SourceMapConsumer(bundleMap.toJSON()); + + var minifiedMap = new SourceMapGenerator({ + file: 'bundle.min.js', + sourceRoot: '..' + }); + minifiedMap.addMapping({ + generated: { line: 1, column: 1 }, + original: { line: 3, column: 3 }, + source: 'temp/bundle.js' + }); + minifiedMap.addMapping({ + generated: { line: 11, column: 11 }, + original: { line: 13, column: 13 }, + source: 'temp/bundle.js' + }); + minifiedMap.addMapping({ + generated: { line: 21, column: 21 }, + original: { line: 23, column: 23 }, + source: 'temp/bundle.js' + }); + minifiedMap = new SourceMapConsumer(minifiedMap.toJSON()); + + var expectedMap = function (sources) { + var map = new SourceMapGenerator({ + file: 'bundle.min.js', + sourceRoot: '..' + }); + map.addMapping({ + generated: { line: 1, column: 1 }, + original: { line: 2, column: 2 }, + source: sources[0] + }); + map.setSourceContent(sources[0], 'foo coffee'); + map.addMapping({ + generated: { line: 11, column: 11 }, + original: { line: 12, column: 12 }, + source: sources[1] + }); + map.setSourceContent(sources[1], 'bar coffee'); + map.addMapping({ + generated: { line: 21, column: 21 }, + original: { line: 22, column: 22 }, + source: sources[2] + }); + map.setSourceContent(sources[2], 'baz coffee'); + return map.toJSON(); + } + + var actualMap = function (aSourceMapPath) { + var map = SourceMapGenerator.fromSourceMap(minifiedMap); + // Note that relying on `bundleMap.file` (which is simply 'bundle.js') + // instead of supplying the second parameter wouldn't work here. + map.applySourceMap(bundleMap, '../temp/bundle.js', aSourceMapPath); + return map.toJSON(); + } + + util.assertEqualMaps(assert, actualMap('../temp/temp_maps'), expectedMap([ + 'coffee/foo.coffee', + '/bar.coffee', + 'http://www.example.com/baz.coffee' + ])); + + util.assertEqualMaps(assert, actualMap('/app/temp/temp_maps'), expectedMap([ + '/app/coffee/foo.coffee', + '/bar.coffee', + 'http://www.example.com/baz.coffee' + ])); + + util.assertEqualMaps(assert, actualMap('http://foo.org/app/temp/temp_maps'), expectedMap([ + 'http://foo.org/app/coffee/foo.coffee', + 'http://foo.org/bar.coffee', + 'http://www.example.com/baz.coffee' + ])); + + // If the third parameter is omitted or set to the current working + // directory we get incorrect source paths: + + util.assertEqualMaps(assert, actualMap(), expectedMap([ + '../coffee/foo.coffee', + '/bar.coffee', + 'http://www.example.com/baz.coffee' + ])); + + util.assertEqualMaps(assert, actualMap(''), expectedMap([ + '../coffee/foo.coffee', + '/bar.coffee', + 'http://www.example.com/baz.coffee' + ])); + + util.assertEqualMaps(assert, actualMap('.'), expectedMap([ + '../coffee/foo.coffee', + '/bar.coffee', + 'http://www.example.com/baz.coffee' + ])); + + util.assertEqualMaps(assert, actualMap('./'), expectedMap([ + '../coffee/foo.coffee', + '/bar.coffee', + 'http://www.example.com/baz.coffee' + ])); + }; + + exports['test applySourceMap name handling'] = function (assert, util) { + // Imagine some CoffeeScript code being compiled into JavaScript and then + // minified. + + var assertName = function(coffeeName, jsName, expectedName) { + var minifiedMap = new SourceMapGenerator({ + file: 'test.js.min' + }); + minifiedMap.addMapping({ + generated: { line: 1, column: 4 }, + original: { line: 1, column: 4 }, + source: 'test.js', + name: jsName + }); + + var coffeeMap = new SourceMapGenerator({ + file: 'test.js' + }); + coffeeMap.addMapping({ + generated: { line: 1, column: 4 }, + original: { line: 1, column: 0 }, + source: 'test.coffee', + name: coffeeName + }); + + minifiedMap.applySourceMap(new SourceMapConsumer(coffeeMap.toJSON())); + + new SourceMapConsumer(minifiedMap.toJSON()).eachMapping(function(mapping) { + assert.equal(mapping.name, expectedName); + }); + }; + + // `foo = 1` -> `var foo = 1;` -> `var a=1` + // CoffeeScript doesn’t rename variables, so there’s no need for it to + // provide names in its source maps. Minifiers do rename variables and + // therefore do provide names in their source maps. So that name should be + // retained if the original map lacks names. + assertName(null, 'foo', 'foo'); + + // `foo = 1` -> `var coffee$foo = 1;` -> `var a=1` + // Imagine that CoffeeScript prefixed all variables with `coffee$`. Even + // though the minifier then also provides a name, the original name is + // what corresponds to the source. + assertName('foo', 'coffee$foo', 'foo'); + + // `foo = 1` -> `var coffee$foo = 1;` -> `var coffee$foo=1` + // Minifiers can turn off variable mangling. Then there’s no need to + // provide names in the source map, but the names from the original map are + // still needed. + assertName('foo', null, 'foo'); + + // `foo = 1` -> `var foo = 1;` -> `var foo=1` + // No renaming at all. + assertName(null, null, null); + }; + exports['test sorting with duplicate generated mappings'] = function (assert, util) { var map = new SourceMapGenerator({ file: 'test.js' @@ -419,6 +653,13 @@ define("test/source-map/test-source-map-generator", ["require", "exports", "modu }); }; + exports['test setting sourcesContent to null when already null'] = function (assert, util) { + var smg = new SourceMapGenerator({ file: "foo.js" }); + assert.doesNotThrow(function() { + smg.setSourceContent("bar.js", null); + }); + }; + }); function run_test() { runSourceMapTests('test/source-map/test-source-map-generator', do_throw); diff --git a/toolkit/devtools/sourcemap/tests/unit/test_source_node.js b/toolkit/devtools/sourcemap/tests/unit/test_source_node.js index fa1639562027..d3037010e8e7 100644 --- a/toolkit/devtools/sourcemap/tests/unit/test_source_node.js +++ b/toolkit/devtools/sourcemap/tests/unit/test_source_node.js @@ -18,6 +18,12 @@ define("test/source-map/test-source-node", ["require", "exports", "module"], fun var SourceMapConsumer = require('source-map/source-map-consumer').SourceMapConsumer; var SourceNode = require('source-map/source-node').SourceNode; + function forEachNewline(fn) { + return function (assert, util) { + ['\n', '\r\n'].forEach(fn.bind(null, assert, util)); + } + } + exports['test .add()'] = function (assert, util) { var node = new SourceNode(null, null, null); @@ -133,20 +139,35 @@ define("test/source-map/test-source-node", ["require", "exports", "module"], fun assert.equal(node.toString(), 'hey sexy mama, want to watch Futurama?'); }; - exports['test .toStringWithSourceMap()'] = function (assert, util) { + exports['test .toStringWithSourceMap()'] = forEachNewline(function (assert, util, nl) { var node = new SourceNode(null, null, null, - ['(function () {\n', + ['(function () {' + nl, ' ', new SourceNode(1, 0, 'a.js', 'someCall', 'originalCall'), new SourceNode(1, 8, 'a.js', '()'), - ';\n', - ' ', new SourceNode(2, 0, 'b.js', ['if (foo) bar()']), ';\n', + ';' + nl, + ' ', new SourceNode(2, 0, 'b.js', ['if (foo) bar()']), ';' + nl, '}());']); - var map = node.toStringWithSourceMap({ + var result = node.toStringWithSourceMap({ file: 'foo.js' - }).map; + }); + + assert.equal(result.code, [ + '(function () {', + ' someCall();', + ' if (foo) bar();', + '}());' + ].join(nl)); + + var map = result.map; + var mapWithoutOptions = node.toStringWithSourceMap().map; assert.ok(map instanceof SourceMapGenerator, 'map instanceof SourceMapGenerator'); + assert.ok(mapWithoutOptions instanceof SourceMapGenerator, 'mapWithoutOptions instanceof SourceMapGenerator'); + assert.ok(!('file' in mapWithoutOptions)); + mapWithoutOptions._file = 'foo.js'; + util.assertEqualMaps(assert, map.toJSON(), mapWithoutOptions.toJSON()); + map = new SourceMapConsumer(map.toString()); var actual; @@ -191,11 +212,12 @@ define("test/source-map/test-source-node", ["require", "exports", "module"], fun assert.equal(actual.source, null); assert.equal(actual.line, null); assert.equal(actual.column, null); - }; + }); - exports['test .fromStringWithSourceMap()'] = function (assert, util) { + exports['test .fromStringWithSourceMap()'] = forEachNewline(function (assert, util, nl) { + var testCode = util.testGeneratedCode.replace(/\n/g, nl); var node = SourceNode.fromStringWithSourceMap( - util.testGeneratedCode, + testCode, new SourceMapConsumer(util.testMap)); var result = node.toStringWithSourceMap({ @@ -204,17 +226,17 @@ define("test/source-map/test-source-node", ["require", "exports", "module"], fun var map = result.map; var code = result.code; - assert.equal(code, util.testGeneratedCode); + assert.equal(code, testCode); assert.ok(map instanceof SourceMapGenerator, 'map instanceof SourceMapGenerator'); map = map.toJSON(); assert.equal(map.version, util.testMap.version); assert.equal(map.file, util.testMap.file); assert.equal(map.mappings, util.testMap.mappings); - }; + }); - exports['test .fromStringWithSourceMap() empty map'] = function (assert, util) { + exports['test .fromStringWithSourceMap() empty map'] = forEachNewline(function (assert, util, nl) { var node = SourceNode.fromStringWithSourceMap( - util.testGeneratedCode, + util.testGeneratedCode.replace(/\n/g, nl), new SourceMapConsumer(util.emptyMap)); var result = node.toStringWithSourceMap({ file: 'min.js' @@ -222,22 +244,22 @@ define("test/source-map/test-source-node", ["require", "exports", "module"], fun var map = result.map; var code = result.code; - assert.equal(code, util.testGeneratedCode); + assert.equal(code, util.testGeneratedCode.replace(/\n/g, nl)); assert.ok(map instanceof SourceMapGenerator, 'map instanceof SourceMapGenerator'); map = map.toJSON(); assert.equal(map.version, util.emptyMap.version); assert.equal(map.file, util.emptyMap.file); assert.equal(map.mappings.length, util.emptyMap.mappings.length); assert.equal(map.mappings, util.emptyMap.mappings); - }; + }); - exports['test .fromStringWithSourceMap() complex version'] = function (assert, util) { + exports['test .fromStringWithSourceMap() complex version'] = forEachNewline(function (assert, util, nl) { var input = new SourceNode(null, null, null, [ - "(function() {\n", - " var Test = {};\n", - " ", new SourceNode(1, 0, "a.js", "Test.A = { value: 1234 };\n"), - " ", new SourceNode(2, 0, "a.js", "Test.A.x = 'xyz';"), "\n", - "}());\n", + "(function() {" + nl, + " var Test = {};" + nl, + " ", new SourceNode(1, 0, "a.js", "Test.A = { value: 1234 };" + nl), + " ", new SourceNode(2, 0, "a.js", "Test.A.x = 'xyz';"), nl, + "}());" + nl, "/* Generated Source */"]); input = input.toStringWithSourceMap({ file: 'foo.js' @@ -258,25 +280,123 @@ define("test/source-map/test-source-node", ["require", "exports", "module"], fun map = map.toJSON(); var inputMap = input.map.toJSON(); util.assertEqualMaps(assert, map, inputMap); + }); + + exports['test .fromStringWithSourceMap() third argument'] = function (assert, util) { + // Assume the following directory structure: + // + // http://foo.org/ + // bar.coffee + // app/ + // coffee/ + // foo.coffee + // coffeeBundle.js # Made from {foo,bar,baz}.coffee + // maps/ + // coffeeBundle.js.map + // js/ + // foo.js + // public/ + // app.js # Made from {foo,coffeeBundle}.js + // app.js.map + // + // http://www.example.com/ + // baz.coffee + + var coffeeBundle = new SourceNode(1, 0, 'foo.coffee', 'foo(coffee);\n'); + coffeeBundle.setSourceContent('foo.coffee', 'foo coffee'); + coffeeBundle.add(new SourceNode(2, 0, '/bar.coffee', 'bar(coffee);\n')); + coffeeBundle.add(new SourceNode(3, 0, 'http://www.example.com/baz.coffee', 'baz(coffee);')); + coffeeBundle = coffeeBundle.toStringWithSourceMap({ + file: 'foo.js', + sourceRoot: '..' + }); + + var foo = new SourceNode(1, 0, 'foo.js', 'foo(js);'); + + var test = function(relativePath, expectedSources) { + var app = new SourceNode(); + app.add(SourceNode.fromStringWithSourceMap( + coffeeBundle.code, + new SourceMapConsumer(coffeeBundle.map.toString()), + relativePath)); + app.add(foo); + var i = 0; + app.walk(function (chunk, loc) { + assert.equal(loc.source, expectedSources[i]); + i++; + }); + app.walkSourceContents(function (sourceFile, sourceContent) { + assert.equal(sourceFile, expectedSources[0]); + assert.equal(sourceContent, 'foo coffee'); + }) + }; + + test('../coffee/maps', [ + '../coffee/foo.coffee', + '/bar.coffee', + 'http://www.example.com/baz.coffee', + 'foo.js' + ]); + + // If the third parameter is omitted or set to the current working + // directory we get incorrect source paths: + + test(undefined, [ + '../foo.coffee', + '/bar.coffee', + 'http://www.example.com/baz.coffee', + 'foo.js' + ]); + + test('', [ + '../foo.coffee', + '/bar.coffee', + 'http://www.example.com/baz.coffee', + 'foo.js' + ]); + + test('.', [ + '../foo.coffee', + '/bar.coffee', + 'http://www.example.com/baz.coffee', + 'foo.js' + ]); + + test('./', [ + '../foo.coffee', + '/bar.coffee', + 'http://www.example.com/baz.coffee', + 'foo.js' + ]); }; - exports['test .fromStringWithSourceMap() merging duplicate mappings'] = function (assert, util) { + exports['test .toStringWithSourceMap() merging duplicate mappings'] = forEachNewline(function (assert, util, nl) { var input = new SourceNode(null, null, null, [ new SourceNode(1, 0, "a.js", "(function"), - new SourceNode(1, 0, "a.js", "() {\n"), + new SourceNode(1, 0, "a.js", "() {" + nl), " ", new SourceNode(1, 0, "a.js", "var Test = "), - new SourceNode(1, 0, "b.js", "{};\n"), + new SourceNode(1, 0, "b.js", "{};" + nl), new SourceNode(2, 0, "b.js", "Test"), new SourceNode(2, 0, "b.js", ".A", "A"), - new SourceNode(2, 20, "b.js", " = { value: 1234 };\n", "A"), - "}());\n", + new SourceNode(2, 20, "b.js", " = { value: ", "A"), + "1234", + new SourceNode(2, 40, "b.js", " };" + nl, "A"), + "}());" + nl, "/* Generated Source */" ]); input = input.toStringWithSourceMap({ file: 'foo.js' }); + assert.equal(input.code, [ + "(function() {", + " var Test = {};", + "Test.A = { value: 1234 };", + "}());", + "/* Generated Source */" + ].join(nl)) + var correctMap = new SourceMapGenerator({ file: 'foo.js' }); @@ -285,9 +405,8 @@ define("test/source-map/test-source-node", ["require", "exports", "module"], fun source: 'a.js', original: { line: 1, column: 0 } }); - correctMap.addMapping({ - generated: { line: 2, column: 0 } - }); + // Here is no need for a empty mapping, + // because mappings ends at eol correctMap.addMapping({ generated: { line: 2, column: 2 }, source: 'a.js', @@ -315,15 +434,143 @@ define("test/source-map/test-source-node", ["require", "exports", "module"], fun name: 'A', original: { line: 2, column: 20 } }); + // This empty mapping is required, + // because there is a hole in the middle of the line correctMap.addMapping({ - generated: { line: 4, column: 0 } + generated: { line: 3, column: 18 } + }); + correctMap.addMapping({ + generated: { line: 3, column: 22 }, + source: 'b.js', + name: 'A', + original: { line: 2, column: 40 } + }); + // Here is no need for a empty mapping, + // because mappings ends at eol + + var inputMap = input.map.toJSON(); + correctMap = correctMap.toJSON(); + util.assertEqualMaps(assert, inputMap, correctMap); + }); + + exports['test .toStringWithSourceMap() multi-line SourceNodes'] = forEachNewline(function (assert, util, nl) { + var input = new SourceNode(null, null, null, [ + new SourceNode(1, 0, "a.js", "(function() {" + nl + "var nextLine = 1;" + nl + "anotherLine();" + nl), + new SourceNode(2, 2, "b.js", "Test.call(this, 123);" + nl), + new SourceNode(2, 2, "b.js", "this['stuff'] = 'v';" + nl), + new SourceNode(2, 2, "b.js", "anotherLine();" + nl), + "/*" + nl + "Generated" + nl + "Source" + nl + "*/" + nl, + new SourceNode(3, 4, "c.js", "anotherLine();" + nl), + "/*" + nl + "Generated" + nl + "Source" + nl + "*/" + ]); + input = input.toStringWithSourceMap({ + file: 'foo.js' + }); + + assert.equal(input.code, [ + "(function() {", + "var nextLine = 1;", + "anotherLine();", + "Test.call(this, 123);", + "this['stuff'] = 'v';", + "anotherLine();", + "/*", + "Generated", + "Source", + "*/", + "anotherLine();", + "/*", + "Generated", + "Source", + "*/" + ].join(nl)); + + var correctMap = new SourceMapGenerator({ + file: 'foo.js' + }); + correctMap.addMapping({ + generated: { line: 1, column: 0 }, + source: 'a.js', + original: { line: 1, column: 0 } + }); + correctMap.addMapping({ + generated: { line: 2, column: 0 }, + source: 'a.js', + original: { line: 1, column: 0 } + }); + correctMap.addMapping({ + generated: { line: 3, column: 0 }, + source: 'a.js', + original: { line: 1, column: 0 } + }); + correctMap.addMapping({ + generated: { line: 4, column: 0 }, + source: 'b.js', + original: { line: 2, column: 2 } + }); + correctMap.addMapping({ + generated: { line: 5, column: 0 }, + source: 'b.js', + original: { line: 2, column: 2 } + }); + correctMap.addMapping({ + generated: { line: 6, column: 0 }, + source: 'b.js', + original: { line: 2, column: 2 } + }); + correctMap.addMapping({ + generated: { line: 11, column: 0 }, + source: 'c.js', + original: { line: 3, column: 4 } }); var inputMap = input.map.toJSON(); correctMap = correctMap.toJSON(); - util.assertEqualMaps(assert, correctMap, inputMap); + util.assertEqualMaps(assert, inputMap, correctMap); + }); + + exports['test .toStringWithSourceMap() with empty string'] = function (assert, util) { + var node = new SourceNode(1, 0, 'empty.js', ''); + var result = node.toStringWithSourceMap(); + assert.equal(result.code, ''); }; + exports['test .toStringWithSourceMap() with consecutive newlines'] = forEachNewline(function (assert, util, nl) { + var input = new SourceNode(null, null, null, [ + "/***/" + nl + nl, + new SourceNode(1, 0, "a.js", "'use strict';" + nl), + new SourceNode(2, 0, "a.js", "a();"), + ]); + input = input.toStringWithSourceMap({ + file: 'foo.js' + }); + + assert.equal(input.code, [ + "/***/", + "", + "'use strict';", + "a();", + ].join(nl)); + + var correctMap = new SourceMapGenerator({ + file: 'foo.js' + }); + correctMap.addMapping({ + generated: { line: 3, column: 0 }, + source: 'a.js', + original: { line: 1, column: 0 } + }); + correctMap.addMapping({ + generated: { line: 4, column: 0 }, + source: 'a.js', + original: { line: 2, column: 0 } + }); + + var inputMap = input.map.toJSON(); + correctMap = correctMap.toJSON(); + util.assertEqualMaps(assert, inputMap, correctMap); + }); + exports['test setSourceContent with toStringWithSourceMap'] = function (assert, util) { var aNode = new SourceNode(1, 1, 'a.js', 'a'); aNode.setSourceContent('a.js', 'someContent'); diff --git a/toolkit/devtools/sourcemap/tests/unit/test_util.js b/toolkit/devtools/sourcemap/tests/unit/test_util.js new file mode 100644 index 000000000000..6419a21e0ef2 --- /dev/null +++ b/toolkit/devtools/sourcemap/tests/unit/test_util.js @@ -0,0 +1,224 @@ +/* + * WARNING! + * + * Do not edit this file directly, it is built from the sources at + * https://github.com/mozilla/source-map/ + */ + +Components.utils.import('resource://test/Utils.jsm'); +/* -*- Mode: js; js-indent-level: 2; -*- */ +/* + * Copyright 2014 Mozilla Foundation and contributors + * Licensed under the New BSD license. See LICENSE or: + * http://opensource.org/licenses/BSD-3-Clause + */ +define("test/source-map/test-util", ["require", "exports", "module"], function (require, exports, module) { + + var libUtil = require('source-map/util'); + + exports['test urls'] = function (assert, util) { + var assertUrl = function (url) { + assert.equal(url, libUtil.urlGenerate(libUtil.urlParse(url))); + }; + assertUrl('http://'); + assertUrl('http://www.example.com'); + assertUrl('http://user:pass@www.example.com'); + assertUrl('http://www.example.com:80'); + assertUrl('http://www.example.com/'); + assertUrl('http://www.example.com/foo/bar'); + assertUrl('http://www.example.com/foo/bar/'); + assertUrl('http://user:pass@www.example.com:80/foo/bar/'); + + assertUrl('//'); + assertUrl('//www.example.com'); + assertUrl('file:///www.example.com'); + + assert.equal(libUtil.urlParse(''), null); + assert.equal(libUtil.urlParse('.'), null); + assert.equal(libUtil.urlParse('..'), null); + assert.equal(libUtil.urlParse('a'), null); + assert.equal(libUtil.urlParse('a/b'), null); + assert.equal(libUtil.urlParse('a//b'), null); + assert.equal(libUtil.urlParse('/a'), null); + assert.equal(libUtil.urlParse('data:foo,bar'), null); + }; + + exports['test normalize()'] = function (assert, util) { + assert.equal(libUtil.normalize('/..'), '/'); + assert.equal(libUtil.normalize('/../'), '/'); + assert.equal(libUtil.normalize('/../../../..'), '/'); + assert.equal(libUtil.normalize('/../../../../a/b/c'), '/a/b/c'); + assert.equal(libUtil.normalize('/a/b/c/../../../d/../../e'), '/e'); + + assert.equal(libUtil.normalize('..'), '..'); + assert.equal(libUtil.normalize('../'), '../'); + assert.equal(libUtil.normalize('../../a/'), '../../a/'); + assert.equal(libUtil.normalize('a/..'), '.'); + assert.equal(libUtil.normalize('a/../../..'), '../..'); + + assert.equal(libUtil.normalize('/.'), '/'); + assert.equal(libUtil.normalize('/./'), '/'); + assert.equal(libUtil.normalize('/./././.'), '/'); + assert.equal(libUtil.normalize('/././././a/b/c'), '/a/b/c'); + assert.equal(libUtil.normalize('/a/b/c/./././d/././e'), '/a/b/c/d/e'); + + assert.equal(libUtil.normalize(''), '.'); + assert.equal(libUtil.normalize('.'), '.'); + assert.equal(libUtil.normalize('./'), '.'); + assert.equal(libUtil.normalize('././a'), 'a'); + assert.equal(libUtil.normalize('a/./'), 'a/'); + assert.equal(libUtil.normalize('a/././.'), 'a'); + + assert.equal(libUtil.normalize('/a/b//c////d/////'), '/a/b/c/d/'); + assert.equal(libUtil.normalize('///a/b//c////d/////'), '///a/b/c/d/'); + assert.equal(libUtil.normalize('a/b//c////d'), 'a/b/c/d'); + + assert.equal(libUtil.normalize('.///.././../a/b//./..'), '../../a') + + assert.equal(libUtil.normalize('http://www.example.com'), 'http://www.example.com'); + assert.equal(libUtil.normalize('http://www.example.com/'), 'http://www.example.com/'); + assert.equal(libUtil.normalize('http://www.example.com/./..//a/b/c/.././d//'), 'http://www.example.com/a/b/d/'); + }; + + exports['test join()'] = function (assert, util) { + assert.equal(libUtil.join('a', 'b'), 'a/b'); + assert.equal(libUtil.join('a/', 'b'), 'a/b'); + assert.equal(libUtil.join('a//', 'b'), 'a/b'); + assert.equal(libUtil.join('a', 'b/'), 'a/b/'); + assert.equal(libUtil.join('a', 'b//'), 'a/b/'); + assert.equal(libUtil.join('a/', '/b'), '/b'); + assert.equal(libUtil.join('a//', '//b'), '//b'); + + assert.equal(libUtil.join('a', '..'), '.'); + assert.equal(libUtil.join('a', '../b'), 'b'); + assert.equal(libUtil.join('a/b', '../c'), 'a/c'); + + assert.equal(libUtil.join('a', '.'), 'a'); + assert.equal(libUtil.join('a', './b'), 'a/b'); + assert.equal(libUtil.join('a/b', './c'), 'a/b/c'); + + assert.equal(libUtil.join('a', 'http://www.example.com'), 'http://www.example.com'); + assert.equal(libUtil.join('a', 'data:foo,bar'), 'data:foo,bar'); + + + assert.equal(libUtil.join('', 'b'), 'b'); + assert.equal(libUtil.join('.', 'b'), 'b'); + assert.equal(libUtil.join('', 'b/'), 'b/'); + assert.equal(libUtil.join('.', 'b/'), 'b/'); + assert.equal(libUtil.join('', 'b//'), 'b/'); + assert.equal(libUtil.join('.', 'b//'), 'b/'); + + assert.equal(libUtil.join('', '..'), '..'); + assert.equal(libUtil.join('.', '..'), '..'); + assert.equal(libUtil.join('', '../b'), '../b'); + assert.equal(libUtil.join('.', '../b'), '../b'); + + assert.equal(libUtil.join('', '.'), '.'); + assert.equal(libUtil.join('.', '.'), '.'); + assert.equal(libUtil.join('', './b'), 'b'); + assert.equal(libUtil.join('.', './b'), 'b'); + + assert.equal(libUtil.join('', 'http://www.example.com'), 'http://www.example.com'); + assert.equal(libUtil.join('.', 'http://www.example.com'), 'http://www.example.com'); + assert.equal(libUtil.join('', 'data:foo,bar'), 'data:foo,bar'); + assert.equal(libUtil.join('.', 'data:foo,bar'), 'data:foo,bar'); + + + assert.equal(libUtil.join('..', 'b'), '../b'); + assert.equal(libUtil.join('..', 'b/'), '../b/'); + assert.equal(libUtil.join('..', 'b//'), '../b/'); + + assert.equal(libUtil.join('..', '..'), '../..'); + assert.equal(libUtil.join('..', '../b'), '../../b'); + + assert.equal(libUtil.join('..', '.'), '..'); + assert.equal(libUtil.join('..', './b'), '../b'); + + assert.equal(libUtil.join('..', 'http://www.example.com'), 'http://www.example.com'); + assert.equal(libUtil.join('..', 'data:foo,bar'), 'data:foo,bar'); + + + assert.equal(libUtil.join('a', ''), 'a'); + assert.equal(libUtil.join('a', '.'), 'a'); + assert.equal(libUtil.join('a/', ''), 'a'); + assert.equal(libUtil.join('a/', '.'), 'a'); + assert.equal(libUtil.join('a//', ''), 'a'); + assert.equal(libUtil.join('a//', '.'), 'a'); + assert.equal(libUtil.join('/a', ''), '/a'); + assert.equal(libUtil.join('/a', '.'), '/a'); + assert.equal(libUtil.join('', ''), '.'); + assert.equal(libUtil.join('.', ''), '.'); + assert.equal(libUtil.join('.', ''), '.'); + assert.equal(libUtil.join('.', '.'), '.'); + assert.equal(libUtil.join('..', ''), '..'); + assert.equal(libUtil.join('..', '.'), '..'); + assert.equal(libUtil.join('http://foo.org/a', ''), 'http://foo.org/a'); + assert.equal(libUtil.join('http://foo.org/a', '.'), 'http://foo.org/a'); + assert.equal(libUtil.join('http://foo.org/a/', ''), 'http://foo.org/a'); + assert.equal(libUtil.join('http://foo.org/a/', '.'), 'http://foo.org/a'); + assert.equal(libUtil.join('http://foo.org/a//', ''), 'http://foo.org/a'); + assert.equal(libUtil.join('http://foo.org/a//', '.'), 'http://foo.org/a'); + assert.equal(libUtil.join('http://foo.org', ''), 'http://foo.org/'); + assert.equal(libUtil.join('http://foo.org', '.'), 'http://foo.org/'); + assert.equal(libUtil.join('http://foo.org/', ''), 'http://foo.org/'); + assert.equal(libUtil.join('http://foo.org/', '.'), 'http://foo.org/'); + assert.equal(libUtil.join('http://foo.org//', ''), 'http://foo.org/'); + assert.equal(libUtil.join('http://foo.org//', '.'), 'http://foo.org/'); + assert.equal(libUtil.join('//www.example.com', ''), '//www.example.com/'); + assert.equal(libUtil.join('//www.example.com', '.'), '//www.example.com/'); + + + assert.equal(libUtil.join('http://foo.org/a', 'b'), 'http://foo.org/a/b'); + assert.equal(libUtil.join('http://foo.org/a/', 'b'), 'http://foo.org/a/b'); + assert.equal(libUtil.join('http://foo.org/a//', 'b'), 'http://foo.org/a/b'); + assert.equal(libUtil.join('http://foo.org/a', 'b/'), 'http://foo.org/a/b/'); + assert.equal(libUtil.join('http://foo.org/a', 'b//'), 'http://foo.org/a/b/'); + assert.equal(libUtil.join('http://foo.org/a/', '/b'), 'http://foo.org/b'); + assert.equal(libUtil.join('http://foo.org/a//', '//b'), 'http://b'); + + assert.equal(libUtil.join('http://foo.org/a', '..'), 'http://foo.org/'); + assert.equal(libUtil.join('http://foo.org/a', '../b'), 'http://foo.org/b'); + assert.equal(libUtil.join('http://foo.org/a/b', '../c'), 'http://foo.org/a/c'); + + assert.equal(libUtil.join('http://foo.org/a', '.'), 'http://foo.org/a'); + assert.equal(libUtil.join('http://foo.org/a', './b'), 'http://foo.org/a/b'); + assert.equal(libUtil.join('http://foo.org/a/b', './c'), 'http://foo.org/a/b/c'); + + assert.equal(libUtil.join('http://foo.org/a', 'http://www.example.com'), 'http://www.example.com'); + assert.equal(libUtil.join('http://foo.org/a', 'data:foo,bar'), 'data:foo,bar'); + + + assert.equal(libUtil.join('http://foo.org', 'a'), 'http://foo.org/a'); + assert.equal(libUtil.join('http://foo.org/', 'a'), 'http://foo.org/a'); + assert.equal(libUtil.join('http://foo.org//', 'a'), 'http://foo.org/a'); + assert.equal(libUtil.join('http://foo.org', '/a'), 'http://foo.org/a'); + assert.equal(libUtil.join('http://foo.org/', '/a'), 'http://foo.org/a'); + assert.equal(libUtil.join('http://foo.org//', '/a'), 'http://foo.org/a'); + + + assert.equal(libUtil.join('http://', 'www.example.com'), 'http://www.example.com'); + assert.equal(libUtil.join('file:///', 'www.example.com'), 'file:///www.example.com'); + assert.equal(libUtil.join('http://', 'ftp://example.com'), 'ftp://example.com'); + + assert.equal(libUtil.join('http://www.example.com', '//foo.org/bar'), 'http://foo.org/bar'); + assert.equal(libUtil.join('//www.example.com', '//foo.org/bar'), '//foo.org/bar'); + }; + + // TODO Issue #128: Define and test this function properly. + exports['test relative()'] = function (assert, util) { + assert.equal(libUtil.relative('/the/root', '/the/root/one.js'), 'one.js'); + assert.equal(libUtil.relative('/the/root', '/the/rootone.js'), '/the/rootone.js'); + + assert.equal(libUtil.relative('', '/the/root/one.js'), '/the/root/one.js'); + assert.equal(libUtil.relative('.', '/the/root/one.js'), '/the/root/one.js'); + assert.equal(libUtil.relative('', 'the/root/one.js'), 'the/root/one.js'); + assert.equal(libUtil.relative('.', 'the/root/one.js'), 'the/root/one.js'); + + assert.equal(libUtil.relative('/', '/the/root/one.js'), 'the/root/one.js'); + assert.equal(libUtil.relative('/', 'the/root/one.js'), 'the/root/one.js'); + }; + +}); +function run_test() { + runSourceMapTests('test/source-map/test-util', do_throw); +}