@roots/entrypoints-webpack-plugin
Version:
Manifest with assets grouped by entrypoint
192 lines (161 loc) • 4.85 kB
text/typescript
import type {
CompilationHooks,
Entrypoints,
Options,
} from '@roots/entrypoints-webpack-plugin'
import type {WebpackPluginInstance} from 'webpack'
import {join} from 'node:path'
import {SyncHook, SyncWaterfallHook} from 'tapable'
import Webpack from 'webpack'
import {HtmlEmitter} from './html.emitter.js'
/**
* {@link https://webpack.js.org/api/plugins/#custom-hooks}
*/
const hookMap = new WeakMap<Webpack.Compilation, CompilationHooks>()
/**
* Produces `entrypoints.json` artifact with compiled assets broken down
* by entrypoint and then filetype.
*
* {@link WebpackPluginInstance}
*
* @example
* ```js
* import {EntrypointsWebpackPlugin} from '@roots/entrypoints-webpack-plugin'
*
* const config = {
* plugins: [new EntrypointsWebpackPlugin()]
* }
* ```
*/
export class EntrypointsWebpackPlugin implements WebpackPluginInstance {
/**
* Get compilation hooks
*
* @param compilation
* @returns
*/
public static getCompilationHooks(
compilation: Webpack.Compilation,
): CompilationHooks {
let hooks = hookMap.get(compilation)
if (hooks === undefined) {
hooks = {
compilation: new SyncHook([`compilation`]),
entrypoints: new SyncWaterfallHook([`entrypoints`]),
}
hookMap.set(compilation, hooks)
}
return hooks
}
/**
* Collected assets
*/
public entrypoints: Entrypoints
/**
* Plugin compiler ident
*/
protected plugin = {
name: `EntrypointsManifestPlugin`,
stage: Infinity,
}
/**
* Class constructor
*/
public constructor(public options: Options) {
if (!this.options.type) {
this.options.type = `object`
}
if (!this.options.publicPath || this.options.publicPath === `auto`) {
this.options.publicPath = ``
}
if (!this.options.name) {
this.options.name = `entrypoints.json`
}
this.entrypoints = new Map()
this.addToManifest = this.addToManifest.bind(this)
this.getFilesFromChunks = this.getFilesFromChunks.bind(this)
this.apply = this.apply.bind(this)
}
public addToManifest({
ident,
path,
type,
}: {
ident: string
path: string
type: string
}) {
if (path.includes(`.map`) || path.includes(`hot-update`)) return
!this.entrypoints.has(ident) && this.entrypoints.set(ident, new Map())
!this.entrypoints.get(ident)?.has(type)
? this.entrypoints.get(ident)?.set(type, new Set([path]))
: this.entrypoints.get(ident)?.get(type)?.add(path)
}
/**
* {@link WebpackPluginInstance.apply}
*/
public apply(compiler: Webpack.Compiler): void {
compiler.hooks.thisCompilation.tap(
this.constructor.name,
compilation => {
compilation.hooks.processAssets.tapPromise(
{
name: this.constructor.name,
stage: Webpack.Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
},
async assets => {
const hooks =
EntrypointsWebpackPlugin.getCompilationHooks(compilation)
hooks.compilation.call(compilation)
this.entrypoints = new Map()
for (const entry of compilation.entrypoints.values()) {
this.getFilesFromChunks(entry.chunks).map(({file}) => {
if (!entry.name) return
const path = join(this.options.publicPath ?? ``, file)
const type = path.split(`.`).pop() ?? `default`
this.addToManifest({ident: entry.name, path, type})
})
}
this.entrypoints = hooks.entrypoints.call(this.entrypoints)
if (this.options.emitHtml) {
new HtmlEmitter(
compilation,
assets,
this.entrypoints,
this.options.publicPath as string,
).emit()
}
let source: any = {}
for (const [name, entry] of this.entrypoints.entries()) {
if (!source[name]) source[name] = {}
for (const [type, assets] of entry.entries()) {
if (!source[name][type]) source[name][type] = []
source[name][type] = [...assets]
}
}
compilation.emitAsset(
this.options.name as string,
new compiler.webpack.sources.RawSource(
JSON.stringify(source, null, 2),
),
)
},
)
},
)
}
/**
* Get assets from an entrypoint
*/
public getFilesFromChunks(
chunks: Webpack.Chunk[],
): Array<{file: string; ident: string}> {
const files: Array<{file: string; ident: string}> = []
for (const chunk of chunks) {
Array.from(chunk.files).map(file => {
files.push({file, ident: chunk.name as string})
})
}
return files
}
}