Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
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
84 changes: 29 additions & 55 deletions packages/qwik-city/buildtime/vite/dev-server.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import type { ViteDevServer } from 'vite';
import type { BuildContext } from '../types';
import type { EndpointModule, HttpMethod } from '../../runtime/src/library/types';
import type { EndpointModule } from '../../runtime/src/library/types';
import type { QwikViteDevResponse } from '../../../qwik/src/optimizer/src/plugins/vite';
import {
endpointHandler,
getEndpointResponse,
} from '../../middleware/request-handler/endpoint-handler';
import { checkPageRedirect } from '../../middleware/request-handler/redirect-handler';
import { getQwikCityUserContext, isAcceptJsonOnly } from '../../middleware/request-handler/utils';
import { fromNodeRequest, toNodeResponse } from '../../middleware/express/utils';
import { loadUserResponse } from '../../middleware/request-handler/user-response';
import { getQwikCityUserContext } from '../../middleware/request-handler/utils';
import { fromNodeHttp } from '../../middleware/express/utils';
import { buildFromUrlPathname } from '../build';

export function configureDevServer(ctx: BuildContext, server: ViteDevServer) {
Expand All @@ -25,22 +21,10 @@ export function configureDevServer(ctx: BuildContext, server: ViteDevServer) {
const result = await buildFromUrlPathname(ctx, pathname);
if (result) {
const { route, params } = result;
const request = await fromNodeRequest(url, nodeReq);
const method = request.method as HttpMethod;

if (route.type !== 'endpoint') {
const pageRedirectResponse = checkPageRedirect(
url,
request.headers,
ctx.opts.trailingSlash
);
if (pageRedirectResponse) {
await toNodeResponse(pageRedirectResponse, nodeRes);
nodeRes.end();
return;
}
}
const { request, response } = fromNodeHttp(url, nodeReq, nodeRes);
const isEndpointOnly = route.type === 'endpoint';

// use vite to dynamically load each layout/page module in this route's hierarchy
const endpointModules: EndpointModule[] = [];
for (const layout of route.layouts) {
const layoutModule = await server.ssrLoadModule(layout.filePath, {
Expand All @@ -53,49 +37,39 @@ export function configureDevServer(ctx: BuildContext, server: ViteDevServer) {
});
endpointModules.push(endpointModule);

const endpointResponse = await getEndpointResponse(
const userResponse = await loadUserResponse(
request,
method,
url,
params,
endpointModules
endpointModules,
ctx.opts.trailingSlash,
isEndpointOnly
);

if (endpointResponse.immediateCommitToNetwork) {
const response = new Response(endpointResponse.body, {
status: endpointResponse.status,
headers: endpointResponse.headers,
if (userResponse.type === 'endpoint') {
// dev server endpoint handler
response(userResponse.status, userResponse.headers, async (stream) => {
if (typeof userResponse.body === 'string') {
stream.write(userResponse.body);
}
});
await toNodeResponse(response, nodeRes);
nodeRes.end();
return;
}

if (route.type === 'endpoint' || isAcceptJsonOnly(request)) {
const response = endpointHandler(method, endpointResponse);
await toNodeResponse(response, nodeRes);
nodeRes.end();
if (userResponse.type === 'page') {
// qwik city vite plugin should handle dev ssr rendering
// but add the qwik city user context to the response object
(nodeRes as QwikViteDevResponse)._qwikUserCtx = {
...(nodeRes as QwikViteDevResponse)._qwikUserCtx,
...getQwikCityUserContext(userResponse),
};
// update node response with status and headers
// but do not end() it, call next() so qwik plugin handles rendering
nodeRes.statusCode = userResponse.status;
userResponse.headers.forEach((value, key) => nodeRes.setHeader(key, value));
next();
return;
}

if (endpointResponse) {
// modify the response, but do not end()
if (typeof endpointResponse.status === 'number') {
nodeRes.statusCode = endpointResponse.status;
}
if (endpointResponse.headers) {
for (const [key, value] of Object.entries(endpointResponse.headers)) {
if (value) {
nodeRes.setHeader(key, value);
}
}
}
}

(nodeRes as QwikViteDevResponse)._qwikUserCtx = {
...(nodeRes as QwikViteDevResponse)._qwikUserCtx,
...getQwikCityUserContext(url, params, method, endpointResponse),
};
}
} catch (e) {
next(e);
Expand Down
2 changes: 1 addition & 1 deletion packages/qwik-city/middleware/cloudflare-pages/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface EventPluginContext {
}

// @public (undocumented)
export function qwikCity(render: Render, opts: QwikCityPlanCloudflarePages): ({ request, next, waitUntil }: EventPluginContext) => Promise<Response>;
export function qwikCity(render: Render, opts: QwikCityPlanCloudflarePages): ({ request, next }: EventPluginContext) => Promise<Response>;

// @public (undocumented)
export interface QwikCityPlanCloudflarePages extends QwikCityPlan {
Expand Down
42 changes: 22 additions & 20 deletions packages/qwik-city/middleware/cloudflare-pages/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { QwikCityRequestOptions } from '../request-handler/types';
import type { QwikCityRequestContext } from '../request-handler/types';
import { requestHandler } from '../request-handler';
import type { QwikCityPlan } from '@builder.io/qwik-city';
import type { Render } from '@builder.io/qwik/server';
Expand All @@ -9,7 +9,7 @@ import type { Render } from '@builder.io/qwik/server';
* @public
*/
export function qwikCity(render: Render, opts: QwikCityPlanCloudflarePages) {
async function onRequest({ request, next, waitUntil }: EventPluginContext) {
async function onRequest({ request, next }: EventPluginContext) {
try {
// early return from cache
const cache = await caches.open('custom:qwikcity');
Expand All @@ -18,29 +18,31 @@ export function qwikCity(render: Render, opts: QwikCityPlanCloudflarePages) {
return cachedResponse;
}

const requestOpts: QwikCityRequestOptions = {
const requestCtx: QwikCityRequestContext<Response> = {
...opts,
render,
url: new URL(request.url),
request,
response: (status, headers, body) => {
const { readable, writable } = new TransformStream();
const stream = writable.getWriter();
body({
write: (chunk) => stream.write(chunk),
}).finally(() => {
stream.close();
});
return new Response(readable, { status, headers });
},
next,
};

const response = await requestHandler(render, requestOpts);
if (response) {
if (response.ok && request.method === 'GET' && !response.url.includes('localhost')) {
const cacheControl = response.headers.get('Cache-Control') || '';
if (
!cacheControl.includes('no-cache') &&
!cacheControl.includes('no-store') &&
!cacheControl.includes('private')
) {
waitUntil(cache.put(request, response.clone()));
}
}
return response;
} else {
return next();
}
const response = await requestHandler<Response>(requestCtx);
return response;
} catch (e: any) {
return new Response(String(e ? e.stack || e : 'Error'), { status: 500 });
return new Response(String(e ? e.stack || e : 'Error'), {
status: 500,
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
});
}
}

Expand Down
22 changes: 8 additions & 14 deletions packages/qwik-city/middleware/express/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { QwikCityRequestOptions } from '../request-handler/types';
import type { QwikCityRequestContext } from '../request-handler/types';
import { requestHandler } from '../request-handler';
import { patchGlobalFetch } from '../request-handler/node-fetch';
import express from 'express';
import { join, resolve } from 'path';
import type { QwikCityPlan } from '@builder.io/qwik-city';
import type { Render } from '@builder.io/qwik/server';
import { fromNodeRequest, toNodeResponse } from './utils';
import { fromNodeHttp } from './utils';

// @builder.io/qwik-city/middleware/express

Expand Down Expand Up @@ -36,25 +36,19 @@ export function qwikCity(render: Render, opts: QwikCityPlanExpress) {
router.use(async (nodeReq, nodeRes, next) => {
try {
const url = new URL(nodeReq.url, `${nodeReq.protocol}://${nodeReq.headers.host}`);
const request = await fromNodeRequest(url, nodeReq);
const serverRequestEv = fromNodeHttp(url, nodeReq, nodeRes);

const requestOpts: QwikCityRequestOptions = {
const requestCtx: QwikCityRequestContext = {
...opts,
request,
...serverRequestEv,
render,
next,
};

const response = await requestHandler(render, requestOpts);
if (response) {
await toNodeResponse(response, nodeRes);
nodeRes.end();
return;
}
await requestHandler(requestCtx);
} catch (e) {
next(e);
return;
}

next();
});

return router;
Expand Down
110 changes: 46 additions & 64 deletions packages/qwik-city/middleware/express/utils.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,58 @@
import { Readable } from 'stream';

export async function fromNodeRequest(url: URL, nodeReq: NodeRequest) {
const headers = new URLSearchParams();
const nodeHeaders = nodeReq.headers;
if (nodeHeaders) {
for (const key in nodeHeaders) {
const value = nodeHeaders[key];
import { Headers as HeadersPolyfill } from 'headers-polyfill';
import type { ServerResponse } from 'http';
import type { ServerRequestEvent } from '../request-handler/types';

export function fromNodeHttp(url: URL, nodeReq: NodeRequest, nodeRes: ServerResponse) {
const requestHeaders = new (typeof Headers === 'function' ? Headers : HeadersPolyfill)();
const nodeRequestHeaders = nodeReq.headers;
if (nodeRequestHeaders) {
for (const key in nodeRequestHeaders) {
const value = nodeRequestHeaders[key];
if (typeof value === 'string') {
headers.set(key.toLocaleLowerCase(), value);
requestHeaders.set(key, value);
} else if (Array.isArray(value)) {
for (const v of value) {
headers.append(key.toLocaleLowerCase(), v);
requestHeaders.append(key, v);
}
}
}
}

let body: string | undefined = undefined;
const contentType = headers.get('content-type');
if (contentType === 'application/x-www-form-urlencoded') {
try {
const buffers = [];
for await (const chunk of nodeReq as any) {
buffers.push(chunk);
}
if (buffers.length > 0) {
body = Buffer.concat(buffers).toString();
}
} catch (e) {
console.error('convertNodeRequest', e);
const getRequestBody = async () => {
const buffers = [];
for await (const chunk of nodeReq as any) {
buffers.push(chunk);
}
}

const request = new Request(url, {
method: nodeReq.method,
headers,
body,
});

if (typeof request.formData !== 'function') {
request.formData = async function formData() {
const formData: FormData = new URLSearchParams(body);
return formData;
};
}

return request;
}

export async function toNodeResponse(response: Response, nodeRes: NodeResponse) {
nodeRes.statusCode = response.status;
response.headers.forEach((value, key) => nodeRes.setHeader(key, value));
if ((response.status < 300 || response.status >= 400) && response.body) {
if (
typeof response.body === 'string' ||
response.body instanceof Buffer ||
response.body instanceof Uint8Array
) {
nodeRes.write(response.body);
} else if (response.body instanceof Readable) {
for await (const chunk of response.body) {
nodeRes.write(chunk);
}
}
}
return Buffer.concat(buffers).toString();
};

const serverRequestEv: ServerRequestEvent = {
request: {
headers: requestHeaders,
formData: async () => {
return new URLSearchParams(await getRequestBody());
},
json: async () => {
return JSON.parse(await getRequestBody()!);
},
method: nodeReq.method || 'GET',
text: getRequestBody,
url: url.href,
},
response: (status, headers, body) => {
nodeRes.statusCode = status;
headers.forEach((value, key) => nodeRes.setHeader(key, value));
body({
write: (chunk) => nodeRes.write(chunk),
}).finally(() => {
nodeRes.end();
});
return nodeRes;
},
url,
};

return serverRequestEv;
}

export interface NodeRequest {
Expand All @@ -72,10 +61,3 @@ export interface NodeRequest {
headers?: Record<string, string | string[] | undefined>;
method?: string;
}

export interface NodeResponse {
statusCode: number;
setHeader(key: string, value: string): void;
write(chunk: any): boolean;
end(): void;
}
Loading