import type { ClientModuleGraph } from "./module-graph.js";
export interface BuildManifest {
/** URL of the client entry module (content-hashed). */
readonly entry: string;
/** URLs of every emitted asset (entry + chunks) - for serving + preloading. */
readonly assets: readonly string[];
/** `routeId → [layout chunk URLs…, own chunk URL]` - the chunks a route needs, for `createWebApp`'s
* `routePreload` (`` the matched route alongside the entry). Each route +
* layout is also a build entrypoint, so it gets a named chunk the bootstrap's lazy import dedupes to. */
readonly routes: Readonly>;
/** URL paths copied from `publicDir`, sorted. Lets the server entry serve them without scanning a
* directory per request, and lets an adapter that needs a file list (CDN upload, platform static
* assets) consume one. Omitted when there is no `public/`. */
readonly publicFiles?: readonly string[];
/** The app's bundled, content-hashed stylesheet(s) - the bootstrap's **aggregate** CSS (every
* `import './x.css'` reachable from the app). The complete stylesheet regardless of which file
* imported the CSS; the always-safe fallback `createWebApp` links when a route has no per-route entry
* in {@link routeStyles}. Omitted when the app imports no CSS. */
readonly css?: readonly string[];
/** `routeId → [chain CSS URLs]` - only the stylesheets the matched route's layout chain + own file
* actually use (Bun emits a per-entrypoint CSS bundle per route/layout, with shared-component CSS
* inlined into each consumer). `createWebApp` links these instead of the aggregate, so a page ships
* only its own CSS. A route is omitted (→ aggregate fallback) when its `[name]` collides with another
* route's basename (ambiguous CSS↔route) or the build emitted orphan shared-chunk CSS - correctness
* over minimality. Absent entirely when the app imports no CSS. */
readonly routeStyles?: Readonly>;
}
/** The built worker bundle - point your `wrangler.toml`'s `main` at `worker`. */
export interface ServerBuild {
/** Path to the bundled, self-contained worker entry. */
readonly worker: string;
/** Paths of every emitted output (entry + any code-split chunks) - what to ship to the platform. */
readonly outputs: readonly string[];
}
/** A deploy target `nifra build --target ` can emit. `static` is pure SSG (no server). */
export declare const BUILD_TARGETS: readonly ["bun", "node", "deno", "cf-pages", "vercel", "static"];
export type BuildTarget = (typeof BUILD_TARGETS)[number];
/** A type guard narrowing an arbitrary string to a {@link BuildTarget}. */
export declare function isBuildTarget(value: string): value is BuildTarget;
export type ServerBuildTarget = "browser" | "node" | "bun";
export interface StaticBuildTargetPlan {
readonly target: "static";
readonly kind: "static";
readonly serverTarget: undefined;
readonly outputFile: undefined;
readonly run: string;
}
export interface ServerBuildTargetPlan {
readonly target: Exclude;
readonly kind: "server";
readonly serverTarget: ServerBuildTarget;
/** The worker's final filename inside the assembled deploy directory. */
readonly outputFile: "_worker.js" | "index.js" | "server.js";
readonly run: string;
}
export type BuildTargetPlan = StaticBuildTargetPlan | ServerBuildTargetPlan;
/**
* Resolve the target-specific deploy shape before any bundling starts. Keeping this decision pure
* means Bun and Vite strategies share the same output filename, server target, and hand-off text;
* the filesystem emitter only has to execute the plan.
*/
export declare function planBuildTarget(target: BuildTarget, outDir: string): BuildTargetPlan;
/**
* A build-tool STRATEGY for {@link buildTargetWith} - the two bundling steps, and nothing else. Everything
* around them (server-entry codegen, deploy assembly, prerender, size report) is bundler-agnostic and
* lives in `buildTargetWith`, so a second bundler (Vite) is this interface, not a second orchestrator.
*
* Plugin lists are `readonly unknown[]` because a Bun plugin and a Vite plugin are different types; each
* strategy casts to its own. `buildTargetWith` only forwards them.
*/
export interface Bundler {
/** Build the client bundle → the shared {@link BuildManifest}. */
buildClient(input: {
readonly routesDir: string;
readonly outDir: string;
readonly clientModule: string;
readonly plugins?: readonly unknown[];
readonly conditions?: readonly string[];
readonly define?: Readonly>;
readonly publicDir?: string | false;
readonly publicEnvPrefix?: string;
/** Project root (Vite needs it; the Bun strategy ignores it). */
readonly root?: string;
}): Promise;
/** Build the server worker → the shared {@link ServerBuild}. */
buildServer(input: {
readonly routesDir: string;
readonly serverEntry: string;
readonly outDir: string;
readonly clientEntry: string;
readonly target: "browser" | "node" | "bun";
readonly plugins?: readonly unknown[];
readonly define?: Readonly>;
readonly root?: string;
}): Promise;
}
/** One `node:`-builtin-in-the-client finding: the offending builtin, the emitted chunk it landed in,
* and the shortest USER-module import chain that pulled it there (entry → … → builtin). */
export interface NodeBuiltinFinding {
readonly builtin: string;
readonly chunk: string;
/** The shortest import path from a user entry to the builtin, as a list of display labels:
* `[entryFile, ...as-written specifiers along the way, builtin]`, e.g.
* `["routes/article/[slug].tsx", "../data.ts", "../db/client.ts", "postgres", "node:tls"]`. The
* entry is its graph key (the route file); each hop is the import's *as-written* specifier; the tail
* is the builtin. Empty only if the builtin module isn't reachable from any traced input (it always
* is when flagged). */
readonly chain: readonly string[];
}
/**
* Scan a build's metafile for any `node:` builtin that a USER module pulled into a CLIENT output
* chunk, returning a sorted, deduped list of {@link NodeBuiltinFinding}s. Three graph facts combine so
* the report is precise AND actionable:
* 1. **What the user wrote** - only builtins imported by a NON-`node:` input count, so Bun's own
* polyfill chain (`node:crypto` → `node:buffer`/`node:stream`/…) doesn't bury the real cause.
* 2. **Where it landed** - the chunk is read from the per-output `inputs`, so the error names the
* emitted file to look at.
* 3. **How it got there** - the shortest import chain from a user entry to the builtin
* (`shortestBuiltinChain`), so the error points straight at the offending `import` line instead of
* leaving the dev to grep the dependency tree (the DX gap this closes).
* Graph-based (never the emitted text), so it survives minification and can't be fooled by a string
* literal that merely contains `"node:crypto"`. Pure + exported for unit testing. Empty ⇒ clean.
*/
export declare function detectNodeBuiltinsInClient(graph: ClientModuleGraph): ReadonlyArray;
/** The marker specifier an author imports to opt a module into the client-leak guard. Matched on the
* import edge's *as-written* `original` first (the robust signal: it's exactly what the author typed,
* before Bun resolves it to `src/server-only.ts` / `dist/server-only.js`). */
export declare const SERVER_ONLY_MARKER = "@nifrajs/web/server-only";
/** One `server-only`-module-in-the-client finding: the offending module (the as-written marker-import
* chain's tail before the marker), the emitted chunk it landed in, and the shortest USER-module import
* chain that pulled it there (entry → … → the server-only module). */
export interface ServerOnlyFinding {
/** The emitted client chunk the server-only module landed in (basename). */
readonly chunk: string;
/** The shortest import path from a user entry to the server-only module, as display labels:
* `[entryFile, ...as-written specifiers…, " (marked server-only)"]`. Mirrors
* {@link NodeBuiltinFinding.chain}; the tail names the marked module so the message reads
* `routes/x.tsx → ../secrets.ts (marked server-only)`. */
readonly chain: readonly string[];
}
/**
* Scan a build's metafile for any module that opts into the `server-only` marker (a side-effect
* `import "@nifrajs/web/server-only"`) yet landed in a CLIENT output chunk, returning a sorted, deduped
* list of {@link ServerOnlyFinding}s. Mirrors {@link detectNodeBuiltinsInClient}: it reads the SAME
* graph facts - which inputs import the marker (the "marked" modules), which chunk each landed in (the
* per-output `inputs`), and the shortest import chain from a user entry to it. The marker module ITSELF
* (which imports nothing) is excluded - only the modules that *opt in* are flagged. Pure + exported for
* unit testing. Empty ⇒ clean.
*/
export declare function detectServerOnlyInClient(graph: ClientModuleGraph): ReadonlyArray;
/** The build-failing message for `node:` builtins that reached the client bundle. `undefined` ⇒ clean. */
export declare function formatNodeBuiltinLeak(findings: ReadonlyArray): string | undefined;
/** The build-failing message for `server-only`-marked modules that reached the client. `undefined` ⇒ clean. */
export declare function formatServerOnlyLeak(findings: ReadonlyArray): string | undefined;
/** One emitted chunk's measured size, in raw bytes + gzipped bytes (over-the-wire weight). */
export interface ChunkSize {
/** The emitted file's basename (e.g. `index-abc123.js`). */
readonly name: string;
/** Raw byte length of the file. */
readonly bytes: number;
/** Gzipped byte length (what the client actually downloads, modulo brotli). */
readonly gzip: number;
}
/** A whole build's size report - every chunk (largest first) + the totals. */
export interface SizeReport {
/** Per-chunk sizes, sorted biggest gzip first (the regression you want to see at the top). */
readonly chunks: readonly ChunkSize[];
/** Sum of every chunk's raw bytes. */
readonly totalBytes: number;
/** Sum of every chunk's gzip bytes. */
readonly totalGzip: number;
}
/**
* Aggregate a list of measured chunks into a {@link SizeReport}: sort biggest-gzip-first (ties broken
* by raw bytes, then name for stable output) and sum the totals. Pure - the measurement (reading the
* file + gzipping it) happens in the orchestrator; this is the deterministic, unit-testable core.
*/
export declare function aggregateSizeReport(chunks: readonly ChunkSize[]): SizeReport;
/** Human-readable byte count: `B`/`KB`/`MB` with one decimal above 1 KB (e.g. `12.3 KB`). Pure. */
export declare function formatBytes(bytes: number): string;
/**
* Render a {@link SizeReport} as a terse aligned table (biggest first) with a totals row - the text
* `nifra build --report` prints. Pure (string in, string out) so the formatting is unit-testable.
*/
export declare function renderSizeReport(report: SizeReport): string;
/** A drift finding between a committed server-manifest and the live `routes/` tree. */
export interface ManifestDrift {
/** Route files present in `routes/` but ABSENT from the committed manifest (the manifest is stale -
* the new route won't be served by the worker). */
readonly missing: readonly string[];
/** Route files the committed manifest imports that no longer exist in `routes/` (a deleted/renamed
* route still wired into the worker - a build/runtime break). */
readonly extra: readonly string[];
}
/**
* Extract the route-relative file list a committed server-manifest declares, as the same
* `routes/`-relative keys `discoverRoutes` produces (e.g. `docs/index.tsx`). Reads the route map's KEYS,
* which carry the file extension and no directory prefix - exactly discovery's shape - so the result is
* independent of the specifier prefix the manifest happened to import with. The `@nifrajs/web` import and
* the baked `clientEntry`/`styles`/`routeStyles` lines carry no route-map entry and are ignored.
*
* `_routesPrefix` is accepted for call-site compatibility but no longer needed: the keys are already
* prefix-free. Pure - operates on source text.
*/
export declare function parseManifestRouteFiles(source: string, _routesPrefix?: string): string[];
/** The baked `clientEntry` URL in a committed server-manifest, or `undefined` if absent. Pure. */
export declare function parseManifestClientEntry(source: string): string | undefined;
/** The baked top-level `styles` array in a committed server-manifest (empty if absent/unparseable). Pure. */
export declare function parseManifestStyles(source: string): string[];
/** The baked per-route `routeStyles` map in a committed server-manifest (empty if absent/unparseable). Pure. */
export declare function parseManifestRouteStyles(source: string): Record;
/**
* 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.
*/
export declare function diffManifestRoutes(manifestFiles: readonly string[], discoveredFiles: readonly string[]): ManifestDrift;
/** True when a drift report is clean (no missing + no extra routes). */
export declare function isManifestInSync(drift: ManifestDrift): boolean;
/**
* Format a {@link ManifestDrift} as a named, actionable error message, or `undefined` when in sync.
* Names the exact missing/extra routes + the one fix (regenerate the manifest by re-running the build).
* `manifestPath` is shown for the dev to locate the stale file. Pure.
*/
export declare function formatManifestDrift(drift: ManifestDrift, manifestPath?: string): string | undefined;
//# sourceMappingURL=build-plan.d.ts.map