Skip to content

Commit

Permalink
fix(nextjs): enhance support for custom server with SWC configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
ndcunningham committed Feb 5, 2025
1 parent 0944e34 commit a8d9697
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 39 deletions.
40 changes: 40 additions & 0 deletions e2e/next/src/next-legacy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}/.swcrc.server`);

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);
});
6 changes: 4 additions & 2 deletions e2e/next/src/next.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}/.swcrc.server`);

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}`
Expand All @@ -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}`
Expand Down
34 changes: 27 additions & 7 deletions packages/js/src/executors/node/node.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,29 @@ function debounce<T>(fn: () => Promise<T>, wait: number): () => Promise<T> {
return pendingPromise;
};
}
// This function is used to process the queue of tasks.
function queueProcesser<T>(fn: () => Promise<T>) {
let isProcessing = false;
let pendingPromise = Promise.resolve(); // Ensures sequential processing

return async () => {
if (isProcessing) {
return pendingPromise;
}

isProcessing = true;
pendingPromise = (async () => {
try {
await fn(); // Process the queue
} catch (error) {
console.error('Error processing queue:', error);
} finally {
isProcessing = false;
}
})();
return pendingPromise;
};
}

export async function* nodeExecutor(
options: NodeExecutorOptions,
Expand Down Expand Up @@ -127,10 +150,7 @@ export async function* nodeExecutor(
await task.start();
};

const debouncedProcessQueue = debounce(
processQueue,
options.debounce ?? 1_000
);
const processQueueSafely = queueProcesser(processQueue);

const addToQueue = async (
childProcess: null | ChildProcess,
Expand All @@ -145,7 +165,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) {
Expand Down Expand Up @@ -276,7 +296,7 @@ export async function* nodeExecutor(
});
});
await addToQueue(childProcess, whenReady);
await debouncedProcessQueue();
await processQueueSafely();
};
if (isDaemonEnabled()) {
additionalExitHandler = await daemonClient.registerFileWatcher(
Expand Down Expand Up @@ -319,7 +339,7 @@ export async function* nodeExecutor(
while (true) {
const event = await output.next();
await addToQueue(null, Promise.resolve(event.value));
await debouncedProcessQueue();
await processQueueSafely();
if (event.done || !options.watch) {
break;
}
Expand Down
15 changes: 12 additions & 3 deletions packages/js/src/utils/swc/add-swc-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 6 additions & 0 deletions packages/next/src/generators/application/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down Expand Up @@ -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,
Expand Down
27 changes: 15 additions & 12 deletions packages/next/src/generators/custom-server/custom-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand All @@ -26,21 +27,15 @@ 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"?`
);
return;
}

// 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 (
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -107,6 +102,9 @@ export async function customServerGenerator(
tsConfig: `${root}/tsconfig.server.json`,
clean: false,
assets: [],
...(options.compiler === 'tsc'
? {}
: { swcrc: `${root}/${swcServerName}` }),
},
configurations: {
development: {},
Expand Down Expand Up @@ -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/**/*']);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
27 changes: 13 additions & 14 deletions packages/next/src/utils/add-swc-to-custom-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
installPackagesTask,
joinPathFragments,
readJson,
updateJson,
} from '@nx/devkit';
import {
swcCliVersion,
Expand All @@ -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 =
Expand All @@ -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) {
Expand Down

0 comments on commit a8d9697

Please sign in to comment.