Last active
December 23, 2021 20:56
-
-
Save WhiteAbeLincoln/f6842dcca3d08fe0aead127fe452a533 to your computer and use it in GitHub Desktop.
Deduplicate entry points for vue-cli multi-page build
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @ts-check | |
const { defineConfig } = require('@vue/cli-service') | |
const path = require('path') | |
/** @typedef {NonNullable<import('@vue/cli-service').ProjectOptions['pages']>[string]} Page */ | |
/** @typedef {import('webpack-chain')} ChainConfig */ | |
/** | |
* Gets the entry points defined for a page. | |
* @param {Page} page | |
* @returns {string[]} an array of entry points | |
*/ | |
function getEntries(page) { | |
if (Array.isArray(page)) { | |
return page | |
} | |
if (typeof page === 'string') { | |
return [page] | |
} | |
return getEntries(page.entry) | |
} | |
/** | |
* Checks any member of an iterable passes the predicate | |
* @template A | |
* @param {Iterable<A>} it | |
* @param {(v: A) => boolean} pred | |
* @returns {boolean} | |
*/ | |
function some(it, pred) { | |
for (const v of it) { | |
if (pred(v)) return true | |
} | |
return false | |
} | |
/** | |
* Returns a function that can get a unique bundle name given a list of entry files | |
*/ | |
function getBundleName() { | |
/** @type {Map<string, string>} */ | |
const cache = new Map() | |
/** | |
* A list of entry files. The files are expected to be absolute. | |
* @param {string[]} entries | |
* @returns A unique bundle name for the given entry files | |
*/ | |
return entries => { | |
const paths = entries.map(p => (p.startsWith(__dirname) ? path.relative(__dirname, p) : p)).sort() | |
const key = paths.join('\n') | |
const entry = cache.get(key) | |
if (entry) { | |
return entry | |
} | |
let value = 'entry-' + paths.map(p => path.basename(p).replace(/\.(m?[tj]sx?|vue)$/, '')).join('-').toLowerCase() | |
let incr = 0 | |
const initVal = value | |
while (some(cache.values(), v => v === value)) { | |
value = `${initVal}-${++incr}` | |
} | |
cache.set(key, value) | |
return value | |
} | |
} | |
/** | |
* Given a pages config, creates a pair of mappings. | |
* The first mapping is of the original chunk/page names to the deduplicated chunk names | |
* The second mapping is of the deduplicated chunk names to the entry files in that chunk. | |
* @param {Record<string, Page>} pages | |
* @returns a pair of maps | |
*/ | |
function createPageMap(pages) { | |
const getter = getBundleName() | |
return Object.entries(pages) | |
.reduce((pair, [key, v]) => { | |
const {origToDedup, dedupEntries: realEntries} = pair | |
const entries = getEntries(v).map(p => path.resolve(__dirname, p)) | |
// make a unique name for the bundle based on the paths for the entries | |
const bundle = getter(entries) | |
if (!realEntries.has(bundle)) { | |
realEntries.set(bundle, entries) | |
} | |
if (!origToDedup.has(key)) { | |
origToDedup.set(key, bundle) | |
} | |
return pair | |
}, /** @type {{origToDedup: Map<string, string>, dedupEntries: Map<string, string[]>}} */({ origToDedup: new Map(), dedupEntries: new Map() })) | |
} | |
/** | |
* Takes a map of original chunk/page names to real chunk names, | |
* a map of the deduplicated entries, and modifies the config | |
* so that each page loads the deduplicated chunk instead of a | |
* unique chunk for each page. | |
* @param {Map<string, string>} origToDedup | |
* @param {Map<string, string[]>} dedupEntries | |
* @param {ChainConfig} cfg | |
*/ | |
function dedupConfigEntries(origToDedup, dedupEntries, cfg) { | |
// We deduplicate our entry points by gathering a list of the unique | |
// entries from our pages. We then create a new entry point for each unique entry | |
for (const [key, entries] of dedupEntries.entries()) { | |
cfg.entry(key).clear().merge(entries) | |
} | |
// next we iterate over the each page's html-webpack plugin | |
// they should be named, with the format `html-${key}`, where | |
// key is the page key in the original pages map. | |
// we replace the entry chunk (by default named after the key), | |
// with the deduplicated chunk. We also remove the original entry | |
// point, since it is no longer used. | |
for (const [key, bundle] of origToDedup.entries()) { | |
cfg.entryPoints.delete(key) | |
cfg.plugin(`html-${key}`) | |
.tap(args => { | |
/** @type {string[]} */ | |
const chunks = args?.[0]?.chunks | |
if (chunks) { | |
const idx = chunks.findIndex(c => c === key) | |
if (idx >= 0) { | |
chunks[idx] = bundle | |
} | |
} | |
return args | |
}) | |
} | |
} | |
// use as follows | |
// extract your pages map from the config | |
const pages = { /* ... */ } | |
// create the deduplicated entries | |
const { origToDedup, dedupEntries } = createPageMap(pages) | |
// call dedupConfigEntries from your chain webpack hook | |
module.exports = defineConfig({ | |
pages, | |
chainWebpack: cfg => { dedupConfigEntries(origToDedup, dedupEntries, cfg) }, | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment