From 1994c9aba5350c3d10b8f1f6cae79b3f4dc9c3cd Mon Sep 17 00:00:00 2001
From: Whaileee
Date: Tue, 27 Feb 2024 16:01:13 +0100
Subject: [PATCH] add changeset
---
.changeset/khaki-beers-admire.md | 6 +
.../concurrent-mode-navigations-test.tsx | 428 +++++++++---------
...ta-browser-router-legacy-formdata-test.tsx | 6 +-
.../__tests__/data-browser-router-test.tsx | 8 +-
.../data-hash-router-noslash-test.tsx | 11 +
packages/router/history.ts | 160 +------
6 files changed, 245 insertions(+), 374 deletions(-)
create mode 100644 .changeset/khaki-beers-admire.md
create mode 100644 packages/react-router-dom/__tests__/data-hash-router-noslash-test.tsx
diff --git a/.changeset/khaki-beers-admire.md b/.changeset/khaki-beers-admire.md
new file mode 100644
index 0000000000..b091900dce
--- /dev/null
+++ b/.changeset/khaki-beers-admire.md
@@ -0,0 +1,6 @@
+---
+"react-router": patch
+"react-router-dom": patch
+---
+
+HashRouter hashType implementation for backwards compatibility with project migrating from React-Router v4/v5
diff --git a/packages/react-router-dom/__tests__/concurrent-mode-navigations-test.tsx b/packages/react-router-dom/__tests__/concurrent-mode-navigations-test.tsx
index babc8222ec..00f1d7b385 100644
--- a/packages/react-router-dom/__tests__/concurrent-mode-navigations-test.tsx
+++ b/packages/react-router-dom/__tests__/concurrent-mode-navigations-test.tsx
@@ -70,219 +70,219 @@ describe("Handles concurrent mode features during navigations", () => {
};
}
- // describe("when the destination route suspends with a boundary", () => {
- // async function assertNavigation(
- // container: HTMLElement,
- // resolve: () => void,
- // resolveLazy: () => void
- // ) {
- // // Start on home
- // expect(getHtml(container)).toMatch("Home");
-
- // // Click to /about and should see Suspense boundary
- // await act(() => {
- // fireEvent.click(screen.getByText("/about"));
- // });
- // await waitFor(() => screen.getByText("Loading..."));
- // expect(getHtml(container)).toMatch("Loading...");
-
- // // Resolve the destination UI to clear the boundary
- // await act(() => resolve());
- // await waitFor(() => screen.getByText("About"));
- // expect(getHtml(container)).toMatch("About");
-
- // // Back to home
- // await act(() => {
- // fireEvent.click(screen.getByText("back"));
- // });
- // await waitFor(() => screen.getByText("Home"));
- // expect(getHtml(container)).toMatch("Home");
-
- // // Click to /lazy and should see Suspense boundary
- // await act(() => {
- // fireEvent.click(screen.getByText("/lazy"));
- // });
- // await waitFor(() => screen.getByText("Loading Lazy Component..."));
- // expect(getHtml(container)).toMatch("Loading Lazy Component...");
-
- // // Resolve the lazy component to clear the boundary
- // await act(() => resolveLazy());
- // await waitFor(() => screen.getByText("Lazy"));
- // expect(getHtml(container)).toMatch("Lazy");
- // }
-
- // // eslint-disable-next-line jest/expect-expect
- // it("MemoryRouter", async () => {
- // let { Home, About, LazyComponent, resolve, resolveLazy } =
- // getComponents();
-
- // let { container } = render(
- //
- //
- // } />
- // Loading...
}>
- //
- //
- // }
- // />
- // Loading Lazy Component...}>
- //
- //
- // }
- // />
- //
- //
- // );
-
- // await assertNavigation(container, resolve, resolveLazy);
- // });
-
- // // eslint-disable-next-line jest/expect-expect
- // it("BrowserRouter", async () => {
- // let { Home, About, LazyComponent, resolve, resolveLazy } =
- // getComponents();
-
- // let { container } = render(
- //
- //
- // } />
- // Loading...}>
- //
- //
- // }
- // />
- // Loading Lazy Component...}>
- //
- //
- // }
- // />
- //
- //
- // );
-
- // await assertNavigation(container, resolve, resolveLazy);
- // });
-
- // // eslint-disable-next-line jest/expect-expect
- // it("HashRouter", async () => {
- // let { Home, About, LazyComponent, resolve, resolveLazy } =
- // getComponents();
-
- // let { container } = render(
- //
- //
- // } />
- // Loading...}>
- //
- //
- // }
- // />
- // Loading Lazy Component...}>
- //
- //
- // }
- // />
- //
- //
- // );
-
- // await assertNavigation(container, resolve, resolveLazy);
- // });
- // // eslint-disable-next-line jest/expect-expect
- // it("HashRouter with noslash", async () => {
- // let { Home, About, LazyComponent, resolve, resolveLazy } =
- // getComponents();
-
- // let { container } = render(
- //
- //
- // } />
- // Loading...}>
- //
- //
- // }
- // />
- // Loading Lazy Component...}>
- //
- //
- // }
- // />
- //
- //
- // );
-
- // await assertNavigation(container, resolve, resolveLazy);
- // });
-
- // // eslint-disable-next-line jest/expect-expect
- // it("RouterProvider", async () => {
- // let { Home, About, LazyComponent, resolve, resolveLazy } =
- // getComponents();
-
- // let router = createMemoryRouter(
- // createRoutesFromElements(
- // <>
- // } />
- // Loading...}>
- //
- //
- // }
- // />
- // Loading Lazy Component...}>
- //
- //
- // }
- // />
- // >
- // )
- // );
- // let { container } = render(
- //
- // );
-
- // await assertNavigation(container, resolve, resolveLazy);
- // });
- // });
+ describe("when the destination route suspends with a boundary", () => {
+ async function assertNavigation(
+ container: HTMLElement,
+ resolve: () => void,
+ resolveLazy: () => void
+ ) {
+ // Start on home
+ expect(getHtml(container)).toMatch("Home");
+
+ // Click to /about and should see Suspense boundary
+ await act(() => {
+ fireEvent.click(screen.getByText("/about"));
+ });
+ await waitFor(() => screen.getByText("Loading..."));
+ expect(getHtml(container)).toMatch("Loading...");
+
+ // Resolve the destination UI to clear the boundary
+ await act(() => resolve());
+ await waitFor(() => screen.getByText("About"));
+ expect(getHtml(container)).toMatch("About");
+
+ // Back to home
+ await act(() => {
+ fireEvent.click(screen.getByText("back"));
+ });
+ await waitFor(() => screen.getByText("Home"));
+ expect(getHtml(container)).toMatch("Home");
+
+ // Click to /lazy and should see Suspense boundary
+ await act(() => {
+ fireEvent.click(screen.getByText("/lazy"));
+ });
+ await waitFor(() => screen.getByText("Loading Lazy Component..."));
+ expect(getHtml(container)).toMatch("Loading Lazy Component...");
+
+ // Resolve the lazy component to clear the boundary
+ await act(() => resolveLazy());
+ await waitFor(() => screen.getByText("Lazy"));
+ expect(getHtml(container)).toMatch("Lazy");
+ }
+
+ // eslint-disable-next-line jest/expect-expect
+ it("MemoryRouter", async () => {
+ let { Home, About, LazyComponent, resolve, resolveLazy } =
+ getComponents();
+
+ let { container } = render(
+
+
+ } />
+ Loading...}>
+
+
+ }
+ />
+ Loading Lazy Component...}>
+
+
+ }
+ />
+
+
+ );
+
+ await assertNavigation(container, resolve, resolveLazy);
+ });
+
+ // eslint-disable-next-line jest/expect-expect
+ it("BrowserRouter", async () => {
+ let { Home, About, LazyComponent, resolve, resolveLazy } =
+ getComponents();
+
+ let { container } = render(
+
+
+ } />
+ Loading...}>
+
+
+ }
+ />
+ Loading Lazy Component...}>
+
+
+ }
+ />
+
+
+ );
+
+ await assertNavigation(container, resolve, resolveLazy);
+ });
+
+ // eslint-disable-next-line jest/expect-expect
+ it("HashRouter", async () => {
+ let { Home, About, LazyComponent, resolve, resolveLazy } =
+ getComponents();
+
+ let { container } = render(
+
+
+ } />
+ Loading...}>
+
+
+ }
+ />
+ Loading Lazy Component...}>
+
+
+ }
+ />
+
+
+ );
+
+ await assertNavigation(container, resolve, resolveLazy);
+ });
+ // eslint-disable-next-line jest/expect-expect
+ it("HashRouter with noslash", async () => {
+ let { Home, About, LazyComponent, resolve, resolveLazy } =
+ getComponents();
+
+ let { container } = render(
+
+
+ } />
+ Loading...}>
+
+
+ }
+ />
+ Loading Lazy Component...}>
+
+
+ }
+ />
+
+
+ );
+
+ await assertNavigation(container, resolve, resolveLazy);
+ });
+
+ // eslint-disable-next-line jest/expect-expect
+ it("RouterProvider", async () => {
+ let { Home, About, LazyComponent, resolve, resolveLazy } =
+ getComponents();
+
+ let router = createMemoryRouter(
+ createRoutesFromElements(
+ <>
+ } />
+ Loading...}>
+
+
+ }
+ />
+ Loading Lazy Component...}>
+
+
+ }
+ />
+ >
+ )
+ );
+ let { container } = render(
+
+ );
+
+ await assertNavigation(container, resolve, resolveLazy);
+ });
+ });
describe("when the destination route suspends without a boundary", () => {
async function assertNavigation(
@@ -341,7 +341,7 @@ describe("Handles concurrent mode features during navigations", () => {
await assertNavigation(container, resolve, resolveLazy);
});
-
+
// eslint-disable-next-line jest/expect-expect
it("HashRouter with noslash", async () => {
let { Home, About, resolve, LazyComponent, resolveLazy } =
diff --git a/packages/react-router-dom/__tests__/data-browser-router-legacy-formdata-test.tsx b/packages/react-router-dom/__tests__/data-browser-router-legacy-formdata-test.tsx
index 02b9c03bb4..38274a9115 100644
--- a/packages/react-router-dom/__tests__/data-browser-router-legacy-formdata-test.tsx
+++ b/packages/react-router-dom/__tests__/data-browser-router-legacy-formdata-test.tsx
@@ -23,7 +23,7 @@ testDomRouter("", createHashRouter, (url) =>
getWindowImpl(url, true)
);
-testDomRouter("", (routes, opts) => createHashRouter(routes, {...opts, hashType: 'noslash'}), (url) =>
+testDomRouter("", (routes, opts) => createHashRouter(routes, { ...opts, hashType: 'noslash' }), (url) =>
getWindowImpl(url, true)
);
@@ -37,8 +37,8 @@ function testDomRouter(
let consoleError: jest.SpyInstance;
beforeEach(() => {
- consoleWarn = jest.spyOn(console, "warn").mockImplementation(() => {});
- consoleError = jest.spyOn(console, "error").mockImplementation(() => {});
+ consoleWarn = jest.spyOn(console, "warn").mockImplementation(() => { });
+ consoleError = jest.spyOn(console, "error").mockImplementation(() => { });
});
afterEach(() => {
diff --git a/packages/react-router-dom/__tests__/data-browser-router-test.tsx b/packages/react-router-dom/__tests__/data-browser-router-test.tsx
index 781eba3ede..55efda3a84 100644
--- a/packages/react-router-dom/__tests__/data-browser-router-test.tsx
+++ b/packages/react-router-dom/__tests__/data-browser-router-test.tsx
@@ -47,11 +47,7 @@ testDomRouter("", createBrowserRouter, (url) =>
getWindowImpl(url, false)
);
-testDomRouter("", (routes, opts) => createHashRouter(routes, {...opts, hashType: 'noslash'}), (url) =>
- getWindowImpl(url.substring(1), true)
-);
-
-function testDomRouter(
+export function testDomRouter(
name: string,
createTestRouter: typeof createBrowserRouter | typeof createHashRouter,
getWindow: (initialUrl: string, isHash?: boolean) => Window
@@ -7475,7 +7471,7 @@ function testDomRouter(
});
}
-function getWindowImpl(initialUrl: string, isHash = false): Window {
+export function getWindowImpl(initialUrl: string, isHash = false): Window {
// Need to use our own custom DOM in order to get a working history
const dom = new JSDOM(``, { url: "http://localhost/" });
dom.window.history.replaceState(null, "", (isHash ? "#" : "") + initialUrl);
diff --git a/packages/react-router-dom/__tests__/data-hash-router-noslash-test.tsx b/packages/react-router-dom/__tests__/data-hash-router-noslash-test.tsx
new file mode 100644
index 0000000000..7f0dfc1b7c
--- /dev/null
+++ b/packages/react-router-dom/__tests__/data-hash-router-noslash-test.tsx
@@ -0,0 +1,11 @@
+import { testDomRouter, getWindowImpl } from "./data-browser-router-test";
+import { createHashRouter } from "react-router-dom";
+
+
+function createRouter (routes, opts?) {
+ return opts === undefined ? createHashRouter(routes, {hashType: 'noslash'}) : createHashRouter(routes, {...opts, hashType:'noslash'})
+}
+// this test cannot be put in data-browser-router-test.tsx file, because it fails on node 16 and 18 in combination with .
+// for some reason the order and execution time of these tests matter and they are influenced by each other.
+testDomRouter("", createRouter, (url) =>
+ getWindowImpl(url.substring(1), true))
diff --git a/packages/router/history.ts b/packages/router/history.ts
index b47ce339b6..8191df8a45 100644
--- a/packages/router/history.ts
+++ b/packages/router/history.ts
@@ -425,8 +425,8 @@ export function createHashHistory(
hashType: HashType
) {
let path;
- if (hashType !== 'slash' && window.location.hash.startsWith("#/")){
- path = "/" + window.location.hash.substring(2)
+ if (hashType !== 'slash' && window.location.hash.startsWith("#/") && window.location.hash.length > 2){
+ path = "/" + window.location.hash.substring(1)
}
else {
path = window.location.hash.substring(1)
@@ -477,7 +477,7 @@ export function createHashHistory(
);
}
- return getHashBasedHistory(
+ return getUrlBasedHistory(
createHashLocation,
createHashHref,
validateHashLocation,
@@ -607,158 +607,16 @@ export interface UrlHistory extends History {}
export type UrlHistoryOptions = {
window?: Window;
v5Compat?: boolean;
+ hashType?: HashType;
};
-function getHashBasedHistory(
- getLocation: (window: Window, globalHistory: Window["history"], hashType: HashType) => Location,
- createHref: (window: Window, to: To, hashType: HashType) => string,
- validateLocation: ((location: Location, to: To) => void) | null,
- options: HashHistoryOptions = {}): HashHistory {
- let { window = document.defaultView!, v5Compat = false, hashType = 'slash' } = options;
- let globalHistory = window.history;
- let action = Action.Pop;
- let listener: Listener | null = null;
-
- let index = getIndex()!;
- // Index should only be null when we initialize. If not, it's because the
- // user called history.pushState or history.replaceState directly, in which
- // case we should log a warning as it will result in bugs.
- if (index == null) {
- index = 0;
- globalHistory.replaceState({ ...globalHistory.state, idx: index }, "");
- }
-
- function getIndex(): number {
- let state = globalHistory.state || { idx: null };
- return state.idx;
- }
-
- function handlePop() {
- action = Action.Pop;
- let nextIndex = getIndex();
- let delta = nextIndex == null ? null : nextIndex - index;
- index = nextIndex;
- if (listener) {
- listener({ action, location: history.location, delta });
- }
- }
-
- function push(to: To, state?: any) {
- action = Action.Push;
- let location = createLocation(history.location, to, state);
- if (validateLocation) validateLocation(location, to);
-
- index = getIndex() + 1;
- let historyState = getHistoryState(location, index);
- let url = history.createHref(location);
-
- // try...catch because iOS limits us to 100 pushState calls :/
- try {
- globalHistory.pushState(historyState, "", url);
- } catch (error) {
- // If the exception is because `state` can't be serialized, let that throw
- // outwards just like a replace call would so the dev knows the cause
- // https://html.spec.whatwg.org/multipage/nav-history-apis.html#shared-history-push/replace-state-steps
- // https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal
- if (error instanceof DOMException && error.name === "DataCloneError") {
- throw error;
- }
- // They are going to lose state here, but there is no real
- // way to warn them about it since the page will refresh...
- window.location.assign(url);
- }
-
- if (v5Compat && listener) {
- listener({ action, location: history.location, delta: 1 });
- }
- }
-
- function replace(to: To, state?: any) {
- action = Action.Replace;
- let location = createLocation(history.location, to, state);
- if (validateLocation) validateLocation(location, to);
-
- index = getIndex();
- let historyState = getHistoryState(location, index);
- let url = history.createHref(location);
- globalHistory.replaceState(historyState, "", url);
-
- if (v5Compat && listener) {
- listener({ action, location: history.location, delta: 0 });
- }
- }
-
- function createURL(to: To): URL {
- // window.location.origin is "null" (the literal string value) in Firefox
- // under certain conditions, notably when serving from a local HTML file
- // See https://bugzilla.mozilla.org/show_bug.cgi?id=878297
- let base =
- window.location.origin !== "null"
- ? window.location.origin
- : window.location.href;
-
- let href = typeof to === "string" ? to : createPath(to);
- // Treating this as a full URL will strip any trailing spaces so we need to
- // pre-encode them since they might be part of a matching splat param from
- // an ancestor route
- href = href.replace(/ $/, "%20");
- invariant(
- base,
- `No window.location.(origin|href) available to create URL for href: ${href}`
- );
- return new URL(href, base);
- }
-
- let history: History = {
- get action() {
- return action;
- },
- get location() {
- return getLocation(window, globalHistory, hashType);
- },
- listen(fn: Listener) {
- if (listener) {
- throw new Error("A history only accepts one active listener");
- }
- window.addEventListener(PopStateEventType, handlePop);
- listener = fn;
-
- return () => {
- window.removeEventListener(PopStateEventType, handlePop);
- listener = null;
- };
- },
- createHref(to) {
- return createHref(window, to, hashType);
- },
- createURL,
- encodeLocation(to) {
- // Encode a Location the same way window.location would
- let url = createURL(to);
- let pathname = hashType === 'slash' ? url.pathname : "/" + url.pathname.slice(1).concat();
- //let pathname = url.pathname;
- return {
- pathname: pathname,
- search: url.search,
- hash: url.hash,
- };
- },
- push,
- replace,
- go(n) {
- return globalHistory.go(n);
- },
- };
-
- return history;
- }
function getUrlBasedHistory(
- getLocation: (window: Window, globalHistory: Window["history"]) => Location,
- createHref: (window: Window, to: To) => string,
+ getLocation: (window: Window, globalHistory: Window["history"], hashType: HashType) => Location,
+ createHref: (window: Window, to: To, hashType: HashType) => string,
validateLocation: ((location: Location, to: To) => void) | null,
options: UrlHistoryOptions = {}
): UrlHistory {
- let { window = document.defaultView!, v5Compat = false } = options;
+ let { window = document.defaultView!, v5Compat = false, hashType = 'slash' } = options;
let globalHistory = window.history;
let action = Action.Pop;
let listener: Listener | null = null;
@@ -858,7 +716,7 @@ function getUrlBasedHistory(
return action;
},
get location() {
- return getLocation(window, globalHistory);
+ return getLocation(window, globalHistory, hashType);
},
listen(fn: Listener) {
if (listener) {
@@ -873,7 +731,7 @@ function getUrlBasedHistory(
};
},
createHref(to) {
- return createHref(window, to);
+ return createHref(window, to, hashType);
},
createURL,
encodeLocation(to) {