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

feat: middleware implementation #109

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e152cd1
feat(middleware): implemented all request handlers as middleware
Industrial May 1, 2022
db6ab08
fix(middleware): made a rebase error, fixed it
Industrial May 1, 2022
3499c7d
refactor(middleware): trying to get rid of as many function parameter…
Industrial May 1, 2022
e56caa2
refactor(middleware): removed more parameters
Industrial May 1, 2022
51fd60e
refactor(middleware): removed all parameters
Industrial May 1, 2022
23bd73a
refactor(middleware): made it a bit cleaner
Industrial May 1, 2022
8c390f3
fix(middleware): requestHandler is now not async anymore for better p…
Industrial May 1, 2022
f584e8d
fix(middleware): changed oak handler
Industrial May 1, 2022
028f684
refactor(middleware): rename factory functions
Industrial May 2, 2022
d33bb44
refactor(middleware): return named functions from middleware factories
Industrial May 2, 2022
f491e98
refactor(middleware): pulled out some scope to let it run once
Industrial May 2, 2022
f9e1c98
refactor(middleware): pull out the importMap too
Industrial May 2, 2022
ecfe4a7
refactor(middleware): used a Promise.all
Industrial May 2, 2022
660913a
feat(middleware): implemented a compose function for middleware
Industrial May 2, 2022
9a14615
refactor(middleware): used a ternary
Industrial May 2, 2022
015c032
fix(middleware): oak handler awaits middleware factory
Industrial May 2, 2022
24b9269
refactor(middleware): removed an ambiguous function
Industrial May 2, 2022
a92c012
refactor(middleware): add some return types
Industrial May 2, 2022
f14a226
refactor(middleware): implemented middleware without create functions
Industrial May 5, 2022
088e156
fix(middleware): oak handler
Industrial May 5, 2022
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
9 changes: 4 additions & 5 deletions src/assets.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Assets } from "./types.ts";
import { ensureDir, extname, join, mime, walk } from "./deps.ts";

const assets = async (dir: string) => {
const meta = {
export default async function assets(dir: string): Promise<Assets> {
const meta: Assets = {
raw: new Map(),
transpile: new Map(),
};
Expand Down Expand Up @@ -30,6 +31,4 @@ const assets = async (dir: string) => {
}

return meta;
};

export default assets;
}
1 change: 1 addition & 0 deletions src/deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export {
} from "https://deno.land/std@0.135.0/path/mod.ts";
export { Buffer, readLines } from "https://deno.land/std@0.135.0/io/mod.ts";
export { serve } from "https://deno.land/std@0.135.0/http/server.ts";
export type { Handler } from "https://deno.land/std@0.135.0/http/server.ts";
export { readableStreamFromReader } from "https://deno.land/std@0.135.0/streams/conversion.ts";
export { default as mime } from "https://esm.sh/mime-types@2.1.35";
export { default as LRU } from "https://deno.land/x/lru@1.0.2/mod.ts";
Expand Down
24 changes: 7 additions & 17 deletions src/oak/handler.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,14 @@
import { Context } from "https://deno.land/x/oak@v10.5.1/mod.ts";
import { isDev, sourceDirectory, vendorDirectory } from "../env.ts";
import { resolveConfig, resolveImportMap } from "../config.ts";
import { createRequestHandler } from "../server/requestHandler.ts";
import { requestHandler } from "../server/middleware.ts";

const cwd = Deno.cwd();

const config = await resolveConfig(cwd);
const importMap = await resolveImportMap(cwd, config);

const requestHandler = await createRequestHandler({
cwd,
importMap,
paths: {
source: sourceDirectory,
vendor: vendorDirectory,
},
isDev,
const ultraRequestHandler = createRequestHandler({
middleware: [
requestHandler,
],
});

export async function ultraHandler(context: Context) {
export async function ultraHandler(context: Context): Promise<void> {
const serverRequestBody = context.request.originalRequest.getBody();

const request = new Request(context.request.url.toString(), {
Expand All @@ -27,7 +17,7 @@ export async function ultraHandler(context: Context) {
body: serverRequestBody.body,
});

const response = await requestHandler(request);
const response = await ultraRequestHandler(request);

context.response.status = response.status;
context.response.headers = response.headers;
Expand Down
4 changes: 2 additions & 2 deletions src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ export const stripTrailingSlash = (url: string): string => {
return url.endsWith("/") ? url.slice(0, -1) : url;
};

export const resolveFileUrl = (from: string, to: string) => {
return new URL(toFileUrl(resolve(from, to)).toString());
export const resolveFileUrl = (directoryPath: string, fileName: string) => {
return new URL(toFileUrl(resolve(directoryPath, fileName)).toString());
};

export const isRemoteSource = (value: string): boolean => {
Expand Down
44 changes: 18 additions & 26 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,12 @@
import { serve } from "./deps.ts";
import {
devServerWebsocketPort,
isDev,
port,
sourceDirectory,
vendorDirectory,
} from "./env.ts";
import { resolveConfig, resolveImportMap } from "./config.ts";
import { Middleware } from "./types.ts";
import { createRequestHandler } from "./server/requestHandler.ts";
import { devServerWebsocketPort, isDev, port } from "./env.ts";
import { serve } from "./deps.ts";

const cwd = Deno.cwd();
const config = await resolveConfig(cwd);
const importMap = await resolveImportMap(cwd, config);

const server = async () => {
const requestHandler = await createRequestHandler({
cwd,
importMap,
paths: {
source: sourceDirectory,
vendor: vendorDirectory,
},
isDev,
export default function () {
const middleware: Middleware[] = [];
const requestHandler = createRequestHandler({
middleware,
});

let message = `Ultra running http://localhost:${port}`;
Expand All @@ -32,7 +17,14 @@ const server = async () => {

console.log(message);

return serve(requestHandler, { port: +port });
};

export default server;
return {
start: () => {
serve(requestHandler, {
port: Number(port),
});
},
use: (middlewareFunction: Middleware) => {
middleware.push(middlewareFunction);
},
};
}
190 changes: 190 additions & 0 deletions src/server/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import assets from "../assets.ts";
import render from "../render.ts";
import transform from "../transform.ts";
import { Context, Middleware, Next } from "../types.ts";
import { LRU, readableStreamFromReader } from "../deps.ts";
import { createURL } from "./request.ts";
import {
disableStreaming,
isDev,
lang,
sourceDirectory,
vendorDirectory,
} from "../env.ts";
import {
replaceFileExt,
resolveFileUrl,
ValidExtensions,
} from "../resolver.ts";
import { resolveConfig, resolveImportMap } from "../config.ts";

const memory = new LRU<string>(500);
const [
rawAssets,
vendorAssets,
importMap,
] = await Promise.all([
assets(sourceDirectory),
assets(`.ultra/${vendorDirectory}`),
(async () => {
const cwd = Deno.cwd();
const config = await resolveConfig(cwd);
const importMap = await resolveImportMap(cwd, config);
return importMap;
})(),
]);

export function dispatch<C extends Context = Context>(
middlewares: Middleware<C>[],
context: C,
index = 0,
): Next {
const nextMiddlewareFunction = middlewares[index];
return nextMiddlewareFunction
? async (shortCircuit?: boolean) => {
if (shortCircuit) {
return;
}

await nextMiddlewareFunction(
context,
dispatch(middlewares, context, index + 1),
);
}
: async () => {};
}

export function compose<C extends Context = Context>(
...middlewares: Middleware<C>[]
): Middleware<C> {
return async function composedMiddleware(
context: C,
next: Next,
) {
await dispatch(middlewares, context)();
await next();
};
}

export const renderPage: Middleware = async (context, next) => {
const url = createURL(context.request);

const body = await render({
url,
importMap,
lang,
disableStreaming,
});

context.response.body = body;
context.response.headers = {
...context.response.headers,
"content-type": "text/html; charset=utf-8",
};

await next(true);
};

export const staticAsset: Middleware = async ({ request, response }, next) => {
const url = createURL(request);

const filePath = `${sourceDirectory}${url.pathname}`;
if (!rawAssets.raw.has(filePath)) {
await next();
return;
}

const contentType = rawAssets.raw.get(filePath);
if (!contentType) {
response.status = 415;
response.statusText = "Unsupported Media Type";

await next(true);
return;
}

response.body = readableStreamFromReader(await Deno.open(`./${filePath}`));
response.headers = {
...response.headers,
"content-type": contentType,
};

await next(true);
};

export const transpileSource: Middleware = async (context, next) => {
const url = createURL(context.request);

const transpilation = async (file: string) => {
let js = memory.get(url.pathname);

if (!js) {
const source = await Deno.readTextFile(
resolveFileUrl(Deno.cwd(), file),
);
const t0 = performance.now();

js = await transform({
source,
sourceUrl: url,
importMap,
});

const t1 = performance.now();
const duration = (t1 - t0).toFixed(2);

console.log(`Transpile ${file} in ${duration}ms`);

if (!isDev) {
memory.set(url.pathname, js);
}
}

return js;
};

const fileTypes: ValidExtensions[] = [".jsx", ".tsx", ".ts"];
for (const fileType of fileTypes) {
const filePath = `${sourceDirectory}${
replaceFileExt(url.pathname, fileType)
}`;
if (rawAssets.transpile.has(filePath)) {
context.response.body = await transpilation(filePath);
context.response.headers = {
...context.response.headers,
"content-type": "application/javascript",
};
await next(true);
return;
}
}

await next();
};

export const vendorMap: Middleware = async ({ request, response }, next) => {
const url = createURL(request);

if (!vendorAssets.raw.has(`.ultra${url.pathname}`)) {
await next();
return;
}

const file = await Deno.open(`./.ultra${url.pathname}`);
const body = readableStreamFromReader(file);

response.body = body;
response.headers = {
...response.headers,
"content-type": "application/javascript",
};

await next(true);
};

export const requestHandler: Middleware = compose(
transpileSource,
staticAsset,
vendorMap,
renderPage,
);
15 changes: 15 additions & 0 deletions src/server/request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export function createURL(request: Request): URL {
const url = new URL(request.url);

const xForwardedProto = request.headers.get("x-forwarded-proto");
if (xForwardedProto) {
url.protocol = `${xForwardedProto}:`;
}

const xForwardedHost = request.headers.get("x-forwarded-host");
if (xForwardedHost) {
url.hostname = xForwardedHost;
}

return url;
}
Loading