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 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 NestJS projects. It detects your stack and generates only the layers that stack needs, one file per action.
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 (fetch) | `client/` | top level | top level | | |
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.schema.ts`.
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/useUser
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** — React state and side effects, calls services.
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 a custom hook inside an existing feature (<feature>/<useName>)')
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);
@@ -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' ? client_repository_1.renderClientRepository : server_repository_1.renderServerRepository;
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 controllerFile = path.join((0, resolve_1.ensureLayerDir)(ctx, 'controller'), `${spec.name}.controller.ts`);
111
- const render = ctx.profile.name === 'nest' ? nest_1.renderNestController : node_1.renderNodeController;
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 === 'nest' ? nest_1.renderNestController : node_1.renderNodeController;
55
- const written = (0, write_1.writeSpecFiles)(dir, (0, actions_1.standardActions)(name), 'controller', (spec, filePath) => render(ctx, spec, name, filePath));
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(`✅ Controllers for "${name}" created at ${dir}`);
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
  }
@@ -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, `use${entity}`, entity);
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 filePath = path.join(ctx.featureDir, 'page.tsx');
100
- (0, fs_1.writeFileSafe)(filePath, (0, page_1.renderPage)(ctx, entity, filePath, all));
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`);
@@ -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
+ }
@@ -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 hook_1 = require("../templates/frontend/hook");
40
- const fs_1 = require("../utils/fs");
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
- function makeHook(feature, name, pascalName) {
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
- const filePath = path.join((0, resolve_1.ensureLayerDir)(ctx, 'hook'), `${name}.ts`);
46
- if ((0, fs_1.fileExists)(filePath)) {
47
- throw new Error(`Hook "${name}" already exists at ${filePath}`);
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
- const entity = pascalName ?? name.replace(/^use/, '');
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' ? client_repository_1.renderClientRepository : server_repository_1.renderServerRepository;
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}`);