@rvoh/psychic 3.0.1 → 3.0.2
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/dist/cjs/src/bin/helpers/printControllerHierarchy.js +168 -0
- package/dist/cjs/src/bin/index.js +7 -0
- package/dist/cjs/src/cli/index.js +25 -0
- package/dist/esm/src/bin/helpers/printControllerHierarchy.js +168 -0
- package/dist/esm/src/bin/index.js +7 -0
- package/dist/esm/src/cli/index.js +25 -0
- package/dist/types/src/bin/helpers/printControllerHierarchy.d.ts +35 -0
- package/dist/types/src/bin/index.d.ts +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import colors from 'yoctocolors';
|
|
2
|
+
import PsychicController from '../../controller/index.js';
|
|
3
|
+
import PsychicApp from '../../psychic-app/index.js';
|
|
4
|
+
const ROOT_CHILD_INDENT = 5;
|
|
5
|
+
const MIN_DASHES = 2;
|
|
6
|
+
export default function printControllerHierarchy(controllersPath) {
|
|
7
|
+
const lines = controllerHierarchyLines(controllersPath);
|
|
8
|
+
for (const line of lines) {
|
|
9
|
+
console.log(line);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function resolveControllerClasses(controllersPath) {
|
|
13
|
+
const psychicApp = PsychicApp.getOrFail();
|
|
14
|
+
const resolvedPath = controllersPath ?? psychicApp.paths.controllers;
|
|
15
|
+
const allControllers = psychicApp.controllers;
|
|
16
|
+
const controllerClasses = Object.values(allControllers).filter(ctrl => {
|
|
17
|
+
return ctrl.globalName.startsWith(globalPrefixFromPath(resolvedPath, psychicApp.paths.controllers));
|
|
18
|
+
});
|
|
19
|
+
return { controllerClasses, resolvedPath };
|
|
20
|
+
}
|
|
21
|
+
export function controllerHierarchyLines(controllersPath) {
|
|
22
|
+
const { controllerClasses, resolvedPath } = resolveControllerClasses(controllersPath);
|
|
23
|
+
const childrenMap = buildChildrenMap(controllerClasses);
|
|
24
|
+
const roots = controllerClasses.filter(ctrl => !controllerClasses.some(other => other !== ctrl && ctrl.prototype instanceof other));
|
|
25
|
+
if (roots.length === 0) {
|
|
26
|
+
return ['No controllers found.'];
|
|
27
|
+
}
|
|
28
|
+
const lines = [];
|
|
29
|
+
for (const root of roots) {
|
|
30
|
+
collectTreeLines(root, childrenMap, { depth: 0, displayColumn: 0, strippedPrefix: '', baseIndent: 0 }, lines, resolvedPath);
|
|
31
|
+
}
|
|
32
|
+
return lines;
|
|
33
|
+
}
|
|
34
|
+
export function controllerHierarchyViolations(controllersPath) {
|
|
35
|
+
const { controllerClasses, resolvedPath } = resolveControllerClasses(controllersPath);
|
|
36
|
+
const violations = [];
|
|
37
|
+
for (const ctrl of controllerClasses) {
|
|
38
|
+
const parentClass = Object.getPrototypeOf(ctrl);
|
|
39
|
+
if (parentClass === PsychicController)
|
|
40
|
+
continue;
|
|
41
|
+
const violation = hierarchyViolation(ctrl.globalName, parentClass.globalName, resolvedPath);
|
|
42
|
+
if (violation) {
|
|
43
|
+
violations.push(violation);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return violations;
|
|
47
|
+
}
|
|
48
|
+
export function buildChildrenMap(controllerClasses) {
|
|
49
|
+
const childrenMap = new Map();
|
|
50
|
+
for (const ctrl of controllerClasses) {
|
|
51
|
+
const parent = Object.getPrototypeOf(ctrl);
|
|
52
|
+
if (!childrenMap.has(parent)) {
|
|
53
|
+
childrenMap.set(parent, []);
|
|
54
|
+
}
|
|
55
|
+
childrenMap.get(parent).push(ctrl);
|
|
56
|
+
}
|
|
57
|
+
return childrenMap;
|
|
58
|
+
}
|
|
59
|
+
export function controllerTreeLine(displayedName, depth, displayColumn, baseIndent) {
|
|
60
|
+
if (depth === 0) {
|
|
61
|
+
return colors.cyan(displayedName);
|
|
62
|
+
}
|
|
63
|
+
const numDashes = Math.max(MIN_DASHES, displayColumn - baseIndent - 2);
|
|
64
|
+
const indentation = ' '.repeat(baseIndent);
|
|
65
|
+
const dashes = '─'.repeat(numDashes);
|
|
66
|
+
return `${indentation}└${dashes} ${colors.cyan(displayedName)}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Computes the actual column where the name starts on screen,
|
|
70
|
+
* accounting for MIN_DASHES potentially pushing the name further right.
|
|
71
|
+
*/
|
|
72
|
+
export function actualDisplayColumn(displayColumn, depth, baseIndent) {
|
|
73
|
+
if (depth === 0)
|
|
74
|
+
return displayColumn;
|
|
75
|
+
const numDashes = Math.max(MIN_DASHES, displayColumn - baseIndent - 2);
|
|
76
|
+
return baseIndent + numDashes + 2;
|
|
77
|
+
}
|
|
78
|
+
export function sharedDirPrefix(a, b) {
|
|
79
|
+
const aParts = a.split('/');
|
|
80
|
+
const bParts = b.split('/');
|
|
81
|
+
let shared = '';
|
|
82
|
+
for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) {
|
|
83
|
+
if (aParts[i] === bParts[i]) {
|
|
84
|
+
shared += aParts[i] + '/';
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return shared;
|
|
91
|
+
}
|
|
92
|
+
export function globalPrefixFromPath(path, defaultControllersPath) {
|
|
93
|
+
if (path === defaultControllersPath)
|
|
94
|
+
return 'controllers/';
|
|
95
|
+
const normalized = path.replace(/\/$/, '');
|
|
96
|
+
const defaultNormalized = defaultControllersPath.replace(/\/$/, '');
|
|
97
|
+
if (normalized.startsWith(defaultNormalized + '/')) {
|
|
98
|
+
const suffix = normalized.slice(defaultNormalized.length + 1);
|
|
99
|
+
return `controllers/${suffix}/`;
|
|
100
|
+
}
|
|
101
|
+
return 'controllers/';
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Returns the directory portion of a globalName.
|
|
105
|
+
* e.g. "controllers/Admin/AuthedController" -> "controllers/Admin/"
|
|
106
|
+
* "controllers/ApplicationController" -> "controllers/"
|
|
107
|
+
*/
|
|
108
|
+
export function globalNameDir(globalName) {
|
|
109
|
+
const lastSlash = globalName.lastIndexOf('/');
|
|
110
|
+
if (lastSlash === -1)
|
|
111
|
+
return '';
|
|
112
|
+
return globalName.slice(0, lastSlash + 1);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Returns the parent directory of a directory path.
|
|
116
|
+
* e.g. "controllers/Admin/" -> "controllers/"
|
|
117
|
+
* "controllers/" -> ""
|
|
118
|
+
*/
|
|
119
|
+
export function parentOfDir(dir) {
|
|
120
|
+
const withoutTrailing = dir.slice(0, -1);
|
|
121
|
+
const lastSlash = withoutTrailing.lastIndexOf('/');
|
|
122
|
+
if (lastSlash === -1)
|
|
123
|
+
return '';
|
|
124
|
+
return withoutTrailing.slice(0, lastSlash + 1);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Checks whether a child controller violates the hierarchy convention.
|
|
128
|
+
* A controller should extend a controller in the same directory or the parent directory.
|
|
129
|
+
* Returns a warning message string if violated, or null if OK.
|
|
130
|
+
*/
|
|
131
|
+
export function hierarchyViolation(childGlobalName, parentGlobalName, controllersPath) {
|
|
132
|
+
const childDir = globalNameDir(childGlobalName);
|
|
133
|
+
const parentControllerDir = globalNameDir(parentGlobalName);
|
|
134
|
+
if (childDir === parentControllerDir || parentOfDir(childDir) === parentControllerDir) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const childFilePath = controllersPath + '/' + childGlobalName.replace(/^controllers\//, '') + '.ts';
|
|
138
|
+
return `[hierarchy violation: ${childFilePath} should extend a BaseController at its same level]`;
|
|
139
|
+
}
|
|
140
|
+
function collectTreeLines(controller, childrenMap, { depth, displayColumn, strippedPrefix, baseIndent, }, lines, controllersPath) {
|
|
141
|
+
const displayedName = controller.globalName.slice(strippedPrefix.length);
|
|
142
|
+
lines.push(controllerTreeLine(displayedName, depth, displayColumn, baseIndent));
|
|
143
|
+
// The actual column where the name appears on screen (may differ from displayColumn
|
|
144
|
+
// when MIN_DASHES pushes the name further right)
|
|
145
|
+
const effectiveDisplayColumn = actualDisplayColumn(displayColumn, depth, baseIndent);
|
|
146
|
+
if (depth > 0) {
|
|
147
|
+
const parentClass = Object.getPrototypeOf(controller);
|
|
148
|
+
if (parentClass !== PsychicController) {
|
|
149
|
+
const violation = hierarchyViolation(controller.globalName, parentClass.globalName, controllersPath);
|
|
150
|
+
if (violation) {
|
|
151
|
+
const warningIndent = ' '.repeat(effectiveDisplayColumn);
|
|
152
|
+
lines.push(warningIndent + colors.yellow(violation));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const childBaseIndent = depth === 0 ? ROOT_CHILD_INDENT : effectiveDisplayColumn - 1;
|
|
157
|
+
const children = childrenMap.get(controller) ?? [];
|
|
158
|
+
for (const child of children) {
|
|
159
|
+
const shared = sharedDirPrefix(controller.globalName, child.globalName);
|
|
160
|
+
const childDisplayColumn = effectiveDisplayColumn + shared.length - strippedPrefix.length;
|
|
161
|
+
collectTreeLines(child, childrenMap, {
|
|
162
|
+
depth: depth + 1,
|
|
163
|
+
displayColumn: childDisplayColumn,
|
|
164
|
+
strippedPrefix: shared,
|
|
165
|
+
baseIndent: childBaseIndent,
|
|
166
|
+
}, lines, controllersPath);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -10,6 +10,7 @@ import PsychicApp from '../psychic-app/index.js';
|
|
|
10
10
|
import enumsFileStr from './helpers/enumsFileStr.js';
|
|
11
11
|
import generateRouteTypes from './helpers/generateRouteTypes.js';
|
|
12
12
|
import { OpenApiSpecDiff } from './helpers/OpenApiSpecDiff.js';
|
|
13
|
+
import printControllerHierarchy, { controllerHierarchyViolations, } from './helpers/printControllerHierarchy.js';
|
|
13
14
|
import printRoutes from './helpers/printRoutes.js';
|
|
14
15
|
export { BreakingChangesDetectedInOpenApiSpecError } from './helpers/OpenApiSpecDiff.js';
|
|
15
16
|
export default class PsychicBin {
|
|
@@ -26,6 +27,12 @@ export default class PsychicBin {
|
|
|
26
27
|
static printRoutes() {
|
|
27
28
|
printRoutes();
|
|
28
29
|
}
|
|
30
|
+
static printControllerHierarchy(controllersPath) {
|
|
31
|
+
printControllerHierarchy(controllersPath);
|
|
32
|
+
}
|
|
33
|
+
static controllerHierarchyViolations(controllersPath) {
|
|
34
|
+
return controllerHierarchyViolations(controllersPath);
|
|
35
|
+
}
|
|
29
36
|
static async sync({ bypassDreamSync = false, schemaOnly = false, } = {}) {
|
|
30
37
|
if (!bypassDreamSync)
|
|
31
38
|
await DreamBin.sync(() => { }, { schemaOnly });
|
|
@@ -160,6 +160,31 @@ export default class PsychicCLI {
|
|
|
160
160
|
await generateSyncOpenapiTypescriptInitializer(openapiFilepath, outfile, initializerName);
|
|
161
161
|
process.exit();
|
|
162
162
|
});
|
|
163
|
+
program
|
|
164
|
+
.command('inspect:controller-hierarchy')
|
|
165
|
+
.alias('i:controller-hierarchy')
|
|
166
|
+
.description('Displays the inheritance hierarchy of all PsychicController classes in the project.')
|
|
167
|
+
.argument('[path]', 'the controllers directory to scan (defaults to the configured controllers path)')
|
|
168
|
+
.action(async (controllersPath) => {
|
|
169
|
+
await initializePsychicApp();
|
|
170
|
+
PsychicBin.printControllerHierarchy(controllersPath);
|
|
171
|
+
process.exit();
|
|
172
|
+
});
|
|
173
|
+
program
|
|
174
|
+
.command('check:controller-hierarchy')
|
|
175
|
+
.description('Checks that all controllers extend a controller in the same or parent directory. Exits with an error if any violations are found.')
|
|
176
|
+
.argument('[path]', 'the controllers directory to scan (defaults to the configured controllers path)')
|
|
177
|
+
.action(async (controllersPath) => {
|
|
178
|
+
await initializePsychicApp();
|
|
179
|
+
const violations = PsychicBin.controllerHierarchyViolations(controllersPath);
|
|
180
|
+
if (violations.length > 0) {
|
|
181
|
+
for (const violation of violations) {
|
|
182
|
+
console.error(violation);
|
|
183
|
+
}
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
process.exit();
|
|
187
|
+
});
|
|
163
188
|
program
|
|
164
189
|
.command('routes')
|
|
165
190
|
.description('Prints a list of routes defined by the application, including path arguments and the controller/action reached by the route.')
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import colors from 'yoctocolors';
|
|
2
|
+
import PsychicController from '../../controller/index.js';
|
|
3
|
+
import PsychicApp from '../../psychic-app/index.js';
|
|
4
|
+
const ROOT_CHILD_INDENT = 5;
|
|
5
|
+
const MIN_DASHES = 2;
|
|
6
|
+
export default function printControllerHierarchy(controllersPath) {
|
|
7
|
+
const lines = controllerHierarchyLines(controllersPath);
|
|
8
|
+
for (const line of lines) {
|
|
9
|
+
console.log(line);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function resolveControllerClasses(controllersPath) {
|
|
13
|
+
const psychicApp = PsychicApp.getOrFail();
|
|
14
|
+
const resolvedPath = controllersPath ?? psychicApp.paths.controllers;
|
|
15
|
+
const allControllers = psychicApp.controllers;
|
|
16
|
+
const controllerClasses = Object.values(allControllers).filter(ctrl => {
|
|
17
|
+
return ctrl.globalName.startsWith(globalPrefixFromPath(resolvedPath, psychicApp.paths.controllers));
|
|
18
|
+
});
|
|
19
|
+
return { controllerClasses, resolvedPath };
|
|
20
|
+
}
|
|
21
|
+
export function controllerHierarchyLines(controllersPath) {
|
|
22
|
+
const { controllerClasses, resolvedPath } = resolveControllerClasses(controllersPath);
|
|
23
|
+
const childrenMap = buildChildrenMap(controllerClasses);
|
|
24
|
+
const roots = controllerClasses.filter(ctrl => !controllerClasses.some(other => other !== ctrl && ctrl.prototype instanceof other));
|
|
25
|
+
if (roots.length === 0) {
|
|
26
|
+
return ['No controllers found.'];
|
|
27
|
+
}
|
|
28
|
+
const lines = [];
|
|
29
|
+
for (const root of roots) {
|
|
30
|
+
collectTreeLines(root, childrenMap, { depth: 0, displayColumn: 0, strippedPrefix: '', baseIndent: 0 }, lines, resolvedPath);
|
|
31
|
+
}
|
|
32
|
+
return lines;
|
|
33
|
+
}
|
|
34
|
+
export function controllerHierarchyViolations(controllersPath) {
|
|
35
|
+
const { controllerClasses, resolvedPath } = resolveControllerClasses(controllersPath);
|
|
36
|
+
const violations = [];
|
|
37
|
+
for (const ctrl of controllerClasses) {
|
|
38
|
+
const parentClass = Object.getPrototypeOf(ctrl);
|
|
39
|
+
if (parentClass === PsychicController)
|
|
40
|
+
continue;
|
|
41
|
+
const violation = hierarchyViolation(ctrl.globalName, parentClass.globalName, resolvedPath);
|
|
42
|
+
if (violation) {
|
|
43
|
+
violations.push(violation);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return violations;
|
|
47
|
+
}
|
|
48
|
+
export function buildChildrenMap(controllerClasses) {
|
|
49
|
+
const childrenMap = new Map();
|
|
50
|
+
for (const ctrl of controllerClasses) {
|
|
51
|
+
const parent = Object.getPrototypeOf(ctrl);
|
|
52
|
+
if (!childrenMap.has(parent)) {
|
|
53
|
+
childrenMap.set(parent, []);
|
|
54
|
+
}
|
|
55
|
+
childrenMap.get(parent).push(ctrl);
|
|
56
|
+
}
|
|
57
|
+
return childrenMap;
|
|
58
|
+
}
|
|
59
|
+
export function controllerTreeLine(displayedName, depth, displayColumn, baseIndent) {
|
|
60
|
+
if (depth === 0) {
|
|
61
|
+
return colors.cyan(displayedName);
|
|
62
|
+
}
|
|
63
|
+
const numDashes = Math.max(MIN_DASHES, displayColumn - baseIndent - 2);
|
|
64
|
+
const indentation = ' '.repeat(baseIndent);
|
|
65
|
+
const dashes = '─'.repeat(numDashes);
|
|
66
|
+
return `${indentation}└${dashes} ${colors.cyan(displayedName)}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Computes the actual column where the name starts on screen,
|
|
70
|
+
* accounting for MIN_DASHES potentially pushing the name further right.
|
|
71
|
+
*/
|
|
72
|
+
export function actualDisplayColumn(displayColumn, depth, baseIndent) {
|
|
73
|
+
if (depth === 0)
|
|
74
|
+
return displayColumn;
|
|
75
|
+
const numDashes = Math.max(MIN_DASHES, displayColumn - baseIndent - 2);
|
|
76
|
+
return baseIndent + numDashes + 2;
|
|
77
|
+
}
|
|
78
|
+
export function sharedDirPrefix(a, b) {
|
|
79
|
+
const aParts = a.split('/');
|
|
80
|
+
const bParts = b.split('/');
|
|
81
|
+
let shared = '';
|
|
82
|
+
for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) {
|
|
83
|
+
if (aParts[i] === bParts[i]) {
|
|
84
|
+
shared += aParts[i] + '/';
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return shared;
|
|
91
|
+
}
|
|
92
|
+
export function globalPrefixFromPath(path, defaultControllersPath) {
|
|
93
|
+
if (path === defaultControllersPath)
|
|
94
|
+
return 'controllers/';
|
|
95
|
+
const normalized = path.replace(/\/$/, '');
|
|
96
|
+
const defaultNormalized = defaultControllersPath.replace(/\/$/, '');
|
|
97
|
+
if (normalized.startsWith(defaultNormalized + '/')) {
|
|
98
|
+
const suffix = normalized.slice(defaultNormalized.length + 1);
|
|
99
|
+
return `controllers/${suffix}/`;
|
|
100
|
+
}
|
|
101
|
+
return 'controllers/';
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Returns the directory portion of a globalName.
|
|
105
|
+
* e.g. "controllers/Admin/AuthedController" -> "controllers/Admin/"
|
|
106
|
+
* "controllers/ApplicationController" -> "controllers/"
|
|
107
|
+
*/
|
|
108
|
+
export function globalNameDir(globalName) {
|
|
109
|
+
const lastSlash = globalName.lastIndexOf('/');
|
|
110
|
+
if (lastSlash === -1)
|
|
111
|
+
return '';
|
|
112
|
+
return globalName.slice(0, lastSlash + 1);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Returns the parent directory of a directory path.
|
|
116
|
+
* e.g. "controllers/Admin/" -> "controllers/"
|
|
117
|
+
* "controllers/" -> ""
|
|
118
|
+
*/
|
|
119
|
+
export function parentOfDir(dir) {
|
|
120
|
+
const withoutTrailing = dir.slice(0, -1);
|
|
121
|
+
const lastSlash = withoutTrailing.lastIndexOf('/');
|
|
122
|
+
if (lastSlash === -1)
|
|
123
|
+
return '';
|
|
124
|
+
return withoutTrailing.slice(0, lastSlash + 1);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Checks whether a child controller violates the hierarchy convention.
|
|
128
|
+
* A controller should extend a controller in the same directory or the parent directory.
|
|
129
|
+
* Returns a warning message string if violated, or null if OK.
|
|
130
|
+
*/
|
|
131
|
+
export function hierarchyViolation(childGlobalName, parentGlobalName, controllersPath) {
|
|
132
|
+
const childDir = globalNameDir(childGlobalName);
|
|
133
|
+
const parentControllerDir = globalNameDir(parentGlobalName);
|
|
134
|
+
if (childDir === parentControllerDir || parentOfDir(childDir) === parentControllerDir) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const childFilePath = controllersPath + '/' + childGlobalName.replace(/^controllers\//, '') + '.ts';
|
|
138
|
+
return `[hierarchy violation: ${childFilePath} should extend a BaseController at its same level]`;
|
|
139
|
+
}
|
|
140
|
+
function collectTreeLines(controller, childrenMap, { depth, displayColumn, strippedPrefix, baseIndent, }, lines, controllersPath) {
|
|
141
|
+
const displayedName = controller.globalName.slice(strippedPrefix.length);
|
|
142
|
+
lines.push(controllerTreeLine(displayedName, depth, displayColumn, baseIndent));
|
|
143
|
+
// The actual column where the name appears on screen (may differ from displayColumn
|
|
144
|
+
// when MIN_DASHES pushes the name further right)
|
|
145
|
+
const effectiveDisplayColumn = actualDisplayColumn(displayColumn, depth, baseIndent);
|
|
146
|
+
if (depth > 0) {
|
|
147
|
+
const parentClass = Object.getPrototypeOf(controller);
|
|
148
|
+
if (parentClass !== PsychicController) {
|
|
149
|
+
const violation = hierarchyViolation(controller.globalName, parentClass.globalName, controllersPath);
|
|
150
|
+
if (violation) {
|
|
151
|
+
const warningIndent = ' '.repeat(effectiveDisplayColumn);
|
|
152
|
+
lines.push(warningIndent + colors.yellow(violation));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const childBaseIndent = depth === 0 ? ROOT_CHILD_INDENT : effectiveDisplayColumn - 1;
|
|
157
|
+
const children = childrenMap.get(controller) ?? [];
|
|
158
|
+
for (const child of children) {
|
|
159
|
+
const shared = sharedDirPrefix(controller.globalName, child.globalName);
|
|
160
|
+
const childDisplayColumn = effectiveDisplayColumn + shared.length - strippedPrefix.length;
|
|
161
|
+
collectTreeLines(child, childrenMap, {
|
|
162
|
+
depth: depth + 1,
|
|
163
|
+
displayColumn: childDisplayColumn,
|
|
164
|
+
strippedPrefix: shared,
|
|
165
|
+
baseIndent: childBaseIndent,
|
|
166
|
+
}, lines, controllersPath);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -10,6 +10,7 @@ import PsychicApp from '../psychic-app/index.js';
|
|
|
10
10
|
import enumsFileStr from './helpers/enumsFileStr.js';
|
|
11
11
|
import generateRouteTypes from './helpers/generateRouteTypes.js';
|
|
12
12
|
import { OpenApiSpecDiff } from './helpers/OpenApiSpecDiff.js';
|
|
13
|
+
import printControllerHierarchy, { controllerHierarchyViolations, } from './helpers/printControllerHierarchy.js';
|
|
13
14
|
import printRoutes from './helpers/printRoutes.js';
|
|
14
15
|
export { BreakingChangesDetectedInOpenApiSpecError } from './helpers/OpenApiSpecDiff.js';
|
|
15
16
|
export default class PsychicBin {
|
|
@@ -26,6 +27,12 @@ export default class PsychicBin {
|
|
|
26
27
|
static printRoutes() {
|
|
27
28
|
printRoutes();
|
|
28
29
|
}
|
|
30
|
+
static printControllerHierarchy(controllersPath) {
|
|
31
|
+
printControllerHierarchy(controllersPath);
|
|
32
|
+
}
|
|
33
|
+
static controllerHierarchyViolations(controllersPath) {
|
|
34
|
+
return controllerHierarchyViolations(controllersPath);
|
|
35
|
+
}
|
|
29
36
|
static async sync({ bypassDreamSync = false, schemaOnly = false, } = {}) {
|
|
30
37
|
if (!bypassDreamSync)
|
|
31
38
|
await DreamBin.sync(() => { }, { schemaOnly });
|
|
@@ -160,6 +160,31 @@ export default class PsychicCLI {
|
|
|
160
160
|
await generateSyncOpenapiTypescriptInitializer(openapiFilepath, outfile, initializerName);
|
|
161
161
|
process.exit();
|
|
162
162
|
});
|
|
163
|
+
program
|
|
164
|
+
.command('inspect:controller-hierarchy')
|
|
165
|
+
.alias('i:controller-hierarchy')
|
|
166
|
+
.description('Displays the inheritance hierarchy of all PsychicController classes in the project.')
|
|
167
|
+
.argument('[path]', 'the controllers directory to scan (defaults to the configured controllers path)')
|
|
168
|
+
.action(async (controllersPath) => {
|
|
169
|
+
await initializePsychicApp();
|
|
170
|
+
PsychicBin.printControllerHierarchy(controllersPath);
|
|
171
|
+
process.exit();
|
|
172
|
+
});
|
|
173
|
+
program
|
|
174
|
+
.command('check:controller-hierarchy')
|
|
175
|
+
.description('Checks that all controllers extend a controller in the same or parent directory. Exits with an error if any violations are found.')
|
|
176
|
+
.argument('[path]', 'the controllers directory to scan (defaults to the configured controllers path)')
|
|
177
|
+
.action(async (controllersPath) => {
|
|
178
|
+
await initializePsychicApp();
|
|
179
|
+
const violations = PsychicBin.controllerHierarchyViolations(controllersPath);
|
|
180
|
+
if (violations.length > 0) {
|
|
181
|
+
for (const violation of violations) {
|
|
182
|
+
console.error(violation);
|
|
183
|
+
}
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
process.exit();
|
|
187
|
+
});
|
|
163
188
|
program
|
|
164
189
|
.command('routes')
|
|
165
190
|
.description('Prints a list of routes defined by the application, including path arguments and the controller/action reached by the route.')
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import PsychicController from '../../controller/index.js';
|
|
2
|
+
export default function printControllerHierarchy(controllersPath?: string): void;
|
|
3
|
+
export declare function resolveControllerClasses(controllersPath?: string): {
|
|
4
|
+
controllerClasses: (typeof PsychicController)[];
|
|
5
|
+
resolvedPath: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function controllerHierarchyLines(controllersPath?: string): string[];
|
|
8
|
+
export declare function controllerHierarchyViolations(controllersPath?: string): string[];
|
|
9
|
+
export declare function buildChildrenMap(controllerClasses: (typeof PsychicController)[]): Map<typeof PsychicController, (typeof PsychicController)[]>;
|
|
10
|
+
export declare function controllerTreeLine(displayedName: string, depth: number, displayColumn: number, baseIndent: number): string;
|
|
11
|
+
/**
|
|
12
|
+
* Computes the actual column where the name starts on screen,
|
|
13
|
+
* accounting for MIN_DASHES potentially pushing the name further right.
|
|
14
|
+
*/
|
|
15
|
+
export declare function actualDisplayColumn(displayColumn: number, depth: number, baseIndent: number): number;
|
|
16
|
+
export declare function sharedDirPrefix(a: string, b: string): string;
|
|
17
|
+
export declare function globalPrefixFromPath(path: string, defaultControllersPath: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Returns the directory portion of a globalName.
|
|
20
|
+
* e.g. "controllers/Admin/AuthedController" -> "controllers/Admin/"
|
|
21
|
+
* "controllers/ApplicationController" -> "controllers/"
|
|
22
|
+
*/
|
|
23
|
+
export declare function globalNameDir(globalName: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Returns the parent directory of a directory path.
|
|
26
|
+
* e.g. "controllers/Admin/" -> "controllers/"
|
|
27
|
+
* "controllers/" -> ""
|
|
28
|
+
*/
|
|
29
|
+
export declare function parentOfDir(dir: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Checks whether a child controller violates the hierarchy convention.
|
|
32
|
+
* A controller should extend a controller in the same directory or the parent directory.
|
|
33
|
+
* Returns a warning message string if violated, or null if OK.
|
|
34
|
+
*/
|
|
35
|
+
export declare function hierarchyViolation(childGlobalName: string, parentGlobalName: string, controllersPath: string): string | null;
|
|
@@ -10,6 +10,8 @@ export default class PsychicBin {
|
|
|
10
10
|
modelName?: string;
|
|
11
11
|
}): Promise<void>;
|
|
12
12
|
static printRoutes(): void;
|
|
13
|
+
static printControllerHierarchy(controllersPath?: string): void;
|
|
14
|
+
static controllerHierarchyViolations(controllersPath?: string): string[];
|
|
13
15
|
static sync({ bypassDreamSync, schemaOnly, }?: {
|
|
14
16
|
bypassDreamSync?: boolean;
|
|
15
17
|
schemaOnly?: boolean;
|