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

546 lines
14 KiB

4 years ago
  1. /* -*- Mode: js; js-indent-level: 2; -*- */
  2. /*
  3. * Copyright 2011 Mozilla Foundation and contributors
  4. * Licensed under the New BSD license. See LICENSE or:
  5. * http://opensource.org/licenses/BSD-3-Clause
  6. */
  7. /**
  8. * This is a helper function for getting values from parameter/options
  9. * objects.
  10. *
  11. * @param args The object we are extracting values from
  12. * @param name The name of the property we are getting.
  13. * @param defaultValue An optional value to return if the property is missing
  14. * from the object. If this is not specified and the property is missing, an
  15. * error will be thrown.
  16. */
  17. function getArg(aArgs, aName, aDefaultValue) {
  18. if (aName in aArgs) {
  19. return aArgs[aName];
  20. } else if (arguments.length === 3) {
  21. return aDefaultValue;
  22. }
  23. throw new Error('"' + aName + '" is a required argument.');
  24. }
  25. exports.getArg = getArg;
  26. const urlRegexp = /^(?:([\w+\-.]+):)?\/\/(?:(\w+:\w+)@)?([\w.-]*)(?::(\d+))?(.*)$/;
  27. const dataUrlRegexp = /^data:.+\,.+$/;
  28. function urlParse(aUrl) {
  29. const match = aUrl.match(urlRegexp);
  30. if (!match) {
  31. return null;
  32. }
  33. return {
  34. scheme: match[1],
  35. auth: match[2],
  36. host: match[3],
  37. port: match[4],
  38. path: match[5]
  39. };
  40. }
  41. exports.urlParse = urlParse;
  42. function urlGenerate(aParsedUrl) {
  43. let url = "";
  44. if (aParsedUrl.scheme) {
  45. url += aParsedUrl.scheme + ":";
  46. }
  47. url += "//";
  48. if (aParsedUrl.auth) {
  49. url += aParsedUrl.auth + "@";
  50. }
  51. if (aParsedUrl.host) {
  52. url += aParsedUrl.host;
  53. }
  54. if (aParsedUrl.port) {
  55. url += ":" + aParsedUrl.port;
  56. }
  57. if (aParsedUrl.path) {
  58. url += aParsedUrl.path;
  59. }
  60. return url;
  61. }
  62. exports.urlGenerate = urlGenerate;
  63. const MAX_CACHED_INPUTS = 32;
  64. /**
  65. * Takes some function `f(input) -> result` and returns a memoized version of
  66. * `f`.
  67. *
  68. * We keep at most `MAX_CACHED_INPUTS` memoized results of `f` alive. The
  69. * memoization is a dumb-simple, linear least-recently-used cache.
  70. */
  71. function lruMemoize(f) {
  72. const cache = [];
  73. return function(input) {
  74. for (let i = 0; i < cache.length; i++) {
  75. if (cache[i].input === input) {
  76. const temp = cache[0];
  77. cache[0] = cache[i];
  78. cache[i] = temp;
  79. return cache[0].result;
  80. }
  81. }
  82. const result = f(input);
  83. cache.unshift({
  84. input,
  85. result,
  86. });
  87. if (cache.length > MAX_CACHED_INPUTS) {
  88. cache.pop();
  89. }
  90. return result;
  91. };
  92. }
  93. /**
  94. * Normalizes a path, or the path portion of a URL:
  95. *
  96. * - Replaces consecutive slashes with one slash.
  97. * - Removes unnecessary '.' parts.
  98. * - Removes unnecessary '<dir>/..' parts.
  99. *
  100. * Based on code in the Node.js 'path' core module.
  101. *
  102. * @param aPath The path or url to normalize.
  103. */
  104. const normalize = lruMemoize(function normalize(aPath) {
  105. let path = aPath;
  106. const url = urlParse(aPath);
  107. if (url) {
  108. if (!url.path) {
  109. return aPath;
  110. }
  111. path = url.path;
  112. }
  113. const isAbsolute = exports.isAbsolute(path);
  114. // Split the path into parts between `/` characters. This is much faster than
  115. // using `.split(/\/+/g)`.
  116. const parts = [];
  117. let start = 0;
  118. let i = 0;
  119. while (true) {
  120. start = i;
  121. i = path.indexOf("/", start);
  122. if (i === -1) {
  123. parts.push(path.slice(start));
  124. break;
  125. } else {
  126. parts.push(path.slice(start, i));
  127. while (i < path.length && path[i] === "/") {
  128. i++;
  129. }
  130. }
  131. }
  132. let up = 0;
  133. for (i = parts.length - 1; i >= 0; i--) {
  134. const part = parts[i];
  135. if (part === ".") {
  136. parts.splice(i, 1);
  137. } else if (part === "..") {
  138. up++;
  139. } else if (up > 0) {
  140. if (part === "") {
  141. // The first part is blank if the path is absolute. Trying to go
  142. // above the root is a no-op. Therefore we can remove all '..' parts
  143. // directly after the root.
  144. parts.splice(i + 1, up);
  145. up = 0;
  146. } else {
  147. parts.splice(i, 2);
  148. up--;
  149. }
  150. }
  151. }
  152. path = parts.join("/");
  153. if (path === "") {
  154. path = isAbsolute ? "/" : ".";
  155. }
  156. if (url) {
  157. url.path = path;
  158. return urlGenerate(url);
  159. }
  160. return path;
  161. });
  162. exports.normalize = normalize;
  163. /**
  164. * Joins two paths/URLs.
  165. *
  166. * @param aRoot The root path or URL.
  167. * @param aPath The path or URL to be joined with the root.
  168. *
  169. * - If aPath is a URL or a data URI, aPath is returned, unless aPath is a
  170. * scheme-relative URL: Then the scheme of aRoot, if any, is prepended
  171. * first.
  172. * - Otherwise aPath is a path. If aRoot is a URL, then its path portion
  173. * is updated with the result and aRoot is returned. Otherwise the result
  174. * is returned.
  175. * - If aPath is absolute, the result is aPath.
  176. * - Otherwise the two paths are joined with a slash.
  177. * - Joining for example 'http://' and 'www.example.com' is also supported.
  178. */
  179. function join(aRoot, aPath) {
  180. if (aRoot === "") {
  181. aRoot = ".";
  182. }
  183. if (aPath === "") {
  184. aPath = ".";
  185. }
  186. const aPathUrl = urlParse(aPath);
  187. const aRootUrl = urlParse(aRoot);
  188. if (aRootUrl) {
  189. aRoot = aRootUrl.path || "/";
  190. }
  191. // `join(foo, '//www.example.org')`
  192. if (aPathUrl && !aPathUrl.scheme) {
  193. if (aRootUrl) {
  194. aPathUrl.scheme = aRootUrl.scheme;
  195. }
  196. return urlGenerate(aPathUrl);
  197. }
  198. if (aPathUrl || aPath.match(dataUrlRegexp)) {
  199. return aPath;
  200. }
  201. // `join('http://', 'www.example.com')`
  202. if (aRootUrl && !aRootUrl.host && !aRootUrl.path) {
  203. aRootUrl.host = aPath;
  204. return urlGenerate(aRootUrl);
  205. }
  206. const joined = aPath.charAt(0) === "/"
  207. ? aPath
  208. : normalize(aRoot.replace(/\/+$/, "") + "/" + aPath);
  209. if (aRootUrl) {
  210. aRootUrl.path = joined;
  211. return urlGenerate(aRootUrl);
  212. }
  213. return joined;
  214. }
  215. exports.join = join;
  216. exports.isAbsolute = function(aPath) {
  217. return aPath.charAt(0) === "/" || urlRegexp.test(aPath);
  218. };
  219. /**
  220. * Make a path relative to a URL or another path.
  221. *
  222. * @param aRoot The root path or URL.
  223. * @param aPath The path or URL to be made relative to aRoot.
  224. */
  225. function relative(aRoot, aPath) {
  226. if (aRoot === "") {
  227. aRoot = ".";
  228. }
  229. aRoot = aRoot.replace(/\/$/, "");
  230. // It is possible for the path to be above the root. In this case, simply
  231. // checking whether the root is a prefix of the path won't work. Instead, we
  232. // need to remove components from the root one by one, until either we find
  233. // a prefix that fits, or we run out of components to remove.
  234. let level = 0;
  235. while (aPath.indexOf(aRoot + "/") !== 0) {
  236. const index = aRoot.lastIndexOf("/");
  237. if (index < 0) {
  238. return aPath;
  239. }
  240. // If the only part of the root that is left is the scheme (i.e. http://,
  241. // file:///, etc.), one or more slashes (/), or simply nothing at all, we
  242. // have exhausted all components, so the path is not relative to the root.
  243. aRoot = aRoot.slice(0, index);
  244. if (aRoot.match(/^([^\/]+:\/)?\/*$/)) {
  245. return aPath;
  246. }
  247. ++level;
  248. }
  249. // Make sure we add a "../" for each component we removed from the root.
  250. return Array(level + 1).join("../") + aPath.substr(aRoot.length + 1);
  251. }
  252. exports.relative = relative;
  253. const supportsNullProto = (function() {
  254. const obj = Object.create(null);
  255. return !("__proto__" in obj);
  256. }());
  257. function identity(s) {
  258. return s;
  259. }
  260. /**
  261. * Because behavior goes wacky when you set `__proto__` on objects, we
  262. * have to prefix all the strings in our set with an arbitrary character.
  263. *
  264. * See https://github.com/mozilla/source-map/pull/31 and
  265. * https://github.com/mozilla/source-map/issues/30
  266. *
  267. * @param String aStr
  268. */
  269. function toSetString(aStr) {
  270. if (isProtoString(aStr)) {
  271. return "$" + aStr;
  272. }
  273. return aStr;
  274. }
  275. exports.toSetString = supportsNullProto ? identity : toSetString;
  276. function fromSetString(aStr) {
  277. if (isProtoString(aStr)) {
  278. return aStr.slice(1);
  279. }
  280. return aStr;
  281. }
  282. exports.fromSetString = supportsNullProto ? identity : fromSetString;
  283. function isProtoString(s) {
  284. if (!s) {
  285. return false;
  286. }
  287. const length = s.length;
  288. if (length < 9 /* "__proto__".length */) {
  289. return false;
  290. }
  291. /* eslint-disable no-multi-spaces */
  292. if (s.charCodeAt(length - 1) !== 95 /* '_' */ ||
  293. s.charCodeAt(length - 2) !== 95 /* '_' */ ||
  294. s.charCodeAt(length - 3) !== 111 /* 'o' */ ||
  295. s.charCodeAt(length - 4) !== 116 /* 't' */ ||
  296. s.charCodeAt(length - 5) !== 111 /* 'o' */ ||
  297. s.charCodeAt(length - 6) !== 114 /* 'r' */ ||
  298. s.charCodeAt(length - 7) !== 112 /* 'p' */ ||
  299. s.charCodeAt(length - 8) !== 95 /* '_' */ ||
  300. s.charCodeAt(length - 9) !== 95 /* '_' */) {
  301. return false;
  302. }
  303. /* eslint-enable no-multi-spaces */
  304. for (let i = length - 10; i >= 0; i--) {
  305. if (s.charCodeAt(i) !== 36 /* '$' */) {
  306. return false;
  307. }
  308. }
  309. return true;
  310. }
  311. /**
  312. * Comparator between two mappings where the original positions are compared.
  313. *
  314. * Optionally pass in `true` as `onlyCompareGenerated` to consider two
  315. * mappings with the same original source/line/column, but different generated
  316. * line and column the same. Useful when searching for a mapping with a
  317. * stubbed out mapping.
  318. */
  319. function compareByOriginalPositions(mappingA, mappingB, onlyCompareOriginal) {
  320. let cmp = strcmp(mappingA.source, mappingB.source);
  321. if (cmp !== 0) {
  322. return cmp;
  323. }
  324. cmp = mappingA.originalLine - mappingB.originalLine;
  325. if (cmp !== 0) {
  326. return cmp;
  327. }
  328. cmp = mappingA.originalColumn - mappingB.originalColumn;
  329. if (cmp !== 0 || onlyCompareOriginal) {
  330. return cmp;
  331. }
  332. cmp = mappingA.generatedColumn - mappingB.generatedColumn;
  333. if (cmp !== 0) {
  334. return cmp;
  335. }
  336. cmp = mappingA.generatedLine - mappingB.generatedLine;
  337. if (cmp !== 0) {
  338. return cmp;
  339. }
  340. return strcmp(mappingA.name, mappingB.name);
  341. }
  342. exports.compareByOriginalPositions = compareByOriginalPositions;
  343. /**
  344. * Comparator between two mappings with deflated source and name indices where
  345. * the generated positions are compared.
  346. *
  347. * Optionally pass in `true` as `onlyCompareGenerated` to consider two
  348. * mappings with the same generated line and column, but different
  349. * source/name/original line and column the same. Useful when searching for a
  350. * mapping with a stubbed out mapping.
  351. */
  352. function compareByGeneratedPositionsDeflated(mappingA, mappingB, onlyCompareGenerated) {
  353. let cmp = mappingA.generatedLine - mappingB.generatedLine;
  354. if (cmp !== 0) {
  355. return cmp;
  356. }
  357. cmp = mappingA.generatedColumn - mappingB.generatedColumn;
  358. if (cmp !== 0 || onlyCompareGenerated) {
  359. return cmp;
  360. }
  361. cmp = strcmp(mappingA.source, mappingB.source);
  362. if (cmp !== 0) {
  363. return cmp;
  364. }
  365. cmp = mappingA.originalLine - mappingB.originalLine;
  366. if (cmp !== 0) {
  367. return cmp;
  368. }
  369. cmp = mappingA.originalColumn - mappingB.originalColumn;
  370. if (cmp !== 0) {
  371. return cmp;
  372. }
  373. return strcmp(mappingA.name, mappingB.name);
  374. }
  375. exports.compareByGeneratedPositionsDeflated = compareByGeneratedPositionsDeflated;
  376. function strcmp(aStr1, aStr2) {
  377. if (aStr1 === aStr2) {
  378. return 0;
  379. }
  380. if (aStr1 === null) {
  381. return 1; // aStr2 !== null
  382. }
  383. if (aStr2 === null) {
  384. return -1; // aStr1 !== null
  385. }
  386. if (aStr1 > aStr2) {
  387. return 1;
  388. }
  389. return -1;
  390. }
  391. /**
  392. * Comparator between two mappings with inflated source and name strings where
  393. * the generated positions are compared.
  394. */
  395. function compareByGeneratedPositionsInflated(mappingA, mappingB) {
  396. let cmp = mappingA.generatedLine - mappingB.generatedLine;
  397. if (cmp !== 0) {
  398. return cmp;
  399. }
  400. cmp = mappingA.generatedColumn - mappingB.generatedColumn;
  401. if (cmp !== 0) {
  402. return cmp;
  403. }
  404. cmp = strcmp(mappingA.source, mappingB.source);
  405. if (cmp !== 0) {
  406. return cmp;
  407. }
  408. cmp = mappingA.originalLine - mappingB.originalLine;
  409. if (cmp !== 0) {
  410. return cmp;
  411. }
  412. cmp = mappingA.originalColumn - mappingB.originalColumn;
  413. if (cmp !== 0) {
  414. return cmp;
  415. }
  416. return strcmp(mappingA.name, mappingB.name);
  417. }
  418. exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflated;
  419. /**
  420. * Strip any JSON XSSI avoidance prefix from the string (as documented
  421. * in the source maps specification), and then parse the string as
  422. * JSON.
  423. */
  424. function parseSourceMapInput(str) {
  425. return JSON.parse(str.replace(/^\)]}'[^\n]*\n/, ""));
  426. }
  427. exports.parseSourceMapInput = parseSourceMapInput;
  428. /**
  429. * Compute the URL of a source given the the source root, the source's
  430. * URL, and the source map's URL.
  431. */
  432. function computeSourceURL(sourceRoot, sourceURL, sourceMapURL) {
  433. sourceURL = sourceURL || "";
  434. if (sourceRoot) {
  435. // This follows what Chrome does.
  436. if (sourceRoot[sourceRoot.length - 1] !== "/" && sourceURL[0] !== "/") {
  437. sourceRoot += "/";
  438. }
  439. // The spec says:
  440. // Line 4: An optional source root, useful for relocating source
  441. // files on a server or removing repeated values in the
  442. // “sources” entry. This value is prepended to the individual
  443. // entries in the “source” field.
  444. sourceURL = sourceRoot + sourceURL;
  445. }
  446. // Historically, SourceMapConsumer did not take the sourceMapURL as
  447. // a parameter. This mode is still somewhat supported, which is why
  448. // this code block is conditional. However, it's preferable to pass
  449. // the source map URL to SourceMapConsumer, so that this function
  450. // can implement the source URL resolution algorithm as outlined in
  451. // the spec. This block is basically the equivalent of:
  452. // new URL(sourceURL, sourceMapURL).toString()
  453. // ... except it avoids using URL, which wasn't available in the
  454. // older releases of node still supported by this library.
  455. //
  456. // The spec says:
  457. // If the sources are not absolute URLs after prepending of the
  458. // “sourceRoot”, the sources are resolved relative to the
  459. // SourceMap (like resolving script src in a html document).
  460. if (sourceMapURL) {
  461. const parsed = urlParse(sourceMapURL);
  462. if (!parsed) {
  463. throw new Error("sourceMapURL could not be parsed");
  464. }
  465. if (parsed.path) {
  466. // Strip the last path component, but keep the "/".
  467. const index = parsed.path.lastIndexOf("/");
  468. if (index >= 0) {
  469. parsed.path = parsed.path.substring(0, index + 1);
  470. }
  471. }
  472. sourceURL = join(urlGenerate(parsed), sourceURL);
  473. }
  474. return normalize(sourceURL);
  475. }
  476. exports.computeSourceURL = computeSourceURL;