2021-08-16 21:29:11 +02:00
|
|
|
import nodePath from 'path'
|
2021-09-23 23:22:14 +02:00
|
|
|
import { Span } from '../../../trace'
|
|
|
|
import { spans } from './profiling-plugin'
|
|
|
|
import isError from '../../../lib/is-error'
|
2021-08-16 21:29:11 +02:00
|
|
|
import { nodeFileTrace } from 'next/dist/compiled/@vercel/nft'
|
2021-09-23 23:22:14 +02:00
|
|
|
import { TRACE_OUTPUT_VERSION } from '../../../shared/lib/constants'
|
2021-10-07 01:46:46 +02:00
|
|
|
import { webpack, sources } from 'next/dist/compiled/webpack/webpack'
|
2021-09-30 19:03:42 +02:00
|
|
|
import { NODE_RESOLVE_OPTIONS } from '../../webpack-config'
|
2021-08-16 21:29:11 +02:00
|
|
|
|
|
|
|
const PLUGIN_NAME = 'TraceEntryPointsPlugin'
|
|
|
|
const TRACE_IGNORES = [
|
|
|
|
'**/*/node_modules/react/**/*.development.js',
|
|
|
|
'**/*/node_modules/react-dom/**/*.development.js',
|
2021-09-15 22:00:52 +02:00
|
|
|
'**/*/next/dist/server/next.js',
|
|
|
|
'**/*/next/dist/bin/next',
|
2021-08-16 21:29:11 +02:00
|
|
|
]
|
|
|
|
|
|
|
|
function getModuleFromDependency(
|
|
|
|
compilation: any,
|
|
|
|
dep: any
|
|
|
|
): webpack.Module & { resource?: string } {
|
2021-10-07 01:46:46 +02:00
|
|
|
return compilation.moduleGraph.getModule(dep)
|
2021-08-16 21:29:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export class TraceEntryPointsPlugin implements webpack.Plugin {
|
|
|
|
private appDir: string
|
|
|
|
private entryTraces: Map<string, string[]>
|
|
|
|
private excludeFiles: string[]
|
|
|
|
|
|
|
|
constructor({
|
|
|
|
appDir,
|
|
|
|
excludeFiles,
|
|
|
|
}: {
|
|
|
|
appDir: string
|
|
|
|
excludeFiles?: string[]
|
|
|
|
}) {
|
|
|
|
this.appDir = appDir
|
|
|
|
this.entryTraces = new Map()
|
|
|
|
this.excludeFiles = excludeFiles || []
|
|
|
|
}
|
|
|
|
|
|
|
|
// Here we output all traced assets and webpack chunks to a
|
|
|
|
// ${page}.js.nft.json file
|
2021-09-14 18:13:11 +02:00
|
|
|
createTraceAssets(compilation: any, assets: any, span: Span) {
|
2021-08-16 21:29:11 +02:00
|
|
|
const outputPath = compilation.outputOptions.path
|
|
|
|
|
2021-09-14 18:13:11 +02:00
|
|
|
const nodeFileTraceSpan = span.traceChild('create-trace-assets')
|
|
|
|
nodeFileTraceSpan.traceFn(() => {
|
|
|
|
for (const entrypoint of compilation.entrypoints.values()) {
|
|
|
|
const entryFiles = new Set<string>()
|
2021-08-16 21:29:11 +02:00
|
|
|
|
2021-09-14 18:13:11 +02:00
|
|
|
for (const chunk of entrypoint
|
|
|
|
.getEntrypointChunk()
|
|
|
|
.getAllReferencedChunks()) {
|
|
|
|
for (const file of chunk.files) {
|
|
|
|
entryFiles.add(nodePath.join(outputPath, file))
|
|
|
|
}
|
|
|
|
for (const file of chunk.auxiliaryFiles) {
|
|
|
|
entryFiles.add(nodePath.join(outputPath, file))
|
|
|
|
}
|
2021-08-16 21:29:11 +02:00
|
|
|
}
|
2021-09-14 18:13:11 +02:00
|
|
|
// don't include the entry itself in the trace
|
2021-10-07 01:46:46 +02:00
|
|
|
entryFiles.delete(nodePath.join(outputPath, `../${entrypoint.name}.js`))
|
|
|
|
const traceOutputName = `../${entrypoint.name}.js.nft.json`
|
2021-09-14 18:13:11 +02:00
|
|
|
const traceOutputPath = nodePath.dirname(
|
|
|
|
nodePath.join(outputPath, traceOutputName)
|
2021-08-16 21:29:11 +02:00
|
|
|
)
|
|
|
|
|
2021-09-14 18:13:11 +02:00
|
|
|
assets[traceOutputName] = new sources.RawSource(
|
|
|
|
JSON.stringify({
|
|
|
|
version: TRACE_OUTPUT_VERSION,
|
|
|
|
files: [
|
|
|
|
...entryFiles,
|
|
|
|
...(this.entryTraces.get(entrypoint.name) || []),
|
|
|
|
].map((file) => {
|
|
|
|
return nodePath
|
|
|
|
.relative(traceOutputPath, file)
|
|
|
|
.replace(/\\/g, '/')
|
|
|
|
}),
|
|
|
|
})
|
2021-08-16 21:29:11 +02:00
|
|
|
)
|
2021-09-14 18:13:11 +02:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
tapfinishModules(
|
|
|
|
compilation: webpack.compilation.Compilation,
|
2021-09-24 15:39:48 +02:00
|
|
|
traceEntrypointsPluginSpan: Span,
|
|
|
|
doResolve?: (
|
|
|
|
request: string,
|
2021-10-01 12:45:10 +02:00
|
|
|
parent: string,
|
|
|
|
job: import('@vercel/nft/out/node-file-trace').Job
|
2021-09-24 15:39:48 +02:00
|
|
|
) => Promise<string>
|
2021-09-14 18:13:11 +02:00
|
|
|
) {
|
|
|
|
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>>()
|
2021-08-16 21:29:11 +02:00
|
|
|
|
|
|
|
const depModMap = new Map<string, any>()
|
|
|
|
|
2021-09-14 18:13:11 +02:00
|
|
|
finishModulesSpan.traceChild('get-entries').traceFn(() => {
|
|
|
|
compilation.entries.forEach((entry) => {
|
|
|
|
const name = entry.name || entry.options?.name
|
|
|
|
|
|
|
|
if (name?.replace(/\\/g, '/').startsWith('pages/')) {
|
|
|
|
for (const dep of entry.dependencies) {
|
|
|
|
if (!dep) continue
|
|
|
|
const entryMod = getModuleFromDependency(compilation, dep)
|
|
|
|
|
|
|
|
if (entryMod && entryMod.resource) {
|
|
|
|
if (
|
|
|
|
entryMod.resource.replace(/\\/g, '/').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)
|
|
|
|
}
|
|
|
|
curMap.set(entryMod.resource, entryMod)
|
2021-09-01 17:56:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-08-16 21:29:11 +02:00
|
|
|
}
|
2021-09-14 18:13:11 +02:00
|
|
|
})
|
2021-08-16 21:29:11 +02:00
|
|
|
})
|
|
|
|
|
2021-09-23 23:22:14 +02:00
|
|
|
const readFile = async (
|
|
|
|
path: string
|
|
|
|
): Promise<Buffer | string | null> => {
|
2021-08-16 21:29:11 +02:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2021-09-23 23:22:14 +02:00
|
|
|
return await new Promise((resolve, reject) => {
|
|
|
|
;(
|
|
|
|
compilation.inputFileSystem
|
|
|
|
.readFile as typeof import('fs').readFile
|
|
|
|
)(path, (err, data) => {
|
|
|
|
if (err) return reject(err)
|
|
|
|
resolve(data)
|
|
|
|
})
|
|
|
|
})
|
2021-08-16 21:29:11 +02:00
|
|
|
} catch (e) {
|
2021-09-16 18:06:57 +02:00
|
|
|
if (
|
|
|
|
isError(e) &&
|
|
|
|
(e.code === 'ENOENT' || e.code === 'EISDIR')
|
|
|
|
) {
|
2021-08-16 21:29:11 +02:00
|
|
|
return null
|
|
|
|
}
|
|
|
|
throw e
|
|
|
|
}
|
|
|
|
}
|
2021-09-23 23:22:14 +02:00
|
|
|
const readlink = async (path: string): Promise<string | null> => {
|
2021-08-16 21:29:11 +02:00
|
|
|
try {
|
2021-09-23 23:22:14 +02:00
|
|
|
return await new Promise((resolve, reject) => {
|
|
|
|
;(
|
|
|
|
compilation.inputFileSystem
|
|
|
|
.readlink as typeof import('fs').readlink
|
|
|
|
)(path, (err, link) => {
|
|
|
|
if (err) return reject(err)
|
|
|
|
resolve(link)
|
|
|
|
})
|
|
|
|
})
|
2021-08-16 21:29:11 +02:00
|
|
|
} catch (e) {
|
|
|
|
if (
|
2021-09-16 18:06:57 +02:00
|
|
|
isError(e) &&
|
|
|
|
(e.code === 'EINVAL' ||
|
|
|
|
e.code === 'ENOENT' ||
|
|
|
|
e.code === 'UNKNOWN')
|
2021-08-16 21:29:11 +02:00
|
|
|
) {
|
2021-09-16 18:06:57 +02:00
|
|
|
return null
|
2021-08-16 21:29:11 +02:00
|
|
|
}
|
2021-09-16 18:06:57 +02:00
|
|
|
throw e
|
2021-08-16 21:29:11 +02:00
|
|
|
}
|
|
|
|
}
|
2021-09-23 23:22:14 +02:00
|
|
|
const stat = async (
|
|
|
|
path: string
|
|
|
|
): Promise<import('fs').Stats | null> => {
|
2021-08-16 21:29:11 +02:00
|
|
|
try {
|
2021-09-23 23:22:14 +02:00
|
|
|
return await new Promise((resolve, reject) => {
|
|
|
|
;(
|
|
|
|
compilation.inputFileSystem.stat as typeof import('fs').stat
|
|
|
|
)(path, (err, stats) => {
|
|
|
|
if (err) return reject(err)
|
|
|
|
resolve(stats)
|
|
|
|
})
|
|
|
|
})
|
2021-08-16 21:29:11 +02:00
|
|
|
} catch (e) {
|
2021-10-01 12:45:10 +02:00
|
|
|
if (
|
|
|
|
isError(e) &&
|
|
|
|
(e.code === 'ENOENT' || e.code === 'ENOTDIR')
|
|
|
|
) {
|
2021-08-16 21:29:11 +02:00
|
|
|
return null
|
|
|
|
}
|
|
|
|
throw e
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const nftCache = {}
|
|
|
|
const entryPaths = Array.from(entryModMap.keys())
|
|
|
|
|
|
|
|
for (const entry of entryPaths) {
|
2021-09-14 18:13:11 +02:00
|
|
|
const entrySpan = finishModulesSpan.traceChild('entry', { entry })
|
|
|
|
await entrySpan.traceAsyncFn(async () => {
|
|
|
|
depModMap.clear()
|
|
|
|
const entryMod = entryModMap.get(entry)
|
|
|
|
// TODO: investigate caching, will require ensuring no traced
|
|
|
|
// files in the cache have changed, we could potentially hash
|
|
|
|
// all traced files and only leverage the cache if the hashes
|
|
|
|
// match
|
|
|
|
// const cachedTraces = entryMod.buildInfo?.cachedNextEntryTrace
|
|
|
|
|
|
|
|
// Use cached trace if available and trace version matches
|
|
|
|
// if (
|
|
|
|
// cachedTraces &&
|
|
|
|
// cachedTraces.version === TRACE_OUTPUT_VERSION
|
|
|
|
// ) {
|
|
|
|
// this.entryTraces.set(
|
|
|
|
// entryNameMap.get(entry)!,
|
|
|
|
// cachedTraces.tracedDeps
|
|
|
|
// )
|
|
|
|
// continue
|
|
|
|
// }
|
2021-09-21 19:10:13 +02:00
|
|
|
const collectDependencies = (mod: any) => {
|
|
|
|
if (!mod || !mod.dependencies) return
|
2021-09-14 18:13:11 +02:00
|
|
|
|
2021-09-21 19:10:13 +02:00
|
|
|
for (const dep of mod.dependencies) {
|
|
|
|
const depMod = getModuleFromDependency(compilation, dep)
|
2021-09-14 18:13:11 +02:00
|
|
|
|
2021-09-21 19:10:13 +02:00
|
|
|
if (depMod?.resource && !depModMap.get(depMod.resource)) {
|
|
|
|
depModMap.set(depMod.resource, depMod)
|
|
|
|
collectDependencies(depMod)
|
2021-09-14 18:13:11 +02:00
|
|
|
}
|
2021-09-21 19:10:13 +02:00
|
|
|
}
|
2021-08-16 21:29:11 +02:00
|
|
|
}
|
2021-09-21 19:10:13 +02:00
|
|
|
collectDependencies(entryMod)
|
2021-08-16 21:29:11 +02:00
|
|
|
|
2021-09-28 19:04:16 +02:00
|
|
|
const toTrace: string[] = [entry]
|
2021-08-16 21:29:11 +02:00
|
|
|
|
2021-09-14 18:13:11 +02:00
|
|
|
const entryName = entryNameMap.get(entry)!
|
|
|
|
const curExtraEntries = additionalEntries.get(entryName)
|
2021-09-01 17:56:04 +02:00
|
|
|
|
2021-09-14 18:13:11 +02:00
|
|
|
if (curExtraEntries) {
|
|
|
|
toTrace.push(...curExtraEntries.keys())
|
|
|
|
}
|
2021-08-16 21:29:11 +02:00
|
|
|
|
2021-09-14 18:13:11 +02:00
|
|
|
const root = nodePath.parse(process.cwd()).root
|
|
|
|
const fileTraceSpan = entrySpan.traceChild('node-file-trace')
|
|
|
|
const result = await fileTraceSpan.traceAsyncFn(() =>
|
|
|
|
nodeFileTrace(toTrace, {
|
|
|
|
base: root,
|
|
|
|
cache: nftCache,
|
|
|
|
processCwd: this.appDir,
|
2021-09-23 23:22:14 +02:00
|
|
|
readFile,
|
|
|
|
readlink,
|
|
|
|
stat,
|
2021-09-24 15:39:48 +02:00
|
|
|
resolve: doResolve
|
2021-10-01 12:45:10 +02:00
|
|
|
? (id, parent, job, _isCjs) => doResolve(id, parent, job)
|
2021-09-24 15:39:48 +02:00
|
|
|
: undefined,
|
2021-09-14 18:13:11 +02:00
|
|
|
ignore: [...TRACE_IGNORES, ...this.excludeFiles],
|
|
|
|
mixedModules: true,
|
|
|
|
})
|
|
|
|
)
|
|
|
|
|
|
|
|
const tracedDeps: string[] = []
|
|
|
|
|
|
|
|
for (const file of result.fileList) {
|
2021-09-29 19:38:21 +02:00
|
|
|
// don't include the entry itself
|
2021-09-14 18:13:11 +02:00
|
|
|
if (result.reasons[file].type === 'initial') {
|
|
|
|
continue
|
|
|
|
}
|
2021-09-29 19:38:21 +02:00
|
|
|
const filepath = nodePath.join(root, file)
|
|
|
|
tracedDeps.push(filepath)
|
2021-08-16 21:29:11 +02:00
|
|
|
}
|
|
|
|
|
2021-09-14 18:13:11 +02:00
|
|
|
// entryMod.buildInfo.cachedNextEntryTrace = {
|
|
|
|
// version: TRACE_OUTPUT_VERSION,
|
|
|
|
// tracedDeps,
|
|
|
|
// }
|
|
|
|
this.entryTraces.set(entryName, tracedDeps)
|
|
|
|
})
|
2021-08-16 21:29:11 +02:00
|
|
|
}
|
2021-09-14 18:13:11 +02:00
|
|
|
})
|
|
|
|
.then(
|
|
|
|
() => callback(),
|
|
|
|
(err) => callback(err)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
2021-08-16 21:29:11 +02:00
|
|
|
|
2021-09-14 18:13:11 +02:00
|
|
|
apply(compiler: webpack.Compiler) {
|
2021-10-07 01:46:46 +02:00
|
|
|
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
|
|
|
|
const compilationSpan = spans.get(compilation) || spans.get(compiler)!
|
|
|
|
const traceEntrypointsPluginSpan = compilationSpan.traceChild(
|
|
|
|
'next-trace-entrypoint-plugin'
|
|
|
|
)
|
|
|
|
traceEntrypointsPluginSpan.traceFn(() => {
|
|
|
|
// @ts-ignore TODO: Remove ignore when webpack 5 is stable
|
|
|
|
compilation.hooks.processAssets.tap(
|
|
|
|
{
|
|
|
|
name: PLUGIN_NAME,
|
|
|
|
// @ts-ignore TODO: Remove ignore when webpack 5 is stable
|
|
|
|
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
|
|
|
|
},
|
|
|
|
(assets: any) => {
|
|
|
|
this.createTraceAssets(
|
|
|
|
compilation,
|
|
|
|
assets,
|
|
|
|
traceEntrypointsPluginSpan
|
|
|
|
)
|
|
|
|
}
|
2021-09-14 18:13:11 +02:00
|
|
|
)
|
2021-10-07 01:46:46 +02:00
|
|
|
let resolver = compilation.resolverFactory.get('normal')
|
2021-09-24 15:39:48 +02:00
|
|
|
|
2021-10-07 01:46:46 +02:00
|
|
|
resolver = resolver.withOptions({
|
|
|
|
...NODE_RESOLVE_OPTIONS,
|
|
|
|
extensions: undefined,
|
|
|
|
})
|
2021-10-01 12:45:10 +02:00
|
|
|
|
2021-10-07 01:46:46 +02:00
|
|
|
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
|
|
|
|
}
|
2021-10-01 12:45:10 +02:00
|
|
|
|
2021-10-07 01:46:46 +02:00
|
|
|
const doResolve = async (
|
|
|
|
request: string,
|
|
|
|
parent: string,
|
|
|
|
job: import('@vercel/nft/out/node-file-trace').Job
|
|
|
|
): Promise<string> => {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
resolver.resolve(
|
|
|
|
{},
|
|
|
|
nodePath.dirname(parent),
|
|
|
|
request,
|
|
|
|
{
|
|
|
|
fileDependencies: compilation.fileDependencies,
|
|
|
|
missingDependencies: compilation.missingDependencies,
|
|
|
|
contextDependencies: compilation.contextDependencies,
|
|
|
|
},
|
|
|
|
async (err: any, result: string, context: any) => {
|
|
|
|
if (err) return reject(err)
|
|
|
|
|
|
|
|
if (!result) {
|
|
|
|
return reject(new Error('module not found'))
|
|
|
|
}
|
2021-10-01 12:45:10 +02:00
|
|
|
|
2021-10-07 01:46:46 +02:00
|
|
|
try {
|
|
|
|
if (result.includes('node_modules')) {
|
|
|
|
let requestPath = result
|
|
|
|
|
|
|
|
if (
|
|
|
|
!nodePath.isAbsolute(request) &&
|
|
|
|
request.includes('/') &&
|
|
|
|
context?.descriptionFileRoot
|
|
|
|
) {
|
|
|
|
requestPath =
|
|
|
|
context.descriptionFileRoot +
|
|
|
|
request.substr(getPkgName(request)?.length || 0) +
|
|
|
|
nodePath.sep +
|
|
|
|
'package.json'
|
|
|
|
}
|
2021-10-01 12:45:10 +02:00
|
|
|
|
2021-10-07 01:46:46 +02:00
|
|
|
// the descriptionFileRoot is not set to the last used
|
|
|
|
// package.json so we use nft's resolving for this
|
|
|
|
// see test/integration/build-trace-extra-entries/app/node_modules/nested-structure for example
|
|
|
|
const packageJsonResult = await job.getPjsonBoundary(
|
|
|
|
requestPath
|
|
|
|
)
|
|
|
|
|
|
|
|
if (packageJsonResult) {
|
|
|
|
await job.emitFile(
|
|
|
|
packageJsonResult + nodePath.sep + 'package.json',
|
|
|
|
'resolve',
|
|
|
|
parent
|
2021-10-01 12:45:10 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
2021-10-07 01:46:46 +02:00
|
|
|
} catch (_err) {
|
|
|
|
// we failed to resolve the package.json boundary,
|
|
|
|
// we don't block emitting the initial asset from this
|
2021-09-29 19:38:21 +02:00
|
|
|
}
|
2021-10-07 01:46:46 +02:00
|
|
|
resolve(result)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
})
|
|
|
|
}
|
2021-09-14 18:13:11 +02:00
|
|
|
|
2021-10-07 01:46:46 +02:00
|
|
|
this.tapfinishModules(
|
|
|
|
compilation,
|
|
|
|
traceEntrypointsPluginSpan,
|
|
|
|
doResolve
|
2021-09-14 18:13:11 +02:00
|
|
|
)
|
|
|
|
})
|
2021-10-07 01:46:46 +02:00
|
|
|
})
|
2021-08-16 21:29:11 +02:00
|
|
|
}
|
|
|
|
}
|