diff --git a/.gitignore b/.gitignore index 001f728f..794b98a0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /.idea /.awcache /.vscode +/.history # misc npm-debug.log diff --git a/lib/filters/not-found-excepion.filter.ts b/lib/filters/not-found-excepion.filter.ts new file mode 100644 index 00000000..78ebb76c --- /dev/null +++ b/lib/filters/not-found-excepion.filter.ts @@ -0,0 +1,72 @@ +import * as fs from 'fs'; +import { validatePath } from '../utils/validate-path.util'; +import { + ArgumentsHost, + Catch, + HttpException, + NotFoundException +} from '@nestjs/common'; +import { ServeStaticModuleOptions } from '../interfaces/serve-static-options.interface'; +import { AbstractLoader } from '../loaders/abstract.loader'; +import { + DEFAULT_RENDER_PATH, + DEFAULT_ROOT_PATH + } from '../serve-static.constants'; +import { BaseExceptionFilter, HttpAdapterHost } from '@nestjs/core'; +import { wildcardToRegExp } from '../utils/wilcard-to-reg-exp.util'; + +@Catch(NotFoundException) +export class NotFoundExceptionFilter extends BaseExceptionFilter { + constructor( + protected httpAdapterHost: HttpAdapterHost, + private loader: AbstractLoader, + private optionsArr: ServeStaticModuleOptions[] + ) { + super(httpAdapterHost.httpAdapter); + } + + catch(exception: HttpException, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + const opts = this.isRenderPath(request); + + if( opts === undefined ){ + return super.catch(exception, host); + } + + opts.renderPath = opts.renderPath || DEFAULT_RENDER_PATH; + const clientPath = opts.rootPath || DEFAULT_ROOT_PATH; + const indexFilePath = this.loader.getIndexFilePath(clientPath); + + const stream = fs.createReadStream(indexFilePath); + if (opts.serveStaticOptions && opts.serveStaticOptions.setHeaders) { + const stat = fs.statSync(indexFilePath); + opts.serveStaticOptions.setHeaders(response, indexFilePath, stat); + } + response.type('text/html').send(stream); + } + + private isRenderPath(request): ServeStaticModuleOptions | undefined { + return this.optionsArr.find( opts => { + let renderPath: string | RegExp = opts.renderPath || DEFAULT_RENDER_PATH; + + if( opts.serveRoot ) { + renderPath = + typeof opts.serveRoot === 'string' + ? opts.serveRoot + validatePath(renderPath as string) + : opts.serveRoot; + } + + const re = renderPath instanceof RegExp ? renderPath : wildcardToRegExp(renderPath); + const queryParamsIndex = request.url.indexOf('?'); + const pathname = + queryParamsIndex >= 0 + ? request.url.slice(0, queryParamsIndex) + : request.url; + + return re.exec(pathname) ? true : false; + }); + } +} diff --git a/lib/loaders/fastify.loader.ts b/lib/loaders/fastify.loader.ts index 3b234394..04b69f40 100644 --- a/lib/loaders/fastify.loader.ts +++ b/lib/loaders/fastify.loader.ts @@ -27,41 +27,17 @@ export class FastifyLoader extends AbstractLoader { options.renderPath = options.renderPath || DEFAULT_RENDER_PATH; const clientPath = options.rootPath || DEFAULT_ROOT_PATH; - const indexFilePath = this.getIndexFilePath(clientPath); if (options.serveRoot) { app.register(fastifyStatic, { root: clientPath, ...(options.serveStaticOptions || {}), - wildcard: false, prefix: options.serveRoot }); - - const renderPath = - typeof options.serveRoot === 'string' - ? options.serveRoot + validatePath(options.renderPath as string) - : options.serveRoot; - - app.get(renderPath, (req: any, res: any) => { - const stream = fs.createReadStream(indexFilePath); - res.type('text/html').send(stream); - }); } else { app.register(fastifyStatic, { root: clientPath, - ...(options.serveStaticOptions || {}), - wildcard: false - }); - app.get(options.renderPath, (req: any, res: any) => { - const stream = fs.createReadStream(indexFilePath); - if ( - options.serveStaticOptions && - options.serveStaticOptions.setHeaders - ) { - const stat = fs.statSync(indexFilePath); - options.serveStaticOptions.setHeaders(res, indexFilePath, stat); - } - res.type('text/html').send(stream); + ...(options.serveStaticOptions || {}) }); } }); diff --git a/lib/serve-static.providers.ts b/lib/serve-static.providers.ts index 96257d02..e2ab4278 100644 --- a/lib/serve-static.providers.ts +++ b/lib/serve-static.providers.ts @@ -1,9 +1,12 @@ import { Provider } from '@nestjs/common'; -import { HttpAdapterHost } from '@nestjs/core'; +import { APP_FILTER, HttpAdapterHost } from '@nestjs/core'; +import { NotFoundExceptionFilter } from './filters/not-found-excepion.filter'; +import { ServeStaticModuleOptions } from './interfaces/serve-static-options.interface'; import { AbstractLoader } from './loaders/abstract.loader'; import { ExpressLoader } from './loaders/express.loader'; import { FastifyLoader } from './loaders/fastify.loader'; import { NoopLoader } from './loaders/noop.loader'; +import { SERVE_STATIC_MODULE_OPTIONS } from './serve-static.constants'; export const serveStaticProviders: Provider[] = [ { @@ -23,5 +26,13 @@ export const serveStaticProviders: Provider[] = [ return new ExpressLoader(); }, inject: [HttpAdapterHost] + }, + { + provide: APP_FILTER, + useFactory: (httpAdapterHost: HttpAdapterHost, loader: AbstractLoader, opts: ServeStaticModuleOptions[]) => { + if( loader instanceof FastifyLoader ) + return new NotFoundExceptionFilter(httpAdapterHost, loader, opts); + }, + inject: [HttpAdapterHost, AbstractLoader, SERVE_STATIC_MODULE_OPTIONS] } ]; diff --git a/lib/utils/reg-escape.util.ts b/lib/utils/reg-escape.util.ts new file mode 100644 index 00000000..44cc21e6 --- /dev/null +++ b/lib/utils/reg-escape.util.ts @@ -0,0 +1,5 @@ +/** + * Escapes all characters in the given string + */ +export const regExpEscape = (s: string) => + s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); diff --git a/lib/utils/wilcard-to-reg-exp.util.ts b/lib/utils/wilcard-to-reg-exp.util.ts new file mode 100644 index 00000000..99667883 --- /dev/null +++ b/lib/utils/wilcard-to-reg-exp.util.ts @@ -0,0 +1,11 @@ +import { regExpEscape } from "./reg-escape.util"; + +/** + * Creates a RegExp from the given string, allowing wildcards. + * + * "*" will be converted to ".*" + * "?" will be converted to "." + * + * Escapes the rest of the characters + */ +export const wildcardToRegExp = (s: string) => new RegExp('^' + s.split(/\*+/).map(regExpEscape).join('.*').replace(/\\\?/g, '.') + '$'); \ No newline at end of file