import { type BunPlugin } from "bun"; import { type BuildManifest, type BuildTarget, type Bundler, type ServerBuild, type SizeReport } from "./build-plan.js"; import { generateServerManifest } from "./index.js"; export * from "./build-plan.js"; export { type CloudflarePagesRoutes, type CloudflarePagesRoutesOptions, cloudflarePagesRoutes, dataFileFor, htmlFileFor, type PrerenderApp, type PrerenderEntry, type PrerenderOptions, type PrerenderResult, prerenderRoutes, } from "./prerender.js"; export interface BuildClientOptions { /** The `routes/` directory to discover (absolute path). */ readonly routesDir: string; /** Output directory for the bundle + `manifest.json` (absolute path). */ readonly outDir: string; /** The adapter's client runtime (exports `mountRouter`), e.g. `"@nifrajs/web-solid/client"`. */ readonly clientModule: string; /** Route/layout file → import specifier (default: `${routesDir}/${file}`). */ readonly resolve?: (file: string) => string; /** Adapter build plugins (e.g. `solidBunPlugin("dom")`). */ readonly plugins?: readonly BunPlugin[]; /** `Bun.build` export conditions (e.g. `["bun", "solid", "browser"]`). */ readonly conditions?: readonly string[]; /** Compile-time replacements (e.g. `{ "process.env.NODE_ENV": '"production"' }`). */ readonly define?: Readonly>; /** Minify the output (default `true`). */ readonly minify?: boolean; /** URL prefix the assets are served under (default `"/assets/"`); also Bun's chunk `publicPath`. */ readonly publicPath?: string; /** * Directory of user-authored static files copied into the build and served at the root (default * `"public"`). Absent directory ⇒ nothing copied, no error. * * NOT the same thing as {@link publicPath}, despite the names: that is the URL prefix for * content-hashed bundle chunks and never covers files an author put on disk. The collision is a * real source of confusion, which is why both are spelled out here. */ readonly publicDir?: string | false; /** * Prefix that opts an environment variable into the **client** bundle (Vite/Next convention; default * `"PUBLIC_"`). Every var in the build environment whose name starts with this prefix is baked into * the client `define` as `"process.env.NAME": JSON.stringify(value)`, so `process.env.PUBLIC_API_URL` * compiles to its literal value in the browser. Vars WITHOUT the prefix are never exposed - the bare * `process.env` define resolves them to `undefined`, so server secrets can't leak into the client * bundle. Set to `""` to disable auto-exposure entirely (no var is baked in). `options.define` still * wins over an auto-exposed var (it's layered last). Sourced from `Bun.env` (falls back to * `process.env`) at build time. */ readonly publicEnvPrefix?: string; } /** * The `process.env.` → `JSON.stringify(value)` define entries for every env var whose name * carries `prefix` (the Vite/Next public-env convention). Exposing ONLY the prefixed vars is the * security boundary: an unprefixed var (a secret) never gets a define, so the bare `process.env` * define resolves it to `undefined` in the client bundle. An empty `prefix` exposes nothing (the * opt-out). Pure + exported so the prefix/redaction contract is unit-testable without a real build. */ export declare function publicEnvDefines(prefix: string, env: Readonly>): Record; export interface CloudflareRouteRules { readonly include: readonly string[]; readonly exclude: readonly string[]; /** Public files the budget could not name, so the build can say so instead of capping silently. */ readonly omitted: readonly string[]; } /** * Build the cf-pages `_routes.json` rules for a set of copied public files, within Cloudflare's budget. * * `exclude` is what Pages serves straight from the CDN instead of invoking the worker, so naming every * public file is ideal - and impossible past ~99 of them. A `public/` of icons, fonts and share images * clears 100 easily, and the rejection lands at `wrangler pages deploy`, long after the build reported * success. * * A glob is emitted only where it cannot be wrong. `/assets/*` always is: the build owns that prefix * outright, since it is where the hashed bundle is written. A `public/` subdirectory is different - the * name is the author's, and a `/blog/*` rule would hand Pages every future path under `/blog/`, so a * `/blog/:slug` route would 404 from the CDN in production only. {@link collapsibleDirs} therefore * collapses a directory only after checking it against the app's real route patterns, which is what * makes the compaction safe rather than merely smaller. Everything else stays an exact path, and a name * containing `*` is dropped rather than escaped, since the rule would become a wildcard. * * What still does not fit is dropped, which is safe because the list is only an optimization: the * generated worker serves any allowlisted path it receives through the ASSETS binding, so an omitted * file costs one worker invocation, not a 404. */ export declare function cloudflareRouteRules(publicFiles: readonly string[], /** * Every route pattern the app serves. Required, not defaulted: an empty list means "this app serves * no routes", which makes every directory collapsible - a defensible answer to state deliberately and * a dangerous one to arrive at by forgetting the argument. */ routePatterns: readonly string[]): CloudflareRouteRules; /** The built asset map - the server reads `entry` for the client script + serves `assets`. */ /** * Copy `from` into `to`, returning the URL paths copied (sorted). * * Lives here, not beside `servePublicDir`, because it is BUILD-time: it reaches for `Bun.Glob` and * `node:fs`, and `public-dir.ts` is reachable from the client bundle graph through the package * entry - a dynamic `import("bun")` there fails the browser build outright. */ export declare function copyPublicDir(from: string, to: string): Promise; /** * Re-emit a committed server-manifest from a freshly-discovered route tree, PRESERVING its baked * client-asset references (`clientEntry` / `styles` / `routeStyles`) and its eager-vs-lazy shape. This is * what makes `nifra sync-manifest` a route-table refresh (renamed / added / removed routes) that does NOT * need a full build. It deliberately does NOT rebuild the client bundle: a brand-new HYDRATING route still * needs a full build so its client chunk exists - this only re-syncs the server manifest's route table. * Pure: `source` + the discovered `manifest` in, new source out. */ export declare function resyncServerManifestSource(source: string, manifest: Parameters[0], routesPrefix: string): string; /** * Diff the route files a committed server-manifest imports against the files freshly discovered in * `routes/`. Returns the `missing` (in routes/, not in manifest - stale manifest) and `extra` (in * manifest, gone from routes/ - dangling import) sets. Empty arrays ⇒ in sync. Pure - the caller * supplies both file lists (the committed source is parsed via {@link parseManifestRouteFiles}; the * fresh list comes from `discoverRoutes`). Lists need not be pre-sorted; the result is sorted. */ /** * Build the client bundle for a file-routed app. Writes the hashed assets + `manifest.json` to * `outDir` and returns the manifest. Throws (with the bundler logs) on build failure - never * silently ships a broken bundle. */ export declare function buildClient(options: BuildClientOptions): Promise; export interface BuildServerOptions { /** The `routes/` directory to discover (absolute path). */ readonly routesDir: string; /** The worker entry module (absolute path) - your `worker.ts`. It imports `{ manifest, clientEntry }` * from the generated `./server-manifest`, builds `createWebApp`, and `export default toFetchHandler(app)`. */ readonly serverEntry: string; /** Output directory for the bundled worker (absolute path). */ readonly outDir: string; /** The content-hashed client entry URL (from `buildClient`'s manifest) - **baked** into the generated * server manifest, since a disk-less worker can't read `manifest.json` at runtime. */ readonly clientEntry: string; /** The app's aggregate stylesheet URLs (`buildClient`'s `BuildManifest.css`) - baked into the generated * manifest so the server entry hands them to `createWebApp` (→ ``). Omit ⇒ no CSS * link (the built SSR page would otherwise render unstyled). */ readonly styles?: readonly string[] | undefined; /** Per-route stylesheet URLs (`buildClient`'s `BuildManifest.routeStyles`) - baked alongside `styles`. */ readonly routeStyles?: Readonly> | undefined; /** Route/layout file → import specifier in the generated manifest (default: a relative path from the * manifest's location - written next to `serverEntry` - to `routesDir`). */ readonly resolve?: (file: string) => string; /** Filename for the generated server-manifest module, written next to `serverEntry` (default * `"server-manifest.ts"`); your `serverEntry` imports it as `./server-manifest`. */ readonly manifestFile?: string; /** Adapter build plugins (e.g. `solidBunPlugin("ssr")` - Solid routes need their SSR transform at * build time; React's JSX is Bun-native and needs none). */ readonly plugins?: readonly BunPlugin[]; /** `Bun.build` resolution conditions (default `["workerd", "edge-light", "browser"]`) - selects each * dependency's edge build. */ readonly conditions?: readonly string[]; /** Compile-time replacements (default `{ "process.env.NODE_ENV": '"production"' }` → production * React/Solid on the edge). Pass an explicit object to override (e.g. `{}` to opt out). */ readonly define?: Readonly>; /** Minify the output (default `true`). */ readonly minify?: boolean; /** `Bun.build` target (default `"browser"` - the right shape for edge runtimes: Cloudflare Workers, * Vercel Edge, Deno, Deno Deploy). Use `"node"` for a `@nifrajs/node` server (so `node:*` built-ins * stay external), or `"bun"` for a Bun server. The default `conditions` + the edge resolve shims * only apply to the `"browser"` target; `"node"`/`"bun"` resolve their own renderer builds via the * matching condition. */ readonly target?: "browser" | "node" | "bun"; /** **Lazy/code-split routes** (default `false`): emit `() => import(route)` loaders + bundle with * `splitting`, so each route is its own chunk loaded on first request (smaller cold-start parse) * instead of all parsed at boot. The output becomes the worker entry **+ chunk files** in `outDir` * - on Cloudflare, ship them with wrangler's `no_bundle` + `find_additional_modules` + an ESModule * `rule` (Node/Deno import the chunks natively). Eager (one self-contained file) stays the default. */ readonly lazy?: boolean; } export declare const reactDedupePlugin: (from: string) => BunPlugin; export declare const preactDedupePlugin: (from: string) => BunPlugin; /** * Dedupe Svelte to a single copy - the Svelte analogue of `reactDedupePlugin`/`preactDedupePlugin`, closing * the same class of bug for Svelte (which had NO build-time dedup before). A workspace- or file-linked * `@nifrajs/web-svelte` can resolve its OWN `svelte` (e.g. a sibling repo's install store) while the app's * components resolve another - SAME version, two physical copies. Svelte 5's client runtime * (`svelte/internal/client`) holds module-level component-context state, so two copies means the compiled * components register on one runtime while `hydrate` runs on the other → hydration throws * `Cannot read properties of undefined (reading 'call')` and the server-rendered markup is wiped. * * Pin every `svelte` + `svelte/internal/*` import to the ONE copy resolvable from `from` (the app root) so * the renderer (`hydrate`/`mount`) and the compiled components share one runtime. Unlike react/preact (a * fixed subpath list), Svelte has many internal subpaths, so each matched import is resolved dynamically. * `svelte/compiler` (build-time only, not in the bundle) doesn't match the filter and is left alone. No-op * when Svelte isn't used / isn't resolvable from `from`. */ export declare const svelteDedupePlugin: (from: string) => BunPlugin; /** * Remix-style `.server` convention for the CLIENT build. A module named `*.server.ts(x)` (`db.server.ts`, * `auth.server.ts`, …) is server-only - empty it in the browser bundle so its (possibly `node:` / native / * Capacitor) import subtree never reaches the client. The body is CJS-with-a-Proxy so any named OR default * import resolves to `undefined` rather than a "missing export" bundle error (verified), and the real * import subtree is gone. The complement to the node-builtin guard: when a server-only import is co-located * in a route file (so it can't be tree-shaken out and the guard fails loud), moving it into a `*.server` * module is the fix. CLIENT-only - buildServer keeps the real module, which runs server-side. */ export declare const serverOnlyEmptyPlugin: () => BunPlugin; /** * Server functions in the CLIENT build: replace each `*.fn.ts` module with stubs that call the routes * the server mounted, so the function bodies - and everything they import - never reach a browser. * * The sibling of {@link serverOnlyEmptyPlugin}, and a deliberate contrast: a `*.server` module is * EMPTIED because nothing may call it from the client, while a `*.fn` module is REPLACED because the * client is supposed to call it, just over HTTP. The generation itself is in * `internal/server-boundary.ts` so the Vite pipeline emits identical stubs from the same code; two * hand-written copies would be a client that works in dev and 404s in production. * * CLIENT-only. The server build keeps the real module, which is what `serverFunctions()` mounts. */ export declare const serverFnStubPlugin: () => BunPlugin; /** * Build a self-contained **worker bundle** for a file-routed app on a disk-less edge (Cloudflare * Workers / workerd). Discovers routes (build-time fs), codegens the static-import server manifest * (`generateServerManifest`, written next to `serverEntry`), then bundles `serverEntry` with * `Bun.build` using **edge conditions** + the adapter's SSR plugins. The output imports no `node:fs` * and does no dynamic-path import, so it runs on workerd: point `wrangler.toml`'s `main` at it and * serve the client assets via Workers Assets. Throws (with the bundler logs) on failure - never * silently ships a broken worker. */ export declare function buildServer(options: BuildServerOptions): Promise; /** * Codegen the per-target **server entry** module (source text) for `buildServer` to bundle. It imports * the app's `adapter` (from `framework.ts`), the optional `backend` (from `backend.ts`), and the * generated `{ manifest, clientEntry }` (from `./server-manifest`), builds `createWebApp`, then wires * the right host: * - `cf-pages` / `vercel`: `export default` the fetch handler (the platform serves /assets/* itself). * - `deno`: same fetch-handler default, plus `Deno.serve` self-host when run directly. * - `bun` / `node`: a self-hosting server that ALSO serves the client bundle from disk (those * runtimes have a filesystem; the static `/assets/*` sit next to the entry). * `adapterImport`/`backendImport` are the specifiers the entry uses (relative to where it's written) - * `buildServer` writes the entry next to `serverEntry`, so they're resolved from there. Pure (string in, * string out) so the generation is unit-testable without a real build. */ export declare function generateServerEntry(options: { readonly target: BuildTarget; /** Import specifier for the module exporting `adapter` (e.g. `"../framework.ts"`). */ readonly adapterImport: string; /** Import specifier for the module exporting `backend`, or `undefined` for a frontend-only app. */ readonly backendImport?: string; /** Import specifier for the module exporting `use`, or `undefined`. Must resolve to the * same edge-safe module as `adapterImport`. */ readonly useImport?: string; /** Document `` passed to `createWebApp`. */ readonly title?: string; /** Encoded root-relative public file paths copied into the deploy directory. */ readonly publicFiles?: readonly string[]; }): string; export interface BuildTargetOptions { /** The `routes/` directory to discover (absolute path). */ readonly routesDir: string; /** Output directory for the assembled deploy dir (absolute path). Cleared and recreated. */ readonly outDir: string; /** A scratch directory for intermediate codegen (the generated server entry + manifest) and the * server bundle, cleaned up after. Absolute path. */ readonly workDir: string; /** The adapter's client runtime module (exports `mountRouter`), e.g. `"@nifrajs/web-react/client"`. */ readonly clientModule: string; /** Import specifier (resolvable from `workDir`) of the module exporting `adapter`. */ readonly adapterImport: string; /** Import specifier (resolvable from `workDir`) of the module exporting `backend`, or `undefined`. */ readonly backendImport?: string; /** Import specifier (resolvable from `workDir`) of the module exporting `use` (app-level middleware * applied before page routes are declared), or `undefined`. Must resolve to the same edge-safe * module as `adapterImport`. */ readonly useImport?: string; /** Factory that builds the app for `static` prerendering, GIVEN the client build's manifest - so the * emitted hydration `<script src>` uses the REAL content-hashed entry (`client.entry`) plus the same * styles/route-preload the server targets use. A pre-built instance can't work here: the hash isn't known * until `buildClient` runs inside `buildTarget`, so a hardcoded entry 404s → pages render but never * hydrate. Required for `target: "static"` (SSG drives `app.fetch`); ignored otherwise. */ readonly prerenderApp?: (client: BuildManifest) => PrerenderAppLike | Promise<PrerenderAppLike>; /** Client-build plugins (e.g. the MDX/Vue/Solid Bun plugins). */ readonly clientPlugins?: readonly BunPlugin[]; /** Server-build plugins (e.g. the SSR variants). */ readonly serverPlugins?: readonly BunPlugin[]; /** Extra Bun.build resolve conditions for the CLIENT build. */ readonly conditions?: readonly string[]; /** Compile-time `define` replacements layered onto both builds. */ readonly define?: Readonly<Record<string, string>>; /** Project-root static directory copied to deploy root. `false` disables it. */ readonly publicDir?: string | false; /** Prefix of environment variables allowed into the client bundle (default `"PUBLIC_"`). */ readonly publicEnvPrefix?: string; /** Document `<title>` for the generated server entry. */ readonly title?: string; } /** Minimal app surface `buildTarget`'s static path needs - a fetch handler (a built `createWebApp`). */ export interface PrerenderAppLike { fetch(req: Request): Response | Promise<Response>; } /** The result of a target build - the deploy dir + the client manifest + an optional size report. */ export interface BuildTargetResult { /** The deploy target that was built. */ readonly target: BuildTarget; /** The assembled output directory. */ readonly outDir: string; /** The client build's manifest (entry URL, assets, per-route chunks/styles). */ readonly client: BuildManifest; /** A human-readable note on how to run/deploy the output. */ readonly run: string; /** Per-chunk size report over the emitted client (+ server) outputs. Always computed; the CLI prints * it only with `--report`. */ readonly size: SizeReport; } /** * Build a full deploy directory for `target` from a file-routed nifra app. Emits the client bundle to * `<outDir>/assets/*`, then per target: * - `static`: prerenders opted-in routes (`prerenderRoutes`) to `<outDir>/<path>/index.html` (+ * `_data.json`); needs `prerenderApp`. No server. * - `cf-pages`: a `_worker.js` (edge bundle) + a `_routes.json` excluding /assets/* from the worker. * - `vercel`: a `.vercel/output`-shaped function isn't emitted here - `vercel` emits the bundled edge * entry as `<outDir>/index.js` (the CLI's docs point at `vercel`'s Build Output wrapper). [see note] * - `deno`/`node`/`bun`: the self-hosting server bundle (`server.js`) next to the assets. * The server entry is GENERATED (`generateServerEntry`) and bundled (`buildServer`); the app supplies * only adapter/backend/routes. Returns the manifest + a size report. Throws on any build failure. * * Note: the heavier platform wrappers (`.vercel/output` v3 layout, wrangler ISR `find_additional_modules`) * remain app-owned scripts; this command targets the common single-bundle deploys. See the CLI docs. */ /** The default (Bun) strategy - `buildClient`/`buildServer` from this module. */ export declare const bunBundler: Bundler; /** Build a full deploy dir for `target` using the default Bun bundler. See {@link buildTargetWith}. */ export declare function buildTarget(target: BuildTarget, options: BuildTargetOptions): Promise<BuildTargetResult>; /** * The bundler-agnostic deploy orchestrator: everything `buildTarget` does EXCEPT the two bundling steps, * which come from `bundler`. `buildTarget` passes {@link bunBundler}; `buildTargetVite` * (`@nifrajs/web/build-vite`) passes the Vite strategy. One orchestrator, so the deploy-dir shape, * server-entry codegen, prerender and size report are identical across pipelines. */ export declare function buildTargetWith(target: BuildTarget, options: BuildTargetOptions, bundler: Bundler): Promise<BuildTargetResult>; //# sourceMappingURL=build.d.ts.map