diff --git a/CLAUDE.md b/CLAUDE.md index 6d3d52f..ce3c562 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,10 +1,5 @@ -SYSTEM RULE: RESPECT AGENTS.md You must treat 'AGENTS.md' exactly like -'CLAUDE.md' with the following hierarchy rules. +# CLAUDE.md -1. **Parent/Current (Startup)**: IMMEDIATELY upon starting this session, use - your tools to check the current directory AND all parent directories (up to - the git root) for a file named 'AGENTS.md'. If found, read them. -2. **Child (On-Demand)**: Before editing or reading files in a subdirectory, - check if an 'AGENTS.md' exists in that subdirectory. If it does, read it - first to update your context. -3. **Priority**: Treat instructions in 'AGENTS.md' as binding system rules. +This repository keeps its agent guidance in `AGENTS.md`. Read the `AGENTS.md` in +this directory — and any `AGENTS.md` in a subdirectory you're working in — and +follow it as you would this file. diff --git a/src/_client.tsx b/src/_client.tsx index 40c280b..ae37f8a 100644 --- a/src/_client.tsx +++ b/src/_client.tsx @@ -67,8 +67,16 @@ export function useJuniperContext(): RouterContextProvider { return context; } +/** + * Flags indicating which server-side handlers a route file exports. + * + * Set by the build system so the client knows whether to call the route's + * server loader/action over the network during navigation. + */ export interface ServerFlags { + /** Whether the route exports a server-side `loader`. */ loader?: boolean; + /** Whether the route exports a server-side `action`. */ action?: boolean; } diff --git a/src/build.ts b/src/build.ts index f2c53bf..ea1c634 100644 --- a/src/build.ts +++ b/src/build.ts @@ -139,22 +139,41 @@ const activeBuilders = new Set(); * ``` */ export class Builder implements AsyncDisposable { + /** Absolute path to the project root; every other path resolves against it. */ readonly projectRoot: string; + /** Absolute path to the routes directory scanned for file-based routes. */ readonly routesPath: string; + /** Absolute path to the public directory served as static assets. */ readonly publicPath: string; + /** Absolute path to the resolved `deno.json`/`deno.jsonc` config file. */ readonly configPath: string; + /** Absolute path to the generated server entrypoint (`main.ts`). */ readonly serverPath: string; + /** Absolute path to the generated client entrypoint (`main.tsx`). */ readonly clientPath: string; + /** Absolute path to the application's client entry (`main.tsx`). */ readonly entryPoint: string; + /** Paths watched for changes by the dev server; defaults to the project root. */ readonly watchPaths: string | string[]; + /** + * Absolute paths pruned from the watch tree. + * @see {@linkcode Builder.resolveWatchPaths} + */ readonly ignorePaths: string[]; + /** Absolute output directory for built assets (`public/build`). */ readonly outdir: string; + /** All esbuild entry points: any extra entries plus the main client entry. */ readonly entryPoints: string[]; + /** Whether build output is written to disk; `false` is used in tests. */ protected write: boolean; + /** Extra esbuild plugins inserted between the Deno resolver and loader. */ protected plugins: esbuild.Plugin[]; + /** The active esbuild incremental build context, once a build has started. */ protected context?: esbuild.BuildContext; + /** Backing flag for {@linkcode Builder.isBuilding}. */ protected _isBuilding: boolean; + /** Whether a build or rebuild is currently in progress. */ get isBuilding(): boolean { return this._isBuilding; } @@ -268,6 +287,10 @@ export class Builder implements AsyncDisposable { await delay(10); } } + /** + * Disposes the builder via explicit resource management (`await using`), + * delegating to {@linkcode Builder.dispose}. + */ [Symbol.asyncDispose](): Promise { return this.dispose(); } @@ -524,6 +547,16 @@ export const client = new Client(${routesConfigString}); }); } + /** + * Rebuilds the application using the esbuild context from a prior + * {@linkcode Builder.build}. Use this for incremental rebuilds (e.g. from the + * dev server); it throws if no build has started or one is already in progress. + * + * @param options - Which generated entrypoints to regenerate before rebuilding. + * @param options.server - Regenerate the server entrypoint (`main.ts`). + * @param options.client - Regenerate the client entrypoint (`main.tsx`). + * @returns A promise that resolves to the esbuild build results. + */ rebuild(options: { server?: boolean; client?: boolean; diff --git a/src/client.tsx b/src/client.tsx index 1d6a7ca..570c53e 100644 --- a/src/client.tsx +++ b/src/client.tsx @@ -29,13 +29,14 @@ import { env } from "./utils/_env.ts"; export type { HydrationData, ServerFlags }; /** Loads a non-root route module on demand. */ -type RouteModuleLoader = () => Promise; +export type RouteModuleLoader = () => Promise; /** Loads the root route module on demand. */ -type RootRouteModuleLoader = () => Promise; +export type RootRouteModuleLoader = () => Promise; /** A client route definition used by the generated `main.tsx`. */ export interface ClientRoute { + /** The route's URL path segment. */ path: string; /** * The route's module. @@ -118,6 +119,12 @@ export class Client { /** Props to apply to the `` element, from root route's htmlProps export. */ htmlProps?: HtmlProps; + /** + * Builds the client route tree from a root route, ready to + * {@linkcode Client.hydrate}. + * + * @param rootRoute - The root client route, typically the generated `main.tsx`. + */ constructor(rootRoute: RootClientRoute) { this.rootRoute = rootRoute; this.routeFileMap = new Map(); diff --git a/src/mod.ts b/src/mod.ts index 8b5495f..72af11a 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -348,9 +348,17 @@ export interface RouteLoaderArgs< Params extends AnyParams = AnyParams, LoaderData = unknown, > { + /** The request-scoped router context, shared across middleware, loaders, and actions. */ context: RouterContextProvider; + /** The matched route params. */ params: Params; + /** The incoming request. */ request: Request; + /** + * Calls the route's server loader from a client loader, resolving its data + * (or a `Response`). Only meaningful in a client loader paired with a server + * loader; on the server it resolves the loader's own data. + */ serverLoader: () => LoaderData | Response | Promise; } @@ -405,9 +413,17 @@ export interface RouteActionArgs< Params extends AnyParams = AnyParams, ActionData = unknown, > { + /** The request-scoped router context, shared across middleware, loaders, and actions. */ context: RouterContextProvider; + /** The matched route params. */ params: Params; + /** The incoming request. */ request: Request; + /** + * Calls the route's server action from a client action, resolving its data + * (or a `Response`). Only meaningful in a client action paired with a server + * action; on the server it resolves the action's own data. + */ serverAction: () => ActionData | Response | Promise; } @@ -432,8 +448,11 @@ export interface RouteActionArgs< export interface RouteMiddlewareArgs< Params extends AnyParams = AnyParams, > { + /** The request-scoped router context, shared across middleware, loaders, and actions. */ context: RouterContextProvider; + /** The matched route params. */ params: Params; + /** The incoming request. */ request: Request; } @@ -651,8 +670,11 @@ export interface ErrorBoundaryProps< resetErrorBoundary: () => void; } -/** A React component used for a route. */ -type BivariantComponent = { +/** + * A React component type for a route export, with props compared bivariantly + * so route components stay assignable regardless of prop variance. + */ +export type BivariantComponent = { bivarianceHack(props: Props): ReactElement | null; }["bivarianceHack"]; @@ -1060,6 +1082,16 @@ export interface RouteModule< */ export type HtmlProps = React.HTMLAttributes; +/** + * The module shape for the root route (`routes/main.tsx`). + * + * Extends {@linkcode RouteModule} with {@linkcode RootRouteModule.htmlProps} for + * configuring the document's `` element. + * + * @template Params - The type of route params. Defaults to `AnyParams`. + * @template LoaderData - The type of loader data. Defaults to `unknown`. + * @template ActionData - The type of action data. Defaults to `unknown`. + */ export interface RootRouteModule< Params extends AnyParams = AnyParams, LoaderData = unknown, diff --git a/src/utils/testing.ts b/src/utils/testing.ts index b755bf3..3fcee8d 100644 --- a/src/utils/testing.ts +++ b/src/utils/testing.ts @@ -152,17 +152,34 @@ export function simulateEnvironment>( }); } +/** + * A route definition for {@linkcode createRoutesStub}. + * + * Extends {@linkcode RouteModule} with the routing metadata the stub needs to + * place the route in the test router. + */ export interface RouteStub extends RouteModule { + /** The route's URL path segment. */ path?: string; + /** Flags marking which server-side handlers the route simulates. */ serverFlags?: ServerFlags; + /** Overrides the id used to match this route in the test router. */ routeId?: string; } +/** + * Props for the component returned by {@linkcode createRoutesStub}. + */ export interface RoutesStubProps { + /** The initial history entries (URLs) the stubbed router starts at. */ initialEntries?: string[]; + /** Initial loader/action data used to hydrate the stubbed routes. */ hydrationData?: HydrationState; } +/** + * Options for {@linkcode createRoutesStub}. + */ export interface CreateRoutesStubOptions { /** * A function to set up initial context values before rendering.