Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(nextjs): produce correct next.config.js file for production server #15824

Merged
merged 1 commit into from
Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 34 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,22 @@ 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}"`
);
await runCommandUntil(`run ${appName}:serve-prod`, (output) => {
return output.includes(`localhost:${port}`);
});

await killPort(port);
}, 300_000);

it('should build and install pruned lock file', () => {
Expand Down Expand Up @@ -399,17 +413,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
118 changes: 63 additions & 55 deletions packages/next/plugins/with-nx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import {
DependentBuildableProjectNode,
} from '@nrwl/js/src/utils/buildable-libs-utils';
import type { NextConfig } from 'next';
import { PHASE_PRODUCTION_SERVER } from 'next/constants';

import path = require('path');
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 @@ -113,62 +114,69 @@ function getNxContext(
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: string) => Promise<NextConfig> {
return async (phase: string) => {
if (phase === PHASE_PRODUCTION_SERVER) {
// If we are running an already built production server, just return the configuration.
const { nx, ...validNextConfig } = _nextConfig;
return { distDir: '.next', ...validNextConfig };
} 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
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