Add an `Assets` resolver (#664)
Summary: This will enable clients to obtain the path to a static asset, even when the app is not hosted at the root of a server, as outlined in #643. This module will be used for simple assets (images, etc.) and API data (fetches from `/api/**`) alike. This supersedes #663. It includes the logic from that PR (`Assets`) without the React-specific context bindings (`AssetsContext`). Test Plan: Unit tests included; `yarn test` suffices. wchargin-branch: assets-resolver
This commit is contained in:
parent
ad0e98ac2c
commit
c1997d041f
|
@ -0,0 +1,42 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import normalize from "../util/pathNormalize";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolver for static assets (e.g., images, PDFs) and API data (e.g.,
|
||||||
|
* the repository registry, plugin data). Any references to resources
|
||||||
|
* should be resolved through this API.
|
||||||
|
*/
|
||||||
|
export class Assets {
|
||||||
|
+_root: ?string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a resolver given a path to the root of the site. This can
|
||||||
|
* be a relative path, like `../..`, or an absolute path, like `/`.
|
||||||
|
*/
|
||||||
|
constructor(root: ?string) {
|
||||||
|
this._root = root == null ? root : normalize(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getRoot(): string {
|
||||||
|
if (this._root == null) {
|
||||||
|
throw new Error("asset root path uninitialized");
|
||||||
|
}
|
||||||
|
return this._root;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a path as if the current directory and "/" both represent
|
||||||
|
* the site root. For instance, "foo", "/foo", and "./foo" all
|
||||||
|
* represent the same file. It is an error to specify a file that is
|
||||||
|
* above the root, like "../bad".
|
||||||
|
*/
|
||||||
|
resolve(path: string) {
|
||||||
|
if (normalize(path.replace(/^\/+/, "")).startsWith("..")) {
|
||||||
|
// It doesn't make sense to traverse past the site's root. This is
|
||||||
|
// likely an error in the caller.
|
||||||
|
throw new Error("path outside site root: " + path);
|
||||||
|
}
|
||||||
|
return normalize(`${this._getRoot()}/${path}`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,164 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import {Assets} from "./assets";
|
||||||
|
|
||||||
|
describe("app/assets", () => {
|
||||||
|
describe("Assets", () => {
|
||||||
|
describe("with an unknown root path (null)", () => {
|
||||||
|
it("can be constructed", () => {
|
||||||
|
const _: Assets = new Assets(null);
|
||||||
|
});
|
||||||
|
it("fails to resolve anything", () => {
|
||||||
|
for (const x of ["", ".", "foo.png", "/foo.png", "/foo/bar/"]) {
|
||||||
|
expect(() => new Assets(null).resolve(x)).toThrowError(
|
||||||
|
"asset root path uninitialized"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("with an empty root path", () => {
|
||||||
|
it("can be constructed", () => {
|
||||||
|
const _: Assets = new Assets("");
|
||||||
|
});
|
||||||
|
it('resolves the root path itself using "."', () => {
|
||||||
|
const assets = new Assets("");
|
||||||
|
expect(assets.resolve(".")).toEqual(".");
|
||||||
|
});
|
||||||
|
it('resolves the root directory using "./"', () => {
|
||||||
|
const assets = new Assets("");
|
||||||
|
expect(assets.resolve("./")).toEqual("./");
|
||||||
|
});
|
||||||
|
it('resolves the root directory using ""', () => {
|
||||||
|
const assets = new Assets("");
|
||||||
|
expect(assets.resolve("")).toEqual("./");
|
||||||
|
});
|
||||||
|
it('resolves an implicitly relative filename ("favicon.png")', () => {
|
||||||
|
const assets = new Assets("");
|
||||||
|
expect(assets.resolve("favicon.png")).toEqual("favicon.png");
|
||||||
|
});
|
||||||
|
it('resolves an explicitly relative filename ("./favicon.png")', () => {
|
||||||
|
const assets = new Assets("");
|
||||||
|
expect(assets.resolve("./favicon.png")).toEqual("favicon.png");
|
||||||
|
});
|
||||||
|
it('resolves a file by absolute filename ("/favicon.png")', () => {
|
||||||
|
const assets = new Assets("");
|
||||||
|
expect(assets.resolve("/favicon.png")).toEqual("favicon.png");
|
||||||
|
});
|
||||||
|
it("errors when given an implicitly relative path above root", () => {
|
||||||
|
const assets = new Assets("");
|
||||||
|
expect(() => assets.resolve("../foo")).toThrow(
|
||||||
|
"path outside site root: ../foo"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("errors when given an explicitly relative path above root", () => {
|
||||||
|
const assets = new Assets("");
|
||||||
|
expect(() => assets.resolve("./../foo")).toThrow(
|
||||||
|
"path outside site root: ./../foo"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("errors when given an absolute path above root", () => {
|
||||||
|
const assets = new Assets("");
|
||||||
|
expect(() => assets.resolve("/../foo")).toThrow(
|
||||||
|
"path outside site root: /../foo"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with a relative root path ("../..")', () => {
|
||||||
|
it("can be constructed", () => {
|
||||||
|
const _: Assets = new Assets("../..");
|
||||||
|
});
|
||||||
|
it('resolves the root path itself using "."', () => {
|
||||||
|
const assets = new Assets("../..");
|
||||||
|
expect(assets.resolve(".")).toEqual("../..");
|
||||||
|
});
|
||||||
|
it('resolves the root directory using "./"', () => {
|
||||||
|
const assets = new Assets("../..");
|
||||||
|
expect(assets.resolve("./")).toEqual("../../");
|
||||||
|
});
|
||||||
|
it('resolves the root directory using ""', () => {
|
||||||
|
const assets = new Assets("../..");
|
||||||
|
expect(assets.resolve("")).toEqual("../../");
|
||||||
|
});
|
||||||
|
it('resolves an implicitly relative filename ("favicon.png")', () => {
|
||||||
|
const assets = new Assets("../..");
|
||||||
|
expect(assets.resolve("favicon.png")).toEqual("../../favicon.png");
|
||||||
|
});
|
||||||
|
it('resolves an explicitly relative filename ("./favicon.png")', () => {
|
||||||
|
const assets = new Assets("../..");
|
||||||
|
expect(assets.resolve("./favicon.png")).toEqual("../../favicon.png");
|
||||||
|
});
|
||||||
|
it('resolves a file by absolute filename ("/favicon.png")', () => {
|
||||||
|
const assets = new Assets("../..");
|
||||||
|
expect(assets.resolve("/favicon.png")).toEqual("../../favicon.png");
|
||||||
|
});
|
||||||
|
it("errors when given an implicitly relative path above root", () => {
|
||||||
|
const assets = new Assets("../..");
|
||||||
|
expect(() => assets.resolve("../foo")).toThrow(
|
||||||
|
"path outside site root: ../foo"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("errors when given an explicitly relative path above root", () => {
|
||||||
|
const assets = new Assets("../..");
|
||||||
|
expect(() => assets.resolve("./../foo")).toThrow(
|
||||||
|
"path outside site root: ./../foo"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("errors when given an absolute path above root", () => {
|
||||||
|
const assets = new Assets("../..");
|
||||||
|
expect(() => assets.resolve("/../foo")).toThrow(
|
||||||
|
"path outside site root: /../foo"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with an absolute root path ("/ab/cd/")', () => {
|
||||||
|
it("can be constructed", () => {
|
||||||
|
const _: Assets = new Assets("/ab/cd/");
|
||||||
|
});
|
||||||
|
it('resolves the root path itself using "."', () => {
|
||||||
|
const assets = new Assets("/ab/cd/");
|
||||||
|
expect(assets.resolve(".")).toEqual("/ab/cd");
|
||||||
|
});
|
||||||
|
it('resolves the root directory using "./"', () => {
|
||||||
|
const assets = new Assets("/ab/cd/");
|
||||||
|
expect(assets.resolve("./")).toEqual("/ab/cd/");
|
||||||
|
});
|
||||||
|
it('resolves the root directory using ""', () => {
|
||||||
|
const assets = new Assets("/ab/cd/");
|
||||||
|
expect(assets.resolve("")).toEqual("/ab/cd/");
|
||||||
|
});
|
||||||
|
it('resolves an implicitly relative filename ("favicon.png")', () => {
|
||||||
|
const assets = new Assets("/ab/cd/");
|
||||||
|
expect(assets.resolve("favicon.png")).toEqual("/ab/cd/favicon.png");
|
||||||
|
});
|
||||||
|
it('resolves an explicitly relative filename ("./favicon.png")', () => {
|
||||||
|
const assets = new Assets("/ab/cd/");
|
||||||
|
expect(assets.resolve("./favicon.png")).toEqual("/ab/cd/favicon.png");
|
||||||
|
});
|
||||||
|
it('resolves a file by absolute filename ("/favicon.png")', () => {
|
||||||
|
const assets = new Assets("/ab/cd/");
|
||||||
|
expect(assets.resolve("/favicon.png")).toEqual("/ab/cd/favicon.png");
|
||||||
|
});
|
||||||
|
it("errors when given an implicitly relative path above root", () => {
|
||||||
|
const assets = new Assets("/ab/cd/");
|
||||||
|
expect(() => assets.resolve("../foo")).toThrow(
|
||||||
|
"path outside site root: ../foo"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("errors when given an explicitly relative path above root", () => {
|
||||||
|
const assets = new Assets("/ab/cd/");
|
||||||
|
expect(() => assets.resolve("./../foo")).toThrow(
|
||||||
|
"path outside site root: ./../foo"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("errors when given an absolute path above root", () => {
|
||||||
|
const assets = new Assets("/ab/cd/");
|
||||||
|
expect(() => assets.resolve("/../foo")).toThrow(
|
||||||
|
"path outside site root: /../foo"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue