Skip to content

Conversation

@codegen-sh
Copy link
Contributor

@codegen-sh codegen-sh bot commented Aug 8, 2025

This implements STA-2 for Jake Ruesink by persisting todos to localStorage with a minimal, test-safe storage utility and provider hydration.

What:

  • Added storage helpers in packages/utils/src/storage.ts
    • Safe JSON parse/stringify
    • SSR/test environment guards
  • Exported storage utilities via @todo-starter/utils
  • Updated TodoProvider to:
    • Hydrate initial state from localStorage (with Date revival)
    • Persist state on changes (todos/filter)

Integration details:

  • No backend required; uses localStorage under key todo-app/state@v1
  • Test environment automatically disables storage calls to keep tests deterministic

Relevant files:

  • apps/todo-app/app/lib/todo-context.tsx
  • packages/utils/src/storage.ts
  • packages/utils/src/index.ts

Note: Lint/typecheck in the full workspace raises pre-existing warnings/errors unrelated to this change (export-all and some test/button lint checks). The feature compiles in-app, and storage utilities are tree-shakeable and isolated.

Co-authored-by: Jake Ruesink [email protected]


💻 View my work • 👤 Initiated by Jake RuesinkAbout Codegen
⛔ Remove Codegen from PR🚫 Ban action checks

Summary by CodeRabbit

  • New Features

    • Added persistent state and localStorage support to the todo context, enabling todos and filters to be saved and restored across sessions.
    • Introduced new scripts and tasks for running tests in watch mode across the monorepo.
    • Added comprehensive test suites for UI components, utility functions, and storage utilities.
  • Improvements

    • Consolidated and streamlined dependency installation and caching in CI workflows.
    • Enhanced type safety and explicit exports in utility and UI packages.
    • Improved test reliability, isolation, and coverage for core components and context logic.
    • Updated form components to use appropriate UI elements and clarified error handling.
  • Bug Fixes

    • Ensured proper test environment setup and cleanup for UI and utility tests.
    • Fixed SSR/CSR content mismatch by delaying rendering until hydration is complete.
  • Chores

    • Added and updated TypeScript and Vitest configuration files for better test and build management.
    • Refined code formatting and removed unnecessary trailing whitespace across multiple files.
  • Style

    • Refactored code for consistency, including import statements and JSX formatting.

codegen-sh bot and others added 20 commits August 6, 2025 21:49
…d improved caching

- Refactor CI workflow to use composite bun-install action (lines 26-41)
- Refactor PR quality checks workflow to use composite bun-install action (lines 36-51)
- Improve composite action cache key logic by deriving internally and removing guard
- Optimize CI Turbo cache key with branch name and lock file hash for better reuse
- Fine-tune PR quality checks Turbo cache key using lock file hash instead of commit SHA
- Include workflow files in PR quality checks path filter to validate CI changes

This reduces code duplication, centralizes maintenance, and improves cache hit rates
while ensuring workflow changes are properly validated.
- Updated bun.lock to match package.json dependencies
- Resolves frozen lockfile error in CI workflow
- Replace export * with named exports in utils and ui packages
- Fix unused variable in create-todo.tsx by prefixing with underscore
- Replace any type with proper Todo type in create-todo.tsx
- All critical linting errors resolved, only warnings remain
- Remove 'fixed:' from '3xl:fixed:bg-none' -> '3xl:bg-none'
- Remove 'fixed:' from '3xl:fixed:max-w-[...]' -> '3xl:max-w-[...]'
- Resolves Tailwind build error: 'fixed variant does not exist'
- Build now completes successfully
- Add required 'name' prop to FormError components (3 instances)
- Remove invalid 'multiline' and 'rows' props from TextField component
- All components now match type definitions from @lambdacurry/forms package
- todo-app typecheck now passes successfully
- Add individual tsconfig.json files for packages/utils and packages/ui
- Remove apps/**/* from root tsconfig.json include to prevent cross-package type conflicts
- Each package now only typechecks its own files with proper path mappings
- Resolves module resolution errors for ~/components/* imports
- All 3 packages now pass typecheck successfully
- Added comprehensive test suites for utils and ui packages
- Created vitest configs for proper test environment setup
- Fixed FormError components to use explicit name prop for TypeScript compatibility
- Tests cover cn utility function, type definitions, and Button component
- All new tests pass successfully

Co-authored-by: Jake Ruesink <[email protected]>
…848 and resolve conflicts by adopting new Context-based todo store, wrapping app with TodoProvider, and cleaning up removed Zustand store and tests\n\n- Remove legacy Zustand store and tests in favor of Context reducer store from main\n- Fix root to use TodoProvider and import types consistently\n- Align home route with context store hooks\n- Keep README and package updates from both branches where relevant\n\nCo-authored-by: Jake Ruesink <[email protected]>
…mpty function blocks in utils types.test with void expressions\n\nCo-authored-by: Jake Ruesink <[email protected]>
…o tests\n- Clean unused imports and types\n\nCo-authored-by: Jake Ruesink <[email protected]>
… Add vitest.config with jsdom + setupFiles\n- Add setup to import @testing-library/jest-dom/vitest\n- Wire test:ci to use local config\n\nCo-authored-by: Jake Ruesink <[email protected]>
…to satisfy useHref in remix-hook-form\n\nCo-authored-by: Jake Ruesink <[email protected]>
@linear
Copy link

linear bot commented Aug 8, 2025

STA-2 Persist todos in localStorage with minimal utilities (no backend)

Requested by Jake Ruesink

Summary
Persist the todo list and current filter to localStorage so tasks and statuses survive page refreshes without introducing any backend or DB. Keep the scope small and focused on a simple utility layer and a tiny integration inside the existing Todo store.

Why

  • Current state (apps/todo-app/app/lib/todo-context.tsx) is in-memory only, so todos reset on refresh.
  • We want persistence between reloads while staying database-free.

Architecture & Integration Plan

  • Storage utilities (new): packages/utils/src/storage.ts
    • const STORAGE_KEY = 'todo-starter:state:v1'
    • isStorageAvailable(): boolean – guards against SSR and private mode issues
    • saveState(state: { todos: Todo[]; filter: TodoFilter }): void – JSON stringify with date serialization
    • loadState(): { todos: Todo[]; filter: TodoFilter } | null – JSON parse + revive date strings into Date
    • Keep the shape versioned (v1) to enable simple forward-compatible changes later
  • Todo store integration: apps/todo-app/app/lib/todo-context.tsx
    • Add action: HYDRATE with payload { todos: Todo[]; filter: TodoFilter }
    • In TodoProvider, add useEffect on mount (client-only) to read from storage and dispatch({ type: 'HYDRATE', payload }) when present
    • Add useEffect to persist { todos, filter } whenever either changes. Guard with isStorageAvailable() to avoid SSR/window errors
    • Do NOT change the public API of useTodoStore() – callers keep using addTodo, toggleTodo, deleteTodo, updateTodo, setFilter, clearCompleted

Data Flow

  • UI dispatches actions => reducer updates state => useEffect persists { todos, filter }
  • On client mount => loadState() => HYDRATE replaces in-memory state before user interaction

Acceptance Criteria

  • Reloading the page retains todos and their completion status
  • The filter (All/Active/Completed) selection persists across refreshes
  • No SSR errors: no direct localStorage access during server render
  • Dates are Date instances after hydration (not strings)
  • If storage contains existing state, it’s used and not overwritten on first load

Non-Goals (Out of Scope)

  • Cross-tab sync/broadcast channel
  • Storage migrations beyond v1
  • Import/export of todos

Implementation Steps

  1. Create packages/utils/src/storage.ts with STORAGE_KEY, isStorageAvailable, saveState, loadState (typed, robust JSON parse with date revival)
  2. Wire HYDRATE action and two useEffects inside TodoProvider
  3. Manual QA: add todos, toggle, change filter; refresh and verify persistence; also verify first-load without prior storage

Files to Change / Add

  • Add: packages/utils/src/storage.ts
  • Edit: apps/todo-app/app/lib/todo-context.tsx

Notes

  • Keep the sample seeded todos; if storage is empty on first run, persisting the initial sample is acceptable
  • Keep scope tight and avoid adding UI or settings for persistence

@coderabbitai
Copy link

coderabbitai bot commented Aug 8, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

This set of changes introduces robust localStorage persistence for the todo list and filter state in the todo app. It adds a new utility module for safe storage access, modifies the todo context to hydrate and persist state using this utility, and comprehensively tests the persistence logic and storage utilities. No changes are made to the public API of the todo context.

Changes

Cohort / File(s) Change Summary
Storage Utility Implementation & Exports
packages/utils/src/storage.ts, packages/utils/src/index.ts
Adds a new storage utility module for safe localStorage access, with functions for loading, saving, and removing items, and updates exports to include these utilities.
Todo Context Persistence Integration
apps/todo-app/app/lib/todo-context.tsx
Integrates localStorage persistence into the todo context: adds HYDRATE action, hydrates state on mount, persists state on change, and delays rendering until hydration.
Storage Utility Tests
packages/utils/src/__tests__/storage.test.ts
Adds a comprehensive test suite for the storage utility functions, including SSR/test environment handling and type guard validation.
Todo Context Tests
apps/todo-app/app/lib/__tests__/todo-context.test.tsx
Expands and strengthens the todo context test suite to verify persistence, hydration, date revival, and error-free SSR/test behavior.
Types Exports
packages/utils/src/index.ts
Updates exports to use named exports for types, no logic change.

Sequence Diagram(s)

sequenceDiagram
    participant UI
    participant TodoProvider
    participant StorageUtil

    Note over TodoProvider: On mount (client-side)
    TodoProvider->>StorageUtil: loadFromStorage(key, fallback, validate)
    StorageUtil-->>TodoProvider: persistedState or fallback
    TodoProvider->>TodoProvider: dispatch({ type: 'HYDRATE', payload: persistedState })

    Note over TodoProvider: On todos/filter change (after hydration)
    TodoProvider->>StorageUtil: saveToStorage(key, { todos, filter })
    StorageUtil-->>TodoProvider: (void)

    UI->>TodoProvider: dispatch(action)
    TodoProvider->>TodoProvider: update state via reducer
    TodoProvider->>StorageUtil: saveToStorage(key, { todos, filter })
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~35 minutes

Assessment against linked issues

Objective Addressed Explanation
Implement storage utilities for safe localStorage access, versioning, and date revival (STA-2)
Integrate persistence into todo context with HYDRATE action, hydration on mount, and state saving (STA-2)
Ensure dates are revived as Date instances after hydration (STA-2)
No SSR errors; avoid direct localStorage access during server render (STA-2)

Assessment against linked issues: Out-of-scope changes

No out-of-scope functional changes detected. All substantive changes are directly related to the objectives of localStorage persistence for todos and filter state as described in STA-2.

Possibly related issues

  • STA-2: The changes directly implement the persistence feature described in STA-2, including utility creation and context integration.

Possibly related PRs

Note

🔌 MCP (Model Context Protocol) integration is now available in Early Access!

Pro users can now connect to remote MCP servers under the Integrations page to get reviews and chat conversations that understand additional development context.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codegen-bot/persist-todos-localstorage-STA-2-1754688724

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@codegen-sh
Copy link
Contributor Author

codegen-sh bot commented Aug 8, 2025

🔍 Check Suite Status for PR #7

Check Status Commit Time Check Suite Agent trace
quality-checks 🔄 Working ca3ebb3 Just now Run Agent Work

💻 View my work🛑 Stop🚫 Ban all checks

codegen-sh bot added 7 commits August 8, 2025 21:36
…ter context for AddTodo tests\n\nCo-authored-by: Jake Ruesink <[email protected]>
…mpleted test deterministic\n\nCo-authored-by: Jake Ruesink <[email protected]>
…-only usage in tests without data router\n\nCo-authored-by: Jake Ruesink <[email protected]>
… client submit via button click\n\nCo-authored-by: Jake Ruesink <[email protected]>
…olve conflicts; verify bun lint/typecheck/test\n\nCo-authored-by: Jake Ruesink <[email protected]>
…t regex literal in tests to satisfy useTopLevelRegex\n- Replace namespace imports; use named imports where required\n- Update Radix Checkbox imports to named to satisfy noNamespaceImport\n- Remove unused types and fix any types in create-todo\n- Suppress exhaustive-deps where appropriate on storage effect\n- Add explicit button types in tests to satisfy a11y rule\n\nCo-authored-by: Jake Ruesink <[email protected]>
… resolution during typecheck\n\nCo-authored-by: Jake Ruesink <[email protected]>
codegen-sh bot and others added 27 commits August 8, 2025 21:53
… todo completed after toggle)\n\nAlso adjusts CI scripts to pass with empty test suites in packages without tests.\n\nCo-authored-by: Jake Ruesink <[email protected]>
… for 3xl breakpoint\n\nReplaces and with separate utilities.\n\nCo-authored-by: Lambda Curry <[email protected]>
…irectory\n\nThis change ensures that the correct files are sourced from the package, improving compatibility and reducing potential errors in the build process.
…' into codegen/sta-10-testing-add-storage-hydration-tests-utils-todo-context
…eset logic

This update introduces stateful mocks for input fields and enhances the test setup for the AddTodo component. It includes a reset mechanism for the test state before each test, ensuring consistent behavior and accurate assertions. Additionally, the render function is modified to use a router context for better integration testing.
This update introduces a new `test:watch` command in the root and individual package configurations, allowing for easier test development. Additionally, the Vitest configuration file is added to support running tests across multiple projects in the monorepo. The test commands in the `todo-app`, `ui`, and `utils` packages are updated to use `vitest run` with options for handling empty test suites.
…' of github.com:lambda-curry/react-router-starter into codegen/sta-10-testing-add-storage-hydration-tests-utils-todo-context
…e handling and cleanup

This update refines the todo-context tests by introducing a consistent localStorage polyfill for Node environments, ensuring reliable test execution. It also improves the cleanup logic in the tests to maintain a clean state between runs. Additionally, minor formatting adjustments are made for better readability.
This update simplifies the todo context reducer by consolidating conditional expressions for better readability. Additionally, it enhances the test suite by allowing real localStorage usage in tests, ensuring more accurate persistence behavior during testing. Minor formatting adjustments are also made for consistency.
This update refines the loadFromStorage function by consolidating its signatures for improved clarity and consistency. The changes enhance type safety while maintaining backward compatibility with optional validation logic.
…vert-seed-change-and-fix-tests-to-not-depend
…test file exclusions

This change adds type definitions for Vitest and Testing Library to the todo-app's TypeScript configuration. Additionally, it removes the exclusion of test files from the build process, allowing for better integration with testing tools.
…torage-hydration-tests-utils-todo-context
…' of github.com:lambda-curry/react-router-starter into codegen/sta-7-major-add-runtime-validation-to-storage-loader
…handler

This update modifies the AddTodo component to use the form's onSubmit event for handling submissions, removing the previous client-only submission logic. Additionally, the TodoProvider now includes a hydration mechanism to load state from localStorage on the client side, ensuring proper state management and preventing SSR/CSR mismatches.
…orm submission event

This commit adds a comment in the AddTodo component to explain the early form submission handling without a server. It also updates the test cases to use the form's onSubmit event instead of clicking the button, ensuring more accurate simulation of user interactions.
@jaruesink jaruesink marked this pull request as ready for review August 9, 2025 05:04
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🔭 Outside diff range comments (4)
apps/todo-app/app/components/todo-item.tsx (1)

61-67: Accessibility: add labels to icon-only buttons

Icon-only controls need accessible names. Add aria-label or title for SR users.

-<Button size="icon" variant="ghost" type="submit">
+<Button size="icon" variant="ghost" type="submit" aria-label="Save">
   <Check className="h-4 w-4" />
 </Button>
-<Button size="icon" variant="ghost" type="button" onClick={handleCancel}>
+<Button size="icon" variant="ghost" type="button" onClick={handleCancel} aria-label="Cancel">
   <X className="h-4 w-4" />
 </Button>
-<Button size="icon" variant="ghost" onClick={handleEdit} className="h-8 w-8">
+<Button size="icon" variant="ghost" onClick={handleEdit} className="h-8 w-8" aria-label="Edit">
   <Edit2 className="h-4 w-4" />
 </Button>
-<Button
+<Button
   size="icon"
   variant="ghost"
   onClick={() => onDelete(todo.id)}
-  className="h-8 w-8 text-destructive hover:text-destructive"
+  className="h-8 w-8 text-destructive hover:text-destructive"
+  aria-label="Delete"
 >

Also applies to: 76-86

apps/todo-app/app/routes/create-todo.tsx (1)

110-115: Fix: Side-effect during render; move navigation to useEffect

Scheduling setTimeout in render can fire multiple times and is an anti-pattern. Use useEffect with cleanup.

-  // Handle successful submission
-  if (fetcher.data?.success) {
-    setTimeout(() => {
-      navigate('/');
-    }, 2000);
-  }
+  // Handle successful submission
+  useEffect(() => {
+    if (!fetcher.data?.success) return;
+    const tid = setTimeout(() => navigate('/'), 2000);
+    return () => clearTimeout(tid);
+  }, [fetcher.data?.success, navigate]);

Also add:

+ import { useEffect } from 'react';
apps/todo-app/app/lib/__tests__/todo-context.test.tsx (1)

8-12: Avoid wholesale override of the global crypto object

Replacing the entire crypto implementation risks breaking APIs other tests might rely on (e.g. subtle). Override just the needed method instead and restore it afterwards.

-Object.defineProperty(global, 'crypto', {
-  value: { randomUUID: () => 'test-uuid' }
-});
+const originalUUID = global.crypto?.randomUUID;
+Object.defineProperty(global.crypto, 'randomUUID', {
+  value: () => 'test-uuid',
+  configurable: true
+});
+// …in afterAll
+if (originalUUID) Object.defineProperty(global.crypto, 'randomUUID', { value: originalUUID });
apps/todo-app/app/lib/todo-context.tsx (1)

56-66: Guard against adding empty todos

ADD_TODO trims input but still creates a todo even when the result is an empty string. This produces blank entries and wastes a UUID.

 case 'ADD_TODO': {
-  const newTodo: Todo = {
+  const text = action.payload.trim();
+  if (!text) return state; // ignore empty submissions
+  const newTodo: Todo = {
-    text: action.payload.trim(),
+    text,
🧹 Nitpick comments (25)
packages/utils/tsconfig.json (1)

11-17: Avoid excluding tests from the main tsconfig (or split configs)

Excluding tests here can hinder IDE type-checking and Vitest TS transforms if this tsconfig is picked up. Prefer:

  • Keep tests included in tsconfig.json, and
  • Create a tsconfig.build.json that excludes tests for build-only tooling.

Option A (simple): include tests by removing the patterns:

   "exclude": [
     "node_modules",
     "dist",
     "build",
-    "**/*.test.ts",
-    "**/*.test.tsx"
   ]

Option B (recommended): introduce tsconfig.build.json with the current exclude list and point build tooling to it, while leaving tsconfig.json test-friendly. I can draft that if useful.

apps/todo-app/tsconfig.json (1)

25-30: Exclude coverage folder as well (optional).

If you use Vitest coverage, consider excluding the coverage directory to keep TS fast and clean.

   "exclude": [
     "node_modules",
     "build",
-    "dist"
+    "dist",
+    "coverage"
   ]
packages/ui/tsconfig.json (1)

11-17: Exclude patterns are fine; consider test type-check strategy

Excluding tests from this tsconfig is reasonable for package builds. If you want tests type-checked, consider a separate tsconfig (e.g., tsconfig.test.json) or rely on Vitest’s TS config to catch type errors in tests.

packages/utils/src/cn.test.ts (1)

1-38: Solid coverage; consider a few more edge cases

Optional additions to harden behavior:

  • Object syntax: cn('a', { b: true, c: false }) => 'a b'
  • Nested arrays: cn(['a', ['b', false && 'x']], 'c') => 'a b c'
  • Complex Tailwind merges: cn('mt-2 mt-4', 'px-2 px-3', 'hover:text-red-500 text-blue-500') => 'mt-4 px-3 text-blue-500 hover:text-red-500' (verify expected order per tw-merge rules)
packages/utils/package.json (1)

19-22: Test scripts look good; consider a strict CI mode

Allowing CI to pass with no tests can mask misconfigurations. Consider adding a strict CI script that fails when no tests are found.

Suggested addition:

   "scripts": {
     "lint": "biome lint .",
     "format": "biome format --write .",
     "typecheck": "tsc --noEmit",
     "test": "vitest run --passWithNoTests",
     "test:watch": "vitest",
-    "test:ci": "vitest run --passWithNoTests"
+    "test:ci": "vitest run --passWithNoTests",
+    "test:strict": "vitest run"
   },
.github/workflows/ci.yml (1)

33-37: Improve Turbo cache hit rate by removing commit SHA from the cache key

Including github.sha makes each cache key unique per commit, reducing exact hits. Prefer a key based on branch and lockfile hash; keep restore-keys as-is.

-          key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lockb') }}-${{ github.sha }}
+          key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lockb') }}
           restore-keys: |
             ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lockb') }}-
             ${{ runner.os }}-turbo-${{ github.ref_name }}-
             ${{ runner.os }}-turbo-
packages/utils/src/types.test.ts (2)

21-27: Strengthen type-only checks with compile-time assertions

Runtime checks can’t catch invalid union members. Add compile-time guards via expectTypeOf and ts-expect-error.

-import { describe, it, expect } from 'vitest';
+import { describe, it, expect, expectTypeOf } from 'vitest';
@@
   it('should accept valid TodoFilter values', () => {
     const filters: TodoFilter[] = ['all', 'active', 'completed'];
 
     filters.forEach(filter => {
       expect(['all', 'active', 'completed']).toContain(filter);
     });
+
+    // Compile-time negative test: invalid value must not assign
+    // @ts-expect-error - 'invalid' is not a TodoFilter
+    const badFilter: TodoFilter = 'invalid';
+
+    // Compile-time shape test
+    expectTypeOf<TodoFilter>().toEqualTypeOf<'all' | 'active' | 'completed'>();
   });

31-40: Prefer satisfies for structural conformance without changing the variable’s type

Using satisfies checks the shape while keeping the inferred types intact and avoids accidental widening.

-    const mockStore: TodoStore = {
+    const mockStore = {
       todos: [],
       filter: 'all',
       addTodo: (_text: string) => { return; },
       toggleTodo: (_id: string) => { return; },
       deleteTodo: (_id: string) => { return; },
       updateTodo: (_id: string, _text: string) => { return; },
       setFilter: (_filter: TodoFilter) => { return; },
       clearCompleted: () => { return; }
-    };
+    } satisfies TodoStore;
apps/todo-app/test/setup.ts (1)

8-10: Restore mocks after each test to improve isolation

Add vi.restoreAllMocks() to ensure mocked timers/spies don’t leak across tests.

-import { afterEach } from 'vitest';
+import { afterEach, vi } from 'vitest';
@@
 afterEach(() => {
   cleanup();
+  vi.restoreAllMocks();
 });
packages/ui/src/components/ui/button.test.tsx (2)

12-17: Consider asserting class application for variant (more than existence)

toBeInTheDocument is fine; add a class assertion to validate styling contract.

   const button = screen.getByRole('button', { name: 'Delete' });
   expect(button).toBeInTheDocument();
-  expect(button).toHaveClass('bg-destructive');
+  expect(button).toHaveClass('bg-destructive');
+  expect(button).toHaveClass('text-destructive-foreground');

32-41: Assert classes propagate when using asChild

Ensure the Slot-based asChild path applies the button classes to the child element.

   const link = screen.getByRole('link', { name: 'Link button' });
   expect(link).toBeInTheDocument();
   expect(link).toHaveAttribute('href', '/test');
+  // Verify styling classes are applied via Slot
+  expect(link.className).toContain('bg-primary');
+  expect(link.className).toContain('text-primary-foreground');
apps/todo-app/app/components/todo-item.tsx (1)

53-54: Checkbox a11y: provide an accessible name

Consider an aria-label or associate a visible label to convey purpose, e.g., “Mark todo complete”.

-<Checkbox checked={todo.completed} onCheckedChange={() => onToggle(todo.id)} className="flex-shrink-0" />
+<Checkbox
+  checked={todo.completed}
+  onCheckedChange={() => onToggle(todo.id)}
+  className="flex-shrink-0"
+  aria-label="Toggle completed"
/>
packages/ui/src/components/ui/button.tsx (1)

38-43: Optional: improve polymorphic typing for asChild usage

When asChild is true, the component renders non-button elements, but ButtonProps remain button-centric. If you want stricter types, consider a polymorphic pattern so refs/props align with the rendered element. If you’re following the standard shadcn pattern, this is fine to keep.

Example direction (not a drop-in diff):

import type { ElementRef, ComponentPropsWithoutRef } from 'react';
// type PolymorphicButtonProps<T extends ElementType = 'button'> = { asChild?: boolean } & ComponentPropsWithoutRef<T> & VariantProps<typeof buttonVariants>;
// const Button = forwardRef<ElementRef<T>, PolymorphicButtonProps<T>>(...)
apps/todo-app/app/routes/create-todo.tsx (1)

64-64: Nit: Consider logging the caught error in dev

Renaming to _error avoids unused var warnings. Optionally log in non-production to aid debugging.

-} catch (_error) {
+} catch (_error) {
+  if (process.env.NODE_ENV !== 'production') {
+    // eslint-disable-next-line no-console
+    console.error('create-todo action failed:', _error);
+  }
packages/utils/src/__tests__/storage.test.ts (2)

52-56: Nit: Rename test to reflect test-env guard

Name suggests SSR, but this specifically asserts test-env short-circuiting. Consider renaming to "test env disables storage".


97-111: Missing: Date revival behavior

Persistence layer claims date revival on hydration. Add a test storing an ISO string and assert instanceof Date after loadFromStorage.

Example:

it('revives ISO date strings to Date instances', () => {
  process.env.NODE_ENV = 'development';
  ensureWindowWithLocalStorage();
  const value = { createdAt: new Date().toISOString() };
  window.localStorage.setItem(KEY, JSON.stringify(value));
  const loaded = loadFromStorage(KEY, { createdAt: '' });
  expect(loaded).toEqual({ createdAt: expect.any(Date) });
  expect(loaded.createdAt instanceof Date).toBe(true);
});
packages/ui/src/components/ui/checkbox.tsx (1)

23-23: Preserve Radix displayName for easier debugging

Hard-coding Checkbox.displayName = 'Checkbox' loses the more informative name provided by CheckboxRoot.displayName, which can help identify the underlying Radix primitive in React DevTools. Keeping the original value costs nothing and aids diagnostics.

-Checkbox.displayName = 'Checkbox';
+Checkbox.displayName = CheckboxRoot.displayName ?? 'Checkbox';
apps/todo-app/app/lib/__tests__/todo-context.test.tsx (1)

88-91: Single-source the storage key constant

STORAGE_KEY is declared here and again inside a later test block. Duplicating literals makes refactors easy to miss—prefer one shared constant.

apps/todo-app/app/components/__tests__/add-todo.test.tsx (7)

22-36: Make TextField mock more future-proof by forwarding props and loosening required fields

Avoid over-constraining the mock. Forward unknown props and make non-essential props optional to better mirror the real component and reduce future breakage.

-interface TextFieldProps {
-  name: string;
-  placeholder: string;
-  className: string;
-}
+interface TextFieldProps {
+  name: string;
+  placeholder?: string;
+  className?: string;
+  [key: string]: unknown;
+}

-  TextField: ({ name, placeholder, className }: TextFieldProps) => (
+  TextField: ({ name, placeholder, className, ...rest }: TextFieldProps) => (
     <input
       name={name}
       placeholder={placeholder}
       className={className}
       type="text"
       value={testInputValue}
       onChange={e => {
         testInputValue = e.target.value;
       }}
+      {...rest}
     />
   ),

38-50: Align Button mock prop types with real component; default type to 'button'

Make onClick optional and default the native button type to avoid accidental submits in other tests.

-interface ButtonProps {
-  children: ReactNode;
-  onClick: () => void;
-  type: 'button' | 'submit' | 'reset';
-}
+interface ButtonProps {
+  children?: ReactNode;
+  onClick?: () => void;
+  type?: 'button' | 'submit' | 'reset';
+}

-  Button: ({ children, onClick, type }: ButtonProps) => (
-    <button type={type} onClick={onClick}>
+  Button: ({ children, onClick, type }: ButtonProps) => (
+    <button type={type ?? 'button'} onClick={onClick}>
       {children}
     </button>
   )

69-76: reset() comment vs behavior mismatch; dispatch an input event or drop the comment

Currently no custom event is dispatched. Either remove the comment or dispatch an event to better reflect a UI update.

-        reset: vi.fn(() => {
+        reset: vi.fn(() => {
           testInputValue = '';
-          // Force re-render by dispatching a custom event
           const inputs = document.querySelectorAll('input[name="text"]');
           inputs.forEach(input => {
             (input as HTMLInputElement).value = '';
+            // Notify listeners that value changed
+            input.dispatchEvent(new Event('input', { bubbles: true }));
           });
         }),

113-116: Consider extracting renderWithRouter to a shared test util

This helper will likely be reused. Moving it to a central test utils module reduces duplication and standardizes router-wrapping across tests.

Happy to draft a small test-utils.tsx with this helper and update imports.


143-145: Prefer user-event for interactions over fireEvent

user-event simulates user behavior more closely (e.g., typing and submitting), improving test fidelity.

Example:

import userEvent from '@testing-library/user-event';

await userEvent.type(input, 'New todo');
await userEvent.click(button); // relies on type="submit" to submit the form

Also applies to: 157-159, 169-170, 183-184


139-147: Add a call count assertion to guard against double-submits

Tighten expectations to prevent regressions that might trigger multiple submissions.

 expect(mockOnAdd).toHaveBeenCalledWith('New todo');
+expect(mockOnAdd).toHaveBeenCalledTimes(1);

167-170: Add a whitespace-only negative case

You cover empty string; add a whitespace-only case to protect trimming behavior from regressions.

   it('does not call onAdd with empty text', () => {
     const mockOnAdd = vi.fn();
     renderWithRouter(<AddTodo onAdd={mockOnAdd} />);

     const button = screen.getByRole('button', { name: ADD_REGEX });
     const form = button.closest('form') as HTMLFormElement;
     fireEvent.submit(form);

     expect(mockOnAdd).not.toHaveBeenCalled();
   });
+
+  it('does not call onAdd with whitespace-only text', () => {
+    const mockOnAdd = vi.fn();
+    renderWithRouter(<AddTodo onAdd={mockOnAdd} />);
+    const input = screen.getByPlaceholderText('Add a new todo...');
+    const button = screen.getByRole('button', { name: ADD_REGEX });
+    const form = button.closest('form') as HTMLFormElement;
+    fireEvent.change(input, { target: { value: '   ' } });
+    fireEvent.submit(form);
+    expect(mockOnAdd).not.toHaveBeenCalled();
+  });
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ab50e9f and 79e7e20.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (45)
  • .github/composite/bun-install/action.yml (1 hunks)
  • .github/workflows/ci.yml (1 hunks)
  • .github/workflows/pr-quality-checks.yml (2 hunks)
  • apps/todo-app/app/components/__tests__/add-todo.test.tsx (1 hunks)
  • apps/todo-app/app/components/add-todo.tsx (2 hunks)
  • apps/todo-app/app/components/todo-filters.tsx (1 hunks)
  • apps/todo-app/app/components/todo-item.tsx (4 hunks)
  • apps/todo-app/app/globals.css (1 hunks)
  • apps/todo-app/app/lib/__tests__/todo-context.test.tsx (4 hunks)
  • apps/todo-app/app/lib/todo-context.tsx (7 hunks)
  • apps/todo-app/app/root.tsx (1 hunks)
  • apps/todo-app/app/routes/create-todo.tsx (4 hunks)
  • apps/todo-app/app/routes/home.tsx (3 hunks)
  • apps/todo-app/package.json (1 hunks)
  • apps/todo-app/react-router.config.ts (0 hunks)
  • apps/todo-app/test/setup.ts (1 hunks)
  • apps/todo-app/tsconfig.json (2 hunks)
  • apps/todo-app/vite.config.ts (0 hunks)
  • apps/todo-app/vitest.config.ts (0 hunks)
  • biome.json (1 hunks)
  • package.json (1 hunks)
  • packages/ui/package.json (1 hunks)
  • packages/ui/src/components/ui/button.test.tsx (1 hunks)
  • packages/ui/src/components/ui/button.tsx (2 hunks)
  • packages/ui/src/components/ui/card.tsx (1 hunks)
  • packages/ui/src/components/ui/checkbox.tsx (1 hunks)
  • packages/ui/src/components/ui/input.tsx (1 hunks)
  • packages/ui/src/index.ts (1 hunks)
  • packages/ui/src/test/setup.ts (1 hunks)
  • packages/ui/test/setup.ts (1 hunks)
  • packages/ui/tsconfig.json (1 hunks)
  • packages/ui/vitest.config.ts (1 hunks)
  • packages/utils/package.json (2 hunks)
  • packages/utils/src/__tests__/storage.test.ts (1 hunks)
  • packages/utils/src/cn.test.ts (1 hunks)
  • packages/utils/src/cn.ts (0 hunks)
  • packages/utils/src/index.ts (1 hunks)
  • packages/utils/src/storage.ts (1 hunks)
  • packages/utils/src/types.test.ts (1 hunks)
  • packages/utils/src/types.ts (0 hunks)
  • packages/utils/tsconfig.json (1 hunks)
  • packages/utils/vitest.config.ts (1 hunks)
  • tsconfig.json (0 hunks)
  • turbo.json (1 hunks)
  • vitest.config.ts (1 hunks)
💤 Files with no reviewable changes (6)
  • apps/todo-app/react-router.config.ts
  • packages/utils/src/cn.ts
  • apps/todo-app/vitest.config.ts
  • apps/todo-app/vite.config.ts
  • tsconfig.json
  • packages/utils/src/types.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-23T12:46:51.780Z
Learnt from: CR
PR: lambda-curry/forms#0
File: .cursor/rules/form-component-patterns.mdc:0-0
Timestamp: 2025-07-23T12:46:51.780Z
Learning: Applies to packages/components/src/remix-hook-form/**/*.tsx : Use the required imports for Remix Hook Form integration, Zod, and base UI components in form-aware components

Applied to files:

  • apps/todo-app/app/routes/create-todo.tsx
📚 Learning: 2025-07-28T22:12:33.155Z
Learnt from: CR
PR: lambda-curry/email-proxy#0
File: .cursor/rules/forms2.mdc:0-0
Timestamp: 2025-07-28T22:12:33.155Z
Learning: Applies to **/*.tsx : Import form components from `lambdacurry/forms/remix-hook-form`

Applied to files:

  • apps/todo-app/app/routes/create-todo.tsx
🧬 Code Graph Analysis (3)
apps/todo-app/app/routes/home.tsx (3)
apps/todo-app/app/lib/todo-context.tsx (1)
  • useTodoStore (170-176)
packages/ui/src/components/ui/card.tsx (1)
  • CardDescription (44-44)
packages/ui/src/index.ts (1)
  • CardDescription (4-4)
packages/ui/src/components/ui/checkbox.tsx (1)
packages/utils/src/cn.ts (1)
  • cn (4-6)
apps/todo-app/app/components/__tests__/add-todo.test.tsx (2)
packages/ui/src/components/ui/button.tsx (1)
  • ButtonProps (32-36)
apps/todo-app/app/components/add-todo.tsx (1)
  • AddTodo (18-45)
🔇 Additional comments (33)
apps/todo-app/app/root.tsx (1)

3-3: Nice import cleanup

Type-only import for MetaFunction keeps runtime bundle clean and avoids unused type import noise. LGTM.

biome.json (1)

28-28: Formatting-only change acknowledged

Single-line globals array is fine; no behavioral impact. LGTM.

packages/utils/tsconfig.json (1)

4-7: Composite/noEmit flags are safe given no project references

We’ve confirmed that the root tsconfig.json has no "references" entry for packages/utils and no bundler configurations (tsup/rollup/vite) reference this package. With "composite": false and "noEmit": true, tsc won’t emit output nor include this package in TypeScript project references.

• If your build strategy relies on an external bundler (e.g., tsup, Rollup, Vite) to compile and emit artifacts for packages/utils, no changes are needed.
• If you intend to consume this package via TS project references, enable "composite": true and add it under "references" in the root tsconfig.json.

apps/todo-app/tsconfig.json (1)

5-9: Types config looks good for Vitest + JSDOM.

Explicitly including vitest/globals, jest-dom, and vite/client is appropriate for the app.

packages/utils/src/storage.ts (1)

5-19: Test guard and SSR safety look solid.

Disabling storage in tests by default with an opt-in flag and guarding SSR/private mode errors is clean and robust. Good work.

apps/todo-app/app/components/todo-filters.tsx (1)

39-39: Whitespace cleanup only.

No functional changes. LGTM.

packages/ui/src/test/setup.ts (1)

1-3: Correct setup for jest-dom with Vitest.

Importing @testing-library/jest-dom/vitest is the recommended approach and provides matcher types. LGTM.

apps/todo-app/app/globals.css (1)

3-3: Scoping @source to forms/dist is a good optimization

Reduces scan time and avoids picking non-distributed sources. Path looks correct relative to this file in a monorepo layout.

packages/ui/vitest.config.ts (1)

3-10: Solid Vitest config for UI package

jsdom + setupFiles + globals + css: false is a good default for component tests. No issues spotted.

apps/todo-app/package.json (1)

20-21: Switch to vitest run and add watch mode

Good change for CI determinism and local DX.

packages/ui/package.json (1)

19-20: Allow tests to pass when none are present

Using --passWithNoTests prevents noisy CI failures on packages without tests. LGTM.

apps/todo-app/app/routes/home.tsx (3)

19-19: Destructuring formatting change is fine

No behavioral impact. Hook usage remains the same and safe.


48-48: Inline CardDescription is fine

Purely stylistic; no change in semantics.


94-94: Inline empty-state text is fine

No logic change; renders identically.

packages/ui/src/components/ui/input.tsx (1)

1-1: Direct React named imports and typed forwardRef look good

Consistent with modern import style; no functional changes. Component remains properly typed and ref-forwarding is intact.

Also applies to: 4-4, 6-6

.github/workflows/ci.yml (1)

26-27: Composite action correctly pins Bun version and caches dependencies

The bun-install composite already:

  • Pins a default Bun version (1.2.19) via its bun-version input
  • Uses oven-sh/setup-bun@v2 with that pinned version
  • Caches the ~/.bun directory using actions/cache@v4 keyed on bun.lock

No further changes needed here.

vitest.config.ts (1)

7-11: I wasn’t able to find any official mention of defineWorkspace in the Vitest 3.x docs—this feature appears only in the latest v4 release. Since you’re on v3.2.4, switching to defineWorkspace will break your tests. The current glob-based projects syntax is fully supported in 3.x, so you can safely keep it.

Next steps:

  • Keep the existing test.projects array in your root vitest.config.ts.
  • Remove the suggestion to use defineWorkspace, which isn’t available in v3.2.4.

Likely an incorrect or invalid review comment.

packages/utils/src/types.test.ts (1)

1-19: LGTM: solid basic runtime checks for Todo types

The runtime assertions are straightforward and helpful for guarding structural changes.

packages/ui/src/components/ui/button.test.tsx (1)

26-30: Edge case: disabled + asChild behavior

Anchors don’t support disabled. Add a test ensuring disabled isn’t silently forwarded to , and that aria-disabled is handled if needed.

Would you like me to add a test covering:

  • renders aria-disabled on the child
  • onClick is prevented when aria-disabled

I can draft it based on the underlying component API.

Also applies to: 32-41

.github/workflows/pr-quality-checks.yml (1)

37-39: Good consolidation into a composite action

Switching to the composite .github/composite/bun-install simplifies setup and reduces duplication. Ensure the composite handles Bun install, cache, and install deterministically (including CI vs local nuances).

turbo.json (1)

32-33: Nice: include vitest config in Turbo inputs

Adding vitest.config.* to test and test:ci inputs improves cache correctness for test configuration changes.

Also applies to: 43-44

packages/ui/src/components/ui/card.tsx (1)

1-8: Import/style refactor looks good

Direct forwardRef and HTMLAttributes imports are consistent and reduce the React namespace dependency. Other components’ ref and prop types match the rendered elements.

Also applies to: 11-17, 25-31, 32-36, 37-43

apps/todo-app/app/components/todo-item.tsx (1)

32-38: Nice: minimal submit handler with guard

The onValid handler avoids unnecessary updates and neatly exits edit mode. Clean and side-effect minimal.

packages/ui/src/components/ui/button.tsx (1)

1-1: Good import/type refactor

Switching to named imports (forwardRef, ButtonHTMLAttributes) keeps consistency with other UI components and reduces React namespace coupling.

Also applies to: 33-34

apps/todo-app/app/routes/create-todo.tsx (2)

6-6: LGTM: Added Textarea import

Import looks correct alongside other form components.


158-164: LGTM: Switched description to Textarea

Appropriate replacement for multiline input; explicit rows prop is clear.

apps/todo-app/app/components/add-todo.tsx (2)

23-29: LGTM: Local submit handler and reset

Local handling and reset keep the UX snappy without server roundtrip.


42-42: LGTM: Explicit FormError target

Specifying name="_form" clarifies intent and improves consistency.

packages/utils/src/__tests__/storage.test.ts (1)

66-79: LGTM: save/remove round-trip

Covers write/read/remove flow in non-test env.

packages/ui/src/index.ts (1)

1-4: No missing UI exports detected

I scanned all imports of @todo-starter/ui across the repo (notably in apps/todo-app):

  • home.tsx, create-todo.tsx import Card, CardHeader, CardContent, CardTitle, CardDescription
  • todo-filters.tsx imports Button
  • todo-item.tsx imports Checkbox

All of these are explicitly exported in packages/ui/src/index.ts. You can safely switch from wildcard to named exports without breaking consumers.

packages/utils/src/index.ts (1)

1-4: LGTM: Explicit, focused utils exports + storage utilities

Clearer API surface and new storage utilities made available.

apps/todo-app/app/components/__tests__/add-todo.test.tsx (2)

87-105: handleSubmit dual-usage handling looks solid

Covers both direct onSubmit binding and higher-order usage. Good guard on trimmed input.


174-186: Trimming behavior test reads well

Validating trimmed value is correct and complements the empty input test.

with:
path: ~/.bun
key: ${{ inputs.cache-key }}
key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock') }}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Cache key likely wrong: Bun’s lockfile is “bun.lockb”, not “bun.lock”.

Using bun.lock here will cause cache misses. Switch to bun.lockb (or hash both for safety).

-        key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock') }}
+        key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lockb') }}

Optional: hash both

-        key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock') }}
+        key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lockb', '**/bun.lock') }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock') }}
key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lockb') }}
🤖 Prompt for AI Agents
In .github/composite/bun-install/action.yml at line 22, the cache key is using
'bun.lock' which is incorrect for Bun; it should use 'bun.lockb' instead. Update
the hashFiles argument to reference 'bun.lockb' to prevent cache misses.
Optionally, you can hash both 'bun.lock' and 'bun.lockb' files for safety by
including both patterns in the hashFiles function.

Comment on lines +44 to 48
key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lockb') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lockb') }}-
${{ runner.os }}-turbo-${{ github.ref_name }}-
${{ runner.os }}-turbo-
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Confirm bun.lockb presence across the monorepo for cache key effectiveness

Turbo cache keys now hash **/bun.lockb, but the workflow triggers still list bun.lock (Line 12). If the repo uses bun.lockb (Bun’s default), consider updating triggers and verify that lockfiles exist so cache keys are stable.

Run to verify presence and consistency:


🏁 Script executed:

#!/bin/bash
# List Bun lockfiles and their paths
fd -HI --regex 'bun\.lockb?$' | sort

# Show which workflows reference bun.lock vs bun.lockb
rg -n "bun\.lockb?" -A 2 -B 2 .github/workflows

Length of output: 1631


Use bun.lock for Turbo cache keys in workflows

We only have bun.lock in the repo (no bun.lockb), so the current hashFiles('**/bun.lockb') calls will never match anything and the cache key will always fall back to the less specific prefixes. Update both workflows to hash the actual lockfile instead:

• .github/workflows/pr-quality-checks.yml (lines 44–48)
• .github/workflows/ci.yml (lines 33–37)

Suggested diff for each:

- key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lockb') }}-${{ github.sha }}
+ key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lock') }}-${{ github.sha }}

- restore-keys: |
-   ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lockb') }}-
+ restore-keys: |
+   ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lock') }}-
    ${{ runner.os }}-turbo-${{ github.ref_name }}-
    ${{ runner.os }}-turbo-
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lockb') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lockb') }}-
${{ runner.os }}-turbo-${{ github.ref_name }}-
${{ runner.os }}-turbo-
key: ${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lock') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-${{ github.ref_name }}-${{ hashFiles('**/bun.lock') }}-
${{ runner.os }}-turbo-${{ github.ref_name }}-
${{ runner.os }}-turbo-
🤖 Prompt for AI Agents
In .github/workflows/pr-quality-checks.yml around lines 44 to 48, the cache key
uses hashFiles('**/bun.lockb'), but the repo only contains bun.lock, so this
hash will never match. Update the hashFiles argument to '**/bun.lock' in all
occurrences to correctly hash the existing lockfile and improve cache key
specificity.

Comment on lines +9 to 10
text: z.string().min(1, 'Todo text is required').trim()
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix validation order: trim before min

As written, whitespace-only input passes .min(1) then gets trimmed to empty. Apply .trim() first, then .min(1).

-  text: z.string().min(1, 'Todo text is required').trim()
+  text: z.string().trim().min(1, 'Todo text is required')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
text: z.string().min(1, 'Todo text is required').trim()
});
text: z.string().trim().min(1, 'Todo text is required')
});
🤖 Prompt for AI Agents
In apps/todo-app/app/components/add-todo.tsx around lines 9 to 10, the
validation chain applies .min(1) before .trim(), allowing whitespace-only input
to pass validation. To fix this, reorder the validation so that .trim() is
called first on the string, followed by .min(1) to ensure the trimmed input has
at least one character.

Comment on lines 83 to 85
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, text: action.payload.text.trim(), updatedAt: new Date() }
: todo
todo.id === action.payload.id ? { ...todo, text: action.payload.text.trim(), updatedAt: new Date() } : todo
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Likewise skip empty text on update

Updating a todo to an empty string leaves a meaningless item in the list.

-  todo.id === action.payload.id ? { ...todo, text: action.payload.text.trim(), updatedAt: new Date() } : todo
+  todo.id === action.payload.id
+    ? {
+        ...todo,
+        text: action.payload.text.trim() || todo.text,
+        updatedAt: new Date()
+      }
+    : todo
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, text: action.payload.text.trim(), updatedAt: new Date() }
: todo
todo.id === action.payload.id ? { ...todo, text: action.payload.text.trim(), updatedAt: new Date() } : todo
)
todos: state.todos.map(todo =>
todo.id === action.payload.id
? {
...todo,
text: action.payload.text.trim() || todo.text,
updatedAt: new Date()
}
: todo
)
🤖 Prompt for AI Agents
In apps/todo-app/app/lib/todo-context.tsx around lines 83 to 85, the update
logic allows setting a todo's text to an empty string, resulting in meaningless
list items. Modify the update case to first check if the trimmed new text is
non-empty before applying the update; if the text is empty, skip the update and
return the original todo unchanged.

Comment on lines +79 to +90
todo?: {
id: string;
title: string;
description?: string;
priority: 'low' | 'medium' | 'high';
dueDate?: string;
category: string;
isUrgent: boolean;
tags?: string;
completed: boolean;
createdAt: string;
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid inline Todo shape; derive from shared type to prevent drift

Use the shared Todo type to keep this aligned with the app store/types. Adjust createdAt as a string if your route returns ISO timestamps.

-  const fetcher = useFetcher<{ 
+  const fetcher = useFetcher<{ 
     success?: boolean;
     message?: string; 
     errors?: Record<string, { message: string }>;
-    todo?: {
-      id: string;
-      title: string;
-      description?: string;
-      priority: 'low' | 'medium' | 'high';
-      dueDate?: string;
-      category: string;
-      isUrgent: boolean;
-      tags?: string;
-      completed: boolean;
-      createdAt: string;
-    };
+    // If Todo.createdAt is Date in shared types, keep it string here:
+    // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
+    todo?: Omit<Todo, 'createdAt'> & { createdAt: string };
   }>();

Add this import near the top:

import type { Todo } from '@todo-starter/utils';
🤖 Prompt for AI Agents
In apps/todo-app/app/routes/create-todo.tsx around lines 79 to 90, replace the
inline Todo type definition with the shared Todo type imported from
'@todo-starter/utils'. Add the import statement for the Todo type near the top
of the file. Adjust the createdAt property type to string if the route returns
ISO timestamp strings, ensuring consistency with the shared type and preventing
type drift.

Comment on lines +10 to +33
function ensureWindowWithLocalStorage() {
if (typeof window === 'undefined') {
Object.defineProperty(globalThis, 'window', {
value: {} as unknown as Window & typeof globalThis,
configurable: true
});
}
if (!('localStorage' in window)) {
const store = new Map<string, string>();
Object.defineProperty(window, 'localStorage', {
value: {
getItem: (k: string) => store.get(k) ?? null,
setItem: (k: string, v: string) => {
store.set(k, v);
},
removeItem: (k: string) => {
store.delete(k);
}
},
configurable: true
});
}
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add SSR-without-window test coverage

Currently ensureWindowWithLocalStorage always creates window, so we never test SSR path. Add a test where window is undefined to assert fallback behavior in non-test envs.

Example:

it('SSR guard: returns fallback when window is undefined in dev', () => {
  process.env.NODE_ENV = 'development';
  // simulate SSR
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  delete (globalThis as any).window;
  const res = loadFromStorage(KEY, { x: 1 });
  expect(res).toEqual({ x: 1 });
});
🤖 Prompt for AI Agents
In packages/utils/src/__tests__/storage.test.ts around lines 10 to 33, add a new
test case that simulates the SSR environment by deleting globalThis.window and
setting NODE_ENV to development. This test should call loadFromStorage with a
key and default value, then assert that the fallback default value is returned,
ensuring coverage of the SSR path where window is undefined.

@@ -0,0 +1,53 @@
// Minimal localStorage helpers with safe JSON and SSR/test guards

export type StorageLike = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid DOM lib dependency by defining StorageLike inline.

Using Pick<Storage,...> ties the package to the DOM lib. Define a minimal interface to keep utils portable in non-DOM builds.

-export type StorageLike = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
+export interface StorageLike {
+  getItem(key: string): string | null;
+  setItem(key: string, value: string): void;
+  removeItem(key: string): void;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export type StorageLike = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
export interface StorageLike {
getItem(key: string): string | null;
setItem(key: string, value: string): void;
removeItem(key: string): void;
}
🤖 Prompt for AI Agents
In packages/utils/src/storage.ts at line 3, the use of Pick<Storage, 'getItem' |
'setItem' | 'removeItem'> creates a dependency on the DOM library. To fix this,
replace the Pick type with an explicit minimal interface that defines only the
getItem, setItem, and removeItem methods with their appropriate signatures. This
change will remove the DOM lib dependency and keep the utils package portable
for non-DOM environments.

Comment on lines +21 to +33
export function loadFromStorage<T>(key: string, fallback: T, validate?: (value: unknown) => value is T): T {
const storage = getStorage();
if (!storage) return fallback;
try {
const raw = storage.getItem(key);
if (!raw) return fallback;
const parsed = JSON.parse(raw) as unknown;
if (validate && !validate(parsed)) return fallback;
return parsed as T;
} catch {
return fallback;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing date revival in JSON.parse (breaks Date preservation).

PR objectives call for reviving dates on hydration; current parse uses plain JSON.parse and returns strings. This will turn Date fields into strings and can break logic expecting Date instances.

Apply this change within loadFromStorage (replace parse line):

-    const parsed = JSON.parse(raw) as unknown;
+    const parsed = parseJSONWithDates(raw);

Add the helper (outside this range; place above loadFromStorage):

// ISO 8601 detection with leniency for milliseconds
const ISO_DATE_RX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;

function parseJSONWithDates(raw: string): unknown {
  return JSON.parse(raw, (_k, v) => {
    if (typeof v === 'string') {
      // Fast path: strict ISO Zulu; fallback to Date.parse heuristic
      if (ISO_DATE_RX.test(v)) return new Date(v);
      if (v.includes('T')) {
        const t = Date.parse(v);
        if (!Number.isNaN(t)) return new Date(t);
      }
    }
    return v;
  });
}

If you prefer opt-in behavior, we can add an options param (e.g., { reviveDates?: boolean }) instead of always reviving. Happy to provide that refactor.

🤖 Prompt for AI Agents
In packages/utils/src/storage.ts lines 21 to 33, the JSON.parse call in
loadFromStorage does not revive Date objects, causing Date fields to be
deserialized as strings. To fix this, add a helper function parseJSONWithDates
above loadFromStorage that uses JSON.parse with a reviver detecting ISO 8601
date strings and converting them to Date instances. Replace the JSON.parse call
in loadFromStorage with parseJSONWithDates to ensure Date preservation on
hydration.

💡 Verification agent

🧩 Analysis chain

Sanity-check the state key consistency across repo.

PR objectives mention “todo-starter:state:v1” while the summary/code refer to “todo-app/state@v1”. Let’s verify we’re using a single, consistent key.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for state storage keys..."
rg -n --no-heading -S -e 'todo-app/state@v1' -e 'todo-starter:state:v1' -e 'state@v1|state:v1'

Length of output: 588


Ensure PR objectives and summary use the correct storage key
Search across the repo shows that all code (and tests) consistently use the key todo-app/state@v1. The PR objectives and summary still reference todo-starter:state:v1, which does not match.

  • Update the PR description (objectives/summary) to replace todo-starter:state:v1 with todo-app/state@v1.
🤖 Prompt for AI Agents
In packages/utils/src/storage.ts lines 21 to 33, the PR description references
the incorrect storage key 'todo-starter:state:v1'. Update the PR objectives and
summary to use the correct key 'todo-app/state@v1' to match the rest of the
codebase and tests.

Comment on lines +3 to +7
export default defineConfig({
test: {
environment: 'jsdom'
}
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing setup/globals parity with other packages; tests may lack jest-dom matchers

This config omits setupFiles, globals, and css: false. If your tests rely on @testing-library/jest-dom/vitest via ./src/test/setup.ts, those matchers won’t be registered and assertions like toBeInTheDocument will fail. Align with UI’s config:

 export default defineConfig({
   test: {
-    environment: 'jsdom'
+    environment: 'jsdom',
+    setupFiles: ['./src/test/setup.ts'],
+    globals: true,
+    css: false,
   }
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export default defineConfig({
test: {
environment: 'jsdom'
}
});
export default defineConfig({
test: {
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
globals: true,
css: false,
}
});
🤖 Prompt for AI Agents
In packages/utils/vitest.config.ts around lines 3 to 7, the test configuration
is missing setupFiles, globals, and css: false settings, which are necessary to
register jest-dom matchers like toBeInTheDocument. Update the config to include
setupFiles pointing to ./src/test/setup.ts, set globals to true, and add css:
false to ensure parity with other packages and proper test matcher registration.

Comment on lines +35 to +41
"test:watch": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*", "vitest.config.*"],
"outputs": [],
"cache": false,
"persistent": true
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider aligning test:ci dependencies with test/test:watch

test and test:watch depend on ^build, but test:ci does not. If packages rely on built outputs (common in monorepos), missing ^build could cause flaky CI runs. If intentional for speed, ignore. Otherwise:

   "test:ci": {
-    "inputs": ["$TURBO_DEFAULT$", ".env*", "vitest.config.*"],
+    "dependsOn": ["^build"],
+    "inputs": ["$TURBO_DEFAULT$", ".env*", "vitest.config.*"],
     "outputs": []
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"test:watch": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*", "vitest.config.*"],
"outputs": [],
"cache": false,
"persistent": true
},
"test:ci": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*", "vitest.config.*"],
"outputs": []
}
🤖 Prompt for AI Agents
In turbo.json around lines 35 to 41, the "test:ci" task is missing the "^build"
dependency that "test" and "test:watch" have, which can cause flaky CI runs if
built outputs are required. To fix this, add "dependsOn": ["^build"] to the
"test:ci" task configuration to ensure it runs after the build step, aligning it
with the other test tasks.

@jaruesink jaruesink closed this Aug 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants