diff --git a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts b/packages/vite/src/node/optimizer/esbuildDepPlugin.ts index 8803ae3a1f5c48..d41caca22d2d4a 100644 --- a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts +++ b/packages/vite/src/node/optimizer/esbuildDepPlugin.ts @@ -5,6 +5,7 @@ import { KNOWN_ASSET_TYPES } from '../constants' import type { ResolvedConfig } from '..' import { flattenId, + isBuiltin, isExternalUrl, isRunningWithYarnPnp, moduleListContains, @@ -42,7 +43,8 @@ const externalTypes = [ export function esbuildDepPlugin( qualified: Record, exportsData: Record, - config: ResolvedConfig + config: ResolvedConfig, + ssr: boolean ): Plugin { // remove optimizable extensions from `externalTypes` list const allExternalTypes = config.optimizeDeps.extensions @@ -77,7 +79,7 @@ export function esbuildDepPlugin( _importer = importer in qualified ? qualified[importer] : importer } const resolver = kind.startsWith('require') ? _resolveRequire : _resolve - return resolver(id, _importer, undefined) + return resolver(id, _importer, undefined, ssr) } const resolveResult = (id: string, resolved: string) => { @@ -87,6 +89,9 @@ export function esbuildDepPlugin( namespace: 'browser-external' } } + if (ssr && isBuiltin(resolved)) { + return + } if (isExternalUrl(resolved)) { return { path: resolved, diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 9024aca62ac1bd..11d9e83d6a0f90 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -26,7 +26,11 @@ import { transformWithEsbuild } from '../plugins/esbuild' import { ESBUILD_MODULES_TARGET } from '../constants' import { esbuildDepPlugin } from './esbuildDepPlugin' import { scanImports } from './scan' -export { initDepsOptimizer, getDepsOptimizer } from './optimizer' +export { + initDepsOptimizer, + initDevSsrDepsOptimizer, + getDepsOptimizer +} from './optimizer' export const debuggerViteDeps = createDebugger('vite:deps') const debug = debuggerViteDeps @@ -47,7 +51,7 @@ export type ExportsData = { } export interface DepsOptimizer { - metadata: (options: { ssr: boolean }) => DepOptimizationMetadata + metadata: DepOptimizationMetadata scanProcessing?: Promise registerMissingImport: ( id: string, @@ -546,6 +550,9 @@ export async function runOptimizeDeps( : JSON.stringify(process.env.NODE_ENV || config.mode) } + const platform = + ssr && config.ssr?.target !== 'webworker' ? 'node' : 'browser' + const start = performance.now() const result = await build({ @@ -555,9 +562,16 @@ export async function runOptimizeDeps( // We can't use platform 'neutral', as esbuild has custom handling // when the platform is 'node' or 'browser' that can't be emulated // by using mainFields and conditions - platform: ssr && config.ssr?.target !== 'webworker' ? 'node' : 'browser', + platform, define, format: 'esm', + // See https://github.com/evanw/esbuild/issues/1921#issuecomment-1152991694 + banner: + platform === 'node' + ? { + js: `import { createRequire } from 'module';const require = createRequire(import.meta.url);` + } + : undefined, target: isBuild ? config.build.target || undefined : ESBUILD_MODULES_TARGET, external: config.optimizeDeps?.exclude, logLevel: 'error', @@ -568,7 +582,7 @@ export async function runOptimizeDeps( metafile: true, plugins: [ ...plugins, - esbuildDepPlugin(flatIdDeps, flatIdToExports, config) + esbuildDepPlugin(flatIdDeps, flatIdToExports, config, ssr) ], ...esbuildOptions, supported: { diff --git a/packages/vite/src/node/optimizer/optimizer.ts b/packages/vite/src/node/optimizer/optimizer.ts index 37a3aef745b4f3..def39f2e8c8361 100644 --- a/packages/vite/src/node/optimizer/optimizer.ts +++ b/packages/vite/src/node/optimizer/optimizer.ts @@ -24,7 +24,6 @@ import { runOptimizeDeps } from '.' import type { - DepOptimizationMetadata, DepOptimizationProcessing, DepsOptimizer, OptimizedDepInfo @@ -39,18 +38,36 @@ const isDebugEnabled = _debug('vite:deps').enabled const debounceMs = 100 const depsOptimizerMap = new WeakMap() +const devSsrDepsOptimizerMap = new WeakMap() export function getDepsOptimizer( - config: ResolvedConfig + config: ResolvedConfig, + type: { ssr?: boolean } ): DepsOptimizer | undefined { // Workers compilation shares the DepsOptimizer from the main build - return depsOptimizerMap.get(config.mainConfig || config) + const isDevSsr = type.ssr && config.command !== 'build' + return (isDevSsr ? devSsrDepsOptimizerMap : depsOptimizerMap).get( + config.mainConfig || config + ) } export async function initDepsOptimizer( config: ResolvedConfig, server?: ViteDevServer -): Promise { +): Promise { + await createDepsOptimizer(config, server) +} + +export async function initDevSsrDepsOptimizer( + config: ResolvedConfig +): Promise { + await createDevSsrDepsOptimizer(config) +} + +async function createDepsOptimizer( + config: ResolvedConfig, + server?: ViteDevServer +): Promise { const { logger } = config const isBuild = config.command === 'build' @@ -62,18 +79,11 @@ export async function initDepsOptimizer( let handle: NodeJS.Timeout | undefined - let ssrServerDepsMetadata: DepOptimizationMetadata - let _metadata = + let metadata = cachedMetadata || initDepsOptimizerMetadata(config, sessionTimestamp) const depsOptimizer: DepsOptimizer = { - metadata: (options: { ssr: boolean }) => { - if (isBuild || !options.ssr) { - return _metadata - } else { - return ssrServerDepsMetadata - } - }, + metadata, registerMissingImport, run: () => debouncedProcessing(0), isOptimizedDepFile: (id: string) => isOptimizedDepFile(id, config), @@ -89,10 +99,6 @@ export async function initDepsOptimizer( depsOptimizerMap.set(config, depsOptimizer) - if (!isBuild && config.ssr) { - ssrServerDepsMetadata = await optimizeServerSsrDeps(config) - } - let newDepsDiscovered = false let newDepsToLog: string[] = [] @@ -137,7 +143,6 @@ export async function initDepsOptimizer( config, sessionTimestamp ) - const metadata = _metadata for (const depInfo of Object.values(discovered)) { addOptimizedDepInfo(metadata, 'discovered', { ...depInfo, @@ -155,8 +160,6 @@ export async function initDepsOptimizer( try { debug(colors.green(`scanning for dependencies...`)) - const metadata = _metadata - const discovered = await discoverProjectDependencies( config, sessionTimestamp @@ -201,7 +204,7 @@ export async function initDepsOptimizer( // Ensure that a rerun will not be issued for current discovered deps if (handle) clearTimeout(handle) - if (Object.keys(_metadata.discovered).length === 0) { + if (Object.keys(metadata.discovered).length === 0) { currentlyProcessing = false return } @@ -217,8 +220,6 @@ export async function initDepsOptimizer( // if the rerun fails, _metadata remains untouched, current discovered // deps are cleaned, and a fullReload is issued - let metadata = _metadata - // All deps, previous known and newly discovered are rebundled, // respect insertion order to keep the metadata file stable @@ -324,7 +325,7 @@ export async function initDepsOptimizer( ) } - metadata = _metadata = newData + metadata = depsOptimizer.metadata = newData resolveEnqueuedProcessingPromises() } @@ -418,7 +419,7 @@ export async function initDepsOptimizer( // 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 - const deps = Object.keys(_metadata.discovered) + const deps = Object.keys(metadata.discovered) const depsString = depsLogString(deps) debug(colors.green(`new dependencies found: ${depsString}`)) runOptimizer() @@ -449,7 +450,6 @@ export async function initDepsOptimizer( `Error: ${id} is a missing dependency in SSR dev server, it needs to be added to optimizeDeps.include` ) } - const metadata = _metadata const optimized = metadata.optimized[id] if (optimized) { return optimized @@ -539,7 +539,7 @@ export async function initDepsOptimizer( if (!firstRunEnsured && !firstRunCalled && registeredIds.length === 0) { setTimeout(() => { if (!firstRunCalled && registeredIds.length === 0) { - getDepsOptimizer(config)?.run() + debouncedProcessing(0) // queue the optimizer run } }, runOptimizerIfIdleAfterMs) } @@ -580,7 +580,7 @@ export async function initDepsOptimizer( if (registeredIds.length > 0) { runOptimizerWhenIdle() } else { - getDepsOptimizer(config)?.run() + debouncedProcessing(0) // queue the optimizer run } } } @@ -596,8 +596,34 @@ export async function initDepsOptimizer( } } } +} - return depsOptimizer +async function createDevSsrDepsOptimizer( + config: ResolvedConfig +): Promise { + const metadata = await optimizeServerSsrDeps(config) + const depsOptimizer = { + metadata, + isOptimizedDepFile: (id: string) => isOptimizedDepFile(id, config), + isOptimizedDepUrl: createIsOptimizedDepUrl(config), + getOptimizedDepId: (depInfo: OptimizedDepInfo) => + `${depInfo.file}?v=${depInfo.browserHash}`, + + registerMissingImport: () => { + throw new Error( + 'Vite Internal Error: registerMissingImport is not supported in dev SSR' + ) + }, + // noop, there is no scanning during dev SSR + // the optimizer blocks the server start + run: () => {}, + registerWorkersSource: (id: string) => {}, + delayDepsOptimizerUntil: (id: string, done: () => Promise) => {}, + resetRegisteredIds: () => {}, + ensureFirstRun: () => {}, + options: config.optimizeDeps + } + devSsrDepsOptimizerMap.set(config, depsOptimizer) } export async function preTransformOptimizeDepsEntries( diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index b08000fbf12930..f9e8fa879bb28a 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -214,7 +214,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { ) } - const depsOptimizer = getDepsOptimizer(config) + const depsOptimizer = getDepsOptimizer(config, { ssr }) const { moduleGraph } = server // since we are already in the transform phase of the importer, it must @@ -275,8 +275,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // the dependency needs to be resolved starting from the original source location of the optimized file // because starting from node_modules/.vite will not find the dependency if it was not hoisted // (that is, if it is under node_modules directory in the package source of the optimized file) - for (const optimizedModule of depsOptimizer.metadata({ ssr }) - .depInfoList) { + for (const optimizedModule of depsOptimizer.metadata.depInfoList) { if (!optimizedModule.src) continue // Ignore chunks if (optimizedModule.file === importerModule.file) { importerFile = optimizedModule.src @@ -479,7 +478,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { const file = cleanUrl(resolvedId) // Remove ?v={hash} const needsInterop = await optimizedDepNeedsInterop( - depsOptimizer.metadata({ ssr }), + depsOptimizer.metadata, file, config ) diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index 331a4d2eaa7ac2..613d7ca5a08503 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -154,7 +154,7 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { } const { root } = config - const depsOptimizer = getDepsOptimizer(config) + const depsOptimizer = getDepsOptimizer(config, { ssr }) const normalizeUrl = async ( url: string, @@ -170,8 +170,7 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { // the dependency needs to be resolved starting from the original source location of the optimized file // because starting from node_modules/.vite will not find the dependency if it was not hoisted // (that is, if it is under node_modules directory in the package source of the optimized file) - for (const optimizedModule of depsOptimizer.metadata({ ssr }) - .depInfoList) { + for (const optimizedModule of depsOptimizer.metadata.depInfoList) { if (!optimizedModule.src) continue // Ignore chunks if (optimizedModule.file === importer) { importerFile = optimizedModule.src @@ -262,7 +261,7 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { const file = cleanUrl(resolvedId) // Remove ?v={hash} const needsInterop = await optimizedDepNeedsInterop( - depsOptimizer.metadata({ ssr }), + depsOptimizer.metadata, file, config ) diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index 8eca908870ae90..9e01808139c4af 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -62,7 +62,8 @@ export async function resolvePlugins( packageCache: config.packageCache, ssrConfig: config.ssr, asSrc: true, - getDepsOptimizer: () => getDepsOptimizer(config), + getDepsOptimizer: (type: { ssr?: boolean }) => + getDepsOptimizer(config, type), shouldExternalize: isBuild && config.build.ssr && config.ssr?.format !== 'cjs' ? (id) => shouldExternalizeForSSR(id, config) diff --git a/packages/vite/src/node/plugins/optimizedDeps.ts b/packages/vite/src/node/plugins/optimizedDeps.ts index 3db6d850f7f286..dff944c3d224e2 100644 --- a/packages/vite/src/node/plugins/optimizedDeps.ts +++ b/packages/vite/src/node/plugins/optimizedDeps.ts @@ -17,8 +17,8 @@ export function optimizedDepsPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:optimized-deps', - async resolveId(id) { - if (getDepsOptimizer(config)?.isOptimizedDepFile(id)) { + async resolveId(id, source, { ssr }) { + if (getDepsOptimizer(config, { ssr })?.isOptimizedDepFile(id)) { return id } }, @@ -28,51 +28,49 @@ export function optimizedDepsPlugin(config: ResolvedConfig): Plugin { // is in importAnalysis, see call to delayDepsOptimizerUntil async load(id, options) { - const ssr = options?.ssr ?? false - const depsOptimizer = getDepsOptimizer(config) + const ssr = options?.ssr === true + const depsOptimizer = getDepsOptimizer(config, { ssr }) if (depsOptimizer?.isOptimizedDepFile(id)) { - const metadata = depsOptimizer?.metadata({ ssr }) - if (metadata) { - const file = cleanUrl(id) - const versionMatch = id.match(DEP_VERSION_RE) - const browserHash = versionMatch - ? versionMatch[1].split('=')[1] - : undefined - - // Search in both the currently optimized and newly discovered deps - const info = optimizedDepInfoFromFile(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 - } 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. - throwProcessingError(id) - return - } - const newMetadata = depsOptimizer.metadata({ ssr }) - if (metadata !== newMetadata) { - const currentInfo = optimizedDepInfoFromFile(newMetadata!, file) - if (info.browserHash !== currentInfo?.browserHash) { - throwOutdatedRequest(id) - } - } + const metadata = depsOptimizer.metadata + const file = cleanUrl(id) + const versionMatch = id.match(DEP_VERSION_RE) + const browserHash = versionMatch + ? versionMatch[1].split('=')[1] + : undefined + + // Search in both the currently optimized and newly discovered deps + const info = optimizedDepInfoFromFile(metadata, file) + if (info) { + if (browserHash && info.browserHash !== browserHash) { + throwOutdatedRequest(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 - throwOutdatedRequest(id) + // 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. + throwProcessingError(id) + return } + const newMetadata = depsOptimizer.metadata + if (metadata !== newMetadata) { + const currentInfo = optimizedDepInfoFromFile(newMetadata!, file) + if (info.browserHash !== currentInfo?.browserHash) { + throwOutdatedRequest(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 + throwOutdatedRequest(id) } } } @@ -85,27 +83,32 @@ export function optimizedDepsBuildPlugin(config: ResolvedConfig): Plugin { buildStart() { if (!config.isWorker) { - getDepsOptimizer(config)?.resetRegisteredIds() + // This will be run for the current active optimizer, during build + // it will be the SSR optimizer if config.build.ssr is defined + getDepsOptimizer(config, { ssr: undefined })?.resetRegisteredIds() } }, - async resolveId(id) { - if (getDepsOptimizer(config)?.isOptimizedDepFile(id)) { + async resolveId(id, importer, { ssr }) { + if (getDepsOptimizer(config, { ssr })?.isOptimizedDepFile(id)) { return id } }, - transform(_code, id) { - getDepsOptimizer(config)?.delayDepsOptimizerUntil(id, async () => { - await this.load({ id }) - }) + transform(_code, id, options) { + const ssr = options?.ssr === true + getDepsOptimizer(config, { ssr })?.delayDepsOptimizerUntil( + id, + async () => { + await this.load({ id }) + } + ) }, async load(id, options) { - const ssr = options?.ssr ?? false - const depsOptimizer = getDepsOptimizer(config) - const metadata = depsOptimizer?.metadata({ ssr }) - if (!metadata || !depsOptimizer?.isOptimizedDepFile(id)) { + const ssr = options?.ssr === true + const depsOptimizer = getDepsOptimizer(config, { ssr }) + if (!depsOptimizer?.isOptimizedDepFile(id)) { return } @@ -114,7 +117,7 @@ export function optimizedDepsBuildPlugin(config: ResolvedConfig): Plugin { const file = cleanUrl(id) // Search in both the currently optimized and newly discovered deps // If all the inputs are dependencies, we aren't going to get any - const info = optimizedDepInfoFromFile(metadata, file) + const info = optimizedDepInfoFromFile(depsOptimizer.metadata, file) if (info) { try { // This is an entry point, it may still not be bundled diff --git a/packages/vite/src/node/plugins/preAlias.ts b/packages/vite/src/node/plugins/preAlias.ts index 46bc2436b0686a..a99d9b02aafc02 100644 --- a/packages/vite/src/node/plugins/preAlias.ts +++ b/packages/vite/src/node/plugins/preAlias.ts @@ -11,10 +11,10 @@ export function preAliasPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:pre-alias', async resolveId(id, importer, options) { - const ssr = options?.ssr ?? false - const depsOptimizer = getDepsOptimizer(config) + const ssr = options?.ssr === true + const depsOptimizer = getDepsOptimizer(config, { ssr }) if (depsOptimizer && bareImportRE.test(id) && !options?.scan) { - return await tryOptimizedResolve(depsOptimizer, ssr, id, importer) + return await tryOptimizedResolve(depsOptimizer, id, importer) } } } diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index 76fcfcbdb298f4..4967b7975c9a54 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -83,7 +83,7 @@ export interface InternalResolveOptions extends ResolveOptions { // True when resolving during the scan phase to discover dependencies scan?: boolean // Resolve using esbuild deps optimization - getDepsOptimizer?: () => DepsOptimizer | undefined + getDepsOptimizer?: (type: { ssr?: boolean }) => DepsOptimizer | undefined shouldExternalize?: (id: string) => boolean | undefined } @@ -102,11 +102,11 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { name: 'vite:resolve', async resolveId(id, importer, resolveOpts) { + const ssr = resolveOpts?.ssr === true + // We need to delay depsOptimizer until here instead of passing it as an option // the resolvePlugin because the optimizer is created on server listen during dev - const depsOptimizer = resolveOptions.getDepsOptimizer?.() - - const ssr = resolveOpts?.ssr === true + const depsOptimizer = resolveOptions.getDepsOptimizer?.({ ssr }) if (id.startsWith(browserExternalId)) { return id @@ -186,7 +186,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { // Inject the current browserHash version if the path doesn't have one if (!normalizedFsPath.match(DEP_VERSION_RE)) { const browserHash = optimizedDepInfoFromFile( - depsOptimizer.metadata({ ssr }), + depsOptimizer.metadata, normalizedFsPath )?.browserHash if (browserHash) { @@ -269,7 +269,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { asSrc && depsOptimizer && !options.scan && - (res = await tryOptimizedResolve(depsOptimizer, ssr, id, importer)) + (res = await tryOptimizedResolve(depsOptimizer, id, importer)) ) { return res } @@ -690,7 +690,7 @@ export function tryNodeResolve( // otherwise we may introduce duplicated modules for externalized files // from pre-bundled deps. if (!isBuild) { - const versionHash = depsOptimizer.metadata({ ssr }).browserHash + const versionHash = depsOptimizer.metadata.browserHash if (versionHash && isJsType) { resolved = injectQuery(resolved, `v=${versionHash}`) } @@ -716,16 +716,12 @@ export function tryNodeResolve( export async function tryOptimizedResolve( depsOptimizer: DepsOptimizer, - ssr: boolean, id: string, importer?: string ): Promise { await depsOptimizer.scanProcessing - const metadata = depsOptimizer.metadata({ ssr }) - if (!metadata) { - return - } + const metadata = depsOptimizer.metadata const depInfo = optimizedDepInfoFromId(metadata, id) if (depInfo) { diff --git a/packages/vite/src/node/plugins/worker.ts b/packages/vite/src/node/plugins/worker.ts index e3f630a60740aa..cf62c91f0f24f9 100644 --- a/packages/vite/src/node/plugins/worker.ts +++ b/packages/vite/src/node/plugins/worker.ts @@ -222,7 +222,8 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { } }, - async transform(raw, id) { + async transform(raw, id, options) { + const ssr = options?.ssr === true const query = parseRequest(id) if (query && query[WORKER_FILE_ID] != null) { // if import worker by worker constructor will had query.type @@ -269,7 +270,7 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { : 'module' const workerOptions = workerType === 'classic' ? '' : ',{type: "module"}' if (isBuild) { - getDepsOptimizer(config)?.registerWorkersSource(id) + getDepsOptimizer(config, { ssr })?.registerWorkersSource(id) if (query.inline != null) { const chunk = await bundleWorkerEntry(config, id, query) // inline as blob data url diff --git a/packages/vite/src/node/plugins/workerImportMetaUrl.ts b/packages/vite/src/node/plugins/workerImportMetaUrl.ts index 5711b48bcedc42..86b8e377a40c71 100644 --- a/packages/vite/src/node/plugins/workerImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/workerImportMetaUrl.ts @@ -76,6 +76,7 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin { name: 'vite:worker-import-meta-url', async transform(code, id, options) { + const ssr = options?.ssr === true if ( !options?.ssr && (code.includes('new Worker') || code.includes('new SharedWorker')) && @@ -117,7 +118,7 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin { let url: string if (isBuild) { - getDepsOptimizer(config)?.registerWorkersSource(id) + getDepsOptimizer(config, { ssr })?.registerWorkersSource(id) url = await workerFileToUrl(config, file, query) } else { url = await fileToUrl(cleanUrl(file), config, this) diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 25645f871a82f2..24a277fa136c08 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -28,7 +28,11 @@ import { ssrRewriteStacktrace } from '../ssr/ssrStacktrace' import { ssrTransform } from '../ssr/ssrTransform' -import { getDepsOptimizer, initDepsOptimizer } from '../optimizer' +import { + getDepsOptimizer, + initDepsOptimizer, + initDevSsrDepsOptimizer +} from '../optimizer' import { CLIENT_DIR } from '../constants' import type { Logger } from '../logger' import { printCommonServerUrls } from '../logger' @@ -306,6 +310,8 @@ export async function createServer( let exitProcess: () => void + let creatingDevSsrOptimizer: Promise | null = null + const server: ViteDevServer = { config, middlewares, @@ -324,6 +330,13 @@ export async function createServer( }, transformIndexHtml: null!, // to be immediately set async ssrLoadModule(url, opts?: { fixStacktrace?: boolean }) { + if (!getDepsOptimizer(config, { ssr: true })) { + if (!creatingDevSsrOptimizer) { + creatingDevSsrOptimizer = initDevSsrDepsOptimizer(config) + } + await creatingDevSsrOptimizer + creatingDevSsrOptimizer = null + } await updateCjsSsrExternals(server) return ssrLoadModule( url, @@ -752,15 +765,21 @@ async function restartServer(server: ViteDevServer) { async function updateCjsSsrExternals(server: ViteDevServer) { if (!server._ssrExternals) { - // We use the non-ssr optimized deps to find known imports let knownImports: string[] = [] - const depsOptimizer = getDepsOptimizer(server.config) + + // Important! We use the non-ssr optimized deps to find known imports + // Only the explicitly defined deps are optimized during dev SSR, so + // we use the generated list from the scanned deps in regular dev. + // This is part of the v2 externalization heuristics and it is kept + // for backwards compatibility in case user needs to fallback to the + // legacy scheme. It may be removed in a future v3 minor. + const depsOptimizer = getDepsOptimizer(server.config, { ssr: false }) + if (depsOptimizer) { await depsOptimizer.scanProcessing - const metadata = depsOptimizer.metadata({ ssr: false }) knownImports = [ - ...Object.keys(metadata.optimized), - ...Object.keys(metadata.discovered) + ...Object.keys(depsOptimizer.metadata.optimized), + ...Object.keys(depsOptimizer.metadata.discovered) ] } server._ssrExternals = cjsSsrResolveExternals(server.config, knownImports) diff --git a/packages/vite/src/node/server/middlewares/transform.ts b/packages/vite/src/node/server/middlewares/transform.ts index f41a29f512f2ac..15151b7a29e08a 100644 --- a/packages/vite/src/node/server/middlewares/transform.ts +++ b/packages/vite/src/node/server/middlewares/transform.ts @@ -71,7 +71,11 @@ export function transformMiddleware( const isSourceMap = withoutQuery.endsWith('.map') // since we generate source map references, handle those requests here if (isSourceMap) { - if (getDepsOptimizer(server.config)?.isOptimizedDepUrl(url)) { + if ( + getDepsOptimizer(server.config, { ssr: false })?.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) @@ -188,7 +192,9 @@ export function transformMiddleware( const type = isDirectCSSRequest(url) ? 'css' : 'js' const isDep = DEP_VERSION_RE.test(url) || - getDepsOptimizer(server.config)?.isOptimizedDepUrl(url) + getDepsOptimizer(server.config, { ssr: false })?.isOptimizedDepUrl( + url + ) return send(req, res, result.code, type, { etag: result.etag, // allow browser to cache npm deps! diff --git a/packages/vite/src/node/server/transformRequest.ts b/packages/vite/src/node/server/transformRequest.ts index b89a79e4b23cb3..782ad3ef1e1725 100644 --- a/packages/vite/src/node/server/transformRequest.ts +++ b/packages/vite/src/node/server/transformRequest.ts @@ -145,7 +145,7 @@ async function doTransform( const result = loadAndTransform(id, url, server, options, timestamp) - const depsOptimizer = getDepsOptimizer(config) + const depsOptimizer = getDepsOptimizer(config, { ssr }) if (depsOptimizer && !config.legacy?.devDepsScanner) { depsOptimizer.delayDepsOptimizerUntil(id, () => result) } diff --git a/playground/ssr-deps/import-builtin-cjs/index.js b/playground/ssr-deps/import-builtin-cjs/index.js new file mode 100644 index 00000000000000..149be9e9ed53b8 --- /dev/null +++ b/playground/ssr-deps/import-builtin-cjs/index.js @@ -0,0 +1,5 @@ +exports.stream = require('stream') + +exports.hello = function () { + return 'Hello World!' +} diff --git a/playground/ssr-deps/import-builtin-cjs/package.json b/playground/ssr-deps/import-builtin-cjs/package.json new file mode 100644 index 00000000000000..73509dfed0624d --- /dev/null +++ b/playground/ssr-deps/import-builtin-cjs/package.json @@ -0,0 +1,6 @@ +{ + "name": "import-builtin", + "private": true, + "type": "commonjs", + "version": "0.0.0" +} diff --git a/playground/ssr-deps/no-external-cjs/package.json b/playground/ssr-deps/no-external-cjs/package.json index 799658987d7b2c..56d7adf9bbd430 100644 --- a/playground/ssr-deps/no-external-cjs/package.json +++ b/playground/ssr-deps/no-external-cjs/package.json @@ -1,5 +1,5 @@ { - "name": "primitive-export", + "name": "no-external-cjs", "private": true, "type": "commonjs", "version": "0.0.0" diff --git a/playground/ssr-deps/package.json b/playground/ssr-deps/package.json index 5f9493a7dbf6be..81068e24d3474d 100644 --- a/playground/ssr-deps/package.json +++ b/playground/ssr-deps/package.json @@ -20,6 +20,7 @@ "require-absolute": "file:./require-absolute", "ts-transpiled-exports": "file:./ts-transpiled-exports", "no-external-cjs": "file:./no-external-cjs", + "import-builtin-cjs": "file:./import-builtin-cjs", "no-external-css": "file:./no-external-css" }, "devDependencies": { diff --git a/playground/ssr-deps/server.js b/playground/ssr-deps/server.js index 0e1c28ce31dbf0..432ff69ca3b7d8 100644 --- a/playground/ssr-deps/server.js +++ b/playground/ssr-deps/server.js @@ -35,7 +35,7 @@ export async function createServer(root = process.cwd(), hmrPort) { }, appType: 'custom', ssr: { - noExternal: ['no-external-cjs', 'no-external-css'] + noExternal: ['no-external-cjs', 'import-builtin-cjs', 'no-external-css'] } }) // use vite's connect instance as middleware diff --git a/playground/ssr-deps/src/app.js b/playground/ssr-deps/src/app.js index 6c94fd808bf8b7..f5b0ad810421fb 100644 --- a/playground/ssr-deps/src/app.js +++ b/playground/ssr-deps/src/app.js @@ -10,6 +10,7 @@ import definePropertyExports from 'define-property-exports' import onlyObjectAssignedExports from 'only-object-assigned-exports' import requireAbsolute from 'require-absolute' import noExternalCjs from 'no-external-cjs' +import importBuiltinCjs from 'import-builtin-cjs' export async function render(url, rootDir) { let html = '' @@ -49,5 +50,8 @@ export async function render(url, rootDir) { const noExternalCjsMessage = noExternalCjs.hello() html += `\n

message from no-external-cjs: ${noExternalCjsMessage}

` + const importBuiltinCjsMessage = importBuiltinCjs.hello() + html += `\n

message from import-builtin-cjs: ${importBuiltinCjsMessage}

` + return html + '\n' } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8935eb766366cf..67f75f4da8630d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -856,6 +856,7 @@ importers: define-property-exports: file:./define-property-exports express: ^4.18.1 forwarded-export: file:./forwarded-export + import-builtin-cjs: file:./import-builtin-cjs no-external-cjs: file:./no-external-cjs no-external-css: file:./no-external-css object-assigned-exports: file:./object-assigned-exports @@ -869,6 +870,7 @@ importers: define-properties-exports: file:playground/ssr-deps/define-properties-exports define-property-exports: file:playground/ssr-deps/define-property-exports forwarded-export: file:playground/ssr-deps/forwarded-export + import-builtin-cjs: file:playground/ssr-deps/import-builtin-cjs no-external-cjs: file:playground/ssr-deps/no-external-cjs no-external-css: file:playground/ssr-deps/no-external-css object-assigned-exports: file:playground/ssr-deps/object-assigned-exports @@ -893,6 +895,9 @@ importers: dependencies: object-assigned-exports: file:playground/ssr-deps/object-assigned-exports + playground/ssr-deps/import-builtin-cjs: + specifiers: {} + playground/ssr-deps/no-external-cjs: specifiers: {} @@ -8858,9 +8863,15 @@ packages: object-assigned-exports: file:playground/ssr-deps/object-assigned-exports dev: false + file:playground/ssr-deps/import-builtin-cjs: + resolution: {directory: playground/ssr-deps/import-builtin-cjs, type: directory} + name: import-builtin + version: 0.0.0 + dev: false + file:playground/ssr-deps/no-external-cjs: resolution: {directory: playground/ssr-deps/no-external-cjs, type: directory} - name: primitive-export + name: no-external-cjs version: 0.0.0 dev: false