Skip to content

Commit

Permalink
Fix client reference keys of barrel-optimized files (#60685)
Browse files Browse the repository at this point in the history
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 <inbox@huozhi.im>
  • Loading branch information
shuding and huozhi authored Jan 16, 2024
1 parent 6a3608f commit 2072f86
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -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')
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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} };`
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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] = {
Expand Down
7 changes: 7 additions & 0 deletions packages/next/src/build/webpack/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,10 @@ export function forEachEntryModule(
callback({ name, entryModule })
}
}

export function formatBarrelOptimizedResource(
resource: string,
matchResource: string
) {
return `${resource}@${matchResource}`
}
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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',
},
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { Button } from '@mui/material'
import { Typography } from '@mui/material'

export default function Page() {
return <Button>test_mui</Button>
return (
<div>
<Button id="button">button</Button>
<Typography id="typography" variant="h1">
typography
</Typography>
</div>
)
}

0 comments on commit 2072f86

Please sign in to comment.