forked from vercel/next.js
/
telemetry-plugin.ts
127 lines (114 loc) · 3.43 KB
/
telemetry-plugin.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import type { webpack5 as webpack } from 'next/dist/compiled/webpack/webpack'
type Feature =
| 'next/image'
| 'next/script'
| 'next/dynamic'
| 'swcLoader'
| 'swcMinify'
interface FeatureUsage {
featureName: Feature
invocationCount: number
}
/**
* A vertex in the module graph.
*/
interface Module {
type: string
identifier(): string
}
/**
* An edge in the module graph.
*/
interface Connection {
originModule: unknown
}
// Map of a feature module to the file it belongs in the next package.
const FEATURE_MODULE_MAP: ReadonlyMap<Feature, string> = new Map([
['next/image', '/next/image.js'],
['next/script', '/next/script.js'],
['next/dynamic', '/next/dynamic.js'],
])
// List of build features used in webpack configuration
const BUILD_FEATURES: Array<Feature> = ['swcLoader', 'swcMinify']
/**
* Plugin that queries the ModuleGraph to look for modules that correspond to
* certain features (e.g. next/image and next/script) and record how many times
* they are imported.
*/
export class TelemetryPlugin implements webpack.WebpackPluginInstance {
private usageTracker = new Map<Feature, FeatureUsage>()
// Build feature usage is on/off and is known before the build starts
constructor(buildFeaturesMap: Map<Feature, boolean>) {
for (const featureName of BUILD_FEATURES) {
this.usageTracker.set(featureName, {
featureName,
invocationCount: buildFeaturesMap.get(featureName) ? 1 : 0,
})
}
for (const featureName of FEATURE_MODULE_MAP.keys()) {
this.usageTracker.set(featureName, {
featureName,
invocationCount: 0,
})
}
}
apply(compiler: webpack.Compiler): void {
compiler.hooks.make.tapAsync(
TelemetryPlugin.name,
async (compilation: webpack.Compilation, callback: () => void) => {
compilation.hooks.finishModules.tapAsync(
TelemetryPlugin.name,
async (modules: Iterable<Module>, modulesFinish: () => void) => {
for (const module of modules) {
const feature = findFeatureInModule(module)
if (!feature) {
continue
}
const connections = (
compilation as any
).moduleGraph.getIncomingConnections(module)
const originModules =
findUniqueOriginModulesInConnections(connections)
this.usageTracker.get(feature)!.invocationCount =
originModules.size
}
modulesFinish()
}
)
callback()
}
)
}
usages(): FeatureUsage[] {
return [...this.usageTracker.values()]
}
}
/**
* Determine if there is a feature of interest in the specified 'module'.
*/
function findFeatureInModule(module: Module): Feature | undefined {
if (module.type !== 'javascript/auto') {
return
}
for (const [feature, path] of FEATURE_MODULE_MAP) {
if (module.identifier().replace(/\\/g, '/').endsWith(path)) {
return feature
}
}
}
/**
* Find unique origin modules in the specified 'connections', which possibly
* contains more than one connection for a module due to different types of
* dependency.
*/
function findUniqueOriginModulesInConnections(
connections: Connection[]
): Set<unknown> {
const originModules = new Set()
for (const connection of connections) {
if (!originModules.has(connection.originModule)) {
originModules.add(connection.originModule)
}
}
return originModules
}