From 08bb5084596e24a634f5ea6c9504baa667964107 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 4 Feb 2022 13:33:29 +0100 Subject: [PATCH 01/24] feat: non-blocking pre bundling of dependencies --- packages/vite/src/node/index.ts | 4 +- packages/vite/src/node/optimizer/index.ts | 503 ++++++++++++------ .../src/node/optimizer/registerMissing.ts | 157 ++++-- .../vite/src/node/plugins/importAnalysis.ts | 70 ++- packages/vite/src/node/plugins/index.ts | 2 + .../vite/src/node/plugins/optimizedDeps.ts | 110 ++++ packages/vite/src/node/plugins/resolve.ts | 40 +- packages/vite/src/node/server/index.ts | 50 +- .../src/node/server/middlewares/transform.ts | 82 +-- 9 files changed, 697 insertions(+), 321 deletions(-) create mode 100644 packages/vite/src/node/plugins/optimizedDeps.ts diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index aea96d14d758be..f22c14687e478f 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -32,7 +32,9 @@ export type { } from './preview' export type { DepOptimizationMetadata, - DepOptimizationOptions + DepOptimizationOptions, + OptimizedDepInfo, + OptimizeDepsResult } from './optimizer' export type { Plugin } from './plugin' export type { PackageCache, PackageData } from './packages' diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 6102e832841a89..0219e3afafa4a2 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -50,6 +50,12 @@ export interface DepOptimizationOptions { * cannot be globs). */ exclude?: string[] + /** + * Hold back server start when doing the initial pre-bundling. + * Default is false. Set to true for testing purposes, or in situations + * where the pre Vite 2.9 strategy is required + */ + holdBackServerStart?: boolean /** * Options to pass to esbuild during the dep scanning and optimization * @@ -81,6 +87,29 @@ export interface DepOptimizationOptions { keepNames?: boolean } +export interface OptimizeDepsResult { + /** + * After a re-optimization, the internal bundled chunks may change + * and a full page reload is required if that is the case + * If the files are stable, we can avoid the reload that is expensive + * for large applications + */ + stableFiles: boolean +} + +export interface OptimizedDepInfo { + file: string + src: string + needsInterop?: boolean + browserHash?: string + fileHash?: string + /** + * During optimization, ids can still be resolved to their final location + * but the bundles may not yet be saved to disk + */ + processing: Promise +} + export interface DepOptimizationMetadata { /** * The main hash is determined by user config and dependency lockfiles. @@ -93,23 +122,33 @@ export interface DepOptimizationMetadata { * optimized deps. */ browserHash: string - optimized: Record< - string, - { - file: string - src: string - needsInterop: boolean - } - > + /** + * Metadata for each already optimized dependency + */ + optimized: Record + /** + * Metadata for each newly discovered dependency after processing + */ + discovered: Record + /** + * During optimization, ids can still be resolved to their final location + * but the bundles may not yet be saved to disk + */ + processing: Promise } export async function optimizeDeps( config: ResolvedConfig, force = config.server.force, asCommand = false, - newDeps?: Record, // missing imports encountered after server has started - ssr?: boolean -): Promise { + currentData: DepOptimizationMetadata | null = null, + newDeps?: Record, // missing imports encountered after server has started + ssr?: boolean, + processing: { + promise: Promise + resolve: (result?: OptimizeDepsResult) => void + } = newProcessingPromise() +): Promise { config = { ...config, command: 'build' @@ -120,10 +159,13 @@ export async function optimizeDeps( const dataPath = path.join(cacheDir, '_metadata.json') const mainHash = getDepHash(root, config) + const data: DepOptimizationMetadata = { hash: mainHash, browserHash: mainHash, - optimized: {} + optimized: {}, + discovered: {}, + processing: processing.promise } if (!force) { @@ -134,7 +176,11 @@ 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 prevData + for (const o of Object.keys(prevData.optimized)) { + prevData.optimized[o].processing = processing.promise + } + setTimeout(() => processing.resolve(), 0) + return { ...prevData, discovered: {}, processing: processing.promise } } } @@ -150,183 +196,316 @@ export async function optimizeDeps( JSON.stringify({ type: 'module' }) ) - let deps: Record, missing: Record + let deps: Record if (!newDeps) { - ;({ deps, missing } = await scanImports(config)) - } else { - deps = newDeps - missing = {} - } + // Initial optimizeDeps at server start. Perform a fast scan using esbuild to + // find deps to pre-bundle and include user harcoded dependencies - // update browser hash - data.browserHash = createHash('sha256') - .update(data.hash + JSON.stringify(deps)) - .digest('hex') - .substring(0, 8) - - const missingIds = Object.keys(missing) - if (missingIds.length) { - throw new Error( - `The following dependencies are imported but could not be resolved:\n\n ${missingIds - .map( - (id) => - `${colors.cyan(id)} ${colors.white( - colors.dim(`(imported by ${missing[id]})`) - )}` - ) - .join(`\n `)}\n\nAre they installed?` - ) - } + let missing: Record + ;({ deps, missing } = await scanImports(config)) - const include = config.optimizeDeps?.include - if (include) { - const resolve = config.createResolver({ asSrc: false }) - for (const id of include) { - // normalize 'foo >bar` as 'foo > bar' to prevent same id being added - // and for pretty printing - const normalizedId = normalizeId(id) - if (!deps[normalizedId]) { - const entry = await resolve(id) - if (entry) { - deps[normalizedId] = entry - } else { - throw new Error( - `Failed to resolve force included dependency: ${colors.cyan(id)}` + const missingIds = Object.keys(missing) + if (missingIds.length) { + processing.resolve() + throw new Error( + `The following dependencies are imported but could not be resolved:\n\n ${missingIds + .map( + (id) => + `${colors.cyan(id)} ${colors.white( + colors.dim(`(imported by ${missing[id]})`) + )}` ) + .join(`\n `)}\n\nAre they installed?` + ) + } + + const include = config.optimizeDeps?.include + if (include) { + const resolve = config.createResolver({ asSrc: false }) + for (const id of include) { + // normalize 'foo >bar` as 'foo > bar' to prevent same id being added + // and for pretty printing + const normalizedId = normalizeId(id) + if (!deps[normalizedId]) { + const entry = await resolve(id) + if (entry) { + deps[normalizedId] = entry + } else { + processing.resolve() + throw new Error( + `Failed to resolve force included dependency: ${colors.cyan(id)}` + ) + } } } } - } - const qualifiedIds = Object.keys(deps) + // update browser hash + data.browserHash = optimizedBrowserHash(data.hash, deps) + + // We generate the mapping of dependency ids to their cache file location + // before processing the dependencies with esbuild. This allow us to continue + // processing files in the importAnalysis and resolve plugins + for (const id in deps) { + const entry = deps[id] + data.optimized[id] = { + file: optimizedFilePath(id, cacheDir), + src: entry, + browserHash: data.browserHash, + processing: processing.promise + } + } + } else { + // Missing dependencies were found at run-time, optimizeDeps called while the + // server is running + deps = depsFromOptimizedInfo(newDeps) + + // Clone optimized info objects, fileHash, browserHash may be changed for them + for (const o of Object.keys(newDeps)) { + data.optimized[o] = { ...newDeps[o] } + } - if (!qualifiedIds.length) { - writeFile(dataPath, JSON.stringify(data, null, 2)) - log(`No dependencies to bundle. Skipping.\n\n\n`) - return data + // update global browser hash, but keep newDeps individual hashs until we know + // if files are stable so we can avoid a full page reload + data.browserHash = optimizedBrowserHash(data.hash, deps) } - const total = qualifiedIds.length - const maxListed = 5 - const listed = Math.min(total, maxListed) - const extra = Math.max(0, total - maxListed) - const depsString = colors.yellow( - qualifiedIds.slice(0, listed).join(`\n `) + - (extra > 0 ? `\n (...and ${extra} more)` : ``) - ) - if (!asCommand) { - if (!newDeps) { - // This is auto run on server start - let the user know that we are - // pre-optimizing deps - logger.info(colors.green(`Pre-bundling dependencies:\n ${depsString}`)) - logger.info( - `(this will be run only when your dependencies or config have changed)` - ) + // We prebundle dependencies with esbuild and cache them, but there is no need + // to wait here. Code that needs to access the cached deps needs to await + // the optimizeDepsMetadata.processing promise + prebundleDeps() + + return data + + async function prebundleDeps() { + const qualifiedIds = Object.keys(deps) + + if (!qualifiedIds.length) { + writeFile(dataPath, JSON.stringify(data, null, 2)) + log(`No dependencies to bundle. Skipping.\n\n\n`) + processing.resolve() + return data } - } else { - logger.info(colors.green(`Optimizing dependencies:\n ${depsString}`)) - } - // esbuild generates nested directory output with lowest common ancestor base - // this is unpredictable and makes it difficult to analyze entry / output - // mapping. So what we do here is: - // 1. flatten all ids to eliminate slash - // 2. in the plugin, read the entry ourselves as virtual files to retain the - // path. - const flatIdDeps: Record = {} - const idToExports: Record = {} - const flatIdToExports: Record = {} - - const { plugins = [], ...esbuildOptions } = - config.optimizeDeps?.esbuildOptions ?? {} - - await init - for (const id in deps) { - const flatId = flattenId(id) - const filePath = (flatIdDeps[flatId] = deps[id]) - const entryContent = fs.readFileSync(filePath, 'utf-8') - let exportsData: ExportsData - try { - exportsData = parse(entryContent) as ExportsData - } catch { - debug( - `Unable to parse dependency: ${id}. Trying again with a JSX transform.` - ) - const transformed = await transformWithEsbuild(entryContent, filePath, { - loader: 'jsx' - }) - // Ensure that optimization won't fail by defaulting '.js' to the JSX parser. - // This is useful for packages such as Gatsby. - esbuildOptions.loader = { - '.js': 'jsx', - ...esbuildOptions.loader + const total = qualifiedIds.length + const maxListed = 5 + const listed = Math.min(total, maxListed) + const extra = Math.max(0, total - maxListed) + const depsString = colors.yellow( + qualifiedIds.slice(0, listed).join(`\n `) + + (extra > 0 ? `\n (...and ${extra} more)` : ``) + ) + if (!asCommand) { + if (!newDeps) { + // This is auto run on server start - let the user know that we are + // pre-optimizing deps + logger.info(colors.green(`Pre-bundling dependencies:\n ${depsString}`)) + logger.info( + `(this will be run only when your dependencies or config have changed)` + ) } - exportsData = parse(transformed.code) as ExportsData + } else { + logger.info(colors.green(`Optimizing dependencies:\n ${depsString}`)) } - for (const { ss, se } of exportsData[0]) { - const exp = entryContent.slice(ss, se) - if (/export\s+\*\s+from/.test(exp)) { - exportsData.hasReExports = true + + // esbuild generates nested directory output with lowest common ancestor base + // this is unpredictable and makes it difficult to analyze entry / output + // mapping. So what we do here is: + // 1. flatten all ids to eliminate slash + // 2. in the plugin, read the entry ourselves as virtual files to retain the + // path. + const flatIdDeps: Record = {} + const idToExports: Record = {} + const flatIdToExports: Record = {} + + const { plugins = [], ...esbuildOptions } = + config.optimizeDeps?.esbuildOptions ?? {} + + await init + for (const id in deps) { + const flatId = flattenId(id) + const filePath = (flatIdDeps[flatId] = deps[id]) + const entryContent = fs.readFileSync(filePath, 'utf-8') + let exportsData: ExportsData + try { + exportsData = parse(entryContent) as ExportsData + } catch { + debug( + `Unable to parse dependency: ${id}. Trying again with a JSX transform.` + ) + const transformed = await transformWithEsbuild(entryContent, filePath, { + loader: 'jsx' + }) + // Ensure that optimization won't fail by defaulting '.js' to the JSX parser. + // This is useful for packages such as Gatsby. + esbuildOptions.loader = { + '.js': 'jsx', + ...esbuildOptions.loader + } + exportsData = parse(transformed.code) as ExportsData + } + for (const { ss, se } of exportsData[0]) { + const exp = entryContent.slice(ss, se) + if (/export\s+\*\s+from/.test(exp)) { + exportsData.hasReExports = true + } } + idToExports[id] = exportsData + flatIdToExports[flatId] = exportsData } - idToExports[id] = exportsData - flatIdToExports[flatId] = exportsData - } - const define: Record = { - 'process.env.NODE_ENV': JSON.stringify(config.mode) - } - for (const key in config.define) { - const value = config.define[key] - define[key] = typeof value === 'string' ? value : JSON.stringify(value) - } + const define: Record = { + 'process.env.NODE_ENV': JSON.stringify(config.mode) + } + for (const key in config.define) { + const value = config.define[key] + define[key] = typeof value === 'string' ? value : JSON.stringify(value) + } - const start = performance.now() - - const result = await build({ - absWorkingDir: process.cwd(), - entryPoints: Object.keys(flatIdDeps), - bundle: true, - format: 'esm', - target: config.build.target || undefined, - external: config.optimizeDeps?.exclude, - logLevel: 'error', - splitting: true, - sourcemap: true, - outdir: cacheDir, - ignoreAnnotations: true, - metafile: true, - define, - plugins: [ - ...plugins, - esbuildDepPlugin(flatIdDeps, flatIdToExports, config, ssr) - ], - ...esbuildOptions - }) - - const meta = result.metafile! - - // the paths in `meta.outputs` are relative to `process.cwd()` - const cacheDirOutputPath = path.relative(process.cwd(), cacheDir) - - for (const id in deps) { - const entry = deps[id] - data.optimized[id] = { - file: normalizePath(path.resolve(cacheDir, flattenId(id) + '.js')), - src: entry, - needsInterop: needsInterop( + const start = performance.now() + + const result = await build({ + absWorkingDir: process.cwd(), + entryPoints: Object.keys(flatIdDeps), + bundle: true, + format: 'esm', + target: config.build.target || undefined, + external: config.optimizeDeps?.exclude, + logLevel: 'error', + splitting: true, + sourcemap: true, + outdir: cacheDir, + ignoreAnnotations: true, + metafile: true, + define, + plugins: [ + ...plugins, + esbuildDepPlugin(flatIdDeps, flatIdToExports, config, ssr) + ], + ...esbuildOptions + }) + + const meta = result.metafile! + + // the paths in `meta.outputs` are relative to `process.cwd()` + const cacheDirOutputPath = path.relative(process.cwd(), cacheDir) + + for (const id in deps) { + const optimizedInfo = data.optimized[id] + optimizedInfo.needsInterop = needsInterop( id, idToExports[id], meta.outputs, cacheDirOutputPath ) + const output = + meta.outputs[path.relative(process.cwd(), optimizedInfo.file)] + if (output) { + // We only need to hash the output.imports in to check for stability, but adding the hash + // and file path gives us a unique hash that may be useful for other things in the future + optimizedInfo.fileHash = getHash( + data.hash + optimizedInfo.file + JSON.stringify(output.imports) + ) + } + } + + // This only runs when missing deps are processed. Previous optimized deps are stable if + // the newly discovered deps don't have common chunks with them. Comparing their fileHash we + // can find out if it is safe to keep the current browser state. If one of the file hashes + // changed, a full page reload is needed + let stableFiles = true + if (currentData) { + for (const dep of Object.keys(currentData.optimized)) { + const currentInfo = currentData.optimized[dep] + const info = data.optimized[dep] + stableFiles &&= + !!info?.fileHash && + !!currentInfo?.fileHash && + info?.fileHash === currentInfo?.fileHash + } + debug(`optimized deps have stable files: ${stableFiles}`) + } + + if (!stableFiles) { + // Overrite individual hashs with the new global browserHash, a full page reload is required + // New deps that ended up with a different hash replaced while doing analysis import are going to + // return a not found so the browser doesn't cache them. And will properly get loaded after the reload + for (const id in deps) { + data.optimized[id].browserHash = data.browserHash + } } + + writeFile(dataPath, JSON.stringify(data, metadataStringifyReplacer, 2)) + + debug(`deps bundled in ${(performance.now() - start).toFixed(2)}ms`) + processing.resolve({ stableFiles }) } +} - writeFile(dataPath, JSON.stringify(data, null, 2)) +export function newProcessingPromise() { + let resolve: (result?: OptimizeDepsResult) => void + const promise = new Promise((_resolve) => { + resolve = _resolve + }) as Promise + return { promise, resolve: resolve! as (result?: OptimizeDepsResult) => void } +} - debug(`deps bundled in ${(performance.now() - start).toFixed(2)}ms`) - return data +// Convert to { id: src } +export function depsFromOptimizedInfo( + depsInfo: Record +) { + return Object.fromEntries( + Object.entries(depsInfo).map((d) => [d[0], d[1].src]) + ) +} + +export function optimizedFilePath(id: string, cacheDir: string) { + return normalizePath(path.resolve(cacheDir, flattenId(id) + '.js')) +} + +function getHash(text: string) { + return createHash('sha256').update(text).digest('hex').substring(0, 8) +} + +export function optimizedBrowserHash( + hash: string, + deps: Record, + missing?: Record +) { + // update browser hash + return getHash( + hash + JSON.stringify(deps) + (missing ? JSON.stringify(missing) : '') + ) +} + +export function isOptimizedDepFile(id: string, config: ResolvedConfig) { + return id.startsWith(config.cacheDir) +} + +export function createIsOptimizedDepUrl(config: ResolvedConfig) { + const { root, cacheDir } = config + + // determine the url prefix of files inside cache directory + const cacheDirRelative = normalizePath(path.relative(root, cacheDir)) + const cacheDirPrefix = cacheDirRelative.startsWith('../') + ? // if the cache directory is outside root, the url prefix would be something + // like '/@fs/absolute/path/to/node_modules/.vite' + `/@fs/${normalizePath(cacheDir).replace(/^\//, '')}` + : // if the cache directory is inside root, the url prefix would be something + // like '/node_modules/.vite' + `/${cacheDirRelative}` + + return cacheDirPrefix + ? function isOptimizedDepUrl(url: string): boolean { + return url.startsWith(cacheDirPrefix) + } + : function isOptimizedDepUrl(url: string): boolean { + return false + } +} + +function metadataStringifyReplacer(key: string, value: any) { + return key !== 'processing' && key !== 'discovered' ? value : undefined } // 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 index 2d0c8b98a99a20..6bd0778befdaad 100644 --- a/packages/vite/src/node/optimizer/registerMissing.ts +++ b/packages/vite/src/node/optimizer/registerMissing.ts @@ -1,5 +1,16 @@ import colors from 'picocolors' -import { optimizeDeps } from '.' +import { + optimizeDeps, + optimizedFilePath, + optimizedBrowserHash, + depsFromOptimizedInfo, + newProcessingPromise +} from '.' +import type { + DepOptimizationMetadata, + OptimizedDepInfo, + OptimizeDepsResult +} from '.' import type { ViteDevServer } from '..' import { resolveSSRExternal } from '../ssr/ssrExternal' @@ -11,21 +22,29 @@ const debounceMs = 100 export function createMissingImporterRegisterFn( server: ViteDevServer -): (id: string, resolved: string, ssr?: boolean) => void { +): (id: string, resolved: string, ssr?: boolean) => OptimizedDepInfo { const { logger } = server.config - let knownOptimized = server._optimizeDepsMetadata!.optimized - let currentMissing: Record = {} + let metadata = server._optimizeDepsMetadata! + let handle: NodeJS.Timeout | undefined + let needFullReload: boolean = false - let pendingResolve: (() => void) | null = null + let processingMissingDeps = newProcessingPromise() async function rerun(ssr: boolean | undefined) { - const newDeps = currentMissing - currentMissing = {} + // debounce time to wait for new missing deps finished, issue a new + // optimization of deps (both old and newly found) once the previous + // optimizeDeps processing is finished + await metadata.processing + + // New deps could have been found here, clear the timeout to already + // consider them in this run + if (handle) clearTimeout(handle) + handle = undefined logger.info( colors.yellow( - `new dependencies found: ${Object.keys(newDeps).join( + `new dependencies found: ${Object.keys(metadata.discovered).join( ', ' )}, updating...` ), @@ -34,50 +53,79 @@ export function createMissingImporterRegisterFn( } ) - for (const id in knownOptimized) { - newDeps[id] = knownOptimized[id].src - } + // All deps, previous known and newly discovered are rebundled, + // respect insertion order to keep the metadata file stable + const newDeps = { ...metadata.optimized, ...metadata.discovered } + const newDepsProcessing = processingMissingDeps + let processingResult: OptimizeDepsResult | undefined + + processingMissingDeps = newProcessingPromise() + + let newData: DepOptimizationMetadata | null = null try { - // Nullify previous metadata so that the resolver won't - // resolve to optimized files during the optimizer re-run - server._isRunningOptimizer = true - server._optimizeDepsMetadata = null + // During optimizer re-run, the resolver may continue to discover + // optimized files. If we directly resolve to node modules there + // is no way to avoid a full-page reload - const newData = (server._optimizeDepsMetadata = await optimizeDeps( + newData = server._optimizeDepsMetadata = await optimizeDeps( server.config, true, false, + metadata, newDeps, - ssr - )) - knownOptimized = newData!.optimized + ssr, + newDepsProcessing + ) + + // While optimizeDeps is running, new missing deps may be discovered, + // in which case they will keep being added to metadata.discovered + for (const o of Object.keys(metadata.discovered)) { + if (!newData.optimized[o] && !newData.discovered[o]) { + newData.discovered[o] = metadata.discovered[o] + delete metadata.discovered[o] + } + } + metadata = newData // update ssr externals if (ssr) { server._ssrExternals = resolveSSRExternal( server.config, - Object.keys(knownOptimized) + Object.keys(metadata.optimized) ) } - logger.info(colors.green(`✨ dependencies updated, reloading page...`), { - timestamp: true - }) + processingResult = await newData!.processing } catch (e) { logger.error( colors.red(`error while updating dependencies:\n${e.stack}`), { timestamp: true, error: e } ) - } finally { - server._isRunningOptimizer = false - if (!handle) { - // No other rerun() pending so resolve and let pending requests proceed - pendingResolve && pendingResolve() - server._pendingReload = pendingResolve = null + fullReload() + return + } + + if (!needFullReload && processingResult?.stableFiles !== false) { + logger.info(colors.green(`✨ new dependencies pre-bundled...`), { + timestamp: true + }) + } else { + logger.info(colors.green(`✨ dependencies updated, reloading page...`), { + timestamp: true + }) + + if (handle) { + // There are newly discovered deps, and another rerun is about to be + // excecuted. Avoid the current full reload, but queue it for the next one + needFullReload = true + } else { + fullReload() } } + } + function fullReload() { // Cached transform results have stale imports (resolved to // old locations) so they need to be invalidated before the page is // reloaded. @@ -87,25 +135,50 @@ export function createMissingImporterRegisterFn( type: 'full-reload', path: '*' }) + + needFullReload = false } return function registerMissingImport( id: string, resolved: string, ssr?: boolean - ) { - if (!knownOptimized[id]) { - currentMissing[id] = resolved - if (handle) clearTimeout(handle) - handle = setTimeout(() => { - handle = undefined - rerun(ssr) - }, debounceMs) - if (!server._pendingReload) { - server._pendingReload = new Promise((r) => { - pendingResolve = r - }) - } + ): OptimizedDepInfo { + const optimized = metadata.optimized[id] + if (optimized) { + return optimized + } + let missing = metadata.discovered[id] + if (missing) { + // We are already discover this dependency, and it will be processed in + // the next rerun call + return missing + } + missing = metadata.discovered[id] = { + file: optimizedFilePath(id, server.config.cacheDir), + src: resolved, + // Assing a browserHash to this missing dependency that is unique to + // the current state of known + missing deps. If the optimizeDeps stage + // ends up with stable paths for the new dep, then we don't need a + // full page reload and this browserHash will be kept + browserHash: optimizedBrowserHash( + server._optimizeDepsMetadata!.hash, + depsFromOptimizedInfo(metadata.optimized), + depsFromOptimizedInfo(metadata.discovered) + ), + // loading of this pre-bundle dep needs to await for its processing + // promise to be resolved + processing: processingMissingDeps.promise } + + if (handle) clearTimeout(handle) + handle = setTimeout(() => { + handle = undefined + rerun(ssr) + }, debounceMs) + + // Return the path for the optimized bundle, this path is known before + // esbuild is run to generate the pre-bundle + return missing } } diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index 953304e0ac38c2..88b96de6f38349 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -45,6 +45,7 @@ import { makeLegalIdentifier } from '@rollup/pluginutils' import { shouldExternalizeForSSR } from '../ssr/ssrExternal' import { performance } from 'perf_hooks' import { transformRequest } from '../server/transformRequest' +import { isOptimizedDepFile, createIsOptimizedDepUrl } from '../optimizer' const isDebug = !!process.env.DEBUG const debug = createDebugger('vite:import-analysis') @@ -54,6 +55,8 @@ const clientDir = normalizePath(CLIENT_DIR) const skipRE = /\.(map|json)$/ const canSkip = (id: string) => skipRE.test(id) || isDirectCSSRequest(id) +const optimizedDepChunkRE = /\/chunk-[A-Z0-9]{8}\.js/ + function isExplicitImportRequired(url: string) { return !isJSRequest(cleanUrl(url)) && !isCSSRequest(url) } @@ -99,12 +102,14 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { const clientPublicPath = path.posix.join(base, CLIENT_PUBLIC_PATH) let server: ViteDevServer + let isOptimizedDepUrl: (url: string) => boolean return { name: 'vite:import-analysis', configureServer(_server) { server = _server + isOptimizedDepUrl = createIsOptimizedDepUrl(server.config) }, async transform(source, importer, options) { @@ -244,11 +249,16 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // mark non-js/css imports with `?import` url = markExplicitImport(url) + // If the url isn't a request for a pre-bundled common chunk, // for relative js/css imports, or self-module virtual imports // (e.g. vue blocks), inherit importer's version query // do not do this for unknown type imports, otherwise the appended // query can break 3rd party plugin's extension checks. - if ((isRelative || isSelfImport) && !/[\?&]import=?\b/.test(url)) { + if ( + (isRelative || isSelfImport) && + !(isOptimizedDepUrl(url) && optimizedDepChunkRE.test(url)) && + !/[\?&]import=?\b/.test(url) + ) { const versionMatch = importer.match(DEP_VERSION_RE) if (versionMatch) { url = injectQuery(url, versionMatch[1]) @@ -396,7 +406,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { specifier, start ) - let url = normalizedUrl + const url = normalizedUrl // record as safe modules server?.moduleGraph.safeModulesPath.add( @@ -405,28 +415,46 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // rewrite if (url !== specifier) { - // for optimized cjs deps, support named imports by rewriting named - // imports to const assignments. - if (resolvedId.endsWith(`&es-interop`)) { - url = url.slice(0, -11) - if (isDynamicImport) { - // rewrite `import('package')` to expose the default directly - str().overwrite( - dynamicIndex, - end + 1, - `import('${url}').then(m => m.default && m.default.__esModule ? m.default : ({ ...m.default, default: m.default }))` - ) - } else { - const exp = source.slice(expStart, expEnd) - const rewritten = transformCjsImport(exp, url, rawUrl, index) - if (rewritten) { - str().overwrite(expStart, expEnd, rewritten) + let rewriteDone = false + if (isOptimizedDepFile(resolvedId, config)) { + // for optimized cjs deps, support named imports by rewriting named + // imports to const assignments. + const optimizeDepsMetadata = server._optimizeDepsMetadata! + const { optimized } = optimizeDepsMetadata + + // The browserHash in resolvedId could be stale in which case there will be a full + // page reload. We could return a 404 in that case but it is safe to return the request + const file = cleanUrl(resolvedId) // Remove ?v={hash} + const dep = Object.keys(optimized).find( + (k) => optimized[k].file === file + ) + + // Wait until the dependency has been pre-bundled + dep && (await optimized[dep].processing) + + if (dep && optimized[dep].needsInterop) { + debug(`${dep} needs interop`) + if (isDynamicImport) { + // rewrite `import('package')` to expose the default directly + str().overwrite( + dynamicIndex, + end + 1, + `import('${url}').then(m => m.default && m.default.__esModule ? m.default : ({ ...m.default, default: m.default }))` + ) } else { - // #1439 export * from '...' - str().overwrite(start, end, url) + const exp = source.slice(expStart, expEnd) + const rewritten = transformCjsImport(exp, url, rawUrl, index) + if (rewritten) { + str().overwrite(expStart, expEnd, rewritten) + } else { + // #1439 export * from '...' + str().overwrite(start, end, url) + } } + rewriteDone = true } - } else { + } + if (!rewriteDone) { str().overwrite(start, end, isDynamicImport ? `'${url}'` : url) } } diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index e4f3d453527af3..c9aa26329bc0b1 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -3,6 +3,7 @@ import type { Plugin } from '../plugin' import aliasPlugin from '@rollup/plugin-alias' import { jsonPlugin } from './json' import { resolvePlugin } from './resolve' +import { optimizedDepsPlugin } from './optimizedDeps' import { esbuildPlugin } from './esbuild' import { importAnalysisPlugin } from './importAnalysis' import { cssPlugin, cssPostPlugin } from './css' @@ -45,6 +46,7 @@ export async function resolvePlugins( ssrConfig: config.ssr, asSrc: true }), + optimizedDepsPlugin(), htmlInlineProxyPlugin(config), cssPlugin(config), config.esbuild !== false ? esbuildPlugin(config.esbuild) : null, diff --git a/packages/vite/src/node/plugins/optimizedDeps.ts b/packages/vite/src/node/plugins/optimizedDeps.ts new file mode 100644 index 00000000000000..efe343dbc971f3 --- /dev/null +++ b/packages/vite/src/node/plugins/optimizedDeps.ts @@ -0,0 +1,110 @@ +import { promises as fs } from 'fs' +import type { Plugin } from '../plugin' +import colors from 'picocolors' +import { cleanUrl, createDebugger } from '../utils' +import { isOptimizedDepFile } from '../optimizer' +import type { DepOptimizationMetadata, OptimizedDepInfo } from '../optimizer' +import type { ViteDevServer } from '..' + +export const ERROR_CODE_OPTIMIZE_DEPS_TIMEOUT = 'ERR_OPTIMIZE_DEPS_TIMEOUT' +export const ERROR_CODE_OPTIMIZE_DEPS_OUTDATED = 'ERR_OPTIMIZE_DEPS_OUTDATED' + +const isDebug = process.env.DEBUG +const debug = createDebugger('vite:optimize-deps') + +export function optimizedDepsPlugin(): Plugin { + let server: ViteDevServer | undefined + + return { + name: 'vite:optimized-deps', + + configureServer(_server) { + server = _server + }, + + apply: 'serve', + + async load(id) { + if (server && isOptimizedDepFile(id, server.config)) { + const metadata = server?._optimizeDepsMetadata + if (metadata) { + const file = cleanUrl(id) + const info = optimizeDepInfoFromFile(metadata, file) + if (info) { + try { + // This is an entry point, it may still not be bundled + await info.processing + } catch { + // If the refresh has not happened after timeout, Vite considers + // something unexpected has happened. In this case, Vite + // returns an empty response that will error. + optimizeDepsTimeout(id) + return + } + const newMetadata = server._optimizeDepsMetadata + if (metadata !== newMetadata) { + const currentInfo = optimizeDepInfoFromFile(newMetadata!, file) + if (info.browserHash !== currentInfo?.browserHash) { + outdatedTimeout(id) + } + } + } + isDebug && debug(`load ${colors.cyan(file)}`) + // Load the file from the cache instead of waiting for other plugin + // load hooks to avoid race conditions, once processing is resolved, + // we are sure that the file has been properly save to disk + try { + return await fs.readFile(file, 'utf-8') + } catch (e) { + // Outdated non-entry points (CHUNK), loaded after a rerun + outdatedTimeout(id) + } + } + } + } + } +} + +function optimizeDepsTimeout(id: string) { + const err: any = new Error( + `Something unexpected happened while optimizing "${id}". ` + + `The current page should have reloaded by now` + ) + err.code = ERROR_CODE_OPTIMIZE_DEPS_TIMEOUT + // This error will be catched by the transform middleware that will + // send a 408 (request timeout) response to the browser + throw new Error(err) +} + +function outdatedTimeout(id: string) { + const err: any = new Error( + `There is a new version of the pre-bundle for "${id}", ` + + `a page reload is going to ask for it.` + ) + err.code = ERROR_CODE_OPTIMIZE_DEPS_OUTDATED + // This error will be catched by the transform middleware that will + // send a 408 (request timeout) response to the browser + throw new Error(err) +} + +function optimizeDepInfoFromFile( + metadata: DepOptimizationMetadata, + file: string +): OptimizedDepInfo | undefined { + return ( + findFileInfo(metadata.optimized, file) || + findFileInfo(metadata.discovered, file) + ) +} + +function findFileInfo( + dependenciesInfo: Record, + file: string +): OptimizedDepInfo | undefined { + for (const o of Object.keys(dependenciesInfo)) { + const info = dependenciesInfo[o] + if (info.file === file) { + return info + } + } +} diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index c98ba473aa9075..de723b361db8c5 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -29,6 +29,8 @@ import { isPossibleTsOutput, getTsSrcPath } from '../utils' +import { createIsOptimizedDepUrl } from '../optimizer' +import type { OptimizedDepInfo } from '../optimizer' import type { ViteDevServer, SSROptions } from '..' import type { PartialResolvedId } from 'rollup' import { resolve as _resolveExports } from 'resolve.exports' @@ -87,6 +89,7 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin { preferRelative = false } = baseOptions let server: ViteDevServer | undefined + let isOptimizedDepUrl: (url: string) => boolean const { target: ssrTarget, noExternal: ssrNoExternal } = ssrConfig ?? {} @@ -95,6 +98,7 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin { configureServer(_server) { server = _server + isOptimizedDepUrl = createIsOptimizedDepUrl(server.config) }, resolveId(id, importer, resolveOpts) { @@ -123,6 +127,15 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin { let res: string | PartialResolvedId | undefined + // resolve pre-bundled deps requests, these could be resolved by + // tryFileResolve or /fs/ resolution but these files may not yet + // exists if we are in the middle of a deps re-processing + if (asSrc && isOptimizedDepUrl?.(id)) { + return id.startsWith(FS_PREFIX) + ? fsPathFromId(id) + : path.resolve(root, id.slice(1)) + } + // explicit fs paths that starts with /@fs/* if (asSrc && id.startsWith(FS_PREFIX)) { const fsPath = fsPathFromId(id) @@ -568,8 +581,7 @@ export function tryNodeResolve( if ( !resolved.includes('node_modules') || // linked !server || // build - server._isRunningOptimizer || // optimizing - !server._optimizeDepsMetadata + !server._registerMissingImport // initial esbuild scan phase ) { return { id: resolved } } @@ -589,19 +601,24 @@ export function tryNodeResolve( // can cache it without re-validation, but only do so for known js types. // otherwise we may introduce duplicated modules for externalized files // from pre-bundled deps. - const versionHash = server._optimizeDepsMetadata?.browserHash + + const versionHash = server._optimizeDepsMetadata?.hash if (versionHash && isJsType) { resolved = injectQuery(resolved, `v=${versionHash}`) } } else { - // this is a missing import. - // queue optimize-deps re-run. - server._registerMissingImport?.(id, resolved, ssr) + // this is a missing import, queue optimize-deps re-run and + // get a resolved its optmized info + const optimizedInfo = server._registerMissingImport!(id, resolved, ssr) + resolved = getOptimizedUrl(optimizedInfo) } - return { id: resolved } + return { id: resolved! } } } +const getOptimizedUrl = (optimizedData: OptimizedDepInfo) => + `${optimizedData.file}?v=${optimizedData.browserHash}` + export function tryOptimizedResolve( id: string, server: ViteDevServer, @@ -611,15 +628,6 @@ export function tryOptimizedResolve( if (!depData) return - const getOptimizedUrl = (optimizedData: typeof depData.optimized[string]) => { - return ( - optimizedData.file + - `?v=${depData.browserHash}${ - optimizedData.needsInterop ? `&es-interop` : `` - }` - ) - } - // check if id has been optimized const isOptimized = depData.optimized[id] if (isOptimized) { diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index aeea862dbc173f..72484afb524d4d 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -44,7 +44,7 @@ import { transformRequest } from './transformRequest' import type { ESBuildTransformResult } from '../plugins/esbuild' import { transformWithEsbuild } from '../plugins/esbuild' import type { TransformOptions as EsbuildTransformOptions } from 'esbuild' -import type { DepOptimizationMetadata } from '../optimizer' +import type { DepOptimizationMetadata, OptimizedDepInfo } from '../optimizer' import { optimizeDeps } from '../optimizer' import { ssrLoadModule } from '../ssr/ssrModuleLoader' import { resolveSSRExternal } from '../ssr/ssrExternal' @@ -277,20 +277,16 @@ export interface ViteDevServer { * @internal */ _forceOptimizeOnRestart: boolean - /** - * @internal - */ - _isRunningOptimizer: boolean /** * @internal */ _registerMissingImport: - | ((id: string, resolved: string, ssr: boolean | undefined) => void) + | (( + id: string, + resolved: string, + ssr: boolean | undefined + ) => OptimizedDepInfo) | null - /** - * @internal - */ - _pendingReload: Promise | null /** * @internal */ @@ -361,12 +357,17 @@ export async function createServer( return transformRequest(url, server, options) }, transformIndexHtml: null!, // to be immediately set - ssrLoadModule(url) { + async ssrLoadModule(url) { + let configFileDependencies: string[] = [] + const optimizeDepsMetadata = server._optimizeDepsMetadata + if (optimizeDepsMetadata) { + await optimizeDepsMetadata.processing + configFileDependencies = Object.keys(optimizeDepsMetadata.optimized) + } + server._ssrExternals ||= resolveSSRExternal( config, - server._optimizeDepsMetadata - ? Object.keys(server._optimizeDepsMetadata.optimized) - : [] + configFileDependencies ) return ssrLoadModule(url, server) }, @@ -416,9 +417,7 @@ export async function createServer( _globImporters: Object.create(null), _restartPromise: null, _forceOptimizeOnRestart: false, - _isRunningOptimizer: false, _registerMissingImport: null, - _pendingReload: null, _pendingRequests: new Map() } @@ -561,15 +560,18 @@ export async function createServer( middlewares.use(errorMiddleware(server, !!middlewareMode)) const runOptimize = async () => { - server._isRunningOptimizer = true - try { - server._optimizeDepsMetadata = await optimizeDeps( - config, - config.server.force || server._forceOptimizeOnRestart - ) - } finally { - server._isRunningOptimizer = false + server._optimizeDepsMetadata = await optimizeDeps( + config, + config.server.force || server._forceOptimizeOnRestart + ) + + if (server.config?.optimizeDeps?.holdBackServerStart) { + await server._optimizeDepsMetadata?.processing } + + // While running the first optimizeDeps, _registerMissingImport is null + // so the resolve plugin resolves straight to node_modules during the + // deps discovery scan phase server._registerMissingImport = createMissingImporterRegisterFn(server) } diff --git a/packages/vite/src/node/server/middlewares/transform.ts b/packages/vite/src/node/server/middlewares/transform.ts index 15f2355e0e389c..40e00c56fed5fe 100644 --- a/packages/vite/src/node/server/middlewares/transform.ts +++ b/packages/vite/src/node/server/middlewares/transform.ts @@ -1,4 +1,3 @@ -import path from 'path' import type { ViteDevServer } from '..' import type { Connect } from 'types/connect' import { @@ -17,22 +16,17 @@ import { send } from '../send' import { transformRequest } from '../transformRequest' import { isHTMLProxy } from '../../plugins/html' import colors from 'picocolors' -import { - CLIENT_PUBLIC_PATH, - DEP_VERSION_RE, - NULL_BYTE_PLACEHOLDER -} from '../../constants' +import { DEP_VERSION_RE, NULL_BYTE_PLACEHOLDER } from '../../constants' import { isCSSRequest, isDirectCSSRequest, isDirectRequest } from '../../plugins/css' - -/** - * Time (ms) Vite has to full-reload the page before returning - * an empty response. - */ -const NEW_DEPENDENCY_BUILD_TIMEOUT = 1000 +import { + ERROR_CODE_OPTIMIZE_DEPS_TIMEOUT, + ERROR_CODE_OPTIMIZE_DEPS_OUTDATED +} from '../../plugins/optimizedDeps' +import { createIsOptimizedDepUrl } from '../../optimizer' const debugCache = createDebugger('vite:cache') const isDebug = !!process.env.DEBUG @@ -43,19 +37,11 @@ export function transformMiddleware( server: ViteDevServer ): Connect.NextHandleFunction { const { - config: { root, logger, cacheDir }, + config: { root, logger }, moduleGraph } = server - // determine the url prefix of files inside cache directory - const cacheDirRelative = normalizePath(path.relative(root, cacheDir)) - const cacheDirPrefix = cacheDirRelative.startsWith('../') - ? // if the cache directory is outside root, the url prefix would be something - // like '/@fs/absolute/path/to/node_modules/.vite' - `/@fs/${normalizePath(cacheDir).replace(/^\//, '')}` - : // if the cache directory is inside root, the url prefix would be something - // like '/node_modules/.vite' - `/${cacheDirRelative}` + const isOptimizedDepUrl = createIsOptimizedDepUrl(server.config) // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...` return async function viteTransformMiddleware(req, res, next) { @@ -63,36 +49,6 @@ export function transformMiddleware( return next() } - if ( - server._pendingReload && - // always allow vite client requests so that it can trigger page reload - !req.url?.startsWith(CLIENT_PUBLIC_PATH) && - !req.url?.includes('vite/dist/client') - ) { - try { - // missing dep pending reload, hold request until reload happens - await Promise.race([ - server._pendingReload, - // If the refresh has not happened after timeout, Vite considers - // something unexpected has happened. In this case, Vite - // returns an empty response that will error. - new Promise((_, reject) => - setTimeout(reject, NEW_DEPENDENCY_BUILD_TIMEOUT) - ) - ]) - } catch { - // Don't do anything if response has already been sent - if (!res.writableEnded) { - // status code request timeout - res.statusCode = 408 - res.end( - `

[vite] Something unexpected happened while optimizing "${req.url}"

` + - `

The current page should have reloaded by now

` - ) - } - return - } - } let url: string try { url = decodeURI(removeTimestampQuery(req.url!)).replace( @@ -179,9 +135,7 @@ export function transformMiddleware( }) if (result) { const type = isDirectCSSRequest(url) ? 'css' : 'js' - const isDep = - DEP_VERSION_RE.test(url) || - (cacheDirPrefix && url.startsWith(cacheDirPrefix)) + const isDep = DEP_VERSION_RE.test(url) || isOptimizedDepUrl(url) return send(req, res, result.code, type, { etag: result.etag, // allow browser to cache npm deps! @@ -192,6 +146,24 @@ export function transformMiddleware( } } } catch (e) { + if (e?.code === ERROR_CODE_OPTIMIZE_DEPS_TIMEOUT) { + if (!res.writableEnded) { + // Don't do anything if response has already been sent + res.statusCode = 504 // status code request timeout + res.end() + } + logger.error(e.message) + return + } + if (e?.code === ERROR_CODE_OPTIMIZE_DEPS_OUTDATED) { + if (!res.writableEnded) { + // Don't do anything if response has already been sent + res.statusCode = 504 // status code request timeout + res.end() + } + logger.error(e.message) + return + } return next(e) } From 3b3d36597f5379890e98b5cfe29abbf0a63b4687 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 4 Feb 2022 23:03:19 +0100 Subject: [PATCH 02/24] fix: normalize path for windows --- packages/vite/src/node/plugins/resolve.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index de723b361db8c5..b4d431e135ec75 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -133,7 +133,7 @@ export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin { if (asSrc && isOptimizedDepUrl?.(id)) { return id.startsWith(FS_PREFIX) ? fsPathFromId(id) - : path.resolve(root, id.slice(1)) + : normalizePath(ensureVolumeInPath(path.resolve(root, id.slice(1)))) } // explicit fs paths that starts with /@fs/* From ee96c174c37abd3249a88a589648b271df611c32 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Sat, 5 Feb 2022 07:33:21 +0100 Subject: [PATCH 03/24] fix: isOptimizedDepFile missing cacheDir normalize path --- packages/vite/src/node/optimizer/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 0219e3afafa4a2..61d5fef5d913a3 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -479,7 +479,7 @@ export function optimizedBrowserHash( } export function isOptimizedDepFile(id: string, config: ResolvedConfig) { - return id.startsWith(config.cacheDir) + return id.startsWith(normalizePath(config.cacheDir)) } export function createIsOptimizedDepUrl(config: ResolvedConfig) { From b642546ebcdc86890645e0c198a286aa115f29db Mon Sep 17 00:00:00 2001 From: patak-dev Date: Tue, 8 Feb 2022 10:39:53 +0100 Subject: [PATCH 04/24] chore: OptimizeDepsProcessing type --- packages/vite/src/node/index.ts | 3 ++- packages/vite/src/node/optimizer/index.ts | 14 ++++++++------ .../vite/src/node/optimizer/registerMissing.ts | 6 +++--- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index f22c14687e478f..c966cc23f6fd34 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -34,7 +34,8 @@ export type { DepOptimizationMetadata, DepOptimizationOptions, OptimizedDepInfo, - OptimizeDepsResult + OptimizeDepsResult, + OptimizeDepsProcessing } from './optimizer' export type { Plugin } from './plugin' export type { PackageCache, PackageData } from './packages' diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 61d5fef5d913a3..100bff71d297ae 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -97,6 +97,11 @@ export interface OptimizeDepsResult { stableFiles: boolean } +export interface OptimizeDepsProcessing { + promise: Promise + resolve: (result?: OptimizeDepsResult) => void +} + export interface OptimizedDepInfo { file: string src: string @@ -144,10 +149,7 @@ export async function optimizeDeps( currentData: DepOptimizationMetadata | null = null, newDeps?: Record, // missing imports encountered after server has started ssr?: boolean, - processing: { - promise: Promise - resolve: (result?: OptimizeDepsResult) => void - } = newProcessingPromise() + processing: OptimizeDepsProcessing = newOptimizeDepsProcessingPromise() ): Promise { config = { ...config, @@ -442,12 +444,12 @@ export async function optimizeDeps( } } -export function newProcessingPromise() { +export function newOptimizeDepsProcessingPromise(): OptimizeDepsProcessing { let resolve: (result?: OptimizeDepsResult) => void const promise = new Promise((_resolve) => { resolve = _resolve }) as Promise - return { promise, resolve: resolve! as (result?: OptimizeDepsResult) => void } + return { promise, resolve: resolve! } } // Convert to { id: src } diff --git a/packages/vite/src/node/optimizer/registerMissing.ts b/packages/vite/src/node/optimizer/registerMissing.ts index 6bd0778befdaad..f4f01e7b5d306d 100644 --- a/packages/vite/src/node/optimizer/registerMissing.ts +++ b/packages/vite/src/node/optimizer/registerMissing.ts @@ -4,7 +4,7 @@ import { optimizedFilePath, optimizedBrowserHash, depsFromOptimizedInfo, - newProcessingPromise + newOptimizeDepsProcessingPromise } from '.' import type { DepOptimizationMetadata, @@ -29,7 +29,7 @@ export function createMissingImporterRegisterFn( let handle: NodeJS.Timeout | undefined let needFullReload: boolean = false - let processingMissingDeps = newProcessingPromise() + let processingMissingDeps = newOptimizeDepsProcessingPromise() async function rerun(ssr: boolean | undefined) { // debounce time to wait for new missing deps finished, issue a new @@ -59,7 +59,7 @@ export function createMissingImporterRegisterFn( const newDepsProcessing = processingMissingDeps let processingResult: OptimizeDepsResult | undefined - processingMissingDeps = newProcessingPromise() + processingMissingDeps = newOptimizeDepsProcessingPromise() let newData: DepOptimizationMetadata | null = null From a43878f90dc44a0ff40340e16b7a5e6a54410f9b Mon Sep 17 00:00:00 2001 From: patak-dev Date: Tue, 8 Feb 2022 10:58:38 +0100 Subject: [PATCH 05/24] chore: break early for stableFiles logic --- packages/vite/src/node/optimizer/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 100bff71d297ae..20b860b40d39a1 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -417,14 +417,15 @@ export async function optimizeDeps( // changed, a full page reload is needed let stableFiles = true if (currentData) { - for (const dep of Object.keys(currentData.optimized)) { + stableFiles = Object.keys(currentData.optimized).every((dep) => { const currentInfo = currentData.optimized[dep] const info = data.optimized[dep] - stableFiles &&= + return ( !!info?.fileHash && !!currentInfo?.fileHash && info?.fileHash === currentInfo?.fileHash - } + ) + }) debug(`optimized deps have stable files: ${stableFiles}`) } From b47641ba2ef4cdfb1a9d0899e045628e4dafdf51 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Tue, 8 Feb 2022 11:07:14 +0100 Subject: [PATCH 06/24] chore: use getXXX naming --- packages/vite/src/node/optimizer/index.ts | 10 +++++----- packages/vite/src/node/optimizer/registerMissing.ts | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 20b860b40d39a1..853c994e38e48e 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -243,7 +243,7 @@ export async function optimizeDeps( } // update browser hash - data.browserHash = optimizedBrowserHash(data.hash, deps) + data.browserHash = getOptimizedBrowserHash(data.hash, deps) // We generate the mapping of dependency ids to their cache file location // before processing the dependencies with esbuild. This allow us to continue @@ -251,7 +251,7 @@ export async function optimizeDeps( for (const id in deps) { const entry = deps[id] data.optimized[id] = { - file: optimizedFilePath(id, cacheDir), + file: getOptimizedFilePath(id, cacheDir), src: entry, browserHash: data.browserHash, processing: processing.promise @@ -269,7 +269,7 @@ export async function optimizeDeps( // update global browser hash, but keep newDeps individual hashs until we know // if files are stable so we can avoid a full page reload - data.browserHash = optimizedBrowserHash(data.hash, deps) + data.browserHash = getOptimizedBrowserHash(data.hash, deps) } // We prebundle dependencies with esbuild and cache them, but there is no need @@ -462,7 +462,7 @@ export function depsFromOptimizedInfo( ) } -export function optimizedFilePath(id: string, cacheDir: string) { +export function getOptimizedFilePath(id: string, cacheDir: string) { return normalizePath(path.resolve(cacheDir, flattenId(id) + '.js')) } @@ -470,7 +470,7 @@ function getHash(text: string) { return createHash('sha256').update(text).digest('hex').substring(0, 8) } -export function optimizedBrowserHash( +export function getOptimizedBrowserHash( hash: string, deps: Record, missing?: Record diff --git a/packages/vite/src/node/optimizer/registerMissing.ts b/packages/vite/src/node/optimizer/registerMissing.ts index f4f01e7b5d306d..a710d469a4a9c6 100644 --- a/packages/vite/src/node/optimizer/registerMissing.ts +++ b/packages/vite/src/node/optimizer/registerMissing.ts @@ -1,8 +1,8 @@ import colors from 'picocolors' import { optimizeDeps, - optimizedFilePath, - optimizedBrowserHash, + getOptimizedFilePath, + getOptimizedBrowserHash, depsFromOptimizedInfo, newOptimizeDepsProcessingPromise } from '.' @@ -155,13 +155,13 @@ export function createMissingImporterRegisterFn( return missing } missing = metadata.discovered[id] = { - file: optimizedFilePath(id, server.config.cacheDir), + file: getOptimizedFilePath(id, server.config.cacheDir), src: resolved, // Assing a browserHash to this missing dependency that is unique to // the current state of known + missing deps. If the optimizeDeps stage // ends up with stable paths for the new dep, then we don't need a // full page reload and this browserHash will be kept - browserHash: optimizedBrowserHash( + browserHash: getOptimizedBrowserHash( server._optimizeDepsMetadata!.hash, depsFromOptimizedInfo(metadata.optimized), depsFromOptimizedInfo(metadata.discovered) From c7fdb42beaab83dadf8408d6ba1afd50abf767b6 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Tue, 8 Feb 2022 11:15:09 +0100 Subject: [PATCH 07/24] chore: refactor stableFiles to alteredFiles --- packages/vite/src/node/optimizer/index.ts | 18 +++++++++--------- .../vite/src/node/optimizer/registerMissing.ts | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 853c994e38e48e..bc26608f9f4cc2 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -94,7 +94,7 @@ export interface OptimizeDepsResult { * If the files are stable, we can avoid the reload that is expensive * for large applications */ - stableFiles: boolean + alteredFiles: boolean } export interface OptimizeDepsProcessing { @@ -415,21 +415,21 @@ export async function optimizeDeps( // the newly discovered deps don't have common chunks with them. Comparing their fileHash we // can find out if it is safe to keep the current browser state. If one of the file hashes // changed, a full page reload is needed - let stableFiles = true + let alteredFiles = false if (currentData) { - stableFiles = Object.keys(currentData.optimized).every((dep) => { + alteredFiles = Object.keys(currentData.optimized).some((dep) => { const currentInfo = currentData.optimized[dep] const info = data.optimized[dep] return ( - !!info?.fileHash && - !!currentInfo?.fileHash && - info?.fileHash === currentInfo?.fileHash + !info?.fileHash || + !currentInfo?.fileHash || + info?.fileHash !== currentInfo?.fileHash ) }) - debug(`optimized deps have stable files: ${stableFiles}`) + debug(`optimized deps have altered files: ${alteredFiles}`) } - if (!stableFiles) { + if (alteredFiles) { // Overrite individual hashs with the new global browserHash, a full page reload is required // New deps that ended up with a different hash replaced while doing analysis import are going to // return a not found so the browser doesn't cache them. And will properly get loaded after the reload @@ -441,7 +441,7 @@ export async function optimizeDeps( writeFile(dataPath, JSON.stringify(data, metadataStringifyReplacer, 2)) debug(`deps bundled in ${(performance.now() - start).toFixed(2)}ms`) - processing.resolve({ stableFiles }) + processing.resolve({ alteredFiles }) } } diff --git a/packages/vite/src/node/optimizer/registerMissing.ts b/packages/vite/src/node/optimizer/registerMissing.ts index a710d469a4a9c6..5e7c8f0d639afb 100644 --- a/packages/vite/src/node/optimizer/registerMissing.ts +++ b/packages/vite/src/node/optimizer/registerMissing.ts @@ -106,7 +106,7 @@ export function createMissingImporterRegisterFn( return } - if (!needFullReload && processingResult?.stableFiles !== false) { + if (!needFullReload && processingResult?.alteredFiles) { logger.info(colors.green(`✨ new dependencies pre-bundled...`), { timestamp: true }) From d71989ac1232b3adfa4872459b53801faba788a3 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Tue, 8 Feb 2022 11:37:06 +0100 Subject: [PATCH 08/24] chore: correct alteredFiles condition --- packages/vite/src/node/optimizer/registerMissing.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/optimizer/registerMissing.ts b/packages/vite/src/node/optimizer/registerMissing.ts index 5e7c8f0d639afb..5613c481d315a8 100644 --- a/packages/vite/src/node/optimizer/registerMissing.ts +++ b/packages/vite/src/node/optimizer/registerMissing.ts @@ -106,7 +106,7 @@ export function createMissingImporterRegisterFn( return } - if (!needFullReload && processingResult?.alteredFiles) { + if (!needFullReload && !processingResult?.alteredFiles) { logger.info(colors.green(`✨ new dependencies pre-bundled...`), { timestamp: true }) From 40c44572e315b3c416d4848cb2b2a02cf95c1982 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Tue, 8 Feb 2022 21:05:20 +0100 Subject: [PATCH 09/24] fix: avoid throwing an error when on outdated dep request --- .../vite/src/node/plugins/optimizedDeps.ts | 19 ++++++++++--------- .../src/node/server/middlewares/transform.ts | 16 +++++++++++----- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/packages/vite/src/node/plugins/optimizedDeps.ts b/packages/vite/src/node/plugins/optimizedDeps.ts index efe343dbc971f3..c8f15133c2a610 100644 --- a/packages/vite/src/node/plugins/optimizedDeps.ts +++ b/packages/vite/src/node/plugins/optimizedDeps.ts @@ -6,8 +6,9 @@ import { isOptimizedDepFile } from '../optimizer' import type { DepOptimizationMetadata, OptimizedDepInfo } from '../optimizer' import type { ViteDevServer } from '..' -export const ERROR_CODE_OPTIMIZE_DEPS_TIMEOUT = 'ERR_OPTIMIZE_DEPS_TIMEOUT' -export const ERROR_CODE_OPTIMIZE_DEPS_OUTDATED = 'ERR_OPTIMIZE_DEPS_OUTDATED' +export const ERR_OPTIMIZE_DEPS_PROCESSING_ERROR = + 'ERR_OPTIMIZE_DEPS_PROCESSING_ERROR' +export const ERR_OUTDATED_OPTIMIZED_DEP = 'ERR_OUTDATED_OPTIMIZED_DEP' const isDebug = process.env.DEBUG const debug = createDebugger('vite:optimize-deps') @@ -38,14 +39,14 @@ export function optimizedDepsPlugin(): Plugin { // If the refresh has not happened after timeout, Vite considers // something unexpected has happened. In this case, Vite // returns an empty response that will error. - optimizeDepsTimeout(id) + throwProcessingError(id) return } const newMetadata = server._optimizeDepsMetadata if (metadata !== newMetadata) { const currentInfo = optimizeDepInfoFromFile(newMetadata!, file) if (info.browserHash !== currentInfo?.browserHash) { - outdatedTimeout(id) + throwOutdatedRequest(id) } } } @@ -57,7 +58,7 @@ export function optimizedDepsPlugin(): Plugin { return await fs.readFile(file, 'utf-8') } catch (e) { // Outdated non-entry points (CHUNK), loaded after a rerun - outdatedTimeout(id) + throwOutdatedRequest(id) } } } @@ -65,23 +66,23 @@ export function optimizedDepsPlugin(): Plugin { } } -function optimizeDepsTimeout(id: string) { +function throwProcessingError(id: string) { const err: any = new Error( `Something unexpected happened while optimizing "${id}". ` + `The current page should have reloaded by now` ) - err.code = ERROR_CODE_OPTIMIZE_DEPS_TIMEOUT + err.code = ERR_OPTIMIZE_DEPS_PROCESSING_ERROR // This error will be catched by the transform middleware that will // send a 408 (request timeout) response to the browser throw new Error(err) } -function outdatedTimeout(id: string) { +function throwOutdatedRequest(id: string) { const err: any = new Error( `There is a new version of the pre-bundle for "${id}", ` + `a page reload is going to ask for it.` ) - err.code = ERROR_CODE_OPTIMIZE_DEPS_OUTDATED + err.code = ERR_OUTDATED_OPTIMIZED_DEP // This error will be catched by the transform middleware that will // send a 408 (request timeout) response to the browser throw new Error(err) diff --git a/packages/vite/src/node/server/middlewares/transform.ts b/packages/vite/src/node/server/middlewares/transform.ts index 40e00c56fed5fe..84da8cf92a951d 100644 --- a/packages/vite/src/node/server/middlewares/transform.ts +++ b/packages/vite/src/node/server/middlewares/transform.ts @@ -23,8 +23,8 @@ import { isDirectRequest } from '../../plugins/css' import { - ERROR_CODE_OPTIMIZE_DEPS_TIMEOUT, - ERROR_CODE_OPTIMIZE_DEPS_OUTDATED + ERR_OPTIMIZE_DEPS_PROCESSING_ERROR, + ERR_OUTDATED_OPTIMIZED_DEP } from '../../plugins/optimizedDeps' import { createIsOptimizedDepUrl } from '../../optimizer' @@ -146,22 +146,28 @@ export function transformMiddleware( } } } catch (e) { - if (e?.code === ERROR_CODE_OPTIMIZE_DEPS_TIMEOUT) { + if (e?.code === ERR_OPTIMIZE_DEPS_PROCESSING_ERROR) { if (!res.writableEnded) { // Don't do anything if response has already been sent res.statusCode = 504 // status code request timeout res.end() } + // This timeout is unexpected logger.error(e.message) return } - if (e?.code === ERROR_CODE_OPTIMIZE_DEPS_OUTDATED) { + if (e?.code === ERR_OUTDATED_OPTIMIZED_DEP) { if (!res.writableEnded) { // Don't do anything if response has already been sent res.statusCode = 504 // status code request timeout res.end() } - logger.error(e.message) + // We don't need to log an error in this case, the request + // is outdated because new dependencies were discovered and + // the new pre-bundle dependendencies have changed. + // A full-page reload has been issued, and these old requests + // can't be properly fullfilled. This isn't an unexpected + // error but a normal part of the missing deps discovery flow return } return next(e) From 6ecafb0d68484240ee7578f2ac8362f5365e1f3d Mon Sep 17 00:00:00 2001 From: patak-dev Date: Tue, 8 Feb 2022 22:40:10 +0100 Subject: [PATCH 10/24] fix: error construction --- packages/vite/src/node/plugins/optimizedDeps.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/vite/src/node/plugins/optimizedDeps.ts b/packages/vite/src/node/plugins/optimizedDeps.ts index c8f15133c2a610..ad4df3bfeb7177 100644 --- a/packages/vite/src/node/plugins/optimizedDeps.ts +++ b/packages/vite/src/node/plugins/optimizedDeps.ts @@ -73,8 +73,8 @@ function throwProcessingError(id: string) { ) err.code = ERR_OPTIMIZE_DEPS_PROCESSING_ERROR // This error will be catched by the transform middleware that will - // send a 408 (request timeout) response to the browser - throw new Error(err) + // send a 504 status code request timeout + throw err } function throwOutdatedRequest(id: string) { @@ -84,8 +84,8 @@ function throwOutdatedRequest(id: string) { ) err.code = ERR_OUTDATED_OPTIMIZED_DEP // This error will be catched by the transform middleware that will - // send a 408 (request timeout) response to the browser - throw new Error(err) + // send a 504 status code request timeout + throw err } function optimizeDepInfoFromFile( From b414675feed8b644e362e4790e5b59b5f5b6a477 Mon Sep 17 00:00:00 2001 From: patak Date: Thu, 10 Feb 2022 12:52:52 +0100 Subject: [PATCH 11/24] chore: remove unneeded condition --- packages/vite/src/node/optimizer/index.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index bc26608f9f4cc2..4160d6908d7d60 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -498,13 +498,9 @@ export function createIsOptimizedDepUrl(config: ResolvedConfig) { // like '/node_modules/.vite' `/${cacheDirRelative}` - return cacheDirPrefix - ? function isOptimizedDepUrl(url: string): boolean { - return url.startsWith(cacheDirPrefix) - } - : function isOptimizedDepUrl(url: string): boolean { - return false - } + return function isOptimizedDepUrl(url: string): boolean { + return url.startsWith(cacheDirPrefix) + } } function metadataStringifyReplacer(key: string, value: any) { From 06389aaff15434766f177a7d32fe9f14d6e0378c Mon Sep 17 00:00:00 2001 From: patak Date: Thu, 10 Feb 2022 20:59:28 +0100 Subject: [PATCH 12/24] fix: correctly get info about optimized deps source maps --- packages/vite/src/node/plugins/optimizedDeps.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vite/src/node/plugins/optimizedDeps.ts b/packages/vite/src/node/plugins/optimizedDeps.ts index ad4df3bfeb7177..dcf33d6a9ac1b9 100644 --- a/packages/vite/src/node/plugins/optimizedDeps.ts +++ b/packages/vite/src/node/plugins/optimizedDeps.ts @@ -92,6 +92,7 @@ function optimizeDepInfoFromFile( metadata: DepOptimizationMetadata, file: string ): OptimizedDepInfo | undefined { + file = file.replace(/\.map$/,'') return ( findFileInfo(metadata.optimized, file) || findFileInfo(metadata.discovered, file) From dce0e3b0796dd7c60eaa05c34a490029cce51f89 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Thu, 10 Feb 2022 21:05:02 +0100 Subject: [PATCH 13/24] chore: mark holdBackServerStart as experimental --- packages/vite/src/node/optimizer/index.ts | 1 + packages/vite/src/node/plugins/optimizedDeps.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 4160d6908d7d60..5f66b1bb1dd178 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -54,6 +54,7 @@ export interface DepOptimizationOptions { * Hold back server start when doing the initial pre-bundling. * Default is false. Set to true for testing purposes, or in situations * where the pre Vite 2.9 strategy is required + * @experimental */ holdBackServerStart?: boolean /** diff --git a/packages/vite/src/node/plugins/optimizedDeps.ts b/packages/vite/src/node/plugins/optimizedDeps.ts index dcf33d6a9ac1b9..71ad48dde60dfc 100644 --- a/packages/vite/src/node/plugins/optimizedDeps.ts +++ b/packages/vite/src/node/plugins/optimizedDeps.ts @@ -92,7 +92,7 @@ function optimizeDepInfoFromFile( metadata: DepOptimizationMetadata, file: string ): OptimizedDepInfo | undefined { - file = file.replace(/\.map$/,'') + file = file.replace(/\.map$/, '') return ( findFileInfo(metadata.optimized, file) || findFileInfo(metadata.discovered, file) From bdcf28aca26911fd961fc2e97a6a357b9eec9b71 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Mon, 14 Feb 2022 22:09:27 +0100 Subject: [PATCH 14/24] fix: process optimized deps source maps in transform middleware --- .../vite/src/node/plugins/optimizedDeps.ts | 1 - .../src/node/server/middlewares/transform.ts | 48 +++++++++++++++---- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/packages/vite/src/node/plugins/optimizedDeps.ts b/packages/vite/src/node/plugins/optimizedDeps.ts index 71ad48dde60dfc..ad4df3bfeb7177 100644 --- a/packages/vite/src/node/plugins/optimizedDeps.ts +++ b/packages/vite/src/node/plugins/optimizedDeps.ts @@ -92,7 +92,6 @@ function optimizeDepInfoFromFile( metadata: DepOptimizationMetadata, file: string ): OptimizedDepInfo | undefined { - file = file.replace(/\.map$/, '') return ( findFileInfo(metadata.optimized, file) || findFileInfo(metadata.discovered, file) diff --git a/packages/vite/src/node/server/middlewares/transform.ts b/packages/vite/src/node/server/middlewares/transform.ts index 84da8cf92a951d..199aeb173a10ad 100644 --- a/packages/vite/src/node/server/middlewares/transform.ts +++ b/packages/vite/src/node/server/middlewares/transform.ts @@ -1,3 +1,5 @@ +import { promises as fs } from 'fs' +import path from 'path' import type { ViteDevServer } from '..' import type { Connect } from 'types/connect' import { @@ -10,13 +12,19 @@ import { prettifyUrl, removeImportQuery, removeTimestampQuery, - unwrapId + unwrapId, + fsPathFromId, + ensureVolumeInPath } from '../../utils' import { send } from '../send' import { transformRequest } from '../transformRequest' import { isHTMLProxy } from '../../plugins/html' import colors from 'picocolors' -import { DEP_VERSION_RE, NULL_BYTE_PLACEHOLDER } from '../../constants' +import { + DEP_VERSION_RE, + NULL_BYTE_PLACEHOLDER, + FS_PREFIX +} from '../../constants' import { isCSSRequest, isDirectCSSRequest, @@ -65,15 +73,35 @@ export function transformMiddleware( const isSourceMap = withoutQuery.endsWith('.map') // since we generate source map references, handle those requests here if (isSourceMap) { - const originalUrl = url.replace(/\.map($|\?)/, '$1') - const map = (await moduleGraph.getModuleByUrl(originalUrl, false)) - ?.transformResult?.map - if (map) { - return send(req, res, JSON.stringify(map), 'json', { - headers: server.config.server.headers - }) + if (isOptimizedDepUrl(url)) { + // If the browser is requesting a source map for an optimized dep, it + // means that the dependency has already been pre-bundled and loaded + try { + const mapFile = url.startsWith(FS_PREFIX) + ? fsPathFromId(url) + : normalizePath( + ensureVolumeInPath(path.resolve(root, url.slice(1))) + ) + const map = await fs.readFile(mapFile, 'utf-8') + return send(req, res, map, 'json', { + headers: server.config.server.headers + }) + } catch (e) { + res.statusCode = 504 // status code request timeout + res.end() + return + } } else { - return next() + const originalUrl = url.replace(/\.map($|\?)/, '$1') + const map = (await moduleGraph.getModuleByUrl(originalUrl, false)) + ?.transformResult?.map + if (map) { + return send(req, res, JSON.stringify(map), 'json', { + headers: server.config.server.headers + }) + } else { + return next() + } } } From 76773684f5d066b98c3a155c379c2558fe5451af Mon Sep 17 00:00:00 2001 From: patak-dev Date: Tue, 15 Feb 2022 08:44:58 +0100 Subject: [PATCH 15/24] chore: check URL version in optimizeDeps --- packages/vite/src/node/plugins/optimizedDeps.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/vite/src/node/plugins/optimizedDeps.ts b/packages/vite/src/node/plugins/optimizedDeps.ts index ad4df3bfeb7177..4783324720138a 100644 --- a/packages/vite/src/node/plugins/optimizedDeps.ts +++ b/packages/vite/src/node/plugins/optimizedDeps.ts @@ -1,6 +1,7 @@ import { promises as fs } from 'fs' import type { Plugin } from '../plugin' import colors from 'picocolors' +import { DEP_VERSION_RE } from '../constants' import { cleanUrl, createDebugger } from '../utils' import { isOptimizedDepFile } from '../optimizer' import type { DepOptimizationMetadata, OptimizedDepInfo } from '../optimizer' @@ -30,8 +31,15 @@ export function optimizedDepsPlugin(): Plugin { const metadata = server?._optimizeDepsMetadata if (metadata) { const file = cleanUrl(id) + const versionMatch = id.match(DEP_VERSION_RE) + const browserHash = versionMatch + ? versionMatch[1].split('=')[1] + : undefined const info = optimizeDepInfoFromFile(metadata, file) if (info) { + if (browserHash && info.browserHash !== browserHash) { + throwOutdatedRequest(id) + } try { // This is an entry point, it may still not be bundled await info.processing From c264ca911adaf62b6a1f00843b7e1834cfee816a Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Wed, 16 Feb 2022 17:13:55 +0800 Subject: [PATCH 16/24] chore: typo --- packages/vite/src/node/optimizer/index.ts | 4 ++-- packages/vite/src/node/plugins/index.ts | 2 +- packages/vite/src/node/plugins/optimizedDeps.ts | 6 ++---- packages/vite/src/node/plugins/resolve.ts | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 5f66b1bb1dd178..04afa6e2ce63d5 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -202,7 +202,7 @@ export async function optimizeDeps( let deps: Record if (!newDeps) { // Initial optimizeDeps at server start. Perform a fast scan using esbuild to - // find deps to pre-bundle and include user harcoded dependencies + // find deps to pre-bundle and include user hard-coded dependencies let missing: Record ;({ deps, missing } = await scanImports(config)) @@ -431,7 +431,7 @@ export async function optimizeDeps( } if (alteredFiles) { - // Overrite individual hashs with the new global browserHash, a full page reload is required + // Overrite individual hashes with the new global browserHash, a full page reload is required // New deps that ended up with a different hash replaced while doing analysis import are going to // return a not found so the browser doesn't cache them. And will properly get loaded after the reload for (const id in deps) { diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index c9aa26329bc0b1..654686fea03747 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -46,7 +46,7 @@ export async function resolvePlugins( ssrConfig: config.ssr, asSrc: true }), - optimizedDepsPlugin(), + isBuild ? null : optimizedDepsPlugin(), htmlInlineProxyPlugin(config), cssPlugin(config), config.esbuild !== false ? esbuildPlugin(config.esbuild) : null, diff --git a/packages/vite/src/node/plugins/optimizedDeps.ts b/packages/vite/src/node/plugins/optimizedDeps.ts index 4783324720138a..c7e03927cf6574 100644 --- a/packages/vite/src/node/plugins/optimizedDeps.ts +++ b/packages/vite/src/node/plugins/optimizedDeps.ts @@ -24,8 +24,6 @@ export function optimizedDepsPlugin(): Plugin { server = _server }, - apply: 'serve', - async load(id) { if (server && isOptimizedDepFile(id, server.config)) { const metadata = server?._optimizeDepsMetadata @@ -80,7 +78,7 @@ function throwProcessingError(id: string) { `The current page should have reloaded by now` ) err.code = ERR_OPTIMIZE_DEPS_PROCESSING_ERROR - // This error will be catched by the transform middleware that will + // This error will be caught by the transform middleware that will // send a 504 status code request timeout throw err } @@ -91,7 +89,7 @@ function throwOutdatedRequest(id: string) { `a page reload is going to ask for it.` ) err.code = ERR_OUTDATED_OPTIMIZED_DEP - // This error will be catched by the transform middleware that will + // This error will be caught by the transform middleware that will // send a 504 status code request timeout throw err } diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index bca28d48ee23bd..fe2af6c9419af5 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -608,7 +608,7 @@ export function tryNodeResolve( } } else { // this is a missing import, queue optimize-deps re-run and - // get a resolved its optmized info + // get a resolved its optimized info const optimizedInfo = server._registerMissingImport!(id, resolved, ssr) resolved = getOptimizedUrl(optimizedInfo) } From db909349cbf8dc41580a53601dad4d1e55c1d295 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Thu, 17 Feb 2022 10:48:43 +0100 Subject: [PATCH 17/24] fix: add /@fs even if the optimize dep doesn't yet exists --- packages/vite/src/node/plugins/importAnalysis.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index 88b96de6f38349..e78f869dbcd71a 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -225,8 +225,12 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { if (resolved.id.startsWith(root + '/')) { // in root: infer short absolute path from root url = resolved.id.slice(root.length) - } else if (fs.existsSync(cleanUrl(resolved.id))) { - // exists but out of root: rewrite to absolute /@fs/ paths + } else if ( + resolved.id.startsWith(normalizePath(config.cacheDir)) || + fs.existsSync(cleanUrl(resolved.id)) + ) { + // an optimized deps may not yet exists in the filesystem, or + // a regular file exists but is out of root: rewrite to absolute /@fs/ paths url = path.posix.join(FS_PREFIX + resolved.id) } else { url = resolved.id From 0f7c1e7313cb2a835c79faa122af413f7dbb18f2 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 18 Feb 2022 08:58:37 +0100 Subject: [PATCH 18/24] fix: avoid source map warnings for outdated map requests --- .../src/node/server/middlewares/transform.ts | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/vite/src/node/server/middlewares/transform.ts b/packages/vite/src/node/server/middlewares/transform.ts index 199aeb173a10ad..ae7bec6e185113 100644 --- a/packages/vite/src/node/server/middlewares/transform.ts +++ b/packages/vite/src/node/server/middlewares/transform.ts @@ -76,20 +76,32 @@ export function transformMiddleware( if (isOptimizedDepUrl(url)) { // If the browser is requesting a source map for an optimized dep, it // means that the dependency has already been pre-bundled and loaded + const mapFile = url.startsWith(FS_PREFIX) + ? fsPathFromId(url) + : normalizePath( + ensureVolumeInPath(path.resolve(root, url.slice(1))) + ) try { - const mapFile = url.startsWith(FS_PREFIX) - ? fsPathFromId(url) - : normalizePath( - ensureVolumeInPath(path.resolve(root, url.slice(1))) - ) const map = await fs.readFile(mapFile, 'utf-8') return send(req, res, map, 'json', { headers: server.config.server.headers }) } catch (e) { - res.statusCode = 504 // status code request timeout - res.end() - return + // Outdated source map request for optimized deps, this isn't an error + // but part of the normal flow when re-optimizing after missing deps + // Send back an empty source map so the browser doesn't issue warnings + const dummySourceMap = { + version: 3, + file: mapFile.replace(/\.map$/, ''), + sources: [], + sourcesContent: [], + names: [], + mappings: ';;;;;;;;;' + } + return send(req, res, JSON.stringify(dummySourceMap), 'json', { + cacheControl: 'no-cache', + headers: server.config.server.headers + }) } } else { const originalUrl = url.replace(/\.map($|\?)/, '$1') From 7f51389c564bb9e6385a03b63565a3428639d797 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 18 Feb 2022 11:32:52 +0100 Subject: [PATCH 19/24] fix: sync swap of new processed deps in disk --- packages/vite/src/node/optimizer/index.ts | 129 +++++++++++++----- .../src/node/optimizer/registerMissing.ts | 2 +- .../vite/src/node/plugins/importAnalysis.ts | 8 +- 3 files changed, 105 insertions(+), 34 deletions(-) diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 04afa6e2ce63d5..74cdbc025988b9 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -157,10 +157,18 @@ export async function optimizeDeps( command: 'build' } - const { root, logger, cacheDir } = config + const { root, logger } = config const log = asCommand ? logger.info : debug - const dataPath = path.join(cacheDir, '_metadata.json') + // Before Vite 2.9, dependencies were cached in the root of the cacheDir + // For compat, we remove the cache if we find the old structure + if (fs.existsSync(path.join(config.cacheDir, '_metadata.json'))) { + emptyDir(config.cacheDir) + } + + const depsCacheDir = getDepsCacheDir(config) + const processingCacheDir = getProcessingDepsCacheDir(config) + const mainHash = getDepHash(root, config) const data: DepOptimizationMetadata = { @@ -174,28 +182,35 @@ export async function optimizeDeps( if (!force) { let prevData: DepOptimizationMetadata | undefined try { - prevData = JSON.parse(fs.readFileSync(dataPath, 'utf-8')) + const prevDataPath = path.join(depsCacheDir, '_metadata.json') + prevData = parseOptimizedDepsMetadata( + fs.readFileSync(prevDataPath, 'utf-8'), + depsCacheDir, + processing.promise + ) } catch (e) {} // hash is consistent, no need to re-bundle if (prevData && prevData.hash === data.hash) { log('Hash is consistent. Skipping. Use --force to override.') - for (const o of Object.keys(prevData.optimized)) { - prevData.optimized[o].processing = processing.promise - } + // let prevData be assigned to the server before resolving the processing setTimeout(() => processing.resolve(), 0) - return { ...prevData, discovered: {}, processing: processing.promise } + return prevData } } - if (fs.existsSync(cacheDir)) { - emptyDir(cacheDir) + // Create a temporal directory so we don't need to delete optimized deps + // until they have been processed. This also avoids leaving the deps cache + // directory in a corrupted state if there is an error + if (fs.existsSync(processingCacheDir)) { + emptyDir(processingCacheDir) } else { - fs.mkdirSync(cacheDir, { recursive: true }) + fs.mkdirSync(processingCacheDir, { recursive: true }) } + // a hint for Node.js // all files in the cache directory should be recognized as ES modules writeFile( - path.resolve(cacheDir, 'package.json'), + path.resolve(processingCacheDir, 'package.json'), JSON.stringify({ type: 'module' }) ) @@ -252,7 +267,7 @@ export async function optimizeDeps( for (const id in deps) { const entry = deps[id] data.optimized[id] = { - file: getOptimizedFilePath(id, cacheDir), + file: getOptimizedFilePath(id, config), src: entry, browserHash: data.browserHash, processing: processing.promise @@ -280,14 +295,16 @@ export async function optimizeDeps( return data - async function prebundleDeps() { + async function prebundleDeps(): Promise { + const dataPath = path.join(processingCacheDir, '_metadata.json') + const qualifiedIds = Object.keys(deps) if (!qualifiedIds.length) { writeFile(dataPath, JSON.stringify(data, null, 2)) log(`No dependencies to bundle. Skipping.\n\n\n`) processing.resolve() - return data + return } const total = qualifiedIds.length @@ -377,7 +394,7 @@ export async function optimizeDeps( logLevel: 'error', splitting: true, sourcemap: true, - outdir: cacheDir, + outdir: processingCacheDir, ignoreAnnotations: true, metafile: true, define, @@ -391,7 +408,10 @@ export async function optimizeDeps( const meta = result.metafile! // the paths in `meta.outputs` are relative to `process.cwd()` - const cacheDirOutputPath = path.relative(process.cwd(), cacheDir) + const processingCacheDirOutputPath = path.relative( + process.cwd(), + processingCacheDir + ) for (const id in deps) { const optimizedInfo = data.optimized[id] @@ -399,7 +419,7 @@ export async function optimizeDeps( id, idToExports[id], meta.outputs, - cacheDirOutputPath + processingCacheDirOutputPath ) const output = meta.outputs[path.relative(process.cwd(), optimizedInfo.file)] @@ -439,7 +459,18 @@ export async function optimizeDeps( } } - writeFile(dataPath, JSON.stringify(data, metadataStringifyReplacer, 2)) + // Rewire the file paths from the temporal processing dir to the final deps cache dir + writeFile( + dataPath, + stringifyOptimizedDepsMetadata(data, processingCacheDir) + ) + + // Processing is done, we can now replace the depsCacheDir with processingCacheDir + if (fs.existsSync(depsCacheDir)) { + const rmSync = fs.rmSync ?? fs.rmdirSync // TODO: Remove after support for Node 12 is dropped + rmSync(depsCacheDir, { recursive: true }) + } + fs.renameSync(processingCacheDir, depsCacheDir) debug(`deps bundled in ${(performance.now() - start).toFixed(2)}ms`) processing.resolve({ alteredFiles }) @@ -463,10 +494,6 @@ export function depsFromOptimizedInfo( ) } -export function getOptimizedFilePath(id: string, cacheDir: string) { - return normalizePath(path.resolve(cacheDir, flattenId(id) + '.js')) -} - function getHash(text: string) { return createHash('sha256').update(text).digest('hex').substring(0, 8) } @@ -482,30 +509,70 @@ export function getOptimizedBrowserHash( ) } +function getCachedDepFilePath(id: string, depsCacheDir: string) { + return normalizePath(path.resolve(depsCacheDir, flattenId(id) + '.js')) +} + +export function getOptimizedFilePath(id: string, config: ResolvedConfig) { + return getCachedDepFilePath(id, getDepsCacheDir(config)) +} + +export function getDepsCacheDir(config: ResolvedConfig) { + return normalizePath(path.resolve(config.cacheDir, 'deps')) +} + +export function getProcessingDepsCacheDir(config: ResolvedConfig) { + return normalizePath(path.resolve(config.cacheDir, 'processing')) +} + export function isOptimizedDepFile(id: string, config: ResolvedConfig) { - return id.startsWith(normalizePath(config.cacheDir)) + return id.startsWith(getDepsCacheDir(config)) } export function createIsOptimizedDepUrl(config: ResolvedConfig) { - const { root, cacheDir } = config + const { root } = config + const depsCacheDir = getDepsCacheDir(config) // determine the url prefix of files inside cache directory - const cacheDirRelative = normalizePath(path.relative(root, cacheDir)) - const cacheDirPrefix = cacheDirRelative.startsWith('../') + const depsCacheDirRelative = normalizePath(path.relative(root, depsCacheDir)) + const depsCacheDirPrefix = depsCacheDirRelative.startsWith('../') ? // if the cache directory is outside root, the url prefix would be something // like '/@fs/absolute/path/to/node_modules/.vite' - `/@fs/${normalizePath(cacheDir).replace(/^\//, '')}` + `/@fs/${normalizePath(depsCacheDir).replace(/^\//, '')}` : // if the cache directory is inside root, the url prefix would be something // like '/node_modules/.vite' - `/${cacheDirRelative}` + `/${depsCacheDirRelative}` return function isOptimizedDepUrl(url: string): boolean { - return url.startsWith(cacheDirPrefix) + return url.startsWith(depsCacheDirPrefix) } } -function metadataStringifyReplacer(key: string, value: any) { - return key !== 'processing' && key !== 'discovered' ? value : undefined +function parseOptimizedDepsMetadata( + jsonMetadata: string, + depsCacheDir: string, + processing: Promise +) { + const metadata = JSON.parse(jsonMetadata) + for (const o of Object.keys(metadata.optimized)) { + metadata.optimized[o].processing = processing + } + return { ...metadata, discovered: {}, processing } +} + +function stringifyOptimizedDepsMetadata( + metadata: DepOptimizationMetadata, + depsCacheDir: string +) { + return JSON.stringify( + metadata, + (key: string, value: any) => { + if (key === 'processing' || key === 'discovered') return + + return value + }, + 2 + ) } // 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 index 5613c481d315a8..ea641f89d78637 100644 --- a/packages/vite/src/node/optimizer/registerMissing.ts +++ b/packages/vite/src/node/optimizer/registerMissing.ts @@ -155,7 +155,7 @@ export function createMissingImporterRegisterFn( return missing } missing = metadata.discovered[id] = { - file: getOptimizedFilePath(id, server.config.cacheDir), + file: getOptimizedFilePath(id, server.config), src: resolved, // Assing a browserHash to this missing dependency that is unique to // the current state of known + missing deps. If the optimizeDeps stage diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index e78f869dbcd71a..b8a1d3815dd956 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -45,7 +45,11 @@ import { makeLegalIdentifier } from '@rollup/pluginutils' import { shouldExternalizeForSSR } from '../ssr/ssrExternal' import { performance } from 'perf_hooks' import { transformRequest } from '../server/transformRequest' -import { isOptimizedDepFile, createIsOptimizedDepUrl } from '../optimizer' +import { + isOptimizedDepFile, + createIsOptimizedDepUrl, + getDepsCacheDir +} from '../optimizer' const isDebug = !!process.env.DEBUG const debug = createDebugger('vite:import-analysis') @@ -226,7 +230,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // in root: infer short absolute path from root url = resolved.id.slice(root.length) } else if ( - resolved.id.startsWith(normalizePath(config.cacheDir)) || + resolved.id.startsWith(getDepsCacheDir(config)) || fs.existsSync(cleanUrl(resolved.id)) ) { // an optimized deps may not yet exists in the filesystem, or From a63780932598b6db492b0a06991b808cc672bffb Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 18 Feb 2022 21:31:48 +0100 Subject: [PATCH 20/24] fix: ensure optimizeDeps isn't run concorrently --- .../src/node/optimizer/registerMissing.ts | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/vite/src/node/optimizer/registerMissing.ts b/packages/vite/src/node/optimizer/registerMissing.ts index ea641f89d78637..26be6db67e2170 100644 --- a/packages/vite/src/node/optimizer/registerMissing.ts +++ b/packages/vite/src/node/optimizer/registerMissing.ts @@ -31,16 +31,31 @@ export function createMissingImporterRegisterFn( let processingMissingDeps = newOptimizeDepsProcessingPromise() + let optimizeDepsPromise = metadata.processing + async function rerun(ssr: boolean | undefined) { // debounce time to wait for new missing deps finished, issue a new // optimization of deps (both old and newly found) once the previous // optimizeDeps processing is finished - await metadata.processing - // New deps could have been found here, clear the timeout to already - // consider them in this run - if (handle) clearTimeout(handle) - handle = undefined + // optimizeDeps needs to be run in serie. Await until the previous + // rerun is finished here. It could happen that two reruns are queued + // in that case, we only need to run one of them + const awaitedOptimizeDepsPromise = optimizeDepsPromise + + await optimizeDepsPromise + + if (awaitedOptimizeDepsPromise !== optimizeDepsPromise) { + // There were two or more rerun queued and one of them already + // started. Only let through the first one, and discard the others + return + } + + if (handle) { + // New deps could have been found here, skip this rerun. Once the + // debounce time is over, a new rerun will be issued + return + } logger.info( colors.yellow( @@ -57,6 +72,8 @@ export function createMissingImporterRegisterFn( // respect insertion order to keep the metadata file stable const newDeps = { ...metadata.optimized, ...metadata.discovered } const newDepsProcessing = processingMissingDeps + optimizeDepsPromise = newDepsProcessing.promise + let processingResult: OptimizeDepsResult | undefined processingMissingDeps = newOptimizeDepsProcessingPromise() From 3f9ec982cf2925dfb5a9612a83739c8fdb542cba Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 18 Feb 2022 21:48:50 +0100 Subject: [PATCH 21/24] fix: post-optimization handling --- .../src/node/optimizer/registerMissing.ts | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/vite/src/node/optimizer/registerMissing.ts b/packages/vite/src/node/optimizer/registerMissing.ts index 26be6db67e2170..138c9bf2604087 100644 --- a/packages/vite/src/node/optimizer/registerMissing.ts +++ b/packages/vite/src/node/optimizer/registerMissing.ts @@ -95,6 +95,16 @@ export function createMissingImporterRegisterFn( newDepsProcessing ) + // update ssr externals + if (ssr) { + server._ssrExternals = resolveSSRExternal( + server.config, + Object.keys(newData.optimized) + ) + } + + processingResult = await newData!.processing + // While optimizeDeps is running, new missing deps may be discovered, // in which case they will keep being added to metadata.discovered for (const o of Object.keys(metadata.discovered)) { @@ -104,21 +114,14 @@ export function createMissingImporterRegisterFn( } } metadata = newData - - // update ssr externals - if (ssr) { - server._ssrExternals = resolveSSRExternal( - server.config, - Object.keys(metadata.optimized) - ) - } - - processingResult = await newData!.processing } catch (e) { logger.error( colors.red(`error while updating dependencies:\n${e.stack}`), { timestamp: true, error: e } ) + + // Reset missing deps, let the server rediscover the dependencies + metadata.discovered = {} fullReload() return } @@ -128,15 +131,27 @@ export function createMissingImporterRegisterFn( timestamp: true }) } else { - logger.info(colors.green(`✨ dependencies updated, reloading page...`), { - timestamp: true - }) - if (handle) { // There are newly discovered deps, and another rerun is about to be // excecuted. Avoid the current full reload, but queue it for the next one needFullReload = true + // We still invalidate the module graph to wipe out cached transforms + server.moduleGraph.invalidateAll() + logger.info( + colors.green( + `✨ dependencies updated, other missing dependencies found...` + ), + { + timestamp: true + } + ) } else { + logger.info( + colors.green(`✨ dependencies updated, reloading page...`), + { + timestamp: true + } + ) fullReload() } } From 44eca4e20adb8c5ddaa0862fede4e9b89083da53 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Sat, 19 Feb 2022 11:13:42 +0100 Subject: [PATCH 22/24] refactor: createOptimizeDepsRun, remove new option --- packages/vite/src/node/optimizer/index.ts | 112 +++++++++++------- .../src/node/optimizer/registerMissing.ts | 97 ++++++++------- packages/vite/src/node/server/index.ts | 11 +- 3 files changed, 128 insertions(+), 92 deletions(-) diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 74cdbc025988b9..0e5768e932ca19 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -50,13 +50,6 @@ export interface DepOptimizationOptions { * cannot be globs). */ exclude?: string[] - /** - * Hold back server start when doing the initial pre-bundling. - * Default is false. Set to true for testing purposes, or in situations - * where the pre Vite 2.9 strategy is required - * @experimental - */ - holdBackServerStart?: boolean /** * Options to pass to esbuild during the dep scanning and optimization * @@ -143,15 +136,43 @@ export interface DepOptimizationMetadata { processing: Promise } +/** + * Used by Vite CLI when running `vite optimize` + */ export async function optimizeDeps( config: ResolvedConfig, force = config.server.force, asCommand = false, - currentData: DepOptimizationMetadata | null = null, newDeps?: Record, // missing imports encountered after server has started - ssr?: boolean, - processing: OptimizeDepsProcessing = newOptimizeDepsProcessingPromise() + ssr?: boolean ): Promise { + const { metadata, run } = await createOptimizeDepsRun( + config, + force, + asCommand, + null, + newDeps, + ssr + ) + await run() + return metadata +} + +/** + * Internally, Vite uses this function to prepare a optimizeDeps run. When Vite starts, we can get + * the metadata and start the server without waiting for the optimizeDeps processing to be completed + */ +export async function createOptimizeDepsRun( + config: ResolvedConfig, + force = config.server.force, + asCommand = false, + currentData: DepOptimizationMetadata | null = null, + newDeps?: Record, // missing imports encountered after server has started + ssr?: boolean +): Promise<{ + metadata: DepOptimizationMetadata + run: () => Promise +}> { config = { ...config, command: 'build' @@ -171,7 +192,9 @@ export async function optimizeDeps( const mainHash = getDepHash(root, config) - const data: DepOptimizationMetadata = { + const processing = newOptimizeDepsProcessingPromise() + + const metadata: DepOptimizationMetadata = { hash: mainHash, browserHash: mainHash, optimized: {}, @@ -190,11 +213,12 @@ export async function optimizeDeps( ) } catch (e) {} // hash is consistent, no need to re-bundle - if (prevData && prevData.hash === data.hash) { + if (prevData && prevData.hash === metadata.hash) { log('Hash is consistent. Skipping. Use --force to override.') - // let prevData be assigned to the server before resolving the processing - setTimeout(() => processing.resolve(), 0) - return prevData + return { + metadata: prevData, + run: () => (processing.resolve(), processing.promise) + } } } @@ -259,17 +283,17 @@ export async function optimizeDeps( } // update browser hash - data.browserHash = getOptimizedBrowserHash(data.hash, deps) + metadata.browserHash = getOptimizedBrowserHash(metadata.hash, deps) // We generate the mapping of dependency ids to their cache file location // before processing the dependencies with esbuild. This allow us to continue // processing files in the importAnalysis and resolve plugins for (const id in deps) { const entry = deps[id] - data.optimized[id] = { + metadata.optimized[id] = { file: getOptimizedFilePath(id, config), src: entry, - browserHash: data.browserHash, + browserHash: metadata.browserHash, processing: processing.promise } } @@ -280,28 +304,26 @@ export async function optimizeDeps( // Clone optimized info objects, fileHash, browserHash may be changed for them for (const o of Object.keys(newDeps)) { - data.optimized[o] = { ...newDeps[o] } + metadata.optimized[o] = { ...newDeps[o] } } // update global browser hash, but keep newDeps individual hashs until we know // if files are stable so we can avoid a full page reload - data.browserHash = getOptimizedBrowserHash(data.hash, deps) + metadata.browserHash = getOptimizedBrowserHash(metadata.hash, deps) } - // We prebundle dependencies with esbuild and cache them, but there is no need - // to wait here. Code that needs to access the cached deps needs to await - // the optimizeDepsMetadata.processing promise - prebundleDeps() + return { metadata, run: prebundleDeps } - return data - - async function prebundleDeps(): Promise { - const dataPath = path.join(processingCacheDir, '_metadata.json') + async function prebundleDeps(): Promise { + // We prebundle dependencies with esbuild and cache them, but there is no need + // to wait here. Code that needs to access the cached deps needs to await + // the optimizeDepsMetadata.processing promise const qualifiedIds = Object.keys(deps) if (!qualifiedIds.length) { - writeFile(dataPath, JSON.stringify(data, null, 2)) + // Write metadata file, delete `deps` folder and rename the `processing` folder to `deps` + commitProcessingDepsCacheSync() log(`No dependencies to bundle. Skipping.\n\n\n`) processing.resolve() return @@ -414,7 +436,7 @@ export async function optimizeDeps( ) for (const id in deps) { - const optimizedInfo = data.optimized[id] + const optimizedInfo = metadata.optimized[id] optimizedInfo.needsInterop = needsInterop( id, idToExports[id], @@ -427,7 +449,7 @@ export async function optimizeDeps( // We only need to hash the output.imports in to check for stability, but adding the hash // and file path gives us a unique hash that may be useful for other things in the future optimizedInfo.fileHash = getHash( - data.hash + optimizedInfo.file + JSON.stringify(output.imports) + metadata.hash + optimizedInfo.file + JSON.stringify(output.imports) ) } } @@ -440,7 +462,7 @@ export async function optimizeDeps( if (currentData) { alteredFiles = Object.keys(currentData.optimized).some((dep) => { const currentInfo = currentData.optimized[dep] - const info = data.optimized[dep] + const info = metadata.optimized[dep] return ( !info?.fileHash || !currentInfo?.fileHash || @@ -455,25 +477,28 @@ export async function optimizeDeps( // New deps that ended up with a different hash replaced while doing analysis import are going to // return a not found so the browser doesn't cache them. And will properly get loaded after the reload for (const id in deps) { - data.optimized[id].browserHash = data.browserHash + metadata.optimized[id].browserHash = metadata.browserHash } } - // Rewire the file paths from the temporal processing dir to the final deps cache dir - writeFile( - dataPath, - stringifyOptimizedDepsMetadata(data, processingCacheDir) - ) + // Write metadata file, delete `deps` folder and rename the new `processing` folder to `deps` in sync + commitProcessingDepsCacheSync() + + debug(`deps bundled in ${(performance.now() - start).toFixed(2)}ms`) + processing.resolve({ alteredFiles }) + return processing.promise + } + function commitProcessingDepsCacheSync() { + // Rewire the file paths from the temporal processing dir to the final deps cache dir + const dataPath = path.join(processingCacheDir, '_metadata.json') + writeFile(dataPath, stringifyOptimizedDepsMetadata(metadata)) // Processing is done, we can now replace the depsCacheDir with processingCacheDir if (fs.existsSync(depsCacheDir)) { const rmSync = fs.rmSync ?? fs.rmdirSync // TODO: Remove after support for Node 12 is dropped rmSync(depsCacheDir, { recursive: true }) } fs.renameSync(processingCacheDir, depsCacheDir) - - debug(`deps bundled in ${(performance.now() - start).toFixed(2)}ms`) - processing.resolve({ alteredFiles }) } } @@ -560,10 +585,7 @@ function parseOptimizedDepsMetadata( return { ...metadata, discovered: {}, processing } } -function stringifyOptimizedDepsMetadata( - metadata: DepOptimizationMetadata, - depsCacheDir: string -) { +function stringifyOptimizedDepsMetadata(metadata: DepOptimizationMetadata) { return JSON.stringify( metadata, (key: string, value: any) => { diff --git a/packages/vite/src/node/optimizer/registerMissing.ts b/packages/vite/src/node/optimizer/registerMissing.ts index 138c9bf2604087..88d99611eaa729 100644 --- a/packages/vite/src/node/optimizer/registerMissing.ts +++ b/packages/vite/src/node/optimizer/registerMissing.ts @@ -1,6 +1,6 @@ import colors from 'picocolors' import { - optimizeDeps, + createOptimizeDepsRun, getOptimizedFilePath, getOptimizedBrowserHash, depsFromOptimizedInfo, @@ -38,6 +38,15 @@ export function createMissingImporterRegisterFn( // optimization of deps (both old and newly found) once the previous // optimizeDeps processing is finished + // a succesful completion of the optimizeDeps rerun will end up + // creating new bundled version of all current and discovered deps + // in the cache dir and a new metadata info object assigned + // to server._optimizeDepsMetadata. A fullReload is only issued if + // the previous bundled dependencies have changed. + + // if the rerun fails, server._optimizeDepsMetadata remains untouched, + // current discovered deps are cleaned, and a fullReload is issued + // optimizeDeps needs to be run in serie. Await until the previous // rerun is finished here. It could happen that two reruns are queued // in that case, we only need to run one of them @@ -72,29 +81,33 @@ export function createMissingImporterRegisterFn( // respect insertion order to keep the metadata file stable const newDeps = { ...metadata.optimized, ...metadata.discovered } const newDepsProcessing = processingMissingDeps + + // Other rerun will await until this run is finished optimizeDepsPromise = newDepsProcessing.promise let processingResult: OptimizeDepsResult | undefined + // Create a new promise for the next rerun, discovered missing + // dependencies will be asigned this promise from this point processingMissingDeps = newOptimizeDepsProcessingPromise() let newData: DepOptimizationMetadata | null = null try { - // During optimizer re-run, the resolver may continue to discover - // optimized files. If we directly resolve to node modules there - // is no way to avoid a full-page reload - - newData = server._optimizeDepsMetadata = await optimizeDeps( + const optimizeDeps = await createOptimizeDepsRun( server.config, true, false, metadata, newDeps, - ssr, - newDepsProcessing + ssr ) + // We await the optimizeDeps run here, we are only going to use + // the newData if there wasn't an error + newData = optimizeDeps.metadata + processingResult = await optimizeDeps.run() + // update ssr externals if (ssr) { server._ssrExternals = resolveSSRExternal( @@ -103,8 +116,6 @@ export function createMissingImporterRegisterFn( ) } - processingResult = await newData!.processing - // While optimizeDeps is running, new missing deps may be discovered, // in which case they will keep being added to metadata.discovered for (const o of Object.keys(metadata.discovered)) { @@ -113,7 +124,36 @@ export function createMissingImporterRegisterFn( delete metadata.discovered[o] } } - metadata = newData + newData.processing = newDepsProcessing.promise + metadata = server._optimizeDepsMetadata = newData + + if (!needFullReload && !processingResult?.alteredFiles) { + logger.info(colors.green(`✨ new dependencies pre-bundled...`), { + timestamp: true + }) + } else { + if (handle) { + // There are newly discovered deps, and another rerun is about to be + // excecuted. Avoid the current full reload, but queue it for the next one + needFullReload = true + logger.info( + colors.green( + `✨ dependencies updated, delaying reload as new dependencies have been found...` + ), + { + timestamp: true + } + ) + } else { + logger.info( + colors.green(`✨ dependencies updated, reloading page...`), + { + timestamp: true + } + ) + fullReload() + } + } } catch (e) { logger.error( colors.red(`error while updating dependencies:\n${e.stack}`), @@ -123,37 +163,10 @@ export function createMissingImporterRegisterFn( // Reset missing deps, let the server rediscover the dependencies metadata.discovered = {} fullReload() - return - } - - if (!needFullReload && !processingResult?.alteredFiles) { - logger.info(colors.green(`✨ new dependencies pre-bundled...`), { - timestamp: true - }) - } else { - if (handle) { - // There are newly discovered deps, and another rerun is about to be - // excecuted. Avoid the current full reload, but queue it for the next one - needFullReload = true - // We still invalidate the module graph to wipe out cached transforms - server.moduleGraph.invalidateAll() - logger.info( - colors.green( - `✨ dependencies updated, other missing dependencies found...` - ), - { - timestamp: true - } - ) - } else { - logger.info( - colors.green(`✨ dependencies updated, reloading page...`), - { - timestamp: true - } - ) - fullReload() - } + } finally { + // Rerun finished, resolve the promise to let awaiting requests or + // other rerun queued be processed + newDepsProcessing.resolve() } } diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 72484afb524d4d..6dec31a0bcf37d 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -45,7 +45,7 @@ import type { ESBuildTransformResult } from '../plugins/esbuild' import { transformWithEsbuild } from '../plugins/esbuild' import type { TransformOptions as EsbuildTransformOptions } from 'esbuild' import type { DepOptimizationMetadata, OptimizedDepInfo } from '../optimizer' -import { optimizeDeps } from '../optimizer' +import { createOptimizeDepsRun } from '../optimizer' import { ssrLoadModule } from '../ssr/ssrModuleLoader' import { resolveSSRExternal } from '../ssr/ssrExternal' import { @@ -560,14 +560,15 @@ export async function createServer( middlewares.use(errorMiddleware(server, !!middlewareMode)) const runOptimize = async () => { - server._optimizeDepsMetadata = await optimizeDeps( + const optimizeDeps = await createOptimizeDepsRun( config, config.server.force || server._forceOptimizeOnRestart ) - if (server.config?.optimizeDeps?.holdBackServerStart) { - await server._optimizeDepsMetadata?.processing - } + // Don't await for the optimization to finish, we can start the + // server right away here + server._optimizeDepsMetadata = optimizeDeps.metadata + optimizeDeps.run() // While running the first optimizeDeps, _registerMissingImport is null // so the resolve plugin resolves straight to node_modules during the From d58777496ed8c97e5a289918fbf1abaf4a4692ea Mon Sep 17 00:00:00 2001 From: patak-dev Date: Sat, 19 Feb 2022 11:31:49 +0100 Subject: [PATCH 23/24] fix: delay full reload condition --- packages/vite/src/node/optimizer/registerMissing.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/node/optimizer/registerMissing.ts b/packages/vite/src/node/optimizer/registerMissing.ts index 88d99611eaa729..7e3a23a40a4d9e 100644 --- a/packages/vite/src/node/optimizer/registerMissing.ts +++ b/packages/vite/src/node/optimizer/registerMissing.ts @@ -132,7 +132,7 @@ export function createMissingImporterRegisterFn( timestamp: true }) } else { - if (handle) { + if (Object.keys(metadata.discovered).length > 0) { // There are newly discovered deps, and another rerun is about to be // excecuted. Avoid the current full reload, but queue it for the next one needFullReload = true From 7888a75f1509a70bfa5518df52442e6c512bce5c Mon Sep 17 00:00:00 2001 From: patak-dev Date: Sat, 19 Feb 2022 20:58:56 +0100 Subject: [PATCH 24/24] chore: cleanup --- packages/vite/src/node/index.ts | 6 +- packages/vite/src/node/optimizer/index.ts | 34 +++++------ .../src/node/optimizer/registerMissing.ts | 56 ++++++++++--------- packages/vite/src/node/server/index.ts | 8 +-- 4 files changed, 53 insertions(+), 51 deletions(-) diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index c966cc23f6fd34..890c812e1d392a 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -33,9 +33,9 @@ export type { export type { DepOptimizationMetadata, DepOptimizationOptions, - OptimizedDepInfo, - OptimizeDepsResult, - OptimizeDepsProcessing + DepOptimizationResult, + DepOptimizationProcessing, + OptimizedDepInfo } from './optimizer' export type { Plugin } from './plugin' export type { PackageCache, PackageData } from './packages' diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 0e5768e932ca19..ed49dbdbafca4e 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -81,7 +81,7 @@ export interface DepOptimizationOptions { keepNames?: boolean } -export interface OptimizeDepsResult { +export interface DepOptimizationResult { /** * After a re-optimization, the internal bundled chunks may change * and a full page reload is required if that is the case @@ -91,9 +91,9 @@ export interface OptimizeDepsResult { alteredFiles: boolean } -export interface OptimizeDepsProcessing { - promise: Promise - resolve: (result?: OptimizeDepsResult) => void +export interface DepOptimizationProcessing { + promise: Promise + resolve: (result?: DepOptimizationResult) => void } export interface OptimizedDepInfo { @@ -106,7 +106,7 @@ export interface OptimizedDepInfo { * During optimization, ids can still be resolved to their final location * but the bundles may not yet be saved to disk */ - processing: Promise + processing: Promise } export interface DepOptimizationMetadata { @@ -133,7 +133,7 @@ export interface DepOptimizationMetadata { * During optimization, ids can still be resolved to their final location * but the bundles may not yet be saved to disk */ - processing: Promise + processing: Promise } /** @@ -171,7 +171,7 @@ export async function createOptimizeDepsRun( ssr?: boolean ): Promise<{ metadata: DepOptimizationMetadata - run: () => Promise + run: () => Promise }> { config = { ...config, @@ -192,7 +192,7 @@ export async function createOptimizeDepsRun( const mainHash = getDepHash(root, config) - const processing = newOptimizeDepsProcessingPromise() + const processing = newDepOptimizationProcessing() const metadata: DepOptimizationMetadata = { hash: mainHash, @@ -291,7 +291,7 @@ export async function createOptimizeDepsRun( for (const id in deps) { const entry = deps[id] metadata.optimized[id] = { - file: getOptimizedFilePath(id, config), + file: getOptimizedDepPath(id, config), src: entry, browserHash: metadata.browserHash, processing: processing.promise @@ -300,7 +300,7 @@ export async function createOptimizeDepsRun( } else { // Missing dependencies were found at run-time, optimizeDeps called while the // server is running - deps = depsFromOptimizedInfo(newDeps) + deps = depsFromOptimizedDepInfo(newDeps) // Clone optimized info objects, fileHash, browserHash may be changed for them for (const o of Object.keys(newDeps)) { @@ -314,7 +314,7 @@ export async function createOptimizeDepsRun( return { metadata, run: prebundleDeps } - async function prebundleDeps(): Promise { + async function prebundleDeps(): Promise { // We prebundle dependencies with esbuild and cache them, but there is no need // to wait here. Code that needs to access the cached deps needs to await // the optimizeDepsMetadata.processing promise @@ -502,16 +502,16 @@ export async function createOptimizeDepsRun( } } -export function newOptimizeDepsProcessingPromise(): OptimizeDepsProcessing { - let resolve: (result?: OptimizeDepsResult) => void +export function newDepOptimizationProcessing(): DepOptimizationProcessing { + let resolve: (result?: DepOptimizationResult) => void const promise = new Promise((_resolve) => { resolve = _resolve - }) as Promise + }) as Promise return { promise, resolve: resolve! } } // Convert to { id: src } -export function depsFromOptimizedInfo( +export function depsFromOptimizedDepInfo( depsInfo: Record ) { return Object.fromEntries( @@ -538,7 +538,7 @@ function getCachedDepFilePath(id: string, depsCacheDir: string) { return normalizePath(path.resolve(depsCacheDir, flattenId(id) + '.js')) } -export function getOptimizedFilePath(id: string, config: ResolvedConfig) { +export function getOptimizedDepPath(id: string, config: ResolvedConfig) { return getCachedDepFilePath(id, getDepsCacheDir(config)) } @@ -576,7 +576,7 @@ export function createIsOptimizedDepUrl(config: ResolvedConfig) { function parseOptimizedDepsMetadata( jsonMetadata: string, depsCacheDir: string, - processing: Promise + processing: Promise ) { const metadata = JSON.parse(jsonMetadata) for (const o of Object.keys(metadata.optimized)) { diff --git a/packages/vite/src/node/optimizer/registerMissing.ts b/packages/vite/src/node/optimizer/registerMissing.ts index 7e3a23a40a4d9e..cdc9e8006c1f52 100644 --- a/packages/vite/src/node/optimizer/registerMissing.ts +++ b/packages/vite/src/node/optimizer/registerMissing.ts @@ -1,15 +1,15 @@ import colors from 'picocolors' import { createOptimizeDepsRun, - getOptimizedFilePath, + getOptimizedDepPath, getOptimizedBrowserHash, - depsFromOptimizedInfo, - newOptimizeDepsProcessingPromise + depsFromOptimizedDepInfo, + newDepOptimizationProcessing } from '.' import type { DepOptimizationMetadata, - OptimizedDepInfo, - OptimizeDepsResult + DepOptimizationResult, + OptimizedDepInfo } from '.' import type { ViteDevServer } from '..' import { resolveSSRExternal } from '../ssr/ssrExternal' @@ -29,9 +29,9 @@ export function createMissingImporterRegisterFn( let handle: NodeJS.Timeout | undefined let needFullReload: boolean = false - let processingMissingDeps = newOptimizeDepsProcessingPromise() + let depOptimizationProcessing = newDepOptimizationProcessing() - let optimizeDepsPromise = metadata.processing + let lastDepOptimizationPromise = metadata.processing async function rerun(ssr: boolean | undefined) { // debounce time to wait for new missing deps finished, issue a new @@ -50,11 +50,11 @@ export function createMissingImporterRegisterFn( // optimizeDeps needs to be run in serie. Await until the previous // rerun is finished here. It could happen that two reruns are queued // in that case, we only need to run one of them - const awaitedOptimizeDepsPromise = optimizeDepsPromise + const awaitedOptimizeDepsPromise = lastDepOptimizationPromise - await optimizeDepsPromise + await lastDepOptimizationPromise - if (awaitedOptimizeDepsPromise !== optimizeDepsPromise) { + if (awaitedOptimizeDepsPromise !== lastDepOptimizationPromise) { // There were two or more rerun queued and one of them already // started. Only let through the first one, and discard the others return @@ -80,16 +80,16 @@ export function createMissingImporterRegisterFn( // All deps, previous known and newly discovered are rebundled, // respect insertion order to keep the metadata file stable const newDeps = { ...metadata.optimized, ...metadata.discovered } - const newDepsProcessing = processingMissingDeps + const thisDepOptimizationProcessing = depOptimizationProcessing // Other rerun will await until this run is finished - optimizeDepsPromise = newDepsProcessing.promise + lastDepOptimizationPromise = thisDepOptimizationProcessing.promise - let processingResult: OptimizeDepsResult | undefined + let processingResult: DepOptimizationResult | undefined // Create a new promise for the next rerun, discovered missing // dependencies will be asigned this promise from this point - processingMissingDeps = newOptimizeDepsProcessingPromise() + depOptimizationProcessing = newDepOptimizationProcessing() let newData: DepOptimizationMetadata | null = null @@ -124,7 +124,7 @@ export function createMissingImporterRegisterFn( delete metadata.discovered[o] } } - newData.processing = newDepsProcessing.promise + newData.processing = thisDepOptimizationProcessing.promise metadata = server._optimizeDepsMetadata = newData if (!needFullReload && !processingResult?.alteredFiles) { @@ -166,7 +166,7 @@ export function createMissingImporterRegisterFn( } finally { // Rerun finished, resolve the promise to let awaiting requests or // other rerun queued be processed - newDepsProcessing.resolve() + thisDepOptimizationProcessing.resolve() } } @@ -195,27 +195,29 @@ export function createMissingImporterRegisterFn( } let missing = metadata.discovered[id] if (missing) { - // We are already discover this dependency, and it will be processed in - // the next rerun call + // We are already discover this dependency + // It will be processed in the next rerun call return missing } missing = metadata.discovered[id] = { - file: getOptimizedFilePath(id, server.config), + file: getOptimizedDepPath(id, server.config), src: resolved, // Assing a browserHash to this missing dependency that is unique to - // the current state of known + missing deps. If the optimizeDeps stage - // ends up with stable paths for the new dep, then we don't need a - // full page reload and this browserHash will be kept + // the current state of known + missing deps. If its optimizeDeps run + // doesn't alter the bundled files of previous known dependendencies, + // we don't need a full reload and this browserHash will be kept browserHash: getOptimizedBrowserHash( - server._optimizeDepsMetadata!.hash, - depsFromOptimizedInfo(metadata.optimized), - depsFromOptimizedInfo(metadata.discovered) + metadata.hash, + depsFromOptimizedDepInfo(metadata.optimized), + depsFromOptimizedDepInfo(metadata.discovered) ), - // loading of this pre-bundle dep needs to await for its processing + // loading of this pre-bundled dep needs to await for its processing // promise to be resolved - processing: processingMissingDeps.promise + processing: depOptimizationProcessing.promise } + // Debounced rerun, let other missing dependencies be discovered before + // the running next optimizeDeps if (handle) clearTimeout(handle) handle = setTimeout(() => { handle = undefined diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 6dec31a0bcf37d..c977b70017973e 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -359,10 +359,10 @@ export async function createServer( transformIndexHtml: null!, // to be immediately set async ssrLoadModule(url) { let configFileDependencies: string[] = [] - const optimizeDepsMetadata = server._optimizeDepsMetadata - if (optimizeDepsMetadata) { - await optimizeDepsMetadata.processing - configFileDependencies = Object.keys(optimizeDepsMetadata.optimized) + const metadata = server._optimizeDepsMetadata + if (metadata) { + await metadata.processing + configFileDependencies = Object.keys(metadata.optimized) } server._ssrExternals ||= resolveSSRExternal(