Skip to content

Commit

Permalink
feat(nextjs): Add support for create nodes for nextjs (nrwl#20193)
Browse files Browse the repository at this point in the history
  • Loading branch information
ndcunningham authored Dec 6, 2023
1 parent 7ffc328 commit b8d24e6
Show file tree
Hide file tree
Showing 20 changed files with 618 additions and 83 deletions.
63 changes: 63 additions & 0 deletions e2e/next-core/src/next-pcv3.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
runCLI,
cleanupProject,
newProject,
uniq,
updateJson,
runE2ETests,
directoryExists,
readJson,
} from 'e2e/utils';

describe('@nx/next/plugin', () => {
let project: string;
let appName: string;

beforeAll(() => {
project = newProject();
appName = uniq('app');
runCLI(
`generate @nx/next:app ${appName} --project-name-and-root-format=as-provided --no-interactive`,
{ env: { NX_PCV3: 'true' } }
);

// update package.json to add next as a script
updateJson(`package.json`, (json) => {
json.scripts = json.scripts || {};
json.scripts.next = 'next';
return json;
});
});

afterAll(() => cleanupProject());

it('nx.json should contain plugin configuration', () => {
const nxJson = readJson('nx.json');
const nextPlugin = nxJson.plugins.find(
(plugin) => plugin.plugin === '@nx/next/plugin'
);
expect(nextPlugin).toBeDefined();
expect(nextPlugin.options).toBeDefined();
expect(nextPlugin.options.buildTargetName).toEqual('build');
expect(nextPlugin.options.startTargetName).toEqual('start');
expect(nextPlugin.options.devTargetName).toEqual('dev');
});

it('should build the app', async () => {
const result = runCLI(`build ${appName}`);
// check build output for PCV3 artifacts (e.g. .next directory) are inside the project directory
directoryExists(`${appName}/.next`);

expect(result).toContain(
`Successfully ran target build for project ${appName}`
);
}, 200_000);

it('should serve the app', async () => {
if (runE2ETests()) {
const e2eResult = runCLI(`run ${appName}-e2e:e2e --verbose`);

expect(e2eResult).toContain('All specs passed!');
}
}, 500_000);
});
2 changes: 1 addition & 1 deletion e2e/next-core/src/next-webpack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,6 @@ describe('Next.js Webpack', () => {
expect(() => {
runCLI(`build ${appName}`);
}).not.toThrow();
checkFilesExist(`apps/${appName}/.next/build-manifest.json`);
checkFilesExist(`dist/apps/${appName}/.next/build-manifest.json`);
}, 300_000);
});
31 changes: 31 additions & 0 deletions e2e/next-extensions/src/next-experimental.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,37 @@ describe('Next.js Experimental Features', () => {
`
);

updateFile(
`apps/${appName}/next.config.js`,
`
//@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,
},
experimental: {
serverActions: true
}
};
const plugins = [
// Add more Next.js plugins to this list if needed.
withNx,
];
module.exports = composePlugins(...plugins)(nextConfig);
`
);

await checkApp(appName, {
checkUnitTest: false,
checkLint: true,
Expand Down
2 changes: 1 addition & 1 deletion packages/cypress/plugins/cypress-preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ function waitForServer(
let pollTimeout: NodeJS.Timeout | null;
const { protocol } = new URL(url);

const timeoutDuration = webServerConfig?.timeout ?? 5 * 1000;
const timeoutDuration = webServerConfig?.timeout ?? 10 * 1000;
const timeout = setTimeout(() => {
clearTimeout(pollTimeout);
reject(
Expand Down
2 changes: 1 addition & 1 deletion packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"next": ">=13.0.0"
},
"dependencies": {
"@nx/devkit": "file:../devkit",
"@babel/plugin-proposal-decorators": "^7.22.7",
"@svgr/webpack": "^8.0.1",
"chalk": "^4.1.0",
Expand All @@ -44,7 +45,6 @@
"url-loader": "^4.1.1",
"tslib": "^2.3.0",
"webpack-merge": "^5.8.0",
"@nx/devkit": "file:../devkit",
"@nx/js": "file:../js",
"@nx/eslint": "file:../eslint",
"@nx/react": "file:../react",
Expand Down
1 change: 1 addition & 0 deletions packages/next/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { createNodes, NextPluginOptions } from './src/plugins/plugin';
60 changes: 26 additions & 34 deletions packages/next/plugins/with-nx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@
import type { NextConfig } from 'next';
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 {
ExecutorContext,
ProjectGraph,
ProjectGraphProjectNode,
Target,
import {
type ExecutorContext,
type ProjectGraph,
type ProjectGraphProjectNode,
type Target,
} from '@nx/devkit';

const baseNXEnvironmentVariables = [
Expand Down Expand Up @@ -48,6 +47,7 @@ const baseNXEnvironmentVariables = [
'NX_MAPPINGS',
'NX_FILE_TO_RUN',
'NX_NEXT_PUBLIC_DIR',
'NX_CYPRESS_COMPONENT_TEST',
];

export interface WithNxOptions extends NextConfig {
Expand Down Expand Up @@ -150,7 +150,10 @@ function withNx(
const { PHASE_PRODUCTION_SERVER, PHASE_DEVELOPMENT_SERVER } = await import(
'next/constants'
);
if (PHASE_PRODUCTION_SERVER === phase) {
if (
PHASE_PRODUCTION_SERVER === phase ||
!process.env.NX_TASK_TARGET_TARGET
) {
// If we are running an already built production server, just return the configuration.
// NOTE: Avoid any `require(...)` or `import(...)` statements here. Development dependencies are not available at production runtime.
const { nx, ...validNextConfig } = _nextConfig;
Expand All @@ -161,15 +164,22 @@ function withNx(
} else {
const {
createProjectGraphAsync,
readCachedProjectGraph,
joinPathFragments,
offsetFromRoot,
workspaceRoot,
} = require('@nx/devkit');

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

const graph = await createProjectGraphAsync();
let graph = readCachedProjectGraph();
if (!graph) {
try {
graph = await createProjectGraphAsync();
} catch (e) {
throw new Error(
'Could not create project graph. Please ensure that your workspace is valid.'
);
}
}

const originalTarget = {
project: process.env.NX_TASK_TARGET_PROJECT,
Expand All @@ -181,25 +191,9 @@ function withNx(
node: projectNode,
options,
projectName: project,
targetName,
configurationName,
} = getNxContext(graph, originalTarget);
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,
project,
targetName,
configurationName
);
dependencies = result.dependencies;
}

// Get next config
const nextConfig = getNextConfig(_nextConfig, context);

Expand Down Expand Up @@ -229,18 +223,16 @@ function withNx(

// outputPath may be undefined if using run-commands or other executors other than @nx/next:build.
// In this case, the user should set distDir in their next.config.js.
if (options.outputPath) {
if (options.outputPath && phase !== PHASE_DEVELOPMENT_SERVER) {
const outputDir = `${offsetFromRoot(projectDirectory)}${
options.outputPath
}`;
// If running dev-server, we should keep `.next` inside project directory since Turbopack expects this.
// See: https://github.com/nrwl/nx/issues/19365
if (phase !== PHASE_DEVELOPMENT_SERVER) {
nextConfig.distDir =
nextConfig.distDir && nextConfig.distDir !== '.next'
? joinPathFragments(outputDir, nextConfig.distDir)
: joinPathFragments(outputDir, '.next');
}
nextConfig.distDir =
nextConfig.distDir && nextConfig.distDir !== '.next'
? joinPathFragments(outputDir, nextConfig.distDir)
: joinPathFragments(outputDir, '.next');
}

const userWebpackConfig = nextConfig.webpack;
Expand Down
9 changes: 6 additions & 3 deletions packages/next/src/executors/export/export.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
ExecutorContext,
parseTargetString,
readTargetOptions,
targetToTargetString,
workspaceLayout,
} from '@nx/devkit';
import exportApp from 'next/dist/export';
Expand Down Expand Up @@ -53,10 +54,12 @@ export default async function exportExecutor(
dependencies = result.dependencies;
}

// Returns { project: ProjectGraphNode; target: string; configuration?: string;}
const buildTarget = parseTargetString(options.buildTarget, context);

try {
const args = getBuildTargetCommand(options);
const buildTargetName = targetToTargetString(buildTarget);
const args = getBuildTargetCommand(buildTargetName);
execFileSync(pmCmd, args, {
stdio: [0, 1, 2],
});
Expand Down Expand Up @@ -88,7 +91,7 @@ export default async function exportExecutor(
return { success: true };
}

function getBuildTargetCommand(options: NextExportBuilderOptions) {
const cmd = ['nx', 'run', options.buildTarget];
function getBuildTargetCommand(buildTarget: string) {
const cmd = ['nx', 'run', buildTarget];
return cmd;
}
2 changes: 1 addition & 1 deletion packages/next/src/executors/server/server.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default async function* serveExecutor(
}

const buildOptions = readTargetOptions<NextBuildBuilderOptions>(
parseTargetString(options.buildTarget, context.projectGraph),
parseTargetString(options.buildTarget, context),
context
);
const projectRoot = context.workspace.projects[context.projectName].root;
Expand Down
43 changes: 43 additions & 0 deletions packages/next/src/generators/application/application.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Tree,
} from '@nx/devkit';

import { Schema } from './schema';
import { applicationGenerator } from './application';

describe('app', () => {
Expand Down Expand Up @@ -731,6 +732,48 @@ describe('app', () => {
});
});

describe('app with Project Configuration V3 enabeled', () => {
let tree: Tree;
let originalPVC3;

const schema: Schema = {
name: 'app',
appDir: true,
unitTestRunner: 'jest',
style: 'css',
e2eTestRunner: 'cypress',
projectNameAndRootFormat: 'as-provided',
};

beforeAll(() => {
tree = createTreeWithEmptyWorkspace();
originalPVC3 = process.env['NX_PCV3'];
process.env['NX_PCV3'] = 'true';
});

afterAll(() => {
if (originalPVC3) {
process.env['NX_PCV3'] = originalPVC3;
} else {
delete process.env['NX_PCV3'];
}
});

it('should not generate build serve and export targets', async () => {
const name = uniq();

await applicationGenerator(tree, {
...schema,
name,
});

const projectConfiguration = readProjectConfiguration(tree, name);
expect(projectConfiguration.targets.build).toBeUndefined();
expect(projectConfiguration.targets.serve).toBeUndefined();
expect(projectConfiguration.targets.export).toBeUndefined();
});
});

function uniq() {
return `str-${(Math.random() * 10000).toFixed(0)}`;
}
2 changes: 2 additions & 0 deletions packages/next/src/generators/application/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { addLinting } from './lib/add-linting';
import { customServerGenerator } from '../custom-server/custom-server';
import { updateCypressTsConfig } from './lib/update-cypress-tsconfig';
import { showPossibleWarnings } from './lib/show-possible-warnings';
import { addPlugin } from './lib/add-plugin';

export async function applicationGenerator(host: Tree, schema: Schema) {
return await applicationGeneratorInternal(host, {
Expand All @@ -41,6 +42,7 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
tasks.push(nextTask);

createApplicationFiles(host, options);

addProject(host, options);

const e2eTask = await addE2e(host, options);
Expand Down
Loading

0 comments on commit b8d24e6

Please sign in to comment.