@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
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises'
|
|
2
2
|
import * as path from 'node:path'
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
getOwnerFileExtension,
|
|
6
|
-
isActionFileName,
|
|
7
|
-
isControllerEntryFileName,
|
|
8
|
-
type OwnerFileExtension,
|
|
9
|
-
} from '../controller-files.ts'
|
|
10
4
|
import { CliError } from '../errors.ts'
|
|
11
5
|
import { loadRouteManifestFromAppRoot, type LoadedRouteManifest } from '../route-map.ts'
|
|
12
6
|
import {
|
|
@@ -61,9 +55,6 @@ export async function checkProject(projectRoot: string): Promise<ProjectDoctorRe
|
|
|
61
55
|
|
|
62
56
|
export async function getProjectFixPlans(projectRoot: string): Promise<DoctorFixPlan[]> {
|
|
63
57
|
let routesFile = path.join(projectRoot, 'app', 'routes.ts')
|
|
64
|
-
let homeActionPath = normalizeRelativePath(
|
|
65
|
-
path.join('app', 'actions', `controller${await inferHomeOwnerExtension(projectRoot)}`),
|
|
66
|
-
)
|
|
67
58
|
|
|
68
59
|
if (await pathExists(routesFile)) {
|
|
69
60
|
let routesSource = await fs.readFile(routesFile, 'utf8')
|
|
@@ -79,18 +70,6 @@ export async function getProjectFixPlans(projectRoot: string): Promise<DoctorFix
|
|
|
79
70
|
path: 'app/routes.ts',
|
|
80
71
|
suite: 'project',
|
|
81
72
|
},
|
|
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
73
|
]
|
|
95
74
|
}
|
|
96
75
|
|
|
@@ -102,14 +81,6 @@ export async function getProjectFixPlans(projectRoot: string): Promise<DoctorFix
|
|
|
102
81
|
path: 'app/routes.ts',
|
|
103
82
|
suite: 'project',
|
|
104
83
|
},
|
|
105
|
-
{
|
|
106
|
-
code: 'missing-owner',
|
|
107
|
-
contents: renderDefaultHomeAction(homeActionPath),
|
|
108
|
-
kind: 'create-file',
|
|
109
|
-
path: homeActionPath,
|
|
110
|
-
routeName: 'home',
|
|
111
|
-
suite: 'project',
|
|
112
|
-
},
|
|
113
84
|
]
|
|
114
85
|
}
|
|
115
86
|
|
|
@@ -224,78 +195,6 @@ function hasOnlyWhitespaceAndComments(source: string): boolean {
|
|
|
224
195
|
return true
|
|
225
196
|
}
|
|
226
197
|
|
|
227
|
-
async function inferHomeOwnerExtension(projectRoot: string): Promise<OwnerFileExtension> {
|
|
228
|
-
let actionsDir = path.join(projectRoot, 'app', 'actions')
|
|
229
|
-
let extensions = await collectOwnerExtensions(actionsDir)
|
|
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
198
|
function renderDefaultRoutesFile(): string {
|
|
300
199
|
return [
|
|
301
200
|
`import { route } from 'remix/routes'`,
|
|
@@ -306,138 +205,3 @@ function renderDefaultRoutesFile(): string {
|
|
|
306
205
|
'',
|
|
307
206
|
].join('\n')
|
|
308
207
|
}
|
|
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 default {',
|
|
322
|
-
' actions: {',
|
|
323
|
-
' home() {',
|
|
324
|
-
' let page = html`',
|
|
325
|
-
' <html lang="en">',
|
|
326
|
-
' <head>',
|
|
327
|
-
' <meta charset="utf-8" />',
|
|
328
|
-
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
329
|
-
' <title>Home</title>',
|
|
330
|
-
' </head>',
|
|
331
|
-
' <body>',
|
|
332
|
-
' <h1>Home</h1>',
|
|
333
|
-
' <p>Update app/routes.ts and app/actions/controller to keep building your app.</p>',
|
|
334
|
-
' </body>',
|
|
335
|
-
' </html>',
|
|
336
|
-
' `',
|
|
337
|
-
' return createHtmlResponse(page)',
|
|
338
|
-
' },',
|
|
339
|
-
' },',
|
|
340
|
-
'}',
|
|
341
|
-
'',
|
|
342
|
-
].join('\n')
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (extension === '.ts') {
|
|
346
|
-
return [
|
|
347
|
-
`import { createController } from 'remix/router'`,
|
|
348
|
-
`import { html } from 'remix/html-template'`,
|
|
349
|
-
`import { createHtmlResponse } from 'remix/response/html'`,
|
|
350
|
-
'',
|
|
351
|
-
`import { routes } from '../routes.ts'`,
|
|
352
|
-
'',
|
|
353
|
-
`export default createController(routes, {`,
|
|
354
|
-
' actions: {',
|
|
355
|
-
' home() {',
|
|
356
|
-
' let page = html`',
|
|
357
|
-
' <html lang="en">',
|
|
358
|
-
' <head>',
|
|
359
|
-
' <meta charset="utf-8" />',
|
|
360
|
-
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
361
|
-
' <title>Home</title>',
|
|
362
|
-
' </head>',
|
|
363
|
-
' <body>',
|
|
364
|
-
' <h1>Home</h1>',
|
|
365
|
-
' <p>Update app/routes.ts and app/actions/controller to keep building your app.</p>',
|
|
366
|
-
' </body>',
|
|
367
|
-
' </html>',
|
|
368
|
-
' `',
|
|
369
|
-
' return createHtmlResponse(page)',
|
|
370
|
-
' },',
|
|
371
|
-
' },',
|
|
372
|
-
`})`,
|
|
373
|
-
'',
|
|
374
|
-
].join('\n')
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
if (extension === '.jsx') {
|
|
378
|
-
return [
|
|
379
|
-
`import { renderToStream } from 'remix/ui/server'`,
|
|
380
|
-
`import { createHtmlResponse } from 'remix/response/html'`,
|
|
381
|
-
'',
|
|
382
|
-
'export default {',
|
|
383
|
-
' actions: {',
|
|
384
|
-
' home() {',
|
|
385
|
-
' let page = <HomePage />',
|
|
386
|
-
' return createHtmlResponse(renderToStream(page))',
|
|
387
|
-
' },',
|
|
388
|
-
' },',
|
|
389
|
-
'}',
|
|
390
|
-
'',
|
|
391
|
-
'function HomePage() {',
|
|
392
|
-
' return () => (',
|
|
393
|
-
' <html lang="en">',
|
|
394
|
-
' <head>',
|
|
395
|
-
' <meta charSet="utf-8" />',
|
|
396
|
-
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
397
|
-
' <title>Home</title>',
|
|
398
|
-
' </head>',
|
|
399
|
-
' <body>',
|
|
400
|
-
' <h1>Home</h1>',
|
|
401
|
-
' <p>Update app/routes.ts and app/actions/controller to keep building your app.</p>',
|
|
402
|
-
' </body>',
|
|
403
|
-
' </html>',
|
|
404
|
-
' )',
|
|
405
|
-
'}',
|
|
406
|
-
'',
|
|
407
|
-
].join('\n')
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
return [
|
|
411
|
-
`import { createController } from 'remix/router'`,
|
|
412
|
-
`import { renderToStream } from 'remix/ui/server'`,
|
|
413
|
-
`import { createHtmlResponse } from 'remix/response/html'`,
|
|
414
|
-
'',
|
|
415
|
-
`import { routes } from '../routes.ts'`,
|
|
416
|
-
'',
|
|
417
|
-
`export default createController(routes, {`,
|
|
418
|
-
' actions: {',
|
|
419
|
-
' home() {',
|
|
420
|
-
' let page = <HomePage />',
|
|
421
|
-
' return createHtmlResponse(renderToStream(page))',
|
|
422
|
-
' },',
|
|
423
|
-
' },',
|
|
424
|
-
`})`,
|
|
425
|
-
'',
|
|
426
|
-
'function HomePage() {',
|
|
427
|
-
' return () => (',
|
|
428
|
-
' <html lang="en">',
|
|
429
|
-
' <head>',
|
|
430
|
-
' <meta charSet="utf-8" />',
|
|
431
|
-
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
432
|
-
' <title>Home</title>',
|
|
433
|
-
' </head>',
|
|
434
|
-
' <body>',
|
|
435
|
-
' <h1>Home</h1>',
|
|
436
|
-
' <p>Update app/routes.ts and app/actions/controller to keep building your app.</p>',
|
|
437
|
-
' </body>',
|
|
438
|
-
' </html>',
|
|
439
|
-
' )',
|
|
440
|
-
'}',
|
|
441
|
-
'',
|
|
442
|
-
].join('\n')
|
|
443
|
-
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import * as path from 'node:path'
|
|
2
|
+
import * as process from 'node:process'
|
|
3
|
+
|
|
4
|
+
import { checkControllerConventions } from './controllers.ts'
|
|
5
|
+
import { checkEnvironment, getEnvironmentFixPlans } from './environment.ts'
|
|
6
|
+
import { applyDoctorFixPlans } from './fixes.ts'
|
|
7
|
+
import { checkProject, getProjectFixPlans } from './project.ts'
|
|
8
|
+
import {
|
|
9
|
+
createDoctorSuite,
|
|
10
|
+
createSkippedDoctorSuite,
|
|
11
|
+
hasWarningFindings,
|
|
12
|
+
type DoctorAppliedFix,
|
|
13
|
+
type DoctorReport,
|
|
14
|
+
type DoctorSuiteName,
|
|
15
|
+
type DoctorSuiteResult,
|
|
16
|
+
} from './types.ts'
|
|
17
|
+
import {
|
|
18
|
+
createCommandReporter,
|
|
19
|
+
createStepProgressReporter,
|
|
20
|
+
type CommandReporter,
|
|
21
|
+
type StepProgressReporter,
|
|
22
|
+
} from '../reporter.ts'
|
|
23
|
+
|
|
24
|
+
const DOCTOR_SUITE_LABELS = {
|
|
25
|
+
actions: {
|
|
26
|
+
complete: 'actions',
|
|
27
|
+
running: 'Checking actions',
|
|
28
|
+
},
|
|
29
|
+
environment: {
|
|
30
|
+
complete: 'environment',
|
|
31
|
+
running: 'Checking environment',
|
|
32
|
+
},
|
|
33
|
+
project: {
|
|
34
|
+
complete: 'project',
|
|
35
|
+
running: 'Checking project',
|
|
36
|
+
},
|
|
37
|
+
} satisfies Record<DoctorSuiteName, string | { complete: string; running: string }>
|
|
38
|
+
|
|
39
|
+
export interface RunRemixDoctorOptions {
|
|
40
|
+
cwd: string
|
|
41
|
+
fix: boolean
|
|
42
|
+
json: boolean
|
|
43
|
+
remixVersion?: string
|
|
44
|
+
strict: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function runRemixDoctor(options: RunRemixDoctorOptions): Promise<number> {
|
|
48
|
+
let reporter: CommandReporter | null = null
|
|
49
|
+
let progress: StepProgressReporter<DoctorSuiteName> | null = null
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
reporter = options.json
|
|
53
|
+
? null
|
|
54
|
+
: createCommandReporter({
|
|
55
|
+
remixVersion: options.remixVersion,
|
|
56
|
+
stderr: process.stdout,
|
|
57
|
+
stdout: process.stdout,
|
|
58
|
+
})
|
|
59
|
+
progress = reporter == null ? null : createDoctorProgressReporter(reporter)
|
|
60
|
+
if (reporter != null) {
|
|
61
|
+
await reporter.status.commandHeader('doctor')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let report = await collectDoctorReport(progress, options, reporter)
|
|
65
|
+
|
|
66
|
+
if (options.json) {
|
|
67
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
|
|
68
|
+
} else if (reporter != null) {
|
|
69
|
+
writeDoctorReport(reporter, report)
|
|
70
|
+
reporter.finish()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if ((options.fix || options.strict) && hasWarningFindings(report.findings)) {
|
|
74
|
+
return 1
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return 0
|
|
78
|
+
} catch (error) {
|
|
79
|
+
progress?.writeSummaryGap()
|
|
80
|
+
reporter?.finish()
|
|
81
|
+
throw error
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function collectDoctorReport(
|
|
86
|
+
progress: StepProgressReporter<DoctorSuiteName> | null,
|
|
87
|
+
options: RunRemixDoctorOptions,
|
|
88
|
+
reporter: CommandReporter | null,
|
|
89
|
+
): Promise<DoctorReport> {
|
|
90
|
+
let cwd = options.cwd
|
|
91
|
+
let appliedFixes: DoctorAppliedFix[] = []
|
|
92
|
+
let environment = await runDoctorSuite(progress, 'environment', async () => {
|
|
93
|
+
let result = await checkEnvironment(cwd)
|
|
94
|
+
let suiteAppliedFixes: DoctorAppliedFix[] = []
|
|
95
|
+
let finalResult = result
|
|
96
|
+
|
|
97
|
+
if (options.fix && result.projectRoot != null) {
|
|
98
|
+
let fixPlans = getEnvironmentFixPlans(result, options.remixVersion)
|
|
99
|
+
if (fixPlans.length > 0) {
|
|
100
|
+
suiteAppliedFixes = await applyDoctorFixPlans(result.projectRoot, fixPlans)
|
|
101
|
+
finalResult = await checkEnvironment(result.projectRoot)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let suite = finalResult.suite
|
|
106
|
+
if (suiteAppliedFixes.length > 0) {
|
|
107
|
+
suite = {
|
|
108
|
+
...suite,
|
|
109
|
+
appliedFixes: suiteAppliedFixes,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
appliedFixes: suiteAppliedFixes,
|
|
115
|
+
projectRoot: finalResult.projectRoot ?? result.projectRoot,
|
|
116
|
+
suite,
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
let findings = [...environment.suite.findings]
|
|
120
|
+
appliedFixes.push(...(environment.appliedFixes ?? []))
|
|
121
|
+
let suites: DoctorSuiteResult[] = [environment.suite]
|
|
122
|
+
let routesFile =
|
|
123
|
+
environment.projectRoot == null
|
|
124
|
+
? undefined
|
|
125
|
+
: path.join(environment.projectRoot, 'app', 'routes.ts')
|
|
126
|
+
writeDoctorSuiteDetails(reporter, environment.suite)
|
|
127
|
+
progress?.writeSummaryGap()
|
|
128
|
+
|
|
129
|
+
if (hasWarningFindings(environment.suite.findings)) {
|
|
130
|
+
let projectSuite = createSkippedDoctorSuite('project', 'Blocked by environment warnings.')
|
|
131
|
+
let actionsSuite = createSkippedDoctorSuite('actions', 'Blocked by environment warnings.')
|
|
132
|
+
suites.push(projectSuite, actionsSuite)
|
|
133
|
+
progress?.skip(projectSuite.name, projectSuite.reason)
|
|
134
|
+
writeDoctorSuiteDetails(reporter, projectSuite)
|
|
135
|
+
progress?.writeSummaryGap()
|
|
136
|
+
progress?.skip(actionsSuite.name, actionsSuite.reason)
|
|
137
|
+
writeDoctorSuiteDetails(reporter, actionsSuite)
|
|
138
|
+
progress?.writeSummaryGap()
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
appRoot: environment.projectRoot,
|
|
142
|
+
appliedFixes,
|
|
143
|
+
findings,
|
|
144
|
+
remainingFindings: findings,
|
|
145
|
+
routesFile,
|
|
146
|
+
suites,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let project = await runDoctorSuite(progress, 'project', async () => {
|
|
151
|
+
let result = await checkProject(environment.projectRoot!)
|
|
152
|
+
let suiteAppliedFixes: DoctorAppliedFix[] = []
|
|
153
|
+
let finalResult = result
|
|
154
|
+
|
|
155
|
+
if (options.fix) {
|
|
156
|
+
let fixPlans = await getProjectFixPlans(environment.projectRoot!)
|
|
157
|
+
if (fixPlans.length > 0) {
|
|
158
|
+
suiteAppliedFixes = await applyDoctorFixPlans(environment.projectRoot!, fixPlans)
|
|
159
|
+
finalResult = await checkProject(environment.projectRoot!)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let suite = finalResult.suite
|
|
164
|
+
if (suiteAppliedFixes.length > 0) {
|
|
165
|
+
suite = {
|
|
166
|
+
...suite,
|
|
167
|
+
appliedFixes: suiteAppliedFixes,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
appliedFixes: suiteAppliedFixes,
|
|
173
|
+
routeManifest: finalResult.routeManifest,
|
|
174
|
+
routesFile: finalResult.routesFile,
|
|
175
|
+
suite,
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
findings.push(...project.suite.findings)
|
|
179
|
+
appliedFixes.push(...(project.appliedFixes ?? []))
|
|
180
|
+
routesFile = project.routesFile
|
|
181
|
+
suites.push(project.suite)
|
|
182
|
+
writeDoctorSuiteDetails(reporter, project.suite)
|
|
183
|
+
progress?.writeSummaryGap()
|
|
184
|
+
|
|
185
|
+
if (hasWarningFindings(project.suite.findings)) {
|
|
186
|
+
let actionsSuite = createSkippedDoctorSuite('actions', 'Blocked by project warnings.')
|
|
187
|
+
suites.push(actionsSuite)
|
|
188
|
+
progress?.skip(actionsSuite.name, actionsSuite.reason)
|
|
189
|
+
writeDoctorSuiteDetails(reporter, actionsSuite)
|
|
190
|
+
progress?.writeSummaryGap()
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
appRoot: environment.projectRoot,
|
|
194
|
+
appliedFixes,
|
|
195
|
+
findings,
|
|
196
|
+
remainingFindings: findings,
|
|
197
|
+
routesFile,
|
|
198
|
+
suites,
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let actions = await runDoctorSuite(progress, 'actions', async () => {
|
|
203
|
+
let controllerResult = await checkControllerConventions(
|
|
204
|
+
project.routeManifest!.appRoot,
|
|
205
|
+
project.routeManifest!.tree,
|
|
206
|
+
)
|
|
207
|
+
return { suite: controllerResult.suite }
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
findings.push(...actions.suite.findings)
|
|
211
|
+
suites.push(actions.suite)
|
|
212
|
+
writeDoctorSuiteDetails(reporter, actions.suite)
|
|
213
|
+
progress?.writeSummaryGap()
|
|
214
|
+
|
|
215
|
+
let report: DoctorReport = {
|
|
216
|
+
appRoot: environment.projectRoot,
|
|
217
|
+
findings,
|
|
218
|
+
routesFile,
|
|
219
|
+
suites,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (options.fix) {
|
|
223
|
+
report.appliedFixes = appliedFixes
|
|
224
|
+
report.remainingFindings = findings
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return report
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function formatAppliedFix(appliedFix: DoctorAppliedFix): string {
|
|
231
|
+
if (appliedFix.kind === 'update-file') {
|
|
232
|
+
return `Updated ${appliedFix.path}`
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return `Created ${appliedFix.path}`
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function runDoctorSuite<
|
|
239
|
+
result extends { appliedFixes?: DoctorAppliedFix[]; suite: DoctorSuiteResult },
|
|
240
|
+
>(
|
|
241
|
+
progress: StepProgressReporter<DoctorSuiteName> | null,
|
|
242
|
+
label: DoctorSuiteName,
|
|
243
|
+
callback: () => Promise<result>,
|
|
244
|
+
): Promise<result> {
|
|
245
|
+
progress?.start(label)
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
let result = await callback()
|
|
249
|
+
|
|
250
|
+
if (result.suite.status === 'ok') {
|
|
251
|
+
progress?.succeed(label)
|
|
252
|
+
} else if (result.suite.status === 'issues') {
|
|
253
|
+
progress?.fail(label)
|
|
254
|
+
} else {
|
|
255
|
+
progress?.skip(label, result.suite.reason)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return result
|
|
259
|
+
} catch (error) {
|
|
260
|
+
progress?.fail(label)
|
|
261
|
+
throw error
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function createDoctorProgressReporter(
|
|
266
|
+
reporter: CommandReporter,
|
|
267
|
+
): StepProgressReporter<DoctorSuiteName> {
|
|
268
|
+
return createStepProgressReporter(reporter.status, DOCTOR_SUITE_LABELS)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function writeDoctorReport(reporter: CommandReporter, report: DoctorReport): void {
|
|
272
|
+
let warningCount = report.findings.filter((finding) => finding.severity === 'warn').length
|
|
273
|
+
let adviceCount = report.findings.length - warningCount
|
|
274
|
+
|
|
275
|
+
if ((report.appliedFixes?.length ?? 0) > 0) {
|
|
276
|
+
let fixCount = report.appliedFixes!.length
|
|
277
|
+
reporter.out.line(`Applied ${fixCount} ${fixCount === 1 ? 'fix' : 'fixes'}.`)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (report.findings.length === 0) {
|
|
281
|
+
reporter.out.line('Doctor found no issues.')
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
reporter.out.line(`Summary: ${warningCount} warnings, ${adviceCount} advice.`)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function writeDoctorSuiteDetails(reporter: CommandReporter | null, suite: DoctorSuiteResult): void {
|
|
288
|
+
if (reporter == null) {
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (suite.findings.length === 0 && (suite.appliedFixes?.length ?? 0) === 0) {
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
reporter.out.withIndent(() => {
|
|
297
|
+
for (let finding of suite.findings) {
|
|
298
|
+
reporter.out.bullet(
|
|
299
|
+
reporter.out.label(finding.severity.toUpperCase(), finding.message, {
|
|
300
|
+
tone: finding.severity === 'warn' ? 'warn' : undefined,
|
|
301
|
+
}),
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if ((suite.appliedFixes?.length ?? 0) > 0) {
|
|
306
|
+
reporter.out.section('Applied fixes:', () => {
|
|
307
|
+
reporter.out.bullets((suite.appliedFixes ?? []).map(formatAppliedFix))
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
})
|
|
311
|
+
}
|
package/src/lib/doctor/types.ts
CHANGED
|
@@ -18,10 +18,7 @@ export type DoctorFindingCode =
|
|
|
18
18
|
| 'route-map-invalid-json'
|
|
19
19
|
| 'route-map-loader-signal'
|
|
20
20
|
| 'missing-owner'
|
|
21
|
-
| '
|
|
22
|
-
| 'incomplete-controller'
|
|
23
|
-
| 'orphan-controller'
|
|
24
|
-
| 'orphan-route-directory'
|
|
21
|
+
| 'missing-route-directory'
|
|
25
22
|
|
|
26
23
|
export type DoctorFixKind = 'create-directory' | 'create-file' | 'update-file'
|
|
27
24
|
|
|
@@ -70,6 +67,10 @@ export interface DoctorReport {
|
|
|
70
67
|
suites: DoctorSuiteResult[]
|
|
71
68
|
}
|
|
72
69
|
|
|
70
|
+
export function hasWarningFindings(findings: DoctorFinding[]): boolean {
|
|
71
|
+
return findings.some((finding) => finding.severity === 'warn')
|
|
72
|
+
}
|
|
73
|
+
|
|
73
74
|
export function createDoctorSuite(
|
|
74
75
|
name: DoctorSuiteName,
|
|
75
76
|
findings: DoctorFinding[],
|
|
@@ -77,7 +78,7 @@ export function createDoctorSuite(
|
|
|
77
78
|
return {
|
|
78
79
|
findings,
|
|
79
80
|
name,
|
|
80
|
-
status: findings
|
|
81
|
+
status: hasWarningFindings(findings) ? 'issues' : 'ok',
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
|