Skip to content

Commit

Permalink
feat(nextjs) : add proxy configuration support (#2407)
Browse files Browse the repository at this point in the history
* feat(nextjs): add proxy configuration support

ISSUES CLOSED: #2011

* chore(bazel): disable recalcitrant bazel e2e tests for now
  • Loading branch information
jdpearce committed Feb 5, 2020
1 parent 305cd42 commit e64f66c
Show file tree
Hide file tree
Showing 11 changed files with 197 additions and 84 deletions.
6 changes: 6 additions & 0 deletions docs/angular/api-next/builders/dev-server.md
Expand Up @@ -40,6 +40,12 @@ Type: `number`

Port to listen on.

### proxyConfig

Type: `string`

Path to the proxy configuration file.

### quiet

Default: `false`
Expand Down
6 changes: 6 additions & 0 deletions docs/react/api-next/builders/dev-server.md
Expand Up @@ -41,6 +41,12 @@ Type: `number`

Port to listen on.

### proxyConfig

Type: `string`

Path to the proxy configuration file.

### quiet

Default: `false`
Expand Down
6 changes: 6 additions & 0 deletions docs/web/api-next/builders/dev-server.md
Expand Up @@ -41,6 +41,12 @@ Type: `number`

Port to listen on.

### proxyConfig

Type: `string`

Path to the proxy configuration file.

### quiet

Default: `false`
Expand Down
120 changes: 69 additions & 51 deletions e2e/next.test.ts
@@ -1,23 +1,82 @@
import { fork } from 'child_process';
import { stringUtils } from '@nrwl/workspace';
import * as http from 'http';
import {
checkFilesExist,
ensureProject,
forEachCli,
readFile,
runCLI,
runCLIAsync,
supportUi,
tmpProjPath,
uniq,
updateFile
} from './utils';
import treeKill = require('tree-kill');

forEachCli('nx', () => {
describe('Next.js Applications', () => {
it('should generate a Next.js app that consumes a react lib', async () => {
it('should be able to serve with a proxy configuration', async () => {
ensureProject();
const appName = uniq('app');

runCLI(
`generate @nrwl/next:app ${appName} --no-interactive --linter=eslint`
);

const proxyConf = {
'/external-api': {
target: 'http://localhost:4200',
pathRewrite: {
'^/external-api/hello': '/api/hello'
}
}
};
updateFile(`apps/${appName}/proxy.conf.json`, JSON.stringify(proxyConf));

updateFile(
`apps/${appName}-e2e/src/integration/app.spec.ts`,
`
describe('next-app', () => {
beforeEach(() => cy.visit('/'));
it('should ', () => {
cy.get('h1').contains('Hello Next.js!');
});
});
`
);

updateFile(
`apps/${appName}/pages/index.tsx`,
`
import React, { useEffect, useState } from 'react';
export const Index = () => {
const [greeting, setGreeting] = useState('');
useEffect(() => {
fetch('/external-api/hello')
.then(r => r.text())
.then(setGreeting);
}, []);
return <h1>{greeting}</h1>;
};
export default Index;
`
);

updateFile(
`apps/${appName}/pages/api/hello.js`,
`
export default (_req, res) => {
res.status(200).send('Hello Next.js!');
};
`
);

const e2eResults = runCLI(`e2e ${appName}-e2e --headless`);
expect(e2eResults).toContain('All specs passed!');
}, 120000);

it('should be able to consume a react lib', async () => {
ensureProject();
const appName = uniq('app');
const libName = uniq('lib');
Expand Down Expand Up @@ -47,7 +106,7 @@ module.exports = {
expect(readFile(`dist/apps/${appName}/BUILD_ID`)).toEqual('fixed');
}, 120000);

it('should generate a Next.js app dynamically loading a lib', async () => {
it('should be able to dynamically load a lib', async () => {
ensureProject();
const appName = uniq('app');
const libName = uniq('lib');
Expand All @@ -73,7 +132,7 @@ module.exports = {
await checkApp(appName, { checkLint: false });
}, 120000);

it('should generate a Next.js app that compiles when using a workspace and react lib written in TypeScript', async () => {
it('should compile when using a workspace and react lib written in TypeScript', async () => {
ensureProject();
const appName = uniq('app');
const tsLibName = uniq('tslib');
Expand Down Expand Up @@ -145,8 +204,8 @@ async function checkApp(appName: string, opts: { checkLint: boolean }) {
const testResults = await runCLIAsync(`test ${appName}`);
expect(testResults.stderr).toContain('Test Suites: 1 passed, 1 total');

// This adds about 80s to the e2e run
// expectAppToRun(appName);
const e2eResults = runCLI(`e2e ${appName}-e2e --headless`);
expect(e2eResults).toContain('All specs passed!');

const buildResult = runCLI(`build ${appName}`);
expect(buildResult).toContain(`Compiled successfully`);
Expand All @@ -157,47 +216,6 @@ async function checkApp(appName: string, opts: { checkLint: boolean }) {
checkFilesExist(`dist/apps/${appName}/exported/index.html`);
}

async function expectAppToRun(appName: string) {
const server = fork(
`./node_modules/@nrwl/cli/bin/nx.js`,
[`serve`, appName],
{
cwd: tmpProjPath(),
silent: true
}
);
expect(server).toBeTruthy();
await new Promise(resolve => {
setTimeout(() => {
getPage().then(page => {
expect(page).toContain(`Here are some things you can do with Nx`);
treeKill(server.pid, 'SIGTERM', err => {
expect(err).toBeFalsy();
resolve();
});
});
}, 5000);
});
if (supportUi()) {
const e2eResults = runCLI(`e2e ${appName}-e2e`);
expect(e2eResults).toContain('All specs passed!');
}
}

function getPage(): Promise<string> {
return new Promise(resolve => {
http.get('http://localhost:4200/', res => {
let data = '';
res.on('data', chunk => {
data += chunk;
});
res.once('end', () => {
resolve(data);
});
});
});
}

forEachCli('angular', () => {
describe('next', () => {
it('is not supported', () => {});
Expand Down
1 change: 1 addition & 0 deletions packages/next/index.ts
@@ -0,0 +1 @@
export * from './src/utils/types';
57 changes: 27 additions & 30 deletions packages/next/src/builders/dev-server/dev-server.impl.ts
Expand Up @@ -5,51 +5,36 @@ import {
scheduleTargetAndForget,
targetFromTargetString
} from '@angular-devkit/architect';
import { terminal } from '@angular-devkit/core';
import * as fs from 'fs';
import {
PHASE_DEVELOPMENT_SERVER,
PHASE_PRODUCTION_SERVER
} from 'next/dist/next-server/lib/constants';
import startServer from 'next/dist/server/lib/start-server';
import NextServer from 'next/dist/server/next-dev-server';
import * as path from 'path';
import { from, Observable, of } from 'rxjs';
import { concatMap, switchMap, tap } from 'rxjs/operators';
import { prepareConfig } from '../../utils/config';
import {
NextBuildBuilderOptions,
NextServeBuilderOptions
NextServeBuilderOptions,
NextServer,
NextServerOptions,
ProxyConfig
} from '../../utils/types';
import { customServer } from './lib/custom-server';
import { defaultServer } from './lib/default-server';

try {
require('dotenv').config();
} catch (e) {}

export interface NextServerOptions {
dev: boolean;
dir: string;
staticMarkup: boolean;
quiet: boolean;
conf: any;
port: number;
path: string;
hostname: string;
}

export default createBuilder<NextServeBuilderOptions>(run);

function defaultServer(settings: NextServerOptions) {
return startServer(settings, settings.port, settings.hostname).then(app =>
app.prepare()
);
}

function customServer(settings: NextServerOptions) {
const nextApp = new NextServer(settings);

return require(path.resolve(settings.dir, settings.path))(nextApp, settings);
}
const infoPrefix = `[ ${terminal.dim(terminal.cyan('info'))} ] `;
const readyPrefix = `[ ${terminal.green('ready')} ]`;

function run(
export function run(
options: NextServeBuilderOptions,
context: BuilderContext
): Observable<BuilderOutput> {
Expand All @@ -75,7 +60,7 @@ function run(
options.dev ? PHASE_DEVELOPMENT_SERVER : PHASE_PRODUCTION_SERVER
);

const settings = {
const settings: NextServerOptions = {
dev: options.dev,
dir: root,
staticMarkup: options.staticMarkup,
Expand All @@ -86,13 +71,25 @@ function run(
hostname: options.hostname
};

const server = options.customServerPath
const server: NextServer = options.customServerPath
? customServer
: defaultServer;

return from(server(settings)).pipe(
// look for the proxy.conf.json
let proxyConfig: ProxyConfig;
const proxyConfigPath = options.proxyConfig
? path.join(context.workspaceRoot, options.proxyConfig)
: path.join(root, 'proxy.conf.json');
if (fs.existsSync(proxyConfigPath)) {
context.logger.info(
`${infoPrefix} found proxy configuration at ${proxyConfigPath}`
);
proxyConfig = require(proxyConfigPath);
}

return from(server(settings, proxyConfig)).pipe(
tap(() => {
context.logger.info(`Ready on ${baseUrl}`);
context.logger.info(`${readyPrefix} on ${baseUrl}`);
}),
switchMap(
e =>
Expand Down
16 changes: 16 additions & 0 deletions packages/next/src/builders/dev-server/lib/custom-server.ts
@@ -0,0 +1,16 @@
import NextServer from 'next/dist/server/next-dev-server';
import * as path from 'path';
import { NextServerOptions, ProxyConfig } from '../../../utils/types';

export function customServer(
settings: NextServerOptions,
proxyConfig?: ProxyConfig
) {
const nextApp = new NextServer(settings);

return require(path.resolve(settings.dir, settings.path))(
nextApp,
settings,
proxyConfig
);
}
32 changes: 32 additions & 0 deletions packages/next/src/builders/dev-server/lib/default-server.ts
@@ -0,0 +1,32 @@
import * as express from 'express';
import next from 'next';
import { NextServerOptions, ProxyConfig } from '../../../utils/types';

/**
* Adapted from https://github.com/zeit/next.js/blob/master/examples/with-custom-reverse-proxy/server.js
* @param settings
*/
export async function defaultServer(
settings: NextServerOptions,
proxyConfig?: ProxyConfig
): Promise<void> {
const app = next(settings);
const handle = app.getRequestHandler();

await app.prepare();

const server: express.Express = express();

// Set up the proxy.
if (proxyConfig) {
const proxyMiddleware = require('http-proxy-middleware');
Object.keys(proxyConfig).forEach(context => {
server.use(proxyMiddleware(context, proxyConfig[context]));
});
}

// Default catch-all handler to allow Next.js to handle all other routes
server.all('*', (req, res) => handle(req, res));

server.listen(settings.port, settings.hostname);
}
5 changes: 5 additions & 0 deletions packages/next/src/builders/dev-server/schema.json
Expand Up @@ -36,6 +36,11 @@
"type": "string",
"description": "Hostname on which the application is served.",
"default": null
},
"proxyConfig": {
"type": "string",
"description": "Path to the proxy configuration file.",
"default": null
}
},
"required": []
Expand Down

0 comments on commit e64f66c

Please sign in to comment.