withastro/astro
Vite dep optimizer
This is the canonical reference for the most common subtle dev-only failure mode in Astro: dependencies that work in astro build but fail in astro dev. It mirrors the long-form deep dive at reference/optimize-deps.md in the repo (referenced from AGENTS.md).
Why this matters
astro build uses Rollup, which handles CJS→ESM cleanly. astro dev uses Vite 7's dev server, which serves modules individually and relies on esbuild's dep optimizer to pre-bundle certain dependencies as ESM before they reach the runtime. When a dep bypasses the optimizer — especially in ESM-only runtimes like Cloudflare Workers (workerd) — you get errors like require is not defined at runtime in dev, even though the build works fine.
This is a category of subtle bugs. There is rarely one definitive fix — the right approach depends on why the dep was missed by the optimizer.
How optimizeDeps works
Two phases
- Scan. esbuild crawls
optimizeDeps.entriesto discover deps. Bare imports that resolve intonode_modulesare added todepImports. The scan does not recurse intonode_modules; it records the top-level dep and marks it external. - Bundle. esbuild bundles each discovered dep, transforming CJS→ESM. Transitive deps are inlined (if JS/TS) or externalized (if non-JS or in another
node_modulespackage).
The scan is intentionally shallow. A transitive dep is only pre-bundled if discovered in the scan, or directly inlined when bundling its parent.
optimizeDeps.include
Entries listed here are added directly to depImports without scanning. The blunt fix. The > notation (@astrojs/prism > prismjs/components/index.js) resolves transitive deps that aren't directly importable from the project root.
optimizeDeps.entries
Glob patterns or file paths that tell esbuild where to start scanning. Default is **/*.html. Astro's vite-plugin-environment sets these to include .astro, .jsx, .tsx, etc. If the scanner never reaches a file that imports a problematic dep, the dep is never discovered.
noDiscovery
When an environment sets optimizeDeps.noDiscovery: true, only optimizeDeps.include is used — no scanning. The Cloudflare adapter explicitly sets noDiscovery: false so the full scan runs.
isOptimizable
Only /\.[cm]?[jt]s$/ files are optimizable. .astro, .vue, .svelte are NOT. A CJS dep reachable only through a .astro file in node_modules will never be discovered unless that .astro file is itself in optimizeDeps.entries.
createRequire banner
For node platform environments, Vite injects import { createRequire } from 'module'; const require = createRequire(import.meta.url);. For browser/webworker environments (like Cloudflare Workers), this banner is not injected. Any surviving require() call breaks at runtime.
How Astro sets up optimizeDeps
vite-plugin-environment (packages/astro/src/vite-plugin-environment/index.ts)
Implements configEnvironment. For each Vite environment (ssr, astro, prerender, client):
- Sets
optimizeDeps.entriesto includesrc/**/*.{jsx,tsx,vue,svelte,html,astro}and**/node_modules/**/*.astro. - The
**/node_modules/**/*.astroentry is critical — it lets esbuild scan.astrofiles inside installed packages, so their CJS deps get discovered. - Only sets entries when
optimizeDeps?.noDiscovery === false. - Maintains
ONLY_DEV_EXTERNAL— a hardcoded fallback list of CJS deps externalized in dev.
vitefu and crawlFrameworkPkgs (packages/astro/src/core/create-vite.ts)
crawlFrameworkPkgs walks node_modules and identifies packages that peer/depend on astro, have astro in their keywords, or match astro-* naming. These are placed in resolve.noExternal (bundled through Vite rather than externalized).
Cloudflare adapter (packages/integrations/cloudflare/)
Sets an explicit optimizeDeps.include for the ssr environment and registers astroFrontmatterScanPlugin (packages/integrations/cloudflare/src/esbuild-plugin-astro-frontmatter.ts) as an esbuild plugin. The frontmatter plugin reads the --- block of .astro files and re-emits it as TypeScript so esbuild can scan the imports.
How non-JS files are handled in the scan
Vite's esbuildScanPlugin (in vite/dist/node/chunks/config.js) routes files by type:
htmlTypesRE(.html,.vue,.svelte,.astro,.imba) →htmlnamespace.- JS/TS files → loaded directly and scanned.
htmlTypeOnLoadCallback reads the file, finds <script> tags, and re-emits them as virtual modules. .astro frontmatter is between --- markers, not in <script> tags, so Vite's built-in handler returns empty output for .astro unless a custom esbuild plugin (like astroFrontmatterScanPlugin) intercepts.
shouldExternalizeDep returns true when resolved === rawId. Absolute-path entries that match htmlTypesRE may be treated as external rather than scanned. (Possible Vite upstream bug.)
Debugging playbook
1. Confirm it's an optimizer issue
Look for the failing dep in .vite/deps_ssr/ (or .vite/deps/ for the client env). Missing → not pre-bundled. Present with require() calls → bundle didn't transform CJS.
DEBUG="vite:deps" astro dev2. Check entries
Look for [vite:deps] Crawling dependencies using entries: in debug output. If empty, vite-plugin-environment likely didn't run (check noDiscovery).
3. Check discovery
Add temporary logging to Vite's esbuildScanPlugin onResolve handler:
if (isOptimizable(resolved, optimizeDepsOptions)) {
console.log(`[dep-scan] FOUND dep: ${id} -> ${resolved}`);
} else {
console.log(`[dep-scan] NOT optimizable: ${id} -> ${resolved}`);
}4. Check .astro entry scanning
Add logging to astroFrontmatterScanPlugin:
build.onLoad({ filter: /\.astro$/ }, async (args) => {
console.log('[astro-frontmatter-scan] scanning:', args.path);
});5. Check vitefu exclusions
console.log(`[vitefu] dep=${dep} isFrameworkPkg=${isFrameworkPkg}`);6. Check computeEntries
console.log(`[computeEntries] env=${environment.name} entries:`, entries);Potential fixes
Add to optimizeDeps.entries
Make sure .astro files in node_modules are scanned:
entries: [
`${srcDirPattern}**/*.{jsx,tsx,vue,svelte,html,astro}`,
'**/node_modules/**/*.astro',
];Add to optimizeDeps.include
Explicit and targeted:
optimizeDeps: {
include: ['some-pkg > problematic-cjs-dep'];
}Add resolveDir to custom esbuild onLoad returns
return {
contents,
loader: 'ts',
resolveDir: dirname(args.path),
};Without resolveDir, imports inside the returned contents silently fail to be discovered.
Audit ONLY_DEV_EXTERNAL
A dep on this list is externalized in dev and never optimized — so a CJS one breaks in ESM-only runtimes. Prefer fixing discovery over adding more entries here.
Key files
| File | Role |
|---|---|
packages/astro/src/vite-plugin-environment/index.ts |
Sets optimizeDeps.entries, include, exclude, noExternal, external per environment. |
packages/astro/src/core/create-vite.ts |
Calls crawlFrameworkPkgs, wires up the Vite plugins. |
packages/integrations/cloudflare/src/index.ts |
Cloudflare configEnvironment — explicit include, registers astroFrontmatterScanPlugin. |
packages/integrations/cloudflare/src/esbuild-plugin-astro-frontmatter.ts |
Extracts frontmatter from .astro files during dep scan. |
vite/dist/node/chunks/config.js (in node_modules) |
Vite's esbuildScanPlugin, computeEntries, globEntries, runOptimizeDeps. |
Related pages
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.