domain-driver 0.4.0 → 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 +40 -2
- package/dist/cli.js +4 -3
- package/dist/commands/target.js +7 -3
- package/dist/init/content.js +3 -2
- package/dist/stack/detect.js +51 -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
|
|
@@ -277,6 +305,16 @@ The cache lives in `~/.cache/domain-driver` (or `$XDG_CACHE_HOME/domain-driver`)
|
|
|
277
305
|
|
|
278
306
|
---
|
|
279
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
|
+
|
|
280
318
|
## Upgrading from 0.3.x
|
|
281
319
|
|
|
282
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.
|
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;
|
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,8 @@ 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.',
|
|
39
40
|
'',
|
|
40
41
|
'## Commands',
|
|
41
42
|
'',
|
|
@@ -70,7 +71,7 @@ const SKILL_LINES = [
|
|
|
70
71
|
'',
|
|
71
72
|
'- Next.js: `app/<feature>` or `src/app/<feature>`; route handlers under `app/api/<feature>`.',
|
|
72
73
|
'- React and Node: `src/features/<feature>` or `features/<feature>`.',
|
|
73
|
-
'- Nest: `src/<feature>` with a `<feature>.module.ts`.',
|
|
74
|
+
'- Nest: `src/features/<feature>` when `src/features` exists, otherwise `src/<feature>`, with a `<feature>.module.ts`.',
|
|
74
75
|
'- 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
76
|
];
|
|
76
77
|
exports.AGENTS_SECTION = AGENTS_LINES.join('\n');
|
package/dist/stack/detect.js
CHANGED
|
@@ -41,22 +41,30 @@ 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(override) {
|
|
53
|
+
function detectStack(override, rootOverride) {
|
|
49
54
|
if (cached)
|
|
50
55
|
return cached;
|
|
51
56
|
const cwd = process.cwd();
|
|
52
57
|
const overridden = override !== undefined;
|
|
53
|
-
const
|
|
58
|
+
const pkg = readPackageJson(cwd, overridden);
|
|
59
|
+
const deps = dependencyNames(pkg);
|
|
54
60
|
const stack = overridden ? parseOverride(override) : inferStack(cwd, deps);
|
|
61
|
+
const root = resolveRoot(cwd, stack, pkg, rootOverride);
|
|
55
62
|
const result = Object.freeze({
|
|
56
63
|
stack,
|
|
57
64
|
source: overridden ? 'override' : 'detected',
|
|
58
65
|
httpFramework: stack === 'node' ? detectHttpFramework(deps) : null,
|
|
59
|
-
featureRoot:
|
|
66
|
+
featureRoot: root.featureRoot,
|
|
67
|
+
featureRootSource: root.source,
|
|
60
68
|
hasNestjsZod: deps.has('nestjs-zod'),
|
|
61
69
|
});
|
|
62
70
|
cached = result;
|
|
@@ -64,18 +72,19 @@ function detectStack(override) {
|
|
|
64
72
|
}
|
|
65
73
|
function describeStack(stack) {
|
|
66
74
|
const base = `Stack: ${stack.stack} (${stack.source})`;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return `${base}, http: ${stack.httpFramework ?? 'none'}`;
|
|
75
|
+
const http = stack.stack === 'node' ? `, http: ${stack.httpFramework ?? 'none'}` : '';
|
|
76
|
+
return `${base}${http}, root: ${stack.featureRoot}${ROOT_LABELS[stack.featureRootSource]}`;
|
|
70
77
|
}
|
|
71
|
-
function
|
|
78
|
+
function readPackageJson(cwd, optional) {
|
|
72
79
|
const pkgPath = path.join(cwd, 'package.json');
|
|
73
80
|
if (!fs.existsSync(pkgPath)) {
|
|
74
81
|
if (optional)
|
|
75
|
-
return
|
|
82
|
+
return {};
|
|
76
83
|
throw new Error(`No package.json found in ${cwd}. Run domain-driver from your project root, or pass --stack <name>.`);
|
|
77
84
|
}
|
|
78
|
-
|
|
85
|
+
return parsePackageJson(pkgPath);
|
|
86
|
+
}
|
|
87
|
+
function dependencyNames(pkg) {
|
|
79
88
|
return new Set([
|
|
80
89
|
...Object.keys(pkg.dependencies ?? {}),
|
|
81
90
|
...Object.keys(pkg.devDependencies ?? {}),
|
|
@@ -113,7 +122,38 @@ function hasApiDir(cwd) {
|
|
|
113
122
|
function detectHttpFramework(deps) {
|
|
114
123
|
return types_1.HTTP_FRAMEWORKS.find((name) => deps.has(name)) ?? null;
|
|
115
124
|
}
|
|
116
|
-
function
|
|
125
|
+
function resolveRoot(cwd, stack, pkg, flag) {
|
|
126
|
+
if (flag !== undefined) {
|
|
127
|
+
return { featureRoot: validateRoot(flag, '--root'), source: 'flag' };
|
|
128
|
+
}
|
|
129
|
+
const configured = configuredRoot(pkg);
|
|
130
|
+
if (configured !== null) {
|
|
131
|
+
return { featureRoot: configured, source: 'config' };
|
|
132
|
+
}
|
|
133
|
+
return { featureRoot: conventionalRoot(cwd, stack), source: 'detected' };
|
|
134
|
+
}
|
|
135
|
+
function configuredRoot(pkg) {
|
|
136
|
+
const value = pkg.domainDriver?.featureRoot;
|
|
137
|
+
if (value === undefined)
|
|
138
|
+
return null;
|
|
139
|
+
if (typeof value !== 'string') {
|
|
140
|
+
throw new Error('package.json "domainDriver.featureRoot" must be a string, for example "src/features".');
|
|
141
|
+
}
|
|
142
|
+
return validateRoot(value, 'package.json');
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The root becomes a filesystem path under the project, so an absolute path or any
|
|
146
|
+
* `..` segment would write outside it. Both are rejected rather than normalised away.
|
|
147
|
+
*/
|
|
148
|
+
function validateRoot(value, source) {
|
|
149
|
+
const normalized = value.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
150
|
+
const escapes = normalized === '' || path.isAbsolute(value) || normalized.split('/').includes('..');
|
|
151
|
+
if (escapes) {
|
|
152
|
+
throw new Error(`Feature root "${value}" from ${source} must be a relative path inside the project, for example src/features.`);
|
|
153
|
+
}
|
|
154
|
+
return normalized;
|
|
155
|
+
}
|
|
156
|
+
function conventionalRoot(cwd, stack) {
|
|
117
157
|
switch (stack) {
|
|
118
158
|
case 'next-fullstack':
|
|
119
159
|
case 'next-frontend':
|
|
@@ -122,7 +162,7 @@ function resolveFeatureRoot(cwd, stack) {
|
|
|
122
162
|
case 'node':
|
|
123
163
|
return (0, fs_1.isDirectory)(path.join(cwd, 'src')) ? 'src/features' : 'features';
|
|
124
164
|
case 'nest':
|
|
125
|
-
return 'src';
|
|
165
|
+
return (0, fs_1.isDirectory)(path.join(cwd, 'src', 'features')) ? 'src/features' : 'src';
|
|
126
166
|
case 'tanstack-start':
|
|
127
167
|
if ((0, fs_1.isDirectory)(path.join(cwd, 'src', 'routes')))
|
|
128
168
|
return 'src/routes';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "domain-driver",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
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": {
|