Skip to content

Commit 339d9e3

Browse files
authored
feat: avoid scanner during build and only optimize CJS in SSR (#8932)
1 parent df5688c commit 339d9e3

File tree

8 files changed

+143
-37
lines changed

8 files changed

+143
-37
lines changed

docs/vite.config.ts

+5
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,10 @@ export default defineConfig({
66
},
77
legacy: {
88
buildSsrCjsExternalHeuristics: true
9+
},
10+
optimizeDeps: {
11+
// vitepress is aliased with replacement `join(DIST_CLIENT_PATH, '/index')`
12+
// This needs to be excluded from optimization
13+
exclude: ['vitepress']
914
}
1015
})

packages/vite/LICENSE.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -975,7 +975,7 @@ Repository: http://github.com/bripkens/connect-history-api-fallback.git
975975

976976
> The MIT License
977977
>
978-
> Copyright (c) 2012 Ben Ripkens http://bripkens.de
978+
> Copyright (c) 2022 Ben Blackmore and contributors
979979
>
980980
> Permission is hereby granted, free of charge, to any person obtaining a copy
981981
> of this software and associated documentation files (the "Software"), to deal

packages/vite/src/node/config.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -545,7 +545,9 @@ export async function resolveConfig(
545545
]
546546
}))
547547
}
548-
return (await container.resolveId(id, importer, { ssr }))?.id
548+
return (
549+
await container.resolveId(id, importer, { ssr, scan: options?.scan })
550+
)?.id
549551
}
550552
}
551553

packages/vite/src/node/optimizer/index.ts

+11-9
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,7 @@ export type ExportsData = {
5454
export interface DepsOptimizer {
5555
metadata: DepOptimizationMetadata
5656
scanProcessing?: Promise<void>
57-
registerMissingImport: (
58-
id: string,
59-
resolved: string,
60-
ssr?: boolean
61-
) => OptimizedDepInfo
57+
registerMissingImport: (id: string, resolved: string) => OptimizedDepInfo
6258
run: () => void
6359

6460
isOptimizedDepFile: (id: string) => boolean
@@ -281,7 +277,7 @@ export async function optimizeServerSsrDeps(
281277
) as string[]
282278
noExternalFilter =
283279
noExternal === true
284-
? (dep: unknown) => false
280+
? (dep: unknown) => true
285281
: createFilter(undefined, exclude, {
286282
resolve: false
287283
})
@@ -705,16 +701,22 @@ export async function addManuallyIncludedOptimizeDeps(
705701
)
706702
}
707703
}
708-
const resolve = config.createResolver({ asSrc: false, scan: true })
704+
const resolve = config.createResolver({
705+
asSrc: false,
706+
scan: true,
707+
ssrOptimizeCheck: ssr
708+
})
709709
for (const id of [...optimizeDepsInclude, ...extra]) {
710710
// normalize 'foo >bar` as 'foo > bar' to prevent same id being added
711711
// and for pretty printing
712712
const normalizedId = normalizeId(id)
713713
if (!deps[normalizedId] && filter?.(normalizedId) !== false) {
714-
const entry = await resolve(id)
714+
const entry = await resolve(id, undefined, undefined, ssr)
715715
if (entry) {
716716
if (isOptimizable(entry, optimizeDeps)) {
717-
deps[normalizedId] = entry
717+
if (!entry.endsWith('?__vite_skip_optimization')) {
718+
deps[normalizedId] = entry
719+
}
718720
} else {
719721
unableToOptimize(entry, 'Cannot optimize dependency')
720722
}

packages/vite/src/node/optimizer/optimizer.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,10 @@ async function createDepsOptimizer(
183183
newDepsDiscovered = true
184184
}
185185

186-
// TODO: We need the scan during build time, until preAliasPlugin
187-
// is refactored to work without the scanned deps. We could skip
188-
// this for build later.
189-
190-
runScanner()
186+
if (!isBuild) {
187+
// Important, the scanner is dev only
188+
runScanner()
189+
}
191190
}
192191

193192
async function runScanner() {

packages/vite/src/node/plugins/preAlias.ts

+68-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1-
import type { Alias, AliasOptions, ResolvedConfig } from '..'
1+
import fs from 'node:fs'
2+
import path from 'node:path'
3+
import type {
4+
Alias,
5+
AliasOptions,
6+
DepOptimizationOptions,
7+
ResolvedConfig
8+
} from '..'
29
import type { Plugin } from '../plugin'
3-
import { bareImportRE } from '../utils'
10+
import { createIsConfiguredAsSsrExternal } from '../ssr/ssrExternal'
11+
import { bareImportRE, isOptimizable, moduleListContains } from '../utils'
412
import { getDepsOptimizer } from '../optimizer'
513
import { tryOptimizedResolve } from './resolve'
614

@@ -9,6 +17,8 @@ import { tryOptimizedResolve } from './resolve'
917
*/
1018
export function preAliasPlugin(config: ResolvedConfig): Plugin {
1119
const findPatterns = getAliasPatterns(config.resolve.alias)
20+
const isConfiguredAsExternal = createIsConfiguredAsSsrExternal(config)
21+
const isBuild = config.command === 'build'
1222
return {
1323
name: 'vite:pre-alias',
1424
async resolveId(id, importer, options) {
@@ -18,16 +28,70 @@ export function preAliasPlugin(config: ResolvedConfig): Plugin {
1828
importer &&
1929
depsOptimizer &&
2030
bareImportRE.test(id) &&
21-
!options?.scan
31+
!options?.scan &&
32+
id !== '@vite/client' &&
33+
id !== '@vite/env'
2234
) {
2335
if (findPatterns.find((pattern) => matches(pattern, id))) {
24-
return await tryOptimizedResolve(depsOptimizer, id, importer)
36+
const optimizedId = await tryOptimizedResolve(
37+
depsOptimizer,
38+
id,
39+
importer
40+
)
41+
if (optimizedId) {
42+
return optimizedId // aliased dep already optimized
43+
}
44+
45+
const resolved = await this.resolve(id, importer, {
46+
skipSelf: true,
47+
...options
48+
})
49+
if (resolved && !depsOptimizer.isOptimizedDepFile(resolved.id)) {
50+
const optimizeDeps = depsOptimizer.options
51+
const resolvedId = resolved.id
52+
const isVirtual = resolvedId === id || resolvedId.includes('\0')
53+
if (
54+
!isVirtual &&
55+
fs.existsSync(resolvedId) &&
56+
!moduleListContains(optimizeDeps.exclude, id) &&
57+
path.isAbsolute(resolvedId) &&
58+
(resolvedId.includes('node_modules') ||
59+
optimizeDeps.include?.includes(id)) &&
60+
isOptimizable(resolvedId, optimizeDeps) &&
61+
!(isBuild && ssr && isConfiguredAsExternal(id)) &&
62+
(!ssr || optimizeAliasReplacementForSSR(resolvedId, optimizeDeps))
63+
) {
64+
// aliased dep has not yet been optimized
65+
const optimizedInfo = depsOptimizer!.registerMissingImport(
66+
id,
67+
resolvedId
68+
)
69+
return { id: depsOptimizer!.getOptimizedDepId(optimizedInfo) }
70+
}
71+
}
72+
return resolved
2573
}
2674
}
2775
}
2876
}
2977
}
3078

79+
function optimizeAliasReplacementForSSR(
80+
id: string,
81+
optimizeDeps: DepOptimizationOptions
82+
) {
83+
if (optimizeDeps.include?.includes(id)) {
84+
return true
85+
}
86+
// In the regular resolution, the default for non-external modules is to
87+
// be optimized if they are CJS. Here, we don't have the package id but
88+
// only the replacement file path. We could find the package.json from
89+
// the id and respect the same default in the future.
90+
// Default to not optimize an aliased replacement for now, forcing the
91+
// user to explicitly add it to the ssr.optimizeDeps.include list.
92+
return false
93+
}
94+
3195
// In sync with rollup plugin alias logic
3296
function matches(pattern: string | RegExp, importee: string) {
3397
if (pattern instanceof RegExp) {

packages/vite/src/node/plugins/resolve.ts

+37-10
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ export interface InternalResolveOptions extends ResolveOptions {
8282
tryEsmOnly?: boolean
8383
// True when resolving during the scan phase to discover dependencies
8484
scan?: boolean
85+
// Appends ?__vite_skip_optimization to the resolved id if shouldn't be optimized
86+
ssrOptimizeCheck?: boolean
8587
// Resolve using esbuild deps optimization
8688
getDepsOptimizer?: (ssr: boolean) => DepsOptimizer | undefined
8789
shouldExternalize?: (id: string) => boolean | undefined
@@ -665,41 +667,66 @@ export function tryNodeResolve(
665667
})
666668
}
667669

670+
const ext = path.extname(resolved)
671+
const isCJS = ext === '.cjs' || (ext === '.js' && pkg.data.type !== 'module')
672+
668673
if (
669-
!resolved.includes('node_modules') || // linked
670-
!depsOptimizer || // resolving before listening to the server
671-
options.scan // initial esbuild scan phase
674+
!options.ssrOptimizeCheck &&
675+
(!resolved.includes('node_modules') || // linked
676+
!depsOptimizer || // resolving before listening to the server
677+
options.scan) // initial esbuild scan phase
672678
) {
673679
return { id: resolved }
674680
}
681+
675682
// if we reach here, it's a valid dep import that hasn't been optimized.
676683
const isJsType = OPTIMIZABLE_ENTRY_RE.test(resolved)
677684

678-
const exclude = depsOptimizer.options.exclude
679-
if (
685+
let exclude = depsOptimizer?.options.exclude
686+
let include = depsOptimizer?.options.exclude
687+
if (options.ssrOptimizeCheck) {
688+
// we don't have the depsOptimizer
689+
exclude = options.ssrConfig?.optimizeDeps?.exclude
690+
include = options.ssrConfig?.optimizeDeps?.exclude
691+
}
692+
693+
const skipOptimization =
680694
!isJsType ||
681695
importer?.includes('node_modules') ||
682696
exclude?.includes(pkgId) ||
683697
exclude?.includes(nestedPath) ||
684698
SPECIAL_QUERY_RE.test(resolved) ||
685-
(!isBuild && ssr)
686-
) {
699+
(!isBuild && ssr) ||
700+
// Only optimize non-external CJS deps during SSR by default
701+
(ssr &&
702+
!isCJS &&
703+
!(include?.includes(pkgId) || include?.includes(nestedPath)))
704+
705+
if (options.ssrOptimizeCheck) {
706+
return {
707+
id: skipOptimization
708+
? injectQuery(resolved, `__vite_skip_optimization`)
709+
: resolved
710+
}
711+
}
712+
713+
if (skipOptimization) {
687714
// excluded from optimization
688715
// Inject a version query to npm deps so that the browser
689716
// can cache it without re-validation, but only do so for known js types.
690717
// otherwise we may introduce duplicated modules for externalized files
691718
// from pre-bundled deps.
692719
if (!isBuild) {
693-
const versionHash = depsOptimizer.metadata.browserHash
720+
const versionHash = depsOptimizer!.metadata.browserHash
694721
if (versionHash && isJsType) {
695722
resolved = injectQuery(resolved, `v=${versionHash}`)
696723
}
697724
}
698725
} else {
699726
// this is a missing import, queue optimize-deps re-run and
700727
// get a resolved its optimized info
701-
const optimizedInfo = depsOptimizer.registerMissingImport(id, resolved, ssr)
702-
resolved = depsOptimizer.getOptimizedDepId(optimizedInfo)
728+
const optimizedInfo = depsOptimizer!.registerMissingImport(id, resolved)
729+
resolved = depsOptimizer!.getOptimizedDepId(optimizedInfo)
703730
}
704731

705732
if (isBuild) {

packages/vite/src/node/ssr/ssrExternal.ts

+14-7
Original file line numberDiff line numberDiff line change
@@ -105,20 +105,17 @@ export function shouldExternalizeForSSR(
105105
return isSsrExternal(id)
106106
}
107107

108-
function createIsSsrExternal(
108+
export function createIsConfiguredAsSsrExternal(
109109
config: ResolvedConfig
110-
): (id: string) => boolean | undefined {
111-
const processedIds = new Map<string, boolean | undefined>()
112-
113-
const { ssr, root } = config
114-
110+
): (id: string) => boolean {
111+
const { ssr } = config
115112
const noExternal = ssr?.noExternal
116113
const noExternalFilter =
117114
noExternal !== 'undefined' &&
118115
typeof noExternal !== 'boolean' &&
119116
createFilter(undefined, noExternal, { resolve: false })
120117

121-
const isConfiguredAsExternal = (id: string) => {
118+
return (id: string) => {
122119
const { ssr } = config
123120
if (!ssr || ssr.external?.includes(id)) {
124121
return true
@@ -131,6 +128,16 @@ function createIsSsrExternal(
131128
}
132129
return true
133130
}
131+
}
132+
133+
function createIsSsrExternal(
134+
config: ResolvedConfig
135+
): (id: string) => boolean | undefined {
136+
const processedIds = new Map<string, boolean | undefined>()
137+
138+
const { ssr, root } = config
139+
140+
const isConfiguredAsExternal = createIsConfiguredAsSsrExternal(config)
134141

135142
const resolveOptions: InternalResolveOptions = {
136143
root,

0 commit comments

Comments
 (0)