Skip to content

Commit

Permalink
Use URLPattern for routing (#241)
Browse files Browse the repository at this point in the history
* Use `URLPattern` for routing

* chore(dependencies): updated changesets for modified dependencies

* Multiple query params

* Ponyfill URLPattern

* Go

* chore(dependencies): updated changesets for modified dependencies

* /

* Remove itty-router completely

* Prettier

* Go

* Go

* Use Error Handling

* Go

* YAS

* Go2

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
ardatan and github-actions[bot] committed Jan 2, 2023
1 parent b8c6eac commit 563cfaa
Show file tree
Hide file tree
Showing 21 changed files with 360 additions and 34 deletions.
5 changes: 5 additions & 0 deletions .changeset/@whatwg-node_fetch-241-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@whatwg-node/fetch': patch
---
dependencies updates:
- Added dependency [`urlpattern-polyfill@^6.0.2` ↗︎](https://www.npmjs.com/package/urlpattern-polyfill/v/6.0.2) (to `dependencies`)
5 changes: 5 additions & 0 deletions .changeset/@whatwg-node_router-241-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@whatwg-node/router': patch
---
dependencies updates:
- Removed dependency [`itty-router@3.0.11` ↗︎](https://www.npmjs.com/package/itty-router/v/3.0.11) (from `dependencies`)
6 changes: 6 additions & 0 deletions .changeset/big-goats-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@whatwg-node/router': minor
'@whatwg-node/fetch': minor
---

Drop itty-router in favor of new URLPattern in the fetch ponyfill
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ dist
CHANGELOG.md
.next
[...slug].js
.changeset
2 changes: 0 additions & 2 deletions e2e/deno/import-map.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
"@e2e/shared-server": "../../e2e/shared-server/src/index.ts",
"@e2e/shared-scripts": "../../e2e/shared-scripts/src/utils.ts",
"@pulumi/pulumi": "npm:@pulumi/pulumi",
"itty-router": "npm:itty-router",
"itty-router-extras": "npm:itty-router-extras",
"child_process": "https://deno.land/std@0.165.0/node/child_process.ts",
"fs": "https://deno.land/std@0.165.0/node/fs.ts",
"util": "https://deno.land/std@0.165.0/node/util.ts"
Expand Down
3 changes: 1 addition & 2 deletions e2e/shared-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"version": "0.0.3",
"private": true,
"dependencies": {
"@whatwg-node/router": "0.0.3",
"itty-router": "3.0.11"
"@whatwg-node/router": "0.0.3"
}
}
7 changes: 5 additions & 2 deletions e2e/shared-server/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { createRouter, Response, DefaultServerAdapterContext } from '@whatwg-node/router';
import { createRouter, Response, DefaultServerAdapterContext, withErrorHandling } from '@whatwg-node/router';

export function createTestServerAdapter<TServerContext = DefaultServerAdapterContext>(base?: string) {
const app = createRouter<TServerContext>({ base });
const app = createRouter<TServerContext>({
base,
plugins: [withErrorHandling as any],
});

app.get(
'/greetings/:name',
Expand Down
6 changes: 6 additions & 0 deletions packages/fetch/dist/create-node-ponyfill.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module.exports = function createNodePonyfill(opts = {}) {
ponyfills.TransformStream = globalThis.TransformStream;
ponyfills.Blob = globalThis.Blob;
ponyfills.crypto = globalThis.crypto;
ponyfills.URLPattern = globalThis.URLPattern;

if (!ponyfills.AbortController) {
const abortControllerModule = require("abort-controller");
Expand Down Expand Up @@ -91,6 +92,11 @@ module.exports = function createNodePonyfill(opts = {}) {
ponyfills.crypto = new cryptoPonyfill.Crypto();
}

if (!ponyfills.URLPattern) {
const urlPatternModule = require('urlpattern-polyfill');
ponyfills.URLPattern = urlPatternModule.URLPattern;
}

// If any of classes of Fetch API is missing, we need to ponyfill them.
if (!ponyfills.fetch ||
!ponyfills.Request ||
Expand Down
2 changes: 2 additions & 0 deletions packages/fetch/dist/deno-ponyfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const crypto = globalThis.crypto;
const btoa = globalThis.btoa;
const TextDecoder = globalThis.TextDecoder;
const TextEncoder = globalThis.TextEncoder;
const URLPattern = (globalThis as any).URLPattern;

export const createFetch = () => globalThis;
export {
Expand All @@ -31,4 +32,5 @@ export {
btoa,
TextDecoder,
TextEncoder,
URLPattern,
};
5 changes: 5 additions & 0 deletions packages/fetch/dist/global-ponyfill.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,9 @@ module.exports.crypto = globalThis.crypto;
module.exports.btoa = globalThis.btoa;
module.exports.TextEncoder = globalThis.TextEncoder;
module.exports.TextDecoder = globalThis.TextDecoder;
module.exports.URLPattern = globalThis.URLPattern;
if (!module.exports.URLPattern) {
const urlPatternModule = require('urlpattern-polyfill');
module.exports.URLPattern = urlPatternModule.URLPattern;
}
module.exports.createFetch = () => globalThis;
6 changes: 5 additions & 1 deletion packages/fetch/dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/// <reference lib="dom" />
/// <reference types="urlpattern-polyfill" />

declare const _fetch: typeof fetch;
declare const _Request: typeof Request;
Expand All @@ -15,6 +16,7 @@ declare const _crypto: typeof crypto;
declare const _btoa: typeof btoa;
declare const _TextEncoder: typeof TextEncoder;
declare const _TextDecoder: typeof TextDecoder;
declare const _URLPattern: typeof URLPattern;

declare module "@whatwg-node/fetch" {
export const fetch: typeof _fetch;
Expand All @@ -32,6 +34,7 @@ declare module "@whatwg-node/fetch" {
export const btoa: typeof _btoa;
export const TextDecoder: typeof _TextDecoder;
export const TextEncoder: typeof _TextEncoder;
export const URLPattern: typeof _URLPattern;
export interface FormDataLimits {
/* Max field name size (in bytes). Default: 100. */
fieldNameSize?: number;
Expand Down Expand Up @@ -63,7 +66,8 @@ declare module "@whatwg-node/fetch" {
crypto: typeof _crypto,
btoa: typeof _btoa,
TextEncoder: typeof _TextEncoder,
TextDecoder: typeof _TextDecoder
TextDecoder: typeof _TextDecoder,
URLPattern: typeof _URLPattern,
});
}

1 change: 1 addition & 0 deletions packages/fetch/dist/node-ponyfill.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ module.exports.crypto = ponyfills.crypto;
module.exports.btoa = ponyfills.btoa;
module.exports.TextEncoder = ponyfills.TextEncoder;
module.exports.TextDecoder = ponyfills.TextDecoder;
module.exports.URLPattern = ponyfills.URLPattern;

exports.createFetch = createNodePonyfill;
3 changes: 2 additions & 1 deletion packages/fetch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"formdata-node": "^4.3.1",
"node-fetch": "^2.6.7",
"undici": "^5.12.0",
"web-streams-polyfill": "^3.2.0"
"web-streams-polyfill": "^3.2.0",
"urlpattern-polyfill": "^6.0.2"
},
"publishConfig": {
"access": "public"
Expand Down
1 change: 0 additions & 1 deletion packages/router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
"dependencies": {
"@whatwg-node/fetch": "0.5.4",
"@whatwg-node/server": "0.5.1",
"itty-router": "3.0.11",
"tslib": "^2.3.1"
}
}
121 changes: 104 additions & 17 deletions packages/router/src/createRouter.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,120 @@
import { createServerAdapter, type DefaultServerAdapterContext } from '@whatwg-node/server';
import { Router as IttyRouter } from 'itty-router';
import { Request as DefaultRequestCtor } from '@whatwg-node/fetch';
import type { Router, RouterBaseObject } from './types';
import { Request as DefaultRequestCtor, URLPattern } from '@whatwg-node/fetch';
import type { HTTPMethod, RouteMethodKey, Router, RouterBaseObject, RouterHandler, RouterRequest } from './types';

interface RouterOptions<TServerContext = DefaultServerAdapterContext> {
base?: string;
RequestCtor?: typeof Request;
plugins?: Array<(router: RouterBaseObject<TServerContext>) => RouterBaseObject<TServerContext>>;
}

const HTTP_METHODS = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH'] as HTTPMethod[];

export function createRouter<TServerContext = DefaultServerAdapterContext>(
options?: RouterOptions<TServerContext>
): Router<TServerContext> {
let ittyRouter = IttyRouter({
base: options?.base,
}) as unknown as RouterBaseObject<TServerContext>;
ittyRouter.all!('*', request => {
let parsedUrl: URL;
Object.defineProperty(request, 'parsedUrl', {
get() {
if (!parsedUrl) {
parsedUrl = new URL(request.url);
const routesByMethod = new Map<HTTPMethod, Map<URLPattern, RouterHandler<TServerContext>[]>>();
function addHandlersToMethod(method: HTTPMethod, path: string, ...handlers: RouterHandler<TServerContext>[]) {
let methodPatternMaps = routesByMethod.get(method);
if (!methodPatternMaps) {
methodPatternMaps = new Map();
routesByMethod.set(method, methodPatternMaps);
}
const basePath = options?.base || '/';
let fullPath = '';
if (basePath === '/') {
fullPath = path;
} else if (path === '/') {
fullPath = basePath;
} else {
fullPath = `${basePath}${path}`;
}
const pattern = new URLPattern({ pathname: fullPath });
methodPatternMaps.set(pattern, handlers);
}
async function handleRequest(request: Request, context: TServerContext) {
const method = request.method as HTTPMethod;
let _parsedUrl: URL;
function getParsedUrl() {
if (!_parsedUrl) {
_parsedUrl = new URL(request.url);
}
return _parsedUrl;
}
const methodPatternMaps = routesByMethod.get(method);
if (methodPatternMaps) {
const queryProxy = new Proxy(
{},
{
get(_, prop) {
const parsedUrl = getParsedUrl();
const allQueries = parsedUrl.searchParams.getAll(prop.toString());
return allQueries.length === 1 ? allQueries[0] : allQueries;
},
has(_, prop) {
const parsedUrl = getParsedUrl();
return parsedUrl.searchParams.has(prop.toString());
},
}
);
for (const [pattern, handlers] of methodPatternMaps) {
const match = pattern.exec(request.url);
if (match) {
const routerRequest = new Proxy(request, {
get(target, prop) {
if (prop === 'parsedUrl') {
return getParsedUrl();
}
if (prop === 'params') {
return match.pathname.groups;
}
if (prop === 'query') {
return queryProxy;
}
const targetProp = target[prop];
if (typeof targetProp === 'function') {
return targetProp.bind(target);
}
return targetProp;
},
has(target, prop) {
return prop in target || prop === 'parsedUrl' || prop === 'params' || prop === 'query';
},
}) as RouterRequest;
for (const handler of handlers) {
const result = await handler(routerRequest as RouterRequest, context);
if (result) {
return result;
}
}
}
}
}
}
let routerBaseObject = new Proxy({} as RouterBaseObject<TServerContext>, {
get(_, prop) {
if (prop === 'handle') {
return handleRequest;
}
const method = prop.toString().toLowerCase() as RouteMethodKey;
return function routeMethodKeyFn(
this: RouterBaseObject<TServerContext>,
path: string,
...handlers: RouterHandler<TServerContext>[]
) {
if (method === 'all') {
for (const httpMethod of HTTP_METHODS) {
addHandlersToMethod(httpMethod, path, ...handlers);
}
} else {
addHandlersToMethod(method.toUpperCase() as HTTPMethod, path, ...handlers);
}
return parsedUrl;
},
});
return this;
};
},
});
options?.plugins?.forEach(plugin => {
ittyRouter = plugin(ittyRouter);
routerBaseObject = plugin(routerBaseObject);
});
return createServerAdapter(ittyRouter, options?.RequestCtor || DefaultRequestCtor);
return createServerAdapter(routerBaseObject, options?.RequestCtor || DefaultRequestCtor);
}
2 changes: 1 addition & 1 deletion packages/router/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from './types';
export * from './createRouter';
export { Response } from '@whatwg-node/fetch';
export { Response, URLPattern } from '@whatwg-node/fetch';
export { withErrorHandling, withCORS, DefaultServerAdapterContext } from '@whatwg-node/server';
2 changes: 1 addition & 1 deletion packages/router/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface RouterRequest extends Request {
method: HTTPMethod;
parsedUrl: URL;
params: Record<string, string>;
query: Record<string, string>;
query: Record<string, string | string[]>;
}

export type RouteMethodKey = Lowercase<HTTPMethod> | 'all';
Expand Down

0 comments on commit 563cfaa

Please sign in to comment.