@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,92 +1,13 @@
1
1
  import * as path from 'node:path';
2
2
  import { getOwnerFileExtension } from "../controller-files.js";
3
- const RESERVED_BINDING_IDENTIFIERS = new Set([
4
- 'await',
5
- 'break',
6
- 'case',
7
- 'catch',
8
- 'class',
9
- 'const',
10
- 'continue',
11
- 'debugger',
12
- 'default',
13
- 'delete',
14
- 'do',
15
- 'else',
16
- 'enum',
17
- 'export',
18
- 'extends',
19
- 'false',
20
- 'finally',
21
- 'for',
22
- 'function',
23
- 'if',
24
- 'implements',
25
- 'import',
26
- 'in',
27
- 'instanceof',
28
- 'interface',
29
- 'let',
30
- 'new',
31
- 'null',
32
- 'package',
33
- 'private',
34
- 'protected',
35
- 'public',
36
- 'return',
37
- 'static',
38
- 'super',
39
- 'switch',
40
- 'this',
41
- 'throw',
42
- 'true',
43
- 'try',
44
- 'typeof',
45
- 'var',
46
- 'void',
47
- 'while',
48
- 'with',
49
- 'yield',
50
- ]);
51
- export function renderActionPlaceholder(routeNode, entryPath) {
52
- let routeMessage = `TODO: implement routes.${routeNode.name}`;
53
- let extension = getOwnerFileExtension(entryPath);
54
- let exportName = getActionExportName(routeNode.key);
55
- if (extension === '.js' || extension === '.jsx') {
56
- return [
57
- `export const ${exportName} = {`,
58
- ` handler() {`,
59
- ` return new Response(${JSON.stringify(routeMessage)})`,
60
- ` },`,
61
- `}`,
62
- '',
63
- ].join('\n');
64
- }
65
- let routeExpression = getRouteAccessExpression(routeNode.name);
66
- let routesImportPath = getRelativeImportPath(entryPath, 'app/routes.ts');
67
- let method = routeNode.method ?? 'ANY';
68
- return [
69
- `import type { BuildAction } from 'remix/fetch-router'`,
70
- '',
71
- `import type { routes } from '${routesImportPath}'`,
72
- '',
73
- `export const ${exportName}: BuildAction<'${method}', typeof ${routeExpression}> = {`,
74
- ` handler() {`,
75
- ` return new Response(${JSON.stringify(routeMessage)})`,
76
- ` },`,
77
- `}`,
78
- '',
79
- ].join('\n');
80
- }
81
- export function renderControllerPlaceholder(routeNode, entryPath, resolvedEntryPathByRouteName) {
3
+ import { ROOT_ROUTE_NAME } from "../controller-ownership.js";
4
+ export function renderControllerPlaceholder(routeNode, entryPath) {
82
5
  let extension = getOwnerFileExtension(entryPath);
83
6
  let lines = [];
84
- let childImports = getControllerChildImports(routeNode, entryPath, resolvedEntryPathByRouteName);
85
- let actionEntries = routeNode.children.map((childNode) => renderControllerActionEntry(childNode, entryPath, resolvedEntryPathByRouteName));
7
+ let actionEntries = routeNode.children
8
+ .filter((childNode) => childNode.kind === 'route')
9
+ .map((childNode) => renderControllerActionEntry(childNode));
86
10
  if (extension === '.js' || extension === '.jsx') {
87
- if (childImports.length > 0) {
88
- lines.push(...childImports, '');
89
- }
90
11
  lines.push('export default {', ' actions: {');
91
12
  lines.push(...actionEntries);
92
13
  lines.push(' },', '}', '');
@@ -94,57 +15,24 @@ export function renderControllerPlaceholder(routeNode, entryPath, resolvedEntryP
94
15
  }
95
16
  let routeExpression = getRouteAccessExpression(routeNode.name);
96
17
  let routesImportPath = getRelativeImportPath(entryPath, 'app/routes.ts');
97
- lines.push(`import type { Controller } from 'remix/fetch-router'`, '');
98
- lines.push(`import type { routes } from '${routesImportPath}'`);
99
- if (childImports.length > 0) {
100
- lines.push('', ...childImports);
101
- }
102
- lines.push('', 'export default {', ' actions: {');
18
+ lines.push(`import { createController } from 'remix/router'`, '');
19
+ lines.push(`import { routes } from '${routesImportPath}'`);
20
+ lines.push('', `export default createController(${routeExpression}, {`, ' actions: {');
103
21
  lines.push(...actionEntries);
104
- lines.push(' },', `} satisfies Controller<typeof ${routeExpression}>`, '');
22
+ lines.push(' },', '})', '');
105
23
  return lines.join('\n');
106
24
  }
107
- function getControllerChildImports(routeNode, entryPath, resolvedEntryPathByRouteName) {
108
- let imports = [];
109
- for (let childNode of routeNode.children) {
110
- if (childNode.kind !== 'group') {
111
- continue;
112
- }
113
- let childEntryPath = resolvedEntryPathByRouteName.get(childNode.name);
114
- if (childEntryPath == null) {
115
- continue;
116
- }
117
- imports.push(`import ${getControllerImportName(childNode.key)} from '${getRelativeImportPath(entryPath, childEntryPath)}'`);
118
- }
119
- return imports;
120
- }
121
- function renderControllerActionEntry(childNode, entryPath, resolvedEntryPathByRouteName) {
122
- if (childNode.kind === 'route') {
123
- return [
124
- ` ${formatObjectKey(childNode.key)}() {`,
125
- ` return new Response(${JSON.stringify(`TODO: implement routes.${childNode.name}`)})`,
126
- ` },`,
127
- ].join('\n');
128
- }
129
- let childEntryPath = resolvedEntryPathByRouteName.get(childNode.name);
130
- if (childEntryPath != null) {
131
- return ` ${formatObjectKey(childNode.key)}: ${getControllerImportName(childNode.key)},`;
132
- }
133
- return ` ${formatObjectKey(childNode.key)}: ${renderInlineControllerPlaceholder(childNode)},`;
134
- }
135
- function renderInlineControllerPlaceholder(routeNode) {
136
- let childEntries = routeNode.children.map((childNode) => childNode.kind === 'route'
137
- ? `${formatObjectKey(childNode.key)}() { return new Response(${JSON.stringify(`TODO: implement routes.${childNode.name}`)}) }`
138
- : `${formatObjectKey(childNode.key)}: ${renderInlineControllerPlaceholder(childNode)}`);
139
- return `{ actions: { ${childEntries.join(', ')} } }`;
140
- }
141
- function getControllerImportName(key) {
142
- return `${toIdentifier(key)}Controller`;
143
- }
144
- function getActionExportName(key) {
145
- return toIdentifier(key);
25
+ function renderControllerActionEntry(childNode) {
26
+ return [
27
+ ` ${formatObjectKey(childNode.key)}() {`,
28
+ ` return new Response(${JSON.stringify(`TODO: implement routes.${childNode.name}`)})`,
29
+ ` },`,
30
+ ].join('\n');
146
31
  }
147
32
  function getRouteAccessExpression(routeName) {
33
+ if (routeName === ROOT_ROUTE_NAME) {
34
+ return 'routes';
35
+ }
148
36
  let segments = routeName.split('.');
149
37
  let expression = 'routes';
150
38
  for (let segment of segments) {
@@ -159,25 +47,6 @@ function getRelativeImportPath(fromFilePath, toFilePath) {
159
47
  function formatObjectKey(key) {
160
48
  return isIdentifier(key) ? key : JSON.stringify(key);
161
49
  }
162
- function toIdentifier(value) {
163
- let parts = value.split(/[^A-Za-z0-9_$]+/).filter(Boolean);
164
- if (parts.length === 0) {
165
- return 'route';
166
- }
167
- let identifier = parts
168
- .map((part, index) => index === 0 ? part.replace(/^./, (char) => char.toLowerCase()) : capitalize(part))
169
- .join('');
170
- if (!/^[A-Za-z_$]/.test(identifier)) {
171
- return `route${capitalize(identifier)}`;
172
- }
173
- if (RESERVED_BINDING_IDENTIFIERS.has(identifier)) {
174
- return `route${capitalize(identifier)}`;
175
- }
176
- return identifier;
177
- }
178
- function capitalize(value) {
179
- return value.length === 0 ? value : `${value[0].toUpperCase()}${value.slice(1)}`;
180
- }
181
50
  function isIdentifier(value) {
182
51
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value);
183
52
  }
@@ -8,6 +8,6 @@ export async function checkControllerConventions(appRoot, tree) {
8
8
  let findings = getControllerFindings(ownership, fixPlans);
9
9
  return {
10
10
  fixPlans,
11
- suite: createDoctorSuite('controllers', findings),
11
+ suite: createDoctorSuite('actions', findings),
12
12
  };
13
13
  }
@@ -39,7 +39,7 @@ export async function checkProject(projectRoot) {
39
39
  }
40
40
  export async function getProjectFixPlans(projectRoot) {
41
41
  let routesFile = path.join(projectRoot, 'app', 'routes.ts');
42
- let homeActionPath = normalizeRelativePath(path.join('app', 'controllers', `home${await inferHomeOwnerExtension(projectRoot)}`));
42
+ let homeActionPath = normalizeRelativePath(path.join('app', 'actions', `controller${await inferHomeOwnerExtension(projectRoot)}`));
43
43
  if (await pathExists(routesFile)) {
44
44
  let routesSource = await fs.readFile(routesFile, 'utf8');
45
45
  if (!hasOnlyWhitespaceAndComments(routesSource)) {
@@ -181,8 +181,8 @@ function hasOnlyWhitespaceAndComments(source) {
181
181
  return true;
182
182
  }
183
183
  async function inferHomeOwnerExtension(projectRoot) {
184
- let controllersDir = path.join(projectRoot, 'app', 'controllers');
185
- let extensions = await collectOwnerExtensions(controllersDir);
184
+ let actionsDir = path.join(projectRoot, 'app', 'actions');
185
+ let extensions = await collectOwnerExtensions(actionsDir);
186
186
  if (extensions.length > 0) {
187
187
  return getMostCommonExtension(extensions);
188
188
  }
@@ -239,7 +239,7 @@ function getMostCommonExtension(extensions) {
239
239
  }
240
240
  function renderDefaultRoutesFile() {
241
241
  return [
242
- `import { route } from 'remix/fetch-router/routes'`,
242
+ `import { route } from 'remix/routes'`,
243
243
  '',
244
244
  'export const routes = route({',
245
245
  ` home: '/',`,
@@ -257,22 +257,24 @@ function renderDefaultHomeAction(entryPath) {
257
257
  `import { html } from 'remix/html-template'`,
258
258
  `import { createHtmlResponse } from 'remix/response/html'`,
259
259
  '',
260
- 'export const home = {',
261
- ' handler() {',
262
- ' let page = html`',
263
- ' <html lang="en">',
264
- ' <head>',
265
- ' <meta charset="utf-8" />',
266
- ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
267
- ' <title>Home</title>',
268
- ' </head>',
269
- ' <body>',
270
- ' <h1>Home</h1>',
271
- ' <p>Update app/routes.ts and app/controllers/home to keep building your app.</p>',
272
- ' </body>',
273
- ' </html>',
274
- ' `',
275
- ' return createHtmlResponse(page)',
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
+ ' },',
276
278
  ' },',
277
279
  '}',
278
280
  '',
@@ -280,30 +282,32 @@ function renderDefaultHomeAction(entryPath) {
280
282
  }
281
283
  if (extension === '.ts') {
282
284
  return [
283
- `import type { BuildAction } from 'remix/fetch-router'`,
285
+ `import { createController } from 'remix/router'`,
284
286
  `import { html } from 'remix/html-template'`,
285
287
  `import { createHtmlResponse } from 'remix/response/html'`,
286
288
  '',
287
- `import type { routes } from '../routes.ts'`,
289
+ `import { routes } from '../routes.ts'`,
288
290
  '',
289
- `export const home: BuildAction<'ANY', typeof routes.home> = {`,
290
- ' handler() {',
291
- ' let page = html`',
292
- ' <html lang="en">',
293
- ' <head>',
294
- ' <meta charset="utf-8" />',
295
- ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
296
- ' <title>Home</title>',
297
- ' </head>',
298
- ' <body>',
299
- ' <h1>Home</h1>',
300
- ' <p>Update app/routes.ts and app/controllers/home to keep building your app.</p>',
301
- ' </body>',
302
- ' </html>',
303
- ' `',
304
- ' return createHtmlResponse(page)',
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
+ ' },',
305
309
  ' },',
306
- '}',
310
+ `})`,
307
311
  '',
308
312
  ].join('\n');
309
313
  }
@@ -312,10 +316,12 @@ function renderDefaultHomeAction(entryPath) {
312
316
  `import { renderToStream } from 'remix/ui/server'`,
313
317
  `import { createHtmlResponse } from 'remix/response/html'`,
314
318
  '',
315
- 'export const home = {',
316
- ' handler() {',
317
- ' let page = <HomePage />',
318
- ' return createHtmlResponse(renderToStream(page))',
319
+ 'export default {',
320
+ ' actions: {',
321
+ ' home() {',
322
+ ' let page = <HomePage />',
323
+ ' return createHtmlResponse(renderToStream(page))',
324
+ ' },',
319
325
  ' },',
320
326
  '}',
321
327
  '',
@@ -329,7 +335,7 @@ function renderDefaultHomeAction(entryPath) {
329
335
  ' </head>',
330
336
  ' <body>',
331
337
  ' <h1>Home</h1>',
332
- ' <p>Update app/routes.ts and app/controllers/home to keep building your app.</p>',
338
+ ' <p>Update app/routes.ts and app/actions/controller to keep building your app.</p>',
333
339
  ' </body>',
334
340
  ' </html>',
335
341
  ' )',
@@ -338,18 +344,20 @@ function renderDefaultHomeAction(entryPath) {
338
344
  ].join('\n');
339
345
  }
340
346
  return [
341
- `import type { BuildAction } from 'remix/fetch-router'`,
347
+ `import { createController } from 'remix/router'`,
342
348
  `import { renderToStream } from 'remix/ui/server'`,
343
349
  `import { createHtmlResponse } from 'remix/response/html'`,
344
350
  '',
345
- `import type { routes } from '../routes.ts'`,
351
+ `import { routes } from '../routes.ts'`,
346
352
  '',
347
- `export const home: BuildAction<'ANY', typeof routes.home> = {`,
348
- ' handler() {',
349
- ' let page = <HomePage />',
350
- ' return createHtmlResponse(renderToStream(page))',
353
+ `export default createController(routes, {`,
354
+ ' actions: {',
355
+ ' home() {',
356
+ ' let page = <HomePage />',
357
+ ' return createHtmlResponse(renderToStream(page))',
358
+ ' },',
351
359
  ' },',
352
- '}',
360
+ `})`,
353
361
  '',
354
362
  'function HomePage() {',
355
363
  ' return () => (',
@@ -361,7 +369,7 @@ function renderDefaultHomeAction(entryPath) {
361
369
  ' </head>',
362
370
  ' <body>',
363
371
  ' <h1>Home</h1>',
364
- ' <p>Update app/routes.ts and app/controllers/home to keep building your app.</p>',
372
+ ' <p>Update app/routes.ts and app/actions/controller to keep building your app.</p>',
365
373
  ' </body>',
366
374
  ' </html>',
367
375
  ' )',
@@ -1,7 +1,7 @@
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
- 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' | 'wrong-owner-kind' | 'ambiguous-owner' | 'duplicate-owner-file' | 'incomplete-controller' | 'promotion-drift' | 'orphan-action' | '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' | 'duplicate-owner-file' | 'incomplete-controller' | 'orphan-controller' | 'orphan-route-directory';
5
5
  export type DoctorFixKind = 'create-directory' | 'create-file' | 'update-file';
6
6
  export interface DoctorFinding {
7
7
  actualPath?: string;
@@ -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,aAAa,CAAA;AACvE,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,kBAAkB,GAClB,iBAAiB,GACjB,sBAAsB,GACtB,uBAAuB,GACvB,iBAAiB,GACjB,eAAe,GACf,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,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"}
@@ -30,7 +30,7 @@ async function loadRawRouteTree(routesFile) {
30
30
  if (!('routes' in routeModule)) {
31
31
  throw new Error(`Route module ${routesFile} must export a named "routes" value.`);
32
32
  }
33
- return normalizeRouteGroup(Reflect.get(routeModule, 'routes'), [], new Map());
33
+ return normalizeRouteGroup(routeModule.routes, [], new Map());
34
34
  }
35
35
  function normalizeRouteGroup(value, parentSegments, seen) {
36
36
  if (!isPlainObject(value)) {
@@ -47,14 +47,15 @@ function normalizeRouteGroup(value, parentSegments, seen) {
47
47
  return Object.entries(value).map(([key, entryValue]) => {
48
48
  let nameSegments = [...parentSegments, key];
49
49
  let name = nameSegments.join('.');
50
- if (isRouteLeaf(entryValue)) {
50
+ let routeLeaf = readRouteLeaf(entryValue);
51
+ if (routeLeaf !== undefined) {
51
52
  return {
52
53
  children: [],
53
54
  key,
54
55
  kind: 'route',
55
- method: entryValue.method,
56
+ method: routeLeaf.method,
56
57
  name,
57
- pattern: entryValue.pattern.source,
58
+ pattern: routeLeaf.pattern,
58
59
  };
59
60
  }
60
61
  if (isPlainObject(entryValue)) {
@@ -72,13 +73,20 @@ function normalizeRouteGroup(value, parentSegments, seen) {
72
73
  seen.delete(value);
73
74
  }
74
75
  }
75
- function isRouteLeaf(value) {
76
+ function readRouteLeaf(value) {
77
+ if (typeof value !== 'object' || value == null)
78
+ return undefined;
79
+ let method = Reflect.get(value, 'method');
80
+ let pattern = Reflect.get(value, 'pattern');
81
+ if (typeof method !== 'string' || !hasToString(pattern))
82
+ return undefined;
83
+ return { method, pattern: pattern.toString() };
84
+ }
85
+ function hasToString(value) {
76
86
  return (typeof value === 'object' &&
77
87
  value != null &&
78
- typeof Reflect.get(value, 'method') === 'string' &&
79
- typeof Reflect.get(value, 'pattern') === 'object' &&
80
- Reflect.get(value, 'pattern') != null &&
81
- typeof Reflect.get(Reflect.get(value, 'pattern'), 'source') === 'string');
88
+ 'toString' in value &&
89
+ typeof value.toString === 'function');
82
90
  }
83
91
  function isPlainObject(value) {
84
92
  if (typeof value !== 'object' || value == null) {
@@ -1,4 +1,4 @@
1
- export type RouteOwnerKind = 'action' | 'controller';
1
+ export type RouteOwnerKind = 'controller' | 'directory';
2
2
  export type RouteTreeNodeKind = 'group' | 'route';
3
3
  export interface RouteTreeOwner {
4
4
  exists: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"route-map.d.ts","sourceRoot":"","sources":["../../src/lib/route-map.ts"],"names":[],"mappings":"AAeA,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,YAAY,CAAA;AACpD,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,OAAO,CAAA;AAEjD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,OAAO,CAAA;IACf,IAAI,EAAE,cAAc,CAAA;IACpB,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,aAAa,EAAE,CAAA;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,iBAAiB,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,cAAc,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,aAAa,EAAE,CAAA;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,gBAAgB,EAAE,CAAA;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,gBAAgB,EAAE,CAAA;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,iBAAiB,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,wBAAsB,YAAY,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,cAAc,CAAC,CAUvF;AAED,wBAAsB,iBAAiB,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAGjG;AAED,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAShG"}
1
+ {"version":3,"file":"route-map.d.ts","sourceRoot":"","sources":["../../src/lib/route-map.ts"],"names":[],"mappings":"AAqBA,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,WAAW,CAAA;AACvD,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,OAAO,CAAA;AAEjD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,OAAO,CAAA;IACf,IAAI,EAAE,cAAc,CAAA;IACpB,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,aAAa,EAAE,CAAA;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,iBAAiB,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,cAAc,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,aAAa,EAAE,CAAA;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,gBAAgB,EAAE,CAAA;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,gBAAgB,EAAE,CAAA;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,iBAAiB,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,wBAAsB,YAAY,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,cAAc,CAAC,CAUvF;AAED,wBAAsB,iBAAiB,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAGjG;AAED,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAShG"}
@@ -3,12 +3,12 @@ import * as path from 'node:path';
3
3
  import * as process from 'node:process';
4
4
  import { spawn } from 'node:child_process';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { inspectControllerOwnership } from "./controller-ownership.js";
6
+ import { ROOT_ROUTE_NAME, inspectControllerOwnership, } from "./controller-ownership.js";
7
7
  import { routeMapLoaderFailed, routeMapLoaderInvalidJson, routeMapLoaderSignal, routeOwnerPlanUnresolved, routesFileNotFound, } from "./errors.js";
8
8
  export async function loadRouteMap(cwd = process.cwd()) {
9
9
  let manifest = await loadRouteManifest(cwd);
10
10
  let ownership = await inspectControllerOwnership(manifest.appRoot, manifest.tree);
11
- let tree = decorateRouteTree(manifest.tree, ownership.subtrees);
11
+ let tree = decorateRouteTree(manifest.tree, ownership);
12
12
  return {
13
13
  appRoot: manifest.appRoot,
14
14
  routesFile: manifest.routesFile,
@@ -70,17 +70,18 @@ async function loadRawRouteMap(appRoot, routesFile) {
70
70
  }
71
71
  return assertRawRouteTree(parsed);
72
72
  }
73
- function decorateRouteTree(rawTree, subtrees, parentSegments = []) {
74
- let subtreesByRouteName = new Map(subtrees.map((subtree) => [subtree.routeName, subtree]));
75
- return decorateRouteTreeWithLookup(rawTree, subtreesByRouteName, parentSegments);
73
+ function decorateRouteTree(rawTree, ownership) {
74
+ let subtreesByRouteName = new Map(ownership.subtrees.map((subtree) => [subtree.routeName, subtree]));
75
+ let directoriesByRouteName = new Map(ownership.routeDirectories.map((directory) => [directory.routeName, directory]));
76
+ return decorateRouteTreeWithLookup(rawTree, subtreesByRouteName, directoriesByRouteName, ownership.scan.routeDirectoryPaths);
76
77
  }
77
- function decorateRouteTreeWithLookup(rawTree, subtreesByRouteName, parentSegments = []) {
78
+ function decorateRouteTreeWithLookup(rawTree, subtreesByRouteName, directoriesByRouteName, actualRouteDirectories, parentSegments = []) {
78
79
  return rawTree.map((rawNode) => {
79
- let owner = getRouteOwner(rawNode, parentSegments, subtreesByRouteName);
80
+ let owner = getRouteOwner(rawNode, parentSegments, subtreesByRouteName, directoriesByRouteName, actualRouteDirectories);
80
81
  let nextParentSegments = rawNode.kind === 'group' ? [...parentSegments, rawNode.key] : parentSegments;
81
82
  return {
82
83
  children: rawNode.kind === 'group'
83
- ? decorateRouteTreeWithLookup(rawNode.children, subtreesByRouteName, nextParentSegments)
84
+ ? decorateRouteTreeWithLookup(rawNode.children, subtreesByRouteName, directoriesByRouteName, actualRouteDirectories, nextParentSegments)
84
85
  : [],
85
86
  key: rawNode.key,
86
87
  kind: rawNode.kind,
@@ -91,21 +92,32 @@ function decorateRouteTreeWithLookup(rawTree, subtreesByRouteName, parentSegment
91
92
  };
92
93
  });
93
94
  }
94
- function getRouteOwner(rawNode, parentSegments, subtreesByRouteName) {
95
+ function getRouteOwner(rawNode, parentSegments, subtreesByRouteName, directoriesByRouteName, actualRouteDirectories) {
95
96
  let ownerRouteName = rawNode.kind === 'group'
96
97
  ? rawNode.name
97
98
  : parentSegments.length === 0
98
- ? rawNode.name
99
+ ? ROOT_ROUTE_NAME
99
100
  : parentSegments.join('.');
100
101
  let subtree = subtreesByRouteName.get(ownerRouteName);
101
- if (subtree == null) {
102
- throw routeOwnerPlanUnresolved(rawNode.name);
102
+ if (subtree != null) {
103
+ return {
104
+ exists: subtree.actualEntryPath != null,
105
+ kind: 'controller',
106
+ path: subtree.actualEntryPath ?? subtree.entryDisplayPath,
107
+ };
103
108
  }
104
- return {
105
- exists: subtree.actualEntryPath != null,
106
- kind: subtree.kind,
107
- path: subtree.actualEntryPath ?? subtree.entryDisplayPath,
108
- };
109
+ if (rawNode.kind === 'group') {
110
+ let directory = directoriesByRouteName.get(rawNode.name);
111
+ if (directory == null) {
112
+ throw routeOwnerPlanUnresolved(rawNode.name);
113
+ }
114
+ return {
115
+ exists: actualRouteDirectories.has(directory.directoryPath),
116
+ kind: 'directory',
117
+ path: directory.directoryPath,
118
+ };
119
+ }
120
+ throw routeOwnerPlanUnresolved(rawNode.name);
109
121
  }
110
122
  function assertRawRouteTree(value) {
111
123
  if (!Array.isArray(value)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remix-run/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Command-line interface for Remix",
5
5
  "author": "Michael Jackson <mjijackson@gmail.com>",
6
6
  "license": "MIT",
@@ -11,11 +11,11 @@
11
11
  },
12
12
  "homepage": "https://github.com/remix-run/remix/tree/main/packages/cli#readme",
13
13
  "files": [
14
- "bootstrap",
15
14
  "LICENSE",
16
15
  "README.md",
17
16
  "dist",
18
17
  "src",
18
+ "template",
19
19
  "!src/**/*.test.ts"
20
20
  ],
21
21
  "type": "module",
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "semver": "^7.7.4",
34
- "@remix-run/test": "^0.3.0",
34
+ "@remix-run/test": "^0.4.0",
35
35
  "@remix-run/terminal": "^0.1.0"
36
36
  },
37
37
  "devDependencies": {