@remix-run/cli 0.1.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 (110) hide show
  1. package/README.md +0 -3
  2. package/dist/lib/bootstrap-project.d.ts.map +1 -1
  3. package/dist/lib/bootstrap-project.js +32 -9
  4. package/dist/lib/cli.d.ts +29 -0
  5. package/dist/lib/cli.d.ts.map +1 -1
  6. package/dist/lib/cli.js +33 -14
  7. package/dist/lib/commands/completion.d.ts.map +1 -1
  8. package/dist/lib/commands/completion.js +5 -1
  9. package/dist/lib/commands/doctor.js +18 -18
  10. package/dist/lib/commands/help.d.ts.map +1 -1
  11. package/dist/lib/commands/help.js +9 -33
  12. package/dist/lib/commands/routes.js +3 -3
  13. package/dist/lib/commands/test.d.ts +1 -1
  14. package/dist/lib/commands/test.d.ts.map +1 -1
  15. package/dist/lib/commands/test.js +8 -4
  16. package/dist/lib/completion.d.ts.map +1 -1
  17. package/dist/lib/completion.js +4 -106
  18. package/dist/lib/controller-files.d.ts +0 -1
  19. package/dist/lib/controller-files.d.ts.map +1 -1
  20. package/dist/lib/controller-files.js +3 -5
  21. package/dist/lib/controller-ownership.d.ts +9 -9
  22. package/dist/lib/controller-ownership.d.ts.map +1 -1
  23. package/dist/lib/controller-ownership.js +56 -91
  24. package/dist/lib/doctor/controller-findings.d.ts +1 -1
  25. package/dist/lib/doctor/controller-findings.d.ts.map +1 -1
  26. package/dist/lib/doctor/controller-findings.js +15 -87
  27. package/dist/lib/doctor/controller-fix-plans.d.ts.map +1 -1
  28. package/dist/lib/doctor/controller-fix-plans.js +13 -24
  29. package/dist/lib/doctor/controller-placeholders.d.ts +2 -3
  30. package/dist/lib/doctor/controller-placeholders.d.ts.map +1 -1
  31. package/dist/lib/doctor/controller-placeholders.js +18 -149
  32. package/dist/lib/doctor/controllers.js +1 -1
  33. package/dist/lib/doctor/project.js +60 -52
  34. package/dist/lib/doctor/types.d.ts +2 -2
  35. package/dist/lib/doctor/types.d.ts.map +1 -1
  36. package/dist/lib/errors.d.ts +0 -6
  37. package/dist/lib/errors.d.ts.map +1 -1
  38. package/dist/lib/errors.js +0 -11
  39. package/dist/lib/load-route-map-worker.js +17 -9
  40. package/dist/lib/route-map.d.ts +1 -1
  41. package/dist/lib/route-map.d.ts.map +1 -1
  42. package/dist/lib/route-map.js +29 -17
  43. package/package.json +4 -5
  44. package/src/lib/bootstrap-project.ts +39 -13
  45. package/src/lib/cli.ts +46 -15
  46. package/src/lib/commands/completion.ts +6 -1
  47. package/src/lib/commands/doctor.ts +18 -21
  48. package/src/lib/commands/help.ts +9 -43
  49. package/src/lib/commands/routes.ts +3 -3
  50. package/src/lib/commands/test.ts +10 -4
  51. package/src/lib/completion.ts +4 -151
  52. package/src/lib/controller-files.ts +4 -8
  53. package/src/lib/controller-ownership.ts +78 -141
  54. package/src/lib/doctor/controller-findings.ts +20 -97
  55. package/src/lib/doctor/controller-fix-plans.ts +13 -29
  56. package/src/lib/doctor/controller-placeholders.ts +17 -189
  57. package/src/lib/doctor/controllers.ts +1 -1
  58. package/src/lib/doctor/project.ts +60 -52
  59. package/src/lib/doctor/types.ts +1 -5
  60. package/src/lib/errors.ts +0 -12
  61. package/src/lib/load-route-map-worker.ts +19 -10
  62. package/src/lib/route-map.ts +61 -16
  63. package/template/.agents/skills/remix/SKILL.md +588 -0
  64. package/template/.agents/skills/remix/references/animate-elements.md +195 -0
  65. package/template/.agents/skills/remix/references/assets-and-browser-modules.md +130 -0
  66. package/template/.agents/skills/remix/references/auth-and-sessions.md +443 -0
  67. package/template/.agents/skills/remix/references/component-model.md +282 -0
  68. package/template/.agents/skills/remix/references/create-mixins.md +158 -0
  69. package/template/.agents/skills/remix/references/data-and-validation.md +379 -0
  70. package/template/.agents/skills/remix/references/hydration-frames-navigation.md +297 -0
  71. package/template/.agents/skills/remix/references/middleware-and-server.md +233 -0
  72. package/template/.agents/skills/remix/references/mixins-styling-events.md +213 -0
  73. package/template/.agents/skills/remix/references/routing-and-controllers.md +391 -0
  74. package/template/.agents/skills/remix/references/testing-patterns.md +172 -0
  75. package/{bootstrap → template}/AGENTS.md +13 -8
  76. package/template/README.md +29 -0
  77. package/template/app/actions/controller.tsx +18 -0
  78. package/template/app/assets/entry.ts +8 -0
  79. package/template/app/assets/prompt-button.tsx +163 -0
  80. package/template/app/assets.ts +20 -0
  81. package/template/app/middleware/render.tsx +43 -0
  82. package/template/app/router.ts +20 -0
  83. package/template/app/routes.ts +6 -0
  84. package/template/app/ui/document.tsx +33 -0
  85. package/template/app/ui/scaffold-home-page.tsx +527 -0
  86. package/template/gitignore +4 -0
  87. package/{bootstrap → template}/package.json +5 -6
  88. package/template/public/favicon.svg +11 -0
  89. package/{bootstrap → template}/server.ts +4 -5
  90. package/{bootstrap → template}/tsconfig.json +3 -3
  91. package/bootstrap/README.md +0 -27
  92. package/bootstrap/app/controllers/auth.tsx +0 -21
  93. package/bootstrap/app/controllers/home.tsx +0 -26
  94. package/bootstrap/app/router.ts +0 -10
  95. package/bootstrap/app/routes.ts +0 -6
  96. package/bootstrap/app/ui/document.tsx +0 -21
  97. package/bootstrap/app/ui/layout.tsx +0 -22
  98. package/bootstrap/app/utils/render.tsx +0 -7
  99. package/dist/lib/commands/skills.d.ts +0 -6
  100. package/dist/lib/commands/skills.d.ts.map +0 -1
  101. package/dist/lib/commands/skills.js +0 -222
  102. package/dist/lib/skills-cache.d.ts +0 -19
  103. package/dist/lib/skills-cache.d.ts.map +0 -1
  104. package/dist/lib/skills-cache.js +0 -89
  105. package/dist/lib/skills.d.ts +0 -30
  106. package/dist/lib/skills.d.ts.map +0 -1
  107. package/dist/lib/skills.js +0 -441
  108. package/src/lib/commands/skills.ts +0 -306
  109. package/src/lib/skills-cache.ts +0 -140
  110. package/src/lib/skills.ts +0 -706
@@ -62,7 +62,7 @@ export async function checkProject(projectRoot: string): Promise<ProjectDoctorRe
62
62
  export async function getProjectFixPlans(projectRoot: string): Promise<DoctorFixPlan[]> {
63
63
  let routesFile = path.join(projectRoot, 'app', 'routes.ts')
64
64
  let homeActionPath = normalizeRelativePath(
65
- path.join('app', 'controllers', `home${await inferHomeOwnerExtension(projectRoot)}`),
65
+ path.join('app', 'actions', `controller${await inferHomeOwnerExtension(projectRoot)}`),
66
66
  )
67
67
 
68
68
  if (await pathExists(routesFile)) {
@@ -225,8 +225,8 @@ function hasOnlyWhitespaceAndComments(source: string): boolean {
225
225
  }
226
226
 
227
227
  async function inferHomeOwnerExtension(projectRoot: string): Promise<OwnerFileExtension> {
228
- let controllersDir = path.join(projectRoot, 'app', 'controllers')
229
- let extensions = await collectOwnerExtensions(controllersDir)
228
+ let actionsDir = path.join(projectRoot, 'app', 'actions')
229
+ let extensions = await collectOwnerExtensions(actionsDir)
230
230
 
231
231
  if (extensions.length > 0) {
232
232
  return getMostCommonExtension(extensions)
@@ -298,7 +298,7 @@ function getMostCommonExtension(extensions: OwnerFileExtension[]): OwnerFileExte
298
298
 
299
299
  function renderDefaultRoutesFile(): string {
300
300
  return [
301
- `import { route } from 'remix/fetch-router/routes'`,
301
+ `import { route } from 'remix/routes'`,
302
302
  '',
303
303
  'export const routes = route({',
304
304
  ` home: '/',`,
@@ -318,22 +318,24 @@ function renderDefaultHomeAction(entryPath: string): string {
318
318
  `import { html } from 'remix/html-template'`,
319
319
  `import { createHtmlResponse } from 'remix/response/html'`,
320
320
  '',
321
- 'export const home = {',
322
- ' handler() {',
323
- ' let page = html`',
324
- ' <html lang="en">',
325
- ' <head>',
326
- ' <meta charset="utf-8" />',
327
- ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
328
- ' <title>Home</title>',
329
- ' </head>',
330
- ' <body>',
331
- ' <h1>Home</h1>',
332
- ' <p>Update app/routes.ts and app/controllers/home to keep building your app.</p>',
333
- ' </body>',
334
- ' </html>',
335
- ' `',
336
- ' return createHtmlResponse(page)',
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
+ ' },',
337
339
  ' },',
338
340
  '}',
339
341
  '',
@@ -342,30 +344,32 @@ function renderDefaultHomeAction(entryPath: string): string {
342
344
 
343
345
  if (extension === '.ts') {
344
346
  return [
345
- `import type { BuildAction } from 'remix/fetch-router'`,
347
+ `import { createController } from 'remix/router'`,
346
348
  `import { html } from 'remix/html-template'`,
347
349
  `import { createHtmlResponse } from 'remix/response/html'`,
348
350
  '',
349
- `import type { routes } from '../routes.ts'`,
351
+ `import { routes } from '../routes.ts'`,
350
352
  '',
351
- `export const home: BuildAction<'ANY', typeof routes.home> = {`,
352
- ' handler() {',
353
- ' let page = html`',
354
- ' <html lang="en">',
355
- ' <head>',
356
- ' <meta charset="utf-8" />',
357
- ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
358
- ' <title>Home</title>',
359
- ' </head>',
360
- ' <body>',
361
- ' <h1>Home</h1>',
362
- ' <p>Update app/routes.ts and app/controllers/home to keep building your app.</p>',
363
- ' </body>',
364
- ' </html>',
365
- ' `',
366
- ' return createHtmlResponse(page)',
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
+ ' },',
367
371
  ' },',
368
- '}',
372
+ `})`,
369
373
  '',
370
374
  ].join('\n')
371
375
  }
@@ -375,10 +379,12 @@ function renderDefaultHomeAction(entryPath: string): string {
375
379
  `import { renderToStream } from 'remix/ui/server'`,
376
380
  `import { createHtmlResponse } from 'remix/response/html'`,
377
381
  '',
378
- 'export const home = {',
379
- ' handler() {',
380
- ' let page = <HomePage />',
381
- ' return createHtmlResponse(renderToStream(page))',
382
+ 'export default {',
383
+ ' actions: {',
384
+ ' home() {',
385
+ ' let page = <HomePage />',
386
+ ' return createHtmlResponse(renderToStream(page))',
387
+ ' },',
382
388
  ' },',
383
389
  '}',
384
390
  '',
@@ -392,7 +398,7 @@ function renderDefaultHomeAction(entryPath: string): string {
392
398
  ' </head>',
393
399
  ' <body>',
394
400
  ' <h1>Home</h1>',
395
- ' <p>Update app/routes.ts and app/controllers/home to keep building your app.</p>',
401
+ ' <p>Update app/routes.ts and app/actions/controller to keep building your app.</p>',
396
402
  ' </body>',
397
403
  ' </html>',
398
404
  ' )',
@@ -402,18 +408,20 @@ function renderDefaultHomeAction(entryPath: string): string {
402
408
  }
403
409
 
404
410
  return [
405
- `import type { BuildAction } from 'remix/fetch-router'`,
411
+ `import { createController } from 'remix/router'`,
406
412
  `import { renderToStream } from 'remix/ui/server'`,
407
413
  `import { createHtmlResponse } from 'remix/response/html'`,
408
414
  '',
409
- `import type { routes } from '../routes.ts'`,
415
+ `import { routes } from '../routes.ts'`,
410
416
  '',
411
- `export const home: BuildAction<'ANY', typeof routes.home> = {`,
412
- ' handler() {',
413
- ' let page = <HomePage />',
414
- ' return createHtmlResponse(renderToStream(page))',
417
+ `export default createController(routes, {`,
418
+ ' actions: {',
419
+ ' home() {',
420
+ ' let page = <HomePage />',
421
+ ' return createHtmlResponse(renderToStream(page))',
422
+ ' },',
415
423
  ' },',
416
- '}',
424
+ `})`,
417
425
  '',
418
426
  'function HomePage() {',
419
427
  ' return () => (',
@@ -425,7 +433,7 @@ function renderDefaultHomeAction(entryPath: string): string {
425
433
  ' </head>',
426
434
  ' <body>',
427
435
  ' <h1>Home</h1>',
428
- ' <p>Update app/routes.ts and app/controllers/home to keep building your app.</p>',
436
+ ' <p>Update app/routes.ts and app/actions/controller to keep building your app.</p>',
429
437
  ' </body>',
430
438
  ' </html>',
431
439
  ' )',
@@ -1,4 +1,4 @@
1
- export type DoctorSuiteName = 'environment' | 'project' | 'controllers'
1
+ export type DoctorSuiteName = 'environment' | 'project' | 'actions'
2
2
  export type DoctorFindingSeverity = 'warn' | 'advice'
3
3
  export type DoctorSuiteStatus = 'ok' | 'issues' | 'skipped'
4
4
 
@@ -18,12 +18,8 @@ export type DoctorFindingCode =
18
18
  | 'route-map-invalid-json'
19
19
  | 'route-map-loader-signal'
20
20
  | 'missing-owner'
21
- | 'wrong-owner-kind'
22
- | 'ambiguous-owner'
23
21
  | 'duplicate-owner-file'
24
22
  | 'incomplete-controller'
25
- | 'promotion-drift'
26
- | 'orphan-action'
27
23
  | 'orphan-controller'
28
24
  | 'orphan-route-directory'
29
25
 
package/src/lib/errors.ts CHANGED
@@ -133,11 +133,6 @@ export const CLI_ERROR_DEFINITIONS = {
133
133
  title: 'Unknown help topic',
134
134
  fix: 'Run `remix help` to see available commands and help topics.',
135
135
  },
136
- unknownSkillsCommand: {
137
- code: 'RMX_UNKNOWN_SKILLS_COMMAND',
138
- title: 'Unknown skills command',
139
- fix: 'Run `remix skills --help` to see available skills commands.',
140
- },
141
136
  } satisfies Record<string, CliErrorDefinition>
142
137
 
143
138
  export class CliError extends Error {
@@ -321,13 +316,6 @@ export function unknownHelpTopic(topic: string): UsageError {
321
316
  })
322
317
  }
323
318
 
324
- export function unknownSkillsCommand(command: string): UsageError {
325
- return createUsageError(CLI_ERROR_DEFINITIONS.unknownSkillsCommand, {
326
- context: { command },
327
- message: `Unknown skills command: ${command}`,
328
- })
329
- }
330
-
331
319
  export function unexpectedExtraArgument(argument: string): UsageError {
332
320
  return createUsageError(CLI_ERROR_DEFINITIONS.unexpectedExtraArgument, {
333
321
  context: { argument },
@@ -24,13 +24,13 @@ async function run(): Promise<void> {
24
24
  }
25
25
 
26
26
  async function loadRawRouteTree(routesFile: string): Promise<RawRouteTreeNode[]> {
27
- let routeModule = await import(pathToFileURL(routesFile).href)
27
+ let routeModule: object = await import(pathToFileURL(routesFile).href)
28
28
 
29
29
  if (!('routes' in routeModule)) {
30
30
  throw new Error(`Route module ${routesFile} must export a named "routes" value.`)
31
31
  }
32
32
 
33
- return normalizeRouteGroup(Reflect.get(routeModule, 'routes'), [], new Map())
33
+ return normalizeRouteGroup(routeModule.routes, [], new Map())
34
34
  }
35
35
 
36
36
  function normalizeRouteGroup(
@@ -55,15 +55,16 @@ function normalizeRouteGroup(
55
55
  return Object.entries(value).map(([key, entryValue]) => {
56
56
  let nameSegments = [...parentSegments, key]
57
57
  let name = nameSegments.join('.')
58
+ let routeLeaf = readRouteLeaf(entryValue)
58
59
 
59
- if (isRouteLeaf(entryValue)) {
60
+ if (routeLeaf !== undefined) {
60
61
  return {
61
62
  children: [],
62
63
  key,
63
64
  kind: 'route',
64
- method: entryValue.method,
65
+ method: routeLeaf.method,
65
66
  name,
66
- pattern: entryValue.pattern.source,
67
+ pattern: routeLeaf.pattern,
67
68
  } satisfies RawRouteTreeNode
68
69
  }
69
70
 
@@ -85,14 +86,22 @@ function normalizeRouteGroup(
85
86
  }
86
87
  }
87
88
 
88
- function isRouteLeaf(value: unknown): value is { method: string; pattern: { source: string } } {
89
+ function readRouteLeaf(value: unknown): { method: string; pattern: string } | undefined {
90
+ if (typeof value !== 'object' || value == null) return undefined
91
+
92
+ let method = Reflect.get(value, 'method')
93
+ let pattern = Reflect.get(value, 'pattern')
94
+ if (typeof method !== 'string' || !hasToString(pattern)) return undefined
95
+
96
+ return { method, pattern: pattern.toString() }
97
+ }
98
+
99
+ function hasToString(value: unknown): value is { toString(): string } {
89
100
  return (
90
101
  typeof value === 'object' &&
91
102
  value != null &&
92
- typeof Reflect.get(value, 'method') === 'string' &&
93
- typeof Reflect.get(value, 'pattern') === 'object' &&
94
- Reflect.get(value, 'pattern') != null &&
95
- typeof Reflect.get(Reflect.get(value, 'pattern'), 'source') === 'string'
103
+ 'toString' in value &&
104
+ typeof value.toString === 'function'
96
105
  )
97
106
  }
98
107
 
@@ -4,7 +4,13 @@ import * as process from 'node:process'
4
4
  import { spawn } from 'node:child_process'
5
5
  import { fileURLToPath } from 'node:url'
6
6
 
7
- import { inspectControllerOwnership, type OwnedSubtree } from './controller-ownership.ts'
7
+ import {
8
+ ROOT_ROUTE_NAME,
9
+ inspectControllerOwnership,
10
+ type ControllerOwnership,
11
+ type OwnedSubtree,
12
+ type RouteDirectoryPlan,
13
+ } from './controller-ownership.ts'
8
14
  import {
9
15
  routeMapLoaderFailed,
10
16
  routeMapLoaderInvalidJson,
@@ -13,7 +19,7 @@ import {
13
19
  routesFileNotFound,
14
20
  } from './errors.ts'
15
21
 
16
- export type RouteOwnerKind = 'action' | 'controller'
22
+ export type RouteOwnerKind = 'controller' | 'directory'
17
23
  export type RouteTreeNodeKind = 'group' | 'route'
18
24
 
19
25
  export interface RouteTreeOwner {
@@ -56,7 +62,7 @@ export interface RawRouteTreeNode {
56
62
  export async function loadRouteMap(cwd: string = process.cwd()): Promise<LoadedRouteMap> {
57
63
  let manifest = await loadRouteManifest(cwd)
58
64
  let ownership = await inspectControllerOwnership(manifest.appRoot, manifest.tree)
59
- let tree = decorateRouteTree(manifest.tree, ownership.subtrees)
65
+ let tree = decorateRouteTree(manifest.tree, ownership)
60
66
 
61
67
  return {
62
68
  appRoot: manifest.appRoot,
@@ -137,28 +143,51 @@ async function loadRawRouteMap(appRoot: string, routesFile: string): Promise<Raw
137
143
 
138
144
  function decorateRouteTree(
139
145
  rawTree: RawRouteTreeNode[],
140
- subtrees: OwnedSubtree[],
141
- parentSegments: string[] = [],
146
+ ownership: ControllerOwnership,
142
147
  ): RouteTreeNode[] {
143
- let subtreesByRouteName = new Map(subtrees.map((subtree) => [subtree.routeName, subtree]))
148
+ let subtreesByRouteName = new Map(
149
+ ownership.subtrees.map((subtree) => [subtree.routeName, subtree]),
150
+ )
151
+ let directoriesByRouteName = new Map(
152
+ ownership.routeDirectories.map((directory) => [directory.routeName, directory]),
153
+ )
144
154
 
145
- return decorateRouteTreeWithLookup(rawTree, subtreesByRouteName, parentSegments)
155
+ return decorateRouteTreeWithLookup(
156
+ rawTree,
157
+ subtreesByRouteName,
158
+ directoriesByRouteName,
159
+ ownership.scan.routeDirectoryPaths,
160
+ )
146
161
  }
147
162
 
148
163
  function decorateRouteTreeWithLookup(
149
164
  rawTree: RawRouteTreeNode[],
150
165
  subtreesByRouteName: Map<string, OwnedSubtree>,
166
+ directoriesByRouteName: Map<string, RouteDirectoryPlan>,
167
+ actualRouteDirectories: Set<string>,
151
168
  parentSegments: string[] = [],
152
169
  ): RouteTreeNode[] {
153
170
  return rawTree.map((rawNode) => {
154
- let owner = getRouteOwner(rawNode, parentSegments, subtreesByRouteName)
171
+ let owner = getRouteOwner(
172
+ rawNode,
173
+ parentSegments,
174
+ subtreesByRouteName,
175
+ directoriesByRouteName,
176
+ actualRouteDirectories,
177
+ )
155
178
  let nextParentSegments =
156
179
  rawNode.kind === 'group' ? [...parentSegments, rawNode.key] : parentSegments
157
180
 
158
181
  return {
159
182
  children:
160
183
  rawNode.kind === 'group'
161
- ? decorateRouteTreeWithLookup(rawNode.children, subtreesByRouteName, nextParentSegments)
184
+ ? decorateRouteTreeWithLookup(
185
+ rawNode.children,
186
+ subtreesByRouteName,
187
+ directoriesByRouteName,
188
+ actualRouteDirectories,
189
+ nextParentSegments,
190
+ )
162
191
  : [],
163
192
  key: rawNode.key,
164
193
  kind: rawNode.kind,
@@ -174,24 +203,40 @@ function getRouteOwner(
174
203
  rawNode: RawRouteTreeNode,
175
204
  parentSegments: string[],
176
205
  subtreesByRouteName: Map<string, OwnedSubtree>,
206
+ directoriesByRouteName: Map<string, RouteDirectoryPlan>,
207
+ actualRouteDirectories: Set<string>,
177
208
  ): RouteTreeOwner {
178
209
  let ownerRouteName =
179
210
  rawNode.kind === 'group'
180
211
  ? rawNode.name
181
212
  : parentSegments.length === 0
182
- ? rawNode.name
213
+ ? ROOT_ROUTE_NAME
183
214
  : parentSegments.join('.')
184
215
  let subtree = subtreesByRouteName.get(ownerRouteName)
185
216
 
186
- if (subtree == null) {
187
- throw routeOwnerPlanUnresolved(rawNode.name)
217
+ if (subtree != null) {
218
+ return {
219
+ exists: subtree.actualEntryPath != null,
220
+ kind: 'controller',
221
+ path: subtree.actualEntryPath ?? subtree.entryDisplayPath,
222
+ }
188
223
  }
189
224
 
190
- return {
191
- exists: subtree.actualEntryPath != null,
192
- kind: subtree.kind,
193
- path: subtree.actualEntryPath ?? subtree.entryDisplayPath,
225
+ if (rawNode.kind === 'group') {
226
+ let directory = directoriesByRouteName.get(rawNode.name)
227
+
228
+ if (directory == null) {
229
+ throw routeOwnerPlanUnresolved(rawNode.name)
230
+ }
231
+
232
+ return {
233
+ exists: actualRouteDirectories.has(directory.directoryPath),
234
+ kind: 'directory',
235
+ path: directory.directoryPath,
236
+ }
194
237
  }
238
+
239
+ throw routeOwnerPlanUnresolved(rawNode.name)
195
240
  }
196
241
 
197
242
  function assertRawRouteTree(value: unknown): RawRouteTreeNode[] {