@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.
- package/dist/lib/bootstrap-project.d.ts.map +1 -1
- package/dist/lib/bootstrap-project.js +32 -9
- package/dist/lib/cli.d.ts +29 -0
- package/dist/lib/cli.d.ts.map +1 -1
- package/dist/lib/cli.js +26 -4
- package/dist/lib/commands/completion.d.ts.map +1 -1
- package/dist/lib/commands/completion.js +5 -1
- package/dist/lib/commands/doctor.js +18 -18
- package/dist/lib/commands/routes.js +3 -3
- package/dist/lib/completion.js +1 -5
- package/dist/lib/controller-files.d.ts +0 -1
- package/dist/lib/controller-files.d.ts.map +1 -1
- package/dist/lib/controller-files.js +3 -5
- package/dist/lib/controller-ownership.d.ts +9 -9
- package/dist/lib/controller-ownership.d.ts.map +1 -1
- package/dist/lib/controller-ownership.js +56 -91
- package/dist/lib/doctor/controller-findings.d.ts +1 -1
- package/dist/lib/doctor/controller-findings.d.ts.map +1 -1
- package/dist/lib/doctor/controller-findings.js +15 -87
- package/dist/lib/doctor/controller-fix-plans.d.ts.map +1 -1
- package/dist/lib/doctor/controller-fix-plans.js +13 -24
- package/dist/lib/doctor/controller-placeholders.d.ts +2 -3
- package/dist/lib/doctor/controller-placeholders.d.ts.map +1 -1
- package/dist/lib/doctor/controller-placeholders.js +18 -149
- package/dist/lib/doctor/controllers.js +1 -1
- package/dist/lib/doctor/project.js +60 -52
- package/dist/lib/doctor/types.d.ts +2 -2
- package/dist/lib/doctor/types.d.ts.map +1 -1
- package/dist/lib/load-route-map-worker.js +17 -9
- package/dist/lib/route-map.d.ts +1 -1
- package/dist/lib/route-map.d.ts.map +1 -1
- package/dist/lib/route-map.js +29 -17
- package/package.json +3 -3
- package/src/lib/bootstrap-project.ts +39 -13
- package/src/lib/cli.ts +39 -4
- package/src/lib/commands/completion.ts +6 -1
- package/src/lib/commands/doctor.ts +18 -21
- package/src/lib/commands/routes.ts +3 -3
- package/src/lib/completion.ts +1 -5
- package/src/lib/controller-files.ts +4 -8
- package/src/lib/controller-ownership.ts +78 -141
- package/src/lib/doctor/controller-findings.ts +20 -97
- package/src/lib/doctor/controller-fix-plans.ts +13 -29
- package/src/lib/doctor/controller-placeholders.ts +17 -189
- package/src/lib/doctor/controllers.ts +1 -1
- package/src/lib/doctor/project.ts +60 -52
- package/src/lib/doctor/types.ts +1 -5
- package/src/lib/load-route-map-worker.ts +19 -10
- package/src/lib/route-map.ts +61 -16
- package/{bootstrap → template}/.agents/skills/remix/SKILL.md +160 -73
- package/{bootstrap → template}/.agents/skills/remix/references/assets-and-browser-modules.md +22 -14
- package/{bootstrap → template}/.agents/skills/remix/references/auth-and-sessions.md +41 -18
- package/{bootstrap → template}/.agents/skills/remix/references/data-and-validation.md +21 -5
- package/{bootstrap → template}/.agents/skills/remix/references/middleware-and-server.md +43 -53
- package/{bootstrap → template}/.agents/skills/remix/references/routing-and-controllers.md +111 -44
- package/{bootstrap → template}/.agents/skills/remix/references/testing-patterns.md +17 -1
- package/{bootstrap → template}/AGENTS.md +10 -9
- package/template/README.md +29 -0
- package/template/app/actions/controller.tsx +18 -0
- package/template/app/assets/entry.ts +8 -0
- package/{bootstrap/app/ui → template/app/assets}/prompt-button.tsx +2 -1
- package/{bootstrap → template}/app/assets.ts +5 -3
- package/template/app/middleware/render.tsx +43 -0
- package/template/app/router.ts +20 -0
- package/{bootstrap → template}/app/routes.ts +1 -2
- package/{bootstrap → template}/app/ui/document.tsx +11 -4
- package/{bootstrap → template}/app/ui/scaffold-home-page.tsx +39 -38
- package/template/gitignore +4 -0
- package/{bootstrap → template}/package.json +5 -6
- package/template/public/favicon.svg +11 -0
- package/{bootstrap → template}/server.ts +10 -11
- package/{bootstrap → template}/tsconfig.json +3 -2
- package/bootstrap/README.md +0 -27
- package/bootstrap/app/assets/entry.ts +0 -19
- package/bootstrap/app/controllers/auth.tsx +0 -21
- package/bootstrap/app/controllers/home.tsx +0 -11
- package/bootstrap/app/router.ts +0 -16
- package/bootstrap/app/ui/layout.tsx +0 -22
- package/bootstrap/app/utils/render.tsx +0 -26
- /package/{bootstrap → template}/.agents/skills/remix/references/animate-elements.md +0 -0
- /package/{bootstrap → template}/.agents/skills/remix/references/component-model.md +0 -0
- /package/{bootstrap → template}/.agents/skills/remix/references/create-mixins.md +0 -0
- /package/{bootstrap → template}/.agents/skills/remix/references/hydration-frames-navigation.md +0 -0
- /package/{bootstrap → template}/.agents/skills/remix/references/mixins-styling-events.md +0 -0
|
@@ -1,108 +1,19 @@
|
|
|
1
1
|
import * as path from 'node:path'
|
|
2
2
|
|
|
3
3
|
import { getOwnerFileExtension } from '../controller-files.ts'
|
|
4
|
-
import type
|
|
5
|
-
|
|
6
|
-
const RESERVED_BINDING_IDENTIFIERS = new Set([
|
|
7
|
-
'await',
|
|
8
|
-
'break',
|
|
9
|
-
'case',
|
|
10
|
-
'catch',
|
|
11
|
-
'class',
|
|
12
|
-
'const',
|
|
13
|
-
'continue',
|
|
14
|
-
'debugger',
|
|
15
|
-
'default',
|
|
16
|
-
'delete',
|
|
17
|
-
'do',
|
|
18
|
-
'else',
|
|
19
|
-
'enum',
|
|
20
|
-
'export',
|
|
21
|
-
'extends',
|
|
22
|
-
'false',
|
|
23
|
-
'finally',
|
|
24
|
-
'for',
|
|
25
|
-
'function',
|
|
26
|
-
'if',
|
|
27
|
-
'implements',
|
|
28
|
-
'import',
|
|
29
|
-
'in',
|
|
30
|
-
'instanceof',
|
|
31
|
-
'interface',
|
|
32
|
-
'let',
|
|
33
|
-
'new',
|
|
34
|
-
'null',
|
|
35
|
-
'package',
|
|
36
|
-
'private',
|
|
37
|
-
'protected',
|
|
38
|
-
'public',
|
|
39
|
-
'return',
|
|
40
|
-
'static',
|
|
41
|
-
'super',
|
|
42
|
-
'switch',
|
|
43
|
-
'this',
|
|
44
|
-
'throw',
|
|
45
|
-
'true',
|
|
46
|
-
'try',
|
|
47
|
-
'typeof',
|
|
48
|
-
'var',
|
|
49
|
-
'void',
|
|
50
|
-
'while',
|
|
51
|
-
'with',
|
|
52
|
-
'yield',
|
|
53
|
-
])
|
|
54
|
-
|
|
55
|
-
export function renderActionPlaceholder(routeNode: OwnershipRouteNode, entryPath: string): string {
|
|
56
|
-
let routeMessage = `TODO: implement routes.${routeNode.name}`
|
|
57
|
-
let extension = getOwnerFileExtension(entryPath)
|
|
58
|
-
let exportName = getActionExportName(routeNode.key)
|
|
59
|
-
|
|
60
|
-
if (extension === '.js' || extension === '.jsx') {
|
|
61
|
-
return [
|
|
62
|
-
`export const ${exportName} = {`,
|
|
63
|
-
` handler() {`,
|
|
64
|
-
` return new Response(${JSON.stringify(routeMessage)})`,
|
|
65
|
-
` },`,
|
|
66
|
-
`}`,
|
|
67
|
-
'',
|
|
68
|
-
].join('\n')
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
let routeExpression = getRouteAccessExpression(routeNode.name)
|
|
72
|
-
let routesImportPath = getRelativeImportPath(entryPath, 'app/routes.ts')
|
|
73
|
-
let method = routeNode.method ?? 'ANY'
|
|
74
|
-
|
|
75
|
-
return [
|
|
76
|
-
`import type { BuildAction } from 'remix/fetch-router'`,
|
|
77
|
-
'',
|
|
78
|
-
`import type { routes } from '${routesImportPath}'`,
|
|
79
|
-
'',
|
|
80
|
-
`export const ${exportName}: BuildAction<'${method}', typeof ${routeExpression}> = {`,
|
|
81
|
-
` handler() {`,
|
|
82
|
-
` return new Response(${JSON.stringify(routeMessage)})`,
|
|
83
|
-
` },`,
|
|
84
|
-
`}`,
|
|
85
|
-
'',
|
|
86
|
-
].join('\n')
|
|
87
|
-
}
|
|
4
|
+
import { ROOT_ROUTE_NAME, type OwnershipRouteNode } from '../controller-ownership.ts'
|
|
88
5
|
|
|
89
6
|
export function renderControllerPlaceholder(
|
|
90
7
|
routeNode: OwnershipRouteNode,
|
|
91
8
|
entryPath: string,
|
|
92
|
-
resolvedEntryPathByRouteName: Map<string, string>,
|
|
93
9
|
): string {
|
|
94
10
|
let extension = getOwnerFileExtension(entryPath)
|
|
95
11
|
let lines: string[] = []
|
|
96
|
-
let
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
)
|
|
12
|
+
let actionEntries = routeNode.children
|
|
13
|
+
.filter((childNode) => childNode.kind === 'route')
|
|
14
|
+
.map((childNode) => renderControllerActionEntry(childNode))
|
|
100
15
|
|
|
101
16
|
if (extension === '.js' || extension === '.jsx') {
|
|
102
|
-
if (childImports.length > 0) {
|
|
103
|
-
lines.push(...childImports, '')
|
|
104
|
-
}
|
|
105
|
-
|
|
106
17
|
lines.push('export default {', ' actions: {')
|
|
107
18
|
lines.push(...actionEntries)
|
|
108
19
|
lines.push(' },', '}', '')
|
|
@@ -112,85 +23,29 @@ export function renderControllerPlaceholder(
|
|
|
112
23
|
let routeExpression = getRouteAccessExpression(routeNode.name)
|
|
113
24
|
let routesImportPath = getRelativeImportPath(entryPath, 'app/routes.ts')
|
|
114
25
|
|
|
115
|
-
lines.push(`import
|
|
116
|
-
lines.push(`import
|
|
117
|
-
|
|
118
|
-
if (childImports.length > 0) {
|
|
119
|
-
lines.push('', ...childImports)
|
|
120
|
-
}
|
|
26
|
+
lines.push(`import { createController } from 'remix/router'`, '')
|
|
27
|
+
lines.push(`import { routes } from '${routesImportPath}'`)
|
|
121
28
|
|
|
122
|
-
lines.push('',
|
|
29
|
+
lines.push('', `export default createController(${routeExpression}, {`, ' actions: {')
|
|
123
30
|
lines.push(...actionEntries)
|
|
124
|
-
lines.push(' },',
|
|
31
|
+
lines.push(' },', '})', '')
|
|
125
32
|
|
|
126
33
|
return lines.join('\n')
|
|
127
34
|
}
|
|
128
35
|
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
for (let childNode of routeNode.children) {
|
|
137
|
-
if (childNode.kind !== 'group') {
|
|
138
|
-
continue
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
let childEntryPath = resolvedEntryPathByRouteName.get(childNode.name)
|
|
142
|
-
if (childEntryPath == null) {
|
|
143
|
-
continue
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
imports.push(
|
|
147
|
-
`import ${getControllerImportName(childNode.key)} from '${getRelativeImportPath(entryPath, childEntryPath)}'`,
|
|
148
|
-
)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return imports
|
|
36
|
+
function renderControllerActionEntry(childNode: OwnershipRouteNode): string {
|
|
37
|
+
return [
|
|
38
|
+
` ${formatObjectKey(childNode.key)}() {`,
|
|
39
|
+
` return new Response(${JSON.stringify(`TODO: implement routes.${childNode.name}`)})`,
|
|
40
|
+
` },`,
|
|
41
|
+
].join('\n')
|
|
152
42
|
}
|
|
153
43
|
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
resolvedEntryPathByRouteName: Map<string, string>,
|
|
158
|
-
): string {
|
|
159
|
-
if (childNode.kind === 'route') {
|
|
160
|
-
return [
|
|
161
|
-
` ${formatObjectKey(childNode.key)}() {`,
|
|
162
|
-
` return new Response(${JSON.stringify(`TODO: implement routes.${childNode.name}`)})`,
|
|
163
|
-
` },`,
|
|
164
|
-
].join('\n')
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
let childEntryPath = resolvedEntryPathByRouteName.get(childNode.name)
|
|
168
|
-
if (childEntryPath != null) {
|
|
169
|
-
return ` ${formatObjectKey(childNode.key)}: ${getControllerImportName(childNode.key)},`
|
|
44
|
+
function getRouteAccessExpression(routeName: string): string {
|
|
45
|
+
if (routeName === ROOT_ROUTE_NAME) {
|
|
46
|
+
return 'routes'
|
|
170
47
|
}
|
|
171
48
|
|
|
172
|
-
return ` ${formatObjectKey(childNode.key)}: ${renderInlineControllerPlaceholder(childNode)},`
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function renderInlineControllerPlaceholder(routeNode: OwnershipRouteNode): string {
|
|
176
|
-
let childEntries = routeNode.children.map((childNode) =>
|
|
177
|
-
childNode.kind === 'route'
|
|
178
|
-
? `${formatObjectKey(childNode.key)}() { return new Response(${JSON.stringify(`TODO: implement routes.${childNode.name}`)}) }`
|
|
179
|
-
: `${formatObjectKey(childNode.key)}: ${renderInlineControllerPlaceholder(childNode)}`,
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
return `{ actions: { ${childEntries.join(', ')} } }`
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function getControllerImportName(key: string): string {
|
|
186
|
-
return `${toIdentifier(key)}Controller`
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function getActionExportName(key: string): string {
|
|
190
|
-
return toIdentifier(key)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function getRouteAccessExpression(routeName: string): string {
|
|
194
49
|
let segments = routeName.split('.')
|
|
195
50
|
let expression = 'routes'
|
|
196
51
|
|
|
@@ -210,33 +65,6 @@ function formatObjectKey(key: string): string {
|
|
|
210
65
|
return isIdentifier(key) ? key : JSON.stringify(key)
|
|
211
66
|
}
|
|
212
67
|
|
|
213
|
-
function toIdentifier(value: string): string {
|
|
214
|
-
let parts = value.split(/[^A-Za-z0-9_$]+/).filter(Boolean)
|
|
215
|
-
if (parts.length === 0) {
|
|
216
|
-
return 'route'
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
let identifier = parts
|
|
220
|
-
.map((part, index) =>
|
|
221
|
-
index === 0 ? part.replace(/^./, (char) => char.toLowerCase()) : capitalize(part),
|
|
222
|
-
)
|
|
223
|
-
.join('')
|
|
224
|
-
|
|
225
|
-
if (!/^[A-Za-z_$]/.test(identifier)) {
|
|
226
|
-
return `route${capitalize(identifier)}`
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (RESERVED_BINDING_IDENTIFIERS.has(identifier)) {
|
|
230
|
-
return `route${capitalize(identifier)}`
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return identifier
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function capitalize(value: string): string {
|
|
237
|
-
return value.length === 0 ? value : `${value[0].toUpperCase()}${value.slice(1)}`
|
|
238
|
-
}
|
|
239
|
-
|
|
240
68
|
function isIdentifier(value: string): boolean {
|
|
241
69
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value)
|
|
242
70
|
}
|
|
@@ -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', '
|
|
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
|
|
229
|
-
let extensions = await collectOwnerExtensions(
|
|
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/
|
|
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
|
|
322
|
-
'
|
|
323
|
-
'
|
|
324
|
-
'
|
|
325
|
-
' <
|
|
326
|
-
' <
|
|
327
|
-
'
|
|
328
|
-
'
|
|
329
|
-
'
|
|
330
|
-
'
|
|
331
|
-
' <
|
|
332
|
-
'
|
|
333
|
-
'
|
|
334
|
-
'
|
|
335
|
-
'
|
|
336
|
-
'
|
|
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
|
|
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
|
|
351
|
+
`import { routes } from '../routes.ts'`,
|
|
350
352
|
'',
|
|
351
|
-
`export
|
|
352
|
-
'
|
|
353
|
-
'
|
|
354
|
-
'
|
|
355
|
-
' <
|
|
356
|
-
' <
|
|
357
|
-
'
|
|
358
|
-
'
|
|
359
|
-
'
|
|
360
|
-
'
|
|
361
|
-
' <
|
|
362
|
-
'
|
|
363
|
-
'
|
|
364
|
-
'
|
|
365
|
-
'
|
|
366
|
-
'
|
|
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
|
|
379
|
-
'
|
|
380
|
-
'
|
|
381
|
-
'
|
|
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/
|
|
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
|
|
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
|
|
415
|
+
`import { routes } from '../routes.ts'`,
|
|
410
416
|
'',
|
|
411
|
-
`export
|
|
412
|
-
'
|
|
413
|
-
'
|
|
414
|
-
'
|
|
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/
|
|
436
|
+
' <p>Update app/routes.ts and app/actions/controller to keep building your app.</p>',
|
|
429
437
|
' </body>',
|
|
430
438
|
' </html>',
|
|
431
439
|
' )',
|
package/src/lib/doctor/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type DoctorSuiteName = 'environment' | 'project' | '
|
|
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
|
|
|
@@ -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(
|
|
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 (
|
|
60
|
+
if (routeLeaf !== undefined) {
|
|
60
61
|
return {
|
|
61
62
|
children: [],
|
|
62
63
|
key,
|
|
63
64
|
kind: 'route',
|
|
64
|
-
method:
|
|
65
|
+
method: routeLeaf.method,
|
|
65
66
|
name,
|
|
66
|
-
pattern:
|
|
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
|
|
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
|
-
|
|
93
|
-
typeof
|
|
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
|
|
package/src/lib/route-map.ts
CHANGED
|
@@ -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 {
|
|
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 = '
|
|
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
|
|
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
|
-
|
|
141
|
-
parentSegments: string[] = [],
|
|
146
|
+
ownership: ControllerOwnership,
|
|
142
147
|
): RouteTreeNode[] {
|
|
143
|
-
let subtreesByRouteName = new Map(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
?
|
|
213
|
+
? ROOT_ROUTE_NAME
|
|
183
214
|
: parentSegments.join('.')
|
|
184
215
|
let subtree = subtreesByRouteName.get(ownerRouteName)
|
|
185
216
|
|
|
186
|
-
if (subtree
|
|
187
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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[] {
|