@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.
Files changed (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -3
  3. package/bootstrap/AGENTS.md +35 -0
  4. package/bootstrap/README.md +27 -0
  5. package/bootstrap/app/controllers/auth.tsx +21 -0
  6. package/bootstrap/app/controllers/home.tsx +26 -0
  7. package/bootstrap/app/router.ts +10 -0
  8. package/bootstrap/app/routes.ts +6 -0
  9. package/bootstrap/app/ui/document.tsx +21 -0
  10. package/bootstrap/app/ui/layout.tsx +22 -0
  11. package/bootstrap/app/utils/render.tsx +7 -0
  12. package/bootstrap/package.json +22 -0
  13. package/bootstrap/server.ts +37 -0
  14. package/bootstrap/tsconfig.json +19 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/lib/bootstrap-project.d.ts +17 -0
  19. package/dist/lib/bootstrap-project.d.ts.map +1 -0
  20. package/dist/lib/bootstrap-project.js +137 -0
  21. package/dist/lib/cli-context.d.ts +11 -0
  22. package/dist/lib/cli-context.d.ts.map +1 -0
  23. package/dist/lib/cli-context.js +19 -0
  24. package/dist/lib/cli.d.ts +6 -0
  25. package/dist/lib/cli.d.ts.map +1 -0
  26. package/dist/lib/cli.js +87 -0
  27. package/dist/lib/commands/completion.d.ts +3 -0
  28. package/dist/lib/commands/completion.d.ts.map +1 -0
  29. package/dist/lib/commands/completion.js +51 -0
  30. package/dist/lib/commands/doctor.d.ts +4 -0
  31. package/dist/lib/commands/doctor.d.ts.map +1 -0
  32. package/dist/lib/commands/doctor.js +293 -0
  33. package/dist/lib/commands/help.d.ts +4 -0
  34. package/dist/lib/commands/help.d.ts.map +1 -0
  35. package/dist/lib/commands/help.js +124 -0
  36. package/dist/lib/commands/new.d.ts +4 -0
  37. package/dist/lib/commands/new.d.ts.map +1 -0
  38. package/dist/lib/commands/new.js +70 -0
  39. package/dist/lib/commands/routes.d.ts +4 -0
  40. package/dist/lib/commands/routes.d.ts.map +1 -0
  41. package/dist/lib/commands/routes.js +161 -0
  42. package/dist/lib/commands/skills.d.ts +6 -0
  43. package/dist/lib/commands/skills.d.ts.map +1 -0
  44. package/dist/lib/commands/skills.js +222 -0
  45. package/dist/lib/commands/test.d.ts +4 -0
  46. package/dist/lib/commands/test.d.ts.map +1 -0
  47. package/dist/lib/commands/test.js +19 -0
  48. package/dist/lib/commands/version.d.ts +4 -0
  49. package/dist/lib/commands/version.d.ts.map +1 -0
  50. package/dist/lib/commands/version.js +29 -0
  51. package/dist/lib/completion.d.ts +12 -0
  52. package/dist/lib/completion.d.ts.map +1 -0
  53. package/dist/lib/completion.js +408 -0
  54. package/dist/lib/contained-path.d.ts +2 -0
  55. package/dist/lib/contained-path.d.ts.map +1 -0
  56. package/dist/lib/contained-path.js +10 -0
  57. package/dist/lib/controller-files.d.ts +14 -0
  58. package/dist/lib/controller-files.d.ts.map +1 -0
  59. package/dist/lib/controller-files.js +53 -0
  60. package/dist/lib/controller-ownership.d.ts +41 -0
  61. package/dist/lib/controller-ownership.d.ts.map +1 -0
  62. package/dist/lib/controller-ownership.js +224 -0
  63. package/dist/lib/display-path.d.ts +2 -0
  64. package/dist/lib/display-path.d.ts.map +1 -0
  65. package/dist/lib/display-path.js +12 -0
  66. package/dist/lib/doctor/controller-findings.d.ts +4 -0
  67. package/dist/lib/doctor/controller-findings.d.ts.map +1 -0
  68. package/dist/lib/doctor/controller-findings.js +132 -0
  69. package/dist/lib/doctor/controller-fix-plans.d.ts +4 -0
  70. package/dist/lib/doctor/controller-fix-plans.d.ts.map +1 -0
  71. package/dist/lib/doctor/controller-fix-plans.js +130 -0
  72. package/dist/lib/doctor/controller-placeholders.d.ts +4 -0
  73. package/dist/lib/doctor/controller-placeholders.d.ts.map +1 -0
  74. package/dist/lib/doctor/controller-placeholders.js +183 -0
  75. package/dist/lib/doctor/controllers.d.ts +8 -0
  76. package/dist/lib/doctor/controllers.d.ts.map +1 -0
  77. package/dist/lib/doctor/controllers.js +13 -0
  78. package/dist/lib/doctor/environment.d.ts +17 -0
  79. package/dist/lib/doctor/environment.d.ts.map +1 -0
  80. package/dist/lib/doctor/environment.js +226 -0
  81. package/dist/lib/doctor/fixes.d.ts +3 -0
  82. package/dist/lib/doctor/fixes.d.ts.map +1 -0
  83. package/dist/lib/doctor/fixes.js +65 -0
  84. package/dist/lib/doctor/project.d.ts +10 -0
  85. package/dist/lib/doctor/project.d.ts.map +1 -0
  86. package/dist/lib/doctor/project.js +371 -0
  87. package/dist/lib/doctor/types.d.ts +48 -0
  88. package/dist/lib/doctor/types.d.ts.map +1 -0
  89. package/dist/lib/doctor/types.js +15 -0
  90. package/dist/lib/errors.d.ts +173 -0
  91. package/dist/lib/errors.d.ts.map +1 -0
  92. package/dist/lib/errors.js +318 -0
  93. package/dist/lib/help-text.d.ts +13 -0
  94. package/dist/lib/help-text.d.ts.map +1 -0
  95. package/dist/lib/help-text.js +43 -0
  96. package/dist/lib/load-route-map-worker.d.ts +2 -0
  97. package/dist/lib/load-route-map-worker.d.ts.map +1 -0
  98. package/dist/lib/load-route-map-worker.js +92 -0
  99. package/dist/lib/parse-args.d.ts +22 -0
  100. package/dist/lib/parse-args.d.ts.map +1 -0
  101. package/dist/lib/parse-args.js +35 -0
  102. package/dist/lib/remix-version.d.ts +2 -0
  103. package/dist/lib/remix-version.d.ts.map +1 -0
  104. package/dist/lib/remix-version.js +58 -0
  105. package/dist/lib/reporter.d.ts +56 -0
  106. package/dist/lib/reporter.d.ts.map +1 -0
  107. package/dist/lib/reporter.js +357 -0
  108. package/dist/lib/route-map.d.ts +38 -0
  109. package/dist/lib/route-map.d.ts.map +1 -0
  110. package/dist/lib/route-map.js +195 -0
  111. package/dist/lib/skills-cache.d.ts +19 -0
  112. package/dist/lib/skills-cache.d.ts.map +1 -0
  113. package/dist/lib/skills-cache.js +89 -0
  114. package/dist/lib/skills.d.ts +30 -0
  115. package/dist/lib/skills.d.ts.map +1 -0
  116. package/dist/lib/skills.js +441 -0
  117. package/dist/lib/terminal.d.ts +17 -0
  118. package/dist/lib/terminal.d.ts.map +1 -0
  119. package/dist/lib/terminal.js +66 -0
  120. package/package.json +48 -5
  121. package/src/index.ts +1 -0
  122. package/src/lib/bootstrap-project.ts +222 -0
  123. package/src/lib/cli-context.ts +38 -0
  124. package/src/lib/cli.ts +120 -0
  125. package/src/lib/commands/completion.ts +74 -0
  126. package/src/lib/commands/doctor.ts +393 -0
  127. package/src/lib/commands/help.ts +157 -0
  128. package/src/lib/commands/new.ts +104 -0
  129. package/src/lib/commands/routes.ts +251 -0
  130. package/src/lib/commands/skills.ts +306 -0
  131. package/src/lib/commands/test.ts +25 -0
  132. package/src/lib/commands/version.ts +39 -0
  133. package/src/lib/completion.ts +561 -0
  134. package/src/lib/contained-path.ts +13 -0
  135. package/src/lib/controller-files.ts +79 -0
  136. package/src/lib/controller-ownership.ts +397 -0
  137. package/src/lib/display-path.ts +16 -0
  138. package/src/lib/doctor/controller-findings.ts +157 -0
  139. package/src/lib/doctor/controller-fix-plans.ts +184 -0
  140. package/src/lib/doctor/controller-placeholders.ts +242 -0
  141. package/src/lib/doctor/controllers.ts +23 -0
  142. package/src/lib/doctor/environment.ts +289 -0
  143. package/src/lib/doctor/fixes.ts +84 -0
  144. package/src/lib/doctor/project.ts +435 -0
  145. package/src/lib/doctor/types.ts +95 -0
  146. package/src/lib/errors.ts +379 -0
  147. package/src/lib/help-text.ts +88 -0
  148. package/src/lib/load-route-map-worker.ts +110 -0
  149. package/src/lib/parse-args.ts +82 -0
  150. package/src/lib/remix-version.ts +80 -0
  151. package/src/lib/reporter.ts +535 -0
  152. package/src/lib/route-map.ts +303 -0
  153. package/src/lib/skills-cache.ts +140 -0
  154. package/src/lib/skills.ts +706 -0
  155. package/src/lib/terminal.ts +86 -0
@@ -0,0 +1,184 @@
1
+ import * as fs from 'node:fs/promises'
2
+ import * as path from 'node:path'
3
+
4
+ import {
5
+ getOwnerCandidateForExtension,
6
+ getOwnerFileExtension,
7
+ type OwnerFileExtension,
8
+ } from '../controller-files.ts'
9
+ import type {
10
+ ControllerOwnership,
11
+ OwnedSubtree,
12
+ OwnershipRouteNode,
13
+ } from '../controller-ownership.ts'
14
+ import { renderActionPlaceholder, renderControllerPlaceholder } from './controller-placeholders.ts'
15
+ import type { DoctorFixPlan } from './types.ts'
16
+
17
+ const OWNER_EXTENSION_PRIORITY: OwnerFileExtension[] = ['.tsx', '.ts', '.jsx', '.js']
18
+
19
+ export async function getControllerFixPlans(
20
+ appRoot: string,
21
+ tree: OwnershipRouteNode[],
22
+ ownership: ControllerOwnership,
23
+ ): Promise<DoctorFixPlan[]> {
24
+ let routeNodesByName = getRouteNodesByName(tree)
25
+ let hasTsconfig = await pathExists(path.join(appRoot, 'tsconfig.json'))
26
+ let resolvedEntryPathByRouteName = new Map<string, string>()
27
+
28
+ for (let subtree of ownership.subtrees) {
29
+ if (subtree.actualEntryPath != null) {
30
+ resolvedEntryPathByRouteName.set(subtree.routeName, subtree.actualEntryPath)
31
+ continue
32
+ }
33
+
34
+ let fixCode = getFixCodeForSubtree(subtree)
35
+ if (fixCode == null) {
36
+ continue
37
+ }
38
+
39
+ let extension = inferOwnerExtension(subtree, ownership, hasTsconfig)
40
+ let targetPath =
41
+ getOwnerCandidateForExtension(subtree.entryCandidates, extension) ?? subtree.entryDisplayPath
42
+
43
+ resolvedEntryPathByRouteName.set(subtree.routeName, targetPath)
44
+ }
45
+
46
+ let fixPlans: DoctorFixPlan[] = []
47
+
48
+ for (let subtree of ownership.subtrees) {
49
+ let code = getFixCodeForSubtree(subtree)
50
+ if (code == null) {
51
+ continue
52
+ }
53
+
54
+ let routeNode = routeNodesByName.get(subtree.routeName)
55
+ let entryPath = resolvedEntryPathByRouteName.get(subtree.routeName)
56
+ if (routeNode == null || entryPath == null) {
57
+ continue
58
+ }
59
+
60
+ let contents =
61
+ subtree.kind === 'action'
62
+ ? renderActionPlaceholder(routeNode, entryPath)
63
+ : renderControllerPlaceholder(routeNode, entryPath, resolvedEntryPathByRouteName)
64
+
65
+ fixPlans.push({
66
+ code,
67
+ contents,
68
+ kind: 'create-file',
69
+ path: entryPath,
70
+ routeName: subtree.routeName,
71
+ suite: 'controllers',
72
+ })
73
+ }
74
+
75
+ return fixPlans
76
+ }
77
+
78
+ function getFixCodeForSubtree(subtree: OwnedSubtree): DoctorFixPlan['code'] | null {
79
+ if (subtree.actualEntryPaths.length > 1 || subtree.actualAlternatePaths.length > 1) {
80
+ return null
81
+ }
82
+
83
+ if (subtree.actualEntryPath != null) {
84
+ return null
85
+ }
86
+
87
+ if (subtree.actualAlternatePath != null) {
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'
100
+ }
101
+
102
+ function inferOwnerExtension(
103
+ subtree: OwnedSubtree,
104
+ ownership: ControllerOwnership,
105
+ hasTsconfig: boolean,
106
+ ): OwnerFileExtension {
107
+ let subtreeExtension = getMostCommonOwnerExtension(subtree.claimedRouteLocalFilePaths)
108
+ if (subtreeExtension != null) {
109
+ return subtreeExtension
110
+ }
111
+
112
+ let alternateExtension = getMostCommonOwnerExtension(subtree.actualAlternatePaths)
113
+ if (alternateExtension != null) {
114
+ return alternateExtension
115
+ }
116
+
117
+ let projectExtension = getMostCommonOwnerExtension([
118
+ ...ownership.scan.actionEntryPaths,
119
+ ...ownership.scan.controllerEntryPaths,
120
+ ])
121
+ if (projectExtension != null) {
122
+ return projectExtension
123
+ }
124
+
125
+ return hasTsconfig ? '.tsx' : '.js'
126
+ }
127
+
128
+ function getMostCommonOwnerExtension(filePaths: string[]): OwnerFileExtension | null {
129
+ let counts = new Map<OwnerFileExtension, number>()
130
+
131
+ for (let filePath of filePaths) {
132
+ let extension = getOwnerFileExtension(filePath)
133
+ if (extension == null) {
134
+ continue
135
+ }
136
+
137
+ counts.set(extension, (counts.get(extension) ?? 0) + 1)
138
+ }
139
+
140
+ if (counts.size === 0) {
141
+ return null
142
+ }
143
+
144
+ let bestExtension: OwnerFileExtension | null = null
145
+ let bestCount = -1
146
+
147
+ for (let extension of OWNER_EXTENSION_PRIORITY) {
148
+ let count = counts.get(extension) ?? 0
149
+ if (count > bestCount) {
150
+ bestCount = count
151
+ bestExtension = extension
152
+ }
153
+ }
154
+
155
+ return bestExtension
156
+ }
157
+
158
+ function getRouteNodesByName(tree: OwnershipRouteNode[]): Map<string, OwnershipRouteNode> {
159
+ let routeNodesByName = new Map<string, OwnershipRouteNode>()
160
+
161
+ function visit(nodes: OwnershipRouteNode[]): void {
162
+ for (let node of nodes) {
163
+ routeNodesByName.set(node.name, node)
164
+ visit(node.children)
165
+ }
166
+ }
167
+
168
+ visit(tree)
169
+ return routeNodesByName
170
+ }
171
+
172
+ async function pathExists(filePath: string): Promise<boolean> {
173
+ try {
174
+ await fs.access(filePath)
175
+ return true
176
+ } catch (error) {
177
+ let nodeError = error as NodeJS.ErrnoException
178
+ if (nodeError.code === 'ENOENT') {
179
+ return false
180
+ }
181
+
182
+ throw error
183
+ }
184
+ }
@@ -0,0 +1,242 @@
1
+ import * as path from 'node:path'
2
+
3
+ import { getOwnerFileExtension } from '../controller-files.ts'
4
+ import type { OwnershipRouteNode } from '../controller-ownership.ts'
5
+
6
+ const RESERVED_BINDING_IDENTIFIERS = new Set([
7
+ 'await',
8
+ 'break',
9
+ 'case',
10
+ 'catch',
11
+ 'class',
12
+ 'const',
13
+ 'continue',
14
+ 'debugger',
15
+ 'default',
16
+ 'delete',
17
+ 'do',
18
+ 'else',
19
+ 'enum',
20
+ 'export',
21
+ 'extends',
22
+ 'false',
23
+ 'finally',
24
+ 'for',
25
+ 'function',
26
+ 'if',
27
+ 'implements',
28
+ 'import',
29
+ 'in',
30
+ 'instanceof',
31
+ 'interface',
32
+ 'let',
33
+ 'new',
34
+ 'null',
35
+ 'package',
36
+ 'private',
37
+ 'protected',
38
+ 'public',
39
+ 'return',
40
+ 'static',
41
+ 'super',
42
+ 'switch',
43
+ 'this',
44
+ 'throw',
45
+ 'true',
46
+ 'try',
47
+ 'typeof',
48
+ 'var',
49
+ 'void',
50
+ 'while',
51
+ 'with',
52
+ 'yield',
53
+ ])
54
+
55
+ export function renderActionPlaceholder(routeNode: OwnershipRouteNode, entryPath: string): string {
56
+ let routeMessage = `TODO: implement routes.${routeNode.name}`
57
+ let extension = getOwnerFileExtension(entryPath)
58
+ let exportName = getActionExportName(routeNode.key)
59
+
60
+ if (extension === '.js' || extension === '.jsx') {
61
+ return [
62
+ `export const ${exportName} = {`,
63
+ ` handler() {`,
64
+ ` return new Response(${JSON.stringify(routeMessage)})`,
65
+ ` },`,
66
+ `}`,
67
+ '',
68
+ ].join('\n')
69
+ }
70
+
71
+ let routeExpression = getRouteAccessExpression(routeNode.name)
72
+ let routesImportPath = getRelativeImportPath(entryPath, 'app/routes.ts')
73
+ let method = routeNode.method ?? 'ANY'
74
+
75
+ return [
76
+ `import type { BuildAction } from 'remix/fetch-router'`,
77
+ '',
78
+ `import type { routes } from '${routesImportPath}'`,
79
+ '',
80
+ `export const ${exportName}: BuildAction<'${method}', typeof ${routeExpression}> = {`,
81
+ ` handler() {`,
82
+ ` return new Response(${JSON.stringify(routeMessage)})`,
83
+ ` },`,
84
+ `}`,
85
+ '',
86
+ ].join('\n')
87
+ }
88
+
89
+ export function renderControllerPlaceholder(
90
+ routeNode: OwnershipRouteNode,
91
+ entryPath: string,
92
+ resolvedEntryPathByRouteName: Map<string, string>,
93
+ ): string {
94
+ let extension = getOwnerFileExtension(entryPath)
95
+ let lines: string[] = []
96
+ let childImports = getControllerChildImports(routeNode, entryPath, resolvedEntryPathByRouteName)
97
+ let actionEntries = routeNode.children.map((childNode) =>
98
+ renderControllerActionEntry(childNode, entryPath, resolvedEntryPathByRouteName),
99
+ )
100
+
101
+ if (extension === '.js' || extension === '.jsx') {
102
+ if (childImports.length > 0) {
103
+ lines.push(...childImports, '')
104
+ }
105
+
106
+ lines.push('export default {', ' actions: {')
107
+ lines.push(...actionEntries)
108
+ lines.push(' },', '}', '')
109
+ return lines.join('\n')
110
+ }
111
+
112
+ let routeExpression = getRouteAccessExpression(routeNode.name)
113
+ let routesImportPath = getRelativeImportPath(entryPath, 'app/routes.ts')
114
+
115
+ lines.push(`import type { Controller } from 'remix/fetch-router'`, '')
116
+ lines.push(`import type { routes } from '${routesImportPath}'`)
117
+
118
+ if (childImports.length > 0) {
119
+ lines.push('', ...childImports)
120
+ }
121
+
122
+ lines.push('', 'export default {', ' actions: {')
123
+ lines.push(...actionEntries)
124
+ lines.push(' },', `} satisfies Controller<typeof ${routeExpression}>`, '')
125
+
126
+ return lines.join('\n')
127
+ }
128
+
129
+ function getControllerChildImports(
130
+ routeNode: OwnershipRouteNode,
131
+ entryPath: string,
132
+ resolvedEntryPathByRouteName: Map<string, string>,
133
+ ): string[] {
134
+ let imports: string[] = []
135
+
136
+ for (let childNode of routeNode.children) {
137
+ if (childNode.kind !== 'group') {
138
+ continue
139
+ }
140
+
141
+ let childEntryPath = resolvedEntryPathByRouteName.get(childNode.name)
142
+ if (childEntryPath == null) {
143
+ continue
144
+ }
145
+
146
+ imports.push(
147
+ `import ${getControllerImportName(childNode.key)} from '${getRelativeImportPath(entryPath, childEntryPath)}'`,
148
+ )
149
+ }
150
+
151
+ return imports
152
+ }
153
+
154
+ function renderControllerActionEntry(
155
+ childNode: OwnershipRouteNode,
156
+ entryPath: string,
157
+ resolvedEntryPathByRouteName: Map<string, string>,
158
+ ): string {
159
+ if (childNode.kind === 'route') {
160
+ return [
161
+ ` ${formatObjectKey(childNode.key)}() {`,
162
+ ` return new Response(${JSON.stringify(`TODO: implement routes.${childNode.name}`)})`,
163
+ ` },`,
164
+ ].join('\n')
165
+ }
166
+
167
+ let childEntryPath = resolvedEntryPathByRouteName.get(childNode.name)
168
+ if (childEntryPath != null) {
169
+ return ` ${formatObjectKey(childNode.key)}: ${getControllerImportName(childNode.key)},`
170
+ }
171
+
172
+ return ` ${formatObjectKey(childNode.key)}: ${renderInlineControllerPlaceholder(childNode)},`
173
+ }
174
+
175
+ function renderInlineControllerPlaceholder(routeNode: OwnershipRouteNode): string {
176
+ let childEntries = routeNode.children.map((childNode) =>
177
+ childNode.kind === 'route'
178
+ ? `${formatObjectKey(childNode.key)}() { return new Response(${JSON.stringify(`TODO: implement routes.${childNode.name}`)}) }`
179
+ : `${formatObjectKey(childNode.key)}: ${renderInlineControllerPlaceholder(childNode)}`,
180
+ )
181
+
182
+ return `{ actions: { ${childEntries.join(', ')} } }`
183
+ }
184
+
185
+ function getControllerImportName(key: string): string {
186
+ return `${toIdentifier(key)}Controller`
187
+ }
188
+
189
+ function getActionExportName(key: string): string {
190
+ return toIdentifier(key)
191
+ }
192
+
193
+ function getRouteAccessExpression(routeName: string): string {
194
+ let segments = routeName.split('.')
195
+ let expression = 'routes'
196
+
197
+ for (let segment of segments) {
198
+ expression += isIdentifier(segment) ? `.${segment}` : `[${JSON.stringify(segment)}]`
199
+ }
200
+
201
+ return expression
202
+ }
203
+
204
+ function getRelativeImportPath(fromFilePath: string, toFilePath: string): string {
205
+ let relativePath = path.posix.relative(path.posix.dirname(fromFilePath), toFilePath)
206
+ return relativePath.startsWith('.') ? relativePath : `./${relativePath}`
207
+ }
208
+
209
+ function formatObjectKey(key: string): string {
210
+ return isIdentifier(key) ? key : JSON.stringify(key)
211
+ }
212
+
213
+ function toIdentifier(value: string): string {
214
+ let parts = value.split(/[^A-Za-z0-9_$]+/).filter(Boolean)
215
+ if (parts.length === 0) {
216
+ return 'route'
217
+ }
218
+
219
+ let identifier = parts
220
+ .map((part, index) =>
221
+ index === 0 ? part.replace(/^./, (char) => char.toLowerCase()) : capitalize(part),
222
+ )
223
+ .join('')
224
+
225
+ if (!/^[A-Za-z_$]/.test(identifier)) {
226
+ return `route${capitalize(identifier)}`
227
+ }
228
+
229
+ if (RESERVED_BINDING_IDENTIFIERS.has(identifier)) {
230
+ return `route${capitalize(identifier)}`
231
+ }
232
+
233
+ return identifier
234
+ }
235
+
236
+ function capitalize(value: string): string {
237
+ return value.length === 0 ? value : `${value[0].toUpperCase()}${value.slice(1)}`
238
+ }
239
+
240
+ function isIdentifier(value: string): boolean {
241
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value)
242
+ }
@@ -0,0 +1,23 @@
1
+ import { inspectControllerOwnership, type OwnershipRouteNode } from '../controller-ownership.ts'
2
+ import { getControllerFindings } from './controller-findings.ts'
3
+ import { getControllerFixPlans } from './controller-fix-plans.ts'
4
+ import { createDoctorSuite, type DoctorFixPlan, type DoctorSuiteResult } from './types.ts'
5
+
6
+ export interface ControllerDoctorResult {
7
+ fixPlans: DoctorFixPlan[]
8
+ suite: DoctorSuiteResult
9
+ }
10
+
11
+ export async function checkControllerConventions(
12
+ appRoot: string,
13
+ tree: OwnershipRouteNode[],
14
+ ): Promise<ControllerDoctorResult> {
15
+ let ownership = await inspectControllerOwnership(appRoot, tree)
16
+ let fixPlans = await getControllerFixPlans(appRoot, tree, ownership)
17
+ let findings = getControllerFindings(ownership, fixPlans)
18
+
19
+ return {
20
+ fixPlans,
21
+ suite: createDoctorSuite('controllers', findings),
22
+ }
23
+ }