Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: avoid scanner during build and only optimize CJS in SSR #8932

Merged
merged 4 commits into from Jul 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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(
bluwy marked this conversation as resolved.
Show resolved Hide resolved
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