blacksmith-cli 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/index.js +276 -20
  2. package/dist/index.js.map +1 -1
  3. package/package.json +1 -1
  4. package/src/templates/backend/utils/__init__.py.hbs +0 -0
  5. package/src/templates/backend/utils/models.py.hbs +11 -0
  6. package/src/templates/frontend/package.json.hbs +9 -1
  7. package/src/templates/frontend/src/__tests__/setup.ts.hbs +21 -0
  8. package/src/templates/frontend/src/__tests__/test-utils.tsx.hbs +80 -0
  9. package/src/templates/frontend/src/pages/home/home.tsx.hbs +93 -11
  10. package/src/templates/frontend/tsconfig.app.json.hbs +1 -0
  11. package/src/templates/frontend/vite.config.ts.hbs +8 -0
  12. package/src/templates/resource/api-hooks/index.ts.hbs +2 -0
  13. package/src/templates/resource/{frontend/hooks → api-hooks}/use-{{kebabs}}-query.ts.hbs +10 -2
  14. package/src/templates/resource/{frontend/hooks → api-hooks}/use-{{kebab}}-mutations.ts.hbs +1 -1
  15. package/src/templates/resource/backend/models.py.hbs +2 -3
  16. package/src/templates/resource/frontend/components/{{kebab}}-card.tsx.hbs +1 -1
  17. package/src/templates/resource/frontend/components/{{kebab}}-list.tsx.hbs +1 -1
  18. package/src/templates/resource/frontend/index.ts.hbs +1 -2
  19. package/src/templates/resource/frontend/pages/{{kebabs}}-page.tsx.hbs +1 -1
  20. package/src/templates/resource/frontend/pages/{{kebab}}-detail-page.tsx.hbs +3 -11
  21. package/src/templates/resource/pages/components/{{kebab}}-card.tsx.hbs +1 -1
  22. package/src/templates/resource/pages/components/{{kebab}}-list.tsx.hbs +1 -1
  23. package/src/templates/resource/pages/hooks/index.ts.hbs +9 -0
  24. package/src/templates/resource/pages/index.ts.hbs +1 -2
  25. package/src/templates/resource/pages/{{kebabs}}-page.tsx.hbs +1 -1
  26. package/src/templates/resource/pages/{{kebab}}-detail-page.tsx.hbs +3 -11
  27. package/src/templates/frontend/src/pages/home/components/features-grid.tsx.hbs +0 -88
  28. package/src/templates/frontend/src/pages/home/components/getting-started.tsx.hbs +0 -88
  29. package/src/templates/frontend/src/pages/home/components/hero-section.tsx.hbs +0 -47
  30. package/src/templates/frontend/src/pages/home/components/resources-section.tsx.hbs +0 -34
  31. package/src/templates/resource/pages/hooks/use-{{kebabs}}-query.ts.hbs +0 -35
  32. package/src/templates/resource/pages/hooks/use-{{kebab}}-mutations.ts.hbs +0 -39
package/dist/index.js CHANGED
@@ -311,7 +311,7 @@ pages/<page>/
311
311
  \u251C\u2500\u2500 routes.tsx # RouteObject[] using Path enum
312
312
  \u251C\u2500\u2500 index.ts # Re-exports public API
313
313
  \u251C\u2500\u2500 components/ # Child components
314
- \u2514\u2500\u2500 hooks/ # Data hooks
314
+ \u2514\u2500\u2500 hooks/ # Page-local hooks (UI logic, not API hooks)
315
315
  \`\`\`
316
316
  - See the \`page-structure\` skill for full conventions
317
317
  `;
@@ -1371,9 +1371,10 @@ var reactSkill = {
1371
1371
  - Display user-facing errors using the project's feedback components (Alert, Toast)
1372
1372
 
1373
1373
  ### Testing
1374
- - Run all tests: \`cd frontend && npm test\`
1375
- - Run a specific test: \`cd frontend && npm test -- --grep "test name"\`
1376
- - Test files live alongside the code they test (\`component.test.tsx\`)
1374
+ - See the \`frontend-testing\` skill for full conventions on test placement, utilities, mocking, and what to test
1375
+ - **Every code change must include corresponding tests** \u2014 see the \`frontend-testing\` skill for the complete rules
1376
+ - Tests use \`.spec.tsx\` / \`.spec.ts\` and live in \`__tests__/\` folders co-located with source code
1377
+ - Always use \`renderWithProviders\` from \`@/__tests__/test-utils\` \u2014 never import \`render\` from \`@testing-library/react\` directly
1377
1378
  `;
1378
1379
  }
1379
1380
  };
@@ -1418,12 +1419,12 @@ const { data, errorMessage } = useApiQuery({
1418
1419
  import { postsRetrieveOptions } from '@/api/generated/@tanstack/react-query.gen'
1419
1420
 
1420
1421
  const { data: post, isLoading, errorMessage } = useApiQuery({
1421
- ...postsRetrieveOptions({ path: { id: Number(id) } }),
1422
+ ...postsRetrieveOptions({ path: { id: id! } }),
1422
1423
  })
1423
1424
 
1424
1425
  // Conditional query (skip until id is available)
1425
1426
  const { data } = useApiQuery({
1426
- ...postsRetrieveOptions({ path: { id: Number(id) } }),
1427
+ ...postsRetrieveOptions({ path: { id: id! } }),
1427
1428
  enabled: !!id,
1428
1429
  })
1429
1430
  \`\`\`
@@ -1478,7 +1479,7 @@ const deletePost = useApiMutation({
1478
1479
  })
1479
1480
 
1480
1481
  const handleDelete = async () => {
1481
- await deletePost.mutateAsync({ path: { id: Number(id) } })
1482
+ await deletePost.mutateAsync({ path: { id: id! } })
1482
1483
  navigate('/posts')
1483
1484
  }
1484
1485
  \`\`\`
@@ -1573,7 +1574,7 @@ export function useCreatePost() {
1573
1574
  })
1574
1575
  }
1575
1576
 
1576
- export function useUpdatePost(id: number) {
1577
+ export function useUpdatePost(id: string) {
1577
1578
  return useApiMutation({
1578
1579
  ...postsUpdateMutation(),
1579
1580
  invalidateKeys: [
@@ -1624,7 +1625,7 @@ pages/<page>/
1624
1625
  \u251C\u2500\u2500 routes.tsx # Exports RouteObject[] for this page
1625
1626
  \u251C\u2500\u2500 index.ts # Re-exports public members (routes)
1626
1627
  \u251C\u2500\u2500 components/ # Components private to this page (optional)
1627
- \u2514\u2500\u2500 hooks/ # Hooks private to this page (optional)
1628
+ \u2514\u2500\u2500 hooks/ # Page-local hooks (UI logic, not API hooks)
1628
1629
  \`\`\`
1629
1630
 
1630
1631
  **\`routes.tsx\`** \u2014 defines the route config using the \`Path\` enum:
@@ -1678,8 +1679,7 @@ export const postsRoutes: RouteObject[] = [
1678
1679
  **\`index.ts\`** \u2014 exports routes first:
1679
1680
  \`\`\`ts
1680
1681
  export { postsRoutes } from './routes'
1681
- export { usePosts } from './hooks/use-posts'
1682
- export { useCreatePost, useUpdatePost, useDeletePost } from './hooks/use-post-mutations'
1682
+ export { usePosts, useCreatePost, useUpdatePost, useDeletePost } from '@/api/hooks/posts'
1683
1683
  \`\`\`
1684
1684
 
1685
1685
  ### Route Paths (\`src/router/paths.ts\`)
@@ -3152,7 +3152,7 @@ function useOrdersPage() {
3152
3152
  orders: data?.results ?? [],
3153
3153
  pagination: { ...pagination, total: data?.count ?? 0 },
3154
3154
  search,
3155
- deleteOrder: (id: number) => deleteOrder.mutate({ path: { id } }),
3155
+ deleteOrder: (id: string) => deleteOrder.mutate({ path: { id } }),
3156
3156
  }
3157
3157
  }
3158
3158
  \`\`\`
@@ -3428,6 +3428,244 @@ class OrderService:
3428
3428
  }
3429
3429
  };
3430
3430
 
3431
+ // src/skills/frontend-testing.ts
3432
+ var frontendTestingSkill = {
3433
+ id: "frontend-testing",
3434
+ name: "Frontend Testing Conventions",
3435
+ description: "Test infrastructure, file placement, test utilities, and rules for when and how to write frontend tests.",
3436
+ render(_ctx) {
3437
+ return `## Frontend Testing Conventions
3438
+
3439
+ ### Stack
3440
+ - **Vitest** \u2014 test runner (configured in \`vite.config.ts\`)
3441
+ - **jsdom** \u2014 browser environment
3442
+ - **React Testing Library** \u2014 component rendering and queries
3443
+ - **\`@testing-library/user-event\`** \u2014 user interaction simulation
3444
+ - **\`@testing-library/jest-dom\`** \u2014 DOM assertion matchers (e.g. \`toBeInTheDocument\`)
3445
+
3446
+ ### Running Tests
3447
+ - Run all tests: \`cd frontend && npm test\`
3448
+ - Watch mode: \`cd frontend && npm run test:watch\`
3449
+ - Run a specific file: \`cd frontend && npx vitest run src/pages/home/__tests__/home.spec.tsx\`
3450
+ - Coverage: \`cd frontend && npm run test:coverage\`
3451
+
3452
+ ### File Placement \u2014 Tests Live Next to the Code
3453
+
3454
+ > **RULE: Every test file goes in a \`__tests__/\` folder co-located with the code it tests. Never put tests in a top-level \`tests/\` directory.**
3455
+
3456
+ \`\`\`
3457
+ pages/customers/
3458
+ \u251C\u2500\u2500 customers-page.tsx
3459
+ \u251C\u2500\u2500 customer-detail-page.tsx
3460
+ \u251C\u2500\u2500 __tests__/ # Page integration tests
3461
+ \u2502 \u251C\u2500\u2500 customers-page.spec.tsx
3462
+ \u2502 \u2514\u2500\u2500 customer-detail-page.spec.tsx
3463
+ \u251C\u2500\u2500 components/
3464
+ \u2502 \u251C\u2500\u2500 customer-card.tsx
3465
+ \u2502 \u251C\u2500\u2500 customer-list.tsx
3466
+ \u2502 \u251C\u2500\u2500 customer-form.tsx
3467
+ \u2502 \u2514\u2500\u2500 __tests__/ # Component unit tests
3468
+ \u2502 \u251C\u2500\u2500 customer-card.spec.tsx
3469
+ \u2502 \u251C\u2500\u2500 customer-list.spec.tsx
3470
+ \u2502 \u2514\u2500\u2500 customer-form.spec.tsx
3471
+ \u2514\u2500\u2500 hooks/
3472
+ \u251C\u2500\u2500 use-customers-page.ts
3473
+ \u2514\u2500\u2500 __tests__/ # Hook tests
3474
+ \u2514\u2500\u2500 use-customers-page.spec.ts
3475
+ \`\`\`
3476
+
3477
+ The same pattern applies to \`features/\`, \`shared/\`, and \`router/\`:
3478
+ \`\`\`
3479
+ features/auth/
3480
+ \u251C\u2500\u2500 pages/
3481
+ \u2502 \u251C\u2500\u2500 login-page.tsx
3482
+ \u2502 \u2514\u2500\u2500 __tests__/
3483
+ \u2502 \u2514\u2500\u2500 login-page.spec.tsx
3484
+ \u251C\u2500\u2500 hooks/
3485
+ \u2502 \u2514\u2500\u2500 __tests__/
3486
+ shared/
3487
+ \u251C\u2500\u2500 components/
3488
+ \u2502 \u2514\u2500\u2500 __tests__/
3489
+ \u251C\u2500\u2500 hooks/
3490
+ \u2502 \u2514\u2500\u2500 __tests__/
3491
+ router/
3492
+ \u251C\u2500\u2500 __tests__/
3493
+ \u2502 \u251C\u2500\u2500 paths.spec.ts
3494
+ \u2502 \u2514\u2500\u2500 auth-guard.spec.tsx
3495
+ \`\`\`
3496
+
3497
+ ### Test File Naming
3498
+ - Use \`.spec.tsx\` for component/page tests (JSX)
3499
+ - Use \`.spec.ts\` for pure logic tests (hooks, utilities, no JSX)
3500
+ - Name matches the source file: \`customer-card.tsx\` \u2192 \`customer-card.spec.tsx\`
3501
+
3502
+ ### Always Use \`renderWithProviders\`
3503
+
3504
+ > **RULE: Never import \`render\` from \`@testing-library/react\` directly. Always use \`renderWithProviders\` from \`@/__tests__/test-utils\`.**
3505
+
3506
+ \`renderWithProviders\` wraps components with all app providers (ThemeProvider, QueryClientProvider, MemoryRouter) so tests match the real app environment.
3507
+
3508
+ \`\`\`tsx
3509
+ import { screen } from '@/__tests__/test-utils'
3510
+ import { renderWithProviders } from '@/__tests__/test-utils'
3511
+ import { MyComponent } from '../my-component'
3512
+
3513
+ describe('MyComponent', () => {
3514
+ it('renders correctly', () => {
3515
+ renderWithProviders(<MyComponent />)
3516
+ expect(screen.getByText('Hello')).toBeInTheDocument()
3517
+ })
3518
+ })
3519
+ \`\`\`
3520
+
3521
+ **Options:**
3522
+ \`\`\`tsx
3523
+ renderWithProviders(<MyComponent />, {
3524
+ routerEntries: ['/customers/1'], // Set initial route
3525
+ queryClient: customQueryClient, // Custom query client
3526
+ })
3527
+ \`\`\`
3528
+
3529
+ **User interactions:**
3530
+ \`\`\`tsx
3531
+ const { user } = renderWithProviders(<MyComponent />)
3532
+ await user.click(screen.getByRole('button', { name: 'Submit' }))
3533
+ await user.type(screen.getByLabelText('Email'), 'test@example.com')
3534
+ \`\`\`
3535
+
3536
+ ### When to Write Tests
3537
+
3538
+ > **RULE: Every code change that touches pages, components, hooks, or utilities must include corresponding test updates.**
3539
+
3540
+ | What changed | Test required |
3541
+ |---|---|
3542
+ | New page | Add \`__tests__/<page>.spec.tsx\` with integration test |
3543
+ | New component | Add \`__tests__/<component>.spec.tsx\` |
3544
+ | New hook (with logic) | Add \`__tests__/<hook>.spec.ts\` |
3545
+ | New utility function | Add \`__tests__/<util>.spec.ts\` |
3546
+ | Modified page/component | Update existing test or add new test cases |
3547
+ | Bug fix | Add regression test that would have caught the bug |
3548
+ | Deleted page/component | Delete corresponding test file |
3549
+
3550
+ ### What to Test
3551
+
3552
+ **Page integration tests** \u2014 test the page as a whole:
3553
+ - Renders correct heading/title
3554
+ - Loading state shows skeleton or spinner
3555
+ - Error state shows error message
3556
+ - Data renders correctly (mock the API hooks)
3557
+ - User interactions (navigation, form submission, delete confirmation)
3558
+
3559
+ **Component unit tests** \u2014 test the component in isolation:
3560
+ - Renders with required props
3561
+ - Handles optional props correctly (present vs absent)
3562
+ - Displays correct content based on props
3563
+ - User interactions trigger correct callbacks
3564
+ - Conditional rendering (empty state, loading state)
3565
+
3566
+ **Hook tests** \u2014 test custom hooks with logic:
3567
+ - Returns correct initial state
3568
+ - Transforms data correctly
3569
+ - Side effects fire as expected
3570
+
3571
+ **Utility/pure function tests** \u2014 test input/output:
3572
+ - Happy path
3573
+ - Edge cases (empty input, null, special characters)
3574
+ - Error cases
3575
+
3576
+ ### Mocking Patterns
3577
+
3578
+ **Mock hooks (for page tests):**
3579
+ \`\`\`tsx
3580
+ vi.mock('@/api/hooks/customers')
3581
+ vi.mock('@/features/auth/hooks/use-auth')
3582
+
3583
+ import { useCustomers } from '@/api/hooks/customers'
3584
+
3585
+ beforeEach(() => {
3586
+ vi.mocked(useCustomers).mockReturnValue({
3587
+ data: { customers: mockCustomers, total: 2 },
3588
+ isLoading: false,
3589
+ errorMessage: null,
3590
+ } as any)
3591
+ })
3592
+ \`\`\`
3593
+
3594
+ **Mock external UI libraries (for auth page tests):**
3595
+ \`\`\`tsx
3596
+ vi.mock('@blacksmith-ui/auth', () => ({
3597
+ LoginForm: ({ onSubmit, error, loading }: any) => (
3598
+ <form onSubmit={(e: any) => { e.preventDefault(); onSubmit({ email: 'test@test.com', password: 'pass' }) }}>
3599
+ {error && <div data-testid="error">{error.message}</div>}
3600
+ <button type="submit">Sign In</button>
3601
+ </form>
3602
+ ),
3603
+ }))
3604
+ \`\`\`
3605
+
3606
+ **Mock react-router-dom hooks (for detail pages):**
3607
+ \`\`\`tsx
3608
+ const mockNavigate = vi.fn()
3609
+ vi.mock('react-router-dom', async () => {
3610
+ const actual = await vi.importActual('react-router-dom')
3611
+ return { ...actual, useParams: () => ({ id: '1' }), useNavigate: () => mockNavigate }
3612
+ })
3613
+ \`\`\`
3614
+
3615
+ ### Test Structure
3616
+ \`\`\`tsx
3617
+ import { screen, waitFor } from '@/__tests__/test-utils'
3618
+ import { renderWithProviders } from '@/__tests__/test-utils'
3619
+
3620
+ // Mocks at the top, before imports of modules that use them
3621
+ vi.mock('@/api/hooks/customers')
3622
+
3623
+ import { useCustomers } from '@/api/hooks/customers'
3624
+ import CustomersPage from '../customers-page'
3625
+
3626
+ const mockCustomers = [
3627
+ { id: '1', title: 'Acme Corp', created_at: '2024-01-15T10:00:00Z' },
3628
+ ]
3629
+
3630
+ describe('CustomersPage', () => {
3631
+ beforeEach(() => {
3632
+ vi.mocked(useCustomers).mockReturnValue({ ... } as any)
3633
+ })
3634
+
3635
+ it('renders page heading', () => {
3636
+ renderWithProviders(<CustomersPage />)
3637
+ expect(screen.getByText('Customers')).toBeInTheDocument()
3638
+ })
3639
+
3640
+ it('shows error message when API fails', () => {
3641
+ vi.mocked(useCustomers).mockReturnValue({
3642
+ data: undefined,
3643
+ isLoading: false,
3644
+ errorMessage: 'Failed to load',
3645
+ } as any)
3646
+
3647
+ renderWithProviders(<CustomersPage />)
3648
+ expect(screen.getByText('Failed to load')).toBeInTheDocument()
3649
+ })
3650
+ })
3651
+ \`\`\`
3652
+
3653
+ ### Key Rules
3654
+
3655
+ 1. **Tests live next to code** \u2014 \`__tests__/\` folder alongside the source, not in a separate top-level directory
3656
+ 2. **Always use \`renderWithProviders\`** \u2014 never import render from \`@testing-library/react\` directly
3657
+ 3. **Every page gets an integration test** \u2014 at minimum: renders heading, handles loading, handles errors
3658
+ 4. **Every component gets a unit test** \u2014 at minimum: renders with required props, handles optional props
3659
+ 5. **Mock at the hook level** \u2014 mock \`useCustomers\`, not \`fetch\`. Mock \`useAuth\`, not the auth adapter
3660
+ 6. **Test behavior, not implementation** \u2014 query by role, text, or label, not by class names or internal state
3661
+ 7. **No test-only IDs unless necessary** \u2014 prefer \`getByRole\`, \`getByText\`, \`getByLabelText\` over \`getByTestId\`
3662
+ 8. **Keep tests focused** \u2014 each \`it()\` tests one behavior. Don't assert 10 things in one test
3663
+ 9. **Clean up mocks** \u2014 use \`beforeEach\` to reset mock return values so tests don't leak state
3664
+ 10. **Update tests when code changes** \u2014 if you modify a component, update its tests. If you delete a component, delete its tests
3665
+ `;
3666
+ }
3667
+ };
3668
+
3431
3669
  // src/skills/clean-code.ts
3432
3670
  var cleanCodeSkill = {
3433
3671
  id: "clean-code",
@@ -3564,14 +3802,16 @@ var aiGuidelinesSkill = {
3564
3802
 
3565
3803
  ### Checklist Before Finishing a Task
3566
3804
  1. Backend tests pass: \`cd backend && ./venv/bin/python manage.py test\`
3567
- 2. Frontend builds: \`cd frontend && npm run build\`
3568
- 3. API types are in sync: \`blacksmith sync\`
3569
- 4. No lint errors in modified files
3570
- 5. All UI uses \`@blacksmith-ui/react\` components \u2014 no raw \`<div>\` for layout, no raw \`<h1>\`-\`<h6>\` for text
3571
- 6. Pages are modular \u2014 page file is a thin orchestrator, sections are in \`components/\`, logic in \`hooks/\`
3572
- 7. Logic is in hooks \u2014 no \`useApiQuery\`, \`useApiMutation\`, \`useEffect\`, or multi-\`useState\` in component bodies
3573
- 8. No hardcoded route paths \u2014 all paths use the \`Path\` enum from \`@/router/paths\`
3574
- 9. New routes have a corresponding \`Path\` enum entry
3805
+ 2. Frontend tests pass: \`cd frontend && npm test\`
3806
+ 3. Frontend builds: \`cd frontend && npm run build\`
3807
+ 4. API types are in sync: \`blacksmith sync\`
3808
+ 5. No lint errors in modified files
3809
+ 6. All UI uses \`@blacksmith-ui/react\` components \u2014 no raw \`<div>\` for layout, no raw \`<h1>\`-\`<h6>\` for text
3810
+ 7. Pages are modular \u2014 page file is a thin orchestrator, sections are in \`components/\`, logic in \`hooks/\`
3811
+ 8. Logic is in hooks \u2014 no \`useApiQuery\`, \`useApiMutation\`, \`useEffect\`, or multi-\`useState\` in component bodies
3812
+ 9. No hardcoded route paths \u2014 all paths use the \`Path\` enum from \`@/router/paths\`
3813
+ 10. New routes have a corresponding \`Path\` enum entry
3814
+ 11. **Tests are co-located** \u2014 every new or modified page, component, or hook has a corresponding \`.spec.tsx\` / \`.spec.ts\` in a \`__tests__/\` folder next to the source file (see the \`frontend-testing\` skill)
3575
3815
  `;
3576
3816
  }
3577
3817
  };
@@ -3598,6 +3838,7 @@ async function setupAiDev({ projectDir, projectName, includeBlacksmithUiSkill })
3598
3838
  skills.push(uiDesignSkill);
3599
3839
  }
3600
3840
  skills.push(blacksmithCliSkill);
3841
+ skills.push(frontendTestingSkill);
3601
3842
  skills.push(programmingParadigmsSkill);
3602
3843
  skills.push(cleanCodeSkill);
3603
3844
  skills.push(aiGuidelinesSkill);
@@ -4157,6 +4398,20 @@ async function makeResource(name) {
4157
4398
  } catch {
4158
4399
  syncSpinner.warn('Could not sync OpenAPI. Run "blacksmith sync" manually.');
4159
4400
  }
4401
+ const apiHooksDir = path7.join(frontendDir, "src", "api", "hooks", names.kebabs);
4402
+ const apiHooksSpinner = spinner(`Creating API hooks: api/hooks/${names.kebabs}/`);
4403
+ try {
4404
+ renderDirectory(
4405
+ path7.join(templatesDir, "resource", "api-hooks"),
4406
+ apiHooksDir,
4407
+ context
4408
+ );
4409
+ apiHooksSpinner.succeed(`Created frontend/src/api/hooks/${names.kebabs}/`);
4410
+ } catch (error) {
4411
+ apiHooksSpinner.fail("Failed to create API hooks");
4412
+ log.error(error.message);
4413
+ process.exit(1);
4414
+ }
4160
4415
  const frontendSpinner = spinner(`Creating frontend page: pages/${names.kebabs}/`);
4161
4416
  try {
4162
4417
  renderDirectory(
@@ -4286,6 +4541,7 @@ var allSkills = [
4286
4541
  blacksmithUiAuthSkill,
4287
4542
  blacksmithHooksSkill,
4288
4543
  blacksmithCliSkill,
4544
+ frontendTestingSkill,
4289
4545
  cleanCodeSkill,
4290
4546
  aiGuidelinesSkill
4291
4547
  ];