Skip to content

Commit 33e09df

Browse files
authored
webpack 5 externals fixes (vercel#24603)
* fix check in externals that validate if the require is resolve-able for the server * performance improvements Fixes vercel#23130 ## Bug - [x] Related issues linked using `fixes #number` - [ ] Integration tests added ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [x] Related issues linked using `fixes #number` - [x] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. ## Documentation / Examples - [ ] Make sure the linting passes
1 parent 7c7e864 commit 33e09df

File tree

9 files changed

+185
-80
lines changed

9 files changed

+185
-80
lines changed

packages/next/build/webpack-config.ts

Lines changed: 90 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import ReactRefreshWebpackPlugin from '@next/react-refresh-utils/ReactRefreshWebpackPlugin'
22
import chalk from 'chalk'
33
import crypto from 'crypto'
4-
import { readFileSync, realpathSync } from 'fs'
4+
import { readFileSync } from 'fs'
55
import { codeFrameColumns } from 'next/dist/compiled/babel/code-frame'
66
import semver from 'next/dist/compiled/semver'
77
import { isWebpack5, webpack } from 'next/dist/compiled/webpack/webpack'
@@ -170,6 +170,35 @@ export function attachReactRefresh(
170170
}
171171
}
172172

173+
const WEBPACK_RESOLVE_OPTIONS = {
174+
// This always uses commonjs resolving, assuming API is identical
175+
// between ESM and CJS in a package
176+
// Otherwise combined ESM+CJS packages will never be external
177+
// as resolving mismatch would lead to opt-out from being external.
178+
dependencyType: 'commonjs',
179+
}
180+
181+
const NODE_RESOLVE_OPTIONS = {
182+
dependencyType: 'commonjs',
183+
modules: ['node_modules'],
184+
alias: false,
185+
fallback: false,
186+
exportsFields: ['exports'],
187+
importsFields: ['imports'],
188+
conditionNames: ['node', 'require', 'module'],
189+
descriptionFiles: ['package.json'],
190+
extensions: ['.js', '.json', '.node'],
191+
enforceExtensions: false,
192+
symlinks: true,
193+
mainFields: ['main'],
194+
mainFiles: ['index'],
195+
roots: [],
196+
fullySpecified: false,
197+
preferRelative: false,
198+
preferAbsolute: false,
199+
restrictions: [],
200+
}
201+
173202
export default async function getBaseWebpackConfig(
174203
dir: string,
175204
{
@@ -606,29 +635,10 @@ export default async function getBaseWebpackConfig(
606635
async function handleExternals(
607636
context: string,
608637
request: string,
609-
getResolve: () => (
610-
resolveContext: string,
611-
resolveRequest: string
612-
) => Promise<string>
638+
getResolve: (
639+
options: any
640+
) => (resolveContext: string, resolveRequest: string) => Promise<string>
613641
) {
614-
if (request === 'next') {
615-
return `commonjs ${request}`
616-
}
617-
618-
const notExternalModules = [
619-
'next/app',
620-
'next/document',
621-
'next/link',
622-
'next/image',
623-
'next/error',
624-
'string-hash',
625-
'next/constants',
626-
]
627-
628-
if (notExternalModules.indexOf(request) !== -1) {
629-
return
630-
}
631-
632642
// We need to externalize internal requests for files intended to
633643
// not be bundled.
634644

@@ -640,18 +650,27 @@ export default async function getBaseWebpackConfig(
640650
// When on Windows, we also want to check for Windows-specific
641651
// absolute paths.
642652
(process.platform === 'win32' && path.win32.isAbsolute(request))
643-
const isLikelyNextExternal =
644-
isLocal && /[/\\]next-server[/\\]/.test(request)
645653

646654
// Relative requires don't need custom resolution, because they
647655
// are relative to requests we've already resolved here.
648656
// Absolute requires (require('/foo')) are extremely uncommon, but
649657
// also have no need for customization as they're already resolved.
650-
if (isLocal && !isLikelyNextExternal) {
651-
return
658+
if (isLocal) {
659+
if (!/[/\\]next-server[/\\]/.test(request)) {
660+
return
661+
}
662+
} else {
663+
if (/^(?:next$|react(?:$|\/))/.test(request)) {
664+
return `commonjs ${request}`
665+
}
666+
667+
const notExternalModules = /^(?:private-next-pages\/|next\/(?:dist\/pages\/|(?:app|document|link|image|constants)$)|string-hash$)/
668+
if (notExternalModules.test(request)) {
669+
return
670+
}
652671
}
653672

654-
const resolve = getResolve()
673+
const resolve = getResolve(WEBPACK_RESOLVE_OPTIONS)
655674

656675
// Resolve the import with the webpack provided context, this
657676
// ensures we're resolving the correct version when multiple
@@ -672,54 +691,59 @@ export default async function getBaseWebpackConfig(
672691
return
673692
}
674693

675-
let isNextExternal: boolean = false
676694
if (isLocal) {
677695
// we need to process next-server/lib/router/router so that
678696
// the DefinePlugin can inject process.env values
679-
isNextExternal = /next[/\\]dist[/\\]next-server[/\\](?!lib[/\\]router[/\\]router)/.test(
697+
const isNextExternal = /next[/\\]dist[/\\]next-server[/\\](?!lib[/\\]router[/\\]router)/.test(
680698
res
681699
)
682700

683-
if (!isNextExternal) {
701+
if (isNextExternal) {
702+
// Generate Next.js external import
703+
const externalRequest = path.posix.join(
704+
'next',
705+
'dist',
706+
path
707+
.relative(
708+
// Root of Next.js package:
709+
path.join(__dirname, '..'),
710+
res
711+
)
712+
// Windows path normalization
713+
.replace(/\\/g, '/')
714+
)
715+
return `commonjs ${externalRequest}`
716+
} else {
684717
return
685718
}
686719
}
687720

688-
// `isNextExternal` special cases Next.js' internal requires that
689-
// should not be bundled. We need to skip the base resolve routine
690-
// to prevent it from being bundled (assumes Next.js version cannot
691-
// mismatch).
692-
if (!isNextExternal) {
693-
// Bundled Node.js code is relocated without its node_modules tree.
694-
// This means we need to make sure its request resolves to the same
695-
// package that'll be available at runtime. If it's not identical,
696-
// we need to bundle the code (even if it _should_ be external).
697-
let baseRes: string | null
698-
try {
699-
baseRes = await resolve(dir, request)
700-
} catch (err) {
701-
baseRes = null
702-
}
721+
// Bundled Node.js code is relocated without its node_modules tree.
722+
// This means we need to make sure its request resolves to the same
723+
// package that'll be available at runtime. If it's not identical,
724+
// we need to bundle the code (even if it _should_ be external).
725+
let baseRes: string | null
726+
try {
727+
const baseResolve = getResolve(NODE_RESOLVE_OPTIONS)
728+
baseRes = await baseResolve(dir, request)
729+
} catch (err) {
730+
baseRes = null
731+
}
703732

704-
// Same as above: if the package, when required from the root,
705-
// would be different from what the real resolution would use, we
706-
// cannot externalize it.
707-
if (
708-
!baseRes ||
709-
(baseRes !== res &&
710-
// if res and baseRes are symlinks they could point to the the same file
711-
realpathSync(baseRes) !== realpathSync(res))
712-
) {
713-
return
714-
}
733+
// Same as above: if the package, when required from the root,
734+
// would be different from what the real resolution would use, we
735+
// cannot externalize it.
736+
// if res or baseRes are symlinks they could point to the the same file,
737+
// but the resolver will resolve symlinks so this is already handled
738+
if (baseRes !== res) {
739+
return
715740
}
716741

717742
// Default pages have to be transpiled
718743
if (
719-
!res.match(/next[/\\]dist[/\\]next-server[/\\]/) &&
720-
(res.match(/[/\\]next[/\\]dist[/\\]/) ||
721-
// This is the @babel/plugin-transform-runtime "helpers: true" option
722-
res.match(/node_modules[/\\]@babel[/\\]runtime[/\\]/))
744+
res.match(/[/\\]next[/\\]dist[/\\]/) ||
745+
// This is the @babel/plugin-transform-runtime "helpers: true" option
746+
res.match(/node_modules[/\\]@babel[/\\]runtime[/\\]/)
723747
) {
724748
return
725749
}
@@ -734,24 +758,8 @@ export default async function getBaseWebpackConfig(
734758

735759
// Anything else that is standard JavaScript within `node_modules`
736760
// can be externalized.
737-
if (isNextExternal || res.match(/node_modules[/\\].*\.js$/)) {
738-
const externalRequest = isNextExternal
739-
? // Generate Next.js external import
740-
path.posix.join(
741-
'next',
742-
'dist',
743-
path
744-
.relative(
745-
// Root of Next.js package:
746-
path.join(__dirname, '..'),
747-
res
748-
)
749-
// Windows path normalization
750-
.replace(/\\/g, '/')
751-
)
752-
: request
753-
754-
return `commonjs ${externalRequest}`
761+
if (/node_modules[/\\].*\.c?js$/.test(res)) {
762+
return `commonjs ${request}`
755763
}
756764

757765
// Default behavior: bundle the code!
@@ -775,7 +783,9 @@ export default async function getBaseWebpackConfig(
775783
}: {
776784
context: string
777785
request: string
778-
getResolve: () => (
786+
getResolve: (
787+
options: any
788+
) => (
779789
resolveContext: string,
780790
resolveRequest: string
781791
) => Promise<string>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module.exports = {
2+
webpack(config) {
3+
config.resolve.alias = {
4+
...config.resolve.alias,
5+
'preact/compat': 'react',
6+
}
7+
8+
return config
9+
},
10+
future: {
11+
webpack5: true,
12+
},
13+
}

test/integration/externals/node_modules/esm-package/correct.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/integration/externals/node_modules/esm-package/package.json

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/integration/externals/node_modules/esm-package/wrong.mjs

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from 'preact/compat'
2+
import World from 'esm-package/entry'
3+
4+
export async function getStaticProps() {
5+
return {
6+
props: {},
7+
}
8+
}
9+
10+
export default function Index(props) {
11+
return <div>Hello {World}</div>
12+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import React from 'preact/compat'
2+
import World from 'esm-package/entry'
3+
4+
export function getServerSideProps() {
5+
return {}
6+
}
7+
8+
export default function Index(props) {
9+
return <div>Hello {World}</div>
10+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import React from 'preact/compat'
2+
import World from 'esm-package/entry'
3+
4+
export default function Index(props) {
5+
return <div>Hello {World}</div>
6+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/* eslint-env jest */
2+
3+
import fs from 'fs-extra'
4+
import { join } from 'path'
5+
import {
6+
nextBuild,
7+
findPort,
8+
nextStart,
9+
killApp,
10+
renderViaHTTP,
11+
} from 'next-test-utils'
12+
13+
jest.setTimeout(1000 * 60 * 2)
14+
const appDir = join(__dirname, '../')
15+
let appPort
16+
let app
17+
18+
describe('Valid resolve alias', () => {
19+
beforeAll(async () => {
20+
await fs.remove(join(appDir, '.next'))
21+
await nextBuild(appDir)
22+
appPort = await findPort()
23+
app = await nextStart(appDir, appPort)
24+
})
25+
afterAll(() => killApp(app))
26+
27+
it('should render the static page', async () => {
28+
const html = await renderViaHTTP(appPort, '/static')
29+
expect(html).toMatch(/Hello <!-- -->World/)
30+
})
31+
32+
it('should render the ssr page', async () => {
33+
const html = await renderViaHTTP(appPort, '/ssr')
34+
expect(html).toMatch(/Hello <!-- -->World/)
35+
})
36+
37+
it('should render the ssg page', async () => {
38+
const html = await renderViaHTTP(appPort, '/ssg')
39+
expect(html).toMatch(/Hello <!-- -->World/)
40+
})
41+
})

0 commit comments

Comments
 (0)