Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for esm externals #27069

Merged
merged 1 commit into from
Jul 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions errors/import-esm-externals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# ESM packages need to be imported

#### Why This Error Occurred

Packages in node_modules that are published as EcmaScript Module, need to be `import`ed via `import ... from 'package'` or `import('package')`.

You get this error when using a different way to reference the package, e. g. `require()`.

#### Possible Ways to Fix It

1. Use `import` or `import()` to reference the package instead. (Recommended)

2. If you are already using `import`, make sure that this is not changed by a transpiler, e. g. TypeScript or Babel.

3. Switch to loose mode (`experimental.esmExternals: 'loose'`), which tries to automatically correct this error.

### Useful Links

- [Node.js ESM require docs](https://nodejs.org/dist/latest-v16.x/docs/api/esm.html#esm_require)
4 changes: 4 additions & 0 deletions errors/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,10 @@
{
"title": "placeholder-blur-data-url",
"path": "/errors/placeholder-blur-data-url.md"
},
{
"title": "import-esm-externals",
"path": "/errors/import-esm-externals.md"
}
]
}
Expand Down
136 changes: 101 additions & 35 deletions packages/next/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@ const WEBPACK_RESOLVE_OPTIONS = {
symlinks: true,
}

const WEBPACK_ESM_RESOLVE_OPTIONS = {
dependencyType: 'esm',
symlinks: true,
}

const NODE_RESOLVE_OPTIONS = {
dependencyType: 'commonjs',
modules: ['node_modules'],
Expand All @@ -201,6 +206,13 @@ const NODE_RESOLVE_OPTIONS = {
restrictions: [],
}

const NODE_ESM_RESOLVE_OPTIONS = {
...NODE_RESOLVE_OPTIONS,
dependencyType: 'esm',
conditionNames: ['node', 'import', 'module'],
fullySpecified: true,
}

export default async function getBaseWebpackConfig(
dir: string,
{
Expand Down Expand Up @@ -653,12 +665,19 @@ export default async function getBaseWebpackConfig(
config.conformance
)

const esmExternals = !!config.experimental?.esmExternals
const looseEsmExternals = config.experimental?.esmExternals === 'loose'

async function handleExternals(
context: string,
request: string,
dependencyType: string,
getResolve: (
options: any
) => (resolveContext: string, resolveRequest: string) => Promise<string>
) => (
resolveContext: string,
resolveRequest: string
) => Promise<[string | null, boolean]>
) {
// We need to externalize internal requests for files intended to
// not be bundled.
Expand Down Expand Up @@ -687,27 +706,53 @@ export default async function getBaseWebpackConfig(
}
}

const resolve = getResolve(WEBPACK_RESOLVE_OPTIONS)
// When in esm externals mode, and using import, we resolve with
// ESM resolving options.
const isEsmRequested = dependencyType === 'esm'
const preferEsm = esmExternals && isEsmRequested

const resolve = getResolve(
preferEsm ? WEBPACK_ESM_RESOLVE_OPTIONS : WEBPACK_RESOLVE_OPTIONS
)

// Resolve the import with the webpack provided context, this
// ensures we're resolving the correct version when multiple
// exist.
let res: string
let res: string | null
let isEsm: boolean = false
try {
res = await resolve(context, request)
;[res, isEsm] = await resolve(context, request)
} catch (err) {
// If the request cannot be resolved, we need to tell webpack to
// "bundle" it so that webpack shows an error (that it cannot be
// resolved).
return
res = null
}

// Same as above, if the request cannot be resolved we need to have
// If resolving fails, and we can use an alternative way
// try the alternative resolving options.
if (!res && (isEsmRequested || looseEsmExternals)) {
const resolveAlternative = getResolve(
preferEsm ? WEBPACK_RESOLVE_OPTIONS : WEBPACK_ESM_RESOLVE_OPTIONS
)
try {
;[res, isEsm] = await resolveAlternative(context, request)
} catch (err) {
res = null
}
}

// If the request cannot be resolved we need to have
// webpack "bundle" it so it surfaces the not found error.
if (!res) {
return
}

// ESM externals can only be imported (and not required).
// Make an exception in loose mode.
if (!isEsmRequested && isEsm && !looseEsmExternals) {
throw new Error(
`ESM packages (${request}) need to be imported. Use 'import' to reference the package instead. https://nextjs.org/docs/messages/import-esm-externals`
)
}

if (isLocal) {
// Makes sure dist/shared and dist/server are not bundled
// we need to process shared/lib/router/router so that
Expand Down Expand Up @@ -741,26 +786,32 @@ export default async function getBaseWebpackConfig(
// package that'll be available at runtime. If it's not identical,
// we need to bundle the code (even if it _should_ be external).
let baseRes: string | null
let baseIsEsm: boolean
try {
const baseResolve = getResolve(NODE_RESOLVE_OPTIONS)
baseRes = await baseResolve(dir, request)
const baseResolve = getResolve(
isEsm ? NODE_ESM_RESOLVE_OPTIONS : NODE_RESOLVE_OPTIONS
)
;[baseRes, baseIsEsm] = await baseResolve(dir, request)
} catch (err) {
baseRes = null
baseIsEsm = false
}

// Same as above: if the package, when required from the root,
// would be different from what the real resolution would use, we
// cannot externalize it.
// if res or baseRes are symlinks they could point to the the same file,
// but the resolver will resolve symlinks so this is already handled
if (baseRes !== res) {
// if request is pointing to a symlink it could point to the the same file,
// the resolver will resolve symlinks so this is handled
if (baseRes !== res || isEsm !== baseIsEsm) {
return
}

const externalType = isEsm ? 'module' : 'commonjs'

if (
res.match(/next[/\\]dist[/\\]shared[/\\](?!lib[/\\]router[/\\]router)/)
) {
return `commonjs ${request}`
return `${externalType} ${request}`
}

// Default pages have to be transpiled
Expand All @@ -783,7 +834,7 @@ export default async function getBaseWebpackConfig(
// Anything else that is standard JavaScript within `node_modules`
// can be externalized.
if (/node_modules[/\\].*\.c?js$/.test(res)) {
return `commonjs ${request}`
return `${externalType} ${request}`
}

// Default behavior: bundle the code!
Expand All @@ -803,17 +854,43 @@ export default async function getBaseWebpackConfig(
? ({
context,
request,
dependencyType,
getResolve,
}: {
context: string
request: string
dependencyType: string
getResolve: (
options: any
) => (
resolveContext: string,
resolveRequest: string
) => Promise<string>
}) => handleExternals(context, request, getResolve)
resolveRequest: string,
callback: (
err?: Error,
result?: string,
resolveData?: { descriptionFileData?: { type?: any } }
) => void
) => void
}) =>
handleExternals(context, request, dependencyType, (options) => {
const resolveFunction = getResolve(options)
return (resolveContext: string, requestToResolve: string) =>
new Promise((resolve, reject) => {
resolveFunction(
resolveContext,
requestToResolve,
(err, result, resolveData) => {
if (err) return reject(err)
if (!result) return resolve([null, false])
const isEsm = /\.js$/i.test(result)
? resolveData?.descriptionFileData?.type ===
'module'
: /\.mjs$/i.test(result)
resolve([result, isEsm])
}
)
})
})
: (
context: string,
request: string,
Expand All @@ -822,13 +899,15 @@ export default async function getBaseWebpackConfig(
handleExternals(
context,
request,
'commonjs',
() => (resolveContext: string, requestToResolve: string) =>
new Promise((resolve) =>
resolve(
resolve([
require.resolve(requestToResolve, {
paths: [resolveContext],
})
)
}),
false,
])
)
).then((result) => callback(undefined, result), callback),
]
Expand Down Expand Up @@ -920,19 +999,6 @@ export default async function getBaseWebpackConfig(
],
},
output: {
...(isWebpack5
? {
environment: {
arrowFunction: false,
bigIntLiteral: false,
const: false,
destructuring: false,
dynamicImport: false,
forOf: false,
module: false,
},
}
: {}),
// we must set publicPath to an empty value to override the default of
// auto which doesn't work in IE11
publicPath: `${config.assetPrefix || ''}/_next/`,
Expand Down
7 changes: 6 additions & 1 deletion packages/next/build/webpack/config/blocks/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ export const base = curry(function base(
) {
config.mode = ctx.isDevelopment ? 'development' : 'production'
config.name = ctx.isServer ? 'server' : 'client'
config.target = ctx.isServer ? 'node' : 'web'
if (isWebpack5) {
// @ts-ignore TODO webpack 5 typings
config.target = ctx.isServer ? 'node12.17' : ['web', 'es5']
} else {
config.target = ctx.isServer ? 'node' : 'web'
}

// Stop compilation early in a production build when an error is encountered.
// This behavior isn't desirable in development due to how the HMR system
Expand Down
2 changes: 1 addition & 1 deletion packages/next/bundles/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"dependencies": {
"schema-utils3": "npm:[email protected]",
"webpack-sources2": "npm:[email protected]",
"webpack5": "npm:webpack@5.43.0"
"webpack5": "npm:webpack@5.44.0"
},
"resolutions": {
"browserslist": "4.16.6",
Expand Down
17 changes: 6 additions & 11 deletions packages/next/bundles/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,11 @@
"@types/estree" "*"
"@types/json-schema" "*"

"@types/estree@*":
"@types/estree@*", "@types/estree@^0.0.50":
version "0.0.50"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83"
integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==

"@types/estree@^0.0.49":
version "0.0.49"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.49.tgz#3facb98ebcd4114a4ecef74e0de2175b56fd4464"
integrity sha512-K1AFuMe8a+pXmfHTtnwBvqoEylNKVeaiKYkjmcEAdytMQVJ/i9Fu7sc13GxgXdO49gkE7Hy8SyJonUZUn+eVaw==

"@types/json-schema@*", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.7":
version "7.0.8"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.8.tgz#edf1bf1dbf4e04413ca8e5b17b3b7d7d54b59818"
Expand Down Expand Up @@ -483,13 +478,13 @@ watchpack@^2.2.0:
source-list-map "^2.0.1"
source-map "^0.6.1"

"webpack5@npm:webpack@5.43.0":
version "5.43.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.43.0.tgz#36a122d6e9bac3836273857f56ed7801d40c9145"
integrity sha512-ex3nB9uxNI0azzb0r3xGwi+LS5Gw1RCRSKk0kg3kq9MYdIPmLS6UI3oEtG7esBaB51t9I+5H+vHmL3htaxqMSw==
"webpack5@npm:webpack@5.44.0":
version "5.44.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.44.0.tgz#97b13a02bd79fb71ac6301ce697920660fa214a1"
integrity sha512-I1S1w4QLoKmH19pX6YhYN0NiSXaWY8Ou00oA+aMcr9IUGeF5azns+IKBkfoAAG9Bu5zOIzZt/mN35OffBya8AQ==
dependencies:
"@types/eslint-scope" "^3.7.0"
"@types/estree" "^0.0.49"
"@types/estree" "^0.0.50"
"@webassemblyjs/ast" "1.11.1"
"@webassemblyjs/wasm-edit" "1.11.1"
"@webassemblyjs/wasm-parser" "1.11.1"
Expand Down
Loading