Build integration API
Shared builder options, runner result, watch-state fallback, path utilities, error formatting, and Webpack/Rspack compiler hooks.
@cookbook/router-cli exposes shared build integration primitives used by bundler plugins.
import {
applyRouterCompilerBuildHooks,
createRouterBuildRunner,
formatRouterBuildErrors,
getFallbackWatchPaths,
normalizeBuilderRouteFiles,
normalizeBuildPath,
resolveBuildPath,
resolveRouterBuildWatchState,
resolveUniqueBuildPaths,
toRouterBuildCliOptions,
} from '@cookbook/router-cli';
import type {
CookbookRouterBuilderPluginOptions,
RouterBuildRunner,
RouterBuildRunnerOptions,
RouterBuildRunnerResult,
RouterBuildWatchState,
RouterCompilerBuildHooksOptions,
} from '@cookbook/router-cli';These APIs are meant for build adapters.
They do not define a universal plugin protocol. Each bundler still owns its own plugin shape, watch API, error channel, and rebuild behavior.
Shared plugin options
interface CookbookRouterBuilderPluginOptions {
readonly cwd?: string;
readonly configFile?: string;
readonly routeFiles?: string | readonly string[];
readonly outDir?: string;
readonly fs?: CliFileSystem;
}
interface RouterBuildRunnerOptions
extends CookbookRouterBuilderPluginOptions {}Prop
Type
These options are extended by the Webpack, Rspack, Rollup, esbuild, and Bun plugin option interfaces.
Vite has its own plugin option shape. It derives cwd from the resolved Vite root and adds debounceMs.
createRouterBuildRunner(options?)
interface RouterBuildRunnerResult
extends CommandResult {
readonly watchPaths: readonly string[];
readonly outDir: string;
readonly error?: Error;
}
interface RouterBuildRunner {
run(): Promise<RouterBuildRunnerResult>;
}
function createRouterBuildRunner(
options?: RouterBuildRunnerOptions,
): RouterBuildRunner;Creates a one-method build runner.
run() performs aggregate generation and resolves watch state.
Prop
Type
run() does not throw for normal generation failures.
It returns:
{
ok: false,
files: [],
errors: ['...'],
watchPaths: ['...'],
outDir: '...',
}If an unexpected exception is thrown, the result includes the normalized original error:
{
ok: false,
files: [],
errors: [error.message],
watchPaths: ['...'],
outDir: '...',
error,
}Watch paths and output directory are still returned after failure. That lets a build adapter keep watching files that can make the next run recover.
resolveRouterBuildWatchState(options?)
interface RouterBuildWatchState {
readonly watchPaths: readonly string[];
readonly outDir: string;
}
function resolveRouterBuildWatchState(
options?: RouterBuildRunnerOptions,
): Promise<RouterBuildWatchState>;Resolves watch inputs without forcing route files to exist.
It first calls effective route option resolution with:
allowEmptyRouteFiles: trueWhen that succeeds, watch paths are:
- resolved config file, when present;
routeFileWatchPaths, when present;- otherwise
routeFiles.
If no watch path is available, it falls back to getFallbackWatchPaths().
When effective option resolution fails, it falls back immediately.
Fallback watch paths are:
- all standard config candidates under
cwd, unlessconfigFileis explicit; - roots derived from explicit
routeFiles, when supplied.
The fallback does not inspect the filesystem.
It exists so broken or missing input can recover after a file is created or fixed.
const state =
await resolveRouterBuildWatchState({
cwd: 'apps/site',
routeFiles: 'src/**/*.route.tsx',
});
state.watchPaths;
// [
// '<root>/cookbook-router.config.ts',
// '<root>/cookbook-router.config.mts',
// ...
// '<root>/src'
// ]toRouterBuildCliOptions(options?)
function toRouterBuildCliOptions(
options?: RouterBuildRunnerOptions,
): CliRouteOptions;Converts builder options into CLI route options.
Behavior:
routeFilesstring becomes a one-item array;- empty or missing route files are omitted;
undefinedfields are not added;cwd,configFile,outDir, andfsare forwarded when supplied.
toRouterBuildCliOptions({
configFile: 'router.config.ts',
routeFiles: 'src/**/*.route.tsx',
outDir: '.router',
cwd: 'apps/site',
});returns:
{
configFile: 'router.config.ts',
routeFiles: ['src/**/*.route.tsx'],
outDir: '.router',
cwd: 'apps/site',
}normalizeBuilderRouteFiles(routeFiles)
function normalizeBuilderRouteFiles(
routeFiles:
| string
| readonly string[]
| undefined,
): readonly string[] | undefined;Normalizes builder route-file input.
| Input | Output |
|---|---|
undefined | undefined |
'routes.ts' | ['routes.ts'] |
['a.ts', 'b.ts'] | ['a.ts', 'b.ts'] |
The function does not validate whether files exist.
getFallbackWatchPaths(options?)
function getFallbackWatchPaths(
options?: RouterBuildRunnerOptions,
): readonly string[];Returns recovery watch paths without loading config or expanding globs.
When configFile is omitted, it returns all standard config candidates scoped under cwd.
When configFile is supplied, it returns only that config path.
When explicit routeFiles are supplied, it also includes watch paths derived from those patterns.
Results are deduplicated.
getFallbackWatchPaths({
cwd: 'apps/site',
routeFiles: 'src/**/*.route.tsx',
});contains:
apps/site/cookbook-router.config.ts
apps/site/cookbook-router.config.mts
apps/site/cookbook-router.config.cts
apps/site/cookbook-router.config.js
apps/site/cookbook-router.config.mjs
apps/site/cookbook-router.config.cjs
apps/site/srcPath helpers
resolveUniqueBuildPaths(root, paths)
function resolveUniqueBuildPaths(
root: string,
paths: readonly string[],
): readonly string[];Resolves each path through resolveBuildPath() and removes duplicates.
Order is first occurrence.
resolveBuildPath(root, path)
function resolveBuildPath(
root: string,
path: string,
): string;Resolves build paths while avoiding accidental double-rooting.
Behavior:
- absolute paths are resolved as absolute paths;
- relative paths already resolving under
rootare kept relative to the current process resolution; - other relative paths are resolved under
root.
This matters when a build tool passes paths already scoped to its project root.
normalizeBuildPath(path)
function normalizeBuildPath(
path: string,
): string;Normalizes path separators for comparisons.
It replaces backslashes with forward slashes.
normalizeBuildPath('a\\b\\c');
// 'a/b/c'It does not resolve the path.
formatRouterBuildErrors(errors)
function formatRouterBuildErrors(
errors: readonly string[],
): string;Formats build errors for bundler diagnostics.
Each line is prefixed:
[cookbook-router]Example:
formatRouterBuildErrors([
'first',
'second',
]);returns:
[cookbook-router] first
[cookbook-router] secondAn empty array returns an empty string.
applyRouterCompilerBuildHooks(compiler, options?)
interface RouterCompilerBuildHooksOptions
extends RouterBuildRunnerOptions {
readonly pluginName?: string;
}
function applyRouterCompilerBuildHooks<
Compiler,
Compilation,
>(
compiler: Compiler,
options?: RouterCompilerBuildHooksOptions,
): void;Installs Webpack/Rspack-style compiler hooks.
This is the shared implementation used by:
@cookbook/router-webpack-plugin;@cookbook/router-rspack-plugin.
Prop
Type
Compiler shape
The compiler must provide promise hooks:
interface RouterCompilerHook<
Arguments extends readonly unknown[],
> {
tapPromise(
name: string,
handler: (...args: Arguments) => Promise<void>,
): void;
}Compiler hooks:
interface RouterCompilerHooks<Compiler, Compilation> {
readonly beforeRun: RouterCompilerHook<[Compiler]>;
readonly watchRun: RouterCompilerHook<[Compiler]>;
readonly afterCompile: RouterCompilerHook<[Compilation]>;
}Compiler options:
interface RouterCompilerOptions {
context?: string;
watchOptions?: {
ignored?: WatchIgnored;
};
}Compiler-like shape:
interface RouterCompilerLike<Compiler, Compilation> {
readonly context?: string;
readonly options: RouterCompilerOptions;
readonly hooks: RouterCompilerHooks<
Compiler,
Compilation
>;
getInfrastructureLogger?(
name: string,
): {
error?(message: string): void;
};
}Compilation-like shape:
interface RouterCompilationLike {
readonly fileDependencies?: {
add(path: string): unknown;
};
readonly contextDependencies?: {
add(path: string): unknown;
};
readonly missingDependencies?: {
add(path: string): unknown;
};
}These helper interfaces are implementation shapes, not exported public types.
Hook behavior
beforeRun:
- runs generation;
- reports errors through infrastructure logger or stderr;
- throws when generation fails.
watchRun:
- runs generation;
- reports errors through infrastructure logger or stderr;
- does not throw when generation fails.
This keeps watch mode alive so the next file change can recover.
afterCompile:
- recomputes watch state;
- adds file, context, and missing dependencies;
- excludes generated output paths from dependencies;
- extends
watchOptions.ignoredwithoutDir.
Dependency classification
afterCompile classifies every watch path.
If the path looks like a file, it is added to:
fileDependencies
missingDependenciesA path looks like a file when its basename contains an extension.
Examples:
| Watch path | Classification |
|---|---|
routes.ts | file + missing dependency |
cookbook-router.config.ts | file + missing dependency |
src | context dependency |
app/routes | context dependency |
Directory-like paths are added to:
contextDependenciesGenerated output paths are skipped.
This prevents the output directory from creating rebuild loops.
Watch ignore behavior
applyRouterCompilerBuildHooks() extends compiler.options.watchOptions.ignored.
When no ignored option exists:
watchOptions.ignored = [outDir];When ignored is an array, outDir is appended unless it is already present.
['node_modules']becomes:
['node_modules', outDir]When ignored is a string or regexp, it becomes an array:
ignoredbecomes:
[ignored, outDir]When ignored is a function, it is wrapped:
watchOptions.ignored = (path) =>
isInsideOutputDirectory(path, outDir) ||
ignored(path);String entries are compared by resolved normalized path before appending. Regular expressions are not deduplicated against outDir.
Error reporting
Build hook errors are formatted with formatRouterBuildErrors().
If the compiler provides:
getInfrastructureLogger(pluginName).errorthe plugin logs there.
Otherwise it writes to stderr:
process.stderr.write(`${message}\n`);In beforeRun, failures are also thrown as a plain Error with unprefixed joined messages:
throw new Error(result.errors.join('\n'));In watchRun, failures are reported but not thrown.
Adapter capability boundary
Vite
@cookbook/router-vite-plugin uses lower-level CLI APIs directly instead of createRouterBuildRunner().
It:
- derives
cwdfrom Viteconfig.root; - runs generation in
buildStart; - throws during production build failures;
- in dev server mode, watches config candidates, route roots, and output directory;
- debounces file events;
- un-watches stale paths;
- re-runs after queued changes;
- sends a full reload after successful regeneration;
- ignores changes inside generated output except output deletion, which triggers regeneration.
Vite option shape:
interface CookbookRouterVitePluginOptions {
readonly configFile?: string;
readonly routeFiles?: string | readonly string[];
readonly outDir?: string;
readonly debounceMs?: number;
readonly fs?: CliFileSystem;
}Webpack
@cookbook/router-webpack-plugin uses applyRouterCompilerBuildHooks().
Plugin class:
class CookbookRouterPlugin {
constructor(
options?: CookbookRouterPluginOptions,
);
}Options extend CookbookRouterBuilderPluginOptions.
Webpack plugin name:
CookbookRouterPluginRspack
@cookbook/router-rspack-plugin uses applyRouterCompilerBuildHooks().
Plugin class:
class CookbookRouterRspackPlugin {
constructor(
options?: CookbookRouterRspackPluginOptions,
);
}Options extend CookbookRouterBuilderPluginOptions.
Rspack plugin name:
CookbookRouterRspackPluginThe package also exports CookbookRouterPlugin as an alias for CookbookRouterRspackPlugin.
Rollup and Rolldown
@cookbook/router-rollup-plugin uses createRouterBuildRunner().
It runs in buildStart.
For every returned watch path, it calls:
this.addWatchFile(path);On failure:
- it warns with formatted errors;
- in watch mode, it keeps running;
- outside watch mode, it calls
this.error(message).
Options extend CookbookRouterBuilderPluginOptions.
esbuild
@cookbook/router-esbuild-plugin uses createRouterBuildRunner().
It runs in build.onStart().
On failure it returns an esbuild OnStartResult with formatted errors.
Options extend CookbookRouterBuilderPluginOptions.
esbuild does not register route roots with a host watcher through this adapter.
Use cbr generate --watch alongside esbuild when route-file creation/deletion outside the module graph must trigger regeneration.
Bun
@cookbook/router-bun-plugin uses createRouterBuildRunner().
It runs in build.onStart().
On failure it throws a formatted Error.
Options extend CookbookRouterBuilderPluginOptions.
Bun does not register route roots with a host watcher through this adapter.
Use cbr generate --watch alongside Bun when route-file creation/deletion outside the module graph must trigger regeneration.
Export inventory for this page
@cookbook/router-cli exports these build-integration values:
applyRouterCompilerBuildHooks
createRouterBuildRunner
formatRouterBuildErrors
getFallbackWatchPaths
normalizeBuilderRouteFiles
normalizeBuildPath
resolveBuildPath
resolveRouterBuildWatchState
resolveUniqueBuildPaths
toRouterBuildCliOptionsIt exports these build-integration types:
CookbookRouterBuilderPluginOptions
RouterBuildRunner
RouterBuildRunnerOptions
RouterBuildRunnerResult
RouterBuildWatchState
RouterCompilerBuildHooksOptionsIt does not export these implementation-only compiler shape types:
RouterCompilerHook
RouterCompilerHooks
RouterCompilerLike
RouterCompilerLogger
RouterCompilerOptions
RouterCompilationLike
WatchIgnored
WatchIgnoredEntryWhere this bites
watchPaths are not all existing files
A watch path can be:
- an existing file;
- a missing config candidate;
- a directory root derived from a glob;
- a route file that will be created later.
Do not register every path only as an existing file dependency.
Webpack/Rspack adapters add file-like paths to both file and missing dependencies so creation can trigger rebuilds.
Do not watch generated output as input
Generated artifacts are outputs.
The compiler hook skips watch paths inside outDir and also extends watch ignores with outDir.
Watching output as input causes rebuild loops.
A failed watch run should not delete previous artifacts
The shared runner returns a failed result and watch paths.
It does not ask adapters to delete prior generated files.
Keep the old valid artifacts and let the next successful run replace them.
Vite has different watch machinery
Do not force Vite through applyRouterCompilerBuildHooks().
Vite needs dev-server watcher mutation, output-directory deletion handling, debounce, stale-path unwatching, and full reload.
Use @cookbook/router-vite-plugin.
esbuild and Bun do not observe new route files by themselves
Their adapters run before builds.
They do not register glob roots with a watcher. Pair them with:
cbr generate --watchwhen route files may be created or deleted outside the current module graph.
Route loading and extraction API
Resolve direct routes, static route modules, JSON route files, globs, watch roots, composition options, and route export metadata.
CLI contract types
Public CLI data contracts for filesystems, route inputs, config loading, command results, manifests, watch handles, and build integration.