Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: basepath edge cases #483

Merged
merged 2 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/wouter/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
useIsomorphicLayoutEffect,
useEvent,
} from "./react-deps.js";
import { absolutePath, relativePath, unescape, stripQm } from "./paths.js";
import { absolutePath, relativePath, sanitizeSearch } from "./paths.js";

/*
* Router and router context. Router is a lightweight object that represents the current
Expand Down Expand Up @@ -67,7 +67,7 @@ const useLocationFromRouter = (router) => {
// it can be passed down as an element prop without any performance concerns.
// (This is achieved via `useEvent`.)
return [
unescape(relativePath(router.base, location)),
relativePath(router.base, location),
useEvent((to, navOpts) => navigate(absolutePath(to, router.base), navOpts)),
];
};
Expand All @@ -76,7 +76,7 @@ export const useLocation = () => useLocationFromRouter(useRouter());

export const useSearch = () => {
const router = useRouter();
return unescape(stripQm(router.searchHook(router)));
return sanitizeSearch(router.searchHook(router));
};

export const matchRoute = (parser, route, path, loose) => {
Expand Down
20 changes: 15 additions & 5 deletions packages/wouter/src/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,37 @@
* Transforms `path` into its relative `base` version
* If base isn't part of the path provided returns absolute path e.g. `~/app`
*/
export const relativePath = (base = "", path) =>
const _relativePath = (base, path) =>
!path.toLowerCase().indexOf(base.toLowerCase())
? path.slice(base.length) || "/"
: "~" + path;

export const absolutePath = (to, base = "") =>
to[0] === "~" ? to.slice(1) : base + to;
/**
* When basepath is `undefined` or '/' it is ignored (we assume it's empty string)
*/
const baseDefaults = (base = "") => (base === "/" ? "" : base);

export const absolutePath = (to, base) =>
to[0] === "~" ? to.slice(1) : baseDefaults(base) + to;

export const relativePath = (base = "", path) =>
_relativePath(unescape(baseDefaults(base)), unescape(path));

/*
* Removes leading question mark
*/
export const stripQm = (str) => (str[0] === "?" ? str.slice(1) : str);
const stripQm = (str) => (str[0] === "?" ? str.slice(1) : str);

/*
* decodes escape sequences such as %20
*/
export const unescape = (str) => {
const unescape = (str) => {
try {
return decodeURI(str);
} catch (_e) {
// fail-safe mode: if string can't be decoded do nothing
return str;
}
};

export const sanitizeSearch = (search) => unescape(stripQm(search));
43 changes: 43 additions & 0 deletions packages/wouter/test/use-location.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,34 @@ describe.each([
expect(result.current[0]).toBe("/шеллы");
unmount();
});

it("can accept unescaped basepaths", async () => {
const { result, unmount } = renderHook(() => useLocation(), {
wrapper: createContainer({
base: "/hello мир", // basepath is not escaped
hook: stub.hook,
}),
});

await stub.act(() => stub.navigate("/hello%20%D0%BC%D0%B8%D1%80/rel"));
expect(result.current[0]).toBe("/rel");

unmount();
});

it("can accept unescaped basepaths", async () => {
const { result, unmount } = renderHook(() => useLocation(), {
wrapper: createContainer({
base: "/hello%20%D0%BC%D0%B8%D1%80", // basepath is already escaped
hook: stub.hook,
}),
});

await stub.act(() => stub.navigate("/hello мир/rel"));
expect(result.current[0]).toBe("/rel");

unmount();
});
});

describe("`update` second parameter", () => {
Expand Down Expand Up @@ -170,5 +198,20 @@ describe.each([
expect(stub.location()).toBe("/app/dashboard");
unmount();
});

it("ignores the '/' basepath", async () => {
const { result, unmount } = renderHook(() => useLocation(), {
wrapper: createContainer({
base: "/",
hook: stub.hook,
}),
});

const update = result.current[1];

await stub.act(() => update("/dashboard"));
expect(stub.location()).toBe("/dashboard");
unmount();
});
});
});
Loading