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(prodServePort);
await killPort(selfContainedPort); await killPort(selfContainedPort);
}, 600_000); }, 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` `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/main.ts`);
checkFilesExist(`${appName}/.server.swcrc`);
const result = runCLI(`build ${appName}`); const result = runCLI(`build ${appName}`);
checkFilesExist(`dist/${appName}/server/main.js`); checkFilesExist(`dist/${appName}-server/server/main.js`);
expect(result).toContain( expect(result).toContain(
`Successfully ran target build for project ${appName}` `Successfully ran target build for project ${appName}`
@ -180,7 +182,7 @@ describe('Next.js Applications', () => {
const result = runCLI(`build ${appName}`); const result = runCLI(`build ${appName}`);
checkFilesExist(`dist/${appName}/server/main.js`); checkFilesExist(`dist/${appName}-server/server/main.js`);
expect(result).toContain( expect(result).toContain(
`Successfully ran target build for project ${appName}` `Successfully ran target build for project ${appName}`

View File

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

View File

@ -54,11 +54,20 @@ export function addSwcConfig(
tree: Tree, tree: Tree,
projectDir: string, projectDir: string,
type: 'commonjs' | 'es6' = 'commonjs', 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; if (tree.exists(swcrcPath)) return;
tree.write(swcrcPath, swcOptionsString(type, defaultExclude, supportTsx)); tree.write(
swcrcPath,
swcOptionsString(
type,
[...defaultExclude, ...additionalExcludes],
supportTsx
)
);
} }
export function addSwcTestConfig( export function addSwcTestConfig(

View File

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

View File

@ -18,6 +18,7 @@ export async function customServerGenerator(
options: CustomServerSchema options: CustomServerSchema
) { ) {
const project = readProjectConfiguration(host, options.project); const project = readProjectConfiguration(host, options.project);
const swcServerName = '.server.swcrc';
const nxJson = readNxJson(host); const nxJson = readNxJson(host);
const hasPlugin = nxJson.plugins?.some((p) => const hasPlugin = nxJson.plugins?.some((p) =>
@ -26,11 +27,7 @@ export async function customServerGenerator(
: p.plugin === '@nx/next/plugin' : p.plugin === '@nx/next/plugin'
); );
if ( if (project.targets?.build?.executor !== '@nx/next:build' && !hasPlugin) {
project.targets?.build?.executor !== '@nx/next:build' &&
project.targets?.build?.executor !== '@nrwl/next:build' &&
!hasPlugin
) {
logger.error( logger.error(
`Project ${options.project} is not a Next.js project. Did you generate it with "nx g @nx/next:app"?` `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) // In Nx 18 next artifacts are inside the project root .next/ & dist/ (for custom server)
const outputPath = hasPlugin const outputPath = `dist/${project.root}-server`;
? `dist/${project.root}`
: project.targets?.build?.options?.outputPath;
const root = project.root; const root = project.root;
if ( if (
@ -68,9 +63,9 @@ export async function customServerGenerator(
// In Nx 18 next artifacts are inside the project root .next/ & dist/ (for custom server) // 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 // So we need ensure the mapping is correct from dist to the project root
const projectPathFromDist = `../../${offsetFromRoot(project.root)}${ const projectPathFromDist = hasPlugin
project.root ? `../../${offsetFromRoot(project.root)}${project.root}`
}`; : `${offsetFromRoot(`dist/${project.root}`)}${project.root}`;
const offset = offsetFromRoot(project.root); const offset = offsetFromRoot(project.root);
const isTsSolution = isUsingTsSolutionSetup(host); const isTsSolution = isUsingTsSolutionSetup(host);
@ -107,6 +102,9 @@ export async function customServerGenerator(
tsConfig: `${root}/tsconfig.server.json`, tsConfig: `${root}/tsconfig.server.json`,
clean: false, clean: false,
assets: [], assets: [],
...(options.compiler === 'tsc'
? {}
: { swcrc: `${root}/${swcServerName}` }),
}, },
configurations: { configurations: {
development: {}, development: {},
@ -150,6 +148,11 @@ export async function customServerGenerator(
}); });
if (options.compiler === 'swc') { 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. // - The fallback `__dirname` is for production builds.
// - Feel free to change this to suit your needs. // - 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 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: // HTTP Server options:
// - Feel free to change this to suit your needs. // - Feel free to change this to suit your needs.

View File

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