@remix-run/cli 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -1
- package/dist/index.js +1 -1
- package/dist/lib/app-root.d.ts +11 -0
- package/dist/lib/app-root.d.ts.map +1 -0
- package/dist/lib/app-root.js +38 -0
- package/dist/lib/bootstrap-project.js +2 -2
- package/dist/lib/cli-context.d.ts +6 -0
- package/dist/lib/cli-context.d.ts.map +1 -1
- package/dist/lib/cli-context.js +12 -1
- package/dist/lib/cli.d.ts +3 -2
- package/dist/lib/cli.d.ts.map +1 -1
- package/dist/lib/cli.js +42 -14
- package/dist/lib/commands/completion.js +4 -4
- package/dist/lib/commands/db.d.ts +4 -0
- package/dist/lib/commands/db.d.ts.map +1 -0
- package/dist/lib/commands/db.js +283 -0
- package/dist/lib/commands/doctor.d.ts +5 -0
- package/dist/lib/commands/doctor.d.ts.map +1 -1
- package/dist/lib/commands/doctor.js +26 -251
- package/dist/lib/commands/help.d.ts.map +1 -1
- package/dist/lib/commands/help.js +17 -9
- package/dist/lib/commands/new.js +6 -6
- package/dist/lib/commands/routes.js +6 -6
- package/dist/lib/commands/test.d.ts +20 -1
- package/dist/lib/commands/test.d.ts.map +1 -1
- package/dist/lib/commands/test.js +353 -10
- package/dist/lib/commands/version.js +3 -3
- package/dist/lib/completion.d.ts +1 -1
- package/dist/lib/completion.d.ts.map +1 -1
- package/dist/lib/completion.js +229 -6
- package/dist/lib/controller-files.d.ts +0 -8
- package/dist/lib/controller-files.d.ts.map +1 -1
- package/dist/lib/controller-files.js +0 -23
- package/dist/lib/controller-ownership.d.ts +4 -12
- package/dist/lib/controller-ownership.d.ts.map +1 -1
- package/dist/lib/controller-ownership.js +31 -127
- package/dist/lib/database-command.d.ts +20 -0
- package/dist/lib/database-command.d.ts.map +1 -0
- package/dist/lib/database-command.js +7 -0
- package/dist/lib/doctor/controller-findings.d.ts +2 -2
- package/dist/lib/doctor/controller-findings.d.ts.map +1 -1
- package/dist/lib/doctor/controller-findings.js +23 -39
- package/dist/lib/doctor/controllers.d.ts +1 -2
- package/dist/lib/doctor/controllers.d.ts.map +1 -1
- package/dist/lib/doctor/controllers.js +4 -7
- package/dist/lib/doctor/environment.js +2 -2
- package/dist/lib/doctor/fixes.js +1 -1
- package/dist/lib/doctor/project.d.ts.map +1 -1
- package/dist/lib/doctor/project.js +3 -212
- package/dist/lib/doctor/run.d.ts +9 -0
- package/dist/lib/doctor/run.d.ts.map +1 -0
- package/dist/lib/doctor/run.js +237 -0
- package/dist/lib/doctor/types.d.ts +2 -1
- package/dist/lib/doctor/types.d.ts.map +1 -1
- package/dist/lib/doctor/types.js +4 -1
- package/dist/lib/errors.d.ts +30 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +55 -0
- package/dist/lib/help-text.js +1 -1
- package/dist/lib/parse-args.d.ts.map +1 -1
- package/dist/lib/parse-args.js +17 -4
- package/dist/lib/remix-config.d.ts +75 -0
- package/dist/lib/remix-config.d.ts.map +1 -0
- package/dist/lib/remix-config.js +411 -0
- package/dist/lib/reporter.js +1 -1
- package/dist/lib/route-map.js +13 -33
- package/package.json +13 -7
- package/schema/remix.json +242 -0
- package/src/lib/app-root.ts +46 -0
- package/src/lib/cli-context.ts +17 -0
- package/src/lib/cli.ts +41 -6
- package/src/lib/commands/db.ts +377 -0
- package/src/lib/commands/doctor.ts +27 -329
- package/src/lib/commands/help.ts +10 -1
- package/src/lib/commands/test.ts +453 -12
- package/src/lib/completion.ts +321 -6
- package/src/lib/controller-files.ts +0 -38
- package/src/lib/controller-ownership.ts +50 -203
- package/src/lib/database-command.ts +31 -0
- package/src/lib/doctor/controller-findings.ts +30 -47
- package/src/lib/doctor/controllers.ts +2 -6
- package/src/lib/doctor/project.ts +0 -236
- package/src/lib/doctor/run.ts +311 -0
- package/src/lib/doctor/types.ts +6 -5
- package/src/lib/errors.ts +65 -0
- package/src/lib/parse-args.ts +19 -3
- package/src/lib/remix-config.ts +639 -0
- package/src/lib/route-map.ts +11 -48
- package/template/.agents/skills/remix/SKILL.md +38 -27
- package/template/.agents/skills/remix/references/assets-and-browser-modules.md +53 -10
- package/template/.agents/skills/remix/references/auth-and-sessions.md +5 -5
- package/template/.agents/skills/remix/references/data-and-validation.md +15 -15
- package/template/.agents/skills/remix/references/hydration-frames-navigation.md +54 -8
- package/template/.agents/skills/remix/references/middleware-and-server.md +68 -6
- package/template/.agents/skills/remix/references/mixins-styling-events.md +2 -0
- package/template/.agents/skills/remix/references/routing-and-controllers.md +8 -8
- package/template/.agents/skills/remix/references/testing-patterns.md +19 -18
- package/template/AGENTS.md +6 -3
- package/template/README.md +6 -3
- package/template/app/actions/controller.tsx +1 -1
- package/template/app/{ui → actions}/document.tsx +7 -5
- package/template/app/{ui/scaffold-home-page.tsx → actions/home-page.tsx} +1 -2
- package/template/app/actions/public/entry.ts +48 -0
- package/template/app/{assets → actions/public}/prompt-button.tsx +29 -25
- package/template/app/assets.ts +15 -2
- package/template/hmr.ts +46 -0
- package/template/package.json +1 -0
- package/template/server.ts +8 -1
- package/template/tsconfig.json +1 -1
- package/dist/lib/doctor/controller-fix-plans.d.ts +0 -4
- package/dist/lib/doctor/controller-fix-plans.d.ts.map +0 -1
- package/dist/lib/doctor/controller-fix-plans.js +0 -119
- package/dist/lib/doctor/controller-placeholders.d.ts +0 -3
- package/dist/lib/doctor/controller-placeholders.d.ts.map +0 -1
- package/dist/lib/doctor/controller-placeholders.js +0 -52
- package/src/lib/doctor/controller-fix-plans.ts +0 -168
- package/src/lib/doctor/controller-placeholders.ts +0 -70
- package/template/app/assets/entry.ts +0 -17
|
@@ -3,16 +3,12 @@ import * as path from 'node:path'
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
getControllerOwnerCandidates,
|
|
6
|
-
getOwnerModuleBaseName,
|
|
7
6
|
getPreferredOwnerDisplayPath,
|
|
8
7
|
getRouteSubtreePath,
|
|
9
|
-
isControllerEntryFileName,
|
|
10
8
|
toDiskSegment,
|
|
11
9
|
} from './controller-files.ts'
|
|
12
10
|
import type { RouteTreeNodeKind } from './route-map.ts'
|
|
13
11
|
|
|
14
|
-
const ACTIONS_PATH_PREFIX = 'app/actions/'
|
|
15
|
-
|
|
16
12
|
export const ROOT_ROUTE_NAME = '<root>'
|
|
17
13
|
|
|
18
14
|
export interface OwnershipRouteNode {
|
|
@@ -23,17 +19,15 @@ export interface OwnershipRouteNode {
|
|
|
23
19
|
name: string
|
|
24
20
|
}
|
|
25
21
|
|
|
26
|
-
export interface ControllerDirectoryScan {
|
|
27
|
-
controllerEntryPaths: Set<string>
|
|
28
|
-
routeDirectoryPaths: Set<string>
|
|
29
|
-
routeLocalFilePaths: Set<string>
|
|
30
|
-
}
|
|
31
|
-
|
|
32
22
|
export interface RouteDirectoryPlan {
|
|
33
23
|
directoryPath: string
|
|
34
24
|
routeName: string
|
|
35
25
|
}
|
|
36
26
|
|
|
27
|
+
export interface RouteDirectory extends RouteDirectoryPlan {
|
|
28
|
+
exists: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
37
31
|
export interface OwnedSubtreePlan {
|
|
38
32
|
entryCandidates: string[]
|
|
39
33
|
entryDisplayPath: string
|
|
@@ -43,16 +37,10 @@ export interface OwnedSubtreePlan {
|
|
|
43
37
|
|
|
44
38
|
export interface OwnedSubtree extends OwnedSubtreePlan {
|
|
45
39
|
actualEntryPath: string | null
|
|
46
|
-
actualEntryPaths: string[]
|
|
47
|
-
claimedFilePaths: string[]
|
|
48
|
-
claimedRouteLocalFilePaths: string[]
|
|
49
40
|
}
|
|
50
41
|
|
|
51
42
|
export interface ControllerOwnership {
|
|
52
|
-
|
|
53
|
-
orphanRouteDirectoryPaths: string[]
|
|
54
|
-
routeDirectories: RouteDirectoryPlan[]
|
|
55
|
-
scan: ControllerDirectoryScan
|
|
43
|
+
routeDirectories: RouteDirectory[]
|
|
56
44
|
subtrees: OwnedSubtree[]
|
|
57
45
|
}
|
|
58
46
|
|
|
@@ -61,15 +49,14 @@ export async function inspectControllerOwnership(
|
|
|
61
49
|
tree: OwnershipRouteNode[],
|
|
62
50
|
): Promise<ControllerOwnership> {
|
|
63
51
|
let subtreePlans = buildOwnedSubtrees(tree)
|
|
64
|
-
let
|
|
65
|
-
let
|
|
66
|
-
|
|
52
|
+
let routeDirectoryPlans = buildRouteDirectories(tree)
|
|
53
|
+
let [routeDirectories, subtrees] = await Promise.all([
|
|
54
|
+
inspectRouteDirectories(appRoot, routeDirectoryPlans),
|
|
55
|
+
inspectOwnedSubtrees(appRoot, subtreePlans),
|
|
56
|
+
])
|
|
67
57
|
|
|
68
58
|
return {
|
|
69
|
-
orphanControllerPaths: getOrphanControllerPaths(subtreePlans, scan),
|
|
70
|
-
orphanRouteDirectoryPaths: getOrphanRouteDirectoryPaths(routeDirectories, scan),
|
|
71
59
|
routeDirectories,
|
|
72
|
-
scan,
|
|
73
60
|
subtrees,
|
|
74
61
|
}
|
|
75
62
|
}
|
|
@@ -138,197 +125,57 @@ function addSubtreePlan(routeName: string, segments: string[], subtrees: OwnedSu
|
|
|
138
125
|
})
|
|
139
126
|
}
|
|
140
127
|
|
|
141
|
-
async function
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
entries = await fs.readdir(currentDir, { withFileTypes: true })
|
|
151
|
-
} catch (error) {
|
|
152
|
-
let nodeError = error as NodeJS.ErrnoException
|
|
153
|
-
if (nodeError.code === 'ENOENT' && isRoot) {
|
|
154
|
-
return
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
throw error
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
for (let entry of entries) {
|
|
161
|
-
let entryPath = path.join(currentDir, entry.name)
|
|
162
|
-
let relativePath = normalizeRelativePath(path.relative(appRoot, entryPath))
|
|
163
|
-
|
|
164
|
-
if (entry.isDirectory()) {
|
|
165
|
-
routeDirectoryPaths.add(relativePath)
|
|
166
|
-
await walk(entryPath, false)
|
|
167
|
-
continue
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (!entry.isFile()) {
|
|
171
|
-
continue
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (isControllerEntryFileName(entry.name)) {
|
|
175
|
-
controllerEntryPaths.add(relativePath)
|
|
176
|
-
continue
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (isRouteLocalFileName(entry.name)) {
|
|
180
|
-
routeLocalFilePaths.add(relativePath)
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
await walk(actionsDir, true)
|
|
186
|
-
|
|
187
|
-
return {
|
|
188
|
-
controllerEntryPaths,
|
|
189
|
-
routeDirectoryPaths,
|
|
190
|
-
routeLocalFilePaths,
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function applyScanToSubtrees(
|
|
195
|
-
subtreePlans: OwnedSubtreePlan[],
|
|
196
|
-
scan: ControllerDirectoryScan,
|
|
197
|
-
): OwnedSubtree[] {
|
|
198
|
-
let claimedRouteLocalPaths = claimFilesToDeepestSubtree(
|
|
199
|
-
[...scan.routeLocalFilePaths],
|
|
200
|
-
subtreePlans,
|
|
201
|
-
)
|
|
202
|
-
let claimedContentPaths = claimFilesToDeepestSubtree(getNestedContentPaths(scan), subtreePlans)
|
|
203
|
-
|
|
204
|
-
return subtreePlans.map((subtree) => {
|
|
205
|
-
let actualEntryPaths = findOwnerPaths(scan, subtree.entryCandidates)
|
|
206
|
-
|
|
207
|
-
return {
|
|
208
|
-
...subtree,
|
|
209
|
-
actualEntryPath: actualEntryPaths[0] ?? null,
|
|
210
|
-
actualEntryPaths,
|
|
211
|
-
claimedFilePaths: claimedContentPaths.get(subtree.routeName) ?? [],
|
|
212
|
-
claimedRouteLocalFilePaths: claimedRouteLocalPaths.get(subtree.routeName) ?? [],
|
|
213
|
-
}
|
|
214
|
-
})
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function getNestedContentPaths(scan: ControllerDirectoryScan): string[] {
|
|
218
|
-
let nestedControllerPaths = [...scan.controllerEntryPaths].filter((filePath) =>
|
|
219
|
-
isNestedControllerPath(filePath),
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
return [...new Set([...nestedControllerPaths, ...scan.routeLocalFilePaths])].sort()
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function claimFilesToDeepestSubtree(
|
|
226
|
-
filePaths: string[],
|
|
227
|
-
subtreePlans: OwnedSubtreePlan[],
|
|
228
|
-
): Map<string, string[]> {
|
|
229
|
-
let subtreesByDepth = [...subtreePlans].sort((left, right) => {
|
|
230
|
-
if (right.subtreePath.length !== left.subtreePath.length) {
|
|
231
|
-
return right.subtreePath.length - left.subtreePath.length
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return left.routeName.localeCompare(right.routeName)
|
|
235
|
-
})
|
|
236
|
-
let claims = new Map<string, string[]>()
|
|
237
|
-
|
|
238
|
-
for (let filePath of filePaths.sort()) {
|
|
239
|
-
let matchingSubtree = subtreesByDepth.find((subtree) =>
|
|
240
|
-
isWithinDirectory(filePath, subtree.subtreePath),
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
if (matchingSubtree == null) {
|
|
244
|
-
continue
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
let claimedPaths = claims.get(matchingSubtree.routeName)
|
|
248
|
-
if (claimedPaths == null) {
|
|
249
|
-
claims.set(matchingSubtree.routeName, [filePath])
|
|
250
|
-
continue
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
claimedPaths.push(filePath)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return claims
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function getOrphanControllerPaths(
|
|
260
|
-
subtreePlans: OwnedSubtreePlan[],
|
|
261
|
-
scan: ControllerDirectoryScan,
|
|
262
|
-
): string[] {
|
|
263
|
-
let expectedControllerPaths = new Set(subtreePlans.flatMap((subtree) => subtree.entryCandidates))
|
|
264
|
-
|
|
265
|
-
return [...scan.controllerEntryPaths]
|
|
266
|
-
.filter((filePath) => !expectedControllerPaths.has(filePath))
|
|
267
|
-
.sort()
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function getOrphanRouteDirectoryPaths(
|
|
271
|
-
routeDirectories: RouteDirectoryPlan[],
|
|
272
|
-
scan: ControllerDirectoryScan,
|
|
273
|
-
): string[] {
|
|
274
|
-
let expectedRouteDirectories = new Set(
|
|
275
|
-
routeDirectories.map((routeDirectory) => routeDirectory.directoryPath),
|
|
276
|
-
)
|
|
277
|
-
let actualControllerDirectories = [...scan.controllerEntryPaths].map((controllerPath) =>
|
|
278
|
-
normalizeRelativePath(path.dirname(controllerPath)),
|
|
128
|
+
async function inspectRouteDirectories(
|
|
129
|
+
appRoot: string,
|
|
130
|
+
plans: RouteDirectoryPlan[],
|
|
131
|
+
): Promise<RouteDirectory[]> {
|
|
132
|
+
return Promise.all(
|
|
133
|
+
plans.map(async (plan) => ({
|
|
134
|
+
...plan,
|
|
135
|
+
exists: await pathHasType(appRoot, plan.directoryPath, 'directory'),
|
|
136
|
+
})),
|
|
279
137
|
)
|
|
280
|
-
|
|
281
|
-
return [...scan.routeDirectoryPaths]
|
|
282
|
-
.filter((directoryPath) => !expectedRouteDirectories.has(directoryPath))
|
|
283
|
-
.filter(
|
|
284
|
-
(directoryPath) =>
|
|
285
|
-
!actualControllerDirectories.some((controllerPath) =>
|
|
286
|
-
isDirectoryWithinDirectory(controllerPath, directoryPath),
|
|
287
|
-
),
|
|
288
|
-
)
|
|
289
|
-
.sort()
|
|
290
138
|
}
|
|
291
139
|
|
|
292
|
-
function
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
140
|
+
async function inspectOwnedSubtrees(
|
|
141
|
+
appRoot: string,
|
|
142
|
+
plans: OwnedSubtreePlan[],
|
|
143
|
+
): Promise<OwnedSubtree[]> {
|
|
144
|
+
return Promise.all(
|
|
145
|
+
plans.map(async (plan) => ({
|
|
146
|
+
...plan,
|
|
147
|
+
actualEntryPath: await findControllerEntryPath(appRoot, plan.entryCandidates),
|
|
148
|
+
})),
|
|
300
149
|
)
|
|
301
150
|
}
|
|
302
151
|
|
|
303
|
-
function
|
|
304
|
-
|
|
305
|
-
|
|
152
|
+
async function findControllerEntryPath(
|
|
153
|
+
appRoot: string,
|
|
154
|
+
candidatePaths: string[],
|
|
155
|
+
): Promise<string | null> {
|
|
306
156
|
for (let candidatePath of candidatePaths) {
|
|
307
|
-
if (
|
|
308
|
-
|
|
157
|
+
if (await pathHasType(appRoot, candidatePath, 'file')) {
|
|
158
|
+
return candidatePath
|
|
309
159
|
}
|
|
310
160
|
}
|
|
311
161
|
|
|
312
|
-
return
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function isNestedControllerPath(filePath: string): boolean {
|
|
316
|
-
return (
|
|
317
|
-
filePath.startsWith(ACTIONS_PATH_PREFIX) &&
|
|
318
|
-
filePath.slice(ACTIONS_PATH_PREFIX.length).includes('/')
|
|
319
|
-
)
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function isWithinDirectory(filePath: string, directoryPath: string): boolean {
|
|
323
|
-
return filePath.startsWith(`${directoryPath}/`)
|
|
162
|
+
return null
|
|
324
163
|
}
|
|
325
164
|
|
|
326
|
-
function
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
165
|
+
async function pathHasType(
|
|
166
|
+
appRoot: string,
|
|
167
|
+
relativePath: string,
|
|
168
|
+
type: 'directory' | 'file',
|
|
169
|
+
): Promise<boolean> {
|
|
170
|
+
try {
|
|
171
|
+
let stats = await fs.stat(path.join(appRoot, relativePath))
|
|
172
|
+
return type === 'directory' ? stats.isDirectory() : stats.isFile()
|
|
173
|
+
} catch (error) {
|
|
174
|
+
let nodeError = error as NodeJS.ErrnoException
|
|
175
|
+
if (nodeError.code === 'ENOENT' || nodeError.code === 'ENOTDIR') {
|
|
176
|
+
return false
|
|
177
|
+
}
|
|
331
178
|
|
|
332
|
-
|
|
333
|
-
|
|
179
|
+
throw error
|
|
180
|
+
}
|
|
334
181
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RemixDbAdapterConfig } from './remix-config.ts'
|
|
2
|
+
|
|
3
|
+
export type DatabaseCommand = 'migrate' | 'reset' | 'seed' | 'status' | 'wipe'
|
|
4
|
+
|
|
5
|
+
export interface DatabaseCommandInvocation {
|
|
6
|
+
command: DatabaseCommand
|
|
7
|
+
connectionEnv?: string
|
|
8
|
+
journalTable?: string
|
|
9
|
+
migrations?: string
|
|
10
|
+
seed?: string
|
|
11
|
+
to?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DatabaseCommandPlan {
|
|
15
|
+
adapter: RemixDbAdapterConfig
|
|
16
|
+
command: DatabaseCommand
|
|
17
|
+
journalTable?: string
|
|
18
|
+
migrations?: string
|
|
19
|
+
seed?: string
|
|
20
|
+
to?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isDatabaseCommand(value: unknown): value is DatabaseCommand {
|
|
24
|
+
return (
|
|
25
|
+
value === 'migrate' ||
|
|
26
|
+
value === 'reset' ||
|
|
27
|
+
value === 'seed' ||
|
|
28
|
+
value === 'status' ||
|
|
29
|
+
value === 'wipe'
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -2,69 +2,52 @@ import {
|
|
|
2
2
|
ROOT_ROUTE_NAME,
|
|
3
3
|
type ControllerOwnership,
|
|
4
4
|
type OwnedSubtree,
|
|
5
|
+
type RouteDirectory,
|
|
5
6
|
} from '../controller-ownership.ts'
|
|
6
|
-
import type { DoctorFinding
|
|
7
|
+
import type { DoctorFinding } from './types.ts'
|
|
8
|
+
|
|
9
|
+
export function getControllerFindings(ownership: ControllerOwnership): DoctorFinding[] {
|
|
10
|
+
let missingRouteNames = new Set(
|
|
11
|
+
ownership.routeDirectories
|
|
12
|
+
.filter((directory) => !directory.exists)
|
|
13
|
+
.map((directory) => directory.routeName),
|
|
14
|
+
)
|
|
7
15
|
|
|
8
|
-
export function getControllerFindings(
|
|
9
|
-
ownership: ControllerOwnership,
|
|
10
|
-
fixPlans: DoctorFixPlan[],
|
|
11
|
-
): DoctorFinding[] {
|
|
12
16
|
return [
|
|
13
|
-
...getSubtreeFindings(ownership.subtrees,
|
|
14
|
-
...ownership.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
message: `
|
|
17
|
+
...getSubtreeFindings(ownership.subtrees, missingRouteNames),
|
|
18
|
+
...getRouteDirectoryFindings(ownership.routeDirectories),
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getRouteDirectoryFindings(directories: RouteDirectory[]): DoctorFinding[] {
|
|
23
|
+
return directories
|
|
24
|
+
.filter((directory) => !directory.exists)
|
|
25
|
+
.map((directory) => ({
|
|
26
|
+
code: 'missing-route-directory' as const,
|
|
27
|
+
expectedPath: directory.directoryPath,
|
|
28
|
+
message: `Route map "${directory.routeName}" is missing action directory ${directory.directoryPath}.`,
|
|
29
|
+
routeName: directory.routeName,
|
|
25
30
|
severity: 'warn' as const,
|
|
26
31
|
suite: 'actions' as const,
|
|
27
|
-
}))
|
|
28
|
-
]
|
|
32
|
+
}))
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
function getSubtreeFindings(
|
|
35
|
+
function getSubtreeFindings(
|
|
36
|
+
subtrees: OwnedSubtree[],
|
|
37
|
+
missingRouteNames: Set<string>,
|
|
38
|
+
): DoctorFinding[] {
|
|
32
39
|
let findings: DoctorFinding[] = []
|
|
33
|
-
let fixableFindingKeys = new Set(
|
|
34
|
-
fixPlans.map((fixPlan) => `${fixPlan.code}:${fixPlan.routeName ?? ''}`),
|
|
35
|
-
)
|
|
36
40
|
|
|
37
41
|
for (let subtree of subtrees) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (subtree.actualEntryPaths.length > 1) {
|
|
41
|
-
findings.push({
|
|
42
|
-
actualPath: subtree.actualEntryPaths[0],
|
|
43
|
-
code: 'duplicate-owner-file',
|
|
44
|
-
expectedPath: subtree.entryDisplayPath,
|
|
45
|
-
message: `${routeMapName} has multiple action controller files: ${subtree.actualEntryPaths.join(', ')}. Keep only one controller owner file.`,
|
|
46
|
-
routeName: subtree.routeName,
|
|
47
|
-
severity: 'warn',
|
|
48
|
-
suite: 'actions',
|
|
49
|
-
})
|
|
42
|
+
if (missingRouteNames.has(subtree.routeName)) {
|
|
50
43
|
continue
|
|
51
44
|
}
|
|
52
45
|
|
|
53
46
|
if (subtree.actualEntryPath == null) {
|
|
54
|
-
let code =
|
|
55
|
-
subtree.claimedFilePaths.length > 0
|
|
56
|
-
? ('incomplete-controller' as const)
|
|
57
|
-
: ('missing-owner' as const)
|
|
58
|
-
|
|
59
47
|
findings.push({
|
|
60
|
-
|
|
61
|
-
code,
|
|
48
|
+
code: 'missing-owner',
|
|
62
49
|
expectedPath: subtree.entryDisplayPath,
|
|
63
|
-
|
|
64
|
-
message:
|
|
65
|
-
subtree.claimedFilePaths.length > 0
|
|
66
|
-
? `${routeMapName} has files under ${subtree.subtreePath}, but is missing action controller ${subtree.entryDisplayPath}.`
|
|
67
|
-
: `${routeMapName} is missing action controller ${subtree.entryDisplayPath}.`,
|
|
50
|
+
message: `${formatRouteMapName(subtree.routeName)} is missing action controller ${subtree.entryDisplayPath}.`,
|
|
68
51
|
routeName: subtree.routeName,
|
|
69
52
|
severity: 'warn',
|
|
70
53
|
suite: 'actions',
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { inspectControllerOwnership, type OwnershipRouteNode } from '../controller-ownership.ts'
|
|
2
2
|
import { getControllerFindings } from './controller-findings.ts'
|
|
3
|
-
import {
|
|
4
|
-
import { createDoctorSuite, type DoctorFixPlan, type DoctorSuiteResult } from './types.ts'
|
|
3
|
+
import { createDoctorSuite, type DoctorSuiteResult } from './types.ts'
|
|
5
4
|
|
|
6
5
|
export interface ControllerDoctorResult {
|
|
7
|
-
fixPlans: DoctorFixPlan[]
|
|
8
6
|
suite: DoctorSuiteResult
|
|
9
7
|
}
|
|
10
8
|
|
|
@@ -13,11 +11,9 @@ export async function checkControllerConventions(
|
|
|
13
11
|
tree: OwnershipRouteNode[],
|
|
14
12
|
): Promise<ControllerDoctorResult> {
|
|
15
13
|
let ownership = await inspectControllerOwnership(appRoot, tree)
|
|
16
|
-
let
|
|
17
|
-
let findings = getControllerFindings(ownership, fixPlans)
|
|
14
|
+
let findings = getControllerFindings(ownership)
|
|
18
15
|
|
|
19
16
|
return {
|
|
20
|
-
fixPlans,
|
|
21
17
|
suite: createDoctorSuite('actions', findings),
|
|
22
18
|
}
|
|
23
19
|
}
|