diff --git a/e2e/next/src/next-legacy.test.ts b/e2e/next/src/next-legacy.test.ts index 01b15a1e287caa..8c05db712e8b0d 100644 --- a/e2e/next/src/next-legacy.test.ts +++ b/e2e/next/src/next-legacy.test.ts @@ -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); }); diff --git a/e2e/next/src/next.test.ts b/e2e/next/src/next.test.ts index fcf104e28b9f14..8a2355f6f33ea5 100644 --- a/e2e/next/src/next.test.ts +++ b/e2e/next/src/next.test.ts @@ -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}` @@ -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}` diff --git a/packages/js/src/executors/node/node.impl.ts b/packages/js/src/executors/node/node.impl.ts index a50474a613bf15..2701900b915dcf 100644 --- a/packages/js/src/executors/node/node.impl.ts +++ b/packages/js/src/executors/node/node.impl.ts @@ -58,6 +58,29 @@ function debounce(fn: () => Promise, wait: number): () => Promise { return pendingPromise; }; } +// This function is used to process the queue of tasks. +function queueProcesser(fn: () => Promise) { + 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, @@ -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, @@ -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) { @@ -276,7 +296,7 @@ export async function* nodeExecutor( }); }); await addToQueue(childProcess, whenReady); - await debouncedProcessQueue(); + await processQueueSafely(); }; if (isDaemonEnabled()) { additionalExitHandler = await daemonClient.registerFileWatcher( @@ -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; } diff --git a/packages/js/src/utils/swc/add-swc-config.ts b/packages/js/src/utils/swc/add-swc-config.ts index f6783e7bffcf87..547af1138c0daa 100644 --- a/packages/js/src/utils/swc/add-swc-config.ts +++ b/packages/js/src/utils/swc/add-swc-config.ts @@ -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( diff --git a/packages/next/src/generators/application/application.ts b/packages/next/src/generators/application/application.ts index a99c7f889eed44..703868f5d80045 100644 --- a/packages/next/src/generators/application/application.ts +++ b/packages/next/src/generators/application/application.ts @@ -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, diff --git a/packages/next/src/generators/custom-server/custom-server.ts b/packages/next/src/generators/custom-server/custom-server.ts index 4c2e6af45d42cf..235c2ff6ea1b0a 100644 --- a/packages/next/src/generators/custom-server/custom-server.ts +++ b/packages/next/src/generators/custom-server/custom-server.ts @@ -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/**/*']); } } diff --git a/packages/next/src/generators/custom-server/files/server/main.ts__tmpl__ b/packages/next/src/generators/custom-server/files/server/main.ts__tmpl__ index 0ec46fe5607a49..3a22b42538aae7 100644 --- a/packages/next/src/generators/custom-server/files/server/main.ts__tmpl__ +++ b/packages/next/src/generators/custom-server/files/server/main.ts__tmpl__ @@ -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. diff --git a/packages/next/src/utils/add-swc-to-custom-server.ts b/packages/next/src/utils/add-swc-to-custom-server.ts index e7bade6a1e61ab..8c08d4ca9edf2f 100644 --- a/packages/next/src/utils/add-swc-to-custom-server.ts +++ b/packages/next/src/utils/add-swc-to-custom-server.ts @@ -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) {