diff --git a/docs/guide/build.md b/docs/guide/build.md index aac86a237b6819..1f1f149a07f7c9 100644 --- a/docs/guide/build.md +++ b/docs/guide/build.md @@ -43,6 +43,20 @@ module.exports = defineConfig({ For example, you can specify multiple Rollup outputs with plugins that are only applied during build. +## Chunking Strategy + +You can configure how chunks are split using `build.rollupOptions.manualChunks` (see [Rollup docs](https://rollupjs.org/guide/en/#outputmanualchunks)). Until Vite 2.7, the default chunking strategy divided the chunks into `index` and `vendor`. It is a good strategy for some SPAs, but it is hard to provide a general solution for every Vite target use case. From Vite 2.8, `manualChunks` is no longer modified by default. You can continue to use the Split Vendor Chunk strategy by adding the `splitVendorChunkPlugin` in your config file: + +```js +// vite.config.js +import { splitVendorChunkPlugin } from 'vite' +module.exports = defineConfig({ + plugins: [splitVendorChunkPlugin()] +}) +``` + +This strategy is also provided as a `splitVendorChunk({ cache: SplitVendorChunkCache })` factory, in case composition with custom logic is needed. `cache.reset()` needs to be called at `buildStart` for build watch mode to work correctly in this case. + ## Rebuild on files changes You can enable rollup watcher with `vite build --watch`. Or, you can directly adjust the underlying [`WatcherOptions`](https://rollupjs.org/guide/en/#watch-options) via `build.watch`: diff --git a/packages/playground/vue/vite.config.ts b/packages/playground/vue/vite.config.ts index 82efdac5e9f876..f99a68ce8b6b10 100644 --- a/packages/playground/vue/vite.config.ts +++ b/packages/playground/vue/vite.config.ts @@ -1,4 +1,4 @@ -import { defineConfig } from 'vite' +import { defineConfig, splitVendorChunkPlugin } from 'vite' import vuePlugin from '@vitejs/plugin-vue' import { vueI18nPlugin } from './CustomBlockPlugin' @@ -12,11 +12,22 @@ export default defineConfig({ vuePlugin({ reactivityTransform: true }), + splitVendorChunkPlugin(), vueI18nPlugin ], build: { // to make tests faster - minify: false + minify: false, + rollupOptions: { + output: { + // Test splitVendorChunkPlugin composition + manualChunks(id) { + if (id.includes('src-import')) { + return 'src-import' + } + } + } + } }, css: { modules: { diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index c8a1a0f17fa4a4..9a3a2a65702e58 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -12,8 +12,6 @@ import type { OutputOptions, RollupOutput, ExternalOption, - GetManualChunk, - GetModuleInfo, WatcherOptions, RollupWatcher, RollupError, @@ -37,7 +35,6 @@ import { dataURIPlugin } from './plugins/dataUri' import { buildImportAnalysisPlugin } from './plugins/importAnalysisBuild' import { resolveSSRExternal, shouldExternalizeForSSR } from './ssr/ssrExternal' import { ssrManifestPlugin } from './ssr/ssrManifestPlugin' -import { isCSSRequest } from './plugins/css' import type { DepOptimizationMetadata } from './optimizer' import { scanImports } from './optimizer/scan' import { assetImportMetaUrlPlugin } from './plugins/assetImportMetaUrl' @@ -483,13 +480,6 @@ async function doBuild( // #1048 add `Symbol.toStringTag` for module default export namespaceToStringTag: true, inlineDynamicImports: ssr && typeof input === 'string', - manualChunks: - !ssr && - !libOptions && - output?.format !== 'umd' && - output?.format !== 'iife' - ? createMoveToVendorChunkFn(config) - : undefined, ...output } } @@ -616,55 +606,6 @@ function getPkgName(root: string) { return name?.startsWith('@') ? name.split('/')[1] : name } -function createMoveToVendorChunkFn(config: ResolvedConfig): GetManualChunk { - const cache = new Map() - return (id, { getModuleInfo }) => { - if ( - id.includes('node_modules') && - !isCSSRequest(id) && - staticImportedByEntry(id, getModuleInfo, cache) - ) { - return 'vendor' - } - } -} - -function staticImportedByEntry( - id: string, - getModuleInfo: GetModuleInfo, - cache: Map, - importStack: string[] = [] -): boolean { - if (cache.has(id)) { - return cache.get(id) as boolean - } - if (importStack.includes(id)) { - // circular deps! - cache.set(id, false) - return false - } - const mod = getModuleInfo(id) - if (!mod) { - cache.set(id, false) - return false - } - - if (mod.isEntry) { - cache.set(id, true) - return true - } - const someImporterIs = mod.importers.some((importer) => - staticImportedByEntry( - importer, - getModuleInfo, - cache, - importStack.concat(id) - ) - ) - cache.set(id, someImporterIs) - return someImporterIs -} - export function resolveLibFilename( libOptions: LibraryOptions, format: ModuleFormat, diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index fe67e8efac8f8c..99567735b21757 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -7,6 +7,10 @@ export { send } from './server/send' export { createLogger, printHttpServerUrls } from './logger' export { transformWithEsbuild } from './plugins/esbuild' export { resolvePackageEntry } from './plugins/resolve' +export { + splitVendorChunkPlugin, + splitVendorChunk +} from './plugins/splitVendorChunk' export { resolvePackageData } from './packages' export { normalizePath } from './utils' @@ -99,6 +103,7 @@ export type { Terser } from 'types/terser' export type { RollupCommonJSOptions } from 'types/commonjs' export type { RollupDynamicImportVarsOptions } from 'types/dynamicImportVars' export type { Matcher, AnymatchPattern, AnymatchFn } from 'types/anymatch' +export type { SplitVendorChunkCache } from './plugins/splitVendorChunk' import type { ChunkMetadata } from './plugins/metadata' diff --git a/packages/vite/src/node/plugins/splitVendorChunk.ts b/packages/vite/src/node/plugins/splitVendorChunk.ts new file mode 100644 index 00000000000000..3f7c16067a5f8e --- /dev/null +++ b/packages/vite/src/node/plugins/splitVendorChunk.ts @@ -0,0 +1,131 @@ +import type { UserConfig } from '../../node' +import type { Plugin } from '../plugin' +import type { + OutputOptions, + GetManualChunk, + GetManualChunkApi, + GetModuleInfo +} from 'rollup' +import { isCSSRequest } from './css' + +// Use splitVendorChunkPlugin() to get the same manualChunks strategy as Vite 2.7 +// We don't recommend using this strategy as a general solution moving forward + +// splitVendorChunk is a simple index/vendor strategy that was used in Vite +// until v2.8. It is exposed to let people continue to use it in case it was +// working well for their setups. +// The cache needs to be reset on buildStart for watch mode to work correctly +// Don't use this manualChunks strategy for ssr, lib mode, and 'umd' or 'iife' + +export class SplitVendorChunkCache { + cache: Map + constructor() { + this.cache = new Map() + } + reset() { + this.cache = new Map() + } +} + +export function splitVendorChunk( + options: { cache?: SplitVendorChunkCache } = {} +): GetManualChunk { + const cache = options.cache ?? new SplitVendorChunkCache() + return (id, { getModuleInfo }) => { + if ( + id.includes('node_modules') && + !isCSSRequest(id) && + staticImportedByEntry(id, getModuleInfo, cache.cache) + ) { + return 'vendor' + } + } +} + +function staticImportedByEntry( + id: string, + getModuleInfo: GetModuleInfo, + cache: Map, + importStack: string[] = [] +): boolean { + if (cache.has(id)) { + return cache.get(id) as boolean + } + if (importStack.includes(id)) { + // circular deps! + cache.set(id, false) + return false + } + const mod = getModuleInfo(id) + if (!mod) { + cache.set(id, false) + return false + } + + if (mod.isEntry) { + cache.set(id, true) + return true + } + const someImporterIs = mod.importers.some((importer) => + staticImportedByEntry( + importer, + getModuleInfo, + cache, + importStack.concat(id) + ) + ) + cache.set(id, someImporterIs) + return someImporterIs +} + +export function splitVendorChunkPlugin(): Plugin { + const caches: SplitVendorChunkCache[] = [] + function createSplitVendorChunk(output: OutputOptions, config: UserConfig) { + const cache = new SplitVendorChunkCache() + caches.push(cache) + const build = config.build ?? {} + const format = output?.format + if (!build.ssr && !build.lib && format !== 'umd' && format !== 'iife') { + return splitVendorChunk({ cache }) + } + } + return { + name: 'vite:split-vendor-chunk', + config(config) { + let outputs = config?.build?.rollupOptions?.output + if (outputs) { + outputs = Array.isArray(outputs) ? outputs : [outputs] + for (const output of outputs) { + const viteManualChunks = createSplitVendorChunk(output, config) + if (viteManualChunks) { + if (output.manualChunks) { + if (typeof output.manualChunks === 'function') { + const userManualChunks = output.manualChunks + output.manualChunks = (id: string, api: GetManualChunkApi) => { + return userManualChunks(id, api) ?? viteManualChunks(id, api) + } + } + // else, leave the object form of manualChunks untouched, as + // we can't safely replicate rollup handling. + } else { + output.manualChunks = viteManualChunks + } + } + } + } else { + return { + build: { + rollupOptions: { + output: { + manualChunks: createSplitVendorChunk({}, config) + } + } + } + } + } + }, + buildStart() { + caches.forEach((cache) => cache.reset()) + } + } +}