From 3bc5d5089713793dcc859f189cf5011372068717 Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Thu, 27 Jun 2024 10:44:39 -0400 Subject: [PATCH] fix(nextjs): allow non-supported next confile to still migrate --- .../convert-to-inferred.ts | 5 +- .../lib/build-post-target-transformer.ts | 2 +- .../lib/skip-project-filter.ts | 44 ------ .../lib/update-next-config.spec.ts | 138 +++++++++++++++++- .../lib/update-next-config.ts | 25 +++- 5 files changed, 160 insertions(+), 54 deletions(-) delete mode 100644 packages/next/src/generators/convert-to-inferred/lib/skip-project-filter.ts diff --git a/packages/next/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/next/src/generators/convert-to-inferred/convert-to-inferred.ts index 498ef29c650ea..dfe5208ff0ddd 100644 --- a/packages/next/src/generators/convert-to-inferred/convert-to-inferred.ts +++ b/packages/next/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -1,10 +1,9 @@ -import { Tree, createProjectGraphAsync, formatFiles } from '@nx/devkit'; +import { createProjectGraphAsync, formatFiles, Tree } from '@nx/devkit'; import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; import { migrateProjectExecutorsToPluginV1 } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; import { createNodes } from '../../plugins/plugin'; import { buildPostTargetTransformer } from './lib/build-post-target-transformer'; import { servePosTargetTransformer } from './lib/serve-post-target-tranformer'; -import { skipProjectFilterFactory } from './lib/skip-project-filter'; interface Schema { project?: string; @@ -33,7 +32,6 @@ export async function convertToInferred(tree: Tree, options: Schema) { targetPluginOptionMapper: (targetName) => ({ buildTargetName: targetName, }), - skipProjectFilter: skipProjectFilterFactory(tree), }, { executors: ['@nx/next:server'], @@ -41,7 +39,6 @@ export async function convertToInferred(tree: Tree, options: Schema) { targetPluginOptionMapper: (targetName) => ({ devTargetName: targetName, }), - skipProjectFilter: skipProjectFilterFactory(tree), }, ], options.project diff --git a/packages/next/src/generators/convert-to-inferred/lib/build-post-target-transformer.ts b/packages/next/src/generators/convert-to-inferred/lib/build-post-target-transformer.ts index 93568470f02e7..02fb153bfadcf 100644 --- a/packages/next/src/generators/convert-to-inferred/lib/build-post-target-transformer.ts +++ b/packages/next/src/generators/convert-to-inferred/lib/build-post-target-transformer.ts @@ -73,7 +73,7 @@ export function buildPostTargetTransformer(migrationLogs: AggregatedLog) { ...configValues[configuration], }; `; - updateNextConfig(tree, partialNextConfig, projectDetails.root); + updateNextConfig(tree, partialNextConfig, projectDetails, migrationLogs); return target; }; } diff --git a/packages/next/src/generators/convert-to-inferred/lib/skip-project-filter.ts b/packages/next/src/generators/convert-to-inferred/lib/skip-project-filter.ts deleted file mode 100644 index 94ed43dc10980..0000000000000 --- a/packages/next/src/generators/convert-to-inferred/lib/skip-project-filter.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ProjectConfiguration, Tree } from '@nx/devkit'; -import { findNextConfigPath } from './utils'; -import { tsquery } from '@phenomnomnominal/tsquery'; - -export function skipProjectFilterFactory(tree: Tree) { - /** - * We should skip if the following are true: - * - If the project contains a next.config.mjs file - * - If the next config contains does not contain composePlugins - */ - return function skipProjectFilter( - projectConfiguration: ProjectConfiguration - ): false | string { - const nextConfigPath = findNextConfigPath(tree, projectConfiguration.root); - if (!nextConfigPath) { - return `The project (${projectConfiguration.root}) does not have a valid next config file. Only .js and .cjs files are supported.`; - } - - if (!checkForComposePlugins(tree, projectConfiguration.root)) { - return `The project (${projectConfiguration.root}) does not have a composePlugins function in ${nextConfigPath}. Migration is not supported.`; - } - - return false; - }; -} - -export function checkForComposePlugins( - tree: Tree, - projectRoot: string -): boolean { - const nextConfigPath = findNextConfigPath(tree, projectRoot); - if (!nextConfigPath) { - return false; - } - - const nextConfigContents = tree.read(nextConfigPath, 'utf-8'); - const ast = tsquery.ast(nextConfigContents); - - // Query to check for composePlugins in module.exports - const composePluginsQuery = `ExpressionStatement > BinaryExpression > CallExpression > CallExpression:has(Identifier[name=composePlugins])`; - const matches = tsquery(ast, composePluginsQuery); - - return matches.length > 0; -} diff --git a/packages/next/src/generators/convert-to-inferred/lib/update-next-config.spec.ts b/packages/next/src/generators/convert-to-inferred/lib/update-next-config.spec.ts index c28bc82c81415..c22b7ef21b67d 100644 --- a/packages/next/src/generators/convert-to-inferred/lib/update-next-config.spec.ts +++ b/packages/next/src/generators/convert-to-inferred/lib/update-next-config.spec.ts @@ -4,13 +4,145 @@ import { updateNextConfig } from './update-next-config'; describe('UpdateNextConfig', () => { let tree: Tree; + const mockLog = { + addLog: jest.fn(), + logs: new Map(), + flushLogs: jest.fn(), + reset: jest.fn(), + }; beforeEach(() => { tree = createTreeWithEmptyWorkspace(); }); - // TODO: Add tests - it('should work', () => { - expect(true).toBe(true); + it('should update the next config file adding the options passed in', () => { + const initConfig = ` + //@ts-check + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { composePlugins, withNx } = require('@nx/next'); + + /** + * @type {import('@nx/next/plugins/with-nx').WithNxOptions} + **/ + const nextConfig = { + nx: { + // Set this to true if you would like to use SVGR + // See: https://github.com/gregberge/svgr + svgr: false, + }, + }; + + const plugins = [ + // Add more Next.js plugins to this list if needed. + withNx, + ]; + + module.exports = composePlugins(...plugins)(nextConfig); + `; + + const projectName = 'my-app'; + tree.write(`${projectName}/next.config.js`, initConfig); + + const executorOptionsString = ` + const configValues = { + default: { + fileReplacements: [ + { + replace: './environments/environment.ts', + with: './environments/environment.foo.ts', + }, + ], + }, + development: {}, + }; + const configuration = process.env.NX_TASK_TARGET_CONFIGURATION || 'default'; + const options = { + ...configValues.default, + //@ts-expect-error: Ignore TypeScript error for indexing configValues with a dynamic key + ...configValues[configuration], + };`; + + const projectDetails = { projectName, root: projectName }; + updateNextConfig(tree, executorOptionsString, projectDetails, mockLog); + + const result = tree.read(`${projectName}/next.config.js`, 'utf-8'); + expect(result).toMatchInlineSnapshot(` + "//@ts-check + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { composePlugins, withNx } = require('@nx/next'); + const configValues = { + default: { + fileReplacements: [ + { + replace: './environments/environment.ts', + with: './environments/environment.foo.ts', + }, + ], + }, + development: {}, + }; + const configuration = process.env.NX_TASK_TARGET_CONFIGURATION || 'default'; + const options = { + ...configValues.default, + //@ts-expect-error: Ignore TypeScript error for indexing configValues with a dynamic key + ...configValues[configuration], + }; + ; + /** + * @type {import('@nx/next/plugins/with-nx').WithNxOptions} + **/ + const nextConfig = { + nx: { + // Set this to true if you would like to use SVGR + // See: https://github.com/gregberge/svgr + svgr: false, + ...options + }, + }; + const plugins = [ + // Add more Next.js plugins to this list if needed. + withNx, + ]; + module.exports = composePlugins(...plugins)(nextConfig); + " + `); + }); + + it('should warm the user if the next config file is not support (.mjs)', () => { + const initConfig = `export default {}`; + const projectDetails = { projectName: 'mjs-config', root: 'mjs-config' }; + tree.write(`${projectDetails.root}/next.config.mjs`, initConfig); + + updateNextConfig(tree, '', projectDetails, mockLog); + + expect(mockLog.addLog).toHaveBeenCalledWith({ + executorName: '@nx/next:build', + log: 'The project mjs-config does not use a supported Next.js config file format. Only .js and .cjs files using "composePlugins" is supported. Leaving it as is.', + project: 'mjs-config', + }); + }); + + it('should warm the user if composePlugins is not found in the next config file', () => { + // Example of a typical next.config.js file + const initConfig = ` + module.exports = { + distDir: 'dist', + reactStrictMode: true, + }; + `; + const projectDetails = { + projectName: 'no-compose-plugins', + root: 'no-compose-plugins', + }; + tree.write(`${projectDetails.root}/next.config.js`, initConfig); + + updateNextConfig(tree, '', projectDetails, mockLog); + + expect(mockLog.addLog).toHaveBeenCalledWith({ + executorName: '@nx/next:build', + log: 'The project no-compose-plugins does not use a supported Next.js config file format. Only .js and .cjs files using "composePlugins" is supported. Leaving it as is.', + project: 'no-compose-plugins', + }); }); }); diff --git a/packages/next/src/generators/convert-to-inferred/lib/update-next-config.ts b/packages/next/src/generators/convert-to-inferred/lib/update-next-config.ts index 304cb0265f966..a4dda729c4947 100644 --- a/packages/next/src/generators/convert-to-inferred/lib/update-next-config.ts +++ b/packages/next/src/generators/convert-to-inferred/lib/update-next-config.ts @@ -2,20 +2,40 @@ import { Tree } from '@nx/devkit'; import { findNextConfigPath } from './utils'; import { tsquery } from '@phenomnomnominal/tsquery'; import * as ts from 'typescript'; +import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; export function updateNextConfig( tree: Tree, updatedConfigFileContents: string, - projectRoot: string + project: { projectName: string; root: string }, + migrationLogs: AggregatedLog ) { - const nextConfigPath = findNextConfigPath(tree, projectRoot); + const nextConfigPath = findNextConfigPath(tree, project.root); if (!nextConfigPath) { + migrationLogs.addLog({ + project: project.projectName, + executorName: '@nx/next:build', + log: `The project ${project.projectName} does not use a supported Next.js config file format. Only .js and .cjs files using "composePlugins" is supported. Leaving it as is.`, + }); return; } const nextConfigContents = tree.read(nextConfigPath, 'utf-8'); let ast = tsquery.ast(nextConfigContents); + // Query to check for composePlugins in module.exports + const composePluginsQuery = `ExpressionStatement > BinaryExpression > CallExpression > CallExpression:has(Identifier[name=composePlugins])`; + const composePluginNode = tsquery(ast, composePluginsQuery)[0]; + + if (!composePluginNode) { + migrationLogs.addLog({ + project: project.projectName, + executorName: '@nx/next:build', + log: `The project ${project.projectName} does not use a supported Next.js config file format. Only .js and .cjs files using "composePlugins" is supported. Leaving it as is.`, + }); + return; + } + let lastRequireEndPosition = -1; const findLastRequire = (node: ts.Node) => { @@ -68,6 +88,7 @@ export function updateNextConfig( } return ts.visitEachChild(node, visit, context); } + return ts.visitNode(rootNode, visit); };