feat(nextjs): Add support for create nodes for nextjs (#20193)
This commit is contained in:
parent
7ffc3284f6
commit
b8d24e6d0e
63
e2e/next-core/src/next-pcv3.test.ts
Normal file
63
e2e/next-core/src/next-pcv3.test.ts
Normal file
@ -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);
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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",
|
||||
@ -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",
|
||||
|
||||
1
packages/next/plugin.ts
Normal file
1
packages/next/plugin.ts
Normal file
@ -0,0 +1 @@
|
||||
export { createNodes, NextPluginOptions } from './src/plugins/plugin';
|
||||
@ -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 = [
|
||||
@ -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 {
|
||||
@ -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;
|
||||
@ -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,
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -2,6 +2,7 @@ import {
|
||||
ExecutorContext,
|
||||
parseTargetString,
|
||||
readTargetOptions,
|
||||
targetToTargetString,
|
||||
workspaceLayout,
|
||||
} from '@nx/devkit';
|
||||
import exportApp from 'next/dist/export';
|
||||
@ -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],
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
Tree,
|
||||
} from '@nx/devkit';
|
||||
|
||||
import { Schema } from './schema';
|
||||
import { applicationGenerator } from './application';
|
||||
|
||||
describe('app', () => {
|
||||
@ -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)}`;
|
||||
}
|
||||
|
||||
@ -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, {
|
||||
@ -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);
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
ensurePackage,
|
||||
getPackageManagerCommand,
|
||||
joinPathFragments,
|
||||
readNxJson,
|
||||
Tree,
|
||||
} from '@nx/devkit';
|
||||
import { Linter } from '@nx/eslint';
|
||||
@ -11,6 +12,12 @@ import { nxVersion } from '../../../utils/versions';
|
||||
import { NormalizedSchema } from './normalize-options';
|
||||
|
||||
export async function addE2e(host: Tree, options: NormalizedSchema) {
|
||||
const nxJson = readNxJson(host);
|
||||
const hasPlugin = nxJson.plugins?.some((p) =>
|
||||
typeof p === 'string'
|
||||
? p === '@nx/next/plugin'
|
||||
: p.plugin === '@nx/next/plugin'
|
||||
);
|
||||
if (options.e2eTestRunner === 'cypress') {
|
||||
const { configurationGenerator } = ensurePackage<
|
||||
typeof import('@nx/cypress')
|
||||
@ -28,8 +35,10 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
|
||||
project: options.e2eProjectName,
|
||||
directory: 'src',
|
||||
skipFormat: true,
|
||||
devServerTarget: `${options.projectName}:serve`,
|
||||
baseUrl: 'http://localhost:4200',
|
||||
devServerTarget: `${options.projectName}:${
|
||||
hasPlugin ? 'start' : 'serve'
|
||||
}`,
|
||||
baseUrl: `http://localhost:${hasPlugin ? '3000' : '4200'}`,
|
||||
jsx: true,
|
||||
});
|
||||
} else if (options.e2eTestRunner === 'playwright') {
|
||||
@ -50,10 +59,10 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
|
||||
js: false,
|
||||
linter: options.linter,
|
||||
setParserOptionsProject: options.setParserOptionsProject,
|
||||
webServerAddress: 'http://127.0.0.1:4200',
|
||||
webServerCommand: `${getPackageManagerCommand().exec} nx serve ${
|
||||
options.projectName
|
||||
}`,
|
||||
webServerAddress: `http://127.0.0.1:${hasPlugin ? '3000' : '4200'}`,
|
||||
webServerCommand: `${getPackageManagerCommand().exec} nx ${
|
||||
hasPlugin ? 'start' : 'serve'
|
||||
} ${options.projectName}`,
|
||||
});
|
||||
}
|
||||
return () => {};
|
||||
|
||||
27
packages/next/src/generators/application/lib/add-plugin.ts
Normal file
27
packages/next/src/generators/application/lib/add-plugin.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Tree, readNxJson, updateNxJson } from '@nx/devkit';
|
||||
|
||||
export function addPlugin(tree: Tree) {
|
||||
const nxJson = readNxJson(tree);
|
||||
nxJson.plugins ??= [];
|
||||
|
||||
for (const plugin of nxJson.plugins) {
|
||||
if (
|
||||
typeof plugin === 'string'
|
||||
? plugin === '@nx/next/plugin'
|
||||
: plugin.plugin === '@nx/next/plugin'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
nxJson.plugins.push({
|
||||
plugin: '@nx/next/plugin',
|
||||
options: {
|
||||
buildTargetName: 'build',
|
||||
serveTargetName: 'serve',
|
||||
exportTargetName: 'export',
|
||||
},
|
||||
});
|
||||
|
||||
updateNxJson(tree, nxJson);
|
||||
}
|
||||
@ -2,52 +2,65 @@ import { NormalizedSchema } from './normalize-options';
|
||||
import {
|
||||
addProjectConfiguration,
|
||||
ProjectConfiguration,
|
||||
readNxJson,
|
||||
Tree,
|
||||
} from '@nx/devkit';
|
||||
|
||||
export function addProject(host: Tree, options: NormalizedSchema) {
|
||||
const targets: Record<string, any> = {};
|
||||
|
||||
targets.build = {
|
||||
executor: '@nx/next:build',
|
||||
outputs: ['{options.outputPath}'],
|
||||
defaultConfiguration: 'production',
|
||||
options: {
|
||||
outputPath: options.outputPath,
|
||||
},
|
||||
configurations: {
|
||||
development: {
|
||||
outputPath: options.appProjectRoot,
|
||||
},
|
||||
production: {},
|
||||
},
|
||||
};
|
||||
// Check if plugin exists in nx.json and if it doesn't then we can continue
|
||||
// with the default targets.
|
||||
|
||||
targets.serve = {
|
||||
executor: '@nx/next:server',
|
||||
defaultConfiguration: 'development',
|
||||
options: {
|
||||
buildTarget: `${options.projectName}:build`,
|
||||
dev: true,
|
||||
},
|
||||
configurations: {
|
||||
development: {
|
||||
buildTarget: `${options.projectName}:build:development`,
|
||||
const nxJson = readNxJson(host);
|
||||
const hasPlugin = nxJson.plugins?.some((p) =>
|
||||
typeof p === 'string'
|
||||
? p === '@nx/next/plugin'
|
||||
: p.plugin === '@nx/next/plugin'
|
||||
);
|
||||
|
||||
if (!hasPlugin) {
|
||||
targets.build = {
|
||||
executor: '@nx/next:build',
|
||||
outputs: ['{options.outputPath}'],
|
||||
defaultConfiguration: 'production',
|
||||
options: {
|
||||
outputPath: options.outputPath,
|
||||
},
|
||||
configurations: {
|
||||
development: {
|
||||
outputPath: options.appProjectRoot,
|
||||
},
|
||||
production: {},
|
||||
},
|
||||
};
|
||||
|
||||
targets.serve = {
|
||||
executor: '@nx/next:server',
|
||||
defaultConfiguration: 'development',
|
||||
options: {
|
||||
buildTarget: `${options.projectName}:build`,
|
||||
dev: true,
|
||||
},
|
||||
production: {
|
||||
buildTarget: `${options.projectName}:build:production`,
|
||||
dev: false,
|
||||
configurations: {
|
||||
development: {
|
||||
buildTarget: `${options.projectName}:build:development`,
|
||||
dev: true,
|
||||
},
|
||||
production: {
|
||||
buildTarget: `${options.projectName}:build:production`,
|
||||
dev: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
targets.export = {
|
||||
executor: '@nx/next:export',
|
||||
options: {
|
||||
buildTarget: `${options.projectName}:build:production`,
|
||||
},
|
||||
};
|
||||
targets.export = {
|
||||
executor: '@nx/next:export',
|
||||
options: {
|
||||
buildTarget: `${options.projectName}:build:production`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const project: ProjectConfiguration = {
|
||||
root: options.appProjectRoot,
|
||||
|
||||
@ -2,6 +2,7 @@ import {
|
||||
addDependenciesToPackageJson,
|
||||
ensurePackage,
|
||||
GeneratorCallback,
|
||||
readNxJson,
|
||||
runTasksInSerial,
|
||||
Tree,
|
||||
} from '@nx/devkit';
|
||||
@ -18,6 +19,7 @@ import {
|
||||
} from '../../utils/versions';
|
||||
import { InitSchema } from './schema';
|
||||
import { addGitIgnoreEntry } from '../../utils/add-gitignore-entry';
|
||||
import { addPlugin } from './lib/add-plugin';
|
||||
|
||||
function updateDependencies(host: Tree) {
|
||||
return addDependenciesToPackageJson(
|
||||
@ -85,6 +87,9 @@ export async function nextInitGenerator(host: Tree, schema: InitSchema) {
|
||||
}
|
||||
|
||||
addGitIgnoreEntry(host);
|
||||
if (process.env.NX_PCV3 === 'true') {
|
||||
addPlugin(host);
|
||||
}
|
||||
|
||||
return runTasksInSerial(...tasks);
|
||||
}
|
||||
|
||||
27
packages/next/src/generators/init/lib/add-plugin.ts
Normal file
27
packages/next/src/generators/init/lib/add-plugin.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Tree, readNxJson, updateNxJson } from '@nx/devkit';
|
||||
|
||||
export function addPlugin(tree: Tree) {
|
||||
const nxJson = readNxJson(tree);
|
||||
nxJson.plugins ??= [];
|
||||
|
||||
for (const plugin of nxJson.plugins) {
|
||||
if (
|
||||
typeof plugin === 'string'
|
||||
? plugin === '@nx/next/plugin'
|
||||
: plugin.plugin === '@nx/next/plugin'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
nxJson.plugins.push({
|
||||
plugin: '@nx/next/plugin',
|
||||
options: {
|
||||
buildTargetName: 'build',
|
||||
devTargetName: 'dev',
|
||||
startTargetName: 'start',
|
||||
},
|
||||
});
|
||||
|
||||
updateNxJson(tree, nxJson);
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`@nx/next/plugin integrated projects should create nodes 1`] = `Promise {}`;
|
||||
|
||||
exports[`@nx/next/plugin root projects should create nodes 1`] = `Promise {}`;
|
||||
95
packages/next/src/plugins/plugin.spec.ts
Normal file
95
packages/next/src/plugins/plugin.spec.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { CreateNodesContext } from '@nx/devkit';
|
||||
import type { NextConfig } from 'next';
|
||||
|
||||
import { createNodes } from './plugin';
|
||||
import { TempFs } from '@nx/devkit/internal-testing-utils';
|
||||
|
||||
describe('@nx/next/plugin', () => {
|
||||
let createNodesFunction = createNodes[1];
|
||||
let context: CreateNodesContext;
|
||||
|
||||
describe('root projects', () => {
|
||||
beforeEach(async () => {
|
||||
context = {
|
||||
nxJsonConfiguration: {
|
||||
namedInputs: {
|
||||
default: ['{projectRoot}/**/*'],
|
||||
production: ['!{projectRoot}/**/*.spec.ts'],
|
||||
},
|
||||
},
|
||||
workspaceRoot: '',
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
it('should create nodes', () => {
|
||||
const nextConfigPath = 'next.config.js';
|
||||
mockNextConfig(nextConfigPath, {});
|
||||
const nodes = createNodesFunction(
|
||||
nextConfigPath,
|
||||
{
|
||||
buildTargetName: 'build',
|
||||
devTargetName: 'dev',
|
||||
startTargetName: 'start',
|
||||
},
|
||||
context
|
||||
);
|
||||
|
||||
expect(nodes).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('integrated projects', () => {
|
||||
const tempFs = new TempFs('test');
|
||||
beforeEach(() => {
|
||||
context = {
|
||||
nxJsonConfiguration: {
|
||||
namedInputs: {
|
||||
default: ['{projectRoot}/**/*'],
|
||||
production: ['!{projectRoot}/**/*.spec.ts'],
|
||||
},
|
||||
},
|
||||
workspaceRoot: tempFs.tempDir,
|
||||
};
|
||||
|
||||
tempFs.createFileSync(
|
||||
'my-app/project.json',
|
||||
JSON.stringify({ name: 'my-app' })
|
||||
);
|
||||
tempFs.createFileSync('my-app/next.config.js', '');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
it('should create nodes', () => {
|
||||
mockNextConfig('my-app/next.config.js', {});
|
||||
const nodes = createNodesFunction(
|
||||
'my-app/next.config.js',
|
||||
{
|
||||
buildTargetName: 'my-build',
|
||||
devTargetName: 'my-serve',
|
||||
startTargetName: 'my-start',
|
||||
},
|
||||
context
|
||||
);
|
||||
|
||||
expect(nodes).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function mockNextConfig(path: string, config: NextConfig) {
|
||||
jest.mock(
|
||||
path,
|
||||
() => ({
|
||||
default: config,
|
||||
}),
|
||||
{
|
||||
virtual: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
219
packages/next/src/plugins/plugin.ts
Normal file
219
packages/next/src/plugins/plugin.ts
Normal file
@ -0,0 +1,219 @@
|
||||
import {
|
||||
CreateDependencies,
|
||||
CreateNodes,
|
||||
CreateNodesContext,
|
||||
NxJsonConfiguration,
|
||||
TargetConfiguration,
|
||||
detectPackageManager,
|
||||
readJsonFile,
|
||||
writeJsonFile,
|
||||
} from '@nx/devkit';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs';
|
||||
import { existsSync, readdirSync } from 'fs';
|
||||
|
||||
import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory';
|
||||
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
|
||||
import { type NextConfig } from 'next';
|
||||
import { PHASE_PRODUCTION_BUILD } from 'next/constants';
|
||||
import { getLockFileName } from '@nx/js';
|
||||
|
||||
export interface NextPluginOptions {
|
||||
buildTargetName?: string;
|
||||
devTargetName?: string;
|
||||
startTargetName?: string;
|
||||
}
|
||||
|
||||
const cachePath = join(projectGraphCacheDirectory, 'next.hash');
|
||||
const targetsCache = existsSync(cachePath) ? readTargetsCache() : {};
|
||||
|
||||
const calculatedTargets: Record<
|
||||
string,
|
||||
Record<string, TargetConfiguration>
|
||||
> = {};
|
||||
|
||||
function readTargetsCache(): Record<
|
||||
string,
|
||||
Record<string, TargetConfiguration>
|
||||
> {
|
||||
return readJsonFile(cachePath);
|
||||
}
|
||||
|
||||
function writeTargetsToCache(
|
||||
targets: Record<string, Record<string, TargetConfiguration>>
|
||||
) {
|
||||
writeJsonFile(cachePath, targets);
|
||||
}
|
||||
|
||||
export const createDependencies: CreateDependencies = () => {
|
||||
writeTargetsToCache(calculatedTargets);
|
||||
return [];
|
||||
};
|
||||
|
||||
// TODO(nicholas): Add support for .mjs files
|
||||
export const createNodes: CreateNodes<NextPluginOptions> = [
|
||||
'**/next.config.{js, cjs}',
|
||||
async (configFilePath, options, context) => {
|
||||
const projectRoot = dirname(configFilePath);
|
||||
|
||||
// Do not create a project if package.json and project.json isn't there.
|
||||
const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot));
|
||||
if (
|
||||
!siblingFiles.includes('package.json') &&
|
||||
!siblingFiles.includes('project.json')
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
|
||||
options = normalizeOptions(options);
|
||||
|
||||
const hash = calculateHashForCreateNodes(projectRoot, options, context, [
|
||||
getLockFileName(detectPackageManager(context.workspaceRoot)),
|
||||
]);
|
||||
|
||||
const targets =
|
||||
targetsCache[hash] ??
|
||||
(await buildNextTargets(configFilePath, projectRoot, options, context));
|
||||
|
||||
calculatedTargets[hash] = targets;
|
||||
|
||||
return {
|
||||
projects: {
|
||||
[projectRoot]: {
|
||||
root: projectRoot,
|
||||
targets,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
];
|
||||
|
||||
async function buildNextTargets(
|
||||
nextConfigPath: string,
|
||||
projectRoot: string,
|
||||
options: NextPluginOptions,
|
||||
context: CreateNodesContext
|
||||
) {
|
||||
const nextConfig = getNextConfig(nextConfigPath, context);
|
||||
const namedInputs = getNamedInputs(projectRoot, context);
|
||||
|
||||
const targets: Record<string, TargetConfiguration> = {};
|
||||
|
||||
targets[options.buildTargetName] = await getBuildTargetConfig(
|
||||
namedInputs,
|
||||
projectRoot,
|
||||
nextConfig
|
||||
);
|
||||
|
||||
targets[options.devTargetName] = getDevTargetConfig(projectRoot);
|
||||
|
||||
targets[options.startTargetName] = getStartTargetConfig(options, projectRoot);
|
||||
return targets;
|
||||
}
|
||||
|
||||
async function getBuildTargetConfig(
|
||||
namedInputs: { [inputName: string]: any[] },
|
||||
projectRoot: string,
|
||||
nextConfig: NextConfig
|
||||
) {
|
||||
// Set output path here so that `withNx` can pick it up.
|
||||
const targetConfig: TargetConfiguration = {
|
||||
command: `next build`,
|
||||
options: {
|
||||
cwd: projectRoot,
|
||||
},
|
||||
dependsOn: ['^build'],
|
||||
cache: true,
|
||||
inputs: getInputs(namedInputs),
|
||||
outputs: [await getOutputs(projectRoot, nextConfig)],
|
||||
};
|
||||
return targetConfig;
|
||||
}
|
||||
|
||||
function getDevTargetConfig(projectRoot: string) {
|
||||
const targetConfig: TargetConfiguration = {
|
||||
command: `next dev`,
|
||||
options: {
|
||||
cwd: projectRoot,
|
||||
},
|
||||
};
|
||||
|
||||
return targetConfig;
|
||||
}
|
||||
|
||||
function getStartTargetConfig(options: NextPluginOptions, projectRoot: string) {
|
||||
const targetConfig: TargetConfiguration = {
|
||||
command: `next start`,
|
||||
options: {
|
||||
cwd: projectRoot,
|
||||
},
|
||||
dependsOn: [options.buildTargetName],
|
||||
};
|
||||
|
||||
return targetConfig;
|
||||
}
|
||||
|
||||
async function getOutputs(projectRoot, nextConfig) {
|
||||
let dir = '.next';
|
||||
|
||||
if (typeof nextConfig === 'function') {
|
||||
// Works for both async and sync functions.
|
||||
const configResult = await Promise.resolve(
|
||||
nextConfig(PHASE_PRODUCTION_BUILD, { defaultConfig: {} })
|
||||
);
|
||||
if (configResult?.distDir) {
|
||||
dir = configResult?.distDir;
|
||||
}
|
||||
} else if (typeof nextConfig === 'object' && nextConfig?.distDir) {
|
||||
// If nextConfig is an object, directly use its 'distDir' property.
|
||||
dir = nextConfig.distDir;
|
||||
}
|
||||
return `{workspaceRoot}/${projectRoot}/${dir}`;
|
||||
}
|
||||
|
||||
function getNextConfig(
|
||||
configFilePath: string,
|
||||
context: CreateNodesContext
|
||||
): Promise<any> {
|
||||
const resolvedPath = join(context.workspaceRoot, configFilePath);
|
||||
|
||||
const module = load(resolvedPath);
|
||||
return module.default ?? module;
|
||||
}
|
||||
|
||||
function normalizeOptions(options: NextPluginOptions): NextPluginOptions {
|
||||
options ??= {};
|
||||
options.buildTargetName ??= 'build';
|
||||
options.devTargetName ??= 'dev';
|
||||
options.startTargetName ??= 'start';
|
||||
return options;
|
||||
}
|
||||
|
||||
function getInputs(
|
||||
namedInputs: NxJsonConfiguration['namedInputs']
|
||||
): TargetConfiguration['inputs'] {
|
||||
return [
|
||||
...('production' in namedInputs
|
||||
? ['default', '^production']
|
||||
: ['default', '^default']),
|
||||
{
|
||||
externalDependencies: ['next'],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the module after ensuring that the require cache is cleared.
|
||||
*/
|
||||
function load(path: string): any {
|
||||
// Clear cache if the path is in the cache
|
||||
if (require.cache[path]) {
|
||||
for (const k of Object.keys(require.cache)) {
|
||||
delete require.cache[k];
|
||||
}
|
||||
}
|
||||
|
||||
// Then require
|
||||
return require(path);
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
export const nxVersion = require('../../package.json').version;
|
||||
|
||||
export const nextVersion = '13.4.1';
|
||||
export const eslintConfigNextVersion = '13.4.1';
|
||||
export const nextVersion = '13.4.4';
|
||||
export const eslintConfigNextVersion = '13.4.4';
|
||||
export const sassVersion = '1.62.1';
|
||||
export const lessLoader = '11.1.0';
|
||||
export const emotionServerVersion = '11.11.0';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user