Skip to content

Commit

Permalink
fix(nextjs): produce correct next.config.js file for production server
Browse files Browse the repository at this point in the history
  • Loading branch information
jaysoo committed Mar 22, 2023
1 parent 44478fb commit 021614d
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 151 deletions.
56 changes: 38 additions & 18 deletions e2e/next/src/next.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
cleanupProject,
getPackageManagerCommand,
isNotWindows,
killPort,
killPorts,
newProject,
packageManagerLockFile,
Expand All @@ -20,25 +21,22 @@ import {
} from '@nrwl/e2e/utils';
import * as http from 'http';
import { checkApp } from './utils';
import { removeSync } from 'fs-extra';

describe('Next.js Applications', () => {
let proj: string;
let originalEnv: string;
let packageManager;

beforeAll(() => {
beforeEach(() => {
proj = newProject();
packageManager = detectPackageManager(tmpProjPath());
});

afterAll(() => cleanupProject());

beforeEach(() => {
originalEnv = process.env.NODE_ENV;
});

afterEach(() => {
process.env.NODE_ENV = originalEnv;
cleanupProject();
});

it('should generate app + libs', async () => {
Expand Down Expand Up @@ -169,6 +167,26 @@ describe('Next.js Applications', () => {
`dist/apps/${appName}/public/a/b.txt`,
`dist/apps/${appName}/public/shared/ui/hello.txt`
);

// Check that the output is self-contained (i.e. can run with its own package.json + node_modules)
const distPath = joinPathFragments(tmpProjPath(), 'dist/apps', appName);
const port = 3000;
const pmc = getPackageManagerCommand();
runCommand(`${pmc.install}`, {
cwd: distPath,
});
runCLI(
`generate @nrwl/workspace:run-commands serve-prod --project ${appName} --cwd=dist/apps/${appName} --command="npx next start --port=${port}"`
);
const p = await runCommandUntil(`run ${appName}:serve-prod`, (output) => {
return output.includes(`localhost:${port}`);
});

const pageData = await getData(port);
expect(pageData).toContain('Hello Nx');

p.kill();
await killPort(port);
}, 300_000);

it('should build and install pruned lock file', () => {
Expand Down Expand Up @@ -399,17 +417,19 @@ describe('Next.js Applications', () => {
}, 300_000);
});

function getData(port: number, path = ''): Promise<any> {
return new Promise((resolve) => {
http.get(`http://localhost:${port}${path}`, (res) => {
expect(res.statusCode).toEqual(200);
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.once('end', () => {
resolve(data);
});
});
function getData(port, path = ''): Promise<any> {
return new Promise((resolve, reject) => {
http
.get(`http://localhost:${port}${path}`, (res) => {
expect(res.statusCode).toEqual(200);
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.once('end', () => {
resolve(data);
});
})
.on('error', (err) => reject(err));
});
}
5 changes: 2 additions & 3 deletions packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,21 @@
},
"dependencies": {
"@babel/plugin-proposal-decorators": "^7.14.5",
"@nrwl/cypress": "file:../cypress",
"@nrwl/devkit": "file:../devkit",
"@nrwl/jest": "file:../jest",
"@nrwl/js": "file:../js",
"@nrwl/linter": "file:../linter",
"@nrwl/react": "file:../react",
"@nrwl/webpack": "file:../webpack",
"@nrwl/workspace": "file:../workspace",
"@svgr/webpack": "^6.1.2",
"chalk": "^4.1.0",
"copy-webpack-plugin": "^10.2.4",
"dotenv": "~10.0.0",
"fs-extra": "^11.1.0",
"ignore": "^5.0.4",
"semver": "7.3.4",
"ts-node": "10.9.1",
"tsconfig-paths": "^4.1.2",
"tsconfig-paths-webpack-plugin": "4.0.0",
"url-loader": "^4.1.1",
"webpack-merge": "^5.8.0"
},
Expand Down
133 changes: 77 additions & 56 deletions packages/next/plugins/with-nx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,15 @@ import {
DependentBuildableProjectNode,
} from '@nrwl/js/src/utils/buildable-libs-utils';
import type { NextConfig } from 'next';

import path = require('path');
import {
PHASE_DEVELOPMENT_SERVER,
PHASE_PRODUCTION_BUILD,
PHASE_PRODUCTION_SERVER,
PHASE_EXPORT,
PHASE_TEST,
} from 'next/constants';

import * as path from 'path';
import { createWebpackConfig } from '../src/utils/config';
import { NextBuildBuilderOptions } from '../src/utils/types';
export interface WithNxOptions extends NextConfig {
Expand Down Expand Up @@ -110,65 +117,79 @@ function getNxContext(
}
}

type Phase =
| typeof PHASE_DEVELOPMENT_SERVER
| typeof PHASE_PRODUCTION_BUILD
| typeof PHASE_PRODUCTION_SERVER
| typeof PHASE_EXPORT
| typeof PHASE_TEST;

export function withNx(
_nextConfig = {} as WithNxOptions,
context: WithNxContext = getWithNxContext()
): () => Promise<NextConfig> {
return async () => {
let dependencies: DependentBuildableProjectNode[] = [];

const graph = await createProjectGraphAsync();

const originalTarget = {
project: process.env.NX_TASK_TARGET_PROJECT,
target: process.env.NX_TASK_TARGET_TARGET,
configuration: process.env.NX_TASK_TARGET_CONFIGURATION,
};

const {
node: projectNode,
options,
projectName: project,
targetName,
configurationName,
} = getNxContext(graph, originalTarget);
const projectDirectory = projectNode.data.root;

if (options.buildLibsFromSource === false && targetName) {
const result = calculateProjectDependencies(
graph,
workspaceRoot,
project,
): (phase: Phase) => Promise<NextConfig> {
return async (phase: Phase) => {
if (phase === PHASE_PRODUCTION_SERVER) {
// If we are running an already built production server, just return the configuration.
const { nx, ...validNextConfig } = _nextConfig;
return { ...validNextConfig, distDir: '.next' };
} else {
// Otherwise, add in webpack and eslint configuration for build or test.
let dependencies: DependentBuildableProjectNode[] = [];

const graph = await createProjectGraphAsync();

const originalTarget = {
project: process.env.NX_TASK_TARGET_PROJECT,
target: process.env.NX_TASK_TARGET_TARGET,
configuration: process.env.NX_TASK_TARGET_CONFIGURATION,
};

const {
node: projectNode,
options,
projectName: project,
targetName,
configurationName
);
dependencies = result.dependencies;
}
configurationName,
} = getNxContext(graph, originalTarget);
const projectDirectory = projectNode.data.root;

if (options.buildLibsFromSource === false && targetName) {
const result = calculateProjectDependencies(
graph,
workspaceRoot,
project,
targetName,
configurationName
);
dependencies = result.dependencies;
}

// Get next config
const nextConfig = getNextConfig(_nextConfig, context);

const outputDir = `${offsetFromRoot(projectDirectory)}${
options.outputPath
}`;
nextConfig.distDir =
nextConfig.distDir && nextConfig.distDir !== '.next'
? joinPathFragments(outputDir, nextConfig.distDir)
: joinPathFragments(outputDir, '.next');

const userWebpackConfig = nextConfig.webpack;

nextConfig.webpack = (a, b) =>
createWebpackConfig(
workspaceRoot,
options.root,
options.fileReplacements,
options.assets,
dependencies,
path.join(workspaceRoot, context.libsDir)
)(userWebpackConfig ? userWebpackConfig(a, b) : a, b);

return nextConfig;
// Get next config
const nextConfig = getNextConfig(_nextConfig, context);

const outputDir = `${offsetFromRoot(projectDirectory)}${
options.outputPath
}`;
nextConfig.distDir =
nextConfig.distDir && nextConfig.distDir !== '.next'
? joinPathFragments(outputDir, nextConfig.distDir)
: joinPathFragments(outputDir, '.next');

const userWebpackConfig = nextConfig.webpack;

nextConfig.webpack = (a, b) =>
createWebpackConfig(
workspaceRoot,
options.root,
options.fileReplacements,
options.assets,
dependencies,
path.join(workspaceRoot, context.libsDir)
)(userWebpackConfig ? userWebpackConfig(a, b) : a, b);

return nextConfig;
}
};
}

Expand Down
62 changes: 4 additions & 58 deletions packages/next/src/executors/build/lib/create-next-config-file.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import type { ExecutorContext } from '@nrwl/devkit';
import {
applyChangesToString,
ChangeType,
workspaceLayout,
workspaceRoot,
stripIndents,
} from '@nrwl/devkit';
import * as ts from 'typescript';
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { ExecutorContext } from '@nrwl/devkit';

import { copyFileSync, existsSync } from 'fs';
import { join } from 'path';

import type { NextBuildBuilderOptions } from '../../../utils/types';
import { findNodes } from 'nx/src/utils/typescript';

export function createNextConfigFile(
options: NextBuildBuilderOptions,
Expand All @@ -21,53 +13,7 @@ export function createNextConfigFile(
? join(context.root, options.nextConfig)
: join(context.root, options.root, 'next.config.js');

// Copy config file and our `with-nx.js` file to remove dependency on @nrwl/next for production build.
if (existsSync(nextConfigPath)) {
writeFileSync(join(options.outputPath, 'with-nx.js'), getWithNxContent());
writeFileSync(
join(options.outputPath, 'next.config.js'),
readFileSync(nextConfigPath)
.toString()
.replace('@nrwl/next/plugins/with-nx', './with-nx.js')
);
}
}

function getWithNxContent() {
const withNxFile = join(__dirname, '../../../../plugins/with-nx.js');
let withNxContent = readFileSync(withNxFile).toString();
const withNxSource = ts.createSourceFile(
withNxFile,
withNxContent,
ts.ScriptTarget.Latest,
true
);
const getWithNxContextDeclaration = findNodes(
withNxSource,
ts.SyntaxKind.FunctionDeclaration
)?.find(
(node: ts.FunctionDeclaration) => node.name?.text === 'getWithNxContext'
);

if (getWithNxContextDeclaration) {
withNxContent = applyChangesToString(withNxContent, [
{
type: ChangeType.Delete,
start: getWithNxContextDeclaration.getStart(withNxSource),
length: getWithNxContextDeclaration.getWidth(withNxSource),
},
{
type: ChangeType.Insert,
index: getWithNxContextDeclaration.getStart(withNxSource),
text: stripIndents`function getWithNxContext() {
return {
workspaceRoot: '${workspaceRoot}',
libsDir: '${workspaceLayout().libsDir}'
}
}`,
},
]);
copyFileSync(nextConfigPath, join(options.outputPath, 'next.config.js'));
}

return withNxContent;
}
10 changes: 7 additions & 3 deletions packages/next/src/generators/application/lib/add-cypress.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { cypressProjectGenerator } from '@nrwl/cypress';
import { Tree } from '@nrwl/devkit';
import { NormalizedSchema } from './normalize-options';
import { ensurePackage, Tree } from '@nrwl/devkit';
import { Linter } from '@nrwl/linter';

import { nxVersion } from '../../../utils/versions';
import { NormalizedSchema } from './normalize-options';

export async function addCypress(host: Tree, options: NormalizedSchema) {
if (options.e2eTestRunner !== 'cypress') {
return () => {};
}

const { cypressProjectGenerator } = ensurePackage<
typeof import('@nrwl/cypress')
>('@nrwl/cypress', nxVersion);
return cypressProjectGenerator(host, {
...options,
linter: Linter.EsLint,
Expand Down
15 changes: 13 additions & 2 deletions packages/next/src/generators/application/lib/add-jest.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import { joinPathFragments, readJson, Tree, updateJson } from '@nrwl/devkit';
import { jestProjectGenerator } from '@nrwl/jest';
import {
ensurePackage,
joinPathFragments,
readJson,
Tree,
updateJson,
} from '@nrwl/devkit';

import { nxVersion } from '../../../utils/versions';
import { NormalizedSchema } from './normalize-options';

export async function addJest(host: Tree, options: NormalizedSchema) {
if (options.unitTestRunner !== 'jest') {
return () => {};
}

const { jestProjectGenerator } = ensurePackage<typeof import('@nrwl/jest')>(
'@nrwl/jest',
nxVersion
);
const jestTask = await jestProjectGenerator(host, {
...options,
project: options.projectName,
Expand Down
Loading

0 comments on commit 021614d

Please sign in to comment.