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 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;
@@ -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-Za-z][A-Za-z0-9]*$/;
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 (!NAME_PATTERN.test(name)) {
9
- throw new Error(`Name "${name}" must be letters and digits only, for example User.`);
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('/');
@@ -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)` before every command. Stacks: `next-fullstack`, `next-frontend`, `react`, `node` (Express, Fastify, Hono, or none), `nest`, `tanstack-start`. Override with `--stack <name>` if detection is wrong.',
38
+ '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');
@@ -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 deps = readDependencies(cwd, overridden);
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: resolveFeatureRoot(cwd, stack),
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
- if (stack.stack !== 'node')
68
- return base;
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 readDependencies(cwd, optional) {
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 new Set();
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
- const pkg = parsePackageJson(pkgPath);
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 resolveFeatureRoot(cwd, stack) {
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.0",
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": {