;
-};
+const ShoudSkipComponent = ({ shouldSkip }: { shouldSkip: ShouldSkip }) =>
+ createElement('meta', {
+ name: 'waku-should-skip',
+ content: JSON.stringify(shouldSkip),
+ });
export function defineRouter(
- getRoutePaths: () => Promise,
+ existsPath: (path: string) => Promise<'static' | 'dynamic' | null>,
getComponent: (
- componentId: string,
+ componentId: string, // "**/layout" or "**/page"
+ unstable_setShouldSkip: (val?: ShouldSkip[string]) => void,
) => Promise | { default: FunctionComponent } | null>,
+ getPathsForBuild?: () => Promise<
+ Iterable<{ path: string; searchParams?: URLSearchParams }>
+ >,
): ReturnType {
- const routePathsPromise = getRoutePaths();
- const existsRoutePathPromise = routePathsPromise.then((routePaths) => {
- const staticPathSet = new Set();
- for (const path of routePaths.static || []) {
- staticPathSet.add(path);
- }
- const existsRoutePath = async (path: string) => {
- if (staticPathSet.has(path)) {
- return true;
- }
- if (await routePaths.dynamic?.(path)) {
- return true;
- }
- return false;
- };
- return existsRoutePath;
- });
+ const shouldSkip: ShouldSkip = {};
const renderEntries: RenderEntries = async (input) => {
const { path, searchParams, skip } = parseInputString(input);
- const existsRoutePath = await existsRoutePathPromise;
- if (!(await existsRoutePath(path))) {
+ if (!(await existsPath(path))) {
return null;
}
const componentIds = getComponentIds(path);
@@ -67,7 +46,13 @@ export function defineRouter(
if (skip?.includes(id)) {
return [];
}
- const mod = await getComponent(id);
+ const mod = await getComponent(id, (val) => {
+ if (val) {
+ shouldSkip[id] = val;
+ } else {
+ delete shouldSkip[id];
+ }
+ });
const component =
typeof mod === 'function' ? mod : mod?.default || Default;
const element = createElement(
@@ -79,24 +64,39 @@ export function defineRouter
(
}),
)
).flat();
+ entries.push([
+ SHOULD_SKIP_ID,
+ createElement(ShoudSkipComponent, { shouldSkip }) as any,
+ ]);
return Object.fromEntries(entries);
};
const getBuildConfig: GetBuildConfig = async (
unstable_collectClientModules,
) => {
- const routePaths = await routePathsPromise;
+ const pathsForBuild = await getPathsForBuild?.();
+ const pathMap = new Map<
+ string,
+ { isStatic: boolean; searchParamsList: URLSearchParams[] }
+ >();
const path2moduleIds: Record = {};
- for (const path of routePaths.static || []) {
- for (const searchParams of [
- new URLSearchParams(),
- ...(routePaths.staticSearchParams?.(path) || []),
- ]) {
- const input = getInputString(path, searchParams);
- const moduleIds = await unstable_collectClientModules(input);
- const search = searchParams.toString();
- path2moduleIds[path + (search ? '?' + search : '')] = moduleIds;
+ for (const {
+ path,
+ searchParams = new URLSearchParams(),
+ } of pathsForBuild || []) {
+ let item = pathMap.get(path);
+ if (!item) {
+ item = {
+ isStatic: (await existsPath(path)) === 'static',
+ searchParamsList: [],
+ };
+ pathMap.set(path, item);
}
+ item.searchParamsList.push(searchParams);
+ const input = getInputString(path, searchParams);
+ const moduleIds = await unstable_collectClientModules(input);
+ const search = searchParams.toString();
+ path2moduleIds[path + (search ? '?' + search : '')] = moduleIds;
}
const customCode = `
globalThis.__WAKU_ROUTER_PREFETCH__ = (path, searchParams) => {
@@ -107,30 +107,39 @@ globalThis.__WAKU_ROUTER_PREFETCH__ = (path, searchParams) => {
import(id);
}
};`;
- return Array.from(routePaths.static || []).map((path) => {
- return {
- pathname: path,
- entries: prefetcher(path, routePaths.staticSearchParams?.(path)),
- customCode,
- };
- });
+ return Array.from(pathMap.entries()).map(
+ ([path, { isStatic, searchParamsList }]) => {
+ const entries = searchParamsList.map((searchParams) => ({
+ input: getInputString(path, searchParams),
+ isStatic,
+ }));
+ return { pathname: path, entries, customCode };
+ },
+ );
};
- const getSsrConfig: GetSsrConfig = async (reqUrl) => {
- const existsRoutePath = await existsRoutePathPromise;
- if (!(await existsRoutePath(reqUrl.pathname))) {
+ // TODO this API is not very understandable and not consistent with RSC
+ const getSsrConfig: GetSsrConfig = async (reqUrl, isPrd) => {
+ const pathType = await existsPath(reqUrl.pathname);
+ if (isPrd ? pathType !== 'dynamic' : pathType === null) {
return null;
}
const componentIds = getComponentIds(reqUrl.pathname);
const input = getInputString(reqUrl.pathname, reqUrl.searchParams);
type Opts = {
createElement: typeof createElement;
+ Fragment: typeof Fragment;
Slot: typeof Slot;
};
- const render = ({ createElement, Slot }: Opts) =>
- componentIds.reduceRight(
- (acc: ReactNode, id) => createElement(Slot, { id }, acc),
+ const render = ({ createElement, Fragment, Slot }: Opts) =>
+ createElement(
+ Fragment,
null,
+ createElement(Slot, { id: SHOULD_SKIP_ID }),
+ componentIds.reduceRight(
+ (acc: ReactNode, id) => createElement(Slot, { id }, acc),
+ null,
+ ),
);
return { input, unstable_render: render };
};
diff --git a/packages/waku/src/server.ts b/packages/waku/src/server.ts
index 375f698bc..34325b90a 100644
--- a/packages/waku/src/server.ts
+++ b/packages/waku/src/server.ts
@@ -1,4 +1,4 @@
-import type { createElement, ReactNode } from 'react';
+import type { createElement, Fragment, ReactNode } from 'react';
import type { Slot } from './client.js';
@@ -19,16 +19,24 @@ export type GetBuildConfig = (
) => Promise<
Iterable<{
pathname: string;
- entries?: Iterable;
+ entries?: Iterable<{
+ input: string;
+ skipPrefetch?: boolean;
+ isStatic?: boolean;
+ }>;
customCode?: string; // optional code to inject TODO hope to remove this
context?: unknown;
}>
>;
-export type GetSsrConfig = (reqUrl: URL) => Promise<{
+export type GetSsrConfig = (
+ reqUrl: URL,
+ isPrd: boolean,
+) => Promise<{
input: string;
unstable_render: (opts: {
createElement: typeof createElement;
+ Fragment: typeof Fragment;
Slot: typeof Slot;
}) => ReactNode;
} | null>;
@@ -48,4 +56,5 @@ export type EntriesDev = {
export type EntriesPrd = EntriesDev & {
loadModule: (id: string) => Promise;
loadHtmlHead: (pathname: string) => string;
+ skipRenderRsc: (input: string) => boolean;
};
diff --git a/packages/website/src/entries.tsx b/packages/website/src/entries.tsx
index 1628bd2f7..8e2b08349 100644
--- a/packages/website/src/entries.tsx
+++ b/packages/website/src/entries.tsx
@@ -1,12 +1,13 @@
import { defineRouter } from 'waku/router/server';
+const STATIC_PATHS = ['/', '/blog/introducing-waku'];
+
export default defineRouter(
- // getRoutePaths
- async () => ({
- static: ['/', '/blog/introducing-waku'],
- }),
+ // existsPath
+ async (path: string) => (STATIC_PATHS.includes(path) ? 'static' : null),
// getComponent (id is "**/layout" or "**/page")
- async (id) => {
+ async (id, unstable_setShouldSkip) => {
+ unstable_setShouldSkip({}); // always skip if possible
switch (id) {
case 'layout':
return import('./routes/layout.js');
@@ -18,4 +19,6 @@ export default defineRouter(
return null;
}
},
+ // getPathsForBuild
+ async () => STATIC_PATHS.map((path) => ({ path })),
);
diff --git a/packages/website/src/main.tsx b/packages/website/src/main.tsx
index 1a0727218..cf977ce15 100644
--- a/packages/website/src/main.tsx
+++ b/packages/website/src/main.tsx
@@ -4,7 +4,7 @@ import { Router } from 'waku/router/client';
const rootElement = (
- true} />
+
);
diff --git a/packages/website/vite.config.ts b/packages/website/vite.config.ts
index 209077196..27d26e893 100644
--- a/packages/website/vite.config.ts
+++ b/packages/website/vite.config.ts
@@ -4,6 +4,11 @@ export default defineConfig(({ mode }) => {
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
if (mode === 'development') {
return {
+ optimizeDeps: {
+ include: [
+ // '@uidotdev/usehooks',
+ ],
+ },
ssr: {
external: ['next-mdx-remote'],
},