diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index cc29ba8960e7d..d74fea3d98337 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -119,7 +119,9 @@ function errorIfEnvConflicted(config: NextConfigComplete, key: string) { function isResourceInPackages(resource: string, packageNames?: string[]) { return packageNames?.some((p: string) => - new RegExp('[/\\\\]node_modules[/\\\\]' + p + '[/\\\\]').test(resource) + resource.includes( + path.sep + pathJoin('node_modules', p.replace(/\//g, path.sep)) + path.sep + ) ) } @@ -1168,21 +1170,38 @@ export default async function getBaseWebpackConfig( return } + // If a package should be transpiled by Next.js, we skip making it external. + // It doesn't matter what the extension is, as we'll transpile it anyway. + const shouldBeBundled = isResourceInPackages( + res, + config.experimental.transpilePackages + ) + if (/node_modules[/\\].*\.[mc]?js$/.test(res)) { if (layer === WEBPACK_LAYERS.server) { // All packages should be bundled for the server layer if they're not opted out. - if (isResourceInPackages(res, optoutBundlingPackages)) { + // This option takes priority over the transpilePackages option. + if ( + isResourceInPackages( + res, + config.experimental.serverComponentsExternalPackages + ) + ) { return `${externalType} ${request}` } return } + if (shouldBeBundled) return + // Anything else that is standard JavaScript within `node_modules` // can be externalized. return `${externalType} ${request}` } + if (shouldBeBundled) return + // Default behavior: bundle the code! } @@ -1196,6 +1215,13 @@ export default async function getBaseWebpackConfig( if (babelIncludeRegexes.some((r) => r.test(excludePath))) { return false } + + const shouldBeBundled = isResourceInPackages( + excludePath, + config.experimental.transpilePackages + ) + if (shouldBeBundled) return false + return excludePath.includes('node_modules') }, } diff --git a/packages/next/server/config-schema.ts b/packages/next/server/config-schema.ts index 0e5980a44baad..29d08ee22729b 100644 --- a/packages/next/server/config-schema.ts +++ b/packages/next/server/config-schema.ts @@ -348,6 +348,12 @@ const configSchema = { }, type: 'array', }, + transpilePackages: { + items: { + type: 'string', + }, + type: 'array', + }, scrollRestoration: { type: 'boolean', }, diff --git a/packages/next/server/config-shared.ts b/packages/next/server/config-shared.ts index fee7fe3560a30..942f5d41e514e 100644 --- a/packages/next/server/config-shared.ts +++ b/packages/next/server/config-shared.ts @@ -159,6 +159,9 @@ export interface ExperimentalConfig { // A list of packages that should be treated as external in the RSC server build serverComponentsExternalPackages?: string[] + // A list of packages that should always be transpiled and bundled in the server + transpilePackages?: string[] + fontLoaders?: [{ loader: string; options?: any }] webVitalsAttribution?: Array diff --git a/test/e2e/app-dir/rsc-external.test.ts b/test/e2e/app-dir/rsc-external.test.ts index 1924f0ffe0177..c7c030d6f37e0 100644 --- a/test/e2e/app-dir/rsc-external.test.ts +++ b/test/e2e/app-dir/rsc-external.test.ts @@ -84,6 +84,11 @@ describe('app dir - rsc external dependency', () => { ) }) + it('should transpile specific external packages with the `transpilePackages` option', async () => { + const clientHtml = await renderViaHTTP(next.url, '/external-imports/client') + expect(clientHtml).toContain('transpilePackages:5') + }) + it('should resolve the subset react in server components based on the react-server condition', async () => { await fetchViaHTTP(next.url, '/react-server').then(async (response) => { const result = await resolveStreamResponse(response) diff --git a/test/e2e/app-dir/rsc-external/app/external-imports/client/page.js b/test/e2e/app-dir/rsc-external/app/external-imports/client/page.js index d2be481e59eab..4c4c3499882ce 100644 --- a/test/e2e/app-dir/rsc-external/app/external-imports/client/page.js +++ b/test/e2e/app-dir/rsc-external/app/external-imports/client/page.js @@ -2,6 +2,8 @@ import getType, { named, value, array, obj } from 'non-isomorphic-text' +import add from 'untranspiled-module' + export default function Page() { return (
@@ -10,6 +12,7 @@ export default function Page() {
{`export value:${value}`}
{`export array:${array.join(',')}`}
{`export object:{x:${obj.x}}`}
+
{`transpilePackages:${add(2, 3)}`}
) } diff --git a/test/e2e/app-dir/rsc-external/next.config.js b/test/e2e/app-dir/rsc-external/next.config.js index fadd1f407c87e..34b0a1a36b0bd 100644 --- a/test/e2e/app-dir/rsc-external/next.config.js +++ b/test/e2e/app-dir/rsc-external/next.config.js @@ -3,5 +3,6 @@ module.exports = { experimental: { appDir: true, serverComponentsExternalPackages: ['conditional-exports-optout'], + transpilePackages: ['untranspiled-module'], }, } diff --git a/test/e2e/app-dir/rsc-external/node_modules_bak/untranspiled-module/index.ts b/test/e2e/app-dir/rsc-external/node_modules_bak/untranspiled-module/index.ts new file mode 100644 index 0000000000000..d964a1a631fbf --- /dev/null +++ b/test/e2e/app-dir/rsc-external/node_modules_bak/untranspiled-module/index.ts @@ -0,0 +1,3 @@ +export default function add(a: number, b: number) { + return a + b +} diff --git a/test/e2e/app-dir/rsc-external/node_modules_bak/untranspiled-module/package.json b/test/e2e/app-dir/rsc-external/node_modules_bak/untranspiled-module/package.json new file mode 100644 index 0000000000000..05558992ba58e --- /dev/null +++ b/test/e2e/app-dir/rsc-external/node_modules_bak/untranspiled-module/package.json @@ -0,0 +1,4 @@ +{ + "name": "untranspiled-module", + "main": "index.ts" +}