diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 4dd591baa1aa68..810bf8cf15811b 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -47,6 +47,7 @@ export interface DepOptimizationMetadata { string, { file: string + src: string needsInterop: boolean } > @@ -55,8 +56,9 @@ export interface DepOptimizationMetadata { export async function optimizeDeps( config: ResolvedConfig, force = config.server.force, - asCommand = false -) { + asCommand = false, + newDeps?: Record // missing imports encountered after server has started +): Promise { config = { ...config, command: 'build' @@ -67,7 +69,7 @@ export async function optimizeDeps( if (!cacheDir) { log(`No package.json. Skipping.`) - return + return null } const dataPath = path.join(cacheDir, 'metadata.json') @@ -84,7 +86,7 @@ export async function optimizeDeps( // hash is consistent, no need to re-bundle if (prevData && prevData.hash === data.hash) { log('Hash is consistent. Skipping. Use --force to override.') - return + return prevData } } @@ -94,7 +96,13 @@ export async function optimizeDeps( fs.mkdirSync(cacheDir, { recursive: true }) } - const { deps, missing } = await scanImports(config) + let deps: Record, missing: Record + if (!newDeps) { + ;({ deps, missing } = await scanImports(config)) + } else { + deps = newDeps + missing = {} + } const missingIds = Object.keys(missing) if (missingIds.length) { @@ -132,17 +140,21 @@ export async function optimizeDeps( if (!qualifiedIds.length) { writeFile(dataPath, JSON.stringify(data, null, 2)) log(`No dependencies to bundle. Skipping.\n\n\n`) - return + return data } const depsString = qualifiedIds.map((id) => chalk.yellow(id)).join(`, `) if (!asCommand) { - // This is auto run on server start - let the user know that we are - // pre-optimizing deps - logger.info(chalk.greenBright(`Pre-bundling dependencies:\n${depsString}`)) - logger.info( - `(this will be run only when your dependencies or config have changed)` - ) + if (!newDeps) { + // This is auto run on server start - let the user know that we are + // pre-optimizing deps + logger.info( + chalk.greenBright(`Pre-bundling dependencies:\n${depsString}`) + ) + logger.info( + `(this will be run only when your dependencies or config have changed)` + ) + } } else { logger.info(chalk.greenBright(`Optimizing dependencies:\n${depsString}`)) } @@ -193,6 +205,7 @@ export async function optimizeDeps( } data.optimized[id] = { file: normalizePath(path.resolve(output)), + src: entry, needsInterop: needsInterop(id, entry, exports) } break @@ -200,6 +213,7 @@ export async function optimizeDeps( } writeFile(dataPath, JSON.stringify(data, null, 2)) + return data } // https://github.com/vitejs/vite/issues/1724#issuecomment-767619642 diff --git a/packages/vite/src/node/optimizer/registerMissing.ts b/packages/vite/src/node/optimizer/registerMissing.ts new file mode 100644 index 00000000000000..8bdb830accb4b8 --- /dev/null +++ b/packages/vite/src/node/optimizer/registerMissing.ts @@ -0,0 +1,62 @@ +import chalk from 'chalk' +import { optimizeDeps } from '.' +import { ViteDevServer } from '..' + +const debounceMs = 100 + +export function createMissingImpoterRegisterFn(server: ViteDevServer) { + const { logger } = server.config + let knownOptimized = server._optimizeDepsMetadata!.optimized + let currentMissing: Record = {} + let handle: NodeJS.Timeout + + async function rerun() { + const newDeps = currentMissing + currentMissing = {} + + for (const id in knownOptimized) { + newDeps[id] = knownOptimized[id].src + } + + logger.info( + chalk.yellow(`new imports encountered, updating dependencies...`), + { + timestamp: true + } + ) + + try { + const newData = (server._optimizeDepsMetadata = await optimizeDeps( + server.config, + true, + false, + newDeps + )) + knownOptimized = newData!.optimized + server.ws.send({ + type: 'full-reload', + path: '*' + }) + } catch (e) { + logger.error( + chalk.red(`error while updating dependencies:\n${e.stack}`), + { timestamp: true } + ) + } finally { + server._hasPendingReload = false + } + + logger.info(chalk.greenBright(`✨ dependencies updated.`), { + timestamp: true + }) + } + + return function registerMissingImport(id: string, resolved: string) { + if (!knownOptimized[id]) { + currentMissing[id] = resolved + if (handle) clearTimeout(handle) + handle = setTimeout(rerun, debounceMs) + server._hasPendingReload = true + } + } +} diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index 691101e13085f1..49d72e178f3d36 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -23,6 +23,7 @@ import slash from 'slash' import { createFilter } from '@rollup/pluginutils' import { PartialResolvedId } from 'rollup' import { resolve as _resolveExports } from 'resolve.exports' +import { isCSSRequest } from './css' const altMainFields = [ 'module', @@ -335,15 +336,29 @@ export function tryNodeResolve( moduleSideEffects: pkg.hasSideEffects(resolved) } } else { - // During serve, inject a version query to npm deps so that the browser - // can cache it without revalidation. Make sure to apply this only to - // files actually inside node_modules so that locally linked packages - // in monorepos are not cached this way. - if (resolved.includes('node_modules')) { - const versionHash = server?._optimizeDepsMetadata?.hash + if (!resolved.includes('node_modules') || !server) { + // linked + return { id: resolved } + } + // if we reach here, it's a valid dep import that hasn't been optimzied. + const exclude = server.config.optimizeDeps?.exclude + if ( + exclude?.includes(pkgId) || + exclude?.includes(id) || + isCSSRequest(resolved) || + server.config.assetsInclude(resolved) + ) { + // excluded from optimization + // Inject a version query to npm deps so that the browser + // can cache it without revalidation. + const versionHash = server._optimizeDepsMetadata?.hash if (versionHash) { resolved = injectQuery(resolved, `v=${versionHash}`) } + } else { + // this is a missing import. + // queue optimize-deps re-run. + server._registerMissingImport?.(id, resolved) } return { id: resolved } } diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index be3b15d74193c6..d4039b49cf5b3d 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -46,6 +46,7 @@ import { DepOptimizationMetadata, optimizeDeps } from '../optimizer' import { ssrLoadModule } from '../ssr/ssrModuleLoader' import { resolveSSRExternal } from '../ssr/ssrExternal' import { ssrRewriteStacktrace } from '../ssr/ssrStacktrace' +import { createMissingImpoterRegisterFn } from '../optimizer/registerMissing' export interface ServerOptions { host?: string @@ -239,6 +240,14 @@ export interface ViteDevServer { module: ModuleNode } > + /** + * @internal + */ + _registerMissingImport: ((id: string, resolved: string) => void) | null + /** + * @internal + */ + _hasPendingReload: boolean } export async function createServer( @@ -310,7 +319,9 @@ export async function createServer( }, _optimizeDepsMetadata: null, _ssrExternals: null, - _globImporters: {} + _globImporters: {}, + _registerMissingImport: null, + _hasPendingReload: false } process.once('SIGTERM', async () => { @@ -435,15 +446,8 @@ export async function createServer( const runOptimize = async () => { if (config.optimizeCacheDir) { - // run optimizer - await optimizeDeps(config) - // after optimization, read updated optimization metadata - const dataPath = path.resolve(config.optimizeCacheDir, 'metadata.json') - if (fs.existsSync(dataPath)) { - server._optimizeDepsMetadata = JSON.parse( - fs.readFileSync(dataPath, 'utf-8') - ) - } + server._optimizeDepsMetadata = await optimizeDeps(config) + server._registerMissingImport = createMissingImpoterRegisterFn(server) } }