diff --git a/packages/void/AGENTS.md b/packages/void/AGENTS.md index 7f8a2ec70..e4ece9a55 100644 --- a/packages/void/AGENTS.md +++ b/packages/void/AGENTS.md @@ -100,6 +100,7 @@ generated entries must preserve the module-boundary rules below. - **Import redirection**: The routing plugin's `resolveId` hook redirects bare `"void"` imports to `void/handler` (the lightweight subpath). Without this, route files pull the entire plugin (vite, @cloudflare/vite-plugin, lightningcss) into the Worker bundle. Uses `import.meta.resolve()` to compute absolute paths at plugin init. Class A frameworks and Astro get the same redirect for their server environments from `createBareVoidRedirectPlugin`, in dev and build: their dev trigger dispatch imports every cron and queue file into workerd, so one `import { defineScheduled } from 'void'` would otherwise break every trigger (and, in Astro, every request). - **Installed runtime module identity**: Keep `void` server runtime subpaths out of Vite dependency optimization. Generated entries import runtime modules by resolved file path; prebundling a bare `void/kv` creates a second `envContext` and loses request bindings. Workspace symlinks can hide this defect. The shared runtime-optimizer plugin excludes the package in server environments on every target while preserving client optimization; packed-consumer checks exercise actual npm/pnpm installations. Vite never discovers bare imports from inside node_modules, so the same plugin prebundles Void's CommonJS dependencies (`void > email-validator`, the void/db driver) in every environment whose resolve conditions contain `workerd`: Void's worker, class A frameworks and Astro. Node environments get only the exclusion. SvelteKit's dev server runs server code in Vite's Node `ssr` graph, which externalizes a bare import that resolves into node_modules before any plugin resolves it, so an installed `void/db` would load the package's D1 runtime. The SvelteKit plugin sets `ssr.noExternal: ['void']` during serve; Vite matches it by package name, so the whole package moves into that graph together and the injected `void/_env` shares void/db's env module. Like the routing plugin, it sends bare `void` to `void/handler` in server environments. In SvelteKit dev for PostgreSQL and MySQL apps, the plugin also inlines installed ES module packages that depend on void, so their `void/db` is the app's. In a D1 app, and for a CommonJS package or one that does not list void, the package stays external and loads its own copy of the env modules. `envContext` and the request scope therefore live on `globalThis` under versioned `Symbol.for` keys, like `authContext`, so every copy reads the current request; this also covers modules that a dependency-optimizer reload evaluates again during a request. Change a key when its store shape changes, because two Void versions in one process share it. +- **Plugin entry imports**: Every `vp dev` and `vp build` imports the plugin entry before it starts. Import dependencies that only some apps or commands use (feature codegen, CLI-only helpers) where they are used, so other apps don't load them at startup. `test/unit/plugin-entry-imports.test.ts` guards this; add a mock there when you move a heavy import out. - **pnpm scaffold build policy**: Keep `create-void` and `void init` aligned with the repository: `better-sqlite3: false` uses version 13's bundled Node-API binaries; `esbuild`, `sharp`, and `workerd` have approved builds. Do not require an unnecessary node-gyp rebuild in fresh applications. - **Scaffold dependency installation**: Vite+ projects install through local/global `vp`, bootstrapping with `npx` when neither is available. Preserve existing package-manager pins and lockfiles; fresh Vite+ projects select pnpm through the build-policy YAML without adding a version pin. Plain Vite projects use their selected package manager. Do not run a global package manager directly for Vite+ installs. - **Prerender discovery and environment validation**: Only `POST /__void/prerender-paths` bypasses whole-schema boot validation, because path discovery runs without remote production secrets. Discovery answers only when the build-time runner marks the request (the `__VOID_PRERENDER` Miniflare binding on Cloudflare, the Hono env argument on Node/Bun/Deno), never on a deployed app or dev server. Keep validation on readiness, application requests, page rendering, and event handlers. diff --git a/packages/void/src/plugin-ai-setup.ts b/packages/void/src/plugin-ai-setup.ts index e824170eb..1a466ae1e 100644 --- a/packages/void/src/plugin-ai-setup.ts +++ b/packages/void/src/plugin-ai-setup.ts @@ -1,6 +1,5 @@ import { getApiUrl, getProxyUrl, getToken } from './cli/auth.ts'; import { ensurePlatformProxyAccess } from './cli/cf-access.ts'; -import { printSuccess } from './cli/output.ts'; import { readProjectConfig } from './cli/project.ts'; import { voidWarn, voidError } from './log.ts'; import { scopedAccessCredentials } from './shared/cf-access.ts'; @@ -97,6 +96,7 @@ export async function runAiSetupPrompt( const { PlatformClient } = await import('./cli/client.ts'); const { attachSelectedPlatform, writeProjectConfig } = await import('./cli/project.ts'); const { inferDefaultSlug } = await import('./cli/resolve-project.ts'); + const { printSuccess } = await import('./cli/output.ts'); const client = new PlatformClient(token, { apiUrl }); diff --git a/packages/void/src/plugin-routing.ts b/packages/void/src/plugin-routing.ts index 0d7a9f9cf..0bea6d7d5 100644 --- a/packages/void/src/plugin-routing.ts +++ b/packages/void/src/plugin-routing.ts @@ -12,7 +12,6 @@ import { type AssetRoutingPolicy, } from './asset-routing.ts'; import { loadVoidAuthConfig } from './auth/config.ts'; -import { generateVoidAuthDatabaseModule, generateVoidAuthDrizzleSchema } from './auth/drizzle.ts'; import { lintRoutingPhases, mergeRoutingRules } from './cli/headers.ts'; import { stampClientAssetCachePolicy } from './client-asset-manifest.ts'; import { findEnvFileForPaths } from './codegen/env-types.ts'; @@ -266,6 +265,9 @@ export function routingPlugin(root: string, options: RoutingPluginOptions): Plug return; } + // Better Auth's schema helpers load zod and most of better-auth; only auth apps need them. + const { generateVoidAuthDatabaseModule, generateVoidAuthDrizzleSchema } = + await import('./auth/drizzle.ts'); const authRuntime: VoidAuthRuntimeOptions = { dialect: compileOptions.auth.dialect, providers: compileOptions.auth.providers, diff --git a/packages/void/test/unit/plugin-entry-imports.test.ts b/packages/void/test/unit/plugin-entry-imports.test.ts new file mode 100644 index 000000000..ffd15c8b4 --- /dev/null +++ b/packages/void/test/unit/plugin-entry-imports.test.ts @@ -0,0 +1,27 @@ +// Every `vp dev`/`vp build` imports the plugin entry first, so auth codegen and CLI output +// must not load at startup. +import { expect, it, vi } from 'vite-plus/test'; + +const loaded = vi.hoisted((): Array => []); + +vi.mock('../../src/auth/drizzle.ts', () => { + loaded.push('src/auth/drizzle.ts'); + return {}; +}); +vi.mock('../../src/cli/output.ts', () => { + loaded.push('src/cli/output.ts'); + return {}; +}); +vi.mock('better-auth/db', () => { + loaded.push('better-auth/db'); + return {}; +}); +vi.mock('terminal-link', () => { + loaded.push('terminal-link'); + return {}; +}); + +it('keeps auth codegen and CLI output out of the plugin entry', async () => { + await import('../../src/index.ts'); + expect(loaded).toEqual([]); +});