@remix-run/cli 0.0.0 → 0.2.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 (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -3
  3. package/bootstrap/.agents/skills/remix/SKILL.md +501 -0
  4. package/bootstrap/.agents/skills/remix/references/animate-elements.md +195 -0
  5. package/bootstrap/.agents/skills/remix/references/assets-and-browser-modules.md +122 -0
  6. package/bootstrap/.agents/skills/remix/references/auth-and-sessions.md +420 -0
  7. package/bootstrap/.agents/skills/remix/references/component-model.md +282 -0
  8. package/bootstrap/.agents/skills/remix/references/create-mixins.md +158 -0
  9. package/bootstrap/.agents/skills/remix/references/data-and-validation.md +363 -0
  10. package/bootstrap/.agents/skills/remix/references/hydration-frames-navigation.md +297 -0
  11. package/bootstrap/.agents/skills/remix/references/middleware-and-server.md +243 -0
  12. package/bootstrap/.agents/skills/remix/references/mixins-styling-events.md +213 -0
  13. package/bootstrap/.agents/skills/remix/references/routing-and-controllers.md +324 -0
  14. package/bootstrap/.agents/skills/remix/references/testing-patterns.md +156 -0
  15. package/bootstrap/AGENTS.md +39 -0
  16. package/bootstrap/README.md +27 -0
  17. package/bootstrap/app/assets/entry.ts +19 -0
  18. package/bootstrap/app/assets.ts +18 -0
  19. package/bootstrap/app/controllers/auth.tsx +21 -0
  20. package/bootstrap/app/controllers/home.tsx +11 -0
  21. package/bootstrap/app/router.ts +16 -0
  22. package/bootstrap/app/routes.ts +7 -0
  23. package/bootstrap/app/ui/document.tsx +26 -0
  24. package/bootstrap/app/ui/layout.tsx +22 -0
  25. package/bootstrap/app/ui/prompt-button.tsx +162 -0
  26. package/bootstrap/app/ui/scaffold-home-page.tsx +526 -0
  27. package/bootstrap/app/utils/render.tsx +26 -0
  28. package/bootstrap/package.json +22 -0
  29. package/bootstrap/server.ts +37 -0
  30. package/bootstrap/tsconfig.json +18 -0
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +1 -0
  34. package/dist/lib/bootstrap-project.d.ts +17 -0
  35. package/dist/lib/bootstrap-project.d.ts.map +1 -0
  36. package/dist/lib/bootstrap-project.js +137 -0
  37. package/dist/lib/cli-context.d.ts +11 -0
  38. package/dist/lib/cli-context.d.ts.map +1 -0
  39. package/dist/lib/cli-context.js +19 -0
  40. package/dist/lib/cli.d.ts +6 -0
  41. package/dist/lib/cli.d.ts.map +1 -0
  42. package/dist/lib/cli.js +84 -0
  43. package/dist/lib/commands/completion.d.ts +3 -0
  44. package/dist/lib/commands/completion.d.ts.map +1 -0
  45. package/dist/lib/commands/completion.js +51 -0
  46. package/dist/lib/commands/doctor.d.ts +4 -0
  47. package/dist/lib/commands/doctor.d.ts.map +1 -0
  48. package/dist/lib/commands/doctor.js +293 -0
  49. package/dist/lib/commands/help.d.ts +4 -0
  50. package/dist/lib/commands/help.d.ts.map +1 -0
  51. package/dist/lib/commands/help.js +100 -0
  52. package/dist/lib/commands/new.d.ts +4 -0
  53. package/dist/lib/commands/new.d.ts.map +1 -0
  54. package/dist/lib/commands/new.js +70 -0
  55. package/dist/lib/commands/routes.d.ts +4 -0
  56. package/dist/lib/commands/routes.d.ts.map +1 -0
  57. package/dist/lib/commands/routes.js +161 -0
  58. package/dist/lib/commands/test.d.ts +4 -0
  59. package/dist/lib/commands/test.d.ts.map +1 -0
  60. package/dist/lib/commands/test.js +23 -0
  61. package/dist/lib/commands/version.d.ts +4 -0
  62. package/dist/lib/commands/version.d.ts.map +1 -0
  63. package/dist/lib/commands/version.js +29 -0
  64. package/dist/lib/completion.d.ts +12 -0
  65. package/dist/lib/completion.d.ts.map +1 -0
  66. package/dist/lib/completion.js +310 -0
  67. package/dist/lib/contained-path.d.ts +2 -0
  68. package/dist/lib/contained-path.d.ts.map +1 -0
  69. package/dist/lib/contained-path.js +10 -0
  70. package/dist/lib/controller-files.d.ts +14 -0
  71. package/dist/lib/controller-files.d.ts.map +1 -0
  72. package/dist/lib/controller-files.js +53 -0
  73. package/dist/lib/controller-ownership.d.ts +41 -0
  74. package/dist/lib/controller-ownership.d.ts.map +1 -0
  75. package/dist/lib/controller-ownership.js +224 -0
  76. package/dist/lib/display-path.d.ts +2 -0
  77. package/dist/lib/display-path.d.ts.map +1 -0
  78. package/dist/lib/display-path.js +12 -0
  79. package/dist/lib/doctor/controller-findings.d.ts +4 -0
  80. package/dist/lib/doctor/controller-findings.d.ts.map +1 -0
  81. package/dist/lib/doctor/controller-findings.js +132 -0
  82. package/dist/lib/doctor/controller-fix-plans.d.ts +4 -0
  83. package/dist/lib/doctor/controller-fix-plans.d.ts.map +1 -0
  84. package/dist/lib/doctor/controller-fix-plans.js +130 -0
  85. package/dist/lib/doctor/controller-placeholders.d.ts +4 -0
  86. package/dist/lib/doctor/controller-placeholders.d.ts.map +1 -0
  87. package/dist/lib/doctor/controller-placeholders.js +183 -0
  88. package/dist/lib/doctor/controllers.d.ts +8 -0
  89. package/dist/lib/doctor/controllers.d.ts.map +1 -0
  90. package/dist/lib/doctor/controllers.js +13 -0
  91. package/dist/lib/doctor/environment.d.ts +17 -0
  92. package/dist/lib/doctor/environment.d.ts.map +1 -0
  93. package/dist/lib/doctor/environment.js +226 -0
  94. package/dist/lib/doctor/fixes.d.ts +3 -0
  95. package/dist/lib/doctor/fixes.d.ts.map +1 -0
  96. package/dist/lib/doctor/fixes.js +65 -0
  97. package/dist/lib/doctor/project.d.ts +10 -0
  98. package/dist/lib/doctor/project.d.ts.map +1 -0
  99. package/dist/lib/doctor/project.js +371 -0
  100. package/dist/lib/doctor/types.d.ts +48 -0
  101. package/dist/lib/doctor/types.d.ts.map +1 -0
  102. package/dist/lib/doctor/types.js +15 -0
  103. package/dist/lib/errors.d.ts +167 -0
  104. package/dist/lib/errors.d.ts.map +1 -0
  105. package/dist/lib/errors.js +307 -0
  106. package/dist/lib/help-text.d.ts +13 -0
  107. package/dist/lib/help-text.d.ts.map +1 -0
  108. package/dist/lib/help-text.js +43 -0
  109. package/dist/lib/load-route-map-worker.d.ts +2 -0
  110. package/dist/lib/load-route-map-worker.d.ts.map +1 -0
  111. package/dist/lib/load-route-map-worker.js +92 -0
  112. package/dist/lib/parse-args.d.ts +22 -0
  113. package/dist/lib/parse-args.d.ts.map +1 -0
  114. package/dist/lib/parse-args.js +35 -0
  115. package/dist/lib/remix-version.d.ts +2 -0
  116. package/dist/lib/remix-version.d.ts.map +1 -0
  117. package/dist/lib/remix-version.js +58 -0
  118. package/dist/lib/reporter.d.ts +56 -0
  119. package/dist/lib/reporter.d.ts.map +1 -0
  120. package/dist/lib/reporter.js +357 -0
  121. package/dist/lib/route-map.d.ts +38 -0
  122. package/dist/lib/route-map.d.ts.map +1 -0
  123. package/dist/lib/route-map.js +195 -0
  124. package/dist/lib/terminal.d.ts +17 -0
  125. package/dist/lib/terminal.d.ts.map +1 -0
  126. package/dist/lib/terminal.js +66 -0
  127. package/package.json +47 -5
  128. package/src/index.ts +1 -0
  129. package/src/lib/bootstrap-project.ts +222 -0
  130. package/src/lib/cli-context.ts +38 -0
  131. package/src/lib/cli.ts +116 -0
  132. package/src/lib/commands/completion.ts +74 -0
  133. package/src/lib/commands/doctor.ts +393 -0
  134. package/src/lib/commands/help.ts +123 -0
  135. package/src/lib/commands/new.ts +104 -0
  136. package/src/lib/commands/routes.ts +251 -0
  137. package/src/lib/commands/test.ts +31 -0
  138. package/src/lib/commands/version.ts +39 -0
  139. package/src/lib/completion.ts +418 -0
  140. package/src/lib/contained-path.ts +13 -0
  141. package/src/lib/controller-files.ts +79 -0
  142. package/src/lib/controller-ownership.ts +397 -0
  143. package/src/lib/display-path.ts +16 -0
  144. package/src/lib/doctor/controller-findings.ts +157 -0
  145. package/src/lib/doctor/controller-fix-plans.ts +184 -0
  146. package/src/lib/doctor/controller-placeholders.ts +242 -0
  147. package/src/lib/doctor/controllers.ts +23 -0
  148. package/src/lib/doctor/environment.ts +289 -0
  149. package/src/lib/doctor/fixes.ts +84 -0
  150. package/src/lib/doctor/project.ts +435 -0
  151. package/src/lib/doctor/types.ts +95 -0
  152. package/src/lib/errors.ts +367 -0
  153. package/src/lib/help-text.ts +88 -0
  154. package/src/lib/load-route-map-worker.ts +110 -0
  155. package/src/lib/parse-args.ts +82 -0
  156. package/src/lib/remix-version.ts +80 -0
  157. package/src/lib/reporter.ts +535 -0
  158. package/src/lib/route-map.ts +303 -0
  159. package/src/lib/terminal.ts +86 -0
@@ -0,0 +1,367 @@
1
+ export interface CliErrorDefinition {
2
+ code: string
3
+ docsUrl?: string
4
+ fix?: string
5
+ title: string
6
+ }
7
+
8
+ export type CliErrorContext = Record<string, boolean | number | string | null | undefined>
9
+
10
+ export interface CliErrorOptions {
11
+ code: string
12
+ context?: CliErrorContext
13
+ docsUrl?: string
14
+ fix?: string
15
+ message: string
16
+ showHelp?: boolean
17
+ title: string
18
+ }
19
+
20
+ interface CreateErrorOptions {
21
+ context?: CliErrorContext
22
+ message: string
23
+ }
24
+
25
+ export const CLI_ERROR_DEFINITIONS = {
26
+ appNameUnavailable: {
27
+ code: 'RMX_APP_NAME_UNAVAILABLE',
28
+ title: 'Could not determine an app name',
29
+ fix: 'Pass --app-name or choose a target directory name that can become an app name.',
30
+ },
31
+ remixVersionUnavailable: {
32
+ code: 'RMX_REMIX_VERSION_UNAVAILABLE',
33
+ title: 'Could not determine the Remix version',
34
+ },
35
+ invalidCompletionRequest: {
36
+ code: 'RMX_INVALID_COMPLETION_REQUEST',
37
+ title: 'Invalid completion request',
38
+ },
39
+ fetchUnavailable: {
40
+ code: 'RMX_FETCH_UNAVAILABLE',
41
+ title: 'This runtime does not provide fetch()',
42
+ fix: 'Run the Remix CLI in a runtime that provides fetch().',
43
+ },
44
+ internalError: {
45
+ code: 'RMX_INTERNAL_ERROR',
46
+ title: 'Unexpected Remix CLI error',
47
+ },
48
+ invalidFlagCombination: {
49
+ code: 'RMX_INVALID_FLAG_COMBINATION',
50
+ title: 'Invalid flag combination',
51
+ fix: 'Remove one of the conflicting flags and try again.',
52
+ },
53
+ invalidPackageName: {
54
+ code: 'RMX_INVALID_PACKAGE_NAME',
55
+ title: 'Could not derive a valid package name',
56
+ fix: 'Choose an app name that can be normalized into a valid npm package name.',
57
+ },
58
+ missingOptionValue: {
59
+ code: 'RMX_MISSING_OPTION_VALUE',
60
+ title: 'Missing option value',
61
+ fix: 'Pass a value immediately after the option.',
62
+ },
63
+ missingTargetDirectory: {
64
+ code: 'RMX_MISSING_TARGET_DIRECTORY',
65
+ title: 'Missing target directory',
66
+ fix: 'Pass a target directory, for example `remix new my-remix-app`.',
67
+ },
68
+ projectRootNotFound: {
69
+ code: 'RMX_PROJECT_ROOT_NOT_FOUND',
70
+ title: 'Could not find a project root',
71
+ fix: 'Run this command inside a Remix project.',
72
+ },
73
+ remoteSkillDataMissing: {
74
+ code: 'RMX_REMOTE_SKILL_DATA_MISSING',
75
+ title: 'Could not find remote Remix skill data',
76
+ },
77
+ routeMapLoaderFailed: {
78
+ code: 'RMX_ROUTE_MAP_LOADER_FAILED',
79
+ title: 'Route-map loader failed',
80
+ fix: 'Check app/routes.ts and make sure it exports a named `routes` value with a valid route map.',
81
+ },
82
+ routeMapLoaderInvalidJson: {
83
+ code: 'RMX_ROUTE_MAP_LOADER_INVALID_JSON',
84
+ title: 'Route-map loader returned invalid JSON',
85
+ fix: 'Check app/routes.ts and make sure route-map loading does not write extra output.',
86
+ },
87
+ routeMapLoaderSignal: {
88
+ code: 'RMX_ROUTE_MAP_LOADER_SIGNAL',
89
+ title: 'Route-map loader exited from a signal',
90
+ fix: 'Check app/routes.ts for side effects or runtime issues and try again.',
91
+ },
92
+ routeOwnerPlanUnresolved: {
93
+ code: 'RMX_ROUTE_OWNER_PLAN_UNRESOLVED',
94
+ title: 'Could not resolve a route owner plan',
95
+ },
96
+ routesFileNotFound: {
97
+ code: 'RMX_ROUTES_FILE_NOT_FOUND',
98
+ title: 'Could not find app/routes.ts',
99
+ fix: 'Run this command inside a Remix app that has app/routes.ts.',
100
+ },
101
+ targetDirectoryNotEmpty: {
102
+ code: 'RMX_TARGET_DIRECTORY_NOT_EMPTY',
103
+ title: 'Target directory is not empty',
104
+ fix: 'Re-run with --force to continue.',
105
+ },
106
+ targetPathNotDirectory: {
107
+ code: 'RMX_TARGET_PATH_NOT_DIRECTORY',
108
+ title: 'Target path is not a directory',
109
+ fix: 'Choose a directory path for the new app target.',
110
+ },
111
+ unexpectedExtraArgument: {
112
+ code: 'RMX_UNEXPECTED_ARGUMENT',
113
+ title: 'Unexpected extra argument',
114
+ fix: 'Remove the extra argument or run the command with --help.',
115
+ },
116
+ unknownArgument: {
117
+ code: 'RMX_UNKNOWN_ARGUMENT',
118
+ title: 'Unknown argument',
119
+ fix: 'Run the command with --help to see supported arguments.',
120
+ },
121
+ unknownCommand: {
122
+ code: 'RMX_UNKNOWN_COMMAND',
123
+ title: 'Unknown command',
124
+ fix: 'Run `remix help` to see available commands.',
125
+ },
126
+ unknownCompletionShell: {
127
+ code: 'RMX_UNKNOWN_COMPLETION_SHELL',
128
+ title: 'Unknown completion shell',
129
+ fix: 'Run `remix completion bash` or `remix completion zsh`.',
130
+ },
131
+ unknownHelpTopic: {
132
+ code: 'RMX_UNKNOWN_HELP_TOPIC',
133
+ title: 'Unknown help topic',
134
+ fix: 'Run `remix help` to see available commands and help topics.',
135
+ },
136
+ } satisfies Record<string, CliErrorDefinition>
137
+
138
+ export class CliError extends Error {
139
+ code: string
140
+ context?: CliErrorContext
141
+ docsUrl?: string
142
+ fix?: string
143
+ showHelp: boolean
144
+ title: string
145
+
146
+ constructor(options: CliErrorOptions) {
147
+ super(options.message)
148
+ this.code = options.code
149
+ this.context = options.context
150
+ this.docsUrl = options.docsUrl
151
+ this.fix = options.fix
152
+ this.name = 'CliError'
153
+ this.showHelp = options.showHelp ?? false
154
+ this.title = options.title
155
+ }
156
+ }
157
+
158
+ export class UsageError extends CliError {
159
+ constructor(options: CliErrorOptions) {
160
+ super({ ...options, showHelp: options.showHelp ?? true })
161
+ this.name = 'UsageError'
162
+ }
163
+ }
164
+
165
+ export function appNameUnavailable(targetDir?: string): UsageError {
166
+ return createUsageError(CLI_ERROR_DEFINITIONS.appNameUnavailable, {
167
+ context: targetDir == null ? undefined : { targetDir },
168
+ message: 'Could not determine an app name from the target directory.',
169
+ })
170
+ }
171
+
172
+ export function remixVersionUnavailable(): CliError {
173
+ return createCliError(CLI_ERROR_DEFINITIONS.remixVersionUnavailable, {
174
+ message: 'Could not determine the current Remix version.',
175
+ })
176
+ }
177
+
178
+ export function invalidCompletionRequest(): CliError {
179
+ return createCliError(CLI_ERROR_DEFINITIONS.invalidCompletionRequest, {
180
+ message: 'Invalid completion request.',
181
+ })
182
+ }
183
+
184
+ export function fetchUnavailable(): CliError {
185
+ return createCliError(CLI_ERROR_DEFINITIONS.fetchUnavailable, {
186
+ message: 'This runtime does not provide fetch().',
187
+ })
188
+ }
189
+
190
+ export function invalidFlagCombination(details: string): UsageError {
191
+ return createUsageError(CLI_ERROR_DEFINITIONS.invalidFlagCombination, {
192
+ context: { details },
193
+ message: details,
194
+ })
195
+ }
196
+
197
+ export function invalidPackageName(input: string): UsageError {
198
+ return createUsageError(CLI_ERROR_DEFINITIONS.invalidPackageName, {
199
+ context: { input },
200
+ message: `Could not derive a valid package name from "${input}".`,
201
+ })
202
+ }
203
+
204
+ export function missingOptionValue(option: string): UsageError {
205
+ return createUsageError(CLI_ERROR_DEFINITIONS.missingOptionValue, {
206
+ context: { option },
207
+ message: `${option} requires a value.`,
208
+ })
209
+ }
210
+
211
+ export function missingTargetDirectory(): UsageError {
212
+ return createUsageError(CLI_ERROR_DEFINITIONS.missingTargetDirectory, {
213
+ message: 'A target directory is required.',
214
+ })
215
+ }
216
+
217
+ export function projectRootNotFound(startDir?: string): CliError {
218
+ return createCliError(CLI_ERROR_DEFINITIONS.projectRootNotFound, {
219
+ context: startDir == null ? undefined : { startDir },
220
+ message: 'Could not find a project root. Run this command inside a Remix project.',
221
+ })
222
+ }
223
+
224
+ export function remoteSkillDataMissing(name: string): CliError {
225
+ return createCliError(CLI_ERROR_DEFINITIONS.remoteSkillDataMissing, {
226
+ context: { name },
227
+ message: `Could not find remote data for Remix skill: ${name}`,
228
+ })
229
+ }
230
+
231
+ export function routeMapLoaderFailed(details: string): CliError {
232
+ return createCliError(CLI_ERROR_DEFINITIONS.routeMapLoaderFailed, {
233
+ context: { details },
234
+ message: details,
235
+ })
236
+ }
237
+
238
+ export function routeMapLoaderInvalidJson(): CliError {
239
+ return createCliError(CLI_ERROR_DEFINITIONS.routeMapLoaderInvalidJson, {
240
+ message: 'Route-map loader returned invalid JSON.',
241
+ })
242
+ }
243
+
244
+ export function routeMapLoaderSignal(signal: NodeJS.Signals): CliError {
245
+ return createCliError(CLI_ERROR_DEFINITIONS.routeMapLoaderSignal, {
246
+ context: { signal },
247
+ message: `Route-map loader exited from signal ${signal}.`,
248
+ })
249
+ }
250
+
251
+ export function routeOwnerPlanUnresolved(routeName: string): CliError {
252
+ return createCliError(CLI_ERROR_DEFINITIONS.routeOwnerPlanUnresolved, {
253
+ context: { routeName },
254
+ message: `Could not resolve owner plan for "${routeName}".`,
255
+ })
256
+ }
257
+
258
+ export function routesFileNotFound(startDir?: string): CliError {
259
+ return createCliError(CLI_ERROR_DEFINITIONS.routesFileNotFound, {
260
+ context: startDir == null ? undefined : { startDir },
261
+ message: 'Could not find app/routes.ts. Run this command inside a Remix app.',
262
+ })
263
+ }
264
+
265
+ export function targetDirectoryNotEmpty(targetDir: string): UsageError {
266
+ return createUsageError(CLI_ERROR_DEFINITIONS.targetDirectoryNotEmpty, {
267
+ context: { targetDir },
268
+ message: `Target directory is not empty: ${targetDir}. Re-run with --force to continue.`,
269
+ })
270
+ }
271
+
272
+ export function targetPathNotDirectory(targetDir: string): UsageError {
273
+ return createUsageError(CLI_ERROR_DEFINITIONS.targetPathNotDirectory, {
274
+ context: { targetDir },
275
+ message: `Target path is not a directory: ${targetDir}`,
276
+ })
277
+ }
278
+
279
+ export function toCliError(error: unknown): CliError {
280
+ if (error instanceof CliError) {
281
+ return error
282
+ }
283
+
284
+ if (error instanceof Error) {
285
+ return createCliError(CLI_ERROR_DEFINITIONS.internalError, { message: error.message })
286
+ }
287
+
288
+ return createCliError(CLI_ERROR_DEFINITIONS.internalError, { message: String(error) })
289
+ }
290
+
291
+ export function unknownArgument(argument: string): UsageError {
292
+ return createUsageError(CLI_ERROR_DEFINITIONS.unknownArgument, {
293
+ context: { argument },
294
+ message: `Unknown argument: ${argument}`,
295
+ })
296
+ }
297
+
298
+ export function unknownCommand(command: string): UsageError {
299
+ return createUsageError(CLI_ERROR_DEFINITIONS.unknownCommand, {
300
+ context: { command },
301
+ message: `Unknown command: ${command}`,
302
+ })
303
+ }
304
+
305
+ export function unknownCompletionShell(shell: string): UsageError {
306
+ return createUsageError(CLI_ERROR_DEFINITIONS.unknownCompletionShell, {
307
+ context: { shell },
308
+ message: `Unknown completion shell: ${shell}`,
309
+ })
310
+ }
311
+
312
+ export function unknownHelpTopic(topic: string): UsageError {
313
+ return createUsageError(CLI_ERROR_DEFINITIONS.unknownHelpTopic, {
314
+ context: { topic },
315
+ message: `Unknown help topic: ${topic}`,
316
+ })
317
+ }
318
+
319
+ export function unexpectedExtraArgument(argument: string): UsageError {
320
+ return createUsageError(CLI_ERROR_DEFINITIONS.unexpectedExtraArgument, {
321
+ context: { argument },
322
+ message: `Unexpected extra argument: ${argument}`,
323
+ })
324
+ }
325
+
326
+ export function renderCliError(error: unknown, options: { helpText?: string } = {}): string {
327
+ let cliError = toCliError(error)
328
+ let lines = [`Error [${cliError.code}] ${cliError.title}`, cliError.message]
329
+
330
+ if (cliError.fix != null && cliError.fix.length > 0) {
331
+ lines.push('', 'Try:', ` ${cliError.fix}`)
332
+ }
333
+
334
+ if (cliError.docsUrl != null && cliError.docsUrl.length > 0) {
335
+ lines.push('', 'Documentation:', ` ${cliError.docsUrl}`)
336
+ }
337
+
338
+ let output = `${lines.join('\n')}\n`
339
+
340
+ if (cliError.showHelp && options.helpText != null) {
341
+ output += `\n${options.helpText}`
342
+ }
343
+
344
+ return output
345
+ }
346
+
347
+ function createCliError(definition: CliErrorDefinition, options: CreateErrorOptions): CliError {
348
+ return new CliError({
349
+ code: definition.code,
350
+ context: options.context,
351
+ docsUrl: definition.docsUrl,
352
+ fix: definition.fix,
353
+ message: options.message,
354
+ title: definition.title,
355
+ })
356
+ }
357
+
358
+ function createUsageError(definition: CliErrorDefinition, options: CreateErrorOptions): UsageError {
359
+ return new UsageError({
360
+ code: definition.code,
361
+ context: options.context,
362
+ docsUrl: definition.docsUrl,
363
+ fix: definition.fix,
364
+ message: options.message,
365
+ title: definition.title,
366
+ })
367
+ }
@@ -0,0 +1,88 @@
1
+ import * as process from 'node:process'
2
+
3
+ import { boldLightBlue, lightYellow } from './terminal.ts'
4
+
5
+ export interface HelpTextSectionRow {
6
+ description: string
7
+ label: string
8
+ }
9
+
10
+ export interface HelpTextOptions {
11
+ commands?: HelpTextSectionRow[]
12
+ description?: string
13
+ examples?: string[]
14
+ options?: HelpTextSectionRow[]
15
+ usage: string[]
16
+ }
17
+
18
+ const SECTION_INDENT = ' '
19
+ const ROW_GAP = 2
20
+
21
+ export function formatHelpText(
22
+ options: HelpTextOptions,
23
+ target: NodeJS.WriteStream = process.stdout,
24
+ ): string {
25
+ let sections: string[] = [renderUsageSection(options.usage, target)]
26
+
27
+ if (options.description != null && options.description.length > 0) {
28
+ sections.push(options.description)
29
+ }
30
+
31
+ if (options.commands != null && options.commands.length > 0) {
32
+ sections.push(renderRowSection('Commands', options.commands, target))
33
+ }
34
+
35
+ if (options.options != null && options.options.length > 0) {
36
+ sections.push(renderRowSection('Options', options.options, target))
37
+ }
38
+
39
+ if (options.examples != null && options.examples.length > 0) {
40
+ sections.push(renderExamplesSection(options.examples, target))
41
+ }
42
+
43
+ return `${sections.join('\n\n')}\n`
44
+ }
45
+
46
+ function renderUsageSection(usage: string[], target: NodeJS.WriteStream): string {
47
+ return renderSection(
48
+ 'Usage',
49
+ usage.map((line) => `${SECTION_INDENT}${highlightSyntax(line, target)}`),
50
+ target,
51
+ )
52
+ }
53
+
54
+ function renderExamplesSection(examples: string[], target: NodeJS.WriteStream): string {
55
+ return renderSection(
56
+ 'Examples',
57
+ examples.map((line) => `${SECTION_INDENT}${highlightSyntax(line, target)}`),
58
+ target,
59
+ )
60
+ }
61
+
62
+ function renderRowSection(
63
+ title: string,
64
+ rows: HelpTextSectionRow[],
65
+ target: NodeJS.WriteStream,
66
+ ): string {
67
+ let labelWidth = rows.reduce((width, row) => Math.max(width, row.label.length), 0) + ROW_GAP
68
+ let lines = rows.map((row) => {
69
+ let paddedLabel = row.label.padEnd(labelWidth)
70
+ return `${SECTION_INDENT}${highlightSyntax(paddedLabel, target)}${row.description}`
71
+ })
72
+
73
+ return renderSection(title, lines, target)
74
+ }
75
+
76
+ function renderSection(title: string, lines: string[], target: NodeJS.WriteStream): string {
77
+ return `${formatHeading(title, target)}:\n${lines.join('\n')}`
78
+ }
79
+
80
+ function formatHeading(title: string, target: NodeJS.WriteStream): string {
81
+ return boldLightBlue(title, target)
82
+ }
83
+
84
+ function highlightSyntax(text: string, target: NodeJS.WriteStream): string {
85
+ return text.replace(/(--[A-Za-z0-9-]+|-[A-Za-z]|\[[A-Za-z][A-Za-z0-9-]*\]|<[^>\n]+>)/g, (token) =>
86
+ lightYellow(token, target),
87
+ )
88
+ }
@@ -0,0 +1,110 @@
1
+ import * as process from 'node:process'
2
+ import { pathToFileURL } from 'node:url'
3
+
4
+ import type { RawRouteTreeNode } from './route-map.ts'
5
+
6
+ void run().catch((error: unknown) => {
7
+ if (error instanceof Error) {
8
+ process.stderr.write(`${error.message}\n`)
9
+ } else {
10
+ process.stderr.write(`${String(error)}\n`)
11
+ }
12
+
13
+ setExitCode(1)
14
+ })
15
+
16
+ async function run(): Promise<void> {
17
+ let routesFile = process.argv[2]
18
+ if (typeof routesFile !== 'string' || routesFile.length === 0) {
19
+ throw new Error('Missing app/routes.ts path.')
20
+ }
21
+
22
+ let tree = await loadRawRouteTree(routesFile)
23
+ process.stdout.write(JSON.stringify(tree))
24
+ }
25
+
26
+ async function loadRawRouteTree(routesFile: string): Promise<RawRouteTreeNode[]> {
27
+ let routeModule = await import(pathToFileURL(routesFile).href)
28
+
29
+ if (!('routes' in routeModule)) {
30
+ throw new Error(`Route module ${routesFile} must export a named "routes" value.`)
31
+ }
32
+
33
+ return normalizeRouteGroup(Reflect.get(routeModule, 'routes'), [], new Map())
34
+ }
35
+
36
+ function normalizeRouteGroup(
37
+ value: unknown,
38
+ parentSegments: string[],
39
+ seen: Map<object, string>,
40
+ ): RawRouteTreeNode[] {
41
+ if (!isPlainObject(value)) {
42
+ let location = parentSegments.length === 0 ? 'routes' : parentSegments.join('.')
43
+ throw new Error(`Invalid route map value at "${location}". Expected a nested route object.`)
44
+ }
45
+
46
+ let existingPath = seen.get(value)
47
+ let currentPath = parentSegments.length === 0 ? 'routes' : parentSegments.join('.')
48
+ if (existingPath != null) {
49
+ throw new Error(`Detected a route map cycle at "${currentPath}" via "${existingPath}".`)
50
+ }
51
+
52
+ seen.set(value, currentPath)
53
+
54
+ try {
55
+ return Object.entries(value).map(([key, entryValue]) => {
56
+ let nameSegments = [...parentSegments, key]
57
+ let name = nameSegments.join('.')
58
+
59
+ if (isRouteLeaf(entryValue)) {
60
+ return {
61
+ children: [],
62
+ key,
63
+ kind: 'route',
64
+ method: entryValue.method,
65
+ name,
66
+ pattern: entryValue.pattern.source,
67
+ } satisfies RawRouteTreeNode
68
+ }
69
+
70
+ if (isPlainObject(entryValue)) {
71
+ return {
72
+ children: normalizeRouteGroup(entryValue, nameSegments, seen),
73
+ key,
74
+ kind: 'group',
75
+ name,
76
+ } satisfies RawRouteTreeNode
77
+ }
78
+
79
+ throw new Error(
80
+ `Invalid route map value at "${name}". Expected a route or nested route object.`,
81
+ )
82
+ })
83
+ } finally {
84
+ seen.delete(value)
85
+ }
86
+ }
87
+
88
+ function isRouteLeaf(value: unknown): value is { method: string; pattern: { source: string } } {
89
+ return (
90
+ typeof value === 'object' &&
91
+ 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'
96
+ )
97
+ }
98
+
99
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
100
+ if (typeof value !== 'object' || value == null) {
101
+ return false
102
+ }
103
+
104
+ let prototype = Object.getPrototypeOf(value)
105
+ return prototype === Object.prototype || prototype === null
106
+ }
107
+
108
+ function setExitCode(exitCode: number) {
109
+ globalThis.process.exitCode = exitCode
110
+ }
@@ -0,0 +1,82 @@
1
+ import { missingOptionValue, unexpectedExtraArgument, unknownArgument } from './errors.ts'
2
+
3
+ export interface ParseArgsBooleanOptionSpec {
4
+ flag: string
5
+ type: 'boolean'
6
+ }
7
+
8
+ export interface ParseArgsStringOptionSpec {
9
+ flag: string
10
+ type: 'string'
11
+ }
12
+
13
+ export type ParseArgsOptionSpec = ParseArgsBooleanOptionSpec | ParseArgsStringOptionSpec
14
+
15
+ export type ParseArgsOptionDefinitions = Record<string, ParseArgsOptionSpec>
16
+
17
+ export interface ParseArgsOptions {
18
+ maxPositionals?: number
19
+ }
20
+
21
+ export type ParsedArgsValues<definitions extends ParseArgsOptionDefinitions> = {
22
+ [key in keyof definitions]: definitions[key]['type'] extends 'boolean'
23
+ ? boolean
24
+ : string | undefined
25
+ }
26
+
27
+ export interface ParsedArgsResult<definitions extends ParseArgsOptionDefinitions> {
28
+ options: ParsedArgsValues<definitions>
29
+ positionals: string[]
30
+ }
31
+
32
+ export function parseArgs<const definitions extends ParseArgsOptionDefinitions>(
33
+ argv: string[],
34
+ definitions: definitions,
35
+ options: ParseArgsOptions = {},
36
+ ): ParsedArgsResult<definitions> {
37
+ let values = {} as ParsedArgsValues<definitions>
38
+ let specsByFlag = new Map<string, { key: keyof definitions; spec: ParseArgsOptionSpec }>()
39
+ let positionals: string[] = []
40
+
41
+ for (let [key, spec] of Object.entries(definitions) as Array<
42
+ [keyof definitions, ParseArgsOptionSpec]
43
+ >) {
44
+ values[key] = (
45
+ spec.type === 'boolean' ? false : undefined
46
+ ) as ParsedArgsValues<definitions>[typeof key]
47
+ specsByFlag.set(spec.flag, { key, spec })
48
+ }
49
+
50
+ for (let index = 0; index < argv.length; index++) {
51
+ let arg = argv[index]!
52
+ let resolved = specsByFlag.get(arg)
53
+
54
+ if (resolved != null) {
55
+ if (resolved.spec.type === 'boolean') {
56
+ values[resolved.key] = true as ParsedArgsValues<definitions>[typeof resolved.key]
57
+ continue
58
+ }
59
+
60
+ let next = argv[index + 1]
61
+ if (next == null || next.startsWith('-')) {
62
+ throw missingOptionValue(arg)
63
+ }
64
+
65
+ values[resolved.key] = next as ParsedArgsValues<definitions>[typeof resolved.key]
66
+ index += 1
67
+ continue
68
+ }
69
+
70
+ if (arg.startsWith('-')) {
71
+ throw unknownArgument(arg)
72
+ }
73
+
74
+ if (options.maxPositionals != null && positionals.length >= options.maxPositionals) {
75
+ throw unexpectedExtraArgument(arg)
76
+ }
77
+
78
+ positionals.push(arg)
79
+ }
80
+
81
+ return { options: values, positionals }
82
+ }