# Vite Dep Optimizer — Astro Reference ## Why this matters `astro build` uses Rollup and handles CJS→ESM transformation reliably. `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 with 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 ### The two phases **Phase 1 — Scan:** esbuild crawls `optimizeDeps.entries` to discover which deps need pre-bundling. When it finds a bare import like `import foo from 'some-pkg'` and `some-pkg` resolves to `node_modules`, it adds it to `depImports`. It does **not** recurse into `node_modules` packages — it records the top-level dep and marks it external. **Phase 2 — Bundle:** esbuild bundles each discovered dep as its own entry point, transforming CJS→ESM. Transitive deps inside that bundle are either inlined (if they're JS/TS) or externalized (if they're non-JS or in a separate `node_modules` package that esbuild doesn't follow into). The key insight: **the scan is intentionally shallow**. A dep's transitive deps are only pre-bundled if they are themselves discovered in the scan, or if they are directly inlined when bundling the parent dep. ### What `optimizeDeps.include` does Entries listed in `optimizeDeps.include` are added directly to `depImports` without needing to be discovered via scanning. This is the blunt-force fix: explicitly tell the optimizer "pre-bundle this dep". The `>` notation (e.g. `@astrojs/prism > prismjs/components/index.js`) resolves transitive deps that aren't directly importable from the project root. ### What `optimizeDeps.entries` does Glob patterns or file paths that tell esbuild _where to start scanning_. By default Vite uses `**/*.html`. In Astro, `vite-plugin-environment` sets these to include source `.astro`, `.jsx`, `.tsx`, etc. files. Entries are crucial — if the scanner never reaches a file that imports a problematic dep, the dep will never be discovered. ### `noDiscovery` When an adapter (e.g. `@cloudflare/vite-plugin`) sets `optimizeDeps.noDiscovery: false` for an environment, the full scan runs. When `noDiscovery: true`, only `optimizeDeps.include` is used — no scanning. In Vite, `noDiscovery` defaults to `undefined` (falsy), which means scanning runs. The Cloudflare adapter explicitly sets `noDiscovery: false`. ### `isOptimizable` Only files matching `/\.[cm]?[jt]s$/` are considered optimizable. **`.astro`, `.vue`, `.svelte`, and other non-JS files are NOT optimizable.** This means: - Non-JS files in `node_modules` are **externalized** during optimization bundling — esbuild does not follow into them. - A CJS dep that is only reachable through a `.astro` file in `node_modules` will not be discovered unless that `.astro` file is itself in `optimizeDeps.entries`. This is a common source of bugs: a package ships `.astro` components that import CJS deps. The `.astro` component is in `node_modules`, so it's not in the project's source entries, and it's not optimizable, so the optimizer never sees its imports. ### The `platform` and `createRequire` banner When esbuild bundles deps for a `node` platform environment, Vite injects: ```js import { createRequire } from 'module'; const require = createRequire(import.meta.url); ``` at the top of each optimized dep. This allows CJS `require()` calls to work at runtime in Node.js. For `browser` / `webworker` platform environments (like Cloudflare Workers), this banner is **not injected**. Any `require()` call that survives into the optimized output will fail at runtime in workerd. This means CJS deps that aren't fully inlined and transformed will break. --- ## How Astro sets up optimizeDeps ### `vite-plugin-environment` (`packages/astro/src/vite-plugin-environment/index.ts`) This plugin implements the `configEnvironment` Vite hook. For each Vite environment (`ssr`, `astro`, `prerender`, `client`), it returns `EnvironmentOptions` including `optimizeDeps`. Key things it does: - Sets `optimizeDeps.entries` to include source files: `src/**/*.{jsx,tsx,vue,svelte,html,astro}` and `**/node_modules/**/*.astro` - The `**/node_modules/**/*.astro` entry is important: it causes esbuild to scan `.astro` files inside installed packages, which allows their CJS deps to be discovered and pre-bundled. - Only sets entries when `_options.optimizeDeps?.noDiscovery === false` — i.e. only for environments where the full scan is enabled. - Sets `ONLY_DEV_EXTERNAL` — a hardcoded list of CJS deps that should be externalized in dev (kept as a fallback/legacy workaround list). ### `vitefu` and `crawlFrameworkPkgs` (`packages/astro/src/core/create-vite.ts`) `crawlFrameworkPkgs` from the `vitefu` package walks the project's `node_modules` dep tree and identifies "framework packages" — packages that peer/depend on `astro`, have `astro` in their keywords, or match `astro-*` naming conventions. Framework packages are placed in `resolve.noExternal` (they get bundled through Vite rather than externalized) and historically in `optimizeDeps.exclude` (though this may cause issues — see Debugging section). The result (`astroPkgsConfig`) is passed to `vitePluginEnvironment`. ### The Cloudflare adapter's `configEnvironment` `@astrojs/cloudflare` also implements `configEnvironment`. For the `ssr` environment, it sets an explicit `optimizeDeps.include` list (things that need to be pre-bundled unconditionally). It also registers `astroFrontmatterScanPlugin` as an esbuild plugin in `optimizeDeps.esbuildOptions.plugins`. `astroFrontmatterScanPlugin` (`packages/integrations/cloudflare/src/esbuild-plugin-astro-frontmatter.ts`) handles `.astro` files during the dep scan: it reads the frontmatter (`---` block) and returns it as TypeScript for esbuild to process. This allows esbuild to see imports declared in `.astro` frontmatter and discover their deps. --- ## How non-JS files are handled in the scan Vite's `esbuildScanPlugin` (inside `vite/dist/node/chunks/config.js`) routes files through different handlers based on type: - Files matching `htmlTypesRE` (`.html`, `.vue`, `.svelte`, `.astro`, `.imba`) → `html` namespace - JS/TS files → loaded directly and scanned for imports For files in the `html` namespace, `htmlTypeOnLoadCallback` reads the file, looks for `