@remix-run/cli 0.2.0 → 0.3.1
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/lib/bootstrap-project.d.ts.map +1 -1
- package/dist/lib/bootstrap-project.js +32 -9
- package/dist/lib/cli.d.ts +29 -0
- package/dist/lib/cli.d.ts.map +1 -1
- package/dist/lib/cli.js +26 -4
- package/dist/lib/commands/completion.d.ts.map +1 -1
- package/dist/lib/commands/completion.js +5 -1
- package/dist/lib/commands/doctor.js +18 -18
- package/dist/lib/commands/routes.js +3 -3
- package/dist/lib/completion.js +1 -5
- package/dist/lib/controller-files.d.ts +0 -1
- package/dist/lib/controller-files.d.ts.map +1 -1
- package/dist/lib/controller-files.js +3 -5
- package/dist/lib/controller-ownership.d.ts +9 -9
- package/dist/lib/controller-ownership.d.ts.map +1 -1
- package/dist/lib/controller-ownership.js +56 -91
- package/dist/lib/doctor/controller-findings.d.ts +1 -1
- package/dist/lib/doctor/controller-findings.d.ts.map +1 -1
- package/dist/lib/doctor/controller-findings.js +15 -87
- package/dist/lib/doctor/controller-fix-plans.d.ts.map +1 -1
- package/dist/lib/doctor/controller-fix-plans.js +13 -24
- package/dist/lib/doctor/controller-placeholders.d.ts +2 -3
- package/dist/lib/doctor/controller-placeholders.d.ts.map +1 -1
- package/dist/lib/doctor/controller-placeholders.js +18 -149
- package/dist/lib/doctor/controllers.js +1 -1
- package/dist/lib/doctor/project.js +60 -52
- package/dist/lib/doctor/types.d.ts +2 -2
- package/dist/lib/doctor/types.d.ts.map +1 -1
- package/dist/lib/help-text.js +1 -1
- package/dist/lib/load-route-map-worker.js +17 -9
- package/dist/lib/route-map.d.ts +1 -1
- package/dist/lib/route-map.d.ts.map +1 -1
- package/dist/lib/route-map.js +29 -17
- package/package.json +5 -5
- package/src/lib/bootstrap-project.ts +39 -13
- package/src/lib/cli.ts +39 -4
- package/src/lib/commands/completion.ts +6 -1
- package/src/lib/commands/doctor.ts +18 -21
- package/src/lib/commands/routes.ts +3 -3
- package/src/lib/completion.ts +1 -5
- package/src/lib/controller-files.ts +4 -8
- package/src/lib/controller-ownership.ts +78 -141
- package/src/lib/doctor/controller-findings.ts +20 -97
- package/src/lib/doctor/controller-fix-plans.ts +13 -29
- package/src/lib/doctor/controller-placeholders.ts +17 -189
- package/src/lib/doctor/controllers.ts +1 -1
- package/src/lib/doctor/project.ts +60 -52
- package/src/lib/doctor/types.ts +1 -5
- package/src/lib/help-text.ts +3 -2
- package/src/lib/load-route-map-worker.ts +19 -10
- package/src/lib/route-map.ts +61 -16
- package/{bootstrap → template}/.agents/skills/remix/SKILL.md +162 -77
- package/{bootstrap → template}/.agents/skills/remix/references/assets-and-browser-modules.md +22 -14
- package/{bootstrap → template}/.agents/skills/remix/references/auth-and-sessions.md +41 -18
- package/{bootstrap → template}/.agents/skills/remix/references/component-model.md +4 -1
- package/{bootstrap → template}/.agents/skills/remix/references/data-and-validation.md +21 -5
- package/{bootstrap → template}/.agents/skills/remix/references/middleware-and-server.md +42 -53
- package/{bootstrap → template}/.agents/skills/remix/references/routing-and-controllers.md +111 -44
- package/{bootstrap → template}/.agents/skills/remix/references/testing-patterns.md +17 -1
- package/{bootstrap → template}/AGENTS.md +10 -9
- package/template/README.md +29 -0
- package/template/app/actions/controller.tsx +18 -0
- package/template/app/assets/entry.ts +8 -0
- package/{bootstrap/app/ui → template/app/assets}/prompt-button.tsx +2 -1
- package/{bootstrap → template}/app/assets.ts +5 -3
- package/template/app/middleware/render.tsx +44 -0
- package/template/app/router.ts +20 -0
- package/{bootstrap → template}/app/routes.ts +1 -2
- package/{bootstrap → template}/app/ui/document.tsx +11 -4
- package/{bootstrap → template}/app/ui/scaffold-home-page.tsx +39 -38
- package/template/gitignore +4 -0
- package/{bootstrap → template}/package.json +5 -6
- package/template/public/favicon.svg +11 -0
- package/template/server.ts +38 -0
- package/{bootstrap → template}/tsconfig.json +3 -2
- package/bootstrap/README.md +0 -27
- package/bootstrap/app/assets/entry.ts +0 -19
- package/bootstrap/app/controllers/auth.tsx +0 -21
- package/bootstrap/app/controllers/home.tsx +0 -11
- package/bootstrap/app/router.ts +0 -16
- package/bootstrap/app/ui/layout.tsx +0 -22
- package/bootstrap/app/utils/render.tsx +0 -26
- package/bootstrap/server.ts +0 -37
- /package/{bootstrap → template}/.agents/skills/remix/references/animate-elements.md +0 -0
- /package/{bootstrap → template}/.agents/skills/remix/references/create-mixins.md +0 -0
- /package/{bootstrap → template}/.agents/skills/remix/references/hydration-frames-navigation.md +0 -0
- /package/{bootstrap → template}/.agents/skills/remix/references/mixins-styling-events.md +0 -0
|
@@ -1,58 +1,69 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { getControllerOwnerCandidates, getOwnerModuleBaseName, getPreferredOwnerDisplayPath, getRouteSubtreePath, isControllerEntryFileName, toDiskSegment, } from "./controller-files.js";
|
|
4
|
+
const ACTIONS_PATH_PREFIX = 'app/actions/';
|
|
5
|
+
export const ROOT_ROUTE_NAME = '<root>';
|
|
4
6
|
export async function inspectControllerOwnership(appRoot, tree) {
|
|
5
7
|
let subtreePlans = buildOwnedSubtrees(tree);
|
|
8
|
+
let routeDirectories = buildRouteDirectories(tree);
|
|
6
9
|
let scan = await scanControllersDirectory(appRoot);
|
|
7
10
|
let subtrees = applyScanToSubtrees(subtreePlans, scan);
|
|
8
11
|
return {
|
|
9
|
-
orphanActionPaths: getOrphanActionPaths(subtreePlans, scan),
|
|
10
12
|
orphanControllerPaths: getOrphanControllerPaths(subtreePlans, scan),
|
|
11
|
-
orphanRouteDirectoryPaths: getOrphanRouteDirectoryPaths(
|
|
13
|
+
orphanRouteDirectoryPaths: getOrphanRouteDirectoryPaths(routeDirectories, scan),
|
|
14
|
+
routeDirectories,
|
|
12
15
|
scan,
|
|
13
16
|
subtrees,
|
|
14
17
|
};
|
|
15
18
|
}
|
|
16
19
|
export function buildOwnedSubtrees(tree, parentSegments = [], subtrees = []) {
|
|
20
|
+
if (parentSegments.length === 0 &&
|
|
21
|
+
subtrees.length === 0 &&
|
|
22
|
+
tree.some((node) => node.kind === 'route')) {
|
|
23
|
+
addSubtreePlan(ROOT_ROUTE_NAME, [], subtrees);
|
|
24
|
+
}
|
|
17
25
|
for (let node of tree) {
|
|
18
|
-
|
|
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);
|
|
26
|
+
if (node.kind !== 'group') {
|
|
32
27
|
continue;
|
|
33
28
|
}
|
|
34
|
-
|
|
29
|
+
let segments = [...parentSegments, toDiskSegment(node.key)];
|
|
30
|
+
if (hasDirectRouteLeaf(node.children)) {
|
|
31
|
+
addSubtreePlan(node.name, segments, subtrees);
|
|
32
|
+
}
|
|
33
|
+
buildOwnedSubtrees(node.children, segments, subtrees);
|
|
34
|
+
}
|
|
35
|
+
return subtrees;
|
|
36
|
+
}
|
|
37
|
+
export function buildRouteDirectories(tree, parentSegments = [], directories = []) {
|
|
38
|
+
for (let node of tree) {
|
|
39
|
+
if (node.kind !== 'group') {
|
|
35
40
|
continue;
|
|
36
41
|
}
|
|
37
|
-
let
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
alternateCandidates,
|
|
41
|
-
alternateDisplayPath: getPreferredOwnerDisplayPath(alternateCandidates),
|
|
42
|
-
entryCandidates,
|
|
43
|
-
entryDisplayPath: getPreferredOwnerDisplayPath(entryCandidates),
|
|
44
|
-
kind: 'action',
|
|
42
|
+
let segments = [...parentSegments, toDiskSegment(node.key)];
|
|
43
|
+
directories.push({
|
|
44
|
+
directoryPath: getRouteSubtreePath(segments),
|
|
45
45
|
routeName: node.name,
|
|
46
|
-
subtreePath: getRouteSubtreePath(segments),
|
|
47
46
|
});
|
|
47
|
+
buildRouteDirectories(node.children, segments, directories);
|
|
48
48
|
}
|
|
49
|
-
return
|
|
49
|
+
return directories;
|
|
50
|
+
}
|
|
51
|
+
function hasDirectRouteLeaf(tree) {
|
|
52
|
+
return tree.some((node) => node.kind === 'route');
|
|
53
|
+
}
|
|
54
|
+
function addSubtreePlan(routeName, segments, subtrees) {
|
|
55
|
+
let entryCandidates = getControllerOwnerCandidates(segments);
|
|
56
|
+
subtrees.push({
|
|
57
|
+
entryCandidates,
|
|
58
|
+
entryDisplayPath: getPreferredOwnerDisplayPath(entryCandidates),
|
|
59
|
+
routeName,
|
|
60
|
+
subtreePath: getRouteSubtreePath(segments),
|
|
61
|
+
});
|
|
50
62
|
}
|
|
51
63
|
async function scanControllersDirectory(appRoot) {
|
|
52
|
-
let
|
|
64
|
+
let actionsDir = path.join(appRoot, 'app', 'actions');
|
|
53
65
|
let controllerEntryPaths = new Set();
|
|
54
|
-
let
|
|
55
|
-
let rootDirectoryPaths = new Set();
|
|
66
|
+
let routeDirectoryPaths = new Set();
|
|
56
67
|
let routeLocalFilePaths = new Set();
|
|
57
68
|
async function walk(currentDir, isRoot) {
|
|
58
69
|
let entries;
|
|
@@ -70,9 +81,7 @@ async function scanControllersDirectory(appRoot) {
|
|
|
70
81
|
let entryPath = path.join(currentDir, entry.name);
|
|
71
82
|
let relativePath = normalizeRelativePath(path.relative(appRoot, entryPath));
|
|
72
83
|
if (entry.isDirectory()) {
|
|
73
|
-
|
|
74
|
-
rootDirectoryPaths.add(relativePath);
|
|
75
|
-
}
|
|
84
|
+
routeDirectoryPaths.add(relativePath);
|
|
76
85
|
await walk(entryPath, false);
|
|
77
86
|
continue;
|
|
78
87
|
}
|
|
@@ -83,20 +92,15 @@ async function scanControllersDirectory(appRoot) {
|
|
|
83
92
|
controllerEntryPaths.add(relativePath);
|
|
84
93
|
continue;
|
|
85
94
|
}
|
|
86
|
-
if (
|
|
87
|
-
actionEntryPaths.add(relativePath);
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
if (!isRoot && isRouteLocalFileName(entry.name)) {
|
|
95
|
+
if (isRouteLocalFileName(entry.name)) {
|
|
91
96
|
routeLocalFilePaths.add(relativePath);
|
|
92
97
|
}
|
|
93
98
|
}
|
|
94
99
|
}
|
|
95
|
-
await walk(
|
|
100
|
+
await walk(actionsDir, true);
|
|
96
101
|
return {
|
|
97
|
-
actionEntryPaths,
|
|
98
102
|
controllerEntryPaths,
|
|
99
|
-
|
|
103
|
+
routeDirectoryPaths,
|
|
100
104
|
routeLocalFilePaths,
|
|
101
105
|
};
|
|
102
106
|
}
|
|
@@ -104,12 +108,9 @@ function applyScanToSubtrees(subtreePlans, scan) {
|
|
|
104
108
|
let claimedRouteLocalPaths = claimFilesToDeepestSubtree([...scan.routeLocalFilePaths], subtreePlans);
|
|
105
109
|
let claimedContentPaths = claimFilesToDeepestSubtree(getNestedContentPaths(scan), subtreePlans);
|
|
106
110
|
return subtreePlans.map((subtree) => {
|
|
107
|
-
let
|
|
108
|
-
let actualEntryPaths = findOwnerPaths(scan, subtree.kind, subtree.entryCandidates);
|
|
111
|
+
let actualEntryPaths = findOwnerPaths(scan, subtree.entryCandidates);
|
|
109
112
|
return {
|
|
110
113
|
...subtree,
|
|
111
|
-
actualAlternatePath: actualAlternatePaths[0] ?? null,
|
|
112
|
-
actualAlternatePaths,
|
|
113
114
|
actualEntryPath: actualEntryPaths[0] ?? null,
|
|
114
115
|
actualEntryPaths,
|
|
115
116
|
claimedFilePaths: claimedContentPaths.get(subtree.routeName) ?? [],
|
|
@@ -143,49 +144,20 @@ function claimFilesToDeepestSubtree(filePaths, subtreePlans) {
|
|
|
143
144
|
}
|
|
144
145
|
return claims;
|
|
145
146
|
}
|
|
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
147
|
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));
|
|
148
|
+
let expectedControllerPaths = new Set(subtreePlans.flatMap((subtree) => subtree.entryCandidates));
|
|
165
149
|
return [...scan.controllerEntryPaths]
|
|
166
150
|
.filter((filePath) => !expectedControllerPaths.has(filePath))
|
|
167
|
-
.filter((filePath) => !alternateControllerPaths.has(filePath))
|
|
168
151
|
.sort();
|
|
169
152
|
}
|
|
170
|
-
function getOrphanRouteDirectoryPaths(
|
|
171
|
-
let
|
|
153
|
+
function getOrphanRouteDirectoryPaths(routeDirectories, scan) {
|
|
154
|
+
let expectedRouteDirectories = new Set(routeDirectories.map((routeDirectory) => routeDirectory.directoryPath));
|
|
172
155
|
let actualControllerDirectories = [...scan.controllerEntryPaths].map((controllerPath) => normalizeRelativePath(path.dirname(controllerPath)));
|
|
173
|
-
return [...scan.
|
|
174
|
-
.filter((directoryPath) => !
|
|
156
|
+
return [...scan.routeDirectoryPaths]
|
|
157
|
+
.filter((directoryPath) => !expectedRouteDirectories.has(directoryPath))
|
|
175
158
|
.filter((directoryPath) => !actualControllerDirectories.some((controllerPath) => isDirectoryWithinDirectory(controllerPath, directoryPath)))
|
|
176
159
|
.sort();
|
|
177
160
|
}
|
|
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
161
|
function isRouteLocalFileName(fileName) {
|
|
190
162
|
let baseName = getOwnerModuleBaseName(fileName);
|
|
191
163
|
return (baseName != null &&
|
|
@@ -193,21 +165,18 @@ function isRouteLocalFileName(fileName) {
|
|
|
193
165
|
!baseName.endsWith('.test') &&
|
|
194
166
|
!baseName.endsWith('.spec'));
|
|
195
167
|
}
|
|
196
|
-
function
|
|
197
|
-
return findOwnerPaths(scan, kind, candidatePaths)[0] ?? null;
|
|
198
|
-
}
|
|
199
|
-
function findOwnerPaths(scan, kind, candidatePaths) {
|
|
168
|
+
function findOwnerPaths(scan, candidatePaths) {
|
|
200
169
|
let existingPaths = [];
|
|
201
170
|
for (let candidatePath of candidatePaths) {
|
|
202
|
-
if (
|
|
171
|
+
if (scan.controllerEntryPaths.has(candidatePath)) {
|
|
203
172
|
existingPaths.push(candidatePath);
|
|
204
173
|
}
|
|
205
174
|
}
|
|
206
175
|
return existingPaths;
|
|
207
176
|
}
|
|
208
177
|
function isNestedControllerPath(filePath) {
|
|
209
|
-
return (filePath.startsWith(
|
|
210
|
-
filePath.slice(
|
|
178
|
+
return (filePath.startsWith(ACTIONS_PATH_PREFIX) &&
|
|
179
|
+
filePath.slice(ACTIONS_PATH_PREFIX.length).includes('/'));
|
|
211
180
|
}
|
|
212
181
|
function isWithinDirectory(filePath, directoryPath) {
|
|
213
182
|
return filePath.startsWith(`${directoryPath}/`);
|
|
@@ -215,10 +184,6 @@ function isWithinDirectory(filePath, directoryPath) {
|
|
|
215
184
|
function isDirectoryWithinDirectory(directoryPath, parentDirectoryPath) {
|
|
216
185
|
return (directoryPath === parentDirectoryPath || isWithinDirectory(directoryPath, parentDirectoryPath));
|
|
217
186
|
}
|
|
218
|
-
function getTopLevelSubtreePath(subtreePath) {
|
|
219
|
-
let subtreeSegments = subtreePath.slice('app/controllers/'.length).split('/');
|
|
220
|
-
return getRouteSubtreePath([subtreeSegments[0]]);
|
|
221
|
-
}
|
|
222
187
|
function normalizeRelativePath(filePath) {
|
|
223
188
|
return filePath.split(path.sep).join('/');
|
|
224
189
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ControllerOwnership } from '../controller-ownership.ts';
|
|
2
2
|
import type { DoctorFinding, DoctorFixPlan } from './types.ts';
|
|
3
3
|
export declare function getControllerFindings(ownership: ControllerOwnership, fixPlans: DoctorFixPlan[]): DoctorFinding[];
|
|
4
4
|
//# sourceMappingURL=controller-findings.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"controller-findings.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/controller-findings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"controller-findings.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/controller-findings.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,4BAA4B,CAAA;AACnC,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,CAkBjB"}
|
|
@@ -1,26 +1,20 @@
|
|
|
1
|
+
import { ROOT_ROUTE_NAME, } from "../controller-ownership.js";
|
|
1
2
|
export function getControllerFindings(ownership, fixPlans) {
|
|
2
3
|
return [
|
|
3
4
|
...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
5
|
...ownership.orphanControllerPaths.map((actualPath) => ({
|
|
12
6
|
actualPath,
|
|
13
7
|
code: 'orphan-controller',
|
|
14
|
-
message: `
|
|
8
|
+
message: `Action controller ${actualPath} does not match any route map.`,
|
|
15
9
|
severity: 'warn',
|
|
16
|
-
suite: '
|
|
10
|
+
suite: 'actions',
|
|
17
11
|
})),
|
|
18
12
|
...ownership.orphanRouteDirectoryPaths.map((actualPath) => ({
|
|
19
13
|
actualPath,
|
|
20
14
|
code: 'orphan-route-directory',
|
|
21
|
-
message: `Directory ${actualPath} does not match any route
|
|
15
|
+
message: `Directory ${actualPath} does not match any route-map key path.`,
|
|
22
16
|
severity: 'warn',
|
|
23
|
-
suite: '
|
|
17
|
+
suite: 'actions',
|
|
24
18
|
})),
|
|
25
19
|
];
|
|
26
20
|
}
|
|
@@ -28,63 +22,20 @@ function getSubtreeFindings(subtrees, fixPlans) {
|
|
|
28
22
|
let findings = [];
|
|
29
23
|
let fixableFindingKeys = new Set(fixPlans.map((fixPlan) => `${fixPlan.code}:${fixPlan.routeName ?? ''}`));
|
|
30
24
|
for (let subtree of subtrees) {
|
|
31
|
-
let
|
|
32
|
-
let actualAlternatePath = subtree.actualAlternatePath ?? undefined;
|
|
25
|
+
let routeMapName = formatRouteMapName(subtree.routeName);
|
|
33
26
|
if (subtree.actualEntryPaths.length > 1) {
|
|
34
27
|
findings.push({
|
|
35
28
|
actualPath: subtree.actualEntryPaths[0],
|
|
36
29
|
code: 'duplicate-owner-file',
|
|
37
30
|
expectedPath: subtree.entryDisplayPath,
|
|
38
|
-
message:
|
|
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}`),
|
|
31
|
+
message: `${routeMapName} has multiple action controller files: ${subtree.actualEntryPaths.join(', ')}. Keep only one controller owner file.`,
|
|
81
32
|
routeName: subtree.routeName,
|
|
82
33
|
severity: 'warn',
|
|
83
|
-
suite: '
|
|
34
|
+
suite: 'actions',
|
|
84
35
|
});
|
|
85
36
|
continue;
|
|
86
37
|
}
|
|
87
|
-
if (subtree.
|
|
38
|
+
if (subtree.actualEntryPath == null) {
|
|
88
39
|
let code = subtree.claimedFilePaths.length > 0
|
|
89
40
|
? 'incomplete-controller'
|
|
90
41
|
: 'missing-owner';
|
|
@@ -94,39 +45,16 @@ function getSubtreeFindings(subtrees, fixPlans) {
|
|
|
94
45
|
expectedPath: subtree.entryDisplayPath,
|
|
95
46
|
fixable: fixableFindingKeys.has(`${code}:${subtree.routeName}`),
|
|
96
47
|
message: subtree.claimedFilePaths.length > 0
|
|
97
|
-
?
|
|
98
|
-
:
|
|
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}.`,
|
|
48
|
+
? `${routeMapName} has files under ${subtree.subtreePath}, but is missing action controller ${subtree.entryDisplayPath}.`
|
|
49
|
+
: `${routeMapName} is missing action controller ${subtree.entryDisplayPath}.`,
|
|
125
50
|
routeName: subtree.routeName,
|
|
126
51
|
severity: 'warn',
|
|
127
|
-
suite: '
|
|
52
|
+
suite: 'actions',
|
|
128
53
|
});
|
|
129
54
|
}
|
|
130
55
|
}
|
|
131
56
|
return findings;
|
|
132
57
|
}
|
|
58
|
+
function formatRouteMapName(routeName) {
|
|
59
|
+
return routeName === ROOT_ROUTE_NAME ? 'Root route map' : `Route map "${routeName}"`;
|
|
60
|
+
}
|
|
@@ -1 +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;
|
|
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;AAGnC,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,CAkD1B"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { getOwnerCandidateForExtension, getOwnerFileExtension, } from "../controller-files.js";
|
|
4
|
-
import {
|
|
4
|
+
import { ROOT_ROUTE_NAME } from "../controller-ownership.js";
|
|
5
|
+
import { renderControllerPlaceholder } from "./controller-placeholders.js";
|
|
5
6
|
const OWNER_EXTENSION_PRIORITY = ['.tsx', '.ts', '.jsx', '.js'];
|
|
6
7
|
export async function getControllerFixPlans(appRoot, tree, ownership) {
|
|
7
8
|
let routeNodesByName = getRouteNodesByName(tree);
|
|
@@ -31,51 +32,33 @@ export async function getControllerFixPlans(appRoot, tree, ownership) {
|
|
|
31
32
|
if (routeNode == null || entryPath == null) {
|
|
32
33
|
continue;
|
|
33
34
|
}
|
|
34
|
-
let contents =
|
|
35
|
-
? renderActionPlaceholder(routeNode, entryPath)
|
|
36
|
-
: renderControllerPlaceholder(routeNode, entryPath, resolvedEntryPathByRouteName);
|
|
35
|
+
let contents = renderControllerPlaceholder(routeNode, entryPath);
|
|
37
36
|
fixPlans.push({
|
|
38
37
|
code,
|
|
39
38
|
contents,
|
|
40
39
|
kind: 'create-file',
|
|
41
40
|
path: entryPath,
|
|
42
41
|
routeName: subtree.routeName,
|
|
43
|
-
suite: '
|
|
42
|
+
suite: 'actions',
|
|
44
43
|
});
|
|
45
44
|
}
|
|
46
45
|
return fixPlans;
|
|
47
46
|
}
|
|
48
47
|
function getFixCodeForSubtree(subtree) {
|
|
49
|
-
if (subtree.actualEntryPaths.length > 1
|
|
48
|
+
if (subtree.actualEntryPaths.length > 1) {
|
|
50
49
|
return null;
|
|
51
50
|
}
|
|
52
51
|
if (subtree.actualEntryPath != null) {
|
|
53
52
|
return null;
|
|
54
53
|
}
|
|
55
|
-
|
|
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';
|
|
54
|
+
return subtree.claimedFilePaths.length > 0 ? 'incomplete-controller' : 'missing-owner';
|
|
65
55
|
}
|
|
66
56
|
function inferOwnerExtension(subtree, ownership, hasTsconfig) {
|
|
67
57
|
let subtreeExtension = getMostCommonOwnerExtension(subtree.claimedRouteLocalFilePaths);
|
|
68
58
|
if (subtreeExtension != null) {
|
|
69
59
|
return subtreeExtension;
|
|
70
60
|
}
|
|
71
|
-
let
|
|
72
|
-
if (alternateExtension != null) {
|
|
73
|
-
return alternateExtension;
|
|
74
|
-
}
|
|
75
|
-
let projectExtension = getMostCommonOwnerExtension([
|
|
76
|
-
...ownership.scan.actionEntryPaths,
|
|
77
|
-
...ownership.scan.controllerEntryPaths,
|
|
78
|
-
]);
|
|
61
|
+
let projectExtension = getMostCommonOwnerExtension([...ownership.scan.controllerEntryPaths]);
|
|
79
62
|
if (projectExtension != null) {
|
|
80
63
|
return projectExtension;
|
|
81
64
|
}
|
|
@@ -106,6 +89,12 @@ function getMostCommonOwnerExtension(filePaths) {
|
|
|
106
89
|
}
|
|
107
90
|
function getRouteNodesByName(tree) {
|
|
108
91
|
let routeNodesByName = new Map();
|
|
92
|
+
routeNodesByName.set(ROOT_ROUTE_NAME, {
|
|
93
|
+
children: tree,
|
|
94
|
+
key: ROOT_ROUTE_NAME,
|
|
95
|
+
kind: 'group',
|
|
96
|
+
name: ROOT_ROUTE_NAME,
|
|
97
|
+
});
|
|
109
98
|
function visit(nodes) {
|
|
110
99
|
for (let node of nodes) {
|
|
111
100
|
routeNodesByName.set(node.name, node);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type
|
|
2
|
-
export declare function
|
|
3
|
-
export declare function renderControllerPlaceholder(routeNode: OwnershipRouteNode, entryPath: string, resolvedEntryPathByRouteName: Map<string, string>): string;
|
|
1
|
+
import { type OwnershipRouteNode } from '../controller-ownership.ts';
|
|
2
|
+
export declare function renderControllerPlaceholder(routeNode: OwnershipRouteNode, entryPath: string): string;
|
|
4
3
|
//# sourceMappingURL=controller-placeholders.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"controller-placeholders.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/controller-placeholders.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"controller-placeholders.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/controller-placeholders.ts"],"names":[],"mappings":"AAGA,OAAO,EAAmB,KAAK,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAErF,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,kBAAkB,EAC7B,SAAS,EAAE,MAAM,GAChB,MAAM,CAyBR"}
|