Skip to content

Instantly share code, notes, and snippets.

@romanlex
Created December 29, 2022 12:33
Show Gist options
  • Save romanlex/c3db7baad945a825d787c0c2e8366563 to your computer and use it in GitHub Desktop.
Save romanlex/c3db7baad945a825d787c0c2e8366563 to your computer and use it in GitHub Desktop.
Plugin for collect routes path by entry points
const minimatch = require('minimatch')
const path = require('path')
const PLUGIN_NAME = 'CollectEntriesRoutesPlugin'
const routeMap = new Map()
/** @typedef {import("webpack").Compiler} Compiler */
/**
* @typedef {Object} Options
* @property {() => boolean} filter
* @property {string} routeFileName
*/
class CollectEntriesRoutes {
/** @type {Options} */
options
/**
*
* @param {Options} options
*/
constructor(options) {
this.options = options
this.modules = {}
this.parserByDeps = {}
this.entriesWithBlobTarget = {}
this.targetBlobs = []
}
createGlobPatternFrom(pathOfFile, file) {
let blob = pathOfFile
if (path.isAbsolute(pathOfFile) === false) {
blob = blob.replace('./', '**/')
}
return `${blob}/${file}**`
}
extractParser(parser) {
const { options } = this
parser.hooks.exportSpecifier.tap(PLUGIN_NAME, (statement, identifierName, exportName) => {
if (!parser.state) return
if (!parser.state.module) return
if (!minimatch(parser.state.module.resource, `**/${options.routeFileName}**`)) return
const { module } = parser.state
const { issuer } = module
if (issuer && this.entriesWithBlobTarget[issuer.context] && module.context !== issuer.context) {
const { declaration } = statement
for (const node of declaration.declarations) {
if (node.id.type !== 'Identifier' && node.id.name !== exportName) continue
this.parserByDeps[module.context] = { exportNode: node, parser }
}
}
})
}
walkExport(exportNode) {
const { init } = exportNode
let arrayOfPaths = []
if (init.type !== 'ArrayExpression') return arrayOfPaths
const { elements } = init
for (const element of elements) {
if (element.type === 'ObjectExpression') {
let object = {}
for (let property of element.properties) {
if (property.type !== 'Property' && property.key.type !== 'Identifier') continue
if (property.value.type === 'Literal') {
object[property.key.name] = property.value.value
continue
}
if (property.value.type === 'MemberExpression') {
object[property.key.name] = '' // ?????????????????????????????
continue
}
}
arrayOfPaths.push(object)
}
}
return arrayOfPaths
}
onEntryOption(context, entry) {
const { options } = this
for (const key of Object.keys(entry)) {
const indexOfEntry = Array.isArray(entry[key].import)
? entry[key].import[entry[key].import.length - 1]
: entry[key].import
const entryDirname = path.dirname(indexOfEntry)
const blobByDirname = this.createGlobPatternFrom(entryDirname, options.routeFileName)
this.entriesWithBlobTarget[entryDirname] = {
blobByDirname,
entryDirname,
name: key,
}
this.targetBlobs.push(blobByDirname)
}
}
onFinishModules(modules, compilation) {
const { options } = this
for (const module of modules) {
if (module && module.resource && this.targetBlobs.some((item) => minimatch(module.resource, item))) {
routeMap.set(module.context, new Set())
let deps = []
for (const dependencyDeclaration of module.dependencies) {
const dependency = compilation.moduleGraph.getModule(dependencyDeclaration)
if (
dependency &&
dependencyDeclaration.name &&
dependency.resource &&
minimatch(dependency.resource, `**/${options.routeFileName}**`)
) {
deps.push(dependency)
}
}
for (const dep of deps) {
if (!this.parserByDeps[dep.context]) continue
const { exportNode } = this.parserByDeps[dep.context]
const paths = this.walkExport(exportNode)
const set = routeMap.get(module.context)
for (const path of paths) {
set.add(path.path)
}
routeMap.set(module.context, set)
}
}
}
}
onOptimizeModules(modules) {
const { options } = this
for (const module of modules) {
if (
options.to &&
module._source &&
module._source._value &&
new RegExp(options.to, 'gi').test(module._source._value)
) {
const objectToReplace = {}
for (const [key, value] of routeMap) {
const entryName = this.entriesWithBlobTarget[key]?.name ?? path.dirname(key)
objectToReplace[entryName] = Array.from(value)
}
module._source._value = module._source._value.replace(
new RegExp(options.to, 'gi'),
JSON.stringify(
Object.keys(objectToReplace)
.sort()
.reduce((obj, key) => {
obj[key] = objectToReplace[key]
return obj
}, {})
)
)
}
}
}
/**
* Apply the plugin
* @param {Compiler} compiler the compiler instance
* @returns {void}
*/
apply(compiler) {
compiler.hooks.entryOption.tap(PLUGIN_NAME, (context, entry) => this.onEntryOption(context, entry))
compiler.hooks.normalModuleFactory.tap(PLUGIN_NAME, (factory) => {
factory.hooks.parser.for('javascript/auto').tap(PLUGIN_NAME, (parser) => {
this.extractParser(parser)
})
})
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
compilation.hooks.finishModules.tapAsync(PLUGIN_NAME, (modules, done) => {
this.onFinishModules(modules, compilation)
done()
})
compilation.hooks.optimizeModules.tap(PLUGIN_NAME, (modules) => this.onOptimizeModules(modules))
})
}
}
module.exports = CollectEntriesRoutes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment