Skip to content

Commit

Permalink
Merge pull request #96 from yakovlev-alexey/next-12-fix-routing
Browse files Browse the repository at this point in the history
  • Loading branch information
kyle-mccarthy authored Jan 30, 2023
2 parents a837101 + 74e42f2 commit a9e2651
Show file tree
Hide file tree
Showing 13 changed files with 363 additions and 15 deletions.
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
- [Examples folder structure](#examples-folder-structure)
- [Basic Setup](#basic-setup)
- [Monorepo](#monorepo)
- [Versioning](#versioning)
- [Known Issues](#known-issues)
- [Contributing](#contributing)
- [License](#license)

<!-- vim-markdown-toc -->
Expand Down Expand Up @@ -100,7 +101,7 @@ Next 9 added [built-in zero-config typescript support](https://nextjs.org/blog/n

If you are having issues with unexpected tokens, files not emitting when building for production, warnings about `allowJs` and `declaration` not being used together, and other typescript related errors; see the `tsconfig.server.json` [file in the example project](/examples/basic/tsconfig.server.json) for the full config.

#### Pass-through 404s
#### Pass-through 404s

Instead of having Nest handle the response for requests that 404, they can be forwarded to Next's request handler.

Expand Down Expand Up @@ -295,11 +296,18 @@ outside of both projects. Changes in it during "dev" runs trigger recompilation
/IndexPage.ts
package.json


To run this project, the "ui" and "server" project must be built, in any order. The "dto" project will be implicitly built by the "server" project. After both of those builds, the "server" project can be started in either dev or production mode.

It is important that "ui" references to "dto" refer to the TypeScript files (.ts files in the "src" folder), and NOT the declaration files (.d.ts files in the "dist" folder), due to how Next not being compiled in the same fashion as the server.

### Known issues

Currently Next ["catch all routes"](https://nextjs.org/docs/routing/dynamic-routes#catch-all-routes) pages do not work correctly. See issue [#101](https://github.com/kyle-mccarthy/nest-next/issues/101) for details.

### Contributing

To contribute make sure your changes pass the test suite. To run test suite `docker`, `docker-compose` are required. Run `npm run test` to run tests. Playwright will be installed with required packages. To run tests in Next development mode run `npm run test-dev`. You can also specify `NODE_VERSION` and major `NEXT_VERSION` variables to run tests in specific environments.

### License

MIT License
Expand Down
16 changes: 14 additions & 2 deletions lib/render.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { DynamicModule, Module } from '@nestjs/common';
import { ApplicationConfig, HttpAdapterHost } from '@nestjs/core';

import Server from 'next';
import type { DynamicRoutes } from 'next/dist/server/router';

import { RenderFilter } from './render.filter';
import { RenderService } from './render.service';
import { RendererConfig } from './types';

import type { RendererConfig } from './types';

@Module({
providers: [RenderService],
Expand All @@ -30,7 +34,15 @@ export class RenderModule {
? nextConfig.basePath
: nextServer.nextConfig.basePath;

const config = { basePath, ...options };
const dynamicRoutes = (nextServer.dynamicRoutes as DynamicRoutes).map(
(route) => route.page,
);

const config: Partial<RendererConfig> = {
basePath,
dynamicRoutes,
...options,
};

return {
exports: [RenderService],
Expand Down
65 changes: 56 additions & 9 deletions lib/render.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import {
RequestHandler,
} from './types';

import { getNamedRouteRegex } from './vendor/next/route-regex';
import { interpolateDynamicPath } from './vendor/next/interpolate-dynamic-path';
import { isDynamicRoute } from './vendor/next/is-dynamic';

export class RenderService {
public static init(
config: Partial<RendererConfig>,
Expand Down Expand Up @@ -38,6 +42,10 @@ export class RenderService {
passthrough404: false,
viewsDir: '/views',
};
private dynamicRouteRegexes = new Map<
string,
ReturnType<typeof getNamedRouteRegex>
>();

/**
* Merge the default config with the config obj passed to method
Expand All @@ -56,6 +64,9 @@ export class RenderService {
if (typeof config.basePath === 'string') {
this.config.basePath = config.basePath;
}
if (config.dynamicRoutes?.length) {
this.initializeDynamicRouteRegexes(config.dynamicRoutes);
}
}

/**
Expand Down Expand Up @@ -167,6 +178,14 @@ export class RenderService {
return isInternalUrl(url);
}

public initializeDynamicRouteRegexes(routes: string[] = []) {
for (const route of routes) {
const pathname = this.getNormalizedPath(route);

this.dynamicRouteRegexes.set(route, getNamedRouteRegex(pathname));
}
}

/**
* Check if the service has been initialized by the module
*/
Expand Down Expand Up @@ -198,7 +217,7 @@ export class RenderService {
if (isFastify) {
response.sent = true;
}
return renderer(req, res, getViewPath(view), data);
return renderer(req, res, getViewPath(view, req.params), data);
} else if (!renderer) {
throw new InternalServerErrorException(
'RenderService: renderer is not set',
Expand Down Expand Up @@ -228,7 +247,10 @@ export class RenderService {
if (isFastifyAdapter) {
server
.getInstance()
.decorateReply('render', function(view: string, data?: ParsedUrlQuery) {
.decorateReply('render', function (
view: string,
data?: ParsedUrlQuery,
) {
const res = this.res;
const req = this.request.raw;

Expand All @@ -240,7 +262,7 @@ export class RenderService {

this.sent = true;

return renderer(req, res, getViewPath(view), data);
return renderer(req, res, getViewPath(view, req.params), data);
} as RenderableResponse['render']);
} else {
server.getInstance().use((req: any, res: any, next: () => any) => {
Expand All @@ -251,21 +273,46 @@ export class RenderService {
);
}

return renderer(req, res, getViewPath(view), data);
return renderer(req, res, getViewPath(view, req.params), data);
}) as RenderableResponse['render'];

next();
});
}
}

public getNormalizedPath(view: string) {
const basePath = this.getViewsDir() ?? '';
const denormalizedPath = [basePath, view].join(
view.startsWith('/') ? '' : '/',
);

const pathname = path.posix.normalize(denormalizedPath);

return pathname;
}

/**
* Format the path to the view
* Format the path to the view including path parameters interpolation
* Copied Next.js code is used for interpolation
* @param view
* @param params
*/
protected getViewPath(view: string) {
const baseDir = this.getViewsDir();
const basePath = baseDir ? baseDir : '';
return path.posix.normalize(`${basePath}/${view}`);
protected getViewPath(view: string, params: ParsedUrlQuery) {
const pathname = this.getNormalizedPath(view);

if (!isDynamicRoute(pathname)) {
return pathname;
}

const regex = this.dynamicRouteRegexes.get(pathname);

if (!regex) {
console.warn(
`RenderService: view ${view} is dynamic and has no route regex`,
);
}

return interpolateDynamicPath(pathname, params, regex);
}
}
1 change: 1 addition & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface RendererConfig {
basePath?: string;
dev: boolean;
passthrough404?: boolean;
dynamicRoutes?: string[];
}

export interface ErrorResponse {
Expand Down
21 changes: 21 additions & 0 deletions lib/vendor/next/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2022 Vercel, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
11 changes: 11 additions & 0 deletions lib/vendor/next/escape-regexp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// regexp is based on https://github.com/sindresorhus/escape-string-regexp
const reHasRegExp = /[|\\{}()[\]^$+*?.-]/;
const reReplaceRegExp = /[|\\{}()[\]^$+*?.-]/g;

export function escapeStringRegexp(str: string) {
// see also: https://github.com/lodash/lodash/blob/2da024c3b4f9947a48517639de7560457cd4ec6c/escapeRegExp.js#L23
if (reHasRegExp.test(str)) {
return str.replace(reReplaceRegExp, '\\$&');
}
return str;
}
41 changes: 41 additions & 0 deletions lib/vendor/next/interpolate-dynamic-path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ParsedUrlQuery } from 'querystring';
import { getNamedRouteRegex } from './route-regex';

export function interpolateDynamicPath(
pathname: string,
params: ParsedUrlQuery,
defaultRouteRegex?: ReturnType<typeof getNamedRouteRegex> | undefined,
) {
if (!defaultRouteRegex) return pathname;

for (const param of Object.keys(defaultRouteRegex.groups)) {
const { optional, repeat } = defaultRouteRegex.groups[param];
let builtParam = `[${repeat ? '...' : ''}${param}]`;

if (optional) {
builtParam = `[${builtParam}]`;
}

const paramIdx = pathname!.indexOf(builtParam);

if (paramIdx > -1) {
let paramValue: string;
const value = params[param];

if (Array.isArray(value)) {
paramValue = value.map((v) => v && encodeURIComponent(v)).join('/');
} else if (value) {
paramValue = encodeURIComponent(value);
} else {
paramValue = '';
}

pathname =
pathname.slice(0, paramIdx) +
paramValue +
pathname.slice(paramIdx + builtParam.length);
}
}

return pathname;
}
6 changes: 6 additions & 0 deletions lib/vendor/next/is-dynamic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Identify /[param]/ in route string
const TEST_ROUTE = /\/\[[^/]+?\](?=\/|$)/;

export function isDynamicRoute(route: string): boolean {
return TEST_ROUTE.test(route);
}
10 changes: 10 additions & 0 deletions lib/vendor/next/remove-trailing-slash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Removes the trailing slash for a given route or page path. Preserves the
* root page. Examples:
* - `/foo/bar/` -> `/foo/bar`
* - `/foo/bar` -> `/foo/bar`
* - `/` -> `/`
*/
export function removeTrailingSlash(route: string) {
return route.replace(/\/$/, '') || '/';
}
Loading

0 comments on commit a9e2651

Please sign in to comment.