create-aron-app 0.1.6 → 0.1.8
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.
- package/README.md +24 -31
- package/dist/index.js +39 -115
- package/package.json +7 -4
- package/templates/.cursor/rules/backend.mdc +112 -0
- package/templates/.cursor/rules/coding_standards.mdc +145 -0
- package/templates/.cursor/rules/frontend_architecture.mdc +334 -0
- package/templates/.github/workflows/ci.yml +40 -0
- package/templates/apps/api/_generated/api.d.ts +57 -0
- package/templates/apps/api/_generated/api.js +23 -0
- package/templates/apps/api/_generated/dataModel.d.ts +60 -0
- package/templates/apps/api/_generated/server.d.ts +143 -0
- package/templates/apps/api/_generated/server.js +93 -0
- package/templates/apps/api/http.ts +16 -0
- package/templates/apps/web/.env.example +10 -0
- package/templates/apps/web/.react-router/types/+future.ts +9 -0
- package/templates/apps/web/.react-router/types/+routes.ts +76 -0
- package/templates/apps/web/.react-router/types/+server-build.d.ts +18 -0
- package/templates/apps/web/.react-router/types/src/+types/root.ts +59 -0
- package/templates/apps/web/.react-router/types/src/routes/(auth)/+types/layout.ts +62 -0
- package/templates/apps/web/.react-router/types/src/routes/(auth)/sign-in/+types/index.ts +65 -0
- package/templates/apps/web/.react-router/types/src/routes/(dashboard)/(todos)/+types/[id].ts +68 -0
- package/templates/apps/web/.react-router/types/src/routes/(dashboard)/(todos)/+types/index.ts +68 -0
- package/templates/apps/web/.react-router/types/src/routes/(dashboard)/(todos)/+types/layout.ts +65 -0
- package/templates/apps/web/.react-router/types/src/routes/(dashboard)/+types/index.ts +65 -0
- package/templates/apps/web/.react-router/types/src/routes/(dashboard)/+types/layout.ts +62 -0
- package/templates/{react-router → apps/web}/project.json +9 -2
- package/templates/{react-router → apps/web}/react-router.config.ts +1 -1
- package/templates/apps/web/src/app.css +3 -0
- package/templates/{react-router → apps/web}/src/components/error_boundary.tsx +1 -1
- package/templates/apps/web/src/libs/convex_query_client.ts +11 -0
- package/templates/apps/web/src/libs/react_query_client.ts +17 -0
- package/templates/apps/web/src/libs/server/auth.ts +32 -0
- package/templates/apps/web/src/libs/server/protected.ts +17 -0
- package/templates/apps/web/src/providers/api_auth_provider.tsx +26 -0
- package/templates/apps/web/src/providers/global_provider.tsx +28 -0
- package/templates/apps/web/src/providers/navigation_loading_bar_provider.tsx +72 -0
- package/templates/apps/web/src/root.tsx +68 -0
- package/templates/{react-router/src/routes/auth → apps/web/src/routes/(auth)}/layout.tsx +1 -1
- package/templates/apps/web/src/routes/(dashboard)/(todos)/[id].tsx +33 -0
- package/templates/apps/web/src/routes/(dashboard)/(todos)/index.tsx +26 -0
- package/templates/apps/web/src/routes/(dashboard)/(todos)/layout.tsx +9 -0
- package/templates/apps/web/src/routes/(dashboard)/index.tsx +20 -0
- package/templates/apps/web/src/routes/(dashboard)/layout.tsx +20 -0
- package/templates/apps/web/src/routes.ts +14 -0
- package/templates/apps/web/src/surfaces/home/bootstrap.ts +9 -0
- package/templates/apps/web/src/surfaces/home/home.tsx +25 -0
- package/templates/apps/web/src/surfaces/home/install.tsx +17 -0
- package/templates/apps/web/src/surfaces/home/layout.tsx +35 -0
- package/templates/apps/web/src/surfaces/home/main/create.tsx +32 -0
- package/templates/apps/web/src/surfaces/sidebar/install.tsx +19 -0
- package/templates/apps/web/src/surfaces/sidebar/layout.tsx +119 -0
- package/templates/apps/web/src/surfaces/sidebar/nav_main/create.tsx +31 -0
- package/templates/apps/web/src/surfaces/sidebar/nav_main/nav_main.tsx +42 -0
- package/templates/apps/web/src/surfaces/sidebar/sidebar.tsx +18 -0
- package/templates/apps/web/src/surfaces/sidebar/user_menu/create.tsx +26 -0
- package/templates/{react-router/src/layouts/sidebar/sidebar_aside → apps/web/src/surfaces/sidebar/user_menu}/user_menu.tsx +13 -9
- package/templates/apps/web/src/surfaces/todos/all_todos/all_todos.tsx +25 -0
- package/templates/apps/web/src/surfaces/todos/all_todos/all_todos_controller.ts +47 -0
- package/templates/apps/web/src/surfaces/todos/all_todos/bootstrap.ts +21 -0
- package/templates/apps/web/src/surfaces/todos/all_todos/header/create.tsx +21 -0
- package/templates/apps/web/src/surfaces/todos/all_todos/install.tsx +20 -0
- package/templates/apps/web/src/surfaces/todos/all_todos/layout.tsx +35 -0
- package/templates/apps/web/src/surfaces/todos/all_todos/main/create.tsx +47 -0
- package/templates/apps/web/src/surfaces/todos/all_todos/main/main.tsx +68 -0
- package/templates/apps/web/src/surfaces/todos/all_todos/main/new_todo_sheet/create.tsx +54 -0
- package/templates/apps/web/src/surfaces/todos/all_todos/main/new_todo_sheet/new_todo_sheet.tsx +97 -0
- package/templates/apps/web/src/surfaces/todos/all_todos/main/new_todo_sheet/schema.ts +11 -0
- package/templates/apps/web/src/surfaces/todos/single_todo/bootstrap.ts +35 -0
- package/templates/apps/web/src/surfaces/todos/single_todo/header/create.tsx +24 -0
- package/templates/apps/web/src/surfaces/todos/single_todo/header/header.tsx +25 -0
- package/templates/apps/web/src/surfaces/todos/single_todo/install.tsx +27 -0
- package/templates/apps/web/src/surfaces/todos/single_todo/layout.tsx +45 -0
- package/templates/apps/web/src/surfaces/todos/single_todo/main/create.tsx +35 -0
- package/templates/apps/web/src/surfaces/todos/single_todo/main/main.tsx +47 -0
- package/templates/apps/web/src/surfaces/todos/single_todo/single_todo.tsx +27 -0
- package/templates/apps/web/src/surfaces/todos/single_todo/single_todo_controller.ts +16 -0
- package/templates/{react-router → apps/web}/vite.config.ts +27 -3
- package/templates/{_base/biome.json → biome.json} +12 -0
- package/templates/bun.lock +1917 -0
- package/templates/{_base/emails → emails}/project.json +1 -1
- package/templates/package.json +91 -0
- package/templates/{_base/shared → shared}/assets/src/styles/global.css +14 -8
- package/templates/shared/ui/src/base/collapsible.tsx +31 -0
- package/templates/shared/ui/src/base/hover-card.tsx +42 -0
- package/templates/shared/ui/src/base/input-group.tsx +168 -0
- package/templates/shared/ui/src/base/panel.tsx +93 -0
- package/templates/{_base/shared → shared}/ui/src/hooks/use_mobile.tsx +1 -1
- package/templates/{_base/shared → shared}/ui/src/hooks/use_query_params.tsx +6 -7
- package/templates/{_base/shared → shared}/ui/tsconfig.json +1 -1
- package/templates/shared/utils/src/convex.ts +4 -0
- package/templates/{_base/tsconfig.base.json → tsconfig.base.json} +2 -1
- package/templates/_base/.cursor/commands/builder.md +0 -0
- package/templates/_base/.cursor/rules/api_architecture.mdc +0 -268
- package/templates/_base/.cursor/rules/coding_standards.mdc +0 -64
- package/templates/_base/.cursor/rules/convex_rules.mdc +0 -675
- package/templates/_base/.cursor/rules/frontend_rules.mdc +0 -268
- package/templates/_base/.env.convex.example +0 -3
- package/templates/_base/.github/workflows/ci.yml +0 -29
- package/templates/_base/_gitignore +0 -58
- package/templates/_base/package.json +0 -73
- package/templates/_base/shared/utils/src/convex.ts +0 -3
- package/templates/nextjs/.env.example +0 -8
- package/templates/nextjs/index.d.ts +0 -6
- package/templates/nextjs/next-env.d.ts +0 -5
- package/templates/nextjs/next.config.js +0 -22
- package/templates/nextjs/postcss.config.js +0 -17
- package/templates/nextjs/project.json +0 -22
- package/templates/nextjs/src/app/(auth)/layout.tsx +0 -21
- package/templates/nextjs/src/app/(auth)/not-allowed/page.tsx +0 -22
- package/templates/nextjs/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +0 -15
- package/templates/nextjs/src/app/(dashboard)/layout.tsx +0 -27
- package/templates/nextjs/src/app/(dashboard)/page.tsx +0 -5
- package/templates/nextjs/src/app/(dashboard)/todos/[id]/page.tsx +0 -23
- package/templates/nextjs/src/app/(dashboard)/todos/page.tsx +0 -16
- package/templates/nextjs/src/app/app.css +0 -3
- package/templates/nextjs/src/app/layout.tsx +0 -26
- package/templates/nextjs/src/convex.ts +0 -11
- package/templates/nextjs/src/middleware.ts +0 -18
- package/templates/nextjs/src/providers/convex_provider.tsx +0 -44
- package/templates/nextjs/src/surfaces/home_surface.tsx +0 -22
- package/templates/nextjs/src/surfaces/todos/all_todos_surface.tsx +0 -97
- package/templates/nextjs/src/surfaces/todos/create_todo_sheet.tsx +0 -107
- package/templates/nextjs/src/surfaces/todos/single_todo_surface.tsx +0 -90
- package/templates/nextjs/src/ui/sidebar/nav_link.tsx +0 -36
- package/templates/nextjs/src/ui/sidebar/sidebar.tsx +0 -125
- package/templates/nextjs/src/utils/font.ts +0 -9
- package/templates/nextjs/tsconfig.json +0 -42
- package/templates/react-router/.env.example +0 -8
- package/templates/react-router/src/app.css +0 -3
- package/templates/react-router/src/layouts/sidebar/sidebar_aside/sidebar_aside.tsx +0 -76
- package/templates/react-router/src/layouts/sidebar/sidebar_layout.tsx +0 -22
- package/templates/react-router/src/providers/api_auth_provider.tsx +0 -38
- package/templates/react-router/src/root.tsx +0 -37
- package/templates/react-router/src/routes/index.tsx +0 -9
- package/templates/react-router/src/routes/layout.tsx +0 -26
- package/templates/react-router/src/routes/todos/[id].tsx +0 -22
- package/templates/react-router/src/routes/todos/index.tsx +0 -13
- package/templates/react-router/src/routes.ts +0 -12
- package/templates/react-router/src/surfaces/home_surface.tsx +0 -20
- package/templates/react-router/src/surfaces/todos/all_todos_surface.tsx +0 -87
- package/templates/react-router/src/surfaces/todos/create_todo_sheet.tsx +0 -102
- package/templates/react-router/src/surfaces/todos/single_todo_surface.tsx +0 -81
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/SKILL.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-backend-api/SKILL.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-backend-api/scripts/api-specs-context.sh +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-backend-api/scripts/execute-request.sh +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-backend-api/scripts/extract-endpoint-detail.sh +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-backend-api/scripts/extract-tag-endpoints.sh +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-backend-api/scripts/extract-tags.js +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-custom-ui/SKILL.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-custom-ui/core-2/custom-sign-in.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-custom-ui/core-2/custom-sign-up.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-custom-ui/core-3/custom-sign-in.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-custom-ui/core-3/custom-sign-up.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-custom-ui/core-3/show-component.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-nextjs-patterns/SKILL.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-nextjs-patterns/references/api-routes.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-nextjs-patterns/references/caching-auth.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-nextjs-patterns/references/middleware-strategies.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-nextjs-patterns/references/server-actions.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-nextjs-patterns/references/server-vs-client.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/clerk/clerk-webhooks/SKILL.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/SKILL.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/agents/openai.yml +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/assets/shadcn-small.png +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/assets/shadcn.png +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/cli.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/customization.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/evals/evals.json +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/mcp.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/rules/base-vs-radix.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/rules/composition.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/rules/forms.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/rules/icons.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/agents/skills/shadcn/rules/styling.md +0 -0
- /package/templates/{_base/.cursor → .cursor}/commands/pr.md +0 -0
- /package/templates/{_base/.nvmrc → .nvmrc} +0 -0
- /package/templates/{_base/.vscode → .vscode}/settings.json +0 -0
- /package/templates/{_base/apps → apps}/api/auth.config.ts +0 -0
- /package/templates/{_base/apps → apps}/api/functions.ts +0 -0
- /package/templates/{_base/apps → apps}/api/project.json +0 -0
- /package/templates/{_base/apps → apps}/api/schema.ts +0 -0
- /package/templates/{_base/apps → apps}/api/todos/crud.ts +0 -0
- /package/templates/{_base/apps → apps}/api/todos/schema.ts +0 -0
- /package/templates/{_base/apps → apps}/api/todos/types.ts +0 -0
- /package/templates/{_base/apps → apps}/api/tsconfig.json +0 -0
- /package/templates/{_base/apps → apps}/api/types.ts +0 -0
- /package/templates/{react-router → apps/web}/postcss.config.js +0 -0
- /package/templates/{react-router → apps/web}/public/favicon.ico +0 -0
- /package/templates/{react-router/src/routes/auth/sign-in.tsx → apps/web/src/routes/(auth)/sign-in/index.tsx} +0 -0
- /package/templates/{react-router → apps/web}/tsconfig.json +0 -0
- /package/templates/{_base/convex.json → convex.json} +0 -0
- /package/templates/{_base/emails → emails}/tsconfig.json +0 -0
- /package/templates/{_base/emails → emails}/welcome_email.tsx +0 -0
- /package/templates/{_base/nx.json → nx.json} +0 -0
- /package/templates/{_base/scripts → scripts}/sync_convex_env.ts +0 -0
- /package/templates/{_base/shared → shared}/assets/image.d.ts +0 -0
- /package/templates/{_base/shared → shared}/assets/tsconfig.json +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/alert_dialog.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/badge.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/basic_data_table.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/button.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/button_group.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/card.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/checkbox.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/command.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/dialog.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/dropdown_menu.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/form.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/input.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/label.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/popover.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/radio_group.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/resizable.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/scroll_area.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/select.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/separator.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/sheet.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/side_bar.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/skeleton.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/spinner.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/switch.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/table.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/text_area.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/tooltip.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/base/utils.ts +0 -0
- /package/templates/{_base/shared → shared}/ui/src/hooks/use_keyboard_press.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/hooks/use_keyboard_release.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/hooks/use_mouse_click.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/hooks/use_mouse_location.tsx +0 -0
- /package/templates/{_base/shared → shared}/ui/src/hooks/use_outside_click.tsx +0 -0
- /package/templates/{_base/shared → shared}/utils/src/time.ts +0 -0
- /package/templates/{_base/shared → shared}/utils/tsconfig.json +0 -0
- /package/templates/{_base/skills-lock.json → skills-lock.json} +0 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-aron-app",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Scaffold a new Convex +
|
|
3
|
+
"version": "0.1.8",
|
|
4
|
+
"description": "Scaffold a new Convex + React Router + Clerk full-stack project",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -12,8 +12,11 @@
|
|
|
12
12
|
"templates"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
-
"build": "bun build src/index.ts --target=node --outfile=dist/index.js --minify && echo '#!/usr/bin/env node' | cat - dist/index.js > /tmp/create-tmp.js && mv /tmp/create-tmp.js dist/index.js && chmod +x dist/index.js",
|
|
15
|
+
"build": "bun build src/index.ts --target=node --outfile=dist/index.js --minify --external giget && echo '#!/usr/bin/env node' | cat - dist/index.js > /tmp/create-tmp.js && mv /tmp/create-tmp.js dist/index.js && chmod +x dist/index.js",
|
|
16
16
|
"start": "bun run src/index.ts",
|
|
17
|
+
"start:local": "bun run src/index.ts --local",
|
|
18
|
+
"template": "bun run build && bun run start",
|
|
19
|
+
"template:local": "bun run build && bun run start:local",
|
|
17
20
|
"release": "npm version patch && npm publish --access public",
|
|
18
21
|
"prepublishOnly": "bun run build"
|
|
19
22
|
},
|
|
@@ -33,7 +36,7 @@
|
|
|
33
36
|
"create",
|
|
34
37
|
"template",
|
|
35
38
|
"convex",
|
|
36
|
-
"
|
|
39
|
+
"react-router",
|
|
37
40
|
"clerk",
|
|
38
41
|
"nx",
|
|
39
42
|
"monorepo"
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Convex backend in apps/api — ents, zQuery/zMutation, CRUD layout, auth, and platform conventions that apply alongside project wrappers.
|
|
3
|
+
globs: templates/apps/api/**/*.ts,apps/api/**/*.ts
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Backend API (`apps/api`)
|
|
8
|
+
|
|
9
|
+
Convex backend using **convex-ents**, **`zQuery` / `zMutation`** from `@/api/functions`, and **Zod** (`z.*`, `zid()`) for public function args — not raw `v.*` validators on public handlers.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Layout
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
apps/api/
|
|
17
|
+
├── <entity>/
|
|
18
|
+
│ ├── schema.ts # ent definition (table + edges + indexes)
|
|
19
|
+
│ ├── types.ts # Zod schemas + TS types
|
|
20
|
+
│ └── crud.ts # queries/mutations (GET → UPDATE → CREATE → DELETE)
|
|
21
|
+
├── schema.ts # spreads all entity ents
|
|
22
|
+
├── types.ts # shared context types
|
|
23
|
+
├── functions.ts # zQuery / zMutation wrappers
|
|
24
|
+
└── auth.config.ts # JWT providers (required for ctx.auth)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Example entity (`todos/`): `schema.ts`, `types.ts`, `crud.ts`.
|
|
28
|
+
|
|
29
|
+
### New entity checklist
|
|
30
|
+
|
|
31
|
+
1. Add `apps/api/<entity>/schema.ts` → export `<entity>Ents`
|
|
32
|
+
2. Add `types.ts` — Zod input/output shapes
|
|
33
|
+
3. Add `crud.ts` — `zQuery` / `zMutation` handlers
|
|
34
|
+
4. Spread ents in `apps/api/schema.ts` via `defineEntSchema`
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Schema (`schema.ts` per entity)
|
|
39
|
+
|
|
40
|
+
- Export `{ tableName: defineEnt({...}).edge(...).index(...) }`
|
|
41
|
+
- Index names include all indexed fields, e.g. `by_user_id`
|
|
42
|
+
- Prefer **indexes** for lookups; avoid unbounded `filter` scans in production code
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Types (`types.ts`)
|
|
47
|
+
|
|
48
|
+
- Zod schemas and inferred types live here, not in ent schema files
|
|
49
|
+
- Use `zodToConvex` from convex-helpers when bridging Zod → Convex validators
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## CRUD (`crud.ts`)
|
|
54
|
+
|
|
55
|
+
Order functions: **GET → UPDATE → CREATE → DELETE**.
|
|
56
|
+
|
|
57
|
+
- Authenticate with `ctx.auth.getUserIdentity()`; resolve the app user as your schema requires
|
|
58
|
+
- **Never** take `userId` from the client for authorization — derive from auth context
|
|
59
|
+
- Prefer **`identity.tokenIdentifier`** for stable auth-linked lookups (not `subject` alone)
|
|
60
|
+
- Use **`ConvexError`** for business errors; messages stay concise
|
|
61
|
+
- For expected present/missing entity semantics: **`getX` / `get`**, **`uniqueX` / `unique`** — no redundant null checks after `getX`
|
|
62
|
+
|
|
63
|
+
### Ent helpers
|
|
64
|
+
|
|
65
|
+
| API | Use when |
|
|
66
|
+
|-----|----------|
|
|
67
|
+
| `getX(id)` | Row must exist |
|
|
68
|
+
| `get(id)` | Row may be missing |
|
|
69
|
+
| `uniqueX` / `firstX` | Index query must yield exactly one / at least one |
|
|
70
|
+
|
|
71
|
+
### Edge patterns
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
const related = await entity.edge("relatedEntity");
|
|
75
|
+
return { ...entity, related };
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Standard update:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
const { entityId, ...updates } = args;
|
|
82
|
+
const entity = await ctx.table("entities").getX(entityId);
|
|
83
|
+
return entity.patch(updates);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Documentation style
|
|
89
|
+
|
|
90
|
+
- **No** JSDoc on every exported function — names should read clearly
|
|
91
|
+
- **Only** inline comments for non-obvious business rules
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Convex platform notes (template-wide)
|
|
96
|
+
|
|
97
|
+
These apply to any `query`/`mutation`/`action` code; **this repo’s public API should still use `zQuery`/`zMutation`** per above.
|
|
98
|
+
|
|
99
|
+
- Register **internal** functions with `internalQuery` / `internalMutation` / `internalAction` — never expose sensitive ops as public `query`/`mutation`
|
|
100
|
+
- **Actions**: add `"use node"` only in files that need Node builtins; **never** mix `"use node"` with queries/mutations in the same file
|
|
101
|
+
- **Pagination**: use `paginationOptsValidator` and `.paginate()` for large lists — avoid unbounded `.collect()` on big tables
|
|
102
|
+
- **Scheduling**: only schedule **internal** function references
|
|
103
|
+
- **Queries**: do not use `Date.now()` in query logic if it should be reactive/cached predictably
|
|
104
|
+
- **DB**: no `.collect().length` for counts at scale — denormalize or bound reads
|
|
105
|
+
|
|
106
|
+
For HTTP routes in Convex, use `httpRouter` + `httpAction` and register exact paths.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Client imports
|
|
111
|
+
|
|
112
|
+
Frontend code imports **`Doc` / `Id`** only through per-entity **`types.ts` aliases** (e.g. `Todo`, `TodoId`). Do not use `Ent<T>` outside the backend.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Project-wide coding standards covering import ordering, naming conventions, code style, and testing policy. Apply across all TypeScript/TSX files.
|
|
3
|
+
globs: **/*.ts,**/*.tsx
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Coding Standards
|
|
7
|
+
|
|
8
|
+
## Naming Conventions
|
|
9
|
+
|
|
10
|
+
### Functions
|
|
11
|
+
- Use verb + noun: `getEntity`, `updateEntity`, `createEntity`, `deleteEntity`
|
|
12
|
+
- Be specific when needed: `getEntityByEmail`, `updateEntityField`
|
|
13
|
+
- Avoid abbreviations unless widely understood
|
|
14
|
+
- Avoid verbose qualifiers: prefer `updateEntity` over `updateEntityCoreDetails`
|
|
15
|
+
|
|
16
|
+
### Fields
|
|
17
|
+
- `camelCase` for all field names
|
|
18
|
+
- Use `userId` — not `clerkId` or other provider-specific names
|
|
19
|
+
|
|
20
|
+
### Variables
|
|
21
|
+
- Keep names concise and descriptive
|
|
22
|
+
- Use singular for single items (`todo`, `entity`) and plural for collections (`todos`, `entities`)
|
|
23
|
+
- Destructure to extract IDs: `const { entityId, ...updates } = args;`
|
|
24
|
+
|
|
25
|
+
### Zod Schemas
|
|
26
|
+
|
|
27
|
+
The inferred type and the schema constant must share the same PascalCase name. Declare the type **above** the const. This applies to both API and web code:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// Good
|
|
31
|
+
export type CreateConversationInput = z.infer<typeof CreateConversationInput>;
|
|
32
|
+
export const CreateConversationInput = z.object({
|
|
33
|
+
title: z.string().min(1).max(120),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Bad — mismatched names, type below const
|
|
37
|
+
export const createConversationSchema = z.object({ title: z.string() });
|
|
38
|
+
export type CreateConversationInput = z.infer<typeof createConversationSchema>;
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Code Style
|
|
42
|
+
|
|
43
|
+
### Formatting
|
|
44
|
+
- Single blank line between functions
|
|
45
|
+
- No blank lines at the start or end of a function body
|
|
46
|
+
- 2-space indentation
|
|
47
|
+
- Trailing commas in multi-line arrays and objects
|
|
48
|
+
|
|
49
|
+
### Class Spacing
|
|
50
|
+
|
|
51
|
+
Each member of a class (property, accessor, or method) must be separated by a blank line. Decorators go on their own line above the member. Never inline a method body — always expand to a full block:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// Good
|
|
55
|
+
export class FooController {
|
|
56
|
+
@observable.ref
|
|
57
|
+
accessor isOpen = false;
|
|
58
|
+
|
|
59
|
+
@observable.ref
|
|
60
|
+
accessor selectedId: string | undefined = undefined;
|
|
61
|
+
|
|
62
|
+
@computed
|
|
63
|
+
get label() {
|
|
64
|
+
return this.isOpen ? "Close" : "Open";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@action
|
|
68
|
+
open(id: string) {
|
|
69
|
+
this.selectedId = id;
|
|
70
|
+
this.isOpen = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Bad — inlined bodies, missing blank lines, stacked decorator
|
|
75
|
+
export class FooController {
|
|
76
|
+
@observable.ref accessor isOpen = false;
|
|
77
|
+
@observable.ref accessor selectedId: string | undefined = undefined;
|
|
78
|
+
@computed get label() { return this.isOpen ? "Close" : "Open"; }
|
|
79
|
+
@action open(id: string) { this.selectedId = id; this.isOpen = true; }
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### No Expression Nesting
|
|
84
|
+
|
|
85
|
+
Never pass an awaited call or a complex expression directly as an argument. Extract to a named variable first:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// Good
|
|
89
|
+
const user = await getUser(userId);
|
|
90
|
+
await sendWelcomeEmail(user);
|
|
91
|
+
|
|
92
|
+
// Bad
|
|
93
|
+
await sendWelcomeEmail(await getUser(userId));
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Block Statements
|
|
97
|
+
|
|
98
|
+
Always use block bodies (`{ }`). This is enforced by Biome (`useBlockStatements`). No single-line `if`/`throw`, no expression-body arrow callbacks:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// Good
|
|
102
|
+
if (!conversation) {
|
|
103
|
+
throw new Error("Not found");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
onSubmit={() => {
|
|
107
|
+
controller.submit({ id: bootstrap.id });
|
|
108
|
+
}}
|
|
109
|
+
|
|
110
|
+
// Bad
|
|
111
|
+
if (!conversation) throw new Error("Not found");
|
|
112
|
+
|
|
113
|
+
onSubmit={() => controller.submit({ id: bootstrap.id })}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Conditional Logic
|
|
117
|
+
|
|
118
|
+
Prefer concise conditionals — avoid unnecessary intermediate variables:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// Good
|
|
122
|
+
const count = entity.limit ?? 0;
|
|
123
|
+
if (count <= 0) {
|
|
124
|
+
throw new Error("Limit reached");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Bad
|
|
128
|
+
const remaining = entity.limit ?? 0;
|
|
129
|
+
if (!entity.limit || entity.limit <= 0) {
|
|
130
|
+
throw new Error("Limit reached");
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Async / Await
|
|
135
|
+
- Always `await` async operations — never fire-and-forget unless intentional
|
|
136
|
+
- Use `Promise.all()` for independent parallel operations
|
|
137
|
+
|
|
138
|
+
### Validation
|
|
139
|
+
- Validate business rules before any database operation
|
|
140
|
+
- Centralise reusable validation in `@/utils/`
|
|
141
|
+
- Keep CRUD files thin — validation logic belongs in utils
|
|
142
|
+
|
|
143
|
+
## Testing
|
|
144
|
+
|
|
145
|
+
Test files **should not** be created unless explicitly requested. Focus on implementation quality over test coverage.
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Web frontend — surface architecture (MobX + React Router + Convex + Clerk SSR). Layers, routing, data loading, Convex/React Query wiring, and component conventions.
|
|
3
|
+
globs: templates/apps/web/**/*.tsx,templates/apps/web/**/*.ts,apps/web/**/*.tsx,apps/web/**/*.ts
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Frontend architecture — React Router + Convex
|
|
8
|
+
|
|
9
|
+
Audience: engineers working on `apps/web` (or `templates/apps/web` in this repo).
|
|
10
|
+
|
|
11
|
+
This stack uses **surfaces** (vertical slices), **MobX** for UI state, **Convex** for data, and **SSR loaders** with `ConvexHttpClient` + Clerk. An older **v2** ruleset documented a Hono BFF + `api` client; this project does **not** use that pattern — use Convex queries/mutations and the paths below instead.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Philosophy
|
|
16
|
+
|
|
17
|
+
1. **Display components are dumb** — props in, JSX out. No data fetching, no surface state, no controller imports.
|
|
18
|
+
2. **`create.tsx` factories are smart closures** — they close over controller/bootstrap at wiring time and return an `observer()` component. This is the only layer that bridges MobX and React hooks.
|
|
19
|
+
3. **Controllers are the brain** — a MobX class owns cross-section UI state (dialogs, selection). No React imports.
|
|
20
|
+
|
|
21
|
+
**Why explicit wiring instead of React Context?** Context hides the dependency graph. Here, access is traceable: surface → `install` → `create` → display. No surprise re-renders or ambiguous state ownership.
|
|
22
|
+
|
|
23
|
+
**Why not add Zustand?** MobX plus optional `GlobalProvider` already cover reactive UI state; Convex plus TanStack Query cover server state — avoid a third paradigm.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Layer stack
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
routes/.../[page].tsx → server loader + clientLoader; passes loaderData as bootstrap
|
|
31
|
+
bootstrap.ts → ConvexHttpClient query only (SSR); no browser APIs
|
|
32
|
+
[feature].tsx → surface entry: useRef install guard, useMemo(createLayout)
|
|
33
|
+
install.tsx → instantiate controller; dynamic import() of section creates
|
|
34
|
+
layout.tsx → LayoutController + observer shell + skeleton slots
|
|
35
|
+
[feature]_controller.ts → MobX — shared UI state; may call convex.mutation
|
|
36
|
+
[section]_presenter.ts → MobX — section-scoped logic (optional)
|
|
37
|
+
[section]/create.tsx → factory → observer; useQuery + convexQuery + initialData
|
|
38
|
+
[section]/[section].tsx → pure display (`useFormContext` exception for forms)
|
|
39
|
+
ui/ → dumb components scoped to this surface
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Folder and naming
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
src/surfaces/todos/
|
|
48
|
+
all_todos/ # list surface
|
|
49
|
+
single_todo/ # id-scoped surface
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- File names: **snake_case**
|
|
53
|
+
- Factory/install args: `Opts` suffix — `InstallSingleTodoOpts`
|
|
54
|
+
- Component props: `Props` suffix — `MainProps`
|
|
55
|
+
- List surfaces: **`All`** prefix — `AllTodos`
|
|
56
|
+
- Id surfaces: **`Single`** prefix — `SingleTodo`
|
|
57
|
+
- CRUD UI folders: `new_`, `edit_`, `delete_` — never `create_` (conflicts with `create.tsx` pattern)
|
|
58
|
+
- Display component names must **not** repeat the parent path — inside `single_todo/header/`, export `Header`, not `SingleTodoHeader`
|
|
59
|
+
- Imports: **`@/...` only** — no relative imports within features
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Setup — MobX (Stage 3)
|
|
64
|
+
|
|
65
|
+
Requires TypeScript 5+ with `experimentalDecorators: false` and Babel decorators in Vite:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{ "compilerOptions": { "experimentalDecorators": false } }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
react({ babel: { plugins: [["@babel/plugin-proposal-decorators", { version: "2023-05" }]] } })
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- Use `accessor` on `@observable` fields
|
|
76
|
+
- Prefer `@action` methods — mutations after `await` inside an `@action async` are safe without `runInAction`
|
|
77
|
+
- Use `@observable.ref` for component slot fields on layout controllers
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Setup — Convex and React Query
|
|
82
|
+
|
|
83
|
+
**Convex React client** (browser, `expectAuth: true`) and **ConvexQueryClient** live in `@/web/libs/convex_query_client`:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { convex, convexQueryClient } from "@/web/libs/convex_query_client";
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**TanStack Query client** is `@/web/libs/react_query_client` — it connects `convexQueryClient` and sets default `queryFn`. Use this singleton anywhere you need a `QueryClient` on the client.
|
|
90
|
+
|
|
91
|
+
**Presenters and controllers** call mutations outside React via the same `convex` singleton:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { convex } from "@/web/libs/convex_query_client";
|
|
95
|
+
import { api } from "@/api/_generated/api";
|
|
96
|
+
|
|
97
|
+
await convex.mutation(api.todos.crud.updateTodo, args);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Creates** use `useQuery` with `convexQuery` from `@convex-dev/react-query` and **`initialData` from bootstrap** so the first paint matches SSR.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Global state (three tiers)
|
|
105
|
+
|
|
106
|
+
| Tier | Mechanism | When |
|
|
107
|
+
|------|-----------|------|
|
|
108
|
+
| Module `stores/` | Plain module variable | One-shot, non-reactive handoff across navigation |
|
|
109
|
+
| `GlobalProvider` | React context + `useGlobal()` | Reactive state across `<Outlet />` (use sparingly) |
|
|
110
|
+
| MobX controller | `[feature]_controller.ts` | All UI state inside a surface |
|
|
111
|
+
|
|
112
|
+
Module store rules: consume-once helpers, one concern per file, document lifetime at top, never for persistence alone.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Routing and auth
|
|
117
|
+
|
|
118
|
+
**Route config** (`src/routes.ts`) uses route groups `(auth)` and `(dashboard)`. URLs come from `layout`/`prefix`/`route`, not folder names.
|
|
119
|
+
|
|
120
|
+
Example shape (adjust files to match your app):
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { index, layout, prefix, type RouteConfig, route } from "@react-router/dev/routes";
|
|
124
|
+
|
|
125
|
+
export default [
|
|
126
|
+
layout("./routes/(auth)/layout.tsx", [route("/sign-in/*", "./routes/(auth)/sign-in/index.tsx")]),
|
|
127
|
+
layout("./routes/(dashboard)/layout.tsx", [
|
|
128
|
+
index("./routes/(dashboard)/index.tsx"),
|
|
129
|
+
...prefix("todos", [
|
|
130
|
+
layout("./routes/(dashboard)/(todos)/layout.tsx", [
|
|
131
|
+
index("./routes/(dashboard)/(todos)/index.tsx"),
|
|
132
|
+
route(":id", "./routes/(dashboard)/(todos)/[id].tsx"),
|
|
133
|
+
]),
|
|
134
|
+
]),
|
|
135
|
+
]),
|
|
136
|
+
] satisfies RouteConfig;
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Middleware** (`root.tsx`): Clerk + a protected-route middleware enforce auth for non-public paths before loaders run.
|
|
140
|
+
|
|
141
|
+
**`root.tsx`**: providers (`ApiAuthProvider`, `GlobalProvider`, …) + `<Outlet />` — no feature sidebar here.
|
|
142
|
+
|
|
143
|
+
**Dashboard layout**: shell only (e.g. `SidebarProvider`, `Sidebar`, `Outlet`).
|
|
144
|
+
|
|
145
|
+
**Router types**: import `Route` from `./+types/...` adjacent to the route file (or the generated types path your tsconfig uses).
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## SSR data loading (loaders + bootstrap)
|
|
150
|
+
|
|
151
|
+
Flow:
|
|
152
|
+
|
|
153
|
+
1. **Server** — route `loader` builds an authenticated `ConvexHttpClient` and runs bootstrap.
|
|
154
|
+
2. **Client** — `clientLoader` returns `serverLoader()` only — no second fetch.
|
|
155
|
+
3. **Hydration** — `useQuery({ ...convexQuery(...), initialData: bootstrap.* })` so the live subscription takes over.
|
|
156
|
+
|
|
157
|
+
### Auth helper — `src/libs/server/auth.ts`
|
|
158
|
+
|
|
159
|
+
Single place for loader-time Convex HTTP client + redirect if unauthenticated:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { getAuth } from "@clerk/react-router/server";
|
|
163
|
+
import { ConvexHttpClient } from "convex/browser";
|
|
164
|
+
import type { LoaderFunctionArgs } from "react-router";
|
|
165
|
+
import { redirect } from "react-router";
|
|
166
|
+
|
|
167
|
+
export const createConvexClient = async (args: LoaderFunctionArgs): Promise<ConvexHttpClient> => {
|
|
168
|
+
const auth = await getAuth(args);
|
|
169
|
+
if (!auth.isAuthenticated) {
|
|
170
|
+
throw redirect(`/sign-in?redirect_url=${encodeURIComponent(args.request.url)}`);
|
|
171
|
+
}
|
|
172
|
+
const token = await auth.getToken({ template: "convex" });
|
|
173
|
+
const client = new ConvexHttpClient(process.env.VITE_CONVEX_URL!);
|
|
174
|
+
if (token) {
|
|
175
|
+
client.setAuth(token);
|
|
176
|
+
}
|
|
177
|
+
return client;
|
|
178
|
+
};
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Use `getToken({ template: "convex" })` (not the default session token). In server loaders use `process.env.VITE_CONVEX_URL` (not `import.meta.env`).
|
|
182
|
+
|
|
183
|
+
### Bootstrap — `surfaces/.../bootstrap.ts`
|
|
184
|
+
|
|
185
|
+
- Accept `{ client: ConvexHttpClient }` from the route loader
|
|
186
|
+
- Use `client.query(api...)` only — no `ConvexReactClient`, `convexQuery`, or `ensureQueryData` in bootstrap
|
|
187
|
+
- May `throw redirect(...)` for invalid params
|
|
188
|
+
|
|
189
|
+
### Route module
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
export async function loader(args: Route.LoaderArgs) {
|
|
193
|
+
const client = await createConvexClient(args);
|
|
194
|
+
return bootstrapAllTodos({ client });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function clientLoader({ serverLoader }: Route.ClientLoaderArgs) {
|
|
198
|
+
return serverLoader();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export default function Page({ loaderData: bootstrap }: Route.ComponentProps) {
|
|
202
|
+
return <AllTodos bootstrap={bootstrap} />;
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Client `create.tsx`
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
const { data: todos } = useQuery({
|
|
210
|
+
...convexQuery(api.todos.crud.listTodos, {}),
|
|
211
|
+
initialData: bootstrap.todos as never,
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Spread `convexQuery` first, then `initialData`. The `as never` cast is often needed for serialized vs local types.
|
|
216
|
+
|
|
217
|
+
### Anti-patterns
|
|
218
|
+
|
|
219
|
+
| Wrong | Right |
|
|
220
|
+
|-------|--------|
|
|
221
|
+
| `clientLoader` calling `ensureQueryData(convexQuery(...))` for SSR | Server `loader` + `clientLoader` → `serverLoader()` |
|
|
222
|
+
| Building `ConvexHttpClient` inside bootstrap | Receive `client` from `createConvexClient` |
|
|
223
|
+
| Duplicating Clerk redirects in every bootstrap | Centralize in `createConvexClient` / middleware |
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Shell vs pages
|
|
228
|
+
|
|
229
|
+
- **Layout-mounted** (e.g. sidebar): `useEffect(() => installSidebar(...), [])` once.
|
|
230
|
+
- **Route-mounted** pages: synchronous **`useRef` install guard** on first render (avoids empty flash; StrictMode-safe).
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Surface entry (`[feature].tsx`)
|
|
235
|
+
|
|
236
|
+
- Receives **`bootstrap` from the route** — never fetch the primary payload inside the surface
|
|
237
|
+
- `useMemo(() => createLayout(), [])` once
|
|
238
|
+
- `useRef` guard calls `installFeature({ layout, bootstrap, navigate })` synchronously on first run
|
|
239
|
+
- Obtain `useNavigate` (and similar) here and **pass into `install`**, not inside `install` itself
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## `install.tsx`
|
|
244
|
+
|
|
245
|
+
- **New `Controller()` here** — exactly once; shared across all `create` closures
|
|
246
|
+
- Use dynamic `import()` per section for code splitting
|
|
247
|
+
- Plain function — not a hook, not a component
|
|
248
|
+
- Do not use React Context to replace explicit controller passing
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## `layout.tsx`
|
|
253
|
+
|
|
254
|
+
- `LayoutController` holds `@observable.ref` slots for `ComponentType`
|
|
255
|
+
- `@action` setters for slots; observer shell renders skeletons until slots load
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Controllers and presenters
|
|
260
|
+
|
|
261
|
+
- **Controller**: cross-section UI state; `convex.mutation` / `convex.query` when needed; **no React**
|
|
262
|
+
- **Presenter**: one section’s logic; same rules — **no React**
|
|
263
|
+
- Prefer **`@action async`** without extra `runInAction` for Stage 3 decorators when using MobX’s supported pattern
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## `create.tsx`
|
|
268
|
+
|
|
269
|
+
- Call child factories **outside** the returned `observer()` so identities stay stable
|
|
270
|
+
- Inside `observer`: `useQuery`, `useForm`, etc.
|
|
271
|
+
- Pass **`initialData: bootstrap.\*`** for the same Convex function used in SSR
|
|
272
|
+
- No `useMutation` for routine writes — use `convex.mutation` in controller/presenter and pass callbacks
|
|
273
|
+
- No `useState` for feature UI state — controller/presenter own that
|
|
274
|
+
- Inline JSX handlers: **block or multi-line** — not one-liner `onClick={() => foo()}`
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Display components
|
|
279
|
+
|
|
280
|
+
- Props + callbacks only; **no** `Doc<"todos">` / raw Convex types — import **`Todo`**, **`TodoId`** from `@/api/todos/types` (or the entity’s `types.ts`)
|
|
281
|
+
- Never import **`Ent<...>`** on the frontend
|
|
282
|
+
- **Forms**: `useForm` + `<FormProvider>` in `create.tsx`; display uses **`useFormContext()`** — never pass `UseFormReturn` as a prop
|
|
283
|
+
- **Thin UIs** can live inline in `create.tsx` without a separate display file
|
|
284
|
+
- Arrow functions, named `Props` type, explicit `return (...)` for JSX
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Schema (`schema.ts`)
|
|
289
|
+
|
|
290
|
+
Same PascalCase name for type and Zod value (see `coding_standards.mdc`):
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
export type NewTodoSchema = z.infer<typeof NewTodoSchema>;
|
|
294
|
+
export const NewTodoSchema = z.object({
|
|
295
|
+
title: z.string().min(1, "Title is required"),
|
|
296
|
+
});
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Factory opts vs observer props
|
|
302
|
+
|
|
303
|
+
- **Opts** — known when the factory runs (`controller`, `bootstrap`, child components created by parent). Closed over.
|
|
304
|
+
- **Props** on the observer — per-render / per-item values (`todoId`, list row props).
|
|
305
|
+
|
|
306
|
+
Component slots are **`ComponentType`**, not `ReactNode`.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## DI tree
|
|
311
|
+
|
|
312
|
+
Nested surfaces (sheets, dialogs) live **under** the section that constructs them so dependencies stay a subtree of that section’s `create.tsx`.
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Next.js variant
|
|
317
|
+
|
|
318
|
+
Adapt loader and client-entry patterns to your framework’s routing and data APIs — layer names and surface rules stay the same.
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## Quick reference
|
|
323
|
+
|
|
324
|
+
| File | Hooks? | Controller |
|
|
325
|
+
|------|--------|------------|
|
|
326
|
+
| Route module | No in loader | No |
|
|
327
|
+
| `bootstrap.ts` | No | No |
|
|
328
|
+
| `[feature].tsx` | `useRef`, `useMemo`, router | No — passes to install |
|
|
329
|
+
| `install.tsx` | No | **Instantiates** |
|
|
330
|
+
| `layout.tsx` | No | No |
|
|
331
|
+
| `*_controller.ts` | No (class) | — |
|
|
332
|
+
| `*_presenter.ts` | No (class) | — |
|
|
333
|
+
| `create.tsx` | Yes, inside observer | Receives via closure |
|
|
334
|
+
| Display | `useFormContext` only | No |
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
actions: read
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
build:
|
|
15
|
+
name: Build & Type-check
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
env:
|
|
18
|
+
SKIP_HOOKS: "1"
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
with:
|
|
23
|
+
fetch-depth: 0
|
|
24
|
+
|
|
25
|
+
- uses: oven-sh/setup-bun@v2
|
|
26
|
+
with:
|
|
27
|
+
bun-version: latest
|
|
28
|
+
|
|
29
|
+
- name: Install dependencies
|
|
30
|
+
run: bun install --frozen-lockfile
|
|
31
|
+
|
|
32
|
+
- name: Typecheck affected projects
|
|
33
|
+
run: bun run typecheck
|
|
34
|
+
|
|
35
|
+
- uses: nrwl/nx-set-shas@v4
|
|
36
|
+
|
|
37
|
+
- name: Build affected projects
|
|
38
|
+
run: bun nx affected -t build
|
|
39
|
+
# - name: Test affected projects
|
|
40
|
+
# run: bun nx affected -t test
|