domain-driver 0.3.1 → 0.4.0
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 +85 -22
- package/dist/cli.js +1 -1
- package/dist/commands/action.js +19 -5
- package/dist/commands/controller-renderer.js +25 -0
- package/dist/commands/controller.js +6 -4
- package/dist/commands/feature.js +8 -3
- package/dist/commands/hints.js +10 -0
- package/dist/commands/hook-renderer.js +63 -0
- package/dist/commands/hook.js +21 -43
- package/dist/commands/repository.js +8 -1
- package/dist/init/content.js +7 -3
- package/dist/stack/detect.js +6 -0
- package/dist/stack/profiles/nest.js +1 -0
- package/dist/stack/profiles/next-frontend.js +1 -0
- package/dist/stack/profiles/next-fullstack.js +1 -0
- package/dist/stack/profiles/node.js +1 -0
- package/dist/stack/profiles/react.js +1 -0
- package/dist/stack/profiles/tanstack-start.js +46 -0
- package/dist/stack/registry.js +4 -1
- package/dist/stack/types.js +1 -1
- package/dist/templates/actions.js +12 -0
- package/dist/templates/controllers/server-fn.js +42 -0
- package/dist/templates/frontend/container.js +10 -6
- package/dist/templates/frontend/hook.js +63 -79
- package/dist/templates/frontend/query-hook.js +83 -0
- package/dist/templates/frontend/query-keys.js +15 -0
- package/dist/templates/frontend/route.js +33 -0
- package/dist/templates/frontend/server-fn-repository.js +27 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# domain-driver 🚀
|
|
2
2
|
|
|
3
|
-
A CLI scaffolding tool for domain-driven feature folders. Like Laravel's `php artisan make`, but for Next.js, React, Node, and
|
|
3
|
+
A CLI scaffolding tool for domain-driven feature folders. Like Laravel's `php artisan make`, but for Next.js, React, Node, NestJS, and TanStack Start projects. It detects your stack and generates only the layers that stack needs, one file per action.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -29,6 +29,7 @@ Stack: next-fullstack (detected)
|
|
|
29
29
|
| Stack | Detected when | Features live in |
|
|
30
30
|
|---|---|---|
|
|
31
31
|
| `nest` | `@nestjs/core` is a dependency | `src/<feature>` |
|
|
32
|
+
| `tanstack-start` | `@tanstack/react-start` is a dependency | `src/routes/<feature>` or `routes/<feature>` |
|
|
32
33
|
| `next-fullstack` | `next` is a dependency and `app/api`, `src/app/api`, `pages/api`, or `src/pages/api` exists | `app/<feature>` or `src/app/<feature>` |
|
|
33
34
|
| `next-frontend` | `next` is a dependency, no api directory | `app/<feature>` or `src/app/<feature>` |
|
|
34
35
|
| `react` | `react` is a dependency, `next` is not | `src/features/<feature>` or `features/<feature>` |
|
|
@@ -46,19 +47,19 @@ domain-driver --stack nest make:feature cat -a
|
|
|
46
47
|
|
|
47
48
|
## What each stack generates
|
|
48
49
|
|
|
49
|
-
| Layer | next-fullstack | next-frontend | react | node | nest |
|
|
50
|
-
|
|
51
|
-
| `page.tsx` | yes | yes | | | |
|
|
52
|
-
| components | client + server | client + server | flat | | |
|
|
53
|
-
| containers, hooks | yes | yes | yes | | |
|
|
54
|
-
| client services + repositories
|
|
55
|
-
| server services + repositories (database stubs) | `server/` | | | yes | yes |
|
|
56
|
-
| controllers | `app/api/<feature>/` route handlers | | | five files + routes file | five files |
|
|
57
|
-
| module | | | | | yes |
|
|
58
|
-
| DTOs (`nestjs-zod`) | | | | | yes |
|
|
59
|
-
| schemas (Zod), types | yes | yes | yes | yes | yes |
|
|
50
|
+
| Layer | next-fullstack | next-frontend | react | node | nest | tanstack-start |
|
|
51
|
+
|---|---|---|---|---|---|---|
|
|
52
|
+
| entry file (`page.tsx`; `index.tsx` on tanstack-start) | yes | yes | | | | yes |
|
|
53
|
+
| components | client + server | client + server | flat | | | flat |
|
|
54
|
+
| containers, hooks | yes | yes | yes | | | yes (Query hooks) |
|
|
55
|
+
| client services + repositories | `client/` (fetch) | top level (fetch) | top level (fetch) | | | `-client/` (calls server functions, no fetch) |
|
|
56
|
+
| server services + repositories (database stubs) | `server/` | | | yes | yes | `-server/` |
|
|
57
|
+
| controllers | `app/api/<feature>/` route handlers | | | five files + routes file | five files | server functions (`-server/functions/*.fn.ts`) |
|
|
58
|
+
| module | | | | | yes | |
|
|
59
|
+
| DTOs (`nestjs-zod`) | | | | | yes | |
|
|
60
|
+
| schemas (Zod), types | yes | yes | yes | yes | yes | yes |
|
|
60
61
|
|
|
61
|
-
Every layer that has actions gets one file per action: `List`, `Show`, `Create`, `Update`, `Delete`. Saving a cat means `CreateCat.controller.ts`, `CreateCat.service.ts`, `CreateCat.repository.ts`, and `CreateCat.
|
|
62
|
+
Every layer that has actions gets one file per action: `List`, `Show`, `Create`, `Update`, `Delete`. Saving a cat means `CreateCat.controller.ts`, `CreateCat.service.ts`, `CreateCat.repository.ts`, `CreateCat.schema.ts`, and, on stacks with a hook layer, `CreateCat.hook.ts` exporting `useCreateCat`.
|
|
62
63
|
|
|
63
64
|
---
|
|
64
65
|
|
|
@@ -69,7 +70,7 @@ Every layer command takes one target, `<feature>/<Name>`: the feature folder on
|
|
|
69
70
|
### `make:feature`
|
|
70
71
|
|
|
71
72
|
```bash
|
|
72
|
-
domain-driver make:feature users # folders + .gitkeep, plus page.tsx (Next) or the module (Nest)
|
|
73
|
+
domain-driver make:feature users # folders + .gitkeep, plus page.tsx (Next), index.tsx (TanStack Start), or the module (Nest)
|
|
73
74
|
domain-driver make:feature users/User -a # every layer for the detected stack, entity User
|
|
74
75
|
```
|
|
75
76
|
|
|
@@ -95,8 +96,9 @@ Scaffolds a bespoke operation as its own files, so it never lands inside `ShowUs
|
|
|
95
96
|
|---|---|
|
|
96
97
|
| `node` | service, repository, controller for the detected framework, plus the line to add above the `/:id` routes in `<feature>.routes.ts` printed |
|
|
97
98
|
| `nest` | injectable service and repository, `@Controller` class, DTO with input, plus the classes to register printed, with the controller listed before `Show<Entity>Controller` |
|
|
98
|
-
| `next-fullstack` | server service and repository, client service and repository, `app/api/<feature>/<slug>/route.ts` |
|
|
99
|
-
| `next-frontend`, `react` | client service and repository calling `/api/<feature>/<slug
|
|
99
|
+
| `next-fullstack` | server service and repository, client service and repository, a hook, `app/api/<feature>/<slug>/route.ts` |
|
|
100
|
+
| `next-frontend`, `react` | client service and repository calling `/api/<feature>/<slug>`, and a hook |
|
|
101
|
+
| `tanstack-start` | server service and repository, client service and repository, a hook, and a server function (`<Name>.fn.ts`) in place of a route handler |
|
|
100
102
|
|
|
101
103
|
### `make:controller`
|
|
102
104
|
|
|
@@ -104,7 +106,7 @@ Scaffolds a bespoke operation as its own files, so it never lands inside `ShowUs
|
|
|
104
106
|
domain-driver make:controller users/User
|
|
105
107
|
```
|
|
106
108
|
|
|
107
|
-
Node: five controllers plus `<feature>.routes.ts` for Express, Fastify, or Hono. Nest: five single-action controllers. Next.js fullstack: `app/api/<feature>/route.ts` and `app/api/<feature>/[id]/route.ts`. Not available on frontend-only stacks.
|
|
109
|
+
Node: five controllers plus `<feature>.routes.ts` for Express, Fastify, or Hono. Nest: five single-action controllers. Next.js fullstack: `app/api/<feature>/route.ts` and `app/api/<feature>/[id]/route.ts`. TanStack Start: five server functions (`<Name>.fn.ts`, built with `createServerFn`) under `-server/functions/`. Not available on frontend-only stacks (`next-frontend`, `react`).
|
|
108
110
|
|
|
109
111
|
### `make:service` and `make:repository`
|
|
110
112
|
|
|
@@ -113,7 +115,7 @@ domain-driver make:service users/User [--side client|server|both]
|
|
|
113
115
|
domain-driver make:repository users/User [--side client|server|both]
|
|
114
116
|
```
|
|
115
117
|
|
|
116
|
-
`--side` matters on `next-fullstack`, where both sides exist. Default is `both`.
|
|
118
|
+
`--side` matters on `next-fullstack` and `tanstack-start`, where both sides exist. Default is `both`.
|
|
117
119
|
|
|
118
120
|
### `make:schema`
|
|
119
121
|
|
|
@@ -129,11 +131,27 @@ Writes `CreateUser.schema.ts` and `UpdateUser.schema.ts`. On Nest it also writes
|
|
|
129
131
|
domain-driver make:types users/User
|
|
130
132
|
domain-driver make:component users/UserCard [client|server]
|
|
131
133
|
domain-driver make:container users/UserContainer
|
|
132
|
-
domain-driver make:hook users/
|
|
134
|
+
domain-driver make:hook users/User
|
|
133
135
|
```
|
|
134
136
|
|
|
135
137
|
Component, container, and hook commands fail with a clear message on backend stacks, and `server` components are rejected on React.
|
|
136
138
|
|
|
139
|
+
`make:hook` writes one file per action instead of a combined hook: `ListUser.hook.ts`, `ShowUser.hook.ts`, `CreateUser.hook.ts`, `UpdateUser.hook.ts`, `DeleteUser.hook.ts`, each exporting one hook (`useListUser`, `useShowUser`, `useCreateUser`, `useUpdateUser`, `useDeleteUser`). There is no combined `useUser.ts`. On `react`, `next-frontend`, and `next-fullstack` these hold plain React state; on `tanstack-start` they are TanStack Query hooks backed by a generated `<feature>.keys.ts` cache-key module. Generating hooks on this profile prints a one-time reminder to install `@tanstack/react-query`, since TanStack Start does not bundle it.
|
|
140
|
+
|
|
141
|
+
Because the five hooks no longer share state, containers wire them together. On `react`, `next-frontend`, and `next-fullstack`, where hooks hold plain state:
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
const { data, loading, error, refetch } = useListCat();
|
|
145
|
+
const { createCat } = useCreateCat({ onSuccess: refetch });
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
On `tanstack-start`, `useListCat()` returns the `useQuery` result (`data`, `isPending`, `error`, ...) and `useCreateCat()` returns the `useMutation` result (`mutate`, `mutateAsync`, `isPending`, ...) directly — there is no `createCat` property and no `onSuccess` option; the hook invalidates the query cache internally on success:
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
const { data, isPending, error } = useListCat();
|
|
152
|
+
const { mutate: createCat } = useCreateCat();
|
|
153
|
+
```
|
|
154
|
+
|
|
137
155
|
### `init`
|
|
138
156
|
|
|
139
157
|
```bash
|
|
@@ -192,14 +210,42 @@ Install `nestjs-zod` and register `ZodValidationPipe` as `APP_PIPE` once in your
|
|
|
192
210
|
|
|
193
211
|
---
|
|
194
212
|
|
|
213
|
+
## Example: `make:feature coffee-type -a` on TanStack Start
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
src/routes/coffee-type/
|
|
217
|
+
├── index.tsx createFileRoute route — the entry, not page.tsx
|
|
218
|
+
├── -components/
|
|
219
|
+
│ └── CoffeeType.tsx
|
|
220
|
+
├── -containers/
|
|
221
|
+
│ └── CoffeeTypeContainer.tsx
|
|
222
|
+
├── -hooks/ coffee-type.keys.ts, plus five hook files (one per action)
|
|
223
|
+
├── -client/
|
|
224
|
+
│ ├── services/ (five files)
|
|
225
|
+
│ └── repositories/ (five files, call the server functions directly)
|
|
226
|
+
├── -server/
|
|
227
|
+
│ ├── functions/ (five *.fn.ts files, built with createServerFn)
|
|
228
|
+
│ ├── services/ (five files)
|
|
229
|
+
│ └── repositories/ (five files, database-agnostic stubs)
|
|
230
|
+
├── -schemas/
|
|
231
|
+
│ ├── CreateCoffeeType.schema.ts
|
|
232
|
+
│ └── UpdateCoffeeType.schema.ts
|
|
233
|
+
└── -types/
|
|
234
|
+
└── CoffeeType.types.ts
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Every layer directory carries a `-` prefix so TanStack Router excludes it from routing. The controller layer is server functions — `<Name>.fn.ts` built with `createServerFn` — and client repositories import and call them directly, so there is no `fetch` and no `Response.json`. Hooks are TanStack Query, backed by the generated `coffee-type.keys.ts`. Generating hooks on this profile prints a one-time reminder to install `@tanstack/react-query`, since TanStack Start does not bundle it.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
195
241
|
## Philosophy
|
|
196
242
|
|
|
197
243
|
Everything for a feature lives in one folder, and every file does one thing.
|
|
198
244
|
|
|
199
|
-
- **repositories** — data access only. Client-side repositories call your API; server-side repositories call your database.
|
|
245
|
+
- **repositories** — data access only. Client-side repositories call your API (on TanStack Start, the server function directly); server-side repositories call your database.
|
|
200
246
|
- **services** — business logic, one class per action.
|
|
201
|
-
- **controllers** — HTTP in, service call, HTTP out, one file per action.
|
|
202
|
-
- **hooks** —
|
|
247
|
+
- **controllers** — HTTP in, service call, HTTP out, one file per action (server functions on TanStack Start).
|
|
248
|
+
- **hooks** — state and side effects, one hook per action, calls services (TanStack Query on TanStack Start, plain React state elsewhere).
|
|
203
249
|
- **containers** — wire hooks into UI.
|
|
204
250
|
- **components** — presentational UI.
|
|
205
251
|
- **schemas** — Zod validation for create and update; the update body never carries the id, it comes from the path.
|
|
@@ -231,6 +277,19 @@ The cache lives in `~/.cache/domain-driver` (or `$XDG_CACHE_HOME/domain-driver`)
|
|
|
231
277
|
|
|
232
278
|
---
|
|
233
279
|
|
|
280
|
+
## Upgrading from 0.3.x
|
|
281
|
+
|
|
282
|
+
Hooks are now one file per action — `<Action><Entity>.hook.ts` exporting `use<Action><Entity>` (`useListCat`, `useCreateCat`, ...) — instead of a single combined `use<Entity>.ts`, which is no longer generated. `make:hook` now takes `<feature>/<Entity>`, not `<feature>/use<Entity>`. `make:action` writes a matching hook alongside the service and repository on any stack that has a hook layer.
|
|
283
|
+
|
|
284
|
+
domain-driver never overwrites a file that already exists, so this only changes new scaffolding: a `use<Entity>.ts` written by an older version is left alone and keeps working. New features and new actions get the per-action hooks; wire them together in the container, since they no longer share state:
|
|
285
|
+
|
|
286
|
+
```tsx
|
|
287
|
+
const { data, loading, error, refetch } = useListCat();
|
|
288
|
+
const { createCat } = useCreateCat({ onSuccess: refetch });
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
This release also adds a sixth stack, `tanstack-start`, detected from `@tanstack/react-start` — see the TanStack Start example above.
|
|
292
|
+
|
|
234
293
|
## Upgrading from 0.1.0
|
|
235
294
|
|
|
236
295
|
Every layer command now takes a single `<feature>/<Name>` target instead of separate feature and name arguments, for example `make:schema users User` becomes `make:schema users/User`. `make:feature users -a` still works and names the entity `Users`; write `users/User` if you want a different entity name.
|
|
@@ -255,6 +314,8 @@ npm test
|
|
|
255
314
|
npm run test:coverage
|
|
256
315
|
```
|
|
257
316
|
|
|
317
|
+
`npm run typecheck:tanstack-fixture` compiles a generated TanStack Start feature against the real `@tanstack/react-start` and `@tanstack/react-query` packages to catch drift between the templates and the real APIs. It is network-dependent and slow, so it is not part of `npm test` — run it locally after touching the `tanstack-start` profile or its templates.
|
|
318
|
+
|
|
258
319
|
---
|
|
259
320
|
|
|
260
321
|
## Roadmap
|
|
@@ -264,6 +325,8 @@ npm run test:coverage
|
|
|
264
325
|
- [x] Per-action files in every layer
|
|
265
326
|
- [x] Bespoke actions with make:action
|
|
266
327
|
- [x] Agent guidance with init and a guarded postinstall
|
|
328
|
+
- [x] TanStack Start stack, with server functions and per-action query hooks
|
|
329
|
+
- [x] Per-action hooks on every stack that has them, replacing the single combined hook
|
|
267
330
|
- [ ] Config file — override stack and feature root per project
|
|
268
331
|
- [ ] Configurable API base URL for client repositories
|
|
269
332
|
- [ ] ORM-aware server repositories
|
package/dist/cli.js
CHANGED
|
@@ -126,7 +126,7 @@ function createProgram(deps) {
|
|
|
126
126
|
});
|
|
127
127
|
program
|
|
128
128
|
.command('make:hook <target>')
|
|
129
|
-
.description('Scaffold
|
|
129
|
+
.description('Scaffold single-responsibility hook files inside an existing feature (<feature>/<Entity>)')
|
|
130
130
|
.action((target) => {
|
|
131
131
|
const { feature, name } = (0, target_1.parseTarget)(target);
|
|
132
132
|
(0, hook_1.makeHook)(feature, name);
|
package/dist/commands/action.js
CHANGED
|
@@ -39,17 +39,18 @@ const path = __importStar(require("path"));
|
|
|
39
39
|
const registry_1 = require("../stack/registry");
|
|
40
40
|
const actions_1 = require("../templates/actions");
|
|
41
41
|
const server_repository_1 = require("../templates/backend/server-repository");
|
|
42
|
-
const nest_1 = require("../templates/controllers/nest");
|
|
43
42
|
const next_action_route_1 = require("../templates/controllers/next-action-route");
|
|
44
43
|
const node_1 = require("../templates/controllers/node");
|
|
45
|
-
const client_repository_1 = require("../templates/frontend/client-repository");
|
|
46
44
|
const dto_1 = require("../templates/nest/dto");
|
|
47
45
|
const service_1 = require("../templates/service");
|
|
48
46
|
const schema_1 = require("../templates/shared/schema");
|
|
49
47
|
const fs_1 = require("../utils/fs");
|
|
50
48
|
const naming_1 = require("../utils/naming");
|
|
51
49
|
const paths_1 = require("../utils/paths");
|
|
50
|
+
const controller_renderer_1 = require("./controller-renderer");
|
|
52
51
|
const hints_1 = require("./hints");
|
|
52
|
+
const hook_renderer_1 = require("./hook-renderer");
|
|
53
|
+
const repository_1 = require("./repository");
|
|
53
54
|
const resolve_1 = require("./resolve");
|
|
54
55
|
const write_1 = require("./write");
|
|
55
56
|
function parseReturns(value) {
|
|
@@ -70,6 +71,8 @@ function makeAction(feature, entity, actionName, options) {
|
|
|
70
71
|
wroteAny = writeController(ctx, spec, entity) || wroteAny;
|
|
71
72
|
if ((0, registry_1.hasLayer)(ctx.profile, 'clientRepository'))
|
|
72
73
|
wroteAny = writeSide(ctx, spec, entity, 'client') || wroteAny;
|
|
74
|
+
if ((0, registry_1.hasLayer)(ctx.profile, 'hook'))
|
|
75
|
+
wroteAny = writeHook(ctx, spec, entity) || wroteAny;
|
|
73
76
|
if (wroteAny)
|
|
74
77
|
console.log(`✅ Action "${spec.name}" scaffolded in "${feature}"`);
|
|
75
78
|
return wroteAny;
|
|
@@ -93,13 +96,23 @@ function writeInput(ctx, spec) {
|
|
|
93
96
|
function writeSide(ctx, spec, entity, side) {
|
|
94
97
|
const repositoryLayer = side === 'client' ? 'clientRepository' : 'serverRepository';
|
|
95
98
|
const serviceLayer = side === 'client' ? 'clientService' : 'serverService';
|
|
96
|
-
const renderRepository = side === 'client' ?
|
|
99
|
+
const renderRepository = side === 'client' ? (0, repository_1.clientRenderer)(ctx) : server_repository_1.renderServerRepository;
|
|
97
100
|
const repositoryFile = path.join((0, resolve_1.ensureLayerDir)(ctx, repositoryLayer), `${spec.name}.repository.ts`);
|
|
98
101
|
const wroteRepository = (0, write_1.writeIfAbsent)(repositoryFile, () => renderRepository(ctx, spec, entity, repositoryFile));
|
|
99
102
|
const serviceFile = path.join((0, resolve_1.ensureLayerDir)(ctx, serviceLayer), `${spec.name}.service.ts`);
|
|
100
103
|
const wroteService = (0, write_1.writeIfAbsent)(serviceFile, () => (0, service_1.renderService)(ctx, spec, entity, serviceFile, side));
|
|
101
104
|
return wroteRepository || wroteService;
|
|
102
105
|
}
|
|
106
|
+
function writeHook(ctx, spec, entity) {
|
|
107
|
+
const dir = (0, resolve_1.ensureLayerDir)(ctx, 'hook');
|
|
108
|
+
(0, hook_renderer_1.ensureQueryKeys)(ctx, dir);
|
|
109
|
+
const render = (0, hook_renderer_1.hookRenderer)(ctx);
|
|
110
|
+
const filePath = path.join(dir, `${spec.name}.hook.ts`);
|
|
111
|
+
const wrote = (0, write_1.writeIfAbsent)(filePath, () => render(ctx, spec, entity, filePath));
|
|
112
|
+
if (wrote)
|
|
113
|
+
(0, hints_1.hintReactQuery)(ctx.profile);
|
|
114
|
+
return wrote;
|
|
115
|
+
}
|
|
103
116
|
function writeController(ctx, spec, entity) {
|
|
104
117
|
if (ctx.profile.name === 'next-fullstack') {
|
|
105
118
|
const routeDir = path.join((0, paths_1.apiRouteDir)(ctx.stack, ctx.feature), spec.path.slice(1));
|
|
@@ -107,8 +120,9 @@ function writeController(ctx, spec, entity) {
|
|
|
107
120
|
const routeFile = path.join(routeDir, 'route.ts');
|
|
108
121
|
return (0, write_1.writeIfAbsent)(routeFile, () => (0, next_action_route_1.renderActionRoute)(ctx, spec, routeFile));
|
|
109
122
|
}
|
|
110
|
-
const
|
|
111
|
-
const
|
|
123
|
+
const suffix = (0, controller_renderer_1.controllerSuffix)(ctx.profile.name);
|
|
124
|
+
const controllerFile = path.join((0, resolve_1.ensureLayerDir)(ctx, 'controller'), `${spec.name}.${suffix}.ts`);
|
|
125
|
+
const render = (0, controller_renderer_1.controllerRenderer)(ctx.profile.name);
|
|
112
126
|
const wroteController = (0, write_1.writeIfAbsent)(controllerFile, () => render(ctx, spec, entity, controllerFile));
|
|
113
127
|
const line = ctx.profile.name === 'node' ? (0, node_1.renderNodeRouteLine)(ctx, spec, entity) : null;
|
|
114
128
|
if (wroteController && line !== null) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.controllerRenderer = controllerRenderer;
|
|
4
|
+
exports.controllerSuffix = controllerSuffix;
|
|
5
|
+
const nest_1 = require("../templates/controllers/nest");
|
|
6
|
+
const node_1 = require("../templates/controllers/node");
|
|
7
|
+
const server_fn_1 = require("../templates/controllers/server-fn");
|
|
8
|
+
const RENDERERS = Object.freeze({
|
|
9
|
+
nest: nest_1.renderNestController,
|
|
10
|
+
node: node_1.renderNodeController,
|
|
11
|
+
'tanstack-start': server_fn_1.renderServerFn,
|
|
12
|
+
});
|
|
13
|
+
const SUFFIXES = Object.freeze({
|
|
14
|
+
'tanstack-start': 'fn',
|
|
15
|
+
});
|
|
16
|
+
function controllerRenderer(stack) {
|
|
17
|
+
const renderer = RENDERERS[stack];
|
|
18
|
+
if (renderer === undefined) {
|
|
19
|
+
throw new Error(`No controller renderer registered for the "${stack}" stack.`);
|
|
20
|
+
}
|
|
21
|
+
return renderer;
|
|
22
|
+
}
|
|
23
|
+
function controllerSuffix(stack) {
|
|
24
|
+
return SUFFIXES[stack] ?? 'controller';
|
|
25
|
+
}
|
|
@@ -37,11 +37,11 @@ exports.makeController = makeController;
|
|
|
37
37
|
const path = __importStar(require("path"));
|
|
38
38
|
const registry_1 = require("../stack/registry");
|
|
39
39
|
const actions_1 = require("../templates/actions");
|
|
40
|
-
const nest_1 = require("../templates/controllers/nest");
|
|
41
40
|
const next_route_1 = require("../templates/controllers/next-route");
|
|
42
41
|
const node_1 = require("../templates/controllers/node");
|
|
43
42
|
const fs_1 = require("../utils/fs");
|
|
44
43
|
const paths_1 = require("../utils/paths");
|
|
44
|
+
const controller_renderer_1 = require("./controller-renderer");
|
|
45
45
|
const resolve_1 = require("./resolve");
|
|
46
46
|
const write_1 = require("./write");
|
|
47
47
|
function makeController(feature, name) {
|
|
@@ -51,10 +51,12 @@ function makeController(feature, name) {
|
|
|
51
51
|
return writeNextRoutes(ctx, name);
|
|
52
52
|
}
|
|
53
53
|
const dir = (0, resolve_1.ensureLayerDir)(ctx, 'controller');
|
|
54
|
-
const render = ctx.profile.name
|
|
55
|
-
const
|
|
54
|
+
const render = (0, controller_renderer_1.controllerRenderer)(ctx.profile.name);
|
|
55
|
+
const suffix = (0, controller_renderer_1.controllerSuffix)(ctx.profile.name);
|
|
56
|
+
const written = (0, write_1.writeSpecFiles)(dir, (0, actions_1.standardActions)(name), suffix, (spec, filePath) => render(ctx, spec, name, filePath));
|
|
57
|
+
const label = ctx.profile.name === 'tanstack-start' ? 'Server functions' : 'Controllers';
|
|
56
58
|
if (written > 0)
|
|
57
|
-
console.log(`✅
|
|
59
|
+
console.log(`✅ ${label} for "${name}" created at ${dir}`);
|
|
58
60
|
const wroteRoutes = ctx.profile.name === 'node' ? writeNodeRoutes(ctx, name) : false;
|
|
59
61
|
return written > 0 || wroteRoutes;
|
|
60
62
|
}
|
package/dist/commands/feature.js
CHANGED
|
@@ -37,6 +37,7 @@ exports.makeFeature = makeFeature;
|
|
|
37
37
|
const path = __importStar(require("path"));
|
|
38
38
|
const registry_1 = require("../stack/registry");
|
|
39
39
|
const page_1 = require("../templates/frontend/page");
|
|
40
|
+
const route_1 = require("../templates/frontend/route");
|
|
40
41
|
const module_1 = require("../templates/nest/module");
|
|
41
42
|
const fs_1 = require("../utils/fs");
|
|
42
43
|
const naming_1 = require("../utils/naming");
|
|
@@ -88,7 +89,7 @@ function scaffoldLayers(ctx, entity) {
|
|
|
88
89
|
if ((0, registry_1.hasLayer)(profile, 'clientService'))
|
|
89
90
|
(0, service_1.makeService)(feature, entity, 'client');
|
|
90
91
|
if ((0, registry_1.hasLayer)(profile, 'hook'))
|
|
91
|
-
(0, hook_1.makeHook)(feature,
|
|
92
|
+
(0, hook_1.makeHook)(feature, entity);
|
|
92
93
|
if ((0, registry_1.hasLayer)(profile, 'component'))
|
|
93
94
|
(0, component_1.makeComponent)(feature, entity, 'client');
|
|
94
95
|
if ((0, registry_1.hasLayer)(profile, 'container'))
|
|
@@ -96,8 +97,12 @@ function scaffoldLayers(ctx, entity) {
|
|
|
96
97
|
}
|
|
97
98
|
function writeEntryFile(ctx, entity, all) {
|
|
98
99
|
if ((0, registry_1.hasLayer)(ctx.profile, 'page')) {
|
|
99
|
-
const
|
|
100
|
-
|
|
100
|
+
const isRoute = ctx.profile.name === 'tanstack-start';
|
|
101
|
+
const filePath = path.join(ctx.featureDir, isRoute ? 'index.tsx' : 'page.tsx');
|
|
102
|
+
const content = isRoute
|
|
103
|
+
? (0, route_1.renderRoute)(ctx, entity, filePath, all)
|
|
104
|
+
: (0, page_1.renderPage)(ctx, entity, filePath, all);
|
|
105
|
+
(0, fs_1.writeFileSafe)(filePath, content);
|
|
101
106
|
}
|
|
102
107
|
if ((0, registry_1.hasLayer)(ctx.profile, 'module')) {
|
|
103
108
|
const filePath = path.join(ctx.featureDir, `${ctx.feature}.module.ts`);
|
package/dist/commands/hints.js
CHANGED
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.resetHints = resetHints;
|
|
4
4
|
exports.hintNestjsZod = hintNestjsZod;
|
|
5
|
+
exports.hintReactQuery = hintReactQuery;
|
|
5
6
|
exports.standardClassNames = standardClassNames;
|
|
6
7
|
exports.hintRegisterInModule = hintRegisterInModule;
|
|
7
8
|
const detect_1 = require("../stack/detect");
|
|
8
9
|
const actions_1 = require("../templates/actions");
|
|
9
10
|
let nestjsZodHinted = false;
|
|
11
|
+
let reactQueryHinted = false;
|
|
10
12
|
function resetHints() {
|
|
11
13
|
nestjsZodHinted = false;
|
|
14
|
+
reactQueryHinted = false;
|
|
12
15
|
}
|
|
13
16
|
function hintNestjsZod(stack) {
|
|
14
17
|
if (stack.stack !== 'nest' || stack.hasNestjsZod || nestjsZodHinted)
|
|
@@ -17,6 +20,13 @@ function hintNestjsZod(stack) {
|
|
|
17
20
|
console.log('ℹ️ nestjs-zod is not installed. Run: npm install nestjs-zod');
|
|
18
21
|
console.log(' Then register the pipe in AppModule: { provide: APP_PIPE, useClass: ZodValidationPipe }');
|
|
19
22
|
}
|
|
23
|
+
function hintReactQuery(profile) {
|
|
24
|
+
if (!profile.queryHooks || reactQueryHinted)
|
|
25
|
+
return;
|
|
26
|
+
reactQueryHinted = true;
|
|
27
|
+
console.log('ℹ️ Hooks use TanStack Query. Install it: npm install @tanstack/react-query');
|
|
28
|
+
console.log(' Then wrap your app in a QueryClientProvider.');
|
|
29
|
+
}
|
|
20
30
|
function standardClassNames(name, suffix) {
|
|
21
31
|
return actions_1.ACTIONS.map((action) => `${action}${name}${suffix}`);
|
|
22
32
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.hookRenderer = hookRenderer;
|
|
37
|
+
exports.ensureQueryKeys = ensureQueryKeys;
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const hook_1 = require("../templates/frontend/hook");
|
|
40
|
+
const query_hook_1 = require("../templates/frontend/query-hook");
|
|
41
|
+
const query_keys_1 = require("../templates/frontend/query-keys");
|
|
42
|
+
const fs_1 = require("../utils/fs");
|
|
43
|
+
/**
|
|
44
|
+
* Picks the hook template for the profile: TanStack Query hooks where the profile supports
|
|
45
|
+
* them, the plain useState/useEffect hook otherwise.
|
|
46
|
+
*/
|
|
47
|
+
function hookRenderer(ctx) {
|
|
48
|
+
return ctx.profile.queryHooks ? query_hook_1.renderQueryHook : hook_1.renderHook;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Ensures the shared `<feature>.keys.ts` module exists for query-hook profiles. Unlike
|
|
52
|
+
* `writeIfAbsent`, this never warns: the keys file is deliberately shared across every hook
|
|
53
|
+
* in the feature, so every `make:action`/`make:hook` call re-checking it is expected, not a
|
|
54
|
+
* skipped write worth flagging.
|
|
55
|
+
*/
|
|
56
|
+
function ensureQueryKeys(ctx, dir) {
|
|
57
|
+
if (!ctx.profile.queryHooks)
|
|
58
|
+
return;
|
|
59
|
+
const keysFile = path.join(dir, `${ctx.feature}.keys.ts`);
|
|
60
|
+
if ((0, fs_1.fileExists)(keysFile))
|
|
61
|
+
return;
|
|
62
|
+
(0, fs_1.writeFileSafe)(keysFile, (0, query_keys_1.renderQueryKeys)(ctx.feature));
|
|
63
|
+
}
|
package/dist/commands/hook.js
CHANGED
|
@@ -1,52 +1,30 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
3
|
exports.makeHook = makeHook;
|
|
37
|
-
const path = __importStar(require("path"));
|
|
38
4
|
const registry_1 = require("../stack/registry");
|
|
39
|
-
const
|
|
40
|
-
const
|
|
5
|
+
const actions_1 = require("../templates/actions");
|
|
6
|
+
const hints_1 = require("./hints");
|
|
7
|
+
const hook_renderer_1 = require("./hook-renderer");
|
|
41
8
|
const resolve_1 = require("./resolve");
|
|
42
|
-
|
|
9
|
+
const write_1 = require("./write");
|
|
10
|
+
const OLD_HOOK_NAME_PATTERN = /^use[A-Z]/;
|
|
11
|
+
function rejectOldHookName(feature, entity) {
|
|
12
|
+
if (!OLD_HOOK_NAME_PATTERN.test(entity))
|
|
13
|
+
return;
|
|
14
|
+
const suggestedEntity = entity.slice('use'.length);
|
|
15
|
+
throw new Error(`make:hook now takes the entity, not the hook name — try make:hook ${feature}/${suggestedEntity}.`);
|
|
16
|
+
}
|
|
17
|
+
function makeHook(feature, entity) {
|
|
43
18
|
const ctx = (0, resolve_1.requireFeature)(feature);
|
|
44
19
|
(0, registry_1.assertLayer)(ctx.profile, 'hook', 'make:hook');
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
20
|
+
rejectOldHookName(feature, entity);
|
|
21
|
+
const dir = (0, resolve_1.ensureLayerDir)(ctx, 'hook');
|
|
22
|
+
(0, hook_renderer_1.ensureQueryKeys)(ctx, dir);
|
|
23
|
+
const render = (0, hook_renderer_1.hookRenderer)(ctx);
|
|
24
|
+
const written = (0, write_1.writeSpecFiles)(dir, (0, actions_1.standardActions)(entity), 'hook', (spec, filePath) => render(ctx, spec, entity, filePath));
|
|
25
|
+
if (written > 0) {
|
|
26
|
+
console.log(`✅ Hooks for "${entity}" created at ${dir}`);
|
|
27
|
+
(0, hints_1.hintReactQuery)(ctx.profile);
|
|
48
28
|
}
|
|
49
|
-
|
|
50
|
-
(0, fs_1.writeFileSafe)(filePath, (0, hook_1.renderHook)(ctx, name, entity, filePath));
|
|
51
|
-
console.log(`✅ Hook "${name}" created at ${filePath}`);
|
|
29
|
+
return written > 0;
|
|
52
30
|
}
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.clientRenderer = clientRenderer;
|
|
3
4
|
exports.makeRepository = makeRepository;
|
|
4
5
|
const actions_1 = require("../templates/actions");
|
|
5
6
|
const server_repository_1 = require("../templates/backend/server-repository");
|
|
6
7
|
const client_repository_1 = require("../templates/frontend/client-repository");
|
|
8
|
+
const server_fn_repository_1 = require("../templates/frontend/server-fn-repository");
|
|
7
9
|
const resolve_1 = require("./resolve");
|
|
8
10
|
const sides_1 = require("./sides");
|
|
9
11
|
const write_1 = require("./write");
|
|
12
|
+
// Selected on the profile name, not on `queryHooks`: the two coincide today but mean different
|
|
13
|
+
// things. `queryHooks` picks the hook renderer; this picks whether a server function exists to call.
|
|
14
|
+
function clientRenderer(ctx) {
|
|
15
|
+
return ctx.profile.name === 'tanstack-start' ? server_fn_repository_1.renderServerFnRepository : client_repository_1.renderClientRepository;
|
|
16
|
+
}
|
|
10
17
|
function makeRepository(feature, name, side = 'both') {
|
|
11
18
|
const ctx = (0, resolve_1.requireFeature)(feature);
|
|
12
19
|
let wroteAny = false;
|
|
13
20
|
for (const current of (0, sides_1.resolveSides)(ctx.profile, side, sides_1.REPOSITORY_SIDES)) {
|
|
14
21
|
const dir = (0, resolve_1.ensureLayerDir)(ctx, sides_1.REPOSITORY_SIDES[current]);
|
|
15
|
-
const render = current === 'client' ?
|
|
22
|
+
const render = current === 'client' ? clientRenderer(ctx) : server_repository_1.renderServerRepository;
|
|
16
23
|
const written = (0, write_1.writeSpecFiles)(dir, (0, actions_1.standardActions)(name), 'repository', (spec, filePath) => render(ctx, spec, name, filePath));
|
|
17
24
|
if (written > 0) {
|
|
18
25
|
console.log(`✅ Repositories (${current}) for "${name}" created at ${dir}`);
|
package/dist/init/content.js
CHANGED
|
@@ -15,6 +15,7 @@ const AGENTS_LINES = [
|
|
|
15
15
|
'Rules the generated code follows, and that new code must keep:',
|
|
16
16
|
'',
|
|
17
17
|
'- One file per action per layer. `findActiveUsers` gets `FindActiveUsers.service.ts`, `FindActiveUsers.repository.ts`, and `FindActiveUsers.controller.ts`. It never goes inside `ShowUser.service.ts` or `ListUser.service.ts`.',
|
|
18
|
+
'- Hooks are one file per action too: `<Action><Entity>.hook.ts` exporting `use<Action><Entity>` (for example `useListUser`, `useCreateUser`). There is no combined `use<Entity>.ts`.',
|
|
18
19
|
'- The chain is controller or hook, then service, then repository. Business logic lives in services. Data access lives in repositories. Controllers validate input and call one service.',
|
|
19
20
|
'- Feature folders are kebab-case (`coffee-type`). Entity, action, and class names are PascalCase (`CoffeeType`, `FindActiveUsers`).',
|
|
20
21
|
'- Generated repositories throw until you wire them to your data source. Generated controllers on Node need a line in `<feature>.routes.ts`, and on Nest need registering in `<feature>.module.ts`; the tool prints the exact line.',
|
|
@@ -34,7 +35,7 @@ const SKILL_LINES = [
|
|
|
34
35
|
'',
|
|
35
36
|
'## Detect the stack',
|
|
36
37
|
'',
|
|
37
|
-
'The tool reads `package.json` and prints `Stack: <stack> (detected)` before every command. Stacks: `next-fullstack`, `next-frontend`, `react`, `node` (Express, Fastify, Hono, or none), `nest`. Override with `--stack <name>` if detection is wrong.',
|
|
38
|
+
'The tool reads `package.json` and prints `Stack: <stack> (detected)` before every command. Stacks: `next-fullstack`, `next-frontend`, `react`, `node` (Express, Fastify, Hono, or none), `nest`, `tanstack-start`. Override with `--stack <name>` if detection is wrong.',
|
|
38
39
|
'',
|
|
39
40
|
'## Commands',
|
|
40
41
|
'',
|
|
@@ -49,10 +50,12 @@ const SKILL_LINES = [
|
|
|
49
50
|
'| Five controllers or Next route handlers | `npx domain-driver make:controller users/User` |',
|
|
50
51
|
'| A bespoke operation | `npx domain-driver make:action users/User findActiveUsers` |',
|
|
51
52
|
'| Bespoke operation with a request body | `npx domain-driver make:action users/User archiveUser --with-input --returns one` |',
|
|
52
|
-
'| Component, container, hook | `npx domain-driver make:component users/UserCard`, `make:container users/UserContainer`, `make:hook users/
|
|
53
|
+
'| Component, container, hook | `npx domain-driver make:component users/UserCard`, `make:container users/UserContainer`, `make:hook users/User` |',
|
|
53
54
|
'| Refresh this guidance | `npx domain-driver init` |',
|
|
54
55
|
'',
|
|
55
|
-
'On `next-fullstack`, `make:service` and `make:repository` take `--side client|server|both` (default both).',
|
|
56
|
+
'On `next-fullstack` and `tanstack-start`, `make:service` and `make:repository` take `--side client|server|both` (default both).',
|
|
57
|
+
'',
|
|
58
|
+
'Hooks are one file per action — `<Action><Entity>.hook.ts` exporting `use<Action><Entity>` — never a combined `use<Entity>.ts`. On `tanstack-start` they are TanStack Query hooks backed by a generated `<feature>.keys.ts`; elsewhere they are plain React state.',
|
|
56
59
|
'',
|
|
57
60
|
'## Rules',
|
|
58
61
|
'',
|
|
@@ -68,6 +71,7 @@ const SKILL_LINES = [
|
|
|
68
71
|
'- Next.js: `app/<feature>` or `src/app/<feature>`; route handlers under `app/api/<feature>`.',
|
|
69
72
|
'- React and Node: `src/features/<feature>` or `features/<feature>`.',
|
|
70
73
|
'- Nest: `src/<feature>` with a `<feature>.module.ts`.',
|
|
74
|
+
'- TanStack Start: `src/routes/<feature>` or `routes/<feature>`; every layer folder is prefixed with `-` so the router ignores it, and the entry file is `index.tsx`, not `page.tsx`.',
|
|
71
75
|
];
|
|
72
76
|
exports.AGENTS_SECTION = AGENTS_LINES.join('\n');
|
|
73
77
|
exports.SKILL_CONTENT = `${SKILL_LINES.join('\n')}\n`;
|
package/dist/stack/detect.js
CHANGED
|
@@ -99,6 +99,8 @@ function parseOverride(value) {
|
|
|
99
99
|
function inferStack(cwd, deps) {
|
|
100
100
|
if (deps.has('@nestjs/core'))
|
|
101
101
|
return 'nest';
|
|
102
|
+
if (deps.has('@tanstack/react-start'))
|
|
103
|
+
return 'tanstack-start';
|
|
102
104
|
if (deps.has('next'))
|
|
103
105
|
return hasApiDir(cwd) ? 'next-fullstack' : 'next-frontend';
|
|
104
106
|
if (deps.has('react'))
|
|
@@ -121,5 +123,9 @@ function resolveFeatureRoot(cwd, stack) {
|
|
|
121
123
|
return (0, fs_1.isDirectory)(path.join(cwd, 'src')) ? 'src/features' : 'features';
|
|
122
124
|
case 'nest':
|
|
123
125
|
return 'src';
|
|
126
|
+
case 'tanstack-start':
|
|
127
|
+
if ((0, fs_1.isDirectory)(path.join(cwd, 'src', 'routes')))
|
|
128
|
+
return 'src/routes';
|
|
129
|
+
return (0, fs_1.isDirectory)(path.join(cwd, 'routes')) ? 'routes' : 'src/routes';
|
|
124
130
|
}
|
|
125
131
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.tanstackStart = void 0;
|
|
4
|
+
exports.tanstackStart = Object.freeze({
|
|
5
|
+
name: 'tanstack-start',
|
|
6
|
+
folders: [
|
|
7
|
+
'-components',
|
|
8
|
+
'-containers',
|
|
9
|
+
'-hooks',
|
|
10
|
+
'-client/services',
|
|
11
|
+
'-client/repositories',
|
|
12
|
+
'-server/functions',
|
|
13
|
+
'-server/services',
|
|
14
|
+
'-server/repositories',
|
|
15
|
+
'-schemas',
|
|
16
|
+
'-types',
|
|
17
|
+
],
|
|
18
|
+
layers: [
|
|
19
|
+
'page',
|
|
20
|
+
'component',
|
|
21
|
+
'container',
|
|
22
|
+
'hook',
|
|
23
|
+
'clientService',
|
|
24
|
+
'clientRepository',
|
|
25
|
+
'serverService',
|
|
26
|
+
'serverRepository',
|
|
27
|
+
'controller',
|
|
28
|
+
'schema',
|
|
29
|
+
'types',
|
|
30
|
+
],
|
|
31
|
+
layerDirs: {
|
|
32
|
+
component: '-components',
|
|
33
|
+
container: '-containers',
|
|
34
|
+
hook: '-hooks',
|
|
35
|
+
clientService: '-client/services',
|
|
36
|
+
clientRepository: '-client/repositories',
|
|
37
|
+
serverService: '-server/services',
|
|
38
|
+
serverRepository: '-server/repositories',
|
|
39
|
+
controller: '-server/functions',
|
|
40
|
+
schema: '-schemas',
|
|
41
|
+
types: '-types',
|
|
42
|
+
},
|
|
43
|
+
clientDirective: false,
|
|
44
|
+
serverComponents: false,
|
|
45
|
+
queryHooks: true,
|
|
46
|
+
});
|
package/dist/stack/registry.js
CHANGED
|
@@ -11,12 +11,14 @@ const next_frontend_1 = require("./profiles/next-frontend");
|
|
|
11
11
|
const react_1 = require("./profiles/react");
|
|
12
12
|
const node_1 = require("./profiles/node");
|
|
13
13
|
const nest_1 = require("./profiles/nest");
|
|
14
|
+
const tanstack_start_1 = require("./profiles/tanstack-start");
|
|
14
15
|
const PROFILES = Object.freeze({
|
|
15
16
|
'next-fullstack': next_fullstack_1.nextFullstack,
|
|
16
17
|
'next-frontend': next_frontend_1.nextFrontend,
|
|
17
18
|
react: react_1.react,
|
|
18
19
|
node: node_1.node,
|
|
19
20
|
nest: nest_1.nest,
|
|
21
|
+
'tanstack-start': tanstack_start_1.tanstackStart,
|
|
20
22
|
});
|
|
21
23
|
const LAYER_COMMANDS = Object.freeze({
|
|
22
24
|
component: 'make:component',
|
|
@@ -45,7 +47,8 @@ function layerDir(profile, layer) {
|
|
|
45
47
|
return dir;
|
|
46
48
|
}
|
|
47
49
|
function componentDir(profile, type) {
|
|
48
|
-
|
|
50
|
+
const base = layerDir(profile, 'component');
|
|
51
|
+
return profile.serverComponents ? `${base}/${type}` : base;
|
|
49
52
|
}
|
|
50
53
|
function availableCommands(profile) {
|
|
51
54
|
const commands = profile.layers
|
package/dist/stack/types.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.LAYERS = exports.HTTP_FRAMEWORKS = exports.STACK_NAMES = void 0;
|
|
4
4
|
exports.isStackName = isStackName;
|
|
5
|
-
exports.STACK_NAMES = ['next-fullstack', 'next-frontend', 'react', 'node', 'nest'];
|
|
5
|
+
exports.STACK_NAMES = ['next-fullstack', 'next-frontend', 'react', 'node', 'nest', 'tanstack-start'];
|
|
6
6
|
exports.HTTP_FRAMEWORKS = ['express', 'fastify', 'hono'];
|
|
7
7
|
exports.LAYERS = [
|
|
8
8
|
'page',
|
|
@@ -5,6 +5,7 @@ exports.actionCase = actionCase;
|
|
|
5
5
|
exports.standardAction = standardAction;
|
|
6
6
|
exports.standardActions = standardActions;
|
|
7
7
|
exports.customAction = customAction;
|
|
8
|
+
exports.isQueryAction = isQueryAction;
|
|
8
9
|
const naming_1 = require("../utils/naming");
|
|
9
10
|
exports.ACTIONS = ['List', 'Show', 'Create', 'Update', 'Delete'];
|
|
10
11
|
exports.WRITE_ACTIONS = ['Create', 'Update'];
|
|
@@ -66,3 +67,14 @@ function customAction(entity, actionName, options) {
|
|
|
66
67
|
failure: `Failed to ${camel} ${entity}`,
|
|
67
68
|
});
|
|
68
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Whether a hook renders as a query (auto-fetches, returns data) rather than a mutation
|
|
72
|
+
* (triggered imperatively). `spec.method === 'get'` alone is not enough: a custom action
|
|
73
|
+
* declared `--returns void` is also a GET (no input forces GET regardless of return kind),
|
|
74
|
+
* but it has no payload worth polling for and must not auto-fire on mount. `usesEntityType`
|
|
75
|
+
* is false exactly when the action returns void, so requiring it here routes that case to
|
|
76
|
+
* the mutation branch instead.
|
|
77
|
+
*/
|
|
78
|
+
function isQueryAction(spec) {
|
|
79
|
+
return spec.method === 'get' && spec.usesEntityType;
|
|
80
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderServerFn = renderServerFn;
|
|
4
|
+
const naming_1 = require("../../utils/naming");
|
|
5
|
+
function inputFor(spec) {
|
|
6
|
+
if (spec.schema !== null && spec.usesId) {
|
|
7
|
+
return {
|
|
8
|
+
validator: `z.object({ id: z.string(), data: ${spec.schema}Schema })`,
|
|
9
|
+
call: 'data.id, data.data',
|
|
10
|
+
handlerArg: '{ data }',
|
|
11
|
+
needsZod: true,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
if (spec.schema !== null) {
|
|
15
|
+
return { validator: `${spec.schema}Schema`, call: 'data', handlerArg: '{ data }', needsZod: false };
|
|
16
|
+
}
|
|
17
|
+
if (spec.usesId) {
|
|
18
|
+
return { validator: 'z.string()', call: 'data', handlerArg: '{ data }', needsZod: true };
|
|
19
|
+
}
|
|
20
|
+
return { validator: null, call: '', handlerArg: '', needsZod: false };
|
|
21
|
+
}
|
|
22
|
+
function renderServerFn(ctx, spec, entity, fromFile) {
|
|
23
|
+
const { validator, call, handlerArg, needsZod } = inputFor(spec);
|
|
24
|
+
const method = spec.method === 'get' ? 'GET' : 'POST';
|
|
25
|
+
const servicePath = ctx.importLayer(fromFile, 'serverService', `${spec.name}.service`);
|
|
26
|
+
const imports = ["import { createServerFn } from '@tanstack/react-start';"];
|
|
27
|
+
if (needsZod)
|
|
28
|
+
imports.push("import { z } from 'zod';");
|
|
29
|
+
if (spec.schema !== null) {
|
|
30
|
+
const schemaPath = ctx.importLayer(fromFile, 'schema', `${spec.schema}.schema`);
|
|
31
|
+
imports.push(`import { ${spec.schema}Schema } from '${schemaPath}';`);
|
|
32
|
+
}
|
|
33
|
+
imports.push(`import { ${spec.name}Service } from '${servicePath}';`);
|
|
34
|
+
const validatorLine = validator === null ? '' : `\n .validator(${validator})`;
|
|
35
|
+
return `${imports.join('\n')}
|
|
36
|
+
|
|
37
|
+
const service = new ${spec.name}Service();
|
|
38
|
+
|
|
39
|
+
export const ${(0, naming_1.lowerFirst)(spec.name)} = createServerFn({ method: '${method}' })${validatorLine}
|
|
40
|
+
.handler(async (${handlerArg}) => service.handle(${call}));
|
|
41
|
+
`;
|
|
42
|
+
}
|
|
@@ -4,21 +4,25 @@ exports.renderContainer = renderContainer;
|
|
|
4
4
|
const registry_1 = require("../../stack/registry");
|
|
5
5
|
function renderContainer(ctx, containerName, entity, fromFile) {
|
|
6
6
|
const header = ctx.profile.clientDirective ? "'use client';\n\n" : '';
|
|
7
|
-
const hookName = `
|
|
8
|
-
const hookPath = ctx.importLayer(fromFile, 'hook',
|
|
7
|
+
const hookName = `useList${entity}`;
|
|
8
|
+
const hookPath = ctx.importLayer(fromFile, 'hook', `List${entity}.hook`);
|
|
9
9
|
const componentPath = ctx.importFrom(fromFile, `${(0, registry_1.componentDir)(ctx.profile, 'client')}/${entity}`);
|
|
10
|
+
const query = ctx.profile.queryHooks;
|
|
11
|
+
const loadingField = query ? 'isPending' : 'loading';
|
|
12
|
+
const errorExpression = query ? '{error.message}' : '{error}';
|
|
13
|
+
const items = query ? '(data ?? [])' : 'data';
|
|
10
14
|
return `${header}import { ${hookName} } from '${hookPath}';
|
|
11
15
|
import ${entity} from '${componentPath}';
|
|
12
16
|
|
|
13
17
|
export default function ${containerName}() {
|
|
14
|
-
const {
|
|
18
|
+
const { data, ${loadingField}, error } = ${hookName}();
|
|
15
19
|
|
|
16
|
-
if (
|
|
17
|
-
if (error) return <div>Error: {
|
|
20
|
+
if (${loadingField}) return <div>Loading...</div>;
|
|
21
|
+
if (error) return <div>Error: ${errorExpression}</div>;
|
|
18
22
|
|
|
19
23
|
return (
|
|
20
24
|
<div>
|
|
21
|
-
{items.map((item) => (
|
|
25
|
+
{${items}.map((item) => (
|
|
22
26
|
<${entity} key={item.id} {...item} />
|
|
23
27
|
))}
|
|
24
28
|
</div>
|
|
@@ -1,109 +1,93 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.renderHook = renderHook;
|
|
4
|
+
const naming_1 = require("../../utils/naming");
|
|
4
5
|
const actions_1 = require("../actions");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return `import { ${action}${entity}Service } from '${servicePath}';`;
|
|
9
|
-
}).join('\n');
|
|
6
|
+
const signatures_1 = require("../signatures");
|
|
7
|
+
function payloadType(spec) {
|
|
8
|
+
return spec.returns.replace(/^Promise<(.*)>$/, '$1');
|
|
10
9
|
}
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const createPath = ctx.importLayer(fromFile, 'schema', `Create${entity}.schema`);
|
|
18
|
-
const updatePath = ctx.importLayer(fromFile, 'schema', `Update${entity}.schema`);
|
|
19
|
-
return `${header}import { useState, useEffect, useCallback } from 'react';
|
|
20
|
-
import { ${entity} } from '${typePath}';
|
|
21
|
-
${serviceImports(ctx, fromFile, entity)}
|
|
22
|
-
import { Create${entity} } from '${createPath}';
|
|
23
|
-
import { Update${entity} } from '${updatePath}';
|
|
24
|
-
|
|
25
|
-
${serviceInstances(entity)}
|
|
10
|
+
function header(ctx, spec, entity, fromFile, hooks) {
|
|
11
|
+
const directive = ctx.profile.clientDirective ? "'use client';\n\n" : '';
|
|
12
|
+
const servicePath = ctx.importLayer(fromFile, 'clientService', `${spec.name}.service`);
|
|
13
|
+
const domain = (0, signatures_1.domainImports)(ctx, fromFile, spec, entity);
|
|
14
|
+
return `${directive}import { ${hooks} } from 'react';
|
|
15
|
+
${domain.join('\n')}${domain.length > 0 ? '\n' : ''}import { ${spec.name}Service } from '${servicePath}';
|
|
26
16
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
17
|
+
const service = new ${spec.name}Service();
|
|
18
|
+
`;
|
|
19
|
+
}
|
|
20
|
+
function renderQuery(ctx, spec, entity, fromFile) {
|
|
21
|
+
const type = payloadType(spec);
|
|
22
|
+
const isList = type.endsWith('[]');
|
|
23
|
+
const stateType = isList ? type : `${type} | null`;
|
|
24
|
+
const initial = isList ? '[]' : 'null';
|
|
25
|
+
const deps = spec.usesId ? '[id]' : '[]';
|
|
26
|
+
return `${header(ctx, spec, entity, fromFile, 'useState, useEffect, useCallback')}
|
|
27
|
+
export function use${spec.name}(${spec.params}) {
|
|
28
|
+
const [data, setData] = useState<${stateType}>(${initial});
|
|
30
29
|
const [loading, setLoading] = useState(false);
|
|
31
30
|
const [error, setError] = useState<string | null>(null);
|
|
32
31
|
|
|
33
|
-
const
|
|
34
|
-
setLoading(true);
|
|
35
|
-
setError(null);
|
|
36
|
-
try {
|
|
37
|
-
const data = await listService.handle();
|
|
38
|
-
setItems(data);
|
|
39
|
-
} catch (err: unknown) {
|
|
40
|
-
setError(err instanceof Error ? err.message : 'Failed to fetch');
|
|
41
|
-
} finally {
|
|
42
|
-
setLoading(false);
|
|
43
|
-
}
|
|
44
|
-
}, []);
|
|
45
|
-
|
|
46
|
-
const fetchOne = useCallback(async (id: string) => {
|
|
32
|
+
const refetch = useCallback(async () => {
|
|
47
33
|
setLoading(true);
|
|
48
34
|
setError(null);
|
|
49
35
|
try {
|
|
50
|
-
|
|
51
|
-
setSelected(data);
|
|
36
|
+
setData(await service.handle(${spec.args}));
|
|
52
37
|
} catch (err: unknown) {
|
|
53
|
-
setError(err instanceof Error ? err.message : '
|
|
38
|
+
setError(err instanceof Error ? err.message : '${spec.failure}');
|
|
54
39
|
} finally {
|
|
55
40
|
setLoading(false);
|
|
56
41
|
}
|
|
57
|
-
},
|
|
42
|
+
}, ${deps});
|
|
58
43
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
const created = await createService.handle(data);
|
|
64
|
-
setItems((prev) => [...prev, created]);
|
|
65
|
-
return created;
|
|
66
|
-
} catch (err: unknown) {
|
|
67
|
-
setError(err instanceof Error ? err.message : 'Failed to create');
|
|
68
|
-
return null;
|
|
69
|
-
} finally {
|
|
70
|
-
setLoading(false);
|
|
71
|
-
}
|
|
72
|
-
}, []);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
void refetch();
|
|
46
|
+
}, [refetch]);
|
|
73
47
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
48
|
+
return { data, loading, error, refetch };
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
function renderMutation(ctx, spec, entity, fromFile) {
|
|
53
|
+
const callable = (0, naming_1.lowerFirst)(spec.name);
|
|
54
|
+
const returnsValue = spec.usesEntityType;
|
|
55
|
+
const type = payloadType(spec);
|
|
56
|
+
const callbackType = returnsValue ? `(result: ${type}) => void` : '() => void';
|
|
57
|
+
const body = returnsValue
|
|
58
|
+
? ` const result = await service.handle(${spec.args});
|
|
59
|
+
onSuccess?.(result);
|
|
60
|
+
return result;`
|
|
61
|
+
: ` await service.handle(${spec.args});
|
|
62
|
+
onSuccess?.();`;
|
|
63
|
+
const failure = returnsValue
|
|
64
|
+
? ` setError(err instanceof Error ? err.message : '${spec.failure}');
|
|
65
|
+
return null;`
|
|
66
|
+
: ` setError(err instanceof Error ? err.message : '${spec.failure}');`;
|
|
67
|
+
return `${header(ctx, spec, entity, fromFile, 'useState, useCallback')}
|
|
68
|
+
export function use${spec.name}(options: { onSuccess?: ${callbackType} } = {}) {
|
|
69
|
+
const { onSuccess } = options;
|
|
70
|
+
const [loading, setLoading] = useState(false);
|
|
71
|
+
const [error, setError] = useState<string | null>(null);
|
|
88
72
|
|
|
89
|
-
const
|
|
73
|
+
const ${callable} = useCallback(async (${spec.params}) => {
|
|
90
74
|
setLoading(true);
|
|
91
75
|
setError(null);
|
|
92
76
|
try {
|
|
93
|
-
|
|
94
|
-
setItems((prev) => prev.filter((item) => item.id !== id));
|
|
77
|
+
${body}
|
|
95
78
|
} catch (err: unknown) {
|
|
96
|
-
|
|
79
|
+
${failure}
|
|
97
80
|
} finally {
|
|
98
81
|
setLoading(false);
|
|
99
82
|
}
|
|
100
|
-
}, []);
|
|
101
|
-
|
|
102
|
-
useEffect(() => {
|
|
103
|
-
fetchAll();
|
|
104
|
-
}, [fetchAll]);
|
|
83
|
+
}, [onSuccess]);
|
|
105
84
|
|
|
106
|
-
return {
|
|
85
|
+
return { ${callable}, loading, error };
|
|
107
86
|
}
|
|
108
87
|
`;
|
|
109
88
|
}
|
|
89
|
+
function renderHook(ctx, spec, entity, fromFile) {
|
|
90
|
+
return (0, actions_1.isQueryAction)(spec)
|
|
91
|
+
? renderQuery(ctx, spec, entity, fromFile)
|
|
92
|
+
: renderMutation(ctx, spec, entity, fromFile);
|
|
93
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderQueryHook = renderQueryHook;
|
|
4
|
+
const actions_1 = require("../actions");
|
|
5
|
+
const signatures_1 = require("../signatures");
|
|
6
|
+
const query_keys_1 = require("./query-keys");
|
|
7
|
+
function preamble(ctx, spec, entity, fromFile, imported) {
|
|
8
|
+
const servicePath = ctx.importLayer(fromFile, 'clientService', `${spec.name}.service`);
|
|
9
|
+
const keysPath = ctx.importLayer(fromFile, 'hook', `${ctx.feature}.keys`);
|
|
10
|
+
const domain = (0, signatures_1.domainImports)(ctx, fromFile, spec, entity);
|
|
11
|
+
return `import { ${imported} } from '@tanstack/react-query';
|
|
12
|
+
${domain.join('\n')}${domain.length > 0 ? '\n' : ''}import { ${spec.name}Service } from '${servicePath}';
|
|
13
|
+
import { ${(0, query_keys_1.keysConstant)(ctx.feature)} } from '${keysPath}';
|
|
14
|
+
|
|
15
|
+
const service = new ${spec.name}Service();
|
|
16
|
+
`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A custom GET action (e.g. `FindActiveCats`) has `usesId: false`, just like List. Testing
|
|
20
|
+
* only `usesId` would give it the exact same key as List's `all` — one cache entry shared by
|
|
21
|
+
* two hooks, with whichever observer mounts last winning the `queryFn`. `spec.path === '/'`
|
|
22
|
+
* is what actually distinguishes List from a custom action, so it is the second branch.
|
|
23
|
+
* The `[...all, name]` spread (rather than a bare `[feature, name]`) is deliberate: TanStack
|
|
24
|
+
* Query invalidates by key prefix, so a Create mutation invalidating `keys.all` still
|
|
25
|
+
* invalidates this custom list for free.
|
|
26
|
+
*/
|
|
27
|
+
function queryKey(ctx, spec) {
|
|
28
|
+
const keys = (0, query_keys_1.keysConstant)(ctx.feature);
|
|
29
|
+
if (spec.usesId)
|
|
30
|
+
return `${keys}.detail(id)`;
|
|
31
|
+
if (spec.path === '/')
|
|
32
|
+
return `${keys}.all`;
|
|
33
|
+
return `[...${keys}.all, '${spec.name}']`;
|
|
34
|
+
}
|
|
35
|
+
function renderQuery(ctx, spec, entity, fromFile) {
|
|
36
|
+
return `${preamble(ctx, spec, entity, fromFile, 'useQuery')}
|
|
37
|
+
export function use${spec.name}(${spec.params}) {
|
|
38
|
+
return useQuery({
|
|
39
|
+
queryKey: ${queryKey(ctx, spec)},
|
|
40
|
+
queryFn: (): ${spec.returns} => service.handle(${spec.args}),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
}
|
|
45
|
+
function mutationInput(spec) {
|
|
46
|
+
if (spec.usesId && spec.schema !== null) {
|
|
47
|
+
return {
|
|
48
|
+
signature: `({ id, data }: { id: string; data: ${spec.schema} })`,
|
|
49
|
+
call: 'id, data',
|
|
50
|
+
idFrom: '{ id }',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (spec.usesId)
|
|
54
|
+
return { signature: '(id: string)', call: 'id', idFrom: 'id' };
|
|
55
|
+
if (spec.schema !== null)
|
|
56
|
+
return { signature: `(data: ${spec.schema})`, call: 'data', idFrom: null };
|
|
57
|
+
return { signature: '()', call: '', idFrom: null };
|
|
58
|
+
}
|
|
59
|
+
function renderMutation(ctx, spec, entity, fromFile) {
|
|
60
|
+
const keys = (0, query_keys_1.keysConstant)(ctx.feature);
|
|
61
|
+
const { signature, call, idFrom } = mutationInput(spec);
|
|
62
|
+
const args = idFrom === null ? '()' : `(_result, ${idFrom})`;
|
|
63
|
+
const detail = idFrom === null
|
|
64
|
+
? ''
|
|
65
|
+
: `\n void queryClient.invalidateQueries({ queryKey: ${keys}.detail(id) });`;
|
|
66
|
+
return `${preamble(ctx, spec, entity, fromFile, 'useMutation, useQueryClient')}
|
|
67
|
+
export function use${spec.name}() {
|
|
68
|
+
const queryClient = useQueryClient();
|
|
69
|
+
|
|
70
|
+
return useMutation({
|
|
71
|
+
mutationFn: ${signature}: ${spec.returns} => service.handle(${call}),
|
|
72
|
+
onSuccess: ${args} => {
|
|
73
|
+
void queryClient.invalidateQueries({ queryKey: ${keys}.all });${detail}
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
function renderQueryHook(ctx, spec, entity, fromFile) {
|
|
80
|
+
return (0, actions_1.isQueryAction)(spec)
|
|
81
|
+
? renderQuery(ctx, spec, entity, fromFile)
|
|
82
|
+
: renderMutation(ctx, spec, entity, fromFile);
|
|
83
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.keysConstant = keysConstant;
|
|
4
|
+
exports.renderQueryKeys = renderQueryKeys;
|
|
5
|
+
const naming_1 = require("../../utils/naming");
|
|
6
|
+
function keysConstant(feature) {
|
|
7
|
+
return `${(0, naming_1.lowerFirst)((0, naming_1.toPascalCase)(feature))}Keys`;
|
|
8
|
+
}
|
|
9
|
+
function renderQueryKeys(feature) {
|
|
10
|
+
return `export const ${keysConstant(feature)} = {
|
|
11
|
+
all: ['${feature}'] as const,
|
|
12
|
+
detail: (id: string) => ['${feature}', id] as const,
|
|
13
|
+
};
|
|
14
|
+
`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderRoute = renderRoute;
|
|
4
|
+
function renderRoute(ctx, entity, fromFile, withContainer) {
|
|
5
|
+
const head = `import { createFileRoute } from '@tanstack/react-router';`;
|
|
6
|
+
const routeBlock = `export const Route = createFileRoute('/${ctx.feature}/')({
|
|
7
|
+
component: ${entity}Page,
|
|
8
|
+
});`;
|
|
9
|
+
if (!withContainer) {
|
|
10
|
+
return `${head}
|
|
11
|
+
|
|
12
|
+
${routeBlock}
|
|
13
|
+
|
|
14
|
+
function ${entity}Page() {
|
|
15
|
+
return (
|
|
16
|
+
<div>
|
|
17
|
+
<h1>${entity}</h1>
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
}
|
|
23
|
+
const containerPath = ctx.importLayer(fromFile, 'container', `${entity}Container`);
|
|
24
|
+
return `${head}
|
|
25
|
+
import ${entity}Container from '${containerPath}';
|
|
26
|
+
|
|
27
|
+
${routeBlock}
|
|
28
|
+
|
|
29
|
+
function ${entity}Page() {
|
|
30
|
+
return <${entity}Container />;
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderServerFnRepository = renderServerFnRepository;
|
|
4
|
+
const naming_1 = require("../../utils/naming");
|
|
5
|
+
const signatures_1 = require("../signatures");
|
|
6
|
+
function callArgument(spec) {
|
|
7
|
+
if (spec.usesId && spec.schema !== null)
|
|
8
|
+
return '({ data: { id, data } })';
|
|
9
|
+
if (spec.usesId)
|
|
10
|
+
return '({ data: id })';
|
|
11
|
+
if (spec.schema !== null)
|
|
12
|
+
return '({ data })';
|
|
13
|
+
return '()';
|
|
14
|
+
}
|
|
15
|
+
function renderServerFnRepository(ctx, spec, entity, fromFile) {
|
|
16
|
+
const fnName = (0, naming_1.lowerFirst)(spec.name);
|
|
17
|
+
const fnPath = ctx.importLayer(fromFile, 'controller', `${spec.name}.fn`);
|
|
18
|
+
const imports = [...(0, signatures_1.domainImports)(ctx, fromFile, spec, entity), `import { ${fnName} } from '${fnPath}';`];
|
|
19
|
+
return `${imports.join('\n')}
|
|
20
|
+
|
|
21
|
+
export class ${spec.name}Repository {
|
|
22
|
+
async handle(${spec.params}): ${spec.returns} {
|
|
23
|
+
return ${fnName}${callArgument(spec)};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "domain-driver",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CLI scaffolding tool for domain-driven feature folders in Next.js, React, Node, and
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "CLI scaffolding tool for domain-driven feature folders in Next.js, React, Node, NestJS, and TanStack Start projects, with per-action files, bespoke actions, and agent guidance",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"domain-driver": "./dist/index.js"
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"build": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\" && tsc",
|
|
19
19
|
"test": "vitest run",
|
|
20
20
|
"test:coverage": "vitest run --coverage",
|
|
21
|
+
"typecheck:tanstack-fixture": "node scripts/typecheck-tanstack-fixture.js",
|
|
21
22
|
"postinstall": "node ./scripts/postinstall.js"
|
|
22
23
|
},
|
|
23
24
|
"keywords": [
|
|
@@ -30,7 +31,9 @@
|
|
|
30
31
|
"node",
|
|
31
32
|
"express",
|
|
32
33
|
"fastify",
|
|
33
|
-
"hono"
|
|
34
|
+
"hono",
|
|
35
|
+
"tanstack",
|
|
36
|
+
"tanstack-start"
|
|
34
37
|
],
|
|
35
38
|
"author": "Isaac Hatilima",
|
|
36
39
|
"license": "MIT",
|