From 2072f864fc1d716d0884628dada7e1c6fbf7fbf2 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Tue, 16 Jan 2024 20:06:22 +0100 Subject: [PATCH] Fix client reference keys of barrel-optimized files (#60685) As Barrel Optimization might split one file into multiple different modules, i.e. when you import different values from it, the target file might be transformed differently, we can no longer rely on the file path as the identifier of the client reference. This fix adds a suffix (`'@' + this._module.matchResource`) to the identifier so it looks like `/filepath/file.js@__barrel_optimize__?names=Foo`. Here's also a quick diagram to explain: ![CleanShot-2024-01-16-QzlxzMKy@2x](https://github.com/vercel/next.js/assets/3676859/99f25975-b965-4ae0-91f2-269a6a0d7458) Closes #59804. Closes NEXT-2108. --------- Co-authored-by: Jiachi Liu --- .../loaders/next-flight-loader/index.ts | 32 ++++++++++++++++--- .../plugins/flight-client-entry-plugin.ts | 8 +++-- .../webpack/plugins/flight-manifest-plugin.ts | 16 +++++++++- packages/next/src/build/webpack/utils.ts | 7 ++++ .../barrel-optimization.test.ts | 12 ++++--- .../fixture/app/mui/page.js | 10 +++++- 6 files changed, 73 insertions(+), 12 deletions(-) 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 + +
+ ) }