From 2727f15d0b8d95a980a31614e5dcebd95b7f6929 Mon Sep 17 00:00:00 2001 From: daishi Date: Mon, 29 Jun 2026 09:42:12 +0900 Subject: [PATCH] update waku beta.6 --- README.md | 2 +- e2e/01_minimal.spec.ts | 4 ++- eslint.config.js | 2 +- examples/01_minimal/package.json | 2 +- examples/01_minimal/src/pages.gen.ts | 8 +++++- examples/02_pending/package.json | 2 +- examples/02_pending/src/pages.gen.ts | 8 +++++- package.json | 4 +-- pnpm-lock.yaml | 20 +++++++-------- src/client.tsx | 37 +++++++++++++--------------- 10 files changed, 50 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 6052ffa..2011088 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/e2e/01_minimal.spec.ts b/e2e/01_minimal.spec.ts index 642b49a..1a5d718 100644 --- a/e2e/01_minimal.spec.ts +++ b/e2e/01_minimal.spec.ts @@ -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); 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); } }); diff --git a/eslint.config.js b/eslint.config.js index 731475f..0a6eb53 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -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, diff --git a/examples/01_minimal/package.json b/examples/01_minimal/package.json index 6720187..8c16076 100644 --- a/examples/01_minimal/package.json +++ b/examples/01_minimal/package.json @@ -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": { diff --git a/examples/01_minimal/src/pages.gen.ts b/examples/01_minimal/src/pages.gen.ts index 89359ba..973c45d 100644 --- a/examples/01_minimal/src/pages.gen.ts +++ b/examples/01_minimal/src/pages.gen.ts @@ -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'; @@ -17,6 +17,10 @@ type Page = | ({ path: '/about' } & GetConfigResponse) | ({ path: '/' } & GetConfigResponse); +// prettier-ignore +type Layout = +| { path: '/' }; + // prettier-ignore declare module 'waku/router' { interface RouteConfig { @@ -24,5 +28,7 @@ declare module 'waku/router' { } interface CreatePagesConfig { pages: Page; + layouts: Layout; } + interface SearchCodecsConfig extends SearchCodecsForPages {} } diff --git a/examples/02_pending/package.json b/examples/02_pending/package.json index 1976226..4159702 100644 --- a/examples/02_pending/package.json +++ b/examples/02_pending/package.json @@ -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": { diff --git a/examples/02_pending/src/pages.gen.ts b/examples/02_pending/src/pages.gen.ts index 76b0911..aae6932 100644 --- a/examples/02_pending/src/pages.gen.ts +++ b/examples/02_pending/src/pages.gen.ts @@ -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'; @@ -11,6 +11,10 @@ type Page = | { path: '/'; render: 'static' } | ({ path: '/slow' } & GetConfigResponse); +// prettier-ignore +type Layout = +| { path: '/' }; + // prettier-ignore declare module 'waku/router' { interface RouteConfig { @@ -18,5 +22,7 @@ declare module 'waku/router' { } interface CreatePagesConfig { pages: Page; + layouts: Layout; } + interface SearchCodecsConfig extends SearchCodecsForPages {} } diff --git a/package.json b/package.json index 1247117..f5b61eb 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2824fc..c823c22 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,8 +84,8 @@ importers: specifier: ^4.1.9 version: 4.1.9(@types/node@25.9.3)(happy-dom@20.10.4)(vite@8.0.16(@types/node@25.9.3)(terser@5.48.0)) waku: - specifier: 1.0.0-beta.3 - version: 1.0.0-beta.3(@types/node@25.9.3)(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.104.1))(react@19.2.7)(terser@5.48.0) + specifier: 1.0.0-beta.6 + version: 1.0.0-beta.6(@types/node@25.9.3)(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.104.1))(react@19.2.7)(terser@5.48.0) waku-navigation: specifier: 'link:' version: 'link:' @@ -213,8 +213,8 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@hono/node-server@2.0.5': - resolution: {integrity: sha512-yQFvDmyDo3y6rEOJZDUYPJ49DIKTPpIk4kGvm40xx4Ejne0Pu9a1+exxPN+C1UppWK/WGZX9F++/Xs231tE86g==} + '@hono/node-server@2.0.6': + resolution: {integrity: sha512-7DeRlKG57JDBNZ5Qj2jwVdgwQy4b0tLubRLl3zCf91/rCf9i7p1V5FtW/yWibm1uUHE493ts9ZXH/7g/LQWl+g==} engines: {node: '>=20'} peerDependencies: hono: ^4 @@ -2261,9 +2261,9 @@ packages: jsdom: optional: true - waku@1.0.0-beta.3: - resolution: {integrity: sha512-j8la6SzYX/pqqnjT/FTsr8de+jCwP4rthyV7IbRm/qEwECbUS0kXcG2VLs5TDGCumLnzq9uRl4iwocxG3+U8rQ==} - engines: {node: ^26.0.0 || ^24.0.0 || ^22.13.0} + waku@1.0.0-beta.6: + resolution: {integrity: sha512-IM5RCLgq9pvImyAu1clH3zAGAU3h5acgMcXbzUjnOihfAKAX71j68+bcJS2T3Zr4Am3oeRFSq5i689kT+zf/3w==} + engines: {node: ^26.0.0 || ^24.0.0 || ^22.15.0} hasBin: true peerDependencies: react: ~19.2.4 @@ -2518,7 +2518,7 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 - '@hono/node-server@2.0.5(hono@4.12.25)': + '@hono/node-server@2.0.6(hono@4.12.25)': dependencies: hono: 4.12.25 @@ -4657,9 +4657,9 @@ snapshots: transitivePeerDependencies: - msw - waku@1.0.0-beta.3(@types/node@25.9.3)(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.104.1))(react@19.2.7)(terser@5.48.0): + waku@1.0.0-beta.6(@types/node@25.9.3)(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.104.1))(react@19.2.7)(terser@5.48.0): dependencies: - '@hono/node-server': 2.0.5(hono@4.12.25) + '@hono/node-server': 2.0.6(hono@4.12.25) '@vitejs/plugin-react': 6.0.2(vite@8.0.16(@types/node@25.9.3)(terser@5.48.0)) '@vitejs/plugin-rsc': 0.5.27(react-dom@19.2.7(react@19.2.7))(react-server-dom-webpack@19.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(webpack@5.104.1))(react@19.2.7)(vite@8.0.16(@types/node@25.9.3)(terser@5.48.0)) dotenv: 17.4.2 diff --git a/src/client.tsx b/src/client.tsx index dd3c085..a37793f 100644 --- a/src/client.tsx +++ b/src/client.tsx @@ -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'; @@ -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 }; @@ -48,8 +48,10 @@ type Elements = Record; 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; @@ -82,7 +84,7 @@ export const useNavigationStatus_UNSTABLE = (): NavigationStatus => /** Props for {@link Link}. Mirrors `waku/router`'s ``. */ 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 @@ -282,7 +284,7 @@ function InnerRouter({ fallbackRoute }: { fallbackRoute: Route }) { }, []); const registryRef = useRef(new Map()); const staticPathSetRef = useRef(new Set()); - const cachedEtagsRef = useRef>({}); + const cachedEtagsRef = useRef>({}); // Stable instance: mutates this Set across renders. const fetchingSlices = useMemo(() => new Set(), []); useEffect(() => { @@ -294,13 +296,14 @@ function InnerRouter({ fallbackRoute }: { fallbackRoute: Route }) { if (routeData && elements[IS_STATIC_ID]) { staticPathSetRef.current.add(routeData[0]); } - const etags: Record = {}; + const etags: Record = {}; 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; } @@ -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 }); @@ -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. ( @@ -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__. @@ -473,7 +472,6 @@ function InnerRouter({ fallbackRoute }: { fallbackRoute: Route }) { await refetch( encodeRoutePath(nextRoute.path), getRscParams(nextRoute.query), - enhanceFetchWithSkip, ); } if (signal.aborted) return resolve(); @@ -484,7 +482,6 @@ function InnerRouter({ fallbackRoute }: { fallbackRoute: Route }) { await refetch( encodeRoutePath(NOT_FOUND_PATH), getRscParams(''), - enhanceFetchWithSkip, ); } if (signal.aborted) return resolve(); @@ -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) => () => {