83347b3efa
<!-- Thanks for opening a PR! Your contribution is much appreciated. To make sure your PR is handled as smoothly as possible we request that you follow the checklist sections below. Choose the right checklist for the change that you're making: --> ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
831 lines
29 KiB
TypeScript
831 lines
29 KiB
TypeScript
import nodePath from 'path'
|
|
import nodeFs from 'fs'
|
|
import { Span } from '../../../trace'
|
|
import { spans } from './profiling-plugin'
|
|
import isError from '../../../lib/is-error'
|
|
import {
|
|
nodeFileTrace,
|
|
NodeFileTraceReasons,
|
|
} from 'next/dist/compiled/@vercel/nft'
|
|
import { TRACE_OUTPUT_VERSION } from '../../../shared/lib/constants'
|
|
import { webpack, sources } from 'next/dist/compiled/webpack/webpack'
|
|
import {
|
|
NODE_ESM_RESOLVE_OPTIONS,
|
|
NODE_RESOLVE_OPTIONS,
|
|
resolveExternal,
|
|
} from '../../webpack-config'
|
|
import { NextConfigComplete } from '../../../server/config-shared'
|
|
import { loadBindings } from '../../swc'
|
|
|
|
const PLUGIN_NAME = 'TraceEntryPointsPlugin'
|
|
const TRACE_IGNORES = [
|
|
'**/*/next/dist/server/next.js',
|
|
'**/*/next/dist/bin/next',
|
|
]
|
|
|
|
const TURBO_TRACE_DEFAULT_MAX_FILES = 128
|
|
|
|
function getModuleFromDependency(
|
|
compilation: any,
|
|
dep: any
|
|
): webpack.Module & { resource?: string; request?: string } {
|
|
return compilation.moduleGraph.getModule(dep)
|
|
}
|
|
|
|
function getFilesMapFromReasons(
|
|
fileList: Set<string>,
|
|
reasons: NodeFileTraceReasons,
|
|
ignoreFn?: (file: string, parent?: string) => Boolean
|
|
) {
|
|
// this uses the reasons tree to collect files specific to a
|
|
// certain parent allowing us to not have to trace each parent
|
|
// separately
|
|
const parentFilesMap = new Map<string, Set<string>>()
|
|
|
|
function propagateToParents(
|
|
parents: Set<string>,
|
|
file: string,
|
|
seen = new Set<string>()
|
|
) {
|
|
for (const parent of parents || []) {
|
|
if (!seen.has(parent)) {
|
|
seen.add(parent)
|
|
let parentFiles = parentFilesMap.get(parent)
|
|
|
|
if (!parentFiles) {
|
|
parentFiles = new Set()
|
|
parentFilesMap.set(parent, parentFiles)
|
|
}
|
|
|
|
if (!ignoreFn?.(file, parent)) {
|
|
parentFiles.add(file)
|
|
}
|
|
const parentReason = reasons.get(parent)
|
|
|
|
if (parentReason?.parents) {
|
|
propagateToParents(parentReason.parents, file, seen)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const file of fileList!) {
|
|
const reason = reasons!.get(file)
|
|
const isInitial =
|
|
reason?.type.length === 1 && reason.type.includes('initial')
|
|
|
|
if (
|
|
!reason ||
|
|
!reason.parents ||
|
|
(isInitial && reason.parents.size === 0)
|
|
) {
|
|
continue
|
|
}
|
|
propagateToParents(reason.parents, file)
|
|
}
|
|
return parentFilesMap
|
|
}
|
|
|
|
export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance {
|
|
private appDir: string
|
|
private appDirEnabled?: boolean
|
|
private tracingRoot: string
|
|
private entryTraces: Map<string, Set<string>>
|
|
private excludeFiles: string[]
|
|
private esmExternals?: NextConfigComplete['experimental']['esmExternals']
|
|
private turbotrace?: NextConfigComplete['experimental']['turbotrace']
|
|
private chunksToTrace: string[] = []
|
|
private turbotraceOutputPath?: string
|
|
private turbotraceFiles?: string[]
|
|
|
|
constructor({
|
|
appDir,
|
|
appDirEnabled,
|
|
excludeFiles,
|
|
esmExternals,
|
|
outputFileTracingRoot,
|
|
turbotrace,
|
|
}: {
|
|
appDir: string
|
|
appDirEnabled?: boolean
|
|
excludeFiles?: string[]
|
|
outputFileTracingRoot?: string
|
|
esmExternals?: NextConfigComplete['experimental']['esmExternals']
|
|
turbotrace?: NextConfigComplete['experimental']['turbotrace']
|
|
}) {
|
|
this.appDir = appDir
|
|
this.entryTraces = new Map()
|
|
this.esmExternals = esmExternals
|
|
this.appDirEnabled = appDirEnabled
|
|
this.excludeFiles = excludeFiles || []
|
|
this.tracingRoot = outputFileTracingRoot || appDir
|
|
this.turbotrace = turbotrace
|
|
}
|
|
|
|
// Here we output all traced assets and webpack chunks to a
|
|
// ${page}.js.nft.json file
|
|
async createTraceAssets(
|
|
compilation: any,
|
|
assets: any,
|
|
span: Span,
|
|
readlink: any,
|
|
stat: any
|
|
) {
|
|
const outputPath = compilation.outputOptions.path
|
|
|
|
await span.traceChild('create-trace-assets').traceAsyncFn(async () => {
|
|
const entryFilesMap = new Map<any, Set<string>>()
|
|
const chunksToTrace = new Set<string>()
|
|
const isTraceable = (file: string) => !file.endsWith('.wasm')
|
|
|
|
for (const entrypoint of compilation.entrypoints.values()) {
|
|
const entryFiles = new Set<string>()
|
|
|
|
for (const chunk of entrypoint
|
|
.getEntrypointChunk()
|
|
.getAllReferencedChunks()) {
|
|
for (const file of chunk.files) {
|
|
if (isTraceable(file)) {
|
|
const filePath = nodePath.join(outputPath, file)
|
|
chunksToTrace.add(filePath)
|
|
entryFiles.add(filePath)
|
|
}
|
|
}
|
|
for (const file of chunk.auxiliaryFiles) {
|
|
if (isTraceable(file)) {
|
|
const filePath = nodePath.join(outputPath, file)
|
|
chunksToTrace.add(filePath)
|
|
entryFiles.add(filePath)
|
|
}
|
|
}
|
|
}
|
|
entryFilesMap.set(entrypoint, entryFiles)
|
|
}
|
|
|
|
// startTrace existed and callable
|
|
if (this.turbotrace) {
|
|
let binding = (await loadBindings()) as any
|
|
if (
|
|
!binding?.isWasm &&
|
|
typeof binding.turbo.startTrace === 'function'
|
|
) {
|
|
this.chunksToTrace = [...chunksToTrace]
|
|
return
|
|
}
|
|
}
|
|
|
|
const result = await nodeFileTrace([...chunksToTrace], {
|
|
base: this.tracingRoot,
|
|
processCwd: this.appDir,
|
|
readFile: async (path) => {
|
|
if (chunksToTrace.has(path)) {
|
|
const source =
|
|
assets[
|
|
nodePath.relative(outputPath, path).replace(/\\/g, '/')
|
|
]?.source?.()
|
|
if (source) return source
|
|
}
|
|
try {
|
|
return await new Promise((resolve, reject) => {
|
|
;(
|
|
compilation.inputFileSystem
|
|
.readFile as typeof import('fs').readFile
|
|
)(path, (err, data) => {
|
|
if (err) return reject(err)
|
|
resolve(data)
|
|
})
|
|
})
|
|
} catch (e) {
|
|
if (isError(e) && (e.code === 'ENOENT' || e.code === 'EISDIR')) {
|
|
return null
|
|
}
|
|
throw e
|
|
}
|
|
},
|
|
readlink,
|
|
stat,
|
|
ignore: [...TRACE_IGNORES, ...this.excludeFiles],
|
|
mixedModules: true,
|
|
})
|
|
const reasons = result.reasons
|
|
const fileList = result.fileList
|
|
result.esmFileList.forEach((file) => fileList.add(file))
|
|
|
|
const parentFilesMap = getFilesMapFromReasons(fileList, reasons)
|
|
|
|
for (const [entrypoint, entryFiles] of entryFilesMap) {
|
|
const traceOutputName = `../${entrypoint.name}.js.nft.json`
|
|
const traceOutputPath = nodePath.dirname(
|
|
nodePath.join(outputPath, traceOutputName)
|
|
)
|
|
const allEntryFiles = new Set<string>()
|
|
|
|
entryFiles.forEach((file) => {
|
|
parentFilesMap
|
|
.get(nodePath.relative(this.tracingRoot, file))
|
|
?.forEach((child) => {
|
|
allEntryFiles.add(nodePath.join(this.tracingRoot, child))
|
|
})
|
|
})
|
|
// don't include the entry itself in the trace
|
|
entryFiles.delete(nodePath.join(outputPath, `../${entrypoint.name}.js`))
|
|
|
|
assets[traceOutputName] = new sources.RawSource(
|
|
JSON.stringify({
|
|
version: TRACE_OUTPUT_VERSION,
|
|
files: [
|
|
...new Set([
|
|
...entryFiles,
|
|
...allEntryFiles,
|
|
...(this.entryTraces.get(entrypoint.name) || []),
|
|
]),
|
|
].map((file) => {
|
|
return nodePath
|
|
.relative(traceOutputPath, file)
|
|
.replace(/\\/g, '/')
|
|
}),
|
|
})
|
|
)
|
|
}
|
|
})
|
|
}
|
|
|
|
tapfinishModules(
|
|
compilation: webpack.Compilation,
|
|
traceEntrypointsPluginSpan: Span,
|
|
doResolve: (
|
|
request: string,
|
|
parent: string,
|
|
job: import('@vercel/nft/out/node-file-trace').Job,
|
|
isEsmRequested: boolean
|
|
) => Promise<string>,
|
|
readlink: any,
|
|
stat: any
|
|
) {
|
|
compilation.hooks.finishModules.tapAsync(
|
|
PLUGIN_NAME,
|
|
async (_stats: any, callback: any) => {
|
|
const finishModulesSpan =
|
|
traceEntrypointsPluginSpan.traceChild('finish-modules')
|
|
await finishModulesSpan
|
|
.traceAsyncFn(async () => {
|
|
// we create entry -> module maps so that we can
|
|
// look them up faster instead of having to iterate
|
|
// over the compilation modules list
|
|
const entryNameMap = new Map<string, string>()
|
|
const entryModMap = new Map<string, any>()
|
|
const additionalEntries = new Map<string, Map<string, any>>()
|
|
|
|
const depModMap = new Map<string, any>()
|
|
|
|
finishModulesSpan.traceChild('get-entries').traceFn(() => {
|
|
compilation.entries.forEach((entry, name) => {
|
|
const normalizedName = name?.replace(/\\/g, '/')
|
|
|
|
const isPage = normalizedName.startsWith('pages/')
|
|
const isApp =
|
|
this.appDirEnabled && normalizedName.startsWith('app/')
|
|
|
|
if (isApp || isPage) {
|
|
for (const dep of entry.dependencies) {
|
|
if (!dep) continue
|
|
const entryMod = getModuleFromDependency(compilation, dep)
|
|
|
|
// since app entries are wrapped in next-app-loader
|
|
// we need to pull the original pagePath for
|
|
// referencing during tracing
|
|
if (isApp && entryMod.request) {
|
|
const loaderQueryIdx = entryMod.request.indexOf('?')
|
|
|
|
const loaderQuery = new URLSearchParams(
|
|
entryMod.request.substring(loaderQueryIdx)
|
|
)
|
|
const resource =
|
|
loaderQuery
|
|
.get('pagePath')
|
|
?.replace(
|
|
'private-next-app-dir',
|
|
nodePath.join(this.appDir, 'app')
|
|
) || ''
|
|
|
|
entryModMap.set(resource, entryMod)
|
|
entryNameMap.set(resource, name)
|
|
}
|
|
|
|
if (entryMod && entryMod.resource) {
|
|
const normalizedResource = entryMod.resource.replace(
|
|
/\\/g,
|
|
'/'
|
|
)
|
|
if (normalizedResource.includes('pages/')) {
|
|
entryNameMap.set(entryMod.resource, name)
|
|
entryModMap.set(entryMod.resource, entryMod)
|
|
} else {
|
|
let curMap = additionalEntries.get(name)
|
|
|
|
if (!curMap) {
|
|
curMap = new Map()
|
|
additionalEntries.set(name, curMap)
|
|
}
|
|
depModMap.set(entryMod.resource, entryMod)
|
|
curMap.set(entryMod.resource, entryMod)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
const readFile = async (
|
|
path: string
|
|
): Promise<Buffer | string | null> => {
|
|
const mod = depModMap.get(path) || entryModMap.get(path)
|
|
|
|
// map the transpiled source when available to avoid
|
|
// parse errors in node-file-trace
|
|
const source = mod?.originalSource?.()
|
|
|
|
if (source) {
|
|
return source.buffer()
|
|
}
|
|
// we don't want to analyze non-transpiled
|
|
// files here, that is done against webpack output
|
|
return ''
|
|
}
|
|
|
|
const entryPaths = Array.from(entryModMap.keys())
|
|
|
|
const collectDependencies = (mod: any) => {
|
|
if (!mod || !mod.dependencies) return
|
|
|
|
for (const dep of mod.dependencies) {
|
|
const depMod = getModuleFromDependency(compilation, dep)
|
|
|
|
if (depMod?.resource && !depModMap.get(depMod.resource)) {
|
|
depModMap.set(depMod.resource, depMod)
|
|
collectDependencies(depMod)
|
|
}
|
|
}
|
|
}
|
|
const entriesToTrace = [...entryPaths]
|
|
|
|
entryPaths.forEach((entry) => {
|
|
collectDependencies(entryModMap.get(entry))
|
|
const entryName = entryNameMap.get(entry)!
|
|
const curExtraEntries = additionalEntries.get(entryName)
|
|
|
|
if (curExtraEntries) {
|
|
entriesToTrace.push(...curExtraEntries.keys())
|
|
}
|
|
})
|
|
// startTrace existed and callable
|
|
if (this.turbotrace) {
|
|
let binding = (await loadBindings()) as any
|
|
if (
|
|
!binding?.isWasm &&
|
|
typeof binding.turbo.startTrace === 'function'
|
|
) {
|
|
await finishModulesSpan
|
|
.traceChild('turbo-trace', {
|
|
traceEntryCount: entriesToTrace.length + '',
|
|
})
|
|
.traceAsyncFn(async () => {
|
|
const contextDirectory =
|
|
this.turbotrace?.contextDirectory ?? this.tracingRoot
|
|
const maxFiles =
|
|
this.turbotrace?.maxFiles ?? TURBO_TRACE_DEFAULT_MAX_FILES
|
|
let chunks = [...entriesToTrace]
|
|
let restChunks =
|
|
chunks.length > maxFiles ? chunks.splice(maxFiles) : []
|
|
let filesTracedInEntries: string[] = []
|
|
while (chunks.length) {
|
|
filesTracedInEntries = filesTracedInEntries.concat(
|
|
await binding.turbo.startTrace({
|
|
action: 'print',
|
|
input: chunks,
|
|
contextDirectory,
|
|
processCwd:
|
|
this.turbotrace?.processCwd ?? this.appDir,
|
|
logLevel: this.turbotrace?.logLevel,
|
|
showAll: this.turbotrace?.logAll,
|
|
})
|
|
)
|
|
chunks = restChunks
|
|
if (restChunks.length) {
|
|
restChunks =
|
|
chunks.length > maxFiles
|
|
? chunks.splice(maxFiles)
|
|
: []
|
|
}
|
|
}
|
|
|
|
// only trace the assets under the appDir
|
|
// exclude files from node_modules, entries and processed by webpack
|
|
const filesTracedFromEntries = filesTracedInEntries
|
|
.map((f) => nodePath.join(contextDirectory, f))
|
|
.filter(
|
|
(f) =>
|
|
!f.includes('/node_modules/') &&
|
|
f.startsWith(this.appDir) &&
|
|
!entriesToTrace.includes(f) &&
|
|
!depModMap.has(f)
|
|
)
|
|
if (!filesTracedFromEntries.length) {
|
|
return
|
|
}
|
|
|
|
// The turbo trace doesn't provide the traced file type and reason at present
|
|
// let's write the traced files into the first [entry].nft.json
|
|
const [[, entryName]] = Array.from(
|
|
entryNameMap.entries()
|
|
).filter(([k]) => k.startsWith(this.appDir))
|
|
const outputPath = compilation.outputOptions.path!
|
|
const traceOutputPath = nodePath.join(
|
|
outputPath,
|
|
`../${entryName}.js.nft.json`
|
|
)
|
|
const traceOutputDir = nodePath.dirname(traceOutputPath)
|
|
|
|
this.turbotraceOutputPath = traceOutputPath
|
|
this.turbotraceFiles = filesTracedFromEntries.map((file) =>
|
|
nodePath.relative(traceOutputDir, file)
|
|
)
|
|
})
|
|
return
|
|
}
|
|
}
|
|
let fileList: Set<string>
|
|
let reasons: NodeFileTraceReasons
|
|
await finishModulesSpan
|
|
.traceChild('node-file-trace', {
|
|
traceEntryCount: entriesToTrace.length + '',
|
|
})
|
|
.traceAsyncFn(async () => {
|
|
const result = await nodeFileTrace(entriesToTrace, {
|
|
base: this.tracingRoot,
|
|
processCwd: this.appDir,
|
|
readFile,
|
|
readlink,
|
|
stat,
|
|
resolve: doResolve
|
|
? async (id, parent, job, isCjs) => {
|
|
return doResolve(id, parent, job, !isCjs)
|
|
}
|
|
: undefined,
|
|
ignore: [
|
|
...TRACE_IGNORES,
|
|
...this.excludeFiles,
|
|
'**/node_modules/**',
|
|
],
|
|
mixedModules: true,
|
|
})
|
|
// @ts-ignore
|
|
fileList = result.fileList
|
|
result.esmFileList.forEach((file) => fileList.add(file))
|
|
reasons = result.reasons
|
|
})
|
|
|
|
await finishModulesSpan
|
|
.traceChild('collect-traced-files')
|
|
.traceAsyncFn(() => {
|
|
const parentFilesMap = getFilesMapFromReasons(
|
|
fileList,
|
|
reasons,
|
|
(file) => {
|
|
// if a file was imported and a loader handled it
|
|
// we don't include it in the trace e.g.
|
|
// static image imports, CSS imports
|
|
file = nodePath.join(this.tracingRoot, file)
|
|
const depMod = depModMap.get(file)
|
|
const isAsset = reasons
|
|
.get(nodePath.relative(this.tracingRoot, file))
|
|
?.type.includes('asset')
|
|
|
|
return (
|
|
!isAsset &&
|
|
Array.isArray(depMod?.loaders) &&
|
|
depMod.loaders.length > 0
|
|
)
|
|
}
|
|
)
|
|
entryPaths.forEach((entry) => {
|
|
const entryName = entryNameMap.get(entry)!
|
|
const normalizedEntry = nodePath.relative(
|
|
this.tracingRoot,
|
|
entry
|
|
)
|
|
const curExtraEntries = additionalEntries.get(entryName)
|
|
const finalDeps = new Set<string>()
|
|
|
|
parentFilesMap.get(normalizedEntry)?.forEach((dep) => {
|
|
finalDeps.add(nodePath.join(this.tracingRoot, dep))
|
|
})
|
|
|
|
if (curExtraEntries) {
|
|
for (const extraEntry of curExtraEntries.keys()) {
|
|
const normalizedExtraEntry = nodePath.relative(
|
|
this.tracingRoot,
|
|
extraEntry
|
|
)
|
|
finalDeps.add(extraEntry)
|
|
parentFilesMap
|
|
.get(normalizedExtraEntry)
|
|
?.forEach((dep) => {
|
|
finalDeps.add(nodePath.join(this.tracingRoot, dep))
|
|
})
|
|
}
|
|
}
|
|
this.entryTraces.set(entryName, finalDeps)
|
|
})
|
|
})
|
|
})
|
|
.then(
|
|
() => callback(),
|
|
(err) => callback(err)
|
|
)
|
|
}
|
|
)
|
|
}
|
|
|
|
apply(compiler: webpack.Compiler) {
|
|
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
|
|
const readlink = async (path: string): Promise<string | null> => {
|
|
try {
|
|
return await new Promise((resolve, reject) => {
|
|
;(
|
|
compilation.inputFileSystem
|
|
.readlink as typeof import('fs').readlink
|
|
)(path, (err, link) => {
|
|
if (err) return reject(err)
|
|
resolve(link)
|
|
})
|
|
})
|
|
} catch (e) {
|
|
if (
|
|
isError(e) &&
|
|
(e.code === 'EINVAL' || e.code === 'ENOENT' || e.code === 'UNKNOWN')
|
|
) {
|
|
return null
|
|
}
|
|
throw e
|
|
}
|
|
}
|
|
const stat = async (path: string): Promise<import('fs').Stats | null> => {
|
|
try {
|
|
return await new Promise((resolve, reject) => {
|
|
;(compilation.inputFileSystem.stat as typeof import('fs').stat)(
|
|
path,
|
|
(err, stats) => {
|
|
if (err) return reject(err)
|
|
resolve(stats)
|
|
}
|
|
)
|
|
})
|
|
} catch (e) {
|
|
if (isError(e) && (e.code === 'ENOENT' || e.code === 'ENOTDIR')) {
|
|
return null
|
|
}
|
|
throw e
|
|
}
|
|
}
|
|
|
|
const compilationSpan = spans.get(compilation) || spans.get(compiler)!
|
|
const traceEntrypointsPluginSpan = compilationSpan.traceChild(
|
|
'next-trace-entrypoint-plugin'
|
|
)
|
|
traceEntrypointsPluginSpan.traceFn(() => {
|
|
compilation.hooks.processAssets.tapAsync(
|
|
{
|
|
name: PLUGIN_NAME,
|
|
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
|
|
},
|
|
(assets: any, callback: any) => {
|
|
this.createTraceAssets(
|
|
compilation,
|
|
assets,
|
|
traceEntrypointsPluginSpan,
|
|
readlink,
|
|
stat
|
|
)
|
|
.then(() => callback())
|
|
.catch((err) => callback(err))
|
|
}
|
|
)
|
|
|
|
let resolver = compilation.resolverFactory.get('normal')
|
|
|
|
function getPkgName(name: string) {
|
|
const segments = name.split('/')
|
|
if (name[0] === '@' && segments.length > 1)
|
|
return segments.length > 1 ? segments.slice(0, 2).join('/') : null
|
|
return segments.length ? segments[0] : null
|
|
}
|
|
|
|
const getResolve = (options: any) => {
|
|
const curResolver = resolver.withOptions(options)
|
|
|
|
return (
|
|
parent: string,
|
|
request: string,
|
|
job: import('@vercel/nft/out/node-file-trace').Job
|
|
) =>
|
|
new Promise<[string, boolean]>((resolve, reject) => {
|
|
const context = nodePath.dirname(parent)
|
|
|
|
curResolver.resolve(
|
|
{},
|
|
context,
|
|
request,
|
|
{
|
|
fileDependencies: compilation.fileDependencies,
|
|
missingDependencies: compilation.missingDependencies,
|
|
contextDependencies: compilation.contextDependencies,
|
|
},
|
|
async (err: any, result?, resContext?) => {
|
|
if (err) return reject(err)
|
|
|
|
if (!result) {
|
|
return reject(new Error('module not found'))
|
|
}
|
|
|
|
// webpack resolver doesn't strip loader query info
|
|
// from the result so use path instead
|
|
if (result.includes('?') || result.includes('!')) {
|
|
result = resContext?.path || result
|
|
}
|
|
|
|
try {
|
|
// we need to collect all parent package.json's used
|
|
// as webpack's resolve doesn't expose this and parent
|
|
// package.json could be needed for resolving e.g. stylis
|
|
// stylis/package.json -> stylis/dist/umd/package.json
|
|
if (result.includes('node_modules')) {
|
|
let requestPath = result
|
|
.replace(/\\/g, '/')
|
|
.replace(/\0/g, '')
|
|
|
|
if (
|
|
!nodePath.isAbsolute(request) &&
|
|
request.includes('/') &&
|
|
resContext?.descriptionFileRoot
|
|
) {
|
|
requestPath = (
|
|
resContext.descriptionFileRoot +
|
|
request.slice(getPkgName(request)?.length || 0) +
|
|
nodePath.sep +
|
|
'package.json'
|
|
)
|
|
.replace(/\\/g, '/')
|
|
.replace(/\0/g, '')
|
|
}
|
|
|
|
const rootSeparatorIndex = requestPath.indexOf('/')
|
|
let separatorIndex: number
|
|
while (
|
|
(separatorIndex = requestPath.lastIndexOf('/')) >
|
|
rootSeparatorIndex
|
|
) {
|
|
requestPath = requestPath.slice(0, separatorIndex)
|
|
const curPackageJsonPath = `${requestPath}/package.json`
|
|
if (await job.isFile(curPackageJsonPath)) {
|
|
await job.emitFile(
|
|
await job.realpath(curPackageJsonPath),
|
|
'resolve',
|
|
parent
|
|
)
|
|
}
|
|
}
|
|
}
|
|
} catch (_err) {
|
|
// we failed to resolve the package.json boundary,
|
|
// we don't block emitting the initial asset from this
|
|
}
|
|
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,
|
|
parent: string,
|
|
job: import('@vercel/nft/out/node-file-trace').Job,
|
|
isEsmRequested: boolean
|
|
): Promise<string> => {
|
|
const context = nodePath.dirname(parent)
|
|
// When in esm externals mode, and using import, we resolve with
|
|
// ESM resolving options.
|
|
const { res } = await resolveExternal(
|
|
this.appDir,
|
|
this.esmExternals,
|
|
context,
|
|
request,
|
|
isEsmRequested,
|
|
!!this.appDirEnabled,
|
|
(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
|
|
)
|
|
|
|
if (!res) {
|
|
throw new Error(`failed to resolve ${request} from ${parent}`)
|
|
}
|
|
return res.replace(/\0/g, '')
|
|
}
|
|
|
|
this.tapfinishModules(
|
|
compilation,
|
|
traceEntrypointsPluginSpan,
|
|
doResolve,
|
|
readlink,
|
|
stat
|
|
)
|
|
})
|
|
})
|
|
|
|
if (this.turbotrace) {
|
|
compiler.hooks.afterEmit.tapPromise(PLUGIN_NAME, async (compilation) => {
|
|
const compilationSpan = spans.get(compilation) || spans.get(compiler)!
|
|
const traceEntrypointsPluginSpan = compilationSpan.traceChild(
|
|
'next-trace-entrypoint-plugin'
|
|
)
|
|
const turbotraceAfterEmitSpan = traceEntrypointsPluginSpan.traceChild(
|
|
'after-emit-turbo-trace'
|
|
)
|
|
await turbotraceAfterEmitSpan.traceAsyncFn(async () => {
|
|
let binding = (await loadBindings()) as any
|
|
if (
|
|
!binding?.isWasm &&
|
|
typeof binding.turbo.startTrace === 'function'
|
|
) {
|
|
const maxFiles =
|
|
this.turbotrace?.maxFiles ?? TURBO_TRACE_DEFAULT_MAX_FILES
|
|
let chunks = [...this.chunksToTrace]
|
|
let restChunks =
|
|
chunks.length > maxFiles ? chunks.splice(maxFiles) : []
|
|
while (chunks.length) {
|
|
await binding.turbo.startTrace({
|
|
action: 'annotate',
|
|
input: chunks,
|
|
contextDirectory:
|
|
this.turbotrace?.contextDirectory ?? this.tracingRoot,
|
|
processCwd: this.turbotrace?.processCwd ?? this.appDir,
|
|
showAll: this.turbotrace?.logAll,
|
|
logLevel: this.turbotrace?.logLevel,
|
|
})
|
|
chunks = restChunks
|
|
if (restChunks.length) {
|
|
restChunks =
|
|
chunks.length > maxFiles ? chunks.splice(maxFiles) : []
|
|
}
|
|
}
|
|
if (this.turbotraceOutputPath && this.turbotraceFiles) {
|
|
const existedNftFile = await nodeFs.promises
|
|
.readFile(this.turbotraceOutputPath, 'utf8')
|
|
.then((content) => JSON.parse(content))
|
|
.catch(() => ({
|
|
version: TRACE_OUTPUT_VERSION,
|
|
files: [],
|
|
}))
|
|
console.log(this.turbotraceOutputPath, this.turbotraceFiles)
|
|
existedNftFile.files.push(...this.turbotraceFiles)
|
|
const filesSet = new Set(existedNftFile.files)
|
|
existedNftFile.files = [...filesSet]
|
|
nodeFs.promises.writeFile(
|
|
this.turbotraceOutputPath,
|
|
JSON.stringify(existedNftFile)
|
|
)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
}
|