diff --git a/integration/hello-world/e2e/mvc.express.spec.ts b/integration/hello-world/e2e/mvc.express.spec.ts new file mode 100644 index 00000000000..af784725ea9 --- /dev/null +++ b/integration/hello-world/e2e/mvc.express.spec.ts @@ -0,0 +1,54 @@ +import { join } from 'path'; +import { INestApplication } from '@nestjs/common'; +import { ExpressAdapter } from '@nestjs/platform-express'; +import { Test } from '@nestjs/testing'; +import * as express from 'express'; +import * as request from 'supertest'; +import * as nunjucks from 'nunjucks'; +import { ApplicationModule } from '../src/app.module'; + +interface IExpressNestApplication extends INestApplication { + setBaseViewsDir(string): IExpressNestApplication + setViewEngine(string): IExpressNestApplication +} + +describe('Hello world MVC', () => { + let server; + let app: IExpressNestApplication; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + imports: [ApplicationModule], + }).compile(); + + const expressApp = express(); + nunjucks.configure(join(__dirname, '..', 'src', 'views'), { + autoescape: true, + express: expressApp + }); + + app = module.createNestApplication(new ExpressAdapter(expressApp)); + app.setViewEngine('njk') + server = app.getHttpServer(); + await app.init(); + }); + + it(`/GET`, () => { + return request(server) + .get('/hello/mvc') + .expect(200) + .expect(/href="\/hello\/mvc/) + }); + + it(`/GET/:id`, () => { + const id = 5; + return request(server) + .get(`/hello/mvc/${id}`) + .expect(200) + .expect(new RegExp(`href="/hello/mvc/${id}`)) + }); + + afterEach(async () => { + await app.close(); + }); +}); diff --git a/integration/hello-world/src/hello/hello.controller.ts b/integration/hello-world/src/hello/hello.controller.ts index be715e1471b..378e18c7d58 100644 --- a/integration/hello-world/src/hello/hello.controller.ts +++ b/integration/hello-world/src/hello/hello.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Header, Param } from '@nestjs/common'; +import { Controller, Get, Header, Param, Render, WithAlias } from '@nestjs/common'; import { Observable, of } from 'rxjs'; import { HelloService } from './hello.service'; import { UserByIdPipe } from './users/user-by-id.pipe'; @@ -30,4 +30,24 @@ export class HelloController { ): any { return user; } + + @Get('mvc') + @Render('mvc') + mvc() { + return { message: 'Hello World!' } + } + + @Get('mvc-alias') + @WithAlias('mvc') + @Render('mvc') + mvcAliased() { + return { message: 'Hello World!' } + } + + @Get('mvc/:id') + @WithAlias('mvc-id') + @Render('mvc-id') + mvcAliasedWithId(@Param('id') id) { + return { message: 'Hello World!', id } + } } diff --git a/integration/hello-world/src/views/mvc-id.njk b/integration/hello-world/src/views/mvc-id.njk new file mode 100644 index 00000000000..fece9ad9bae --- /dev/null +++ b/integration/hello-world/src/views/mvc-id.njk @@ -0,0 +1,16 @@ + + + + + + App + + + +

{{ message }}

+
+ Aliased 'mvc-id' With ID: {{ getUrl('mvc-id', { id: id }) }} +
+ + + \ No newline at end of file diff --git a/integration/hello-world/src/views/mvc.njk b/integration/hello-world/src/views/mvc.njk new file mode 100644 index 00000000000..7afc7435336 --- /dev/null +++ b/integration/hello-world/src/views/mvc.njk @@ -0,0 +1,16 @@ + + + + + + App + + + +

{{ message }}

+
+ Aliased 'mvc' to {{ getUrl('mvc') }} +
+ + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c188b670182..e9c758e0ce4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4499,6 +4499,12 @@ "through": ">=2.2.7 <3" } }, + "a-sync-waterfall": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", + "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", + "dev": true + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -20186,6 +20192,161 @@ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true }, + "nunjucks": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.1.tgz", + "integrity": "sha512-LYlVuC1ZNSalQQkLNNPvcgPt2M9FTY9bs39mTCuFXtqh7jWbYzhDlmz2M6onPiXEhdZo+b9anRhc+uBGuJZ2bQ==", + "dev": true, + "requires": { + "a-sync-waterfall": "^1.0.0", + "asap": "^2.0.3", + "chokidar": "^3.3.0", + "commander": "^3.0.2" + }, + "dependencies": { + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "optional": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "dev": true, + "optional": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "optional": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chokidar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.1.tgz", + "integrity": "sha512-TQTJyr2stihpC4Sya9hs2Xh+O2wf+igjL36Y75xx2WdHuiICcn/XJza46Jwt0eT5hVpQOzo3FpY3cj3RVYLX0g==", + "dev": true, + "optional": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.4.0" + } + }, + "commander": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", + "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "optional": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "optional": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "optional": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "optional": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "optional": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "optional": true + }, + "readdirp": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", + "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", + "dev": true, + "optional": true, + "requires": { + "picomatch": "^2.2.1" + }, + "dependencies": { + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true, + "optional": true + } + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "optional": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, "nyc": { "version": "15.1.0", "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", diff --git a/package.json b/package.json index 06f7a56431e..57e45da06b0 100644 --- a/package.json +++ b/package.json @@ -147,6 +147,7 @@ "mysql": "2.18.1", "nats": "1.4.9", "nodemon": "2.0.4", + "nunjucks": "^3.2.1", "nyc": "15.1.0", "prettier": "2.0.5", "redis": "3.0.2", diff --git a/packages/common/constants.ts b/packages/common/constants.ts index 377b76b8b83..823b46141dc 100644 --- a/packages/common/constants.ts +++ b/packages/common/constants.ts @@ -26,3 +26,4 @@ export const HTTP_CODE_METADATA = '__httpCode__'; export const MODULE_PATH = '__module_path__'; export const HEADERS_METADATA = '__headers__'; export const REDIRECT_METADATA = '__redirect__'; +export const ROUTE_ALIAS_METADATA = '__route_alias__'; \ No newline at end of file diff --git a/packages/common/decorators/http/index.ts b/packages/common/decorators/http/index.ts index 83eed87da7c..dedf8487af3 100644 --- a/packages/common/decorators/http/index.ts +++ b/packages/common/decorators/http/index.ts @@ -5,3 +5,4 @@ export * from './create-route-param-metadata.decorator'; export * from './render.decorator'; export * from './header.decorator'; export * from './redirect.decorator'; +export * from './route-alias.decorator'; diff --git a/packages/common/decorators/http/route-alias.decorator.ts b/packages/common/decorators/http/route-alias.decorator.ts new file mode 100644 index 00000000000..9bdb3161881 --- /dev/null +++ b/packages/common/decorators/http/route-alias.decorator.ts @@ -0,0 +1,23 @@ +import { ROUTE_ALIAS_METADATA } from '../../constants'; + +/** + * Alias for the route, which can be resolved to the full route path + * + * For example: `@WithAlias('alias')` + * + * @param routeAlias alias for the route + * + * @see [Model-View-Controller](https://docs.nestjs.com/techniques.mvc) + * + * @publicApi + */ +export function WithAlias(routeAlias: string | Symbol): MethodDecorator { + return ( + target: object, + key: string | symbol, + descriptor: TypedPropertyDescriptor, + ) => { + Reflect.defineMetadata(ROUTE_ALIAS_METADATA, routeAlias, descriptor.value); + return descriptor; + }; +} diff --git a/packages/common/test/decorators/route-alias.decorator.spec.ts b/packages/common/test/decorators/route-alias.decorator.spec.ts new file mode 100644 index 00000000000..76ab2988962 --- /dev/null +++ b/packages/common/test/decorators/route-alias.decorator.spec.ts @@ -0,0 +1,17 @@ +import { expect } from 'chai'; +import { WithAlias } from '../../decorators/http/route-alias.decorator'; +import { ROUTE_ALIAS_METADATA } from '../../constants'; + +describe('@WithAlias', () => { + const alias = 'alias'; + + class Test { + @WithAlias(alias) + public static test() {} + } + + it('should enhance method with expected alias string', () => { + const metadata = Reflect.getMetadata(ROUTE_ALIAS_METADATA, Test.test); + expect(metadata).to.be.eql(alias); + }); +}); diff --git a/packages/core/router/route-alias-resolver.ts b/packages/core/router/route-alias-resolver.ts new file mode 100644 index 00000000000..c2a8b506acb --- /dev/null +++ b/packages/core/router/route-alias-resolver.ts @@ -0,0 +1,49 @@ +import { isObject } from '@nestjs/common/utils/shared.utils'; + +export class RouteAliasResolver { + private readonly aliasMap: Map + constructor() { + this.aliasMap = new Map() + } + public register(alias: string | Symbol, basePath: string, path: string[]) { + if (this.aliasMap.has(alias)) { + throw new Error(`Conflict ${alias} already registered`); + } + this.aliasMap.set(alias, this.createPath(basePath, path)); + } + public createResolveFn() { + const resolveFn = this.resolve.bind(this); + return function(alias: string | Symbol, params?: object) { + return resolveFn(alias, params) + } + } + private resolve(alias: string | Symbol, params?: object): string { + if (!this.aliasMap.has(alias)) { + throw new Error(`Not Found: ${alias} not registered`); + } + + return this.aliasMap.get(alias).reduce((path, part) => { + if (part.startsWith(':') && isObject(params)) { + const paramValue = params[part.replace(':', '')]; + return path + '/' + (paramValue !== undefined ? paramValue : part); + } + return part ? path + '/' + part : path; + }, ''); + } + private createPath(basePath: string, path: string[]): string[] { + const base = basePath ? [this.stripSlashes(basePath)] : []; + return base.concat(this.splitPath(path)) + } + private splitPath(path: string[]): string[] { + const pathParts = []; + path.forEach(part => { + part.split('/').forEach(partial => { + partial && pathParts.push(this.stripSlashes(partial)); + }); + }); + return pathParts; + } + private stripSlashes(str: string) { + return str.replace(/^\/?(.*)\/?$/, '$1') + } +} \ No newline at end of file diff --git a/packages/core/router/router-execution-context.ts b/packages/core/router/router-execution-context.ts index 32839566879..17be8166b51 100644 --- a/packages/core/router/router-execution-context.ts +++ b/packages/core/router/router-execution-context.ts @@ -38,6 +38,7 @@ import { RedirectResponse, RouterResponseController, } from './router-response-controller'; +import { RouteAliasResolver } from './route-alias-resolver'; export interface ParamProperties { index: number; @@ -55,6 +56,7 @@ export class RouterExecutionContext { private readonly handlerMetadataStorage = new HandlerMetadataStorage(); private readonly contextUtils = new ContextUtils(); private readonly responseController: RouterResponseController; + private readonly aliasResolver: RouteAliasResolver; constructor( private readonly paramsFactory: IRouteParamsFactory, @@ -67,6 +69,7 @@ export class RouterExecutionContext { readonly applicationRef: HttpServer, ) { this.responseController = new RouterResponseController(applicationRef); + this.aliasResolver = new RouteAliasResolver(); } public create( @@ -164,6 +167,11 @@ export class RouterExecutionContext { }; } + public bindResponseLocals(response) { + response.locals = response.locals || {}; + response.locals.getUrl = this.aliasResolver.createResolveFn() + } + public getMetadata( instance: Controller, callback: (...args: any[]) => any, @@ -265,6 +273,14 @@ export class RouterExecutionContext { return Reflect.getMetadata(HEADERS_METADATA, callback) || []; } + public registerAlias( + alias: string | Symbol, + basePath: string, + path: string[] + ): void { + this.aliasResolver.register(alias, basePath, path); + } + public exchangeKeysForValues( keys: string[], metadata: Record, @@ -402,6 +418,7 @@ export class RouterExecutionContext { const renderTemplate = this.reflectRenderTemplate(callback); if (renderTemplate) { return async (result: TResult, res: TResponse) => { + this.bindResponseLocals(res); await this.responseController.render(result, res, renderTemplate); }; } diff --git a/packages/core/router/router-explorer.ts b/packages/core/router/router-explorer.ts index 42ce0f40756..cd56077c692 100644 --- a/packages/core/router/router-explorer.ts +++ b/packages/core/router/router-explorer.ts @@ -1,5 +1,5 @@ import { HttpServer } from '@nestjs/common'; -import { METHOD_METADATA, PATH_METADATA } from '@nestjs/common/constants'; +import { METHOD_METADATA, PATH_METADATA, ROUTE_ALIAS_METADATA } from '@nestjs/common/constants'; import { RequestMethod } from '@nestjs/common/enums/request-method.enum'; import { InternalServerErrorException } from '@nestjs/common/exceptions'; import { Controller } from '@nestjs/common/interfaces/controllers/controller.interface'; @@ -40,6 +40,7 @@ export interface RoutePathProperties { requestMethod: RequestMethod; targetCallback: RouterProxyCallback; methodName: string; + routeAlias?: string | Symbol; } export class RouterExplorer { @@ -133,6 +134,10 @@ export class RouterExplorer { METHOD_METADATA, targetCallback, ); + const routeAlias: string | Symbol = Reflect.getMetadata( + ROUTE_ALIAS_METADATA, + targetCallback, + ); const path = isString(routePath) ? [this.validateRoutePath(routePath)] : routePath.map(p => this.validateRoutePath(p)); @@ -141,6 +146,7 @@ export class RouterExplorer { requestMethod, targetCallback, methodName, + routeAlias, }; } @@ -153,7 +159,7 @@ export class RouterExplorer { host: string, ) { (routePaths || []).forEach(pathProperties => { - const { path, requestMethod } = pathProperties; + const { path, requestMethod, routeAlias } = pathProperties; this.applyCallbackToRouter( router, pathProperties, @@ -162,6 +168,7 @@ export class RouterExplorer { basePath, host, ); + routeAlias && this.executionContextCreator.registerAlias(routeAlias, basePath, path); path.forEach(item => { const pathStr = this.stripEndSlash(basePath) + this.stripEndSlash(item); this.logger.log(ROUTE_MAPPED_MESSAGE(pathStr, requestMethod)); diff --git a/packages/core/test/router/route-alias-resolver.spec.ts b/packages/core/test/router/route-alias-resolver.spec.ts new file mode 100644 index 00000000000..35e3e853b4b --- /dev/null +++ b/packages/core/test/router/route-alias-resolver.spec.ts @@ -0,0 +1,48 @@ +import { RouteAliasResolver } from '../../router/route-alias-resolver'; +import { expect } from 'chai'; + +describe('RouteAliasResolver', () => { + + let aliasResolver: RouteAliasResolver; + + beforeEach(() => { + aliasResolver = new RouteAliasResolver(); + }); + + describe('registering aliases', () => { + it('should throw if attempting to override alias', () => { + aliasResolver.register('foo', '', ['bar']); + expect(() => aliasResolver.register('foo', '', ['bar'])).to.throw + }); + }); + + describe('resolving aliases', () => { + it('should include base path in resolution', () => { + const alias = 'foo'; + aliasResolver.register(alias, '/bar', ['baz']); + const resolver = aliasResolver.createResolveFn(); + expect(resolver(alias)).to.be.eq('/bar/baz') + }); + + it('should interpolate route parameters', () => { + const alias = 'foo'; + aliasResolver.register(alias, '/bar', ['baz/:id']); + const resolver = aliasResolver.createResolveFn(); + expect(resolver(alias, { id: 1 })).to.be.eq('/bar/baz/1') + }); + + it('should leave route parameter declaration if no value given', () => { + const alias = 'foo'; + aliasResolver.register(alias, '/bar', ['baz/:id']); + const resolver = aliasResolver.createResolveFn(); + expect(resolver(alias)).to.be.eq('/bar/baz/:id') + }); + + it('should resolve aliases with Symbols', () => { + const alias = Symbol('foo'); + aliasResolver.register(alias, '/bar', ['baz']); + const resolver = aliasResolver.createResolveFn(); + expect(resolver(alias)).to.be.eq('/bar/baz') + }); + }); +}); \ No newline at end of file diff --git a/sample/15-mvc/src/app.controller.ts b/sample/15-mvc/src/app.controller.ts index 6c6dfd2f0e8..00bbae6649e 100644 --- a/sample/15-mvc/src/app.controller.ts +++ b/sample/15-mvc/src/app.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Render } from '@nestjs/common'; +import { Controller, Get, Render, WithAlias } from '@nestjs/common'; @Controller() export class AppController { @@ -7,4 +7,18 @@ export class AppController { root() { return { message: 'Hello world!' }; } + + @Get('/alias') + @WithAlias('aliased') + @Render('aliased') + aliased() { + return { message: 'Hello world!' }; + } + + @Get('/alias/:id') + @WithAlias('aliased_id') + @Render('aliased') + aliasedWithId() { + return { message: 'Hello world!' }; + } } diff --git a/sample/15-mvc/src/main.ts b/sample/15-mvc/src/main.ts index 48ec3a3aa62..8b6eda569b2 100644 --- a/sample/15-mvc/src/main.ts +++ b/sample/15-mvc/src/main.ts @@ -2,13 +2,17 @@ import { NestFactory } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; import { join } from 'path'; import { AppModule } from './app.module'; +import * as nunjucks from 'nunjucks' async function bootstrap() { const app = await NestFactory.create(AppModule); - app.useStaticAssets(join(__dirname, '..', 'public')); app.setBaseViewsDir(join(__dirname, '..', 'views')); - app.setViewEngine('hbs'); + app.setViewEngine('njk') + nunjucks.configure('views', { + autoescape: true, + express: app + }); await app.listen(3000); console.log(`Application is running on: ${await app.getUrl()}`); diff --git a/sample/15-mvc/views/aliased.njk b/sample/15-mvc/views/aliased.njk new file mode 100644 index 00000000000..06633f562dc --- /dev/null +++ b/sample/15-mvc/views/aliased.njk @@ -0,0 +1,19 @@ + + + + + + App + + + +

{{ message }}

+ + + + + \ No newline at end of file diff --git a/sample/15-mvc/views/index.hbs b/sample/15-mvc/views/index.njk similarity index 100% rename from sample/15-mvc/views/index.hbs rename to sample/15-mvc/views/index.njk