Fix handling subpath for server components externals (#62150)

Follow up of #61986 where we didn't handle the subpath externals very
well, found by @sokra

Closes NEXT-2517
This commit is contained in:
Jiachi Liu 2024-02-16 17:24:12 +01:00 committed by GitHub
parent 646a3d99db
commit cbdd1d2654
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 45 additions and 8 deletions

View file

@ -25,6 +25,15 @@ const externalPattern = new RegExp(
const nodeModulesRegex = /node_modules[/\\].*\.[mc]?js$/
function containsImportInPackages(
request: string,
packages: string[]
): boolean {
return packages.some(
(pkg) => request === pkg || request.startsWith(pkg + '/')
)
}
export function isResourceInPackages(
resource: string,
packageNames?: string[],
@ -74,7 +83,7 @@ export async function resolveExternal(
// For package that marked as externals that should be not bundled,
// we don't resolve them as ESM since it could be resolved as async module,
// such as `import(external package)` in the bundle, valued as a `Promise`.
!optOutBundlingPackages.some((optOut) => request.startsWith(optOut))
!containsImportInPackages(request, optOutBundlingPackages)
? [true, false]
: [false]
@ -151,7 +160,6 @@ export function makeExternalHandler({
}) {
let resolvedExternalPackageDirs: Map<string, string>
const looseEsmExternals = config.experimental?.esmExternals === 'loose'
const optOutBundlingPackagesSet = new Set(optOutBundlingPackages)
return async function handleExternals(
context: string,
@ -276,7 +284,7 @@ export function makeExternalHandler({
: request
// Check if it's opt out bundling package first
if (optOutBundlingPackagesSet.has(fullRequest)) {
if (containsImportInPackages(fullRequest, optOutBundlingPackages)) {
return fullRequest
}
return resolveNextExternal(fullRequest)

View file

@ -1,7 +1,13 @@
'use client'
import { dir } from 'external-package'
import { dir as subDir } from 'external-package/subpath'
export default function Page() {
return <div id="directory-ssr">{dir}</div>
return (
<>
<div id="directory-ssr">{dir}</div>
<div id="subdirectory-ssr">{subDir}</div>
</>
)
}

View file

@ -1,5 +1,11 @@
import { dir } from 'external-package'
import { dir as subDir } from 'external-package/subpath'
export default function Page() {
return <div id="directory">{dir}</div>
return (
<>
<div id="directory">{dir}</div>
<div id="subdirectory">{subDir}</div>
</>
)
}

View file

@ -11,9 +11,13 @@ createNextDescribe(
const $ = await next.render$('/')
const text = $('#directory').text()
const subpath = $('#subdirectory').text()
expect(text).toBe(
path.join(next.testDir, 'node_modules', 'external-package')
)
expect(subpath).toBe(
path.join(next.testDir, 'node_modules', 'external-package', 'subpath')
)
})
it('uses externals for predefined list in server-external-packages.json', async () => {
@ -28,7 +32,13 @@ createNextDescribe(
it('should externalize serverComponentsExternalPackages for server rendering layer', async () => {
await next.fetch('/client')
const ssrBundle = await next.readFile('.next/server/app/client/page.js')
expect(ssrBundle).not.toContain('external-package-mark')
expect(ssrBundle).not.toContain('external-package-mark:index')
expect(ssrBundle).not.toContain('external-package-mark:subpath')
await next.fetch('/')
const rscBundle = await next.readFile('.next/server/app/page.js')
expect(rscBundle).not.toContain('external-package-mark:index')
expect(rscBundle).not.toContain('external-package-mark:subpath')
})
}
}

View file

@ -1,4 +1,4 @@
module.exports = {
dir: __dirname,
value: 'external-package-mark',
value: 'external-package-mark:index',
}

View file

@ -2,5 +2,10 @@
"name": "external-package",
"version": "1.0.0",
"description": "External package",
"main": "index.js"
"main": "index.js",
"exports": {
"./package.json": "./package.json",
".": "./index.js",
"./subpath": "./subpath/index.js"
}
}

View file

@ -0,0 +1,2 @@
exports.value = 'external-package-mark:subpath'
exports.dir = __dirname