@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
|
@@ -2,16 +2,18 @@ import * as fs from 'node:fs/promises'
|
|
|
2
2
|
import * as path from 'node:path'
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
getActionOwnerCandidates,
|
|
6
5
|
getControllerOwnerCandidates,
|
|
7
6
|
getOwnerModuleBaseName,
|
|
8
7
|
getPreferredOwnerDisplayPath,
|
|
9
8
|
getRouteSubtreePath,
|
|
10
|
-
isActionFileName,
|
|
11
9
|
isControllerEntryFileName,
|
|
12
10
|
toDiskSegment,
|
|
13
11
|
} from './controller-files.ts'
|
|
14
|
-
import type {
|
|
12
|
+
import type { RouteTreeNodeKind } from './route-map.ts'
|
|
13
|
+
|
|
14
|
+
const ACTIONS_PATH_PREFIX = 'app/actions/'
|
|
15
|
+
|
|
16
|
+
export const ROOT_ROUTE_NAME = '<root>'
|
|
15
17
|
|
|
16
18
|
export interface OwnershipRouteNode {
|
|
17
19
|
children: OwnershipRouteNode[]
|
|
@@ -22,25 +24,24 @@ export interface OwnershipRouteNode {
|
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export interface ControllerDirectoryScan {
|
|
25
|
-
actionEntryPaths: Set<string>
|
|
26
27
|
controllerEntryPaths: Set<string>
|
|
27
|
-
|
|
28
|
+
routeDirectoryPaths: Set<string>
|
|
28
29
|
routeLocalFilePaths: Set<string>
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
export interface RouteDirectoryPlan {
|
|
33
|
+
directoryPath: string
|
|
34
|
+
routeName: string
|
|
35
|
+
}
|
|
36
|
+
|
|
31
37
|
export interface OwnedSubtreePlan {
|
|
32
|
-
alternateCandidates: string[]
|
|
33
|
-
alternateDisplayPath: string
|
|
34
38
|
entryCandidates: string[]
|
|
35
39
|
entryDisplayPath: string
|
|
36
|
-
kind: RouteOwnerKind
|
|
37
40
|
routeName: string
|
|
38
41
|
subtreePath: string
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
export interface OwnedSubtree extends OwnedSubtreePlan {
|
|
42
|
-
actualAlternatePath: string | null
|
|
43
|
-
actualAlternatePaths: string[]
|
|
44
45
|
actualEntryPath: string | null
|
|
45
46
|
actualEntryPaths: string[]
|
|
46
47
|
claimedFilePaths: string[]
|
|
@@ -48,9 +49,9 @@ export interface OwnedSubtree extends OwnedSubtreePlan {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
export interface ControllerOwnership {
|
|
51
|
-
orphanActionPaths: string[]
|
|
52
52
|
orphanControllerPaths: string[]
|
|
53
53
|
orphanRouteDirectoryPaths: string[]
|
|
54
|
+
routeDirectories: RouteDirectoryPlan[]
|
|
54
55
|
scan: ControllerDirectoryScan
|
|
55
56
|
subtrees: OwnedSubtree[]
|
|
56
57
|
}
|
|
@@ -60,13 +61,14 @@ export async function inspectControllerOwnership(
|
|
|
60
61
|
tree: OwnershipRouteNode[],
|
|
61
62
|
): Promise<ControllerOwnership> {
|
|
62
63
|
let subtreePlans = buildOwnedSubtrees(tree)
|
|
64
|
+
let routeDirectories = buildRouteDirectories(tree)
|
|
63
65
|
let scan = await scanControllersDirectory(appRoot)
|
|
64
66
|
let subtrees = applyScanToSubtrees(subtreePlans, scan)
|
|
65
67
|
|
|
66
68
|
return {
|
|
67
|
-
orphanActionPaths: getOrphanActionPaths(subtreePlans, scan),
|
|
68
69
|
orphanControllerPaths: getOrphanControllerPaths(subtreePlans, scan),
|
|
69
|
-
orphanRouteDirectoryPaths: getOrphanRouteDirectoryPaths(
|
|
70
|
+
orphanRouteDirectoryPaths: getOrphanRouteDirectoryPaths(routeDirectories, scan),
|
|
71
|
+
routeDirectories,
|
|
70
72
|
scan,
|
|
71
73
|
subtrees,
|
|
72
74
|
}
|
|
@@ -77,52 +79,69 @@ export function buildOwnedSubtrees(
|
|
|
77
79
|
parentSegments: string[] = [],
|
|
78
80
|
subtrees: OwnedSubtreePlan[] = [],
|
|
79
81
|
): OwnedSubtreePlan[] {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
if (
|
|
83
|
+
parentSegments.length === 0 &&
|
|
84
|
+
subtrees.length === 0 &&
|
|
85
|
+
tree.some((node) => node.kind === 'route')
|
|
86
|
+
) {
|
|
87
|
+
addSubtreePlan(ROOT_ROUTE_NAME, [], subtrees)
|
|
88
|
+
}
|
|
82
89
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
let entryCandidates = getControllerOwnerCandidates(segments)
|
|
86
|
-
|
|
87
|
-
subtrees.push({
|
|
88
|
-
alternateCandidates,
|
|
89
|
-
alternateDisplayPath: getPreferredOwnerDisplayPath(alternateCandidates),
|
|
90
|
-
entryCandidates,
|
|
91
|
-
entryDisplayPath: getPreferredOwnerDisplayPath(entryCandidates),
|
|
92
|
-
kind: 'controller',
|
|
93
|
-
routeName: node.name,
|
|
94
|
-
subtreePath: getRouteSubtreePath(segments),
|
|
95
|
-
})
|
|
96
|
-
buildOwnedSubtrees(node.children, segments, subtrees)
|
|
90
|
+
for (let node of tree) {
|
|
91
|
+
if (node.kind !== 'group') {
|
|
97
92
|
continue
|
|
98
93
|
}
|
|
99
94
|
|
|
100
|
-
|
|
101
|
-
|
|
95
|
+
let segments = [...parentSegments, toDiskSegment(node.key)]
|
|
96
|
+
if (hasDirectRouteLeaf(node.children)) {
|
|
97
|
+
addSubtreePlan(node.name, segments, subtrees)
|
|
102
98
|
}
|
|
99
|
+
buildOwnedSubtrees(node.children, segments, subtrees)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return subtrees
|
|
103
|
+
}
|
|
103
104
|
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
export function buildRouteDirectories(
|
|
106
|
+
tree: OwnershipRouteNode[],
|
|
107
|
+
parentSegments: string[] = [],
|
|
108
|
+
directories: RouteDirectoryPlan[] = [],
|
|
109
|
+
): RouteDirectoryPlan[] {
|
|
110
|
+
for (let node of tree) {
|
|
111
|
+
if (node.kind !== 'group') {
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
106
114
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
entryCandidates,
|
|
111
|
-
entryDisplayPath: getPreferredOwnerDisplayPath(entryCandidates),
|
|
112
|
-
kind: 'action',
|
|
115
|
+
let segments = [...parentSegments, toDiskSegment(node.key)]
|
|
116
|
+
directories.push({
|
|
117
|
+
directoryPath: getRouteSubtreePath(segments),
|
|
113
118
|
routeName: node.name,
|
|
114
|
-
subtreePath: getRouteSubtreePath(segments),
|
|
115
119
|
})
|
|
120
|
+
buildRouteDirectories(node.children, segments, directories)
|
|
116
121
|
}
|
|
117
122
|
|
|
118
|
-
return
|
|
123
|
+
return directories
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function hasDirectRouteLeaf(tree: OwnershipRouteNode[]): boolean {
|
|
127
|
+
return tree.some((node) => node.kind === 'route')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function addSubtreePlan(routeName: string, segments: string[], subtrees: OwnedSubtreePlan[]): void {
|
|
131
|
+
let entryCandidates = getControllerOwnerCandidates(segments)
|
|
132
|
+
|
|
133
|
+
subtrees.push({
|
|
134
|
+
entryCandidates,
|
|
135
|
+
entryDisplayPath: getPreferredOwnerDisplayPath(entryCandidates),
|
|
136
|
+
routeName,
|
|
137
|
+
subtreePath: getRouteSubtreePath(segments),
|
|
138
|
+
})
|
|
119
139
|
}
|
|
120
140
|
|
|
121
141
|
async function scanControllersDirectory(appRoot: string): Promise<ControllerDirectoryScan> {
|
|
122
|
-
let
|
|
142
|
+
let actionsDir = path.join(appRoot, 'app', 'actions')
|
|
123
143
|
let controllerEntryPaths = new Set<string>()
|
|
124
|
-
let
|
|
125
|
-
let rootDirectoryPaths = new Set<string>()
|
|
144
|
+
let routeDirectoryPaths = new Set<string>()
|
|
126
145
|
let routeLocalFilePaths = new Set<string>()
|
|
127
146
|
|
|
128
147
|
async function walk(currentDir: string, isRoot: boolean): Promise<void> {
|
|
@@ -143,10 +162,7 @@ async function scanControllersDirectory(appRoot: string): Promise<ControllerDire
|
|
|
143
162
|
let relativePath = normalizeRelativePath(path.relative(appRoot, entryPath))
|
|
144
163
|
|
|
145
164
|
if (entry.isDirectory()) {
|
|
146
|
-
|
|
147
|
-
rootDirectoryPaths.add(relativePath)
|
|
148
|
-
}
|
|
149
|
-
|
|
165
|
+
routeDirectoryPaths.add(relativePath)
|
|
150
166
|
await walk(entryPath, false)
|
|
151
167
|
continue
|
|
152
168
|
}
|
|
@@ -160,23 +176,17 @@ async function scanControllersDirectory(appRoot: string): Promise<ControllerDire
|
|
|
160
176
|
continue
|
|
161
177
|
}
|
|
162
178
|
|
|
163
|
-
if (
|
|
164
|
-
actionEntryPaths.add(relativePath)
|
|
165
|
-
continue
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (!isRoot && isRouteLocalFileName(entry.name)) {
|
|
179
|
+
if (isRouteLocalFileName(entry.name)) {
|
|
169
180
|
routeLocalFilePaths.add(relativePath)
|
|
170
181
|
}
|
|
171
182
|
}
|
|
172
183
|
}
|
|
173
184
|
|
|
174
|
-
await walk(
|
|
185
|
+
await walk(actionsDir, true)
|
|
175
186
|
|
|
176
187
|
return {
|
|
177
|
-
actionEntryPaths,
|
|
178
188
|
controllerEntryPaths,
|
|
179
|
-
|
|
189
|
+
routeDirectoryPaths,
|
|
180
190
|
routeLocalFilePaths,
|
|
181
191
|
}
|
|
182
192
|
}
|
|
@@ -192,17 +202,10 @@ function applyScanToSubtrees(
|
|
|
192
202
|
let claimedContentPaths = claimFilesToDeepestSubtree(getNestedContentPaths(scan), subtreePlans)
|
|
193
203
|
|
|
194
204
|
return subtreePlans.map((subtree) => {
|
|
195
|
-
let
|
|
196
|
-
scan,
|
|
197
|
-
invertOwnerKind(subtree.kind),
|
|
198
|
-
subtree.alternateCandidates,
|
|
199
|
-
)
|
|
200
|
-
let actualEntryPaths = findOwnerPaths(scan, subtree.kind, subtree.entryCandidates)
|
|
205
|
+
let actualEntryPaths = findOwnerPaths(scan, subtree.entryCandidates)
|
|
201
206
|
|
|
202
207
|
return {
|
|
203
208
|
...subtree,
|
|
204
|
-
actualAlternatePath: actualAlternatePaths[0] ?? null,
|
|
205
|
-
actualAlternatePaths,
|
|
206
209
|
actualEntryPath: actualEntryPaths[0] ?? null,
|
|
207
210
|
actualEntryPaths,
|
|
208
211
|
claimedFilePaths: claimedContentPaths.get(subtree.routeName) ?? [],
|
|
@@ -253,61 +256,30 @@ function claimFilesToDeepestSubtree(
|
|
|
253
256
|
return claims
|
|
254
257
|
}
|
|
255
258
|
|
|
256
|
-
function getOrphanActionPaths(
|
|
257
|
-
subtreePlans: OwnedSubtreePlan[],
|
|
258
|
-
scan: ControllerDirectoryScan,
|
|
259
|
-
): string[] {
|
|
260
|
-
let expectedActionPaths = new Set(
|
|
261
|
-
subtreePlans
|
|
262
|
-
.filter((subtree) => subtree.kind === 'action')
|
|
263
|
-
.flatMap((subtree) => subtree.entryCandidates),
|
|
264
|
-
)
|
|
265
|
-
let alternateActionPaths = new Set(
|
|
266
|
-
subtreePlans
|
|
267
|
-
.filter((subtree) => subtree.kind === 'controller')
|
|
268
|
-
.flatMap((subtree) => subtree.alternateCandidates),
|
|
269
|
-
)
|
|
270
|
-
|
|
271
|
-
return [...scan.actionEntryPaths]
|
|
272
|
-
.filter((filePath) => !expectedActionPaths.has(filePath))
|
|
273
|
-
.filter((filePath) => !alternateActionPaths.has(filePath))
|
|
274
|
-
.sort()
|
|
275
|
-
}
|
|
276
|
-
|
|
277
259
|
function getOrphanControllerPaths(
|
|
278
260
|
subtreePlans: OwnedSubtreePlan[],
|
|
279
261
|
scan: ControllerDirectoryScan,
|
|
280
262
|
): string[] {
|
|
281
|
-
let expectedControllerPaths = new Set(
|
|
282
|
-
subtreePlans
|
|
283
|
-
.filter((subtree) => subtree.kind === 'controller')
|
|
284
|
-
.flatMap((subtree) => subtree.entryCandidates),
|
|
285
|
-
)
|
|
286
|
-
let alternateControllerPaths = new Set(
|
|
287
|
-
subtreePlans
|
|
288
|
-
.filter((subtree) => subtree.kind === 'action')
|
|
289
|
-
.flatMap((subtree) => subtree.alternateCandidates),
|
|
290
|
-
)
|
|
263
|
+
let expectedControllerPaths = new Set(subtreePlans.flatMap((subtree) => subtree.entryCandidates))
|
|
291
264
|
|
|
292
265
|
return [...scan.controllerEntryPaths]
|
|
293
266
|
.filter((filePath) => !expectedControllerPaths.has(filePath))
|
|
294
|
-
.filter((filePath) => !alternateControllerPaths.has(filePath))
|
|
295
267
|
.sort()
|
|
296
268
|
}
|
|
297
269
|
|
|
298
270
|
function getOrphanRouteDirectoryPaths(
|
|
299
|
-
|
|
271
|
+
routeDirectories: RouteDirectoryPlan[],
|
|
300
272
|
scan: ControllerDirectoryScan,
|
|
301
273
|
): string[] {
|
|
302
|
-
let
|
|
303
|
-
|
|
274
|
+
let expectedRouteDirectories = new Set(
|
|
275
|
+
routeDirectories.map((routeDirectory) => routeDirectory.directoryPath),
|
|
304
276
|
)
|
|
305
277
|
let actualControllerDirectories = [...scan.controllerEntryPaths].map((controllerPath) =>
|
|
306
278
|
normalizeRelativePath(path.dirname(controllerPath)),
|
|
307
279
|
)
|
|
308
280
|
|
|
309
|
-
return [...scan.
|
|
310
|
-
.filter((directoryPath) => !
|
|
281
|
+
return [...scan.routeDirectoryPaths]
|
|
282
|
+
.filter((directoryPath) => !expectedRouteDirectories.has(directoryPath))
|
|
311
283
|
.filter(
|
|
312
284
|
(directoryPath) =>
|
|
313
285
|
!actualControllerDirectories.some((controllerPath) =>
|
|
@@ -317,24 +289,6 @@ function getOrphanRouteDirectoryPaths(
|
|
|
317
289
|
.sort()
|
|
318
290
|
}
|
|
319
291
|
|
|
320
|
-
function hasOwnerPath(
|
|
321
|
-
scan: ControllerDirectoryScan,
|
|
322
|
-
kind: RouteOwnerKind,
|
|
323
|
-
filePath: string,
|
|
324
|
-
): boolean {
|
|
325
|
-
return kind === 'action'
|
|
326
|
-
? scan.actionEntryPaths.has(filePath)
|
|
327
|
-
: scan.controllerEntryPaths.has(filePath)
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function invertOwnerKind(kind: RouteOwnerKind): RouteOwnerKind {
|
|
331
|
-
return kind === 'action' ? 'controller' : 'action'
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function isActionCandidate(fileName: string): boolean {
|
|
335
|
-
return isActionFileName(fileName)
|
|
336
|
-
}
|
|
337
|
-
|
|
338
292
|
function isRouteLocalFileName(fileName: string): boolean {
|
|
339
293
|
let baseName = getOwnerModuleBaseName(fileName)
|
|
340
294
|
|
|
@@ -346,23 +300,11 @@ function isRouteLocalFileName(fileName: string): boolean {
|
|
|
346
300
|
)
|
|
347
301
|
}
|
|
348
302
|
|
|
349
|
-
function
|
|
350
|
-
scan: ControllerDirectoryScan,
|
|
351
|
-
kind: RouteOwnerKind,
|
|
352
|
-
candidatePaths: string[],
|
|
353
|
-
): string | null {
|
|
354
|
-
return findOwnerPaths(scan, kind, candidatePaths)[0] ?? null
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function findOwnerPaths(
|
|
358
|
-
scan: ControllerDirectoryScan,
|
|
359
|
-
kind: RouteOwnerKind,
|
|
360
|
-
candidatePaths: string[],
|
|
361
|
-
): string[] {
|
|
303
|
+
function findOwnerPaths(scan: ControllerDirectoryScan, candidatePaths: string[]): string[] {
|
|
362
304
|
let existingPaths: string[] = []
|
|
363
305
|
|
|
364
306
|
for (let candidatePath of candidatePaths) {
|
|
365
|
-
if (
|
|
307
|
+
if (scan.controllerEntryPaths.has(candidatePath)) {
|
|
366
308
|
existingPaths.push(candidatePath)
|
|
367
309
|
}
|
|
368
310
|
}
|
|
@@ -372,8 +314,8 @@ function findOwnerPaths(
|
|
|
372
314
|
|
|
373
315
|
function isNestedControllerPath(filePath: string): boolean {
|
|
374
316
|
return (
|
|
375
|
-
filePath.startsWith(
|
|
376
|
-
filePath.slice(
|
|
317
|
+
filePath.startsWith(ACTIONS_PATH_PREFIX) &&
|
|
318
|
+
filePath.slice(ACTIONS_PATH_PREFIX.length).includes('/')
|
|
377
319
|
)
|
|
378
320
|
}
|
|
379
321
|
|
|
@@ -387,11 +329,6 @@ function isDirectoryWithinDirectory(directoryPath: string, parentDirectoryPath:
|
|
|
387
329
|
)
|
|
388
330
|
}
|
|
389
331
|
|
|
390
|
-
function getTopLevelSubtreePath(subtreePath: string): string {
|
|
391
|
-
let subtreeSegments = subtreePath.slice('app/controllers/'.length).split('/')
|
|
392
|
-
return getRouteSubtreePath([subtreeSegments[0]])
|
|
393
|
-
}
|
|
394
|
-
|
|
395
332
|
function normalizeRelativePath(filePath: string): string {
|
|
396
333
|
return filePath.split(path.sep).join('/')
|
|
397
334
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
ROOT_ROUTE_NAME,
|
|
3
|
+
type ControllerOwnership,
|
|
4
|
+
type OwnedSubtree,
|
|
5
|
+
} from '../controller-ownership.ts'
|
|
2
6
|
import type { DoctorFinding, DoctorFixPlan } from './types.ts'
|
|
3
7
|
|
|
4
8
|
export function getControllerFindings(
|
|
@@ -7,26 +11,19 @@ export function getControllerFindings(
|
|
|
7
11
|
): DoctorFinding[] {
|
|
8
12
|
return [
|
|
9
13
|
...getSubtreeFindings(ownership.subtrees, fixPlans),
|
|
10
|
-
...ownership.orphanActionPaths.map((actualPath) => ({
|
|
11
|
-
actualPath,
|
|
12
|
-
code: 'orphan-action' as const,
|
|
13
|
-
message: `Standalone action ${actualPath} does not match any top-level route.`,
|
|
14
|
-
severity: 'warn' as const,
|
|
15
|
-
suite: 'controllers' as const,
|
|
16
|
-
})),
|
|
17
14
|
...ownership.orphanControllerPaths.map((actualPath) => ({
|
|
18
15
|
actualPath,
|
|
19
16
|
code: 'orphan-controller' as const,
|
|
20
|
-
message: `
|
|
17
|
+
message: `Action controller ${actualPath} does not match any route map.`,
|
|
21
18
|
severity: 'warn' as const,
|
|
22
|
-
suite: '
|
|
19
|
+
suite: 'actions' as const,
|
|
23
20
|
})),
|
|
24
21
|
...ownership.orphanRouteDirectoryPaths.map((actualPath) => ({
|
|
25
22
|
actualPath,
|
|
26
23
|
code: 'orphan-route-directory' as const,
|
|
27
|
-
message: `Directory ${actualPath} does not match any route
|
|
24
|
+
message: `Directory ${actualPath} does not match any route-map key path.`,
|
|
28
25
|
severity: 'warn' as const,
|
|
29
|
-
suite: '
|
|
26
|
+
suite: 'actions' as const,
|
|
30
27
|
})),
|
|
31
28
|
]
|
|
32
29
|
}
|
|
@@ -38,71 +35,22 @@ function getSubtreeFindings(subtrees: OwnedSubtree[], fixPlans: DoctorFixPlan[])
|
|
|
38
35
|
)
|
|
39
36
|
|
|
40
37
|
for (let subtree of subtrees) {
|
|
41
|
-
let
|
|
42
|
-
let actualAlternatePath = subtree.actualAlternatePath ?? undefined
|
|
38
|
+
let routeMapName = formatRouteMapName(subtree.routeName)
|
|
43
39
|
|
|
44
40
|
if (subtree.actualEntryPaths.length > 1) {
|
|
45
41
|
findings.push({
|
|
46
42
|
actualPath: subtree.actualEntryPaths[0],
|
|
47
43
|
code: 'duplicate-owner-file',
|
|
48
44
|
expectedPath: subtree.entryDisplayPath,
|
|
49
|
-
message:
|
|
50
|
-
routeName: subtree.routeName,
|
|
51
|
-
severity: 'warn',
|
|
52
|
-
suite: 'controllers',
|
|
53
|
-
})
|
|
54
|
-
continue
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (subtree.actualAlternatePaths.length > 1) {
|
|
58
|
-
let alternateKind = subtree.kind === 'action' ? 'controller' : 'action'
|
|
59
|
-
|
|
60
|
-
findings.push({
|
|
61
|
-
actualPath: subtree.actualAlternatePaths[0],
|
|
62
|
-
code: 'duplicate-owner-file',
|
|
63
|
-
expectedPath: subtree.entryDisplayPath,
|
|
64
|
-
message: `Route "${subtree.routeName}" has multiple ${alternateKind} files: ${subtree.actualAlternatePaths.join(', ')}. Keep only one ${alternateKind} owner file.`,
|
|
65
|
-
routeName: subtree.routeName,
|
|
66
|
-
severity: 'warn',
|
|
67
|
-
suite: 'controllers',
|
|
68
|
-
})
|
|
69
|
-
continue
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (actualEntryPath != null && actualAlternatePath != null) {
|
|
73
|
-
findings.push({
|
|
74
|
-
actualPath: actualAlternatePath,
|
|
75
|
-
code: 'ambiguous-owner',
|
|
76
|
-
expectedPath: actualEntryPath,
|
|
77
|
-
message:
|
|
78
|
-
subtree.kind === 'action'
|
|
79
|
-
? `Route "${subtree.routeName}" has both action ${actualEntryPath} and controller ${actualAlternatePath}.`
|
|
80
|
-
: `Route "${subtree.routeName}" has both controller ${actualEntryPath} and standalone action ${actualAlternatePath}.`,
|
|
81
|
-
routeName: subtree.routeName,
|
|
82
|
-
severity: 'warn',
|
|
83
|
-
suite: 'controllers',
|
|
84
|
-
})
|
|
85
|
-
continue
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (actualAlternatePath != null) {
|
|
89
|
-
findings.push({
|
|
90
|
-
actualPath: actualAlternatePath,
|
|
91
|
-
code: 'wrong-owner-kind',
|
|
92
|
-
expectedPath: subtree.entryDisplayPath,
|
|
93
|
-
message:
|
|
94
|
-
subtree.kind === 'action'
|
|
95
|
-
? `Route "${subtree.routeName}" expects action ${subtree.entryDisplayPath}, but found controller ${actualAlternatePath}.`
|
|
96
|
-
: `Route "${subtree.routeName}" expects controller ${subtree.entryDisplayPath}, but found standalone action ${actualAlternatePath}.`,
|
|
97
|
-
fixable: fixableFindingKeys.has(`wrong-owner-kind:${subtree.routeName}`),
|
|
45
|
+
message: `${routeMapName} has multiple action controller files: ${subtree.actualEntryPaths.join(', ')}. Keep only one controller owner file.`,
|
|
98
46
|
routeName: subtree.routeName,
|
|
99
47
|
severity: 'warn',
|
|
100
|
-
suite: '
|
|
48
|
+
suite: 'actions',
|
|
101
49
|
})
|
|
102
50
|
continue
|
|
103
51
|
}
|
|
104
52
|
|
|
105
|
-
if (subtree.
|
|
53
|
+
if (subtree.actualEntryPath == null) {
|
|
106
54
|
let code =
|
|
107
55
|
subtree.claimedFilePaths.length > 0
|
|
108
56
|
? ('incomplete-controller' as const)
|
|
@@ -115,43 +63,18 @@ function getSubtreeFindings(subtrees: OwnedSubtree[], fixPlans: DoctorFixPlan[])
|
|
|
115
63
|
fixable: fixableFindingKeys.has(`${code}:${subtree.routeName}`),
|
|
116
64
|
message:
|
|
117
65
|
subtree.claimedFilePaths.length > 0
|
|
118
|
-
?
|
|
119
|
-
:
|
|
66
|
+
? `${routeMapName} has files under ${subtree.subtreePath}, but is missing action controller ${subtree.entryDisplayPath}.`
|
|
67
|
+
: `${routeMapName} is missing action controller ${subtree.entryDisplayPath}.`,
|
|
120
68
|
routeName: subtree.routeName,
|
|
121
69
|
severity: 'warn',
|
|
122
|
-
suite: '
|
|
123
|
-
})
|
|
124
|
-
continue
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (subtree.kind === 'action' && subtree.claimedFilePaths.length > 0) {
|
|
128
|
-
findings.push({
|
|
129
|
-
actualPath: subtree.subtreePath,
|
|
130
|
-
code: 'promotion-drift',
|
|
131
|
-
expectedPath: actualEntryPath ?? subtree.entryDisplayPath,
|
|
132
|
-
message:
|
|
133
|
-
actualEntryPath == null
|
|
134
|
-
? `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}.`
|
|
135
|
-
: `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}.`,
|
|
136
|
-
routeName: subtree.routeName,
|
|
137
|
-
severity: 'warn',
|
|
138
|
-
suite: 'controllers',
|
|
139
|
-
})
|
|
140
|
-
continue
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (subtree.actualEntryPath == null) {
|
|
144
|
-
findings.push({
|
|
145
|
-
code: 'missing-owner',
|
|
146
|
-
expectedPath: subtree.entryDisplayPath,
|
|
147
|
-
fixable: fixableFindingKeys.has(`missing-owner:${subtree.routeName}`),
|
|
148
|
-
message: `Route "${subtree.routeName}" is missing action ${subtree.entryDisplayPath}.`,
|
|
149
|
-
routeName: subtree.routeName,
|
|
150
|
-
severity: 'warn',
|
|
151
|
-
suite: 'controllers',
|
|
70
|
+
suite: 'actions',
|
|
152
71
|
})
|
|
153
72
|
}
|
|
154
73
|
}
|
|
155
74
|
|
|
156
75
|
return findings
|
|
157
76
|
}
|
|
77
|
+
|
|
78
|
+
function formatRouteMapName(routeName: string): string {
|
|
79
|
+
return routeName === ROOT_ROUTE_NAME ? 'Root route map' : `Route map "${routeName}"`
|
|
80
|
+
}
|
|
@@ -11,7 +11,8 @@ import type {
|
|
|
11
11
|
OwnedSubtree,
|
|
12
12
|
OwnershipRouteNode,
|
|
13
13
|
} from '../controller-ownership.ts'
|
|
14
|
-
import {
|
|
14
|
+
import { ROOT_ROUTE_NAME } from '../controller-ownership.ts'
|
|
15
|
+
import { renderControllerPlaceholder } from './controller-placeholders.ts'
|
|
15
16
|
import type { DoctorFixPlan } from './types.ts'
|
|
16
17
|
|
|
17
18
|
const OWNER_EXTENSION_PRIORITY: OwnerFileExtension[] = ['.tsx', '.ts', '.jsx', '.js']
|
|
@@ -57,10 +58,7 @@ export async function getControllerFixPlans(
|
|
|
57
58
|
continue
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
let contents =
|
|
61
|
-
subtree.kind === 'action'
|
|
62
|
-
? renderActionPlaceholder(routeNode, entryPath)
|
|
63
|
-
: renderControllerPlaceholder(routeNode, entryPath, resolvedEntryPathByRouteName)
|
|
61
|
+
let contents = renderControllerPlaceholder(routeNode, entryPath)
|
|
64
62
|
|
|
65
63
|
fixPlans.push({
|
|
66
64
|
code,
|
|
@@ -68,7 +66,7 @@ export async function getControllerFixPlans(
|
|
|
68
66
|
kind: 'create-file',
|
|
69
67
|
path: entryPath,
|
|
70
68
|
routeName: subtree.routeName,
|
|
71
|
-
suite: '
|
|
69
|
+
suite: 'actions',
|
|
72
70
|
})
|
|
73
71
|
}
|
|
74
72
|
|
|
@@ -76,7 +74,7 @@ export async function getControllerFixPlans(
|
|
|
76
74
|
}
|
|
77
75
|
|
|
78
76
|
function getFixCodeForSubtree(subtree: OwnedSubtree): DoctorFixPlan['code'] | null {
|
|
79
|
-
if (subtree.actualEntryPaths.length > 1
|
|
77
|
+
if (subtree.actualEntryPaths.length > 1) {
|
|
80
78
|
return null
|
|
81
79
|
}
|
|
82
80
|
|
|
@@ -84,19 +82,7 @@ function getFixCodeForSubtree(subtree: OwnedSubtree): DoctorFixPlan['code'] | nu
|
|
|
84
82
|
return null
|
|
85
83
|
}
|
|
86
84
|
|
|
87
|
-
|
|
88
|
-
return 'wrong-owner-kind'
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (subtree.kind === 'controller') {
|
|
92
|
-
return subtree.claimedFilePaths.length > 0 ? 'incomplete-controller' : 'missing-owner'
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (subtree.claimedFilePaths.length > 0) {
|
|
96
|
-
return null
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return 'missing-owner'
|
|
85
|
+
return subtree.claimedFilePaths.length > 0 ? 'incomplete-controller' : 'missing-owner'
|
|
100
86
|
}
|
|
101
87
|
|
|
102
88
|
function inferOwnerExtension(
|
|
@@ -109,15 +95,7 @@ function inferOwnerExtension(
|
|
|
109
95
|
return subtreeExtension
|
|
110
96
|
}
|
|
111
97
|
|
|
112
|
-
let
|
|
113
|
-
if (alternateExtension != null) {
|
|
114
|
-
return alternateExtension
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
let projectExtension = getMostCommonOwnerExtension([
|
|
118
|
-
...ownership.scan.actionEntryPaths,
|
|
119
|
-
...ownership.scan.controllerEntryPaths,
|
|
120
|
-
])
|
|
98
|
+
let projectExtension = getMostCommonOwnerExtension([...ownership.scan.controllerEntryPaths])
|
|
121
99
|
if (projectExtension != null) {
|
|
122
100
|
return projectExtension
|
|
123
101
|
}
|
|
@@ -157,6 +135,12 @@ function getMostCommonOwnerExtension(filePaths: string[]): OwnerFileExtension |
|
|
|
157
135
|
|
|
158
136
|
function getRouteNodesByName(tree: OwnershipRouteNode[]): Map<string, OwnershipRouteNode> {
|
|
159
137
|
let routeNodesByName = new Map<string, OwnershipRouteNode>()
|
|
138
|
+
routeNodesByName.set(ROOT_ROUTE_NAME, {
|
|
139
|
+
children: tree,
|
|
140
|
+
key: ROOT_ROUTE_NAME,
|
|
141
|
+
kind: 'group',
|
|
142
|
+
name: ROOT_ROUTE_NAME,
|
|
143
|
+
})
|
|
160
144
|
|
|
161
145
|
function visit(nodes: OwnershipRouteNode[]): void {
|
|
162
146
|
for (let node of nodes) {
|