Open-Source Wikis

/

Astro

/

Reference

/

Vite dep optimizer

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.entries to discover deps. Bare imports that resolve into node_modules are added to depImports. The scan does not recurse into node_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_modules package).

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.entries to include src/**/*.{jsx,tsx,vue,svelte,html,astro} and **/node_modules/**/*.astro.
  • The **/node_modules/**/*.astro entry is critical — it lets esbuild scan .astro files 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) → html namespace.
  • 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 dev

2. 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.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Vite dep optimizer – Astro wiki | Factory