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

Share resolve logic for trace and externals #30499

Merged
merged 2 commits into from
Oct 28, 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
211 changes: 126 additions & 85 deletions packages/next/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,93 @@ let TSCONFIG_WARNED = false
export const nextImageLoaderRegex =
/\.(png|jpg|jpeg|gif|webp|avif|ico|bmp|svg)$/i

export async function resolveExternal(
appDir: string,
esmExternalsConfig: NextConfigComplete['experimental']['esmExternals'],
context: string,
request: string,
isEsmRequested: boolean,
getResolve: (
options: any
) => (
resolveContext: string,
resolveRequest: string
) => Promise<[string | null, boolean]>,
isLocalCallback?: (res: string) => any,
baseResolveCheck = true,
esmResolveOptions: any = NODE_ESM_RESOLVE_OPTIONS,
nodeResolveOptions: any = NODE_RESOLVE_OPTIONS,
baseEsmResolveOptions: any = NODE_BASE_ESM_RESOLVE_OPTIONS,
baseResolveOptions: any = NODE_BASE_RESOLVE_OPTIONS
) {
const esmExternals = !!esmExternalsConfig
const looseEsmExternals = esmExternalsConfig === 'loose'

let res: string | null = null
let isEsm: boolean = false

let preferEsmOptions =
esmExternals && isEsmRequested ? [true, false] : [false]
for (const preferEsm of preferEsmOptions) {
const resolve = getResolve(
preferEsm ? esmResolveOptions : nodeResolveOptions
)

// Resolve the import with the webpack provided context, this
// ensures we're resolving the correct version when multiple
// exist.
try {
;[res, isEsm] = await resolve(context, request)
} catch (err) {
res = null
}

if (!res) {
continue
}

// ESM externals can only be imported (and not required).
// Make an exception in loose mode.
if (!isEsmRequested && isEsm && !looseEsmExternals) {
continue
}

if (isLocalCallback) {
return { localRes: isLocalCallback(res) }
}

// Bundled Node.js code is relocated without its node_modules tree.
// This means we need to make sure its request resolves to the same
// package that'll be available at runtime. If it's not identical,
// we need to bundle the code (even if it _should_ be external).
if (baseResolveCheck) {
let baseRes: string | null
let baseIsEsm: boolean
try {
const baseResolve = getResolve(
isEsm ? baseEsmResolveOptions : baseResolveOptions
)
;[baseRes, baseIsEsm] = await baseResolve(appDir, 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 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) {
res = null
continue
}
}
break
}
return { res, isEsm }
}

export default async function getBaseWebpackConfig(
dir: string,
{
Expand Down Expand Up @@ -695,8 +782,6 @@ export default async function getBaseWebpackConfig(
}

const crossOrigin = config.crossOrigin

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

async function handleExternals(
Expand All @@ -712,7 +797,6 @@ export default async function getBaseWebpackConfig(
) {
// We need to externalize internal requests for files intended to
// not be bundled.

const isLocal: boolean =
request.startsWith('.') ||
// Always check for unix-style path, as webpack sometimes
Expand Down Expand Up @@ -742,94 +826,51 @@ export default async function getBaseWebpackConfig(
// ESM resolving options.
const isEsmRequested = dependencyType === 'esm'

let res: string | null = null
let isEsm: boolean = false

let preferEsmOptions =
esmExternals && isEsmRequested ? [true, false] : [false]
for (const preferEsm of preferEsmOptions) {
const resolve = getResolve(
preferEsm ? NODE_ESM_RESOLVE_OPTIONS : NODE_RESOLVE_OPTIONS
)

// Resolve the import with the webpack provided context, this
// ensures we're resolving the correct version when multiple
// exist.
try {
;[res, isEsm] = await resolve(context, request)
} catch (err) {
res = null
}

if (!res) {
continue
}

// ESM externals can only be imported (and not required).
// Make an exception in loose mode.
if (!isEsmRequested && isEsm && !looseEsmExternals) {
continue
}

if (isLocal) {
// Makes sure dist/shared and dist/server are not bundled
// we need to process shared `router/router` and `dynamic`,
// so that the DefinePlugin can inject process.env values
const isNextExternal =
/next[/\\]dist[/\\](shared|server)[/\\](?!lib[/\\](router[/\\]router|dynamic))/.test(
res
)

if (isNextExternal) {
// Generate Next.js external import
const externalRequest = path.posix.join(
'next',
'dist',
path
.relative(
// Root of Next.js package:
path.join(__dirname, '..'),
res
)
// Windows path normalization
.replace(/\\/g, '/')
)
return `commonjs ${externalRequest}`
} else {
// We don't want to retry local requests
// with other preferEsm options
return
}
}
const isLocalCallback = (localRes: string) => {
// Makes sure dist/shared and dist/server are not bundled
// we need to process shared `router/router` and `dynamic`,
// so that the DefinePlugin can inject process.env values
const isNextExternal =
/next[/\\]dist[/\\](shared|server)[/\\](?!lib[/\\](router[/\\]router|dynamic))/.test(
localRes
)

// Bundled Node.js code is relocated without its node_modules tree.
// This means we need to make sure its request resolves to the same
// 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(
isEsm ? NODE_BASE_ESM_RESOLVE_OPTIONS : NODE_BASE_RESOLVE_OPTIONS
if (isNextExternal) {
// Generate Next.js external import
const externalRequest = path.posix.join(
'next',
'dist',
path
.relative(
// Root of Next.js package:
path.join(__dirname, '..'),
localRes
)
// Windows path normalization
.replace(/\\/g, '/')
)
;[baseRes, baseIsEsm] = await baseResolve(dir, request)
} catch (err) {
baseRes = null
baseIsEsm = false
return `commonjs ${externalRequest}`
} else {
// We don't want to retry local requests
// with other preferEsm options
return
}
}

// 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 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) {
res = null
continue
}
const resolveResult = await resolveExternal(
dir,
config.experimental.esmExternals,
context,
request,
isEsmRequested,
getResolve,
isLocal ? isLocalCallback : undefined
)

break
if ('localRes' in resolveResult) {
return resolveResult.localRes
}
const { res, isEsm } = resolveResult

// If the request cannot be resolved we need to have
// webpack "bundle" it so it surfaces the not found error.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
nextImageLoaderRegex,
NODE_ESM_RESOLVE_OPTIONS,
NODE_RESOLVE_OPTIONS,
resolveExternal,
} from '../../webpack-config'
import { NextConfigComplete } from '../../../server/config-shared'

Expand Down Expand Up @@ -407,7 +408,7 @@ export class TraceEntryPointsPlugin implements webpack5.WebpackPluginInstance {
request: string,
job: import('@vercel/nft/out/node-file-trace').Job
) =>
new Promise<string>((resolve, reject) => {
new Promise<[string, boolean]>((resolve, reject) => {
const context = nodePath.dirname(parent)

curResolver.resolve(
Expand All @@ -419,7 +420,7 @@ export class TraceEntryPointsPlugin implements webpack5.WebpackPluginInstance {
missingDependencies: compilation.missingDependencies,
contextDependencies: compilation.contextDependencies,
},
async (err: any, result?: string | false, resContext?: any) => {
async (err: any, result?, resContext?) => {
if (err) return reject(err)

if (!result) {
Expand Down Expand Up @@ -472,22 +473,32 @@ export class TraceEntryPointsPlugin implements webpack5.WebpackPluginInstance {
// we failed to resolve the package.json boundary,
// we don't block emitting the initial asset from this
}
resolve(result)
resolve([result, options.dependencyType === 'esm'])
}
)
})
}

const CJS_RESOLVE_OPTIONS = {
...NODE_RESOLVE_OPTIONS,
fullySpecified: undefined,
modules: undefined,
extensions: undefined,
}
const BASE_CJS_RESOLVE_OPTIONS = {
...CJS_RESOLVE_OPTIONS,
alias: false,
}
const ESM_RESOLVE_OPTIONS = {
...NODE_ESM_RESOLVE_OPTIONS,
fullySpecified: undefined,
modules: undefined,
extensions: undefined,
}
const BASE_ESM_RESOLVE_OPTIONS = {
...ESM_RESOLVE_OPTIONS,
alias: false,
}

const doResolve = async (
request: string,
Expand All @@ -500,30 +511,25 @@ export class TraceEntryPointsPlugin implements webpack5.WebpackPluginInstance {
`not resolving ${request} as this is handled by next-image-loader`
)
}
const context = nodePath.dirname(parent)
// When in esm externals mode, and using import, we resolve with
// ESM resolving options.
const esmExternals = this.esmExternals
const looseEsmExternals = this.esmExternals === 'loose'
const preferEsm = esmExternals && isEsmRequested
const resolve = getResolve(
preferEsm ? ESM_RESOLVE_OPTIONS : CJS_RESOLVE_OPTIONS
const { res } = await resolveExternal(
this.appDir,
this.esmExternals,
context,
request,
isEsmRequested,
(options) => (_: string, resRequest: string) => {
return getResolve(options)(parent, resRequest, job)
},
undefined,
undefined,
ESM_RESOLVE_OPTIONS,
CJS_RESOLVE_OPTIONS,
BASE_ESM_RESOLVE_OPTIONS,
BASE_CJS_RESOLVE_OPTIONS
)
// Resolve the import with the webpack provided context, this
// ensures we're resolving the correct version when multiple
// exist.
let res: string = ''
try {
res = await resolve(parent, request, job)
} catch (_) {}

// If resolving fails, and we can use an alternative way
// try the alternative resolving options.
if (!res && (isEsmRequested || looseEsmExternals)) {
const resolveAlternative = getResolve(
preferEsm ? CJS_RESOLVE_OPTIONS : ESM_RESOLVE_OPTIONS
)
res = await resolveAlternative(parent, request, job)
}

if (!res) {
throw new Error(`failed to resolve ${request} from ${parent}`)
Expand Down
16 changes: 14 additions & 2 deletions test/integration/production/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,12 +217,24 @@ describe('Production Usage', () => {
expect(version).toBe(1)

expect(
check.tests.every((item) => files.some((file) => item.test(file)))
check.tests.every((item) => {
if (files.some((file) => item.test(file))) {
return true
}
console.error(`Failed to find ${item} in`, files)
return false
})
).toBe(true)

if (sep === '/') {
expect(
check.notTests.some((item) => files.some((file) => item.test(file)))
check.notTests.some((item) => {
if (files.some((file) => item.test(file))) {
console.error(`Found unexpected ${item} in`, files)
return true
}
return false
})
).toBe(false)
}
}
Expand Down