From 73227515edd60dc9c7d1d5ddaba0bd76549f7127 Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Thu, 13 Apr 2023 17:41:17 -0400 Subject: [PATCH] feat(nextjs): add composePlugins util when using multiple plugins (#16296) --- packages/next/index.ts | 2 + packages/next/plugins/with-less.ts | 2 +- packages/next/plugins/with-nx.ts | 6 +- packages/next/plugins/with-stylus.ts | 2 +- .../files/common/next.config.js__tmpl__ | 32 ++++++++-- packages/next/src/utils/config.spec.ts | 59 ++++++++++++++++++- packages/next/src/utils/config.ts | 38 ++++++++++++ 7 files changed, 128 insertions(+), 13 deletions(-) diff --git a/packages/next/index.ts b/packages/next/index.ts index dec99640c0ca4..1e5bc580c9a33 100644 --- a/packages/next/index.ts +++ b/packages/next/index.ts @@ -5,3 +5,5 @@ export { applicationGenerator } from './src/generators/application/application'; 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'; diff --git a/packages/next/plugins/with-less.ts b/packages/next/plugins/with-less.ts index f7dc5336fdd7a..979d5d523bc31 100644 --- a/packages/next/plugins/with-less.ts +++ b/packages/next/plugins/with-less.ts @@ -1,6 +1,6 @@ // Adapted from https://raw.githubusercontent.com/elado/next-with-less/main/src/index.js import { merge } from 'webpack-merge'; -import { NextConfigFn } from './with-nx'; +import { NextConfigFn } from '../src/utils/config'; const addLessToRegExp = (rx) => new RegExp(rx.source.replace('|sass', '|sass|less'), rx.flags); diff --git a/packages/next/plugins/with-nx.ts b/packages/next/plugins/with-nx.ts index b0ecd6c36b6fd..ba2861b8a983a 100644 --- a/packages/next/plugins/with-nx.ts +++ b/packages/next/plugins/with-nx.ts @@ -16,7 +16,7 @@ import type { NextConfig } from 'next'; import { PHASE_PRODUCTION_SERVER } from 'next/constants'; import * as path from 'path'; -import { createWebpackConfig } from '../src/utils/config'; +import { createWebpackConfig, NextConfigFn } from '../src/utils/config'; import { NextBuildBuilderOptions } from '../src/utils/types'; export interface WithNxOptions extends NextConfig { @@ -25,10 +25,6 @@ export interface WithNxOptions extends NextConfig { }; } -export interface NextConfigFn { - (phase: string): Promise; -} - export interface WithNxContext { workspaceRoot: string; libsDir: string; diff --git a/packages/next/plugins/with-stylus.ts b/packages/next/plugins/with-stylus.ts index 1f4f1ce7d7287..f82aff4607af2 100644 --- a/packages/next/plugins/with-stylus.ts +++ b/packages/next/plugins/with-stylus.ts @@ -1,6 +1,6 @@ // Adapted from https://raw.githubusercontent.com/elado/next-with-less/main/src/index.js import { merge } from 'webpack-merge'; -import { NextConfigFn } from './with-nx'; +import { NextConfigFn } from '../src/utils/config'; const addStylusToRegExp = (rx) => new RegExp(rx.source.replace('|sass', '|sass|styl'), rx.flags); diff --git a/packages/next/src/generators/application/files/common/next.config.js__tmpl__ b/packages/next/src/generators/application/files/common/next.config.js__tmpl__ index 69c955d9d1223..84f60029cb1a1 100644 --- a/packages/next/src/generators/application/files/common/next.config.js__tmpl__ +++ b/packages/next/src/generators/application/files/common/next.config.js__tmpl__ @@ -1,7 +1,7 @@ //@ts-check // eslint-disable-next-line @typescript-eslint/no-var-requires -const { withNx } = require('@nrwl/next/plugins/with-nx'); +const { composePlugins, withNx } = require('@nrwl/next'); <% if (style === 'less') { %> // This plugin is needed until this PR is merged. @@ -24,7 +24,13 @@ const nextConfig = { <% } %> }; -module.exports = withLess(withNx(nextConfig)); +const plugins = [ + // Add more Next.js plugins to this list if needed. + withLess, + withNx, +]; + +module.exports = composePlugins(...plugins)(nextConfig)); <% } else if (style === 'styl') { %> const { withStylus } = require('@nrwl/next/plugins/with-stylus'); @@ -44,7 +50,13 @@ const nextConfig = { <% } %> }; -module.exports = withStylus(withNx(nextConfig)); +const plugins = [ + // Add more Next.js plugins to this list if needed. + withStylus, + withNx, +]; + +module.exports = composePlugins(...plugins)(nextConfig); <% } else if ( style === 'styled-components' ||style === '@emotion/styled' @@ -67,7 +79,12 @@ const nextConfig = { <% } %> }; -module.exports = withNx(nextConfig); +const plugins = [ + // Add more Next.js plugins to this list if needed. + withNx, +]; + +module.exports = composePlugins(...plugins)(nextConfig); <% } else { // Defaults to CSS/SASS (which don't need plugin as of Next 9.3) %> /** @@ -86,5 +103,10 @@ const nextConfig = { <% } %> }; -module.exports = withNx(nextConfig); +const plugins = [ + // Add more Next.js plugins to this list if needed. + withNx, +]; + +module.exports = composePlugins(...plugins)(nextConfig); <% } %> diff --git a/packages/next/src/utils/config.spec.ts b/packages/next/src/utils/config.spec.ts index ebf903a44add4..8ae787ab0668f 100644 --- a/packages/next/src/utils/config.spec.ts +++ b/packages/next/src/utils/config.spec.ts @@ -1,5 +1,6 @@ +import type { NextConfig } from 'next'; import 'nx/src/utils/testing/mock-fs'; -import { createWebpackConfig } from './config'; +import { composePlugins, createWebpackConfig, NextConfigFn } from './config'; import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'; jest.mock('@nrwl/webpack', () => ({})); @@ -75,4 +76,60 @@ 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', + }, + }); + }); + }); }); diff --git a/packages/next/src/utils/config.ts b/packages/next/src/utils/config.ts index 3e6a0cc885bf1..1b50517d7a333 100644 --- a/packages/next/src/utils/config.ts +++ b/packages/next/src/utils/config.ts @@ -1,3 +1,4 @@ +import type { NextConfig } from 'next'; import { join, resolve } from 'path'; import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'; import { Configuration, RuleSetRule } from 'webpack'; @@ -7,6 +8,7 @@ import { createTmpTsConfig, DependentBuildableProjectNode, } from '@nrwl/js/src/utils/buildable-libs-utils'; +import { NxWebpackExecutionContext } from '@nrwl/webpack'; export function createWebpackConfig( workspaceRoot: string, @@ -94,3 +96,39 @@ function isTsRule(r: RuleSetRule): boolean { return r.test.test('a.ts'); } + +export interface NextConfigFn { + (phase: string, context?: any): Promise | 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 { + 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; + }; + }; +}