Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion examples/web-socket/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
},
"dependencies": {
"node-file-router": "workspace:../../dist",
"ws": "8.18.0"
"ws": "8.18.1"
},
"devDependencies": {
"@types/node": "20.1.4",
Expand Down
10 changes: 5 additions & 5 deletions examples/web-socket/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions src/components/dynamic-routes/parse-dynamic-route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { exactSlugSegment } from './exact-slug-segment';
import { catchAllSegment } from './catch-all-segment';
import { optionalCatchAllSegment } from './optional-catch-all-segment';
import type { ParamExtractor } from './common/route-params-parser';

export interface ParsedDynamicRoute {
route: string;
paramExtractors: Record<string, ParamExtractor>;
}

export function parseDynamicRoute(initialRoute: string): ParsedDynamicRoute {
return [exactSlugSegment, catchAllSegment, optionalCatchAllSegment]
.filter((dynamicRoute) => dynamicRoute.isMatch(initialRoute))
.reduce(
(acc, route) => {
const parsedRoute = route.parse(acc.route);
return {
route: parsedRoute.route,
paramExtractors: {
...acc.paramExtractors,
...parsedRoute.paramExtractors
}
};
},
{ route: initialRoute, paramExtractors: {} }
);
}
79 changes: 44 additions & 35 deletions src/components/file-route-resolver.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import fs from 'node:fs/promises';
import path from 'node:path';

import { exactSlugSegment } from './dynamic-routes/exact-slug-segment';
import { catchAllSegment } from './dynamic-routes/catch-all-segment';
import { optionalCatchAllSegment } from './dynamic-routes/optional-catch-all-segment';
import { Route } from './route/route';
import { Middleware } from './route/middleware';
import { catchAllSegment } from './dynamic-routes/catch-all-segment';
import { exactSlugSegment } from './dynamic-routes/exact-slug-segment';
import { optionalCatchAllSegment } from './dynamic-routes/optional-catch-all-segment';
import { parseDynamicRoute } from './dynamic-routes/parse-dynamic-route';
import { isCommonJs } from '../utils/env.utils';
import { validateFileFormat } from '../validations/validations';

import { isClass } from '../utils/common.utils';
import type { ParamExtractor } from './dynamic-routes/common/route-params-parser';
import type { Dirent } from 'node:fs';

interface Props {
Expand All @@ -19,11 +19,6 @@ interface Props {
clearImportCache: boolean;
}

interface RouteWithParams {
route: string;
paramExtractors: Record<string, ParamExtractor>;
}

export class FileRouteResolver {
private static readonly fileExtensions = ['js', 'mjs', 'cjs', 'ts'].join('|');
private static readonly fileExtensionPattern = new RegExp(
Expand Down Expand Up @@ -121,7 +116,8 @@ export class FileRouteResolver {
fullPath,
entry,
routePath,
nestingLevel
nestingLevel,
parentRoute
);

routes.push(route);
Expand Down Expand Up @@ -166,10 +162,7 @@ export class FileRouteResolver {
.then((module) => validateFileFormat(fullPath, module))
.then((module) => module.default);

const routePath = filePath.replace(
FileRouteResolver.middlewareFilePattern,
''
);
const routePath = this.normalizeMiddlewarePath(filePath);

return new Middleware({
path: routePath,
Expand All @@ -182,7 +175,8 @@ export class FileRouteResolver {
fullPath: string,
entry: Dirent,
routePath: string,
nestingLevel: number
nestingLevel: number,
parentRoute: string
): Promise<Route> {
if (this.clearImportCache) {
delete require.cache[fullPath];
Expand All @@ -201,13 +195,14 @@ export class FileRouteResolver {
);

const [method, pureRouteName] = this.extractMethodFromRoute(initialRoute);
const { route, paramExtractors } = this.parseDynamicParams(pureRouteName);
const { route, paramExtractors } = parseDynamicRoute(pureRouteName);

const isIndex = FileRouteResolver.indexFilePattern.test(entry.name);
const routeKey = isIndex ? route.replace(/\/index$/, '') : route;

const formattedUrlPath =
(isIndex ? pureRouteName.replace(/\/index$/, '') : pureRouteName) || '/';
const directoryPaths = this.buildDirectoryPaths(parentRoute);

return new Route({
method,
Expand All @@ -216,28 +211,11 @@ export class FileRouteResolver {
regex: new RegExp(`^${routeKey}/?$`),
nestingLevel,
paramExtractors,
urlPath: formattedUrlPath
urlPath: formattedUrlPath,
directoryPaths
});
}

private parseDynamicParams(initialRoute: string): RouteWithParams {
return [exactSlugSegment, catchAllSegment, optionalCatchAllSegment]
.filter((dynamicRoute) => dynamicRoute.isMatch(initialRoute))
.reduce(
(acc, route) => {
const parsedRoute = route.parse(acc.route);
return {
route: parsedRoute.route,
paramExtractors: {
...acc.paramExtractors,
...parsedRoute.paramExtractors
}
};
},
{ route: initialRoute, paramExtractors: {} }
);
}

private extractMethodFromRoute(route: string): [string | undefined, string] {
const pattern = /\.\[([^\]]+)\](\..+)?$/;

Expand All @@ -251,4 +229,35 @@ export class FileRouteResolver {

return [method, pureRoute];
}

private normalizeMiddlewarePath(filePath: string) {
const pathWithoutFile = filePath.replace(
FileRouteResolver.middlewareFilePattern,
''
);

if (pathWithoutFile === '/' || pathWithoutFile === '') {
return '/';
}

return pathWithoutFile.replace(/\/$/, '');
}

private buildDirectoryPaths(parentRoute: string): string[] {
const directories = ['/'];

if (!parentRoute) {
return directories;
}

const segments = parentRoute.split('/').filter(Boolean);
let currentPath = '';

for (const segment of segments) {
currentPath += `/${segment}`;
directories.push(currentPath);
}

return directories;
}
}
16 changes: 15 additions & 1 deletion src/components/route/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
import { parseDynamicRoute } from '../dynamic-routes/parse-dynamic-route';

interface Props {
path: string;
handler: (...args: unknown[]) => Promise<void>;
nestingLevel: number;
}

function createMiddlewareRegExp(path: string, nestingLevel: number) {
if (!nestingLevel || path === '/') {
return /.*/;
}

const { route } = parseDynamicRoute(path);

return new RegExp(`^${route}(?:\\/.*)?$`);
}

export class Middleware {
path: string;
regexp: RegExp;
handler: (...args: unknown[]) => Promise<void>;
nestingLevel: number;

constructor(props: Props) {
this.path = props.path;
this.handler = props.handler;
this.nestingLevel = props.nestingLevel;
this.regexp = new RegExp(props.nestingLevel ? `^${props.path}.*?$` : '.*');
this.regexp = createMiddlewareRegExp(props.path, props.nestingLevel);
}
}
2 changes: 2 additions & 0 deletions src/components/route/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface Props {
paramExtractors: Record<string, ParamExtractor>;
nestingLevel: number;
urlPath: string;
directoryPaths: string[];
}

export class Route {
Expand All @@ -23,6 +24,7 @@ export class Route {
regex: RegExp;
nestingLevel: number;
urlPath: string;
directoryPaths: string[];

private paramExtractors: Record<string, ParamExtractor>;

Expand Down
45 changes: 31 additions & 14 deletions src/file-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { executeWithMiddlewares } from './components/middleware-executer';
import type { RequestHandler } from './types/request-handlers';
import type { Adapter } from './types/adapter';
import type { Route } from './components/route/route';
import type { Dictionary } from './types/dictionary';

interface InitParams {
routes: Route[];
Expand Down Expand Up @@ -49,28 +50,41 @@ export async function initFileRouter({
const notFoundHandler =
(await resolveNotFoundHandler(normalizedBaseDir)) || defaultNotFoundHandler;

function extractMiddlewares(pathname: string) {
return middlewares
.filter(({ regexp }) => regexp.test(`${pathname}/`))
.flatMap(({ handler }) => (Array.isArray(handler) ? handler : [handler]));
function extractMiddlewares(pathname: string, matchedRoute?: Route) {
let matched = middlewares;
Comment thread
Danilqa marked this conversation as resolved.
Resolved

if (matchedRoute) {
const allowedPaths = new Set(matchedRoute.directoryPaths);
matched = middlewares.filter(({ path: middlewarePath }) =>
allowedPaths.has(middlewarePath)
);
} else {
matched = middlewares.filter(({ regexp }) => regexp.test(pathname));
}

return matched.flatMap(({ handler }) =>
Array.isArray(handler) ? handler : [handler]
);
}

function runHandler<R>(
pathname: string,
method: string | undefined,
args: unknown[]
) {
const matchedRoute = routeHandlers.find(
function matchRoute(pathname: string, method: string | undefined) {
return routeHandlers.find(
({ regex, method: routeMethod }) =>
regex.test(pathname) && (!routeMethod || method === routeMethod)
);
}

function runHandler<R>(
method: string | undefined,
args: unknown[],
matchedRoute?: Route,
routeParams?: Dictionary<string>
) {
if (!matchedRoute) {
return notFoundHandler(...args);
}

const { handler } = matchedRoute;
const routeParams = matchedRoute.getRouteParams(pathname);

if (isRecordWith<RequestHandler>(handler) && method && handler[method]) {
return handler[method](...args, routeParams);
Expand Down Expand Up @@ -104,12 +118,15 @@ export async function initFileRouter({
return function requestHandler<R>(...args: unknown[]) {
const pathname = getPathname(...args);
const method = getMethod ? getMethod(...args) : undefined;
const matchedMiddlewares = extractMiddlewares(pathname);
const matchedRoute = matchRoute(pathname, method);
const routeParams = matchedRoute?.getRouteParams(pathname);
const matchedMiddlewares = extractMiddlewares(pathname, matchedRoute);

return executeWithMiddlewares<R>(
matchedMiddlewares,
() => runHandler<R>(pathname, method, args),
args
() => runHandler<R>(method, args, matchedRoute, routeParams),
args,
routeParams
);
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createTestRequestHandler } from '../../../../test-utils';

export default createTestRequestHandler(__filename, 'route-apples-index-get');
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createTestRequestHandler } from '../../../../test-utils';

export default createTestRequestHandler(__filename, 'route-apples-invite-post');
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createTestMiddlewareRequestHandler } from '../../../../test-utils';

export default createTestMiddlewareRequestHandler('m-dynamic-apples');
3 changes: 3 additions & 0 deletions tests/api-middlewares/static-vs-dynamic/[id]/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createTestMiddlewareRequestHandler } from '../../../test-utils';

export default createTestMiddlewareRequestHandler('m-dynamic-id');
3 changes: 3 additions & 0 deletions tests/api-middlewares/static-vs-dynamic/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createTestMiddlewareRequestHandler } from '../../test-utils';

export default createTestMiddlewareRequestHandler('m-root-static-vs-dynamic');
3 changes: 3 additions & 0 deletions tests/api-middlewares/static-vs-dynamic/static/test.[get].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createTestRequestHandler } from '../../../test-utils';

export default createTestRequestHandler(__filename, 'route-static-test-get');
Loading
Loading