fix(nextjs): add extension alias support for handling ESM libs (#31323)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->
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
<!-- This is the behavior we should expect with the changes in this PR
-->

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)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
This is also related to our webpack and rspack packages. Changes have
also been made to them to ensure consistency across bundlers.

Fixes #30714
This commit is contained in:
Nicholas Cunningham 2025-06-05 13:54:29 -06:00 committed by GitHub
parent 5c405fa72f
commit ae89efb8d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 131 additions and 19 deletions

View File

@ -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}`
);
});
});

View File

@ -10,7 +10,6 @@ import {
uniq, uniq,
updateFile, updateFile,
} from '@nx/e2e/utils'; } from '@nx/e2e/utils';
import * as http from 'http';
import { checkApp } from './utils'; import { checkApp } from './utils';
describe('Next.js Applications', () => { describe('Next.js Applications', () => {
@ -240,20 +239,3 @@ describe('Next.js Applications', () => {
expect(postBuildPagesContent).toMatchSnapshot(); expect(postBuildPagesContent).toMatchSnapshot();
}); });
}); });
function getData(port, path = ''): Promise<any> {
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));
});
}

View File

@ -87,4 +87,47 @@ describe('React (TS solution)', () => {
checkFilesExist(`packages/${appName}/dist/index.html`); checkFilesExist(`packages/${appName}/dist/index.html`);
}, 90_000); }, 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);
}); });

View File

@ -261,6 +261,17 @@ export function getNextConfig(
}, },
...validNextConfig, ...validNextConfig,
webpack: (config, options) => { 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. * 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. * The 'upward' mode allows the root babel.config.json and per-project .babelrc files to be picked up.

View File

@ -27,6 +27,13 @@ const IGNORED_RSPACK_WARNINGS = [
]; ];
const extensions = ['...', '.ts', '.tsx', '.mjs', '.js', '.jsx']; 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']; const mainFields = ['module', 'main'];
export function applyBaseConfig( export function applyBaseConfig(
@ -385,6 +392,10 @@ function applyNxDependentConfig(
config.resolve = { config.resolve = {
...config.resolve, ...config.resolve,
extensions: [...(config?.resolve?.extensions ?? []), ...extensions], extensions: [...(config?.resolve?.extensions ?? []), ...extensions],
extensionAlias: {
...(config.resolve?.extensionAlias ?? {}),
...extensionAlias,
},
alias: { alias: {
...(config.resolve?.alias ?? {}), ...(config.resolve?.alias ?? {}),
...(options.fileReplacements?.reduce( ...(options.fileReplacements?.reduce(

View File

@ -28,8 +28,10 @@ const IGNORED_WEBPACK_WARNINGS = [
]; ];
const extensionAlias = { const extensionAlias = {
'.js': ['.ts', '.js'], '.js': ['.ts', '.tsx', '.js', '.jsx'],
'.mjs': ['.mts', '.mjs'], '.mjs': ['.mts', '.mjs'],
'.cjs': ['.cts', '.cjs'],
'.jsx': ['.tsx', '.jsx'],
}; };
const extensions = ['.ts', '.tsx', '.mjs', '.js', '.jsx']; const extensions = ['.ts', '.tsx', '.mjs', '.js', '.jsx'];
const mainFields = ['module', 'main']; const mainFields = ['module', 'main'];