diff --git a/docs/vite.config.ts b/docs/vite.config.ts index 3da9f8c96af31a..94f3ed69000771 100644 --- a/docs/vite.config.ts +++ b/docs/vite.config.ts @@ -6,5 +6,10 @@ export default defineConfig({ }, legacy: { buildSsrCjsExternalHeuristics: true + }, + optimizeDeps: { + // vitepress is aliased with replacement `join(DIST_CLIENT_PATH, '/index')` + // This needs to be excluded from optimization + exclude: ['vitepress'] } }) diff --git a/packages/vite/LICENSE.md b/packages/vite/LICENSE.md index 9bfb71d673183a..b6ea3555b32d39 100644 --- a/packages/vite/LICENSE.md +++ b/packages/vite/LICENSE.md @@ -975,7 +975,7 @@ Repository: http://github.com/bripkens/connect-history-api-fallback.git > The MIT License > -> Copyright (c) 2012 Ben Ripkens http://bripkens.de +> Copyright (c) 2022 Ben Blackmore and contributors > > Permission is hereby granted, free of charge, to any person obtaining a copy > of this software and associated documentation files (the "Software"), to deal diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 779a7cdeec9e7e..c06b826132535e 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -545,7 +545,9 @@ export async function resolveConfig( ] })) } - return (await container.resolveId(id, importer, { ssr }))?.id + return ( + await container.resolveId(id, importer, { ssr, scan: options?.scan }) + )?.id } } diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index d941b2fa6c7570..0798f27e079379 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -54,11 +54,7 @@ export type ExportsData = { export interface DepsOptimizer { metadata: DepOptimizationMetadata scanProcessing?: Promise - registerMissingImport: ( - id: string, - resolved: string, - ssr?: boolean - ) => OptimizedDepInfo + registerMissingImport: (id: string, resolved: string) => OptimizedDepInfo run: () => void isOptimizedDepFile: (id: string) => boolean @@ -281,7 +277,7 @@ export async function optimizeServerSsrDeps( ) as string[] noExternalFilter = noExternal === true - ? (dep: unknown) => false + ? (dep: unknown) => true : createFilter(undefined, exclude, { resolve: false }) @@ -705,16 +701,22 @@ export async function addManuallyIncludedOptimizeDeps( ) } } - const resolve = config.createResolver({ asSrc: false, scan: true }) + const resolve = config.createResolver({ + asSrc: false, + scan: true, + ssrOptimizeCheck: ssr + }) for (const id of [...optimizeDepsInclude, ...extra]) { // normalize 'foo >bar` as 'foo > bar' to prevent same id being added // and for pretty printing const normalizedId = normalizeId(id) if (!deps[normalizedId] && filter?.(normalizedId) !== false) { - const entry = await resolve(id) + const entry = await resolve(id, undefined, undefined, ssr) if (entry) { if (isOptimizable(entry, optimizeDeps)) { - deps[normalizedId] = entry + if (!entry.endsWith('?__vite_skip_optimization')) { + deps[normalizedId] = entry + } } else { unableToOptimize(entry, 'Cannot optimize dependency') } diff --git a/packages/vite/src/node/optimizer/optimizer.ts b/packages/vite/src/node/optimizer/optimizer.ts index 4759edeb9d0b51..652ab47c5f83b1 100644 --- a/packages/vite/src/node/optimizer/optimizer.ts +++ b/packages/vite/src/node/optimizer/optimizer.ts @@ -183,11 +183,10 @@ async function createDepsOptimizer( newDepsDiscovered = true } - // TODO: We need the scan during build time, until preAliasPlugin - // is refactored to work without the scanned deps. We could skip - // this for build later. - - runScanner() + if (!isBuild) { + // Important, the scanner is dev only + runScanner() + } } async function runScanner() { diff --git a/packages/vite/src/node/plugins/preAlias.ts b/packages/vite/src/node/plugins/preAlias.ts index d9cd9cdfb888d6..9eb4cfeeffa85d 100644 --- a/packages/vite/src/node/plugins/preAlias.ts +++ b/packages/vite/src/node/plugins/preAlias.ts @@ -1,6 +1,14 @@ -import type { Alias, AliasOptions, ResolvedConfig } from '..' +import fs from 'node:fs' +import path from 'node:path' +import type { + Alias, + AliasOptions, + DepOptimizationOptions, + ResolvedConfig +} from '..' import type { Plugin } from '../plugin' -import { bareImportRE } from '../utils' +import { createIsConfiguredAsSsrExternal } from '../ssr/ssrExternal' +import { bareImportRE, isOptimizable, moduleListContains } from '../utils' import { getDepsOptimizer } from '../optimizer' import { tryOptimizedResolve } from './resolve' @@ -9,6 +17,8 @@ import { tryOptimizedResolve } from './resolve' */ export function preAliasPlugin(config: ResolvedConfig): Plugin { const findPatterns = getAliasPatterns(config.resolve.alias) + const isConfiguredAsExternal = createIsConfiguredAsSsrExternal(config) + const isBuild = config.command === 'build' return { name: 'vite:pre-alias', async resolveId(id, importer, options) { @@ -18,16 +28,70 @@ export function preAliasPlugin(config: ResolvedConfig): Plugin { importer && depsOptimizer && bareImportRE.test(id) && - !options?.scan + !options?.scan && + id !== '@vite/client' && + id !== '@vite/env' ) { if (findPatterns.find((pattern) => matches(pattern, id))) { - return await tryOptimizedResolve(depsOptimizer, id, importer) + const optimizedId = await tryOptimizedResolve( + depsOptimizer, + id, + importer + ) + if (optimizedId) { + return optimizedId // aliased dep already optimized + } + + const resolved = await this.resolve(id, importer, { + skipSelf: true, + ...options + }) + if (resolved && !depsOptimizer.isOptimizedDepFile(resolved.id)) { + const optimizeDeps = depsOptimizer.options + const resolvedId = resolved.id + const isVirtual = resolvedId === id || resolvedId.includes('\0') + if ( + !isVirtual && + fs.existsSync(resolvedId) && + !moduleListContains(optimizeDeps.exclude, id) && + path.isAbsolute(resolvedId) && + (resolvedId.includes('node_modules') || + optimizeDeps.include?.includes(id)) && + isOptimizable(resolvedId, optimizeDeps) && + !(isBuild && ssr && isConfiguredAsExternal(id)) && + (!ssr || optimizeAliasReplacementForSSR(resolvedId, optimizeDeps)) + ) { + // aliased dep has not yet been optimized + const optimizedInfo = depsOptimizer!.registerMissingImport( + id, + resolvedId + ) + return { id: depsOptimizer!.getOptimizedDepId(optimizedInfo) } + } + } + return resolved } } } } } +function optimizeAliasReplacementForSSR( + id: string, + optimizeDeps: DepOptimizationOptions +) { + if (optimizeDeps.include?.includes(id)) { + return true + } + // In the regular resolution, the default for non-external modules is to + // be optimized if they are CJS. Here, we don't have the package id but + // only the replacement file path. We could find the package.json from + // the id and respect the same default in the future. + // Default to not optimize an aliased replacement for now, forcing the + // user to explicitly add it to the ssr.optimizeDeps.include list. + return false +} + // In sync with rollup plugin alias logic function matches(pattern: string | RegExp, importee: string) { if (pattern instanceof RegExp) { diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index cbad9fab449e0d..051f11024a6768 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -82,6 +82,8 @@ export interface InternalResolveOptions extends ResolveOptions { tryEsmOnly?: boolean // True when resolving during the scan phase to discover dependencies scan?: boolean + // Appends ?__vite_skip_optimization to the resolved id if shouldn't be optimized + ssrOptimizeCheck?: boolean // Resolve using esbuild deps optimization getDepsOptimizer?: (ssr: boolean) => DepsOptimizer | undefined shouldExternalize?: (id: string) => boolean | undefined @@ -665,32 +667,57 @@ export function tryNodeResolve( }) } + const ext = path.extname(resolved) + const isCJS = ext === '.cjs' || (ext === '.js' && pkg.data.type !== 'module') + if ( - !resolved.includes('node_modules') || // linked - !depsOptimizer || // resolving before listening to the server - options.scan // initial esbuild scan phase + !options.ssrOptimizeCheck && + (!resolved.includes('node_modules') || // linked + !depsOptimizer || // resolving before listening to the server + options.scan) // initial esbuild scan phase ) { return { id: resolved } } + // if we reach here, it's a valid dep import that hasn't been optimized. const isJsType = OPTIMIZABLE_ENTRY_RE.test(resolved) - const exclude = depsOptimizer.options.exclude - if ( + let exclude = depsOptimizer?.options.exclude + let include = depsOptimizer?.options.exclude + if (options.ssrOptimizeCheck) { + // we don't have the depsOptimizer + exclude = options.ssrConfig?.optimizeDeps?.exclude + include = options.ssrConfig?.optimizeDeps?.exclude + } + + const skipOptimization = !isJsType || importer?.includes('node_modules') || exclude?.includes(pkgId) || exclude?.includes(nestedPath) || SPECIAL_QUERY_RE.test(resolved) || - (!isBuild && ssr) - ) { + (!isBuild && ssr) || + // Only optimize non-external CJS deps during SSR by default + (ssr && + !isCJS && + !(include?.includes(pkgId) || include?.includes(nestedPath))) + + if (options.ssrOptimizeCheck) { + return { + id: skipOptimization + ? injectQuery(resolved, `__vite_skip_optimization`) + : resolved + } + } + + if (skipOptimization) { // excluded from optimization // Inject a version query to npm deps so that the browser // 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. if (!isBuild) { - const versionHash = depsOptimizer.metadata.browserHash + const versionHash = depsOptimizer!.metadata.browserHash if (versionHash && isJsType) { resolved = injectQuery(resolved, `v=${versionHash}`) } @@ -698,8 +725,8 @@ export function tryNodeResolve( } else { // this is a missing import, queue optimize-deps re-run and // get a resolved its optimized info - const optimizedInfo = depsOptimizer.registerMissingImport(id, resolved, ssr) - resolved = depsOptimizer.getOptimizedDepId(optimizedInfo) + const optimizedInfo = depsOptimizer!.registerMissingImport(id, resolved) + resolved = depsOptimizer!.getOptimizedDepId(optimizedInfo) } if (isBuild) { diff --git a/packages/vite/src/node/ssr/ssrExternal.ts b/packages/vite/src/node/ssr/ssrExternal.ts index 59d396d2c78a55..c403d85f83d9bb 100644 --- a/packages/vite/src/node/ssr/ssrExternal.ts +++ b/packages/vite/src/node/ssr/ssrExternal.ts @@ -105,20 +105,17 @@ export function shouldExternalizeForSSR( return isSsrExternal(id) } -function createIsSsrExternal( +export function createIsConfiguredAsSsrExternal( config: ResolvedConfig -): (id: string) => boolean | undefined { - const processedIds = new Map() - - const { ssr, root } = config - +): (id: string) => boolean { + const { ssr } = config const noExternal = ssr?.noExternal const noExternalFilter = noExternal !== 'undefined' && typeof noExternal !== 'boolean' && createFilter(undefined, noExternal, { resolve: false }) - const isConfiguredAsExternal = (id: string) => { + return (id: string) => { const { ssr } = config if (!ssr || ssr.external?.includes(id)) { return true @@ -131,6 +128,16 @@ function createIsSsrExternal( } return true } +} + +function createIsSsrExternal( + config: ResolvedConfig +): (id: string) => boolean | undefined { + const processedIds = new Map() + + const { ssr, root } = config + + const isConfiguredAsExternal = createIsConfiguredAsSsrExternal(config) const resolveOptions: InternalResolveOptions = { root,