Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions examples/file-router/components/form.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"use client";

import { useActionState } from "react";
import { useState } from "react";

import { Link } from "@lazarv/react-server/navigation";

import { createOrUpdateNote, Note } from "../actions";

export default function NoteForm({ note }: { note: Note }) {
Expand Down Expand Up @@ -42,9 +45,9 @@ export default function NoteForm({ note }: { note: Note }) {
)) ??
(state.error && <p className="error">{state.error?.toString()}</p>)}
<div className="button-group">
<a href="/forms" className="button">
<Link to="/" className="button">
Cancel
</a>
</Link>
<button type="submit" className="button primary" disabled={isPending}>
Save Note
</button>
Expand Down
8 changes: 8 additions & 0 deletions examples/file-router/pages/(error).middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { usePathname } from "@lazarv/react-server";

export default function RedirectMiddleware() {
const pathname = usePathname();
if (pathname === "/middleware-error") {
throw new Error("Error thrown in middleware");
}
}
19 changes: 19 additions & 0 deletions examples/file-router/pages/(redirect_extern).middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { redirect, usePathname } from "@lazarv/react-server";

export const priority = 200;

export default function RedirectMiddleware() {
const pathname = usePathname();
if (pathname === "/redirect-notfound") {
redirect("notexisting");
}
if (pathname === "/redirect-external") {
redirect("https://react-server.dev");
}
if (pathname.startsWith("/redirect-api-external")) {
redirect("/api-redirect");
}
if (pathname === "/redirect-about") {
redirect("/about");
}
}
3 changes: 3 additions & 0 deletions examples/file-router/pages/GET.api-redirect.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default async function GetRedirect() {
return Response.redirect("https://react-server.dev", 302);
}
4 changes: 3 additions & 1 deletion examples/file-router/pages/about.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Link } from "@lazarv/react-server/navigation";

export default function AboutPage() {
return (
<div>
<h1>About Page</h1>
<p>This is the about page of the file-based routing example.</p>
<a href="/">Go to Home Page</a>
<Link to="/">Go to Home Page</Link>
</div>
);
}
4 changes: 3 additions & 1 deletion examples/file-router/pages/auth/login.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Link } from "@lazarv/react-server/navigation";

export default function LoginPage() {
return (
<div>
Expand All @@ -15,7 +17,7 @@ export default function LoginPage() {
<br />
<button type="submit">Login</button>
</form>
<a href="/">Go to Home Page</a>
<Link to="/">Go to Home Page</Link>
</div>
);
}
22 changes: 18 additions & 4 deletions examples/file-router/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
import { Link } from "@lazarv/react-server/navigation";

export default function IndexPage() {
return (
<div>
<h1>Welcome to the File Router Example</h1>
<p>This is the home page of the file-based routing example.</p>
<a href="/about">Go to About Page</a>
<Link to="/about">Go to About Page</Link>
<br />
<a href="/auth">Go to Login Page</a>
<Link to="/auth">Go to Login Page</Link>
<br />
<a href="/forms">Go to Forms Page</a>
<Link to="/forms">Go to Forms Page</Link>
<br />
<a href="/forms-simple">Go to Simple Forms Page</a>
<Link to="/forms-simple">Go to Simple Forms Page</Link>
<br />
<Link to="/notexisting">404 Route not found</Link>
<h2>Redirect:</h2>
<Link to="/redirect-notfound">404 Route not found</Link>
<br />
<Link to="/redirect-external">External</Link>
<br />
<Link to="/redirect-api-external">External with API</Link>
<br />
<Link to="/redirect-about">Internal redirect to existing about page</Link>
<h2>Error:</h2>
<Link to="/middleware-error">Throw error in middleware</Link>
</div>
);
}
54 changes: 37 additions & 17 deletions examples/pokemon/src/lib/pokemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ export async function getAllPokemons(): Promise<Pokemon[]> {
let next = `https://pokeapi.co/api/v2/pokemon?limit=${process.env.POKEMON_LIMIT || 1000}`;
while (next) {
const response = await fetch(next);
if (!response.ok) {
throw new Error("Failed to fetch Pokemons");
}
const data = await response.json();
pokemons = pokemons.concat(data.results);
next = data.next;
Expand Down Expand Up @@ -68,6 +71,9 @@ export async function getPokemons(
export async function getPokemon(name: string): Promise<Pokemon> {
"use cache";
const pokemonData = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`);
if (!pokemonData.ok) {
throw new Error(`Failed to fetch Pokemon: ${name}`);
}
return pokemonData.json();
}

Expand All @@ -87,6 +93,9 @@ export type AbilityDetails = {
export async function getAbility(name: string): Promise<AbilityDetails> {
"use cache";
const abilityData = await fetch(`https://pokeapi.co/api/v2/ability/${name}`);
if (!abilityData.ok) {
throw new Error(`Failed to fetch Ability: ${name}`);
}
return abilityData.json();
}

Expand All @@ -101,6 +110,9 @@ export type TypeDetails = {
export async function getType(name: string): Promise<TypeDetails> {
"use cache";
const typeData = await fetch(`https://pokeapi.co/api/v2/type/${name}`);
if (!typeData.ok) {
throw new Error(`Failed to fetch Type: ${name}`);
}
return typeData.json();
}

Expand All @@ -125,29 +137,37 @@ export async function getSpecies(name: string): Promise<SpeciesDetails> {
const speciesData = await fetch(
`https://pokeapi.co/api/v2/pokemon-species/${name}`
);
if (!speciesData.ok) {
throw new Error(`Failed to fetch Species: ${name}`);
}
return speciesData.json();
}

export async function getPokemonDetails(name: string): Promise<
Omit<Pokemon, "abilities" | "types" | "species"> & {
abilities: AbilityDetails[];
types: TypeDetails[];
species: SpeciesDetails;
}
| (Omit<Pokemon, "abilities" | "types" | "species"> & {
abilities: AbilityDetails[];
types: TypeDetails[];
species: SpeciesDetails;
})
| null
> {
"use cache";
const pokemon = await getPokemon(name);
try {
const pokemon = await getPokemon(name);

if (!pokemon) {
throw new Error(`Pokemon "${name}" not found`);
}
if (!pokemon) {
throw new Error(`Pokemon "${name}" not found`);
}

const abilities = await Promise.all(
pokemon.abilities.map((ability) => getAbility(ability.ability.name))
);
const types = await Promise.all(
pokemon.types.map((type) => getType(type.type.name))
);
const species = await getSpecies(pokemon.species.name);
return { ...pokemon, abilities, types, species };
const abilities = await Promise.all(
pokemon.abilities.map((ability) => getAbility(ability.ability.name))
);
const types = await Promise.all(
pokemon.types.map((type) => getType(type.type.name))
);
const species = await getSpecies(pokemon.species.name);
return { ...pokemon, abilities, types, species };
} catch {
return null;
}
}
51 changes: 48 additions & 3 deletions packages/react-server/client/ClientProvider.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -686,8 +686,10 @@ function getFlightResponse(url, options = {}) {
try {
response = await fetch(srcString, {
...options.request,
method: options.method ?? (options.body ? "POST" : "GET"),
body: options.body,
method:
options.method ??
(options.body && options.body !== "{}" ? "POST" : "GET"),
body: options.body === "{}" ? undefined : options.body,
headers: {
...options.request?.headers,
accept: "text/x-component",
Expand All @@ -698,6 +700,13 @@ function getFlightResponse(url, options = {}) {
credentials: "include",
signal: abortController?.signal,
});

if (!response.body) {
throw new Error(
`The fetch to ${srcString} did not return a readable body.`
);
}

const { body } = response;

window.dispatchEvent(
Expand All @@ -716,18 +725,50 @@ function getFlightResponse(url, options = {}) {
return;
}

let chunks = 0;
let redirectTo = null;
const reader = body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (value) {
if (!redirectTo) {
const decodedValue = decoder.decode(value);
redirectTo = decodedValue.match(
/1:E\{"digest":"Location=(?<location>[^"]+)"/
)?.groups.location;
}

controller.enqueue(value);
chunks++;
}
if (done) {
break;
}
}

if (chunks === 0) {
throw new Error(
`The fetch to ${srcString} returned an empty body.`
);
}

controller.close();

if (redirectTo) {
const url = new URL(redirectTo, location.origin);

if (url.origin === location.origin) {
navigate(redirectTo, {
outlet: options.outlet,
external: options.outlet !== PAGE_ROOT,
push: false,
});
} else {
location.replace(redirectTo);
}
}

if (outletAbortControllers.has(options.outlet || url)) {
const abortControllers = outletAbortControllers.get(
options.outlet || url
Expand Down Expand Up @@ -759,7 +800,11 @@ function getFlightResponse(url, options = {}) {
const encoder = new TextEncoder();
await new Promise((resolve) => setTimeout(resolve, 0));

controller.enqueue(encoder.encode(`0:["$L1"]\n1:null\n`));
controller.enqueue(
encoder.encode(
`0:["$L1"]\n1:E{"digest":"${e.digest}","message":"${e.message}","env":"${e.environmentName}","stack":[],"owner":null}\n`
)
);
controller.close();
resolve();
});
Expand Down
11 changes: 10 additions & 1 deletion packages/react-server/client/ErrorBoundary.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,16 @@ function FallbackRenderComponent({

useEffect(() => {
if (redirectTo) {
navigate(redirectTo, { outlet, external: outlet !== PAGE_ROOT });
const url = new URL(redirectTo, location.origin);
if (url.origin === location.origin) {
navigate(redirectTo, {
outlet,
external: outlet !== PAGE_ROOT,
push: false,
});
} else {
location.replace(redirectTo);
}
}
}, [redirectTo, navigate, outlet]);

Expand Down
4 changes: 4 additions & 0 deletions packages/react-server/client/ReactServerComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,10 @@ function FlightComponent({
} else {
throw error;
}
} else if (
Component?.[0]?._payload?.reason?.digest?.startsWith("Location=")
) {
componentToRender = prevComponent.current;
} else {
prevComponent.current = Component;
}
Expand Down
Loading