From e38385b8db141d08b61f4ca088b398d296546b90 Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Fri, 12 Oct 2018 21:52:06 -0700 Subject: [PATCH 1/2] Use WHATWG's URL to implement all of source-map's URL operations. --- lib/util.js | 551 ++++++++++++++----------------- package.json | 5 +- test/test-source-map-consumer.js | 26 +- test/test-util.js | 173 ++++------ 4 files changed, 325 insertions(+), 430 deletions(-) diff --git a/lib/util.js b/lib/util.js index ae0897cd..000ec075 100644 --- a/lib/util.js +++ b/lib/util.js @@ -5,6 +5,19 @@ * http://opensource.org/licenses/BSD-3-Clause */ +/** + * URL is pulled from an npm package because the implementation in Firefox + * was found to be a bit buggy. For example: + * + * new URL("webpack-internal:///file/./folder/thing.js").href + * // Expected: webpack-internal:///file/folder/thing.js + * // Firefox: webpack-internal:///file/./folder/thing.js + * + * and this way we can be confident that this library will work consistently + * across any other potentially buggy platforms. + */ +const { URL } = require("whatwg-url"); + /** * This is a helper function for getting values from parameter/options * objects. @@ -26,261 +39,6 @@ function getArg(aArgs, aName, aDefaultValue) { } exports.getArg = getArg; -const urlRegexp = /^(?:([\w+\-.]+):)?\/\/(?:(\w+:\w+)@)?([\w.-]*)(?::(\d+))?(.*)$/; -const dataUrlRegexp = /^data:.+\,.+$/; - -function urlParse(aUrl) { - const match = aUrl.match(urlRegexp); - if (!match) { - return null; - } - return { - scheme: match[1], - auth: match[2], - host: match[3], - port: match[4], - path: match[5] - }; -} -exports.urlParse = urlParse; - -function urlGenerate(aParsedUrl) { - let url = ""; - if (aParsedUrl.scheme) { - url += aParsedUrl.scheme + ":"; - } - url += "//"; - if (aParsedUrl.auth) { - url += aParsedUrl.auth + "@"; - } - if (aParsedUrl.host) { - url += aParsedUrl.host; - } - if (aParsedUrl.port) { - url += ":" + aParsedUrl.port; - } - if (aParsedUrl.path) { - url += aParsedUrl.path; - } - return url; -} -exports.urlGenerate = urlGenerate; - -const MAX_CACHED_INPUTS = 32; - -/** - * Takes some function `f(input) -> result` and returns a memoized version of - * `f`. - * - * We keep at most `MAX_CACHED_INPUTS` memoized results of `f` alive. The - * memoization is a dumb-simple, linear least-recently-used cache. - */ -function lruMemoize(f) { - const cache = []; - - return function(input) { - for (let i = 0; i < cache.length; i++) { - if (cache[i].input === input) { - const temp = cache[0]; - cache[0] = cache[i]; - cache[i] = temp; - return cache[0].result; - } - } - - const result = f(input); - - cache.unshift({ - input, - result, - }); - - if (cache.length > MAX_CACHED_INPUTS) { - cache.pop(); - } - - return result; - }; -} - -/** - * Normalizes a path, or the path portion of a URL: - * - * - Replaces consecutive 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. - */ -const normalize = lruMemoize(function normalize(aPath) { - let path = aPath; - const url = urlParse(aPath); - if (url) { - if (!url.path) { - return aPath; - } - path = url.path; - } - const isAbsolute = exports.isAbsolute(path); - - // Split the path into parts between `/` characters. This is much faster than - // using `.split(/\/+/g)`. - const parts = []; - let start = 0; - let i = 0; - while (true) { - start = i; - i = path.indexOf("/", start); - if (i === -1) { - parts.push(path.slice(start)); - break; - } else { - parts.push(path.slice(start, i)); - while (i < path.length && path[i] === "/") { - i++; - } - } - } - - let up = 0; - for (i = parts.length - 1; i >= 0; i--) { - const 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 = "."; - } - const aPathUrl = urlParse(aPath); - const 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; - } - - // `join('http://', 'www.example.com')` - if (aRootUrl && !aRootUrl.host && !aRootUrl.path) { - aRootUrl.host = aPath; - return urlGenerate(aRootUrl); - } - - const joined = aPath.charAt(0) === "/" - ? aPath - : normalize(aRoot.replace(/\/+$/, "") + "/" + aPath); - - if (aRootUrl) { - aRootUrl.path = joined; - return urlGenerate(aRootUrl); - } - return joined; -} -exports.join = join; - -exports.isAbsolute = function(aPath) { - return aPath.charAt(0) === "/" || urlRegexp.test(aPath); -}; - -/** - * 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(/\/$/, ""); - - // It is possible for the path to be above the root. In this case, simply - // checking whether the root is a prefix of the path won't work. Instead, we - // need to remove components from the root one by one, until either we find - // a prefix that fits, or we run out of components to remove. - let level = 0; - while (aPath.indexOf(aRoot + "/") !== 0) { - const index = aRoot.lastIndexOf("/"); - if (index < 0) { - return aPath; - } - - // If the only part of the root that is left is the scheme (i.e. http://, - // file:///, etc.), one or more slashes (/), or simply nothing at all, we - // have exhausted all components, so the path is not relative to the root. - aRoot = aRoot.slice(0, index); - if (aRoot.match(/^([^\/]+:\/)?\/*$/)) { - return aPath; - } - - ++level; - } - - // Make sure we add a "../" for each component we removed from the root. - return Array(level + 1).join("../") + aPath.substr(aRoot.length + 1); -} -exports.relative = relative; - const supportsNullProto = (function() { const obj = Object.create(null); return !("__proto__" in obj); @@ -415,55 +173,252 @@ function parseSourceMapInput(str) { } exports.parseSourceMapInput = parseSourceMapInput; +// We use 'http' as the base here because we want URLs processed relative +// to the safe base to be treated as "special" URLs during parsing using +// the WHATWG URL parsing. This ensures that backslash normalization +// applies to the path and such. +const PROTOCOL = "http:"; +const PROTOCOL_AND_HOST = `${PROTOCOL}//host`; + /** - * Compute the URL of a source given the the source root, the source's - * URL, and the source map's URL. + * Make it easy to create small utilities that tweak a URL's path. */ -function computeSourceURL(sourceRoot, sourceURL, sourceMapURL) { - sourceURL = sourceURL || ""; - - if (sourceRoot) { - // This follows what Chrome does. - if (sourceRoot[sourceRoot.length - 1] !== "/" && sourceURL[0] !== "/") { - sourceRoot += "/"; +function createSafeHandler(cb) { + return input => { + const type = getURLType(input); + const base = buildSafeBase(input); + const url = new URL(input, base); + + cb(url); + + const result = url.toString(); + + if (type === "absolute") { + return result; + } else if (type === "scheme-relative") { + return result.slice(PROTOCOL.length); + } else if (type === "path-absolute") { + return result.slice(PROTOCOL_AND_HOST.length); } - // The spec says: - // Line 4: An optional source root, useful for relocating source - // files on a server or removing repeated values in the - // “sources” entry. This value is prepended to the individual - // entries in the “source” field. - sourceURL = sourceRoot + sourceURL; - } - // Historically, SourceMapConsumer did not take the sourceMapURL as - // a parameter. This mode is still somewhat supported, which is why - // this code block is conditional. However, it's preferable to pass - // the source map URL to SourceMapConsumer, so that this function - // can implement the source URL resolution algorithm as outlined in - // the spec. This block is basically the equivalent of: - // new URL(sourceURL, sourceMapURL).toString() - // ... except it avoids using URL, which wasn't available in the - // older releases of node still supported by this library. + // This assumes that the callback will only change + // the path, search and hash values. + return computeRelativeURL(base, result); + }; +} + +function withBase(url, base) { + return new URL(url, base).toString(); +} + +function buildUniqueSegment(prefix, str) { + let id = 0; + do { + const ident = prefix + (id++); + if (str.indexOf(ident) === -1) return ident; + } while (true); +} + +function buildSafeBase(str) { + const maxDotParts = str.split("..").length - 1; + + // If we used a segment that also existed in `str`, then we would be unable + // to compute relative paths. For example, if `segment` were just "a": // - // The spec says: - // If the sources are not absolute URLs after prepending of the - // “sourceRoot”, the sources are resolved relative to the - // SourceMap (like resolving script src in a html document). - if (sourceMapURL) { - const parsed = urlParse(sourceMapURL); - if (!parsed) { - throw new Error("sourceMapURL could not be parsed"); - } - if (parsed.path) { - // Strip the last path component, but keep the "/". - const index = parsed.path.lastIndexOf("/"); - if (index >= 0) { - parsed.path = parsed.path.substring(0, index + 1); - } - } - sourceURL = join(urlGenerate(parsed), sourceURL); + // const url = "../../a/" + // const base = buildSafeBase(url); // http://host/a/a/ + // const joined = "http://host/a/"; + // const result = relative(base, joined); + // + // Expected: "../../a/"; + // Actual: "a/" + // + const segment = buildUniqueSegment("p", str); + + let base = `${PROTOCOL_AND_HOST}/`; + for (let i = 0; i < maxDotParts; i++) { + base += `${segment}/`; + } + return base; +} + +function getURLType(url) { + try { + new URL(url); + return "absolute"; + } catch (err) {} + + if (url[0] === "/") { + if (url[1] === "/") return "scheme-relative"; + return "path-absolute"; + } + return "path-relative"; +} + +/** + * Given two URLs that are assumed to be on the same + * protocol/host/user/password build a relative URL from the + * path, params, and hash values. + * + * @param rootURL The root URL that the target will be relative to. + * @param targetURL The target that the relative URL points to. + * @return A rootURL-relative, normalized URL value. + */ +function computeRelativeURL(rootURL, targetURL) { + if (typeof rootURL === "string") rootURL = new URL(rootURL); + if (typeof targetURL === "string") targetURL = new URL(targetURL); + + const targetParts = targetURL.pathname.split("/"); + const rootParts = rootURL.pathname.split("/"); + + // If we've got a URL path ending with a "/", we remove it since we'd + // otherwise be relative to the wrong location. + if (rootParts.length > 0 && !rootParts[rootParts.length - 1]) { + rootParts.pop(); + } + + while ( + targetParts.length > 0 && + rootParts.length > 0 && + targetParts[0] === rootParts[0] + ) { + targetParts.shift(); + rootParts.shift(); + } + + const relativePath = rootParts + .map(() => "..") + .concat(targetParts) + .join("/"); + + return relativePath + targetURL.search + targetURL.hash; +} + +/** + * Given a URL, ensure that it is treated as a directory URL. + * + * @param url + * @return A normalized URL value. + */ +const ensureDirectory = createSafeHandler(url => { + url.pathname = url.pathname.replace(/\/?$/, "/"); +}); + +/** + * Given a URL, strip off any filename if one is present. + * + * @param url + * @return A normalized URL value. + */ +const trimFilename = createSafeHandler(url => { + url.href = new URL(".", url.toString()).toString(); +}); + +/** + * Normalize a given URL. + * * Convert backslashes. + * * Remove any ".." and "." segments. + * + * @param url + * @return A normalized URL value. + */ +const normalize = createSafeHandler(url => {}); +exports.normalize = normalize; + +/** + * Joins two paths/URLs. + * + * All returned URLs will be normalized. + * + * @param aRoot The root path or URL. Assumed to reference a directory. + * @param aPath The path or URL to be joined with the root. + * @return A joined and normalized URL value. + */ +function join(aRoot, aPath) { + const pathType = getURLType(aPath); + const rootType = getURLType(aRoot); + + aRoot = ensureDirectory(aRoot); + + if (pathType === "absolute") { + return withBase(aPath, undefined); + } + if (rootType === "absolute") { + return withBase(aPath, aRoot); } - return normalize(sourceURL); + if (pathType === "scheme-relative") { + return normalize(aPath); + } + if (rootType === "scheme-relative") { + return withBase(aPath, withBase(aRoot, PROTOCOL_AND_HOST)).slice(PROTOCOL.length); + } + + if (pathType === "path-absolute") { + return normalize(aPath); + } + if (rootType === "path-absolute") { + return withBase(aPath, withBase(aRoot, PROTOCOL_AND_HOST)).slice(PROTOCOL_AND_HOST.length); + } + + const base = buildSafeBase(aPath + aRoot); + const newPath = withBase(aPath, withBase(aRoot, base)); + return computeRelativeURL(base, newPath); +} +exports.join = join; + +/** + * Make a path relative to a URL or another path. If returning a + * relative URL is not possible, the original target will be returned. + * All returned URLs will be normalized. + * + * @param aRoot The root path or URL. + * @param aPath The path or URL to be made relative to aRoot. + * @return A rootURL-relative (if possible), normalized URL value. + */ +function relative(rootURL, targetURL) { + const result = relativeIfPossible(rootURL, targetURL); + + return typeof result === "string" ? result : normalize(targetURL); +} +exports.relative = relative; + +function relativeIfPossible(rootURL, targetURL) { + const urlType = getURLType(rootURL); + if (urlType !== getURLType(targetURL)) { + return null; + } + + const base = buildSafeBase(rootURL + targetURL); + const root = new URL(rootURL, base); + const target = new URL(targetURL, base); + + try { + new URL("", target.toString()); + } catch (err) { + // Bail if the URL doesn't support things being relative to it, + // For example, data: and blob: URLs. + return null; + } + + if ( + target.protocol !== root.protocol || + target.user !== root.user || + target.password !== root.password || + target.hostname !== root.hostname || + target.port !== root.port + ) { + return null; + } + + return computeRelativeURL(root, target); +} + +/** + * Compute the URL of a source given the the source root, the source's + * URL, and the source map's URL. + */ +function computeSourceURL(sourceRoot, sourceURL, sourceMapURL) { + return join(trimFilename(sourceMapURL || ""), join(sourceRoot || "", sourceURL || "")); } exports.computeSourceURL = computeSourceURL; diff --git a/package.json b/package.json index 6c177549..27c49f19 100644 --- a/package.json +++ b/package.json @@ -87,5 +87,8 @@ "nyc": { "reporter": "html" }, - "typings": "source-map" + "typings": "source-map", + "dependencies": { + "whatwg-url": "^7.0.0" + } } diff --git a/test/test-source-map-consumer.js b/test/test-source-map-consumer.js index d2d51a09..a922dc29 100644 --- a/test/test-source-map-consumer.js +++ b/test/test-source-map-consumer.js @@ -1049,26 +1049,6 @@ exports["test sourceRoot + originalPositionFor"] = async function(assert) { map.destroy(); }; -exports["test github issue #56"] = async function(assert) { - let map = new SourceMapGenerator({ - sourceRoot: "http://", - file: "www.example.com/foo.js" - }); - map.addMapping({ - original: { line: 1, column: 1 }, - generated: { line: 2, column: 2 }, - source: "www.example.com/original.js" - }); - - map = await new SourceMapConsumer(map.toString()); - - const sources = map.sources; - assert.equal(sources.length, 1); - assert.equal(sources[0], "http://www.example.com/original.js"); - - map.destroy(); -}; - // Was github issue #43, but that's no longer valid. exports["test source resolution with sourceMapURL"] = async function(assert) { let map = new SourceMapGenerator({ @@ -1108,7 +1088,7 @@ exports["test sourceRoot prepending"] = async function(assert) { const sources = map.sources; assert.equal(sources.length, 1, "Should only be one source."); - assert.equal(sources[0], "http://example.com/foo/bar/original.js", + assert.equal(sources[0], "http://example.com/original.js", "Source include the source root."); map.destroy(); @@ -1444,7 +1424,7 @@ exports["test webpack URL resolution"] = async function(assert) { const consumer = await new SourceMapConsumer(map); assert.equal(consumer.sources.length, 1); - assert.equal(consumer.sources[0], "webpack:///webpack/bootstrap 67e184f9679733298d44"); + assert.equal(consumer.sources[0], "webpack:///webpack/bootstrap%2067e184f9679733298d44"); consumer.destroy(); }; @@ -1461,7 +1441,7 @@ exports["test webpack URL resolution with sourceMapURL"] = async function(assert const consumer = await new SourceMapConsumer(map, "http://www.example.com/q.js.map"); assert.equal(consumer.sources.length, 1); - assert.equal(consumer.sources[0], "webpack:///webpack/bootstrap 67e184f9679733298d44"); + assert.equal(consumer.sources[0], "webpack:///webpack/bootstrap%2067e184f9679733298d44"); consumer.destroy(); }; diff --git a/test/test-util.js b/test/test-util.js index e665847c..509a3d9e 100644 --- a/test/test-util.js +++ b/test/test-util.js @@ -6,46 +6,6 @@ */ const libUtil = require("../lib/util"); - -exports["test urls"] = function(assert) { - const 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); - - let parsed = libUtil.urlParse("http://x-y.com/bar"); - assert.equal(parsed.scheme, "http"); - assert.equal(parsed.host, "x-y.com"); - assert.equal(parsed.path, "/bar"); - - const webpackURL = "webpack:///webpack/bootstrap 67e184f9679733298d44"; - parsed = libUtil.urlParse(webpackURL); - assert.equal(parsed.scheme, "webpack"); - assert.equal(parsed.host, ""); - assert.equal(parsed.path, "/webpack/bootstrap 67e184f9679733298d44"); - assert.equal(webpackURL, libUtil.urlGenerate(parsed)); -}; - exports["test normalize()"] = function(assert) { assert.equal(libUtil.normalize("/.."), "/"); assert.equal(libUtil.normalize("/../"), "/"); @@ -53,11 +13,12 @@ exports["test normalize()"] = function(assert) { 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("../"), "../"); + assert.equal(libUtil.normalize("../../a/"), "../../a/"); - assert.equal(libUtil.normalize("a/.."), "."); - assert.equal(libUtil.normalize("a/../../.."), "../.."); + assert.equal(libUtil.normalize("a/.."), ""); + assert.equal(libUtil.normalize("a/../../.."), "../../"); assert.equal(libUtil.normalize("/."), "/"); assert.equal(libUtil.normalize("/./"), "/"); @@ -65,42 +26,43 @@ exports["test normalize()"] = function(assert) { 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(""), ""); + 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/././."), "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//c////d/////"), "/a/b//c////d/////"); - assert.equal(libUtil.normalize(".///.././../a/b//./.."), "../../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("http://www.example.com"), "http://www.example.com"); + assert.equal(libUtil.normalize(".///.././../a/b//./.."), "a/b/"); + + 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/"); + assert.equal(libUtil.normalize("http://www.example.com/./..//a/b/c/.././d//"), "http://www.example.com//a/b/d//"); }; exports["test join()"] = function(assert) { 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//"), "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//", "//b"), "//b/"); - assert.equal(libUtil.join("a", ".."), "."); + 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", "."), "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", "http://www.example.com"), "http://www.example.com/"); assert.equal(libUtil.join("a", "data:foo,bar"), "data:foo,bar"); @@ -108,101 +70,96 @@ exports["test join()"] = function(assert) { 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(".", "b//"), "b//"); - assert.equal(libUtil.join("", ".."), ".."); - assert.equal(libUtil.join(".", ".."), ".."); + 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("", "."), ""); + 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("", "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("..", "b//"), "../b//"); - assert.equal(libUtil.join("..", ".."), "../.."); + assert.equal(libUtil.join("..", ".."), "../../"); assert.equal(libUtil.join("..", "../b"), "../../b"); - assert.equal(libUtil.join("..", "."), ".."); + 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("..", "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("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("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/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//", "//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", "."), "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", "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://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"); }; @@ -213,7 +170,7 @@ exports["test relative()"] = function(assert) { assert.equal(libUtil.relative("http://the/root", "http://the/root/one.js"), "one.js"); assert.equal(libUtil.relative("/the/root", "/the/rootone.js"), "../rootone.js"); assert.equal(libUtil.relative("http://the/root", "http://the/rootone.js"), "../rootone.js"); - assert.equal(libUtil.relative("/the/root", "/therootone.js"), "/therootone.js"); + assert.equal(libUtil.relative("/the/root", "/therootone.js"), "../../therootone.js"); assert.equal(libUtil.relative("http://the/root", "/therootone.js"), "/therootone.js"); assert.equal(libUtil.relative("", "/the/root/one.js"), "/the/root/one.js"); @@ -236,7 +193,7 @@ exports["test computeSourceURL"] = function(assert) { assert.equal(libUtil.computeSourceURL("src/", "test.js", "http://example.com"), "http://example.com/src/test.js"); assert.equal(libUtil.computeSourceURL("src", "/test.js", "http://example.com"), - "http://example.com/src/test.js"); + "http://example.com/test.js"); assert.equal(libUtil.computeSourceURL("http://mozilla.com", "src/test.js", "http://example.com"), "http://mozilla.com/src/test.js"); assert.equal(libUtil.computeSourceURL("", "test.js", "http://example.com/src/test.js.map"), @@ -247,12 +204,12 @@ exports["test computeSourceURL"] = function(assert) { assert.equal(libUtil.computeSourceURL(undefined, "src/test.js"), "src/test.js"); assert.equal(libUtil.computeSourceURL("src", "test.js"), "src/test.js"); assert.equal(libUtil.computeSourceURL("src/", "test.js"), "src/test.js"); - assert.equal(libUtil.computeSourceURL("src", "/test.js"), "src/test.js"); + assert.equal(libUtil.computeSourceURL("src", "/test.js"), "/test.js"); assert.equal(libUtil.computeSourceURL("src", "../test.js"), "test.js"); assert.equal(libUtil.computeSourceURL("src/dir", "../././../test.js"), "test.js"); // This gives different results with the old algorithm and the new // spec-compliant algorithm. assert.equal(libUtil.computeSourceURL("http://example.com/dir", "/test.js"), - "http://example.com/dir/test.js"); + "http://example.com/test.js"); }; From 778cb51e9f10b42d981110227af45af900945dc9 Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Thu, 18 Oct 2018 13:52:28 -0700 Subject: [PATCH 2/2] Preserve string-concat sources behavior for absolute-path sources. --- lib/util.js | 23 +++++++++++++++++++++++ test/test-source-map-consumer.js | 2 +- test/test-util.js | 8 +++++--- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/lib/util.js b/lib/util.js index 000ec075..4fabe16d 100644 --- a/lib/util.js +++ b/lib/util.js @@ -419,6 +419,29 @@ function relativeIfPossible(rootURL, targetURL) { * URL, and the source map's URL. */ function computeSourceURL(sourceRoot, sourceURL, sourceMapURL) { + // The source map spec states that "sourceRoot" and "sources" entries are to be appended. While + // that is a little vague, implementations have generally interpreted that as joining the + // URLs with a `/` between then, assuming the "sourceRoot" doesn't already end with one. + // For example, + // + // sourceRoot: "some-dir", + // sources: ["/some-path.js"] + // + // and + // + // sourceRoot: "some-dir/", + // sources: ["/some-path.js"] + // + // must behave as "some-dir/some-path.js". + // + // With this library's the transition to a more URL-focused implementation, that behavior is + // preserved here. To acheive that, we trim the "/" from absolute-path when a sourceRoot value + // is present in order to make the sources entries behave as if they are relative to the + // "sourceRoot", as they would have if the two strings were simply concated. + if (sourceRoot && getURLType(sourceURL) === "path-absolute") { + sourceURL = sourceURL.replace(/^\//, ""); + } + return join(trimFilename(sourceMapURL || ""), join(sourceRoot || "", sourceURL || "")); } exports.computeSourceURL = computeSourceURL; diff --git a/test/test-source-map-consumer.js b/test/test-source-map-consumer.js index a922dc29..3d86b39e 100644 --- a/test/test-source-map-consumer.js +++ b/test/test-source-map-consumer.js @@ -1088,7 +1088,7 @@ exports["test sourceRoot prepending"] = async function(assert) { const sources = map.sources; assert.equal(sources.length, 1, "Should only be one source."); - assert.equal(sources[0], "http://example.com/original.js", + assert.equal(sources[0], "http://example.com/foo/bar/original.js", "Source include the source root."); map.destroy(); diff --git a/test/test-util.js b/test/test-util.js index 509a3d9e..1e335989 100644 --- a/test/test-util.js +++ b/test/test-util.js @@ -193,23 +193,25 @@ exports["test computeSourceURL"] = function(assert) { assert.equal(libUtil.computeSourceURL("src/", "test.js", "http://example.com"), "http://example.com/src/test.js"); assert.equal(libUtil.computeSourceURL("src", "/test.js", "http://example.com"), - "http://example.com/test.js"); + "http://example.com/src/test.js"); assert.equal(libUtil.computeSourceURL("http://mozilla.com", "src/test.js", "http://example.com"), "http://mozilla.com/src/test.js"); assert.equal(libUtil.computeSourceURL("", "test.js", "http://example.com/src/test.js.map"), "http://example.com/src/test.js"); + assert.equal(libUtil.computeSourceURL("", "/test.js", "http://example.com/src/test.js.map"), + "http://example.com/test.js"); // Legacy code won't pass in the sourceMapURL. assert.equal(libUtil.computeSourceURL("", "src/test.js"), "src/test.js"); assert.equal(libUtil.computeSourceURL(undefined, "src/test.js"), "src/test.js"); assert.equal(libUtil.computeSourceURL("src", "test.js"), "src/test.js"); assert.equal(libUtil.computeSourceURL("src/", "test.js"), "src/test.js"); - assert.equal(libUtil.computeSourceURL("src", "/test.js"), "/test.js"); + assert.equal(libUtil.computeSourceURL("src", "/test.js"), "src/test.js"); assert.equal(libUtil.computeSourceURL("src", "../test.js"), "test.js"); assert.equal(libUtil.computeSourceURL("src/dir", "../././../test.js"), "test.js"); // This gives different results with the old algorithm and the new // spec-compliant algorithm. assert.equal(libUtil.computeSourceURL("http://example.com/dir", "/test.js"), - "http://example.com/test.js"); + "http://example.com/dir/test.js"); };