@neohm/nh-cli 1.1.5 → 1.3.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 +179 -18
- package/bin/nh.js +5 -0
- package/bin/nhx.js +11 -0
- package/package.json +8 -5
- package/src/commands/check.js +7 -2
- package/src/commands/gen.flag.js +357 -0
- package/src/commands/gen.js +41 -79
- package/src/commands/migration.js +2 -0
- package/src/commands/seed.js +2 -0
- package/src/index.js +67 -4
- package/src/lib/collision.js +82 -0
- package/src/lib/flavour.js +106 -0
- package/src/lib/format.js +54 -0
- package/src/lib/registry.js +120 -0
- package/src/lib/sync.engine.js +34 -7
- package/src/lib/templates.js +373 -61
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const inquirer = require('inquirer');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
|
|
8
|
+
const { projectPaths, MODULE_SUBFOLDERS } = require('../lib/paths');
|
|
9
|
+
const { assertFlavourMatchesProject, cli } = require('../lib/flavour');
|
|
10
|
+
const naming = require('../lib/naming');
|
|
11
|
+
const tpl = require('../lib/templates');
|
|
12
|
+
const {
|
|
13
|
+
writeFile,
|
|
14
|
+
report: reportFile,
|
|
15
|
+
log,
|
|
16
|
+
ensureDir,
|
|
17
|
+
} = require('../lib/fs.utils');
|
|
18
|
+
const sync = require('../lib/sync.engine');
|
|
19
|
+
const { formatFiles } = require('../lib/format');
|
|
20
|
+
const { registerJob, registerEntity } = require('../lib/registry');
|
|
21
|
+
const { findConflict, describeConflict } = require('../lib/collision');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Single-artifact generation: `nh gen -c`, `-s`, `-dp`, `-lp`, `-e`, `-j`, and
|
|
25
|
+
* the `-x` (external, API-key surface) variants of the first four.
|
|
26
|
+
*
|
|
27
|
+
* Bare `nh gen` still runs the full wizard — this path exists for adding one
|
|
28
|
+
* more file to a module that already exists, which is most of the work after
|
|
29
|
+
* the initial scaffold.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
// Short noun, used in the name prompt and error messages.
|
|
33
|
+
const LABELS = {
|
|
34
|
+
controller: 'controller',
|
|
35
|
+
service: 'service',
|
|
36
|
+
dataProcessor: 'data processor',
|
|
37
|
+
listProcessor: 'list processor',
|
|
38
|
+
entity: 'entity',
|
|
39
|
+
job: 'job',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// What the target actually writes, for the header line.
|
|
43
|
+
const DESCRIBES = {
|
|
44
|
+
controller: 'controller',
|
|
45
|
+
service: 'service',
|
|
46
|
+
dataProcessor: 'data processor (+ its payload DTO)',
|
|
47
|
+
listProcessor: 'list processor',
|
|
48
|
+
entity: 'entity, its job and subscriber, and a migration',
|
|
49
|
+
job: 'job',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// `-x` mirrors a target under <module>/external/. Entities are tables and jobs
|
|
53
|
+
// are queue workers — neither has an inward/outward face, so neither takes -x.
|
|
54
|
+
const EXTERNAL_CAPABLE = new Set([
|
|
55
|
+
'controller',
|
|
56
|
+
'service',
|
|
57
|
+
'dataProcessor',
|
|
58
|
+
'listProcessor',
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const FLAG = {
|
|
62
|
+
controller: '-c',
|
|
63
|
+
service: '-s',
|
|
64
|
+
dataProcessor: '-dp',
|
|
65
|
+
listProcessor: '-lp',
|
|
66
|
+
entity: '-e',
|
|
67
|
+
job: '-j',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The files a target owns, as {className, file} pairs.
|
|
72
|
+
*
|
|
73
|
+
* "Owns" excludes anything reused rather than claimed: the payload DTO for
|
|
74
|
+
* -dp, the attributes DTO for -e, and the timestamped migration. Those are
|
|
75
|
+
* shared or unique-by-construction, so they are created when absent and left
|
|
76
|
+
* alone when present — only the artifacts listed here are name-checked.
|
|
77
|
+
*
|
|
78
|
+
* An entity owns three files, not one: TypeORM drives the job through the
|
|
79
|
+
* subscriber, so an entity without them is a table nothing reacts to. The
|
|
80
|
+
* wizard has always written all three together; -e does the same.
|
|
81
|
+
*/
|
|
82
|
+
function primaryArtifacts(target, moduleDir, n, external) {
|
|
83
|
+
const at = (...parts) =>
|
|
84
|
+
external
|
|
85
|
+
? path.join(moduleDir, 'external', ...parts)
|
|
86
|
+
: path.join(moduleDir, ...parts);
|
|
87
|
+
const one = (className, ...parts) => [{ className, file: at(...parts) }];
|
|
88
|
+
|
|
89
|
+
switch (target) {
|
|
90
|
+
case 'controller':
|
|
91
|
+
return external
|
|
92
|
+
? one(
|
|
93
|
+
n.externalControllerClass,
|
|
94
|
+
'controllers',
|
|
95
|
+
n.externalControllerFile,
|
|
96
|
+
)
|
|
97
|
+
: one(n.controllerClass, 'controllers', n.controllerFile);
|
|
98
|
+
case 'service':
|
|
99
|
+
return external
|
|
100
|
+
? one(n.externalServiceClass, 'services', n.externalServiceFile)
|
|
101
|
+
: one(n.serviceClass, 'services', n.serviceFile);
|
|
102
|
+
case 'dataProcessor':
|
|
103
|
+
return external
|
|
104
|
+
? one(n.externalProcessorClass, 'libraries', n.externalProcessorFile)
|
|
105
|
+
: one(n.processorClass, 'libraries', n.processorFile);
|
|
106
|
+
case 'listProcessor':
|
|
107
|
+
return external
|
|
108
|
+
? one(
|
|
109
|
+
n.externalListProcessorClass,
|
|
110
|
+
'libraries',
|
|
111
|
+
n.externalListProcessorFile,
|
|
112
|
+
)
|
|
113
|
+
: one(n.listProcessorClass, 'libraries', n.listProcessorFile);
|
|
114
|
+
case 'entity':
|
|
115
|
+
return [
|
|
116
|
+
{
|
|
117
|
+
className: n.entityClass,
|
|
118
|
+
file: path.join(moduleDir, 'entities', n.entityFile),
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
className: n.jobClass,
|
|
122
|
+
file: path.join(moduleDir, 'jobs', n.jobFile),
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
className: n.subscriberClass,
|
|
126
|
+
file: path.join(moduleDir, 'subscribers', n.subscriberFile),
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
case 'job':
|
|
130
|
+
return [
|
|
131
|
+
{
|
|
132
|
+
className: n.jobClass,
|
|
133
|
+
file: path.join(moduleDir, 'jobs', n.jobFile),
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
default:
|
|
137
|
+
throw new Error(`Unknown target "${target}".`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Create the module skeleton if this is the first file going into it. */
|
|
142
|
+
function ensureModule(paths, moduleDot, external, report) {
|
|
143
|
+
const moduleDir = paths.moduleDir(moduleDot);
|
|
144
|
+
const existed = fs.existsSync(moduleDir);
|
|
145
|
+
|
|
146
|
+
for (const sub of MODULE_SUBFOLDERS) ensureDir(path.join(moduleDir, sub));
|
|
147
|
+
if (external) {
|
|
148
|
+
for (const sub of ['controllers', 'libraries', 'services']) {
|
|
149
|
+
ensureDir(path.join(moduleDir, 'external', sub));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const modulePath = path.join(moduleDir, `${moduleDot}.module.ts`);
|
|
154
|
+
const es6Path = path.join(moduleDir, 'es6.classes.ts');
|
|
155
|
+
const a = writeFile(modulePath, tpl.moduleFile(moduleDot));
|
|
156
|
+
const b = writeFile(es6Path, tpl.emptyEs6Classes());
|
|
157
|
+
if (a !== 'skipped') report(a, modulePath, paths.root);
|
|
158
|
+
if (b !== 'skipped') report(b, es6Path, paths.root);
|
|
159
|
+
|
|
160
|
+
if (!existed) log.info(`Created module "${moduleDot}".`);
|
|
161
|
+
return moduleDir;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function genFlag(target, { external = false } = {}) {
|
|
165
|
+
if (!LABELS[target]) throw new Error(`Unknown target "${target}".`);
|
|
166
|
+
if (external && !EXTERNAL_CAPABLE.has(target)) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`-x is not supported for ${LABELS[target]} — external applies to ` +
|
|
169
|
+
`-c, -s, -dp and -lp only. Run \`nh gen ${FLAG[target]}\` without -x.`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const paths = projectPaths();
|
|
174
|
+
assertFlavourMatchesProject(paths.root);
|
|
175
|
+
log.info(`Project: ${paths.root}\n`);
|
|
176
|
+
|
|
177
|
+
const touched = [];
|
|
178
|
+
const report = (result, filePath, root) => {
|
|
179
|
+
if (result !== 'skipped') touched.push(filePath);
|
|
180
|
+
reportFile(result, filePath, root);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
log.info(`Generating: ${external ? 'external ' : ''}${DESCRIBES[target]}.`);
|
|
184
|
+
|
|
185
|
+
// 1. Module ---------------------------------------------------------------
|
|
186
|
+
const known = sync.scanModules(paths).map((m) => m.moduleDot);
|
|
187
|
+
const { moduleNameRaw } = await inquirer.prompt([
|
|
188
|
+
{
|
|
189
|
+
type: 'input',
|
|
190
|
+
name: 'moduleNameRaw',
|
|
191
|
+
message: known.length
|
|
192
|
+
? `Module name (existing: ${known.join(', ')}):`
|
|
193
|
+
: 'Module name:',
|
|
194
|
+
validate: (v) => (v && v.trim() ? true : 'Module name is required'),
|
|
195
|
+
},
|
|
196
|
+
]);
|
|
197
|
+
const moduleDot = naming.toDotCase(moduleNameRaw);
|
|
198
|
+
|
|
199
|
+
// 2. Name -----------------------------------------------------------------
|
|
200
|
+
const { rawName } = await inquirer.prompt([
|
|
201
|
+
{
|
|
202
|
+
type: 'input',
|
|
203
|
+
name: 'rawName',
|
|
204
|
+
message: `${LABELS[target].replace(/^./, (c) => c.toUpperCase())} name (PascalCase):`,
|
|
205
|
+
validate: (v) => (v && v.trim() ? true : 'Name is required'),
|
|
206
|
+
},
|
|
207
|
+
]);
|
|
208
|
+
const n = tpl.names(rawName);
|
|
209
|
+
|
|
210
|
+
// 3. Target-specific answers ---------------------------------------------
|
|
211
|
+
let routePath;
|
|
212
|
+
if (target === 'controller') {
|
|
213
|
+
({ routePath } = await inquirer.prompt([
|
|
214
|
+
{
|
|
215
|
+
type: 'input',
|
|
216
|
+
name: 'routePath',
|
|
217
|
+
message: external
|
|
218
|
+
? 'Route path (mounted at api/e/v1/<path>):'
|
|
219
|
+
: 'Route path (mounted at api/v1/<path>):',
|
|
220
|
+
default: n.kebab,
|
|
221
|
+
validate: (v) => (v && v.trim() ? true : 'Route path is required'),
|
|
222
|
+
},
|
|
223
|
+
]));
|
|
224
|
+
routePath = routePath.trim().replace(/^\/+|\/+$/g, '');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let tableName;
|
|
228
|
+
if (target === 'listProcessor' || target === 'entity') {
|
|
229
|
+
({ tableName } = await inquirer.prompt([
|
|
230
|
+
{
|
|
231
|
+
type: 'input',
|
|
232
|
+
name: 'tableName',
|
|
233
|
+
message:
|
|
234
|
+
target === 'entity' ? 'Table name:' : 'Table to list (table name):',
|
|
235
|
+
default: `nh_${naming.toWords(rawName).join('_')}s`,
|
|
236
|
+
validate: (v) => (v && v.trim() ? true : 'Table name is required'),
|
|
237
|
+
},
|
|
238
|
+
]));
|
|
239
|
+
tableName = tableName.trim();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 4. Refuse to shadow a name that is already taken ------------------------
|
|
243
|
+
// Checked for every artifact up front, so a target that writes three files
|
|
244
|
+
// does not create the first and then abort on the second.
|
|
245
|
+
const artifacts = primaryArtifacts(
|
|
246
|
+
target,
|
|
247
|
+
paths.moduleDir(moduleDot),
|
|
248
|
+
n,
|
|
249
|
+
external,
|
|
250
|
+
);
|
|
251
|
+
for (const artifact of artifacts) {
|
|
252
|
+
const conflict = findConflict(paths.srcDir, {
|
|
253
|
+
className: artifact.className,
|
|
254
|
+
fileName: path.basename(artifact.file),
|
|
255
|
+
});
|
|
256
|
+
if (conflict) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`${describeConflict(conflict, paths.root)}.\n` +
|
|
259
|
+
` Pick a different name, or edit that file instead — ` +
|
|
260
|
+
`nothing was generated.`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const dest = artifacts[0].file;
|
|
265
|
+
|
|
266
|
+
// 5. Write ----------------------------------------------------------------
|
|
267
|
+
const moduleDir = ensureModule(paths, moduleDot, external, report);
|
|
268
|
+
const write = (filePath, content) =>
|
|
269
|
+
report(writeFile(filePath, content), filePath, paths.root);
|
|
270
|
+
|
|
271
|
+
switch (target) {
|
|
272
|
+
case 'controller':
|
|
273
|
+
write(
|
|
274
|
+
dest,
|
|
275
|
+
external
|
|
276
|
+
? tpl.bareExternalController(rawName, routePath)
|
|
277
|
+
: tpl.bareController(rawName, routePath),
|
|
278
|
+
);
|
|
279
|
+
break;
|
|
280
|
+
|
|
281
|
+
case 'service':
|
|
282
|
+
write(dest, tpl.bareService(rawName, { external }));
|
|
283
|
+
break;
|
|
284
|
+
|
|
285
|
+
case 'dataProcessor':
|
|
286
|
+
write(dest, tpl.processor(rawName, { external }));
|
|
287
|
+
// The processor's payload DTO lives in the module's dtos/ either way —
|
|
288
|
+
// created here only if it is not already there.
|
|
289
|
+
write(path.join(moduleDir, 'dtos', n.dtoFile), tpl.dto(rawName));
|
|
290
|
+
break;
|
|
291
|
+
|
|
292
|
+
case 'listProcessor':
|
|
293
|
+
write(dest, tpl.listProcessor(rawName, tableName, { external }));
|
|
294
|
+
break;
|
|
295
|
+
|
|
296
|
+
case 'entity': {
|
|
297
|
+
write(
|
|
298
|
+
path.join(moduleDir, 'dtos', n.attributesDtoFile),
|
|
299
|
+
tpl.attributesDto(rawName),
|
|
300
|
+
);
|
|
301
|
+
write(dest, tpl.entity(rawName, tableName));
|
|
302
|
+
|
|
303
|
+
// The entity's lifecycle hook and the job it drives. Same pair the
|
|
304
|
+
// wizard writes: the subscriber listens to the entity and hands each
|
|
305
|
+
// event to the job, which is why the job takes a DatabaseEventDto here
|
|
306
|
+
// rather than the loose payload `nh gen -j` scaffolds.
|
|
307
|
+
write(path.join(moduleDir, 'jobs', n.jobFile), tpl.job(rawName));
|
|
308
|
+
write(
|
|
309
|
+
path.join(moduleDir, 'subscribers', n.subscriberFile),
|
|
310
|
+
tpl.subscriber(rawName),
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const ts = Date.now();
|
|
314
|
+
const migrationPath = path.join(
|
|
315
|
+
paths.migrationsDir,
|
|
316
|
+
naming.migrationFileName(n.migrationName, ts),
|
|
317
|
+
);
|
|
318
|
+
write(migrationPath, tpl.genMigration(rawName, tableName, ts));
|
|
319
|
+
|
|
320
|
+
registerEntity(paths, rawName, moduleDot, report);
|
|
321
|
+
registerJob(paths, rawName, moduleDot, report);
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
case 'job':
|
|
326
|
+
write(dest, tpl.additionalJob(rawName));
|
|
327
|
+
registerJob(paths, rawName, moduleDot, report);
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 6. Wire it into the barrel + app.module, then format -------------------
|
|
332
|
+
log.info('\nWiring imports...');
|
|
333
|
+
const mod = sync.scanModule(
|
|
334
|
+
moduleDir,
|
|
335
|
+
path.join(moduleDir, `${moduleDot}.module.ts`),
|
|
336
|
+
);
|
|
337
|
+
const written = sync.applyPlan(sync.buildPlan(paths, [mod]));
|
|
338
|
+
for (const w of written) report('overwritten', w, paths.root);
|
|
339
|
+
if (!written.length) log.info(' (already in sync)');
|
|
340
|
+
|
|
341
|
+
const formatted = await formatFiles(touched, paths.root);
|
|
342
|
+
if (formatted === null) {
|
|
343
|
+
log.info(' (prettier not installed — files left unformatted)');
|
|
344
|
+
} else if (formatted) {
|
|
345
|
+
log.info(` formatted ${formatted} file(s)`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const made = artifacts.map((a) => a.className).join(', ');
|
|
349
|
+
log.done(`${made} generated in "${moduleDot}".`);
|
|
350
|
+
console.log(
|
|
351
|
+
chalk.dim(
|
|
352
|
+
`Run \`${cli()} sync\` anytime to re-wire imports after manual edits.`,
|
|
353
|
+
),
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
module.exports = genFlag;
|
package/src/commands/gen.js
CHANGED
|
@@ -6,21 +6,28 @@ const inquirer = require("inquirer");
|
|
|
6
6
|
const chalk = require("chalk");
|
|
7
7
|
|
|
8
8
|
const { projectPaths, MODULE_SUBFOLDERS } = require("../lib/paths");
|
|
9
|
+
const { assertFlavourMatchesProject, cli } = require("../lib/flavour");
|
|
9
10
|
const naming = require("../lib/naming");
|
|
10
11
|
const tpl = require("../lib/templates");
|
|
11
|
-
const { writeFile, report, log, ensureDir } = require("../lib/fs.utils");
|
|
12
|
+
const { writeFile, report: reportFile, log, ensureDir } = require("../lib/fs.utils");
|
|
12
13
|
const sync = require("../lib/sync.engine");
|
|
13
|
-
const {
|
|
14
|
-
|
|
15
|
-
ensureMapEntry,
|
|
16
|
-
detectExportName,
|
|
17
|
-
entityConstantsPath,
|
|
18
|
-
} = require("../lib/constants.engine");
|
|
14
|
+
const { formatFiles } = require("../lib/format");
|
|
15
|
+
const { registerJob, registerEntity } = require("../lib/registry");
|
|
19
16
|
|
|
20
17
|
async function gen() {
|
|
21
18
|
const paths = projectPaths();
|
|
19
|
+
assertFlavourMatchesProject(paths.root);
|
|
22
20
|
log.info(`Project: ${paths.root}\n`);
|
|
23
21
|
|
|
22
|
+
// Every file this run writes or rewrites, formatted in one pass at the end.
|
|
23
|
+
// Each write is announced through report(), so recording paths here catches
|
|
24
|
+
// templates, the constant registries, the es6 barrel and app.module alike.
|
|
25
|
+
const touched = [];
|
|
26
|
+
const report = (result, filePath, root) => {
|
|
27
|
+
if (result !== "skipped") touched.push(filePath);
|
|
28
|
+
reportFile(result, filePath, root);
|
|
29
|
+
};
|
|
30
|
+
|
|
24
31
|
// 1. Module ---------------------------------------------------------------
|
|
25
32
|
const { moduleNameRaw } = await inquirer.prompt([
|
|
26
33
|
{
|
|
@@ -94,74 +101,8 @@ async function gen() {
|
|
|
94
101
|
|
|
95
102
|
// Register the entity + job identifiers in the project constant registries
|
|
96
103
|
// (the generated job references JobConstants.<name>Job, so the entry must exist).
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
jobConstantsPath,
|
|
100
|
-
tpl.jobConstantsFile(entityName),
|
|
101
|
-
`${n.camel}Job`,
|
|
102
|
-
`${n.dot}.job`,
|
|
103
|
-
);
|
|
104
|
-
if (jobEntry === "failed") {
|
|
105
|
-
log.warn(`Could not update ${path.relative(paths.root, jobConstantsPath)} — add ${n.camel}Job: '${n.dot}.job' manually.`);
|
|
106
|
-
} else {
|
|
107
|
-
report(jobEntry, jobConstantsPath, paths.root);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const entityConstants = entityConstantsPath(paths.constantsDir);
|
|
111
|
-
const entityEntry = ensureConstantEntry(
|
|
112
|
-
entityConstants,
|
|
113
|
-
tpl.entityConstantsFile(entityName),
|
|
114
|
-
n.camel,
|
|
115
|
-
n.dot,
|
|
116
|
-
);
|
|
117
|
-
if (entityEntry === "failed") {
|
|
118
|
-
log.warn(`Could not update ${path.relative(paths.root, entityConstants)} — add ${n.camel}: '${n.dot}' manually.`);
|
|
119
|
-
} else {
|
|
120
|
-
report(entityEntry, entityConstants, paths.root);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ...and mirror them in the identifier → class map registries.
|
|
124
|
-
const jobConstantsRef = {
|
|
125
|
-
exportName: detectExportName(jobConstantsPath) || "JobConstants",
|
|
126
|
-
importPath: "./job.constants",
|
|
127
|
-
};
|
|
128
|
-
const jobMapsPath = path.join(paths.constantsDir, "job.maps.ts");
|
|
129
|
-
const jobMapEntry = ensureMapEntry(
|
|
130
|
-
jobMapsPath,
|
|
131
|
-
tpl.jobMapsFile(entityName, moduleDot, jobConstantsRef),
|
|
132
|
-
`[${jobConstantsRef.exportName}.${n.camel}Job]`,
|
|
133
|
-
n.jobClass,
|
|
134
|
-
[
|
|
135
|
-
{ className: n.jobClass, importPath: `../${moduleDot}/jobs/${n.dot}.job` },
|
|
136
|
-
{ className: jobConstantsRef.exportName, importPath: jobConstantsRef.importPath },
|
|
137
|
-
],
|
|
138
|
-
);
|
|
139
|
-
if (jobMapEntry === "failed") {
|
|
140
|
-
log.warn(`Could not update ${path.relative(paths.root, jobMapsPath)} — map ${n.jobClass} manually.`);
|
|
141
|
-
} else {
|
|
142
|
-
report(jobMapEntry, jobMapsPath, paths.root);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const entityConstantsRef = {
|
|
146
|
-
exportName: detectExportName(entityConstants) || "EntityConstants",
|
|
147
|
-
importPath: `./${path.basename(entityConstants, ".ts")}`,
|
|
148
|
-
};
|
|
149
|
-
const entityMapsPath = path.join(paths.constantsDir, "entity.maps.ts");
|
|
150
|
-
const entityMapEntry = ensureMapEntry(
|
|
151
|
-
entityMapsPath,
|
|
152
|
-
tpl.entityMapsFile(entityName, moduleDot, entityConstantsRef),
|
|
153
|
-
`[${entityConstantsRef.exportName}.${n.camel}]`,
|
|
154
|
-
n.entityClass,
|
|
155
|
-
[
|
|
156
|
-
{ className: n.entityClass, importPath: `../${moduleDot}/entities/${n.dot}.entity` },
|
|
157
|
-
{ className: entityConstantsRef.exportName, importPath: entityConstantsRef.importPath },
|
|
158
|
-
],
|
|
159
|
-
);
|
|
160
|
-
if (entityMapEntry === "failed") {
|
|
161
|
-
log.warn(`Could not update ${path.relative(paths.root, entityMapsPath)} — map ${n.entityClass} manually.`);
|
|
162
|
-
} else {
|
|
163
|
-
report(entityMapEntry, entityMapsPath, paths.root);
|
|
164
|
-
}
|
|
104
|
+
registerJob(paths, entityName, moduleDot, report);
|
|
105
|
+
registerEntity(paths, entityName, moduleDot, report);
|
|
165
106
|
|
|
166
107
|
// 4. Optional: controller -------------------------------------------------
|
|
167
108
|
const { controller } = await inquirer.prompt([
|
|
@@ -173,8 +114,11 @@ async function gen() {
|
|
|
173
114
|
},
|
|
174
115
|
]);
|
|
175
116
|
|
|
176
|
-
//
|
|
177
|
-
//
|
|
117
|
+
// The service is always generated — it is the layer the controller and any
|
|
118
|
+
// job, subscriber or sibling service goes through, never an opt-in. It is
|
|
119
|
+
// written after the processor prompts, because those answers decide which
|
|
120
|
+
// methods it (and the controller) ships with: `get()` always, `upsert()`
|
|
121
|
+
// with a data processor, `list()` with a listing processor.
|
|
178
122
|
|
|
179
123
|
// 5. Optional: data processor (includes Add*DataDto) ----------------------
|
|
180
124
|
const { processor } = await inquirer.prompt([
|
|
@@ -213,10 +157,17 @@ async function gen() {
|
|
|
213
157
|
);
|
|
214
158
|
}
|
|
215
159
|
|
|
160
|
+
const servicePath = path.join(moduleDir, "services", n.serviceFile);
|
|
161
|
+
report(
|
|
162
|
+
writeFile(servicePath, tpl.service(entityName, { processor, listProcessor })),
|
|
163
|
+
servicePath,
|
|
164
|
+
paths.root,
|
|
165
|
+
);
|
|
166
|
+
|
|
216
167
|
if (controller) {
|
|
217
168
|
const p = path.join(moduleDir, "controllers", n.controllerFile);
|
|
218
169
|
report(
|
|
219
|
-
writeFile(p, tpl.controller(entityName, { listProcessor })),
|
|
170
|
+
writeFile(p, tpl.controller(entityName, { processor, listProcessor })),
|
|
220
171
|
p,
|
|
221
172
|
paths.root,
|
|
222
173
|
);
|
|
@@ -255,9 +206,20 @@ async function gen() {
|
|
|
255
206
|
for (const w of written) report("overwritten", w, paths.root);
|
|
256
207
|
if (!written.length) log.info(" (already in sync)");
|
|
257
208
|
|
|
209
|
+
// The boilerplate lints with `prettier/prettier: error`, so hand the
|
|
210
|
+
// generated files to the project's own prettier before declaring done.
|
|
211
|
+
const formatted = await formatFiles(touched, paths.root);
|
|
212
|
+
if (formatted === null) {
|
|
213
|
+
log.info(" (prettier not installed — files left unformatted)");
|
|
214
|
+
} else if (formatted) {
|
|
215
|
+
log.info(` formatted ${formatted} file(s)`);
|
|
216
|
+
}
|
|
217
|
+
|
|
258
218
|
log.done(`Module "${moduleDot}" scaffolded.`);
|
|
259
219
|
console.log(
|
|
260
|
-
chalk.dim(
|
|
220
|
+
chalk.dim(
|
|
221
|
+
`Run \`${cli()} sync\` anytime to re-wire imports after manual edits.`,
|
|
222
|
+
),
|
|
261
223
|
);
|
|
262
224
|
}
|
|
263
225
|
|
|
@@ -4,12 +4,14 @@ const path = require("path");
|
|
|
4
4
|
const inquirer = require("inquirer");
|
|
5
5
|
|
|
6
6
|
const { projectPaths } = require("../lib/paths");
|
|
7
|
+
const { assertFlavourMatchesProject } = require("../lib/flavour");
|
|
7
8
|
const naming = require("../lib/naming");
|
|
8
9
|
const tpl = require("../lib/templates");
|
|
9
10
|
const { writeFile, report, log } = require("../lib/fs.utils");
|
|
10
11
|
|
|
11
12
|
async function migration() {
|
|
12
13
|
const paths = projectPaths();
|
|
14
|
+
assertFlavourMatchesProject(paths.root);
|
|
13
15
|
|
|
14
16
|
const { tableName, migrationName } = await inquirer.prompt([
|
|
15
17
|
{
|
package/src/commands/seed.js
CHANGED
|
@@ -4,12 +4,14 @@ const path = require("path");
|
|
|
4
4
|
const inquirer = require("inquirer");
|
|
5
5
|
|
|
6
6
|
const { projectPaths } = require("../lib/paths");
|
|
7
|
+
const { assertFlavourMatchesProject } = require("../lib/flavour");
|
|
7
8
|
const naming = require("../lib/naming");
|
|
8
9
|
const tpl = require("../lib/templates");
|
|
9
10
|
const { writeFile, report, log } = require("../lib/fs.utils");
|
|
10
11
|
|
|
11
12
|
async function seed() {
|
|
12
13
|
const paths = projectPaths();
|
|
14
|
+
assertFlavourMatchesProject(paths.root);
|
|
13
15
|
|
|
14
16
|
const { tableName, seedName } = await inquirer.prompt([
|
|
15
17
|
{
|
package/src/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { Command } = require('commander');
|
|
4
4
|
const chalk = require('chalk');
|
|
5
5
|
const pkg = require('../package.json');
|
|
6
|
+
const flavour = require('./lib/flavour');
|
|
6
7
|
|
|
7
8
|
const program = new Command();
|
|
8
9
|
|
|
@@ -17,15 +18,77 @@ function run(action) {
|
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
// `nh` and `nhx` are the same program against different packages: the bin
|
|
22
|
+
// entrypoint picks the flavour before this module is required, and everything
|
|
23
|
+
// that generates code reads it from there.
|
|
24
|
+
const { cli, package: packageName } = flavour.current();
|
|
25
|
+
|
|
20
26
|
program
|
|
21
|
-
.name(
|
|
22
|
-
.description(
|
|
27
|
+
.name(cli)
|
|
28
|
+
.description(
|
|
29
|
+
`${cli} — scaffolding CLI for neohm. NestJS projects (built on ${packageName})`,
|
|
30
|
+
)
|
|
23
31
|
.version(pkg.version, '-v, --version', 'output the current version');
|
|
24
32
|
|
|
33
|
+
// `gen` with no target flag runs the full wizard; a target flag generates just
|
|
34
|
+
// that one artifact. -x mirrors a target onto the module's external (API-key)
|
|
35
|
+
// surface and applies to -c, -s, -dp and -lp only.
|
|
36
|
+
const GEN_TARGETS = [
|
|
37
|
+
['controller', 'controller'],
|
|
38
|
+
['service', 'service'],
|
|
39
|
+
['dataProcessor', 'dataProcessor'],
|
|
40
|
+
['listProcessor', 'listProcessor'],
|
|
41
|
+
['entity', 'entity'],
|
|
42
|
+
['job', 'job'],
|
|
43
|
+
];
|
|
44
|
+
|
|
25
45
|
program
|
|
26
46
|
.command('gen')
|
|
27
|
-
.description(
|
|
28
|
-
|
|
47
|
+
.description(
|
|
48
|
+
'scaffolding wizard; with a flag, generate one artifact instead',
|
|
49
|
+
)
|
|
50
|
+
.option('-c, --controller', 'generate only a controller')
|
|
51
|
+
.option('-s, --service', 'generate only a service')
|
|
52
|
+
.option('-dp, --data-processor', 'generate only a data processor')
|
|
53
|
+
.option('-lp, --list-processor', 'generate only a listing processor')
|
|
54
|
+
.option('-e, --entity', 'generate an entity and its migration')
|
|
55
|
+
.option('-j, --job', 'generate only a job')
|
|
56
|
+
.option('-x, --external', 'put it on the external (api/e/v1) surface')
|
|
57
|
+
.addHelpText(
|
|
58
|
+
'after',
|
|
59
|
+
`
|
|
60
|
+
Examples:
|
|
61
|
+
${cli} gen full wizard: module, entity, controller, service, ...
|
|
62
|
+
${cli} gen -c controller only (asks module, name, route path)
|
|
63
|
+
${cli} gen -s bare injectable service
|
|
64
|
+
${cli} gen -dp data processor (+ its Add<Name>DataDto if missing)
|
|
65
|
+
${cli} gen -lp listing processor
|
|
66
|
+
${cli} gen -e entity + migration, registered in the constants/maps
|
|
67
|
+
${cli} gen -j job, registered in job.constants/job.maps
|
|
68
|
+
${cli} gen -x -c external controller under <module>/external/controllers
|
|
69
|
+
${cli} gen -x -dp external data processor under <module>/external/libraries`,
|
|
70
|
+
)
|
|
71
|
+
.action(
|
|
72
|
+
run((opts) => {
|
|
73
|
+
const chosen = GEN_TARGETS.filter(([key]) => opts[key]).map(
|
|
74
|
+
([, target]) => target,
|
|
75
|
+
);
|
|
76
|
+
if (chosen.length > 1) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
'Pick one target flag at a time (-c, -s, -dp, -lp, -e or -j).',
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (!chosen.length) {
|
|
82
|
+
if (opts.external) {
|
|
83
|
+
throw new Error('-x needs a target: -c, -s, -dp or -lp.');
|
|
84
|
+
}
|
|
85
|
+
return require('./commands/gen')();
|
|
86
|
+
}
|
|
87
|
+
return require('./commands/gen.flag')(chosen[0], {
|
|
88
|
+
external: Boolean(opts.external),
|
|
89
|
+
});
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
29
92
|
|
|
30
93
|
program
|
|
31
94
|
.command('migration')
|