From 305eefcad651d6c7730b6309f11c36b90d202550 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Fri, 1 Apr 2022 23:05:41 +0200 Subject: [PATCH 1/8] feat: non-blocking needs interop --- packages/vite/src/node/index.ts | 3 +- packages/vite/src/node/optimizer/index.ts | 173 ++++++++++++------ .../src/node/optimizer/registerMissing.ts | 4 +- .../vite/src/node/plugins/importAnalysis.ts | 132 ++++++------- 4 files changed, 176 insertions(+), 136 deletions(-) diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 2e849d846527ca..a6dcd1d2cc56bc 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -41,7 +41,8 @@ export type { DepOptimizationResult, DepOptimizationProcessing, OptimizedDepInfo, - OptimizedDeps + OptimizedDeps, + ExportsData } 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 88c41801938b98..5bb99ac826093c 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -32,6 +32,7 @@ export type ExportsData = ReturnType & { // es-module-lexer has a facade detection but isn't always accurate for our // use case when the module has default export hasReExports?: true + jsxLoader: boolean } export interface OptimizedDeps { @@ -62,6 +63,11 @@ export interface DepOptimizationOptions { * cannot be globs). */ exclude?: string[] + /** + * Force ESM interop when importing for these dependencies. Some legacy + * packages advertise themselves as ESM but use `require` internally + */ + needsInterop?: string[] /** * Options to pass to esbuild during the dep scanning and optimization * @@ -131,6 +137,11 @@ export interface OptimizedDepInfo { * but the bundles may not yet be saved to disk */ processing?: Promise + /** + * ExportData cache, discovered deps will parse the src entry to get exports + * data used both to define if interop is needed and when pre-bundling + */ + exportsData?: Promise } export interface DepOptimizationMetadata { @@ -294,12 +305,13 @@ export async function discoverProjectDependencies( ) const discovered: Record = {} for (const id in deps) { - const entry = deps[id] + const src = deps[id] discovered[id] = { id, file: getOptimizedDepPath(id, config), - src: entry, - browserHash: browserHash + src, + browserHash: browserHash, + exportsData: extractExportsData(src, config) } } return discovered @@ -389,52 +401,24 @@ export async function runOptimizeDeps( const { plugins = [], ...esbuildOptions } = config.optimizeDeps?.esbuildOptions ?? {} - await init for (const id in depsInfo) { - const flatId = flattenId(id) - const filePath = (flatIdDeps[flatId] = depsInfo[id].src!) - let exportsData: ExportsData - if (config.optimizeDeps.extensions?.some((ext) => filePath.endsWith(ext))) { - // For custom supported extensions, build the entry file to transform it into JS, - // and then parse with es-module-lexer. Note that the `bundle` option is not `true`, - // so only the entry file is being transformed. - const result = await build({ - ...esbuildOptions, - plugins, - entryPoints: [filePath], - write: false, - format: 'esm' - }) - exportsData = parse(result.outputFiles[0].text) as ExportsData - } else { - const entryContent = fs.readFileSync(filePath, 'utf-8') - 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 - } + const src = depsInfo[id].src! + const exportsData = await (depsInfo[id].exportsData ?? + extractExportsData(src, config)) + if (exportsData.jsxLoader) { + // 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 flatId = flattenId(id) + flatIdDeps[flatId] = src idToExports[id] = exportsData flatIdToExports[flatId] = exportsData + + depsInfo[id].needsInterop ??= needsInterop(id, idToExports[id], config) } const define: Record = { @@ -479,9 +463,9 @@ export async function runOptimizeDeps( for (const id in depsInfo) { const output = esbuildOutputFromId(meta.outputs, id, processingCacheDir) + const { exportsData, ...info } = depsInfo[id] addOptimizedDepInfo(metadata, 'optimized', { - ...depsInfo[id], - needsInterop: needsInterop(id, idToExports[id], output), + ...info, // 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 fileHash: getHash( @@ -489,6 +473,17 @@ export async function runOptimizeDeps( ), browserHash: metadata.browserHash }) + + // After bundling we have more information and can warn the user about legacy packages + // that require manual configuration + if ( + !depsInfo[id].needsInterop && + hasExportsMismatch(idToExports[id], output) + ) { + config.logger.warn( + `${id} needs ES Interop, add it to the optimizeDeps.needsInterop config array` + ) + } } for (const o of Object.keys(meta.outputs)) { @@ -738,6 +733,55 @@ function esbuildOutputFromId( ] } +export async function extractExportsData( + filePath: string, + config: ResolvedConfig +): Promise { + await init + let exportsData: ExportsData + + const esbuildOptions = config.optimizeDeps?.esbuildOptions ?? {} + if (config.optimizeDeps.extensions?.some((ext) => filePath.endsWith(ext))) { + // For custom supported extensions, build the entry file to transform it into JS, + // and then parse with es-module-lexer. Note that the `bundle` option is not `true`, + // so only the entry file is being transformed. + const result = await build({ + ...esbuildOptions, + entryPoints: [filePath], + write: false, + format: 'esm' + }) + exportsData = parse(result.outputFiles[0].text) as ExportsData + } else { + const entryContent = fs.readFileSync(filePath, 'utf-8') + try { + exportsData = parse(entryContent) as ExportsData + } catch { + debug( + `Unable to parse: ${filePath}.\n 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 + exportsData.jsxLoader = true + } + for (const { ss, se } of exportsData[0]) { + const exp = entryContent.slice(ss, se) + if (/export\s+\*\s+from/.test(exp)) { + exportsData.hasReExports = true + } + } + } + return exportsData +} + // https://github.com/vitejs/vite/issues/1724#issuecomment-767619642 // a list of modules that pretends to be ESM but still uses `require`. // this causes esbuild to wrap them as CJS even when its entry appears to be ESM. @@ -746,9 +790,12 @@ const KNOWN_INTEROP_IDS = new Set(['moment']) function needsInterop( id: string, exportsData: ExportsData, - output: { exports: string[] } + config: ResolvedConfig ): boolean { - if (KNOWN_INTEROP_IDS.has(id)) { + if ( + config.optimizeDeps?.needsInterop?.includes(id) || + KNOWN_INTEROP_IDS.has(id) + ) { return true } const [imports, exports] = exportsData @@ -756,10 +803,19 @@ function needsInterop( if (!exports.length && !imports.length) { return true } + // ESM module + return false +} + +function hasExportsMismatch( + exportsData: ExportsData, + output: { exports: string[] } +): boolean { + const [, exports] = exportsData // if a peer dependency used require() on a ESM dependency, esbuild turns the - // ESM dependency's entry chunk into a single default export... detect - // such cases by checking exports mismatch, and force interop. + // ESM dependency's entry chunk into a single default export... we can detect + // such cases by checking exports mismatch, and warn the user. const generatedExports: string[] = output.exports if ( @@ -853,14 +909,17 @@ function findOptimizedDepInfoInRecord( export async function optimizedDepNeedsInterop( metadata: DepOptimizationMetadata, - file: string + file: string, + config: ResolvedConfig ): Promise { const depInfo = optimizedDepInfoFromFile(metadata, file) - - if (!depInfo) return undefined - - // Wait until the dependency has been pre-bundled - await depInfo.processing - + if (depInfo?.src && depInfo.needsInterop === undefined) { + depInfo.exportsData ??= extractExportsData(depInfo.src, config) + depInfo.needsInterop = needsInterop( + depInfo.id, + await depInfo.exportsData, + config + ) + } return depInfo?.needsInterop } diff --git a/packages/vite/src/node/optimizer/registerMissing.ts b/packages/vite/src/node/optimizer/registerMissing.ts index ee4824389c202b..b99a0523b3b8cb 100644 --- a/packages/vite/src/node/optimizer/registerMissing.ts +++ b/packages/vite/src/node/optimizer/registerMissing.ts @@ -11,6 +11,7 @@ import { addOptimizedDepInfo, discoverProjectDependencies, depsLogString, + extractExportsData, debuggerViteDeps as debug } from '.' import type { @@ -379,7 +380,8 @@ export function createOptimizedDeps(server: ViteDevServer): OptimizedDeps { ), // loading of this pre-bundled dep needs to await for its processing // promise to be resolved - processing: depOptimizationProcessing.promise + processing: depOptimizationProcessing.promise, + exportsData: extractExportsData(resolved, config) }) // Debounced rerun, let other missing dependencies be discovered before diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index f6d6c410411712..344d71b1896daa 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -305,11 +305,6 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { return [url, resolved.id] } - // Import rewrites, we do them after all the URLs have been resolved - // to help with the discovery of new dependencies. If we need to wait - // for each dependency there could be one reload per import - const importRewrites: (() => Promise)[] = [] - for (let index = 0; index < imports.length; index++) { const { s: start, @@ -437,75 +432,66 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { server?.moduleGraph.safeModulesPath.add(fsPathFromUrl(url)) if (url !== specifier) { - importRewrites.push(async () => { - let rewriteDone = false - if ( - server?._optimizedDeps && - isOptimizedDepFile(resolvedId, config) && - !resolvedId.match(optimizedDepChunkRE) - ) { - // for optimized cjs deps, support named imports by rewriting named imports to const assignments. - // internal optimized chunks don't need es interop and are excluded - - // 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 needsInterop = await optimizedDepNeedsInterop( - server._optimizedDeps!.metadata, - file - ) - - if (needsInterop === undefined) { - // Non-entry dynamic imports from dependencies will reach here as there isn't - // optimize info for them, but they don't need es interop. If the request isn't - // a dynamic import, then it is an internal Vite error - if (!file.match(optimizedDepDynamicRE)) { - config.logger.error( - colors.red( - `Vite Error, ${url} optimized info should be defined` - ) - ) - } - } else if (needsInterop) { - debug(`${url} needs interop`) - if (isDynamicImport) { - // rewrite `import('package')` to expose the default directly - str().overwrite( - expStart, - expEnd, - `import('${url}').then(m => m.default && m.default.__esModule ? m.default : ({ ...m.default, default: m.default }))`, - { contentOnly: true } + let rewriteDone = false + if ( + server?._optimizedDeps && + isOptimizedDepFile(resolvedId, config) && + !resolvedId.match(optimizedDepChunkRE) + ) { + // for optimized cjs deps, support named imports by rewriting named imports to const assignments. + // internal optimized chunks don't need es interop and are excluded + + // 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 needsInterop = await optimizedDepNeedsInterop( + server._optimizedDeps!.metadata, + file, + config + ) + + if (needsInterop === undefined) { + // Non-entry dynamic imports from dependencies will reach here as there isn't + // optimize info for them, but they don't need es interop. If the request isn't + // a dynamic import, then it is an internal Vite error + if (!file.match(optimizedDepDynamicRE)) { + config.logger.error( + colors.red( + `Vite Error, ${url} optimized info should be defined` ) + ) + } + } else if (needsInterop) { + debug(`${url} needs interop`) + if (isDynamicImport) { + // rewrite `import('package')` to expose the default directly + str().overwrite( + expStart, + expEnd, + `import('${url}').then(m => m.default && m.default.__esModule ? m.default : ({ ...m.default, default: m.default }))`, + { contentOnly: true } + ) + } else { + const exp = source.slice(expStart, expEnd) + const rewritten = transformCjsImport(exp, url, rawUrl, index) + if (rewritten) { + str().overwrite(expStart, expEnd, rewritten, { + contentOnly: true + }) } else { - const exp = source.slice(expStart, expEnd) - const rewritten = transformCjsImport( - exp, - url, - rawUrl, - index - ) - if (rewritten) { - str().overwrite(expStart, expEnd, rewritten, { - contentOnly: true - }) - } else { - // #1439 export * from '...' - str().overwrite(start, end, url, { contentOnly: true }) - } + // #1439 export * from '...' + str().overwrite(start, end, url, { contentOnly: true }) } - rewriteDone = true } + rewriteDone = true } - if (!rewriteDone) { - str().overwrite( - start, - end, - isDynamicImport ? `'${url}'` : url, - { contentOnly: true } - ) - } - }) + } + if (!rewriteDone) { + str().overwrite(start, end, isDynamicImport ? `'${url}'` : url, { + contentOnly: true + }) + } } // record for HMR import chain analysis @@ -670,14 +656,6 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { }) } - // Await for import rewrites that requires dependencies to be pre-bundled to - // know if es interop is needed after starting further transformRequest calls - // This will let Vite process deeper into the user code and find more missing - // dependencies before the next page reload - for (const rewrite of importRewrites) { - await rewrite() - } - if (s) { return s.toString() } else { From bfff06a8f9ec1d1520cce20d1b4ec049c83b8ccd Mon Sep 17 00:00:00 2001 From: patak Date: Sat, 2 Apr 2022 14:04:24 +0200 Subject: [PATCH 2/8] chore: update jsxLoader hint Co-authored-by: Bjorn Lu --- packages/vite/src/node/optimizer/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 5bb99ac826093c..eea5f6cefb196c 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -32,7 +32,8 @@ export type ExportsData = ReturnType & { // es-module-lexer has a facade detection but isn't always accurate for our // use case when the module has default export hasReExports?: true - jsxLoader: boolean + // hint if the dep requires loading as jsx + jsxLoader?: true } export interface OptimizedDeps { From 19e7168cbe1c53d9f20dd52d43e3676f99b76a55 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Sat, 2 Apr 2022 22:52:36 +0200 Subject: [PATCH 3/8] fix: avoid the breaking change --- packages/vite/src/node/optimizer/index.ts | 49 +++++++------------ .../src/node/optimizer/registerMissing.ts | 16 +++++- 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index eea5f6cefb196c..263eb40ce1f4a8 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -112,6 +112,7 @@ export interface DepOptimizationOptions { export interface DepOptimizationResult { metadata: DepOptimizationMetadata + needsInteropMismatch: string[] /** * When doing a re-run, if there are newly discovered dependendencies * the page reload will be delayed until the next rerun so we need @@ -378,17 +379,25 @@ export async function runOptimizeDeps( const qualifiedIds = Object.keys(depsInfo) - if (!qualifiedIds.length) { - return { - metadata, - commit() { - // Write metadata file, delete `deps` folder and rename the `processing` folder to `deps` - commitProcessingDepsCacheSync() - }, - cancel + const processingResult: DepOptimizationResult = { + metadata, + needsInteropMismatch: [], + commit() { + // Write metadata file, delete `deps` folder and rename the `processing` folder to `deps` + // Processing is done, we can now replace the depsCacheDir with processingCacheDir + // Rewire the file paths from the temporal processing dir to the final deps cache dir + removeDirSync(depsCacheDir) + fs.renameSync(processingCacheDir, depsCacheDir) + }, + cancel() { + removeDirSync(processingCacheDir) } } + if (!qualifiedIds.length) { + return processingResult + } + // 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: @@ -481,9 +490,7 @@ export async function runOptimizeDeps( !depsInfo[id].needsInterop && hasExportsMismatch(idToExports[id], output) ) { - config.logger.warn( - `${id} needs ES Interop, add it to the optimizeDeps.needsInterop config array` - ) + processingResult.needsInteropMismatch.push(id) } } @@ -514,25 +521,7 @@ export async function runOptimizeDeps( debug(`deps bundled in ${(performance.now() - start).toFixed(2)}ms`) - return { - metadata, - commit() { - // Write metadata file, delete `deps` folder and rename the new `processing` folder to `deps` in sync - commitProcessingDepsCacheSync() - }, - cancel - } - - function commitProcessingDepsCacheSync() { - // Processing is done, we can now replace the depsCacheDir with processingCacheDir - // Rewire the file paths from the temporal processing dir to the final deps cache dir - removeDirSync(depsCacheDir) - fs.renameSync(processingCacheDir, depsCacheDir) - } - - function cancel() { - removeDirSync(processingCacheDir) - } + return processingResult } function removeDirSync(dir: string) { diff --git a/packages/vite/src/node/optimizer/registerMissing.ts b/packages/vite/src/node/optimizer/registerMissing.ts index b99a0523b3b8cb..e61e1bf0a99143 100644 --- a/packages/vite/src/node/optimizer/registerMissing.ts +++ b/packages/vite/src/node/optimizer/registerMissing.ts @@ -176,13 +176,14 @@ export function createOptimizedDeps(server: ViteDevServer): OptimizedDeps { try { const processingResult = await runOptimizeDeps(config, newDeps) - const newData = processingResult.metadata + const { metadata: newData, needsInteropMismatch } = processingResult // After a re-optimization, if the internal bundled chunks change a full page reload // is required. If the files are stable, we can avoid the reload that is expensive // for large applications. Comparing their fileHash we can find out if it is safe to // keep the current browser state. const needsReload = + needsInteropMismatch.length > 0 || metadata.hash !== newData.hash || Object.keys(metadata.optimized).some((dep) => { return ( @@ -285,6 +286,19 @@ export function createOptimizedDeps(server: ViteDevServer): OptimizedDeps { timestamp: true } ) + if (needsInteropMismatch.length > 0) { + config.logger.warn( + `Mixed ESM and CJS detected in ${colors.yellow( + needsInteropMismatch.join(', ') + )}, add ${ + needsInteropMismatch.length === 1 ? 'it' : 'them' + } to optimizeDeps.needsInterop to speed up cold start`, + { + timestamp: true + } + ) + } + fullReload() } } From 5837ce92912331ea6c52ac5c06a994cab8e2eddc Mon Sep 17 00:00:00 2001 From: patak-dev Date: Sun, 3 Apr 2022 11:27:54 +0200 Subject: [PATCH 4/8] fix: handle edge case and simplify --- packages/vite/src/node/optimizer/index.ts | 55 +++++++------------ .../src/node/optimizer/registerMissing.ts | 18 +++++- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 263eb40ce1f4a8..913640686f7765 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -112,7 +112,6 @@ export interface DepOptimizationOptions { export interface DepOptimizationResult { metadata: DepOptimizationMetadata - needsInteropMismatch: string[] /** * When doing a re-run, if there are newly discovered dependendencies * the page reload will be delayed until the next rerun so we need @@ -381,7 +380,6 @@ export async function runOptimizeDeps( const processingResult: DepOptimizationResult = { metadata, - needsInteropMismatch: [], commit() { // Write metadata file, delete `deps` folder and rename the `processing` folder to `deps` // Processing is done, we can now replace the depsCacheDir with processingCacheDir @@ -427,8 +425,6 @@ export async function runOptimizeDeps( flatIdDeps[flatId] = src idToExports[id] = exportsData flatIdToExports[flatId] = exportsData - - depsInfo[id].needsInterop ??= needsInterop(id, idToExports[id], config) } const define: Record = { @@ -481,17 +477,11 @@ export async function runOptimizeDeps( fileHash: getHash( metadata.hash + depsInfo[id].file + JSON.stringify(output.imports) ), - browserHash: metadata.browserHash + browserHash: metadata.browserHash, + // After bundling we have more information and can warn the user about legacy packages + // that require manual configuration + needsInterop: needsInterop(config, id, idToExports[id], output) }) - - // After bundling we have more information and can warn the user about legacy packages - // that require manual configuration - if ( - !depsInfo[id].needsInterop && - hasExportsMismatch(idToExports[id], output) - ) { - processingResult.needsInteropMismatch.push(id) - } } for (const o of Object.keys(meta.outputs)) { @@ -778,9 +768,10 @@ export async function extractExportsData( const KNOWN_INTEROP_IDS = new Set(['moment']) function needsInterop( + config: ResolvedConfig, id: string, exportsData: ExportsData, - config: ResolvedConfig + output?: { exports: string[] } ): boolean { if ( config.optimizeDeps?.needsInterop?.includes(id) || @@ -793,26 +784,20 @@ function needsInterop( if (!exports.length && !imports.length) { return true } - // ESM module - return false -} -function hasExportsMismatch( - exportsData: ExportsData, - output: { exports: string[] } -): boolean { - const [, exports] = exportsData - - // if a peer dependency used require() on a ESM dependency, esbuild turns the - // ESM dependency's entry chunk into a single default export... we can detect - // such cases by checking exports mismatch, and warn the user. - const generatedExports: string[] = output.exports + if (output) { + // if a peer dependency used require() on a ESM dependency, esbuild turns the + // ESM dependency's entry chunk into a single default export... detect + // such cases by checking exports mismatch, and force interop. + const generatedExports: string[] = output.exports - if ( - !generatedExports || - (isSingleDefaultExport(generatedExports) && !isSingleDefaultExport(exports)) - ) { - return true + if ( + !generatedExports || + (isSingleDefaultExport(generatedExports) && + !isSingleDefaultExport(exports)) + ) { + return true + } } return false } @@ -906,9 +891,9 @@ export async function optimizedDepNeedsInterop( if (depInfo?.src && depInfo.needsInterop === undefined) { depInfo.exportsData ??= extractExportsData(depInfo.src, config) depInfo.needsInterop = needsInterop( + config, depInfo.id, - await depInfo.exportsData, - config + await depInfo.exportsData ) } return depInfo?.needsInterop diff --git a/packages/vite/src/node/optimizer/registerMissing.ts b/packages/vite/src/node/optimizer/registerMissing.ts index e61e1bf0a99143..203563db6b4172 100644 --- a/packages/vite/src/node/optimizer/registerMissing.ts +++ b/packages/vite/src/node/optimizer/registerMissing.ts @@ -176,7 +176,23 @@ export function createOptimizedDeps(server: ViteDevServer): OptimizedDeps { try { const processingResult = await runOptimizeDeps(config, newDeps) - const { metadata: newData, needsInteropMismatch } = processingResult + const newData = processingResult.metadata + + const needsInteropMismatch = [] + for (const dep in metadata.discovered) { + const discoveredDepInfo = metadata.discovered[dep] + const depInfo = newData.optimized[dep] + if (depInfo) { + if ( + discoveredDepInfo.needsInterop !== undefined && + depInfo.needsInterop !== discoveredDepInfo.needsInterop + ) { + // This only happens when a discovered dependency has mixed ESM and CJS syntax + // and it hasn't been manually added to optimizeDeps.needsInterop + needsInteropMismatch.push(dep) + } + } + } // After a re-optimization, if the internal bundled chunks change a full page reload // is required. If the files are stable, we can avoid the reload that is expensive From 80934990ca8391ae56931a4eb54907b03b8d5d14 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Mon, 23 May 2022 23:27:41 +0200 Subject: [PATCH 5/8] test: try to exclude external component --- playground/ssr-vue/vite.config.js | 3 +++ playground/ssr-vue/vite.config.noexternal.js | 3 +++ 2 files changed, 6 insertions(+) diff --git a/playground/ssr-vue/vite.config.js b/playground/ssr-vue/vite.config.js index 03277fd6560551..83128683536388 100644 --- a/playground/ssr-vue/vite.config.js +++ b/playground/ssr-vue/vite.config.js @@ -51,5 +51,8 @@ module.exports = { // this package has uncompiled .vue files 'example-external-component' ] + }, + optimizeDeps: { + exclude: ['example-external-component'] } } diff --git a/playground/ssr-vue/vite.config.noexternal.js b/playground/ssr-vue/vite.config.noexternal.js index ac74bf1430e94e..ce9a389defc68b 100644 --- a/playground/ssr-vue/vite.config.noexternal.js +++ b/playground/ssr-vue/vite.config.noexternal.js @@ -18,5 +18,8 @@ module.exports = Object.assign(config, { replacement: '@vue/runtime-core/dist/runtime-core.cjs.js' } ] + }, + optimizeDeps: { + exclude: ['example-external-component'] } }) From 0872fe40dd227d8c2bbb13a7e312b565e1a26fdb Mon Sep 17 00:00:00 2001 From: bluwy Date: Tue, 24 May 2022 14:52:24 +0800 Subject: [PATCH 6/8] chore: fix ssr-vue test --- playground/ssr-vue/__tests__/ssr-vue.spec.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/playground/ssr-vue/__tests__/ssr-vue.spec.ts b/playground/ssr-vue/__tests__/ssr-vue.spec.ts index c58cea4cd13e59..4988241581a4a0 100644 --- a/playground/ssr-vue/__tests__/ssr-vue.spec.ts +++ b/playground/ssr-vue/__tests__/ssr-vue.spec.ts @@ -13,7 +13,8 @@ import { const url = `http://localhost:${port}` test('vuex can be import succeed by named import', async () => { - await page.goto(url + '/store') + // wait networkidle for dynamic optimize vuex + await page.goto(url + '/store', { waitUntil: 'networkidle' }) expect(await page.textContent('h1')).toMatch('bar') // raw http request @@ -111,19 +112,20 @@ test('/', async () => { }) test('css', async () => { + await page.goto(url) if (isBuild) { expect(await getColor('h1')).toBe('green') expect(await getColor('.jsx')).toBe('blue') } else { // During dev, the CSS is loaded from async chunk and we may have to wait // when the test runs concurrently. - await page.waitForLoadState('networkidle') await untilUpdated(() => getColor('h1'), 'green') await untilUpdated(() => getColor('.jsx'), 'blue') } }) test('asset', async () => { + await page.goto(url) // should have no 404s browserLogs.forEach((msg) => { expect(msg).not.toMatch('404') @@ -135,36 +137,39 @@ test('asset', async () => { }) test('jsx', async () => { + await page.goto(url) expect(await page.textContent('.jsx')).toMatch('from JSX') }) test('virtual module', async () => { + await page.goto(url) expect(await page.textContent('.virtual')).toMatch('hi') }) test('nested virtual module', async () => { + await page.goto(url) expect(await page.textContent('.nested-virtual')).toMatch('[success]') }) test('hydration', async () => { + await page.goto(url) expect(await page.textContent('button')).toMatch('0') await page.click('button') - await page.waitForLoadState('networkidle') expect(await page.textContent('button')).toMatch('1') }) test('hmr', async () => { + await page.goto(url) editFile('src/pages/Home.vue', (code) => code.replace('Home', 'changed')) - await page.waitForLoadState('networkidle') await untilUpdated(() => page.textContent('h1'), 'changed') }) test('client navigation', async () => { + await page.goto(url) await untilUpdated(() => page.textContent('a[href="/about"]'), 'About') await page.click('a[href="/about"]') await untilUpdated(() => page.textContent('h1'), 'About') editFile('src/pages/About.vue', (code) => code.replace('About', 'changed')) - await page.waitForLoadState('networkidle') await untilUpdated(() => page.textContent('h1'), 'changed') await page.click('a[href="/"]') await untilUpdated(() => page.textContent('a[href="/"]'), 'Home') From 1c383957fa8832b00ebf04141ee0323087b738ab Mon Sep 17 00:00:00 2001 From: bluwy Date: Tue, 24 May 2022 15:00:38 +0800 Subject: [PATCH 7/8] chore: update ssr-react tests --- playground/ssr-react/__tests__/ssr-react.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/playground/ssr-react/__tests__/ssr-react.spec.ts b/playground/ssr-react/__tests__/ssr-react.spec.ts index 62bb7d2014f770..49b031f9e70fd8 100644 --- a/playground/ssr-react/__tests__/ssr-react.spec.ts +++ b/playground/ssr-react/__tests__/ssr-react.spec.ts @@ -40,6 +40,7 @@ test('/', async () => { }) test('hmr', async () => { + await page.goto(url) editFile('src/pages/Home.jsx', (code) => code.replace('

Home', '

changed') ) @@ -47,6 +48,7 @@ test('hmr', async () => { }) test('client navigation', async () => { + await page.goto(url) await untilUpdated(() => page.textContent('a[href="/about"]'), 'About') await page.click('a[href="/about"]') await untilUpdated(() => page.textContent('h1'), 'About') From b0070c20c44b6426fe144806d85529cd37e1e151 Mon Sep 17 00:00:00 2001 From: patak-dev Date: Wed, 25 May 2022 19:26:34 +0200 Subject: [PATCH 8/8] chore: mark new config option as experimental --- packages/vite/src/node/optimizer/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 8a3b32d893b67a..9fcd19b92062af 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -69,6 +69,7 @@ export interface DepOptimizationOptions { /** * Force ESM interop when importing for these dependencies. Some legacy * packages advertise themselves as ESM but use `require` internally + * @experimental */ needsInterop?: string[] /**