domain-driver 0.4.1 → 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 CHANGED
@@ -266,6 +266,49 @@ Every layer directory carries a `-` prefix so TanStack Router excludes it from r
266
266
 
267
267
  ---
268
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
+
269
312
  ## Philosophy
270
313
 
271
314
  Everything for a feature lives in one folder, and every file does one thing.
@@ -305,6 +348,18 @@ The cache lives in `~/.cache/domain-driver` (or `$XDG_CACHE_HOME/domain-driver`)
305
348
 
306
349
  ---
307
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
+
308
363
  ## Upgrading from 0.4.0
309
364
 
310
365
  Two changes affect existing projects.
@@ -319,7 +374,7 @@ The entity half of a `<feature>/<Entity>` target must now be PascalCase. `make:s
319
374
 
320
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.
321
376
 
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:
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:
323
378
 
324
379
  ```tsx
325
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 hints_1 = require("./commands/hints");
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");
@@ -79,7 +80,8 @@ function createProgram(deps) {
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
82
  .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');
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');
83
85
  let pendingNotice = Promise.resolve(null);
84
86
  program.hook('preAction', (_thisCommand, actionCommand) => {
85
87
  const name = actionCommand.name();
@@ -95,7 +97,11 @@ function createProgram(deps) {
95
97
  if (SKIP_DETECTION.has(name))
96
98
  return;
97
99
  const { stack, root } = program.opts();
98
- deps.log((0, detect_1.describeStack)((0, detect_1.detectStack)(stack, root)));
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 })));
99
105
  });
100
106
  program.hook('postAction', async () => {
101
107
  const notice = await pendingNotice;
@@ -140,7 +146,7 @@ function createProgram(deps) {
140
146
  const { feature, name } = (0, target_1.parseTarget)(target);
141
147
  const wrote = (0, service_1.makeService)(feature, name, (0, sides_1.parseSide)(options.side));
142
148
  if (wrote)
143
- (0, hints_1.hintRegisterInModule)(feature, (0, hints_1.standardClassNames)(name, 'Service'));
149
+ (0, register_1.registerClasses)((0, resolve_1.requireFeature)(feature), 'service', (0, register_1.standardActionNames)(name));
144
150
  });
145
151
  program
146
152
  .command('make:repository <target>')
@@ -150,7 +156,7 @@ function createProgram(deps) {
150
156
  const { feature, name } = (0, target_1.parseTarget)(target);
151
157
  const wrote = (0, repository_1.makeRepository)(feature, name, (0, sides_1.parseSide)(options.side));
152
158
  if (wrote)
153
- (0, hints_1.hintRegisterInModule)(feature, (0, hints_1.standardClassNames)(name, 'Repository'));
159
+ (0, register_1.registerClasses)((0, resolve_1.requireFeature)(feature), 'repository', (0, register_1.standardActionNames)(name));
154
160
  });
155
161
  program
156
162
  .command('make:controller <target>')
@@ -159,7 +165,7 @@ function createProgram(deps) {
159
165
  const { feature, name } = (0, target_1.parseTarget)(target);
160
166
  const wrote = (0, controller_1.makeController)(feature, name);
161
167
  if (wrote)
162
- (0, hints_1.hintRegisterInModule)(feature, (0, hints_1.standardClassNames)(name, 'Controller'));
168
+ (0, register_1.registerClasses)((0, resolve_1.requireFeature)(feature), 'controller', (0, register_1.standardActionNames)(name));
163
169
  });
164
170
  program
165
171
  .command('make:action <target> <action>')
@@ -172,7 +178,7 @@ function createProgram(deps) {
172
178
  const wrote = (0, action_1.makeAction)(feature, name, action, { withInput: options.withInput, returns });
173
179
  if (wrote) {
174
180
  const { pascal } = (0, actions_1.actionCase)(action);
175
- (0, hints_1.hintRegisterInModule)(feature, [`${pascal}Controller`, `${pascal}Service`, `${pascal}Repository`], `list ${pascal}Controller before Show${name}Controller in controllers`);
181
+ (0, register_1.registerCustomAction)((0, resolve_1.requireFeature)(feature), pascal, name);
176
182
  }
177
183
  });
178
184
  program
@@ -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
  }
@@ -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
+ }
@@ -37,6 +37,7 @@ const SKILL_LINES = [
37
37
  '',
38
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
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`.',
40
41
  '',
41
42
  '## Commands',
42
43
  '',
@@ -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
+ }
@@ -50,15 +50,16 @@ let cached;
50
50
  function resetStackCache() {
51
51
  cached = undefined;
52
52
  }
53
- function detectStack(override, rootOverride) {
53
+ function detectStack(options = {}) {
54
54
  if (cached)
55
55
  return cached;
56
56
  const cwd = process.cwd();
57
+ const override = options.stack;
57
58
  const overridden = override !== undefined;
58
59
  const pkg = readPackageJson(cwd, overridden);
59
60
  const deps = dependencyNames(pkg);
60
61
  const stack = overridden ? parseOverride(override) : inferStack(cwd, deps);
61
- const root = resolveRoot(cwd, stack, pkg, rootOverride);
62
+ const root = resolveRoot(cwd, stack, pkg, options.root);
62
63
  const result = Object.freeze({
63
64
  stack,
64
65
  source: overridden ? 'override' : 'detected',
@@ -66,6 +67,8 @@ function detectStack(override, rootOverride) {
66
67
  featureRoot: root.featureRoot,
67
68
  featureRootSource: root.source,
68
69
  hasNestjsZod: deps.has('nestjs-zod'),
70
+ rootModule: configuredRootModule(pkg),
71
+ autoRegister: options.autoRegister ?? configuredAutoRegister(pkg),
69
72
  });
70
73
  cached = result;
71
74
  return result;
@@ -132,6 +135,24 @@ function resolveRoot(cwd, stack, pkg, flag) {
132
135
  }
133
136
  return { featureRoot: conventionalRoot(cwd, stack), source: 'detected' };
134
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
+ }
135
156
  function configuredRoot(pkg) {
136
157
  const value = pkg.domainDriver?.featureRoot;
137
158
  if (value === undefined)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domain-driver",
3
- "version": "0.4.1",
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": {