@remix-run/cli 0.2.0 → 0.3.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 (84) hide show
  1. package/dist/lib/bootstrap-project.d.ts.map +1 -1
  2. package/dist/lib/bootstrap-project.js +32 -9
  3. package/dist/lib/cli.d.ts +29 -0
  4. package/dist/lib/cli.d.ts.map +1 -1
  5. package/dist/lib/cli.js +26 -4
  6. package/dist/lib/commands/completion.d.ts.map +1 -1
  7. package/dist/lib/commands/completion.js +5 -1
  8. package/dist/lib/commands/doctor.js +18 -18
  9. package/dist/lib/commands/routes.js +3 -3
  10. package/dist/lib/completion.js +1 -5
  11. package/dist/lib/controller-files.d.ts +0 -1
  12. package/dist/lib/controller-files.d.ts.map +1 -1
  13. package/dist/lib/controller-files.js +3 -5
  14. package/dist/lib/controller-ownership.d.ts +9 -9
  15. package/dist/lib/controller-ownership.d.ts.map +1 -1
  16. package/dist/lib/controller-ownership.js +56 -91
  17. package/dist/lib/doctor/controller-findings.d.ts +1 -1
  18. package/dist/lib/doctor/controller-findings.d.ts.map +1 -1
  19. package/dist/lib/doctor/controller-findings.js +15 -87
  20. package/dist/lib/doctor/controller-fix-plans.d.ts.map +1 -1
  21. package/dist/lib/doctor/controller-fix-plans.js +13 -24
  22. package/dist/lib/doctor/controller-placeholders.d.ts +2 -3
  23. package/dist/lib/doctor/controller-placeholders.d.ts.map +1 -1
  24. package/dist/lib/doctor/controller-placeholders.js +18 -149
  25. package/dist/lib/doctor/controllers.js +1 -1
  26. package/dist/lib/doctor/project.js +60 -52
  27. package/dist/lib/doctor/types.d.ts +2 -2
  28. package/dist/lib/doctor/types.d.ts.map +1 -1
  29. package/dist/lib/load-route-map-worker.js +17 -9
  30. package/dist/lib/route-map.d.ts +1 -1
  31. package/dist/lib/route-map.d.ts.map +1 -1
  32. package/dist/lib/route-map.js +29 -17
  33. package/package.json +3 -3
  34. package/src/lib/bootstrap-project.ts +39 -13
  35. package/src/lib/cli.ts +39 -4
  36. package/src/lib/commands/completion.ts +6 -1
  37. package/src/lib/commands/doctor.ts +18 -21
  38. package/src/lib/commands/routes.ts +3 -3
  39. package/src/lib/completion.ts +1 -5
  40. package/src/lib/controller-files.ts +4 -8
  41. package/src/lib/controller-ownership.ts +78 -141
  42. package/src/lib/doctor/controller-findings.ts +20 -97
  43. package/src/lib/doctor/controller-fix-plans.ts +13 -29
  44. package/src/lib/doctor/controller-placeholders.ts +17 -189
  45. package/src/lib/doctor/controllers.ts +1 -1
  46. package/src/lib/doctor/project.ts +60 -52
  47. package/src/lib/doctor/types.ts +1 -5
  48. package/src/lib/load-route-map-worker.ts +19 -10
  49. package/src/lib/route-map.ts +61 -16
  50. package/{bootstrap → template}/.agents/skills/remix/SKILL.md +160 -73
  51. package/{bootstrap → template}/.agents/skills/remix/references/assets-and-browser-modules.md +22 -14
  52. package/{bootstrap → template}/.agents/skills/remix/references/auth-and-sessions.md +41 -18
  53. package/{bootstrap → template}/.agents/skills/remix/references/data-and-validation.md +21 -5
  54. package/{bootstrap → template}/.agents/skills/remix/references/middleware-and-server.md +43 -53
  55. package/{bootstrap → template}/.agents/skills/remix/references/routing-and-controllers.md +111 -44
  56. package/{bootstrap → template}/.agents/skills/remix/references/testing-patterns.md +17 -1
  57. package/{bootstrap → template}/AGENTS.md +10 -9
  58. package/template/README.md +29 -0
  59. package/template/app/actions/controller.tsx +18 -0
  60. package/template/app/assets/entry.ts +8 -0
  61. package/{bootstrap/app/ui → template/app/assets}/prompt-button.tsx +2 -1
  62. package/{bootstrap → template}/app/assets.ts +5 -3
  63. package/template/app/middleware/render.tsx +43 -0
  64. package/template/app/router.ts +20 -0
  65. package/{bootstrap → template}/app/routes.ts +1 -2
  66. package/{bootstrap → template}/app/ui/document.tsx +11 -4
  67. package/{bootstrap → template}/app/ui/scaffold-home-page.tsx +39 -38
  68. package/template/gitignore +4 -0
  69. package/{bootstrap → template}/package.json +5 -6
  70. package/template/public/favicon.svg +11 -0
  71. package/{bootstrap → template}/server.ts +10 -11
  72. package/{bootstrap → template}/tsconfig.json +3 -2
  73. package/bootstrap/README.md +0 -27
  74. package/bootstrap/app/assets/entry.ts +0 -19
  75. package/bootstrap/app/controllers/auth.tsx +0 -21
  76. package/bootstrap/app/controllers/home.tsx +0 -11
  77. package/bootstrap/app/router.ts +0 -16
  78. package/bootstrap/app/ui/layout.tsx +0 -22
  79. package/bootstrap/app/utils/render.tsx +0 -26
  80. /package/{bootstrap → template}/.agents/skills/remix/references/animate-elements.md +0 -0
  81. /package/{bootstrap → template}/.agents/skills/remix/references/component-model.md +0 -0
  82. /package/{bootstrap → template}/.agents/skills/remix/references/create-mixins.md +0 -0
  83. /package/{bootstrap → template}/.agents/skills/remix/references/hydration-frames-navigation.md +0 -0
  84. /package/{bootstrap → template}/.agents/skills/remix/references/mixins-styling-events.md +0 -0
@@ -1,58 +1,69 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
- import { getActionOwnerCandidates, getControllerOwnerCandidates, getOwnerModuleBaseName, getPreferredOwnerDisplayPath, getRouteSubtreePath, isActionFileName, isControllerEntryFileName, toDiskSegment, } from "./controller-files.js";
3
+ import { getControllerOwnerCandidates, getOwnerModuleBaseName, getPreferredOwnerDisplayPath, getRouteSubtreePath, isControllerEntryFileName, toDiskSegment, } from "./controller-files.js";
4
+ const ACTIONS_PATH_PREFIX = 'app/actions/';
5
+ export const ROOT_ROUTE_NAME = '<root>';
4
6
  export async function inspectControllerOwnership(appRoot, tree) {
5
7
  let subtreePlans = buildOwnedSubtrees(tree);
8
+ let routeDirectories = buildRouteDirectories(tree);
6
9
  let scan = await scanControllersDirectory(appRoot);
7
10
  let subtrees = applyScanToSubtrees(subtreePlans, scan);
8
11
  return {
9
- orphanActionPaths: getOrphanActionPaths(subtreePlans, scan),
10
12
  orphanControllerPaths: getOrphanControllerPaths(subtreePlans, scan),
11
- orphanRouteDirectoryPaths: getOrphanRouteDirectoryPaths(subtreePlans, scan),
13
+ orphanRouteDirectoryPaths: getOrphanRouteDirectoryPaths(routeDirectories, scan),
14
+ routeDirectories,
12
15
  scan,
13
16
  subtrees,
14
17
  };
15
18
  }
16
19
  export function buildOwnedSubtrees(tree, parentSegments = [], subtrees = []) {
20
+ if (parentSegments.length === 0 &&
21
+ subtrees.length === 0 &&
22
+ tree.some((node) => node.kind === 'route')) {
23
+ addSubtreePlan(ROOT_ROUTE_NAME, [], subtrees);
24
+ }
17
25
  for (let node of tree) {
18
- let segments = [...parentSegments, toDiskSegment(node.key)];
19
- if (node.kind === 'group') {
20
- let alternateCandidates = getActionOwnerCandidates(segments);
21
- let entryCandidates = getControllerOwnerCandidates(segments);
22
- subtrees.push({
23
- alternateCandidates,
24
- alternateDisplayPath: getPreferredOwnerDisplayPath(alternateCandidates),
25
- entryCandidates,
26
- entryDisplayPath: getPreferredOwnerDisplayPath(entryCandidates),
27
- kind: 'controller',
28
- routeName: node.name,
29
- subtreePath: getRouteSubtreePath(segments),
30
- });
31
- buildOwnedSubtrees(node.children, segments, subtrees);
26
+ if (node.kind !== 'group') {
32
27
  continue;
33
28
  }
34
- if (parentSegments.length > 0) {
29
+ let segments = [...parentSegments, toDiskSegment(node.key)];
30
+ if (hasDirectRouteLeaf(node.children)) {
31
+ addSubtreePlan(node.name, segments, subtrees);
32
+ }
33
+ buildOwnedSubtrees(node.children, segments, subtrees);
34
+ }
35
+ return subtrees;
36
+ }
37
+ export function buildRouteDirectories(tree, parentSegments = [], directories = []) {
38
+ for (let node of tree) {
39
+ if (node.kind !== 'group') {
35
40
  continue;
36
41
  }
37
- let alternateCandidates = getControllerOwnerCandidates(segments);
38
- let entryCandidates = getActionOwnerCandidates(segments);
39
- subtrees.push({
40
- alternateCandidates,
41
- alternateDisplayPath: getPreferredOwnerDisplayPath(alternateCandidates),
42
- entryCandidates,
43
- entryDisplayPath: getPreferredOwnerDisplayPath(entryCandidates),
44
- kind: 'action',
42
+ let segments = [...parentSegments, toDiskSegment(node.key)];
43
+ directories.push({
44
+ directoryPath: getRouteSubtreePath(segments),
45
45
  routeName: node.name,
46
- subtreePath: getRouteSubtreePath(segments),
47
46
  });
47
+ buildRouteDirectories(node.children, segments, directories);
48
48
  }
49
- return subtrees;
49
+ return directories;
50
+ }
51
+ function hasDirectRouteLeaf(tree) {
52
+ return tree.some((node) => node.kind === 'route');
53
+ }
54
+ function addSubtreePlan(routeName, segments, subtrees) {
55
+ let entryCandidates = getControllerOwnerCandidates(segments);
56
+ subtrees.push({
57
+ entryCandidates,
58
+ entryDisplayPath: getPreferredOwnerDisplayPath(entryCandidates),
59
+ routeName,
60
+ subtreePath: getRouteSubtreePath(segments),
61
+ });
50
62
  }
51
63
  async function scanControllersDirectory(appRoot) {
52
- let controllersDir = path.join(appRoot, 'app', 'controllers');
64
+ let actionsDir = path.join(appRoot, 'app', 'actions');
53
65
  let controllerEntryPaths = new Set();
54
- let actionEntryPaths = new Set();
55
- let rootDirectoryPaths = new Set();
66
+ let routeDirectoryPaths = new Set();
56
67
  let routeLocalFilePaths = new Set();
57
68
  async function walk(currentDir, isRoot) {
58
69
  let entries;
@@ -70,9 +81,7 @@ async function scanControllersDirectory(appRoot) {
70
81
  let entryPath = path.join(currentDir, entry.name);
71
82
  let relativePath = normalizeRelativePath(path.relative(appRoot, entryPath));
72
83
  if (entry.isDirectory()) {
73
- if (isRoot) {
74
- rootDirectoryPaths.add(relativePath);
75
- }
84
+ routeDirectoryPaths.add(relativePath);
76
85
  await walk(entryPath, false);
77
86
  continue;
78
87
  }
@@ -83,20 +92,15 @@ async function scanControllersDirectory(appRoot) {
83
92
  controllerEntryPaths.add(relativePath);
84
93
  continue;
85
94
  }
86
- if (isRoot && isActionCandidate(entry.name)) {
87
- actionEntryPaths.add(relativePath);
88
- continue;
89
- }
90
- if (!isRoot && isRouteLocalFileName(entry.name)) {
95
+ if (isRouteLocalFileName(entry.name)) {
91
96
  routeLocalFilePaths.add(relativePath);
92
97
  }
93
98
  }
94
99
  }
95
- await walk(controllersDir, true);
100
+ await walk(actionsDir, true);
96
101
  return {
97
- actionEntryPaths,
98
102
  controllerEntryPaths,
99
- rootDirectoryPaths,
103
+ routeDirectoryPaths,
100
104
  routeLocalFilePaths,
101
105
  };
102
106
  }
@@ -104,12 +108,9 @@ function applyScanToSubtrees(subtreePlans, scan) {
104
108
  let claimedRouteLocalPaths = claimFilesToDeepestSubtree([...scan.routeLocalFilePaths], subtreePlans);
105
109
  let claimedContentPaths = claimFilesToDeepestSubtree(getNestedContentPaths(scan), subtreePlans);
106
110
  return subtreePlans.map((subtree) => {
107
- let actualAlternatePaths = findOwnerPaths(scan, invertOwnerKind(subtree.kind), subtree.alternateCandidates);
108
- let actualEntryPaths = findOwnerPaths(scan, subtree.kind, subtree.entryCandidates);
111
+ let actualEntryPaths = findOwnerPaths(scan, subtree.entryCandidates);
109
112
  return {
110
113
  ...subtree,
111
- actualAlternatePath: actualAlternatePaths[0] ?? null,
112
- actualAlternatePaths,
113
114
  actualEntryPath: actualEntryPaths[0] ?? null,
114
115
  actualEntryPaths,
115
116
  claimedFilePaths: claimedContentPaths.get(subtree.routeName) ?? [],
@@ -143,49 +144,20 @@ function claimFilesToDeepestSubtree(filePaths, subtreePlans) {
143
144
  }
144
145
  return claims;
145
146
  }
146
- function getOrphanActionPaths(subtreePlans, scan) {
147
- let expectedActionPaths = new Set(subtreePlans
148
- .filter((subtree) => subtree.kind === 'action')
149
- .flatMap((subtree) => subtree.entryCandidates));
150
- let alternateActionPaths = new Set(subtreePlans
151
- .filter((subtree) => subtree.kind === 'controller')
152
- .flatMap((subtree) => subtree.alternateCandidates));
153
- return [...scan.actionEntryPaths]
154
- .filter((filePath) => !expectedActionPaths.has(filePath))
155
- .filter((filePath) => !alternateActionPaths.has(filePath))
156
- .sort();
157
- }
158
147
  function getOrphanControllerPaths(subtreePlans, scan) {
159
- let expectedControllerPaths = new Set(subtreePlans
160
- .filter((subtree) => subtree.kind === 'controller')
161
- .flatMap((subtree) => subtree.entryCandidates));
162
- let alternateControllerPaths = new Set(subtreePlans
163
- .filter((subtree) => subtree.kind === 'action')
164
- .flatMap((subtree) => subtree.alternateCandidates));
148
+ let expectedControllerPaths = new Set(subtreePlans.flatMap((subtree) => subtree.entryCandidates));
165
149
  return [...scan.controllerEntryPaths]
166
150
  .filter((filePath) => !expectedControllerPaths.has(filePath))
167
- .filter((filePath) => !alternateControllerPaths.has(filePath))
168
151
  .sort();
169
152
  }
170
- function getOrphanRouteDirectoryPaths(subtreePlans, scan) {
171
- let expectedRootDirectories = new Set(subtreePlans.map((subtree) => getTopLevelSubtreePath(subtree.subtreePath)));
153
+ function getOrphanRouteDirectoryPaths(routeDirectories, scan) {
154
+ let expectedRouteDirectories = new Set(routeDirectories.map((routeDirectory) => routeDirectory.directoryPath));
172
155
  let actualControllerDirectories = [...scan.controllerEntryPaths].map((controllerPath) => normalizeRelativePath(path.dirname(controllerPath)));
173
- return [...scan.rootDirectoryPaths]
174
- .filter((directoryPath) => !expectedRootDirectories.has(directoryPath))
156
+ return [...scan.routeDirectoryPaths]
157
+ .filter((directoryPath) => !expectedRouteDirectories.has(directoryPath))
175
158
  .filter((directoryPath) => !actualControllerDirectories.some((controllerPath) => isDirectoryWithinDirectory(controllerPath, directoryPath)))
176
159
  .sort();
177
160
  }
178
- function hasOwnerPath(scan, kind, filePath) {
179
- return kind === 'action'
180
- ? scan.actionEntryPaths.has(filePath)
181
- : scan.controllerEntryPaths.has(filePath);
182
- }
183
- function invertOwnerKind(kind) {
184
- return kind === 'action' ? 'controller' : 'action';
185
- }
186
- function isActionCandidate(fileName) {
187
- return isActionFileName(fileName);
188
- }
189
161
  function isRouteLocalFileName(fileName) {
190
162
  let baseName = getOwnerModuleBaseName(fileName);
191
163
  return (baseName != null &&
@@ -193,21 +165,18 @@ function isRouteLocalFileName(fileName) {
193
165
  !baseName.endsWith('.test') &&
194
166
  !baseName.endsWith('.spec'));
195
167
  }
196
- function findOwnerPath(scan, kind, candidatePaths) {
197
- return findOwnerPaths(scan, kind, candidatePaths)[0] ?? null;
198
- }
199
- function findOwnerPaths(scan, kind, candidatePaths) {
168
+ function findOwnerPaths(scan, candidatePaths) {
200
169
  let existingPaths = [];
201
170
  for (let candidatePath of candidatePaths) {
202
- if (hasOwnerPath(scan, kind, candidatePath)) {
171
+ if (scan.controllerEntryPaths.has(candidatePath)) {
203
172
  existingPaths.push(candidatePath);
204
173
  }
205
174
  }
206
175
  return existingPaths;
207
176
  }
208
177
  function isNestedControllerPath(filePath) {
209
- return (filePath.startsWith('app/controllers/') &&
210
- filePath.slice('app/controllers/'.length).includes('/'));
178
+ return (filePath.startsWith(ACTIONS_PATH_PREFIX) &&
179
+ filePath.slice(ACTIONS_PATH_PREFIX.length).includes('/'));
211
180
  }
212
181
  function isWithinDirectory(filePath, directoryPath) {
213
182
  return filePath.startsWith(`${directoryPath}/`);
@@ -215,10 +184,6 @@ function isWithinDirectory(filePath, directoryPath) {
215
184
  function isDirectoryWithinDirectory(directoryPath, parentDirectoryPath) {
216
185
  return (directoryPath === parentDirectoryPath || isWithinDirectory(directoryPath, parentDirectoryPath));
217
186
  }
218
- function getTopLevelSubtreePath(subtreePath) {
219
- let subtreeSegments = subtreePath.slice('app/controllers/'.length).split('/');
220
- return getRouteSubtreePath([subtreeSegments[0]]);
221
- }
222
187
  function normalizeRelativePath(filePath) {
223
188
  return filePath.split(path.sep).join('/');
224
189
  }
@@ -1,4 +1,4 @@
1
- import type { ControllerOwnership } from '../controller-ownership.ts';
1
+ import { type ControllerOwnership } from '../controller-ownership.ts';
2
2
  import type { DoctorFinding, DoctorFixPlan } from './types.ts';
3
3
  export declare function getControllerFindings(ownership: ControllerOwnership, fixPlans: DoctorFixPlan[]): DoctorFinding[];
4
4
  //# sourceMappingURL=controller-findings.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"controller-findings.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/controller-findings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAgB,MAAM,4BAA4B,CAAA;AACnF,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE9D,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,mBAAmB,EAC9B,QAAQ,EAAE,aAAa,EAAE,GACxB,aAAa,EAAE,CAyBjB"}
1
+ {"version":3,"file":"controller-findings.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/controller-findings.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,4BAA4B,CAAA;AACnC,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE9D,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,mBAAmB,EAC9B,QAAQ,EAAE,aAAa,EAAE,GACxB,aAAa,EAAE,CAkBjB"}
@@ -1,26 +1,20 @@
1
+ import { ROOT_ROUTE_NAME, } from "../controller-ownership.js";
1
2
  export function getControllerFindings(ownership, fixPlans) {
2
3
  return [
3
4
  ...getSubtreeFindings(ownership.subtrees, fixPlans),
4
- ...ownership.orphanActionPaths.map((actualPath) => ({
5
- actualPath,
6
- code: 'orphan-action',
7
- message: `Standalone action ${actualPath} does not match any top-level route.`,
8
- severity: 'warn',
9
- suite: 'controllers',
10
- })),
11
5
  ...ownership.orphanControllerPaths.map((actualPath) => ({
12
6
  actualPath,
13
7
  code: 'orphan-controller',
14
- message: `Controller ${actualPath} does not match any route group.`,
8
+ message: `Action controller ${actualPath} does not match any route map.`,
15
9
  severity: 'warn',
16
- suite: 'controllers',
10
+ suite: 'actions',
17
11
  })),
18
12
  ...ownership.orphanRouteDirectoryPaths.map((actualPath) => ({
19
13
  actualPath,
20
14
  code: 'orphan-route-directory',
21
- message: `Directory ${actualPath} does not match any route subtree.`,
15
+ message: `Directory ${actualPath} does not match any route-map key path.`,
22
16
  severity: 'warn',
23
- suite: 'controllers',
17
+ suite: 'actions',
24
18
  })),
25
19
  ];
26
20
  }
@@ -28,63 +22,20 @@ function getSubtreeFindings(subtrees, fixPlans) {
28
22
  let findings = [];
29
23
  let fixableFindingKeys = new Set(fixPlans.map((fixPlan) => `${fixPlan.code}:${fixPlan.routeName ?? ''}`));
30
24
  for (let subtree of subtrees) {
31
- let actualEntryPath = subtree.actualEntryPath ?? undefined;
32
- let actualAlternatePath = subtree.actualAlternatePath ?? undefined;
25
+ let routeMapName = formatRouteMapName(subtree.routeName);
33
26
  if (subtree.actualEntryPaths.length > 1) {
34
27
  findings.push({
35
28
  actualPath: subtree.actualEntryPaths[0],
36
29
  code: 'duplicate-owner-file',
37
30
  expectedPath: subtree.entryDisplayPath,
38
- message: `Route "${subtree.routeName}" has multiple ${subtree.kind} files: ${subtree.actualEntryPaths.join(', ')}. Keep only one ${subtree.kind} owner file.`,
39
- routeName: subtree.routeName,
40
- severity: 'warn',
41
- suite: 'controllers',
42
- });
43
- continue;
44
- }
45
- if (subtree.actualAlternatePaths.length > 1) {
46
- let alternateKind = subtree.kind === 'action' ? 'controller' : 'action';
47
- findings.push({
48
- actualPath: subtree.actualAlternatePaths[0],
49
- code: 'duplicate-owner-file',
50
- expectedPath: subtree.entryDisplayPath,
51
- message: `Route "${subtree.routeName}" has multiple ${alternateKind} files: ${subtree.actualAlternatePaths.join(', ')}. Keep only one ${alternateKind} owner file.`,
52
- routeName: subtree.routeName,
53
- severity: 'warn',
54
- suite: 'controllers',
55
- });
56
- continue;
57
- }
58
- if (actualEntryPath != null && actualAlternatePath != null) {
59
- findings.push({
60
- actualPath: actualAlternatePath,
61
- code: 'ambiguous-owner',
62
- expectedPath: actualEntryPath,
63
- message: subtree.kind === 'action'
64
- ? `Route "${subtree.routeName}" has both action ${actualEntryPath} and controller ${actualAlternatePath}.`
65
- : `Route "${subtree.routeName}" has both controller ${actualEntryPath} and standalone action ${actualAlternatePath}.`,
66
- routeName: subtree.routeName,
67
- severity: 'warn',
68
- suite: 'controllers',
69
- });
70
- continue;
71
- }
72
- if (actualAlternatePath != null) {
73
- findings.push({
74
- actualPath: actualAlternatePath,
75
- code: 'wrong-owner-kind',
76
- expectedPath: subtree.entryDisplayPath,
77
- message: subtree.kind === 'action'
78
- ? `Route "${subtree.routeName}" expects action ${subtree.entryDisplayPath}, but found controller ${actualAlternatePath}.`
79
- : `Route "${subtree.routeName}" expects controller ${subtree.entryDisplayPath}, but found standalone action ${actualAlternatePath}.`,
80
- fixable: fixableFindingKeys.has(`wrong-owner-kind:${subtree.routeName}`),
31
+ message: `${routeMapName} has multiple action controller files: ${subtree.actualEntryPaths.join(', ')}. Keep only one controller owner file.`,
81
32
  routeName: subtree.routeName,
82
33
  severity: 'warn',
83
- suite: 'controllers',
34
+ suite: 'actions',
84
35
  });
85
36
  continue;
86
37
  }
87
- if (subtree.kind === 'controller' && subtree.actualEntryPath == null) {
38
+ if (subtree.actualEntryPath == null) {
88
39
  let code = subtree.claimedFilePaths.length > 0
89
40
  ? 'incomplete-controller'
90
41
  : 'missing-owner';
@@ -94,39 +45,16 @@ function getSubtreeFindings(subtrees, fixPlans) {
94
45
  expectedPath: subtree.entryDisplayPath,
95
46
  fixable: fixableFindingKeys.has(`${code}:${subtree.routeName}`),
96
47
  message: subtree.claimedFilePaths.length > 0
97
- ? `Route "${subtree.routeName}" has files under ${subtree.subtreePath}, but is missing controller ${subtree.entryDisplayPath}.`
98
- : `Route "${subtree.routeName}" is missing controller ${subtree.entryDisplayPath}.`,
99
- routeName: subtree.routeName,
100
- severity: 'warn',
101
- suite: 'controllers',
102
- });
103
- continue;
104
- }
105
- if (subtree.kind === 'action' && subtree.claimedFilePaths.length > 0) {
106
- findings.push({
107
- actualPath: subtree.subtreePath,
108
- code: 'promotion-drift',
109
- expectedPath: actualEntryPath ?? subtree.entryDisplayPath,
110
- message: actualEntryPath == null
111
- ? `Route "${subtree.routeName}" has files under ${subtree.subtreePath}, but is still expected to use action ${subtree.entryDisplayPath}. Promote it to controller ${subtree.alternateDisplayPath} or move the files back into ${subtree.entryDisplayPath}.`
112
- : `Route "${subtree.routeName}" uses action ${actualEntryPath}, but also has files under ${subtree.subtreePath}. Promote it to controller ${subtree.alternateDisplayPath} or keep the route in ${actualEntryPath}.`,
113
- routeName: subtree.routeName,
114
- severity: 'warn',
115
- suite: 'controllers',
116
- });
117
- continue;
118
- }
119
- if (subtree.actualEntryPath == null) {
120
- findings.push({
121
- code: 'missing-owner',
122
- expectedPath: subtree.entryDisplayPath,
123
- fixable: fixableFindingKeys.has(`missing-owner:${subtree.routeName}`),
124
- message: `Route "${subtree.routeName}" is missing action ${subtree.entryDisplayPath}.`,
48
+ ? `${routeMapName} has files under ${subtree.subtreePath}, but is missing action controller ${subtree.entryDisplayPath}.`
49
+ : `${routeMapName} is missing action controller ${subtree.entryDisplayPath}.`,
125
50
  routeName: subtree.routeName,
126
51
  severity: 'warn',
127
- suite: 'controllers',
52
+ suite: 'actions',
128
53
  });
129
54
  }
130
55
  }
131
56
  return findings;
132
57
  }
58
+ function formatRouteMapName(routeName) {
59
+ return routeName === ROOT_ROUTE_NAME ? 'Root route map' : `Route map "${routeName}"`;
60
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"controller-fix-plans.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/controller-fix-plans.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,mBAAmB,EAEnB,kBAAkB,EACnB,MAAM,4BAA4B,CAAA;AAEnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAI/C,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,kBAAkB,EAAE,EAC1B,SAAS,EAAE,mBAAmB,GAC7B,OAAO,CAAC,aAAa,EAAE,CAAC,CAqD1B"}
1
+ {"version":3,"file":"controller-fix-plans.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/controller-fix-plans.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,mBAAmB,EAEnB,kBAAkB,EACnB,MAAM,4BAA4B,CAAA;AAGnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAI/C,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,kBAAkB,EAAE,EAC1B,SAAS,EAAE,mBAAmB,GAC7B,OAAO,CAAC,aAAa,EAAE,CAAC,CAkD1B"}
@@ -1,7 +1,8 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { getOwnerCandidateForExtension, getOwnerFileExtension, } from "../controller-files.js";
4
- import { renderActionPlaceholder, renderControllerPlaceholder } from "./controller-placeholders.js";
4
+ import { ROOT_ROUTE_NAME } from "../controller-ownership.js";
5
+ import { renderControllerPlaceholder } from "./controller-placeholders.js";
5
6
  const OWNER_EXTENSION_PRIORITY = ['.tsx', '.ts', '.jsx', '.js'];
6
7
  export async function getControllerFixPlans(appRoot, tree, ownership) {
7
8
  let routeNodesByName = getRouteNodesByName(tree);
@@ -31,51 +32,33 @@ export async function getControllerFixPlans(appRoot, tree, ownership) {
31
32
  if (routeNode == null || entryPath == null) {
32
33
  continue;
33
34
  }
34
- let contents = subtree.kind === 'action'
35
- ? renderActionPlaceholder(routeNode, entryPath)
36
- : renderControllerPlaceholder(routeNode, entryPath, resolvedEntryPathByRouteName);
35
+ let contents = renderControllerPlaceholder(routeNode, entryPath);
37
36
  fixPlans.push({
38
37
  code,
39
38
  contents,
40
39
  kind: 'create-file',
41
40
  path: entryPath,
42
41
  routeName: subtree.routeName,
43
- suite: 'controllers',
42
+ suite: 'actions',
44
43
  });
45
44
  }
46
45
  return fixPlans;
47
46
  }
48
47
  function getFixCodeForSubtree(subtree) {
49
- if (subtree.actualEntryPaths.length > 1 || subtree.actualAlternatePaths.length > 1) {
48
+ if (subtree.actualEntryPaths.length > 1) {
50
49
  return null;
51
50
  }
52
51
  if (subtree.actualEntryPath != null) {
53
52
  return null;
54
53
  }
55
- if (subtree.actualAlternatePath != null) {
56
- return 'wrong-owner-kind';
57
- }
58
- if (subtree.kind === 'controller') {
59
- return subtree.claimedFilePaths.length > 0 ? 'incomplete-controller' : 'missing-owner';
60
- }
61
- if (subtree.claimedFilePaths.length > 0) {
62
- return null;
63
- }
64
- return 'missing-owner';
54
+ return subtree.claimedFilePaths.length > 0 ? 'incomplete-controller' : 'missing-owner';
65
55
  }
66
56
  function inferOwnerExtension(subtree, ownership, hasTsconfig) {
67
57
  let subtreeExtension = getMostCommonOwnerExtension(subtree.claimedRouteLocalFilePaths);
68
58
  if (subtreeExtension != null) {
69
59
  return subtreeExtension;
70
60
  }
71
- let alternateExtension = getMostCommonOwnerExtension(subtree.actualAlternatePaths);
72
- if (alternateExtension != null) {
73
- return alternateExtension;
74
- }
75
- let projectExtension = getMostCommonOwnerExtension([
76
- ...ownership.scan.actionEntryPaths,
77
- ...ownership.scan.controllerEntryPaths,
78
- ]);
61
+ let projectExtension = getMostCommonOwnerExtension([...ownership.scan.controllerEntryPaths]);
79
62
  if (projectExtension != null) {
80
63
  return projectExtension;
81
64
  }
@@ -106,6 +89,12 @@ function getMostCommonOwnerExtension(filePaths) {
106
89
  }
107
90
  function getRouteNodesByName(tree) {
108
91
  let routeNodesByName = new Map();
92
+ routeNodesByName.set(ROOT_ROUTE_NAME, {
93
+ children: tree,
94
+ key: ROOT_ROUTE_NAME,
95
+ kind: 'group',
96
+ name: ROOT_ROUTE_NAME,
97
+ });
109
98
  function visit(nodes) {
110
99
  for (let node of nodes) {
111
100
  routeNodesByName.set(node.name, node);
@@ -1,4 +1,3 @@
1
- import type { OwnershipRouteNode } from '../controller-ownership.ts';
2
- export declare function renderActionPlaceholder(routeNode: OwnershipRouteNode, entryPath: string): string;
3
- export declare function renderControllerPlaceholder(routeNode: OwnershipRouteNode, entryPath: string, resolvedEntryPathByRouteName: Map<string, string>): string;
1
+ import { type OwnershipRouteNode } from '../controller-ownership.ts';
2
+ export declare function renderControllerPlaceholder(routeNode: OwnershipRouteNode, entryPath: string): string;
4
3
  //# sourceMappingURL=controller-placeholders.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"controller-placeholders.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/controller-placeholders.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAmDpE,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAgChG;AAED,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,kBAAkB,EAC7B,SAAS,EAAE,MAAM,EACjB,4BAA4B,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAChD,MAAM,CAkCR"}
1
+ {"version":3,"file":"controller-placeholders.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/controller-placeholders.ts"],"names":[],"mappings":"AAGA,OAAO,EAAmB,KAAK,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAErF,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,kBAAkB,EAC7B,SAAS,EAAE,MAAM,GAChB,MAAM,CAyBR"}