From 35c6fec92c3dea119b082d773c0c8b7a2451f07b Mon Sep 17 00:00:00 2001 From: Yangshun Date: Tue, 20 Oct 2026 16:56:25 +0800 Subject: [PATCH 1/2] perf(void): keep auth codegen and CLI output out of the plugin entry Every `vp dev` and `vp build` imports the plugin entry before doing anything else. It statically loaded Better Auth's schema helpers (with zod) for the routing plugin's auth codegen and the CLI's terminal-link stack for the AI setup prompt, although only auth apps and that prompt use them. Import both where they are used. This drops 151 modules from the entry's own import graph and about 25ms from `import('void')`. Co-Authored-By: Claude Opus 5.5 --- packages/void/AGENTS.md | 1 + packages/void/src/plugin-ai-setup.ts | 2 +- packages/void/src/plugin-routing.ts | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/void/AGENTS.md b/packages/void/AGENTS.md index 7f8a2ec70..d5ceca91e 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. - **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, From 46fcb55c1b8c9625d4ffd6c376bf25a1ffed293f Mon Sep 17 00:00:00 2001 From: LongYinan Date: Tue, 20 Oct 2026 18:04:21 +0800 Subject: [PATCH 2/2] test(void): guard the plugin entry against loading auth codegen and CLI output Every vp dev and vp build imports the plugin entry first. The test mocks auth/drizzle.ts, cli/output.ts, better-auth/db and terminal-link, imports the entry, and expects none of them to load. It fails on main, where the entry loads both modules statically. AGENTS.md points to the test. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/void/AGENTS.md | 2 +- .../test/unit/plugin-entry-imports.test.ts | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 packages/void/test/unit/plugin-entry-imports.test.ts diff --git a/packages/void/AGENTS.md b/packages/void/AGENTS.md index d5ceca91e..e4ece9a55 100644 --- a/packages/void/AGENTS.md +++ b/packages/void/AGENTS.md @@ -100,7 +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. +- **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/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([]); +});