rsnext/packages/next/build/webpack/plugins/next-trace-entrypoints-plugin.ts
JJ Kasper 24b09ad4f8
Add entrypoint tracing (#25538)
This adds tracing entrypoints directly after they have have been transpiled to allow us to trace before the webpack runtime has been added to the modules. This should allow for more accurate tracing of entrypoints and allow the trace step to be cached. 

## Bug

- [x] Related issues linked using `fixes #number`
- [x] Integration tests added


x-ref: https://github.com/vercel/next.js/issues/24700
x-ref: https://github.com/vercel/next.js/issues/26200
x-ref: https://github.com/vercel/next.js/issues/23894
x-ref: https://github.com/vercel/next.js/issues/25431
2021-08-16 19:29:11 +00:00

261 lines
8.2 KiB
TypeScript

import nodePath from 'path'
import { nodeFileTrace } from 'next/dist/compiled/@vercel/nft'
import {
webpack,
isWebpack5,
sources,
} from 'next/dist/compiled/webpack/webpack'
import { TRACE_OUTPUT_VERSION } from '../../../shared/lib/constants'
const PLUGIN_NAME = 'TraceEntryPointsPlugin'
const TRACE_IGNORES = [
'**/*/node_modules/react/**/*.development.js',
'**/*/node_modules/react-dom/**/*.development.js',
]
function getModuleFromDependency(
compilation: any,
dep: any
): webpack.Module & { resource?: string } {
if (isWebpack5) {
return compilation.moduleGraph.getModule(dep)
}
return dep.module
}
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
createTraceAssets(compilation: any, assets: any) {
const outputPath = compilation.outputOptions.path
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) {
entryFiles.add(nodePath.join(outputPath, file))
}
for (const file of chunk.auxiliaryFiles) {
entryFiles.add(nodePath.join(outputPath, file))
}
}
// don't include the entry itself in the trace
entryFiles.delete(
nodePath.join(
outputPath,
`${isWebpack5 ? '../' : ''}${entrypoint.name}.js`
)
)
const traceOutputName = `${isWebpack5 ? '../' : ''}${
entrypoint.name
}.js.nft.json`
const traceOutputPath = nodePath.join(outputPath, traceOutputName)
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, '/')
}
),
})
)
}
}
apply(compiler: webpack.Compiler) {
if (isWebpack5) {
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
// @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)
}
)
})
} else {
compiler.hooks.emit.tap(PLUGIN_NAME, (compilation: any) => {
this.createTraceAssets(compilation, compilation.assets)
})
}
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
compilation.hooks.finishModules.tapAsync(
PLUGIN_NAME,
async (_stats: any, callback: any) => {
// 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>()
try {
const depModMap = new Map<string, any>()
compilation.entries.forEach((entry) => {
const name = entry.name || entry.options?.name
if (name?.startsWith('pages/') && entry.dependencies[0]) {
const entryMod = getModuleFromDependency(
compilation,
entry.dependencies[0]
)
if (entryMod.resource) {
entryNameMap.set(entryMod.resource, name)
entryModMap.set(entryMod.resource, entryMod)
}
}
})
// TODO: investigate allowing non-sync fs calls in node-file-trace
// for better performance
const readFile = (path: string) => {
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 {
return compilation.inputFileSystem.readFileSync(path)
} catch (e) {
if (e.code === 'ENOENT' || e.code === 'EISDIR') {
return null
}
throw e
}
}
const readlink = (path: string) => {
try {
return compilation.inputFileSystem.readlinkSync(path)
} catch (e) {
if (
e.code !== 'EINVAL' &&
e.code !== 'ENOENT' &&
e.code !== 'UNKNOWN'
) {
throw e
}
return null
}
}
const stat = (path: string) => {
try {
return compilation.inputFileSystem.statSync(path)
} catch (e) {
if (e.code === 'ENOENT') {
return null
}
throw e
}
}
const nftCache = {}
const entryPaths = Array.from(entryModMap.keys())
for (const entry of entryPaths) {
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 (
// isWebpack5 &&
// cachedTraces &&
// cachedTraces.version === TRACE_OUTPUT_VERSION
// ) {
// this.entryTraces.set(
// entryNameMap.get(entry)!,
// cachedTraces.tracedDeps
// )
// continue
// }
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)
}
}
}
collectDependencies(entryMod)
const toTrace: string[] = [entry, ...depModMap.keys()]
const root = nodePath.parse(process.cwd()).root
const result = await nodeFileTrace(toTrace, {
base: root,
cache: nftCache,
processCwd: this.appDir,
readFile,
readlink,
stat,
ignore: [...TRACE_IGNORES, ...this.excludeFiles],
})
const tracedDeps: string[] = []
for (const file of result.fileList) {
if (result.reasons[file].type === 'initial') {
continue
}
tracedDeps.push(nodePath.join(root, file))
}
// entryMod.buildInfo.cachedNextEntryTrace = {
// version: TRACE_OUTPUT_VERSION,
// tracedDeps,
// }
this.entryTraces.set(entryNameMap.get(entry)!, tracedDeps)
}
callback()
} catch (err) {
callback(err)
}
}
)
})
}
}