Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new defineEntries (and context middleware) #961

Merged
merged 26 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
38 changes: 16 additions & 22 deletions examples/31_minimal/src/entries.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,21 @@
import { defineEntries } from 'waku/server';
import { Slot } from 'waku/client';
import {
new_defineEntries,
unstable_renderRsc as renderRsc,
} from 'waku/server';

import App from './components/App';

export default defineEntries(
// renderEntries
async (rscPath) => {
return {
App: <App name={rscPath || 'Waku'} />,
};
},
// getBuildConfig
async () => [{ pathname: '/', entries: [{ rscPath: '' }] }],
// getSsrConfig
async (pathname) => {
switch (pathname) {
case '/':
return {
rscPath: '',
html: <Slot id="App" />,
};
dai-shi marked this conversation as resolved.
Show resolved Hide resolved
default:
return null;
export default new_defineEntries({
unstable_handleRequest: async (config, ctx) => {
const basePrefix = config.basePath + config.rscBase + '/';
if (ctx.req.url.pathname.startsWith(basePrefix)) {
// const rscPath = decodeRscPath(
// decodeURI(ctx.req.url.pathname.slice(basePrefix.length)),
// );
ctx.res.body = renderRsc(config, ctx, { App: <App name="Waku" /> });
}
},
);
unstable_getBuildConfig: async () => [
{ pathname: '/', entries: [{ rscPath: '' }] },
],
});
9 changes: 9 additions & 0 deletions examples/31_minimal/waku.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** @type {import('waku/config').Config} */
export default {
middleware: () => [
import('waku/middleware/context'),
import('waku/middleware/dev-server'),
import('waku/middleware/handler'),
import('waku/middleware/fallback'),
],
};
dai-shi marked this conversation as resolved.
Show resolved Hide resolved
5 changes: 5 additions & 0 deletions packages/waku/src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ type DeepRequired<T> = T extends (...args: any[]) => any

export type ResolvedConfig = DeepRequired<Config>;

export type PureConfig = Omit<
DeepRequired<Config>,
'middleware' | 'unstable_honoEnhancer'
>;

const DEFAULT_MIDDLEWARE = () => [
import('waku/middleware/context'),
import('waku/middleware/dev-server'),
Expand Down
4 changes: 2 additions & 2 deletions packages/waku/src/lib/hono/ctx.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { Context, Env } from 'hono';

import { unstable_getCustomContext } from '../../server.js';
import { getContext } from '../middleware/context.js';

// Internal context key
const HONO_CONTEXT = '__hono_context';

export const getHonoContext = <E extends Env = Env>() => {
const c = unstable_getCustomContext()[HONO_CONTEXT];
const c = getContext().data[HONO_CONTEXT];
if (!c) {
throw new Error('Hono context is not available');
}
Expand Down
12 changes: 5 additions & 7 deletions packages/waku/src/lib/middleware/context.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import type { AsyncLocalStorage as AsyncLocalStorageType } from 'node:async_hooks';

import type { HandlerReq, HandlerRes, Middleware } from './types.js';
import type { HandlerReq, Middleware } from './types.js';

type Context = {
readonly req: HandlerReq;
readonly res: HandlerRes;
readonly data: Record<string, unknown>;
};

Expand All @@ -14,9 +13,7 @@ try {
const { AsyncLocalStorage } = await import('node:async_hooks');
contextStorage = new AsyncLocalStorage();
} catch {
console.warn(
'AsyncLocalStorage is not available, rerender and getCustomContext are only available in sync.',
);
console.warn('AsyncLocalStorage is not available');
}

let previousContext: Context | undefined;
Expand All @@ -39,7 +36,6 @@ export const context: Middleware = () => {
return async (ctx, next) => {
const context: Context = {
req: ctx.req,
res: ctx.res,
data: ctx.data,
};
return runWithContext(context, next);
Expand All @@ -49,7 +45,9 @@ export const context: Middleware = () => {
export function getContext() {
const context = contextStorage?.getStore() ?? currentContext;
if (!context) {
throw new Error('Context is not available');
throw new Error(
'Context is not available. Make sure to use the context middleware.',
);
}
return context;
}
8 changes: 4 additions & 4 deletions packages/waku/src/lib/middleware/dev-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,10 +336,10 @@ export const devServer: Middleware = (options) => {
let initialModules: ClonableModuleNode[];

return async (ctx, next) => {
const [{ middleware: _removed, ...config }, vite] = await Promise.all([
configPromise,
vitePromise,
]);
const [
{ middleware: _removed1, unstable_honoEnhancer: _removed2, ...config },
vite,
] = await Promise.all([configPromise, vitePromise]);

if (!initialModules) {
const processedModules = new Set<string>();
Expand Down
3 changes: 2 additions & 1 deletion packages/waku/src/lib/middleware/fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export const fallback: Middleware = (options) => {
return async (ctx, next) => {
if (!ctx.res.body) {
const config = await configPromise;
ctx.req.url = new URL(config.basePath, ctx.req.url);
const newUrl = new URL(config.basePath, ctx.req.url);
ctx.req.url.pathname = newUrl.pathname;
}
return next();
};
Expand Down
51 changes: 51 additions & 0 deletions packages/waku/src/lib/middleware/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { resolveConfig } from '../config.js';
import type { PureConfig } from '../config.js';
import { setAllEnvInternal } from '../../server.js';
import type { Middleware, HandlerContext } from './types.js';

// TODO avoid copy-pasting
type HandleRequest = (config: PureConfig, ctx: HandlerContext) => Promise<void>;

// TODO avoid copy-pasting
const SERVER_MODULE_MAP = {
'rsdw-server': 'react-server-dom-webpack/server.edge',
} as const;

export const handler: Middleware = (options) => {
const env = options.env || {};
setAllEnvInternal(env);
const entriesPromise =
options.cmd === 'start'
? options.loadEntries()
: ('Error: loadEntries are not available' as never);
dai-shi marked this conversation as resolved.
Show resolved Hide resolved
const configPromise =
options.cmd === 'start'
? entriesPromise.then((entries) =>
entries.loadConfig().then((config) => resolveConfig(config)),
)
: resolveConfig(options.config);

return async (ctx, next) => {
const { unstable_devServer: devServer } = ctx;
const [
{ middleware: _removed1, unstable_honoEnhancer: _removed2, ...config },
entriesPrd,
] = await Promise.all([configPromise, entriesPromise]);
const entriesDev = devServer && (await devServer.loadEntriesDev(config));
const entries: { default: object } = devServer ? entriesDev! : entriesPrd;
const rsdwServer = devServer
? await devServer.loadServerModuleRsc(SERVER_MODULE_MAP['rsdw-server'])
: await entriesPrd.loadModule('rsdw-server');
ctx.unstable_modules = { rsdwServer };
if ('unstable_handleRequest' in entries.default) {
await (entries.default.unstable_handleRequest as HandleRequest)(
config,
ctx,
);
if (ctx.res.body || ctx.res.status) {
return;
}
}
await next();
};
};
8 changes: 4 additions & 4 deletions packages/waku/src/lib/middleware/rsc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ export const rsc: Middleware = (options) => {
: resolveConfig(options.config);

return async (ctx, next) => {
const [{ middleware: _removed, ...config }, entries] = await Promise.all([
configPromise,
entriesPromise,
]);
const [
{ middleware: _removed1, unstable_honoEnhancer: _removed2, ...config },
entries,
] = await Promise.all([configPromise, entriesPromise]);
const basePrefix = config.basePath + config.rscBase + '/';
if (ctx.req.url.pathname.startsWith(basePrefix)) {
const { headers } = ctx.req;
Expand Down
8 changes: 4 additions & 4 deletions packages/waku/src/lib/middleware/ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ export const ssr: Middleware = (options) => {

return async (ctx, next) => {
const { unstable_devServer: devServer } = ctx;
const [{ middleware: _removed, ...config }, entries] = await Promise.all([
configPromise,
entriesPromise,
]);
const [
{ middleware: _removed1, unstable_honoEnhancer: _removed2, ...config },
entries,
] = await Promise.all([configPromise, entriesPromise]);
const entriesDev = devServer && (await devServer.loadEntriesDev(config));
try {
const htmlHead = devServer
Expand Down
11 changes: 7 additions & 4 deletions packages/waku/src/lib/middleware/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import type { EntriesDev, EntriesPrd } from '../../server.js';
export type ClonableModuleNode = { url: string; file: string };

export type HandlerReq = {
body: ReadableStream | null;
url: URL;
method: string;
headers: Record<string, string>;
readonly body: ReadableStream | null;
readonly url: URL;
readonly method: string;
readonly headers: Readonly<Record<string, string>>;
};
dai-shi marked this conversation as resolved.
Show resolved Hide resolved

export type HandlerRes = {
Expand All @@ -32,6 +32,9 @@ export type HandlerContext = {
pathname: string,
) => Promise<TransformStream<any, any>>;
};
unstable_modules?: {
rsdwServer: unknown;
};
};

export type Handler = (
Expand Down
4 changes: 2 additions & 2 deletions packages/waku/src/lib/renderers/html-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { injectRSCPayload } from 'rsc-html-stream/server';
import type * as WakuClientType from '../../client.js';
import type { EntriesPrd } from '../../server.js';
import { SRC_MAIN } from '../constants.js';
import type { ResolvedConfig } from '../config.js';
import type { PureConfig } from '../config.js';
import { concatUint8Arrays } from '../utils/stream.js';
import {
joinPath,
Expand Down Expand Up @@ -168,7 +168,7 @@ const rectifyHtml = () => {

export const renderHtml = async (
opts: {
config: Omit<ResolvedConfig, 'middleware'>;
config: PureConfig;
pathname: string;
searchParams: URLSearchParams;
htmlHead: string;
Expand Down
8 changes: 4 additions & 4 deletions packages/waku/src/lib/renderers/rsc-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
setAllEnvInternal as setAllEnvInternalType,
runWithRenderStoreInternal as runWithRenderStoreInternalType,
} from '../../server.js';
import type { ResolvedConfig } from '../config.js';
import type { PureConfig } from '../config.js';
import { filePathToFileURL } from '../utils/path.js';
import { streamToArrayBuffer } from '../utils/stream.js';
import { decodeFuncId } from '../renderers/utils.js';
Expand All @@ -25,7 +25,7 @@ const resolveClientEntryForPrd = (id: string, config: { basePath: string }) => {

export type RenderRscArgs = {
env: Record<string, string>;
config: Omit<ResolvedConfig, 'middleware'>;
config: PureConfig;
rscPath: string;
context: Record<string, unknown> | undefined;
// TODO we hope to get only decoded one
Expand Down Expand Up @@ -231,7 +231,7 @@ export async function renderRsc(

type GetBuildConfigArgs = {
env: Record<string, string>;
config: Omit<ResolvedConfig, 'middleware'>;
config: PureConfig;
};

type GetBuildConfigOpts = { entries: EntriesPrd };
Expand Down Expand Up @@ -300,7 +300,7 @@ export async function getBuildConfig(

export type GetSsrConfigArgs = {
env: Record<string, string>;
config: Omit<ResolvedConfig, 'middleware'>;
config: PureConfig;
pathname: string;
searchParams: URLSearchParams;
};
Expand Down
46 changes: 46 additions & 0 deletions packages/waku/src/lib/renderers/rsc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { ReactNode } from 'react';
import type { default as RSDWServerType } from 'react-server-dom-webpack/server.edge';

import type { PureConfig } from '../config.js';
// TODO move types somewhere
import type { HandlerContext } from '../middleware/types.js';

type Elements = Record<string, ReactNode>;

const resolveClientEntryForPrd = (id: string, config: { basePath: string }) => {
return config.basePath + id + '.js';
};

export function renderRsc(
config: PureConfig,
ctx: HandlerContext,
elements: Elements,
): ReadableStream {
if (Object.keys(elements).some((key) => key.startsWith('_'))) {
throw new Error('"_" prefix is reserved');
}
const modules = ctx.unstable_modules;
if (!modules) {
throw new Error('handler middleware required (missing modules)');
}
const {
default: {
renderToReadableStream,
// decodeReply,
},
} = modules.rsdwServer as { default: typeof RSDWServerType };
const resolveClientEntry = ctx.unstable_devServer
? ctx.unstable_devServer.resolveClientEntry
: resolveClientEntryForPrd;
const clientBundlerConfig = new Proxy(
{},
{
get(_target, encodedId: string) {
const [file, name] = encodedId.split('#') as [string, string];
const id = resolveClientEntry(file, config);
return { id, chunks: [id], name, async: true };
},
},
);
return renderToReadableStream(elements, clientBundlerConfig);
}
1 change: 1 addition & 0 deletions packages/waku/src/middleware/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { handler as default } from '../lib/middleware/handler.js';
Loading
Loading