fix(nextjs): remove the need to install @nx/next for production builds (#16469)
This commit is contained in:
parent
a10b6b1290
commit
564ffaeebd
@ -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';
|
||||
|
||||
@ -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?: {
|
||||
@ -78,6 +69,7 @@ function getNxContext(
|
||||
targetName: string;
|
||||
configurationName?: string;
|
||||
} {
|
||||
const { parseTargetString } = require('@nx/devkit');
|
||||
const targetConfig = getTargetConfig(graph, target);
|
||||
|
||||
if (
|
||||
@ -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).
|
||||
*/
|
||||
@ -130,22 +121,36 @@ async function determineDistDirForProdServer(
|
||||
const target = process.env.NX_TASK_TARGET_TARGET;
|
||||
const configuration = process.env.NX_TASK_TARGET_CONFIGURATION;
|
||||
|
||||
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 { 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';
|
||||
}
|
||||
} catch {
|
||||
// ignored -- fallback to Next.js default of '.next'
|
||||
}
|
||||
export function withNx(
|
||||
|
||||
return nextConfig.distDir || '.next';
|
||||
}
|
||||
|
||||
function withNx(
|
||||
_nextConfig = {} as WithNxOptions,
|
||||
context: WithNxContext = getWithNxContext()
|
||||
): NextConfigFn {
|
||||
@ -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[] = [];
|
||||
|
||||
@ -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,
|
||||
@ -202,6 +217,7 @@ export function withNx(
|
||||
|
||||
const userWebpackConfig = nextConfig.webpack;
|
||||
|
||||
const { createWebpackConfig } = require('../src/utils/config');
|
||||
nextConfig.webpack = (a, b) =>
|
||||
createWebpackConfig(
|
||||
workspaceRoot,
|
||||
@ -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 };
|
||||
|
||||
@ -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()`);
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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');
|
||||
|
||||
|
||||
59
packages/next/src/utils/compose-plugins.spec.ts
Normal file
59
packages/next/src/utils/compose-plugins.spec.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { NextConfig } from 'next';
|
||||
import { composePlugins } from './compose-plugins';
|
||||
import { NextConfigFn } from './config';
|
||||
|
||||
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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
30
packages/next/src/utils/compose-plugins.ts
Normal file
30
packages/next/src/utils/compose-plugins.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import type { NextConfig } from 'next';
|
||||
import type {
|
||||
NextConfigFn,
|
||||
NextPlugin,
|
||||
NextPluginThatReturnsConfigFn,
|
||||
} from './config';
|
||||
|
||||
export function composePlugins(
|
||||
...plugins: (NextPlugin | NextPluginThatReturnsConfigFn)[]
|
||||
): (baseConfig: NextConfig) => NextConfigFn {
|
||||
return function (baseConfig: NextConfig) {
|
||||
return async function combined(
|
||||
phase: string,
|
||||
context: any
|
||||
): Promise<NextConfig> {
|
||||
let config = baseConfig;
|
||||
for (const plugin of plugins) {
|
||||
const fn = await plugin;
|
||||
const configOrFn = fn(config);
|
||||
if (typeof configOrFn === 'function') {
|
||||
config = await configOrFn(phase, context);
|
||||
} else {
|
||||
config = configOrFn;
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
};
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
import type { NextConfig } from 'next';
|
||||
import 'nx/src/utils/testing/mock-fs';
|
||||
import { composePlugins, createWebpackConfig, NextConfigFn } from './config';
|
||||
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';
|
||||
import { createWebpackConfig } from './config';
|
||||
|
||||
jest.mock('@nx/webpack', () => ({}));
|
||||
jest.mock('tsconfig-paths-webpack-plugin');
|
||||
@ -76,60 +75,4 @@ describe('Next.js webpack config builder', () => {
|
||||
expect(config.module.rules.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -8,7 +8,18 @@ import {
|
||||
createTmpTsConfig,
|
||||
DependentBuildableProjectNode,
|
||||
} from '@nx/js/src/utils/buildable-libs-utils';
|
||||
import { NxWebpackExecutionContext } from '@nx/webpack';
|
||||
|
||||
export interface NextConfigFn {
|
||||
(phase: string, context?: any): Promise<NextConfig> | NextConfig;
|
||||
}
|
||||
|
||||
export interface NextPlugin {
|
||||
(config: NextConfig): NextConfig;
|
||||
}
|
||||
|
||||
export interface NextPluginThatReturnsConfigFn {
|
||||
(config: NextConfig): NextConfigFn;
|
||||
}
|
||||
|
||||
export function createWebpackConfig(
|
||||
workspaceRoot: string,
|
||||
@ -96,39 +107,3 @@ function isTsRule(r: RuleSetRule): boolean {
|
||||
|
||||
return r.test.test('a.ts');
|
||||
}
|
||||
|
||||
export interface NextConfigFn {
|
||||
(phase: string, context?: any): Promise<NextConfig> | NextConfig;
|
||||
}
|
||||
|
||||
export interface NextPlugin {
|
||||
(config: NextConfig): NextConfig;
|
||||
}
|
||||
|
||||
export interface NextPluginThatReturnsConfigFn {
|
||||
(config: NextConfig): NextConfigFn;
|
||||
}
|
||||
|
||||
export function composePlugins(
|
||||
...plugins: (NextPlugin | NextPluginThatReturnsConfigFn)[]
|
||||
): (baseConfig: NextConfig) => NextConfigFn {
|
||||
return function (baseConfig: NextConfig) {
|
||||
return async function combined(
|
||||
phase: string,
|
||||
context: any
|
||||
): Promise<NextConfig> {
|
||||
let config = baseConfig;
|
||||
for (const plugin of plugins) {
|
||||
const fn = await plugin;
|
||||
const configOrFn = fn(config);
|
||||
if (typeof configOrFn === 'function') {
|
||||
config = await configOrFn(phase, context);
|
||||
} else {
|
||||
config = configOrFn;
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user