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

Fog of War #9600

Merged
merged 42 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
d155a3f
POC of fog of war
brophdawg11 May 21, 2024
ab53086
Non-optimized loading for pathless/index routes
brophdawg11 May 23, 2024
7c5c7e9
Pin to RR experimental
brophdawg11 May 23, 2024
8590fe3
Fix TS errors around hasChildren
brophdawg11 May 23, 2024
ea2ef90
Sorry TS, need a build
brophdawg11 May 23, 2024
3fd0f53
First pass at remix fog of war prefetching
brophdawg11 May 29, 2024
02591eb
Move to patchRoutes approach
brophdawg11 May 31, 2024
0f191a5
Update approach to use an updater function for batching
brophdawg11 Jun 5, 2024
82578b2
Switch to data-attribute/mutation-observer approach
brophdawg11 Jun 5, 2024
bdeb2df
Update to latest RR experimental
brophdawg11 Jun 5, 2024
c4b10d0
Fix build issues
brophdawg11 Jun 5, 2024
a7ab460
Minor updates
brophdawg11 Jun 5, 2024
9c4d3f9
Update to latest RR experimental
brophdawg11 Jun 7, 2024
f32fee9
Code cleanups and use patch function
brophdawg11 Jun 7, 2024
b239db4
Start integraton tests
brophdawg11 Jun 7, 2024
e21e9aa
Add discover props and update tests
brophdawg11 Jun 10, 2024
922fd09
Cleanups
brophdawg11 Jun 10, 2024
c2a6fd7
fix TS issues
brophdawg11 Jun 10, 2024
7800ea0
Fix lint
brophdawg11 Jun 11, 2024
21befc9
Support basename
brophdawg11 Jun 11, 2024
84ac12c
Remove global opt out
brophdawg11 Jun 12, 2024
e3b9513
Update to latest RR
brophdawg11 Jun 12, 2024
2a12381
Add future flag and disable in spa mode
brophdawg11 Jun 12, 2024
8829152
Add build time error for fog of war + spa mode
brophdawg11 Jun 12, 2024
b2f9f46
Update fogOfWar singleton initialization
brophdawg11 Jun 12, 2024
6ce93be
Enable flag in E2E tests
brophdawg11 Jun 12, 2024
306af79
Listen for attribute changes
brophdawg11 Jun 12, 2024
3aaa19c
Add manifest preload back behind future flag
brophdawg11 Jun 13, 2024
3918d0a
Remove dead code for nextMatches preloading
brophdawg11 Jun 13, 2024
b638c4c
Minor updates
brophdawg11 Jun 13, 2024
ff915c9
Add tests for dynamic attribute update
brophdawg11 Jun 13, 2024
f68d251
Change discover prop to be render|none to match prefetch
brophdawg11 Jun 13, 2024
ab9b606
lint fixes
brophdawg11 Jun 13, 2024
0a5e3bc
Minor updates
brophdawg11 Jun 13, 2024
f55702c
Docs
brophdawg11 Jun 13, 2024
0dd903c
Changeset
brophdawg11 Jun 13, 2024
9569fed
Extract implementation to hook
brophdawg11 Jun 14, 2024
67ce35a
Cleanups
brophdawg11 Jun 14, 2024
5d07d72
Merge branch 'dev' into brophdawg11/fog-of-war
brophdawg11 Jun 14, 2024
a5205ce
Merge branch 'dev' into brophdawg11/fog-of-war
brophdawg11 Jun 14, 2024
ca303ee
Fix flakey tests
brophdawg11 Jun 14, 2024
287cb89
Merge branch 'dev' into brophdawg11/fog-of-war
brophdawg11 Jun 14, 2024
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
303 changes: 303 additions & 0 deletions integration/fog-of-war-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
import { test, expect } from "@playwright/test";

import {
createAppFixture,
createFixture,
js,
} from "./helpers/create-fixture.js";
import { PlaywrightFixture } from "./helpers/playwright-fixture.js";

// TODO: Add a test for dynamically changing the value of data-discover and
// ensure the mutation observer picks it up.

function getFiles() {
return {
"app/root.tsx": js`
import * as React from "react";
import { Link, Links, Meta, Outlet, Scripts } from "@remix-run/react";

export default function Root() {
let [showLink, setShowLink] = React.useState(false);
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Link to="/">Home</Link><br/>
<Link to="/a">/a</Link><br/>
<button onClick={() => setShowLink(true)}>Show Link</button>
{showLink ? <Link to="/a/b">/a/b</Link> : null}
<Outlet />
<Scripts />
</body>
</html>
);
}
`,

"app/routes/_index.tsx": js`
export default function Index() {
return <h1>Index</h1>
}
`,

"app/routes/a.tsx": js`
import { Link, Outlet, useLoaderData } from "@remix-run/react";

export function loader({ request }) {
return { message: "A LOADER" };
}

export default function Index() {
let data = useLoaderData();
return (
<>
<h1 id="a">A: {data.message}</h1>
<Link to="/a/b">/a/b</Link>
<Outlet/>
</>
)
}
`,
"app/routes/a.b.tsx": js`
import { Outlet, useLoaderData } from "@remix-run/react";

export function loader({ request }) {
return { message: "B LOADER" };
}

export default function Index() {
let data = useLoaderData();
return (
<>
<h2 id="b">B: {data.message}</h2>
<Outlet/>
</>
)
}
`,
"app/routes/a.b.c.tsx": js`
import { Outlet, useLoaderData } from "@remix-run/react";

export function loader({ request }) {
return { message: "C LOADER" };
}

export default function Index() {
let data = useLoaderData();
return <h3 id="c">C: {data.message}</h3>
}
`,
};
}

test.describe("Fog of War", () => {
let oldConsoleError: typeof console.error;

test.beforeEach(() => {
oldConsoleError = console.error;
});

test.afterEach(() => {
console.error = oldConsoleError;
});

test("loads minimal manifest on initial load", async ({ page }) => {
let fixture = await createFixture({
config: {
future: {
unstable_fogOfWar: true,
},
},
files: {
...getFiles(),
"app/entry.client.tsx": js`
import { RemixBrowser } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";

startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser discover={"click"} />
</StrictMode>
);
});
`,
},
});
let appFixture = await createAppFixture(fixture);
let app = new PlaywrightFixture(appFixture, page);
let res = await fixture.requestDocument("/");
let html = await res.text();

expect(html).toContain('"root": {');
expect(html).toContain('"routes/_index": {');
expect(html).not.toContain('"routes/a"');

// Linking to A loads A and succeeds
await app.goto("/", true);
await app.clickLink("/a");
await page.waitForSelector("#a");
expect(await app.getHtml("#a")).toBe(`<h1 id="a">A: A LOADER</h1>`);
expect(
await page.evaluate(() =>
Object.keys((window as any).__remixManifest.routes)
)
).toContain("routes/a");
});

test("prefetches initially rendered links", async ({ page }) => {
let fixture = await createFixture({
config: {
future: {
unstable_fogOfWar: true,
},
},
files: getFiles(),
});
let appFixture = await createAppFixture(fixture);
let app = new PlaywrightFixture(appFixture, page);

await app.goto("/", true);
expect(
await page.evaluate(() =>
Object.keys((window as any).__remixManifest.routes)
)
).toEqual(["root", "routes/_index", "routes/a"]);

await app.clickLink("/a");
await page.waitForSelector("#a");
expect(await app.getHtml("#a")).toBe(`<h1 id="a">A: A LOADER</h1>`);
});

test("prefetches links rendered via navigations", async ({ page }) => {
let fixture = await createFixture({
config: {
future: {
unstable_fogOfWar: true,
},
},
files: getFiles(),
});
let appFixture = await createAppFixture(fixture);
let app = new PlaywrightFixture(appFixture, page);

await app.goto("/", true);
expect(
await page.evaluate(() =>
Object.keys((window as any).__remixManifest.routes)
)
).toEqual(["root", "routes/_index", "routes/a"]);

await app.clickLink("/a");
await page.waitForSelector("#a");

await page.waitForFunction(
() => (window as any).__remixManifest.routes["routes/a.b"]
);

expect(
await page.evaluate(() =>
Object.keys((window as any).__remixManifest.routes)
)
).toEqual(["root", "routes/_index", "routes/a", "routes/a.b"]);
});

test("prefetches links rendered via in-page stateful updates", async ({
page,
}) => {
let fixture = await createFixture({
config: {
future: {
unstable_fogOfWar: true,
},
},
files: getFiles(),
});
let appFixture = await createAppFixture(fixture);
let app = new PlaywrightFixture(appFixture, page);

await app.goto("/", true);
expect(
await page.evaluate(() =>
Object.keys((window as any).__remixManifest.routes)
)
).toEqual(["root", "routes/_index", "routes/a"]);

await app.clickElement("button");
await page.waitForFunction(
() => (window as any).__remixManifest.routes["routes/a.b"]
);

expect(
await page.evaluate(() =>
Object.keys((window as any).__remixManifest.routes)
)
).toEqual(["root", "routes/_index", "routes/a", "routes/a.b"]);
});

test('does not prefetch links with discover="click"', async ({ page }) => {
let fixture = await createFixture({
config: {
future: {
unstable_fogOfWar: true,
},
},
files: {
...getFiles(),
"app/routes/a.tsx": js`
import { Link, Outlet, useLoaderData } from "@remix-run/react";

export function loader({ request }) {
return { message: "A LOADER" };
}

export default function Index() {
let data = useLoaderData();
return (
<>
<h1 id="a">A: {data.message}</h1>
<Link to="/a/b" discover="click">/a/b</Link>
<Outlet/>
</>
)
}
`,
},
});
let appFixture = await createAppFixture(fixture);
let app = new PlaywrightFixture(appFixture, page);

await app.goto("/", true);
expect(
await page.evaluate(() =>
Object.keys((window as any).__remixManifest.routes)
)
).toEqual(["root", "routes/_index", "routes/a"]);

await app.clickLink("/a");
await page.waitForSelector("#a");
await new Promise((resolve) => setTimeout(resolve, 250));

// /a/b is not discovered yet even thought it's rendered
expect(
await page.evaluate(() =>
Object.keys((window as any).__remixManifest.routes)
)
).toEqual(["root", "routes/_index", "routes/a"]);

// /a/b gets discovered on click
await app.clickLink("/a/b");
await page.waitForSelector("#b");

expect(
await page.evaluate(() =>
Object.keys((window as any).__remixManifest.routes)
)
).toEqual(["root", "routes/_index", "routes/a", "routes/a.b"]);
});
});
2 changes: 1 addition & 1 deletion integration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"@remix-run/dev": "workspace:*",
"@remix-run/express": "workspace:*",
"@remix-run/node": "workspace:*",
"@remix-run/router": "1.16.1",
"@remix-run/router": "0.0.0-experimental-c5cae38f3",
"@remix-run/server-runtime": "workspace:*",
"@types/express": "^4.17.9",
"@vanilla-extract/css": "^1.10.0",
Expand Down
1 change: 1 addition & 0 deletions packages/remix-dev/__tests__/readConfig-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ describe("readConfig", () => {
"entryServerFile": "entry.server.tsx",
"entryServerFilePath": Any<String>,
"future": {
"unstable_fogOfWar": false,
"unstable_singleFetch": false,
"v3_fetcherPersist": false,
"v3_relativeSplatPath": false,
Expand Down
8 changes: 8 additions & 0 deletions packages/remix-dev/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ interface FutureConfig {
v3_relativeSplatPath: boolean;
v3_throwAbortReason: boolean;
unstable_singleFetch: boolean;
unstable_fogOfWar: boolean;
}

type NodeBuiltinsPolyfillOptions = Pick<
Expand Down Expand Up @@ -527,6 +528,12 @@ export async function resolveConfig(
entryServerFile = `entry.server.${serverRuntime}.tsx`;
}

if (isSpaMode && appConfig.future?.unstable_fogOfWar === true) {
throw new Error(
"You can not use `future.unstable_fogOfWar` in SPA Mode (`ssr: false`)"
);
}

let entryClientFilePath = userEntryClientFile
? path.resolve(appDirectory, userEntryClientFile)
: path.resolve(defaultsDirectory, entryClientFile);
Expand Down Expand Up @@ -602,6 +609,7 @@ export async function resolveConfig(
v3_relativeSplatPath: appConfig.future?.v3_relativeSplatPath === true,
v3_throwAbortReason: appConfig.future?.v3_throwAbortReason === true,
unstable_singleFetch: appConfig.future?.unstable_singleFetch === true,
unstable_fogOfWar: appConfig.future?.unstable_fogOfWar === true,
};

if (appConfig.future) {
Expand Down
2 changes: 1 addition & 1 deletion packages/remix-dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"@mdx-js/mdx": "^2.3.0",
"@npmcli/package-json": "^4.0.1",
"@remix-run/node": "workspace:*",
"@remix-run/router": "1.16.1",
"@remix-run/router": "0.0.0-experimental-c5cae38f3",
"@remix-run/server-runtime": "workspace:*",
"@types/mdx": "^2.0.5",
"@vanilla-extract/integration": "^6.2.0",
Expand Down
Loading
Loading