Skip to content
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ import { Link } from 'waku-navigation';

```ts
export type LinkProps = {
to: InferredPaths; // type-safe, from your generated routes
to: RouteHref; // type-safe, from your generated routes
scroll?: boolean; // false keeps scroll position; otherwise browser default
unstable_prefetchOnEnter?: boolean; // prefetch on pointer enter
unstable_prefetchOnView?: boolean; // prefetch when scrolled into view
Expand Down
4 changes: 3 additions & 1 deletion e2e/01_minimal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,10 @@ test('refetch sends X-Waku-Router-Skip mapping cached element ids to etags', asy
expect(typeof parsed).toBe('object');
const entries = Object.entries(parsed as Record<string, unknown>);
expect(entries.length).toBeGreaterThan(0);
// Each etag is either a string (dynamic slot) or waku's numeric static
// sentinel 1 (static slot); /'s slots are static, so they come back as 1.
for (const [, etag] of entries) {
expect(typeof etag).toBe('string');
expect(['string', 'number']).toContain(typeof etag);
}
});

Expand Down
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';

export default tseslint.config(
{ ignores: ['dist/', 'website/', 'examples/*/dist/'] },
{ ignores: ['dist/', 'website/', 'examples/*/dist/', '**/*.gen.ts'] },
eslint.configs.recommended,
tseslint.configs.recommended,
importPlugin.flatConfigs.recommended,
Expand Down
2 changes: 1 addition & 1 deletion examples/01_minimal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-server-dom-webpack": "^19.2.3",
"waku": "1.0.0-beta.3",
"waku": "1.0.0-beta.6",
"waku-navigation": "^0.0.1"
},
"devDependencies": {
Expand Down
8 changes: 7 additions & 1 deletion examples/01_minimal/src/pages.gen.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// deno-fmt-ignore-file
// biome-ignore format: generated types do not need formatting
// prettier-ignore
import type { PathsForPages, GetConfigResponse } from 'waku/router';
import type { PathsForPages, GetConfigResponse, SearchCodecsForPages } from 'waku/router';

// prettier-ignore
import type { getConfig as File_SlicesClock_getConfig } from './pages/_slices/clock';
Expand All @@ -17,12 +17,18 @@ type Page =
| ({ path: '/about' } & GetConfigResponse<typeof File_About_getConfig>)
| ({ path: '/' } & GetConfigResponse<typeof File_Index_getConfig>);

// prettier-ignore
type Layout =
| { path: '/' };

// prettier-ignore
declare module 'waku/router' {
interface RouteConfig {
paths: PathsForPages<Page>;
}
interface CreatePagesConfig {
pages: Page;
layouts: Layout;
}
interface SearchCodecsConfig extends SearchCodecsForPages<Page> {}
}
2 changes: 1 addition & 1 deletion examples/02_pending/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-server-dom-webpack": "^19.2.3",
"waku": "1.0.0-beta.3",
"waku": "1.0.0-beta.6",
"waku-navigation": "^0.0.1"
},
"devDependencies": {
Expand Down
8 changes: 7 additions & 1 deletion examples/02_pending/src/pages.gen.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// deno-fmt-ignore-file
// biome-ignore format: generated types do not need formatting
// prettier-ignore
import type { PathsForPages, GetConfigResponse } from 'waku/router';
import type { PathsForPages, GetConfigResponse, SearchCodecsForPages } from 'waku/router';

// prettier-ignore
import type { getConfig as File_Slow_getConfig } from './pages/slow';
Expand All @@ -11,12 +11,18 @@ type Page =
| { path: '/'; render: 'static' }
| ({ path: '/slow' } & GetConfigResponse<typeof File_Slow_getConfig>);

// prettier-ignore
type Layout =
| { path: '/' };

// prettier-ignore
declare module 'waku/router' {
interface RouteConfig {
paths: PathsForPages<Page>;
}
interface CreatePagesConfig {
pages: Page;
layouts: Layout;
}
interface SearchCodecsConfig extends SearchCodecsForPages<Page> {}
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,12 @@
"typescript-eslint": "^8.61.1",
"vite": "^8.0.16",
"vitest": "^4.1.9",
"waku": "1.0.0-beta.3",
"waku": "1.0.0-beta.6",
"waku-navigation": "link:"
},
"peerDependencies": {
"react": ">=19.0.0",
"waku": ">=1.0.0-beta.3"
"waku": ">=1.0.0-beta.6"
},
"packageManager": "pnpm@11.3.0+sha512.2c403d6594527287672b1f7056343a1f7c3634036a67ffabfcc2b3d7595d843768f8787148d1b57cf7956c90606bbd192857c363af19e96d2d0ec9ec5741d215"
}
20 changes: 10 additions & 10 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 17 additions & 20 deletions src/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
Root,
Slot,
unstable_prefetchRsc as prefetchRsc,
unstable_withEnhanceFetchFn as withEnhanceFetchFn,
unstable_registerFetchEnhancer as registerFetchEnhancer,
useElementsPromise_UNSTABLE as useElementsPromise,
useRefetch,
} from 'waku/minimal/client';
Expand All @@ -39,7 +39,7 @@ import {
unstable_ROUTE_ID as ROUTE_ID,
unstable_RouterContext as RouterContext,
unstable_SKIP_HEADER as SKIP_HEADER,
type Unstable_InferredPaths as InferredPaths,
type Unstable_RouteHref as RouteHref,
} from 'waku/router/client';

export { Slice };
Expand All @@ -48,8 +48,10 @@ type Elements = Record<string, unknown>;
type Route = { path: string; query: string; hash: string };

const NOT_FOUND_PATH = '/404';
// Mirrors waku's unexported ETAG_ID_PREFIX (router/common.js).
// Mirror waku's unexported etag constants (router/isomorphic-utils/route-path).
// STATIC_ETAG is a numeric sentinel a static slot carries instead of a string.
const ETAG_ID_PREFIX = 'ETAG:';
const STATIC_ETAG = 1;

type NavigationStatus = { pending?: boolean };
type TransitionFunction = () => void | Promise<void>;
Expand Down Expand Up @@ -82,7 +84,7 @@ export const useNavigationStatus_UNSTABLE = (): NavigationStatus =>
/** Props for {@link Link}. Mirrors `waku/router`'s `<Link>`. */
export type LinkProps = {
/** Destination, type-checked against your app's generated routes. */
to: InferredPaths;
to: RouteHref;
children: ReactNode;
/**
* Whether to scroll on navigation. `false` keeps the current scroll
Expand Down Expand Up @@ -282,7 +284,7 @@ function InnerRouter({ fallbackRoute }: { fallbackRoute: Route }) {
}, []);
const registryRef = useRef(new Map<string, NavStatusEntry>());
const staticPathSetRef = useRef(new Set<string>());
const cachedEtagsRef = useRef<Record<string, string>>({});
const cachedEtagsRef = useRef<Record<string, string | number>>({});
// Stable instance: <Slice> mutates this Set across renders.
const fetchingSlices = useMemo(() => new Set<string>(), []);
useEffect(() => {
Expand All @@ -294,13 +296,14 @@ function InnerRouter({ fallbackRoute }: { fallbackRoute: Route }) {
if (routeData && elements[IS_STATIC_ID]) {
staticPathSetRef.current.add(routeData[0]);
}
const etags: Record<string, string> = {};
const etags: Record<string, string | number> = {};
for (const [key, value] of Object.entries(elements)) {
// Skip empty (clear signal) and non-Latin1 (breaks the fetch header).
// Keep the static sentinel; for string tags drop empty (clear signal)
// and non-Latin1 (breaks the fetch header).
if (
key.startsWith(ETAG_ID_PREFIX) &&
typeof value === 'string' &&
/^[ -ÿ]+$/.test(value)
(value === STATIC_ETAG ||
(typeof value === 'string' && /^[ -ÿ]+$/.test(value)))
) {
etags[key.slice(ETAG_ID_PREFIX.length)] = value;
}
Expand All @@ -318,9 +321,9 @@ function InnerRouter({ fallbackRoute }: { fallbackRoute: Route }) {
}, []);
// Send our cached etags via X-Waku-Router-Skip so the server can skip
// re-rendering slots whose etag still matches.
const enhanceFetchWithSkip = useMemo(
useEffect(
() =>
withEnhanceFetchFn((fetchFn) => (input, init) => {
registerFetchEnhancer((fetchFn) => (input, init) => {
const headers = new Headers(init?.headers);
headers.set(SKIP_HEADER, JSON.stringify(cachedEtagsRef.current));
return fetchFn(input, { ...init, headers });
Expand Down Expand Up @@ -367,11 +370,7 @@ function InnerRouter({ fallbackRoute }: { fallbackRoute: Route }) {
const prefetchRoute = useCallback(
(next: Route) => {
if (staticPathSetRef.current.has(next.path)) return;
prefetchRsc(
encodeRoutePath(next.path),
getRscParams(next.query),
enhanceFetchWithSkip,
);
prefetchRsc(encodeRoutePath(next.path), getRscParams(next.query));
// When the build publishes it, __WAKU_ROUTER_PREFETCH__ yields the
// route's JS chunk ids to preload.
(
Expand All @@ -385,7 +384,7 @@ function InnerRouter({ fallbackRoute }: { fallbackRoute: Route }) {
preloadModule(id, { as: 'script' }),
);
},
[enhanceFetchWithSkip, getRscParams],
[getRscParams],
);
// Vite HMR (dev only): clear caches and refetch the current route when Waku's
// runtime fires __WAKU_RSC_RELOAD_LISTENERS__.
Expand Down Expand Up @@ -473,7 +472,6 @@ function InnerRouter({ fallbackRoute }: { fallbackRoute: Route }) {
await refetch(
encodeRoutePath(nextRoute.path),
getRscParams(nextRoute.query),
enhanceFetchWithSkip,
);
}
if (signal.aborted) return resolve();
Expand All @@ -484,7 +482,6 @@ function InnerRouter({ fallbackRoute }: { fallbackRoute: Route }) {
await refetch(
encodeRoutePath(NOT_FOUND_PATH),
getRscParams(''),
enhanceFetchWithSkip,
);
}
if (signal.aborted) return resolve();
Expand Down Expand Up @@ -517,7 +514,7 @@ function InnerRouter({ fallbackRoute }: { fallbackRoute: Route }) {
return () => {
window.navigation.removeEventListener('navigate', callback);
};
}, [refetch, enhanceFetchWithSkip, getRscParams, emitRouteEvent]);
}, [refetch, getRscParams, emitRouteEvent]);
// Mirror waku's INTERNAL_ServerRouter context shape; only route and
// prefetchRoute are used.
const notAvailable = (name: string) => () => {
Expand Down
Loading