@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,418 @@
1
+ export interface CompletionResult {
2
+ mode: 'files' | 'none' | 'values'
3
+ values?: string[]
4
+ }
5
+
6
+ const COMPLETION_SHELLS = ['bash', 'zsh'] as const
7
+ const HELP_COMMANDS = ['completion', 'doctor', 'help', 'new', 'routes', 'test', 'version'] as const
8
+ const ROOT_COMMANDS = ['completion', 'doctor', 'help', 'new', 'routes', 'test', 'version'] as const
9
+
10
+ export type CompletionShell = (typeof COMPLETION_SHELLS)[number]
11
+
12
+ export function isCompletionShell(value: string): value is CompletionShell {
13
+ return COMPLETION_SHELLS.includes(value as CompletionShell)
14
+ }
15
+
16
+ export function getCompletionResult(words: string[], currentIndex: number): CompletionResult {
17
+ let resolvedIndex = Math.max(0, currentIndex)
18
+ let currentWord = words[resolvedIndex] ?? ''
19
+ let tokens = getTokensBeforeCursor(words, resolvedIndex)
20
+
21
+ return completeTopLevel(tokens, currentWord)
22
+ }
23
+
24
+ export function renderCompletionResult(result: CompletionResult): string {
25
+ let lines = [`mode:${result.mode}`]
26
+
27
+ if (result.mode === 'values') {
28
+ lines.push(...(result.values ?? []))
29
+ }
30
+
31
+ return `${lines.join('\n')}\n`
32
+ }
33
+
34
+ export function getCompletionScript(): string {
35
+ return `###-begin-remix-completion-###
36
+ #
37
+ # Remix command completion script
38
+ #
39
+ # Installation:
40
+ # remix completion bash >> ~/.bashrc
41
+ # remix completion zsh >> ~/.zshrc
42
+ #
43
+
44
+ if type complete &>/dev/null; then
45
+ _remix_completion() {
46
+ local words cword current output mode si
47
+ local lines
48
+
49
+ if type _get_comp_words_by_ref &>/dev/null; then
50
+ _get_comp_words_by_ref -w words -i cword
51
+ else
52
+ cword="$COMP_CWORD"
53
+ words=("\${COMP_WORDS[@]}")
54
+ fi
55
+
56
+ current="\${words[cword]}"
57
+ output=$(remix completion -- "$cword" "\${words[@]}" 2>/dev/null) || return 0
58
+ si="$IFS"
59
+ IFS=$'\\n' lines=($output)
60
+ IFS="$si"
61
+ mode="\${lines[0]}"
62
+
63
+ if [[ "$mode" == "mode:values" ]]; then
64
+ COMPREPLY=("\${lines[@]:1}")
65
+ return 0
66
+ fi
67
+
68
+ if [[ "$mode" == "mode:files" ]]; then
69
+ COMPREPLY=($(compgen -f -- "$current"))
70
+ return 0
71
+ fi
72
+
73
+ COMPREPLY=()
74
+ }
75
+
76
+ complete -o default -F _remix_completion remix
77
+ elif type compdef &>/dev/null; then
78
+ _remix_completion() {
79
+ local output mode
80
+ local -a lines values
81
+
82
+ output=$(remix completion -- "$((CURRENT - 1))" "\${words[@]}" 2>/dev/null) || return 0
83
+ lines=("\${(@f)output}")
84
+ mode="\${lines[1]}"
85
+
86
+ if [[ "$mode" == "mode:values" ]]; then
87
+ values=("\${(@)lines[2,-1]}")
88
+ if (( \${#values[@]} > 0 )); then
89
+ compadd -- "\${values[@]}"
90
+ fi
91
+ return 0
92
+ fi
93
+
94
+ if [[ "$mode" == "mode:files" ]]; then
95
+ if autoload -U +X _files 2>/dev/null; then
96
+ _files
97
+ else
98
+ compadd -f
99
+ fi
100
+ return 0
101
+ fi
102
+
103
+ return 0
104
+ }
105
+
106
+ compdef _remix_completion remix
107
+ fi
108
+ ###-end-remix-completion-###
109
+ `
110
+ }
111
+
112
+ function completeTopLevel(tokens: string[], currentWord: string): CompletionResult {
113
+ let usedFlags = new Set<string>()
114
+ let index = 0
115
+
116
+ while (index < tokens.length) {
117
+ let token = tokens[index]
118
+
119
+ if (token === '--no-color') {
120
+ usedFlags.add('--no-color')
121
+ index++
122
+ continue
123
+ }
124
+
125
+ if (token === '-h' || token === '--help') {
126
+ usedFlags.add('-h')
127
+ return completeValues([], currentWord)
128
+ }
129
+
130
+ if (token === '-v' || token === '--version') {
131
+ usedFlags.add('-v')
132
+ return completeValues([], currentWord)
133
+ }
134
+
135
+ return completeCommand(token, tokens.slice(index + 1), currentWord, usedFlags)
136
+ }
137
+
138
+ return completeValues(getTopLevelSuggestions(currentWord, usedFlags), currentWord)
139
+ }
140
+
141
+ function completeCommand(
142
+ command: string,
143
+ tokens: string[],
144
+ currentWord: string,
145
+ usedGlobalFlags: Set<string>,
146
+ ): CompletionResult {
147
+ if (command === 'help') {
148
+ return completeHelp(tokens, currentWord, usedGlobalFlags)
149
+ }
150
+
151
+ if (command === 'new') {
152
+ return completeNew(tokens, currentWord, usedGlobalFlags)
153
+ }
154
+
155
+ if (command === 'doctor') {
156
+ return completeSimpleFlags(tokens, currentWord, usedGlobalFlags, [
157
+ '--fix',
158
+ '--json',
159
+ '--strict',
160
+ ])
161
+ }
162
+
163
+ if (command === 'routes') {
164
+ return completeRoutes(tokens, currentWord, usedGlobalFlags)
165
+ }
166
+
167
+ if (command === 'version') {
168
+ return completeSimpleFlags(tokens, currentWord, usedGlobalFlags, [])
169
+ }
170
+
171
+ if (command === 'test') {
172
+ return completeSimpleFlags(tokens, currentWord, usedGlobalFlags, [
173
+ '--coverage',
174
+ '--watch',
175
+ '--help',
176
+ ])
177
+ }
178
+
179
+ if (command === 'completion') {
180
+ return completeCompletionCommand(tokens, currentWord, usedGlobalFlags)
181
+ }
182
+
183
+ return completeValues([], currentWord)
184
+ }
185
+
186
+ function completeHelp(
187
+ tokens: string[],
188
+ currentWord: string,
189
+ usedGlobalFlags: Set<string>,
190
+ ): CompletionResult {
191
+ let filteredTokens = filterGlobalCommandTokens(tokens, usedGlobalFlags)
192
+ if (filteredTokens == null) {
193
+ return completeValues([], currentWord)
194
+ }
195
+
196
+ if (filteredTokens.length === 0) {
197
+ return completeValues(withHelpFlags([...HELP_COMMANDS], usedGlobalFlags), currentWord)
198
+ }
199
+
200
+ let [, ...rest] = filteredTokens
201
+ if (rest.length === 0) {
202
+ return completeValues([], currentWord)
203
+ }
204
+
205
+ return completeValues([], currentWord)
206
+ }
207
+
208
+ function completeNew(
209
+ tokens: string[],
210
+ currentWord: string,
211
+ usedGlobalFlags: Set<string>,
212
+ ): CompletionResult {
213
+ let filteredTokens = filterGlobalCommandTokens(tokens, usedGlobalFlags)
214
+ if (filteredTokens == null) {
215
+ return completeValues([], currentWord)
216
+ }
217
+
218
+ let hasAppName = false
219
+ let hasForce = false
220
+ let hasTargetDir = false
221
+ let expectsAppName = false
222
+
223
+ for (let token of filteredTokens) {
224
+ if (expectsAppName) {
225
+ expectsAppName = false
226
+ continue
227
+ }
228
+
229
+ if (token === '--app-name') {
230
+ hasAppName = true
231
+ expectsAppName = true
232
+ continue
233
+ }
234
+
235
+ if (token === '--force') {
236
+ hasForce = true
237
+ continue
238
+ }
239
+
240
+ if (token.startsWith('-')) {
241
+ return completeValues([], currentWord)
242
+ }
243
+
244
+ if (!hasTargetDir) {
245
+ hasTargetDir = true
246
+ continue
247
+ }
248
+
249
+ return completeValues([], currentWord)
250
+ }
251
+
252
+ if (expectsAppName) {
253
+ return { mode: 'none' }
254
+ }
255
+
256
+ let flags = withHelpFlags(
257
+ [...(!hasAppName ? ['--app-name'] : []), ...(!hasForce ? ['--force'] : [])],
258
+ usedGlobalFlags,
259
+ )
260
+
261
+ if (!hasTargetDir && !currentWord.startsWith('-')) {
262
+ return { mode: 'files' }
263
+ }
264
+
265
+ return completeValues(flags, currentWord)
266
+ }
267
+
268
+ function completeRoutes(
269
+ tokens: string[],
270
+ currentWord: string,
271
+ usedGlobalFlags: Set<string>,
272
+ ): CompletionResult {
273
+ let filteredTokens = filterGlobalCommandTokens(tokens, usedGlobalFlags)
274
+ if (filteredTokens == null) {
275
+ return completeValues([], currentWord)
276
+ }
277
+
278
+ let hasJson = false
279
+ let hasNoHeaders = false
280
+ let hasTable = false
281
+ let hasVerbose = false
282
+
283
+ for (let token of filteredTokens) {
284
+ if (token === '--json') {
285
+ hasJson = true
286
+ continue
287
+ }
288
+
289
+ if (token === '--table') {
290
+ hasTable = true
291
+ continue
292
+ }
293
+
294
+ if (token === '--no-headers') {
295
+ hasNoHeaders = true
296
+ continue
297
+ }
298
+
299
+ if (token === '--verbose') {
300
+ hasVerbose = true
301
+ continue
302
+ }
303
+
304
+ return completeValues([], currentWord)
305
+ }
306
+
307
+ let flags = withHelpFlags(
308
+ [
309
+ ...(!hasJson && !hasTable && !hasVerbose ? ['--json'] : []),
310
+ ...(!hasTable && !hasJson ? ['--table'] : []),
311
+ ...(!hasNoHeaders && hasTable ? ['--no-headers'] : []),
312
+ ...(!hasVerbose && !hasJson ? ['--verbose'] : []),
313
+ ],
314
+ usedGlobalFlags,
315
+ )
316
+
317
+ return completeValues(flags, currentWord)
318
+ }
319
+
320
+ function completeCompletionCommand(
321
+ tokens: string[],
322
+ currentWord: string,
323
+ usedGlobalFlags: Set<string>,
324
+ ): CompletionResult {
325
+ let filteredTokens = filterGlobalCommandTokens(tokens, usedGlobalFlags)
326
+ if (filteredTokens == null) {
327
+ return completeValues([], currentWord)
328
+ }
329
+
330
+ if (filteredTokens.length === 0) {
331
+ return completeValues(withHelpFlags([...COMPLETION_SHELLS], usedGlobalFlags), currentWord)
332
+ }
333
+
334
+ return completeValues([], currentWord)
335
+ }
336
+
337
+ function completeSimpleFlags(
338
+ tokens: string[],
339
+ currentWord: string,
340
+ usedGlobalFlags: Set<string>,
341
+ commandFlags: string[],
342
+ ): CompletionResult {
343
+ let filteredTokens = filterGlobalCommandTokens(tokens, usedGlobalFlags)
344
+ if (filteredTokens == null) {
345
+ return completeValues([], currentWord)
346
+ }
347
+
348
+ let usedFlags = new Set<string>()
349
+ for (let token of filteredTokens) {
350
+ if (!commandFlags.includes(token)) {
351
+ return completeValues([], currentWord)
352
+ }
353
+
354
+ usedFlags.add(token)
355
+ }
356
+
357
+ let availableFlags = commandFlags.filter((flag) => !usedFlags.has(flag))
358
+ return completeValues(withHelpFlags(availableFlags, usedGlobalFlags), currentWord)
359
+ }
360
+
361
+ function getTopLevelSuggestions(currentWord: string, usedFlags: Set<string>): string[] {
362
+ let flags = [
363
+ ...(!usedFlags.has('-h') ? ['-h', '--help'] : []),
364
+ ...(!usedFlags.has('--no-color') ? ['--no-color'] : []),
365
+ ...(!usedFlags.has('-v') ? ['-v', '--version'] : []),
366
+ ]
367
+
368
+ if (currentWord.startsWith('-')) {
369
+ return flags
370
+ }
371
+
372
+ return [...ROOT_COMMANDS, ...flags]
373
+ }
374
+
375
+ function withHelpFlags(values: string[], usedGlobalFlags: Set<string>): string[] {
376
+ return [
377
+ ...values,
378
+ ...(!usedGlobalFlags.has('-h') ? ['-h', '--help'] : []),
379
+ ...(!usedGlobalFlags.has('--no-color') ? ['--no-color'] : []),
380
+ ]
381
+ }
382
+
383
+ function filterGlobalCommandTokens(
384
+ tokens: string[],
385
+ usedGlobalFlags: Set<string>,
386
+ ): string[] | null {
387
+ let filtered: string[] = []
388
+
389
+ for (let token of tokens) {
390
+ if (token === '--no-color') {
391
+ usedGlobalFlags.add('--no-color')
392
+ continue
393
+ }
394
+
395
+ if (token === '-h' || token === '--help') {
396
+ usedGlobalFlags.add('-h')
397
+ return null
398
+ }
399
+
400
+ filtered.push(token)
401
+ }
402
+
403
+ return filtered
404
+ }
405
+
406
+ function completeValues(values: string[], currentWord: string): CompletionResult {
407
+ return {
408
+ mode: 'values',
409
+ values: values.filter((value) => value.startsWith(currentWord)),
410
+ }
411
+ }
412
+
413
+ function getTokensBeforeCursor(words: string[], currentIndex: number): string[] {
414
+ let startIndex = words[0] === 'remix' ? 1 : 0
415
+ let endIndex = Math.max(startIndex, Math.min(currentIndex, words.length))
416
+
417
+ return words.slice(startIndex, endIndex)
418
+ }
@@ -0,0 +1,13 @@
1
+ import * as path from 'node:path'
2
+
3
+ export function resolveContainedPath(rootDir: string, relativePath: string): string {
4
+ let resolvedRootDir = path.resolve(rootDir)
5
+ let resolvedPath = path.resolve(resolvedRootDir, relativePath)
6
+ let pathFromRoot = path.relative(resolvedRootDir, resolvedPath)
7
+
8
+ if (pathFromRoot === '' || (!pathFromRoot.startsWith('..') && !path.isAbsolute(pathFromRoot))) {
9
+ return resolvedPath
10
+ }
11
+
12
+ throw new Error(`Resolved path escapes the allowed root: ${relativePath}`)
13
+ }
@@ -0,0 +1,79 @@
1
+ import * as path from 'node:path'
2
+
3
+ const OWNER_FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'] as const
4
+ export type OwnerFileExtension = (typeof OWNER_FILE_EXTENSIONS)[number]
5
+
6
+ export function getActionOwnerCandidates(segments: string[]): string[] {
7
+ return OWNER_FILE_EXTENSIONS.map((extension) =>
8
+ normalizeRelativePath(path.join('app', 'controllers', `${path.join(...segments)}${extension}`)),
9
+ )
10
+ }
11
+
12
+ export function getControllerOwnerCandidates(segments: string[]): string[] {
13
+ return OWNER_FILE_EXTENSIONS.map((extension) =>
14
+ normalizeRelativePath(path.join('app', 'controllers', ...segments, `controller${extension}`)),
15
+ )
16
+ }
17
+
18
+ export function getRouteSubtreePath(segments: string[]): string {
19
+ return normalizeRelativePath(path.join('app', 'controllers', ...segments))
20
+ }
21
+
22
+ export function getPreferredOwnerDisplayPath(candidates: string[]): string {
23
+ let tsxCandidate = candidates.find((candidate) => candidate.endsWith('.tsx'))
24
+ return tsxCandidate ?? candidates[0] ?? ''
25
+ }
26
+
27
+ export function getOwnerCandidateForExtension(
28
+ candidates: string[],
29
+ extension: OwnerFileExtension,
30
+ ): string | null {
31
+ return candidates.find((candidate) => candidate.endsWith(extension)) ?? null
32
+ }
33
+
34
+ export function getOwnerFileExtension(filePath: string): OwnerFileExtension | null {
35
+ return OWNER_FILE_EXTENSIONS.find((extension) => filePath.endsWith(extension)) ?? null
36
+ }
37
+
38
+ export function toDiskSegment(segment: string): string {
39
+ let diskSegment = segment
40
+ .replace(/[\\/]+/g, '-')
41
+ .replace(/\.+/g, '-')
42
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
43
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
44
+ .toLowerCase()
45
+ .replace(/[^a-z0-9]+/g, '-')
46
+ .replace(/-+/g, '-')
47
+ .replace(/^-+|-+$/g, '')
48
+
49
+ return diskSegment.length === 0 ? 'route' : diskSegment
50
+ }
51
+
52
+ export function isControllerEntryFileName(fileName: string): boolean {
53
+ return OWNER_FILE_EXTENSIONS.some((extension) => fileName === `controller${extension}`)
54
+ }
55
+
56
+ export function getOwnerModuleBaseName(fileName: string): string | null {
57
+ let extension = OWNER_FILE_EXTENSIONS.find((ownerExtension) => fileName.endsWith(ownerExtension))
58
+
59
+ if (extension == null) {
60
+ return null
61
+ }
62
+
63
+ return fileName.slice(0, -extension.length)
64
+ }
65
+
66
+ export function isActionFileName(fileName: string): boolean {
67
+ let baseName = getOwnerModuleBaseName(fileName)
68
+
69
+ return (
70
+ baseName != null &&
71
+ baseName !== 'controller' &&
72
+ !baseName.endsWith('.test') &&
73
+ !baseName.endsWith('.spec')
74
+ )
75
+ }
76
+
77
+ function normalizeRelativePath(filePath: string): string {
78
+ return filePath.split(path.sep).join('/')
79
+ }