Skip to content

Commit

Permalink
fix(nextjs): remove the need to install @nx/next for production builds
Browse files Browse the repository at this point in the history
  • Loading branch information
jaysoo committed Apr 21, 2023
1 parent ec3c642 commit 3a6f537
Show file tree
Hide file tree
Showing 9 changed files with 302 additions and 135 deletions.
2 changes: 1 addition & 1 deletion packages/next/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ export { componentGenerator } from './src/generators/component/component';
export { libraryGenerator } from './src/generators/library/library';
export { pageGenerator } from './src/generators/page/page';
export { withNx } from './plugins/with-nx';
export { composePlugins } from './src/utils/config';
export { composePlugins } from './src/utils/compose-plugins';
86 changes: 52 additions & 34 deletions packages/next/plugins/with-nx.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@
import {
createProjectGraphAsync,
joinPathFragments,
offsetFromRoot,
parseTargetString,
ProjectGraph,
ProjectGraphProjectNode,
Target,
workspaceRoot,
} from '@nx/devkit';
import {
calculateProjectDependencies,
DependentBuildableProjectNode,
} from '@nx/js/src/utils/buildable-libs-utils';
/**
* WARNING: Do not add development dependencies to top-level imports.
* Instead, `require` them inline during the build phase.
*/
import * as path from 'path';
import type { NextConfig } from 'next';
import { PHASE_PRODUCTION_SERVER } from 'next/constants';

import * as path from 'path';
import { createWebpackConfig, NextConfigFn } from '../src/utils/config';
import { NextBuildBuilderOptions } from '../src/utils/types';
import type { NextConfigFn } from '../src/utils/config';
import type { NextBuildBuilderOptions } from '../src/utils/types';
import type { DependentBuildableProjectNode } from '@nx/js/src/utils/buildable-libs-utils';
import type { ProjectGraph, ProjectGraphProjectNode, Target } from '@nx/devkit';

export interface WithNxOptions extends NextConfig {
nx?: {
Expand Down Expand Up @@ -78,6 +69,7 @@ function getNxContext(
targetName: string;
configurationName?: string;
} {
const { parseTargetString } = require('@nx/devkit');
const targetConfig = getTargetConfig(graph, target);

if (
Expand Down Expand Up @@ -119,7 +111,6 @@ function getNxContext(
);
}
}

/**
* Try to read output dir from project, and default to '.next' if executing outside of Nx (e.g. dist is added to a docker image).
*/
Expand All @@ -130,22 +121,36 @@ async function determineDistDirForProdServer(
const target = process.env.NX_TASK_TARGET_TARGET;
const configuration = process.env.NX_TASK_TARGET_CONFIGURATION;

if (project && target) {
const originalTarget = { project, target, configuration };
const graph = await createProjectGraphAsync();

const { options, node: projectNode } = getNxContext(graph, originalTarget);
const outputDir = `${offsetFromRoot(projectNode.data.root)}${
options.outputPath
}`;
return nextConfig.distDir && nextConfig.distDir !== '.next'
? joinPathFragments(outputDir, nextConfig.distDir)
: joinPathFragments(outputDir, '.next');
} else {
return '.next';
try {
if (project && target) {
// If NX env vars are set, then devkit must be available.
const {
createProjectGraphAsync,
joinPathFragments,
offsetFromRoot,
} = require('@nx/devkit');
const originalTarget = { project, target, configuration };
const graph = await createProjectGraphAsync();

const { options, node: projectNode } = getNxContext(
graph,
originalTarget
);
const outputDir = `${offsetFromRoot(projectNode.data.root)}${
options.outputPath
}`;
return nextConfig.distDir && nextConfig.distDir !== '.next'
? joinPathFragments(outputDir, nextConfig.distDir)
: joinPathFragments(outputDir, '.next');
}
} catch {
// ignored -- fallback to Next.js default of '.next'
}

return nextConfig.distDir || '.next';
}
export function withNx(

function withNx(
_nextConfig = {} as WithNxOptions,
context: WithNxContext = getWithNxContext()
): NextConfigFn {
Expand All @@ -155,9 +160,16 @@ export function withNx(
const { nx, ...validNextConfig } = _nextConfig;
return {
...validNextConfig,
distDir: await determineDistDirForProdServer(validNextConfig),
distDir: await determineDistDirForProdServer(_nextConfig),
};
} else {
const {
createProjectGraphAsync,
joinPathFragments,
offsetFromRoot,
workspaceRoot,
} = require('@nx/devkit');

// Otherwise, add in webpack and eslint configuration for build or test.
let dependencies: DependentBuildableProjectNode[] = [];

Expand All @@ -179,6 +191,9 @@ export function withNx(
const projectDirectory = projectNode.data.root;

if (options.buildLibsFromSource === false && targetName) {
const {
calculateProjectDependencies,
} = require('@nx/js/src/utils/buildable-libs-utils');
const result = calculateProjectDependencies(
graph,
workspaceRoot,
Expand All @@ -202,6 +217,7 @@ export function withNx(

const userWebpackConfig = nextConfig.webpack;

const { createWebpackConfig } = require('../src/utils/config');
nextConfig.webpack = (a, b) =>
createWebpackConfig(
workspaceRoot,
Expand Down Expand Up @@ -407,3 +423,5 @@ module.exports = withNx;
// Support for newer generated code: `const { withNx } = require(...);`
module.exports.withNx = withNx;
module.exports.getNextConfig = getNextConfig;

export { withNx };
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { getWithNxContent } from './create-next-config-file';
import { stripIndents } from '@nx/devkit';

describe('Next.js config: getWithNxContent', () => {
it('should swap distDir and getWithNxContext with static values', () => {
const result = getWithNxContent({
withNxFile: `with-nx.js`,
withNxContent: stripIndents`
// SHOULD BE LEFT INTACT
const constants = require("next/constants");
// TO BE SWAPPED
function getWithNxContext() {
const { workspaceRoot, workspaceLayout } = require('@nx/devkit');
return {
workspaceRoot,
libsDir: workspaceLayout().libsDir,
};
}
// SHOULD BE LEFT INTACT
function withNx(nextConfig = {}, context = getWithNxContext()) {
return (phase) => {
if (phase === constants.PHASE_PRODUCTION_SERVER) {
//...
} else {
// ...
}
};
}
// SHOULD BE LEFT INTACT
module.exports.withNx = withNx;
`,
});

expect(result).toContain(`const constants = require("next/constants")`);
expect(result).toContain(stripIndents`
// SHOULD BE LEFT INTACT
function withNx(nextConfig = {}, context = getWithNxContext()) {
return (phase) => {
if (phase === constants.PHASE_PRODUCTION_SERVER) {
//...
} else {
// ...
}
};
}
// SHOULD BE LEFT INTACT
module.exports.withNx = withNx;
`);
expect(result).not.toContain(
`const { workspaceRoot, workspaceLayout } = require('@nx/devkit');`
);
expect(result).toContain(`libsDir: ''`);
expect(result).not.toContain(`libsDir: workspaceLayout.libsDir()`);
});
});
92 changes: 88 additions & 4 deletions packages/next/src/executors/build/lib/create-next-config-file.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
import { ExecutorContext } from '@nx/devkit';

import { copyFileSync, existsSync } from 'fs';
import type { ExecutorContext } from '@nx/devkit';
import {
applyChangesToString,
ChangeType,
stripIndents,
workspaceLayout,
workspaceRoot,
} from '@nx/devkit';
import * as ts from 'typescript';
import {
copyFileSync,
existsSync,
mkdirSync,
readFileSync,
writeFileSync,
} 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 @@ -13,7 +27,77 @@ export function createNextConfigFile(
? join(context.root, options.nextConfig)
: join(context.root, options.root, 'next.config.js');

// Copy config file and our `.nx-helpers` folder to remove dependency on @nrwl/next for production build.
if (existsSync(nextConfigPath)) {
copyFileSync(nextConfigPath, join(options.outputPath, 'next.config.js'));
const helpersPath = join(options.outputPath, '.nx-helpers');
mkdirSync(helpersPath, { recursive: true });
copyFileSync(
join(__dirname, '../../../utils/compose-plugins.js'),
join(helpersPath, 'compose-plugins.js')
);
writeFileSync(join(helpersPath, 'with-nx.js'), getWithNxContent());
writeFileSync(
join(helpersPath, 'compiled.js'),
`
const withNx = require('./with-nx');
module.exports = withNx;
module.exports.withNx = withNx;
module.exports.composePlugins = require('./compose-plugins').composePlugins;
`
);
writeFileSync(
join(options.outputPath, 'next.config.js'),
readFileSync(nextConfigPath)
.toString()
.replace(/["']@nx\/next["']/, `'./.nx-helpers/compiled.js'`)
// TODO(v17): Remove this once users have all migrated to new @nx scope and import from '@nx/next' not the deep import paths.
.replace('@nx/next/plugins/with-nx', './.nx-helpers/compiled.js')
.replace('@nrwl/next/plugins/with-nx', './.nx-helpers/compiled.js')
);
}
}
function readSource() {
const withNxFile = join(__dirname, '../../../../plugins/with-nx.js');
const withNxContent = readFileSync(withNxFile).toString();
return {
withNxFile,
withNxContent,
};
}

// Exported for testing
export function getWithNxContent({ withNxFile, withNxContent } = readSource()) {
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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const plugins = [
withNx,
];

module.exports = composePlugins(...plugins)(nextConfig));
module.exports = composePlugins(...plugins)(nextConfig);
<% } else if (style === 'styl') { %>
const { withStylus } = require('@nx/next/plugins/with-stylus');

Expand Down
58 changes: 58 additions & 0 deletions packages/next/src/utils/compose-plugins.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { NextConfig } from 'next';
import { composePlugins, NextConfigFn } from './compose-plugins';

describe('composePlugins', () => {
it('should combine multiple plugins', async () => {
const nextConfig: NextConfig = {
env: {
original: 'original',
},
};
const a = (config: NextConfig): NextConfig => {
config.env['a'] = 'a';
return config;
};
const b = (config: NextConfig): NextConfig => {
config.env['b'] = 'b';
return config;
};
const fn = await composePlugins(a, b);
const output = await fn(nextConfig)('test', {});

expect(output).toEqual({
env: {
original: 'original',
a: 'a',
b: 'b',
},
});
});

it('should compose plugins that return an async function', async () => {
const nextConfig: NextConfig = {
env: {
original: 'original',
},
};
const a = (config: NextConfig): NextConfig => {
config.env['a'] = 'a';
return config;
};
const b = (config: NextConfig): NextConfigFn => {
return (phase: string) => {
config.env['b'] = phase;
return config;
};
};
const fn = await composePlugins(a, b);
const output = await fn(nextConfig)('test', {});

expect(output).toEqual({
env: {
original: 'original',
a: 'a',
b: 'test',
},
});
});
});
Loading

0 comments on commit 3a6f537

Please sign in to comment.