domain-driver 0.4.0 → 0.5.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 +96 -3
- package/dist/cli.js +15 -8
- package/dist/commands/feature.js +2 -0
- package/dist/commands/hints.js +0 -13
- package/dist/commands/register.js +105 -0
- package/dist/commands/target.js +7 -3
- package/dist/init/content.js +4 -2
- package/dist/nest/register.js +135 -0
- package/dist/nest/registration.js +129 -0
- package/dist/nest/root-module.js +57 -0
- package/dist/nest/typescript.js +27 -0
- package/dist/stack/detect.js +72 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,12 +23,12 @@ 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
32
|
| `tanstack-start` | `@tanstack/react-start` is a dependency | `src/routes/<feature>` or `routes/<feature>` |
|
|
33
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>` |
|
|
34
34
|
| `next-frontend` | `next` is a dependency, no api directory | `app/<feature>` or `src/app/<feature>` |
|
|
@@ -43,6 +43,34 @@ Override detection with `--stack`:
|
|
|
43
43
|
domain-driver --stack nest make:feature cat -a
|
|
44
44
|
```
|
|
45
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
|
+
|
|
46
74
|
---
|
|
47
75
|
|
|
48
76
|
## What each stack generates
|
|
@@ -238,6 +266,49 @@ Every layer directory carries a `-` prefix so TanStack Router excludes it from r
|
|
|
238
266
|
|
|
239
267
|
---
|
|
240
268
|
|
|
269
|
+
## Nest module registration
|
|
270
|
+
|
|
271
|
+
On NestJS, generated classes are wired into the module files that have to know about them. `make:feature` adds the feature module to your root module, and `make:controller`, `make:service`, `make:repository` and `make:action` add their classes to `<feature>.module.ts`.
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
✅ Registered AssetsModule in src/app.module.ts
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
The edit is made through the TypeScript compiler API, borrowed from your own project rather than bundled, and applied as a text insertion at AST-computed positions. Your comments, import order and formatting survive untouched — only the two inserted lines are new. Registering the same class twice does nothing.
|
|
278
|
+
|
|
279
|
+
A bespoke action's controller is inserted **before** `Show<Entity>Controller` rather than appended, because Nest matches routes in declaration order and a custom `GET /actives` would otherwise be swallowed by `GET /:id`.
|
|
280
|
+
|
|
281
|
+
### When it does not edit
|
|
282
|
+
|
|
283
|
+
If the root module cannot be found, TypeScript cannot be resolved, the file has no `@Module` decorator, or the edited result would not parse, nothing is written and the lines are printed for you to paste:
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
ℹ️ Did not edit src/app.module.ts (no @Module decorator with an object argument was found). Add by hand:
|
|
287
|
+
import { AssetsModule } from './features/assets/assets.module';
|
|
288
|
+
imports: [ ..., AssetsModule ]
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
The command still succeeds, because the files it generated were still generated.
|
|
292
|
+
|
|
293
|
+
### Turning it off, and pointing it somewhere else
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
domain-driver --no-auto-register make:feature assets/Asset -a
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
```json
|
|
300
|
+
{
|
|
301
|
+
"domainDriver": {
|
|
302
|
+
"autoRegister": false,
|
|
303
|
+
"rootModule": "src/core/root.module.ts"
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
`autoRegister: false` restores the old behaviour, where nothing but newly generated files is ever written. `rootModule` names the root module for projects where `src/app.module.ts` and `app.module.ts` are both wrong.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
241
312
|
## Philosophy
|
|
242
313
|
|
|
243
314
|
Everything for a feature lives in one folder, and every file does one thing.
|
|
@@ -277,11 +348,33 @@ The cache lives in `~/.cache/domain-driver` (or `$XDG_CACHE_HOME/domain-driver`)
|
|
|
277
348
|
|
|
278
349
|
---
|
|
279
350
|
|
|
351
|
+
## Upgrading from 0.4.x
|
|
352
|
+
|
|
353
|
+
On NestJS, scaffolding now **edits two files it did not create**: your root module and each feature's module. Every earlier version only ever created files. The edit is surgical and verified — see [Nest module registration](#nest-module-registration) for what it does and when it refuses — but if you want the old write-only guarantee back:
|
|
354
|
+
|
|
355
|
+
```json
|
|
356
|
+
{ "domainDriver": { "autoRegister": false } }
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Nothing changes for any other stack.
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Upgrading from 0.4.0
|
|
364
|
+
|
|
365
|
+
Two changes affect existing projects.
|
|
366
|
+
|
|
367
|
+
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`.
|
|
368
|
+
|
|
369
|
+
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`.
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
280
373
|
## Upgrading from 0.3.x
|
|
281
374
|
|
|
282
375
|
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
376
|
|
|
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:
|
|
377
|
+
domain-driver never overwrites a generated 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
378
|
|
|
286
379
|
```tsx
|
|
287
380
|
const { data, loading, error, refetch } = useListCat();
|
package/dist/cli.js
CHANGED
|
@@ -43,7 +43,8 @@ const component_1 = require("./commands/component");
|
|
|
43
43
|
const container_1 = require("./commands/container");
|
|
44
44
|
const controller_1 = require("./commands/controller");
|
|
45
45
|
const feature_1 = require("./commands/feature");
|
|
46
|
-
const
|
|
46
|
+
const register_1 = require("./commands/register");
|
|
47
|
+
const resolve_1 = require("./commands/resolve");
|
|
47
48
|
const hook_1 = require("./commands/hook");
|
|
48
49
|
const repository_1 = require("./commands/repository");
|
|
49
50
|
const schema_1 = require("./commands/schema");
|
|
@@ -78,7 +79,9 @@ function createProgram(deps) {
|
|
|
78
79
|
.name('domain-driver')
|
|
79
80
|
.description('CLI scaffolding tool for domain-driven feature folders in Next.js, React, Node, and NestJS projects')
|
|
80
81
|
.version(deps.current)
|
|
81
|
-
.option('--stack <name>', `Override stack detection (${types_2.STACK_NAMES.join(', ')})`)
|
|
82
|
+
.option('--stack <name>', `Override stack detection (${types_2.STACK_NAMES.join(', ')})`)
|
|
83
|
+
.option('--root <dir>', 'Override where feature folders are created, for example src/features')
|
|
84
|
+
.option('--no-auto-register', 'Do not edit Nest module files; print what to add instead');
|
|
82
85
|
let pendingNotice = Promise.resolve(null);
|
|
83
86
|
program.hook('preAction', (_thisCommand, actionCommand) => {
|
|
84
87
|
const name = actionCommand.name();
|
|
@@ -93,8 +96,12 @@ function createProgram(deps) {
|
|
|
93
96
|
}
|
|
94
97
|
if (SKIP_DETECTION.has(name))
|
|
95
98
|
return;
|
|
96
|
-
const { stack } = program.opts();
|
|
97
|
-
|
|
99
|
+
const { stack, root } = program.opts();
|
|
100
|
+
// commander defaults a --no-x flag to true, which would shadow the package.json key,
|
|
101
|
+
// so only an explicitly passed flag counts as an override.
|
|
102
|
+
const passed = program.getOptionValueSource('autoRegister') === 'cli';
|
|
103
|
+
const autoRegister = passed ? program.opts().autoRegister : undefined;
|
|
104
|
+
deps.log((0, detect_1.describeStack)((0, detect_1.detectStack)({ stack, root, autoRegister })));
|
|
98
105
|
});
|
|
99
106
|
program.hook('postAction', async () => {
|
|
100
107
|
const notice = await pendingNotice;
|
|
@@ -139,7 +146,7 @@ function createProgram(deps) {
|
|
|
139
146
|
const { feature, name } = (0, target_1.parseTarget)(target);
|
|
140
147
|
const wrote = (0, service_1.makeService)(feature, name, (0, sides_1.parseSide)(options.side));
|
|
141
148
|
if (wrote)
|
|
142
|
-
(0,
|
|
149
|
+
(0, register_1.registerClasses)((0, resolve_1.requireFeature)(feature), 'service', (0, register_1.standardActionNames)(name));
|
|
143
150
|
});
|
|
144
151
|
program
|
|
145
152
|
.command('make:repository <target>')
|
|
@@ -149,7 +156,7 @@ function createProgram(deps) {
|
|
|
149
156
|
const { feature, name } = (0, target_1.parseTarget)(target);
|
|
150
157
|
const wrote = (0, repository_1.makeRepository)(feature, name, (0, sides_1.parseSide)(options.side));
|
|
151
158
|
if (wrote)
|
|
152
|
-
(0,
|
|
159
|
+
(0, register_1.registerClasses)((0, resolve_1.requireFeature)(feature), 'repository', (0, register_1.standardActionNames)(name));
|
|
153
160
|
});
|
|
154
161
|
program
|
|
155
162
|
.command('make:controller <target>')
|
|
@@ -158,7 +165,7 @@ function createProgram(deps) {
|
|
|
158
165
|
const { feature, name } = (0, target_1.parseTarget)(target);
|
|
159
166
|
const wrote = (0, controller_1.makeController)(feature, name);
|
|
160
167
|
if (wrote)
|
|
161
|
-
(0,
|
|
168
|
+
(0, register_1.registerClasses)((0, resolve_1.requireFeature)(feature), 'controller', (0, register_1.standardActionNames)(name));
|
|
162
169
|
});
|
|
163
170
|
program
|
|
164
171
|
.command('make:action <target> <action>')
|
|
@@ -171,7 +178,7 @@ function createProgram(deps) {
|
|
|
171
178
|
const wrote = (0, action_1.makeAction)(feature, name, action, { withInput: options.withInput, returns });
|
|
172
179
|
if (wrote) {
|
|
173
180
|
const { pascal } = (0, actions_1.actionCase)(action);
|
|
174
|
-
(0,
|
|
181
|
+
(0, register_1.registerCustomAction)((0, resolve_1.requireFeature)(feature), pascal, name);
|
|
175
182
|
}
|
|
176
183
|
});
|
|
177
184
|
program
|
package/dist/commands/feature.js
CHANGED
|
@@ -46,6 +46,7 @@ const container_1 = require("./container");
|
|
|
46
46
|
const controller_1 = require("./controller");
|
|
47
47
|
const hook_1 = require("./hook");
|
|
48
48
|
const repository_1 = require("./repository");
|
|
49
|
+
const register_1 = require("./register");
|
|
49
50
|
const resolve_1 = require("./resolve");
|
|
50
51
|
const schema_1 = require("./schema");
|
|
51
52
|
const service_1 = require("./service");
|
|
@@ -61,6 +62,7 @@ async function makeFeature(name, all = false, entityName) {
|
|
|
61
62
|
if (all)
|
|
62
63
|
scaffoldLayers(ctx, entity);
|
|
63
64
|
writeEntryFile(ctx, entity, all);
|
|
65
|
+
(0, register_1.registerFeatureModule)(ctx, entity);
|
|
64
66
|
if (all)
|
|
65
67
|
console.log(`✅ All files scaffolded for "${name}"`);
|
|
66
68
|
}
|
package/dist/commands/hints.js
CHANGED
|
@@ -3,10 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.resetHints = resetHints;
|
|
4
4
|
exports.hintNestjsZod = hintNestjsZod;
|
|
5
5
|
exports.hintReactQuery = hintReactQuery;
|
|
6
|
-
exports.standardClassNames = standardClassNames;
|
|
7
|
-
exports.hintRegisterInModule = hintRegisterInModule;
|
|
8
|
-
const detect_1 = require("../stack/detect");
|
|
9
|
-
const actions_1 = require("../templates/actions");
|
|
10
6
|
let nestjsZodHinted = false;
|
|
11
7
|
let reactQueryHinted = false;
|
|
12
8
|
function resetHints() {
|
|
@@ -27,12 +23,3 @@ function hintReactQuery(profile) {
|
|
|
27
23
|
console.log('ℹ️ Hooks use TanStack Query. Install it: npm install @tanstack/react-query');
|
|
28
24
|
console.log(' Then wrap your app in a QueryClientProvider.');
|
|
29
25
|
}
|
|
30
|
-
function standardClassNames(name, suffix) {
|
|
31
|
-
return actions_1.ACTIONS.map((action) => `${action}${name}${suffix}`);
|
|
32
|
-
}
|
|
33
|
-
function hintRegisterInModule(feature, classNames, note) {
|
|
34
|
-
if ((0, detect_1.detectStack)().stack !== 'nest')
|
|
35
|
-
return;
|
|
36
|
-
const suffix = note !== undefined ? ` (${note})` : '';
|
|
37
|
-
console.log(`ℹ️ Register ${classNames.join(', ')} in ${feature}.module.ts${suffix}`);
|
|
38
|
-
}
|
|
@@ -0,0 +1,105 @@
|
|
|
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.KINDS = void 0;
|
|
37
|
+
exports.registerClasses = registerClasses;
|
|
38
|
+
exports.standardActionNames = standardActionNames;
|
|
39
|
+
exports.registerCustomAction = registerCustomAction;
|
|
40
|
+
exports.registerFeatureModule = registerFeatureModule;
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const registration_1 = require("../nest/registration");
|
|
43
|
+
const root_module_1 = require("../nest/root-module");
|
|
44
|
+
const registry_1 = require("../stack/registry");
|
|
45
|
+
const actions_1 = require("../templates/actions");
|
|
46
|
+
const fs_1 = require("../utils/fs");
|
|
47
|
+
exports.KINDS = Object.freeze({
|
|
48
|
+
controller: { layer: 'controller', suffix: 'Controller', fileSuffix: 'controller', property: 'controllers' },
|
|
49
|
+
service: { layer: 'serverService', suffix: 'Service', fileSuffix: 'service', property: 'providers' },
|
|
50
|
+
repository: { layer: 'serverRepository', suffix: 'Repository', fileSuffix: 'repository', property: 'providers' },
|
|
51
|
+
});
|
|
52
|
+
function featureModule(ctx) {
|
|
53
|
+
return path.join(ctx.featureDir, `${ctx.feature}.module.ts`);
|
|
54
|
+
}
|
|
55
|
+
function entriesFor(ctx, kind, names) {
|
|
56
|
+
const dir = path.join(ctx.featureDir, (0, registry_1.layerDir)(ctx.profile, kind.layer));
|
|
57
|
+
return names
|
|
58
|
+
.map((name) => ({
|
|
59
|
+
identifier: `${name}${kind.suffix}`,
|
|
60
|
+
definedIn: path.join(dir, `${name}.${kind.fileSuffix}.ts`),
|
|
61
|
+
property: kind.property,
|
|
62
|
+
}))
|
|
63
|
+
.filter((entry) => (0, fs_1.fileExists)(entry.definedIn));
|
|
64
|
+
}
|
|
65
|
+
/** Registers generated classes in the feature's own module. Nest is the only stack with one. */
|
|
66
|
+
function registerClasses(ctx, kind, actionNames) {
|
|
67
|
+
if (ctx.profile.name !== 'nest')
|
|
68
|
+
return;
|
|
69
|
+
(0, registration_1.applyRegistrations)(featureModule(ctx), entriesFor(ctx, exports.KINDS[kind], actionNames), ctx.stack.autoRegister);
|
|
70
|
+
}
|
|
71
|
+
function standardActionNames(entity) {
|
|
72
|
+
return actions_1.ACTIONS.map((action) => `${action}${entity}`);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A bespoke action's controller must be declared ahead of Show<Entity>Controller: Nest matches
|
|
76
|
+
* routes in declaration order, and a custom GET path would otherwise be swallowed by `/:id`.
|
|
77
|
+
*/
|
|
78
|
+
function registerCustomAction(ctx, action, entity) {
|
|
79
|
+
if (ctx.profile.name !== 'nest')
|
|
80
|
+
return;
|
|
81
|
+
const entries = [
|
|
82
|
+
{ ...entriesFor(ctx, exports.KINDS.controller, [action])[0], before: `Show${entity}Controller` },
|
|
83
|
+
entriesFor(ctx, exports.KINDS.service, [action])[0],
|
|
84
|
+
entriesFor(ctx, exports.KINDS.repository, [action])[0],
|
|
85
|
+
];
|
|
86
|
+
(0, registration_1.applyRegistrations)(featureModule(ctx), entries, ctx.stack.autoRegister);
|
|
87
|
+
}
|
|
88
|
+
/** Registers the feature's module in the application's root module. */
|
|
89
|
+
function registerFeatureModule(ctx, entity) {
|
|
90
|
+
if (ctx.profile.name !== 'nest')
|
|
91
|
+
return;
|
|
92
|
+
const moduleFile = featureModule(ctx);
|
|
93
|
+
if (!(0, fs_1.fileExists)(moduleFile))
|
|
94
|
+
return;
|
|
95
|
+
const rootModule = (0, root_module_1.findRootModule)(process.cwd(), ctx.stack.featureRoot, ctx.stack.rootModule);
|
|
96
|
+
if (rootModule === null) {
|
|
97
|
+
if (!ctx.stack.autoRegister)
|
|
98
|
+
return;
|
|
99
|
+
console.log(`ℹ️ No root module found, so ${entity}Module was not registered. Add it by hand, or name the file with "domainDriver": { "rootModule": "..." } in package.json.`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (path.resolve(rootModule) === path.resolve(moduleFile))
|
|
103
|
+
return;
|
|
104
|
+
(0, registration_1.applyRegistrations)(rootModule, [{ identifier: `${entity}Module`, definedIn: moduleFile, property: 'imports' }], ctx.stack.autoRegister);
|
|
105
|
+
}
|
package/dist/commands/target.js
CHANGED
|
@@ -3,11 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.parseTarget = parseTarget;
|
|
4
4
|
exports.parseFeatureTarget = parseFeatureTarget;
|
|
5
5
|
const naming_1 = require("../utils/naming");
|
|
6
|
-
const NAME_PATTERN = /^[A-
|
|
6
|
+
const NAME_PATTERN = /^[A-Z][A-Za-z0-9]*$/;
|
|
7
|
+
const HOOK_NAME_PATTERN = /^use[A-Z]/;
|
|
7
8
|
function validateName(name) {
|
|
8
|
-
if (
|
|
9
|
-
|
|
9
|
+
if (NAME_PATTERN.test(name))
|
|
10
|
+
return;
|
|
11
|
+
if (HOOK_NAME_PATTERN.test(name)) {
|
|
12
|
+
throw new Error(`Name "${name}" is a hook name. Pass the entity instead, for example ${name.slice(3)}.`);
|
|
10
13
|
}
|
|
14
|
+
throw new Error(`Name "${name}" must be PascalCase, for example User.`);
|
|
11
15
|
}
|
|
12
16
|
function parseTarget(value) {
|
|
13
17
|
const parts = value.split('/');
|
package/dist/init/content.js
CHANGED
|
@@ -35,7 +35,9 @@ const SKILL_LINES = [
|
|
|
35
35
|
'',
|
|
36
36
|
'## Detect the stack',
|
|
37
37
|
'',
|
|
38
|
-
'The tool reads `package.json` and prints `Stack: <stack> (detected)
|
|
38
|
+
'The tool reads `package.json` and prints `Stack: <stack> (detected), root: <dir>` 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.',
|
|
39
|
+
'Where features are written can be overridden too: `--root <dir>` for one command, or a `domainDriver.featureRoot` key in `package.json` for the project. Read the root off the stack line rather than assuming the convention.',
|
|
40
|
+
'On Nest the tool registers what it generates: the feature module in the root module, and controllers, services and repositories in `<feature>.module.ts`. Do not add those by hand — it prints what it registered, and prints the lines to paste when it cannot. A bespoke action controller is placed before `Show<Entity>Controller` so its route is not swallowed by `/:id`.',
|
|
39
41
|
'',
|
|
40
42
|
'## Commands',
|
|
41
43
|
'',
|
|
@@ -70,7 +72,7 @@ const SKILL_LINES = [
|
|
|
70
72
|
'',
|
|
71
73
|
'- Next.js: `app/<feature>` or `src/app/<feature>`; route handlers under `app/api/<feature>`.',
|
|
72
74
|
'- React and Node: `src/features/<feature>` or `features/<feature>`.',
|
|
73
|
-
'- Nest: `src/<feature>` with a `<feature>.module.ts`.',
|
|
75
|
+
'- Nest: `src/features/<feature>` when `src/features` exists, otherwise `src/<feature>`, with a `<feature>.module.ts`.',
|
|
74
76
|
'- 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`.',
|
|
75
77
|
];
|
|
76
78
|
exports.AGENTS_SECTION = AGENTS_LINES.join('\n');
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerInModule = registerInModule;
|
|
4
|
+
function bail(reason) {
|
|
5
|
+
return { status: 'bailed', reason };
|
|
6
|
+
}
|
|
7
|
+
function parse(ts, source) {
|
|
8
|
+
return ts.createSourceFile('module.ts', source, ts.ScriptTarget.Latest, true);
|
|
9
|
+
}
|
|
10
|
+
function moduleArgument(ts, file) {
|
|
11
|
+
for (const statement of file.statements) {
|
|
12
|
+
if (!ts.isClassDeclaration(statement) || !ts.canHaveDecorators(statement))
|
|
13
|
+
continue;
|
|
14
|
+
for (const decorator of ts.getDecorators(statement) ?? []) {
|
|
15
|
+
const call = decorator.expression;
|
|
16
|
+
if (!ts.isCallExpression(call))
|
|
17
|
+
continue;
|
|
18
|
+
if (!ts.isIdentifier(call.expression) || call.expression.text !== 'Module')
|
|
19
|
+
continue;
|
|
20
|
+
const [argument] = call.arguments;
|
|
21
|
+
if (argument !== undefined && ts.isObjectLiteralExpression(argument))
|
|
22
|
+
return argument;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
function propertyArray(ts, object, name) {
|
|
28
|
+
for (const property of object.properties) {
|
|
29
|
+
if (!ts.isPropertyAssignment(property))
|
|
30
|
+
continue;
|
|
31
|
+
const key = property.name;
|
|
32
|
+
const text = ts.isIdentifier(key) || ts.isStringLiteral(key) ? key.text : null;
|
|
33
|
+
if (text !== name)
|
|
34
|
+
continue;
|
|
35
|
+
return ts.isArrayLiteralExpression(property.initializer) ? property.initializer : 'not-array';
|
|
36
|
+
}
|
|
37
|
+
return 'missing';
|
|
38
|
+
}
|
|
39
|
+
function contains(ts, array, identifier) {
|
|
40
|
+
return array.elements.some((element) => ts.isIdentifier(element) && element.text === identifier);
|
|
41
|
+
}
|
|
42
|
+
function newlineOf(source) {
|
|
43
|
+
return source.includes('\r\n') ? '\r\n' : '\n';
|
|
44
|
+
}
|
|
45
|
+
function alreadyImported(ts, file, identifier) {
|
|
46
|
+
return file.statements.filter(ts.isImportDeclaration).some((declaration) => {
|
|
47
|
+
const bindings = declaration.importClause?.namedBindings;
|
|
48
|
+
if (bindings === undefined || !ts.isNamedImports(bindings))
|
|
49
|
+
return false;
|
|
50
|
+
return bindings.elements.some((element) => element.name.text === identifier);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function indentAt(source, position) {
|
|
54
|
+
const lineStart = source.lastIndexOf('\n', position - 1) + 1;
|
|
55
|
+
return /^[ \t]*/.exec(source.slice(lineStart, position))?.[0] ?? '';
|
|
56
|
+
}
|
|
57
|
+
function importEdit(ts, source, file, request) {
|
|
58
|
+
// A second `import { X }` for an identifier already bound is a duplicate declaration, which
|
|
59
|
+
// is a type error rather than a syntax error, so nothing downstream would catch it.
|
|
60
|
+
if (alreadyImported(ts, file, request.identifier))
|
|
61
|
+
return null;
|
|
62
|
+
const eol = newlineOf(source);
|
|
63
|
+
const line = `import { ${request.identifier} } from '${request.importPath}';`;
|
|
64
|
+
const imports = file.statements.filter(ts.isImportDeclaration);
|
|
65
|
+
if (imports.length === 0)
|
|
66
|
+
return { at: 0, text: `${line}${eol}` };
|
|
67
|
+
return { at: imports[imports.length - 1].end, text: `${eol}${line}` };
|
|
68
|
+
}
|
|
69
|
+
function elementEdit(ts, source, file, array, request) {
|
|
70
|
+
const identifier = request.identifier;
|
|
71
|
+
const eol = newlineOf(source);
|
|
72
|
+
if (array.elements.length === 0) {
|
|
73
|
+
const outer = indentAt(source, array.getStart(file));
|
|
74
|
+
return { at: array.end - 1, text: `${eol}${outer} ${identifier},${eol}${outer}` };
|
|
75
|
+
}
|
|
76
|
+
const anchor = request.before === undefined
|
|
77
|
+
? undefined
|
|
78
|
+
: array.elements.find((element) => ts.isIdentifier(element) && element.text === request.before);
|
|
79
|
+
const last = array.elements[array.elements.length - 1];
|
|
80
|
+
const singleLine = !source.slice(array.getStart(file), last.end).includes('\n');
|
|
81
|
+
if (anchor !== undefined) {
|
|
82
|
+
const at = anchor.getStart(file);
|
|
83
|
+
return { at, text: singleLine ? `${identifier}, ` : `${identifier},${eol}${indentAt(source, at)}` };
|
|
84
|
+
}
|
|
85
|
+
const text = singleLine ? `, ${identifier}` : `,${eol}${indentAt(source, last.getStart(file))}${identifier}`;
|
|
86
|
+
return { at: last.end, text };
|
|
87
|
+
}
|
|
88
|
+
function apply(source, edits) {
|
|
89
|
+
return [...edits]
|
|
90
|
+
.sort((a, b) => b.at - a.at)
|
|
91
|
+
.reduce((text, edit) => `${text.slice(0, edit.at)}${edit.text}${text.slice(edit.at)}`, source);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The edit is only returned if the result both parses cleanly and reads back as registered.
|
|
95
|
+
* A wrong offset produces a file that fails one of those, and the caller keeps the original.
|
|
96
|
+
*/
|
|
97
|
+
function parses(ts, source) {
|
|
98
|
+
const diagnostics = ts.transpileModule(source, {
|
|
99
|
+
reportDiagnostics: true,
|
|
100
|
+
compilerOptions: { target: ts.ScriptTarget.Latest },
|
|
101
|
+
}).diagnostics;
|
|
102
|
+
return (diagnostics ?? []).length === 0;
|
|
103
|
+
}
|
|
104
|
+
function verify(ts, source, request) {
|
|
105
|
+
if (!parses(ts, source))
|
|
106
|
+
return false;
|
|
107
|
+
const file = parse(ts, source);
|
|
108
|
+
const object = moduleArgument(ts, file);
|
|
109
|
+
if (object === null)
|
|
110
|
+
return false;
|
|
111
|
+
const array = propertyArray(ts, object, request.property);
|
|
112
|
+
if (array === 'missing' || array === 'not-array')
|
|
113
|
+
return false;
|
|
114
|
+
return contains(ts, array, request.identifier);
|
|
115
|
+
}
|
|
116
|
+
function registerInModule(ts, source, request) {
|
|
117
|
+
if (!parses(ts, source))
|
|
118
|
+
return bail('the file does not parse');
|
|
119
|
+
const file = parse(ts, source);
|
|
120
|
+
const object = moduleArgument(ts, file);
|
|
121
|
+
if (object === null)
|
|
122
|
+
return bail('no @Module decorator with an object argument was found');
|
|
123
|
+
const array = propertyArray(ts, object, request.property);
|
|
124
|
+
if (array === 'missing')
|
|
125
|
+
return bail(`the @Module decorator has no "${request.property}" property`);
|
|
126
|
+
if (array === 'not-array')
|
|
127
|
+
return bail(`"${request.property}" is not an array literal`);
|
|
128
|
+
if (contains(ts, array, request.identifier))
|
|
129
|
+
return { status: 'already-registered' };
|
|
130
|
+
const edits = [importEdit(ts, source, file, request), elementEdit(ts, source, file, array, request)];
|
|
131
|
+
const edited = apply(source, edits.filter((edit) => edit !== null));
|
|
132
|
+
if (!verify(ts, edited, request))
|
|
133
|
+
return bail('the edited file did not read back as valid');
|
|
134
|
+
return { status: 'edited', source: edited };
|
|
135
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
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.applyRegistrations = applyRegistrations;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const imports_1 = require("../utils/imports");
|
|
40
|
+
const register_1 = require("./register");
|
|
41
|
+
const typescript_1 = require("./typescript");
|
|
42
|
+
/**
|
|
43
|
+
* Edits one module file in place, or explains what to add by hand and changes nothing.
|
|
44
|
+
* A bail is never a failure: the generated files exist either way, so the caller succeeds.
|
|
45
|
+
*/
|
|
46
|
+
function applyRegistrations(moduleFile, entries, enabled) {
|
|
47
|
+
if (entries.length === 0)
|
|
48
|
+
return;
|
|
49
|
+
if (!enabled)
|
|
50
|
+
return explain(moduleFile, entries, 'automatic registration is off');
|
|
51
|
+
if (!fs.existsSync(moduleFile))
|
|
52
|
+
return explain(moduleFile, entries, 'the module file does not exist');
|
|
53
|
+
const ts = (0, typescript_1.loadTypeScript)(process.cwd());
|
|
54
|
+
if (ts === null)
|
|
55
|
+
return explain(moduleFile, entries, 'typescript could not be resolved from this project');
|
|
56
|
+
let source;
|
|
57
|
+
try {
|
|
58
|
+
source = fs.readFileSync(moduleFile, 'utf-8');
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return explain(moduleFile, entries, 'the file could not be read');
|
|
62
|
+
}
|
|
63
|
+
const added = [];
|
|
64
|
+
const outstanding = [];
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
const result = (0, register_1.registerInModule)(ts, source, {
|
|
67
|
+
identifier: entry.identifier,
|
|
68
|
+
importPath: (0, imports_1.resolveImport)(moduleFile, entry.definedIn),
|
|
69
|
+
property: entry.property,
|
|
70
|
+
before: entry.before,
|
|
71
|
+
});
|
|
72
|
+
if (result.status === 'bailed') {
|
|
73
|
+
return explain(moduleFile, [...outstanding, ...entries.slice(entries.indexOf(entry))], result.reason);
|
|
74
|
+
}
|
|
75
|
+
if (result.status === 'already-registered')
|
|
76
|
+
continue;
|
|
77
|
+
source = result.source;
|
|
78
|
+
added.push(entry.identifier);
|
|
79
|
+
}
|
|
80
|
+
if (added.length === 0)
|
|
81
|
+
return;
|
|
82
|
+
if (!write(moduleFile, source))
|
|
83
|
+
return explain(moduleFile, entries, 'the file could not be written');
|
|
84
|
+
console.log(`✅ Registered ${added.join(', ')} in ${relative(moduleFile)}`);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Writes through a sibling temp file and renames. `writeFileSync` truncates before writing, so a
|
|
88
|
+
* failure partway through would leave the file that boots the application cut in half.
|
|
89
|
+
* A same-directory rename is atomic, which makes that outcome unreachable.
|
|
90
|
+
*/
|
|
91
|
+
function write(moduleFile, source) {
|
|
92
|
+
const temporary = `${moduleFile}.domain-driver.tmp`;
|
|
93
|
+
try {
|
|
94
|
+
fs.writeFileSync(temporary, source);
|
|
95
|
+
fs.renameSync(temporary, moduleFile);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
try {
|
|
100
|
+
fs.rmSync(temporary, { force: true });
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// The temp file is already gone, or unremovable for the same reason the write failed.
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function explain(moduleFile, entries, reason) {
|
|
109
|
+
if (entries.length === 0)
|
|
110
|
+
return;
|
|
111
|
+
console.log(`ℹ️ Did not edit ${relative(moduleFile)} (${reason}). Add by hand:`);
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
console.log(` import { ${entry.identifier} } from '${(0, imports_1.resolveImport)(moduleFile, entry.definedIn)}';`);
|
|
114
|
+
}
|
|
115
|
+
const byProperty = new Map();
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
byProperty.set(entry.property, [...(byProperty.get(entry.property) ?? []), entry]);
|
|
118
|
+
}
|
|
119
|
+
for (const [property, group] of byProperty) {
|
|
120
|
+
console.log(` ${property}: [ ..., ${group.map(placement).join(', ')} ]`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** An ordered entry has to say where it goes, or the pasted line reproduces the bug it prevents. */
|
|
124
|
+
function placement(entry) {
|
|
125
|
+
return entry.before === undefined ? entry.identifier : `${entry.identifier}, ${entry.before}`;
|
|
126
|
+
}
|
|
127
|
+
function relative(target) {
|
|
128
|
+
return path.relative(process.cwd(), target) || target;
|
|
129
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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.findRootModule = findRootModule;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const fs_1 = require("../utils/fs");
|
|
39
|
+
/**
|
|
40
|
+
* Probes the conventional locations for a Nest root module. A project that keeps it
|
|
41
|
+
* anywhere else names it with the `domainDriver.rootModule` key instead of being guessed at.
|
|
42
|
+
*/
|
|
43
|
+
function findRootModule(cwd, featureRoot, configured) {
|
|
44
|
+
if (configured !== null) {
|
|
45
|
+
const target = path.join(cwd, configured);
|
|
46
|
+
return (0, fs_1.fileExists)(target) ? target : null;
|
|
47
|
+
}
|
|
48
|
+
// Most specific first: a monorepo's stray root-level src/app.module.ts must not win over
|
|
49
|
+
// the one sitting beside the feature root it actually belongs to.
|
|
50
|
+
const candidates = [path.join(path.dirname(featureRoot), 'app.module.ts'), 'src/app.module.ts', 'app.module.ts'];
|
|
51
|
+
for (const candidate of candidates) {
|
|
52
|
+
const target = path.join(cwd, candidate);
|
|
53
|
+
if ((0, fs_1.fileExists)(target))
|
|
54
|
+
return target;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadTypeScript = loadTypeScript;
|
|
4
|
+
/**
|
|
5
|
+
* Borrows the consuming project's own TypeScript rather than shipping one. Every TypeScript
|
|
6
|
+
* Nest project has it — it cannot compile without one — so this keeps a scaffolding CLI free
|
|
7
|
+
* of a heavy runtime dependency. Returning null is a normal outcome, not an error: the caller
|
|
8
|
+
* falls back to printing the lines for the user to paste.
|
|
9
|
+
*/
|
|
10
|
+
function loadTypeScript(cwd, resolve = require.resolve, load = require) {
|
|
11
|
+
try {
|
|
12
|
+
const candidate = load(resolve('typescript', { paths: [cwd] }));
|
|
13
|
+
return isTypeScript(candidate) ? candidate : null;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function isTypeScript(candidate) {
|
|
20
|
+
if (typeof candidate !== 'object' || candidate === null)
|
|
21
|
+
return false;
|
|
22
|
+
const api = candidate;
|
|
23
|
+
return (typeof api.createSourceFile === 'function' &&
|
|
24
|
+
typeof api.transpileModule === 'function' &&
|
|
25
|
+
typeof api.getDecorators === 'function' &&
|
|
26
|
+
typeof api.canHaveDecorators === 'function');
|
|
27
|
+
}
|
package/dist/stack/detect.js
CHANGED
|
@@ -41,41 +41,53 @@ const path = __importStar(require("path"));
|
|
|
41
41
|
const fs_1 = require("../utils/fs");
|
|
42
42
|
const types_1 = require("./types");
|
|
43
43
|
const API_DIRS = ['app/api', 'src/app/api', 'pages/api', 'src/pages/api'];
|
|
44
|
+
const ROOT_LABELS = Object.freeze({
|
|
45
|
+
detected: '',
|
|
46
|
+
flag: ' (--root)',
|
|
47
|
+
config: ' (package.json)',
|
|
48
|
+
});
|
|
44
49
|
let cached;
|
|
45
50
|
function resetStackCache() {
|
|
46
51
|
cached = undefined;
|
|
47
52
|
}
|
|
48
|
-
function detectStack(
|
|
53
|
+
function detectStack(options = {}) {
|
|
49
54
|
if (cached)
|
|
50
55
|
return cached;
|
|
51
56
|
const cwd = process.cwd();
|
|
57
|
+
const override = options.stack;
|
|
52
58
|
const overridden = override !== undefined;
|
|
53
|
-
const
|
|
59
|
+
const pkg = readPackageJson(cwd, overridden);
|
|
60
|
+
const deps = dependencyNames(pkg);
|
|
54
61
|
const stack = overridden ? parseOverride(override) : inferStack(cwd, deps);
|
|
62
|
+
const root = resolveRoot(cwd, stack, pkg, options.root);
|
|
55
63
|
const result = Object.freeze({
|
|
56
64
|
stack,
|
|
57
65
|
source: overridden ? 'override' : 'detected',
|
|
58
66
|
httpFramework: stack === 'node' ? detectHttpFramework(deps) : null,
|
|
59
|
-
featureRoot:
|
|
67
|
+
featureRoot: root.featureRoot,
|
|
68
|
+
featureRootSource: root.source,
|
|
60
69
|
hasNestjsZod: deps.has('nestjs-zod'),
|
|
70
|
+
rootModule: configuredRootModule(pkg),
|
|
71
|
+
autoRegister: options.autoRegister ?? configuredAutoRegister(pkg),
|
|
61
72
|
});
|
|
62
73
|
cached = result;
|
|
63
74
|
return result;
|
|
64
75
|
}
|
|
65
76
|
function describeStack(stack) {
|
|
66
77
|
const base = `Stack: ${stack.stack} (${stack.source})`;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return `${base}, http: ${stack.httpFramework ?? 'none'}`;
|
|
78
|
+
const http = stack.stack === 'node' ? `, http: ${stack.httpFramework ?? 'none'}` : '';
|
|
79
|
+
return `${base}${http}, root: ${stack.featureRoot}${ROOT_LABELS[stack.featureRootSource]}`;
|
|
70
80
|
}
|
|
71
|
-
function
|
|
81
|
+
function readPackageJson(cwd, optional) {
|
|
72
82
|
const pkgPath = path.join(cwd, 'package.json');
|
|
73
83
|
if (!fs.existsSync(pkgPath)) {
|
|
74
84
|
if (optional)
|
|
75
|
-
return
|
|
85
|
+
return {};
|
|
76
86
|
throw new Error(`No package.json found in ${cwd}. Run domain-driver from your project root, or pass --stack <name>.`);
|
|
77
87
|
}
|
|
78
|
-
|
|
88
|
+
return parsePackageJson(pkgPath);
|
|
89
|
+
}
|
|
90
|
+
function dependencyNames(pkg) {
|
|
79
91
|
return new Set([
|
|
80
92
|
...Object.keys(pkg.dependencies ?? {}),
|
|
81
93
|
...Object.keys(pkg.devDependencies ?? {}),
|
|
@@ -113,7 +125,56 @@ function hasApiDir(cwd) {
|
|
|
113
125
|
function detectHttpFramework(deps) {
|
|
114
126
|
return types_1.HTTP_FRAMEWORKS.find((name) => deps.has(name)) ?? null;
|
|
115
127
|
}
|
|
116
|
-
function
|
|
128
|
+
function resolveRoot(cwd, stack, pkg, flag) {
|
|
129
|
+
if (flag !== undefined) {
|
|
130
|
+
return { featureRoot: validateRoot(flag, '--root'), source: 'flag' };
|
|
131
|
+
}
|
|
132
|
+
const configured = configuredRoot(pkg);
|
|
133
|
+
if (configured !== null) {
|
|
134
|
+
return { featureRoot: configured, source: 'config' };
|
|
135
|
+
}
|
|
136
|
+
return { featureRoot: conventionalRoot(cwd, stack), source: 'detected' };
|
|
137
|
+
}
|
|
138
|
+
function configuredRootModule(pkg) {
|
|
139
|
+
const value = pkg.domainDriver?.rootModule;
|
|
140
|
+
if (value === undefined)
|
|
141
|
+
return null;
|
|
142
|
+
if (typeof value !== 'string') {
|
|
143
|
+
throw new Error('package.json "domainDriver.rootModule" must be a string, for example "src/app.module.ts".');
|
|
144
|
+
}
|
|
145
|
+
return validateRoot(value, 'package.json');
|
|
146
|
+
}
|
|
147
|
+
function configuredAutoRegister(pkg) {
|
|
148
|
+
const value = pkg.domainDriver?.autoRegister;
|
|
149
|
+
if (value === undefined)
|
|
150
|
+
return true;
|
|
151
|
+
if (typeof value !== 'boolean') {
|
|
152
|
+
throw new Error('package.json "domainDriver.autoRegister" must be true or false.');
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
function configuredRoot(pkg) {
|
|
157
|
+
const value = pkg.domainDriver?.featureRoot;
|
|
158
|
+
if (value === undefined)
|
|
159
|
+
return null;
|
|
160
|
+
if (typeof value !== 'string') {
|
|
161
|
+
throw new Error('package.json "domainDriver.featureRoot" must be a string, for example "src/features".');
|
|
162
|
+
}
|
|
163
|
+
return validateRoot(value, 'package.json');
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The root becomes a filesystem path under the project, so an absolute path or any
|
|
167
|
+
* `..` segment would write outside it. Both are rejected rather than normalised away.
|
|
168
|
+
*/
|
|
169
|
+
function validateRoot(value, source) {
|
|
170
|
+
const normalized = value.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
171
|
+
const escapes = normalized === '' || path.isAbsolute(value) || normalized.split('/').includes('..');
|
|
172
|
+
if (escapes) {
|
|
173
|
+
throw new Error(`Feature root "${value}" from ${source} must be a relative path inside the project, for example src/features.`);
|
|
174
|
+
}
|
|
175
|
+
return normalized;
|
|
176
|
+
}
|
|
177
|
+
function conventionalRoot(cwd, stack) {
|
|
117
178
|
switch (stack) {
|
|
118
179
|
case 'next-fullstack':
|
|
119
180
|
case 'next-frontend':
|
|
@@ -122,7 +183,7 @@ function resolveFeatureRoot(cwd, stack) {
|
|
|
122
183
|
case 'node':
|
|
123
184
|
return (0, fs_1.isDirectory)(path.join(cwd, 'src')) ? 'src/features' : 'features';
|
|
124
185
|
case 'nest':
|
|
125
|
-
return 'src';
|
|
186
|
+
return (0, fs_1.isDirectory)(path.join(cwd, 'src', 'features')) ? 'src/features' : 'src';
|
|
126
187
|
case 'tanstack-start':
|
|
127
188
|
if ((0, fs_1.isDirectory)(path.join(cwd, 'src', 'routes')))
|
|
128
189
|
return 'src/routes';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "domain-driver",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
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": {
|