diff --git a/e2e/fixtures/ssg-performance/src/Path.tsx b/e2e/fixtures/ssg-performance/src/Path.tsx
index 20a5119f7..0591daef4 100644
--- a/e2e/fixtures/ssg-performance/src/Path.tsx
+++ b/e2e/fixtures/ssg-performance/src/Path.tsx
@@ -1,5 +1,3 @@
-import { getPath } from './context.js';
-
-export function Path() {
- return
{getPath()}
;
+export function Path({ path }: { path: string }) {
+ return {path}
;
}
diff --git a/e2e/fixtures/ssg-performance/src/context.ts b/e2e/fixtures/ssg-performance/src/context.ts
deleted file mode 100644
index e7954cef8..000000000
--- a/e2e/fixtures/ssg-performance/src/context.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { cache } from 'react';
-
-// Straight copy of https://github.com/manvalls/server-only-context/tree/main
-// Inlined for demonstration purposes.
-function createContext(defaultValue: T): [() => T, (v: T) => void] {
- const getRef = cache(() => ({ current: defaultValue }));
-
- const getValue = (): T => getRef().current;
-
- const setValue = (value: T) => {
- getRef().current = value;
- };
-
- return [getValue, setValue];
-}
-
-export const [getPath, setPath] = createContext('/');
diff --git a/e2e/fixtures/ssg-performance/src/pages/[path].tsx b/e2e/fixtures/ssg-performance/src/pages/[slug].tsx
similarity index 62%
rename from e2e/fixtures/ssg-performance/src/pages/[path].tsx
rename to e2e/fixtures/ssg-performance/src/pages/[slug].tsx
index 7f4dc3ec8..ae2d9b6a0 100644
--- a/e2e/fixtures/ssg-performance/src/pages/[path].tsx
+++ b/e2e/fixtures/ssg-performance/src/pages/[slug].tsx
@@ -1,8 +1,9 @@
+import type { PageProps } from 'waku/router';
import { Path } from '../Path.js';
-export default async function Test() {
+export default async function Test({ path }: PageProps<'/[slug]'>) {
await new Promise((resolve) => setTimeout(resolve, 1000));
- return ;
+ return ;
}
export async function getConfig() {
diff --git a/e2e/fixtures/ssg-performance/src/pages/_layout.tsx b/e2e/fixtures/ssg-performance/src/pages/_layout.tsx
index 712e6f75b..8f20781fd 100644
--- a/e2e/fixtures/ssg-performance/src/pages/_layout.tsx
+++ b/e2e/fixtures/ssg-performance/src/pages/_layout.tsx
@@ -1,10 +1,5 @@
import type { PropsWithChildren } from 'react';
-import { setPath } from '../context.js';
-export default function Layout({
- children,
- path,
-}: PropsWithChildren<{ path: string }>) {
- setPath(path);
- return children;
+export default function Layout({ children }: PropsWithChildren) {
+ return {children}
;
}
diff --git a/e2e/fixtures/ssg-wildcard/package.json b/e2e/fixtures/ssg-wildcard/package.json
index ec779274e..91e4e58b7 100644
--- a/e2e/fixtures/ssg-wildcard/package.json
+++ b/e2e/fixtures/ssg-wildcard/package.json
@@ -1,5 +1,5 @@
{
- "name": "waku-example",
+ "name": "ssg-wildcard",
"version": "0.1.0",
"type": "module",
"private": true,
diff --git a/examples/11_fs-router/src/main.tsx b/examples/11_fs-router/src/main.tsx
index bddb502d5..354c5d2f7 100644
--- a/examples/11_fs-router/src/main.tsx
+++ b/examples/11_fs-router/src/main.tsx
@@ -1,6 +1,6 @@
import { StrictMode } from 'react';
import { createRoot, hydrateRoot } from 'react-dom/client';
-import { Router } from 'waku/router/client';
+import { NewRouter as Router } from 'waku/router/client';
const rootElement = (
diff --git a/examples/44_cloudflare/waku.cloudflare-dev-server.ts b/examples/44_cloudflare/waku.cloudflare-dev-server.ts
index a50d6d895..b79168281 100644
--- a/examples/44_cloudflare/waku.cloudflare-dev-server.ts
+++ b/examples/44_cloudflare/waku.cloudflare-dev-server.ts
@@ -9,10 +9,7 @@ export const cloudflareDevServer = (cfOptions: any) => {
Object.assign(globalThis, { WebSocketPair });
});
return async (req: Request, app: Hono) => {
- const [proxy, _] = await Promise.all([
- await wranglerPromise,
- await miniflarePromise,
- ]);
+ const [proxy, _] = await Promise.all([wranglerPromise, miniflarePromise]);
Object.assign(req, { cf: proxy.cf });
Object.assign(globalThis, {
caches: proxy.caches,
diff --git a/packages/waku/src/lib/builder/build.ts b/packages/waku/src/lib/builder/build.ts
index b2b76d9a1..d5eaafe99 100644
--- a/packages/waku/src/lib/builder/build.ts
+++ b/packages/waku/src/lib/builder/build.ts
@@ -528,6 +528,9 @@ const willEmitPublicIndexHtml = async (
}
};
+// we write a max of 2500 pages at a time to avoid OOM
+const PATH_SLICE_SIZE = 2500;
+
const emitHtmlFiles = async (
rootDir: string,
env: Record,
@@ -559,104 +562,112 @@ const emitHtmlFiles = async (
/.*?(.*?)<\/head>.*/s,
'$1',
);
- const dynamicHtmlPathMap = new Map();
- await Promise.all(
- Array.from(buildConfig).map(
- async ({ pathname, isStatic, entries, customCode, context }) => {
- const pathSpec =
- typeof pathname === 'string' ? pathname2pathSpec(pathname) : pathname;
- let htmlStr = publicIndexHtml;
- let htmlHead = publicIndexHtmlHead;
- if (cssAssets.length) {
- const cssStr = cssAssets
- .map(
- (asset) =>
- ``,
- )
- .join('\n');
- // HACK is this too naive to inject style code?
- htmlStr = htmlStr.replace(/<\/head>/, cssStr);
- htmlHead += cssStr;
- }
- const rscPathsForPrefetch = new Set();
- const moduleIdsForPrefetch = new Set();
- for (const { rscPath, skipPrefetch } of entries || []) {
- if (!skipPrefetch) {
- rscPathsForPrefetch.add(rscPath);
- for (const id of getClientModules(rscPath)) {
- moduleIdsForPrefetch.add(id);
- }
- }
- }
- const code =
- generatePrefetchCode(
- basePrefix,
- rscPathsForPrefetch,
- moduleIdsForPrefetch,
- ) + (customCode || '');
- if (code) {
- // HACK is this too naive to inject script code?
- htmlStr = htmlStr.replace(
- /<\/head>/,
- ``,
- );
- htmlHead += ``;
- }
- if (!isStatic) {
- dynamicHtmlPathMap.set(pathSpec, htmlHead);
- return;
- }
- pathname = pathSpec2pathname(pathSpec);
- const destHtmlFile = joinPath(
- rootDir,
- config.distDir,
- DIST_PUBLIC,
- extname(pathname)
- ? pathname
- : pathname === '/404'
- ? '404.html' // HACK special treatment for 404, better way?
- : pathname + '/index.html',
- );
- // In partial mode, skip if the file already exists.
- if (existsSync(destHtmlFile)) {
- return;
- }
- const htmlReadable = await renderHtml({
- config,
- pathname,
- searchParams: new URLSearchParams(),
- htmlHead,
- renderRscForHtml: (rscPath, rscParams) =>
- renderRsc(
- { env, config, rscPath, context, decodedBody: rscParams },
- { isDev: false, entries: distEntries },
- ),
- getSsrConfigForHtml: (pathname, searchParams) =>
- getSsrConfig(
- { env, config, pathname, searchParams },
- { isDev: false, entries: distEntries },
- ),
- isDev: false,
- loadModule: distEntries.loadModule,
- });
- await mkdir(joinPath(destHtmlFile, '..'), { recursive: true });
- if (htmlReadable) {
- await pipeline(
- Readable.fromWeb(htmlReadable as any),
- createWriteStream(destHtmlFile),
- );
- } else {
- await writeFile(destHtmlFile, htmlStr);
+ const handlePath = async ({
+ pathname,
+ isStatic,
+ entries,
+ customCode,
+ context,
+ }: BuildConfig[number]) => {
+ const pathSpec =
+ typeof pathname === 'string' ? pathname2pathSpec(pathname) : pathname;
+ let htmlStr = publicIndexHtml;
+ let htmlHead = publicIndexHtmlHead;
+ if (cssAssets.length) {
+ const cssStr = cssAssets
+ .map(
+ (asset) =>
+ ``,
+ )
+ .join('\n');
+ // HACK is this too naive to inject style code?
+ htmlStr = htmlStr.replace(/<\/head>/, cssStr);
+ htmlHead += cssStr;
+ }
+ const rscPathsForPrefetch = new Set();
+ const moduleIdsForPrefetch = new Set();
+ for (const { rscPath, skipPrefetch } of entries || []) {
+ if (!skipPrefetch) {
+ rscPathsForPrefetch.add(rscPath);
+ for (const id of getClientModules(rscPath)) {
+ moduleIdsForPrefetch.add(id);
}
- },
- ),
- );
+ }
+ }
+ const code =
+ generatePrefetchCode(
+ basePrefix,
+ rscPathsForPrefetch,
+ moduleIdsForPrefetch,
+ ) + (customCode || '');
+ if (code) {
+ // HACK is this too naive to inject script code?
+ htmlStr = htmlStr.replace(
+ /<\/head>/,
+ ``,
+ );
+ htmlHead += ``;
+ }
+ if (!isStatic) {
+ dynamicHtmlPathMap.set(pathSpec, htmlHead);
+ return;
+ }
+ const pathFromSpec = pathSpec2pathname(pathSpec);
+ const destHtmlFile = joinPath(
+ rootDir,
+ config.distDir,
+ DIST_PUBLIC,
+ extname(pathFromSpec)
+ ? pathFromSpec
+ : pathFromSpec === '/404'
+ ? '404.html' // HACK special treatment for 404, better way?
+ : pathFromSpec + '/index.html',
+ );
+ // In partial mode, skip if the file already exists.
+ if (existsSync(destHtmlFile)) {
+ return;
+ }
+ const htmlReadable = await renderHtml({
+ config,
+ pathname: pathFromSpec,
+ searchParams: new URLSearchParams(),
+ htmlHead,
+ renderRscForHtml: (rscPath, rscParams) =>
+ renderRsc(
+ { env, config, rscPath, context, decodedBody: rscParams },
+ { isDev: false, entries: distEntries },
+ ),
+ getSsrConfigForHtml: (pathname, searchParams) =>
+ getSsrConfig(
+ { env, config, pathname, searchParams },
+ { isDev: false, entries: distEntries },
+ ),
+ isDev: false,
+ loadModule: distEntries.loadModule,
+ });
+ await mkdir(joinPath(destHtmlFile, '..'), { recursive: true });
+ if (htmlReadable) {
+ await pipeline(
+ Readable.fromWeb(htmlReadable as any),
+ createWriteStream(destHtmlFile),
+ );
+ } else {
+ await writeFile(destHtmlFile, htmlStr);
+ }
+ };
+
+ const dynamicHtmlPathMap = new Map();
+ for (let start = 0; start * PATH_SLICE_SIZE < buildConfig.length; start++) {
+ const end = start * PATH_SLICE_SIZE + PATH_SLICE_SIZE;
+ await Promise.all(buildConfig.slice(start, end).map(handlePath));
+ }
+
const dynamicHtmlPaths = Array.from(dynamicHtmlPathMap);
- const code = `
+ const endCode = `
export const dynamicHtmlPaths = ${JSON.stringify(dynamicHtmlPaths)};
export const publicIndexHtml = ${JSON.stringify(publicIndexHtml)};
`;
- await appendFile(distEntriesFile, code);
+ await appendFile(distEntriesFile, endCode);
};
// FIXME this is too hacky
@@ -735,166 +746,167 @@ const emitStaticFiles = async (
'$1',
);
const dynamicHtmlPathMap = new Map();
- await Promise.all(
- Array.from(buildConfig).map(
- async ({ pathSpec, isStatic, entries, customCode }) => {
- const moduleIdsForPrefetch = new Set();
- for (const { rscPath, isStatic } of entries || []) {
- if (!isStatic) {
- continue;
- }
- const destRscFile = joinPath(
- rootDir,
- config.distDir,
- DIST_PUBLIC,
- config.rscBase,
- encodeRscPath(rscPath),
- );
- // Skip if the file already exists.
- if (existsSync(destRscFile)) {
- continue;
- }
- await mkdir(joinPath(destRscFile, '..'), { recursive: true });
- const utils = {
- renderRsc: (elements: Record) =>
- renderRscNew(config, { unstable_modules }, elements, (id) =>
- moduleIdsForPrefetch.add(id),
- ),
- renderHtml: () => {
- throw new Error('Cannot render HTML in RSC build');
- },
- };
- const input = {
- type: 'component',
- rscPath,
- rscParams: undefined,
- req: {
- body: null,
- url: new URL(
- 'http://localhost/' +
- config.rscBase +
- '/' +
- encodeRscPath(rscPath),
- ),
- method: 'GET',
- headers: {},
- },
- } as const;
- const res = await distEntries.default.unstable_handleRequest(
- input,
- utils,
- );
- const rscReadable = res instanceof ReadableStream ? res : res?.body;
- await pipeline(
- Readable.fromWeb(rscReadable as never),
- createWriteStream(destRscFile),
- );
- }
- let htmlStr = publicIndexHtml;
- let htmlHead = publicIndexHtmlHead;
- if (cssAssets.length) {
- const cssStr = cssAssets
- .map(
- (asset) =>
- ``,
- )
- .join('\n');
- // HACK is this too naive to inject style code?
- htmlStr = htmlStr.replace(/<\/head>/, cssStr);
- htmlHead += cssStr;
- }
- const rscPathsForPrefetch = new Set();
- for (const { rscPath, skipPrefetch } of entries || []) {
- if (!skipPrefetch) {
- rscPathsForPrefetch.add(rscPath);
- }
- }
- const code =
- generatePrefetchCode(
- basePrefix,
- rscPathsForPrefetch,
- moduleIdsForPrefetch,
- ) + (customCode || '');
- if (code) {
- // HACK is this too naive to inject script code?
- htmlStr = htmlStr.replace(
- /<\/head>/,
- ``,
- );
- htmlHead += ``;
- }
- const pathname = pathSpec2pathname(pathSpec);
- const destFile = joinPath(
- rootDir,
- config.distDir,
- DIST_PUBLIC,
- extname(pathname)
- ? pathname
- : pathname === '/404'
- ? '404.html' // HACK special treatment for 404, better way?
- : pathname + '/index.html',
+ const handlePath = async ({
+ pathSpec,
+ isStatic,
+ entries,
+ customCode,
+ }: new_BuildConfig[number]) => {
+ const moduleIdsForPrefetch = new Set();
+ for (const { rscPath, isStatic } of entries || []) {
+ if (!isStatic) {
+ continue;
+ }
+ const destRscFile = joinPath(
+ rootDir,
+ config.distDir,
+ DIST_PUBLIC,
+ config.rscBase,
+ encodeRscPath(rscPath),
+ );
+ // Skip if the file already exists.
+ if (existsSync(destRscFile)) {
+ continue;
+ }
+ await mkdir(joinPath(destRscFile, '..'), { recursive: true });
+ const utils = {
+ renderRsc: (elements: Record) =>
+ renderRscNew(config, { unstable_modules }, elements, (id) =>
+ moduleIdsForPrefetch.add(id),
+ ),
+ renderHtml: () => {
+ throw new Error('Cannot render HTML in RSC build');
+ },
+ };
+ const input = {
+ type: 'component',
+ rscPath,
+ rscParams: undefined,
+ req: {
+ body: null,
+ url: new URL(
+ 'http://localhost/' + config.rscBase + '/' + encodeRscPath(rscPath),
+ ),
+ method: 'GET',
+ headers: {},
+ },
+ } as const;
+ const res = await distEntries.default.unstable_handleRequest(
+ input,
+ utils,
+ );
+ const rscReadable = res instanceof ReadableStream ? res : res?.body;
+ await pipeline(
+ Readable.fromWeb(rscReadable as never),
+ createWriteStream(destRscFile),
+ );
+ }
+ let htmlStr = publicIndexHtml;
+ let htmlHead = publicIndexHtmlHead;
+ if (cssAssets.length) {
+ const cssStr = cssAssets
+ .map(
+ (asset) =>
+ ``,
+ )
+ .join('\n');
+ // HACK is this too naive to inject style code?
+ htmlStr = htmlStr.replace(/<\/head>/, cssStr);
+ htmlHead += cssStr;
+ }
+ const rscPathsForPrefetch = new Set();
+ for (const { rscPath, skipPrefetch } of entries || []) {
+ if (!skipPrefetch) {
+ rscPathsForPrefetch.add(rscPath);
+ }
+ }
+ const code =
+ generatePrefetchCode(
+ basePrefix,
+ rscPathsForPrefetch,
+ moduleIdsForPrefetch,
+ ) + (customCode || '');
+ if (code) {
+ // HACK is this too naive to inject script code?
+ htmlStr = htmlStr.replace(
+ /<\/head>/,
+ ``,
+ );
+ htmlHead += ``;
+ }
+ const pathname = pathSpec2pathname(pathSpec);
+ const destFile = joinPath(
+ rootDir,
+ config.distDir,
+ DIST_PUBLIC,
+ extname(pathname)
+ ? pathname
+ : pathname === '/404'
+ ? '404.html' // HACK special treatment for 404, better way?
+ : pathname + '/index.html',
+ );
+ if (!isStatic) {
+ if (destFile.endsWith('.html')) {
+ // HACK doesn't feel ideal
+ dynamicHtmlPathMap.set(pathSpec, htmlHead);
+ }
+ return;
+ }
+ // In partial mode, skip if the file already exists.
+ if (existsSync(destFile)) {
+ return;
+ }
+ const utils = {
+ renderRsc: (elements: Record) =>
+ renderRscNew(config, { unstable_modules }, elements),
+ renderHtml: (
+ elements: Record,
+ html: ReactNode,
+ rscPath: string,
+ ) => {
+ const readable = renderHtmlNew(
+ config,
+ { unstable_modules },
+ htmlHead,
+ elements,
+ html,
+ rscPath,
);
- if (!isStatic) {
- if (destFile.endsWith('.html')) {
- // HACK doesn't feel ideal
- dynamicHtmlPathMap.set(pathSpec, htmlHead);
- }
- return;
- }
- // In partial mode, skip if the file already exists.
- if (existsSync(destFile)) {
- return;
- }
- const utils = {
- renderRsc: (elements: Record) =>
- renderRscNew(config, { unstable_modules }, elements),
- renderHtml: (
- elements: Record,
- html: ReactNode,
- rscPath: string,
- ) => {
- const readable = renderHtmlNew(
- config,
- { unstable_modules },
- htmlHead,
- elements,
- html,
- rscPath,
- );
- const headers = { 'content-type': 'text/html; charset=utf-8' };
- return {
- body: readable,
- headers,
- };
- },
+ const headers = { 'content-type': 'text/html; charset=utf-8' };
+ return {
+ body: readable,
+ headers,
};
- const input = {
- type: 'custom',
- pathname,
- req: {
- body: null,
- url: new URL('http://localhost' + pathname),
- method: 'GET',
- headers: {},
- },
- } as const;
- const res = await distEntries.default.unstable_handleRequest(
- input,
- utils,
- );
- const readable = res instanceof ReadableStream ? res : res?.body;
- await mkdir(joinPath(destFile, '..'), { recursive: true });
- if (readable) {
- await pipeline(
- Readable.fromWeb(readable as never),
- createWriteStream(destFile),
- );
- } else if (destFile.endsWith('.html')) {
- await writeFile(destFile, htmlStr);
- }
},
- ),
- );
+ };
+ const input = {
+ type: 'custom',
+ pathname,
+ req: {
+ body: null,
+ url: new URL('http://localhost' + pathname),
+ method: 'GET',
+ headers: {},
+ },
+ } as const;
+ const res = await distEntries.default.unstable_handleRequest(input, utils);
+ const readable = res instanceof ReadableStream ? res : res?.body;
+ await mkdir(joinPath(destFile, '..'), { recursive: true });
+ if (readable) {
+ await pipeline(
+ Readable.fromWeb(readable as never),
+ createWriteStream(destFile),
+ );
+ } else if (destFile.endsWith('.html')) {
+ await writeFile(destFile, htmlStr);
+ }
+ };
+
+ for (let start = 0; start * PATH_SLICE_SIZE < buildConfig.length; start++) {
+ const end = start * PATH_SLICE_SIZE + PATH_SLICE_SIZE;
+ await Promise.all(buildConfig.slice(start, end).map(handlePath));
+ }
+
const dynamicHtmlPaths = Array.from(dynamicHtmlPathMap);
const code = `
export const dynamicHtmlPaths = ${JSON.stringify(dynamicHtmlPaths)};
diff --git a/packages/waku/src/lib/plugins/vite-plugin-rsc-managed.ts b/packages/waku/src/lib/plugins/vite-plugin-rsc-managed.ts
index 9c03e28b2..b99c60592 100644
--- a/packages/waku/src/lib/plugins/vite-plugin-rsc-managed.ts
+++ b/packages/waku/src/lib/plugins/vite-plugin-rsc-managed.ts
@@ -22,7 +22,7 @@ export default fsRouter(
const getManagedMain = () => `
import { StrictMode, createElement } from 'react';
import { createRoot, hydrateRoot } from 'react-dom/client';
-import { Router } from 'waku/router/client';
+import { NewRouter as Router } from 'waku/router/client';
const rootElement = createElement(StrictMode, null, createElement(Router));
diff --git a/packages/waku/src/router/fs-router.ts b/packages/waku/src/router/fs-router.ts
index 946e3ffa1..46172123c 100644
--- a/packages/waku/src/router/fs-router.ts
+++ b/packages/waku/src/router/fs-router.ts
@@ -1,5 +1,5 @@
import { unstable_getPlatformObject } from '../server.js';
-import { createPages } from './create-pages.js';
+import { new_createPages as createPages } from './create-pages.js';
import { EXTENSIONS } from '../lib/constants.js';
@@ -64,7 +64,11 @@ export function fsRouter(
? pathItems.slice(0, -1)
: pathItems
).join('/');
- if (pathItems.at(-1) === '_layout') {
+ if (pathItems.at(-1) === '[path]') {
+ throw new Error(
+ 'Page file cannot be named [path]. This will conflict with the path prop of the page component.',
+ );
+ } else if (pathItems.at(-1) === '_layout') {
createLayout({
path,
component: mod.default,