fix(nextjs): produce correct next.config.js file for production server (#15824)
This commit is contained in:
parent
5e5a3f7487
commit
c7e49c564a
@ -5,6 +5,7 @@ import {
|
||||
cleanupProject,
|
||||
getPackageManagerCommand,
|
||||
isNotWindows,
|
||||
killPort,
|
||||
killPorts,
|
||||
newProject,
|
||||
packageManagerLockFile,
|
||||
@ -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 () => {
|
||||
@ -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', () => {
|
||||
@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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 {
|
||||
@ -113,62 +114,69 @@ function getNxContext(
|
||||
export function withNx(
|
||||
_nextConfig = {} as WithNxOptions,
|
||||
context: WithNxContext = getWithNxContext()
|
||||
): () => Promise<NextConfig> {
|
||||
return async () => {
|
||||
let dependencies: DependentBuildableProjectNode[] = [];
|
||||
): (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 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 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,
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
@ -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')
|
||||
);
|
||||
copyFileSync(nextConfigPath, join(options.outputPath, 'next.config.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}'
|
||||
}
|
||||
}`,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
return withNxContent;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,5 +1,12 @@
|
||||
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) {
|
||||
@ -7,6 +14,10 @@ export async function addJest(host: Tree, options: NormalizedSchema) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const { jestProjectGenerator } = ensurePackage<typeof import('@nrwl/jest')>(
|
||||
'@nrwl/jest',
|
||||
nxVersion
|
||||
);
|
||||
const jestTask = await jestProjectGenerator(host, {
|
||||
...options,
|
||||
project: options.projectName,
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
import {
|
||||
addDependenciesToPackageJson,
|
||||
convertNxGenerator,
|
||||
ensurePackage,
|
||||
GeneratorCallback,
|
||||
runTasksInSerial,
|
||||
Tree,
|
||||
} from '@nrwl/devkit';
|
||||
|
||||
import { jestInitGenerator } from '@nrwl/jest';
|
||||
import { cypressInitGenerator } from '@nrwl/cypress';
|
||||
import { reactDomVersion, reactVersion } from '@nrwl/react/src/utils/versions';
|
||||
import reactInitGenerator from '@nrwl/react/src/generators/init/init';
|
||||
import { initGenerator as jsInitGenerator } from '@nrwl/js';
|
||||
@ -48,10 +47,17 @@ export async function nextInitGenerator(host: Tree, schema: InitSchema) {
|
||||
);
|
||||
|
||||
if (!schema.unitTestRunner || schema.unitTestRunner === 'jest') {
|
||||
const { jestInitGenerator } = ensurePackage<typeof import('@nrwl/jest')>(
|
||||
'@nrwl/jest',
|
||||
nxVersion
|
||||
);
|
||||
const jestTask = await jestInitGenerator(host, schema);
|
||||
tasks.push(jestTask);
|
||||
}
|
||||
if (!schema.e2eTestRunner || schema.e2eTestRunner === 'cypress') {
|
||||
const { cypressInitGenerator } = ensurePackage<
|
||||
typeof import('@nrwl/cypress')
|
||||
>('@nrwl/cypress', nxVersion);
|
||||
const cypressTask = await cypressInitGenerator(host, {});
|
||||
tasks.push(cypressTask);
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import { join, resolve } from 'path';
|
||||
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
|
||||
import { Configuration, RuleSetRule } from 'webpack';
|
||||
import { FileReplacement } from './types';
|
||||
import { createCopyPlugin, normalizeAssets } from '@nrwl/webpack';
|
||||
import { createCopyPlugin } from './create-copy-plugin';
|
||||
import {
|
||||
createTmpTsConfig,
|
||||
DependentBuildableProjectNode,
|
||||
@ -77,14 +77,7 @@ export function createWebpackConfig(
|
||||
|
||||
// Copy (shared) assets to `public` folder during client-side compilation
|
||||
if (!isServer && Array.isArray(assets) && assets.length > 0) {
|
||||
config.plugins.push(
|
||||
createCopyPlugin(
|
||||
normalizeAssets(assets, workspaceRoot, projectRoot).map((asset) => ({
|
||||
...asset,
|
||||
output: join('../public', asset.output),
|
||||
}))
|
||||
)
|
||||
);
|
||||
config.plugins.push(createCopyPlugin(assets, workspaceRoot, projectRoot));
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
83
packages/next/src/utils/create-copy-plugin.ts
Normal file
83
packages/next/src/utils/create-copy-plugin.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import * as CopyWebpackPlugin from 'copy-webpack-plugin';
|
||||
import { normalizePath } from 'nx/src/utils/path';
|
||||
import { basename, dirname, join, relative, resolve } from 'path';
|
||||
import { statSync } from 'fs';
|
||||
|
||||
interface AssetGlobPattern {
|
||||
glob: string;
|
||||
input: string;
|
||||
output: string;
|
||||
ignore?: string[];
|
||||
}
|
||||
|
||||
function normalizeAssets(
|
||||
assets: any[],
|
||||
root: string,
|
||||
sourceRoot: string
|
||||
): AssetGlobPattern[] {
|
||||
return assets.map((asset) => {
|
||||
if (typeof asset === 'string') {
|
||||
const assetPath = normalizePath(asset);
|
||||
const resolvedAssetPath = resolve(root, assetPath);
|
||||
const resolvedSourceRoot = resolve(root, sourceRoot);
|
||||
|
||||
if (!resolvedAssetPath.startsWith(resolvedSourceRoot)) {
|
||||
throw new Error(
|
||||
`The ${resolvedAssetPath} asset path must start with the project source root: ${sourceRoot}`
|
||||
);
|
||||
}
|
||||
|
||||
const isDirectory = statSync(resolvedAssetPath).isDirectory();
|
||||
const input = isDirectory
|
||||
? resolvedAssetPath
|
||||
: dirname(resolvedAssetPath);
|
||||
const output = relative(resolvedSourceRoot, resolve(root, input));
|
||||
const glob = isDirectory ? '**/*' : basename(resolvedAssetPath);
|
||||
return {
|
||||
input,
|
||||
output,
|
||||
glob,
|
||||
};
|
||||
} else {
|
||||
if (asset.output.startsWith('..')) {
|
||||
throw new Error(
|
||||
'An asset cannot be written to a location outside of the output path.'
|
||||
);
|
||||
}
|
||||
|
||||
const assetPath = normalizePath(asset.input);
|
||||
const resolvedAssetPath = resolve(root, assetPath);
|
||||
return {
|
||||
...asset,
|
||||
input: resolvedAssetPath,
|
||||
// Now we remove starting slash to make Webpack place it from the output root.
|
||||
output: asset.output.replace(/^\//, ''),
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function createCopyPlugin(
|
||||
assets: any[],
|
||||
root: string,
|
||||
sourceRoot: string
|
||||
) {
|
||||
return new CopyWebpackPlugin({
|
||||
patterns: normalizeAssets(assets, root, sourceRoot).map((asset) => {
|
||||
return {
|
||||
context: asset.input,
|
||||
to: join('../public', asset.output),
|
||||
from: asset.glob,
|
||||
globOptions: {
|
||||
ignore: [
|
||||
'.gitkeep',
|
||||
'**/.DS_Store',
|
||||
'**/Thumbs.db',
|
||||
...(asset.ignore ?? []),
|
||||
],
|
||||
dot: true,
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user