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

step 1 for middleware architecture #545

Merged
merged 9 commits into from
Feb 29, 2024
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
4 changes: 4 additions & 0 deletions packages/waku/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
"types": "./dist/config.d.ts",
"default": "./dist/config.js"
},
"./middleware": {
"types": "./dist/middleware.d.ts",
"default": "./dist/middleware.js"
},
"./prd": {
"types": "./dist/prd.d.ts",
"default": "./dist/prd.js"
Expand Down
35 changes: 24 additions & 11 deletions packages/waku/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import * as dotenv from 'dotenv';

import type { Config } from './config.js';
import { resolveConfig } from './lib/config.js';
import { honoMiddleware as honoDevMiddleware } from './lib/middleware/hono-dev.js';
import { honoMiddleware as honoPrdMiddleware } from './lib/middleware/hono-prd.js';
import { honoMiddleware as honoDevMiddleware } from './lib/old-wrappers/hono-dev.js';
import { honoMiddleware as honoPrdMiddleware } from './lib/old-wrappers/hono-prd.js';
import { runner } from './lib/hono/runner.js';
import { build } from './lib/builder/build.js';

const require = createRequire(new URL('.', import.meta.url));
Expand Down Expand Up @@ -131,15 +132,27 @@ async function runStart(options: { ssr: boolean }) {
import(pathToFileURL(path.resolve(distDir, entriesJs)).toString());
const app = new Hono();
app.use('*', serveStatic({ root: path.join(distDir, publicDir) }));
app.use(
'*',
honoPrdMiddleware({
...options,
config,
loadEntries,
env: process.env as any,
}),
);
if (process.env.WAKU_WIP_MIDDLEWARE) {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It only works behind this flag.

app.use(
'*',
runner({
config,
env: process.env as any,
cmd: 'start',
loadEntries,
}),
);
} else {
app.use(
'*',
honoPrdMiddleware({
...options,
config,
loadEntries,
env: process.env as any,
}),
);
}
if (!options.ssr) {
// history api fallback
app.use(
Expand Down
7 changes: 3 additions & 4 deletions packages/waku/src/dev.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export { honoMiddleware as unstable_honoMiddleware } from './lib/middleware/hono-dev.js';
export { connectMiddleware as unstable_connectMiddleware } from './lib/middleware/connect-dev.js';
export { honoMiddleware as unstable_honoMiddleware } from './lib/old-wrappers/hono-dev.js';
export { connectMiddleware as unstable_connectMiddleware } from './lib/old-wrappers/connect-dev.js';

export { createHandler as unstable_createHandler } from './lib/handlers/handler-dev.js';
export { build } from './lib/builder/build.js';
export { build as unstable_build } from './lib/builder/build.js';
1 change: 1 addition & 0 deletions packages/waku/src/lib/builder/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,7 @@ const emitHtmlFiles = async (
const dynamicHtmlPaths = Array.from(dynamicHtmlPathMap);
const code = `
export const dynamicHtmlPaths= ${JSON.stringify(dynamicHtmlPaths)};
export const publicIndexHtml= ${JSON.stringify(publicIndexHtml)};
`;
await appendFile(distEntriesFile, code);
};
Expand Down
2 changes: 1 addition & 1 deletion packages/waku/src/lib/builder/serve-aws-lambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Hono } from 'hono';
import { handle } from 'hono/aws-lambda';
import { serveStatic } from '@hono/node-server/serve-static';

import { honoMiddleware } from '../middleware/hono-prd.js';
import { honoMiddleware } from '../old-wrappers/hono-prd.js';
import path from 'node:path';
import { existsSync, readFileSync } from 'node:fs';

Expand Down
2 changes: 1 addition & 1 deletion packages/waku/src/lib/builder/serve-cloudflare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { serveStatic } from 'hono/cloudflare-workers';
// eslint-disable-next-line import/no-unresolved
import manifest from '__STATIC_CONTENT_MANIFEST';

import { honoMiddleware } from '../middleware/hono-prd.js';
import { honoMiddleware } from '../old-wrappers/hono-prd.js';

const ssr = !!import.meta.env.WAKU_BUILD_SSR;
const loadEntries = () => import(import.meta.env.WAKU_ENTRIES_FILE!);
Expand Down
2 changes: 1 addition & 1 deletion packages/waku/src/lib/builder/serve-deno.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Hono } from 'https://deno.land/x/hono/mod.ts';
// @ts-expect-error no types
import { serveStatic } from 'https://deno.land/x/hono/middleware.ts';

import { honoMiddleware } from '../middleware/hono-prd.js';
import { honoMiddleware } from '../old-wrappers/hono-prd.js';

declare const Deno: any;

Expand Down
2 changes: 1 addition & 1 deletion packages/waku/src/lib/builder/serve-netlify.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Hono } from 'hono';
import type { Context } from '@netlify/functions';

import { honoMiddleware } from '../middleware/hono-prd.js';
import { honoMiddleware } from '../old-wrappers/hono-prd.js';

const ssr = !!import.meta.env.WAKU_BUILD_SSR;
const loadEntries = () => import(import.meta.env.WAKU_ENTRIES_FILE!);
Expand Down
2 changes: 1 addition & 1 deletion packages/waku/src/lib/builder/serve-partykit.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Hono } from 'hono';

import { honoMiddleware } from '../middleware/hono-prd.js';
import { honoMiddleware } from '../old-wrappers/hono-prd.js';

const ssr = !!import.meta.env.WAKU_BUILD_SSR;
const loadEntries = () => import(import.meta.env.WAKU_ENTRIES_FILE!);
Expand Down
2 changes: 1 addition & 1 deletion packages/waku/src/lib/builder/serve-vercel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
import { Hono } from 'hono';
import { getRequestListener } from '@hono/node-server';

import { honoMiddleware } from '../middleware/hono-prd.js';
import { honoMiddleware } from '../old-wrappers/hono-prd.js';

const ssr = !!import.meta.env.WAKU_BUILD_SSR;
const distDir = import.meta.env.WAKU_CONFIG_DIST_DIR!;
Expand Down
61 changes: 61 additions & 0 deletions packages/waku/src/lib/hono/runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { MiddlewareHandler } from 'hono';

import type { HandlerContext, MiddlewareOptions } from '../middleware/types.js';

const createEmptyReadableStream = () =>
new ReadableStream({
start(controller) {
controller.close();
},
});

export const runner = (options: MiddlewareOptions): MiddlewareHandler => {
const middlewareList = [
import('waku/middleware').then((mod) => mod.ssr),
import('waku/middleware').then((mod) => mod.rsc),
// import('waku/middleware').then((mod) => mod.fallback),
];
const handlersPromise = Promise.all(
middlewareList.map(async (middleware) => (await middleware)(options)),
);
return async (c, next) => {
const ctx: HandlerContext = {
req: {
body: c.req.raw.body || createEmptyReadableStream(),
url: new URL(c.req.url),
method: c.req.method,
headers: Object.fromEntries(
Array.from(c.req.raw.headers.entries()).map(([k, v]) => [k, v]),
),
},
res: {},
context: {},
};
const handlers = await handlersPromise;
const run = async (index: number) => {
if (index >= handlers.length) {
return next();
}
let alreadyCalled = false;
await handlers[index]!(ctx, async () => {
if (!alreadyCalled) {
alreadyCalled = true;
await run(index + 1);
}
});
};
await run(0);
if ('status' in ctx.res) {
c.status(ctx.res.status as any);
}
if ('headers' in ctx.res) {
for (const [k, v] of Object.entries(ctx.res.headers)) {
c.header(k, v);
}
}
if ('body' in ctx.res) {
return c.body(ctx.res.body);
}
return c.body(null);
};
};
20 changes: 20 additions & 0 deletions packages/waku/src/lib/middleware/fallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { stringToStream } from '../utils/stream.js';
import type { Middleware } from './types.js';

export const fallback: Middleware = (options) => {
if (options.cmd === 'dev') {
// pass through in dev command
return async (_ctx, next) => next();
}

const entriesPromise = options.loadEntries();

return async (ctx, _next) => {
const entries = await entriesPromise;
ctx.res.body = stringToStream(entries.publicIndexHtml);
ctx.res.headers = {
...ctx.res.headers,
'content-type': 'text/html; charset=utf-8',
};
};
};
55 changes: 55 additions & 0 deletions packages/waku/src/lib/middleware/rsc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { resolveConfig } from '../config.js';
import { decodeInput, hasStatusCode } from '../renderers/utils.js';
import { renderRsc } from '../renderers/rsc-renderer.js';
import type { Middleware } from './types.js';

export const rsc: Middleware = (options) => {
if (options.cmd === 'dev') {
throw new Error('not implemented yet');
}

(globalThis as any).__WAKU_PRIVATE_ENV__ = options.env || {};
const configPromise = resolveConfig(options.config || {});
const entriesPromise = options.loadEntries();

return async (ctx, next) => {
const [config, entries] = await Promise.all([
configPromise,
entriesPromise,
]);
const basePrefix = config.basePath + config.rscPath + '/';
if (ctx.req.url.pathname.startsWith(basePrefix)) {
const { method, headers } = ctx.req;
if (method !== 'GET' && method !== 'POST') {
throw new Error(`Unsupported method '${method}'`);
}
try {
const input = decodeInput(
ctx.req.url.pathname.slice(basePrefix.length),
);
const readable = await renderRsc({
config,
input,
searchParams: ctx.req.url.searchParams,
method,
context: ctx.context,
body: ctx.req.body,
contentType: headers['content-type'] || '',
isDev: false,
entries,
});
ctx.res.body = readable;
return;
} catch (err) {
if (hasStatusCode(err)) {
ctx.res.status = err.statusCode;
} else {
console.info('Cannot process RSC', err);
ctx.res.status = 500;
}
return;
}
}
await next();
};
};
78 changes: 78 additions & 0 deletions packages/waku/src/lib/middleware/ssr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { resolveConfig } from '../config.js';
import { getPathMapping } from '../utils/path.js';
import { renderHtml } from '../renderers/html-renderer.js';
import { hasStatusCode } from '../renderers/utils.js';
import { renderRsc, getSsrConfig } from '../renderers/rsc-renderer.js';
import type { Middleware } from './types.js';

export const CLIENT_PREFIX = 'client/';

export const ssr: Middleware = (options) => {
if (options.cmd === 'dev') {
throw new Error('not implemented yet');
}

(globalThis as any).__WAKU_PRIVATE_ENV__ = options.env || {};
const configPromise = resolveConfig(options.config || {});
const entriesPromise = options.loadEntries();

return async (ctx, next) => {
const [config, entries] = await Promise.all([
configPromise,
entriesPromise,
]);
try {
const { dynamicHtmlPaths } = entries;
const htmlHead = dynamicHtmlPaths.find(([pathSpec]) =>
getPathMapping(pathSpec, ctx.req.url.pathname),
)?.[1];
if (htmlHead) {
const readable = await renderHtml({
config,
pathname: ctx.req.url.pathname,
searchParams: ctx.req.url.searchParams,
htmlHead,
// TODO refactor: avoid this and try using next() instead
renderRscForHtml: (input, searchParams) =>
renderRsc({
entries,
config,
input,
searchParams,
method: 'GET',
context: ctx.context,
isDev: false,
}),
getSsrConfigForHtml: (pathname, searchParams) =>
getSsrConfig({
config,
pathname,
searchParams,
isDev: false,
entries,
}),
loadClientModule: (key) => entries.loadModule(CLIENT_PREFIX + key),
isDev: false,
loadModule: entries.loadModule,
});
if (readable) {
ctx.res.headers = {
...ctx.res.headers,
'content-type': 'text/html; charset=utf-8',
};
ctx.res.body = readable;
return;
}
}
} catch (err) {
if (hasStatusCode(err)) {
ctx.res.status = err.statusCode;
} else {
console.info('Cannot process SSR', err);
ctx.res.status = 500;
}
return;
}
await next();
};
};
35 changes: 35 additions & 0 deletions packages/waku/src/lib/middleware/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Config } from '../../config.js';
import type { EntriesPrd } from '../../server.js';

export type HandlerReq = {
readonly body: ReadableStream;
readonly url: URL;
readonly method: string;
readonly headers: Record<string, string>;
};

export type HandlerRes = {
body?: ReadableStream;
headers?: Record<string, string>;
status?: number;
};

export type RscContext = Record<string, unknown>;

export type HandlerContext = {
readonly req: HandlerReq;
readonly res: HandlerRes;
readonly context: RscContext;
};

export type Handler = (
ctx: HandlerContext,
next: () => Promise<void>,
) => Promise<void>;

export type MiddlewareOptions = {
config?: Config;
env?: Record<string, string>;
} & ({ cmd: 'dev' } | { cmd: 'start'; loadEntries: () => Promise<EntriesPrd> });

export type Middleware = (options: MiddlewareOptions) => Handler;
10 changes: 10 additions & 0 deletions packages/waku/src/lib/utils/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,13 @@ export const streamToString = async (
outs.push(decoder.decode());
return outs.join('');
};

export const stringToStream = (str: string): ReadableStream => {
const encoder = new TextEncoder();
return new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode(str));
controller.close();
},
});
};
Loading
Loading