Skip to content

Commit 599081a

Browse files
authoredOct 29, 2021
Update output tracing to do separate passes (#30637)
* Update output tracing to do separate passes * fix windows backslashes
1 parent f363cc8 commit 599081a

File tree

1 file changed

+194
-125
lines changed

1 file changed

+194
-125
lines changed
 

‎packages/next/build/webpack/plugins/next-trace-entrypoints-plugin.ts

+194-125
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const TRACE_IGNORES = [
2424
'**/*/next/dist/server/next.js',
2525
'**/*/next/dist/bin/next',
2626
]
27+
const root = nodePath.parse(process.cwd()).root
2728

2829
function getModuleFromDependency(
2930
compilation: any,
@@ -32,6 +33,54 @@ function getModuleFromDependency(
3233
return compilation.moduleGraph.getModule(dep)
3334
}
3435

36+
function getFilesMapFromReasons(
37+
fileList: Set<string>,
38+
reasons: NodeFileTraceReasons
39+
) {
40+
// this uses the reasons tree to collect files specific to a
41+
// certain parent allowing us to not have to trace each parent
42+
// separately
43+
const parentFilesMap = new Map<string, Set<string>>()
44+
45+
function propagateToParents(
46+
parents: Set<string>,
47+
file: string,
48+
seen = new Set<string>()
49+
) {
50+
for (const parent of parents || []) {
51+
if (!seen.has(parent)) {
52+
seen.add(parent)
53+
let parentFiles = parentFilesMap.get(parent)
54+
55+
if (!parentFiles) {
56+
parentFiles = new Set()
57+
parentFilesMap.set(parent, parentFiles)
58+
}
59+
parentFiles.add(file)
60+
const parentReason = reasons.get(parent)
61+
62+
if (parentReason?.parents) {
63+
propagateToParents(parentReason.parents, file, seen)
64+
}
65+
}
66+
}
67+
}
68+
69+
for (const file of fileList!) {
70+
const reason = reasons!.get(file)
71+
72+
if (
73+
!reason ||
74+
!reason.parents ||
75+
(reason.type === 'initial' && reason.parents.size === 0)
76+
) {
77+
continue
78+
}
79+
propagateToParents(reason.parents, file)
80+
}
81+
return parentFilesMap
82+
}
83+
3584
export class TraceEntryPointsPlugin implements webpack5.WebpackPluginInstance {
3685
private appDir: string
3786
private entryTraces: Map<string, Set<string>>
@@ -59,36 +108,107 @@ export class TraceEntryPointsPlugin implements webpack5.WebpackPluginInstance {
59108

60109
// Here we output all traced assets and webpack chunks to a
61110
// ${page}.js.nft.json file
62-
createTraceAssets(compilation: any, assets: any, span: Span) {
111+
async createTraceAssets(
112+
compilation: any,
113+
assets: any,
114+
span: Span,
115+
readlink: any,
116+
stat: any,
117+
doResolve: any
118+
) {
63119
const outputPath = compilation.outputOptions.path
64120

65-
const nodeFileTraceSpan = span.traceChild('create-trace-assets')
66-
nodeFileTraceSpan.traceFn(() => {
121+
await span.traceChild('create-trace-assets').traceAsyncFn(async () => {
122+
const entryFilesMap = new Map<any, Set<string>>()
123+
const chunksToTrace = new Set<string>()
124+
67125
for (const entrypoint of compilation.entrypoints.values()) {
68126
const entryFiles = new Set<string>()
69127

70128
for (const chunk of entrypoint
71129
.getEntrypointChunk()
72130
.getAllReferencedChunks()) {
73131
for (const file of chunk.files) {
74-
entryFiles.add(nodePath.join(outputPath, file))
132+
const filePath = nodePath.join(outputPath, file)
133+
chunksToTrace.add(filePath)
134+
entryFiles.add(filePath)
75135
}
76136
for (const file of chunk.auxiliaryFiles) {
77-
entryFiles.add(nodePath.join(outputPath, file))
137+
const filePath = nodePath.join(outputPath, file)
138+
chunksToTrace.add(filePath)
139+
entryFiles.add(filePath)
78140
}
79141
}
80-
// don't include the entry itself in the trace
81-
entryFiles.delete(nodePath.join(outputPath, `../${entrypoint.name}.js`))
142+
entryFilesMap.set(entrypoint, entryFiles)
143+
}
144+
145+
const result = await nodeFileTrace([...chunksToTrace], {
146+
base: root,
147+
processCwd: this.appDir,
148+
readFile: async (path) => {
149+
if (chunksToTrace.has(path)) {
150+
const source =
151+
assets[
152+
nodePath.relative(outputPath, path).replace(/\\/g, '/')
153+
]?.source?.()
154+
if (source) return source
155+
}
156+
try {
157+
return await new Promise((resolve, reject) => {
158+
;(
159+
compilation.inputFileSystem
160+
.readFile as typeof import('fs').readFile
161+
)(path, (err, data) => {
162+
if (err) return reject(err)
163+
resolve(data)
164+
})
165+
})
166+
} catch (e) {
167+
if (isError(e) && (e.code === 'ENOENT' || e.code === 'EISDIR')) {
168+
return null
169+
}
170+
throw e
171+
}
172+
},
173+
readlink,
174+
stat,
175+
resolve: doResolve
176+
? (id, parent, job, isCjs) => {
177+
return doResolve(id, parent, job, !isCjs)
178+
}
179+
: undefined,
180+
ignore: [...TRACE_IGNORES, ...this.excludeFiles],
181+
mixedModules: true,
182+
})
183+
const reasons = result.reasons
184+
const fileList = result.fileList
185+
result.esmFileList.forEach((file) => fileList.add(file))
186+
187+
const parentFilesMap = getFilesMapFromReasons(fileList, reasons)
188+
189+
for (const [entrypoint, entryFiles] of entryFilesMap) {
82190
const traceOutputName = `../${entrypoint.name}.js.nft.json`
83191
const traceOutputPath = nodePath.dirname(
84192
nodePath.join(outputPath, traceOutputName)
85193
)
194+
const allEntryFiles = new Set<string>()
195+
196+
entryFiles.forEach((file) => {
197+
parentFilesMap
198+
.get(nodePath.relative(root, file))
199+
?.forEach((child) => {
200+
allEntryFiles.add(nodePath.join(root, child))
201+
})
202+
})
203+
// don't include the entry itself in the trace
204+
entryFiles.delete(nodePath.join(outputPath, `../${entrypoint.name}.js`))
86205

87206
assets[traceOutputName] = new sources.RawSource(
88207
JSON.stringify({
89208
version: TRACE_OUTPUT_VERSION,
90209
files: [
91210
...entryFiles,
211+
...allEntryFiles,
92212
...(this.entryTraces.get(entrypoint.name) || []),
93213
].map((file) => {
94214
return nodePath
@@ -104,12 +224,14 @@ export class TraceEntryPointsPlugin implements webpack5.WebpackPluginInstance {
104224
tapfinishModules(
105225
compilation: webpack5.Compilation,
106226
traceEntrypointsPluginSpan: Span,
107-
doResolve?: (
227+
doResolve: (
108228
request: string,
109229
parent: string,
110230
job: import('@vercel/nft/out/node-file-trace').Job,
111231
isEsmRequested: boolean
112-
) => Promise<string>
232+
) => Promise<string>,
233+
readlink: any,
234+
stat: any
113235
) {
114236
compilation.hooks.finishModules.tapAsync(
115237
PLUGIN_NAME,
@@ -168,71 +290,9 @@ export class TraceEntryPointsPlugin implements webpack5.WebpackPluginInstance {
168290
if (source) {
169291
return source.buffer()
170292
}
171-
172-
try {
173-
return await new Promise((resolve, reject) => {
174-
;(
175-
compilation.inputFileSystem
176-
.readFile as typeof import('fs').readFile
177-
)(path, (err, data) => {
178-
if (err) return reject(err)
179-
resolve(data)
180-
})
181-
})
182-
} catch (e) {
183-
if (
184-
isError(e) &&
185-
(e.code === 'ENOENT' || e.code === 'EISDIR')
186-
) {
187-
return null
188-
}
189-
throw e
190-
}
191-
}
192-
const readlink = async (path: string): Promise<string | null> => {
193-
try {
194-
return await new Promise((resolve, reject) => {
195-
;(
196-
compilation.inputFileSystem
197-
.readlink as typeof import('fs').readlink
198-
)(path, (err, link) => {
199-
if (err) return reject(err)
200-
resolve(link)
201-
})
202-
})
203-
} catch (e) {
204-
if (
205-
isError(e) &&
206-
(e.code === 'EINVAL' ||
207-
e.code === 'ENOENT' ||
208-
e.code === 'UNKNOWN')
209-
) {
210-
return null
211-
}
212-
throw e
213-
}
214-
}
215-
const stat = async (
216-
path: string
217-
): Promise<import('fs').Stats | null> => {
218-
try {
219-
return await new Promise((resolve, reject) => {
220-
;(
221-
compilation.inputFileSystem.stat as typeof import('fs').stat
222-
)(path, (err, stats) => {
223-
if (err) return reject(err)
224-
resolve(stats)
225-
})
226-
})
227-
} catch (e) {
228-
if (
229-
isError(e) &&
230-
(e.code === 'ENOENT' || e.code === 'ENOTDIR')
231-
) {
232-
return null
233-
}
234-
throw e
235-
}
293+
// we don't want to analyze non-transpiled
294+
// files here, that is done against webpack output
295+
return ''
236296
}
237297

238298
const entryPaths = Array.from(entryModMap.keys())
@@ -262,8 +322,6 @@ export class TraceEntryPointsPlugin implements webpack5.WebpackPluginInstance {
262322
})
263323
let fileList: Set<string>
264324
let reasons: NodeFileTraceReasons
265-
const root = nodePath.parse(process.cwd()).root
266-
267325
await finishModulesSpan
268326
.traceChild('node-file-trace', {
269327
traceEntryCount: entriesToTrace.length + '',
@@ -276,11 +334,15 @@ export class TraceEntryPointsPlugin implements webpack5.WebpackPluginInstance {
276334
readlink,
277335
stat,
278336
resolve: doResolve
279-
? (id, parent, job, isCjs) =>
280-
// @ts-ignore
281-
doResolve(id, parent, job, !isCjs)
337+
? async (id, parent, job, isCjs) => {
338+
return doResolve(id, parent, job, !isCjs)
339+
}
282340
: undefined,
283-
ignore: [...TRACE_IGNORES, ...this.excludeFiles],
341+
ignore: [
342+
...TRACE_IGNORES,
343+
...this.excludeFiles,
344+
'**/node_modules/**',
345+
],
284346
mixedModules: true,
285347
})
286348
// @ts-ignore
@@ -289,50 +351,10 @@ export class TraceEntryPointsPlugin implements webpack5.WebpackPluginInstance {
289351
reasons = result.reasons
290352
})
291353

292-
// this uses the reasons tree to collect files specific to a certain
293-
// parent allowing us to not have to trace each parent separately
294-
const parentFilesMap = new Map<string, Set<string>>()
295-
296-
function propagateToParents(
297-
parents: Set<string>,
298-
file: string,
299-
seen = new Set<string>()
300-
) {
301-
for (const parent of parents || []) {
302-
if (!seen.has(parent)) {
303-
seen.add(parent)
304-
let parentFiles = parentFilesMap.get(parent)
305-
306-
if (!parentFiles) {
307-
parentFiles = new Set()
308-
parentFilesMap.set(parent, parentFiles)
309-
}
310-
parentFiles.add(file)
311-
const parentReason = reasons.get(parent)
312-
313-
if (parentReason?.parents) {
314-
propagateToParents(parentReason.parents, file, seen)
315-
}
316-
}
317-
}
318-
}
319-
320354
await finishModulesSpan
321355
.traceChild('collect-traced-files')
322356
.traceAsyncFn(() => {
323-
for (const file of fileList!) {
324-
const reason = reasons!.get(file)
325-
326-
if (
327-
!reason ||
328-
!reason.parents ||
329-
(reason.type === 'initial' && reason.parents.size === 0)
330-
) {
331-
continue
332-
}
333-
propagateToParents(reason.parents, file)
334-
}
335-
357+
const parentFilesMap = getFilesMapFromReasons(fileList, reasons)
336358
entryPaths.forEach((entry) => {
337359
const entryName = entryNameMap.get(entry)!
338360
const normalizedEntry = nodePath.relative(root, entry)
@@ -371,24 +393,69 @@ export class TraceEntryPointsPlugin implements webpack5.WebpackPluginInstance {
371393

372394
apply(compiler: webpack5.Compiler) {
373395
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
396+
const readlink = async (path: string): Promise<string | null> => {
397+
try {
398+
return await new Promise((resolve, reject) => {
399+
;(
400+
compilation.inputFileSystem
401+
.readlink as typeof import('fs').readlink
402+
)(path, (err, link) => {
403+
if (err) return reject(err)
404+
resolve(link)
405+
})
406+
})
407+
} catch (e) {
408+
if (
409+
isError(e) &&
410+
(e.code === 'EINVAL' || e.code === 'ENOENT' || e.code === 'UNKNOWN')
411+
) {
412+
return null
413+
}
414+
throw e
415+
}
416+
}
417+
const stat = async (path: string): Promise<import('fs').Stats | null> => {
418+
try {
419+
return await new Promise((resolve, reject) => {
420+
;(compilation.inputFileSystem.stat as typeof import('fs').stat)(
421+
path,
422+
(err, stats) => {
423+
if (err) return reject(err)
424+
resolve(stats)
425+
}
426+
)
427+
})
428+
} catch (e) {
429+
if (isError(e) && (e.code === 'ENOENT' || e.code === 'ENOTDIR')) {
430+
return null
431+
}
432+
throw e
433+
}
434+
}
435+
374436
const compilationSpan = spans.get(compilation) || spans.get(compiler)!
375437
const traceEntrypointsPluginSpan = compilationSpan.traceChild(
376438
'next-trace-entrypoint-plugin'
377439
)
378440
traceEntrypointsPluginSpan.traceFn(() => {
379441
// @ts-ignore TODO: Remove ignore when webpack 5 is stable
380-
compilation.hooks.processAssets.tap(
442+
compilation.hooks.processAssets.tapAsync(
381443
{
382444
name: PLUGIN_NAME,
383445
// @ts-ignore TODO: Remove ignore when webpack 5 is stable
384446
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
385447
},
386-
(assets: any) => {
448+
(assets: any, callback: any) => {
387449
this.createTraceAssets(
388450
compilation,
389451
assets,
390-
traceEntrypointsPluginSpan
452+
traceEntrypointsPluginSpan,
453+
readlink,
454+
stat,
455+
doResolve
391456
)
457+
.then(() => callback())
458+
.catch((err) => callback(err))
392459
}
393460
)
394461
let resolver = compilation.resolverFactory.get('normal')
@@ -540,7 +607,9 @@ export class TraceEntryPointsPlugin implements webpack5.WebpackPluginInstance {
540607
this.tapfinishModules(
541608
compilation,
542609
traceEntrypointsPluginSpan,
543-
doResolve
610+
doResolve,
611+
readlink,
612+
stat
544613
)
545614
})
546615
})

0 commit comments

Comments
 (0)
Please sign in to comment.