From ae89efb8d1f88306d28412d893477815740e9805 Mon Sep 17 00:00:00 2001 From: Nicholas Cunningham Date: Thu, 5 Jun 2025 13:54:29 -0600 Subject: [PATCH] fix(nextjs): add extension alias support for handling ESM libs (#31323) ## Current Behavior Currently, if you try to import a ESM lib after you generate a Next.js application it fails to build due to how the module exports `export * from './lib/lib8446520.js';`. This has been addressed with webpack and needs to be extended to Next.js. ## Expected Behavior You should work out of the box and you should be able to import a lib defined like: `export * from './lib/lib8446520.js';.` ## Related Issue(s) This is also related to our webpack and rspack packages. Changes have also been made to them to ensure consistency across bundlers. Fixes #30714 --- e2e/next/src/next-ts-solutions.test.ts | 63 +++++++++++++++++++ e2e/next/src/next.test.ts | 18 ------ e2e/react/src/react-ts-solution.test.ts | 43 +++++++++++++ packages/next/plugins/with-nx.ts | 11 ++++ .../src/plugins/utils/apply-base-config.ts | 11 ++++ .../lib/apply-base-config.ts | 4 +- 6 files changed, 131 insertions(+), 19 deletions(-) create mode 100644 e2e/next/src/next-ts-solutions.test.ts diff --git a/e2e/next/src/next-ts-solutions.test.ts b/e2e/next/src/next-ts-solutions.test.ts new file mode 100644 index 0000000000..62e14b9db0 --- /dev/null +++ b/e2e/next/src/next-ts-solutions.test.ts @@ -0,0 +1,63 @@ +import { + cleanupProject, + getPackageManagerCommand, + newProject, + readFile, + runCLI, + runCommand, + uniq, + updateFile, + updateJson, +} from '@nx/e2e/utils'; +describe('Next TS Solutions', () => { + let proj: string; + + beforeAll(() => { + proj = newProject({ + packages: ['@nx/next'], + preset: 'ts', + }); + }); + afterAll(() => cleanupProject()); + + it('should support importing a esm library', async () => { + const appName = uniq('app'); + const libName = uniq('lib'); + + runCLI( + `generate @nx/next:app ${appName} --no-interactive --style=css --linter=none --unitTestRunner=none --e2eTestRunner=none` + ); + + runCLI( + `generate @nx/js:lib packages/${libName} --bundler=vite --no-interactive --unit-test-runner=none --skipFormat --linter=eslint` + ); + + updateFile( + `${appName}/src/app/page.tsx`, + ` + import {${libName}} from '@${proj}/${libName}'; + ${readFile(`${appName}/src/app/page.tsx`)} + console.log(${libName}()); + ` + ); + runCLI('sync'); + + // Add library to package.json to make sure it is linked (not needed for npm package manager) + updateJson(`${appName}/package.json`, (json) => { + return { + ...json, + devDependencies: { + ...(json.devDependencies || {}), + [`@${proj}/${libName}`]: 'workspace:*', + }, + }; + }); + + runCommand(`cd ${appName} && ${getPackageManagerCommand().install}`); + + const output = runCLI(`build ${appName}`); + expect(output).toContain( + `Successfully ran target build for project @${proj}/${appName}` + ); + }); +}); diff --git a/e2e/next/src/next.test.ts b/e2e/next/src/next.test.ts index 0c3e579ba5..b2e5774a20 100644 --- a/e2e/next/src/next.test.ts +++ b/e2e/next/src/next.test.ts @@ -10,7 +10,6 @@ import { uniq, updateFile, } from '@nx/e2e/utils'; -import * as http from 'http'; import { checkApp } from './utils'; describe('Next.js Applications', () => { @@ -240,20 +239,3 @@ describe('Next.js Applications', () => { expect(postBuildPagesContent).toMatchSnapshot(); }); }); - -function getData(port, path = ''): Promise { - return new Promise((resolve, reject) => { - http - .get(`http://localhost:${port}${path}`, (res) => { - expect(res.statusCode).toEqual(200); - let data = ''; - res.on('data', (chunk) => { - data += chunk; - }); - res.once('end', () => { - resolve(data); - }); - }) - .on('error', (err) => reject(err)); - }); -} diff --git a/e2e/react/src/react-ts-solution.test.ts b/e2e/react/src/react-ts-solution.test.ts index 1f9b87b1a6..1acc99d6a5 100644 --- a/e2e/react/src/react-ts-solution.test.ts +++ b/e2e/react/src/react-ts-solution.test.ts @@ -87,4 +87,47 @@ describe('React (TS solution)', () => { checkFilesExist(`packages/${appName}/dist/index.html`); }, 90_000); + + it('should be able to use Rspack to build apps with an imported lib', async () => { + const appName = uniq('app'); + const libName = uniq('lib'); + + runCLI( + `generate @nx/react:app packages/${appName} --bundler=rspack --no-interactive --skipFormat --linter=eslint --unitTestRunner=none` + ); + runCLI( + `generate @nx/js:lib libs/${libName} --bundler=none --no-interactive --unit-test-runner=none --skipFormat --linter=eslint` + ); + + const mainPath = `packages/${appName}/src/main.tsx`; + updateFile( + mainPath, + ` + import {${libName}} from '@${workspaceName}/${libName}'; + ${readFile(mainPath)} + console.log(${libName}()); + ` + ); + + runCLI('sync'); + + // Add library to package.json to make sure it is linked (not needed for npm package manager) + updateJson(`packages/${appName}/package.json`, (json) => { + return { + ...json, + devDependencies: { + ...(json.devDependencies || {}), + [`@${workspaceName}/${libName}`]: 'workspace:*', + }, + }; + }); + + runCommand( + `cd packages/${appName} && ${getPackageManagerCommand().install}` + ); + + runCLI(`build ${appName}`); + + checkFilesExist(`packages/${appName}/dist/index.html`); + }, 90_000); }); diff --git a/packages/next/plugins/with-nx.ts b/packages/next/plugins/with-nx.ts index f9c63040cf..313f8d5c02 100644 --- a/packages/next/plugins/with-nx.ts +++ b/packages/next/plugins/with-nx.ts @@ -261,6 +261,17 @@ export function getNextConfig( }, ...validNextConfig, webpack: (config, options) => { + /** + * To support ESM library export, we need to ensure the extensionAlias contains both `.js` and `.ts` extensions. + * This is because Webpack uses the `extensionAlias` to resolve the correct file extension when importing modules. + */ + config.resolve.extensionAlias = { + ...(config.resolve.extensionAlias || {}), + '.js': ['.ts', '.tsx', '.js', '.jsx'], + '.mjs': ['.mts', '.mjs'], + '.cjs': ['.cts', '.cjs'], + '.jsx': ['.tsx', '.jsx'], + }; /* * Update babel to support our monorepo setup. * The 'upward' mode allows the root babel.config.json and per-project .babelrc files to be picked up. diff --git a/packages/rspack/src/plugins/utils/apply-base-config.ts b/packages/rspack/src/plugins/utils/apply-base-config.ts index 46d6ed686d..39f7f4e158 100644 --- a/packages/rspack/src/plugins/utils/apply-base-config.ts +++ b/packages/rspack/src/plugins/utils/apply-base-config.ts @@ -27,6 +27,13 @@ const IGNORED_RSPACK_WARNINGS = [ ]; const extensions = ['...', '.ts', '.tsx', '.mjs', '.js', '.jsx']; + +const extensionAlias = { + '.js': ['.ts', '.tsx', '.js', '.jsx'], + '.mjs': ['.mts', '.mjs'], + '.cjs': ['.cts', '.cjs'], + '.jsx': ['.tsx', '.jsx'], +}; const mainFields = ['module', 'main']; export function applyBaseConfig( @@ -385,6 +392,10 @@ function applyNxDependentConfig( config.resolve = { ...config.resolve, extensions: [...(config?.resolve?.extensions ?? []), ...extensions], + extensionAlias: { + ...(config.resolve?.extensionAlias ?? {}), + ...extensionAlias, + }, alias: { ...(config.resolve?.alias ?? {}), ...(options.fileReplacements?.reduce( diff --git a/packages/webpack/src/plugins/nx-webpack-plugin/lib/apply-base-config.ts b/packages/webpack/src/plugins/nx-webpack-plugin/lib/apply-base-config.ts index 2402d1f831..9fe8f92977 100644 --- a/packages/webpack/src/plugins/nx-webpack-plugin/lib/apply-base-config.ts +++ b/packages/webpack/src/plugins/nx-webpack-plugin/lib/apply-base-config.ts @@ -28,8 +28,10 @@ const IGNORED_WEBPACK_WARNINGS = [ ]; const extensionAlias = { - '.js': ['.ts', '.js'], + '.js': ['.ts', '.tsx', '.js', '.jsx'], '.mjs': ['.mts', '.mjs'], + '.cjs': ['.cts', '.cjs'], + '.jsx': ['.tsx', '.jsx'], }; const extensions = ['.ts', '.tsx', '.mjs', '.js', '.jsx']; const mainFields = ['module', 'main'];