@remix-run/cli 0.0.0 → 0.2.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 (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -3
  3. package/bootstrap/.agents/skills/remix/SKILL.md +501 -0
  4. package/bootstrap/.agents/skills/remix/references/animate-elements.md +195 -0
  5. package/bootstrap/.agents/skills/remix/references/assets-and-browser-modules.md +122 -0
  6. package/bootstrap/.agents/skills/remix/references/auth-and-sessions.md +420 -0
  7. package/bootstrap/.agents/skills/remix/references/component-model.md +282 -0
  8. package/bootstrap/.agents/skills/remix/references/create-mixins.md +158 -0
  9. package/bootstrap/.agents/skills/remix/references/data-and-validation.md +363 -0
  10. package/bootstrap/.agents/skills/remix/references/hydration-frames-navigation.md +297 -0
  11. package/bootstrap/.agents/skills/remix/references/middleware-and-server.md +243 -0
  12. package/bootstrap/.agents/skills/remix/references/mixins-styling-events.md +213 -0
  13. package/bootstrap/.agents/skills/remix/references/routing-and-controllers.md +324 -0
  14. package/bootstrap/.agents/skills/remix/references/testing-patterns.md +156 -0
  15. package/bootstrap/AGENTS.md +39 -0
  16. package/bootstrap/README.md +27 -0
  17. package/bootstrap/app/assets/entry.ts +19 -0
  18. package/bootstrap/app/assets.ts +18 -0
  19. package/bootstrap/app/controllers/auth.tsx +21 -0
  20. package/bootstrap/app/controllers/home.tsx +11 -0
  21. package/bootstrap/app/router.ts +16 -0
  22. package/bootstrap/app/routes.ts +7 -0
  23. package/bootstrap/app/ui/document.tsx +26 -0
  24. package/bootstrap/app/ui/layout.tsx +22 -0
  25. package/bootstrap/app/ui/prompt-button.tsx +162 -0
  26. package/bootstrap/app/ui/scaffold-home-page.tsx +526 -0
  27. package/bootstrap/app/utils/render.tsx +26 -0
  28. package/bootstrap/package.json +22 -0
  29. package/bootstrap/server.ts +37 -0
  30. package/bootstrap/tsconfig.json +18 -0
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +1 -0
  34. package/dist/lib/bootstrap-project.d.ts +17 -0
  35. package/dist/lib/bootstrap-project.d.ts.map +1 -0
  36. package/dist/lib/bootstrap-project.js +137 -0
  37. package/dist/lib/cli-context.d.ts +11 -0
  38. package/dist/lib/cli-context.d.ts.map +1 -0
  39. package/dist/lib/cli-context.js +19 -0
  40. package/dist/lib/cli.d.ts +6 -0
  41. package/dist/lib/cli.d.ts.map +1 -0
  42. package/dist/lib/cli.js +84 -0
  43. package/dist/lib/commands/completion.d.ts +3 -0
  44. package/dist/lib/commands/completion.d.ts.map +1 -0
  45. package/dist/lib/commands/completion.js +51 -0
  46. package/dist/lib/commands/doctor.d.ts +4 -0
  47. package/dist/lib/commands/doctor.d.ts.map +1 -0
  48. package/dist/lib/commands/doctor.js +293 -0
  49. package/dist/lib/commands/help.d.ts +4 -0
  50. package/dist/lib/commands/help.d.ts.map +1 -0
  51. package/dist/lib/commands/help.js +100 -0
  52. package/dist/lib/commands/new.d.ts +4 -0
  53. package/dist/lib/commands/new.d.ts.map +1 -0
  54. package/dist/lib/commands/new.js +70 -0
  55. package/dist/lib/commands/routes.d.ts +4 -0
  56. package/dist/lib/commands/routes.d.ts.map +1 -0
  57. package/dist/lib/commands/routes.js +161 -0
  58. package/dist/lib/commands/test.d.ts +4 -0
  59. package/dist/lib/commands/test.d.ts.map +1 -0
  60. package/dist/lib/commands/test.js +23 -0
  61. package/dist/lib/commands/version.d.ts +4 -0
  62. package/dist/lib/commands/version.d.ts.map +1 -0
  63. package/dist/lib/commands/version.js +29 -0
  64. package/dist/lib/completion.d.ts +12 -0
  65. package/dist/lib/completion.d.ts.map +1 -0
  66. package/dist/lib/completion.js +310 -0
  67. package/dist/lib/contained-path.d.ts +2 -0
  68. package/dist/lib/contained-path.d.ts.map +1 -0
  69. package/dist/lib/contained-path.js +10 -0
  70. package/dist/lib/controller-files.d.ts +14 -0
  71. package/dist/lib/controller-files.d.ts.map +1 -0
  72. package/dist/lib/controller-files.js +53 -0
  73. package/dist/lib/controller-ownership.d.ts +41 -0
  74. package/dist/lib/controller-ownership.d.ts.map +1 -0
  75. package/dist/lib/controller-ownership.js +224 -0
  76. package/dist/lib/display-path.d.ts +2 -0
  77. package/dist/lib/display-path.d.ts.map +1 -0
  78. package/dist/lib/display-path.js +12 -0
  79. package/dist/lib/doctor/controller-findings.d.ts +4 -0
  80. package/dist/lib/doctor/controller-findings.d.ts.map +1 -0
  81. package/dist/lib/doctor/controller-findings.js +132 -0
  82. package/dist/lib/doctor/controller-fix-plans.d.ts +4 -0
  83. package/dist/lib/doctor/controller-fix-plans.d.ts.map +1 -0
  84. package/dist/lib/doctor/controller-fix-plans.js +130 -0
  85. package/dist/lib/doctor/controller-placeholders.d.ts +4 -0
  86. package/dist/lib/doctor/controller-placeholders.d.ts.map +1 -0
  87. package/dist/lib/doctor/controller-placeholders.js +183 -0
  88. package/dist/lib/doctor/controllers.d.ts +8 -0
  89. package/dist/lib/doctor/controllers.d.ts.map +1 -0
  90. package/dist/lib/doctor/controllers.js +13 -0
  91. package/dist/lib/doctor/environment.d.ts +17 -0
  92. package/dist/lib/doctor/environment.d.ts.map +1 -0
  93. package/dist/lib/doctor/environment.js +226 -0
  94. package/dist/lib/doctor/fixes.d.ts +3 -0
  95. package/dist/lib/doctor/fixes.d.ts.map +1 -0
  96. package/dist/lib/doctor/fixes.js +65 -0
  97. package/dist/lib/doctor/project.d.ts +10 -0
  98. package/dist/lib/doctor/project.d.ts.map +1 -0
  99. package/dist/lib/doctor/project.js +371 -0
  100. package/dist/lib/doctor/types.d.ts +48 -0
  101. package/dist/lib/doctor/types.d.ts.map +1 -0
  102. package/dist/lib/doctor/types.js +15 -0
  103. package/dist/lib/errors.d.ts +167 -0
  104. package/dist/lib/errors.d.ts.map +1 -0
  105. package/dist/lib/errors.js +307 -0
  106. package/dist/lib/help-text.d.ts +13 -0
  107. package/dist/lib/help-text.d.ts.map +1 -0
  108. package/dist/lib/help-text.js +43 -0
  109. package/dist/lib/load-route-map-worker.d.ts +2 -0
  110. package/dist/lib/load-route-map-worker.d.ts.map +1 -0
  111. package/dist/lib/load-route-map-worker.js +92 -0
  112. package/dist/lib/parse-args.d.ts +22 -0
  113. package/dist/lib/parse-args.d.ts.map +1 -0
  114. package/dist/lib/parse-args.js +35 -0
  115. package/dist/lib/remix-version.d.ts +2 -0
  116. package/dist/lib/remix-version.d.ts.map +1 -0
  117. package/dist/lib/remix-version.js +58 -0
  118. package/dist/lib/reporter.d.ts +56 -0
  119. package/dist/lib/reporter.d.ts.map +1 -0
  120. package/dist/lib/reporter.js +357 -0
  121. package/dist/lib/route-map.d.ts +38 -0
  122. package/dist/lib/route-map.d.ts.map +1 -0
  123. package/dist/lib/route-map.js +195 -0
  124. package/dist/lib/terminal.d.ts +17 -0
  125. package/dist/lib/terminal.d.ts.map +1 -0
  126. package/dist/lib/terminal.js +66 -0
  127. package/package.json +47 -5
  128. package/src/index.ts +1 -0
  129. package/src/lib/bootstrap-project.ts +222 -0
  130. package/src/lib/cli-context.ts +38 -0
  131. package/src/lib/cli.ts +116 -0
  132. package/src/lib/commands/completion.ts +74 -0
  133. package/src/lib/commands/doctor.ts +393 -0
  134. package/src/lib/commands/help.ts +123 -0
  135. package/src/lib/commands/new.ts +104 -0
  136. package/src/lib/commands/routes.ts +251 -0
  137. package/src/lib/commands/test.ts +31 -0
  138. package/src/lib/commands/version.ts +39 -0
  139. package/src/lib/completion.ts +418 -0
  140. package/src/lib/contained-path.ts +13 -0
  141. package/src/lib/controller-files.ts +79 -0
  142. package/src/lib/controller-ownership.ts +397 -0
  143. package/src/lib/display-path.ts +16 -0
  144. package/src/lib/doctor/controller-findings.ts +157 -0
  145. package/src/lib/doctor/controller-fix-plans.ts +184 -0
  146. package/src/lib/doctor/controller-placeholders.ts +242 -0
  147. package/src/lib/doctor/controllers.ts +23 -0
  148. package/src/lib/doctor/environment.ts +289 -0
  149. package/src/lib/doctor/fixes.ts +84 -0
  150. package/src/lib/doctor/project.ts +435 -0
  151. package/src/lib/doctor/types.ts +95 -0
  152. package/src/lib/errors.ts +367 -0
  153. package/src/lib/help-text.ts +88 -0
  154. package/src/lib/load-route-map-worker.ts +110 -0
  155. package/src/lib/parse-args.ts +82 -0
  156. package/src/lib/remix-version.ts +80 -0
  157. package/src/lib/reporter.ts +535 -0
  158. package/src/lib/route-map.ts +303 -0
  159. package/src/lib/terminal.ts +86 -0
@@ -0,0 +1,435 @@
1
+ import * as fs from 'node:fs/promises'
2
+ import * as path from 'node:path'
3
+
4
+ import {
5
+ getOwnerFileExtension,
6
+ isActionFileName,
7
+ isControllerEntryFileName,
8
+ type OwnerFileExtension,
9
+ } from '../controller-files.ts'
10
+ import { CliError } from '../errors.ts'
11
+ import { loadRouteManifestFromAppRoot, type LoadedRouteManifest } from '../route-map.ts'
12
+ import {
13
+ createDoctorSuite,
14
+ type DoctorFinding,
15
+ type DoctorFixPlan,
16
+ type DoctorSuiteResult,
17
+ } from './types.ts'
18
+
19
+ export interface ProjectDoctorResult {
20
+ routesFile: string
21
+ routeManifest?: LoadedRouteManifest
22
+ suite: DoctorSuiteResult
23
+ }
24
+
25
+ export async function checkProject(projectRoot: string): Promise<ProjectDoctorResult> {
26
+ let routesFile = path.join(projectRoot, 'app', 'routes.ts')
27
+
28
+ if (!(await pathExists(routesFile))) {
29
+ return {
30
+ routesFile,
31
+ suite: createDoctorSuite('project', [
32
+ {
33
+ code: 'routes-file-missing',
34
+ expectedPath: 'app/routes.ts',
35
+ fixable: true,
36
+ message: 'Project is missing app/routes.ts.',
37
+ severity: 'warn',
38
+ suite: 'project',
39
+ },
40
+ ]),
41
+ }
42
+ }
43
+
44
+ try {
45
+ let routeManifest = await loadRouteManifestFromAppRoot(projectRoot)
46
+
47
+ return {
48
+ routeManifest,
49
+ routesFile,
50
+ suite: createDoctorSuite('project', []),
51
+ }
52
+ } catch (error) {
53
+ let finding = toProjectFinding(error)
54
+
55
+ return {
56
+ routesFile,
57
+ suite: createDoctorSuite('project', [finding]),
58
+ }
59
+ }
60
+ }
61
+
62
+ export async function getProjectFixPlans(projectRoot: string): Promise<DoctorFixPlan[]> {
63
+ let routesFile = path.join(projectRoot, 'app', 'routes.ts')
64
+ let homeActionPath = normalizeRelativePath(
65
+ path.join('app', 'controllers', `home${await inferHomeOwnerExtension(projectRoot)}`),
66
+ )
67
+
68
+ if (await pathExists(routesFile)) {
69
+ let routesSource = await fs.readFile(routesFile, 'utf8')
70
+ if (!hasOnlyWhitespaceAndComments(routesSource)) {
71
+ return []
72
+ }
73
+
74
+ return [
75
+ {
76
+ code: 'routes-export-missing',
77
+ contents: renderDefaultRoutesFile(),
78
+ kind: 'update-file',
79
+ path: 'app/routes.ts',
80
+ suite: 'project',
81
+ },
82
+ ...((await pathExists(path.join(projectRoot, homeActionPath)))
83
+ ? []
84
+ : [
85
+ {
86
+ code: 'missing-owner',
87
+ contents: renderDefaultHomeAction(homeActionPath),
88
+ kind: 'create-file',
89
+ path: homeActionPath,
90
+ routeName: 'home',
91
+ suite: 'project',
92
+ } satisfies DoctorFixPlan,
93
+ ]),
94
+ ]
95
+ }
96
+
97
+ return [
98
+ {
99
+ code: 'routes-file-missing',
100
+ contents: renderDefaultRoutesFile(),
101
+ kind: 'create-file',
102
+ path: 'app/routes.ts',
103
+ suite: 'project',
104
+ },
105
+ {
106
+ code: 'missing-owner',
107
+ contents: renderDefaultHomeAction(homeActionPath),
108
+ kind: 'create-file',
109
+ path: homeActionPath,
110
+ routeName: 'home',
111
+ suite: 'project',
112
+ },
113
+ ]
114
+ }
115
+
116
+ function toProjectFinding(error: unknown): DoctorFinding {
117
+ if (error instanceof CliError) {
118
+ if (error.code === 'RMX_ROUTE_MAP_LOADER_INVALID_JSON') {
119
+ return {
120
+ code: 'route-map-invalid-json',
121
+ message: 'Route-map loader returned invalid JSON while loading app/routes.ts.',
122
+ severity: 'warn',
123
+ suite: 'project',
124
+ }
125
+ }
126
+
127
+ if (error.code === 'RMX_ROUTE_MAP_LOADER_SIGNAL') {
128
+ return {
129
+ code: 'route-map-loader-signal',
130
+ message: error.message,
131
+ severity: 'warn',
132
+ suite: 'project',
133
+ }
134
+ }
135
+
136
+ if (error.code === 'RMX_ROUTE_MAP_LOADER_FAILED') {
137
+ return classifyRouteMapLoaderFailure(error.message)
138
+ }
139
+ }
140
+
141
+ let message = error instanceof Error ? error.message : String(error)
142
+ return {
143
+ code: 'route-module-import-failed',
144
+ message: `Failed to load app/routes.ts: ${message}`,
145
+ severity: 'warn',
146
+ suite: 'project',
147
+ }
148
+ }
149
+
150
+ function classifyRouteMapLoaderFailure(message: string): DoctorFinding {
151
+ if (message.includes('must export a named "routes" value')) {
152
+ return {
153
+ code: 'routes-export-missing',
154
+ message: 'app/routes.ts must export a named "routes" value.',
155
+ severity: 'warn',
156
+ suite: 'project',
157
+ }
158
+ }
159
+
160
+ if (
161
+ message.startsWith('Invalid route map value at "') ||
162
+ message.startsWith('Detected a route map cycle at "')
163
+ ) {
164
+ return {
165
+ code: 'route-map-invalid',
166
+ message,
167
+ severity: 'warn',
168
+ suite: 'project',
169
+ }
170
+ }
171
+
172
+ return {
173
+ code: 'route-module-import-failed',
174
+ message: `Failed to load app/routes.ts: ${message}`,
175
+ severity: 'warn',
176
+ suite: 'project',
177
+ }
178
+ }
179
+
180
+ async function pathExists(filePath: string): Promise<boolean> {
181
+ try {
182
+ await fs.access(filePath)
183
+ return true
184
+ } catch (error) {
185
+ let nodeError = error as NodeJS.ErrnoException
186
+ if (nodeError.code === 'ENOENT') {
187
+ return false
188
+ }
189
+
190
+ throw error
191
+ }
192
+ }
193
+
194
+ function hasOnlyWhitespaceAndComments(source: string): boolean {
195
+ for (let index = 0; index < source.length; ) {
196
+ let char = source[index]
197
+ let next = source[index + 1]
198
+
199
+ if (char == null || /\s/.test(char)) {
200
+ index += 1
201
+ continue
202
+ }
203
+
204
+ if (char === '/' && next === '/') {
205
+ index += 2
206
+ while (index < source.length && source[index] !== '\n') {
207
+ index += 1
208
+ }
209
+ continue
210
+ }
211
+
212
+ if (char === '/' && next === '*') {
213
+ index += 2
214
+ while (index < source.length && !(source[index] === '*' && source[index + 1] === '/')) {
215
+ index += 1
216
+ }
217
+ index = Math.min(index + 2, source.length)
218
+ continue
219
+ }
220
+
221
+ return false
222
+ }
223
+
224
+ return true
225
+ }
226
+
227
+ async function inferHomeOwnerExtension(projectRoot: string): Promise<OwnerFileExtension> {
228
+ let controllersDir = path.join(projectRoot, 'app', 'controllers')
229
+ let extensions = await collectOwnerExtensions(controllersDir)
230
+
231
+ if (extensions.length > 0) {
232
+ return getMostCommonExtension(extensions)
233
+ }
234
+
235
+ if (await pathExists(path.join(projectRoot, 'tsconfig.json'))) {
236
+ return '.tsx'
237
+ }
238
+
239
+ return '.js'
240
+ }
241
+
242
+ async function collectOwnerExtensions(directory: string): Promise<OwnerFileExtension[]> {
243
+ try {
244
+ let entries = await fs.readdir(directory, { withFileTypes: true })
245
+ let extensions: OwnerFileExtension[] = []
246
+
247
+ for (let entry of entries) {
248
+ let absolutePath = path.join(directory, entry.name)
249
+
250
+ if (entry.isDirectory()) {
251
+ extensions.push(...(await collectOwnerExtensions(absolutePath)))
252
+ continue
253
+ }
254
+
255
+ let fileName = path.basename(absolutePath)
256
+ if (!isActionFileName(fileName) && !isControllerEntryFileName(fileName)) {
257
+ continue
258
+ }
259
+
260
+ let extension = getOwnerFileExtension(fileName)
261
+ if (extension != null) {
262
+ extensions.push(extension)
263
+ }
264
+ }
265
+
266
+ return extensions
267
+ } catch (error) {
268
+ let nodeError = error as NodeJS.ErrnoException
269
+ if (nodeError.code === 'ENOENT') {
270
+ return []
271
+ }
272
+
273
+ throw error
274
+ }
275
+ }
276
+
277
+ function getMostCommonExtension(extensions: OwnerFileExtension[]): OwnerFileExtension {
278
+ let priority: OwnerFileExtension[] = ['.tsx', '.ts', '.jsx', '.js']
279
+ let counts = new Map<OwnerFileExtension, number>()
280
+
281
+ for (let extension of extensions) {
282
+ counts.set(extension, (counts.get(extension) ?? 0) + 1)
283
+ }
284
+
285
+ let bestExtension = priority[0]
286
+ let bestCount = -1
287
+
288
+ for (let extension of priority) {
289
+ let count = counts.get(extension) ?? 0
290
+ if (count > bestCount) {
291
+ bestCount = count
292
+ bestExtension = extension
293
+ }
294
+ }
295
+
296
+ return bestExtension
297
+ }
298
+
299
+ function renderDefaultRoutesFile(): string {
300
+ return [
301
+ `import { route } from 'remix/fetch-router/routes'`,
302
+ '',
303
+ 'export const routes = route({',
304
+ ` home: '/',`,
305
+ '})',
306
+ '',
307
+ ].join('\n')
308
+ }
309
+
310
+ function normalizeRelativePath(filePath: string): string {
311
+ return filePath.split(path.sep).join('/')
312
+ }
313
+
314
+ function renderDefaultHomeAction(entryPath: string): string {
315
+ let extension = getOwnerFileExtension(entryPath)
316
+ if (extension === '.js') {
317
+ return [
318
+ `import { html } from 'remix/html-template'`,
319
+ `import { createHtmlResponse } from 'remix/response/html'`,
320
+ '',
321
+ 'export const home = {',
322
+ ' handler() {',
323
+ ' let page = html`',
324
+ ' <html lang="en">',
325
+ ' <head>',
326
+ ' <meta charset="utf-8" />',
327
+ ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
328
+ ' <title>Home</title>',
329
+ ' </head>',
330
+ ' <body>',
331
+ ' <h1>Home</h1>',
332
+ ' <p>Update app/routes.ts and app/controllers/home to keep building your app.</p>',
333
+ ' </body>',
334
+ ' </html>',
335
+ ' `',
336
+ ' return createHtmlResponse(page)',
337
+ ' },',
338
+ '}',
339
+ '',
340
+ ].join('\n')
341
+ }
342
+
343
+ if (extension === '.ts') {
344
+ return [
345
+ `import type { BuildAction } from 'remix/fetch-router'`,
346
+ `import { html } from 'remix/html-template'`,
347
+ `import { createHtmlResponse } from 'remix/response/html'`,
348
+ '',
349
+ `import type { routes } from '../routes.ts'`,
350
+ '',
351
+ `export const home: BuildAction<'ANY', typeof routes.home> = {`,
352
+ ' handler() {',
353
+ ' let page = html`',
354
+ ' <html lang="en">',
355
+ ' <head>',
356
+ ' <meta charset="utf-8" />',
357
+ ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
358
+ ' <title>Home</title>',
359
+ ' </head>',
360
+ ' <body>',
361
+ ' <h1>Home</h1>',
362
+ ' <p>Update app/routes.ts and app/controllers/home to keep building your app.</p>',
363
+ ' </body>',
364
+ ' </html>',
365
+ ' `',
366
+ ' return createHtmlResponse(page)',
367
+ ' },',
368
+ '}',
369
+ '',
370
+ ].join('\n')
371
+ }
372
+
373
+ if (extension === '.jsx') {
374
+ return [
375
+ `import { renderToStream } from 'remix/ui/server'`,
376
+ `import { createHtmlResponse } from 'remix/response/html'`,
377
+ '',
378
+ 'export const home = {',
379
+ ' handler() {',
380
+ ' let page = <HomePage />',
381
+ ' return createHtmlResponse(renderToStream(page))',
382
+ ' },',
383
+ '}',
384
+ '',
385
+ 'function HomePage() {',
386
+ ' return () => (',
387
+ ' <html lang="en">',
388
+ ' <head>',
389
+ ' <meta charSet="utf-8" />',
390
+ ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
391
+ ' <title>Home</title>',
392
+ ' </head>',
393
+ ' <body>',
394
+ ' <h1>Home</h1>',
395
+ ' <p>Update app/routes.ts and app/controllers/home to keep building your app.</p>',
396
+ ' </body>',
397
+ ' </html>',
398
+ ' )',
399
+ '}',
400
+ '',
401
+ ].join('\n')
402
+ }
403
+
404
+ return [
405
+ `import type { BuildAction } from 'remix/fetch-router'`,
406
+ `import { renderToStream } from 'remix/ui/server'`,
407
+ `import { createHtmlResponse } from 'remix/response/html'`,
408
+ '',
409
+ `import type { routes } from '../routes.ts'`,
410
+ '',
411
+ `export const home: BuildAction<'ANY', typeof routes.home> = {`,
412
+ ' handler() {',
413
+ ' let page = <HomePage />',
414
+ ' return createHtmlResponse(renderToStream(page))',
415
+ ' },',
416
+ '}',
417
+ '',
418
+ 'function HomePage() {',
419
+ ' return () => (',
420
+ ' <html lang="en">',
421
+ ' <head>',
422
+ ' <meta charSet="utf-8" />',
423
+ ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
424
+ ' <title>Home</title>',
425
+ ' </head>',
426
+ ' <body>',
427
+ ' <h1>Home</h1>',
428
+ ' <p>Update app/routes.ts and app/controllers/home to keep building your app.</p>',
429
+ ' </body>',
430
+ ' </html>',
431
+ ' )',
432
+ '}',
433
+ '',
434
+ ].join('\n')
435
+ }
@@ -0,0 +1,95 @@
1
+ export type DoctorSuiteName = 'environment' | 'project' | 'controllers'
2
+ export type DoctorFindingSeverity = 'warn' | 'advice'
3
+ export type DoctorSuiteStatus = 'ok' | 'issues' | 'skipped'
4
+
5
+ export type DoctorFindingCode =
6
+ | 'project-root-not-found'
7
+ | 'package-json-read-failed'
8
+ | 'package-json-invalid'
9
+ | 'node-engine-missing'
10
+ | 'node-engine-unparseable'
11
+ | 'node-version-unsupported'
12
+ | 'remix-dependency-missing'
13
+ | 'remix-install-missing'
14
+ | 'routes-file-missing'
15
+ | 'routes-export-missing'
16
+ | 'route-map-invalid'
17
+ | 'route-module-import-failed'
18
+ | 'route-map-invalid-json'
19
+ | 'route-map-loader-signal'
20
+ | 'missing-owner'
21
+ | 'wrong-owner-kind'
22
+ | 'ambiguous-owner'
23
+ | 'duplicate-owner-file'
24
+ | 'incomplete-controller'
25
+ | 'promotion-drift'
26
+ | 'orphan-action'
27
+ | 'orphan-controller'
28
+ | 'orphan-route-directory'
29
+
30
+ export type DoctorFixKind = 'create-directory' | 'create-file' | 'update-file'
31
+
32
+ export interface DoctorFinding {
33
+ actualPath?: string
34
+ code: DoctorFindingCode
35
+ expectedPath?: string
36
+ fixable?: boolean
37
+ message: string
38
+ routeName?: string
39
+ severity: DoctorFindingSeverity
40
+ suite: DoctorSuiteName
41
+ }
42
+
43
+ export interface DoctorFixPlan {
44
+ code: DoctorFindingCode
45
+ contents?: string
46
+ kind: DoctorFixKind
47
+ path: string
48
+ routeName?: string
49
+ suite: DoctorSuiteName
50
+ }
51
+
52
+ export interface DoctorAppliedFix {
53
+ code: DoctorFindingCode
54
+ kind: DoctorFixKind
55
+ path: string
56
+ routeName?: string
57
+ suite: DoctorSuiteName
58
+ }
59
+
60
+ export interface DoctorSuiteResult {
61
+ appliedFixes?: DoctorAppliedFix[]
62
+ findings: DoctorFinding[]
63
+ name: DoctorSuiteName
64
+ reason?: string
65
+ status: DoctorSuiteStatus
66
+ }
67
+
68
+ export interface DoctorReport {
69
+ appliedFixes?: DoctorAppliedFix[]
70
+ appRoot?: string
71
+ findings: DoctorFinding[]
72
+ remainingFindings?: DoctorFinding[]
73
+ routesFile?: string
74
+ suites: DoctorSuiteResult[]
75
+ }
76
+
77
+ export function createDoctorSuite(
78
+ name: DoctorSuiteName,
79
+ findings: DoctorFinding[],
80
+ ): DoctorSuiteResult {
81
+ return {
82
+ findings,
83
+ name,
84
+ status: findings.some((finding) => finding.severity === 'warn') ? 'issues' : 'ok',
85
+ }
86
+ }
87
+
88
+ export function createSkippedDoctorSuite(name: DoctorSuiteName, reason: string): DoctorSuiteResult {
89
+ return {
90
+ findings: [],
91
+ name,
92
+ reason,
93
+ status: 'skipped',
94
+ }
95
+ }