domain-driver 0.3.1 → 0.4.1
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 +125 -24
- package/dist/cli.js +5 -4
- 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/commands/target.js +7 -3
- package/dist/init/content.js +9 -4
- package/dist/stack/detect.js +57 -11
- 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
|
|
|
@@ -23,12 +23,13 @@ npx domain-driver make:feature <feature>[/<Entity>]
|
|
|
23
23
|
Every command reads your `package.json` once and prints the stack it found before writing anything:
|
|
24
24
|
|
|
25
25
|
```
|
|
26
|
-
Stack: next-fullstack (detected)
|
|
26
|
+
Stack: next-fullstack (detected), root: src/app
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
| Stack | Detected when | Features live in |
|
|
30
30
|
|---|---|---|
|
|
31
|
-
| `nest` | `@nestjs/core` is a dependency | `src/<feature>` |
|
|
31
|
+
| `nest` | `@nestjs/core` is a dependency | `src/features/<feature>` if `src/features` exists, otherwise `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>` |
|
|
@@ -42,23 +43,51 @@ Override detection with `--stack`:
|
|
|
42
43
|
domain-driver --stack nest make:feature cat -a
|
|
43
44
|
```
|
|
44
45
|
|
|
46
|
+
### Choosing where features live
|
|
47
|
+
|
|
48
|
+
The "Features live in" column is a convention, not a rule. Two overrides take precedence over it, in this order.
|
|
49
|
+
|
|
50
|
+
`--root` wins over everything, and suits one-off scaffolding:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
domain-driver --root src/modules make:feature billing/Billing -a
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For a project you work in daily, set it once in `package.json` instead of retyping the flag on every command:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"domainDriver": {
|
|
61
|
+
"featureRoot": "src/modules"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Either way the stack line tells you which one won, so you can see where files will land before they land:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
Stack: nest (detected), root: src/modules (package.json)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The root must be a relative path inside the project. An absolute path or one containing `..` is rejected rather than quietly normalised.
|
|
73
|
+
|
|
45
74
|
---
|
|
46
75
|
|
|
47
76
|
## What each stack generates
|
|
48
77
|
|
|
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 |
|
|
78
|
+
| Layer | next-fullstack | next-frontend | react | node | nest | tanstack-start |
|
|
79
|
+
|---|---|---|---|---|---|---|
|
|
80
|
+
| entry file (`page.tsx`; `index.tsx` on tanstack-start) | yes | yes | | | | yes |
|
|
81
|
+
| components | client + server | client + server | flat | | | flat |
|
|
82
|
+
| containers, hooks | yes | yes | yes | | | yes (Query hooks) |
|
|
83
|
+
| client services + repositories | `client/` (fetch) | top level (fetch) | top level (fetch) | | | `-client/` (calls server functions, no fetch) |
|
|
84
|
+
| server services + repositories (database stubs) | `server/` | | | yes | yes | `-server/` |
|
|
85
|
+
| controllers | `app/api/<feature>/` route handlers | | | five files + routes file | five files | server functions (`-server/functions/*.fn.ts`) |
|
|
86
|
+
| module | | | | | yes | |
|
|
87
|
+
| DTOs (`nestjs-zod`) | | | | | yes | |
|
|
88
|
+
| schemas (Zod), types | yes | yes | yes | yes | yes | yes |
|
|
60
89
|
|
|
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.
|
|
90
|
+
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
91
|
|
|
63
92
|
---
|
|
64
93
|
|
|
@@ -69,7 +98,7 @@ Every layer command takes one target, `<feature>/<Name>`: the feature folder on
|
|
|
69
98
|
### `make:feature`
|
|
70
99
|
|
|
71
100
|
```bash
|
|
72
|
-
domain-driver make:feature users # folders + .gitkeep, plus page.tsx (Next) or the module (Nest)
|
|
101
|
+
domain-driver make:feature users # folders + .gitkeep, plus page.tsx (Next), index.tsx (TanStack Start), or the module (Nest)
|
|
73
102
|
domain-driver make:feature users/User -a # every layer for the detected stack, entity User
|
|
74
103
|
```
|
|
75
104
|
|
|
@@ -95,8 +124,9 @@ Scaffolds a bespoke operation as its own files, so it never lands inside `ShowUs
|
|
|
95
124
|
|---|---|
|
|
96
125
|
| `node` | service, repository, controller for the detected framework, plus the line to add above the `/:id` routes in `<feature>.routes.ts` printed |
|
|
97
126
|
| `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
|
|
127
|
+
| `next-fullstack` | server service and repository, client service and repository, a hook, `app/api/<feature>/<slug>/route.ts` |
|
|
128
|
+
| `next-frontend`, `react` | client service and repository calling `/api/<feature>/<slug>`, and a hook |
|
|
129
|
+
| `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
130
|
|
|
101
131
|
### `make:controller`
|
|
102
132
|
|
|
@@ -104,7 +134,7 @@ Scaffolds a bespoke operation as its own files, so it never lands inside `ShowUs
|
|
|
104
134
|
domain-driver make:controller users/User
|
|
105
135
|
```
|
|
106
136
|
|
|
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.
|
|
137
|
+
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
138
|
|
|
109
139
|
### `make:service` and `make:repository`
|
|
110
140
|
|
|
@@ -113,7 +143,7 @@ domain-driver make:service users/User [--side client|server|both]
|
|
|
113
143
|
domain-driver make:repository users/User [--side client|server|both]
|
|
114
144
|
```
|
|
115
145
|
|
|
116
|
-
`--side` matters on `next-fullstack`, where both sides exist. Default is `both`.
|
|
146
|
+
`--side` matters on `next-fullstack` and `tanstack-start`, where both sides exist. Default is `both`.
|
|
117
147
|
|
|
118
148
|
### `make:schema`
|
|
119
149
|
|
|
@@ -129,11 +159,27 @@ Writes `CreateUser.schema.ts` and `UpdateUser.schema.ts`. On Nest it also writes
|
|
|
129
159
|
domain-driver make:types users/User
|
|
130
160
|
domain-driver make:component users/UserCard [client|server]
|
|
131
161
|
domain-driver make:container users/UserContainer
|
|
132
|
-
domain-driver make:hook users/
|
|
162
|
+
domain-driver make:hook users/User
|
|
133
163
|
```
|
|
134
164
|
|
|
135
165
|
Component, container, and hook commands fail with a clear message on backend stacks, and `server` components are rejected on React.
|
|
136
166
|
|
|
167
|
+
`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.
|
|
168
|
+
|
|
169
|
+
Because the five hooks no longer share state, containers wire them together. On `react`, `next-frontend`, and `next-fullstack`, where hooks hold plain state:
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
const { data, loading, error, refetch } = useListCat();
|
|
173
|
+
const { createCat } = useCreateCat({ onSuccess: refetch });
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
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:
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
const { data, isPending, error } = useListCat();
|
|
180
|
+
const { mutate: createCat } = useCreateCat();
|
|
181
|
+
```
|
|
182
|
+
|
|
137
183
|
### `init`
|
|
138
184
|
|
|
139
185
|
```bash
|
|
@@ -192,14 +238,42 @@ Install `nestjs-zod` and register `ZodValidationPipe` as `APP_PIPE` once in your
|
|
|
192
238
|
|
|
193
239
|
---
|
|
194
240
|
|
|
241
|
+
## Example: `make:feature coffee-type -a` on TanStack Start
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
src/routes/coffee-type/
|
|
245
|
+
├── index.tsx createFileRoute route — the entry, not page.tsx
|
|
246
|
+
├── -components/
|
|
247
|
+
│ └── CoffeeType.tsx
|
|
248
|
+
├── -containers/
|
|
249
|
+
│ └── CoffeeTypeContainer.tsx
|
|
250
|
+
├── -hooks/ coffee-type.keys.ts, plus five hook files (one per action)
|
|
251
|
+
├── -client/
|
|
252
|
+
│ ├── services/ (five files)
|
|
253
|
+
│ └── repositories/ (five files, call the server functions directly)
|
|
254
|
+
├── -server/
|
|
255
|
+
│ ├── functions/ (five *.fn.ts files, built with createServerFn)
|
|
256
|
+
│ ├── services/ (five files)
|
|
257
|
+
│ └── repositories/ (five files, database-agnostic stubs)
|
|
258
|
+
├── -schemas/
|
|
259
|
+
│ ├── CreateCoffeeType.schema.ts
|
|
260
|
+
│ └── UpdateCoffeeType.schema.ts
|
|
261
|
+
└── -types/
|
|
262
|
+
└── CoffeeType.types.ts
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
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.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
195
269
|
## Philosophy
|
|
196
270
|
|
|
197
271
|
Everything for a feature lives in one folder, and every file does one thing.
|
|
198
272
|
|
|
199
|
-
- **repositories** — data access only. Client-side repositories call your API; server-side repositories call your database.
|
|
273
|
+
- **repositories** — data access only. Client-side repositories call your API (on TanStack Start, the server function directly); server-side repositories call your database.
|
|
200
274
|
- **services** — business logic, one class per action.
|
|
201
|
-
- **controllers** — HTTP in, service call, HTTP out, one file per action.
|
|
202
|
-
- **hooks** —
|
|
275
|
+
- **controllers** — HTTP in, service call, HTTP out, one file per action (server functions on TanStack Start).
|
|
276
|
+
- **hooks** — state and side effects, one hook per action, calls services (TanStack Query on TanStack Start, plain React state elsewhere).
|
|
203
277
|
- **containers** — wire hooks into UI.
|
|
204
278
|
- **components** — presentational UI.
|
|
205
279
|
- **schemas** — Zod validation for create and update; the update body never carries the id, it comes from the path.
|
|
@@ -231,6 +305,29 @@ The cache lives in `~/.cache/domain-driver` (or `$XDG_CACHE_HOME/domain-driver`)
|
|
|
231
305
|
|
|
232
306
|
---
|
|
233
307
|
|
|
308
|
+
## Upgrading from 0.4.0
|
|
309
|
+
|
|
310
|
+
Two changes affect existing projects.
|
|
311
|
+
|
|
312
|
+
On NestJS, a project that already has a `src/features` directory now scaffolds into it instead of directly into `src`. If you keep features in `src` you are unaffected, since the behaviour is opt-in by that directory existing. To pin either choice explicitly, set `domainDriver.featureRoot` in `package.json`.
|
|
313
|
+
|
|
314
|
+
The entity half of a `<feature>/<Entity>` target must now be PascalCase. `make:schema users/user` previously produced `Listuser.service.ts` exporting `ListuserService`; it now fails with a message telling you to pass `User`.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Upgrading from 0.3.x
|
|
319
|
+
|
|
320
|
+
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.
|
|
321
|
+
|
|
322
|
+
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:
|
|
323
|
+
|
|
324
|
+
```tsx
|
|
325
|
+
const { data, loading, error, refetch } = useListCat();
|
|
326
|
+
const { createCat } = useCreateCat({ onSuccess: refetch });
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
This release also adds a sixth stack, `tanstack-start`, detected from `@tanstack/react-start` — see the TanStack Start example above.
|
|
330
|
+
|
|
234
331
|
## Upgrading from 0.1.0
|
|
235
332
|
|
|
236
333
|
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 +352,8 @@ npm test
|
|
|
255
352
|
npm run test:coverage
|
|
256
353
|
```
|
|
257
354
|
|
|
355
|
+
`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.
|
|
356
|
+
|
|
258
357
|
---
|
|
259
358
|
|
|
260
359
|
## Roadmap
|
|
@@ -264,6 +363,8 @@ npm run test:coverage
|
|
|
264
363
|
- [x] Per-action files in every layer
|
|
265
364
|
- [x] Bespoke actions with make:action
|
|
266
365
|
- [x] Agent guidance with init and a guarded postinstall
|
|
366
|
+
- [x] TanStack Start stack, with server functions and per-action query hooks
|
|
367
|
+
- [x] Per-action hooks on every stack that has them, replacing the single combined hook
|
|
267
368
|
- [ ] Config file — override stack and feature root per project
|
|
268
369
|
- [ ] Configurable API base URL for client repositories
|
|
269
370
|
- [ ] ORM-aware server repositories
|
package/dist/cli.js
CHANGED
|
@@ -78,7 +78,8 @@ function createProgram(deps) {
|
|
|
78
78
|
.name('domain-driver')
|
|
79
79
|
.description('CLI scaffolding tool for domain-driven feature folders in Next.js, React, Node, and NestJS projects')
|
|
80
80
|
.version(deps.current)
|
|
81
|
-
.option('--stack <name>', `Override stack detection (${types_2.STACK_NAMES.join(', ')})`)
|
|
81
|
+
.option('--stack <name>', `Override stack detection (${types_2.STACK_NAMES.join(', ')})`)
|
|
82
|
+
.option('--root <dir>', 'Override where feature folders are created, for example src/features');
|
|
82
83
|
let pendingNotice = Promise.resolve(null);
|
|
83
84
|
program.hook('preAction', (_thisCommand, actionCommand) => {
|
|
84
85
|
const name = actionCommand.name();
|
|
@@ -93,8 +94,8 @@ function createProgram(deps) {
|
|
|
93
94
|
}
|
|
94
95
|
if (SKIP_DETECTION.has(name))
|
|
95
96
|
return;
|
|
96
|
-
const { stack } = program.opts();
|
|
97
|
-
deps.log((0, detect_1.describeStack)((0, detect_1.detectStack)(stack)));
|
|
97
|
+
const { stack, root } = program.opts();
|
|
98
|
+
deps.log((0, detect_1.describeStack)((0, detect_1.detectStack)(stack, root)));
|
|
98
99
|
});
|
|
99
100
|
program.hook('postAction', async () => {
|
|
100
101
|
const notice = await pendingNotice;
|
|
@@ -126,7 +127,7 @@ function createProgram(deps) {
|
|
|
126
127
|
});
|
|
127
128
|
program
|
|
128
129
|
.command('make:hook <target>')
|
|
129
|
-
.description('Scaffold
|
|
130
|
+
.description('Scaffold single-responsibility hook files inside an existing feature (<feature>/<Entity>)')
|
|
130
131
|
.action((target) => {
|
|
131
132
|
const { feature, name } = (0, target_1.parseTarget)(target);
|
|
132
133
|
(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}`);
|