@neohm/nh-cli 1.1.4 → 1.2.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 +146 -12
- package/package.json +1 -1
- package/src/commands/gen.flag.js +353 -0
- package/src/commands/gen.js +36 -78
- package/src/index.js +57 -2
- package/src/lib/collision.js +82 -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 +349 -32
package/src/commands/gen.js
CHANGED
|
@@ -8,19 +8,24 @@ const chalk = require("chalk");
|
|
|
8
8
|
const { projectPaths, MODULE_SUBFOLDERS } = require("../lib/paths");
|
|
9
9
|
const naming = require("../lib/naming");
|
|
10
10
|
const tpl = require("../lib/templates");
|
|
11
|
-
const { writeFile, report, log, ensureDir } = require("../lib/fs.utils");
|
|
11
|
+
const { writeFile, report: reportFile, log, ensureDir } = require("../lib/fs.utils");
|
|
12
12
|
const sync = require("../lib/sync.engine");
|
|
13
|
-
const {
|
|
14
|
-
|
|
15
|
-
ensureMapEntry,
|
|
16
|
-
detectExportName,
|
|
17
|
-
entityConstantsPath,
|
|
18
|
-
} = require("../lib/constants.engine");
|
|
13
|
+
const { formatFiles } = require("../lib/format");
|
|
14
|
+
const { registerJob, registerEntity } = require("../lib/registry");
|
|
19
15
|
|
|
20
16
|
async function gen() {
|
|
21
17
|
const paths = projectPaths();
|
|
22
18
|
log.info(`Project: ${paths.root}\n`);
|
|
23
19
|
|
|
20
|
+
// Every file this run writes or rewrites, formatted in one pass at the end.
|
|
21
|
+
// Each write is announced through report(), so recording paths here catches
|
|
22
|
+
// templates, the constant registries, the es6 barrel and app.module alike.
|
|
23
|
+
const touched = [];
|
|
24
|
+
const report = (result, filePath, root) => {
|
|
25
|
+
if (result !== "skipped") touched.push(filePath);
|
|
26
|
+
reportFile(result, filePath, root);
|
|
27
|
+
};
|
|
28
|
+
|
|
24
29
|
// 1. Module ---------------------------------------------------------------
|
|
25
30
|
const { moduleNameRaw } = await inquirer.prompt([
|
|
26
31
|
{
|
|
@@ -94,74 +99,8 @@ async function gen() {
|
|
|
94
99
|
|
|
95
100
|
// Register the entity + job identifiers in the project constant registries
|
|
96
101
|
// (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
|
-
}
|
|
102
|
+
registerJob(paths, entityName, moduleDot, report);
|
|
103
|
+
registerEntity(paths, entityName, moduleDot, report);
|
|
165
104
|
|
|
166
105
|
// 4. Optional: controller -------------------------------------------------
|
|
167
106
|
const { controller } = await inquirer.prompt([
|
|
@@ -173,8 +112,11 @@ async function gen() {
|
|
|
173
112
|
},
|
|
174
113
|
]);
|
|
175
114
|
|
|
176
|
-
//
|
|
177
|
-
//
|
|
115
|
+
// The service is always generated — it is the layer the controller and any
|
|
116
|
+
// job, subscriber or sibling service goes through, never an opt-in. It is
|
|
117
|
+
// written after the processor prompts, because those answers decide which
|
|
118
|
+
// methods it (and the controller) ships with: `get()` always, `upsert()`
|
|
119
|
+
// with a data processor, `list()` with a listing processor.
|
|
178
120
|
|
|
179
121
|
// 5. Optional: data processor (includes Add*DataDto) ----------------------
|
|
180
122
|
const { processor } = await inquirer.prompt([
|
|
@@ -213,10 +155,17 @@ async function gen() {
|
|
|
213
155
|
);
|
|
214
156
|
}
|
|
215
157
|
|
|
158
|
+
const servicePath = path.join(moduleDir, "services", n.serviceFile);
|
|
159
|
+
report(
|
|
160
|
+
writeFile(servicePath, tpl.service(entityName, { processor, listProcessor })),
|
|
161
|
+
servicePath,
|
|
162
|
+
paths.root,
|
|
163
|
+
);
|
|
164
|
+
|
|
216
165
|
if (controller) {
|
|
217
166
|
const p = path.join(moduleDir, "controllers", n.controllerFile);
|
|
218
167
|
report(
|
|
219
|
-
writeFile(p, tpl.controller(entityName, { listProcessor })),
|
|
168
|
+
writeFile(p, tpl.controller(entityName, { processor, listProcessor })),
|
|
220
169
|
p,
|
|
221
170
|
paths.root,
|
|
222
171
|
);
|
|
@@ -255,6 +204,15 @@ async function gen() {
|
|
|
255
204
|
for (const w of written) report("overwritten", w, paths.root);
|
|
256
205
|
if (!written.length) log.info(" (already in sync)");
|
|
257
206
|
|
|
207
|
+
// The boilerplate lints with `prettier/prettier: error`, so hand the
|
|
208
|
+
// generated files to the project's own prettier before declaring done.
|
|
209
|
+
const formatted = await formatFiles(touched, paths.root);
|
|
210
|
+
if (formatted === null) {
|
|
211
|
+
log.info(" (prettier not installed — files left unformatted)");
|
|
212
|
+
} else if (formatted) {
|
|
213
|
+
log.info(` formatted ${formatted} file(s)`);
|
|
214
|
+
}
|
|
215
|
+
|
|
258
216
|
log.done(`Module "${moduleDot}" scaffolded.`);
|
|
259
217
|
console.log(
|
|
260
218
|
chalk.dim("Run `nh sync` anytime to re-wire imports after manual edits."),
|
package/src/index.js
CHANGED
|
@@ -22,10 +22,65 @@ program
|
|
|
22
22
|
.description('nh — scaffolding CLI for neohm. NestJS projects (built on @neohm/nestend)')
|
|
23
23
|
.version(pkg.version, '-v, --version', 'output the current version');
|
|
24
24
|
|
|
25
|
+
// `nh gen` with no target flag runs the full wizard; a target flag generates
|
|
26
|
+
// just that one artifact. -x mirrors a target onto the module's external
|
|
27
|
+
// (API-key) surface and applies to -c, -s, -dp and -lp only.
|
|
28
|
+
const GEN_TARGETS = [
|
|
29
|
+
['controller', 'controller'],
|
|
30
|
+
['service', 'service'],
|
|
31
|
+
['dataProcessor', 'dataProcessor'],
|
|
32
|
+
['listProcessor', 'listProcessor'],
|
|
33
|
+
['entity', 'entity'],
|
|
34
|
+
['job', 'job'],
|
|
35
|
+
];
|
|
36
|
+
|
|
25
37
|
program
|
|
26
38
|
.command('gen')
|
|
27
|
-
.description(
|
|
28
|
-
|
|
39
|
+
.description(
|
|
40
|
+
'scaffolding wizard; with a flag, generate one artifact instead',
|
|
41
|
+
)
|
|
42
|
+
.option('-c, --controller', 'generate only a controller')
|
|
43
|
+
.option('-s, --service', 'generate only a service')
|
|
44
|
+
.option('-dp, --data-processor', 'generate only a data processor')
|
|
45
|
+
.option('-lp, --list-processor', 'generate only a listing processor')
|
|
46
|
+
.option('-e, --entity', 'generate an entity and its migration')
|
|
47
|
+
.option('-j, --job', 'generate only a job')
|
|
48
|
+
.option('-x, --external', 'put it on the external (api/e/v1) surface')
|
|
49
|
+
.addHelpText(
|
|
50
|
+
'after',
|
|
51
|
+
`
|
|
52
|
+
Examples:
|
|
53
|
+
nh gen full wizard: module, entity, controller, service, ...
|
|
54
|
+
nh gen -c controller only (asks module, name, route path)
|
|
55
|
+
nh gen -s bare injectable service
|
|
56
|
+
nh gen -dp data processor (+ its Add<Name>DataDto if missing)
|
|
57
|
+
nh gen -lp listing processor
|
|
58
|
+
nh gen -e entity + migration, registered in the constants/maps
|
|
59
|
+
nh gen -j job, registered in job.constants/job.maps
|
|
60
|
+
nh gen -x -c external controller under <module>/external/controllers
|
|
61
|
+
nh gen -x -dp external data processor under <module>/external/libraries`,
|
|
62
|
+
)
|
|
63
|
+
.action(
|
|
64
|
+
run((opts) => {
|
|
65
|
+
const chosen = GEN_TARGETS.filter(([key]) => opts[key]).map(
|
|
66
|
+
([, target]) => target,
|
|
67
|
+
);
|
|
68
|
+
if (chosen.length > 1) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
'Pick one target flag at a time (-c, -s, -dp, -lp, -e or -j).',
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (!chosen.length) {
|
|
74
|
+
if (opts.external) {
|
|
75
|
+
throw new Error('-x needs a target: -c, -s, -dp or -lp.');
|
|
76
|
+
}
|
|
77
|
+
return require('./commands/gen')();
|
|
78
|
+
}
|
|
79
|
+
return require('./commands/gen.flag')(chosen[0], {
|
|
80
|
+
external: Boolean(opts.external),
|
|
81
|
+
});
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
29
84
|
|
|
30
85
|
program
|
|
31
86
|
.command('migration')
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Name-collision guard for the flag generators.
|
|
8
|
+
*
|
|
9
|
+
* `nh gen -j Order` must not quietly land next to an existing OrderJob: the
|
|
10
|
+
* job identifier registries (job.constants / job.maps) are project-wide, so a
|
|
11
|
+
* second class of the same name is a genuine conflict rather than a shadowed
|
|
12
|
+
* local. The same holds for controllers, services and processors, where two
|
|
13
|
+
* identically named providers in one Nest container is a bug that surfaces
|
|
14
|
+
* far from its cause.
|
|
15
|
+
*
|
|
16
|
+
* So before writing anything, look for the target *anywhere* under src/ —
|
|
17
|
+
* by filename and by exported class name — and report what is already there
|
|
18
|
+
* instead of creating a near-duplicate.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const SKIP_DIRS = new Set(['node_modules', 'dist', 'coverage', '.git']);
|
|
22
|
+
|
|
23
|
+
/** Every .ts file under `dir`, recursively. */
|
|
24
|
+
function walkTs(dir, out = []) {
|
|
25
|
+
if (!fs.existsSync(dir)) return out;
|
|
26
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
27
|
+
if (entry.isDirectory()) {
|
|
28
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
29
|
+
walkTs(path.join(dir, entry.name), out);
|
|
30
|
+
} else if (entry.isFile() && entry.name.endsWith('.ts')) {
|
|
31
|
+
out.push(path.join(dir, entry.name));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function exportedClassNames(filePath) {
|
|
38
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
39
|
+
return [...content.matchAll(/export\s+(?:abstract\s+)?class\s+(\w+)/g)].map(
|
|
40
|
+
(m) => m[1],
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Look for `className` / `fileName` under srcDir.
|
|
46
|
+
*
|
|
47
|
+
* Returns `null` when the name is free, otherwise
|
|
48
|
+
* `{ kind: 'class' | 'file', file }` describing the first conflict found —
|
|
49
|
+
* a class match is reported ahead of a filename match, since it is the one
|
|
50
|
+
* that actually breaks at runtime.
|
|
51
|
+
*/
|
|
52
|
+
function findConflict(srcDir, { className, fileName }) {
|
|
53
|
+
const files = walkTs(srcDir);
|
|
54
|
+
|
|
55
|
+
if (className) {
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
if (exportedClassNames(file).includes(className)) {
|
|
58
|
+
return { kind: 'class', file, name: className };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (fileName) {
|
|
64
|
+
for (const file of files) {
|
|
65
|
+
if (path.basename(file) === fileName) {
|
|
66
|
+
return { kind: 'file', file, name: fileName };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Human-readable one-liner for a conflict, relative to the project root. */
|
|
75
|
+
function describeConflict(conflict, root) {
|
|
76
|
+
const rel = path.relative(root, conflict.file);
|
|
77
|
+
return conflict.kind === 'class'
|
|
78
|
+
? `a class named ${conflict.name} already exists in ${rel}`
|
|
79
|
+
: `a file named ${conflict.name} already exists at ${rel}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { walkTs, exportedClassNames, findConflict, describeConflict };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Format generated files with the *target project's* prettier, honouring its
|
|
8
|
+
* own .prettierrc.
|
|
9
|
+
*
|
|
10
|
+
* The boilerplate lints with `prettier/prettier: error`, so a scaffold that is
|
|
11
|
+
* merely valid TypeScript is not good enough — it has to land already
|
|
12
|
+
* formatted, or `yarn lint` fails on freshly generated code. Templates aim to
|
|
13
|
+
* emit prettier-clean output on their own; this is the backstop that keeps
|
|
14
|
+
* that true for entity names long enough to push a line past 80 columns.
|
|
15
|
+
*
|
|
16
|
+
* Best-effort by design: a project without prettier installed still gets its
|
|
17
|
+
* files, just unformatted.
|
|
18
|
+
*/
|
|
19
|
+
function loadPrettier(root) {
|
|
20
|
+
try {
|
|
21
|
+
return require(require.resolve('prettier', { paths: [root] }));
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Format `files` in place. Returns the count actually rewritten, or null when
|
|
29
|
+
* prettier could not be resolved from the project.
|
|
30
|
+
*/
|
|
31
|
+
async function formatFiles(files, root) {
|
|
32
|
+
const prettier = loadPrettier(root);
|
|
33
|
+
if (!prettier) return null;
|
|
34
|
+
|
|
35
|
+
let changed = 0;
|
|
36
|
+
for (const file of new Set(files)) {
|
|
37
|
+
if (!file || !fs.existsSync(file)) continue;
|
|
38
|
+
if (!['.ts', '.js', '.json'].includes(path.extname(file))) continue;
|
|
39
|
+
try {
|
|
40
|
+
const source = fs.readFileSync(file, 'utf8');
|
|
41
|
+
const config = (await prettier.resolveConfig(file)) || {};
|
|
42
|
+
const next = await prettier.format(source, { ...config, filepath: file });
|
|
43
|
+
if (next !== source) {
|
|
44
|
+
fs.writeFileSync(file, next);
|
|
45
|
+
changed++;
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// A file prettier cannot parse is left exactly as generated.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return changed;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { formatFiles, loadPrettier };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const tpl = require('./templates');
|
|
6
|
+
const { log } = require('./fs.utils');
|
|
7
|
+
const {
|
|
8
|
+
ensureConstantEntry,
|
|
9
|
+
ensureMapEntry,
|
|
10
|
+
detectExportName,
|
|
11
|
+
entityConstantsPath,
|
|
12
|
+
} = require('./constants.engine');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The project-wide identifier registries.
|
|
16
|
+
*
|
|
17
|
+
* Entities and jobs are addressed by string identifier at runtime, so each one
|
|
18
|
+
* has to appear twice: as `<name>: '<dot.name>'` in the constants file, and as
|
|
19
|
+
* `[Constants.<name>]: <Class>` in the map beside it. A generated job
|
|
20
|
+
* references `JobConstants.<name>Job` directly, so a missing entry is a
|
|
21
|
+
* compile error rather than a latent one.
|
|
22
|
+
*
|
|
23
|
+
* Shared by the `nh gen` wizard and the `nh gen -e` / `nh gen -j` flags so
|
|
24
|
+
* both register identically. Registration is best-effort: a registry the
|
|
25
|
+
* engine cannot parse produces a warning telling you the one line to add, not
|
|
26
|
+
* a failed run that has already written half a module.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** Register `<base>Job` in job.constants.ts and job.maps.ts. */
|
|
30
|
+
function registerJob(paths, base, moduleDot, report) {
|
|
31
|
+
const n = tpl.names(base);
|
|
32
|
+
|
|
33
|
+
const constantsPath = path.join(paths.constantsDir, 'job.constants.ts');
|
|
34
|
+
const entry = ensureConstantEntry(
|
|
35
|
+
constantsPath,
|
|
36
|
+
tpl.jobConstantsFile(base),
|
|
37
|
+
`${n.camel}Job`,
|
|
38
|
+
`${n.dot}.job`,
|
|
39
|
+
);
|
|
40
|
+
if (entry === 'failed') {
|
|
41
|
+
log.warn(
|
|
42
|
+
`Could not update ${path.relative(paths.root, constantsPath)} — add ${n.camel}Job: '${n.dot}.job' manually.`,
|
|
43
|
+
);
|
|
44
|
+
} else {
|
|
45
|
+
report(entry, constantsPath, paths.root);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const constantsRef = {
|
|
49
|
+
exportName: detectExportName(constantsPath) || 'JobConstants',
|
|
50
|
+
importPath: './job.constants',
|
|
51
|
+
};
|
|
52
|
+
const mapsPath = path.join(paths.constantsDir, 'job.maps.ts');
|
|
53
|
+
const mapEntry = ensureMapEntry(
|
|
54
|
+
mapsPath,
|
|
55
|
+
tpl.jobMapsFile(base, moduleDot, constantsRef),
|
|
56
|
+
`[${constantsRef.exportName}.${n.camel}Job]`,
|
|
57
|
+
n.jobClass,
|
|
58
|
+
[
|
|
59
|
+
{ className: n.jobClass, importPath: `../${moduleDot}/jobs/${n.dot}.job` },
|
|
60
|
+
{ className: constantsRef.exportName, importPath: constantsRef.importPath },
|
|
61
|
+
],
|
|
62
|
+
);
|
|
63
|
+
if (mapEntry === 'failed') {
|
|
64
|
+
log.warn(
|
|
65
|
+
`Could not update ${path.relative(paths.root, mapsPath)} — map ${n.jobClass} manually.`,
|
|
66
|
+
);
|
|
67
|
+
} else {
|
|
68
|
+
report(mapEntry, mapsPath, paths.root);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Register `<base>` in entity.constants.ts and entity.maps.ts. */
|
|
73
|
+
function registerEntity(paths, base, moduleDot, report) {
|
|
74
|
+
const n = tpl.names(base);
|
|
75
|
+
|
|
76
|
+
// The boilerplate ships this file misspelled in some projects, so the
|
|
77
|
+
// engine reports whichever spelling is actually on disk.
|
|
78
|
+
const constantsPath = entityConstantsPath(paths.constantsDir);
|
|
79
|
+
const entry = ensureConstantEntry(
|
|
80
|
+
constantsPath,
|
|
81
|
+
tpl.entityConstantsFile(base),
|
|
82
|
+
n.camel,
|
|
83
|
+
n.dot,
|
|
84
|
+
);
|
|
85
|
+
if (entry === 'failed') {
|
|
86
|
+
log.warn(
|
|
87
|
+
`Could not update ${path.relative(paths.root, constantsPath)} — add ${n.camel}: '${n.dot}' manually.`,
|
|
88
|
+
);
|
|
89
|
+
} else {
|
|
90
|
+
report(entry, constantsPath, paths.root);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const constantsRef = {
|
|
94
|
+
exportName: detectExportName(constantsPath) || 'EntityConstants',
|
|
95
|
+
importPath: `./${path.basename(constantsPath, '.ts')}`,
|
|
96
|
+
};
|
|
97
|
+
const mapsPath = path.join(paths.constantsDir, 'entity.maps.ts');
|
|
98
|
+
const mapEntry = ensureMapEntry(
|
|
99
|
+
mapsPath,
|
|
100
|
+
tpl.entityMapsFile(base, moduleDot, constantsRef),
|
|
101
|
+
`[${constantsRef.exportName}.${n.camel}]`,
|
|
102
|
+
n.entityClass,
|
|
103
|
+
[
|
|
104
|
+
{
|
|
105
|
+
className: n.entityClass,
|
|
106
|
+
importPath: `../${moduleDot}/entities/${n.dot}.entity`,
|
|
107
|
+
},
|
|
108
|
+
{ className: constantsRef.exportName, importPath: constantsRef.importPath },
|
|
109
|
+
],
|
|
110
|
+
);
|
|
111
|
+
if (mapEntry === 'failed') {
|
|
112
|
+
log.warn(
|
|
113
|
+
`Could not update ${path.relative(paths.root, mapsPath)} — map ${n.entityClass} manually.`,
|
|
114
|
+
);
|
|
115
|
+
} else {
|
|
116
|
+
report(mapEntry, mapsPath, paths.root);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = { registerJob, registerEntity };
|
package/src/lib/sync.engine.js
CHANGED
|
@@ -27,6 +27,14 @@ const CATEGORIES = [
|
|
|
27
27
|
// Files in a category folder that are not providers themselves.
|
|
28
28
|
const NON_PROVIDER_FILES = new Set(['es6.classes.ts', 'index.ts']);
|
|
29
29
|
|
|
30
|
+
// A module may expose an API-key surface from `<module>/external/<folder>`
|
|
31
|
+
// (e.g. external/controllers/external.test.controller.ts). Those classes are
|
|
32
|
+
// providers of the same categories as their internal siblings and belong in
|
|
33
|
+
// the same es6.classes arrays — listed after the internal ones — so the
|
|
34
|
+
// scanner has to walk both roots or `nh sync` would strip them from the
|
|
35
|
+
// barrel.
|
|
36
|
+
const EXTERNAL_DIR = 'external';
|
|
37
|
+
|
|
30
38
|
function exportedClassName(filePath) {
|
|
31
39
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
32
40
|
const match = content.match(/export\s+class\s+(\w+)/);
|
|
@@ -80,12 +88,17 @@ function scanModule(dir, modFile) {
|
|
|
80
88
|
const moduleDot = path.basename(dir);
|
|
81
89
|
const providers = {};
|
|
82
90
|
for (const cat of CATEGORIES) {
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
// Internal first, then the external/ mirror of the same category.
|
|
92
|
+
const roots = [
|
|
93
|
+
{ base: dir, prefix: '' },
|
|
94
|
+
{ base: path.join(dir, EXTERNAL_DIR), prefix: `${EXTERNAL_DIR}/` },
|
|
95
|
+
];
|
|
96
|
+
providers[cat.key] = roots.flatMap(({ base, prefix }) =>
|
|
97
|
+
listClassFiles(path.join(base, cat.folder)).map((file) => ({
|
|
85
98
|
file,
|
|
86
99
|
className: exportedClassName(file),
|
|
87
|
-
importPath: `./${cat.folder}/${path.basename(file, '.ts')}`,
|
|
88
|
-
}),
|
|
100
|
+
importPath: `./${prefix}${cat.folder}/${path.basename(file, '.ts')}`,
|
|
101
|
+
})),
|
|
89
102
|
);
|
|
90
103
|
}
|
|
91
104
|
return {
|
|
@@ -101,7 +114,15 @@ function scanModule(dir, modFile) {
|
|
|
101
114
|
};
|
|
102
115
|
}
|
|
103
116
|
|
|
104
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* Render the es6.classes.ts content for a scanned module.
|
|
119
|
+
*
|
|
120
|
+
* The array literals are wrapped exactly the way prettier's 80-column printer
|
|
121
|
+
* would: inline while they fit, one class per line once they don't. `nh gen`
|
|
122
|
+
* formats what it writes, so if this emitted a single long line instead,
|
|
123
|
+
* every later `nh check` would report the barrel as drifted purely because
|
|
124
|
+
* prettier had reflowed it.
|
|
125
|
+
*/
|
|
105
126
|
function buildEs6Content(mod) {
|
|
106
127
|
const importLines = [];
|
|
107
128
|
const arrayLines = [];
|
|
@@ -110,8 +131,13 @@ function buildEs6Content(mod) {
|
|
|
110
131
|
for (const e of entries) {
|
|
111
132
|
importLines.push(`import { ${e.className} } from '${e.importPath}';`);
|
|
112
133
|
}
|
|
113
|
-
const classes = entries.map((e) => e.className)
|
|
114
|
-
|
|
134
|
+
const classes = entries.map((e) => e.className);
|
|
135
|
+
const inline = ` ${cat.key}: [${classes.join(', ')}],`;
|
|
136
|
+
arrayLines.push(
|
|
137
|
+
inline.length <= 80
|
|
138
|
+
? inline
|
|
139
|
+
: ` ${cat.key}: [\n${classes.map((c) => ` ${c},`).join('\n')}\n ],`,
|
|
140
|
+
);
|
|
115
141
|
}
|
|
116
142
|
const header = importLines.length ? importLines.join('\n') + '\n\n' : '';
|
|
117
143
|
return `${header}export const es6Classes = {\n${arrayLines.join('\n')}\n};\n`;
|
|
@@ -233,6 +259,7 @@ function applyPlan(plan) {
|
|
|
233
259
|
|
|
234
260
|
module.exports = {
|
|
235
261
|
CATEGORIES,
|
|
262
|
+
EXTERNAL_DIR,
|
|
236
263
|
scanModules,
|
|
237
264
|
scanModule,
|
|
238
265
|
buildEs6Content,
|