diff --git a/example/context/server-session.test.ts b/example/context/server-session.test.ts index 311c435..f7fb26d 100644 --- a/example/context/server-session.test.ts +++ b/example/context/server-session.test.ts @@ -43,7 +43,6 @@ describe("server-session context", () => { it("should have sessionId as first 8 characters of a UUID", () => { const session = createServerSession(); - // UUID first segment is 8 hex characters assertMatch(session.sessionId, /^[0-9a-f]{8}$/); }); }); diff --git a/example/context/server-session.ts b/example/context/server-session.ts index 821762c..9fcc485 100644 --- a/example/context/server-session.ts +++ b/example/context/server-session.ts @@ -21,7 +21,6 @@ export function createServerSession(): ServerSession { }; } -// Register server session context serialization registerContext({ name: "serverSession", context: serverSessionContext, diff --git a/example/routes/features/_ignored/main.ts b/example/routes/features/_ignored/main.ts index 6466436..1c4e8ae 100644 --- a/example/routes/features/_ignored/main.ts +++ b/example/routes/features/_ignored/main.ts @@ -1,2 +1 @@ -// This file is ignored for routing purposes because of the _ prefix in the directory name. export const ignored = true; diff --git a/example/routes/features/data/server-deferred.ts b/example/routes/features/data/server-deferred.ts index 7ee3274..341b7d4 100644 --- a/example/routes/features/data/server-deferred.ts +++ b/example/routes/features/data/server-deferred.ts @@ -8,12 +8,10 @@ export interface ServerDeferredLoaderData { } export function loader(): ServerDeferredLoaderData { - // This runs on the server - we can access server-only resources here const timestamp = new Date().toISOString(); return { fastData: `Server data loaded at ${timestamp}`, - // These promises will be serialized and streamed to the client slowData: delay(1500).then(() => "Server data loaded after 1.5 seconds"), verySlowData: delay(3000).then( () => "Server data loaded after 3 seconds", diff --git a/example/routes/features/middleware/client-middleware.tsx b/example/routes/features/middleware/client-middleware.tsx index 1b67757..7f04dd2 100644 --- a/example/routes/features/middleware/client-middleware.tsx +++ b/example/routes/features/middleware/client-middleware.tsx @@ -39,7 +39,7 @@ export function loader( try { navigationInfo = context.get(navigationInfoContext); } catch { - // Context not set (SSR or initial load) - fall back to server loader + // ignore } if (!navigationInfo) { return serverLoader() as Promise; @@ -60,7 +60,7 @@ export default function ClientMiddlewareExample({ try { liveNavigationInfo = context.get(navigationInfoContext); } catch { - // Context not set during SSR or initial load + // ignore } return ( diff --git a/example/routes/features/middleware/combined.ts b/example/routes/features/middleware/combined.ts index 75be415..3a3cd50 100644 --- a/example/routes/features/middleware/combined.ts +++ b/example/routes/features/middleware/combined.ts @@ -11,7 +11,6 @@ import { const app = new Hono(); -// Server middleware - runs on every HTTP request app.use(async (c, next) => { const context = c.get("context"); @@ -31,7 +30,6 @@ app.use(async (c, next) => { export default app; -// Server loader - called during SSR or when client calls serverLoader() export function loader( { context }: RouteLoaderArgs, ): LoaderData { @@ -40,7 +38,7 @@ export function loader( return { serverInfo, - clientInfo: null, // Not available on server + clientInfo: null, loadedAt: new Date().toISOString(), source: "server", }; diff --git a/example/routes/features/middleware/combined.tsx b/example/routes/features/middleware/combined.tsx index 90b9aa0..530f9e7 100644 --- a/example/routes/features/middleware/combined.tsx +++ b/example/routes/features/middleware/combined.tsx @@ -7,7 +7,6 @@ import type { RouteProps, } from "@udibo/juniper"; -// Contexts for sharing data export interface ServerRequestInfo { requestId: string; serverTimestamp: string; @@ -25,7 +24,6 @@ export const clientNavigationInfoContext = createContext< ClientNavigationInfo >(); -// Client middleware - runs during client-side navigation export const middleware: MiddlewareFunction[] = [ async ({ context }, next) => { const clientInfo: ClientNavigationInfo = { @@ -51,26 +49,22 @@ export interface LoaderData { export function loader( { context, serverLoader }: RouteLoaderArgs, ): LoaderData | Promise { - // Check if we have client navigation info (means we're doing client-side navigation) let clientInfo: ClientNavigationInfo | undefined; try { clientInfo = context.get(clientNavigationInfoContext); } catch { - // Context not set yet (SSR or initial load) + // ignore } if (!clientInfo) { - // Initial SSR - call server loader return serverLoader() as Promise; } - // Client-side navigation - we have client middleware context - // Also get server info if it was set during SSR let serverInfo: ServerRequestInfo | undefined; try { serverInfo = context.get(serverRequestInfoContext); } catch { - // Server context not available on client + // ignore } return { @@ -87,18 +81,17 @@ export default function CombinedMiddlewareExample({ }: RouteProps, LoaderData>) { const { serverInfo, clientInfo, loadedAt, source } = loaderData; - // Get live context values from props (may not be set during SSR/initial load) let liveServerInfo: ServerRequestInfo | undefined; let liveClientInfo: ClientNavigationInfo | undefined; try { liveServerInfo = context.get(serverRequestInfoContext); } catch { - // Not set + // ignore } try { liveClientInfo = context.get(clientNavigationInfoContext); } catch { - // Not set + // ignore } return ( diff --git a/src/_build.ts b/src/_build.ts index 8209099..c70b5e7 100644 --- a/src/_build.ts +++ b/src/_build.ts @@ -118,7 +118,7 @@ export async function getServerFlags( return flags; } } catch { - // File doesn't exist or can't be read + // skip } return undefined; } diff --git a/src/_client.test.tsx b/src/_client.test.tsx index cc8ed96..252eb29 100644 --- a/src/_client.test.tsx +++ b/src/_client.test.tsx @@ -18,8 +18,6 @@ describe("App", () => { it("should render with suppressHydrationWarning", () => { render(Test content); const html = document.documentElement; - // suppressHydrationWarning is a React internal prop and won't be visible in the DOM - // but we can verify the html element exists and has the expected structure assertExists(html); }); diff --git a/src/_client.tsx b/src/_client.tsx index bcfd7ad..40c280b 100644 --- a/src/_client.tsx +++ b/src/_client.tsx @@ -168,7 +168,7 @@ export function App({ children, htmlProps }: AppProps) { } const MAX_RELOAD_RETRIES = 2; -const RELOAD_WINDOW_MS = 30000; // 30 seconds +const RELOAD_WINDOW_MS = 30000; interface ReloadState { count: number; @@ -189,10 +189,8 @@ function shouldReload(key: string): boolean { const { count, timestamp } = getReloadState(key); const now = Date.now(); - // Reset counter if outside the time window if (now - timestamp > RELOAD_WINDOW_MS) return true; - // Stop refreshing if we've exceeded max retries return count < MAX_RELOAD_RETRIES; } @@ -201,13 +199,11 @@ function recordReload(key: string): void { const now = Date.now(); if (now - timestamp > RELOAD_WINDOW_MS) { - // Start new window sessionStorage.setItem( key, JSON.stringify({ count: 1, timestamp: now }), ); } else { - // Increment within existing window sessionStorage.setItem( key, JSON.stringify({ count: count + 1, timestamp }), @@ -219,23 +215,14 @@ function clearReloadState(key: string): void { try { sessionStorage.removeItem(key); } catch { - // Ignore errors (e.g., sessionStorage not available) + // ignore } } const LAZY_LOAD_RELOAD_KEY = "__juniper_lazy_load_reload"; const SAME_LOCATION_RELOAD_KEY = "__juniper_same_location_reload"; -/** - * A promise that never settles. Returned from a server-data fetch once a - * full-page navigation or reload has been kicked off (`location.assign` / - * `location.reload`), so React Router keeps the navigation in its *pending* - * state — the current page stays rendered — until the browser replaces the - * document. Resolving instead (even with `undefined`) lets the router commit - * the navigation and render the destination route for a frame first, which - * shows as a visible flash of the wrong page (e.g. an auth-gated page rendering - * unauthenticated for a moment before the redirect). - */ +// Never settles, so React Router stays pending during a full-page navigation/reload; resolving would render the destination route for a frame and flash the wrong page. function holdForDocumentNavigation(): Promise { return new Promise(() => {}); } @@ -258,19 +245,15 @@ async function fetchServerData( const contentType = response.headers.get("Content-Type"); - // Handle streaming CBOR response (data with deferred promises) if (contentType === "application/cbor-stream") { if (!response.ok) { - // For errors, read the full stream and deserialize const buffer = await response.arrayBuffer(); const deserialized = deserializeLoaderData(new Uint8Array(buffer)); throw deserializeError(deserialized as Record); } - // Stream the response - promises will be resolved as data arrives return await deserializeStreamingLoaderData(response); } - // Handle non-streaming CBOR response if (contentType === "application/cbor") { const buffer = await response.arrayBuffer(); const deserialized = deserializeLoaderData(new Uint8Array(buffer)); @@ -289,31 +272,22 @@ async function fetchServerData( const currentUrl = new URL(globalThis.location.href); const redirectUrl = new URL(location, currentUrl); - // `redirectDocument()` requests a full-page navigation (e.g. to a - // server-only route the client router can't render). Hand it to the - // browser instead of attempting a client-side route transition, which - // would 404 on the unmatched route or mis-handle its response. + // `redirectDocument()` needs a real browser navigation; a client-side transition would 404 on the server-only route. if (redirectData.reloadDocument) { delay(0).then(() => { globalThis.location.assign(redirectUrl.href); }); - // Hold the router pending so the destination route never renders before - // the browser navigation lands — otherwise the page flashes. return holdForDocumentNavigation(); } - // If redirecting to the same location, do a browser reload instead if (redirectUrl.href === currentUrl.href) { if (shouldReload(SAME_LOCATION_RELOAD_KEY)) { recordReload(SAME_LOCATION_RELOAD_KEY); delay(0).then(() => { globalThis.location.reload(); }); - // Hold pending while the reload runs (same rationale as above). return holdForDocumentNavigation(); } - // Reload retries exhausted — resolve so the router can render rather than - // hang forever. return undefined; } @@ -611,7 +585,6 @@ export function createLazyRoute( } throw error; } - // Middleware can't be lazily loaded, so strip it from the result const { middleware: _, ...rest } = createRoute( routeFile, serverFlags, diff --git a/src/_dev.ts b/src/_dev.ts index 54d6c24..455be92 100644 --- a/src/_dev.ts +++ b/src/_dev.ts @@ -29,15 +29,9 @@ interface RebuildRequest { client: boolean; } -/** - * Regular expression to match valid route files - */ const VALID_ROUTE_FILE_REGEX = /^routes\/(?:(?!_)[^\/]+\/)*(?!_)[^\/]*(? { describe("registerType", () => { it("should register a custom type serializer", async () => { - // Custom class for testing class Money { constructor(public amount: number, public currency: string) {} static isMoney(value: unknown): value is Money { @@ -95,7 +94,6 @@ describe("Serialization Module", () => { deserialize: (data) => new Money(data.amount, data.currency), }); - // Use hydration data flow to test custom type serialization const money = new Money(100, "USD"); const hydrationData = { matches: [{ id: "/" }], @@ -289,7 +287,6 @@ describe("Serialization Module", () => { const serialized = await serializeHydrationData(hydrationData); const deserialized = deserializeHydrationData(serialized); - // The resolved promise should be reconstructed as a Promise const loaderData = deserialized.loaderData?.["/"] as { user: Promise<{ name: string }>; count: number; @@ -352,7 +349,6 @@ describe("Serialization Module", () => { }); it("should handle custom types in loader data", async () => { - // Register a custom type class Point { constructor(public x: number, public y: number) {} static isPoint(value: unknown): value is Point { @@ -390,7 +386,6 @@ describe("Serialization Module", () => { describe("resetRegistries", () => { it("should clear custom registrations and restore built-ins", () => { - // Register a custom type registerType({ name: "CustomType", is: (_v: unknown): _v is never => false, @@ -398,10 +393,8 @@ describe("Serialization Module", () => { deserialize: (v) => v, }); - // Reset should clear custom registrations resetRegistries(); - // Should be able to register the same name again registerType({ name: "CustomType", is: (_v: unknown): _v is never => false, @@ -409,7 +402,6 @@ describe("Serialization Module", () => { deserialize: (v) => v, }); - // Built-in error serializers should still work const error = new HttpError(400, "Bad Request"); const serialized = serializeError(error); assertEquals(serialized.__errorType, "HttpError"); @@ -459,7 +451,6 @@ describe("Serialization Module", () => { const data = { message: "hello", count: 42 }; const stream = createStreamingLoaderData(data); - // Create a mock Response from the stream const response = new Response(stream, { headers: { "Content-Type": "application/cbor-stream" }, }); @@ -485,9 +476,7 @@ describe("Serialization Module", () => { }>(response); assertEquals(result.sync, "immediate"); - // The deferred value should be a Promise assertEquals(result.deferred instanceof Promise, true); - // And should resolve to the correct value assertEquals(await result.deferred, "delayed value"); }); @@ -581,7 +570,6 @@ describe("Serialization Module", () => { }); it("should handle custom types in deferred data", async () => { - // Register a custom type class Point { constructor(public x: number, public y: number) {} static isPoint(value: unknown): value is Point { @@ -616,7 +604,6 @@ describe("Serialization Module", () => { }); it("should resolve promises in completion order for streaming", async () => { - // Use delays to test that promises resolve independently const { promise: slowPromise, resolve: slowResolve } = Promise .withResolvers(); const { promise: fastPromise, resolve: fastResolve } = Promise @@ -637,11 +624,9 @@ describe("Serialization Module", () => { fast: Promise; }>(response); - // Resolve fast before slow fastResolve("fast result"); assertEquals(await result.fast, "fast result"); - // Then resolve slow slowResolve("slow result"); assertEquals(await result.slow, "slow result"); }); diff --git a/src/_serialization.ts b/src/_serialization.ts index bcf5264..37f9de7 100644 --- a/src/_serialization.ts +++ b/src/_serialization.ts @@ -47,15 +47,12 @@ export interface ContextSerializer { deserialize: (data: S | undefined) => T; } -// CBOR tag numbers for custom types -// Using high tag numbers to avoid conflicts with standard CBOR tags const PROMISE_RESOLVED_TAG = 40000; const PROMISE_REJECTED_TAG = 40001; const CUSTOM_TYPE_TAG = 40002; const ERROR_TAG = 40003; -const PROMISE_PENDING_TAG = 40004; // For streaming: placeholder for unresolved promise +const PROMISE_PENDING_TAG = 40004; -// Internal registries // deno-lint-ignore no-explicit-any const typeRegistry = new Map>(); // deno-lint-ignore no-explicit-any @@ -128,7 +125,6 @@ export function resetRegistries(): void { initializeBuiltInSerializers(); } -// Find the matching type serializer for a value function findTypeSerializer( value: unknown, ): TypeSerializer | undefined { @@ -140,7 +136,6 @@ function findTypeSerializer( return undefined; } -// Find the matching error serializer for an error function findErrorSerializer( error: unknown, ): ErrorSerializer | undefined { @@ -167,7 +162,6 @@ export function serializeError(error: unknown): Record { }; } - // Fallback for unregistered errors if (error instanceof Error) { const serialized: Record = { __errorType: "Error", @@ -180,7 +174,6 @@ export function serializeError(error: unknown): Record { return serialized; } - // Non-Error thrown values return { __errorType: "Unknown", value: error }; } @@ -198,22 +191,17 @@ export function deserializeError(data: Record): unknown { return serializer.deserialize(data); } - // Fallback: try to create error from name if (errorType === "Unknown") { return data.value; } - // Create generic Error const error = new Error(data.message as string); if (data.name) error.name = data.name as string; if (data.stack) error.stack = data.stack as string; return error; } -/** - * Check if a value is a thenable (has a .then method). - * This is more robust than instanceof Promise for cross-realm promises. - */ +// Thenable check (not instanceof Promise) for cross-realm compatibility. function isThenable(value: unknown): value is PromiseLike { return ( value !== null && @@ -223,21 +211,11 @@ function isThenable(value: unknown): value is PromiseLike { ); } -/** - * Recursively process a value to prepare it for CBOR encoding. - * - Converts Promises to tagged resolved/rejected values - * - Converts custom types to tagged values - * - Converts errors to tagged values - * - * @param value - The value to process - * @returns The processed value ready for CBOR encoding - */ async function processValue(value: unknown): Promise { if (value === null || value === undefined) { return value; } - // Use thenable check instead of instanceof Promise for cross-realm compatibility if (isThenable(value)) { try { const resolved = await value; @@ -252,7 +230,6 @@ async function processValue(value: unknown): Promise { return new Tag(ERROR_TAG, serializeError(value)); } - // Check for custom types const typeSerializer = findTypeSerializer(value); if (typeSerializer) { return new Tag(CUSTOM_TYPE_TAG, { @@ -266,7 +243,6 @@ async function processValue(value: unknown): Promise { return processed; } - // Preserve Date objects - CBOR2 handles them natively if (value instanceof Date) { return value; } @@ -282,15 +258,6 @@ async function processValue(value: unknown): Promise { return value; } -/** - * Recursively restore values from CBOR decoded data. - * - Converts tagged promise values back to Promises - * - Converts tagged custom types back to their original types - * - Converts tagged errors back to Error objects - * - * @param value - The decoded value to restore - * @returns The restored value - */ function restoreValue(value: unknown): unknown { if (value === null || value === undefined) { return value; @@ -326,7 +293,6 @@ function restoreValue(value: unknown): unknown { return deserializeError(value.contents as Record); } - // Unknown tag, return contents return restoreValue(value.contents); } @@ -334,7 +300,6 @@ function restoreValue(value: unknown): unknown { return value.map(restoreValue); } - // Preserve Date objects (CBOR2 automatically decodes date tags to Date instances) if (value instanceof Date) { return value; } @@ -394,10 +359,6 @@ export function deserializeLoaderData(data: Uint8Array): T { return restoreValue(decoded) as T; } -// ============================================================================ -// Streaming Serialization (for client-side navigation with deferred data) -// ============================================================================ - interface PendingPromise { id: string; promise: PromiseLike; @@ -464,7 +425,6 @@ function processValueForStreaming( ); } - // Preserve Date objects - CBOR2 handles them natively if (value instanceof Date) { return value; } @@ -492,7 +452,7 @@ function encodeLengthPrefixedChunk(data: unknown): Uint8Array { const cborData = encode(data); const chunk = new Uint8Array(4 + cborData.length); const view = new DataView(chunk.buffer); - view.setUint32(0, cborData.length, false); // big-endian + view.setUint32(0, cborData.length, false); chunk.set(cborData, 4); return chunk; } @@ -524,7 +484,6 @@ export function createStreamingLoaderData( return new ReadableStream({ async start(controller) { - // Send initial chunk with structure and pending promise placeholders const initialChunk = encodeLengthPrefixedChunk(processedData); controller.enqueue(initialChunk); @@ -533,12 +492,10 @@ export function createStreamingLoaderData( return; } - // Process all promises and send resolutions as they complete const resolutionPromises = pendingPromises.map( async ({ id, promise }) => { try { const resolved = await promise; - // Process the resolved value (handle nested custom types, errors, etc.) const processedValue = await processValue(resolved); const resolution: PromiseResolution = { id, @@ -557,7 +514,6 @@ export function createStreamingLoaderData( }, ); - // Send resolutions as they complete (in completion order for lower latency) const remaining = [...resolutionPromises]; while (remaining.length > 0) { const { chunk, index } = await Promise.race( @@ -572,11 +528,6 @@ export function createStreamingLoaderData( }); } -/** - * Restore a value from streaming format, wiring up pending promises. - * Pending promise tags are converted to actual Promises that will be - * resolved when the corresponding resolution message is received. - */ function restoreValueWithPendingPromises( value: unknown, promiseResolvers: Map(); promiseResolvers.set(id, { resolve, reject }); return promise; @@ -638,7 +588,6 @@ function restoreValueWithPendingPromises( ); } - // Preserve Date objects (CBOR2 automatically decodes date tags to Date instances) if (value instanceof Date) { return value; } @@ -654,16 +603,11 @@ function restoreValueWithPendingPromises( return value; } -/** - * Read a length-prefixed chunk from a reader. - * Returns null if end of stream. - */ async function readLengthPrefixedChunk( reader: ReadableStreamDefaultReader, ): Promise { let buffer = new Uint8Array(0); - // Read until we have at least 4 bytes for the length while (buffer.length < 4) { const { done, value } = await reader.read(); if (done) { @@ -677,9 +621,8 @@ async function readLengthPrefixedChunk( } const view = new DataView(buffer.buffer, buffer.byteOffset); - const length = view.getUint32(0, false); // big-endian + const length = view.getUint32(0, false); - // Read until we have the full chunk while (buffer.length < 4 + length) { const { done, value } = await reader.read(); if (done) { @@ -693,10 +636,7 @@ async function readLengthPrefixedChunk( const chunkData = buffer.slice(4, 4 + length); - // If there's leftover data, we need to handle it - // For simplicity, we assume chunks align with reads (which they should for our use case) if (buffer.length > 4 + length) { - // This shouldn't happen with proper chunking, but let's handle it console.warn("Extra data after chunk, this may indicate a protocol issue"); } @@ -720,7 +660,6 @@ export async function deserializeStreamingLoaderData( { resolve: (value: unknown) => void; reject: (error: unknown) => void } >(); - // Read initial chunk const initialChunk = await readLengthPrefixedChunk(reader); if (!initialChunk) { throw new Error("Empty streaming response"); @@ -732,7 +671,6 @@ export async function deserializeStreamingLoaderData( promiseResolvers, ); - // If there are pending promises, start reading resolutions in background if (promiseResolvers.size > 0) { (async () => { try { @@ -742,7 +680,6 @@ export async function deserializeStreamingLoaderData( const resolver = promiseResolvers.get(resolution.id); if (resolver) { if (resolution.status === "resolved") { - // Restore the value (handles nested tags) const restoredValue = restoreValue(resolution.value); resolver.resolve(restoredValue); } else { @@ -753,7 +690,6 @@ export async function deserializeStreamingLoaderData( } } } catch (error) { - // If stream fails, reject all remaining promises for (const resolver of promiseResolvers.values()) { resolver.reject(error); } @@ -814,7 +750,7 @@ export function serializeAllContext( result[name] = data; } } catch { - // Context not set, skip + // skip } } @@ -880,7 +816,6 @@ export async function serializeHydrationData( ): Promise { const { publicEnv, ...rest } = hydrationData; - // Process all values (resolve promises, convert custom types to tags) const processedData = await processValue(rest); return { @@ -899,10 +834,8 @@ export async function serializeHydrationData( export function deserializeHydrationData( serialized: SerializedHydrationData, ): HydrationData { - // Decode from base64 and CBOR const decoded = decodeFromBase64>(serialized.data); - // Restore values (convert tags back to original types) const restored = restoreValue(decoded) as { serializedContext?: unknown; matches: { id: string }[]; @@ -915,16 +848,15 @@ export function deserializeHydrationData( publicEnv: serialized.publicEnv, serializedContext: restored.serializedContext, matches: restored.matches, - // Convert null to undefined for React Router compatibility + // React Router needs undefined, not null, for these fields. errors: restored.errors ?? undefined, loaderData: restored.loaderData ?? undefined, actionData: restored.actionData ?? undefined, }; } -// Initialize built-in error serializers function initializeBuiltInSerializers(): void { - // HttpError (must be registered before generic Error) + // Order matters: HttpError before generic Error, generic Error registered last as fallback. _addErrorSerializer({ name: "HttpError", is: (e): e is HttpError => e instanceof HttpError || isHttpErrorLike(e), @@ -952,7 +884,6 @@ function initializeBuiltInSerializers(): void { }, }); - // TypeError _addErrorSerializer({ name: "TypeError", is: (e): e is TypeError => e instanceof TypeError, @@ -972,7 +903,6 @@ function initializeBuiltInSerializers(): void { }, }); - // RangeError _addErrorSerializer({ name: "RangeError", is: (e): e is RangeError => e instanceof RangeError, @@ -992,7 +922,6 @@ function initializeBuiltInSerializers(): void { }, }); - // ReferenceError _addErrorSerializer({ name: "ReferenceError", is: (e): e is ReferenceError => e instanceof ReferenceError, @@ -1012,7 +941,6 @@ function initializeBuiltInSerializers(): void { }, }); - // SyntaxError _addErrorSerializer({ name: "SyntaxError", is: (e): e is SyntaxError => e instanceof SyntaxError, @@ -1032,7 +960,6 @@ function initializeBuiltInSerializers(): void { }, }); - // URIError _addErrorSerializer({ name: "URIError", is: (e): e is URIError => e instanceof URIError, @@ -1052,7 +979,6 @@ function initializeBuiltInSerializers(): void { }, }); - // Generic Error (must be registered last as fallback) _addErrorSerializer({ name: "Error", is: (e): e is Error => e instanceof Error && e.constructor === Error, @@ -1073,5 +999,4 @@ function initializeBuiltInSerializers(): void { }); } -// Initialize built-in serializers on module load initializeBuiltInSerializers(); diff --git a/src/_server.tsx b/src/_server.tsx index cb8f62a..806cdee 100644 --- a/src/_server.tsx +++ b/src/_server.tsx @@ -309,7 +309,6 @@ async function renderDocument( await startActiveSpan("render.attempt", async (renderAttemptSpan) => { renderAttemptSpan.setAttribute("render.attempt.index", renderAttempts); try { - // Use CBOR-based serialization const hydrationData = { publicEnv: getPublicEnv(allPublicEnvKeys), serializedContext: serializeAllContext(requestContext), @@ -523,7 +522,6 @@ function wrapLazyWithServerFallback( const result = await lazyFn() as Record; const { middleware: _middleware, ...rest } = result; - // Strip client loader/action only if server has them if (hasServerLoader) delete rest.loader; if (hasServerAction) delete rest.action; @@ -553,7 +551,6 @@ export function mergeServerRoutes< routeObj: RouteObject, serverModule: Pick | undefined, ): void { - // Save client handlers before potentially deleting them const clientLoader = routeObj.loader; const clientAction = routeObj.action; @@ -574,7 +571,6 @@ export function mergeServerRoutes< }); }; } else if (clientLoader) { - // Use client loader on server when no server loader exists routeObj.loader = clientLoader; } @@ -590,7 +586,6 @@ export function mergeServerRoutes< }); }; } else if (clientAction) { - // Use client action on server when no server action exists routeObj.action = clientAction; } } @@ -671,7 +666,6 @@ interface HandlersResult { errorHandler: ErrorHandler; } -/** Response headers that must not be relayed onto the redirect envelope. */ const REDIRECT_ENVELOPE_OMIT = new Set([ "location", "content-type", @@ -801,15 +795,12 @@ export function createHandlers< c.header("Vary", "Accept"); if (dataOrResponse instanceof Response) { - // A loader/action redirect: relay it as the redirect envelope so the - // client navigates (SPA or full-page, per `redirectDocument()`). if (isRedirectResponse(dataOrResponse)) { return toRedirectEnvelope(dataOrResponse); } return dataOrResponse; } - // Check if data contains promises - if so, use streaming if (containsPromises(dataOrResponse)) { c.header("Content-Type", "application/cbor-stream"); @@ -819,7 +810,6 @@ export function createHandlers< }); } - // No promises - use non-streaming response for better performance const cborData = await serializeLoaderData(dataOrResponse); return new Response(cborData as unknown as BodyInit, { headers: { @@ -842,7 +832,6 @@ export function createHandlers< console.error(error); if (c.req.header("X-Juniper-Route-Id")) { - // Use registry-based error serialization const serialized = serializeError(error); const cborData = cborEncode(serialized); const headers = new Headers({ @@ -868,8 +857,6 @@ export function createHandlers< }); } - // For 404s on paths with file extensions, return simple response - // to avoid wasting resources rendering pages for bot requests if (error.status === 404) { const url = new URL(c.req.url); const lastSegment = url.pathname.split("/").pop() ?? ""; @@ -962,7 +949,6 @@ export function buildApp< } routeApp.use(async (c, next) => { - // Index routes should only set context for exact path matches if (currentRouteId.endsWith("/index")) { const parentPath = currentRouteId.slice(0, -6) || "/"; const path = new URL(c.req.url).pathname; @@ -1113,10 +1099,7 @@ export function buildApp< app.route("*", catchallApp); } - // Catch data requests that didn't match any route handler at this level. - // This happens when the client navigates to a non-existent URL and React Router - // needs to re-fetch parent route data (e.g., GET /identity/applications with - // X-Juniper-Route-Id: "/" to refresh the root loader). + // Catches data requests unmatched at this level: React Router re-fetches parent route data when navigating to a non-existent URL. if (isClientRoute && errorHandler) { const handleDataRequest = reactHandlers[reactHandlers.length - 1]; app.use("*", (c, next) => { diff --git a/src/build.test.ts b/src/build.test.ts index d459a87..da282f4 100644 --- a/src/build.test.ts +++ b/src/build.test.ts @@ -33,7 +33,6 @@ describe("Builder", () => { const tmp = await Deno.makeTempDir(); const routesDir = path.resolve(tmp, "routes"); await Deno.mkdir(routesDir, { recursive: true }); - // server routes await Deno.writeTextFile( path.resolve(routesDir, "main.ts"), "export const default_ = {} as unknown as any;\nexport default default_\n", @@ -53,7 +52,7 @@ describe("Builder", () => { "export const default_ = {} as unknown as any;\nexport default default_\n", ); - const clientRoutesDir = routesDir; // client uses same root, but .tsx + const clientRoutesDir = routesDir; await Deno.writeTextFile( path.resolve(clientRoutesDir, "main.tsx"), "export default function X(){}\n", @@ -84,7 +83,6 @@ describe("Builder", () => { true, ); - // server side const serverChildren = [ ...serverProps.fileModuleChildren, ...serverProps.parameterizedChildren, @@ -92,22 +90,19 @@ describe("Builder", () => { ]; assertEquals(Boolean(serverProps.main), true); assertEquals(Boolean(serverProps.index), true); - // has parameterized dir child and a catchall route under api assertEquals(serverChildren.some((c) => c.path === ":user"), true); assertEquals(serverChildren.some((c) => c.path === "api"), true); - // client side const clientChildren = [ ...clientProps.fileModuleChildren, ...clientProps.parameterizedChildren, ...clientProps.directoryChildren, ]; - assertEquals(Boolean(clientProps.main), true); // root main uses await import + assertEquals(Boolean(clientProps.main), true); assertEquals(Boolean(clientProps.index), true); assertEquals(clientChildren.some((c) => c.path === ":id"), true); assertEquals(clientChildren.some((c) => c.path === "blog"), true); - // spot check generatedRouteObjectToString formatting for a tiny object const s = generatedRouteObjectToString({ path: "/", children: [{ path: ":id" }], @@ -220,13 +215,11 @@ describe("Builder", () => { write: false, }); const watchPaths = await builder.resolveWatchPaths(); - // The root is expanded into its children so the ignored subtree is skipped. assertEquals(watchPaths.includes(exampleDir), false); assertEquals( watchPaths.includes(path.resolve(exampleDir, "public")), false, ); - // Sibling directories are still watched as whole subtrees. assertEquals( watchPaths.includes(path.resolve(exampleDir, "routes")), true, @@ -241,7 +234,6 @@ describe("Builder", () => { write: false, }); const watchPaths = await builder.resolveWatchPaths(); - // public is expanded so build is dropped but its siblings remain watched. assertEquals( watchPaths.includes(path.resolve(exampleDir, "public", "build")), false, @@ -250,7 +242,6 @@ describe("Builder", () => { watchPaths.includes(path.resolve(exampleDir, "public", "images")), true, ); - // Directories without an ignored descendant stay collapsed to one root. assertEquals( watchPaths.includes(path.resolve(exampleDir, "routes")), true, diff --git a/src/build.ts b/src/build.ts index 32b98bc..f2c53bf 100644 --- a/src/build.ts +++ b/src/build.ts @@ -34,7 +34,6 @@ const fmtCommand = new Deno.Command(Deno.execPath(), { stdout: "piped", }); -/** Normalizes a path to forward slashes for cross-platform comparison. */ function toPosixPath(value: string): string { return value.replace(/\\/g, "/"); } @@ -216,17 +215,11 @@ export class Builder implements AsyncDisposable { return resolved; } - /** - * Recursively collects the watchable paths under {@linkcode dir}, descending - * only into directories that contain an ignored path and dropping ignored - * entries entirely. - */ private async collectWatchPaths( dir: string, into: string[], ): Promise { if (this.isPathIgnored(dir)) return; - // If no ignored path lives inside this directory, watch the whole subtree. const prefix = toPosixPath(dir).replace(/\/+$/, "") + "/"; const containsIgnored = this.ignorePaths.some((ignore) => toPosixPath(ignore).startsWith(prefix) @@ -235,7 +228,6 @@ export class Builder implements AsyncDisposable { into.push(dir); return; } - // Otherwise descend one level and recurse, dropping ignored children. try { for await (const entry of Deno.readDir(dir)) { const child = path.join(dir, entry.name); @@ -246,11 +238,10 @@ export class Builder implements AsyncDisposable { } } } catch { - // Directory became unreadable; skip it rather than crash the watcher. + // skip } } - /** Whether {@linkcode absolutePath} is itself an ignored path or inside one. */ private isPathIgnored(absolutePath: string): boolean { const target = toPosixPath(absolutePath); return this.ignorePaths.some((ignore) => { @@ -274,7 +265,6 @@ export class Builder implements AsyncDisposable { } if (activeBuilders.size === 0) { await esbuild.stop(); - // Wait for esbuild to stop completely await delay(10); } } diff --git a/src/client.test.tsx b/src/client.test.tsx index 730f342..34873d0 100644 --- a/src/client.test.tsx +++ b/src/client.test.tsx @@ -94,8 +94,7 @@ describe("Client", () => { assertEquals(client.rootRoute.path, "/"); assertEquals(client.routeFileMap.size, 8); - // 12 = 8 explicit routes + 4 implicit catchalls for routes without their own catchall - // Implicit catchalls: /about/[...], /blog/[...], /blog/create/[...], /blog/[id]/[...] + // 12 = 8 explicit routes + 4 implicit catchalls for routes without their own catchall. assertEquals(client.routeObjectMap.size, 12); assertEquals(client.routeObjects.length, 1); @@ -210,7 +209,6 @@ describe("Client", () => { }; const client = new Client(routesWithLazyMain); - // htmlProps is not extracted from lazy modules since they're not loaded yet assertEquals(client.htmlProps, undefined); }); }); @@ -340,12 +338,7 @@ describe("createRoute", () => { }); it("holds the navigation pending (no flash) on a reloadDocument redirect", async () => { - // A `reloadDocument` redirect (e.g. an auth guard bouncing to a server-only - // /auth/login) must trigger a full-page navigation WITHOUT the loader - // resolving — otherwise the router commits the navigation and renders the - // destination route for a frame before the browser leaves (the flash). - // fetchServerData reads globalThis.location.href and calls .assign(); Deno - // test has no DOM location, so provide a fake one. + // Deno test has no DOM location, but fetchServerData reads location.href and calls .assign(). const assigned: string[] = []; const originalLocation = globalThis.location; Object.defineProperty(globalThis, "location", { @@ -382,8 +375,6 @@ describe("createRoute", () => { pattern: "/identity", } as LoaderFunctionArgs); - // The loader must NOT settle — it holds the router pending while the - // browser navigates. Race it against a tick; the timeout must win. const pending = Symbol("pending"); const outcome = await Promise.race([ Promise.resolve(result).then(() => "settled"), @@ -391,7 +382,6 @@ describe("createRoute", () => { ]); assert(outcome === pending, "loader should not resolve"); - // And it kicked off a full-page navigation to the resolved URL. assertEquals(assigned, ["http://localhost/auth/login"]); } finally { Object.defineProperty(globalThis, "location", { @@ -486,7 +476,6 @@ describe("getHydrationData", () => { assertEquals(data.errors["/blog"].name, "Bad Request"); assertEquals(data.errors["/blog"].status, 400); - // CustomError without registered serializer falls back to generic Error assertIsError(data.errors["/blog/create"], Error, "Custom error"); assertEquals(data.errors["/blog/create"] instanceof CustomError, false); }), @@ -495,7 +484,6 @@ describe("getHydrationData", () => { it( "with registered CustomError serializer", async () => { - // Register CustomError serializer for this test registerCustomError(); try { await simulateBrowser(errorHydrationData, () => { @@ -512,7 +500,6 @@ describe("getHydrationData", () => { assertEquals(data.errors["/blog"].name, "Bad Request"); assertEquals(data.errors["/blog"].status, 400); - // CustomError with registered serializer deserializes properly assertIsError( data.errors["/blog/create"], CustomError, @@ -521,7 +508,6 @@ describe("getHydrationData", () => { assertEquals(data.errors["/blog/create"].detail, "Custom detail"); })(); } finally { - // Reset registries to clean up for other tests resetRegistries(); } }, @@ -758,7 +744,6 @@ describe("HydrationData serialization and deserialization", () => { let hydrationData: HydrationData, deserializedHydrationData: HydrationData; beforeAll(async () => { - // Register CustomError for this test suite registerCustomError(); hydrationData = { diff --git a/src/client.tsx b/src/client.tsx index 0654646..1d6a7ca 100644 --- a/src/client.tsx +++ b/src/client.tsx @@ -125,7 +125,6 @@ export class Client { this.routeObjects = [{ id: rootRouteId, path: rootRoute.path }]; this.routeObjectMap = new Map(); - // Extract htmlProps from root route module if available if (rootRoute.main && typeof rootRoute.main !== "function") { this.htmlProps = rootRoute.main.htmlProps; } @@ -215,8 +214,7 @@ export class Client { this.routeFileMap.set(catchallRouteId, route.catchall); this.routeObjectMap.set(catchallRouteId, catchallRouteObject); } else { - // Add default catchall that throws 404 for unmatched routes - // This ensures errors bubble to the nearest ErrorBoundary + // Default catchall throws 404 so errors bubble to the nearest ErrorBoundary. const catchallRouteId = generateRouteId(currentPath, "", "catchall"); const catchallRouteObject: RouteObject = { id: catchallRouteId, diff --git a/src/deno.ts b/src/deno.ts index fee07ca..89ae82e 100644 --- a/src/deno.ts +++ b/src/deno.ts @@ -14,7 +14,6 @@ export class FakeSubprocessReadableStream this.stream = new ReadableStream(underlyingSource); } - // Delegate ReadableStream methods to the underlying stream getReader() { return this.stream.getReader(); } @@ -47,7 +46,6 @@ export class FakeSubprocessReadableStream return this.stream[Symbol.asyncIterator](); } - // SubprocessReadableStream specific methods async arrayBuffer(): Promise { const result = await this.getReader().read(); return result.value?.buffer ?? new ArrayBuffer(0); @@ -168,7 +166,6 @@ export class FakeChildProcess implements Deno.ChildProcess { get stdout() { return new FakeSubprocessReadableStream({ start(controller) { - // Immediately close the stream for testing controller.close(); }, }); @@ -177,7 +174,6 @@ export class FakeChildProcess implements Deno.ChildProcess { get stderr() { return new FakeSubprocessReadableStream({ start(controller) { - // Immediately close the stream for testing controller.close(); }, }); @@ -192,11 +188,9 @@ export class FakeChildProcess implements Deno.ChildProcess { } ref(): void { - // No-op for testing } unref(): void { - // No-op for testing } output(): Promise { diff --git a/src/dev.test.ts b/src/dev.test.ts index ab983be..3c289af 100644 --- a/src/dev.test.ts +++ b/src/dev.test.ts @@ -954,11 +954,9 @@ describe("DevServer", () => { it("isValidRouteFile returns true for route .ts/.tsx and false for private/test files", () => { const devServer = new DevServer({ builder }); - // valid assertEquals(devServer.isValidRouteFile("routes/index.ts"), true); assertEquals(devServer.isValidRouteFile("routes/blog/index.tsx"), true); assertEquals(devServer.isValidRouteFile("routes/blog/[id].ts"), true); - // invalid: private files and tests assertEquals( devServer.isValidRouteFile("routes/_components/Button.tsx"), false, @@ -1028,7 +1026,6 @@ describe("DevServer", () => { it("shouldTriggerRebuild honors ignore rules", () => { const devServer = new DevServer({ builder }); const abs = (p: string) => `${builder.projectRoot}/${p}`; - // Ignored by suffixes assertEquals( devServer.shouldTriggerRebuild(abs("somefile~"), "somefile~"), false, @@ -1045,7 +1042,6 @@ describe("DevServer", () => { devServer.shouldTriggerRebuild(abs("app.log"), "app.log"), false, ); - // Ignored build/dev entrypoints assertEquals( devServer.shouldTriggerRebuild(abs("build.ts"), "build.ts"), false, @@ -1054,7 +1050,6 @@ describe("DevServer", () => { devServer.shouldTriggerRebuild(abs("dev.ts"), "dev.ts"), false, ); - // Ignored public build outputs assertEquals( devServer.shouldTriggerRebuild( abs("public/build/app.js"), @@ -1062,7 +1057,6 @@ describe("DevServer", () => { ), false, ); - // Underscore prefixed files/dirs trigger rebuilds (they may be imported by routes) assertEquals( devServer.shouldTriggerRebuild(abs("_private.ts"), "_private.ts"), true, @@ -1074,7 +1068,6 @@ describe("DevServer", () => { ), true, ); - // Ignored tests assertEquals( devServer.shouldTriggerRebuild( abs("routes/page.test.tsx"), @@ -1082,7 +1075,6 @@ describe("DevServer", () => { ), false, ); - // Allowed others assertEquals( devServer.shouldTriggerRebuild( abs("routes/page.tsx"), @@ -1106,7 +1098,6 @@ describe("DevServer", () => { const devServer = new DevServer({ builder: builderWithIgnore }); const abs = (p: string) => `${builderWithIgnore.projectRoot}/${p}`; - // Paths in ignored directories should not trigger rebuilds assertEquals( devServer.shouldTriggerRebuild( abs("docker/compose.yml"), @@ -1129,7 +1120,6 @@ describe("DevServer", () => { false, ); - // Paths not in ignored directories should still trigger rebuilds assertEquals( devServer.shouldTriggerRebuild( abs("routes/page.tsx"), @@ -1180,8 +1170,6 @@ describe("DevServer", () => { } }); - // Skipping explicit busy-builder test: Builder.isBuilding is read-only and non-trivial to override here. - it("checkRebuildQueue rebuilds and restarts when idle", async () => { const devServer = new DevServer({ builder }); devServer.queuedRebuild = { @@ -1194,7 +1182,6 @@ describe("DevServer", () => { "rebuild", () => Promise.resolve({} as esbuild.BuildResult), ); - // Spy on restartApp to verify it was called using _restartSpy = stub( devServer, "restartApp", diff --git a/src/server.test.tsx b/src/server.test.tsx index f716bce..d404810 100644 --- a/src/server.test.tsx +++ b/src/server.test.tsx @@ -96,7 +96,6 @@ describe("createServer", () => { ], }); - // Server route should take priority and return plain text, not HTML const testRes = await server.request("http://localhost/test"); assertEquals(testRes.status, 200); assertEquals(await testRes.text(), "Server Route"); @@ -500,13 +499,11 @@ describe("createServer", () => { ], }); - // Request to a non-existent path under /admin should use admin's error boundary const res = await server.request("http://localhost/admin/non-existent"); assertEquals(res.status, 404); const html = await res.text(); assertStringIncludes(html, ""); assertStringIncludes(html, "
Admin Error Boundary
"); - // Should NOT use root error boundary assertEquals(html.includes("Root Error Boundary"), false); }); @@ -537,13 +534,11 @@ describe("createServer", () => { ], }); - // Request to a non-existent path at root level should use root's error boundary const res = await server.request("http://localhost/non-existent"); assertEquals(res.status, 404); const html = await res.text(); assertStringIncludes(html, ""); assertStringIncludes(html, "
Root Error Boundary
"); - // Should NOT use admin error boundary assertEquals(html.includes("Admin Error Boundary"), false); }); @@ -588,9 +583,6 @@ describe("createServer", () => { ], }); - // Data request for root loader at a non-existent URL under /admin. - // This simulates client-side navigation to a 404 page where React Router - // needs to re-fetch the root route's loader data. const res = await server.request("http://localhost/admin/non-existent", { headers: { "X-Juniper-Route-Id": "/" }, }); @@ -619,7 +611,6 @@ describe("createServer", () => { children: [ { path: "settings", - // No error boundary on settings main: { default: () => , }, @@ -655,7 +646,6 @@ describe("createServer", () => { ], }); - // Request to non-existent path under /admin/settings should bubble up to admin's error boundary const res = await server.request( "http://localhost/admin/settings/non-existent", ); @@ -760,7 +750,6 @@ describe("redirect header preservation", () => { assertEquals(res.status, 200); assertEquals(res.headers.get("X-Juniper"), "redirect"); - // All headers should be preserved except Location, Content-Type, Content-Length assertEquals( res.headers.get("Set-Cookie"), "session=abc123; Path=/; HttpOnly", @@ -768,7 +757,6 @@ describe("redirect header preservation", () => { assertEquals(res.headers.get("X-Custom-Header"), "custom-value"); assertEquals(res.headers.get("Cache-Control"), "no-store"); - // Location should be in the JSON body, not as a header assertEquals(res.headers.get("Location"), null); const data = await res.json(); @@ -784,8 +772,6 @@ describe("redirect header preservation", () => { const server = createServer(import.meta.url, client, { path: "/", main: { - // A full-page redirect to a server-only route the client router - // cannot render (the OAuth2 authorize/callback case). loader: () => Promise.resolve(redirectDocument("/auth/login")), }, }); @@ -796,8 +782,6 @@ describe("redirect header preservation", () => { assertEquals(res.status, 200); assertEquals(res.headers.get("X-Juniper"), "redirect"); - // The internal reload-document header is not leaked as a response header; - // the intent travels in the JSON body so the client can act on it. assertEquals(res.headers.get("X-Remix-Reload-Document"), null); const data = await res.json(); @@ -838,7 +822,6 @@ describe("redirect header preservation", () => { assertEquals(res.status, 200); assertEquals(res.headers.get("X-Juniper"), "redirect"); - // Multiple Set-Cookie headers should be preserved individually const cookies = res.headers.getSetCookie(); assertEquals(cookies.length, 2); assertEquals(cookies[0], "access_token=abc; Path=/; HttpOnly"); @@ -860,9 +843,6 @@ describe("redirect header preservation", () => { const server = createServer(import.meta.url, client, { path: "/", - // A route-group guard that short-circuits BEFORE the React handlers — the - // case handleDataRequest never sees. The outer interceptor must still - // repackage it into the envelope. main: { default: guard }, }); @@ -884,8 +864,6 @@ describe("redirect header preservation", () => { const { Hono } = await import("hono"); const guard = new Hono(); - // A middleware can return react-router's redirectDocument() Response; the - // framework relays the full-page intent the same as a loader/action would. guard.use(() => Promise.resolve(redirectDocument("/auth/login"))); const server = createServer(import.meta.url, client, { @@ -921,7 +899,6 @@ describe("redirect header preservation", () => { main: { default: guard }, }); - // No X-Juniper-Route-Id: a full page load. The browser follows the 302. const res = await server.request("http://localhost/"); assertEquals(res.status, 302); @@ -933,7 +910,6 @@ describe("redirect header preservation", () => { describe("build artifact cache control", () => { it("should set no-cache headers with etag for /build/main.js", async () => { - // Create a minimal public/build directory structure for serving const tempDir = await Deno.makeTempDir(); try { const buildDir = `${tempDir}/public/build`; diff --git a/src/server.tsx b/src/server.tsx index 6360129..4ff6309 100644 --- a/src/server.tsx +++ b/src/server.tsx @@ -87,20 +87,9 @@ export function createServer< appWrapper.use(async (c, next) => { c.set("context", new RouterContextProvider()); await next(); - // Client-side (data-mode) navigations fetch loader/action data with an - // `X-Juniper-Route-Id` header and expect a redirect envelope, not a raw 3xx - // that `fetch` would silently follow. Repackage ANY redirect that surfaces - // on such a request — including one from a Hono handler/middleware that - // short-circuited before the React Router handlers (e.g. an auth guard's - // `c.redirect()`, or a `return redirectDocument()`) — into that envelope, - // honoring `redirectDocument()` (full-page) vs `redirect()` (SPA) uniformly. - // Document requests carry no such header and keep their browser-followed - // 302s. Loader/action redirects are already enveloped (status 200) by - // `handleDataRequest`, so `isRedirectResponse` skips them here. + // Data-mode requests (with `X-Juniper-Route-Id`) expect a redirect envelope, not a raw 3xx that `fetch` would silently follow. if (c.req.header("X-Juniper-Route-Id") && isRedirectResponse(c.res)) { c.res = toRedirectEnvelope(c.res); - // Assigning `c.res` merges the prior response's headers in; drop the ones - // that must not appear on the 200 envelope (they live in the body). c.res.headers.delete("Location"); c.res.headers.delete("X-Remix-Reload-Document"); } diff --git a/src/utils/global-jsdom.ts b/src/utils/global-jsdom.ts index a1e4f19..c22aab8 100644 --- a/src/utils/global-jsdom.ts +++ b/src/utils/global-jsdom.ts @@ -18,7 +18,7 @@ import globalJsdom from "global-jsdom"; import { stubFormData } from "./testing.ts"; -// Parse port from DENO_SERVE_ADDRESS (format: "tcp:0.0.0.0:8100") or default to 8000 +// DENO_SERVE_ADDRESS format: "tcp:0.0.0.0:8100". const address = Deno.env.get("DENO_SERVE_ADDRESS"); let port = 8000; if (address) { @@ -33,5 +33,5 @@ globalJsdom(undefined, { pretendToBeVisual: true, }); -// Stub FormData globally - this is never restored +// Stubbed globally and never restored. stubFormData(); diff --git a/src/utils/react-compiler-plugin.ts b/src/utils/react-compiler-plugin.ts index 66402e6..b139b54 100644 --- a/src/utils/react-compiler-plugin.ts +++ b/src/utils/react-compiler-plugin.ts @@ -43,7 +43,6 @@ export function reactCompilerPlugin( }); build.onLoad({ filter, namespace: "" }, async (args) => { - // Skip remote URLs (JSR/npm packages) - they don't need React Compiler processing const isRemote = args.path.startsWith("http://") || args.path.startsWith("https://"); if (isRemote) { diff --git a/templates/postgres/build.ts b/templates/postgres/build.ts index b92d711..595832d 100644 --- a/templates/postgres/build.ts +++ b/templates/postgres/build.ts @@ -6,9 +6,7 @@ const projectRoot = path.dirname(path.fromFileUrl(import.meta.url)); export const builder = new Builder({ projectRoot, configPath: "./deno.json", - // The Postgres container bind-mounts its data into ./docker/volumes as - // root, which the dev server's file watcher cannot read. Ignoring ./docker - // keeps `Deno.watchFs` from descending into it and crashing on startup. + // ./docker holds root-owned Postgres bind-mounts that crash Deno.watchFs. ignorePaths: ["./docker"], }); diff --git a/templates/postgres/main.test.ts b/templates/postgres/main.test.ts index d10e8bd..992a5de 100644 --- a/templates/postgres/main.test.ts +++ b/templates/postgres/main.test.ts @@ -5,14 +5,6 @@ import { describe, it } from "@std/testing/bdd"; import { server } from "./main.ts"; -// Importing ./main.ts builds the database pool (through the routes), but these -// tests never run a DB-backed loader in this process: the static-file test -// short-circuits before route matching, and the "serves application" test runs -// the loader in a spawned subprocess that owns and tears down its own pool. So -// there's no in-process query to leak, and no closeDb() is needed here. A test -// that calls `server.request("/")` would query in-process and must add -// `afterAll(() => closeDb())` — see services/message.test.ts. - describe("serves static files", () => { it("should serve a static file", async () => { const res = await server.request("http://localhost/favicon.ico"); diff --git a/templates/tanstack/context/query.ts b/templates/tanstack/context/query.ts index 0129bcc..769adf8 100644 --- a/templates/tanstack/context/query.ts +++ b/templates/tanstack/context/query.ts @@ -20,7 +20,6 @@ export function createQueryClient(): QueryClient { }); } -// Register QueryClient context serialization registerContext({ name: "queryClient", context: queryClientContext, diff --git a/templates/tanstack/routes/contacts/[id]/edit.test.tsx b/templates/tanstack/routes/contacts/[id]/edit.test.tsx index e8428b8..a4ae949 100644 --- a/templates/tanstack/routes/contacts/[id]/edit.test.tsx +++ b/templates/tanstack/routes/contacts/[id]/edit.test.tsx @@ -235,7 +235,6 @@ describe("Contact edit route", () => { path: "/contacts/:id/edit", default: contactEditRoute.default, HydrateFallback: contactEditRoute.HydrateFallback, - // Loader that never resolves to keep HydrateFallback visible loader: () => new Promise(() => {}), }], { getContext: getContext(queryClient) }); render( diff --git a/templates/tanstack/routes/contacts/[id]/index.test.tsx b/templates/tanstack/routes/contacts/[id]/index.test.tsx index f5c1cac..caa2777 100644 --- a/templates/tanstack/routes/contacts/[id]/index.test.tsx +++ b/templates/tanstack/routes/contacts/[id]/index.test.tsx @@ -241,7 +241,6 @@ describe("Contact view route", () => { ...contactViewRoute, path: "/contacts/:id", loader() { - // Return a promise that never resolves to keep showing HydrateFallback return new Promise(() => {}); }, HydrateFallback: contactViewRoute.HydrateFallback, diff --git a/templates/tanstack/routes/contacts/index.test.tsx b/templates/tanstack/routes/contacts/index.test.tsx index ca1e16e..db29c8e 100644 --- a/templates/tanstack/routes/contacts/index.test.tsx +++ b/templates/tanstack/routes/contacts/index.test.tsx @@ -177,7 +177,6 @@ describe("Contacts index route", () => { path: "/contacts", default: contactsRoute.default, HydrateFallback: contactsRoute.HydrateFallback, - // Loader that never resolves to keep HydrateFallback visible loader: () => new Promise(() => {}), }], { getContext: getContext(queryClient) }); render( diff --git a/tutorials/blog/routes/api/main.ts b/tutorials/blog/routes/api/main.ts index 97d0c14..3ed872c 100644 --- a/tutorials/blog/routes/api/main.ts +++ b/tutorials/blog/routes/api/main.ts @@ -3,7 +3,6 @@ import { cors } from "hono/cors"; const app = new Hono(); -// Enable CORS for API routes app.use(cors()); export default app; diff --git a/tutorials/blog/routes/api/posts.ts b/tutorials/blog/routes/api/posts.ts index cf9ad3c..70af88e 100644 --- a/tutorials/blog/routes/api/posts.ts +++ b/tutorials/blog/routes/api/posts.ts @@ -3,24 +3,20 @@ import { type NewPost, postService } from "@/services/post.ts"; const app = new Hono(); -// GET /api/posts - List all posts app.get("/", async (c) => { const posts = await postService.list(); return c.json({ data: posts }); }); -// GET /api/posts/:id - Get a single post app.get("/:id", async (c) => { const id = c.req.param("id"); const post = await postService.get(id); return c.json({ data: post }); }); -// POST /api/posts - Create a post app.post("/", async (c) => { const body = await c.req.json(); - // Validate if (!body.title || body.title.length < 3) { return c.json({ error: "Title must be at least 3 characters" }, 400); } @@ -32,12 +28,10 @@ app.post("/", async (c) => { return c.json({ data: post }, 201); }); -// PUT /api/posts/:id - Update a post app.put("/:id", async (c) => { const id = c.req.param("id"); const body = await c.req.json>(); - // Validate if (body.title !== undefined && body.title.length < 3) { return c.json({ error: "Title must be at least 3 characters" }, 400); } @@ -49,7 +43,6 @@ app.put("/:id", async (c) => { return c.json({ data: post }); }); -// DELETE /api/posts/:id - Delete a post app.delete("/:id", async (c) => { const id = c.req.param("id"); await postService.delete(id); diff --git a/tutorials/blog/routes/blog/[id]/edit.ts b/tutorials/blog/routes/blog/[id]/edit.ts index f19e76d..1392abe 100644 --- a/tutorials/blog/routes/blog/[id]/edit.ts +++ b/tutorials/blog/routes/blog/[id]/edit.ts @@ -15,7 +15,6 @@ export async function action({ const title = formData.get("title") as string; const content = formData.get("content") as string; - // Validate const errors: Record = {}; if (!title || title.length < 3) { errors.title = "Title must be at least 3 characters"; diff --git a/tutorials/blog/routes/blog/new.ts b/tutorials/blog/routes/blog/new.ts index 62788d0..780705d 100644 --- a/tutorials/blog/routes/blog/new.ts +++ b/tutorials/blog/routes/blog/new.ts @@ -7,7 +7,6 @@ export async function action({ request }: RouteActionArgs) { const title = formData.get("title") as string; const content = formData.get("content") as string; - // Validate const errors: Record = {}; if (!title || title.length < 3) { errors.title = "Title must be at least 3 characters"; diff --git a/tutorials/blog/services/post.ts b/tutorials/blog/services/post.ts index a232863..de4576e 100644 --- a/tutorials/blog/services/post.ts +++ b/tutorials/blog/services/post.ts @@ -39,7 +39,6 @@ export const postService = { entries.push(entry.value); } - // Sort by creation date, newest first entries.sort( (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), @@ -86,7 +85,7 @@ export const postService = { async delete(id: string): Promise { const db = await getKv(); - await this.get(id); // Ensure it exists + await this.get(id); await db.delete(["posts", id]); }, };