diff --git a/packages/next/src/build/webpack/loaders/next-flight-loader/index.ts b/packages/next/src/build/webpack/loaders/next-flight-loader/index.ts index fceaf9add3d04..897f86b6ff7e8 100644 --- a/packages/next/src/build/webpack/loaders/next-flight-loader/index.ts +++ b/packages/next/src/build/webpack/loaders/next-flight-loader/index.ts @@ -1,7 +1,11 @@ import { RSC_MOD_REF_PROXY_ALIAS } from '../../../../lib/constants' -import { RSC_MODULE_TYPES } from '../../../../shared/lib/constants' +import { + BARREL_OPTIMIZATION_PREFIX, + RSC_MODULE_TYPES, +} from '../../../../shared/lib/constants' import { warnOnce } from '../../../../shared/lib/utils/warn-once' import { getRSCModuleInformation } from '../../../analysis/get-page-static-info' +import { formatBarrelOptimizedResource } from '../../utils' import { getModuleBuildInfo } from '../get-module-build-info' const noopHeadPath = require.resolve('next/dist/client/components/noop-head') @@ -24,6 +28,26 @@ export default function transformSource( const buildInfo = getModuleBuildInfo(this._module) buildInfo.rsc = getRSCModuleInformation(source, true) + // Resource key is the unique identifier for the resource. When RSC renders + // a client module, that key is used to identify that module across all compiler + // layers. + // + // Usually it's the module's file path + the export name (e.g. `foo.js#bar`). + // But with Barrel Optimizations, one file can be splitted into multiple modules, + // so when you import `foo.js#bar` and `foo.js#baz`, they are actually different + // "foo.js" being created by the Barrel Loader (one only exports `bar`, the other + // only exports `baz`). + // + // Because of that, we must add another query param to the resource key to + // differentiate them. + let resourceKey: string = this.resourcePath + if (this._module?.matchResource?.startsWith(BARREL_OPTIMIZATION_PREFIX)) { + resourceKey = formatBarrelOptimizedResource( + resourceKey, + this._module.matchResource + ) + } + // A client boundary. if (buildInfo.rsc?.type === RSC_MODULE_TYPES.client) { const sourceType = this._module?.parser?.sourceType @@ -61,7 +85,7 @@ export default function transformSource( let esmSource = `\ import { createProxy } from "${MODULE_PROXY_PATH}" -const proxy = createProxy(String.raw\`${this.resourcePath}\`) +const proxy = createProxy(String.raw\`${resourceKey}\`) // Accessing the __esModule property and exporting $$typeof are required here. // The __esModule getter forces the proxy target to create the default export @@ -73,14 +97,14 @@ const __default__ = proxy.default; let cnt = 0 for (const ref of clientRefs) { if (ref === '') { - esmSource += `\nexports[''] = createProxy(String.raw\`${this.resourcePath}#\`);` + esmSource += `\nexports[''] = createProxy(String.raw\`${resourceKey}#\`);` } else if (ref === 'default') { esmSource += ` export { __esModule, $$typeof }; export default __default__;` } else { esmSource += ` -const e${cnt} = createProxy(String.raw\`${this.resourcePath}#${ref}\`); +const e${cnt} = createProxy(String.raw\`${resourceKey}#${ref}\`); export { e${cnt++} as ${ref} };` } } diff --git a/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts b/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts index 222414c52a10d..360f01a9612a6 100644 --- a/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts +++ b/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts @@ -29,7 +29,11 @@ import { isCSSMod, regexCSS, } from '../loaders/utils' -import { traverseModules, forEachEntryModule } from '../utils' +import { + traverseModules, + forEachEntryModule, + formatBarrelOptimizedResource, +} from '../utils' import { normalizePathSep } from '../../../shared/lib/page-path/normalize-path-sep' import { getProxiedPluginState } from '../../build-context' import { generateRandomActionKeyRaw } from '../../../server/app-render/action-encryption-utils' @@ -196,7 +200,7 @@ export class FlightClientEntryPlugin { // so it's only necessary to add it for matchResource or mod.resourceResolveData const modResource = modPath ? modPath.startsWith(BARREL_OPTIMIZATION_PREFIX) - ? mod.resource + ? formatBarrelOptimizedResource(mod.resource, modPath) : modPath + modQuery : mod.resource diff --git a/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts index 8f0879241e62f..f9af3cd2d4626 100644 --- a/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts +++ b/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts @@ -8,6 +8,7 @@ import path from 'path' import { webpack, sources } from 'next/dist/compiled/webpack/webpack' import { + BARREL_OPTIMIZATION_PREFIX, CLIENT_REFERENCE_MANIFEST, SYSTEM_ENTRYPOINTS, } from '../../../shared/lib/constants' @@ -18,6 +19,7 @@ import { WEBPACK_LAYERS } from '../../../lib/constants' import { normalizePagePath } from '../../../shared/lib/page-path/normalize-page-path' import { CLIENT_STATIC_FILES_RUNTIME_MAIN_APP } from '../../../shared/lib/constants' import { getDeploymentIdQueryOrEmptyString } from '../../deployment-id' +import { formatBarrelOptimizedResource } from '../utils' interface Options { dev: boolean @@ -277,7 +279,7 @@ export class ClientReferenceManifestPlugin { return } - const resource = + let resource = mod.type === 'css/mini-extract' ? // @ts-expect-error TODO: use `identifier()` instead. mod._identifier.slice(mod._identifier.lastIndexOf('!') + 1) @@ -313,6 +315,18 @@ export class ClientReferenceManifestPlugin { ) : null + // An extra query param is added to the resource key when it's optimized + // through the Barrel Loader. That's because the same file might be created + // as multiple modules (depending on what you import from it). + // See also: webpack/loaders/next-flight-loader/index.ts. + if (mod.matchResource?.startsWith(BARREL_OPTIMIZATION_PREFIX)) { + ssrNamedModuleId = formatBarrelOptimizedResource( + ssrNamedModuleId, + mod.matchResource + ) + resource = formatBarrelOptimizedResource(resource, mod.matchResource) + } + function addClientReference() { const exportName = resource manifest.clientModules[exportName] = { diff --git a/packages/next/src/build/webpack/utils.ts b/packages/next/src/build/webpack/utils.ts index 95aa01b18bc23..dbc0fa1ccf24f 100644 --- a/packages/next/src/build/webpack/utils.ts +++ b/packages/next/src/build/webpack/utils.ts @@ -77,3 +77,10 @@ export function forEachEntryModule( callback({ name, entryModule }) } } + +export function formatBarrelOptimizedResource( + resource: string, + matchResource: string +) { + return `${resource}@${matchResource}` +} diff --git a/test/development/basic/barrel-optimization/barrel-optimization.test.ts b/test/development/basic/barrel-optimization/barrel-optimization.test.ts index 6fb86c5861381..b20179e489cd5 100644 --- a/test/development/basic/barrel-optimization/barrel-optimization.test.ts +++ b/test/development/basic/barrel-optimization/barrel-optimization.test.ts @@ -1,6 +1,6 @@ import { join } from 'path' import { createNextDescribe } from 'e2e-utils' -import { shouldRunTurboDevTest } from 'next-test-utils' +import { hasRedbox, shouldRunTurboDevTest } from 'next-test-utils' createNextDescribe( 'optimizePackageImports', @@ -28,7 +28,7 @@ createNextDescribe( '@heroicons/react': '2.0.18', '@visx/visx': '3.3.0', 'recursive-barrel': '1.0.0', - '@mui/material': '5.14.19', + '@mui/material': '5.15.4', '@emotion/styled': '11.11.0', '@emotion/react': '11.11.1', }, @@ -137,8 +137,12 @@ createNextDescribe( }) // Ensure that MUI is working - const html = await next.render('/mui') - expect(html).toContain('test_mui') + const $ = await next.render$('/mui') + expect(await $('#button').text()).toContain('button') + expect(await $('#typography').text()).toContain('typography') + + const browser = await next.browser('/mui') + expect(await hasRedbox(browser)).toBe(false) const modules = [...logs.matchAll(/\((\d+) modules\)/g)] expect(modules.length).toBeGreaterThanOrEqual(1) diff --git a/test/development/basic/barrel-optimization/fixture/app/mui/page.js b/test/development/basic/barrel-optimization/fixture/app/mui/page.js index 0a1913f8709b4..4a60ac62638c7 100644 --- a/test/development/basic/barrel-optimization/fixture/app/mui/page.js +++ b/test/development/basic/barrel-optimization/fixture/app/mui/page.js @@ -1,5 +1,13 @@ import { Button } from '@mui/material' +import { Typography } from '@mui/material' export default function Page() { - return + return ( +
+ + + typography + +
+ ) }