import { NormalModule, webpack } from 'next/dist/compiled/webpack/webpack' /** * List of target triples next-swc native binary supports. */ export type SWC_TARGET_TRIPLE = | 'x86_64-apple-darwin' | 'x86_64-unknown-linux-gnu' | 'x86_64-pc-windows-msvc' | 'i686-pc-windows-msvc' | 'aarch64-unknown-linux-gnu' | 'armv7-unknown-linux-gnueabihf' | 'aarch64-apple-darwin' | 'aarch64-linux-android' | 'arm-linux-androideabi' | 'x86_64-unknown-freebsd' | 'x86_64-unknown-linux-musl' | 'aarch64-unknown-linux-musl' | 'aarch64-pc-windows-msvc' export type Feature = | 'next/image' | 'next/future/image' | 'next/script' | 'next/dynamic' | 'swcLoader' | 'swcMinify' | 'swcRelay' | 'swcStyledComponents' | 'swcReactRemoveProperties' | 'swcExperimentalDecorators' | 'swcRemoveConsole' | 'swcImportSource' | 'swcEmotion' | `swc/target/${SWC_TARGET_TRIPLE}` 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 = new Map([ ['next/image', '/next/image.js'], ['next/future/image', '/next/future/image.js'], ['next/script', '/next/script.js'], ['next/dynamic', '/next/dynamic.js'], ]) // List of build features used in webpack configuration const BUILD_FEATURES: Array = [ 'swcLoader', 'swcMinify', 'swcRelay', 'swcStyledComponents', 'swcReactRemoveProperties', 'swcExperimentalDecorators', 'swcRemoveConsole', 'swcImportSource', 'swcEmotion', 'swc/target/x86_64-apple-darwin', 'swc/target/x86_64-unknown-linux-gnu', 'swc/target/x86_64-pc-windows-msvc', 'swc/target/i686-pc-windows-msvc', 'swc/target/aarch64-unknown-linux-gnu', 'swc/target/armv7-unknown-linux-gnueabihf', 'swc/target/aarch64-apple-darwin', 'swc/target/aarch64-linux-android', 'swc/target/arm-linux-androideabi', 'swc/target/x86_64-unknown-freebsd', 'swc/target/x86_64-unknown-linux-musl', 'swc/target/aarch64-unknown-linux-musl', 'swc/target/aarch64-pc-windows-msvc', ] const ELIMINATED_PACKAGES = new Set() /** * 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[], originModule: Module ): Set { const originModules = new Set() for (const connection of connections) { if ( !originModules.has(connection.originModule) && connection.originModule !== originModule ) { originModules.add(connection.originModule) } } return originModules } /** * 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() // Build feature usage is on/off and is known before the build starts constructor(buildFeaturesMap: Map) { 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, 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, module ) this.usageTracker.get(feature)!.invocationCount = originModules.size } modulesFinish() } ) callback() } ) if (compiler.options.mode === 'production' && !compiler.watchMode) { compiler.hooks.compilation.tap(TelemetryPlugin.name, (compilation) => { const moduleHooks = NormalModule.getCompilationHooks(compilation) moduleHooks.loader.tap(TelemetryPlugin.name, (loaderContext: any) => { loaderContext.eliminatedPackages = ELIMINATED_PACKAGES }) }) } } usages(): FeatureUsage[] { return [...this.usageTracker.values()] } packagesUsedInServerSideProps(): string[] { return Array.from(ELIMINATED_PACKAGES) } }