Skip to content

Commit

Permalink
Updates and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed Mar 18, 2024
1 parent 93c5e69 commit b250661
Show file tree
Hide file tree
Showing 3 changed files with 268 additions and 11 deletions.
247 changes: 247 additions & 0 deletions integration/vite-spa-mode-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,253 @@ test.describe("SPA Mode", () => {
expect(html.match(/window.__remixContext =/g)?.length).toBe(1);
expect(html.match(/💿 Hey developer 👋/g)?.length).toBe(1);
});

test.describe("<RemixBrowser routes> prop", () => {
test("Allows users to provide client-side-only routes via RemixBrowser", async ({
page,
}) => {
fixture = await createFixture({
compiler: "vite",
spaMode: true,
files: {
"vite.config.ts": js`
import { defineConfig } from "vite";
import { vitePlugin as remix } from "@remix-run/dev";
export default defineConfig({
plugins: [remix({
// We don't want to pick up the app/routes/_index.tsx file from
// the template and instead want to use only the src/root.tsx
// file below
appDirectory: "src",
ssr: false,
})],
});
`,
"src/root.tsx": js`
import {
Meta,
Links,
Outlet,
Routes,
Route,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function Root() {
return <Outlet />;
}
`,
"src/entry.client.tsx": js`
import { Link, RemixBrowser, Outlet, useLoaderData } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
const routes = [{
index: true,
loader() {
return "Index Loader";
},
Component() {
let data = useLoaderData();
return (
<>
<Link to="/parent/child">Go to /parent/child</Link>
<p id="index-data">{data}</p>
</>
);
},
}, {
path: '/parent',
loader() {
return "Parent Loader";
},
Component() {
let data = useLoaderData();
return (
<>
<p id="parent-data">{data}</p>
<Outlet />
</>
);
},
children: [{
path: 'child',
loader() {
return "Child Loader";
},
Component() {
let data = useLoaderData();
return <p id="child-data">{data}</p>;
},
}]
}];
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser routes={routes} />
</StrictMode>
);
});
`,
},
});
appFixture = await createAppFixture(fixture);
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/");
await page.waitForSelector("#index-data");
expect(await app.getHtml("#index-data")).toContain("Index Loader");

await app.clickLink("/parent/child");
await page.waitForSelector("#child-data");
expect(await app.getHtml("#parent-data")).toContain("Parent Loader");
expect(await app.getHtml("#child-data")).toContain("Child Loader");

let app2 = new PlaywrightFixture(appFixture, page);
await app2.goto("/parent/child");
await page.waitForSelector("#child-data");
expect(await app2.getHtml("#parent-data")).toContain("Parent Loader");
expect(await app2.getHtml("#child-data")).toContain("Child Loader");
});

test("Allows users to combine file routes with RemixBrowser routes", async ({
page,
}) => {
fixture = await createFixture({
compiler: "vite",
spaMode: true,
files: {
"vite.config.ts": js`
import { defineConfig } from "vite";
import { vitePlugin as remix } from "@remix-run/dev";
export default defineConfig({
plugins: [remix({
ssr: false,
})],
});
`,
"app/root.tsx": js`
import {
Meta,
Links,
Outlet,
Routes,
Route,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function Root() {
return <Outlet />;
}
`,
"app/entry.client.tsx": js`
import { Link, RemixBrowser, Outlet, useLoaderData } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
const routes = [{
path: '/parent',
loader() {
return "Parent Loader";
},
Component() {
let data = useLoaderData();
return (
<>
<p id="parent-data">{data}</p>
<Outlet />
</>
);
},
children: [{
path: 'child',
loader() {
return "Child Loader";
},
Component() {
let data = useLoaderData();
return <p id="child-data">{data}</p>;
},
}]
}];
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser routes={routes} />
</StrictMode>
);
});
`,
"app/routes/_index.tsx": js`
import { Link, useLoaderData } from "@remix-run/react";
export function clientLoader() {
return "Index Loader";
}
export default function Component() {
let data = useLoaderData();
return (
<>
<Link to="/parent/child">Go to /parent/child</Link>
<p id="index-data">{data}</p>
</>
);
}
`,
},
});
appFixture = await createAppFixture(fixture);
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/");
await page.waitForSelector("#index-data");
expect(await app.getHtml("#index-data")).toContain("Index Loader");

await app.clickLink("/parent/child");
await page.waitForSelector("#child-data");
expect(await app.getHtml("#parent-data")).toContain("Parent Loader");
expect(await app.getHtml("#child-data")).toContain("Child Loader");
});
});
});

test.describe("normal apps", () => {
Expand Down
27 changes: 17 additions & 10 deletions packages/remix-react/browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -260,13 +260,19 @@ export function RemixBrowser(props: RemixBrowserProps): ReactElement {
);

if (props.routes) {
let loader: LoaderFunction = () => null;
loader.hydrate = true;
let rootRoute = routes[0];
if (!rootRoute.children) {
rootRoute.children = [];
}
// If a route doesn't have a loader, add a dummy hydrating loader to stop
// rendering at that level for hydration
let hydratingLoader: LoaderFunction = () => null;
hydratingLoader.hydrate = true;
for (let route of props.routes) {
if (!route.loader) {
route = { ...route, loader };
route = { ...route, loader: hydratingLoader };
}
routes[0].children?.push(route);
rootRoute.children.push(route);
}
}

Expand Down Expand Up @@ -344,10 +350,11 @@ export function RemixBrowser(props: RemixBrowserProps): ReactElement {
: undefined,
});

let rootRoute = router.routes[0];
if (rootRoute?.children) {
rootRoute.children.forEach((route) =>
addPropRouteToManifest(route as DataRouteObject, rootRoute.id)
// Do this after creating the router so ID's have been added to the routes that we an use as keys in the manifest
if (props.routes) {
let rootDataRoute = router.routes[0];
rootDataRoute.children?.forEach((route) =>
addPropRoutesToRemix(route as DataRouteObject, rootDataRoute.id)
);
}

Expand Down Expand Up @@ -440,7 +447,7 @@ export function RemixBrowser(props: RemixBrowserProps): ReactElement {
);
}

function addPropRouteToManifest(route: DataRouteObject, parentId: string) {
function addPropRoutesToRemix(route: DataRouteObject, parentId: string) {
if (!window.__remixManifest.routes[route.id]) {
window.__remixManifest.routes[route.id] = {
index: route.index,
Expand Down Expand Up @@ -468,7 +475,7 @@ function addPropRouteToManifest(route: DataRouteObject, parentId: string) {
}
if (route.children) {
for (let child of route.children) {
addPropRouteToManifest(child, route.id);
addPropRoutesToRemix(child, route.id);
}
}
}
5 changes: 4 additions & 1 deletion packages/remix-server-runtime/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,9 +533,12 @@ async function handleDocumentRequest(
) {
let context;
try {
// When in SPA mode, always request the root, allowing us to respond for
// routes being defined and passed to <RemixBrowser routes>, which would
// otherwise 404 because no server-side routes exist
if (build.isSpaMode) {
let url = new URL(request.url);
url.pathname = "/";
url.pathname = build.basename || "/";
request = new Request(url);
}
context = await staticHandler.query(request, {
Expand Down

0 comments on commit b250661

Please sign in to comment.