diff --git a/deno.json b/deno.json index 44e3c14792..ad89db52cd 100644 --- a/deno.json +++ b/deno.json @@ -71,6 +71,7 @@ ], "exports": { ".": "./src/index.ts", + "./index.client": "./src/index.client.ts", "./head": "./src/react/components/Head.tsx", "./router": "./src/react/router/index.tsx", "./context": "./src/react/context/index.tsx", @@ -144,6 +145,7 @@ "./server": "./src/server/index.ts" }, "imports": { + "veryfront/index.client": "./src/index.client.ts", "veryfront/head": "./src/react/runtime/core.ts", "veryfront/router": "./src/react/runtime/core.ts", "veryfront/context": "./src/react/runtime/core.ts", diff --git a/scripts/build/browser-safe-exports.mjs b/scripts/build/browser-safe-exports.mjs index e7bffe26f0..0ea817cba1 100644 --- a/scripts/build/browser-safe-exports.mjs +++ b/scripts/build/browser-safe-exports.mjs @@ -1,4 +1,8 @@ export const BROWSER_SAFE_EXPORTS = [ + // Client/SSR-safe mirror of the root barrel (server bootstrap surface removed). + // The import rewriter redirects `veryfront` here for browser/ssr; it must ship + // in the npm package (built to esm/src/index.client.js) or that redirect 404s. + "./index.client", "./head", "./router", "./context", diff --git a/src/index.client.ts b/src/index.client.ts new file mode 100644 index 0000000000..55e02febc1 --- /dev/null +++ b/src/index.client.ts @@ -0,0 +1,70 @@ +/** + * Client/SSR-safe mirror of the `veryfront` root barrel ({@link file://./index.ts}). + * + * The root barrel re-exports the server bootstrap surface (`createHandler`, + * `startServer`, `toNodeHandler`) from `#veryfront/server`. Because the browser + * and SSR pipelines transform modules per-file (no cross-module tree-shaking), + * an ESM re-export eagerly loads its source module — so pulling the root barrel + * into a client chunk drags the entire server graph in, including + * `src/server/production-server.ts`, which has module top-level `await` and + * cannot be transformed to the es2020 browser target (→ HTTP 500 on that chunk, + * which aborts hydration). + * + * A client-reachable module doing a *used* value import from the barrel (e.g. + * `import { getEnv } from "veryfront"`) is not dead-stripped, so it keeps the + * barrel — and the leak. This barrel exposes exactly the same browser-safe + * surface minus the server bootstrap functions, which no client/SSR page code + * ever legitimately calls. The import rewriter redirects `veryfront` to this + * module for the `browser` and `ssr` targets (see + * `src/transforms/import-rewriter/strategies/veryfront-strategy.ts`), the same + * mechanism `veryfront/workflow` already uses. + * + * Keep the exports below in sync with {@link file://./index.ts} — everything + * except the `createHandler` / `startServer` / `toNodeHandler` value export. + * + * @module veryfront + */ + +export { defineConfig, defineConfigWithEnv, mergeConfigs } from "#veryfront/config"; +export type { VeryfrontConfig } from "#veryfront/config"; + +export { getEnv } from "#veryfront/platform"; + +// NOTE: the server bootstrap value export (`createHandler`, `startServer`, +// `toNodeHandler` from "#veryfront/server") is intentionally omitted here — it +// is server-only and pulls production-server.ts (top-level await) into client +// chunks. Types are erased at transform time, so re-exporting them is inert. +export type { StartServerOptions, VeryfrontHandler, VeryfrontServer } from "#veryfront/server"; + +export { + badRequest, + forbidden, + json, + notFound as apiNotFound, + redirect as apiRedirect, + serverError, + unauthorized, +} from "#veryfront/routing"; +export type { APIContext, APIHandler, APIResponse, APIRoute } from "#veryfront/routing"; + +export { notFound, redirect } from "#veryfront/data"; +export type { + DataContext, + InferGetServerDataProps, + PageWithData, + StaticPathsResult, +} from "#veryfront/data"; + +export type { MDXFrontmatter, PageContext } from "#veryfront/types"; + +export { + CommonSchemas, + createValidatedHandler, + createValidationError, + INPUT_VALIDATION_FAILED, + parseFormData, + parseJsonBody, + parseQueryParams, + sanitizeData, +} from "#veryfront/security"; +export type { ValidatedHandlerConfig, ValidatedHandlerFunction } from "#veryfront/security"; diff --git a/src/transforms/import-rewriter/strategies/veryfront-strategy.test.ts b/src/transforms/import-rewriter/strategies/veryfront-strategy.test.ts index 78a18cbab4..917aa4d74b 100644 --- a/src/transforms/import-rewriter/strategies/veryfront-strategy.test.ts +++ b/src/transforms/import-rewriter/strategies/veryfront-strategy.test.ts @@ -84,6 +84,27 @@ describe("VeryfrontStrategy", () => { ); }); + it("should redirect the root veryfront barrel to the client-safe barrel for browser", () => { + // Regression: the full root barrel re-exports `#veryfront/server`, which + // pulls `server/production-server.ts` (module top-level await) into client + // chunks — it 500s on the es2020 browser target and aborts hydration. A + // *used* value import (`import { getEnv } from "veryfront"`) is not + // dead-stripped, so the barrel must resolve to the server-free mirror. + const result = strategy.rewrite( + makeInfo("veryfront"), + makeCtx({ target: "browser" }), + ); + assertEquals(result.specifier, "/_vf_modules/_veryfront/index.client.js"); + }); + + it("should redirect the root veryfront barrel to the client-safe barrel for SSR", () => { + const result = strategy.rewrite( + makeInfo("veryfront"), + makeCtx({ target: "ssr" }), + ); + assertEquals(result.specifier, "/_vf_modules/_veryfront/index.client.js?ssr=true"); + }); + it("should not apply SSR override to non-overridden modules", () => { const result = strategy.rewrite( makeInfo("veryfront/head"), diff --git a/src/transforms/import-rewriter/strategies/veryfront-strategy.ts b/src/transforms/import-rewriter/strategies/veryfront-strategy.ts index 0461046764..8d8466f753 100644 --- a/src/transforms/import-rewriter/strategies/veryfront-strategy.ts +++ b/src/transforms/import-rewriter/strategies/veryfront-strategy.ts @@ -27,6 +27,15 @@ import { */ const REACT_ONLY_MODULE_OVERRIDES: Record = { "veryfront/workflow": "/_vf_modules/_veryfront/workflow/react/index.js", + // The root barrel re-exports the server bootstrap surface from + // `#veryfront/server`, which transitively pulls `server/production-server.ts` + // (module top-level await → cannot transform to the es2020 browser target → + // HTTP 500, aborting hydration). A *used* value import from the barrel (e.g. + // `import { getEnv } from "veryfront"`) survives dead-code stripping and drags + // the whole server graph into the client. Redirect to a client/SSR-safe mirror + // barrel that omits only the server bootstrap value export. See + // `src/index.client.ts`. + "veryfront": "/_vf_modules/_veryfront/index.client.js", }; export class VeryfrontStrategy implements ImportRewriteStrategy {