Resolve a relative path to the application root (#665)

Summary:
This is necessary for #643. If we’re serving `/prototype/index.html`, we
need to to use `..` to refer to the root of the site. This patch adds
`rootFromPath`, which performs the relevant transformation. (The
implementation is trivial, but figuring out exactly what the
specification should be was not!)

Test Plan:
Unit tests added; `yarn test` suffices.

wchargin-branch: rootFromPath
This commit is contained in:
William Chargin 2018-08-15 13:03:19 -07:00 committed by GitHub
parent c1997d041f
commit 621a93851c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 82 additions and 1 deletions

View File

@ -40,3 +40,26 @@ export class Assets {
return normalize(`${this._getRoot()}/${path}`);
}
}
/**
* Given an absolute path `p`, return a relative path `r` such that a
* web page at pathname `p` should use `r` to refer to the root of the
* application. The result will only contain components `.` and `..`.
*
* Examples:
*
* - "/foo/" maps to "..";
* - "/foo/bar" also maps to "..";
* - "/foo/bar/" maps to "../..";
* - "/" maps to ".".
*
* If the argument does not start with "/", an error will be thrown.
*/
export function rootFromPath(path: string) {
const normalized = normalize(path);
if (normalized[0] !== "/") {
throw new Error("expected absolute path: " + JSON.stringify(path));
}
const levels = (normalized.match(/\//g) || []).length;
return normalize(new Array(levels - 1).fill("..").join("/"));
}

View File

@ -1,6 +1,6 @@
// @flow
import {Assets} from "./assets";
import {Assets, rootFromPath} from "./assets";
describe("app/assets", () => {
describe("Assets", () => {
@ -161,4 +161,62 @@ describe("app/assets", () => {
});
});
});
describe("rootFromPath", () => {
it("throws on the empty path", () => {
expect(() => rootFromPath("")).toThrow('expected absolute path: ""');
});
it('throws on an implicitly relative path ("wat")', () => {
expect(() => rootFromPath("wat")).toThrow(
'expected absolute path: "wat"'
);
});
it('throws on an explicitly relative path ("./wat")', () => {
expect(() => rootFromPath("./wat")).toThrow(
'expected absolute path: "./wat"'
);
});
describe('returns "." for a path at root', () => {
it('with no file component ("/")', () => {
expect(rootFromPath("/")).toEqual(".");
});
it('with a file component ("/index.html")', () => {
expect(rootFromPath("/index.html")).toEqual(".");
});
it('with superfluous slashes ("///")', () => {
expect(rootFromPath("///")).toEqual(".");
});
it('with indirection, like "/foo/../"', () => {
expect(rootFromPath("/foo/../")).toEqual(".");
});
});
describe('returns ".." for a path one level deep', () => {
it('with no file component ("/foo/")', () => {
expect(rootFromPath("/foo/")).toEqual("..");
});
it('with a file component ("/foo/index.html")', () => {
expect(rootFromPath("/foo/index.html")).toEqual("..");
});
it('with superfluous slashes ("//foo//")', () => {
expect(rootFromPath("//foo//")).toEqual("..");
});
it('with indirection, like "/foo/bar/../"', () => {
expect(rootFromPath("/foo/bar/../")).toEqual("..");
});
});
describe('returns "../.." for a path two levels deep', () => {
it('with no file component ("/foo/bar/")', () => {
expect(rootFromPath("/foo/bar/")).toEqual("../..");
});
it('with a file component ("/foo/bar/index.html")', () => {
expect(rootFromPath("/foo/bar/index.html")).toEqual("../..");
});
it('with superfluous slashes ("//foo//bar//")', () => {
expect(rootFromPath("//foo//bar//")).toEqual("../..");
});
it('with indirection, like "/foo/bar/baz/../"', () => {
expect(rootFromPath("/foo/bar/baz/../")).toEqual("../..");
});
});
});
});