@remix-run/cli 0.0.0 → 0.1.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/LICENSE +21 -0
- package/README.md +80 -3
- package/bootstrap/AGENTS.md +35 -0
- package/bootstrap/README.md +27 -0
- package/bootstrap/app/controllers/auth.tsx +21 -0
- package/bootstrap/app/controllers/home.tsx +26 -0
- package/bootstrap/app/router.ts +10 -0
- package/bootstrap/app/routes.ts +6 -0
- package/bootstrap/app/ui/document.tsx +21 -0
- package/bootstrap/app/ui/layout.tsx +22 -0
- package/bootstrap/app/utils/render.tsx +7 -0
- package/bootstrap/package.json +22 -0
- package/bootstrap/server.ts +37 -0
- package/bootstrap/tsconfig.json +19 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/bootstrap-project.d.ts +17 -0
- package/dist/lib/bootstrap-project.d.ts.map +1 -0
- package/dist/lib/bootstrap-project.js +137 -0
- package/dist/lib/cli-context.d.ts +11 -0
- package/dist/lib/cli-context.d.ts.map +1 -0
- package/dist/lib/cli-context.js +19 -0
- package/dist/lib/cli.d.ts +6 -0
- package/dist/lib/cli.d.ts.map +1 -0
- package/dist/lib/cli.js +87 -0
- package/dist/lib/commands/completion.d.ts +3 -0
- package/dist/lib/commands/completion.d.ts.map +1 -0
- package/dist/lib/commands/completion.js +51 -0
- package/dist/lib/commands/doctor.d.ts +4 -0
- package/dist/lib/commands/doctor.d.ts.map +1 -0
- package/dist/lib/commands/doctor.js +293 -0
- package/dist/lib/commands/help.d.ts +4 -0
- package/dist/lib/commands/help.d.ts.map +1 -0
- package/dist/lib/commands/help.js +124 -0
- package/dist/lib/commands/new.d.ts +4 -0
- package/dist/lib/commands/new.d.ts.map +1 -0
- package/dist/lib/commands/new.js +70 -0
- package/dist/lib/commands/routes.d.ts +4 -0
- package/dist/lib/commands/routes.d.ts.map +1 -0
- package/dist/lib/commands/routes.js +161 -0
- package/dist/lib/commands/skills.d.ts +6 -0
- package/dist/lib/commands/skills.d.ts.map +1 -0
- package/dist/lib/commands/skills.js +222 -0
- package/dist/lib/commands/test.d.ts +4 -0
- package/dist/lib/commands/test.d.ts.map +1 -0
- package/dist/lib/commands/test.js +19 -0
- package/dist/lib/commands/version.d.ts +4 -0
- package/dist/lib/commands/version.d.ts.map +1 -0
- package/dist/lib/commands/version.js +29 -0
- package/dist/lib/completion.d.ts +12 -0
- package/dist/lib/completion.d.ts.map +1 -0
- package/dist/lib/completion.js +408 -0
- package/dist/lib/contained-path.d.ts +2 -0
- package/dist/lib/contained-path.d.ts.map +1 -0
- package/dist/lib/contained-path.js +10 -0
- package/dist/lib/controller-files.d.ts +14 -0
- package/dist/lib/controller-files.d.ts.map +1 -0
- package/dist/lib/controller-files.js +53 -0
- package/dist/lib/controller-ownership.d.ts +41 -0
- package/dist/lib/controller-ownership.d.ts.map +1 -0
- package/dist/lib/controller-ownership.js +224 -0
- package/dist/lib/display-path.d.ts +2 -0
- package/dist/lib/display-path.d.ts.map +1 -0
- package/dist/lib/display-path.js +12 -0
- package/dist/lib/doctor/controller-findings.d.ts +4 -0
- package/dist/lib/doctor/controller-findings.d.ts.map +1 -0
- package/dist/lib/doctor/controller-findings.js +132 -0
- package/dist/lib/doctor/controller-fix-plans.d.ts +4 -0
- package/dist/lib/doctor/controller-fix-plans.d.ts.map +1 -0
- package/dist/lib/doctor/controller-fix-plans.js +130 -0
- package/dist/lib/doctor/controller-placeholders.d.ts +4 -0
- package/dist/lib/doctor/controller-placeholders.d.ts.map +1 -0
- package/dist/lib/doctor/controller-placeholders.js +183 -0
- package/dist/lib/doctor/controllers.d.ts +8 -0
- package/dist/lib/doctor/controllers.d.ts.map +1 -0
- package/dist/lib/doctor/controllers.js +13 -0
- package/dist/lib/doctor/environment.d.ts +17 -0
- package/dist/lib/doctor/environment.d.ts.map +1 -0
- package/dist/lib/doctor/environment.js +226 -0
- package/dist/lib/doctor/fixes.d.ts +3 -0
- package/dist/lib/doctor/fixes.d.ts.map +1 -0
- package/dist/lib/doctor/fixes.js +65 -0
- package/dist/lib/doctor/project.d.ts +10 -0
- package/dist/lib/doctor/project.d.ts.map +1 -0
- package/dist/lib/doctor/project.js +371 -0
- package/dist/lib/doctor/types.d.ts +48 -0
- package/dist/lib/doctor/types.d.ts.map +1 -0
- package/dist/lib/doctor/types.js +15 -0
- package/dist/lib/errors.d.ts +173 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +318 -0
- package/dist/lib/help-text.d.ts +13 -0
- package/dist/lib/help-text.d.ts.map +1 -0
- package/dist/lib/help-text.js +43 -0
- package/dist/lib/load-route-map-worker.d.ts +2 -0
- package/dist/lib/load-route-map-worker.d.ts.map +1 -0
- package/dist/lib/load-route-map-worker.js +92 -0
- package/dist/lib/parse-args.d.ts +22 -0
- package/dist/lib/parse-args.d.ts.map +1 -0
- package/dist/lib/parse-args.js +35 -0
- package/dist/lib/remix-version.d.ts +2 -0
- package/dist/lib/remix-version.d.ts.map +1 -0
- package/dist/lib/remix-version.js +58 -0
- package/dist/lib/reporter.d.ts +56 -0
- package/dist/lib/reporter.d.ts.map +1 -0
- package/dist/lib/reporter.js +357 -0
- package/dist/lib/route-map.d.ts +38 -0
- package/dist/lib/route-map.d.ts.map +1 -0
- package/dist/lib/route-map.js +195 -0
- package/dist/lib/skills-cache.d.ts +19 -0
- package/dist/lib/skills-cache.d.ts.map +1 -0
- package/dist/lib/skills-cache.js +89 -0
- package/dist/lib/skills.d.ts +30 -0
- package/dist/lib/skills.d.ts.map +1 -0
- package/dist/lib/skills.js +441 -0
- package/dist/lib/terminal.d.ts +17 -0
- package/dist/lib/terminal.d.ts.map +1 -0
- package/dist/lib/terminal.js +66 -0
- package/package.json +48 -5
- package/src/index.ts +1 -0
- package/src/lib/bootstrap-project.ts +222 -0
- package/src/lib/cli-context.ts +38 -0
- package/src/lib/cli.ts +120 -0
- package/src/lib/commands/completion.ts +74 -0
- package/src/lib/commands/doctor.ts +393 -0
- package/src/lib/commands/help.ts +157 -0
- package/src/lib/commands/new.ts +104 -0
- package/src/lib/commands/routes.ts +251 -0
- package/src/lib/commands/skills.ts +306 -0
- package/src/lib/commands/test.ts +25 -0
- package/src/lib/commands/version.ts +39 -0
- package/src/lib/completion.ts +561 -0
- package/src/lib/contained-path.ts +13 -0
- package/src/lib/controller-files.ts +79 -0
- package/src/lib/controller-ownership.ts +397 -0
- package/src/lib/display-path.ts +16 -0
- package/src/lib/doctor/controller-findings.ts +157 -0
- package/src/lib/doctor/controller-fix-plans.ts +184 -0
- package/src/lib/doctor/controller-placeholders.ts +242 -0
- package/src/lib/doctor/controllers.ts +23 -0
- package/src/lib/doctor/environment.ts +289 -0
- package/src/lib/doctor/fixes.ts +84 -0
- package/src/lib/doctor/project.ts +435 -0
- package/src/lib/doctor/types.ts +95 -0
- package/src/lib/errors.ts +379 -0
- package/src/lib/help-text.ts +88 -0
- package/src/lib/load-route-map-worker.ts +110 -0
- package/src/lib/parse-args.ts +82 -0
- package/src/lib/remix-version.ts +80 -0
- package/src/lib/reporter.ts +535 -0
- package/src/lib/route-map.ts +303 -0
- package/src/lib/skills-cache.ts +140 -0
- package/src/lib/skills.ts +706 -0
- package/src/lib/terminal.ts +86 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { getActionOwnerCandidates, getControllerOwnerCandidates, getOwnerModuleBaseName, getPreferredOwnerDisplayPath, getRouteSubtreePath, isActionFileName, isControllerEntryFileName, toDiskSegment, } from "./controller-files.js";
|
|
4
|
+
export async function inspectControllerOwnership(appRoot, tree) {
|
|
5
|
+
let subtreePlans = buildOwnedSubtrees(tree);
|
|
6
|
+
let scan = await scanControllersDirectory(appRoot);
|
|
7
|
+
let subtrees = applyScanToSubtrees(subtreePlans, scan);
|
|
8
|
+
return {
|
|
9
|
+
orphanActionPaths: getOrphanActionPaths(subtreePlans, scan),
|
|
10
|
+
orphanControllerPaths: getOrphanControllerPaths(subtreePlans, scan),
|
|
11
|
+
orphanRouteDirectoryPaths: getOrphanRouteDirectoryPaths(subtreePlans, scan),
|
|
12
|
+
scan,
|
|
13
|
+
subtrees,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function buildOwnedSubtrees(tree, parentSegments = [], subtrees = []) {
|
|
17
|
+
for (let node of tree) {
|
|
18
|
+
let segments = [...parentSegments, toDiskSegment(node.key)];
|
|
19
|
+
if (node.kind === 'group') {
|
|
20
|
+
let alternateCandidates = getActionOwnerCandidates(segments);
|
|
21
|
+
let entryCandidates = getControllerOwnerCandidates(segments);
|
|
22
|
+
subtrees.push({
|
|
23
|
+
alternateCandidates,
|
|
24
|
+
alternateDisplayPath: getPreferredOwnerDisplayPath(alternateCandidates),
|
|
25
|
+
entryCandidates,
|
|
26
|
+
entryDisplayPath: getPreferredOwnerDisplayPath(entryCandidates),
|
|
27
|
+
kind: 'controller',
|
|
28
|
+
routeName: node.name,
|
|
29
|
+
subtreePath: getRouteSubtreePath(segments),
|
|
30
|
+
});
|
|
31
|
+
buildOwnedSubtrees(node.children, segments, subtrees);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (parentSegments.length > 0) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
let alternateCandidates = getControllerOwnerCandidates(segments);
|
|
38
|
+
let entryCandidates = getActionOwnerCandidates(segments);
|
|
39
|
+
subtrees.push({
|
|
40
|
+
alternateCandidates,
|
|
41
|
+
alternateDisplayPath: getPreferredOwnerDisplayPath(alternateCandidates),
|
|
42
|
+
entryCandidates,
|
|
43
|
+
entryDisplayPath: getPreferredOwnerDisplayPath(entryCandidates),
|
|
44
|
+
kind: 'action',
|
|
45
|
+
routeName: node.name,
|
|
46
|
+
subtreePath: getRouteSubtreePath(segments),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return subtrees;
|
|
50
|
+
}
|
|
51
|
+
async function scanControllersDirectory(appRoot) {
|
|
52
|
+
let controllersDir = path.join(appRoot, 'app', 'controllers');
|
|
53
|
+
let controllerEntryPaths = new Set();
|
|
54
|
+
let actionEntryPaths = new Set();
|
|
55
|
+
let rootDirectoryPaths = new Set();
|
|
56
|
+
let routeLocalFilePaths = new Set();
|
|
57
|
+
async function walk(currentDir, isRoot) {
|
|
58
|
+
let entries;
|
|
59
|
+
try {
|
|
60
|
+
entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
let nodeError = error;
|
|
64
|
+
if (nodeError.code === 'ENOENT' && isRoot) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
for (let entry of entries) {
|
|
70
|
+
let entryPath = path.join(currentDir, entry.name);
|
|
71
|
+
let relativePath = normalizeRelativePath(path.relative(appRoot, entryPath));
|
|
72
|
+
if (entry.isDirectory()) {
|
|
73
|
+
if (isRoot) {
|
|
74
|
+
rootDirectoryPaths.add(relativePath);
|
|
75
|
+
}
|
|
76
|
+
await walk(entryPath, false);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (!entry.isFile()) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (isControllerEntryFileName(entry.name)) {
|
|
83
|
+
controllerEntryPaths.add(relativePath);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (isRoot && isActionCandidate(entry.name)) {
|
|
87
|
+
actionEntryPaths.add(relativePath);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (!isRoot && isRouteLocalFileName(entry.name)) {
|
|
91
|
+
routeLocalFilePaths.add(relativePath);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
await walk(controllersDir, true);
|
|
96
|
+
return {
|
|
97
|
+
actionEntryPaths,
|
|
98
|
+
controllerEntryPaths,
|
|
99
|
+
rootDirectoryPaths,
|
|
100
|
+
routeLocalFilePaths,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function applyScanToSubtrees(subtreePlans, scan) {
|
|
104
|
+
let claimedRouteLocalPaths = claimFilesToDeepestSubtree([...scan.routeLocalFilePaths], subtreePlans);
|
|
105
|
+
let claimedContentPaths = claimFilesToDeepestSubtree(getNestedContentPaths(scan), subtreePlans);
|
|
106
|
+
return subtreePlans.map((subtree) => {
|
|
107
|
+
let actualAlternatePaths = findOwnerPaths(scan, invertOwnerKind(subtree.kind), subtree.alternateCandidates);
|
|
108
|
+
let actualEntryPaths = findOwnerPaths(scan, subtree.kind, subtree.entryCandidates);
|
|
109
|
+
return {
|
|
110
|
+
...subtree,
|
|
111
|
+
actualAlternatePath: actualAlternatePaths[0] ?? null,
|
|
112
|
+
actualAlternatePaths,
|
|
113
|
+
actualEntryPath: actualEntryPaths[0] ?? null,
|
|
114
|
+
actualEntryPaths,
|
|
115
|
+
claimedFilePaths: claimedContentPaths.get(subtree.routeName) ?? [],
|
|
116
|
+
claimedRouteLocalFilePaths: claimedRouteLocalPaths.get(subtree.routeName) ?? [],
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function getNestedContentPaths(scan) {
|
|
121
|
+
let nestedControllerPaths = [...scan.controllerEntryPaths].filter((filePath) => isNestedControllerPath(filePath));
|
|
122
|
+
return [...new Set([...nestedControllerPaths, ...scan.routeLocalFilePaths])].sort();
|
|
123
|
+
}
|
|
124
|
+
function claimFilesToDeepestSubtree(filePaths, subtreePlans) {
|
|
125
|
+
let subtreesByDepth = [...subtreePlans].sort((left, right) => {
|
|
126
|
+
if (right.subtreePath.length !== left.subtreePath.length) {
|
|
127
|
+
return right.subtreePath.length - left.subtreePath.length;
|
|
128
|
+
}
|
|
129
|
+
return left.routeName.localeCompare(right.routeName);
|
|
130
|
+
});
|
|
131
|
+
let claims = new Map();
|
|
132
|
+
for (let filePath of filePaths.sort()) {
|
|
133
|
+
let matchingSubtree = subtreesByDepth.find((subtree) => isWithinDirectory(filePath, subtree.subtreePath));
|
|
134
|
+
if (matchingSubtree == null) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
let claimedPaths = claims.get(matchingSubtree.routeName);
|
|
138
|
+
if (claimedPaths == null) {
|
|
139
|
+
claims.set(matchingSubtree.routeName, [filePath]);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
claimedPaths.push(filePath);
|
|
143
|
+
}
|
|
144
|
+
return claims;
|
|
145
|
+
}
|
|
146
|
+
function getOrphanActionPaths(subtreePlans, scan) {
|
|
147
|
+
let expectedActionPaths = new Set(subtreePlans
|
|
148
|
+
.filter((subtree) => subtree.kind === 'action')
|
|
149
|
+
.flatMap((subtree) => subtree.entryCandidates));
|
|
150
|
+
let alternateActionPaths = new Set(subtreePlans
|
|
151
|
+
.filter((subtree) => subtree.kind === 'controller')
|
|
152
|
+
.flatMap((subtree) => subtree.alternateCandidates));
|
|
153
|
+
return [...scan.actionEntryPaths]
|
|
154
|
+
.filter((filePath) => !expectedActionPaths.has(filePath))
|
|
155
|
+
.filter((filePath) => !alternateActionPaths.has(filePath))
|
|
156
|
+
.sort();
|
|
157
|
+
}
|
|
158
|
+
function getOrphanControllerPaths(subtreePlans, scan) {
|
|
159
|
+
let expectedControllerPaths = new Set(subtreePlans
|
|
160
|
+
.filter((subtree) => subtree.kind === 'controller')
|
|
161
|
+
.flatMap((subtree) => subtree.entryCandidates));
|
|
162
|
+
let alternateControllerPaths = new Set(subtreePlans
|
|
163
|
+
.filter((subtree) => subtree.kind === 'action')
|
|
164
|
+
.flatMap((subtree) => subtree.alternateCandidates));
|
|
165
|
+
return [...scan.controllerEntryPaths]
|
|
166
|
+
.filter((filePath) => !expectedControllerPaths.has(filePath))
|
|
167
|
+
.filter((filePath) => !alternateControllerPaths.has(filePath))
|
|
168
|
+
.sort();
|
|
169
|
+
}
|
|
170
|
+
function getOrphanRouteDirectoryPaths(subtreePlans, scan) {
|
|
171
|
+
let expectedRootDirectories = new Set(subtreePlans.map((subtree) => getTopLevelSubtreePath(subtree.subtreePath)));
|
|
172
|
+
let actualControllerDirectories = [...scan.controllerEntryPaths].map((controllerPath) => normalizeRelativePath(path.dirname(controllerPath)));
|
|
173
|
+
return [...scan.rootDirectoryPaths]
|
|
174
|
+
.filter((directoryPath) => !expectedRootDirectories.has(directoryPath))
|
|
175
|
+
.filter((directoryPath) => !actualControllerDirectories.some((controllerPath) => isDirectoryWithinDirectory(controllerPath, directoryPath)))
|
|
176
|
+
.sort();
|
|
177
|
+
}
|
|
178
|
+
function hasOwnerPath(scan, kind, filePath) {
|
|
179
|
+
return kind === 'action'
|
|
180
|
+
? scan.actionEntryPaths.has(filePath)
|
|
181
|
+
: scan.controllerEntryPaths.has(filePath);
|
|
182
|
+
}
|
|
183
|
+
function invertOwnerKind(kind) {
|
|
184
|
+
return kind === 'action' ? 'controller' : 'action';
|
|
185
|
+
}
|
|
186
|
+
function isActionCandidate(fileName) {
|
|
187
|
+
return isActionFileName(fileName);
|
|
188
|
+
}
|
|
189
|
+
function isRouteLocalFileName(fileName) {
|
|
190
|
+
let baseName = getOwnerModuleBaseName(fileName);
|
|
191
|
+
return (baseName != null &&
|
|
192
|
+
baseName !== 'controller' &&
|
|
193
|
+
!baseName.endsWith('.test') &&
|
|
194
|
+
!baseName.endsWith('.spec'));
|
|
195
|
+
}
|
|
196
|
+
function findOwnerPath(scan, kind, candidatePaths) {
|
|
197
|
+
return findOwnerPaths(scan, kind, candidatePaths)[0] ?? null;
|
|
198
|
+
}
|
|
199
|
+
function findOwnerPaths(scan, kind, candidatePaths) {
|
|
200
|
+
let existingPaths = [];
|
|
201
|
+
for (let candidatePath of candidatePaths) {
|
|
202
|
+
if (hasOwnerPath(scan, kind, candidatePath)) {
|
|
203
|
+
existingPaths.push(candidatePath);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return existingPaths;
|
|
207
|
+
}
|
|
208
|
+
function isNestedControllerPath(filePath) {
|
|
209
|
+
return (filePath.startsWith('app/controllers/') &&
|
|
210
|
+
filePath.slice('app/controllers/'.length).includes('/'));
|
|
211
|
+
}
|
|
212
|
+
function isWithinDirectory(filePath, directoryPath) {
|
|
213
|
+
return filePath.startsWith(`${directoryPath}/`);
|
|
214
|
+
}
|
|
215
|
+
function isDirectoryWithinDirectory(directoryPath, parentDirectoryPath) {
|
|
216
|
+
return (directoryPath === parentDirectoryPath || isWithinDirectory(directoryPath, parentDirectoryPath));
|
|
217
|
+
}
|
|
218
|
+
function getTopLevelSubtreePath(subtreePath) {
|
|
219
|
+
let subtreeSegments = subtreePath.slice('app/controllers/'.length).split('/');
|
|
220
|
+
return getRouteSubtreePath([subtreeSegments[0]]);
|
|
221
|
+
}
|
|
222
|
+
function normalizeRelativePath(filePath) {
|
|
223
|
+
return filePath.split(path.sep).join('/');
|
|
224
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"display-path.d.ts","sourceRoot":"","sources":["../../src/lib/display-path.ts"],"names":[],"mappings":"AAGA,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAE,MAAsB,GAAG,MAAM,CAYpF"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import * as process from 'node:process';
|
|
3
|
+
export function getDisplayPath(filePath, cwd = process.cwd()) {
|
|
4
|
+
let relativePath = path.relative(path.resolve(cwd), path.resolve(filePath));
|
|
5
|
+
if (relativePath.length === 0) {
|
|
6
|
+
return '.';
|
|
7
|
+
}
|
|
8
|
+
if (path.isAbsolute(relativePath)) {
|
|
9
|
+
return filePath;
|
|
10
|
+
}
|
|
11
|
+
return relativePath;
|
|
12
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ControllerOwnership } from '../controller-ownership.ts';
|
|
2
|
+
import type { DoctorFinding, DoctorFixPlan } from './types.ts';
|
|
3
|
+
export declare function getControllerFindings(ownership: ControllerOwnership, fixPlans: DoctorFixPlan[]): DoctorFinding[];
|
|
4
|
+
//# sourceMappingURL=controller-findings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller-findings.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/controller-findings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAgB,MAAM,4BAA4B,CAAA;AACnF,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE9D,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,mBAAmB,EAC9B,QAAQ,EAAE,aAAa,EAAE,GACxB,aAAa,EAAE,CAyBjB"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
export function getControllerFindings(ownership, fixPlans) {
|
|
2
|
+
return [
|
|
3
|
+
...getSubtreeFindings(ownership.subtrees, fixPlans),
|
|
4
|
+
...ownership.orphanActionPaths.map((actualPath) => ({
|
|
5
|
+
actualPath,
|
|
6
|
+
code: 'orphan-action',
|
|
7
|
+
message: `Standalone action ${actualPath} does not match any top-level route.`,
|
|
8
|
+
severity: 'warn',
|
|
9
|
+
suite: 'controllers',
|
|
10
|
+
})),
|
|
11
|
+
...ownership.orphanControllerPaths.map((actualPath) => ({
|
|
12
|
+
actualPath,
|
|
13
|
+
code: 'orphan-controller',
|
|
14
|
+
message: `Controller ${actualPath} does not match any route group.`,
|
|
15
|
+
severity: 'warn',
|
|
16
|
+
suite: 'controllers',
|
|
17
|
+
})),
|
|
18
|
+
...ownership.orphanRouteDirectoryPaths.map((actualPath) => ({
|
|
19
|
+
actualPath,
|
|
20
|
+
code: 'orphan-route-directory',
|
|
21
|
+
message: `Directory ${actualPath} does not match any route subtree.`,
|
|
22
|
+
severity: 'warn',
|
|
23
|
+
suite: 'controllers',
|
|
24
|
+
})),
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
function getSubtreeFindings(subtrees, fixPlans) {
|
|
28
|
+
let findings = [];
|
|
29
|
+
let fixableFindingKeys = new Set(fixPlans.map((fixPlan) => `${fixPlan.code}:${fixPlan.routeName ?? ''}`));
|
|
30
|
+
for (let subtree of subtrees) {
|
|
31
|
+
let actualEntryPath = subtree.actualEntryPath ?? undefined;
|
|
32
|
+
let actualAlternatePath = subtree.actualAlternatePath ?? undefined;
|
|
33
|
+
if (subtree.actualEntryPaths.length > 1) {
|
|
34
|
+
findings.push({
|
|
35
|
+
actualPath: subtree.actualEntryPaths[0],
|
|
36
|
+
code: 'duplicate-owner-file',
|
|
37
|
+
expectedPath: subtree.entryDisplayPath,
|
|
38
|
+
message: `Route "${subtree.routeName}" has multiple ${subtree.kind} files: ${subtree.actualEntryPaths.join(', ')}. Keep only one ${subtree.kind} owner file.`,
|
|
39
|
+
routeName: subtree.routeName,
|
|
40
|
+
severity: 'warn',
|
|
41
|
+
suite: 'controllers',
|
|
42
|
+
});
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (subtree.actualAlternatePaths.length > 1) {
|
|
46
|
+
let alternateKind = subtree.kind === 'action' ? 'controller' : 'action';
|
|
47
|
+
findings.push({
|
|
48
|
+
actualPath: subtree.actualAlternatePaths[0],
|
|
49
|
+
code: 'duplicate-owner-file',
|
|
50
|
+
expectedPath: subtree.entryDisplayPath,
|
|
51
|
+
message: `Route "${subtree.routeName}" has multiple ${alternateKind} files: ${subtree.actualAlternatePaths.join(', ')}. Keep only one ${alternateKind} owner file.`,
|
|
52
|
+
routeName: subtree.routeName,
|
|
53
|
+
severity: 'warn',
|
|
54
|
+
suite: 'controllers',
|
|
55
|
+
});
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (actualEntryPath != null && actualAlternatePath != null) {
|
|
59
|
+
findings.push({
|
|
60
|
+
actualPath: actualAlternatePath,
|
|
61
|
+
code: 'ambiguous-owner',
|
|
62
|
+
expectedPath: actualEntryPath,
|
|
63
|
+
message: subtree.kind === 'action'
|
|
64
|
+
? `Route "${subtree.routeName}" has both action ${actualEntryPath} and controller ${actualAlternatePath}.`
|
|
65
|
+
: `Route "${subtree.routeName}" has both controller ${actualEntryPath} and standalone action ${actualAlternatePath}.`,
|
|
66
|
+
routeName: subtree.routeName,
|
|
67
|
+
severity: 'warn',
|
|
68
|
+
suite: 'controllers',
|
|
69
|
+
});
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (actualAlternatePath != null) {
|
|
73
|
+
findings.push({
|
|
74
|
+
actualPath: actualAlternatePath,
|
|
75
|
+
code: 'wrong-owner-kind',
|
|
76
|
+
expectedPath: subtree.entryDisplayPath,
|
|
77
|
+
message: subtree.kind === 'action'
|
|
78
|
+
? `Route "${subtree.routeName}" expects action ${subtree.entryDisplayPath}, but found controller ${actualAlternatePath}.`
|
|
79
|
+
: `Route "${subtree.routeName}" expects controller ${subtree.entryDisplayPath}, but found standalone action ${actualAlternatePath}.`,
|
|
80
|
+
fixable: fixableFindingKeys.has(`wrong-owner-kind:${subtree.routeName}`),
|
|
81
|
+
routeName: subtree.routeName,
|
|
82
|
+
severity: 'warn',
|
|
83
|
+
suite: 'controllers',
|
|
84
|
+
});
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (subtree.kind === 'controller' && subtree.actualEntryPath == null) {
|
|
88
|
+
let code = subtree.claimedFilePaths.length > 0
|
|
89
|
+
? 'incomplete-controller'
|
|
90
|
+
: 'missing-owner';
|
|
91
|
+
findings.push({
|
|
92
|
+
actualPath: subtree.claimedFilePaths.length > 0 ? subtree.subtreePath : undefined,
|
|
93
|
+
code,
|
|
94
|
+
expectedPath: subtree.entryDisplayPath,
|
|
95
|
+
fixable: fixableFindingKeys.has(`${code}:${subtree.routeName}`),
|
|
96
|
+
message: subtree.claimedFilePaths.length > 0
|
|
97
|
+
? `Route "${subtree.routeName}" has files under ${subtree.subtreePath}, but is missing controller ${subtree.entryDisplayPath}.`
|
|
98
|
+
: `Route "${subtree.routeName}" is missing controller ${subtree.entryDisplayPath}.`,
|
|
99
|
+
routeName: subtree.routeName,
|
|
100
|
+
severity: 'warn',
|
|
101
|
+
suite: 'controllers',
|
|
102
|
+
});
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (subtree.kind === 'action' && subtree.claimedFilePaths.length > 0) {
|
|
106
|
+
findings.push({
|
|
107
|
+
actualPath: subtree.subtreePath,
|
|
108
|
+
code: 'promotion-drift',
|
|
109
|
+
expectedPath: actualEntryPath ?? subtree.entryDisplayPath,
|
|
110
|
+
message: actualEntryPath == null
|
|
111
|
+
? `Route "${subtree.routeName}" has files under ${subtree.subtreePath}, but is still expected to use action ${subtree.entryDisplayPath}. Promote it to controller ${subtree.alternateDisplayPath} or move the files back into ${subtree.entryDisplayPath}.`
|
|
112
|
+
: `Route "${subtree.routeName}" uses action ${actualEntryPath}, but also has files under ${subtree.subtreePath}. Promote it to controller ${subtree.alternateDisplayPath} or keep the route in ${actualEntryPath}.`,
|
|
113
|
+
routeName: subtree.routeName,
|
|
114
|
+
severity: 'warn',
|
|
115
|
+
suite: 'controllers',
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (subtree.actualEntryPath == null) {
|
|
120
|
+
findings.push({
|
|
121
|
+
code: 'missing-owner',
|
|
122
|
+
expectedPath: subtree.entryDisplayPath,
|
|
123
|
+
fixable: fixableFindingKeys.has(`missing-owner:${subtree.routeName}`),
|
|
124
|
+
message: `Route "${subtree.routeName}" is missing action ${subtree.entryDisplayPath}.`,
|
|
125
|
+
routeName: subtree.routeName,
|
|
126
|
+
severity: 'warn',
|
|
127
|
+
suite: 'controllers',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return findings;
|
|
132
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ControllerOwnership, OwnershipRouteNode } from '../controller-ownership.ts';
|
|
2
|
+
import type { DoctorFixPlan } from './types.ts';
|
|
3
|
+
export declare function getControllerFixPlans(appRoot: string, tree: OwnershipRouteNode[], ownership: ControllerOwnership): Promise<DoctorFixPlan[]>;
|
|
4
|
+
//# sourceMappingURL=controller-fix-plans.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller-fix-plans.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/controller-fix-plans.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,mBAAmB,EAEnB,kBAAkB,EACnB,MAAM,4BAA4B,CAAA;AAEnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAI/C,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,kBAAkB,EAAE,EAC1B,SAAS,EAAE,mBAAmB,GAC7B,OAAO,CAAC,aAAa,EAAE,CAAC,CAqD1B"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { getOwnerCandidateForExtension, getOwnerFileExtension, } from "../controller-files.js";
|
|
4
|
+
import { renderActionPlaceholder, renderControllerPlaceholder } from "./controller-placeholders.js";
|
|
5
|
+
const OWNER_EXTENSION_PRIORITY = ['.tsx', '.ts', '.jsx', '.js'];
|
|
6
|
+
export async function getControllerFixPlans(appRoot, tree, ownership) {
|
|
7
|
+
let routeNodesByName = getRouteNodesByName(tree);
|
|
8
|
+
let hasTsconfig = await pathExists(path.join(appRoot, 'tsconfig.json'));
|
|
9
|
+
let resolvedEntryPathByRouteName = new Map();
|
|
10
|
+
for (let subtree of ownership.subtrees) {
|
|
11
|
+
if (subtree.actualEntryPath != null) {
|
|
12
|
+
resolvedEntryPathByRouteName.set(subtree.routeName, subtree.actualEntryPath);
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
let fixCode = getFixCodeForSubtree(subtree);
|
|
16
|
+
if (fixCode == null) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
let extension = inferOwnerExtension(subtree, ownership, hasTsconfig);
|
|
20
|
+
let targetPath = getOwnerCandidateForExtension(subtree.entryCandidates, extension) ?? subtree.entryDisplayPath;
|
|
21
|
+
resolvedEntryPathByRouteName.set(subtree.routeName, targetPath);
|
|
22
|
+
}
|
|
23
|
+
let fixPlans = [];
|
|
24
|
+
for (let subtree of ownership.subtrees) {
|
|
25
|
+
let code = getFixCodeForSubtree(subtree);
|
|
26
|
+
if (code == null) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
let routeNode = routeNodesByName.get(subtree.routeName);
|
|
30
|
+
let entryPath = resolvedEntryPathByRouteName.get(subtree.routeName);
|
|
31
|
+
if (routeNode == null || entryPath == null) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
let contents = subtree.kind === 'action'
|
|
35
|
+
? renderActionPlaceholder(routeNode, entryPath)
|
|
36
|
+
: renderControllerPlaceholder(routeNode, entryPath, resolvedEntryPathByRouteName);
|
|
37
|
+
fixPlans.push({
|
|
38
|
+
code,
|
|
39
|
+
contents,
|
|
40
|
+
kind: 'create-file',
|
|
41
|
+
path: entryPath,
|
|
42
|
+
routeName: subtree.routeName,
|
|
43
|
+
suite: 'controllers',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return fixPlans;
|
|
47
|
+
}
|
|
48
|
+
function getFixCodeForSubtree(subtree) {
|
|
49
|
+
if (subtree.actualEntryPaths.length > 1 || subtree.actualAlternatePaths.length > 1) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
if (subtree.actualEntryPath != null) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (subtree.actualAlternatePath != null) {
|
|
56
|
+
return 'wrong-owner-kind';
|
|
57
|
+
}
|
|
58
|
+
if (subtree.kind === 'controller') {
|
|
59
|
+
return subtree.claimedFilePaths.length > 0 ? 'incomplete-controller' : 'missing-owner';
|
|
60
|
+
}
|
|
61
|
+
if (subtree.claimedFilePaths.length > 0) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return 'missing-owner';
|
|
65
|
+
}
|
|
66
|
+
function inferOwnerExtension(subtree, ownership, hasTsconfig) {
|
|
67
|
+
let subtreeExtension = getMostCommonOwnerExtension(subtree.claimedRouteLocalFilePaths);
|
|
68
|
+
if (subtreeExtension != null) {
|
|
69
|
+
return subtreeExtension;
|
|
70
|
+
}
|
|
71
|
+
let alternateExtension = getMostCommonOwnerExtension(subtree.actualAlternatePaths);
|
|
72
|
+
if (alternateExtension != null) {
|
|
73
|
+
return alternateExtension;
|
|
74
|
+
}
|
|
75
|
+
let projectExtension = getMostCommonOwnerExtension([
|
|
76
|
+
...ownership.scan.actionEntryPaths,
|
|
77
|
+
...ownership.scan.controllerEntryPaths,
|
|
78
|
+
]);
|
|
79
|
+
if (projectExtension != null) {
|
|
80
|
+
return projectExtension;
|
|
81
|
+
}
|
|
82
|
+
return hasTsconfig ? '.tsx' : '.js';
|
|
83
|
+
}
|
|
84
|
+
function getMostCommonOwnerExtension(filePaths) {
|
|
85
|
+
let counts = new Map();
|
|
86
|
+
for (let filePath of filePaths) {
|
|
87
|
+
let extension = getOwnerFileExtension(filePath);
|
|
88
|
+
if (extension == null) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
counts.set(extension, (counts.get(extension) ?? 0) + 1);
|
|
92
|
+
}
|
|
93
|
+
if (counts.size === 0) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
let bestExtension = null;
|
|
97
|
+
let bestCount = -1;
|
|
98
|
+
for (let extension of OWNER_EXTENSION_PRIORITY) {
|
|
99
|
+
let count = counts.get(extension) ?? 0;
|
|
100
|
+
if (count > bestCount) {
|
|
101
|
+
bestCount = count;
|
|
102
|
+
bestExtension = extension;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return bestExtension;
|
|
106
|
+
}
|
|
107
|
+
function getRouteNodesByName(tree) {
|
|
108
|
+
let routeNodesByName = new Map();
|
|
109
|
+
function visit(nodes) {
|
|
110
|
+
for (let node of nodes) {
|
|
111
|
+
routeNodesByName.set(node.name, node);
|
|
112
|
+
visit(node.children);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
visit(tree);
|
|
116
|
+
return routeNodesByName;
|
|
117
|
+
}
|
|
118
|
+
async function pathExists(filePath) {
|
|
119
|
+
try {
|
|
120
|
+
await fs.access(filePath);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
let nodeError = error;
|
|
125
|
+
if (nodeError.code === 'ENOENT') {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { OwnershipRouteNode } from '../controller-ownership.ts';
|
|
2
|
+
export declare function renderActionPlaceholder(routeNode: OwnershipRouteNode, entryPath: string): string;
|
|
3
|
+
export declare function renderControllerPlaceholder(routeNode: OwnershipRouteNode, entryPath: string, resolvedEntryPathByRouteName: Map<string, string>): string;
|
|
4
|
+
//# sourceMappingURL=controller-placeholders.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller-placeholders.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/controller-placeholders.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAmDpE,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAgChG;AAED,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,kBAAkB,EAC7B,SAAS,EAAE,MAAM,EACjB,4BAA4B,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAChD,MAAM,CAkCR"}
|