@remix-run/cli 0.3.4 → 0.5.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 (119) hide show
  1. package/README.md +105 -1
  2. package/dist/index.js +1 -1
  3. package/dist/lib/app-root.d.ts +11 -0
  4. package/dist/lib/app-root.d.ts.map +1 -0
  5. package/dist/lib/app-root.js +38 -0
  6. package/dist/lib/bootstrap-project.js +2 -2
  7. package/dist/lib/cli-context.d.ts +6 -0
  8. package/dist/lib/cli-context.d.ts.map +1 -1
  9. package/dist/lib/cli-context.js +12 -1
  10. package/dist/lib/cli.d.ts +3 -2
  11. package/dist/lib/cli.d.ts.map +1 -1
  12. package/dist/lib/cli.js +42 -14
  13. package/dist/lib/commands/completion.js +4 -4
  14. package/dist/lib/commands/db.d.ts +4 -0
  15. package/dist/lib/commands/db.d.ts.map +1 -0
  16. package/dist/lib/commands/db.js +283 -0
  17. package/dist/lib/commands/doctor.d.ts +5 -0
  18. package/dist/lib/commands/doctor.d.ts.map +1 -1
  19. package/dist/lib/commands/doctor.js +26 -251
  20. package/dist/lib/commands/help.d.ts.map +1 -1
  21. package/dist/lib/commands/help.js +17 -9
  22. package/dist/lib/commands/new.js +6 -6
  23. package/dist/lib/commands/routes.js +6 -6
  24. package/dist/lib/commands/test.d.ts +20 -1
  25. package/dist/lib/commands/test.d.ts.map +1 -1
  26. package/dist/lib/commands/test.js +353 -10
  27. package/dist/lib/commands/version.js +3 -3
  28. package/dist/lib/completion.d.ts +1 -1
  29. package/dist/lib/completion.d.ts.map +1 -1
  30. package/dist/lib/completion.js +229 -6
  31. package/dist/lib/controller-files.d.ts +0 -8
  32. package/dist/lib/controller-files.d.ts.map +1 -1
  33. package/dist/lib/controller-files.js +0 -23
  34. package/dist/lib/controller-ownership.d.ts +4 -12
  35. package/dist/lib/controller-ownership.d.ts.map +1 -1
  36. package/dist/lib/controller-ownership.js +31 -127
  37. package/dist/lib/database-command.d.ts +20 -0
  38. package/dist/lib/database-command.d.ts.map +1 -0
  39. package/dist/lib/database-command.js +7 -0
  40. package/dist/lib/doctor/controller-findings.d.ts +2 -2
  41. package/dist/lib/doctor/controller-findings.d.ts.map +1 -1
  42. package/dist/lib/doctor/controller-findings.js +23 -39
  43. package/dist/lib/doctor/controllers.d.ts +1 -2
  44. package/dist/lib/doctor/controllers.d.ts.map +1 -1
  45. package/dist/lib/doctor/controllers.js +4 -7
  46. package/dist/lib/doctor/environment.js +2 -2
  47. package/dist/lib/doctor/fixes.js +1 -1
  48. package/dist/lib/doctor/project.d.ts.map +1 -1
  49. package/dist/lib/doctor/project.js +3 -212
  50. package/dist/lib/doctor/run.d.ts +9 -0
  51. package/dist/lib/doctor/run.d.ts.map +1 -0
  52. package/dist/lib/doctor/run.js +237 -0
  53. package/dist/lib/doctor/types.d.ts +2 -1
  54. package/dist/lib/doctor/types.d.ts.map +1 -1
  55. package/dist/lib/doctor/types.js +4 -1
  56. package/dist/lib/errors.d.ts +30 -0
  57. package/dist/lib/errors.d.ts.map +1 -1
  58. package/dist/lib/errors.js +55 -0
  59. package/dist/lib/help-text.js +1 -1
  60. package/dist/lib/parse-args.d.ts.map +1 -1
  61. package/dist/lib/parse-args.js +17 -4
  62. package/dist/lib/remix-config.d.ts +75 -0
  63. package/dist/lib/remix-config.d.ts.map +1 -0
  64. package/dist/lib/remix-config.js +411 -0
  65. package/dist/lib/reporter.js +1 -1
  66. package/dist/lib/route-map.js +13 -33
  67. package/package.json +13 -7
  68. package/schema/remix.json +242 -0
  69. package/src/lib/app-root.ts +46 -0
  70. package/src/lib/cli-context.ts +17 -0
  71. package/src/lib/cli.ts +41 -6
  72. package/src/lib/commands/db.ts +377 -0
  73. package/src/lib/commands/doctor.ts +27 -329
  74. package/src/lib/commands/help.ts +10 -1
  75. package/src/lib/commands/test.ts +453 -12
  76. package/src/lib/completion.ts +321 -6
  77. package/src/lib/controller-files.ts +0 -38
  78. package/src/lib/controller-ownership.ts +50 -203
  79. package/src/lib/database-command.ts +31 -0
  80. package/src/lib/doctor/controller-findings.ts +30 -47
  81. package/src/lib/doctor/controllers.ts +2 -6
  82. package/src/lib/doctor/project.ts +0 -236
  83. package/src/lib/doctor/run.ts +311 -0
  84. package/src/lib/doctor/types.ts +6 -5
  85. package/src/lib/errors.ts +65 -0
  86. package/src/lib/parse-args.ts +19 -3
  87. package/src/lib/remix-config.ts +639 -0
  88. package/src/lib/route-map.ts +11 -48
  89. package/template/.agents/skills/remix/SKILL.md +38 -27
  90. package/template/.agents/skills/remix/references/assets-and-browser-modules.md +54 -11
  91. package/template/.agents/skills/remix/references/auth-and-sessions.md +5 -5
  92. package/template/.agents/skills/remix/references/data-and-validation.md +15 -15
  93. package/template/.agents/skills/remix/references/hydration-frames-navigation.md +63 -9
  94. package/template/.agents/skills/remix/references/middleware-and-server.md +68 -6
  95. package/template/.agents/skills/remix/references/mixins-styling-events.md +2 -0
  96. package/template/.agents/skills/remix/references/routing-and-controllers.md +8 -8
  97. package/template/.agents/skills/remix/references/testing-patterns.md +19 -18
  98. package/template/AGENTS.md +6 -3
  99. package/template/README.md +6 -3
  100. package/template/app/actions/controller.tsx +1 -1
  101. package/template/app/{ui → actions}/document.tsx +7 -5
  102. package/template/app/{ui/scaffold-home-page.tsx → actions/home-page.tsx} +1 -2
  103. package/template/app/actions/public/entry.ts +48 -0
  104. package/template/app/{assets → actions/public}/prompt-button.tsx +29 -25
  105. package/template/app/assets.ts +15 -2
  106. package/template/app/middleware/render.tsx +8 -2
  107. package/template/hmr.ts +46 -0
  108. package/template/package.json +1 -0
  109. package/template/server.ts +8 -1
  110. package/template/tsconfig.json +1 -1
  111. package/dist/lib/doctor/controller-fix-plans.d.ts +0 -4
  112. package/dist/lib/doctor/controller-fix-plans.d.ts.map +0 -1
  113. package/dist/lib/doctor/controller-fix-plans.js +0 -119
  114. package/dist/lib/doctor/controller-placeholders.d.ts +0 -3
  115. package/dist/lib/doctor/controller-placeholders.d.ts.map +0 -1
  116. package/dist/lib/doctor/controller-placeholders.js +0 -52
  117. package/src/lib/doctor/controller-fix-plans.ts +0 -168
  118. package/src/lib/doctor/controller-placeholders.ts +0 -70
  119. package/template/app/assets/entry.ts +0 -17
@@ -1,52 +1,36 @@
1
- import { ROOT_ROUTE_NAME, } from "../controller-ownership.js";
2
- export function getControllerFindings(ownership, fixPlans) {
1
+ import { ROOT_ROUTE_NAME, } from '../controller-ownership.js';
2
+ export function getControllerFindings(ownership) {
3
+ let missingRouteNames = new Set(ownership.routeDirectories
4
+ .filter((directory) => !directory.exists)
5
+ .map((directory) => directory.routeName));
3
6
  return [
4
- ...getSubtreeFindings(ownership.subtrees, fixPlans),
5
- ...ownership.orphanControllerPaths.map((actualPath) => ({
6
- actualPath,
7
- code: 'orphan-controller',
8
- message: `Action controller ${actualPath} does not match any route map.`,
9
- severity: 'warn',
10
- suite: 'actions',
11
- })),
12
- ...ownership.orphanRouteDirectoryPaths.map((actualPath) => ({
13
- actualPath,
14
- code: 'orphan-route-directory',
15
- message: `Directory ${actualPath} does not match any route-map key path.`,
16
- severity: 'warn',
17
- suite: 'actions',
18
- })),
7
+ ...getSubtreeFindings(ownership.subtrees, missingRouteNames),
8
+ ...getRouteDirectoryFindings(ownership.routeDirectories),
19
9
  ];
20
10
  }
21
- function getSubtreeFindings(subtrees, fixPlans) {
11
+ function getRouteDirectoryFindings(directories) {
12
+ return directories
13
+ .filter((directory) => !directory.exists)
14
+ .map((directory) => ({
15
+ code: 'missing-route-directory',
16
+ expectedPath: directory.directoryPath,
17
+ message: `Route map "${directory.routeName}" is missing action directory ${directory.directoryPath}.`,
18
+ routeName: directory.routeName,
19
+ severity: 'warn',
20
+ suite: 'actions',
21
+ }));
22
+ }
23
+ function getSubtreeFindings(subtrees, missingRouteNames) {
22
24
  let findings = [];
23
- let fixableFindingKeys = new Set(fixPlans.map((fixPlan) => `${fixPlan.code}:${fixPlan.routeName ?? ''}`));
24
25
  for (let subtree of subtrees) {
25
- let routeMapName = formatRouteMapName(subtree.routeName);
26
- if (subtree.actualEntryPaths.length > 1) {
27
- findings.push({
28
- actualPath: subtree.actualEntryPaths[0],
29
- code: 'duplicate-owner-file',
30
- expectedPath: subtree.entryDisplayPath,
31
- message: `${routeMapName} has multiple action controller files: ${subtree.actualEntryPaths.join(', ')}. Keep only one controller owner file.`,
32
- routeName: subtree.routeName,
33
- severity: 'warn',
34
- suite: 'actions',
35
- });
26
+ if (missingRouteNames.has(subtree.routeName)) {
36
27
  continue;
37
28
  }
38
29
  if (subtree.actualEntryPath == null) {
39
- let code = subtree.claimedFilePaths.length > 0
40
- ? 'incomplete-controller'
41
- : 'missing-owner';
42
30
  findings.push({
43
- actualPath: subtree.claimedFilePaths.length > 0 ? subtree.subtreePath : undefined,
44
- code,
31
+ code: 'missing-owner',
45
32
  expectedPath: subtree.entryDisplayPath,
46
- fixable: fixableFindingKeys.has(`${code}:${subtree.routeName}`),
47
- message: subtree.claimedFilePaths.length > 0
48
- ? `${routeMapName} has files under ${subtree.subtreePath}, but is missing action controller ${subtree.entryDisplayPath}.`
49
- : `${routeMapName} is missing action controller ${subtree.entryDisplayPath}.`,
33
+ message: `${formatRouteMapName(subtree.routeName)} is missing action controller ${subtree.entryDisplayPath}.`,
50
34
  routeName: subtree.routeName,
51
35
  severity: 'warn',
52
36
  suite: 'actions',
@@ -1,7 +1,6 @@
1
1
  import { type OwnershipRouteNode } from '../controller-ownership.ts';
2
- import { type DoctorFixPlan, type DoctorSuiteResult } from './types.ts';
2
+ import { type DoctorSuiteResult } from './types.ts';
3
3
  export interface ControllerDoctorResult {
4
- fixPlans: DoctorFixPlan[];
5
4
  suite: DoctorSuiteResult;
6
5
  }
7
6
  export declare function checkControllerConventions(appRoot: string, tree: OwnershipRouteNode[]): Promise<ControllerDoctorResult>;
@@ -1 +1 @@
1
- {"version":3,"file":"controllers.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/controllers.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,KAAK,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAGhG,OAAO,EAAqB,KAAK,aAAa,EAAE,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAE1F,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,aAAa,EAAE,CAAA;IACzB,KAAK,EAAE,iBAAiB,CAAA;CACzB;AAED,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,kBAAkB,EAAE,GACzB,OAAO,CAAC,sBAAsB,CAAC,CASjC"}
1
+ {"version":3,"file":"controllers.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/controllers.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,KAAK,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAEhG,OAAO,EAAqB,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAEtE,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,iBAAiB,CAAA;CACzB;AAED,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,kBAAkB,EAAE,GACzB,OAAO,CAAC,sBAAsB,CAAC,CAOjC"}
@@ -1,13 +1,10 @@
1
- import { inspectControllerOwnership } from "../controller-ownership.js";
2
- import { getControllerFindings } from "./controller-findings.js";
3
- import { getControllerFixPlans } from "./controller-fix-plans.js";
4
- import { createDoctorSuite } from "./types.js";
1
+ import { inspectControllerOwnership } from '../controller-ownership.js';
2
+ import { getControllerFindings } from './controller-findings.js';
3
+ import { createDoctorSuite } from './types.js';
5
4
  export async function checkControllerConventions(appRoot, tree) {
6
5
  let ownership = await inspectControllerOwnership(appRoot, tree);
7
- let fixPlans = await getControllerFixPlans(appRoot, tree, ownership);
8
- let findings = getControllerFindings(ownership, fixPlans);
6
+ let findings = getControllerFindings(ownership);
9
7
  return {
10
- fixPlans,
11
8
  suite: createDoctorSuite('actions', findings),
12
9
  };
13
10
  }
@@ -3,8 +3,8 @@ import * as path from 'node:path';
3
3
  import * as process from 'node:process';
4
4
  import { createRequire } from 'node:module';
5
5
  import * as semver from 'semver';
6
- import { MINIMUM_SUPPORTED_NODE_VERSION } from "../bootstrap-project.js";
7
- import { createDoctorSuite, } from "./types.js";
6
+ import { MINIMUM_SUPPORTED_NODE_VERSION } from '../bootstrap-project.js';
7
+ import { createDoctorSuite, } from './types.js';
8
8
  export async function checkEnvironment(cwd = process.cwd()) {
9
9
  let projectRoot = await findNearestPackageRoot(cwd);
10
10
  if (projectRoot == null) {
@@ -1,6 +1,6 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
- import { resolveContainedPath } from "../contained-path.js";
3
+ import { resolveContainedPath } from '../contained-path.js';
4
4
  export async function applyDoctorFixPlans(appRoot, fixPlans) {
5
5
  let appliedFixes = [];
6
6
  for (let fixPlan of dedupeDoctorFixPlans(fixPlans)) {
@@ -1 +1 @@
1
- {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/project.ts"],"names":[],"mappings":"AAUA,OAAO,EAAgC,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AACxF,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACvB,MAAM,YAAY,CAAA;AAEnB,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,mBAAmB,CAAA;IACnC,KAAK,EAAE,iBAAiB,CAAA;CACzB;AAED,wBAAsB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAmCpF;AAED,wBAAsB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAoDtF"}
1
+ {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/project.ts"],"names":[],"mappings":"AAIA,OAAO,EAAgC,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AACxF,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACvB,MAAM,YAAY,CAAA;AAEnB,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,mBAAmB,CAAA;IACnC,KAAK,EAAE,iBAAiB,CAAA;CACzB;AAED,wBAAsB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAmCpF;AAED,wBAAsB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CA6BtF"}
@@ -1,9 +1,8 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
- import { getOwnerFileExtension, isActionFileName, isControllerEntryFileName, } from "../controller-files.js";
4
- import { CliError } from "../errors.js";
5
- import { loadRouteManifestFromAppRoot } from "../route-map.js";
6
- import { createDoctorSuite, } from "./types.js";
3
+ import { CliError } from '../errors.js';
4
+ import { loadRouteManifestFromAppRoot } from '../route-map.js';
5
+ import { createDoctorSuite, } from './types.js';
7
6
  export async function checkProject(projectRoot) {
8
7
  let routesFile = path.join(projectRoot, 'app', 'routes.ts');
9
8
  if (!(await pathExists(routesFile))) {
@@ -39,7 +38,6 @@ export async function checkProject(projectRoot) {
39
38
  }
40
39
  export async function getProjectFixPlans(projectRoot) {
41
40
  let routesFile = path.join(projectRoot, 'app', 'routes.ts');
42
- let homeActionPath = normalizeRelativePath(path.join('app', 'actions', `controller${await inferHomeOwnerExtension(projectRoot)}`));
43
41
  if (await pathExists(routesFile)) {
44
42
  let routesSource = await fs.readFile(routesFile, 'utf8');
45
43
  if (!hasOnlyWhitespaceAndComments(routesSource)) {
@@ -53,18 +51,6 @@ export async function getProjectFixPlans(projectRoot) {
53
51
  path: 'app/routes.ts',
54
52
  suite: 'project',
55
53
  },
56
- ...((await pathExists(path.join(projectRoot, homeActionPath)))
57
- ? []
58
- : [
59
- {
60
- code: 'missing-owner',
61
- contents: renderDefaultHomeAction(homeActionPath),
62
- kind: 'create-file',
63
- path: homeActionPath,
64
- routeName: 'home',
65
- suite: 'project',
66
- },
67
- ]),
68
54
  ];
69
55
  }
70
56
  return [
@@ -75,14 +61,6 @@ export async function getProjectFixPlans(projectRoot) {
75
61
  path: 'app/routes.ts',
76
62
  suite: 'project',
77
63
  },
78
- {
79
- code: 'missing-owner',
80
- contents: renderDefaultHomeAction(homeActionPath),
81
- kind: 'create-file',
82
- path: homeActionPath,
83
- routeName: 'home',
84
- suite: 'project',
85
- },
86
64
  ];
87
65
  }
88
66
  function toProjectFinding(error) {
@@ -180,63 +158,6 @@ function hasOnlyWhitespaceAndComments(source) {
180
158
  }
181
159
  return true;
182
160
  }
183
- async function inferHomeOwnerExtension(projectRoot) {
184
- let actionsDir = path.join(projectRoot, 'app', 'actions');
185
- let extensions = await collectOwnerExtensions(actionsDir);
186
- if (extensions.length > 0) {
187
- return getMostCommonExtension(extensions);
188
- }
189
- if (await pathExists(path.join(projectRoot, 'tsconfig.json'))) {
190
- return '.tsx';
191
- }
192
- return '.js';
193
- }
194
- async function collectOwnerExtensions(directory) {
195
- try {
196
- let entries = await fs.readdir(directory, { withFileTypes: true });
197
- let extensions = [];
198
- for (let entry of entries) {
199
- let absolutePath = path.join(directory, entry.name);
200
- if (entry.isDirectory()) {
201
- extensions.push(...(await collectOwnerExtensions(absolutePath)));
202
- continue;
203
- }
204
- let fileName = path.basename(absolutePath);
205
- if (!isActionFileName(fileName) && !isControllerEntryFileName(fileName)) {
206
- continue;
207
- }
208
- let extension = getOwnerFileExtension(fileName);
209
- if (extension != null) {
210
- extensions.push(extension);
211
- }
212
- }
213
- return extensions;
214
- }
215
- catch (error) {
216
- let nodeError = error;
217
- if (nodeError.code === 'ENOENT') {
218
- return [];
219
- }
220
- throw error;
221
- }
222
- }
223
- function getMostCommonExtension(extensions) {
224
- let priority = ['.tsx', '.ts', '.jsx', '.js'];
225
- let counts = new Map();
226
- for (let extension of extensions) {
227
- counts.set(extension, (counts.get(extension) ?? 0) + 1);
228
- }
229
- let bestExtension = priority[0];
230
- let bestCount = -1;
231
- for (let extension of priority) {
232
- let count = counts.get(extension) ?? 0;
233
- if (count > bestCount) {
234
- bestCount = count;
235
- bestExtension = extension;
236
- }
237
- }
238
- return bestExtension;
239
- }
240
161
  function renderDefaultRoutesFile() {
241
162
  return [
242
163
  `import { route } from 'remix/routes'`,
@@ -247,133 +168,3 @@ function renderDefaultRoutesFile() {
247
168
  '',
248
169
  ].join('\n');
249
170
  }
250
- function normalizeRelativePath(filePath) {
251
- return filePath.split(path.sep).join('/');
252
- }
253
- function renderDefaultHomeAction(entryPath) {
254
- let extension = getOwnerFileExtension(entryPath);
255
- if (extension === '.js') {
256
- return [
257
- `import { html } from 'remix/html-template'`,
258
- `import { createHtmlResponse } from 'remix/response/html'`,
259
- '',
260
- 'export default {',
261
- ' actions: {',
262
- ' home() {',
263
- ' let page = html`',
264
- ' <html lang="en">',
265
- ' <head>',
266
- ' <meta charset="utf-8" />',
267
- ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
268
- ' <title>Home</title>',
269
- ' </head>',
270
- ' <body>',
271
- ' <h1>Home</h1>',
272
- ' <p>Update app/routes.ts and app/actions/controller to keep building your app.</p>',
273
- ' </body>',
274
- ' </html>',
275
- ' `',
276
- ' return createHtmlResponse(page)',
277
- ' },',
278
- ' },',
279
- '}',
280
- '',
281
- ].join('\n');
282
- }
283
- if (extension === '.ts') {
284
- return [
285
- `import { createController } from 'remix/router'`,
286
- `import { html } from 'remix/html-template'`,
287
- `import { createHtmlResponse } from 'remix/response/html'`,
288
- '',
289
- `import { routes } from '../routes.ts'`,
290
- '',
291
- `export default createController(routes, {`,
292
- ' actions: {',
293
- ' home() {',
294
- ' let page = html`',
295
- ' <html lang="en">',
296
- ' <head>',
297
- ' <meta charset="utf-8" />',
298
- ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
299
- ' <title>Home</title>',
300
- ' </head>',
301
- ' <body>',
302
- ' <h1>Home</h1>',
303
- ' <p>Update app/routes.ts and app/actions/controller to keep building your app.</p>',
304
- ' </body>',
305
- ' </html>',
306
- ' `',
307
- ' return createHtmlResponse(page)',
308
- ' },',
309
- ' },',
310
- `})`,
311
- '',
312
- ].join('\n');
313
- }
314
- if (extension === '.jsx') {
315
- return [
316
- `import { renderToStream } from 'remix/ui/server'`,
317
- `import { createHtmlResponse } from 'remix/response/html'`,
318
- '',
319
- 'export default {',
320
- ' actions: {',
321
- ' home() {',
322
- ' let page = <HomePage />',
323
- ' return createHtmlResponse(renderToStream(page))',
324
- ' },',
325
- ' },',
326
- '}',
327
- '',
328
- 'function HomePage() {',
329
- ' return () => (',
330
- ' <html lang="en">',
331
- ' <head>',
332
- ' <meta charSet="utf-8" />',
333
- ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
334
- ' <title>Home</title>',
335
- ' </head>',
336
- ' <body>',
337
- ' <h1>Home</h1>',
338
- ' <p>Update app/routes.ts and app/actions/controller to keep building your app.</p>',
339
- ' </body>',
340
- ' </html>',
341
- ' )',
342
- '}',
343
- '',
344
- ].join('\n');
345
- }
346
- return [
347
- `import { createController } from 'remix/router'`,
348
- `import { renderToStream } from 'remix/ui/server'`,
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 = <HomePage />',
357
- ' return createHtmlResponse(renderToStream(page))',
358
- ' },',
359
- ' },',
360
- `})`,
361
- '',
362
- 'function HomePage() {',
363
- ' return () => (',
364
- ' <html lang="en">',
365
- ' <head>',
366
- ' <meta charSet="utf-8" />',
367
- ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
368
- ' <title>Home</title>',
369
- ' </head>',
370
- ' <body>',
371
- ' <h1>Home</h1>',
372
- ' <p>Update app/routes.ts and app/actions/controller to keep building your app.</p>',
373
- ' </body>',
374
- ' </html>',
375
- ' )',
376
- '}',
377
- '',
378
- ].join('\n');
379
- }
@@ -0,0 +1,9 @@
1
+ export interface RunRemixDoctorOptions {
2
+ cwd: string;
3
+ fix: boolean;
4
+ json: boolean;
5
+ remixVersion?: string;
6
+ strict: boolean;
7
+ }
8
+ export declare function runRemixDoctor(options: RunRemixDoctorOptions): Promise<number>;
9
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/run.ts"],"names":[],"mappings":"AAsCA,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,OAAO,CAAA;IACZ,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,MAAM,EAAE,OAAO,CAAA;CAChB;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAoCpF"}
@@ -0,0 +1,237 @@
1
+ import * as path from 'node:path';
2
+ import * as process from 'node:process';
3
+ import { checkControllerConventions } from './controllers.js';
4
+ import { checkEnvironment, getEnvironmentFixPlans } from './environment.js';
5
+ import { applyDoctorFixPlans } from './fixes.js';
6
+ import { checkProject, getProjectFixPlans } from './project.js';
7
+ import { createDoctorSuite, createSkippedDoctorSuite, hasWarningFindings, } from './types.js';
8
+ import { createCommandReporter, createStepProgressReporter, } from '../reporter.js';
9
+ const DOCTOR_SUITE_LABELS = {
10
+ actions: {
11
+ complete: 'actions',
12
+ running: 'Checking actions',
13
+ },
14
+ environment: {
15
+ complete: 'environment',
16
+ running: 'Checking environment',
17
+ },
18
+ project: {
19
+ complete: 'project',
20
+ running: 'Checking project',
21
+ },
22
+ };
23
+ export async function runRemixDoctor(options) {
24
+ let reporter = null;
25
+ let progress = null;
26
+ try {
27
+ reporter = options.json
28
+ ? null
29
+ : createCommandReporter({
30
+ remixVersion: options.remixVersion,
31
+ stderr: process.stdout,
32
+ stdout: process.stdout,
33
+ });
34
+ progress = reporter == null ? null : createDoctorProgressReporter(reporter);
35
+ if (reporter != null) {
36
+ await reporter.status.commandHeader('doctor');
37
+ }
38
+ let report = await collectDoctorReport(progress, options, reporter);
39
+ if (options.json) {
40
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
41
+ }
42
+ else if (reporter != null) {
43
+ writeDoctorReport(reporter, report);
44
+ reporter.finish();
45
+ }
46
+ if ((options.fix || options.strict) && hasWarningFindings(report.findings)) {
47
+ return 1;
48
+ }
49
+ return 0;
50
+ }
51
+ catch (error) {
52
+ progress?.writeSummaryGap();
53
+ reporter?.finish();
54
+ throw error;
55
+ }
56
+ }
57
+ async function collectDoctorReport(progress, options, reporter) {
58
+ let cwd = options.cwd;
59
+ let appliedFixes = [];
60
+ let environment = await runDoctorSuite(progress, 'environment', async () => {
61
+ let result = await checkEnvironment(cwd);
62
+ let suiteAppliedFixes = [];
63
+ let finalResult = result;
64
+ if (options.fix && result.projectRoot != null) {
65
+ let fixPlans = getEnvironmentFixPlans(result, options.remixVersion);
66
+ if (fixPlans.length > 0) {
67
+ suiteAppliedFixes = await applyDoctorFixPlans(result.projectRoot, fixPlans);
68
+ finalResult = await checkEnvironment(result.projectRoot);
69
+ }
70
+ }
71
+ let suite = finalResult.suite;
72
+ if (suiteAppliedFixes.length > 0) {
73
+ suite = {
74
+ ...suite,
75
+ appliedFixes: suiteAppliedFixes,
76
+ };
77
+ }
78
+ return {
79
+ appliedFixes: suiteAppliedFixes,
80
+ projectRoot: finalResult.projectRoot ?? result.projectRoot,
81
+ suite,
82
+ };
83
+ });
84
+ let findings = [...environment.suite.findings];
85
+ appliedFixes.push(...(environment.appliedFixes ?? []));
86
+ let suites = [environment.suite];
87
+ let routesFile = environment.projectRoot == null
88
+ ? undefined
89
+ : path.join(environment.projectRoot, 'app', 'routes.ts');
90
+ writeDoctorSuiteDetails(reporter, environment.suite);
91
+ progress?.writeSummaryGap();
92
+ if (hasWarningFindings(environment.suite.findings)) {
93
+ let projectSuite = createSkippedDoctorSuite('project', 'Blocked by environment warnings.');
94
+ let actionsSuite = createSkippedDoctorSuite('actions', 'Blocked by environment warnings.');
95
+ suites.push(projectSuite, actionsSuite);
96
+ progress?.skip(projectSuite.name, projectSuite.reason);
97
+ writeDoctorSuiteDetails(reporter, projectSuite);
98
+ progress?.writeSummaryGap();
99
+ progress?.skip(actionsSuite.name, actionsSuite.reason);
100
+ writeDoctorSuiteDetails(reporter, actionsSuite);
101
+ progress?.writeSummaryGap();
102
+ return {
103
+ appRoot: environment.projectRoot,
104
+ appliedFixes,
105
+ findings,
106
+ remainingFindings: findings,
107
+ routesFile,
108
+ suites,
109
+ };
110
+ }
111
+ let project = await runDoctorSuite(progress, 'project', async () => {
112
+ let result = await checkProject(environment.projectRoot);
113
+ let suiteAppliedFixes = [];
114
+ let finalResult = result;
115
+ if (options.fix) {
116
+ let fixPlans = await getProjectFixPlans(environment.projectRoot);
117
+ if (fixPlans.length > 0) {
118
+ suiteAppliedFixes = await applyDoctorFixPlans(environment.projectRoot, fixPlans);
119
+ finalResult = await checkProject(environment.projectRoot);
120
+ }
121
+ }
122
+ let suite = finalResult.suite;
123
+ if (suiteAppliedFixes.length > 0) {
124
+ suite = {
125
+ ...suite,
126
+ appliedFixes: suiteAppliedFixes,
127
+ };
128
+ }
129
+ return {
130
+ appliedFixes: suiteAppliedFixes,
131
+ routeManifest: finalResult.routeManifest,
132
+ routesFile: finalResult.routesFile,
133
+ suite,
134
+ };
135
+ });
136
+ findings.push(...project.suite.findings);
137
+ appliedFixes.push(...(project.appliedFixes ?? []));
138
+ routesFile = project.routesFile;
139
+ suites.push(project.suite);
140
+ writeDoctorSuiteDetails(reporter, project.suite);
141
+ progress?.writeSummaryGap();
142
+ if (hasWarningFindings(project.suite.findings)) {
143
+ let actionsSuite = createSkippedDoctorSuite('actions', 'Blocked by project warnings.');
144
+ suites.push(actionsSuite);
145
+ progress?.skip(actionsSuite.name, actionsSuite.reason);
146
+ writeDoctorSuiteDetails(reporter, actionsSuite);
147
+ progress?.writeSummaryGap();
148
+ return {
149
+ appRoot: environment.projectRoot,
150
+ appliedFixes,
151
+ findings,
152
+ remainingFindings: findings,
153
+ routesFile,
154
+ suites,
155
+ };
156
+ }
157
+ let actions = await runDoctorSuite(progress, 'actions', async () => {
158
+ let controllerResult = await checkControllerConventions(project.routeManifest.appRoot, project.routeManifest.tree);
159
+ return { suite: controllerResult.suite };
160
+ });
161
+ findings.push(...actions.suite.findings);
162
+ suites.push(actions.suite);
163
+ writeDoctorSuiteDetails(reporter, actions.suite);
164
+ progress?.writeSummaryGap();
165
+ let report = {
166
+ appRoot: environment.projectRoot,
167
+ findings,
168
+ routesFile,
169
+ suites,
170
+ };
171
+ if (options.fix) {
172
+ report.appliedFixes = appliedFixes;
173
+ report.remainingFindings = findings;
174
+ }
175
+ return report;
176
+ }
177
+ function formatAppliedFix(appliedFix) {
178
+ if (appliedFix.kind === 'update-file') {
179
+ return `Updated ${appliedFix.path}`;
180
+ }
181
+ return `Created ${appliedFix.path}`;
182
+ }
183
+ async function runDoctorSuite(progress, label, callback) {
184
+ progress?.start(label);
185
+ try {
186
+ let result = await callback();
187
+ if (result.suite.status === 'ok') {
188
+ progress?.succeed(label);
189
+ }
190
+ else if (result.suite.status === 'issues') {
191
+ progress?.fail(label);
192
+ }
193
+ else {
194
+ progress?.skip(label, result.suite.reason);
195
+ }
196
+ return result;
197
+ }
198
+ catch (error) {
199
+ progress?.fail(label);
200
+ throw error;
201
+ }
202
+ }
203
+ function createDoctorProgressReporter(reporter) {
204
+ return createStepProgressReporter(reporter.status, DOCTOR_SUITE_LABELS);
205
+ }
206
+ function writeDoctorReport(reporter, report) {
207
+ let warningCount = report.findings.filter((finding) => finding.severity === 'warn').length;
208
+ let adviceCount = report.findings.length - warningCount;
209
+ if ((report.appliedFixes?.length ?? 0) > 0) {
210
+ let fixCount = report.appliedFixes.length;
211
+ reporter.out.line(`Applied ${fixCount} ${fixCount === 1 ? 'fix' : 'fixes'}.`);
212
+ }
213
+ if (report.findings.length === 0) {
214
+ reporter.out.line('Doctor found no issues.');
215
+ }
216
+ reporter.out.line(`Summary: ${warningCount} warnings, ${adviceCount} advice.`);
217
+ }
218
+ function writeDoctorSuiteDetails(reporter, suite) {
219
+ if (reporter == null) {
220
+ return;
221
+ }
222
+ if (suite.findings.length === 0 && (suite.appliedFixes?.length ?? 0) === 0) {
223
+ return;
224
+ }
225
+ reporter.out.withIndent(() => {
226
+ for (let finding of suite.findings) {
227
+ reporter.out.bullet(reporter.out.label(finding.severity.toUpperCase(), finding.message, {
228
+ tone: finding.severity === 'warn' ? 'warn' : undefined,
229
+ }));
230
+ }
231
+ if ((suite.appliedFixes?.length ?? 0) > 0) {
232
+ reporter.out.section('Applied fixes:', () => {
233
+ reporter.out.bullets((suite.appliedFixes ?? []).map(formatAppliedFix));
234
+ });
235
+ }
236
+ });
237
+ }
@@ -1,7 +1,7 @@
1
1
  export type DoctorSuiteName = 'environment' | 'project' | 'actions';
2
2
  export type DoctorFindingSeverity = 'warn' | 'advice';
3
3
  export type DoctorSuiteStatus = 'ok' | 'issues' | 'skipped';
4
- export type DoctorFindingCode = 'project-root-not-found' | 'package-json-read-failed' | 'package-json-invalid' | 'node-engine-missing' | 'node-engine-unparseable' | 'node-version-unsupported' | 'remix-dependency-missing' | 'remix-install-missing' | 'routes-file-missing' | 'routes-export-missing' | 'route-map-invalid' | 'route-module-import-failed' | 'route-map-invalid-json' | 'route-map-loader-signal' | 'missing-owner' | 'duplicate-owner-file' | 'incomplete-controller' | 'orphan-controller' | 'orphan-route-directory';
4
+ export type DoctorFindingCode = 'project-root-not-found' | 'package-json-read-failed' | 'package-json-invalid' | 'node-engine-missing' | 'node-engine-unparseable' | 'node-version-unsupported' | 'remix-dependency-missing' | 'remix-install-missing' | 'routes-file-missing' | 'routes-export-missing' | 'route-map-invalid' | 'route-module-import-failed' | 'route-map-invalid-json' | 'route-map-loader-signal' | 'missing-owner' | 'missing-route-directory';
5
5
  export type DoctorFixKind = 'create-directory' | 'create-file' | 'update-file';
6
6
  export interface DoctorFinding {
7
7
  actualPath?: string;
@@ -43,6 +43,7 @@ export interface DoctorReport {
43
43
  routesFile?: string;
44
44
  suites: DoctorSuiteResult[];
45
45
  }
46
+ export declare function hasWarningFindings(findings: DoctorFinding[]): boolean;
46
47
  export declare function createDoctorSuite(name: DoctorSuiteName, findings: DoctorFinding[]): DoctorSuiteResult;
47
48
  export declare function createSkippedDoctorSuite(name: DoctorSuiteName, reason: string): DoctorSuiteResult;
48
49
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG,aAAa,GAAG,SAAS,GAAG,SAAS,CAAA;AACnE,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,QAAQ,CAAA;AACrD,MAAM,MAAM,iBAAiB,GAAG,IAAI,GAAG,QAAQ,GAAG,SAAS,CAAA;AAE3D,MAAM,MAAM,iBAAiB,GACzB,wBAAwB,GACxB,0BAA0B,GAC1B,sBAAsB,GACtB,qBAAqB,GACrB,yBAAyB,GACzB,0BAA0B,GAC1B,0BAA0B,GAC1B,uBAAuB,GACvB,qBAAqB,GACrB,uBAAuB,GACvB,mBAAmB,GACnB,4BAA4B,GAC5B,wBAAwB,GACxB,yBAAyB,GACzB,eAAe,GACf,sBAAsB,GACtB,uBAAuB,GACvB,mBAAmB,GACnB,wBAAwB,CAAA;AAE5B,MAAM,MAAM,aAAa,GAAG,kBAAkB,GAAG,aAAa,GAAG,aAAa,CAAA;AAE9E,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,iBAAiB,CAAA;IACvB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,KAAK,EAAE,eAAe,CAAA;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,iBAAiB,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,aAAa,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,eAAe,CAAA;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,iBAAiB,CAAA;IACvB,IAAI,EAAE,aAAa,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,eAAe,CAAA;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,YAAY,CAAC,EAAE,gBAAgB,EAAE,CAAA;IACjC,QAAQ,EAAE,aAAa,EAAE,CAAA;IACzB,IAAI,EAAE,eAAe,CAAA;IACrB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,iBAAiB,CAAA;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,gBAAgB,EAAE,CAAA;IACjC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,aAAa,EAAE,CAAA;IACzB,iBAAiB,CAAC,EAAE,aAAa,EAAE,CAAA;IACnC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,iBAAiB,EAAE,CAAA;CAC5B;AAED,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,eAAe,EACrB,QAAQ,EAAE,aAAa,EAAE,GACxB,iBAAiB,CAMnB;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,iBAAiB,CAOjG"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG,aAAa,GAAG,SAAS,GAAG,SAAS,CAAA;AACnE,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,QAAQ,CAAA;AACrD,MAAM,MAAM,iBAAiB,GAAG,IAAI,GAAG,QAAQ,GAAG,SAAS,CAAA;AAE3D,MAAM,MAAM,iBAAiB,GACzB,wBAAwB,GACxB,0BAA0B,GAC1B,sBAAsB,GACtB,qBAAqB,GACrB,yBAAyB,GACzB,0BAA0B,GAC1B,0BAA0B,GAC1B,uBAAuB,GACvB,qBAAqB,GACrB,uBAAuB,GACvB,mBAAmB,GACnB,4BAA4B,GAC5B,wBAAwB,GACxB,yBAAyB,GACzB,eAAe,GACf,yBAAyB,CAAA;AAE7B,MAAM,MAAM,aAAa,GAAG,kBAAkB,GAAG,aAAa,GAAG,aAAa,CAAA;AAE9E,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,iBAAiB,CAAA;IACvB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,KAAK,EAAE,eAAe,CAAA;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,iBAAiB,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,aAAa,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,eAAe,CAAA;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,iBAAiB,CAAA;IACvB,IAAI,EAAE,aAAa,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,eAAe,CAAA;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,YAAY,CAAC,EAAE,gBAAgB,EAAE,CAAA;IACjC,QAAQ,EAAE,aAAa,EAAE,CAAA;IACzB,IAAI,EAAE,eAAe,CAAA;IACrB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,iBAAiB,CAAA;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,gBAAgB,EAAE,CAAA;IACjC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,aAAa,EAAE,CAAA;IACzB,iBAAiB,CAAC,EAAE,aAAa,EAAE,CAAA;IACnC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,iBAAiB,EAAE,CAAA;CAC5B;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,OAAO,CAErE;AAED,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,eAAe,EACrB,QAAQ,EAAE,aAAa,EAAE,GACxB,iBAAiB,CAMnB;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,iBAAiB,CAOjG"}