From 093fed105bc2fa6fd5514b419ef90b7f9313220b Mon Sep 17 00:00:00 2001 From: Nicholas Cunningham Date: Mon, 13 Mar 2023 17:47:08 -0600 Subject: [PATCH] fix(nextjs): move webpack config to withNx from build.impl --- nx-dev/nx-dev/next.config.js | 2 +- packages/next/plugins/with-nx.spec.ts | 6 +- packages/next/plugins/with-nx.ts | 95 +++++++++++++++++++ .../next/src/executors/build/build.impl.ts | 31 +----- .../next/src/executors/export/export.impl.ts | 40 ++++---- .../next/src/executors/server/server.impl.ts | 39 +------- packages/next/src/utils/config.spec.ts | 82 +--------------- packages/next/src/utils/config.ts | 86 +---------------- packages/next/src/utils/types.ts | 1 - 9 files changed, 128 insertions(+), 254 deletions(-) diff --git a/nx-dev/nx-dev/next.config.js b/nx-dev/nx-dev/next.config.js index 8e16595629457c..e6c98a5019b36a 100644 --- a/nx-dev/nx-dev/next.config.js +++ b/nx-dev/nx-dev/next.config.js @@ -1,5 +1,5 @@ // nx-ignore-next-line -const withNx = require('@nrwl/next/plugins/with-nx'); +const { withNx } = require('@nrwl/next/plugins/with-nx'); const { copySync } = require('fs-extra'); const path = require('path'); const redirectRules = require('./redirect-rules.config'); diff --git a/packages/next/plugins/with-nx.spec.ts b/packages/next/plugins/with-nx.spec.ts index 37369146349d03..f0dfcf7c17e5ba 100644 --- a/packages/next/plugins/with-nx.spec.ts +++ b/packages/next/plugins/with-nx.spec.ts @@ -1,10 +1,10 @@ import { NextConfigComplete } from 'next/dist/server/config-shared'; -import { withNx } from './with-nx'; +import { getNextConfig } from './with-nx'; describe('withNx', () => { describe('svgr', () => { it('should be used by default', () => { - const config = withNx({}); + const config = getNextConfig(); const result = config.webpack( { @@ -32,7 +32,7 @@ describe('withNx', () => { }); it('should not be used when disabled', () => { - const config = withNx({ + const config = getNextConfig({ nx: { svgr: false, }, diff --git a/packages/next/plugins/with-nx.ts b/packages/next/plugins/with-nx.ts index d750a6f13f1172..5799c47fe7c20c 100644 --- a/packages/next/plugins/with-nx.ts +++ b/packages/next/plugins/with-nx.ts @@ -1,5 +1,18 @@ +import { + createProjectGraphAsync, + joinPathFragments, + offsetFromRoot, + parseTargetString, + workspaceRoot, +} from '@nrwl/devkit'; +import { + calculateProjectDependencies, + DependentBuildableProjectNode, +} from '@nrwl/js/src/utils/buildable-libs-utils'; import type { NextConfig } from 'next'; +import path = require('path'); +import { createWebpackConfig } from '../src/utils/config'; export interface WithNxOptions extends NextConfig { nx?: { svgr?: boolean; @@ -35,6 +48,87 @@ function getWithNxContext(): WithNxContext { } export function withNx( + _nextConfig = {} as WithNxOptions, + context: WithNxContext = getWithNxContext() +): () => Promise { + return async () => { + let dependencies: DependentBuildableProjectNode[] = []; + + const graph = await createProjectGraphAsync(); + const project = process.env.NX_TASK_TARGET_PROJECT; + let projectNode = graph.nodes[project]; + + const targetName = process.env.NX_TASK_TARGET_TARGET; + const configurationName = process.env.NX_TASK_TARGET_CONFIGURATION; + + let options = projectNode.data.targets[targetName].options; + if (configurationName) { + Object.assign( + options, + projectNode.data.targets[targetName].configurations[configurationName] + ); + } + + // If we are running serve or export pull the options from the dependent target first (ex. build) + if ( + ['@nrwl/next:server', '@nrwl/next:export'].includes( + projectNode.data.targets[targetName].executor + ) + ) { + const target = parseTargetString(options.buildTarget, graph); + projectNode = graph.nodes[target.project]; + options = projectNode.data.targets[target.target].options; + + if (target.configuration) { + Object.assign( + options, + projectNode.data.targets[target.target].configurations[ + target.configuration + ] + ); + } + } + const projectDirectory = projectNode.data.root; + + if (!options.buildLibsFromSource && 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; + }; +} + +export function getNextConfig( nextConfig = {} as WithNxOptions, context: WithNxContext = getWithNxContext() ): NextConfig { @@ -223,3 +317,4 @@ function addNxEnvVariables(config: any) { module.exports = withNx; // Support for newer generated code: `const { withNx } = require(...);` module.exports.withNx = withNx; +module.exports.getNextConfig = getNextConfig; diff --git a/packages/next/src/executors/build/build.impl.ts b/packages/next/src/executors/build/build.impl.ts index 586f31f0b70957..7f170fecc0c59c 100644 --- a/packages/next/src/executors/build/build.impl.ts +++ b/packages/next/src/executors/build/build.impl.ts @@ -2,7 +2,6 @@ import 'dotenv/config'; import { ExecutorContext, readJsonFile, - workspaceLayout, workspaceRoot, writeJsonFile, } from '@nrwl/devkit'; @@ -12,18 +11,13 @@ import { join, resolve } from 'path'; import { copySync, existsSync, mkdir, writeFileSync } from 'fs-extra'; import { gte } from 'semver'; import { directoryExists } from '@nrwl/workspace/src/utilities/fileutils'; -import { - calculateProjectDependencies, - DependentBuildableProjectNode, -} from '@nrwl/js/src/utils/buildable-libs-utils'; import { checkAndCleanWithSemver } from '@nrwl/devkit/src/utils/semver'; -import { prepareConfig } from '../../utils/config'; import { updatePackageJson } from './lib/update-package-json'; import { createNextConfigFile } from './lib/create-next-config-file'; import { checkPublicDirectory } from './lib/check-project'; import { NextBuildBuilderOptions } from '../../utils/types'; -import { PHASE_PRODUCTION_BUILD } from '../../utils/constants'; + import { getLockFileName } from 'nx/src/lock-file/lock-file'; export default async function buildExecutor( @@ -33,23 +27,10 @@ export default async function buildExecutor( // Cast to any to overwrite NODE_ENV (process.env as any).NODE_ENV ||= 'production'; - let dependencies: DependentBuildableProjectNode[] = []; const root = resolve(context.root, options.root); - const libsDir = join(context.root, workspaceLayout().libsDir); checkPublicDirectory(root); - if (!options.buildLibsFromSource && context.targetName) { - const result = calculateProjectDependencies( - context.projectGraph, - context.root, - context.projectName, - context.targetName, - context.configurationName - ); - dependencies = result.dependencies; - } - // Set `__NEXT_REACT_ROOT` based on installed ReactDOM version const packageJsonPath = join(root, 'package.json'); const packageJson = existsSync(packageJsonPath) @@ -66,15 +47,7 @@ export default async function buildExecutor( (process.env as any).__NEXT_REACT_ROOT ||= 'true'; } - const config = await prepareConfig( - PHASE_PRODUCTION_BUILD, - options, - context, - dependencies, - libsDir - ); - - await build(root, config as any); + await build(root); if (!directoryExists(options.outputPath)) { mkdir(options.outputPath); diff --git a/packages/next/src/executors/export/export.impl.ts b/packages/next/src/executors/export/export.impl.ts index 11dfc04e0552f3..ae9440fdf207c9 100644 --- a/packages/next/src/executors/export/export.impl.ts +++ b/packages/next/src/executors/export/export.impl.ts @@ -3,7 +3,6 @@ import { ExecutorContext, parseTargetString, readTargetOptions, - runExecutor, workspaceLayout, } from '@nrwl/devkit'; import exportApp from 'next/dist/export'; @@ -13,13 +12,18 @@ import { DependentBuildableProjectNode, } from '@nrwl/js/src/utils/buildable-libs-utils'; -import { prepareConfig } from '../../utils/config'; import { NextBuildBuilderOptions, NextExportBuilderOptions, } from '../../utils/types'; import { PHASE_EXPORT } from '../../utils/constants'; import nextTrace = require('next/dist/trace'); +import { platform } from 'os'; +import { execFileSync } from 'child_process'; +import * as chalk from 'chalk'; + +// platform specific command name +const pmCmd = platform() === 'win32' ? `npx.cmd` : 'npx'; export default async function exportExecutor( options: NextExportBuilderOptions, @@ -38,13 +42,18 @@ export default async function exportExecutor( } const libsDir = join(context.root, workspaceLayout().libsDir); - const buildTarget = parseTargetString(options.buildTarget); - const build = await runExecutor(buildTarget, {}, context); + const buildTarget = parseTargetString( + options.buildTarget, + context.projectGraph + ); - for await (const result of build) { - if (!result.success) { - return result; - } + try { + const args = getBuildTargetCommand(options); + execFileSync(pmCmd, args, { + stdio: [0, 1, 2], + }); + } catch { + throw new Error(`Build target failed: ${chalk.bold(options.buildTarget)}`); } const buildOptions = readTargetOptions( @@ -52,13 +61,6 @@ export default async function exportExecutor( context ); const root = resolve(context.root, buildOptions.root); - const config = await prepareConfig( - PHASE_EXPORT, - buildOptions, - context, - dependencies, - libsDir - ); // Taken from: // https://github.com/vercel/next.js/blob/ead56eaab68409e96c19f7d9139747bac1197aa9/packages/next/cli/next-export.ts#L13 @@ -72,9 +74,13 @@ export default async function exportExecutor( threads: options.threads, outdir: `${buildOptions.outputPath}/exported`, } as any, - nextExportCliSpan, - config + nextExportCliSpan ); return { success: true }; } + +function getBuildTargetCommand(options: NextExportBuilderOptions) { + const cmd = ['nx', 'run', options.buildTarget]; + return cmd; +} diff --git a/packages/next/src/executors/server/server.impl.ts b/packages/next/src/executors/server/server.impl.ts index 00d581803f620f..3b3216692e66b3 100644 --- a/packages/next/src/executors/server/server.impl.ts +++ b/packages/next/src/executors/server/server.impl.ts @@ -5,14 +5,11 @@ import { parseTargetString, readTargetOptions, runExecutor, - workspaceLayout, } from '@nrwl/devkit'; import * as chalk from 'chalk'; import { existsSync } from 'fs'; import { join, resolve } from 'path'; -import { calculateProjectDependencies } from '@nrwl/js/src/utils/buildable-libs-utils'; -import { prepareConfig } from '../../utils/config'; import { NextBuildBuilderOptions, NextServeBuilderOptions, @@ -22,10 +19,6 @@ import { } from '../../utils/types'; import { customServer } from './lib/custom-server'; import { defaultServer } from './lib/default-server'; -import { - PHASE_DEVELOPMENT_SERVER, - PHASE_PRODUCTION_SERVER, -} from '../../utils/constants'; const infoPrefix = `[ ${chalk.dim(chalk.cyan('info'))} ] `; @@ -48,42 +41,16 @@ export default async function* serveExecutor( context ); const root = resolve(context.root, buildOptions.root); - const config = await prepareConfig( - options.dev ? PHASE_DEVELOPMENT_SERVER : PHASE_PRODUCTION_SERVER, - buildOptions, - context, - getDependencies(options, context), - join(context.root, workspaceLayout().libsDir) - ); if (options.customServerTarget) { - yield* runCustomServer(root, config, options, buildOptions, context); - } else { - yield* runNextDevServer(root, config, options, buildOptions, context); - } -} - -function getDependencies( - options: NextServeBuilderOptions, - context: ExecutorContext -) { - if (options.buildLibsFromSource) { - return []; + yield* runCustomServer(root, options, buildOptions, context); } else { - const result = calculateProjectDependencies( - context.projectGraph, - context.root, - context.projectName, - 'build', // should be generalized - context.configurationName - ); - return result.dependencies; + yield* runNextDevServer(root, options, buildOptions, context); } } async function* runNextDevServer( root: string, - config: ReturnType, options: NextServeBuilderOptions, buildOptions: NextBuildBuilderOptions, context: ExecutorContext @@ -94,7 +61,6 @@ async function* runNextDevServer( dir: root, staticMarkup: options.staticMarkup, quiet: options.quiet, - conf: config, port: options.port, customServer: !!options.customServerTarget, hostname: options.hostname || 'localhost', @@ -144,7 +110,6 @@ async function* runNextDevServer( async function* runCustomServer( root: string, - config: ReturnType, options: NextServeBuilderOptions, buildOptions: NextBuildBuilderOptions, context: ExecutorContext diff --git a/packages/next/src/utils/config.spec.ts b/packages/next/src/utils/config.spec.ts index 79b9a20cfcc311..40b1f312c2d622 100644 --- a/packages/next/src/utils/config.spec.ts +++ b/packages/next/src/utils/config.spec.ts @@ -1,5 +1,5 @@ import 'nx/src/utils/testing/mock-fs'; -import { createWebpackConfig, prepareConfig } from './config'; +import { createWebpackConfig } from './config'; import { NextBuildBuilderOptions } from '@nrwl/next'; import { dirname } from 'path'; import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'; @@ -70,84 +70,4 @@ describe('Next.js webpack config builder', () => { expect(config.module.rules.length).toBe(1); }); }); - - describe('prepareConfig', () => { - it('should set the dist directory', async () => { - const config = await prepareConfig( - PHASE_PRODUCTION_BUILD, - { - root: 'apps/wibble', - outputPath: 'dist/apps/wibble', - fileReplacements: [], - }, - { root: '/root' } as any, - [], - '' - ); - - expect(config).toEqual( - expect.objectContaining({ - distDir: '../../dist/apps/wibble/.next', - }) - ); - }); - - it('should support nextConfig option to customize the config', async () => { - const fullPath = require.resolve('./config.fixture'); - const rootPath = dirname(fullPath); - const config = await prepareConfig( - PHASE_PRODUCTION_BUILD, - { - root: 'apps/wibble', - outputPath: 'dist/apps/wibble', - fileReplacements: [], - nextConfig: 'config.fixture', - customValue: 'test', - } as NextBuildBuilderOptions, - { root: rootPath } as any, - [], - '' - ); - - expect(config).toMatchObject({ - myCustomValue: 'test', - }); - }); - - it('should provide error message when nextConfig path is invalid', async () => { - await expect(() => - prepareConfig( - PHASE_PRODUCTION_BUILD, - { - root: 'apps/wibble', - outputPath: 'dist/apps/wibble', - fileReplacements: [], - nextConfig: 'config-does-not-exist.fixture', - customValue: 'test', - } as NextBuildBuilderOptions, - { root: '/root' } as any, - [], - '' - ) - ).rejects.toThrow(/Could not find file/); - }); - - it('should provide error message when nextConfig does not export a function', async () => { - await expect(() => - prepareConfig( - PHASE_PRODUCTION_BUILD, - { - root: 'apps/wibble', - outputPath: 'dist/apps/wibble', - fileReplacements: [], - nextConfig: require.resolve('./config-not-a-function.fixture'), - customValue: 'test', - } as NextBuildBuilderOptions, - { root: '/root' } as any, - [], - '' - ) - ).rejects.toThrow(/option does not export a function/); - }); - }); }); diff --git a/packages/next/src/utils/config.ts b/packages/next/src/utils/config.ts index 6aae860e09f65b..8c8a7d7e423fff 100644 --- a/packages/next/src/utils/config.ts +++ b/packages/next/src/utils/config.ts @@ -1,31 +1,13 @@ -import { - ExecutorContext, - joinPathFragments, - offsetFromRoot, -} from '@nrwl/devkit'; -// ignoring while we support both Next 11.1.0 and versions before it -// @ts-ignore -import type { NextConfig } from 'next/dist/server/config-shared'; -// @ts-ignore -import type { - PHASE_DEVELOPMENT_SERVER, - PHASE_EXPORT, - PHASE_PRODUCTION_BUILD, - PHASE_PRODUCTION_SERVER, -} from 'next/dist/shared/lib/constants'; import { join, resolve } from 'path'; import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'; import { Configuration } from 'webpack'; -import { FileReplacement, NextBuildBuilderOptions } from './types'; +import { FileReplacement } from './types'; import { createCopyPlugin, normalizeAssets } from '@nrwl/webpack'; -import { WithNxOptions } from '../../plugins/with-nx'; import { createTmpTsConfig, DependentBuildableProjectNode, } from '@nrwl/js/src/utils/buildable-libs-utils'; -const loadConfig = require('next/dist/server/config').default; - export function createWebpackConfig( workspaceRoot: string, projectRoot: string, @@ -98,69 +80,3 @@ export function createWebpackConfig( return config; }; } - -export async function prepareConfig( - phase: - | typeof PHASE_PRODUCTION_BUILD - | typeof PHASE_EXPORT - | typeof PHASE_DEVELOPMENT_SERVER - | typeof PHASE_PRODUCTION_SERVER, - options: NextBuildBuilderOptions, - context: ExecutorContext, - dependencies: DependentBuildableProjectNode[], - libsDir: string -) { - const config = (await loadConfig(phase, options.root, null)) as NextConfig & - WithNxOptions; - - const userWebpack = config.webpack; - const userNextConfig = getConfigEnhancer(options.nextConfig, context.root); - // Yes, these do have different capitalisation... - const outputDir = `${offsetFromRoot(options.root)}${options.outputPath}`; - config.distDir = - config.distDir && config.distDir !== '.next' - ? joinPathFragments(outputDir, config.distDir) - : joinPathFragments(outputDir, '.next'); - config.webpack = (a, b) => - createWebpackConfig( - context.root, - options.root, - options.fileReplacements, - options.assets, - dependencies, - libsDir - )(userWebpack ? userWebpack(a, b) : a, b); - - if (typeof userNextConfig !== 'function') { - throw new Error( - `Module specified by 'nextConfig' option does not export a function. It should be of form 'module.exports = (phase, config, options) => config;'` - ); - } - - return userNextConfig(phase, config, { options }); -} - -function getConfigEnhancer( - pluginPath: undefined | string, - workspaceRoot: string -) { - if (!pluginPath) { - return (_, x) => x; - } - - let fullPath: string; - - try { - fullPath = require.resolve(pluginPath); - } catch { - fullPath = join(workspaceRoot, pluginPath); - } - - try { - return require(fullPath); - } catch { - throw new Error( - `Could not find file specified by 'nextConfig' option: ${fullPath}` - ); - } -} diff --git a/packages/next/src/utils/types.ts b/packages/next/src/utils/types.ts index 350fd620ca4111..8d6bb0b7e32825 100644 --- a/packages/next/src/utils/types.ts +++ b/packages/next/src/utils/types.ts @@ -17,7 +17,6 @@ export interface NextServerOptions { dir: string; staticMarkup: boolean; quiet: boolean; - conf: any; port: number; path: string; hostname: string;