Skip to content

Commit

Permalink
Feature/519338 redirect nextjs (#969)
Browse files Browse the repository at this point in the history
* #519338: implement redirect service and middleware

* #519338 refactoring redirects service. added unit tests

* #519338 moved redirects service into sitecore-jss

* #519338 fixed graphql-request for nextJs middleware and tests

* #519338 modified yarn.lock

* #519338 fixed test for sitecore-jss-nextjs

* #519338 refactoring: moved constants

* #519338 refactoring and fixed comments

* #519338 update yarn.lock

* #519338 fixed lint

* #519338 fixed comments and added unit-test

* #519338 added unit test for middleware redirects

* #519338 fixed lint error and yarn.lock

* #519338 fixed yarn.lock

* #519338 removed unit-test of redirects middleware and fixed yarn.lock

* #519338 added isomorphic-fetch for run pipline

* #519338 removed unused package

* #519338 removed test(override fetch)
  • Loading branch information
sc-ruslanmatkovskyi authored Apr 13, 2022
1 parent 214a396 commit b963d40
Show file tree
Hide file tree
Showing 21 changed files with 364 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ const port = process.env.PORT || 3000;
const configOverride: { [key: string]: string } = {};
if (disconnected) {
if (process.env.FETCH_WITH === constants.FETCH_WITH.GRAPHQL) {
throw new Error(chalk.red('GraphQL requests to Dictionary and Layout services are not supported in disconnected mode.'))
throw new Error(
chalk.red(
'GraphQL requests to Dictionary and Layout services are not supported in disconnected mode.'
)
);
}
configOverride.sitecoreApiHost = `http://localhost:${port}`;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Describes a file that represents a component definition
*/
export interface ComponentFile {
export interface ComponentFile {
path: string;
moduleName: string;
componentName: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ const Layout = ({ layoutData }: LayoutProps): JSX.Element => {
<Navigation />
{/* root placeholder for the app, which we add components to using route data */}
<div className="container">
{route && <Placeholder name="<%- helper.getAppPrefix(appPrefix, appName) %>jss-main" rendering={route} />}
{route && (
<Placeholder
name="<%- helper.getAppPrefix(appPrefix, appName) %>jss-main"
rendering={route}
/>
)}
</div>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const Navigation = (): JSX.Element => (
<nav>
<ul>
<li>
<a href="https://sitecore.com">
<a href="https://sitecore.com">
<img src={`${publicUrl}/sc_logo.svg`} alt="Sitecore" />
</a>
</li>
Expand All @@ -24,4 +24,4 @@ const Navigation = (): JSX.Element => (
</div>
);

export default Navigation;
export default Navigation;
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ export interface MiddlewarePlugin {
/**
* A middleware to be called, it's required to return @type {NextResponse} for other middlewares
*/
(req: NextRequest, res: NextResponse, ev: NextFetchEvent): Promise<NextResponse>;
exec(req: NextRequest, res?: NextResponse, ev?: NextFetchEvent): Promise<NextResponse>;
}

export default async function middleware(req: NextRequest, ev: NextFetchEvent): Promise<NextResponse> {
export default async function middleware(
req: NextRequest,
ev: NextFetchEvent
): Promise<NextResponse> {
const response = NextResponse.next();

return (Object.values(plugins) as MiddlewarePlugin[])
.sort((p1, p2) => p1.order - p2.order)
.reduce((p, plugin) => p.then((res) => plugin(req, res, ev)), Promise.resolve(response));
.reduce((p, plugin) => p.then((res) => plugin.exec(req, res, ev)), Promise.resolve(response));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { NextRequest, NextResponse } from 'next/server';
import { RedirectsMiddleware } from '@sitecore-jss/sitecore-jss-nextjs';
import config from 'temp/config';
import { MiddlewarePlugin } from '..';

class RedirectsPlugin implements MiddlewarePlugin {
private redirectsMiddleware: RedirectsMiddleware;
order = 0;

constructor() {
this.redirectsMiddleware = new RedirectsMiddleware({
endpoint: config.graphQLEndpoint,
apiKey: config.sitecoreApiKey,
siteName: config.jssAppName,
});
}

/**
* exec async method - to find coincidence in url.pathname and redirects of site
* @param req<NextRequest>
* @returns Promise<NextResponse>
*/
async exec(req: NextRequest): Promise<NextResponse> {
return this.redirectsMiddleware.getHandler(req);
}
}

export const redirectsPlugin = new RedirectsPlugin();
3 changes: 2 additions & 1 deletion packages/sitecore-jss-nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"build": "npm run clean && tsc -p tsconfig.json && tsc -p tsconfig-esm.json",
"clean": "del-cli dist types",
"lint": "eslint ./src/**/*.tsx ./src/**/*.ts",
"test": "mocha --require ts-node/register/transpile-only --require ./src/tests/shim.ts ./src/tests/jsdom-setup.ts ./src/tests/enzyme-setup.ts \"./src/**/*.test.ts\" \"./src/**/*.test.tsx\" --exit",
"test": "mocha --require ./test/setup.js \"./src/**/*.test.ts\" \"./src/**/*.test.tsx\" --exit",
"prepublishOnly": "npm run build",
"coverage": "nyc npm test",
"generate-docs": "npx typedoc --plugin typedoc-plugin-markdown --readme none --out ../../ref-docs/sitecore-jss-nextjs src/index.ts --githubPages false"
Expand Down Expand Up @@ -72,6 +72,7 @@
"@sitecore-jss/sitecore-jss-dev-tools": "^21.0.0-canary.28",
"@sitecore-jss/sitecore-jss-react": "^21.0.0-canary.28",
"prop-types": "^15.7.2",
"regex-parser": "^2.2.11",
"sync-disk-cache": "^2.1.0"
},
"description": "",
Expand Down
1 change: 1 addition & 0 deletions packages/sitecore-jss-nextjs/src/middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export {
EditingRenderMiddleware,
EditingRenderMiddlewareConfig,
} from './editing-render-middleware';
export { RedirectsMiddleware } from './redirects-middleware';
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import regexParser from 'regex-parser';
import { NextResponse, NextRequest } from 'next/server';
import {
RedirectInfo,
GraphQLRedirectsService,
GraphQLRedirectsServiceConfig,
REDIRECT_TYPE_301,
REDIRECT_TYPE_302,
REDIRECT_TYPE_SERVER_TRANSFER,
} from '@sitecore-jss/sitecore-jss/site';

/**
* extended RedirectsMiddlewareConfig config type for RedirectsMiddleware
*/
export type RedirectsMiddlewareConfig = Omit<GraphQLRedirectsServiceConfig, 'fetch'>;

/**
* Middleware / handler fetches all redirects from Sitecore instance by grapqhl service
* compares with current url and redirects to target url
*/
export class RedirectsMiddleware {
private redirectsService: GraphQLRedirectsService;

/**
* NOTE: we provide native fetch for compatibility on Next.js Edge Runtime
* (underlying default 'cross-fetch' is not currently compatible: https://github.com/lquixada/cross-fetch/issues/78)
* @param {RedirectsMiddlewareConfig} [config] redirects middleware config
*/
constructor(config: RedirectsMiddlewareConfig) {
this.redirectsService = new GraphQLRedirectsService({ ...config, fetch: fetch });
}

/**
* Gets the Next.js API route handler
* @returns route handler
*/
public getHandler(): (req: NextRequest) => Promise<NextResponse> {
return this.handler;
}

private handler = async (req: NextRequest): Promise<NextResponse> => {
const url = req.nextUrl.clone();
// Find the redirect from result of RedirectService

const existsRedirect = await this.getExistsRedirect(url);

if (!existsRedirect) {
return NextResponse.next();
}

url.search = existsRedirect.isQueryStringPreserved ? url.search : '';
url.pathname = existsRedirect.target;

/** return Response redirect with http code of redirect type **/
switch (existsRedirect.redirectType) {
case REDIRECT_TYPE_301:
return NextResponse.redirect(url, 301);
case REDIRECT_TYPE_302:
return NextResponse.redirect(url, 302);
case REDIRECT_TYPE_SERVER_TRANSFER:
return NextResponse.rewrite(url);
default:
return NextResponse.next();
}
};

/**
* Method returns RedirectInfo when matches
* @param url
* @return Promise<RedirectInfo>
* @private
*/
private async getExistsRedirect(url: URL): Promise<RedirectInfo | undefined> {
const redirects = await this.redirectsService.fetchRedirects();

return redirects.find((redirect: RedirectInfo) =>
regexParser(redirect.pattern).test(url.pathname)
);
}
}
4 changes: 4 additions & 0 deletions packages/sitecore-jss-nextjs/test/setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
require('ts-node/register/transpile-only');
require('../src/tests/shim.ts');
require('../src/tests/jsdom-setup.ts');
require('../src/tests/enzyme-setup.ts');
4 changes: 3 additions & 1 deletion packages/sitecore-jss-proxy/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ async function renderAppToResponse(
* function replies with HTTP 500 when an error occurs
* @param {Error} error
*/
async function replyWithError(error: Error) {
async function replyWithError(error: Error): Promise<void> {
console.error(error);

let errorResponse = {
Expand Down Expand Up @@ -279,6 +279,8 @@ async function renderAppToResponse(

// as the response is ending, we parse the current response body which is JSON, then
// render the app using that JSON, but return HTML to the final response.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
serverResponse.end = async () => {
try {
const layoutServiceData = await extractLayoutServiceDataFromProxyResponse();
Expand Down
2 changes: 2 additions & 0 deletions packages/sitecore-jss/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ export const JSS_MODE = {
CONNECTED: 'connected',
DISCONNECTED: 'disconnected',
};

export const siteNameError = 'The siteName cannot be empty';
1 change: 1 addition & 0 deletions packages/sitecore-jss/src/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ export default Object.freeze({
experienceEditor: debug(`${rootNamespace}:editing`),
sitemap: debug(`${rootNamespace}:sitemap`),
robots: debug(`${rootNamespace}:robots`),
redirects: debug(`${rootNamespace}:redirects`),
});
1 change: 0 additions & 1 deletion packages/sitecore-jss/src/graphql-request-client.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable no-unused-expressions */

import { expect, use, spy } from 'chai';
import spies from 'chai-spies';
import nock from 'nock';
Expand Down
6 changes: 5 additions & 1 deletion packages/sitecore-jss/src/graphql-request-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export type GraphQLRequestClientConfig = {
* Override debugger for logging. Uses 'sitecore-jss:http' by default.
*/
debugger?: Debugger;
/**
* Override fetch method. Uses 'graphql-request' library default otherwise ('cross-fetch').
*/
fetch?: typeof fetch;
};

/**
Expand Down Expand Up @@ -54,7 +58,7 @@ export class GraphQLRequestClient implements GraphQLClient {
);
}

this.client = new Client(endpoint, { headers: this.headers });
this.client = new Client(endpoint, { headers: this.headers, fetch: clientConfig.fetch });
this.debug = clientConfig.debugger || debuggers.http;
}

Expand Down
87 changes: 87 additions & 0 deletions packages/sitecore-jss/src/site/graphql-redirects-service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { expect } from 'chai';
import nock from 'nock';
import { GraphQLRedirectsService, RedirectsQueryResult } from './graphql-redirects-service';
import { siteNameError } from '../constants';

const redirectsQueryResultNull = {
site: {
siteInfo: {
redirects: [],
},
},
} as RedirectsQueryResult;

const redirectsQueryResult = {
site: {
siteInfo: {
redirects: [
{
pattern: '/notfound',
target: '/404',
redirectType: 'REDIRECT_301',
isQueryStringPreserved: true,
},
],
},
},
} as RedirectsQueryResult;

describe('GraphQLRedirectsService', () => {
const endpoint = 'http://site';
const apiKey = 'some-api-key';
const siteName = 'site-name';

afterEach(() => {
nock.cleanAll();
});

const mockRedirectsRequest = (siteName?: string) => {
nock(endpoint)
.post('/')
.reply(
200,
siteName
? {
data: redirectsQueryResult,
}
: {
data: redirectsQueryResultNull,
}
);
};

describe('fetch redirects from site by graphql', () => {
it('should get error if redirects has empty siteName', async () => {
mockRedirectsRequest();

const service = new GraphQLRedirectsService({ endpoint, apiKey, siteName: '' });
await service.fetchRedirects().catch((error: Error) => {
expect(error.message).to.equal(siteNameError);
});

return expect(nock.isDone()).to.be.false;
});

it('should get redirects', async () => {
mockRedirectsRequest(siteName);

const service = new GraphQLRedirectsService({ endpoint, apiKey, siteName });
const result = await service.fetchRedirects();

expect(result).to.deep.equal(redirectsQueryResult.site.siteInfo.redirects);

return expect(nock.isDone()).to.be.true;
});

it('should get no redirects', async () => {
mockRedirectsRequest();

const service = new GraphQLRedirectsService({ endpoint, apiKey, siteName });
const result = await service.fetchRedirects();

expect(result).to.deep.equal(redirectsQueryResultNull.site.siteInfo.redirects);

return expect(nock.isDone()).to.be.true;
});
});
});
Loading

0 comments on commit b963d40

Please sign in to comment.