fix(nextjs): enhance support for custom server with SWC configuration (#29895)

This pull request contains a few changes to enhance our swc support for
Next.js with a custom server.

### Issues
Currently, we have a few issues with our configuration when using
executors for Next.js with a custom server:

1. The custom server does not have an independent build configuration.
2. The custom server does not have an independent output directory.
3. Serving via `@nx/next-server` or via `@nx/js:node` with
configurations `production` and `development` does not always work.
(These are contained inside `project.json`).

### Changes
All the above issues have been addressed

1. We now have an independent swc build configuration
called`.server.swrc` (_follows the same format as `.eslintrc`,
`.babelrc`_) etc...
2. Now each custom server output will be named `{app}-server` such that
if you have multiple custom servers for multiple apps the names will not
clash.
3. Serving now works out of the box but can be adjusted to suit your
needs via updating the custom server entry file `main.ts`
This commit is contained in:
Nicholas Cunningham 2025-02-06 11:34:32 -07:00 committed by GitHub
parent 8bd0bcdd97
commit 29e5ce2963
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 92 additions and 33 deletions

View File

@ -279,4 +279,44 @@ describe('@nx/next (legacy)', () => {
await killPort(prodServePort);
await killPort(selfContainedPort);
}, 600_000);
it('should support --custom-server flag (swc)', async () => {
const appName = uniq('app');
runCLI(
`generate @nx/next:app ${appName} --no-interactive --custom-server --linter=eslint --unitTestRunner=jest`,
{ env: { NX_ADD_PLUGINS: 'false' } }
);
// Check for custom server files added to source
checkFilesExist(`${appName}/server/main.ts`);
checkFilesExist(`${appName}/.server.swcrc`);
const result = runCLI(`build ${appName}`);
checkFilesExist(`dist/${appName}-server/server/main.js`);
expect(result).toContain(
`Successfully ran target build for project ${appName}`
);
}, 300_000);
it('should support --custom-server flag (tsc)', async () => {
const appName = uniq('app');
runCLI(
`generate @nx/next:app ${appName} --swc=false --no-interactive --custom-server --linter=eslint --unitTestRunner=jest`,
{ env: { NX_ADD_PLUGINS: 'false' } }
);
checkFilesExist(`${appName}/server/main.ts`);
const result = runCLI(`build ${appName}`);
checkFilesExist(`dist/${appName}-server/server/main.js`);
expect(result).toContain(
`Successfully ran target build for project ${appName}`
);
}, 300_000);
});

View File

@ -158,11 +158,13 @@ describe('Next.js Applications', () => {
`generate @nx/next:app ${appName} --no-interactive --custom-server --linter=eslint --unitTestRunner=jest`
);
// Check for custom server files added to source
checkFilesExist(`${appName}/server/main.ts`);
checkFilesExist(`${appName}/.server.swcrc`);
const result = runCLI(`build ${appName}`);
checkFilesExist(`dist/${appName}/server/main.js`);
checkFilesExist(`dist/${appName}-server/server/main.js`);
expect(result).toContain(
`Successfully ran target build for project ${appName}`
@ -180,7 +182,7 @@ describe('Next.js Applications', () => {
const result = runCLI(`build ${appName}`);
checkFilesExist(`dist/${appName}/server/main.js`);
checkFilesExist(`dist/${appName}-server/server/main.js`);
expect(result).toContain(
`Successfully ran target build for project ${appName}`

View File

@ -145,7 +145,7 @@ export async function* nodeExecutor(
// Wait for build to finish.
const result = await buildResult;
if (!result.success) {
if (result && !result.success) {
// If in watch-mode, don't throw or else the process exits.
if (options.watch) {
if (!task.killed) {

View File

@ -54,11 +54,20 @@ export function addSwcConfig(
tree: Tree,
projectDir: string,
type: 'commonjs' | 'es6' = 'commonjs',
supportTsx: boolean = false
supportTsx: boolean = false,
swcName: string = '.swcrc',
additionalExcludes: string[] = []
) {
const swcrcPath = join(projectDir, '.swcrc');
const swcrcPath = join(projectDir, swcName);
if (tree.exists(swcrcPath)) return;
tree.write(swcrcPath, swcOptionsString(type, defaultExclude, supportTsx));
tree.write(
swcrcPath,
swcOptionsString(
type,
[...defaultExclude, ...additionalExcludes],
supportTsx
)
);
}
export function addSwcTestConfig(

View File

@ -32,6 +32,7 @@ import {
updateTsconfigFiles,
} from '@nx/js/src/utils/typescript/ts-solution-setup';
import { sortPackageJsonFields } from '@nx/js/src/utils/package-json/sort-fields';
import { configureForSwc } from '../../utils/add-swc-to-custom-server';
export async function applicationGenerator(host: Tree, schema: Schema) {
return await applicationGeneratorInternal(host, {
@ -93,6 +94,11 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
updateCypressTsConfig(host, options);
setDefaults(host, options);
if (options.swc) {
const swcTask = configureForSwc(host, options.appProjectRoot);
tasks.push(swcTask);
}
if (options.customServer) {
await customServerGenerator(host, {
project: options.projectName,

View File

@ -18,6 +18,7 @@ export async function customServerGenerator(
options: CustomServerSchema
) {
const project = readProjectConfiguration(host, options.project);
const swcServerName = '.server.swcrc';
const nxJson = readNxJson(host);
const hasPlugin = nxJson.plugins?.some((p) =>
@ -26,11 +27,7 @@ export async function customServerGenerator(
: p.plugin === '@nx/next/plugin'
);
if (
project.targets?.build?.executor !== '@nx/next:build' &&
project.targets?.build?.executor !== '@nrwl/next:build' &&
!hasPlugin
) {
if (project.targets?.build?.executor !== '@nx/next:build' && !hasPlugin) {
logger.error(
`Project ${options.project} is not a Next.js project. Did you generate it with "nx g @nx/next:app"?`
);
@ -38,9 +35,7 @@ export async function customServerGenerator(
}
// In Nx 18 next artifacts are inside the project root .next/ & dist/ (for custom server)
const outputPath = hasPlugin
? `dist/${project.root}`
: project.targets?.build?.options?.outputPath;
const outputPath = `dist/${project.root}-server`;
const root = project.root;
if (
@ -68,9 +63,9 @@ export async function customServerGenerator(
// In Nx 18 next artifacts are inside the project root .next/ & dist/ (for custom server)
// So we need ensure the mapping is correct from dist to the project root
const projectPathFromDist = `../../${offsetFromRoot(project.root)}${
project.root
}`;
const projectPathFromDist = hasPlugin
? `../../${offsetFromRoot(project.root)}${project.root}`
: `${offsetFromRoot(`dist/${project.root}`)}${project.root}`;
const offset = offsetFromRoot(project.root);
const isTsSolution = isUsingTsSolutionSetup(host);
@ -107,6 +102,9 @@ export async function customServerGenerator(
tsConfig: `${root}/tsconfig.server.json`,
clean: false,
assets: [],
...(options.compiler === 'tsc'
? {}
: { swcrc: `${root}/${swcServerName}` }),
},
configurations: {
development: {},
@ -150,6 +148,11 @@ export async function customServerGenerator(
});
if (options.compiler === 'swc') {
return configureForSwc(host, project.root);
// Update app swc to exlude server files
updateJson(host, join(project.root, '.swcrc'), (json) => {
json.exclude = [...(json.exclude ?? []), 'server/**'];
return json;
});
return configureForSwc(host, project.root, swcServerName, ['src/**/*']);
}
}

View File

@ -15,8 +15,8 @@ import next from 'next';
// - The fallback `__dirname` is for production builds.
// - Feel free to change this to suit your needs.
const dir = process.env.NX_NEXT_DIR || <%- hasPlugin ? `path.join(__dirname, '${projectPathFromDist}')` : `path.join(__dirname, '..')`; %>
const dev = process.env.NODE_ENV === 'development';
const dir = process.env.NX_NEXT_DIR || <%- hasPlugin ? `path.join(__dirname, '${projectPathFromDist}')` : `path.join(__dirname, dev ? '..' : '', '${projectPathFromDist}')`; %>
// HTTP Server options:
// - Feel free to change this to suit your needs.

View File

@ -4,7 +4,6 @@ import {
installPackagesTask,
joinPathFragments,
readJson,
updateJson,
} from '@nx/devkit';
import {
swcCliVersion,
@ -14,8 +13,13 @@ import {
} from '@nx/js/src/utils/versions';
import { addSwcConfig } from '@nx/js/src/utils/swc/add-swc-config';
export function configureForSwc(tree: Tree, projectRoot: string) {
const swcConfigPath = joinPathFragments(projectRoot, '.swcrc');
export function configureForSwc(
tree: Tree,
projectRoot: string,
swcConfigName = '.swcrc',
additonalExludes: string[] = []
) {
const swcConfigPath = joinPathFragments(projectRoot, swcConfigName);
const rootPackageJson = readJson(tree, 'package.json');
const hasSwcDepedency =
@ -27,22 +31,17 @@ export function configureForSwc(tree: Tree, projectRoot: string) {
rootPackageJson.devDependencies?.['@swc/cli'];
if (!tree.exists(swcConfigPath)) {
addSwcConfig(tree, projectRoot);
}
if (tree.exists(swcConfigPath)) {
updateJson(tree, swcConfigPath, (json) => {
return {
...json,
exclude: [...json.exclude, '.*.d.ts$'],
};
});
// We need to create a swc config file specific for custom server
addSwcConfig(tree, projectRoot, 'commonjs', false, swcConfigName, [
...additonalExludes,
'.*.d.ts$',
]);
}
if (!hasSwcDepedency || !hasSwcCliDependency) {
addSwcDependencies(tree);
return () => installPackagesTask(tree);
}
return () => installPackagesTask(tree);
}
function addSwcDependencies(tree: Tree) {