Skip to content

Commit

Permalink
feat: avoid scanner during build and only optimize CJS in SSR (#8932)
Browse files Browse the repository at this point in the history
  • Loading branch information
patak-dev committed Jul 6, 2022
1 parent df5688c commit 339d9e3
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 37 deletions.
5 changes: 5 additions & 0 deletions docs/vite.config.ts
Expand Up @@ -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']
}
})
2 changes: 1 addition & 1 deletion packages/vite/LICENSE.md
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion packages/vite/src/node/config.ts
Expand Up @@ -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
}
}

Expand Down
20 changes: 11 additions & 9 deletions packages/vite/src/node/optimizer/index.ts
Expand Up @@ -54,11 +54,7 @@ export type ExportsData = {
export interface DepsOptimizer {
metadata: DepOptimizationMetadata
scanProcessing?: Promise<void>
registerMissingImport: (
id: string,
resolved: string,
ssr?: boolean
) => OptimizedDepInfo
registerMissingImport: (id: string, resolved: string) => OptimizedDepInfo
run: () => void

isOptimizedDepFile: (id: string) => boolean
Expand Down Expand Up @@ -281,7 +277,7 @@ export async function optimizeServerSsrDeps(
) as string[]
noExternalFilter =
noExternal === true
? (dep: unknown) => false
? (dep: unknown) => true
: createFilter(undefined, exclude, {
resolve: false
})
Expand Down Expand Up @@ -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')
}
Expand Down
9 changes: 4 additions & 5 deletions packages/vite/src/node/optimizer/optimizer.ts
Expand Up @@ -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() {
Expand Down
72 changes: 68 additions & 4 deletions 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'

Expand All @@ -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) {
Expand All @@ -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) {
Expand Down
47 changes: 37 additions & 10 deletions packages/vite/src/node/plugins/resolve.ts
Expand Up @@ -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
Expand Down Expand Up @@ -665,41 +667,66 @@ 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}`)
}
}
} 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) {
Expand Down
21 changes: 14 additions & 7 deletions packages/vite/src/node/ssr/ssrExternal.ts
Expand Up @@ -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<string, boolean | undefined>()

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
Expand All @@ -131,6 +128,16 @@ function createIsSsrExternal(
}
return true
}
}

function createIsSsrExternal(
config: ResolvedConfig
): (id: string) => boolean | undefined {
const processedIds = new Map<string, boolean | undefined>()

const { ssr, root } = config

const isConfiguredAsExternal = createIsConfiguredAsSsrExternal(config)

const resolveOptions: InternalResolveOptions = {
root,
Expand Down

0 comments on commit 339d9e3

Please sign in to comment.