diff --git a/lib/bundle.js b/lib/bundle.js index d5578bf1..179d7d7a 100644 --- a/lib/bundle.js +++ b/lib/bundle.js @@ -95,7 +95,7 @@ function crawl (parent, key, path, pathFromRoot, indirections, inventory, $refs, */ function inventory$Ref ($refParent, $refKey, path, pathFromRoot, indirections, inventory, $refs, options) { let $ref = $refKey === null ? $refParent : $refParent[$refKey]; - let $refPath = url.resolve(path, $ref.$ref); + let $refPath = url.resolve(pathFromRoot, $ref.$ref); let pointer = $refs._resolve($refPath, pathFromRoot, options); if (pointer === null) { return; diff --git a/lib/resolve-external.js b/lib/resolve-external.js index c7238fbd..01f1ccd6 100644 --- a/lib/resolve-external.js +++ b/lib/resolve-external.js @@ -44,6 +44,7 @@ function resolveExternal (parser, options) { * @param {string} path - The full path of `obj`, possibly with a JSON Pointer in the hash * @param {$Refs} $refs * @param {$RefParserOptions} options + * @param {boolean} external - Whether `obj` was found in an external document. * * @returns {Promise[]} * Returns an array of promises. There will be one promise for each JSON reference in `obj`. @@ -51,7 +52,7 @@ function resolveExternal (parser, options) { * If any of the JSON references point to files that contain additional JSON references, * then the corresponding promise will internally reference an array of promises. */ -function crawl (obj, path, $refs, options) { +function crawl (obj, path, $refs, options, external) { let promises = []; if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj)) { @@ -59,16 +60,17 @@ function crawl (obj, path, $refs, options) { promises.push(resolve$Ref(obj, path, $refs, options)); } else { + if (external && $Ref.is$Ref(obj)) { + /* Correct the reference in the external document so we can resolve it */ + const withoutHash = url.stripHash(path); + obj.$ref = withoutHash + obj.$ref; + } + for (let key of Object.keys(obj)) { let keyPath = Pointer.join(path, key); let value = obj[key]; - if ($Ref.isExternal$Ref(value)) { - promises.push(resolve$Ref(value, keyPath, $refs, options)); - } - else { - promises = promises.concat(crawl(value, keyPath, $refs, options)); - } + promises = promises.concat(crawl(value, keyPath, $refs, options, external)); } } } @@ -94,6 +96,12 @@ async function resolve$Ref ($ref, path, $refs, options) { let resolvedPath = url.resolve(path, $ref.$ref); let withoutHash = url.stripHash(resolvedPath); + /* Correct the $ref to use a path relative to the root, so that $Refs._resolve can resolve it, + otherwise transitive relative external references will be incorrect if the second external + relative ref doesn't work relative to the root document. + */ + $ref.$ref = url.relative($refs._root$Ref.path, resolvedPath); + // Do we already have this $ref? $ref = $refs._$refs[withoutHash]; if ($ref) { @@ -107,7 +115,7 @@ async function resolve$Ref ($ref, path, $refs, options) { // Crawl the parsed value // console.log('Resolving $ref pointers in %s', withoutHash); - let promises = crawl(result, withoutHash + "#", $refs, options); + let promises = crawl(result, withoutHash + "#", $refs, options, true); return Promise.all(promises); } diff --git a/lib/util/url.js b/lib/util/url.js index 81b3a0d7..5e0a62a6 100644 --- a/lib/util/url.js +++ b/lib/util/url.js @@ -1,5 +1,7 @@ "use strict"; +const pathModule = require("path"); + let isWindows = /^win/.test(process.platform), forwardSlashPattern = /\//g, protocolPattern = /^(\w{2,}):\/\//i, @@ -269,3 +271,22 @@ exports.safePointerToPath = function safePointerToPath (pointer) { .replace(jsonPointerTilde, "~"); }); }; + +/** + * Like path.relative(from, to) but for URLs. It will return a relative + * URL if it can, otherwise an absolute URL is returned. + * @param {string} from + * @param {string} to + * @returns {string} + */ +exports.relative = function relative (from, to) { + if (!exports.isFileSystemPath(from) || !exports.isFileSystemPath(to)) { + return exports.resolve(from, to); + } + + const fromDir = pathModule.dirname(exports.stripHash(from)); + const toPath = exports.stripHash(to); + + const result = pathModule.relative(fromDir, toPath); + return result + exports.getHash(to); +};