Skip to content

Commit

Permalink
feat: cancellable scan during optimization (#12225)
Browse files Browse the repository at this point in the history
Co-authored-by: dominikg
  • Loading branch information
patak-dev committed Feb 28, 2023
1 parent 1ddd08b commit 1e1cd3b
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 82 deletions.
46 changes: 26 additions & 20 deletions packages/vite/src/node/optimizer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ export async function optimizeDeps(
return cachedMetadata
}

const deps = await discoverProjectDependencies(config)
const deps = await discoverProjectDependencies(config).result

const depsString = depsLogString(Object.keys(deps))
log(colors.green(`Optimizing dependencies:\n ${depsString}`))
Expand Down Expand Up @@ -380,26 +380,32 @@ export function loadCachedDepOptimizationMetadata(
* Initial optimizeDeps at server start. Perform a fast scan using esbuild to
* find deps to pre-bundle and include user hard-coded dependencies
*/
export async function discoverProjectDependencies(
config: ResolvedConfig,
): Promise<Record<string, string>> {
const { deps, missing } = await scanImports(config)

const missingIds = Object.keys(missing)
if (missingIds.length) {
throw new Error(
`The following dependencies are imported but could not be resolved:\n\n ${missingIds
.map(
(id) =>
`${colors.cyan(id)} ${colors.white(
colors.dim(`(imported by ${missing[id]})`),
)}`,
export function discoverProjectDependencies(config: ResolvedConfig): {
cancel: () => Promise<void>
result: Promise<Record<string, string>>
} {
const { cancel, result } = scanImports(config)

return {
cancel,
result: result.then(({ deps, missing }) => {
const missingIds = Object.keys(missing)
if (missingIds.length) {
throw new Error(
`The following dependencies are imported but could not be resolved:\n\n ${missingIds
.map(
(id) =>
`${colors.cyan(id)} ${colors.white(
colors.dim(`(imported by ${missing[id]})`),
)}`,
)
.join(`\n `)}\n\nAre they installed?`,
)
.join(`\n `)}\n\nAre they installed?`,
)
}
}

return deps
return deps
}),
}
}

export function toDiscoveredDependencies(
Expand Down Expand Up @@ -679,7 +685,7 @@ export async function findKnownImports(
config: ResolvedConfig,
ssr: boolean,
): Promise<string[]> {
const deps = (await scanImports(config)).deps
const { deps } = await scanImports(config).result
await addManuallyIncludedOptimizeDeps(deps, config, ssr)
return Object.keys(deps)
}
Expand Down
11 changes: 10 additions & 1 deletion packages/vite/src/node/optimizer/optimizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,18 @@ async function createDepsOptimizer(
let firstRunCalled = !!cachedMetadata

let postScanOptimizationResult: Promise<DepOptimizationResult> | undefined
let discover:
| {
cancel: () => Promise<void>
result: Promise<Record<string, string>>
}
| undefined

let optimizingNewDeps: Promise<DepOptimizationResult> | undefined
async function close() {
closed = true
await Promise.allSettled([
discover?.cancel(),
depsOptimizer.scanProcessing,
postScanOptimizationResult,
optimizingNewDeps,
Expand Down Expand Up @@ -204,7 +211,9 @@ async function createDepsOptimizer(
try {
debug(colors.green(`scanning for dependencies...`))

const deps = await discoverProjectDependencies(config)
discover = discoverProjectDependencies(config)
const deps = await discover.result
discover = undefined

debug(
colors.green(
Expand Down
165 changes: 104 additions & 61 deletions packages/vite/src/node/optimizer/scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import fs from 'node:fs'
import path from 'node:path'
import { performance } from 'node:perf_hooks'
import glob from 'fast-glob'
import type { Loader, OnLoadResult, Plugin } from 'esbuild'
import { build, formatMessages, transform } from 'esbuild'
import type { BuildContext, Loader, OnLoadResult, Plugin } from 'esbuild'
import esbuild, { formatMessages, transform } from 'esbuild'
import colors from 'picocolors'
import type { ResolvedConfig } from '..'
import {
Expand Down Expand Up @@ -47,14 +47,91 @@ const htmlTypesRE = /\.(html|vue|svelte|astro|imba)$/
export const importsRE =
/(?<!\/\/.*)(?<=^|;|\*\/)\s*import(?!\s+type)(?:[\w*{}\n\r\t, ]+from)?\s*("[^"]+"|'[^']+')\s*(?=$|;|\/\/|\/\*)/gm

export async function scanImports(config: ResolvedConfig): Promise<{
deps: Record<string, string>
missing: Record<string, string>
}> {
export function scanImports(config: ResolvedConfig): {
cancel: () => Promise<void>
result: Promise<{
deps: Record<string, string>
missing: Record<string, string>
}>
} {
// Only used to scan non-ssr code

const start = performance.now()
const deps: Record<string, string> = {}
const missing: Record<string, string> = {}
let entries: string[]

const esbuildContext: Promise<BuildContext | undefined> = computeEntries(
config,
).then((computedEntries) => {
entries = computedEntries

if (!entries.length) {
if (!config.optimizeDeps.entries && !config.optimizeDeps.include) {
config.logger.warn(
colors.yellow(
'(!) Could not auto-determine entry point from rollupOptions or html files ' +
'and there are no explicit optimizeDeps.include patterns. ' +
'Skipping dependency pre-bundling.',
),
)
}
return
}

debug(`Crawling dependencies using entries:\n ${entries.join('\n ')}`)
return prepareEsbuildScanner(config, entries, deps, missing)
})

const result = esbuildContext
.then((context) => {
if (!context) {
return { deps: {}, missing: {} }
}
return context
.rebuild()
.then(() => {
return {
// Ensure a fixed order so hashes are stable and improve logs
deps: orderedDependencies(deps),
missing,
}
})
.finally(() => {
context.dispose()
})
})
.catch(async (e) => {
const prependMessage = colors.red(`\
Failed to scan for dependencies from entries:
${entries.join('\n')}
`)
if (e.errors) {
const msgs = await formatMessages(e.errors, {
kind: 'error',
color: true,
})
e.message = prependMessage + msgs.join('\n')
} else {
e.message = prependMessage + e.message
}
throw e
})
.finally(() => {
debug(
`Scan completed in ${(performance.now() - start).toFixed(2)}ms:`,
deps,
)
})

return {
cancel: () => esbuildContext.then((context) => context?.cancel()),
result,
}
}

async function computeEntries(config: ResolvedConfig) {
let entries: string[] = []

const explicitEntryPatterns = config.optimizeDeps.entries
Expand Down Expand Up @@ -83,68 +160,34 @@ export async function scanImports(config: ResolvedConfig): Promise<{
(entry) => isScannable(entry) && fs.existsSync(entry),
)

if (!entries.length) {
if (!explicitEntryPatterns && !config.optimizeDeps.include) {
config.logger.warn(
colors.yellow(
'(!) Could not auto-determine entry point from rollupOptions or html files ' +
'and there are no explicit optimizeDeps.include patterns. ' +
'Skipping dependency pre-bundling.',
),
)
}
return { deps: {}, missing: {} }
} else {
debug(`Crawling dependencies using entries:\n ${entries.join('\n ')}`)
}
return entries
}

const deps: Record<string, string> = {}
const missing: Record<string, string> = {}
async function prepareEsbuildScanner(
config: ResolvedConfig,
entries: string[],
deps: Record<string, string>,
missing: Record<string, string>,
) {
const container = await createPluginContainer(config)
const plugin = esbuildScanPlugin(config, container, deps, missing, entries)

const { plugins = [], ...esbuildOptions } =
config.optimizeDeps?.esbuildOptions ?? {}

try {
await build({
absWorkingDir: process.cwd(),
write: false,
stdin: {
contents: entries.map((e) => `import ${JSON.stringify(e)}`).join('\n'),
loader: 'js',
},
bundle: true,
format: 'esm',
logLevel: 'silent',
plugins: [...plugins, plugin],
...esbuildOptions,
})
} catch (e) {
const prependMessage = colors.red(`\
Failed to scan for dependencies from entries:
${entries.join('\n')}
`)
if (e.errors) {
const msgs = await formatMessages(e.errors, {
kind: 'error',
color: true,
})
e.message = prependMessage + msgs.join('\n')
} else {
e.message = prependMessage + e.message
}
throw e
}

debug(`Scan completed in ${(performance.now() - start).toFixed(2)}ms:`, deps)

return {
// Ensure a fixed order so hashes are stable and improve logs
deps: orderedDependencies(deps),
missing,
}
return await esbuild.context({
absWorkingDir: process.cwd(),
write: false,
stdin: {
contents: entries.map((e) => `import ${JSON.stringify(e)}`).join('\n'),
loader: 'js',
},
bundle: true,
format: 'esm',
logLevel: 'silent',
plugins: [...plugins, plugin],
...esbuildOptions,
})
}

function orderedDependencies(deps: Record<string, string>) {
Expand Down

0 comments on commit 1e1cd3b

Please sign in to comment.