昨天追到 pre-bundling 的入口 createDepsOptimizer
裡的 init
有三個關鍵函式,今天繼續來往下追,在開始前先複習一下簡化過的入口邏輯結構:
// packages/vite/src/node/optimizer/optimizer.ts
export function createDepsOptimizer(...) {
let inited = false
async function init() {
if (inited) return
inited = true
// 根據 metadata 載入之前 pre-bundle 過的快取資料
const cachedMetadata = await loadCachedDepOptimizationMetadata(environment)
// 快取資料不存在則執行
if (!cachedMetadata) {
// 在設定中開啟 optimizeDeps.noDiscovery
if (noDiscovery) {
// ... 略 ...
} else {
// 掃描專案中的依賴套件
discover = discoverProjectDependencies(
devToScanEnvironment(environment),
)
// 利用 esbuild 執行 pre-bundling
optimizationResult = runOptimizeDeps(environment, knownDeps)
}
}
}
}
這裡先將以上的邏輯整理成下面這張圖更清楚:
解析 loadCachedDepOptimizationMetadata
這個函式如程式碼中註解,這段基本上就是文件上關於檔案系統快取的實現,最後返回 pre-bundle 過的快取資料:
export async function loadCachedDepOptimizationMetadata(...) {
// ... 略 ...
// 取得 node_modules/.vite/deps 這個路徑
const depsCacheDir = getDepsCacheDir(environment)
// 是否有設定 optimizeDeps.force 或 --force
// 有的話每次強制重新 pre-bundle
if (!force) {
let cachedMetadata
try {
// 取得並解析 _metadata.json 這個檔案
const cachedMetadataPath = path.join(...)
cachedMetadata = parseDepsOptimizerMetadata(
await fsp.readFile(cachedMetadataPath, 'utf-8'),
depsCacheDir,
)
} catch {}
if (cachedMetadata) {
// 比較 lockfile、config 中的 hash 引出對應訊息
}
} else {
environment.logger.info('Forced re-optimization of dependencies')
}
// 刪除快取資料夾
await fsp.rm(depsCacheDir, { recursive: true, force: true })
}
當快取資料不存在時,會繼續進到 discoverProjectDependencies
這個函式來掃描專案中有用到哪些依賴套件:
export function discoverProjectDependencies(environment: ScanEnvironment): {
cancel: () => Promise<void>
result: Promise<Record<string, string>>
} {
// 利用 esbuild 快速掃瞄找出需要做 pre-bundle 的依賴
const { cancel, result } = scanImports(environment)
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...}\n\nAre they installed?`,
)
}
return deps
}),
}
}
而其中看起來真正做事的會是 scanImports
這個函式,再往裡面追的話可以看到:
export function scanImports(environment: ScanEnvironment): {
cancel: () => Promise<void>
result: Promise<{...}>
} {
// ... 初始化參數略 ...
// 根據 rollupOptions.input 進入點去分析需掃描的套件
const esbuildContext = computeEntries(
environment,
).then((computedEntries) => {
// ... 略 ...
// 往這個函式追進去可以看到關於 esbuild 的初始化設定
return prepareEsbuildScanner(...)
})
const result = esbuildContext
.then((context) => {
// 利用 esbuild 來解析套件的依賴關係
return context
.rebuild()
.then(() => {...})
.finally(() => {
// 用來釋放執行 esbuild 的記憶體
return disposeContext()
})
})
// ... 略 ...
}
原本以為實際上用到 esbuild 來執行 pre-bundle 的地方應該會是在更後面的 runOptimizeDeps
裡,不是看很懂為什麼這邊會用 esbuild 來做 rebuild,是真的有打包出什麼內容嗎?
再繼續往 prepareEsbuildScanner
這個地方追進去會發現一個關鍵點:
async function prepareEsbuildScanner(...) {
// 看起來好像有針對掃描檔案另外做許多邏輯處理的 plugin
const plugin = esbuildScanPlugin(environment, deps, missing, entries)
// ... 針對 tsconfig 的處理略 ...
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,
tsconfigRaw,
})
}
看到這裡在初始化 esbuild 設定檔的地方應該是關鍵,於是好奇查了下 esbuild 的文件找到關於這個 write
的意思:
The build API call can either write to the file system directly or return the files that would have been written as in-memory buffers
翻成白話文的話應該是說這個 write 的設定會決定 build API 如何輸出打包後的內容,如果設定成 false
的話就會有點類似 dry run 的方式把結果寫進記憶體緩衝區中。所以可以整理一下這兩處使用 esbuild 情境的差異:
scanImports
:利用 esbuild 去做 dry run,快速識別專案中的套件依賴關係runOptimizeDeps
:利用 esbuild 實際執行 pre-bundling先劇透下如果往 runOptimizeDeps
裡面追進去會看到其中有一段 prepareEsbuildOptimizerRun
在準備 esbuild 設定 (ref),其中就沒有用到 write
這個屬性,因此會真的去打包出東西來。
今天重新釐清了一下從 pre-bundling 進入點的綜觀邏輯,並一路追到其中在掃描整個專案中套件依賴的原理,比較有趣的是找到 esbuild 其中有一個 write
設定可以拿來控制類似你這次的行為是想要掃描檔案還是要做打包,算是跟 esbuild 文件有了第一次的交集,明天會繼續把剩下的 runOptimizeDeps
追完,再做個總整理。
另外也試著利用 Excalidraw 把目前追原始碼的地圖給整理了出來:
實際用了這工具後,也發現裡面還有 beta 版 AI 功能,可以用 prompt 可能像是程式碼、條列筆記等,請它快速初始化流程圖草稿,不用真的從頭拉起方便許多,雖然免費版每天有次數限制就是了,也推薦給大家: